Jakie są narzuty związane z tworzeniem nowego HttpClient na wywołanie w kliencie WebAPI?

162

Jaki powinien być HttpClientczas życia klienta WebAPI?
Czy lepiej jest mieć jedną instancję HttpClientdla wielu wywołań?

Jakie są narzuty związane z tworzeniem i usuwaniem HttpClientżądania na żądanie, jak w przykładzie poniżej (pobrane z http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from- klient sieciowy ):

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:9000/");
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // New code:
    HttpResponseMessage response = await client.GetAsync("api/products/1");
    if (response.IsSuccessStatusCode)
    {
        Product product = await response.Content.ReadAsAsync<Product>();
        Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
    }
}
Bruno Pessanha
źródło
Nie jestem jednak pewien, możesz użyć tej Stopwatchklasy do porównania jej. Moim zdaniem bardziej sensowne byłoby posiadanie jednego HttpClient, zakładając, że wszystkie te instancje są używane w tym samym kontekście.
Matthew

Odpowiedzi:

215

HttpClientzostał zaprojektowany do ponownego wykorzystania w wielu połączeniach . Nawet w wielu wątkach. HttpClientHandlerMa mandatów oraz plików cookie, które są przeznaczone do ponownego użycia w poprzek połączeń. Posiadanie nowej HttpClientinstancji wymaga ponownego skonfigurowania wszystkich tych rzeczy. PonadtoDefaultRequestHeaders właściwość zawiera właściwości przeznaczone dla wielu wywołań. Konieczność resetowania tych wartości przy każdym żądaniu mija się z celem.

Inną ważną zaletą HttpClientjest możliwość dodawania HttpMessageHandlersdo potoku żądań / odpowiedzi w celu zastosowania przekrojowych problemów. Mogą to być rejestrowanie, audyt, ograniczanie przepustowości, obsługa przekierowań, obsługa offline, przechwytywanie metryk. Wiele różnych rzeczy. Jeśli nowy HttpClient jest tworzony dla każdego żądania, wszystkie te programy obsługi komunikatów muszą być skonfigurowane dla każdego żądania i w jakiś sposób należy również podać stan poziomu aplikacji, który jest współużytkowany między żądaniami dla tych programów obsługi.

Im częściej używasz funkcji programu HttpClient, tym bardziej przekonasz się, że ponowne wykorzystanie istniejącej instancji ma sens.

Jednak moim zdaniem największym problemem jest to, że kiedy HttpClientklasa jest usuwana, usuwa HttpClientHandler, co następnie wymusza zamknięcie TCP/IPpołączenia w puli połączeń, którą zarządza ServicePointManager. Oznacza to, że każde żądanie z nowym HttpClientwymaga ponownego ustanowienia nowego TCP/IPpołączenia.

Z moich testów, używając zwykłego protokołu HTTP w sieci LAN, wpływ wydajności jest dość znikomy. Podejrzewam, że dzieje się tak dlatego, że istnieje podstawowa funkcja utrzymywania aktywności TCP, która wstrzymuje połączenie, nawet gdy HttpClientHandlerpróbuje je zamknąć.

W przypadku żądań przesyłanych przez Internet spotkałem się z inną historią. Widziałem 40% spadek wydajności z powodu konieczności ponownego otwierania żądania za każdym razem.

Podejrzewam, że uderzenie w HTTPSpołączenie byłoby jeszcze gorsze.

Moja rada jest taka, aby zachować wystąpienie HttpClient przez cały okres istnienia aplikacji dla każdego odrębnego interfejsu API, z którym się łączysz.

