Ubuntu 24.04 OOM killer: comprobarlo, arreglarlo y evitar repeticiones

¿Te fue útil?

Tu servicio estaba sano. Luego dejó de estarlo. Un bucle de reinicios. Un hueco en las métricas. Alguien dice «¿quizá un deploy?»
Revisa la línea de tiempo y no encuentras nada—salvo la fría y eficiente mano del OOM killer.

En Ubuntu 24.04, puedes perder un proceso por o bien el OOM killer del kernel o por el gestor OOM en espacio de usuario de systemd.
Si no demuestras cuál lo hizo (y por qué), “arreglarás” la capa equivocada y el incidente volverá, puntualmente, normalmente a las 02:17.

Qué significa realmente “OOM” en Linux 2025

“Fuera de memoria” suena binario, como si el sistema se hubiera quedado sin RAM. En producción es más bien un problema de colas
que se convierte en pelea: llegan solicitudes de asignación, reclaim y compactación luchan, la lógica OOM del kernel o del espacio de usuario decide que el progreso requiere un sacrificio, y tu servicio se convierte en la víctima.

En Ubuntu 24.04 normalmente tratas con:

  • OOM killer del kernel (clásico): se dispara cuando el kernel no puede satisfacer asignaciones de memoria y el reclaim falla; selecciona una víctima según una puntuación de badness.
  • Aplicación de memoria en cgroup v2: los límites de memoria pueden desencadenar OOM dentro de un cgroup aunque el host tenga RAM libre; la víctima se elige dentro de ese cgroup.
  • systemd-oomd: demonio en espacio de usuario que mata procesos o slices de forma proactiva cuando PSI (pressure stall information) indica presión de memoria sostenida, a menudo antes de que el kernel llegue a un OOM duro.

Seis a diez hechos que te mejoran

  1. El OOM killer del kernel existía antes que los contenedores; fue creado para “un gran sistema” y luego se adaptó a cgroups donde “OOM en una caja” se volvió normal.
  2. PSI (pressure stall information) es relativamente nuevo en la historia de Linux y cambió las reglas: puedes medir “tiempo estancado esperando memoria”, no solo “bytes libres”.
  3. La historia del swap por defecto en Ubuntu ha cambiado con los años. Algunas flotas pasaron a swapfiles por defecto; otras deshabilitaron swap en hosts de contenedores, lo que hace los OOM más agudos y rápidos.
  4. El page cache también es memoria. “Libre” puede estar bajo mientras el sistema sigue sano, porque la cache es reclamable. El truco es saber cuándo el reclaim deja de funcionar.
  5. OOM no es solo por fugas. Puedes OOM por un pico legítimo de carga, una estampida de peticiones, o una consulta que construye una gran tabla en memoria.
  6. cgroup v2 cambió la semántica. Bajo la jerarquía unificada, memory.current, memory.max y PSI son de primera clase. Si aún razonas en términos de cgroup v1, te equivocarás en el diagnóstico.
  7. systemd-oomd puede matar “lo equivocado” desde tu punto de vista porque apunta a unidades/slices basadas en presión y políticas configuradas, no en impacto de negocio.
  8. Overcommit es política, no magia. Linux puede prometer más memoria virtual de la que existe; la factura llega después, a menudo en picos de tráfico.

Una frase para tener pegada al monitor:
La esperanza no es una estrategia. — idea parafraseada frecuente en operaciones y fiabilidad.
El punto es: necesitas evidencia y luego controles.

Chiste corto #1: El OOM killer es como la temporada de presupuestos—todos se sorprenden, y aún así escoge una víctima.

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

Cuando un servicio muere y sospechas OOM, quieres certeza rápida, no una excavación arqueológica de dos horas.
Aquí está el orden de triaje que ahorra tiempo.

Primero: confirma la kill e identifica al responsable

  • Busca líneas de kernel OOM en dmesg / journal: “Out of memory”, “Killed process”.
  • Busca acciones de systemd-oomd: “Killed … due to memory pressure” en el journal.
  • Comprueba si el servicio está en un cgroup con memory.max establecido: runtime de contenedores, límite de unidad systemd, QoS de Kubernetes, etc.

Segundo: confirma que fue presión de memoria, no un fallo disfrazado

  • Código de salida del servicio: 137 (SIGKILL) es común en muertes relacionadas con OOM (especialmente contenedores), pero no exclusivo.
  • Revisa contadores oom_kill en memory.events del cgroup.
  • Correlaciona con picos de presión PSI memory “some/full”.

