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.
źródło
Odpowiedzi:
Przychodzą mi na myśl dwie rzeczy. Większość Microsoft PowerShell został napisany w wersji 1 i 2. Wersja 1 i 2 mają System.Threading.Thread.ApartmentState MTA. W wersjach od 3 do 5 stan mieszkania domyślnie zmienił się na STA.
Druga myśl to brzmi, jakby używali System.Threading.ThreadPool do zarządzania wątkami. Jak duży jest twój wątek?
Jeśli to nie rozwiąże problemu, zacznij kopać w System.Threading.
Kiedy przeczytałem twoje pytanie, pomyślałem o tym blogu. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455
Współpracownik zademonstrował przykładowy program, który tworzy tysiąc elementów pracy, z których każdy symuluje połączenie sieciowe, które trwa 500 ms. W pierwszej demonstracji wywołania sieciowe blokowały wywołania synchroniczne, a przykładowy program ograniczył pulę wątków do dziesięciu wątków, aby efekt był bardziej widoczny. W tej konfiguracji kilka pierwszych elementów pracy zostało szybko wysłanych do wątków, ale potem zaczęło się budować opóźnienie, ponieważ nie było już dostępnych wątków do obsługi nowych elementów pracy, więc pozostałe elementy pracy musiały czekać dłużej i dłużej na wątek stają się dostępne do obsługi. Średni czas oczekiwania na rozpoczęcie pracy wynosił ponad dwie minuty.
Aktualizacja 1: Uruchomiłem PowerShell 7.0 z menu Start, a stan wątku to STA. Czy stan wątku jest inny w dwóch wersjach?
Aktualizacja 2: Chciałbym lepszej odpowiedzi, ale będziesz porównywał oba środowiska, aż coś się wyróżni.
Aktualizacja 3:
https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient
Po prostu porównuj oba środowiska, a problem powinien się wyróżniać
źródło