J'ai récemment créé une application simple pour tester l'appel HTTP débit qui peut être généré de manière asynchrone par rapport à un classique multithread approche.
L'application est capable d'effectuer un nombre prédéfini de HTTP appels et à la fin il affiche le temps total nécessaire pour les effectuer. Au cours de mes tests, tous HTTP appels ont été faits à mon IIS local sever et ils ont récupéré un petit fichier texte (12 octets la taille).
La partie la plus importante du code pour la mise en œuvre asynchrone est indiquée ci-dessous:
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
La partie la plus importante du multithreading la mise en œuvre est répertoriée ci-dessous:
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
Exécuter les tests ont révélé que la version multithread a été plus rapide. Il prit autour de 0,6 secondes pour les 10k demandes, tandis que les async on a pris environ 2 secondes pour la même quantité de charge. C'était un peu une surprise, car je m'attendais au asynchrone à être plus rapide. Peut-être que c'était à cause du fait que mon HTTP appels ont été très rapide. Dans un scénario réel, où le serveur doit effectuer un plus significatif de fonctionnement et d'où il devrait également y avoir de la latence du réseau, les résultats pourraient être inversées.
Par contre, ce qui m'intéresse est la façon dont HttpClient se comporte lorsque la charge est augmentée. Depuis cela, il faut environ 2 secondes pour livrer 10k messages, je pensais que ça allait prendre environ 20 secondes pour offrir 10 fois le nombre de messages, mais l'exécution de l'essai a montré qu'il a besoin d'environ 50 secondes pour livrer les 100 messages. En outre, il prend habituellement plus de 2 minutes pour livrer 200k messages et souvent, quelques milliers d'entre eux (3-4k) échoue avec l'exception suivante:
Une opération sur un socket n'a pas pu être effectuée car le système n'avait pas suffisamment d'espace mémoire tampon ou à cause d'une file d'attente était pleine.
J'ai vérifié les journaux IIS et les opérations qui ont échoué n'a jamais pu le serveur. Ils ont échoué dans le client. J'ai couru les tests sur une machine Windows 7 avec la valeur par défaut plage de ports éphémères de 49152 à 65535. L'exécution de la commande netstat a montré que près de 5-6k ports ont été utilisés lors des tests, donc en théorie il devrait y avoir beaucoup d'autres disponibles. Si le manque de ports a été en effet la cause des exceptions, cela signifie que soit netstat n'avait pas bien rendre compte de la situation ou HttClient utilise seulement un nombre maximal de ports après quoi il commence à lancer des exceptions.
En revanche, le multithread approche de la génération de HTTP appels se sont comportés très prévisible. Je l'ai pris environ 0,6 secondes pour les 10k messages, autour de 5,5 secondes pour 100 messages et comme prévu autour de 55 secondes pour 1 million de messages. Aucun des messages d'échec. De plus, tandis qu'il courait, il n'a jamais utilisé de plus de 55 MO de RAM (selon le Gestionnaire des Tâches de Windows). La mémoire utilisée lors de l'envoi de messages en mode asynchrone a augmenté proportionnellement à la charge. Il a utilisé environ 500 MO de RAM au cours de l'200k messages de tests.
Je pense qu'il y a deux raisons principales pour lesquelles les résultats ci-dessus. La première est que HttpClient semble être très gourmand en créant de nouvelles connexions avec le serveur. Le nombre élevé de ports utilisés rapportés par netstat signifie qu'il n'a probablement pas grand chose à tirer de HTTP keep-alive.
La seconde est que HttpClient ne semble pas avoir un mécanisme de limitation. En fait, cela semble être un problème général lié à des opérations asynchrones. Si vous avez besoin d'effectuer un très grand nombre d'opérations, ils seront tous commencé à la fois, et leurs suites seront exécutées dès qu'ils seront disponibles. En théorie, cela devrait être ok, parce que dans des opérations asynchrones de la charge sur des systèmes externes, mais comme prouvé ci-dessus, ce n'est pas tout à fait le cas. Avoir un grand nombre de demandes commencé à la fois permettra d'accroître l'utilisation de la mémoire et ralentir la totalité de l'exécution.
J'ai réussi à obtenir de meilleurs résultats, de la mémoire et temps d'exécution des sages, en limitant le nombre maximal de requêtes asynchrones avec un simple mais primitive mécanisme de délai:
public async void TestAsyncWithDelay()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
await Task.Delay(DELAY_TIME);
ProcessUrlAsyncWithReqCount(httpClient);
}
}
Il serait vraiment utile si HttpClient inclus un mécanisme pour limiter le nombre de requêtes simultanées. Lors de l'utilisation de la Tâche de la classe (qui est basé sur l' .Net pool de threads) la limitation est réalisé automatiquement en limitant le nombre de threads simultanés.
Pour un aperçu complet, j'ai aussi créé une version de l'async test basé sur HttpWebRequest plutôt que HttpClient et a réussi à obtenir de bien meilleurs résultats. Pour commencer, il permet de fixer une limite sur le nombre de connexions simultanées (avec ServicePointManager.DefaultConnectionLimit ou par l'intermédiaire de la configuration), ce qui signifie qu'il n'a jamais manqué de ports et de ne jamais échoué sur toute demande (HttpClient, par défaut, est basé sur HttpWebRequest, mais il semble ignorer la limite de connexion).
L'async HttpWebRequest approche était encore d'environ 50% à 60% plus lent que le multithreading, mais c'était prévisible et fiable. Le seul inconvénient c'est qu'il a utilisé une grande quantité de mémoire, en vertu de grosse charge. Par exemple, il est nécessaire autour de 1,6 GO pour l'envoi de 1 million de demandes. En limitant le nombre de requêtes simultanées (comme je l'ai fait ci-dessus pour HttpClient), j'ai réussi à réduire la mémoire utilisée à seulement 20 MO et d'obtenir un temps d'exécution de seulement 10% plus lent que l'approche de multithreading.
Après cette longue présentation, mes questions sont: Est la classe HttpClient .Net 4.5 un mauvais choix de l'intensité de charge des applications? Est-il possible de gaz, qui devrait corriger les problèmes que j'ai mentionné à ce sujet? Comment au sujet de la async saveur de HttpWebRequest?
Mise à jour (merci @Stephen Cleary)
Comme il s'avère, HttpClient, tout comme HttpWebRequest (sur lesquels elle est fondée par défaut), peut avoir son nombre de connexions simultanées sur le même hôte limitée avec ServicePointManager.DefaultConnectionLimit. La chose étrange est que, selon MSDN, la valeur par défaut pour la connexion de la limite est de 2. J'ai aussi vérifié que de mon côté en utilisant le débogueur qui fait qu'en effet 2 est la valeur par défaut. Cependant, il semble que, à moins de définir explicitement une valeur pour ServicePointManager.DefaultConnectionLimit, la valeur par défaut sera ignoré. Puisque je n'ai pas explicitement une valeur pour le cours de mes HttpClient tests, j'ai pensé qu'il était ignoré.
Après le réglage de ServicePointManager.DefaultConnectionLimit à 100 HttpClient est devenu fiable et prévisible (netstat confirme que seulement 100 ports sont utilisés). Il est encore plus lent que async HttpWebRequest (environ 40%), mais étrangement, il utilise moins de mémoire. Pour le test qui consiste à 1 million de demandes, il a utilisé un maximum de 550 MO, comparativement à 1,6 GO dans la async HttpWebRequest.
Ainsi, alors que HttpClient en combinaison ServicePointManager.DefaultConnectionLimit semblent s'assurer de la fiabilité (au moins pour le scénario où tous les appels sont passés vers le même hôte), il ressemble encore à sa performance est affectée négativement par l'absence d'un mécanisme de limitation. Quelque chose qui permettrait de limiter le nombre simultané de demandes pour une valeur configurable et mettre le reste dans une file d'attente, il est bien plus adapté pour une grande évolutivité des scénarios.