Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM

Uma condição típica ao implementar CI/CD no Kubernetes: o aplicativo deve ser capaz de não aceitar novas solicitações de clientes antes de parar completamente e, o mais importante, concluir com êxito as existentes.

Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM

A conformidade com essa condição permite que você obtenha tempo de inatividade zero durante a implantação. No entanto, mesmo ao usar pacotes muito populares (como NGINX e PHP-FPM), você pode encontrar dificuldades que levarão a uma onda de erros a cada implantação...

Teoria. Como o pod vive

Já publicamos em detalhes sobre o ciclo de vida de um pod Este artigo. No contexto do tema em consideração, estamos interessados ​​no seguinte: no momento em que o pod entra no estado Terminando, novas solicitações deixam de ser enviadas para ele (pod removido na lista de terminais do serviço). Assim, para evitar tempo de inatividade durante a implantação, basta resolvermos o problema de parar a aplicação corretamente.

Você também deve lembrar que o período de carência padrão é 30 segundos: depois disso, o pod será encerrado e a aplicação deverá ter tempo para processar todas as solicitações antes deste período. Nota: embora qualquer solicitação que leve mais de 5 a 10 segundos já seja problemática, e o desligamento normal não ajudará mais ...

Para entender melhor o que acontece quando um pod é encerrado, basta observar o diagrama a seguir:

Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM

A1, B1 - Recebendo alterações sobre o estado do foco
A2 - SIGTERM de Partida
B2 – Removendo um pod dos endpoints
B3 – Recebendo alterações (a lista de endpoints mudou)
B4 - Atualizar regras de iptables

Observação: a exclusão do pod do endpoint e o envio do SIGTERM não acontecem sequencialmente, mas em paralelo. E devido ao fato do Ingress não receber imediatamente a lista atualizada de Endpoints, novas solicitações de clientes serão enviadas para o pod, o que causará um erro 500 durante o encerramento do pod (para material mais detalhado sobre este assunto, nós traduzido). Este problema precisa ser resolvido das seguintes maneiras:

  • Enviar conexão: feche os cabeçalhos de resposta (se se tratar de uma aplicação HTTP).
  • Caso não seja possível fazer alterações no código, o artigo a seguir descreve uma solução que permitirá processar solicitações até o final do período de carência.

Teoria. Como NGINX e PHP-FPM encerram seus processos

NGINX

Vamos começar com o NGINX, já que tudo é mais ou menos óbvio com ele. Mergulhando na teoria, aprendemos que o NGINX tem um processo mestre e vários “trabalhadores” – estes são processos filhos que processam solicitações de clientes. Uma opção conveniente é fornecida: usando o comando nginx -s <SIGNAL> encerrar processos no modo de desligamento rápido ou no modo de desligamento normal. Obviamente, é esta última opção que nos interessa.

Então tudo é simples: você precisa adicionar gancho pré-parada um comando que enviará um sinal de desligamento normal. Isso pode ser feito em Deployment, no bloco container:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

Agora, quando o pod for encerrado, veremos o seguinte nos logs do contêiner NGINX:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

E isso significará o que precisamos: o NGINX aguarda a conclusão das solicitações e, em seguida, encerra o processo. Porém, a seguir consideraremos também um problema comum devido ao qual, mesmo com o comando nginx -s quit o processo termina incorretamente.

E nesta fase terminamos com o NGINX: pelo menos pelos logs você pode entender que tudo está funcionando como deveria.

Qual é o problema com PHP-FPM? Como ele lida com o desligamento normal? Vamos descobrir.

PHP-FPM

No caso do PHP-FPM, há um pouco menos de informação. Se você se concentrar em manual oficial de acordo com PHP-FPM, dirá que os seguintes sinais POSIX são aceitos:

  1. SIGINT, SIGTERM — desligamento rápido;
  2. SIGQUIT - desligamento normal (o que precisamos).

Os demais sinais não são necessários nesta tarefa, portanto omitiremos sua análise. Para encerrar o processo corretamente, você precisará escrever o seguinte gancho preStop:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

À primeira vista, isso é tudo o que é necessário para realizar um desligamento normal em ambos os contêineres. Contudo, a tarefa é mais difícil do que parece. Abaixo estão dois casos em que o encerramento normal não funcionou e causou indisponibilidade de curto prazo do projeto durante a implantação.

Prática. Possíveis problemas com o desligamento normal

NGINX

