HttpClientHandler / HttpClient 内存泄漏

Posted

技术标签:

【中文标题】HttpClientHandler / HttpClient 内存泄漏【英文标题】:HttpClientHandler / HttpClient Memory Leak 【发布时间】:2022-01-13 12:23:28 【问题描述】:

我有 10-150 个长寿类对象,它们调用使用 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;
        
    

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

.NET Memory Profiler 指出HttpClientHandler 是这里的主要问题,指出它同时具有“具有直接委托根的已处置实例”(红色感叹号)和“已处置但仍未被 GC 处理的实例”(黄色感叹号)。分析器指示已被植根的代表是 AsyncCallbacks,源于 HttpWebRequest。

它也可能与 RemoteCertValidationCallback 相关,这与 HTTPS 证书验证有关,因为 TlsStream 是一个位于根目录下方的对象,即“已处置但未 GCed”。

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


更新 强制GC.Collect() 无效。

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

我已尝试根据建议创建 HttpClient 和 HttpClientHandler 的类级别实例,但这没有明显的效果。即使我将这些设置为类级别,由于代理设置经常需要更改这一事实,它们仍然会重新创建并且很少重复使用。一旦发起请求,HttpClientHandler 不允许修改代理设置或任何属性,因此我不断地重新创建处理程序,就像最初使用独立的 using 语句所做的那样。

HttpClienthandler 仍然使用“直接委托根”处理 AsyncCallback -> HttpWebRequest。我开始怀疑 HttpClient 是否不是为快速请求和短期对象而设计的。看不到尽头..希望有人建议使用 HttpClientHandler 可行。


内存分析器截图:

【问题讨论】:

你为什么要在每次通话中处理 HttpClientHandlerHttpClientHttpClient 应该是整个应用程序中的一个长期存在的对象(因此,HttpClientHandler 也应该如此。这样,您只需要生成一个实例。 很难看出发生了什么,但一般来说:找出根源。启用 .net 源代码调试并了解何时应该释放这些根,为什么?尝试删除 handler.CookieContainer = _CookieContainer - 也许这有什么可疑之处? 我不确定,但如果我没记错的话,一般 http 不建议使用 using(),因为当您遇到错误时,它不会释放所有内存,您需要在连接上调用 abort。 相当有趣的案例。简洁再现gist.github.com/alexandrnikitin/6b2e71c27ce5e9ec5601 @ChrisEelmaa 我在 prev repro 中使用了 void 而不是 Task,我的错 :) 实际上它没有泄漏,这是我的 repro gist.github.com/alexandrnikitin/86b3e5a517455f7ff8b0 【参考方案1】:

使用复制形式 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();
        
    

【讨论】:

感谢您深入了解这一点。不幸的是,HttpClient 类不符合我的要求 - 由于公共代理的动态和不稳定性质,必须经常重新创建对象。看起来 HttpClient 对于短期连接来说不是一个可行的解决方案——更改代理设置需要重新构造 HttpClientHandler,因此需要重新构造 HttpClient。无论哪种方式,对象都应该能够根据需要尽可能长或短而不会泄漏;这绝对似乎是 HttpClient 中的一个缺陷。【参考方案2】:

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

【讨论】:

【参考方案3】:

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

【讨论】:

【参考方案4】:

正如 Matt Clark 所提到的,当您将默认 HttpClient 用作短期对象并为每个请求创建新的 HttpClients 时,它会泄漏。

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

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

【讨论】:

不幸的是,所有者似乎已取消列出此软件包“所有者已取消列出此软件包。这可能意味着该软件包已弃用或不应再使用。”​​ 即使它未列出,您仍然可以使用它。它仍然有效。

以上是关于HttpClientHandler / HttpClient 内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

多个 HttpClientHandler 实例超时错误

HttpClientHandler 抛出 PlatformNotSupportedException

如何在 .NET Core 中使用 HttpClientHandler 和 HttpClientFactory

TestServer CreateClient 和 HttpClientHandler - 如何?

HttpClientHandler / 请求被中止:无法创建 SSL/TLS 安全通道

HttpClient 和 HttpClientHandler 是不是必须在请求之间进行处理? [关闭]