WebAPIクライアントで呼び出しごとに新しいHttpClientを作成するオーバーヘッドは何ですか?


162

HttpClientWebAPIクライアントの存続期間はどうあるべきですか?複数の呼び出しに対しての
インスタンスを1つ持つ方が良いHttpClientですか?

HttpClient以下の例のように、リクエストごとに作成および破棄するオーバーヘッドは何ですか(http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-から取得)a-net-client):

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:9000/");
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // New code:
    HttpResponseMessage response = await client.GetAsync("api/products/1");
    if (response.IsSuccessStatusCode)
    {
        Product product = await response.Content.ReadAsAsync<Product>();
        Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
    }
}

Stopwatchクラスがベンチマークに使用できるかどうかはわかりません。私の推定ではHttpClient、これらのすべてのインスタンスが同じコンテキストで使用されていると仮定すると、単一のを使用する方が理にかなっています。
マシュー14年

回答:


215

HttpClient複数の通話で再利用できるように設計されています。複数のスレッド間でも。にHttpClientHandlerは、呼び出し全体で再利用することを目的とした認証情報とCookieがあります。新しいHttpClientインスタンスを作成するには、それらすべてを再設定する必要があります。また、このDefaultRequestHeadersプロパティには、複数の呼び出しを目的としたプロパティが含まれています。リクエストごとにこれらの値をリセットする必要があるため、ポイントが無効になります。

のもう1つの主要な利点HttpClientは、HttpMessageHandlers横断的関心事を適用するために要求/応答パイプラインに追加できることです。これらは、ロギング、監査、スロットル、リダイレクト処理、オフライン処理、メトリックのキャプチャに使用できます。あらゆる種類のもの。各リクエストで新しいHttpClientが作成される場合、これらのメッセージハンドラーのすべてを各リクエストで設定する必要があり、これらのハンドラーのリクエスト間で共有されるアプリケーションレベルの状態も提供する必要があります。

の機能を使用すればするほどHttpClient、既存のインスタンスを再利用することが理にかなっていることがわかります。

ただし、私の意見では、最大の問題は、HttpClientクラスが破棄されると、が破棄され、によって管理される接続のプール内の接続がHttpClientHandler強制的に閉じTCP/IPられることですServicePointManager。これは、新しいリクエストごとにHttpClient新しいTCP/IP接続を再確立する必要があることを意味します。

私のテストでは、LANでプレーンHTTPを使用しているため、パフォーマンスへの影響はほとんど無視できます。これは、接続を閉じよHttpClientHandlerうとしても接続を開いたままにしておくTCPキープアライブが根底にあるためと考えられます。

インターネット経由のリクエストで、私は別の話を見ました。毎回リクエストを再度開く必要があるため、40%のパフォーマンスヒットがありました。

HTTPS接続のヒットがさらに悪くなるのではないかと思います。

私のアドバイスは、接続する個別のAPIごとに、アプリケーションの存続期間にわたってHttpClientのインスタンス保持することです。


5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerこの発言について、どの程度確信していますか。それは信じがたいことです。HttpClient私には、頻繁にインスタンス化されるはずの作業単位のように見えます。
usr

2
@vkelmanはい、新しいHttpClientHandlerで作成した場合でも、HttpClientのインスタンスを再利用できます。また、HttpClientHandlerを再利用し、接続を終了せずにHttpClientを破棄できるようにするHttpClientの特別なコンストラクターがあることにも注意してください。
Darrel Miller

2
@vkelman HttpClientを保持したいが、HttpClientHandlerを保持したい場合は、2番目のパラメーターがfalseのときに接続を開いたままにします。
Darrel Miller

2
@DarrelMillerつまり、接続がHttpClientHandlerにバインドされているようです。スケールするために接続を破棄したくないので、HttpClientHandlerを保持し、そこからすべてのHttpClientインスタンスを作成するか、静的HttpClientインスタンスを作成する必要があることを知っています。ただし、CookieContainerがHttpClientHandlerにバインドされていて、リクエストごとにCookieを変える必要がある場合、何をお勧めしますか?リクエストごとにCookieContainerを変更することで、静的なHttpClientHandlerでのスレッド同期を回避したいと思います。
Dave Black

2
@ Sana.91できます。これをサービスコレクションのシングルトンとして登録し、そのようにアクセスすることをお勧めします。
Darrel Miller

69

