Runtime: Singleton HttpClient não respeita as alterações de DNS

Criado em 29 ago. 2016  ·  77Comentários  ·  Fonte: dotnet/runtime

Conforme descrito na postagem a seguir: http://byterot.blogspot.co.uk/2016/07/singleton-httpclient-dns.html , quando você começar a manter uma instância HttpClient compartilhada para melhorar o desempenho, você Encontrará um problema em que o cliente não respeitará as atualizações de registro DNS em caso de cenários de failover.

O problema subjacente é que o valor padrão de ConnectionLeaseTimeout é definido como -1 , infinito. Ele só fechará ao descartar o cliente, o que é muito ineficiente.

A correção é atualizar o valor no ponto de serviço assim:

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1 minute

Infelizmente, não há como fazer isso com o .NET Core hoje.

O ServicePointManager deve ser trazido para o .NET Core ou a funcionalidade equivalente semelhante deve ser habilitada de outra forma.

area-System.Net.Http enhancement

Comentários muito úteis

Isso é absolutamente um problema. A solução proposta por @darrelmiller parece demonstrar uma falta de compreensão do caso de uso.

Usamos os Gerenciadores de Tráfego do Azure para avaliar a integridade de uma instância. Eles trabalham em cima do DNS com TTLs curtos. Um host não sabe magicamente que não é saudável e começa a emitir cabeçalhos C onnection:close . O gerenciador de tráfego simplesmente detecta o estado não íntegro e ajusta a resolução DNS de acordo. Por design, isso é COMPLETAMENTE TRANSPARENTE para clientes e hosts... como está, um aplicativo com um HttpClient estático resolvendo DNS (sem saber) por um gerenciador de tráfego nunca receberá um novo host quando o host resolvido anteriormente não estiver íntegro.

A partir do netcore 1.1, estou procurando uma solução para esse problema exato. Eu não vejo um, nativamente de qualquer maneira.

Todos 77 comentários

Não acho que seja a mesma propriedade. Esse é o tempo limite da conexão, não o ConnectionLeaseTimeout.

Conforme documentado em https://msdn.microsoft.com/en-us/library/system.net.servicepoint.connectionleasetimeout.aspx , ele deve ser descartado periodicamente em alguns cenários. Mas não há nenhuma maneira aparente de alterar o comportamento no .NET Core.

@onovotny Sim, é uma coisa estranha que parece fazer uma conexão explícita

@darrelmiller, então isso está saindo da minha área, não tenho certeza ... mas se houver uma solução documentada que resolva o problema original, isso seria um grande benefício.

@onovotny , não há classe ServicePointManager no .NET Core. Isso soa como um problema do .NET Framework (Desktop). Por favor, abra um problema no UserVoice ou Connect.

@onovotny O problema original, pelo que entendi, é que, no lado do servidor, alguém deseja atualizar uma entrada DNS para que todas as solicitações futuras para hospedar A sejam direcionadas para o novo endereço IP. A melhor solução na minha opinião é quando essa entrada DNS é alterada, o aplicativo no host A deve ser informado para retornar um cabeçalho C onnection:close para o cliente em todas as respostas futuras. Isso forçará todos os clientes a encerrar a conexão e estabelecer uma nova. A solução é não ter um valor de tempo limite no cliente para que o cliente envie um C onnection:close quando esse tempo limite de "aluguel" expirar.

Isso combina bem com a pergunta na Unidade de Stackoverflow

Isso é absolutamente um problema. A solução proposta por @darrelmiller parece demonstrar uma falta de compreensão do caso de uso.

Usamos os Gerenciadores de Tráfego do Azure para avaliar a integridade de uma instância. Eles trabalham em cima do DNS com TTLs curtos. Um host não sabe magicamente que não é saudável e começa a emitir cabeçalhos C onnection:close . O gerenciador de tráfego simplesmente detecta o estado não íntegro e ajusta a resolução DNS de acordo. Por design, isso é COMPLETAMENTE TRANSPARENTE para clientes e hosts... como está, um aplicativo com um HttpClient estático resolvendo DNS (sem saber) por um gerenciador de tráfego nunca receberá um novo host quando o host resolvido anteriormente não estiver íntegro.

A partir do netcore 1.1, estou procurando uma solução para esse problema exato. Eu não vejo um, nativamente de qualquer maneira.

@ kudoz83 Você está correto, um host não sabe magicamente quando um gerenciador de tráfego ajustou a resolução DNS, por isso eu disse

quando essa entrada DNS é alterada, o aplicativo no host A deve ser informado

Se você não acredita que seja prático notificar um servidor de origem de que ele deve retornar os cabeçalhos connect:close, restam apenas algumas opções:

  • Feche periodicamente as conexões de um cliente ou middleware e pague o custo de reabrir essas conexões de forma redundante
  • Faça com que o cliente pesquise um serviço para saber quando ele deve redefinir as conexões.
  • Use um middleware que seja inteligente o suficiente para saber quando o DNS foi trocado.

E sobre eu não entender o caso de uso. O problema original foi baseado em uma postagem de blog em que o caso de uso estava relacionado à alternância entre os slots de produção e de preparação e não tinha nada a ver com o monitoramento de integridade. Embora ambos os cenários possam se beneficiar de receber notificações de comutadores DNS/slot.

AFAIK não há uma maneira de notificar um host com falha que falhou? Geralmente, isso significaria que está em algum tipo de estado de erro. A postagem original está falando sobre cenários de failover.

O Gerenciador de Tráfego do Azure é explicado aqui https://docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-monitoring

Ele opera no nível DNS e é transparente para clientes e hosts - por design. As instâncias do Azure que se comunicam entre si por meio do Gerenciador de Tráfego para resolução de DNS estão em apuros sem poder definir um TTL na atualização de DNS para um HttpClient.

A maneira como o HttpClient opera atualmente parece estar em desacordo com as próprias ofertas de serviços do Azure da Microsoft - que é o ponto principal. Sem mencionar que qualquer serviço semelhante (por exemplo, Amazon Route 53) funciona exatamente da mesma maneira e tem exatamente a mesma dependência curta de DNS TTL.

