Entre junio y agosto el laboratorio se me cayó tres veces. Dos fueron apagones. La tercera fue un portátil que decidió echarse una siesta a las 4 de la mañana. Ninguna de las tres resultó ser lo que yo pensé al principio, y eso es justamente lo que vale la pena escribir.

Un poco de contexto. Mi homelab son cuatro hosts Proxmox en un mini rack de 9 pulgadas: tres mini PCs (un Beelink U59, un Beelink MINI S y un GT100 genérico) más un Lenovo ThinkPad P15v que bauticé megatron, con la tapa cerrada, haciendo de servidor. Encima corre Talos Kubernetes con siete nodes, Cilium, Flux, Kyverno, todo el combo. El storage y la UPS viven en una Synology.

26 de junio. Se va la luz. No vuelve nada.

El primer apagón fue un jueves por la noche. La luz volvió a los pocos minutos. Mis tres mini PCs no.

Mi primera teoría fue la obvia: la batería de la UPS está muerta. El lab se cayó de golpe y la BIOS no supo qué hacer.

Me equivoqué en las dos.

Revisé last -x en los hosts. Decía “shutdown”, no “crash”. Los nodos se habían apagado limpiamente. La Synology es el NUT master de la UPS, y los cuatro hosts Proxmox son NUT netclients con upsmon configurado para correr shutdown -h +0. La UPS dijo “batería baja”, la NAS le creyó y mandó a todo el mundo a dormir. Ordenado, elegante, exactamente como estaba diseñado.

Solo que la batería no estaba baja. La UPS es una APC Back-UPS RS 1500M2, batería de marzo de 2025, al 97% de carga y con unos 40 minutos de autonomía al 14% de carga. Durante un bajón corto de la red levantó la bandera de batería baja igual, y el latch FSD de NUT tumbó toda la flota con 40 minutos de autonomía sin usar. Mirando atrás en los logs, tenía cinco microcortes de unos 5 segundos en dos meses. La red eléctrica donde vivo no es estable, y la UPS se lo estaba tomando demasiado a pecho.

¿Y por qué no volvió nada cuando regresó la luz? Porque “Restore on AC Power Loss” en la BIOS nunca quedó en “Power On”. Y como NUT los había apagado limpiamente, hasta la opción “Last State” los dejaba apagados. Las VMs tenían onboot=1, lo cual era correcto e inútil si el host de abajo está a oscuras.

Lo que hice ese fin de semana:

  • Una PrometheusRule llamada UPSSpuriousLowBattery que dispara cuando la bandera de batería baja está arriba mientras upsBatteryChargeValue > 50. Esa es la firma exacta de esta falla. Llega a Mattermost, ntfy y Telegram.
  • Cambié la política de UPS en la Synology de “Until low battery” a “Customize time”, 600 segundos. Aguantar los microcortes y apagar solo si el apagón es de verdad.
  • Wake-on-LAN habilitado y persistido en los cuatro hosts.
  • “Restore on AC” en “Power On” en la BIOS de los tres mini PCs, un nodo a la vez, con su drain y su apagado como debe ser. El ThinkPad no tiene esa opción, así que depende de WoL.

Escribí el postmortem y una línea en mis notas que decía que el cluster ya se recuperaba solo de un apagón completo del rack.

10 de agosto. Se va la luz. No vuelve nada. Otra vez.

Siete de la mañana, lunes. Cloudflare empieza a devolver 530 para los doce servicios públicos. Uptime Kuma, que corre en un cluster pequeño en OCI por fuera del lab, registra el minuto exacto: 07:40.

Llegué a mirarlo como a la 13:29. La API de Kubernetes decía network is unreachable. Un barrido de la /24 mostró solo dos cosas vivas: el firewall Sophos y la Synology. Los cuatro hosts Proxmox apagados. Casi seis horas después. Igual que en junio, me tocó ir y oprimir los botones.

O sea que el arreglo de junio no sirvió de nada. Mi teoría de trabajo, todavía sin confirmar: NUT apagó los hosts a S5, pero la UPS nunca cortó la salida, porque la luz volvió antes de que se agotara la batería. El “Restore on AC” de la BIOS solo dispara si ve que la corriente se va y regresa. Desde el punto de vista de la board, la corriente nunca se fue. Le dijeron que se apagara, y apagada se quedó.

WoL tampoco me salvó. Los magic packets desde mi Mac en la VLAN20 no cruzaron el Sophos, y los dos Beelink no tienen opción de recibir WoL en la BIOS, punto.

Y luego se puso peor. Los hosts subieron, los siete nodes pasaron a Ready, Cilium empezó a reconstruirse, y el cluster igual no se recuperaba solo. Los pods se morían y no se creaban reemplazos en ninguna parte. FailedCreate en un montón de namespaces.

El culpable era Kyverno. El admission controller no podía bajar su imagen, eso dejó al webhook validate.kyverno.svc-fail sin backend, y como está configurado fail-closed, el API server estaba rechazando absolutamente toda creación de pods en el cluster. Yo tenía una nota de junio sobre esto, como algo que podía morder durante un drain de un solo nodo. En un arranque en frío es otro animal: bloquea toda la recuperación. Cuando Kyverno por fin arrancó, los pods pasaron de 235 a 313 en pocos minutos y el resto se sanó solo.

