Migruję miliony użytkowników z tymczasowej usługi AD do usługi Azure AD B2C za pomocą interfejsu API MS Graph, aby utworzyć użytkowników w B2C. Napisałem aplikację konsolową .Net Core 3.1 do przeprowadzenia tej migracji. Aby przyspieszyć, wykonuję jednoczesne połączenia z Graph API. To działa świetnie - w pewnym sensie.
Podczas programowania doświadczyłem zadowalającej wydajności podczas uruchamiania z Visual Studio 2019, ale do testu uruchamiam z wiersza poleceń w Powershell 7. Z Powershell wydajność jednoczesnych wywołań do HttpClient jest bardzo zła. Wygląda na to, że istnieje ograniczenie liczby jednoczesnych wywołań, na które HttpClient zezwala podczas uruchamiania z Powershell, więc wywołania w równoległych partiach większych niż 40 do 50 żądań zaczynają się nakładać. Wygląda na to, że blokuje pozostałe 40 do 50 równoczesnych żądań.
Nie szukam pomocy w programowaniu asynchronicznym. Szukam sposobu, aby rozwiązać problem z różnicą między zachowaniem w czasie wykonywania programu Visual Studio a zachowaniem w wierszu polecenia programu PowerShell. Uruchamianie w trybie zwolnienia z zielonego przycisku strzałki w programie Visual Studio działa zgodnie z oczekiwaniami. Uruchamianie z wiersza poleceń nie.
Wypełniam listę zadań wywołaniami asynchronicznymi, a następnie oczekuję na Task.WhenAll (zadania). Każde połączenie trwa od 300 do 400 milisekund. Podczas uruchamiania z Visual Studio działa zgodnie z oczekiwaniami. Wykonuję równoczesne partie 1000 połączeń i każda z nich kończy się indywidualnie w oczekiwanym czasie. Cały blok zadań zajmuje tylko kilka milisekund dłużej niż najdłuższe indywidualne wywołanie.
Zachowanie zmienia się, gdy uruchamiam tę samą kompilację z wiersza polecenia Powershell. Pierwsze 40 do 50 połączeń zajmuje oczekiwane 300 do 400 milisekund, ale potem poszczególne czasy połączeń rosną do 20 sekund. Myślę, że połączenia są serializowane, więc tylko 40 do 50 jest wykonywanych jednocześnie, podczas gdy inni czekają.
Po wielu godzinach prób i błędów udało mi się zawęzić go do HttpClient. Aby wyodrębnić problem, wyśmiewałem wywołania HttpClient.SendAsync metodą, która wykonuje Task.Delay (300) i zwraca próbny wynik. W takim przypadku uruchamianie z konsoli zachowuje się identycznie jak uruchamianie z Visual Studio.
Korzystam z IHttpClientFactory, a nawet próbowałem dostosować limit połączeń w ServicePointManager.
Oto mój kod rejestracyjny.
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;
}
Oto 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;
}
}
Oto kod konfigurujący zadania.
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();
Oto jak wyśmiewałem 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();
Oto miary dla 10 000 użytkowników B2C utworzonych za pomocą GraphAPI przy użyciu 500 równoczesnych żądań. Pierwsze 500 żądań jest dłuższych niż zwykle, ponieważ tworzone są połączenia TCP.
Oto link do wskaźników uruchamiania konsoli .
Oto link do metryk uruchamiania programu Visual Studio .
Czasy blokowania w pomiarach przebiegu VS są inne niż w tym, co powiedziałem w tym poście, ponieważ przeniosłem cały dostęp do pliku synchronicznego na koniec procesu, aby jak najbardziej odizolować problematyczny kod dla przebiegów testowych.
Projekt jest kompilowany przy użyciu .Net Core 3.1. Używam Visual Studio 2019 16.4.5.