Calculer pm.max_children pour PHP-FPM au lieu de le deviner

Publié le 24 min de lecture

Le message server reached pm.max_children ne veut pas dire qu'il faut doubler la valeur. Mesurez, calculez, choisissez le mode de fonctionnement, puis prouvez que la valeur convient vraiment.

Cette ligne finit toujours par apparaître dans le journal de PHP-FPM, et elle livre son conseil au passage :

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

Le conseil n'est pas faux, il est seulement incomplet. pm.max_children est le seul réglage de PHP-FPM pour lequel une valeur trop élevée est plus dangereuse qu'une valeur trop basse. Trop bas, cela coûte du temps d'attente et, dans le pire des cas, une erreur 502. Trop haut, cela coûte la RAM du serveur entier, et c'est alors le noyau qui choisit lui-même le processus qu'il termine. L'expérience montre que ce n'est pas PHP, mais la base de données.

Ce guide montre le chemin qui mène de l'estimation au calcul : mesurer la mémoire consommée par un processus de travail, déterminer le budget, choisir le mode de fonctionnement, puis prouver que la valeur retenue est la bonne.

Toutes les indications valent pour Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS et Ubuntu 22.04 LTS. Les commandes sont écrites pour un shell root ; en tant qu'utilisateur normal, faites-les précéder de sudo. Remplacez partout le numéro de version de PHP par celui de votre système.

SystèmePHPServiceFichier de poolJournal 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

Un simple coup d'œil dans le répertoire indique quelles versions sont installées. Sur un serveur qui a connu une montée de version de la distribution, il y en a souvent deux :

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

Ce que pm.max_children limite réellement

Cette valeur ne limite ni les visiteurs ni les connexions, mais le nombre de requêtes PHP exécutées au même instant. Une requête de 80 millisecondes occupe un processus de travail pendant 80 millisecondes, puis le libère. Presque tout le reste en découle :

  • Un trafic important demande peu de processus tant que les scripts sont rapides. 40 requêtes par seconde à 80 millisecondes chacune donnent un peu plus de trois requêtes simultanées, pas 40.
  • Un seul point lent fait basculer le calcul. Un appel vers une interface de programmation externe sans délai d'expiration propre garde un processus occupé pendant cinq secondes, alors que celui-ci ne fait rien. Cinq appels de ce type par seconde immobilisent 25 processus en permanence.
  • Les connexions en attente n'occupent aucun processus. Une connexion keep-alive vers nginx coûte un descripteur de fichier, mais aucun processus de travail.

Lorsque tous les processus sont occupés, les nouvelles requêtes patientent dans la file d'attente d'acceptation du socket. Sa taille se règle dans le fichier de pool via listen.backlog, 511 par défaut, et le noyau la borne en plus par net.core.somaxconn, fixé à 4096 sur les quatre systèmes :

sysctl net.core.somaxconn

Tant que la file suffit, le visiteur ne constate que des temps de chargement plus longs. Quand elle est pleine à son tour, le noyau refuse la connexion, nginx journalise 11: Resource temporarily unavailable et le visiteur reçoit une erreur 502. Pour distinguer cette cause des autres : Résoudre l'erreur nginx 502 Bad Gateway.

Un détail qui sème beaucoup de confusion : l'avertissement apparaît une fois par phase de saturation, et non pour chaque requête concernée. FPM pose un indicateur interne et ne l'efface qu'au moment où un processus se libère. Une seule ligne peut donc représenter toute une heure de pointe. Compter les lignes revient à sous-estimer le problème. La mesure honnête, c'est le compteur max children reached de la page d'état, appelée plus bas.

Le chemin de retour, avant toute modification

Deux choses peuvent mal tourner ici, et toutes les deux touchent la production.

Premièrement : FPM ne démarre plus. Un systemctl reload envoie le signal USR2 au processus maître, qui se relance alors lui-même avec la nouvelle configuration. Si celle-ci est invalide, l'initialisation échoue et le processus maître s'arrête. Une instance qui tournait devient donc une instance morte. C'est pourquoi il faut toujours vérifier avant de recharger, sans aucune exception :

