Featured image of post Améliorer drastiquement les performances de votre site PHP

Améliorer drastiquement les performances de votre site PHP

Découvrez comment optimiser votre installation PHP sur votre serveur de production pour réduire votre temps de réponse de 50% à 75% !

Introduction

Lorsque vous lancez votre site Web en production, il est crucial de veiller à ce qu’il offre des performances optimales. Malheureusement, de nombreux sites PHP ne parviennent pas à atteindre leur plein potentiel en termes de rapidité et de réactivité. Une application PHP non optimisée peut entraîner des temps de chargement lents, une expérience utilisateur médiocre et une utilisation inefficace des ressources du serveur. Dans cet article, nous explorerons les risques associés à l’utilisation d’un site PHP non optimisé et l’importance d’adopter des pratiques d’optimisation pour offrir une expérience utilisateur exceptionnelle.

Un site PHP non optimisé peut entraîner des retards significatifs lors du chargement des pages, ce qui peut frustrer les visiteurs et réduire leur engagement. Dans un monde où les utilisateurs sont habitués à une navigation rapide et fluide, chaque milliseconde compte. Si votre site est lent à charger, les visiteurs peuvent se détourner et chercher des alternatives plus rapides et plus performantes.

En outre, un site PHP non optimisé peut avoir un impact négatif sur l’utilisation des ressources du serveur. Des temps de réponse lents signifient que le serveur est capable de traiter moins de requêtes par unité de temps, ce qui réduit sa capacité à gérer efficacement un trafic élevé. Par conséquent, votre site risque de rencontrer des problèmes de performances lorsque le nombre de visiteurs augmente.

De plus, un code PHP non optimisé peut entraîner une utilisation excessive des ressources système, telles que la mémoire et le processeur. Des opérations complexes et inefficaces, des requêtes SQL mal conçues ou des boucles mal optimisées peuvent ralentir l’exécution du code PHP et entraîner une surutilisation des ressources. Cela peut se traduire par des coûts supplémentaires si vous devez mettre à niveau votre infrastructure pour faire face à cette surcharge.

Il est donc crucial d’accorder une attention particulière à l’optimisation des performances dès les premières étapes du développement de votre application PHP. En adoptant des pratiques d’optimisation solides, en utilisant des outils de mise en cache tels que l’extension Opcache et en effectuant régulièrement des mises à jour de code, vous pouvez considérablement améliorer les performances de votre site et offrir une expérience utilisateur exceptionnelle.

Dans les chapitres suivants, nous explorerons en détail différentes techniques et approches pour optimiser les performances de votre application PHP. Nous aborderons le choix du serveur Web, le mode d’exécution PHP, l’utilisation de Composer, la configuration d’Opcache, la mise à jour du code et conclurons par des conseils pratiques. En suivant ces recommandations, vous serez en mesure de maximiser les performances de votre site PHP, offrant ainsi une expérience utilisateur fluide, réactive et satisfaisante.

Avant de vous livrer les précieux conseils, il est nécessaire de soulever le capot pour comprendre comment les serveurs Web fonctionnent avec les sites PHP en général, avant de pouvoir plonger plus en profondeur sur le fonctionnement interne du moteur de PHP.

Lorsqu’une requête HTTP arrive sur un serveur Web et doit être traitée par PHP, elle suit un workflow spécifique. Comprendre ce workflow est essentiel pour optimiser les performances de votre application PHP. Voici une description détaillée des étapes clés :

  • 1. Réception de la requête par le serveur Web
  • 2. Traitement des requêtes statiques
  • 3. Passage de la requête au moteur PHP
  • 4. Traitement par PHP
  • 5. Génération de la réponse
  • 6. Envoi de la réponse au serveur Web
  • 7. Réception de la réponse par le client