Tercero: localiza la fuente de la presión

  • ¿Fue un proceso que creció? (crecimiento de RSS, fuga de heap, hilos descontrolados)
  • ¿Fueron muchos procesos? (fork bomb, estampida de peticiones, fanout de logs, sidecars)
  • ¿Fallo del reclaim? (páginas sucias, bloqueo IO, thrashing de swap, fragmentación de memoria)
  • ¿Fue un límite? (límite de contenedor demasiado bajo, MemoryMax de unidad, clase QoS equivocada)

Si solo haces una cosa: decide si esto fue kernel OOM, cgroup OOM o
systemd-oomd. La prevención cambia por completo.

Demostrarlo con logs: kernel vs systemd-oomd vs cgroup

Huellas del OOM killer del kernel

El OOM del kernel deja migas muy distintivas: contexto de asignación, el proceso víctima, el “oom_score_adj”,
y a menudo una lista de tareas con estadísticas de memoria. No necesitas adivinar; necesitas leer en el lugar correcto.

Huellas de systemd-oomd

systemd-oomd es más silencioso pero aún auditable. Registrará decisiones y la unidad objetivo. Actúa
sobre señales de presión sostenida y políticas configuradas, no sobre “falló una asignación”.

Huellas de OOM por límite de cgroup

Si estás en contenedores o servicios systemd con límites de memoria, puedes ser eliminado mientras el host está
bien. Tus gráficas de host muestran RAM libre. Tu servicio aún muere. Eso es una historia de cgroup.
La prueba está en memory.events y archivos relacionados.

Tareas prácticas (comandos + significado de la salida + decisiones)

Estas son las tareas que realmente ejecuto durante incidentes. Cada una incluye qué significa la salida y la decisión
que tomas a partir de ella. Ejecútalas como root o con sudo cuando haga falta.

Tarea 1: Confirmar que el servicio murió por SIGKILL (a menudo OOM)

cr0x@server:~$ systemctl status myservice --no-pager
● myservice.service - My Service
     Loaded: loaded (/etc/systemd/system/myservice.service; enabled; preset: enabled)
     Active: failed (Result: signal) since Mon 2025-12-29 10:41:02 UTC; 3min ago
   Main PID: 21477 (code=killed, signal=KILL)
     Memory: 0B
        CPU: 2min 11.203s

Significado: El proceso principal fue matado con SIGKILL. OOM es un sospechoso principal porque el kernel (y oomd)
suelen usar SIGKILL, pero un operador también pudo haber usado kill -9.

Decisión: Pasa a la evidencia del journal. No “arregles” nada todavía.

Tarea 2: Buscar en el journal líneas de kernel OOM alrededor del evento

cr0x@server:~$ journalctl -k --since "2025-12-29 10:35" --until "2025-12-29 10:45" | egrep -i "out of memory|oom|killed process|oom-killer"
Dec 29 10:41:01 server kernel: Out of memory: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Dec 29 10:41:01 server kernel: oom_reaper: reaped process 21477 (myservice), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Significado: Esto es una kill por OOM del kernel. Nombra a la víctima y proporciona estadísticas de memoria.

Decisión: Enfócate en por qué el kernel llegó a OOM: agotamiento de memoria global, swap deshabilitado, reclaim bloqueado o un pico.
systemd-oomd no es el culpable en este evento concreto.

Tarea 3: Si los logs del kernel están silenciosos, busca acciones de systemd-oomd

cr0x@server:~$ journalctl --since "2025-12-29 10:35" --until "2025-12-29 10:45" -u systemd-oomd --no-pager
Dec 29 10:40:58 server systemd-oomd[783]: Memory pressure high for /system.slice/myservice.service, killing 1 process(es) in this unit.
Dec 29 10:40:58 server systemd-oomd[783]: Killed /system.slice/myservice.service (myservice), pid=21477, uid=110, total_vm=3281440kB, rss=1513408kB

Significado: systemd-oomd actuó, apuntando a una unidad systemd porque la presión de memoria se mantuvo alta.

Decisión: Debes inspeccionar la configuración de oomd y la presión PSI, y considerar cambiar protecciones o políticas de la unidad.
Añadir RAM puede ayudar, pero la política aún puede matarte antes.

