HttpClientHandler / HttpClient内存泄漏
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HttpClientHandler / 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
是这里的主要问题,声明它既有“具有直接委托根的Disposed实例”(红色感叹号)和“已经处置但仍然没有GCed的实例”(黄色感叹号)。探查器指示已经植根的代表是来自HttpWebRequest的AsyncCallback
s。
它也可能与RemoteCertValidationCallback
有关,与HTTPS证书验证有关,因为TlsStream
是根目录中的一个对象,即“Disposed但not not GCed”。
考虑到这一切 - 我怎样才能更正确地使用HttpClient并避免这些内存问题?我应该每小时左右强迫一次GC.Collect()
吗?我知道这被认为是不好的做法,但我不知道如何回收这个不太适当处理的内存,这些短命对象的更好的使用模式对我来说并不明显,因为它似乎是.NET对象本身的一个缺陷。
更新迫使GC.Collect()
没有效果。
进程的总管理字节数最多保持在20-30 MB左右,而进程总内存(在任务管理器中)继续爬升,表明存在非托管内存泄漏。因此,此使用模式正在创建非托管内存泄漏。
我已经尝试根据建议创建HttpClient和HttpClientHandler的类级别实例,但这没有明显的效果。即使我将这些设置为类级别,它们仍然会重新创建并且很少重复使用,因为代理设置通常需要更改。一旦请求启动,HttpClientHandler就不允许修改代理设置或任何属性,因此我不断重新创建处理程序,就像最初使用独立的using
语句一样。
HttpClienthandler仍然使用“直接委托根”来处理AsyncCallback - > HttpWebRequest。我开始想知道HttpClient是否可能不是为快速请求和短生命对象而设计的。没有尽头......希望有人建议使用HttpClientHandler可行。
记忆探测器镜头:
使用repr形式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();
}
}
}
这就是我在不重新创建对象的情况下更改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和HttpClientHandler的基本Api客户端。不要为每个请求重新创建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注册为singleton。为具体请求重用相同的对象是无状态且安全的。
正如Matt Clark所提到的,当你将它用作短期对象并为每个请求创建新的HttpClients时,默认的HttpClient
会泄漏。
作为一种解决方法,我能够通过使用以下Nuget包而不是内置的System.Net.Http
程序集继续使用HttpClient作为短期对象:https://www.nuget.org/packages/HttpClient
但是,不确定这个软件包的来源是什么,只要我引用它,内存泄漏就消失了。确保删除对内置.NET System.Net.Http
库的引用,并改为使用Nuget包。
以上是关于HttpClientHandler / HttpClient内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
HttpClientHandler 抛出 PlatformNotSupportedException
如何在 .NET Core 中使用 HttpClientHandler 和 HttpClientFactory
TestServer CreateClient 和 HttpClientHandler - 如何?