Czy powinniśmy utworzyć nową pojedynczą instancję HttpClient dla wszystkich żądań?

57

ostatnio natknąłem się na ten post na blogu od potworów asp.net, który mówi o problemach z używaniem HttpClientw następujący sposób:

using(var client = new HttpClient())
{
}

Zgodnie z postem na blogu, jeśli usuwamy HttpClientpo każdym żądaniu, może utrzymać otwarte połączenia TCP. Może to potencjalnie prowadzić do System.Net.Sockets.SocketException.

Prawidłowym sposobem według postu jest utworzenie pojedynczej instancji, HttpClientponieważ pomaga to ograniczyć marnotrawstwo gniazd.

Z postu:

Jeśli udostępniamy jedno wystąpienie HttpClient, możemy zmniejszyć marnotrawstwo gniazd, ponownie je wykorzystując:

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

Zawsze korzystałem z HttpClientprzedmiotu po jego użyciu, ponieważ uważałem, że jest to najlepszy sposób na jego użycie. Ale ten post na blogu sprawia, że ​​czuję, że przez tak długi czas robiłem to źle.

Czy powinniśmy utworzyć nową pojedynczą instancję HttpClientdla wszystkich żądań? Czy są jakieś pułapki związane z użyciem instancji statycznej?

Ankit Vijay
źródło
Czy napotkałeś jakieś problemy, które przypisałeś sposobowi korzystania z niego?
whatsisname
Może sprawdź tę odpowiedź, a także to .
John Wu
@ Whatsisname nie Nie, ale patrząc na blog, mam wrażenie, że cały czas źle to wykorzystuję. Dlatego chciałem dowiedzieć się od innych programistów, czy zauważą jakiś problem w obu podejściach.
Ankit Vijay,
3
Nie próbowałem tego sam (więc nie udzielam tego jako odpowiedzi), ale zgodnie z Microsoft .NET Core 2.1 powinieneś używać HttpClientFactory, jak opisano na docs.microsoft.com/en-us/dotnet/standard/ …
Joeri Sebrechts,
(Jak stwierdzono w mojej odpowiedzi, chciałem tylko uczynić ją bardziej widoczną, więc piszę krótki komentarz.) Instancja statyczna poprawnie obsłuży zakończenie połączenia uściślającego połączenie tcp, gdy zrobisz Close()lub zainicjujesz nowy Get(). Jeśli po prostu zlikwidujesz klienta, gdy skończysz z nim, nie będzie nikogo, kto poradziłby sobie z tym zamykającym uściskiem dłoni, i dlatego wszystkie porty będą miały stan TIME_WAIT.
Mladen B.

Odpowiedzi:

39

Wygląda na przekonujący post na blogu. Zanim jednak podejmę decyzję, najpierw przeprowadzę te same testy, które napisał bloger, ale na własnym kodzie. Chciałbym również dowiedzieć się nieco więcej o HttpClient i jego zachowaniu.

Ten post stwierdza:

Instancja HttpClient to zbiór ustawień stosowanych do wszystkich żądań wykonywanych przez tę instancję. Ponadto każda instancja HttpClient korzysta z własnej puli połączeń, izolując swoje żądania od żądań wykonanych przez inne instancje HttpClient.

Więc prawdopodobnie dzieje się, gdy HttpClient jest współdzielony, to że połączenia są ponownie wykorzystywane, co jest w porządku, jeśli nie potrzebujesz trwałych połączeń. Jedynym sposobem, aby dowiedzieć się na pewno, czy ma to znaczenie dla Twojej sytuacji, jest uruchomienie własnych testów wydajności.

Jeśli kopiesz, znajdziesz kilka innych zasobów, które rozwiązują ten problem (w tym artykuł najlepszych praktyk firmy Microsoft), więc prawdopodobnie dobrym pomysłem jest wdrożenie go (z pewnymi środkami ostrożności).

Bibliografia

Używasz Httpclient źle i to destabilizuje twoje oprogramowanie
Singleton HttpClient? Uważaj na to poważne zachowanie i jak je naprawić
Wzorce i praktyki Microsoft - Optymalizacja wydajności: niewłaściwa instancja
Pojedyncze wystąpienie wielokrotnego użytku HttpClient w recenzji kodu
Singleton HttpClient nie przestrzega zmian DNS (CoreFX)
Ogólne porady dotyczące korzystania z HttpClient

