.Net 4.5の非同期HttpClientは、集中的な負荷のあるアプリケーションにとって悪い選択ですか?


130

私は最近、非同期の方法で生成できるHTTP呼び出しのスループットをテストするための単純なアプリケーションと、従来のマルチスレッドアプローチを作成しました。

アプリケーションは、事前定義された数のHTTP呼び出しを実行でき、最後にそれらを実行するために必要な合計時間を表示します。テスト中、すべてのHTTP呼び出しはローカルIISサーバーに対して行われ、小さなテキストファイル(サイズは12バイト)を取得しました。

非同期実装のコードの最も重要な部分を以下に示します。

public async void TestAsync()
{
    this.TestInit();
    HttpClient httpClient = new HttpClient();

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        ProcessUrlAsync(httpClient);
    }
}

private async void ProcessUrlAsync(HttpClient httpClient)
{
    HttpResponseMessage httpResponse = null;

    try
    {
        Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
        httpResponse = await getTask;

        Interlocked.Increment(ref _successfulCalls);
    }
    catch (Exception ex)
    {
        Interlocked.Increment(ref _failedCalls);
    }
    finally
    { 
        if(httpResponse != null) httpResponse.Dispose();
    }

    lock (_syncLock)
    {
        _itemsLeft--;
        if (_itemsLeft == 0)
        {
            _utcEndTime = DateTime.UtcNow;
            this.DisplayTestResults();
        }
    }
}

マルチスレッド実装の最も重要な部分を以下に示します。

public void TestParallel2()
{
    this.TestInit();
    ServicePointManager.DefaultConnectionLimit = 100;

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        Task.Run(() =>
        {
            try
            {
                this.PerformWebRequestGet();
                Interlocked.Increment(ref _successfulCalls);
            }
            catch (Exception ex)
            {
                Interlocked.Increment(ref _failedCalls);
            }

            lock (_syncLock)
            {
                _itemsLeft--;
                if (_itemsLeft == 0)
                {
                    _utcEndTime = DateTime.UtcNow;
                    this.DisplayTestResults();
                }
            }
        });
    }
}

private void PerformWebRequestGet()
{ 
    HttpWebRequest request = null;
    HttpWebResponse response = null;

    try
    {
        request = (HttpWebRequest)WebRequest.Create(URL);
        request.Method = "GET";
        request.KeepAlive = true;
        response = (HttpWebResponse)request.GetResponse();
    }
    finally
    {
        if (response != null) response.Close();
    }
}

テストを実行すると、マルチスレッドバージョンの方が高速であることがわかりました。1万回のリクエストが完了するまでに約0.6秒かかりましたが、非同期のリクエストは、同じ量の負荷で完了するまでに約2秒かかりました。非同期の方が速いと思っていたので、これはちょっとした驚きでした。多分それは私のHTTP呼び出しが非常に高速であったという事実のためでした。実際のシナリオでは、サーバーがより意味のある操作を実行する必要があり、ネットワーク遅延も発生する場合、結果が逆になる可能性があります。

ただし、本当に気になるのは、負荷が増加したときのHttpClientの動作です。10kメッセージの配信には約2秒かかるため、メッセージの10倍の数のメッセージを配信するには約20秒かかると思いましたが、テストを実行すると、100kメッセージの配信に約50秒かかることがわかりました。さらに、通常200kメッセージを配信するのに2分以上かかり、次の例外を除いて、数千(3-4k)のメッセージが失敗することがよくあります。

システムに十分なバッファスペースがないか、キューがいっぱいだったため、ソケットに対する操作を実行できませんでした。

IISログを確認したところ、失敗した操作はサーバーに到達しませんでした。クライアント内で失敗しました。私はWindows 7マシンでテストを実行しました。エフェメラルポートのデフォルトの範囲は49152から65535です。netstatを実行すると、テスト中に約5〜6kのポートが使用されていることがわかりました。ポートの不足が実際に例外の原因であった場合は、netstatが状況を適切に報告しなかったか、HttClientが最大数のポートのみを使用して例外をスローし始めたことを意味します。

対照的に、HTTP呼び出しを生成するマルチスレッドアプローチは、非常に予測可能でした。10kメッセージの場合は約0.6秒、100kメッセージの場合は約5.5秒、100万メッセージの場合は約55秒かかりました。失敗したメッセージはありません。さらに、実行中に55 MBを超えるRAMを使用することはありませんでした(Windowsタスクマネージャによると)。メッセージを非同期で送信するときに使用されるメモリは、負荷に比例して増加しました。200kメッセージテスト中に約500 MBのRAMを使用しました。

