Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM

Unha condición típica ao implementar CI/CD en Kubernetes: a aplicación debe ser capaz de non aceptar novas solicitudes de clientes antes de deterse por completo e, o máis importante, completar con éxito as existentes.

Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM

O cumprimento desta condición permítelle conseguir un tempo de inactividade cero durante a implantación. Non obstante, mesmo cando se usan paquetes moi populares (como NGINX e PHP-FPM), podes atopar dificultades que provocarán un aumento de erros con cada implementación...

Teoría. Como vive pod

Xa publicamos en detalle sobre o ciclo de vida dunha vaina Este artigo. No contexto do tema en consideración, interésanos o seguinte: no momento en que a vaina entra no estado Rematar, deixan de enviarse novas solicitudes (pod eliminado da lista de puntos finais para o servizo). Así, para evitar tempo de inactividade durante a implantación, abonda con resolver o problema de parar a aplicación correctamente.

Tamén debes lembrar que o período de carencia predeterminado é 30 segundos: despois disto, o pod finalizarase e a solicitude debe ter tempo para procesar todas as solicitudes antes deste prazo. Nota: aínda que calquera solicitude que leve máis de 5-10 segundos xa é problemática, e un apagado gracioso xa non o axudará...

Para comprender mellor o que ocorre cando un pod termina, basta con mirar o seguinte diagrama:

Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM

A1, B1 - Recepción de cambios sobre o estado do fogar
A2 - Saída SIGTERM
B2 - Eliminar un pod dos extremos
B3 - Recepción de cambios (a lista de puntos finais cambiou)
B4 - Actualizar as regras de iptables

Ten en conta: a eliminación do pod do punto final e o envío de SIGTERM non se realizan de forma secuencial, senón en paralelo. E debido ao feito de que Ingress non recibe inmediatamente a lista actualizada de Endpoints, as novas solicitudes dos clientes enviaranse ao pod, o que provocará un erro 500 durante a terminación do pod. (para obter material máis detallado sobre esta cuestión, nós traducido). Este problema debe resolverse das seguintes formas:

  • Enviar conexión: peche nas cabeceiras de resposta (se se trata dunha aplicación HTTP).
  • Se non é posible facer cambios no código, o seguinte artigo describe unha solución que lle permitirá procesar as solicitudes ata o final do período de gracia.

Teoría. Como NGINX e PHP-FPM terminan os seus procesos

Nginx

Comecemos con NGINX, xa que todo é máis ou menos obvio con el. Afondando na teoría, aprendemos que NGINX ten un proceso mestre e varios "traballadores": estes son procesos fillos que procesan as solicitudes dos clientes. Ofrécese unha opción conveniente: usar o comando nginx -s <SIGNAL> finalizar os procesos en modo de apagado rápido ou apagado elegante. Evidentemente, é esta última opción a que nos interesa.

Entón todo é sinxelo: hai que engadir preStop-gancho un comando que enviará un sinal de apagado elegante. Isto pódese facer en Implementación, no bloque contedor:

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

Agora, cando se apague o pod, veremos o seguinte nos rexistros do contedor 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 isto significará o que necesitamos: NGINX agarda a que se completen as solicitudes e logo mata o proceso. Non obstante, a continuación tamén teremos en conta un problema común debido ao cal, mesmo co comando nginx -s quit o proceso remata incorrectamente.

E nesta fase rematamos con NGINX: polo menos a partir dos rexistros pódese entender que todo funciona como debería.

Cal é o problema con PHP-FPM? Como xestiona o apagado gracioso? Imos descubrir.

PHP-FPM

No caso de PHP-FPM, hai un pouco menos de información. Se te enfocas manual oficial segundo PHP-FPM, dirá que se aceptan os seguintes sinais POSIX:

  1. SIGINT, SIGTERM - apagado rápido;
  2. SIGQUIT - apagado gracioso (o que necesitamos).

Os sinais restantes non son necesarios nesta tarefa, polo que omitiremos a súa análise. Para finalizar o proceso correctamente, terás que escribir o seguinte gancho preStop:

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

A primeira vista, isto é todo o necesario para realizar un apagado elegante en ambos os contedores. Non obstante, a tarefa é máis difícil do que parece. A continuación móstranse dous casos nos que a parada graciosa non funcionou e provocou a indisponibilidade do proxecto a curto prazo durante a implantación.

Práctica. Posibles problemas co apagado gracioso

Nginx

