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

У меня есть где-то от 10 до 150 долгоживущих объектов класса, которые вызывают методы, выполняющие простые вызовы HTTPS API, используя 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 является главной проблемой, заявив, что в ней есть "Удаленные экземпляры с прямыми корнями делегатов" (красный восклицательный знак) и "Экземпляры, которые были удалены, но все еще не GCed" (желтый восклицательный знак). Делегаты, на которых указывает профилировщик, укоренились AsyncCallbackс, вытекающих из HttpWebRequest.

Это также может относиться к RemoteCertValidationCallback, что-то делать с проверкой сертификата HTTPS, как TlsStream является объектом ниже в корне, который "Уничтожен, но не GCed".

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


ОБНОВЛЕНИЕ Принудительно GC.Collect() не имел никакого эффекта.

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

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

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


Память профилирует снимки:Начальный стек, указывающий, что HttpClientHandler является корневой проблемой, имея 304 живых экземпляра, которые должны были быть GC'd

4 ответа

Решение

Используя репродукцию Александра Никитина, я смог обнаружить, что это, кажется, происходит ТОЛЬКО тогда, когда 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();
        }
    }
}

Вот базовый клиент Api, который эффективно использует HttpClient и HttpClientHandler. НЕ воссоздайте HTTPClient для каждого запроса. Максимально используйте Httpclient

My Performance Api Client

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace MyApiClient 
{
    public class MyApiClient : IDisposable
    {
        private readonly TimeSpan _timeout;
        private HttpClient _httpClient;
        private HttpClientHandler _httpClientHandler;
        private readonly string _baseUrl;
        private const string ClientUserAgent = "my-api-client-v1";
        private const string MediaTypeJson = "application/json";

        public MyApiClient(string baseUrl, TimeSpan? timeout = null)
        {
            _baseUrl = NormalizeBaseUrl(baseUrl);
            _timeout = timeout ?? TimeSpan.FromSeconds(90);
        }

        public async Task<string> PostAsync(string url, object input)
        {
            EnsureHttpClientCreated();

            using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
            {
                using (var response = await _httpClient.PostAsync(url, requestContent))
                {
                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }

        public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
        {
            var strResponse = await PostAsync(url, input);

            return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
        {
            var strResponse = await GetAsync(url);

            return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        public async Task<string> GetAsync(string url)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.GetAsync(url))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public async Task<string> PutAsync(string url, object input)
        {
            return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
        }

        public async Task<string> PutAsync(string url, HttpContent content)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.PutAsync(url, content))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public async Task<string> DeleteAsync(string url)
        {
            EnsureHttpClientCreated();

            using (var response = await _httpClient.DeleteAsync(url))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }

        public void Dispose()
        {
            _httpClientHandler?.Dispose();
            _httpClient?.Dispose();
        }

        private void CreateHttpClient()
        {
            _httpClientHandler = new HttpClientHandler
            {
                AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
            };

            _httpClient = new HttpClient(_httpClientHandler, false)
            {
                Timeout = _timeout
            };

            _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

            if (!string.IsNullOrWhiteSpace(_baseUrl))
            {
                _httpClient.BaseAddress = new Uri(_baseUrl);
            }

            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
        }

        private void EnsureHttpClientCreated()
        {
            if (_httpClient == null)
            {
                CreateHttpClient();
            }
        }

        private static string ConvertToJsonString(object obj)
        {
            if (obj == null)
            {
                return string.Empty;
            }

            return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
        }

        private static string NormalizeBaseUrl(string url)
        {
            return url.EndsWith("/") ? url : url + "/";
        }
    }
}

Использование;

using ( var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}

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

Вот как я меняю HttpClientHandler прокси без воссоздания объекта.

public static void ChangeProxy(this HttpClientHandler handler, WebProxy newProxy)
{
    if (handler.Proxy is WebProxy currentHandlerProxy)
    {
        currentHandlerProxy.Address = newProxy.Address;
        currentHandlerProxy.Credentials = newProxy.Credentials;
    }
    else
    {
        handler.Proxy = newProxy;
    }
}

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

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

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

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