Estou migrando milhões de usuários do AD no local para o Azure AD B2C usando a API do MS Graph para criar os usuários no B2C. Eu escrevi um aplicativo de console .Net Core 3.1 para executar essa migração. Para acelerar, estou fazendo chamadas simultâneas para a API do Graph. Isso está funcionando muito bem - mais ou menos.
Durante o desenvolvimento, experimentei um desempenho aceitável durante a execução do Visual Studio 2019, mas, para teste, estou executando na linha de comando do Powershell 7. No Powershell, o desempenho de chamadas simultâneas para o HttpClient é muito ruim. Parece que há um limite para o número de chamadas simultâneas permitidas pelo HttpClient ao executar a partir do Powershell; portanto, as chamadas em lotes simultâneos com mais de 40 a 50 solicitações começam a se acumular. Parece estar executando de 40 a 50 solicitações simultâneas enquanto bloqueia o restante.
Não estou procurando ajuda com programação assíncrona. Estou procurando uma maneira de solucionar a diferença entre o comportamento em tempo de execução do Visual Studio e o comportamento em tempo de execução da linha de comando do Powershell. A execução no modo de liberação do botão de seta verde do Visual Studio se comporta conforme o esperado. A execução na linha de comando não.
Encho uma lista de tarefas com chamadas assíncronas e aguardo Task.WhenAll (tarefas). Cada chamada leva entre 300 e 400 milissegundos. Ao executar no Visual Studio, ele funciona conforme o esperado. Faço lotes simultâneos de 1000 chamadas e cada uma delas é concluída individualmente dentro do tempo esperado. Todo o bloco de tarefas leva apenas alguns milissegundos a mais que a chamada individual mais longa.
O comportamento muda quando executo a mesma compilação na linha de comando do Powershell. As primeiras 40 a 50 chamadas levam os esperados 300 a 400 milissegundos, mas os tempos de chamada individuais crescem até 20 segundos cada. Eu acho que as chamadas estão serializando, então apenas 40 a 50 estão sendo executadas de cada vez, enquanto os outros esperam.
Após horas de tentativa e erro, consegui reduzi-lo ao HttpClient. Para isolar o problema, zombei das chamadas para HttpClient.SendAsync com um método que executa Task.Delay (300) e retorna um resultado simulado. Nesse caso, a execução no console se comporta de forma idêntica à execução no Visual Studio.
Estou usando o IHttpClientFactory e até tentei ajustar o limite de conexão no ServicePointManager.
Aqui está o meu 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;
}
Aqui está o 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;
}
}
Aqui está o código que configura as tarefas.
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();
Aqui está como eu zombei do 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();
Aqui estão as métricas para 10k usuários B2C criados via GraphAPI usando 500 solicitações simultâneas. As primeiras 500 solicitações são mais longas que o normal porque as conexões TCP estão sendo criadas.
Aqui está um link para as métricas de execução do console .
Aqui está um link para as métricas de execução do Visual Studio .
Os tempos de bloqueio nas métricas de execução do VS são diferentes do que eu disse neste post porque movi todo o acesso ao arquivo síncrono para o final do processo, em um esforço para isolar o código problemático o máximo possível para as execuções de teste.
O projeto é compilado usando o .Net Core 3.1. Estou usando o Visual Studio 2019 16.4.5.