上記の結果には主に2つの理由があると思います。1つ目は、HttpClientがサーバーとの新しい接続を作成することに非常に貪欲であるように見えることです。netstatによって報告される使用ポートの数が多いということは、おそらくHTTPキープアライブのメリットがあまりないことを意味します。

2つ目は、HttpClientにスロットルメカニズムがないように見えることです。実際、これは非同期操作に関連する一般的な問題のようです。非常に多数の操作を実行する必要がある場合は、すべての操作が一度に開始され、利用可能なときに継続が実行されます。理論的にはこれは問題ありません。非同期操作では外部システムに負荷がかかるためですが、上記で証明されているように、これは完全には当てはまりません。一度に多数のリクエストを開始すると、メモリ使用量が増加し、実行全体が遅くなります。

シンプルだがプリミティブな遅延メカニズムで非同期リクエストの最大数を制限することで、メモリと実行時間に関して、より良い結果を得ることができました。

public async void TestAsyncWithDelay()
{
    this.TestInit();
    HttpClient httpClient = new HttpClient();

    for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
    {
        if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
            await Task.Delay(DELAY_TIME);

        ProcessUrlAsyncWithReqCount(httpClient);
    }
}

HttpClientに同時リクエストの数を制限するメカニズムが含まれている場合は、非常に便利です。タスククラス(.Netスレッドプールに基づく)を使用する場合、同時スレッドの数を制限することにより、スロットルが自動的に実現されます。

完全な概要として、HttpClientではなくHttpWebRequestに基づいた非同期テストのバージョンも作成し、より良い結果を得ることができました。まず、(ServicePointManager.DefaultConnectionLimitまたはconfigを使用して)同時接続の数に制限を設定できます。つまり、ポートが不足したり、リクエストが失敗したりすることはありません(HttpClientは、デフォルトでは、HttpWebRequestに基づいています) 、しかしそれは接続制限設定を無視するようです)。

非同期のHttpWebRequestアプローチは、マルチスレッドのアプローチよりも約50〜60%遅くなりましたが、予測可能で信頼性がありました。それの唯一の欠点は、大きな負荷の下で大量のメモリを使用することでした。たとえば、100万のリクエストを送信するには約1.6 GBが必要でした。同時リクエストの数を制限することにより(上記のHttpClientの場合と同様)、使用メモリをわずか20 MBに削減し、マルチスレッドアプローチよりも実行時間を10%遅くすることができました。

この長いプレゼンテーションの後、私の質問は次のとおりです。.Net 4.5のHttpClientクラスは、集中的な負荷のアプリケーションにとって悪い選択ですか?それを絞る方法はありますか?私が言及する問題を修正する必要がありますか?HttpWebRequestの非同期フレーバーはどうですか?

更新(@Stephen Clearyに感謝)

結局のところ、HttpClientは、HttpWebRequest(デフォルトではこれに基づいています)と同様に、ServicePointManager.DefaultConnectionLimitで制限された同じホスト上の同時接続数を制限できます。奇妙なことに、MSDNによると、接続制限のデフォルト値は2です。また、実際に2がデフォルト値であることを示すデバッガーを使用して自分の側でも確認しました。ただし、ServicePointManager.DefaultConnectionLimitに値を明示的に設定しない限り、デフォルト値は無視されるようです。私のHttpClientテスト中に明示的に値を設定しなかったので、無視されたと思いました。

ServicePointManager.DefaultConnectionLimitを100に設定した後、HttpClientは信頼性が高く予測可能なものになりました(netstatは100個のポートのみが使用されていることを確認しています)。非同期のHttpWebRequestよりも遅い(約40%)が、奇妙なことに、使用するメモリが少ない。100万件のリクエストを含むテストでは、非同期HttpWebRequestの1.6 GBと比較して、最大550 MBを使用しました。

したがって、HttpClientをServicePointManager.DefaultConnectionLimitと組み合わせて使用​​すると、信頼性は保証されますが(少なくとも同じホストに対してすべての呼び出しが行われているシナリオでは)、適切なスロットリングメカニズムがないためにパフォーマンスが低下するように見えます。同時リクエスト数を設定可能な値に制限し、残りをキューに入れるものは、高いスケーラビリティのシナリオにはるかに適しています。


4
HttpClient尊重する必要がありServicePointManager.DefaultConnectionLimitます。
Stephen Cleary 2013