Obviamente, encontrei este tópico porque estou impactado pelo problema. Estou simplesmente tentando esclarecer que não parece haver uma solução ideal atualmente além de apenas recriar arbitrariamente HttpClients (ao custo de desempenho e complexidade) para garantir que o failover de DNS seja bem-sucedido.

AFAIK não há uma maneira de notificar um host com falha que falhou?

Se um host "com falha" não pudesse executar nenhum código, não precisaríamos do código de status HTTP 503 porque um servidor de origem nunca seria capaz de devolvê-lo. Há cenários em que um host com falha pode não ser capaz de processar uma notificação, mas nesses casos provavelmente também não manterá uma conexão TCP/IP ativa aberta por muito tempo.

Quando Ali originalmente trouxe esse tópico à tona no Twitter, antes de escrever a postagem no blog, ele estava convencido do fato de que um cliente continuaria fazendo solicitações e recebendo respostas de um servidor que havia sido trocado.

Entendo que sua situação é diferente e que você não pode confiar em um servidor de origem capaz de fechar uma conexão de forma confiável. O que não tenho certeza se entendi é por que, para sua situação, você não pode usar um HttpMessageHandler para detectar respostas 5XX, fechar a conexão e tentar novamente a solicitação. Ou ainda mais simples, apenas convença seu servidor web a retornar c onnection:close sempre que retornar um código de status 5XX.

Também vale a pena notar que o comportamento que você está preocupado não está no HttpClient. Está no HttpHandler subjacente ou mesmo abaixo dele, e há várias implementações diferentes do HttpHandler em uso. Na minha opinião, o comportamento atual é consistente com a maneira como o HTTP foi projetado para funcionar. Concordo que há um desafio quando as configurações de DNS são atualizadas enquanto as conexões estão abertas, mas isso não é um problema .net. Este é um problema de arquitetura HTTP. Isso não significa que o .net não possa fazer algo para tornar isso um problema menor.

Estou curioso para saber qual você acredita que a solução deveria ser. Você acha que algo como o ConnectionLeaseTimeout deve ser reimplementado que simplesmente descarta uma conexão periodicamente, independentemente de ter havido um failover ou não? Ou você tem uma solução melhor?

Desculpe,

Eu não deveria ter soado tão combativo quanto pareci ontem. Estava tendo um dia ruim.

Honestamente, não estou ciente de como ConnectionLeaseTimeout funciona nos bastidores. A funcionalidade desejada seria que o HttpClient fosse configurável para realizar uma nova pesquisa de DNS, antes de estabelecer novas conexões, em vez de armazenar em cache a pesquisa de DNS anterior até que o cliente seja descartado. Eu não acredito que isso exija matar uma conexão existente ...

@kudoz83 Não se preocupe, todos nós temos dias assim :-)

Pelo que entendi ConnectionLeaseTimeout é literalmente um temporizador que fecha periodicamente uma conexão ociosa do lado do cliente. Ele é implementado no ServicePointManager, que não existe no core. Isso é diferente do tempo limite de "conexão ociosa", que só fecha uma conexão quando ela não é usada por um determinado período de tempo.

Provavelmente há uma chamada Win32 que permitiria liberar o cache do cliente DNS. Isso garantiria que novas conexões fizessem uma pesquisa de DNS. Dei uma olhada rápida, mas não encontrei, mas tenho certeza que está lá em algum lugar.

No .net core no Windows, o gerenciamento de conexões HTTP é, na verdade, deixado para o sistema operacional. O mais próximo que você obtém é a chamada para a API Win32 WinHttpOpen https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs# L718 Não há APIs públicas que eu possa encontrar para fazer qualquer coisa com o _sessionHandle. Para obter o tipo de mudança de comportamento que você está procurando, seria necessário que o pessoal do sistema operacional fizesse alterações. Pelo menos é assim que eu entendo.

Não seria mais fácil obter o Gerenciador de Tráfego do Azure apenas para definir um TTL muito baixo nas entradas DNS? Se você não estiver preocupado com conexões de longa duração, um TTL baixo deve minimizar a chance de novas conexões serem configuradas em um servidor inativo.

A vantagem da forma como o WinHttpHandler é construído é que obtemos coisas como HTTP/2 gratuitamente com a atualização do sistema operacional. A desvantagem é que não temos muito controle no código gerenciado.

@darrelmiller

[ConnectionLeaseTimeout] é implementado em ServicePointManager, que não existe no core.

Parece que ServicePoint.ConnectionLeaseTimeout está voltando no .Net Standard 2.0/.Net Core 1.2. (Embora eu não tenha ideia se, por exemplo, as conexões WinHttpHandler irão aderir a ele.)

Acho que há alguma falha de comunicação aqui: os TTLs de DNS não estão relacionados aos tempos de vida da conexão HTTP/TCP.

Novas conexões HTTP fazem uma pesquisa de DNS e se conectam ao endereço de host retornado. Várias solicitações HTTP podem ser feitas na mesma conexão única, mas, uma vez aberta, a conexão HTTP (e a conexão TCP subjacente) não faz uma pesquisa de DNS novamente.

Este é o comportamento correto, portanto, mesmo que você faça milhares de solicitações em uma conexão que dura dias, a resolução do DNS não importa, pois a conexão não foi alterada. Se você fizer uma nova solicitação HTTP em uma nova conexão HTTP , verá uma pesquisa de DNS para a nova conexão.

Essa atualização de DNS não tem nada a ver com HttpClient , que apenas usa HttpClientHandler por padrão (que usa a pilha de soquetes HttpWebRequest ) e qualquer resolução DNS é tratada por essa pilha. ServicePointManager gerencia objetos ServicePoint nessa pilha que são conexões com um host específico. ServicePointManager tem algum mínimo cache do cliente de pesquisas de DNS que você pode ajustar.

As configurações padrão destinam-se a manter as conexões abertas indefinidamente para maior eficiência. Se o host conectado ficar indisponível, a conexão será interrompida automaticamente, causando uma nova conexão com uma nova pesquisa de DNS, no entanto, os TTLs de DNS não causarão novas conexões. Se você precisar disso, terá que codificá-lo em seu aplicativo.

