Cómo diagnosticar y corregir fugas de memoria en PHP-FPM en servidores Linux
Por Kevin Marcondes · Publicado el · Actualizado el · 8 min de lectura
Si los workers de PHP-FPM crecen sin control hasta que actúa el OOM Killer, es posible que haya una fuga de memoria en PHP-FPM o fragmentación/OpCache. Aquí verás cómo confirmar la causa en producción, reproducirla con seguridad en staging y aplicar ajustes y mitigaciones que estabilicen el entorno mientras se implementa la corrección definitiva.
El problema: OOM Killer, caídas y 502/503
Las fugas de memoria en PHP-FPM suelen manifestarse como un aumento continuo del RSS por worker, mucho swap, latencia y errores 502/503 bajo carga. Antes que nada, identifica el patrón del incidente y aísla si se debe a exceso de concurrencia (dimensionamiento) o a retención/fragmentación de memoria por proceso.
Usa las señales abajo como pistas iniciales para decidir el siguiente paso del diagnóstico.
| Síntoma | Hipótesis probable | Próximo paso |
|---|---|---|
| OOM Killer mata php-fpm | pm.max_children alto o leak por worker | Medir RSS por worker; ajustar pm y memory_limit; activar rotación (pm.max_requests) |
| Crecimiento continuo de RSS de los workers | Fragmentación/OpCache o objetos no liberados/extensión en C | Inspeccionar /proc/PID/smaps_rollup y pmap -x; observar Private_Dirty |
| Swap alta y latencia | Falta de RAM efectiva o dimensionamiento del pool | Revisar max_children; considerar reducir concurrencia/optimizar endpoints críticos |
| Errores 502/503 en picos | Cola del FPM agotada/timeout por saturación | Activar status_path; medir cola; correlacionar con rutas más pesadas |
Causas probables de fuga de memoria en PHP-FPM
En PHP “puro”, el recolector libera la mayor parte de los objetos al final de cada petición. Un crecimiento anormal de la memoria por worker suele venir de:
No todo crecimiento es una fuga: peticiones que procesan archivos grandes, imágenes o arrays enormes pueden elevar picos de memoria sin retención. Diferencia pico (VmHWM alto) de retención (VmRSS sube y no vuelve).
- Extensiones en C (imagen, XML, criptografía) que no liberan buffers en ciertos caminos de código.
- Fragmentación del heap y del OpCache: muchos archivos/clases generan fragmentación y retención, incluso sin “fugar” lógicamente.
- Caches estáticos/singletons que crecen según el catálogo/rutas accedidas.
- Ciclos de referencia y objetos ligados a recursos externos que el GC no recolecta bien.
- Workers demasiado longevos sin reciclado (pm.max_requests muy alto o 0).
Diagnóstico rápido en producción con ps, top y /proc
Objetivo: confirmar si el problema afecta a pocos workers o a todo el pool, y si hay retención entre peticiones. Enfócate en métricas de bajo costo y sin downtime. Evita cambios invasivos durante horario laboral.
Correlaciona picos con rutas/tenants usando logs estructurados. Si aún no los tienes, consulta la guía de registros estructurados y correlación.
# Visão rápida de consumo de memória
free -h && swapon --summary
# Quais processos consomem mais RSS (memória residente)
ps -o pid,ppid,cmd,%mem,rss --sort=-rss -C php-fpm | head
# Inspecione um worker específico ($PID)
PID=<um_pid_de_worker>
cat /proc/$PID/status | egrep 'VmRSS|VmHWM|Threads'
# Resumo de mapas de memória do processo
cat /proc/$PID/smaps_rollup | egrep 'Rss|Pss|Private'
Mapeando asignaciones: pmap y análisis de smaps
Si un worker crece después de cada petición, verifica dónde está la memoria (anónima vs mapeada, privada vs compartida). Busca grandes áreas Private_Dirty (heap) y crecimiento continuo del anon RSS.
Combina pmap (resumen) con smaps_rollup (total por tipo). Haz esto en un único worker con baja carga.
PID=<um_pid_de_worker>
# Top 20 regiões por RSS no processo
pmap -x $PID | sort -k3 -n | tail -n 20
# Resumo consolidado (verifique Private_* e Anonymous)
cat /proc/$PID/smaps_rollup | egrep 'Anonymous|Shared|Private|Rss|Pss'
Observando llamadas al sistema de memoria con strace (mmap/brk)
Cuando el RSS crece y quieres confirmar actividad de asignación, observa syscalls de memoria. strace añade overhead — úsalo en un único worker y por tiempo corto, preferiblemente fuera de pico.
PID=<um_pid_de_worker>
# Observar syscalls de memória (mmap/brk/munmap/mremap)
strace -p $PID -e trace=brk,mmap,munmap,mremap -f -tt -o /tmp/trace.$PID.log &
# Gere algumas requisições representativas... depois
sleep 10; kill %+
# Consulte o log em busca de alocações que não recebem munmap correspondente
awk '/mmap\\(/ {m++} /munmap\\(/ {u++} END {print "mmap:",m, "munmap:",u}' /tmp/trace.$PID.log
Reproducción en staging con Valgrind y Xdebug
Idealmente, reproduce el caso fuera de producción con un script que invoque el endpoint problemático muchas veces. Valgrind Massif identifica picos y crecimiento del heap. Xdebug en modo trace muestra picos por función. No ejecutes Valgrind en tus workers de producción.
# 1) Script reprodutor simples (CLI)
cat > repro.php <<'PHP'
<?php
function hit() {
// Simule chamada ao código/endpoint alvo
$data = file_get_contents('http://localhost/rota-pesada');
return strlen($data);
}
for ($i = 0; $i < 500; $i++) {
hit();
if ($i % 10 === 0) {
fprintf(STDERR, "it=%d rss=%d peak=%d\n", $i, memory_get_usage(), memory_get_peak_usage());
}
}
PHP
# 2) Heap profiling com Valgrind Massif
valgrind --tool=massif --time-unit=B --massif-out-file=massif.out \
php -d detect_leaks=1 repro.php
ms_print massif.out | head -n 100
# 3) Trace de memória por função com Xdebug (em CLI)
php -d xdebug.mode=trace -d xdebug.start_with_request=yes \
-d xdebug.output_dir=. repro.php
ls -1 cachegrind* trace*
Tuning y mitigación inmediata en PHP-FPM y PHP
Mitiga el riesgo de OOM mientras investigas: recicla workers, limita picos y ajusta el tamaño del pool. Documenta los cambios y mide el impacto.
Dimensionamiento práctico: max_children ≈ (RAM disponible para FPM) / (RSS medio por worker bajo carga). Mide el RSS después del calentamiento y con tráfico representativo.
Revisa también el OpCache: la fragmentación puede amplificar el uso de memoria. Consulta parámetros comunes abajo y la documentación de PHP-FPM (https://www.php.net/manual/en/install.fpm.configuration.php), de memory_limit (https://www.php.net/manual/en/ini.core.php#ini.memory-limit) y de OpCache (https://www.php.net/manual/en/opcache.configuration.php).
- Activa reciclado: pm.max_requests entre algunas centenas y pocos miles, según carga y latencia aceptable.
- Aprieta memory_limit en el pool (php_admin_value) para contener outliers; ajusta por ruta/job pesado.
- Si el backlog del FPM crece, reduce max_children y aumenta capacidad por request (optimiza código) en vez de solo aumentar concurrencia.
- Monitorea cola y latencia tras cada cambio; si hay regresión, revisa la configuración.
; /etc/php-fpm.d/www.conf (exemplos)
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 1000
pm.process_idle_timeout = 10s
request_terminate_timeout = 60s
php_admin_value[memory_limit] = 256M
; php.ini / opcache
opcache.enable=1
opcache.validate_timestamps=1
opcache.memory_consumption=256
opcache.max_accelerated_files=100000
opcache.interned_strings_buffer=16
opcache.jit_buffer_size=0 ; se JIT causar pressão de memória
Buenas prácticas y monitoreo continuo
- Registra memory_get_peak_usage por ruta y correlaciónalo con request-id. Consulta la guía de registros estructurados y correlación para ejemplos.
- Supervisa RSS/Private_Dirty por worker y crea alertas antes del OOM.
- Adopta APM para detectar endpoints que elevan asignaciones; compara soluciones en el comparativo de observabilidad para PHP.
- Añade tests de regresión de memoria en el pipeline (CLI que ejecute el caso N iteraciones). Basa esto en guías de CI/CD con PHP.
- En legados, aísla los “hot paths” con tests; busca ideas en guías sobre tests en PHP legado.
Plan de acción sugerido en 7 pasos
1) Confirma impacto: RSS por worker, swap y cola del FPM. 2) Identifica si el crecimiento es global o por ruta. 3) Recopila smaps_rollup/pmap de un worker objetivo. 4) Activa pm.max_requests para estabilizar. 5) Reproduce en staging y ejecuta Valgrind/Xdebug. 6) Aplica correcciones en código/extensión/configuración. 7) Monitorea después del cambio por varios ciclos de tráfico.
Preguntas frecuentes
¿Cómo diferenciar una fuga real de un pico legítimo de memoria?
Una fuga real aparece como un crecimiento del VmRSS que no vuelve al nivel base tras la petición, ciclo tras ciclo. Los picos legítimos elevan el VmHWM (máximo histórico), pero el VmRSS cae al finalizar. Compara smaps_rollup, recoge métricas entre peticiones y observa si Private_Dirty crece de forma monótona.
¿pm.max_requests resuelve la fuga?
No resuelve la causa, pero reciclar workers limita la vida de un proceso “vaciado” o con fragmentación, reduciendo el riesgo de OOM. Elige un valor que equilibre estabilidad y latencia de cold start. Monitorea tras el ajuste para evitar fluctuaciones en el tiempo de respuesta.
¿OpCache puede causar aumento de memoria?
Sí. La fragmentación y el crecimiento del cache de scripts y cadenas internadas elevan el RSS, especialmente en aplicaciones con muchos archivos/clases. Ajusta opcache.memory_consumption y max_accelerated_files, y recicla workers periódicamente. Analiza también si despliegues frecuentes sin reinicio del FPM mantienen restos en la cache.
¿Es seguro usar strace y valgrind en producción?
strace añade overhead; úsalo solo en un worker y por tiempo corto, fuera de picos. Valgrind no está indicado en producción: su overhead es grande y puede afectar el servicio. Reproduce en staging con tráfico sintético para obtener perfiles del heap y trazas de funciones.
¿Cómo estimar pm.max_children con poca RAM disponible?
Mide el RSS medio por worker bajo una carga representativa (tras el calentamiento). Resta del total la RAM del sistema y de otros servicios. Divide la RAM restante por el RSS medio y aplica un margen de seguridad. Si el resultado es bajo, prioriza optimizaciones por petición en lugar de aumentar concurrencia.
¿Quieres estabilizar PHP-FPM y evitar OOM?
Hablemos de un plan de diagnóstico y mitigación seguro para tu entorno: identificar la causa raíz, ajustar FPM y monitorear sin interrumpir el servicio.
Hablar sobre diagnóstico PHP-FPMVer también
- Cómo recuperar estadísticas e índices corruptos en PostgreSQL que dejaron consultas lentas
- API REST con picos de latencia por queries N+1 y GC en PHP — cómo diagnosticar y corregir
- Error 'Too many connections' en PostgreSQL durante pico de facturación — guía práctica de diagnóstico y corrección
- Reducir costos migrando Magento a OpenMage con plan de rollback y pruebas de aceptación