Was ist der Aufwand für das Erstellen eines neuen HttpClient pro Aufruf in einem WebAPI-Client?

162

Was soll das sein HttpClient Lebensdauer eines WebAPI-Clients sein?
Ist es besser, eine Instanz von HttpClientfür mehrere Anrufe zu haben?

Was ist der Aufwand für das Erstellen und Entsorgen einer HttpClientAnfrage, wie im folgenden Beispiel (entnommen aus http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-) ? a-net-client ):

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
quelle
Ich bin mir nicht sicher, ob Sie die StopwatchKlasse zum Benchmarking verwenden können. Nach meiner Einschätzung ist es sinnvoller, eine einzige zu haben HttpClient, vorausgesetzt, alle diese Instanzen werden im selben Kontext verwendet.
Matthew

Antworten:

215

HttpClientwurde für die Wiederverwendung für mehrere Anrufe entwickelt . Auch über mehrere Threads hinweg. Das HttpClientHandlerverfügt über Anmeldeinformationen und Cookies, die für Anrufe wiederverwendet werden sollen. Um eine neue HttpClientInstanz zu haben, müssen Sie all diese Dinge neu einrichten. Außerdem DefaultRequestHeadersenthält die Eigenschaft Eigenschaften, die für mehrere Aufrufe vorgesehen sind. Wenn Sie diese Werte bei jeder Anforderung zurücksetzen müssen, wird der Punkt zunichte gemacht.

Ein weiterer großer Vorteil von HttpClient ist die Möglichkeit, HttpMessageHandlersdie Anforderungs- / Antwort-Pipeline zu erweitern, um Querschnittsthemen anzuwenden. Diese können zum Protokollieren, Überwachen, Drosseln, Umleiten, Offline-Behandeln und Erfassen von Metriken verwendet werden. Alle möglichen Dinge. Wenn für jede Anforderung ein neuer HttpClient erstellt wird, müssen alle diese Nachrichtenhandler für jede Anforderung eingerichtet werden, und es muss auch jeder Status auf Anwendungsebene bereitgestellt werden, der von Anforderungen für diese Handler gemeinsam genutzt wird.

Je mehr Sie die Funktionen von nutzen HttpClient , desto mehr werden Sie feststellen, dass die Wiederverwendung einer vorhandenen Instanz sinnvoll ist.

Das größte Problem ist jedoch meiner Meinung nach, dass eine HttpClientKlasse , wenn sie entsorgt wird HttpClientHandler, entsorgt , wodurch die TCP/IPVerbindung in dem von ihnen verwalteten Verbindungspool zwangsweise geschlossen wird ServicePointManager. Dies bedeutet, dass für jede Anforderung mit einer neuen HttpClienteine neue TCP/IPVerbindung wiederhergestellt werden muss .

Nach meinen Tests mit einfachem HTTP in einem LAN ist der Leistungseinbruch ziemlich vernachlässigbar. Ich vermute, dies liegt daran, dass ein zugrunde liegendes TCP-Keepalive die Verbindung offen hält, selbst wenn HttpClientHandlerversucht wird, sie zu schließen.

Auf Anfragen, die über das Internet gehen, habe ich eine andere Geschichte gesehen. Ich habe einen Leistungseinbruch von 40% festgestellt, weil ich die Anfrage jedes Mal neu öffnen musste.

Ich vermute, dass der Treffer bei einer HTTPSVerbindung noch schlimmer wäre.

Mein Rat ist , eine Instanz von HttpClient für die Lebensdauer Ihrer Anwendung für jede einzelne API, mit der Sie eine Verbindung herstellen, beizubehalten.

Darrel Miller
quelle
5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerWie sicher sind Sie sich über diese Aussage? Das ist kaum zu glauben. HttpClientsieht für mich aus wie eine Arbeitseinheit, die oft instanziiert werden soll.
usr
2
@vkelman Ja, Sie können eine Instanz von HttpClient auch dann wiederverwenden, wenn Sie sie mit einem neuen HttpClientHandler erstellt haben. Beachten Sie auch, dass es für HttpClient einen speziellen Konstruktor gibt, mit dem Sie einen HttpClientHandler wiederverwenden und den HttpClient entsorgen können, ohne die Verbindung zu beenden.
Darrel Miller
2
@vkelman Ich bevorzuge es, den HttpClient in der Nähe zu halten, aber wenn Sie es vorziehen, den HttpClientHandler in der Nähe zu halten, bleibt die Verbindung offen, wenn der zweite Parameter falsch ist.
Darrel Miller
2
@DarrelMiller Es klingt also so, als ob die Verbindung an den HttpClientHandler gebunden ist. Ich weiß, dass ich zum Skalieren die Verbindung nicht zerstören möchte, daher muss ich entweder einen HttpClientHandler behalten und alle meine HttpClient-Instanzen daraus erstellen ODER eine statische HttpClient-Instanz erstellen. Was empfehlen Sie jedoch, wenn der CookieContainer an den HttpClientHandler gebunden ist und meine Cookies je nach Anforderung unterschiedlich sein müssen? Ich möchte die Thread-Synchronisation auf einem statischen HttpClientHandler vermeiden, indem ich seinen CookieContainer für jede Anforderung ändere.
Dave Black
2
@ Sana.91 Du könntest. Es ist besser, es als Singleton in der Service-Sammlung zu registrieren und auf diese Weise darauf zuzugreifen.
Darrel Miller
69

Wenn Sie möchten, dass Ihre Anwendung skaliert, ist der Unterschied RIESIG! Abhängig von der Last sehen Sie sehr unterschiedliche Leistungszahlen. Wie Darrel Miller erwähnt, wurde der HttpClient so konzipiert, dass er bei allen Anforderungen wiederverwendet werden kann. Dies wurde von den Leuten im BCL-Team bestätigt, die es geschrieben haben.

Ein aktuelles Projekt war es, einem sehr großen und bekannten Online-Computerhändler dabei zu helfen, für einige neue Systeme den Black Friday / Holiday-Verkehr zu skalieren. Bei der Verwendung von HttpClient sind einige Leistungsprobleme aufgetreten. Da es implementiert wird IDisposable, haben die Entwickler das getan, was Sie normalerweise tun würden, indem sie eine Instanz erstellt und in eine using()Anweisung eingefügt haben . Als wir mit dem Lasttest begannen, brachte die App den Server in die Knie - ja, der Server nicht nur die App. Der Grund ist, dass jede Instanz von HttpClient einen Port auf dem Server öffnet. Aufgrund der nicht deterministischen Finalisierung von GC und der Tatsache, dass Sie mit Computerressourcen arbeiten, die sich über mehrere OSI-Ebenen erstrecken , kann das Schließen von Netzwerkports eine Weile dauern. In der Tat Windows-Betriebssystem selbstDas Schließen eines Ports kann bis zu 20 Sekunden dauern (laut Microsoft). Wir haben Ports schneller geöffnet, als sie geschlossen werden konnten - Erschöpfung des Server-Ports, die die CPU auf 100% hämmerte. Mein Fix bestand darin, den HttpClient in eine statische Instanz zu ändern, die das Problem löste. Ja, es handelt sich um eine verfügbare Ressource, aber der Overhead wird durch den Leistungsunterschied bei weitem aufgewogen. Ich empfehle Ihnen, einige Lasttests durchzuführen, um zu sehen, wie sich Ihre App verhält.

Dokumentation und Beispiel finden Sie auch auf der Seite WebAPI-Anleitung unter https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Achten Sie besonders auf diesen Hinweis:

HttpClient soll einmal instanziiert und während der gesamten Lebensdauer einer Anwendung wiederverwendet werden. Insbesondere in Serveranwendungen wird durch das Erstellen einer neuen HttpClient-Instanz für jede Anforderung die Anzahl der unter hoher Last verfügbaren Sockets erschöpft. Dies führt zu SocketException-Fehlern.

Wenn Sie feststellen, dass Sie eine Statik HttpClientmit unterschiedlichen Headern, Basisadressen usw. verwenden müssen, müssen Sie die HttpRequestMessagemanuell erstellen und diese Werte auf dem festlegen HttpRequestMessage. Verwenden Sie dann dieHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

UPDATE für .NET Core : Sie sollten die IHttpClientFactoryVia Dependency Injection verwenden, um HttpClientInstanzen zu erstellen . Es wird die Lebensdauer für Sie verwalten und Sie müssen es nicht explizit entsorgen. Siehe Erstellen von HTTP-Anforderungen mit IHttpClientFactory in ASP.NET Core

Dave Black
quelle
1
Dieser Beitrag enthält nützliche Einblicke für diejenigen, die Stresstests durchführen werden ..!
Sana.91
9

Wie die anderen Antworten besagen, HttpClientist es zur Wiederverwendung gedacht. Wenn Sie jedoch eine einzelne HttpClientInstanz in einer Multithread-Anwendung wiederverwenden , können Sie die Werte ihrer statusbehafteten Eigenschaften wie BaseAddressund nicht ändern DefaultRequestHeaders(Sie können sie also nur verwenden, wenn sie in Ihrer Anwendung konstant sind).

Ein Ansatz für das Erhalten diese Einschränkung um ist Verpackung HttpClientmit einer Klasse , die alle die Duplikate HttpClientMethoden , die Sie brauchen ( GetAsync, PostAsyncusw.) und die Delegierten sie zu einem Singleton HttpClient. Dies ist jedoch ziemlich langwierig (Sie müssen auch die Erweiterungsmethoden umbrechen ), und zum Glück gibt es einen anderen Weg : Erstellen Sie weiterhin neue HttpClientInstanzen, aber verwenden Sie den zugrunde liegenden Wert erneut HttpClientHandler. Stellen Sie nur sicher, dass Sie den Handler nicht entsorgen:

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
quelle
2
Der bessere Weg ist, eine HttpClient-Instanz beizubehalten, dann Ihre eigenen lokalen HttpRequestMessage-Instanzen zu erstellen und dann die .SendAsync () -Methode auf dem HttpClient zu verwenden. Auf diese Weise ist es immer noch threadsicher. Jede HttpRequestMessage verfügt über eigene Authentifizierungs- / URL-Werte.
Tim P.
@ TimP. warum ist es besser SendAsyncist viel weniger bequem als die speziellen Methoden wie PutAsync, PostAsJsonAsyncusw.
Ohad Schneider
2
Mit SendAsync können Sie die URL und andere Eigenschaften wie z. B. Header ändern und trotzdem threadsicher sein.
Tim P.
2
Ja, der Handler ist der Schlüssel. Solange dies von den HttpClient-Instanzen gemeinsam genutzt wird, ist alles in Ordnung. Ich habe Ihren früheren Kommentar falsch verstanden.
Dave Black
1
Wenn wir einen gemeinsam genutzten Handler behalten, müssen wir uns dann noch um veraltete DNS-Probleme kümmern?
Shanti
5

Bezogen auf Websites mit hohem Volumen, jedoch nicht direkt auf HttpClient. Wir haben den folgenden Codeausschnitt in allen unseren Diensten.

        // 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;

Von 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

"Mit dieser Eigenschaft können Sie sicherstellen, dass die aktiven Verbindungen eines ServicePoint-Objekts nicht auf unbestimmte Zeit geöffnet bleiben. Diese Eigenschaft ist für Szenarien vorgesehen, in denen Verbindungen regelmäßig getrennt und wiederhergestellt werden sollten, z. B. Lastausgleichsszenarien.

Wenn KeepAlive für eine Anforderung wahr ist, legt die MaxIdleTime-Eigenschaft standardmäßig das Zeitlimit für das Schließen von ServicePoint-Verbindungen aufgrund von Inaktivität fest. Wenn der ServicePoint über aktive Verbindungen verfügt, hat MaxIdleTime keine Auswirkung und die Verbindungen bleiben auf unbestimmte Zeit geöffnet.

Wenn die ConnectionLeaseTimeout-Eigenschaft auf einen anderen Wert als -1 festgelegt ist und die angegebene Zeit abgelaufen ist, wird eine aktive ServicePoint-Verbindung nach der Bearbeitung einer Anforderung geschlossen, indem KeepAlive in dieser Anforderung auf false gesetzt wird. Das Festlegen dieses Werts wirkt sich auf alle Verbindungen aus, die vom ServicePoint-Objekt verwaltet werden. "

Wenn Sie Dienste hinter einem CDN oder einem anderen Endpunkt haben, für den Sie ein Failover durchführen möchten, hilft diese Einstellung Anrufern, Ihnen zu Ihrem neuen Ziel zu folgen. In diesem Beispiel sollten 60 Sekunden nach einem Failover alle Anrufer erneut eine Verbindung zum neuen Endpunkt herstellen. Es erfordert, dass Sie Ihre abhängigen Dienste (die Dienste, die SIE aufrufen) und deren Endpunkte kennen.

Keine Rückerstattung Keine Rückgabe
quelle
Sie belasten den Server immer noch stark, indem Sie Verbindungen öffnen und schließen. Wenn Sie instanzbasierte HttpClients mit instanzbasierten HttpClientHandlern verwenden, wird der Port immer noch erschöpft sein, wenn Sie nicht vorsichtig sind.
Dave Black
Nicht anderer Meinung. Alles ist ein Kompromiss. Für uns ist das Verfolgen eines umgeleiteten CDN oder DNS Geld in der Bank im Vergleich zu Einnahmeverlusten.
Keine Rückerstattung Keine Rückgabe
1

Vielleicht möchten Sie auch auf diesen Blog-Beitrag von Simon Timms verweisen: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Ist HttpClientaber anders. Obwohl es die IDisposableSchnittstelle implementiert , ist es tatsächlich ein gemeinsam genutztes Objekt. Dies bedeutet, dass es unter der Abdeckung wiedereintrittsfähig und gewindesicher ist. Anstatt HttpClientfür jede Ausführung eine neue Instanz von zu erstellen , sollten Sie eine einzelne Instanz HttpClientfür die gesamte Lebensdauer der Anwendung freigeben. Schauen wir uns an, warum.

SvenAelterman
quelle
1

Eine Sache, auf die Sie hinweisen sollten, ist, dass keiner der Blog-Hinweise "Nicht verwenden" lautet, dass Sie nicht nur die BaseAddress und den DefaultHeader berücksichtigen müssen. Sobald Sie HttpClient statisch gemacht haben, gibt es interne Zustände, die über Anforderungen hinweg übertragen werden. Ein Beispiel: Sie authentifizieren sich bei einem Drittanbieter mit HttpClient, um ein FedAuth-Token zu erhalten (ignorieren Sie, warum nicht OAuth / OWIN / etc verwendet wird). Diese Antwortnachricht enthält einen Set-Cookie-Header für FedAuth. Dieser wird Ihrem HttpClient-Status hinzugefügt. Der nächste Benutzer, der sich bei Ihrer API anmeldet, sendet das FedAuth-Cookie der letzten Person, es sei denn, Sie verwalten diese Cookies bei jeder Anforderung.

Eskapismus
quelle
0

Als erstes Problem ist die Verwendung dieser Klasse mit der usingAnweisung nicht die beste Wahl, auch wenn Sie darüber verfügenHttpClient der zugrunde liegende Socket selbst dann nicht sofort freigegeben wird und ein schwerwiegendes Problem mit dem Namen "Sockets-Erschöpfung" verursachen kann.

Es gibt jedoch ein zweites Problem HttpClient, das auftreten kann, wenn Sie es als Singleton- oder statisches Objekt verwenden. In diesem Fall HttpClientrespektiert ein Singleton oder Static nichtDNS Änderungen nicht.

In .net Core können Sie dasselbe mit HttpClientFactory wie folgt tun :

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"]);
});

Dokumentation und Beispiel hier

Reza Jenabi
quelle