webentwicklung-frage-antwort-db.com.de

Das Crawlen von HttpClient führt zu einem Speicherverlust

Ich arbeite an einer WebCrawler Implementierung , stelle jedoch einen merkwürdigen Speicherverlust im HttpClient der ASP.NET-Web-API fest.

Die gekürzte Version ist also hier:


[UPDATE 2]

Ich habe das Problem gefunden und es ist nicht HttpClient, der undicht ist. Siehe meine Antwort.


[UPDATE 1]

Ich habe hinzugefügt, ohne Wirkung zu entsorgen:

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

Die Datei mit den Links ist hier verfügbar.

Dies führt zu einem konstanten Anstieg des Speichers. Die Speicheranalyse zeigt viele Bytes, die möglicherweise vom AsyncCallback gehalten werden. Ich habe schon viele Speicherverluste analysiert, aber dieser scheint auf der HttpClient-Ebene zu sein.

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

Ich verwende C # 4.0, also keine asynchrone/Warte hier, so dass nur TPL 4.0 verwendet wird.

Der obige Code funktioniert, ist jedoch nicht optimiert und löst manchmal Wutanfälle aus, die jedoch ausreichen, um den Effekt zu reproduzieren. Punkt ist, ich kann keinen Punkt finden, der zu einem Speicherverlust führen könnte.

18
Aliostad

OK, ich bin dem auf den Grund gegangen. Vielen Dank an @Tugberk, @Darrel und @youssef, die sich damit beschäftigt haben.

Grundsätzlich war das anfängliche Problem, dass ich zu viele Aufgaben verursachte. Dies begann seinen Tribut zu fordern, so dass ich dies reduzieren und einen gewissen Status haben musste, um sicherzustellen, dass die Anzahl der gleichzeitigen Aufgaben begrenzt ist. Dies ist im Grunde eine große Herausforderung für Schreibprozesse, die TPL verwenden müssen, um die Aufgaben zu planen. Wir können Threads im Thread-Pool steuern, aber wir müssen auch die Aufgaben steuern, die wir erstellen, damit kein Level von async/await dabei hilft.

Mit diesem Code konnte ich das Leck nur ein paar Mal reproduzieren - ein anderes Mal fiel es nach dem Wachsen plötzlich ab. Ich weiß, dass es in 4.5 eine Überarbeitung der GC gab. Vielleicht ist das Problem hier, dass die GC nicht genug eingesetzt hat, obwohl ich mir die Leistungsindikatoren für die GC-Sammlungen der Generation 0, 1 und 2 angesehen habe.

Der Vorteil hier ist, dass die Wiederverwendung von HttpClient KEIN Speicherverlust verursacht.

19
Aliostad

Ich bin nicht gut darin, Speicherprobleme zu definieren, aber ich habe es mit dem folgenden Code versucht. Es ist in .NET 4.5 und verwendet auch die async/await-Funktion von C #. Die Speichernutzung scheint während des gesamten Vorgangs zwischen 10 und 15 MB zu liegen (nicht sicher, ob Sie dies für eine bessere Speichernutzung halten). Aber wenn Sie # Gen 0 Sammlungen , # Gen 1 Sammlungen und # Gen 2 Sammlungen Leistungsindikatoren beobachten, sind sie mit dem folgenden Code ziemlich hoch.

Wenn Sie die nachstehenden GC.Collect-Aufrufe entfernen, wird der gesamte Vorgang zwischen 30 MB und 50 MB hin und her verschoben. Der interessante Teil ist, dass ich beim Ausführen Ihres Codes auf meinem 4-Core-Computer auch keine abnormale Speichernutzung sehe. Ich habe .NET 4.5 auf meinem Computer installiert, und wenn nicht, hängt das Problem möglicherweise mit den CLR-Interna von .NET 4.0 zusammen, und ich bin mir sicher, dass TPL in .NET 4.5 aufgrund der Ressourcennutzung eine Menge verbessert hat.

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

Ein kürzlich gemeldeter "Memory Leak" in unserer QS-Umgebung hat uns Folgendes gelehrt:

Betrachten Sie den Stapel TCP

Gehen Sie nicht davon aus, dass der TCP Stack das kann, was in der Zeit "für die Anwendung für angemessen gehalten" gefragt wird. Sicher, wir können Aufgaben nach Belieben abspalten und wir lieben Asych, aber ...

Beobachten Sie den TCP Stack

Führen Sie NETSTAT aus, wenn Sie glauben, dass ein Speicherverlust vorliegt. Wenn Sie verbleibende Sitzungen oder halbfertige Zustände sehen, möchten Sie möglicherweise Ihr Design im Sinne der Wiederverwendung von HTTPClient überdenken und den Umfang der gleichzeitig ausgeführten Arbeit begrenzen. Möglicherweise müssen Sie auch die Verwendung des Lastenausgleichs für mehrere Computer in Betracht ziehen.

Half-Baked-Sessions werden in NETSTAT mit Fin-Waits 1 oder 2 und Time-Waits oder sogar RST-WAIT 1 und 2 angezeigt. Selbst "etablierte" Sessions können so gut wie tot sein, wenn sie nur auf Timeouts warten.

Der Stack und .NET sind höchstwahrscheinlich nicht kaputt

Durch Überladen des Stapels wird die Maschine in den Ruhezustand versetzt. Die Wiederherstellung benötigt Zeit und 99% der Zeit, in der der Stack wiederhergestellt wird. Denken Sie auch daran, dass .NET Ressourcen nicht vor Ablauf dieser Zeit freigibt und dass kein Benutzer die vollständige Kontrolle über GC hat.

Wenn Sie die App beenden und es 5 Minuten dauert, bis sich NETSTAT beruhigt hat, ist das ein gutes Zeichen dafür, dass das System überfordert ist. Es ist auch ein gutes Beispiel dafür, wie unabhängig der Stack von der Anwendung ist.

2
John Peters

Die Standardvariable "HttpClient" tritt auf, wenn Sie sie als kurzlebiges Objekt verwenden und pro Anforderung neue HTTP-Clients erstellen.

Hier ist eine Reproduktion dieses Verhaltens.

Als Problemumgehung konnte ich HttpClient als kurzlebiges Objekt verwenden, indem ich das folgende Nuget-Paket anstelle der integrierten System.Net.Http-Assembly verwendete: https://www.nuget.org/packages/HttpClient

Ich bin mir nicht sicher, woher dieses Paket stammt. Sobald ich darauf verwiesen habe, ist der Speicherverlust verschwunden. Stellen Sie sicher, dass Sie den Verweis auf die integrierte .NET System.Net.Http-Bibliothek entfernen und stattdessen das Nuget-Paket verwenden.

0
Elad Nava