Performances PHP

Un PHP rapide se règle, il ne s'achète pas.

Notre équipe travaille avec PHP depuis vingt-cinq ans et sait où une requête perd son temps. Nous le retrouvons sur votre serveur et le récupérons, couche par couche.

Anatomie d'une requête

Une page est une chaîne d'attentes : trouver le serveur, ouvrir la connexion, attendre PHP, compiler, exécuter, interroger la base de données, envoyer. Passez d'un état à l'autre pour voir quels maillons nous raccourcissons.

Illustration : des proportions, pas une mesure
  1. Résolution DNS Assurée par le résolveur du visiteur. Identique dans tous les états : cette partie est le réseau du visiteur. Identique dans tous les états : cette partie est le réseau du visiteur.
  2. Connexion et TLS Une configuration TLS ancienne, une nouvelle connexion pour chaque fichier. TLS 1.3 et une seule connexion HTTP/2 réutilisée pour chaque fichier. TLS 1.3 et une seule connexion HTTP/2 réutilisée pour chaque fichier.
  3. Attente d'un worker PHP Tous les workers sont occupés, la requête attend dans la file. Un pool dimensionné selon la mémoire : un worker libre est prêt. Aucun worker PHP n'est nécessaire.
  4. Compilation des scripts PHP lit et compile chaque fichier à chaque requête. OPcache sert les scripts compilés depuis la mémoire. Rien à compiler.
  5. Recherche des fichiers et des classes L'autoloader parcourt les répertoires pour chaque classe. Un autoloader à classmap et un cache realpath déjà rempli. Rien à charger.
  6. Exécution de votre code Le travail propre de l'application. À peu près pareil : le JIT change rarement cela pour un site typique. Votre code ne s'exécute pas pour cette visite.
  7. Requêtes en base de données Des index manquants, et la même requête sur chaque page. Index ajoutés, réponses répétées gardées dans Redis. Aucune requête n'est envoyée.
  8. Envoi de la page Envoyée sans compression. Compressée avant de quitter le serveur. La page terminée, directement depuis le cache de pages.
Le réseau Travail sur notre serveur Le premier octet atteint le visiteur

Les barres ci-dessus sont un dessin. Ce chiffre est une mesure.

< 20 ms Le premier octet d'une page, depuis notre serveur, mesuré sur le serveur lui-même, chiffrement compris.

Le reste du temps, c'est la distance entre nos serveurs en Allemagne et vous. Cette part dépend de l'endroit où vous êtes : mesurez-la depuis votre propre connexion.

Mesurez depuis votre connexion

Les réglages qui suppriment l'essentiel de l'attente

Compiler, trouver les fichiers et charger les classes se fait avant que votre code n'exécute une seule ligne. Ces réglages font que cela n'arrive qu'une fois, et non à chaque requête.

conf.d/10-opcache.iniUn exemple - les valeurs sont fixées site par site
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.preload=/srv/app/preload.php
opcache.preload_user=www-data
opcache.jit=tracing
opcache.jit_buffer_size=64M
realpath_cache_size=4096K
realpath_cache_ttl=600
deploy
$ composer install --no-dev --classmap-authoritative
  1. OPcache

    PHP compile chaque fichier avant de l'exécuter. OPcache garde le résultat compilé en mémoire partagée, et la requête suivante saute ce travail. Nous dimensionnons sa mémoire et son nombre de fichiers selon le site, et lisons son taux de succès pour vérifier que rien n'est évincé.

    En production, PHP cesse de vérifier chaque fichier ; chaque déploiement vide lui-même le cache.

  2. Préchargement

    Pour un framework, le préchargement charge ses fichiers principaux une seule fois au démarrage de PHP et les garde prêts pour chaque requête. Modifier ces fichiers exige un redémarrage, qui fait donc partie du déploiement.

  3. JIT

    Le JIT transforme le code PHP le plus sollicité en code machine. Il aide les tâches qui occupent le processeur - traitement d'images, calculs, longues boucles. Un site typique passe son temps à attendre la base de données et le réseau, et là le JIT change peu de chose. Nous l'activons là où une mesure montre une différence, pas partout.

  4. Cache realpath

    Chaque include demande au disque où se trouve réellement un fichier. Un cache realpath plus grand garde ces réponses en mémoire, ce qui compte pour les frameworks qui chargent des centaines de fichiers par requête.

  5. Autoloader Composer

    Une classmap faisant autorité trouve chaque classe en une seule recherche au lieu de parcourir les répertoires, et les paquets de développement restent hors du serveur.

Le bouton de réglage : PHP-FPM

