Das gleichzeitige Verhalten von HttpClient unterscheidet sich bei der Ausführung in Powershell von dem in Visual Studio

10

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.

Mark Lauter
quelle
2
Haben Sie den Status Ihrer Verbindungen mit dem Dienstprogramm netstat nach dem ersten Stapel überprüft? Möglicherweise erhalten Sie einen Einblick in die Vorgänge nach Abschluss der ersten Aufgaben.
Pranav Negandhi
Wenn Sie es nicht auf diese Weise auflösen (die HTTP-Anforderung asynchronisieren), können Sie immer synchronisierte HTTP-Aufrufe für jeden Benutzer in einer ConcurrentQueue [Objekt] -Produktor / Produzent-Parallelität verwenden. Ich habe dies kürzlich für ungefähr 200 Millionen Dateien in PowerShell getan.
thepip3r
1
@ thepip3r Ich habe gerade Ihre Empfehlung erneut gelesen und diesmal verstanden. Ich werde mir das merken.
Mark Lauter
1
Nein, ich sage, wenn Sie PowerShell anstelle von c # verwenden möchten : leeholmes.com/blog/2018/09/05/… .
thepip3r
1
@ thepip3r Lies einfach den Blogeintrag von Stephen Cleary. Ich sollte gut sein
Mark Lauter

Antworten:

3

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?

PS C:\Program Files\PowerShell\7>  [System.Threading.Thread]::CurrentThread

ManagedThreadId    : 12
IsAlive            : True
IsBackground       : False
IsThreadPoolThread : False
Priority           : Normal
ThreadState        : Running
CurrentCulture     : en-US
CurrentUICulture   : en-US
ExecutionContext   : System.Threading.ExecutionContext
Name               : Pipeline Execution Thread
ApartmentState     : STA

Update 2: Ich wünsche eine bessere Antwort, aber Sie müssen die beiden Umgebungen vergleichen, bis etwas auffällt.

PS C:\Windows\system32> [System.Net.ServicePointManager].GetProperties() | select name

Name                               
----                               
SecurityProtocol                   
MaxServicePoints                   
DefaultConnectionLimit             
MaxServicePointIdleTime            
UseNagleAlgorithm                  
Expect100Continue                  
EnableDnsRoundRobin                
DnsRefreshTimeout                  
CertificatePolicy                  
ServerCertificateValidationCallback
ReusePort                          
CheckCertificateRevocationList     
EncryptionPolicy            

Update 3:

https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient

Darüber hinaus verwendet jede HttpClient-Instanz ihren eigenen Verbindungspool, wodurch ihre Anforderungen von Anforderungen isoliert werden, die von anderen HttpClient-Instanzen ausgeführt werden.

Wenn eine App, die HttpClient und verwandte Klassen im Windows.Web.Http-Namespace verwendet, große Datenmengen (50 Megabyte oder mehr) herunterlädt, sollte die App diese Downloads streamen und nicht die Standardpufferung verwenden. Wenn die Standardpufferung verwendet wird, wird die Client-Speichernutzung sehr groß, was möglicherweise zu einer verringerten Leistung führt.

Vergleichen Sie einfach die beiden Umgebungen weiter und das Problem sollte auffallen

Add-Type -AssemblyName System.Net.Http
$client = New-Object -TypeName System.Net.Http.Httpclient
$client | format-list *

DefaultRequestHeaders        : {}
BaseAddress                  : 
Timeout                      : 00:01:40
MaxResponseContentBufferSize : 2147483647
Aaron
quelle
Bei Ausführung in Powershell 7.0 gibt System.Threading.Thread.CurrentThread.GetApartmentState () MTA aus Program.Main () zurück
Mark Lauter
Der Standard-Min-Thread-Pool war 12, ich habe versucht, die Min-Pool-Größe auf meine Stapelgröße zu erhöhen (500 zum Testen). Dies hatte keinen Einfluss auf das Verhalten.
Mark Lauter
Wie viele Threads werden in beiden Umgebungen generiert?
Aaron
Ich habe mich gefragt, wie viele Threads der 'HttpClient' hat, weil er alles an der Arbeit erledigt.
Aaron
Wie ist der Wohnungszustand in beiden Versionen?
Aaron