Se você quiser que outra pesquisa de DNS seja feita, será necessário redefinir a conexão no cliente. Você pode forçar uma vida útil máxima da conexão TCP, enviar um cabeçalho connection: close ou apenas descartar o HttpClient ou o ServicePoint subjacente. Você também pode fazer a resolução de DNS com Dns.GetHostAddresses e usar o endereço IP para suas solicitações HTTP.

Espero que ajude, se eu estiver fora disso, por favor me avise.

@manigandham

Acho que há alguma falha de comunicação aqui: os TTLs de DNS não estão relacionados aos tempos de vida da conexão HTTP/TCP.

Sim, os TTLs de DNS não estão relacionados, mas não completamente.

Novas conexões HTTP fazem uma pesquisa de DNS e se conectam ao endereço de host retornado.

Sim, está correto. Mas o Windows tem um cache DNS local que respeita o TTL. Se o TTL for alto e o servidor DNS tiver alterado o registro DNS, a nova conexão será aberta no endereço IP antigo devido ao cache do cliente do registro DNS. A liberação do cache DNS do cliente é a única solução alternativa para isso que conheço.

Várias solicitações HTTP podem ser feitas na mesma conexão única, mas, uma vez aberta, a conexão HTTP (e a conexão TCP subjacente) não faz uma pesquisa de DNS novamente.

Corrija novamente. Em uma troca de produção/staging em que o servidor trocado ainda está respondendo, isso fará com que muitas solicitações futuras sejam direcionadas para o servidor antigo, o que é ruim. No entanto, a conversa mais recente foi sobre o cenário em que um servidor falha e o Gerenciador de Tráfego troca para um novo servidor. O que acontece com a conexão existente com o servidor com falha é desconhecido. Se for uma falha grave de hardware, há uma boa chance de a conexão cair. Isso forçaria o cliente a restabelecer uma conexão e é aí que um TTL baixo é útil. Se, no entanto, a falha do servidor não interromper o servidor e ele simplesmente retornar respostas 5XX, seria útil para o servidor retornar connection:close para garantir que solicitações futuras sejam feitas em uma nova conexão, esperançosamente para o novo servidor de trabalho. servidor

Essa atualização de DNS não tem nada a ver com HttpClient, que apenas usa HttpClientHandler por padrão (que por si só usa a pilha de soquetes HttpWebRequest)

Sim e não. No .Net core, o HttpClientHandler foi reescrito e não usa mais HttpWebRequest e, portanto, não usa ServicePointManager. Daí a razão desta questão.

Dei uma olhada mais de perto no IIS e não parece haver uma maneira de obter respostas 5XX com cabeçalhos de conexão: fechar . Seria necessário implementar um HttpModule para fazer essa mágica.

Como um FYI, aqui está um post relacionado cobrindo esse problema e uma solução provisória. Cuidado com HttpClient

Qualquer ServicePointManager deve ser trazido para o .NET Core

Embora tenhamos trazido ServicePointManager e HttpWebRequest para paridade de plataforma, ele não suporta a maioria das funcionalidades no .Net Core, pois a pilha subjacente resolve para WinHTTP ou Curl, que não expõem os botões de controle avançados.

Usar ServicePointManager para controlar HttpClient é, na verdade, um efeito colateral ao usar HttpWebRequest e a pilha HTTP gerenciada no .Net Framework. Como WinHttpHandler/CurlHandler são usados ​​em .Net Core, o SPM não tem controle sobre instâncias de HttpClient.

ou funcionalidade equivalente similar deve ser habilitada de alguma outra forma.

Aqui está a resposta da equipe do WinHTTP:

  1. Por motivos de segurança, enquanto o cliente continuar enviando solicitações e _há conexões ativas_, o WinHTTP continuará usando o IP de destino. Não há como limitar o tempo em que essas conexões permanecem ativas.
  2. Se todas as conexões com o ponto de extremidade remoto foram fechadas pelo servidor, _novas_ conexões serão feitas consultando o DNS e, portanto, em direção ao novo IP.

Para forçar os clientes para o próximo IP, a única solução viável é garantir que o servidor rejeite novas conexões e feche as existentes (já mencionadas neste tópico).
Isso é o mesmo com o que eu entendo da documentação do

Considerando outros dispositivos de dispositivo de rede que estão armazenando DNS em cache (por exemplo, roteadores/APs domésticos), eu não recomendaria o failover de DNS como uma solução para a técnica de baixa latência e alta disponibilidade. Se isso for necessário, minha recomendação para um failover controlado seria ter um LB dedicado (hardware ou software) que saiba como parar de drenagem corretamente.

tal como está, um aplicativo com um HttpClient estático resolvendo DNS (sem saber) por um gerenciador de tráfego nunca receberá um novo host quando o host resolvido anteriormente não estiver íntegro.

Como mencionado acima, se por não saudável você quer dizer que o servidor retorna um código de erro HTTP e fecha a conexão TCP enquanto você observa o TTL do DNS expirado, isso é realmente um bug.

Descreva seu cenário com mais detalhes:

  1. Durante o failover, qual é o cache DNS e os TTLs da máquina cliente. Você pode usar ipconfig /showdns e nslookup -type=soa <domain_name> para comparar o que a máquina pensa que o TTL é e os TTLs do NS autoritário.
  2. Existem conexões ativas para o servidor remoto? Você pode usar netstat /a /n /b para ver as conexões e os processos que as possuem.
  3. Criar uma reprodução nos ajudaria muito: por favor, anexe qualquer informação disponível, como: versão do .Net Core/Framework, código para cliente/servidor, rastreamentos de rede, DNS remoto LB/Gerenciador de tráfego envolvido, etc.

Se você se conectar a uma máquina e a conexão TCP/HTTP continuar funcionando, por que você deveria, em algum momento, presumir que o servidor não é mais apropriado.
Acho que há espaço para melhorias no lado do Azure. Por exemplo, quando as máquinas não estão íntegras ou trocam de ambientes, as conexões existentes podem ser fechadas.

