Calcular o pm.max_children do PHP-FPM em vez de adivinhar

Publicado a 24 min de leitura

A mensagem server reached pm.max_children não quer dizer que deva duplicar o valor. Meça, faça as contas, escolha o modo de funcionamento e comprove depois que o valor assenta mesmo.

A dada altura, esta linha aparece no log do PHP-FPM, e já traz o conselho incluído:

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

O conselho não está errado, apenas incompleto. pm.max_children é o único parâmetro do PHP-FPM em que um valor demasiado alto é mais perigoso do que um valor demasiado baixo. Demasiado baixo custa tempo de espera e, no pior dos casos, um 502. Demasiado alto custa a memória RAM do servidor inteiro, e a partir daí é o kernel que escolhe sozinho o processo que termina. A experiência mostra que não escolhe o PHP, escolhe a base de dados.

Este guia mostra o caminho que vai de adivinhar a calcular: medir a memória que um processo de trabalho consome de facto, determinar o orçamento, escolher o modo de funcionamento e, no fim, comprovar que o valor está certo.

Todas as indicações se referem ao Debian 13 (trixie), ao Debian 12 (bookworm), ao Ubuntu 24.04 LTS e ao Ubuntu 22.04 LTS. Os comandos estão escritos para serem executados como root; como utilizador normal, anteponha sudo. Substitua em toda a parte o número de versão do PHP pelo do seu sistema.

SistemaPHPServiçoFicheiro do poolLog do FPM
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/pool.d/www.conf/var/log/php8.4-fpm.log
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/pool.d/www.conf/var/log/php8.2-fpm.log
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/pool.d/www.conf/var/log/php8.3-fpm.log
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/pool.d/www.conf/var/log/php8.1-fpm.log

Que versões estão instaladas, mostra-o um olhar ao diretório. Em servidores que passaram por uma atualização de distribuição, são muitas vezes duas:

