Lire un 502 Bad Gateway : remonter jusqu'au pool PHP-FPM
Un 502 Bad Gateway ne dit presque rien : il constate qu'un intermédiaire n'a pas obtenu de réponse valable de PHP-FPM. La cause peut être un pool saturé, un socket absent, un travailleur tué par le noyau ou un délai dépassé, quatre incidents que l'on confond en augmentant le plafond de travailleurs au hasard. Voici la méthode d'élimination, du journal du serveur web jusqu'à la file d'attente du pool.
Un 502 constate une absence de réponse, il ne la nomme pas
Le code 502 est émis par un intermédiaire, le serveur web ou le mandataire placé devant lui : la réponse reçue de l'amont est absente ou invalide. Il ne dit ni pourquoi ni qui a échoué. La page affichée est celle du serveur web : un script qui s'arrête sur une erreur fatale renvoie un 500, un serveur saturé un 503, un amont trop lent un 504. Le 502 est un constat de non-réponse, et c'est aux journaux de nommer la cause.
L'expression « no worker available » décrit cet état sans l'expliquer : une requête s'est présentée sur le socket du pool et aucun travailleur n'était en mesure de la prendre. Deux situations se cachent derrière. Soit les travailleurs existent, tous traitent une requête, et la file d'attente a débordé ; soit plus rien n'écoute sur le socket, et la connexion est refusée net. Le libellé varie selon qui l'écrit, mais la question ne change pas : le serveur web a-t-il échoué à se connecter, ou perdu une connexion établie ?
La méthode tient en une phrase : partir de la preuve la plus en aval et remonter maillon par maillon. Le journal du serveur web indique laquelle des phases a cédé — connexion refusée, connexion acceptée puis abandonnée, attente trop longue — et ces trois lignes ne désignent pas les mêmes composants. Noter aussi la forme de la panne : toutes les pages en 502 en permanence orientent vers un socket ou une configuration, des 502 intermittents aux heures de pointe vers la capacité ou un travailleur tué.
Le trajet d'une requête, du socket jusqu'au travailleur
La chaîne compte quatre maillons : le serveur web, le socket d'écoute, le processus maître du pool, puis un travailleur. Le maître ne sert aucune page ; il ouvre le socket, accepte les connexions et confie chacune à un travailleur libre. Un travailleur traite une requête à la fois, ce que la documentation d'Apache rappelle à propos de PHP-FPM : le modèle est de type prefork. Toute la concurrence du site tient donc dans le nombre de travailleurs.
Le socket est le point de rencontre, et sa nature change les pannes. Avec un socket Unix, l'accès est contrôlé par les droits du système de fichiers, réglés par listen.owner, listen.group et listen.mode, à 0660 par défaut ; sur Linux, le serveur web doit y disposer de la lecture et de l'écriture. Avec un socket TCP, ces droits disparaissent : listen.allowed_clients, vide par défaut, accepte toute adresse capable de joindre le port. Le manuel rappelle qu'un point d'accès FastCGI exposé permet l'exécution de code arbitraire.
Deux détails côté serveur web expliquent des 502 que la configuration du pool ne montre pas. mod_proxy_fcgi ne démarre pas l'application : si le service est arrêté, rien ne le relance et la connexion est refusée. Et la réutilisation de connexion, activée par enablereuse, peut ouvrir jusqu'à MaxRequestWorkers connexions vers l'amont ; la documentation avertit que les travailleurs risquent alors d'être tous occupés à tenir des connexions persistantes inactives, jusqu'aux délais d'attente côté visiteur.
Saturation : lire la file d'attente avant de toucher au plafond
pm.max_children fixe le nombre de requêtes servies simultanément, selon le manuel. Au-delà, les requêtes ne sont pas perdues : elles attendent dans la file d'écoute du socket, bornée par listen.backlog, à 511 par défaut sur Linux. Quand cette file est pleine, le noyau refuse les nouvelles connexions et le serveur web traduit ce refus en 502. La saturation n'est donc pas un mur : c'est une file qui se remplit, puis un refus.
La page d'état du pool rend ce mécanisme visible, à condition d'être activée par pm.status_path et restreinte aux appels internes. Elle publie les compteurs utiles : listen queue, requêtes en attente d'un processus libre ; max listen queue, son plus haut niveau observé ; listen queue len, sa taille maximale ; idle, active et total processes ; max children reached, non nul si le plafond a déjà été atteint ; memory peak, le pic depuis le démarrage.
L'interprétation est mécanique. Un max children reached non nul avec une file non nulle désigne un manque de capacité ; une file à zéro et des travailleurs inactifs pendant l'incident l'excluent et renvoient vers le socket, la mémoire ou le délai. Attention au piège : la page d'état est servie par un travailleur, donc invisible quand tous sont occupés ; pm.status_listen ouvre un second point d'écoute indépendant. Relever ces valeurs avant toute modification, sinon la mesure suivante n'aura aucun point de comparaison.
Socket absent ou droits insuffisants : le 502 qui ignore la charge
Un 502 permanent, sur toutes les pages et dès la première requête, ne ressemble pas à une saturation : il apparaît d'un coup, après un redémarrage, une mise à jour de paquets ou un changement de version de PHP. Le service est peut-être arrêté, ou le socket a changé de chemin — les paquets le placent souvent dans un répertoire qui porte le numéro de version, si bien qu'une montée de version laisse l'ancien chemin dans la configuration du serveur web.
Les droits produisent le même symptôme avec un message moins évident. Le serveur web doit pouvoir traverser le répertoire du socket et ouvrir le fichier ; un umask ou une règle de tmpfiles qui recrée ce répertoire au démarrage peut retirer ces droits. Un listen.mode à 0660 ne suffit pas si l'utilisateur du serveur web n'est pas dans le groupe du pool ; listen.acl_users et listen.acl_groups couvrent ce cas. Sur les systèmes BSD dérivés, le manuel précise que les droits sont ignorés.
Trois lectures suffisent, sans effet sur la production : l'état du service, la présence du socket et ses droits, l'appartenance de l'utilisateur du serveur web au groupe attendu. Une fois la cause corrigée, distinguer rechargement et redémarrage : le premier laisse finir les requêtes en cours, le second les coupe. Vérifier la syntaxe de la configuration avant l'un comme avant l'autre, et garder une copie du fichier de pool pour revenir en arrière.
Travailleur tué : mémoire du noyau et délai d'exécution
Un travailleur peut disparaître au milieu d'une requête. La connexion se ferme alors sans en-têtes de réponse, et le serveur web signale une réponse tronquée ou une connexion interrompue prématurément : un 502 qui n'a rien à voir avec la capacité du pool. Deux mécanismes coupent un travailleur : le noyau, quand la machine manque de mémoire et sacrifie un processus, et PHP-FPM lui-même, quand request_terminate_timeout est atteint. Le journal du pool donne le signal reçu.
Côté noyau, le journal système conserve la trace du processus tué, avec son nom et le motif de saturation mémoire. Côté processus, la mémoire résidente se lit travailleur par travailleur, mais son addition trompe : les pages partagées, bibliothèques et segments du cache d'opcodes, sont comptées dans chaque processus. proc_pid_status distingue la part anonyme de la part fichier et partagée, ce qui aide à savoir si la croissance vient du script ou d'un segment commun. Retenir le travailleur le plus gros en charge, pas la moyenne au repos.
Le délai d'exécution du pool mérite une lecture précise. request_terminate_timeout arrête le travailleur après un temps donné, et zéro le désactive ; le manuel le présente comme le recours quand max_execution_time ne suffit pas à interrompre un script, par exemple sur une attente de base de données. Le visiteur reçoit alors un 502, puisque la connexion se ferme, et non le 504 d'un serveur web qui aurait attendu trop longtemps. Un seuil trop bas tue des pages légitimes aux heures chargées.
L'erreur qui imite un 502, et l'ordre de lecture
Tout 502 ne vient pas du pool. Un en-tête de réponse trop volumineux, cookies nombreux ou redirection bavarde, dépasse la mémoire tampon du serveur web et produit un 502 alors que PHP a répondu. Une application qui appelle elle-même un service interne, ou un autre pool, propage le 502 de son amont. Une extension PHP qui plante salement ferme la connexion comme un processus tué. Dans ces trois cas, augmenter le plafond de travailleurs ne change rien : les requêtes actives restent basses pendant l'incident.
L'ordre de lecture découle du trajet. D'abord le journal d'erreurs du serveur web, qui situe la phase et parfois le chemin du socket. Ensuite le journal du pool, avec catch_workers_output activé, sans quoi la sortie des travailleurs part vers le vide conformément à la spécification FastCGI. Puis le journal des requêtes lentes, s'il est réglé, qui nomme le script fautif. Enfin la page d'état et les files : travailleurs actifs et inactifs, requêtes en attente, connexions, charge et mémoire. Le journal système vient en dernier, pour la mémoire.
Ce qui compte autant que l'ordre, c'est ce qu'on ne fait pas d'abord. Redémarrer le service remet les compteurs à zéro : max children reached, max listen queue et memory peak disparaissent avec l'incident. Copier les journaux et relever la page d'état avant toute intervention coûte une minute et évite de chercher à l'aveugle. Ensuite, une seule variable à la fois, un rechargement plutôt qu'un redémarrage, et une mesure après, à charge comparable : sans mesure de départ, aucun réglage n'est jugeable.