22

我正在研究 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。

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

4

4 回答 4

21

好的,我明白了这一点。感谢@Tugberk、@Darrel 和@youssef 在这方面花时间。

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

我设法用这段代码只重现了几次泄漏 - 其他时候在增长后它会突然下降。我知道在 4.5 中对 GC 进行了改进,所以这里的问题可能是 GC 没有发挥足够的作用,尽管我一直在查看 GC 第 0、1 和 2 代集合的性能计数器。

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

于 2013-01-31T13:31:00.057 回答
5

我不擅长定义内存问题,但我尝试使用以下代码。它在 .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();
            }
        }
    }
}
于 2012-12-29T23:41:27.590 回答
2

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

考虑 TCP 堆栈

不要假设 TCP 堆栈可以在“认为适合应用程序”的时间内完成所要求的事情。当然,我们可以随意分拆任务,我们只是喜欢 asych,但是....

观察 TCP 堆栈

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

NETSTAT 中出现半生不熟的会话,带有 Fin-Waits 1 或 2 以及 Time-Waits 甚至 RST-WAIT 1 和 2。即使是“已建立”的会话也可能在等待超时触发时几乎失效。

Stack 和 .NET 很可能没有损坏

堆栈过载会使机器进入睡眠状态。恢复需要时间,而且 99% 的时间堆栈都会恢复。还要记住,.NET 不会提前释放资源,并且没有用户可以完全控制 GC。

如果您杀死该应用程序并且 NETSTAT 需要 5 分钟才能稳定下来,这是系统不堪重负的一个很好的迹象。这也是堆栈如何独立于应用程序的一个很好的展示。

于 2015-09-09T11:27:25.880 回答
0

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

是这种行为的再现。

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

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

于 2015-09-29T14:55:13.120 回答