Respuesta al OOM Killer
Cuando Kubernetes marca tu pod de Node.js como OOMKilled, tienes minutos para decidir: fuga, pico de tráfico o límites mal configurados. Esta página clasifica el heap vs RSS y elige la mitigación correcta.
Busca en todas las páginas de la documentación
Cuando Kubernetes marca tu pod de Node.js como OOMKilled, tienes minutos para decidir: fuga, pico de tráfico o límites mal configurados. Esta página clasifica el heap vs RSS y elige la mitigación correcta.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
# 1. Confirma OOMKilled (no CrashLoop por otras razones)
kubectl describe pod <pod> -n production | grep -E "OOMKilled|Reason|Exit Code"
# 2. Verifica las métricas de memoria: heap vs RSS
# Grafana: process_resident_memory_bytes vs v8 heap used
# 3. Correlaciona con el despliegue y el tráfico
kubectl rollout history deployment/<svc>
# Compara el pico de RPS con la pendiente de la memoria
# 4. Mitiga
kubectl scale deployment/<svc> --replicas=<N+2> # pico de tráfico
kubectl rollout undo deployment/<svc> # despliegue defectuoso
# fuga: estrangula el tráfico, reinicio gradual, programa instantánea del heapCuándo usar esto:
OOMKilled y Exit Code 137heapUsed se mantiene plano (buffers nativos, gRPC, sharp)// src/observability/memory-metrics.ts
import { monitorEventLoopDelay } from "node:perf_hooks";
import v8 from "node:v8";
const loopDelay = monitorEventLoopDelay({ resolution: 20 });
loopDelay.enable();
export function startMemoryReporter(log: (obj: object) => void, intervalMs = 30_000) {
setInterval(() => {
const mem = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
log({
msg: "memory_sample",
rssMb: Math.round(mem.rss / 1024 / 1024),
heapUsedMb: Math.round(mem.heapUsed / 1024 / 1024),
heapTotalMb: Math.round(mem.heapTotal / 1024 / 1024),
externalMb: Math.round(mem.external / 1024 / 1024),
arrayBuffersMb: Math.round(mem.arrayBuffers / 1024 / 1024),
heapLimitMb: Math.round(heapStats.heap_size_limit / 1024 / 1024),
eventLoopP99Ms: Math.round(loopDelay.percentile(99) / 1e6),
});
loopDelay.reset();
}, intervalMs).unref();
}Lo que esto demuestra:
rss vs heapUsed señala el crecimiento nativo/bufferexternal y arrayBuffers rastrean la presión de Buffer y TypedArray| Métrica | Qué mide | Crece cuando |
|---|---|---|
heapUsed | Objetos JavaScript de V8 | Cierres con fugas, Maps en crecimiento, objetos en caché |
rss | Memoria residente total del proceso | Heap + complementos nativos + pilas de hilos + mmap |
external | Objetos C++ vinculados a JS | Buffers, criptografía nativa, algunos controladores ORM |
arrayBuffers | Memoria de ArrayBuffer desvinculada | Cargas binarias, subidas de archivos |
| Señal | Probable fuga | Probable tráfico/capacidad |
|---|---|---|
| Tráfico | RPS plano | RPS sube 2-10x |
| Pendiente de memoria | Lineal durante horas | Cambio brusco en el pico |
| Reinicios | Mismo pod, mismo patrón | Todos los pods, carga proporcional |
heapUsed | Sigue a RSS hacia arriba | Puede mantenerse moderado |
| Solución | Corrección de código + instantánea | Escalar HPA, ajustar pool, aumentar límites |
¿OOMKilled?
├─ ¿Despliegue en los últimos 60 min? → revertir primero
├─ ¿Pico de RPS? → escalar réplicas, verificar tamaños de pool
├─ ¿heapUsed ≈ RSS subiendo? → búsqueda de fugas (instantánea, clinic heapprofiler)
├─ ¿RSS >> heapUsed? → verificar Buffers, sharp, gRPC, motor Prisma
└─ ¿Límites demasiado ajustados? → aumentar la solicitud/límite de memoria con evidenciaresources:
requests:
memory: "512Mi"
limits:
memory: "768Mi" # margen por encima del RSS en estado estableNODE_OPTIONS=--max-old-space-size=512 por debajo del límite de k8s (deja ~30% para no-heap)heapdump detiene el bucle de eventos. Solución: Copia el pod, drena el tráfico, haz una instantánea en el clon.external - Los detectores de fugas solo observan el heap. Solución: Registra arrayBuffers y external cada 30 segundos.Map con clave por userId crece indefinidamente. Solución: LRU con entradas máximas o Redis.| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
clinic heapprofiler | Fuga reproducible en staging | Producción bajo carga de clientes |
clinic doctor | Bucle de eventos + memoria combinados | Ya sabes que es OOM |
| Solo aumentar límites | Pico de capacidad probado | RSS sube bajo tráfico plano |
| Cambiar a worker threads | Bucle de bloqueo ligado a la CPU | API ligada a E/S (no solucionará la fuga) |
137 (128 + 9 SIGKILL). Los eventos de Kubernetes muestran OOMKilled como la razón.
Usa kill -USR2 <pid> si está conectado a heapdump, o ejecuta en un pod drenado. Nunca tomes una instantánea de la única réplica bajo tráfico completo.
Causan agotamiento y tiempos de espera antes de OOM, pero el crecimiento del pool inactivo aumenta el RSS. Verifica totalCount vs max del pool de pg.
Solo en diagnósticos controlados. Forzar GC oculta las señales de fuga en las métricas.
Versiones de stack: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.
Revisado por Chris St. John·Última actualización: 16 jul 2026