Qual è l'overhead della creazione di un nuovo HttpClient per chiamata in un client WebAPI?


162

Quale dovrebbe essere la HttpClientdurata di un client WebAPI?
È meglio avere un'istanza di HttpClientper più chiamate?

Qual è il sovraccarico della creazione e dello smaltimento di una HttpClientrichiesta, come nell'esempio seguente (tratto da 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);
    }
}

Non sono sicuro, potresti usare la Stopwatchclasse per confrontarla, comunque. La mia stima sarebbe che ha più senso avere un singolo HttpClient, supponendo che tutte quelle istanze siano usate nello stesso contesto.
Matteo

Risposte:


215

HttpClientè stato progettato per essere riutilizzato per più chiamate . Anche su più thread. Il HttpClientHandlerdispone di credenziali e cookie che sono destinati ad essere riutilizzati tra le chiamate. Avere una nuova HttpClientistanza richiede di reimpostare tutta quella roba. Inoltre, la DefaultRequestHeadersproprietà contiene proprietà destinate a più chiamate. Dover reimpostare quei valori su ogni richiesta sconfigge il punto.

Un altro grande vantaggio di HttpClientè la possibilità di aggiungere HttpMessageHandlersnella pipeline richiesta / risposta per applicare preoccupazioni trasversali. Questi potrebbero essere per la registrazione, il controllo, la limitazione, la gestione dei reindirizzamenti, la gestione offline, l'acquisizione di metriche. Ogni sorta di cose diverse. Se viene creato un nuovo HttpClient su ogni richiesta, tutti questi gestori di messaggi devono essere configurati su ogni richiesta e in qualche modo deve essere fornito anche qualsiasi stato a livello di applicazione condiviso tra le richieste per questi gestori.

Più usi le funzionalità HttpClient, più vedrai che riutilizzare un'istanza esistente ha senso.

Tuttavia, il problema più grande, a mio avviso, è che quando una HttpClientclasse viene eliminata, viene eliminata HttpClientHandler, il che quindi chiude forzatamente la TCP/IPconnessione nel pool di connessioni gestite da ServicePointManager. Ciò significa che ogni richiesta con una nuova HttpClientrichiede di ristabilire una nuova TCP/IPconnessione.

Dai miei test, utilizzando il semplice HTTP su una LAN, l'hit di prestazioni è abbastanza trascurabile. Sospetto che ciò sia dovuto al fatto che esiste un keepalive TCP sottostante che mantiene aperta la connessione anche quando HttpClientHandlertenta di chiuderla.

Su richieste che vanno su Internet, ho visto una storia diversa. Ho riscontrato un aumento delle prestazioni del 40% a causa della necessità di riaprire la richiesta ogni volta.

Sospetto che l'hit su una HTTPSconnessione sarebbe ancora peggio.

Il mio consiglio è di conservare un'istanza di HttpClient per tutta la durata della tua applicazione per ogni distinta API a cui ti connetti.


5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerSei sicuro di questa affermazione? È difficile da credere. HttpClientmi sembra un'unità di lavoro che dovrebbe essere istanziata spesso.
usr

2
@vkelman Sì, puoi sempre riutilizzare un'istanza di HttpClient anche se l'hai creata con un nuovo HttpClientHandler. Si noti inoltre che esiste un costruttore speciale per HttpClient che consente di riutilizzare un HttpClientHandler e di smaltire HttpClient senza interrompere la connessione.
Darrel Miller,

2
@vkelman Preferisco mantenere l'HttpClient in giro, ma se preferisci mantenere l'HttpClientHandler in giro, manterrà la connessione aperta quando il secondo parametro è falso.
Darrel Miller,

2
@DarrelMiller Quindi sembra che la connessione sia legata a HttpClientHandler. So che per ridimensionare non voglio distruggere la connessione, quindi ho bisogno di mantenere un HttpClientHandler in giro e creare tutte le mie istanze HttpClient da quella OPPURE creare un'istanza HttpClient statica. Tuttavia, se CookieContainer è associato a HttpClientHandler e i miei cookie devono differire per richiesta, cosa mi consigliate? Vorrei evitare la sincronizzazione dei thread su un HttpClientHandler statico modificando il suo CookieContainer per ogni richiesta.
Dave Black,

