22

WebCrawlerの実装に取り​​組んでいますが、ASP.NETWebAPIの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がないため、TPL4.0のみが使用されます。

上記のコードは機能しますが、最適化されておらず、時にはタントラムをスローしますが、効果を再現するには十分です。ポイントは、メモリがリークする原因となるポイントが見つからないことです。

4

4 に答える 4

21

OK、私はこれの底に着きました。これに時間を費やしてくれた@Tugberk、@ Darrel、@youssefに感謝します。

基本的に、最初の問題は、私があまりにも多くのタスクを生成していたことでした。これは犠牲になり始めたので、私はこれを削減し、並行タスクの数が制限されていることを確認するためのいくつかの状態を持たなければなりませんでした。これは基本的に、タスクをスケジュールするためにTPLを使用する必要があるプロセスを作成するための大きな課題です。スレッドプール内のスレッドを制御できますが、作成するタスクも制御する必要があるため、async/awaitこれを支援するレベルはありません。

私はこのコードでリークを数回しか再現できませんでした。それ以外の場合は、成長した後、突然ドロップします。4.5でGCが刷新されたことを知っているので、おそらくここでの問題は、GC世代0、1、および2コレクションのパフォーマンスカウンターを調べていたにもかかわらず、GCが十分に機能しなかったことです。

したがって、ここでのポイントは、再利用HttpClientによってメモリリークが発生しないことです。

于 2013-01-31T13:31:00.057 に答える
5

私はメモリの問題を定義するのは得意ではありませんが、次のコードで試してみました。.NET 4.5にあり、C#の非同期/待機機能も使用します。プロセス全体でメモリ使用量が約10〜15 MBに保たれているようです(ただし、これがより良いメモリ使用量であるかどうかはわかりません)。しかし、#Gen 0コレクション#Gen 1コレクション、および#Gen 2コレクションのパフォーマンスカウンターを見ると、以下のコードでかなり高くなっています。

以下の呼び出しを削除するGC.Collectと、プロセス全体で30MBから50MBの間を行き来します。興味深いのは、4コアのマシンでコードを実行しても、プロセスによる異常なメモリ使用量が見られないことです。マシンに.NET4.5をインストールしていますが、インストールしていない場合、問題は.NET 4.0のCLR内部に関連している可能性があり、リソース使用量に基づいて.NET4.5でTPLが大幅に改善されたと確信しています。

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の再利用に沿って設計を再考し、スピンアップされる同時作業の量を制限することをお勧めします。また、複数のマシン間で負荷分散を使用することを検討する必要がある場合もあります。

中途半端なセッションは、Fin-Waits1または2とTime-WaitsまたはRST-WAIT1および2でNETSTATに表示されます。「確立された」セッションでさえ、タイムアウトが発生するのを待つだけで事実上停止する可能性があります。

スタックと.NETはおそらく壊れていません

スタックをオーバーロードすると、マシンがスリープ状態になります。回復には時間がかかり、99%の時間でスタックが回復します。また、.NETは時間の前にリソースを解放せず、GCを完全に制御できるユーザーはいないことにも注意してください。

アプリを強制終了し、NETSTATが落ち着くまでに5分かかる場合、それはシステムが圧倒されているかなり良い兆候です。また、スタックがアプリケーションからどのように独立しているかを示す良い例でもあります。

于 2015-09-09T11:27:25.880 に答える
0

デフォルトHttpClientは、短命のオブジェクトとして使用し、リクエストごとに新しいHttpClientを作成するとリークします。

これがこの振る舞いの再現です。

System.Net.Http回避策として、組み込みアセンブリ の代わりに次のNugetパッケージを使用することで、HttpClientを短命のオブジェクトとして使用し続けることができました: https ://www.nuget.org/packages/HttpClient

このパッケージの出所はわかりませんが、参照するとすぐにメモリリークがなくなりました。組み込みの.NETSystem.Net.Httpライブラリへの参照を削除し、代わりにNugetパッケージを使用してください。

于 2015-09-29T14:55:13.120 に答える