ls /etc/php/
ls /etc/php/*/fpm/pool.d/

O que o pm.max_children limita realmente

O valor não limita visitantes nem ligações, limita o número de pedidos PHP que estão a ser executados no mesmo instante. Um pedido de 80 milissegundos ocupa um processo de trabalho durante 80 milissegundos e liberta-o a seguir. Daqui decorre quase tudo o resto:

  • Muito tráfego precisa de poucos processos, desde que os scripts sejam rápidos. 40 pedidos por segundo com 80 milissegundos cada dão pouco mais de três pedidos em simultâneo, não 40.
  • Um único ponto lento deita a conta abaixo. Uma chamada a uma interface de programação alheia sem um timeout próprio mantém um processo ocupado durante cinco segundos, apesar de esse processo não estar a fazer nada. Cinco chamadas destas por segundo prendem 25 processos de forma permanente.
  • Ligações em espera não ocupam processo nenhum. Uma ligação keep-alive ao nginx custa um descritor de ficheiro, mas não custa um processo de trabalho.

Quando todos os processos estão ocupados, os novos pedidos ficam à espera na fila de aceitação do socket. O tamanho dessa fila está no ficheiro do pool, em listen.backlog, com 511 de origem, e o kernel limita-a ainda através de net.core.somaxconn, que nos quatro sistemas está em 4096:

sysctl net.core.somaxconn

Enquanto a fila chegar, o visitante nota apenas tempos de carregamento mais longos. Quando também ela enche, o kernel rejeita a ligação, o nginx regista 11: Resource temporarily unavailable e ao visitante chega um 502. Para distinguir das restantes causas: resolver o nginx 502 Bad Gateway.

Um pormenor que causa muita confusão: o aviso surge uma vez por fase de saturação, não por cada pedido afetado. O FPM coloca internamente uma marca e só a apaga quando volta a haver um processo livre. Uma única linha pode representar uma hora de ponta inteira. Quem conta linhas subestima o problema. O valor honesto é o contador max children reached na página de estado, que é consultada mais abaixo.

O caminho de volta, antes de mudar seja o que for

Há duas coisas que podem correr mal aqui, e ambas atingem o serviço em produção.

Primeiro: o FPM deixa de arrancar. Um systemctl reload envia ao processo principal o sinal USR2, e este reinicia-se a si próprio com a nova configuração. Se ela for inválida, a inicialização é interrompida e o processo principal termina. De uma instância a funcionar passa a haver uma instância morta. Por isso, sem exceção, verifique primeiro e recarregue depois:

php-fpm8.2 -t

Em caso de sucesso, a saída termina com test is successful.

Segundo: o valor está demasiado alto. Nesse caso o servidor não fica logo em baixo, fica apenas no pico de carga seguinte, e de forma tão completa que até um início de sessão por SSH bloqueia ou nem chega a estabelecer-se. Para isso precisa de um segundo caminho até ao servidor.

Nos servidores root KVM e nos servidores dedicados da KernelHost, esse caminho é a consola VNC na área de cliente. Está ligada à camada de virtualização ou à própria máquina, e continua acessível mesmo quando o serviço SSH já não responde por falta de memória. Inicie sessão por aí antes, nem que seja uma única vez. Uma via de salvamento que só se experimenta pela primeira vez durante a emergência não é via nenhuma.

Faça além disso uma cópia do ficheiro do pool, com data e hora no nome, para que uma segunda tentativa não sobrescreva a primeira cópia:

mkdir -p /root/backups
cp -a /etc/php/8.2/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)
ls -l /root/backups/

O caminho de volta tem então três linhas:

cp -a /root/backups/www.conf.2026-09-03-1015 /etc/php/8.2/fpm/pool.d/www.conf
php-fpm8.2 -t
systemctl reload php8.2-fpm

Uma proteção para o caso de se enganar nas contas

Um limite de memória na unidade systemd faz com que um erro de avaliação atinja um processo de trabalho do PHP e não a base de dados: o kernel passa a terminar processos dentro do control group do FPM, em vez de procurar o maior processo de todo o sistema.

mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/memoria.conf <<'EOF'
[Service]
MemoryHigh=1500M
MemoryMax=2G
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm

Verifique se ficou mesmo aplicado:

systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax

MemoryHigh trava e liberta memória, MemoryMax é o limite duro. Os quatro sistemas usam a hierarquia unificada de control groups, portanto os valores atuam de imediato. Isto é uma proteção, não um substituto da conta: quando o limite é atingido, o visitante afetado continua a ver um erro, só que não cai o servidor inteiro. Sobre como guardar corretamente estes ficheiros de complemento: criar um serviço systemd.

E a terceira regra, que não precisa de comando nenhum: uma alteração de cada vez. Quem mexe ao mesmo tempo no modo de funcionamento, no pm.max_children e nos valores spare fica sem saber, depois, o que é que fez efeito.

Levantamento: qual é o pool que vale

Cada pool tem o seu próprio pm.max_children, e para a memória o que conta é a soma de todos os pools:

grep -n "^pm" /etc/php/*/fpm/pool.d/*.conf

No estado de origem, os quatro sistemas trazem exatamente o mesmo:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Estes não são valores recomendados, são marcadores de posição com os quais o PHP arranca mesmo numa máquina de testes muito pequena. A isso acresce um limite máximo global, válido para todos os pools em conjunto:

grep -n "^process.max" /etc/php/*/fpm/php-fpm.conf

Se a saída ficar vazia, a linha está comentada e vale o valor predefinido 0, ou seja, nenhum limite global. Em servidores com muitos pools, é precisamente esta linha que impede que a soma rebente com a memória assim que vários pools ficam sob carga ao mesmo tempo.

No fim, o que vale não é o ficheiro, é aquilo que o FPM faz dele. A opção -tt imprime a configuração completamente resolvida, incluindo todos os valores predefinidos que não constam de ficheiro nenhum:

php-fpm8.2 -tt 2>&1 | grep -E "^\[|pm |pm\.|listen ="

Esta saída serve mais tarde também de prova de que uma alteração ficou mesmo aplicada.

Medir a memória que um processo de trabalho consome

