PHP-FPM: pool, workers e o 502 que volta sempre
O site cai no horário de pico, volta sozinho e ninguém acha erro no log do PHP. O problema quase nunca é o código — é a conta entre workers, memória e o que o nginx espera do outro lado.
Equipe EasyOps Cloud · · 7 min de leitura
Todo dia às onze da manhã o site devolve 502 Bad Gateway por alguns minutos.
Depois volta sozinho. O log do PHP não registra erro, o log da aplicação termina
no meio, e o top mostra a máquina com memória sobrando.
É o retrato clássico de PHP-FPM mal dimensionado. E a razão de ser difícil de
diagnosticar é que o erro aparece no nginx, enquanto a causa está no processo
que ele tenta acessar — dois logs diferentes, duas configurações que precisam
concordar, e nenhuma delas reclama sozinha.
O que o 502 significa aqui
O nginx não executa PHP. Ele repassa a requisição ao PHP-FPM por um socket e
espera. O 502 significa que essa conversa falhou — não que o PHP tenha dado
erro.
Três causas produzem exatamente o mesmo código:
- Não havia worker livre. Todos ocupados, a fila estourou, a conexão foi recusada.
- O worker morreu no meio. Falta de memória, ou o próprio FPM o reciclou.
- O socket está errado. Caminho, permissão ou dono incorretos.
O log do nginx distingue as três em uma linha:
sudo tail -50 /var/log/nginx/error.logconnect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily
unavailable) é fila cheia. (2: No such file or directory) é caminho errado. (13:
Permission denied) é permissão. E recv() failed (104: Connection reset by peer)
é worker que morreu.
Quantos workers cabem de fato
Aqui está a decisão central, e ela é aritmética simples que quase ninguém faz.
Cada worker do PHP-FPM é um processo com memória própria. O número máximo de workers multiplicado pelo consumo de cada um não pode ultrapassar a RAM disponível — senão o kernel começa a matar processos, e o site cai de um jeito bem pior.
Meça o consumo real por worker, em vez de estimar:
ps -o rss,cmd -C php-fpm8.3 --sort=-rss | head
ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1; n++} END {printf "media: %.0f MB\n", s/n/1024}'Uma aplicação enxuta fica em torno de 40 MB por worker; um sistema grande com muito framework passa de 150 MB. Com o número em mãos:
max_children = (RAM disponível para PHP) / (média por worker)Em uma VPS de 4 GB rodando também o banco e o nginx, sobram talvez 2 GB para o
PHP. Com 80 MB por worker, isso dá 25 — não 200, que é o valor que aparece em
tutorial copiado.
Errar para cima é pior que errar para baixo. Com max_children alto demais, o
pico consome toda a memória e a máquina inteira degrada; com valor conservador,
as requisições excedentes esperam na fila e o site fica lento, mas de pé.
Escolher o gerenciador de processos
; /etc/php/8.3/fpm/pool.d/meusite.conf
[meusite]
user = meuapp
group = meuapp
listen = /run/php/meusite.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 25
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500As três estratégias e onde cada uma serve:
| pm | Comportamento | Quando usar |
|---|---|---|
| static | Número fixo de workers, sempre no ar | Tráfego alto e constante |
| dynamic | Cresce e encolhe entre limites | Padrão para a maioria |
| ondemand | Cria sob demanda, encerra ocioso | Muitos sites pequenos na mesma máquina |
O pm.max_requests = 500 recicla o worker após 500 requisições. É a defesa barata
contra vazamento de memória em extensão — o worker é recriado antes de crescer
demais. Sem ele, um vazamento pequeno vira um problema grande depois de dias.
Se você hospeda vários sites na mesma máquina, use um pool por site, com usuário próprio cada um. Isso isola consumo e permissão: um site que estoura os workers não derruba os vizinhos, e um site comprometido não lê os arquivos dos outros.
Timeouts que precisam concordar
Um dos erros mais frequentes é configurar timeout em um lado só. Os três valores precisam estar alinhados, do maior para o menor:
; php.ini
max_execution_time = 60; pool
request_terminate_timeout = 70location ~ \.php$ {
fastcgi_pass unix:/run/php/meusite.sock;
fastcgi_read_timeout 75s;
}A ordem importa. O PHP deve desistir primeiro, para que o erro seja registrado
como erro de PHP — com stack trace e arquivo — em vez de virar um 504 genérico
no nginx. Se o nginx desiste antes, você perde a informação de qual código
travou.
O request_terminate_timeout é a rede de segurança: ele mata o worker quando o max_execution_time
não dá conta, o que acontece em espera por rede ou por banco — esse tempo não
conta para o PHP.
O slowlog resolve o "por que travou"
Quando os workers estão todos ocupados, a pergunta é o que eles estão fazendo. O
slowlog responde com stack trace, sem instrumentar nada:
slowlog = /var/log/php-fpm/meusite-slow.log
request_slowlog_timeout = 5ssudo tail -f /var/log/php-fpm/meusite-slow.logCada entrada mostra a pilha de chamadas no momento em que a requisição passou de 5 segundos. Na prática, o resultado quase sempre aponta para uma das três: consulta sem índice, chamada a API externa sem timeout, ou laço processando arquivo grande.
Chamada externa sem timeout é a mais insidiosa. Um parceiro que fica lento consome todos os seus workers em minutos, e o seu site cai por causa do sistema de outra pessoa. Todo cliente HTTP dentro da aplicação precisa de timeout explícito e curto.
Ver o estado em tempo real
pm.status_path = /fpm-statuslocation = /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/meusite.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}curl -s http://127.0.0.1/fpm-statusDuas linhas dessa saída valem por todo o resto. listen queue diferente de zero
significa que há requisição esperando worker — é o sinal antecedente do 502. E max
children reached com valor crescente é a confirmação de que o limite está sendo
atingido de fato.
Se max children reached sobe e ainda há memória livre, aumente max_children. Se
sobe e a memória está no limite, o caminho é reduzir o consumo por requisição ou
aumentar a máquina — não adianta aumentar o número de workers sem RAM para
sustentá-los.
OPcache: o ajuste de maior retorno
Sem OPcache, o PHP recompila cada arquivo a cada requisição. Ligá-lo costuma cortar o tempo de resposta pela metade e reduzir o consumo de CPU na mesma proporção.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 192
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2Em produção com deploy controlado, validate_timestamps = 0 extrai o máximo — mas
exige recarregar o FPM a cada implantação, senão o código novo simplesmente não
entra em vigor. É uma troca consciente: mais desempenho, um passo obrigatório no
deploy.
Confira se o cache não está estourando, o que anula o benefício:
php -r 'print_r(opcache_get_status(false)["opcache_statistics"]);'oom_restarts maior que zero significa que a memória do OPcache é insuficiente.
Socket unix ou TCP
Uma decisão pequena com efeito mensurável. O PHP-FPM aceita conexão por socket unix ou por porta TCP, e o padrão da distribuição varia.
Socket unix é mais rápido — não passa pela pilha de rede — e mais seguro, porque
o acesso é controlado por permissão de arquivo em vez de por firewall. É a
escolha certa quando nginx e PHP-FPM estão na mesma máquina, que é o caso da
maioria esmagadora.
TCP só faz sentido quando os dois estão em máquinas diferentes. Nesse caso, o PHP-FPM passa a escutar em rede, e aí vale tudo que se aplica a qualquer serviço exposto: escutar apenas no IP interno, restringir a origem e nunca deixar acessível pela internet.
; socket unix — preferível
listen = /run/php/meusite.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; TCP, apenas se em máquinas separadas
; listen = 10.0.0.11:9000
; listen.allowed_clients = 10.0.0.10O listen.owner precisa ser o usuário do nginx, e não o da aplicação. É a
inversão que mais confunde: o socket é criado pelo FPM, mas quem precisa abrir o
arquivo é o nginx. Errar isso produz o 502 com Permission denied no log — o
mesmo código, causa completamente diferente.
O roteiro quando o 502 aparecer
# 1. o que o nginx diz exatamente
sudo tail -30 /var/log/nginx/error.log
# 2. o FPM está vivo?
systemctl status php8.3-fpm
# 3. há fila?
curl -s http://127.0.0.1/fpm-status | grep -E 'listen queue|max children'
# 4. memória sobrando?
free -h
# 5. o que trava
sudo tail -40 /var/log/php-fpm/meusite-slow.logEssa sequência distingue os três cenários em poucos minutos: fila cheia com
memória sobrando é max_children baixo; fila cheia com memória no limite é
consumo por requisição; FPM parado é outro problema, e o diagnóstico do systemd
cobre.
E vale lembrar que 502 também acontece por motivos que nada têm a ver com PHP —
os erros de configuração do nginx como reverse proxy produzem o mesmo código, e
conferir de que lado está o problema é sempre o primeiro passo.