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爬取导致内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

HttpClientHandler / HttpClient内存泄漏

使用HttpClient异步方法时内存泄漏c#

QByteArray导致的内存泄漏问题

常见的内存泄漏原因及解决方法

安卓内存分析——常见内存泄漏场景二

LeakCanary检测内存泄漏