É aqui que a maioria dos guias se torna imprecisa. Há três números que são confundidos com regularidade:

  • memory_limit, 128M de origem no modo FPM: um limite máximo por pedido, não um consumo. Um script que precisa de 12 MB continua a precisar apenas de 12 MB com 512M.
  • RSS: tudo o que está neste momento em memória RAM, incluindo o OPcache partilhado e as bibliotecas partilhadas. Quem soma o RSS de 20 processos conta o mesmo OPcache vinte vezes.
  • PSS: as páginas partilhadas são divididas pelo número dos seus utilizadores. É o único dos três números que faz sentido somar.

Para a conta precisa do PSS. O kernel entrega-o já agregado em /proc/<pid>/smaps_rollup, legível como root:

pgrep -f "php-fpm: pool www" | while read -r p; do
  awk '/^Pss:/ {print $2}' "/proc/$p/smaps_rollup"
done | awk '{s+=$1; n++} END {
  if (!n) { print "nenhum processo de trabalho encontrado"; exit }
  printf "Processos: %d   Soma: %.0f MiB   Média: %.1f MiB\n", n, s/1024, s/1024/n
}'

Para comparar, o mesmo pool visto através do RSS:

ps -eo pid,rss,args --sort=-rss | grep '[p]hp-fpm' | head

Conforme o tamanho do OPcache, a soma dos RSS fica bastante mais alta. Quem faz as contas com ela acaba com um pm.max_children demasiado pequeno e compra tempo de espera que não era preciso.

Há duas condições que decidem se a medição vale alguma coisa. Meça sob carga real, não logo a seguir a um reinício: um processo acabado de criar é barato, porque no início limita-se a partilhar as páginas de memória do processo principal, e só fica caro com os primeiros pedidos. E não se fique apenas pela média: se a média estiver nos 60 MiB e o maior processo nos 190 MiB, não faça as contas com 60.

A segunda fonte: o log de acessos do FPM

Se lho pedir, o FPM regista por cada pedido o consumo de pico e o tempo de execução. As duas linhas estão comentadas no ficheiro do pool:

access.log = /var/log/php8.2-fpm.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

A grafia %{mili}d está correta assim mesmo, vem sem alterações do ficheiro que acompanha o pacote e devolve o tempo de execução em milissegundos, enquanto %{kilo}M dá o consumo de pico em kilobytes. A seguir verifique, recarregue e veja se o ficheiro é criado:

php-fpm8.2 -t
systemctl reload php8.2-fpm
ls -l /var/log/php8.2-fpm.access.log

Deixe o log a correr um dia inteiro, para que as horas de ponta fiquem lá dentro. Depois avalie o consumo de pico em quantis:

awk '{print $(NF-1)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Pedidos: %d   Mediana: %d kB   p95: %d kB   Máximo: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

A mesma avaliação para o tempo de execução, de que vai precisar já a seguir para a segunda conta, vai buscar a coluna anterior:

awk '{print $(NF-2)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Mediana: %.0f ms   p95: %.0f ms   Máximo: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'

Duas ressalvas. Primeira: as posições dos campos dependem da linha de formato mostrada acima, porque essa linha termina com o tempo de execução, a memória e a percentagem de CPU. Quem alterar o access.format tem de as ajustar. Segunda: o %{kilo}M é a contabilidade de memória do próprio PHP e, por isso, um limite inferior: falta ali o código do programa, as extensões e a memória que a biblioteca C não devolve logo a seguir a um pedido. Para a fórmula vale portanto o PSS. O log de acessos serve para encontrar os scripts fora da norma, aqueles que fazem um processo crescer de forma duradoura.

Volte a desligar o log a seguir, ou configure-lhe uma rotação. A regra que vem com o pacote abrange apenas o log de erros, e um disco cheio produz sintomas que já não têm nada que ver com o PHP-FPM: disco cheio no Linux, encontrar e libertar espaço.

A conta

Há dois números: um limite máximo que vem da memória RAM e uma necessidade que vem da carga. O valor certo é o mais pequeno dos dois.

Limite máximo a partir da memória RAM

pm.max_children = orçamento para PHP  /  PSS por processo de trabalho