Tarea 4: Verificar si systemd-oomd está habilitado y activo

cr0x@server:~$ systemctl is-enabled systemd-oomd && systemctl is-active systemd-oomd
enabled
active

Significado: oomd está en juego en este host.

Decisión: Cuando veas SIGKILL sin logs de kernel OOM, trata a oomd como sospechoso de primera clase.

Tarea 5: Identificar la ruta de cgroup de tu servicio y comprobar si tiene un límite de memoria

cr0x@server:~$ systemctl show -p ControlGroup -p MemoryMax -p MemoryHigh myservice
ControlGroup=/system.slice/myservice.service
MemoryMax=infinity
MemoryHigh=infinity

Significado: Esta unidad no tiene un límite de memoria explícito en systemd. Si aún sufres OOM por cgroup, puede ser un límite heredado de un slice padre,
un límite de contenedor, o un límite en otro controlador.

Decisión: Inspecciona slices padres y los archivos de cgroup v2 directamente.

Tarea 6: Leer memory.events del cgroup para prueba contundente de OOM por cgroup

cr0x@server:~$ cgpath=$(systemctl show -p ControlGroup --value myservice); cat /sys/fs/cgroup${cgpath}/memory.events
low 0
high 12
max 3
oom 1
oom_kill 1

Significado: oom_kill 1 es el arma humeante: ocurrió una kill debido a la política de memoria del cgroup.
max 3 indica que el cgroup golpeó su límite duro varias veces; no siempre mató, pero chocó contra el techo.

Decisión: Corrige el límite o el comportamiento de memoria. No pierdas tiempo en gráficas de RAM del host; esto es local al cgroup.

Tarea 7: Inspeccionar uso actual vs límite a nivel de cgroup

cr0x@server:~$ cat /sys/fs/cgroup${cgpath}/memory.current; cat /sys/fs/cgroup${cgpath}/memory.max
1634328576
2147483648

Significado: El servicio está usando ~1.52 GiB con un tope de 2 GiB. Si ves kills con uso cercano al límite, tienes un problema real de margen.

Decisión: O subes el límite, o reduces la huella, o añades retropresión para que no se lance contra el muro.

Tarea 8: Comprobar PSI de memoria para ver si el host está estancándose

cr0x@server:~$ cat /proc/pressure/memory
some avg10=0.48 avg60=0.92 avg300=1.22 total=39203341
full avg10=0.09 avg60=0.20 avg300=0.18 total=8123402

Significado: PSI muestra que el sistema pasa tiempo medible estancado por memoria. “full” significa que tareas están completamente bloqueadas esperando memoria.
Si “full” es no trivial, no solo tienes poca RAM libre—estás perdiendo tiempo de trabajo.

Decisión: Si PSI es alto y sostenido, persigue soluciones sistémicas: comportamiento de reclaim, estrategia de swap, modelado de carga o más RAM.

Tarea 9: Confirmar el estado de swap y si estás operando sin la red de seguridad

cr0x@server:~$ swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file  8G  512M   -2

Significado: Existe swap y se está usando. Eso puede comprar tiempo y evitar OOM agudos, pero también puede ocultar fugas hasta que la latencia colapse.

Decisión: Si swap está desactivado en un host de propósito general, considera habilitar un swapfile moderado. Si swap está activo y muy usado,
investiga crecimiento de memoria y riesgos de bloqueo IO.

Tarea 10: Buscar los detalles de selección de víctima del kernel (badness, constraints)

cr0x@server:~$ journalctl -k --since "2025-12-29 10:40" --no-pager | egrep -i "oom_score_adj|constraint|MemAvailable|Killed process" | head -n 20
Dec 29 10:41:01 server kernel: myservice invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Dec 29 10:41:01 server kernel: Constraint: CONSTRAINT_NONE, nodemask=(null), cpuset=/, mems_allowed=0
Dec 29 10:41:01 server kernel: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0

Significado: CONSTRAINT_NONE sugiere que esto fue presión de memoria global (no restringida a un cpuset/mems).

Decisión: Mira el host entero: consumidores top de memoria, reclaim, swap e IO. Si esperabas un límite de cgroup, tu suposición es incorrecta.

Tarea 11: Identificar los principales consumidores de memoria en el momento (o ahora, si siguen presentes)

