Cómo bajamos los despliegues a cero errores 502
Cada vez que desplegábamos el backend de Mythex, algunas peticiones fallaban con 502 Bad Gateway. No muchas, pero un 502 durante un despliegue significa que el clic de alguien no hizo nada, o que una compilación que alguien observaba se quedó parada un momento. Lo rastreamos hasta dos problemas separados: un watchdog de autorecuperación que peleaba con nuestros despliegues, y un desfase entre el momento en que un servidor dejaba de escuchar y el momento en que el balanceador de carga dejaba de enviarle tráfico. En tres despliegues consecutivos de producción tras las correcciones, el recuento bajó de 10 a 6 y finalmente a 0.
La configuración
Nuestra API y el orquestador se ejecutan como contenedores en un pequeño grupo de servidores detrás de un Application Load Balancer (ALB) de AWS. Un despliegue actualiza un servidor cada vez: drena los contenedores antiguos, arranca los nuevos y pasa al siguiente. Un watchdog en cada servidor comprueba que los contenedores estén levantados y reinicia cualquiera que no lo esté; existe para que un proceso que se cae a las 3 de la madrugada vuelva sin que nadie tenga que despertarse.
Ambas cosas son razonables por separado. Juntas, no lo eran.
Problema 1: el watchdog competía con el despliegue
El watchdog se ejecutaba una vez por minuto y se «curaba» con una sola comprobación de salud fallida. Un despliegue drena deliberadamente los servicios durante hasta 150 segundos, periodo en el que una comprobación de salud debe fallar. Así que el watchdog se disparaba durante cada uno de los despliegues.
La mayoría de las veces perdía la carrera sin consecuencias. Una vez la ganó: reescribió el archivo de compose (la descripción de qué contenedores deben ejecutarse) en mitad de un despliegue, y dejó un servidor sin ningún contenedor de puerta de enlace, mientras el trabajo de despliegue informaba de éxito.
Las soluciones:
- Un solo bloqueo. El despliegue ahora toma el mismo bloqueo de archivo que el script de arranque del servidor, así que el watchdog y un despliegue nunca pueden ejecutarse a la vez.
- Cuatro avisos, no uno. El watchdog necesita cuatro comprobaciones fallidas consecutivas antes de actuar. Un servicio en drenaje ya no parece muerto.
- Una sola definición. El archivo de compose tiene ahora una única fuente, publicada en los servidores, en lugar de copias que podían desfasarse.
- Verificar, no confiar. El trabajo de despliegue ya no pasa porque un comando devolvió un código de salida correcto. Comprueba la API, el orquestador, la puerta de enlace y el worker de publicación de cada servidor antes de poder terminar con éxito.
Problema 2: el servidor se fue antes que el tráfico
El segundo problema era más discreto. Cuando un despliegue drena un servidor, este empieza a responder a su endpoint de salud con 503 para que el balanceador de carga lo saque de rotación. El nuestro hacía eso durante 5 segundos y luego se apagaba.
Pero el ALB solo comprobaba la salud cada cierto tiempo, y necesitaba varios fallos seguidos antes de dejar de enrutar a un destino: hasta unos 60 segundos de extremo a extremo. Así que durante unos 55 segundos, el balanceador seguía enviando peticiones reales a un servidor que ya había dejado de escuchar. Esos eran nuestros 502.
La solución fue hacer que las dos líneas de tiempo coincidieran:
- El grupo de destino ahora comprueba cada 5 segundos y marca un destino como no saludable tras 2 fallos: unos 10 segundos para detectarlo.
- El drenaje sigue sirviendo tráfico real durante 15 segundos después de empezar a fallar las comprobaciones de salud, así que siempre hay solapamiento.
- Estos ajustes viven en el script que aprovisiona la infraestructura, no en una consola en la que alguien hizo clic una vez.
Hacer que los números se sostengan entre sí
Tiempos como estos dependen unos de otros. El drenaje tiene que durar más que el tiempo que tarda el balanceador en darse cuenta; el drenaje más el apagado tienen que terminar antes de que el contenedor sea forzado a cerrarse. Es fácil que alguien cambie un número dentro de seis meses y reintroduzca el fallo sin darse cuenta.
Así que escribimos esas relaciones como pruebas: el drenaje debe ser más largo que el intervalo de comprobación de salud multiplicado por el umbral de fallos; el tiempo para volver a marcarse como saludable debe seguir siendo corto; y todo el drenaje debe caber dentro del periodo de gracia de parada del contenedor. Si cambias un número sin los demás, la CI falla. También sometimos a prueba de mutaciones las propias pruebas: cambiamos cada número a un valor incorrecto y comprobamos que alguna prueba lo detectaba.
Medir un despliegue es fácil de hacer mal
Nuestra primera medición decía que un despliegue limpio estaba roto en un 3 %. No lo estaba. Contábamos cada respuesta que no fuera 200, y un 503 de un endpoint de salud durante un drenaje no es un fallo: es el drenaje haciendo su trabajo, indicando al balanceador que no elija ese servidor mientras sigue atendiendo peticiones reales.
Lo que sienten los usuarios es un 502: la petición no llegó a ningún sitio. Contar solo esos dio la imagen real: 10, luego 6, luego 0 en tres despliegues de producción.
Lo que aprendimos
- La autorecuperación tiene que saber de las caídas planificadas. Un watchdog que no distingue un despliegue de un fallo acabará peleando con el despliegue.
- Drenar es un protocolo de acuerdo. El servidor tiene que seguir sirviendo hasta que el balanceador ha dejado demostrablemente de enviarle tráfico, y ese tiempo se configura en el balanceador, no en el servidor.
- Codifica los invariantes como pruebas. Los números operativos se desvían. Las pruebas que los relacionan entre sí no.
- Decide qué medir antes de medir. «Errores» era la métrica equivocada; «peticiones que no llegaron a ningún sitio» era la correcta.
Si publicas una app en Mythex, nada de esto es visible para ti, que es justo el objetivo. Relacionado: cómo tu app publicada sigue activa mientras la vuelves a publicar.