O orçamento não é a memória RAM toda:

free -m

A coluna available já tem em conta a cache que pode ser libertada, mas não inclui a memória que os seus processos de trabalho estão a ocupar nesse momento. O orçamento é, portanto, o available mais a soma de PSS medida, menos uma reserva. Para a reserva, cerca de 20% da memória total tem dado bons resultados, com um mínimo de 512 MB. É ela que absorve aquilo que não aparece em medição nenhuma: um buffer pool da base de dados que cresce, um backup, uma atualização de pacotes, uma importação em massa lançada na pior altura.

RAM totalOcupado de forma permanenteReservaOrçamento para PHPPSS por processopm.max_children
4 GB1,5 GB0,8 GB1,7 GB60 MB29
8 GB3,0 GB1,6 GB3,4 GB80 MB43
16 GB6,0 GB3,2 GB6,8 GB110 MB63

Arredonde sempre para baixo. Um processo a mais não rende nada, um processo a mais do que o servidor suporta pode custar tudo.

Necessidade a partir da carga

processos necessários = pedidos por segundo  ×  tempo médio de execução em segundos

Os pedidos por segundo vêm da página de estado do FPM: accepted conn a dividir por start since dá a média desde o último arranque. O tempo de execução vem da avaliação acima, e é o valor p95, não a mediana, senão estará a planear para a tarde calma.

Exemplo: 40 pedidos por segundo no pico, tempo p95 de 300 milissegundos, dá 12 pedidos em simultâneo e, com uma margem para picos curtos, uns 20 a 25. Se o limite máximo estiver nos 43, escreva o valor da necessidade e deixe a memória restante onde ela rende mais, ou seja, na cache da base de dados.

Se a necessidade ficar acima do limite máximo, mesmo assim não a escreva. Nesse caso, ou a memória RAM é pequena de mais, ou os scripts são lentos de mais, ou passam pelo PHP pedidos que podiam ser servidos como ficheiro estático ou a partir de uma cache. As três causas têm solução, um valor demasiado alto não tem: limita-se a adiar o momento da falha.

dynamic, ondemand ou static

O modo de funcionamento define quando é que os processos nascem e desaparecem. O limite máximo pm.max_children aplica-se nos três casos.

Modo de funcionamentoProcessos no arranqueComportamentoMemóriaAdequado a
dynamicpm.start_serversmantém entre min_spare e max_spare processos parados à disposição e cria mais, até ao max_children, quando é precisooscila com a cargaao caso normal: um a poucos pools, com carga variável
ondemandnenhumsó cria um processo quando chega um pedido e volta a terminá-lo passado o pm.process_idle_timeouta mais baixa em repousoa muitos pools num servidor, a sites com pouco tráfego
staticpm.max_childrenexatamente esse número, de forma permanente, sem criar nem terminar processos durante o serviçoconstante, sempre no máximoa um pool em hardware reservado para isso, com carga uniforme

dynamic é a predefinição e é a escolha certa para o caso normal. Custa algum tempo de CPU na criação dos processos e exige que os quatro valores encaixem uns nos outros.

ondemand poupa memória de forma sensível em repouso, quando há uma dúzia de pools de sites pouco visitados na mesma máquina. O preço é o primeiro pedido depois de uma fase de sossego, que tem de esperar pela criação do processo. O pm.process_idle_timeout controla quanto tempo sobrevive um processo parado (dez segundos de origem) e atua exclusivamente neste modo de funcionamento. Repare ainda: aqui o aviso de saturação diz max_children sem o prefixo pm.. Quem procurar pela formulação habitual não encontra nada.

static é honesto: aquilo que escrever fica ocupado de imediato e de forma permanente, e em troca deixa de haver surpresas sob carga. Faz sentido quando o PHP é o principal consumidor da máquina. Se o PHP partilhar a memória RAM com uma base de dados, o static tira a esta a possibilidade de manter mais cache durante algum tempo.

