Calcolare pm.max_children di PHP-FPM invece di tirare a indovinare

Pubblicato il 22 min di lettura

Il messaggio server reached pm.max_children non significa che devi raddoppiare il valore. Misura, calcola, scegli la modalità e dimostra poi che il valore regge davvero.

Prima o poi nel log di PHP-FPM compare questa riga, che porta con sé anche il consiglio:

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

Il consiglio non è sbagliato, è solo incompleto. pm.max_children è l'unico parametro di PHP-FPM in cui un valore troppo alto è più pericoloso di uno troppo basso. Troppo basso costa tempo di attesa e, nel caso estremo, un 502. Troppo alto costa la RAM dell'intero server, e a quel punto è il kernel a scegliere quale processo terminare. Per esperienza non tocca a PHP, ma al database.

Questa guida mostra il percorso che porta dall'indovinare al calcolare: misurare il fabbisogno di memoria di un processo di lavoro, stabilire il budget, scegliere la modalità e dimostrare poi che il valore è quello giusto.

Tutte le indicazioni valgono per Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS e Ubuntu 22.04 LTS. I comandi sono scritti per l'uso come root; se lavori come utente normale, anteponi sudo. Sostituisci ovunque il numero di versione di PHP con quello del tuo sistema.

SistemaPHPServizioFile del poolLog di 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

Quali versioni siano installate lo mostra un'occhiata alla directory. Sui server che hanno alle spalle un upgrade di distribuzione spesso sono due:

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

Che cosa limita davvero pm.max_children

Il valore non limita i visitatori e nemmeno le connessioni, ma il numero di richieste PHP che nello stesso istante vengono eseguite. Una richiesta da 80 millisecondi occupa un processo di lavoro per 80 millisecondi e poi lo libera. Da qui discende quasi tutto il resto:

  • Molto traffico richiede pochi processi, finché gli script sono veloci. 40 richieste al secondo da 80 millisecondi ciascuna danno poco più di tre richieste simultanee, non 40.
  • Un solo punto lento ribalta il calcolo. Una chiamata a un'interfaccia di programmazione esterna senza un termine proprio tiene occupato un processo per cinque secondi, anche se questo non sta facendo nulla. Cinque chiamate del genere al secondo impegnano stabilmente 25 processi.
  • Le connessioni in attesa non occupano alcun processo. Una connessione keep-alive verso nginx costa un descrittore di file, non un processo di lavoro.

Se tutti i processi sono occupati, le nuove richieste attendono nella coda di accettazione del socket. La sua dimensione è indicata nel file del pool sotto listen.backlog, di serie 511, e il kernel la limita in più tramite net.core.somaxconn, che su tutti e quattro i sistemi vale 4096:

sysctl net.core.somaxconn

Finché la coda basta, il visitatore nota soltanto tempi di caricamento più lunghi. Quando si riempie anche quella, il kernel rifiuta la connessione, nginx registra 11: Resource temporarily unavailable e al visitatore arriva un 502. Per distinguerlo dalle altre cause: risolvere l'errore nginx 502 Bad Gateway.

Un dettaglio che genera parecchia confusione: l'avviso compare una volta per fase di saturazione, non per ogni richiesta coinvolta. FPM imposta internamente un contrassegno e lo cancella solo quando torna libero un processo. Una singola riga può quindi rappresentare un'intera ora di punta. Chi conta le righe sottovaluta il problema. La misura onesta è il contatore max children reached nella pagina di stato, che viene richiamata più avanti.

La via del ritorno, prima di cambiare qualcosa

Qui due cose possono andare storte, ed entrambe colpiscono il servizio in produzione.

Primo: FPM non si avvia più. Un systemctl reload invia al processo principale il segnale USR2, dopodiché questo si riavvia da solo con la nuova configurazione. Se la configurazione non è valida, l'inizializzazione si interrompe e il processo principale termina. Un'istanza in esecuzione diventa così un'istanza morta. Per questo, senza eccezioni, prima si verifica e poi si ricarica:

php-fpm8.2 -t

Se tutto è in ordine, l'output termina con test is successful.

Secondo: il valore è troppo alto. In quel caso il server non si rompe subito, ma al prossimo picco di carico, e in modo così radicale che anche un accesso SSH si blocca o non si stabilisce affatto. Per questo ti serve una seconda via d'ingresso al server.

