PHP est omniprésent et c’est sans doute le langage le plus largement déployé sur l’internet.

Cependant, il n’est pas vraiment connu pour ses capacités de haute performance, en particulier lorsqu’il s’agit de systèmes hautement concurrents. C’est la raison pour laquelle des langages tels que Node (oui, je sais, ce n’est pas un langage), Go et Elixir prennent le relais pour des cas d’utilisation aussi spécialisés.

Cela dit, il y a BEAUCOUP de choses que vous pouvez faire pour améliorer les performances de PHP sur votre serveur. Cet article se concentre sur le côté php-fpm, qui est le moyen naturel de configurer votre serveur si vous utilisez Nginx.

Si vous savez ce qu’est php-fpm, n’hésitez pas à passer à la section sur l’optimisation.

Qu’est-ce que PHP-fpm ?

Peu de développeurs s’intéressent à l’aspect DevOps des choses, et même parmi ceux qui le font, très peu savent ce qui se passe sous le capot. Il est intéressant de noter que lorsque le navigateur envoie une requête à un serveur utilisant PHP, ce n’est pas PHP qui constitue le premier point de contact, mais le serveur HTTP, dont les principaux sont Apache et Nginx. Ces “serveurs web” doivent ensuite décider comment se connecter à PHP et lui transmettre le type de requête, les données et les en-têtes.

request-response-in-php-e1542398057451
Le cycle demande-réponse dans le cas de PHP (Image credit : ProinerTech)

Dans les applications PHP modernes, la partie “fichier de recherche” ci-dessus est le fichier index.php, auquel le serveur est configuré pour déléguer toutes les requêtes.

La façon dont le serveur web se connecte à PHP a évolué, et cet article serait trop long si nous devions entrer dans les détails. Mais grosso modo, à l’époque où Apache dominait en tant que serveur web de choix, PHP était un module inclus dans le serveur.

Ainsi, à chaque fois qu’une requête était reçue, le serveur démarrait un nouveau processus, qui incluait automatiquement PHP, et l’exécutait. Cette méthode était appelée mod_php, abréviation de “PHP as a module” (PHP en tant que module) Cette approche avait ses limites, que Nginx a surmontées avec php-fpm.

Dans php-fpm, la responsabilité de la gestion des processus PHP incombe au programme PHP du serveur. En d’autres termes, le serveur web (Nginx, dans notre cas) ne se soucie pas de savoir où se trouve PHP et comment il est chargé, tant qu’il sait comment lui envoyer et recevoir des données. Si vous voulez, vous pouvez considérer PHP dans ce cas comme un autre serveur en lui-même, qui gère quelques processus PHP enfants pour les requêtes entrantes (ainsi, nous avons la requête qui atteint un serveur, qui est reçue par un serveur et transmise à un autre serveur — assez fou ! :-P).

Si vous avez déjà configuré Nginx, ou même si vous vous y êtes intéressé, vous rencontrerez quelque chose comme ceci :

    location ~ .php$ {
        try_files $uri =404 ;
        fastcgi_split_path_info ^(. .php)(/. )$ ;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock ;
        fastcgi_index index.php ;
        include fastcgi_params ;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name ;
    }

La ligne qui nous intéresse est la suivante : fastcgi_pass unix:/run/php/php7.2-fpm.sock;, qui indique à Nginx de communiquer avec le processus PHP à travers le socket nommé php7.2-fpm.sock. Ainsi, pour chaque requête entrante, Nginx écrit des données dans ce fichier et, lorsqu’il reçoit la sortie, la renvoie au navigateur.

Une fois de plus, je dois souligner que ce n’est pas l’image la plus complète ou la plus précise de ce qui se passe, mais c’est tout à fait exact pour la plupart des tâches DevOps.

Ceci mis à part, récapitulons ce que nous avons appris jusqu’à présent :

  • PHP ne reçoit pas directement les requêtes envoyées par les navigateurs. Les serveurs web comme Nginx les interceptent d’abord.
  • Le serveur web sait comment se connecter au processus PHP, et transmet toutes les données de la requête (il colle littéralement tout) à PHP.
  • Lorsque PHP a terminé son travail, il renvoie la réponse au serveur web, qui la renvoie au client (ou au navigateur, dans la plupart des cas).

Ou graphiquement :

php-and-nginx
Comment PHP et Nginx travaillent ensemble (Image credit : DataDog)

Bien, mais maintenant vient la question à un million de dollars : qu’est-ce que PHP-FPM exactement ?

La partie “FPM” de PHP signifie “Fast Process Manager”, ce qui est juste une façon sophistiquée de dire que le PHP qui tourne sur un serveur n’est pas un processus unique, mais plutôt quelques processus PHP qui sont créés, contrôlés et tués par ce gestionnaire de processus FPM. C’est à ce gestionnaire de processus que le serveur web transmet les requêtes.

Le PHP-FPM est un trou de lapin à lui tout seul, alors n’hésitez pas à l’explorer si vous le souhaitez, mais pour nos besoins, cette explication suffira 🙂

Pourquoi optimiser PHP-FPM ?