ou simplesmente descarte o ... subjacente ServicePoint

@manigandham Como? Não é descartável e o único método relevante que posso ver é CloseConnectionGroup que você realmente não pode usar, pois o nome do grupo é um detalhe de implementação (atualmente, algum hash do manipulador). Além disso, não tenho certeza do que aconteceria se você tentasse descartar o ServicePoint enquanto o HttpClient estivesse usando (especialmente simultaneamente).

@ohadschn

O .NET Core 2.0 traz de volta as classes e funcionalidades ServicePoint : https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint?view=netcore-2.0

Você pode voltar a usar o tempo limite de concessão de conexão para definir um prazo máximo para conexões ativas antes que elas sejam redefinidas.

Nada de exótico em fechar a conexão enquanto a requisição HttpClient está em andamento, seria o mesmo que você perder a conectividade ao navegar na internet. Ou a solicitação nunca é feita ou acontece e você não obtém a resposta de volta - ambos devem levar a erros http retornados e você terá que lidar com eles em seu aplicativo.

Com a configuração de tempo limite de concessão de conexão, você pode garantir que isso seja feito somente depois que uma solicitação for enviada e redefinida antes da próxima acima do limite de tempo.

O .NET Core 2.0 traz de volta as classes e funcionalidades do ServicePoint: https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint?view=netcore-2.0
Você pode voltar a usar o tempo limite de concessão de conexão para definir um prazo máximo para conexões ativas antes que elas sejam redefinidas.

Na verdade, ele não traz de volta a funcionalidade completamente. A superfície da API voltou. Mas é basicamente um no-op, já que as pilhas HTTP não usam esse modelo de objeto.

cc: @stephentoub @geoffkizer

Eu consideraria muitos dos participantes deste tópico como uma espécie de "time dos sonhos" neste assunto, mas ainda não tenho certeza se algum consenso foi alcançado sobre quais são as melhores práticas que temos hoje. Seria ótimo obter uma resposta decisiva deste grupo em alguns pontos-chave.

Digamos que eu tenha uma biblioteca HTTP de uso geral direcionada a muitas plataformas, nem todas com suporte a ServicePointManager , e estou tentando fornecer um comportamento que gerencie instâncias HttpClient inteligente, de acordo com as práticas recomendadas, por padrão. Não tenho ideia de como os hosts serão chamados, muito menos se eles se comportam bem em relação às alterações de DNS/ conexão: cabeçalhos

  1. Como faço para gerenciar instâncias HttpClient otimizada? Um por terminal? Um por host/esquema/porta? Literalmente um singleton total? (Duvido que, uma vez que os consumidores podem querer tirar proveito dos cabeçalhos padrão, etc.)

  2. Como eu lidaria de maneira mais eficaz com esse problema de DNS? Descartar instâncias após 1 minuto? Tratá-lo como "altamente improvável" e não fornecer nenhum comportamento de descarte por padrão?

(Não é uma situação hipotética, aliás; estou lutando com essas perguntas exatas agora.)

Obrigado!

Eu nem seria convidado para os testes do time dos sonhos, mas tenho uma ideia sobre seu comentário singleton HttpClient . Primeiro, você sempre pode usar o método SendAsync para especificar cabeçalhos e endereços diferentes, então você pode agrupar isso com sua classe que fornece dados compartilhados semelhantes a HttpClient (por exemplo, cabeçalhos padrão, endereço base) ainda acaba chamando o mesmo cliente único. Mas ainda melhor, você pode criar várias instâncias HttpClient que compartilham o mesmo HttpClientHandler : https://github.com/dotnet/corefx/issues/5811.

Quanto à questão do DNS, o consenso aqui parece ser que:

Para forçar os clientes ao próximo IP, a única solução viável é garantir que o servidor rejeite novas conexões e feche as existentes

Se isso não for viável para você, Darrel Miller sugeriu mais algumas opções aqui: https://github.com/dotnet/corefx/issues/11224#issuecomment -270498159.

@ohadschn Obrigado, e espero não estar fugindo muito do assunto, mas existe algum _benefício_ em compartilhar o mesmo HttpClient (ou HttpClientHandler) entre vários hosts? Posso estar errado, mas me parece que um novo host significa nova pesquisa de DNS, nova conexão, novo soquete. Um soquete em TIME_WAIT pode ser recuperado e redefinido para um host diferente? Eu tinha assumido que não, mas definitivamente estamos atingindo os limites da minha experiência. Se puder, talvez esse seja o benefício de usar um singleton HttpClient mesmo com vários hosts?

Em relação ao conselho do DNS, senti que era onde o consenso estava mais faltando, e compreensivelmente. Garantir que o servidor se comporte corretamente não é viável se você não tiver controle do que está acontecendo nessa extremidade. Eu _poderia_ projetar algo onde as instâncias são descartadas periodicamente. A questão é se isso vale a pena na maioria dos casos. Acho que cabe a mim fazer um julgamento. :)

Para mim, o benefício mais óbvio de compartilhar o mesmo HttpClient [ Handler ] é a simplicidade. Você não terá que manter coisas como mapeamento entre hosts e clientes. Eu não saberia sobre as coisas do TIME_WAIT, também não é minha especialidade.

Em relação ao DNS, você mencionou uma abordagem possível quando você não controla o servidor. Você também pode implementar um mecanismo que consulta periodicamente o DNS e descarta instâncias somente se ocorrer uma alteração real...

