La página está arriba. Los pods están verdes. Los logs del contenedor parecen aburridos. Mientras tanto tu host está gritando:
carga promedio en la estratosfera, SSH tarda 30 segundos en devolver un carácter, y kswapd se come CPU como si le pagaran por ciclo.
Este es el tipo especial de fallo en producción donde todo “funciona” hasta que el negocio nota latencia,
timeouts y bases de datos misteriosamente “lentas”. Bienvenido a la tormenta de swap: no hay un único crash, solo una fusión en cámara lenta.
Qué es una tormenta de swap (y por qué te engaña)
Una tormenta de swap es una presión de memoria sostenida que obliga al kernel a expulsar continuamente páginas de RAM al swap,
y luego volver a traerlas, repetidamente. No es simplemente “algo de swap usado.” Es que el sistema pasa tanto tiempo
moviendo páginas que el trabajo útil pasa a un segundo plano.
La parte desagradable es que muchas aplicaciones siguen “funcionando.” Responden, lentamente. Reintentan. Hacen timeout y se reintentan.
Tu orquestador ve procesos todavía vivos, health checks apenas pasando, y piensa que todo está bien.
Los humanos lo notan primero: todo se siente pegajoso.
Dos señales que separan “swap usado” de “tormenta de swap”
- Faults de página mayores se disparan (lectura de páginas desde swap en disco).
- PSI de memoria muestra bloqueos sostenidos (tareas esperando reclaim de memoria / IO).
Si solo miras el “porcentaje de swap usado”, te mentirán. El swap puede estar al 20% y estable durante semanas sin drama.
A la inversa, el swap puede estar “solo” al 5% y aún así estar en tormenta si el conjunto de trabajo churnea.
Datos interesantes y contexto histórico (porque este lío tiene historia)
- El comportamiento OOM temprano de Linux era notoriamente tosco. El OOM killer del kernel evolucionó durante décadas; todavía sorprende a la gente bajo presión.
- Los cgroups llegaron para detener a los “vecinos ruidosos”. Se diseñaron para sistemas compartidos mucho antes de que los contenedores los popularizaran.
- La contabilidad de swap en cgroups ha sido controvertida. Añade sobrecarga y ha tenido bugs en el mundo real; muchas plataformas la desactivaron por defecto.
- Kubernetes históricamente desaconsejaba el swap. No porque el swap sea malvado, sino porque el aislamiento de memoria predecible es difícil cuando el swapping entra en juego.
- El número de “memoria libre” se ha malinterpretado desde siempre. Linux usa RAM para cache de páginas agresivamente; que “free” sea bajo a menudo es sano.
- Pressure Stall Information (PSI) es relativamente nueva. Es una de las mejores herramientas modernas para ver “esperando por memoria” sin adivinar.
- El swap en SSD hizo las tormentas más silenciosas, no más seguras. El swap más rápido reduce el dolor… hasta que enmascara el problema y alcanzas amplificación de escritura y acantilados de latencia.
- Los valores por defecto de overcommit son un artefacto cultural. Linux asume que muchos programas asignan más de lo que tocan; esto es verdad hasta que deja de serlo.
Por qué los contenedores parecen sanos mientras el host muere
Los contenedores no tienen su propio kernel. Son procesos agrupados por cgroups y namespaces.
La presión de memoria la gestiona el kernel del host, y es ese kernel el que hace reclaim y swap.
Aquí está la ilusión: un contenedor puede seguir ejecutándose y respondiendo mientras el host está intercambiando intensamente, porque
los procesos del contenedor siguen siendo programados y siguen avanzando—solo que a un coste terrible.
El uso de CPU del contenedor incluso puede parecer más bajo porque está bloqueado en IO (swap-in), no gastando CPU.
Los modos de fallo principales que crean el patrón “contenedores bien, host derritiéndose”
- Sin límites de memoria (o límites equivocados). Un contenedor crece hasta que el host reclama y todos empiezan a swapear.
- Límites establecidos pero swap sin restricción. El contenedor se mantiene bajo su tope de RAM pero aún empuja presión hacia el reclaim global por patrones de page cache y recursos compartidos.
- Page cache + IO de sistema de ficheros dominan. Contenedores que hacen IO pueden saturar cache, forzando reclaim y swap para otras cargas.
- Overcommit + ráfagas. Muchos servicios asignan agresivamente al mismo tiempo; no OOMeas inmediatamente, churnneas.
- Política OOM evita matar. El sistema hace swap en vez de fallar rápido, intercambiando corrección por “disponibilidad” de la peor manera.
Un giro más: la telemetría a nivel de contenedor puede engañar. Algunas herramientas informan uso de memoria del cgroup pero no
el dolor del reclaim a nivel host. Verás contenedores “dentro de límites” mientras el host pasa el día barajando páginas.
Broma #1: El swap es como un trastero: te sientes organizado hasta que te das cuenta de que pagas mensualmente por guardar la basura que aún necesitas a diario.
Conceptos básicos de memoria en Linux que realmente necesitas
No necesitas memorizar código del kernel. Necesitas algunos conceptos para razonar sobre tormentas de swap sin superstición.
Conjunto de trabajo vs memoria asignada
La mayoría de las aplicaciones asignan memoria que no tocan activamente. Al kernel no le importa la “asignada”, le importan
las páginas “usadas recientemente”. Tu conjunto de trabajo son las páginas que tocas con suficiente frecuencia como para que expulsarlas duela.
Las tormentas de swap ocurren cuando el conjunto de trabajo no cabe, o cuando cabe pero el kernel se ve forzado a churnear páginas debido a
demandas en competencia (page cache, otros cgroups, o un ofensor único que sigue ensuciando memoria).
Memoria anónima vs memoria respaldada por archivo
- Anónima: heap, stack—swappeable al swap.
- Respaldada por archivo: page cache—evictable sin swap (solo volver a leer desde el archivo) a menos que esté dirty.
Cuando ejecutas bases de datos, caches, JVMs y servicios muy verbosos en logs en el mismo host, el reclaim anónimo y el respaldado por archivo
interactúan de formas entretenidas. “Entretenidas” aquí significa “un postmortem que leerás a las 2 a. m.”
Reclaim, kswapd y reclaim directo
El kernel intenta recuperar memoria en segundo plano (kswapd). Bajo fuerte presión, los propios procesos pueden entrar en
direct reclaim: se quedan bloqueados intentando liberar memoria. Ahí es donde la latencia va a morir.
Por qué las tormentas de swap se sienten como problemas de CPU
El reclaim quema CPU. La compresión puede quemar CPU (zswap/zram). Faultear páginas de vuelta quema CPU y IO.
Y tus hilos de aplicación pueden estar bloqueados, haciendo confusos los gráficos de utilización: baja CPU de la app, alta CPU del sistema, alto IO wait.
cgroups, Docker y las aristas alrededor del swap
Docker usa cgroups para restringir recursos. Pero “restricciones de memoria” son una colección variada según la versión del kernel,
cgroup v1 vs v2, y cómo esté configurado Docker.
cgroup v1 vs v2: diferencias prácticas para tormentas de swap
En cgroup v1, memoria y swap se gestionaban con perillas separadas (memory.limit_in_bytes, memory.memsw.limit_in_bytes),
y la contabilidad de swap podía estar desactivada. En cgroup v2, la memoria está más unificada y la interfaz es más limpia:
memory.max, memory.swap.max, memory.high, además de métricas de presión.
Si estás en cgroup v2 y no usas memory.high, te estás perdiendo una de las mejores herramientas para evitar que un solo cgroup convierta
el host en una tostadora alimentada por swap.
Flags de memoria de Docker: qué significan en realidad
--memory: límite duro. Si se excede, el cgroup reclamará; si no puede, obtendrás OOM (dentro de ese cgroup).--memory-swap: en muchas configuraciones, límite total de memoria+swap. La semántica varía; en algunos sistemas se ignora sin contabilidad de swap.--oom-kill-disable: casi siempre una mala idea en producción. Anima al host a sufrir más tiempo.
Lo de que el contenedor “funciona” mientras el host se derrite suele ser resultado de una decisión de política:
le pedimos al sistema “no mates, solo esfuérzate más.” El kernel cumplió.
Una cita que deberías tatuar en tus runbooks
“La esperanza no es una estrategia.” — idea parafraseada común en círculos de ingeniería/ops; el punto sigue siendo válido.
Guía rápida de diagnóstico
Este es el orden que uso cuando alguien avisa “el host está lento” y sospecho presión de memoria. Está diseñado para
llevarte a una decisión rápido: matar, limitar, mover o tunear.
Primero: confirma que es realmente una tormenta de swap (no solo ‘swap usado’)
- Revisa la actividad actual de swap (tasas de swap-in/out) y los faults de página mayores.
- Revisa PSI memory para ver si hay bloqueos sostenidos.
- Comprueba si el subsistema de IO está saturado (swap es IO).
Segundo: encuentra el cgroup/contenedor culpable
- Compara uso de memoria por contenedor, incluyendo RSS y cache.
- Revisa qué cgroups están disparando OOM o alto reclaim.
- Busca un patrón de carga (crecimiento de heap JVM, job por lotes, ráfaga de logs, compactación, reconstrucción de índices).
Tercero: decide la mitigación
- Si la latencia importa más que completar: falla rápido (límites estrictos, permitir OOM, reiniciar limpio).
- Si completar importa más que la latencia: aisla (nodos dedicados, bajar swappiness, swap controlado, más lento pero estable).
- Si estás a ciegas: añade PSI + métricas de memoria por cgroup primero. Ajustar sin visibilidad es jugar a la ruleta.
Tareas prácticas: comandos, salidas, decisiones
Estos son los comandos que realmente ejecuto en un host real. Cada tarea incluye lo que significa la salida y qué decisión permite.
Ajusta nombres de interfaces y rutas para que coincidan con tu entorno.
Tarea 1: Confirma que existe swap y cuánto se usa
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 62Gi 54Gi 1.2Gi 1.1Gi 6.8Gi 2.3Gi
Swap: 16Gi 12Gi 4.0Gi
Significado: El swap está muy usado (12Gi) y la memoria disponible es baja (2.3Gi). No es prueba de tormenta, pero es sospechoso.
Decisión: Pasa a métricas de actividad; el swap usado por sí solo no justifica acción.
Tarea 2: Mide la actividad de swap y la presión por faults de página
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 1 12453248 312000 68000 512000 60 210 120 980 1200 2400 12 18 42 28 0
2 2 12454800 298000 66000 500000 180 640 200 1800 1800 3200 8 22 30 40 0
4 2 12456000 286000 64000 490000 220 710 260 2100 1900 3300 9 23 24 44 0
3 3 12456800 280000 62000 482000 240 680 300 2000 2000 3500 10 24 22 44 0
2 2 12458000 276000 60000 475000 210 650 280 1900 1950 3400 9 23 25 43 0
Significado: si/so no triviales (swap-in/out) cada segundo y alto wa (IO wait). Eso es paginación activa.
Decisión: Trátalo como tormenta. Siguiente: determina si el IO está saturado y qué cgroup presiona memoria.
Tarea 3: Revisa PSI para bloqueos de memoria (host-level)
cr0x@server:~$ cat /proc/pressure/memory
some avg10=18.40 avg60=12.12 avg300=8.50 total=192003210
full avg10=6.20 avg60=3.90 avg300=2.10 total=48200321
Significado: La presión full indica que las tareas se quedan con frecuencia bloqueadas porque el reclaim de memoria no puede seguir el ritmo. Esto se correlaciona fuertemente con picos de latencia.
Decisión: Deja de buscar “bugs de CPU.” Esto es presión de memoria. Encuentra al culpable y limita/mata/aisla.
Tarea 4: Identifica si estás en cgroup v1 o v2
cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs
Significado: cgroup v2 está activo. Puedes usar memory.high y memory.swap.max.
Decisión: Prefiere controles de v2; evita perillas de la era v1 que no aplican.
Tarea 5: Ver consumidores de memoria por contenedor
cr0x@server:~$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a1b2c3d4e5f6 api-prod 35.20% 1.8GiB / 2GiB 90.00% 2.1GB / 1.9GB 120MB / 3GB 210
b2c3d4e5f6g7 search-indexer 4.10% 7.4GiB / 8GiB 92.50% 150MB / 90MB 30GB / 2GB 65
c3d4e5f6g7h8 metrics-agent 0.50% 220MiB / 512MiB 42.97% 20MB / 18MB 2MB / 1MB 14
Significado: search-indexer está cerca de su límite de memoria y haciendo IO de bloque enorme (30GB reads/writes), lo que podría ser churn de page cache, compactación o spill.
Decisión: Investiga los métricas del cgroup de ese contenedor (reclaim, swap, eventos OOM).
Tarea 6: Inspecciona límites de memoria + swap (v2) para un contenedor sospechoso
cr0x@server:~$ CID=b2c3d4e5f6g7
cr0x@server:~$ CG=$(docker inspect -f '{{.HostConfig.CgroupParent}}' "$CID")
cr0x@server:~$ docker inspect -f '{{.Id}} {{.Name}}' "$CID"
b2c3d4e5f6g7h8i9j0 /search-indexer
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.max
8589934592
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.swap.max
max
Significado: El límite de RAM es 8GiB, pero el swap es ilimitado (max). Bajo presión, este cgroup puede empujar mucho swap.
Decisión: Establece memory.swap.max o configura Docker para acotar el swap en contenedores que no deberían paginar.
Tarea 7: Revisa eventos por cgroup: ¿estás alcanzando reclaim/OOM?
cr0x@server:~$ cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.events
low 0
high 1224
max 18
oom 2
oom_kill 2
Significado: El cgroup alcanzó high frecuentemente y ha tenido kills por OOM. Esto no es “inestabilidad aleatoria”; es un problema de dimensionamiento.
Decisión: O aumentas memoria, reduces el conjunto de trabajo, o aceptas reinicios pero previenes el thrash a nivel host acotando swap y usando memory.high.
Tarea 8: Observa uso de swap por proceso (encuentra al verdadero glotón)
cr0x@server:~$ sudo smem -rs swap | head -n 8
PID User Command Swap USS PSS RSS
18231 root java -jar indexer.jar 6144M 4096M 4200M 7000M
9132 root python3 /app/worker.py 820M 600M 650M 1200M
2210 root dockerd 90M 60M 70M 180M
1987 root containerd 40M 25M 30M 90M
1544 root /usr/bin/prometheus 10M 900M 920M 980M
1123 root /usr/sbin/sshd 1M 2M 3M 8M
Significado: El indexador Java tiene 6GiB paginados. Eso explica “contenedor vivo pero lento”: está faultando páginas constantemente.
Decisión: Si esta carga no debe paginar, acótala y fuerza OOM/reinicio. Si debe hacerlo, aísla a un host con swap más rápido y menor contención.
Tarea 9: Revisa saturación de disco (swap es IO; IO es latencia)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 01/02/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
10.21 0.00 22.11 41.90 0.00 25.78
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 120.0 12800.0 2.0 1.64 18.2 106.7 210.0 24400.0 8.0 3.67 32.5 116.2 9.80 99.20
Significado: %util cerca de 100% y alto await. El NVMe está saturado; swap-in/out se encolará, causando bloqueos por todas partes.
Decisión: Mitigación inmediata: reduce la presión de memoria (mata al culpable, baja concurrencia). A largo plazo: separa el swap del IO de trabajo o usa almacenamiento más rápido.
Tarea 10: Ver qué procesos están atrapados en reclaim o esperas de IO
cr0x@server:~$ ps -eo pid,stat,wchan:20,comm --sort=stat | head -n 12
PID STAT WCHAN COMMAND
18231 D io_schedule java
19102 D io_schedule java
9132 D io_schedule python3
24011 D balance_pgdat postgres
24022 D balance_pgdat postgres
2210 Ssl ep_poll dockerd
1987 Ssl ep_poll containerd
Significado: Estado D + io_schedule indica sueño no interrumpible esperando IO. balance_pgdat sugiere reclaim directo.
Decisión: Tu latencia es espera a nivel kernel. Deja de escalar tráfico; empeorará la cola. Reduce carga o detén al culpable.
Tarea 11: Revisa logs del kernel para OOM y advertencias de reclaim
cr0x@server:~$ sudo dmesg -T | tail -n 12
[Thu Jan 2 10:14:22 2026] Memory cgroup out of memory: Killed process 18231 (java) total-vm:12422392kB, anon-rss:7023120kB, file-rss:10244kB, shmem-rss:0kB
[Thu Jan 2 10:14:22 2026] oom_reaper: reaped process 18231 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
[Thu Jan 2 10:14:25 2026] Out of memory: Killed process 9132 (python3) total-vm:2048320kB, anon-rss:1192200kB, file-rss:9120kB, shmem-rss:0kB
Significado: Hubo kills por OOM de cgroup. Eso es preferible a tormentas de swap a nivel host—si tu servicio puede reiniciar limpiamente.
Decisión: Confirma políticas de reinicio, ajusta límites y establece expectativas: OOM controlado vence al colapso incontrolado del host.
Tarea 12: Inspecciona swappiness del host y postura de overcommit
cr0x@server:~$ sysctl vm.swappiness vm.overcommit_memory vm.overcommit_ratio
vm.swappiness = 60
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
Significado: Swappiness 60 es algo por defecto y puede ser demasiado ansioso en hosts con contenedores mixtos. Overcommit es heurístico (0).
Decisión: Si este host ejecuta servicios sensibles a latencia, considera bajar swappiness y apretar overcommit, pero solo después de tener límites por cgroup.
Tarea 13: Verifica zswap/zram (ayuda a veces, oculta problemas siempre)
cr0x@server:~$ grep -H . /sys/module/zswap/parameters/enabled /sys/block/zram0/disksize 2>/dev/null
/sys/module/zswap/parameters/enabled:Y
Significado: zswap está habilitado; las páginas de swap pueden comprimirse en RAM. Esto reduce IO pero aumenta CPU y puede enmascarar la presión hasta que sea demasiado tarde.
Decisión: Mantenlo si aporta estabilidad. No lo uses como permiso para ejecutar cargas sin límites de memoria.
Tarea 14: Revisa supuestos de contabilidad de memoria del daemon Docker y del kernel
cr0x@server:~$ docker info | sed -n '1,40p'
Client:
Version: 26.1.0
Context: default
Debug Mode: false
Server:
Containers: 38
Running: 33
Paused: 0
Stopped: 5
Server Version: 26.1.0
Storage Driver: overlay2
Cgroup Driver: systemd
Cgroup Version: 2
Kernel Version: 6.5.0
Operating System: Ubuntu 24.04 LTS
OSType: linux
Significado: systemd + cgroup v2. Bien. Tus controles existen; solo necesitas usarlos.
Decisión: Implementa políticas conscientes de cgroup v2 (memory.high, memory.swap.max) en lugar de guías legacy.
Tres minihistorias corporativas desde las trincheras de la memoria
Mini-historia 1: El incidente causado por una suposición equivocada
Una compañía SaaS mediana ejecutaba una flota de workers de indexación en Docker en unos pocos hosts potentes. Tenían swap habilitado “por seguridad”,
y tenían límites de memoria en contenedores. Todos se sentían responsables. Todos dormían.
Durante una semana ocupada, el backlog de indexación creció y un ingeniero aumentó la concurrencia de workers dentro del contenedor.
El contenedor se mantuvo bajo su tope --memory la mayor parte del tiempo, pero el patrón de asignación de la carga se volvió picudo:
grandes buffers transitorios, IO de ficheros intenso y caching agresivo. El host empezó a swapear.
La suposición equivocada fue sutil: “Si los contenedores tienen límites de memoria, el host no se swapeará hasta el colapso.”
En realidad, los límites evitan que un solo cgroup use RAM infinita, pero no garantizan automáticamente equidad a nivel host
ni previenen el dolor de reclaim global—especialmente cuando el swap es ilimitado y la ruta de IO es compartida.
Los síntomas fueron clásicos. La latencia de la API se duplicó, luego se triplicó. Los inicios de sesión SSH se trabaron. La monitorización mostraba “memoria del contenedor dentro de límites,”
así que el equipo persiguió optimizaciones de red y base de datos durante horas. Eventualmente alguien ejecutó cat /proc/pressure/memory
y la historia se escribió sola.
La solución no fue exótica. Establecieron memory.swap.max para los workers de indexación, añadieron memory.high para estrangularlos antes del OOM,
y movieron la carga de indexación a nodos dedicados con su propio presupuesto de IO. La mayor mejora vino de lo aburrido:
escribir que “los límites de memoria no protegen al host” y convertirlo en una puerta de despliegue.
Mini-historia 2: La optimización que salió mal
Otra organización tenía una canalización de logging multi-tenant. Querían reducir la carga de escrituras en disco, así que habilitaron zswap y aumentaron el tamaño de swap.
Los resultados iniciales fueron geniales: menos picos de escritura, gráficos de IO más suaves, menos OOMs inmediatos.
Luego llegó un incidente menor: un cliente activó logging verboso más compresión a nivel de aplicación.
El volumen de logs se disparó, la CPU subió y la presión de memoria aumentó. Con zswap, el kernel comprimía páginas de swap primero en RAM.
Eso redujo IO de swap, pero aumentó el tiempo de CPU dedicado a comprimir y reclaim.
En los dashboards parecía “saturación de CPU”, no “presión de memoria.” El equipo afinó límites de CPU, añadió cores y agrandó el host.
El sistema empeoró. Más RAM significó caches más grandes y más para churnear; más CPU permitió que zswap comprima más, retrasando la falla obvia.
La jitter de latencia se volvió permanente.
El problema no fue zswap en sí. Fue tratarlo como optimización de rendimiento en lugar de amortiguador de presión.
El verdadero problema era crecimiento de memoria incontrolado en una etapa de parsing y ausencia de throttling con memory.high. El swapping era el síntoma,
zswap fue el amplificador que lo hizo más difícil de ver.
Lo arreglaron poniendo techos estrictos por etapa, añadiendo backpressure en la canalización y usando zswap solo en nodos dedicados a procesamiento por lotes donde la latencia no importaba.
También cambiaron la alerta: PSI memoria sostenida > umbral pasó a ser incidente serio.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un equipo de servicios financieros ejecutaba una mezcla de APIs de cara al cliente y una conciliación nocturna por lotes en el mismo clúster de Kubernetes.
Eran dolorosamente conservadores: requests/limits por servicio, umbrales de eviction revisados trimestralmente,
y cada pool de nodos tenía una “política de swap” documentada. No era emocionante. También por eso no tenían outages emocionantes.
Una noche, un job por lotes empezó a usar más memoria tras una actualización de una librería de un vendor. Creció de forma sostenida, no explosiva.
En un equipo menos disciplinado, esto se convierte en una lenta tormenta de swap y un informe de incidente con las palabras “no lo sabemos.”
Su sistema hizo algo poco glamoroso: el job alcanzó su límite de memoria, fue OOM-killado y reinició con una concurrencia reducida
(un fallback predefinido). El lote corrió más lento. Las APIs se mantuvieron rápidas. El on-call recibió una alerta específica:
“batch job OOMKilled; PSI del host normal.”
A la mañana siguiente, revertieron la librería, abrieron issue upstream y ajustaron la request de memoria del job de forma permanente.
Nadie escribió un mensaje heroico en Slack. Ese es el punto. La práctica correcta—límites más fallos controlados—evitó que existiera un evento “host derritiéndose”.
Soluciones que funcionan: límites, OOM, swappiness y monitorización
1) Pon límites de memoria en cada contenedor que importe
No poner límite es una decisión. Es decidir que el host será tu límite. El host es un mal límite porque falla de forma colectiva: todas las cargas sufren, y luego todo colapsa.
Usa límites por servicio dimensionados al conjunto de trabajo, no a fantasías de picos de asignación. Para JVMs, eso significa configurar el heap
intencionalmente y dejar margen para off-heap y asignaciones nativas.
2) Usa memory.high (cgroup v2) para estrangular antes del OOM
memory.max es un acantilado. memory.high es un límite de velocidad. Cuando defines memory.high, el kernel empieza a reclamar
y a throttlear asignaciones una vez que el cgroup lo excede, lo que tiende a reducir el thrash a nivel host.
Para servicios con ráfagas, memory.high un poco por debajo de memory.max suele producir un sistema que es más lento bajo presión pero aún
controlable, en vez de caótico.
3) Acota el swap por carga, o desactívalo para servicios sensibles a latencia
El swap ilimitado es cómo terminas con un servidor “arriba” pero inútil. Para servicios que deben ser responsivos,
prefiere o bien que no usen swap o una asignación de swap muy pequeña.
Si necesitas swap para cargas por lotes, aíslalas. El swap no es gratis; es decidir intercambiar latencia por completitud.
Mezclar “debe ser rápido” con “puede ser lento” en el mismo nodo respaldado por swap es un experimento sociológico.
4) Baja el swappiness (con cuidado) en hosts con contenedores mixtos
vm.swappiness controla cuán agresivamente el kernel intercambia memoria anónima frente a reclamar page cache.
En hosts con bases de datos o servicios de baja latencia, un valor más bajo (como 10 o 1) puede reducir el intercambio de páginas calientes.
No cargo-cultes vm.swappiness=1 por todas partes. Si tu host depende del swap para evitar OOM y bajas swappiness sin arreglar el dimensionamiento,
solo cambiarás tormentas de swap por tormentas de OOM.
5) Prefiere OOM controlado sobre thrash incontrolado
Esta es la parte que la gente odia emocionalmente pero ama operativamente: para muchos servicios, reiniciar es más barato que paginar.
Si un servicio no puede correr dentro de su presupuesto, matarlo es honestidad.
Evita deshabilitar el OOM killer para contenedores a menos que entiendas profundamente las consecuencias. Deshabilitar el OOM kill es cómo conviertes un proceso malo en un incidente a nivel host.
Broma #2: Deshabilitar el OOM killer es como quitar la alarma de incendios porque es ruidosa—ahora puedes disfrutar de las llamas en paz.
6) Vigila PSI y métricas de reclaim, no solo “memoria usada”
Las alertas más útiles son las que te dicen “el kernel está bloqueando tareas.”
PSI te da eso directamente. Combínalo con tasas de swap-in/out y latencia de IO.
7) Separa rutas de IO cuando el swap sea inevitable
Si el swap y tu carga principal comparten el mismo disco, has creado un bucle de realimentación: el swap causa encolamiento de IO,
el encolamiento de IO ralentiza las apps, las apps lentas mantienen la memoria por más tiempo, la presión de memoria aumenta, el swap aumenta. Felicidades, construiste un carrusel.
En sistemas serios, el swap pertenece a almacenamiento rápido, a veces dispositivos separados, o usa zram para margen de emergencia acotado
(con el CPU contabilizado).
8) Haz del presupuesto de memoria parte del despliegue, no del postmortem
Realidad corporativa: los equipos no “recordarán añadir límites después.” Van a enviar hoy. Tu trabajo es convertir esto en una línea de defensa:
checks en CI para Compose, políticas de admission en Kubernetes, y alertas en runtime para “sin límite establecido.”
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: La load average del host es enorme; uso de CPU parece moderado
Causa raíz: Hilos bloqueados en IO wait durante swap-in o reclaim directo. La carga cuenta tareas ejecutables + tareas no interrumpibles.
Solución: Confirma con vmstat/iostat/ps (estado D). Reduce presión de memoria de inmediato; acota/mata al culpable; añade memory.high.
2) Síntoma: Los contenedores muestran “dentro del límite de memoria”, pero el host swappea fuertemente
Causa raíz: Existen límites pero el swap es ilimitado, el churn del page cache fuerza reclaim global, o múltiples contenedores colectivamente exceden la capacidad del host.
Solución: Establece límites de swap por cgroup (memory.swap.max) y presupuestos realistas de memoria. No oversuscribas sin una política explícita.
3) Síntoma: Picos de latencia aleatorios en servicios no relacionados
Causa raíz: Reclaim global y encolamiento de IO crean acoplamiento cruzado entre servicios. Un ofensor de memoria castiga a todos.
Solución: Aísla cargas ruidosas en nodos/pools separados; aplica límites; vigila PSI.
4) Síntoma: “Añadimos más swap y empeoró”
Causa raíz: Más swap aumenta el tiempo que un host puede permanecer en un estado degradado de paginación, amplificando la latencia tail y la confusión operativa.
Solución: Trata el swap como buffer de emergencia, no como capacidad. Prefiere OOM/restart para servicios sensibles a latencia, o aísla jobs por lotes.
5) Síntoma: OOM kills ocurren pero el host sigue lento después
Causa raíz: El swap permanece poblado; el reclaim y los refaults continúan; la cola de IO sigue drenando; caches fragmentados.
Solución: Tras eliminar al culpable, permite tiempo de recuperación; reduce la presión de IO; considera limpiar caches temporalmente solo como último recurso y entendiendo el blast radius.
6) Síntoma: “Memoria libre” cerca de cero; alguien entra en pánico
Causa raíz: Linux usa RAM para page cache; poco free es normal cuando available es sano y PSI es bajo.
Solución: Educa a los equipos a usar available y PSI, no free. Alerta sobre presión y churn, no estética.
7) Síntoma: Después de habilitar zswap/zram, la CPU aumentó y el throughput bajó
Causa raíz: Overhead de compresión más presión de memoria continua. Moviste el coste del disco a la CPU.
Solución: Actívalo solo cuando exista margen de CPU; limita uso de swap; arregla el presupuesto real de memoria.
8) Síntoma: “Swappiness=1 lo arregló” (hasta la próxima semana)
Causa raíz: Reducir swap enmascaró un dimensionamiento pobre temporalmente; la presión sigue existiendo y ahora puede convertirse en OOM abrupto.
Solución: Dimensiona las cargas, establece límites, añade memory.high/backpressure. Afina swappiness como paso final, no como medida inicial.
Listas de verificación / plan paso a paso
Respuesta inmediata al incidente (15 minutos)
- Ejecuta
vmstat 1ycat /proc/pressure/memory. Confirma paginación activa + bloqueos. - Ejecuta
iostat -xz 1. Confirma saturación de disco / await. - Encuentra al culpable:
docker stats, luego swap por proceso consmemo inspecciona eventos de cgroup. - Mitiga:
- Reduce concurrencia / tráfico.
- Detén el contenedor culpable o reinícialo con menor consumo de memoria.
- Si es necesario, mueve temporalmente al culpable a un host dedicado.
- Verifica la recuperación: tasas de swap-in/out bajan,
fullde PSI se acerca a ~0, y await de IO se normaliza.
Plan de estabilización (mismo día)
- Establece límites de memoria para cualquier contenedor que no los tenga.
- En cgroup v2: añade
memory.highpara workloads con ráfagas para reducir thrash. - Acota swap donde corresponda (
memory.swap.max), especialmente para servicios sensibles a latencia. - Revisa el uso de
--oom-kill-disable; elimínalo salvo que haya una razón muy específica. - Ajusta la separación de roles de nodo: batch vs servicios sensibles a latencia no deberían compartir el mismo destino de memoria/IO.
Plan de hardening (próximo sprint)
- Añade alertas sobre PSI memoria (
someyfull) sostenido. - Añade alertas sobre tasa de swap-in/out, faults mayores y await de disco.
- Implementa policy-as-code: lints de Compose o controles de admisión que requieran límites de memoria.
- Documenta presupuestos de memoria por servicio y comportamiento ante reinicio (qué pasa en OOM, cuán rápido se recupera).
- Prueba con carga bajo presión de memoria: lanza picos de concurrencia y verifica que el host permanezca interactivo.
Preguntas frecuentes
1) ¿El swap siempre es malo para hosts Docker?
No. El swap es una herramienta. Es útil como buffer de emergencia y para cargas por lotes. Es peligroso cuando se convierte en muleta para
servicios mal dimensionados y deja al host “arriba pero inutilizable.”
2) ¿Por qué mis contenedores muestran baja CPU mientras el sistema está lento?
Porque están bloqueados. En tormentas de swap, los hilos a menudo quedan en IO wait o reclaim directo. Tu gráfico de CPU no muestra “esperando disco”
a menos que mires iowait, PSI o tareas bloqueadas.
3) ¿Debería desactivar el swap para prevenir tormentas?
Si ejecutas servicios sensibles a latencia y tienes buenos límites, desactivar swap puede mejorar la predictibilidad.
Pero también hace más probable el OOM. El enfoque correcto suele ser: establece límites y políticas primero, luego decide la postura sobre swap.
4) ¿Cuál es la diferencia entre OOM y thrash de swap en términos de impacto al usuario?
OOM es abrupto y ruidoso: un proceso muere y se reinicia. El thrash de swap es prolongado y sigiloso: todo está lento, los timeouts se encadenan,
y obtienes fallos secundarios. En muchos sistemas, un OOM controlado es el mal menor.
5) ¿Por qué añadir más RAM a veces no lo soluciona?
Más RAM ayuda solo si el conjunto de trabajo cabe y reduces el churn. Si la carga escala para llenar la memoria (caches, heaps, compactaciones),
puedes acabar con la misma presión, solo en un lienzo más grande.
6) ¿Cómo establezco límites de swap para contenedores Docker en cgroup v2?
Los flags de Docker pueden ser inconsistentes según la configuración. La forma fiable suele ser usar controles de cgroup v2 directamente vía scopes de systemd
o configuración de runtime, asegurando que memory.swap.max esté definido para el cgroup del contenedor. Valídalo leyendo el archivo en /sys/fs/cgroup.
7) ¿Por qué la “memoria libre” es baja incluso cuando el sistema está bien?
Linux usa RAM libre como cache. Esa cache es reclaimable. Mira la memoria “available” y las métricas de presión, no “free.”
Poco free + bajo PSI suele ser normal.
8) ¿Qué métricas deberían hacer que me despierten para detectar tormentas de swap temprano?
PSI memoria sostenida (/proc/pressure/memory), tasas de swap-in/out, faults de página mayores, await/%util de disco,
y eventos de memoria por cgroup (high/max/oom). Las alertas deben dispararse por condiciones sostenidas, no por picos de un segundo.
9) ¿Puede overlay2 o el comportamiento del sistema de ficheros contribuir a tormentas de swap?
Indirectamente, sí. Un churn intenso de metadata del sistema de ficheros y la amplificación de escritura pueden saturar IO, haciendo que swap-in/out sea mucho más doloroso.
No genera presión de memoria por sí mismo, pero convierte la presión de memoria en un outage a nivel sistema más rápido.
10) ¿Es zram mejor que swap en disco para contenedores?
zram evita IO de disco al comprimir en RAM. Puede suavizar tormentas si tienes margen de CPU, pero sigue siendo swapping—la latencia aumenta,
y puede ocultar problemas de capacidad. Úsalo como buffer, no como permiso para abusar de la memoria.
Pasos prácticos siguientes
Si tu host Docker se está derritiendo mientras los contenedores “funcionan”, deja de debatir si el swap es moralmente bueno. Trátalo como lo que es:
un instrumento de deuda de rendimiento con tasa de interés variable y un modelo de capitalización desagradable.
Haz esto a continuación, en este orden:
- Instrumenta la presión: añade alertas y dashboards basados en PSI junto con actividad de swap y latencia de IO.
- Presupuesta memoria por servicio: establece límites realistas; quita “ilimitado” como valor por defecto.
- Controla el comportamiento del swap: acota el swap por carga (especialmente las sensibles a latencia) y considera throttling con
memory.high. - Prefiere fallos controlados: permite OOMs por cgroup + reinicio para servicios que pueden recuperarse, en vez de thrash a nivel host.
- Aísla a los problemáticos: indexación por lotes, compactación y todo lo que tiende a inflarse no debería compartir nodos con APIs de baja latencia.
Tu objetivo no es “no usar nunca swap.” Tu objetivo es “nunca dejar que el swap decida la cronología de tus incidentes.” El kernel hará lo que le pidas.
Pide algo que sea soportable.