HttpClient爬网导致内存泄漏

我正在进行WebCrawler 实现,但在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(); } 

包含链接的文件可在此处获得 。

这导致内存不断上升。 内存分析显示可能由AsyncCallback保存的许多字节。 之前我做了很多内存泄漏分析,但这个似乎是在HttpClient级别。

进程的内存配置文件显示可能由异步回调保留的缓冲区

我使用C#4.0所以没有async / await这里只使用TPL 4.0。

上面的代码有效,但没有优化,有时会发脾气,但足以重现效果。 点是我找不到任何可能导致内存泄漏的点。

好的,我到底了。 感谢@Tugberk,@ Darrel和@youssef花时间在这上面。

基本上最初的问题是我产生了太多的任务。 这开始造成损失,所以我不得不削减它,并有一些状态,以确保并发任务的数量有限。 对于编写必须使用TPL来安排任务的流程来说,这基本上是一个巨大的挑战。 我们可以控制线程池中的线程,但是我们还需要控制我们正在创建的任务,因此没有async/await级别可以帮助它。

我设法用这个代码重复泄漏了几次 – 其他时候在生长之后它会突然下降。 我知道在4.5中对GC进行了改造,所以这里的问题可能是GC没有充分发挥,尽管我一直在寻找GC生成0,1和2集合上的perf计数器。

所以这里的重点是重新使用HttpClient不会导致内存泄漏。

我不擅长定义内存问题,但我尝试使用以下代码。 它在.NET 4.5中,也使用C#的async / awaitfunction。 它似乎在整个过程中保持大约10-15 MB的内存使用量(不确定你是否认为这是更好的内存使用情况)。 但是如果你看#Gen 0 Collections#Gen 1 Collections#Gen 2 Collections perf计数器,它们的代码相当高。

如果你删除下面的GC.Collect调用,它会在30MB到50MB之间来回传输整个过程。 有趣的是,当我在我的4核机器上运行代码时,我也没有看到该进程的内存使用exception。 我在我的机器上安装了.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 requestUris = File.ReadAllLines(@"C:\Users\Tugberk\Downloads\links.txt").ToList(); ConcurrentDictionary> tasks = new ConcurrentDictionary>(); List requestsToDispose = new List(); 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 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(); } } } } 

最近在我们的QA环境中报告的“内存泄漏”告诉我们:

考虑TCP堆栈

不要假设TCP Stack可以在“认为适合应用程序”的时间内执行所要求的操作。 当然,我们可以随意分离任务,我们只是喜欢asych,但….

观看TCP堆栈

当您认为存在内存泄漏时运行NETSTAT。 如果您看到剩余会话或半生不熟的状态,您可能希望重新考虑您的设计沿着HTTPClient重用并限制正在旋转的并发工作量。 您还可能需要考虑在多台计算机上使用负载平衡。

半成品会话出现在NETSTAT中,Fin-Waits 1或2以及Time-Waits甚至是RST-WAIT 1和2.即使是“已建立”的会话也几乎已经死了,只是等待超时才能开火。

Stack和.NET很可能不会被破坏

堆栈重载会使计算机进入hibernate状态。 恢复需要时间,并且有99%的时间可以恢复堆栈。 还要记住,.NET不会在他们的时间之前释放资源,并且没有用户完全控制GC。

如果你杀了应用程序,NETSTAT需要花费5分钟才能安定下来,这是一个非常好的迹象,系统不堪重负。 它也很好地展示了堆栈如何独立于应用程序。

当您将其用作短期对象并为每个请求创建新的HttpClients时,默认的HttpClient泄漏。

这是这种行为的再现。

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

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