Sui server root KVM e sui server dedicati di KernelHost è la console VNC nell'area clienti. È agganciata al livello di virtualizzazione, o al collegamento stesso, e resta raggiungibile anche quando il servizio SSH non risponde più per mancanza di memoria. Accedi prima almeno una volta per quella strada. Una via di salvataggio provata per la prima volta durante l'emergenza non è una via di salvataggio.

Crea inoltre una copia del file del pool, con marca temporale, in modo che un secondo tentativo non sovrascriva la prima copia:

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/

La via del ritorno è poi lunga tre righe:

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

Un parapetto per il caso in cui sbagli i conti

Un limite di memoria sull'unità systemd fa sì che una stima sbagliata colpisca un processo di lavoro PHP e non il database: il kernel termina allora un processo all'interno della control group di FPM, invece di cercarsi il più grande dell'intero 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

Verifica che sia stato recepito:

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

MemoryHigh frena e libera memoria, MemoryMax è il limite rigido. Tutti e quattro i sistemi usano la gerarchia unificata delle control group, quindi i valori hanno effetto immediato. Questo è un parapetto, non un sostituto del calcolo: al raggiungimento del limite il visitatore coinvolto vede comunque un errore, però non cade l'intero server. Per collocare in modo pulito questi file aggiuntivi: creare un servizio systemd personalizzato.

E la terza regola, quella che non richiede alcun comando: una modifica per volta. Chi tocca insieme la modalità, pm.max_children e i valori spare, poi non sa che cosa abbia funzionato.

Ricognizione: quale pool conta davvero

Ogni pool ha il proprio pm.max_children, e per la memoria conta la somma su tutti i pool:

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

Nella configurazione predefinita su tutti e quattro i sistemi c'è la stessa cosa:

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

Non sono raccomandazioni, ma segnaposto che permettono a PHP di avviarsi anche su una macchina di prova molto piccola. A questo si aggiunge un limite massimo globale valido per tutti i pool:

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

Se l'output resta vuoto, la riga è commentata e vale il valore predefinito 0, cioè nessun limite globale. Sui server con molti pool è proprio questa riga a impedire che la somma faccia saltare la memoria non appena più pool si trovano sotto carico contemporaneamente.

Alla fine non conta il file, ma quello che FPM ne ricava. L'opzione -tt stampa la configurazione completamente risolta, con tutti i valori predefiniti che non compaiono in nessun file:

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

Più avanti questo output sarà anche la prova che una modifica è stata recepita.

Misurare il fabbisogno di memoria di un processo di lavoro

È qui che la maggior parte delle guide diventa imprecisa. Tre numeri vengono regolarmente confusi:

  • memory_limit, di serie 128M in modalità FPM: un tetto massimo per richiesta, non un consumo. Uno script che ha bisogno di 12 MB continua a usarne 12 anche con 512M.
  • RSS: tutto ciò che in quel momento si trova nella RAM, compresi l'opcache condiviso e le librerie condivise. Chi somma l'RSS di 20 processi conta lo stesso opcache venti volte.
  • PSS: le pagine condivise vengono divise per il numero di processi che le usano. È l'unico dei tre numeri che si possa sommare con senso.

Per il calcolo ti serve il PSS. Il kernel lo fornisce già aggregato in /proc/<pid>/smaps_rollup, leggibile come 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 "nessun processo di lavoro trovato"; exit }
  printf "Processi: %d   Somma: %.0f MiB   Media: %.1f MiB\n", n, s/1024, s/1024/n
}'

Per confronto, lo stesso pool misurato con l'RSS:

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

La somma dell'RSS risulta nettamente più alta, a seconda della dimensione dell'opcache. Chi la usa per i conti ottiene un pm.max_children troppo piccolo e si compra tempo di attesa di cui non ci sarebbe bisogno.

Due condizioni decidono se la misura vale qualcosa. Misura sotto carico reale, non dopo un riavvio: un processo appena generato costa poco, perché all'inizio si limita a condividere le pagine di memoria del processo principale, e diventa costoso solo con le prime richieste. E non fermarti al valore medio: se la media è 60 MiB e il processo più grande arriva a 190 MiB, non fare i conti con 60.