php-fpm8.2 -t

En cas de succès, la sortie se termine par test is successful.

Deuxièmement : la valeur est trop élevée. Le serveur ne tombe alors pas tout de suite, mais au prochain pic de charge, et de façon assez radicale pour qu'une connexion SSH reste bloquée ou n'aboutisse pas du tout. Il vous faut donc un second accès au serveur.

Sur les serveurs root KVM et les serveurs dédiés de KernelHost, c'est la console VNC de l'espace client. Elle est raccordée à la couche de virtualisation, ou directement au serveur lui-même, et reste accessible même quand le service SSH ne répond plus par manque de mémoire. Connectez-vous au préalable une fois par ce biais. Une voie de secours que l'on essaie pour la première fois en pleine urgence n'en est pas une.

Créez en plus une copie du fichier de pool, avec un horodatage, pour qu'une seconde tentative n'écrase pas la première copie :

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/

Le retour en arrière tient alors en trois lignes :

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 garde-fou au cas où votre calcul serait faux

Une limite de mémoire posée sur l'unité systemd fait qu'une erreur d'appréciation frappe un processus de travail PHP et non la base de données : le noyau met alors fin à un processus à l'intérieur du groupe de contrôle de FPM, au lieu de chercher le plus gros processus de tout le système.

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

Vérifier que le réglage est bien arrivé :

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

MemoryHigh freine et fait de la place, MemoryMax est la limite dure. Les quatre systèmes utilisent la hiérarchie unifiée des groupes de contrôle, les valeurs agissent donc immédiatement. C'est un garde-fou, pas un substitut au calcul : une fois la limite atteinte, le visiteur concerné voit toujours une erreur, mais le serveur entier n'est plus emporté. Pour ranger proprement ce genre de fichiers complémentaires : Créer son propre service systemd.

Et la troisième règle, celle qui ne demande aucune commande : une modification à la fois. Si vous touchez en même temps au mode de fonctionnement, à pm.max_children et aux valeurs spare, vous ne saurez pas ensuite ce qui a produit l'effet.

État des lieux : quel pool s'applique

Chaque pool possède son propre pm.max_children, et côté mémoire, c'est la somme sur tous les pools qui compte :

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

Dans l'état livré par la distribution, les quatre systèmes contiennent la même chose :

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

Ce ne sont pas des recommandations, mais des valeurs de remplissage qui permettent à PHP de démarrer même sur une très petite machine de test. À cela s'ajoute un plafond global, valable pour l'ensemble des pools :

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

Si la sortie reste vide, la ligne est commentée et la valeur par défaut 0 s'applique, c'est-à-dire aucune limite globale. Sur les serveurs qui hébergent beaucoup de pools, c'est précisément cette ligne qui empêche la somme de faire exploser la mémoire dès que plusieurs pools se retrouvent sous charge en même temps.

Ce qui fait foi au bout du compte, ce n'est pas le fichier, mais ce que FPM en fait. L'option -tt affiche la configuration entièrement résolue, y compris toutes les valeurs par défaut qui ne figurent dans aucun fichier :

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

Cette sortie servira plus tard de preuve qu'une modification est bien arrivée à destination.

Mesurer la mémoire consommée par un processus de travail

C'est ici que la plupart des guides deviennent imprécis. Trois chiffres sont régulièrement confondus :

  • memory_limit, 128M par défaut en mode FPM : un plafond par requête, pas une consommation. Un script qui a besoin de 12 MB n'utilise que 12 MB, même avec 512M.
  • RSS : tout ce qui se trouve à cet instant en mémoire, y compris l'opcache partagé et les bibliothèques partagées. Additionner le RSS de 20 processus revient à compter vingt fois le même opcache.
  • PSS : les pages partagées sont divisées par le nombre de leurs utilisateurs. C'est le seul des trois chiffres que l'on peut additionner de façon sensée.

