HttpClientHandler / HttpClient内存泄漏

时间:2015-01-01 16:32:33

标签: c# memory memory-leaks garbage-collection httpclient

我有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处理的实例' (黄色感叹号)。探查器指示已被植根的代理是AsyncCallback s,源自HttpWebRequest。

它也可能与RemoteCertValidationCallback有关,与HTTPS证书验证有关,因为TlsStream是根目录中的一个对象,它是“已处置但未经过GC处理”。

考虑到这一切 - 我怎样才能更正确地使用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

4 个答案:

答案 0 :(得分:14)

使用repro表单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();
        }
    }
}

答案 1 :(得分:1)

这是我在不重新创建对象的情况下更改N > 0代理的方法。

HttpClientHandler

答案 2 :(得分:1)

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

我的Performance 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 :(得分:0)

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

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

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