Сканирование HttpClient приводит к утечке памяти

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

Таким образом, урезанная версия здесь:


[ОБНОВЛЕНИЕ 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.

Профиль памяти процесса, показывающий буферы, удерживаемые возможно асинхронными обратными вызовами

Я использую C# 4.0, поэтому здесь нет асинхронного / ожидающего, поэтому используется только TPL 4.0.

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

4 ответа

Решение

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

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

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

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

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

Если вы удалите 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();
            }
        }
    }
}

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

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

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

Смотреть стек TCP

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

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

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

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

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

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

Вот воспроизведение этого поведения.

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

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

Другие вопросы по тегам