web-dev-qa-db-ja.com

HttpClientクロールによりメモリリークが発生する

WebCrawler implementation に取り組んでいますが、ASP.NET Web APIのHttpClientで奇妙なメモリリークに直面しています。

だから削減バージョンはここにあります:


[更新2]

私は問題を見つけました、そしてそれはリークしているのはHttpClientではありません。私の答えを見てください。


[更新1]

効果なしで破棄を追加しました:

    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レベルにあるようです。

Memory profile of the process showing buffers held possibly by async callbacks

私はC#4.0を使用しているため、ここでは非同期/待機がないため、TPL 4.0のみが使用されています。

上記のコードは機能しますが、最適化されておらず、時々かんしゃくを投げますが、効果を再現するには十分です。ポイントは、メモリリークが発生する可能性のあるポイントが見つからないことです。

21
Aliostad

わかりました、これで終わりです。これに時間を費やしてくれた@ Tugberk、@ Darrel、@ youssefに感謝します。

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

私はこのコードで数回だけリークを再現することができました-それ以外の場合、成長した後に突然低下するだけでした。私は4.5でGCの刷新があったことを知っているので、おそらくここでの問題は、GC生成0、1、および2コレクションのパフォーマンスカウンターを見ていても、GCが十分に機能しなかったことです。

したがって、ここでの要点は、HttpClientを再利用してもメモリリークが発生しないことです。

20
Aliostad

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

以下のGC.Collect呼び出しを削除すると、プロセス全体で30MBから50MBの間でやり取りされます。おもしろいのは、4コアマシンでコードを実行したときに、プロセスによるメモリ使用量の異常が見られないことです。私のマシンには.NET 4.5がインストールされていますが、インストールされていない場合、問題は.NET 4.0のCLR内部に関連している可能性があります。

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();
            }
        }
    }
}
5
tugberk

QA環境で最近報告された「メモリリーク」により、次のことがわかりました。

TCPスタックを検討してください

TCPスタックは、「アプリケーションに適切だと思われる」時に要求されることを実行できると思い込まないでください。タスクを自由にスピンオフできるので、asychが大好きですが... 。

TCPスタックを見る

メモリリークがあると思われる場合は、NETSTATを実行してください。残りのセッションまたは中途半端な状態が見られる場合は、HTTPClientの再利用に沿って設計を見直し、スピンアップされる同時作業の量を制限することができます。また、複数のマシン間で負荷分散を使用することを検討する必要がある場合もあります。

ハーフベイクセッションは、Fin-Waits 1または2とTime-WaitsまたはRST-WAIT 1および2のNETSTATで表示されます。「確立された」セッションでさえ、タイムアウトが発生するのを待つだけで実質的に停止する可能性があります。

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

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

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

2
John Peters

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

ここ は、この動作の再現です。

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

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

0
Elad Nava