🧪 Problem-Driven Systems Lab

🧠 Caso 05 — Presión de memoria y fugas de recursos#

Estado Stacks Categoría

[!IMPORTANT] 📖 Ver Análisis Técnico Senior de esta solución (PHP)

Este documento es un resumen ejecutivo. La evidencia de ingeniería, los algoritmos y la remediación profunda viven en el link de arriba y en comparison.md.


🔍 Qué problema representa#

El sistema consume memoria, descriptores o conexiones de forma progresiva hasta degradar o caerse. La señal típica: en dev funciona perfecto; en producción, tras unas horas, los reinicios automáticos empiezan a aparecer "sin razón aparente".

Las fugas no fallan al instante — acumulan estado, presionan al GC (o al asignador), generan jitter de latencia, y eventualmente disparan OOM kills o reinicios "preventivos". Son incidentes caros porque son difíciles de reproducir y casi imposibles de detectar sin telemetría continua.


⚠️ Síntomas típicos#

  • Uso de memoria creciente que nunca vuelve a línea base tras requests
  • OOM kills, reinicios o garbage collection excesiva
  • Conexiones, sockets o file descriptors que no se liberan
  • Procesos estables localmente pero inestables bajo carga real prolongada (soak)
  • Latencia que se degrada con el tiempo, no con la carga puntual

🧩 Causas frecuentes#

  • Colecciones o caches sin límites (un Map/Array/List que crece para siempre)
  • Listeners, closures o referencias retenidas (cierra los datos por captura léxica)
  • Streams, conexiones o archivos sin cierre correcto en path de error
  • Buffers grandes o serialización costosa que se mantiene viva más de lo necesario

🔬 Estrategia de diagnóstico#

  • Tomar snapshots de memoria o heap antes y después de operaciones repetidas
  • Medir crecimiento por operación: si crece linealmente con N requests, hay leak real
  • Auditar recursos abiertos y lifecycle de objetos (¿quién retiene esa referencia?)
  • Comparar comportamiento en pruebas prolongadas (soak) — no en una sola corrida

💡 Opciones de solución#

  • Liberar recursos explícitamente cuando corresponda (close, dispose, unset, try-with-resources)
  • Acotar cachés y buffers con políticas de eviction (LRU, TTL, cap fijo)
  • Revisar ciclos de vida de objetos persistentes — singletons, listeners globales, módulos cargados
  • Soak testing en CI: correr durante horas para detectar lo que un test corto no ve

🏗️ Implementación actual#

✅ PHP 8 (memory_get_usage + retención explícita)#

batch-legacy acumula estado entre requests en un módulo estático; batch-optimized aplica cache acotada con eviction. memory_get_usage() y métricas locales dejan visible el retained_kb y el pressure_level en el tiempo. Ver php/README.md. Modo aislado: puerto 815.

Python 3.12 (tracemalloc + gc.collect())#

Usa tracemalloc.start() para diff snapshots, sys.getsizeof() para medir objetos individuales, gc.collect() para liberar ciclos. Cache acotada con collections.OrderedDict + popitem(last=False) como LRU. Ver python/README.md. Modo aislado: puerto 835. Hub: http://localhost:8200/05/.

Node.js 22 (heap V8 + RSS + external)#

process.memoryUsage() expone los 4 números clave: heapUsed, heapTotal, rss, external. La fuga real es un array de módulo que retiene buffers cross-request. La versión optimizada usa un Map acotado con eviction explícita. Ver node/README.md. Modo aislado: puerto 825. Hub: http://localhost:8300/05/.

Java 21 (LinkedHashMap LRU + Runtime metrics)#

LinkedHashMap con removeEldestEntry es LRU built-in del JDK — una línea agrega política de eviction. Runtime.getRuntime().totalMemory()/freeMemory()/maxMemory() mide el heap del JVM directo, sin agente. El leak legacy es una static List<byte[]> retenida por una raíz GC: el GC corre, no encuentra nada que liberar, el heap crece monótonamente hasta -Xmx. Ver java/README.md. Modo aislado: puerto 845. Hub: http://localhost:8400/05/.

.NET 8 (LRU manual + Process.WorkingSet64 + GC metrics)#

LRU acotada compuesta a mano con Dictionary<K, LinkedListNode<...>> + LinkedList<...> — O(1) en lookup y reordenamiento. Process.GetCurrentProcess().WorkingSet64 mide el RSS visto por el OS; GC.GetTotalMemory() mide lo que el CLR considera vivo; GC.CollectionCount(2) revela presion de Gen2 (incluido LOH si los payloads superan 85 KB). El leak legacy es una static List<byte[]> retenida por una raiz GC: el GC corre, no encuentra nada que liberar, WorkingSet64 crece monotonicamente. Ver dotnet/README.md. Modo aislado: puerto 855. Hub: http://localhost:8500/05/.


