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.
fonte
Respostas:
Duas coisas vem a mente. A maioria dos microsoft powershell foi escrita nas versões 1 e 2. As versões 1 e 2 têm System.Threading.Thread.ApartmentState do MTA. Nas versões 3 a 5, o estado do apartamento foi alterado para STA por padrão.
O segundo pensamento é que parece que eles estão usando System.Threading.ThreadPool para gerenciar os threads. Qual é o tamanho do seu pool de threads?
Se isso não resolver o problema, comece a cavar em System.Threading.
Quando li sua pergunta, pensei neste blog. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455
Um colega demonstrou com um programa de amostra que cria mil itens de trabalho, cada um dos quais simula uma chamada de rede que leva 500 ms para ser concluída. Na primeira demonstração, as chamadas de rede estavam bloqueando as chamadas síncronas e o programa de amostra limitou o pool de threads a dez threads, a fim de tornar o efeito mais aparente. Sob essa configuração, os primeiros itens de trabalho foram despachados rapidamente para os threads, mas a latência começou a aumentar, pois não havia mais threads disponíveis para atender a novos itens de trabalho; portanto, os itens de trabalho restantes tiveram que esperar mais e mais tempo para que um thread fosse fique disponível para repará-lo. A latência média para o início do item de trabalho foi superior a dois minutos.
Atualização 1: executei o PowerShell 7.0 no menu Iniciar e o estado do thread era STA. O estado do encadeamento é diferente nas duas versões?
Atualização 2: Desejo uma resposta melhor, mas você terá que comparar os dois ambientes até que algo se destaque.
Atualização 3:
https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient
Continue comparando os dois ambientes e o problema deve se destacar
fonte