2
あなたの観察は調査する価値があるようです。ただし、気になるのは1つです。一度に数千の非同期IOを発行するのは非常に不自然だと思います。本番環境ではこれを行うことはありません。あなたが非同期であるという事実は、さまざまなリソースを消費することに夢中になれるという意味ではありません。(Microsoftの公式サンプルは、この点でも少し誤解を招くものです。)
usr

1
ただし、時間の遅れでスロットルしないでください。経験的に決定した固定並行性レベルでスロットルします。単純な解決策はSemaphoreSlim.WaitAsyncですが、任意の大量のタスクには適していません。
usr

1
@FlorinDumitrescuスロットリングには、SemaphoreSlim既に説明したように、またはActionBlock<T>TPL Dataflowからを使用できます。
2013

1
@svick、ご提案ありがとうございます。スロットル/同時実行制限のメカニズムを手動で実装することに興味はありません。前述のように、私の質問に含まれている実装は、テストと理論の検証のみを目的としていました。生産には至らないので、改善はしていません。私が興味を持っているのは、.Netフレームワークが非同期IO操作(HttpClientを含む)の同時実行性を制限するための組み込みメカニズムを提供しているかどうかです。
Florin Dumitrescu 2013

回答:


64

質問で述べたテストに加えて、最近、HTTP呼び出しがはるかに少ない新しいテストを作成しました(以前の100万と比較して5000)が、実行にはるかに長い時間がかかったリクエスト(以前の約1ミリ秒と比較して500ミリ秒)です。同期マルチスレッドアプリケーション(HttpWebRequestに基づく)と非同期I / Oアプリケーション(HTTPクライアントに基づく)の両方のテスターアプリケーションは、CPUの約3%と30 MBのメモリを使用して約10秒で実行されます。2つのテスターの唯一の違いは、マルチスレッド化されたテスターは実行に310スレッドを使用したのに対し、非同期テスターは22テスターを使用したことです。

私のテストの結論として、非同期HTTP呼び出しは、非常に高速な要求を処理する場合の最良のオプションではありません。その背後にある理由は、非同期I / O呼び出しを含むタスクを実行すると、非同期呼び出しが行われ、タスクの残りがコールバックとして登録されるとすぐに、タスクが開始されたスレッドが終了するためです。次に、I / O操作が完了すると、最初の使用可能なスレッドで実行するためにコールバックがキューに入れられます。これらすべてによりオーバーヘッドが発生し、それらを開始したスレッドで実行すると、高速I / O操作がより効率的になります。

非同期HTTP呼び出しは、I / O操作の完了を待機するスレッドをビジー状態にしないため、長いI / O操作または長いI / O操作の可能性がある場合に適しています。これにより、アプリケーションが使用するスレッドの総数が減り、CPUバインド操作が費やすCPU時間を増やすことができます。さらに、限られた数のスレッドのみを割り当てるアプリケーション(Webアプリケーションの場合など)では、非同期I / Oにより、I / O呼び出しを同期的に実行する場合に発生する可能性があるスレッドプールのスレッドの枯渇を防ぎます。

したがって、非同期のHttpClientは、負荷の高いアプリケーションのボトルネックにはなりません。それはその性質上、非常に高速なHTTPリクエストにはあまり適していませんが、長い、または潜在的に長いリクエスト、特に使用可能なスレッド数が限られているアプリケーション内では理想的です。また、ServicePointManager.DefaultConnectionLimitを介して並行性を制限することをお勧めします。適切なレベルの並列処理を確保するのに十分高いが、一時的なポートの枯渇を防ぐのに十分低い値を使用します。この質問に対して提示されたテストと結論の詳細については、こちらをご覧ください


3
「非常に速い」とはどのくらい速いですか?1ms?100ms?1,000ms?
Tim P.

私は「非同期」アプローチのようなものを使用して、WindowsにデプロイされたWebLogic Webサーバーの負荷を再生していますが、一時的なポート枯渇の問題がかなり早く発生しています。ServicePointManager.DefaultConnectionLimitには触れていません。また、各リクエストですべて(HttpClientと応答)を破棄して再作成しています。接続が開いたままになり、ポートが使い果たされる原因は何か考えていますか?
Iravanchi 2014年

@TimP。私のテストでは、上記のように、「非常に高速」なのは、完了するまでにわずか1ミリ秒かかるリクエストでした。現実の世界では、これは常に主観的です。私の見解では、ローカルネットワークデータベースでの小さなクエリに相当するものは高速と見なすことができ、インターネット経由のAPI呼び出しに相当するものは低速または潜在的に低速と見なすことができます。
Florin Dumitrescu 2014年