⚖️ Trade-offs#

  • Más liberación manual puede aumentar la complejidad y el riesgo de double-free (en lenguajes con GC, peor: liberar muy agresivo invalida caches útiles).
  • Pools y cachés mal configurados también degradan rendimiento (un cap muy bajo = misses costosos).
  • Reducir buffers puede subir uso de CPU o I/O — el ahorro de RAM no es gratis.
  • "Leak en lenguaje con GC" es trampa semántica: el GC funciona, pero las referencias siguen alcanzables desde una raíz. Hay que entender el modelo, no culpar al runtime.

💼 Valor de negocio#

Disminuye incidentes silenciosos, reinicios inesperados y consumo innecesario de infraestructura. Permite dimensionar correctamente (saber cuánta RAM realmente necesita un servicio, no inflar "por si acaso"). Reduce páginas operacionales por OOM y mejora la confianza en deploys largos sin restart programado.


🛠️ Stacks disponibles#

StackEstado
🐘 PHP 8OPERATIVO (memory_get_usage + retención explícita en módulo)
🐍 Python 3.12OPERATIVO (tracemalloc + gc.collect() + cache acotado)
🟢 Node.js 22OPERATIVO (process.memoryUsage() con heap V8 + RSS + external)
☕ Java 21OPERATIVO (LinkedHashMap.removeEldestEntry LRU built-in + Runtime metrics)
🔵 .NET 8OPERATIVO (LRU manual Dictionary+LinkedList + Process.WorkingSet64 + GC.GetTotalMemory)

🚀 Cómo levantar#

Modo hub (recomendado):

bash
docker compose -f compose.root.yml    up -d --build && curl http://localhost:8100/05/health   # PHP
docker compose -f compose.python.yml  up -d --build && curl http://localhost:8200/05/health   # Python
docker compose -f compose.nodejs.yml  up -d --build && curl http://localhost:8300/05/health   # Node
docker compose -f compose.java.yml    up -d --build && curl http://localhost:8400/05/health   # Java
docker compose -f compose.dotnet.yml  up -d --build && curl http://localhost:8500/05/health   # .NET

Modo aislado (recomendado para este caso — medir memoria sin contaminación):

bash
docker compose -f cases/05-memory-pressure-and-resource-leaks/php/compose.yml    up -d --build  # :815
docker compose -f cases/05-memory-pressure-and-resource-leaks/python/compose.yml up -d --build  # :835
docker compose -f cases/05-memory-pressure-and-resource-leaks/node/compose.yml   up -d --build  # :825
docker compose -f cases/05-memory-pressure-and-resource-leaks/java/compose.yml   up -d --build  # :845

Generar presión y observar la diferencia (ejemplo Java):

bash
# Legacy: 50 requests con 128 KB cada uno → heap crece monotonicamente
for i in {1..50}; do curl -s "http://localhost:8400/05/batch-legacy?size_kb=128" > /dev/null; done
curl http://localhost:8400/05/state   # heap_used_mb sube, retained_count alto

# Optimized: 5000 requests con cap=1000 → heap estable, eviction visible
for i in {1..5000}; do curl -s "http://localhost:8400/05/batch-optimized?size_kb=64" > /dev/null; done
curl http://localhost:8400/05/state   # heap estable, evictions_total > 4000

📚 Lectura recomendada#

DocumentoQué cubre
comparison.mdComparativa multi-stack (PHP · Python · Node.js · Java) con snippets y diferencias del modelo de memoria por runtime
docs/context.mdPor qué las fugas se ven tarde en producción
docs/symptoms.mdCómo se ve un leak antes del OOM kill
docs/diagnosis.mdEstrategia de snapshots y diff
docs/root-causes.mdLas 4 causas más frecuentes
docs/solution-options.mdOpciones ordenadas por riesgo
docs/trade-offs.mdLo que cuesta cada estrategia de mitigación
docs/business-value.mdImpacto operacional y de costos cloud

📁 Estructura del caso#

05-memory-pressure-and-resource-leaks/
├── README.md                    ← este archivo
├── comparison.md                ← comparativa multi-stack PHP · Python · Node · Java
├── compose.compare.yml          ← levanta los 7 stacks juntos
├── docs/                        ← análisis problem-driven (8 documentos)
├── shared/                      ← assets compartidos
├── 🐘 php/                      ← `OPERATIVO` — memory_get_usage + retención
├── 🐍 python/                   ← `OPERATIVO` — tracemalloc + cache acotada
├── 🟢 node/                     ← `OPERATIVO` — process.memoryUsage heap V8
├── ☕ java/                     ← `OPERATIVO` — LinkedHashMap LRU + Runtime
├── 🔵 dotnet/                   ← `OPERATIVO` — LRU manual Dictionary+LinkedList + Process.WorkingSet64
├── 🐹 go/                   ← `OPERATIVO` — container/list LRU + runtime.ReadMemStats
└── 🦀 rust/                   ← `OPERATIVO` — sin GC: impl Drop que cuenta liberaciones

Ver esta carpeta en GitHub ↗