Robert Harvey
źródło
1
To dobra obszerna lista. To jest mój weekendowy odczyt.
Ankit Vijay,
„Jeśli kopiesz, znajdziesz kilka innych zasobów, które rozwiązują ten problem ...” chcesz powiedzieć, że problem z połączeniem TCP jest otwarty?
Ankit Vijay,
Krótka odpowiedź: użyj statycznego HttpClient . Jeśli potrzebujesz obsługi zmian DNS (swojego serwera WWW lub innych serwerów), musisz się martwić ustawieniami limitu czasu.
Jess
3
Świadczy to o tym, jak zepsuty HttpClient jest to, że korzystanie z niego jest „weekendowym czytaniem” skomentowanym przez @AnkitVijay.
usr
@ Zgadnij oprócz zmian DNS - czy wyrzucenie całego ruchu twojego klienta przez jedno gniazdo również zakłóci równoważenie obciążenia?
Iain,
16

Spóźniam się na przyjęcie, ale oto moja podróż edukacyjna na ten trudny temat.

1. Gdzie możemy znaleźć oficjalnego rzecznika ponownego użycia HttpClient?

Mam na myśli, że jeśli ponowne użycie HttpClient jest zamierzone i jest to ważne , taki adwokat jest lepiej udokumentowany we własnej dokumentacji API, zamiast ukrywać się w wielu „Zaawansowanych tematach”, „Wzorcu (anty)” lub innych postach na blogu . W przeciwnym razie skąd nowy uczeń powinien to wiedzieć, zanim będzie za późno?

Na dzień dzisiejszy (maj 2018 r.) Pierwszy wynik wyszukiwania w Google „c # httpclient” wskazuje na tę stronę referencyjną interfejsu API w witrynie MSDN , która wcale nie wspomina o tej intencji. Lekcja 1 dla początkujących polega na tym, że zawsze kliknij link „Inne wersje” tuż za nagłówkiem strony pomocy MSDN, prawdopodobnie znajdziesz tam linki do „bieżącej wersji”. W tym przypadku HttpClient przeniesie Cię do najnowszego dokumentu zawierającego opis tego zamiaru .

Podejrzewam, że wielu programistów, którzy byli nowi w tym temacie, również nie znalazło właściwej strony z dokumentacją, dlatego ta wiedza nie jest szeroko rozpowszechniona, a ludzie byli zaskoczeni, gdy odkryli ją później , być może w trudny sposób .

2. (Nie?) Koncepcja using IDisposable

Ten jest nieco nie na temat, ale nadal warto podkreślić, że nie jest to przypadek, aby zobaczyć ludzi w tych wspomnianych blogach obwiniając jak HttpClient„s IDisposableinterfejs sprawia, że mają tendencję do korzystania z using (var client = new HttpClient()) {...}wzorca, a następnie doprowadzić do problemu.

Uważam, że sprowadza się to do niewypowiedzianej (błędnej?) Koncepcji: „oczekuje się, że obiekt IDisposable będzie krótkotrwały” .

JEDNAK, chociaż na pewno wygląda to na coś krótkotrwałego, gdy piszemy kod w tym stylu:

using (var foo = new SomeDisposableObject())
{
    ...
}

Oficjalna dokumentacja IDisposable nie wspomina IDisposableobiekty mają być krótkotrwałe. Z definicji IDisposable jest jedynie mechanizmem umożliwiającym uwolnienie niezarządzanych zasobów. Nic więcej. W tym sensie OCZEKUJESZ, że ostatecznie doprowadzisz do usunięcia, ale nie wymaga to, abyś zrobił to w krótkim czasie.

Dlatego Twoim zadaniem jest właściwy wybór momentu uruchomienia usuwania w oparciu o wymagania dotyczące cyklu życia Twojego obiektu. Nic nie stoi na przeszkodzie, abyś mógł używać IDisposable w długim okresie:

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

