HttpClient爬取导致内存泄漏
Posted
技术标签:
【中文标题】HttpClient爬取导致内存泄漏【英文标题】:HttpClient crawling results in memory leak 【发布时间】:2012-12-28 19:57:56 【问题描述】:我正在开发 WebCrawler implementation,但在 ASP.NET Web API 的 HttpClient 中遇到了奇怪的内存泄漏。
所以精简版在这里:
[更新 2]
我发现了问题,不是 HttpClient 泄漏了。看我的回答。
[更新 1]
我添加了 dispose 但没有任何效果:
static void Main(string[] args)
int waiting = 0;
const int MaxWaiting = 100;
var httpClient = new HttpClient();
foreach (var link in File.ReadAllLines("links.txt"))
while (waiting>=MaxWaiting)
Thread.Sleep(1000);
Console.WriteLine("Waiting ...");
httpClient.GetAsync(link)
.ContinueWith(t =>
try
var httpResponseMessage = t.Result;
if (httpResponseMessage.IsSuccessStatusCode)
httpResponseMessage.Content.LoadIntoBufferAsync()
.ContinueWith(t2=>
if(t2.IsFaulted)
httpResponseMessage.Dispose();
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine(t2.Exception);
else
httpResponseMessage.Content.
ReadAsStringAsync()
.ContinueWith(t3 =>
Interlocked.Decrement(ref waiting);
try
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(httpResponseMessage.RequestMessage.RequestUri);
string s =
t3.Result;
catch (Exception ex3)
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(ex3);
httpResponseMessage.Dispose();
);
);
catch(Exception e)
Interlocked.Decrement(ref waiting);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e);
);
Interlocked.Increment(ref waiting);
Console.Read();
包含链接的文件可用here。
这会导致内存不断上升。内存分析显示 AsyncCallback 可能持有许多字节。我之前做过很多内存泄漏分析,但这一次似乎是在 HttpClient 级别。
我使用的是 C# 4.0,所以这里没有 async/await,所以只使用了 TPL 4.0。
上面的代码有效,但没有优化,有时会发脾气,但足以重现效果。关键是我找不到任何可能导致内存泄漏的点。
【问题讨论】:
这将每秒至少启动一个请求。也许您每秒处理 @usr 我只是限制等待的请求数量,并每秒检查一次。如果我不这样做,这将创建 1000 多个任务。 在检查并等待之后,您仍然可以开始另一个。你总是开始一个。 我已经尝试过了,我确实看到一开始有增加。由于服务器未响应而导致大约 50 次异常后,内存负载确实从 397MB 下降到 69MB 并保持在那里。似乎您有许多尚未返回的并发请求待处理。超时导致挂起的任务被取消后一切恢复正常。 @Aliostad 这无济于事,但它可能会稍微减少代码......除非您使用 HttpCompletionOption.ResponseHeadersRead,否则 LoadIntoBufferAsync 是多余的。 【参考方案1】:好的,我明白了。感谢@Tugberk、@Darrel 和@youssef 在这方面花时间。
基本上,最初的问题是我产生了太多任务。这开始造成损失,所以我不得不减少这一点,并有一些状态来确保并发任务的数量是有限的。 这对于编写必须使用 TPL 来安排任务的进程来说基本上是一个很大的挑战。我们可以控制线程池中的线程,但我们还需要控制我们正在创建的任务,因此没有任何级别的async/await
对此有帮助。
我只用这段代码成功地重现了几次泄漏 - 其他时候在增长后它会突然下降。我知道在 4.5 中对 GC 进行了改进,所以这里的问题可能是 GC 没有发挥足够的作用,尽管我一直在查看 GC 第 0、1 和 2 代集合的性能计数器。
所以这里的要点是重复使用HttpClient
不会导致内存泄漏。
【讨论】:
感谢您的澄清,我使用 SemaphoreSlim 来限制等待时间。 ***.com/questions/10806951/…【参考方案2】:我不擅长定义内存问题,但我尝试使用以下代码。它在 .NET 4.5 中,也使用 C# 的 async/await 特性。它似乎将整个过程的内存使用量保持在 10 - 15 MB 左右(但不确定您是否看到了更好的内存使用量)。但是,如果您查看 # Gen 0 Collections、# Gen 1 Collections 和 # Gen 2 Collections 性能计数器,它们在下面的代码中相当高.
如果您删除下面的 GC.Collect
调用,则整个过程会在 30MB 到 50MB 之间来回切换。有趣的是,当我在我的 4 核机器上运行您的代码时,我也没有看到进程内存使用异常。我的机器上安装了 .NET 4.5,如果您没有安装,问题可能与 .NET 4.0 的 CLR 内部有关,我确信 TPL 在资源使用情况下在 .NET 4.5 上有了很大改进。
class Program
static void Main(string[] args)
ServicePointManager.DefaultConnectionLimit = 500;
CrawlAsync().ContinueWith(task => Console.WriteLine("***DONE!"));
Console.ReadLine();
private static async Task CrawlAsync()
int numberOfCores = Environment.ProcessorCount;
List<string> requestUris = File.ReadAllLines(@"C:\Users\Tugberk\Downloads\links.txt").ToList();
ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>> tasks = new ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>>();
List<HttpRequestMessage> requestsToDispose = new List<HttpRequestMessage>();
var httpClient = new HttpClient();
for (int i = 0; i < numberOfCores; i++)
string requestUri = requestUris.First();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
Task task = MakeCall(httpClient, requestMessage);
tasks.AddOrUpdate(task.Id, Tuple.Create(task, requestMessage), (index, t) => t);
requestUris.RemoveAt(0);
while (tasks.Values.Count > 0)
Task task = await Task.WhenAny(tasks.Values.Select(x => x.Item1));
Tuple<Task, HttpRequestMessage> removedTask;
tasks.TryRemove(task.Id, out removedTask);
removedTask.Item1.Dispose();
removedTask.Item2.Dispose();
if (requestUris.Count > 0)
var requestUri = requestUris.First();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
Task newTask = MakeCall(httpClient, requestMessage);
tasks.AddOrUpdate(newTask.Id, Tuple.Create(newTask, requestMessage), (index, t) => t);
requestUris.RemoveAt(0);
GC.Collect(0);
GC.Collect(1);
GC.Collect(2);
httpClient.Dispose();
private static async Task MakeCall(HttpClient httpClient, HttpRequestMessage requestMessage)
Console.WriteLine("**Starting new request for 0!", requestMessage.RequestUri);
var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
Console.WriteLine("**Request is completed for 0! Status Code: 1", requestMessage.RequestUri, response.StatusCode);
using (response)
if (response.IsSuccessStatusCode)
using (response.Content)
Console.WriteLine("**Getting the html for 0!", requestMessage.RequestUri);
string html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Console.WriteLine("**Got the HTML for 0! Legth: 1", requestMessage.RequestUri, html.Length);
else if (response.Content != null)
response.Content.Dispose();
【讨论】:
谢谢。我的代码收藏量也很高。我已经检查以确保它正在收集。GC.Collect()
无论如何都不好,除了非常特殊的情况(例如,在另一个非托管应用程序中托管 CLR 时),不应触及生产环境
我知道。我只是把它们用来证明这一点。只需删除它们。当您删除 GC.Collect
调用时,我的机器中的内存使用量保持在 30MB 到 50MB 之间。
是否需要调用:response.Content.Dispose()?
@TalAvissar 不,response.Dispose 已经这样做了:github.com/dotnet/corefx/blob/…【参考方案3】:
最近在我们的 QA 环境中报告的“内存泄漏”告诉我们:
考虑 TCP 堆栈
不要假设 TCP 堆栈可以在“认为适合应用程序”的时间内完成所要求的事情。当然,我们可以随意拆分 Tasks,而且我们只是喜欢 asych,但是....
观察 TCP 堆栈
当您认为您有内存泄漏时运行 NETSTAT。如果您看到剩余会话或半生不熟的状态,您可能需要按照 HTTPClient 重用的思路重新考虑您的设计,并限制正在启动的并发工作量。您可能还需要考虑在多台机器上使用负载平衡。
半生不熟的会话显示在 NETSTAT 中,带有 Fin-Waits 1 或 2 以及 Time-Waits 甚至 RST-WAIT 1 和 2。即使是“已建立”的会话也可能在等待超时触发时几乎失效。
Stack 和 .NET 很可能没有损坏
堆栈重载会使机器进入睡眠状态。恢复需要时间,而且 99% 的时间堆栈都会恢复。还要记住,.NET 不会提前释放资源,并且没有用户可以完全控制 GC。
如果您终止该应用程序并且 NETSTAT 需要 5 分钟才能稳定下来,这是系统不堪重负的一个很好的迹象。这也很好地展示了堆栈如何独立于应用程序。
【讨论】:
【参考方案4】:当您将默认的HttpClient
用作短期对象并为每个请求创建新的 HttpClients 时,它会泄漏。
Here 是这种行为的再现。
作为一种解决方法,通过使用以下 Nuget 包而不是内置的 System.Net.Http
程序集,我能够继续使用 HttpClient 作为短期对象:
https://www.nuget.org/packages/HttpClient
不确定这个包的来源是什么,但是,一旦我引用它,内存泄漏就消失了。确保删除对内置 .NET System.Net.Http
库的引用并改用 Nuget 包。
【讨论】:
"默认 HttpClient 在您将其用作短期对象并为每个请求创建新的 HttpClient 时会泄漏。" ......即使你在处置? 是的。 .NET HttpClient 实现中似乎存在内存泄漏。此外,强制GC.Collect()
没有任何影响。
过去我也看到过奇怪的内存泄漏,一个建议是使用带有“关闭”的 http 标头连接。例如:client.DefaultRequestHeaders.Add("Connection", "close"); ...也许值得一试?
您是否尝试过 Dispose 重载来释放托管和非托管资源?
是的,内存泄漏仍然存在。以上是关于HttpClient爬取导致内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章