Análisis práctico — comparativa: Nginx Unit vs PHP‑FPM vs RoadRunner para ejecutar monolitos PHP en contenedores — latencia, throughput y consumo de memoria
Por Kevin Marcondes · Publicado el · 10 min de lectura
Comparativa: Nginx Unit vs PHP‑FPM vs RoadRunner para ejecutar monólitos PHP en contenedores — aquí encontrarás lo que realmente cambia en p50/p95/p99, en RPS por core y en uso de RAM. El objetivo es orientar tu elección con criterios objetivos y ajustes prácticos para producción.
Metodología: qué medimos y por qué importa
La cola de latencia (p95/p99), el throughput sostenible y la memoria por contenedor son las señales que más exponen cuellos de botella en producción. Medir esos tres puntos, bajo patrones de carga variados, evita sorpresas tras el despliegue.
Para aislar el runtime, usa carga HTTP con keep‑alive, warm‑up de OPcache y preloading, e incluye el enrutamiento interno que realmente utilizas (por ejemplo, proxy o ingress). Evalúa en rangos de concurrencia (por ejemplo, 25/50/100 conexiones) y recoge p50/p95/p99, RPS estable y uso de RAM reportado por el cgroup.
En monólitos PHP, librerías, autoload y caches de metadatos pesan más en la latencia fría que el proxy en sí. Runtimes con workers persistentes suelen beneficiar frameworks pesados; modelos request‑per‑process ayudan a contener el crecimiento de memoria a costa de cold starts más frecuentes.
Para profundizar en matices Unit × FPM (sin RoadRunner), consulta también el comparativo: Nginx Unit vs PHP‑FPM (containers). Para planear mediciones reproducibles, sigue la guía de pruebas de carga con k6 y Docker Compose.
- Métricas clave: p50/p95/p99, RPS sostenible, uso de RAM por cgroup
- Patrones de carga: concurrencia, keep‑alive y jitter de llegada
- Calentar la app: OPcache, preloading, caches del framework
- Entorno: misma imagen base, mismas extensiones, mismo número de workers
¿Cuál tiene menor latencia bajo carga?
RoadRunner suele mostrar menor latencia en la cola (p95/p99) en aplicaciones con mucho bootstrap de framework, porque mantiene workers PHP calientes entre peticiones. Nginx Unit y PHP‑FPM se acercan cuando OPcache y preloading están bien ajustados y el pool evita cold starts.
En cargas I/O‑bound (cache/DB rápidos), la diferencia en p50 puede ser pequeña. Lo que cambia es la varianza en picos: colas cortas y workers persistentes tienden a estabilizar p95, siempre que exista back‑pressure para no saturar la CPU.
Si el monolito realiza mucho trabajo por petición (serialización, validaciones pesadas), la persistencia de contexto reduce parse/autoload repetidos, favoreciendo RoadRunner. En endpoints ligeros, la simplicidad del FPM o del Unit puede empatar la latencia media.
¿Cuál entrega mayor throughput por núcleo?
Cuando el cuello de botella es CPU en usuario (parse, render, serialización), workers persistentes y pooling eficiente tienden a rendir más RPS por core. En esos escenarios, RoadRunner frecuentemente escala mejor por núcleo que PHP‑FPM y Nginx Unit.
Con I/O predominante (cache en RAM, BD con baja latencia), el throughput converge y el limitador pasa a ser las conexiones en el backend y las colas de red. Ajustes como tamaño de pools, afinidad CPU y desactivar logs síncronos suelen importar más que elegir el runtime.
Para ser justos, compara RPS estable (sin caídas) a lo largo de minutos, no picos instantáneos. Observa también la relación RPS/latencia p95: un throughput que crece a costa de un p95 incontrolado no es sostenible.
¿Cuál consume menos memoria por petición y por pod?
PHP‑FPM y Nginx Unit tienden a tener un baseline de memoria por proceso más bajo, con recuperación más predecible entre peticiones. RoadRunner mantiene la aplicación residente, lo que eleva el baseline del worker pero reduce asignaciones repetidas por llamada.
En frameworks grandes, el consumo por worker en RoadRunner sube al mantener caches y singletons vivos; controla esto limitando y reciclando workers tras N peticiones o por uso de memoria. En FPM, pm.max_requests cumple un papel similar, reduciendo fragmentación.
La medición correcta en contenedores usa PSS/RSS del cgroup, no solo el RSS del host. Compara la suma del master + workers y normaliza por RPS estable para estimar “MB por 100 RPS” bajo el mismo perfil de carga.
- RoadRunner: baseline de memoria mayor; menos asignaciones repetidas
- PHP‑FPM: baseline menor; reciclaje mediante pm.max_requests
- Nginx Unit: procesos de app gestionados por Unit; perfil próximo al FPM
Arquitectura y modelo de procesos de cada runtime
Los tres ejecutan PHP, pero con modelos distintos, y eso explica las diferencias de latencia, throughput y memoria.
PHP‑FPM: un gestor de procesos (master) crea pools de workers y cada petición es atendida por un worker dedicado. Es el modelo clásico request‑per‑process, con aislamiento predecible y reciclaje configurable.
Nginx Unit: un servidor de aplicaciones multiprotocolo que gestiona procesos de la app PHP directamente, sin FPM. La configuración en runtime vía API permite cambiar rutas y procesos sin un reload completo.
RoadRunner: un servidor en Go que mantiene workers PHP persistentes y se comunica con ellos vía IPC. El servidor HTTP, colas y jobs conviven en el mismo binario, reduciendo saltos y manteniendo el bootstrap caliente.
- Aislamiento: más fuerte en FPM/Unit; persistencia: más fuerte en RoadRunner
- Reload: FPM por señal; Unit por API; RoadRunner con reinicio gracioso de workers
- Observabilidad: exporta métricas de colas/pool a Prometheus cuando sea posible
Tuning esencial en contenedores para resultados consistentes
Algunos ajustes tienen alto impacto práctico, independientemente del runtime. Aplícalos antes de concluir la comparación.
En PHP‑FPM: dimensiona pm.max_children según el límite de CPU/RAM del pod; usa pm = dynamic o static según previsibilidad; ajusta pm.max_requests para reciclar workers y contener fragmentación; atiende slowlog y límites de archivos. Consulta también el servicio de tuning de PHP‑FPM y kernel en VPS ARM.
En Nginx Unit: define el número de procesos de app compatible con la CPU del contenedor; usa la API para reconfiguración sin downtime; desactiva logs verbosos en producción y emplea buffers adecuados.
En RoadRunner: fija número de workers por CPU; limita jobs por worker y por memoria para evitar crecimiento infinito; maneja back‑pressure cuando la cola crece; aísla logs de acceso y de app; prueba el modo de reinicio gracioso antes del deploy.
- OPcache: habilitar, ajustar memory_consumption y revalidate_freq
- Preloading: cargar hotpaths del framework en la inicialización
- Kernel/contenedor: usar tini/PID 1, ulimits adecuados, TCP backlog
- Proxy/Ingress: buffers, timeouts y keep‑alive alineados al runtime
Operación en producción: health checks, logs y deploy sin downtime
Todos soportan operación sin downtime, siempre que health/readiness, logs y reload gracioso estén bien configurados. Healthchecks de liveness simples y readiness que consideren conexiones a DB/cache evitan servir tráfico antes del calentamiento.
Implementa readiness con una ruta que valide dependencias y latencia del backend, como se describe en la guía de health checks para PHP en Swarm. Para despliegues, estrategias canario o blue‑green reducen el riesgo; consulta el paso a paso de deploy blue‑green con Docker Compose.
Los logs síncronos reducen el throughput; prefiere STDOUT/STDERR con agregación, formatos estructurados y muestreo de accesos en alto volumen. Prueba reloads bajo carga, midiendo p95 durante el intercambio para confirmar ausencia de caídas.
- Readiness: solo estar Ready después de calentar OPcache/preloading
- Reload: prueba señales/estrategias de cada runtime bajo carga real
- Logs: estructurarlos y evitar flush síncrono en caminos calientes
¿Cuándo elegir cada uno? (criterios objetivos)
La decisión depende del peso del framework, el perfil CPU/I‑O y los requisitos operativos. Usa los criterios abajo como atajo y valida con pruebas de carga en tu propio código. Este comparativo se centra en contenedores para monólitos PHP y en los efectos prácticos sobre latencia, throughput y memoria.
Para profundizar Unit × FPM en contenedores, consulta también el comparativo dedicado.
| Criterio | Nginx Unit | PHP‑FPM | RoadRunner |
|---|---|---|---|
| Latencia tail (framework pesado) | Buena, mejora con OPcache/preloading | Buena, mejora con OPcache/preloading | Muy buena, workers persistentes reducen cold start |
| Throughput por núcleo (CPU‑bound) | Bueno, cercano al FPM con tuning | Bueno, escala con pools y afinidad | Muy bueno, aprovecha bootstrap caliente |
| Memoria por worker | Moderada, procesos gestionados por Unit | Baja a moderada, reciclable vía pm.max_requests | Baseline mayor; controla reciclaje/limites |
| Operación y reload | API dinámica, reconfiguración sin reiniciar | Señales del master, patrón consolidado | Reinicio gracioso de workers en el binario |
| Complejidad en el stack | Baja: Unit puede servir HTTP directamente | Baja a media: normalmente con Nginx/Ingress | Media: servidor Go + workers y orquestación |
| Compatibilidad amplia (ecosistema PHP) | Amplia, sin FPM | Máxima, compatible con la mayoría de guías | Amplia vía PSR; atender singletons stateful |
| Cuándo destaca | Deploys dinámicos y simplicidad operativa | WordPress, monólitos estables, madurez | Laravel/Symfony pesados; cola + HTTP en el mismo binario |
Preguntas frecuentes
¿RoadRunner es compatible con cualquier framework PHP?
Sí, RoadRunner sirve aplicaciones que siguen PSR (por ejemplo PSR‑7/PSR‑15). Los frameworks populares suelen funcionar bien, pero revisa singletons y caches que mantienen estado y configura reciclaje de workers para contener el crecimiento de memoria con el tiempo.
¿Nginx Unit sustituye a un reverse proxy como Nginx/Ingress?
Puede servir HTTP directamente y exponer la aplicación, reduciendo capas. En clústeres, normalmente se sigue usando un Ingress Controller o un proxy en el borde para TLS, rate limiting y enrutamiento global. Unit destaca por reconfigurar rutas/procesos en runtime vía API.
¿PHP‑FPM sigue siendo la elección por defecto para WordPress?
En la mayoría de casos, sí. PHP‑FPM está ampliamente soportado por tutoriales, plugins y paneles de hosting. Ofrece aislamiento simple por pool y reciclaje predecible. Combínalo con OPcache y un proxy HTTP bien configurado para buen rendimiento y estabilidad.
¿Cómo medir p95/p99 sin falsear resultados?
Calienta OPcache/preloading, usa keep‑alive y genera carga con jitter realista. Recoge p50/p95/p99 por ventanas de minutos, no por picos. Mide RAM en el cgroup y ejecuta el generador de carga cercano al servicio para evitar latencia de red que enmascare al runtime.
¿Es posible hacer reload sin downtime en los tres runtimes?
Sí, con estrategias propias: PHP‑FPM acepta señales para reciclar pools, Nginx Unit reconfigura vía API sin reiniciar y RoadRunner reinicia workers de forma graciosa. Prueba siempre bajo carga para confirmar que el intercambio no eleva el p95 ni rompe conexiones.
¿Los workers persistentes aumentan el riesgo de fugas de memoria?
El riesgo existe si la app acumula estado. Mitígalo con reciclaje por número de peticiones o por límite de memoria y monitoriza el crecimiento del heap. En FPM/Unit, el modelo request‑per‑process ya limita por defecto la vida del worker.
¿Quieres decidir entre Nginx Unit, PHP‑FPM y RoadRunner con datos de tu monolito?
Diseñamos un plan de pruebas de carga reproducible, ajustamos el runtime y validamos latencia, throughput y memoria antes del deploy.
Hablar sobre tu runtime PHPVer también
- Comparativo: ZFS vs LVM+LUKS en VPS con bases de datos — snapshots, rendimiento y recuperación tras fallos
- Comparativa: ZFS vs Btrfs para snapshots, replicación y retención incremental en VPS con contenedores Docker — pros y contras prácticos
- Comparativa: Flyway vs Liquibase vs Sqitch para versionado de base de datos en proyectos PHP/Laravel
- nftables vs iptables en Ubuntu 22.04: cuál usar y ejemplos prácticos