Ce mode de fonctionnement est commun pour tous les serveurs Web :

  1. Lorsqu’un utilisateur envoie une requête HTTP à votre serveur Web, celui-ci la reçoit en premier lieu. Le serveur Web peut être Apache, Nginx, ou tout autre serveur compatible PHP.
  2. Le serveur Web vérifie si la requête concerne un fichier statique, tel qu’une image, un fichier CSS ou JavaScript. Si tel est le cas, le serveur Web peut renvoyer directement le fichier au client sans impliquer PHP. Cela permet de gagner du temps et de libérer des ressources pour les requêtes dynamiques.
  3. Si la requête nécessite un traitement par PHP, le serveur Web transmet la requête au moteur PHP approprié. Dans le cas de PHP-FPM (PHP FastCGI Process Manager), le serveur Web communique avec le processus PHP-FPM pour exécuter le code PHP.
  4. Le moteur PHP-FPM reçoit la requête et exécute le code PHP associé. Cela peut inclure la récupération de données à partir de la base de données, le traitement de formulaires, l’accès aux fichiers, ou toute autre opération spécifique à l’application.
  5. Une fois que le code PHP est exécuté, il génère une réponse pour la requête. Cela peut être une page HTML complète, du JSON, du XML ou tout autre type de données à renvoyer au client.
  6. Le moteur PHP-FPM envoie la réponse générée au serveur Web, qui est chargé de la renvoyer au client. Le serveur Web peut également effectuer d’autres traitements, tels que la compression de la réponse ou l’ajout d’en-têtes supplémentaires.
  7. Enfin, le client (le navigateur Web) reçoit la réponse du serveur et traite les données reçues. Cela peut inclure le rendu de la page, l’exécution de scripts JavaScript ou tout autre traitement côté client.

Il est important de noter que chaque étape de ce workflow peut être optimisée pour améliorer les performances globales de l’application PHP. Par exemple, la mise en cache des requêtes statiques, l’utilisation d’un serveur Web léger et performant, l’optimisation du code PHP, l’utilisation d’outils de mise en cache tels que Opcache, ou encore l’optimisation des requêtes SQL peuvent tous contribuer à réduire les temps de réponse et à offrir une expérience utilisateur plus réactive.

Choix du Serveur Web

Lorsque vous développez une application PHP, le choix du serveur Web est une décision cruciale. Le serveur Web joue un rôle essentiel dans la gestion des requêtes HTTP et dans la performance globale de votre application. Il existe plusieurs options populaires, chacune ayant ses propres caractéristiques et avantages. Examinons de plus près certains des serveurs Web les plus couramment utilisés avec PHP.

Apache HTTP Server

Apache est l’un des serveurs Web les plus anciens et les plus répandus. Il est apprécié pour sa flexibilité et sa compatibilité avec de nombreux systèmes d’exploitation. Apache prend en charge PHP via le module mod_php, qui permet une intégration directe entre le serveur et le moteur PHP. Cela facilite le déploiement de scripts PHP et offre une grande compatibilité avec les applications existantes. Cependant, Apache peut être relativement lourd en termes de consommation de ressources, ce qui peut affecter les performances dans certains cas.

En effet, Apache possède plusieurs MPM (module de traitement des processus multi-threads “Multi-Processing Module”) et il est nécessaire de savoir choisir lequel est le plus adapté à notre besoin :

MPM prefork
Mode de gestion d'un autre âge, il s'agit du pire mode de gestion en terme de performances. Il n'y a qu'un seul processus Apache parent, qui crée à la volée un processus enfant lorsqu'une requête entre, avant de tuer ce processus enfant lorsqu'il a fini son travail. Dès que le serveur doit gérer plus de requêtes qu'il n'a de CPU, il va s'emballer, saturer, et les CPU vont rester bloqués à 100%, à dépenser plus d'énergie à créer et supprimer des processus plutôt qu'à servir des ressources. Autant dire qu'il est facile de DDOS un serveur Apache utilisant ce mode de gestion.
MPM worker
L'objectif principal de MPM worker est de fournir une meilleure gestion des ressources en utilisant un modèle multi-thread. Contrairement à MPM prefork, MPM worker utilise un ensemble de threads pour gérer les connexions et les requêtes. Cela permet d'économiser des ressources système en réduisant le surcoût de création et de gestion des processus individuels. MPM worker est particulièrement efficace dans les situations où le serveur doit gérer un grand nombre de connexions simultanées.
MPM event
MPM event est une variante de MPM worker introduite à partir d'Apache 2.4. Il améliore encore la performance en introduisant une gestion plus efficace des requêtes persistantes. Avec MPM event, les connexions persistantes sont traitées à part dans un thread dédié, tandis que les autres connexions sont gérées comme avec MPM worker, dans un pool de threads. Cela permet de maximiser l'utilisation des ressources du serveur et d'améliorer la capacité de réponse aux requêtes.

Il convient de noter que le choix du MPM dépend de plusieurs facteurs, notamment du type d’application, du nombre de connexions simultanées attendues et des ressources disponibles. MPM prefork est souvent utilisé avec PHP sous sa forme mod_php, car il offre une compatibilité directe avec ce module sans avoir de faire de configuration. Cependant, MPM worker et MPM event offrent de bien meilleures performances dans des environnements à fort trafic. Il est fortement recommandé de ne pas utiliser MPM prefork en dehors de son poste de développement, et encore moins en production.