Embora as respostas fornecidas que especificam o que deve acontecer no lado do servidor sejam precisas para um mundo ideal, no mundo real os desenvolvedores geralmente não têm controle de toda a pilha do lado do servidor com a qual estão se comunicando. A esse respeito, gosto muito do conceito de ConnectionLeaseTimeout, pois é uma concessão às realidades da vida cotidiana de John e Jane Doeveloper.
Alguém sugeriu escrever middleware para impor esse tempo limite no .NET Core. Essa classe baseada em NodaTime e System.Collections.Immutable faria o trabalho?

    public class ConnectionLeaseTimeoutHandler : DelegatingHandler
    {
        private static string GetConnectionKey(Uri requestUri) => $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}";

        private readonly IClock clock;
        private readonly Duration leaseTimeout;
        private ImmutableDictionary<string, Instant> connectionLeases = ImmutableDictionary<string, Instant>.Empty;

        public ConnectionLeaseTimeoutHandler(HttpMessageHandler innerHandler, IClock clock, Duration leaseTimeout)
            : base(innerHandler)
        {
            this.clock = clock;
            this.leaseTimeout = leaseTimeout;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string key = GetConnectionKey(request.RequestUri);
            Instant now = clock.GetCurrentInstant();

            Instant leaseStart = ImmutableInterlocked.GetOrAdd(ref connectionLeases, key, now);

            if (now - leaseStart > leaseTimeout)
            {
                request.Headers.ConnectionClose = true;
                ImmutableInterlocked.TryRemove(ref connectionLeases, key, out leaseStart);
            }

            return base.SendAsync(request, cancellationToken);
        }
    }

@snboisen Acho que parece certo, mas não me prenda a isso. :)

Para mim, a grande questão é se enviar um cabeçalho Connection: close é mesmo uma solução viável. Vale a pena repetir aqui o que @darrelmiller disse nos comentários da postagem do blog que gerou este tópico:

ConnectionLeaseTimeout é uma fera estranha, que eu não conhecia até que Oren mencionou isso hoje. Isso fará com que as solicitações de saída incluam cabeçalhos C onnection:Close após a expiração da concessão, fazendo com que a conexão seja encerrada após o servidor enviar sua resposta. Faz sentido por que isso é definido como infinito como padrão, porque é uma maneira estranha de forçar o fechamento de conexões quando um cliente está fazendo solicitações ativamente.

Respeito a opinião de Darrel e isso faz com que toda essa abordagem pareça um tanto hackeada. Por outro lado, concordo que o que deve fazer um desenvolvedor que não tem controle do que está acontecendo no lado do servidor? Se funcionar _às vezes_, talvez seja a melhor opção que temos? É certamente "mais barato" do que descartar periodicamente HttpClients ou fazer pesquisas de DSN periodicamente.

Existe um tíquete do Azure aberto para esse problema?
Da minha perspectiva, parece que todos vocês estão tentando contornar a maneira como isso é implementado no gerenciador de tráfego do Azure.
Eu não sou um usuário do Azure, então eu olho para ele de longe, me sentindo menos inclinado a pular em seus aros.

@tmds Eu não vi um, mas como outros já mencionaram, isso se relaciona a mais do que apenas o Gerenciador de Tráfego do Azure: a AWS tem um conceito semelhante e há o Serviço de Aplicativo do Azure com sua troca de slot de preparo/produção. E aposto que outros provedores de nuvem também oferecem algo semelhante a esse mecanismo de troca.

@snboisen Você está dizendo que a AWS e outros provedores de nuvem (quais?) também estão usando tempos limite de DNS para implementar esses recursos?

@tmds Para AWS, sim, pelo menos para failover: https://aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
Não conheço outros provedores de nuvem, mas parece provável que alguns também o façam.

@tmds Para AWS, sim, pelo menos para failover: https://aws.amazon.com/route53/faqs/#adjust_ttl_to_use_failover
Não conheço outros provedores de nuvem, mas parece provável que alguns também o façam.

Existem dois cenários problemáticos aqui:

  • cliente está conectado a um servidor que não está mais lá
  • cliente está conectado a um servidor que agora está executando uma versão desatualizada do software

O failover de DNS está relacionado ao primeiro. É apropriado remover servidores inacessíveis do DNS (em AWS, Azure, ...).
A maneira correta de detectá-lo no cliente é um mecanismo de tempo limite. Não tenho certeza se existe tal propriedade no HttpClient (tempo limite máximo de resposta em uma conexão estabelecida).
No nível do protocolo, os protocolos websocket e http2 mais recentes definem mensagens ping/pong para verificar a vitalidade da conexão.

Quando sugiro abrir um problema com o Azure, é para o segundo cenário.
Não há motivo para um cliente fechar uma conexão de trabalho. Se o servidor não for mais apropriado, os clientes devem ser desconectados e o servidor deve ficar inacessível.
Encontrei um bom documento sobre implantações AWS Blue/Green: https://d0.awsstatic.com/whitepapers/AWS_Blue_Green_Deployments.pdf.
Ele lista várias técnicas. Para aqueles que usam DNS, menciona _DNS TTL complexidades_. Para aqueles que usam um balanceador de carga _sem complexidades de DNS_.

@tmds Obrigado pelo link, muito interessante.

Eu me pergunto o quão comum é confiar no DNS TTL versus usar um balanceador de carga. O último parece ser uma solução muito mais confiável em geral.

Experimentou algum comportamento semelhante ao trabalhar com 2 aplicativos de API diferentes nos serviços de aplicativos do Azure que se comunicavam por meio de HttpClient . Percebemos cerca de 250 solicitações em nossos testes de carga, começamos a obter tempos limite e "Não foi possível estabelecer uma conexão" HttpRequestException - Não importava se eram usuários simultâneos ou solicitações sequenciais.

Quando mudamos para um bloco using para garantir que o HttpClient seja descartado em cada solicitação, não recebemos nenhuma dessas exceções. O impacto no desempenho, se houver, foi insignificante de um Singleton para a sintaxe using .

Quando mudamos para um bloco using para garantir que o HttpClient seja descartado em cada solicitação, não recebemos nenhuma dessas exceções. O impacto no desempenho, se houver, foi insignificante de um Singleton para a sintaxe using .

Não se trata necessariamente de desempenho, trata-se de não vazar conexões TCP e obter SocketException s.

Do MSDN :

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

Além disso, consulte https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

@khellang Obrigado pelo link (e pelo esclarecimento sobre os SocketException s, eu definitivamente tive um mal-entendido lá) - me deparei com esse post e 3 ou 4 outros posts descrevendo exatamente a mesma coisa.