cr0x@server:~$ ps -eo pid,ppid,comm,rss,vsz,oom_score_adj --sort=-rss | head -n 12
  PID  PPID COMMAND           RSS    VSZ OOM_SCORE_ADJ
30102     1 java         4123456 7258120             0
21477     1 myservice    1512200 3281440             0
 9821     1 postgres      812344 1623340             0
 1350     1 prometheus    402112  912440             0

Significado: RSS es memoria residente real. VSZ es espacio de direcciones virtual (a menudo engañoso). Si algo más eclipsa a tu servicio,
la víctima pudo haber sido “desafortunada” más que “la más grande”.

Decisión: Decide si limitar o mover el proceso pesado, o proteger tu servicio vía oom_score_adj y políticas de unidad.

Tarea 12: Comprobar ajustes de overcommit de memoria (política que cambia cuándo ocurre OOM)

cr0x@server:~$ sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Significado: El modo de overcommit 0 es heurístico. Puede permitir asignaciones que más tarde disparen OOM bajo carga.

Decisión: Para algunas clases de sistemas (bases de datos, reservas de memoria críticas), considera una política más estricta (modo 2).
Para muchos servidores de aplicaciones, el modo 0 está bien; céntrate en límites y fugas primero.

Tarea 13: Comprobar problemas de reclaim/IO que hacen que la “memoria disponible” mienta

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 524288 112344  30240 8123448    0    4   120   320  912 2100 11  4 83  2  0
 3  1 524288  80320  30188 8051120    0   64   128  4096 1102 2600 18  6 62 14  0
 4  2 524288  60220  30152 7983340    0  256   140  8192 1200 2901 22  7 48 23  0
 2  1 524288  93210  30160 8041200    0   32   110  1024  980 2400 14  5 74  7  0
 1  0 524288 104220  30180 8100100    0    0   100   512  900 2200 10  4 84  2  0

Significado: Alto so (swap out) más alto wa (IO wait) pueden señalar thrashing de swap o contención de almacenamiento.
Eso puede empujar al sistema a estancamientos de memoria y luego a decisiones de OOM.

Decisión: Si hay picos de IO wait, trata el almacenamiento como parte del incidente de memoria. Arregla saturación de IO, comportamiento de páginas sucias,
y considera un respaldo de swap más rápido o una estrategia de swap distinta.

Tarea 14: Inspeccionar protecciones de la unidad systemd que influyen en las elecciones de kill

cr0x@server:~$ systemctl show myservice -p OOMScoreAdjust -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit
OOMScoreAdjust=0
ManagedOOMMemoryPressure=auto
ManagedOOMMemoryPressureLimit=0

Significado: Con ManagedOOMMemoryPressure=auto, systemd puede optar por gestionar según los valores por defecto y el comportamiento de la versión.

Decisión: Para servicios críticos, elige explícitamente: o protégelos (deshabilitar la gestión oomd para la unidad)
o aplica una política de slice que mate trabajo menos importante primero.

Tarea 15: Comprobar si tu servicio corre dentro de un contenedor con límites propios

cr0x@server:~$ cat /proc/21477/cgroup
0::/system.slice/myservice.service

Significado: Este ejemplo muestra un servicio systemd ejecutándose directamente en el host. En Docker/Kubernetes verías rutas que indican
scopes/slices de contenedor.

Decisión: Si la ruta apunta a scopes de contenedor, ve al cgroup del contenedor y lee memory.max y memory.events allí.

Tarea 16: Si tienes un servicio de negocio crítico, establece un ajuste deliberado de OOM score

cr0x@server:~$ sudo systemctl edit myservice
# (editor opens)
# Add:
# [Service]
# OOMScoreAdjust=-500
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice

Significado: Valores más bajos (más negativos) hacen que el kernel sea menos probable de elegir ese proceso como víctima OOM.
Esto no previene el OOM; cambia quién recibe el disparo.

Decisión: Usa esto solo cuando también tengas un plan sobre qué debe morir en su lugar (trabajos por lotes, caches, workers best-effort).
De lo contrario solo desplazas el radio de impacto.

Por qué sucedió: modos de fallo previsibles

1) La ilusión de “el host tiene memoria” (OOM por límite de cgroup)

El host puede tener gigabytes libres y tu servicio aún ser eliminado si su cgroup alcanza memory.max.
Esto es común en nodos Kubernetes, hosts Docker y servicios systemd donde alguien puso MemoryMax
meses atrás y lo olvidó.

