PHP-FPM: remédier à server reached pm.max_children

Contrairement à d’autres modèles d’intégration de PHP aux serveurs web, PHP-FPM est un dæmon qui tourne indépendamment du serveur. Il est lancé par le système d’initialisation de l’OS et communique par la suite avec le serveur web via FastCGI.

Explication du problème

FPM pour FastCGI Process Manager, manage un pool de processus enfants. Chaque processus ne peut traiter qu’une seule requête à la fois. Dès lors qu’il y a plus de requêtes concurrentes que de processus disponibles, on atteint pm.max_children et les nouvelles requêtes sont mises en file d’attente.

Ainsi, lorsque l’on rencontre ce problème, la première chose à faire est de corréler le nombre de processus enfants disponibles avec le trafic de votre site. Si votre site rencontre un certain succès et que pm.max_children est à 5… il serait peut-être temps d’ajuster le réglage.

Diagnostiquer la cause du problème

Prenons un exemple concret. Comme le problème s’est récemment présenté à moi pour le serveur qui héberge ce blog, cela nous servira d’exemple.

Estimation rapide du trafic

Comme je l’ai mentionné, la première chose à faire est d’avoir une estimation du trafic auquel doit répondre le serveur. Cela nous permet de savoir si les réglages de PHP sont adaptés, ce qu’il est possible d’optimiser, si le serveur est clairement sous-dimensionné ou si le problème vient d’ailleurs.

Nous pouvons avoir une première idée rapidement grâce au logiciel d’analytics. À titre d’exemple, ce blog a un peu plus de 2000 pages vues par jour. En outre, il y a quelques autres sites hébergés sur ce serveur. On est donc à un peu plus de 3500VU/jour.

Le serveur en question est un VPS aux ressources modestes, le pm.max_children est aux alentours de 10 et le serveur n’est pas en mesure d’en supporter bien plus à cause de sa capacité en RAM limité.

Pour savoir comment calculer la valeur maximale que peut supporter votre serveur, vous pouvez vous référer à mon article sur la configuration de PHP avec Apache.

Il faut bien avoir en tête que le trafic n’est pas lissé sur la journée. Il peut donc y avoir des pics de trafic qui mettent temporairement à mal un serveur, alors qu’il répond efficacement aux requêtes le reste du temps.

De plus, qu’il s’agisse d’Apache ou de Nginx, le serveur passe la requête à PHP seulement si elle requiert un traitement par ce dernier – c’est déterminé par la configuration du serveur. Par exemple, dans le cas d’Apache, le fichier /etc/apache2/conf-enabled/php7.0-fpm.conf.

<FilesMatch ".+\.ph(p[3457]?|t|tml)$">
    SetHandler "proxy:unix:/run/php/php7.0-fpm.sock|fcgi://localhost"
</FilesMatch>

Dans le cas de ce blog, ce n’est pas parce qu’il s’agit d’un site en PHP que toutes les requêtes doivent être générées de manière dynamique. La plupart des pages peuvent être pré-généréres et mises en cache.

Ainsi, le serveur web peut servir au client un fichier html déjà existant et n’a pas à avoir recours à PHP. Il y a 99% de chance pour que l’article que vous êtes en train de lire provienne du cache et a été servi par Apache tel quel, sans avoir recours à PHP ni besoin de requêter la base de données.

Dès lors, même avec un trafic élevé, la charge serveur reste minimale et les processus PHP ne sont pas trop sollicités. La mise en place d’un système de cache est un tout autre sujet, mais c’est primordial. Vous gagnerez énormément en performances et la plupart des CMS et frameworks ont des systèmes de caches faciles à mettre en place et éprouvés ; ne vous en privez donc pas !

Activation des logs du serveur web et de PHP

Néanmoins, dans mon cas, malgré ce système de cache efficace, le serveur atteignait régulièrement la limite du nombre de processus PHP utilisés. Première chose à faire, jeter un œil aux logs d’erreur de PHP et de Apache ou Nginx selon votre cas à la recherche d’indices potentiels.

En outre, on regarde l’access log du serveur web pour avoir une idée précise des requêtes traitées au moment de l’épuisement des ressources PHP. Si comme moi, vous n’activez pas le logging des requêtes par défaut, alors vous l’activez momentanément et vous laissez tourner le tout un moment. L’idéal est de laisser tourner jusqu’à ce que le problème se manifeste de nouveau.