Entendo que uma instanciação única é a intenção, mas quando a estamos usando no Azure App Services, estamos atingindo consistentemente HttpRequestExceptions sob uma carga bastante modesta (mencionada acima, 250 solicitações sequenciais). A menos que haja outra solução, descartar o HttpClient é a solução que tivemos que implementar para evitar essas exceções consistentes.

Estou assumindo que uma solução "mais correta" seria capturar essas exceções, cancelar todas as solicitações pendentes explicitamente descartar o HttpClient , reinstanciar o HttpClient e tentar novamente todas as solicitações.

Parece um pouco confuso para algo que

destina-se a ser instanciado uma vez e reutilizado ao longo da vida do aplicativo

Adoraríamos uma solução mais limpa, mas parece que não é algo errado com nosso código, mas sim uma limitação do framework. Estou mais do que feliz em estar errado, só preciso de alguma orientação sobre como podemos usar isso como pretendido e não encontrar esses problemas.

@snboisen Testei seu ConnectionLeaseTimeoutHandler e descobri que ele não funciona com várias conexões. Um "fechar" é emitido para apenas uma conexão cada vez que ocorre o tempo limite de concessão, mas pode haver várias conexões abertas com uma única instância HttpClient ao enviar solicitações em paralelo. Pelo que vejo no violinista (eu uso um endereço IP incorreto para gerar erros para verificar o switch DNS) depois que "fechar" é enviado algumas solicitações usam o novo IP e algumas solicitações usam o IP antigo. No meu caso, tenho um limite de conexão de 2 e está levando várias iterações do tempo limite de concessão de conexão para atualizar as duas conexões. Obviamente, seria muito pior com mais conexões simultâneas de um servidor de produção, mas provavelmente acabaria mudando depois de algum tempo.

Não conheço uma maneira de emitir "perto" para cada conexão individual em uso por uma instância HttpClient. Isso certamente seria o ideal, mas o que vou fazer é usar um método de fábrica GetOrCreate() para HttpClient e criar uma nova instância de HttpClient a cada 120 segundos para garantir que todas as novas conexões sejam criadas. Não tenho certeza de como descartar a instância HttpClient antiga quando todas as solicitações forem concluídas para fechar as conexões, mas acho que o .NET cuida disso porque nunca foi um problema criar uma nova instância HttpClient por solicitação.

Como uma observação lateral, acredito que a atualização deve ocorrer no cliente. O servidor não pode ser responsável por comunicar alterações de DNS nem qualquer middleware. A finalidade do DNS é compensar essa responsabilidade para que você possa alterar os IPs a qualquer momento sem coordenação entre cliente e servidor e não apenas fornecer um nome amigável para um IP. O caso de uso perfeito é um endereço IP do AWS Elastic. A alteração é refletida apenas no DNS. A AWS não saberá enviar cabeçalhos "fechar" do seu servidor web em execução em uma instância ec2. A AWS não estará ciente de todos os servidores da Web localizados em uma instância ec2 que eles gerenciam. Cenários para o servidor antigo que não envia um cabeçalho "fechar" são novas implantações de servidor em que exatamente o mesmo software está sendo implantado em um novo servidor com todo o tráfego comutado pelo DNS. Nada deve mudar no servidor antigo, especialmente se você precisar voltar. Tudo deve ser alterado sem problemas em um só lugar com DNS.

Independentemente dos maiores problemas discutidos aqui, ConnectionLeaseTimeout agora está implementado no núcleo? E se sim, por que esta questão não está encerrada? Consegui codificar um exemplo exatamente como o post original sem problemas agora. Se isso resolve meus problemas de DNS de conexão ou não, _parece_ ConnectionLeaseTimeout _is_ implementado no núcleo.

ServicePointMananager.ConnectionLeaseTimeout não é implementado no .NET Core. Usar essa propriedade no .NET Core não é operacional. É funcional apenas no .NET Framework. E por padrão, essa propriedade é definida como "off" no .NET Framework.

Entendo que uma instanciação única é a intenção, mas quando a estamos usando nos Serviços de Aplicativo do Azure, estamos atingindo consistentemente HttpRequestExceptions sob uma carga bastante modesta (mencionada acima, 250 solicitações sequenciais). A menos que haja outra solução, descartar o HttpClient é a solução que tivemos que implementar para evitar essas exceções consistentes.

Fomos bem sucedidos em remediar o problema de esgotamento do soquete envolvendo HttpClient em uma API personalizada que permite que cada solicitação http em um aplicativo reutilize a mesma instância HttpClient (ela faz mais do que apenas isso , como RestSharp mas esse também é um dos benefícios). Recuperar isso está atrás de um lock{} porque a cada 60 minutos criamos um novo HttpClient e o devolvemos. Isso nos permitiu fazer solicitações http de alto volume de nosso aplicativo da Web (devido às integrações pesadas que temos) sem reutilizar conexões antigas que não foram fechadas. Estamos fazendo isso há cerca de um ano com sucesso (depois de atingir os problemas de exaustão do soquete).

Tenho certeza de que ainda há implicações de desempenho nisso (especialmente devido a todo o bloqueio), mas é melhor do que tudo morrer devido à exaustão do soquete.

@KallDrexx por que você precisa bloquear a instância 'global'? Ingenuamente eu iria armazená-lo em estático e apenas substituí-lo a cada 60 minutos por um novo. Isso não seria suficiente?

Nosso código para obter um HttpClient é:

``` c#
var timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
cadeado (cadeado)
{
timeSinceCreated = DateTime.UtcNow - _lastCreatedAt;
if (timeSinceCreated.TotalSeconds > SecondsToRecreateClient)
{
_currentClient = new HttpClient();
_lastCreatedAt = DateTime.UtcNow;
}
}
}

        return _currentClient;

```

Então eu acho que o bloqueio ocorre apenas uma vez por minuto para que vários threads não estejam tentando criar um novo HttpApiClient simultaneamente. Achei que estávamos travando de forma mais agressiva.

Entendo, então é uma razão de conveniência.
Como alternativa, você pode usar a atualização baseada em timer (assíncrona) ou permitir que vários deles sejam criados em paralelo (estilo de inicialização lenta inseguro de thread) ou usar um mecanismo de sincronização diferente e não bloquear os outros threads durante o tempo de criação (eles poderia reutilizar o antigo).