Independentemente do modo de funcionamento, vale a pena olhar para o pm.max_requests, 0 de origem e portanto sem limite. Um valor como 500 substitui cada processo de trabalho ao fim de 500 pedidos. Contra um consumo de memória que cresce devagar por causa de uma extensão mal feita, isto é eficaz e barato, porque o OPcache continua partilhado e não é reconstruído. Mas não ponha o valor a 20, senão o FPM passa o tempo a criar processos.

start_servers e os valores spare

Estes três valores só atuam com pm = dynamic e decidem com que rapidez o FPM reage a um pico de carga:

  • pm.min_spare_servers: é o número mínimo de processos parados que o FPM mantém à disposição, a almofada para os picos. Baixo de mais significa que cada pico começa por esperar que os processos sejam criados.
  • pm.max_spare_servers: é o número máximo de processos parados que o FPM tolera. Sem este limite, o FPM ficaria com todos os processos depois de um pico e, com eles, com a memória que ocupam.
  • pm.start_servers: é o número de processos que existem imediatamente a seguir ao arranque.

No arranque, o FPM verifica quatro condições e recusa o serviço se uma delas for violada: os dois valores spare têm de ser maiores do que zero, nenhum deles pode ser maior do que pm.max_children, o max_spare não pode ser menor do que o min_spare e o start_servers tem de ficar entre os dois. Se o pm.start_servers faltar por completo, o FPM calcula-o sozinho e regista:

NOTICE: [pool www] pm.start_servers is not set. It's been set to 3.

A fórmula para isso é min_spare + (max_spare - min_spare) / 2, ou seja, o meio entre os dois valores spare. Como ponto de partida, na prática funciona:

pm.max_children       = 40
pm.start_servers      = 10
pm.min_spare_servers  = 6
pm.max_spare_servers  = 16

Fácil de esquecer: também os processos parados ocupam memória. O orçamento tem de aguentar o pm.max_children, mas o consumo do dia a dia corresponde mais ou menos ao max_spare mais os processos ativos. Um max_spare alto mantém a memória ocupada também às três da manhã.

A relação com o swap

É tentador incluir o swap na conta. Não o faça. Um processo de trabalho cujos dados estão no disco responde ordens de grandeza mais devagar e fica, por isso, ocupado mais tempo. O número de pedidos em simultâneo sobe, o FPM cria mais processos e estes empurram ainda mais memória para fora. É esta realimentação que explica por que motivo um servidor sobrelotado não vai piorando aos poucos, cai em poucos minutos.

O que o swap faz mesmo assim é servir de almofada para o caso de erro. Sem ele, uma sobrelotação acaba de forma abrupta, com o kernel a terminar um processo. Com ele, fica primeiro com um servidor lento e, com isso, com uma janela de tempo para intervir. A regra é: swap sim, mas o pm.max_children calcula-se exclusivamente contra a memória RAM real, nunca contra a soma de memória RAM e swap.

Se já está a paginar para o swap, vê-o em dois sítios. O free -m mostra a ocupação, mas não diz nada sobre a atividade, porque uma página que foi para o swap uma vez e nunca mais foi precisa é inofensiva. Elucidativas são as colunas si e so, ou seja, a entrada e a saída de páginas por segundo:

free -m
vmstat 1 5

Valores permanentemente acima de zero significam que o servidor está a trabalhar contra o disco. Se o seu kernel trouxer o indicador de pressão de memória, ele é ainda mais direto, porque não diz quanto foi para o swap, diz quanto tempo os processos tiveram de esperar por causa disso:

cat /proc/pressure/memory

Se o ficheiro não existir, a função está desligada no kernel e fica-se pelo vmstat. Como criar swap, registá-lo de forma permanente e acertar o vm.swappiness, está no artigo criar swap e evitar falhas por falta de memória.

Valor demasiado alto: o OOM killer em vez de uma fila de espera

Suponha que escreve 200 para que o aviso desapareça de uma vez. Em repouso não acontece nada, a página carrega, tudo parece resolvido. Na avalanche seguinte, o FPM cria mesmo até 200 processos, cada um cresce até ao seu tamanho real com os primeiros pedidos, a memória livre desce, o kernel começa por deitar fora a cache de ficheiros (o que torna a base de dados mais lenta e faz as consultas demorar mais), depois começa a paginar para o swap e depois entra o OOM killer.

