Devemos criar uma nova instância única do HttpClient para todas as solicitações?

58

Recentemente me deparei com este post de monstros asp.net que fala sobre problemas com o uso HttpClientda seguinte maneira:

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

De acordo com a postagem do blog, se descartamos a HttpClientsolicitação após cada solicitação, ela pode manter as conexões TCP abertas. Isso pode potencialmente levar a System.Net.Sockets.SocketException.

A maneira correta conforme a postagem é criar uma única instância, HttpClientpois isso ajuda a reduzir o desperdício de soquetes.

Da postagem:

Se compartilharmos uma única instância do HttpClient, podemos reduzir o desperdício de soquetes reutilizando-os:

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

Sempre descartei o HttpClientobjeto depois de usá-lo, pois senti que essa é a melhor maneira de usá-lo. Mas esta postagem no blog agora me faz sentir que eu estava fazendo errado todo esse tempo.

Devemos criar uma nova instância única HttpClientpara todas as solicitações? Existem armadilhas no uso de instância estática?

Ankit Vijay
fonte
Você encontrou algum problema atribuído à maneira como o está usando?
Whatsisname
Talvez verifique esta resposta e também esta .
John Wu
@whatsisname não, eu não tenho, mas olhando para o blog, eu sinto que posso estar usando isso errado o tempo todo. Por isso, queria entender dos colegas desenvolvedores se eles veem algum problema em qualquer uma das abordagens.
Ankit Vijay
3
Eu não tentei isso sozinho (portanto, não fornecendo isso como resposta), mas de acordo com a Microsoft a partir do .NET Core 2.1, você deveria usar o HttpClientFactory conforme descrito em docs.microsoft.com/en-us/dotnet/standard/ ...
Joeri Sebrechts
(Como indicado na minha resposta, só queria torná-lo mais visível, por isso estou escrevendo um breve comentário.) A instância estática manipulará adequadamente o aperto de mão de fechamento da conexão tcp, depois de fazer Close()ou iniciar um novo Get(). Se você simplesmente eliminar o cliente quando terminar, não haverá ninguém para lidar com esse aperto de mão de fechamento e todas as suas portas terão o estado TIME_WAIT, por causa disso.
Mladen B.

Respostas:

40

Parece uma publicação atraente no blog. No entanto, antes de tomar uma decisão, primeiro eu executava os mesmos testes que o autor do blog, mas em seu próprio código. Eu também tentaria descobrir um pouco mais sobre o HttpClient e seu comportamento.

Esta publicação afirma:

Uma instância HttpClient é uma coleção de configurações aplicadas a todas as solicitações executadas por essa instância. Além disso, toda instância HttpClient usa seu próprio conjunto de conexões, isolando suas solicitações de solicitações executadas por outras instâncias HttpClient.

Portanto, o que provavelmente está acontecendo quando um HttpClient é compartilhado é que as conexões estão sendo reutilizadas, o que é bom se você não precisar de conexões persistentes. A única maneira de saber com certeza se isso importa ou não para sua situação é executar seus próprios testes de desempenho.

Se você cavar, encontrará vários outros recursos que abordam esse problema (incluindo um artigo de práticas recomendadas da Microsoft), por isso é provavelmente uma boa ideia implementá-lo de qualquer maneira (com algumas precauções).

Referências

Você está usando o Httpclient errado e está desestabilizando seu software
Singleton HttpClient? Cuidado com esse comportamento sério e como corrigi-lo
Padrões e práticas da Microsoft - Otimização de desempenho: instanciação imprópria
Instância única de HttpClient reutilizável na revisão de código
Singleton HttpClient não respeita as alterações de DNS (CoreFX)
Conselho geral sobre o uso de HttpClient

Robert Harvey
fonte
11
Essa é uma boa lista extensa. Esta é a minha leitura de fim de semana.
Ankit Vijay
"Se você cavar, encontrará vários outros recursos que abordam esse problema ...", você quer dizer o problema de abertura da conexão TCP?
Ankit Vijay
Resposta curta: use um HttpClient estático . Se você precisar dar suporte a alterações de DNS (do seu servidor da web ou de outros servidores), precisará se preocupar com as configurações de tempo limite.
21417 Jess
3
É uma prova de quão confuso o HttpClient é que usá-lo é uma "leitura de fim de semana", como comentado por @AnkitVijay.
usr
@ Jess além das alterações no DNS - jogar todo o tráfego do seu cliente através de um único soquete também atrapalha o balanceamento de carga?
Iain
16

Estou atrasado para a festa, mas aqui está minha jornada de aprendizado sobre este assunto complicado.

1. Onde podemos encontrar o advogado oficial da reutilização do HttpClient?