Pour le calcul, c'est le PSS qu'il vous faut. Le noyau le fournit déjà agrégé dans /proc/<pid>/smaps_rollup, lisible en tant que 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 "aucun processus de travail trouvé"; exit }
  printf "Processus : %d   Somme : %.0f MiB   Moyenne : %.1f MiB\n", n, s/1024, s/1024/n
}'

À titre de comparaison, le même pool vu à travers le RSS :

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

Selon la taille de l'opcache, la somme des RSS est nettement plus élevée. Calculer avec elle donne un pm.max_children trop petit et vous fait payer un temps d'attente dont vous n'aviez pas besoin.

Deux conditions déterminent si la mesure vaut quelque chose. Mesurez sous charge réelle, pas juste après un redémarrage : un processus fraîchement créé ne coûte presque rien, parce qu'il ne fait d'abord que partager les pages mémoire du processus maître, et il ne devient coûteux qu'avec les premières requêtes. Et ne vous contentez pas de la moyenne : si la moyenne est à 60 MiB et le plus gros processus à 190 MiB, ne calculez pas avec 60.

La deuxième source : le journal d'accès de FPM

Sur demande, FPM journalise pour chaque requête la consommation de pointe et la durée d'exécution. Les deux lignes figurent, commentées, dans le fichier de 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%%"

L'écriture %{mili}d est correcte telle quelle, elle provient sans modification du fichier livré et donne la durée d'exécution en millisecondes, %{kilo}M la consommation de pointe en kilo-octets. Ensuite, vérifiez, rechargez et regardez si le fichier apparaît :

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

Laissez le journal tourner une journée entière, afin que les heures de pointe y figurent. Vous évaluez ensuite la consommation de pointe sous forme de quantiles :

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 "Requêtes : %d   Médiane : %d kB   p95 : %d kB   Maximum : %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

La même évaluation pour la durée d'exécution, dont vous aurez besoin juste après pour le second calcul, va chercher la colonne précédente :

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

Deux limites. Premièrement, les positions des champs dépendent de la ligne de format montrée plus haut, parce que celle-ci se termine par la durée d'exécution, la mémoire et la part de CPU. Si vous modifiez access.format, vous devez les adapter. Deuxièmement, %{kilo}M correspond à la comptabilité mémoire de PHP lui-même et constitue donc une borne inférieure : le code du programme, les extensions et la mémoire que la bibliothèque C ne rend pas immédiatement après une requête n'y figurent pas. Pour la formule, c'est donc le PSS qui fait foi. Le journal d'accès sert à repérer les scripts atypiques, ceux qui font grossir durablement un processus.

Désactivez ensuite ce journal ou mettez en place une rotation pour lui. La règle livrée ne couvre que le journal d'erreurs, et un disque plein produit des symptômes qui n'ont plus rien à voir avec PHP-FPM : Disque plein : trouver et libérer de l'espace sur un serveur Linux.

Le calcul

Il y a deux chiffres : un plafond issu de la RAM et un besoin issu de la charge. La bonne valeur est la plus petite des deux.

Le plafond issu de la RAM

pm.max_children = budget pour PHP  /  PSS par processus de travail

Le budget n'est pas la totalité de la RAM :

free -m

La colonne available tient déjà compte du cache libérable, mais elle ne contient pas la mémoire que vos processus de travail détiennent à cet instant. Le budget vaut donc available plus la somme PSS mesurée, moins une réserve. Une réserve d'environ 20 % de la mémoire totale a fait ses preuves, avec un minimum de 512 MB. Elle absorbe ce qu'aucune mesure ne montre : un pool de tampons de base de données qui grandit, une sauvegarde, une mise à jour de paquets, un import lancé au mauvais moment.

RAM totaleOccupé en permanenceRéserveBudget pour PHPPSS par processuspm.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