Este escolhe a vítima pelo consumo de memória, e o maior processo isolado num servidor web não é um processo de trabalho do PHP com 80 MB, é a base de dados com o seu buffer pool:

dmesg -T | grep -iE "out of memory|oom-kill"
journalctl -k --since "24 hours ago" | grep -i "out of memory"

As duas linhas que interessam têm este aspeto:

php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Out of memory: Killed process 1234 (mariadbd) total-vm:2891234kB, anon-rss:1783456kB,
file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:4096kB oom_score_adj:0

Aqui, quem desencadeia e quem é atingido são dois processos diferentes, e é precisamente isso que torna o diagnóstico desagradável: na caixa de correio está um aviso sobre uma base de dados que caiu, e ninguém se lembra da configuração do PHP de anteontem. Conforme a base de dados, entre parênteses aparece mariadbd ou mysqld.

Se acabar mesmo por atingir um processo de trabalho, o próprio FPM comunica-o e, com o limite de memória definido, fica ainda registado no journal:

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start
php8.2-fpm.service: A process of this unit has been killed by the OOM killer.

A diferença entre estes dois sintomas é o verdadeiro ponto deste artigo. Um pm.max_children demasiado baixo produz uma fila de espera: mensurável, documentada no log, atribuível a um valor concreto, e o servidor continua a responder. Um valor demasiado alto produz um processo terminado num sítio que não escolheu, num serviço que, conforme os casos, não volta por si próprio. Tempo de espera é um estado de funcionamento, um evento OOM é um incidente.

Erros frequentes e soluções

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: num dado momento, todos os processos de trabalho estavam ocupados. A linha aparece uma vez por fase de saturação, ou seja, uma única linha pode significar uma hora inteira de saturação. Só aumente o valor depois de ter medido o PSS e determinado o orçamento.

WARNING: [pool www] server reached max_children setting (5), consider raising it: a mesma situação com pm = ondemand, na formulação sem o prefixo pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, seguido de ERROR: failed to post process the configuration e de ERROR: FPM initialization failed: é o caso mais frequente quando se baixa o pm.max_children. Quem passa de 50 para 8 e deixa ficar pm.max_spare_servers = 20 tem a seguir um serviço que já não arranca. Os quatro valores mudam-se em conjunto.

ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: o pm.start_servers está fora do intervalo. Corrija o valor ou remova a linha, e nesse caso o FPM calcula-o sozinho.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): isto é o memory_limit e não tem nada que ver com o pm.max_children. Um único script pediu mais do que o PHP permite por pedido. Um pm.max_children mais alto não muda nada nisso e, ao contrário, um memory_limit mais baixo não reduz a memória de que um processo de trabalho precisa, apenas interrompe os scripts mais cedo. Para o planeamento vale mesmo assim: o memory_limit é o teto daquilo que um processo pode pedir.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream no log de erros do nginx: todos os processos ocupados e, ainda por cima, a fila de aceitação cheia. Um listen.backlog maior apenas adia o problema, a causa está no número de processos ou no tempo de execução dos scripts.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: o processo foi terminado de fora à força, quase sempre pelo OOM killer. Aqui o valor está demasiado alto, não demasiado baixo. Confirme no log do kernel.

"Aumentei o valor e não muda nada." Quase sempre foi editado o ficheiro errado: uma segunda versão do PHP em /etc/php/, um segundo pool ou uma cópia dentro de pool.d/ que nem sequer é lida. O que conta é qual o socket que o nginx contacta e qual o pool que está à escuta nele:

grep -Rn "fastcgi_pass" /etc/nginx/
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

"Depois do reload o site desapareceu." Um reload reinicia o FPM com a nova configuração e, se esta for inválida, o processo principal termina. Reponha a cópia de /root/backups e habitue-se a correr php-fpm8.2 -t antes de cada reload.

Como saber que o valor está certo

"O aviso desapareceu" não é prova nenhuma. Com o valor 500 também desaparece, até ao primeiro evento OOM. Sólidas são cinco verificações.