También perseguí una pista falsa un buen rato. Los pulls de imágenes a docker.io y registry.k8s.io fallaban por timeout en el handshake TLS, y yo estaba seguro de que el bloqueo de CDN del Sophos se los estaba comiendo. Hice curl a los mismos registries desde el Mac y desde un pod dentro del cluster. 200 rápidos en los dos casos. Era pura congestión de todo el mundo bajando imágenes al tiempo, y se despejó con el backoff.

El orden de recuperación que funcionó, y que ahora tengo de runbook: Cilium, después CoreDNS, después el admission de Kyverno, después unos 50 workloads se sanan solos, y al final forzar un reconcile de Flux para lo que quede trabado. A las 14:30, 13 de 13 servicios arriba.

12 de agosto. Megatron se queda dormido.

Dos días después, 04:37:53. Megatron se muere solo. Sin carga, sin evento eléctrico, sin nada. Se quedó abajo 4 horas y 11 minutos hasta que lo prendí a mano.

Mi primer instinto fue buscar logs. No había. El journal se cortaba a mitad de frase. El SMART del NVMe estaba limpio, cero errores de medio.

Me costó aceptarlo: la ausencia de logs era la evidencia. El NVMe de arranque tenía APST habilitado con un timeout de inactividad de 100 ms. Entró en un estado de bajo consumo profundo y nunca despertó. La raíz vive en ese disco, así que no había dónde escribir el panic. El SMART estaba limpio porque el controlador estaba dormido y nunca vio un error de I/O. Un reporte SMART sano no absuelve al disco.

La otra pista era lo que había en pantalla cuando llegué: el menú de arranque PXE. PXE es lo último en el orden de boot, después de todos los discos. Caer ahí significa que la BIOS no vio el NVMe en el POST. Debí preguntar “qué había en la pantalla” en los primeros cinco minutos.

APST: Enabled — Idle Time Prior to Transition: 100 ms → Power State 3
PCIe LnkCtl: ASPM L1 Enabled

El arreglo: nvme_core.default_ps_max_latency_us=0 en el cmdline del kernel.

Y nunca fue un problema de megatron. patroxmus2 y patroxmus3 arrancan desde NVMe y tenían exactamente la misma configuración. Tres de mis cuatro hosts estaban a una noche tranquila de la misma falla. Los tres ya quedaron arreglados. El cuarto arranca desde SATA.

Lo que la investigación sacó a la luz

Jalar ese hilo destapó cosas que no tenían nada que ver con la luz.

El 81% de los CPU requests del cluster eran ficción. 19.557m pedidos contra un p95 real de 3.688m. Eso fue lo que convirtió la muerte de un solo nodo en cuatro horas de aplicaciones caídas: nada podía reprogramarse porque el scheduler creía que los otros nodes estaban llenos. Ajustar los requests liberó unos 6,3 cores. Un node pasó de 100% de CPU pedido a 52%.

Cuatro reglas de nodeAffinity no hacían nada. Referenciaban megatron y megatron.local, pero los nodes de Kubernetes se llaman talos-*, así que el NotIn coincidía con los siete nodes y no excluía ninguno.

La automatización de reinicio de nodos corrió cada 5 minutos durante todo el apagón y no pudo ayudar, porque el hipervisor mismo estaba abajo. Registraba un genérico “all restart paths failed” y nunca dijo lo que debía decir: que un humano tiene que ir a oprimir un botón. Ahora manda una alerta distinta: PHYSICAL INTERVENTION REQUIRED.

Agregar telemetría de temperatura de los hosts se pagó solo en 20 minutos. patroxmus2, un Intel N100, estaba a 97 °C con 71 eventos de throttling por segundo y un load average de 1,40 en 4 cores. No era un pico, era un estado permanente. La causa raíz la encontré a ojo, no con sensores: los mini PCs estaban apilados uno encima del otro, cada uno respirando el aire caliente del de abajo. Aire comprimido, los separé, un ventilador USB dentro del rack. De 97 °C a 83-89 °C y cero throttling.

Y una trampa en la que casi caigo: patroxmus3, un AMD Ryzen 5 3550H, estaba a 94 °C y perfectamente sano, haciendo boost a 2802 MHz sobre una base de 2,1 GHz. Los chips móviles de AMD están diseñados para correr en su techo térmico. El diagnóstico no es la temperatura. Es si el chip sigue haciendo boost por encima del clock base. Una alerta plana por encima de 90 °C habría gritado lobo en esa caja para siempre. Las alertas ahora miden el margen contra el crit que reporta cada sensor.

La última. Esa misma noche, tarde, n8n devolvía 503 “Database is not ready” mientras todos los chequeos de salud que yo había corrido decían que el lab estaba bien. El pod estaba Running. También estaba 0/1 Ready, durante 78 minutos. Phase responde “¿el kubelet está corriendo este contenedor?”. Readiness responde “¿puede servir?”. Solo la segunda se correlaciona con un usuario viendo un error. Ahora hay una alerta PodRunningButNotReady a los 15 minutos, un umbral que medí reproduciendo la expresión sobre la ventana de mantenimiento, no uno que adiviné.

Queda abierto: confirmar en los logs de la Synology si la UPS de verdad mantiene la salida durante los cortes cortos, y conseguir un smart plug o una PDU conmutada para los dos Beelink que no saben de WoL. Mientras tanto, la verdad honesta es que mi lab sobrevive un apagón solo si yo estoy en la casa.