私は最近、非同期の方法で生成できる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と組み合わせて使用すると、信頼性は保証されますが(少なくとも同じホストに対してすべての呼び出しが行われているシナリオでは)、適切なスロットリングメカニズムがないためにパフォーマンスが低下するように見えます。同時リクエスト数を設定可能な値に制限し、残りをキューに入れるものは、高いスケーラビリティのシナリオにはるかに適しています。
SemaphoreSlim
既に説明したように、またはActionBlock<T>
TPL Dataflowからを使用できます。
HttpClient
尊重する必要がありServicePointManager.DefaultConnectionLimit
ます。