ちょっと長くなってしまったので、先に結論。
HttpClient
は Dispose されると接続プールも破棄されます。
なので HttpClient
インスタンスは可能な限り使い回して、接続プールの恩恵を最大化しましょう。
主要メソッドはスレッドセーフなので、マルチスレッドでも安心して使えます。(公式リファレンスの受け売り感)
// 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 問題は下記。
本稿では HttpWebRequest
/ HttpClient
の接続プールに関する特性と、それに沿った HttpClient
の効果的な使用法を示していきます。
HttpWebRequest の特性
-
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 – Adarsh's blog
- 接続のライフサイクルは下記によって制御されている。
-
ServicePoint.CloseConnectionGroup(...)
- 指定された接続グループの接続を明示的にクローズする。
-
ServicePoint.MaxIdleTime
-
ServicePoint
の最大アイドル時間。これを過ぎるとServicePoint
と管理下の接続が破棄される。 - 既定値は
ServicePointManager.MaxServicePointIdleTime
(これの既定値は 100,000 ms)。
-
-
ServicePoint.ConnectionLeaseTimeout
- 接続を維持する時間。
- 既定値は -1(無制限)。
-
HttpWebRequest.Connection
又はHttpWebRequest.KeepAlive
- HTTP ヘッダーに
Connection: Close
をつける事で接続が破棄される。
- HTTP ヘッダーに
-
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
- `HttpWebRequest` でいうところの接続グループに近い。
- 接続のライフサイクルは下記によって制御されている。
-
HttpClient.Dispose()
-
HttpClient
にプールされている接続を明示的にクローズする。
-
-
HttpRequestHeaders.ConnectionClose
- HTTP ヘッダーに
Connection: Close
をつける事で接続が破棄される。
- HTTP ヘッダーに
-
バックエンドに HttpWebRequest を使用している実装では、上記「HttpWebRequest の特性」の項に記載したような、
ServicePoint
のライフサイクルの影響を受ける。
-
-
HttpClient
の主要メソッドはスレッドセーフである。
The following methods are thread safe:
- CancelPendingRequests
- DeleteAsync
- GetAsync
- GetByteArrayAsync
- GetStreamAsync
- GetStringAsync
- PostAsync
- PutAsync
- SendAsync
HttpClient Class (System.Net.Http) | Microsoft Docs
HttpClient のアンチパターンと良いパターン
× IDisposable な HttpClient の良くある使用法
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
architecture-center/index.md at master · mspnp/architecture-center
○ HttpClient の効果的な使い方
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
ただし、セキュリティ要件等によっては、接続グループを分けるように 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 – Sharad Cornejo Altuzar's blog
重要な事は、シングルトンか否かではなく、貴重なリソース(ソケット)を無為に使い潰さないよう配慮する事でしょう。
余談: HttpClient を破棄しないと DNS の変更が反映されない件
さて、いざ再利用をしてみると問題があるそうです。
しかし、深刻な問題があるのがわかりました。DNSの変更が反映されず、HttpClientは(HttpClientHandlerを通じて)ソケットがクローズするまでコネクションを使い続けます。無制限にです。
開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント - InfoQ
原文では続けて下記のように分析しています。
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
HttpClient
のライフサイクル中は接続がプールされている為、接続が破棄されない限り DNS の変更が反映されないのはその通りだと思います。
ただ HttpWebRequest
の頃も同様に、MaxIdleTime
や ConnectionLeaseTime
によるタイムアウト、 または CloseConnectionGroup()
をしない限り接続はプールされ続けますので、この件は HttpClient
とは無関係に存在する課題です。
言い換えれば、氏の ConnectionLeaseTime
による解決策は、レガシーな HttpWebRequest
を使用したコードにも通用する方法といえるでしょう。
上手くいかないケースも
バックエンドで ServicePoint
を使用していない実装ではこの解決策が通用しません。
そもそも管理下にあるオブジェクト(HttpClient
)が裏側で与り知らぬオブジェクト(ServicePoint
/ ServicePointManager
)に依存している かも知れない 事を正しく認識しなくてはならない状況は、System.Web
の呪縛1と同様に辛いものがあります。
個人的には HttpClient
インスタンスの破棄と再作成で対応する方がシンプルで良いと思います。
See Also
- HttpClient Class (System.Net.Http) | Microsoft Docs
- 開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント - InfoQ
- Calling a Web API From a .NET Client (C#) | Microsoft Docs
- HttpClient, HttpClientHandler, and WebRequestHandler Explained – Henrik's Blog
- Best practices for using HttpClient on Services – Sharad Cornejo Altuzar's blog
- Improper Instantiation antipattern | Microsoft Docs
- Byte Rot: Singleton HttpClient? Beware of this serious behaviour and how to fix it
- .NET アプリケーションのパフォーマンスとスケーラビリティの向上 - 第 10 章 「Web サービス パフォーマンスの向上」
-
超ビッグなインスタンス
System.Web.HttpContext.Current
がそこかしこから参照されている状況。依存関係が見え辛くなり、次第にコードがスパゲッティ化していく。 ↩