Bien entendu, vous recueillez là de nombreuses informations que n’est pas en mesure de vous fournir votre outil d’analyse de trafic. Toutes les requêtes provenant de bots, les utilisateurs n’ayant pas le JavaScript activé, les requêtes de flux RSS et d’éventuelles API se retrouvent toutes dans votre log.

En complément, vous pouvez activer l’access log de PHP-FPM lui-même. Vous verrez ainsi les requêtes réellement traitées par PHP. Pour l’activer, direction /etc/php/7.x/fpm/pool.d/www.conf. Puis activez l’access_log en dé-commentant access.log = xxx et en spécifiant le chemin du fichier de log.

Assurez-vous bien que le dossier dans lequel php est censé logger existe, sinon le processus ne voudra pas redémarrer.

Activation du slow log

Par ailleurs, pour approfondir votre investigation, vous pouvez également activer le slow log. Ce fichier de log concerne toutes les requêtes traitées par PHP qui prennent plus qu’un laps de temps donné (que vous définissez).

Cela peut s’avérer très instructif car même en l’absence d’un très fort trafic, si certaines requêtes mettent plusieurs secondes à être traitées par le moteur de PHP, alors elles monopolisent un processus pendant autant de temps. Avec un pm.max_children à 10 et un temps de traitement moyen de 100ms, le serveur peut traiter 100 requêtes par seconde, soit 6000 par minute.

Dans le même temps, si le traitement est plus lent et prend 2 secondes, on passe à 5 requêtes par seconde et 350 requêtes par minute. Maintenant imaginez des traitement encore plus lent, et vos 10 processus php seront toujours submergés dès lors qu’il y a un temps soit peu de trafic. C’est pourquoi il faut bien avoir en tête ce qu’il est acceptable de tolérer en requête lente, lent étant très relatif.

Bien entendu, tout dépend de ce que font les processus PHP. À nouveaux, ce n’est pas l’objet de cet article, mais les processus lourds (compression d’image etc) peuvent facilement être traités de manière asynchrone en dehors de PHP-FPM, les requêtes à la base de données doivent être otpimisées – le processus PHP est bloqué pendant tout le temps de la requête – etc etc.

Afin d’activer le slow log, il vous faudra dé-commenter la ligne slowlog = xxx. Tout comme pour l’access log, vous devez spécifier le chemin du fichier de log et vous assurer que le répertoire dans lequel il sera écrit existe bien. De plus, vous devez spécifier le seuil à partir duquel une requête sera considérée comme lente, c’est en seconde : request_slowlog_timeout = xx.

À partir de là, les fichiers de log générés vont vous donner de bons indices. Vous savez quelles sont les pages requêtées sur le serveur HTTP, quand et à quelle fréquence, quels fichiers sont traitées par PHP et lesquels sont lents (donc potentiellement bloquants).

Activation du mode status de PHP

En supplément, vous pouvez activer le mode status de PHP-FPM. Ce dernier va vous permettre d’avoir des informations en temps réel :

  • combien de processus sont utilisés,
  • combien sont idle,
  • combien il y a eu eu de processus lancés au maximum,
  • combien de fois le max_children a été atteint,
  • combien de requêtes ont été traitées.

Toujours dans le fichier de configuration, il faut dé-commenter pm.status_path = xxx. Il y a ici une petite subtilité, par défaut, le nom n’est pas le nom d’un fichier se terminant en .php. De ce fait, votre serveur web n’a aucun moyen de savoir qu’il doit transmettre cette requête à PHP.