Quero dizer, se a reutilização do HttpClient é planejada e isso é importante , esse advogado é melhor documentado em sua própria documentação da API, em vez de estar oculto em muitos "Tópicos avançados", "Padrão (anti) de desempenho" ou outras postagens do blog por aí . Caso contrário, como um novo aluno deve saber isso antes que seja tarde demais?

A partir de agora (maio de 2018), o primeiro resultado de pesquisa ao pesquisar "c # httpclient" aponta para esta página de referência da API no MSDN , que não menciona essa intenção. Bem, a lição 1 aqui para iniciantes é: sempre clique no link "Outras versões" logo após o título da página de ajuda do MSDN, você provavelmente encontrará links para a "versão atual" lá. Nesse caso HttpClient, ele o levará ao documento mais recente aqui, contendo a descrição da intenção .

Suspeito que muitos desenvolvedores que não conheciam esse tópico também não encontraram a página de documentação correta; é por isso que esse conhecimento não é amplamente difundido e as pessoas ficaram surpresas quando descobriram mais tarde , possivelmente da maneira mais difícil .

2. A (mis?) Concepção de using IDisposable

Este é um pouco fora de tópico, mas ainda vale ressaltar que, não é uma coincidência ver as pessoas nessas postagens do blog culparem como HttpClienta IDisposableinterface faz com que elas usem o using (var client = new HttpClient()) {...}padrão e depois levem ao problema.

Eu acredito que isso se resume a uma concepção não dita (mis?): "Espera-se que um objeto descartável seja de curta duração" .

NO ENTANTO, embora certamente pareça algo de curta duração quando escrevemos código neste estilo:

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

a documentação oficial sobre IDisposable nunca menciona que os IDisposableobjetos precisam ter vida curta. Por definição, o IDisposable é apenas um mecanismo que permite liberar recursos não gerenciados. Nada mais. Nesse sentido, espera-se que você ative o descarte, mas isso não exige que você faça isso de maneira breve.

Portanto, é seu trabalho escolher adequadamente quando acionar o descarte, com base nos requisitos do ciclo de vida do seu objeto real. Não há nada que o impeça de usar um IDisposable de uma maneira duradoura:

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

Com esse novo entendimento, agora que revisitamos a postagem do blog , podemos notar claramente que a "correção" é inicializada HttpClientuma vez, mas nunca a elimina, é por isso que podemos ver em sua saída do netstat que a conexão permanece no estado ESTABLISHED, o que significa que NÃO foi fechado corretamente. Se estivesse fechado, seu estado estaria em TIME_WAIT. Na prática, não é grande coisa vazar apenas uma conexão aberta após todo o programa terminar, e o pôster do blog ainda vê um ganho de desempenho após a correção; mas ainda assim, é conceitualmente incorreto culpar o IDisposable e optar por NÃO descartá-lo.

3. Temos que colocar o HttpClient em uma propriedade estática ou até colocá-lo como um singleton?

Com base no entendimento da seção anterior, acho que a resposta aqui fica clara: "não necessariamente". Realmente depende de como você organiza seu código, desde que você reutilize um HttpClient AND (idealmente) o descarte eventualmente.

Hilariamente, nem mesmo o exemplo na seção Observações do documento oficial atual é estritamente correto. Ele define uma classe "GoodController", contendo uma propriedade estática HttpClient que não será descartada; que desobedece o que outro exemplo da seção Exemplos enfatiza: "precisa chamar descarte ... para que o aplicativo não vaze recursos".

E, por último, o singleton não deixa de ter seus próprios desafios.

"Quantas pessoas pensam que variável global é uma boa idéia? Ninguém.

Quantas pessoas pensam que o singleton é uma boa ideia? Um pouco.

O que da? Singletons são apenas um monte de variáveis ​​globais ".

- Citado nesta palestra inspiradora, "Estado Global e Singletons"

PS: SqlConnection

Este é irrelevante para as perguntas e respostas atuais, mas provavelmente é bom saber. O padrão de uso do SqlConnection é diferente. Você NÃO precisa reutilizar o SqlConnection , porque ele manipulará seu pool de conexões melhor dessa maneira.

A diferença é causada por sua abordagem de implementação. Cada instância HttpClient usa seu próprio conjunto de conexões (citado aqui ); mas o próprio SqlConnection é gerenciado por um pool de conexão central, de acordo com isso .

E você ainda precisa descartar o SqlConnection, o mesmo que você deve fazer para o HttpClient.

RayLuo
fonte
14

Eu fiz alguns testes ver melhorias de desempenho com estática HttpClient. Usei o código abaixo para meus testes:

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

Para teste:

  • Corri o código com 10, 100, 1000 e 1000 conexões.
  • Execute cada teste três vezes para descobrir a média.
  • Executado um método de cada vez

Eu encontrei a melhoria de desempenho entre 40% a 60% de uon usando estática em HttpClientvez de descartá-la para HttpClientsolicitação. Coloquei os detalhes do resultado do teste de desempenho na postagem do blog aqui .

