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.
la source
Réponses:
Deux choses viennent à l'esprit. La plupart des microsoft powershell ont été écrits dans les versions 1 et 2. Les versions 1 et 2 ont System.Threading.Thread.ApartmentState de MTA. Dans les versions 3 à 5, l'état de l'appartement est devenu STA par défaut.
La deuxième pensée est qu'il semble qu'ils utilisent System.Threading.ThreadPool pour gérer les threads. Quelle est la taille de votre pool de threads?
Si ceux-ci ne résolvent pas le problème, commencez à creuser sous System.Threading.
Quand j'ai lu votre question, j'ai pensé à ce blog. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455
Un collègue a fait une démonstration avec un exemple de programme qui crée un millier d'éléments de travail, chacun simulant un appel réseau qui prend 500 ms. Dans la première démonstration, les appels réseau bloquaient les appels synchrones et l'exemple de programme a limité le pool de threads à dix threads afin de rendre l'effet plus apparent. Sous cette configuration, les premiers éléments de travail ont été rapidement distribués aux threads, mais la latence a commencé à se créer car il n'y avait plus de threads disponibles pour gérer les nouveaux éléments de travail, de sorte que les autres éléments de travail devaient attendre de plus en plus longtemps pour qu'un thread soit devenir disponible pour le réparer. La latence moyenne au début de l'élément de travail était supérieure à deux minutes.
Mise à jour 1: j'ai exécuté PowerShell 7.0 à partir du menu Démarrer et l'état du thread était STA. L'état du thread est-il différent dans les deux versions?
Mise à jour 2: je souhaite une meilleure réponse, mais vous devrez comparer les deux environnements jusqu'à ce que quelque chose se démarque.
Mise à jour 3:
https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient
Continuez simplement à comparer les deux environnements et le problème devrait ressortir
la source