La seconda fonte: il log degli accessi di FPM

Su richiesta FPM registra per ogni richiesta il consumo di picco e il tempo di esecuzione. Le due righe si trovano commentate nel file del 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%%"

La grafia %{mili}d è corretta così, arriva invariata dal file fornito con il pacchetto e restituisce il tempo di esecuzione in millisecondi, mentre %{kilo}M dà il consumo di picco in kilobyte. Poi verifica, ricarica e controlla che il file venga creato:

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

Lascia girare il log per un giorno intero, così da includere le ore di punta. Poi analizza il consumo di picco per quantili:

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 "Richieste: %d   Mediana: %d kB   p95: %d kB   Massimo: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

La stessa analisi per il tempo di esecuzione, che ti servirà subito dopo per il secondo calcolo, prende la colonna precedente:

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   Massimo: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'

Due limitazioni. Primo, le posizioni dei campi dipendono dalla riga di formato mostrata sopra, perché questa termina con tempo di esecuzione, memoria e quota di CPU. Chi modifica access.format deve adattarle. Secondo, %{kilo}M è la contabilità di memoria di PHP stesso e quindi un limite inferiore: non comprende il codice del programma, le estensioni e la memoria che la libreria C non restituisce subito dopo una richiesta. Per la formula vale perciò il PSS. Il log degli accessi serve a trovare gli script anomali, quelli che tengono un processo grande in modo permanente.

Dopo, disattiva di nuovo il log oppure predisponi per esso una rotazione. La regola fornita di serie copre solo il log degli errori, e un disco pieno produce sintomi che con PHP-FPM non hanno più nulla a che vedere: disco pieno: trovare e liberare spazio su un server Linux.

Il calcolo

I numeri sono due: un limite massimo che deriva dalla RAM e un fabbisogno che deriva dal carico. Il valore giusto è il più piccolo dei due.

Il limite massimo dalla RAM

pm.max_children = budget per PHP  /  PSS per processo di lavoro

Il budget non è tutta la RAM:

free -m

La colonna available tiene già conto della cache liberabile, ma non comprende la memoria che i tuoi processi di lavoro stanno occupando in quel momento. Il budget è quindi available più la somma PSS misurata, meno una riserva. Per la riserva funziona bene circa il 20% della memoria totale, con un minimo di 512 MB. La riserva assorbe ciò che in nessuna misura compare: un buffer pool del database che cresce, un backup, un upgrade dei pacchetti, un'importazione lanciata nel momento sbagliato.

RAM totaleOccupata stabilmenteRiservaBudget per PHPPSS per 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

Arrotonda sempre per difetto. Un processo in più non porta nulla, un processo di troppo può costare tutto.

Il fabbisogno dal carico

processi necessari = richieste al secondo  ×  tempo medio di esecuzione in secondi

Le richieste al secondo le fornisce la pagina di stato di FPM: accepted conn diviso start since dà la media dall'ultimo avvio. Il tempo di esecuzione viene dall'analisi qui sopra, e serve il valore p95, non la mediana, altrimenti dimensioni per il pomeriggio tranquillo.

Un esempio: 40 richieste al secondo nel picco, tempo p95 di 300 millisecondi, fanno 12 richieste simultanee, e con un margine per le brevi impennate si arriva a circa 20-25. Se il limite massimo è 43, inserisci il valore del fabbisogno e lascia la memoria restante dove rende di più, cioè nella cache del database.

Se il fabbisogno risulta superiore al limite massimo, non inserirlo comunque. In quel caso o la RAM è troppo poca, o gli script sono troppo lenti, oppure passano per PHP richieste che potrebbero essere servite come file statico o da una cache. Tutte e tre le cause si possono risolvere, un valore troppo alto no: sposta soltanto il momento del guasto.

dynamic, ondemand o static

La modalità stabilisce quando i processi nascono e scompaiono. Il limite massimo pm.max_children vale in tutti e tre i casi.

