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.