La madrugada
De madrugada, el pool de almacenamiento ZFS de uno de los hipervisores Proxmox llegó al 99,8% de uso y se llevó consigo más de 14 nodos del clúster Kubernetes de producción. El incidente duró unas 14 horas, hasta que los últimos nodos nuevos, creados en otro hipervisor, estuvieron disponibles.
La explicación obvia era que los datos habían crecido. Era correcta, pero incompleta, y un post-mortem que se queda en la explicación obvia no evita la próxima caída.
El pool estaba prometido cuatro veces
Empecé por la API de Proxmox, en modo solo lectura, relevando los nodos, el almacenamiento y los discos de cada VM. Había 62 TB aprovisionados en un pool físico de 14 TB, un overcommit de 4,3 a 1. Solo las cuatro VMs de base de datos tenían 6,5 TB cada una, en thin provisioning. Y el hipervisor no tenía backup, ni replicación, ni monitoreo de capacidad.
Reconstruí la línea de tiempo de la caída y la recuperación a partir de los eventos de transición de los propios nodos de Kubernetes, y la crucé con los otros cuatro incidentes del día. Una alerta de disponibilidad de la API era legítima, porque la API realmente estaba degradada. Otra podía estar ligada a un servicio que corría en un nodo que se cayó. Una tercera no tenía ninguna relación: era un problema de permisos.
Bloques zombis
Los datos reales no explicaban la velocidad con que se llenó el pool, así que fui más a fondo. El TRIM estaba apagado en los 47 volúmenes del almacenamiento distribuido de Kubernetes, Longhorn. MongoDB libera bloques en cada checkpoint y avisa al disco, pero Longhorn ignoraba el aviso, y ZFS nunca recuperaba el espacio.
Eso venía acumulándose desde hacía más de 500 días. En cada réplica de MongoDB, el sistema de archivos mostraba unos 437 GB, mientras el volumen ocupaba 1,1 TB. Solo en MongoDB había unos 4,8 TB de espacio muerto, multiplicados por las tres réplicas de Longhorn. Varios volúmenes ya estaban por encima del tamaño aprovisionado, hasta el 135%. Y MongoDB no tenía TTL: los campos de fecha se guardaban como números enteros, así que el índice TTL nativo nunca funcionaría sin cambiar la aplicación.
Un error mío, corregido en voz alta
En medio del incidente, mirando una de las alertas del día, mi primera lectura fue que la regla estaba mal configurada. Cuando abrí la regla completa, vi que era correcta y que la API realmente estaba caída. Corregí el registro en el momento. El problema real era otro: la página de estado pública decía "todos los sistemas operativos" mientras la API respondía 503.
El post-mortem
La causa raíz quedó registrada como la combinación de tres decisiones: overcommit de 4,3 a 1, TRIM apagado con tres réplicas por volumen y datos sin expiración. Identifiqué nueve fallas de proceso, cinco de ellas con prioridad P0 o P1, como la falta de alerta de capacidad en el hipervisor y la ausencia de backup y replicación.
La corrección de mayor impacto no dependía del equipo de desarrollo: encender el TRIM en los volúmenes y correr fstrim, con una estimación de 3 a 5 TB recuperados, además de corregir los volúmenes por encima del límite y crear la alerta de capacidad de ZFS.
Qué cambió
La causa raíz fue más allá de "los datos crecieron" y llegó al mecanismo que hacía desaparecer el espacio. El plan de acción quedó priorizado, con la corrección más importante al alcance de la propia infraestructura, y las nueve fallas de proceso quedaron registradas para que no se repitan.
Lo que me llevo
La explicación obvia rara vez es la causa raíz completa. "El disco se llenó porque los datos crecieron" era cierto, pero 4,8 TB de espacio muerto no eran datos. Vinieron de tres decisiones razonables por separado, que juntas formaban una bomba de tiempo.
Y en medio de un incidente, corregir en voz alta una lectura equivocada vale más que defenderla.