Primeira, a página de estado do FPM. Escreva pm.status_path = /status no ficheiro do pool, verifique e recarregue, e consulte o socket diretamente, passando totalmente ao lado do nginx. Assim a página não fica acessível a partir da internet:

apt-get install -y libfcgi-bin
SCRIPT_NAME=/status SCRIPT_FILENAME=/status REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock

Quatro linhas da saída contêm a resposta:

CampoSignificadoValor pretendido
max children reachedquantas vezes o limite máximo foi atingido desde o arranque0
max listen queuea fila de espera mais longa observada no socket0
max active processeso maior número de processos ativos em simultâneobastante abaixo do pm.max_children
slow requestspedidos acima do request_slowlog_timeout0, de preferência

Isto só passa a ser elucidativo ao fim de uma semana completa, com todas as horas de ponta lá dentro. Se depois disso o max active processes estiver em 12 enquanto o pm.max_children está em 43, tem folga e pode usar a reserva noutro sítio.

Segunda, a memória sob carga, não de noite, mas no pico. O available tem de ficar bastante acima de zero e as colunas si e so devem estar a zero:

free -m
vmstat 1 5

Terceira: nenhum evento OOM. Esta verificação é a mais importante, porque exclui o pior dos sintomas. Uma saída vazia é o resultado desejado:

journalctl -k --since "7 days ago" | grep -i "out of memory"

Quarta, a soma de todos os pools. Num servidor com vários sites não conta o valor isolado, conta a soma, multiplicada pelo PSS por processo. Essa soma tem de caber no orçamento, mesmo que todos os pools fiquem sob carga ao mesmo tempo:

php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Quinta: aguenta um reinício. É o ponto que mais vezes se salta. Um valor que está num ficheiro que nem sequer é lido só dá nas vistas no reinício seguinte, e esse raramente acontece no momento em que se está a olhar:

systemctl is-enabled php8.2-fpm
systemctl restart php8.2-fpm
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Reinicie a seguir o servidor uma vez de forma controlada, enquanto ainda está a acompanhar. Nos servidores root KVM e nos servidores dedicados da KernelHost, esse reinício é despoletado na área de cliente e o arranque acompanha-se pela consola VNC, mesmo que o serviço web ainda não responda. Os servidores estão no datacenter maincubes em Frankfurt am Main.

Lista de verificação rápida

  1. Cópia do ficheiro do pool para /root/backups, e acesso pela consola VNC experimentado uma vez.
  2. Medir o PSS por processo de trabalho sob carga real, não a seguir a um reinício, e não fazer as contas com RSS.
  3. Determinar o orçamento: available mais a soma de PSS em curso, menos uma reserva definida de forma consciente.
  4. Calcular o limite máximo, contrapor a necessidade a partir dos pedidos por segundo vezes o tempo p95, ficar com o valor mais pequeno e arredondar para baixo.
  5. Escolher o modo de funcionamento e ajustar os valores spare em conjunto com o pm.max_children.
  6. php-fpm8.2 -t, depois systemctl reload, depois php-fpm8.2 -tt como prova de que o valor está carregado.
  7. Uma semana depois, verificar o max children reached, o max listen queue e o log do kernel.

Perguntas frequentes

