web-dev-qa-db-fra.com

L'analyse par HttpClient entraîne une fuite de mémoire

Je travaille sur un WebCrawler implementation mais je suis confronté à une fuite de mémoire étrange dans HttpClient de l'API Web ASP.NET.

Donc, la version réduite est ici:


[MISE À JOUR 2]

J'ai trouvé le problème et ce n'est pas HttpClient qui fuit. Voir ma réponse.


[MISE À JOUR 1]

J'ai ajouté disposer sans effet:

    static void Main(string[] args)
    {
        int waiting = 0;
        const int MaxWaiting = 100;
        var httpClient = new HttpClient();
        foreach (var link in File.ReadAllLines("links.txt"))
        {

            while (waiting>=MaxWaiting)
            {
                Thread.Sleep(1000);
                Console.WriteLine("Waiting ...");
            }
            httpClient.GetAsync(link)
                .ContinueWith(t =>
                                  {
                                      try
                                      {
                                          var httpResponseMessage = t.Result;
                                          if (httpResponseMessage.IsSuccessStatusCode)
                                              httpResponseMessage.Content.LoadIntoBufferAsync()
                                                  .ContinueWith(t2=>
                                                                    {
                                                                        if(t2.IsFaulted)
                                                                        {
                                                                            httpResponseMessage.Dispose();
                                                                            Console.ForegroundColor = ConsoleColor.Magenta;
                                                                            Console.WriteLine(t2.Exception);
                                                                        }
                                                                        else
                                                                        {
                                                                            httpResponseMessage.Content.
                                                                                ReadAsStringAsync()
                                                                                .ContinueWith(t3 =>
                                                                                {
                                                                                    Interlocked.Decrement(ref waiting);

                                                                                    try
                                                                                    {
                                                                                        Console.ForegroundColor = ConsoleColor.White;

                                                                                        Console.WriteLine(httpResponseMessage.RequestMessage.RequestUri);
                                                                                        string s =
                                                                                            t3.Result;

                                                                                    }
                                                                                    catch (Exception ex3)
                                                                                    {
                                                                                        Console.ForegroundColor = ConsoleColor.Yellow;

                                                                                        Console.WriteLine(ex3);
                                                                                    }
                                                                                    httpResponseMessage.Dispose();
                                                                                });                                                                                
                                                                        }
                                                                    }
                                                  );
                                      }
                                      catch(Exception e)
                                      {
                                          Interlocked.Decrement(ref waiting);
                                          Console.ForegroundColor = ConsoleColor.Red;                                             
                                          Console.WriteLine(e);
                                      }
                                  }
                );

            Interlocked.Increment(ref waiting);

        }

        Console.Read();
    }

Le fichier contenant les liens est disponible ici .

Cela entraîne une augmentation constante de la mémoire. L'analyse de la mémoire montre de nombreux octets éventuellement détenus par AsyncCallback. J'ai déjà effectué de nombreuses analyses de fuite de mémoire auparavant, mais celle-ci semble se situer au niveau HttpClient. 

Memory profile of the process showing buffers held possibly by async callbacks

J'utilise C # 4.0 donc pas async/wait ici, donc seulement TPL 4.0 est utilisé.

Le code ci-dessus fonctionne mais n’est pas optimisé et il est parfois suffisant de reproduire l’effet. Le point est que je ne peux trouver aucun point qui pourrait causer une fuite de mémoire.

18
Aliostad

OK, je suis au fond de ça. Merci à @Tugberk, @Darrel et @youssef d'avoir passé du temps à ce sujet.

Fondamentalement, le problème initial était que je créais trop de tâches. Cela a commencé à faire des ravages et j'ai donc dû réduire ce nombre et disposer d'un état pour m'assurer que le nombre de tâches simultanées est limité. Ceci est fondamentalement un gros défi pour les processus d'écriture qui doivent utiliser TPL pour planifier les tâches. Nous pouvons contrôler les threads dans le pool de threads, mais nous devons également contrôler les tâches que nous créons afin qu'aucun niveau de async/await ne vous aide.

J'ai réussi à reproduire la fuite à quelques reprises seulement avec ce code - d'autres fois après sa croissance, elle tomberait soudainement. Je sais qu’il ya eu une refonte de GC dans la version 4.5, alors le problème ici est peut-être que GC n’a pas suffisamment joué, bien que j’ai examiné les compteurs de performances des collections de générations 0, 1 et 2 de GC. 

En résumé, la réutilisation de HttpClient ne provoque PAS de fuite de mémoire.

19
Aliostad

Je ne suis pas doué pour définir les problèmes de mémoire, mais je l'ai essayé avec le code suivant. Il se trouve dans .NET 4.5 et utilise également la fonctionnalité async/wait de C #. Il semble que l’utilisation de la mémoire se maintienne autour de 10 à 15 Mo pour l’ensemble du processus (mais vous ne savez pas vraiment si cette utilisation sera meilleure). Mais si vous regardez # Gen 0 Collections , # Gen 1 Collections et # Gen 2 Collections perf, ils sont assez hauts avec le code ci-dessous.

Si vous supprimez les appels GC.Collect ci-dessous, il passe de 30 Mo à 50 Mo pour l’ensemble du processus. La partie intéressante est que lorsque j'exécute votre code sur ma machine à 4 cœurs, je ne vois pas non plus d'utilisation anormale de la mémoire par le processus. J'ai installé .NET 4.5 sur ma machine et si ce n’est pas le cas, le problème pourrait être lié aux composants internes du CLR de .NET 4.0 et je suis sûr que la TPL s’est beaucoup améliorée sur .NET 4.5 en fonction de l’utilisation des ressources.

class Program {

    static void Main(string[] args) {

        ServicePointManager.DefaultConnectionLimit = 500;
        CrawlAsync().ContinueWith(task => Console.WriteLine("***DONE!"));
        Console.ReadLine();
    }

    private static async Task CrawlAsync() {

        int numberOfCores = Environment.ProcessorCount;
        List<string> requestUris = File.ReadAllLines(@"C:\Users\Tugberk\Downloads\links.txt").ToList();
        ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>> tasks = new ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>>();
        List<HttpRequestMessage> requestsToDispose = new List<HttpRequestMessage>();

        var httpClient = new HttpClient();

        for (int i = 0; i < numberOfCores; i++) {

            string requestUri = requestUris.First();
            var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
            Task task = MakeCall(httpClient, requestMessage);
            tasks.AddOrUpdate(task.Id, Tuple.Create(task, requestMessage), (index, t) => t);
            requestUris.RemoveAt(0);
        }

        while (tasks.Values.Count > 0) {

            Task task = await Task.WhenAny(tasks.Values.Select(x => x.Item1));

            Tuple<Task, HttpRequestMessage> removedTask;
            tasks.TryRemove(task.Id, out removedTask);
            removedTask.Item1.Dispose();
            removedTask.Item2.Dispose();

            if (requestUris.Count > 0) {

                var requestUri = requestUris.First();
                var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
                Task newTask = MakeCall(httpClient, requestMessage);
                tasks.AddOrUpdate(newTask.Id, Tuple.Create(newTask, requestMessage), (index, t) => t);
                requestUris.RemoveAt(0);
            }

            GC.Collect(0);
            GC.Collect(1);
            GC.Collect(2);
        }

        httpClient.Dispose();
    }

    private static async Task MakeCall(HttpClient httpClient, HttpRequestMessage requestMessage) {

        Console.WriteLine("**Starting new request for {0}!", requestMessage.RequestUri);
        var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
        Console.WriteLine("**Request is completed for {0}! Status Code: {1}", requestMessage.RequestUri, response.StatusCode);

        using (response) {
            if (response.IsSuccessStatusCode){
                using (response.Content) {

                    Console.WriteLine("**Getting the HTML for {0}!", requestMessage.RequestUri);
                    string html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    Console.WriteLine("**Got the HTML for {0}! Legth: {1}", requestMessage.RequestUri, html.Length);
                }
            }
            else if (response.Content != null) {

                response.Content.Dispose();
            }
        }
    }
}
5
tugberk

Une récente "fuite de mémoire" dans notre environnement d'assurance qualité nous a appris ceci:

Considérez le TCP Stack

Ne supposez pas que TCP Stack peut faire ce qui est demandé dans le délai "jugé approprié pour l'application". Bien sûr, nous pouvons créer des tâches à volonté et nous adorons asych, mais ...

Regardez la pile TCP

Exécutez NETSTAT lorsque vous pensez avoir une fuite de mémoire. Si vous voyez des sessions résiduelles ou des états à moitié cuits, vous voudrez peut-être repenser votre conception en fonction de la réutilisation de HTTPClient et en limitant la quantité de travail simultané qui est générée. Vous devrez peut-être également envisager d'utiliser l'équilibrage de charge sur plusieurs ordinateurs.

Les sessions à moitié cuites s'affichent dans NETSTAT avec Fin-Waits 1 ou 2 et Time-Waits ou même RST-WAIT 1 et 2. Même les sessions "Etablies" peuvent être virtuellement mortes, n'attendant que les temps morts.

La pile et .NET ne sont probablement pas cassés

La surcharge de la pile met la machine en veille. La récupération prend du temps et 99% du temps de récupération de la pile. N'oubliez pas non plus que .NET ne publiera pas de ressources avant l'heure et qu'aucun utilisateur ne contrôle totalement GC. 

Si vous supprimez l'application et qu'il faut 5 minutes à NETSTAT pour s'installer, c'est un très bon signe que le système est surchargé. C'est aussi une bonne démonstration de la façon dont la pile est indépendante de l'application.

2
John Peters

La valeur par défaut HttpClient fuit lorsque vous l'utilisez comme objet de courte durée et créez de nouveaux clients HTTP par demande.

Ici est une reproduction de ce comportement.

Pour résoudre ce problème, j'ai pu continuer à utiliser HttpClient en tant qu'objet de courte durée en utilisant le package Nuget suivant au lieu de l'assembly System.Net.Http intégré: https://www.nuget.org/packages/HttpClient

Je ne sais pas quelle est l'origine de ce paquet, cependant, dès que je l'ai référencé, la fuite de mémoire a disparu. Assurez-vous de supprimer la référence à la bibliothèque .NET System.Net.Http intégrée et utilisez plutôt le package Nuget.

0
Elad Nava