Entendo, então é uma razão de conveniência.

Não sei se classificaria como uma conveniência.

Se muitos threads tentarem criar um novo HttpClient ao mesmo tempo, ainda corremos o risco de esgotamento do soquete devido a muitos serem criados a cada minuto, especialmente se os antigos não tiverem os soquetes limpos adequadamente naquele minuto (causando uma cascata).

Além disso, eu ainda precisaria de alguma medida de bloqueio porque não sei se atualizar um DateTime é uma operação atômica (suspeito que não) e, portanto, alguns threads podem ler o _lastCreatedAt em no meio de uma operação de atualização.

E para que todos os threads reutilizem o cliente antigo enquanto simultaneamente um thread cria um novo cliente requer muita lógica complexa, então temos a garantia de que um thread cria com sucesso o novo cliente. Sem mencionar que ainda precisamos de locks porque não podemos garantir que uma thread não retornará _currentClient enquanto outra thread estiver instanciando.

Existem muitas incógnitas e complexidade adicional para arriscar fazer isso na produção.

@KallDrexx

  • Parece que ReaderWriterLockSlim seria mais adequado para o seu uso.
  • DateTime atribuição x64 verdadeiro (porque contém um único campo ulong ). Observe que seu código atual não é thread-safe se esse não for o caso, porque você está observando o DateTime fora do bloqueio.
  • A abordagem baseada em cronômetro sugerida por @karelz não exigirá um DateTime ... o cronômetro simplesmente mudaria o HttpClient . Você ainda precisaria cercar o campo HttpClient com alguma barreira de memória ( volatile , Interlocked.Exchange etc).

Parece que o ReaderWriterLockSlim seria mais adequado para o seu uso.

Legal eu não sabia dessa aula! No entanto, você mencionar isso também me fez pensar em usar também um SemaphoreSlim , pois ele é amigável para async/await (onde parece que ReaderWriterLockSlim não é), o que pode ajudar com uma possível contenção de thread quando é necessário um novo HttpClient . Então, novamente, o ReaderWriterLockSlim lidará melhor com o possível problema não atômico de DateTime que você apontou corretamente, então vou ter que pensar um pouco, obrigado!

A abordagem baseada em cronômetro sugerida por @karelz não exigirá um DateTime ... o cronômetro simplesmente alternaria o HttpClient. Você ainda precisa cercar o campo HttpClient com alguma barreira de memória (volátil, Interlocked.Exchange etc).

Ah, eu tinha entendido errado o que ele quis dizer. Isso faz sentido, embora eu precise me familiarizar mais com os mecanismos de barreira de memória para fazê-lo corretamente.

Obrigado.

Edit: Só para adicionar, esta é uma boa razão pela qual eu acho que algo deve ser construído para lidar com isso, então todo consumidor de HttpClient não precisa reimplementar/passar por isso.

Gostaria de saber se ainda é um problema no .net core 2.0? Ainda precisamos nos preocupar com a mudança de DNS?

Gostaria de saber se ainda é um problema no .net core 2.0? Ainda precisamos nos preocupar com a mudança de DNS?

Meu entendimento é que não é um problema com o DNS em si, apenas que HttpClient tentará reutilizar as conexões existentes (para evitar o esgotamento do soquete) e, portanto, mesmo quando o DNS for alterado, você ainda estará se conectando ao antigo servidor porque você está passando pela conexão anterior.

@KallDrexx , então significa que ainda terá problemas para implantação azul-verde?

@withinoneyear do meu entendimento sim. Se você criar um HttpClient e utilizá-lo contra o Production Green, troque a produção para o Blue, o HttpClient ainda terá uma conexão aberta com o Green até que a conexão seja fechada.

Eu sei que isso é antigo, mas acredito que você não deve usar DateTime.UtcNow para medição de intervalo de tempo. Use uma fonte de tempo monitônica como Stopwatch.GetTimestamp. Dessa forma, você fica imune ao ajuste do horário local pelo operador ou à sincronização do horário. Havia alguns chipsets com bugs que faziam os valores do QPC voltarem, mas não acredito que muitos deles estejam em uso agora.

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar/baz/123?a=ab"));
sp.ConnectionLeaseTimeout = 60*1000; // 1 minuto

Este código é colocado no construtor?

@Whynotme8998 @lundcm Acho que não. Se você for HttpClient for singleton e acessar vários pontos, o construtor não seria o local correto para colocá-lo.
Verifique esta implementação . Também no README do projeto há um link para um post no blog explicando o problema. Esse artigo também aponta para este tópico.

Alguém pode me ajudar a entender exatamente o que ConnectionLeaseTimeout significa?

Se no meu aplicativo eu definir para 1 minuto, isso significa que a cada minuto a concessão da conexão expirará? O que exatamente isso significa para falar com meu back-end?

Vejo que as pessoas estão falando sobre a atualização do DNS no back-end? Em um aplicativo Web padrão do Azure, publicar meu back-end ASP.NET causaria uma atualização de DNS?

Um tempo limite de 1 minuto não deixaria potencialmente uma janela em que o DNS fosse atualizado, mas o tempo limite ainda não ocorreu?

Editar. Perceber que isso não funciona no .NET Standard 2.0. Qual é o trabalho em torno?

Esperando por isso https://www.stevejgordon.co.uk/introduction-to-httpclientfactory-aspnetcore ou implementando pedaços dele você mesmo.

Alterar ConnectionLeaseTimeout não funcionou no Xamarin com .netstandard 2.0 !!!

@paradisehuman Eu suponho que você queira dizer ServicePoint.ConnectionLeaseTimeout . AFAIK também não funciona em .NET Core. No .NET Core 2.1, apresentamos SocketsHttpHandler.PooledConnectionLifetime . Além disso, o novo HttpClientFactory no ASP.NET pode alavancar ambos e faz ainda mais por você.
O Mono/Xamarin tem sua própria implementação de pilha de rede (eles não consomem código-fonte CoreFX para rede) - portanto, seria melhor registrar o bug em seu repositório com mais detalhes.