Darrel Miller
źródło
5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerJak bardzo jesteś pewien tego stwierdzenia? Trudno w to uwierzyć. HttpClientwygląda dla mnie jak jednostka pracy, która powinna być często tworzona.
usr
2
@vkelman Tak, nadal możesz ponownie używać wystąpienia HttpClient, nawet jeśli utworzono je za pomocą nowego HttpClientHandler. Należy również zauważyć, że istnieje specjalny konstruktor dla HttpClient, który umożliwia ponowne użycie HttpClientHandler i pozbycie się HttpClient bez przerywania połączenia.
Darrel Miller,
2
@vkelman Wolę trzymać HttpClient w pobliżu, ale jeśli wolisz trzymać HttpClientHandler w pobliżu, utrzyma połączenie otwarte, gdy drugi parametr będzie fałszywy.
Darrel Miller
2
@DarrelMiller Wygląda na to, że połączenie jest powiązane z HttpClientHandler. Wiem, że w celu skalowania nie chcę niszczyć połączenia, więc muszę zachować HttpClientHandler i utworzyć wszystkie moje wystąpienia HttpClient z tego LUB utworzyć statyczne wystąpienie HttpClient. Jeśli jednak CookieContainer jest powiązany z HttpClientHandler, a moje pliki cookie muszą się różnić w zależności od żądania, co zalecamy? Chciałbym uniknąć synchronizacji wątków na statycznym HttpClientHandler, modyfikując jego CookieContainer dla każdego żądania.
Dave Black
2
@ Sana.91 Możesz. Byłoby lepiej zarejestrować go jako singleton w zbiorze usług i uzyskać do niego dostęp w ten sposób.
Darrel Miller,
69

Jeśli chcesz, aby Twoja aplikacja była skalowalna, różnica jest OGROMNA! W zależności od obciążenia zobaczysz bardzo różne wartości wydajności. Jak wspomina Darrel Miller, HttpClient został zaprojektowany do ponownego wykorzystania w żądaniach. Zostało to potwierdzone przez facetów z zespołu BCL, którzy to napisali.

Niedawnym projektem, który miałem, była pomoc bardzo dużemu i znanemu sklepowi internetowemu z komputerami w zakresie ruchu w Czarny piątek / święta dla niektórych nowych systemów. Wystąpiły problemy z wydajnością związane z użyciem HttpClient. Ponieważ implementuje IDisposable, programiści zrobili to, co normalnie zrobiłbyś, tworząc instancję i umieszczając ją w using()instrukcji. Kiedy zaczęliśmy testować obciążenie, aplikacja rzuciła serwer na kolana - tak, serwer nie tylko aplikacja. Powodem jest to, że każde wystąpienie HttpClient otwiera port na serwerze. Ze względu na niedeterministyczną finalizację GC i fakt, że pracujesz z zasobami komputerowymi obejmującymi wiele warstw OSI , zamykanie portów sieciowych może trochę potrwać. W rzeczywistości system operacyjny Windows samZamknięcie portu może zająć do 20 sekund (na Microsoft). Otwieraliśmy porty szybciej, niż można było je zamknąć - wyczerpanie portu serwera, które obciążyło procesor w 100%. Moja poprawka polegała na zmianie HttpClient na instancję statyczną, co rozwiązało problem. Tak, jest to zasób jednorazowy, ale różnica w wydajności znacznie przewyższa wszelkie koszty ogólne. Zachęcam do przeprowadzenia testów obciążenia, aby zobaczyć, jak zachowuje się Twoja aplikacja.

Możesz również sprawdzić stronę z wytycznymi dotyczącymi interfejsu WebAPI w celu uzyskania dokumentacji i przykładów pod adresem https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Zwróć szczególną uwagę na to wezwanie:

HttpClient ma być utworzony raz i ponownie użyty przez cały okres istnienia aplikacji. Szczególnie w aplikacjach serwerowych utworzenie nowej instancji HttpClient dla każdego żądania spowoduje wyczerpanie liczby gniazd dostępnych przy dużym obciążeniu. Spowoduje to błędy SocketException.

Jeśli okaże się, że musisz użyć statycznej HttpClientz różnymi nagłówkami, adresem bazowym itp., Co musisz zrobić, to utworzyć HttpRequestMessageręcznie i ustawić te wartości w pliku HttpRequestMessage. Następnie użyjHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

