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.