アプリケーションをスケーリングする場合、違いは非常に大きくなります。負荷に応じて、非常に異なるパフォーマンスの数値が表示されます。Darrel Millerが述べているように、HttpClientはリクエスト間で再利用できるように設計されています。これは、それを書いたBCLチームのメンバーによって確認されました。

私が最近行ったプロジェクトは、非常に大規模で有名なオンラインコンピュータ小売業者が、いくつかの新しいシステムのブラックフライデー/ホリデートラフィックをスケールアウトできるようにすることでした。HttpClientの使用に関するパフォーマンスの問題が発生しました。これはを実装しているためIDisposable、開発者はインスタンスを作成してusing()ステートメント内に配置することで、通常行うことを行います。アプリの負荷テストを開始すると、サーバーが完全に機能しなくなりました-はい、サーバーはアプリだけではありません。その理由は、HttpClientのすべてのインスタンスがサーバーのポートを開くためです。GCの確定的でないファイナライズと、複数のOSIレイヤーにまたがるコンピューターリソースを使用しているという事実により、ネットワークポートを閉じるにはしばらく時間がかかる場合があります。実際にはWindows OS 自体(Microsoftごとに)ポートを閉じるのに最大20秒かかることがあります。ポートを閉じるよりも速く開いていました-サーバーポートの枯渇によりCPUが100%になりました。私の修正は、HttpClientを静的インスタンスに変更して、問題を解決することでした。はい、それは使い捨てのリソースですが、オーバーヘッドはパフォーマンスの違いよりもはるかに重要です。アプリの動作を確認するために、負荷テストを行うことをお勧めします。

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-clientにあるドキュメントと例については、WebAPIガイダンスページを確認することもでき ます

このコールアウトに特に注意してください:

HttpClientは、一度インスタンス化され、アプリケーションの存続期間を通じて再利用されることを目的としています。特にサーバーアプリケーションでは、要求ごとに新しいHttpClientインスタンスを作成すると、負荷が高い場合に使用可能なソケットの数がなくなります。これにより、SocketExceptionエラーが発生します。

HttpClient異なるヘッダー、ベースアドレスなどでstaticを使用する必要がある場合は、HttpRequestMessage手動で作成し、にそれらの値を設定する必要がありますHttpRequestMessage。次に、HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

.NET Coreの更新:インスタンスIHttpClientFactoryを作成するには、via Dependency Injectionを使用する必要がありHttpClientます。それはあなたのために寿命を管理し、あなたはそれを明示的に処分する必要はありません。ASP.NET CoreでIHttpClientFactoryを使用してHTTPリクエストを作成するをご覧ください。


1
この投稿には、ストレステストを行う人のための役立つ洞察が含まれています。
Sana.91

9

他の回答が述べるように、HttpClient再利用のためのものです。ただし、HttpClientマルチスレッドアプリケーション全体で単一のインスタンスを再利用するBaseAddressと、次のようなステートフルプロパティの値を変更できなくなりますDefaultRequestHeaders(彼らはあなたのアプリケーション全体で一定である場合にのみ、それらを使用できるように)。

この制限を回避取得するための一つのアプローチは、ラッピングされHttpClientたクラスその重複すべてとHttpClientあなたが必要とするメソッド(GetAsyncPostAsyncなど)とシングルトンに委譲してHttpClient。ただし、これはかなり退屈であり(拡張メソッドもラップする必要があります)、幸いにも別の方法がありHttpClientますHttpClientHandler。新しいインスタンスを作成し続けますが、基になるを再利用します。ハンドラを破棄しないようにしてください。

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
    //client code can dispose these HttpClient instances
    return new HttpClient(_sharedHandler, disposeHandler: false)         
    {
       DefaultRequestHeaders = 
       {
            Authorization = new AuthenticationHeaderValue("Bearer", token) 
       } 
    };
}

2
より良い方法は、1つのHttpClientインスタンスを保持し、独自のローカルHttpRequestMessageインスタンスを作成してから、HttpClientで.SendAsync()メソッドを使用することです。この方法でも、スレッドセーフです。各HttpRequestMessageには独自の認証/ URL値があります。
Tim P.

@TimP。なぜそれが良いのですか?SendAsyncはるかに便利などの専用のメソッドよりもPutAsyncPostAsJsonAsync
オハッドSchneiderの

2
SendAsyncを使用すると、URLやヘッダーなどの他のプロパティを変更しても、スレッドセーフであることができます。
Tim P.