Em primeiro lugar, é útil lembrar: além de executar o comando nginx -s quit Há mais uma etapa que vale a pena prestar atenção. Encontramos um problema em que o NGINX ainda enviava SIGTERM em vez do sinal SIGQUIT, fazendo com que as solicitações não fossem concluídas corretamente. Casos semelhantes podem ser encontrados, por exemplo, aqui. Infelizmente, não conseguimos determinar o motivo específico desse comportamento: havia uma suspeita sobre a versão NGINX, mas não foi confirmada. O sintoma era que mensagens foram observadas nos logs do contêiner NGINX: "abra o soquete nº 10 deixado na conexão 5", após o qual o pod parou.

Podemos observar tal problema, por exemplo, a partir das respostas sobre o Ingress que precisamos:

Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM
Indicadores de códigos de status no momento da implantação

Nesse caso, recebemos apenas um código de erro 503 do próprio Ingress: ele não pode acessar o contêiner NGINX, pois não está mais acessível. Se você observar os logs do contêiner com NGINX, eles contêm o seguinte:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

Após a alteração do sinal de parada, o contêiner começa a parar corretamente: isso é confirmado pelo fato de o erro 503 não ser mais observado.

Se você encontrar um problema semelhante, faz sentido descobrir qual sinal de parada é usado no contêiner e como exatamente se parece o gancho preStop. É bem possível que a razão esteja precisamente nisso.

PHP-FPM... e mais

O problema do PHP-FPM é descrito de forma trivial: ele não espera a conclusão dos processos filhos, ele os encerra, razão pela qual ocorrem erros 502 durante a implantação e outras operações. Existem vários relatórios de bugs no bugs.php.net desde 2005 (por exemplo aqui и aqui), que descreve esse problema. Mas provavelmente você não verá nada nos logs: o PHP-FPM anunciará a conclusão de seu processo sem erros ou notificações de terceiros.

Vale esclarecer que o problema em si pode depender em menor ou maior grau da própria aplicação e pode não se manifestar, por exemplo, no monitoramento. Se você encontrar isso, uma solução simples vem à mente primeiro: adicione um gancho preStop com sleep(30). Isso permitirá que você conclua todas as solicitações anteriores (e não aceitamos novas, pois pod em condição Terminando), e após 30 segundos o próprio pod terminará com um sinal SIGTERM.

Acontece que lifecycle para o contêiner ficará assim:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

No entanto, devido aos 30 segundos sleep nós fortemente aumentaremos o tempo de implantação, pois cada pod será encerrado mínimo 30 segundos, o que é ruim. O que pode ser feito sobre isso?

Voltemo-nos para o responsável pela execução direta do pedido. No nosso caso é PHP-FPMQue por padrão não monitora a execução de seus processos filhos: O processo mestre é encerrado imediatamente. Você pode alterar esse comportamento usando a diretiva process_control_timeout, que especifica os limites de tempo para os processos filhos aguardarem sinais do mestre. Se você definir o valor como 20 segundos, isso cobrirá a maioria das consultas em execução no contêiner e interromperá o processo mestre assim que forem concluídas.

Com esse conhecimento, voltemos ao nosso último problema. Conforme mencionado, o Kubernetes não é uma plataforma monolítica: a comunicação entre os seus diferentes componentes leva algum tempo. Isto é especialmente verdadeiro quando consideramos a operação de Ingresses e outros componentes relacionados, pois devido a tal atraso no momento da implantação é fácil obter um aumento de 500 erros. Por exemplo, um erro pode ocorrer na fase de envio de uma solicitação ao upstream, mas o “intervalo de tempo” de interação entre os componentes é bastante curto - menos de um segundo.

Assim, No total com a já mencionada directiva process_control_timeout você pode usar a seguinte construção para lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

Neste caso compensaremos o atraso com o comando sleep e não aumente significativamente o tempo de implantação: afinal, a diferença entre 30 segundos e um é perceptível?.. Na verdade, é o process_control_timeoutE lifecycle usado apenas como uma “rede de segurança” em caso de atraso.

De um modo geral o comportamento descrito e a solução alternativa correspondente não se aplicam apenas ao PHP-FPM. Uma situação semelhante pode surgir de uma forma ou de outra ao usar outras linguagens/frameworks. Se você não conseguir corrigir o desligamento normal de outras maneiras - por exemplo, reescrevendo o código para que o aplicativo processe corretamente os sinais de encerramento - você poderá usar o método descrito. Pode não ser o mais bonito, mas funciona.

Prática. Teste de carga para verificar o funcionamento do pod

