Estoy migrando millones de usuarios de AD local a Azure AD B2C usando la API de MS Graph para crear los usuarios en B2C. He escrito una aplicación de consola .Net Core 3.1 para realizar esta migración. Para acelerar las cosas, estoy haciendo llamadas concurrentes a la API Graph. Esto está funcionando muy bien, más o menos.
Durante el desarrollo, experimenté un rendimiento aceptable al ejecutar desde Visual Studio 2019, pero para la prueba, ejecuto desde la línea de comandos en Powershell 7. Desde Powershell, el rendimiento de las llamadas simultáneas al HttpClient es muy malo. Parece que hay un límite en la cantidad de llamadas simultáneas que HttpClient permite cuando se ejecuta desde Powershell, por lo que las llamadas en lotes concurrentes de más de 40 a 50 solicitudes comienzan a acumularse. Parece estar ejecutando 40 a 50 solicitudes concurrentes mientras bloquea el resto.
No estoy buscando ayuda con la programación asíncrona. Estoy buscando una forma de solucionar la diferencia entre el comportamiento en tiempo de ejecución de Visual Studio y el comportamiento en tiempo de ejecución de la línea de comandos de Powershell. La ejecución en modo de lanzamiento desde el botón de flecha verde de Visual Studio se comporta como se esperaba. Ejecutar desde la línea de comando no lo hace.
Lleno una lista de tareas con llamadas asíncronas y luego espero Task.WhenAll (tareas). Cada llamada toma entre 300 y 400 milisegundos. Cuando se ejecuta desde Visual Studio, funciona como se esperaba. Hago lotes concurrentes de 1000 llamadas y cada una se completa individualmente dentro del tiempo esperado. Todo el bloque de tareas tarda solo unos pocos milisegundos más que la llamada individual más larga.
El comportamiento cambia cuando ejecuto la misma compilación desde la línea de comandos de Powershell. Las primeras 40 a 50 llamadas toman los esperados 300 a 400 milisegundos, pero luego los tiempos de llamada individuales crecen hasta 20 segundos cada uno. Creo que las llamadas se están serializando, por lo que solo se ejecutan de 40 a 50 a la vez mientras los demás esperan.
Después de horas de prueba y error, pude reducirlo al HttpClient. Para aislar el problema, me burlé de las llamadas a HttpClient.SendAsync con un método que realiza Task.Delay (300) y devuelve un resultado simulado. En este caso, la ejecución desde la consola se comporta de manera idéntica a la ejecución desde Visual Studio.
Estoy usando IHttpClientFactory e incluso he intentado ajustar el límite de conexión en ServicePointManager.
Aquí está mi código de registro.
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;
}
Aquí está el 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;
}
}
Aquí está el código que configura las tareas.
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();
Así es como me burlé del 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();
Aquí hay métricas para usuarios de 10k B2C creados a través de GraphAPI utilizando 500 solicitudes simultáneas. Las primeras 500 solicitudes son más largas de lo normal porque se están creando las conexiones TCP.
Aquí hay un enlace a las métricas de ejecución de la consola .
Aquí hay un enlace a las métricas de ejecución de Visual Studio .
Los tiempos de bloqueo en las métricas de ejecución de VS son diferentes de lo que dije en esta publicación porque moví todo el acceso a archivos sincrónicos al final del proceso en un esfuerzo por aislar el código problemático tanto como sea posible para las ejecuciones de prueba.
El proyecto se compila utilizando .Net Core 3.1. Estoy usando Visual Studio 2019 16.4.5.