Qual é a sobrecarga de criar um novo HttpClient por chamada em um cliente WebAPI?

162

Qual deve ser a HttpClientvida útil de um cliente WebAPI?
É melhor ter uma instância do HttpClientpara várias chamadas?

Qual é a sobrecarga de criar e descartar uma HttpClientsolicitação por, como no exemplo abaixo (extraído de 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
fonte
Não tenho certeza, você poderia usar a Stopwatchclasse para compará-la, no entanto. Minha estimativa seria que faz mais sentido ter um único HttpClient, supondo que todas essas instâncias sejam usadas no mesmo contexto.
21314 Matthew

Respostas:

215

HttpClientfoi projetado para ser reutilizado para várias chamadas . Mesmo em vários segmentos. O HttpClientHandlercredenciais e Cookies que se destinam a ser entre as chamadas reutilizado. Ter uma nova HttpClientinstância requer a restauração de todas essas coisas. Além disso, a DefaultRequestHeaderspropriedade contém propriedades destinadas a várias chamadas. Ter que redefinir esses valores em cada solicitação anula o ponto.

Outro grande benefício HttpClienté a capacidade de adicionar HttpMessageHandlersao pipeline de solicitação / resposta para aplicar preocupações transversais. Estes podem ser para registro, auditoria, limitação, manipulação de redirecionamento, manipulação offline, captura de métricas. Todos os tipos de coisas diferentes. Se um novo HttpClient for criado em cada solicitação, todos esses manipuladores de mensagens precisarão ser configurados em cada solicitação e, de alguma forma, qualquer estado de nível de aplicativo compartilhado entre solicitações para esses manipuladores também precisará ser fornecido.

Quanto mais você usar os recursos HttpClient, mais verá que reutilizar uma instância existente faz sentido.

No entanto, o maior problema, na minha opinião, é que, quando uma HttpClientclasse é descartada, ela é descartada, o HttpClientHandlerque fecha forçosamente a TCP/IPconexão no pool de conexões gerenciadas por ServicePointManager. Isso significa que cada solicitação com uma nova HttpClientrequer o restabelecimento de uma nova TCP/IPconexão.

Nos meus testes, usando HTTP simples em uma LAN, o impacto no desempenho é bastante insignificante. Suspeito que isso ocorra porque existe um keepalive TCP subjacente que mantém a conexão aberta mesmo quando HttpClientHandlertenta fechá-la.

Em solicitações que passam pela Internet, eu vi uma história diferente. Vi um impacto de 40% no desempenho devido à necessidade de reabrir a solicitação todas as vezes.

Eu suspeito que o golpe em uma HTTPSconexão seria ainda pior.

Meu conselho é manter uma instância do HttpClient durante a vida útil do seu aplicativo para cada API distinta à qual você se conecta.

Darrel Miller
fonte
5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerVocê tem certeza sobre esta afirmação? Isso é difícil de acreditar. HttpClientparece-me uma unidade de trabalho que deveria ser instanciada com frequência.
usr
2
@ vkelman Sim, você ainda pode reutilizar uma instância do HttpClient, mesmo que a tenha criado com um novo HttpClientHandler. Observe também que há um construtor especial para HttpClient que permite reutilizar um HttpClientHandler e descartar o HttpClient sem interromper a conexão.
Darrel Miller
2
@ vkelman Eu prefiro manter o HttpClient por perto, mas se você preferir manter o HttpClientHandler por perto, ele manterá a conexão aberta quando o segundo parâmetro for falso.
Darrel Miller
2
@DarrelMiller Parece que a conexão está vinculada ao HttpClientHandler. Eu sei que, para escalar, não quero destruir a conexão, portanto, preciso manter um HttpClientHandler por perto e criar todas as minhas instâncias HttpClient a partir dele OU criar uma instância estática de HttpClient. No entanto, se o CookieContainer estiver vinculado ao HttpClientHandler e meus cookies precisarem diferir por solicitação, o que você recomenda? Gostaria de evitar a sincronização de threads em um HttpClientHandler estático, modificando seu CookieContainer para cada solicitação.
Dave Preto
2
@ Sana.91 Você poderia. Seria melhor registrá-lo como um singleton na coleção de serviços e acessá-lo dessa maneira.
Darrel Miller
69

Se você deseja que seu aplicativo seja dimensionado, a diferença é ENORME! Dependendo da carga, você verá números de desempenho muito diferentes. Como menciona Darrel Miller, o HttpClient foi projetado para ser reutilizado em solicitações. Isso foi confirmado pelos caras da equipe da BCL que o escreveram.

Um projeto recente que tive foi ajudar um varejista de computadores on-line muito grande e conhecido a expandir o tráfego da Black Friday / feriado para alguns novos sistemas. Encontramos alguns problemas de desempenho relacionados ao uso do HttpClient. Como ele é implementado IDisposable, os desenvolvedores fizeram o que você normalmente faria criando uma instância e colocando-a dentro de uma using()instrução. Depois que começamos a testar o aplicativo, o aplicativo ficou de joelhos - sim, o servidor não apenas o aplicativo. O motivo é que todas as instâncias do HttpClient abrem uma porta no servidor. Devido à finalização não determinística do GC e ao fato de você estar trabalhando com recursos de computador que abrangem várias camadas OSI , o fechamento das portas de rede pode demorar um pouco. De fato, o próprio sistema operacional Windowspode levar até 20 segundos para fechar uma porta (por Microsoft). Estávamos abrindo portas mais rapidamente do que podiam ser fechadas - a exaustão da porta do servidor que aumentou a CPU a 100%. Minha correção foi alterar o HttpClient para uma instância estática que resolveu o problema. Sim, é um recurso descartável, mas qualquer sobrecarga é amplamente compensada pela diferença de desempenho. Convido você a fazer alguns testes de carga para ver como seu aplicativo se comporta.

Você também pode verificar a página de Orientação da WebAPI para obter documentação e exemplo em https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Preste atenção especial a esta chamada:

O HttpClient deve ser instanciado uma vez e reutilizado ao longo da vida de um aplicativo. Especialmente em aplicativos de servidor, a criação de uma nova instância HttpClient para cada solicitação esgotará o número de soquetes disponíveis sob cargas pesadas. Isso resultará em erros de SocketException.

Se você achar que precisa usar uma estática HttpClientcom diferentes cabeçalhos, endereço base etc., o que você precisará fazer é criar o HttpRequestMessagemanual e definir esses valores no HttpRequestMessage. Então, use oHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

ATUALIZAÇÃO para .NET Core : você deve usar a IHttpClientFactoryinjeção de dependência via para criar HttpClientinstâncias. Ele gerenciará a vida útil para você e você não precisará descartá-lo explicitamente. Consulte Fazer solicitações HTTP usando IHttpClientFactory no ASP.NET Core

Dave Black
fonte
1
este post contém informações úteis para quem fará testes de estresse ..!
Sana.91
9

Como as outras respostas afirmam, HttpClientdestina-se à reutilização. No entanto, reutilizar uma única HttpClientinstância em um aplicativo multithread significa que você não pode alterar os valores de suas propriedades com estado, como BaseAddresseDefaultRequestHeaders (portanto, você pode usá-los apenas se eles forem constantes em seu aplicativo).

Uma abordagem para contornar essa limitação é agrupar HttpClientuma classe que duplica todos os HttpClientmétodos necessários ( GetAsync, PostAsyncetc) e os delega a um singleton HttpClient. No entanto, isso é muito tedioso (você também precisará envolver os métodos de extensão ) e, felizmente, existe outra maneira - continue criando novas HttpClientinstâncias, mas reutilize as subjacentes HttpClientHandler. Apenas certifique-se de não descartar o manipulador:

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
fonte
2
A melhor maneira é manter uma instância HttpClient e criar suas próprias instâncias HttpRequestMessage locais e, em seguida, usar o método .SendAsync () no HttpClient. Dessa forma, ainda será seguro para threads. Cada HttpRequestMessage terá seus próprios valores de autenticação / URL.
Tim P.
@TimP. porque é melhor? SendAsyncé muito menos conveniente do que os métodos específicos, tais como PutAsync, PostAsJsonAsyncetc
Oade Schneider
2
SendAsync permite que você altere a URL e outras propriedades, como cabeçalhos, e ainda seja seguro para threads.
Tim P.
2
Sim, o manipulador é a chave. Contanto que isso seja compartilhado entre as instâncias HttpClient, você estará bem. Eu interpretei mal o seu comentário anterior.
Dave Preto
1
Se mantivermos um manipulador compartilhado, ainda precisamos resolver um problema antigo de DNS?
Shanti
5

Relacionado a sites de alto volume, mas não diretamente ao HttpClient. Temos o trecho de código abaixo em todos os nossos serviços.

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

Em 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

"Você pode usar esta propriedade para garantir que as conexões ativas de um objeto ServicePoint não permaneçam abertas indefinidamente. Essa propriedade é destinada a cenários em que as conexões devem ser descartadas e restabelecidas periodicamente, como cenários de balanceamento de carga.

Por padrão, quando KeepAlive é verdadeiro para uma solicitação, a propriedade MaxIdleTime define o tempo limite para o fechamento de conexões do ServicePoint devido à inatividade. Se o ServicePoint tiver conexões ativas, o MaxIdleTime não terá efeito e as conexões permanecerão abertas indefinidamente.

Quando a propriedade ConnectionLeaseTimeout é definida como um valor diferente de -1 e após o tempo especificado, uma conexão ativa do ServicePoint é fechada após atender a uma solicitação, configurando KeepAlive como false nessa solicitação. A configuração desse valor afeta todas as conexões gerenciadas pelo objeto ServicePoint. "

Quando você tem serviços atrás de uma CDN ou outro ponto de extremidade que deseja fazer failover, essa configuração ajuda os chamadores a segui-lo até seu novo destino. Neste exemplo, 60 segundos após um failover, todos os chamadores devem se reconectar ao novo terminal. Requer que você conheça seus serviços dependentes (aqueles que você chama) e seus pontos de extremidade.

Sem Reembolsos Sem Devoluções
fonte
Você ainda coloca muita carga no servidor, abrindo e fechando conexões. Se você usar HttpClients com base em instância com HttpClientHandlers com base em instância, você continuará com o esgotamento da porta se não for cuidadoso.
Dave Black
Não discordo. Tudo é uma troca. Para nós, seguir uma CDN ou DNS redirecionado é dinheiro no banco vs. receita perdida.
Sem reembolsos Sem devoluções
1

Você também pode consultar esta publicação no blog de Simon Timms: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Mas HttpClienté diferente. Embora implemente a IDisposableinterface, na verdade é um objeto compartilhado. Isso significa que, embaixo das tampas, é reentrante) e segura para a rosca. Em vez de criar uma nova instância HttpClientpara cada execução, você deve compartilhar uma única instância HttpClientpor toda a vida útil do aplicativo. Vamos ver o porquê.