O teste de carga é uma das formas de verificar o funcionamento do contêiner, pois esse procedimento o aproxima das condições reais de combate quando os usuários visitam o local. Para testar as recomendações acima, você pode usar Yandex.Tankom: Cobre perfeitamente todas as nossas necessidades. A seguir estão dicas e recomendações para a realização de testes com um exemplo claro de nossa experiência graças aos gráficos do Grafana e do próprio Yandex.Tank.

O mais importante aqui é verifique as alterações passo a passo. Depois de adicionar uma nova correção, execute o teste e veja se os resultados mudaram em comparação com a última execução. Caso contrário, será difícil identificar soluções ineficazes e, a longo prazo, só poderá causar danos (por exemplo, aumentar o tempo de implementação).

Outra nuance é observar os logs do contêiner durante seu encerramento. As informações sobre o desligamento normal estão registradas lá? Há algum erro nos logs ao acessar outros recursos (por exemplo, para um contêiner PHP-FPM vizinho)? Erros no próprio aplicativo (como no caso do NGINX descrito acima)? Espero que as informações introdutórias deste artigo ajudem você a entender melhor o que acontece com o contêiner durante seu encerramento.

Assim, o primeiro teste ocorreu sem lifecycle e sem diretivas adicionais para o servidor de aplicativos (process_control_timeout em PHP-FPM). O objetivo deste teste foi identificar o número aproximado de erros (e se há algum). Além disso, a partir de informações adicionais, você deve saber que o tempo médio de implantação de cada pod foi de cerca de 5 a 10 segundos até que estivesse totalmente pronto. Os resultados são:

Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM

O painel de informações do Yandex.Tank mostra um pico de 502 erros, que ocorreram no momento da implantação e duraram em média até 5 segundos. Presumivelmente, isso ocorreu porque as solicitações existentes para o pod antigo estavam sendo encerradas quando ele estava sendo encerrado. Depois disso, apareceram 503 erros, resultado de um contêiner NGINX parado, que também interrompeu conexões devido ao backend (o que impediu o Ingress de se conectar a ele).

Vamos ver como process_control_timeout no PHP-FPM nos ajudará a aguardar a conclusão dos processos filhos, ou seja, corrigir tais erros. Reimplante usando esta diretiva:

Dicas e truques do Kubernetes: recursos de desligamento normal em NGINX e PHP-FPM

Não há mais erros durante a 500ª implantação! A implantação foi bem-sucedida e o desligamento normal funciona.

Porém, vale lembrar o problema com os contêineres do Ingress, um pequeno percentual de erros que podemos receber por defasagem de tempo. Para evitá-los, basta adicionar uma estrutura com sleep e repita a implantação. No entanto, no nosso caso particular, nenhuma alteração foi visível (novamente, nenhum erro).

Conclusão

Para encerrar o processo normalmente, esperamos o seguinte comportamento do aplicativo:

  1. Aguarde alguns segundos e pare de aceitar novas conexões.
  2. Aguarde a conclusão de todas as solicitações e feche todas as conexões de manutenção de atividade que não estão executando solicitações.
  3. Encerre seu processo.

No entanto, nem todos os aplicativos podem funcionar dessa forma. Uma solução para o problema nas realidades do Kubernetes é:

  • adicionar um gancho pré-parada que irá aguardar alguns segundos;
  • estudando o arquivo de configuração do nosso backend para os parâmetros apropriados.

O exemplo do NGINX deixa claro que mesmo um aplicativo que inicialmente deveria processar sinais de encerramento corretamente pode não fazê-lo, portanto, é fundamental verificar se há erros 500 durante a implantação do aplicativo. Isso também permite que você analise o problema de forma mais ampla e não se concentre em um único pod ou contêiner, mas observe toda a infraestrutura como um todo.

Como ferramenta de teste, você pode usar o Yandex.Tank em conjunto com qualquer sistema de monitoramento (no nosso caso, os dados foram retirados do Grafana com backend Prometheus para o teste). Problemas com desligamento normal são claramente visíveis sob cargas pesadas que o benchmark pode gerar, e o monitoramento ajuda a analisar a situação com mais detalhes durante ou após o teste.

Em resposta ao feedback do artigo: vale ressaltar que os problemas e soluções são descritos aqui em relação ao NGINX Ingress. Para outros casos, existem outras soluções, que poderemos considerar nos seguintes materiais da série.

PS

Outros da série de dicas e truques do K8s:

Fonte: habr.com

Adicionar um comentário