Nginx

Nginx est un serveur Web léger, performant et de plus en plus populaire. Il est réputé pour sa capacité à gérer efficacement de nombreuses connexions simultanées avec une consommation de ressources minimale. Nginx peut être utilisé avec PHP en utilisant PHP-FPM (PHP FastCGI Process Manager) pour gérer les requêtes PHP. Cette combinaison offre une bonne performance et une grande stabilité, ce qui en fait un choix judicieux pour les applications à fort trafic.

Contrairement à Apache, Nginx utilise un modèle de traitement asynchrone et événementiel qui lui permet de gérer un grand nombre de connexions simultanées de manière efficace avec une consommation de ressources réduite. Jetons un coup d’œil à la gestion des processus dans Nginx.

Nginx master process
Lorsque Nginx démarre, il crée un processus principal appelé le "master process". Le rôle du processus principal est de coordonner les autres processus et de gérer les tâches administratives. Le master process lit la configuration, ouvre les sockets d'écoute et supervise les processus de travail.
Nginx worker processes
Le master process crée également un ou plusieurs "worker processes" qui effectuent le travail réel de traitement des requêtes. Chaque worker process est indépendant et peut gérer plusieurs connexions simultanées en utilisant des mécanismes d'entrées/sorties non bloquantes et des événements.
Nginx cache manager process
En plus des processus principaux et des processus de travail, Nginx peut également créer un processus supplémentaire appelé le "cache manager process". Ce processus est responsable de la gestion du cache de contenu statique. Il surveille l'utilisation du cache, supprime les entrées expirées et libère de l'espace lorsque cela est nécessaire.

L’architecture de Nginx basée sur des threads lui permet de gérer un grand nombre de connexions simultanées avec une empreinte mémoire relativement faible. Contrairement à Apache, qui utilise un modèle de processus préfork ou multi-thread, Nginx suit un modèle événementiel non bloquant qui permet d’économiser des ressources système et d’améliorer les performances globales.

Nginx est capable de servir plusieurs milliers de ressources statiques à la seconde, ce qui en fait un serveur Web de choix, loin devant Apache, lorsque la priorité est mise sur la performance. Néanmoins, point important à garder en tête, je ne parle pour le moment que de fichiers statiques. Lorsqu’il s’agit de servir des pages PHP, c’est bien différent est nécessite un chapitre à part, comme celui qui suit !

Choisir le bon mode d’exécution PHP

Il existe 2 manières de faire fonctionner un site PHP. La première, simple, facile à mettre en place, pratique pour les développeurs, mais à peu près aussi performante que de conduire une voiture avec des roues carrées, et une autre plus complexe, mais faite spécifiquement pour des performances maximales dans un environnement de production.

mod_php

Commençons d’abord avec mod_php. Dans ce mode, PHP est intégré directement dans le serveur Apache en tant que module. Cela signifie que chaque processus Apache dispose de son propre interpréteur PHP embarqué, ce qui permet une intégration étroite entre les deux. Cependant, cela peut entraîner une surcharge de mémoire si vous avez de nombreux processus Apache en cours d’exécution simultanément, car chaque processus doit charger et maintenir son propre interpréteur PHP. En effet, pour utiliser le module PHP, Apache doit utiliser le MPM prefork, qui comme on l’a vu plus tôt, est la pire option possible en termes de performances.

En effet, traiter une requête avec mod_php donne le workflow suivant :

  • Requête
  • Apache master
  • + Apache enfant
  • + PHP
  • - PHP
  • - Apache enfant
  • Réponse
  1. Comme vu avec le MPM prefork, le processus Apache master crée un processus enfant pour traiter chaque requête.
  2. Le processus enfant réalise qu’il s’agit d’une requête pour un fichier PHP, et donc créée un processus PHP pour traiter cette requête.
  3. Le processus PHP traite la requête et retourne la réponse au processus Apache enfant.
  4. Le processus Apache enfant tue le processus PHP et retourne la réponse au processus Apache master.
  5. Le processus Apache master tue le processus Apache enfant et retourne la réponse à l’utilisateur.

