Ubuntu 24.04: DKMS falló tras actualizar el kernel — recuperar controladores sin tiempo de inactividad

Parcheas una flota. El kernel sube de versión. Unos minutos después tu monitor se enciende: GPUs desaparecidas, pools ZFS protestando, offloads de NIC perdidos, quizá una tela de InfiniBand inestable. Los servicios siguen activos… por ahora. Entonces lo ves: DKMS no compiló los módulos para el kernel nuevo, así que el próximo reinicio es una trampa.

Esta es la realidad de correr Ubuntu 24.04 en producción. Las actualizaciones del kernel son rutinarias; los fallos de DKMS son el precio de usar controladores fuera del kernel mainline. El objetivo aquí no es “arreglarlo después de que se rompa”. El objetivo es: arreglarlo mientras el sistema sigue en línea y hacer que la próxima actualización del kernel sea aburrida.

Cómo DKMS realmente falla tras una actualización del kernel

DKMS (Dynamic Kernel Module Support) existe porque los proveedores siguen entregando módulos de kernel que no están en el kernel mainline. NVIDIA, ZFS-on-Linux, algunos controladores NIC y RAID, VirtualBox, algunos agentes de seguridad—cualquier cosa que compile contra los headers del kernel es candidata.

Cuando Ubuntu instala un kernel nuevo, los scripts del paquete intentan recompilar los módulos DKMS para ese kernel. Si esa recompilación falla, puede que no lo notes de inmediato porque el kernel que se está ejecutando todavía tiene módulos cargados y funcionales. La rotura aparece cuando:

  • Reinicias y el kernel nuevo arranca sin el módulo.
  • initramfs se generó sin el módulo, provocando fallos en el arranque temprano (almacenamiento, root-on-ZFS, cifrado, etc.).
  • Secure Boot bloquea el módulo sin firmar, y obtienes el caso “compilado pero no cargable”.
  • Los headers del kernel nuevo no estaban instalados, así que DKMS no tuvo nada contra qué compilar.

La mayoría de los incidentes “DKMS roto” son uno de estos cuatro. La solución rara vez es misteriosa; suele ser laboriosa y operacionalmente aterradora. El truco es reducir el riesgo: diagnosticar con precisión, compilar para el kernel al que vas a arrancar, validar la capacidad de carga y solo entonces permitir reinicios.

Verdad seca: DKMS no es “dinámico” en el sentido que la gerencia imagina. Es “dinámico” como lo es un formulario en papel: puedes volver a rellenarlo cada vez que cambia el kernel.

Guion de diagnóstico rápido

Cuando intentas evitar tiempo de inactividad, la velocidad importa. El camino más rápido es: identificar el kernel objetivo, confirmar si el módulo existe para él, confirmar si puede cargarse y luego validar los artefactos de arranque (initramfs). Todo lo demás es guarnición.

Primero: ¿qué kernel estás ejecutando y qué kernels están instalados?

  • Si todavía ejecutas el kernel antiguo, puedes reconstruir con calma antes de reiniciar.
  • Si ya estás en el kernel nuevo y faltan módulos, necesitas restaurar la funcionalidad en el kernel en vivo (a veces posible, a veces no).

Segundo: ¿DKMS muestra “built” para el kernel objetivo?

  • Si no está compilado: estás en modo “reconstruir y arreglar dependencias de construcción”.
  • Si está compilado: comprueba si se instaló en /lib/modules/<kernel> y si modprobe tiene éxito.

Tercero: ¿Secure Boot está bloqueando el módulo?

  • Secure Boot activado + módulo sin firmar = compilará bien y luego fallará al cargar con errores de firma.
  • Este es el bucle número uno de “lo recompilé tres veces y nada cambió”.

Cuarto: ¿incluye initramfs lo que necesitas?

  • Si el módulo es necesario para el arranque temprano (almacenamiento/root en ZFS, criptografía), “compilado” no basta.
  • Regenera initramfs para el kernel objetivo y verifica que contenga el módulo.

Quinto: bloquea cambios riesgosos mientras lo arreglas

  • Retén paquetes del kernel si unattended upgrades sigue descargando nuevos kernels mientras estás en mitad de la recuperación.
  • Fija un kernel conocido bueno como opción de reversión.

Datos y contexto interesantes (por qué esto sigue ocurriendo)

  • DKMS se originó en el ecosistema Dell a mediados de los 2000 para mantener los controladores de proveedores compilables tras actualizaciones del kernel, especialmente en flotas empresariales.
  • Ubuntu ha incluido integración de DKMS durante años, pero aún depende de scripts de empaquetado y de la presencia de headers—sin headers, no hay módulo.
  • La aplicación de Secure Boot convirtió “errores de compilación” en “errores de carga”. El módulo puede compilar perfectamente y aun así ser rechazado por el kernel.
  • ZFS on Linux vivió fuera del árbol durante mucho tiempo por fricciones de licencia; esa historia explica por qué muchas instalaciones de Ubuntu todavía dependen de DKMS para los módulos ZFS.
  • La estabilidad del ABI del kernel no es una promesa para módulos fuera del árbol. Pequeños saltos de kernel pueden romper compilaciones si el módulo usa APIs internas.
  • La cadencia HWE y SRU de Ubuntu puede sorprenderte: una actualización del kernel puede llegar vía unattended upgrades aunque “no cambiaste nada”.
  • initramfs es a menudo el verdadero dominio de fallo. El sistema arranca un kernel; luego el userspace temprano no encuentra el módulo de almacenamiento que necesita.
  • Las compilaciones DKMS pueden verse afectadas por cambios en la toolchain (gcc, make, binutils). “Kernel actualizado” a veces es atajo para “tu compilador también avanzó”.
  • Algunos proveedores entregan módulos precompilados para versiones concretas de kernel, pero las versiones de kernel de Ubuntu derivan; DKMS se convierte en la solución de respaldo—hasta que deja de serlo.

Una cita que ha sobrevivido más postmortems de los que cualquier persona merece: “La esperanza no es una estrategia.” — Gene Kranz. También aplica a las recompilaciones DKMS.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estos no son “ejecuta todo”. Son una caja de herramientas. Cada tarea incluye: el comando, salida de ejemplo, qué significa y la decisión que tomas a partir de ello.

Tarea 1: Confirmar el kernel en ejecución

cr0x@server:~$ uname -r
6.8.0-51-generic

Qué significa: Este es el kernel que se está ejecutando actualmente. Si DKMS falló durante la instalación de un kernel más nuevo, por lo general puedes arreglarlo sin corte inmediato, porque no estás usando aún ese kernel nuevo.

Decisión: Si el kernel en ejecución sigue siendo el conocido bueno, reconstruye DKMS para el kernel nuevo ahora y programa un reinicio controlado más tarde.

Tarea 2: Listar kernels instalados y ver cuál usará el próximo reinicio

cr0x@server:~$ dpkg -l 'linux-image-*generic' | awk '/^ii/{print $2,$3}'
linux-image-6.8.0-51-generic 6.8.0-51.52
linux-image-6.8.0-52-generic 6.8.0-52.53

Qué significa: Tienes al menos dos kernels instalados; la versión más alta suele seleccionarse al arranque.

Decisión: Identifica el kernel “objetivo” para el que debes tener módulos DKMS (aquí: 6.8.0-52-generic).

Tarea 3: Comprobar el estado de DKMS en los kernels

cr0x@server:~$ dkms status
zfs/2.2.2, 6.8.0-51-generic, x86_64: installed
zfs/2.2.2, 6.8.0-52-generic, x86_64: built
nvidia/550.90.07, 6.8.0-51-generic, x86_64: installed
nvidia/550.90.07, 6.8.0-52-generic, x86_64: added

Qué significa: “installed” significa que el módulo está compilado y copiado en /lib/modules/<kernel>. “built” está compilado pero puede que no esté instalado. “added” significa que DKMS conoce el módulo pero no lo ha compilado para ese kernel.

Decisión: Para el kernel objetivo, cualquier estado que no sea “installed” es un riesgo. Compila+instala ahora.

Tarea 4: Verificar que existen los headers del kernel objetivo

cr0x@server:~$ dpkg -l | awk '/linux-headers-6.8.0-52-generic/{print $1,$2,$3}'
ii linux-headers-6.8.0-52-generic 6.8.0-52.53

Qué significa: DKMS necesita headers. Si faltan, DKMS fallará con errores como “Kernel headers for target not found”.

Decisión: Si faltan headers, instálalos antes de reconstruir los módulos DKMS.

Tarea 5: Instalar los headers faltantes (si procede)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y linux-headers-6.8.0-52-generic
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  linux-headers-6.8.0-52-generic
Setting up linux-headers-6.8.0-52-generic (6.8.0-52.53) ...

Qué significa: Los headers ya están presentes; DKMS tiene una oportunidad justa.

Decisión: Reconstruye los módulos DKMS para el kernel objetivo.

Tarea 6: Forzar la instalación automática DKMS para el kernel objetivo

cr0x@server:~$ sudo dkms autoinstall -k 6.8.0-52-generic
Sign command: /lib/modules/6.8.0-52-generic/build/scripts/sign-file
Signing key: /var/lib/shim-signed/mok/MOK.priv
Public certificate (MOK): /var/lib/shim-signed/mok/MOK.der
Building module:
Cleaning build area... done.
Building module(s).... done.
Installing /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Installing /lib/modules/6.8.0-52-generic/updates/dkms/nvidia.ko
depmod... done.

Qué significa: DKMS compiló e instaló módulos para ese kernel específico, y depmod actualizó los mapas de dependencias de módulos.

Decisión: Si esto tuvo éxito, pasa a la validación: ¿puede cargarse el módulo (o al menos está presente y firmado)?

Tarea 7: Si falla, lee el log de compilación DKMS en serio

cr0x@server:~$ sudo tail -n 40 /var/lib/dkms/nvidia/550.90.07/build/make.log
CONFTEST: drm_prime_pages_to_sg_has_drm_device_arg
CONFTEST: drm_gem_object_put_unlocked
error: implicit declaration of function ‘drm_gem_object_put_unlocked’
make[2]: *** [scripts/Makefile.build:243: /var/lib/dkms/nvidia/550.90.07/build/nvidia-drm/nvidia-drm-gem.o] Error 1
make[1]: *** [Makefile:1926: /var/lib/dkms/nvidia/550.90.07/build] Error 2
make: *** [Makefile:234: __sub-make] Error 2

Qué significa: Esto es un desajuste de API en tiempo de compilación. No es un paquete que falte; es que el código fuente del módulo no soporta esta API del kernel.

Decisión: Deja de intentar reconstrucciones al azar. Necesitas una versión del controlador/módulo compatible con ese kernel (p. ej., actualizar el paquete del controlador NVIDIA), o debes arrancar el kernel anterior hasta que puedas.

Tarea 8: Validar la presencia del módulo para el kernel objetivo sin reiniciar

cr0x@server:~$ ls -l /lib/modules/6.8.0-52-generic/updates/dkms/ | egrep 'zfs|nvidia' | head
-rw-r--r-- 1 root root  8532480 Dec 29 10:12 nvidia.ko
-rw-r--r-- 1 root root 17362944 Dec 29 10:12 zfs.ko

Qué significa: Los archivos existen donde DKMS los coloca para ese kernel.

Decisión: Valida la capacidad de carga y el estado de firma (especialmente si Secure Boot está activo).

Tarea 9: Comprobar el estado de Secure Boot (el detector “compilado pero bloqueado”)

cr0x@server:~$ mokutil --sb-state
SecureBoot enabled

Qué significa: El kernel aplicará verificación de firma de módulos. Los DKMS sin firmar fallarán al cargarse.

Decisión: Si Secure Boot está habilitado, asegúrate de que los módulos DKMS estén firmados con una clave inscrita, o planifica un flujo controlado de inscripción MOK.

Tarea 10: Intentar cargar el módulo en el kernel en ejecución (solo cuando sea seguro)

cr0x@server:~$ sudo modprobe -v zfs
insmod /lib/modules/6.8.0-51-generic/updates/dkms/spl.ko
insmod /lib/modules/6.8.0-51-generic/updates/dkms/zfs.ko

Qué significa: En el kernel en ejecución, la carga del módulo tiene éxito. Esto es una comprobación de cordura de que tu instalación DKMS no está rota globalmente.

Decisión: Si modprobe falla con “Required key not available”, tienes problemas de firma con Secure Boot. Si falla con “Unknown symbol”, tienes un desajuste kernel/módulo.

Tarea 11: Inspeccionar los logs del kernel por errores de firma o símbolos

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Mon Dec 29 10:19:02 2025] Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7
[Mon Dec 29 10:19:02 2025] nvidia: module verification failed: signature and/or required key missing - tainting kernel

Qué significa: Secure Boot o la política de lockdown están bloqueando o marcando como tainted. Algunos entornos toleran el taint; otros lo tratan como incumplimiento.

Decisión: Si tu política requiere módulos firmados, arregla el firmado y la inscripción ahora, antes de reiniciar en un kernel que rechazará el módulo por completo.

Tarea 12: Verificar que initramfs se reconstruyó para el kernel objetivo

cr0x@server:~$ ls -lh /boot/initrd.img-6.8.0-52-generic
-rw-r--r-- 1 root root 98M Dec 29 10:14 /boot/initrd.img-6.8.0-52-generic

Qué significa: El initramfs existe y se actualizó recientemente, pero eso no garantiza que contenga tu módulo.

Decisión: Si el módulo es necesario en el arranque (ZFS root, HBA de almacenamiento, NIC especial), debes verificar su presencia dentro del initramfs.

Tarea 13: Confirmar que el módulo está dentro del initramfs (el paso “confía pero verifica”)

cr0x@server:~$ lsinitramfs /boot/initrd.img-6.8.0-52-generic | egrep '/zfs\.ko|/nvidia\.ko' | head
usr/lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko

Qué significa: ZFS está incluido en el userspace temprano para ese kernel. Para GPUs, por lo general no es necesario incluirlas en initramfs; para escenarios de arranque por red/almacenamiento puede ser obligatorio.

Decisión: Si falta, regenera initramfs después de arreglar la instalación DKMS.

Tarea 14: Reconstruir initramfs para un kernel específico (dirigido, no a lo loco)

cr0x@server:~$ sudo update-initramfs -u -k 6.8.0-52-generic
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Qué significa: Forzaste la regeneración de initramfs para el kernel que te importa.

Decisión: Vuelve a ejecutar las comprobaciones con lsinitramfs; solo entonces considera reiniciar.

Tarea 15: Asegurar que el mapa de dependencias de módulos es correcto para el kernel objetivo

cr0x@server:~$ sudo depmod -a 6.8.0-52-generic

Qué significa: modules.dep y amigos se actualizan. Algunos scripts postinst hacen esto; algunos fallos lo omiten. Ejecutarlo manualmente es barato.

Decisión: Si modprobe después se queja de que no puede encontrar dependencias, probablemente omitiste depmod o los módulos quedaron en una ruta no estándar.

Tarea 16: Retener actualizaciones del kernel mientras estabilizas (opcional pero frecuentemente sensato)

cr0x@server:~$ sudo apt-mark hold linux-image-generic linux-headers-generic
linux-image-generic set on hold.
linux-headers-generic set on hold.

Qué significa: Estás evitando que los meta-paquetes tiren nuevos kernels automáticamente.

Decisión: Usa esto durante la respuesta a incidentes. Quita los hold cuando tengas una canalización DKMS repetible y una puerta de validación.

Tarea 17: Confirmar cuál será la entrada de arranque por defecto (para no reiniciar en la trampa)

cr0x@server:~$ grep -E 'GRUB_DEFAULT|GRUB_TIMEOUT|GRUB_SAVEDEFAULT' /etc/default/grub
GRUB_DEFAULT=0
GRUB_TIMEOUT=5

Qué significa: El valor por defecto es la primera entrada del menú, típicamente el kernel más nuevo.

Decisión: Si el kernel más nuevo no tiene módulos funcionales, o arreglas DKMS para él o temporalmente configuras GRUB para arrancar el kernel conocido bueno.

Tarea 18: Detectar paquetes “medio configurados” tras una actualización desordenada

cr0x@server:~$ sudo dpkg --audit
The following packages are in a mess due to serious problems during installation. They must be reinstalled for them to work properly:
 linux-image-6.8.0-52-generic

Qué significa: La instalación del paquete del kernel no terminó correctamente, lo que puede saltarse los triggers DKMS y la generación de initramfs.

Decisión: Arregla el estado del empaquetado antes de depurar DKMS sin fin.

Tarea 19: Reparar el estado de paquetes y volver a ejecutar triggers postinst

cr0x@server:~$ sudo apt-get -f install
Reading package lists... Done
Building dependency tree... Done
Correcting dependencies... Done
Setting up linux-image-6.8.0-52-generic (6.8.0-52.53) ...
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic

Qué significa: Los hooks post-install del kernel se ejecutaron. Eso a menudo incluye triggers de reconstrucción DKMS.

Decisión: Revisa de nuevo dkms status para el kernel objetivo; valida la presencia de módulos y el contenido del initramfs otra vez.

Broma 1: DKMS es como una suscripción al gimnasio: solo notas que no funciona cuando realmente intentas usarla.

Recuperar controladores sin tiempo de inactividad: estrategia que funciona

“Sin tiempo de inactividad” no significa magia. Significa que evitas reiniciar en un kernel que no puede cargar módulos críticos y evitas reiniciar hardware en medio del tráfico. Para la mayoría de incidentes DKMS, el sistema sigue ejecutándose en el kernel anterior y todo está bien—hasta que reinicias. Esa es tu ventana.

Paso 1: Decide qué significa “controlador crítico” en este equipo

No trates todos los módulos DKMS por igual. Un módulo VirtualBox faltante en un servidor es molesto; un módulo de almacenamiento faltante en un nodo root-on-ZFS es catastrófico. Clasifica el host:

  • Crítico para almacenamiento: ZFS root, pools ZFS, controladores HBA, dependencias de dm-crypt.
  • Crítico para red: controladores NIC fuera del árbol (raro en Ubuntu, pero ocurre), módulos DPDK, pilas SR-IOV, offloads de proveedor.
  • Crítico para cómputo: nodos con NVIDIA GPU, clusters ML, transcodificadores de vídeo.
  • “Deseable”: controladores de estaciones de trabajo y módulos no esenciales.

Crítico significa: no reiniciar hasta que el kernel objetivo tenga un módulo validado y cargable y un initramfs sano.

Paso 2: Compila para el kernel al que vas a arrancar, no para el que estás ejecutando

Los valores por defecto de DKMS pueden engañar. Si solo ejecutas dkms autoinstall sin -k, a menudo apunta al kernel en ejecución. Eso no es lo que necesitas durante la recuperación. Necesitas el kernel que se usará en el próximo arranque.

Compila explícitamente para la versión de kernel objetivo. Siempre.

Paso 3: Prefiere empaquetado del proveedor que siga tu línea de kernel

Cuando un módulo DKMS falla por desajustes de API, tienes dos opciones realistas:

  • Actualizar el paquete del controlador/módulo a una versión compatible con el kernel nuevo.
  • Aplazar el reinicio y fijar la versión del kernel hasta que exista un controlador compatible.

Intentar parchear el código del módulo en una caja de producción a las 2am es un hobby, no una práctica SRE.

Paso 4: Valida con “puede cargar” y “está en initramfs”

La presencia en disco no basta. Necesitas al menos uno de:

  • Prueba de carga en el kernel objetivo (difícil sin reiniciar).
  • Validación de firma (si Secure Boot está habilitado).
  • Verificación de inclusión en initramfs para módulos críticos en arranque temprano.

Un compromiso práctico: valida la ruta del artefacto DKMS, ejecuta comprobaciones modinfo, verifica el estado de firmado del módulo y valida initramfs. Luego reinicia en una ventana de mantenimiento controlada con un kernel de reversión conocido listo.

Paso 5: No rompas tu propia red mientras arreglas un controlador

La mayoría de las recuperaciones DKMS son intensivas en CPU y disco pero no perturban el tráfico. La zona de peligro es cuando descargas/cargas módulos en vivo. A menos que tengas redundancia (bonding, multipath, clustering), evita descargar/cargar controladores de red/almacenamiento en un host crítico sin redundancia durante horas de negocio.

Reconstruir e instalar es seguro. Descargar/cargar es un cambio.

Paso 6: Crea una “puerta de reinicio”

En producción, el control más simple para no tener tiempo de inactividad es la política: no permitir reiniciar si los módulos DKMS no están instalados para el kernel más reciente instalado. Puedes aplicarlo con un script local que compruebe:

  • dkms status para el kernel objetivo muestra “installed” para los módulos críticos
  • lsinitramfs contiene los módulos de arranque temprano
  • mokutil --sb-state y el estado de firma están alineados

Luego lo integras en tu proceso de cambios. Aburrido. Funciona.

Secure Boot y firmado de módulos (MOK): el rompedor silencioso

Si ejecutas Ubuntu 24.04 en hardware con Secure Boot habilitado—y muchas organizaciones lo hacen porque cumplimiento ama las casillas—DKMS puede “tener éxito” y aun así perder funcionalidad. He aquí por qué:

  • DKMS compila un módulo.
  • El kernel se niega a cargarlo si no está firmado (o está firmado con una clave no inscrita).
  • Lo descubres solo cuando el controlador se necesita por primera vez, a menudo después de un reinicio.

Cómo reconocer fallos de firma de Secure Boot rápidamente

Síntomas típicos:

  • modprobe: ERROR: could not insert '...': Required key not available
  • dmesg muestra “module verification failed” o restricciones de lockdown
  • dkms status dice “installed” pero la funcionalidad está ausente

Qué hacer al respecto (opciones pragmáticas)

  1. Firmar módulos DKMS e inscribir la clave (MOK). Esta es la opción limpia cuando Secure Boot debe permanecer habilitado.
  2. Deshabilitar Secure Boot en firmware. Es operativamente la más simple pero puede violar políticas.
  3. Usar controladores firmados y en-tree cuando sea posible. A largo plazo es lo mejor, no siempre disponible.

Comprobar si DKMS está firmando módulos

cr0x@server:~$ sudo grep -R "sign-file" -n /etc/dkms /etc/modprobe.d 2>/dev/null | head

Qué significa: Puede que no haya una configuración explícita. En Ubuntu, el firmado de módulos para DKMS a menudo se integra con la herramienta shim/MOK y los scripts de empaquetado.

Decisión: Si Secure Boot está activo y ves fallos de firma, no adivines. Verifica el firmado del módulo con modinfo.

Inspeccionar metadatos de firma en un módulo

cr0x@server:~$ modinfo -F signer /lib/modules/6.8.0-52-generic/updates/dkms/zfs.ko
Canonical Ltd. Secure Boot Signing

Qué significa: El módulo lleva una cadena de firmante. Si está vacía, puede que esté sin firmar (o sin metadatos).

Decisión: Si falta el firmante y Secure Boot está activo, necesitas firmar e inscribir, o aceptar que el módulo no se cargará.

Verificar claves MOK inscritas

cr0x@server:~$ sudo mokutil --list-enrolled | head
[key 1]
SHA1 Fingerprint: 12:34:56:78:90:...
Subject: CN=Canonical Ltd. Secure Boot Signing

Qué significa: El sistema confía en un conjunto de claves. Si tu firmado DKMS usa una clave diferente, el kernel la rechazará.

Decisión: Alinea tu clave de firmado con las claves inscritas, o inscribe la clave correcta vía MOK (lo cual normalmente requiere un reinicio al gestor MOK).

Broma 2: Secure Boot es el portero en la discoteca del kernel: tu módulo puede estar perfecto y aun así no estar en la lista.

initramfs, arranque temprano y por qué “se compiló” no basta

initramfs es la imagen comprimida de userspace temprano que el kernel carga para pasar de “kernel iniciado” a “sistema raíz montado”. Si tu módulo crítico no está en initramfs, la presencia del módulo en disco es irrelevante porque el disco podría no estar accesible todavía.

Esto importa para:

  • Sistemas root-on-ZFS
  • Root cifrado que necesita módulos específicos temprano
  • Algunos flujos exóticos de arranque por red o almacenamiento

Modo de fallo: DKMS instaló módulos, pero initramfs se generó antes de la instalación

Esto ocurre durante actualizaciones interrumpidas, operaciones de paquetes en paralelo o cuando DKMS corre tarde y initramfs se ejecutó antes. Arrancas y descubres que el userspace temprano no encuentra ZFS/SPL, o que tu controlador de almacenamiento no está presente.

Arreglo: reconstruye initramfs después de la instalación DKMS para el kernel objetivo y verifica contenidos con lsinitramfs.

Modo de fallo: múltiples kernels, initramfs obsoleto

Puedes tener un initramfs correcto para el kernel en ejecución pero no para el kernel más nuevo instalado. Así es como se arma la trampa del reinicio. Siempre valida el initramfs que coincide con el kernel al que vas a reiniciar.

Tres mini-historias corporativas (realistas, anonimizadas)

Mini-historia 1: El incidente causado por una suposición equivocada

Gestionaban un pequeño clúster GPU para inferencia por lotes. Nada exótico: hosts Ubuntu, controlador NVIDIA DKMS, un scheduler de trabajos y una ventana de cambios los martes. El ritmo de actualizaciones era “kernels automáticamente, controladores cuando alguien se queja”. Funcionó hasta que dejó de funcionar.

Una actualización del kernel llegó el viernes por la noche vía unattended upgrades. Nadie lo notó porque los nodos seguían ejecutando el kernel antiguo y las GPUs seguían disponibles. El lunes por la mañana drenaron un nodo para mantenimiento no relacionado y lo reiniciaron. Volvió sin cargar los módulos NVIDIA.

La suposición equivocada fue sutil: “Si el controlador está instalado, está instalado.” Nunca comprobaron si el controlador se había compilado para el kernel nuevo instalado. El nodo arrancó en el kernel más nuevo (como debía) y DKMS había fallado silenciosamente días antes.

Intentaron la solución clásica: reinstalar el paquete del controlador. Todavía no cargó. Finalmente alguien miró dmesg y encontró un mensaje de aplicación de firma de Secure Boot. Secure Boot se había habilitado en firmware tras una renovación de hardware reciente, pero nadie actualizó el runbook.

La solución fue sencilla—firmar e inscribir la clave correctamente—pero requirió reinicios hacia el gestor MOK. Perdieron un día coordinando reinicios entre nodos, algo evitable si hubieran tenido una puerta de reinicio y una comprobación “DKMS instalado para el kernel más nuevo”.

Mini-historia 2: La optimización que salió mal

Una firma financiera se cansó de parches lentos. Decidieron “optimizar” quitando herramientas de compilación de servidores de producción: sin gcc, sin make, sin headers, solo paquetes mínimos. A seguridad le gustó. La imagen era más pequeña, los escaneos más limpios y los servidores parecían más tipo appliance.

Luego se desplegó una actualización del kernel. DKMS intentó recompilar el módulo NIC fuera del árbol que necesitaban para las funciones de una tarjeta en particular. Sin compilador, sin headers, sin construcción. DKMS falló, pero el kernel actual siguió funcionando. El fallo permaneció invisible.

La siguiente oleada de reinicios ocurrió durante un mantenimiento de energía en el centro de datos. Los reinicios fueron obligatorios. Varios hosts arrancaron en el kernel nuevo sin el módulo NIC. El controlador incluido en el kernel funcionó lo suficiente para arrancar, pero carecía de las funciones de offload que habían afinado para la latencia. El síntoma no fue “sin red”. Fue peor: colapso de rendimiento intermitente y timeouts bajo carga.

Revirtieron la decisión de “imagen mínima” para esa flota y movieron las compilaciones DKMS a una canalización controlada: precompilan módulos para el kernel objetivo en un entorno de construcción, distribuyen los artefactos y verifican antes de reiniciar. La optimización no estaba mal en principio. Estaba mal sin reemplazar la construcción implícita de DKMS en el host por una cadena de suministro explícita.

La lección: si quitas compiladores de hosts, te responsabilizas por el proceso de construcción de módulos de extremo a extremo. Si no lo haces, solo pospones el fallo hasta el momento del reinicio.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una compañía de medios ejecutaba muchos servidores con intensivo uso de almacenamiento, algunos con pools ZFS. Su práctica era dolorosamente aburrida: cada actualización del kernel iba seguida de una comprobación automatizada de “preparación para reinicio”. Verificaba el estado DKMS para ZFS contra el kernel más nuevo instalado, comprobaba que initramfs contuviera ZFS y confirmaba que permanecía instalado un kernel conocido bueno como reversión.

Una mañana, la comprobación señaló un fallo en un subconjunto de hosts. DKMS mostraba ZFS “built” pero no “installed” para el kernel más nuevo. Los hosts aún funcionaban, así que no entraron en pánico. Bloquearon reinicios vía su orquestador y abrieron un ticket.

La causa raíz fue una carrera de empaquetado durante una actualización unattended anterior: la generación de initramfs ocurrió, luego la instalación DKMS falló y se reintentó, dejando un estado inconsistente. La comprobación aburrida lo detectó antes de cualquier reinicio. Ejecutaron dkms autoinstall -k, reconstruyeron initramfs para el kernel objetivo y limpiaron la puerta.

Cero tiempo de inactividad, sin drama, sin fin de semana. Así es la “excelencia operacional” cuando quitas la diapositiva de PowerPoint.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: dkms status muestra “added” para el kernel nuevo

Causa raíz: El módulo DKMS está registrado pero no compilado para ese kernel; faltan headers o la compilación falló antes.

Solución: Instala los headers para el kernel objetivo y luego ejecuta sudo dkms autoinstall -k <kernel>. Valida que los archivos existan bajo /lib/modules/<kernel>/updates/dkms.

2) Síntoma: La compilación DKMS falla con “Kernel headers not found”

Causa raíz: Falta el paquete linux-headers-<kernel>, o el enlace simbólico /lib/modules/<kernel>/build está roto.

Solución: Instala los headers coincidentes; verifica que ls -l /lib/modules/<kernel>/build apunte a los headers.

3) Síntoma: El módulo se compila, pero modprobe falla con “Required key not available”

Causa raíz: Secure Boot habilitado; módulo sin firmar o firmado con clave no inscrita.

Solución: Asegura que los módulos DKMS estén firmados con una clave de confianza e inscríbela vía MOK, o deshabilita Secure Boot si la política lo permite.

4) Síntoma: Al arrancar en el kernel nuevo se pierde ZFS/almacenamiento raíz

Causa raíz: initramfs para el kernel nuevo carece de los módulos requeridos, a menudo por el orden de triggers DKMS/postinst.

Solución: Tras la instalación DKMS, ejecuta update-initramfs -u -k <kernel> y confirma con lsinitramfs.

5) Síntoma: Errores de compilación DKMS sobre símbolos faltantes / declaraciones implícitas

Causa raíz: Cambio en la API del kernel; la versión del controlador no es compatible con el kernel nuevo.

Solución: Actualiza el paquete del controlador/módulo (p. ej., versión más nueva de NVIDIA/ZFS), o mantén el kernel y arranca en el anterior hasta disponer de paquetes compatibles.

6) Síntoma: Todo parece instalado, pero el hardware no funciona después del reinicio

Causa raíz: Compilaste para la versión de kernel equivocada (kernel en ejecución en lugar del instalado más nuevo), o arrancaste un kernel distinto al esperado.

Solución: Confirma los kernels instalados, confirma la selección de arranque por defecto, reconstruye explícitamente para el kernel de arranque con dkms autoinstall -k.

7) Síntoma: Las actualizaciones de paquetes cuelgan o dejan estado “medio configurado”

Causa raíz: Actualización interrumpida, contención del lock de dpkg, disco lleno o fallos en scripts postinst (a menudo DKMS).

Solución: Repara el estado de dpkg: apt-get -f install, comprueba espacio en disco y vuelve a ejecutar compilaciones DKMS después de que la capa de empaquetado esté sana.

Listas de verificación / plan paso a paso

Lista A: Recuperación sin tiempo de inactividad en un host que aún ejecuta el kernel antiguo

  1. Identificar el kernel objetivo (el más nuevo instalado): usa dpkg -l para imágenes instaladas.
  2. Comprobar estado DKMS para módulos críticos contra ese kernel: dkms status.
  3. Instalar headers para el kernel objetivo si faltan: apt-get install linux-headers-<kernel>.
  4. Reconstruir módulos para el kernel objetivo: dkms autoinstall -k <kernel>.
  5. Validar artefactos existentes en /lib/modules/<kernel>/updates/dkms.
  6. Comprobación Secure Boot: mokutil --sb-state y confirmar firmante del módulo con modinfo.
  7. Reconstruir initramfs para el kernel objetivo (hosts críticos de almacenamiento): update-initramfs -u -k <kernel>.
  8. Verificar contenido de initramfs: lsinitramfs incluye los módulos requeridos.
  9. Mantener reversión disponible: confirmar que un kernel antiguo conocido bueno sigue instalado.
  10. Programar reinicio con plan de reversión (acceso por consola, selección GRUB, manos remotas si es necesario).

Lista B: Si ya reiniciaste en el kernel roto

  1. Confirmar qué falta: lsmod, modprobe y dmesg.
  2. Comprobar Secure Boot de inmediato; no pierdas tiempo reconstruyendo módulos sin firmar si Secure Boot los bloqueará.
  3. Instalar prerequisitos de compilación (temporalmente): headers, toolchain de compilación si DKMS lo necesita.
  4. Reconstruir DKMS para el kernel en ejecución: dkms autoinstall -k $(uname -r).
  5. Si la compilación falla por desajuste de API: detente y elige una versión de controlador compatible o revierte al kernel anterior vía GRUB.
  6. Arregla initramfs si hay módulos de arranque temprano implicados, y luego prueba el reinicio.

Lista C: Evitarlo la próxima vez (higiene de producción)

  1. Crear una “puerta de reinicio” que verifique DKMS instalado para el kernel más nuevo y valide initramfs donde haga falta.
  2. Escalar actualizaciones de kernel en hosts canarios con hardware representativo.
  3. Tratar la política de Secure Boot como una restricción de primera clase, no como una nota en la BIOS.
  4. Mantener al menos un kernel de reversión instalado y arrancable en todo momento.
  5. Controlar unattended upgrades para que los kernels no cambien sin validación.

Preguntas frecuentes

1) ¿Por qué DKMS “falló” solo después de la actualización del kernel?

Porque los módulos DKMS se compilan contra los headers de una versión específica del kernel. Cuando el kernel cambia, el módulo debe recompilarse. Si esa recompilación falla, no lo notarás hasta que arranques el kernel nuevo o intentes cargar el módulo para él.

2) ¿Puedo arreglar DKMS sin reiniciar?

Puedes recompilar e instalar módulos para el kernel siguiente sin reiniciar, sí. Normalmente no puedes probar cargarlos en ese kernel sin arrancarlo realmente. Por eso validas artefactos, firmas e initramfs antes del reinicio.

3) ¿Qué significan “added” vs “built” vs “installed” en dkms status?

added: DKMS conoce el código fuente del módulo pero no lo ha compilado para ese kernel. built: compilado pero no necesariamente instalado en el árbol de módulos del kernel. installed: colocado en /lib/modules/<kernel> y depmod se ha ejecutado (o debería haberse ejecutado).

4) ¿Realmente necesito headers que coincidan con el kernel?

Sí. DKMS compila contra los headers de la versión de kernel que apuntas. “Suficientemente cercano” no existe aquí; instala linux-headers-<versión exacta>.

5) ¿Por qué Secure Boot empeora tanto esto?

Porque convierte un problema de compilación en un problema de cumplimiento en tiempo de ejecución. Puedes compilar el módulo con éxito y aun así no poder cargarlo. El kernel rechazará módulos no firmados por una clave de confianza cuando Secure Boot y las políticas de lockdown lo requieran.

6) Si Secure Boot está habilitado, ¿debo desactivarlo?

Solo si tu política lo permite. Deshabilitar Secure Boot puede ser la solución operativamente más simple, pero la corrección adecuada en entornos regulados es firmar los módulos DKMS con una clave que controles e inscribirla vía MOK.

7) ¿Por qué mi sistema arrancó pero luego el almacenamiento o la red estaban rotos?

A menudo porque el controlador se carga más tarde de lo que crees, o existe un controlador in-tree de reserva que carece de funciones. Otra causa común: initramfs carece de un módulo necesario temprano, así que el arranque tiene éxito parcialmente y luego los dispositivos aparecen tarde o incorrectamente.

8) ¿Cuál es la reversión más segura si no puedo conseguir que DKMS compile para el kernel nuevo?

Arranca el kernel anterior conocido bueno y fija temporalmente los meta-paquetes del kernel. Luego actualiza el paquete del controlador/módulo a una versión que soporte el kernel nuevo antes de probar el reinicio de nuevo.

9) ¿Debería mantener compiladores fuera de los servidores de producción?

Depende. Si dependes de compilaciones DKMS en el host, necesitas la toolchain y los headers. Si los quitas, debes reemplazar la construcción on-host de DKMS con una canalización que produzca y distribuya módulos compatibles para cada kernel que despliegues.

10) ¿Cómo evito que se acumulen kernels “trampa de reinicio”?

Tener un paso de validación después de la instalación del kernel que compruebe el estado DKMS para módulos críticos en el kernel más nuevo instalado. Si falla, bloquea la automatización de reinicios y alerta. Esto es más barato que la respuesta a incidentes.

Próximos pasos que puedes hacer hoy

Si ejecutas Ubuntu 24.04 con controladores gestionados por DKMS, deja de tratar las actualizaciones del kernel como “solo parches de seguridad”. También son eventos de reconstrucción de controladores. El camino práctico hacia cero tiempo de inactividad es corto:

  1. Elige tus módulos DKMS críticos por rol de host (almacenamiento, red, GPU).
  2. Después de cada instalación de kernel, reconstruye módulos para el kernel más nuevo instalado (dkms autoinstall -k).
  3. Valida firmas si Secure Boot está habilitado; no asumas que el éxito de compilación implica éxito de carga.
  4. Regenera y verifica initramfs para módulos críticos en arranque temprano.
  5. Sólo entonces reinicia. Mantén un kernel de reversión instalado y arrancable.

Haz eso, y “DKMS falló tras la actualización del kernel” dejará de ser un incidente. Será un ítem de la lista de verificación que se completa antes de que alguien lo note.

Ubuntu 24.04: Disco «lleno» pero df muestra espacio — agotamiento de inodos explicado (y solucionado)

Estás conectado por SSH a un servidor Ubuntu 24.04 que «tiene mucho espacio» según df -h.
Sin embargo, cada despliegue falla, los logs no rotan, apt no puede desempaquetar y el kernel no para de lanzar
No space left on device como si le pagaran por cada error.

Este es uno de esos incidentes que hace dudar hasta a la gente más experimentada. El disco no está lleno.
Algo más lo está. Normalmente: los inodos. Y una vez que entiendes qué significa eso, la solución es casi aburrida.
Casi.

Qué estás viendo: «disco lleno» con GB libres

En Ubuntu, «disco lleno» suele significar una de tres cosas:

  • Agotamiento de espacio en bloques: el caso clásico; te quedaste sin bytes.
  • Agotamiento de inodos: te quedaste sin entradas de metadatos de archivos; los bytes pueden quedar libres.
  • Bloques reservados, cuotas o corrupción del sistema de archivos: tienes espacio pero no puedes usarlo.

El agotamiento de inodos es el más traicionero porque no aparece en el primer comando que todo el mundo ejecuta.
df sin opciones sólo informa bloques usados/disponibles. Puedes tener 200 GB libres y aún así
no poder crear un archivo de 0 bytes. El sistema de archivos no puede asignar un nuevo inodo, así que no puede crear un archivo nuevo.

Notarás efectos secundarios extraños:

  • No se pueden crear nuevos archivos de log, por lo que los servicios se caen o dejan de registrar justo cuando los necesitas.
  • apt falla a mitad de instalación porque no puede crear archivos temporales o desempaquetar archivos.
  • Las compilaciones de Docker empiezan a fallar en operaciones de «writing layer» aunque los volúmenes parezcan correctos.
  • Algunas aplicaciones informan «disco lleno» mientras otras siguen funcionando (porque no están creando archivos).

Hay una prueba simple: intenta crear un archivo en el sistema de archivos afectado. Si falla con «No space left»
mientras df -h muestra espacio libre, deja de discutir con df y revisa los inodos.

Inodos explicados como si gestionaras producción

Un inodo es el registro del sistema de archivos para un archivo o directorio. Es metadatos: propietario, permisos, marcas de tiempo,
tamaño, punteros a bloques de datos. En muchos sistemas de archivos, el nombre del archivo no se almacena en el inodo; vive en
las entradas del directorio que mapean nombres a números de inodo.

La verdad operativa importante: la mayoría de los sistemas Linux tienen dos «presupuestos» separados:
bloques (bytes) e inodos (cantidad de archivos). Si gastas cualquiera de los dos presupuestos, se acabó.

Por qué se agotan los inodos en sistemas reales

El agotamiento de inodos normalmente no es «muchos archivos grandes». Es «millones de archivos diminutos».
Piensa en caches, buzones de correo, artefactos de compilación, capas de contenedores, espacios de trabajo de CI, subidas temporales
y buffers de métricas que alguien olvidó expirar.

Un archivo de 1 KB todavía consume un inodo. Un archivo de 0 bytes todavía consume un inodo. Un directorio también consume un inodo.
Cuando tienes 12 millones de archivos pequeños, el disco puede estar mayormente vacío en bytes, pero la tabla de inodos está frita.

Qué sistemas de archivos tienden a darte problemas

  • ext4: común en Ubuntu; los inodos suelen crearse al formatear según una ratio de inodos. Si estimaste mal, puedes quedarte sin inodos.
  • XFS: los inodos son más dinámicos; el agotamiento es menos común pero no imposible.
  • btrfs: la asignación de metadatos es diferente; aún puedes tener problemas de espacio para metadatos, pero no es la historia de «conteo fijo de inodos».
  • overlayfs (Docker): no es un tipo de sistema de archivos por sí mismo, pero amplifica el comportamiento de «muchos archivos» en hosts con muchos contenedores.

Una cita que vale la pena mantener en tu runbook mental:

“La esperanza no es una estrategia.” — General Gordon R. Sullivan

Guía rápida de diagnóstico (primero/segundo/tercero)

Cuando la alerta dice «No space left on device» pero las gráficas indican que estás bien, no divagues.
Sigue la guía.

Primero: confirma qué «espacio» está realmente agotado

  1. Revisa bloques (df -h) e inodos (df -i) para el montaje afectado.
  2. Intenta crear un archivo en ese montaje; confirma la ruta de error.
  3. Revisa si hay un remonte en solo lectura o errores del sistema de archivos en dmesg.

Segundo: encuentra el montaje y los mayores consumidores de inodos

  1. Identifica qué ruta del sistema de archivos está fallando (logs, temp, directorio de datos).
  2. Busca directorios con alto conteo de archivos usando find y un par de conteos dirigidos.
  3. Si es Docker/Kubernetes, revisa overlay2, imágenes, contenedores y logs.

Tercero: libera inodos de forma segura

  1. Empieza con limpiezas obvias y seguras: logs antiguos, caches, archivos temporales, vacuum del journal, basura de contenedores.
  2. Elimina archivos, no directorios, si la aplicación espera una estructura de directorios.
  3. Confirma que el uso de inodos baja (df -i) y que los servicios se recuperan.

No necesitas heroísmos. Necesitas un plan de eliminación controlado y un postmortem que impida que vuelva a pasar.

Tareas prácticas: comandos, significado de salidas, decisiones

A continuación hay tareas reales que puedes ejecutar en Ubuntu 24.04. Cada una incluye qué observar y qué decisión tomar.
Hazlas en orden si estás de guardia; escoge si ya conoces el montaje afectado.

Tarea 1: Confirma uso de bloques (la comprobación obvia)

cr0x@server:~$ df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/sda2      ext4   200G   62G  128G  33% /
tmpfs          tmpfs  3.1G  1.2M  3.1G   1% /run
/dev/sdb1      ext4   1.8T  1.1T  648G  63% /data

Significado: El espacio en bloques está bien en / y /data.
Decisión: Si aún recibes «No space left», pasa a comprobar inodos. No pierdas tiempo buscando archivos grandes todavía.

Tarea 2: Comprueba uso de inodos (la mitad de las veces es el culpable)

cr0x@server:~$ df -iT
Filesystem     Type    Inodes  IUsed   IFree IUse% Mounted on
/dev/sda2      ext4   13107200 13107190     10  100% /
tmpfs          tmpfs    790000     420 789580    1% /run
/dev/sdb1      ext4  122142720  982134 121160586   1% /data

Significado: El sistema de archivos raíz tiene IUse% 100%. Sólo 10 inodos libres. Eso impide crear archivos.
Decisión: Debes eliminar archivos en / (o moverlos) para liberar inodos. Eliminar un archivo enorme no ayudará si sigue siendo un solo inodo de todos modos.

Tarea 3: Reproduce el fallo de forma controlada

cr0x@server:~$ touch /tmp/inode-test-file
touch: cannot touch '/tmp/inode-test-file': No space left on device

Significado: El sistema de archivos no puede asignar un inodo para un archivo pequeño.
Decisión: Trátalo como un incidente de disponibilidad. Cualquier cosa que necesite escribir (logs, sockets, archivos PID, temporales) puede fallar a continuación.

Tarea 4: Identifica qué ruta está en el sistema de archivos afectado

cr0x@server:~$ findmnt -T /var/log
TARGET SOURCE    FSTYPE OPTIONS
/      /dev/sda2 ext4   rw,relatime,errors=remount-ro

Significado: /var/log está en /. Si los logs explotaron en millones de archivos, ese es tu campo de batalla.
Decisión: Centra la búsqueda bajo /var, /tmp, /var/lib y cualquier directorio de aplicaciones en /.

Tarea 5: Detecta los sospechosos habituales por tamaño (bytes)

cr0x@server:~$ sudo du -xh --max-depth=1 /var | sort -h
12M	/var/cache
180M	/var/log
2.1G	/var/lib
2.4G	/var

Significado: El uso en bytes no es extremo. Esta es tu primera pista de que el problema es el conteo de archivos, no el tamaño.
Decisión: Deja de optimizar por GB. Empieza a optimizar por cantidad de archivos.

Tarea 6: Encuentra directorios con gran número de archivos (barrido de alto nivel)

cr0x@server:~$ sudo bash -lc 'for d in /var/* /tmp /home; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/cache	1320
/var/log	5402
/var/lib	12877190
/tmp	120
/home	88

Significado: /var/lib tiene ~12.8 millones de archivos. Eso no es «un poco desordenado»; son tus inodos.
Decisión: Enfócate en /var/lib. Si este es un host de contenedores, espera encontrar /var/lib/docker o /var/lib/containerd.

Tarea 7: Filtra rápidamente dentro de /var/lib

cr0x@server:~$ sudo bash -lc 'for d in /var/lib/*; do [ -d "$d" ] && printf "%s\t" "$d" && find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -n -k2 | tail -n 10'
/var/lib/systemd	2200
/var/lib/dpkg	9800
/var/lib/docker	12866012

Significado: Docker se está comiendo tu presupuesto de inodos mediante capas, caché de compilación y logs de contenedores.
Decisión: Decide si puedes ejecutar prune de forma segura ahora mismo. Si este host es de propósito único, hacer prune suele ser correcto. Si es un servidor «pet» con cargas desconocidas, sé más cauto.

Tarea 8: Revisa la vista de Docker (bytes, no inodos, pero útil)

cr0x@server:~$ sudo docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        12        23.4GB    18.1GB (77%)
Containers      65        18        4.2GB     2.9GB (69%)
Local Volumes   26        20        11.8GB    1.1GB (9%)
Build Cache     152       0         6.4GB     6.4GB

Significado: Hay bastante para podar, pero esta salida no muestra el conteo de archivos.
Decisión: Procede con acciones de prune si entiendes el impacto: contenedores detenidos, imágenes huérfanas, caché de compilación.

Tarea 9: Prune de Docker de forma segura (empieza conservador)

cr0x@server:~$ sudo docker image prune -a -f
Deleted Images:
deleted: sha256:1a2b3c...
deleted: sha256:4d5e6f...
Total reclaimed space: 17.8GB

Significado: Imágenes no usadas eliminadas; el conteo de inodos también debería bajar porque las capas contienen muchos archivos pequeños.
Decisión: Vuelve a comprobar df -i. Si el uso de inodos no baja lo suficiente, elimina contenedores y caché de compilación a continuación.

Tarea 10: Podar la caché de compilación (a menudo una fábrica silenciosa de inodos)

cr0x@server:~$ sudo docker builder prune -a -f
Deleted build cache objects:
k2r1m3n4o5p6...
Total reclaimed space: 6.4GB

Significado: Los hosts de CI/compilación suelen dejar fragmentos de caché atrás.
Decisión: Si necesitas builds reproducibles y reconstrucciones rápidas, programa esta limpieza en lugar de ejecutarla constantemente. Por ahora, estás estabilizando el nodo.

Tarea 11: Confirma la recuperación de inodos (esta es tu métrica de éxito)

cr0x@server:~$ df -i /
Filesystem     Inodes   IUsed    IFree IUse% Mounted on
/dev/sda2     13107200 8123400  4983800   63% /

Significado: Has vuelto del borde del precipicio. Los servicios pueden crear archivos de nuevo.
Decisión: Reinicia los servicios que se cayeron por escrituras fallidas y coloca guardarraíles para que no vuelva a ocurrir.

Tarea 12: Si los logs son el culpable, límpialos y establéceles límites (systemd-journald)

cr0x@server:~$ sudo journalctl --disk-usage
Archived and active journals take up 1.7G in the file system.

Significado: Los journals no suelen ser especialmente intensivos en inodos (son archivos menos numerosos y más grandes), pero pueden presionar por bytes.
Decisión: Si importan los bytes también, haz vacuum. Si importan los inodos, céntrate en aplicaciones que crean muchos archivos de log separados, no en el journal en sí.

cr0x@server:~$ sudo journalctl --vacuum-time=7d
Vacuuming done, freed 1.2G of archived journals from /var/log/journal.

Significado: Bytes liberados. Inodos liberados sólo de forma modesta.
Decisión: Establece límites persistentes en la configuración de journald si este host es propenso a logs ruidosos.

Tarea 13: Si apt falla, limpia cachés de paquetes

cr0x@server:~$ sudo apt-get clean

Significado: Limpia los archivos de paquetes descargados bajo /var/cache/apt/archives.
Decisión: Buena higiene, pero normalmente no es una bala de plata para inodos. Ayuda cuando las caches contienen muchos archivos pequeños y parciales.

Tarea 14: Encuentra directorios con gran conteo de archivos usando du (estilo inodos)

cr0x@server:~$ sudo du -x --inodes --max-depth=2 /var/lib | sort -n | tail -n 10
1200	/var/lib/systemd
9800	/var/lib/dpkg
12866012	/var/lib/docker
12877190	/var/lib

Significado: Esta es la vista valiosa: consumo de inodos por directorio.
Decisión: Apunta al mayor consumidor. No «limpies un poco por todas partes». Perderás tiempo y seguirás al 100%.

Tarea 15: En caso de duda, inspecciona fan-out patológico

cr0x@server:~$ sudo find /var/lib/docker -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -n | tail -n 5
  42000 /var/lib/docker/containers/8a7b.../mounts
  78000 /var/lib/docker/overlay2/3f2d.../diff/usr/lib
 120000 /var/lib/docker/overlay2/9c1e.../diff/var/cache
 250000 /var/lib/docker/overlay2/b7aa.../diff/usr/share
 980000 /var/lib/docker/overlay2/2d9b.../diff/node_modules

Significado: Una capa de contenedor con node_modules puede generar conteos de archivos absurdos.
Decisión: Arregla la compilación (multi-stage builds, eliminar dependencias de desarrollo, .dockerignore) y/o mueve el data root de Docker a un sistema de archivos diseñado para esta carga.

Tarea 16: Confirma el tipo de sistema de archivos y detalles de provisión de inodos

cr0x@server:~$ sudo tune2fs -l /dev/sda2 | egrep -i 'Filesystem features|Inode count|Inode size|Block count|Reserved block count'
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Inode count:              13107200
Inode size:               256
Block count:              52428800
Reserved block count:     2621440

Significado: ext4 tiene aquí un conteo de inodos fijo. No puedes «añadir más inodos» sin reconstruir el sistema de archivos.
Decisión: Si el trabajo de este host son «millones de archivos pequeños», planifica una migración: nuevo sistema de archivos con una ratio de inodos distinta, o una disposición de almacenamiento diferente.

Broma #1: Los inodos son como salas de reuniones: puedes tener un edificio vacío y aún estar «lleno» si cada sala está reservada con una nota pegajosa.

Tres mini-historias corporativas (anonimizadas, dolorosamente reales)

1) Incidente causado por una suposición errónea: «df dice que estamos bien»

Una compañía SaaS mediana gestionaba una flota de servidores Ubuntu que manejaban ingestión de webhooks. Los ingenieros monitorizaban el uso de disco en porcentaje,
alertaban al 80% y se sentían orgullosos: «Ya no llenamos discos». Un martes por la tarde, la canalización de ingestión empezó a devolver
500 intermitentes. Los reintentos se acumularon, las colas se llenaron, los dashboards se encendieron.

El de guardia siguió la rutina estándar: df -h parecía sano. La CPU estaba bien. La memoria no era ideal pero manejable.
Reiniciaron un servicio y murió inmediatamente porque no podía crear un archivo PID. Ese mensaje de error finalmente apareció:
No space left on device.

Alguien sugirió «quizá el disco mintió», que es una forma curiosamente humana de decir «no medimos lo correcto».
Ejecutaron df -i y encontraron el filesystem raíz al 100% de uso de inodos. El culpable no era una base de datos. Era
una «cola de reintentos temporal» implementada como un JSON por evento bajo /var/lib/app/retry/.
Cada archivo era minúsculo. Había millones.

La solución fue inmediata: borrar archivos más antiguos que un umbral y reiniciar. La solución real tomó un sprint:
migrar la cola de reintentos a un sistema de colas diseñado para eso, dejar de usar el sistema de archivos como una base de datos barata y añadir alertas de inodos.
El postmortem tuvo un título educado. El chat interno no.

2) Optimización que salió mal: «cachear todo en disco local»

Un equipo de ingeniería de datos aceleró trabajos ETL cacheando artefactos intermedios en SSD local.
Pasaron de «un artefacto por lote» a «un artefacto por partición», por paralelismo.
El rendimiento mejoró. Los costes parecieron mejores. Todos siguieron con su trabajo.

Semanas después, los nodos empezaron a fallar de forma escalonada. No todos a la vez, lo que lo hizo más difícil.
Algunos trabajos tenían éxito, otros fallaban al azar al intentar escribir salida. Los errores eran inconsistentes:
excepciones de Python, errores de E/S en Java, ocasional «sistema de archivos en solo lectura» después de que el kernel remontara un disco problemático.

La causa raíz fue vergonzosamente mecánica: el cacheado creó decenas de millones de archivos diminutos.
El sistema ext4 se había formateado con una ratio de inodos por defecto adecuada para uso general, no para «millones de shards».
Los nodos no se quedaron sin bytes; se quedaron sin identidades de archivo. La «optimización» fue efectivamente una prueba de estrés de inodos.

«Lo arreglaron» aumentando el tamaño del disco. Eso no ayudó, porque el conteo de inodos seguía fijo.
Luego reformatearon con una densidad de inodos más adecuada y cambiaron la estrategia de cache para empacar particiones en paquetes tipo tar.
El rendimiento regresó ligeramente. La fiabilidad mejoró dramáticamente. Es un intercambio razonable.

3) Práctica aburrida pero correcta que salvó el día: archivos separados y guardarraíles

Otra organización ejecutaba cargas mixtas en nodos Kubernetes: servicios del sistema, Docker/containerd y algo de espacio temporal local.
Tenían una regla: cualquier cosa que pueda explotar en cantidad de archivos tiene su propio sistema de archivos. Docker vivía en /var/lib/docker
montado desde un volumen dedicado. El espacio temporal estaba en un montaje separado con políticas agresivas de limpieza.

También tenían dos monitores aburridos: «uso de bloques» y «uso de inodos». Nada de ML sofisticado. Sólo dos series temporales y alertas que paginaban
antes del precipicio. Probaron las alertas trimestralmente creando una tormenta temporal de inodos en staging (sí, eso existe).

Un día una nueva pipeline de compilación empezó a producir capas patológicas con árboles de dependencias enormes.
Los inodos en el volumen de Docker subieron rápido. La alerta saltó temprano. El de guardia no tuvo que aprender nada nuevo bajo estrés.
Hicieron prune, revertieron la pipeline y elevaron los límites. El resto del nodo se mantuvo sano porque la raíz no se vio involucrada.

El informe del incidente fue corto. La solución fue aburrida. Todos durmieron tranquilos.
Esa es la esencia del SRE.

Datos interesantes y un poco de historia (porque explican los modos de fallo)

  • Los inodos vienen del Unix temprano: el concepto data de la concepción original del sistema de archivos Unix, donde metadatos y bloques de datos eran estructuras separadas.
  • Los sistemas ext tradicionales preasignan inodos: ext2/ext3/ext4 normalmente deciden el conteo de inodos en el momento de mkfs basándose en una ratio de bytes por inodo, no dinámicamente según la carga.
  • Las ratios de inodos por defecto son un compromiso: están pensadas para cargas de propósito general; no están adaptadas a capas de contenedores, caches de CI o explosiones de maildir.
  • Los directorios también consumen inodos: «Sólo creamos directorios» no es una defensa; cada directorio también consume un inodo.
  • «No space left on device» es un mensaje sobrecargado: la misma cadena de error puede significar falta de bloques, falta de inodos, cuota excedida o incluso un sistema de archivos remontado en solo lectura tras errores.
  • Existen bloques reservados por una razón: ext4 suele reservar un porcentaje de bloques para root, pensado para mantener el sistema usable bajo presión; no reserva inodos de la misma manera.
  • Las cargas de pequeños archivos son más difíciles de lo que parecen: las operaciones de metadatos dominan; la eficiencia de inodos y búsquedas en directorios puede importar más que el rendimiento secuencial.
  • Las imágenes de contenedores magnifican los patrones de archivos pequeños: ecosistemas de lenguajes con árboles de dependencias enormes (Node, Python, Ruby) pueden crear capas con un número masivo de archivos.
  • Algunos sistemas de archivos se movieron hacia metadatos dinámicos: XFS y btrfs manejan metadatos de forma distinta, lo que cambia la forma de los fallos «llenos», pero no los elimina.

Soluciones: desde limpieza rápida hasta prevención permanente

Estabilización inmediata (minutos): libera inodos sin empeorar la situación

Tu trabajo durante un incidente no es «hacerlo bonito». Es «hacerlo escribible otra vez» sin borrar lo incorrecto.
Aquí lo que suele ser seguro y efectivo, en orden descendente de sentido:

  • Eliminar caches efímeros conocidos (caché de compilación, caché de paquetes, archivos temporales) con comandos diseñados para ello.
  • Podar basura de contenedores si el host está cargado de contenedores y puedes tolerar eliminar artefactos no usados.
  • Expirar archivos generados por aplicaciones por antigüedad, no por suposiciones. Prefiere políticas «más antiguos que N días».
  • Mover directorios fuera del sistema de archivos si eliminar es arriesgado: archiva a otro montaje y luego borra localmente.

Cuando sea por logs: arregla el problema de cantidad de archivos, no solo la rotación

Logrotate resuelve «un archivo que crece sin parar». No resuelve automáticamente «creamos un archivo por petición».
Si tu aplicación crea archivos de log únicos por unidad de trabajo (ID de request, job, tenant), te estás causando un ataque de denegación de servicio distribuido
contra tu propia tabla de inodos.

Prefiere:

  • registro en una única secuencia con campos estructurados (JSON está bien, pero mantén la coherencia)
  • integración con journald cuando proceda
  • spooling local acotado con retención explícita

Cuando sea Docker: elige un data root que coincida con la carga

Docker sobre ext4 puede funcionar bien, hasta que no. Si sabes que un nodo va a construir imágenes, ejecutar muchos contenedores
y triturar capas, trata /var/lib/docker como un datastore de alta rotación y dale su propio sistema de archivos.

Opciones prácticas:

  • Montaje separado para /var/lib/docker con una densidad de inodos que coincida con el conteo esperado de archivos.
  • Limpieza de caché de compilación programada, no sólo manual durante incidentes.
  • Arreglar las builds de imágenes para reducir fan-out: multi-stage builds, recortar dependencias, usar .dockerignore.

Arreglo permanente: diseña el sistema de archivos para la carga

Si el agotamiento de inodos se repite, no tienes un «problema de limpieza». Tienes un problema de planificación de capacidad.
En ext4, el conteo de inodos se fija al crear el sistema de archivos. La única solución real es migrar a un sistema con más inodos
(o una disposición distinta), lo que implica:

  • crear un nuevo sistema de archivos con mayor densidad de inodos
  • mover datos
  • actualizar montajes y servicios
  • añadir monitorización y políticas de retención

Cómo crear ext4 con más inodos (migración planificada)

El conteo de inodos en ext4 se ve influido por -i (bytes por inodo) y -N (conteo explícito de inodos).
Menos bytes por inodo significa más inodos. Más inodos significa más sobrecarga de metadatos. Es un intercambio, no caramelos gratis.

cr0x@server:~$ sudo mkfs.ext4 -i 16384 /dev/sdc1
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 976754176 4k blocks and 61079552 inodes
Filesystem UUID: 9f1f4a1c-8b1d-4c1b-9d88-8d1aa14d4e1e
Superblock backups stored on blocks:
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

Significado: Este ejemplo crea muchos más inodos que la ratio por defecto en el mismo tamaño de volumen.
Decisión: Usa esto sólo cuando sepas que necesitas muchos archivos. Para datos secuenciales grandes, no desperdicies metadatos.

Broma #2: Si tratas el sistema de archivos como una base de datos, eventualmente te cobrará en inodos.

Errores comunes: síntoma → causa raíz → arreglo

1) «df muestra 30% usado pero no puedo escribir archivos» → agotamiento de inodos → comprobar y eliminar puntos calientes de archivos pequeños

  • Síntoma: las escrituras fallan, touch falla, apt falla, servicios se caen al crear archivos temporales.
  • Causa raíz: df -i muestra 100% de uso de inodos en el montaje.
  • Arreglo: identifica directorios con mayor conteo de archivos con du --inodes / find ... | wc -l, elimina archivos efímeros seguros y evita recurrencias.

2) «Eliminé un archivo enorme, sigue roto» → liberaste bloques, no inodos → elimina muchos archivos en su lugar

  • Síntoma: los GB libres aumentan, pero «No space left» continúa.
  • Causa raíz: el uso de inodos no cambió.
  • Arreglo: libera inodos eliminando por cantidad de archivos, no por tamaño. Apunta a caches, spools y artefactos de compilación.

3) «Sólo root puede escribir; usuarios no» → bloques reservados o cuotas → verificar con tune2fs y herramientas de cuotas

  • Síntoma: root puede crear archivos, no-root no.
  • Causa raíz: porcentaje de bloques reservados en ext4, o cuotas de usuario alcanzadas.
  • Arreglo: revisa bloques reservados con tune2fs; revisa cuotas; ajusta con cuidado. No pongas 0% de bloques reservados en particiones del sistema a ciegas.

4) «Se volvió de solo lectura y ahora todo falla» → errores del sistema de archivos → investigar dmesg, ejecutar fsck (offline)

  • Síntoma: el kernel remontó con errors=remount-ro, las escrituras fallan con errores de solo lectura.
  • Causa raíz: errores de I/O o corrupción del sistema de archivos, no capacidad.
  • Arreglo: inspecciona dmesg; planifica un reinicio en modo recuperación y ejecuta fsck. La limpieza de capacidad no arreglará la corrupción.

5) «Nodo Kubernetes con DiskPressure pero df parece bien» → presión de inodos del runtime de contenedores → podar y separar montajes

  • Síntoma: pods expulsados; kubelet se queja; nodo inestable.
  • Causa raíz: directorios del runtime llenan el presupuesto de inodos (overlay2, logs).
  • Arreglo: podar almacenamiento del runtime, aplicar recolección de basura de imágenes, poner el runtime en un volumen dedicado con monitorización.

6) «Limpiamos /tmp; ayudó una hora» → la aplicación recrea la tormenta → arregla la retención en la fuente

  • Síntoma: incidentes de inodos repetidos tras la limpieza.
  • Causa raíz: bug de la aplicación, diseño malo (un archivo por evento) o falta de TTL/rotación.
  • Arreglo: añade política de retención, rediseña el almacenamiento (base de datos/cola/objeto), aplica límites y alertas.

Listas de verificación / plan paso a paso

Checklist de guardia (estabilizar en 15–30 minutos)

  1. Ejecuta df -hT y df -iT para el montaje afectado.
  2. Confirma con touch en la ruta afectada.
  3. Encuentra el mapeo de montaje con findmnt -T.
  4. Identifica los mayores consumidores de inodos con du -x --inodes --max-depth=2 y find ... | wc -l dirigidos.
  5. Elige una acción de limpieza que sea segura y de alto impacto (prune de Docker, limpieza de caches, eliminación por tiempo).
  6. Revisa df -i hasta que estés por debajo de ~90% en el montaje crítico.
  7. Reinicia servicios impactados (sólo después de que las escrituras funcionen).
  8. Captura evidencia: comandos ejecutados, conteos de inodos antes/después, directorios responsables.

Checklist de ingeniería (prevenir recurrencias)

  1. Añade monitorización y alertas de inodos por sistema de archivos (no sólo uso de disco en %).
  2. Pon directorios de alta rotación en montajes dedicados: /var/lib/docker, spool de aplicaciones, caché de compilación.
  3. Implementa retención en el productor: TTLs, límites, compactación periódica o un backend distinto.
  4. Revisa builds e imágenes: reduce el conteo de archivos por capa; evita vendorizar enormes árboles de dependencias en imagenes de runtime.
  5. Si usas ext4 para cargas de pequeños archivos, planifica la densidad de inodos al formatear y documenta la decisión.
  6. Realiza un game day en staging: simula presión de inodos y valida alertas y pasos de recuperación.

Checklist de migración (cuando el conteo de inodos de ext4 es fundamentalmente incorrecto)

  1. Mide el conteo actual de archivos y la tasa de crecimiento (nuevos archivos diarios, comportamiento de retención).
  2. Elige el objetivo: ext4 con mayor densidad de inodos, XFS u otra arquitectura (almacenamiento de objetos, base de datos, cola).
  3. Provisiona un nuevo volumen y sistema de archivos; móntalo en la ruta prevista.
  4. Detén la carga, copia datos (preservando dueño/permisos), valida y haz el corte.
  5. Rehabilita la carga con valores de retención por defecto habilitados desde el primer día.
  6. Deja guardarraíles: alertas, timers de limpieza y una política de límite duro.

Preguntas frecuentes

1) ¿Qué es un inodo en una frase?

Un inodo es un registro de metadatos del sistema de archivos que representa un archivo o directorio; necesitas un inodo libre para crear un nuevo archivo.

2) ¿Por qué Ubuntu dice «No space left on device» cuando df -h muestra espacio libre?

Porque «espacio» puede significar bytes (bloques) o metadatos de archivo (inodos). df -h muestra bloques; df -i muestra inodos.

3) ¿Cómo confirmo rápidamente el agotamiento de inodos?

Ejecuta df -i en el montaje y busca IUse% 100%, luego intenta touch para confirmar que la creación de archivos falla.

4) ¿Puedo aumentar el conteo de inodos en un ext4 existente?

No de forma práctica en caliente. El conteo de inodos de ext4 se define en la creación del sistema de archivos. La solución real es migrar a un sistema con más inodos.

5) ¿Por qué los contenedores hacen más probable el problema de inodos?

Las capas de imagen y los árboles de dependencias pueden contener un número enorme de archivos pequeños. El almacenamiento por overlay multiplica operaciones de metadatos, y las cachés de compilación se acumulan silenciosamente.

6) ¿Es seguro borrar un directorio grande?

A veces. Es más seguro eliminar archivos acotados por tiempo bajo una ruta efímera conocida que borrar un directorio que tu servicio espera. Prefiere eliminaciones dirigidas y verifica las configuraciones de servicio.

7) ¿Qué debo monitorizar para detectar esto temprano?

Monitoriza el uso de inodos por sistema de archivos (df -i) y alerta sobre crecimientos sostenidos y niveles críticos (por ejemplo, 85% y 95%), no sólo el % de disco usado.

8) Si me quedo sin inodos, ¿debo reiniciar?

Reiniciar no crea inodos. Puede limpiar temporalmente algunos archivos temporales, pero no es una solución y puede complicar la investigación. Libera inodos de forma deliberada.

9) ¿Por qué a veces afecta sólo a una aplicación?

Porque sólo las aplicaciones que necesitan crear archivos están bloqueadas. Los servicios de solo lectura pueden seguir funcionando hasta que intenten escribir logs, sockets o estado.

10) ¿Los logs de journald suelen causar agotamiento de inodos?

Menos comúnmente que los logs de «un archivo por evento». journald tiende a almacenar datos en archivos menos numerosos y más grandes, lo que presiona más por bloques que por inodos.

Siguientes pasos que deberías hacer

Si sólo adoptas un hábito: cuando veas «No space left on device», ejecuta df -i con la misma naturalidad que df -h.
El sistema de archivos tiene dos límites, y la producción no perdona cuál olvidaste monitorizar.

Pasos prácticos:

  1. Añade alertas de inodos en cada montaje persistente (especialmente / y el almacenamiento del runtime de contenedores).
  2. Mueve rutas de alta rotación a sistemas de archivos dedicados para que una carga defectuosa no deje inservible todo el nodo.
  3. Arregla el comportamiento del productor: retención, TTLs, menos archivos, mejor empaquetado de artefactos.
  4. Para ext4, planifica la densidad de inodos desde el inicio para cargas de archivos pequeños y documenta la razón.
  5. Realiza un game day pequeño: crea una tormenta controlada de inodos en staging, verifica alertas y el playbook de recuperación.

No necesitas más disco. Necesitas menos archivos, mejores controles de ciclo de vida y una disposición del sistema de archivos que coincida con la realidad en lugar de con suposiciones.

Seguridad futura de la CPU: ¿Se acabaron las sorpresas tipo Spectre?

Tu ticket de incidentes dice “CPU 40% más lenta después de parchear”. Tu ticket de seguridad dice “las mitigaciones deben permanecer activas”. Tu plan de capacidad dice “lol”. Entre esos tres está la realidad de la seguridad moderna de las CPUs: la próxima sorpresa no será exactamente como Spectre, pero rimará con ella.

Si gestionas sistemas en producción—especialmente multi-inquilino, de alto rendimiento o regulados—tu trabajo no es ganar una discusión sobre si la ejecución especulativa fue un error. Tu trabajo es mantener la flota lo suficientemente rápida, segura y depurable cuando ambos objetivos colisionan.

La respuesta al principio (y qué hacer con ella)

No, las sorpresas tipo Spectre no han terminado. Hemos pasado la fase de “todo arde” de 2018, pero la lección subyacente sigue: las características de rendimiento crean efectos colaterales medibles, y a los atacantes les encantan los efectos colaterales medibles. Las CPUs siguen optimizando agresivamente. El software sigue construyendo abstracciones sobre esas optimizaciones. La cadena de suministro sigue siendo compleja (firmware, microcode, hipervisores, kernels, librerías, compiladores). La superficie de ataque sigue siendo un objetivo móvil.

La buena noticia: ya no somos impotentes. El hardware ahora se envía con más perillas de mitigación, mejores valores por defecto y contratos más claros. Los kernels aprendieron nuevos trucos. Los proveedores cloud adoptaron patrones operativos que no involucran parcheos desesperados a las 3 a.m. La mala noticia: las mitigaciones no son “configura y olvida”. Son gestión de configuración, ciclo de vida y ingeniería de rendimiento.

Qué deberías hacer (opinión)

  • Deja de tratar las mitigaciones como un binario. Haz una política por carga de trabajo: multi-inquilino vs dedicado, exposición de navegador/JS vs solo servidor, cripto sensible vs caché sin estado.
  • Posee tu inventario de CPU/firmware. “Estamos parcheados” no tiene sentido sin versiones de microcode, versiones de kernel y mitigaciones habilitadas verificadas en cada clase de host.
  • Haz benchmarks con mitigaciones activas. No una vez. Continuamente. Vincúlalo a despliegues de kernel y microcode.
  • Prefiere aislamiento aburrido sobre palancas ingeniosas. Hosts dedicados, límites fuertes en VMs y deshabilitar SMT cuando sea necesario supera esperar que una bandera de microcode te salve.
  • Instrumenta el costo. Si no puedes explicar a dónde se fueron los ciclos (syscalls, cambios de contexto, mispredicciones de ramas, I/O), no puedes elegir mitigaciones de forma segura.

Una idea parafraseada de Gene Kim (reliability/operations): Los cambios rápidos y frecuentes son más seguros cuando tienes bucles de retroalimentación fuertes y puedes detectar y recuperar rápido. Así sobrevives a las sorpresas de seguridad: haz que el cambio sea rutinario, no heroico.

Qué cambió desde 2018: chips, kernels y cultura

Datos interesantes y contexto histórico (corto y concreto)

  1. 2018 obligó a la industria a hablar de microarquitectura como si importara. Antes de eso, muchos equipos de ops trataban los internos de la CPU como “magia del proveedor” y se enfocaban en tunear OS/aplicaciones.
  2. Las primeras mitigaciones fueron instrumentos rudimentarios. Las respuestas iniciales del kernel a menudo intercambiaban latencia por seguridad porque la alternativa era “no enviar nada”.
  3. Retpoline fue una estrategia de compilador, no una característica de hardware. Redujo ciertos riesgos de branch target injection sin depender únicamente del comportamiento del microcode.
  4. Hyper-threading (SMT) pasó de “rendimiento gratis” a “perilla de riesgo”. Algunos canales de filtración son peores cuando hilos hermanos comparten recursos del core.
  5. El microcode se convirtió en una dependencia operativa. Actualizar BIOS/firmware solía ser raro en flotas; ahora es una tarea recurrente, a veces entregada vía paquetes del OS.
  6. Los proveedores cloud cambiaron silenciosamente políticas de scheduling. Niveles de aislamiento, hosts dedicados y controles contra “vecinos ruidosos” de pronto tuvieron un ángulo de seguridad, no solo de rendimiento.
  7. La investigación de ataques se orientó hacia nuevos side channels. El timing de caché fue solo el comienzo; predictores, buffers y efectos de ejecución transitoria se hicieron tema común.
  8. La postura de seguridad empezó a incluir “regresiones de rendimiento como riesgo”. Una mitigación que reduce a la mitad el throughput puede forzar atajos inseguros de escalado o el aplazamiento de parches—ambos son fallos de seguridad.

El hardware mejoró en ser explícito

Las CPUs modernas incluyen más perillas y semánticas para el control de especulación. Eso no significa “arreglado”, significa “el contrato es menos implícito”. Algunas mitigaciones ahora son características arquitectónicas en lugar de hacks: barreras más claras, mejores semánticas de separación de privilegios y formas más previsibles de vaciar o particionar estado.

Pero el progreso del hardware es desigual. Diferentes generaciones de CPU, proveedores y SKUs varían ampliamente. No puedes tratar «Intel» o «AMD» como un comportamiento único. Incluso dentro de una familia de modelos, las revisiones de microcode pueden cambiar el comportamiento de mitigación y el rendimiento.

Los kernels aprendieron a negociar

Linux (y otros SO) aprendieron a detectar capacidades de CPU, aplicar mitigaciones condicionalmente y exponer el estado de formas que los operadores pueden auditar. Eso es importante. En 2018, muchos equipos básicamente togglaban flags de arranque y esperaban. Hoy puedes consultar: “¿Está IBRS activo?” “¿Está KPTI habilitado?” “¿Se considera SMT inseguro aquí?”—y hacerlo a escala.

Además, compiladores y runtimes cambiaron. Algunas mitigaciones viven en elecciones de generación de código, no solo en switches del kernel. Esa es una lección de confiabilidad: tu “plataforma” incluye toolchains.

Broma #1: La ejecución especulativa es como un becario que empieza tres tareas a la vez “para ser eficiente” y luego derrama café en producción. Rápido y sorprendentemente creativo.

Por qué “Spectre” es una clase, no un fallo

Cuando la gente pregunta si Spectre está “terminado”, a menudo quieren decir: “¿Hemos acabado con las vulnerabilidades de ejecución especulativa?” Eso es como preguntar si terminaste con “fallos en sistemas distribuidos”. Puedes cerrar un ticket. No cerraste la categoría.

El patrón básico

Los problemas tipo Spectre abusan de un desajuste entre el comportamiento arquitectónico (lo que la CPU promete que ocurrirá) y el comportamiento microarquitectónico (lo que realmente ocurre internamente para acelerar). La ejecución transitoria puede tocar datos que deberían ser inaccesibles y luego filtrar una pista sobre ellos mediante timing u otros side channels. La CPU luego “deshace” el estado arquitectónico, pero no puede deshacer la física. Las cachés se calentaron. Los predictores se entrenaron. Los buffers se llenaron. Un atacante ingenioso puede medir ese residuo.

Por qué las mitigaciones son complicadas

Mitigar es difícil porque:

  • Estás peleando contra la medición. Si el atacante puede medir unos nanosegundos consistentemente, tienes un problema—incluso si no ocurrió nada “mal” arquitectónicamente.
  • Las mitigaciones viven en múltiples capas. Características de hardware, microcode, kernel, hipervisor, compilador, librerías y a veces la propia aplicación.
  • Las cargas reaccionan de forma distinta. Una carga con muchas syscalls puede sufrir con ciertas mitigaciones del kernel; una carga CPU-bound puede notar poco.
  • Los modelos de amenaza difieren. El sandbox del navegador es distinto de un equipo HPC single-tenant y distinto de nodos Kubernetes compartidos.

“Lo parcheamos” no es un estado, es una afirmación

Operativamente, trata la seguridad tipo Spectre como la durabilidad de datos en almacenamiento: no la declaras, la verificas continuamente. La verificación debe ser barata, automatizable y ligada al control de cambios.

De dónde vendrán las próximas sorpresas

La próxima ola no necesariamente se llamará “Spectre vNext”, pero seguirá explotando el mismo meta-problema: las características de rendimiento de la CPU crean estado compartido, y el estado compartido filtra.

1) Predictores, buffers y estructuras compartidas “invisibles”

Las cachés son el side channel famoso. Los atacantes reales también se preocupan por predictores de rama, predictores de retorno, store buffers, line fill buffers, TLBs y otros estados microarquitectónicos que pueden ser influidos y medidos a través de límites de seguridad.

A medida que los chips agregan más ingenio (predictores más grandes, pipelines más profundos, issue más ancho), el número de lugares donde puede esconderse “estado residual” aumenta. Incluso si los proveedores añaden particionamiento, todavía hay transiciones: user→kernel, VM→hipervisor, container→container en el mismo host, process→process.

2) Cómputo heterogéneo y aceleradores

Las CPUs ahora comparten trabajo con GPUs, NPUs, DPUs y “enclaves de seguridad”. Eso cambia la superficie de side-channel. Algunos de estos componentes tienen sus propias cachés y agendas. Si crees que la ejecución especulativa es complicada, espera hasta tener que razonar sobre memoria compartida de la GPU y kernels multi-inquilino.

3) Cadena de suministro de firmware y deriva de configuración

Las mitigaciones a menudo dependen de microcode y configuraciones de firmware. Las flotas derivan. Alguien reemplaza una placa base, una actualización de BIOS revierte una configuración, o un proveedor envía un valor por defecto de “rendimiento” que vuelve a habilitar comportamiento arriesgado. Tu modelo de amenaza puede ser perfecto y aun así fallar porque tu inventario es ficción.

4) Presión cross-tenant en la nube

La realidad del negocio: la multi-tenancy paga las facturas. Ahí es exactamente donde importan los side channels. Si operas nodos compartidos, debes asumir vecinos curiosos. Si operas hardware single-tenant, aún debes preocuparte por escapes de sandbox, exposición de navegador o cargas maliciosas que tú mismo ejecutas (hola, CI/CD).

5) El “impuesto de mitigación” que provoca conductas inseguras

Este es el modo de fallo poco discutido: mitigaciones que dañan el rendimiento pueden empujar a los equipos a desactivarlas, retrasar parches o sobrecomprometer nodos para cumplir SLOs. Así obtienes deuda de seguridad con intereses. La próxima sorpresa podría ser organizacional, no microarquitectónica.

Broma #2: Nada motiva un formulario de “aceptación de riesgo” como una regresión de rendimiento del 20% y un cierre de trimestre.

Modelos de riesgo que realmente encajan con producción

Empieza por límites, no por nombres de CVE

Los problemas tipo Spectre tratan de filtraciones a través de límites. Así que mapea tu entorno por límites:

  • Usuario ↔ kernel (usuarios locales no confiables, procesos sandboxed, rutas de escape de containers)
  • VM ↔ hypervisor (virtualización multi-inquilino)
  • Proceso ↔ proceso (host compartido con dominios de confianza distintos)
  • Hilo ↔ hilo (siblings de SMT)
  • Host ↔ host (menos directo, pero piensa en cachés compartidas en algunos diseños, offloads de NIC o side channels de almacenamiento compartido)

Tres posturas comunes en producción

Postura A: “Ejecutamos código no confiable” (mitigaciones más fuertes)

Ejemplos: nube pública, runners CI para colaboradores externos, granjas de render frente a navegadores, hosts de plugins, PaaS multi-inquilino. Aquí, no te pongas creativo. Habilita mitigaciones por defecto. Considera deshabilitar SMT en nodos compartidos. Considera hosts dedicados para tenants sensibles. Estás comprando reducción de probabilidad de divulgación de datos entre tenants.

Postura B: “Ejecutamos código semi-confiable” (balanceado)

Ejemplos: Kubernetes interno con muchos equipos, clusters analíticos compartidos, bases de datos multi-inquilino. Te importa el movimiento lateral y la exposición accidental. Las mitigaciones deberían mantenerse activas, pero puedes usar niveles de aislamiento: cargas sensibles en nodos más estrictos, cargas generales en otros. Las decisiones sobre SMT deben ser específicas por carga.

Postura C: “Ejecutamos código confiable en hardware dedicado” (aún no es gratis)

Ejemplos: cajas DB dedicadas, appliances de propósito único, HPC. Puedes aceptar cierto riesgo por rendimiento, pero cuidado con dos trampas: (1) navegadores y runtimes JIT pueden introducir comportamiento “poco confiable”, y (2) la amenaza interna y la cadena de suministro son reales. Si deshabilitas mitigaciones, documenta la decisión, aísla el sistema y verifica continuamente que siga aislado.

Haz la política ejecutable

Una política en una wiki es un cuento para dormir. Una política en automatización es un control. Quieres:

  • Etiquetas de nodo (por ejemplo, “smt_off_required”, “mitigations_strict”)
  • Perfiles de parámetros de arranque gestionados por config management
  • Comprobaciones de cumplimiento continuas: versión de microcode, flags del kernel, estado de vulnerabilidad
  • Gates de regresión de rendimiento para despliegues de kernel/microcode

Tareas prácticas: auditar, verificar y elegir mitigaciones (con comandos)

Estas no son teóricas. Son los tipos de comprobaciones que ejecutas durante un incidente, un despliegue o una auditoría de cumplimiento. Cada tarea incluye: comando, salida de ejemplo, qué significa y la decisión que tomas.

Task 1: Comprobar el estado de vulnerabilidad reportado por el kernel

cr0x@server:~$ grep . /sys/devices/system/cpu/vulnerabilities/*
/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/itlb_multihit:KVM: Mitigation: VMX disabled
/sys/devices/system/cpu/vulnerabilities/l1tf:Mitigation: PTE Inversion; VMX: conditional cache flushes, SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/mds:Mitigation: Clear CPU buffers; SMT vulnerable
/sys/devices/system/cpu/vulnerabilities/meltdown:Mitigation: PTI
/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Mitigation: Clear CPU buffers; SMT Host state unknown
/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected
/sys/devices/system/cpu/vulnerabilities/retbleed:Mitigation: IBRS
/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Mitigation: Safe RET
/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl and seccomp
/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization
/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional
/sys/devices/system/cpu/vulnerabilities/srbds:Mitigation: Microcode
/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected

Qué significa: El kernel te dice qué mitigaciones están activas y dónde queda riesgo (notablemente líneas que incluyen “SMT vulnerable” o “Host state unknown”).

Decisión: Si ejecutas multi-inquilino o código no confiable y ves “SMT vulnerable”, escala para considerar deshabilitar SMT o aplicar aislamiento más estricto en esos nodos.

Task 2: Confirmar el estado de SMT (hyper-threading)

cr0x@server:~$ cat /sys/devices/system/cpu/smt/active
1

Qué significa: 1 significa SMT activo; 0 significa deshabilitado.

Decisión: En nodos compartidos que manejan cargas no confiables, prefiere 0 a menos que tengas una razón cuantificada para no hacerlo. En cajas dedicadas single-tenant, decide según la carga y tolerancia al riesgo.

Task 3: Ver qué mitigaciones arrancó el kernel

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.6.15 root=UUID=... ro mitigations=auto,nosmt spectre_v2=on

Qué significa: Los parámetros del kernel definen comportamientos de alto nivel. mitigations=auto,nosmt solicita mitigaciones automáticas mientras deshabilita SMT.

Decisión: Trata esto como el estado deseado. Luego verifica el estado real vía /sys/devices/system/cpu/vulnerabilities/* porque algunas banderas se ignoran si no son soportadas.

Task 4: Verificar la revisión de microcode cargada

cr0x@server:~$ dmesg | grep -i microcode | tail -n 5
[    0.612345] microcode: Current revision: 0x000000f6
[    0.612346] microcode: Updated early from: 0x000000e2
[    1.234567] microcode: Microcode Update Driver: v2.2.

Qué significa: Puedes ver si el microcode fue actualizado temprano y qué revisión está activa.

Decisión: Si la flota tiene revisiones mixtas en el mismo modelo de CPU, tienes deriva. Arregla la deriva antes de debatir rendimiento. Microcode mixto igual a comportamiento mixto.

Task 5: Correlacionar modelo de CPU y stepping (porque importa)

cr0x@server:~$ lscpu | egrep 'Model name|Vendor ID|CPU family|Model:|Stepping:|Flags'
Vendor ID:                       GenuineIntel
Model name:                      Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz
CPU family:                      6
Model:                           106
Stepping:                        6
Flags:                           fpu vme de pse tsc ... ssbd ibrs ibpb stibp arch_capabilities

Qué significa: Flags como ibrs, ibpb, stibp, ssbd y arch_capabilities indican qué mecanismos de mitigación existen.

Decisión: Usa esto para segmentar clases de host. No despliegues el mismo perfil de mitigación a CPUs con capacidades fundamentalmente diferentes sin medir.

Task 6: Validar estado de KPTI / PTI (relacionado con Meltdown)

cr0x@server:~$ dmesg | egrep -i 'pti|kpti|page table isolation' | tail -n 5
[    0.000000] Kernel/User page tables isolation: enabled

Qué significa: PTI está habilitado. Eso típicamente incrementa la sobrecarga de syscalls en sistemas afectados.

Decisión: Si ves latencia repentina en cargas con muchas syscalls, PTI es sospechoso. Pero no lo desactives a la ligera; prefiere actualizar hardware donde sea menos costoso o innecesario.

Task 7: Comprobar detalles del modo de mitigación de Spectre v2

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/spectre_v2
Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling, STIBP: conditional

Qué significa: El kernel escogió una mezcla específica. “Conditional” a menudo significa que el kernel lo aplica en cambios de contexto o cuando detecta transiciones riesgosas.

Decisión: Si operas trading de baja latencia o RPCs de alta frecuencia, mide los costos en cambios de contexto y considera upgrades de CPU o niveles de aislamiento en lugar de desactivar mitigaciones globalmente.

Task 8: Confirmar si el kernel considera que SMT es seguro para problemas tipo MDS

cr0x@server:~$ cat /sys/devices/system/cpu/vulnerabilities/mds
Mitigation: Clear CPU buffers; SMT vulnerable

Qué significa: Limpiar buffers CPU ayuda, pero SMT aún deja vías de exposición que el kernel señala.

Decisión: Para hosts multi-inquilino, esto es una señal fuerte para deshabilitar SMT o pasar a tenancy dedicada.

Task 9: Medir cambio de contexto y presión de syscalls rápidamente

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 842112  52124 912340    0    0    12    33  820 1600 12  6 82  0  0
 3  0      0 841900  52124 912500    0    0     0     4 1100 4200 28 14 58  0  0
 4  0      0 841880  52124 912600    0    0     0     0 1300 6100 35 18 47  0  0
 1  0      0 841870  52124 912650    0    0     0     0  900 2000 18  8 74  0  0

Qué significa: Observa cs (cambios de contexto) y sy (CPU del kernel). Si cs se dispara y sy crece después de cambios en mitigaciones, encontraste dónde cae el impuesto.

Decisión: Considera reducir la tasa de syscalls (batching, I/O asíncrono, menos procesos) o mover esa carga a CPUs más nuevas con mitigaciones más baratas.

Task 10: Detectar sobrecarga relacionada con mitigaciones vía perf (alto nivel)

cr0x@server:~$ sudo perf stat -a -- sleep 5
 Performance counter stats for 'system wide':

        24,118.32 msec cpu-clock                 #    4.823 CPUs utilized
     1,204,883,112      context-switches         #   49.953 K/sec
        18,992,114      cpu-migrations           #  787.471 /sec
         2,113,992      page-faults              #  87.624 /sec
  62,901,223,111,222      cycles                 #    2.608 GHz
  43,118,441,902,112      instructions           #    0.69  insn per cycle
     9,882,991,443      branches                #  409.687 M/sec
       412,888,120      branch-misses           #    4.18% of all branches

       5.000904564 seconds time elapsed

Qué significa: Un IPC bajo y mispredicciones de rama elevadas pueden correlacionar con barreras de especulación y efectos en predictores, aunque no es una prueba por sí sola.

Decisión: Si las mispredicciones de rama aumentan tras un despliegue de mitigaciones, no adivines. Reproduce en staging y compara con una pareja base de kernel/microcode.

Task 11: Comprobar si KVM está en juego y qué reporta

cr0x@server:~$ lsmod | grep -E '^kvm|^kvm_intel|^kvm_amd'
kvm_intel             372736  0
kvm                  1032192  1 kvm_intel

Qué significa: El host es un hipervisor. Los controles de especulación pueden aplicarse en entradas/salidas de VM, y algunas vulnerabilidades exponen riesgo entre VMs.

Decisión: Trata esta clase de host como de mayor sensibilidad. Evita toggles de “rendimiento” personalizados a menos que puedas demostrar que la seguridad cross-VM se mantiene.

Task 12: Confirmar paquetes de microcode instalados (ejemplo Debian/Ubuntu)

cr0x@server:~$ dpkg -l | egrep 'intel-microcode|amd64-microcode'
ii  intel-microcode  3.20231114.1ubuntu1  amd64  Processor microcode firmware for Intel CPUs

Qué significa: El microcode gestionado por el OS está presente y versionado, lo que facilita actualizaciones de flota respecto a enfoques solo BIOS.

Decisión: Si el microcode solo viene vía BIOS y no tienes un pipeline de firmware, te vas a quedar atrás en mitigaciones. Construye ese pipeline.

Task 13: Confirmar paquetes de microcode instalados (ejemplo RHEL)

cr0x@server:~$ rpm -qa | egrep '^microcode_ctl|^linux-firmware'
microcode_ctl-20240109-1.el9.x86_64
linux-firmware-20240115-2.el9.noarch

Qué significa: La entrega de microcode forma parte del parcheo del OS, con su propia cadencia.

Decisión: Trata las actualizaciones de microcode como actualizaciones de kernel: despliegue por etapas, canary y comprobaciones de regresión de rendimiento.

Task 14: Validar si las mitigaciones fueron deshabilitadas (intencional o accidentalmente)

cr0x@server:~$ grep -Eo 'mitigations=[^ ]+|nospectre_v[0-9]+|spectre_v[0-9]+=[^ ]+|nopti|nosmt' /proc/cmdline
mitigations=off
nopti

Qué significa: Este host está corriendo con mitigaciones explícitamente deshabilitadas. Eso no es un “tal vez”. Es una elección.

Decisión: Si esto no es un entorno dedicado e aislado con aceptación de riesgo documentada, trátalo como un incidente de seguridad (o al menos una infracción de cumplimiento) y remédialo.

Task 15: Cuantificar la delta de rendimiento con seguridad (A/B boot de kernel)

cr0x@server:~$ sudo systemctl reboot --boot-loader-entry=auto-mitigations
Failed to reboot: Boot loader entry not supported

Qué significa: No todos los entornos soportan cambio fácil de entrada de arranque. Puede que necesites otro enfoque (perfiles GRUB, kexec o hosts canario dedicados).

Decisión: Construye un mecanismo canario reproducible. Si no puedes probar A/B combos de kernel+microcode, discutirás sobre rendimiento para siempre.

Task 16: Comprobar kernel en tiempo real vs kernel genérico (sensibilidad de latencia)

cr0x@server:~$ uname -a
Linux server 6.6.15-rt14 #1 SMP PREEMPT_RT x86_64 GNU/Linux

Qué significa: PREEMPT_RT o kernels de baja latencia interactúan distinto con la sobrecarga de mitigaciones porque el comportamiento de scheduling y preempción cambia.

Decisión: Si ejecutas cargas RT, prueba mitigaciones en kernels RT específicamente. No saques conclusiones de kernels genéricos.

Guía de diagnóstico rápido

Esto es para el día en que parcheas un kernel o microcode y tus dashboards SLO se vuelven arte moderno.

Primero: prueba si la regresión está relacionada con mitigaciones

  1. Comprueba el estado de mitigaciones rápidamente: grep . /sys/devices/system/cpu/vulnerabilities/*. Busca redacciones cambiadas respecto al último estado conocido bueno.
  2. Comprueba flags de arranque: cat /proc/cmdline. Confirma que no heredaste mitigations=off o que no añadiste flags más estrictos en una imagen nueva.
  3. Comprueba revisión de microcode: dmesg | grep -i microcode. Un cambio de microcode puede alterar comportamiento sin cambio de kernel.

Segundo: localiza el costo (¿dónde se fue la CPU?)

  1. Presión de syscall/cambio de contexto: vmstat 1. Si sy y cs suben, mitigaciones que afectan cruces al kernel son sospechosas.
  2. Agitación del scheduler: comprueba migraciones y presión de runqueue. Alta cpu-migrations en perf stat o r elevado en vmstat apunta a interacciones con el scheduler.
  3. Síntomas de rama/predictor: perf stat centrado en branch misses e IPC. No es definitivo, pero es una brújula útil.

Tercero: aisla variables y elige la solución menos mala

  1. Canaryea una sola clase de host: mismo modelo de CPU, misma carga, misma forma de tráfico. Cambia solo una variable: kernel o microcode, no ambas.
  2. Compara políticas “estrictas” vs “auto”: si debes ajustar, hazlo por pool de nodos, no globalmente.
  3. Prefiere soluciones estructurales: nodos dedicados para cargas sensibles, reduce cruces al kernel, evita modelos de hilos de alta agitación, fija procesos críticos en CPU.

Si no puedes responder “¿qué transición se volvió más lenta?” (user→kernel, VM→host, hilo→hilo), no estás diagnosticando; estás negociando con la física.

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición errónea

Una compañía SaaS mediana ejecutaba una flota mixta: servidores más nuevos para bases de datos, nodos más antiguos para batch y un gran cluster Kubernetes para “todo lo demás”. Tras una sprint de seguridad, habilitaron un perfil de mitigación más estricto en el pool de Kubernetes. Parecía limpio en el config management: una configuración, un despliegue, un check verde.

Luego, la latencia de la API orientada al cliente empezó a subir durante dos días. No fue un precipicio—peor: una degradación lenta que hizo que la gente discutiera: “Es el código”, “Es la DB”, “Es la red”, “Es el load balancer”. Clásico.

La suposición errónea fue simple: asumieron que todos los nodos del pool tenían el mismo comportamiento de CPU. En realidad, el pool tenía dos generaciones de CPU. En una generación, el modo de mitigación se apoyaba mucho en transiciones más caras, y la carga de la API resultó ser syscall-heavy por una librería de logging y configuraciones TLS que incrementaban cruces al kernel. En la generación más nueva, las mismas configuraciones eran mucho más baratas.

Lo descubrieron solo al comparar salidas de /sys/devices/system/cpu/vulnerabilities/spectre_v2 entre nodos y notar distintas cadenas de mitigación en nodos “idénticos”. Las revisiones de microcode también estaban desparejas porque algunos servidores tenían microcode gestionado por el OS y otros dependían de BIOS que nunca se programó.

La solución no fue “apagar mitigaciones”. Separaron el pool por modelo de CPU y baseline de microcode, y reequilibraron cargas: pods API syscall-heavy se movieron al pool más nuevo. También construyeron una comprobación de cumplimiento de microcode en la admisión de nodos.

La lección: cuando tu riesgo y rendimiento dependen de la microarquitectura, pools homogéneos no son un lujo. Son un control.

Mini-historia 2: La optimización que salió mal

Un equipo fintech perseguía tail latency en un servicio de pricing. Hicieron todo lo esperado: fijaron hilos, ajustaron colas NIC, redujeron asignaciones y sacaron hot paths del kernel donde fue posible. Luego se volvieron audaces. Deshabilitaron SMT con la teoría de que menos recursos compartidos reducirían jitter. Ayudó un poco.

Animados, dieron el siguiente paso: intentaron aflojar ciertas mitigaciones en un entorno dedicado. Al fin y al cabo, el sistema era “single tenant”. El rendimiento mejoró en benchmarks sintéticos y se sintieron astutos. Lo desplegaron en producción con una nota de aceptación de riesgo.

Dos meses después, un proyecto separado reutilizó la misma imagen de host para ejecutar jobs CI de repositorios internos. “Interno” rápidamente se volvió “semi-confiable”, porque existen contratistas y dependencias externas. Las cargas CI eran ruidosas, intensivas en JIT y peligrosamente cercanas al proceso de pricing en términos de scheduling. No hubo explotación (que se sepa), pero una revisión de seguridad marcó la incompatibilidad: la imagen asumía un modelo de amenaza que ya no era cierto.

Peor aún, cuando volvieron a habilitar mitigaciones, la regresión de rendimiento fue más aguda de lo esperado. El tuning del sistema se había vuelto dependiente de las configuraciones relajadas anteriores: más hilos, más cambios de contexto y algunas asunciones de “fast path”. Se habían optimizado hasta quedarse sin salida.

La solución fue aburrida y cara: pools e imágenes de host separadas. Pricing corrió en nodos dedicados y estrictos. CI corrió en otro lado con distinto aislamiento y expectativas de rendimiento. También empezaron a tratar las configuraciones de mitigación como parte de la “API” entre plataforma y equipos de aplicación.

La lección: las optimizaciones que cambian la postura de seguridad tienden a reutilizarse fuera de contexto. Las imágenes se propagan. El riesgo también.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una gran empresa ejecutaba una nube privada con varios proveedores de hardware y ciclos de vida de servidores largos. Vivían en el mundo real: ciclos de compras, ventanas de mantenimiento, apps legacy y auditores de cumplimiento que prefieren papeleo más que uptime.

Tras 2018, hicieron algo dolorosamente poco sexy: construyeron una canalización de inventario. Cada host reportaba modelo de CPU, revisión de microcode, versión de kernel, parámetros de arranque y el contenido de /sys/devices/system/cpu/vulnerabilities/*. Estos datos alimentaban un dashboard y un motor de políticas. Los nodos que derivaban fuera de cumplimiento quedaban acotados en Kubernetes o drenados en su scheduler de VMs.

Años después, una nueva actualización de microcode introdujo un cambio de rendimiento medible en un subconjunto de hosts. Porque tenían inventario y canarios, lo detectaron en horas. Porque tenían clases de host, el radio de impacto quedó contenido. Porque tenían un camino de rollback, se recuperaron antes de que el impacto al cliente fuera titular.

El rastro de auditoría también importó. Seguridad preguntó, “¿Qué nodos siguen vulnerables en este modo?” Respondieron con una consulta, no con una reunión.

La lección: lo opuesto a la sorpresa no es la predicción. Es observabilidad más control.

Errores comunes: síntoma → causa raíz → arreglo

1) Síntoma: “La CPU está alta después de parchear”

  • Causa raíz: Más tiempo en transiciones al kernel (PTI/KPTI, barreras de especulación), a menudo amplificado por cargas con muchas syscalls.
  • Arreglo: Mide vmstat (sy, cs), reduce la tasa de syscalls (batching, I/O asíncrono), actualiza a CPUs con mitigaciones más baratas o aisla la carga a la clase de nodo adecuada.

2) Síntoma: “La tail latency explotó, el promedio está bien”

  • Causa raíz: Mitigaciones condicionales en cambios de contexto interactuando con churn del scheduler; contención entre siblings SMT; vecinos ruidosos.
  • Arreglo: Deshabilitar SMT para pools sensibles, fijar hilos críticos, reducir migraciones y separar cargas ruidosas. Valida con perf stat y métricas del scheduler.

3) Síntoma: “Algunos nodos son rápidos, otros lentos, misma imagen”

  • Causa raíz: Deriva de microcode y steppings mixtos; el kernel selecciona rutas de mitigación distintas.
  • Arreglo: Aplicar baselines de microcode, segmentar pools por modelo/stepping de CPU y hacer que el estado de mitigación sea parte de la readiness del nodo.

4) Síntoma: “El escaneo de seguridad dice vulnerable, pero lo parcheamos”

  • Causa raíz: Parche aplicado solo a nivel OS; falta firmware/microcode; o mitigaciones deshabilitadas vía parámetros de arranque.
  • Arreglo: Verifica vía /sys/devices/system/cpu/vulnerabilities/* y revisión de microcode; remedia con paquetes de microcode o actualizaciones BIOS; elimina flags de arranque riesgosos.

5) Síntoma: “Las cargas VM se pusieron más lentas, el bare metal no”

  • Causa raíz: La sobrecarga de entrada/salida de VM aumentó por hooks de mitigación; el hipervisor aplica barreras más estrictas.
  • Arreglo: Mide la sobrecarga de virtualización; considera hosts dedicados, generaciones de CPU más nuevas o ajustar densidad de VM. Evita desactivar mitigaciones globalmente en hipervisores.

6) Síntoma: “Deshabilitamos mitigaciones y no pasó nada”

  • Causa raíz: Confundir ausencia de evidencia con evidencia de ausencia; el modelo de amenaza cambió en silencio después (nuevas cargas, nuevos tenants, nuevos runtimes).
  • Arreglo: Trata cambios de mitigación como una API sensible de seguridad. Requiere política explícita, garantías de aislamiento y revalidación periódica del modelo de amenaza.

Listas de verificación / plan paso a paso

Paso a paso: construye una postura tipo Spectre con la que puedas vivir

  1. Clasifica pools de nodos por límite de confianza. Multi-inquilino compartido, interno compartido, dedicado sensible, dedicado general.
  2. Inventaria CPU y microcode. Recopila lscpu, revisión de microcode desde dmesg y versión de kernel desde uname -r.
  3. Inventaria el estado de mitigación. Recopila /sys/devices/system/cpu/vulnerabilities/* por nodo y almacénalo centralmente.
  4. Define perfiles de mitigación. Para cada pool, especifica flags de arranque del kernel (por ejemplo, mitigations=auto, opcional nosmt) y baseline de microcode requerido.
  5. Haz ejecutable el cumplimiento. Nodos fuera de perfil no deberían aceptar cargas sensibles (cordon/drain, taints de scheduler o restricciones de placement de VM).
  6. Canaryea cada despliegue de kernel/microcode. Una clase de host a la vez; compara latencia, throughput y contadores de CPU.
  7. Haz benchmarks con formas de tráfico reales. Los microbenchmarks sintéticos fallan en patrones de syscall, comportamiento de caché y churn de allocators.
  8. Documenta aceptaciones de riesgo con caducidad. Si desactivas algo, ponle fecha de expiración y fuerza una re-aprobación.
  9. Entrena a los respondedores de incidentes. Añade la “Guía de diagnóstico rápido” al runbook on-call y haz simulacros.
  10. Planifica la renovación de hardware con la seguridad en mente. CPUs más nuevas pueden reducir el impuesto de mitigación; eso es un caso de negocio, no un lujo.

Lista de verificación: antes de deshabilitar SMT

  • Confirma si el kernel reporta “SMT vulnerable” para issues relevantes.
  • Mide la diferencia de rendimiento en cargas representativas.
  • Decide por pool, no por host.
  • Asegura margen de capacidad para la caída de throughput.
  • Actualiza reglas de scheduling para que las cargas sensibles aterricen en el pool previsto.

Lista de verificación: antes de relajar mitigaciones por rendimiento

  • ¿El sistema es realmente single-tenant end-to-end?
  • ¿Puede ejecutarse código no confiable (jobs CI, plugins, navegadores, runtimes JIT, scripts de clientes)?
  • ¿Es el host alcanzable por atacantes con ejecución local?
  • ¿Tienes hardware dedicado y control de acceso fuerte?
  • ¿Tienes un camino de rollback que no requiera un héroe?

Preguntas frecuentes

1) ¿Se acabaron las sorpresas tipo Spectre?

No. La onda de choque inicial terminó, pero la dinámica subyacente sigue: las características de rendimiento crean estado compartido, y el estado compartido filtra. Espera investigación continua y actualizaciones periódicas de mitigaciones.

2) Si mi kernel dice “Mitigation: …” ¿estoy a salvo?

Estás más seguro que si dice “Vulnerable”, pero “seguro” depende de tu modelo de amenaza. Atiende frases como “SMT vulnerable” y “Host state unknown”. Esos son el kernel diciéndote el riesgo restante.

3) ¿Debo deshabilitar SMT en todas partes?

No. Deshabilita SMT donde exista riesgo cross-tenant o ejecución de código no confiable y donde el kernel indique exposición relacionada con SMT. Mantén SMT donde el aislamiento de hardware y la confianza de la carga lo justifiquen, y donde hayas medido el beneficio.

4) ¿Es esto principalmente un problema cloud?

La nube multi-inquilino agudiza el modelo de amenaza, pero los side channels importan también on-prem: clusters compartidos, multi-tenancy interna, sistemas CI y cualquier entorno donde la “ejecución local” sea plausible.

5) ¿Cuál es el modo de fallo operativo más común?

Deriva: microcode mixto, CPUs mixtas y flags de arranque inconsistentes. Las flotas se vuelven un patchwork y terminas con riesgo desigual y rendimiento impredecible.

6) ¿Puedo confiar en el aislamiento de containers para protegerme?

Los containers comparten el kernel y los side channels no respetan namespaces. Los containers son excelentes para empaquetar y controlar recursos, no como límite de seguridad duro frente a filtraciones microarquitectónicas.

7) ¿Por qué las mitigaciones a veces dañan más la latencia que el throughput?

Porque muchas mitigaciones penalizan transiciones (cambios de contexto, syscalls, exits de VM). La tail latency es sensible al trabajo extra en la ruta crítica y a la interferencia del scheduler.

8) ¿Qué debería almacenar en mi CMDB o sistema de inventario?

Modelo/stepping de CPU, revisión de microcode, versión de kernel, parámetros de arranque, estado de SMT y el contenido de /sys/devices/system/cpu/vulnerabilities/*. Ese conjunto te permite responder la mayoría de preguntas de auditoría e incidentes rápidamente.

9) ¿Las CPUs nuevas son “inmunes”?

No. Las CPUs más nuevas suelen tener mejor soporte de mitigaciones y pueden reducir el coste de rendimiento, pero “inmune” es exagerado. La seguridad es un objetivo móvil y las nuevas funciones pueden introducir nuevas vías de filtración.

10) Si el rendimiento es crítico, ¿cuál es la mejor movida a largo plazo?

Compra donde importa: generaciones de CPU más nuevas, hosts dedicados para cargas sensibles y decisiones de arquitectura que reduzcan cruces al kernel. Desactivar mitigaciones raramente es una estrategia estable.

Siguientes pasos prácticos

Si quieres menos sorpresas, no busques predicción perfecta. Busca verificación rápida y despliegue controlado.

  1. Implementa auditoría continua de mitigaciones raspando /sys/devices/system/cpu/vulnerabilities/*, /proc/cmdline y revisión de microcode en tu pipeline de métricas.
  2. Separa pools por generación de CPU y baseline de microcode. La homogeneidad es una característica de rendimiento y un control de seguridad.
  3. Crea dos o tres perfiles de mitigación alineados con límites de confianza y hazlos cumplir por automatización (etiquetas de nodo, taints, reglas de placement).
  4. Construye un proceso canario para actualizaciones de kernel y microcode con benchmarks de cargas reales y seguimiento de tail latency.
  5. Decide tu postura sobre SMT explícitamente para cada pool, escríbelo y haz detectable la deriva.

La era de Spectre no terminó. Maduró. Los equipos que tratan la seguridad de la CPU como cualquier otro problema de sistemas en producción—inventario, canarios, observabilidad y controles aburridos—son los que duermen tranquilos.

VoIP sobre VPN: Evita audio robótico con MTU, jitter y fundamentos de QoS

Ya conoces ese sonido. La llamada comienza bien y luego alguien se transforma en una máquina de fax que hace casting para una película de robots.
Todos culpan “la VPN”, luego al ISP, después al softphone y finalmente a la fase lunar. Mientras tanto, el culpable real suele ser aburrido:
incompatibilidad MTU/MSS, jitter por bufferbloat o una QoS que no sobrevive al viaje por el túnel.

Administro redes de producción donde la voz es solo otra carga—hasta que deja de serlo. La voz castiga las suposiciones perezosas.
No le importa que tu prueba de throughput parezca fantástica; le importan los paquetes pequeños que llegan a tiempo, de forma constante, con mínima pérdida y sin reordenamientos.

Un modelo mental que realmente predice fallos

Si recuerdas una sola cosa: la voz es un flujo en tiempo real que viaja sobre una red de mejor esfuerzo. Una VPN añade cabeceras, oculta las marcas QoS internas a menos que las preserves deliberadamente, y puede alterar el ritmo de los paquetes.
Los modos de fallo habituales no son misteriosos. Son física y colas.

Qué suele ser “audio robótico”

“Robótico” rara vez es un problema de “calidad” del códec. Es la pérdida de paquetes y la ocultación en acción.
El audio RTP llega en paquetes pequeños (a menudo 20 ms de audio por paquete). Pierdes unos cuantos, el jitter se dispara, el buffer de jitter se estira, el decodificador adivina y oyes al robot.
La voz puede sobrevivir cierta pérdida; simplemente no la oculta con elegancia.

La pila VoIP-sobre-VPN en un diagrama (conceptual)

Piensa en el paquete como un conjunto anidado de sobres:

  • Interior: señalización SIP + medios RTP (a menudo UDP) con marcas DSCP que te gustaría conservar
  • Luego: tu envoltura VPN (WireGuard/IPsec/OpenVPN) añade overhead y puede cambiar la MTU
  • Exterior: colas del ISP e internet (donde vive el bufferbloat) y donde la QoS puede o no funcionar
  • Endpoints: softphone o teléfono IP, y una PBX/ITSP

La rotura suele ocurrir en uno de tres lugares:
(1) tamaño (MTU/fragmentación),
(2) temporización (jitter/colas),
(3) priorización (QoS/DSCP y shaping).

Parafraseando a W. Edwards Deming: “Sin datos, solo eres otra persona con una opinión.” Trata los problemas de voz como incidentes: mide, aísla, cambia una variable y vuelve a medir.

Guía rápida de diagnóstico

Cuando el CEO dice “las llamadas están rotas”, no empiezas debatiendo códecs. Empiezas limitando el radio afectado y localizando la cola.
Aquí está el orden que encuentra las causas raíz rápidamente.

Primero: confirma si es pérdida, jitter o MTU

  1. Revisa las estadísticas RTP en el cliente/PBX: % de pérdida, jitter, paquetes tardíos. Si no tienes esto, captura paquetes y calcúlalo (más tarde).
    Si ves incluso 1–2% de pérdida durante los momentos “robóticos”, trátalo como un problema de red hasta que se demuestre lo contrario.
  2. Ejecuta una prueba rápida de path MTU a través de la VPN. Si PMTUD está roto, obtendrás paquetes grandes que se pierden, especialmente en VPNs basadas en UDP.
  3. Revisa el retardo de cola bajo carga en el enlace más estrecho (generalmente la subida del usuario). El bufferbloat es el asesino silencioso de la voz.

Segundo: aísla dónde se rompe

  1. Evita la VPN para una llamada de prueba (split tunnel o política temporal). Si la voz mejora drásticamente, céntrate en el overhead del túnel, MTU y manejo de QoS en los bordes del túnel.
  2. Compara cableado vs Wi‑Fi. Si Wi‑Fi es peor, estás en terreno de contención de airtime y retransmisiones. Arregla eso por separado.
  3. Prueba desde una red conocida buena (un circuito de laboratorio, otro ISP o una VM en la nube con un softphone). Si eso está limpio, el problema está en el borde del usuario.

Tercero: aplica las “soluciones aburridas”

  • Configura explícitamente la MTU de la interfaz VPN y clampa TCP MSS donde sea relevante.
  • Aplica gestión de colas inteligente (fq_codel/cake) en el verdadero cuello de botella y haz shaping ligeramente por debajo de la tasa de línea.
  • Marca el tráfico de voz y priorízalo donde controles la cola (a menudo el borde WAN), no solo en tus sueños.

Broma #1: Una VPN es como una maleta—si sigues metiendo cabeceras extra, eventualmente la cremallera (MTU) se rinde en el peor momento.

MTU, MSS y fragmentación: por qué “robótico” a menudo significa “pérdida pequeña”

Los problemas de MTU no siempre parecen “no puede conectar”. Pueden parecer “conecta, pero a veces suena embrujado”.
Eso se debe a que la señalización puede sobrevivir mientras ciertos paquetes de medios o re-invites se pierden, o porque la fragmentación aumenta la sensibilidad a la pérdida.

Qué cambia cuando añades una VPN

Cada túnel añade overhead:

  • WireGuard añade una cabecera UDP/IP externa más overhead de WireGuard.
  • IPsec añade overhead ESP/AH (más posible encapsulado UDP para NAT‑T).
  • OpenVPN añade overhead en espacio de usuario y puede añadir enmarcado extra según el modo.

El paquete interior que antes cabía con MTU 1500 puede ahora no entrar. Si tu ruta no permite fragmentación como crees, algo se pierde.
Y UDP no retransmite; simplemente te decepciona en tiempo real.

Path MTU discovery (PMTUD) y cómo falla

PMTUD depende de mensajes ICMP “Fragmentation Needed” (para IPv4) o Packet Too Big (para IPv6). Muchas redes bloquean o limitan la tasa de ICMP.
Resultado: envías paquetes demasiado grandes, los routers los descartan y tu emisor nunca se entera. Eso se llama “PMTUD black hole”.

Por qué RTP usualmente no es “demasiado grande” — pero igual sufre

Los paquetes de voz RTP suelen ser pequeños: decenas a un par de centenas de bytes de payload, más cabeceras. Entonces, ¿por qué los problemas de MTU afectan las llamadas?

  • Señalización y cambios de sesión (SIP INVITE/200 OK con SDP, registros TLS) pueden ser grandes.
  • La encapsulación VPN puede fragmentar incluso paquetes moderados, aumentando la probabilidad de pérdida.
  • Los picos de jitter ocurren cuando fragmentación y reensamblado interactúan con colas congestionadas.
  • Algunos softphones agrupan o envían UDPs más grandes en ciertas configuraciones (comfort noise, SRTP o ptime inusual).

Orientación accionable

  • Para WireGuard, empieza con MTU 1420 si no estás seguro. No es magia; es un valor conservador que evita problemas comunes de overhead.
  • Para OpenVPN, sé explícito con la MTU del túnel y clampa MSS para los flujos TCP que atraviesan el túnel.
  • No “bajes la MTU en todas partes” a ciegas. Puedes arreglar una ruta y perjudicar otra. Mide y luego ajusta.

Jitter, bufferbloat y por qué las pruebas de velocidad engañan

Puedes tener 500 Mbps de bajada y aun así sonar como si llamaras desde un submarino. La voz necesita baja variación de latencia, no cifras para presumir.
El enemigo práctico más grande es el bufferbloat: colas sobredimensionadas en routers/modems que se llenan bajo carga y añaden cientos de milisegundos de retardo.

Jitter vs latencia vs pérdida de paquetes

  • Latencia: cuánto tarda un paquete de extremo a extremo.
  • Jitter: cuánto varía esa latencia paquete a paquete.
  • Pérdida: paquetes que nunca llegan (o llegan demasiado tarde para importar).

Los códecs de voz usan buffers de jitter. Esos buffers pueden suavizar la variación hasta cierto punto, a costa de añadir retardo.
Cuando el jitter se vuelve feo, los buffers o crecen (aumentando retardo) o tiran paquetes tardíos (aumentando pérdida). En ambos casos: audio robótico.

Dónde nace el jitter

La mayor parte del jitter en incidentes VoIP-sobre-VPN no es “internet”. Es la cola del borde:

  • Router doméstico del usuario con un buffer ascendente profundo
  • Cortafuegos de sucursal corporativa que hace inspección y buffering
  • CPU del concentrador VPN saturada causando retardo en la programación de paquetes
  • Contención/retransmisiones Wi‑Fi (parece jitter y pérdida)

Gestión de colas que realmente funciona

Si controlas el cuello de botella, puedes arreglar la voz.
Los algoritmos de gestión de colas inteligentes (SQM) como fq_codel y cake evitan activamente que las colas crezcan sin límite y mantienen la latencia estable bajo carga.

El truco: debes hacer shaping un poco por debajo de la tasa real del enlace para que tu equipo, no el módem del ISP, sea el cuello de botella y por tanto controle la cola.
Si no lo haces, estás pidiendo educadamente al módem que se comporte. No lo hará.

Broma #2: Bufferbloat es lo que pasa cuando tu router atesora paquetes como si fueran antigüedades coleccionables.

Fundamentos de QoS/DSCP para voz a través de VPNs (y qué se pierde)

QoS no es una casilla mágica de “mejorarlo”. Es una forma de decidir qué se sacrifica primero cuando el enlace está congestionado.
Eso es todo. Si no hay congestión, QoS no cambia nada.

DSCP y el mito de “QoS extremo a extremo”

La voz suele marcar RTP como DSCP EF (Expedited Forwarding) y SIP como CS3/AF31 según tu política.
Dentro de tu LAN eso puede ayudar. A través de internet la mayoría de proveedores lo ignorará. A través de una VPN, puede que ni siquiera sobreviva la encapsulación.

Qué puedes controlar

  • Borde LAN: prioriza la voz desde teléfonos/softphones hacia tu gateway VPN.
  • Egreso WAN del gateway VPN: aplica shaping y prioridad a los paquetes exteriores que corresponden a flujos de voz.
  • Borde de sucursal/usuario: si lo gestionas, despliega SQM y marca la voz localmente.

VPN en detalle: marcas internas vs externas

Muchas implementaciones de túnel encapsulan paquetes internos en un paquete exterior. El paquete exterior es lo que ve el ISP.
Si el paquete exterior no está marcado (o si está marcado pero luego se lo quitan), tu “EF” interno es solo decorativo.

El enfoque práctico:

  • Clasifica la voz antes de cifrar cuando sea posible, luego aplica prioridad al flujo cifrado (cabecera exterior) en el egress.
  • Preserva DSCP a través del túnel si tu equipo lo soporta y tu política lo permite.
  • No confíes en Wi‑Fi WMM para salvarte si la cola de subida se está derritiendo.

Precaución con QoS: puedes empeorarlo

Una política QoS mala puede dejar sin recursos el tráfico de control, o crear micro‑picos y reordenamientos. La voz necesita prioridad, pero también estabilidad.
Mantén las clases simples: voz, interactivo, bulk. Luego aplica shaping.

Tareas prácticas: comandos, salidas y decisiones

Estas son tareas “ejecuta ahora”. Cada una incluye un comando, qué te dice la salida y qué decisión tomar.
Úsalas en endpoints Linux, gateways VPN o hosts de troubleshooting. Ajusta nombres de interfaz e IPs para tu entorno.

Tarea 1: Confirma la MTU de la interfaz del túnel VPN

cr0x@server:~$ ip link show dev wg0
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none

Significado: wg0 MTU es 1420. Buen punto de partida conservador para WireGuard.
Decisión: Si la MTU es 1500 en un túnel, asume problemas a menos que hayas probado que la ruta lo soporta. Si el audio robótico se correlaciona con ciertas rutas, prueba MTU más bajas.

Tarea 2: Mide path MTU con ping “do not fragment” (IPv4)

cr0x@server:~$ ping -M do -s 1372 -c 3 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1372(1400) bytes of data.
1380 bytes from 10.20.30.40: icmp_seq=1 ttl=63 time=18.4 ms
1380 bytes from 10.20.30.40: icmp_seq=2 ttl=63 time=18.7 ms
1380 bytes from 10.20.30.40: icmp_seq=3 ttl=63 time=18.2 ms

--- 10.20.30.40 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms

Significado: El paquete de 1400 bytes (incluyendo cabeceras) pasa sin fragmentación.
Decisión: Aumenta -s hasta que falle para encontrar el máximo. Establece la MTU del túnel de forma segura por debajo de eso menos el overhead de encapsulación.

Tarea 3: Observa síntomas de fallo PMTUD (IPv4)

cr0x@server:~$ ping -M do -s 1472 -c 2 10.20.30.40
PING 10.20.30.40 (10.20.30.40) 1472(1500) bytes of data.
ping: local error: message too long, mtu=1420
ping: local error: message too long, mtu=1420

--- 10.20.30.40 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1013ms

Significado: La MTU de tu interfaz local es 1420; el kernel se niega a enviar más grande con DF activado.
Decisión: Si las apps envían paquetes más grandes de todos modos (la encapsulación UDP VPN puede hacerlo), clampa o configura MTU/MSS para que no lo hagan.

Tarea 4: Revisa reglas de clamp TCP MSS (iptables)

cr0x@server:~$ sudo iptables -t mangle -S | grep -i mss
-A FORWARD -o wg0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Significado: Los SYN TCP tienen MSS clamped según PMTU.
Decisión: Si llevas SIP sobre TCP/TLS por la VPN y ves stalls o retransmisiones, habilita esto. No arregla RTP (UDP), pero estabiliza la señalización.

Tarea 5: Verifica marcas DSCP en paquetes salientes

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp and portrange 10000-20000 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:10:41.112233 IP (tos 0xb8, ttl 63, id 44211, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
12:10:41.132244 IP (tos 0xb8, ttl 63, id 44212, offset 0, flags [DF], proto UDP (17), length 214) 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Significado: TOS 0xb8 corresponde a DSCP EF (46). Tu host está marcando RTP.
Decisión: Comprueba si la marca sobrevive la encapsulación y si tu cola WAN la respeta. Si desaparece en el paquete exterior, necesitas QoS en el egress del túnel, no esperanzas.

Tarea 6: Confirma DSCP en el paquete externo del VPN

cr0x@server:~$ sudo tcpdump -ni eth0 -vv udp port 51820 -c 5
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:12:03.220011 IP (tos 0x00, ttl 64, id 12001, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180
12:12:03.240022 IP (tos 0x00, ttl 64, id 12002, offset 0, flags [DF], proto UDP (17), length 208) 203.0.113.5.51820 > 203.0.113.9.51820: UDP, length 180

Significado: Los paquetes externos no están marcados (tos 0x00). Incluso si el RTP interno es EF, el ISP solo ve el exterior.
Decisión: Aplica clasificación en el gateway VPN: identifica flujos de voz antes del cifrado (o por heurísticas de puerto/peer) y establece DSCP/prioridad en egress.

Tarea 7: Identifica el verdadero cuello de botella y el qdisc actual

cr0x@server:~$ tc qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn

Significado: fq_codel está activo. Eso es una buena línea base para latencia bajo carga.
Decisión: Si ves pfifo_fast o un qdisc profundo del proveedor en el borde WAN, planifica desplegar shaping + fq_codel/cake donde ocurre la congestión.

Tarea 8: Revisa estadísticas qdisc por drops/overlimits (problemas de shaping)

cr0x@server:~$ tc -s qdisc show dev eth0
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn
 Sent 98234123 bytes 84521 pkt (dropped 213, overlimits 0 requeues 12)
 backlog 0b 0p requeues 12
  maxpacket 1514 drop_overlimit 213 new_flow_count 541 ecn_mark 0

Significado: Ocurrieron algunas caídas. Las caídas no siempre son malas—las caídas controladas pueden prevenir latencia masiva. Pero caídas + audio robótico sugiere que estás perdiendo RTP, no bulk.
Decisión: Añade clasificación para que la voz tenga prioridad (o al menos aislamiento), y asegúrate de que la tasa de shaping coincida con la subida real.

Tarea 9: Chequeo rápido de jitter y pérdida con mtr (línea base)

cr0x@server:~$ mtr -rwzc 50 203.0.113.9
Start: 2025-12-28T12:20:00+0000
HOST: server                          Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. 192.0.2.1                         0.0%    50    1.1   1.3   0.9   3.8   0.6
  2. 198.51.100.1                      0.0%    50    8.2   8.5   7.9  13.4   1.1
  3. 203.0.113.9                       0.0%    50   19.0  19.2  18.6  26.8   1.4

Significado: Sin pérdida, latencia estable, bajo jitter (StDev). Buena línea base.
Decisión: Si ves pérdida en el salto 1 bajo carga, es tu LAN/Wi‑Fi/router. Si la pérdida empieza más adelante, es upstream—aun así quizá solucionable con shaping en tu borde.

Tarea 10: Comprueba si la CPU del gateway VPN causa retardo en la programación de paquetes

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.5.0 (vpn-gw) 	12/28/2025 	_x86_64_	(8 CPU)

12:21:01     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
12:21:02     all   18.20    0.00   22.40    0.10    0.00   21.70    0.00    0.00    0.00   37.60
12:21:02       0   20.00    0.00   28.00    0.00    0.00   30.00    0.00    0.00    0.00   22.00

Significado: Softirq alto puede indicar procesamiento intensivo de paquetes (cifrado, forwarding).
Decisión: Si softirq está al máximo durante los problemas de llamadas, considera habilitar multiqueue, usar crypto más rápido, añadir CPU disponible o reducir overhead VPN (offloads).

Tarea 11: Inspecciona NIC offloads (pueden romper capturas, a veces el timing)

cr0x@server:~$ sudo ethtool -k eth0 | egrep 'tso|gso|gro'
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on

Significado: Offloads están habilitados. Usualmente están bien, pero pueden confundir capturas de paquetes y en algunos casos interactuar mal con túneles.
Decisión: Para un troubleshooting preciso, deshabilita temporalmente GRO/LRO en un host de prueba y vuelve a probar. No deshabilites offloads al azar en gateways con mucho tráfico sin un plan.

Tarea 12: Revisa errores y drops de recepción UDP

cr0x@server:~$ netstat -su
Udp:
    128934 packets received
    12 packets to unknown port received
    0 packet receive errors
    4311 packets sent
UdpLite:
IpExt:
    InOctets: 221009331
    OutOctets: 198887112

Significado: Errores de recepción UDP son cero. Bien.
Decisión: Si los errores de recepción suben durante llamadas, podrías estar alcanzando límites de buffer de socket o drops del kernel; ajusta buffers, soluciona saturación de CPU o reduce la contención de tráfico.

Tarea 13: Verifica la tasa de paquetes SIP/RTP durante una llamada (chequeo de sentido común)

cr0x@server:~$ sudo tcpdump -ni any udp portrange 10000-20000 -ttt -c 10
tcpdump: listening on any, link-type LINUX_SLL2, snapshot length 262144 bytes
 0.000000 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019884 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.020042 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186
 0.019901 IP 192.0.2.10.14562 > 198.51.100.20.10012: UDP, length 186

Significado: Tiempo entre paquetes alrededor de 20 ms sugiere ptime=20ms (común). Grandes huecos indican jitter o retrasos en la programación.
Decisión: Si el timing es irregular en el punto de captura cerca del emisor, investiga CPU del emisor/Wi‑Fi. Si es regular en el emisor pero irregular en el receptor, es red/colas.

Tarea 14: Identifica si el tráfico va por la VPN o la evita

cr0x@server:~$ ip route get 198.51.100.20
198.51.100.20 via 10.10.0.1 dev wg0 src 10.10.0.2 uid 1000
    cache

Significado: La ruta al endpoint de medios usa wg0.
Decisión: Si tu prueba de “evitar VPN” aún enruta por wg0, no evitaste nada. Arregla el policy routing/split tunnel y luego compara calidad de llamada.

Tarea 15: Confirma MTU en la interfaz WAN física (y detecta mismatch de jumbo)

cr0x@server:~$ ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff

Significado: La interfaz WAN es MTU estándar 1500.
Decisión: Si estás en PPPoE o ciertos enlaces celulares, la MTU WAN puede ser menor (1492, 1428, etc.). Eso te empuja a bajar la MTU del túnel.

Tarea 16: Detecta bufferbloat bajo carga con un ping simple mientras saturas la subida

cr0x@server:~$ ping -i 0.2 -c 20 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=18.9 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=57 time=210.4 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=57 time=245.7 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=57 time=198.1 ms

--- 1.1.1.1 ping statistics ---
20 packets transmitted, 20 received, 0% packet loss, time 3812ms
rtt min/avg/max/mdev = 18.4/156.2/265.1/72.9 ms

Significado: La latencia salta masivamente bajo carga: clásico bufferbloat.
Decisión: Despliega SQM shaping en la subida y prioriza voz; no pierdas tiempo persiguiendo códecs.

Tres microhistorias corporativas desde las trincheras

Incidente #1: La suposición equivocada (MTU “no puede ser, usamos 1500 en todas partes”)

Una empresa mediana migró un centro de llamadas a softphones sobre una VPN full‑tunnel. Funcionó en el piloto. Luego lo desplegaron a unos cientos de agentes remotos.
En el plazo de un día, la cola del helpdesk se convirtió en un segundo centro de llamadas—pero con peor audio.

La primera suposición del equipo de red fue clásica: “MTU no puede ser; Ethernet es 1500 y la VPN está bien configurada.”
Se centraron en el proveedor SIP, luego culparon al Wi‑Fi doméstico y después intentaron cambiar códecs.
Las llamadas mejoraban de forma aleatoria, lo peor porque fomenta superstición.

El patrón que resolvió el caso: el audio robótico se disparaba en ciertos flujos—transferencias, llamadas de consulta y cuando el softphone renegociaba SRTP.
Ahí la señalización aumentaba, y en algunos escenarios la ruta del túnel requería fragmentación. ICMP “fragmentation needed” estaba bloqueado en el borde del usuario por una configuración de “seguridad”.
PMTUD black holes. Nada glamuroso. Muy real.

La solución fue aburrida y decisiva: establecer una MTU conservadora en el túnel, clamar MSS para señalización TCP y documentar “no bloquear todo ICMP” en la base de acceso remoto.
También añadieron una prueba de una página: ping DF a través del túnel a un endpoint conocido. Detectó regresiones después.

Lección: “1500 en todas partes” no es un diseño. Es un deseo.

Incidente #2: La optimización que salió mal (priorizar voz… acelerando todo)

Otra organización tenía un gateway VPN capaz y quería “calidad premium de voz.” Alguien activó aceleración hardware y fast‑path en el firewall de borde.
El throughput subió. La latencia en una prueba sintética bajó. Todos celebraron.

Dos semanas después, quejas: “Audio robótico solo durante grandes subidas de archivos.” Ese detalle importaba.
Bajo carga, el fast path evitaba partes de la pila QoS y de gestión de colas. El tráfico bulk y la voz aterrizaban en la misma cola profunda en el lado del WAN.
La aceleración mejoró el pico de throughput, pero eliminó el mecanismo que mantenía la latencia estable.

Los ingenieros hicieron lo típico: añadieron más reglas QoS. Más clases. Más matches. Empeoró.
La clasificación costaba CPU en el slow path, mientras que el fast path seguía pasando la mayor parte del tráfico a la misma cola del cuello de botella.
Ahora tenían complejidad y seguían con bufferbloat.

La solución final no fue “más QoS.” Fue: hacer shaping de la subida justo por debajo de la capacidad real, habilitar un qdisc moderno y mantener el modelo de clases simple.
Luego decidir si la aceleración era compatible con esa política. Donde no lo era, la voz ganó.

Lección: optimizar por throughput sin respetar el comportamiento de colas es construir una forma más rápida de sonar terrible.

Incidente #3: La práctica aburrida que salvó el día (tests estándar + control de cambios)

Una compañía global ejecutaba voz sobre IPsec entre sucursales y HQ. Nada sofisticado. La diferencia clave: trataban la voz como un servicio de producción.
Cada cambio de red tenía una checklist preflight y postflight, incluyendo un puñado de pruebas relevantes para VoIP.

Un viernes, un ISP cambió equipos de acceso en una oficina regional. Los usuarios notaron “ligero robot” en las llamadas.
El equipo local corrió las pruebas estándar: ping en reposo vs bajo carga de subida, pings DF para MTU y una comprobación rápida de DSCP en egress WAN.
No debatieron. Midieron.

Los datos mostraron que PMTUD estaba roto en el nuevo acceso, y que el buffer upstream era más profundo que antes. Dos problemas. Ambos accionables.
Bajaron la MTU del túnel ligeramente, habilitaron MSS clamping y ajustaron el shaping para mantener la latencia estable. Las llamadas se estabilizaron inmediatamente.

El lunes, escalaron al ISP con evidencia precisa: timestamps, umbral de fallo MTU y gráficos de latencia bajo carga.
El ISP arregló el manejo de ICMP más tarde. Pero la empresa no tuvo que esperar para recuperar voz usable.

Lección: la característica de fiabilidad más efectiva es una prueba repetible que realmente ejecutes.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: audio robótico durante subidas o uso compartido de pantalla

  • Causa raíz: bufferbloat en upstream; paquetes de voz atrapados detrás de tráfico bulk en una cola profunda.
  • Solución: habilitar SQM (fq_codel/cake) y shape ligeramente por debajo de la subida; añadir prioridad simple para RTP/SIP en egress WAN.

2) Síntoma: la llamada conecta, luego el audio cae o se vuelve cortado después de un minuto

  • Causa raíz: PMTUD/MTU black hole activado por rekey, renegociación SRTP o aumento del tamaño en re-INVITE SIP.
  • Solución: fijar MTU del túnel explícitamente; permitir ICMP “frag needed”/PTB; para señalización TCP, clamar MSS.

3) Síntoma: audio unidireccional (tú los oyes, ellos no te oyen)

  • Causa raíz: problema de NAT traversal o enrutamiento asimétrico; RTP fijado a la interfaz equivocada; tiempos de espera/estado UDP en firewalls.
  • Solución: asegurar settings NAT correctos (SIP ALG normalmente off), confirmar rutas, aumentar timeout UDP en dispositivos stateful y validar RTP simétrico si está soportado.

4) Síntoma: bien en cable, mal en Wi‑Fi

  • Causa raíz: contención de airtime, reintentos o comportamiento power‑save; la VPN añade overhead y sensibilidad al jitter.
  • Solución: mover llamadas a 5 GHz/6 GHz, reducir contención de canal, deshabilitar power save agresivo en dispositivos de voz, preferir cable para roles con muchas llamadas.

5) Síntoma: solo usuarios remotos en cierto ISP tienen problemas

  • Causa raíz: shaping/upstream del ISP, comportamiento CGNAT, mal peering o filtrado ICMP que afecta PMTUD.
  • Solución: reducir MTU del túnel, aplicar shaping en el borde del usuario si está gestionado, probar transporte/puerto alternativo y recolectar evidencia para escalar.

6) Síntoma: “QoS está habilitado” pero la voz degrada bajo carga

  • Causa raíz: QoS configurado en LAN mientras la congestión está en WAN; o DSCP marcado en paquetes internos pero no en los externos del túnel.
  • Solución: priorizar en la cola de egress que realmente está congestionada; mapear/clasificar voz a paquetes exteriores; verificar con tcpdump y estadísticas qdisc.

7) Síntoma: bursts esporádicos de robot, especialmente en horas pico

  • Causa raíz: microbursts y oscilación de colas; contentión de CPU en el concentrador VPN; o congestión upstream.
  • Solución: revisar softirq/CPU, habilitar pacing/SQM, asegurar suficiente headroom en gateway y evitar jerarquías de clases demasiado complicadas.

8) Síntoma: llamadas bien, pero “hold/resume” falla o transfers fallan

  • Causa raíz: fragmentación de señalización SIP o problemas MTU que afectan mensajes SIP más grandes; a veces SIP sobre TCP/TLS afectado por MSS.
  • Solución: clamar MSS, reducir MTU, permitir ICMP PTB y validar settings SIP (y deshabilitar SIP ALG si mangla paquetes).

Listas de verificación / plan paso a paso

Paso a paso: estabiliza VoIP sobre VPN en una semana (no un trimestre)

  1. Elige un usuario representativo que falle y reproduce el problema a demanda (subir archivos mientras estás en llamada suele ser suficiente).
    Sin reproducibilidad, no hay progreso.
  2. Recopila estadísticas de voz (desde softphone/PBX): jitter, pérdida, concealment, RTT si está disponible.
    Decide: ¿motivado por pérdida (red) vs CPU (endpoint) vs señalización (transporte SIP/MTU)?
  3. Confirma el enrutamiento: asegúrate de que los medios realmente atraviesan la VPN cuando crees que lo hacen. Arregla confusión de split tunnel temprano.
  4. Mide path MTU a través del túnel usando pings DF a endpoints conocidos.
    Decide una MTU segura (conservadora vence a teórica).
  5. Establece MTU del túnel explícitamente en ambos extremos (y documenta por qué).
    Evita el ajuste “auto” a menos que lo hayas probado en todos los tipos de acceso (banda ancha doméstica, LTE, Wi‑Fi de hotel, etc.).
  6. Clampa TCP MSS en el túnel para flujos TCP reenviados (SIP/TLS, provisioning, management).
  7. Encuentra el verdadero cuello de botella (usualmente la subida). Usa ping‑under‑load para confirmar regresión por bufferbloat.
  8. Despliega SQM shaping en el cuello de botella, ligeramente por debajo de la tasa de línea, con fq_codel o cake.
  9. Mantén las clases QoS simples y prioriza la voz solo donde importa: la cola de egress.
  10. Verifica el manejo DSCP con capturas: marca interna, marca externa y si la cola la respeta.
  11. Re-prueba el caso original (llamada + subida) y confirma mejoras en jitter/pérdida.
  12. Despliega gradualmente con un grupo canario y un plan de rollback. Los cambios en voz son visibles para usuarios al instante; trátalo como un deploy en producción.

Checklist operativa: cada vez que toques VPN o WAN

  • Registra las MTU actuales (WAN + túnel) y políticas qdisc/shaping.
  • Ejecuta la prueba MTU DF through tunnel a un endpoint estable.
  • Ejecuta ping en reposo vs ping bajo carga para medir regresión por bufferbloat.
  • Captura 30 segundos de RTP durante una llamada de prueba y revisa pérdidas/picos de jitter.
  • Confirma DSCP en el paquete exterior en el lado WAN (si dependes de marcado).
  • Revisa softirq de CPU del gateway bajo carga.

Datos interesantes y contexto histórico

  • Dato 1: RTP (Real-time Transport Protocol) se estandarizó a mediados de los 90 para transportar medios en tiempo real sobre redes IP.
  • Dato 2: SIP creció en popularidad en parte porque se parecía a HTTP para llamadas—basado en texto, extensible—ideal para funciones, a veces doloroso para MTU.
  • Dato 3: Gran parte del dolor de PMTUD viene de prácticas de filtrado ICMP que se hicieron comunes como respuesta de seguridad en la era temprana de internet.
  • Dato 4: Los despliegues tempranos de VoIP a menudo confiaban en marcas DiffServ (DSCP) dentro de redes empresariales, pero “QoS a través del internet público” nunca fue ampliamente fiable.
  • Dato 5: La adopción de VPN creció para trabajo remoto, y las quejas sobre la calidad de voz siguieron porque los enlaces de consumo suelen ser el segmento más estrecho y con más bufferbloat.
  • Dato 6: WireGuard se hizo popular en parte porque es ligero y rápido, pero “crypto rápido” no cancela “colas malas”.
  • Dato 7: Bufferbloat se identificó y nombró porque el equipo de consumo venía con buffers demasiado grandes que mejoraban benchmarks de throughput mientras destrozaban aplicaciones sensibles a latencia.
  • Dato 8: Qdiscs modernos de Linux como fq_codel se desarrollaron específicamente para mantener la latencia acotada bajo carga, importante para voz y juegos.
  • Dato 9: Muchos diseños VPN empresariales históricamente asumieron un underlay de 1500 bytes; PPPoE, LTE y apilamiento de túneles hicieron esa suposición cada vez más frágil.

Preguntas frecuentes

1) ¿Por qué el audio es “robótico” y no solo bajo o retrasado?

Porque estás oyendo concealment por pérdida de paquetes. El decodificador adivina tramas faltantes. Volumen bajo o silencioso suele ser ganancia o problema de dispositivo; robótico suele ser pérdida/jitter.

2) ¿Qué porcentaje de pérdida se vuelve audible en VoIP?

Depende del códec y del concealment, pero incluso ~1% de pérdida puede notarse, especialmente si es en ráfagas. Un 0.1% estable puede estar bien; 2% en ráfagas a menudo no.

3) ¿La MTU es solo un problema de TCP? RTP es UDP.

La MTU también afecta a UDP. Si los paquetes exceden la path MTU y PMTUD está roto, se descartan. Además, la fragmentación aumenta la sensibilidad a pérdida y el jitter cuando los fragmentos compiten en las colas.

4) ¿Debo simplemente fijar MTU a 1200 y ya?

No. Reducirás eficiencia y podrías romper otros protocolos o rutas innecesariamente. Mide path MTU, elige un valor seguro y documéntalo. Conservador, no extremo.

5) ¿Marcar DSCP ayuda en internet público?

A veces dentro del dominio de un ISP, a menudo no de extremo a extremo. La ganancia confiable es priorizar donde controlas la cola: tu egress WAN y bordes gestionados.

6) ¿Puede QoS arreglar pérdida de paquetes por mal ISP?

QoS no crea ancho de banda. Puede prevenir pérdidas/jitter auto infligidos al gestionar tus propias colas. Si el ISP está descartando upstream, necesitas mejor ruta o proveedor distinto.

7) ¿Por qué solo falla cuando alguien sube un archivo?

La subida satura el upstream. Las colas upstream se hinchan, la latencia y jitter explotan y el RTP llega tarde. Las pruebas de velocidad rara vez lo detectan porque premian buffers profundos.

8) ¿WireGuard es automáticamente mejor que OpenVPN para voz?

WireGuard suele tener menos overhead y es más fácil de razonar, pero la calidad de voz depende sobre todo de corrección MTU, gestión de colas y enrutamiento estable. Puedes romper voz en cualquier VPN.

9) ¿Cuál es la política QoS más simple que realmente funciona?

Haz shaping del WAN un poco por debajo de la tasa real y prioriza la voz (RTP) por encima del bulk. Mantén el modelo de clases pequeño. Verifica con estadísticas qdisc y pruebas de llamadas reales.

10) ¿Cómo demuestro que es MTU y no “el códec”?

Reproduce con pings DF para ver umbrales y observa fallos alrededor de tamaños de paquete específicos; correlaciona con eventos de llamada que aumentan señalización; corrige MTU y verifica que el problema desaparece sin cambiar códecs.

Pasos prácticos siguientes

Si quieres llamadas que suenen humanas, trata la voz como un SLO de latencia, no como una vibra.

  1. Ejecuta la guía rápida de diagnóstico en un usuario afectado y captura evidencia: MTU, jitter bajo carga, comportamiento DSCP.
  2. Fija la MTU del túnel explícitamente (empieza conservador), y clampa MSS para rutas de señalización TCP.
  3. Despliega SQM shaping en el cuello de botella uplink con fq_codel/cake y prioriza la voz en esa cola.
  4. Verifica con mediciones (estadísticas qdisc, tcpdump DSCP, mtr y estadísticas de jitter/pérdida del cliente), no con sensaciones.
  5. Documenta: la MTU elegida, por qué se eligió y las pruebas de regresión. El tú del futuro estará cansado e indiferente.

La mayoría de los “misterios” VoIP‑sobre‑VPN son solo redes haciendo cosas de red. Haz los paquetes más pequeños, mejora las colas y haz que tus prioridades sean reales donde ocurre la congestión.

Volúmenes NFS de Docker con tiempo de espera: Opciones de montaje que realmente mejoran la estabilidad

Despliegas un contenedor perfectamente aburrido. Escribe unos archivos. Luego, a las 02:17, todo se congela como si esperara una autorización.
df se queda colgado. Los hilos de tu aplicación se acumulan. Los logs de Docker no dicen nada útil. La única pista es un rastro de “nfs: server not responding”.

NFS en sistemas containerizados falla de formas que parecen errores de la aplicación, fallos del kernel o “vibras de red”.
Normalmente no es ninguno de esos. Son las semánticas de montaje chocando con fallos de red transitorios, sorpresas de DNS y un driver de volúmenes que no te dice lo que hizo.

Por qué los volúmenes NFS de Docker agotan el tiempo (y por qué parece aleatorio)

“Volúmenes NFS” en Docker son simplemente montajes NFS de Linux creados por el host y luego bind-mountados dentro de contenedores.
Suena simple, y lo es—hasta que recuerdas que NFS es un sistema de archivos de red con semánticas de reintento,
timeouts de RPC, locking y estado.

La mayoría de los “tiempos de espera” no son un único timeout

Cuando alguien informa “NFS timed out”, normalmente quiere decir una de estas cosas:

  • El cliente está reintentando para siempre (hard mount) y el hilo de la aplicación queda bloqueado en sueño no interrumpible (D state). Esto parece un cuelgue.
  • El cliente se rindió (soft mount) y devolvió un error de E/S. Esto parece corrupción, escrituras fallidas o “mi app falla al azar.”
  • El servidor se fue y volvió, pero la vista del cliente sobre el export ya no coincide (stale file handles). Esto parece “funcionaba ayer”.
  • Problemas de plomería RPC (especialmente NFSv3: portmapper/rpcbind, mountd, lockd) causan fallos parciales donde algunas operaciones funcionan y otras agotan el tiempo.
  • Flaps en resolución de nombres o enrutamiento causan parones intermitentes que se autorrecuperan. Estos son los peores porque alimentan superstición.

Docker amplifica los modos de fallo

NFS es sensible al tiempo de montaje. Docker es muy bueno iniciando contenedores rápidamente, de forma concurrente y a veces antes de que la red esté lista.
Si el montaje NFS se desencadena bajo demanda, tu “inicio de contenedor” se convierte en “inicio de contenedor más red más DNS más disponibilidad del servidor”.
Eso está bien en un laboratorio. En producción, es una manera elaborada de convertir pequeña fluctuación de red en una caída completa del servicio.

Hard vs soft no es un control de rendimiento; es una decisión de riesgo

Para la mayoría de cargas con estado, el valor seguro por defecto es hard: seguir reintentando, no fingir que las escrituras tuvieron éxito.
Pero los montajes hard pueden colgar procesos cuando el servidor no está alcanzable. Tu trabajo es hacer que “servidor no alcanzable” sea raro y breve,
y hacer el montaje resiliente ante el caos normal de las redes.

Hay una idea parafraseada de Werner Vogels (CTO de Amazon) que vale la pena mantener en la cabeza: “Todo falla, así que diseña para el fallo.”
Los montajes NFS son exactamente donde esa filosofía deja de ser inspiradora y pasa a ser una lista de comprobación.

Hechos interesantes y contexto (breve, concreto, útil)

  • NFS existe desde antes de los contenedores. Se originó en los años 80 como forma de compartir archivos en red sin complejidad de estado en el cliente.
  • NFSv3 es mayormente stateless. Eso hizo que el failover fuera más simple en algunos aspectos, pero desplazó la complejidad a daemons auxiliares (rpcbind, mountd, lockd).
  • NFSv4 consolidó los canales laterales. v4 suele usar un solo puerto conocido (2049) e integra locking y estado, lo que a menudo mejora la compatibilidad con firewalls y NAT.
  • “Hard mount” es el predeterminado histórico por una razón. Perder datos silenciosamente es peor que esperar; los hard mounts favorecen corrección sobre disponibilidad.
  • El cliente NFS de Linux tiene múltiples capas de timeouts. Existe el timeout por RPC (timeo), el número de reintentos (retrans) y luego comportamientos de recuperación a mayor nivel.
  • Stale file handles son un impuesto clásico de NFS. Ocurren cuando el mapeo inode/file handle del servidor cambia bajo el cliente—común después de failover o cambios en el export.
  • NFS sobre TCP no siempre fue el predeterminado. UDP fue popular al principio; TCP es ahora la opción sensata por fiabilidad y control de congestión.
  • DNS importa más de lo que crees. Los clientes NFS pueden cachear el nombre a IP de forma distinta a tu aplicación; un cambio de DNS a mitad puede producir síntomas de “la mitad del mundo funciona”.

Broma #1: NFS es como una impresora compartida en la oficina—cuando funciona, nadie se da cuenta; cuando falla, de repente todos tienen documentos “críticos para el negocio”.

Guía de diagnóstico rápido (encuentra el cuello de botella deprisa)

El objetivo no es “recopilar cada métrica”. El objetivo es decidir, rápido, si tienes un problema de ruta de red,
un problema del servidor, un problema de semánticas de montaje del cliente o un problema de orquestación de Docker.
Aquí está el orden que ahorra tiempo.

Primero: confirma que es NFS y no la app

  1. En el host Docker, intenta un simple stat o ls en la ruta montada. Si cuelga, no es tu app. Es el montaje.
  2. Revisa dmesg por server not responding / timed out / stale file handle. Los mensajes del kernel son contundentes y generalmente correctos.

Segundo: decide “red vs servidor” con una prueba

  1. Desde el cliente, verifica conectividad al puerto 2049 y (si usas NFSv3) rpcbind/portmapper. Si no puedes conectar, deja de culpar las opciones de montaje.
  2. Desde otro host en el mismo segmento de red, prueba lo mismo. Si el problema está aislado a un cliente, sospecha firewall local, agotamiento de conntrack, MTU o una ruta mala.

Tercero: verifica versión de protocolo y opciones de montaje

  1. Comprueba si estás en NFSv3 o NFSv4. Muchos “tiempos de espera aleatorios” son en realidad problemas de rpcbind/mountd de NFSv3 en redes modernas.
  2. Confirma hard, timeo, retrans, tcp, y si usaste intr (comportamiento obsoleto) u otras flags legacy.

Cuarto: inspecciona logs y saturación en el servidor

  1. El load average del servidor no basta. Mira hilos NFS, latencia de disco y caídas de red.
  2. Si el servidor es un appliance NAS, identifica si está limitado por CPU (cifrado, checksumming) o por I/O (discos, rebuild, borrado de snapshots).

Si haces esas cuatro fases, normalmente puedes nombrar la clase de fallo en menos de diez minutos. La parte larga es la política.

Patrones de configuración de Docker que no te sabotean

Patrón 1: driver local de Docker con opciones NFS (bien, pero verifica qué montó)

El driver local de Docker puede montar NFS usando type=nfs y o=....
Esto es común en Compose y Swarm.
La trampa: la gente asume que Docker “hace algo inteligente”. No lo hace. Pasa opciones al helper de montaje.
Si el helper de montaje cae a otra versión o ignora una opción, puede que no lo notes.

Patrón 2: pre-montar en el host y bind-mountar en contenedores (a menudo más predecible)

Si pre-montas vía /etc/fstab o unidades systemd, puedes controlar ordering, reintentos y observar el montaje directamente.
Docker entonces solo bind-mounta una ruta local. Esto reduce la “magia de Docker”, lo cual suele ser bueno para dormir tranquilo.

Patrón 3: separar montajes por clase de carga

No uses un solo export NFS y un solo conjunto de opciones para todo.
Trata NFS como un servicio con SLOs: metadata baja latencia (caches de CI), throughput masivo (media), corrección primero (datos de aplicaciones stateful).
Distintos montajes, distintas opciones, distintas expectativas.

Tareas prácticas: comandos, salida y la decisión que tomas

Estas son las acciones de on-call que convierten “NFS es inestable” en un siguiente paso claro. Ejecútalas en el host Docker salvo indicación.
Cada tarea incluye (1) comando, (2) qué significa la salida, (3) qué decisión tomar.

Tarea 1: Identificar qué montajes son NFS y cómo están configurados

cr0x@server:~$ findmnt -t nfs,nfs4 -o TARGET,SOURCE,FSTYPE,OPTIONS
TARGET                SOURCE                     FSTYPE OPTIONS
/var/lib/docker-nfs    nas01:/exports/appdata     nfs4   rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21

Significado: Confirma versión NFS, proto y si estás en hard o soft. También muestra si rsize/wsize son enormes y potencialmente desajustados.

Decisión: Si ves vers=3 inesperadamente, planifica migrar a v4 o auditar puertos rpcbind/mountd. Si ves soft en cargas con muchas escrituras, cámbialo.

Tarea 2: Confirmar configuración del volumen Docker (qué cree Docker que pidió)

cr0x@server:~$ docker volume inspect appdata
[
  {
    "CreatedAt": "2026-01-01T10:12:44Z",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/appdata/_data",
    "Name": "appdata",
    "Options": {
      "device": ":/exports/appdata",
      "o": "addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2,noatime",
      "type": "nfs"
    },
    "Scope": "local"
  }
]

Significado: Esto es configuración, no la verdad. Las opciones de Docker pueden ser correctas mientras el montaje real difiere.

Decisión: Compárala con findmnt. Si difieren, investiga comportamiento del helper de montaje, defaults y soporte del kernel.

Tarea 3: Buscar errores del cliente NFS en el kernel ahora mismo

cr0x@server:~$ dmesg -T | egrep -i 'nfs:|rpc:|stale|not responding|timed out' | tail -n 20
[Fri Jan  3 01:58:41 2026] nfs: server nas01 not responding, still trying
[Fri Jan  3 01:59:12 2026] nfs: server nas01 OK

Significado: “Not responding, still trying” indica un hard mount reintentando tras una interrupción.

Decisión: Si estos eventos coinciden con bloqueos de la app, investiga caídas de red o stalls del servidor; no “arregles” la app.

Tarea 4: Confirmar el estado de procesos durante un hang (¿está en D-state?)

cr0x@server:~$ ps -eo pid,stat,comm,wchan:40 | egrep 'D|nfs' | head
 8421 D    php-fpm          nfs_wait_on_request
 9133 D    rsync            nfs_wait_on_request

Significado: Estado D con nfs_wait_on_request apunta a I/O del kernel bloqueada esperando NFS.

Decisión: Trata como incidente de infraestructura. Reiniciar contenedores no ayudará si el montaje está hard-stuck.

Tarea 5: Comprobar conectividad TCP básica al servidor NFS

cr0x@server:~$ nc -vz -w 2 10.10.8.10 2049
Connection to 10.10.8.10 2049 port [tcp/nfs] succeeded!

Significado: Puerto 2049 accesible ahora.

Decisión: Si esto falla durante el incidente, tus opciones de montaje no son el problema principal; arregla enrutamiento, ACLs, firewall o disponibilidad del servidor.

Tarea 6: Si usas NFSv3, confirma que rpcbind es alcanzable (dependencia oculta común)

cr0x@server:~$ nc -vz -w 2 10.10.8.10 111
Connection to 10.10.8.10 111 port [tcp/sunrpc] succeeded!

Significado: rpcbind/portmapper alcanzable. Sin ello, los montajes NFSv3 pueden fallar o colgar durante la negociación.

Decisión: Si 111 está bloqueado y estás en v3, migra a v4 o abre los puertos requeridos correctamente (y documéntalo).

Tarea 7: Identificar versión NFS negociada y dirección del servidor usada (atrapa sorpresas de DNS)

cr0x@server:~$ nfsstat -m
/var/lib/docker-nfs from nas01:/exports/appdata
 Flags: rw,hard,noatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.10.8.21,local_lock=none

Significado: Confirma ajustes negociados. Fíjate en el nombre del servidor vs IP, y en cualquier comportamiento de local_lock.

Decisión: Si el montaje usa un hostname y tu DNS es inestable, cambia a IP o fija entradas en hosts—luego planifica una mejor estrategia de DNS.

Tarea 8: Medir retransmisiones y dolor a nivel RPC (¿es pérdida de paquetes?)

cr0x@server:~$ nfsstat -rc
Client rpc stats:
calls      retrans    authrefrsh
148233     912        148245

Significado: Retransmits indican RPCs que tuvieron que ser reenviadas. Un conteo creciente de retrans se correlaciona con pérdida, congestión o stalls del servidor.

Decisión: Si retrans salta durante incidentes, inspecciona caídas de red y carga del servidor; considera aumentar timeo modestamente, no disminuirlo.

Tarea 9: Comprobar errores y drops en la interfaz (no adivines)

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    RX:  bytes packets errors dropped  missed   mcast
    128G  98M     0     127      0      1234
    TX:  bytes packets errors dropped carrier collsns
    141G  92M     0      84      0      0

Significado: Drops en RX/TX pueden ser suficientes para disparar “not responding” de NFS bajo carga.

Decisión: Si los drops crecen, investiga NIC rings, mismatch de MTU, congestión del switch o saturación de CPU del host.

Tarea 10: Detectar mismatch de MTU rápidamente (las jumbo frames son inocentes hasta probadas culpables)

cr0x@server:~$ ping -c 3 -M do -s 8972 10.10.8.10
PING 10.10.8.10 (10.10.8.10) 8972(9000) bytes of data.
From 10.10.8.21 icmp_seq=1 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=2 Frag needed and DF set (mtu = 1500)
From 10.10.8.21 icmp_seq=3 Frag needed and DF set (mtu = 1500)

--- 10.10.8.10 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2043ms

Significado: La MTU de la ruta es 1500, pero tu host cree que es 9000. Eso provoca blackholing y stalls “aleatorios”.

Decisión: Arregla MTU de extremo a extremo o reduce a 1500. Luego reevalúa la estabilidad de NFS antes de tocar opciones de montaje.

Tarea 11: Confirmar que el export del servidor existe y los permisos son sensatos (vista servidor)

cr0x@server:~$ showmount -e 10.10.8.10
Export list for 10.10.8.10:
/exports/appdata 10.10.8.0/24
/exports/shared  10.10.0.0/16

Significado: Muestra exports (útil sobretodo en entornos NFSv3; para v4 sigue siendo una pista útil).

Decisión: Si el export no aparece o la subred del cliente no está permitida, deja de tunear el cliente y arregla la política de export.

Tarea 12: Capturar una traza corta de paquetes NFS durante el evento (prueba pérdida vs silencio del servidor)

cr0x@server:~$ sudo tcpdump -i eth0 -nn host 10.10.8.10 and port 2049 -c 30
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
10:02:11.101223 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:12.102988 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232
10:02:13.105441 IP 10.10.8.21.51344 > 10.10.8.10.2049: Flags [P.], seq 219:451, ack 1001, win 501, length 232

Significado: Repetidos retransmits sin respuestas del servidor indican que el servidor no responde o que las respuestas no regresan.

Decisión: Si ves retransmits del cliente y ningún reply del servidor, ve a salud del servidor / ruta de retorno de red. Si ves replies del servidor pero el cliente sigue retransmitiendo, sospecha enrutamiento asimétrico o estado del firewall.

Tarea 13: Revisar logs del daemon Docker por intentos y fallos de montaje

cr0x@server:~$ journalctl -u docker --since "30 min ago" | egrep -i 'mount|nfs|volume|rpc' | tail -n 30
Jan 03 09:32:14 server dockerd[1321]: time="2026-01-03T09:32:14.112345678Z" level=error msg="error while mounting volume 'appdata': failed to mount local volume: mount :/exports/appdata:/var/lib/docker/volumes/appdata/_data, data: addr=10.10.8.10,vers=4.1,proto=tcp,hard,timeo=600,retrans=2,noatime, type: nfs: connection timed out"

Significado: Confirma que Docker no pudo montar, frente a la app fallando más tarde.

Decisión: Si los montajes fallan al inicio del contenedor, prioriza la disponibilidad de la red y el servidor; no persigas afinados de runtime todavía.

Tarea 14: Inspeccionar ordering de systemd (network-online no es lo mismo que network)

cr0x@server:~$ systemctl status network-online.target
● network-online.target - Network is Online
     Loaded: loaded (/lib/systemd/system/network-online.target; static)
     Active: active since Fri 2026-01-03 09:10:03 UTC; 1h 2min ago

Significado: Si este target no está activo cuando ocurren los montajes, tu NFS puede competir con la preparación de la red.

Decisión: Si ves problemas de ordering, mueve montajes a unidades systemd con After=network-online.target y Wants=network-online.target, o usa automount.

Tarea 15: Validar que el montaje responde (chequeo rápido)

cr0x@server:~$ time bash -c 'stat /var/lib/docker-nfs/. && ls -l /var/lib/docker-nfs >/dev/null'
real    0m0.082s
user    0m0.004s
sys     0m0.012s

Significado: Operaciones básicas de metadata son rápidas. Si esto a veces tarda segundos o cuelga, tienes latencia intermitente o stalls.

Decisión: Si la metadata está lenta, investiga latencia de disco del servidor y saturación de hilos NFS; las opciones de montaje no salvarán un array ahogándose.

Tres micro-historias corporativas (cómo falla esto en la práctica)

1) Incidente causado por una suposición errónea: “El volumen es local, porque Docker dijo ‘local’”

Una empresa mediana ejecutaba un clúster Swarm para servicios internos. Un equipo creó un volumen Docker con el driver local y opciones NFS.
Todos leyeron “local” y asumieron que los datos vivían en cada nodo. Esa suposición moldeó todo: drills de fallo, ventanas de mantenimiento, incluso propiedad del incidente.

Durante un mantenimiento de red, un switch top-of-rack hizo flapping. Solo algunos nodos perdieron conectividad al NAS por unos segundos.
Los nodos afectados tenían volúmenes montados en hard. Sus contenedores no se reiniciaron; simplemente dejaron de avanzar. Los health checks caducaron.
El orquestador empezó a reprogramar, pero las nuevas tareas recalcaron en los mismos nodos dañados porque el scheduler no sabía que NFS era el cuello de botella.

La respuesta de on-call fue clásica: reiniciar el servicio. Eso solo creó más procesos bloqueados. Alguien intentó borrar y recrear el volumen.
Docker cumplió, pero el montaje del kernel todavía estaba trabado. El host se convirtió en un museo de tareas atascadas.

La solución no fue heroica. Documentaron que “driver local” aún puede ser almacenamiento remoto, añadieron una comprobación preflight en pipelines de despliegue
para verificar el tipo de montaje con findmnt, y separaron servicios críticos de NFS de nodos que no podían alcanzar la VLAN de almacenamiento.
El mayor cambio fue cultural: almacenamiento dejó de ser “problema de otro” en el momento en que los contenedores entraron en escena.

2) Optimización que salió mal: “Bajamos timeouts para que los fallos fallen rápido”

Otra organización tenía un problema intermitente: las apps se quedaban colgadas cuando NFS fallaba momentáneamente. Alguien propuso un cambio “simple”:
pasar a soft, bajar timeo y subir retrans para que el cliente se rindiera rápido y la app lo manejara.
En un ticket parecía razonable, porque todo parece razonable en un ticket.

En la práctica, las aplicaciones no estaban diseñadas para manejar EIO a mitad de flujo.
Un worker en background escribía a un archivo temporal y luego lo renombraba en su lugar. Con montajes soft y timeouts bajos,
la escritura a veces fallaba pero el flujo no siempre propagaba el error. El rename ocurrió con contenido parcial.
Tareas posteriores procesaron basura.

El incidente no fue una caída limpia; fue peor. El sistema permaneció “arriba” mientras producía resultados incorrectos.
Eso desencadenó una respuesta en cámara lenta: rollback, reprocesar, auditar salidas. Eventualmente se revirtieron las opciones de montaje.
Luego arreglaron el problema real: pérdida de paquetes intermitente por un bond LACP mal configurado y un mismatch de MTU que solo aparecía bajo carga.

El takeaway que escribieron en su runbook fue dolorosamente exacto: “Fail fast” es genial cuando el fallo se expone de forma fiable.
Los montajes soft hicieron el fallo más fácil de ignorar, no más fácil de gestionar.

3) Práctica aburrida pero correcta que salvó el día: pre-mount + automount + dependencias explícitas

Una firma financiera ejecutaba trabajos batch stateful en contenedores, escribiendo artefactos en NFS.
Tenían una regla sobria: montajes NFS los gestiona systemd, no la creación de volúmenes de Docker en runtime.
Cada montaje tenía una unidad automount, un timeout definido y dependencia de network-online.target.

Una mañana, un ciclo rutinario de reboot afectó un nodo mientras parchaban el NAS. El NAS era alcanzable pero lento por unos minutos.
Los contenedores arrancaron, pero sus rutas respaldadas por NFS fueron automontadas solo cuando se necesitaron. El intento de automount esperó y luego tuvo éxito cuando el NAS se recuperó.
Los jobs empezaron un poco tarde, y nadie se despertó.

La diferencia no fue mejor hardware. Fue que el ciclo de vida del montaje no estaba acoplado al ciclo de vida del contenedor.
Docker no decidió cuándo ocurrió el montaje, y los fallos eran visibles a nivel sistema con logs claros.

Ese es el tipo de práctica que los ejecutivos rara vez elogian porque no pasó nada. También es la práctica que te mantiene empleado.

Errores comunes: síntoma → causa raíz → corrección

1) Síntoma: los contenedores “se congelan” y no se detienen; docker stop cuelga

Causa raíz: montaje NFS en hard atascado; procesos en estado D esperando I/O del kernel.

Corrección: restaurar conectividad/salud del servidor; no esperes que las señales funcionen. Si debes recuperar un nodo, desmonta tras la recuperación del servidor, o reinicia el host como último recurso. Prevén recurrencias con red estable y timeo/retrans sensatos.

2) Síntoma: “funciona en un nodo, falla en otro”

Causa raíz: diferencias por nodo en routing/firewall/MTU, o agotamiento de conntrack en un subconjunto de nodos.

Corrección: compara ip route, ip -s link y reglas de firewall. Valida MTU con pings DF. Asegura configuración de red idéntica en la flota.

3) Síntoma: montajes fallan en boot o justo después de reinicio del host

Causa raíz: intentos de montaje compiten con la preparación de la red; Docker inicia contenedores antes de que network-online sea verdadero.

Corrección: gestiona montajes vía unidades systemd con ordering explícito, o usa automount. Evita montajes bajo demanda iniciados por el arranque de contenedores.

4) Síntoma: “permission denied” intermitente o problemas de identidad extraños

Causa raíz: mismatch de UID/GID, root-squash, o problemas de idmapping de NFSv4. Los contenedores empeoran esto porque los espacios de usuario y usuarios de imagen varían.

Corrección: estandariza UID/GID para quienes escriben, valida opciones de export en el servidor, y para NFSv4 confirma la configuración de idmapping. No lo tapes con 0777; eso no es estabilidad, es rendición.

5) Síntoma: frecuentes “stale file handle” tras failover del NAS o mantenimiento del export

Causa raíz: el mapeo de file handles en el servidor cambió; los clientes mantienen referencias que ya no resuelven.

Corrección: evita mover/re-escribir exports bajo los clientes; usa rutas estables. Para recuperación, remonta y reinicia las cargas afectadas. Arquitectónicamente, prefiere métodos HA estables soportados por tu NAS y versión NFS, y prueba failover con clientes reales.

6) Síntoma: fallos “aleatorios” de montaje solo en redes aseguradas

Causa raíz: puertos dinámicos de NFSv3 bloqueados; rpcbind/mountd/lockd no permitidos por firewall/security groups.

Corrección: migra a NFSv4 cuando sea posible. Si estás atado a v3, fija puertos de los daemons en el servidor y ábrelos intencionalmente—luego documéntalo para que la próxima persona no “optimice” tu firewall.

7) Síntoma: picos de latencia altos, luego recuperación, repitiéndose bajo carga

Causa raíz: latencia de disco del servidor (rebuild/snapshot), saturación de hilos NFS o colas de red congestionadas.

Corrección: mide latencia I/O del servidor e hilos de servicio NFS; arregla el cuello de botella. Opciones cliente como rsize/wsize no salvarán un array saturado.

8) Síntoma: cambiar a soft “arregla” los cuelgues pero introduce problemas de datos misteriosos

Causa raíz: montajes soft convierten outages en errores de I/O; las aplicaciones manejan mal fallos parciales.

Corrección: revierte a hard para escrituras stateful, arregla la conectividad subyacente y actualiza aplicaciones para manejar errores cuando corresponda.

Listas de comprobación / plan paso a paso

Paso a paso: estabilizar un despliegue NFS Docker existente

  1. Inventaría montajes con findmnt -t nfs,nfs4. Anota vers, proto, hard/soft, timeo, retrans, y si usaste hostnames.
  2. Confirma la realidad con nfsstat -m. Si Docker dice una cosa y el kernel otra, confía en el kernel.
  3. Decide protocolo: preferir NFSv4.1+. Si estás en v3, lista dependencias de firewall y casos de fallo que no toleras.
  4. Arregla la red antes de ajustar: valida MTU extremo a extremo; elimina drops en interfaces; verifica simetría de enrutamiento; asegura estabilidad del puerto 2049.
  5. Elige semánticas de montaje:
    • Escrituras con estado: hard, timeo moderado, retrans relativamente bajo, TCP.
    • Solo lectura/cache: considera soft solo si tu app maneja EIO y aceptas “error en vez de colgar”.
  6. Haz montajes predecibles: pre-monta vía systemd o usa automount. Evita montajes en runtime iniciados por el arranque de contenedores cuando sea posible.
  7. Prueba fallos: desconecta la red del servidor (en laboratorio), reinicia un cliente, hace flapping a una ruta y observa. Si tu prueba es “esperar y confiar”, no estás probando.
  8. Operacionaliza: añade dashboards para retransmits, drops de interfaz, saturación de hilos NFS del servidor y latencia de disco. Añade un runbook de on-call que empiece con la Guía de diagnóstico rápido arriba.

Checklist corta para opciones de montaje (estabilidad primero)

  • Usa TCP: proto=tcp
  • Prefiere NFSv4.1+: vers=4.1 (o 4.2 si está soportado)
  • Corrección primero: hard
  • No sobre-afines: comienza con timeo=600, retrans=2 y ajusta solo con evidencia
  • Reduce churn de metadata: noatime para cargas típicas
  • Sé cauteloso con actimeo y similares; caché no es “rendimiento gratis”
  • Considera nconnect solo después de medir capacidad del servidor y del firewall

Preguntas frecuentes

1) ¿Debo usar NFSv3 o NFSv4 para volúmenes Docker?

Usa NFSv4.1+ salvo que tengas una razón de compatibilidad específica. En redes con muchos contenedores, menos daemons auxiliares y puertos suelen significar menos fallos “aleatorios” de montaje.

2) ¿Es soft aceptable alguna vez?

Sí—para datos de solo lectura o tipo cache donde un error de I/O es preferible a un colgado, y donde tu aplicación está diseñada para tratar EIO como normal. Para escrituras stateful es una mina con bandera roja.

3) ¿Por qué docker stop cuelga cuando NFS está caído?

Porque los procesos están bloqueados en I/O del kernel sobre un filesystem montado en hard. Las señales no pueden interrumpir un hilo en sueño no interrumpible. Arregla la alcanzabilidad del montaje.

4) ¿Qué hacen realmente timeo y retrans?

Gobernan el comportamiento de reintento RPC. timeo es el timeout base para un RPC; retrans es cuántos reintentos ocurren antes de que el cliente informe eventos de “not responding” (y para montajes soft, antes de fallar la I/O).

5) ¿Debo tunear rsize y wsize a valores enormes?

No por superstición. Los defaults modernos suelen ser buenos. Valores sobredimensionados pueden interactuar mal con MTU, límites del servidor o drops de red. Afina solo tras medir throughput y retransmits.

6) ¿Usar una IP en vez de un hostname ayuda?

Puede. Si DNS es inestable, lento o cambia inesperadamente, usar una IP evita la resolución de nombres como dependencia de fallo. La compensación es perder migración de servidor fácil a menos que gestiones la IP como endpoint estable.

7) ¿Qué causa “stale file handle” y cómo lo prevengo?

Normalmente lo causa un cambio en el servidor que invalida file handles: mover la ruta del export, comportamiento de failover o cambios en el filesystem bajo el export. Prevénlo manteniendo exports estables y usando métodos HA soportados por tu NAS, probándolos con clientes reales.

8) ¿Debo montar vía volúmenes Docker o pre-montar en el host?

Pre-montar (mounts systemd/automount) suele ser más predecible y más fácil de depurar. Montar desde Docker funciona, pero acopla el ciclo de vida del montaje al del contenedor, que no es donde quieres centrar tu historia de fiabilidad.

9) ¿Qué hay de nolock para arreglar cuelgues?

Evítalo a menos que estés absolutamente seguro de que tu carga no depende de locks. Puede “arreglar” problemas relacionados con lockd en NFSv3 deshabilitando locking, pero eso intercambia outages por bugs de corrección.

10) Si mi servidor NFS está bien, ¿por qué solo algunos clientes ven timeouts?

Porque “el servidor está bien” suele ser atajo para “respondió a un ping”. Problemas locales del cliente como mismatch de MTU, enrutamiento asimétrico, límites de conntrack y drops de NIC pueden romper selectivamente NFS mientras otro tráfico parece mayormente OK.

Conclusión: siguientes pasos que reducen alertas

Si luchas con tiempos de espera de volúmenes NFS en Docker, no empieces a tornear timeo como si fuera una radio.
Empieza por nombrar el fallo: ruta de red, saturación del servidor, fricción por versión de protocolo u ordering de orquestación.
Luego toma una decisión deliberada sobre semánticas: montajes correctness-first hard para escrituras stateful, y solo soft scoped y cuidadosamente para datos desechables.

Pasos prácticos que puedes hacer esta semana:

  1. Audita cada host Docker con findmnt y nfsstat -m; registra opciones reales y versión NFS.
  2. Estandariza en NFSv4.1+ sobre TCP salvo que tengas una razón en contrario.
  3. Arregla MTU y contadores de drops antes de cambiar el tuning de montajes.
  4. Mueve montajes críticos a montajes gestionados por systemd (idealmente automount) con ordering explícito network-online.
  5. Escribe un runbook basado en la Guía de diagnóstico rápido, y practícalo una vez mientras está tranquilo.

El objetivo final no es “NFS nunca falle”. El objetivo es: cuando falle, que se comporte de manera predecible, se recupere limpiamente y no convierta tus contenedores en arte moderno.

ZFS ZVOL vs Dataset: La decisión que cambia tu dolor futuro

Puedes usar ZFS durante años sin pensar nunca en la diferencia entre un dataset y un zvol.
Luego virtualizas algo importante, añades instantáneas “por si acaso”, la replicación se convierte en un requisito de junta,
y de repente tu plataforma de almacenamiento desarrolla opiniones. Opiniones fuertes.

La elección zvol vs dataset no es académica. Cambia cómo se moldea el IO, qué puede hacer la caché, cómo se comportan las instantáneas,
cómo falla la replicación y qué perillas de ajuste existen. Elegir mal no solo te deja con peor rendimiento: te deja
deuda operativa que se compone cada trimestre.

Datasets y zvols: qué son realmente (no lo que dicen en Slack)

Dataset: un sistema de archivos con superpoderes ZFS

Un dataset de ZFS es un sistema de archivos ZFS. Tiene semántica de archivos: directorios, permisos, propiedad, atributos extendidos.
Puede exportarse por NFS/SMB, montarse localmente y manipularse con herramientas normales. ZFS añade su propia capa de funciones:
instantáneas, clones, compresión, sumas de comprobación, cuotas/reservas, afinado de recordsize y toda la seguridad transaccional que
te hace dormir un poco mejor.

Cuando pones datos en un dataset, ZFS controla cómo coloca “registros” de tamaño variable (bloques, pero no bloques de tamaño fijo como
los sistemas de archivos tradicionales). Eso importa porque cambia la amplificación, la eficiencia de la caché y los patrones de IO. La perilla clave es
recordsize.

Zvol: un dispositivo de bloques tallado por ZFS

Un zvol es un volumen ZFS: un dispositivo de bloques virtual expuesto como /dev/zvol/pool/volume. No entiende archivos.
Tu sistema de archivos invitado (ext4, XFS, NTFS) o tu motor de base de datos ve un disco y escribe bloques. ZFS almacena esos bloques como objetos
con un tamaño de bloque fijo controlado por volblocksize.

Los zvols existen para casos donde el consumidor requiere un dispositivo de bloques: LUNs iSCSI, discos de VM, algunos runtimes de contenedores,
algunos hipervisores y, ocasionalmente, pilas de aplicaciones que insisten en dispositivos crudos.

La traducción al mundo real

  • Dataset = “ZFS es el sistema de archivos; los clientes hablan protocolos de archivos; ZFS ve los archivos y puede optimizar en torno a ellos.”
  • Zvol = “ZFS proporciona un disco falso; otra cosa construye un sistema de archivos; ZFS ve bloques y adivina.”

ZFS es extremadamente bueno en ambos, pero se comportan de forma distinta. El dolor viene de asumir que se comportan igual.

Un chiste corto, porque el almacenamiento necesita humildad: si quieres empezar un debate acalorado en un datacenter, saca niveles RAID.
Si quieres acabarlo, menciona “zvol volblocksize” y observa a todos revisando notas en silencio.

Reglas de decisión: cuándo usar un dataset y cuándo un zvol

Posición por defecto: los datasets son la opción aburrida — y la aburrida gana

Si tu carga puede consumir almacenamiento como un sistema de archivos (montaje local, NFS, SMB), usa un dataset. Obtienes operaciones más simples:
inspección más fácil, copia/restauración más sencilla, permisos claros y menos casos límite relacionados con tamaños de bloque y TRIM/UNMAP.
También obtienes el comportamiento de ZFS afinado para archivos por defecto.

Los datasets también son más fáciles de depurar porque tus herramientas siguen hablando “archivo”. Puedes medir fragmentación a nivel de archivo, mirar
directorios, razonar sobre metadatos y mantener un modelo mental limpio.

Cuando un zvol es la herramienta correcta

Usa un zvol cuando el consumidor requiera un dispositivo de bloques:

  • Discos de VM (especialmente para hipervisores que quieren volúmenes crudos, o cuando quieres instantáneas ZFS del disco virtual)
  • Objetivos iSCSI (los LUNs son por definición de bloque)
  • Algunas configuraciones en clúster que replican dispositivos de bloque o requieren semánticas SCSI
  • Aplicaciones legacy que solo soportan “poner la base de datos en un dispositivo crudo” (raro, pero sucede)

El modelo zvol es potente: las instantáneas de un disco de VM son rápidas, los clones son instantáneos, la replicación funciona y puedes comprimir y
sumar-check todo.

Pero: los dispositivos de bloque multiplican la responsabilidad

Cuando usas un zvol, ahora eres responsable del apilamiento entre un sistema de archivos invitado y ZFS. La alineación importa. El tamaño de bloque importa.
Las barreras de escritura importan. El comportamiento Trim/UNMAP importa. Las opciones de sync se vuelven una cuestión de política, no un detalle de ajuste.

Una matriz de decisión simple que puedes defender

  • ¿Necesitas NFS/SMB o archivos locales? Dataset.
  • ¿Necesitas LUN iSCSI o bloque crudo para hipervisor? Zvol.
  • ¿Necesitas visibilidad por archivo y restaurar un único archivo fácilmente? Dataset.
  • ¿Necesitas clones instantáneos de VM desde una imagen base? Zvol (o un dataset con archivos sparse, pero conoce tu tooling).
  • ¿Necesitas una instantánea consistente de una aplicación que maneja su propio sistema de archivos? Zvol (y coordinar flush/quiesce).
  • ¿Intentas “optimizar rendimiento” sin conocer patrones de IO? Dataset, luego mide. El ajuste heroico viene después.

Recordsize vs volblocksize: donde se decide el rendimiento

Datasets: recordsize es el tamaño máximo de un bloque de datos que ZFS usará para archivos. Archivos secuenciales grandes (backups,
medios, logs) prefieren recordsize grandes como 1M. Bases de datos y IO aleatorio prefieren valores pequeños (16K, 8K) porque reescribir una región pequeña no fuerza una gran rotación de bloques.

Zvols: volblocksize es fijo en la creación. Elegir mal y no puedes cambiarlo después sin reconstruir el zvol.
Eso no es “molesto”. Es “vamos a programar una migración en Q4 porque los gráficos de latencia parecen una sierra.”

Instantáneas: aparentemente similares, operativamente diferentes

Hacer una instantánea de un dataset captura el estado del sistema de archivos. Hacer una instantánea de un zvol captura bloques crudos. En ambos casos, ZFS usa copy-on-write,
así que la instantánea es barata al crearla y costosa más tarde si sigues reescribiendo bloques referenciados por instantáneas.

Con zvols, ese coste es más fácil de activar, porque los discos de VM reescriben bloques constantemente: actualizaciones de metadatos, churn de journales,
limpieza del sistema de archivos, incluso “no estar pasando nada” puede significar que algo está reescribiendo. Mantener instantáneas demasiado tiempo se convierte en un impuesto.

Cuotas, reservas y la trampa del thin-provisioning

Los datasets te dan quota y refquota. Los zvols te dan un tamaño fijo, pero puedes crear volúmenes sparse
y fingir que tienes más espacio del real. Eso es una decisión de negocio que se disfraza de función de ingeniería.

El thin-provisioning está bien cuando tienes monitorización, alertas y supervisión adulta. Es un desastre cuando se usa para evitar decir
“no” en una cola de tickets.

Segundo chiste corto (y el último): El thin provisioning es como pedir pantalones una talla menor como “motivación.”
A veces funciona, pero la mayoría de las veces simplemente no puedes respirar.

Modos de fallo que solo conoces en producción

Amplificación de escritura por bloques mal dimensionados

Un dataset con recordsize=1M respaldando una base de datos que hace escrituras aleatorias de 8K puede causar amplificación dolorosa: cada pequeña
actualización toca un registro grande. ZFS tiene lógica para manejar escrituras más pequeñas (y almacenará bloques más pequeños en algunos casos), pero no confíes en que te salve de un ajuste inadecuado. Mientras tanto, un zvol con volblocksize=128K sirviendo un sistema de archivos de VM que escribe bloques de 4K está igualmente desajustado.

Síntoma: buen throughput en benchmarks, latencia de cola miserable en cargas reales.

Semántica sync: donde la latencia se esconde

ZFS respeta las escrituras síncronas. Si una aplicación (o hipervisor) emite escrituras sync, ZFS debe confirmarlas de forma segura—es decir, en almacenamiento estable, no solo en RAM. Sin un dispositivo SLOG dedicado (rápido y protegido contra pérdida de energía), las cargas con muchas escrituras sync pueden tener como cuello de botella la latencia del pool principal.

Los consumidores de zvol suelen usar escrituras sync más agresivamente que los protocolos de archivos. Las VMs y bases de datos tienden a preocuparse por la durabilidad.
Los clientes NFS pueden emitir escrituras sync dependiendo de las opciones de montaje y del comportamiento de la aplicación. En cualquier caso, si los picos de latencia se correlacionan
con carga de escrituras sync, el debate “zvol vs dataset” se convierte en “¿entendemos nuestra ruta de escrituras sync?”

TRIM/UNMAP y el mito de “el espacio libre vuelve automáticamente”

Los datasets pueden liberar bloques cuando se eliminan archivos. Los zvols dependen de que el invitado emita TRIM/UNMAP (y de que tu stack lo haga pasar).
Sin ello, tu zvol parece lleno para siempre, las instantáneas se inflan y empiezas a culpar a la “fragmentación ZFS” por lo que es básicamente ausencia de señalización de recolección de basura.

Explosión de retención de instantáneas

Mantener instantáneas horarias durante 90 días de un zvol de VM se siente responsable hasta que te das cuenta de que estás reteniendo cada bloque modificado por cada actualización de Windows, ejecución de gestor de paquetes y rotación de logs. Las matemáticas se ponen feas rápido. Los datasets también sufren esto, pero el churn de VM es un tipo especial de entusiasmo.

Sorpresa en replicación: los datasets son más amigables para la lógica incremental

La replicación ZFS funciona tanto para datasets como para zvols, pero la replicación de zvol puede ser más grande y sensible al churn de bloques.
Un pequeño cambio en un sistema de archivos invitado puede reescribir bloques por todas partes. Tu send incremental puede parecer sospechosamente un send completo, y tu enlace WAN te enviará la carta de renuncia.

Fricción de herramientas: los humanos son parte del sistema

La mayoría de los equipos tienen mayor músculo operativo alrededor de sistemas de archivos que de dispositivos de bloque. Saben cómo comprobar permisos, cómo copiar
archivos, cómo montar e inspeccionar. Los flujos de trabajo con zvol te empujan a herramientas diferentes: tablas de particiones, sistemas de archivos invitados, comprobaciones a nivel de bloque.
La fricción aparece a las 3 a.m., no en la reunión de diseño.

Datos e historia interesantes para usar en reviews de diseño

  1. ZFS introdujo la suma de comprobación de extremo a extremo como característica central, no como añadido; cambió cómo la gente discutía la “corrupción silenciosa.”
  2. Copy-on-write no era nuevo cuando llegó ZFS, pero ZFS lo hizo operativo y mainstream para pilas de almacenamiento general.
  3. Los zvols fueron diseñados para integrarse con ecosistemas de bloque como iSCSI y plataformas de VM, mucho antes de que “hiperconvergente” fuera una palabra de ventas.
  4. Los valores por defecto de recordsize se eligieron para cargas de archivos de propósito general, no para bases de datos; los valores por defecto son política embebida en código.
  5. Volblocksize es inmutable después de crear el zvol en la mayoría de implementaciones comunes; ese detalle único impulsa muchas migraciones.
  6. El ARC (Adaptive Replacement Cache) hizo que el comportamiento de caché de ZFS sea distinto del de muchos sistemas de archivos tradicionales; no es “solo page cache.”
  7. El L2ARC llegó como caché de segundo nivel, pero nunca reemplazó la necesidad de dimensionar la RAM correctamente; principalmente cambia tasas de acierto, no hace milagros.
  8. Los dispositivos SLOG se convirtieron en un patrón estándar porque la latencia de escrituras síncronas domina ciertas cargas; “SSD rápido” sin protección contra pérdida de energía no es un SLOG.
  9. Send/receive de replicación dio a ZFS un primitivo de backup integrado; no es “rsync”, es un flujo de transacciones de bloques.

Tareas prácticas: comandos, salidas y qué decidir

Estos no son comandos “bonitos de demo”. Son los que ejecutas cuando intentas elegir entre un dataset y un zvol, o cuando intentas probarte que tomaste la decisión correcta.

Tarea 1: Inventario de lo que realmente tienes

cr0x@server:~$ zfs list -o name,type,used,avail,refer,mountpoint -r tank
NAME                         TYPE   USED  AVAIL  REFER  MOUNTPOINT
tank                         filesystem  1.12T  8.44T   192K  /tank
tank/vm                      filesystem   420G  8.44T   128K  /tank/vm
tank/vm/web01                volume      80.0G  8.44T  10.2G  -
tank/vm/db01                 volume     250G   8.44T   96.4G  -
tank/nfs                     filesystem  320G  8.44T   320G  /tank/nfs

Significado: Tienes tanto datasets (filesystem) como zvols (volume). Los zvols no tienen mountpoint.

Decisión: Identifica qué cargas están en volúmenes y pregunta: ¿realmente necesitan bloque, o fue solo inercia?

Tarea 2: Comprobar recordsize del dataset (y si coincide con la carga)

cr0x@server:~$ zfs get -o name,property,value recordsize tank/nfs
NAME      PROPERTY    VALUE
tank/nfs  recordsize  128K

Significado: Este dataset usa registros de 128K, un buen valor general.

Decisión: Si este dataset aloja bases de datos o imágenes de VM como archivos, considera 16K o 32K; si aloja backups, considera 1M.

Tarea 3: Comprobar volblocksize del zvol (la perilla “no se puede cambiar después”)

cr0x@server:~$ zfs get -o name,property,value volblocksize tank/vm/db01
NAME         PROPERTY     VALUE
tank/vm/db01 volblocksize 8K

Significado: Este zvol usa bloques de 8K—comúnmente razonable para IO aleatorio de bases de datos y muchos sistemas de archivos de VM.

Decisión: Si ves 64K/128K aquí para discos de arranque generales, espera amplificación de escritura y considera reconstruir correctamente.

Tarea 4: Comprobar la política sync, porque controla durabilidad vs latencia

cr0x@server:~$ zfs get -o name,property,value sync tank/vm
NAME     PROPERTY  VALUE
tank/vm  sync      standard

Significado: ZFS respetará las escrituras síncronas pero no forzará que todas las escrituras sean síncronas.

Decisión: Si alguien configuró sync=disabled “por rendimiento”, programa una conversación de riesgo y un plan de reversión.

Tarea 5: Ver estado de compresión y ratio (esto a menudo decide el coste)

cr0x@server:~$ zfs get -o name,property,value,source compression,compressratio tank/vm/db01
NAME         PROPERTY       VALUE   SOURCE
tank/vm/db01 compression    lz4     local
tank/vm/db01 compressratio  1.62x   -

Significado: LZ4 está activado y ayudando.

Decisión: Mantenlo. Si la compresión está desactivada en discos de VM, actívala salvo que tengas un cuello de botella de CPU demostrado.

Tarea 6: Comprobar cuántas instantáneas estás arrastrando

cr0x@server:~$ zfs list -t snapshot -o name,used,refer -S used | head
NAME                               USED  REFER
tank/vm/db01@hourly-2025-12-24-23  8.4G  96.4G
tank/vm/db01@hourly-2025-12-24-22  7.9G  96.4G
tank/vm/web01@hourly-2025-12-24-23 2.1G  10.2G
tank/nfs@daily-2025-12-24          1.4G  320G

Significado: La columna USED es el espacio de instantánea único a esa instantánea.

Decisión: Si las instantáneas horarias de VM consumen varios gigabytes, acorta la retención o reduce la frecuencia; las instantáneas no son gratis.

Tarea 7: Identificar “espacio retenido por instantáneas” en un zvol

cr0x@server:~$ zfs get -o name,property,value usedbysnapshots,usedbydataset,logicalused tank/vm/db01
NAME         PROPERTY         VALUE
tank/vm/db01 usedbysnapshots  112G
tank/vm/db01 usedbydataset    96.4G
tank/vm/db01 logicalused      250G

Significado: Las instantáneas están reteniendo más espacio que los datos vivos actuales.

Decisión: Tu “política de backup” ahora es una política de planificación de capacidad. Corrige la retención y considera el TRIM/UNMAP desde el invitado.

Tarea 8: Comprobar salud del pool y errores primero (afinar rendimiento en un pool enfermo es comedia)

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:14:33 with 0 errors on Sun Dec 22 03:10:11 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            ata-SSD1                ONLINE       0     0     0
            ata-SSD2                ONLINE       0     0     0

errors: No known data errors

Significado: Pool sano, scrub limpio.

Decisión: Si ves errores de suma de comprobación o vdevs degradados, detente. Arregla hardware/ruteo antes de debatir zvol vs dataset.

Tarea 9: Vigilar IO en tiempo real por dataset/zvol

cr0x@server:~$ zpool iostat -v tank 2 3
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                        1.12T  8.44T    210    980  12.4M  88.1M
  mirror-0                  1.12T  8.44T    210    980  12.4M  88.1M
    ata-SSD1                    -      -    105    490  6.2M   44.0M
    ata-SSD2                    -      -    105    490  6.2M   44.1M
--------------------------  -----  -----  -----  -----  -----  -----

Significado: Ves IOPS de lectura/escritura y ancho de banda; esto te dice si estás limitado por IOPS o por throughput.

Decisión: Altas IOPS de escritura con bajo ancho de banda sugiere escrituras pequeñas aleatorias—volblocksize y la ruta sync importan mucho.

Tarea 10: Comprobar presión del ARC (porque la RAM es tu primera capa de almacenamiento)

cr0x@server:~$ arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  size     c
12:20:01   532    41      7    12   2     29   5      0   0  48.1G  52.0G
12:20:02   611    58      9    16   2     42   7      0   0  48.1G  52.0G
12:20:03   590    55      9    15   2     40   7      0   0  48.1G  52.0G

Significado: Las tasas de miss del ARC son bajas; la caché está saludable.

Decisión: Si miss% es consistentemente alto bajo carga, añadir RAM a menudo supera cambios ingeniosos en el layout de almacenamiento.

Tarea 11: Comprobar si un zvol es sparse (thin) y si es seguro

cr0x@server:~$ zfs get -o name,property,value refreservation,volsize,used tank/vm/web01
NAME          PROPERTY        VALUE
tank/vm/web01 refreservation  none
tank/vm/web01 volsize         80G
tank/vm/web01 used            10.2G

Significado: Sin reservation: esto es efectivamente thin desde la perspectiva de “espacio garantizado”.

Decisión: Si ejecutas VMs críticas, establece refreservation para garantizar espacio o acepta el riesgo de fallos por pool lleno.

Tarea 12: Confirmar ashift (tu línea base de alineación de sector físico)

cr0x@server:~$ zdb -C tank | grep -E 'ashift|vdev_tree' -n | head
45:        vdev_tree:
67:            ashift: 12

Significado: ashift=12 significa sectores de 4K. Generalmente correcto para SSDs y HDDs modernos.

Decisión: Si ashift es incorrecto (demasiado pequeño), el rendimiento puede quedar dañado permanentemente; reconstruyes el pool, no lo “tuneas.”

Tarea 13: Evaluar tamaño de envío de instantáneas antes de replicar por un enlace pequeño

cr0x@server:~$ zfs send -nvP tank/vm/db01@hourly-2025-12-24-23 | head
send from @hourly-2025-12-24-22 to tank/vm/db01@hourly-2025-12-24-23 estimated size is 18.7G
total estimated size is 18.7G

Significado: La replicación incremental sigue siendo 18.7G—el churn de bloques es alto.

Decisión: Reduce frecuencia/retención de instantáneas, mejora TRIM del invitado o cambia arquitectura (almacenamiento de apps basado en dataset) si es posible.

Tarea 14: Comprobar si TRIM está habilitado en el pool

cr0x@server:~$ zpool get -o name,property,value autotrim tank
NAME  PROPERTY  VALUE
tank  autotrim  on

Significado: El pool está haciendo trim de bloques liberados hacia los SSDs.

Decisión: Si autotrim está off en pools SSD, considera activarlo—especialmente si dependes de que los invitados zvol devuelvan espacio.

Tarea 15: Comprobar propiedades por dataset que cambian comportamiento de forma sigilosa

cr0x@server:~$ zfs get -o name,property,value atime,xattr,primarycache,logbias tank/vm
NAME     PROPERTY      VALUE
tank/vm  atime         off
tank/vm  xattr         sa
tank/vm  primarycache  all
tank/vm  logbias       latency

Significado: atime off reduce escrituras de metadatos; xattr=sa almacena xattrs de forma más eficiente; logbias=latency favorece latencia sync.

Decisión: Para zvols de VM, logbias=latency suele ser razonable. Si logbias=throughput aparece, valida que no fuera un ajuste por imitación.

Guía de diagnóstico rápido (encuentra el cuello de botella en minutos)

Cuando el rendimiento es malo, el debate zvol vs dataset a menudo se convierte en una distracción. Usa esta secuencia para localizar el límite real rápidamente.

Primero: prueba que el pool no esté enfermo

  1. Ejecuta: zpool status -v
    Busca: vdevs degradados, errores de suma de comprobación, resilver en progreso, scrub lento.
    Interpretación: Si el pool está malsano, todo lo demás es ruido.
  2. Ejecuta: dmesg | tail (y los logs específicos del SO)
    Busca: resets de enlace, timeouts, errores NVMe, problemas de HBA.
    Interpretación: Un camino de disco intermitente parece “picos de latencia aleatorios.”

Segundo: clasifica el IO (pequeño aleatorio vs grande secuencial, sync vs async)

  1. Ejecuta: zpool iostat -v 2
    Busca: IOPS altas con MB/s bajos (aleatorio) vs MB/s altos (secuencial).
    Interpretación: El IO aleatorio tensiona la latencia, el secuencial tensiona el ancho de banda.
  2. Ejecuta: zfs get sync tank/... y revisa configuraciones de la aplicación
    Busca: cargas sync sin SLOG o en medios lentos.
    Interpretación: Las escrituras sync expondrán la ruta duradera más lenta.

Tercero: comprueba memoria y caché antes de comprar hardware

  1. Ejecuta: arcstat 1
    Busca: miss% alto, ARC sin crecer, presión de memoria.
    Interpretación: Si fallas caché constantemente, estás forzando lecturas desde disco que podrías evitar.
  2. Ejecuta: zfs get primarycache secondarycache tank/...
    Busca: alguien configuró caché solo para metadatos “para ahorrar RAM.”
    Interpretación: Eso puede ser válido en algunos diseños, pero a menudo es autolesión accidental.

Cuarto: valida tamaño de bloque e impuesto de instantáneas

  1. Ejecuta: zfs get recordsize tank/dataset o zfs get volblocksize tank/volume
    Interpretación: Desajuste = amplificación = latencia de cola.
  2. Ejecuta: zfs get usedbysnapshots tank/...
    Interpretación: Si las instantáneas retienen espacio masivo, también aumentan trabajo de metadatos y asignación.

Tres micro-historias corporativas (anonimizadas, pero dolorosamente reales)

Micro-historia 1: Un incidente causado por una suposición errónea (“un zvol es básicamente un dataset”)

Una empresa SaaS mediana migró de un SAN legacy a ZFS. El ingeniero de almacenamiento—inteligente, rápido, un poco demasiado confiado—estandarizó en zvols
para todo “porque los discos de VM son volúmenes, y eso es lo que hacía el SAN.” El almacenamiento tipo NFS también se movió a sistemas de archivos ext4 respaldados por zvols
montados en clientes Linux. Funcionó en pruebas. Incluso funcionó en producción por un tiempo.

Las primeras señales fueron sutiles: ventanas de backup más largas. La replicación empezó a perder su RPO, pero solo en ciertos volúmenes.
Luego un pool que había estado estable meses golpeó un precipicio de capacidad. “Tenemos 30% libre,” dijo alguien, señalando el dashboard del pool. “¿Entonces por qué no podemos crear un nuevo disco VM?”

La respuesta fueron las instantáneas. Los zvols se instantaneaban cada hora, y los sistemas de archivos invitados estaban churnando bloques constantemente. Los archivos eliminados dentro de los invitados no se traducían en bloques liberados a menos que TRIM atravesara toda la pila. No lo hizo, porque los OS invitados no estaban configurados y la ruta del hipervisor no lo pasaba limpio.

Mientras tanto, la carga tipo NFS ejecutándose dentro del ext4 invitado no tenía razón de estar dentro de un zvol. Querían semántica de archivos pero construyeron una tarta de capas file-on-block-on-ZFS. La respuesta en on-call fue borrar “instantáneas viejas” hasta que el pool dejó de gritar, lo que funcionó brevemente y luego se convirtió en un ritual de emergencia.

La solución no fue glamorosa: migrar datos tipo NFS a datasets exportados directamente, implementar retención sensata para instantáneas de zvols, y validar TRIM extremo a extremo. Tomó un mes de migración cuidadosa deshacer un diseño basado en la suposición errónea de que “volumen vs sistema de archivos es solo empaquetado.”

Micro-historia 2: Una optimización que salió mal (“set sync=disabled, está bien”)

Otra organización, cercana a finanzas y extremadamente alérgica al downtime, ejecutaba un clúster de bases de datos virtualizado. La latencia subía en horas pico. Alguien buscó en foros, encontró las palabras mágicas sync=disabled, y propuso un remedio rápido. El cambio se hizo en la jerarquía de zvol que respaldaba los discos VM.

La latencia mejoró inmediatamente. Los gráficos se veían geniales. El equipo declaró victoria y siguió con otros incendios. Durante unas semanas todo estuvo tranquilo, que es exactamente cómo el riesgo te enseña a ignorarlo.

Luego hubo un evento de energía: no un apagado limpio, no un failover ordenado—solo un momento donde el plan UPS se encontró con la realidad y la realidad ganó. El hipervisor volvió. Varias VMs arrancaron. Algunas no. La base de datos sí, pero revirtió más transacciones de las que gustaron, y al menos un sistema de archivos requirió reparación.

La revisión del incidente fue incómoda porque nadie pudo decir, con la cara seria, que no habían cambiado durabilidad por rendimiento. Lo habían hecho. Eso es lo que hace esa opción. La reversión fue restaurar sync=standard y añadir un SLOG apropiado con protección contra pérdida de energía. La solución a largo plazo fue cultural: ningún “arreglo de rendimiento” que cambie la semántica de durabilidad sin aceptación de riesgo escrita y una prueba que simule pérdida de energía.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día (probar tamaño de send y disciplina de instantáneas)

Un gran equipo de plataforma interna gestionaba dos datacenters con replicación ZFS entre ellos. Tenían el hábito, que parecía tedioso:
antes de incorporar una nueva carga, ejecutarían un “ensayo de replicación” de una semana con instantáneas y zfs send -nvP para estimar tamaños incrementales. También aplicaban políticas de retención como adultos: retención corta para volúmenes con churn, más larga para datasets con datos estables.

Un equipo de producto solicitó “instantáneas horarias por seis meses” para una flota de VMs. El equipo de plataforma no discutió filosóficamente. Hicieron el ensayo. Los incrementales eran enormes y erráticos, y el enlace WAN se habría saturado regularmente. En vez de decir “no”, ofrecieron una alternativa aburrida: diario con retención larga, horario corto con retención breve, más backups a nivel de aplicación para los datos críticos. También movieron algunos datos fuera de discos VM a datasets exportados por NFS, porque eran datos de archivos fingiendo ser datos de bloque.

Meses después, una interrupción en un sitio forzó un failover. La replicación estaba al día, la recuperación fue predecible y el postmortem fue deliciosamente poco eventful. El crédito fue para una práctica que nadie quería hacer porque no era “ingeniería”, era “proceso.”
Ahorró el día de todos.

Errores comunes: síntomas → causa raíz → corrección

1) El almacenamiento de VM es lento y con picos, especialmente durante actualizaciones

  • Síntomas: picos de latencia en cola, interfaces congeladas, arranques lentos, paradas periódicas de IO.
  • Causa raíz: volblocksize del zvol desajustado al tamaño de IO del invitado; instantáneas retenidas demasiado tiempo; escrituras sync bloqueándose en medios lentos.
  • Corrección: reconstruir zvols con volblocksize sensato (a menudo 8K o 16K para VMs generales), reducir retención de instantáneas, validar SLOG para cargas sync-heavy.

2) El pool muestra mucho espacio libre, pero aparece comportamiento de “sin espacio”

  • Síntomas: fallan asignaciones, escrituras bloqueadas, no se pueden crear nuevos zvols, ENOSPC extraño en invitados.
  • Causa raíz: thin provisioning sin refreservation; instantáneas reteniendo espacio; pool demasiado lleno (ZFS necesita margen).
  • Corrección: aplicar reservas para zvols críticos, borrar/expirar instantáneas, mantener el pool por debajo de un umbral sensato y alertas de capacidad que incluyan crecimiento de instantáneas.

3) Incrementales de replicación son enormes para VMs basadas en zvol

  • Síntomas: send/receive que tarda eternamente, saturación de red, incumplimiento de RPO.
  • Causa raíz: churn del sistema de archivos invitado; falta de TRIM/UNMAP; intervalo de instantáneas mal elegido.
  • Corrección: habilitar y verificar TRIM desde el invitado hacia el zvol, ajustar cadencia de instantáneas, mover datos tipo archivo a datasets y probar tamaños estimados de send antes de fijar la política.

4) “Deshabilitamos sync y no pasó nada” (todavía)

  • Síntomas: latencia asombrosa; dashboards sospechosamente calmados; sin fallos inmediatos.
  • Causa raíz: se cambiaron las semánticas de durabilidad; estás reconociendo escrituras antes de que sean seguras.
  • Corrección: revertir a sync=standard o sync=always según corresponda; añadir un SLOG apropiado; probar escenarios de pérdida de energía y documentar aceptación de riesgo si insistes en “engañar a la física.”

5) Carga NFS funciona mal cuando está dentro de una VM en un zvol

  • Síntomas: cargas con muchos metadatos son lentas; backups y restauraciones son torpes; la resolución de problemas es dolorosa.
  • Causa raíz: apilamiento innecesario: carga de archivos colocada dentro de un sistema de archivos invitado encima de un zvol, perdiendo optimizaciones y visibilidad a nivel de archivo de ZFS.
  • Corrección: almacenar y exportar como dataset directamente; ajustar recordsize y atime; mantener la pila simple.

6) El rollback de instantánea “funciona” pero la app queda corrupta

  • Síntomas: después del rollback, el sistema de archivos se monta pero los datos de la aplicación son inconsistentes.
  • Causa raíz: desajuste de consistencia por crash; las instantáneas de zvol capturan bloques, no la quiescencia de la aplicación; las instantáneas de dataset también requieren coordinación para consistencia.
  • Corrección: quiescer aplicaciones (fsfreeze, flush de DB, hooks del agente de invitado del hipervisor) antes de crear instantáneas; validar procedimientos de restauración periódicamente.

Listas de verificación / plan paso a paso

Paso a paso: elegir dataset vs zvol para una nueva carga

  1. Identifica la interfaz que necesitas: protocolo de archivos (NFS/SMB/montaje local) → dataset; protocolo de bloque (iSCSI/disco VM) → zvol.
  2. Anota supuestos de patrón de IO: mayormente secuencial? mayormente aleatorio? ¿sync-heavy? Esto decide recordsize/volblocksize y necesidades de SLOG.
  3. Elige la capa más simple viable: evita file-on-block-on-ZFS a menos que sea imprescindible.
  4. Activa compresión lz4 por defecto salvo que se demuestre lo contrario.
  5. Decide la política de instantáneas desde el inicio: frecuencia y retención; no dejes que crezca como “sustituto de backup.”
  6. Decide expectativas de replicación: haz un ensayo con tamaños estimados de send si te importan RPO/RTO.
  7. Guardarraíles de capacidad: reservas para zvols críticos; cuotas para datasets; mantén margen en el pool.
  8. Documenta recuperación: cómo restaurar un archivo, una VM o una base de datos; incluye pasos de quiescing.

Lista: configurar un zvol para discos de VM (base de producción)

  • Crear con un volblocksize sensato (a menudo 8K o 16K; coincide con las realidades del invitado e hipervisor).
  • Habilitar compression=lz4.
  • Mantener sync=standard; añadir SLOG si la latencia sync importa.
  • Planear retención de instantáneas acorde al churn; probar zfs send -nvP para dimensionar replicación.
  • Verificar TRIM/UNMAP extremo a extremo si esperas recuperación de espacio.
  • Considerar refreservation para invitados críticos y prevenir catástrofes por pool lleno.

Lista: configurar un dataset para apps y almacenamiento de archivos

  • Elegir recordsize según IO: 1M para backups/media; más pequeño para patrones tipo BD.
  • Habilitar compression=lz4.
  • Desactivar atime salvo que realmente lo necesites.
  • Usar quota/refquota para evitar que “un tenant se coma el pool.”
  • Instantanear con retención, no acaparamiento.
  • Exportar vía NFS/SMB con ajustes de cliente sensatos; medir con cargas reales.

Preguntas frecuentes (FAQ)

1) ¿Un zvol siempre es más rápido que un dataset para discos de VM?

No. Un zvol puede ser excelente para discos de VM, pero “más rápido” depende del comportamiento sync, el tamaño de bloque, el churn de instantáneas y la ruta IO del hipervisor. Un dataset que aloje QCOW2/raw también puede rendir muy bien con el recordsize y caché adecuados. Mide, no vibes.

2) ¿Puedo cambiar volblocksize después?

Prácticamente: no. Trata volblocksize como inmutable. Si elegiste mal, la solución limpia es migrar a un nuevo zvol con el tamaño correcto y un corte controlado.

3) ¿Debería poner recordsize=16K para bases de datos en datasets?

A menudo razonable, pero no universal. Muchas bases de datos usan páginas de 8K; 16K puede ser un compromiso decente. Pero si tu carga es mayormente escaneos secuenciales o blobs grandes, recordsize mayor puede ayudar. Perfilá tu IO.

4) ¿Son las instantáneas ZFS backups?

Son un bloque poderoso, no una estrategia de backup. Las instantáneas no protegen contra pérdida del pool, errores operativos en el pool o corrupción replicada si replicás muy pronto. Usa instantáneas junto con replicación y/o almacenamiento de backup separado y retenciones.

5) ¿Por qué eliminar archivos dentro de una VM no libera espacio en el pool ZFS?

Porque ZFS ve un dispositivo de bloques. A menos que el invitado emita TRIM/UNMAP y la pila lo pase, ZFS no sabe qué bloques están libres dentro del sistema de archivos invitado.

6) ¿Debería usar dedup en zvols para ahorrar espacio?

Usualmente no. Dedup consume mucha RAM y es operativamente implacable. La compresión típicamente te da ganancias seguras con menos riesgo. Si quieres dedup, pruébalo con datos realistas y presupuestá RAM en serio.

7) ¿Un SLOG ayuda todas las escrituras?

No. Un SLOG ayuda las escrituras síncronas. Si tu carga es mayormente asíncrona, un SLOG no moverá mucho la aguja. Si tu carga es sync-heavy, un SLOG apropiado puede ser la diferencia entre “bien” y “todo en llamas.”

8) ¿Cuándo debería preferir datasets para contenedores?

Si tu plataforma de contenedores puede usar datasets ZFS directamente (común en muchas configuraciones Linux), los datasets suelen dar mejor visibilidad y operaciones más simples que meter almacenamiento de contenedores en discos VM sobre zvols. Mantén capas mínimas.

9) ¿Puedo usar sync=disabled con seguridad para discos VM si tengo UPS?

Un UPS reduce riesgo; no lo elimina. Existen kernel panics, resets de controladores, bugs de firmware y error humano. Si necesitas durabilidad, mantén semánticas sync correctas e ingeniería del camino de hardware (SLOG con protección) para soportarlo.

10) ¿Cuál es el mejor predeterminado: zvol o dataset?

Por defecto usa dataset a menos que el consumidor requiera bloque. Cuando necesites bloque, usa zvols intencionalmente: elige volblocksize, planifica instantáneas y confirma TRIM y comportamiento sync.

Próximos pasos que puedes hacer esta semana

Aquí está la ruta práctica que reduce dolor futuro sin convertir tu almacenamiento en un experimento científico.

  1. Inventario tu entorno: lista datasets vs volúmenes y mapea a cargas. Cualquier cosa “tipo archivo” viviendo dentro de un zvol es una bandera roja para investigar.
  2. Audita las perillas irreversibles: comprueba volblocksize en zvols y recordsize en datasets clave. Anota desajustes con patrones de carga.
  3. Mide el impuesto de instantáneas: identifica qué zvols/datasets tienen gran usedbysnapshots. Alinea la retención con la necesidad del negocio, no con la ansiedad.
  4. Valida comportamiento sync: encuentra cualquier sync=disabled y trátalo como solicitud de cambio que necesita aceptación de riesgo explícita. Si la latencia sync es un problema, ingeniería con SLOG, no pensamiento mágico.
  5. Corre un ensayo de replicación: usa zfs send -nvP para estimar incrementales durante una semana. Si los números son salvajes, arregla las causas de churn antes de prometer RPOs estrictos.

Una idea parafraseada de John Allspaw (operaciones/confiabilidad): Los incidentes nacen del trabajo normal en sistemas complejos, no de una mala persona teniendo un mal día. (idea parafraseada)

La elección zvol vs dataset es exactamente ese tipo de decisión de “trabajo normal”. Tómala deliberadamente. El tú del futuro seguirá teniendo incidentes,
pero serán del tipo interesante—los que puedes arreglar—en lugar del tipo lento y corrosivo causado por un primitivo de almacenamiento elegido hace años y defendido por orgullo.

Docker Compose: las variables de entorno te traicionan — errores de .env que rompen producción

El incidente empieza pequeño. Un contenedor arranca con NODE_ENV=development, o tu base de datos de repente acepta
conexiones con una contraseña por defecto. Nada “cambió” en la aplicación. El job de CI está en verde. Enviaste el mismo
archivo Compose que enviaste la semana pasada.

Lo que cambió fue la parte más frágil de tu despliegue: la cadena invisible de variables de entorno que atraviesa Docker Compose, tu shell y un pequeño archivo .env que nadie revisa porque “no es código”.
Es código. Simplemente no se linttea.

Un modelo mental que no te engañará

Docker Compose usa las variables de entorno de dos maneras diferentes, y la mayoría de fallos en producción ocurren cuando los equipos
las tratan como si fueran lo mismo:

1) Variables usadas por Compose en sí (tiempo de renderizado)

Estas variables existen para renderizar la configuración de Compose: cosas como ${IMAGE_TAG} dentro
de compose.yaml, COMPOSE_PROJECT_NAME o COMPOSE_PROFILES.
Compose las resuelve antes de arrancar los contenedores. Si Compose las resuelve mal, puede que los contenedores ni siquiera sean los
que crees que desplegaste.

2) Variables pasadas a los contenedores (tiempo de ejecución)

Estas variables forman parte del entorno del contenedor: lo que tu app lee vía getenv.
Provienen de environment:, env_file:, y a veces del shell del host mediante
paso implícito.

Las variables de renderizado influyen en el YAML final. Las variables en tiempo de ejecución influyen en el comportamiento del proceso dentro del contenedor.
Confundir estas dos es cómo terminas “arreglando” un bug del contenedor editando el perfil de shell del host, para luego descubrir que
systemd no lee tu perfil de shell.

Una verdad operativa: no puedes “simplemente revisar el archivo .env.” Tienes que comprobar qué renderizó realmente Compose y qué recibió realmente el contenedor.

Cita para tener en tu escritorio: idea parafraseada“La esperanza no es una estrategia.” (idea parafraseada atribuida a
Gordon Sullivan, frecuentemente citada en círculos de ingeniería y fiabilidad)

Broma #1: Las variables de entorno son como el chisme de oficina—todos juran que no lo empezaron, pero de algún modo está en cada habitación.

Hechos e historia que deberías conocer (para dejar de discutir con YAML)

  • Hecho 1: El archivo .env usado por Docker Compose no es automáticamente el mismo formato que un script de shell. Es un parser más simple “KEY=VALUE” con sus propias particularidades.
  • Hecho 2: Compose se desarrolló originalmente a partir de Fig (2014), y gran parte de su comportamiento con variables es conveniencia heredada más que una elegancia de diseño pura.
  • Hecho 3: Compose v2 está implementado como un plugin del CLI de Docker, y el comportamiento puede variar sutilmente entre versiones porque ahora el motor está más cercano al ecosistema del CLI de Docker.
  • Hecho 4: Compose usa variables de entorno tanto para el renderizado de la configuración como para el entorno de los contenedores; reglas de precedencia diferentes aplican para cada ruta.
  • Hecho 5: La interpolación de variables sucede antes de la mayoría de validaciones. Una variable faltante puede convertirse silenciosamente en una cadena vacía y aún formar un valor YAML “válido”.
  • Hecho 6: env_file es entrada en tiempo de ejecución para contenedores; generalmente no influye en la interpolación de Compose a menos que explícitamente cargues variables en el shell o uses una cadena de herramientas que lo haga.
  • Hecho 7: El comando docker compose config es lo más parecido a un suero de la verdad: muestra la configuración completamente renderizada que Compose ejecutará.
  • Hecho 8: El mismo proyecto en dos hosts puede renderizar de forma diferente porque Compose lee el entorno del host, el directorio actual y entradas opcionales --env-file.
  • Hecho 9: COMPOSE_PROJECT_NAME afecta nombres de redes, nombres de volúmenes y nombres de contenedores. Un cambio de nombre de proyecto puede “huir” volúmenes antiguos y crear volúmenes nuevos.

Guion de diagnóstico rápido

Cuando producción está en llamas, no necesitas filosofía. Necesitas una secuencia que reduzca rápidamente el radio de daño.
Aquí está el orden que uso porque separa bugs de “tiempo de renderizado” de bugs de “tiempo de ejecución” en minutos.

Primero: confirma qué renderizó Compose

  1. Ejecuta docker compose config e inspecciona los valores interpolados (tags de imagen, puertos, rutas de volúmenes,
    nombre del proyecto, perfiles). Si la config renderizada es errónea, no pierdas tiempo dentro de los contenedores.
  2. Revisa cadenas vacías, valores tipo “null”, valores por defecto inesperados o definiciones duplicadas de servicios debido a perfiles.

Segundo: confirma qué recibió realmente el contenedor

  1. Inspecciona el entorno del contenedor (docker inspect) o imprime dentro del contenedor
    (env).
  2. Compáralo con lo que crees haber establecido vía env_file y environment.

Tercero: confirma qué .env y qué entorno del host se usaron

  1. Verifica el directorio de trabajo y el archivo env seleccionado. Si ejecutaste Compose desde el directorio equivocado, podrías
    estar usando el .env incorrecto.
  2. Revisa CI/CD: ¿pasa --env-file? ¿exporta variables? ¿systemd limpia el entorno?

Si el almacenamiento o la red se ven raros, sospecha del nombre de proyecto y nombres de volúmenes

Un COMPOSE_PROJECT_NAME cambiado o un cambio de nombre de directorio puede crear redes nuevas y volúmenes nuevos.
La app “perdió” sus datos porque está escribiendo en un volumen distinto con otro nombre.

Tareas prácticas: comandos, salidas y decisiones

Estas son las pruebas de campo. Cada una incluye: un comando, qué significa una salida típica y la decisión que tomas.
Ejecútalas en orden cuando no estés seguro de dónde se oculta la verdad.

Tarea 1: Verificar la versión de Compose (el comportamiento varía)

cr0x@server:~$ docker compose version
Docker Compose version v2.24.6

Significado: Estás en Compose v2.x. Bien—la mayoría de comportamientos y flags modernos aplican.
Si esto fuera v1, varios flags y comportamientos límite difieren.
Decisión: Captura esta versión en las notas del incidente; si el comportamiento difiere entre hosts, alinea versiones.

Tarea 2: Ver qué nombre de proyecto piensa Compose

cr0x@server:~$ docker compose ls
NAME                STATUS              CONFIG FILES
payments-prod        running(6)          /srv/payments/compose.yaml

Significado: El proyecto es payments-prod. Redes/volúmenes se prefijarán con eso.
Decisión: Si esperabas otro nombre de proyecto, detente: podrías estar operando sobre el proyecto equivocado.

Tarea 3: Renderizar la config totalmente interpolada (la “verdad”)

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3
    ports:
      - mode: ingress
        target: 8080
        published: "8080"
        protocol: tcp
  db:
    environment:
      POSTGRES_DB: payments
    image: postgres:15
volumes:
  payments-prod_db-data: {}
networks:
  default:
    name: payments-prod_default

Significado: La interpolación ocurrió. Esto es lo que Compose ejecutará.
Decisión: Si el tag de imagen o el puerto están mal aquí, el bug está en la resolución de variables (no en el runtime del contenedor).

Tarea 4: Identificar qué archivo env se está usando

cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan  2 09:11 /srv/payments/.env

Significado: Existe un .env local en el directorio del proyecto.
Decisión: Verifica que estés ejecutando Compose desde este directorio; de lo contrario no estarás leyendo este archivo.

Tarea 5: Detectar minas de espacio en blanco y comillas en .env

cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"

Significado: BAD_SPACES =oops es sospechoso: muchos parsers tratan esa clave como BAD_SPACES (con un espacio final) o la rechazan.
QUOTED="literal quotes?" puede preservar las comillas dependiendo del parser.
Decisión: Arregla el formato: no espacios alrededor de =, evita comillas a menos que conozcas el comportamiento del parser.

Tarea 6: Comprobar si falta una variable en tiempo de renderizado

cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12:    image: registry.local/payments-api:${IMAGE_TAG}

Significado: Compose necesita IMAGE_TAG para renderizar la cadena de imagen.
Decisión: Asegura que IMAGE_TAG esté establecido en el .env correcto o exportado en el entorno usado por Compose.

Tarea 7: Detectar interpolación vacía silenciosa

cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:

Significado: Un IMAGE_TAG vacío renderiza una referencia de imagen medio inválida que aún podría pasar el parseo YAML.
Decisión: Añade comprobaciones de variables requeridas usando valores por defecto/interpolación de Compose (ver más adelante) y falla el CI en vacíos.

Tarea 8: Inspeccionar el entorno dentro de un contenedor en ejecución

cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info

Significado: El contenedor recibió variables. Si falta algo, es un problema de inyección de entorno en tiempo de ejecución.
Decisión: Compáralo con docker compose config y el contenido de env_file.

Tarea 9: Confirmar qué piensa Docker que es el entorno (autoritativo)

cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]

Significado: Esto es lo que Docker pasará al proceso. Si no está aquí, tu app no lo verá.
Decisión: Si la config de Compose lo muestra pero inspect no, tienes una deriva de despliegue o un problema de recreación.

Tarea 10: Detectar problemas de “no recreó el contenedor”

cr0x@server:~$ docker compose up -d
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Running

Significado: Compose no recreó contenedores. Los cambios de env no se aplicarán a un contenedor en ejecución a menos que se recree.
Decisión: Si cambiaste vars de entorno, fuerza la recreación: docker compose up -d --force-recreate.

Tarea 11: Forzar recrear y confirmar que se aplicó el nuevo entorno

cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
 ✔ Container payments-prod-db-1   Running
 ✔ Container payments-prod-api-1  Started

Significado: El contenedor API fue reiniciado/recreado.
Decisión: Repite la Tarea 8/9 para confirmar que los cambios de entorno realmente llegaron.

Tarea 12: Detectar deriva de nombre de proyecto que crea volúmenes “nuevos”

cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local     payments-prod_db-data
local     payments_db-data

Significado: Existen dos volúmenes con nombres similares, probablemente de nombres de proyecto diferentes.
Decisión: Confirma qué volumen está adjunto al contenedor DB en ejecución antes de “limpiar” nada.

Tarea 13: Confirmar qué volumen está usando realmente un contenedor

cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data

Significado: La BD está usando payments-prod_db-data.
Decisión: Si la app “perdió datos”, compáralo con el volumen que esperabas. No borres volúmenes hasta que pruebes que no se usan.

Tarea 14: Identificar qué .env se usa cuando ejecutas desde otro directorio

cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
  api:
    image: registry.local/payments-api:

Significado: Ejecutar desde /tmp probablemente hizo que Compose no encontrara el /srv/payments/.env previsto, por lo que la interpolación falló.
Decisión: Siempre ejecuta desde el directorio del proyecto o suministra --env-file /srv/payments/.env.

Tarea 15: Probar precedencia entre host y .env

cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7:    image: registry.local/payments-api:2.0.0

Significado: La variable exportada en el host sobreescribió el valor en .env.
Decisión: En producción, evita confiar en “lo que esté exportado en el shell”. Haz explícita la fuente de las variables.

Tarea 16: Detectar CRLF accidental de Windows en .env (sí, aún sucede)

cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators

Significado: CRLF puede colarse en claves/valores, causando desconcertantes “variable no encontrada” o valores con \r ocultos.
Decisión: Convierte a LF: sed -i 's/\r$//' /srv/payments/.env, luego vuelve a renderizar la config.

Tarea 17: Confirmar retornos de carro ocultos en un valor específico

cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'

Significado: Ese \r final es real. Puede romper tokens de autenticación, URLs o contraseñas.
Decisión: Normaliza finales de línea en CI y trata .env como un artefacto de texto que necesita comprobaciones.

Tarea 18: Mostrar diferencias entre env_file y environment en la config final

cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
  api:
    environment:
      DB_HOST: db
      LOG_LEVEL: info
    image: registry.local/payments-api:1.9.3

Significado: Puedes ver los valores inline de environment: claramente. Si usaste env_file, puede que no se expanda inline en la salida como esperas.
Decisión: Si tu auditoría depende de ver variables, no confíes solo en env_file; usa pasos explícitos de validación de configuración.

Precedencia y alcance: quién gana cuando las variables chocan

La mayoría de equipos no puede responder esta pregunta sin adivinar: “Si establezco FOO en el shell, en .env,
y en environment:, ¿cuál gana?” La respuesta depende de si te refieres a tiempo de renderizado o tiempo de ejecución.
Por eso esto sigue rompiéndose en producción: la gente habla sin comprenderse.

Precedencia en tiempo de renderizado (interpolación en compose.yaml)

Cuando Compose interpola ${VAR} en el YAML, mira sus fuentes de entorno. En la práctica, el entorno exportado del proceso Compose es el competidor fuerte. El .env local suele ser un comodín de respaldo.

En otras palabras: si tu CI exporta IMAGE_TAG, normalmente sobreescribirá el .env. Si tu unidad systemd ejecuta Compose con un entorno mayormente vacío, ignorará lo que tenía tu shell interactivo.

Regla operativa: las variables de renderizado deben ser explícitas. O pásalas vía CI de forma controlada o suministra un env file explícito con --env-file. No dejes que shells aleatorios decidan.

Precedencia en tiempo de ejecución (lo que recibe el contenedor)

El entorno del contenedor se construye a partir de las definiciones de servicio de Compose:

  • environment: entradas son explícitas y visibles en el archivo Compose.
  • env_file: carga pares clave/valor desde un archivo al entorno del contenedor.
  • Algunas variables pueden pasarse desde el host si las referencias en environment: sin valor, dependiendo de la sintaxis.

Regla práctica: trata environment: como una API de lo que el contenedor espera y
trata env_file como un detalle de implementación de cómo le das esos valores. Al depurar,
siempre comprueba qué llegó realmente en docker inspect.

El nombre de proyecto es parte secreta de la historia del entorno

COMPOSE_PROJECT_NAME parece “solo un nombre.” No lo es.
Cambia nombres de redes y volúmenes. Si atás tus datos a volúmenes y tu monitorización a nombres de contenedores,
el nombre del proyecto es una variable de producción, lo reconozcas o no.

Interpolación y parseo: los bordes afilados en .env y Compose

El formato .env parece shell. No es shell. Es un archivo key/value con la flexibilidad justa para que te sientas demasiado confiado.

Espacios en blanco: el asesino silencioso

Muchos parsers tratan KEY =value como una clave diferente a KEY=value, o la rechazan.
De cualquier modo, terminas con “KEY no establecido” y Compose sustituye silenciosamente una cadena vacía.

No “seas tolerante.” Sé estricto. Para archivos env en producción:
no espacios alrededor del igual, y las claves deberían coincidir con [A-Z0-9_]+.

Comillas: a veces literales, a veces eliminadas, siempre confusas

En algunos ecosistemas, FOO="bar" significa que el valor es bar sin comillas. En otros, esas
comillas forman parte del valor. El comportamiento de Compose puede sorprender dependiendo de qué ruta de parseo estés usando.
La única postura segura: evita comillas en .env a menos que hayas verificado el comportamiento con docker compose config y un contenedor en ejecución.

Defaults de interpolación: úsalos, pero entiéndelos

Compose soporta patrones como:
${VAR:-default} y ${VAR?error} en muchos contextos.
Aquí es donde los equipos pueden convertir un fallo invisible en un fallo ruidoso.

Si IMAGE_TAG debe existir en prod, hazlo requerido. Si LOG_LEVEL puede tener un valor por defecto, asígnalo.
Falla rápido en todo lo que cambie el comportamiento de formas que no puedas ver.

Vacío es distinto de no establecido

La interpolación de Compose a menudo trata una variable vacía como “establecida,” lo que puede derrotar los defaults. Si una pipeline
pone IMAGE_TAG como cadena vacía (sí, sucede), tu ${IMAGE_TAG:-latest} puede o no comportarse como esperas.
Prueba esto explícitamente en tu entorno.

.env vs env_file: mismo aspecto, distinta semántica

El archivo .env (para Compose) se usa para la interpolación de variables de Compose y algunas configuraciones de Compose.
La directiva env_file: alimenta el entorno de ejecución de los contenedores.
La gente los mezcla porque el contenido de los archivos parece idéntico. El resultado es de confianza caótica.

Si quieres que valores influyan en la interpolación, deben estar en el entorno que Compose usa para renderizar
(exportados en el shell, --env-file explícito, o el manejo de entorno de tu orquestador). Si quieres valores dentro del contenedor,
deben estar en environment: o env_file:.

Broma #2: Un archivo .env es como un niño pequeño—que esté callado no significa que esté bien, significa que deberías comprobarlo inmediatamente.

Tres micro-historias corporativas desde las trincheras

Historia 1: El incidente causado por una suposición errónea

Un equipo fintech ejecutaba una API orientada al cliente en un par de VMs con Docker Compose. Tenían un .env en el repo
y un prod.env separado almacenado en el host. En su cabeza, “Compose carga env desde env_file.” Estaban
medio en lo cierto y totalmente condenados.

El archivo Compose usaba ${IMAGE_TAG} para fijar la imagen de la API. Las variables de runtime del contenedor venían de
env_file: ./prod.env. Un candidato a release necesitaba un hotfix, así que un ingeniero actualizó IMAGE_TAG
en prod.env, ejecutó docker compose up -d y esperó que la nueva imagen se desplegara.

No ocurrió. La interpolación de Compose no miró env_file para renderizar el campo image:.
Los contenedores se quedaron en el tag antiguo. Mientras tanto, el ingeniero también actualizó una variable de runtime en prod.env
y asumió que el contenedor la tomó; no fue así, porque Compose no recreó el contenedor. Así que ahora tenían
código viejo, env viejo y una nueva creencia.

Dos horas después, la API lanzaba errores que parecían una regresión del hotfix. No lo era. El hotfix nunca se desplegó. Su monitorización mostraba “despliegue completado”
porque el job finalizó; no validó la salida de docker compose config ni comprobó los IDs de imagen de los contenedores en ejecución.

La solución fue aburrida: hacer que los tags de imagen sean una variable requerida en tiempo de renderizado y establecerla explícitamente en el comando de despliegue,
verificar con docker compose config, luego forzar la recreación o hacer roll de contenedores correctamente. También dejaron de
usar prod.env como un archivo mágico que “controla todo.” Controla exactamente lo que conectas a él.

Historia 2: La optimización que salió mal

Una compañía de medios quería despliegues más rápidos. Alguien notó que recrear contenedores toma tiempo, especialmente para un
servicio con muchos sidecars. Cambiaron el proceso: actualizar .env, luego ejecutar docker compose up -d
sin forzar recreación, para “evitar downtime.”

Por un tiempo, pareció funcionar—porque la mayoría de cambios eran cambios de tag de imagen, y Compose tiraría y reiniciaría
servicios cuando detectara una imagen nueva. Pero las variables de entorno no son imágenes. Un cambio crítico de configuración
alternó un flag de feature para el enrutamiento de peticiones. La mitad de la flota se actualizó (nodos nuevos donde los contenedores se recrearon),
la otra mitad no. El resultado fue comportamiento de cerebro dividido donde las peticiones tomaban rutas distintas según la VM.

El debugging fue doloroso porque el archivo Compose se veía correcto, el .env se veía correcto y los
contenedores estaban todos “arriba.” El bug estaba en el proceso: optimizaron la única acción que aplica reliably cambios de env.
Introdujeron no determinismo en el despliegue de configuración.

El playbook de recuperación fue contundente: si el env cambió, los contenedores se recrean. Si quieres cero downtime,
lo haces con balanceadores y reinicios en rolling, no esperando que Compose infiera tu intención.

Historia 3: La práctica correcta y aburrida que salvó el día

Un equipo SaaS B2B ejecutaba stacks basados en Compose para servicios internos: métricas, job runners y una base de datos legacy.
Eran alérgicos a lo “ingenioso.” Su despliegue en producción requería tres comprobaciones:
renderizar la config, validar los IDs de imagen en ejecución y registrar el checksum efectivo del entorno.

Un viernes, se fusionó un cambio que introdujo una nueva variable RATE_LIMIT_MODE usada en la interpolación de Compose
para seleccionar la imagen de un sidecar. El desarrollador la añadió a .env.example pero olvidó la fuente de env de producción.
La pipeline de CI tampoco la estaba exportando.

El job de despliegue falló temprano porque su Compose usó ${RATE_LIMIT_MODE?must be set}.
Ese es el truco completo: convirtieron la interpolación vacía silenciosa en una parada en seco. No hubo despliegue parcial, ni comportamiento misterioso.

Arreglaron la pipeline, desplegaron el lunes y nadie recibió paginación. Fue tan anodino que molestó al equipo.
Así sabes que fue correcto.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: el tag de imagen queda en blanco o “latest” inesperadamente

Causa raíz: Variable de render-time faltante o vacía, Compose interpola a cadena vacía; o CI exporta una variable vacía que sobreescribe .env.

Solución: Usa interpolación requerida: image: myapp:${IMAGE_TAG?set IMAGE_TAG}. En CI, falla si IMAGE_TAG está vacío. Valida con docker compose config.

2) Síntoma: “Actualicé .env pero el contenedor no cambió comportamiento”

Causa raíz: El contenedor no se recreó; el contenedor en ejecución mantiene el entorno antiguo.

Solución: Aplica cambios con docker compose up -d --force-recreate (o docker compose restart si procede, pero recrear es más seguro para cambios de env). Verifica con docker inspect ... Config.Env.

3) Síntoma: producción usa settings de desarrollo aunque exista prod.env

Causa raíz: Compose está leyendo .env desde el directorio de trabajo actual, no desde la ruta prevista; o --env-file no se suministra en la automatización.

Solución: En systemd/CI, ejecuta desde el directorio del proyecto o especifica --env-file /srv/app/.env. Añade una comprobación que imprima el checksum del env durante el despliegue.

4) Síntoma: falla la autenticación por contraseña, pero el valor “se ve bien”

Causa raíz: CRLF o espacios finales en .env inyectan caracteres ocultos (a menudo \r) en el valor.

Solución: Normaliza finales de línea (sed -i 's/\r$//'), y valida imprimiendo la repr o hexdump del valor en un contenedor de prueba controlado.

5) Síntoma: la base de datos “perdió” datos después de un redeploy

Causa raíz: Cambio de nombre de proyecto (cambio de nombre de directorio, cambio de COMPOSE_PROJECT_NAME), creando un volumen nuevo con nombre distinto.

Solución: Fija el nombre de proyecto explícitamente para producción. Audita con docker volume ls y docker inspect mounts antes de limpiar. Trata los nombres de volumen como parte del estado.

6) Síntoma: variables en el contenedor no coinciden con las del .env

Causa raíz: Confundir .env (render-time de Compose) con env_file (runtime del contenedor); o el entorno del host sobreescribe valores.

Solución: Decide qué fuente es autorizada. Para valores críticos en runtime, usa claves explícitas en environment: y súmelas desde un archivo env controlado. Para render-time, pásalas con --env-file y valida la salida de la config.

7) Síntoma: una variable con comillas se comporta de forma extraña

Causa raíz: Las comillas se tratan literal o se eliminan de forma diferente a la esperada; parsers distintos en la cadena de herramientas.

Solución: Elimina comillas en .env salvo que sean necesarias. Cuando sean necesarias, valida con docker compose config e inspecciona dentro del contenedor.

8) Síntoma: el servicio no arranca, el mapeo de puertos es absurdo

Causa raíz: La interpolación produjo una cadena de puerto inválida (vacía, no numérica, incluye espacio), pero YAML aún parsea.

Solución: Requiere variables y valida puertos en CI grepeando la config renderizada. Usa defaults solo para valores seguros de desarrollo.

9) Síntoma: “funciona localmente, falla en CI” con el mismo archivo Compose

Causa raíz: El shell local exporta variables y CI no; o CI tiene diferente locale/finales de línea; o CI ejecuta desde otro directorio.

Solución: Haz explícita la fuente de env en CI. Imprime docker compose config (o al menos las líneas relevantes) y asegúrate de que sea determinista.

10) Síntoma: secretos aparecen en logs o bundles de soporte

Causa raíz: Almacenar secretos en .env e imprimir la config renderizada o el env del contenedor durante la depuración; las vars de entorno se filtran fácilmente vía listados de procesos y volcados de crash.

Solución: Usa secretos de Compose cuando sea posible, o credenciales montadas en archivos con permisos estrictos. En la herramienta de incidentes, redacta salidas de env por defecto.

Listas de verificación / plan paso a paso para producción

Lista A: Hacer la interpolación de Compose determinista

  1. Fija la fuente de env: En la automatización, siempre ejecuta con una ruta --env-file explícita y un directorio de trabajo fijo.
  2. Requerir variables críticas: Usa ${VAR?message} para tags de imagen, endpoints externos y nombres de proyecto.
  3. Deja de exportar variables aleatorias: Limpia el entorno en los jobs de CI. Si es necesario, establécelo explícitamente.
  4. Renderiza y diff: Guarda la salida de docker compose config como artefacto de build y haz diff contra despliegues previos.

Lista B: Hacer el entorno runtime del contenedor auditable

  1. Documenta el contrato: Lista las vars de entorno de runtime requeridas por servicio (nombres, significado, valores permitidos).
  2. Prefiere claves explícitas en environment:: Hace el contrato visible en code review.
  3. Usa env_file para valores en bloque, no para comportamiento misterioso: Manténlo mínimo y estructurado. Evita mezclar “dev” y “prod” en el mismo archivo.
  4. Recrear en cambios de env: Si el entorno runtime cambió, los contenedores deben recrearse. Planifica downtime/rolling en consecuencia.

Lista C: No dejes que el estado derive (volúmenes/redes)

  1. Fija el nombre del proyecto: Establece name: en el modelo Compose o COMPOSE_PROJECT_NAME en una fuente de env controlada.
  2. Declara volúmenes explícitamente: Usa volúmenes nombrados para servicios stateful; evita volúmenes anónimos accidentales.
  3. Audita antes de limpiar: Siempre inspecciona mounts y referencias de contenedores antes de eliminar volúmenes.

Lista D: Trata .env como código de producción

  1. Permisos: chmod 600 .env si contiene material sensible.
  2. Normaliza finales de línea: Impone LF en CI.
  3. Reglas de lint: No espacios alrededor de =, no tabs, no espacios finales, patrones de clave predecibles.
  4. Control de cambios: Requiere revisión para cambios en env, y guarda un historial (aunque el archivo esté seguro fuera de Git).

Guía operativa que previene la mayoría de incidentes .env

Usa defaults solo para ergonomía de desarrollador, no para seguridad en producción

Defaults como ${LOG_LEVEL:-debug} están bien para trabajo local. En producción pueden convertir una config faltante
en comportamiento sorprendente. Prefiere valores explícitos en fuentes de env de producción y variables requeridas para todo lo que
afecte integridad de datos, autenticación o enrutamiento.

Falla temprano en el host, no tarde en el contenedor

Si una variable es requerida, falla en tiempo de renderizado. Quieres que el despliegue se detenga antes de tirar imágenes, antes de
tocar volúmenes, antes de reiniciar cualquier cosa. Es más barato y más seguro.

Deja de tratar secretos como “solo vars de entorno”

Las variables de entorno se filtran. Se filtran en reportes de crash, endpoints de debug, listados de procesos, bundles de soporte accidentales y capturas de pantalla humanas. También permanecen en metadata de contenedores más tiempo de lo que crees.
Usa mecanismos de secretos cuando puedas. Si no puedes, al menos separa secretos de no-secretos y diseña comandos de diagnóstico para redactar por defecto.

Haz la configuración observable

Tu sistema debería reportar la versión de configuración efectiva sin volcar secretos. Un checksum de config,
un SHA de git, un digest de imagen y una variable “mode” no sensible suelen ser suficientes para confirmar que el sistema es lo que crees.

Preguntas frecuentes

1) ¿Compose carga automáticamente .env?

Típicamente, sí—.env en el directorio del proyecto se usa como fuente cómoda para la interpolación de variables de Compose y ciertas configuraciones de Compose. Pero “directorio del proyecto” depende de dónde ejecutes el comando y cómo referencias el archivo Compose. Si ejecutas desde el directorio equivocado, puedes cargar silenciosamente el .env equivocado o ninguno.

2) ¿Es .env lo mismo que env_file?

No. .env comúnmente influye la interpolación en tiempo de renderizado de Compose. env_file inyecta
variables dentro del contenedor en tiempo de ejecución. Los archivos se ven similares; la semántica es distinta. Confundirlos es un modo clásico de fallo.

3) ¿Por qué no se aplicó mi cambio en .env después de docker compose up -d?

Porque los contenedores no absorben mágicamente nuevas variables de entorno. Si Compose no recrea el contenedor,
el entorno en ejecución permanece igual. Usa docker compose up -d --force-recreate cuando cambie el env,
y verifica vía docker inspect.

4) ¿Qué gana: las variables exportadas en el shell o .env?

En muchos setups comunes, las variables exportadas en el entorno que ejecuta Compose sobreescriben valores de .env.
Por eso ocurre “funciona en mi máquina”: tu shell exporta algo que CI no, o viceversa. Haz explícita la fuente de env en la automatización.

5) ¿Puedo tener múltiples archivos env?

Sí, pero sé intencional sobre el propósito: uno para render-time (pasado con --env-file) y posiblemente uno
o más para inyección runtime (env_file: por servicio). Evita apilar tantos archivos que nadie pueda predecir el resultado.

6) ¿Por qué mi app ve comillas en los valores?

Porque tu parser podría tratar las comillas como literales. El formato .env no es un estándar universal y
diferentes herramientas interpretan comillas y escapes de forma distinta. Si necesitas caracteres especiales, prueba la ruta exacta:
render-time vía docker compose config y runtime vía docker inspect.

7) ¿Cómo evito que variables vacías se cuelen en producción?

Usa interpolación requerida (${VAR?message}) para valores críticos y añade checks en CI que fallen si
la config renderizada contiene tags de imagen en blanco, puertos vacíos o hostnames vacíos. Esta es una de las correcciones de mayor impacto que puedes entregar.

8) ¿Por qué redeplegar creó volúmenes nuevos y “borró datos”?

Probablemente un cambio de nombre de proyecto. Compose prefija nombres de volúmenes y redes con el nombre del proyecto, que viene del
nombre del directorio, configuración explícita o entorno. Fíjalo para producción para que los volúmenes permanezcan estables. Luego confirma que
el contenedor DB está adjunto al volumen previsto antes de limpiar.

9) ¿Es seguro imprimir docker compose config en logs de CI?

No siempre. Si inlineas secretos en el archivo Compose o los interpolas en campos mostrados en la salida, puedes filtrar credenciales. Si debes imprimir la config, redácta las claves sensibles o imprime solo líneas específicas (referencias de imagen, puertos, settings no sensibles).

10) ¿Cuándo debo usar secretos de Compose en lugar de vars de entorno?

Usa secretos cuando puedas: credenciales, tokens de API, claves privadas, cualquier cosa que lamentarías ver en un log o dump de crash.
Las vars de entorno están bien para configuración no sensible y toggles de features. Si debes usar vars para secretos, restringe permisos y reduce dónde se muestran.

Próximos pasos que puedes hacer esta semana

  1. Añade una “comprobación de renderizado” en CI: ejecuta docker compose config y falla en campos críticos vacíos
    (tags de imagen, puertos, hostnames). Guarda la config renderizada como artefacto con secretos redactados.
  2. Haz variables críticas requeridas: convierte ${VAR} en ${VAR?set VAR} para
    puntos de interpolación críticos en producción.
  3. Fija el nombre de proyecto en producción: evita la deriva accidental de volúmenes y redes. Trátalo como estado.
  4. Estandariza la ejecución de despliegue: directorio de trabajo fijo, --env-file explícito,
    y una política: cambios de env requieren recreación o reinicio rolling.
  5. Deja de almacenar secretos en archivos .env casuales: muévelos a un mecanismo de secretos o montajes de archivos y
    ajusta las herramientas de diagnóstico para evitar filtrarlos durante incidentes.

Docker Compose está bien. Lo que no está bien son las suposiciones no declaradas alrededor de .env.
Haz las variables explícitas, la config renderizada observable y el entorno del contenedor verificable.
Entonces la próxima “regresión misteriosa” será un diff de cinco minutos en lugar de un fin de semana.

Google Search Console “Página con redirección”: cuándo está bien y cuándo perjudica

Abres Google Search Console, haces clic en Páginas, y ahí está: “Página con redirección”.
No indexada. No elegible. No invitada a la fiesta. Mientras tanto tu PM pregunta por qué bajó el tráfico, el
equipo de marketing actualiza los paneles como si fuera un deporte, y tú miras una redirección que “funciona” perfectamente
en el navegador.

Aquí está la visión desde sistemas de producción: “Página con redirección” suele ser normal e incluso deseable. Pero se vuelve tóxica
cuando, por accidente, le has enseñado a Google que tus URLs preferidas son inestables, contradictorias o lentas en resolverse.
Esa es la diferencia entre una capa de canonicalización limpia y un laberinto de redirecciones construido por comité.

Qué significa realmente “Página con redirección”

En Search Console, “Página con redirección” es un estado de indexación, no un juicio moral.
Significa que Google intentó obtener una URL y recibió una respuesta de redirección en lugar de una respuesta final con contenido
(habitualmente un 200). Google entonces decidió que la URL inicial no es la canónica indexable. Así que no indexa
esa URL de partida; sigue la redirección y (quizá) indexa el destino.

Por eso este informe a menudo contiene muchas URLs que intencionalmente no quieres indexadas: variantes HTTP antiguas,
rutas antiguas tras una migración, variantes con/sin barra final, variantes con mayúsculas/minúsculas y parámetros basura.

La pregunta operativa clave no es “¿Cómo hago que ese estado desaparezca?” Es:
¿Se están indexando, posicionando y sirviendo de forma fiable las URLs destino correctas?

Qué no es

  • No es un error por defecto. Una redirección es una respuesta válida.
  • No es prueba de que Google esté confundido. Es prueba de que Google está prestando atención.
  • No garantiza que el destino esté indexado. La redirección puede llevar a un callejón sin salida.

Cómo lo trata Google internamente (versión práctica)

Googlebot solicita la URL A, recibe una redirección a la URL B y registra la relación.
Si las señales son consistentes (la redirección es estable, B devuelve 200 y es canónica, los enlaces internos apuntan a B, el sitemap
lista B, hreflang apunta a B, etc.), Google tiende a consolidar las señales de indexación en B y eliminar A.
Si las señales son inconsistentes, obtienes el equivalente en Search Console de un encogimiento de hombros.

Una realidad de ingeniería que a veces los SEOs pasan por alto: las redirecciones no son gratis. Consumen presupuesto de rastreo,
añaden latencia, pueden cachearse de forma extraña y generar comportamientos límite entre CDNs, navegadores y bots. En
producción, la redirección más simple es la que no necesitas.

Cuándo está bien (y deberías ignorarlo)

“Página con redirección” es saludable cuando representa una canonicalización intencional o
un comportamiento de migración planificado, y las URLs destino están indexadas y rinden bien.
Quieres esas redirecciones porque comprimen variantes de URL en una URL preferida.

Escenarios donde es normal

  • HTTP → HTTPS. Quieres que las URLs HTTP redirijan permanentemente.
    GSC a menudo mostrará las URLs HTTP como “Página con redirección”. Eso está bien.
  • www → non-www (o al revés). De nuevo, el host no preferido debería redirigir.
  • Normalización de barra final. Elige una y redirige la otra.
  • Estructura de URL antigua tras una migración. Rutas antiguas redirigen a rutas nuevas.
  • Limpieza de parámetros (algunos parámetros de seguimiento, ids de sesión). Redirigir o usar canonical según la semántica.
  • Enrutamiento localizado o por región cuando se hace con cuidado (y no basado en heurísticas IP inestables).

Cómo se ve “bien” en las métricas

  • Las URLs destino aparecen bajo Indexadas y muestran impresiones/clics.
  • Las redirecciones son de un solo salto (A → B), no A → B → C → D.
  • El tipo de redirección es mayormente 301/308 para movimientos permanentes (con raras excepciones).
  • Enlaces internos, canonicals y sitemaps apuntan abrumadoramente a las URLs destino.
  • Los registros del servidor muestran que Googlebot recupera con éxito el contenido destino (200).

Si esas condiciones son verdaderas, resiste la tentación de “arreglar” el informe intentando indexar las URLs redirigidas.
Indexar URLs antiguas es como conservar tu viejo número de buscapersonas porque algunos aún lo tienen.
No quieres esa vida.

Cuándo perjudica (y cómo se manifiesta)

“Página con redirección” perjudica cuando enmascara una desalineación entre lo que quieres indexado y lo que
Google puede recuperar, renderizar y confiar como canónico de forma fiable. También perjudica cuando las redirecciones se usan como cinta adhesiva
para problemas más profundos (contenido duplicado, locales mal enroutados, enlaces internos rotos, protocolo/host inconsistentes).

Modos de fallo de alto impacto

1) Cadenas de redirección y desperdicio de rastreo

Las cadenas ocurren cuando se apilan múltiples reglas de normalización: HTTP → HTTPS → www → añadir barra final → reescribir a nueva ruta.
Cada salto añade latencia y probabilidad de fallo. Google seguirá cadenas, pero no indefinidamente, y no sin coste.
Las cadenas también aumentan la probabilidad de crear accidentalmente un bucle.

2) Redirección a un destino no indexable

Redirigir a una URL que devuelve 404, 410, 5xx, bloqueada por robots.txt, o con “noindex” es la forma silenciosa de eliminar páginas.
GSC mostrará la URL de inicio como “Página con redirección”, pero tu problema real es que el destino no puede ser indexado.

3) Uso de 302/307 como redirección “permanente”

Las redirecciones temporales no siempre son malas, pero se usan mal con facilidad. Si mantienes un 302 meses, Google puede
eventualmente tratarlo como un 301, o puede mantener la URL antigua en el índice más tiempo del que deseas. Eso no es una estrategia;
es indecisión en forma de HTTP.

4) Señales mixtas: la redirección dice una cosa, el canonical otra

Si la URL A redirige a B, pero el canonical de B apunta de vuelta a A (o a C), has creado una discusión de canonicalización.
Google elegirá un ganador. Puede que no sea tu favorito.

5) Redirecciones desencadenadas por user-agent, geo, cookies o JS

Las redirecciones condicionales son la forma más rápida de crear un SEO “funciona en mi portátil”. Googlebot no es tu navegador.
Tu borde de CDN no es tu origin. Tu origin no es tu entorno de staging. Si la redirección depende de condiciones,
debes probarla tal como Google la ve.

6) Sitemaps llenos de URLs que redirigen

Un sitemap debe listar URLs canónicas y indexables. Cuando alimentas a Google con miles de URLs redirigidas en el sitemap,
le estás enviando de recado. Él cumplirá por un tiempo, luego te dará menos prioridad silenciosamente.

Broma #1: Las cadenas de redirección son como las cadenas de aprobación corporativas: nadie sabe quién añadió el último salto, pero todos sufren la latencia.

Cómo se ve “perjudica” en los resultados

  • Las URLs preferidas no están indexadas, o lo están de forma intermitente.
  • El informe de cobertura muestra picos en “Duplicado, Google eligió otro canonical” junto a redirecciones.
  • El informe de rendimiento muestra impresiones en caída para páginas migradas, sin recuperación.
  • Los registros de servidor muestran a Googlebot golpeando repetidamente las URLs que redirigen en lugar de los destinos canónicos.
  • Grandes porciones de la actividad de rastreo se gastan en redirecciones en vez de en URLs de contenido.

Física de redirecciones: 301/302/307/308, caché y canonicalización

Códigos de estado, en la forma en que los operadores los piensan

  • 301 (Moved Permanently): “Esta es la nueva dirección.” Cacheado agresivamente por clientes y a menudo por intermediarios. Bueno para movimientos canónicos.
  • 302 (Found): “Temporalmente aquí.” Históricamente tratado como temporal. Los motores de búsqueda se han vuelto más flexibles, pero tu intención importa.
  • 307 (Temporary Redirect): Igual que 302 pero preserva la semántica del método más estrictamente. Relevante principalmente para APIs.
  • 308 (Permanent Redirect): Igual que 301 pero preserva la semántica del método más estrictamente. Cada vez más común.

Etiquetas canonical vs redirecciones: elige tu herramienta

Una redirección es una instrucción del servidor: “No te quedes aquí.” Un canonical es una pista dentro de la página: “Indexa esa otra.”
Si puedes redirigir de forma segura (sin impacto al usuario, sin razón funcional para mantener la URL antigua), hazlo. Es más fuerte y más limpio.
Usa canonicals para casos donde el duplicado debe permanecer accesible (filtros, ordenados, parámetros de seguimiento, vistas imprimibles).

Pero no los mezcles a la ligera. Si rediriges A → B, entonces el canonical de B casi siempre debería ser B. Quieres una historia única.

Latencia y fiabilidad: por qué a los SREs les importan las “redirecciones simples”

Cada salto es otra petición que puede fallar, otro handshake TLS, otra búsqueda en caché, otro lugar donde un header mal configurado puede romper cosas.
Multiply por la tasa de rastreo de Googlebot y tu propio tráfico de usuarios y obtienes un coste real.

Una cita para colgar en un panel: La esperanza no es una estrategia. — Gene Kranz.
Las redirecciones dejadas a la “esperanza” de que los motores de búsqueda lo solucionen son un incidente en cámara lenta.

Comportamiento de caché que no puedes ignorar

Las redirecciones permanentes pueden almacenarse en caché durante mucho tiempo. Si por accidente despliegas un 301 malo, no basta con arreglar el servidor y seguir.
Navegadores, CDNs y bots pueden seguir usando la ruta antigua. Por eso los cambios de redirección merecen gestión de cambios.

Guía de diagnóstico rápido

Cuando “Página con redirección” parece sospechosa, no empieces reescribiendo la mitad de tus reglas. Empieza con evidencias.
Esta secuencia encuentra el cuello de botella rápido, incluso cuando varios equipos han tocado la pila.

1) Confirma el destino final y el número de saltos

  • Toma una muestra de URLs afectadas desde GSC.
  • Sigue las redirecciones y registra: número de saltos, códigos de estado, URL final, estado final.
  • Si el número de saltos > 1, ya tienes trabajo accionable.

2) Valida que el destino sea indexable

  • El estado final debe ser 200.
  • No tener cabecera o meta tag “noindex”.
  • No estar bloqueado por robots.txt.
  • El canonical apunta a sí mismo (o a un canonical claramente intencionado).

3) Revisa si tu propio sitio se está saboteando

  • Los enlaces internos deben apuntar a destinos finales, no a URLs que redirigen.
  • Los sitemaps deben listar URLs canónicas, no las que redirigen.
  • Hreflang (si se usa) debe referenciar destinos canónicos.

4) Revisa los registros del servidor para el comportamiento de Googlebot

  • ¿Googlebot está golpeando repetidamente las URLs que redirigen? Eso sugiere que la fuente de descubrimiento aún apunta a ellas.
  • ¿Googlebot falla en el destino (timeouts, 5xx, bloqueado)? Eso es un problema de fiabilidad, no “SEO”.

5) Si es una migración: compara cobertura antigua vs nueva

  • Las URLs antiguas deberían aparecer como “Página con redirección.” Las nuevas deberían estar indexadas.
  • Si las nuevas no están indexadas, probablemente tengas uno de: noindex, bloqueo por robots, enlazado interno débil, o conflictos de canonical.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estas son las comprobaciones que realmente ejecuto. Cada tarea incluye: el comando, qué significa una salida típica y qué decisión tomar después.
Sustituye dominios de ejemplo y rutas por los tuyos. Los comandos asumen que puedes alcanzar el sitio desde una shell.

Tarea 1: Seguir redirecciones y contar saltos

cr0x@server:~$ curl -sSIL -o /dev/null -w "final=%{url_effective} code=%{http_code} redirects=%{num_redirects}\n" http://example.com/Old-Path
final=https://www.example.com/new-path/ code=200 redirects=2

Significado: Ocurrieron dos redirecciones. El final es 200.
Decisión: Si redirects > 1, intenta colapsar reglas para que la primera respuesta apunte directamente a la URL final.

Tarea 2: Imprimir la cadena completa de redirección (ver cada Location)

cr0x@server:~$ curl -sSIL http://example.com/Old-Path | sed -n '1,120p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/Old-Path
HTTP/2 301
location: https://www.example.com/new-path/
HTTP/2 200
content-type: text/html; charset=UTF-8

Significado: HTTP→HTTPS y luego normalización de host/ruta.
Decisión: Cambia la primera redirección para que vaya directamente al host/ruta final, no a un intermedio.

Tarea 3: Detectar bucles de redirección

cr0x@server:~$ curl -sSIL --max-redirs 10 https://www.example.com/loop-test/ | tail -n +1
curl: (47) Maximum (10) redirects followed

Significado: Bucle o cadena excesiva.
Decisión: Trátalo como un incidente: identifica el conjunto de reglas (CDN, balanceador, origin) que crea el bucle y arréglalo antes de pensar en indexación.

Tarea 4: Verificar la etiqueta canonical en el destino

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'rel="canonical"'
<link rel="canonical" href="https://www.example.com/new-path/" />

Significado: El canonical apunta a sí mismo. Bien.
Decisión: Si el canonical apunta inesperadamente a otro lugar, corrige plantillas o cabeceras; de lo contrario, Google podría ignorar tu intención de redirección.

Tarea 5: Comprobar noindex en el destino (meta tag)

cr0x@server:~$ curl -sS https://www.example.com/new-path/ | grep -i -m1 'noindex'

Significado: Sin salida significa que no se encontró la meta etiqueta “noindex” en la primera coincidencia.
Decisión: Si ves noindex, detente. Esa es la razón por la que no se indexa. Corrige la configuración de release, flags del CMS o fugas de entorno de staging.

Tarea 6: Comprobar encabezado X-Robots-Tag por noindex

cr0x@server:~$ curl -sSIL https://www.example.com/new-path/ | grep -i '^x-robots-tag'
X-Robots-Tag: index, follow

Significado: Los encabezados permiten la indexación.
Decisión: Si ves “noindex”, arréglalo en la fuente (app, reglas del CDN o middleware de seguridad). Los encabezados anulan las buenas intenciones.

Tarea 7: Confirmar que robots.txt no bloquee el destino

cr0x@server:~$ curl -sS https://www.example.com/robots.txt | sed -n '1,120p'
User-agent: *
Disallow: /private/

Significado: Se muestra un robots.txt básico.
Decisión: Si la ruta destino está desautorizada, Google puede ver la redirección pero no rastrear el contenido. Actualiza robots.txt y vuelve a probar en GSC.

Tarea 8: Comprobar si tu sitemap lista URLs que redirigen

cr0x@server:~$ curl -sS https://www.example.com/sitemap.xml | grep -n 'http://example.com' | head
42:  <loc>http://example.com/old-path</loc>

Significado: El sitemap contiene URLs no canónicas (host HTTP).
Decisión: Regenera los sitemaps para listar solo URLs canónicas finales. Esta es una limpieza de bajo riesgo y alto retorno.

Tarea 9: Detectar enlaces internos que siguen apuntando a URLs que redirigen

cr0x@server:~$ curl -sS https://www.example.com/ | grep -oE 'href="http://example.com[^"]+"' | head
href="http://example.com/old-path"

Significado: La página principal todavía enlaza a la URL HTTP antigua.
Decisión: Arregla la generación de enlaces internos (plantillas, campos del CMS). Los enlaces internos son tu propia donación de presupuesto de rastreo al fondo de redirecciones.

Tarea 10: Comprobar el tipo de redirección (301 vs 302) en el edge

cr0x@server:~$ curl -sSIL https://example.com/old-path | head -n 5
HTTP/2 302
location: https://www.example.com/new-path/

Significado: Hay una redirección temporal en vigor.
Decisión: Si el movimiento es permanente, cambia a 301/308. Si es realmente temporal (mantenimiento, A/B), asegúrate de que tenga límite temporal y esté monitorizada.

Tarea 11: Confirmar que el destino devuelve 200 consistentemente (no 403/500 a veces)

cr0x@server:~$ for i in {1..5}; do curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" https://www.example.com/new-path/; done
200 0.142
200 0.151
200 0.139
500 0.312
200 0.145

Significado: 500 intermitente. Eso es un bug de fiabilidad.
Decisión: No discutas con GSC hasta que el origin sea estable. Arregla errores río arriba y luego solicita reindexación.

Tarea 12: Inspeccionar reglas de Nginx para doble normalización

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'return 301|rewrite .* permanent' | head -n 20
123:    return 301 https://$host$request_uri;
287:    rewrite ^/Old-Path$ /new-path/ permanent;

Significado: Múltiples directivas de redirección pueden apilarse (protocolo + ruta).
Decisión: Combina en una única redirección de canonicalización cuando sea posible, o asegura el orden para evitar multi-saltos.

Tarea 13: Inspeccionar reglas de Apache por coincidencias no deseadas

cr0x@server:~$ sudo apachectl -S 2>/dev/null | sed -n '1,80p'
VirtualHost configuration:
*:443                  is a NameVirtualHost
         default server www.example.com (/etc/apache2/sites-enabled/000-default.conf:1)

Significado: Confirma qué vhost es el por defecto; un vhost por defecto incorrecto puede crear redirecciones de host que no planeaste.
Decisión: Asegura que el vhost del host canónico sea correcto y que los hosts no canónicos redirijan explícitamente a él en un solo salto.

Tarea 14: Usar logs de acceso para ver si Googlebot está atascado en URLs que redirigen

cr0x@server:~$ sudo awk '$9 ~ /^30/ && $0 ~ /Googlebot/ {print $4,$7,$9,$11}' /var/log/nginx/access.log | head
[27/Dec/2025:09:12:44 /old-path 301 "-" 
[27/Dec/2025:09:12:45 /old-path 301 "-" 

Significado: Googlebot solicita repetidamente la URL que redirige. Las fuentes de descubrimiento aún apuntan allí.
Decisión: Arregla enlaces internos y sitemaps; considera actualizar referencias externas si las controlas (perfiles, propiedades propias).

Tarea 15: Comprobar comportamiento inconsistente por user-agent (peligrosas redirecciones “inteligentes”)

cr0x@server:~$ curl -sSIL -A "Mozilla/5.0" https://www.example.com/ | head -n 5
HTTP/2 200
content-type: text/html; charset=UTF-8
cr0x@server:~$ curl -sSIL -A "Googlebot/2.1" https://www.example.com/ | head -n 5
HTTP/2 302
location: https://www.example.com/bot-landing/

Significado: Respuestas diferentes para Googlebot. Eso es una señal de alarma a menos que tengas una razón legítima.
Decisión: Elimina la lógica de redirección basada en UA; puede parecer cloaking y crea inestabilidad en la indexación.

Tarea 16: Validar que HSTS no puede culparse por tu confusión de redirecciones

cr0x@server:~$ curl -sSIL https://www.example.com/ | grep -i '^strict-transport-security'
Strict-Transport-Security: max-age=31536000; includeSubDomains

Significado: HSTS está habilitado, por lo que los navegadores forzarán HTTPS después del primer contacto.
Decisión: No “depures” redirecciones solo en un navegador; usa curl desde un entorno limpio. HSTS puede ocultar comportamiento HTTP y hacer que persigas fantasmas.

Tres mini-historias corporativas desde las trincheras de redirección

Incidente: la suposición equivocada (“Google simplemente lo resolverá”)

Una empresa SaaS de tamaño medio migró de un CMS legado a un framework moderno. El plan parecía limpio:
las URLs antiguas redirigirían a las nuevas, y el sitio nuevo sería más rápido. Ingeniería implementó redirecciones en la capa de app,
y QA verificó en un navegador. Todos se fueron a casa.

En la primera semana, Search Console empezó a llenarse de “Página con redirección” y las impresiones cayeron para páginas de alto valor.
El equipo SEO entró en pánico y exigió “quitar las redirecciones”. Habría sido la extintora equivocada.
El SRE de guardia hizo lo poco glamoroso: sacó los registros del servidor para Googlebot y reprodujo peticiones con curl.

La suposición que los rompió fue: “Si funciona en el navegador, Googlebot ve lo mismo.”
Su CDN tenía reglas de mitigación de bots que trataban a user-agents desconocidos de forma distinta durante picos de tráfico.
Cuando el origin estaba lento, el edge devolvía una redirección temporal a una página genérica de “inténtalo de nuevo”—bien para humanos,
terrible para la indexación. Googlebot siguió la redirección y encontró contenido delgado.

La solución no fue “magia SEO.” Fue higiene de producción:
eximieron a bots verificados de la redirección de mitigación, mejoraron el cache de las nuevas páginas y dejaron de redirigir a un fallback genérico.
Después de eso, “Página con redirección” permaneció para las URLs antiguas (esperado), mientras las nuevas se estabilizaron y reindexaron.

Optimización que salió mal: colapsar parámetros con redirecciones

Una organización de e‑commerce tenía un problema de parámetros: URLs infinitas como ?color=blue&sort=popular&ref=ads.
Las estadísticas de rastreo se veían mal, y alguien propuso una “solución” simple: redirigir cualquier URL con parámetros a la página de categoría sin parámetros.
Una regla de reescritura para gobernarlas a todas.

Se desplegó rápido. Demasiado rápido. La conversión bajó. El tráfico orgánico en variantes de cola larga de la categoría se desplomó.
Search Console mostró muchas “Página con redirección”, pero el daño real fue que estaban redirigiendo la intención real del usuario.
Algunas combinaciones de parámetros representaban páginas filtradas con inventario único que los usuarios buscaban.

Peor aún, la regla de redirección desencadenó cadenas: URL con parámetros → categoría limpia → redirección por geo → categoría localizada.
Googlebot pasó más tiempo rebotando que rastreando. Aumentó la latencia. El sitio pareció “inestable”.

El rollback fue incómodo pero necesario. Reemplazaron la redirección contundente por una política:
eliminar solo parámetros conocidos de seguimiento (utm/ref), mantener filtros funcionales indexables solo donde el contenido lo justificara,
y usar etiquetas canonical para duplicados. De repente “Página con redirección” se limitó a URLs basura, no a las generadoras de ingresos.

Aburrido pero correcto: la higiene de sitemap y enlaces internos salvó el día

Una plataforma editorial consolidó dominios: cuatro subdominios en un host canónico.
Implementaron redirecciones 301 y esperaban turbulencia. El giro: lo trataron como un cambio operativo, no como un deseo SEO.

Antes del lanzamiento, generaron una tabla de mapeo (antiguo → nuevo), ejecutaron pruebas automáticas de redirección y actualizaron enlaces internos en plantillas.
No solo la navegación. Footer, módulos de artículos relacionados, feeds RSS, todo. También regeneraron sitemaps para incluir solo URLs canónicas
y los desplegaron con el mismo release.

Tras el lanzamiento, Search Console se llenó de “Página con redirección” para los hosts antiguos (como se esperaba), pero el host nuevo se indexó rápido.
Las estadísticas de rastreo mostraron que Googlebot dejó las URLs antiguas más rápido que en migraciones previas.
Su monitorización basada en logs mostró una fuerte caída en hits de redirección en semanas, lo que significaba que las fuentes de descubrimiento estaban limpias.

La lección no fue glamorosa: el trabajo aburrido previene la interrupción emocionante.
Las redirecciones son un puente. Aún tienes que mover el tráfico, los enlaces y las señales al otro lado.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: picos de “Página con redirección” tras un despliegue

Causa raíz: Una nueva regla introdujo redirecciones multi-salto o bucles (a menudo barra final + locale + normalización de host).
Solución: Ejecuta pruebas de cadena de redirección en una muestra de URLs; colapsa a un solo salto; añade pruebas de regresión en CI para URLs canónicas.

2) Síntoma: Las URLs antiguas muestran “Página con redirección”, pero las nuevas son “Rastreada — actualmente no indexada”

Causa raíz: Las páginas destino son de baja calidad/delgadas, están bloqueadas, lentas o tienen contradicciones de canonical/noindex.
Solución: Verifica que el destino devuelva 200, sea indexable, tenga self-canonical y contenido sustancial. Mejora rendimiento y plantillas.

3) Síntoma: GSC muestra “Página con redirección” para URLs que deberían ser finales

Causa raíz: Enlaces internos o sitemap apuntan a variantes no canónicas, por lo que Google sigue descubriendo la versión incorrecta primero.
Solución: Actualiza enlaces internos, sitemap, hreflang y datos estructurados para referenciar solo destinos canónicos.

4) Síntoma: Las redirecciones funcionan en el navegador, fallan para Googlebot

Causa raíz: Lógica condicional basada en user-agent, cookies, geo o mitigación de bots en CDN/WAF.
Solución: Prueba con UA de Googlebot, compara encabezados, elimina redirecciones condicionales y asegura que la canonicalización se aplique consistentemente.

5) Síntoma: Las páginas desaparecen después de que “limpiaron” parámetros

Causa raíz: La regla de redirección colapsó URLs significativas en páginas genéricas, eliminando relevancia de cola larga.
Solución: Redirige/elimina solo parámetros de seguimiento; maneja filtros funcionales con canonicals, reglas noindex o permite indexación selectiva.

6) Síntoma: Las URLs que redirigen permanecen en el índice durante meses

Causa raíz: Redirecciones temporales (302/307) usadas para movimientos permanentes, o señales de canonical inconsistentes.
Solución: Usa 301/308 para movimientos permanentes; asegura que el destino sea canónico; asegura que enlaces internos y sitemaps apunten al destino.

7) Síntoma: Las redirecciones causan 5xx intermitentes y caídas de rastreo

Causa raíz: El manejo de redirecciones en la capa de app dispara lógica costosa; sobrecarga del origin; misses de caché; overhead de handshake TLS en cada salto.
Solución: Mueve redirecciones al edge/servidor web cuando sea posible; cachea redirecciones; reduce saltos; monitoriza p95 de latencia en endpoints de redirección.

Broma #2: La forma más rápida de encontrar una regla de redirección no documentada es eliminarla y esperar a que alguien importante lo note.

Listas de verificación / plan paso a paso

Lista A: Ves “Página con redirección” y quieres saber si debes preocuparte

  1. Elige 20 URLs del informe (mezcla de importantes y al azar).
  2. Ejecuta curl con conteo de redirecciones. Si >1 salto en muchas, preocúpate.
  3. Confirma que las URLs finales devuelven 200 y son indexables (noindex/robots/canonical).
  4. Comprueba si las URLs finales están indexadas y recibiendo impresiones.
  5. Si las URLs finales están sanas, trata “Página con redirección” como informativa.

Lista B: Limpieza de redirecciones que no causará un nuevo incidente

  1. Inventaría las reglas de redirección actuales en capas: CDN/WAF, balanceador, servidor web, app.
  2. Define la política de URL canónica: protocolo, host, barra final, minúsculas, patrones de locale.
  3. Asegura un único salto hacia la canónica cuando sea posible.
  4. Actualiza enlaces internos y plantillas para usar URLs canónicas.
  5. Regenera sitemaps para listar solo URLs canónicas.
  6. Despliega con monitorización: tasa de redirección, 4xx/5xx en destino, latencia.
  7. Tras el despliegue, muestrea logs de Googlebot y verifica que llegue a páginas 200.

Lista C: Plan específico para migración (dominios o estructuras de URL)

  1. Crea un archivo de mapeo (antiguo → nuevo) para todas las URLs de alto valor; no te fíes solo de regex.
  2. Implementa redirecciones 301/308 y prueba por bucles y cadenas.
  3. Mantén paridad de contenido: títulos, encabezados, datos estructurados cuando proceda.
  4. Asegura que las nuevas páginas tengan canonicals autoreferenciadas.
  5. Cambia los sitemaps a las nuevas URLs en el lanzamiento.
  6. Monitoriza la indexación: las páginas nuevas deberían subir a medida que las antiguas se vuelven “Página con redirección.”
  7. Mantén las redirecciones el tiempo suficiente (meses a años según el ecosistema), no dos semanas porque alguien quiere “configuraciones limpias.”

Hechos interesantes y contexto histórico

  • Hecho 1: Los códigos HTTP 301 y 302 datan de las primeras especificaciones de HTTP; la web ha estado moviendo páginas desde prácticamente siempre.
  • Hecho 2: 307 y 308 se introdujeron más tarde para clarificar el comportamiento de preservación del método; importan más para APIs pero aparecen en pilas modernas.
  • Hecho 3: Los motores de búsqueda trataban históricamente al 302 como “no pasar señales”, pero con el tiempo se volvieron más flexibles cuando la redirección persiste.
  • Hecho 4: HSTS puede hacer invisibles las redirecciones HTTP→HTTPS en pruebas de navegador porque el navegador actualiza a HTTPS antes de hacer la petición.
  • Hecho 5: Los CDNs suelen implementar redirecciones en el edge; eso puede ser más rápido, pero también puede crear interacciones ocultas con redirecciones del origin.
  • Hecho 6: La canonicalización temprana en SEO se hacía a menudo con redirecciones porque las etiquetas canonical no existían al principio; más tarde, las hints canonical se volvieron estándar.
  • Hecho 7: Las cadenas de redirección se hicieron más comunes a medida que las pilas se apilaron: CMS + framework + CDN + WAF + balanceador, cada uno “ayudando” con la normalización.
  • Hecho 8: Los bots no se comportan como usuarios: pueden rastrear a escala, reintentar agresivamente y amplificar pequeñas ineficiencias en costes de infraestructura grandes.

Preguntas frecuentes

1) ¿Debería intentar eliminar “Página con redirección” en Search Console?

No como objetivo. Tu objetivo es que las URLs destino estén indexadas y rindiendo. Que las URLs redirigidas estén “no indexadas” es esperado.
Limpia solo cuando el comportamiento de redirección sea ineficiente o inconsistente.

2) ¿Es “Página con redirección” una penalización?

No. Es una clasificación. La penalización es lo que hagas después—como mantener cadenas, redirigir a páginas delgadas o enviar señales canónicas mixtas.

3) ¿Cuántas redirecciones son demasiadas?

En la práctica: apunta a un salto. Dos es soportable. Más que eso es un olor a fiabilidad, y puede ralentizar el rastreo y desperdiciar presupuesto.
Si ves 3+, arréglalo salvo que haya una razón muy específica.

4) ¿Perjudica un 302 al SEO comparado con un 301?

A veces. Si el movimiento es permanente, usa 301 o 308. Un 302 de larga duración puede funcionar, pero comunica incertidumbre y puede retrasar la consolidación.
No construyas tu estrategia de indexación sobre “probablemente Google lo trate como 301 eventualmente.”

5) ¿Por qué mi sitemap muestra URLs que GSC dice son “Página con redirección”?

Porque tu generador de sitemap está usando la base URL equivocada (HTTP vs HTTPS, host equivocado) o está emitiendo rutas legadas.
Arregla el generador para que los sitemaps list
en solo URLs canónicas finales. Ese es uno de los mejores wins en todo este tema.

6) ¿Y si necesito ambas versiones accesibles (como páginas filtradas), pero no quiero que se indexen?

No las redirijas si son funcionalmente necesarias. Manténlas accesibles y usa etiquetas canonical o reglas noindex deliberadamente.
Las redirecciones son para “esto no debería existir como landing page.”

7) ¿Puede “Página con redirección” ser causado por redirecciones en JavaScript?

Sí, pero eso es modo difícil. Las redirecciones basadas en JS pueden ser más lentas, menos fiables para bots y pueden parecer sospechosas si se abusan.
Prefiere redirecciones del lado servidor salvo que tengas una razón fuerte.

8) ¿Cuánto tiempo debería mantener redirecciones después de una migración?

Más tiempo del que piensas. Meses como mínimo; a menudo un año o más para sitios significativos, especialmente si las URLs antiguas están ampliamente enlazadas.
Quitar redirecciones temprano es cómo conviertes tu migración en una cosecha permanente de 404.

9) ¿Por qué las URLs redirigidas siguen siendo rastreadas mucho?

Google sigue encontrándolas vía enlaces internos, sitemaps o enlaces externos. Las fuentes internas están bajo tu control; arréglalas primero.
Los enlaces externos tardan en decaer. La meta es dejar de alimentar el problema.

10) ¿Podría “Página con redirección” ocultar una mala configuración de seguridad o WAF?

Absolutamente. Los WAFs a veces redirigen tráfico sospechoso, tráfico rate-limitado o ciertos user agents. Si Googlebot recibe ese trato,
verás inestabilidad en la indexación. Confirma el comportamiento con pruebas por user-agent y logs del edge.

Conclusión: pasos prácticos siguientes

“Página con redirección” no es tu enemiga. Es una linterna. A veces ilumina URLs que intencionalmente retiraste. Genial.
Otras veces expone deuda de redirecciones: cadenas, bucles, canonicals mixtos y redirecciones “temporales” que se volvieron permanentes por pereza.

Pasos siguientes que pagan rápido:

  1. Muestra 20–50 URLs del informe y mide saltos con curl.
  2. Confirma que los destinos son indexables (200, sin noindex, no bloqueados, canonical autoreferenciado).
  3. Arregla enlaces internos y sitemaps para apuntar a URLs canónicas finales.
  4. Colapsa redirecciones a un salto y estandariza 301/308 para movimientos permanentes.
  5. Observa logs: Googlebot debería pasar menos tiempo en redirecciones y más en páginas reales.

Si tratas las redirecciones como infraestructura de producción—observable, testeable y aburrida—obtendrás el mejor resultado SEO posible:
Google gastará su tiempo en tu contenido en lugar de en tu fontanería.

MariaDB vs SQLite para ráfagas de escritura: ¿Quién maneja los picos sin drama?

Las ráfagas de escritura no llegan con educación. Aparecen en manada: runners de trabajos que despiertan a la vez, una cola que se descarga después de un deploy, clientes móviles que se reconectan tras salir de un túnel, o un backfill de “ups” que prometiste que correrá “despacio”. La pregunta no es si tu base de datos puede escribir. La pregunta es si puede escribir mucho, ahora mismo, sin convertir el on-call en un hobby.

MariaDB y SQLite pueden almacenar tus datos. Pero ante picos actúan como especies distintas. MariaDB es un servidor con controles de concurrencia, flushing en segundo plano, buffer pools y una larga historia de haber recibido cargas de producción. SQLite es una librería que vive dentro de tu proceso, brutalmente eficiente y maravillosamente de bajo mantenimiento—hasta que le pides hacer algo parecido a una tormenta de múltiples escritores.

La pregunta real: qué significa “ráfaga” para tu sistema

“Ráfaga de escritura” es una frase vaga que provoca malentendidos costosos. Hay al menos cuatro bestias diferentes que la gente llama ráfaga:

  1. Pico corto, alta concurrencia: 500 peticiones llegan a la vez, cada una haciendo un insert pequeño.
  2. Subida sostenida: 10× la tasa normal de escritura durante 10–30 minutos (jobs por lotes, backfills).
  3. Explosión de latencia en cola larga: el throughput medio parece correcto, pero cada 20 segundos los commits se detienen 300–2000 ms.
  4. Acantilado de I/O: el disco o el sistema de almacenamiento choca contra un muro de flush (comportamiento de fsync/flush cache), y todo se encola detrás.

MariaDB vs SQLite bajo “ráfagas” trata principalmente de cómo se comportan frente a la concurrencia y cómo pagan por la durabilidad. Si sólo tienes un escritor y toleras algo de encolamiento, SQLite puede ser ridículamente bueno. Si tienes muchos escritores, muchos procesos, o necesitas seguir sirviendo lecturas mientras las escrituras golpean, MariaDB suele ser el adulto en la sala.

Pero hay trampas en ambos lados. La trampa de SQLite es el bloqueo. La trampa de MariaDB es pensar que el servidor de la base de datos es el cuello de botella cuando en realidad es el subsistema de almacenamiento (o tu política de commits).

Algunos hechos e historia que realmente importan

Algunos puntos de contexto que son cortos, concretos y sorprendentemente predictivos del comportamiento ante ráfagas:

  • SQLite es una librería, no un servidor. No hay un daemon separado; tu app la enlaza y lee/escribe directamente el archivo de BD. Eso es una superpotencia de rendimiento y una limitación operativa.
  • El diseño original de SQLite se optimizó para sistemas embebidos. Se hizo popular en escritorio/móvil porque es “simplemente un archivo” y no necesita un DBA que lo cuide.
  • El modo WAL en SQLite se introdujo para mejorar la concurrencia. Separa lecturas de escrituras mediante la escritura en un write-ahead log, permitiendo lectores durante escrituras—hasta cierto punto.
  • SQLite aún tiene una regla de un solo escritor a nivel de base de datos. WAL ayuda a las lecturas, pero múltiples escritores concurrentes todavía se serializan en el bloqueo de escritura.
  • MariaDB es un fork de MySQL. El fork ocurrió tras la adquisición de Sun por Oracle; MariaDB se convirtió en la opción “amigable con la comunidad” para muchas organizaciones.
  • InnoDB se convirtió en el engine por defecto por una razón. Está construido alrededor de MVCC, redo logs, flushing en segundo plano y recuperación ante fallos—características que importan cuando golpean las ráfagas.
  • El rendimiento de MariaDB durante ráfagas depende mucho del comportamiento de fsync. Tu política de flush del redo log puede desplazar el dolor de “cada commit se detiene” a “algunos commits se detienen pero el throughput mejora”. Es una compensación, no dinero gratis.
  • La mayoría de incidentes de “la base de datos está lenta” durante picos de escritura son en realidad “el almacenamiento está lento.” La base de datos es lo primero que lo admite bloqueándose en fsync.

Anatomía de la ruta de escritura: MariaDB/InnoDB vs SQLite

SQLite: un archivo, un escritor, muy poca ceremonia

SQLite escribe en un único archivo de base de datos (además, en modo WAL, un archivo WAL y un archivo de índice de memoria compartida). Tu proceso emite SQL; SQLite lo traduce en actualizaciones de páginas. Durante el commit de una transacción, SQLite debe asegurar la durabilidad según tus pragmas. Esto normalmente significa forzar los datos a almacenamiento estable usando llamadas tipo fsync, según la plataforma y el sistema de archivos.

Bajo ráfagas, el detalle crítico de SQLite es qué tan rápido puede ciclar “adquirir bloqueo de escritura → escribir páginas/WAL → política de sync → liberar bloqueo”. Si los commits son frecuentes y pequeños, la sobrecarga la dominan las llamadas de sync y los traspasos de bloqueo. Si los commits se agrupan, SQLite puede volar.

El modo WAL cambia la forma: los escritores apenden al WAL y los lectores pueden seguir leyendo el snapshot principal. Pero sigue habiendo un solo escritor a la vez, y los checkpoints pueden convertirse en una segunda clase de ráfaga (más sobre eso después).

MariaDB/InnoDB: concurrencia, buffering y I/O en segundo plano

MariaDB es un proceso servidor con múltiples hilos trabajadores. InnoDB mantiene un buffer pool (cache) para páginas, un redo log (write-ahead), y a menudo un undo log para MVCC. Cuando haces commit, InnoDB escribe registros de redo y—dependiendo de la configuración—los flush a disco. Las páginas sucias se vacían en segundo plano.

Bajo ráfagas, la superpotencia de InnoDB es que puede aceptar muchos escritores concurrentes, encolar el trabajo y suavizarlo con flushing en segundo plano—suponiendo que lo hayas dimensionado y tu I/O pueda seguir el ritmo. Su debilidad es que aún puede golpear un muro duro donde el redo log o el flushing de páginas sucias se vuelven urgentes, y entonces los picos de latencia parecen un colapso sincronizado.

Hay una idea parafraseada de Werner Vogels (CTO de Amazon) que la gente de operaciones repite porque sigue siendo cierta: todo falla, así que diseña para la recuperación y minimiza el radio del impacto (idea parafraseada). En el mundo de ráfagas, eso suele significar: espera amplificación de escritura y espera que el disco sea el primero en quejarse.

Quién maneja mejor los picos (y cuándo)

Si quieres una regla limpia y honesta: SQLite maneja las ráfagas sin drama cuando puedes moldear la carga de escritura en menos transacciones y no tienes muchos escritores entre procesos. MariaDB maneja las ráfagas sin drama cuando tienes muchos escritores concurrentes, múltiples instancias de aplicación y necesitas comportamiento predecible bajo contención—siempre que tu almacenamiento y configuración no te saboteen.

SQLite gana cuando

  • Proceso único o escritores controlados: un hilo escritor, una cola, o un proceso escritor dedicado.
  • Transacciones cortas, commits agrupados: puedes commitear cada N registros o cada T milisegundos.
  • Disco local, fsync de baja latencia: NVMe, no un filesystem de red inestable.
  • Quieres simplicidad: sin servidor, menos partes móviles, menos páginas que te despierten a las 3 a.m.
  • Carga de lecturas con ráfagas ocasionales: el modo WAL puede mantener las lecturas ágiles mientras ocurren escrituras.

SQLite pierde (ruidosamente) cuando

  • Muchos escritores concurrentes: se serializan y tus hilos de aplicación se amontonan tras “database is locked.”
  • Múltiples procesos escriben al mismo tiempo: especialmente en hosts ocupados o contenedores sin coordinación.
  • El checkpointing se vuelve una ráfaga: el WAL crece, se dispara un checkpoint y de repente tienes una tormenta de escritura dentro de tu tormenta.
  • El almacenamiento tiene extrañas semánticas de fsync: algunos almacenamientos virtualizados o en red hacen que la durabilidad sea extremadamente cara o inconsistente.

MariaDB gana cuando

  • Tienes concurrencia real: múltiples instancias de app, cada una escribiendo al mismo tiempo.
  • Necesitas herramientas operativas: replicación, backups, cambios de esquema en línea, hooks de observabilidad.
  • Necesitas aislar la carga: el buffer pool absorbe picos, los thread pools y colas pueden evitar un colapso total.
  • Necesitas semánticas de aislamiento predecibles: MVCC con lecturas consistentes bajo carga de escritura.

MariaDB falla cuando

  • Tu disco no puede vaciar lo suficientemente rápido: el flush del redo log detiene el mundo; la latencia se dispara.
  • Dimensionas mal el buffer pool: demasiado pequeño y hay thrashing; demasiado grande y llega el drama de cache del SO y swapping.
  • “Afinas” la durabilidad a ciegas: compras throughput vendiendo a tu yo del futuro un incidente de pérdida de datos.
  • Tu esquema fuerza puntos calientes: contadores de fila única, índices pobres o inserts monotónicos peleando por las mismas estructuras.

Broma #1: SQLite es el amigo que siempre llega a tiempo—salvo que invites a tres amigos más a hablar a la vez, entonces simplemente cierra la puerta.

Perillas de durabilidad: qué compras realmente con fsync

Las ráfagas son donde las configuraciones de durabilidad dejan de ser teóricas. Se convierten en una factura que tu almacenamiento debe pagar, inmediatamente, en efectivo.

Palancas de durabilidad en SQLite

SQLite expone la durabilidad vía pragmas. Las grandes para ráfagas:

  • journal_mode=WAL: usualmente la recomendación por defecto para lecturas concurrentes y rendimiento de escritura sostenido.
  • synchronous: controla cuán agresivamente SQLite sincroniza datos al disco. Mayor durabilidad normalmente significa más coste de fsync.
  • busy_timeout: no mejora el throughput, pero evita fallos inútiles al esperar bloqueos.
  • wal_autocheckpoint: controla cuándo SQLite intenta checkpointear (mover el contenido del WAL al archivo principal DB).

Aquí está la parte sutil: en modo WAL, el sistema puede sentirse genial hasta que el WAL crece y el checkpointing se vuelve inevitable. Ese “impuesto de checkpoint” frecuentemente aparece como latencias periódicas que parecen el “hipo” de la base de datos. Si estás insertando logs o series temporales, esto puede morder fuerte.

Perillas de durabilidad en MariaDB/InnoDB

En InnoDB, las perillas críticas para ráfagas están relacionadas con el flush del redo log y qué tan rápido se pueden escribir las páginas sucias:

  • innodb_flush_log_at_trx_commit: la clásica compensación durabilidad/throughput. Valor 1 es lo más seguro (flush en cada commit), 2 intercambia algo de durabilidad por velocidad, 0 es más rápido pero más arriesgado.
  • sync_binlog: si usas binlogs para replicación, esto puede ser un coste adicional de fsync.
  • innodb_redo_log_capacity (o dimensionado de archivos de log en versiones antiguas): demasiado pequeño y sufres checkpoints frecuentes; demasiado grande y cambia el tiempo de recuperación. Los picos suelen revelar logs subdimensionados.
  • innodb_io_capacity / innodb_io_capacity_max: indica a InnoDB cuán agresivo ser con el flushing en segundo plano.

Para tolerancia a ráfagas, quieres que la base de datos absorba la ráfaga y haga flush de forma constante en vez de entrar en pánico y flushar. El panic flushing es donde la latencia se vuelve “interesante.”

Patrones comunes de ráfaga y qué falla primero

Patrón: transacciones mínimas a alta QPS

Este es el clásico “insertar una fila y commitear” en bucle, multiplicado por concurrencia. Es una tormenta de commits.

  • SQLite: contención de bloqueos + coste de fsync. Verás “database is locked” o esperas largas a menos que encoles escrituras y agrupes commits.
  • MariaDB: puede manejar concurrencia, pero el fsync por commit puede dominar la latencia. Verás muchos commits de transacciones, esperas de flush de log y saturación de I/O.

Patrón: backfill con índices pesados

Añades columnas, rellenas datos y actualizas índices secundarios. Ahora cada escritura se expande en múltiples actualizaciones de B-tree.

  • SQLite: un solo escritor lo hace predecible pero lento; la ventana de bloqueo es más larga, así que todos los demás esperan más.
  • MariaDB: el throughput depende del buffer pool y del I/O. Índices calientes pueden causar contención de latches; demasiados hilos pueden empeorar la situación.

Patrón: la ráfaga coincide con el ciclo de checkpoint/flush

Este es el escenario “está bien, está bien, está bien… ¿por qué se incendia cada 30 segundos?”.

  • SQLite WAL checkpoint: ciclos largos de checkpoint pueden bloquear o ralentizar escrituras, dependiendo del modo y condiciones.
  • InnoDB checkpoint: el redo log se llena, las páginas sucias deben vaciarse, y el trabajo de primer plano empieza a esperar al I/O en segundo plano.

Patrón: jitter de latencia del almacenamiento

Todo está normal hasta que el disco se pausa. Volúmenes en la nube, flushes de cache de RAID, ruido de vecinos, commits del journal del filesystem—elige tu villano.

  • SQLite: tu hilo de aplicación es la base de datos; se bloquea. Los picos de latencia se propagan directamente a la latencia de la petición.
  • MariaDB: puede encolar y paralelizar, pero eventualmente los hilos del servidor también se bloquean. La diferencia es que puedes verlo desde dentro del engine mediante contadores de estado y logs.

Broma #2: “Simplemente lo haremos síncrono y rápido” es el equivalente en bases de datos de “seré calmado y puntual durante la cola de seguridad del aeropuerto.”

Tres mini-historias corporativas desde las trincheras

Incidente causado por una suposición errónea: “SQLite puede manejar unos pocos escritores, ¿verdad?”

Un equipo de producto de tamaño medio lanzó un nuevo servicio de ingestión. Cada contenedor tomaba eventos de una cola y los escribía en un archivo SQLite local para “buffering temporal”; luego otro job enviaría el archivo a object storage. La suposición fue que “es disco local, así que será rápido.” Y lo fue—durante la demo del camino feliz.

Luego llegó producción. El autoscaling arrancó múltiples contenedores en el mismo nodo, todos escribiendo al mismo archivo de SQLite vía un hostPath compartido. En cuanto subió el tráfico, los escritores colisionaron. SQLite hizo lo que está diseñado para hacer: serializar escrituras. La aplicación hizo lo que está diseñada para hacer: entrar en pánico.

Los síntomas fueron desordenados: timeouts de petición, errores “database is locked”, y un bucle de reintentos que multiplicó la ráfaga. El host en sí parecía infrautilizado en CPU, lo que incentivó el instinto de depuración exactamente equivocado: “no puede ser la base de datos; la CPU está inactiva.”

La solución fue embarazosamente simple y operativamente adulta: un escritor por archivo de base de datos. Cambiaron a archivos SQLite por contenedor e introdujeron una cola de escritura explícita en proceso. Cuando necesitaron escrituras entre contenedores, movieron la capa de buffering a MariaDB con pooling de conexiones y batching de transacciones.

La lección: SQLite es increíble cuando controlas la serialización de escrituras intencionalmente. Es caos cuando descubres la serialización por accidente.

Optimización que salió mal: “Relajemos fsync y subamos hilos”

Una plataforma administrativa interna corrió sobre MariaDB. Durante un job de importación trimestral, vieron picos de latencia en commits. Alguien (bienintencionado, cansado) cambió innodb_flush_log_at_trx_commit de 1 a 2 y aumentó la concurrencia en el importador de 16 a 128 hilos. Querían “empujar el lote más rápido” y reducir la ventana de dolor.

El throughput mejoró durante unos cinco minutos. Luego el sistema golpeó otro muro: churn del buffer pool más amplificación de escritura por índices secundarios. Las páginas sucias se acumularon más rápido de lo que el flushing podía seguir. InnoDB empezó a hacer flushing agresivo. La latencia pasó de esporádica a consistentemente terrible, y el primario empezó a retrasar la replicación porque el patrón de fsync del binlog cambió bajo carga.

No perdieron datos, pero sí tiempo: la importación tomó más al final porque el sistema oscilaba entre ráfagas de progreso y largos stalls. Mientras tanto, el tráfico orientado al usuario sufrió porque la base de datos no pudo mantener tiempos de respuesta estables.

La solución eventual no fue “más tuning.” Fue moldeado disciplinado de la carga: limitar el importador, agrupar commits y programar el trabajo con un límite de velocidad predecible. Mantuvieron las configuraciones de durabilidad conservadoras y arreglaron el verdadero problema: el importador no tenía por qué comportarse como una prueba DDoS.

La lección: girar perillas sin controlar la concurrencia es cómo cambias un modo de fallo por uno más confuso.

Práctica aburrida pero correcta que salvó el día: “Medir fsync, mantener margen, ensayar restauraciones”

Un servicio adyacente a pagos (del tipo donde no puedes ser creativo con la durabilidad) usaba MariaDB con InnoDB. Cada pocas semanas tenían una ráfaga: jobs de conciliación más un pico de tráfico. Nunca causó una caída, y nadie celebró eso. Ese era el punto.

Tenían una rutina aburrida. Medían la latencia del disco (incluyendo fsync) continuamente, no sólo IOPS. Mantuvieron un margen en la capacidad del redo log y dimensionaron el buffer pool para que el sistema no hiciera thrash durante las ráfagas. También ensayaban restauraciones con una cadencia para que nadie aprendiera el comportamiento de backups durante un incidente.

Un día el jitter de latencia del almacenamiento se duplicó debido a un vecino ruidoso en el hardware subyacente. El servicio no se cayó. Se volvió más lento, las alarmas saltaron temprano y el equipo aplicó una mitigación conocida: limitar temporalmente la tasa de jobs y pausar escritores no críticos. El tráfico de usuarios se mantuvo dentro del SLO.

Más tarde, cuando migraron a otro almacenamiento, ya tenían líneas base que demostraban que la capa de almacenamiento era la culpable. Las reuniones de procurement son mucho más fáciles cuando muestras gráficas en vez de emociones.

La lección: la práctica “aburrida” de medir lo correcto y mantener margen es el seguro más barato contra ráfagas que puedes comprar.

Guion rápido de diagnóstico

Cuando una ráfaga de escritura golpea y todo se vuelve raro, no tienes tiempo para filosofar. Necesitas un árbol de decisiones rápido: ¿estamos limitados por bloqueos, CPU o I/O?

Primero: confirma la forma del dolor (latencia vs throughput)

  • Si el throughput se mantiene alto pero la latencia p95/p99 explota: busca stalls por fsync/journal/checkpoint.
  • Si el throughput colapsa: busca contención de bloqueos, agotamiento de hilos o saturación del almacenamiento.

Segundo: decide si es específico de SQLite o MariaDB

  • SQLite: errores como “database is locked”, esperas largas, crecimiento del WAL, o stalls de checkpoint.
  • MariaDB: hilos esperando flush de log, flushing de páginas sucias, esperas por bloqueos de fila, o lag de replicación que complica la presión.

Tercero: prueba o elimina el almacenamiento como cuello de botella

  • Revisa latencia de disco, profundidad de cola y comportamiento de fsync bajo carga.
  • Si el almacenamiento es inestable, casi cualquier base de datos parecerá culpable.

Cuarto: deja de empeorarlo

  • Limita la fuente de la ráfaga (job por lotes, importador, bucle de reintentos).
  • Agrupa commits. Reduce la concurrencia. Apaga “reintentos infinitos sin jitter”.
  • Captura evidencia antes de reiniciar servicios. Los reinicios borran las pistas y rara vez arreglan la física.

Tareas prácticas: comandos, salidas y decisiones

Estas son las cosas que puedes ejecutar durante un incidente o una sesión de ajuste. Cada una incluye: comando, qué significa la salida y la decisión que tomas.

1) Ver si el sistema está saturado de I/O (Linux)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_ (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.1    0.0    4.3    31.7     0.0    51.9

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await  svctm
nvme0n1          20.0  900.0   640.0 42000.0    0.0    50.0   98.7   22.4   0.8

Significado: %util cercano a 100% junto con await alto indica que el dispositivo es el limitador; iowait también está elevado.

Decisión: Deja de “ajustar SQL” primero. Limita escritores, reduce la frecuencia de commits o muévete a almacenamiento más rápido. Si es un volumen compartido, investiga ruido de vecinos.

2) Identificar qué proceso está emitiendo más I/O de escritura

cr0x@server:~$ pidstat -d 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_  (8 CPU)

# Time   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:01:01  999     2145      0.00  52000.00     0.00  mariadbd
12:01:01 1000     8812      0.00   1200.00     0.00  importer

Significado: MariaDB está haciendo la mayoría de las escrituras; importer contribuye pero no domina.

Decisión: Enfócate en el comportamiento de flushing/redo de InnoDB y la latencia de almacenamiento, no sólo en agrupar la app.

3) Revisar filesystem y opciones de montaje (sorpresas de durabilidad viven aquí)

cr0x@server:~$ mount | grep -E " /var/lib/mysql | /data "
/dev/nvme0n1p2 on /var/lib/mysql type ext4 (rw,relatime,commit=5)
/dev/nvme1n1p1 on /data type xfs (rw,relatime,attr2,inode64,logbufs=8)

Significado: el intervalo commit de ext4 y el comportamiento del journal pueden afectar picos periódicos de latencia.

Decisión: Si ves stalls periódicos alineados con commits del journal, considera tunear el filesystem o mover archivos DB a un volumen con mejores características de latencia.

4) Medir latencia cruda tipo fsync con una prueba simple de sync

cr0x@server:~$ dd if=/dev/zero of=/var/lib/mysql/sync-test.bin bs=4k count=4096 oflag=dsync
4096+0 records in
4096+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 3.91 s, 4.3 MB/s

Significado: oflag=dsync fuerza sync por bloque; bajo throughput implica alto coste por sync. No es un modelo perfecto, pero revela “el almacenamiento engaña”.

Decisión: Si esto se ve terrible en discos “rápidos”, detente y arregla almacenamiento o settings de virtualización antes de culpar a la base de datos.

5) MariaDB: confirmar política de flush de InnoDB y tamaño de redo

cr0x@server:~$ mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_redo_log_capacity','innodb_io_capacity','innodb_io_capacity_max');"
+------------------------------+-----------+
| Variable_name                | Value     |
+------------------------------+-----------+
| innodb_flush_log_at_trx_commit | 1       |
| sync_binlog                   | 1        |
| innodb_redo_log_capacity      | 1073741824|
| innodb_io_capacity            | 200      |
| innodb_io_capacity_max        | 2000     |
+------------------------------+-----------+

Significado: Durabilidad total en redo y binlog (costosa durante ráfagas). La capacidad de redo puede ser pequeña depende de la carga.

Decisión: Si el p99 está muriendo y puedes tolerar pequeñas compensaciones de durabilidad, considera ajustar settings—pero sólo con aprobación de negocio. De lo contrario, mejora el rendimiento del almacenamiento y considera agrupar commits.

6) MariaDB: ver si esperas en flush de log

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_log_waits'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_log_waits | 1834  |
+------------------+-------+
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_os_log_fsyncs | 920044 |
+----------------------+--------+

Significado: Log waits significa que transacciones tuvieron que esperar al flush del redo log. Ráfagas + latencia de fsync = dolor.

Decisión: Reduce la frecuencia de commits (batch), reduce concurrencia o mejora latencia de fsync. No añadas sólo CPU.

7) MariaDB: comprobar presión de páginas sucias (deuda de flush)

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_total';"
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_dirty | 412345 |
+--------------------------------+--------+
+--------------------------------+--------+
| Variable_name                  | Value  |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_total | 524288 |
+--------------------------------+--------+

Significado: Una ratio muy alta de páginas sucias sugiere que el sistema está atrasado en flushing; los checkpoints pueden forzar stalls.

Decisión: Aumenta innodb_io_capacity con cuidado, asegúrate de que el almacenamiento puede sostener escrituras y reduce la tasa de entrada hasta que las páginas sucias se estabilicen.

8) MariaDB: identificar esperas por bloqueos y tablas calientes

cr0x@server:~$ mariadb -e "SELECT * FROM information_schema.innodb_lock_waits\G"
*************************** 1. row ***************************
requesting_trx_id: 123456
blocking_trx_id: 123455
blocked_table: `app`.`events`
blocked_lock_type: RECORD
blocking_lock_type: RECORD

Significado: Tienes contención en una tabla/índice específico.

Decisión: Arregla el punto caliente: añade índice, cambia patrón de acceso, evita contadores de una sola fila o haz sharding por clave/tiempo. Meter más hilos en contención de bloqueos lo empeora.

9) MariaDB: inspeccionar estados de hilos actuales (¿qué están esperando?)

cr0x@server:~$ mariadb -e "SHOW PROCESSLIST;"
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| Id  | User | Host      | db   | Command | Time | State                  | Info                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| 101 | app  | 10.0.0.12 | app  | Query   |   12 | Waiting for handler commit | INSERT INTO events ...     |
| 102 | app  | 10.0.0.13 | app  | Query   |   11 | Waiting for handler commit | INSERT INTO events ...     |
| 103 | app  | 10.0.0.14 | app  | Sleep   |    0 |                        | NULL                         |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+

Significado: “Waiting for handler commit” comúnmente se correlaciona con presión de commit/fsync.

Decisión: Investiga settings de flush de redo/binlog y latencia de disco; considera agrupar escrituras.

10) SQLite: verificar journal mode y synchronous

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA journal_mode; PRAGMA synchronous; PRAGMA wal_autocheckpoint;"
wal
2
1000

Significado: El modo WAL está habilitado; synchronous=2 es FULL (durable, más lento); autocheckpoint en 1000 páginas.

Decisión: Si estás teniendo picos y ves stalls, considera si realmente necesitas FULL. También planifica la estrategia de checkpoints (manual/controlado) en lugar de dejar que wal_autocheckpoint te sorprenda.

11) SQLite: detectar contención de bloqueo con una prueba controlada de escritura

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA busy_timeout=2000; BEGIN IMMEDIATE; INSERT INTO events(ts, payload) VALUES(strftime('%s','now'),'x'); COMMIT;"

Significado: Si esto falla intermitentemente con “database is locked”, tienes escritores compitiendo o transacciones largas.

Decisión: Introduce una cola de un solo escritor, acorta transacciones y asegúrate de que los lectores no mantienen bloqueos más tiempo del esperado (por ejemplo, SELECTs de larga duración dentro de una transacción).

12) SQLite: vigilar crecimiento del WAL y salud de checkpoints

cr0x@server:~$ ls -lh /data/app.db /data/app.db-wal /data/app.db-shm
-rw-r--r-- 1 app app 1.2G Dec 30 12:05 /data/app.db
-rw-r--r-- 1 app app 3.8G Dec 30 12:05 /data/app.db-wal
-rw-r--r-- 1 app app  32K Dec 30 12:05 /data/app.db-shm

Significado: El WAL es más grande que la BD principal. No es automáticamente fatal, pero es una señal de que el checkpointing no está al día.

Decisión: Ejecuta un checkpoint controlado en una ventana tranquila, o ajusta la carga para que los checkpoints ocurran de forma predecible. Investiga lectores de larga duración que impidan el progreso del checkpoint.

13) SQLite: comprobar si los lectores bloquean los checkpoints (base ocupada)

cr0x@server:~$ sqlite3 /data/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0

Significado: Los tres números son (busy, log, checkpointed). Ceros después de TRUNCATE sugiere que el checkpoint tuvo éxito rápidamente y el WAL se truncó.

Decisión: Si “busy” no es cero o el WAL no se trunca, busca transacciones de lectura de larga duración y arréglalas (acorta lecturas, evita mantener transacciones abiertas).

14) MariaDB: confirmar dimensionado del buffer pool y presión

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';"
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 18403921 |
+-------------------------+----------+

Significado: Si las lecturas de buffer pool suben rápidamente durante la ráfaga, estás perdiendo caché y haciendo más I/O físico del planeado.

Decisión: Aumenta buffer pool (si RAM lo permite), reduce el working set (índices, patrones de consulta) o shardea la carga. No ignores el SO; el swapping arruinará tu día.

15) Sospecha de almacenamiento en red: comprobar distribución de latencia rápidamente

cr0x@server:~$ ioping -c 10 -W 2000 /var/lib/mysql
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=1 time=0.8 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=2 time=1.1 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=3 time=47.9 ms
...
--- /var/lib/mysql ioping statistics ---
10 requests completed in 12.3 s, min/avg/max = 0.7/6.4/47.9 ms

Significado: Ese pico de latencia máximo es exactamente lo que parece la latencia de commit cuando el disco hace un hipo.

Decisión: Si ves jitter como este, deja de perseguir micro-optimización en SQL. Arregla QoS de almacenamiento, mueve volúmenes o añade buffering/batching.

16) Encontrar tormentas de reintentos en logs de aplicación (la “ráfaga auto-amplificadora”)

cr0x@server:~$ journalctl -u app-ingester --since "10 min ago" | grep -E "database is locked|retrying" | tail -n 5
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=7
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=8
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=9
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=10
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=11

Significado: No sólo estás experimentando contención; la estás multiplicando con reintentos.

Decisión: Añade backoff exponencial con jitter, limita los reintentos y considera una cola de un solo escritor. Reintentar agresivamente es cómo conviertes un pico en una caída.

Errores comunes (síntomas → causa raíz → solución)

1) Síntoma: errores “database is locked” durante picos (SQLite)

Causa raíz: Múltiples escritores concurrentes o transacciones de larga duración que mantienen bloqueos; la realidad de un solo escritor choca con carga multi-escritor.

Solución: Serializa escrituras explícitamente (un hilo/proceso escritor), usa WAL mode, establece un busy_timeout sensato y agrupa commits. Evita mantener transacciones de lectura abiertas mientras escribes.

2) Síntoma: stalls periódicos 200–2000 ms cada N segundos (SQLite)

Causa raíz: Ciclos de checkpoint WAL o commits del journal del filesystem que crean comportamiento de sync ráfaga.

Solución: Controla checkpoints (manuales en ventanas tranquilas), ajusta wal_autocheckpoint, reduce synchronous sólo con requisitos claros de durabilidad y valida jitter de latencia del almacenamiento.

3) Síntoma: p99 de MariaDB se dispara mientras la CPU está baja

Causa raíz: Commits ligados a I/O: la latencia de redo/binlog fsync domina; hilos esperan log flush o handler commit.

Solución: Agrupa transacciones, reduce concurrencia, revisa innodb_flush_log_at_trx_commit y sync_binlog con aprobación de negocio, y mejora la latencia del almacenamiento.

4) Síntoma: el throughput colapsa cuando “añades más workers” (MariaDB)

Causa raíz: Contención de locks/latches o presión de flushing amplificada por thrash de hilos; más concurrencia aumenta los context switches y la contención.

Solución: Limita la concurrencia, usa pooling de conexiones, arregla índices/tablas calientes y ajusta el flushing en segundo plano de InnoDB en lugar de añadir hilos.

5) Síntoma: el archivo WAL crece eternamente (SQLite)

Causa raíz: Lectores de larga duración impiden que el checkpoint complete; o wal_autocheckpoint no coincide con la carga.

Solución: Asegura que los lectores no mantienen transacciones abiertas, ejecuta wal_checkpoint en ventanas controladas y considera repartir la carga entre múltiples archivos DB si la contención es estructural.

6) Síntoma: picos de lag de replicación en MariaDB durante imports

Causa raíz: fsync del binlog y flush de redo bajo carga pesada de escritura; la aplicación en el replica (según la configuración) puede no seguir el ritmo.

Solución: Agrupa escrituras, programa imports, revisa settings de durabilidad del binlog y asegúrate de que la configuración de apply en réplicas coincide con la carga. No trates la replicación como “gratis”.

7) Síntoma: “Rápido en mi portátil, lento en prod” (ambos)

Causa raíz: Semánticas de almacenamiento diferentes: NVMe de portátil vs volumen compartido en la nube; fsync y jitter de latencia son universos distintos.

Solución: Benchmark en almacenamiento similar a producción, mide la distribución de latencias y establece SLOs alrededor de la latencia p99 de commit—no sólo el throughput medio.

Listas de verificación / plan paso a paso

Si eliges entre MariaDB y SQLite para escrituras ráfaga

  1. Cuenta escritores, no peticiones. ¿Cuántos procesos/hosts pueden escribir concurrentemente?
  2. Decide si puedes imponer un escritor único. Si sí, SQLite sigue siendo opción.
  3. Define requisitos de durabilidad claramente. “Podemos perder 1 segundo de datos” es un requisito real; “debe ser durable” no lo es.
  4. Mide la latencia de fsync del almacenamiento. Si es inestable, ambas bases parecerán poco fiables ante los picos.
  5. Planea los backfills. Si rutinariamente importas o reprocesas datos, diseña throttling y batching desde el día uno.

Plan práctico de endurecimiento para SQLite

  1. Habilita WAL mode y confirma que permanece habilitado.
  2. Configura busy_timeout a un valor no trivial (centenas a miles de ms) y maneja SQLITE_BUSY con backoff + jitter.
  3. Agrupa commits: commit cada N filas o cada T milisegundos.
  4. Introduce una cola de escritura con un hilo escritor. Si existen múltiples procesos, introduce un proceso escritor único.
  5. Controla checkpoints: ejecuta wal_checkpoint durante baja carga; ajusta wal_autocheckpoint.
  6. Vigila tamaño del WAL y éxito de checkpoints como métricas de primera clase.

Plan práctico de endurecimiento para MariaDB

  1. Confirma que usas InnoDB para tablas con escrituras ráfaga.
  2. Dimensiona buffer pool para que el working set quepa tanto como sea razonable sin hacer swapping.
  3. Revisa la capacidad del redo log; evita redos demasiado pequeños que fuerzan checkpoints frecuentes.
  4. Alinea innodb_io_capacity con la capacidad real del almacenamiento (no con deseos).
  5. Limita la concurrencia de la aplicación; usa pooling de conexiones; evita tormentas de hilos.
  6. Agrupa escrituras y usa inserts multi-fila cuando sea seguro.
  7. Mide y alerta sobre log waits, indicadores de latencia de fsync y ratio de páginas sucias.

Cuándo migrar de SQLite a MariaDB (o viceversa)

  • Migrar SQLite → MariaDB cuando no puedes imponer un escritor único, necesitas escrituras multi-host o las herramientas operativas (replicación/backup online) importan.
  • Migrar MariaDB → SQLite cuando la carga es local, de un solo escritor y estás pagando sobrecarga operativa innecesaria por un dataset pequeño embebido.

Preguntas frecuentes

1) ¿Puede SQLite manejar alto throughput de escritura?

Sí—si agrupas transacciones y mantienes los escritores serializados. SQLite puede ser extremadamente rápido por núcleo porque evita saltos de red y sobrecarga de servidor.

2) ¿Por qué SQLite dice “database is locked” en vez de encolar escritores?

El modelo de bloqueo de SQLite es simple e intencional. Espera que la aplicación controle la concurrencia (busy_timeout, reintentos y, idealmente, un escritor único). Si quieres que la base de datos gestione alta concurrencia multi-escritor, estás describiendo una BD servidor.

3) ¿Es el modo WAL siempre la opción correcta para SQLite ante ráfagas?

A menudo, pero no siempre. WAL ayuda a lecturas concurrentes durante escrituras y puede suavizar carga de escritura sostenida. También introduce comportamiento de checkpoint que debes gestionar. Si ignoras los checkpoints, obtendrás stalls periódicos y archivos WAL gigantes.

4) Para MariaDB, ¿qué configuración afecta más el comportamiento ante ráfagas?

innodb_flush_log_at_trx_commit y (si usas binlog) sync_binlog. Determinan directamente con qué frecuencia pagas el coste de fsync. Cambiarlas altera la durabilidad, así que trátalo como una decisión de negocio.

5) ¿Por qué los picos de escritura a veces empeoran después de añadir índices?

Los índices aumentan la amplificación de escritura. Un insert se convierte en múltiples actualizaciones de B-tree y más páginas sucias. En picos, la diferencia entre “una escritura” y “cinco escrituras” no es teórica; es tu p99.

6) ¿Debería poner SQLite en almacenamiento en red?

Normalmente no. SQLite depende de bloqueos y semánticas de sync correctas y de baja latencia. Filesystems en red y algunos volúmenes distribuidos pueden hacer el locking impredecible y fsync dolorosamente lento. Si debes hacerlo, prueba la implementación de almacenamiento exacta bajo carga.

7) Si MariaDB va lento durante ráfagas, ¿debo subir CPU?

Sólo después de probar que estás limitado por CPU. La mayoría del dolor en ráfagas es latencia I/O o contención. Añadir CPU a un cuello de botella por fsync es como poner más cajas registradoras cuando la tienda sólo tiene una única salida.

8) ¿Cuál es la forma más simple de hacer que cualquiera de las dos bases maneje ráfagas mejor?

Agrupa commits y limita la concurrencia. Las ráfagas suelen ser autoinfligidas por “trabajadores ilimitados” y “commit por fila”. Arregla eso primero.

9) ¿Cuál es más segura para durabilidad ante picos?

Ambas pueden ser seguras; ambas pueden configurarse de forma insegura. Los valores por defecto de MariaDB tienden a ser conservadores para workloads de servidor. SQLite también puede ser totalmente durable, pero el coste de rendimiento ante ráfagas es más visible porque está en el camino de la petición.

10) ¿Cómo sé si estoy limitado por checkpointing?

SQLite: el WAL crece y los checkpoints reportan “busy” o no se truncan. MariaDB: suben los log waits, aumentan las páginas sucias y ves stalls ligados al flushing. En ambos casos, correlaciona con picos de latencia del disco.

Conclusión: pasos prácticos siguientes

Si tienes escrituras ráfaga y estás decidiendo entre MariaDB y SQLite, no empieces por la ideología. Empieza por el modelo de escritura.

  • Si puedes imponer un escritor, agrupar commits y mantener la base en almacenamiento local de baja latencia, SQLite manejará los picos de forma silenciosa y económica.
  • Si tienes muchos escritores entre procesos/hosts y necesitas herramientas operativas como replicación y observabilidad robusta, MariaDB es la apuesta más segura—siempre que respetes la física de fsync y afines con cuidado.

Luego haz el trabajo poco glamoroso que evita el drama: mide la latencia de fsync, limita la concurrencia, agrupa escrituras y convierte checkpoints/flushing en una parte controlada del sistema en lugar de una sorpresa. Tu yo futuro seguirá estando cansado, pero al menos estará aburrido. Ese es el objetivo.

Dovecot: maildir vs mdbox — elige el almacenamiento que no te perseguirá

No te fijas en el formato de buzón cuando todo está tranquilo. Te fijas cuando el iPhone del CEO muestra “Cannot Get Mail”, tus discos están al 70% de inactividad y, sin embargo, cada inicio de sesión IMAP se siente como negociar con un archivador lleno de confeti.

El almacenamiento de correo es una de esas decisiones de infraestructura que permanece aburrida—hasta que se convierte en lo único de lo que todo el mundo quiere hablar. Mantengámoslo aburrido, a propósito.

La decisión que realmente importa

“Maildir vs mdbox” suena a debate de formatos. No lo es. Es un debate de filosofía operacional:

  • Maildir apuesta por la semántica del sistema de archivos: cada mensaje es un archivo separado; los renombrados atómicos son tus aliados; la corrupción tiende a localizarse; y obtienes muchos inodos.
  • mdbox apuesta por la agregación gestionada por Dovecot: los mensajes viven en archivos contenedor más grandes con metadatos de Dovecot; reduces la presión de inodos; las operaciones pueden ser más rápidas bajo ciertos patrones de E/S; y cuando fallas, puedes estropear más.

Si gestionas un servidor pequeño con un sistema de archivos razonable y quieres depuración sencilla y recuperación parcial fácil, Maildir es la opción por defecto que envejecerá bien. Si operas a escala donde el número de inodos, el escaneo de directorios y la sobrecarga de pequeños archivos te están matando, mdbox puede ser el dolor correcto—pero solo si eres disciplinado con las copias, el mantenimiento y las herramientas operativas.

Una cita que vale la pena pegar en una nota adhesiva, porque se aplica directamente a formatos de buzón: “La esperanza no es una estrategia.” — Gene Kranz.

Maildir y mdbox en una sola pantalla

Maildir: qué es

Maildir almacena cada mensaje como un archivo separado en una estructura de directorios—típicamente cur/, new/ y tmp/ por buzón. Las banderas a menudo se codifican en el nombre del archivo. La entrega y los movimientos dependen del comportamiento de renombrado atómico.

Ambiente operativo: Cuando algo se rompe, a menudo puedes abrir un directorio y ver los mensajes. Puedes recuperar a un usuario sin sentir que estás desactivando una bomba.

mdbox: qué es

mdbox almacena mensajes dentro de archivos “box” gestionados por Dovecot (con metadatos de índice y mapa acompañantes). Piénsalo como Dovecot ocupándose más de la capa de almacenamiento: menos archivos, más estructura y mayor dependencia de las garantías de consistencia de Dovecot.

Ambiente operativo: Cuando es rápido, es agradable. Cuando necesitas reparar, quieres tener tus herramientas listas y tus copias verificadas.

Qué deberías elegir (opinión)

  • Elige Maildir si: eres una operación pequeña a mediana, valoras la recuperación simple, tienes SSD razonables, usas snapshots/copias y deseas dominios de fallo previsibles.
  • Elige mdbox si: tienes muchos usuarios, muchos mensajes, la presión de inodos es real, el coste de listar directorios duele, o necesitas características de almacenamiento como recuentos de archivos eficientes—y estás dispuesto a operacionalizar el mantenimiento de Dovecot y los ensayos de copia/restauración.
  • Evita “no importa” como decisión. Importa el día que necesites restaurar un buzón a las 3 a. m. y tu proceso de restauración sea básicamente “restaurar todo y rezar”.

Broma #1: Las decisiones sobre almacenamiento de correo son como los tatuajes: parecen divertidas hasta que intentas quitártelas durante la revisión trimestral de incidencias.

Hechos e historia que explican los compromisos actuales

Un poco de contexto hace que los compromisos parezcan menos arbitrarios. Aquí hay puntos concretos que muestran por qué existen estos formatos y por qué se comportan como lo hacen:

  1. Maildir se diseñó para evitar los problemas de bloqueo de mbox. mbox tradicional almacena todo un buzón en un solo archivo; el acceso concurrente históricamente causó problemas de bloqueo y riesgo de corrupción.
  2. El truco de “renombrado atómico” de Maildir depende de las garantías del sistema de archivos. El patrón tmp→new/cur depende de la atomicidad dentro del mismo sistema de archivos.
  3. IMAP multiplicó las necesidades de “metadatos” del buzón. Indexado, banderas y seguimiento de UID se volvieron críticos para el rendimiento; los archivos de índice de Dovecot son respuesta a esa realidad.
  4. La sobrecarga de archivos pequeños se convirtió en un gran problema conforme creció la retención de correo. Millones de archivos diminutos estresan inodos, búsquedas en directorios y herramientas de backup; esa presión es una razón por la que existen formatos agregados.
  5. Dovecot introdujo formatos “box” para reducir el desgaste del sistema de archivos. mdbox y diseños similares trasladan trabajo desde el sistema de archivos a estructuras gestionadas por Dovecot.
  6. La evolución de los sistemas de archivos importa. Ext4, XFS, ZFS y btrfs manejan directorios y metadatos de forma distinta; el mismo formato puede ser “aceptable” en uno y problemático en otro.
  7. Los snapshots copy-on-write cambiaron las expectativas de respaldo. Con snapshots ZFS/btrfs, el “punto en el tiempo consistente” es más sencillo—pero solo si tu modelo de indexado y bloqueo se comporta bien bajo snapshots.
  8. Los clientes de correo se volvieron más agresivos. Los clientes móviles hacen sincronizaciones frecuentes; la búsqueda/FTS en servidor se volvió esperada; los formatos que amplifican la E/S de metadatos pueden sentirse peor hoy que en 2008.

Cómo falla cada formato en producción

Modos de fallo de Maildir

1) “Demasiados archivos” se convierte en una interrupción real. Alcanzas la extenuación de inodos, las copias se ralentizan hasta arrastrarse o las exploraciones de directorios se convierten en tu piso de latencia. Maildir no te avisa cortésmente; simplemente se vuelve lento y luego repentinamente imposible.

2) La corrupción parcial es sobrevivible—pero no gratuita. Algunos archivos de mensaje pueden corromperse por problemas de disco o transferencias rotas. Normalmente puedes recuperar el resto. Pero si tus archivos de índice se vuelven raros, los clientes verán mensajes ausentes o duplicados hasta que reconstruyas índices.

3) Las copias mienten si no haces snapshots. Un backup archivo por archivo mientras la entrega está en curso puede capturar estados inconsistentes (mensajes en tmp/, renombrados parciales). Aún puede funcionar, pero necesitas entender qué significa “consistente” para maildir.

Modos de fallo de mdbox

1) La consistencia de metadatos se vuelve tu vida. mdbox se apoya en metadatos de Dovecot (índices, archivos map). Si esos están desincronizados o corruptos, el buzón puede parecer vacío o revuelto incluso si los archivos box subyacentes existen.

2) Radio de impacto mayor por archivo. Un archivo contenedor corrupto puede afectar a más mensajes. Las herramientas de Dovecot pueden reparar en muchos casos, pero el aislamiento de “archivo de mensaje único” de maildir no es lo habitual aquí.

3) La complejidad de restauración aumenta. Restaurar un buzón puede ser sencillo si tienes directorios por usuario y buenas herramientas. También puede ser un desastre si hiciste “un volumen gigante y esperanza”. El diseño importa.

Broma #2: El mejor formato de buzón es el que puedes restaurar mientras tu café todavía es potable.

Modelo de rendimiento: por lo que realmente pagas

El impuesto oculto en Maildir: metadatos y operaciones de directorio

El rendimiento de Maildir está dominado por metadatos del sistema de archivos: crear, renombrar, stat, listar directorios y actualizar marcas de tiempo. Si tienes SSD y un sistema de archivos que maneja bien los directorios, Maildir puede ser muy rápido. Si tienes discos giratorios o rutas de metadatos saturadas, Maildir puede sentirse como si hiciera todo excepto servir correo.

Cuando los usuarios tienen cientos de miles de mensajes en una sola carpeta, Maildir puede degradarse drásticamente porque el servidor termina haciendo muchas operaciones de directorio solo para responder “¿qué hay de nuevo?”. Los índices de Dovecot ayudan, pero el recuento subyacente de archivos aún te persigue en backups, tiempo de fsck y uso de inodos.

El impuesto oculto en mdbox: estructuras gestionadas por Dovecot y flujo de reparación

mdbox tiende a reducir la presión del recuento de archivos, lo que puede reducir la sobrecarga de directorios e inodos. Pero pagas en otra moneda: necesitas confiar y mantener las estructuras de metadatos de Dovecot. Eso significa que te importa la integridad de índices, la salud de los archivos map y cómo interactúan tus copias/restauraciones con esos archivos.

En sistemas muy ocupados, mdbox puede ser más amable con el sistema de archivos, pero también puede amplificar las consecuencias de afinamientos “ingeniosos” o prácticas de copia inseguras.

Latencia vs rendimiento sostenido: elige lo que sienten tus usuarios

La mayoría de las interrupciones de correo no son “el servidor no puede manejar el throughput”. Son “el login es lento”, “abrir INBOX es lento”, “la búsqueda es lenta”, “las actualizaciones de banderas son lentas”. Eso es latencia. La latencia viene de viajes de ida y vuelta al almacenamiento y de la contención de metadatos.

Regla práctica: Si tu dolor es intenso en metadatos (recuento de archivos, escaneos de directorio, rastreo de backups), mdbox empieza a verse mejor. Si tu dolor es sencillez de reparación y recuperación, Maildir es difícil de superar.

Copias, restauraciones y por qué “son solo archivos” es una trampa

Backups de Maildir: engañosamente simples

Maildir parece “solo archivos”, lo que hace que la gente se relaje. No lo hagas. Maildir en vivo tiene estados transitorios (tmp/), renombrados y actualizaciones de índices. Si lo respaldas sin snapshots, puedes capturar un buzón en pleno vuelo.

Qué funciona bien:

  • Snapshots de sistema de archivos (ZFS, btrfs, snapshots LVM thin) y leer las copias desde el snapshot.
  • Backups que preserven permisos, propietario y marcas de tiempo (la entrega de correo y Dovecot son sensibles a esto).
  • Prácticas regulares de reconstrucción de índices durante las pruebas de restauración.

Backups de mdbox: necesitas consistencia, no solo copias

mdbox requiere capturar tanto los archivos box como los archivos de metadatos/índice en un punto en el tiempo consistente. Los backups basados en snapshots son la línea base sensata. Si te basas en copias archivo por archivo sin snapshots, corres el riesgo de capturar estados descoordinados—el archivo box dice una cosa, el índice otra y el mapa una tercera.

Restauraciones: planifica para “un usuario, una carpeta, un mensaje”

La restauración que importa no es “restaurar todo el servidor”. Es “restaurar una carpeta de buzón para un ejecutivo porque un cliente sincronizó y borró todo”. Esa restauración debe ser un runbook, no una improvisación heroica.

Si no puedes restaurar un solo buzón sin restaurar el mundo, no elegiste un formato de almacenamiento; elegiste un incidente futuro.

Replicación/HA: verificación de la realidad

El formato de buzón no reemplaza la estrategia de replicación. Solo cambia los modos de fallo y la ergonomía operativa.

Replicación de Dovecot y elección de formato

Dovecot puede replicar buzones a nivel de aplicación. Eso puede suavizar algunas diferencias del sistema de archivos. Pero la replicación no arregla:

  • La mala planificación de capacidad (la extenuación de inodos sigue ocurriendo, solo que en dos máquinas).
  • El almacenamiento lento (ahora tienes almacenamiento lento más sobrecarga de replicación).
  • Prácticas de copia inseguras (la replicación replicará gustosamente eliminaciones y algunas formas de corrupción).

Snapshots no son replicación, replicación no es backup

Los snapshots te dan recuperación a un punto en el tiempo; la replicación te da disponibilidad. Quieres ambos si el sistema de correo importa. Si solo puedes permitirte uno, elige copias que hayas probado. Disponibilidad sin recuperación es solo una forma más rápida de permanecer caído.

Tareas prácticas: comandos, salidas y lo que decides

Estos son los chequeos que realmente ejecuto cuando alguien dice “IMAP está lento”, “los mensajes desaparecieron” o “el disco está bien, pero el correo muere”. Cada tarea incluye qué significa la salida y la decisión que tomas.

1) Confirmar el formato actual de buzón

cr0x@server:~$ doveconf -n | egrep '^(mail_location|mail_attachment_dir|mail_plugins|namespace|mail_fsync)'
mail_location = maildir:~/Maildir
mail_plugins = $mail_plugins quota
mail_fsync = optimized

Significado: Este servidor usa maildir en el directorio home de cada usuario. Si esperabas mdbox, tus “suposiciones de rendimiento” ya están equivocadas.

Decisión: Mantén la investigación alineada con el formato: la presión de inodos y las operaciones de directorio importan más con maildir; la consistencia de metadatos y la integridad map/índice importan más con mdbox.

2) Comprobar la versión de Dovecot (las funciones y errores importan)

cr0x@server:~$ dovecot --version
2.3.19.1 (9b53102964)

Significado: Estás en una 2.3.x moderna. El comportamiento difiere entre versiones mayores/menores, especialmente alrededor del manejo de índices y los valores por defecto de fsync.

Decisión: Si estás en algo antiguo, considera actualizar antes de hacer afinamientos “ingeniosos”. Los errores antiguos en almacenamiento de correo no son encantadores.

3) Medir uso de inodos (el asesino silencioso de Maildir)

cr0x@server:~$ df -ih /var/vmail
Filesystem      Inodes IUsed   IFree IUse% Mounted on
/dev/sdb1         50M   41M     9M   83% /var/vmail

Significado: 83% de uso de inodos. Eso no es “bien”. Eso es “a una mala importación de tiempo de inactividad”.

Decisión: Si el uso de inodos crece rápido, o bien (a) migra a un sistema de archivos con más inodos/estrategia de asignación distinta, (b) aplica retención, (c) mueve usuarios de alto volumen a mdbox, o (d) rediseña el particionado/archivo de carpetas.

4) Contar archivos de mensaje en una carpeta caliente

cr0x@server:~$ find /var/vmail/acme.example/jane/Maildir/cur -type f | wc -l
287641

Significado: 287k archivos en un directorio. Muchos sistemas de archivos manejan esto mal bajo churn, incluso si las lecturas están cacheadas.

Decisión: Considera particionar carpetas (archivos por año), cambios en la política del cliente, o mover a ese usuario a mdbox si el coste operativo se repite.

5) Identificar si Dovecot pasa tiempo en IO wait

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           3.21    0.00    1.10   24.50    0.00   71.19

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
nvme0n1         85.00  220.00  7400.0 14800.0     95.0     3.20   10.50     5.10    12.60   0.35  10.70

Significado: El iowait de CPU es alto (24.5%). El almacenamiento no está saturado (%util ~10%), pero la latencia (await) no es buena. Eso a menudo apunta a patrones intensivos en sync o contención de metadatos más que a límites de throughput bruto.

Decisión: Revisa ajustes de fsync, procesos Dovecot que provoquen picos de sync y opciones de montaje del sistema de archivos. Para maildir, los patrones de escritura de metadatos pueden provocar esto incluso en SSD.

6) Ver qué procesos causan presión de IO

cr0x@server:~$ pidstat -d 1 5
Linux 6.1.0-18-amd64 (server) 	01/03/2026 	_x86_64_	(8 CPU)

# Time        UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:10:01     1001     23142      0.00   9200.00      0.00  dovecot
12:10:01     1001     23188      0.00   5100.00      0.00  dovecot
12:10:01        0     11422      0.00   1400.00      0.00  rsync

Significado: Dovecot está escribiendo intensamente (probablemente actualizaciones de índices, cambios de banderas, entregas). También hay un rsync en ejecución—clásico “backup compitiendo con la E/S en vivo”.

Decisión: Si no tienes snapshots, detén los backups que rastrean archivos durante las horas pico. Pasa a backups basados en snapshot o programa rsync fuera de horas, o aplícale throttling.

7) Comprobar salud del servicio Dovecot y concurrencia

cr0x@server:~$ doveadm service status
auth: client connections: 12, server connections: 12
imap: client connections: 380, server connections: 380
lmtp: client connections: 0, server connections: 0
indexer-worker: client connections: 8, server connections: 8

Significado: IMAP tiene 380 conexiones activas. Los indexer workers están activos. Si estás infraaprovisionado en indexers, las búsquedas y la apertura de buzones pueden arrastrarse.

Decisión: Ajusta límites de procesos con responsabilidad. Si los indexers están saturados, aumenta workers o arregla la causa (por ejemplo, reconstrucciones constantes de índices por problemas de permisos o cachés rotas).

8) Medir latencia de apertura de buzón y status (desde la perspectiva de Dovecot)

cr0x@server:~$ doveadm -v mailbox status -u jane@example.com messages recent uidnext unseen INBOX
INBOX messages=142003 recent=0 uidnext=412887 unseen=12

Significado: Este comando debe devolver rápido. Si se queda colgado, tienes latencia de E/S, problemas de índice o contención de bloqueo.

Decisión: Si es lento: busca corrupción de índices, costosas exploraciones de sistema de archivos o almacenamiento bloqueado. Para maildir con INBOX enorme, fomenta archivado.

9) Identificar y reconstruir archivos de índice rotos de forma segura

cr0x@server:~$ doveadm -Dv index -u jane@example.com INBOX
doveadm(jane@example.com): Debug: Loading modules from directory: /usr/lib/dovecot/modules
doveadm(jane@example.com): Debug: Added plugin: quota
doveadm(jane@example.com): Debug: Finished indexing INBOX

Significado: Dovecot puede reconstruir índices. La salida de depuración confirma la carga de módulos y que el indexado finalizó.

Decisión: Si esto arregla mensajes faltantes en los clientes, tuviste inconsistencia de índices en lugar de pérdida de mensajes. Añade mantenimiento periódico de índices o arregla la causa raíz (permisos, errores de disco, reinicios forzados).

10) Buscar errores a nivel de sistema de archivos (el suero de la verdad aburrido)

cr0x@server:~$ dmesg -T | egrep -i 'ext4|xfs|btrfs|zfs|nvme|i/o error|reset|abort' | tail -n 10
[Fri Jan  3 11:58:41 2026] nvme nvme0: I/O 123 QID 6 timeout, completion polled
[Fri Jan  3 11:58:41 2026] nvme nvme0: resetting controller
[Fri Jan  3 11:58:43 2026] EXT4-fs warning (device sdb1): ext4_dx_add_entry: Directory index full, reach max htree level

Significado: Timeouts NVMe y una advertencia de indexado de directorio en ext4. Eso no es un problema de Dovecot. Es comportamiento de almacenamiento y sistema de archivos bajo estrés.

Decisión: Repara hardware/firmware, revisa la salud del NVMe y considera afinamiento del sistema de archivos o mover el almacenamiento de correo a un sistema de archivos mejor adaptado para directorios enormes.

11) Confirmar salud real del disco (antes de culpar al formato de mail)

cr0x@server:~$ smartctl -a /dev/nvme0 | egrep -i 'critical_warning|media_errors|num_err_log_entries|temperature'
Critical Warning:                   0x00
Temperature:                       41 Celsius
Media and Data Integrity Errors:    0
Error Information Log Entries:      2

Significado: No hay errores de medio, pero hay entradas en el registro de errores. Combinado con resets NVMe, puedes tener problemas intermitentes de controlador/firmware.

Decisión: Programa mantenimiento: actualizaciones de firmware, comprobaciones de controlador y considera redundancia. Los formatos de buzón no te salvarán de hardware inestable.

12) Comprobar distribución de directorios y archivos (detectar layouts patológicos)

cr0x@server:~$ du -sh /var/vmail/acme.example/jane/Maildir
96G	/var/vmail/acme.example/jane/Maildir

Significado: 96 GB para un solo usuario. Grande está bien, pero grande más muchos archivos pequeños lo cambia todo.

Decisión: Si unos pocos usuarios dominan almacenamiento y rendimiento, trátalos de forma especial: tier de almacenamiento separado, mdbox o volumen dedicado.

13) Comprobar opciones de montaje del sistema de archivos (los asesinos de latencia se esconden aquí)

cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /var/vmail
/var/vmail /dev/sdb1 ext4 rw,relatime,errors=remount-ro

Significado: Opciones estándar. Si ves sync u opciones de journaling extremadamente agresivas, podrías haberte infligido latencia a ti mismo.

Decisión: Evita cambios aleatorios de opciones de montaje por moda. Haz cambios solo con mejoras medidas de latencia y un plan de reversión.

14) Para mdbox: localizar metadatos clave y validar signos básicos de integridad

cr0x@server:~$ ls -la /var/vmail/acme.example/jane/mdbox/ | head
total 64
drwx------  5 vmail vmail 4096 Jan  3 12:01 .
drwx------ 12 vmail vmail 4096 Jan  3 12:01 ..
-rw-------  1 vmail vmail 8192 Jan  3 11:59 dovecot.index
-rw-------  1 vmail vmail 4096 Jan  3 11:59 dovecot.index.log
-rw-------  1 vmail vmail 2048 Jan  3 12:00 dovecot.map.index
-rw-------  1 vmail vmail 4096 Jan  3 11:58 storage

Significado: La presencia de archivos de índice y mapa es esperada. Archivos faltantes o de tamaño cero durante la operación normal pueden indicar corrupción o problemas de permisos.

Decisión: Si faltan o son ilegibles, arregla permisos/propietario primero; si sospechas corrupción, pasa a restauración desde snapshot o a los flujos de reparación de Dovecot.

15) Observar contención activa de locks en archivos de buzón

cr0x@server:~$ lsof +D /var/vmail/acme.example/jane/Maildir 2>/dev/null | head -n 10
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
dovecot 23142 vmail   15r  REG  8,17     12456 918273 /var/vmail/acme.example/jane/Maildir/dovecot.index
dovecot 23142 vmail   16u  REG  8,17     40960 918274 /var/vmail/acme.example/jane/Maildir/dovecot.index.log
dovecot 23188 vmail   18r  REG  8,17     53248 918275 /var/vmail/acme.example/jane/Maildir/cur/1735891023.M1234P23188.server,S=53248:2,S

Significado: Puedes ver qué archivos están calientes. Si muchos procesos compiten por archivos de índice/log, puedes tener una carga que churnea banderas o fuerza reescrituras de índice.

Decisión: Si la contención es consistente, evalúa la configuración de índices, la latencia del almacenamiento y el comportamiento del cliente (por ejemplo, clientes que se resyncéan todo constantemente).

Guía rápida de diagnóstico

Este es el orden que encuentra cuellos de botella rápidamente sin convertirse en un proyecto arqueológico de una semana.

Primero: prueba si es latencia de almacenamiento, CPU o concurrencia de Dovecot

  • IO wait y latencia: iostat -x 1 3 y pidstat -d 1 5. Iowait alto o await alto apunta a almacenamiento o patrones de sync.
  • Saturación de CPU: top o pidstat -u 1 5. Si la CPU está al máximo, no estás eligiendo entre maildir y mdbox—estás eligiendo entre escalar y reescribir.
  • Presión de conexiones: doveadm service status. Si las conexiones IMAP se disparan y los procesos se quedan sin recursos, ajusta límites y comportamiento de clientes.

Segundo: determina si el problema es “metadatos de sistema de archivos” o “metadatos de Dovecot”

  • Sospechas de Maildir: uso de inodos (df -ih), recuentos enormes de archivos en carpetas (find ... | wc -l), advertencias de directorio ext4 en dmesg.
  • Sospechas de mdbox: dovecot.map.index faltante/inválido y churn de logs de índice, consultas status lentas pese a métricas de almacenamiento razonables.

Tercero: valida si el problema está localizado o es sistémico

  • Ejecuta doveadm mailbox status en un usuario “caliente” y en uno “normal”.
  • Si solo unos pocos usuarios son lentos, trátalos como casos especiales (archivado, división de buzón, formato distinto, volumen distinto).
  • Si todos están lentos, sospecha del almacenamiento, indexers globales, backups o un cambio reciente en montaje/opciones/kernel/firmware.

Cuarto: elige la acción correctiva de menor riesgo

  • Reconstruir índices (seguro y reversible).
  • Detener E/S competidora (backups, escaneos antivirus, envío de logs agresivo).
  • Arreglar errores de sistema de archivos/hardware antes de afinar Dovecot.
  • Solo entonces considera migrar entre formatos.

Errores comunes: síntomas → causa raíz → solución

1) “El login IMAP es lento, pero la utilización del disco es baja”

Síntomas: Usuarios reportan retrasos al abrir carpetas; el monitoreo muestra %util de discos bajo.

Causa raíz: Alta latencia por operación (E/S de metadatos, escrituras sincrónicas, búsquedas en directorios). Baja utilización no significa baja latencia.

Solución: Mide await con iostat -x. Si la latencia es alta, reduce comportamientos sync-intensivos, mueve backups fuera del FS en vivo y considera mdbox si la sobrecarga de inodos/directorios domina.

2) “Las copias son consistentes porque usamos rsync nocturno”

Síntomas: Las restauraciones producen buzones raros: mensajes recientes ausentes, duplicados o tormentas de resync en clientes.

Causa raíz: Copia archivo por archivo capturó maildir/mdbox a medio actualizar; índices y mensajes no son del mismo punto en el tiempo.

Solución: Backups basados en snapshot. Restaura desde snapshot. Reconstruye índices post-restauración usando doveadm index o eliminando archivos de índice obsoletos con cuidado.

3) “Lo pondremos todo en un INBOX masivo”

Síntomas: Uno o dos usuarios siempre lentos; ventanas de backup se disparan; aparecen advertencias del sistema de archivos.

Causa raíz: Carpetas gigantes amplifican costos de listado y metadatos (maildir) o sobrecarga de indexado (cualquier formato).

Solución: Aplica políticas de archivado y foldering. Considera reglas Sieve en servidor. Divide buzones calientes entre tiers de almacenamiento.

4) “Migramos formatos y no planificamos la transición de índices”

Síntomas: Tras la migración, los clientes ven correo faltante hasta que se resyncean; la carga del servidor se dispara.

Causa raíz: Los índices no se reconstruyeron limpiamente o se restauraron de forma inconsistente; los clientes disparan sincronizaciones pesadas.

Solución: Reconstrucción de índices post-migración, reconexión escalonada de clientes y control de concurrencia. Comunica el resync esperado al helpdesk.

5) “Afinamos fsync para mejorar rendimiento”

Síntomas: El rendimiento mejora… hasta un fallo o corte de energía; entonces los usuarios pierden actualizaciones de banderas, entregas recientes o ven estado corrupto.

Causa raíz: Ajustes de durabilidad inseguros. El almacenamiento de correo es intensivo en escrituras de metadatos; perder unos segundos puede crear inconsistencias confusas.

Solución: Mantén la durabilidad razonable. Si quieres velocidad, compra mejor almacenamiento o rediseña; no apuestes la integridad a menos que toleres la pérdida.

6) “El antivirus escanea toda la tienda de correo cada hora”

Síntomas: Picos periódicos de latencia; muchas fallas de caché; picos de E/S; usuarios se quejan en oleadas.

Causa raíz: Los muchos archivos de Maildir castigan los escaneos completos; incluso mdbox sufre si los escaneos thrashéan caches.

Solución: Escanea en la ingestión (pipeline LMTP/SMTP) o usa escaneos dirigidos. Excluye índices y directorios transitorios de escaneos amplios cuando sea apropiado.

7) “Asumimos que el sistema de archivos no importa”

Síntomas: La misma configuración se comporta distinto en hosts diferentes; las actualizaciones cambian el rendimiento “aleatoriamente”.

Causa raíz: El indexado de directorios, el comportamiento del asignador y el journaling difieren por sistema de archivos y versión de kernel.

Solución: Estandariza la elección del sistema de archivos y las opciones de montaje para volúmenes de correo. Mide operaciones de buzón, no solo throughput secuencial.

Tres mini-historias corporativas (anonimizadas, plausibles e instructivas)

Mini-historia 1: La caída causada por una suposición equivocada

Gestionaban una plataforma de correo corporativa mediana. Nada exótico: Dovecot, Postfix, un clúster de VM y un volumen de almacenamiento “temporal” que se volvió permanente. Un miembro nuevo del equipo preguntó qué formato de buzón usaban. La respuesta fue segura y equivocada: “Es Maildir, así que son solo archivos. Las copias son fáciles.”

El trabajo de backup era una copia por recorrido nocturno. Sin snapshots. El trabajo corría mientras había entregas y ocasionalmente durante la sincronización móvil pico. “Funcionó” en el sentido de que produjo un montón de archivos. También capturó estados transitorios: mensajes en tmp, renombrados a medio completar, logs de índice a medio actualizar.

Meses después, una falla de almacenamiento obligó a una restauración. La restauración completó rápido—la dirección aplaudió. Luego la cola del helpdesk se volvió un ataque de denegación de servicio. Los usuarios vieron mensajes faltantes, hilos duplicados y carpetas que parecían vacías hasta que hacían clic y esperaban. Algunos clientes “lo arreglaron” volviendo a descargar todo, lo que hizo que el servidor trabajara más, lo que hizo que los clientes reintentaran más.

La causa raíz no fue Maildir. Fue la suposición de que el backup a nivel de archivo equivale a un backup consistente. Pasaron a backups basados en snapshot y añadieron un ensayo de restauración que incluía reconstrucción de índices y reconexión escalonada de clientes. La siguiente restauración fue aburrida. A todos les molestó menos. Así supieron que funcionó.

Mini-historia 2: La optimización que salió mal

Otra compañía tenía presión seria de inodos. Pasaron a mdbox para reducir el recuento de archivos y acortar el tiempo de escaneo de backups. Buena intuición. Luego decidieron exprimir rendimiento extra ajustando la durabilidad: reduciendo comportamiento de sync y empujando más el cache de escritura. En benchmarks se veía genial. Sus gráficas quedaron más bonitas.

Entonces tuvieron un evento de energía en un rack. No un desastre, solo unos minutos de caos. Los sistemas reiniciaron. La mayoría de servicios se recuperó. El correo no se recuperó limpiamente. Los usuarios podían iniciar sesión, pero el correo nuevo aparecía de forma inconsistente y las banderas se comportaban como sugerencias. Parte del correo existía en archivos box pero no aparecía en vistas IMAP hasta que los índices fueron reparados. Algunas reparaciones funcionaron; otras requirieron restauraciones de metadatos desde snapshots.

La revisión post-mortem no fue agradable. La “optimización” redujo el coste de cada escritura cambiando las propiedades de fiabilidad en las que confiaban implícitamente. Con almacenamiento agregado y estructuras de metadatos, pequeñas inconsistencias pueden cascada en estados visibles al usuario difíciles de explicar y más difíciles de soportar.

Revirtieron los ajustes riesgosos, invirtieron en protección de energía adecuada para el almacenamiento y estandarizaron en un enfoque probado de snapshots+replicación. El rendimiento quedó algo peor que la fantasía del benchmark. La disponibilidad fue mejor que la realidad de la caída. Elige la realidad.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una empresa regulada ejecutaba Dovecot a escala. Su tienda de correo era lo bastante grande como para que “restaurar todo” no fuera un plan; era una carta de renuncia. Usaban Maildir para la mayoría de usuarios y mdbox para un subconjunto de cuentas de alto volumen. La clave no eran los formatos. Era la disciplina.

Practicaban restauraciones trimestralmente. No “probamos backups”. Restauraciones reales: elegir un buzón al azar, restaurarlo en un host aislado, reconstruir índices, validar acceso IMAP y verificar recuentos de mensajes y entregas recientes. También tenían una política: snapshot cada pocos minutos, retención corta localmente, replicar snapshots fuera del host y probar la ruta de restauración de extremo a extremo.

Cuando un controlador de almacenamiento empezó a fallar, lo vieron temprano en dmesg y SMART. Antes de que se convirtiera en pérdida de datos, hicieron failover del servicio de correo al replicado y siguieron sirviendo usuarios. Luego restauraron algunos buzones afectados desde el último snapshot conocido bueno y los validaron con herramientas de Dovecot antes de reintroducirlos.

Sin heroísmos. Sin misterio. Solo el tipo de higiene operativa aburrida que parece cara hasta que es lo más barato que alguna vez hiciste.

Listas de verificación / plan paso a paso

Elegir un formato: lista de decisión

  1. ¿Cuántos mensajes por usuario? Si muchos usuarios superan 200k mensajes en una carpeta, Maildir castigará a tu sistema de archivos a menos que gestiones el particionado de carpetas.
  2. ¿Estás limitado por inodos? Si df -ih tiende por encima del 70% y sigue creciendo, trátalo como un problema de escalado, no como una simple etiqueta de advertencia.
  3. ¿Tienes backups basados en snapshots? Si no, arregla eso antes de cambiar formatos. Si no, solo cambiarás la forma de la inconsistencia de backups.
  4. ¿Necesitas recuperación parcial fácil? Maildir tiende a ser más amigable para recuperación quirúrgica. mdbox puede estar bien, pero necesitas herramientas y procesos practicados.
  5. ¿Cuál es tu sistema de archivos? Mide operaciones de buzón en el sistema de archivos y kernel reales que vas a ejecutar. Las cargas de correo son intensivas en metadatos y raras.
  6. ¿Tienes tiempo de personal para mantenimiento? Si no, elige la vía con las operaciones day-2 más simples: Maildir más buen snapshotting.

Plan de migración: Maildir → mdbox (secuencia relativamente segura)

  1. Inventaría usuarios e identifica buzones calientes (carpetas grandes, churn intenso).
  2. Implementa backups basados en snapshot y realiza un ensayo de restauración antes de migrar.
  3. Prepara un servidor de staging con la versión y configuración objetivo de Dovecot.
  4. Migra un grupo piloto pequeño primero. Monitoriza latencia IMAP, tiempo de reconstrucción de índices y problemas visibles por usuarios.
  5. Programa migraciones fuera de horas. Limita la concurrencia. No migres a todos a la vez a menos que disfrutes vivir peligrosamente.
  6. Después de cada lote: reconstruye índices, valida recuentos de buzones y observa tormentas de resync en clientes.
  7. Mantén capacidad de rollback vía snapshots y un marcador claro de corte.

Lista operativa base (independientemente del formato)

  • Backups basados en snapshot con restauraciones probadas.
  • Monitoreo de uso de inodos, crecimiento de directorios y crecimiento de volumen de correo.
  • Monitoreo de latencia de almacenamiento (await) y errores del kernel relacionados con almacenamiento.
  • Procedimientos definidos para reconstrucción de índices y reparación de buzones.
  • Controles sobre el comportamiento de clientes cuando sea posible (los patrones de sync agresivos pueden hacerte un DOS).

Preguntas frecuentes

1) ¿Maildir siempre es más seguro que mdbox?

No. Maildir suele tener un radio de impacto menor por corrupción de un solo archivo, pero puede fallar espectacularmente por extenuación de inodos y sobrecarga de metadatos. “Más seguro” depende de lo que sea más probable que estropees.

2) ¿mdbox siempre es más rápido?

No. mdbox puede reducir la sobrecarga de archivos pequeños, pero si tu cuello de botella es churn de índices, ajustes de sync o latencia de almacenamiento lenta, no lo arreglará mágicamente. También puede añadir complejidad durante reparaciones y restauraciones.

3) ¿Cuál es la mayor razón por la que los sistemas Maildir se vuelven lentos con el tiempo?

El crecimiento del recuento de archivos más las operaciones de metadatos. El sistema no se ralentiza linealmente; se ralentiza cuando los directorios se vuelven enormes, los backups comienzan a arrastrarse y el uso de inodos se acerca al precipicio.

4) ¿Cuál es la mayor razón por la que los sistemas mdbox se vuelven problemáticos?

La dependencia operativa en metadatos gestionados por Dovecot y la necesidad de copias consistentes. Si no snapshotteas, puedes restaurar un buzón que existe pero que no “tiene sentido” para Dovecot hasta que lo repares.

5) ¿Debería almacenar correo en NFS?

Sólo si entiendes la semántica de bloqueo del servidor/cliente NFS, las características de latencia y el comportamiento bajo carga. El almacenamiento de correo es intensivo en metadatos y sensible a picos de latencia. Muchos problemas “misteriosos” de correo son simplemente almacenamiento en red portándose mal.

6) ¿Puedo mezclar formatos en el mismo sistema?

Sí, y a veces es la respuesta pragmática: mantiene a la mayoría de usuarios en Maildir y mueve cuentas de alto volumen e intensivas en inodos a mdbox. Solo asegúrate de que tus herramientas operativas cubran ambos.

7) ¿Los snapshots reemplazan la replicación de Dovecot?

No. Los snapshots te ayudan a retroceder y restaurar. La replicación te ayuda a mantener disponibilidad. Resuelven problemas distintos y fallan de formas distintas.

8) ¿Cómo sé si es corrupción de índices o pérdida real de mensajes?

Compara la realidad del sistema de archivos con la vista IMAP. Si los mensajes existen en disco (archivos maildir o almacenamiento mdbox) pero no aparecen, reconstruye índices. Si no existen en disco, es pérdida y necesitas restauraciones.

9) ¿Cuál es el mantenimiento “mínimo viable” para una capa de almacenamiento Dovecot sana?

Backups basados en snapshot, ejercicios periódicos de restauración, monitoreo de uso de inodos y latencia de almacenamiento, y un runbook para reconstrucción/reparación de índices. Todo lo demás es optimización.

Próximos pasos que puedes hacer esta semana

  1. Ejecuta lo básico: doveconf -n, df -ih, iostat -x y doveadm mailbox status en un usuario caliente. Anota qué es realmente cierto.
  2. Verifica la consistencia de backups: Si no snapshotteas, trata tus backups como “copias a mejor esfuerzo”, no como restauraciones en las que te jugarías tu puesto.
  3. Haz un ensayo de restauración: Elige un buzón, restaúralo en un entorno aislado, reconstruye índices, valida IMAP. Mide el tiempo. Documenta el proceso.
  4. Identifica el precipicio de crecimiento: Inodos, carpetas enormes y latencia de almacenamiento son los tres grandes. Pon alertas en ellos.
  5. Decide con intención: Si tu dolor es inodos y sobrecarga de metadatos, mdbox podría ser el movimiento. Si tu dolor es recuperabilidad y simplicidad operativa, mantén Maildir y escala el sistema de archivos y el enfoque de backups correctamente.

Elige el formato que coincida con tu presupuesto de fallos y tu realidad de restauración. Tu yo del futuro será quien esté de guardia. No le hagas una broma pesada.