Infelizmente, não há como fazer isso com o .NET Core hoje. O ServicePointManager deve ser trazido para o .NET Core ou a funcionalidade equivalente semelhante deve ser habilitada de outra forma.

@onovotny , SocketsHttpHandler no .NET Core 2.1 expõe PooledConnectionLifetime, que atende a uma finalidade semelhante a ConnectionLeaseTimeout, apenas no nível do manipulador. Podemos considerar esta questão abordada neste momento?

@stephentoub vendo isso agora, sim, acho que PooledConnectionLifetime deve resolver isso. A comunidade pode abrir outro problema se algo ainda for necessário.

@onovotny @karelz @stephentoub ,

Todos vocês concordam que este é um resumo preciso da melhor forma de resolver o problema original de HttpClient/DNS singleton?

  • No .NET Framework 2.0+, use ServicePoint.ConnectionLeaseTimeout .

  • No .NET Core 2.1, use SocketsHttpHandler.PooledConnectionLifetime .

  • Para todas as outras plataformas/alvos/versões, não há solução/hack/contorno confiável, então nem se preocupe em tentar.

À medida que continuo aprendendo coisas com este segmento e outros (como ConnectionLeaseTimeout não funcionando de forma confiável em plataformas no .NET Standard 2.0), estou de volta à prancheta tentando resolver isso de uma biblioteca de uma maneira que funciona em tantas plataformas/versões quanto possível. O sniffing de plataforma é bom. Descartar/recriar periodicamente HttpClient é bom, se for eficaz. Tenho certeza de que errei na minha primeira tentativa, que era simplesmente enviar um cabeçalho connection:close para o host em intervalos regulares. Agradecemos antecipadamente por qualquer conselho!

@tmenier Para todas as outras plataformas/destinos/versões, não há solução/hack/contorno confiável, então nem se preocupe em tentar.

Você sempre pode reciclar a instância regularmente (por exemplo, basta criar uma nova instância e defini-la como variável estática que outros usam).
Isso é o que HttpClientFactory faz. Você pode optar por usar HttpClientFactory .

@karelz Obrigado, acho que isso me fornece um caminho a seguir. Talvez eu deva pular as instâncias de sniffing de plataforma e reciclar em todos os lugares, ou é aconselhável usar outras abordagens quando disponíveis (para evitar sobrecarga, etc.)?

A parte complicada pode ser saber _quando_ é seguro descartar instâncias recicladas (preciso me preocupar com simultaneidade/solicitações pendentes), mas posso dar uma olhada em como HttpClientFactory lida com isso. Obrigado novamente.

@tmenier Você não deseja reciclar instâncias a cada HttpClient chamada, pois isso o levará ao esgotamento do soquete tcp. A reciclagem precisa ser gerenciada (tempo limite ou manutenção de um pool com HttpClient s que podem ser expirados.

@KallDrexx Certo, estou apenas falando sobre _periodicamente_ reciclar um singleton (ou "pseudo"-singleton), como a cada poucos minutos. Acredito que envolverá o gerenciamento de um pool e algum tipo de descarte atrasado de instâncias mortas, uma vez que seja determinado que não há solicitações pendentes.

Ah desculpe não entendi o que você quis dizer com reciclar.

Você não deve precisar descartar os antigos. Deixe GC coletá-los.
O código deve ser tão simples quanto definir um novo "pseudo"-singleton periodicamente com base no temporizador. Nada mais.

Descobri que não descartá-los pode levar a lentidão à medida que você fica sem conexões http em cenários de alto desempenho.

@tmenier Sobre como usar SocketsHttpHandler.PooledConnectionLifetime no .NET Core 2.1, a criação de HttpClientHandler está completamente encapsulada em https://github.com/dotnet/wcf/blob/master/src/System.Private.ServiceModel/src/System/ ServiceModel/Channels/HttpChannelFactory.cs. Existe uma maneira de fazer isso ao usar o cliente WCF?

@edavedian sugiro perguntar no repositório dotnet/wcf. cc @mconnew @Lxiamail

a pilha subjacente resolve para WinHTTP ou Curl que não expõe os botões de controle avançados

Existe no .Net Core um cliente HTTP que funcione em sockets?

Existe no .Net Core um cliente HTTP que funcione em sockets?

Sim, SocketsHttpHandler, e a partir de 2.1 é o padrão.

O HttpClient armazena em cache os URIs que ele atinge, de forma semelhante ao que o RestClient acredito que faça? Se isso acontecer, por favor, deixe-me saber em qual versão (.NET Core 3.0/.NET Framework 3.0 etc) ele suporta isso.

Vejo que o cache só é feito em RestClient se o projeto for um aplicativo .Net Core 2.1+?

O HttpClient armazena em cache os URIs que ele atinge, da mesma forma que o RestClient, acredito, faz?

HttpClient não faz nenhum cache.

@SpiritBob não está claro a que forma de cache você está se referindo. Este problema está encerrado; se você puder, por favor, abra um novo problema com perguntas, podemos ajudá-lo a encontrar respostas.

@scalablecory Acredito que Davidsh respondeu com o que eu tinha em mente. Obrigado!

Acabei de ler este tópico e parece que houve um caso de uso que não foi abordado.

Ao usar HttpClientFactory , os HttpClient s resultantes fecharão automaticamente as conexões e farão uma nova pesquisa de DNS quando receberem um HttpRequestException com um SocketException interno indicando que o host não é mais válido e que o DNS foi potencialmente alterado (código de erro 11001 - nenhum host é conhecido)?

Se não, qual é a solução para fazer HttpClient realizar pesquisas de DNS antes que o TTL expire nos registros DNS?

Atualmente, estou usando HttpClientFactory com um HttpMessageHandler (configurado por meio de injeção de dependência usando AddHttpClient<T, U>().ConfigurePrimaryHttpMessageHandler(...) ) e quando um registro DNS é alterado antes do TTL expirar, ocorre um erro. Especificamente, vejo isso acontecendo ao fazer vários uploads para o Armazenamento de Blobs do Azure.

Esta página foi útil?
0 / 5 - 0 avaliações