La prueba no está en free -h. Está en memory.events para ese cgroup.

2) La política de “sin swap por rendimiento” (OOM global agudo)

Desactivar swap puede ser válido para algunos entornos sensibles a la latencia, pero cambia la degradación gradual por muerte súbita.
Si lo haces, debes tener límites estrictos de memoria, control de admisión y planificación de capacidad excelentes. Muchas organizaciones no tienen ninguno de esos.

3) Una fuga que solo se manifiesta bajo tráfico real

El clásico. Una caché sin límites. Una explosión de etiquetas en métricas. Una ruta de petición que retiene referencias.
Todo parece bien en staging porque staging no recibe la diversidad real de usuarios.

4) El reclaim no funciona porque el almacenamiento es el cuello de botella oculto

El reclaim de memoria a menudo depende de IO: escribir páginas sucias, intercambiar, leer de vuelta. Cuando el almacenamiento está saturado,
la presión de memoria se convierte en estancamientos y luego en kills.
Regla SRE: si incidentes de memoria se correlacionan con IO wait, no tienes “un problema de memoria.” Tienes un problema de sistema.

5) systemd-oomd está haciendo lo que le pediste (o lo que “auto” decidió)

oomd está diseñado para matar antes que el kernel, para evitar que todo el sistema se vuelva inutilizable.
Eso es bueno. También sorprende cuando mata una unidad que asumías protegida.
Si ejecutas hosts multi-inquilino, oomd puede ser tu amigo. Si ejecutas hosts de propósito único, puede ser ruido a menos que esté configurado.

Chiste corto #2: Desactivar el swap para “evitar latencia” es como quitar la alarma de incendios para “evitar ruido”.

Tres mini-historias corporativas (anonimizadas, plausibles, técnicamente precisas)

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

Una SaaS mediana ejecutaba una API de informes en un pool de hosts Ubuntu. El on-call vio una oleada de reinicios y hizo lo normal:
comprobó gráficas de memoria del host. Todo parecía bien—mucha RAM libre, sin uso de swap, sin humo. El equipo culpó a “crashes aleatorios” y revertió una actualización de biblioteca inocua.

Los reinicios continuaron. Un ingeniero senior finalmente sacó memory.events para la unidad systemd y encontró
oom_kill incrementando. El servicio tenía un MemoryMax heredado de un slice padre usado para “apps no críticas.”
Nadie recordaba haberlo puesto porque se hizo durante una racha de recorte de costes y vivía en un repositorio de infraestructura
que el equipo de la API nunca leía.

La suposición equivocada era simple: “Si el host tiene RAM libre, no puede ser OOM.” En un mundo de cgroups, esa suposición está muerta.
El host estaba bien; el servicio estaba encajonado.

La solución no fue dramática: subir el límite de memoria de la unidad para ajustarlo al uso pico real, añadir una alerta en memory.events del cgroup para high/max antes de oom_kill, y documentar la política del slice.
El postmortem no culpó al kernel. Culpó la falta de propiedad y las restricciones invisibles.

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

Una plataforma de pagos quería bajar p99. Alguien propuso desactivar swap en nodos de aplicación porque “swap es lento.”
También ajustaron la JVM para usar un heap mayor y reducir la frecuencia de GC. La primera semana fue estupenda—dashboards limpios y latencia de cola ligeramente mejor.

Luego llegó una campaña de tráfico. El uso de memoria subió mientras las caches se calentaban y la concurrencia aumentaba. Sin swap,
no había amortiguador para picos transitorios. El kernel alcanzó OOM rápido y mató workers Java al azar. Algunos nodos sobrevivieron,
otros no, creando cargas desiguales y tormentas de reintentos.

El equipo intentó arreglarlo aumentando aún más los heaps, lo que lo empeoró: el heap consumió cache de archivos y
redujo las opciones de reclaim. Cuando el sistema entró en problemas, tenía menos zonas donde refugiarse.

La solución final fue poco emocionante y muy eficaz: reactivar un swapfile moderado, reducir el heap a una fracción segura
de la RAM, y aplicar límites de memoria por worker vía slices systemd para que un único worker no consumiera el nodo entero.
Mantuvieron las mejoras de latencia reduciendo los picos de concurrencia en lugar de quitar la red de seguridad del sistema.

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