En primeiro lugar, é útil lembrar: ademais de executar o comando nginx -s quit Hai unha etapa máis á que paga a pena prestar atención. Atopamos un problema polo que NGINX aínda enviaba SIGTERM en lugar do sinal SIGQUIT, o que provocou que as solicitudes non se completasen correctamente. Pódense atopar casos similares, por exemplo, aquí. Desafortunadamente, non puidemos determinar o motivo específico deste comportamento: había unha sospeita sobre a versión de NGINX, pero non se confirmou. O síntoma foi que se observaron mensaxes nos rexistros do contedor de NGINX: "abrir o socket #10 que queda na conexión 5", despois de que a vaina parou.

Podemos observar tal problema, por exemplo, a partir das respostas no Ingress que necesitamos:

Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM
Indicadores dos códigos de estado no momento da implantación

Neste caso, só recibimos un código de erro 503 do propio Ingress: non pode acceder ao contedor NGINX, xa que xa non é accesible. Se miras os rexistros de contedores con NGINX, conteñen o seguinte:

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

Despois de cambiar o sinal de parada, o recipiente comeza a deterse correctamente: isto é confirmado polo feito de que xa non se observa o erro 503.

Se atopas un problema semellante, ten sentido descubrir que sinal de parada se usa no recipiente e como é exactamente o gancho preStop. É moi posible que a razón estea precisamente nisto.

PHP-FPM... e moito máis

O problema con PHP-FPM descríbese dun xeito trivial: non agarda a que se completen os procesos fillos, os termina, polo que se producen erros 502 durante a implantación e outras operacións. Hai varios informes de erros en bugs.php.net desde 2005 (p. ex aquí и aquí), que describe este problema. Pero o máis probable é que non vexa nada nos rexistros: PHP-FPM anunciará a finalización do seu proceso sen erros nin notificacións de terceiros.

Cabe aclarar que o propio problema pode depender en menor ou maior medida da propia aplicación e pode non manifestarse, por exemplo, no seguimento. Se o atopas, primeiro vén á mente unha solución sinxela: engade un gancho preStop con sleep(30). Permitirache completar todas as solicitudes anteriores (e non aceptamos novas, xa que pod xa ser quen de Rematar), e despois de 30 segundos a propia vaina rematará cun sinal SIGTERM.

Acontece que lifecycle pois o recipiente terá o seguinte aspecto:

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

Non obstante, debido aos 30 segundos sleep nós fuertemente aumentaremos o tempo de implantación, xa que cada pod finalizará mínimo 30 segundos, que é malo. Que se pode facer ao respecto?

Imos dirixirnos ao responsable da execución directa da solicitude. No noso caso é PHP-FPMQue por defecto non supervisa a execución dos seus procesos fillos: o proceso mestre finaliza inmediatamente. Podes cambiar este comportamento usando a directiva process_control_timeout, que especifica os límites de tempo para que os procesos fillos agarden os sinais do mestre. Se estableces o valor en 20 segundos, cubrirá a maioría das consultas que se executan no contedor e deterá o proceso mestre unha vez que se completen.

Con este coñecemento, volvamos ao noso último problema. Como se mencionou, Kubernetes non é unha plataforma monolítica: a comunicación entre os seus diferentes compoñentes leva algún tempo. Isto é especialmente certo cando consideramos o funcionamento de Ingresses e outros compoñentes relacionados, xa que debido a tal atraso no momento da implantación é fácil conseguir un aumento de 500 erros. Por exemplo, pode ocorrer un erro na fase de envío dunha solicitude a un canal ascendente, pero o "desfase de tempo" da interacción entre os compoñentes é bastante curto, menos dun segundo.

Polo tanto En total coa directiva xa mencionada process_control_timeout pode utilizar a seguinte construción para lifecycle:

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

Neste caso, compensaremos o atraso co comando sleep e non aumentes moito o tempo de implantación: hai unha diferenza notable entre 30 segundos e un?... De feito, é o process_control_timeoutE lifecycle usado só como "rede de seguridade" en caso de desfase.

En xeral o comportamento descrito e a solución alternativa non só se aplican a PHP-FPM. Unha situación semellante pode darse dun xeito ou doutro cando se usan outras linguas/marcos. Se non pode corrixir o apagado elegante doutros xeitos, por exemplo, reescribindo o código para que a aplicación procese correctamente os sinais de terminación, pode utilizar o método descrito. Quizais non sexa o máis bonito, pero funciona.

Práctica. Probas de carga para comprobar o funcionamento do pod

