Problema de escalabilidade ao usar solicitações da Web assíncronas de saída no IIS 7.5

Um pouco de uma descrição longa abaixo, mas é um problema bastante complicado. Eu tentei cobrir o que sabemos sobre o problema, a fim de restringir a pesquisa. A questão é mais uma investigação em andamento do que uma baseada em uma única pergunta, mas acho que pode ajudar os outros também. Mas, por favor, adicione informações nos comentários ou corrija-me se achar que estou errado sobre algumas suposições abaixo.

ATUALIZAÇÃO 19/2, 2013: Nós eliminamos alguns pontos de interrogação e eu tenho uma teoria sobre qual é o principal problema que atualizarei abaixo. Não está pronto para escrever uma resposta "resolvida" para ele ainda.

ATUALIZAÇÃO 24/4 de 2013: As coisas foram estáveis ​​na produção (embora eu acredite que é temporário) por um tempo agora e acho que isso se deve a dois motivos. 1) aumento de porta e 2) número reduzido de solicitações de saída (encaminhadas). Continuarei esta atualização mais abaixo no contexto correto.

Estamos atualmente fazendo uma investigação em nosso ambiente de produção paradeterminar por que nosso servidor da web do IIS não é dimensionado quando muitas solicitações de serviço da web assíncronas de saída estão sendo feitas (uma solicitação recebida pode acionar várias solicitações de saída).

A CPU está apenas a 20%, mas recebemos erros HTTP 503 nas solicitações recebidas e muitas solicitações da Web de saída recebem a seguinte exceção:“SocketException: uma operação em um soquete não pôde ser executada porque o sistema não tinha espaço suficiente no buffer ou porque a fila estava cheia” Claramente há um gargalo de escalabilidade em algum lugar e precisamos descobrir o que é e se é possível resolvê-lo por configuração.

Contexto de aplicação:

Estamos executando o pipeline gerenciado integrado do IIS v7.5 usando o .NET 4.5 no sistema operacional Windows 2008 R2 de 64 bits. Nós usamos apenas 1 processo de trabalho no IIS. Hardware varia um pouco, mas a máquina usada para examinar o erro é um núcleo Intel Xeon 8 (16 hyper threaded).

Usamos solicitações da web assíncronas e síncronas. Aqueles que são assíncronos estão usando o novo suporte async .NET para fazer com que cada solicitação recebida faça várias solicitações HTTP no aplicativo para outros servidores em conexões TCP persistentes (keep-alive). O tempo de execução da solicitação síncrona é baixo de 0 a 32 ms (ocorrem mais vezes devido à troca de contexto de encadeamento). Para as solicitações assíncronas, o tempo de execução pode ser de até 120 ms antes que as solicitações sejam canceladas.

Normalmente, cada servidor atende até 1000 solicitações recebidas. As solicitações de saída são ~ 300 solicitações / seg até ~ 600 solicitações / seg quando o problema começa a surgir. Os problemas só ocorrem quando async assíncrono. pedidos estão habilitados no servidor e nós vamos acima de um certo nível de pedidos de saída (~ 600 req./s).

Possíveis soluções para o problema:

Pesquisando na Internet sobre este problema revela uma infinidade de candidatos possíveis soluções. No entanto, eles são muito dependentes de versões do .NET, IIS e sistema operacional, portanto, leva tempo para encontrar algo em nosso contexto (ano de 2013).

Abaixo está uma lista de candidatos à solução e as conclusões a que chegamos até agora com relação ao nosso contexto de configuração. Eu categorizei as áreas problemáticas detectadas, até agora nas seguintes categorias principais:

Algumas fila (s) encherProblemas com conexões e portas TCP(ATUALIZAÇÃO 19/2, 2013: Este é o problema)Alocação muito lenta de recursosProblemas de memória(ATUALIZAÇÃO 19/2, 2013: Este é provavelmente outro problema)1) Algumas fila (s) encher

A mensagem de exceção de solicitação assíncrona de saída indica que alguma fila de buffer foi preenchida. Mas não diz qual fila / buffer. Através doFórum do IIS (e postagem de blog referenciada lá) Eu consegui distinguir 4 de possivelmente 6 (ou mais) tipos diferentes de filas no pipeline de requisição rotulado A-F abaixo.

Embora deva ser declarado que de todas as filas abaixo definidas, vemos com certeza que o contador de desempenho 1.B) ThreadPool Requests Enfileirado fica muito cheio durante a carga problemática.Portanto, é provável que a causa do problema esteja no nível .NET e não abaixo dela (C-F).

1.A) Fila de nível do .NET Framework?