Como calculo corretamente o pm.max_children?
Através de dois números, e ganha o mais pequeno. O limite máximo é o orçamento para PHP a dividir pela memória proporcional (PSS) de um processo de trabalho. O orçamento é a coluna available do free -m, mais a memória que os processos de trabalho em execução estão a ocupar nesse momento, menos uma reserva de cerca de 20% da memória total. O segundo número é a necessidade: pedidos por segundo no pico multiplicados pelo tempo de execução p95 em segundos. Se o limite máximo der 43 e a necessidade 22, escreva 22 e deixe o resto da memória para a base de dados. Arredonde sempre para baixo.
Porque é que um valor demasiado alto é mais perigoso do que um demasiado baixo?
Um valor demasiado baixo produz uma fila de espera. Os pedidos ficam à espera na fila de aceitação do socket, a página fica mais lenta, o servidor continua a responder e o log diz exatamente o que se passa. Um valor demasiado alto produz falta de memória sob carga, e nessa altura é o kernel que escolhe sozinho uma vítima, e escolhe-a pelo consumo de memória. O maior processo num servidor web é normalmente a base de dados com o seu buffer pool, não um processo de trabalho do PHP. Está portanto a trocar tempo de espera mensurável pela falha não anunciada de outro serviço.
Porque é que devo fazer as contas com PSS e não com RSS?
O RSS inclui também as zonas de memória partilhadas, sobretudo o OPcache e as bibliotecas partilhadas. Quem soma o RSS de 20 processos de trabalho conta o mesmo OPcache vinte vezes e chega a um consumo de memória muito exagerado, ou seja, a um pm.max_children desnecessariamente pequeno. O PSS divide as páginas partilhadas pelo número dos seus utilizadores e pode, por isso, ser somado corretamente. O valor está, como root, em /proc/PID/smaps_rollup, na linha Pss.
Quando uso dynamic, quando ondemand e quando static?
dynamic é a predefinição e é a escolha certa para o caso normal: um a poucos pools com carga variável. ondemand compensa quando há muitos pools de sites pouco visitados na mesma máquina, porque em repouso não existe lá processo nenhum. O preço é o primeiro pedido depois de uma fase de sossego, que tem de esperar pela criação de um processo. static assenta quando o PHP é o principal consumidor da máquina e a carga é uniforme: a memória fica ocupada de imediato e de forma permanente, e em troca deixa de haver surpresas sob carga. Se o PHP partilhar o servidor com uma base de dados, o static tira a esta a possibilidade de manter mais cache durante algum tempo.
Posso incluir o swap na conta?
Não. Um processo de trabalho cujos dados estão no disco responde ordens de grandeza mais devagar e fica, por isso, ocupado mais tempo. O número de pedidos em simultâneo sobe, o FPM cria mais processos e estes empurram ainda mais memória para fora. É por isso que um servidor sobrelotado cai em poucos minutos, em vez de ir ficando lento aos poucos. O swap continua a fazer sentido, mas como almofada para o caso de erro: dá uma janela de tempo para intervir antes de o kernel terminar um processo. As contas fazem-se exclusivamente contra a memória RAM real.
Aumentei o pm.max_children e não muda nada. A que se deve?
Quase sempre foi editado o ficheiro errado. Em servidores com várias versões do PHP em /etc/php/ ou com vários pools, só conta o pool cujo socket o nginx contacta de facto. Compare o fastcgi_pass da configuração do nginx com as linhas listen dos ficheiros dos pools. Aquilo que o FPM carregou realmente mostra-o o php-fpm8.2 -tt, cuja saída contém a configuração completamente resolvida, incluindo todos os valores predefinidos.
Depois de baixar o pm.max_children, o PHP-FPM já não arranca. O que aconteceu?
Provavelmente os valores spare continuam nos números antigos, mais altos. O FPM recusa o arranque com a mensagem de que pm.min_spare_servers e pm.max_spare_servers não podem ser maiores do que pm.max_children, seguida de FPM initialization failed. Ajuste os quatro valores em conjunto: os dois valores spare têm de ser maiores do que zero e, no máximo, iguais ao pm.max_children, o max_spare não pode ser menor do que o min_spare e o pm.start_servers tem de ficar entre os dois.
O memory_limit tem alguma coisa que ver com o pm.max_children?
Só indiretamente. O memory_limit é o limite máximo que o PHP impõe por pedido, 128M de origem no modo FPM. A mensagem sobre memória esgotada (Allowed memory size exhausted) diz respeito a um único script e não melhora com um pm.max_children mais alto. Ao contrário, um memory_limit mais baixo não reduz a memória de que um processo de trabalho precisa, apenas interrompe os scripts mais cedo. A relação está no planeamento: quem puser o memory_limit em 1024M tem de contar, no pior dos casos, também com esse tamanho por processo.

PHP-FPM pm.max_children Memória RAM Debian Ubuntu nginx OOM killer Otimização de servidores