El archivo de intercambio que faltaba y dejó congelados nuestros servidores
El 24 de septiembre, uno de nuestros servidores de producción dejó de responder. El balanceador de carga lo marcó como no saludable y nuestra herramienta de comandos remotos no podía alcanzarlo, pero nada se había caído y nada se había matado por usar demasiada memoria. El servidor estaba en thrashing: sin espacio de swap y con contenedores autorizados a usar más memoria de la que tenía la máquina, Linux dedicaba su tiempo a expulsar y volver a leer los archivos que necesitaba para ejecutarse, y apenas avanzaba. Añadir un archivo de swap de 2 GB en cada servidor lo solucionó. La lección más interesante fue sobre las comprobaciones de salud.
Cómo se veía el «colgado»
- Las comprobaciones de salud del balanceador de carga agotaban el tiempo en ese servidor.
- Los comandos enviados a través de AWS Systems Manager se quedaban en «Pending» y nunca se ejecutaban.
- No había ninguna muerte por falta de memoria en los registros. Ningún proceso murió.
Unas semanas antes habíamos trasladado producción a servidores ARM más pequeños con 2 GB de RAM cada uno. Nuestro servidor de staging, aún más pequeño, se había congelado de la misma forma mientras descomprimía una imagen de contenedor durante un despliegue.
¿Por qué no había error de memoria?
Linux solo mata un proceso cuando se queda realmente sin memoria que pueda recuperar. Antes de llegar a ese punto, recupera memoria de la caché de páginas: las copias de archivos que mantiene en RAM, incluido el código de cada programa en ejecución.
En nuestros servidores, los límites de memoria combinados de los contenedores sumaban unos 3,3 GB en una máquina con unos 1,8 GB utilizables. Normalmente no todos usan su límite a la vez. Cuando lo hacían, el kernel seguía expulsando código de programa de la caché para hacer sitio, y luego tenía que volver a leerlo del disco en cuanto ese código se ejecutaba de nuevo. Ese ciclo (thrashing) puede dejar una máquina casi totalmente no responsiva sin quedarse técnicamente sin memoria. Así que no se mata nada, y nada se recupera.
Sin swap, el kernel no tiene dónde poner la memoria que está asignada pero rara vez se usa, así que tiene que quitarla de la caché.
La solución
El script de arranque de los servidores ahora crea un archivo de swap de 2 GB con un valor bajo de swappiness (10) cada vez que un servidor arranca o se repara. Un swappiness bajo significa que el kernel sigue prefiriendo mantener la memoria de trabajo en RAM, pero las páginas que casi no se tocan pueden pasar al disco en lugar de forzar la salida del código de los programas de la caché. Lo aplicamos en vivo a los dos servidores de producción y a staging, y la canalización de despliegue vuelve a publicar el script de arranque en cada despliegue de producción, para que un servidor sustituido también lo reciba.
Lo que más nos preocupó: el servidor no saludable se quedó
Nuestro grupo de servidores usaba comprobaciones de salud de EC2, es decir, «¿la máquina está encendida?», y no comprobaciones de salud del balanceador de carga, es decir, «¿está respondiendo peticiones?». Así que un servidor al que el balanceador ya había dado por perdido seguía siendo saludable para el grupo. Nunca se sustituiría. Producción funcionaría silenciosamente en un solo servidor hasta que alguien lo notara y reiniciara el otro.
Cambiar el grupo a comprobaciones del balanceador de carga parece la solución obvia, pero no es sencillo para nosotros: durante un despliegue, los servidores responden a propósito con un error en su endpoint de salud para que el balanceador los saque de servicio (consulta cómo llegamos a cero 502 en los despliegues). Un grupo que sustituye servidores según esa señal los sustituiría en mitad de cada despliegue.
Así que, por ahora, añadimos un paso a nuestra lista de comprobación de despliegue: después de cada despliegue, comprobar la salud de cada destino en el balanceador de carga, no solo si el trabajo de despliegue terminó bien. Si los comandos a un servidor se quedan en «Pending», está en thrashing: reinícialo; ya está fuera de rotación.
Lo que aprendimos
- «No se cae» no es lo mismo que «sano». El thrashing no produce ningún error, solo una lentitud tan extrema que la máquina deja de responder.
- Los servidores pequeños necesitan swap. Aunque sea poco, y con un swappiness bajo, da al kernel dónde poner la memoria fría.
- Los límites de memoria que suman más que la máquina son una apuesta. A veces se pierde. Sabe cuándo la has hecho.
- Conoce en qué comprobación de salud confía tu autoescalado. Una comprobación que solo pregunta «¿la VM está encendida?» no sustituirá a un servidor que ha dejado de funcionar.