Dzięki temu nowemu zrozumieniu, teraz ponownie odwiedzamy ten post na blogu , możemy wyraźnie zauważyć, że „poprawka” inicjuje się HttpClientraz, ale nigdy go nie usuwa, dlatego z jego danych wyjściowych netstat wynika, że ​​połączenie pozostaje w stanie USTALONYM, co oznacza, że ​​ma NIE zostało poprawnie zamknięte. Gdyby był zamknięty, jego stanem byłby TIME_WAIT. W praktyce nie jest wielkim problemem, aby przeciekać tylko jedno połączenie otwarte po zakończeniu całego programu, a plakat na blogu nadal widzi wzrost wydajności po poprawce; ale nadal koncepcyjnie niewłaściwe jest obwinianie IDisposable i NIE wyrzucanie go.

3. Czy musimy umieścić HttpClient we właściwości statycznej, czy nawet jako singleton?

Na podstawie zrozumienia poprzedniej części myślę, że odpowiedź tutaj jest jasna: „niekoniecznie”. To naprawdę zależy od tego, jak zorganizujesz swój kod, pod warunkiem, że użyjesz HttpClient ORAZ (najlepiej) ostatecznie go usuniesz.

Zabawne jest, że nawet przykład w sekcji Uwagi w obecnym oficjalnym dokumencie nie ma racji. Definiuje klasę „GoodController”, zawierającą statyczną właściwość HttpClient, która nie będzie usuwana; co nie zgadza się z tym, co podkreśla inny przykład w sekcji Przykłady : „trzeba zadzwonić do dysponowania ... aby aplikacja nie przeciekała zasobów”.

I wreszcie singleton nie jest pozbawiony własnych wyzwań.

„Ile osób uważa, że ​​zmienna globalna jest dobrym pomysłem? Nikt.

Ile osób uważa singleton za dobry pomysł? Kilka.

Co daje? Singletony to tylko garść zmiennych globalnych. ”

- Cytat z tego inspirującego przemówienia „Global State and Singletons”

PS: SqlConnection

Ten jest nieistotny dla obecnych pytań i odpowiedzi, ale prawdopodobnie jest dobrze znany. Wzorzec użycia SqlConnection jest inny. Państwo nie ma potrzeby ponownego użycia SqlConnection , ponieważ będzie ona obsługiwać jego puli połączeń lepiej.

Różnica wynika z ich podejścia do wdrożenia. Każda instancja HttpClient korzysta z własnej puli połączeń (cytowanej tutaj ); ale zgodnie z tym SqlConnection jest zarządzany przez centralną pulę połączeń .

I nadal musisz pozbyć się SqlConnection, tak jak powinieneś zrobić dla HttpClient.

RayLuo
źródło
14

Zrobiłem kilka testów, aby zobaczyć poprawę wydajności z static HttpClient. Do testów użyłem poniższego kodu:

namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

Dla testów:

  • Uruchomiłem kod z 10, 100, 1000 i 1000 połączeń.
  • Przebiegł każdy test 3 razy, aby sprawdzić średnią.
  • Wykonano jedną metodę na raz

Znalazłem poprawę wydajności od 40% do 60% uon przy użyciu statycznego HttpClientzamiast wysyłania go na HttpClientżądanie. Umieściłem szczegóły wyniku testu wydajności w poście na blogu tutaj .

Ankit Vijay
źródło
1

Aby poprawnie zamknąć połączenie TCP , musimy ukończyć sekwencję pakietów FIN - FIN + ACK - ACK (podobnie jak SYN - SYN + ACK - ACK podczas otwierania połączenia TCP ). Jeśli po prostu wywołamy metodę .Close () (zwykle dzieje się, gdy pozbywa się HttpClient ), i nie czekamy, aż strona zdalna potwierdzi nasze zamknięcie żądania (za pomocą FIN + ACK), otrzymamy stan TIME_WAIT na lokalny port TCP, ponieważ zlikwidowaliśmy naszego odbiornika (HttpClient) i nigdy nie mieliśmy szansy zresetować stanu portu do odpowiedniego stanu zamkniętego, gdy zdalny uczestnik wyśle ​​nam pakiet FIN + ACK.

Właściwym sposobem zamknięcia połączenia TCP byłoby wywołanie metody .Close () i oczekiwanie na zdarzenie close z drugiej strony (FIN + ACK) po naszej stronie. Tylko wtedy możemy wysłać ostateczne potwierdzenie i zlikwidować klienta HttpClient.