Pour que vous visualisiez mieux l’absurdité de la situation, c’est comme si vous alliez dans un restaurant:

  1. Que le responsable de l’accueil sortait une baguette magique pour faire apparaître une table et un serveur, qui resterait derrière vous et ne traiterait que votre commande à vous.
  2. Que lorsque vous voulez passer une commande, le serveur sortait lui aussi une baguette magique pour faire apparaître une cuisine tout entière, avec les cuisiniers, juste pour votre commande à vous.
  3. Que lorsque les cuisiniers sont terminés de préparer votre assiette, le serveur sortait un .44 magnum de sa poche et abattait froidement les cuisiniers, avant de faire disparaître la cuisine avec sa baguette magique.
  4. Et enfin, lorsque vous avez terminé de manger et que vous ressortez du restaurant, le responsable de l’accueil abattait lui aussi à son tour le serveur qu’il avait fait apparaître rien que pour vous.
  5. Et ce, individuellement, pour chaque client venant manger dans ce restaurant.

Alors autant j’avoue que ce serait le restaurant le plus METAL 🤘🏻 dans lequel j’aurais été manger, mais ce serait aussi le restaurant le moins efficace du monde.

Morale de l’histoire, ne pas tuer ses employés, c’est bien. Et c’est exactement ce que fait PHP-FPM !

PHP-FPM

PHP-FPM est une solution de gestion des processus FastCGI pour PHP. Dans ce mode, PHP est exécuté en tant que processus indépendant, séparé du serveur Web. PHP-FPM gère les processus PHP de manière efficace, reprenant le fonctionnement de Apache MPM worker, MPM event ou de Nginx, PHP-FPM crée à l’avance un pool de processus enfants qui se chargeront de traiter les requêtes. Ainsi, on évite de créer et tuer des processus enfants à tour de bras, et on concentre les ressources serveur à générer des pages. Cela permet d’optimiser l’utilisation des ressources système et d’améliorer les performances globales. De plus, PHP-FPM offre une grande flexibilité, car il peut être utilisé avec différents serveurs Web tels que Nginx, Apache (via MPM event ou MPM worker). Il facilite également la mise à jour de PHP sans avoir à redémarrer le serveur Web. PHP-FPM est particulièrement recommandé pour les environnements à fort trafic et où la scalabilité est un facteur clé.

Le second intérêt d’utiliser PHP-FPM (en dehors des performances), c’est qu’on peut en installer plusieurs, pour supporter plusieurs versions de PHP en parallèle, ce qui est très pratique lorsque l’on gère un serveur qui doit servir plusieurs sites, chacun tournant sur une version de PHP différente (même si c’est vraiment important de mettre à jour son code à la dernière version de PHP).

À présent que nous avons installé PHP-FPM, il convient de le configurer proprement. Les valeurs par défaut du fichier /etc/php/<votre_version>/fpm/pool.d/www.conf sont correctes pour une utilisation classique, mais ne sont vraiment pas adaptés pour un site devant gérer un très fort trafic.

Les paramètres qui nous intéressent sont les suivants :

ParamètreDescriptionOptions
pm

Comment PHP-FPM doit gérer le nombre de ses processus enfants.

static : Le nombre ne change pas.

dynamic : Le nombre fluctue en fonction de la charge, mais dans une limite définie.

ondemand : Les enfants sont créés à la demande (comme le mode mpm_prefork pour Apache).

pm.max_children

Nombre maximum de processus enfants.

pm=static : C'est le nombre de processus enfants qui seront utilisés.

pm=dynamic : Le nombre de processus enfants pourra monter jusqu'à cette limite.

pm=ondemand : Le nombre de processus enfants pourra monter jusqu'à cette limite.

pm.start_servers

Nombre de processus enfants à créer au démarrage de PHP-FPM.

Utilisé uniquement avec pm=dynamic.
pm.min_spare_servers

Nombre minimum de processus enfants à garder même quand il n'y a plus assez de trafic pour qu'ils travaillent.

Utilisé uniquement avec pm=dynamic.
pm.max_spare_servers

Nombre maximum de processus enfants à garder quand il n'y a plus assez de trafic pour qu'ils travaillent.

Utilisé uniquement avec pm=dynamic.
pm.process_idle_timeout

Nombre de secondes d'inactivité après lesquels un processus enfant est tué.

Utilisé uniquement avec pm=ondemand.
pm.max_requests

Nombre requêtes qu'un processus enfant peut traiter avant d'être redémarré.

Utilisé avec tous les modes.

Pour les sites ayant un fort trafic, ces paramètres ne sont pas adaptés. Il est préférable de passer PHP-FPM en mode static, de sortir sa calculette, et de configurer correctement les paramètres de la manière suivante :