Deux solutions s’offrent alors à vous :

  • définir un nom se terminant par .php (ou php[3-7] dans la configuration d’Apache telle que nous l’avons vu précédemment,
  • définir un VHOST spécifique. Je préfère cette seconde solution car cela me permet de n’autoriser qu’une seule ip. Nul n’a à connaître le statut de mon pool PHP-FPM.

Le plus simple pour moi est même de n’autoriser que localhost, pour Apache, le VHOST ressemble donc à cela :

<VirtualHost *:80>
    <FilesMatch "php-status">
        SetHandler "proxy:unix:/run/php/php7.x-fpm.sock|fcgi://localhost"

        require ip 127.0.0.1
    </FilesMatch>
</VirtualHost>

Pour Nginx, cela peut ressembler à quelque chose de la sorte :

location ~ ^/(php-status)$ {
    fastcgi_pass   fastcgi_pass unix:/var/run/php-fpm.sock;
    allow 127.0.0.1;
    deny all;
}

Dans les deux cas, si vous utilisez une connexion via TCP au lieu du socket UNIX, il faut remplacer les infos, cela coule de source.

Une fois les configurations du serveur web et de PHP-FPM rechargées via service xxx reload, vous pouvez récupérer le statut de PHP via curl :

curl http://localhost/php-status

# ou pour le statut process par process
curl http://localhost:php-status?full

Dans la vue processus par processus, vous pourrez accéder aux informations sur les différents processus en cours : leur PID, la RAM & CPU consommés, le temps depuis lequel ils sont lancés etc.

Remédier au problème

Avec toutes ces informations à votre disposition, vous devez être savoir ce qui utilise tous les processus de votre pool FPM. Deux solutions :

  • soit ce trafic est normal et incompressible, auquel cas il faut augmenter le nombre de processus ou scaler si votre serveur ne peut en servir d’avantage (voir mon article sur l’installation de PHP-FPM pour calculer le nombre max de processus),
  • soit la cause n’est pas normale et il faut y remédier.

Par cause anormale, j’entends deux choses : des requêtes traitées par PHP qui pourraient être mises en cache et servies directement par le serveur web, ou un trafic indésirable (des bots par exemple).

Dans mon cas, il s’agissait de la seconde situation. Des bots ne cessaient de requêter xmlrpc.php. Donc en plus d’un danger potentiel car ces bots cherchent à trouver le mot de passe permettant d’interagir avec cette API, cela pénalisait fortement les performances.

La solution est donc d’interdir l’accès à cette ressource si elle n’est pas utilisée. On peut la désactiver au niveau de WordPress ou en interdire l’accès au niveau du serveur web. C’est donc ce que j’ai fait avec Apache :

<Files xmlrpc.php>
    Require all denied
</Files>

Enfin, pour me débarrasser de ces intrus fort encombrants, s’ils insistent un peu trop, je les bloque via fail2ban. L’usage de fail2ban sort du cadre de cet article, cependant, car j’ai justement écrit un article à ce sujet !

Et voilà, PHP devrait maintenant retrouver un peu d’air pour respirer et vos sites renouer avec les performances si chères à nos visiteurs.

Déjà 3 réponses, rejoignez la discussion !

  • Tranxene50

    dit :

    Salut !

    Merci pour l’article. :)

    Cependant, atteindre le nombre de processus PHP disponibles n’est pas un souci, bien au contraire.

    Exemple : 16 processus PHP pour traiter 512 requêtes simultanées pendant 60 secondes (bench)

    Pour info, j’ai testé avec un site « classique » sous WordPress et sans dispositif de cache.

    Le load average monte au delà de 16 mais toutes les requêtes sont correctement traitées (HTTP 200).

    Req/s => 13.25 (bof…)
    Requêtes totales => 795 en 60s (soit ~45000 par heure)

    Avec un système de cache pour WordPress, PHP est alors totalement court-circuité et on peut atteindre ~1000 req/s (c’est Apache qui s’en charge)

    Et si on ajoute un Varnish dans la boucle (qui stocke les pages HTML pendant X secondes), j’imagine que l’on peut encore nettement augmenter les perfs.

    A+

    • Buzut

      dit :

      Hello!

      Merci pour ton commentaire détaillé et très intéressant. Le problème qui peut se poser – et s’est posé – c’est que certains traitements PHP sont lents.

      Ainsi, si ton max_children est atteint, ta requête est en file d’attente, et tant qu’elle est dans cette file, elle maintient une ressource Apache occupée.

      Dès lors, si pour une raison ou une autre (attaque, gros succès, code pas optimisé…) de nouvelles connexions sont ajoutées plus rapidement qu’elles ne peuvent être traitées, alors tu atteints également le MaxRequestWorkers.

      À cet instant, même les connexions qui ne nécessitent pas PHP sont en attente et ton navigateur finit par arrêter pour cause de timeout (30sec généralement, si ta patience t’as permis d’attendre jusque là).

      Bien entendu, comme tu le soulignes, il est possible d’optimiser de pousser encore la config, notamment avec du Varnish ou d’autres solutions de cache.

      @+

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *