Обход сканирования HttpClient приводит к утечке памяти

Я работаю над WebCrawler реализация, но столкнулся с странной утечкой памяти в ASP.NET Web API HttpClient.

Итак, вырезанная версия находится здесь:


[ОБНОВЛЕНИЕ 2]

Я нашел проблему, и это не утечка HttpClient. См. Мой ответ.


[ОБНОВЛЕНИЕ 1]

Я добавил команду без эффекта:

    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();
    }

Файл, содержащий ссылки, доступен здесь.

Это приводит к постоянному увеличению памяти. Анализ памяти показывает много байтов, которые могут быть у AsyncCallback. Ранее я делал много анализов утечки памяти, но этот, похоже, находится на уровне HttpClient.

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

Я использую С# 4.0, поэтому нет async/await здесь, поэтому используется только TPL 4.0.

Приведенный выше код работает, но не оптимизирован, а иногда вызывает истерику, но для воспроизведения эффекта достаточно. Точка: я не могу найти точку, которая может вызвать утечку памяти.

Ответ 1

Хорошо, я дошел до конца. Спасибо @Tugberk, @Darrel и @youssef за потраченное на это время.

В основном исходная проблема заключалась в том, что я порождал слишком много задач. Это начало снижаться, поэтому мне пришлось отказаться от этого и иметь некоторое состояние, чтобы убедиться, что количество одновременных задач ограничено. Это в основном большая проблема для написания процессов, которые должны использовать TPL для планирования задач. Мы можем управлять потоками в пуле потоков, но нам также нужно контролировать задачи, которые мы создаем, поэтому ни один уровень async/await не поможет.

Мне удалось воспроизвести утечку всего пару раз с помощью этого кода - в других случаях после его роста он просто внезапно упадет. Я знаю, что была обновленная GC в 4.5, поэтому, возможно, проблема здесь в том, что GC не ударил достаточно, хотя я смотрел на перфомансы на сборниках GC 0, 1 и 2.

Таким образом, вынос здесь заключается в том, что повторное использование HttpClient НЕ приводит к утечке памяти.

Ответ 2

Я не умею определять проблемы с памятью, но я дал ему попробовать следующий код. Он в .NET 4.5 и использует функцию async/await для С#. Кажется, что память использует около 10-15 МБ для всего процесса (не уверен, что вы видите, что это лучшее использование памяти, хотя). Но если вы наблюдаете # Gen 0 Collections, # Gen 1 Collections и # Gen 2 Collections perf counters, они довольно высоки с приведенным ниже кодом.

Если вы удалите приведенные ниже вызовы GC.Collect, он будет перемещаться между 30 МБ и 50 МБ для всего процесса. Интересная часть заключается в том, что, когда я запускаю свой код на своей 4-ядерной машине, я тоже не вижу аномального использования памяти этим процессом. У меня установлен .NET 4.5 на моем компьютере, и если вы этого не сделаете, проблема может быть связана с внутренними средами CLR.NET 4.0, и я уверен, что TPL значительно улучшился на .NET 4.5 на основе использования ресурсов.

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();
            }
        }
    }
}

Ответ 3

Недавний отчет "Утечка памяти" в нашей среде QA научил нас этому:

Рассмотрим стек TCP

Не предполагайте, что стек TCP может делать то, что задано в то время, "считающееся подходящим для приложения". Конечно, мы можем отталкивать Задачи по своему усмотрению, и мы просто любим асих, но....

Смотрите TCP-стек

Запустите NETSTAT, если у вас есть утечка памяти. Если вы видите остаточные сеансы или полупеченные состояния, вы можете переосмыслить свой дизайн в соответствии с повторным использованием HTTPClient и ограничить объем одновременной работы. Вам также может потребоваться использовать балансировку нагрузки на нескольких компьютерах.

Получистые сессии отображаются в NETSTAT с помощью Fin-Waits 1 или 2 и Time-Waits или даже RST-WAIT 1 и 2. Даже сеансы "Установленные" могут быть практически мертвы, просто ожидая, чтобы тайм-ауты загорелись.

Стеки и .NET скорее всего не сломаны

Перегрузка стека заставляет машину спать. Восстановление требует времени и 99% времени, когда стек восстановится. Помните также, что .NET не будет выпускать ресурсы до своего времени и что никакой пользователь не имеет полного контроля над GC.

Если вы убьете приложение, и для NETSTAT потребуется 5 минут, это довольно хороший знак, когда система перегружена. Это также хорошее представление о том, как стек не зависит от приложения.

Ответ 4

По умолчанию HttpClient утечка, когда вы используете его как непродолжительный объект и создаете новые HttpClients для каждого запроса.

Здесь является воспроизведение этого поведения.

В качестве обходного пути я смог использовать HttpClient как непродолжительный объект, используя следующий пакет Nuget вместо встроенной сборки System.Net.Http: https://www.nuget.org/packages/HttpClient

Не уверен, что происхождение этого пакета, однако, как только я на него ссылался, пропала утечка памяти. Убедитесь, что вы удалите ссылку на встроенную библиотеку .NET System.Net.Http и вместо этого используйте пакет Nuget.