HttpClientHandler / HttpClient 内存泄漏问题

25

我有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内存分析器显示HttpClientHandler是主要问题,表示它具有“带有直接委托根”的已释放实例(红色感叹号)和“已释放但仍未GCed”的实例(黄色感叹号)。分析器指示的委托来自于HttpWebRequest,并且是AsyncCallback

这也可能与RemoteCertValidationCallback相关,涉及HTTPS证书验证,因为位于该根目录下进一步的对象是“已释放但未GCed”的TlsStream

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


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

进程的总托管字节数始终保持在最多20-30 MB左右,而进程的整体内存(在任务管理器中)继续上升,表明存在未受控制的内存泄漏。因此,这种使用模式会创建未受控制的内存泄漏。

我尝试了按建议创建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


2
很难看出发生了什么,但一般来说:找出根源。启用 .net 源代码调试并理解何时应该释放这些根源以及原因是什么?尝试删除 handler.CookieContainer = _CookieContainer - 也许有一些可疑的事情正在进行? - Erti-Chris Eelmaa
2
我不确定,但如果我记得正确的话,在一般的http中不建议使用(),因为当出现错误时它不会释放所有内存,你需要在连接上调用abort。 - Pedro.The.Kid
@ChrisEelmaa 我现在已经转而使用HttpWebRequest异步了,HttpClient只是它的一个包装器,这样做使内存问题减轻了两倍(例如,在应用程序崩溃之前,它现在可以运行8个小时而不是3个小时)。我没有看到解决方案,似乎这是框架中的一个缺陷,正如你所建议的-花更多时间来解决它已经超出了我的兴趣水平。感谢您的建议。 - user1111380
1
非常有趣的案例。简洁的重现请参见 https://gist.github.com/alexandrnikitin/6b2e71c27ce5e9ec5601 - Alexandr Nikitin
1
@ChrisEelmaa 我之前在 repro 中使用了 void 而不是 Task,这是我的错误 :) 实际上它没有泄漏,这是我的 repro https://gist.github.com/alexandrnikitin/86b3e5a517455f7ff8b0 - Alexandr Nikitin
显示剩余14条评论
4个回答

17
使用复制的表单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();
        }
    }
}

4
感谢您解决了这个问题。不幸的是,HttpClient类不能满足我的要求 - 由于公共代理的动态和不稳定性,这些对象必须经常重新创建。看起来HttpClient对于短暂的连接并不可行 - 更改代理设置需要重新构建HttpClientHandler,因此也需要重新构建HttpClient。无论如何,这些对象应该能够根据需要长时间或短时间存活而不泄漏;这绝对似乎是HttpClient的一个缺陷。 - user1111380

3

这里是一个基本的 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;
}
< p >注意:如果您正在使用依赖注入库,请将MyApiClient注册为单例。它是无状态的,并且可以安全地重用相同的对象进行具体请求。


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

-1

正如Matt Clark所提到的,当您将HttpClient用作短暂对象并为每个请求创建新的HttpClient时,默认情况下会发生泄漏。

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

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


很不幸,看起来所有者已经取消了此软件包的列表。“所有者已取消此软件包的列表。这可能意味着该软件包已被弃用或不应再使用。” - Choco Smith
即使它未列出,您仍然可以使用它。它仍然有效。 - Elad Nava

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接