Indeksowanie HttpClient powoduje wyciek pamięci

Pracuję nad implementacją WebCrawler ale mam do czynienia z dziwnym wyciekiem pamięci w ASP.NET HttpClient interfejsu Web API.

Więc Wersja cut down jest tutaj:


[UPDATE 2]

Znalazłem problem i nie wycieka HttpClient. Zobacz moją odpowiedź.


[UPDATE 1]

Dodałem bez efektu:

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

Plik zawierający linki jest dostępny tutaj .

Daje to stałą powstanie pamięci. Analiza pamięci pokazuje wiele bajtów utrzymywanych prawdopodobnie przez AsyncCallback. Zrobiłem wcześniej wiele analiz wycieku pamięci, ale ten wydaje się być na poziomie HttpClient.

Profil pamięci procesu pokazujący bufory utrzymywane prawdopodobnie przez wywołania asynchroniczne

Używam C# 4.0, więc nie ma tu asynchronicznych/oczekujących, więc używa się tylko TPL 4.0.

Powyższy kod działa, ale nie jest zoptymalizowany i czasami rzuca furią, ale jest wystarczający do odtworzenia efektu. Chodzi o to, że nie mogę znaleźć żadnego punktu, który mógłby spowodować wyciek pamięci.

Author: Aliostad, 2012-12-28

4 answers

OK, doszedłem do sedna sprawy. Dzięki @Tugberk, @ Darrel i @youssef za spędzanie czasu na tym.

W zasadzie początkowy problem polegał na tym, że wykonywałem zbyt wiele zadań. To zaczęło zbierać swoje żniwo, więc musiałem ograniczyć to i mieć jakiś stan, aby upewnić się, że liczba jednoczesnych zadań jest ograniczona. jest to w zasadzie duże wyzwanie dla pisania procesów, które muszą używać TPL do planowania zadań. Możemy kontrolować wątki w puli wątków, ale musimy również kontrolować zadania, które tworzymy, więc żaden poziom async/await w tym nie pomoże.

Udało mi się odtworzyć przeciek tylko kilka razy z tym kodem - innym razem po wyrośnięciu nagle spadnie. Wiem, że w 4.5 była przeróbka GC, więc może tu chodzi o to, że GC nie kopnęło wystarczająco, chociaż patrzyłem na liczniki perf na kolekcjach GC generacji 0, 1 i 2.

Czyli ponowne użycie HttpClient nie powoduje wycieku pamięci.

 21
Author: Aliostad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-28 10:29:06

Nie jestem dobry w definiowaniu problemów z pamięcią, ale wypróbowałem poniższy kod. Jest w. NET 4.5 i używa również funkcji async / wait w C#. Wydaje się, że zużycie pamięci wynosi około 10-15 MB dla całego procesu(nie jestem pewien, czy widzisz to lepsze zużycie pamięci). Ale jeśli obejrzysz # Gen 0 Kolekcje, # Kolekcje Gen 1 i # Kolekcje Gen 2 liczniki perf, są dość wysokie z poniższym kodem.

Jeśli usuniesz GC.Collect wywołania poniżej, to przechodzi tam iz powrotem między 30MB - 50MB dla całego procesu. Interesujące jest to, że kiedy uruchamiam Twój kod na moim 4 rdzeniowym komputerze, nie widzę nieprawidłowego zużycia pamięci przez proces. Mam. NET 4.5 zainstalowany na moim komputerze, a jeśli nie, problem może być związany z CLR wewnętrzne. NET 4.0 i jestem pewien, że TPL znacznie poprawiła się na.Net 4.5 W oparciu o wykorzystanie zasobów.

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
Author: tugberk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-30 00:22:08

Ostatnio zgłoszony "wyciek pamięci" w naszym środowisku QA nauczył nas tego:

Rozważmy stos TCP

Nie zakładaj, że stos TCP może wykonać to, co jest wymagane w czasie "uważanym za właściwe dla aplikacji". Jasne, że możemy oddzielić zadania do woli i po prostu kochamy asycha, ale....

Obserwuj stos TCP

Uruchom NETSTAT, gdy wydaje ci się, że masz wyciek pamięci. Jeśli widzisz Pozostałe sesje lub stany w połowie upieczone, możesz przemyśleć swój projekt zgodnie z HttpClient ponowne wykorzystanie i ograniczenie ilości pracy współbieżnej. Konieczne może być również rozważenie zastosowania równoważenia obciążenia na wielu maszynach.

Sesje Half-baked pojawiają się w NETSTAT z Fin-Waits 1 lub 2 i Time-Waits lub nawet RST-WAIT 1 i 2. Nawet" ustalone " sesje mogą być praktycznie martwe, tylko czekając na przerwy w strzelaniu.

Stos i. NET najprawdopodobniej nie są zepsute

Przeciążenie stosu powoduje usypianie maszyny. Odzyskiwanie zajmuje czas i 99% czasu stos wyzdrowieje. Pamiętaj również, że. NET NIE WYDA zasobów przed upływem czasu i że żaden użytkownik nie ma pełnej kontroli nad GC.

Jeśli zabijesz aplikację i NETSTAT zajmie 5 minut, to całkiem dobry znak, że system jest przytłoczony. Jest to również dobry pokaz tego, jak stos jest niezależny od aplikacji.

 2
Author: JWP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Domyślny HttpClient przecieka, gdy używasz go jako krótkotrwałego obiektu i tworzysz nowe HttpClients na żądanie.

Tutaj jest reprodukcją tego zachowania.

Jako obejście udało mi się nadal używać HttpClient jako krótkotrwałego obiektu, używając następującego pakietu Nuget zamiast wbudowanego zestawu System.Net.Http: https://www.nuget.org/packages/HttpClient

Nie wiem, jakie jest pochodzenie tego pakietu, jednak, jak tylko wspomniałem, wyciek pamięci zniknął. Upewnij się, że usuniesz odniesienie do wbudowanej biblioteki.NET System.Net.Http i zamiast tego użyjesz pakietu Nuget.

 0
Author: Elad Nava,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:26