きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

HttpClient × HttpClientFactory × より良い実装方法を調べてみた

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();として追加したHttpClientを、コンストラクタにDIするようにした。もともとNewしていたところが、this._httpclientを使うようになった。

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を使ったほうが良いのだと思う。

参考