Arrondissez toujours vers le bas. Un processus de plus n'apporte rien, un processus de trop peut tout coûter.

Le besoin issu de la charge

processus nécessaires = requêtes par seconde  ×  durée d'exécution moyenne en secondes

Les requêtes par seconde viennent de la page d'état de FPM : accepted conn divisé par start since donne la moyenne depuis le dernier démarrage. La durée d'exécution provient de l'évaluation ci-dessus, et il faut prendre la valeur p95 et non la médiane, sinon vous dimensionnez pour l'après-midi calme.

Exemple : 40 requêtes par seconde en pointe, durée p95 de 300 millisecondes, cela fait 12 requêtes simultanées, soit environ 20 à 25 avec une marge pour les courtes poussées. Si le plafond se situe à 43, inscrivez la valeur du besoin et laissez le reste de la mémoire là où elle sert davantage, à savoir dans le cache de la base de données.

Si le besoin se situe au-dessus du plafond, ne l'inscrivez pas pour autant. Dans ce cas, soit la RAM est trop petite, soit les scripts sont trop lents, soit des requêtes passent par PHP alors qu'elles pourraient être servies comme fichier statique ou depuis un cache. Ces trois causes se corrigent, une valeur trop élevée non : elle ne fait que déplacer le moment de la panne.

dynamic, ondemand ou static

Le mode de fonctionnement détermine quand les processus naissent et disparaissent. Le plafond pm.max_children s'applique dans les trois cas.

ModeProcessus au démarrageComportementMémoireConvient à
dynamicpm.start_serversgarde entre min_spare et max_spare processus inactifs en réserve, en crée d'autres au besoin jusqu'à max_childrenvarie avec la chargeau cas normal : un à quelques pools, charge variable
ondemandaucunne démarre un processus qu'à l'arrivée d'une requête et le termine après pm.process_idle_timeoutla plus basse au reposaux serveurs qui portent beaucoup de pools, aux sites à faible trafic
staticpm.max_childrenexactement ce nombre, en permanence, sans création ni arrêt de processus pendant le fonctionnementconstante, au maximumà un pool sur du matériel prévu pour cela, avec une charge régulière

dynamic est le réglage par défaut et le bon choix dans le cas normal. Il coûte un peu de temps de calcul pour la création des processus et exige que les quatre valeurs soient cohérentes entre elles.

ondemand économise sensiblement de la mémoire au repos lorsqu'une douzaine de pools destinés à des sites rarement consultés cohabitent sur une machine. Le prix à payer, c'est la première requête après une phase de calme, qui doit attendre la création d'un processus. pm.process_idle_timeout règle la durée de survie d'un processus inactif (dix secondes par défaut) et n'agit que dans ce mode. Notez encore ceci : l'avertissement de saturation parle ici de max_children sans le préfixe pm.. Chercher la formulation habituelle ne donne donc aucun résultat.

static joue franc jeu : ce que vous inscrivez est occupé immédiatement et en permanence, en échange de quoi il n'y a plus de surprise sous charge. C'est pertinent quand PHP est le principal consommateur de la machine. Si PHP partage la RAM avec une base de données, static prive celle-ci de la possibilité de garder temporairement plus de cache.

Quel que soit le mode, pm.max_requests mérite un coup d'œil : 0 par défaut, donc sans limite. Une valeur comme 500 remplace chaque processus de travail au bout de 500 requêtes. Contre une consommation mémoire qui grimpe lentement à cause d'une extension mal écrite, c'est efficace et peu coûteux, car l'opcache reste partagé et n'est pas reconstruit. Ne descendez pas jusqu'à 20 pour autant, sinon FPM passe son temps à créer des processus.

start_servers et les valeurs spare

