Análise prática — comparativo: Nginx Unit vs PHP‑FPM vs RoadRunner para executar monólitos PHP em containers — latência, throughput e consumo de memória
Por Kevin Marcondes · Publicado em · 10 min de leitura
comparativo: Nginx Unit vs PHP‑FPM vs RoadRunner para executar monólitos PHP em containers — latência, throughput e consumo de memória: aqui você encontra o que realmente muda no p50/p95/p99, no RPS por core e no uso de RAM. O objetivo é orientar sua escolha com critérios objetivos e ajustes práticos para produção.
Metodologia: o que medimos e por que isso importa
Latência tail (p95/p99), throughput sustentável e memória por container são os sinais que mais expõem gargalos em produção. Medir esses três pontos, sob padrões de carga diferentes, evita surpresas depois do deploy.
Para isolar o runtime, use carga HTTP com keep‑alive, warm‑up de OPcache e preloading, e inclua o roteamento interno que você realmente usa (por exemplo, proxy ou ingress). Avalie em faixas de concorrência (por exemplo, 25/50/100 conexões) e colete p50/p95/p99, RPS estável e uso de RAM reportado pelo cgroup.
Em monólitos PHP, bibliotecas, autoload e caches de metadados pesam mais na latência fria do que o proxy em si. Runtimes com workers persistentes tendem a beneficiar frameworks pesados; modelos request‑per‑process ajudam a conter crescimento de memória ao preço de cold starts mais frequentes.
Para aprofundar nuances Unit × FPM (sem RoadRunner), veja também o estudo focado em containers em Comparativo: Nginx Unit vs PHP-FPM (containers). Para planejar medições reprodutíveis, siga o guia de testes de carga com k6.
- Métricas‑chave: p50/p95/p99, RPS sustentável, uso de RAM pelo cgroup
- Padrões de carga: conexões concorrentes, keep‑alive e jitter de chegada
- Aquecer o aplicativo: OPcache, preloading, caches do framework
- Ambiente: mesma imagem base, mesmas extensões, mesmo número de workers
Qual tem menor latência sob carga?
RoadRunner costuma apresentar menor latência tail (p95/p99) em apps com muito bootstrap de framework, porque mantém workers PHP quentes entre requisições. Nginx Unit e PHP‑FPM se aproximam quando OPcache e preloading estão bem ajustados e o pool evita cold starts.
Em workloads I/O‑bound (cache/banco rápidos), a diferença de p50 pode ser pequena. O que muda é a variância sob picos: filas curtas e workers persistentes tendem a estabilizar p95, desde que haja back‑pressure para não saturar a CPU.
Se o monólito faz muito trabalho por requisição (serialização, validação pesada), a persistência de contexto reduz parse/autoload repetidos, favorecendo RoadRunner. Já em endpoints leves, a simplicidade do FPM ou do Unit pode empatar a latência média.
Qual entrega maior throughput por núcleo?
Quando o gargalo é CPU no usuário (parse, view, serialização), workers persistentes e pooling eficiente tendem a render mais RPS por core. Nesses cenários, RoadRunner frequentemente escala melhor por núcleo do que PHP‑FPM e Nginx Unit.
Com I/O predominante (cache na RAM, DB com baixa latência), o throughput converge, e o limitador passa a ser conexões no backend e filas de rede. Ajustes como tamanhos de pool, afinidade CPU e desativar logs síncronos fazem mais diferença que a escolha do runtime.
Para fairness, compare RPS estável (sem quedas) ao longo de minutos, não picos instantâneos. Observe também a relação RPS/latência p95: throughput que cresce às custas de p95 descontrolado não é sustentável.
Qual consome menos memória por requisição e por pod?
PHP‑FPM e Nginx Unit tendem a ter baseline de memória mais baixo por processo, com recuperação mais previsível entre requisições. RoadRunner mantém o aplicativo residente, o que aumenta o baseline do worker, mas reduz alocações repetidas a cada chamada.
Em frameworks grandes, o consumo por worker em RoadRunner sobe por manter caches e singletons vivos; controle isso definindo limites e reciclando workers após N requisições ou por uso de memória. Em FPM, o pm.max_requests cumpre papel similar, reduzindo fragmentação.
Medição correta em containers usa PSS/RSS do cgroup, não o RSS do host. Compare a soma do master + workers e normalize por RPS estável para estimar “MB por 100 RPS” sob o mesmo perfil de carga.
- RoadRunner: baseline de memória maior; menos alocações repetidas
- PHP‑FPM: baseline menor; reciclagem por pm.max_requests
- Nginx Unit: processos de app gerenciados pelo Unit; perfil próximo ao FPM
Arquitetura e modelo de processos de cada runtime
Os três executam PHP, mas com modelos diferentes, e isso explica as diferenças de latência, throughput e memória.
PHP‑FPM: um gerenciador de processos (master) cria pools de workers e cada requisição é servida por um worker dedicado. É o modelo clássico request‑per‑process, com isolamento previsível e reciclagem configurável.
Nginx Unit: um servidor de aplicações multiprotocolo que gerencia processos do app PHP diretamente, sem FPM. A configuração em runtime via API permite trocar rotas e processos sem reload completo.
RoadRunner: um servidor em Go que mantém workers PHP persistentes e se comunica com eles via IPC. O servidor HTTP, filas e jobs convivem no mesmo binário, reduzindo hops e mantendo o bootstrap quente.
- Isolamento: mais forte no FPM/Unit; persistência: mais forte no RoadRunner
- Reload: FPM por sinal; Unit por API; RoadRunner com reinício gracioso de workers
- Observabilidade: exporte métricas de filas/pool para Prometheus onde possível
Tuning essencial em containers para resultados consistentes
Alguns ajustes têm alto impacto prático, independentemente do runtime. Aplique‑os antes de concluir a comparação.
No PHP‑FPM: dimensione pm.max_children pelo limite de CPU/RAM do pod; use pm = dynamic ou static conforme previsibilidade; ajuste pm.max_requests para reciclar workers e conter fragmentação; trate slowlog e limites de arquivo. Veja também o serviço de tuning de PHP‑FPM.
No Nginx Unit: defina número de processos do app compatível com CPU do container; use a API para reconfiguração sem downtime; desative logs verbosos em produção e use buffers adequados.
No RoadRunner: fixe número de workers por CPU; limite jobs por worker e por memória para prevenir growth infinito; trate back‑pressure quando a fila crescer; isole logs de acesso e de app; teste o modo de reinício gracioso antes do deploy.
- OPcache: habilitar, ajustar memory_consumption e revalidate_freq
- Preloading: carregar hotpaths do framework na inicialização
- Kernel/contêiner: usar tini/PID 1, ulimits adequados, TCP backlog
- Proxy/Ingress: buffers, timeouts e keep‑alive alinhados ao runtime
Operação em produção: health checks, logs e deploy sem downtime
Todos suportam operação sem downtime, desde que health/readiness, logs e reload gracioso estejam bem configurados. Healthchecks de liveness simples e readiness que considerem conexões ao DB/cache evitam servir tráfego antes do aquecimento.
Implemente readiness com rota que valide dependências e latência do backend, como descrito no guia de health checks para PHP em Swarm. Para deploys, estratégias canário ou blue‑green reduzem risco; veja o passo a passo de deploy blue‑green com Docker Compose.
Logs síncronos derrubam throughput; prefira STDOUT/STDERR com agregação, formatos estruturados e amostragem de acesso em alto volume. Teste reloads sob carga, medindo p95 durante a troca para confirmar ausência de quedas.
- Readiness: só fique Ready após aquecer OPcache/preloading
- Reload: teste sinais/estratégias de cada runtime sob carga real
- Logs: estruturar e evitar flush síncrono em caminhos quentes
Quando escolher cada um? (critérios objetivos)
A decisão muda conforme peso do framework, perfil CPU/I‑O e requisitos operacionais. Use os critérios abaixo como atalho e valide com testes de carga no seu código. Este comparativo mantém o foco em containers para monólitos PHP e nos efeitos práticos de latência, throughput e memória.
Para aprofundar Unit × FPM em containers, leia também o comparativo dedicado.
| Critério | Nginx Unit | PHP-FPM | RoadRunner |
|---|---|---|---|
| Latência tail (framework pesado) | Boa, melhora com OPcache/preloading | Boa, melhora com OPcache/preloading | Muito boa, workers persistentes reduzem cold start |
| Throughput por núcleo (CPU-bound) | Bom, próximo ao FPM com tuning | Bom, escala com pools e afinidade | Muito bom, aproveita bootstrap quente |
| Memória por worker | Moderada, processos gerenciados pelo Unit | Baixa a moderada, reciclável por pm.max_requests | Maior baseline; controlar reciclagem/limites |
| Operação e reload | API dinâmica, reconfiguração sem reiniciar | Sinais do master, padrão consolidado | Reinício gracioso de workers no binário |
| Complexidade no stack | Baixa: Unit pode servir HTTP diretamente | Baixa a média: geralmente com Nginx/Ingress | Média: servidor Go + workers e orquestração |
| Compatibilidade ampla (ecosistema PHP) | Ampla, sem FPM | Máxima, compatível com a maioria dos guias | Ampla via PSR; atenção a singletons stateful |
| Quando brilha | Deploys dinâmicos e simplicidade operacional | WordPress, monólitos estáveis, maturidade | Laravel/Symfony pesados, fila + HTTP no mesmo binário |
Perguntas frequentes
RoadRunner é compatível com qualquer framework PHP?
Sim, RoadRunner serve aplicações que seguem PSR (como PSR‑7/PSR‑15). Frameworks populares costumam funcionar bem, mas revise singletons e caches que mantêm estado e defina reciclagem de workers para conter crescimento de memória ao longo do tempo.
Nginx Unit substitui um reverse proxy como Nginx/Ingress?
Pode servir HTTP diretamente e expor a aplicação, reduzindo camadas. Em clusters, ainda é comum usar um Ingress Controller ou proxy na borda para TLS, rate limiting e roteamento global. O Unit brilha ao reconfigurar rotas/processos em runtime via API.
PHP‑FPM ainda é a escolha padrão para WordPress?
Na maioria dos casos, sim. PHP‑FPM é amplamente suportado por tutoriais, plugins e painéis de hospedagem. Ele oferece isolamento simples por pool e reciclagem previsível. Combine com OPcache e um proxy HTTP bem configurado para bom desempenho e estabilidade.
Como medir p95/p99 sem falsear resultados?
Aqueça OPcache/preloading, use keep‑alive e gere carga com jitter realista. Colete p50/p95/p99 por janelas de minutos, não picos. Meça RAM no cgroup e execute o gerador de carga próximo ao serviço para evitar latência de rede mascarando o runtime.
É possível fazer reload sem downtime nos três runtimes?
Sim, com estratégias próprias: o PHP‑FPM aceita sinais para reciclar pools, o Nginx Unit reconfigura via API sem reiniciar e o RoadRunner reinicia workers de forma graciosa. Teste sob carga para garantir que a troca não eleva o p95 ou derruba conexões.
Workers persistentes aumentam risco de vazamentos de memória?
O risco existe se o app acumula estado. Mitigue com reciclagem por número de requisições ou por limite de memória e monitore crescimento de heap. Em FPM/Unit, o modelo request‑per‑process já limita o tempo de vida do worker por padrão.
Quer decidir entre Nginx Unit, PHP‑FPM e RoadRunner com dados do seu monólito?
Desenhamos um plano de testes de carga reproduzível, ajustamos o runtime e validamos latência, throughput e memória antes do deploy.
Falar sobre seu runtime PHPVeja também
- Comparativo: ZFS vs LVM+LUKS em VPS com bancos de dados — snapshots, desempenho e recuperação após falha
- nftables vs iptables no Ubuntu 22.04: qual usar e exemplos práticos
- Comparativo: Flyway vs Liquibase vs Sqitch para versionamento de banco de dados em projetos PHP/Laravel
- Comparativo: ZFS vs Btrfs para snapshots, replicação e retenção incremental em VPS com containers Docker — prós e contras práticos