UPDATE for .NET Core : IHttpClientFactorydo tworzenia HttpClientwystąpień należy użyć metody via Dependency Injection . Będzie zarządzał Twoim życiem i nie musisz go jawnie pozbywać. Zobacz Tworzenie żądań HTTP przy użyciu IHttpClientFactory w ASP.NET Core

Dave Black
źródło
1
ten post zawiera przydatne informacje dla tych, którzy będą przeprowadzać testy warunków skrajnych ..!
Sana. 91
9

Jak podano w innych odpowiedziach, HttpClientjest przeznaczony do ponownego wykorzystania. Jednak ponowne użycie pojedynczego HttpClientwystąpienia w aplikacji wielowątkowej oznacza, że ​​nie można zmienić wartości jego właściwości stanowych, takich jak BaseAddressi DefaultRequestHeaders(więc można ich używać tylko wtedy, gdy są stałe w całej aplikacji).

Jednym ze sposobów na poruszanie się po to ograniczenie jest opakowanie HttpClientz klasy, która powiela wszystkie HttpClientmetody, czego potrzeba ( GetAsync, PostAsyncetc) oraz delegatów je do Singleton HttpClient. Jest to jednak dość żmudne (będziesz musiał również opakować metody rozszerzające ) i na szczęście jest inny sposób - twórz nowe HttpClientinstancje, ale ponownie używaj bazowego HttpClientHandler. Tylko upewnij się, że nie pozbędziesz się obsługi:

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
    //client code can dispose these HttpClient instances
    return new HttpClient(_sharedHandler, disposeHandler: false)         
    {
       DefaultRequestHeaders = 
       {
            Authorization = new AuthenticationHeaderValue("Bearer", token) 
       } 
    };
}
Ohad Schneider
źródło
2
Lepszym sposobem jest zachowanie jednego wystąpienia HttpClient, a następnie utworzenie własnych lokalnych wystąpień HttpRequestMessage, a następnie użycie metody .SendAsync () na HttpClient. W ten sposób nadal będzie bezpieczny dla wątków. Każdy HttpRequestMessage będzie miał własne wartości uwierzytelniania / adresu URL.
Tim P.
@TimP. dlaczego jest lepiej SendAsyncjest znacznie mniej wygodne niż dedykowanych metod, takich jak PutAsync, PostAsJsonAsyncitd.
Ohad Schneider
2
SendAsync pozwala zmienić adres URL i inne właściwości, takie jak nagłówki, i nadal zapewniać bezpieczeństwo wątków.
Tim P.
2
Tak, przewodnik jest kluczem. Tak długo, jak jest to współdzielone między wystąpieniami HttpClient, wszystko jest w porządku. Źle odczytałem twój wcześniejszy komentarz.
Dave Black
1
Jeśli utrzymujemy współdzieloną procedurę obsługi, czy nadal musimy zająć się przestarzałym problemem DNS?
shanti
5

Powiązane z witrynami sieci Web o dużej objętości, ale nie bezpośrednio z HttpClient. Poniżej mamy fragment kodu we wszystkich naszych usługach.

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
        const int DefaultConnectionLeaseTimeout = 60000;

        ServicePoint sp =
                ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
        sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

Od https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2); k (DevLang-csharp) & rd = true

„Możesz użyć tej właściwości, aby upewnić się, że aktywne połączenia obiektu ServicePoint nie pozostaną otwarte w nieskończoność. Ta właściwość jest przeznaczona dla scenariuszy, w których połączenia powinny być przerywane i wznawiane okresowo, takich jak scenariusze równoważenia obciążenia.

Domyślnie, gdy KeepAlive ma wartość true dla żądania, właściwość MaxIdleTime ustawia limit czasu dla zamykania połączeń ServicePoint z powodu braku aktywności. Jeśli ServicePoint ma aktywne połączenia, MaxIdleTime nie działa, a połączenia pozostają otwarte przez czas nieokreślony.