Ces trois valeurs n'agissent qu'avec pm = dynamic et déterminent la vitesse à laquelle FPM réagit à un pic de charge :

  • pm.min_spare_servers : c'est le nombre minimal de processus inactifs que FPM garde disponibles, le tampon pour les poussées. Trop bas, chaque pic commence par attendre la création de processus.
  • pm.max_spare_servers : c'est le nombre maximal de processus inactifs que FPM tolère. Sans cette limite, FPM conserverait tous les processus après un pic, et donc la mémoire qu'ils occupent.
  • pm.start_servers : c'est le nombre de processus qui existent immédiatement après le démarrage.

Au démarrage, FPM contrôle quatre conditions et refuse de démarrer si l'une d'elles est violée : les deux valeurs spare doivent être supérieures à zéro, aucune des deux ne doit dépasser pm.max_children, max_spare ne doit pas être inférieur à min_spare, et start_servers doit se situer entre les deux. Si pm.start_servers manque totalement, FPM calcule lui-même et journalise :

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

La formule employée est min_spare + (max_spare - min_spare) / 2, c'est-à-dire le milieu entre les deux valeurs spare. Comme point de départ, ceci fonctionne en pratique :

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

Facile à oublier : les processus inactifs occupent eux aussi de la mémoire. Le budget doit porter pm.max_children, alors que la consommation de tous les jours correspond à peu près à max_spare plus les processus actifs. Un max_spare élevé garde la mémoire occupée même à trois heures du matin.

Le rapport avec le swap

Il est tentant d'inclure la mémoire d'échange dans le calcul. Ne le faites pas. Un processus de travail dont les données se trouvent sur le disque répond plusieurs ordres de grandeur plus lentement et reste donc occupé plus longtemps. Le nombre de requêtes simultanées augmente, FPM crée de nouveaux processus, et ceux-ci chassent encore de la mémoire. C'est cette boucle de rétroaction qui explique qu'un serveur surréservé ne se dégrade pas progressivement, mais bascule en quelques minutes.

Ce que le swap apporte malgré tout, c'est un tampon pour le cas d'erreur. Sans lui, une surréservation se termine brutalement par un processus tué par le noyau. Avec lui, vous obtenez d'abord un serveur lent, et donc une fenêtre de temps pour intervenir. La règle : le swap oui, mais calculez pm.max_children uniquement face à la vraie RAM, jamais face à la somme de la RAM et du swap.

Deux endroits vous disent si le système utilise déjà le swap. free -m montre l'occupation, mais ne dit rien de l'activité, car une page envoyée une fois sur le disque et jamais redemandée est inoffensive. Ce qui est parlant, ce sont les colonnes si et so, c'est-à-dire les entrées et sorties de swap par seconde :

free -m
vmstat 1 5

Des valeurs durablement supérieures à zéro signifient que le serveur travaille contre le disque. Si votre noyau fournit l'indicateur de pression mémoire, il est encore plus direct, car il ne dit pas combien de pages ont été déplacées, mais combien de temps les processus ont dû attendre à cause de cela :

cat /proc/pressure/memory

Si le fichier n'existe pas, la fonction est désactivée dans le noyau et vous en restez à vmstat. La façon de créer un swap, de l'inscrire durablement et de régler vm.swappiness correctement figure dans l'article Configurer le swap et éviter les plantages par manque de mémoire.

Valeur trop élevée : l'OOM killer au lieu d'une file d'attente

Supposons que vous inscriviez 200 pour que l'avertissement disparaisse enfin. Au repos, rien ne se passe, la page se charge, tout semble réglé. À la prochaine ruée, FPM crée effectivement jusqu'à 200 processus, chacun atteint sa taille réelle avec les premières requêtes, la mémoire libre chute, le noyau commence par jeter le cache de fichiers (ce qui ralentit la base de données et allonge la durée de ses requêtes), ensuite il déplace des pages vers le swap, et enfin l'OOM killer entre en scène.

Celui-ci choisit sa victime d'après la consommation mémoire, et le plus gros processus isolé sur un serveur web n'est pas un processus de travail PHP à 80 MB, mais la base de données avec son pool de tampons :

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

Les deux lignes qui comptent ressemblent à ceci :

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