Pourquoi se préoccuper de toute cette danse alors que les choses fonctionnent bien ? Pourquoi ne pas laisser les choses telles qu’elles sont.

Ironiquement, c’est précisément le conseil que je donne pour la plupart des cas d’utilisation. Si votre installation fonctionne bien et ne présente pas de cas d’utilisation extraordinaires, utilisez les paramètres par défaut. Cependant, si vous envisagez de passer à une échelle supérieure à celle d’une seule machine, il est essentiel de tirer le maximum d’une seule machine, car cela peut réduire de moitié (voire plus !) la facture du serveur.

Une autre chose à réaliser est que Nginx a été conçu pour gérer d’énormes charges de travail. Il est capable de gérer des milliers de connexions en même temps, mais si ce n’est pas le cas de votre installation PHP, vous ne ferez que gaspiller des ressources car Nginx devra attendre que PHP termine le processus en cours et accepte le suivant, réduisant ainsi à néant tous les avantages que Nginx a été conçu pour fournir !

Ceci étant dit, regardons ce que nous changerions exactement lorsque nous essayons d’optimiser php-fpm.

Comment optimiser PHP-FPM ?

L’emplacement du fichier de configuration de php-fpm peut varier sur le serveur, vous devrez donc faire quelques recherches pour le localiser. Vous pouvez utiliser la commande find si vous êtes sous UNIX. Sur mon Ubuntu, le chemin est /etc/php/7.2/fpm/php-fpm.conf. Le 7.2 est, bien sûr, la version de PHP que j’utilise.

Voici à quoi ressemblent les premières lignes de ce fichier :

;;;;;;;;;;;;;;;;;;;; ;
configuration FPM ;
;;;;;;;;;;;;;;;;;;;; ;

tous les chemins relatifs dans ce fichier de configuration sont relatifs au préfixe d'installation de PHP (/usr)
; préfixe (/usr). Ce préfixe peut être modifié dynamiquement en utilisant l'argument
; l'argument '-p' de la ligne de commande.

;;;;;;;;;;;;;;;;; ;
options globales ;
;;;;;;;;;;;;;;;;; ;

[global]
; Fichier Pid
; Note : le préfixe par défaut est /var
; Valeur par défaut : none
pid = /run/php/php7.2-fpm.pid

; Fichier journal des erreurs
; S'il est défini sur "syslog", le journal est envoyé à syslogd au lieu d'être écrit
dans un fichier local.
remarque : le préfixe par défaut est /var
; Valeur par défaut : log/php-fpm.log
error_log = /var/log/php7.2-fpm.log

Quelques éléments devraient être immédiatement évidents : la ligne pid = /run/php/php7.2-fpm.pid nous indique quel fichier contient l’identifiant du processus php-fpm.

Nous voyons également que /var/log/php7.2-fpm.log est l’endroit où php-fpm va stocker ses logs.

A l’intérieur de ce fichier, ajoutez trois variables supplémentaires comme ceci :

emergency_restart_threshold 10
emergency_restart_interval 1m
process_control_timeout 10s

Les deux premiers paramètres sont des avertissements et indiquent au processus php-fpm que si dix processus enfants échouent en l’espace d’une minute, le processus php-fpm principal doit se redémarrer lui-même.

Cela peut ne pas sembler robuste, mais PHP est un processus de courte durée qui laisse échapper de la mémoire, donc redémarrer le processus principal en cas d’échec important peut résoudre beaucoup de problèmes.

La troisième option, process_control_timeout, indique aux processus enfants d’attendre ce temps avant d’exécuter le signal reçu du processus parent. Cette option est utile dans les cas où les processus enfants sont en train de faire quelque chose lorsque le processus parent envoie un signal KILL, par exemple. Avec dix secondes sous la main, ils auront plus de chances de terminer leurs tâches et de sortir de manière élégante.

Étonnamment, ce n’est pas le cœur de la configuration de php-fpm! En effet, pour servir les requêtes web, le php-fpm crée un nouveau pool de processus, qui aura une configuration distincte. Dans mon cas, le nom du pool s’est avéré être www et le fichier que je voulais éditer était /etc/php/7.2/fpm/pool.d/www.conf.

Voyons comment commence ce fichier :

; Démarrez un nouveau pool nommé 'www'.
; la variable $pool peut être utilisée dans n'importe quelle directive et sera remplacée par le ; nom du pool ('www' ici)
; nom du pool ('www' ici)
[www]

; Par préfixe de pool
; Cela ne s'applique qu'aux directives suivantes :
; - 'access.log'
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values' ; - 'php_admin_values'
; - 'php_admin_values'
; S'il n'est pas défini, le préfixe global (ou /usr) s'applique à la place.
note : Cette directive peut également être relative au préfixe global.
valeur par défaut : none
;préfixe = /path/to/pools/$pool

; Utilisateur Unix/groupe de processus
remarque : l'utilisateur est obligatoire. Si le groupe n'est pas défini, le groupe par défaut de l'utilisateur
par défaut de l'utilisateur ; sera utilisé.
utilisateur = www-data
group = www-data