ModalitàProcessi all'avvioComportamentoMemoriaAdatta a
dynamicpm.start_serverstiene pronti tra min_spare e max_spare processi inattivi, ne genera altri fino a max_children quando serveoscilla con il caricoal caso normale: da uno a pochi pool, carico variabile
ondemandnessunoavvia un processo solo quando arriva una richiesta e lo termina dopo pm.process_idle_timeouta riposo la più bassaa molti pool su un solo server, a siti con poco traffico
staticpm.max_childrenesattamente questo numero, in modo permanente, senza generare né terminare processi durante l'eserciziocostante al massimoa un solo pool su hardware dedicato allo scopo, con carico uniforme

dynamic è l'impostazione predefinita ed è quella giusta nel caso normale. Costa un po' di tempo di calcolo per generare i processi e richiede che i quattro valori siano coerenti fra loro.

ondemand a riposo fa risparmiare memoria in modo sensibile, quando su una macchina convivono una dozzina di pool per siti consultati di rado. Il prezzo è la prima richiesta dopo una fase di quiete, che deve aspettare la generazione del processo. pm.process_idle_timeout regola quanto sopravvive un processo inattivo (di serie dieci secondi) e agisce esclusivamente in questa modalità. Tieni presente inoltre che qui l'avviso di saturazione nomina max_children senza il prefisso pm.. Chi cerca la formulazione abituale non trova nulla.

static è onesta: quello che inserisci viene occupato subito e in modo permanente, in cambio sotto carico non ci sono più sorprese. Ha senso quando PHP è il consumatore principale della macchina. Se invece PHP condivide la RAM con un database, static toglie a quest'ultimo la possibilità di tenere temporaneamente più cache.

Indipendentemente dalla modalità vale la pena guardare pm.max_requests, di serie 0 e quindi senza limite. Un valore come 500 sostituisce ogni processo di lavoro dopo 500 richieste. Contro un consumo di memoria che cresce lentamente per colpa di un'estensione poco pulita è una misura efficace ed economica, perché l'opcache resta condiviso e non viene ricostruito. Non impostare però il valore su 20, altrimenti FPM passa il tempo a generare processi.

start_servers e i valori spare

Questi tre valori agiscono solo con pm = dynamic e decidono quanto in fretta FPM reagisce a un picco di carico:

  • pm.min_spare_servers: è il numero minimo di processi inattivi che FPM tiene pronti, il cuscinetto per le impennate. Troppo basso significa che ogni picco deve prima aspettare la generazione dei processi.
  • pm.max_spare_servers: è il numero massimo di processi inattivi che FPM tollera. Senza questo limite, dopo un picco FPM terrebbe tutti i processi e con essi la loro memoria.
  • pm.start_servers: è il numero di processi che esistono subito dopo l'avvio.

All'avvio FPM verifica quattro condizioni e rifiuta il servizio se una di esse viene violata: entrambi i valori spare devono essere maggiori di zero, nessuno dei due può superare pm.max_children, max_spare non può essere minore di min_spare e start_servers deve stare in mezzo. Se pm.start_servers manca del tutto, FPM calcola da sé e registra:

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

La formula è min_spare + (max_spare - min_spare) / 2, cioè il punto medio tra i due valori spare. Come punto di partenza nella pratica funziona questo:

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

Facile da trascurare: anche i processi inattivi occupano memoria. Il budget deve reggere pm.max_children, mentre il consumo di tutti i giorni corrisponde più o meno a max_spare più i processi attivi. Un max_spare alto tiene la memoria occupata anche alle tre di notte.

Il rapporto con lo swap

Viene la tentazione di includere lo swap nel calcolo. Non farlo. Un processo di lavoro i cui dati si trovano sul disco risponde più lentamente di ordini di grandezza e resta quindi occupato più a lungo. Il numero di richieste simultanee sale, FPM genera altri processi e questi spingono fuori altra memoria. Questa retroazione è il motivo per cui un server con la memoria sovrallocata non peggiora gradualmente, ma crolla nel giro di pochi minuti.

Quello che lo swap offre comunque è un cuscinetto per il caso di guasto. Senza di esso una sovrallocazione finisce di colpo con il kernel che termina un processo. Con lo swap ottieni prima un server lento e quindi una finestra di tempo per intervenire. La regola è questa: swap sì, ma pm.max_children si calcola esclusivamente sulla RAM reale, mai sulla somma di RAM e swap.