A proba de carga é unha das formas de comprobar o funcionamento do contedor, xa que este procedemento achégao ás condicións reais de combate cando os usuarios visitan o sitio. Para probar as recomendacións anteriores, pode usar Yandex.Tankom: cobre todas as nosas necesidades perfectamente. A continuación móstranse consellos e recomendacións para realizar probas cun exemplo claro da nosa experiencia grazas aos gráficos de Grafana e Yandex.Tank.

O máis importante aquí é comprobar os cambios paso a paso. Despois de engadir unha nova corrección, executa a proba e mira se os resultados cambiaron en comparación coa última execución. En caso contrario, será difícil identificar solucións ineficaces e, a longo prazo, só pode facer dano (por exemplo, aumentar o tempo de implantación).

Outro matiz é mirar os rexistros do contedor durante a súa terminación. Rexistrase alí información sobre o apagado gracioso? Hai algún erro nos rexistros ao acceder a outros recursos (por exemplo, a un contedor PHP-FPM veciño)? Erros na propia aplicación (como no caso de NGINX descrito anteriormente)? Espero que a información introdutoria deste artigo che axude a comprender mellor o que acontece co contedor durante a súa terminación.

Entón, a primeira proba tivo lugar sen lifecycle e sen directivas adicionais para o servidor de aplicacións (process_control_timeout en PHP-FPM). O obxectivo desta proba era identificar o número aproximado de erros (e se os hai). Ademais, a partir da información adicional, debes saber que o tempo medio de implantación de cada pod era duns 5-10 segundos ata que estaba totalmente listo. Os resultados son:

Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM

O panel de información de Yandex.Tank mostra un aumento de erros 502, que ocorreu no momento da implantación e que durou de media ata 5 segundos. Presumiblemente, isto debeuse a que as solicitudes existentes ao antigo pod estaban a ser rematadas cando estaba a ser finalizada. Despois disto, apareceron erros 503, que foi o resultado dun contedor NGINX parado, que tamén deixou as conexións debido ao backend (o que impediu que Ingress se conectase a el).

A ver como process_control_timeout en PHP-FPM axudaranos a esperar a finalización dos procesos fillos, é dicir. corrixir tales erros. Volve implementar usando esta directiva:

Consellos e trucos de Kubernetes: funcións de apagado elegante en NGINX e PHP-FPM

Non hai máis erros durante a implantación número 500! A implantación foi exitosa, o apagado elegante funciona.

Non obstante, convén lembrar o problema dos contedores Ingress, unha pequena porcentaxe de erros nos que podemos recibir por un atraso temporal. Para evitalos, só queda engadir unha estrutura con sleep e repita o despregamento. Non obstante, no noso caso particular, non houbo cambios visibles (de novo, sen erros).

Conclusión

Para finalizar o proceso con gracia, esperamos o seguinte comportamento da aplicación:

  1. Agarde uns segundos e despois deixe de aceptar novas conexións.
  2. Agarda a que se completen todas as solicitudes e pecha todas as conexións keepalive que non estean executando solicitudes.
  3. Finaliza o teu proceso.

Non obstante, non todas as aplicacións poden funcionar deste xeito. Unha solución ao problema nas realidades de Kubernetes é:

  • engadindo un gancho pre-stop que agardará uns segundos;
  • estudando o ficheiro de configuración do noso backend para os parámetros axeitados.

O exemplo con NGINX deixa claro que incluso unha aplicación que inicialmente debería procesar correctamente os sinais de terminación pode non facelo, polo que é fundamental comprobar se hai 500 erros durante a implantación da aplicación. Isto tamén permítelle ver o problema de forma máis ampla e non centrarse nun só recipiente ou contedor, senón mirar a infraestrutura enteira no seu conxunto.

Como ferramenta de proba, podes usar Yandex.Tank xunto con calquera sistema de seguimento (no noso caso, os datos foron tomados de Grafana cun backend de Prometheus para a proba). Os problemas coa parada graciosa son claramente visibles baixo cargas pesadas que pode xerar o punto de referencia, e o seguimento axuda a analizar a situación con máis detalle durante ou despois da proba.

En resposta aos comentarios sobre o artigo: paga a pena mencionar que aquí se describen os problemas e as solucións en relación con NGINX Ingress. Para outros casos, existen outras solucións, que podemos considerar nos seguintes materiais da serie.

PS

Outros da serie de consellos e trucos K8s:

Fonte: www.habr.com

Engadir un comentario