Un rapide coup d’oeil à la fin de l’extrait ci-dessus résout l’énigme de la raison pour laquelle le processus serveur s’exécute en tant que www-data. Si vous avez rencontré des problèmes d’autorisation de fichiers lors de la configuration de votre site web, vous avez probablement changé le propriétaire ou le groupe du répertoire en www-data, permettant ainsi au processus PHP d’écrire dans les fichiers journaux et de télécharger des documents, etc.

Enfin, nous arrivons à la source du problème, le paramètre du gestionnaire de processus (pm). En règle générale, les valeurs par défaut sont les suivantes

pm = dynamique
pm.max_children = 5
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 200

Alors, que signifie“dynamique” ici ? Je pense que c’est la documentation officielle qui l’explique le mieux (je veux dire que cela devrait déjà faire partie du fichier que vous éditez, mais je l’ai reproduit ici juste au cas où ce ne serait pas le cas) :

; Choisissez comment le gestionnaire de processus contrôlera le nombre de processus enfants.
valeurs possibles :
; statique - un nombre fixe (pm.max_children) de processus enfants ;
; dynamique - le nombre de processus enfants est fixé dynamiquement en fonction des directives
; dynamique - le nombre de processus enfants est fixé dynamiquement en fonction des directives suivantes. Avec cette gestion des processus, il y aura ; toujours au moins un
; toujours au moins 1 enfant.
; pm.max_children - le nombre maximum d'enfants qui peuvent ; être en vie en même temps
; être en vie en même temps.
; pm.start_servers - le nombre d'enfants créés au démarrage.
; pm.min_spare_servers - le nombre minimum d'enfants en état d'inactivité
; inactifs (en attente d'un traitement). Si le nombre
; de processus "inactifs" est inférieur à ce nombre, des
; nombre, certains enfants seront créés.
; pm.max_spare_servers - le nombre maximum d'enfants en état d'inactivité
; inactifs (en attente d'un processus). Si le nombre
; de processus "inactifs" est supérieur à ce nombre, certains
; est supérieur à ce nombre, certains enfants seront tués.
ondemand - aucun enfant n'est créé au démarrage. Les enfants seront bifurqués lorsque
; nouvelles demandes se connectent. Les paramètres suivants sont utilisés :
; pm.max_children - le nombre maximum d'enfants qui ; peuvent être en vie en même temps
; peuvent être en vie en même temps.
; pm.process_idle_timeout - Le nombre de secondes au bout desquelles ; un processus inactif sera tué
; un processus inactif sera tué.
remarque : cette valeur est obligatoire.

Nous voyons donc qu’il y a trois valeurs possibles :

  • Statique: Un nombre fixe de processus PHP sera maintenu quoi qu’il arrive.
  • Dynamique: Nous pouvons spécifier le nombre minimum et maximum de processus que php-fpm maintiendra en vie à un moment donné.
  • à la demande: Les processus sont créés et détruits à la demande.

Quelle est l’importance de ces paramètres ?

En termes simples, si vous avez un site web à faible trafic, le paramètre “dynamique” est un gaspillage de ressources la plupart du temps. Si vous avez défini pm.min_spare_servers à 3, trois processus PHP seront créés et maintenus même en l’absence de trafic sur le site web. Dans de tels cas, “ondemand” est une meilleure option, laissant le système décider quand lancer de nouveaux processus.

D’autre part, les sites web qui gèrent de grandes quantités de trafic ou qui doivent réagir rapidement seront pénalisés par ce paramètre. La création d’un nouveau processus PHP, son intégration dans un pool et sa surveillance constituent une charge supplémentaire qu’il est préférable d’éviter.

L’utilisation de pm = static fixe le nombre de processus enfants, ce qui permet d’utiliser le maximum de ressources système pour servir les requêtes plutôt que pour gérer PHP. Si vous optez pour cette solution, sachez qu’elle a ses règles et ses pièges. Un article assez dense mais très utile à ce sujet se trouve ici.

Dernières paroles

Étant donné que les articles sur les performances des sites web peuvent déclencher des guerres ou servir à embrouiller les gens, je pense que quelques mots s’imposent avant de clore cet article. L’optimisation des performances est autant une affaire de suppositions et de magie noire que de connaissance du système.

Même si vous connaissez par cœur tous les paramètres de php-fpm, le succès n’est pas garanti. Si vous n’avez aucune idée de l’existence de php-fpm, vous n’avez pas besoin de perdre votre temps à vous en préoccuper. Continuez simplement à faire ce que vous faites déjà et continuez.

En même temps, évitez de devenir un accro de la performance. Oui, vous pouvez obtenir de meilleures performances en recompilant PHP à partir de zéro et en supprimant tous les modules que vous n’utiliserez pas, mais cette approche n’est pas assez saine dans les environnements de production. L’idée d’optimiser quelque chose est de regarder si vos besoins diffèrent des valeurs par défaut (ce qui est rarement le cas !), et de faire des changements mineurs si nécessaire.

Si vous n’êtes pas prêt à passer du temps à optimiser vos serveurs PHP, vous pouvez alors envisager d’utiliser une plateforme fiable comme Kinsta qui s’occupe de l’optimisation des performances et de la sécurité.