B2Cでユーザーを作成するためにMS Graph APIを使用して、数百万のユーザーをオンプレミスADからAzure AD B2Cに移行しています。この移行を実行するための.Net Core 3.1コンソールアプリケーションを作成しました。速度を上げるために、Graph APIを同時に呼び出しています。これはうまく機能しています。
開発中にVisual Studio 2019から実行しているときに許容できるパフォーマンスを体験しましたが、テストのためにPowershell 7のコマンドラインから実行しています。Powershellから、HttpClientへの同時呼び出しのパフォーマンスは非常に悪いです。Powershellからの実行時にHttpClientが許可する同時呼び出しの数には制限があるため、40から50を超える同時バッチでの要求がスタックし始めます。残りをブロックしている間、40から50の同時リクエストを実行しているようです。
私は非同期プログラミングの支援を探していません。Visual Studioの実行時の動作とPowershellコマンドラインの実行時の動作の違いをトラブルシューティングする方法を探しています。Visual Studioの緑の矢印ボタンからリリースモードで実行すると、期待どおりに動作します。コマンドラインからの実行ではできません。
タスクリストに非同期呼び出しを入力し、Task.WhenAll(tasks)を待ちます。各呼び出しには300〜400ミリ秒かかります。Visual Studioから実行すると、期待どおりに動作します。私は1000の呼び出しの同時バッチを作成し、それぞれが予想時間内に個別に完了します。タスクブロック全体は、最長の個々の呼び出しよりも数ミリ秒長くかかります。
Powershellコマンドラインから同じビルドを実行すると、動作が変わります。最初の40から50の呼び出しは、予想される300から400ミリ秒かかりますが、その後、個々の呼び出し時間はそれぞれ最大20秒になります。呼び出しはシリアル化されていると思うので、他の呼び出しが待機している間、一度に実行されるのは40〜50だけです。
何時間もの試行錯誤の末、HttpClientに絞り込むことができました。問題を特定するために、Task.Delay(300)を実行してモック結果を返すメソッドを使用して、HttpClient.SendAsyncの呼び出しをモック化しました。この場合、コンソールからの実行は、Visual Studioからの実行と同じように動作します。
私はIHttpClientFactoryを使用していますが、ServicePointManagerの接続制限を調整しようとしました。
これが私の登録コードです。
public static IServiceCollection RegisterHttpClient(this IServiceCollection services, int batchSize)
{
ServicePointManager.DefaultConnectionLimit = batchSize;
ServicePointManager.MaxServicePoints = batchSize;
ServicePointManager.SetTcpKeepAlive(true, 1000, 5000);
services.AddHttpClient(MSGraphRequestManager.HttpClientName, c =>
{
c.Timeout = TimeSpan.FromSeconds(360);
c.DefaultRequestHeaders.Add("User-Agent", "xxxxxxxxxxxx");
})
.ConfigurePrimaryHttpMessageHandler(() => new DefaultHttpClientHandler(batchSize));
return services;
}
これがDefaultHttpClientHandlerです。
internal class DefaultHttpClientHandler : HttpClientHandler
{
public DefaultHttpClientHandler(int maxConnections)
{
this.MaxConnectionsPerServer = maxConnections;
this.UseProxy = false;
this.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
}
}
タスクを設定するコードは次のとおりです。
var timer = Stopwatch.StartNew();
var tasks = new Task<(UpsertUserResult, TimeSpan)>[users.Length];
for (var i = 0; i < users.Length; ++i)
{
tasks[i] = this.CreateUserAsync(users[i]);
}
var results = await Task.WhenAll(tasks);
timer.Stop();
これがHttpClientをモックアウトした方法です。
var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
#if use_http
using var response = await httpClient.SendAsync(request);
#else
await Task.Delay(300);
var graphUser = new User { Id = "mockid" };
using var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(graphUser)) };
#endif
var responseContent = await response.Content.ReadAsStringAsync();
500の同時リクエストを使用してGraphAPI経由で作成された10k B2Cユーザーのメトリックを次に示します。TCP接続が作成されているため、最初の500リクエストは通常より長くなります。
これは、コンソール実行メトリックへのリンクです。
Visual Studioの実行メトリックへのリンクは次のとおりです。
テストの実行で問題のあるコードをできるだけ分離するために、すべての同期ファイルアクセスをプロセスの最後に移動したため、VS実行メトリックのブロック時間はこの投稿で述べたものとは異なります。
プロジェクトは.Net Core 3.1を使用してコンパイルされます。Visual Studio 2019 16.4.5を使用しています。