世界のやまさ

SEKAI NO YAMASA

HttpClient よりも HttpClientFactory を利用したほうが良いかも

.NET において REST API を呼び出す際、多くの方が HttpClient を使用すると思いますが、その時の注意点が帝国兵さんの Qiita で書かれています。

qiita.com

この記事に書いてある通りで、 HTTP Client インスタンスをいちいち破棄して作成を繰り返していると、CLOSE_WAIT または TIME_WAIT 状態の SNAT ポートが増えてしまい、ポートが枯渇して通信ができなくなります。

具体的には hping3 で繰り返しリクエストを行っているのと同じ状態であると思います。 hping3 の使い方の具体例は宇田さんのサイトをご覧ください。

www.syuheiuda.com

さて、 private static readonly HttpClient HttpClient; を使うのも良いと思いますが、 .NET Core 2.1 以降 であれば HttpClientFactory を使うほうが良いと思います。

HttpClientFactory とは

docs.microsoft.com

HttpClientFactory は、自己主張性の強いファクトリで、アプリケーションに使用する HttpClient インスタンスを作成するため、.NET Core 2.1 以降で使用できます。

いやいや、この説明だけじゃなんのことかさっぱりわからんですよね。

HttpClientFactory は名前の通り、先の HttpClient のファクトリーでより使いやすくなってます。

HttpClientFactory のメリット

仕組みは色々とあるのですが、利用者として特に2点が有用だと思いました。

  1. ASP.NET Core アプリケーションなどに DI できる
  2. ポリシーを利用してリトライ回数や待ち時間を調整できる

1. ASP.NET Core アプリケーションなどに DI できる

ASP.NET Core アプリケーションに DI して利用する方法はこちらを参照。

docs.microsoft.com

サンプルコードを引用します。

Startup.ConfigureServicesIServiceCollection に登録します。

services.AddHttpClient();

DI が利用できる箇所で、IHttpClientFactory を使用して、HttpClient インスタンスが取得できます。サンプルコードでは Model で行ってますが、Service や Controller でも可能でしょう。

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/aspnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

2. ポリシーを利用してリトライ回数や待ち時間を調整できる

REST API を呼び出すと、相手先のサーバーがたまたま調子悪くて、5秒後に呼び出したらうまくいく。といった経験がある人も多いかと思います。

今までは、try catch で wait を自分で入れたりしてましたが、それらをあらかじめ設定することができます。

例えば、1回目失敗したら3秒後に再実施、以降5秒後、10秒後、30秒後と繰り返し最終的にエラーを返す。といったらことが可能になります。詳細はこちらをご確認ください。

docs.microsoft.com

SDK から HttpClientを使う場合

自分で HttpClient を使わず、SDK などから REST API を呼び出すということも多々あると思います。例えば自前の Web アプリから Azure Active Directory を利用して認証を行うなどです。AzureAD/azure-activedirectory-library-for-dotnet: ADAL authentication libraries for .net の場合、Wiki の Providing an HttpClientIHttpClientFactory をコンストラクタで渡す例が書いてありました。

IHttpClientFactory myHttpClientFactory = new MyHttpClientFactory();

AuthenticationContext authenticationContext = new AuthenticationContext(
     authority: "https://login.microsoftonline.com/common",
     validateAuthority: true,
     tokenCache: <some token cache>,
     httpClientFactory: myHttpClientFactory);

こうすることで HttpClient を自分のアプリケーションと SDK で共有できるので、より効率的に SNAT を利用できることができます。

まとめ

.NET Core 2.1 以降 であれば HttpClient を直接使うより、 HttpClientFactory を利用したほうがよりメリットを享受できると感じました。

帝国兵さんの Qiita 記事には SNAT の話があったので、そちらも後日 blog にまとめたいと思いました。

2019/10/05 追記 HttpClientFactory の実装について

id:nishikawa_sh さんのコメントをいただいて、ソースを追いかけてみました。

まず、https://github.com/aspnet/Extensions/blob/release/2.2/src/HttpClientFactory/Http/src/DefaultHttpClientFactory.cs#L125 に注目するとコンストラクタの引数が2つあります。

var client = new HttpClient(handler, disposeHandler: false);

これはHttpClient Constructor (System.Net.Http) | Microsoft Docsにこう書いてあります。

提供されたハンドラーを使用して、HttpClient クラスの新しいインスタンスを初期化し、このインスタンスが破棄されるときにそのハンドラーを破棄するかどうかを指定します。

したがって、第一パラメーターの HttpMessageHandler を渡しますが、ハンドラーは残ったままとなります。

じゃぁ、ハンドラーはどうやって作っているかというと、先のソースの一行上 CreateHandler() で作っており、 _activeHandlers フィールドで管理しています。これは、コレクションになっていて要素はタイマーを使って消しているようでなかなか凝った実装をしているようです。

おそらくこの _activeHandlers がPoolされている図の部分ですね。

f:id:nnasaki:20191005075843p:plain
HttpClientFactory を使用して回復力の高い HTTP 要求を実装する | Microsoft Docs より引用

さて、Poolされている HttpMessageHandler はどうなっているかというと、abstract class で実態は .NET Framework および .NET Core 2.0 以前 と .NET Core 2.1 以降 で異なっていました。

この実装の違いは HttpClient Class (System.Net.Http) | Microsoft Docs に SocketsHttpHandler を利用するメリットが書いてありました。

  • 以前の実装と比較して、パフォーマンスが大幅に向上しています。
  • プラットフォームの依存関係を削除することで、デプロイとサービスが簡単になります。 たとえば、はlibcurl 、macOS 用の .net core と Linux 用の .net core に依存しなくなりました。
  • すべての .NET プラットフォームでの一貫した動作。

結論として、 .NET Core 2.1 以降 は HttpClientFactory の利便性だけでなく、パフォーマンスも改善しているようだということがわかりました。