pm=static
On ne dépense plus de ressources à créer ou tuer des processus enfants. Même si le trafic baisse on continue de maintenir le même nombre des processus enfants qui consomment des ressources serveur.
pm.max_children=(sortez votre calculette)

Cette valeur est très difficile à calculer, et une erreur dans l'estimation peut finir par saturer votre serveur. Tout d'abord, il faut estimer la consommation moyenne de RAM de votre application PHP (exemple : 100 Mo). Ensuite vous prenez le total de RAM que votre serveur possède (exemple : 12 Go) auquel vous soustrayez une marge pour l'OS et les autres applications présentes sur le serveur (exemple : -3 Go). Vous prenez le total restant que vous divisez par la consommation de RAM de votre application PHP trouvée à la première étape.

Dans mon exemple, avec une app PHP qui consomme 100 Mo sur un serveur ayant 12 Go de RAM et pour lequel on garde en réserve 3 Go de RAM pour les autres applications, cela nous donne (12 - 3) / 0.1 = 90 processus enfants.

Si vous êtes trop optimistes sur la consommation de votre application PHP, vous allez finir par entièrement saturer la RAM de votre serveur. Et si à l'inverse vous l'imaginez trop lourde par rapport à la réalité, vous n'allez pas tirer pleinement parti des performances du serveur.

pm.max_requests=1000
Pour de la production, on peut au moins autoriser 1'000 requêtes par processus enfant, car le code de votre application devrait être suffisamment stable et optimisé pour ne pas trop avoir de fuites de mémoire. Si le code de votre application a été fait avec amour, vous pouvez monter cette limite jusqu'à 5'000, voir même 10'000 si vous ne constatez aucun problème de mémoire sur le long terme.

Configurer PHP-FPM n’est pas extrêmement difficile, mais nécessite soit une très bonne connaissance de la consommation RAM, à la fois de votre site PHP et des autres applications installées sur le serveur, soit d’effectuer un monitoring régulier afin d’optimiser la configuration petit à petit.

À noter aussi que côté performance, utiliser Apache ou Nginx ne fait aucune différence de performance avec PHP-FPM, puisque tous les 2 se contentent de jouer les passe-plats entre la requête de l’utilisateur et PHP-FPM (mais Nginx reste loin devant pour toutes les autres requêtes statiques).

Composer

Composer est aujourd’hui un outil incontournable pour la gestion des dépendances dans les applications PHP. Il agit en tant que point d’entrée central pour répertorier toutes les classes, localiser leurs emplacements physiques sur le disque et les charger en mémoire lorsque nécessaire.

Heureusement, Composer effectue déjà cette tâche automatiquement. Les jours où nous devions utiliser des instructions require_once dans tous nos fichiers PHP sont révolus depuis longtemps.

Cependant, saviez-vous que Composer offre également des options pour optimiser ces chargements ? Néanmoins, ces options ne doivent pas être utilisées pendant le développement, car elles empêchent le chargement des nouvelles dépendances installées ultérieurement (il sera nécessaire de lancer une nouvelle installation globale via Composer).

Par défaut, lorsque vous importez une classe, Composer vérifie en arrière-plan si le fichier correspondant existe sur le disque, puis le charge. Cela signifie qu’il y a deux accès au disque par fichier. Avec les sites actuels utilisant des centaines de bibliothèques, totalisant ainsi plusieurs milliers de classes, ces vérifications ont un coût en termes de CPU et d’accès disque non négligeable. Cependant, si toutes les bibliothèques respectent les normes PSR en matière de nommage de classes et de namespace, il est possible d’optimiser Composer pour qu’il génère un unique et vaste tableau répertoriant toutes les classes et leurs emplacements, sans vérifier si elles existent effectivement sur le disque dur. Cette optimisation permet de passer directement à l’étape de chargement du fichier, sans dépenser de ressources pour vérifier son existence (à moins que vous ne disposiez d’un disque dur qui fait mystérieusement disparaître des fichiers de manière aléatoire, les fichiers devraient toujours être présents).

Lors de l’installation de votre site PHP sur le serveur, remplacez la commande composer install par composer install -o -a.

Si vous avez déjà installé vos dépendances, vous pouvez exécuter la commande composer dump-autoload -o -a pour obtenir le même résultat sans réinstaller l’ensemble des dépendances.

