Comportamento simultaneo di HttpClient diverso durante l'esecuzione in Powershell rispetto a Visual Studio


10

Sto migrando milioni di utenti da AD locale ad Azure AD B2C usando l'API MS Graph per creare gli utenti in B2C. Ho scritto un'applicazione console .Net Core 3.1 per eseguire questa migrazione. Per velocizzare le cose, sto effettuando chiamate simultanee all'API Graph. Funziona benissimo.

Durante lo sviluppo ho riscontrato prestazioni accettabili durante l'esecuzione da Visual Studio 2019, ma per il test sto eseguendo dalla riga di comando in Powershell 7. Da Powershell le prestazioni delle chiamate simultanee a HttpClient sono pessime. Sembra che ci sia un limite al numero di chiamate simultanee che HttpClient consente durante l'esecuzione da Powershell, quindi le chiamate in batch simultanei con più di 40-50 richieste iniziano ad accumularsi. Sembra che stia eseguendo da 40 a 50 richieste simultanee mentre blocca il resto.

Non sto cercando assistenza con la programmazione asincrona. Sto cercando un modo per risolvere la differenza tra il comportamento di runtime di Visual Studio e il comportamento di runtime della riga di comando di Powershell. L'esecuzione in modalità di rilascio dal pulsante freccia verde di Visual Studio si comporta come previsto. L'esecuzione dalla riga di comando no.

Riempo un elenco di attività con chiamate asincrone e quindi aspetto Task.WhenAll (attività). Ogni chiamata dura tra 300 e 400 millisecondi. Durante l'esecuzione da Visual Studio funziona come previsto. Faccio batch simultanei di 1000 chiamate e ognuno di essi viene completato individualmente entro il tempo previsto. L'intero blocco di attività richiede solo pochi millisecondi in più rispetto alla singola chiamata più lunga.

Il comportamento cambia quando eseguo la stessa build dalla riga di comando di Powershell. Le prime 40-50 chiamate impiegano dai 300 ai 400 millisecondi previsti, ma i tempi delle singole chiamate aumentano fino a 20 secondi ciascuno. Penso che le chiamate stiano serializzando, quindi solo da 40 a 50 vengono eseguite alla volta mentre gli altri attendono.

Dopo ore di tentativi ed errori sono stato in grado di restringerlo a HttpClient. Per isolare il problema ho deriso le chiamate a HttpClient.SendAsync con un metodo che esegue Task.Delay (300) e restituisce un risultato fittizio. In questo caso l'esecuzione dalla console si comporta in modo identico all'esecuzione da Visual Studio.

Sto usando IHttpClientFactory e ho anche provato a regolare il limite di connessione su ServicePointManager.

Ecco il mio codice di registrazione.

    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;
    }

Ecco 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;
    }
}

Ecco il codice che imposta le attività.

        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();

Ecco come ho deriso 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();

Ecco le metriche per gli utenti 10k B2C creati tramite GraphAPI utilizzando 500 richieste simultanee. Le prime 500 richieste sono più lunghe del normale perché vengono create le connessioni TCP.

Ecco un link alle metriche di esecuzione della console .

Ecco un collegamento alle metriche di esecuzione di Visual Studio .

I tempi di blocco nelle metriche di esecuzione VS sono diversi da quelli che ho detto in questo post perché ho spostato tutto il file di accesso sincrono alla fine del processo nel tentativo di isolare il codice problematico il più possibile per le esecuzioni di test.

Il progetto è compilato usando .Net Core 3.1. Sto usando Visual Studio 2019 16.4.5.


2
Hai rivisto lo stato delle tue connessioni con l'utilità netstat dopo il primo batch? Potrebbe fornire alcune informazioni su cosa sta succedendo dopo che le prime attività sono state completate.
Pranav Negandhi,

Se non si finisce per risolverlo in questo modo (Asincronizzare la richiesta HTTP), è sempre possibile utilizzare la sincronizzazione delle chiamate HTTP per ciascun utente in un parallelismo consumatore / produttore [oggetto] ConcurrentQueue. Di recente l'ho fatto per circa 200 milioni di file in PowerShell.
thepip3r

1
@ thepip3r Ho appena riletto il tuo encomio e l'ho capito questa volta. Lo terro 'a mente.
Mark Lauter,

1
No, sto dicendo che se volevi usare PowerShell invece di c #: leeholmes.com/blog/2018/09/05/… .
thepip3r

1
@ thepip3r Ho appena letto il post sul blog di Stephen Cleary. Dovrei essere bravo.
Mark Lauter,

Risposte:


3

Due cose vengono in mente. La maggior parte dei microsoft powershell è stata scritta nella versione 1 e 2. Le versioni 1 e 2 hanno System.Threading.Thread.ApartmentState di MTA. Nella versione da 3 a 5 lo stato dell'appartamento è cambiato in STA per impostazione predefinita.

Il secondo pensiero è che stanno usando System.Threading.ThreadPool per gestire i thread. Quanto è grande il tuo threadpool?

Se quelli non risolvono il problema, inizia a scavare in System.Threading.

Quando ho letto la tua domanda ho pensato a questo blog. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

Un collega ha dimostrato con un programma di esempio che crea un migliaio di elementi di lavoro, ognuno dei quali simula una chiamata di rete che richiede 500 ms per il completamento. Nella prima dimostrazione, le chiamate di rete bloccavano le chiamate sincrone e il programma di esempio limitava il pool di thread a dieci thread per rendere l'effetto più evidente. Con questa configurazione, i primi pochi elementi di lavoro venivano spediti rapidamente ai thread, ma poi la latenza iniziava a svilupparsi poiché non c'erano più thread disponibili per servire i nuovi elementi di lavoro, quindi gli elementi di lavoro rimanenti dovevano attendere più a lungo per un thread diventare disponibile per l'assistenza. La latenza media all'inizio dell'articolo di lavoro è stata di oltre due minuti.

Aggiornamento 1: ho eseguito PowerShell 7.0 dal menu Start e lo stato del thread era STA. Lo stato del thread è diverso nelle due versioni?

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

Aggiornamento 2: desidero una risposta migliore, ma dovrai confrontare i due ambienti fino a quando qualcosa non si distingue.

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

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

Aggiornamento 3:

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

Inoltre, ogni istanza di HttpClient utilizza il proprio pool di connessioni, isolando le sue richieste dalle richieste eseguite da altre istanze di HttpClient.

Se un'app che utilizza HttpClient e le classi correlate nello spazio dei nomi Windows.Web.Http scarica grandi quantità di dati (almeno 50 megabyte), allora l'app dovrebbe eseguire lo streaming di tali download e non utilizzare il buffering predefinito. Se si utilizza il buffering predefinito, l'utilizzo della memoria del client diventerà molto elevato, con conseguente potenziale riduzione delle prestazioni.

Continua a confrontare i due ambienti e il problema dovrebbe emergere

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

Quando si esegue Powershell 7.0 System.Threading.Thread.CurrentThread.GetApartmentState () restituisce MTA da Program.Main ()
Mark Lauter il

Il pool di thread min predefinito era 12, ho provato ad aumentare la dimensione del pool minimo alla dimensione del mio batch (500 per il test). Ciò non ha avuto alcun effetto sul comportamento.
Mark Lauter,

Quanti thread vengono generati in entrambi gli ambienti?
Aaron,

Mi chiedevo quanti thread ha 'HttpClient' perché sta facendo tutto sul lavoro.
Aaron,

Qual è lo stato dell'appartamento in entrambe le versioni?
Aaron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.