Usamos a classe de estrutura .NET WebClient para emitir a chamada assíncrona (suporte assíncrono) em oposição ao HttpClient que tivemos o mesmo problema, mas com um limite de req / s muito menor. Não sabemos se a implementação do .NET Framework oculta qualquer fila interna ou não acima do pool de segmentos. Nós não achamos que este é o caso.

1.B) Pool de threads do .NET

O pool de threads atua como uma fila natural, pois o Agendador de Threads (padrão) do .NET está escolhendo segmentos do pool de threads a ser executado.

Contador de desempenho: [ASP.NET v4.0.30319]. [Pedidos em fila].

Possibilidades de configuração:

(ApplicationPool) maxConcurrentRequestsPerCPU deve ser 5000 (em vez de 12 anteriores). Portanto, no nosso caso, deve ser 5000 * 16 = 80.000 solicitações / s, o que deve ser suficiente em nosso cenário.(processModel) autoConfig = true / false que permitealgumas configurações relacionadas ao threadPool a ser definido de acordo com a configuração da máquina.Usamos true, que é um candidato a erro em potencial, já que esses valores podem ser configurados erroneamente para nossa (alta) necessidade.1.C) Fila nativa global, ampla do processo (somente modo integrado ao IIS)

Se o conjunto de encadeamentos estiver cheio, as solicitações começam a se acumular nessa fila nativa (não gerenciada).

Contador de desempenho:[ASP.NET v4.0.30319]. [Solicitações na fila nativa]

Possibilidades de configuração: ????

1.D) fila HTTP.sys

Essa fila não é a mesma fila que 1.C) acima. Aqui está uma explicação como me foi dito“A fila do kernel HTTP.sys é essencialmente uma porta de conclusão na qual o modo de usuário (IIS) recebe solicitações do modo kernel (HTTP.sys). Ele tem um limite de fila e, quando isso for excedido, você receberá um código de status 503. O log HTTPErr também indicará que isso aconteceu registrando um status 503 e uma mensagem “QueueFull”.

Contador de desempenho: Não consegui encontrar nenhum contador de desempenho para essa fila, mas ao habilitar o log HTTPErr do IIS, deve ser possível detectar se essa fila é inundada.

Possibilidades de configuração: Isso é definido no IIS no pool de aplicativos, configuração avançada: Comprimento da Fila. O valor padrão é 1000. Vi recomendações para aumentá-lo para 10.000. Embora tentar este aumento não tenha resolvido o nosso problema.

1.E) Fila (s) desconhecida (s) do Sistema Operacional?

Embora improvável, acho que o sistema operacional poderia ter uma fila entre o buffer da placa de rede e a fila HTTP.sys.

1.F) Buffer da placa de rede:

Conforme a solicitação chega à placa de rede, deve ser natural que eles sejam colocados em algum buffer para serem capturados por algum encadeamento do kernel do sistema operacional. Como esta é a execução no nível do kernel e, portanto, rápida, não é provável que seja o culpado.

Contador de desempenho do Windows: [Interface de rede]. [Pacotes recebidos descartados] usando a instância da placa de rede.

Possibilidades de configuração: ????

2) Problemas com conexões e portas TCP

Este é um candidato que aparece aqui e ali, embora nossos pedidos TCP de saída (assíncrono) sejam feitos de uma conexão TCP persistente (keep-alive). Assim, à medida que o tráfego cresce, o número de portas efêmeras disponíveis deve crescer apenas devido às solicitações recebidas. E sabemos com certeza que o problema só surge quando temos solicitações de saída ativadas.

No entanto, o problema ainda pode surgir devido a que a porta é alocada durante um período de tempo maior da solicitação. Uma solicitação de saída pode levar até 120 ms para ser executada (antes que a tarefa .NET (thread) seja cancelada), o que pode significar que o número de portas seja alocado por um período de tempo maior. Analisando o Contador de Desempenho do Windows, verifica-se essa suposição desde o número de TCPv4. [Conexão Estabelecida] vai do normal de 2-3.000 até o pico de quase 12.000 no total quando o problema ocorre.

Verificamos que a quantidade máxima configurada de conexões TCP está definida para o padrão de 16384. Nesse caso, pode não ser o problema, embora estejamos perigosamente próximos do limite máximo.

Quando tentamos usar o netstat no servidor, ele geralmente retorna sem nenhuma saída, e o uso do TcpView mostra poucos itens no começo. Se deixarmos o TcpView rodar por um tempo, ele logo começa a mostrar novas conexões (recebidas) rapidamente (digamos 25 conexões / seg). Quase todas as conexões estão no estado TIME_WAIT desde o início, sugerindo que elas já foram concluídas e aguardando a limpeza. Essas conexões usam portas efêmeras? A porta local é sempre 80 e a porta remota está aumentando. Queríamos usar o TcpView para ver as conexões de saída, mas não podemos vê-las listadas, o que é muito estranho. Essas duas ferramentas não conseguem lidar com a quantidade de conexões que estamos tendo?(Para ser continuado ... Mas por favor preencha com informações se você sabe ...)

