Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM

Una condición típica al implementar CI/CD en Kubernetes: la aplicación debe poder no aceptar nuevas solicitudes de clientes antes de detenerse por completo y, lo más importante, completar con éxito las existentes.

Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM

El cumplimiento de esta condición le permite lograr cero tiempo de inactividad durante la implementación. Sin embargo, incluso cuando se utilizan paquetes muy populares (como NGINX y PHP-FPM), se pueden encontrar dificultades que provocarán una oleada de errores con cada implementación...

Teoría. como vive la vaina

Ya hemos publicado en detalle sobre el ciclo de vida de una vaina. este articulo. En el contexto del tema que nos ocupa, nos interesa lo siguiente: en el momento en que la vaina ingresa al estado Terminación, se dejan de enviar nuevas solicitudes (pod remoto de la lista de puntos finales del servicio). Así, para evitar tiempos de inactividad durante el despliegue, basta con que solucionemos el problema de detener la aplicación correctamente.

También debe recordar que el período de gracia predeterminado es 30 segundos: después de esto, el pod finalizará y la aplicación debe tener tiempo para procesar todas las solicitudes antes de este período. Nota: aunque cualquier solicitud que demore más de 5 a 10 segundos ya es problemática, y el cierre elegante ya no ayudará...

Para comprender mejor qué sucede cuando termina un pod, simplemente mire el siguiente diagrama:

Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM

A1, B1 - Recibir cambios sobre el estado del hogar.
A2 - Salida SIGTERM
B2: eliminar un pod de los puntos finales
B3: recepción de cambios (la lista de puntos finales ha cambiado)
B4 - Actualizar reglas de iptables

Tenga en cuenta: la eliminación del pod de endpoints y el envío de SIGTERM no se realizan de forma secuencial, sino en paralelo. Y debido al hecho de que Ingress no recibe inmediatamente la lista actualizada de Endpoints, se enviarán nuevas solicitudes de los clientes al pod, lo que provocará un error 500 durante la terminación del pod. (para obtener material más detallado sobre este tema, traducido). Este problema debe resolverse de las siguientes maneras:

  • Enviar conexión: cerrar en los encabezados de respuesta (si se trata de una aplicación HTTP).
  • Si no es posible realizar cambios en el código, el siguiente artículo describe una solución que le permitirá procesar solicitudes hasta el final del período de gracia.

Teoría. Cómo NGINX y PHP-FPM terminan sus procesos

Nginx

Empecemos por NGINX, ya que con él todo es más o menos obvio. Al profundizar en la teoría, aprendemos que NGINX tiene un proceso maestro y varios "trabajadores": estos son procesos secundarios que procesan las solicitudes de los clientes. Se proporciona una opción conveniente: usar el comando nginx -s <SIGNAL> finalizar procesos ya sea en modo de apagado rápido o apagado elegante. Evidentemente es esta última opción la que nos interesa.

Entonces todo es simple: necesitas agregar preStop-gancho un comando que enviará una señal de apagado elegante. Esto se puede hacer en Despliegue, en el bloque contenedor:

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

Ahora, cuando el pod se cierre, veremos lo siguiente en los registros del contenedor 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

Y esto significará lo que necesitamos: NGINX espera a que se completen las solicitudes y luego finaliza el proceso. Sin embargo, a continuación también consideraremos un problema común debido al cual, incluso con el comando nginx -s quit el proceso termina incorrectamente.

Y en esta etapa hemos terminado con NGINX: al menos a partir de los registros se puede entender que todo funciona como debería.

¿Cuál es el problema con PHP-FPM? ¿Cómo maneja el apagado elegante? Vamos a resolverlo.

PHP-FPM

En el caso de PHP-FPM, hay un poco menos de información. Si te concentras en manual oficial según PHP-FPM, dirá que se aceptan las siguientes señales POSIX:

  1. SIGINT, SIGTERM — apagado rápido;
  2. SIGQUIT — cierre elegante (lo que necesitamos).

El resto de señales no son necesarias en esta tarea, por lo que omitiremos su análisis. Para finalizar el proceso correctamente, deberá escribir el siguiente gancho preStop:

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

A primera vista, esto es todo lo que se necesita para realizar un cierre correcto en ambos contenedores. Sin embargo, la tarea es más difícil de lo que parece. A continuación se presentan dos casos en los que el cierre ordenado no funcionó y provocó una indisponibilidad a corto plazo del proyecto durante la implementación.

Práctica. Posibles problemas con el cierre elegante

Nginx