Désormais, toutes les classes importées par Composer seront chargées plus rapidement, grâce au fait que leur namespace fournit suffisamment d’informations pour localiser leur fichier physiquement sur le disque dur. Cela réduit de moitié les opérations nécessaires pour les charger.

Veuillez noter que cette optimisation ne fonctionne QU’AVEC les sites qui respectent les normes PSR. Étant donné que Drupal installe physiquement les fichiers de son noyau à un emplacement différent de celui indiqué par leur espace de noms, cette optimisation ne peut pas être utilisée dans ce cas.

Configurer Opcache

Opcache est un système de cache interne de PHP conçu pour accélérer le chargement et l’exécution des fichiers PHP. Il est installé par défaut avec les versions récentes de PHP, mais sa configuration par défaut est très légère et n’est pas adaptée aux applications complexes et volumineuses telles que les sites basés sur Symfony, Laravel, Drupal, et autres grands frameworks PHP.

Pour comprendre le fonctionnement d’Opcache, il est important de comprendre comment PHP fonctionne réellement. En effet, lorsque vous livrez du code source PHP sur un serveur, ce n’est pas exactement ce qui est exécuté lors de l’appel à ces fichiers.

  • Code source
  • Parsing
  • Bytecode
  • Exécution
  • Réponse
  1. Lorsque le moteur PHP est chargé d’exécuter le contenu d’un fichier PHP, il le charge et le parse pour générer une structure plus compréhensible pour une machine. Ce processus de parsing génère un arbre logique abstrait appelé Abstract Syntax Tree (AST). Le parsing est extrêmement coûteux en termes de calcul CPU, d’autant plus que chaque nouvelle version de PHP optimise davantage les performances lors de l’exécution, ce qui implique une complexité accrue et une augmentation du temps de calcul lors de la génération de l’AST.
  2. L’arbre logique abstrait est ensuite transformé en bytecode (opcode), qui est une représentation binaire optimisée et prête à être exécutée par le moteur PHP.
  3. Le moteur PHP importe le bytecode et l’exécute en utilisant des instructions directes pour le CPU et la machine virtuelle PHP (oui, comme Java, mais cachée dans ses entrailles et personne n’y fait attention). Il exécute toutes les instructions jusqu’à ce que la réponse soit renvoyée au processus qui a demandé le chargement du fichier PHP..

Ces étapes sont répétées à chaque chargement de fichier, même si c’est le même fichier qui est réappelé à chaque fois.

On peut rapidement constater qu’il existe un problème de performance. Étant donné que le fichier source ne change pas à chaque appel, pourquoi parser et générer du bytecode à chaque fois ? C’est précisément là qu’intervient Opcache.

Opcache s’interpose entre la lecture du code source d’un fichier par le moteur PHP et la génération du bytecode, en mettant en cache le bytecode généré. Il contourne ainsi toute la partie coûteuse en ressources, et inutile puisque les fichiers n’ont pas changé entre deux requêtes.

Ainsi, dès le deuxième appel, le workflow PHP se résume à :

  • Exécution
  • Réponse

On peut alors clairement percevoir les gains de performances offerts par PHP 😁.

Cependant, comme mentionnée précédemment, la configuration par défaut d’Opcache n’est pas adaptée aux sites volumineux ni aux environnements de production. Voyons maintenant comment corriger cela.

Cache

Dans le fichier php.ini de votre installation, à la section “Opcache” située vers la fin du fichier, vous trouverez les clés de configuration suivantes :

opcache.enable

Cette clé détermine si Opcache doit être activé pour les requêtes Web. Il est recommandé de l’activer en définissant la valeur à 1.

opcache.enable_cli

Cette clé concerne l’activation d’Opcache pour les commandes en CLI (Command Line Interface) de PHP. Si vous utilisez le même fichier php.ini pour les requêtes Web et les commandes CLI, il est préférable de désactiver Opcache en CLI en définissant la valeur à 0.

Cela garantit l’utilisation de la version du code présente sur le disque lors des déploiements, même si cela peut être plus lent, plutôt que de contourner ce contrôle et de charger une version antérieure en mémoire.

opcache.memory_consumption

Cette clé détermine la quantité maximale de cache que vous autorisez à occuper en mémoire. Il est recommandé de définir la valeur maximale possible, soit 128 Mo. Étant donné que ce cache est partagé entre les différents processus PHP-FPM, son impact sur la RAM est minimal.

opcache.interned_strings_buffer

Pour cette valeur-là, il faut comprendre comment PHP gère les valeurs textuelles réelles en dur dans le code. Si je vous donne le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Entity;