Le déclencheur et la victime sont ici deux processus différents, et c'est précisément ce qui rend le diagnostic désagréable : la boîte de réception contient un message sur une base de données plantée, et personne ne pense à la configuration PHP de l'avant-veille. Selon la base de données, les parenthèses contiennent mariadbd ou mysqld.

Si c'est malgré tout un processus de travail qui est touché, FPM le signale lui-même, et si la limite de mémoire est en place, cela figure en plus dans le 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 différence entre ces deux symptômes est le vrai propos de cet article. Un pm.max_children trop bas produit une file d'attente : mesurable, documentée dans le journal, rattachable à une valeur précise, et le serveur reste joignable. Une valeur trop élevée produit un processus tué à un endroit que vous n'avez pas choisi, sur un service qui ne revient pas forcément de lui-même. Le temps d'attente est un état de fonctionnement, un événement OOM est un incident.

Erreurs fréquentes et solutions

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it : à un instant donné, tous les processus de travail étaient occupés. La ligne apparaît une fois par phase de saturation, une seule ligne peut donc représenter une heure de charge maximale. N'augmentez la valeur qu'après avoir mesuré le PSS et déterminé le budget.

WARNING: [pool www] server reached max_children setting (5), consider raising it : la même situation sous pm = ondemand, formulée sans le préfixe pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, suivi de ERROR: failed to post process the configuration et de ERROR: FPM initialization failed : le cas le plus fréquent lorsqu'on abaisse pm.max_children. Passer de 50 à 8 en laissant pm.max_spare_servers = 20 en place vous laisse avec un service qui ne démarre plus. Les quatre valeurs se modifient ensemble.

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 se trouve en dehors de la plage. Corrigez la valeur ou supprimez la ligne, FPM calculera alors lui-même.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) : il s'agit de memory_limit, qui n'a rien à voir avec pm.max_children. Un script isolé a demandé plus que ce que PHP autorise par requête. Un pm.max_children plus élevé n'y change rien et, à l'inverse, un memory_limit plus bas ne réduit pas la mémoire consommée par un processus de travail, il interrompt seulement les scripts plus tôt. Pour la planification, il reste vrai que memory_limit est le plafond de ce qu'un processus a le droit de demander.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream dans le journal d'erreurs de nginx : tous les processus sont occupés et, par-dessus le marché, la file d'attente d'acceptation est pleine. Un listen.backlog plus grand ne fait que repousser le problème, la cause tient au nombre de processus ou à la durée d'exécution des scripts.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start : le processus a été tué brutalement de l'extérieur, en règle générale par l'OOM killer. Ici, la valeur est trop élevée, pas trop basse. Contre-vérification dans le journal du noyau.

« J'ai augmenté la valeur, rien ne change. » Presque toujours, c'est le mauvais fichier qui a été modifié : une seconde version de PHP sous /etc/php/, un second pool, ou une copie dans pool.d/ qui n'est pas lue du tout. Ce qui fait foi, c'est le socket que nginx contacte et le pool qui écoute dessus :

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"

« Après le reload, le site a disparu. » Un reload relance FPM avec la nouvelle configuration et, si celle-ci est invalide, le processus maître s'arrête. Restaurez la copie déposée dans /root/backups et prenez l'habitude de lancer php-fpm8.2 -t avant chaque reload.

Comment savoir que la valeur est la bonne

« L'avertissement a disparu » ne prouve rien. Avec la valeur 500, il disparaît aussi, jusqu'au premier événement OOM. Cinq vérifications, elles, sont probantes.

Premièrement, la page d'état de FPM. Inscrivez pm.status_path = /status dans le fichier de pool, vérifiez, rechargez, puis interrogez le socket directement, en contournant complètement nginx. La page n'est ainsi pas accessible depuis le réseau :

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

Quatre lignes de la sortie portent la réponse :