Se stai già scrivendo in swap lo vedi in due punti. free -m mostra l'occupazione, ma non dice nulla sull'attività, perché una pagina spostata una volta sola e mai più richiesta è innocua. Significative sono le colonne si e so, cioè le pagine lette dallo swap e scritte nello swap al secondo:

free -m
vmstat 1 5

Valori stabilmente sopra lo zero significano che il server sta lavorando contro il disco. Se il tuo kernel mette a disposizione l'indicatore di pressione, questo è ancora più diretto, perché non dice quanto è stato spostato in swap, ma per quanto tempo i processi hanno dovuto aspettare per quel motivo:

cat /proc/pressure/memory

Se il file non esiste, la funzione è disattivata nel kernel e ti resta vmstat. Come si crea uno swap, come lo si rende permanente e come si imposta vm.swappiness nel modo giusto è spiegato nell'articolo configurare lo swap ed evitare i crash per memoria esaurita.

Valore troppo alto: l'OOM killer al posto di una coda

Poniamo che tu inserisca 200, così l'avviso finalmente sparisce. A riposo non succede nulla, la pagina carica, sembra tutto risolto. Alla prossima ondata di traffico FPM genera davvero fino a 200 processi, ognuno cresce con le prime richieste fino alla sua dimensione reale, la memoria libera cala, il kernel butta via prima la cache dei file (per cui il database rallenta e le sue query durano di più), poi comincia a spostare pagine in swap, e infine entra in azione l'OOM killer.

Questo sceglie la vittima in base al consumo di memoria, e il singolo processo più grande su un server web non è un processo di lavoro PHP da 80 MB, ma il database con il suo buffer pool:

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

Le due righe che contano hanno questo aspetto:

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

Qui chi provoca e chi subisce sono due processi diversi, ed è proprio questo a rendere sgradevole la ricerca dell'errore: nella casella di posta arriva un avviso su un database andato in crash, e nessuno pensa alla configurazione PHP dell'altro ieri. A seconda del database, tra parentesi compare mariadbd oppure mysqld.

Se invece a essere colpito è un processo di lavoro, FPM lo segnala da sé, e con un limite di memoria impostato la cosa compare anche nel 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.

La differenza tra i due quadri di guasto è il punto vero di questo articolo. Un pm.max_children troppo basso produce una coda: misurabile, documentata nel log, riconducibile a un valore preciso, e il server resta raggiungibile. Uno troppo alto produce un processo terminato in un punto che non hai scelto tu, in un servizio che in certi casi non riparte da solo. Il tempo di attesa è uno stato di esercizio, un evento OOM è un incidente.

Errori frequenti e soluzioni

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: in un dato momento tutti i processi di lavoro erano occupati. La riga compare una volta per fase di saturazione, quindi una sola riga può significare un'ora di carico pieno. Aumenta il valore solo dopo aver misurato il PSS e stabilito il budget.

WARNING: [pool www] server reached max_children setting (5), consider raising it: la stessa situazione con pm = ondemand, nella formulazione senza il prefisso pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, seguito da ERROR: failed to post process the configuration e ERROR: FPM initialization failed: è il caso più frequente quando si abbassa pm.max_children. Chi passa da 50 a 8 e lascia pm.max_spare_servers = 20 si ritrova poi con un servizio che non parte più. I quattro valori vanno cambiati insieme.

ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: pm.start_servers si trova fuori dall'intervallo. Correggi il valore oppure rimuovi la riga, così FPM calcola da sé.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): questo è memory_limit e non ha nulla a che vedere con pm.max_children. Un singolo script ha richiesto più di quanto PHP consenta per richiesta. Un pm.max_children più alto non cambia nulla, e al contrario un memory_limit più basso non riduce il fabbisogno di memoria di un processo di lavoro, interrompe soltanto gli script prima. Per la pianificazione vale comunque questo: memory_limit è il tetto massimo di ciò che un processo può richiedere.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream nel log degli errori di nginx: tutti i processi occupati e in più la coda di accettazione piena. Un listen.backlog più alto sposta solo il problema, la causa sta nel numero di processi o nel tempo di esecuzione degli script.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: il processo è stato terminato con forza dall'esterno, di regola dall'OOM killer. Qui il valore è troppo alto, non troppo basso. Controprova nel log del kernel.

