🎨 Sysprovider Code
Sysprovider LogoWiki
🇪🇸Hosting español para ecommerce

Optimización de PHP-FPM con opcache y ajuste de procesos dinámicos

Actualizado el 10 de junio de 2026

Introducción

PHP-FPM (FastCGI Process Manager) es el motor de ejecución de PHP más utilizado en entornos de producción, especialmente con servidores web como Nginx. Sin embargo, una configuración deficiente puede convertir un servidor potente en un cuello de botella. La optimización de PHP-FPM se centra en dos pilares fundamentales: el caché de opcode (Opcache) para eliminar la sobrecarga de compilación en cada petición, y el ajuste de procesos dinámicos para adaptar los recursos del servidor a la carga variable de tráfico.

Este artículo técnico detalla, paso a paso, cómo tunear ambos componentes para maximizar el rendimiento, reducir la latencia y evitar errores 502/504 en servidores de alto tráfico. Se asume un entorno Linux (Debian/Ubuntu/CentOS) con PHP 7.4+ y Nginx.

Fundamentos de Opcache: Más Allá del Caché de Opcode

Opcache almacena en memoria compartida el código PHP compilado (opcodes). Sin él, cada petición fuerza a PHP a leer, analizar y compilar el script desde cero. En aplicaciones como WordPress o Laravel, con cientos de archivos, esto representa un desperdicio masivo de CPU.

¿Por qué no usar solo APC o XCache?

Aunque extensiones antiguas como APC ofrecían caché de opcode, Opcache está integrado en el núcleo de PHP desde la versión 5.5 y es mantenido activamente. Ofrece mejor integración con el motor Zend, soporte para archivos precompilados y una gestión de memoria más eficiente.

Configuración Inicial y Explicación de Directivas Clave

La configuración se realiza en php.ini o en un archivo dedicado (ej. /etc/php/8.2/fpm/conf.d/10-opcache.ini). A continuación, una configuración recomendada para un servidor con 8 GB de RAM y una aplicación de tamaño medio (WordPress con 50 plugins):

[opcache]
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.fast_shutdown=1
opcache.validate_timestamps=1
opcache.revalidate_path=0
opcache.save_comments=1
opcache.load_comments=1
opcache.enable_file_override=1
opcache.huge_code_pages=1

Explicación de cada directiva:

  • opcache.memory_consumption=256: Tamaño total de memoria compartida para opcodes. 256 MB es un punto de partida seguro para sitios con 100-200 archivos PHP. Monitorear con opcache_get_status().
  • opcache.interned_strings_buffer=16: Memoria para almacenar cadenas internadas (strings repetidos). Un valor bajo causa fragmentación; uno alto desperdicia RAM. 16 MB suele ser adecuado.
  • opcache.max_accelerated_files=10000: Número máximo de archivos PHP cacheados. Debe ser mayor que el número real de archivos. Usar find . -name "*.php" | wc -l para calcular. Si tu app tiene 8000 archivos, pon 10000.
  • opcache.revalidate_freq=2: Tiempo en segundos para comprobar cambios en archivos cacheados. En producción, valores entre 2 y 60 segundos. Si haces deploys frecuentes, considera opcache.validate_timestamps=0 y reiniciar PHP-FPM manualmente.
  • opcache.fast_shutdown=1: Acelera el apagado del proceso PHP liberando la memoria del opcode de forma rápida. Reduce la latencia en peticiones.
  • opcache.huge_code_pages=1: (Requiere PHP 7.0+) Permite usar páginas de memoria grandes (2 MB o 1 GB) para reducir TLB misses. Aumenta la velocidad de acceso al opcode. Debe estar soportado por el kernel (verificar con grep HUGETLB /proc/meminfo).

Monitoreo y Ajuste Fino

Ejecuta un script de monitoreo simple para ver el estado de Opcache:

<?php
// opcache_status.php
$status = opcache_get_status(false);
echo "Memoria usada: " . round($status['memory_usage']['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Memoria libre: " . round($status['memory_usage']['free_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Archivos cacheados: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo "Hits: " . $status['opcache_statistics']['hits'] . "\n";
echo "Misses: " . $status['opcache_statistics']['misses'] . "\n";
echo "Hit rate: " . round(($status['opcache_statistics']['hits'] / ($status['opcache_statistics']['hits'] + $status['opcache_statistics']['misses'])) * 100, 2) . "%\n";
?>

Interpretación:

  • Hit rate debe ser > 98%. Si es menor, aumenta memory_consumption o max_accelerated_files.
  • Misses altos indican que los archivos no entran en caché. Verificar max_accelerated_files.
  • Fragmentación: Si opcache_get_status()['memory_usage']['free_memory'] es bajo pero used_memory no llega al límite, puede haber fragmentación. Considera reiniciar PHP-FPM periódicamente (cron semanal).

Ajuste de Procesos Dinámicos en PHP-FPM

El pool de PHP-FPM gestiona procesos worker que ejecutan scripts PHP. La configuración del pool se define en archivos como /etc/php/8.2/fpm/pool.d/www.conf. Existen tres modos de gestión de procesos:

ModoDescripciónCaso de uso
staticNúmero fijo de procesos hijos.Carga predecible, recursos dedicados.
dynamicNúmero variable según demanda.La mayoría de sitios web (recomendado).
ondemandProcesos se crean bajo demanda y se destruyen tras inactividad.Bajo tráfico, máxima eficiencia de RAM.

Configuración Dinámica Paso a Paso

Para un servidor con 4 GB de RAM y 4 núcleos, asumiendo que cada proceso PHP consume ~30 MB (medir con ps aux | grep php-fpm), podemos calcular:

  • RAM disponible para PHP: 70% de 4 GB = ~2.8 GB.
  • Máximo de procesos: 2800 MB / 30 MB ≈ 93 procesos.

Configuración en www.conf:

; Modo dinámico
pm = dynamic

; Número mínimo de procesos hijos inactivos (spares)
pm.min_spare_servers = 5

; Número máximo de procesos hijos inactivos
pm.max_spare_servers = 30

; Número máximo de procesos hijos (límite absoluto)
pm.max_children = 90

; Número de procesos hijos creados al iniciar
pm.start_servers = 15

; Tiempo de espera para matar un proceso inactivo (segundos)
pm.process_idle_timeout = 10s

; Límite de peticiones por proceso antes de reiniciar (evita memory leaks)
pm.max_requests = 500

Cálculo de pm.max_children:

# Fórmula: (RAM_total * 0.7) / (memoria_por_proceso_promedio)
# Ejemplo: (4096 * 0.7) / 30 ≈ 95
# Ajustar a la baja para dejar margen al sistema operativo y otros servicios.

Nota importante: Si pm.max_children es demasiado alto, el servidor puede entrar en swap (intercambio de memoria) y degradar el rendimiento drásticamente. Monitorear con free -m y vmstat 1.

Ajuste de pm.max_requests

Cada proceso PHP maneja un número limitado de peticiones antes de ser reiniciado. Esto previene la acumulación de fugas de memoria (memory leaks) en bibliotecas o plugins mal escritos.

pm.max_requests = 500

Valores típicos: 200-1000. Si tu aplicación tiene fugas de memoria conocidas, reduce a 200. Si es estable, 1000-2000 es aceptable. Para verificar el promedio de peticiones por proceso antes de morir, usa logs de PHP-FPM ([pool www] child 12345 exited with code 0 after 512 requests).

Monitoreo del Pool

Herramienta de línea de comandos para ver el estado del pool:

# Ver procesos activos
ps aux | grep php-fpm | grep -v grep | wc -l

# Estadísticas detalladas (requiere habilitar status page)
# En www.conf:
pm.status_path = /status
# Luego en Nginx:
location /status {
    fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
    include fastcgi_params;
    allow 127.0.0.1;
    deny all;
}

Acceder a http://tudominio.com/status para ver:

pool:                 www
process manager:      dynamic
start time:           10/Oct/2023:12:00:00 +0000
start since:          3600
accepted conn:        50000
listen queue:         0
max listen queue:     5
listen queue len:     128
idle processes:       25
active processes:     65
total processes:      90
max active processes: 85
max children reached: 0
slow requests:        0

Interpretación:

  • listen queue > 0 indica que las peticiones están esperando por un proceso libre. Si es persistente, aumentar pm.max_children.
  • max children reached > 0 significa que se alcanzó el límite de procesos. Aumentar pm.max_children o escalar hardware.
  • slow requests > 0 indica scripts lentos. Habilitar request_slowlog_timeout = 5s y slowlog = /var/log/php-slow.log.

Integración con Nginx y Opcache + Dynamic Tuning

Configuración de Nginx para Aprovechar Opcache

Nginx no tiene control directo sobre Opcache, pero puede evitar que PHP-FPM reprocese archivos estáticos. Configura un bloque de ubicación para archivos estáticos y usa try_files para que Nginx sirva directamente:

server {
    listen 80;
    server_name ejemplo.com;
    root /var/www/ejemplo;

    index index.php;

    # Archivos estáticos - Nginx los sirve sin pasar por PHP
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # Pasar peticiones PHP a PHP-FPM
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PHP_VALUE "opcache.revalidate_freq=2";
        fastcgi_read_timeout 60;
    }

    # Denegar acceso a archivos ocultos
    location ~ /\. {
        deny all;
    }
}

Ajuste de Tiempos de Espera

Los procesos PHP pueden colgarse por scripts lentos o bloqueos de base de datos. Configura tiempos de espera en PHP-FPM y Nginx:

PHP-FPM (www.conf):

request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php-slow.log

Nginx (nginx.conf):

http {
    fastcgi_read_timeout 60;
    proxy_read_timeout 60;
}

Advertencia: request_terminate_timeout mata el proceso PHP si supera el límite. Esto puede causar errores 502 si no se maneja adecuadamente. Ajusta según el tiempo máximo que esperas que tarde una petición legítima.

Estrategias Avanzadas de Optimización

Uso de opcache.file_cache

Para sistemas con poca RAM o como capa adicional, Opcache puede escribir archivos precompilados en disco:

opcache.file_cache=/tmp/opcache
opcache.file_cache_only=0

Esto permite que los opcodes sobrevivan a reinicios de PHP-FPM, reduciendo el calentamiento inicial.

Balanceo de Carga con Múltiples Pools

Para aplicaciones con diferentes perfiles de carga (ej. API y frontend), crea pools separados:

Pool API (/etc/php/8.2/fpm/pool.d/api.conf):

[api]
user = www-data
group = www-data
listen = /var/run/php/php8.2-fpm-api.sock
pm = dynamic
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.max_requests = 200

Pool Frontend (/etc/php/8.2/fpm/pool.d/frontend.conf):

[frontend]
user = www-data
group = www-data
listen = /var/run/php/php8.2-fpm-frontend.sock
pm = dynamic
pm.max_children = 60
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

Luego en Nginx, redirige según la URL:

location /api/ {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-api.sock;
    include fastcgi_params;
}
location / {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-frontend.sock;
    include fastcgi_params;
}

Monitoreo con Prometheus y Grafana

Para entornos de producción, integra métricas de PHP-FPM con Prometheus usando un exporter:

# Instalar php-fpm-exporter (Go)
wget https://github.com/hipages/php-fpm-exporter/releases/download/v2.1.0/php-fpm-exporter_linux_amd64.tar.gz
tar xvf php-fpm-exporter_linux_amd64.tar.gz
sudo mv php-fpm-exporter /usr/local/bin/

# Ejecutar como servicio
sudo tee /etc/systemd/system/php-fpm-exporter.service <<EOF
[Unit]

Description=PHP-FPM Exporter

After=network.target

[Service]

ExecStart=/usr/local/bin/php-fpm-exporter --phpfpm.scrape-uri=http://127.0.0.1/status

Restart=always

User=www-data

[Install]

WantedBy=multi-user.target

EOF

sudo systemctl enable php-fpm-exporter
sudo systemctl start php-fpm-exporter

Luego añade un target en prometheus.yml y visualiza en Grafana con un dashboard que muestre:

  • Número de procesos activos vs. inactivos.
  • Tasa de aciertos de Opcache.
  • Uso de memoria de Opcache.
  • Cola de escucha de PHP-FPM.

Solución de Problemas Comunes

Error 502 Bad Gateway

Causas:

  • PHP-FPM caído.
  • pm.max_children alcanzado.
  • Tiempo de espera agotado.

Solución:

# Verificar estado
systemctl status php8.2-fpm
# Aumentar límites
# En www.conf: pm.max_children = 150
# En nginx.conf: fastcgi_read_timeout 120;

Opcache no acelera

Causas:

  • opcache.enable=0 en php.ini.
  • opcache.validate_timestamps=1 con revalidate_freq muy bajo (0).
  • Archivos cacheados exceden max_accelerated_files.

Solución:

# Forzar recarga
sudo systemctl reload php8.2-fpm
# Verificar con opcache_get_status()

Uso excesivo de RAM

Causas:

  • pm.max_children demasiado alto.
  • Fugas de memoria en scripts PHP.

Solución:

# Reducir pm.max_children
# Habilitar pm.max_requests=

¿Necesitas ayuda?Son dos de nuestros técnicos, Agustín y Mikel, y están disponibles para resolver cualquier problema.

Hablar con ellos ahora
Agustín y Mikel