Laisser un commentaire

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 :

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 :

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 :

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.

Commentaires

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```