"Ho aumentato il valore e non cambia nulla." Quasi sempre è stato modificato il file sbagliato: una seconda versione di PHP sotto /etc/php/, un secondo pool oppure una copia in pool.d/ che non viene nemmeno letta. Conta quale socket contatta nginx e quale pool è in ascolto su di esso:

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"

"Dopo il reload il sito è sparito." Un reload riavvia FPM con la nuova configurazione e, se questa non è valida, il processo principale termina. Ripristina la copia da /root/backups e abituati a eseguire php-fpm8.2 -t prima di ogni reload.

Come riconosci che il valore è giusto

"L'avviso è sparito" non è una prova. Anche con il valore 500 sparisce, fino al primo evento OOM. Solide sono cinque verifiche.

Primo, la pagina di stato di FPM. Inserisci pm.status_path = /status nel file del pool, verifica e ricarica, poi interroga il socket direttamente, aggirando del tutto nginx. Così la pagina non è raggiungibile dalla rete:

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

La risposta sta in quattro righe dell'output:

CampoSignificatoValore obiettivo
max children reachedquante volte il limite massimo è stato raggiunto dall'avvio0
max listen queuecoda più lunga osservata sul socket0
max active processesnumero massimo di processi attivi contemporaneamentenettamente sotto pm.max_children
slow requestsrichieste oltre request_slowlog_timeoutpossibilmente 0

Il dato diventa significativo solo dopo una settimana intera, con tutte le ore di punta. Se poi max active processes resta a 12 mentre pm.max_children è a 43, hai margine e puoi impiegare la riserva altrove.

Secondo, la memoria sotto carico, non di notte ma nel picco. available deve restare nettamente sopra lo zero, le colonne si e so devono stare a zero:

free -m
vmstat 1 5

Terzo: nessun evento OOM. Questa verifica è la più importante, perché esclude il quadro di guasto peggiore. Un output vuoto è il risultato desiderato:

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

Quarto, la somma su tutti i pool. Su un server con più siti non conta il singolo valore, ma la somma, moltiplicata per il PSS di ogni processo. Deve rientrare nel budget anche quando tutti i pool si trovano sotto carico nello stesso momento:

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

Quinto: il valore sopravvive a un riavvio. È il punto che si salta più spesso. Un valore scritto in un file che non viene nemmeno letto salta fuori solo al riavvio successivo, e quello capita di rado proprio mentre stai guardando:

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

Riavvia poi il server una volta in modo controllato, finché sei ancora lì a guardare. Sui server root KVM e sui server dedicati di KernelHost questo riavvio lo lanci dall'area clienti e segui l'avvio dalla console VNC, anche quando il servizio web non risponde ancora. I server si trovano nel datacenter maincubes di Francoforte sul Meno.

Checklist rapida

  1. Copia del file del pool in /root/backups, accesso tramite la console VNC provato almeno una volta.
  2. Misurare il PSS per processo di lavoro sotto carico reale, non dopo un riavvio, e non fare i conti con l'RSS.
  3. Stabilire il budget: available più la somma PSS corrente, meno una riserva scelta consapevolmente.
  4. Calcolare il limite massimo, confrontarlo con il fabbisogno (richieste al secondo per tempo p95), prendere il valore più piccolo, arrotondare per difetto.
  5. Scegliere la modalità e adattare i valori spare insieme a pm.max_children.
  6. php-fpm8.2 -t, poi systemctl reload, poi php-fpm8.2 -tt come prova che il valore è stato caricato.
  7. Una settimana dopo, controllare max children reached, max listen queue e il log del kernel.

Domande frequenti

