Estados P y C: qué hace tu CPU cuando está inactiva

El informe de incidente siempre empieza igual: “La CPU estaba al 12% y aun así la latencia aumentó.”
Miras los paneles, ves margen libre, y alguien sugiere “quizá la base de datos está lenta.”
Mientras tanto, el verdadero culpable es más silencioso: la CPU está ahorrando energía tan agresivamente que despertarla te cuesta milisegundos que no habías presupuestado.

Los estados P y C son las perillas y engranajes detrás de la “inactividad”. También explican por qué un servidor puede estar “mayormente inactivo”
y aun así sentirse como si caminara por molasses cuando llega la siguiente ráfaga de trabajo. Si gestionas sistemas en producción,
no necesitas convertirte en un microarquitecto de CPU, pero sí necesitas la alfabetización operativa suficiente para no pisar minas.

Un modelo mental apto para producción: P-states vs C-states

Piensa en tu CPU como si tuviera dos tipos de “modos”:

  • P-states (estados de rendimiento): la CPU está ejecutando, pero en distintos puntos de frecuencia/voltaje.
    Un P-state de mayor rendimiento suele significar mayor frecuencia y mayor consumo. Un P-state bajo significa más lento pero más barato.
  • C-states (estados de inactividad): la CPU no ejecuta instrucciones útiles.
    Cuanto más profundo es el C-state, más partes del núcleo (y a veces del paquete completo) pueden apagarse, ahorrando energía—pero despertarlo tarda más.

Esa es la versión simple. La versión operativa añade dos notas al pie que deberías tatuar en tus runbooks:

  • Los P-states no son una perilla “configurar y olvidar”. El SO, el firmware y la propia CPU pueden influir en la selección de frecuencia.
    En sistemas Intel modernos, el hardware puede tomar gran parte de las decisiones incluso si crees que Linux manda.
  • Los C-states pueden ser por núcleo y por paquete. Un núcleo ruidoso vecino puede impedir que todo el socket alcance un sueño profundo de paquete.
    A la inversa, un sistema “silencioso” puede entrar tan profundo en sueño que la siguiente petición pague una penalización de despertado fea.

Un atajo mental más: los P-states tratan sobre “qué tan rápido mientras trabaja”. Los C-states tratan sobre “qué tan dormido mientras espera”.
La mayoría de incidentes ocurren cuando optimizas uno y te olvidas del otro.

Qué hace realmente la CPU cuando está “inactiva”

Cuando Linux no tiene nada ejecutable para una CPU, ejecuta un bucle de idle. El bucle de inactividad no es solo girar
(a menos que lo fuerces). Normalmente emite una instrucción como HLT (halt) o usa mecanismos más avanzados
que permiten a la CPU entrar en estados de sueño más profundos.

Estados C por núcleo: C0 hasta “lo suficientemente profundo como para molestarte”

C0 significa “activo”. Todo lo demás es alguna variante de “no ejecutar instrucciones”. El mapeo exacto difiere entre proveedores,
pero el patrón operativo es consistente:

  • C1: sueño ligero. Salida rápida. Ahorro de energía mínimo.
  • C1E: C1 mejorado; a menudo reduce el voltaje con más agresividad.
  • C3: sueño más profundo; más relojes internos desconectados; mayor latencia de salida.
  • C6/C7 y compañía: sueño muy profundo; puede vaciar cachés, apagar partes del núcleo; la latencia de salida puede ser apreciable.

La latencia de salida es el impuesto oculto. Si tu carga es explosiva y sensible a la latencia, los C-states profundos pueden convertir
“mayormente inactivo” en “periódicamente lento”.

Estados C de paquete: todo el socket hace la siesta

Los estados C de paquete (a menudo etiquetados PC2/PC3/PC6/PC10) son donde están los grandes ahorros de energía.
También son donde viven las sorpresas. El paquete solo puede profundizar si se cumplen condiciones:

  • Todos los núcleos están lo suficientemente inactivos.
  • Los componentes uncore (LLC, controlador de memoria, interconexión) pueden reducir reloj/potencia.
  • Los dispositivos y el firmware están de acuerdo en que es seguro.

En entornos de servidores, una única fuente de interrupciones ruidosa, el tick del temporizador o una política de energía mal configurada puede bloquear los estados profundos de paquete.
O al contrario: se permite un sueño profundo de paquete y tu latencia tail empieza a hacer malabarismos interpretativos.

P-states: la selección de frecuencia ya no es una sola perilla

La historia antigua era: el SO selecciona una frecuencia de una tabla; la CPU la usa. La historia moderna es: el SO establece políticas y sugerencias,
y la lógica interna de la CPU a menudo hace los bucles de control rápidos. El controlador intel_pstate de Intel, CPPC de AMD,
y los P-states gestionados por hardware difuminan la línea entre “gobernador” y “firmware”.

El turbo complica aún más las cosas. Turbo no es “una frecuencia”. Es un conjunto de comportamientos de impulso oportunista
limitados por energía, temperatura, corriente y cuántos núcleos están activos. Tu monitor puede decir “3.5 GHz”
mientras la CPU hace impulsos por núcleo que varían microsegundo a microsegundo.

Broma #1: Si alguna vez quieres sentirte impotente, intenta discutir con una CPU sobre lo que significa “frecuencia máxima”.

Por qué debes preocuparte: latencia, jitter, rendimiento y costes

Latencia y latencia tail

Los C-states profundos añaden latencia de despertado. La escalada de frecuencia añade latencia de rampa. Normalmente esto es de microsegundos a milisegundos bajos,
lo que suena pequeño hasta que estás ejecutando:

  • Servicios RPC con SLO estrictos (p99 importa, no la media)
  • backends de almacenamiento donde el tiempo de finalización de IO es visible para el usuario
  • bases de datos con contención de bloqueos donde pequeños retrasos se magnifican
  • sistemas de trading de baja latencia donde el jitter es un riesgo profesional

En otras palabras: si tu sistema está “inactivo la mayor parte del tiempo” pero debe responder rápido cuando no lo está, debes preocuparte.

Rendimiento sostenido y throughput

Los P-states y el turbo deciden cuánta carga haces por vatio. Pero el turbo está limitado por umbrales de energía (PL1/PL2 en Intel),
térmica y restricciones de plataforma. Si fuerzas “modo rendimiento” en todas partes, podrías ganar en pruebas cortas y perder rendimiento sostenido
porque alcanzas límites de energía/temperatura y te estrangulan fuerte.

Consumo, refrigeración y dinero real

Si operas a escala, la política de energía de la CPU cambia la historia del centro de datos: electricidad, refrigeración y densidad por rack.
Incluso si no pagas la factura de energía directamente, la pagarás en planificación de capacidad.

Aquí la verdad cínica de SRE: no puedes gastar tu camino fuera del jitter de latencia si la configuración de tu flota es inconsistente.
Y no puedes afinar tu camino fuera de un presupuesto energético si tu aplicación está girando sin hacer nada.

Datos interesantes y breve historia (porque este lío tiene raíces)

  • ACPI estandarizó los estados de energía para que los sistemas operativos gestionaran energía entre distintos fabricantes en lugar de interfaces BIOS a medida.
  • Las CPUs de la era “SpeedStep” pusieron la escalada de frecuencia en el mainstream; antes de eso, “gestión de energía” era sobre todo “apagar la pantalla”.
  • El comportamiento turbo moderno está limitado por energía, no por frecuencia: las CPUs persiguen envolventes de energía y temperatura, no un reloj fijo.
  • Los C-states son anteriores a la nube, pero la nube volvió dolorosos sus trade-offs: las cargas multiinquilino son explosivas e impredecibles.
  • Los kernels sin tick (NO_HZ) redujeron las interrupciones periódicas del temporizador para que las CPUs pudieran permanecer inactivas más tiempo y alcanzar C-states más profundos.
  • Intel introdujo control P-state gestionado por hardware para reaccionar más rápido que el bucle del planificador del SO.
  • RAPL (Running Average Power Limit) dio a software una forma de medir/limitar energía de CPU, convirtiendo la energía en métrica de primera clase.
  • Los C-states de paquete se volvieron importantes a medida que la potencia del uncore (LLC, controlador de memoria, interconexión) rivalizaba con la del núcleo.
  • La virtualización complicó todo: la “inactividad” de un guest no es necesariamente inactividad del host; halting en una VM implica política del hipervisor.

Cómo controla Linux los P-states y C-states

El plano de control: drivers, gobernadores y políticas

En Linux, la escalada de frecuencia de CPU suele gestionarse por el subsistema cpufreq. Dos controladores comunes que verás:

  • intel_pstate (Intel): a menudo el predeterminado en Intel modernos. Puede operar en “modo activo” donde la CPU participa mucho en las decisiones.
  • acpi-cpufreq: controlador ACPI más tradicional con tablas de frecuencias explícitas.

Los gobernadores son políticas como performance, powersave, y (según el driver) schedutil.
No trates los nombres de gobernador como verdades universales; su comportamiento puede variar según el controlador.

El plano de inactividad: cpuidle, controladores C-state y restricciones de latencia

Los C-states en Linux se gestionan por el subsistema cpuidle. Selecciona un estado de inactividad basado en:

  • duración prevista de inactividad (cuánto tiempo hasta que un evento despierte la CPU)
  • latencia de salida de cada estado de inactividad
  • restricciones QoS (pistas de sensibilidad a latencia del kernel/usuarios)
  • lo que la plataforma y el firmware permiten

BIOS/UEFI: el lugar donde “solo cambiaremos una opción” se convierte en folklore

Las configuraciones de firmware pueden sobrescribir o restringir todo:

  • Máximo C-state permitido (por ejemplo, limitar a C1)
  • Límites de C-state de paquete
  • Habilitar/deshabilitar turbo
  • Energy/Performance Bias (EPB de Intel)
  • “Perfiles de energía” del proveedor que hacen varias cosas a la vez

En producción, el modo de fallo más común no es “kernel equivocado”. Es “valores predeterminados de BIOS diferentes entre lotes”.

Una cita sobre fiabilidad (idea parafraseada)

Idea parafraseada atribuida a John Ousterhout: la complejidad es la causa raíz de muchos problemas de fiabilidad.
La gestión de energía es complejidad con un vatímetro.

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

El único ajuste que importa es el ajuste que puedes verificar. A continuación están las tareas reales que espero que un ingeniero on-call ejecute,
con comandos, salidas de ejemplo y qué decisión tomar a partir de ellas.

Tarea 1: Identificar el driver de frecuencia activo y los gobernadores

cr0x@server:~$ cpupower frequency-info
analyzing CPU 0:
  driver: intel_pstate
  CPUs which run at the same hardware frequency: 0
  available cpufreq governors: performance powersave
  current policy: frequency should be within 800 MHz and 3900 MHz.
                  The governor "powersave" may decide which speed to use
  current CPU frequency: 1200 MHz (asserted by call to hardware)
  boost state support:
    Supported: yes
    Active: yes

Qué significa: Estás usando intel_pstate; las opciones de gobernador son limitadas y el comportamiento es específico del driver.
La frecuencia actual es baja porque la política lo permite. El turbo está habilitado.

Decisión: Si estás diagnosticando picos de latencia, ten en cuenta que powersave bajo intel_pstate aún puede impulsar,
pero las características de rampa difieren. No cambies gobernadores a ciegas; mide primero.

Tarea 2: Comprobar política de frecuencia mínima/máxima por CPU

cr0x@server:~$ for f in /sys/devices/system/cpu/cpu*/cpufreq/scaling_{min,max}_freq; do echo "$f: $(cat $f)"; done | head
/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq: 800000
/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq: 3900000
/sys/devices/system/cpu/cpu1/cpufreq/scaling_min_freq: 800000
/sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq: 3900000

Qué significa: Los límites de política del SO son amplios. Si el rendimiento sigue siendo pobre, la limitación está en otra parte
(límites de energía, térmica, C-states, contención).

Decisión: Si scaling_max_freq está inesperadamente bajo, sospecha un perfil de afinamiento, restricciones del runtime de contenedores,
o eventos de límite de potencia de la plataforma.

Tarea 3: Inspeccionar estado de turbo/boost

cr0x@server:~$ cat /sys/devices/system/cpu/cpufreq/boost
1

Qué significa: Turbo/boost está habilitado.

Decisión: Para servicios sensibles a latencia, el turbo suele ayudar (tiempo de servicio más rápido).
Para latencia determinista, el turbo puede añadir variabilidad térmica; considera bloquear la política solo tras medir.

Tarea 4: Verificar disponibilidad y residencia de estados de inactividad de CPU (C-states por núcleo)

cr0x@server:~$ sudo cpupower idle-info
CPUidle driver: intel_idle
CPUidle governor: menu
analyzing CPU 0:
  Number of idle states: 4
  Available idle states: POLL C1 C1E C6
  C1: exit latency 2 us
  C1E: exit latency 10 us
  C6: exit latency 85 us

Qué significa: Existe un C6 profundo con ~85 µs de latencia de salida (ejemplo). No es catastrófico, pero no es gratis.

Decisión: Si tus p99 se correlacionan con periodos de inactividad, considera limitar el C-state más profundo solo en nodos afectados y vuelve a probar.

Tarea 5: Comprobar tiempo por estado y contadores de uso de estados de inactividad

cr0x@server:~$ for s in /sys/devices/system/cpu/cpu0/cpuidle/state*; do \
  echo "$(basename $s) name=$(cat $s/name) disable=$(cat $s/disable) time=$(cat $s/time) usage=$(cat $s/usage)"; \
done
state0 name=POLL disable=0 time=122 usage=18
state1 name=C1 disable=0 time=983421 usage=24011
state2 name=C1E disable=0 time=221934 usage=9120
state3 name=C6 disable=0 time=55290321 usage=110432

Qué significa: CPU0 pasa mucho tiempo en C6. Eso es bueno para la energía. Puede ser malo para la latencia de despertado.

Decisión: Si ves latencia tail, este es candidato principal. A continuación, correlaciona con métricas de la aplicación e interrupciones.

Tarea 6: Comprobar residencia de C-state de paquete (Intel, vía turbostat)

cr0x@server:~$ sudo turbostat --Summary --quiet --show PkgWatt,PkgTmp,Pkg%pc2,Pkg%pc6,Pkg%pc10 --interval 1 --num_iterations 3
PkgWatt  PkgTmp  Pkg%pc2  Pkg%pc6  Pkg%pc10
  32.15     54      2.12     8.41     61.77
  28.02     52      1.88     7.96     68.10
  35.44     55      2.30     9.02     58.33

Qué significa: El paquete frecuentemente alcanza PC10 (sueño profundo). Potencia baja. Excelente para eficiencia.
También causa clásico “cold-start latency” al despertar.

Decisión: Si ejecutas cargas de baja latencia, considera limitar los C-states de paquete o usar un perfil tuned de baja latencia en esos nodos.
Si ejecutas trabajos por lotes, celebra y sigue adelante.

Tarea 7: Buscar señales de límite de potencia y throttling (Intel RAPL / térmica)

cr0x@server:~$ sudo turbostat --quiet --show Bzy_MHz,Avg_MHz,Busy%,CoreTmp,PkgTmp,PkgWatt,CorWatt,GFXWatt --interval 1 --num_iterations 2
Bzy_MHz  Avg_MHz  Busy%  CoreTmp  PkgTmp  PkgWatt  CorWatt  GFXWatt
   4200     1850  22.15       72      79    165.2     92.1     0.0
   4100     1902  23.40       74      81    165.0     93.0     0.0

Qué significa: Existen clocks de boost, pero la potencia del paquete es alta. Si las temperaturas suben, puedes estrangular pronto.

Decisión: Si el rendimiento es inconsistente bajo carga, inspecciona refrigeración, límites de potencia y comportamiento sostenido de turbo antes de culpar al kernel.

Tarea 8: Confirmar modo de tick del kernel y comportamiento de temporizadores (disrupción de inactividad)

cr0x@server:~$ grep -E 'NO_HZ|CONFIG_HZ' -n /boot/config-$(uname -r) | head -n 5
114:CONFIG_HZ=250
501:CONFIG_NO_HZ_COMMON=y
504:CONFIG_NO_HZ_IDLE=y
507:CONFIG_NO_HZ_FULL is not set

Qué significa: El idle sin ticks está activado (NO_HZ_IDLE), lo que ayuda a los C-states profundos. No es totalmente sin ticks.

Decisión: Si tu carga necesita latencia consistente baja, puedes preferir menos transiciones profundas de inactividad (política), no necesariamente cambiar la configuración del kernel.

Tarea 9: Identificar hotspots de interrupciones que impiden inactividad o causan tormentas de wake

cr0x@server:~$ sudo cat /proc/interrupts | head -n 15
           CPU0       CPU1       CPU2       CPU3
  0:         21         18         19         22   IO-APIC   2-edge      timer
  1:          0          0          0          0   IO-APIC   1-edge      i8042
 24:     883421     102331      99321      90122   PCI-MSI  327680-edge  eth0-TxRx-0
 25:     112331     843221     121112     110998   PCI-MSI  327681-edge  eth0-TxRx-1

Qué significa: Las colas de NIC están pesadas en ciertas CPUs. Esto puede impedir que los núcleos entren en inactividad y también causar despertados explosivos.

Decisión: Considera ajustar la afinidad de IRQ (o el comportamiento de irqbalance) si ves hotspots en un solo núcleo o picos de latencia alineados a interrupciones.

Tarea 10: Comprobar el estado de irqbalance y si está luchando con tu afinamiento

cr0x@server:~$ systemctl status irqbalance --no-pager
● irqbalance.service - irqbalance daemon
     Loaded: loaded (/lib/systemd/system/irqbalance.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-01-10 08:21:10 UTC; 2h 12min ago
       Docs: man:irqbalance(1)
   Main PID: 912 (irqbalance)

Qué significa: irqbalance está activo. Buen valor por defecto—a menos que hagas pinning manual de IRQs para baja latencia y olvides desactivarlo.

Decisión: Si necesitas aislamiento estricto de CPU, configura irqbalance con CPUs prohibidas o desactívalo y gestiona afinidades explícitamente.

Tarea 11: Ver si un perfil tuned está imponiendo ahorro agresivo de energía

cr0x@server:~$ tuned-adm active
Current active profile: virtual-guest

Qué significa: Un perfil tuned está activo, potencialmente alterando el gobernador de CPU y otros parámetros relevantes para latencia.

Decisión: Para un host de base de datos o servicio RPC sensible a latencia, prueba latency-performance (o el perfil recomendado por el proveedor) en un nodo canario.

Tarea 12: Ver rápidamente el gobernador actual en todas las CPUs

cr0x@server:~$ grep -H . /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null | head
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu1/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu2/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor:powersave

Qué significa: Todas las CPUs están en powersave.

Decisión: Si persigues regresiones de latencia, cambia un host a performance temporalmente y mide p99. No lo apliques a toda la flota por corazonadas.

Tarea 13: Cambiar temporalmente el gobernador (y entender el riesgo)

cr0x@server:~$ sudo cpupower frequency-set -g performance
Setting cpu: 0
Setting cpu: 1
Setting cpu: 2
Setting cpu: 3

Qué significa: Has solicitado el gobernador de rendimiento. En intel_pstate, esto cambia el comportamiento de la política, no fija un reloj.

Decisión: Usa esto como experimento controlado. Si la latencia mejora materialmente y el presupuesto de energía lo permite, considera un perfil tuned en lugar de cambios ad-hoc.

Tarea 14: Limitar el C-state más profundo (prueba quirúrgica, no modo de vida)

cr0x@server:~$ echo 1 | sudo tee /sys/devices/system/cpu/cpu0/cpuidle/state3/disable
1

Qué significa: Has deshabilitado un estado de inactividad (aquí, state3 podría ser C6). Esto fuerza un sueño más superficial en CPU0.

Decisión: Si p99 mejora y el consumo sube aceptablemente, aplícalo mediante un método persistente (args del kernel, tuned, o unidad systemd) y documéntalo.

Tarea 15: Comprobar efectos de virtualización: ¿estás afinando al invitado mientras el host decide?

cr0x@server:~$ systemd-detect-virt
kvm

Qué significa: Estás dentro de una VM. Los controles de energía del invitado pueden tener efecto limitado; la política del host y el planificador del hipervisor importan más.

Decisión: Si necesitas comportamiento de baja latencia, trabaja con el equipo de plataforma: pinning de CPU, gobernador del host y política de C-states son las palancas reales.

Tarea 16: Comprobar presión de CPU y contención en el scheduling (porque “inactivo” puede ser mentira)

cr0x@server:~$ cat /proc/pressure/cpu
some avg10=0.00 avg60=0.10 avg300=0.08 total=18873412
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

Qué significa: La presión de CPU es baja; el planificador no está luchando. Si la latencia es mala, céntrate en el comportamiento de wake/sleep, interrupciones, IO o contención de locks.

Decisión: Si some o full está alto, no persigas primero los C-states—arregla la contención, límites de CPU o vecinos ruidosos.

Guía rápida de diagnóstico

Este es el orden que uso cuando alguien dice “la latencia sube cuando la máquina está mayormente inactiva” o “la CPU está baja pero las cosas van lentas.”
Está optimizado para encontrar el cuello de botella rápido, no para hacerte sentir ingenioso.

Primero: decide si persigues comportamiento de energía de la CPU o otra cosa

  1. Comprobar presión de CPU (contención del planificador):
    si PSI está alto, no estás “inactivo”, estás sobresuscrito o estrangulado.
  2. Comprobar cola de ejecución y tiempo de steal (especialmente en VMs):
    baja utilización puede coexistir con alta latencia si estás esperando ser programado.
  3. Comprobar iowait y latencia de almacenamiento:
    mucha “CPU inactiva” en realidad es “bloqueado en IO”.

Segundo: confirma qué política de energía está activa

  1. Driver + gobernador vía cpupower frequency-info.
  2. ¿Turbo activado? vía /sys/devices/system/cpu/cpufreq/boost.
  3. Perfil tuned o servicio del proveedor que imponga política.

Tercero: mide residencia de C-states y perturbaciones de wake

  1. Uso/tiempo de C-states de núcleo vía /sys/.../cpuidle o cpupower idle-info.
  2. C-states de paquete vía turbostat (si está disponible).
  3. Hotspots de interrupciones vía /proc/interrupts y herramientas de afinidad IRQ.

Haz un cambio a la vez, en un nodo, con un temporizador

La forma más rápida de perder una semana es alternar ajustes de BIOS, parámetros del kernel y perfiles tuned en la misma ventana de mantenimiento.
Cambia una cosa, mide p95/p99 y potencia/térmicas, luego decide.

Tres micro-historias del mundo corporativo

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

Un equipo desplegó una nueva pasarela API interna “ligera”. Era eficiente: CPU baja, ráfagas cortas, mucho uso de interrupciones de red.
En los paneles, la CPU rondaba 15–25%. Todos se felicitaron por no sobredimensionar.
Luego la latencia p99 se duplicó fuera de horas pico, justo cuando el tráfico se calmó.

La primera suposición fue clásica: “menos carga significa más margen”. Pero el servicio era explosivo.
Durante periodos tranquilos, las CPUs entraban en C-states profundos de paquete. Cuando llegaba la siguiente ráfaga, el manejo de peticiones pagaba latencia de despertado
más latencia de rampa de frecuencia. Individualmente pequeño, colectivamente feo.

La segunda suposición: “Estamos en governor performance, así que la frecuencia es alta.” No lo estaban.
La mitad de la flota tenía un perfil de BIOS diferente. Esos hosts permitían estados de paquete más profundos y tenían una política más sesgada a la energía.
La flota era heterogénea y el balanceador mezclaba hosts con distintos comportamientos de wake.

La solución no fue heroica. Estandarizaron perfiles de firmware, luego probaron en canario un perfil tuned de baja latencia solo en nodos de gateway.
Mantuvieron el sueño profundo habilitado para workers por lotes. La latencia se estabilizó, la energía siguió razonable, y el incidente acabó con una breve lista de verificación añadida al aprovisionamiento.

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

Un equipo de almacenamiento quería reducir consumo. Forzaron C-states más profundos y pusieron gobernadores en powersave en nodos de almacenamiento.
En teoría era responsable: los nodos esperaban mucho IO y las CPUs no parecían ocupadas.

Lo que no vieron fue cómo se comporta el almacenamiento bajo cargas mixtas. Las rutas de finalización de IO son sensibles a latencia y están impulsadas por interrupciones.
El servicio permanecía quieto y de repente procesaba una tormenta de finalizaciones, trabajo de checksum y respuestas de red.
Con C-states profundos, la latencia del manejador desde la interrupción creció. Con escalado agresivo de frecuencia, los núcleos empezaban lentos y luego subían.

El contratiempo mostró un síntoma extraño: la latencia promedio seguía aceptable, pero la latencia tail y el jitter se volvieron brutales.
Los clientes reintentaron. Los reintentos causaron microráfagas. Las microráfagas hicieron que las CPUs rebotaran entre sueño y despertar aún más.
El cambio “ahorro de energía” creó un bucle que desperdició tanto energía como tiempo.

Revirtieron en los frontends de almacenamiento pero mantuvieron el ahorro en nodos de compactación en background.
La lección real fue el alcance: la política de energía es específica por carga. Aplícala por rol, no por cluster, y siempre vigila p99 y tasas de reintento.

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

Otra compañía ejecutaba una flota Kubernetes con generaciones mixtas de instancias. Su equipo de plataforma hizo algo profundamente no sexy:
mantuvieron una matriz de capacidades hardware y una prueba de aprovisionamiento que registraba disponibilidad de C-states, estado de turbo y residencia de inactividad.
Cada nueva actualización de BIOS tenía que pasar las mismas pruebas antes de entrar en la imagen dorada.

Un trimestre, una actualización de firmware del proveedor cambió los límites predeterminados de C-state de paquete.
Nada explotó inmediatamente. Ese es el truco—este tipo de cambio no siempre rompe las cosas ruidosamente. Solo altera las características de latencia.

Sus pruebas lo detectaron porque la residencia de C-states de paquete registrada cambió significativamente en nodos inactivos.
No necesitaron que un cliente se quejara primero. Pausaron el despliegue, ajustaron la política de firmware y documentaron la diferencia.

El resultado fue aburrido: ningún incidente. El equipo de plataforma no recibió elogios.
Pero los equipos de aplicación nunca tuvieron que aprender sobre C-states de paquete a las 3 a.m., que es la forma más alta de éxito operativo.

Broma #2: El mejor cambio en gestión de energía es el que nunca aparece en una diapositiva de postmortem.

Errores comunes (síntomas → causa raíz → solución)

1) “La CPU está baja pero la latencia p99 es alta”

Síntomas: baja utilización promedio de CPU; grandes picos de latencia tail durante periodos tranquilos; mejor latencia bajo carga sostenida.

Causa raíz: C-states profundos y/o escalado de frecuencia agresivo causan penalizaciones de despertado y rampa; el tráfico explosivo desencadena transiciones repetidas.

Solución: mide residencia de C-states (núcleo y paquete). Prueba en canario un perfil de baja latencia o limita los C-states más profundos en nodos afectados.
Confirma la consistencia de perfiles de BIOS en la flota.

2) “Frecuencia atrapada baja incluso bajo carga”

Síntomas: cpupower frequency-info muestra frecuencia actual baja; rendimiento por debajo de lo esperado; temperaturas moderadas.

Causa raíz: scaling_max_freq limitado por política, interacciones con cuota de CPU en contenedores, o límites de potencia de la plataforma.

Solución: comprueba scaling_max_freq y perfil tuned; confirma turbo; inspecciona límites de potencia/throttling con turbostat.
En contenedores, verifica cuota de CPU y asignaciones de cpuset.

3) “El rendimiento mejoró en un nodo pero no en otro”

Síntomas: mismo software, latencia distinta; cambios de afinamiento funcionan de forma inconsistente entre hosts.

Causa raíz: valores predeterminados de firmware heterogéneos, diferencias de microcódigo, o distintos drivers de frecuencia (intel_pstate vs acpi-cpufreq).

Solución: estandariza configuraciones de BIOS; asegura parámetros del kernel consistentes; inventaría selección de driver y versiones de microcódigo.

4) “Tormentas de IRQ impiden inactividad y desperdician energía”

Síntomas: el paquete nunca alcanza C-states profundos; vatios en inactividad más altos; ciertas CPUs muestran conteos enormes de interrupciones.

Causa raíz: desequilibrio de afinidad de interrupciones, colas NIC mal configuradas, dispositivos ruidosos o comportamiento del temporizador.

Solución: inspecciona /proc/interrupts; ajusta afinidad de IRQ; configura correctamente el número de colas; revisa la configuración de irqbalance.

5) “Deshabilitamos C-states y ahora el throughput empeoró”

Síntomas: aumento del consumo; aparece throttling térmico; rendimiento sostenido baja después de un pequeño pico inicial.

Causa raíz: eliminar ahorros de inactividad eleva temperatura/potencia base, reduciendo cabeza de turbo y provocando estrangulamiento.

Solución: no deshabilites estados profundos indiscriminadamente. Usa políticas por rol. Monitoriza térmicas y potencia de paquete; busca estabilidad, no relojes máximos.

6) “Hemos fijado CPUs, pero la latencia aún jitterea”

Síntomas: aislamiento de CPU configurado; aún ves jitter; colas largas ocasionales.

Causa raíz: la gestión de energía aún hace transiciones de núcleo/paquete; interrupciones caen sobre CPUs aisladas; contención entre siblings de SMT.

Solución: alinea afinidad de IRQ con aislamiento; considera limitar C-states profundos para núcleos aislados; revisa la política SMT para cargas críticas de latencia.

Listas de verificación / plan paso a paso

Checklist A: Estandarizar una línea base de energía para la flota (la parte aburrida que previene sorpresas)

  1. Inventaria modelos de CPU, versiones de microcódigo y estado de virtualización en los nodos.
  2. Registra el driver de frecuencia (intel_pstate/acpi-cpufreq) y gobernadores en la gestión de configuración.
  3. Registra ajustes de perfil de BIOS/UEFI (límites C-state, turbo, EPB) por generación de hardware.
  4. Define políticas por rol: crítico-latencia, equilibrado, batch/eficiencia.
  5. Aplica perfiles tuned o equivalentes vía automatización; no hagas ajustes manuales aislados.
  6. Muestra continuamente la residencia de C-states de paquete y vatios en canarios inactivos para detectar desviaciones tras actualizaciones de firmware.

Checklist B: Afinar un nodo de servicio sensible a latencia (de forma segura)

  1. Base: captura p95/p99 de latencia, tasa de errores/reintentos y consumo en reposo y bajo ráfaga típica.
  2. Confirma que no es contención de CPU: revisa PSI CPU y colas de ejecución.
  3. Mide residencia de C-states de paquete en reposo y durante ráfagas.
  4. Cambio en canario: cambia el perfil tuned o gobernador en un nodo.
  5. Si sigue habiendo picos, prueba limitar el C-state más profundo (temporal) y vuelve a medir.
  6. Valida térmicas y límites de potencia sostenida; vigila estrangulamiento.
  7. Despliega por rol, no por flota. Documenta la política con “por qué”, no solo “qué”.

Checklist C: Afinar un nodo batch orientado a eficiencia

  1. Confirma que la carga prioriza throughput y tolera jitter.
  2. Habilita/permite C-states de paquete profundos y gobernadores balanceados.
  3. Vigila tormentas de interrupciones que mantengan el paquete despierto (vatios desperdiciados).
  4. Monitorea energía por trabajo (o por GB procesado), no solo tiempo de ejecución.

Preguntas frecuentes

1) ¿Los P-states y C-states son independientes?

Mayormente, pero no totalmente. Los C-states gobiernan qué pasa en inactividad; los P-states gobiernan rendimiento activo.
En la práctica interactúan vía térmicas y límites de potencia: un inactivo profundo puede mejorar la cabeza de turbo, y deshabilitar inactividad puede reducir impulso sostenido.

2) ¿Debería usar siempre el gobernador performance en servidores?

No. Para frontends sensibles a latencia puede ayudar. Para flotas batch suele ser un desperdicio.
Además, en intel_pstate, performance no significa “reloj máximo fijo”. Significa una política más agresiva.
Toma decisiones por rol y mide p99 y vatios.

3) Si los C-states añaden latencia, ¿por qué no deshabilitarlos en todas partes?

Porque lo pagarás en consumo, calor y a veces en throttling—además de menor cabeza de turbo.
Deshabilitar C-states profundos puede ser una herramienta dirigida para roles específicos. Rara vez es buen predeterminado para toda la flota.

4) ¿Por qué la latencia mejora bajo carga sostenida?

Bajo carga sostenida, los núcleos permanecen en C0 y las frecuencias se estabilizan en niveles más altos. Evitas costos de despertado y rampa.
Las cargas explosivas pagan esos costos repetidamente, y la latencia tail sufre.

5) ¿Cómo sé si el SO o el hardware controla la frecuencia?

Empieza con cpupower frequency-info para ver el driver. En Intel moderno, intel_pstate en modo activo significa que el hardware juega un rol importante.
También fíjate si la frecuencia actual aparece con “asserted by call to hardware” en la salida.

6) ¿La virtualización cambia la historia?

Sí. La inactividad de un guest está mediada por el hipervisor. La frecuencia y el sueño profundo de paquete suelen estar controlados por el host.
Si ajustas dentro de una VM y no ves resultados, no es mala suerte; es que no manejas las palancas correctas.

7) ¿Cuál es la diferencia operativa entre C-states por núcleo y por paquete?

Los C-states por núcleo afectan la profundidad de sueño y latencia de despertado de un solo núcleo. Los estados de paquete afectan componentes a nivel socket y pueden ahorrar mucha más energía.
Los estados de paquete también pueden causar penalizaciones más notables en la “primera petición después de inactividad”, según la plataforma.

8) ¿Puede el afinamiento de interrupciones arreglar latencia relacionada con C-states?

A veces. Si las interrupciones despiertan constantemente núcleos inactivos, verás desperdicio de energía y jitter.
Si las interrupciones se concentran en pocas CPUs, esas CPUs pueden nunca dormir mientras otras van profundo, creando comportamiento de respuesta desigual.
Balancear o fijar interrupciones correctamente puede estabilizar la latencia.

9) ¿Cómo decido entre modos “baja latencia” y “energía eficiente”?

Usa el SLO y el modelo de costes de la carga. Si tienes objetivos p99 estrictos y tráfico explosivo, sesga hacia baja latencia en esos nodos.
Si tienes trabajos batch o colas elásticas, sesga hacia eficiencia. Evita mezclar políticas en el mismo pool detrás de un balanceador.

10) ¿Cuál es el primer experimento seguro si sospecho de C-states?

Canarya un solo nodo: captura la línea base, luego cambia a un perfil tuned de baja latencia o limita temporalmente el estado de inactividad más profundo.
Si p99 mejora sin provocar throttling o aumento inaceptable de consumo, has probado la causalidad.

Conclusión: pasos prácticos siguientes

“Inactivo” no es un estado neutral. Es una decisión de política activa tomada por capas de silicio, firmware y código del kernel—cada una intentando ahorrar energía,
ocasionalmente robándote presupuesto de latencia en el proceso.

Pasos siguientes que realmente aguantan en producción:

  1. Mide antes de afinar: recoge driver/gobernador, residencia de C-states y latencia p95/p99 en un nodo canario.
  2. Estandariza la política de firmware: los valores predeterminados de BIOS inconsistentes son un asesino silencioso de flotas.
  3. Separa por rol: nodos de baja latencia y nodos eficientes no deben compartir la misma política de energía.
  4. Haz los cambios reversibles: toggles mediante perfiles tuned o gestión de configuración, no sesiones SSH artesanales.
  5. Vigila la latencia tail y reintentos: las medias te mentirán con toda tranquilidad.

Caché de CPU (L1/L2/L3) en términos sencillos: por qué la memoria gana

Tu servicio está “ligado a CPU”. Los paneles lo dicen. El CPU está al 80–90%, la latencia es fea, y la primera reacción del equipo es añadir núcleos.
Entonces añades núcleos y nada mejora. O empeora. Felicidades: acabas de encontrarte con el verdadero jefe—la memoria.

Las cachés de CPU (L1/L2/L3) existen porque los CPUs modernos pueden hacer aritmética más rápido de lo que el sistema puede suministrarles datos. La mayoría de las fallas de rendimiento en producción
no son “el CPU es lento”. Son “el CPU está esperando”. Este texto explica las cachés sin palabrería infantil y luego muestra cómo probar lo que está ocurriendo
en una máquina Linux real con comandos que puedes ejecutar hoy.

Por qué la memoria gana (y el CPU mayormente espera)

Los CPUs son una locura. Un núcleo moderno puede ejecutar múltiples instrucciones por ciclo, especular, reordenar, vectorizar y en general actuar como un contable hiperactivo
haciendo impuestos a las 4 a.m. Mientras tanto, la DRAM es comparativamente lenta. El núcleo puede retirar instrucciones en sub-nanosegundos; un viaje a DRAM puede tomar
decenas a cientos de nanosegundos dependiendo de la topología, la contención y si te has internado en NUMA remota.

La consecuencia práctica: tu CPU pasa mucho tiempo parado esperando cargas desde memoria. No es disco. No es red. Ni siquiera “código lento” en el sentido habitual.
Está esperando la siguiente línea de caché.

Las cachés intentan mantener al CPU ocupado manteniendo los datos usados con frecuencia cerca. No son “agradables de tener”. Son la única razón por la que la computación de propósito general
funciona a los ritmos de reloj actuales. Si cada carga golpeara DRAM, tus núcleos pasarían la mayoría de los ciclos torturándose.

Aquí está el modelo mental que sobrevive al contacto con producción: el rendimiento está dominado por qué tan a menudo fallas las cachés y
qué costosas son esas fallas. Las fallas más caras son las que escapan del paquete del chip y van a DRAM, y las realmente picantes son las DRAM NUMA remotas accedidas sobre un interconector mientras otros núcleos pelean por el ancho de banda.

Una regla empírica: cuando tu ruta de solicitud toca “muchas cosas”, el costo no es la aritmética; es el seguimiento de punteros y las fallas de caché.
Y si lo haces concurrentemente con muchos hilos, puedes convertir tu subsistema de memoria en el cuello de botella mientras los gráficos de CPU te mienten en la cara.

L1/L2/L3 en términos sencillos

Piensa en los niveles de caché como “despensas” cada vez más grandes y cada vez más lentas entre el núcleo y la DRAM.
El nombre es histórico y simple: L1 está más cerca del núcleo, L2 es la siguiente, L3 suele ser compartida entre núcleos en un socket (no siempre), y luego DRAM.

Para qué sirve cada nivel

  • Caché L1: pequeña y extremadamente rápida. A menudo dividida en L1i (instrucciones) y L1d (datos). Es el primer lugar donde el núcleo busca.
  • Caché L2: más grande, un poco más lenta, típicamente privada por núcleo. Atrapa lo que sale de L1.
  • Caché L3: mucho más grande, más lenta, frecuentemente compartida entre núcleos. Reduce viajes a DRAM y actúa como amortiguador para la contención.

Qué significan operativamente “hit” y “miss”

Un hit en caché significa que los datos que necesitas ya están cerca; la carga se satisface rápido y la tubería sigue avanzando.
Un miss de caché significa que el CPU debe obtener esos datos de un nivel inferior. Si la falta llega a DRAM, el núcleo puede quedarse muy estancado.

Las fallas ocurren porque las cachés son finitas y porque las cargas reales tienen patrones de acceso desordenados. El CPU intenta predecir y prefetch, pero no puede predecir
todo—especialmente código con punteros, acceso aleatorio o estructuras de datos más grandes que la caché.

Por qué no puedes “simplemente usar L3”

A veces la gente habla como si L3 fuera una piscina mágica compartida que contendrá tu conjunto de trabajo. No lo es. L3 es compartida, contienda y a menudo inclusiva o parcialmente
inclusiva según la arquitectura. Además, el ancho de banda y la latencia de L3 siguen siendo mucho mejores que DRAM, pero no son gratis.

Si el conjunto de trabajo de tu carga es más grande que L3, vas a DRAM. Si es más grande que DRAM… bueno, eso se llama “swap”, y es un grito de ayuda.

Líneas de caché, localidad y la regla “lo tocaste, lo compraste”

Los CPUs no traen bytes individuales a caché. Traen líneas de caché, comúnmente 64 bytes en x86_64. Cuando cargas un valor, a menudo arrastras
valores cercanos también. Eso es bueno si tu código usa memoria vecina (localidad espacial). Es malo si solo querías un campo y el resto es basura,
porque acabas de contaminar la caché con cosas que no reutilizarás.

La localidad es todo el juego:

  • Localidad temporal: si lo usas de nuevo pronto, la caché ayuda.
  • Localidad espacial: si usas memoria cercana, la caché ayuda.

Bases de datos, caches y enrutadores de solicitudes a menudo viven o mueren por lo predecible que sea su patrón de acceso. Los escaneos secuenciales pueden ser rápidos porque los prefetchers de hardware pueden mantener el ritmo. El rastreo de punteros aleatorio por una gran tabla hash puede ser lento porque cada paso es “sorpresa, ve a memoria”.

Traducción operativa seca: si ves CPU alto pero también muchos ciclos estancados, no tienes un problema de “cálculo”. Tienes un problema de “alimentar al núcleo”.
Tu ruta de código más caliente probablemente está dominada por fallas de caché o mispredicts de ramas, no por matemática.

Broma #1: Las fallas de caché son como “preguntas rápidas” en el chat corporativo—cada una parece pequeña hasta que te das cuenta de que todo tu día está esperando por ellas.

Prefetching: el intento del CPU de ser útil

Los CPUs intentan detectar patrones y prefetch líneas de caché futuras. Funciona bien para accesos en streaming y con paso fijo. Funciona mal para rastreo de punteros, porque
la dirección de la siguiente carga depende del resultado de la carga anterior.

Por eso “optimizé el bucle” a veces no hace nada. El bucle no es el problema; la cadena de dependencia de memoria lo es.

La parte que nadie quiere depurar: coherencia y falso compartido

En sistemas multi-núcleo, cada núcleo tiene sus propias cachés. Cuando un núcleo escribe en una línea de caché, las copias de otros núcleos deben invalidarse o actualizarse para que todos
vean una vista consistente. Eso es coherencia de caché. Es necesario. También es una trampa de rendimiento.

Falso compartido: cuando tus hilos pelean por una línea de caché que no “comparten”

El falso compartido ocurre cuando dos hilos actualizan variables distintas que por casualidad viven en la misma línea de caché. No están compartiendo datos lógicamente, pero el protocolo de
coherencia trata la línea entera como una unidad. Así que cada escritura genera invalidaciones y transferencias de propiedad, y tu rendimiento se desploma.

En cuanto a síntomas, parece “más hilos lo hacen más lento” con mucho tiempo de CPU gastado, pero sin mucho progreso. Verás alto tráfico cache-to-cache y
fallas de coherencia si miras con las herramientas correctas.

Broma #2: El falso compartido es como cuando dos equipos “poseen” la misma celda de una hoja de cálculo; las ediciones son correctas, el proceso no lo es.

Las cargas con muchas escrituras pagan extra

Las lecturas se pueden compartir. Las escrituras requieren propiedad exclusiva de la línea, lo que dispara acciones de coherencia. Si tienes un contador caliente actualizado por muchos hilos,
el contador se convierte en un cuello de botella serializado aunque “tengas muchos núcleos”.

Por eso existen contadores por hilo, bloqueos shardados y batching. No estás siendo elegante. Estás evitando una factura de físicas.

NUMA: el impuesto de latencia al escalar

En muchos servidores, la memoria está físicamente atada a sockets de CPU. Acceder a memoria “local” es más rápido que acceder a memoria conectada a otro socket.
Eso es NUMA (Non-Uniform Memory Access). No es un caso marginal. Es la configuración por defecto en mucho hardware de producción real.

Puedes pasar por alto NUMA hasta que no puedas. El modo de falla aparece cuando:

  • mueves hilos a través de sockets,
  • tu asignador esparce páginas entre nodos,
  • o el planificador migra hilos lejos de su memoria.

Entonces la latencia se dispara, el rendimiento se estanca y el CPU parece “ocupado” porque está estancado. Puedes perder semanas afinando código de aplicación cuando la solución
es pinnear procesos, arreglar la política de asignación, o elegir menos sockets con relojes más altos para cargas sensibles a latencia.

Hechos e historia interesantes para repetir en reuniones

  1. La “pared de la memoria” se convirtió en una preocupación mainstream en los años 90: la velocidad de CPU mejoró más rápido que la latencia de DRAM, haciendo las cachés obligatorias.
  2. Las líneas de caché son una elección de diseño: 64 bytes es común en x86, pero otras arquitecturas han usado tamaños distintos; es un equilibrio entre ancho de banda y contaminación.
  3. L1 suele estar dividida en caché de instrucciones y datos porque mezclarlas causa conflictos; las lecturas de código y las cargas de datos tienen patrones distintos.
  4. El compartir L3 es intencional: ayuda cuando los hilos comparten datos mayormente de lectura y reduce viajes a DRAM, pero también crea contención bajo carga.
  5. Existen prefetchers de hardware porque el acceso secuencial es común; pueden acelerar dramáticamente lecturas en streaming sin cambios en el código.
  6. Los protocolos de coherencia (como variantes de MESI) son una gran razón por la que multi-núcleo “simplemente funciona”, pero también imponen costos reales bajo contención por escrituras.
  7. Los TLB también son cachés: la Translation Lookaside Buffer cachea traducciones de direcciones; los misses de TLB pueden doler como las fallas de caché.
  8. Las páginas grandes reducen la presión del TLB mapeando más memoria por entrada; pueden ayudar a algunas cargas y perjudicar a otras.
  9. Las sorpresas tempranas de escalado multi-núcleo en los 2000 enseñaron a los equipos que “más hilos” no es un plan de rendimiento si la memoria y el locking no se manejan.

Guion rápido de diagnóstico

Cuando un sistema está lento, quieres encontrar el recurso limitante rápido, no escribir poesía sobre microarquitectura. Esta es una lista de verificación de campo.

Primero: confirma si estás limitado por cómputo o por espera

  • Revisa la utilización de CPU y métricas de nivel de ejecución: cola de ejecución, cambios de contexto, presión de IRQ.
  • Mira ciclos estancados / fallas de caché con perf si puedes.
  • Si las instrucciones por ciclo son bajas y las fallas de caché son altas, probablemente esté limitado por latencia de memoria o por ancho de banda de memoria.

Segundo: decide si es por latencia o por ancho de banda

  • Latencia limitada: rastreo de punteros, acceso aleatorio, muchas fallas en LLC, ancho de banda de memoria bajo.
  • Ancho de banda limitado: streaming, escaneos grandes, muchos núcleos leyendo/escribiendo, alto ancho de banda cerca del límite de la plataforma.

Tercero: revisa NUMA y topología

  • ¿Corren los hilos en un socket pero se asigna memoria en otro?
  • ¿Estás haciendo thrashing entre sockets en el LLC?
  • ¿La carga es sensible a la latencia de cola (casi siempre lo es), haciendo la memoria remota un asesino silencioso?

Cuarto: comprueba lo “obvio pero aburrido”

  • ¿Estás haciendo swap o hay presión de memoria (tormentas de reclaim)?
  • ¿Estás alcanzando límites de memoria de cgroup?
  • ¿Estás saturando un único bloqueo o contador (falso compartido, mutex contendido)?

Idea parafraseada (atribuida): el mensaje operativo de Gene Kim es que los bucles de retroalimentación rápidos vencen a los heroísmos—mide primero, luego cambia una cosa a la vez.

Tareas prácticas: comandos, salidas y decisiones

Estas están pensadas para ejecutarse en un host Linux donde diagnosticas rendimiento. Algunas requieren root o permisos de perf.
El punto no es memorizar comandos; es conectar salidas con decisiones.

Task 1: Identify cache sizes and topology

cr0x@server:~$ lscpu
Architecture:             x86_64
CPU(s):                   64
Thread(s) per core:       2
Core(s) per socket:       16
Socket(s):                2
L1d cache:                32K
L1i cache:                32K
L2 cache:                 1M
L3 cache:                 35.8M
NUMA node(s):             2
NUMA node0 CPU(s):        0-31
NUMA node1 CPU(s):        32-63

Qué significa: tienes dos sockets, dos nodos NUMA y un L3 por socket (a menudo). Tu conjunto de trabajo que sobrepasa ~36MB por socket
empieza a pagar precios de DRAM.
Decisión: si el servicio es sensible a latencia, planifica consciencia NUMA (pinning, política de memoria) y mantén pequeñas las estructuras de datos calientes.

Task 2: Verify cache line size (and stop guessing)

cr0x@server:~$ getconf LEVEL1_DCACHE_LINESIZE
64

Qué significa: los límites de riesgo de falso compartido son de 64 bytes.
Decisión: en código de bajo nivel, alinea contadores/estructuras por hilo calientes a límites de 64B para evitar el ping-pong de líneas de caché.

Task 3: Confirm NUMA distances

cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 0 size: 256000 MB
node 0 free: 120000 MB
node 1 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
node 1 size: 256000 MB
node 1 free: 118000 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

Qué significa: la memoria remota es ~2x la “distancia.” No literalmente 2x latencia, pero direccionalmente significativo.
Decisión: si eres sensible a la latencia de cola, mantiene hilos y su memoria locales (o reduce tráfico cruzado de sockets limitando la afinidad de CPU).

Task 4: Check if the kernel is fighting you with automatic NUMA balancing

cr0x@server:~$ cat /proc/sys/kernel/numa_balancing
1

Qué significa: el kernel puede migrar páginas para “seguir” a los hilos. Genial a veces, ruidoso otras.
Decisión: para cargas estables y pinned, puedes desactivarlo (con cuidado, probado) u sobrescribir con colocación explícita.

Task 5: Observe per-process NUMA memory placement

cr0x@server:~$ pidof myservice
24718
cr0x@server:~$ numastat -p 24718
Per-node process memory usage (in MBs) for PID 24718 (myservice)
Node 0          38000.25
Node 1           2100.10
Total           40100.35

Qué significa: el proceso usa mayormente memoria del nodo0. Si sus hilos corren en node1, pagarás penalidades remotas.
Decisión: alinea afinidad de CPU y política de asignación de memoria; si está desbalanceado por accidente, arregla el scheduling o la colocación al arrancar.

Task 6: Check memory pressure and swapping (the performance cliff)

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
 3  0      0 1200000  80000 9000000   0    0     2    15  900 3200 45  7 48  0  0
 5  0      0 1180000  80000 8900000   0    0     0     0 1100 4100 55  8 37  0  0
 7  0      0 1170000  80000 8850000   0    0     0     0 1300 5200 61  9 30  0  0

Qué significa: no hay swap-in/out (si/so = 0), así que no estás en la categoría “todo es terrible”. El CPU está ocupado, pero no esperando IO.
Decisión: procede al análisis de caché/memoria; no pierdas tiempo culpando al disco.

Task 7: See if you’re bandwidth-bound (quick read on memory throughput)

cr0x@server:~$ sudo perf stat -a -e cycles,instructions,cache-references,cache-misses,LLC-loads,LLC-load-misses -I 1000 -- sleep 5
# time(ms)  cycles        instructions   cache-references  cache-misses  LLC-loads    LLC-load-misses
     1000   5,210,000,000  2,340,000,000  120,000,000       9,800,000     22,000,000   6,700,000
     2000   5,300,000,000  2,310,000,000  118,000,000      10,200,000     21,500,000   6,900,000
     3000   5,280,000,000  2,290,000,000  121,000,000      10,500,000     22,300,000   7,100,000

Qué significa: instrucciones/ciclo es bajo (aprox. 0.43 aquí), y las fallas de caché/LLC son significativas. El CPU está esperando mucho.
Decisión: trata esto como dominado por latencia de memoria a menos que los contadores de ancho de banda muestren saturación; busca acceso aleatorio, rastreo de punteros o NUMA.

Task 8: Identify top functions and whether they stall (profile with perf)

cr0x@server:~$ sudo perf top -p 24718
Samples: 2K of event 'cycles', Event count (approx.): 2500000000
  18.50%  myservice  myservice  [.] hashmap_lookup
  12.20%  myservice  myservice  [.] parse_request
   8.90%  libc.so.6  libc.so.6   [.] memcmp
   7.40%  myservice  myservice  [.] cache_get
   5.10%  myservice  myservice  [.] serialize_response

Qué significa: los hotspots son lookup/compare pesados—candidatos clásicos para fallas de caché y mispredicts de ramas.
Decisión: inspecciona las estructuras de datos: ¿las claves están dispersas? ¿estás persiguiendo punteros? ¿puedes empaquetar datos? ¿puedes reducir comparaciones?

Task 9: Check for scheduler migration (NUMA’s quiet enabler)

cr0x@server:~$ pidstat -w -p 24718 1 3
Linux 6.5.0 (server)  01/09/2026  _x86_64_  (64 CPU)

01:02:11      UID       PID   cswch/s nvcswch/s  Command
01:02:12     1001     24718   1200.00    850.00  myservice
01:02:13     1001     24718   1350.00    920.00  myservice
01:02:14     1001     24718   1100.00    800.00  myservice

Qué significa: los cambios de contexto altos pueden indicar contención por locks o demasiados hilos listos para ejecutar.
Decisión: si la latencia es irregular, reduce el número de hilos, investiga locks o pinnea hilos críticos para reducir migraciones.

Task 10: Check run queue and per-CPU saturation (don’t confuse “busy” with “progress”)

cr0x@server:~$ mpstat -P ALL 1 2
Linux 6.5.0 (server)  01/09/2026  _x86_64_  (64 CPU)

01:03:01 AM  CPU   %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
01:03:02 AM  all   62.0  0.0   9.0   0.1    0.0  0.5    0.0    0.0    0.0   28.4
01:03:02 AM   0    95.0  0.0   4.0   0.0    0.0  0.0    0.0    0.0    0.0    1.0
01:03:02 AM  32    20.0  0.0   5.0   0.0    0.0  0.0    0.0    0.0    0.0   75.0

Qué significa: CPU0 está saturado mientras CPU32 está mayormente idle. Esto puede ser un problema de afinidad, un shard caliente o un embudo de lock.
Decisión: si un solo núcleo está caliente, el escalado no ocurrirá hasta que elimines el embudo. Investiga la distribución de trabajo por núcleo y los locks.

Task 11: Verify CPU affinity and cgroup constraints

cr0x@server:~$ taskset -pc 24718
pid 24718's current affinity list: 0-15

Qué significa: el proceso está pinned a CPUs 0–15 (un subconjunto de un socket). Puede ser intencional o accidental.
Decisión: si está pinned, asegúrate de que la memoria sea local a ese nodo; si es accidental, arregla tu unit file / conjunto de CPU del orquestador.

Task 12: Check LLC miss rate per process (perf stat on PID)

cr0x@server:~$ sudo perf stat -p 24718 -e cycles,instructions,LLC-loads,LLC-load-misses -- sleep 10
 Performance counter stats for process id '24718':

     18,320,000,000      cycles
      7,410,000,000      instructions              #    0.40  insn per cycle
        210,000,000      LLC-loads
         78,000,000      LLC-load-misses           #   37.14% of all LLC hits

      10.001948393 seconds time elapsed

Qué significa: una tasa de fallas de carga en LLC de ~37% es una gran señal de que tu conjunto de trabajo no cabe en caché o el acceso es aleatorio.
Decisión: reduce el conjunto de trabajo, aumenta la localidad o cambia el diseño de datos. También valida la localidad NUMA.

Task 13: Spot page faults and major faults (TLB and paging hints)

cr0x@server:~$ pidstat -r -p 24718 1 3
Linux 6.5.0 (server)  01/09/2026  _x86_64_  (64 CPU)

01:04:10      UID       PID  minflt/s  majflt/s     VSZ     RSS  %MEM  Command
01:04:11     1001     24718   8200.00      0.00  9800000 4200000  12.8  myservice
01:04:12     1001     24718   7900.00      0.00  9800000 4200000  12.8  myservice
01:04:13     1001     24718   8100.00      0.00  9800000 4200000  12.8  myservice

Qué significa: fallas menores altas pueden ser normales (paginación por demanda, archivos mapeados), pero si las fallas aumentan bajo carga puede correlacionar con
churn de páginas y presión de TLB.
Decisión: si las fallas correlacionan con picos de latencia, revisa el comportamiento del asignador, el uso de mmap y considera páginas grandes solo después de medir.

Task 14: Validate transparent huge pages (THP) status

cr0x@server:~$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

Qué significa: THP está siempre activado. Algunas bases de datos lo adoran, algunos servicios sensibles a latencia odian el comportamiento de asignación/compactación.
Decisión: si ves paradas periódicas, prueba madvise o never en staging y compara la latencia de cola.

Task 15: Check memory bandwidth counters (Intel/AMD tooling varies)

cr0x@server:~$ sudo perf stat -a -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ -- sleep 5
 Performance counter stats for 'system wide':

       8,120,000,000      uncore_imc_0/cas_count_read/
       4,010,000,000      uncore_imc_0/cas_count_write/

       5.001234567 seconds time elapsed

Qué significa: estos contadores aproximan transacciones DRAM; si son altos y cerca de los límites de la plataforma, estás limitado por ancho de banda.
Decisión: si estás limitado por ancho de banda, añadir núcleos no ayudará. Reduce los datos escaneados, comprime, mejora la localidad o acerca el trabajo a los datos.

Task 16: Identify lock contention (often misdiagnosed as “cache issues”)

cr0x@server:~$ sudo perf lock report -p 24718
Name                 acquired  contended   total wait (ns)   avg wait (ns)
pthread_mutex_lock      12000       3400      9800000000         2882352

Qué significa: los hilos pasan tiempo real esperando locks. Esto puede amplificar efectos de caché (las líneas de caché rebotan con la propiedad del lock).
Decisión: reduce la granularidad de locks, haz sharding o cambia el algoritmo. No “optimices memoria” si tu cuello de botella es un mutex.

Task 17: Watch LLC occupancy and memory stalls (if supported)

cr0x@server:~$ sudo perf stat -p 24718 -e cpu/mem-loads/,cpu/mem-stores/ -- sleep 5
 Performance counter stats for process id '24718':

        320,000,000      cpu/mem-loads/
         95,000,000      cpu/mem-stores/

       5.000912345 seconds time elapsed

Qué significa: tráfico pesado de loads/stores sugiere que el trabajo es centrado en memoria. Combínalo con métricas de fallas de LLC para decidir si es amigable con caché.
Decisión: si hay muchas cargas con altas tasas de fallas, enfócate en la localidad de estructuras de datos y en reducir el rastreo de punteros.

Task 18: Validate that you’re not accidentally throttling (frequency matters)

cr0x@server:~$ cat /proc/cpuinfo | grep -m1 "cpu MHz"
cpu MHz		: 1796.234

Qué significa: la frecuencia del CPU es relativamente baja (posible ahorro de energía o restricciones térmicas).
Decisión: si el rendimiento empeoró tras un cambio de plataforma, valida el governor de CPU y la temperatura antes de culpar a las cachés.

Tres mini-historias corporativas desde las trincheras

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

Un servicio de pagos empezó a tener timeouts todos los días a más o menos la misma hora. El equipo lo llamó “saturación de CPU” porque los paneles mostraban CPU al 90%,
y el flame graph destacaba parsing de JSON y algo de hashing. Hicieron lo que hacen los equipos: añadieron instancias, aumentaron pools de hilos y subieron límites de autoscaling.
El incidente empeoró. Las colas de latencia se volvieron más agresivas.

La suposición equivocada fue sutil: “CPU alto significa que el núcleo está ocupado calculando.” En realidad, los núcleos estaban ocupados esperando. perf stat mostró IPC bajo
y una alta tasa de fallas en LLC. La ruta de la solicitud tenía una consulta de enriquecimiento respaldada por caché que se había expandido silenciosamente: más claves, más metadatos, más objetos con punteros,
y un conjunto de trabajo que ya no cabía ni cerca de L3.

Entonces el cambio de escalado lo empujó a un nuevo modo de falla. Más hilos significaron más accesos aleatorios en paralelo, lo que incrementó el paralelismo a nivel de memoria
pero también la contención. El controlador de memoria se calentó, el ancho de banda subió y la latencia media subió con él. Fue un clásico: cuanto más empujabas,
más el subsistema de memoria se resistía.

La solución no fue heroica. Redujeron el overhead de objetos, empaquetaron campos en arreglos contiguos para la ruta caliente y limitaron el conjunto de enriquecimiento por solicitud.
También dejaron de pinear el proceso a ambos sockets sin controlar la colocación de memoria. Una vez mejoró la localidad, la utilización de CPU siguió alta,
pero el throughput subió y la latencia de cola bajó. Los gráficos de CPU parecían lo mismo. El sistema se comportó diferente. Esa es la lección.

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

Un equipo intentó acelerar una API de analytics “mejorando el cache”. Reemplazaron un vector simple de structs por un hash map con clave string para evitar escaneos lineales. Los microbenchmarks en laptop se vieron geniales. Producción no estuvo de acuerdo.

La nueva estructura destruyó la localidad. El código antiguo escaneaba un arreglo contiguo: predecible, amigable con prefetch y con caché. El código nuevo hacía búsquedas aleatorias, cada una involucrando rastreo de punteros, hashing de strings y múltiples cargas dependientes. En servidores reales bajo carga, convirtió un bucle mayormente amigable con L2/L3 en una fiesta DRAM.

Peor aún, el hash map introdujo una ruta de resize compartida. Bajo picos de tráfico, ocurrieron resizes, los locks contendieron y las líneas de caché rebotaron entre núcleos.
El equipo vio CPU más alto y concluyó “necesitamos más CPU”. Pero el “más CPU” aumentó la contención, y su p99 empeoró.

Revirtieron, luego implementaron un compromiso aburrido: mantener un vector ordenado para la ruta caliente y hacer reconstrucciones ocasionales fuera del hilo de solicitud,
con un pointer de snapshot estable. Aceptaron O(log n) con buena localidad en lugar de O(1) con constantes terribles. Producción volvió a ser aburrida,
que es el tipo de éxito con el que puedes construir una carrera.

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

Un servicio adyacente a almacenamiento—muchas lecturas de metadatos, algunas escrituras—se migró a una nueva plataforma hardware. Todos esperaban que fuera más rápido. No lo fue.
Hubo picos esporádicos de latencia y ocasionales caídas de throughput, pero nada obvio: no swap, discos bien, red bien.

El equipo tenía un hábito que los salvó: un “bundle de triaje de rendimiento” que ejecutaban ante cualquier regresión. Incluía lscpu,
topología NUMA, perf stat para IPC y fallas de LLC, y una comprobación rápida de frecuencia de CPU y governors. No emocionante. Fiable.

El bundle mostró inmediatamente dos sorpresas. Primero, las nuevas máquinas tenían más sockets, y el servicio se estaba planificando a través de sockets sin
colocación de memoria consistente. Segundo, la frecuencia de CPU era más baja bajo carga sostenida por ajustes de energía en la imagen base.

La solución fue procedimental: actualizar la baseline de tuning de hosts (governor, settings de firmware donde correspondiera), y pinear el servicio a un solo
nodo NUMA con memoria ligada a ese nodo. Sin cambios en código. La latencia se estabilizó. El rollout terminó. El postmortem fue corto, que es un lujo.

Errores comunes (síntomas → causa raíz → solución)

1) “CPU está alto así que necesitamos más CPU”

Síntomas: CPU 80–95%, throughput plano, p95/p99 peor al añadir hilos/instancias.
Causa raíz: IPC bajo debido a fallas de caché o stalls de memoria; el CPU está “ocupado esperando”.
Solución: mide IPC y fallas de LLC con perf stat; reduce el conjunto de trabajo, mejora la localidad o arregla la colocación NUMA. No escales hilos a ciegas.

2) “Hash map siempre es más rápido que un escaneo”

Síntomas: más lento tras cambiar a estructura “O(1)”; perf muestra hotspots en hashing/strcmp/memcmp.
Causa raíz: acceso aleatorio y rastreo de punteros causan viajes a DRAM; la mala localidad vence al big-O en hardware real.
Solución: prefiere estructuras contiguas para rutas calientes (arrays, vectores, vectores ordenados). Haz benchmarks con datasets y concurrencia parecidos a producción.

3) “Más hilos = más throughput”

Síntomas: el throughput mejora y luego colapsa; aumentan los cambios de contexto; suben las fallas de LLC.
Causa raíz: saturación de ancho de banda de memoria, contención de locks o falso compartido se vuelven dominantes.
Solución: limita el número de hilos cerca de la rodilla de la curva; shardea locks/contadores; evita escrituras compartidas calientes; pinnea hilos si es sensible a NUMA.

4) “NUMA no importa; Linux lo manejará”

Síntomas: buena latencia promedio, latencia de cola terrible; regresiones al pasar a hosts multi-socket.
Causa raíz: acceso a memoria remota y tráfico entre sockets; migración del planificador rompe la localidad.
Solución: usa numastat y numactl; pinea CPU y memoria; considera correr un proceso por socket para previsibilidad.

5) “Si desactivamos las cachés, podemos probar el peor caso”

Síntomas: alguien sugiere apagar cachés o limpiar constantemente como estrategia de prueba.
Causa raíz: malentendido; los sistemas modernos no están diseñados para ese modo y los resultados no se mapearán a la realidad.
Solución: prueba con conjuntos de trabajo realistas y patrones de acceso; usa contadores de perfilado, no experimentos de feria de ciencias.

6) “Páginas grandes siempre ayudan”

Síntomas: THP activado y paradas periódicas; actividad de compactación; picos de latencia durante el crecimiento de memoria.
Causa raíz: overhead de asignación/compactación de THP; desajuste con patrones de asignación.
Solución: mide always vs madvise vs never; si usas páginas grandes, asigna por adelantado y monitorea la latencia de cola.

Listas de verificación / plan paso a paso

Checklist A: Demuestra que es memoria, no cómputo

  1. Captura la topología de CPU: lscpu. Registra sockets/NUMA y tamaños de caché.
  2. Revisa swapping/presión de memoria: vmstat 1. Si si/so > 0, arregla memoria primero.
  3. Mide IPC y fallas de LLC: perf stat (system-wide o PID). IPC bajo + fallas altas de LLC = sospecha de stalls de memoria.
  4. Busca funciones calientes: perf top. Si los hotspots son lookup/compare/alloc, espera problemas de localidad.

Checklist B: Decide si es por latencia o por ancho de banda

  1. Si la tasa de fallas de LLC es alta pero los contadores de ancho de banda son moderados: probablemente es rastreo de punteros limitado por latencia.
  2. Si los contadores de ancho de banda están cerca del límite de la plataforma y los núcleos no ayudan: probablemente es escaneo/stream limitado por ancho de banda.
  3. Cambia una cosa y vuelve a medir: reduce concurrencia, reduce conjunto de trabajo o cambia el patrón de acceso.

Checklist C: Arregla NUMA antes de reescribir código

  1. Mapea nodos NUMA: numactl --hardware.
  2. Revisa memoria por proceso y nodo: numastat -p PID.
  3. Revisa afinidad de CPU: taskset -pc PID.
  4. Alinea: pinea CPUs a un nodo y enlaza memoria al mismo nodo (prueba en staging primero).

Checklist D: Haz los datos amigables con caché (lo aburrido gana)

  1. Aplana estructuras con muchos punteros en las rutas calientes.
  2. Empaqueta campos calientes juntos; separa campos fríos (hot/cold split).
  3. Prefiere arrays/vectores e iteración predecible sobre acceso aleatorio.
  4. Shardea contadores con muchas escrituras; agrupa actualizaciones.
  5. Haz benchmarks con tamaños similares a producción; los efectos de caché aparecen cuando los datos son lo suficientemente grandes.

Preguntas frecuentes

1) ¿L1 siempre es más rápida que L2, y L2 siempre más rápida que L3?

Generalmente sí en términos de latencia, pero el rendimiento real depende de contención, patrón de acceso y de si la línea ya está presente por prefetching.
Además, las características de ancho de banda difieren; L3 puede entregar alto ancho de banda agregado pero con mayor latencia.

2) ¿Por qué mi CPU muestra 90% de uso si está “esperando en memoria”?

Porque “uso de CPU” significa principalmente que el núcleo no está idle. Una pipeline estancada sigue ejecutando instrucciones, manejando misses, haciendo especulación
y quemando ciclos. Necesitas contadores (IPC, fallas de caché, ciclos estancados) para ver la espera.

3) ¿Cuál es la diferencia entre caché de CPU y la page cache de Linux?

Las cachés de CPU son gestionadas por hardware y son pequeñas (KB/MB). La page cache de Linux es gestionada por el SO, usa DRAM y cachea datos respaldados por archivos (GBs).
Interactúan, pero resuelven problemas distintos a diferentes escalas.

4) ¿Puedo “aumentar L3” cambiando software?

No literalmente. Lo que puedes hacer es actuar como si tuvieras más caché reduciendo tu conjunto de trabajo caliente, mejorando la localidad y evitando contaminación de caché.

5) ¿Por qué las listas enlazadas y los árboles con punteros rinden mal?

Destruyen la localidad espacial. Cada puntero conduce a una línea de caché distinta, a menudo lejana. Eso significa cargas dependientes y viajes frecuentes a DRAM,
que hacen que el núcleo se estanque.

6) ¿Cuándo debería preocuparme por el falso compartido?

Cuando tienes varios hilos actualizando campos/contadores distintos en bucles apretados y el rendimiento empeora con más hilos.
Es común en contadores de métricas, ring buffers y arreglos ingenuos de estado por conexión.

7) ¿Las fallas de caché siempre son malas?

Algunas fallas son inevitables. La pregunta es si tu carga está estructurada para que las faltas se amortigüen (streaming) o sean catastróficas (cargas dependientes aleatorias).
Optimiza para reducir fallas en la ruta caliente, no para alcanzar un mítico “cero fallas”.

8) ¿Los CPUs más rápidos arreglan problemas de memoria?

A veces los empeoran. Los núcleos más rápidos pueden demandar datos más deprisa y golpear la pared de memoria antes. Una plataforma con mejor ancho de banda de memoria,
mejor topología NUMA o caches más grandes puede importar más que GHz puros.

9) ¿Debería pinear todo a un socket?

Para servicios sensibles a latencia, pinear a un socket (y enlazar memoria) puede ser una gran ventaja: localidad predecible, menos accesos remotos.
Para trabajos de alto throughput, esparcirse por sockets puede ayudar—si mantienes la localidad y evitas hotspots de escrituras compartidas.

10) ¿Qué métrica debería vigilar en dashboards para detectar problemas de caché temprano?

Si puedes, exporta IPC (instrucciones por ciclo) y tasas de fallas de LLC o ciclos estancados desde perf/PMU. Si no, vigila este patrón:
CPU sube, throughput plano, latencia sube al escalar. Ese patrón grita memoria.

Conclusión: qué hacer la próxima semana

Las cachés de CPU no son trivia. Son la razón por la que un cambio “simple” puede hundir el p99 y por la que añadir núcleos muchas veces solo añade decepción.
La memoria gana porque marca el ritmo: si tu núcleo no consigue datos barato, no puede hacer trabajo útil.

Pasos prácticos:

  • Pon perf stat (IPC + fallas de LLC) en tu kit estándar de incidentes para páginas “ligadas a CPU”.
  • Documenta la topología NUMA por clase de host y decide si los servicios deberían pinearse (y cómo) por defecto.
  • Audita rutas calientes por localidad: aplana estructuras, separa campos hot/cold y evita hotspots de escrituras compartidas.
  • Haz benchmarks con tamaños de dataset realistas. Si tu benchmark cabe en L3, no es un benchmark; es una demo.
  • Cuando se sugiera una optimización, haz una pregunta primero: “¿Qué hace esto a las fallas de caché y al tráfico de memoria?”

MySQL vs PostgreSQL: cargas JSON — atajo rápido o dolor a largo plazo

Añades una columna JSON porque “solo necesitas flexibilidad”. Luego tus paneles se vuelven lentos, tus réplicas se retrasan,
y alguien pide “una consulta ad-hoc rápida” que se convierte en un escaneo de tabla a través de millones de filas. JSON es la
cinta adhesiva del modelado de datos: a veces salva el día, otras veces es la razón por la que hubo que salvarlo.

MySQL y PostgreSQL soportan JSON, pero fomentan hábitos muy diferentes. Uno te dejará lanzar rápido y acumular deuda en silencio.
El otro te permite construir índices y restricciones potentes—pero también te da suficiente cuerda para tejer un suéter de bloat y contención de locks si no tienes cuidado.

La decisión en una página: qué elegir y cuándo

Usa PostgreSQL cuando…

  • Necesitas consultas ricas (contención, existencia, filtros anidados) y quieres que el optimizador tenga opciones. JSONB + GIN de PostgreSQL es la caja de herramientas madura.
  • Quieres restricciones sobre datos semiestructurados: CHECK constraints, índices de expresión, columnas generadas e índices funcionales son ciudadanos de primera clase.
  • Esperas que JSON permanezca más de un trimestre. PostgreSQL suele envejecer mejor cuando JSON se convierte en “esquema central”.
  • Puedes operar vacuum con competencia. PostgreSQL te lo recompensará, pero solo si respetas el housekeeping de MVCC.

Usa MySQL cuando…

  • Tu uso de JSON es principalmente almacenamiento de documentos + recuperación, no filtrado analítico intenso. Si las consultas son “buscar por id y devolver blob”, MySQL puede ir muy bien.
  • Dependes de columnas generadas para proyectar rutas JSON calientes en escalares indexables. Este es el camino práctico de MySQL hacia la predictibilidad.
  • Ya estás estandarizado operativamente en MySQL y JSON es una pequeña parte de la carga. Operaciones consistentes superan a la elegancia teórica.

Qué le diría a un equipo en producción

Si tus columnas JSON son un hack transitorio (ingesta rápida, normalizar después), elige la base de datos que tu equipo ya
opere bien. Pero si JSON es el contrato de interfaz (eventos, configuraciones, feature flags, atributos de usuario) y esperas consultar
dentro de él a escala, PostgreSQL suele ser la apuesta a largo plazo más segura.

MySQL puede rendir bien con JSON, pero a menudo exige que “declares las partes importantes” mediante columnas generadas e índices dirigidos.
Si no lo haces, acabarás explicándole a la dirección por qué tu esquema flexible se convirtió en latencia inflexible.

Una cita que debería estar en cada runbook de on-call: “La esperanza no es una estrategia.” — un máximo de operaciones ampliamente repetido (idea parafraseada).
Con JSON, esperar que la base de datos lo resuelva es la forma de comprarte un incidente de fin de semana.

Hechos e historia: cómo llegamos aquí

JSON en la base de datos parece moderno, pero la industria lleva décadas rondando esta idea: “almacena datos flexibles junto a los estructurados,
y consúltalos sin renunciar a la seguridad transaccional.” Los detalles difieren, y esos detalles son por qué estás leyendo esto en lugar de dormir.

8 hechos que vale la pena tener en la cabeza

  1. PostgreSQL añadió JSON en 9.2 (2012), luego introdujo JSONB en 9.4 (2014) para almacenamiento binario y mejor indexación.
  2. MySQL introdujo un tipo JSON nativo en 5.7 (2015); antes era TEXT con una oración y una regex.
  3. JSONB normaliza el orden de claves y elimina claves duplicadas (la última clave gana). Eso es genial para indexación, sorprendente para “almacenar exactamente lo que envié”.
  4. MySQL también almacena JSON en formato binario, y valida JSON al insertar, evitando algunos horrores de “blob inválido”.
  5. Los índices GIN de PostgreSQL se construyeron originalmente para búsqueda de texto completo, luego se convirtieron en la herramienta para contención de JSONB.
  6. Las columnas generadas de MySQL existen desde 5.7, y son la razón por la que muchos despliegues JSON en MySQL no colapsan.
  7. MVCC en PostgreSQL significa que las actualizaciones crean nuevas versiones de fila; las actualizaciones grandes de JSON pueden amplificar el bloat a menos que vacuum esté al día.
  8. Los formatos de replicación importan: binlog por fila de MySQL y logical decoding de PostgreSQL se comportan distinto bajo actualizaciones frecuentes de JSON y filas calientes.

Semántica JSON: qué almacenan realmente los motores

MySQL: JSON es un tipo, pero trátalo como documento a menos que proyectes campos

El tipo JSON de MySQL no es “TEXT con etiqueta.” Se valida, se almacena en una representación binaria y se manipula con funciones JSON.
Esa es la buena noticia. La noticia operacional es que rara vez obtendrás rendimiento sostenido a menos que hagas una de dos cosas:
(1) mantengas JSON mayormente de escritura-única/lectura-por-clave-primaria, o (2) extraigas las rutas consultadas con frecuencia en columnas generadas y las indexees.

MySQL te dejará escribir una consulta que parece selectiva pero no es indexable. El optimizador hará lo que pueda, luego escaneará.
A veces puedes rescatarla con índices funcionales (dependiendo de la versión) o columnas generadas, pero debes ser intencional.

PostgreSQL: JSONB es para consultar; JSON (texto) es para preservar la entrada exacta

PostgreSQL te ofrece dos filosofías diferentes:
json almacena el texto original (incluyendo espacios y orden), y
jsonb almacena un formato binario descompuesto optimizado para operadores e indexación.
Si quieres rendimiento, casi siempre querrás JSONB.

Los operadores de PostgreSQL son expresivos: contención (@>), existencia (?, ?|, ?&),
extracción por ruta (->, ->>, #>, #>>), y consultas JSON path.
Esa expresividad puede ser una trampa: la gente escribe filtros ingeniosos que parecen baratos y terminan con la CPU ahogada en descompresión o atascados en un índice que no coincide con el predicado.

Broma 1/2: JSON es como un cajón de sastre—todo cabe hasta que realmente necesitas encontrar las tijeras.

Indexación JSON: donde se gana o se pierde rendimiento

Indexación en MySQL: las columnas generadas son el movimiento adulto

En MySQL, indexar expresiones JSON arbitrarias ha mejorado con el tiempo, pero el patrón operacional más confiable sigue siendo:
define columnas generadas para las pocas rutas JSON que consultas todo el tiempo, cástralas a tipos escalares estables e índexalas.
Esto hace tres cosas:

  • Le da al optimizador un índice B-tree normal que entiende.
  • Evita extracciones JSON repetidas en tiempo de ejecución.
  • Te obliga a admitir qué campos son realmente parte del “esquema real”.

La contrapartida: los cambios de esquema se vuelven más lentos y políticos, porque ahora el blob JSON tiene tentáculos en DDL y migraciones.
Eso no es un bug. Es el precio de fingir que los datos semiestructurados no tienen estructura (porque la tienen, una vez que dependes de ellos).

Cuando la indexación JSON de MySQL falla en la práctica

  • Predicados excesivamente dinámicos (diferentes rutas JSON según la entrada del usuario) te empujan hacia escaneos.
  • Comparar cadenas JSON con números provoca casts implícitos y rompe el uso de índices.
  • Usar funciones en WHERE sin una expresión indexable hace que el optimizador se encoge de hombros y haga el trabajo de la forma lenta.

Indexación en PostgreSQL: GIN es potente, pero debes elegir la clase de operador

La historia de indexación JSONB de PostgreSQL es más fuerte, pero no es magia. Los índices GIN pueden acelerar contención y consultas de existencia,
pero tienen diferentes clases de operador:

  • jsonb_ops: indexa más tipos de operaciones pero puede ser más grande.
  • jsonb_path_ops: más compacto y rápido para contención, pero soporta menos operadores.

Si tu carga es “encuentra filas donde JSON contiene estos pares”, jsonb_path_ops suele ser la elección correcta.
Si necesitas existencia flexible y más soporte de operadores, jsonb_ops.
Elige mal y tendrás un índice que existe solo para hacer sufrir a VACUUM.

Índices de expresión: el puente práctico entre JSON y relacional

Si filtras frecuentemente por un campo extraído (por ejemplo, payload->>'customer_id'), un índice de expresión puede superar a un GIN amplio
en tamaño y predictibilidad. También es más fácil razonar sobre la selectividad.

Broma 2/2: Un índice GIN es como la cafeína—increíble cuando está dirigido, remordimiento cuando te excedes.

Patrones de consulta que separan “bien” de “arde todo”

Patrón 1: “Recuperar por id y devolver JSON” (poco riesgoso)

Tanto MySQL como PostgreSQL manejan esto bien. El coste dominante es I/O y tamaño de fila, no las funciones JSON.
Donde los equipos se lesionan es la lenta erosión: JSON crece, el tamaño de fila crece, la eficiencia de caché cae, y de repente “lecturas simples” se vuelven lecturas desde disco.

Patrón 2: “Filtrar por claves JSON con alta cardinalidad” (indexar o morir)

Si filtras por user_id, tenant_id, order_id dentro de JSON, en efecto estás filtrando por una clave relacional.
No finjas que es flexible. Promuévela: columna generada + índice en MySQL, índice de expresión en Postgres, o simplemente hazla una columna real.
Esto no es ideología. Es para evitar escaneos completos y planes de consulta inestables.

Patrón 3: “Analítica ad-hoc sobre JSON” (cuidado con la erosión lenta)

JSON atrae para analítica porque se autodescribe. En bases OLTP de producción, eso es una trampa.
La analítica ad-hoc tiende a:

  • Usar funciones sobre muchas filas, causando consumo de CPU.
  • Forzar escaneos secuenciales porque los predicados no coinciden con índices.
  • Serializar tu carga en una tabla grande y un subsistema de disco caliente.

Si el negocio quiere analítica, o separa una réplica de reporting con guardrails estrictos, o transmite eventos a otro sitio.
“Solo ejecútalo en prod” es una decisión de presupuesto disfrazada de decisión de ingeniería.

Patrón 4: actualizaciones parciales a JSON (filas calientes, logs pesados)

Ambas bases pueden actualizar rutas dentro de JSON, pero las características de rendimiento difieren y el impacto operacional es parecido:
actualizaciones frecuentes a documentos JSON grandes significan más bytes escritos, más churn de índices, más trabajo de replicación y más invalidación de caché.

La regla práctica: si un campo JSON se actualiza con frecuencia y se lee con frecuencia, merece una columna real o una tabla separada.
JSON no es un pase libre sobre la normalización; es una factura diferida.

Actualizaciones, WAL/binlog y retraso de replicación

MySQL: volumen de binlog y realidades de replicación por fila

En MySQL, las actualizaciones grandes de JSON pueden producir eventos de binlog grandes—especialmente con replicación por fila. Si actualizas muchas filas o documentos grandes,
tus réplicas pagan el precio. El retraso de replicación rara vez es “un problema de la réplica”. Es un problema de amplificación de escritura de la aplicación.

También vigila el tamaño de las transacciones y la frecuencia de commits. Una carga que actualiza JSON en ráfagas puede crear picos desagradables: presión de fsync,
stalls en el flush de binlog y backlog del hilo SQL en la réplica.

PostgreSQL: presión de WAL + churn de MVCC

PostgreSQL escribe WAL por cambios, y MVCC significa que las actualizaciones crean nuevas versiones de fila. Actualiza frecuentemente un campo JSONB grande y obtendrás:
más WAL, más tuples muertas, más trabajo de vacuum y potencialmente más bloat de índices.

El retraso de replicación se manifiesta como backlog del WAL sender o demora de replay. La clave es distinguir:
la réplica no puede aplicar lo bastante rápido (limitada por CPU/I/O aplicando cambios) vs
el primario produce demasiado WAL (amplificación de escritura).

Guía operacional

  • Mide bytes de WAL/binlog por segundo durante el pico. Es lo más cercano a la “verdad” sobre amplificación de escritura.
  • Particiona o separa campos JSON calientes si las tasas de actualización son altas.
  • En PostgreSQL, ajusta autovacuum para tablas con muchas actualizaciones de JSON, o la deuda de vacuum aparecerá como deuda de latencia.

Realidad de almacenamiento e I/O: bloat, churn de páginas y comportamiento de caché

Tamaño de fila y caché: tu impuesto invisible

Las columnas JSON agrandan las filas. Filas más grandes significan menos filas por página. Menos filas por página significa más lecturas de página para el mismo número de filas lógicas.
Esto se manifiesta como:

  • Mayor churn en el buffer pool de MySQL (InnoDB).
  • Más churn en shared_buffers de PostgreSQL.
  • Más presión sobre la caché de páginas del SO.

La mayoría de las “regresiones de rendimiento misteriosas” tras añadir JSON son en realidad “duplicamos el tamaño de fila y nadie ajustó la memoria o patrones de acceso.”

Bloat en PostgreSQL: MVCC significa que le debes el collector vacuum

PostgreSQL no actualiza en sitio; crea nuevas versiones de fila. Si JSONB es grande y se actualiza frecuentemente, se acumulan tuples muertas y los índices churnean.
Autovacuum puede manejar mucho, pero necesita los umbrales correctos. Los ajustes por defecto están diseñados para ser seguros para principiantes, no óptimos para tu desastre.

MySQL: índices secundarios y presión de undo/redo

InnoDB de MySQL tiene su propia amplificación de escritura: redo logs, undo logs, doublewrite buffer, mantenimiento de índices secundarios.
Las actualizaciones grandes de JSON aumentan los bytes tocados y pueden empujarte a stalls de flush de logs. Lo verás como picos intermitentes de latencia,
“commits que de repente son lentos” y réplicas quedándose atrás.

Tareas prácticas: 14 comandos que puedes ejecutar hoy

Estos son los tipos de comandos que echo a correr durante un incidente o una revisión de rendimiento. Cada tarea incluye:
el comando, qué significa la salida y qué decisión tomar a continuación.
Los hostnames y rutas son deliberadamente aburridos; lo aburrido se repite.

Tarea 1 (MySQL): confirmar uso de JSON y presión de tamaño

cr0x@server:~$ mysql -e "SELECT table_schema, table_name, column_name, data_type FROM information_schema.columns WHERE data_type='json' ORDER BY table_schema, table_name;"
+--------------+------------+-------------+-----------+
| table_schema | table_name | column_name | data_type |
+--------------+------------+-------------+-----------+
| app          | events     | payload     | json      |
| app          | users      | attrs       | json      |
+--------------+------------+-------------+-----------+

Significado: ahora sabes qué tablas son candidatas a dolores relacionados con JSON.
Decisión: preselecciona las 1–3 tablas principales por recuento de filas y tasa de actualización. Ahí es donde importan la indexación y las decisiones de esquema.

Tarea 2 (MySQL): comprobar tamaños de tablas y huella de índices

cr0x@server:~$ mysql -e "SELECT table_name, table_rows, ROUND(data_length/1024/1024,1) AS data_mb, ROUND(index_length/1024/1024,1) AS index_mb FROM information_schema.tables WHERE table_schema='app' ORDER BY data_length DESC LIMIT 10;"
+------------+------------+---------+----------+
| table_name | table_rows | data_mb | index_mb |
+------------+------------+---------+----------+
| events     |    4821031 |  8120.4 |   2104.7 |
| users      |     820114 |  1190.8 |    412.2 |
+------------+------------+---------+----------+

Significado: las tablas con mucho JSON tienden a inflar data_mb.
Decisión: si data_mb crece más rápido que el crecimiento del negocio, necesitas limitar el tamaño del payload, comprimir aguas arriba o normalizar campos calientes.

Tarea 3 (MySQL): identificar predicados JSON lentos en el slow log

cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log --limit 5
#  1.2s user time, 40ms system time, 27.31M rss, 190.55M vsz
# Query 1: 0.68 QPS, 0.31x concurrency, ID 0xA1B2C3D4 at byte 91234
# Time range: 2025-12-28T00:00:00 to 2025-12-28T01:00:00
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Exec time     62   180s    120ms     12s    540ms     3s   900ms   300ms
# Rows examine  90  1200M      10   2.5M   360k   1.1M   500k   200k
# Query: SELECT ... WHERE JSON_EXTRACT(payload,'$.customer.id') = ?

Significado: rows examined es tu “impuesto por escaneo.” JSON_EXTRACT en WHERE sin índice es un sospechoso habitual.
Decisión: crea una columna generada para esa ruta (o un índice funcional si es apropiado) y reescribe la consulta para usarla.

Tarea 4 (MySQL): verificar si una consulta usa un índice

cr0x@server:~$ mysql -e "EXPLAIN SELECT id FROM app.events WHERE JSON_UNQUOTE(JSON_EXTRACT(payload,'$.customer.id'))='12345' LIMIT 10\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: events
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4821031
     filtered: 10.00
        Extra: Using where

Significado: type: ALL y sin key significa escaneo completo de tabla.
Decisión: no optimices buffers primero. Arregla el esquema/consulta: columna generada + índice, o rediseña.

Tarea 5 (MySQL): añadir una columna generada para una ruta JSON caliente

cr0x@server:~$ mysql -e "ALTER TABLE app.events ADD COLUMN customer_id VARCHAR(64) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(payload,'$.customer.id'))) STORED, ADD INDEX idx_events_customer_id (customer_id);"
Query OK, 0 rows affected (2 min 41 sec)
Records: 0  Duplicates: 0  Warnings: 0

Significado: la columna generada STORED materializa el valor, el índice pasa a ser utilizable.
Decisión: reescribe las consultas de la aplicación para filtrar por customer_id en lugar de JSON_EXTRACT en WHERE. Luego vuelve a comprobar EXPLAIN.

Tarea 6 (MySQL): validar que el optimizador ahora usa el nuevo índice

cr0x@server:~$ mysql -e "EXPLAIN SELECT id FROM app.events WHERE customer_id='12345' LIMIT 10\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: events
         type: ref
possible_keys: idx_events_customer_id
          key: idx_events_customer_id
      key_len: 258
          ref: const
         rows: 120
        Extra: Using index

Significado: pasaste de escanear millones a tocar ~120 filas.
Decisión: despliega el cambio y observa la latencia de escritura: mantener el nuevo índice aumenta el coste de escritura.

Tarea 7 (MySQL): comprobar retraso de replicación y presión de aplicación

cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G" | egrep "Seconds_Behind_Source|Replica_SQL_Running|Replica_IO_Running|Last_SQL_Error"
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 87
Last_SQL_Error:

Significado: hay lag aunque los threads estén activos. Usualmente la aplicación no puede aplicar tan rápido como escribe.
Decisión: mide la tasa de binlog y el tamaño de las transacciones; reduce el volumen de actualizaciones JSON o cambia el batching antes de culpar a la réplica.

Tarea 8 (PostgreSQL): listar columnas JSON/JSONB y sus tablas

cr0x@server:~$ psql -d appdb -c "SELECT table_schema, table_name, column_name, data_type FROM information_schema.columns WHERE data_type IN ('json','jsonb') ORDER BY 1,2,3;"
 table_schema | table_name | column_name | data_type
--------------+------------+-------------+-----------
 public       | events     | payload     | jsonb
 public       | users      | attrs       | jsonb
(2 rows)

Significado: alcance. Igual que MySQL: identifica las pocas tablas que más importan.
Decisión: céntrate primero en tablas con altas tasas de actualización y consultas cara al cliente.

Tarea 9 (PostgreSQL): encontrar las peores consultas JSON por tiempo total

cr0x@server:~$ psql -d appdb -c "SELECT calls, total_exec_time::bigint AS total_ms, mean_exec_time::numeric(10,2) AS mean_ms, rows, query FROM pg_stat_statements WHERE query ILIKE '%jsonb%' OR query ILIKE '%->%' OR query ILIKE '%@>%' ORDER BY total_exec_time DESC LIMIT 5;"
 calls | total_ms | mean_ms | rows |                   query
-------+----------+---------+------+-------------------------------------------
 18211 |   932144 |   51.20 |    0 | SELECT ... WHERE payload @> $1
  4102 |   512030 |  124.82 |    0 | SELECT ... WHERE (payload->>'customer')= $1
(2 rows)

Significado: tienes consultas calientes, no teorías.
Decisión: ejecuta EXPLAIN (ANALYZE, BUFFERS) en los principales culpables y construye el índice correcto para la forma del predicado.

Tarea 10 (PostgreSQL): inspeccionar un plan de consulta JSONB con buffers

cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM events WHERE payload @> '{\"customer\":{\"id\":\"12345\"}}'::jsonb LIMIT 10;"
                                                          QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..8.44 rows=10 width=8) (actual time=0.088..0.146 rows=10 loops=1)
   Buffers: shared hit=42
   ->  Index Scan using idx_events_payload_gin on events  (cost=0.42..22134.77 rows=26235 width=8) (actual time=0.086..0.141 rows=10 loops=1)
         Index Cond: (payload @> '{"customer": {"id": "12345"}}'::jsonb)
         Buffers: shared hit=42
 Planning Time: 0.412 ms
 Execution Time: 0.182 ms
(7 rows)

Significado: index scan + mayormente buffer hits = saludable.
Decisión: conserva este índice si soporta rutas de producto centrales. Si es solo para consultas ad-hoc, no pagues el impuesto de escritura.

Tarea 11 (PostgreSQL): crear un GIN dirigido (elige la clase de operador)

cr0x@server:~$ psql -d appdb -c "CREATE INDEX CONCURRENTLY idx_events_payload_pathops ON events USING gin (payload jsonb_path_ops);"
CREATE INDEX

Significado: la construcción concurrente evita bloquear escrituras (a costa de tiempo y trabajo extra).
Decisión: usa jsonb_path_ops cuando la contención domine; de lo contrario considera jsonb_ops o índices de expresión.

Tarea 12 (PostgreSQL): construir un índice de expresión para una ruta caliente

cr0x@server:~$ psql -d appdb -c "CREATE INDEX CONCURRENTLY idx_events_customer_id_expr ON events ((payload->'customer'->>'id'));"
CREATE INDEX

Significado: esto hace que los predicados de igualdad sobre ese valor extraído sean previsibles y baratos.
Decisión: si esta ruta es estable y muy usada, considera promoverla a una columna real para reducir el procesamiento JSON por completo.

Tarea 13 (PostgreSQL): verificar señales de bloat y efectividad de autovacuum

cr0x@server:~$ psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
 relname | n_live_tup | n_dead_tup |    last_autovacuum     |     last_vacuum
---------+------------+------------+------------------------+------------------------
 events  |    4809123 |     912044 | 2025-12-28 00:41:12+00 | 2025-12-22 03:11:02+00
 users   |     820104 |      12033 | 2025-12-28 00:38:01+00 | 2025-12-23 02:08:40+00
(2 rows)

Significado: las tuples muertas en events son altas; autovacuum está corriendo, pero puede estar subdimensionado para el churn de actualizaciones.
Decisión: ajusta umbrales de autovacuum por tabla, reduce la frecuencia de actualizaciones a JSONB grandes, o separa campos mutables.

Tarea 14 (Nivel sistema): identificar si estás limitado por I/O o CPU

cr0x@server:~$ iostat -x 1 5
Linux 6.1.0 (db01) 	12/29/2025 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.11    0.00    6.34   18.90    0.00   52.65

Device            r/s     rkB/s   rrqm/s  %rrqm  r_await rareq-sz     w/s     wkB/s   w_await wareq-sz  aqu-sz  %util
nvme0n1         320.0  18240.0     0.0   0.00    4.20    57.00   410.0  24576.0    9.80    59.95   6.10   92.0

Significado: %util alto y iowait significativo apuntan a saturación de almacenamiento. Las cargas JSON a menudo inflan I/O debido a filas más grandes y churn de índices.
Decisión: arregla patrones de consulta/índice primero; si sigue saturado, escala IOPS (mejores discos) o reduce la amplificación de escritura (cambios de esquema/diseño).

Guion de diagnóstico rápido

Cuando las consultas JSON se vuelven lentas, la gente pierde horas discutiendo “elección de base de datos” en lugar de encontrar el verdadero cuello de botella.
Este guion es el orden en que lo ejecutaría en un incidente—porque converge rápido.

Primero: demuestra si es un escaneo, una falta de índice o I/O bruto

  • MySQL: ejecuta EXPLAIN en la consulta lenta. Si type: ALL, para y arregla el predicado/índice.
  • PostgreSQL: ejecuta EXPLAIN (ANALYZE, BUFFERS). Si ves sequential scans en tablas grandes, necesitas un índice coincidente o reescribir la consulta.
  • Sistema: comprueba iostat -x. Si el almacenamiento está al 100%, los escaneos y el bloat serán tus sospechosos principales.

Segundo: cuantifica la amplificación de escritura y presión de replicación

  • MySQL: inspecciona el retraso de replicación y patrones de crecimiento del binlog; las actualizaciones grandes de JSON suelen correlacionarse con picos de lag.
  • PostgreSQL: comprueba la generación de WAL y las tuples muertas; las actualizaciones intensas de JSON pueden convertir el vacuum en una crisis de fondo permanente.

Tercero: revisa la efectividad de caché y la erosión del tamaño de fila

  • ¿Tu working set caliente sigue en memoria, o el crecimiento de JSON lo expulsó?
  • ¿Añadiste un GIN amplio que duplicó el coste de escritura?
  • ¿Alguien empezó a hacer filtros ad-hoc sobre claves JSON sin indexar?

Cuarto: arregla lo más pequeño que cambie la curva

  • Promueve claves calientes a columnas reales (mejor) o a columnas generadas/índices de expresión (siguiente mejor).
  • Añade el índice más adecuado para la forma del predicado, y valida con EXPLAIN.
  • Si las actualizaciones son el problema, separa campos mutables del blob JSON.

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

Error 1: “La consulta parece selectiva pero es lenta”

Síntomas: la latencia crece con el tamaño de la tabla; EXPLAIN muestra escaneo completo; CPU se dispara en pico.

Causa raíz: extracción JSON en WHERE sin una expresión indexable (MySQL), o desacople entre operador e índice (PostgreSQL).

Solución: MySQL: columna generada STORED + índice B-tree; PostgreSQL: índice de expresión o clase de operador GIN correcta; reescribe el predicado para que coincida con el índice.

Error 2: “Añadimos un índice GIN y las escrituras se volvieron más lentas”

Síntomas: la latencia de insert/update aumenta; la tasa de WAL/binlog sube; el retraso de replicación empeora tras crear el índice.

Causa raíz: GIN amplio sobre JSONB grande con actualizaciones frecuentes; alto coste de mantenimiento de índice.

Solución: reemplaza por índices de expresión más estrechos; usa jsonb_path_ops si es solo contención; separa campos mutables; reconsidera si necesitas esa consulta en OLTP.

Error 3: “Postgres está lento con el tiempo; vacuum no da abasto”

Síntomas: tamaños de tabla e índice crecen; consultas lentas; autovacuum corre constantemente; tuples muertas altas.

Causa raíz: actualizaciones frecuentes a JSONB grandes crean muchas tuples muertas; umbrales de autovacuum no sintonizados para el churn de la tabla.

Solución: ajusta autovacuum por tabla; reduce frecuencia/tamaño de actualizaciones; mueve datos mutables a tabla separada; considera particionado para tablas tipo evento.

Error 4: “Lag de replicación en MySQL tras añadir características JSON”

Síntomas: Seconds_Behind_Source sube durante ráfagas; réplicas se recuperan lentamente; commits son espaciados.

Causa raíz: grandes eventos de binlog por fila derivados de actualizaciones JSON; transacciones sobredimensionadas; demasiados índices secundarios en proyecciones JSON.

Solución: reduce volumen de actualizaciones JSON; agrupa las operaciones de otra manera; limita las proyecciones indexadas a rutas realmente calientes; verifica ajustes de binlog/redo log y patrones de commit.

Error 5: “Almacenamos todo en JSON y ahora necesitamos restricciones”

Síntomas: valores inconsistentes en JSON; validaciones en la aplicación que se desbordan; consultas deben manejar claves faltantes y tipos erróneos.

Causa raíz: esquema externalizado al código de la aplicación; sin restricciones aplicadas; migraciones evitadas hasta demasiado tarde.

Solución: promueve campos clave a columnas; añade CHECK constraints (Postgres) o aplica columnas generadas + NOT NULL/tipos (MySQL); introduce payloads versionados.

Tres micro-historias corporativas desde las trincheras JSON

1) Incidente causado por una suposición errónea: “JSON es básicamente gratis de consultar”

Una compañía SaaS mediana lanzó un “feed de actividad” respaldado por una tabla de eventos. Cada evento tenía un payload JSON.
El equipo de producto quería filtrado: “mostrar solo eventos donde payload.actor.role = ‘admin’.” Fácil, pensaron.
El backend usaba MySQL, y la primera implementación usó JSON_EXTRACT en la cláusula WHERE.

En staging iba bien. En producción fue un desastre en cámara lenta: la tabla events era grande, y el filtro era popular.
La consulta parecía selectiva, pero hacía un escaneo completo, tocando millones de filas por petición en pico.
CPU al máximo, I/O saturado y todo el clúster desarrolló el síntoma de “todo es lento” que hace que los ejecutivos entren al canal de incidentes.

La suposición equivocada no fue “MySQL no puede hacer JSON.” Fue: “si el predicado es estrecho, la base de datos lo optimizará.”
Las bases optimizan lo que indexas, no lo que esperas. La extracción JSON sin expresión indexable no es estrecha; es matemática cara repetida en muchas filas.

La solución fue dolorosamente directa: añadir una columna generada STORED para actor_role, indexarla y cambiar la consulta.
El postmortem añadió una regla: cualquier clave JSON usada en un WHERE crítico debe proyectarse e indexarse, o moverse a una columna real.
El esquema flexible siguió existiendo, pero solo donde no estaba en la ruta crítica.

2) Optimización que salió mal: “Solo añade un GIN grande”

Otra empresa usaba PostgreSQL y tenía una única tabla masiva events con payloads JSONB.
Querían búsquedas ad-hoc más rápidas para soporte al cliente, así que alguien añadió un GIN amplio sobre todo el payload usando la clase de operador por defecto.
La velocidad de consulta mejoró al instante. Todos celebraron y siguieron adelante.

Dos semanas después, la latencia de escritura empezó a crecer. La actividad de autovacuum se volvió constante. Apareció retraso de replicación en picos.
El índice GIN era caro de mantener porque los payloads eran grandes y se actualizaban con campos de enriquecimiento.
El índice también creció rápido, aumentando la presión de checkpoints e I/O. La victoria de “búsqueda de soporte” se convirtió en “cada endpoint API es más lento”.

El problema no fue que GIN sea malo. Fue que indexaron todo, para una carga de consultas que en realidad no era central.
El índice convirtió la base de datos en un motor de búsqueda. PostgreSQL puede hacer eso, pero se paga en amplificación de escritura y bloat.

La solución eventual: eliminar el índice amplio, añadir dos índices de expresión para un puñado de claves usadas en filtros de soporte,
y sacar la búsqueda tipo texto completo fuera del camino OLTP. Soporte recuperó su flujo, pero producción dejó de pagar el impuesto en cada escritura.

3) Práctica aburrida pero correcta que salvó el día: “Haz del JSON un contrato, versiona y prueba”

Un equipo fintech almacenó metadata de verificación de clientes en JSONB en PostgreSQL. Incluía campos anidados, claves opcionales y bloques específicos por proveedor.
Sabían que estos datos evolucionarían, y también sabían que tendrían que consultar algunos campos de forma fiable para informes de cumplimiento.
Así que hicieron algo que parece poco sexy: añadieron una columna schema_version entero y escribieron migraciones explícitas para cambios en la forma del payload.

También promovieron unos campos críticos a columnas reales: customer_id, verification_status y vendor_name.
Todo lo demás vivía en JSONB. Encima de eso, tuvieron CHECK constraints que aseguraban que la columna status coincidiera con un conjunto conocido,
y tests de aplicación que validaban compatibilidad de esquema por versión.

Meses después, un proveedor cambió su formato de payload de forma sutil (un campo se movió más profundo).
Los equipos que almacenan JSON crudo sin contrato suelen descubrirlo cuando los informes fallan a las 2 a.m.
Este equipo lo descubrió en CI, porque un test de validación de esquema falló y la herramienta de migración obligó a una transformación explícita.

La práctica “aburrida” no fue un índice sofisticado. Fue tratar JSON como un contrato versionado, no como un cajón de sastre sin límites.
Producción se benefició: el rendimiento de consultas se mantuvo estable y la frecuencia de incidentes bajó—ese tipo de victoria que nunca recibe un correo de celebración.

Listas de verificación / plan paso a paso

Si estás iniciando una nueva feature con mucho JSON

  1. Escribe los 5 principales patrones de consulta que esperas en los próximos seis meses (no solo la semana de lanzamiento).
  2. Clasifica campos: inmutables vs mutables; frecuentemente filtrados vs raramente filtrados; alta cardinalidad vs baja cardinalidad.
  3. Promueve los campos “frecuentemente filtrados y alta cardinalidad” a columnas reales (preferido) o a columnas generadas/índices de expresión.
  4. Elige la estrategia de indexación específica por base de datos:
    • MySQL: columnas generadas STORED + índices B-tree; evita JSON_EXTRACT en cláusulas WHERE calientes.
    • PostgreSQL: índices de expresión para rutas calientes; GIN para contención/existencia; selecciona la clase de operador intencionalmente.
  5. Fija presupuestos de tamaño de payload (límites suaves y duros). El crecimiento de JSON es silencioso hasta que no lo es.
  6. Planifica la evolución: añade schema_version, documenta transformaciones y haz las migraciones rutinarias.

Si ya lo lanzaste y está lento

  1. Encuentra las 3 consultas principales por tiempo total (slow log / pg_stat_statements).
  2. Ejecuta EXPLAIN con la realidad (MySQL EXPLAIN, Postgres EXPLAIN ANALYZE BUFFERS). No adivines.
  3. Añade el índice más pequeño que coincida con el predicado (índice de columna generada o índice de expresión) y verifica cambios de plan.
  4. Mide el coste del lado escribiente tras indexar (latencia de commit, tasa WAL/binlog, retraso de replicación).
  5. Si las actualizaciones son intensas, separa campos mutables del JSON y ponlos en otra tabla con clave correcta.
  6. Pon guardrails a las consultas ad-hoc (timeouts, réplicas de lectura o una ruta de reporting dedicada).

Si estás decidiendo entre MySQL y PostgreSQL para JSON hoy

  • Elige PostgreSQL si la consulta JSON es una característica de producto, no un detalle de implementación.
  • Elige MySQL si JSON es mayormente almacenamiento y estás dispuesto a proyectar las claves calientes en columnas generadas indexadas.
  • Elige la base de datos que tu equipo pueda operar en condiciones de incidente. Una característica teóricamente superior no despertará a tu on-call a las 3 a.m.

Preguntas frecuentes

1) ¿PostgreSQL siempre es mejor para JSON que MySQL?

No. PostgreSQL suele ser mejor para consultas complejas y indexación flexible. MySQL puede ser excelente cuando mantienes el uso de JSON simple
o proyectas rutas calientes en columnas generadas indexadas. “Siempre” es como empiezan los outages.

2) ¿Debería almacenar JSON como TEXT/VARCHAR en su lugar?

Usualmente no. Pierdes validación y muchos operadores JSON. Si realmente nunca consultas dentro del JSON y solo lo almacenas y recuperas,
TEXT puede servir—pero te cargas el riesgo de higiene de datos. Los tipos JSON nativos son más seguros para la corrección.

3) ¿Cuándo debe una clave JSON convertirse en una columna real?

Si se usa en joins, en WHERE calientes, para ordenación o se necesita para restricciones, debe ser columna. Si se actualiza frecuentemente,
probablemente sea columna o tabla separada. JSON es para variabilidad, no para identidad central.

4) ¿Los índices GIN resuelven el rendimiento de JSONB en PostgreSQL?

Resuelven algunos problemas. También pueden crear otros (coste de escritura, bloat, mantenimiento).
Usa GIN cuando tus predicados se alineen con contención/existencia y los datos indexados sean lo bastante estables para justificar el impuesto de escritura.

5) ¿Cuál es el equivalente en MySQL de un índice GIN en JSONB de Postgres?

No hay un equivalente directo. En MySQL típicamente creas columnas generadas que extraen valores escalares e indizas esos valores.
Es una filosofía diferente: decides qué importa desde el principio.

6) ¿Cómo evito “claves aleatorias por todas partes” en JSON?

Trata JSON como un contrato: versiona, valida y documenta las formas permitidas.
Aplica invariantes críticas con restricciones de base de datos (Postgres) o columnas generadas + NOT NULL/casts de tipo (MySQL).

7) ¿Por qué las actualizaciones parciales de JSON siguen sintiéndose caras?

Porque la “actualización parcial” a nivel SQL aún puede significar reescritura sustancial y churn de índices a nivel de almacenamiento,
además de volumen de WAL/binlog. Documentos grandes actualizados frecuentemente son caros, independientemente de lo bonito que se vea el SQL.

8) ¿Puedo usar JSON para datos multi-tenant y simplemente filtrar por tenant_id dentro del JSON?

Puedes, pero no deberías. El aislamiento por tenant pertenece a una columna real con índice.
Ponerlo en JSON facilita escanear accidentalmente entre tenants y complica hacer cumplir límites de rendimiento y restricciones.

9) ¿Cuál es el patrón híbrido más seguro?

Almacena campos centrales como columnas (ids, status, timestamps, claves foráneas), guarda campos opcionales/específicos de proveedor en JSON/JSONB,
e indexa solo el subconjunto pequeño de rutas JSON que realmente consultas. Todo lo demás permanece flexible sin impulsar el coste de las consultas centrales.

Conclusión: próximos pasos que no te avergüencen después

JSON en MySQL y PostgreSQL ya no es una novedad. Es una herramienta de producción—y como todas las herramientas de producción, recompensa la disciplina.
MySQL tiende a querer que proyectes estructura fuera del JSON e indexes explícitamente. PostgreSQL te da consultas e indexación más expresivas,
pero te cobrará en WAL, bloat y mantenimiento si indexas demasiado o actualizas campos JSONB grandes con demasiada frecuencia.

Pasos prácticos siguientes:

  1. Identifica las 3 consultas JSON principales por tiempo total y ejecuta EXPLAIN con estadísticas reales de ejecución.
  2. Promueve las 3 claves JSON principales usadas para filtrado/joining a columnas o columnas generadas/índices de expresión y indexalas.
  3. Mide la amplificación de escritura (tasa WAL/binlog) antes y después de indexar; vigila el retraso de replicación.
  4. Establece un presupuesto de tamaño de payload e implémentalo en la ingestión.
  5. Versiona tus payloads JSON. El tú del futuro, de lo contrario, pasará un fin de semana descifrando “por qué a veces existe esta clave”.

Elige la base de datos que coincida con las fortalezas operativas de tu equipo, y luego diseña el uso de JSON como si esperases que se vuelva permanente—porque normalmente lo hace.

ZFS SMB: Solucionar de verdad “Copia lenta en Windows”

Windows Explorer muestra “Copiando… 112 MB/s” durante tres segundos, luego cae a 0 B/s y se queda allí como si estuviera reflexionando sobre su vida. Los usuarios culpan “la red”. La red culpa “el almacenamiento”. El almacenamiento culpa “Windows”. Cada uno tiene una parte de error distinta.

Si ejecutas SMB respaldado por ZFS (normalmente Samba en Linux, a veces en un appliance de almacenamiento), puedes hacer que las copias desde Windows sean consistentemente rápidas. Pero no lo consigues girando perillas al azar. Lo haces demostrando de dónde viene la latencia y luego arreglando la parte específica que está fallando.

Qué significa realmente “copia lenta” (y qué no es)

“La copia en Windows es lenta” no es un problema único. Es un síntoma visible por el usuario de una canalización que incluye: comportamiento del cliente Windows, semántica del protocolo SMB, detalles de implementación de Samba, grupos de transacciones y rutas de escritura de ZFS, y latencia del medio físico. Tu tarea es encontrar la etapa que convierte ancho de banda en espera.

Los tres patrones de copia que debes separar

  • Copias grandes y secuenciales (p. ej., ISO, VHDX): deberían correr cerca de la velocidad del enlace hasta que el servidor no pueda comprometer escrituras lo suficientemente rápido.
  • Muchos archivos pequeños (p. ej., árboles de código): dominados por metadata (create, setattr, close, rename), no por el throughput.
  • Cargas mixtas (carpetas personales + VMs + escáneres): “lento” suele ser bloqueo por cabeza de línea: un patrón malo arruina la cola para todos.

Lo que normalmente no es

Rara vez es “SMB es lento”. SMB3 puede ser muy rápido. Rara vez es “ZFS es lento”. ZFS puede saturar redes serias. Suele ser picos de latencia por escrituras síncronas, I/O aleatorio pequeño, amplificación de metadata o alineación de caché deficiente, visibles por un cliente que reporta velocidad en ráfagas optimistas.

Un cambio de encuadre más: Windows Explorer no es una herramienta de benchmark; es un visualizador de ansiedad. Ese gráfico es más anillo de humor que osciloscopio.

Hechos interesantes y contexto histórico (para entender el comportamiento)

  1. SMB1 vs SMB2/3 lo cambió todo. SMB2 (era Vista/2008) redujo la verbosidad, aumentó lecturas/escrituras y añadió pipelining. Muchas historias de “SMB es lento” en realidad son “estás atascado en SMB1”.
  2. Samba empezó como un proyecto reimplementado. Creció desde “hacer que UNIX hable con Windows” hasta un servidor SMB de nivel empresarial. Algunas configuraciones por defecto son conservadoras porque Samba debe sobrevivir clientes raros.
  3. Las escrituras de ZFS se agrupan. ZFS compromete datos en grupos de transacciones (TXGs). Eso hace que el throughput sea excelente, pero también crea un comportamiento de “pulso” visible si la fase de commit se atrasa.
  4. Las escrituras síncronas son una promesa, no una sensación. Cuando un cliente SMB solicita durabilidad, ZFS debe comprometer de forma segura. Si tu pool no puede hacer fsync de baja latencia, obtendrás el clásico gráfico “rápido y luego cero”.
  5. Los manejadores durables y los leases de SMB cambiaron el comportamiento de close/open. Windows moderno cachea agresivamente. Eso es bueno, hasta que una aplicación fuerza semánticas de durabilidad y convierte el caching en dolor síncrono.
  6. Recordsize importa más para compartidos que la gente admite. El recordsize de ZFS modela la amplificación de I/O. Un recordsize incorrecto no solo desperdicia espacio: fuerza IOPS extra bajo accesos aleatorios pequeños.
  7. La compresión suele ayudar en SMB, incluso con CPUs rápidas. Muchos archivos de oficina se comprimen bien, reduciendo carga de disco y red. La ganancia suele ser en latencia, no en ancho de banda puro.
  8. El signing de SMB se volvió más común por seguridad. Activar signing puede costar CPU. La configuración “segura” a veces se convierte en “seguramente lenta” cuando el servidor tiene CPU débil o está limitado por un solo hilo.

Guía rápida de diagnóstico

Este es el orden que encuentra el cuello de botella rápido, sin caer en la trampa de “afinar todo”.

Primero: clasifica la carga

  • ¿Un archivo grande? ¿Muchos archivos pequeños? ¿Escrituras de aplicación con requisitos de durabilidad?
  • ¿La velocidad cae a intervalos fijos (cada pocos segundos)? Eso huele a latencia de commit de TXG.
  • ¿Ocurre solo en ciertos shares? Eso huele a propiedades del dataset o a opciones del share SMB.

Segundo: decide si es red, CPU o latencia de almacenamiento

  • Red: errores de interfaz, retransmisiones, MTU incorrecto, hashing LACP malo, clientes Wi‑Fi fingiendo ser servidores.
  • CPU: un núcleo saturado en smbd, sobrecarga por signing/encryption, interrupciones, saturación de softirq.
  • Latencia de almacenamiento: alto await en vdevs, camino sync de ZFS bloqueado, SLOG ausente/lento, pool casi lleno o fragmentado.

Tercero: valida comportamiento sync (este suele ser el villano)

  • Revisa la propiedad sync del dataset y las configuraciones de Samba que fuerzan sync (p. ej., strict sync).
  • Mide la latencia de fsync desde el host SMB, no desde tu portátil.
  • Si necesitas semánticas sync, asegúrate de un SLOG apropiado (protegido contra pérdida de energía) o acepta los límites de rendimiento de tus vdevs principales.

Cuarto: aisla el caso de “muchos archivos pequeños”

  • La metadata es la carga. Revisa atime, comportamiento de xattr y rendimiento de bloques pequeños.
  • Verifica que el layout del pool coincida con las expectativas de IOPS de metadata (tradeoffs espejos vs RAIDZ).

Quinto: ajusta solo lo que las mediciones implican

Si no puedes mostrar un antes/después en latencia de I/O, utilización de CPU o retransmisiones, no estás afinando: estás adornando.

Deja de adivinar: mide a dónde va el tiempo

Las copias SMB son una negociación entre un cliente que hace buffering y un servidor que compromete. Explorer reporta “velocidad” basado en qué tan rápido los datos son aceptados en buffers, no en qué tan rápido son escritos de forma duradera. Mientras tanto, ZFS puede aceptar datos rápidamente en ARC y buffers sucios, luego pausar mientras compromete TXGs. Esa pausa es donde el gráfico llega a cero.

Tu plan de medición debe responder tres preguntas:

  1. ¿El cliente está esperando al servidor (latencia), o no está enviando (throttling del cliente)?
  2. ¿El servidor espera a vaciados de disco (ruta sync) o a la CPU (signing/encryption) o a la red?
  3. ¿ZFS amplifica la carga (mismatch de recordsize, fragmentación, presión de metadata)?

Ingeniería de confiabilidad tiene una regla simple aplicable aquí: mide el sistema que tienes, no el sistema que quisieras tener.

Idea parafraseada (Gene Kim): “Mejorar el flujo significa encontrar y eliminar la restricción.” Ese es todo el juego.

Realidades de ZFS que afectan a SMB

TXGs y el patrón “rápido y luego cero”

ZFS acumula datos sucios en memoria y periódicamente los compromete a disco como un grupo de transacción. Si la fase de commit toma demasiado tiempo, el sistema restringe a los escritores. Desde la vista del cliente: ráfaga rápida, luego stall. Repetir. Eso no es “jitter de red”. Es la durabilidad del almacenamiento poniéndose al día.

Escrituras síncronas: el impuesto de durabilidad

Cuando la carga emite escrituras síncronas (o cuando el servidor las trata así), ZFS debe asegurar que los datos estén en almacenamiento estable antes de reconocerlos. En pools sin un dispositivo de intent log rápido, las escrituras síncronas impactan tus vdevs principales. Si éstos son RAIDZ con HDDs, puedes predecir el resultado: dolor con marca temporal.

Recordsize, ashift y amplificación de I/O

El recordsize de ZFS controla el tamaño máximo de bloque para datos de archivo. Los compartidos SMB suelen almacenar archivos de tamaños mixtos; un recordsize demasiado grande no siempre dañará lecturas secuenciales, pero puede perjudicar escrituras aleatorias y sobreescrituras parciales. Uno demasiado pequeño puede aumentar la sobrecarga de metadata y reducir la eficiencia de compresión.

La metadata no es “gratis”

Las copias de archivos pequeños estresan la metadata: entradas de directorio, ACLs, xattrs, marcas de tiempo. ZFS puede manejar esto bien, pero solo si el layout del pool y la caché son sensatos. Si construiste un RAIDZ ancho para capacidad y luego lo usaste como un share SMB intensivo en metadata, básicamente compraste un autobús y lo inscribiste en una carrera de motocicletas.

Llenado del pool y fragmentación

A medida que los pools se llenan, la asignación se vuelve más difícil, la fragmentación sube y la latencia aumenta. Los usuarios SMB experimentan esto como “estaba bien el mes pasado”. ZFS no olvida repentinamente cómo escribir; se queda sin lugares fáciles para poner bloques.

Realidades de SMB que afectan a ZFS

Semántica de copia en Windows: buffering, close y durabilidad

Windows puede hacer buffering de escrituras y solo forzar durabilidad al cerrar el archivo, dependiendo de flags de aplicación y configuración del servidor. Algunas apps (y algunas herramientas de seguridad) solicitan write-through. Eso convierte tu carga de “mayormente async” a “intensamente sync” al instante.

Signing y encriptación: la seguridad tiene coste en CPU

El signing de SMB suele ser exigido por políticas. La encriptación puede estar habilitada para ciertos shares. Ambos consumen CPU. Si tu servidor SMB tiene una CPU modesta con NIC rápida, puedes alcanzar un techo donde la red está inactiva y un núcleo está sudando por la criptografía.

SMB3 Multichannel: genial cuando funciona, irrelevante cuando no

Multichannel puede usar múltiples NICs y colas RSS. Cuando está mal configurado, obtienes exactamente un flujo TCP atascado en una cola. Entonces alguien dice “pero tenemos doble 10GbE” como si el servidor estuviera obligado a ocuparse.

Opportunistic locks, leases y antivirus

El caching del cliente (oplocks/leases) reduce la verbosidad. Pero los escáneres de seguridad de endpoints adoran abrir archivos, forzar actualizaciones de atributos y, en general, romper el comportamiento de caching. Esto puede convertir una copia de “muchos archivos pequeños” en un festival de llamadas al sistema.

Broma #1: El troubleshooting de SMB es como la política de oficina: todos insisten en que son el cuello de botella y, de alguna manera, todos tienen razón.

Tareas prácticas: comandos, salidas, decisiones

A continuación hay tareas reales que puedes ejecutar en el servidor SMB/ZFS. Cada una incluye qué significa la salida y qué decisión debes tomar después. Están sesgadas hacia Linux + Samba + OpenZFS, porque ahí viven la mayoría de los tickets “la copia en Windows es lenta”.

Tarea 1: Confirma la salud del pool (porque el rendimiento suele ser síntoma de un disco moribundo)

cr0x@server:~$ sudo zpool status -v tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 04:12:19 with 0 errors on Tue Dec 10 03:20:01 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          mirror-0                  ONLINE       0     0     0
            ata-SAMSUNG_SSD_1TB_A   ONLINE       0     0     0
            ata-SAMSUNG_SSD_1TB_B   ONLINE       0     0     0

errors: No known data errors

Significado: “ONLINE” y scrub limpio significa que no estás luchando contra reintentos silenciosos o carga de resilver. Si ves DEGRADED, resilvering o errores de checksum, detén el tuning y arregla el hardware primero.

Decisión: Si algún vdev muestra errores o actividad de resilver, programa remediación y vuelve a probar el rendimiento después de la estabilización.

Tarea 2: Revisa el llenado del pool (los pools casi llenos se vuelven lentos de forma predecible)

cr0x@server:~$ zfs list -o name,used,avail,refer,mountpoint tank
NAME   USED  AVAIL  REFER  MOUNTPOINT
tank   38.2T 2.1T   96K    /tank

Significado: ~95% usado (38.2T usados, 2.1T disponibles) es zona de peligro para muchas cargas. La asignación se vuelve restringida; la fragmentación sube.

Decisión: Si estás por encima de ~80–85% usado y el rendimiento importa, planifica recuperar/expandir espacio. Ningún ajuste de Samba vencerá a la física.

Tarea 3: Identifica qué dataset respalda el share SMB y muestra sus propiedades clave

cr0x@server:~$ sudo zfs get -H -o property,value recordsize,compression,atime,sync,xattr,acltype,primarycache,logbias tank/shares/engineering
recordsize	1M
compression	lz4
atime	on
sync	standard
xattr	sa
acltype	posixacl
primarycache	all
logbias	latency

Significado: Tienes recordsize 1M (bueno para archivos grandes secuenciales, riesgoso para sobreescrituras parciales), atime activado (escrituras de metadata extra), sync standard (sync respetado), xattr en SA (suele ser bueno), logbias latency (prefiere SLOG si está presente).

Decisión: Si el share es de “muchos archivos pequeños”, considera recordsize=128K y atime=off. Si son imágenes de VM, trátalo diferente (y probablemente no vía SMB).

Tarea 4: Mide latencia de I/O del pool durante una copia (la verdad está en iostat)

cr0x@server:~$ sudo zpool iostat -v tank 1 5
                              capacity     operations     bandwidth
pool                        alloc   free   read  write   read  write
--------------------------  -----  -----  -----  -----  -----  -----
tank                        38.2T  2.1T     12   2400   3.1M   210M
  mirror-0                  38.2T  2.1T     12   2400   3.1M   210M
    ata-SAMSUNG_SSD_1TB_A      -      -      6   1250   1.6M   108M
    ata-SAMSUNG_SSD_1TB_B      -      -      6   1150   1.5M   102M
--------------------------  -----  -----  -----  -----  -----  -----

Significado: Operaciones de escritura altas (2400/s) con ancho de banda moderado sugiere escrituras pequeñas o comportamiento intensivo en sync. Si el ancho de banda es bajo pero las ops son altas, estás limitado por IOPS o por flushes.

Decisión: Si las escrituras son pequeñas y frecuentes, investiga semánticas sync, carga de metadata y mismatch de recordsize. Si las ops son bajas y el ancho de banda es bajo, sospecha red o throttling SMB.

Tarea 5: Observa latencia por vdev con iostat (await es la alarma)

cr0x@server:~$ sudo iostat -x 1 3
Linux 6.6.15 (server) 	12/25/2025 	_x86_64_	(16 CPU)

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  r_await  w_await  svctm  %util
nvme0n1           2.0   950.0    64.0 118000.0   248.0     3.20    3.4     1.2      3.4    0.6   58.0
nvme1n1           1.0   910.0    32.0 112000.0   246.0     3.05    3.3     1.1      3.3    0.6   55.0

Significado: ~3.3ms de await en escritura está bien para NVMe. Si ves decenas/centenas de ms durante copias, el almacenamiento está limitando tu throughput.

Decisión: Await alto + CPU baja + red limpia = problema en la ruta de almacenamiento (escrituras sync, pool lleno, vdevs lentos o problemas con SLOG).

Tarea 6: Comprueba si tienes un SLOG (y si está haciendo algo)

cr0x@server:~$ sudo zpool status tank | sed -n '1,120p'
  pool: tank
 state: ONLINE
config:

        NAME                         STATE     READ WRITE CKSUM
        tank                         ONLINE       0     0     0
          mirror-0                   ONLINE       0     0     0
            ata-SAMSUNG_SSD_1TB_A    ONLINE       0     0     0
            ata-SAMSUNG_SSD_1TB_B    ONLINE       0     0     0
        logs
          nvme-SLOG_INTEL_OPTANE     ONLINE       0     0     0

Significado: Hay un dispositivo de log separado. Bien. Pero la existencia no es rendimiento; debe ser rápido y protegido contra pérdida de energía.

Decisión: Si existen cargas sync y no hay SLOG, decide si necesitas semánticas sync. Si las necesitas, añade un SLOG adecuado. Si no, no lo falsifiques con sync=disabled a menos que estés cómodo perdiendo datos reconocidos en un corte de energía.

Tarea 7: Observa throttling de TXG y comportamiento de datos sucios

cr0x@server:~$ sudo arcstat 1 3
    time  read  miss  miss%  dmis  dm%  pmis  pm%  mmis  mm%  arcsz     c  avail
12:10:01   320    12      4     1    0    11    3     0    0   84.2G  96.0G  21.4G
12:10:02   410    16      3     2    0    14    3     0    0   84.2G  96.0G  21.4G
12:10:03   390    10      2     1    0     9    2     0    0   84.2G  96.0G  21.4G

Significado: ARC no está fallando gravemente; el caching no es tu restricción actual. Si ARC es pequeño respecto a la memoria, o los misses se disparan, podrías estar limitado por memoria o mal configurado.

Decisión: Si los misses de ARC son altos durante lecturas, ajusta tamaño de memoria/ARC o el working set (o acepta que la carga no cachea bien).

Tarea 8: Confirma versión de Samba y si se negoció SMB3

cr0x@server:~$ smbd -V
Version 4.19.5-Debian

cr0x@server:~$ sudo smbstatus -b | sed -n '1,60p'
Samba version 4.19.5-Debian
PID     Username     Group        Machine                                   Protocol Version  Encryption           Signing
-------------------------------------------------------------------------------------------------------------------------
23144   user1        domain users  10.10.20.55 (ipv4:10.10.20.55:53122)     SMB3_11           -                    partial

Significado: Se negoció SMB3_11, bien. El signing está “partial” (depende de configuración y cliente). Si ves SMB1, tienes problemas mayores que tuning.

Decisión: Si aparece SMB1, desactívalo y arregla la compatibilidad del cliente intencionalmente. No mantengas SMB1 “por ese escáner legado”. Reemplaza el escáner o aíslelo.

Tarea 9: Revisa la configuración del share Samba en busca de killers de sync (strict sync, sync always)

cr0x@server:~$ sudo testparm -sv | sed -n '/^\[engineering\]/,/^\[/{p}'
[engineering]
	path = /tank/shares/engineering
	read only = No
	vfs objects = acl_xattr
	strict sync = Yes
	sync always = No

Significado: strict sync = Yes fuerza a Samba a vaciar más operaciones. Esta es una clásica configuración “la activamos por seguridad” que puede hundir el throughput.

Decisión: Si no tienes una necesidad de cumplimiento para semánticas estrictas, pon strict sync = No y valida la corrección de las aplicaciones. Si la necesitas, invierte en SLOG y almacenamiento de baja latencia.

Tarea 10: Verifica si signing/encryption están activados y si la CPU es el limitante

cr0x@server:~$ sudo smbstatus -b | awk 'NR==1 || NR==2 || $0 ~ /SMB3/'
Samba version 4.19.5-Debian
PID     Username     Group        Machine                                   Protocol Version  Encryption           Signing
23144   user1        domain users  10.10.20.55 (ipv4:10.10.20.55:53122)     SMB3_11           AES-128-GCM          mandatory

cr0x@server:~$ top -b -n 1 | sed -n '1,20p'
top - 12:12:41 up 34 days,  3:01,  2 users,  load average: 9.12, 8.40, 7.95
Tasks: 291 total,   2 running, 289 sleeping,   0 stopped,   0 zombie
%Cpu(s): 12.1 us,  2.0 sy,  0.0 ni, 78.0 id,  0.0 wa,  0.0 hi,  7.9 si,  0.0 st
MiB Mem :  256000.0 total,  21000.0 free,  95000.0 used, 140000.0 buff/cache

Significado: La encriptación está activada. La CPU está mayormente inactiva aquí, por lo que la encriptación probablemente no es el cuello de botella en este momento. Si ves un núcleo pegado y softirq alto, vuelve a revisar.

Decisión: Si encryption/signing es obligatorio y la CPU está caliente, actualiza CPU, usa sistemas con AES-NI, asegúrate de RSS y multiqueue, o limita la encriptación a shares sensibles.

Tarea 11: Verifica enlace NIC, duplex y contadores de errores (chequeos baratos, consecuencias caras)

cr0x@server:~$ ip -s link show dev bond0
2: bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 9c:dc:71:aa:bb:cc brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast
    1223344556677 1023344556      0       0       0  120034
    TX:  bytes packets errors dropped carrier collsns
    1334455667788 1124455667      0       0       0       0

cr0x@server:~$ ethtool bond0 | sed -n '1,25p'
Settings for bond0:
	Supported ports: [ ]
	Supported link modes:   Not reported
	Speed: 20000Mb/s
	Duplex: Full
	Auto-negotiation: off

Significado: Sin errores, full duplex, velocidad esperada. Si ves errores o drops, arregla la red antes de tocar ZFS.

Decisión: Si existen errores: revisa cableado, puertos del switch, consistencia de MTU y opciones de offload. Afinar sobre pérdida de paquetes es teatro de rendimiento.

Tarea 12: Revisa retransmisiones TCP y presión de sockets (SMB sobre una red enferma es mentira)

cr0x@server:~$ ss -s
Total: 884
TCP:   211 (estab 104, closed 72, orphaned 0, timewait 72)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  11        8         3
TCP	  139       113       26
INET	  150       121       29
FRAG	  0         0         0

cr0x@server:~$ netstat -s | sed -n '1,80p'
Tcp:
    154239 active connection openings
    149802 passive connection openings
    1124 failed connection attempts
    1821 connection resets received
    0 connections established
    224159 segments received
    231008 segments sent out
    214 segments retransmitted

Significado: Existen retransmisiones pero no son locas. Si las retransmisiones saltan durante copias, verás stalls no relacionados con almacenamiento. SMB es sensible a picos de latencia.

Decisión: Retransmisiones altas: inspecciona buffers del switch, mismatch de MTU, driver/firmware de NIC o un path de firewall sobrecargado.

Tarea 13: Identifica si la carga es sync-heavy (prueba de fsync del servidor)

cr0x@server:~$ sync; sudo bash -c 'time dd if=/dev/zero of=/tank/shares/engineering/.fsync-test bs=1M count=256 conv=fdatasync status=none'
real	0m1.92s
user	0m0.00s
sys	0m0.28s

cr0x@server:~$ sudo rm -f /tank/shares/engineering/.fsync-test

Significado: Esto mide “escribir y luego forzar durabilidad”. Si esto es lento (p. ej., 10–60s), tu pool no puede comprometer escrituras sync lo suficientemente rápido para cargas SMB que las requieran.

Decisión: fsync lento: añade/valida SLOG, reduce flushes forzados en Samba si es aceptable, o rediseña almacenamiento para escrituras de baja latencia.

Tarea 14: Confirma que el dataset no está forzando sync accidentalmente (o lo contrario)

cr0x@server:~$ sudo zfs get -H -o name,property,value sync tank/shares/engineering tank/shares/finance
tank/shares/engineering	sync	standard
tank/shares/finance	sync	always

Significado: El share finance está forzado a sync=always. Eso puede ser intencional (apps que necesitan durabilidad) o una mala configuración que lo hace lento.

Decisión: Si sync=always existe, confirma con los dueños de la app por qué. Si nadie lo justifica, vuelve a standard y prueba.

Tarea 15: Revisa compresión y ratio real (porque “activamos compresión” no es igual a “está funcionando”)

cr0x@server:~$ zfs get -H -o name,property,value compression,compressratio tank/shares/engineering
tank/shares/engineering	compression	lz4
tank/shares/engineering	compressratio	1.62x

Significado: 1.62x significa que estás ahorrando I/O y espacio. Si el ratio está ~1.00x, la compresión no ayuda mucho pero normalmente LZ4 no perjudica.

Decisión: Mantén LZ4 casi siempre. Solo desactívalo si mides saturación de CPU y datos casi incomprimibles.

Tarea 16: Busca fragmentación patológica (especialmente si el pool es antiguo y casi lleno)

cr0x@server:~$ sudo zdb -bbbs tank | sed -n '1,40p'
Block Size Histogram:
 512: 0
 1K : 0
 2K : 1048576
 4K : 2097152
 8K : 1048576
 16K: 524288
 32K: 262144
 64K: 131072
 128K: 65536
 256K: 32768
 512K: 16384
 1M : 8192

Significado: Esto es una vista aproximada; en la práctica correlacionarás fragmentación con comportamiento de asignación y latencia. Una alta diversidad de bloques pequeños en un dataset pensado para escrituras grandes secuenciales puede ser una pista.

Decisión: Si la fragmentación y el llenado son altos, planifica migración de datos o expansión del pool. ZFS es excelente, pero no se desfragmenta por desearlo.

Tres microhistorias del mundo corporativo

Microhistoria 1: El incidente causado por una suposición equivocada

Tuvieron un servidor de archivos ZFS nuevo y dos uplinks 10GbE. El despliegue se veía bien la primera semana, sobre todo porque la prueba fue “copiar un ISO de 20GB una vez” y todos se fueron a casa contentos.

Luego llegó el cierre de trimestre. Finanzas volcó miles de PDFs y hojas de cálculo pequeñas en un share desde una app Windows que insistía en write-through. Los usuarios reportaron copias “que se detenían cada pocos segundos”. El equipo de red no vio saturación, así que se declararon victoriosos y culparon a Windows. El equipo de almacenamiento vio mucha RAM libre y asumió que ARC lo suavizaría. No lo hizo.

La suposición equivocada fue sutil: “Si el pool puede hacer 1GB/s en escrituras secuenciales, puede hacer copias de archivos de oficina”. Son deportes distintos. El ancho de banda secuencial es una vuelta de victoria; la metadata sincrónica es el circuito de obstáculos.

Una vez que alguien ejecutó un simple dd ... conv=fdatasync en el dataset, quedó obvio. La latencia de commit sync era el cuello de botella. El pool era RAIDZ en HDDs. Perfecto para capacidad, terrible para durabilidad de baja latencia.

La solución también fue sutil: no desactivaron sync. Agregaron un SLOG apropiado protegido contra pérdida de energía y quitaron strict sync de los shares que no lo requerían. Finanzas mantuvo sus semánticas; engineering recuperó su velocidad. Los tickets del helpdesk se detuvieron, que es el único KPI que importa cuando estás de guardia.

Microhistoria 2: La optimización que salió mal

Otra empresa tenía copias lentas de directorios personales. Alguien leyó en un foro y decidió que “recordsize más grande = más rápido”. Así que pusieron recordsize=1M en todos los datasets SMB, incluidos directorios personales y árboles de proyectos compartidos.

Las copias de archivos grandes mejoraron ligeramente. Luego las quejas se volvieron raras: guardar documentos pequeños se sentía lento, el acceso a PST de Outlook se volvió inestable y algunas apps empezaron a “no responder” al guardar. El servidor SMB no estaba caído; simplemente hacía trabajo extra.

¿Por qué? Las sobreescrituras parciales en registros grandes pueden crear amplificación de escritura. Un pequeño cambio en un archivo puede desencadenar un read-modify-write de un bloque grande, especialmente cuando la carga es aleatoria y la app hace muchas actualizaciones pequeñas. ZFS es copy-on-write, así que ya hace contabilidad cuidadosa; añadir amplificación es como pedirle que haga malabarismos en una caminadora.

La “optimización” que salió mal también incrementó la agitación de metadata porque los perfiles de usuario generan montones de archivos diminutos y actualizaciones de atributos. Un recordsize mayor no ayudó el camino de metadata en absoluto. Solo empeoró el camino de datos.

El rollback fue disciplinado: separaron datasets por carga. Directorios personales fueron a recordsize 128K, atime off. Los archivos de medios/proyectos grandes se quedaron en 1M. El rendimiento se estabilizó. La lección perduró: afinar no es un buffet donde pones todo lo que parece apetitoso.

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

Un equipo con un cluster ZFS + Samba tenía un hábito poco glamuroso: reportes de scrub semanales y snapshots de rendimiento mensuales. No dashboards para pared ejecutiva. Solo un archivo de texto con zpool status, zpool iostat bajo carga y contadores básicos de errores NIC.

Un martes, los usuarios reportaron que las copias se habían vuelto “espigadas”. El ingeniero de guardia no adivinó. Sacó la línea base y la comparó con los números actuales. El gran cambio: la latencia de escritura en una pata del mirror había subido, y aparecían errores corregibles—lo suficiente para disparar reintentos, no para fallar el disco.

Porque tenían datos de referencia, no pasaron media jornada discutiendo sobre flags de Samba. Reemplazaron el disco durante una ventana de mantenimiento, resilverearon y los stalls de copia desaparecieron.

No pasó nada heroico. Ningún tunable mágico. Solo notar que una “regresión de rendimiento” a menudo es “hardware envejeciendo lentamente”. Esto es lo que la competencia aburrida parece en producción.

Decisiones de ajuste que realmente impactan

1) Decide explícitamente tu postura sobre sync (no dejes que te suceda)

Las cargas SMB pueden ser intensivas en sync, especialmente con ciertas aplicaciones y políticas. Tienes tres opciones:

  • Respetar sync y pagarlo: mantener sync=standard, evitar configuraciones de Samba que forcen flushes extra, y desplegar un SLOG real si se necesita.
  • Forzar siempre sync: sync=always para shares con cumplimiento estricto. Espera menor throughput; diseña almacenamiento en consecuencia.
  • Desactivar sync: sync=disabled es una decisión de negocio para arriesgar perder escrituras reconocidas en corte de energía o crash. Puede ser válido en shares temporales, pero no lo disimules como “rendimiento gratis”. Es un contrato de durabilidad distinto.

2) Separa datasets por carga (un share, un comportamiento)

Un dataset para todo es la forma más rápida de que nada funcione bien. Separa:

  • Directorios personales (intensivo en metadata, archivos pequeños)
  • Árboles de proyecto de engineering (muchos archivos pequeños, mayormente lectura)
  • Archivos de medios (grandes y secuenciales)
  • Zonas de caída de aplicaciones (pueden requerir durabilidad estricta)

Luego ajusta propiedades por dataset: recordsize, atime, sync, compresión, comportamiento de ACL.

3) Acierta el recordsize lo suficiente

  • Compartidos SMB generales: empieza con recordsize=128K.
  • Archivos grandes: considera recordsize=1M si la mayoría de archivos son grandes y secuenciales.
  • Bases de datos/VMs sobre SMB: evítalo si puedes; si debes, usa configuraciones especializadas y prueba a fondo. Servir archivos SMB y datastores de VM no es un matrimonio casual.

4) Apaga atime para shares SMB (salvo que tengas una razón real)

atime=on añade escrituras de metadata en lecturas. La mayoría de organizaciones no usa la hora de acceso para nada significativo, y Windows ciertamente no necesita que tu servidor ZFS escriba metadata extra cada vez que alguien abre un archivo.

5) Mantén LZ4 activado por defecto

LZ4 es uno de los pocos “por defecto” que defenderé en producción. A menudo mejora el throughput efectivo y reduce I/O. No lo sobrepienses hasta que tengas evidencia de saturación de CPU.

6) Usa un SLOG real cuando lo necesites (y no escatimes)

Un dispositivo SLOG no es “cualquier SSD”. Necesita baja latencia bajo carga de escrituras sync y protección contra pérdida de energía. Si no, construiste un generador de latencia caro.

7) Samba: evita “strict sync” salvo que lo justifiques

strict sync puede destruir el throughput para cargas que generan muchos puntos de fsync (incluyendo algunos comportamientos de Windows al cerrar archivos). Si necesitas semánticas estrictas, haz que el almacenamiento sea capaz. Si no, no lo pagues.

8) Signing/encryption SMB: limita su alcance

Los equipos de seguridad gustan de políticas globales. Los sistemas de producción gustan de presupuestos. Si signing/encryption debe ser obligatorio, asegura que el host SMB tenga margen de CPU y aceleración criptográfica moderna. Si solo ciertos shares contienen datos sensibles, delimita políticas por share o por segmento de tráfico.

Broma #2: Nada hace un servidor de archivos más rápido que una reunión de políticas que termina con “no cambiamos nada”.

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

1) Síntoma: La copia empieza rápida, luego cae a 0 B/s repetidamente

Causa raíz: Stalls de commit TXG por escrituras síncronas o latencia de flush lenta (sin SLOG, vdevs lentos, pool muy lleno).

Solución: Mide fsync (dd ... conv=fdatasync), verifica configuraciones sync de Samba, añade un SLOG apropiado o rediseña el pool para latencia, recupera espacio.

2) Síntoma: Archivos grandes copian bien; muchos archivos pequeños van lentos

Causa raíz: Carga limitada por metadata (ACLs, xattrs, timestamps) además de I/O aleatorio pequeño.

Solución: atime=off, asegúrate de propiedades adecuadas del dataset, considera espejos para pools intensivos en metadata, verifica que los módulos VFS de Samba no añadan sobrecarga, acepta que esto es IOPS no bandwidth.

3) Síntoma: Velocidad tope ~110 MB/s en “10GbE”

Causa raíz: Cliente/servidor negociado a 1GbE, hashing LACP malo, o flujo TCP único sin multichannel.

Solución: Revisa velocidad de enlace con ethtool, valida configuración del switch, prueba SMB multichannel y verifica que el cliente no esté en un segmento 1GbE.

4) Síntoma: Rendimiento peor tras activar signing o encriptación SMB

Causa raíz: Cuello de botella de CPU en crypto/signing, puntos calientes single-thread, colas RSS insuficientes.

Solución: Mide CPU por núcleo durante la transferencia, habilita multiqueue/RSS, actualiza CPU, delimita signing/encryption o usa hardware con aceleración.

5) Síntoma: Copias se cuelgan intermitentemente por “exactamente unos segundos”

Causa raíz: Retransmisiones de red, bufferbloat o congestión del switch; a veces la temporización de TXG alinea con las pausas percibidas.

Solución: Revisa retransmisiones (netstat -s), drops de interfaz y contadores del switch. Si está limpio, vuelve a la latencia de almacenamiento y sync.

6) Síntoma: Un share es lento; otro en el mismo servidor va bien

Causa raíz: Mismatch en propiedades del dataset (sync=always, recordsize raro, atime on), diferencias en config Samba (strict sync, módulos VFS) o cuotas/reservas afectando asignación.

Solución: Compara salidas de zfs get y bloques de testparm -sv para ambos shares. Normaliza intencionalmente.

7) Síntoma: “Windows dice que tomará 2 horas” pero el servidor parece inactivo

Causa raíz: Escaneo en el cliente (antivirus, indexación), sobrecarga de archivos pequeños o el cliente esperando por operaciones metadata por archivo.

Solución: Reproduce con un cliente limpio, prueba con opciones de robocopy y confirma métricas del servidor durante la operación. No ajustes servidores para compensar endpoints problemáticos.

Listas de verificación / plan paso a paso

Paso a paso: arreglar copias SMB “rápido y luego cero” en ZFS

  1. Confirma salud del pool: zpool status -v. Si está degradado o con errores, detente y arregla discos.
  2. Revisa llenado del pool: zfs list. Si >85% usado, planifica recuperación/expansión de espacio.
  3. Identifica dataset y propiedades: zfs get recordsize,atime,sync,compression.
  4. Inspecciona configuración del share Samba: testparm -sv buscando strict sync, ajustes aio, módulos VFS.
  5. Mide latencia sync: prueba servidor-side dd ... conv=fdatasync. Si es lenta, es tu principal sospechosa.
  6. Comprueba presencia/rendimiento de SLOG: zpool status para logs y asegurarse de que la clase de dispositivo sea apropiada.
  7. Observa latencia de disco bajo carga: iostat -x y zpool iostat mientras reproduces.
  8. Verifica salud de red: ip -s link, retransmisiones (netstat -s) y velocidad de enlace (ethtool).
  9. Aplica un cambio a la vez: por ejemplo, desactiva strict sync en un share de prueba o añade SLOG; luego repite la misma transferencia y compara.
  10. Anota el resultado: captura latencia, throughput y si los stalls desaparecieron. La memoria falla; los tickets no.

Checklist base (lo aburrido que agradecerás)

  • Scrub semanal programado; reportes de scrub revisados.
  • Snapshot mensual de: zpool status, propiedades clave de zfs get, ip -s link y una prueba repetible de throughput + fsync.
  • Layout de datasets documentado por categoría de carga.
  • Política explícita sobre sync: qué shares requieren garantías de durabilidad.
  • Control de cambios para la configuración de Samba; no “fixes de una línea” en producción a las 2am.

Preguntas frecuentes

1) ¿Por qué Explorer muestra velocidad alta y luego 0 B/s?

Explorer reporta basado en buffering y aceptación a corto plazo. ZFS y Samba pueden aceptar datos rápido y luego hacer una pausa mientras comprometen escrituras sync o TXGs. Mide latencia del servidor.

2) ¿Es robocopy más rápido que Explorer?

A veces. La ventaja mayor es que robocopy es más predecible y scriptable, y expone reintentos y comportamiento por archivo. No arreglará la latencia sync del servidor.

3) ¿Debo poner sync=disabled para hacerlo rápido?

Sólo si aceptas perder escrituras reconocidas en corte de energía o crash. Para shares temporales puede ser aceptable. Para datos de negocio es una degradación de durabilidad, no un truco de tuning.

4) ¿Necesito un SLOG para SMB?

Si tu carga genera muchas escrituras sync (o las configuraciones de Samba fuerzan flushing estricto), un buen SLOG puede ser transformador. Si la carga es mayormente async, un SLOG no ayudará mucho.

5) ¿Qué recordsize debo usar para shares SMB?

Empieza en 128K para shares de propósito general. Usa 1M para archivos grandes secuenciales. Evita cambios globales; separa datasets por carga.

6) ¿Activar LZ4 ralentiza?

Normalmente no, y con frecuencia acelera al reducir I/O. Si la CPU ya está saturada (encryption/signing, carga intensa), mide antes de decidir.

7) ¿RAIDZ es malo para SMB?

No “malo”, pero RAIDZ es menos amigable con escrituras aleatorias pequeñas y cargas intensivas en metadata que los espejos. Si tu caso SMB son muchos archivos pequeños y comportamiento sync, los mirrors suelen ganar en latencia.

8) ¿Por qué un share SMB es lento y otros están bien?

Propiedades diferentes de dataset o opciones de share Samba. Busca sync=always, atime=on, recordsize extraño o strict sync activado en un solo share.

9) ¿SMB Multichannel lo arregla todo?

No. Puede aumentar throughput y resiliencia, pero no arreglará latencia de almacenamiento o stalls por sync. Además requiere NIC, driver y soporte del cliente correctos.

10) ¿Cómo sé si estoy limitado por CPU?

Durante la transferencia, uno o más núcleos estarán consistentemente altos, a menudo en smbd o en networking/crypto del kernel. Mientras tanto, discos y NICs no estarán saturados. Ese es tu indicio.

Próximos pasos que puedes ejecutar esta semana

Haz estos en orden. Cada paso aclara una decisión, y ninguno requiere fe.

  1. Elige una prueba reproducible (un archivo grande y una carpeta de “muchos archivos pequeños”) y mantenla constante.
  2. Ejecuta la guía rápida de diagnóstico y captura salidas: zpool iostat, iostat -x, ip -s link, netstat -s, smbstatus.
  3. Prueba o elimina la latencia sync con la prueba servidor-side dd ... conv=fdatasync en el dataset.
  4. Separa datasets por carga si no lo has hecho. Ajusta atime=off y un recordsize sensato por categoría.
  5. Arregla el verdadero cuello de botella: añade un SLOG apropiado para shares sync-heavy, recupera espacio si el pool está demasiado lleno, o atiende problemas de CPU/red si la evidencia apunta ahí.
  6. Escribe un runbook de una página con tus comandos base y salidas “normales”. El tú del futuro le comprará café al tú del pasado.

El objetivo no es un gráfico perfecto. El objetivo es rendimiento predecible bajo el contrato de durabilidad que realmente quieres ofrecer. Una vez que eliges ese contrato a propósito, ZFS y SMB dejan de ser misteriosos y pasan a ser… simplemente exigentes.

Docker: Copias de seguridad que nunca probaste — Cómo ejecutar correctamente un simulacro de restauración

Tienes copias de seguridad. Incluso tienes una marca verde en algún panel. Luego un nodo muere, el on-call inicia una restauración,
y de repente lo único que estás restaurando es tu respeto por la Ley de Murphy.

Docker facilita desplazar aplicaciones. También facilita olvidar dónde vive realmente la información: volúmenes, bind mounts,
secretos, archivos env, registros y un par de directorios “temporales” que alguien codificó a las 2 a.m.

Un simulacro de restauración es un producto, no un ritual

Una “copia de seguridad” es una promesa. Un simulacro de restauración es donde amortizas esa promesa y demuestras que puedes cumplirla bajo presión.
El entregable no es un tarball en almacenamiento de objetos. Es un proceso de recuperación repetible con límites temporales conocidos.

Tu simulacro de restauración tiene un trabajo: convertir suposiciones en mediciones. ¿Cuál es tu RPO (cuánto dato puedes perder)
y RTO (cuánto tiempo puedes estar caído)? ¿Qué partes son lentas? ¿Qué partes son frágiles? ¿Qué requiere la memoria y la cafeína
de una persona específica?

El resultado más valioso de un simulacro suele ser aburrido: una lista de archivos faltantes, permisos erróneos, secretos no localizables
y sorpresas del tipo “pensamos que esto estaba en la copia”. Aburrido es bueno. Aburrido es cómo sobrevives a los incidentes.

Una cita para tener en tu escritorio: La esperanza no es una estrategia. (atribuido al Gral. Gordon R. Sullivan)

Qué estás restaurando realmente en Docker

Docker no “contiene” el estado. Solo facilita extraviar el estado. Para los simulacros de restauración, trata tu sistema en capas:
estado del host, estado del contenedor, estado de datos y estado de despliegue. Luego decide qué prometes restaurar.

1) Estado de datos

  • Volúmenes nombrados (gestionados por Docker): normalmente bajo /var/lib/docker/volumes.
  • Bind mounts: en cualquier lugar del sistema de archivos del host; a menudo no están en la misma política de backup que los volúmenes.
  • Almacenamiento externo: NFS, iSCSI, Ceph, EBS, LUNs SAN, datasets ZFS, LVM, etc.
  • Bases de datos: Postgres/MySQL/Redis/Elastic/etc. El método de respaldo importa más que su ubicación.

2) Estado de despliegue

  • Archivos Compose, archivos de entorno y overrides.
  • Secretos y su mecanismo de entrega (Swarm secrets, archivos, SOPS, plantillas de Vault, etc.).
  • Etiquetas de imagen: “latest” no es un plan de restauración.
  • Acceso al registro: si no puedes hacer pull, no puedes arrancar.

3) Estado del host

  • Configuración de Docker Engine, driver de almacenamiento, flags del daemon.
  • Detalles del kernel y sistema de archivos: expectativas de overlay2, xfs ftype, SELinux/AppArmor.
  • Red: reglas de firewall, DNS, rutas, MTU.

4) Estado de runtime (generalmente no vale la pena “restaurar”)

Las capas de contenedor y archivos efímeros de runtime pueden recrearse. Si estás haciendo backup de todo el directorio raíz de Docker
(/var/lib/docker) esperando resucitar contenedores byte a byte, te estás inscribiendo para fallos sutiles.
El objetivo correcto casi siempre es volúmenes de datos más configuración de despliegue, y reconstruir contenedores de forma limpia.

Broma #1: Si tu plan de recuperación comienza con “creo que los datos están en ese nodo”, felicitaciones: has inventado un punto único de sorpresa.

Hechos y contexto histórico (para que dejes de repetirlo)

  • Hecho 1: La era temprana de Docker con AUFS normalizó la idea de que los contenedores son desechables; muchos equipos equivocadamente hicieron los datos desechables también.
  • Hecho 2: El cambio de AUFS a overlay2 no fue solo por rendimiento: cambiaron las semánticas de restauración y los requisitos del sistema de archivos (notablemente las expectativas de XFS ftype=1).
  • Hecho 3: El movimiento de la industria hacia “infraestructura inmutable” redujo las restauraciones de host pero aumentó la necesidad de restaurar el estado externalizado (volúmenes, stores de objetos, bases de datos gestionadas).
  • Hecho 4: Compose se convirtió en la descripción de aplicaciones por defecto para muchas organizaciones, incluso cuando la rigurosidad operativa (rotación de secretos, versiones fijadas, healthchecks) no acompañó.
  • Hecho 5: Muchos incidentes atribuidos a “Docker” son en realidad problemas de coherencia de almacenamiento: copias consistentes a nivel de crash tomadas por debajo de una base de datos activa.
  • Hecho 6: El ransomware cambió la estrategia de backups de “podemos restaurar?” a “podemos restaurar sin confiar en que el atacante no cifró nuestras claves de backup?”
  • Hecho 7: Los registros de imágenes se volvieron infraestructura crítica; perder un registro privado o sus credenciales puede bloquear restauraciones aunque los datos estén a salvo.
  • Hecho 8: Los snapshots de sistemas de archivos (LVM/ZFS) facilitaron backups rápidos—pero también fomentaron la excesiva confianza cuando las aplicaciones no eran seguras para snapshots.
  • Hecho 9: El auge de contenedores sin root cambió las rutas de backup y los modelos de permisos; restaurar datos como root puede romper silenciosamente runtimes rootless más adelante.

Elige el alcance del simulacro: host, app o capa de datos

Un simulacro de restauración puede ser tres cosas distintas. Si no declaras cuál vas a hacer, “tendrás éxito” en el más fácil
y fallarás en el que importa.

Alcance A: Simulacro de restauración de datos (el más común y valioso)

Restauras volúmenes/bind-mounts y redeployas contenedores desde imágenes y configuraciones conocidas. Esta es la opción por defecto correcta
para la mayoría de los entornos de producción con Docker Compose.

Alcance B: Simulacro de restauración de la app (despliegue + datos)

Restauras la pila exacta de la aplicación: archivos Compose, env/secretos, proxy inverso, certificados, además de los datos. Esto valida
la suposición de “todo lo necesario para ejecutar”. También expone la enfermedad de “guardamos esa configuración en el portátil de alguien”.

Alcance C: Simulacro de reconstrucción del host (raro, pero hazlo al menos anualmente)

Asumes que el nodo se perdió. Provisionas un host nuevo y restauras sobre él. Aquí descubrirás dependencias de kernels antiguos, paquetes faltantes,
reglas iptables personalizadas, hacks extraños de MTU y desajustes de driver de almacenamiento.

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

Durante una restauración, típicamente estás bloqueado por una de cuatro cosas: identidad/credenciales, integridad de datos,
velocidad de transferencia o correctitud de la aplicación. No adivines. Triagia en este orden.

Primero: ¿Puedes acceder siquiera a lo que necesitas?

  • ¿Tienes las credenciales del repositorio de backups y las claves de cifrado?
  • ¿Puede el host de restauración alcanzar el object storage / servidor de backups / NAS?
  • ¿Puedes hacer pull de imágenes de contenedor (o tienes una caché air-gapped)?

Segundo: ¿El backup está completo y es internamente consistente?

  • ¿Tienes todas las rutas de volúmenes/bind-mount esperadas para la app?
  • ¿Coinciden las sumas de verificación? ¿Puedes listar y extraer archivos?
  • Para bases de datos: ¿tienes un backup lógico o solo una copia de sistema de archivos crash-consistente?

Tercero: ¿Dónde se va el tiempo?

  • ¿Rendimiento de red (egreso de object storage, restricciones VPN, throttling)?
  • ¿Descompresión y crypto (herramientas de restauración single-threaded)?
  • ¿IOPS y tormentas de restauración de archivos pequeños (millones de archivos pequeños)?

Cuarto: ¿Por qué no arranca la app?

  • Permisos/propiedad/etiquetas SELinux en los datos restaurados.
  • Divergencia de configuración: env vars, secretos, etiquetas de imagen cambiadas.
  • Incompatibilidad de esquema: restaurar datos antiguos en una versión nueva de la app.

Si solo recuerdas una cosa: mide la velocidad de transferencia y verifica las claves temprano. Todo lo demás es secundario.

Construye un entorno de restauración realista

Un simulacro de restauración en el mismo host que produjo las copias es una mentira reconfortante. Comparte las mismas imágenes en caché,
las mismas credenciales ya logueadas y las mismas reglas de firewall afinadas a mano. Tu objetivo es fallar honestamente.

Qué significa “realista”

  • Host limpio: nueva VM o hardware, misma familia de OS, mismas versiones mayores.
  • Mismas restricciones de red: misma ruta a almacenamiento de backups, mismo NAT/VPN, mismo DNS.
  • Sin estado oculto: no reutilices /var/lib/docker antiguo; no montes volúmenes de producción directamente.
  • Con límite de tiempo: estás probando RTO; deja de admirar los logs y empieza un cronómetro.

Define criterios de éxito desde el principio

  • RPO validado: puedes señalar el backup más reciente exitoso y mostrar su marca de tiempo y contenido.
  • RTO medido: desde “host provisionado” hasta “servicio responde correctamente”.
  • Correctitud verificada: no solo “los contenedores están en marcha” sino “los datos son correctos”.

Tareas prácticas: comandos, salidas, decisiones

Estas son tareas de simulacro que espero ver en un runbook. Cada una incluye un comando, qué significa su salida y
la decisión que tomas a partir de ella. Ejecútalas en el host objetivo de restauración salvo que se indique otra cosa.

Tarea 1: Inventario de contenedores en ejecución y sus montajes (entorno de origen)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES               IMAGE                         STATUS
api                 registry.local/api:1.42.0     Up 3 days
postgres            postgres:15                   Up 3 days
nginx               nginx:1.25                    Up 3 days

Significado: Esta es la lista mínima de “qué existe”. No es suficiente, pero es un comienzo.
Decisión: Identifica qué contenedores son con estado (aquí: postgres) y cuáles son sin estado.

cr0x@server:~$ docker inspect postgres --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
volume pgdata -> /var/lib/postgresql/data
bind /srv/postgres/conf -> /etc/postgresql

Significado: Tienes tanto un volumen nombrado como un bind mount. Dos políticas de backup, dos modos de falla.
Decisión: Tu plan de restauración debe capturar tanto pgdata como /srv/postgres/conf.

Tarea 2: Listar volúmenes Docker y mapearlos a proyectos

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     myapp_pgdata
local     myapp_redisdata
local     shared_uploads

Significado: Los nombres de volumen suelen codificar nombres de proyecto Compose. Eso es útil durante restauraciones.
Decisión: Decide qué volúmenes son críticos y cuáles pueden reconstruirse (p. ej., caches).

Tarea 3: Identificar dónde viven los volúmenes en disco (host de restauración)

cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/var/lib/docker

Significado: Directorio raíz por defecto de Docker. Los volúmenes estarán bajo esta ruta a menos que se configure otra cosa.
Decisión: Confirma que esto coincide con tus expectativas de backup; las discrepancias causan “restauración exitosa, datos faltantes.”

Tarea 4: Verificar sistema de archivos y espacio libre antes de restaurar

cr0x@server:~$ df -hT /var/lib/docker /srv
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/sda2      ext4   200G   32G  158G  17% /
/dev/sdb1      xfs    800G  120G  680G  15% /srv

Significado: Tienes margen de capacidad. También nota tipos de sistema de archivos; algunos comportamientos difieren para overlay y permisos.
Decisión: Si el espacio disponible es escaso, no “intentes de todas formas.” Redimensiona primero o elige un objetivo de restauración más grande.

Tarea 5: Confirmar driver de almacenamiento Docker y compatibilidad de kernel

cr0x@server:~$ docker info --format 'Driver={{.Driver}}; BackingFS={{.BackingFilesystem}}'
Driver=overlay2; BackingFS=extfs

Significado: overlay2 sobre ext4 (Docker reporta “extfs”). Si tu host original usó un driver distinto, no asumas portabilidad de /var/lib/docker.
Decisión: Prefiere restaurar solo volúmenes y configuración; reconstruye contenedores desde imágenes.

Tarea 6: Verificar que el artefacto de backup existe y es reciente

cr0x@server:~$ ls -lh /backups/myapp/
total 4.1G
-rw------- 1 root root 1.9G Jan  2 01:05 myapp-volumes-2026-01-02.tar.zst
-rw------- 1 root root 2.2G Jan  2 01:06 myapp-bindmounts-2026-01-02.tar.zst
-rw------- 1 root root  12K Jan  2 01:06 myapp-compose-2026-01-02.tgz

Significado: Artefactos separados para volúmenes, bind mounts y configuración de despliegue es saludable. Permite restauraciones parciales.
Decisión: Si el archivo más reciente es más antiguo que tu RPO, detente y escala. Restaurar datos obsoletos sin avisar es cómo los incidentes se vuelven carreras.

Tarea 7: Validar integridad del archivo antes de extraer

cr0x@server:~$ zstd -t /backups/myapp/myapp-volumes-2026-01-02.tar.zst
/backups/myapp/myapp-volumes-2026-01-02.tar.zst: OK

Significado: El stream comprimido no está corrupto.
Decisión: Si esto falla, no extraigas “algo de él.” Localiza otro set de backups o rehace la tubería de backups.

Tarea 8: Listado en seco de archivos dentro del backup (buscar rutas faltantes)

cr0x@server:~$ tar -I zstd -tf /backups/myapp/myapp-bindmounts-2026-01-02.tar.zst | head
srv/postgres/conf/postgresql.conf
srv/postgres/conf/pg_hba.conf
srv/myapp/env/.env.production
srv/nginx/conf.d/app.conf

Significado: Puedes ver archivos de configuración bind-mounted esperados.
Decisión: Si faltan directorios clave, detente y corrige la definición de backup. Los simulacros de restauración no son trucos de magia.

Tarea 9: Restaurar bind mounts primero a un prefijo de staging (evita sobreescribir)

cr0x@server:~$ mkdir -p /restore-staging
cr0x@server:~$ tar -I zstd -xpf /backups/myapp/myapp-bindmounts-2026-01-02.tar.zst -C /restore-staging
cr0x@server:~$ ls -la /restore-staging/srv/postgres/conf
total 24
drwxr-xr-x 2 root root 4096 Jan  2 01:06 .
drwxr-xr-x 3 root root 4096 Jan  2 01:06 ..
-rw-r--r-- 1 root root  980 Jan  2 01:06 pg_hba.conf
-rw-r--r-- 1 root root 3150 Jan  2 01:06 postgresql.conf

Significado: Archivos restaurados conservando permisos (-p). La propiedad importará más tarde.
Decisión: Compara staging con el layout objetivo. Solo entonces mueve a su lugar.

Tarea 10: Restaurar datos de volúmenes nombrados usando un contenedor helper

Para volúmenes nombrados, no copies a mano dentro de los internos de Docker. Usa un contenedor temporal que monte el volumen.

cr0x@server:~$ docker volume create myapp_pgdata
myapp_pgdata
cr0x@server:~$ docker run --rm -v myapp_pgdata:/data -v /backups/myapp:/backup alpine:3.20 sh -c "cd /data && tar -I zstd -xpf /backup/myapp-volumes-2026-01-02.tar.zst --strip-components=2 ./volumes/myapp_pgdata"
tar: removing leading './' from member names

Significado: Estás extrayendo solo el subárbol para ese volumen dentro de la ruta montada del volumen.
Decisión: Si el layout del archivo no coincide con lo esperado, detente y revisa el script de backup; no improvises restauraciones parciales.

Tarea 11: Comprobación de cordura del contenido y la propiedad del volumen restaurado

cr0x@server:~$ docker run --rm -v myapp_pgdata:/data alpine:3.20 sh -c "ls -la /data | head"
total 128
drwx------    19 999      999           4096 Jan  2 01:04 .
drwxr-xr-x     1 root     root          4096 Jan  2 02:10 ..
-rw-------     1 999      999              3 Jan  2 01:04 PG_VERSION
drwx------     5 999      999           4096 Jan  2 01:04 base

Significado: La propiedad es 999:999, típico de la imagen oficial de Postgres. Bien.
Decisión: Si la propiedad es incorrecta (p. ej., root), arréglalo ahora (chown) o Postgres puede negarse a arrancar.

Tarea 12: Restaurar configuración de despliegue y fijar versiones de imágenes

cr0x@server:~$ mkdir -p /opt/myapp
cr0x@server:~$ tar -xpf /backups/myapp/myapp-compose-2026-01-02.tgz -C /opt/myapp
cr0x@server:~$ ls -la /opt/myapp
total 40
drwxr-xr-x 3 root root 4096 Jan  2 02:13 .
drwxr-xr-x 3 root root 4096 Jan  2 02:13 ..
-rw-r--r-- 1 root root 2241 Jan  2 01:06 docker-compose.yml
-rw------- 1 root root  412 Jan  2 01:06 .env.production

Significado: La configuración está presente, incluido el archivo env. Trátalo como sensible.
Decisión: Asegura que las imágenes estén fijadas a etiquetas o digests que confíes. Si el Compose usa latest, arréglalo como parte del simulacro.

Tarea 13: Validar que se puedan descargar las imágenes (o que ya estén disponibles)

cr0x@server:~$ docker compose -f /opt/myapp/docker-compose.yml pull
[+] Pulling 3/3
 ✔ postgres Pulled
 ✔ api      Pulled
 ✔ nginx    Pulled

Significado: Tu ruta de registro, credenciales y red son funcionales.
Decisión: Si los pulls fallan, tu plan de restauración debe incluir un mirror del registro, tarballs de imágenes offline o pasos de recuperación de credenciales.

Tarea 14: Levantar la pila y vigilar fallos rápidos

cr0x@server:~$ docker compose -f /opt/myapp/docker-compose.yml up -d
[+] Running 3/3
 ✔ Container myapp-postgres-1  Started
 ✔ Container myapp-api-1       Started
 ✔ Container myapp-nginx-1     Started

Significado: Los contenedores arrancaron, pero esto no prueba correctitud.
Decisión: Revisa inmediatamente logs y endpoints de salud. “Started” aún puede significar “roto silenciosamente”.

Tarea 15: Verificar salud y leer los primeros errores, no los últimos

cr0x@server:~$ docker compose -f /opt/myapp/docker-compose.yml ps
NAME                IMAGE                         COMMAND                  SERVICE    STATUS          PORTS
myapp-api-1          registry.local/api:1.42.0     "gunicorn -c ..."        api        Up 20s (healthy) 0.0.0.0:8080->8080/tcp
myapp-nginx-1        nginx:1.25                    "/docker-entrypoint…"    nginx      Up 20s          0.0.0.0:80->80/tcp
myapp-postgres-1     postgres:15                   "docker-entrypoint…"     postgres   Up 20s (healthy) 5432/tcp

Significado: Las health checks están pasando. Es una señal fuerte, no una garantía.
Decisión: Si la salud falla, revisa los logs relevantes más tempranos (DB primero, luego app, luego proxy).

cr0x@server:~$ docker logs --tail=50 myapp-postgres-1
2026-01-02 02:14:12.101 UTC [1] LOG:  database system is ready to accept connections

Significado: Postgres arrancó limpiamente. Si ves “invalid checkpoint record” o “permission denied”, tu restauración no es correcta.
Decisión: Para errores de BD, decide si necesitas una restauración lógica en lugar de una copia de sistema de archivos.

Tarea 16: Demostrar correctitud con una consulta a nivel de aplicación

cr0x@server:~$ curl -fsS http://127.0.0.1:8080/health
{"status":"ok","db":"ok","version":"1.42.0"}

Significado: Tu app dice que está saludable. Ahora valida los datos, no solo la vivacidad.
Decisión: Ejecuta una consulta conocida o un chequeo de negocio (p. ej., “existe un registro de cliente específico”).

cr0x@server:~$ docker exec -i myapp-postgres-1 psql -U postgres -tAc "select now(), count(*) from users;"
2026-01-02 02:14:35.812396+00|1842

Significado: Tienes datos y parecen plausibles.
Decisión: Compara con un rango esperado o un informe de checksums. Si el conteo es cero, restauraste lo incorrecto o apuntaste a un volumen vacío.

Tarea 17: Medir el rendimiento de restauración para predecir RTO

cr0x@server:~$ /usr/bin/time -f 'elapsed=%E cpu=%P' tar -I zstd -xpf /backups/myapp/myapp-volumes-2026-01-02.tar.zst -C /restore-staging-voltest
elapsed=0:02:41 cpu=380%

Significado: La CPU es un cuello de botella (380% indica ~4 núcleos ocupados). Bueno saberlo.
Decisión: Si la restauración está limitada por CPU, puedes reducir el RTO añadiendo cores, ajustando el nivel de compresión o cambiando herramientas—antes del incidente real.

Tarea 18: Verificar permisos y contexto SELinux/AppArmor (donde aplique)

cr0x@server:~$ getenforce
Enforcing

Significado: SELinux está activo. Los bind mounts pueden necesitar etiquetado, o los contenedores verán “permission denied” a pesar de permisos UNIX correctos.
Decisión: Si usas SELinux, asegúrate de que tu Compose use :z o :Z según corresponda, y relabel las rutas restauradas.

cr0x@server:~$ ls -lZ /srv/postgres/conf | head -n 3
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 pg_hba.conf
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 postgresql.conf

Significado: El tipo SELinux por defecto en los archivos de configuración puede no coincidir con lo que espera tu contenedor.
Decisión: Si los contenedores fallan al leer bind mounts bajo SELinux, debes relabel o ajustar opciones de montaje; no desactives SELinux como “solución”.

Tarea 19: Confirma que la restauración no intercambió volúmenes silenciosamente

cr0x@server:~$ docker inspect myapp-postgres-1 --format '{{range .Mounts}}{{.Name}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
myapp_pgdata /var/lib/docker/volumes/myapp_pgdata/_data -> /var/lib/postgresql/data

Significado: El contenedor está usando el volumen esperado.
Decisión: Si ves un nombre de volumen inesperado, estás restaurando en un volumen pero corriendo contra otro. Eso es una falla clásica en simulacros.

Tarea 20: Captura evidencia y tiempos (para que el simulacro mejore)

cr0x@server:~$ journalctl -u docker --since "today" | tail -n 5
Jan 02 02:13:55 server dockerd[1023]: API listen on /run/docker.sock
Jan 02 02:14:03 server dockerd[1023]: Loading containers: done.

Significado: Tienes marcas de tiempo para el arranque del daemon Docker y la carga de contenedores.
Decisión: Registra estas en el informe del simulacro junto con inicio/fin de restauración. Si no mides, discutirás durante el incidente en lugar de solucionarlo.

Broma #2: Un simulacro de restauración es como usar hilo dental—todos dicen que lo hacen, y la evidencia suele ser sangrienta.

Tres mini-historias corporativas (cómo falla esto en la vida real)

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

Una compañía SaaS mediana ejecutaba Docker Compose en un par de VMs potentes. Sus backups eran “simples”: tar nocturno de
/srv más un snapshot semanal del disco de la VM. La suposición era que todo importante vivía en /srv.

El incidente comenzó con una falla de almacenamiento mundana. La VM no arrancó bien después de un incidente en el host. El equipo levantó
una nueva VM y restauró /srv desde el backup nocturno. Compose arrancó. Nginx sirvió páginas. La API devolvía 500s.

Los logs de Postgres mostraron que se había inicializado un clúster de base de datos vacío. Nadie lo había restaurado—porque nadie lo había respaldado.
La BD usaba un volumen nombrado de Docker, situado en la raíz de Docker bajo /var/lib/docker/volumes, fuera del alcance del backup.
El snapshot semanal de la VM lo contenía, pero era demasiado viejo para el RPO implícito de la compañía y vivía en un sistema diferente gestionado por otro equipo.

El postmortem no fue dramático. Fue peor: fue obvio. Habían confundido “nuestro directorio de datos de la app” con “dónde Docker guarda el estado”.
La solución no fue elegante tampoco: inventariar montajes, respaldar explícitamente volúmenes nombrados y ejecutar un simulacro trimestral en un host limpio.
También: deja de llamarlo “backups simples” si no incluye tu base de datos.

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

Otra organización se tomó en serio la velocidad. Su tiempo de restauración era demasiado lento para la paciencia de liderazgo, así que optimizaron.
Pasaron de dumps lógicos de BD a snapshots crash-consistentes del volumen de la base de datos. Fue más rápido y produjo transferencias incrementales más pequeñas. Todos celebraron.

Seis meses después, necesitaron restaurar. Un despliegue malo corrompió el estado de la aplicación y revirtieron. La restauración “funcionó”
mecánicamente: el snapshot se extrajo, los contenedores arrancaron, las healthchecks se pusieron en verde. Luego el tráfico subió y la BD comenzó a lanzar
errores: corrupción sutil de índices, seguida por rarezas del planer de consultas, seguida por un crash loop.

La causa raíz fue aburrida pero mortal: el snapshot se tomó mientras la base de datos tenía carga de escritura, sin coordinar un checkpoint
ni usar un mecanismo de backup nativo de la BD. El backup del volumen fue consistente a nivel de sistema de archivos, no necesariamente a nivel de base de datos.
Restauró rápido y falló tarde—exactamente el tipo de fallo que consume más tiempo.

La corrección fue un compromiso: mantener snapshots rápidos para recuperaciones cortas “ups”, pero también tomar backups nativos periódicos de BD (o correr
el procedimiento de base backup soportado) que puedan validarse. Añadieron además un trabajo de verificación que inicia una BD restaurada en un sandbox
y ejecuta checks de integridad. Las optimizaciones están permitidas. Las optimizaciones no verificadas son solo riesgo con etiqueta de rendimiento.

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

Una compañía cercana a finanzas ejecutaba varios servicios orientados al cliente en Docker. Su líder SRE no era romántico. Cada trimestre, corrían
un simulacro de restauración usando una VPC aislada, una imagen de VM limpia y una copia del repositorio de backups. El simulacro tenía una lista de verificación y un cronómetro.

El simulacro siempre incluía los mismos pasos tediosos: verificar que las claves de cifrado sean accesibles para el on-call, validar manifiestos de backup, restaurar volúmenes
en staging primero, luego intercambiarlos en su lugar, luego ejecutar un puñado de checks de cordura a nivel de aplicación. Finalmente, documentar tiempos y actualizar el runbook.
A nadie le encantaba. Nadie lo ponía en una presentación.

Entonces llegó un incidente real: un error operativo borró un volumen de producción y se replicó rápidamente. El on-call siguió el runbook sin improvisar.
Ya sabían que el paso más lento era la descompresión y habían ajustado el tamaño del host de restauración para ello. Ya sabían exactamente qué secretos debían estar presentes y dónde vivían.
Ya habían lidiado con el etiquetado SELinux—en el simulacro, no durante el incidente.

La restauración terminó dentro de la ventana esperada. No porque el equipo fuera heroico, sino porque fueron aburridos a propósito. En ops, lo aburrido es una característica.

Listas de verificación / plan paso a paso

Plan de simulacro de restauración (repetible, no “veamos qué pasa”)

  1. Declara alcance y criterios de éxito.

    • ¿Qué servicios? ¿Qué conjuntos de datos? ¿Qué RPO/RTO estás validando?
    • ¿Qué significa “correcto” (consultas, checksums, acciones UI, conteos de mensajes)?
  2. Congela el inventario.

    • Exporta archivos Compose y referencias de env/secretos.
    • Lista volúmenes y bind mounts por contenedor.
    • Registra referencias de imágenes (tags o digests).
  3. Provisiona un objetivo de restauración limpio.

    • Misma familia de OS, CPU/memoria similar, mismas elecciones de sistema de archivos.
    • Misma ruta de red a backups y registros (o explícitamente diferente, si pruebas una región DR).
  4. Obtén artefactos de backup y valida integridad.

    • Checksum, descifrar, listar contenidos, verificar marcas de tiempo.
    • Confirma que tienes claves y contraseñas en el modelo de acceso que esperas durante un incidente.
  5. Restaura primero a staging.

    • Bind mounts en /restore-staging.
    • Volúmenes vía contenedores helper en volúmenes recién creados.
  6. Aplica permisos, etiquetas y propiedad.

    • Los volúmenes de BD deben coincidir con las expectativas de UID/GID del contenedor.
    • SELinux/AppArmor: asegura etiquetas y opciones de montaje correctas.
  7. Arranca la pila fijando imágenes conocidas como buenas.

    • Haz pull de imágenes; si falla el pull, usa imágenes cacheadas/offline.
    • Arranca BD primero, luego app, luego proxies de borde.
  8. Verifica la correctitud.

    • Endpoint de salud + al menos una consulta de datos por servicio crítico.
    • Para colas/caches: verifica si necesitas restaurarlos (a menudo no).
  9. Mide tiempos y redacta el informe del simulacro.

    • Inicio/fin de restauración, throughput de transferencia, cuellos de botella, fallos, correcciones.
    • Actualiza el runbook y automatiza los pasos frágiles.

Qué automatizar después de tu primer simulacro honesto

  • Exportación de inventario: montajes, volúmenes, imágenes, configuraciones Compose.
  • Generación de manifiesto de backup: rutas y volúmenes esperados, tamaños, marcas de tiempo.
  • Checks de integridad: checksums, pruebas de archivos, restauraciones periódicas a sandbox.
  • Normalización de permisos: mapeo conocido de UID/GID por servicio.
  • Retención de imágenes: conserva imágenes requeridas para la ventana RPO (o exporta tarballs).

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

1) “Los contenedores están arriba, pero la app está vacía”

Síntoma: Las healthchecks pasan, pero faltan datos de usuario o se reiniciaron a valores por defecto.
Causa raíz: Restauraste en el nombre de volumen equivocado, o Compose creó un volumen nuevo y vacío por mismatch de nombre de proyecto.
Solución: Inspecciona montajes (docker inspect), asegura que los nombres de volumen coincidan y nombra volúmenes explícitamente en Compose en vez de depender del scope implícito del proyecto.

2) “Permiso denegado” en bind mounts restaurados

Síntoma: Contenedores fallan con errores de acceso a archivos; los archivos parecen correctos en el host.
Causa raíz: Etiquetas SELinux incorrectas, o un contenedor sin root espera otra propiedad que la restauración produjo.
Solución: Usa opciones de montaje :z/:Z

3) Postgres/MySQL arranca y luego se comporta raro bajo carga

Síntoma: La BD arranca, luego ves errores tipo corrupción o crashes.
Causa raíz: Backup crash-consistente tomado sin coordinar con la BD; estado WAL/checkpoint inconsistente.
Solución: Prefiere métodos de backup nativos de la BD para restauraciones duraderas; si usas snapshots, coordina con el modo de backup soportado por la BD y valida en sandbox.

4) La restauración es “lenta sin motivo”

Síntoma: Horas de restauración, CPU al máximo, discos infrautilizados.
Causa raíz: Descompresión/cifrado single-thread o nivel de compresión muy alto; millones de archivos pequeños amplifican operaciones de metadata.
Solución: Benchmark de descompresión, considera menor compresión o herramientas paralelas, y reestructura backups (p. ej., archivos por volumen) para reducir thrash de metadata.

5) No puedes hacer pull de imágenes durante la restauración

Síntoma: Auth al registro falla, DNS falla o las imágenes ya no existen.
Causa raíz: Credenciales guardadas solo en el host antiguo; retención del registro garbage-collectó tags en los que confiabas; dependencia de límites de rate de registros públicos.
Solución: Almacena credenciales de registro en un gestor de secretos recuperable, fija por digest o tags inmutables y guarda una caché/offline de imágenes críticas.

6) Compose “funciona en prod” pero falla en el host de restauración

Síntoma: Mismo archivo Compose, comportamiento distinto: puertos, DNS, redes, problemas de MTU.
Causa raíz: Configuración oculta del host en deriva: sysctls, iptables, módulos de kernel, daemon.json personalizado o redes específicas del cloud.
Solución: Codifica el aprovisionamiento del host (IaC), exporta y versiona configuraciones del daemon y haz un simulacro de host limpio al año.

7) El backup está presente, pero las claves no

Síntoma: Ves el archivo de backup pero no puedes descifrarlo o acceder durante la respuesta al incidente.
Causa raíz: Claves de cifrado/contraseñas bloqueadas por una persona, un portátil muerto o un SSO roto.
Solución: Practica la recuperación de claves durante los simulacros, guarda accesos de emergencia (break-glass) correctamente y verifica el procedimiento con un rol de on-call con privilegios mínimos necesarios.

8) Restauraste la configuración pero no las dependencias aburridas

Síntoma: La app arranca pero no puede enviar correo, no alcanza al proveedor de pagos o fallan callbacks.
Causa raíz: Faltan certificados TLS, reglas de firewall, registros DNS, secretos de webhook o listas de permitidos salientes.
Solución: Trata dependencias externas como parte del “estado de despliegue” y pruébalas en el simulacro (o simula explícitamente y documenta).

Preguntas frecuentes

1) ¿Debería respaldar /var/lib/docker?

Usualmente no. Haz backup de los volúmenes y de cualquier directorio bind-mounted de la aplicación, además de los configs Compose y referencias de secretos.
Respaldar todo el directorio raíz de Docker es frágil entre versiones, drivers de almacenamiento y diferencias de host.

2) ¿Cuál es la forma más segura de respaldar una base de datos en Docker?

Usa el mecanismo de backup soportado por la base de datos (dumps lógicos, base backups, archivado de WAL, etc.) y valida restaurando en un sandbox.
Los backups a nivel de sistema de archivos pueden funcionar si se coordinan correctamente, pero “pareció bien una vez” no es un método.

3) ¿Con qué frecuencia debo correr simulacros de restauración?

Trimestral para sistemas críticos es una línea base sensata. Mensual si el sistema cambia constantemente o si RTO/RPO son estrictos.
También ejecuta un simulacro tras cambios mayores: migración de almacenamiento, upgrade de Docker, upgrade de base de datos o cambio de herramientas de backup.

4) ¿Puedo ejecutar un simulacro sin duplicar datos de producción (preocupaciones de privacidad)?

Sí: usa datasets enmascarados, fixtures sintéticos o restaura en un entorno aislado cifrado con controles de acceso estrictos.
Pero aún necesitas restaurar la estructura realista: permisos, tamaños, conteos de archivos, esquema y comportamiento de runtime.

5) ¿Qué es lo que más hace explotar el tiempo de restauración?

Archivos pequeños y árboles con mucho metadata, especialmente cuando se combinan con cifrado y compresión. Puedes tener ancho de banda suficiente
y aun así estar bloqueado por CPU o IOPS.

6) ¿Debería comprimir los backups?

Usualmente sí, pero elige una compresión que coincida con tus restricciones de restauración. Si estás limitado por CPU durante la restauración, una compresión fuerte
perjudica el RTO. Mídelo con una extracción cronometrada durante los simulacros y ajusta.

7) ¿Cómo sé si restauré lo correcto?

No confíes en el estado de los contenedores. Usa checks a nivel de aplicación: ejecuta consultas de BD, verifica conteos de registros, valida un cliente/cuenta conocido
o ejecuta una transacción de negocio de solo lectura. Automatiza estos checks en el simulacro.

8) ¿Necesito restaurar Redis u otras caches?

Típicamente no—las caches son reconstruibles y restaurarlas puede reintroducir estado malo. Pero debes confirmar que la app tolera una cache vacía
y que la configuración de cache (contraseñas, TLS, políticas de maxmemory) está respaldada.

9) ¿Qué pasa con los secretos en variables de entorno?

Si tu producción depende de un archivo env, ese archivo forma parte del estado de despliegue y debe ser recuperable. Mejor: migra secretos a un manager
de secretos o al equivalente de Docker secrets, e incluye la recuperación break-glass en el simulacro.

10) ¿Puedo hacer esto con Docker Compose y aun así ser “enterprise-grade”?

Sí, si tratas Compose como un artefacto con versionado, imágenes fijadas, restauraciones probadas y gestión disciplinada del estado.
“Enterprise-grade” es un comportamiento, no una elección de herramienta.

Conclusión: pasos siguientes que puedes hacer esta semana

Si solo haces una cosa, programa un simulacro de restauración en un host limpio y mídelo. No en producción, no en tu laptop, no “alguna vez”.
Ponlo en el calendario e invita a quien sea responsable de backups, almacenamiento y la app. Quieres tener todos los modos de falla en la sala.

Luego haz estos pasos, en orden:

  1. Inventaria montajes para cada contenedor con estado y anota las rutas autorizadas y nombres de volúmenes.
  2. Separa artefactos en datos (volúmenes), bind mounts y configuración de despliegue para poder restaurar de forma quirúrgica.
  3. Valida integridad del set de backups más reciente y demuestra que tienes las claves para descifrarlo con permisos de on-call.
  4. Restaura en un sandbox y ejecuta checks de correctitud a nivel de app, no solo “el contenedor está en ejecución”.
  5. Mide RTO, identifica el paso más lento y arregla esa única cosa antes de optimizar otra cosa.

Las copias de seguridad que nunca restauraste no son backups. Son optimismo comprimido. Ejecuta el simulacro, anota qué falló y hazlo aburrido.

ZFS: ECC vs sin ECC — matemáticas del riesgo para despliegues reales

Si usas ZFS el tiempo suficiente, tarde o temprano te enfrentarás a la misma pregunta incómoda: “¿Realmente necesito memoria ECC, o es sólo folklore de quienes prefieren placas base caras?”
La respuesta honesta es aburrida y tajante: depende de tu presupuesto de riesgo, del valor de tus datos y de cómo se usa realmente tu pool ZFS —no de sensaciones, dogmas de foros ni de una captura de pantalla alarmante.

ZFS es excelente detectando corrupción. No es mágico para prevenir corrupción que ocurre antes de calcular la suma de comprobación, ni para corrupción que sucede en el lugar equivocado en el momento equivocado.
Este artículo es la matemática, los modos de fallo y el plan operativo—para que puedas tomar una decisión que puedas defender en una revisión de incidentes.

Qué cambia ECC (y qué no)

La memoria ECC (Error-Correcting Code) no es “más rápida” ni es un talismán. Es un control: detecta y corrige ciertas clases de errores de RAM (típicamente errores de un bit) y detecta (pero puede no corregir) algunos errores de múltiples bits.
Reduce la probabilidad de que una falla transitoria de memoria se convierta en basura persistente escrita al disco.

La memoria sin ECC no es “corrupción garantizada.” Es únicamente riesgo no gestionado. La mayoría de los sistemas funcionan largos periodos sin incidencias visibles.
Luego, un día, durante un scrub, un resilver, alta rotación del ARC, actualizaciones de metadata o un periodo de memoria ajustada, obtienes un error de checksum que no puedes explicar —o peor, no lo obtienes porque se checksummó lo equivocado.

Aquí está el encuadre práctico:

  • ECC reduce la incertidumbre. Aún necesitas redundancia, scrubs, backups, monitorización y restauraciones probadas.
  • ECC es más valiosa donde ZFS está más estresado. Workloads con mucha metadata, dedup, alta rotación del ARC, special vdevs y pools grandes que scrubean durante días.
  • ECC no arregla una planificación deficiente. Si tu única copia está en un solo pool, tu problema real es “sin backups,” no “sin ECC.”

Una idea para grapar a toda decisión de almacenamiento: “La esperanza no es una estrategia.” — atribuido en la cultura de operaciones a Vince Lombardi, pero trátalo como un proverbio.

Hechos y contexto histórico (útil para operar)

  1. Los errores suaves son cosa conocida. “Los rayos cósmicos voltean bits” suena a ciencia ficción, pero se ha medido en flotas de producción durante décadas.
  2. La densidad de DRAM volvió los errores más relevantes. Al achicar las celdas, el margen para ruido y fuga de carga se redujo; las tasas de error se hicieron más visibles a escala.
  3. ECC se volvió estándar en servidores porque el tiempo de actividad cuesta dinero. No porque los servidores sean moralmente superiores, sino porque las fallas tienen facturas asociadas.
  4. ZFS popularizó checksums de extremo a extremo para admins mainstream. Las sumas de comprobación de datos y metadata no son exclusivas de ZFS, pero ZFS las hizo operativamente accesibles.
  5. Los scrubs son un cambio cultural. RAID tradicional a menudo descubría la degradación sólo durante un rebuild; ZFS normaliza “leer todo periódicamente y verificar”.
  6. Copy-on-write cambia el radio de daño. ZFS no sobrescribe en el lugar, lo que reduce algunos patrones de corrupción pero introduce otros (especialmente alrededor de las actualizaciones de metadata).
  7. Dedup fue una lección de humildad. La dedup de ZFS puede funcionar, pero es una característica que consume mucha memoria y convierte pequeños errores en grandes caídas.
  8. El “NAS de consumo” creció. Laboratorios domésticos y PYMEs empezaron a ejecutar pools ZFS con expectativas empresariales, a menudo sobre RAM y placas de consumo.

Dónde los errores de memoria afectan a ZFS: un modelo de fallo

1) El problema del tiempo del checksum

ZFS protege bloques con checksums almacenados por separado. Bien. Pero existe una ventana temporal: la suma se calcula sobre datos en memoria.
Si los datos están corruptos antes de calcular la suma, ZFS calcula fielmente la suma de comprobación de los bytes corruptos y escribe ambos. Eso no es “corrupción silenciosa dentro de ZFS”; es “datos incorrectos válidamente checksummados.”

ECC ayuda reduciendo la probabilidad de que los bytes que alimentan la suma estén equivocados.
Sin ECC estás apostando a que los errores transitorios no caerán en esa ventana con demasiada frecuencia.

2) La metadata es donde se arruina tu día

La corrupción de datos es dolorosa. La corrupción de metadata es existencial. La metadata de ZFS incluye punteros de bloque, spacemaps, metadata de asignación, estructuras MOS, dnodes y más.
Un bit malo en la metadata puede significar:

  • un problema de importación del pool irrecuperable
  • un dataset que no monta
  • un objeto que apunta al bloque equivocado
  • un resilver que se comporta “raro” porque sigue punteros dañados

ZFS es resistente, pero no inmune. Tu redundancia (mirror/RAIDZ) ayuda si la corrupción está en disco y es detectable.
Si se escribe la metadata equivocada, la redundancia puede replicar el error porque es una escritura lógicamente consistente.

3) ARC, rotación por expulsión y “la RAM como multiplicador de fallos”

ARC es la caché en memoria de ZFS. Es una característica de rendimiento, pero también un lugar donde un bit volteado puede amplificarse:
los datos cacheados erróneos pueden servirse, reescribirse o usarse para construir estado derivado.

Bajo presión de memoria, ARC expulsa agresivamente. Esa rotación aumenta la cantidad de transacciones de memoria y la cantidad de datos tocados.
Más datos tocados significa más oportunidades para que una falla importe.

4) Special vdevs y aceleración de bloques pequeños de metadata

Los special vdevs (a menudo mirrors SSD que alojan metadata y bloques pequeños) son un cohete de rendimiento y una trampa de fiabilidad.
Si pierdes ese vdev y no tienes redundancia, puedes perder el pool. Si se corrompe lo que va allí y la corrupción está válidamente checksummada, puedes perder integridad en las estructuras más importantes.

5) Scrub, resilver y las fases de “alta lectura”

Scrubs y resilvers leen mucho. También estresan la tubería: CPU, memoria, HBA, cableado, discos.
Son cuando salen a la luz problemas latentes.
Si ejecutas sin ECC, estas operaciones son tu sorteo de lotería, porque pasan volúmenes masivos de datos a través de la RAM.

Broma #1: Si tu calendario de scrub es “cuando me acuerdo”, felicitaciones—has inventado el bit rot de Schrödinger.

Matemática del riesgo aplicada a despliegues reales

La mayoría de los debates sobre ECC se atascan en absolutos: “Debes tenerlo” frente a “Nunca he tenido un problema.”
Las decisiones de producción viven en probabilidades y costes. Así que modelemos de forma útil.

La ecuación central: tasa × exposición × consecuencia

No necesitas la tasa exacta de volteo de bits por rayos cósmicos de tus DIMMs para hacer matemáticas útiles. Necesitas:

  • Tasa de error (R): con qué frecuencia ocurren errores de memoria (corregibles o no). Varía por hardware, antigüedad, temperatura y calidad del DIMM.
  • Exposición (E): cuánta data y metadata pasa por la memoria de forma “peligrosa” (escrituras, actualizaciones de metadata, ventanas de checksum, pipelines de scrub/resilver).
  • Consecuencia (C): cuánto cuesta cuando algo sale mal (desde “un archivo incorrecto” hasta “pool que no importa”).

Tu riesgo no es “R.” Tu riesgo es R × E × C.

El riesgo no se distribuye igual entre workloads

Un archivo de medios que es mayormente de sólo lectura tras la ingestión tiene un perfil de exposición distinto a:

  • un datastore de VM con churn constante
  • una base de datos con latencia estricta y escrituras síncronas
  • un destino de backups que procesa flujos secuenciales grandes y poda frecuente
  • un entorno con mucha dedup que convierte la metadata en tus datos más calientes

Define tu “unidad de pérdida”

Deja de discutir en abstracto. Decide qué significa pérdida para ti:

  • Unidad A: un archivo corrupto que se restaura desde backup (molesto)
  • Unidad B: una VM con corrupción de sistema de ficheros (doloroso)
  • Unidad C: fallo de importación del pool, restauración de varios días y un postmortem con directivos (consecuencias profesionales)

ECC reduce sobre todo la probabilidad de eventos Unidad B/C. No se trata de tu colección de MP3; se trata del radio de daño.

Los backups reducen la consecuencia, no la probabilidad

Backups sólidos reducen C. ECC reduce R.
Si tienes ambos, obtienes un beneficio multiplicativo: menos incidentes y más baratos.

Por qué “checksums de ZFS hacen innecesario ECC” es un atajo erróneo pero común

Los checksums de ZFS te protegen cuando:

  • el disco devuelve datos equivocados
  • fallos en el cableado/HBA corrompen bits en tránsito desde el disco
  • ocurre degradación de sectores en disco

Los checksums de ZFS no garantizan protección cuando:

  • datos malos son checksummados y escritos
  • punteros de metadata se corrompen antes del checksum
  • tu aplicación escribe basura y ZFS la preserva fielmente

ECC es un control aguas arriba que reduce la probabilidad de que “datos malos se vuelvan verdad.”

Entonces, ¿cuál es la recomendación real?

Si tu pool contiene datos empresariales, datos irremplazables o datos cuya corrupción es difícil de detectar a nivel de aplicación, ECC es la opción predeterminada correcta.
Sin ECC puede ser defendible para:

  • caches desechables
  • réplicas secundarias donde la integridad primaria está protegida
  • laboratorios domésticos donde el tiempo de inactividad es aceptable y los backups son reales (probados)
  • almacenamiento frío donde la ingestión está controlada y verificada

Si tu plan es “me daré cuenta de la corrupción”, estás asumiendo que la corrupción es ruidosa. A menudo no lo es.

Cuándo sin ECC es aceptable (y cuándo es temerario)

Aceptable: puedes tolerar datos incorrectos y restaurar rápidamente

Sin ECC puede estar bien cuando:

  • tus datos están replicados en otro lugar (y verificas las réplicas)
  • puedes destruir y reconstruir el pool desde la fuente de verdad
  • tu host ZFS no realiza trabajo intensivo en metadata (sin dedup, sin special vdevs)
  • scrubeas regularmente y monitorizas tendencias de error

Temerario: el pool es la fuente de verdad

Sin ECC es una mala apuesta cuando:

  • tienes un pool con la única copia de datos de producción
  • usas ZFS para almacenamiento de VM con escrituras constantes y snapshots
  • habilitaste dedup porque “ahorra espacio”
  • estás cerca de los límites de memoria y ARC está constantemente bajo presión
  • ejecutes special vdevs sin redundancia, o con SSDs de consumo sin protección contra pérdida de energía

En esos escenarios, ECC es barato comparado con el primer incidente donde tengas que explicar por qué los datos son “consistentes pero equivocados.”

Tres microhistorias corporativas desde la trinchera

Microhistoria 1: El incidente causado por una suposición equivocada

Una compañía mediana ejecutaba un clúster de VM respaldado por ZFS para servicios internos. Los hosts eran máquinas de escritorio reutilizadas: muchos núcleos, mucha RAM, sin ECC.
El ingeniero de almacenamiento había pedido placas de servidor, pero compras oyó “ZFS tiene checksums” y lo tradujo por “ECC es opcional.”

Todo parecía bien hasta una ventana de mantenimiento rutinaria: una actualización del kernel, reinicio y luego un scrub programado se lanzó automáticamente.
A mitad del scrub, un host empezó a registrar errores de checksum. No muchos. Lo suficiente para inquietar. El pool siguió en línea, el scrub terminó y el equipo lo archivó como “un disco inestable.”

En las dos semanas siguientes aparecieron problemas esporádicos en aplicaciones: la base de datos SQLite de un servicio empezó a devolver errores de “malformado”. El sistema de archivos de otra VM necesitó reparaciones tras un apagado no limpio.
El equipo persiguió pistas falsas: latencia de almacenamiento, fallos de red, un SSD sospechoso.

El punto de quiebre fue cuando compararon backups: restaurar la misma imagen de VM desde dos snapshots distintos produjo checksums diferentes en algunos bloques.
Eso no es “degradación de disco”; es “algo escribió verdades inconsistentes en momentos diferentes.”

Tras un análisis doloroso, hallaron un patrón: los errores de checksum aparecían durante actividad intensa de memoria. Los logs mostraron síntomas tipo MCE en una máquina, pero nada concluyente porque la plataforma no exponía bien la telemetría de memoria.
Reemplazar los DIMMs redujo los errores, pero no reconstruyó la confianza. Cambiaron la plataforma por sistemas con ECC y añadieron pruebas mensuales de restauración.

La suposición equivocada no fue “sin ECC siempre corrompe.” Fue “los checksums hacen irrelevante la corrección aguas arriba.”
Los checksums detectan mentiras. No impiden que las escribas.

Microhistoria 2: La optimización que salió mal

Otro equipo usaba ZFS para un repositorio de backups. La presión de espacio era real, así que alguien sugirió dedup + compresión. En teoría era genial: los backups son repetitivos, la dedup debería destacar y ZFS lo trae incorporado.
Habilitaron dedup en un dataset grande y vieron crecer el ahorro. Todos se sintieron inteligentes.

Luego llegaron las quejas de rendimiento. Las ventanas de ingestión se alargaron. La caja empezó a intercambiar (swap) bajo carga.
El equipo reaccionó ajustando ARC y añadiendo un SSD rápido para L2ARC, intentando “cachear la salida.” También aumentaron recordsize buscando throughput.

Lo que no internalizaron: dedup empuja una enorme cantidad de metadata a memoria. La DDT (tabla de dedup) es voraz. Bajo estrés de memoria, todo se vuelve más lento y el sistema más vulnerable a casos límite.
Ejecutaban sin ECC porque “son sólo backups” y porque la plataforma era un appliance optimizado por coste.

La falla no fue inmediata, por eso fue tan didáctica. Tras unos meses, un scrub encontró errores de checksum en bloques de metadata.
Las restauraciones empezaron a fallar para subconjuntos de backups—el peor tipo de falla, porque los backups existían pero no eran fiables.

El rollback tomó semanas: deshabilitar dedup para datos nuevos, migrar backups críticos a un pool nuevo y ejecutar verificación completa de restauración en los conjuntos más importantes.
La optimización no era malvada; estaba mal emparejada con el hardware y la madurez operacional.

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

Un grupo financiero ejecutaba ZFS en un par de servidores de almacenamiento con RAM ECC, special vdevs espejados y un calendario que nadie discutía: scrub semanal, pruebas SMART extendidas mensuales y simulacros de restauración trimestrales.
La configuración era casi ofensivamente poco glamorosa. Sin dedup. Sin tunables exóticos. Sólo mirrors y disciplina.

Un trimestre, durante un simulacro, notaron que una restauración fue más lenta de lo esperado y el host receptor registró un puñado de errores de memoria corregidos.
Nada se cayó. No se perdió datos. Pero existía la telemetría, y el simulacro obligó al equipo a revisarla mientras nadie tenía un incendio real.

Cambiaron el DIMM proactivamente, luego hicieron otro simulacro y un scrub. Limpio.
Dos semanas después, el DIMM gemelo (mismo lote) empezó a reportar errores corregidos en otro servidor. Lo reemplazaron también.

Lo divertido es lo que no ocurrió: ningún incidente de cliente, ninguna corrupción de pool, ninguna reunión de “¿cuánto tiempo ha pasado esto?”
ECC no “salvó el día” solo. La práctica aburrida sí: observar errores corregidos, tratarlos como señal de degradación de hardware y validar restauraciones mientras era un evento de calendario, no una crisis.

Guía rápida de diagnóstico: encuentra el cuello de botella

Cuando ZFS empieza a comportarse mal—errores de checksum, scrubs lentos, bloqueos aleatorios—puedes perder días discutiendo sobre ECC como si fuera teología.
Esta guía es para el momento en que necesitas respuestas rápidas.

Primero: confirma qué tipo de fallo es

  • Fallo de integridad: errores de checksum, archivos corruptos, aumento de errores del pool.
  • Fallo de disponibilidad/rendimiento: bloqueos de I/O, scrub que tarda una eternidad, latencia alta, timeouts.
  • Presión de recursos: swapping, OOM kills, thrash del ARC, saturación de CPU.

Segundo: aisla “camino de disco” vs “camino de memoria/CPU”

  • Si zpool status muestra errores de checksum en un dispositivo específico, sospecha disco/cable/HBA primero.
  • Si los errores aparecen en múltiples dispositivos a la vez, sospecha HBA, backplane, RAM o CPU.
  • Si el pool está limpio pero las apps ven corrupción, sospecha bugs a nivel de aplicación, RAM o la capa de red sobre el almacenamiento.

Tercero: decide si puedes mantener el sistema en línea

  • Errores de memoria corregibles son una advertencia. Normalmente puedes seguir en línea, pero planifica una ventana de mantenimiento.
  • Errores no corregibles o aumento de errores de checksum: detén escrituras, snapshot lo que puedas y planifica un failover/restauración controlada.
  • Resilver/scrub en hardware inestable: arriesgado. Arregla la plataforma primero si puedes.

Tareas prácticas: comandos, salidas y decisiones

Estas son tareas reales que puedes ejecutar en Linux con OpenZFS. Cada una incluye qué buscar y la decisión asociada.
(Si usas FreeBSD, los comandos difieren, pero la lógica operacional es la misma.)

Task 1: Comprobar salud del pool y contadores de error

cr0x@server:~$ zpool status -v tank
  pool: tank
 state: ONLINE
status: One or more devices has experienced an unrecoverable error.
action: Determine if the device needs to be replaced, and clear the errors
  scan: scrub repaired 0B in 05:12:44 with 3 errors on Sun Dec  8 03:20:55 2025
config:

        NAME                        STATE     READ WRITE CKSUM
        tank                        ONLINE       0     0     0
          raidz1-0                  ONLINE       0     0     0
            ata-WDC_WD80...-part1   ONLINE       0     0     3
            ata-WDC_WD80...-part1   ONLINE       0     0     0
            ata-WDC_WD80...-part1   ONLINE       0     0     0
            ata-WDC_WD80...-part1   ONLINE       0     0     0

errors: Permanent errors have been detected in the following files:

        /tank/vmstore/vm-112-disk-0.qcow2

Qué significa: Errores CKSUM en un solo disco suelen indicar un disco, cable, puerto HBA o backplane defectuoso. “Permanent errors” significa que ZFS no pudo reconstruir algunos bloques.

Decisión: Si la redundancia no puede sanar, restaura el archivo afectado desde backup/snapshot. Luego investiga la ruta del dispositivo (SMART, cableado). No “limpies y olvides.”

Task 2: Mostrar propiedades detalladas del pool que afectan integridad y recuperación

cr0x@server:~$ zpool get ashift,autotrim,autoexpand,autoreplace,listsnapshots tank
NAME  PROPERTY       VALUE   SOURCE
tank  ashift         12      local
tank  autotrim       off     default
tank  autoexpand     off     default
tank  autoreplace    off     default
tank  listsnapshots  off     default

Qué significa: ashift afecta amplificación de escritura y rendimiento. No arreglará problemas de ECC, pero un ashift incorrecto puede hacer scrubs/resilvers terriblemente largos.

Decisión: Si ashift es incorrecto para tus discos, planifica una migración (no un cambio rápido). Si los scrubs tardan días, tu ventana de exposición crece—otra razón para valorar ECC.

Task 3: Confirmar calendario de scrub y resultado del último scrub

cr0x@server:~$ zpool status tank | sed -n '1,20p'
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 05:12:44 with 3 errors on Sun Dec  8 03:20:55 2025
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          raidz1-0  ONLINE       0     0     0

Qué significa: Tienes un scrub reciente y encontró errores. Scrub es tu sistema de alerta temprana; trátalo como tal.

Decisión: Si los scrubs encuentran rutinariamente nuevos errores de checksum, deja de asumir que es “aleatorio.” Trendéalo y escala a triage de hardware.

Task 4: Revisar logs de ZFS y mensajes del kernel alrededor de I/O

cr0x@server:~$ dmesg -T | egrep -i 'zfs|checksum|ata|sas|mce|edac' | tail -n 20
[Sun Dec  8 03:21:12 2025] ZFS: vdev I/O error, zpool=tank, vdev=/dev/sdb1, error=52
[Sun Dec  8 03:21:12 2025] ata3.00: status: { DRDY ERR }
[Sun Dec  8 03:21:12 2025] ata3.00: error: { UNC }
[Sun Dec  8 03:21:13 2025] mce: [Hardware Error]: CPU 0: Machine Check: 0 Bank 8: b200000000070005

Qué significa: Errores mixtos de I/O de almacenamiento y entradas MCE son una bandera roja. No asumas que el disco es culpable si la CPU reporta machine checks.

Decisión: Si MCE/EDAC sugiere problemas de memoria, prioriza la estabilidad de RAM/plataforma antes de ejecutar otro scrub/resilver que pueda escribir nueva “verdad.”

Task 5: Verificar que ECC esté realmente habilitado y reconocido

cr0x@server:~$ sudo dmidecode -t memory | egrep -i 'error correction|ecc|type:|manufacturer' | head -n 20
        Error Correction Type: Multi-bit ECC
        Type: DDR4
        Manufacturer: Micron Technology
        Error Correction Type: Multi-bit ECC
        Type: DDR4
        Manufacturer: Micron Technology

Qué significa: La plataforma reporta capacidad ECC. Esto no garantiza que Linux reciba eventos EDAC, pero es una línea base necesaria.

Decisión: Si reporta “None” u “Unknown”, deja de decirte que tienes ECC “porque los DIMMs son ECC.” También necesitas una placa/CPU que lo utilice.

Task 6: Comprobar contadores EDAC para errores corregidos/no corregidos

cr0x@server:~$ sudo edac-util -v
edac-util: EDAC drivers are loaded. 1 MC detected:
  mc0: 2 Uncorrected Errors with no DIMM info
  mc0: 41 Corrected Errors with no DIMM info

Qué significa: Errores corregidos indican que ECC está haciendo trabajo. Errores no corregidos significan fallos de memoria que ECC no pudo arreglar.

Decisión: Cualquier error no corregido: programa mantenimiento inmediato y reemplaza DIMMs/sockets/placa sospechosos. Errores corregidos en aumento: considéralos una señal de pre-fallo y planifica reemplazo.

Task 7: Inspeccionar detalle por DIMM (cuando esté disponible)

cr0x@server:~$ sudo ras-mc-ctl --summary
Summary of memory errors:
Location: mc0/csrow0/channel0/dimm0
  Corrected: 37
  Uncorrected: 0
Location: mc0/csrow0/channel1/dimm0
  Corrected: 4
  Uncorrected: 0

Qué significa: Los errores están localizados en un DIMM/canal, exactamente lo que quieres para un reemplazo accionable.

Decisión: Reemplaza el DIMM con el mayor conteo de errores corregidos primero, y vuelve a comprobar. Si los errores siguen en el mismo canal, sospecha slot o controlador de memoria.

Task 8: Confirmar que ZFS no esté usando dedup accidentalmente

cr0x@server:~$ zfs get -r dedup tank
NAME                PROPERTY  VALUE  SOURCE
tank                dedup     off    default
tank/vmstore        dedup     off    default
tank/backups        dedup     off    default

Qué significa: Dedup está desactivado, bien. Si está activado en alguna parte, la presión de memoria y la sensibilidad de la metadata aumentan drásticamente.

Decisión: Si encuentras dedup habilitado sin justificación y plan de dimensionamiento, desactívalo para escrituras nuevas (set dedup=off) y planifica migración de datos antiguos si es necesario.

Task 9: Comprobar tamaño del ARC y señales de presión de memoria

cr0x@server:~$ arc_summary | egrep -i 'arc size|target size|memory|evict' | head -n 12
ARC size (current):                                   27.4 GiB
Target size (adaptive):                               30.1 GiB
Min size (hard limit):                                8.0 GiB
Max size (high water):                                32.0 GiB
Evict skips:                                          0
Demand data hits:                                     89.3%

Qué significa: El ARC es grande y estable. Si ves expulsiones constantes, bajas tasas de aciertos o la caja intercambiando, estás en un estado de alta rotación donde las fallas duelen más.

Decisión: Si hay thrash del ARC o swap, reduce la carga, añade RAM o limita el ARC. No hagas resilvers en un host que esté intercambiando y volviéndose inestable.

Task 10: Comprobar swapping y presión de reclaim

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            64Gi        58Gi       1.2Gi       1.0Gi       4.8Gi       2.6Gi
Swap:           16Gi        12Gi       4.0Gi

Qué significa: Uso activo de swap en un host de almacenamiento es un olor a problema de rendimiento y, de forma indirecta, un amplificador de riesgo de integridad (más rotación, más estrés durante operaciones críticas).

Decisión: Encuentra qué consume memoria (VMs, dedup, workloads con mucha metadata). Añade RAM o reduce alcance. Si no puedes añadir ECC, al menos evita operar caliente y con swap.

Task 11: Verificar SMART y errores UDMA CRC (el cableado habla)

cr0x@server:~$ sudo smartctl -a /dev/sdb | egrep -i 'reallocated|pending|offline_uncorrectable|udma_crc_error_count' 
197 Current_Pending_Sector  0x0012   100   100   000    Old_age   Always       -       0
198 Offline_Uncorrectable   0x0010   100   100   000    Old_age   Offline      -       0
199 UDMA_CRC_Error_Count    0x003e   200   199   000    Old_age   Always       -       12

Qué significa: Errores UDMA CRC implican normalmente cables/backplanes más que el medio. Errores de checksum de ZFS que correlacionan con incrementos de CRC suelen ser “datos mangled en tránsito.”

Decisión: Reemplaza cables, reseat conexiones, revisa backplane/puerto HBA. Luego scrubea otra vez para confirmar estabilidad.

Task 12: Identificar si los errores de checksum son nuevos o históricos

cr0x@server:~$ zpool status -v tank | tail -n 15
errors: Permanent errors have been detected in the following files:

        /tank/vmstore/vm-112-disk-0.qcow2

Qué significa: “Permanent errors” persisten hasta que restauras/sobrescribes los bloques afectados. Limpiar errores no arregla los datos.

Decisión: Restaura el archivo desde un snapshot/backup conocido bueno o bórralo y regenera. Luego usa zpool clear sólo después de la remediación.

Task 13: Mapear un problema a nivel de bloque a snapshots e intentar auto-sanación

cr0x@server:~$ zfs list -t snapshot -o name,creation -S creation tank/vmstore | head
NAME                                CREATION
tank/vmstore@hourly-2025-12-08-0300  Sun Dec  8 03:00 2025
tank/vmstore@hourly-2025-12-08-0200  Sun Dec  8 02:00 2025
tank/vmstore@daily-2025-12-07        Sat Dec  7 23:55 2025

Qué significa: Tienes snapshots para revertir o clonar, que son tu camino más rápido a la corrección.

Decisión: Si un archivo está marcado como permanentemente corrupto, restaura desde el snapshot conocido bueno más reciente y valida a nivel de aplicación.

Task 14: Forzar una lectura dirigida para sacar a la luz errores latentes

cr0x@server:~$ sudo dd if=/tank/vmstore/vm-112-disk-0.qcow2 of=/dev/null bs=16M status=progress
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 7 s, 307 MB/s
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 14 s, 305 MB/s
...output...

Qué significa: Una lectura secuencial completa puede disparar la verificación de checksum y mostrar si los errores reaparecen. No sustituye al scrub, pero es una herramienta rápida de triage para un objeto específico.

Decisión: Si las lecturas generan nuevos errores de checksum, trata la ruta subyacente como inestable; no esperes al próximo scrub semanal para enterarte de lo que ya sabes.

Task 15: Comprobar throughput de scrub/resilver e identificar si estás limitado por CPU o I/O

cr0x@server:~$ iostat -x 2 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.31    0.00    6.22   21.10    0.00   60.37

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   w_await aqu-sz  %util
sdb              84.0  10432.0     0.0    0.0   28.4   124.2        3.0     64.0    2.1    2.40   98.0
sdc              82.0  10240.0     0.0    0.0   29.1   124.9        2.0     48.0    1.9    2.35   97.5

Qué significa: Alto %iowait y utilización cercana al 100% del disco sugiere que el scrub está limitado por disco. Si la CPU estuviera al máximo y los discos ociosos, estarías limitado por CPU/checksum.

Decisión: Disco limitado: revisa layout de vdev, ashift, salud de drives y cableado. CPU limitada: considera CPU más rápida, habilitar offloads de checksum si aplica, o reducir recordsize/rotación de metadata.

Task 16: Confirmar redundancia del special vdev (si usas uno)

cr0x@server:~$ zpool status tank | sed -n '1,80p'
  pool: tank
 state: ONLINE
config:

        NAME                       STATE     READ WRITE CKSUM
        tank                       ONLINE       0     0     0
          raidz2-0                 ONLINE       0     0     0
            sda1                   ONLINE       0     0     0
            sdb1                   ONLINE       0     0     0
            sdc1                   ONLINE       0     0     0
            sdd1                   ONLINE       0     0     0
          special                  ONLINE       0     0     0
            mirror-1               ONLINE       0     0     0
              nvme0n1p1            ONLINE       0     0     0
              nvme1n1p1            ONLINE       0     0     0

Qué significa: El special vdev está espejado. Esa es la línea mínima de seguridad si pones metadata allí.

Decisión: Si special es un único dispositivo, arréglalo antes de optimizar otra cosa. Un special vdev único es un punto único de fallo del pool.

Broma #2: Ejecutar ZFS con dedup activada en no-ECC es como hacer malabares con motosierras porque “ahorra pasos.”

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

1) Errores de checksum “aleatorios” en múltiples discos

  • Síntoma: CKSUM incrementa en más de un disco, a veces diferentes discos en distintos días.
  • Causa raíz: Problema de ruta compartida (HBA, backplane, alimentación, cables) o inestabilidad de memoria/CPU causando datos malos escritos/validados.
  • Solución: Revisa SMART CRC, cambia cables/puertos, actualiza firmware del HBA, revisa logs MCE/EDAC, ejecuta memtest en mantenimiento y detén escrituras hasta estabilizar.

2) “ZFS dice reparado, pero la app sigue rota”

  • Síntoma: El scrub reporta reparaciones, pero la base de datos/formatos de archivos siguen quejándose.
  • Causa raíz: ZFS reparó bloques corruptos desde la redundancia, pero el estado a nivel de aplicación ya pudo haber incorporado escrituras malas (especialmente si la corrupción fue pre-checksum).
  • Solución: Restaura desde backups o snapshots consistentes a nivel de aplicación. Añade checksums a nivel de app donde sea posible (las bases de datos a menudo los tienen).

3) Los scrubs están limpios, pero aún no confías en el pool

  • Síntoma: No hay errores ZFS, pero tuviste crashes inexplicables, kernel panics o reportes de corrupción de ficheros.
  • Causa raíz: Inestabilidad de memoria que afecta cómputo y comportamiento de la aplicación más que las lecturas de disco, o corrupción que ocurre antes de que los datos lleguen a ZFS.
  • Solución: Revisa EDAC/MCE, ejecuta pruebas de memoria, verifica PSU y temperatura, valida con checksums end-to-end de la aplicación y considera ECC si esto es fuente de verdad de almacenamiento.

4) “Borramos errores y ahora está bien”

  • Síntoma: Alguien ejecutó zpool clear y declaró victoria.
  • Causa raíz: Confundir contadores con corrupción. Limpiar reinicia el reporte, no la realidad.
  • Solución: Identifica y remedia archivos dañados (restaurar/sobrescribir). Limpia sólo después de arreglar datos y estabilizar hardware.

5) Pool no importa después de un evento de energía

  • Síntoma: La importación falla o se cuelga tras una pérdida abrupta de energía.
  • Causa raíz: Problemas de hardware/firmware, memoria mala o ruta de almacenamiento inestable expuestos por la intensa reproducción y operaciones de metadata al arrancar.
  • Solución: Valida RAM (logs ECC o memtest), revisa firmware HBA, asegúrate de manejo correcto de pérdida de energía (UPS) y documenta y prueba entornos de arranque y procedimientos de recuperación.

6) “Añadimos RAM y ahora tenemos errores”

  • Síntoma: Los errores comienzan tras ampliar RAM.
  • Causa raíz: Mezcla de tipos/timings de DIMM, DIMM marginal, ajustes BIOS incorrectos o una placa que no puede manejar la configuración de forma fiable.
  • Solución: Usa configuraciones de memoria validadas, actualiza BIOS, reduce la velocidad a ajustes estables y vigila contadores EDAC. Reemplaza DIMMs sospechosos pronto.

Listas de verificación / plan paso a paso

Lista de decisión: ¿debe este sistema ZFS usar ECC?

  1. ¿Es este pool una fuente de verdad? Si sí, por defecto usa ECC.
  2. ¿Es difícil detectar la corrupción? Imágenes de VM, bases de datos, fotos, datos científicos: sí. Por defecto usa ECC.
  3. ¿Usas dedup, special vdevs o snapshots intensos? Si sí, ECC fuertemente recomendado.
  4. ¿Puedes restaurar rápido y lo has probado? Si no, ECC no te salvará, pero sin ECC te hará más daño.
  5. ¿Tienes telemetría de errores de memoria? Si no, vuelas a ciegas—prefiere plataformas ECC con visibilidad EDAC.

Lista operacional: si debes ejecutar sin ECC

  1. Manténlo simple: mirrors/RAIDZ, sin dedup, evita special vdevs de un solo dispositivo.
  2. Ejecuta scrubs regulares y alerta ante nuevos errores de checksum inmediatamente.
  3. Mantén margen de memoria: evita swap; limita ARC si es necesario.
  4. Usa checksums a nivel de aplicación donde sea posible (checks de BD, hashes para archivos).
  5. Tén backups verificados: pruebas periódicas de restauración, no “tenemos backups en algún lugar.”
  6. Mantén un plan de repuestos de hardware: cables conocidos buenos, HBA de repuesto, disco de repuesto y procedimiento documentado de reemplazo.

Paso a paso: responder a los primeros errores de checksum

  1. Congela suposiciones: no declares “disco malo” aún.
  2. Captura la salida de zpool status -v y logs del sistema alrededor del tiempo.
  3. Revisa SMART, especialmente CRC y sectores pendientes.
  4. Comprueba contadores MCE/EDAC. Si hay errores corregidos, trata el hardware como degradado.
  5. Identifica archivos afectados; restaura desde snapshot/backup si es posible.
  6. Arregla la capa física (cable/puerto/HBA) antes de scrubea otra vez.
  7. Ejecuta scrub y verifica que la tendencia de errores esté plana.
  8. Si los errores reaparecen en múltiples dispositivos, planifica mantenimiento para aislar RAM/HBA/backplane.

Preguntas frecuentes

1) ¿ZFS requiere RAM ECC?

ZFS no requiere ECC para funcionar. ECC es un control de fiabilidad. Si el pool contiene datos importantes, ECC es la opción predeterminada correcta.

2) Si ZFS tiene checksums, ¿cómo puede importar la corrupción de RAM?

Los checksums detectan corrupción después de que se calcula la suma. Si los datos corruptos son checksummados y escritos, ZFS más tarde los validará como “correctos”, porque coinciden con su checksum.

3) ¿Está bien sin ECC para un NAS doméstico?

A veces. Si tienes backups reales y puedes tolerar restauraciones ocasionales, sin ECC puede ser un compromiso aceptable.
Si guardas fotos irremplazables y tu “backup” es otro disco en la misma caja, estás apostando, no haciendo ingeniería.

4) ¿Qué es peor: no tener ECC o no tener un calendario de scrub?

No tener un calendario de scrub suele ser peor a corto plazo porque descubrirás problemas latentes sólo durante un rebuild—cuando menos puedes permitirte sorpresas.
No tener ECC aumenta la probabilidad de que algunas sorpresas sean más extrañas y difíciles de atribuir.

5) ¿Los mirrors/RAIDZ hacen que ECC sea menos importante?

La redundancia ayuda cuando la corrupción está en disco y es detectable. ECC ayuda a prevenir escrituras malas y protege operaciones en memoria.
Abordan modos de fallo diferentes; se complementan, no se sustituyen.

6) ¿Puedo “validar” mi sistema sin ECC ejecutando memtest una vez?

Memtest es útil, pero es una prueba puntual. Algunas fallas dependen de temperatura o carga y aparecen sólo tras meses.
Si te tomas en serio la integridad, prefiere ECC más monitorización para ver errores corregidos antes de que se conviertan en incidentes.

7) ¿Qué características de ZFS hacen más importante ECC?

Dedup, special vdevs, snapshotting/cloning intensivo, workloads con mucha metadata y sistemas que operan cerca de los límites de memoria.
Estas aumentan la cantidad de estado crítico tocado en memoria y el coste de equivocarse.

8) Si veo errores ECC corregidos, ¿debo entrar en pánico?

No. Los errores corregidos significan que ECC hizo su trabajo. Pero no los ignores. Una tendencia al alza es señal de mantenimiento: reemplaza el DIMM, revisa la refrigeración y verifica ajustes BIOS.

9) ¿ECC es suficiente para garantizar integridad?

No. Aún necesitas redundancia, scrubs, backups y validación. ECC reduce una clase de riesgo aguas arriba; no hace tu sistema invencible ni opcionales los backups.

10) ¿Cuál es la mejora de fiabilidad más barata si no puedo tener ECC?

Disciplina operacional: scrubs, monitorización SMART, pruebas de restauración y mantener el sistema fuera de swap. Además, simplificar el pool (mirrors) y evitar funciones arriesgadas (dedup, special vdev único).

Siguientes pasos que puedes hacer esta semana

  1. Decide tu unidad de pérdida. Si la pérdida del pool es un evento de carrera, compra hardware con ECC o mueve la carga.
  2. Habilita y monitoriza las señales correctas. Rastrea salud con zpool status, resultados de scrub, SMART CRC/sectores pendientes y contadores EDAC/MCE.
  3. Programa scrubs y prueba restauraciones. Los scrubs encuentran problemas; las pruebas de restauración demuestran que puedes sobrevivirlos.
  4. Audita tus características ZFS. Si dedup está activado “por ahorrar espacio”, desactívalo para escrituras nuevas y rediseña antes de reintroducirlo.
  5. Si sigues sin ECC, reduce la exposición. Mantén margen de memoria, evita swap y usa topologías de pool conservadoras.

La postura madura no es “siempre ECC” ni “nunca ECC.” Es: conoce tus modos de fallo, valora tus consecuencias y elige el hardware que coincida con la seriedad de tus promesas.
ZFS te avisará cuando detecte mentiras. ECC ayuda a que no las escribas en primer lugar.

Apilamiento 3D y el futuro de los chiplets: hacia dónde van las CPU

A las 02:17, tu teléfono de guardia vibra. La latencia ha subido, la CPU está “solo” al 55 %, y alguien en el chat dice: “Debe ser la red”. Miras las gráficas y sientes ese pavor familiar: el sistema está lento, pero no de una forma que tu viejo modelo mental pueda explicar.

Bienvenido a la era en la que las CPU ya no son una losa monolítica de silicio. Son barrios de chiplets, cosidos con enlaces de alta velocidad, a veces con silicio extra apilado encima como un rascacielos. Los modos de fallo son diferentes. Los mandos de ajuste son diferentes. Y si sigues tratando a un paquete moderno como una única CPU uniforme, seguirás enviando misterios a producción.

Por qué cambiaron las CPU: física, dinero y el fin de “simplemente reducir tamaño”

Durante décadas, podías tratar el progreso de las CPU como una suscripción predecible: cada generación era más densa, más rápida y (en su mayoría) más barata por unidad de cómputo. Esa era no terminó con un comunicado espectacular. Terminó con mil compromisos pequeños: corriente de fuga, coste de litografía, variabilidad y la incómoda verdad de que los cables no escalan como lo hacen los transistores.

Cuando escuches “chiplets” y “apilamiento 3D”, no lo traduzcas como “ingeniería ingeniosa”. Tradúcelo como: las viejas suposiciones económicas y físicas se rompieron, así que el empaquetado se convirtió en la nueva arquitectura. Movemos la innovación del interior de un dado hacia entre dados.

Hechos y contexto histórico (lo que realmente te ayuda a razonar)

  • Hecho 1: El escalado de Dennard (densidad de potencia constante al reducir transistores) se detuvo a mediados de los 2000, forzando la paralización del crecimiento de frecuencia y empujando diseños multicore.
  • Hecho 2: La latencia de interconexión ha sido un cuello de botella principal durante años; los cables en el chip no se vuelven proporcionalmente más rápidos con cada nodo, así que “dado más grande” significa más tiempo moviendo bits.
  • Hecho 3: Los límites de retícula limitan cuán grande puede ser una exposición de litografía; los dados muy grandes se convierten en pesadillas de rendimiento salvo que los dividas o los hilvanes.
  • Hecho 4: La industria ha usado módulos multi-dado durante mucho tiempo (piensa: paquetes de dos dados tempranos, módulos para servidores), pero los chiplets actuales están mucho más estandarizados y son críticos para el rendimiento.
  • Hecho 5: High Bandwidth Memory (HBM) se volvió práctica apilando dados DRAM y conectándolos con TSVs, demostrando que la integración vertical puede vencer al ancho de banda tradicional de DIMM.
  • Hecho 6: El apilamiento de caché 3D en CPUs de consumo mostró una lección concreta: añadir SRAM verticalmente puede mejorar el rendimiento sin agrandar el dado lógico más caliente.
  • Hecho 7: Núcleos heterogéneos (concepto big/little) han existido en móviles por años; ahora son comunes en servidores porque la potencia y las térmicas —no la frecuencia máxima— definen el rendimiento sostenido.
  • Hecho 8: El empaquetado avanzado (interposers 2.5D, puentes de silicio, fan-out) es hoy un diferenciador competitivo, no un detalle de fabricación en el backend.

Conclusión operativa: la próxima ganancia de rendimiento del 10–15 % es menos probable que provenga de una nueva instrucción y más probable que provenga de mejor localidad, jerarquías de memoria más inteligentes y enlaces entre dados más apretados. Si tu carga es sensible a la varianza de latencia, necesitas tratar el empaquetado y la topología como tratas el enrutamiento de red.

Chiplets, interconexiones y por qué “socket” ya no significa lo que crees

Una CPU chiplet es un paquete que contiene múltiples dados, cada uno especializándose en algo: núcleos, caché, controladores de memoria, E/S, aceleradores, a veces incluso procesadores de seguridad. El paquete es el producto. La “CPU” ya no es una losa única; es un pequeño sistema distribuido que vive bajo un disipador.

Los chiplets existen por tres razones directas:

  1. Rendimiento de fabricación (yield): los dados más pequeños tienen mejor yield; los defectos no matan un dado gigante entero.
  2. Mezcla de nodos de proceso: lógica rápida en un nodo avanzado, E/S en un nodo más económico y maduro.
  3. Agilidad de producto: reutilizar un dado de IO válido en varios SKUs; variar conteos de núcleos y mosaicos de caché sin rehacerlo todo.

La interconexión es ahora arquitectura

En un dado monolítico, los caminos núcleo–caché y núcleo–memoria son mayormente “internos”. En chiplets, esos caminos pueden atravesar una tela entre dados. La interconexión tiene ancho de banda, latencia y características de congestión, y puede introducir efectos topológicos que se parecen sospechosamente a un problema de red —excepto que no puedes tcpdumpear para solucionarlo.

Los paquetes modernos usan telas propietarias, y hay un empuje industrial hacia estándares interoperables die‑to‑die como UCIe. El punto clave no es el acrónimo. Es que los enlaces entre dados se tratan como E/S de alta velocidad: serializados, sincronizados, gestionados en potencia, entrenados, a veces reintentados. Eso significa que el estado del enlace, contadores de error y estados de energía pueden afectar el rendimiento de maneras que parecen “aleatorias” a menos que las midas.

Broma #1: Los chiplets son como microservicios: a todos les encanta la flexibilidad hasta que tienes que depurar la latencia a través de fronteras que creaste a propósito.

NUMA no era nuevo. Simplemente dejaste de respetarlo.

Las CPU chiplet convierten cada servidor en una máquina NUMA más matizada. A veces los “nodos NUMA” mapean a controladores de memoria; a veces a complejos de núcleos; a veces a ambos. En cualquier caso, la localidad importa: qué núcleo accede a qué memoria, qué porción de la caché de último nivel está más cerca y con qué frecuencia cruzas la interconexión.

Si tu playbook de rendimiento todavía empieza y termina con “añadir núcleos” y “fijar hilos”, chocarás con el nuevo muro: contención de interconexión y jerarquía de memoria. El paquete de la CPU ahora tiene patrones de tráfico internos, y tu carga puede crear puntos calientes.

Apilamiento 3D: ancho de banda vertical, problemas verticales

El apilamiento 3D es el uso de múltiples dados apilados verticalmente con conexiones densas (a menudo through‑silicon vias, micro-bumps o hybrid bonding). Se usa para caché, DRAM (HBM) y cada vez más para arreglos lógica‑sobre‑lógica.

¿Por qué apilar?

  • Ancho de banda: las conexiones verticales pueden ser mucho más densas que el enrutamiento borde‑a‑borde en el paquete.
  • Latencia: la menor distancia física puede reducir el tiempo de acceso para ciertas estructuras (especialmente caché).
  • Eficiencia de área: puedes añadir capacidad sin crecer la huella 2D de un dado lógico caliente.

Pero no obtienes algo por nada. El apilamiento 3D introduce un triángulo operativo feo: térmicas, yield y fiabilidad.

Caché apilada: por qué funciona

Apilar SRAM sobre un dado de cómputo te da una gran caché de último nivel sin hacer el dado de cómputo enorme. Eso puede ser una ganancia masiva para cargas con conjuntos de trabajo justo por encima de los tamaños de caché tradicionales: muchos juegos, algunos flujos EDA, ciertas bases de datos en memoria, almacenes clave‑valor con claves calientes y canalizaciones analíticas con escaneos repetidos.

Desde la perspectiva operativa, la caché apilada cambia dos cosas:

  1. El rendimiento se vuelve más bimodal. Si tu carga cabe en caché, eres un héroe. Si no, vuelves a DRAM y la mejora se evapora.
  2. El margen térmico se vuelve precioso. El silicio extra encima del dado de cómputo afecta el flujo de calor; el comportamiento turbo y las frecuencias sostenidas pueden cambiar de maneras que aparecen como varianza en la latencia.

HBM: el truco de ancho de banda con costo

HBM apila dados DRAM y los coloca cerca del dado de cómputo (a menudo vía interposer). Esto entrega un ancho de banda enorme comparado con DIMMs tradicionales, pero la capacidad por pila es limitada y el coste es alto. También cambia fallos y observabilidad: los errores de memoria pueden mostrarse de forma distinta y la planificación de capacidad se vuelve otro deporte.

El empaquetado 3D y 2.5D también está forzando una nueva regla de diseño: tu software debe entender niveles. HBM vs DDR, memoria cercana vs memoria lejana, caché en paquete vs caché en dado. “Simplemente asigna memoria” se convierte en una decisión de rendimiento.

Broma #2: Apilar dados es genial hasta que recuerdas que el calor también se apila, y a diferencia de tu backlog no puede posponerse.

El verdadero enemigo: bytes, no flops

La mayoría de los sistemas de producción no están limitados por el rendimiento aritmético bruto. Están limitados por mover datos: de memoria a caché, de caché al núcleo, del núcleo al NIC, del almacenamiento a la memoria y de vuelta. Los chiplets y el apilamiento 3D son reconocimientos industriales de que la memoria y la interconexión son el evento principal.

Aquí es donde los instintos de SRE ayudan. Cuando el paquete de la CPU se convierte en una tela, los cuellos de botella parecen:

  • Alta IPC pero bajo rendimiento (esperando memoria o por contención de locks).
  • CPU no ocupada pero latencia alta (stalls, fallos de caché, memoria remota).
  • El rendimiento cae tras escalar (el tráfico entre chiplets crece superlinealmente).

Qué cambia con chiplets y apilamiento

La localidad de memoria ya no es opcional. En un dado monolítico grande, el acceso “remoto” aún puede ser bastante rápido. En chiplets, el acceso remoto puede atravesar saltos de tela y competir con otro tráfico. En un SKU con caché apilada, la caché “local” puede ser mayor pero la penalización por fallarla puede ser más visible debido al cambio en frecuencia/térmicas.

El ancho de banda no es uniforme. Algunos dados tienen acceso más cercano a ciertos controladores de memoria. Algunos núcleos comparten slices de caché más estrechamente. La topología puede recompensar una buena planificación y castigar una programación ingenua.

La varianza de latencia se vuelve normal. Estados de gestión de energía, clock gating en la tela y algoritmos de boost pueden cambiar las latencias internas. Tu p99 lo notará antes que los promedios.

Térmicas y energía: el paquete es el nuevo campo de batalla

En el papel, compras una CPU con un TDP y una frecuencia boost y das el asunto por cerrado. En realidad, las CPUs modernas son sistemas gestionados por energía que negocian constantemente las frecuencias basadas en temperatura, corriente y características de la carga. Los chiplets y los apilamientos 3D complican esa negociación.

Puntos calientes y gradientes térmicos

Con chiplets, no tienes un perfil térmico uniforme. Tienes puntos calientes donde los núcleos son densos, dados de IO separados que corren más fríos y a veces dados apilados que impiden la extracción de calor del dado de cómputo debajo. En cargas de producción de larga duración, las frecuencias sostenidas importan más que los picos.

Dos consecuencias operativas:

  • Los benchmarks mienten con más frecuencia. Los benchmarks cortos alcanzan el boost; la producción llega al estado estable y a los límites de potencia.
  • La refrigeración es rendimiento. Un disipador o flujo de aire marginal no solo causará throttling; causará varianza, que es más difícil de depurar.

Fiabilidad: más conexiones, más lugares para echarse a perder

Más dados y más interconexiones significan más puntos potenciales de fallo: micro-bumps, TSVs, sustratos de paquete y entrenamiento de enlaces. Los proveedores diseñan para esto, por supuesto. Pero en campo lo verás como errores corregidos, enlaces degradados o incidentes de “un host está raro”.

Un aforismo operativo útil, parafraseando una idea de una voz notable en fiabilidad: Los sistemas complejos fallan de formas complejas; reduce lo desconocido y mide las cosas correctas. (idea parafraseada, inspirada en el pensamiento de ingeniería de fiabilidad a menudo atribuido a John Allspaw)

Traducción: no asumas uniformidad entre hosts, y no asumas que dos sockets se comportan igual solo porque el SKU coincide.

Qué significa esto para SREs: rendimiento, fiabilidad y vecinos ruidosos

No necesitas convertirte en ingeniero de empaquetado. Necesitas dejar de tratar la “CPU” como un recurso escalar único. En un mundo chiplet + apilamiento, gestionas:

  • Cómputo topológico (los núcleos no están a igual distancia de memoria y caché)
  • Capacidad de interconexión (la tela interna puede saturarse)
  • Margen térmico (frecuencias sostenidas, throttling y p99)
  • Política de energía (limitación, turbo e interacciones con el scheduler)

La observabilidad debe ampliarse

El monitoreo tradicional del host —%CPU, load average, memoria usada— cada vez fallará más al explicar cuellos de botella. Necesitas al menos un manejo básico de:

  • Localidad NUMA (¿están alineados hilos y memoria?)
  • Comportamiento de caché (fallos LLC, presión de ancho de banda)
  • Frecuencia y throttling (¿estás limitado por potencia?)
  • Colocación del scheduler (¿Kubernetes o systemd movieron tu carga entre nodos?)

Y sí, esto es molesto. Pero es menos molesto que un cuarto de los casos de “actualizamos CPUs y empeoró”.

Guion de diagnóstico rápido: encuentra el cuello de botella en minutos

Este es el flujo de triaje que uso cuando un servicio se vuelve más lento en una nueva plataforma chiplet/apilada, o se vuelve más lento después de escalar. El objetivo no es la causa raíz perfecta. El objetivo es tomar la decisión siguiente correcta rápidamente.

Primero: determina si estás limitado por cómputo, memoria o “tela”

  1. Revisa la frecuencia de CPU y el throttling: si los relojes están bajos bajo carga, estás limitado por potencia/térmicas.
  2. Revisa el ancho de banda de memoria y la presión de fallos de caché: si los fallos LLC y el ancho de banda son altos, estás limitado por memoria.
  3. Revisa la localidad NUMA: si el acceso a memoria remota es alto, probablemente estás limitado por topología/scheduler.

Segundo: confirma topología y colocación

  1. Verifica nodos NUMA y mapeo CPU‑a‑nodo.
  2. Verifica afinidad de procesos y política de memoria.
  3. Comprueba si la carga está rebotando entre nodos (migraciones del scheduler).

Tercero: aísla una variable y vuelve a ejecutar

  1. Fija la carga a un nodo NUMA; compara p95/p99.
  2. Forza la asignación de memoria local; compara rendimiento.
  3. Aplica una política de energía conservadora; compara la varianza.

Si no puedes reproducir un cambio significativo controlando colocación y estado de energía, el problema probablemente sea de capa superior (locks, GC, E/S), y deberías dejar de culpar al paquete de la CPU. Las CPUs modernas son complicadas, pero no mágicas.

Tareas prácticas con comandos: qué ejecutar, qué significa, qué decidir

Estas son tareas reales que puedes ejecutar en hosts Linux para entender comportamientos relacionados con chiplets/apilamiento 3D. Los comandos son deliberadamente aburridos. Las herramientas aburridas te mantienen honesto.

Tarea 1: Mapea la topología NUMA rápidamente

cr0x@server:~$ lscpu | egrep 'Model name|Socket|Thread|Core|NUMA|CPU\(s\)'
CPU(s):                               128
Model name:                           AMD EPYC 9xx4
Thread(s) per core:                   2
Core(s) per socket:                   64
Socket(s):                            1
NUMA node(s):                         8

Qué significa la salida: Tienes 8 nodos NUMA en un solo socket. Eso es una topología tipo chiplet: múltiples dominios de memoria y saltos de interconexión dentro de un paquete.

Decisión: Si la latencia importa, planifica fijar servicios clave dentro de un nodo NUMA y mantener la memoria local. El scheduling por defecto puede ser “aceptable”, pero “aceptable” es como muere el p99.

Tarea 2: Ver qué CPUs pertenecen a cada nodo NUMA

cr0x@server:~$ numactl --hardware
available: 8 nodes (0-7)
node 0 cpus: 0-15
node 0 size: 64000 MB
node 0 free: 61234 MB
node 1 cpus: 16-31
node 1 size: 64000 MB
node 1 free: 60110 MB
node 2 cpus: 32-47
node 2 size: 64000 MB
node 2 free: 59872 MB
node 3 cpus: 48-63
node 3 size: 64000 MB
node 3 free: 62155 MB
node 4 cpus: 64-79
node 4 size: 64000 MB
node 4 free: 60990 MB
node 5 cpus: 80-95
node 5 size: 64000 MB
node 5 free: 61801 MB
node 6 cpus: 96-111
node 6 size: 64000 MB
node 6 free: 61644 MB
node 7 cpus: 112-127
node 7 size: 64000 MB
node 7 free: 62002 MB

Qué significa la salida: Cada nodo NUMA tiene un rango de CPU y una porción de memoria. Si tu proceso corre en CPUs del nodo 0 pero asigna memoria desde el nodo 6, pagará un peaje de tela en cada acceso remoto.

Decisión: Para servicios sensibles a la latencia, alinea la afinidad de CPU y la política de memoria. Para trabajos de throughput, puede que prefieras intercalar para ancho de banda.

Tarea 3: Comprueba si el kernel está registrando problemas de localidad NUMA

cr0x@server:~$ numastat -p 1 3
Per-node process memory usage (in MBs) for PID 1 (systemd)
Node 0 Node 1 Node 2 Node 3 Node 4 Node 5 Node 6 Node 7 Total
----- ----- ----- ----- ----- ----- ----- ----- -----
Numa_Hit      12     10      9      8      9     10      8      9    75
Numa_Miss      1      0      0      0      0      0      0      0     1
Numa_Foreign   0      0      0      0      0      0      0      0     0
Interleave_Hit 0      0      0      0      0      0      0      0     0
Local_Node    12     10      9      8      9     10      8      9    75
Other_Node     1      0      0      0      0      0      0      0     1

Qué significa la salida: Para PID 1 está bien. Para tu servicio real, si Other_Node es grande, estás pagando penalizaciones remotas.

Decisión: Si el acceso remoto es alto y la latencia tail es mala, fija y localiza. Si tu objetivo es throughput y estás limitado por ancho de banda, considera intercalar.

Tarea 4: Verifica el comportamiento de la frecuencia de CPU bajo carga

cr0x@server:~$ sudo turbostat --Summary --quiet --show CPU,Avg_MHz,Busy%,Bzy_MHz,PkgTmp,PkgWatt --interval 5
CPU  Avg_MHz  Busy%  Bzy_MHz  PkgTmp  PkgWatt
-    2850     62.10  4588     86      310.12

Qué significa la salida: Los núcleos ocupados están corriendo alto (Bzy_MHz), la temperatura del paquete es alta y la potencia es sustancial. Si Bzy_MHz colapsa con el tiempo mientras Busy% se mantiene alto, probablemente estés limitado por potencia/térmicas.

Decisión: Para cargas sostenidas, ajusta capado de potencia, refrigeración o reduce la concurrencia. No persigas números de boost de una sola ejecución.

Tarea 5: Confirma que la política de energía (governor) no te sabotea

cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
performance

Qué significa la salida: El governor está en performance. Si está en powersave en un host sensible a la latencia, básicamente estás pidiendo jitter.

Decisión: Ajusta la política apropiada por rol de clúster. Un clúster batch puede ahorrar energía; un clúster OLTP no debe disfrazarse de portátil.

Tarea 6: Medir migraciones del scheduler (un silencioso asesino NUMA)

cr0x@server:~$ pidstat -w -p $(pgrep -n myservice) 1 5
Linux 6.5.0 (server)  01/12/2026  _x86_64_  (128 CPU)

01:10:01 PM   UID       PID   cswch/s nvcswch/s  Command
01:10:02 PM  1001     43210   120.00     15.00  myservice
01:10:03 PM  1001     43210   135.00     20.00  myservice
01:10:04 PM  1001     43210   128.00     18.00  myservice

Qué significa la salida: Los cambios de contexto son moderados. Si además ves migraciones frecuentes de CPU (vía perf o schedstat), puedes perder localidad de caché entre chiplets.

Decisión: Considera fijar CPUs para los hilos más calientes, o ajusta tu runtime (hilos de GC, conteos de workers) para reducir churn.

Tarea 7: Comprobar presión de ancho de banda de memoria con pcm-memory (si está instalado)

cr0x@server:~$ sudo pcm-memory 1 -csv
Time,Ch0Read,Ch0Write,Ch1Read,Ch1Write,SystemRead,SystemWrite
1.00,12.3,5.1,11.8,4.9,198.4,82.1
2.00,12.5,5.0,12.1,4.8,201.0,80.9

Qué significa la salida: El ancho de banda de lectura/escritura del sistema es alto. Si está cerca de los límites de la plataforma durante tu incidente, estás atascado por memoria, no por CPU.

Decisión: Reduce el tráfico de memoria: arregla el layout de datos, disminuye copias, aumenta la tasa de aciertos de caché o migra a una plataforma con caché apilada/HBM si tu conjunto de trabajo encaja.

Tarea 8: Observar señales de fallos de caché y stalls con perf

cr0x@server:~$ sudo perf stat -p $(pgrep -n myservice) -e cycles,instructions,cache-misses,branches,branch-misses -a -- sleep 10
 Performance counter stats for 'system wide':

    38,112,001,220      cycles
    52,880,441,900      instructions              #    1.39  insn per cycle
       902,110,332      cache-misses
     9,221,001,004      branches
       112,210,991      branch-misses

      10.002113349 seconds time elapsed

Qué significa la salida: Muchos fallos de caché. La IPC es decente, pero los fallos aún pueden dominar el tiempo de muro según la carga. En CPUs chiplet, los fallos pueden convertirse en tráfico de tela y accesos a memoria remota.

Decisión: Si los fallos de caché correlacionan con picos de latencia, prioriza localidad: fija hilos, reduce contención de estado compartido y prueba SKUs con caché apilada cuando el conjunto de trabajo esté justo por encima del LLC.

Tarea 9: Comprobar errores de memoria y tormentas de errores corregidos

cr0x@server:~$ sudo ras-mc-ctl --summary
Memory controller events summary:
  Corrected errors: 24
  Uncorrected errors: 0
  No DIMM labels were found

Qué significa la salida: Hay errores corregidos. Una tasa creciente puede causar degradación del rendimiento y comportamiento impredecible, y en plataformas de empaquetado avanzado quieres notarlo pronto.

Decisión: Si los errores corregidos aumentan, programa mantenimiento: volver a asentar o reemplazar DIMMs, actualizar firmware o retirar el host. No esperes a que los errores no corregidos te enseñen humildad.

Tarea 10: Validar salud de enlaces/PCIe (el dado de IO también forma parte del relato)

cr0x@server:~$ sudo lspci -vv | sed -n '/Ethernet controller/,+25p' | egrep 'LnkSta:|LnkCap:'
LnkCap: Port #0, Speed 16GT/s, Width x16
LnkSta: Speed 16GT/s (ok), Width x16 (ok)

Qué significa la salida: El enlace está funcionando a la velocidad/ancho esperados. Si ves enlaces downtrained, el rendimiento de E/S cae y los ciclos de CPU se desperdician en overhead de interrupciones/paquetes.

Decisión: Los enlaces downtrained provocan: revisa risers, configuraciones BIOS, firmware y el asiento físico. No “optimices” software alrededor de hardware roto.

Tarea 11: Confirmar distribución de interrupciones (evita congestión IRQ en un solo núcleo)

cr0x@server:~$ cat /proc/interrupts | egrep 'eth0|mlx|ens' | head
  55:   10223342          0          0          0   IR-PCI-MSI 524288-edge      ens3f0-TxRx-0
  56:          0    9981221          0          0   IR-PCI-MSI 524289-edge      ens3f0-TxRx-1
  57:          0          0    9875522          0   IR-PCI-MSI 524290-edge      ens3f0-TxRx-2

Qué significa la salida: Las interrupciones están repartidas entre CPUs. Si todas las interrupciones caen en una CPU de un nodo NUMA mientras tu carga corre en otro lugar, tendrás tráfico entre nodos y jitter.

Decisión: Fija las IRQs cerca del nodo NUMA del NIC y cerca de los hilos del servicio que consumen paquetes. La localidad también aplica para E/S.

Tarea 12: Comprobar la política de memoria y ejecutar una prueba localmente

cr0x@server:~$ numactl --cpunodebind=2 --membind=2 ./bench --duration 30
throughput=118223 ops/s
p99_latency_ms=3.4

Qué significa la salida: Forzaste CPU y memoria al nodo 2. Compara esto con resultados sin fijar. Un delta grande indica penalizaciones NUMA/tela.

Decisión: Si fijar mejora el p99 de forma material, implementa colocación (CPUAffinity en systemd, topology manager de Kubernetes o fijación a nivel de carga) en lugar de perseguir micro‑optimizaciones.

Tarea 13: Inspeccionar hugepages e indicadores de presión TLB

cr0x@server:~$ grep -E 'HugePages_Total|HugePages_Free|Hugepagesize' /proc/meminfo
HugePages_Total:    4096
HugePages_Free:     3900
Hugepagesize:       2048 kB

Qué significa la salida: Hay hugepages disponibles. En cargas intensivas de memoria, las hugepages pueden reducir fallos de TLB, lo cual importa más cuando la latencia de memoria ya es mayor por accesos remotos.

Decisión: Si el profiling muestra presión TLB, activa hugepages y valida el impacto. No hagas cargo‑cult: mide.

Tarea 14: Detectar throttling y razones de límite de potencia (ejemplo Intel via RAPL)

cr0x@server:~$ dmesg | egrep -i 'thrott|powercap|rapl' | tail -n 5
[ 8123.221901] intel_rapl: power limit changed to 210W
[ 8123.222110] CPU0: Package power limit exceeded, capping frequency

Qué significa la salida: El sistema está capando potencia. Tu benchmark puede haber corrido antes del cap; las ejecuciones de producción durante él.

Decisión: Alinea configuraciones BIOS/firmware de potencia con la intención de la carga. Si limitas por presupuesto de centro de datos, ajusta los SLOs y afina la concurrencia.

Tres mini-historias corporativas de la era chiplet

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

Una compañía SaaS de tamaño medio migró una capa API sensible a latencia a servidores nuevos. Mismo conteo de cores que antes, frecuencias boost anunciadas más altas y una cifra llamativa de L3 que parecía dinero gratis. El despliegue fue conservador: canary al 5 %, métricas bien, luego 25 %, luego 50 %.

Alrededor de la mitad de la flota, la latencia p99 comenzó a oscilar. No subir de forma suave: oscilaba. Las gráficas tenían un patrón en sierra que hacía a la gente discutir sobre patrones de tráfico y GC. La utilización de CPU se mantuvo moderada. La red parecía limpia. El almacenamiento estaba tranquilo. El canal de incidentes se llenó con la peor frase en operaciones: “Nada parece estar mal”.

La suposición errónea: trataron la CPU como uniforme y asumieron que si la CPU% promedio estaba bien, la CPU no era el cuello de botella. En realidad, la carga se estaba programando a través de nodos NUMA y asignando memoria de forma remota frecuentemente debido al comportamiento del runtime y la libertad del scheduler de contenedores para mover tareas. Los accesos remotos no eran catastróficos; eran variables, lo que destruyó la latencia tail.

Lo probaron fijando el servicio a un solo nodo NUMA y forzando asignación local en una prueba. El p99 se estabilizó de inmediato y la sierra desapareció. La solución no fue glamurosa: scheduling consciente de topología, fijado de CPU para los pods más calientes y una política de memoria deliberada. También dejaron de sobrecargar pods sensibles a latencia y batch en el mismo socket. “Más utilización” no era el objetivo; la latencia predecible sí lo era.

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

Una fintech ejecutaba un motor de riesgo que escaneaba un gran dataset en memoria repetidamente. Compraron un SKU con caché apilada porque un benchmark del proveedor mostró una gran mejora. Las pruebas tempranas fueron prometedoras. El throughput mejoró. Todos celebraron. Entonces hicieron lo que las empresas hacen: “optimizaron”.

El equipo aumentó agresivamente el paralelismo, asumiendo que la caché extra mantendría la escalabilidad. También activaron una política turbo más agresiva en BIOS para perseguir aceleraciones de corta duración. En staging, la carga terminó más rápido—la mayoría de las veces.

En producción, la optimización falló de dos maneras. Primero, los hilos extra aumentaron el tráfico entre chiplets porque la carga tenía una estructura compartida que no estaba particionada limpiamente. La interconexión se congestinó. Segundo, la política turbo elevó las temperaturas rápido, causando throttling térmico a mitad de ejecución. El sistema no solo se ralentizó; se volvió impredecible. Algunas ejecuciones terminaron rápido; otras golpearon throttling y se arrastraron.

La solución fue casi aburrida: reducir el paralelismo hasta un punto donde la localidad se mantuviera alta, particionar el dataset con más cuidado y establecer una política de energía optimizada para frecuencia sostenida en lugar de pico boost. La caché apilada aún ayudó—pero solo cuando el software respetó la topología y el envelope térmico. La lección: más caché no excusa un mal escalado.

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

Un gran equipo de plataforma corporativa estandarizó una “checklist de bring‑up de hardware” para nuevas generaciones de CPU. Incluía baselines de BIOS/firmware, versiones de microcode, verificación de topología NUMA y un conjunto fijo de pruebas smoke de perf/latencia fijadas a nodos específicos.

Cuando llegó un lote de nuevos servidores, las pruebas detectaron una regresión sutil: el ancho de banda de memoria era menor de lo esperado en un nodo NUMA y la latencia p99 bajo una carga sintética mixta era peor. Nada fallaba abiertamente. La mayoría de equipos lo habría declarado “dentro de la varianza” y seguido.

La checklist forzó la escalada. Resultó que una configuración BIOS relacionada con interleaving de memoria y gestión de potencia difería del baseline por un cambio en el valor por defecto del proveedor. Los servidores estaban técnicamente “funcionando”, pero no funcionaban igual que el resto de la flota. Esa discrepancia se habría convertido en una pesadilla de guardia más tarde, porque el comportamiento heterogéneo dentro de un grupo autoscalado convierte incidentes en juegos de probabilidad.

Arreglaron el baseline, reimaginizaron los hosts, volvieron a ejecutar las mismas pruebas fijadas y obtuvieron los resultados esperados. Sin heroicidades. Sin incidentes nocturnos. Solo disciplina operativa: medir, estandarizar y negarse a aceptar variancia silenciosa en un mundo donde los paquetes son pequeños sistemas distribuidos.

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

1) Síntoma: p99 sube al escalar a más cores

Causa raíz: Contención entre chiplets y aumento de accesos a memoria remota al dispersar hilos entre nodos NUMA; estructuras de datos compartidas amplifican el tráfico.

Solución: Particionar estado, reducir compartición entre hilos, fijar workers críticos dentro de un nodo NUMA y usar scheduling consciente de topología.

2) Síntoma: Utilización de CPU moderada pero throughput bajo

Causa raíz: Stalls de memoria (fallos LLC, latencia DRAM), congestión de la tela o migraciones frecuentes se ocultan detrás de “no ocupado”.

Solución: Usa perf stat y herramientas de ancho de banda de memoria; revisa numastat; fija y localiza; reduce churn del asignador y las copias.

3) Síntoma: Nuevos servidores son más rápidos en benchmarks pero peores en producción

Causa raíz: Los benchmarks alcanzan boost clocks y estados de caché caliente; la producción alcanza límites de potencia sostenida y cargas mixtas.

Solución: Prueba con ejecuciones en estado estable, incluye métricas p99 y valida bajo concurrencia y condiciones térmicas realistas.

4) Síntoma: Un host en la pool es consistentemente raro

Causa raíz: Enlace PCIe downtrained, canal de memoria degradado, tormenta de errores corregidos o deriva de BIOS afectando potencia/topología.

Solución: Revisa lspci -vv, resúmenes RAS, versiones de microcode/BIOS; pone en cuarentena y remedia en lugar de ajustar alrededor del problema.

5) Síntoma: Jitter de latencia aparece tras habilitar funciones “ahorro de energía”

Causa raíz: C‑states agresivos, clock gating en la tela, escalado de frecuencia o límites de paquete causan comportamiento variable de wake/boost.

Solución: Usa governor de performance para tiers de latencia, ajusta estados de energía en BIOS y valida con turbostat bajo carga real.

6) Síntoma: Rendimiento pps de red cae tras refresco de hardware

Causa raíz: IRQs y threads están en diferentes nodos NUMA; el dado de IO y la localidad del NIC importan, y el tráfico entre nodos añade latencia.

Solución: Alinea afinidad de IRQs y threads de aplicación al nodo NUMA del NIC; confirma ancho/velocidad de enlace; evita la sobreconsolidación.

7) Síntoma: “Añadimos caché apilada pero no vimos mejora”

Causa raíz: El conjunto de trabajo no cabe, o la carga está limitada por ancho de banda en lugar de latencia de caché; la ganancia es específica de la carga.

Solución: Perfila tasas de fallos de caché y ancho de banda; prueba tamaños de datos representativos; considera HBM o cambios algorítmicos si estás limitado por ancho de banda.

8) Síntoma: Tras contenerizar, el rendimiento regresó en CPUs chiplet

Causa raíz: El scheduler de contenedores movió hilos entre CPUs/nodos NUMA; las cuotas cgroup CPU introdujeron ráfagas; la localidad del page cache empeoró.

Solución: Usa CPU manager/topology manager, establece requests/limits explícitos adecuadamente y fija pods con uso intensivo de memoria a nodos NUMA.

Listas de verificación / plan paso a paso para nuevas plataformas

Plan paso a paso: llevar una nueva plataforma chiplet/apilada a producción

  1. Topología baseline: registra lscpu y numactl --hardware para el SKU; almacénalo con tus artefactos de build.
  2. Estandariza firmware: configuraciones BIOS, microcode y políticas de potencia deben ser consistentes en la pool.
  3. Elige postura de energía por tier: clusters de latencia obtienen policy performance; clusters batch pueden estar intencionalmente con capado de potencia.
  4. Ejecuta pruebas smoke fijadas: mide throughput y p99 con CPU+memoria ligadas a un nodo; luego ejecuta sin fijar; compara deltas.
  5. Valida margen de ancho de banda de memoria: si tu carga es limitada por memoria, la planificación de capacidad es planificación de ancho de banda.
  6. Valida localidad de E/S: revisa salud de enlaces PCIe y distribución de IRQ; asegura que la afinidad del NIC coincida con la colocación de CPU.
  7. Decide política de colocación: acepta NUMA (fijar y localizar) o intercalar explícitamente para ancho de banda. No hagas “híbrido accidental”.
  8. Despliega con detección de varianza: vigila no solo medianas sino dispersión entre hosts; alerta temprano sobre “un host raro”.
  9. Documenta modos de fallo: firmas de throttling, umbrales de errores corregidos y cómo poner en cuarentena un host.
  10. Vuelve a probar tras actualizaciones de kernel: cambios en el scheduler pueden ayudar o perjudicar el manejo de topología; valida periódicamente.

Checklist: decidir entre caché apilada vs más ancho de banda de memoria

  • Si tu conjunto de trabajo es ligeramente mayor que el LLC y ves muchos fallos de LLC: la caché apilada puede ser una gran ganancia.
  • Si el ancho de banda de memoria está cerca del máximo y los stalls dominan: la caché apilada puede no salvarte; prioriza ancho de banda (plataformas HBM, más canales) o reduce tráfico.
  • Si la latencia tail importa: prefiere soluciones que reduzcan la varianza (localidad, política de potencia estable) sobre picos máximos.

Checklist: qué evitar al adoptar CPUs con muchos chiplets

  • No asumas “un socket = uniforme.” Mide comportamiento NUMA.
  • No aceptes deriva de BIOS en un grupo autoscalado.
  • No optimices aplicaciones sin primero verificar comportamiento de potencia y throttling.
  • No mezcles cargas de latencia y batch en el mismo socket a menos que tengas aislamiento estricto.

FAQ

1) ¿Los chiplets siempre son más rápidos que los dados monolíticos?

No. Los chiplets son principalmente una estrategia económica y de velocidad de producto, con beneficios de rendimiento cuando la interconexión y la topología están bien gestionadas. La mala localidad puede borrar la ganancia.

2) ¿El apilamiento 3D hará que las CPU funcionen más calientes?

A menudo, sí en la práctica. Las pilas pueden impedir la salida de calor y crear puntos calientes. Los proveedores diseñan alrededor de esto, pero las cargas sostenidas pueden ver throttling más temprano o más varianza.

3) ¿Es obligatorio afinar NUMA ahora?

Para servicios sensibles a latencia en CPUs con muchos chiplets, está casi al borde de ser obligatorio. Para trabajo embarrassingly parallel en batch, a menudo puedes prescindir—hasta que no puedas.

4) ¿Qué cargas se benefician más de caché apilada?

Cargas con un conjunto de trabajo mayor que la caché normal pero más pequeño que patrones de streaming DRAM: workloads clave‑valor calientes, algunas analíticas, ciertas simulaciones y estructuras de datos en memoria con muchas lecturas.

5) ¿Cuál es el riesgo operativo de empaquetado más avanzado?

Más componentes y enlaces pueden significar degradaciones sutiles: tormentas de errores corregidos, downtraining de enlaces o varianza de plataforma. Tus prácticas de monitoreo y cuarentena importan más.

6) ¿Los chiplets significan que “más cores” dejará de ayudar?

Más cores seguirán ayudando para cargas paralelas, pero el escalado será más sensible al ancho de banda de memoria, la congestión de interconexión y la contención de estado compartido. Las ganancias fáciles se acabaron.

7) ¿Cómo cambia HBM la planificación de capacidad?

HBM te empuja hacia un modelo escalonado: ancho de banda muy alto pero capacidad limitada. Planifica qué debe quedarse en HBM, qué puede volcarse a DDR y cómo se comporta tu asignador/runtime.

8) ¿UCIe hará que los paquetes CPU sean modulares como piezas de PC?

Eventualmente, más modulares que hoy—pero no esperes plug‑and‑play. Integridad de señal, suministro de potencia, térmicas y validación siguen siendo difíciles, y el “estándar” no eliminará la física.

9) ¿Cuál es el cambio más simple y “suficientemente bueno” para reducir latencia tail en CPUs chiplet?

Fija tus hilos más calientes a un nodo NUMA y mantén su memoria local. Luego verifica con una prueba A/B fijada. Si ayuda, invierte en scheduling consciente de topología.

10) ¿Debería comprar SKUs con caché apilada para todo?

No. Cómpralas para cargas que demuestren sensibilidad a caché en el profiling. Si no, pagas por silicio que mayormente solo adorna tu hoja de procurement.

Próximos pasos prácticos

El apilamiento 3D y los chiplets no son una tendencia; son la forma del camino por delante. La CPU se está convirtiendo en un sistema distribuido a nivel de paquete con restricciones térmicas y topológicas. Tu software y tu operación deben comportarse en consecuencia.

Qué hacer la próxima semana (no el próximo trimestre)

  1. Elige un servicio con SLOs de latencia y ejecuta la prueba NUMA fijada vs no fijada (numactl) para cuantificar sensibilidad.
  2. Añade dos paneles a nivel host: frecuencia/throttling de CPU (derivado de turbostat) y accesos remotos NUMA (numastat/derivado de PMU si lo tienes).
  3. Estandariza baselines BIOS/microcode para cada pool de hardware; alerta sobre deriva.
  4. Escribe un runbook de una página usando el Guion de diagnóstico rápido arriba para que la guardia no culpe a la red por reflejo.
  5. Decide tu filosofía de colocación: localidad‑first para tiers de latencia; interleave/ancho‑de‑banda‑first para tiers de throughput—luego aplícalo.

Si no haces nada más, haz esto: deja de tratar el %CPU como la verdad. En chiplets y diseños apilados, %CPU es una vibra. Mide localidad, mide ancho de banda y mide throttling. Entonces podrás discutir con confianza, que es el único tipo de discusión que las operaciones pueden permitirse.

Proxmox «no se puede asignar memoria»: ballooning, sobreasignación y cómo ajustarlo

Haces clic en Start en una VM y Proxmox responde con el equivalente digital de un encogimiento de hombros:
«no se puede asignar memoria». O peor, la VM arranca y luego el host empieza a matar procesos al azar
como un encargado estresado en un teatro con una sola salida.

Las fallas de memoria en Proxmox no son místicas. Son problemas de contabilidad: lo que el host cree que tiene,
lo que las VMs afirman que podrían usar, lo que realmente tocan y lo que el kernel está dispuesto a
prometer en ese momento. Arregla la contabilidad y la mayoría del drama desaparece.

Guía rápida de diagnóstico

Si estás de guardia y el cluster está gritando, no quieres una clase de filosofía. Quieres un bucle cerrado:
confirma el modo de fallo, identifica el limitador, haz un cambio seguro, repite.

Primero: ¿es esto agotamiento de memoria del host o un límite por VM?

  • Si la VM falla al iniciar con no se puede asignar memoria, sospecha límites de compromiso del host,
    límites de cgroup, hugepages o fragmentación—a menudo visible inmediatamente en dmesg / journal.
  • Si la VM arranca y luego es asesinada, suele ser el OOM killer del invitado (dentro de la VM) o
    el OOM killer del host (matando QEMU), dependiendo de qué registros muestren el golpe.

Segundo: comprueba la «capacidad real» del host, no los gráficos bonitos

  • Memoria libre del host y swap: free -h
  • Presión de memoria del host y stalls de reclaim: vmstat 1
  • Evidencia de OOM: journalctl -k y dmesg -T
  • Tamaño de ZFS ARC (si usas ZFS): arcstat / /proc/spl/kstat/zfs/arcstats

Tercero: verifica la asignación y la política en el lado de Proxmox

  • Configuración de la VM: objetivo de ballooning vs máximo, hugepages, NUMA, etc.:
    qm config <vmid>
  • Política de sobreasignación del nodo: pvesh get /nodes/<node>/status y
    /etc/pve/datacenter.cfg
  • Si es un contenedor (LXC), comprueba el límite de memoria del cgroup y el límite de swap:
    pct config <ctid>

Cuarto: elige la mitigación inmediata menos mala

  • Para liberar RAM y reducir la presión ahora: detén una VM no crítica.
  • Si ZFS está comiéndose la máquina: limita ARC (persistente) o reinicia como último recurso.
  • Si estás sobreasignado: reduce la memoria máxima de las VMs (no solo el objetivo del balloon).
  • Si no hay swap y estás justo: añade swap (host) para evitar OOM instantáneos mientras arreglas el dimensionamiento.

Chiste #1: La sobreasignación de memoria es como el presupuesto corporativo—todo funciona hasta que todos tratan de justificar el almuerzo el mismo día.

Qué significa realmente «no se puede asignar memoria» en Proxmox

Proxmox es una capa de gestión. El asignador real es Linux, y para VMs suele ser QEMU/KVM. Cuando ves
no se puede asignar memoria, está ocurriendo una de estas cosas:

  • QEMU no puede reservar la RAM solicitada por la VM en el momento de inicio. Eso puede fallar incluso si
    free parece correcto, porque a Linux le importan las reglas de compromiso y la fragmentación.
  • El kernel rechaza la asignación debido a la lógica de overcommit/CommitLimit. Linux rastrea cuánto
    memoria han prometido potencialmente usar los procesos (memoria virtual), y puede negar nuevas promesas.
  • Se piden hugepages pero no están disponibles. Las hugepages están pre-reservadas. Si no existen,
    la asignación falla de forma inmediata y ruidosa.
  • Los límites de cgroup bloquean la asignación. Más común con contenedores, pero puede aplicarse si systemd
    slices o cgroups personalizados están en juego.
  • La memoria está disponible pero no en la forma que pediste. La fragmentación puede impedir asignaciones
    contiguas grandes, especialmente con hugepages o ciertas necesidades DMA.

Mientras tanto, la «solución» a la que recurren la gente—ballooning—no cambia lo que QEMU pidió si aún configuraste una gran
memoria máxima. Ballooning ajusta lo que el invitado se anima a usar, no lo que el host debe estar preparado
para respaldar en el peor momento posible.

Dos números importan: objetivo del invitado y máximo del invitado

En las opciones de VM de Proxmox, el ballooning te da:

  • Memory (max): el techo de la VM. QEMU contabiliza reservas por ello.
  • Balloon (min/target): el objetivo en tiempo de ejecución que puede reducirse bajo presión.

Si estableces max en 64 GB «por si acaso» y objetivo del balloon en 8 GB «porque normalmente está inactiva», le has dicho al host:
«Por favor, prepárate para cubrir mis 64 GB de estilo de vida». El host, siendo adulto, puede decir que no.

Hechos interesantes y un poco de historia (para que no lo repitas)

  1. El comportamiento de overcommit de Linux es antiguo e intencional: existe porque muchas asignaciones nunca se usan por completo,
    y una contabilidad estricta desperdiciaría RAM en promesas vacías.
  2. El OOM killer precede a la mayoría de las pilas modernas de virtualización; fue la respuesta pragmática de Linux a «alguien está mintiendo
    sobre la memoria» mucho antes de que el marketing de la nube convirtiera mentir en una característica.
  3. El ballooning se volvió común con los primeros hipervisores porque los invitados ociosos acaparaban caché y hacían que la consolidación
    pareciera peor de lo que era.
  4. KSM (Kernel Samepage Merging) fue diseñado para desduplicar páginas de memoria idénticas entre VMs—especialmente común cuando
    muchas VMs ejecutan la misma imagen del SO.
  5. Transparent Huge Pages (THP) se introdujeron para mejorar el rendimiento usando páginas mayores automáticamente, pero
    pueden crear picos de latencia bajo presión de memoria debido al trabajo de compactación.
  6. ZFS ARC no es «solo caché». Compite con la memoria anónima. Si no lo limitas, consumirá RAM hasta que el kernel lo obligue
    a ceder—a veces demasiado tarde.
  7. Los cgroups cambiaron la situación: en lugar de que todo el host sea una familia feliz, ahora los límites de memoria pueden
    hacer que una sola VM o contenedor falle aun cuando el host parece bien.
  8. Antes se recomendaba swap obligatoriamente; luego la gente lo abusó; luego la gente lo rechazó; luego los SSD modernos hicieron
    que «un swap pequeño y controlado» vuelva a tener sentido en muchos casos.

Una cita operativa que sigue siendo dolorosamente relevante (idea parafraseada): Werner Vogels ha dicho que el núcleo de la
fiabilidad es esperar fallos y diseñar para ello, no fingir que no ocurrirán.

Ballooning: qué hace, qué no hace y por qué te confunde

Qué es realmente el ballooning

El ballooning usa un driver dentro del invitado (típicamente virtio-balloon). El host le pide al invitado «inflar» un globo,
es decir: asignar memoria dentro del invitado y fijarla para que el invitado no pueda usarla. Desde la perspectiva del host,
esa memoria pasa a ser reclamable porque el invitado la cedió voluntariamente.

Es ingenioso. También está limitado por la física y el comportamiento del invitado:

  • Si el invitado está bajo presión real de memoria, no puede darte mucho sin hacer swap o sufrir OOM él mismo.
  • Si el invitado no tiene el driver del balloon, el ballooning es básicamente danza interpretativa.
  • El ballooning es reactivo. Si el host ya está en problemas, puede que llegues demasiado tarde.

Ballooning en Proxmox: la trampa importante

La configuración de ballooning en Proxmox a menudo da una falsa sensación de seguridad. La gente pone objetivos de balloon bajos y
memoria máxima alta, pensando que «solo usan el objetivo». Pero la contabilidad de QEMU y la lógica de compromiso del kernel
a menudo deben considerar la máxima.

Postura operativa: el ballooning es una herramienta de ajuste, no una excusa para evitar dimensionar. Úsalo para
cargas elásticas donde el SO invitado pueda manejarlo. No lo uses como estrategia principal para meter VMs en un host hasta que
este se queje.

Cuándo vale la pena el ballooning

  • Clusters de desarrollo/pruebas donde los invitados están inactivos y los picos son raros y tolerables.
  • Flotas tipo VDI con muchas VMs similares, a menudo combinadas con KSM.
  • Servidores de propósito general donde puedes imponer valores máximos sensatos, no fantasiosos.

Cuándo el ballooning es una trampa

  • Bases de datos con latencia estricta y pools de búfer (la presión de memoria en el invitado se convierte en presión IO).
  • Sistemas con swap deshabilitado en los invitados (el ballooning puede forzar OOM dentro del invitado).
  • Hosts ya ajustados en memoria donde el tiempo de respuesta del ballooning es demasiado lento.

Sobreasignación: cuándo es sensata y cuándo es temeraria

Tres «sobreasignaciones» diferentes que la gente confunde

En la práctica, estás manejando tres capas:

  1. Sobreasignación del scheduler/contabilidad de Proxmox: si Proxmox cree que está bien iniciar otra VM
    según la RAM configurada, objetivos de balloon y la memoria del nodo.
  2. Sobreasignación de memoria virtual de Linux: vm.overcommit_memory y CommitLimit.
  3. Sobreasignación física real: si la suma de la memoria activamente usada por los invitados excede la RAM del host
    (y si tienes swap, compresión o un plan).

Contabilidad de commit de Linux en un párrafo operativo

Linux decide si permitir una asignación basada en cuánto podría usarse si los procesos lo tocan. Ese número «podría usarse» se
rastrea como Committed_AS. El techo permitido es CommitLimit, aproximadamente RAM + swap menos algunas reservas,
modificado por las configuraciones de overcommit. Si Committed_AS se acerca al CommitLimit, el kernel empieza
a rechazar asignaciones—hola, «no se puede asignar memoria».

Guía con opinión

  • Producción: mantén la sobreasignación moderada y aplica máximos de VM realistas. Si no puedes declarar tu
    ratio de sobreasignación y tu plan de expulsión, no estás sobreasignando—estás apostando.
  • Laboratorio: sobreasigna agresivamente si aceptas eventos OOM ocasionales. Solo etiquétalo honestamente y deja de fingir que es prod.
  • Cargas mixtas: separa usuarios ruidosos de memoria (BD, analítica) en sus propios nodos o capéalos fuertemente.
    «Coexistencia» es como la gente llama a la situación justo antes de la revisión del incidente.

ZFS ARC, caché de páginas y la memoria del host que olvidaste presupuestar

Proxmox a menudo se ejecuta sobre ZFS porque los snapshots y send/receive son adictivos. Pero ZFS no tiene vergüenza: usará RAM
para ARC (Adaptive Replacement Cache). Eso es genial hasta que no lo es.

ARC frente a «memoria libre»

ARC es reclamable, pero no instantáneamente y no siempre de la forma que quiere el inicio de una VM. Bajo presión, el kernel
intenta reclamar page cache y ARC, pero si estás en un bucle ajustado de asignaciones (iniciar una VM, inflar memoria,
forkear procesos), puedes sufrir fallos transitorios.

Qué hacer

  • En hosts ZFS con muchas VMs, fija un máximo sensato para ARC (zfs_arc_max). No dejes que ARC «compita» con tus invitados.
  • Trata la memoria del host como infraestructura compartida. El host necesita memoria para:
    kernel, slab, red, metadatos ZFS, overhead de QEMU y tus agentes de monitorización que juran ser ligeros.

Swap: no es un pecado, pero tampoco un plan de vida

No tener swap significa que has quitado los amortiguadores. Con virtualización, eso puede ser fatal porque un pico repentino
de presión se convierte en kills OOM inmediatos en lugar de una degradación lenta y diagnosticable.

Pero el swap también puede convertirse en un lodazal de rendimiento. El objetivo es un swap controlado: suficiente para
sobrevivir a ráfagas, no suficiente para ocultar una sobreasignación crónica.

Recomendaciones de swap para el host (prácticas, no dogmáticas)

  • Si ejecutas ZFS y muchas VMs: añade swap. Incluso una cantidad moderada puede evitar que el host mate
    QEMU durante picos breves.
  • Si tu almacenamiento es lento: mantén el swap más pequeño y prioriza un dimensionamiento correcto de RAM.
    Hacer swap a un RAID HDD ocupado no es «estabilidad», es «sufrimiento prolongado».
  • Si usas SSD/NVMe: el swap es mucho más tolerable, pero sigue sin ser gratuito. Monitoriza la tasa de swap-in/out,
    no solo el swap usado.

Chiste #2: El swap es como una reunión que pudo haber sido un correo—a veces salva el día, pero si vives allí, tu carrera se acaba.

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

Estas son las comprobaciones que realmente ejecuto cuando un nodo Proxmox empieza a lanzar errores de asignación de memoria. Cada tarea incluye:
un comando, salida de ejemplo, qué significa y qué decisión impulsa.

Tarea 1: Comprobar RAM y swap del host de un vistazo

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            62Gi        54Gi       1.2Gi       2.3Gi       6.9Gi       2.8Gi
Swap:            8Gi       1.6Gi       6.4Gi

Significado: «available» es tu margen a corto plazo antes de que el reclaim se ponga feo. 2.8 GiB en un host de 62 GiB
con virtualización es ajustado pero no instantáneamente catastrófico.

Decisión: Si available es < 1–2 GiB y las VMs están fallando al iniciar, detén VMs no críticas ahora.
Si swap es 0, añade swap como estabilizador mientras corriges el dimensionado.

Tarea 2: Identificar si el kernel está rechazando asignaciones por límites de commit

cr0x@server:~$ grep -E 'CommitLimit|Committed_AS' /proc/meminfo
CommitLimit:    71303168 kB
Committed_AS:   70598240 kB

Significado: Estás cerca del techo de commit. El kernel puede rechazar nuevas reservas de memoria incluso si
hay caché que podría reclamarse.

Decisión: Reduce las memorias máximas de las VMs, añade swap (aumenta CommitLimit) o mueve cargas.
Cambios en el objetivo del balloon no ayudarán si el problema es el máximo.

Tarea 3: Confirmar la política de sobreasignación

cr0x@server:~$ sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Significado: El modo 0 es overcommit heurístico. Ratio importa sobre todo para el modo 2. Aún así, la conducta de commit está en juego.

Decisión: No cambies esto en pánico a menos que entiendas el impacto. Si estás tocando los límites de commit,
arreglar el dimensionado es mejor que «simplemente sobreasignar más».

Tarea 4: Buscar evidencia del OOM killer en el host

cr0x@server:~$ journalctl -k -b | tail -n 30
Dec 26 10:14:03 pve1 kernel: Out of memory: Killed process 21433 (qemu-system-x86) total-vm:28751400kB, anon-rss:23110248kB, file-rss:0kB, shmem-rss:0kB
Dec 26 10:14:03 pve1 kernel: oom_reaper: reaped process 21433 (qemu-system-x86), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Significado: El host mató a QEMU. Esa VM no «falló», fue ejecutada.

Decisión: Trátalo como agotamiento de memoria del host/sobreasignación. Reduce consolidación, capea ARC, añade swap
y deja de confiar en el ballooning como cinturón de seguridad.

Tarea 5: Comprobar la presión de memoria y el comportamiento de reclaim en vivo

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
 6  1 1677720 312000  8200 5120000  40  120   180   260  900 1800 18 12 55 15  0
 5  2 1677800 280000  8100 5010000  10  200   140   320  920 1700 15 10 50 25  0
 7  3 1677850 260000  8000 4920000  80  500   220   600 1100 2200 20 15 35 30  0

Significado: Valores no nulos en si/so indican swapping. Alto wa sugiere IO wait.
Si b crece y id se desploma, el host está thrashing.

Decisión: Si el swapping es sostenido y el IO wait sube, detén VMs o mueve carga. No puedes «afinar» para salir de una tormenta de thrash en tiempo real.

Tarea 6: Encontrar los mayores consumidores de memoria en el host (RSS, no fantasías VIRT)

cr0x@server:~$ ps -eo pid,comm,rss,vsz --sort=-rss | head -n 10
 21433 qemu-system-x86 23110248 28751400
 19877 qemu-system-x86 16188012 21045740
  1652 pveproxy          312400  824000
  1321 pvedaemon         210880  693000
  1799 zfs               180200  0
  1544 pvestatd          122000  610000

Significado: RSS es memoria residente real. Los procesos QEMU dominan, como era de esperar.

Decisión: Si una VM se descontrola, capea su memoria máxima o investiga dentro del invitado.
Si son «muchas VMs medianas», es matemática de consolidación, no un villano único.

Tarea 7: Inspeccionar la configuración de memoria de una VM (ballooning vs max)

cr0x@server:~$ qm config 104 | egrep 'memory|balloon|numa|hugepages'
memory: 32768
balloon: 8192
numa: 1
hugepages: 2

Significado: Max es 32 GiB, objetivo del balloon 8 GiB. Hugepages están habilitadas (2 = hugepages de 2MB).

Decisión: Si el nodo falla al asignar, el max de 32 GiB de esta VM podría ser demasiado generoso.
Si hugepages están habilitadas, confirma disponibilidad de hugepages (Tarea 8) o deshabilítalas para más flexibilidad.

Tarea 8: Validar disponibilidad de hugepages (causa clásica de fallos al iniciar)

cr0x@server:~$ grep -i huge /proc/meminfo
AnonHugePages:   1048576 kB
HugePages_Total:    8192
HugePages_Free:      120
HugePages_Rsvd:       50
Hugepagesize:       2048 kB

Significado: Solo 120 hugepages libres (~240 MiB). Si intentas iniciar una VM que necesita muchas hugepages, falla.

Decisión: O provisiona suficientes hugepages en el arranque, o deja de usar hugepages para esa clase de VMs.
Las hugepages son una herramienta de rendimiento, no un defecto por defecto.

Tarea 9: Comprobar el comportamiento de THP (puede causar latencia bajo presión)

cr0x@server:~$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

Significado: THP está habilitado siempre.

Decisión: Para nodos sensibles a la latencia, considera madvise o never.
No cambies esto en medio de un incidente a menos que estés seguro; planea un mantenimiento y mide.

Tarea 10: Si usas ZFS, verifica rápidamente el tamaño del ARC

cr0x@server:~$ awk '/^size/ {print}' /proc/spl/kstat/zfs/arcstats
size                            4    34359738368

Significado: ARC es ~32 GiB. En un host de 64 GiB con muchas VMs, eso puede ser demasiado.

Decisión: Si te falta memoria y ARC es grande, limita ARC de forma persistente (ver sección de checklist)
y planifica un reinicio si necesitas alivio inmediato.

Tarea 11: Confirmar estado de KSM (ayuda con muchas VMs similares, pero puede costar CPU)

cr0x@server:~$ systemctl is-active ksmtuned
inactive

Significado: El servicio de tuning de KSM no está activo. En algunas configuraciones Proxmox, KSM se configura diferente;
esto es solo una señal rápida.

Decisión: Si ejecutas docenas de VMs similares, habilitar KSM puede reducir uso de memoria. Si la CPU ya está caliente,
KSM puede ser contraproducente. Prueba en un nodo primero.

Tarea 12: Comprobar info de memoria en el nodo Proxmox (qué cree Proxmox que está pasando)

cr0x@server:~$ pvesh get /nodes/pve1/status | egrep '"memory"|"swap"|"loadavg"'
"loadavg": [
  "2.61",
  "2.45",
  "2.31"
],
"memory": {
  "free": 1288490188,
  "total": 66571993088,
  "used": 651834
},
"swap": {
  "free": 6871947673,
  "total": 8589934592,
  "used": 1717986919
}

Significado: La API de Proxmox da una vista que puede diferir de tus expectativas inmediatas (unidades,
caching y tiempo). No la trates como verdad absoluta; coteja con free y meminfo.

Decisión: Usa esto para automatización y dashboards, pero al depurar fallos de asignación, confía primero
en la evidencia del kernel y los logs de QEMU.

Tarea 13: Inspeccionar un fallo de inicio de VM en los logs de tareas

cr0x@server:~$ journalctl -u pvedaemon -b | tail -n 20
Dec 26 10:18:11 pve1 pvedaemon[1321]: start VM 104: UPID:pve1:0000A3F9:00B2B6D1:676D5A13:qmstart:104:root@pam:
Dec 26 10:18:12 pve1 pvedaemon[1321]: VM 104 qmp command failed - unable to execute QMP command 'cont': Cannot allocate memory
Dec 26 10:18:12 pve1 pvedaemon[1321]: start failed: command '/usr/bin/kvm -id 104 ...' failed: exit code 1

Significado: El fallo está en la etapa de arranque/cont de QEMU, no dentro del invitado.

Decisión: Concéntrate en límites de commit del host, hugepages y fragmentación—no en ajustes dentro del invitado.

Tarea 14: Validar la configuración de memoria de un contenedor (LXC) y el límite de swap

cr0x@server:~$ pct config 210 | egrep 'memory|swap|features'
memory: 4096
swap: 512
features: nesting=1,keyctl=1

Significado: El contenedor tiene 4 GiB RAM y 512 MiB de swap permitido. Si hace picos por encima, las asignaciones fallan dentro del contenedor.

Decisión: Para contenedores, «no se puede asignar memoria» suele ser un límite de cgroup. Aumenta memory/swap
o arregla el comportamiento de la aplicación. La memoria libre del host no salvará a un LXC con un techo rígido.

Tarea 15: Comprobar señales de riesgo de fragmentación (rápido y tocho)

cr0x@server:~$ cat /proc/buddyinfo | head
Node 0, zone      DMA      1      1      1      1      0      0      0      0      0      0      0
Node 0, zone    DMA32   1024    512    220     12      0      0      0      0      0      0      0
Node 0, zone   Normal   2048   1880    940    110      2      0      0      0      0      0      0

Significado: El buddy allocator muestra cuántos bloques libres existen en diferentes órdenes. Si los órdenes altos están
mayormente en cero, las asignaciones contiguas grandes (incluyendo algunas necesidades de hugepage) pueden fallar aun con «suficiente total libre».

Decisión: Si las hugepages/THP/compactación son parte de tu configuración, considera reducir la dependencia de asignaciones
contiguas o programar reinicios de mantenimiento periódicos para nodos que deben satisfacer esas asignaciones.

Tres microhistorias corporativas desde el frente

Incidente: una suposición errónea («ballooning significa que no reservará el máximo»)

Una compañía mediana ejecutaba un cluster Proxmox interno para apps de línea de negocio y algunos trabajos batch pesados.
El equipo tenía la costumbre: poner la memoria máxima de la VM alta «para que nadie tenga que abrir un ticket», y luego poner
el objetivo del balloon bajo para «mantener la utilización eficiente».

Funcionó—hasta que actualizaron algunas VMs y lanzaron una corrida de reportes trimestrales. Nuevos procesos se generaron, los mapas de memoria
se expandieron y varias VMs se reiniciaron para parcheo. De repente: no se puede asignar memoria al iniciar VMs.
El dashboard aún mostraba «free» porque la caché parecía reclamable.

La causa raíz no fue una fuga. Fue contabilidad. El Committed_AS del host se acercó al CommitLimit.
Cada VM con un máximo generoso contribuyó al total de memoria prometida, aunque «normalmente» estuviera baja. Cuando varios
reinicios ocurrieron juntos, QEMU intentó reservar lo que se le había dicho que podía necesitar. El kernel se negó. El error
fue correcto; su modelo mental no.

La solución fue aburrida: redujeron la memoria máxima de las VMs a lo que cada servicio podía justificar, mantuvieron el ballooning
para elasticidad y añadieron swap en hosts donde faltaba. Lo más importante: dejaron de tratar el «max» como un deseo.
La siguiente corrida trimestral aún hizo picos, pero dejó de romper los reinicios.

Optimización que salió mal (hugepages por todas partes)

Otra organización persiguió la latencia. Un ingeniero orientado al rendimiento habilitó hugepages para toda una clase de VMs porque un blog
decía que mejoraba el comportamiento del TLB. Y puede hacerlo. También dejaron Transparent Huge Pages en «always», porque más hugepages
sonaban a más rendimiento. Así es como el optimismo se vuelve configuración.

Durante semanas, todo parecía bien. Luego un nodo empezó a fallar al iniciar VMs tras migraciones rutinarias. La misma VM arrancaba en
otros nodos. En este nodo: no se puede asignar memoria. La memoria libre no estaba mal, pero las hugepages libres estaban casi a cero.
Buddyinfo mostró fragmentación: la memoria existía, solo que no en los trozos adecuados.

Intentaron «arreglarlo» aumentando hugepages dinámicamente. Eso lo empeoró: el kernel tuvo que compactar memoria para satisfacer la petición,
generando picos de CPU y bloqueando reclaim. La latencia se disparó durante las horas punta. Lo mejor es que el informe de incidente lo llamó
«intermitente». Fue intermitente como la gravedad lo es cuando estás dentro de una habitación.

El plan de recuperación fue: deshabilitar hugepages para VMs generales, reservar hugepages solo para un pequeño conjunto de instancias
críticas y previsibles, y poner THP en madvise. El rendimiento mejoró en general porque el sistema dejó de pelear consigo mismo.

Práctica aburrida pero correcta que salvó el día (reserva y límites en el host)

Un tercer equipo usaba Proxmox para cargas mixtas: apps web, algunas VMs Windows y un par de appliances intensivos en almacenamiento.
Tenían una regla sosa: cada nodo mantiene una «reserva del host» fija de RAM que nunca se asigna a invitados en papel.
También limitaron ZFS ARC desde el primer día.

No era sofisticado. Significaba que podían ejecutar menos VMs por nodo de lo que la hoja de cálculo quería. Pero durante un incidente
donde un invitado ruidoso empezó a consumir memoria (un servicio Java mal configurado), el host tuvo suficiente margen para mantener
vivos los procesos QEMU y evitar un OOM del host.

El invitado aún sufrió (como debía), pero el radio de impacto se quedó dentro de esa VM. El cluster no empezó a matar cargas no relacionadas.
Dranearon el nodo, arreglaron la configuración del invitado y reanudaron. Sin reinicio a medianoche, sin fallos en cascada, sin «¿por qué murió nuestra VM de firewall?»

La práctica que los salvó no fue un ajuste secreto del kernel. Fue presupuestar y negarse a gastar el fondo de emergencia.

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

La VM no arranca: «No se puede asignar memoria» inmediatamente

  • Síntoma: El inicio falla al instante; QEMU sale con error de asignación.
  • Causa raíz: Límite de commit del host alcanzado, hugepages faltantes o fragmentación para la asignación solicitada.
  • Solución: Baja la memoria máxima de la VM; añade swap en el host; deshabilita hugepages para esa VM; provisiona hugepages en el arranque si es necesario.

La VM arranca y luego se apaga o reinicia aleatoriamente

  • Síntoma: La VM parece «caerse», los logs no muestran un apagado limpio.
  • Causa raíz: El OOM killer del host mató a QEMU, a menudo tras un pico de memoria o un reclaim intenso.
  • Solución: Encuentra logs de OOM; reduce la sobreasignación del host; reserva memoria del host; capea ZFS ARC; asegúrate de que exista swap y monitoriza su actividad.

Los invitados se vuelven lentos, luego el host se vuelve lento y todo se vuelve filosófico

  • Síntoma: IO wait sube; tasas de swap-in/out aumentan; la latencia de las VMs se dispara.
  • Causa raíz: Thrashing: no hay suficiente RAM para los working sets, y swap/reclaim domina.
  • Solución: Detén o migra VMs; reduce límites de memoria; añade RAM; rediseña la consolidación. Ningún sysctl te salvará aquí.

Ballooning habilitado pero la memoria nunca «vuelve»

  • Síntoma: El host sigue lleno; los invitados no liberan memoria como se esperaba.
  • Causa raíz: Driver de balloon no instalado/activo, el invitado no puede reclamar o el «max» aún obliga al compromiso del host.
  • Solución: Instala el driver virtio-balloon; verifica dentro del invitado; fija un max realista; usa ballooning como elasticidad, no como sustituto del dimensionado.

Todo iba bien hasta que aumentaron los snapshots y la replicación de ZFS

  • Síntoma: La presión de memoria del host aumenta durante actividad intensiva de almacenamiento; se fallan inicios de VM.
  • Causa raíz: Crecimiento del ARC, presión de metadatos, crecimiento de slab y uso de memoria dependiente de IO.
  • Solución: Capa ARC; monitoriza slab; mantén margen; evita correr el nodo al 95% de «usado» y llamarlo eficiente.

Los contenedores muestran «no se puede asignar memoria» mientras el host tiene bastante

  • Síntoma: Apps dentro de LXC fallan asignaciones; el host parece bien.
  • Causa raíz: Límite de memoria del cgroup alcanzado (cupo memory/swap del contenedor).
  • Solución: Eleva límites del contenedor; ajusta la aplicación; asegúrate de que el swap del contenedor esté permitido si esperas picos.

Listas de comprobación / plan paso a paso

Paso a paso: arreglar un nodo que lanza errores de asignación

  1. Confirma OOM del host vs fallo en tiempo de inicio.
    Revisa journalctl -k por kills OOM y logs de pvedaemon para el contexto del fallo de inicio.
  2. Mide la presión de commit.
    Si Committed_AS está cerca de CommitLimit, estás en territorio de «promesas exceden la realidad».
  3. Lista VMs con memoria máxima grande.
    Reduce la memoria máxima de las culpables. No te limites a ajustar objetivos de balloon.
  4. Comprueba hugepages y ajustes de THP.
    Si hugepages están habilitadas para VMs, asegúrate de preasignación adecuada o apágalas para cargas generales.
  5. Comprueba ZFS ARC si aplica.
    Si ARC es grande y eres primero un host de VMs, capéalo.
  6. Asegúrate de que exista swap y sea sensato.
    Añade swap si no hay; monitoriza si/so. El swap es para picos, no para pagar la renta.
  7. Reserva memoria para el host.
    Mantén un buffer fijo para host + ZFS + overhead de QEMU. Tu yo futuro te lo agradecerá en silencio.
  8. Vuelve a probar inicios de VM en secuencia controlada.
    No inicies todo de golpe tras ajustar. Inicia servicios críticos primero.

Ajuste persistente: tope de ZFS ARC (ejemplo)

Si el nodo es un host de VMs y ZFS es un medio, establece un máximo para ARC. Un método común:
crea un archivo de configuración de modprobe y actualiza initramfs para que se aplique al arranque.

cr0x@server:~$ echo "options zfs zfs_arc_max=17179869184" | sudo tee /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=17179869184
cr0x@server:~$ sudo update-initramfs -u
update-initramfs: Generating /boot/initrd.img-6.8.12-4-pve

Significado: ARC capado a 16 GiB (el valor está en bytes). Le acabas de decir a ZFS que no se coma toda la máquina.

Decisión: Elige un tope que deje suficiente RAM para invitados más la reserva del host. Valida tras reiniciar leyendo arcstats de nuevo.

Ajuste persistente: añadir swap al host (ejemplo con archivo)

cr0x@server:~$ sudo fallocate -l 8G /swapfile
cr0x@server:~$ sudo chmod 600 /swapfile
cr0x@server:~$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 8 GiB (8589930496 bytes)
no label, UUID=0a3b1e4c-2f1e-4f65-a3da-b8c6e3f3a8d7
cr0x@server:~$ sudo swapon /swapfile
cr0x@server:~$ swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   8G   0B   -2

Significado: El swap está activo. CommitLimit aumenta y tienes un colchón contra ráfagas de asignación.

Decisión: Si el uso de swap se vuelve sostenido con alto si/so, eso no está «funcionando como diseñado».
Es señal de reducir consolidación o añadir RAM.

Política: reservar RAM para el host (una regla simple que funciona)

  • Reserva al menos 10–20% de la RAM del host para el host en nodos mixtos.
    Más si ejecutas ZFS, Ceph, redes pesadas o muchas VMs pequeñas.
  • Mantén un objetivo de «suma máxima de memoria de invitados» que puedas defender. Si la suma de los valores máximos de VM excede un múltiplo fijo
    de la RAM del host, hazlo intencionalmente y solo donde el comportamiento de cargas lo soporte.

Checklist de ballooning (úsalo correctamente)

  • Habilita ballooning solo si el invitado tiene soporte virtio-balloon.
  • Configura la memoria máxima cerca de la realidad; el objetivo del balloon puede ser más bajo para inactividad.
  • Monitoriza swap del invitado y eventos OOM en el invitado después de habilitar ballooning.
  • No balloonices bases de datos a menos que aceptes picos de IO y latencia impredecible.
  • Preguntas frecuentes

    1) ¿Por qué Proxmox dice «no se puede asignar memoria» cuando free muestra GB libres?

    Porque free muestra una instantánea de la memoria física, mientras que la contabilidad de commit del kernel y las reglas de fragmentación
    pueden negar una nueva asignación. Además, «free» ignora si la memoria está disponible en la forma necesaria (p. ej., hugepages).

    2) ¿El ballooning reduce lo que el host debe reservar?

    Reduce lo que el invitado usa en tiempo de ejecución, pero si tu VM tiene un máximo alto, el host todavía puede estar obligado por la promesa.
    Ballooning no es un salvoconducto para evitar dimensionar.

    3) ¿Debería poner vm.overcommit_memory=1 para evitar fallos de asignación?

    Eso es una herramienta brusca. Puede reducir fallos en el inicio, pero aumenta la posibilidad de un OOM catastrófico después.
    En producción, es preferible arreglar el dimensionado de VMs y añadir swap antes que aflojar las barreras de seguridad del kernel.

    4) ¿Cuánto swap debería tener un host Proxmox?

    Suficiente para sobrevivir ráfagas y mejorar CommitLimit, no tanto como para ocultar una sobreasignación crónica. Comúnmente: unos pocos GB hasta decenas bajas de GB
    dependiendo de la RAM del host y la volatilidad de las cargas. Mide la actividad de swap; si está constantemente ocupada, estás subdimensionado.

    5) ¿Es ZFS ARC la razón por la que mi nodo «se queda sin memoria»?

    A veces. ARC puede crecer y competir con las VMs. Si los inicios de VM fallan o el host OOM mientras ARC es masivo,
    limita ARC. Si ARC es moderado, busca en otro lado (límites de commit, hugepages, invitados descontrolados).

    6) ¿Debería habilitar KSM en Proxmox?

    Si ejecutas muchas VMs similares (mismo SO, páginas de memoria parecidas), KSM puede ahorrar RAM. Cuesta CPU y puede añadir latencia.
    Habilítalo deliberadamente, mide la sobrecarga de CPU y no lo trates como memoria gratis.

    7) ¿Por qué los contenedores reciben «no se puede asignar memoria» cuando el host está bien?

    LXC está gobernado por cgroups. Un contenedor puede quedarse sin memoria dentro de su límite aunque el host tenga bastante.
    Ajusta límites con pct o arregla la carga del contenedor.

    8) ¿Merecen la pena las hugepages?

    Para ciertas cargas de alto rendimiento y baja latencia: sí. Para consolidación general: a menudo no.
    Las hugepages aumentan la predictibilidad del comportamiento del TLB pero reducen la flexibilidad y pueden crear fallos de inicio si no se provisioning con cuidado.

    9) ¿Cuál es la diferencia entre OOM del invitado y OOM del host?

    OOM del invitado ocurre dentro de la VM: el kernel del invitado mata procesos, pero la VM sigue en pie. OOM del host mata procesos en
    el hipervisor, incluyendo QEMU—tu VM desaparece. El OOM del host es el que arruina la tarde.

    10) ¿Puedo «arreglar» esto permanentemente sin añadir RAM?

    A menudo sí: fija memorias máximas realistas para las VMs, reserva RAM para el host, limita ARC si hace falta y evita ratios de sobreasignación
    que asumen milagros. Si los working sets exceden genuinamente la RAM física, la solución permanente es: más RAM o menos cargas por nodo.

    Próximos pasos (los sensatos)

    «No se puede asignar memoria» en Proxmox no es una maldición. Es el kernel aplicando un límite que ya cruzaste en
    política, configuración o expectativas.

    1. Deja de tratar la memoria máxima de la VM como sugerencia. Hazla un contrato.
    2. Usa ballooning para elasticidad, no para negación. Objetivo bajo, tope realista.
    3. Da al host un fondo de emergencia. Reserva RAM; añade swap; mantén ZFS ARC en su carril.
    4. Prefiere nodos predecibles sobre ajustes heroicos. Separa cargas cuando sus modos de fallo difieran.
    5. Operationaliza esto. Añade alertas para proximidad a CommitLimit, tasa de swap-in/out, logs OOM y tamaño de ARC.

    Haz eso, y la próxima vez que Proxmox se queje de memoria, será porque realmente te quedaste sin ella—no porque tu
    configuración contó una historia encantadora que el kernel se negó a creer.

    PostgreSQL vs SQLite en un VPS: la elección rápida sin remordimientos

    Estás en un VPS. Quieres “una base de datos”. No un proyecto de fin de semana, ni un rebaño de yaks. Algo que no te despierte a las 03:00 porque un único archivo se quedó atascado, o porque tu app de pronto tiene tráfico real y tu elección “simple” se convierte en una migración con dientes.

    La forma más rápida de elegir entre PostgreSQL y SQLite es dejar de discutir características y empezar a hacer una pregunta brutal: ¿dónde está tu límite de concurrencia y de fallo? Si está dentro de un solo proceso, SQLite es un bisturí. Si está entre muchos procesos, usuarios, trabajos y conexiones, PostgreSQL es la llave aburrida y probada en batalla.

    La decisión en un minuto

    Si solo lees esta sección, aún tomarás una decisión respetable.

    Elige SQLite si todas estas son ciertas

    • Tu app es mayormente de un solo escritor y de tráfico moderado (piensa: un proceso web o un worker de cola que hace escrituras, no un enjambre).
    • Puedes convivir con semánticas de bloqueo por fichero y algún “database is locked” ocasional si lo usas mal.
    • Quieres cero sobrecarga operativa: sin demonio, sin ajuste de vacuum en segundo plano, sin drama de pooling de conexiones.
    • Tu dominio de fallo es “este VPS y este disco” y estás bien con eso.
    • Quieres paridad de desarrollo local fácil: distribuir un solo archivo de BD es una ventaja potente.

    Elige PostgreSQL si alguna de estas es cierta

    • Tienes múltiples escritores, varias instancias de la app, cron jobs, workers, consultas analíticas, herramientas de administración… cualquier cosa que se comporte como una pequeña multitud.
    • Necesitas concurrencia robusta sin convertir tu app en un coordinador de bloqueos.
    • Te importan las garantías de aislamiento, durabilidad y recuperabilidad frente a modos de fallo del mundo real.
    • Quieres cambios de esquema en línea, indexación más rica y planes de consulta que escalen más allá de “mono lindo”.
    • Prevés crecimiento y prefieres escalar añadiendo CPU/RAM ahora y réplicas después, en lugar de hacer una migración de alto riesgo más adelante.

    Regla empírica: si tu base de datos tiene que mediar la impaciencia humana (tráfico web) y la impaciencia de la máquina (trabajos), PostgreSQL es el adulto en la sala.

    Broma #1: SQLite es como una bicicleta: rápida, elegante y perfecta hasta que intentas mover un sofá con ella.

    Un modelo mental que evita arrepentimientos

    La mayoría de los debates “Postgres vs SQLite” mueren porque la gente compara sintaxis SQL o listas de características. La elección es realmente sobre la forma operativa: quién habla con la base de datos, con qué frecuencia y qué pasa cuando las cosas van mal.

    SQLite: una biblioteca con un archivo, no un servidor

    SQLite se ejecuta en proceso. No hay un demonio servidor aceptando conexiones. Tu app enlaza una biblioteca; la “base de datos” es un archivo (más archivos opcionales de journaling/WAL). Eso significa:

    • La latencia puede ser excelente porque no hay salto de red. Las llamadas son llamadas a funciones.
    • La concurrencia está limitada por el bloqueo de archivos. Las lecturas van bien. Las escrituras requieren coordinación; WAL mejora esto pero no lo convierte en una orgía de escrituras.
    • La durabilidad depende de las semánticas del sistema de archivos, opciones de montaje y tu uso de ajustes síncronos. No es “inseguro”, es “tú manejas los bordes afilados”.
    • Las copias de seguridad son copias de archivos, lo cual puede ser maravillosamente simple—hasta que haces una copia en el momento equivocado sin usar las API de backup de SQLite.

    PostgreSQL: un servidor con procesos, memoria y opiniones

    PostgreSQL se ejecuta como servidor de base de datos con sus propios procesos, caches, write-ahead log (WAL), vacuum en segundo plano y semánticas transaccionales bien definidas. Eso implica:

    • Alta concurrencia con MVCC (control de concurrencia multiversión): los lectores no bloquean a los escritores como esperarías con bloqueos de archivo.
    • Durabilidad y recuperación tras fallos son pilares. Aún necesitas configurar y probar, pero el sistema está diseñado para los días malos.
    • Existe sobrecarga operativa: actualizaciones, backups, monitorización, vacuum y gestión de conexiones.
    • Las rutas de escalado son más claras: replicación, réplicas de lectura, particionado, poolers de conexión y herramientas maduras.

    La pregunta del límite

    Pregunta: “¿Es la base de datos un límite de servicio compartido?” Si la respuesta es sí, PostgreSQL. Si no, SQLite puede ser una base de datos de producción legítima. No subestimes cuán a menudo “no” se transforma silenciosamente en “sí” cuando añades un worker, luego una segunda instancia de la app, luego un panel de administración que ejecuta consultas pesadas.

    Datos interesantes y un poco de historia

    Un poco de contexto ayuda porque las decisiones de diseño no fueron arbitrarias. Son cicatrices de uso real.

    1. SQLite nació en 2000 como una base de datos embebida para evitar la sobrecarga de DB cliente/servidor para un proyecto específico; se convirtió en el motor SQL “pequeño” por defecto del mundo.
    2. PostgreSQL se remonta a los años 80 (proyecto POSTGRES en UC Berkeley), y su ADN se nota: extensibilidad, corrección y una obsesión académica por el comportamiento transaccional.
    3. SQLite es posiblemente el motor más desplegado porque se incluye en teléfonos, navegadores, sistemas operativos y multitud de aplicaciones como biblioteca.
    4. PostgreSQL popularizó la extensibilidad rica mediante tipos personalizados, operadores y extensiones; por eso es la plataforma “SQL plus” por defecto en muchas pilas modernas.
    5. El modo WAL de SQLite se añadió más tarde para reducir el bloqueo de escritores y mejorar la concurrencia; cambió para qué casos “SQLite es bueno” en producción.
    6. El MVCC de PostgreSQL hace que versiones antiguas de filas permanezcan hasta que vacuum las limpia; esto es una característica de rendimiento y una tarea operativa.
    7. SQLite es famoso por la portabilidad estricta de archivos de base de datos entre arquitecturas y versiones, pero aún depende del comportamiento del sistema de archivos para la durabilidad.
    8. El WAL de PostgreSQL también se llama WAL (mismo acrónimo, implementaciones distintas), y es la base para replicación y recuperación punto-en-tiempo.
    9. “database is locked” en SQLite no es un bug; es una consecuencia explícita del modelo de bloqueo. El bug es asumir que se comporta como una BD servidor.

    Realidades del VPS: discos, memoria y vecinos

    Un VPS no es un portátil ni una base de datos gestionada. Es una pequeña porción de una máquina más grande con IO compartido y, a veces, vecinos impredecibles. Tu elección de base de datos debería respetar eso.

    El IO de disco es la primera mentira que dicen tus benchmarks

    En un VPS, tu “SSD” puede ser rápido, o puede ser “rápido cuando los vecinos duermen”. SQLite y PostgreSQL ambos se preocupan por el comportamiento de fsync, pero lo experimentan de forma distinta:

    • SQLite escribe en un único archivo de base de datos (más journaling/WAL). Las escrituras aleatorias pueden ser castigadoras si tu carga tiene mucha churn.
    • PostgreSQL escribe en múltiples archivos: archivos de datos y segmentos WAL. Las escrituras WAL son más secuenciales y pueden ser más amables con discos reales, pero ahora tienes procesos en segundo plano y checkpoints.

    La memoria no es solo “cache”; es política

    SQLite depende en gran medida del caché de páginas del SO. Eso está bien—Linux es bueno para cachear. PostgreSQL tiene sus propios shared buffers además del caché del SO. Si lo dimensionas mal en un VPS pequeño, puedes terminar con doble cache y dejando al resto del sistema sin recursos.

    El modelo de procesos importa cuando tienes poca RAM

    SQLite vive dentro del proceso de tu app. PostgreSQL usa múltiples procesos y memoria por conexión. En un VPS de 1 GB, un montón de conexiones inactivas puede ser un fallo de rendimiento, no un detalle menor. Si ejecutas Postgres en hierro pequeño, aprenderás a amar el pooling de conexiones.

    Radio de impacto operativo

    El radio de impacto de SQLite suele ser “este archivo”. El de PostgreSQL es “este clúster”, pero con mejores herramientas para aislar y recuperar. SQLite puede recuperarse copiando un archivo—a menos que lo copies en el momento equivocado. PostgreSQL puede recuperarse reproduciendo WAL—a menos que nunca hayas probado tus backups. Elige tu veneno; luego mitígalo.

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

    Abajo hay tareas que puedes ejecutar en un VPS hoy. Cada una te da una señal, no una corazonada. El objetivo es decidir basándote en evidencia: capacidad de IO, necesidades de concurrencia y riesgos de fallo.

    Task 1: Comprueba la presión de CPU y memoria (¿te permiten siquiera ejecutar Postgres?)

    cr0x@server:~$ lscpu | egrep 'Model name|CPU\(s\)'
    CPU(s):                               2
    Model name:                           Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz
    
    cr0x@server:~$ free -h
                   total        used        free      shared  buff/cache   available
    Mem:           1.0Gi       220Mi       180Mi        12Mi       620Mi       690Mi
    Swap:          1.0Gi         0B       1.0Gi
    

    Qué significa: Con 1 GB de RAM, Postgres es posible pero debes ser disciplinado (pool de conexiones, ajustar memoria). SQLite se sentirá sin esfuerzo.

    Decisión: Si no puedes permitirte unos cientos de MB para Postgres más margen para tu app, prefiere SQLite o mejora el VPS.

    Task 2: Identifica el tipo de almacenamiento y opciones de montaje (la durabilidad vive aquí)

    cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /
    /dev/vda1 ext4 rw,relatime,errors=remount-ro
    

    Qué significa: ext4 con relatime es normal. Si ves opciones extrañas como data=writeback o sistemas de archivos en red exóticos, debes tratar las afirmaciones de durabilidad de SQLite con sospecha y ajustar Postgres también con cuidado.

    Decisión: Si estás en almacenamiento en red o extraño, Postgres con WAL+fsync probado suele ser más seguro que “copiar el archivo de la BD”.

    Task 3: Chequeo rápido de latencia de disco (tu futuro ticket “bd lenta”)

    cr0x@server:~$ iostat -xz 1 3
    Linux 6.2.0 (server) 	12/30/2025 	_x86_64_	(2 CPU)
    
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
               3.10    0.00    1.20    0.40    0.10   95.20
    
    Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
    vda              5.00    8.00   80.0   210.0   2.10   0.40   0.52
    

    Qué significa: await en dígitos bajos es aceptable. Si ves picos de 20–100 ms, tanto SQLite como Postgres sufrirán, pero SQLite lo mostrará como bloqueos dentro de los hilos de la app.

    Decisión: Alto IO wait aboga por Postgres con ajuste cuidadoso de checkpoints y posiblemente mover a mejor almacenamiento; también aboga por reducir la amplificación de escritura de cualquier forma.

    Task 4: Mide el coste de sync del sistema de archivos (SQLite y Postgres pagan esta factura)

    cr0x@server:~$ sudo dd if=/dev/zero of=/var/tmp/fsync.test bs=4k count=25000 conv=fdatasync status=progress
    102400000 bytes (102 MB, 98 MiB) copied, 1.52 s, 67.4 MB/s
    25000+0 records in
    25000+0 records out
    102400000 bytes (102 MB, 98 MiB) copied, 1.52 s, 67.3 MB/s
    

    Qué significa: Es burdo, pero aproxima “qué doloroso es forzar durabilidad”. Si esto es glacial, tus ajustes “seguros” harán daño.

    Decisión: Si el sync forzado es caro, SQLite necesita WAL + ajustes síncronos sensatos; Postgres necesita ajuste de checkpoints y no abusar de synchronous_commit para escrituras no críticas.

    Task 5: Verifica límites de archivos abiertos (a Postgres le importará más)

    cr0x@server:~$ ulimit -n
    1024
    

    Qué significa: 1024 es limitado para Postgres bajo carga con muchas conexiones y archivos. SQLite se preocupa menos, pero tu app podría.

    Decisión: Si eliges Postgres, aumenta los límites vía systemd o limits.conf; si no puedes, mantén conexiones bajas y usa un pooler.

    Task 6: Inspecciona el recuento de conexiones en vivo (si ya es una multitud, SQLite se pondrá picante)

    cr0x@server:~$ sudo ss -tanp | awk '$4 ~ /:5432$/ {c++} END {print c+0}'
    0
    

    Qué significa: No hay Postgres ahora, pero el patrón es lo que importa: ¿cuántos clientes DB concurrentes existirán?

    Decisión: Si esperas docenas/cientos de conexiones concurrentes, Postgres más un pooler gana. SQLite no tiene “conexiones” en el mismo sentido; tiene “hilos y procesos peleando por un archivo”.

    Task 7: Crea una base SQLite con WAL e inspecciona pragmas (hazla menos frágil)

    cr0x@server:~$ sqlite3 /var/lib/myapp/app.db 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA wal_autocheckpoint=1000;'
    wal
    

    Qué significa: Modo WAL activado; synchronous NORMAL es un compromiso común (suficientemente duradero para muchas apps, menos dolor IO que FULL).

    Decisión: Si eliges SQLite, debes ser explícito con los pragmas. Los ajustes por defecto no son “política de producción”, son “valores genéricos de biblioteca”.

    Task 8: Simula escrituras concurrentes en SQLite (detecta el muro de bloqueo temprano)

    cr0x@server:~$ for i in $(seq 1 20); do (sqlite3 /var/lib/myapp/app.db "BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS t(x); INSERT INTO t VALUES($i); COMMIT;" >/dev/null 2>&1 &); done; wait; echo done
    done
    

    Qué significa: Es una prueba gruesa. Si la repites con más contención y empiezas a ver “database is locked” en stderr, esa es tu sirena de advertencia.

    Decisión: Si tu carga real se parece a esto (muchos escritores), deja de romantizar SQLite y usa Postgres.

    Task 9: Instala Postgres y confirma la salud del servicio

    cr0x@server:~$ sudo apt-get update -qq
    ...output...
    
    cr0x@server:~$ sudo apt-get install -y postgresql
    ...output...
    
    cr0x@server:~$ sudo systemctl status postgresql --no-pager
    ● postgresql.service - PostgreSQL RDBMS
         Loaded: loaded (/lib/systemd/system/postgresql.service; enabled)
         Active: active (exited)
    

    Qué significa: En Debian/Ubuntu, el servicio wrapper puede mostrar “active (exited)” mientras las unidades del clúster se ejecutan. No te asustes; revisa el clúster.

    Decisión: Si no puedes mantener un servicio saludable en tu VPS (permisos, disco lleno, presión de memoria), SQLite podría ser la opción más sensata hasta estabilizar el host.

    Task 10: Comprueba la preparación del clúster Postgres

    cr0x@server:~$ pg_lsclusters
    Ver Cluster Port Status Owner    Data directory              Log file
    16  main    5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log
    

    Qué significa: Está online. Tienes un directorio de datos y una ruta de log—dos cosas que aprenderás a respetar.

    Decisión: Si Postgres arranca limpio y se mantiene online con tu app, es una señal fuerte de que puedes permitirte la operación.

    Task 11: Inspecciona ajustes de durabilidad y checkpoints de Postgres (no vayas a ciegas)

    cr0x@server:~$ sudo -u postgres psql -c "SHOW synchronous_commit; SHOW fsync; SHOW full_page_writes; SHOW checkpoint_timeout; SHOW max_wal_size;"
     synchronous_commit
    -------------------
     on
    (1 row)
    
     fsync
    -------
     on
    (1 row)
    
     full_page_writes
    ------------------
     on
    (1 row)
    
     checkpoint_timeout
    --------------------
     5min
    (1 row)
    
     max_wal_size
    --------------
     1GB
    (1 row)
    

    Qué significa: Los valores por defecto son conservadores. Apuntan a seguridad en hardware genérico, no necesariamente a tu VPS específico.

    Decisión: Si necesitas alto rendimiento de escritura, puedes ajustar checkpoints y tamaño WAL. Si necesitas máxima seguridad, mantén estos valores conservadores e invierte en backups y pruebas.

    Task 12: Detecta presión de vacuum (el “impuesto de mantenimiento” de Postgres)

    cr0x@server:~$ sudo -u postgres psql -c "SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
     relname | n_dead_tup
    ---------+------------
    (0 rows)
    

    Qué significa: Todavía no hay tablas de usuario. Más adelante, esto muestra si los tuples muertos se están acumulando. Los montones significan bloat, consultas más lentas y, eventualmente, miseria por paging.

    Decisión: Si eliges Postgres, debes monitorizar vacuum/bloat. Si no puedes comprometerte con eso, la simplicidad de SQLite empieza a verse atractiva—siempre que la concurrencia cuadre.

    Task 13: Identifica consultas lentas en Postgres (decide si necesitas índices u otra BD)

    cr0x@server:~$ sudo -u postgres psql -c "SHOW shared_preload_libraries;"
     shared_preload_libraries
    --------------------------
    (1 row)
    

    Qué significa: Si pg_stat_statements no está habilitado, te falta una lente clave para el comportamiento de consultas.

    Decisión: Si ejecutas algo más que tráfico de juguete, habilita estadísticas de consultas y trátalas como telemetría de producción. SQLite tiene opciones, pero Postgres hace este tipo de análisis rutinario.

    Task 14: Estima tamaño y crecimiento de la BD (archivo SQLite vs clúster Postgres)

    cr0x@server:~$ du -sh /var/lib/myapp/app.db
    48M	/var/lib/myapp/app.db
    
    cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size(current_database()));"
     pg_size_pretty
    ----------------
     7289 kB
    (1 row)
    

    Qué significa: SQLite es un archivo; Postgres es un árbol de directorios más WAL. Los patrones de crecimiento difieren: el archivo SQLite crece y puede no reducirse; Postgres puede inflarse a menos que se vacuumee.

    Decisión: Si necesitas gestión de tamaño predecible y retención, Postgres con vacuum y particionado adecuados (si se requiere) tiende a ser más fácil de controlar que un archivo monolítico.

    Task 15: Prueba el flujo de backup/restore (esto decide si duermes)

    cr0x@server:~$ sqlite3 /var/lib/myapp/app.db ".backup '/var/backups/app.db.bak'"
    
    cr0x@server:~$ ls -lh /var/backups/app.db.bak
    -rw-r--r-- 1 root root 48M Dec 30 03:12 /var/backups/app.db.bak
    
    cr0x@server:~$ sudo -u postgres pg_dump -Fc -f /var/backups/pg.dump postgres
    
    cr0x@server:~$ ls -lh /var/backups/pg.dump
    -rw-r--r-- 1 postgres postgres 36K Dec 30 03:13 /var/backups/pg.dump
    

    Qué significa: Ambos pueden ser respaldados. La clave es la consistencia y probar la restauración. SQLite necesita el método de backup correcto; Postgres necesita que practiques restaurar y permisos.

    Decisión: Si no puedes o no vas a probar restauraciones, no elijas ninguno—porque no estás eligiendo una base de datos, estás eligiendo un incidente futuro.

    Guía rápida de diagnóstico

    Esta es la secuencia de triage para “algo está lento”. El objetivo es aislar el cuello de botella en minutos, no debatir arquitectura en Slack durante horas.

    Primero: ¿es CPU, memoria o disco?

    cr0x@server:~$ uptime
     03:20:11 up 12 days,  2:41,  1 user,  load average: 0.22, 0.40, 0.35
    
    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
     1  0      0 184320  28000 635000    0    0    10    25  120  180  3  1 95  1  0
     0  0      0 183900  28000 635200    0    0     0     0  110  170  2  1 97  0  0
    

    Interpretación: Alto wa significa espera de IO de disco; alto si/so significa swap; alto r con poco idle significa presión de CPU.

    Acción: Si el host está intercambiando, arregla la memoria primero (reduce conexiones, ajusta Postgres, añade RAM). Si la espera IO es alta, mira checkpointing, costes de fsync y patrones de escritura.

    Segundo: ¿la base de datos está bloqueada o en espera?

    SQLite: busca errores de bloqueo en logs de la app; comprueba si haces transacciones largas.

    Postgres: comprueba bloqueos que estén bloqueando.

    cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, wait_event_type, wait_event, state, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY pid;"
     pid  | wait_event_type | wait_event | state  | query
    ------+-----------------+------------+--------+-------
    (0 rows)
    

    Interpretación: Si ves sesiones esperando por bloqueos, no estás “lento”, estás serializado. Arreglo distinto: acorta transacciones, añade índices para reducir duración de bloqueos, evita DDL de larga ejecución en horas pico.

    Tercero: ¿es un problema de consulta o de capacidad?

    Para Postgres, identifica consultas lentas y haz EXPLAIN. Para SQLite, examina patrones de acceso e índices y considera mover consultas pesadas fuera del camino caliente.

    cr0x@server:~$ sudo -u postgres psql -c "EXPLAIN (ANALYZE, BUFFERS) SELECT 1;"
                                          QUERY PLAN
    --------------------------------------------------------------------------------------
     Result  (cost=0.00..0.01 rows=1 width=4) (actual time=0.003..0.004 rows=1 loops=1)
     Planning Time: 0.020 ms
     Execution Time: 0.010 ms
    (3 rows)
    

    Interpretación: En uso real, busca scans secuenciales en tablas grandes, grandes hits de buffers o tiempo gastado esperando IO.

    Acción: Si las consultas son lentas por falta de índices, arregla el esquema. Si son lentas porque el disco es lento, mejora el almacenamiento o reduce churn de escritura. Si son lentas por concurrencia, arregla el pooling de conexiones o elige la BD correcta.

    Errores comunes (síntomas → causa raíz → solución)

    Estos no son fracasos morales. Son resultados predecibles de tratar la base de datos como una caja negra.

    1) “database is locked” aparece esporádicamente (SQLite)

    Síntomas: Errores de la app bajo carga, picos durante jobs en segundo plano, peticiones fallando y luego teniendo éxito al reintentar.

    Causa raíz: Múltiples escritores o transacciones largas sosteniendo bloqueos de escritura. WAL ayuda, pero un único escritor aún necesita tiempo.

    Solución: Habilita WAL; mantén transacciones cortas; serializa escrituras vía una cola de trabajos; añade busy_timeout; o migra a Postgres si necesitas escrituras concurrentes.

    2) SQLite se siente rápido hasta que despliegas múltiples instancias de la app

    Síntomas: Funciona en dev, inestable en prod; el rendimiento cae solo después de escalar horizontalmente.

    Causa raíz: El bloqueo de archivos entre procesos se vuelve contención. Además: los sistemas de archivos compartidos son una trampa.

    Solución: No compartas SQLite sobre NFS. Si necesitas más de un proceso escritor, usa Postgres.

    3) Postgres está “lento” pero la CPU está inactiva

    Síntomas: Alta latencia, CPU baja, paradas periódicas.

    Causa raíz: Espera de IO durante checkpoints o carga de escritura pesada con muchos fsync; max_wal_size muy pequeño; almacenamiento pobre.

    Solución: Aumenta max_wal_size; ajusta checkpoints; mueve WAL a disco más rápido si es posible; reduce escrituras síncronas para rutas no críticas (con cuidado).

    4) Postgres se cae con muchas conexiones en un VPS pequeño

    Síntomas: Picos de memoria, kills por OOM, “too many clients”, timeouts aleatorios.

    Causa raíz: Patrón de una conexión por petición; overhead por conexión; sin pooling.

    Solución: Usa PgBouncer; reduce max_connections; usa un tamaño de pool sensato; cambia la app para reutilizar conexiones.

    5) Existen backups pero las restauraciones fallan

    Síntomas: Prueba de restore falla; permisos rotos; roles faltantes; archivo de backup de SQLite corrupto o inconsistente.

    Causa raíz: Backups tomados incorrectamente (copia de archivo SQLite durante escritura) o no probados (pg_dump sin globals/roles).

    Solución: Para SQLite, usa .backup o la API de backup; para Postgres, realiza simulacros de restauración incluyendo roles y esquema; automatiza la verificación.

    6) Tablas de Postgres se inflan y las consultas degradan con semanas

    Síntomas: Uso de disco crece más rápido que los datos; índices hinchados; consultas lentas; vacuum ejecutándose constantemente.

    Causa raíz: Tuplas muertas de MVCC se acumulan; autovacuum no da abasto; patrones agresivos de UPDATE/DELETE.

    Solución: Ajusta autovacuum por tabla; evita hot updates donde sea posible; considera particionado o mantenimiento periódico.

    7) El archivo SQLite se infla y nunca se reduce

    Síntomas: Uso de disco crece incluso tras deletes; VPS se queda sin disco.

    Causa raíz: SQLite reutiliza páginas pero no siempre devuelve espacio al sistema de archivos; fragmentación; deletes grandes.

    Solución: VACUUM periódico (costoso); diseña una estrategia de retención; considera dividir tablas grandes o migrar a Postgres si el churn es alto.

    8) “Usamos Postgres porque es enterprise” y ahora operaciones se ahogan

    Síntomas: Nadie se ocupa de upgrades, vacuum, backups; la BD es una mascota, no ganado.

    Causa raíz: Elegir Postgres sin asignar madurez operativa.

    Solución: Invierte en lo básico de operaciones (monitorización, drills de backup, cadencia de upgrades) o mantén la simplicidad con SQLite hasta que realmente necesites la DB servidor.

    Tres microhistorias corporativas

    Microhistoria 1: El incidente causado por una suposición errónea (archivo SQLite en “almacenamiento compartido”)

    La compañía era mediana, el producto sano, y a alguien se le ocurrió una idea brillante: ejecutar dos instancias de la app tras un balanceador “para resiliencia”. La base de datos era SQLite, situada en lo que el proveedor de VPS anunciaba como “almacenamiento compartido”, montado en ambas instancias. Parecía elegante. Un archivo. Dos instancias. ¿Qué podría salir mal?

    Funcionó unos días. Luego llegó el primer pico de tráfico—nada dramático, solo un correo de marketing. Las peticiones empezaron a acumularse. La latencia se disparó. Algunos usuarios obtuvieron errores; otros, lecturas obsoletas; unos cuantos vieron actualizaciones parciales que desaparecían al refrescar.

    El on-call revisó logs y encontró “database is locked” intermitente, pero no constante. Peor aún, aparecieron mensajes ocasionales tipo “disk I/O error” que parecían hardware. No lo eran. Eran el sistema de archivos y el gestor de bloqueos teniendo un desacuerdo sobre quién poseía la verdad entre dos nodos.

    La suposición errónea fue sutil: “Si el almacenamiento es compartido, el bloqueo de archivo es compartido”. En muchos sistemas de archivos compartidos, los locks de asesoramiento no se comportan como los locks locales ext4, especialmente bajo fallos o latencia. SQLite no estaba “roto”; el entorno violó las asunciones que hace para ofrecer semánticas ACID.

    La solución fue aburrida: migrar a Postgres en un nodo primero, luego añadir una réplica más tarde. También eliminaron el montaje compartido y trataron los límites de almacenamiento como límites de fallo. El informe del incidente no culpó a SQLite; culpó a la arquitectura que fingió que un archivo podía ser un sistema distribuido.

    Microhistoria 2: La optimización que salió mal (Postgres afinado para velocidad, pagado en ansiedad por pérdida de datos)

    Otra organización tenía Postgres en un VPS pequeño. Las escrituras eran intensas: eventos, logs, contadores. El equipo quería menor latencia y vio un post que sugería apagar perillas de durabilidad. Cambiaron ajustes para reducir la presión de fsync y hacer que los commits retornaran más rápido. Todos aplaudieron. Los gráficos bajaron y todo parecía perfecto.

    Dos semanas después el host del VPS tuvo un reinicio no planificado. Nada dramático—uno de esos “mantenimientos de nodo” que solo conoces después. Postgres reinició bien, pero un trozo de las escrituras más recientes faltaba. No catastrófico, pero suficiente para generar preguntas de clientes y alarmas internas.

    Entonces llegó el impuesto real: la incertidumbre. No podían decir con confianza qué se había perdido, y el equipo de producto empezó a tratar la base de datos como “posiblemente consistente”. Eso es corrosivo. Convierte cada bug en un debate sobre si los datos son reales.

    La optimización salió mal porque optimizó lo incorrecto: latencia en estado estable a costa de durabilidad predecible. Hay razones válidas para relajar durabilidad para analítica efímera o caches. Pero ellos la usaban para estado orientado al cliente.

    La solución fue restaurar ajustes seguros para tablas core, aislar datos de alto volumen y bajo valor en rutas separadas, y ejecutar backups con pruebas de restauración. También introdujeron batching para reducir la frecuencia de commits en lugar de apostar por el comportamiento ante fallos.

    Microhistoria 3: La práctica aburrida pero correcta que salvó el día (drills de backup y automatización de restauración)

    Esta es menos dramática, y ese es el punto. Un equipo que ejecutaba un SaaS en un VPS usaba Postgres. No eran sofisticados. No tenían equipo de plataforma. Pero hacían una cosa sin descanso: drills de restauración semanales a una VM de pruebas, con una lista de verificación.

    Tenían un script que bajaba el backup más reciente, lo restauraba, ejecutaba un pequeño conjunto de consultas de sanity y confirmaba que la app podía arrancar contra él. También mantenían un “runbook” mínimo que describía cómo promover la BD restaurada si la primaria moría. A nadie le encantaba hacerlo. Era como pasar hilo dental.

    Luego un desarrollador ejecutó por error una migración destructiva contra producción. No malintencionado. Solo una variable de entorno mal puesta y una herramienta de migración obediente. El on-call silenció alertas, juró en voz baja y empezó el drill de restauración que habían practicado.

    Todavía tuvieron una mala hora, pero no una mala semana. Restauraron, reenviaron migraciones correctamente y reprodujeron una ventana corta de eventos de negocio desde logs. El CEO nunca tuvo que aprender qué significa “WAL”, lo cual es el mayor cumplido que pueden recibir las operaciones.

    Cita (idea parafraseada): “No te elevas a la ocasión; vuelves a tu preparación.” — idea parafraseada común en círculos de fiabilidad/ops

    Listas de verificación / plan paso a paso

    Lista A: Si te inclinas por SQLite (dále forma de producción)

    1. Confirma la realidad de un solo escritor: lista todas las rutas de código que escriben (peticiones web, workers, cron, scripts admin). Si hay más de un actor a la vez, planea serializar o migrar.
    2. Usa modo WAL: establece PRAGMA journal_mode=WAL.
    3. Establece synchronous sensato: generalmente NORMAL es un buen compromiso para VPS; usa FULL si no toleras pérdida de escrituras recientes tras un crash.
    4. Configura busy_timeout: haz que la app espere brevemente en lugar de fallar instantáneamente por contención de locks.
    5. Respaldos correctos: usa el mecanismo de backup de SQLite, no “cp del archivo en horas pico”.
    6. Planifica crecimiento de archivo: monitoriza tamaño de BD y disco libre; programa VACUUM periódico solo si es necesario.
    7. No pongas SQLite en NFS/montajes compartidos: disco local únicamente, salvo que disfrutes depurar bloqueos de archivos con latencia.

    Lista B: Si te inclinas por PostgreSQL (hazlo aburrido, estable y barato)

    1. Tamaño de conexiones adecuado: mantén max_connections sensato; usa un pooler para apps web.
    2. Configura memoria deliberadamente: ajusta shared_buffers de forma conservadora en RAM pequeña; deja margen para caché del SO y tu app.
    3. Habilita visibilidad de consultas: activa estadísticas de consultas para ver qué es lento antes de que los usuarios lo digan.
    4. Monitoriza vacuum: observa tuplas muertas y actividad de autovacuum; el bloat es una fuga lenta.
    5. Backups y pruebas de restore: automatiza ambos. Un backup sin prueba de restauración es un deseo.
    6. Planificación de actualizaciones: decide cómo manejarás updates menores y upgrades de versión antes de que te obliguen.
    7. Gestión de disco: monitoriza uso de disco para datos y WAL; evita operar al 90% en un VPS.

    Paso a paso: la ruta de decisión sin remordimientos (15 minutos)

    1. Ejecuta Task 1–4 para entender la realidad de RAM e IO.
    2. Lista tus escritores. Si hay más de un escritor concurrente ahora o pronto, elige Postgres.
    3. Si SQLite sigue siendo plausible, ejecuta Task 7–8. Si aparece contención de locks en una prueba de juguete, elige Postgres.
    4. Si eliges Postgres, ejecuta Task 9–12 y confirma que puedes mantenerlo saludable en este VPS.
    5. Ejecuta Task 15 y realiza al menos un drill de restauración. Elige el sistema cuya ruta de restauración puedas ejecutar bajo estrés.

    Broma #2: La base de datos más rápida es la que no perdiste a las 03:00, que es también por qué los backups tienen el mejor ROI de cualquier característica que nunca vas a demostrar.

    Preguntas frecuentes

    1) ¿Puede SQLite manejar tráfico de producción?

    Sí, si “tráfico de producción” significa mayormente lecturas, un número pequeño de escrituras y un modelo de concurrencia controlado. Se usa en muchos sistemas reales. Simplemente no quiere ser tu coordinador de escrituras multi-tenant.

    2) ¿El modo WAL hace a SQLite “tan bueno como Postgres”?

    No. WAL reduce bloqueo lector/escritor y mejora la concurrencia, pero aún tienes un único archivo de base de datos con semánticas de bloqueo y menos herramientas de concurrencia. Postgres está diseñado como servicio compartido.

    3) ¿Es Postgres excesivo para un VPS pequeño?

    A veces. Si tu VPS es diminuto y tu carga es simple, Postgres puede ser piezas móviles de más. Pero si tienes múltiples escritores o trayectoria de crecimiento, lo “excesivo” rápidamente se convierte en “gracias por evitarme migrar bajo presión”.

    4) ¿Cuál es el mayor coste oculto de Postgres en un VPS?

    Gestión de conexiones y memoria. Sin pooling y límites sensatos, Postgres puede quemar RAM con sesiones inactivas y morir de una forma que parece “inestabilidad aleatoria”. No es aleatorio; es matemáticas.

    5) ¿Cuál es el mayor coste oculto de SQLite en un VPS?

    Contención por locks y supuestos operativos. En el momento que tienes múltiples escritores, transacciones largas, o pones el archivo en almacenamiento cuestionable, heredas modos de fallo que se sienten misteriosos hasta que aceptas el modelo de bloqueo.

    6) Si empiezo con SQLite, ¿qué tan dolorosa es la migración a Postgres?

    Va desde “un fin de semana” hasta “un trimestre”, dependiendo de la complejidad del esquema, volumen de datos y cuánto tu app dependa de rarezas de SQLite. Si prevés crecimiento, diseña tu app con una abstracción de BD y herramientas de migración desde el día uno.

    7) ¿Debería usar SQLite para cache y Postgres como fuente de la verdad?

    Puedes, pero no construyas un sistema distribuido accidentalmente. Si necesitas cache, considera caches en memoria o estrategias nativas de Postgres. Si usas SQLite como cache local, trátalo como descartable y reconstruible.

    8) ¿Y la durabilidad: es SQLite inseguro?

    SQLite puede ser duradero cuando se configura correctamente y se usa en un sistema de archivos que respete sus expectativas. El riesgo no es “SQLite es inseguro”, es “SQLite facilita que seas inseguro sin notarlo”. Postgres centraliza esos comportamientos de durabilidad en un servidor diseñado para caídas.

    9) ¿Necesito replicación en un VPS?

    No siempre. Para muchos despliegues en VPS, la primera victoria es backups fiables y drills de restauración. La replicación vale la pena cuando tus requisitos de uptime exceden “restaurar en X minutos” y puedes permitirte la complejidad.

    10) ¿Cómo decido si mi app tiene “múltiples escritores”?

    Si las escrituras pueden ocurrir concurrentemente desde más de un proceso OS o contenedor (workers web, job workers, tareas programadas, scripts admin), tienes múltiples escritores. Si despliegas múltiples instancias de la app, definitivamente los tienes.

    Siguientes pasos que puedes hacer hoy

    Elige un camino y hazlo operable en la realidad. Las bases de datos no fallan porque elegiste la marca equivocada; fallan porque no emparejaste el sistema con la carga y no practicaste la recuperación.

    Si eliges SQLite

    • Habilita WAL y establece synchronous explícitamente.
    • Añade busy timeout y mantén transacciones cortas.
    • Implementa backups usando el mecanismo de backup de SQLite y realiza una prueba de restauración.
    • Escribe una regla clara: “no filesystem compartido, no caos multi-writer”.

    Si eliges PostgreSQL

    • Configura pooling de conexiones y límites sensatos de inmediato.
    • Activa visibilidad de consultas y vigila consultas lentas y bloqueos.
    • Automatiza backups y realiza drills de restauración según un calendario.
    • Monitoriza uso de disco y salud de vacuum antes de que sea necesario.

    La edición sin remordimientos no trata de elegir la “mejor” base de datos. Se trata de elegir la base de datos cuyos modos de fallo puedas predecir, observar y recuperar en un VPS durante horas humanas.

    Pentium 4 / NetBurst: el error más sonoro de la era GHz

    Si administraste sistemas en producción a principios de los años 2000, probablemente recuerdas la sensación: compraste “más GHz”,
    tus gráficas no mejoraron y sí lo hizo tu pager. La latencia siguió grosera, el rendimiento por segundo se mantuvo plano y los ventiladores aprendieron
    nuevas formas de chillar.

    NetBurst (la microarquitectura del Pentium 4) es un caso de estudio sobre lo que ocurre cuando el marketing y la microarquitectura
    se estrechan demasiado la mano. No es que los ingenieros no supieran lo que hacían. Es que las restricciones eran brutales,
    la apuesta era estrecha y el mundo real no quiso cooperar.

    La tesis: GHz era un proxy, no un producto

    NetBurst se diseñó para frecuencia. No “frecuencia eficiente” ni “buena frecuencia”, sino “pon el número en la caja
    y que el mundo lo discuta después”. Intel había pasado años entrenando a los clientes a interpretar la velocidad de reloj
    como rendimiento. El mercado recompensó esa simplificación. Luego llegaron las facturas: pipelines de instrucciones tan largos
    que las predicciones fallidas eran costosas, un subsistema de memoria que no podía seguir el ritmo y una densidad de potencia que convirtió
    el diseño de rack en un pasatiempo para los nerds de HVAC.

    Esto no fue una sola mala decisión de diseño. Fue un conjunto apilado de compensaciones que todas asumían una cosa:
    los relojes seguirían subiendo y el software jugaría junto. Cuando cualquiera de esas suposiciones falló—código con muchas ramas, cargas pesadas en memoria,
    restricciones realistas del datacenter—todo el enfoque se desplomó.

    Si quieres la traducción SRE: NetBurst se optimizó para picos bajo microbenchmarks ideales y castigó la latencia de cola
    bajo carga mixta de producción. Se puede enviar mucha decepción así.

    Exactamente una vez vi una presentación de compras tratar “3.0 GHz” como si fuera un SLA de throughput.
    Eso es como estimar el rendimiento de red contando las letras en “Ethernet”.

    Internos de NetBurst: la tubería que se comió tu IPC

    Canalizaciones profundas: geniales para frecuencia, terribles para errores

    La historia clásica de NetBurst es “pipeline muy profundo”. La historia práctica es “alto coste por predicción fallida”.
    Un pipeline más profundo te ayuda a alcanzar frecuencias más altas porque cada etapa hace menos trabajo. La desventaja es que ahora
    has alargado la distancia entre “creímos la rama” y “descubrimos que estábamos equivocados”. Cuando estás equivocado, limpias mucho trabajo en vuelo y empiezas de nuevo.

    Las CPUs modernas siguen teniendo pipelines profundos, pero lo compensan con predictores mejores, cachés más grandes, ejecución más ancha
    y gestión de potencia cuidadosa. NetBurst se metió profundo temprano, con predictores y sistemas de memoria que no cubrían completamente
    la apuesta en rutas de código típicas de servidor.

    Trace cache: ingenioso, complejo y sensible a la carga

    La trace cache de NetBurst almacenaba micro-ops (uops) decodificados, no instrucciones x86 crudas. Era inteligente: decodificar x86
    no es trivial, y una uop cache puede reducir el coste del front-end. Pero también hizo el rendimiento más dependiente de cómo fluía y se alineaba el código.
    Si tu flujo de instrucciones no encajaba bien—muchas ramas, distribución extraña, mala localidad—la trace cache dejó de ser un regalo y se convirtió en otro lugar donde fallar.

    La idea no estaba mal; era temprana y frágil. Las uop caches de hoy funcionan porque el resto del sistema mejoró al alimentarlas, y porque
    los trade-offs de potencia/rendimiento se gestionan con más delicadeza.

    FSB y northbridge compartido: la caseta de peaje del ancho de banda

    Los sistemas Pentium 4 dependían de un front-side bus (FSB) hacia un controlador de memoria separado (northbridge). Eso significa que tu
    núcleo CPU es rápido, tu memoria está “en otro lugar” y cada petición es un viaje a través de un bus compartido. Bajo carga,
    ese bus se convierte en un problema de planificación. Añade múltiples CPUs y se vuelve un proyecto en grupo.

    Compáralo con diseños posteriores con controladores de memoria integrados (AMD lo hizo antes en x86; Intel después). Cuando
    acercas la memoria y le das caminos más dedicados, reduces la contención y lowers la latencia. En producción, la latencia es moneda.
    NetBurst la gastó como turista.

    Era SSE2/SSE3: fuerte en cálculo en streaming, desigual en otros escenarios

    NetBurst se comportaba bien en algunos workloads vectorizados y de streaming—código que podía procesar arrays de forma predecible y
    evitar lógica con muchas ramas. Por eso los benchmarks podían verse bien si estaban diseñados para alimentar la máquina con el tipo
    de trabajo que le gustaba. Pero los servicios reales no son tan educados. Analizan, ramifican, asignan, bloquean y esperan I/O.

    NetBurst era el equivalente CPU de un motor ajustado para una pista específica. Mételo en tráfico urbano y aprenderás qué significa “curva de par”.

    Por qué las cargas reales duelen: cachés, ramas, memoria y espera

    IPC es lo que sientes; GHz es lo que presumes

    Instrucciones por ciclo (IPC) es un proxy tosco pero útil para “cuánto trabajo se hace por tic”. NetBurst a menudo
    tenía menor IPC que sus contemporáneos en muchas cargas de propósito general. Así que el chip corría a mayor frecuencia para
    compensar. Eso puede funcionar—hasta que no, porque:

    • El código con muchas ramas dispara predicciones fallidas, que son más costosas en pipelines profundos.
    • Los fallos de caché paran la ejecución, y un núcleo rápido simplemente alcanza la parada antes.
    • La latencia FSB/memoria se convierte en un muro que no puedes atravesar con reloj.
    • La potencia/térmicos fuerzan throttling, así que los GHz prometidos son aspiracionales.

    Predicción de ramas fallida: el impuesto de latencia que sigues pagando

    Las cargas de servidor están llenas de ramas impredecibles: enrutamiento de peticiones, parsing, comprobaciones de autorización, búsquedas en hash tables,
    llamadas virtuales, decisiones de compresión, rutas de ejecución de bases de datos. Cuando los predictores fallan, los pipelines profundos pierden
    trabajo y tiempo. La CPU no “se ralentiza”. Simplemente hace menos trabajo útil mientras está muy ocupada.

    Muro de memoria: cuando el núcleo va más rápido que el sistema

    NetBurst podía ejecutar rápido cuando se le alimentaba, pero muchas cargas están limitadas por la memoria. Un fallo de caché son cientos
    de ciclos de espera. Ese número no es un fallo moral; es física más topología. El efecto práctico es que una CPU con más GHz puede verse peor
    si alcanza stalls de memoria más frecuentemente o no puede ocultarlos eficazmente.

    Desde la perspectiva del operador, esto se manifiesta como: alta utilización de CPU, throughput mediocre y un sistema que
    se siente “atascado” sin saturación obvia de I/O. No está atascado. Está esperando memoria y peleándose consigo mismo.

    Ejecución especulativa: útil, pero amplifica el coste de las apuestas equivocadas

    La especulación es cómo las CPUs modernas obtienen rendimiento: adivina una ruta, ejecútala y descártala si está equivocada. En un pipeline profundo,
    la ruta equivocada es cara. La apuesta de NetBurst fue que mejores relojes pagarían por eso. A veces lo hicieron. A menudo, no.

    Una de las lecciones operacionales más simples de la era NetBurst: no trates “CPU al 95%” como “CPU hace 95% de trabajo útil”.
    Necesitas contadores, no sensaciones.

    Térmicos y energía: cuando la CPU negocia con la física

    La densidad de potencia se convirtió en una característica del producto (por accidente)

    NetBurst se calentaba. Especialmente los Pentium 4 basados en Prescott, que se hicieron notorios por consumo y calor. El calor no es solo
    una factura de electricidad; es riesgo de fiabilidad, ruido de ventiladores y variabilidad de rendimiento.

    En producción, los mapas térmicos se convierten en mapas de incidentes. Si un diseño exige enfriamiento intenso, tu margen desaparece:
    filtros polvorientos, un ventilador fallido, una rejilla bloqueada, un pasillo cálido que sube o un rack pegado a la pared se convierten en eventos de rendimiento.
    Y los eventos de rendimiento se convierten en eventos de disponibilidad.

    Térmico throttling: el freno invisible

    Cuando una CPU reduce frecuencia por calor, el reloj cambia, la ejecución cambia y la latencia de cola de tu servicio se desplaza de maneras que tus pruebas de carga nunca modelaron.
    Con sistemas de la era NetBurst, no era raro ver “el benchmark dice X” pero “prod hace Y” porque las condiciones ambientales no estaban controladas como en un laboratorio.

    Broma #1: Prescott no era un reemplazo de calentador, pero hacía las guardias de invierno un poco más soportables si te sentabas cerca del rack.

    Fiabilidad y operaciones: los sistemas calientes envejecen más rápido

    Capacitores, VRMs, ventiladores y placas base no aman el calor. Incluso cuando sobreviven, se desvían. Ese desvío se convierte en errores intermitentes,
    reinicios espontáneos y folklore de “funciona después de volver a conectar”. Eso no es misticismo; es expansión térmica, entrega de potencia marginal
    y componentes fuera de su zona de confort.

    Una idea parafraseada a menudo atribuida a W. Edwards Deming aplica claramente a ops: “No puedes gestionar lo que no mides.”
    Con NetBurst tenías que medir térmicos, porque la CPU seguro que lo hacía.

    Hyper-Threading: el truco bueno que expuso las malas suposiciones

    Hyper-Threading (SMT) llegó a algunos modelos de Pentium 4 y fue legítimamente útil en las condiciones adecuadas:
    podía llenar los huecos del pipeline ejecutando otro hilo cuando uno estaba estancado. Suena como rendimiento gratis,
    y a veces lo era.

    Cuando ayudaba

    • Cargas mixtas donde un hilo espera fallos de caché y el otro puede usar las unidades de ejecución.
    • Servicios con mucho I/O donde un hilo se bloquea frecuentemente y la sobrecarga del scheduler es manejable.
    • Algunos roles de servidor orientados al throughput con peticiones independientes y contención limitada de locks.

    Cuando perjudicaba

    • Cargas limitadas por ancho de banda de memoria: dos hilos solo compiten más por el mismo cuello de botella.
    • Cargas con muchos locks: mayor contención, más rebote de líneas de caché, peor latencia de cola.
    • Servicios sensibles a latencia: jitter por recursos compartidos y artefactos de scheduling.

    Hyper-Threading en NetBurst es un buen microcosmos de una regla general: SMT mejora diseños buenos y empeora diseños frágiles.
    Puede aumentar el throughput mientras hace la latencia más fea. Si tu SLO es p99, no “actívalo y reza”. Haz pruebas A/B con concurrencia parecida a producción y revisa la cola.

    Hechos históricos que importan (y algunos que todavía duelen)

    1. NetBurst debutó con Willamette (Pentium 4, 2000), priorizando velocidad de reloj sobre IPC.
    2. Northwood mejoró la eficiencia y las frecuencias, y se convirtió en el Pentium 4 “menos doloroso” para muchos compradores.
    3. Prescott (2004) pasó a un proceso más pequeño, añadió características y se hizo famoso por calor y consumo.
    4. La “carrera de GHz” moldeó las decisiones de compra tanto que “reloj más alto” a menudo ganó a mejor arquitectura en conversaciones de ventas.
    5. Acceso a memoria basado en FSB implicaba que la CPU competía por ancho de banda sobre un bus compartido hacia el northbridge.
    6. Trace cache almacenaba micro-ops decodificados, buscando reducir la sobrecarga de decodificación y alimentar eficientemente la larga pipeline.
    7. Hyper-Threading llegó en modelos selectos y podía mejorar throughput usando recursos de ejecución ociosos.
    8. Pentium M (derivado de la línea P6) a menudo superaba a Pentium 4 con relojes mucho más bajos, especialmente en tareas del mundo real.
    9. Intel finalmente pivotó lejos de NetBurst; Core (de otra línea) reemplazó la estrategia en lugar de iterarla indefinidamente.

    Tres mini-historias corporativas desde las trincheras

    Mini-historia 1: el incidente causado por una suposición equivocada (“GHz = capacidad”)

    Una empresa mediana heredó una flota de servidores web envejecidos y planeó una renovación rápida. Los criterios de selección eran
    dolorosamente simples: elegir las cajas Pentium 4 de mayor reloj dentro del presupuesto. La nota de compras literalmente equiparaba
    “+20% de reloj” con “+20% de peticiones por segundo”. Nadie actuaba con malicia; estaban ocupados.

    El despliegue fue fluido hasta que el tráfico alcanzó su pico normal. La utilización de CPU parecía bien—alta pero estable.
    La red estaba bajo control. Los discos no gritaban. Sin embargo la latencia p95 subió y luego la p99 se disparó. El equipo on-call
    hizo lo que hacen los equipos: reiniciaron servicios, movieron tráfico, culparon al balanceador y miraron gráficas hasta que las gráficas los miraron de vuelta.

    El problema real fue el comportamiento de memoria. La carga había cambiado con los años: más personalización, más lógica de plantillas, más enrutamiento dinámico.
    Eso significó más punteros encadenados y ramas. Los nuevos servidores tenían relojes más altos pero latencia de memoria similar y una topología FSB compartida
    que empeoraba bajo concurrencia. Eran más rápidos llegando a los mismos stalls de memoria, y Hyper-Threading añadió contención en el peor momento.

    La solución no fue “afinar Linux más”. La solución fue re-baselinear la capacidad usando una prueba parecida a producción:
    concurrencia realista, fases de caché caliente y fría y latencia de cola como métrica de primera clase. La empresa terminó cambiando la mezcla de la flota:
    menos cajas “reloj rápido”, más nodos balanceados con mejores subsistemas de memoria. También dejaron de usar GHz como número primario de capacidad.
    Los milagros ocurren cuando dejas de mentirte a ti mismo.

    Mini-historia 2: la optimización que salió mal (“usar HT para ganar rendimiento gratis”)

    Otra compañía ejecutaba un servicio Java con muchas peticiones de corta duración. Activaron Hyper-Threading en toda la flota
    y doblaron los hilos de trabajo, esperando ganancias lineales de throughput. Las pruebas sintéticas tempranas se veían geniales. Entonces llegaron
    los informes de incidentes: picos de latencia esporádicos, pausas de GC alineándose con ráfagas de tráfico y un nuevo tipo de “está lento pero nada está al máximo”.

    El sistema no estaba hambriento de CPU; estaba hambriento de caché y de locks. Dos CPUs lógicas compartían recursos de ejecución
    y, más importante, caché compartida y rutas de ancho de banda de memoria. Los patrones de asignación y sincronización de la JVM
    crearon rebotes de líneas de caché, y la mayor concurrencia amplificó la contención en hotspots que antes parecían inofensivos.

    Intentaron arreglarlo aumentando el tamaño del heap, luego fijando hilos a CPUs, luego tocando perillas que se sentían “de sistemas”.
    Algunas cosas ayudaron, la mayoría no. La verdadera mejora vino al dar un paso atrás: tratar Hyper-Threading como una herramienta de throughput con coste de latencia.
    Mide el coste.

    Revirtieron a menos hilos de trabajo, habilitaron HT solo en nodos que servían tráfico batch no interactivo y usaron perfiles de aplicación para eliminar un par de cuellos de botella de locks.
    El throughput quedó ligeramente por encima de antes de la “optimización” y la latencia de cola volvió a ser aburrida. La lección no fue “HT es malo”.
    La lección fue “HT es un multiplicador, y también multiplica tus errores”.

    Mini-historia 3: la práctica aburrida pero correcta que salvó el día (“margen térmico es capacidad”)

    Un equipo de servicios financieros ejecutaba trabajos nocturnos intensivos en cómputo en un clúster que incluía nodos Pentium 4 de la era Prescott.
    A nadie le gustaban esas cajas, pero los trabajos eran estables y el clúster era “suficiente”. El superpoder silencioso del equipo era que trataban el entorno
    como parte de la capacidad: monitorización de temperatura de entrada, chequeos de salud de ventiladores y alertas sobre indicadores de throttling térmico.

    Un verano, una unidad de enfriamiento se degradó durante el fin de semana. No fue una caída total—solo rendimiento degradado. El lunes por la mañana
    las duraciones de los trabajos subieron lentamente. La mayoría de equipos habría culpado al scheduler o a la base de datos. Este equipo notó una correlación sutil:
    nodos en una fila mostraban lecturas térmicas ligeramente más altas y frecuencia efectiva de CPU ligeramente menor.

    Vaciaron esos nodos, desplazaron trabajos a racks más fríos y abrieron un ticket con facilities con evidencia concreta.
    También redujeron temporalmente la concurrencia por nodo para bajar la salida de calor y estabilizar los tiempos de ejecución. Sin drama, sin heroísmos,
    sin sala de guerra a medianoche.

    El resultado: los trabajos terminaron a tiempo, sin incidentes visibles al cliente, y el problema de enfriamiento se solucionó antes de que se convirtiera
    en una fiesta de fallos de hardware. La práctica era aburrida—mide térmicos, vigila el throttling, mantiene margen—pero convirtió una “ralentización misteriosa”
    en un cambio controlado. Lo aburrido está subestimado.

    Tareas prácticas: 12+ comandos para diagnosticar “CPU rápida, sistema lento”

    Estos son ejecutables en un servidor Linux típico. No tratas de “probar que NetBurst es malo” en 2026.
    Aprendes a reconocer los mismos modos de fallo: stalls de pipeline, muro de memoria, artefactos de scheduling,
    throttling térmico y utilización engañosa.

    Tarea 1: Identificar la CPU y si HT está presente

    cr0x@server:~$ lscpu
    Architecture:            x86_64
    CPU op-mode(s):          32-bit, 64-bit
    CPU(s):                  2
    Thread(s) per core:      2
    Core(s) per socket:      1
    Socket(s):               1
    Model name:              Intel(R) Pentium(R) 4 CPU 3.00GHz
    Flags:                   fpu vme de pse tsc ... ht ... sse2

    Qué significa: “Thread(s) per core: 2” indica Hyper-Threading. El nombre del modelo te da la familia.

    Decisión: Si HT está presente, haz benchmarks con HT activado/desactivado para servicios sensibles a latencia; no asumas que es una ganancia.

    Tarea 2: Comprobar la frecuencia actual y el driver de escalado

    cr0x@server:~$ grep -E 'model name|cpu MHz' /proc/cpuinfo | head
    model name	: Intel(R) Pentium(R) 4 CPU 3.00GHz
    cpu MHz		: 2793.000

    Qué significa: La CPU no está a la frecuencia nominal. Podría ser ahorro de energía o throttling.

    Decisión: Si la frecuencia está inesperadamente baja bajo carga, investiga governors y throttling térmico a continuación.

    Tarea 3: Confirmar el governor de frecuencia de CPU

    cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
    ondemand

    Qué significa: “ondemand” puede reducir la frecuencia hasta que la carga aumente; en plataformas antiguas puede responder lentamente.

    Decisión: Para servicios de baja latencia, considera “performance” y vuelve a probar; para batch, “ondemand” puede estar bien.

    Tarea 4: Buscar zonas térmicas y temperaturas

    cr0x@server:~$ for z in /sys/class/thermal/thermal_zone*/temp; do echo "$z: $(cat $z)"; done
    /sys/class/thermal/thermal_zone0/temp: 78000
    /sys/class/thermal/thermal_zone1/temp: 65000

    Qué significa: Las temperaturas están en miligrados Celsius. 78000 = 78°C.

    Decisión: Si las temperaturas se acercan a umbrales de throttling durante picos, trata el enfriamiento como un limitador de capacidad, no como “trivia de facilities”.

    Tarea 5: Detectar indicadores de throttling en logs del kernel

    cr0x@server:~$ dmesg | grep -i -E 'throttl|thermal|critical|overheat' | tail
    CPU0: Thermal monitoring enabled (TM1)
    CPU0: Temperature above threshold, cpu clock throttled
    CPU0: Temperature/speed normal

    Qué significa: La CPU redujo velocidad por calor. Tu “misterio” de throughput puede ser física simple.

    Decisión: Arregla flujo de aire/enfriamiento, reduce carga o reduce concurrencia. No ajustes software alrededor de un fallo térmico.

    Tarea 6: Comprobar la cola de ejecución y saturación de CPU 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 120000  15000 210000    0    0     2     5  900 1400 85 10  5  0  0
     4  0      0 118000  15000 209000    0    0     0     8 1100 1800 92  7  1  0  0

    Qué significa: “r” (run queue) consistentemente por encima del número de CPUs implica contención de CPU. Bajo “id” significa ocupado.

    Decisión: Si la run queue es alta, estás saturado de CPU o estancado. Siguiente: determina si es computación, memoria o locks.

    Tarea 7: Identificar los principales consumidores de CPU y si están haciendo spin

    cr0x@server:~$ top -b -n 1 | head -n 15
    top - 12:14:01 up 21 days,  3:11,  1 user,  load average: 3.90, 3.60, 3.20
    Tasks: 184 total,   2 running, 182 sleeping,   0 stopped,   0 zombie
    %Cpu(s): 92.0 us,  7.0 sy,  0.0 ni,  1.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    2174 app      20   0  1856m  612m  122m R  98.7  7.6  12:11.02 java

    Qué significa: Alta CPU en modo usuario puede seguir estando “esperando” (stalls de memoria, fallos de rama). “wa” bajo no despeja la CPU.

    Decisión: Si un proceso domina, perfílalo; si muchos procesos comparten, sospecha contención sistémica o efectos del scheduler.

    Tarea 8: Comprobar conmutaciones de contexto y presión de interrupciones

    cr0x@server:~$ pidstat -w 1 5
    Linux 6.1.0 (server) 	01/09/2026 	_x86_64_	(2 CPU)
    
    12:14:20      UID       PID   cswch/s nvcswch/s  Command
    12:14:21     1001      2174   1200.00    350.00  java

    Qué significa: Conmutaciones de contexto muy altas pueden implicar demasiados hilos, contención de locks o churn de timers/IO.

    Decisión: Si cswch/s es alto y la latencia es mala, reduce el número de hilos o arregla hotspots de locks antes de comprar hardware.

    Tarea 9: Señales de estrés de ancho de banda de memoria vía perf (ciclos vs stalls frontend/backend)

    cr0x@server:~$ sudo perf stat -p 2174 -e cycles,instructions,branches,branch-misses,cache-misses -a -- sleep 10
     Performance counter stats for 'system wide':
    
         32,100,000,000      cycles
         12,400,000,000      instructions              #    0.39  insn per cycle
          2,900,000,000      branches
            145,000,000      branch-misses             #    5.00% of all branches
            610,000,000      cache-misses

    Qué significa: IPC bajo (0.39) junto con muchos fallos de caché/ramas sugiere que la CPU está ocupada pero no productiva.

    Decisión: Optimiza caminos de código para localidad/predictibilidad de ramas, o elige una CPU con mejor IPC y manejo de memoria.

    Tarea 10: Confirmar si realmente estás limitado por I/O

    cr0x@server:~$ iostat -xz 1 3
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              90.50    0.00    7.20    0.10    0.00    2.20
    
    Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await
    sda              2.10    1.00   45.0    28.0     0.0     0.2    3.0    1.2

    Qué significa: El disco está poco utilizado; await es bajo. Esto no es un cuello de botella de almacenamiento.

    Decisión: Deja de culpar a los discos. Enfócate en CPU/memoria/locks y perfilado a nivel de petición.

    Tarea 11: Revisar presión de memoria y paginación (el asesino silencioso del rendimiento)

    cr0x@server:~$ free -m
                   total        used        free      shared  buff/cache   available
    Mem:            2048        1720         120          12         207         210
    Swap:           2048         900        1148

    Qué significa: Uso de swap puede estar bien, pero si hay paginación activa bajo carga verás stalls y picos.

    Decisión: Si la actividad de swap correlaciona con latencia, reduce la huella de memoria, añade RAM o ajusta el emplazamiento de cargas.

    Tarea 12: Verificar paginación activa, no solo uso de swap

    cr0x@server:~$ sar -B 1 5
    Linux 6.1.0 (server) 	01/09/2026 	_x86_64_	(2 CPU)
    
    12:15:10  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgsteal/s
    12:15:11      0.00      0.00    820.00      0.00   1200.00      0.00      0.00
    12:15:12     10.00     45.00   2100.00     15.00    400.00    800.00    300.00

    Qué significa: Fallos mayores (majflt/s) y escaneos indican presión real de memoria.

    Decisión: Paginación bajo carga es un problema de capacidad. Arregla memoria, no flags de CPU.

    Tarea 13: Inspeccionar la presión del scheduler de un vistazo

    cr0x@server:~$ cat /proc/pressure/cpu
    some avg10=12.34 avg60=10.01 avg300=8.55 total=987654321

    Qué significa: CPU PSI “some” indica tiempo que las tareas pasan esperando recursos de CPU.

    Decisión: Si PSI sube con la latencia, necesitas CPU más efectiva (IPC), menos threads ejecutables o degradación de carga.

    Tarea 14: Detectar contención de locks (a menudo mal diagnosticada como “CPU lenta”)

    cr0x@server:~$ sudo perf top -p 2174
    Samples: 31K of event 'cpu-clock', 4000 Hz, Event count (approx.): 7750000000
    Overhead  Shared Object        Symbol
      12.40%  libc.so.6            [.] pthread_mutex_lock
       9.10%  libjvm.so            [.] SpinPause

    Qué significa: El tiempo se va en locking y spinning, no en trabajo productivo.

    Decisión: Reduce la contención (shard locks, reduce threads, arregla secciones críticas calientes). Más GHz no te salvará.

    Tarea 15: Validar la afinidad de caché mediante un microbenchmark rápido (no sustituye pruebas reales)

    cr0x@server:~$ taskset -c 0 sysbench cpu --cpu-max-prime=20000 run
    CPU speed:
        events per second:  580.21
    
    General statistics:
        total time:                          10.0004s
        total number of events:              5804

    Qué significa: Una prueba intensiva de cómputo puede verse “bien” incluso si tu servicio está limitado por memoria/ramas.

    Decisión: Usa microbenchmarks solo para una comprobación de cordura; basa decisiones en tests representativos de la carga y latencia.

    Broma #2: Si tu plan es “añadir hilos hasta que vaya rápido”, no estás optimizando—estás invocando demonios de contención.

    Guía de diagnóstico rápido: qué comprobar primero/segundo/tercero

    Este es el atajo de grado producción para sorpresas tipo NetBurst: sistemas que parecen “ricos en CPU” en papel pero actúan
    lentos en cargas reales. Quieres el cuello de botella rápido, no un debate filosófico sobre microarquitectura.

    Primero: verifica que la CPU que crees tener sea la que estás recibiendo

    1. Frecuencia bajo carga: comprueba /proc/cpuinfo MHz, el governor y dmesg por throttling.
    2. Térmicos: revisa zonas térmicas y el estado de ventiladores/flujo de aire vía la telemetría disponible.
    3. Virtualización: confirma que no estás limitado por cuotas de CPU o vecinos ruidosos (PSI, cgroups).

    Objetivo: eliminar “la CPU literalmente no está corriendo a la velocidad esperada” en 5 minutos.

    Segundo: determina si estás limitado por computación, memoria o contención

    1. Run queue y PSI: vmstat y /proc/pressure/cpu para espera de CPU.
    2. perf IPC: ciclos vs instrucciones; IPC bajo sugiere stalls/fallos.
    3. Señales de contención de locks: perf top, pidstat context switches, volcados de hilos de la aplicación.

    Objetivo: clasificar el dolor. No puedes arreglar lo que no nombras.

    Tercero: confirma que no es I/O ni paginación

    1. Disco: iostat -xz para utilización y await.
    2. Paginación: sar -B para fallos mayores y actividad de escaneo.
    3. Red: revisa drops/errores y colas (no mostrado arriba, pero deberías hacerlo).

    Objetivo: dejar de perder tiempo en el subsistema equivocado.

    Cuarto: decide si es un problema de encaje de hardware o de software

    • Si el IPC es bajo por fallos de caché y branch misses, necesitas mejor localidad o una CPU con mejor IPC—no más GHz.
    • Si domina la contención, reduce la concurrencia o rediseña los caminos calientes—mejorar hardware no arreglará código serializado.
    • Si hay throttling, arregla primero el enfriamiento y la entrega de potencia; de lo contrario cada otro cambio es ruido.

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

    1) Síntoma: “La CPU está al límite, pero el throughput es mediocre”

    Causa raíz: IPC bajo por fallos de caché, predicciones de rama o latencia de memoria; la CPU parece ocupada pero está estancada.

    Solución: Usa perf stat para confirmar IPC bajo y altos fallos; luego optimiza para localidad, reduce pointer chasing y perfila los caminos calientes. Si vas a comprar hardware, prioriza IPC y subsistema de memoria, no reloj.

    2) Síntoma: “Los picos de latencia aparecen solo en tardes calurosas / después de cambiar un ventilador”

    Causa raíz: Throttling térmico o flujo de aire pobre causando caídas de frecuencia y jitter.

    Solución: Confirma mediante dmesg y lecturas de zonas térmicas; remedia el enfriamiento, limpia filtros, verifica curvas de ventiladores y mantiene margen de temperatura de entrada. Trata los térmicos como dependencia de primer orden del SLO.

    3) Síntoma: “Habilitamos Hyper-Threading y la p99 empeoró”

    Causa raíz: Contención de recursos en unidades de ejecución/cachés compartidas, mayor contención de locks o saturación de ancho de banda de memoria.

    Solución: Prueba A/B HT on/off con concurrencia parecida a producción; reduce hilos; arregla hotspots de locks; considera HT solo para cargas orientadas a throughput o I/O.

    4) Síntoma: “Los microbenchmarks mejoraron, la producción se volvió más lenta”

    Causa raíz: Los microbenchmarks son intensivos y predecibles; producción es ramificada y dependiente de memoria. Diseños tipo NetBurst recompensan lo primero y castigan lo segundo.

    Solución: Mide con mezclas de peticiones realistas, fases de caché caliente/fría y latencia de cola. Incluye concurrencia, comportamiento del asignador y tamaños de datos realistas.

    5) Síntoma: “El load average aumentó después de ‘optimizar’ añadiendo hilos”

    Causa raíz: Sobre-suscripción y contención; más hilos ejecutables incrementan overhead de scheduling y locking.

    Solución: Usa pidstat para medir context switches, perf top para símbolos de lock y reduce concurrencia. Añade paralelismo solo donde el trabajo es paralelo y el cuello de botella se mueve.

    6) Síntoma: “Las actualizaciones de CPU no ayudaron a la base de datos”

    Causa raíz: La carga está limitada por latencia de memoria o ancho de banda (fallos en buffer pool, pointer chasing en B-trees, cache misses).

    Solución: Aumenta la tasa de aciertos de caché efectiva (índices, forma de consultas), añade RAM, reduce el working set y mide misses/IPC. No lances GHz contra un muro de memoria.

    7) Síntoma: “Todo parece bien salvo pausas ocasionales y timeouts”

    Causa raíz: Paginación, pausas de GC o picos de contención que no aparecen como utilización sostenida.

    Solución: Revisa fallos mayores, PSI y métricas de pausa de la aplicación. Arregla presión de memoria y reduce la amplificación de cola (timeouts, reintentos, thundering herds).

    Listas de verificación / plan paso a paso

    Checklist A: Comprar hardware sin repetir el error NetBurst

    1. Define el éxito como latencia y throughput (p50/p95/p99 + RPS sostenido), no la velocidad de reloj.
    2. Mide proxies de IPC: usa perf en cargas representativas; compara ciclos/instrucciones y tasas de fallos.
    3. Modela comportamiento de memoria: tamaño del working set, tasas de acierto de caché, concurrencia esperada y necesidades de ancho de banda.
    4. Valida térmicos: prueba en rack, con temperatura ambiente realista y perfiles de ventiladores.
    5. Prueba impacto de SMT/HT: on/off, con conteos reales de hilos y seguimiento de latencia de cola.
    6. Prefiere sistemas balanceados: canales de memoria, tamaños de caché e interconexión importan tanto como los relojes de los cores.

    Checklist B: Cuando un despliegue de “CPU más rápida” hace prod más lenta

    1. Confirma frecuencia y throttling (governor, temperaturas, dmesg).
    2. Compara perf IPC y tasas de fallos antes/después.
    3. Revisa conteos de hilos y context switching; revierte primero los cambios de “doblar hilos”.
    4. Valida presión de memoria y paginación; arregla fallos mayores inmediatamente.
    5. Busca regresiones de contención de locks introducidas por la nueva concurrencia.
    6. Si aún no está claro, captura un flame graph o artefacto de profiling y revísalo como una línea de tiempo de incidente.

    Checklist C: Estabilizar latencia de cola en sistemas viejos, calientes y con persecución de frecuencia

    1. Reduce la concurrencia para que coincida con los cores (especialmente con HT) y observa el impacto en p99.
    2. Afina la afinidad de hilos solo si conoces tu topología; de lo contrario te puedes encerrar en una esquina.
    3. Mantén el governor de CPU consistente (a menudo “performance” para nodos críticos de latencia).
    4. Haz cumplir margen térmico: alerta por temperatura y eventos de throttling, no solo por utilización de CPU.
    5. Optimiza caminos calientes para localidad; elimina ramas impredecibles cuando sea posible.
    6. Introduce backpressure y timeouts razonables para evitar tormentas de reintentos.

    Preguntas frecuentes

    1) ¿Fue Pentium 4 realmente “malo” o solo malentendido?

    Fue una apuesta estrecha. En cargas que coincidían con sus puntos fuertes (streaming, código predecible y alto apalancamiento de reloj),
    podía rendir bien. En cargas mixtas de servidor, a menudo entregaba peor rendimiento real por vatio y por dólar que alternativas. “Malentendido” es generoso; “mal vendido” está más cerca.

    2) ¿Por qué más GHz no se tradujo en más rendimiento?

    Porque el rendimiento depende del trabajo útil por ciclo (IPC) y de con qué frecuencia te atas en memoria, ramas y contención.
    NetBurst aumentó el conteo de ciclos pero a menudo redujo el trabajo útil por ciclo en cargas reales.

    3) ¿Cuál es la lección operativa para sistemas modernos?

    No aceptes una sola métrica de titular. Para CPUs es GHz; para almacenamiento es “IOPS”; para redes es “Gbps”.
    Pregunta siempre: ¿bajo qué latencia, con qué concurrencia y con qué comportamiento de cola?

    4) ¿Hyper-Threading “arregló” NetBurst?

    Ayudó en throughput en algunos casos llenando ranuras de ejecución ociosas, pero no cambió los fundamentos:
    penalizaciones por pipeline profundo, cuellos de botella de memoria y limitaciones térmicas. También podía empeorar la latencia de cola añadiendo contención.
    Trátalo como un ajuste, no como un valor por defecto bueno.

    5) ¿Por qué Pentium M a veces vencía a Pentium 4 con relojes mucho más bajos?

    Pentium M (de la estirpe P6) enfatizaba IPC y eficiencia. En cargas con muchas ramas y sensibles a caché, un IPC más alto
    junto con mejor eficiencia a menudo vence a la frecuencia bruta, especialmente cuando el reloj provoca throttling de potencia y térmico.

    6) ¿Cómo puedo saber si mi carga está limitada por memoria en vez de por CPU?

    Busca IPC bajo con muchos fallos de caché en perf, además de mejora limitada al añadir cores o subir frecuencia.
    También verás plateau de throughput mientras la CPU se mantiene “ocupada”. Eso suele ser un muro de memoria o de contención.

    7) ¿Es el throttling térmico lo suficientemente común como para importar?

    En diseños que corren calientes y en datacenters reales, sí. Incluso throttling modesto crea jitter. El jitter se convierte en latencia de cola,
    y la latencia de cola se convierte en incidentes cuando reintentos y timeouts amplifican la carga.

    8) ¿Qué debo benchmarkear para evitar errores de la era GHz?

    Mide el servicio real: mezcla de peticiones realista, tamaño de datos realista, concurrencia realista e informa p95/p99 de latencia además de throughput.
    Añade una fase de caché fría y una ejecución sostenida lo suficientemente larga para calentar el sistema.

    9) ¿Existen equivalentes modernos de la trampa NetBurst?

    Sí. Cualquier vez que optimices una métrica pico a costa del comportamiento sistémico: frecuencias turbo sin presupuesto térmico, benchmarks de almacenamiento
    que ignoran latencia de fsync o pruebas de red que ignoran pérdida de paquetes bajo carga. El patrón es el mismo: el pico gana en la presentación, la cola pierde al cliente.

    Conclusión: qué hacer la próxima vez que te vendan GHz

    NetBurst no es solo trivia retro de CPUs. Es una historia clara sobre incentivos, medición y el coste de apostar
    a un solo número. Intel optimizó para frecuencia porque el mercado pagaba por frecuencia. Las cargas que importaban—
    código con muchas ramas, sistemas pesados en memoria, racks con restricciones térmicas—enviaron la factura.

    Los siguientes pasos prácticos son aburridos, y por eso funcionan:

    1. Define el rendimiento usando latencia de cola, no el pico de throughput y definitivamente no la velocidad de reloj.
    2. Instrumenta para cuellos de botella: contadores perf, PSI, métricas de paginación y señales térmicas/de throttling.
    3. Benchmarkea como en producción: concurrencia, tamaño de datos, comportamiento de caché, heat soak y mezclas de peticiones realistas.
    4. Trata los térmicos como capacidad: si la CPU hace throttling, tu arquitectura está “limitada por enfriamiento”. Acéptalo.
    5. Sospecha de “rendimiento gratis”: HT/SMT, concurrencia agresiva y micro-optimizaciones que ignoran la contención.

    Si recuerdas solo una cosa: los relojes son un componente, no una garantía. El sistema es el producto. Operalo como tal.