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 · Publicado em · 10 min de leitura

Notebook com tela exibindo dashboard de métricas e gráficos, teclado em primeiro plano
Tela borrada com gráficos e métricas, indicada para conteúdo sobre latência, throughput e memória. Foto: Atlantic Ambience / Pexels

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 PHP

Veja também