Aby dodać, sensowne jest utrzymywanie otwartych połączeń TCP, jeśli wykonujesz żądania HTTP, ze względu na nagłówek HTTP „Connection: Keep-Alive”. Co więcej, możesz poprosić zdalnego partnera, aby zamknął dla ciebie połączenie, ustawiając nagłówek HTTP „Connection: Close”. W ten sposób porty lokalne będą zawsze odpowiednio zamknięte, zamiast znajdować się w stanie TIME_WAIT.

Mladen B.
źródło
1

Oto podstawowy klient API, który skutecznie wykorzystuje HttpClient i HttpClientHandler. Gdy tworzysz nowego klienta HttpClient w celu złożenia wniosku, wiąże się to z dużym nakładem pracy. NIE odtwarzaj ponownie HttpClient dla każdego żądania. Ponownie wykorzystaj HttpClient w jak największym stopniu ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

Stosowanie:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Alper Ebicoglu
źródło
-5

Nie ma jednego sposobu na użycie klasy HttpClient. Kluczem do sukcesu jest zaprojektowanie aplikacji w sposób odpowiedni dla środowiska i ograniczeń.

HTTP to świetny protokół do użycia, gdy trzeba ujawnić publiczne interfejsy API. Może być również skutecznie wykorzystywany w przypadku usług wewnętrznych o niskim opóźnieniu - chociaż wzór kolejki komunikatów RPC jest często lepszym wyborem dla usług wewnętrznych.

Wykonanie HTTP dobrze się komplikuje.

Rozważ następujące:

  1. Utworzenie gniazda i ustanowienie połączenia TCP wymaga przepustowości sieci i czasu.
  2. HTTP / 1.1 obsługuje żądania potokowe na tym samym gnieździe. Wysyłanie wielu żądań jeden po drugim, bez konieczności oczekiwania na poprzednie odpowiedzi - prawdopodobnie odpowiada to za poprawę prędkości zgłoszoną w poście na blogu.
  3. Buforowanie i moduł równoważenia obciążenia - jeśli masz moduł równoważenia obciążenia przed serwerami, to upewnienie się, że Twoje żądania mają odpowiednie nagłówki pamięci podręcznej, może zmniejszyć obciążenie serwerów i szybciej uzyskać odpowiedzi od klientów.
  4. Nigdy nie odpytuj zasobu, używaj dzielenia HTTP, aby zwracać okresowe odpowiedzi.

Ale przede wszystkim przetestuj, zmierz i potwierdź. Jeśli nie zachowuje się zgodnie z planem, możemy odpowiedzieć na konkretne pytania dotyczące sposobu osiągnięcia oczekiwanych rezultatów.

Michael Shaw
źródło
4
To właściwie nie odpowiada na pytania.
whatsisname
Wydaje się, że zakładasz, że istnieje JEDNA poprawna droga. Nie wydaje mi się Wiem, że musisz go używać w odpowiedni sposób, a następnie przetestować i zmierzyć jego zachowanie, a następnie dostosowywać swoje podejście, aż będziesz szczęśliwy.
Michael Shaw,
Napisałeś trochę o tym, czy używać HTTP, czy nie komunikować się. OP zapytał o to, jak najlepiej użyć konkretnego komponentu biblioteki.
whatsisname
1
@MichaelShaw: HttpClientimplementujeIDisposable . Dlatego nie jest nierozsądne oczekiwać, że będzie to krótkotrwały obiekt, który umie po sobie posprzątać, odpowiedni do zawijania usinginstrukcji za każdym razem, gdy jej potrzebujesz. Niestety tak nie działa. Wpis na blogu, do którego prowadzi OP, wyraźnie pokazuje, że istnieją zasoby (w szczególności połączenia przez gniazdo TCP), które istnieją długo po tym, jak usinginstrukcja wyszła poza zakres i HttpClientobiekt został prawdopodobnie usunięty.
Robert Harvey,
1
Rozumiem ten proces myślowy. Po prostu jeśli myślisz o HTTP z punktu widzenia architektury i zamierzasz wysyłać wiele zapytań do tej samej usługi - wtedy myślisz o buforowaniu i potokowaniu, a wtedy myśl o uczynieniu z HttpClient obiektu krótkotrwałego po prostu źle się czuję. Podobnie, jeśli wysyłasz żądania do różnych serwerów i nie uzyskasz żadnych korzyści z utrzymywania gniazda w stanie aktywnym, wówczas usunięcie obiektu HttpClient po jego użyciu ma sens.
Michael Shaw