Cómo conseguimos despliegues con cero errores 502
Cada vez que desplegábamos el backend de Mythex, unas cuantas 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 estaba mirando se quedó parada un momento. Lo rastreamos hasta dos problemas separados: un watchdog de autorecuperación que se peleaba con nuestros despliegues, y un desfase entre el momento en que un servidor deja de escuchar y el momento en que el balanceador de carga deja de enviarle tráfico. En tres despliegues de producción consecutivos después de las correcciones, el recuento pasó de 10 a 6 y después 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 lo que no lo esté; existe para que un proceso caído 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 «reparaba» con un health check fallido. Un despliegue drena servicios a propósito durante hasta 150 segundos, y durante ese tiempo un health check debe fallar. Así que el watchdog se activaba durante cada despliegue.
La mayoría de las veces perdía la carrera sin consecuencias. Una vez, la ganó: reescribió el archivo compose (la descripción de qué contenedores deben ejecutarse) en mitad de un despliegue, y dejó un servidor sin ningún contenedor de gateway, 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 intentos, 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 compose tiene ahora una única fuente, publicada en los servidores, en lugar de copias que podían desviarse.
- Verificar, no confiar. El trabajo de despliegue ya no se da por bueno porque un comando devolvió un resultado. Comprueba la API, el orquestador, el gateway 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 lo saque de la 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 varias comprobaciones fallidas seguidas 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 los health checks, así que siempre hay solapamiento.
- Estos ajustes viven en el script que aprovisiona la infraestructura, no en una consola que alguien tocó una vez.
Hacer que los números se respalden entre sí
Tiempos como estos dependen unos de otros. El drenaje tiene que durar más que lo que tarda el balanceador en detectar el fallo; el drenaje más el apagado tienen que terminar antes de que el contenedor sea forzado a detenerse. Es fácil que alguien cambie un número dentro de seis meses y reintroduzca el error sin darse cuenta.
Así que escribimos esas relaciones como pruebas: el drenaje debe ser más largo que el intervalo del health check multiplicado por el umbral de fallos; el tiempo para volver a marcarse como saludable debe seguir siendo corto; 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 probamos las propias pruebas con mutaciones: cambiamos cada número a un valor incorrecto y comprobamos que una prueba lo detectaba.
Medir un despliegue es fácil de hacer mal
Nuestra primera medición decía que un despliegue limpio tenía alrededor de un 3 % de fallos. No era así. Contábamos cada respuesta que no era 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 sirviendo peticiones reales.
Lo que sienten los usuarios es un 502: la petición no llegó a ninguna parte. Contar solo esos dio la imagen real: 10, luego 6, luego 0 en tres despliegues de producción.
Lo que sacamos de todo esto
- La autorrecuperación tiene que saber de los apagados planificados. Un watchdog que no distingue un despliegue de una caída acabará peleándose con el despliegue.
- El drenaje es un acuerdo entre dos partes. El servidor tiene que seguir sirviendo hasta que el balanceador haya 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é estás midiendo antes de medir. «Errores» era la métrica equivocada; «peticiones que no llegaron a ninguna parte» era la correcta.
Si publicas una app en Mythex, nada de esto es visible para ti, que es precisamente el objetivo. Relacionado: cómo tu app publicada sigue activa mientras la republicas.