ChampSignificationValeur visée
max children reachedcombien de fois le plafond a été atteint depuis le démarrage0
max listen queuela plus longue file d'attente observée sur le socket0
max active processesle nombre maximal de processus actifs en même tempsnettement sous pm.max_children
slow requestsrequêtes dépassant request_slowlog_timeout0 si possible

Ces chiffres ne deviennent parlants qu'après une semaine complète, heures de pointe comprises. Si max active processes affiche ensuite 12 alors que pm.max_children vaut 43, vous avez de la marge et pouvez employer la réserve ailleurs.

Deuxièmement, la mémoire sous charge, pas la nuit, mais en pointe. available doit rester nettement au-dessus de zéro, et les colonnes si et so doivent afficher zéro :

free -m
vmstat 1 5

Troisièmement : aucun événement OOM. C'est la vérification la plus importante, parce qu'elle exclut le pire des symptômes. Une sortie vide est le résultat souhaité :

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

Quatrièmement, la somme sur tous les pools. Sur un serveur qui héberge plusieurs sites, ce n'est pas la valeur isolée qui compte, mais la somme, multipliée par le PSS de chaque processus. Elle doit tenir dans le budget, même quand tous les pools sont sous charge en même temps :

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

Cinquièmement : cela survit à un redémarrage. C'est le point le plus souvent sauté. Une valeur posée dans un fichier qui n'est pas lu ne se remarque qu'au redémarrage suivant, et celui-ci arrive rarement au moment où vous regardez :

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

Redémarrez ensuite le serveur une fois, de façon contrôlée, tant que vous êtes encore devant. Sur les serveurs root KVM et les serveurs dédiés de KernelHost, vous déclenchez ce redémarrage depuis l'espace client et vous suivez le démarrage par la console VNC, même quand le service web ne répond pas encore. Les serveurs se trouvent dans le datacenter maincubes à Francfort-sur-le-Main.

Liste de contrôle rapide

  1. Copie du fichier de pool dans /root/backups, accès par la console VNC essayé une fois.
  2. Mesurer le PSS par processus de travail sous charge réelle, pas après un redémarrage, et ne pas calculer avec le RSS.
  3. Déterminer le budget : available plus la somme PSS en cours, moins une réserve fixée en connaissance de cause.
  4. Calculer le plafond, le confronter au besoin (requêtes par seconde multipliées par la durée p95), retenir la plus petite valeur, arrondir vers le bas.
  5. Choisir le mode de fonctionnement et ajuster les valeurs spare en même temps que pm.max_children.
  6. php-fpm8.2 -t, puis systemctl reload, puis php-fpm8.2 -tt comme preuve que la valeur est bien chargée.
  7. Une semaine plus tard, vérifier max children reached, max listen queue et le journal du noyau.

Questions fréquentes