SvenAelterman
fonte
1

Uma coisa a salientar, que nenhum dos blogs "não use" é que não é apenas o BaseAddress e o DefaultHeader que você precisa considerar. Depois de tornar o HttpClient estático, há estados internos que serão transportados pelas solicitações. Um exemplo: você está se autenticando com um terceiro no HttpClient para obter um token FedAuth (ignore por que não usa OAuth / OWIN / etc), essa mensagem de resposta possui um cabeçalho Set-Cookie para FedAuth, que é adicionado ao seu estado HttpClient. O próximo usuário a fazer login na sua API enviará o cookie FedAuth da última pessoa, a menos que você esteja gerenciando esses cookies em cada solicitação.

escapismc
fonte
0

Como primeira edição, enquanto essa classe é descartável, usá-la com a usingdeclaração não é a melhor opção, porque mesmo quando você descartaHttpClient objeto, o soquete subjacente não é liberado imediatamente e pode causar um problema sério chamado exaustão de soquetes.

Mas há um segundo problema HttpClientque você pode ter ao usá-lo como objeto único ou estático. Nesse caso, um singleton ou estático HttpClientnão respeitaDNS alterações.

No núcleo .net, você pode fazer o mesmo com o HttpClientFactory, algo assim:

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

documentação e exemplo aqui

Reza Jenabi
fonte