class User
{
  private string $name = "John";

  public function getName(): string
  {
      return $this->name;
  }

  public function setName(string $name): void
  {
      $this->name = $name;
  }
}

Si je vous dis que la valeur John est une string mise en cache par PHP, je ne devrais pas vous surprendre 😉. En revanche, si je vous dis que PHP a aussi extrait App\Entity, User, $name, getName et setName en tant que string à mettre en cache, c’est déjà un peu plus surprenant, et en même temps logique ! C’est tout autant de chaînes de texte que l’on pourrait appeler dans du code. Et donc Opcache extrait toutes ces chaînes, qu’il s’agisse de vrai texte en dur, ou namespace, nom de classe, nom de variable ou nom de fonction, et les stocks dans son cache !

En effet, en plus des valeurs textuelles réelles, Opcache extrait et met en cache les chaînes correspondant aux namespaces, noms de classe, noms de variables et noms de fonctions. Dans le cas d’applications PHP volumineuses avec de nombreux “vendors”, ce cache peut se remplir rapidement. Il est donc recommandé de définir la valeur maximale, actuellement 32 Mo.

opcache.max_accelerated_files

Cette clé définit le nombre maximal de fichiers PHP pour lesquels Opcache conserve leur représentation bytecode en mémoire. Dans les projets importants comptant un grand nombre de fichiers PHP (y compris les “vendors”), la limite par défaut est rapidement atteinte. Il est recommandé de définir la limite maximale à 100000 fichiers.

opcache.max_wasted_percentage

Cette clé spécifie le pourcentage maximal de cache gaspillé (fuite de mémoire) autorisé en mémoire avant que PHP ne redémarre pour vider son cache de force. Si vous disposez de suffisamment de RAM sur votre serveur, il est recommandé de définir cette valeur à 15%. Cela permet de trouver un équilibre entre le maintien des performances au détriment de la consommation de RAM et le temps dépensé pour redémarrer les processus PHP plutôt que de répondre aux requêtes.

opcache.validate_timestamps

Là ça va être subtil. Par défaut, Opcache stocke le moment où la mise en cache du bytecode d’un fichier PHP a été effectuée, afin de ne pas vérifier le fichier à chaque appel, mais plutôt après quelques secondes (2 secondes par défaut).

Autant c’est très aimable de la part de PHP et developer-friendly, autant cela entraîne une perte de temps et de ressources en production, car les fichiers PHP ne sont généralement pas modifiés fréquemment et jamais tout seuls.

Pour maximiser les performances, il est recommandé de définir cette valeur à 0, ce qui permet à PHP de conserver les fichiers en cache en permanence sans vérifier s’ils ont été modifiés sur le disque.

En contrepartie, lors du déploiement d’une nouvelle version sur le serveur, il sera nécessaire de redémarrer PHP-FPM pour vider complètement le cache et charger la nouvelle version des fichiers.

Avec cette nouvelle configuration, votre site PHP devrait déjà bénéficier d’une amélioration de performances de l’ordre de 15% à 20%, ce qui est significatif pour l’optimisation de votre application 😁.

Preload

Pour aller encore plus loin dans l’optimisation des performances de votre site PHP, nous allons exploiter la fonctionnalité de Preload introduite à partir de PHP 7.4.

Le concept du Preload est similaire à ce que nous avons déjà mis en place avec Opcache, où nous configurions Opcache pour conserver en permanence la version en cache sans jamais vérifier les fichiers sur le disque. Cependant, le Preload va encore plus loin. Au lieu de stocker le bytecode en RAM, il stocke le code compilé lui-même et dans le coeur de PHP. Cela signifie que les fonctions préchargées ne font pas partie de votre code, mais deviennent des éléments natifs de PHP disponibles de manière globale. L’avantage est un gain significatif de performances pour les classes chargées via le Preload. Cependant, l’inconvénient est que, tout comme avec la configuration Opcache en mode intensif, vous devrez redémarrer PHP-FPM après le déploiement d’une nouvelle version.

Il est important de noter que la possibilité d’utiliser le Preload dépend de la solution sur laquelle votre site est construit. Pour pouvoir “précharger” des classes, vous devez fournir à PHP un fichier spécial à exécuter au démarrage du service PHP-FPM. Ce fichier mettra en mémoire les classes les plus fréquemment utilisées.

Par exemple, si votre site est basé sur Symfony, le Preload est pris en charge nativement et le fichier à fournir à PHP est app/config/preload.php. En revanche, pour Drupal, le Preload n’est pas pris en charge nativement, mais il existe un module permettant de le faire.