2
はい、ハンドラーが鍵です。それがHttpClientインスタンス間で共有されている限り問題ありません。以前のコメントを読み違えました。
デイブブラック

1
共有ハンドラーを保持している場合でも、古いDNS問題に対処する必要がありますか?
シャンティ

5

大量のWebサイトに関連していますが、HttpClientには直接関連していません。すべてのサービスに以下のコードスニペットがあります。

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
        const int DefaultConnectionLeaseTimeout = 60000;

        ServicePoint sp =
                ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
        sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

から https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2); k(DevLang-csharp)&rd = true

「このプロパティを使用して、ServicePointオブジェクトのアクティブな接続が無期限に開いたままにならないようにすることができます。このプロパティは、負荷分散シナリオなど、接続を定期的に削除して再確立する必要があるシナリオを対象としています。

既定では、要求に対してKeepAliveがtrueの場合、MaxIdleTimeプロパティは、非アクティブのためにServicePoint接続を閉じるためのタイムアウトを設定します。ServicePointにアクティブな接続がある場合、MaxIdleTimeは効果がなく、接続は無期限に開いたままになります。

ConnectionLeaseTimeoutプロパティが-1以外の値に設定されていて、指定された時間が経過した後、リクエストでKeepAliveをfalseに設定することにより、リクエストを処理した後、アクティブなServicePoint接続が閉じられます。この値を設定すると、ServicePointオブジェクトによって管理されるすべての接続に影響します。」

フェイルオーバーしたいサービスがCDNまたは他のエンドポイントの背後にある場合、この設定は、発信者が新しい宛先にあなたを追跡するのに役立ちます。この例では、フェイルオーバーの60秒後、すべての発信者が新しいエンドポイントに再接続する必要があります。依存するサービス(YOUが呼び出すサービス)とそのエンドポイントを知っている必要があります。


接続を開いたり閉じたりすることで、サーバーに大きな負荷をかけます。インスタンスベースのHttpClientHandlersでインスタンスベースのHttpClientを使用する場合でも、注意しないとポートを使い果たしてしまいます。
デイブブラック

反対しない。すべてがトレードオフです。私たちにとって、再ルーティングされたCDNまたはDNSをたどることは、銀行のお金と収益の損失です。
払い戻しなし返品不可2017

1

Simon Timmsによるこのブログ投稿を参照することもできますhttps : //aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

しかしHttpClient、違います。IDisposableインターフェースを実装していますが、実際には共有オブジェクトです。つまり、内部では再入可能であり、スレッドセーフです。HttpClient実行ごとにの新しいインスタンスを作成する代わりにHttpClient、アプリケーションの存続期間全体にわたっての単一のインスタンスを共有する必要があります。その理由を見てみましょう。


1

指摘すべきことの1つは、「使用しないでください」というブログノートのどれも、考慮する必要があるのはBaseAddressとDefaultHeaderだけではないということです。HttpClientを静的にすると、リクエスト間で伝達される内部状態があります。例:FedAuthトークンを取得するためにHttpClientを使用してサードパーティに対して認証を行っています(OAuth / OWIN / etcを使用しない理由は無視してください)。その応答メッセージにはFedAuthのSet-Cookieヘッダーがあり、これがHttpClient状態に追加されます。APIに次にログインするユーザーは、各リクエストでこれらのCookieを管理していない限り、最後のユーザーのFedAuth Cookieを送信します。


0

最初の問題として、このクラスは使い捨てusingですが、ステートメントでそれを使用することは最良の選択ではありません。HttpClientオブジェクト、基になるソケットがすぐに解放されず、「ソケットの枯渇」という深刻な問題を引き起こす可能性があるはありません。

しかし、次の問題があります HttpClient、シングルトンオブジェクトまたは静的オブジェクトとして使用する場合に発生する可能ます。この場合、シングルトンまたはスタティックHttpClientは尊重しませんDNS変更を。

、.NETのコアあなたが同じことを行うことができますHttpClientFactory このような何か:

public interface IBuyService
{
    Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
    private readonly HttpClient _httpClient;

    public BuyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Buy> GetBuyItems()
    {
        var uri = "Uri";

        var responseString = await _httpClient.GetStringAsync(uri);

        var buy = JsonConvert.DeserializeObject<Buy>(responseString);
        return buy;
    }
}

ConfigureServices

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

ドキュメントとここの

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.