- Purpose
- 背景
- A)アンチパターン
- B)HttpClientをSingleTonで追加したパターン
- C)HttpClientをservices.AddHttpClient()で追加したパターン
- D)IHttpClientFactoryをつかった例
- 比較×まとめ
- 参考
Purpose
HttpClient をdotnet coreで使うBestPracticeについて考える
背景
WebAPIを開発する際、HttpClientを使って他のサービスのAPIを呼び出すことがある。特に意識して使ってこなかったが、HttpClientクラスは1アプリケーションのライフタイムの中で1インスタンスとし、それを使いまわす方法が良いとされている。また、Unit Testの観点からも、特定のクラスでNewしてhttpClientのインスタンスを作成し、それを利用するコードはアンチパターンであり、避けたほうが良いとされる。
じゃあ、どうやるのがベストプラクティスなのか・・・というところを調べてみた。
A)アンチパターン
まずは、どこでもいわれているやっちゃダメなパターン。正直、この実装で問題になったことがないため、ダメな理由があまり腹落ちしていない。 ダメな理由としては、以下の点があげられる。引用元:IHttpClientFactory を使用して回復力の高い HTTP 要求を実装する
このクラスでは IDisposable が実装されますが、これを using ステートメント内で宣言およびインスタンス化することはお勧めできません。その理由は、HttpClient オブジェクトが破棄されても、基になるソケットがすぐに解放されず、"ソケットの枯渇" の問題が発生する可能性があるということにあります。そのため、HttpClient は一度インスタンス化されたら、アプリケーションの有効期間にわたって再利用されることを目的としています。 すべての要求に対して HttpClient クラスをインスタンス化すると、高負荷の下で使用可能なソケットの数が枯渇してしまいます。 この問題により、SocketException エラーが発生します。 この問題を解決するために可能なアプローチは、HttpClient クライアントの使用に関するこの Microsoft の記事で説明されているように、HttpClient オブジェクトをシングルトンまたは静的として作成することに基づいています。 これは、1 日に数回実行される有効期間の短いコンソールアプリまたは類似のものに適したソリューションとなります
実装
とあるServiceクラスの中のProvateメソッドにて・・・
private (HttpStatusCode? statusCode, T response) GetRequestCore<T>(stringrequestUrl) where T : BaseModel { try { using (var client = new HttpClient()) { var response = client.GetAsync(requestUrl).Result; if (response.StatusCode != HttpStatusCode.OK) { return (response.StatusCode, null); } var responseObject = JsonConvert.DeserializeObject<T>(response.Content.ReadAsStringAsync().Result); return (response.StatusCode, responseObject); } } catch (Exception ex) { this._logger.LogError(ex.Message); this._logger.LogDebug(ex.ToString()); return (null, null); } }
パフォーマンス測定
試しにGo言語で提供されているBoomというWebAPIの負荷テストツールを使って、パフォーマンスを計測した。全体の比較は最後に行う。
Summary: Total: 226.2775 secs Slowest: 85.4129 secs Fastest: 0.1362 secs Average: 2.1248 secs Requests/sec: 44.1935 Total data: 592432 bytes Size/request: 59 bytes Status code distribution: [200] 10000 responses Response time histogram: 0.136 [1] | 8.664 [9717] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 17.192 [113] | 25.719 [25] | 34.247 [38] | 42.775 [6] | 51.302 [0] | 59.830 [0] | 68.358 [0] | 76.885 [0] | 85.413 [100] | Latency distribution: 10% in 0.2849 secs 25% in 0.3556 secs 50% in 0.5339 secs 75% in 1.0070 secs 90% in 2.6377 secs 95% in 5.7889 secs 99% in 84.0619 secs
B)HttpClientをSingleTonで追加したパターン
毎回HTTP ClientのインスタンスをNewするのを避けるために、SingletonメソッドでHttpClientを登録し、それを利用クラスにDIしてみた。 Singletonにすることで、HttpClientのインスタンスを一つに強制できるため、上記問題が発生しないはず。と、思ったところで、別の問題が発生する。それがDNS問題。上記と同じ引用元から抜粋。
開発者が遭遇するもう 1 つの問題は、長時間実行されるプロセスで HttpClient の共有インスタンスを使用するタイミングです。 HttpClient がシングルトンまたは静的オブジェクトとしてインスタンス化される状況では、dotnet/runtime GitHub リポジトリのこちらの問題で説明されているように、DNS の変更を処理できません。
この方法でもダメそうであるが、念のため動作確認をした。
実装
services.AddSingleton
private (HttpStatusCode? statusCode, T response) GetRequestCore<T>(string requestUrl) where T : BaseModel { try { var response = this._httpClient.GetAsync(requestUrl).Result; if (response.StatusCode != HttpStatusCode.OK) { return (response.StatusCode, null); } var responseObject = JsonConvert.DeserializeObject<T>(response.Content.ReadAsStringAsync().Result); return (response.StatusCode, responseObject); } catch (Exception ex) { this._logger.LogError(ex.Message); this._logger.LogDebug(ex.ToString()); return (null, null); } }
パフォーマンス測定
同様の測定結果。
Summary: Total: 102.0918 secs Slowest: 57.7160 secs Fastest: 0.1089 secs Average: 0.9169 secs Requests/sec: 97.9510 Total data: 483378 bytes Size/request: 48 bytes Status code distribution: [200] 10000 responses Response time histogram: 0.109 [1] | 5.870 [9896] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 11.630 [35] | 17.391 [16] | 23.152 [29] | 28.912 [17] | 34.673 [1] | 40.434 [4] | 46.195 [0] | 51.955 [0] | 57.716 [1] | Latency distribution: 10% in 0.2496 secs 25% in 0.4129 secs 50% in 0.6419 secs 75% in 1.0041 secs 90% in 1.4033 secs 95% in 1.6492 secs 99% in 6.3136 secs
C)HttpClientをservices.AddHttpClient()で追加したパターン
Bと違うところは、Startup処理でHttpClientを有効にするときに、AddHttpClientメソッドを使うようにしたところ。この方法は、型指定されたクライアント クラスを使用するの方法で紹介されているものである。たぶん。
実装
services.AddHttpClient();を利用して追加したHttpClientを、コンストラクタにDIするようにした。そのほかの実装はBと同じ。
パフォーマンス測定
同様の測定結果。
Summary: Total: 106.9550 secs Slowest: 86.1316 secs Fastest: 0.1015 secs Average: 1.0623 secs Requests/sec: 93.4972 Total data: 127866 bytes Size/request: 12 bytes Status code distribution: [200] 10000 responses Response time histogram: 0.101 [1] | 8.704 [9899] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 17.307 [0] | 25.910 [0] | 34.513 [0] | 43.117 [0] | 51.720 [0] | 60.323 [0] | 68.926 [0] | 77.529 [0] | 86.132 [100] | Latency distribution: 10% in 0.1173 secs 25% in 0.1978 secs 50% in 0.2073 secs 75% in 0.2394 secs 90% in 0.2621 secs 95% in 0.2760 secs 99% in 85.5207 secs
D)IHttpClientFactoryをつかった例
HttpClientのドキュメントには、以下のように記載されている。
CreateClient が呼び出されるたびに、次のことが行われます。 - HttpClient の新しいインスタンスが作成されます。 - 構成アクションが呼び出されます。
HttpClientFactoryの実装は以下の通り。
public HttpClient CreateClient(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } HttpMessageHandler handler = CreateHandler(name); var client = new HttpClient(handler, disposeHandler: false); HttpClientFactoryOptions options = _optionsMonitor.Get(name); for (int i = 0; i < options.HttpClientActions.Count; i++) { options.HttpClientActions[i](client); } return client; }
CreateClientが呼ばれると、結局HTTPClientのインスタンスが、生成される。この点は、直にNewするのと同じなので、代り映えしないように見える。しかし、ドキュメントには、こう書かれている。
基になっている HttpClientMessageHandler インスタンスのプールと有効期間が管理されます。 自動管理により、HttpClient の有効期間を手動で管理するときの一般的な DNS (ドメイン ネーム システム) の問題が発生しなくなります。
IHttpClientFactory から HttpClient オブジェクトを取得するたび、新しいインスタンスが返されます。 ただし、各 HttpClient では、HttpMessageHandler の有効期間が切れていない限り、リソースの消費量を減らすために IHttpClientFactory によってプールおよび再利用されている HttpMessageHandler を使用します。
HTTP Client Factoryがソケットの数の管理をしてくれるのと同時に、ライフサイクルについても管理してくれるため、必要に応じて安全に破棄してくれる。これがHttp Client Factoryを利用することが推奨されている理由であるようだ。
実装
services.AddHttpClient(); でHttpClientの利用を宣言する。これは、Cと同様である。
DIしたHttpClientFactory経由でHttpClientのインスタンスを作成する。
private (HttpStatusCode? statusCode, T response) GetRequestCore<T>(string requestUrl) where T :del { try { var response = this._clientFactory.CreateClient().GetAsync(requestUrl).Result; if (response.StatusCode != HttpStatusCode.OK) { return (response.StatusCode, null); } var responseObject = JsonConvert.DeserializeObject<T>(response.Content.ReadAsStringAsync()); return (response.StatusCode, responseObject); } catch (Exception ex) { this._logger.LogError(ex.Message); this._logger.LogDebug(ex.ToString()); return (null, null); } }
パフォーマンス測定
同様の測定。
Summary: Total: 81.9840 secs Slowest: 37.1453 secs Fastest: 0.1029 secs Average: 0.7286 secs Requests/sec: 121.9750 Total data: 392810 bytes Size/request: 39 bytes Status code distribution: [200] 10000 responses Response time histogram: 0.103 [1] | 3.807 [9923] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 7.511 [25] | 11.216 [20] | 14.920 [16] | 18.624 [14] | 22.328 [0] | 26.033 [0] | 29.737 [0] | 33.441 [0] | 37.145 [1] | Latency distribution: 10% in 0.2539 secs 25% in 0.4032 secs 50% in 0.6024 secs 75% in 0.8922 secs 90% in 1.1329 secs 95% in 1.2771 secs 99% in 1.7285 secs
比較×まとめ
A | B | C | D | |
---|---|---|---|---|
total | 226.2775 secs | 102.0918 secs | 106.9550 secs | 81.9840 secs |
slowest | 85.4129 secs | 57.7160 secs | 86.1316 secs | 37.1453 secs |
fastest | 0.1362 secs | 0.1089 secs | 0.1015 secs | 0.1029 secs |
average | 2.1248 secs | 0.9169 secs | 1.0623 secs | 0.7286 secs |
total data | 592432 bytes | 483378 bytes | 127866 bytes | 392810 bytes |
なぜTotalDataが・・・というのはあるが、基本的にパフォーマンスが良かったのはHttpClientFactory経由でHttpClientを作成したパターンであった。実行速度に注目した検証をしたが、本当に大事なのは、実行速度よりもソケットの枯渇とDNS問題への対応だと思うため、やはりHTTP Client Factoryを使ったほうが良いのだと思う。