2
@ Sana.91 Potresti. Sarebbe meglio registrarlo come singleton nella raccolta servizi e accedervi in ​​questo modo.
Darrel Miller,

69

Se vuoi ridimensionare la tua applicazione, la differenza è ENORME! A seconda del carico, vedrai numeri di prestazioni molto diversi. Come menziona Darrel Miller, HttpClient è stato progettato per essere riutilizzato tra le richieste. Ciò è stato confermato dai ragazzi del team BCL che lo hanno scritto.

Un recente progetto che ho avuto è stato quello di aiutare un grande e noto rivenditore di computer online a scalare il traffico del Black Friday / festivo per alcuni nuovi sistemi. Abbiamo riscontrato alcuni problemi di prestazioni relativi all'uso di HttpClient. Dal momento che implementa IDisposable, gli sviluppatori hanno fatto quello che avresti fatto normalmente creando un'istanza e inserendola all'interno di using()un'istruzione. Una volta iniziato il test del carico, l'app ha messo in ginocchio il server: sì, il server non è solo l'app. Il motivo è che ogni istanza di HttpClient apre una porta sul server. A causa della finalizzazione non deterministica di GC e del fatto che si sta lavorando con risorse di computer che si estendono su più livelli OSI , la chiusura delle porte di rete può richiedere del tempo. In effetti il ​​sistema operativo Windows stessopossono essere necessari fino a 20 secondi per chiudere una porta (per Microsoft). Stavamo aprendo le porte più velocemente di quanto potessero essere chiuse - esaurimento delle porte del server che ha portato la CPU al 100%. La mia soluzione era quella di cambiare HttpClient in un'istanza statica che risolvesse il problema. Sì, è una risorsa usa e getta, ma qualsiasi sovraccarico è ampiamente compensato dalla differenza di prestazioni. Ti incoraggio a fare alcuni test di carico per vedere come si comporta la tua app.

Puoi anche consultare la pagina Guida WebAPI per la documentazione e l'esempio all'indirizzo https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Presta particolare attenzione a questo invito:

HttpClient deve essere istanziato una volta e riutilizzato per tutta la vita di un'applicazione. Soprattutto nelle applicazioni server, la creazione di una nuova istanza HttpClient per ogni richiesta esaurirà il numero di socket disponibili con carichi pesanti. Ciò comporterà errori SocketException.

Se trovi che devi usare una statica HttpClientcon intestazioni, indirizzo di base diversi, ecc. Quello che dovrai fare è creare HttpRequestMessagemanualmente e impostare quei valori su HttpRequestMessage. Quindi, utilizzare ilHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

AGGIORNAMENTO per .NET Core : IHttpClientFactoryper creare HttpClientistanze è necessario utilizzare via Dependency Injection . Gestirà la vita per te e non è necessario eliminarlo esplicitamente. Vedere Effettuare richieste HTTP utilizzando IHttpClientFactory in ASP.NET Core


1
questo post contiene informazioni utili per coloro che faranno stress test ..!
Sana.91

9

Come affermano le altre risposte, HttpClientè destinato al riutilizzo. Tuttavia, il riutilizzo di una singola HttpClientistanza in un'applicazione multi-thread significa che non è possibile modificare i valori delle sue proprietà stateful, come BaseAddresse DefaultRequestHeaders(quindi è possibile utilizzarli solo se sono costanti in tutta l'applicazione).

Un approccio per aggirare questa limitazione è il wrapping HttpClientcon una classe che duplica tutti i HttpClientmetodi necessari ( GetAsync, PostAsyncecc.) E li delega a un singleton HttpClient. Tuttavia è piuttosto noioso (dovrai avvolgere anche i metodi di estensione ), e fortunatamente c'è un altro modo : continua a creare nuove HttpClientistanze, ma riutilizza il sottostante HttpClientHandler. Assicurati solo di non smaltire il gestore:

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

2
Il modo migliore per procedere è mantenere un'istanza HttpClient, quindi creare le proprie istanze HttpRequestMessage locali e quindi utilizzare il metodo .SendAsync () su HttpClient. In questo modo sarà ancora sicuro per i thread. Ogni HttpRequestMessage avrà i propri valori di autenticazione / URL.
Tim P.

@TimP. perché è meglio? SendAsyncè molto meno conveniente dei metodi dedicati come PutAsync, PostAsJsonAsyncecc.
Ohad Schneider,

2
SendAsync ti consente di modificare l'URL e altre proprietà come le intestazioni e di essere sicuro per i thread.
Tim P.

2
Sì, il gestore è la chiave. Fintanto che è condiviso tra le istanze di HttpClient stai bene. Ho letto male il tuo commento precedente.
Dave Black,

1
Se manteniamo un gestore condiviso, dobbiamo ancora occuparci del problema DNS obsoleto?
Shanti,

5

Relativo a siti Web ad alto volume ma non direttamente a HttpClient. Abbiamo lo snippet di codice riportato di seguito in tutti i nostri servizi.

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

Da 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) e RD = true

"È possibile utilizzare questa proprietà per garantire che le connessioni attive di un oggetto ServicePoint non rimangano aperte indefinitamente. Questa proprietà è destinata a scenari in cui le connessioni devono essere eliminate e ristabilite periodicamente, come gli scenari di bilanciamento del carico.

Per impostazione predefinita, quando KeepAlive è true per una richiesta, la proprietà MaxIdleTime imposta il timeout per la chiusura delle connessioni ServicePoint a causa dell'inattività. Se ServicePoint ha connessioni attive, MaxIdleTime non ha alcun effetto e le connessioni rimangono aperte indefinitamente.

Quando la proprietà ConnectionLeaseTimeout è impostata su un valore diverso da -1 e allo scadere del tempo specificato, una connessione ServicePoint attiva viene chiusa dopo aver gestito una richiesta impostando KeepAlive su false in quella richiesta. L'impostazione di questo valore influisce su tutte le connessioni gestite dall'oggetto ServicePoint. "

Quando si dispone di servizi dietro una rete CDN o un altro endpoint che si desidera eseguire il failover, questa impostazione consente ai chiamanti di seguire l'utente verso la nuova destinazione. In questo esempio, 60 secondi dopo un failover, tutti i chiamanti devono riconnettersi al nuovo endpoint. Richiede che tu conosca i tuoi servizi dipendenti (quei servizi che TU chiami) e i loro endpoint.


Stai ancora caricando molto sul server aprendo e chiudendo le connessioni. Se si utilizzano HttpClients basati su istanza con HttpClientHandlers basato su istanza, se non si presta attenzione si incorrerà ancora in esaurimento delle porte.
Dave Black,

Non in disaccordo. Tutto è un compromesso. Per noi seguire un CDN o DNS reindirizzato è denaro in banca contro entrate perse.
Nessun rimborso Nessun

1

Puoi anche fare riferimento a questo post di Simon Timms: https://aspnetmonsters.com/2016/08/2016 08-27-httpclientwrong/

Ma HttpClientè diverso. Sebbene implementa l' IDisposableinterfaccia, in realtà è un oggetto condiviso. Ciò significa che sotto le coperte è rientrante) e sicuro per i fili. Invece di creare una nuova istanza di HttpClientper ogni esecuzione, è necessario condividere una singola istanza di HttpClientper l'intera durata dell'applicazione. Diamo un'occhiata al perché.


1

Una cosa da sottolineare, che nessuna delle note sui blog "non usare" è che non è solo il BaseAddress e DefaultHeader che devi considerare. Una volta reso statico HttpClient, ci sono stati interni che verranno trasferiti tra le richieste. Un esempio: stai eseguendo l'autenticazione con terze parti con HttpClient per ottenere un token FedAuth (ignora perché non usi OAuth / OWIN / etc), quel messaggio di risposta ha un'intestazione Set-Cookie per FedAuth, questo viene aggiunto al tuo stato HttpClient. Il prossimo utente che accederà alla tua API invierà il cookie FedAuth dell'ultima persona a meno che tu non stia gestendo questi cookie su ogni richiesta.


0

Come primo problema, mentre questa classe è usa e getta, usarla con l' usingistruzione non è la scelta migliore perché anche quando si smaltisce un HttpClientoggetto, il socket sottostante non viene immediatamente rilasciato e può causare un grave problema chiamato esaurimento dei socket.

Ma c'è un secondo problema HttpClientche puoi avere quando lo usi come oggetto singleton o statico. In questo caso, un singleton o statico HttpClientnon rispetta le DNSmodifiche.

in .net core puoi fare lo stesso con HttpClientFactory in questo modo:

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

documentazione ed esempio qui

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.