Je migre des millions d'utilisateurs d'AD sur site vers Azure AD B2C à l'aide de l'API MS Graph pour créer les utilisateurs dans B2C. J'ai écrit une application console .Net Core 3.1 pour effectuer cette migration. Pour accélérer les choses, je fais des appels simultanés à l'API Graph. Cela fonctionne très bien - en quelque sorte.
Pendant le développement, j'ai rencontré des performances acceptables lors de l'exécution à partir de Visual Studio 2019, mais pour le test, j'exécute à partir de la ligne de commande dans Powershell 7. À partir de Powershell, les performances des appels simultanés à HttpClient sont très mauvaises. Il semble qu'il y ait une limite au nombre d'appels simultanés que HttpClient autorise lors de l'exécution à partir de Powershell, donc les appels en lots simultanés supérieurs à 40 à 50 demandes commencent à s'empiler. Il semble exécuter 40 à 50 requêtes simultanées tout en bloquant le reste.
Je ne cherche pas d'aide pour la programmation asynchrone. Je cherche un moyen de résoudre la différence entre le comportement d'exécution de Visual Studio et le comportement d'exécution de la ligne de commande Powershell. L'exécution en mode de libération à partir du bouton fléché vert de Visual Studio se comporte comme prévu. L'exécution à partir de la ligne de commande ne fonctionne pas.
Je remplis une liste de tâches avec des appels asynchrones, puis j'attends Task.WhenAll (tâches). Chaque appel prend entre 300 et 400 millisecondes. Lors de l'exécution à partir de Visual Studio, cela fonctionne comme prévu. Je fais des lots simultanés de 1000 appels et chacun se termine individuellement dans le délai prévu. L'ensemble du bloc de tâches ne prend que quelques millisecondes de plus que l'appel individuel le plus long.
Le comportement change lorsque j'exécute la même version à partir de la ligne de commande Powershell. Les 40 à 50 premiers appels prennent les 300 à 400 millisecondes attendus, mais la durée des appels individuels augmente jusqu'à 20 secondes chacun. Je pense que les appels sérialisent, donc seulement 40 à 50 sont exécutés à la fois pendant que les autres attendent.
Après des heures d'essais et d'erreurs, j'ai pu le réduire au HttpClient. Pour isoler le problème, j'ai simulé les appels à HttpClient.SendAsync avec une méthode qui exécute Task.Delay (300) et renvoie un résultat factice. Dans ce cas, l'exécution à partir de la console se comporte de manière identique à l'exécution à partir de Visual Studio.
J'utilise IHttpClientFactory et j'ai même essayé d'ajuster la limite de connexion sur ServicePointManager.
Voici mon code d'enregistrement.
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;
}
Voici le 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;
}
}
Voici le code qui définit les tâches.
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();
Voici comment je me suis moqué du 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();
Voici les mesures pour les utilisateurs B2C 10k créés via GraphAPI en utilisant 500 demandes simultanées. Les 500 premières demandes sont plus longues que la normale car les connexions TCP sont en cours de création.
Voici un lien vers les métriques d'exécution de la console .
Voici un lien vers les métriques d'exécution de Visual Studio .
Les temps de blocage dans les métriques d'exécution VS sont différents de ce que j'ai dit dans ce post parce que j'ai déplacé tous les accès aux fichiers synchrones à la fin du processus dans le but d'isoler le code problématique autant que possible pour les tests.
Le projet est compilé à l'aide de .Net Core 3.1. J'utilise Visual Studio 2019 16.4.5.