En primer lugar, conviene recordar: además de ejecutar el comando nginx -s quit Hay una etapa más a la que vale la pena prestar atención. Encontramos un problema por el cual NGINX aún enviaba SIGTERM en lugar de la señal SIGQUIT, lo que provocaba que las solicitudes no se completaran correctamente. Casos similares se pueden encontrar, por ejemplo, aquí. Desafortunadamente, no pudimos determinar el motivo específico de este comportamiento: había una sospecha sobre la versión NGINX, pero no se confirmó. El síntoma fue que se observaron mensajes en los registros del contenedor NGINX: "Abra el enchufe n.º 10 que queda en la conexión 5", después de lo cual la cápsula se detuvo.

Podemos observar tal problema, por ejemplo, a partir de las respuestas al Ingress que necesitamos:

Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM
Indicadores de códigos de estado en el momento del despliegue.

En este caso, recibimos solo un código de error 503 del propio Ingress: no puede acceder al contenedor NGINX porque ya no es accesible. Si observa los registros del contenedor con NGINX, contienen lo siguiente:

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

Después de cambiar la señal de parada, el contenedor comienza a detenerse correctamente: esto se confirma por el hecho de que ya no se observa el error 503.

Si encuentra un problema similar, tiene sentido averiguar qué señal de parada se utiliza en el contenedor y cómo se ve exactamente el gancho preStop. Es muy posible que la razón resida precisamente en esto.

PHP-FPM... y más

El problema con PHP-FPM se describe de forma trivial: no espera a que se completen los procesos secundarios, sino que los finaliza, razón por la cual ocurren errores 502 durante la implementación y otras operaciones. Hay varios informes de errores en bugs.php.net desde 2005 (p. ej. aquí и aquí), que describe este problema. Pero lo más probable es que no vea nada en los registros: PHP-FPM anunciará la finalización de su proceso sin errores ni notificaciones de terceros.

Vale aclarar que el problema en sí puede depender en mayor o menor medida de la propia aplicación y puede no manifestarse, por ejemplo, en el seguimiento. Si lo encuentra, primero le viene a la mente una solución simple: agregue un gancho preStop con sleep(30). Le permitirá completar todas las solicitudes anteriores (y no aceptamos nuevas, ya que pod ya capaz de Terminación), y después de 30 segundos el módulo terminará con una señal SIGTERM.

Resulta que lifecycle para el contenedor se verá así:

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

Sin embargo, debido a los 30 segundos sleep nosotros fuertemente Aumentaremos el tiempo de implementación, ya que cada pod será terminado. mínimo 30 segundos, lo cual es malo. ¿Qué se puede hacer con esto?

Pasemos al responsable de la ejecución directa de la solicitud. En nuestro caso es PHP-FPMQue por defecto no monitorea la ejecución de sus procesos secundarios: El proceso maestro finaliza inmediatamente. Puedes cambiar este comportamiento usando la directiva. process_control_timeout, que especifica los límites de tiempo para que los procesos secundarios esperen señales del maestro. Si establece el valor en 20 segundos, esto cubrirá la mayoría de las consultas que se ejecutan en el contenedor y detendrá el proceso maestro una vez que se completen.

Con este conocimiento, volvamos a nuestro último problema. Como se mencionó, Kubernetes no es una plataforma monolítica: la comunicación entre sus diferentes componentes lleva algún tiempo. Esto es especialmente cierto cuando consideramos el funcionamiento de Ingresses y otros componentes relacionados, ya que debido a tal retraso en el momento de la implementación, es fácil obtener un aumento de 500 errores. Por ejemplo, puede ocurrir un error en la etapa de envío de una solicitud a un canal ascendente, pero el "retraso" de la interacción entre los componentes es bastante corto: menos de un segundo.

Por lo tanto, juntos con la directiva ya mencionada process_control_timeout puedes usar la siguiente construcción para lifecycle:

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

En este caso compensaremos el retraso con el comando sleep y no aumentes mucho el tiempo de despliegue: ¿hay una diferencia notable entre 30 segundos y uno? process_control_timeoutY lifecycle Se utiliza únicamente como “red de seguridad” en caso de retraso.

En general, El comportamiento descrito y la solución correspondiente se aplican no sólo a PHP-FPM.. Una situación similar puede surgir de una forma u otra al utilizar otros lenguajes/marcos. Si no puede solucionar el cierre elegante de otras formas (por ejemplo, reescribiendo el código para que la aplicación procese correctamente las señales de terminación), puede utilizar el método descrito. Puede que no sea el más bonito, pero funciona.

Práctica. Pruebas de carga para comprobar el funcionamiento del pod.