Comment calculer correctement pm.max_children ?
Avec deux chiffres, et c'est le plus petit qui gagne. Le plafond, c'est le budget pour PHP divisé par la mémoire proportionnelle (PSS) d'un processus de travail. Ce budget correspond à la colonne available de free -m, plus la mémoire que les processus de travail en cours détiennent à cet instant, moins une réserve d'environ 20 % de la mémoire totale. Le second chiffre est le besoin : les requêtes par seconde en pointe multipliées par la durée d'exécution p95 en secondes. Si le plafond donne 43 et le besoin 22, inscrivez 22 et laissez le reste de la mémoire à la base de données. Arrondissez toujours vers le bas.
Pourquoi une valeur trop élevée est-elle plus dangereuse qu'une valeur trop basse ?
Une valeur trop basse crée une file d'attente. Les requêtes patientent dans la file d'attente d'acceptation du socket, la page ralentit, le serveur reste joignable et le journal vous dit exactement ce qui se passe. Une valeur trop élevée provoque un manque de mémoire sous charge, et c'est alors le noyau qui choisit lui-même sa victime, d'après la consommation mémoire. Le plus gros processus d'un serveur web est en général la base de données avec son pool de tampons, pas un processus de travail PHP. Vous échangez donc un temps d'attente mesurable contre la panne sans préavis d'un autre service.
Pourquoi faut-il calculer avec le PSS et non avec le RSS ?
Le RSS contient aussi les zones de mémoire partagées, à commencer par l'opcache et les bibliothèques partagées. Additionner le RSS de 20 processus de travail revient à compter vingt fois le même opcache et aboutit à une consommation très surévaluée, donc à un pm.max_children inutilement petit. Le PSS, lui, divise les pages partagées par le nombre de processus qui les utilisent et s'additionne donc correctement. La valeur se lit en tant que root dans /proc/PID/smaps_rollup, à la ligne Pss.
Quand choisir dynamic, quand ondemand et quand static ?
dynamic est le réglage par défaut et le bon choix dans le cas normal : un à quelques pools avec une charge variable. ondemand vaut la peine quand beaucoup de pools destinés à des sites rarement consultés cohabitent sur une machine, car au repos aucun processus n'y existe. Le prix à payer, c'est la première requête après une phase de calme, qui doit attendre la création d'un processus. static convient quand PHP est le principal consommateur de la machine et que la charge est régulière : la mémoire est occupée immédiatement et en permanence, en échange de quoi il n'y a plus de surprise sous charge. Si PHP partage le serveur avec une base de données, static prive celle-ci de la possibilité de garder temporairement plus de cache.
Puis-je inclure le swap dans le calcul ?
Non. Un processus de travail dont les données se trouvent sur le disque répond plusieurs ordres de grandeur plus lentement et reste donc occupé plus longtemps. Le nombre de requêtes simultanées augmente, FPM crée de nouveaux processus, et ceux-ci chassent encore de la mémoire. C'est pourquoi un serveur surréservé bascule en quelques minutes au lieu de ralentir progressivement. Le swap reste malgré tout utile, mais comme tampon pour le cas d'erreur : il vous laisse une fenêtre de temps pour intervenir avant que le noyau ne tue un processus. Le calcul se fait exclusivement face à la vraie RAM.
J'ai augmenté pm.max_children et rien ne change. D'où cela vient-il ?
Presque toujours, c'est le mauvais fichier qui a été modifié. Sur les serveurs qui portent plusieurs versions de PHP sous /etc/php/ ou plusieurs pools, seul compte le pool dont le socket est réellement contacté par nginx. Comparez fastcgi_pass dans la configuration de nginx avec les lignes listen des fichiers de pool. Ce que FPM a réellement chargé, c'est php-fpm8.2 -tt qui le montre : sa sortie contient la configuration entièrement résolue, y compris toutes les valeurs par défaut.
Après avoir abaissé pm.max_children, PHP-FPM ne démarre plus. Que s'est-il passé ?
Les valeurs spare sont probablement restées sur les anciens chiffres, plus élevés. FPM refuse de démarrer avec le message indiquant que pm.min_spare_servers et pm.max_spare_servers ne peuvent pas être supérieurs à pm.max_children, suivi de FPM initialization failed. Ajustez les quatre valeurs ensemble : les deux valeurs spare doivent être supérieures à zéro et au plus égales à pm.max_children, max_spare ne doit pas être inférieur à min_spare, et pm.start_servers doit se situer entre les deux.
memory_limit a-t-il un rapport avec pm.max_children ?
Seulement de façon indirecte. memory_limit est le plafond que PHP impose par requête, 128M par défaut en mode FPM. Le message de mémoire épuisée (Allowed memory size exhausted) concerne un script isolé et ne s'arrange pas avec un pm.max_children plus élevé. À l'inverse, un memory_limit plus bas ne réduit pas la mémoire dont a besoin un processus de travail, il interrompt seulement les scripts plus tôt. Le rapport se situe dans la planification : qui règle memory_limit sur 1024M doit, dans le pire des cas, compter aussi avec cette taille par processus.

PHP-FPM pm.max_children RAM Debian Ubuntu nginx OOM killer Optimisation serveur