HttpClientHandler / HttpClient 内存泄漏

HttpClientHandler / HttpClient Memory Leak

我有 10-150 个长期存在的 class 对象,它们使用 HttpClient 调用执行简单 HTTPS API 调用的方法。 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;
        }
    }
}

在 运行 这些方法(包括通过 using 语句进行适当处理)2-3 小时后,程序已爬到 1GB-1.5GB 内存并最终因各种内存不足而崩溃错误。很多时候连接是通过不可靠的代理进行的,因此连接可能无法按预期完成(超时和其他错误很常见)。

.NET Memory Profiler 指出 HttpClientHandler 是这里的主要问题,并指出它同时具有 'Disposed instances with direct delegate roots'(红色感叹号)和 'Instances that have been disposed but are still not GCed'(黄色感叹号)。探查器指示的委托是 AsyncCallbacks,源于 HttpWebRequest。

它也可能与 RemoteCertValidationCallback 有关,这与 HTTPS 证书验证有关,因为 TlsStream 是根目录下的一个对象,即 'Disposed but not GCed'.

考虑到所有这些 - 我怎样才能更正确地使用 HttpClient 并避免这些内存问题?我应该每隔一小时左右强制执行一次 GC.Collect() 吗?我知道这被认为是不好的做法,但我不知道还有什么方法可以回收未完全正确处理的内存,而且这些 short-lived 对象的更好使用模式对我来说并不明显,因为它似乎是 .NET 对象本身的缺陷。


更新 强制 GC.Collect() 没有效果。

进程的总托管字节最多保持在 20-30 MB 左右,而进程总内存(在任务管理器中)继续攀升,表明存在非托管内存泄漏。因此,这种使用模式造成了非托管内存泄漏。

我已尝试根据建议创建 HttpClient 和 HttpClientHandler 的 class 级实例,但这没有明显效果。即使我将它们设置为 class 级别,它们仍然是 re-created 并且很少 re-used 因为代理设置经常需要更改。 HttpClientHandler 不允许在发起请求后修改代理设置或任何属性,因此我一直是 re-creating 处理程序,就像最初对独立 using 语句所做的一样。

HttpClienthandler 仍在处理 "direct delegate roots" 到 AsyncCallback -> HttpWebRequest。我开始怀疑 HttpClient 是否不是为快速请求和 short-living 对象而设计的。看不到尽头.. 希望有人建议使 HttpClientHandler 的使用可行。


内存分析器截图:

使用 Alexandr Nikitin 的重现形式,我发现这似乎只有当 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();
        }
    }
}

正如 Matt Clark 提到的,默认 HttpClient 当您将其用作短期对象并根据请求创建新的 HttpClient 时会泄漏。

作为一种解决方法,我可以通过使用以下 Nuget 包而不是内置的 System.Net.Http 程序集,继续将 HttpClient 用作短期对象: https://www.nuget.org/packages/HttpClient

不确定这个包的来源是什么,但是,我一引用它,内存泄漏就消失了。确保删除对内置 .NET System.Net.Http 库的引用并改用 Nuget 包。

这就是我在不重新创建对象的情况下更改 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;
    }
}

这是一个基本的 Api 客户端,它有效地使用了 HttpClient 和 HttpClientHandler。不要为每个请求重新创建 HTTPClient。尽可能复用Httpclient

我的表现Api客户

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 注册为单例。为具体请求重用同一个对象是无状态和安全的。