Ich migriere Millionen von Benutzern von On-Prem-AD zu Azure AD B2C mithilfe der MS Graph-API, um die Benutzer in B2C zu erstellen. Ich habe eine .Net Core 3.1-Konsolenanwendung geschrieben, um diese Migration durchzuführen. Um die Dinge zu beschleunigen, rufe ich gleichzeitig die Graph-API auf. Das funktioniert großartig.
Während der Entwicklung wurde beim Ausführen von Visual Studio 2019 eine akzeptable Leistung festgestellt, aber zum Testen werde ich in Powershell 7 über die Befehlszeile ausgeführt. In Powershell ist die Leistung gleichzeitiger Aufrufe des HttpClient sehr schlecht. Es scheint, dass die Anzahl der gleichzeitigen Aufrufe, die HttpClient beim Ausführen von Powershell zulässt, begrenzt ist. Daher werden Anrufe in gleichzeitigen Stapeln mit mehr als 40 bis 50 Anforderungen gestapelt. Es scheint 40 bis 50 gleichzeitige Anforderungen auszuführen, während der Rest blockiert wird.
Ich suche keine Unterstützung bei der asynchronen Programmierung. Ich suche nach einer Möglichkeit, den Unterschied zwischen dem Laufzeitverhalten von Visual Studio und dem Laufzeitverhalten der Powershell-Befehlszeile zu beheben. Das Ausführen im Release-Modus über die grüne Pfeiltaste von Visual Studio verhält sich wie erwartet. Das Ausführen über die Befehlszeile funktioniert nicht.
Ich fülle eine Aufgabenliste mit asynchronen Aufrufen und warte dann auf Task.WhenAll (Aufgaben). Jeder Anruf dauert zwischen 300 und 400 Millisekunden. Unter Visual Studio funktioniert es wie erwartet. Ich mache gleichzeitig Stapel von 1000 Anrufen und jeder wird innerhalb der erwarteten Zeit einzeln abgeschlossen. Der gesamte Taskblock dauert nur wenige Millisekunden länger als der längste Einzelaufruf.
Das Verhalten ändert sich, wenn ich denselben Build über die Powershell-Befehlszeile ausführe. Die ersten 40 bis 50 Anrufe dauern die erwarteten 300 bis 400 Millisekunden, aber dann werden die einzelnen Anrufzeiten jeweils auf 20 Sekunden erhöht. Ich denke, die Anrufe werden serialisiert, sodass nur 40 bis 50 gleichzeitig ausgeführt werden, während die anderen warten.
Nach stundenlangem Ausprobieren konnte ich es auf den HttpClient eingrenzen. Um das Problem einzugrenzen, habe ich die Aufrufe von HttpClient.SendAsync mit einer Methode verspottet, die Task.Delay (300) ausführt und ein Scheinergebnis zurückgibt. In diesem Fall verhält sich das Ausführen von der Konsole aus identisch mit dem Ausführen von Visual Studio.
Ich verwende IHttpClientFactory und habe sogar versucht, das Verbindungslimit in ServicePointManager anzupassen.
Hier ist mein Registrierungscode.
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;
}
Hier ist der 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;
}
}
Hier ist der Code, der die Aufgaben einrichtet.
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();
So habe ich den HttpClient verspottet.
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();
Hier finden Sie Metriken für 10.000 B2C-Benutzer, die über GraphAPI mit 500 gleichzeitigen Anforderungen erstellt wurden. Die ersten 500 Anforderungen sind länger als normal, da die TCP-Verbindungen erstellt werden.
Hier ist ein Link zu den Konsolenlaufmetriken .
Hier ist ein Link zu den Visual Studio-Ausführungsmetriken .
Die Blockierungszeiten in den VS-Laufmetriken unterscheiden sich von den Angaben in diesem Beitrag, da ich den gesamten synchronen Dateizugriff an das Ende des Prozesses verschoben habe, um den problematischen Code für die Testläufe so weit wie möglich zu isolieren.
Das Projekt wird mit .Net Core 3.1 kompiliert. Ich verwende Visual Studio 2019 16.4.5.
quelle
Antworten:
Zwei Dinge fallen mir ein. Die meisten Microsoft Powershell wurden in Version 1 und 2 geschrieben. Version 1 und 2 haben System.Threading.Thread.ApartmentState von MTA. In den Versionen 3 bis 5 wurde der Apartmentstatus standardmäßig in STA geändert.
Der zweite Gedanke ist, dass sie System.Threading.ThreadPool verwenden, um die Threads zu verwalten. Wie groß ist dein Threadpool?
Wenn diese das Problem nicht lösen, beginnen Sie unter System.Threading zu graben.
Als ich Ihre Frage las, dachte ich an diesen Blog. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455
Ein Kollege demonstrierte mit einem Beispielprogramm, das tausend Arbeitselemente erstellt, von denen jedes einen Netzwerkanruf simuliert, dessen Abschluss 500 ms dauert. In der ersten Demonstration blockierten die Netzwerkaufrufe synchrone Anrufe, und das Beispielprogramm beschränkte den Thread-Pool auf zehn Threads, um den Effekt deutlicher zu machen. In dieser Konfiguration wurden die ersten Arbeitselemente schnell an Threads gesendet, aber dann begann sich die Latenz zu erhöhen, da keine Threads mehr verfügbar waren, um neue Arbeitselemente zu warten, sodass die verbleibenden Arbeitselemente immer länger auf einen Thread warten mussten verfügbar werden, um es zu warten. Die durchschnittliche Latenz zum Start des Arbeitselements betrug mehr als zwei Minuten.
Update 1: Ich habe PowerShell 7.0 über das Startmenü ausgeführt und der Thread-Status war STA. Ist der Thread-Status in beiden Versionen unterschiedlich?
Update 2: Ich wünsche eine bessere Antwort, aber Sie müssen die beiden Umgebungen vergleichen, bis etwas auffällt.
Update 3:
https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient
Vergleichen Sie einfach die beiden Umgebungen weiter und das Problem sollte auffallen
quelle