Une fois que vous avez votre fichier PHP de Preload, il vous suffit de modifier votre php.ini et de spécifier le CHEMIN ABSOLU de votre fichier de Preload comme valeur de la configuration opcache.preload. Cette étape permet à PHP de charger et de mettre en mémoire les classes préchargées, ce qui entraîne des améliorations significatives des performances.

N’oubliez pas que lors de chaque déploiement d’une nouvelle version de votre site, vous devrez redémarrer PHP-FPM pour que les modifications apportées au Preload prennent effet et que les nouvelles classes soient chargées.

Mettre a jour son code

Félicitations ! Maintenant que vous avez installé Nginx, migré vers PHP-FPM, configuré celui-ci pour tirer le meilleur parti des ressources de votre serveur de production, optimisé Opcache et mis en place le Preload, vous avez réussi à réduire de moitié, au minimum, les temps d’exécution nécessaires pour générer les pages de vos visiteurs ! 🎉

Cependant, j’ai une mauvaise nouvelle à vous annoncer. Vous avez atteint le sommet de la montagne, vous êtes au sommet, et il n’y a plus rien à gravir pour aller plus haut. Vous avez optimisé votre serveur pour atteindre les performances maximales tout en respectant scrupuleusement votre code source et ses instructions.

Si malgré toutes ces optimisations, vos pages prennent encore plus de 100 ms à se générer, alors il est temps de retrousser vos manches et de procéder à un sérieux refactoring de votre code.

Voici quelques pistes à explorer :

  1. Mise à jour de PHP : Assurez-vous d’utiliser la dernière version de PHP. Chaque nouvelle release apporte des améliorations supplémentaires en termes de performances et d’optimisation.
  2. Optimisation des requêtes SQL : Vérifiez vos requêtes SQL. Disposez-vous de clés primaires, de clés étrangères et d’index sur les colonnes pertinentes ? Optimisez vos requêtes pour minimiser les temps d’accès à la base de données.
  3. Nombre de requêtes SQL : Évitez-vous d’effectuer 300 requêtes SQL à chaque chargement de page ? Préférez récupérer un ensemble de données et de boucler ensuite dessus plutôt que de faire des appels SQL à l’intérieur d’une boucle.
  4. Réduction des opérations complexes : Si vous effectuez des opérations complexes à chaque chargement de page, envisagez de les déplacer en arrière-plan ou de mettre en cache les résultats de ces opérations. Réduisez au maximum les tâches intensives afin d’accélérer la génération des pages.
  5. Utiliser des fonctions natives : Utilisez les fonctions natives de PHP, telles que array_map(), array_filter(), array_reduce(), etc., pour effectuer des opérations sur les tableaux. Ces fonctions sont généralement optimisées et offrent de bonnes performances.
  6. Remplacer les annotations par les attributs : Le système d’attributs ajouté depuis PHP 8 permet de remplacer les annotations greffées dans des commentaires et parsés au runtime, par des vrai objets natifs de PHP, et donc eux aussi sont optimisés et compilés par PHP, contrairement aux commentaires.
  7. Utilisation d’un serveur Redis : Songez à mettre en place un serveur Redis pour le cache applicatif. Redis est une solution de stockage en mémoire rapide qui peut considérablement améliorer les performances de votre application en stockant temporairement des données fréquemment utilisées.
  8. Approche en microservices : Si aucune des solutions précédentes ne résout votre problème, il est peut-être temps de considérer une approche basée sur des microservices. Divisez votre application monolithique en plusieurs petites applications spécialisées, légères et ultra-performantes. Cela permettra de répartir la charge et d’améliorer l’évolutivité et les performances globales de votre système.

Conclusion

L’installation de PHP sur une machine de développement ou sur un serveur est relativement simple, ce qui permet de créer rapidement un site web. Cette facilité d’utilisation est l’un des atouts majeurs de PHP, mais elle représente également l’une de ses principales faiblesses, car toutes les configurations par défaut sont conçues pour faciliter le travail des développeurs et ne sont pas du tout optimisées pour un environnement de production.

Cependant, il est tout à fait possible de prendre en main le serveur de production en moins d’une heure et d’optimiser chaque détail, jusqu’au dernier octet de cache. Ces optimisations peuvent réduire de 50% à 75% le temps d’exécution de PHP, ce qui permet de servir davantage de pages plus rapidement, en économisant un temps précieux. 🎉