【.NET】HttpClient と接続プールの振る舞い

ちょっと長くなってしまったので、先に結論。

`HttpClient` は Dispose されると接続プールも破棄されます。
なので `HttpClient` インスタンスは可能な限り使い回して、接続プールの恩恵を最大化しましょう。
主要メソッドはスレッドセーフなので、マルチスレッドでも安心して使えます。（公式リファレンスの受け売り感）

```csharp:staticフィールドに保持する例
// https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1#Remarks
public class GoodController : ApiController {
    private static readonly HttpClient _httpClient;

    static GoodController() {
        _httpClient = new HttpClient();
    }
}
```
※ [static 化による DNS 問題](#%E4%BD%99%E8%AB%87-httpclient-%E3%82%92%E7%A0%B4%E6%A3%84%E3%81%97%E3%81%AA%E3%81%84%E3%81%A8-dns-%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%8C%E5%8F%8D%E6%98%A0%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E4%BB%B6)は下記。

---

本稿では [`HttpWebRequest`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httpwebrequest?view=netframework-4.7.1) / [`HttpClient`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1) の接続プールに関する特性と、それに沿った `HttpClient` の効果的な使用法を示していきます。

## HttpWebRequest の特性
- `HttpWebRequest` は内部で [`ServicePoint`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httpwebrequest.servicepoint?view=netframework-4.7.1#System_Net_HttpWebRequest_ServicePoint) の接続グループによる接続プーリングが行われている。

> So as soon as your application invoke the GetResponse() method there are number things happened. Following is the sequence of operations took place.

> - a)      System.Net see it is a first request, it creates a connection group for it,
- b)      Since this is new connection group, it also creates a servicepoint on this group (for the sack of simplicity, assume that there is no authentication involved and servicepoint is on per host basis),
- c)      It creates a new connection and open a new socket connection to remote host,
>
...
Second request would be send on the same connection as previous request, no new connection is created.
>
*[Understanding System.Net Connection Management and ServicepointManager &#8211; Adarsh&#039;s blog](https://blogs.msdn.microsoft.com/adarshk/2005/01/02/understanding-system-net-connection-management-and-servicepointmanager/)*

- 接続のライフサイクルは下記によって制御されている。
    - [`ServicePoint.CloseConnectionGroup(...)`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.servicepoint.closeconnectiongroup?view=netframework-4.7.1#System_Net_ServicePoint_CloseConnectionGroup_System_String_)
        - 指定された接続グループの接続を明示的にクローズする。
    - [`ServicePoint.MaxIdleTime`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.servicepoint.maxidletime?view=netframework-4.7.1#System_Net_ServicePoint_MaxIdleTime)
        - `ServicePoint` の最大アイドル時間。これを過ぎると `ServicePoint` と管理下の接続が破棄される。
        - 既定値は [`ServicePointManager.MaxServicePointIdleTime`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.servicepointmanager.maxservicepointidletime?view=netframework-4.7.1) （これの既定値は 100,000 ms）。
    - [`ServicePoint.ConnectionLeaseTimeout`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.servicepoint.connectionleasetimeout?view=netframework-4.7.1)
        - 接続を維持する時間。
        - 既定値は -1（無制限）。
    - [`HttpWebRequest.Connection`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httpwebrequest.connection?view=netframework-4.7.1) 又は [`HttpWebRequest.KeepAlive`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httpwebrequest.keepalive?view=netframework-4.7.1)
        - HTTP ヘッダーに `Connection: Close` をつける事で接続が破棄される。


## HttpClient の特性
- `HttpClient` はインスタンス毎に独立した接続プーリングを行っている。
> In addition, every HttpClient instance uses its own connection pool, isolating its requests from requests executed by other HttpClient instances.
>
*[HttpClient Class (System.Net.Http) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1)*

    - `HttpWebRequest` でいうところの接続グループに近い。
- 接続のライフサイクルは下記によって制御されている。
    - `HttpClient.Dispose()`
        - `HttpClient` にプールされている接続を明示的にクローズする。
    - [`HttpRequestHeaders.ConnectionClose`](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.headers.httprequestheaders.connectionclose?view=netframework-4.7.1)
        - HTTP ヘッダーに `Connection: Close` をつける事で接続が破棄される。
    - [バックエンドに HttpWebRequest を使用している実装](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1#Remarks)では、上記「HttpWebRequest の特性」の項に記載したような、`ServicePoint` のライフサイクルの影響を受ける。
- `HttpClient` の主要メソッドはスレッドセーフである。
> The following methods are thread safe:
> 1. CancelPendingRequests
> 2. DeleteAsync
> 3. GetAsync
> 4. GetByteArrayAsync
> 5. GetStreamAsync
> 6. GetStringAsync
> 7. PostAsync
> 8. PutAsync
> 9. SendAsync
> 
*[HttpClient Class (System.Net.Http) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1)*


## HttpClient のアンチパターンと良いパターン

### × IDisposable な HttpClient の良くある使用法
```csharp
public class GoodController : ApiController {
    public string GetHello() {
        ...
        using (var client = new HttpClient()) {
            var res = await client.GetAsync(...);
            ...
        }
    }
}
```
リクエストの度に `HttpClient` を生成/破棄してしまうと、TIME_WAIT な接続だらけになってしまい、ソケットを使い潰してしまうので NG。

> In a web application, this technique is not scalable. A new HttpClient object is created for each user request. Under heavy load, the web server may exhaust the number of available sockets, resulting in SocketException errors.
>
*[Improper Instantiation antipattern | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/architecture/antipatterns/improper-instantiation)*
*[architecture-center/index.md at master · mspnp/architecture-center](https://github.com/mspnp/architecture-center/blob/master/docs/antipatterns/improper-instantiation/index.md)*


### ○ HttpClient の効果的な使い方
```csharp
public class GoodController : ApiController {
    private static readonly HttpClient _httpClient;

    static GoodController() {
        _httpClient = new HttpClient();
    }

    public string GetHello() {
        ...
        var res = await _httpClient.GetAsync(...);
        ...
    }
}
```
`HttpClient` はシングルトンにしてアプリケーションとライフサイクルを合わせる事で、コネクションの再利用を最大化する事が基本となります。
これによって、接続プールのライフサイクルを `HttpWebRequest` の頃と（ほぼ）同様にする事ができます。
> Note
HttpClient is intended to be instantiated once and re-used throughout the life of an application. Especially in server applications, creating a new HttpClient instance for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors.
>
*[Calling a Web API From a .NET Client (C#) | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client#create-and-initialize-httpclient)*

ただし、セキュリティ要件等によっては、[接続グループを分ける](https://docs.microsoft.com/ja-jp/dotnet/framework/network-programming/connection-grouping)ように `HttpClient` インスタンスを複数運用する事が望ましいケースもあります。

ロードバランサーを経由する接続では、最適な接続先（ホスト）が時間と共に変わる可能性がありますので、定期的な再接続が必要になるかも知れません。

> However some services might actually ask you to close the connection after each request. In particular if your request goes through a load balancer by keeping the connection alive your requests will not go through the load balancer so you might end up getting throttled.
>
*[Best practices for using HttpClient on Services &#8211; Sharad Cornejo Altuzar&#039;s blog](https://blogs.msdn.microsoft.com/shacorn/2016/10/21/best-practices-for-using-httpclient-on-services/)*

**重要な事**は、シングルトンか否かではなく、**貴重なリソース（ソケット）を無為に使い潰さないよう配慮**する事でしょう。



## 余談: HttpClient を破棄しないと DNS の変更が反映されない件
さて、いざ再利用をしてみると問題があるそうです。
>> しかし、深刻な問題があるのがわかりました。DNSの変更が反映されず、HttpClientは(HttpClientHandlerを通じて)ソケットがクローズするまでコネクションを使い続けます。無制限にです。
>
*[開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント - InfoQ](https://www.infoq.com/jp/news/2016/09/HttpClient)*

原文では続けて下記のように分析しています。

> HttpClientHandler creates a connection group (named with its hashcode) and does not close the connections in the group until getting disposed. This basically means the DNS check never happens as long as a connection is open. 
>
*[Byte Rot: Singleton HttpClient? Beware of this serious behaviour and how to fix it](http://byterot.blogspot.jp/2016/07/singleton-httpclient-dns.html)*

`HttpClient` のライフサイクル中は接続がプールされている為、接続が破棄されない限り DNS の変更が反映されないのはその通りだと思います。

ただ `HttpWebRequest` の頃も同様に、`MaxIdleTime` や `ConnectionLeaseTime` によるタイムアウト、 または `CloseConnectionGroup()` をしない限り接続はプールされ続けますので、この件は `HttpClient` とは無関係に存在する課題です。

言い換えれば、氏の [`ConnectionLeaseTime` による解決策](http://byterot.blogspot.jp/2016/07/singleton-httpclient-dns.html#Conclusion_61)は、レガシーな `HttpWebRequest` を使用したコードにも通用する方法といえるでしょう。

### 上手くいかないケースも
バックエンドで `ServicePoint` を[使用していない実装](https://github.com/dotnet/corefx/issues/11224)ではこの解決策が通用しません。

そもそも管理下にあるオブジェクト（`HttpClient`）が裏側で与り知らぬオブジェクト（`ServicePoint` / `ServicePointManager`）に依存している **かも知れない** 事を正しく認識しなくてはならない状況は、`System.Web` の呪縛[^1]と同様に辛いものがあります。

[^1]: 超ビッグなインスタンス `System.Web.HttpContext.Current` がそこかしこから参照されている状況。依存関係が見え辛くなり、次第にコードがスパゲッティ化していく。

個人的には `HttpClient` インスタンスの破棄と再作成で対応する方がシンプルで良いと思います。


## See Also
- [HttpClient Class (System.Net.Http) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.7.1)
- [開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント - InfoQ](https://www.infoq.com/jp/news/2016/09/HttpClient)
- [Calling a Web API From a .NET Client (C#) | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client#create-and-initialize-httpclient)
- [HttpClient, HttpClientHandler, and WebRequestHandler Explained &#8211; Henrik&#039;s Blog](https://blogs.msdn.microsoft.com/henrikn/2012/08/07/httpclient-httpclienthandler-and-webrequesthandler-explained/)
- [Best practices for using HttpClient on Services &#8211; Sharad Cornejo Altuzar&#039;s blog](https://blogs.msdn.microsoft.com/shacorn/2016/10/21/best-practices-for-using-httpclient-on-services/)
- [Improper Instantiation antipattern | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/architecture/antipatterns/improper-instantiation)
- [Byte Rot: Singleton HttpClient? Beware of this serious behaviour and how to fix it](http://byterot.blogspot.jp/2016/07/singleton-httpclient-dns.html)
- [.NET アプリケーションのパフォーマンスとスケーラビリティの向上 - 第 10 章 「Web サービス パフォーマンスの向上」](https://msdn.microsoft.com/ja-jp/library/ff647786.aspx)