PHP-FPM entretient un pool de workers PHP. Trop peu, et les requêtes font la queue ; trop, et le serveur manque de mémoire et ralentit pour tout le monde. Le pool se règle sur ce que le serveur possède réellement.

pm = static
pm.max_children = 64

Un nombre fixe de workers, toujours actifs

Pour un serveur dédié à un seul site très fréquenté. Aucun temps perdu à lancer des processus, et la mémoire utilisée est connue d'avance.

pm = dynamic
pm.max_children = 64
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12

Quelques workers prêts, davantage sous charge

Garde quelques workers en attente et en ajoute quand le trafic monte, jusqu'à la limite. Le choix habituel pour un site aux pics quotidiens.

pm = ondemand
pm.max_children = 64
pm.process_idle_timeout = 10s

Des workers seulement à l'arrivée d'une requête

Pour de nombreux petits sites sur un même serveur : la mémoire va aux sites visités, et la première requête après une période calme paie un court démarrage.

Comment nous fixons pm.max_children

pm.max_children = mémoire pour PHP ÷ mémoire par worker

pm.max_children 64
$ ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1} END {print s/NR/1024 " MB"}'

Un exemple de calcul, pas une recommandation : les vrais chiffres viennent de votre serveur, et nous gardons une marge pour les pics.

pm.max_requests
Renouvelle un worker après un nombre donné de requêtes, pour qu'une lente fuite de mémoire ne devienne jamais une panne.
request_slowlog_timeout
Une requête qui dure plus longtemps écrit sa pile d'appels dans le journal des lenteurs, qui désigne la fonction à examiner.
request_terminate_timeout
Arrête une requête incontrôlée avant qu'elle ne bloque un worker pour de bon.
pm.status_path
Montre la file d'attente et la fréquence à laquelle le pool a atteint sa limite - le premier endroit que nous regardons quand un site ralentit aux heures de pointe.

Deux routes du serveur web à PHP

Nous exploitons les deux et choisissons selon les besoins du site. Dans les deux cas, la requête la plus rapide est celle à laquelle PHP n'a jamais à répondre.

LiteSpeed + LSAPI

  1. Visiteur
  2. LiteSpeed
  3. LSCachecache de pages
  4. LSAPI
  5. lsphp

LiteSpeed dialogue avec PHP par son propre protocole LSAPI et garde des pages entières dans LSCache, que WordPress et d'autres applications peuvent purger au moment exact où le contenu change.

Nginx + PHP-FPM

  1. Visiteur
  2. Nginx
  3. fastcgi_cachecache de pages
  4. FastCGI
  5. PHP-FPM

Nginx sert lui-même les fichiers statiques, transmet PHP à un pool PHP-FPM par un socket local, et peut garder les pages terminées dans son cache FastCGI pour les visiteurs non connectés.

  • Redis pour les objets et les sessions

    Les réponses qu'une page redemande sans cesse à la base de données restent en mémoire, et les sessions ne touchent plus le disque.

  • Un cache de pages complètes

    Un visiteur non connecté reçoit une page terminée sans que PHP ni la base de données ne soient sollicités. Paniers, comptes et paiements en sont exclus.

  • HTTP/2 et compression

    Une seule connexion chiffrée transporte tous les fichiers de la page, le texte est compressé avant de partir, et les fichiers statiques ont de longues durées de cache avec des noms versionnés.

  • Temps passé en base de données

    Le journal des requêtes lentes et EXPLAIN montrent quelle requête attend et pourquoi ; le plus souvent, un index ou une requête répétée en fait l'essentiel.

    Comment nous optimisons les bases de données

Nous mesurons d'abord, puis changeons une seule chose

Chaque changement se fait par rapport à une mesure prise avant, pour vous montrer ce qu'il a apporté. Voici ces mesures.

  • opcache_get_status()Taux de succès, mémoire libre, et si des scripts sont évincés.
  • pm.status_pathLa file d'attente et la fréquence à laquelle le pool a atteint sa limite.
  • slowlogLa pile d'appels de chaque requête qui a duré trop longtemps.
  • slow_query_log + EXPLAINQuelles requêtes attendent, combien de lignes elles lisent et quel index manque.
  • redis-cli INFO statsLes succès du cache face aux échecs, et si des clés sont évincées.
  • curl -w %{time_starttransfer}Le temps jusqu'au premier octet, mesuré de la même façon avant et après.

Vous voulez l'autre moitié du chiffre - la distance entre nos serveurs et vous ? Lancer le test de vitesse

Dites-nous quelle page est lente

Envoyez-nous l'adresse et l'endroit où elle tourne aujourd'hui. Nous examinons d'abord le serveur, puis vous disons ce que nous changerions et ce que cela devrait apporter - et chiffrons le travail après cet échange.