Las pruebas de carga son una de las formas de comprobar cómo funciona el contenedor, ya que este procedimiento lo acerca a las condiciones reales de combate cuando los usuarios visitan el sitio. Para probar las recomendaciones anteriores, puede utilizar Yandex.Tankom: Cubre todas nuestras necesidades a la perfección. A continuación se ofrecen consejos y recomendaciones para realizar pruebas con un claro ejemplo de nuestra experiencia gracias a los gráficos de Grafana y el propio Yandex.Tank.

Lo más importante aquí es comprobar los cambios paso a paso. Después de agregar una nueva solución, ejecute la prueba y vea si los resultados han cambiado en comparación con la última ejecución. De lo contrario, será difícil identificar soluciones ineficaces y, a largo plazo, sólo puede causar daño (por ejemplo, aumentar el tiempo de implementación).

Otro matiz es mirar los registros del contenedor durante su terminación. ¿Se registra allí información sobre el cierre ordenado? ¿Hay algún error en los registros al acceder a otros recursos (por ejemplo, a un contenedor PHP-FPM vecino)? ¿Errores en la propia aplicación (como en el caso de NGINX descrito anteriormente)? Espero que la información introductoria de este artículo le ayude a comprender mejor qué le sucede al contenedor durante su terminación.

Así, la primera prueba se realizó sin lifecycle y sin directivas adicionales para el servidor de aplicaciones (process_control_timeout en PHP-FPM). El propósito de esta prueba fue identificar el número aproximado de errores (y si existen). Además, a partir de información adicional, debe saber que el tiempo promedio de implementación de cada pod fue de aproximadamente 5 a 10 segundos hasta que estuvo completamente listo. Los resultados son:

Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM

El panel de información de Yandex.Tank muestra un pico de errores 502, que ocurrieron en el momento de la implementación y duraron en promedio hasta 5 segundos. Presumiblemente, esto se debió a que las solicitudes existentes al pod anterior se estaban cancelando cuando se canceló. Después de esto, aparecieron errores 503, que fue el resultado de un contenedor NGINX detenido, que también cortó las conexiones debido al backend (lo que impidió que Ingress se conectara a él).

veamos como process_control_timeout en PHP-FPM nos ayudará a esperar a que se completen los procesos secundarios, es decir corregir tales errores. Vuelva a implementar usando esta directiva:

Consejos y trucos de Kubernetes: características de cierre elegante en NGINX y PHP-FPM

¡No hay más errores durante la implementación número 500! La implementación es exitosa, el cierre elegante funciona.

Sin embargo, conviene recordar el problema de los contenedores de Ingress, un pequeño porcentaje de errores en los que podemos recibir debido a un desfase de tiempo. Para evitarlos sólo queda añadir una estructura con sleep y repetir el despliegue. Sin embargo, en nuestro caso particular, no se observaron cambios (nuevamente, no hubo errores).

Conclusión

Para finalizar el proceso correctamente, esperamos el siguiente comportamiento de la aplicación:

  1. Espere unos segundos y luego deje de aceptar nuevas conexiones.
  2. Espere a que se completen todas las solicitudes y cierre todas las conexiones keepalive que no estén ejecutando solicitudes.
  3. Finaliza tu proceso.

Sin embargo, no todas las aplicaciones pueden funcionar de esta manera. Una solución al problema en las realidades de Kubernetes es:

  • añadiendo un gancho de parada previa que esperará unos segundos;
  • estudiando el archivo de configuración de nuestro backend para los parámetros apropiados.

El ejemplo con NGINX deja claro que incluso una aplicación que inicialmente debería procesar correctamente las señales de terminación puede no hacerlo, por lo que es fundamental comprobar si hay errores 500 durante la implementación de la aplicación. Esto también le permite ver el problema de manera más amplia y no centrarse en un solo módulo o contenedor, sino en toda la infraestructura como un todo.

Como herramienta de prueba, puede utilizar Yandex.Tank junto con cualquier sistema de monitoreo (en nuestro caso, los datos se tomaron de Grafana con un backend de Prometheus para la prueba). Los problemas con el apagado suave son claramente visibles bajo cargas pesadas que puede generar el punto de referencia, y el monitoreo ayuda a analizar la situación con más detalle durante o después de la prueba.

En respuesta a los comentarios sobre el artículo: vale la pena mencionar que los problemas y las soluciones se describen aquí en relación con NGINX Ingress. Para otros casos, existen otras soluciones, que podemos considerar en los siguientes materiales de la serie.

PS

Otros de la serie de consejos y trucos del K8:

Fuente: habr.com

Añadir un comentario