Gdy właściwość ConnectionLeaseTimeout jest ustawiona na wartość inną niż-1 i po upływie określonego czasu aktywne połączenie ServicePoint jest zamykane po obsłużeniu żądania przez ustawienie KeepAlive na false w tym żądaniu. Ustawienie tej wartości ma wpływ na wszystkie połączenia zarządzane przez obiekt ServicePoint ”.

Jeśli masz usługi za CDN lub innym punktem końcowym, który chcesz przełączać w tryb failover, to ustawienie pomaga dzwoniącym podążać za Tobą do nowego miejsca docelowego. W tym przykładzie 60 sekund po przełączeniu awaryjnym wszyscy wywołujący powinni ponownie połączyć się z nowym punktem końcowym. Wymaga to znajomości usług zależnych (usług, które TY wywołujesz) i ich punktów końcowych.

Brak zwrotów Brak zwrotów
źródło
Wciąż obciążasz serwer, otwierając i zamykając połączenia. Jeśli używasz opartych na wystąpieniach HttpClients z opartymi na wystąpieniach HttpClientHandlers, nadal będziesz wyczerpać port, jeśli nie będziesz ostrożny.
Dave Black
Nie zgadzam się. Wszystko jest kompromisem. Dla nas śledzenie przekierowanego CDN lub DNS to pieniądze w banku vs. utracone przychody.
Brak zwrotów Brak zwrotów
1

Możesz również odnieść się do tego posta na blogu autorstwa Simona Timmsa: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Ale HttpClientjest inny. Chociaż implementuje IDisposableinterfejs, w rzeczywistości jest to obiekt współdzielony. Oznacza to, że pod osłonami jest ponownie wchodzący) i bezpieczny dla wątków. Zamiast tworzyć nowe wystąpienie HttpClientdla każdego wykonania, należy udostępniać jedno wystąpienie HttpClientprzez cały okres istnienia aplikacji. Spójrzmy, dlaczego.

SvenAelterman
źródło
1

Należy zwrócić uwagę na to, że żadna z notatek z blogów „nie używaj” polega na tym, że należy wziąć pod uwagę nie tylko adres BaseAddress i DefaultHeader. Po ustawieniu HttpClient statycznego istnieją stany wewnętrzne, które będą przenoszone między żądaniami. Przykład: Uwierzytelniasz firmę zewnętrzną za pomocą HttpClient, aby uzyskać token FedAuth (zignoruj, dlaczego nie używasz OAuth / OWIN / itp.), Ta wiadomość odpowiedzi ma nagłówek Set-Cookie dla FedAuth, który jest dodawany do stanu HttpClient. Następny użytkownik, który zaloguje się do Twojego API, wyśle ​​plik cookie FedAuth ostatniej osoby, chyba że zarządzasz tymi plikami cookie przy każdym żądaniu.

eskapizmc
źródło
0

Po pierwsze, podczas gdy ta klasa jest jednorazowa, używanie jej z usinginstrukcją nie jest najlepszym wyborem, ponieważ nawet gdy wyrzucaszHttpClient obiektu gniazdo bazowe nie jest natychmiast zwalniane i może spowodować poważny problem o nazwie „wyczerpanie gniazd.

Ale jest drugi problem HttpClient, który możesz mieć, gdy używasz go jako obiektu pojedynczego lub statycznego. W tym przypadku singleton lub static HttpClientnie są uwzględnianeDNS zmian.

w .net core możesz zrobić to samo z HttpClientFactory, coś takiego:

public interface IBuyService
{
    Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
    private readonly HttpClient _httpClient;

    public BuyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Buy> GetBuyItems()
    {
        var uri = "Uri";

        var responseString = await _httpClient.GetStringAsync(uri);

        var buy = JsonConvert.DeserializeObject<Buy>(responseString);
        return buy;
    }
}

ConfigureServices

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

dokumentację i przykład tutaj

Reza Jenabi
źródło