Un servicio de ingestión de datos procesaba archivos grandes de clientes. No era glamuroso: mayormente streaming IO, descompresión,
algo de parsing. El equipo de plataforma tenía una política estricta: cada unidad de servicio debe declarar expectativas de memoria con
MemoryHigh y MemoryMax, y debe emitir una métrica de “RSS actual”. Los equipos se quejaban de que era burocrático.

Una noche, un cliente nuevo envió un archivo mal formado que desencadenó un comportamiento patológico en una librería de parsing.
RSS creció de forma sostenida. El servicio no se cayó de inmediato; simplemente siguió pidiendo memoria. En un host sin límites,
habría arrastrado todo abajo.

En su lugar, MemoryHigh causó señales de throttling pronto (aumentó la presión de reclaim, el rendimiento degradó pero siguió funcional),
y MemoryMax evitó el agotamiento total del nodo. El worker de ingestión fue matado dentro de su propio cgroup,
sin llevarse por delante sidecars de base de datos o exporters del nodo.

El on-call vio la alerta: memory.events high subiendo. Pudo correlacionarlo con un job de un único cliente,
aislarlo y desplegar un fix del parser al día siguiente. La política aburrida convirtió un incidente de nodo en un job fallido.
Nadie celebró. Ese es el objetivo.

Prevención eficaz: límites, swap, ajustes y disciplina en tiempo de ejecución

Decide qué capa debe fallar primero

No previenes el agotamiento de memoria esperando que el kernel “lo gestione.” Lo previenes diseñando un orden de fallo:
qué cargas se aprietan, cuáles se matan y cuáles están protegidas.

  • Proteger: bases de datos, servicios de coordinación, agentes del nodo que mantienen el host manejable.
  • Best-effort: trabajos por lotes, caches que se pueden recomponer, workers asíncronos que pueden reintentar.
  • Nunca sin límites: cualquier cosa que pueda amplificarse con entrada de usuario (regex, descompresión, parseo JSON, capas de caching).

Usa límites systemd intencionalmente (MemoryHigh + MemoryMax)

MemoryMax es un muro duro. Útil, pero brutal: al alcanzarlo puedes ser matado. MemoryHigh es un umbral de presión:
dispara reclaim y throttling y te da la oportunidad de recuperarte antes de morir.

Un patrón pragmático para servicios de larga ejecución:

  • Fija MemoryHigh a un nivel donde la degradación de rendimiento sea aceptable pero las alertas sean visibles.
  • Fija MemoryMax lo suficientemente alto para permitir picos conocidos, pero lo bastante bajo para proteger el host.
cr0x@server:~$ sudo systemctl edit myservice
# Add:
# [Service]
# MemoryHigh=3G
# MemoryMax=4G
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice

Configura oomd en vez de fingir que no existe

Si systemd-oomd está habilitado, necesitas una política explícita. “auto” no es una política; es un valor por defecto que eventualmente
te sorprenderá en la peor hora posible.

Enfoques típicos:

  • Nodos multi-inquilino: mantener oomd, usar slices, poner cargas best-effort en un slice que oomd pueda matar primero.
  • Nodos de propósito único: considerar deshabilitar la gestión de oomd para la unidad crítica si está siendo matada prematuramente, pero mantener el OOM del kernel como último recurso.

Swap: elige una estrategia y hazte responsable

Swap no es “malo.” El swap no gestionado es malo. Si habilitas swap, monitoriza tasas de swap-in/out y IO wait. Si deshabilitas swap,
acepta que los OOM serán súbitos y frecuentes a menos que tengas límites estrictos y cargas predecibles.

Detén los picos de memoria en el límite de la aplicación

La forma más rápida de prevenir OOM es dejar de aceptar trabajo que se convierte en crecimiento de memoria sin límites.
Hábitos de producción:

  • Limitar caches (tamaño + TTL) y medir tasas de expulsión.
  • Limitar la concurrencia. La mayoría de servicios no necesitan “tantos hilos como sea posible”, necesitan “tantos como puedas mantener en cache y RAM”.
  • Limitar tamaños de payload y aplicar parseo en streaming.
  • Usar retropresión: colas con límites, shedding de carga, circuit breakers.

Conoce la diferencia entre RSS y “parece grande”

A los ingenieros les gusta culpar “fugas” mirando VSZ. Así es como terminas “arreglando” reservas mmap que nunca fueron residentes.
Usa RSS, PSS (si puedes) y cgroup memory.current. Y correlaciona con presión (PSI), no solo con bytes.