Mais longe, como um chute lateral aqui. Foi sugerido neste post do blog "Uso de thread do ASP.NET no IIS 7.5, IIS 7.0 e IIS 6.0"que ServicePointManager.DefaultConnectionLimit deve ser definido como int maxValue, que poderia ser um problema. Mas no .NET 4.5, este é o padrão já desde o início.

ATUALIZAÇÃO 19/2, 2013:

É razoável supor que, de fato, atingimos o limite máximo de 16.384 portas. Duplicamos o número de portas em todos os servidores, com exceção de um, e apenas o servidor antigo teria problemas quando atingimos o pico de carga das solicitações de saída. Então, por que o TCP.v4 [Connections Established] nunca nos mostra um número maior que ~ 12.000 em tempos problemáticos? MY theory: Muito provavelmente, embora não seja estabelecido como fato (ainda), o Performance Counter TCPv4 [Conexões Estabelecidas] não é equivalente ao número de portas atualmente alocadas. Ainda não tive tempo de acompanhar o estado do TCP, mas imagino que existam mais estados de TCP do que a "Conexão Estabelecida" mostra, o que tornaria a porta como sendo ccupied. Embora não possamos usar o contador de desempenho "Conexão Estabelecida" como uma forma de detectar o perigo de ficar sem portas, é importante encontrarmos alguma outra maneira de detectar quando alcançarmos esse intervalo máximo de portas. E conforme descrito no texto acima, não podemos usar o NetStat ou o aplicativo TCPview para isso em nossos servidores de produção. Isto é um problema! (Eu vou escrever mais sobre isso em uma próxima resposta que eu acho para este post)O número de portas é restrito em janelas para um máximo de 65.535 (embora o primeiro ~ 1000 provavelmente não deva ser usado). Mas deve ser possível evitar o problema de ficar sem portas, diminuindo o tempo para o estado TCP TIME_WAIT (padrão para 240 segundos), conforme descrito em vários locais. Ele deve liberar as portas mais rapidamente. Eu estava um pouco hesitante sobre isso, já que usamos consultas de banco de dados de longa duração, bem como chamadas do WCF no TCP, e eu não gostaria de diminuir a restrição de tempo. Apesar de não ter percebido a leitura da máquina de estado TCP ainda, acho que talvez não seja um problema. O estado TIME_WAIT, eu acho, só está lá para permitir o aperto de mão de um desligamento adequado para o cliente. Portanto, a transferência de dados real em uma conexão TCP existente não deve expirar devido a esse limite de tempo. Pior cenário, o cliente não é desligado corretamente e, em vez disso, chega ao tempo limite. Eu acho que todos os navegadores podem não estar implementando isso corretamente e isso poderia ser um problema apenas no lado do cliente. Embora eu esteja supondo um pouco aqui ...

ATUALIZAÇÃO FINAL 19/2, 2013

ATUALIZAÇÃO 24/4 de 2013: Nós aumentamos o número de portas para o valor máximo. Ao mesmo tempo, não recebemos tantas solicitações de saída encaminhadas quanto antes. Estes dois em combinação devem ser a razão pela qual não tivemos nenhum incidente. No entanto, é apenas temporário, pois o número de solicitações de saída está destinado a aumentar novamente no futuro nesses servidores. O problema, portanto, é que, na minha opinião, essa porta para as solicitações recebidas deve permanecer aberta durante o período de tempo para a resposta das solicitações encaminhadas. Em nosso aplicativo, esse limite de cancelamento para essas solicitações encaminhadas é de 120 ms, o que pode ser comparado com os <1 ms normais para manipular uma solicitação não encaminhada. Então, em essência, eu acredito que o número definido de portas é o principal gargalo de escalabilidade em servidores de alta taxa de transferência (> 1000 solicitações / seg em ~ 16 máquinas de núcleos) que estamos usando. Isto em combinação com o trabalho do GC na recarga de cache (veja abaixo) torna o servidor especialmente vulnerável.

ATUALIZAÇÃO FINAL 24/4

3) alocação muito lenta de recursos

Nossos contadores de desempenho mostram que o número de solicitações enfileiradas no Conjunto de Encadeamentos (1B) varia muito durante o tempo do problema. Então, potencialmente, isso significa que temos uma situação dinâmica na qual o comprimento da fila começa a oscilar devido a mudanças no ambiente. Por exemplo, esse seria o caso se houvesse mecanismos de proteção contra inundação que são ativados quando o tráfego está inundando. Como é, temos vários desses mecanismos:

3.A) Balanceador de carga da Web

Quando as coisas ficam muito ruins e o servidor responde com um erro HTTP 503, o balanceador de carga removerá automaticamente o servidor da Web de estar ativo na produção por um período de 15 segundos. Isso significa que os outros servidores receberão o aumento da carga durante o período de tempo. Durante o “período de resfriamento”, o servidor pode terminar de atender a sua solicitação e será automaticamente restabelecido quando o balanceador de carga fizer seu próximo ping. É claro que isso só é bom, desde que todos os servidores não tenham um problema de uma só vez. Felizmente, até agora, não estivemos nesta situação.

3.B) válvula de aplicação específica

No aplicativo da Web, temos nossa própria válvula construída (Sim. É uma "válvula". Não é um "valor") acionada por um Contador de desempenho do Windows para solicitações enfileiradas no pool de threads. Existe um thread, iniciado em Application_Start, que verifica este valor do contador de desempenho por segundo. E se o valor exceder 2000, todo o tráfego de saída deixa de ser iniciado. No segundo seguinte, se o valor da fila estiver abaixo de 2000, o tráfego de saída será iniciado novamente.

O mais estranho é que isso não nos ajudou a alcançar o cenário de erro, já que não registramos muito isso. Pode significar que, quando o tráfego nos atinge com dificuldade, as coisas correm mal rapidamente, de modo que a verificação do intervalo de 1 segundo na verdade é muito alta.

3.C) Aumento lento de pool de threads (e diminuição) de threads

Há outro aspecto disso também. Quando há necessidade de mais encadeamentos no pool de aplicativos, esses encadeamentos são alocados muito lentamente. Pelo que li, 1-2 threads por segundo. Isso ocorre porque é caro criar encadeamentos e, como você não quer muitos encadeamentos de qualquer maneira, a fim de evitar uma troca de contexto cara no caso síncrono, acho que isso é natural. No entanto, isso também deve significar que, se ocorrer uma grande explosão repentina de tráfego, o número de threads não estará próximo o suficiente para satisfazer a necessidade no cenário assíncrono e o enfileiramento de solicitações será iniciado. Este é um candidato a um problema muito provável, eu acho. Uma solução candidata pode ser aumentar a quantidade mínima de encadeamentos criados no ThreadPool. Mas eu acho que isso também pode afetar o desempenho das solicitações em execução síncrona.

4) problemas de memória

(Joey Reyes escreveu sobre issoaqui em um post de blog) Como os objetos são coletados mais tarde para solicitações assíncronas (até 120ms mais tarde no nosso caso), problemas de memória podem surgir, pois os objetos podem ser promovidos para a geração 1 e a memória não será recuperada com a freqüência que deveria. O aumento da pressão no Garbage Collector pode muito bem fazer com que a mudança de contexto de thread estendida ocorra e enfraqueça ainda mais a capacidade do servidor.

No entanto, não vemos um aumento no uso de GC nem de CPU durante o período do problema, por isso, não achamos que o mecanismo de otimização de CPU sugerido seja uma solução para nós.

ATUALIZAÇÃO 19/2, 2013: Usamos um mecanismo de troca de cache em intervalos regulares nos quais um cache (quase) cheio na memória é recarregado na memória e o cache antigo pode ser coletado como lixo. Nesses momentos, o GC terá que trabalhar mais e roubar recursos do tratamento normal da solicitação. Usando o contador de desempenho do Windows para alternância de contexto de thread, ele mostra que o número de switches de contexto diminui significativamente em relação ao valor alto normal no momento de um alto uso de GC. Eu acho que durante essas recargas de cache, o servidor é extremamente vulnerável para pedidos de enfileiramento e é necessário reduzir o footprint do GC. Uma possível solução para o problema seria apenas preencher o cache sem alocar memória o tempo todo. Um pouco mais de trabalho, mas deve ser factível.

ATUALIZAÇÃO 24/4 de 2013: Ainda estou no meio do ajuste da memória de recarga do cache para evitar que o GC seja executado. Mas normalmente temos cerca de 1.000 solicitações enfileiradas temporariamente quando o GC é executado. Como ele é executado em todos os encadeamentos, é natural que ele roube recursos do tratamento de solicitações normais. Vou atualizar esse status assim que esse ajuste for implementado e pudermos ver a diferença.

ATUALIZAÇÃO FINAL 24/4

questionAnswers(2)

yourAnswerToTheQuestion