129 votes

Est async HttpClient de .net 4.5 un mauvais choix pour des applications intensives de charge ?

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.

64voto

Florin Dumitrescu Points 3501

Outre les tests mentionnés dans la question, j'ai récemment créé une nouvelle impliquant beaucoup moins HTTP appels (5000 par rapport à 1 millions précédemment), mais sur les demandes qui ont pris beaucoup plus de temps à exécuter (500 millisecondes par rapport à environ 1 milliseconde précédemment). Les deux testeur d'applications, de la manière synchrone multithread (basé principalement sur HttpWebRequest) et asynchronous I/O (basé sur le client HTTP) produit des résultats similaires: environ 10 secondes à s'exécuter à l'aide d'environ 3% de la CPU et 30 MO de mémoire. La seule différence entre les deux testeurs a été que le multithread celui utilisé 310 threads pour exécuter, tandis que l'asynchrone un juste 22. Ainsi, dans une application qui aurait combiné les deux I/O bound et de la CPU, les opérations de la version asynchrone aurait donné de meilleurs résultats car il y aurait eu plus de temps PROCESSEUR disponible pour les threads d'effectuer les opérations CPU, qui sont ceux qui en ont besoin (les threads en attente pour les opérations d'e/S à compléter êtes juste de gaspiller).

En conclusion de ces tests, HTTP asynchrone appels ne sont pas la meilleure option lorsque vous traitez avec de très rapide des demandes. La raison derrière cela est que lors de l'exécution d'une tâche qui contient un I/O asynchrone appel, le fil sur lequel le travail est commencé, c'est d'arrêter dès que la que l'appel asynchrone est faite et le reste de la tâche est enregistrée comme un rappel. Ensuite, lors de l'opération d'e/S est terminée, le callback est mis en file d'attente pour l'exécution à la première du fil. Tout cela crée une surcharge, ce qui le rend rapide des opérations d'e/S pour être plus efficace lorsqu'il est exécuté sur le fil qui les a lancés.

HTTP asynchrone appels sont une bonne option lorsque vous traitez avec de longues ou potentiellement longue opérations d'e/S, car il permet de ne pas garder tous les threads occupés à attendre pour les opérations d'e/S à compléter. Cela diminue le nombre de threads utilisés par une application permettant aux plus de temps PROCESSEUR pour être passé par CPU opérations. En outre, sur les applications qui ne allouer un nombre limité de fils (comme c'est le cas avec les applications web), I/O asynchrone empêche thread du pool de l'épuisement, ce qui peut se produire si vous effectuez appels d'e/S de manière synchrone.

Donc, async HttpClient n'est pas un goulot d'étranglement de l'intensité de charge des applications. C'est juste que, de par sa nature, il n'est pas très bien adapté pour les très rapide des requêtes HTTP, il est idéal pour de longues ou potentiellement longue, en particulier dans les applications qui ne disposent que d'un nombre limité de threads disponibles. Aussi, il est conseillé de limiter la simultanéité via ServicePointManager.DefaultConnectionLimit avec une valeur suffisamment élevée pour assurer un bon niveau de parallélisme, mais suffisamment bas pour éviter de port éphémère épuisement. Vous pouvez trouver plus de détails sur les tests et les conclusions présentées à cette question ici.

27voto

Darrel Miller Points 56797

Une chose à considérer qui peuvent avoir une incidence sur vos résultats, c'est qu'avec la HttpWebRequest vous n'obtenez pas les ResponseStream et de consommation qui stream. Avec HttpClient, par défaut, il faudra copier le flux réseau dans un flux de mémoire. Afin d'utiliser HttpClient de la même manière que vous utilisez actuellement HttpWebRquest vous aurez besoin de faire

var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

L'autre chose, c'est que je ne suis pas vraiment sûr de ce que la vraie différence, à partir d'un filetage perspective, vous avez fait les tests. Si vous plongez dans les profondeurs de HttpClientHandler simplement, il ne Tâche.Usine.StartNew afin d'effectuer une demande asynchrone. Le filetage comportement est déléguée au contexte de synchronisation exactement de la même manière que votre exemple avec HttpWebRequest exemple est fait.

Sans aucun doute, HttpClient ajouter une surcharge en tant que par défaut il utilise HttpWebRequest que sa bibliothèque de transport. Ainsi, vous serez toujours en mesure d'obtenir de meilleurs perf avec un HttpWebRequest directement lors de l'utilisation de HttpClientHandler. Les avantages que HttpClient apporte est avec les classes standard, comme HttpResponseMessage, HttpRequestMessage, HttpContent et tous les fortement typé en-têtes. En soi, ce n'est pas une perf d'optimisation.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X