Утечка памяти HttpClientHandler/HttpClient

У меня есть от 10 до 150 объектов живого класса, которые вызывают методы, выполняющие простые вызовы API HTTPS с использованием HttpClient. Пример вызова PUT:

using (HttpClientHandler handler = new HttpClientHandler())
{
    handler.UseCookies = true;
    handler.CookieContainer = _Cookies;

    using (HttpClient client = new HttpClient(handler, true))
    {
        client.Timeout = new TimeSpan(0, 0, (int)(SettingsData.Values.ProxyTimeout * 1.5));
        client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Statics.UserAgent);

        try
        {
            using (StringContent sData = new StringContent(data, Encoding.UTF8, contentType))
            using (HttpResponseMessage response = await client.PutAsync(url, sData))
            {
                using (var content = response.Content)
                {
                    ret = await content.ReadAsStringAsync();
                }

            }
        }
        catch (ThreadAbortException)
        {
            throw;
        }
        catch (Exception ex)
        {
            LastErrorText = ex.Message;
        }
    }
}

Через 2-3 часа после запуска этих методов, которые включают правильное удаление с помощью операторов using, программа ползла до 1 ГБ-1,5 ГБ памяти и, в конечном счете, с ошибками памяти. Много раз соединения осуществляются через ненадежные прокси-серверы, поэтому соединения могут не выполняться, как ожидалось (таймауты и другие ошибки являются общими). ​​

.NET Memory Profiler указал, что HttpClientHandler является основной проблемой здесь, заявив, что он имеет как "Disposed экземпляры с прямыми корнями делегата" (красный восклицательный знак), так и "Экземпляры, которые были удалены, но все еще не GCed" ( желтый восклицательный знак). Делегаты, о которых указывает профайлер, были укоренены AsyncCallback s, вытекающие из HttpWebRequest.

Он также может относиться к RemoteCertValidationCallback, что-то относящееся к проверке сертификата HTTPS, поскольку TlsStream - это объект, расположенный ниже в корне, который является "Disposed, но не GCed".

С учетом всего этого - как я могу более правильно использовать HttpClient и избежать этих проблем с памятью? Должен ли я форсировать GC.Collect() каждый час или около того? Я знаю, что это считается плохой практикой, но я не знаю, как еще вернуть эту память, которая не совсем правильно утилизируется, и лучшая модель использования этих короткоживущих объектов не очевидна для меня, как кажется быть недостатком в самих объектах .NET.


UPDATE Принуждение GC.Collect() не имело эффекта.

Всего управляемые байты для процесса остаются согласованными примерно на 20-30 МБ, в то время как общая память процесса (в диспетчере задач) продолжает расти, указывая на неуправляемую утечку памяти. Таким образом, этот шаблон использования создает неуправляемую утечку памяти.

Я попытался создать экземпляры уровня класса как HttpClient, так и HttpClientHandler по предложению, но это не оказало заметного эффекта. Даже когда я устанавливаю их на уровень класса, они все еще воссоздаются и редко используются повторно из-за того, что параметры прокси-сервера часто требуют изменения. HttpClientHandler не позволяет изменять настройки прокси-сервера или какие-либо свойства после того, как запрос был инициирован, поэтому я постоянно пересобираю обработчик, как это было первоначально сделано с независимыми операторами using.

HttpClienthandler все еще используется с "прямыми корнями делегата" в AsyncCallback → HttpWebRequest. Я начинаю удивляться, может быть, HttpClient просто не был предназначен для быстрых запросов и объектов с коротким проживанием. Нет конца в поле зрения. В надежде, что у кого-то есть предложение сделать использование HttpClientHandler жизнеспособным.


Профайлер памяти: Initial stack indicating that HttpClientHandler is the root issue, having 304 live instances that should have been GC'd

enter image description here

enter image description here

Ответ 1

Используя репрограмму Александра Никитина, я смог обнаружить, что это, похоже, происходит ТОЛЬКО, когда у вас HttpClient - короткоживущий объект. Если вы заставите обработчика и клиента долго жить, это, похоже, не произойдет:

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace HttpClientMemoryLeak
{
    using System.Net;
    using System.Threading;

    class Program
    {
        static HttpClientHandler handler = new HttpClientHandler();

        private static HttpClient client = new HttpClient(handler);

        public static async Task TestMethod()
        {
            try
            {
                using (var response = await client.PutAsync("http://localhost/any/url", null))
                {
                }
            }
            catch
            {
            }
        }

        static void Main(string[] args)
        {
            for (int i = 0; i < 1000000; i++)
            {
                Thread.Sleep(10);
                TestMethod();
            }

            Console.WriteLine("Finished!");
            Console.ReadKey();
        }
    }
}

Ответ 2

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

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

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