Ankit Vijay
fonte
1

Para fechar corretamente a conexão TCP , precisamos concluir uma sequência de pacotes FIN - FIN + ACK - ACK (assim como SYN - SYN + ACK - ACK, ao abrir uma conexão TCP ). Se simplesmente chamarmos o método .Close () (normalmente acontece quando um HttpClient está sendo descartado) e não esperarmos que o lado remoto confirme nossa solicitação de fechamento (com FIN + ACK), terminaremos com o estado TIME_WAIT em a porta TCP local, porque descartamos nosso ouvinte (HttpClient) e nunca tivemos a chance de redefinir o estado da porta para um estado fechado adequado, uma vez que o ponto remoto nos envia o pacote FIN + ACK.

A maneira correta de fechar a conexão TCP seria chamar o método .Close () e aguardar o evento close do outro lado (FIN + ACK) chegar ao nosso lado. Somente então podemos enviar nosso ACK final e descartar o HttpClient.

Apenas para adicionar, faz sentido manter as conexões TCP abertas, se você estiver executando solicitações HTTP, devido ao cabeçalho HTTP "Connection: Keep-Alive". Além disso, você pode pedir ao ponto remoto para fechar a conexão, definindo o cabeçalho HTTP "Conexão: Fechar". Dessa forma, suas portas locais sempre estarão fechadas corretamente, em vez de estarem no estado TIME_WAIT.

Mladen B.
fonte
1

Aqui está um cliente básico da API que usa o HttpClient e HttpClientHandler com eficiência. Quando você cria um novo HttpClient para fazer uma solicitação, há muita sobrecarga. NÃO recrie HttpClient para cada solicitação. Reutilize o HttpClient o máximo possível ...

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 + "/";
    }
}

Uso:

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
fonte
-5

Não há uma maneira de usar a classe HttpClient. A chave é arquitetar seu aplicativo de uma maneira que faça sentido para seu ambiente e restrições.

O HTTP é um ótimo protocolo para usar quando você precisa expor APIs públicas. Também pode ser usado efetivamente para serviços internos leves e de baixa latência - embora o padrão da fila de mensagens RPC seja frequentemente uma escolha melhor para serviços internos.

Há muita complexidade em se executar bem o HTTP.

Considere o seguinte:

  1. Criar um soquete e estabelecer uma conexão TCP usa largura de banda e tempo da rede.
  2. O HTTP / 1.1 suporta solicitações de pipeline no mesmo soquete. Enviar várias solicitações uma após a outra, sem precisar esperar pelas respostas anteriores - isso provavelmente é responsável pela melhoria de velocidade relatada pela postagem do blog.
  3. Armazenamento em cache e balanceador de carga - se você tiver um balanceador de carga na frente dos servidores, garantir que seus pedidos tenham cabeçalhos de cache apropriados pode reduzir a carga em seus servidores e obter respostas mais rapidamente aos clientes.
  4. Nunca sondar um recurso, use chunking HTTP para retornar respostas periódicas.

Mas, acima de tudo, teste, meça e confirme. Se não estiver funcionando como planejado, podemos responder a perguntas específicas sobre como alcançar os resultados esperados.

Michael Shaw
fonte
4
Na verdade, isso não responde a nada solicitado.
Whatsisname
Você parece assumir que existe uma maneira correta. Eu não acho que exista. Sei que você deve usá-lo da maneira apropriada, testar e medir como se comporta e, em seguida, ajustar sua abordagem até que você esteja feliz.
Michael Shaw
Você escreveu um pouco sobre o uso de HTTP ou não para se comunicar. O OP perguntou sobre a melhor forma de usar um componente específico da biblioteca.
Whatsisname
11
@ MichaelShaw: HttpClientimplementa IDisposable. Portanto, não é irracional esperar que seja um objeto de vida curta que saiba como limpar a si próprio, adequado para agrupar uma usingdeclaração toda vez que você precisar. Infelizmente, não é assim que realmente funciona. A postagem do blog que o OP vinculou demonstra claramente que existem recursos (especificamente, conexões de soquete TCP) que usingpermanecem por muito tempo após a declaração ficar fora do escopo e o HttpClientobjeto ter sido descartado.
Robert Harvey
11
Eu entendo esse processo de pensamento. É só se você estivesse pensando em HTTP do ponto de vista da arquitetura e pretendesse fazer muitas solicitações para o mesmo serviço - então pensaria em cache e pipelining e, em seguida, o pensamento de tornar o HttpClient um objeto de vida curta seria simplesmente se sinta errado. Da mesma forma, se você estiver fazendo solicitações para servidores diferentes e não obtiver nenhum benefício em manter o soquete ativo, o descarte do objeto HttpClient após seu uso faz sentido.
Michael Shaw