Nginx et PHP-FPM: une nuit de réglages face au pic de trafic
Son site venait de subir un pic de commandes imprévu, et l'infrastructure avait basculé en quelques minutes du « tout va bien » au mur de 502 Bad Gateway. « On a doublé les workers, on a poussé le max_children, et tout s'est effondré en cascade. C'est pire qu'avant », m'a-t-il expliqué, presque en s'excusant de ne pas avoir anticipé. Je l'ai écouté, puis je lui ai posé la question que je pose à chaque candidat ou client qui traverse ce moment: « Qu'est-ce que ton pool FPM te racontait pendant la montée en charge? »
Cette scène, je la vois revenir presque à chaque mission autour de PHP en production. Quand la charge monte, l'instinct commande de pousser les curseurs vers le haut, comme si l'infrastructure était un simple bouton de volume. Or ce que ce développeur - et beaucoup d'autres avec lui - avait surtout diagnostiqué tard, c'est une saturation de son pool PHP-FPM que ses réglages ne lui permettaient pas de voir. C'est précisément de cette lecture fine que nous allons parler: celle des signaux d'alerte, des files d'attente, des processus, des tampons et des timeouts qui séparent un site qui encaisse d'un site qui cède sous la charge.
Anatomie d'une saturation: comprendre les signaux d'alerte FPM
Avant de toucher à un seul paramètre, la première discipline que j'attends d'un profil senior en entretien - et que je valorise dans une grille d'évaluation - c'est la capacité à lire une situation avant de la modifier. Sur un serveur PHP en charge, cette lecture passe quasi exclusivement par la page de statut de PHP-FPM. Si tu ne sais pas encore la consulter régulièrement, sache que c'est ton premier point de bascule entre la réaction à chaud et l'anticipation à froid.
La page de statut expose plusieurs indicateurs qu'il faut apprendre à distinguer, car ils ne racontent pas la même histoire. Le plus précieux d'entre eux s'appelle « listen queue »: c'est le nombre de requêtes qui attendent qu'un processus PHP se libère pour être traitées. Quand ce chiffre décolle, c'est le signe qu'un nouveau visiteur arrive plus vite que ton pool ne peut l'absorber. À ses côtés, tu trouveras « max listen queue », qui mémorise le pic historique, « max children reached », qui te dit si tu as touché la limite supérieure de processus, « slow requests », qui isole les requêtes anormalement longues, et « memory peak », qui suit la consommation mémoire maximale d'un worker. Cinq indicateurs, cinq angles complémentaires qui, croisés entre eux, racontent presque toujours l'histoire vraie de l'incident.
Le réflexe que j'essaie de transmettre à chaque candidat qui me parle de « tuning », c'est celui-ci: avant d'augmenter la moindre valeur, identifie précisément lequel de ces signaux hurle. Une « listen queue » qui grimpe avec un « max children » jamais atteint, c'est une histoire de processus trop lents, pas un pool trop petit. Un « slow requests » qui s'envole, c'est une question applicative, une dépendance externe qui sature ou une requête qui touche la base de données de manière imbécile, pas une question d'infrastructure. Confondre ces signaux, c'est exactement ce qui a coûté sa nuit à mon développeur: il a vu un mur d'erreurs 502 et il a augmenté le nombre de processus, alors que la cause profonde était ailleurs. Et un dernier point, non négociable: la page de statut de PHP-FPM comme celle d'Nginx, si tu décides de l'exposer, doit l'être uniquement derrière un contrôle d'accès strict, car elle révèle une cartographie complète de ton infrastructure à quiconque sait la lire.
« Avant de toucher un seul curseur, apprends à lire la page de statut FPM. C'est elle qui te dira quoi régler, pas ton intuition. »
Le juste équilibre des processus: pm.max_children et gestion de la mémoire
Dès qu'on parle de processus PHP, la conversation finit toujours par buter sur « pm.max_children ». Et c'est normal: c'est le paramètre le plus visible, celui sur lequel tout le monde a un avis. Mais c'est aussi le piège le plus classique du métier, celui qui transforme un incident en catastrophe quand on le manipule sans données réelles sous les yeux.
Pour commencer, rappelle-toi que PHP-FPM te propose trois modes de gestion des workers, et que chacun raconte une histoire différente:
| Dimension | static | dynamic | ondemand |
|---|---|---|---|
| Logique | Nombre fixe de processus toujours actifs | Plage souple entre un minimum et un maximum, démarrant à pm.start_servers | Processus créés à la demande, détruits après inactivité |
| Paramètres clés | pm.max_children | pm.max_children, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers | pm.max_children, pm.process_idle_timeout |
| Profil de trafic adapté | Charge prévisible et constante, recherche de latence minimale | Trafic variable avec pics marqués | Sites à faible trafic ou priorité absolue à l'économie mémoire |
| Compromis principal | Mémoire réservée en permanence, même au calme | Ajustement continu, plus de paramètres à surveiller en parallèle | Cold-start sur la première requête, overhead de fork à chaque réveil |
Le point que je martèle en coaching, c'est qu'« augmenter pm.max_children » n'est pas une solution, c'est un déplacement de problème. Chaque processus PHP consomme de la mémoire résidente; si tu fixes pm.max_children à 200 sans savoir combien consomme réellement un de tes workers en exécution réelle, tu peux très bien faire passer ton serveur d'un état « pool saturé » à un état « swap généralisé », ce qui est strictement pire. La mémoire réellement consommée par un processus PHP-FPM ne se devine pas: elle se mesure, sur la machine, sous la charge réelle, avec un œil sur les dépendances chargées par l'application ( Composer, frameworks, extensions). C'est une question que j'aime poser en entretien pour séparer le candidat qui a vécu un incident de celui qui l'a seulement lu: « Quelle est la consommation mémoire type d'un de tes workers, et comment l'as-tu mesurée? » Si la réponse est « je ne sais pas, j'ai augmenté le max_children et ça a tenu », je sais que la prochaine crise ressemblera à celle du mardi matin.
Maîtriser les files d'attente: listen.backlog et gestion des connexions Nginx
Le malentendu le plus fréquent que je rencontre chez les candidats qui se présentent comme experts PHP, c'est la confusion entre la file d'attente du socket d'écoute et la capacité réelle de traitement. Sous Linux et les autres plateformes non BSD documentées, le paramètre listen.backlog de PHP-FPM a une valeur par défaut de 511. Ce n'est pas le nombre de requêtes que PHP peut traiter en parallèle: c'est la taille de la file du socket, le nombre de connexions qui peuvent patienter avant même qu'un processus ne soit techniquement en mesure de les accepter. La nuance est capitale, parce qu'elle change complètement la lecture d'un incident: une file pleine n'a pas la même cause qu'un pool saturé, et donc n'a pas le même remède.
Côté Nginx, le pendant de cette logique se joue autour des workers. Le paramètre worker_processes auto détecte automatiquement le nombre de processeurs disponibles, ce qui constitue un excellent point de départ quand on n'a pas d'historique de charge documenté. Le paramètre worker_connections, lui, plafonne le nombre de connexions ouvertes par worker, avec une valeur par défaut de 512. La tentation est grande de multiplier les deux et d'annoncer fièrement « 4 cœurs × 512 connexions = 2048 visiteurs simultanés ».
C'est un raccourci que je corrige systématiquement en entretien, parce qu'il est faux par construction. D'abord, worker_connections ne compte pas que les visiteurs: il inclut aussi les connexions vers les serveurs amont, c'est-à-dire vers PHP-FPM, vers la base de données, vers les services externes, vers le cache. Une même requête entrante peut mobiliser plusieurs de ces connexions simultanément. Ensuite, la limite réelle peut être bien plus basse si ton système impose un plafond de descripteurs de fichiers ouverts plus restrictif que ce que Nginx autorise: c'est typiquement le cas en conteneur, où les limites sont fixées par l'orchestrateur ( Kubernetes, Docker, systemd) et non par la machine physique. Sans connaître ces plafonds, le calcul mental « workers × connexions » n'a aucune valeur opérationnelle, et c'est précisément le genre de chiffre rond qui rassure faussement avant l'incident.
Tuning des buffers FastCGI et timeouts: éviter les goulots d'étranglement
Quand un site encaisse mal un pic, on entend souvent « c'est un problème de buffers » ou « c'est un timeout trop court ». Ces deux intuitions ne sont pas fausses, mais elles demandent à être démêlées, parce qu'elles pointent vers des réglages très différents.
Commençons par le tamponnement FastCGI, qui est activé par défaut dans Nginx. Concrètement, Nginx lit la réponse de PHP-FPM le plus vite possible vers ses propres tampons en mémoire; si la réponse ne tient pas dans ces tampons, l'excédent peut être écrit dans un fichier temporaire sur disque. La taille par défaut de fastcgi_buffers est de 8 tampons, dont la taille unitaire est de 4 Kio ou 8 Kio selon la taille de page de la plateforme. Sur une réponse lourde, un export CSV volumineux ou un endpoint d'API qui retourne plusieurs mégaoctets, ces valeurs deviennent vite étriquées: Nginx doit alors solliciter le disque, ce qui peut transformer un pic de trafic en pic d'I/O et propager la latence à l'ensemble des requêtes.
Côté timeouts, le malentendu classique concerne fastcgi_read_timeout, qui vaut 60 secondes par défaut. Ce délai ne mesure pas la durée totale de la réponse: il mesure l'intervalle maximal entre deux opérations de lecture depuis PHP-FPM. Si PHP envoie ses données régulièrement, mais que la transmission totale dure 90 secondes, ça passe. Si PHP bloque 61 secondes sans rien envoyer entre deux paquets, la connexion tombe. Et c'est précisément le piège: augmenter fastcgi_read_timeout ne « répare » pas une application lente, il tolère juste plus longtemps le silence du backend. Pour identifier ces silences, le request_slowlog_timeout de PHP-FPM est un allié précieux: il déclenche l'écriture d'une trace de pile dans un slowlog dès qu'une requête dépasse le seuil défini; une valeur à 0 désactive le mécanisme, et la profondeur de trace par défaut, request_slowlog_trace_depth, est de 20. Sans slowlog actif, tu navigues à l'aveugle pendant l'incident.
À côté des buffers FastCGI, OPcache mérite un mot, parce qu'il est souvent le levier le plus rentable et le moins risqué du dossier. Trois paramètres à connaître pour ne pas le sous-employer: opcache.memory_consumption à 128 Mio par défaut, applicable dès 8 Mio au minimum; opcache.max_accelerated_files à 10 000 par défaut, dans une plage autorisée de 200 à 1 000 000; et opcache.validate_timestamps, activé par défaut, avec une fréquence de revalidation de 2 secondes via opcache.revalidate_freq. Le piège classique, et je l'ai vu en mission comme en entretien, c'est la désactivation de validate_timestamps sans procédure de déploiement qui réinitialise ou redémarre le cache. Une fois la vérification désactivée, les modifications de code ne sont plus prises en compte tant que tu ne vides pas manuellement OPcache ou que tu ne redémarres pas le service. Pour un site qui déploie plusieurs fois par jour, c'est une grenade laissée à moitié dégoupillée.
« Augmenter un timeout ne répare pas une application lente: cela autorise juste un silence plus long avant l'échec. »
Protection contre les abus: implémenter limit_req pour stabiliser le débit
Le dernier pan de l'optimisation sous charge, et souvent le plus négligé par les profils qui se concentrent uniquement sur le back-end, c'est la protection du débit entrant. Tous les pics de trafic ne sont pas nobles: entre les crawlers mal configurés, les rafales d'un script tiers, les tentatives d'API abuse, les erreurs de boucle d'un partenaire et les véritables utilisateurs, ton serveur sert de tout. Sans gouvernance du débit, c'est ton application la plus coûteuse qui paie l'addition des autres.
Nginx met à disposition un module dédié, limit_req, qui applique une limitation par clé selon le modèle du seau percé. Concrètement, tu définis un taux moyen de requêtes autorisées, un plafond de « burst » pour absorber les pics brefs, et Nginx se charge du reste. L'exemple canon de la documentation officielle est parlant: 1 requête par seconde en moyenne, avec un burst maximal de 5. Au-delà du burst, Nginx rejette la requête excédentaire avec un code 503 par défaut. C'est net, c'est lisible, et c'est un signal que tu peux brancher directement sur ton monitoring pour distinguer un pic légitime d'un comportement abusif.
Trois points que j'aime rappeler aux candidats qui découvrent le module. D'abord, le mode limit_req_dry_run permet de comptabiliser les dépassements sans réellement limiter le trafic: idéal pour observer ce qu'une règle donnerait sur ta production pendant une semaine avant de l'activer pour de bon. Ensuite, la zone mémoire que tu déclares pour stocker les états a un coût réel: sur une plateforme 32 bits, 1 Mio permet de conserver environ 16 000 états de 64 octets; sur 64 bits, environ 8 000 états de 128 octets. Dimensionner la zone sans réfléchir, c'est s'exposer à des erreurs « zone storage exhausted » au pire moment, c'est-à-dire précisément quand tu aurais le plus besoin que la limitation tienne. Enfin, l'IP seule n'est pas toujours la bonne clé de partition: derrière un proxy ou un CDN, tu risques de brimer un sous-réseau entier d'utilisateurs légitimes. La clé par header forwarded, lorsqu'elle est fiable et validée, est souvent plus juste et plus difficile à contourner.
Plan d'action: ce que tu peux faire dès ce soir
Je termine comme je commence mes accompagnements, par un plan court, lisible, applicable. Cinq gestes à poser dans l'ordre, pour passer d'un site qui subit la charge à un site qui la gère, et pour ne plus revivre la nuit blanche du mardi matin.
1. Active et consulte régulièrement la page de statut PHP-FPM, et identifie lequel des cinq indicateurs ( listen queue, max listen queue, max children reached, slow requests, memory peak) hurle en premier sous charge, avant de modifier le moindre réglage.
2. Mesure la consommation mémoire réelle d'un de tes workers en exécution, sur la machine et sous la charge réelle, avant de toucher à pm.max_children; la valeur ne se devine pas, elle s'observe.
3. Choisis ton mode de gestion FPM ( static, dynamic ou ondemand) en fonction de ton profil de trafic, et arrête de mélanger les paramètres des trois modes au gré des copier-coller trouvés en ligne.
4. Vérifie les plafonds de descripteurs de fichiers imposés par ton système ou ton conteneur avant de raisonner en worker_connections × worker_processes; le chiffre que tu annonces aux autres n'est valide qu'après cette vérification.
5. Active request_slowlog_timeout, dimensionne tes fastcgi_buffers à la taille réelle de tes plus grosses réponses, et pose une zone limit_req avec un burst mesuré sur ta propre production, pas deviné sur un tutoriel.