Nota para ingenieros de almacenamiento: los incidentes de memoria suelen ser incidentes de IO con otra máscara

Si tu sistema está reclamando, intercambiando o escribiendo páginas sucias bajo presión, la latencia de almacenamiento pasa a formar parte de la historia de memoria.
Un disco lento o saturado puede convertir “presión recuperable” en “matar algo ahora”.

Si ves alto IO wait durante presión, arregla la ruta de almacenamiento: profundidad de cola, volúmenes vecinos ruidosos, throttling, ajustes de writeback,
y el respaldo de swap.

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

1) Síntoma: “El servicio recibió SIGKILL, pero no hay OOM en dmesg”

Causa raíz: systemd-oomd lo mató por PSI, o la kill ocurrió dentro de un contenedor/cgroup y estás mirando el scope de journal equivocado.

Solución: Revisa journalctl -u systemd-oomd y memory.events de la unidad. Confirma la ruta del cgroup y lee los archivos correctos.

2) Síntoma: “El host muestra 30% de RAM libre, pero el contenedor sigue OOMeando”

Causa raíz: memory.max del cgroup es demasiado bajo (límite de contenedor), o los picos de memoria superan el margen.

Solución: Inspecciona memory.max y memory.events en el cgroup del contenedor. Sube el límite o reduce picos.

3) Síntoma: “Todo ralentiza durante minutos, luego un proceso muere”

Causa raíz: Reclaim y thrash de swap; IO wait hace intolerables los estancamientos de memoria.

Solución: Revisa vmstat por swap/wa. Reduce la presión de dirty writeback, mejora rendimiento de almacenamiento, considera una estrategia de swap sensata,
y limita a los peores ofensores.

4) Síntoma: “OOM mata a un proceso pequeño, no al que consume más”

Causa raíz: Puntuación de badness del OOM, diferencias en oom_score_adj, consideraciones de memoria compartida, o el hog está protegido por política.

Solución: Inspecciona logs por oom_score_adj; establece deliberadamente OOMScoreAdjust para servicios críticos y limita trabajo best-effort
con límites de cgroup para que el kernel tenga mejores opciones.

5) Síntoma: “OOM ocurre justo después del deploy, pero no siempre”

Causa raíz: Caches frías + mayor concurrencia + nuevas asignaciones. También común: un nuevo camino de código que asigna según entrada de usuario.

Solución: Añade tests canary que simulen cold start, limita caches, añade límites de tamaño de petición, y define MemoryHigh para exponer presión temprano.

6) Síntoma: “Añadimos RAM y el OOM sigue ocurriendo”

Causa raíz: El límite está en un cgroup, no en el host. O una fuga escala para llenar lo que compres.

Solución: Prueba dónde está el límite (memory.max). Luego mide la pendiente de crecimiento a lo largo del tiempo para confirmar fuga vs carga.

Listas de comprobación / plan paso a paso

Checklist de incidente (15 minutos, sin heroísmos)

  1. Obtén la hora exacta de la muerte y la señal: systemctl status o estado del runtime de contenedores.
  2. Revisa el log del kernel alrededor de esa hora por “Killed process”.
  3. Si el kernel está en silencio, revisa el journal de systemd-oomd.
  4. Lee memory.events del cgroup del servicio y de su slice padre.
  5. Captura PSI memoria: /proc/pressure/memory (host) y PSI de cgroup si procede.
  6. Captura los principales consumidores por RSS y sus oom_score_adj.
  7. Comprueba estado de swap y vmstat por interacción swap/IO wait.
  8. Anota: kernel OOM vs oomd vs OOM por límite de cgroup. No lo dejes ambiguo.

Plan de estabilización (mismo día)

  1. Si es OOM por cgroup: eleva MemoryMax (o límite de contenedor) para detener la hemorragia.
  2. Si es OOM global del kernel: añade swap si procede, reduce concurrencia y limita temporalmente cargas no críticas.
  3. Si es kill por oomd: ajusta la política ManagedOOM o mueve cargas best-effort a un slice matable.
  4. Configura alertas en memory.events “high” y PSI “full” para detectar presión antes de kills.
  5. Protege unidades críticas con OOMScoreAdjust, pero solo después de proveer una clase de víctimas segura.