1
@Iravanchiは、「非同期」アプローチでは、要求の送信と応答の処理を別々に実行します。コール数が多い場合、すべてのリクエストは非常に速く送信され、応答は到着時に処理されます。接続を破棄できるのは、応答が到着した後であるため、多数の同時接続が蓄積し、一時的なポートを使い果たしてしまう可能性があります。ServicePointManager.DefaultConnectionLimitを使用して、同時接続の最大数を制限する必要があります。
Florin Dumitrescu 2014年

1
@FlorinDumitrescu、私はまた、ネットワーク呼び出しは本質的に予測不可能であることを付け加えます。90msの時間の10msで実行されるものは、そのネットワークリソースが混雑しているか、残りの10%の時間利用できないときに、ブロッキングの問題を引き起こす可能性があります。
Tim P.

27

結果に影響を与える可能性があると考える1つのことは、HttpWebRequestではResponseStreamを取得せず、そのストリームを消費しないことです。HttpClientでは、デフォルトでネットワークストリームをメモリストリームにコピーします。現在HttpWebRquestを使用しているのと同じ方法でHttpClientを使用するには、以下を実行する必要があります。

var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

もう1つは、スレッド化の観点から、実際にテストしている実際の違いが本当にわからないということです。HttpClientHandlerの詳細を掘り下げる場合、非同期リクエストを実行するためにTask.Factory.StartNewを実行します。スレッド化動作は、HttpWebRequestの例を使用した例が実行されるのとまったく同じ方法で同期コンテキストに委任されます。

間違いなく、HttpClientはデフォルトでトランスポートライブラリとしてHttpWebRequestを使用するため、オーバーヘッドが多少追加されます。したがって、HttpClientHandlerを使用している間は常に、HttpWebRequestを使用して直接より良いパフォーマンスを得ることができます。HttpClientがもたらす利点は、HttpResponseMessage、HttpRequestMessage、HttpContentなどの標準クラスと、厳密に型指定されたすべてのヘッダーにあります。それ自体はパフォーマンスの最適化ではありません。


(古い答えですが)HttpClient使いやすいようで、非同期の方法だと思っていましたが、これには多くの「賛否両論」があるようです。たぶん、HttpClientより直感的に使用できるように書き直す必要がありますか?それとも、ドキュメントはそれを最も効率的に使用する方法についての重要なことを本当に強調していましたか?
mortb 2017

@ mortb、Flurl.Http flurl.ioは、HttpClientのラッパーを使用する方が直感的です
Michael Freidgeim

1
@MichaelFreidgeim:ありがとう、私は今までにHttpClientと一緒に暮らすことを学びました...
mortb

17

これはOPの質問の「非同期」の部分に直接答えることはしませんが、これは彼が使用している実装のエラーに対処します。

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

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

以下のリンクでも回答:

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

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client


クライアントでTCPポートの枯渇を引き起こすまったく同じ問題が見つかりました。解決策は、反復呼び出しが行われている長期間HttpClientインスタンスをリースすることであり、呼び出しごとに作成して破棄することではありませんでした。私が到達した結論は、「それがDisposeを実装しているからといって、それがDisposeを安くするという意味ではありません」でした。
PhillipH 2016年

したがって、HttpClientが静的で、次のリクエストでヘッダーを変更する必要がある場合、最初のリクエストはどうなりますか?HttpClientは静的であるため、HttpClient.DefaultRequestHeaders.Accept.Clear();を発行するなど、HttpClientを変更することに害はありませんか。?たとえば、トークンを介して認証しているユーザーがいる場合、それらのトークンは、APIへのリクエストのヘッダーとして追加する必要があります。HttpClientを静的に設定してから、HttpClientでこのヘッダーを変更すると悪影響がありますか?
crizzwald 2016年

ヘッダー/クッキーなどのHttpClientインスタンスメンバーを使用する必要がある場合は、静的HttpClientを使用しないでください。それ以外の場合、インスタンスデータ(ヘッダー、Cookie)はすべてのリクエストで同じになります。
Dave Black、

これが事実だからです...あなたはあなたの投稿であなたが上で説明していることをどのように防ぐのですか?負荷に対して?ロードバランサーとより多くのサーバーをスローしますか?
crizzwald 2016年

@crizzwald-私の投稿で、使用された解決策に言及しました。HttpClientの静的インスタンスを使用します。HttpClientでヘッダー/ Cookieを使用する必要がある場合は、別の方法を使用することを検討します。
Dave Black
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.