Come calcolo correttamente pm.max_children?
Attraverso due numeri, e vince il più piccolo. Il limite massimo è il budget per PHP diviso la memoria proporzionale (PSS) di un processo di lavoro. Il budget è la colonna available di free -m, più la memoria che i processi di lavoro attivi stanno occupando in quel momento, meno una riserva pari a circa il 20% della memoria totale. Il secondo numero è il fabbisogno: richieste al secondo nel picco moltiplicate per il tempo p95 in secondi. Se il limite massimo dà 43 e il fabbisogno 22, inserisci 22 e lascia il resto della memoria al database. Arrotonda sempre per difetto.
Perché un valore troppo alto è più pericoloso di uno troppo basso?
Un valore troppo basso produce una coda. Le richieste attendono nella coda di accettazione del socket, la pagina rallenta, il server resta raggiungibile e il log ti dice esattamente che cosa sta succedendo. Un valore troppo alto produce sotto carico una carenza di memoria, e allora è il kernel a cercarsi una vittima, per giunta in base al consumo di memoria. Il processo più grande su un server web è di solito il database con il suo buffer pool, non un processo di lavoro PHP. Stai quindi scambiando un tempo di attesa misurabile con il guasto improvviso di un altro servizio.
Perché devo fare i conti con il PSS e non con l'RSS?
L'RSS comprende anche le aree di memoria condivise, in primo luogo l'opcache e le librerie condivise. Chi somma l'RSS di 20 processi di lavoro conta lo stesso opcache venti volte e arriva a un fabbisogno di memoria enormemente sovrastimato, quindi a un pm.max_children inutilmente piccolo. Il PSS divide le pagine condivise per il numero di processi che le usano e si può perciò sommare correttamente. Il valore si legge come root in /proc/PID/smaps_rollup, nella riga Pss.
Quando scelgo dynamic, quando ondemand e quando static?
dynamic è l'impostazione predefinita ed è quella giusta nel caso normale: da uno a pochi pool con carico variabile. ondemand conviene quando su una macchina convivono molti pool per siti consultati di rado, perché a riposo lì non esiste alcun processo. Il prezzo è la prima richiesta dopo una fase di quiete, che deve aspettare la generazione di un processo. static va bene quando PHP è il consumatore principale della macchina e il carico è uniforme: la memoria viene occupata subito e in modo permanente, in cambio sotto carico non ci sono più sorprese. Se invece PHP condivide il server con un database, static toglie a quest'ultimo la possibilità di tenere temporaneamente più cache.
Posso includere lo swap nel calcolo?
No. Un processo di lavoro i cui dati si trovano sul disco risponde più lentamente di ordini di grandezza e resta quindi occupato più a lungo. Il numero di richieste simultanee sale, FPM genera altri processi e questi spingono fuori altra memoria. Per questo un server con la memoria sovrallocata crolla nel giro di pochi minuti invece di rallentare gradualmente. Lo swap resta comunque utile, ma come cuscinetto per il caso di guasto: ti procura una finestra di tempo per intervenire prima che il kernel termini un processo. Il calcolo si fa esclusivamente sulla RAM reale.
Ho aumentato pm.max_children e non cambia nulla. Da che cosa dipende?
Quasi sempre è stato modificato il file sbagliato. Sui server con più versioni di PHP sotto /etc/php/ o con più pool conta solo il pool il cui socket viene davvero contattato da nginx. Confronta fastcgi_pass della configurazione di nginx con le righe listen dei file dei pool. Quello che FPM ha effettivamente caricato lo mostra php-fpm8.2 -tt, il cui output contiene la configurazione completamente risolta, valori predefiniti compresi.
Dopo aver abbassato pm.max_children, PHP-FPM non parte più. Che cosa è successo?
Probabilmente i valori spare sono rimasti sui vecchi numeri, più alti. FPM rifiuta l'avvio con il messaggio che pm.min_spare_servers e pm.max_spare_servers non possono essere maggiori di pm.max_children, seguito da FPM initialization failed. Adatta i quattro valori insieme: entrambi i valori spare devono essere maggiori di zero e al massimo pari a pm.max_children, max_spare non può essere minore di min_spare e pm.start_servers deve stare tra i due.
memory_limit ha qualcosa a che vedere con pm.max_children?
Solo indirettamente. memory_limit è il tetto massimo che PHP impone per ogni richiesta, di serie 128M in modalità FPM. Il messaggio sulla memoria esaurita (Allowed memory size exhausted) riguarda un singolo script e non migliora con un pm.max_children più alto. Al contrario, un memory_limit più basso non riduce il fabbisogno di memoria di un processo di lavoro, interrompe soltanto gli script prima. Il collegamento sta nella pianificazione: chi imposta memory_limit su 1024M deve, nel caso peggiore, mettere in conto anche quella dimensione per ogni processo.

PHP-FPM pm.max_children RAM Debian Ubuntu nginx OOM killer Ottimizzazione del server