Plan de prevención (esta semana)

  1. Define presupuestos de memoria por servicio: RSS en estado estable esperado y pico peor caso.
  2. Fija MemoryHigh y MemoryMax acorde; documenta la propiedad.
  3. Añade guardarraíles a nivel aplicación: límites de tamaño de petición, caches acotadas, topes de concurrencia.
  4. Instrumenta memoria: gauges de RSS, métricas de heap donde aplique y chequeos periódicos de fugas.
  5. Realiza un load test controlado que simule cold start + pico de concurrencia.
  6. Revisa swap y ruta de IO: asegúrate de que el reclaim tiene dónde ir sin destrozar el almacenamiento.

Preguntas frecuentes

1) ¿Cómo distingo rápidamente kernel OOM de systemd-oomd?

Kernel OOM muestra “Out of memory” y “Killed process” en journalctl -k. systemd-oomd muestra decisiones de kill en
journalctl -u systemd-oomd. Si ambos están silenciosos, revisa memory.events del cgroup por oom_kill.

2) ¿Qué significa el código de salida 137?

Normalmente significa que el proceso recibió SIGKILL (128 + 9). OOM es una razón común, especialmente en contenedores, pero un operador o un watchdog también puede SIGKILL.
Siempre corrobora con logs y eventos de cgroup.

3) ¿Por qué el OOM killer eligió mi servicio importante?

El kernel elige según la puntuación de badness y constraints. Si todo es crítico y está sin límites, algo crítico morirá.
Usa OOMScoreAdjust para proteger unidades clave, pero también crea clases matables (batch, cache) con límites.

4) ¿Puedo simplemente desactivar systemd-oomd?

Puedes, pero no lo hagas como reflejo. En nodos multi-inquilino, oomd puede prevenir el colapso total del host actuando temprano.
Si lo desactivas, ten confianza en que tus límites de cgroup y controles de carga evitan presión global.

5) ¿Cuál es la diferencia entre MemoryHigh y MemoryMax?

MemoryHigh aplica presión de reclaim y throttling al ser excedido—una advertencia temprana y control blando.
MemoryMax es un límite duro; excederlo puede desencadenar kills OOM dentro del cgroup.

6) ¿Por qué la memoria “free” parece baja aun cuando el sistema está bien?

Linux usa RAM para page cache. “Free” baja no es inherentemente mala. Mira la memoria “available” y, mejor, la presión PSI para saber si el sistema se está estancando.

7) ¿Siempre se recomienda swap en servidores Ubuntu 24.04?

No siempre. Swap puede reducir la frecuencia de kills OOM duros, pero puede aumentar la latencia bajo presión.
Para hosts de propósito general, un swapfile moderado suele ser un balance positivo. Para cargas de latencia estricta, puedes desactivar swap—
pero entonces debes imponer límites estrictos y control de admisión.

8) ¿Cómo pruebo un OOM de contenedor vs un OOM del host?

Revisa el cgroup del contenedor: memory.events con oom_kill indica OOM por cgroup.
Un OOM del host mostrará entradas del kernel “Killed process” y a menudo afectará múltiples servicios.

9) ¿Cuál es la mejor métrica de aviso temprano?

PSI de memoria (/proc/pressure/memory) más la tendencia de memory.events high del cgroup. Los bytes te dicen “cuánto.”
La presión te dice “qué tan mal.”

Siguientes pasos (qué hacer antes de la próxima página)

No trates el OOM como si fuera el tiempo. Es ingeniería. La victoria inmediata es demostrar el asesino: kernel OOM, cgroup OOM o systemd-oomd.
Una vez que lo sepas, las soluciones dejan de ser superstición.

Haz estas tres cosas esta semana:

  1. Añade una alerta en memory.events (high y oom_kill) para tus servicios principales.
  2. Establece presupuestos explícitos MemoryHigh/MemoryMax para servicios importantes, y pon trabajo best-effort en un slice matable.
  3. Decide tu estrategia de swap y monitorízala—porque “una vez desactivamos swap” no es un plan, es un rumor.

La próxima vez que un servicio desaparezca, deberías poder responder “¿qué lo mató?” en menos de cinco minutos.
Luego puedes dedicar el resto de tu tiempo a prevenir la repetición, en vez de discutir con una captura de pantalla del dashboard.

← Anterior
Errores de lectura en ZFS: ¿disco, cable o controlador?
Siguiente →
Desastres con metal líquido: la mejora que acaba en factura de reparación

Deja un comentario