El archivo de intercambio que faltaba y que congeló 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 intercambio (swap) y con contenedores autorizados a usar más memoria de la que tenía la máquina, Linux pasaba el tiempo desalojando y volviendo a leer los archivos que necesitaba para funcionar, y apenas avanzaba. Añadir un archivo de swap de 2 GB en cada servidor lo arregló. La lección más interesante fue sobre los health checks.
Cómo se veía el «bloqueo»
- Los health checks del balanceador de carga agotaban el tiempo con ese servidor.
- Los comandos enviados a través de AWS Systems Manager se quedaban en «Pending» y nunca se ejecutaban.
- No había ningún corte por falta de memoria en los registros. Ningún proceso había muerto.
Una semana 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 bloqueado de la misma forma mientras descomprimía una imagen de contenedor durante un despliegue.
¿Por qué no hubo un error de memoria?
Linux solo mata un proceso cuando de verdad se queda 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 desalojando código de programas 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, el thrashing, puede dejar una máquina casi completamente sin respuesta sin que técnicamente se quede 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á reservada pero se usa poco, así que tiene que quitársela a 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 poco usadas pueden pasar al disco en lugar de forzar la salida del código de los programas de la caché. Lo aplicamos en directo a los dos servidores de producción y al de staging, y el pipeline de despliegue vuelve a publicar el script de arranque en cada despliegue de producción, de modo que un servidor sustituido también lo reciba.
Lo que más nos preocupó: el servidor no saludable seguía dentro
Nuestro grupo de servidores usaba health checks de EC2 («¿la máquina está encendida?») en lugar de health checks del balanceador de carga («¿está respondiendo peticiones?»). Así que un servidor al que el balanceador había dado por perdido seguía siendo saludable para el grupo. Nunca sería reemplazado. Producción funcionaría en silencio con un solo servidor hasta que alguien lo notara y reiniciara el otro.
Cambiar el grupo a health checks del balanceador 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 drene (consulta cómo conseguimos despliegues sin 502). Un grupo que reemplaza servidores con esa señal los sustituiría en mitad de cada despliegue.
Por ahora hemos añadido un paso a nuestra lista de comprobación de despliegue: después de cada despliegue, comprobar el estado de cada destino en el balanceador de carga, no solo si el trabajo de despliegue ha terminado. Si los comandos a un servidor se quedan en «Pending», está en thrashing: reinícialo; ya está fuera de rotación.
Lo que sacamos de todo esto
- «No se cae» no es «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, con un swappiness bajo, le 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 la pierdes. Sabe cuándo la has hecho.
- Conoce en qué health check confía tu autoescalado. Un health check que solo pregunta «¿la VM está encendida?» no reemplazará un servidor que ha dejado de funcionar.