Transcript de ma conférence « Comprenez comment PHP fonctionne, vos applications marcheront mieux » au Forum PHP 2022

7 novembre 2022conference, transcript, php, afup

Le 13 octobre 2022, j’étais au Forum PHP à Paris, pour ma conférence « Comprenez comment PHP fonctionne, vos applications marcheront mieux ». C’était la première fois que je donnais ce nouveau talk en public 🎉

Cette conférence a été enregistrée et la vidéo est disponible sur la chaine Youtube de l’AFUP :


Cela dit, puisque tout le monde n’accroche pas avec le format vidéo et que beaucoup préfèrent du texte, voici une tentative de transcript de cette conférence.


Vous trouverez en-dessous de chaque slide le texte qui lui correspond, parfois enrichi d’informations que je n’ai pas données lors de la présentation ou qui ne sont pas visibles dans les slides – parce qu’elles sont passées par ma gestuelles ou par des animations / transitions invisibles avec ces images figées. Le style est volontairement assez proche de l’oral.


Bonjour à toutes, bonjour à tous 👋

Aujourd’hui nous sommes au Forum PHP. Donc, sans plus attendre, parlons de PHP !


Avec les quelques mots sur ce slide, je vais peut-être hanter vos pensées pour la journée 🤣

Mais, plus sérieusement, je vais commencer par une histoire, une histoire vraie, une histoire vécue…


Cette histoire date d’il y a bien longtemps, dans un bureau à un arrêt de TGV d’ici…


À un moment, le DSI du groupe pour lequel je travaillais surgit dans le bureau.
Et il crie… Alarme ! « LE SITE NE REPOND PLUS !!! »


Les devs, dont je faisais partie, commencent à investiguer.


Les ops aussi, à l’autre bout du bâtiment, investiguaient.

On les connaissait un peu : on prenait parfois le café avec eux. Ils n’avaient pas de machine à café de leur côté du batiment, les pauvres (😱), donc ils venaient du nôtre pour les pauses.

Donc on a commencé à s’échanger, sur IRC (oui, ça date et je montre mon âge), des liens vers les dashboards qui montraient des choses suspectes.


Là, le DSI nous dit… Non, nous ordonne ! D’aller bosser dans le bureau des ops.

On se regarde, on hésite, mais bon, c’est notre N+3 ou N+4. On se lève, on part.


Et il nous reprend : « prenez vos PC ! »

On bossait avec des PC fixes, hein.
Donc une tour, un écran, un clavier, une souris. Un cable Ethernet. Un cable d’alimentation !


Donc, on débranche tout ça…
Puis on va chercher un charriot pour tout déplacer…
Puis on pose deux PC sur le charriot, on traverse tout le batiment.


Arrivés à l’autre bout, le DSI, qui nous avait accompagné, ordonne aux ops : « Trouvez leur des prises Ethernet ! Des prises électriques ! Et des cables plus long si il faut ! »

Donc, pendant un moment, les ops arrêtent de bosser sur la prod KO !


Finalement, le DSI s’en va.

On a discuté cinq minutes entre devs et ops et on s’est mis d’accord.
Les ops ont fait SSH+VIM sur un serveur alors qu’on regardait par-dessus leur épaule (c’était le bon vieux temps), et hop, la prod est repartie.


On n’a pas branché les PC.
On a juste perdu 20 minutes.


Avant de continuer, j’aimerais vous poser une question…


Dans la salle, levez la main, qui se définirait comme plutôt dev ?
(je lève une main)


Et qui se définirait comme plutôt ops ?
(je garde la première main levée et lève la seconde)


Et bien… Et si on arrêtait de considérer qu’on est « dev » ou qu’on est « ops » ?


Et si on faisait un peu les deux ?


OK, l’anecdote que je racontais juste avant n’est pas directement liée au sujet de mon talk…
Et, oui, le principe de démonter des PC fixes, les déplacer jusqu’à l’autre bout du batiment… C’était peu efficace.

Par contre, l’idée de bosser ensemble, avec nos expertises complémentaires, ce n’était pas stupide !


Je m’appelle Pascal Martin, je suis Principal Engineer chez Bedrock à Lyon, et je suis aussi AWS Hero.

Je travaille beaucoup sur de l’infra et du back, sur AWS et Kubernetes, sur des sujets de performances, résilience, scalabilité et couts.


On devait parler de PHP, quand même…
Donc, allons-y !


Déjà, c’est quoi, PHP ?


Et bien, c’est un langage de programmation open-source.


Sa 1ère version a été publiée le 8 juin 1995 par Rasmus Lerdorf.


En juin 1998, Zeev Suraski et Andi Gutmans rejoignent l’aventure avec une ré-écriture majeure.
C’est la sortie de PHP 3.

Le combo de leurs prénoms donne “Zend”, la société qu’ils ont créée, dont on retrouve le nom à plusieurs endroits dans PHP.


PHP 5.3 et composer, en 2008-2009, marquent un tournant pour PHP, qui se professionnalise et se rapproche d’un langage qui répond mieux aux attentes de sa communauté.

D’ailleurs, depuis déjà quelques années, on ne programme plus vraiment qu’en PHP, mais on travaille plus avec PHP et son écosystème.


Depuis 2010 et la RFC Release Process, PHP a un cycle de release et de versions supportées documenté.

PHP 8, la dernière version majeure du langage, a été publiée en novembre 2020.


Quand vous travaillez avec PHP, vous passez par un composant qui sait interpréter vos requêtes et les transmettre au moteur de PHP.


Cela permet à ce moteur d’être utilisé dans différentes conditions : sur un serveur web ou un autre, en ligne de commandes…

Ce composant s’appelle une SAPI : une Server-API.


Le moteur de PHP a son cycle de vie, avec plusieurs étapes.

  • MINIT : pour “module-init”, l’initialisation du moteur de PHP
  • RINIT : l’initialisation pour le traitement d’une requête (ex: allocations mémoire)
  • TRAITEMENT DE LA REQUÊTE
  • RSHUTDOWN : les opérations à jouer après le traitement de la requête (ex: nettoyage, libérations mémoire)
  • MSHUTDOWN : les opérations à jouer à la fin de l’exécution du moteur PHP

Le schéma que vous avez sous les yeux correspond au cas où le programme PHP est lancé, traite une requête, puis se termine. C’est exactement ce qu’il se passe lorsque vous exécutez le binaire php en ligne de commandes.
Oui, PHP fournit une SAPI “CLI” ;-)


Mais lancer encore et encore le programme PHP prend du temps et consomme des ressources.

Pour optimiser, une approche est d’intégrer le moteur de PHP dans le processus du serveur Web.
C’est ce qu’on faisait presque tout le temps, il y a une dizaine d’années, avec le serveur Web Apache 2 et une SAPI dédiée.


Depuis PHP 5.3, beaucoup d’entre nous on arrêté de travailler avec cette SAPI et ont migré vers php-fpm.
Avec le serveur Web nginx, ou toujours avec Apache 2.

php-fpm est aussi une SAPI, plus évoluée.
Elle implémente son propre serveur, sa propre gestion d’un pool de processus, ou, même, sa propre gestion de plusieurs pools de processus pour héberger différentes applications en les isolant un minimum les unes des autres.


Un processus worker implémente le cycle de vie de PHP et sait traiter plusieurs requêtes à la suite les unes des autres.


Ensuite, pour paralléliser, on positionne process worker par pool.


Un processus master pilote le tout : il reçoit les requêtes et les dispatche sur les processus worker.


Le nombre de processus par pool FPM peut être configuré de trois manières différentes :

  • dynamic → nombre de processus variable ⇒ adapté pour servir plusieurs applications depuis le même serveur, en espérant qu’elles n’aient pas leurs pics de charge en même temps 🙏.
  • ondemand → démarre des processus lorsque des requêtes arrivent ⇒ adapté pour des sites à faible trafic.
  • Et static → nombre de processus fixe ⇒ adapté quand un serveur héberge une seule application, avec une quantité de ressources (CPU/RAM) fixe.

Chez Bedrock, nous hébergeons nos applications dans Kubernetes, dans des Pods. Un peu comme des petites VMs dont la quantité de CPU et RAM est fixe.
Nous exploitons donc FPM en mode static.

Nous faisions d’ailleurs déjà comme ça quand nous étions on-prem, que ce soit pour les projets qui tournaient dans des VM ou pour ceux qui tournaient sur des machines physiques. Après tout, dans ces deux cas là aussi, la quantité de ressources par machine est fixe !


php-fpm est un serveur…
Mais les utilisateurs parlent HTTP… Alors que php-fpm n’est pas un serveur HTTP !

php-fpm ne comprend pas le protocole HTTP : il parle en CGI, à travers une socket réseau ou une socket unix.


Vous voudrez donc le placer derrière un serveur Web qui, lui, parle HTTP / HTTPS.
Nginx ou Apache2, typiquement.


Aussi, PHP est extensible : on peut lui ajouter des fonctionnalités.
Cela se fait par le biais d’extensions.


Cf slide.

ℹ️ Compilation statique = rare et quasi inexistant pour des extensions non hébergées dans le repository Git de PHP.


Au démarrage et à l’arrêt d’un processus php, chaque extension a son MINIT et MSHUTDOWN.
Et pareil au début et à la fin du traitement de chaque requête : chaque extension a son RINIT, RSHUTDOWN.

Donc, n’activez pas 36 extensions si vous n’en avez pas besoin !


Cf slide


Notez que beaucoup d’extensions sont juste de la glue vers une lib système : curl, gmagick, ssh… Et ces extensions ont repris les noms de fonctions et de paramètres – et leur ordre – des bibliothèques sous-jacentes.

Ca peut expliquer des incohérences dans les noms de fonctions et de paramètres ;-)


Vous écrivez du code PHP…


Comme celui-ci.
Dans des fichiers texte.
Mais le processeur de votre serveur ne comprend pas ceci, il n’exécute absolument pas votre code PHP !

L’exécution de code PHP passe par plusieurs étapes…


Tout d’abord, ce code PHP est compilé en opcodes : un ensemble d’instructions simples, qui ressemblent un peu à du langage Assembleur, si vous avez eu la chance d’y gouter un jour.

Ces opcodes sont ensuite exécutées par le moteur de PHP, le Zend Engine.
Et chaque opcode peut correspondre à des dizaines, voire centaines ou milliers de lignes de code C !

À l’écran, vous avez le dump d’opcodes (en format texte) correspondant au bout de code montré juste avant.
Je l’ai obtenu avec l’extension VLD, via https://3v4l.org/IETTL/vld


Par exemple, voici un extrait du code de l’opcode ZEND_ECHO, qui est l’opcode n°136 en PHP 8.
Vous pouvez le trouver dans le code-source de PHP.

Oui, c’est du C ;-)
Mais, passé le premier moment d’horreur, on arrive à lire un peu, non ? Voire à comprendre, dans les grandes lignes, ce qu’il se passe ?


Vos applications, nos applications, consomment des ressources.


Quand on pense “ressources”, on pense “serveurs” et quand on pense “serveurs”, on pense souvent en premier à leurs processeurs…


La phase de compilation du code PHP en opcodes est faite, de base, à chaque fois qu’un script PHP va être exécuté.
À chaque fois (une fois par requête) qu’il est include ou require.

Si vous répondez à un million de requêtes HTTP par jour, PHP compilera un million de fois votre code en opcodes !
Alors que, je parie, votre code ne change pas si souvent. Du moins, en production ?

Autrement dit, vous allez consommer beaucoup de CPU à faire, encore et encore, la même chose :-/

Heureusement, et on verra tout à l’heure comment, on peut conserver ces opcodes en cache, pour éviter de devoir rejouer encore et encore cette étape de compilation \o/


Ensuite, et c’est sans doute le plus évident et ce à quoi vous pensez naturellement : exécuter votre code consomme du CPU !
Combien dépend bien sûr de ce que fait votre application.

Par exemple, si vous effectuez des calculs cryptographiques ou du ré-encodage d’images, vous consommerez sans doute bien plus de CPU que si votre application passe son temps à attendre.


Au sein de nos plateformes, de plus en plus souvent des systèmes distribués, PHP n’est plus qu’un composant.

Souvent, celui qui joue le rôle de glue entre les autres.
Celui qui émet des requêtes dans tous les sens et attend les réponses…

Et quand PHP attend les résultats de requêtes SQL ou d’appels d’API distantes, il ne consomme pas ou très peu de CPU.


Au-delà des moments où votre application ne fait rien, il arrive qu’elle ait besoin de CPU.
Parce qu’elle effectue des calculs, ou parce qu’elle charge un gros fichier de configuration YAML, ou parce qu’elle sérialise en JSON une énorme structure de données.

Dans ces moments, si PHP n’a pas assez de processeur à sa disposition…
Et bien, ça rame !

Et ne pas avoir assez de processeur, ça peut arriver.

  • Que ça soit parce qu’il n’y a pas assez de CPU physique disponibles sur un serveur.
  • Ou parce qu’on atteint une limite logicielle, comme une sécurité configurée pour protéger les autres applications déployées sur la même machine.

Même si PHP consomme au maximum un seul coeur à la fois pour traiter une requête, n’oublions pas que nous lançons quasiment toujours de multiples exécutions de PHP en parallèle, sur nos serveurs, pour savoir traiter plusieurs requêtes en parallèle.

Ces multiples processus PHP doivent alors se partager les quelques coeurs CPU dont dispose notre serveur.


La mémoire RAM est une autre ressource limitée, dans un serveur.


Et on consomme de la mémoire tout au long de l’exécution de notre code PHP, par exemple pour les variables où nous stockons des données.


Le simple fait d’avoir un process php, aussi, ça consomme de la mémoire.
Si vous avez plusieurs processus PHP, encore plus.

Et vous avez quasiment toujours plusieurs processus PHP, pour parallèliser.
Ces processus consomment de la mémoire lorsqu’ils ne font rien – et encore plus lorsqu’ils font quelque chose.

L’OS fait les choses bien et partage de la mémoire, donc avoir dix workers php-fpm ne consomme pas cinq fois plus qu’en avoir deux.
Mais ça consomme un peu plus quand même ;-)


Aussi, du simple fait de travailler, PHP consomme de la mémoire.

Par exemple, lors de la compilation de votre code PHP en opcode…
Et bien, ces opcodes sont stockées en mémoire !

Et avec des applications PHP modernes, avec un framework et la moitié d’internet en dépendances, ça peut consommer pas mal de mémoire !


Dans le pire des cas, vous consommez plus de mémoire que le système ne peut vous en accorder.
Et l’OS tuera votre processus avec un OOMKill – un Out Of Memory Kill.

C’est brutal, mais c’est un des rôles du système : se protéger.
Quitte à vous choisir (ou votre application) comme victime.

Ce n’est pas Linux ou Kubernetes qui est méchant.
Ils jouent leur rôle, pour éviter au serveur de tomber entrainant plusieurs applications dans sa chute.


Mais on ne s’arrête pas au CPU et à la RAM !


Je pourrais aussi parler de bande passante réseau, limitante si vous échangez de gros volumes de données.

J’ai déjà vu une application souffrir parce qu’elle stockait des données énormes en cache, dans Redis, sur des serveurs dédiés…
Et toute la bande passante des machines était consommée pour parler avec ces Redis !

Mais aussi… Cf slide


Mais il y a une autre ressource, extrêmement critique, dont je veux vous parler plus longuement.
Quelque chose qui, sur des applications à fort trafic, est régulièrement une source de douleurs.

Quelque chose qui nous a fait souffrir moult fois, chez Bedrock.


Je reviens sur php-fpm, qui est LE moyen le plus utilisé, aujourd’hui, lorsqu’on souhaite répondre à des requêtes HTTP en PHP.

Avec php-fpm, chaque processus ne sait traiter qu’une seule requête à la fois.
Et il reste utilisé pendant tout le traitement, même si PHP ne fait rien d’autre qu’attendre une réponse d’une base de données ou d’une API.


Je me permet d’insister.

Si « PHP attend », par exemple, qu’une base de données réponde… et si le timeout est trop long…

Rapidement, vous allez vous retrouver dans une situation où vous n’aurez plus aucun processus disponible pour accepter de nouvelle requête et boum, votre application ne répondra plus !


À l’échelle, les processus php-fpm sont donc une ressource limitée et critique.

Donc, on en lance beaucoup en parallèle…
Mais vous ne pouvez pas en lancer une infinité :

  • chaque processus consomme de la mémoire, qui est une ressource limitée sur votre serveur.
  • chaque processus peut consommer jusqu’à un coeur CPU (selon ce que fait votre application) et les CPUs sont une ressource limitée sur votre serveur.

Et puis !


Souvent, on me dit qu’on va faire des choses en kernel::terminate(), après avoir répondu à l’utilisateur.

OK, c’est sympa et ça part d’un bon sentiment : répondre plus rapidement à l’utilisateur, pour qu’il ou elle attende moins longtemps.


Mais, voyons voir…

Pendant ce temp, le process fpm reste occupé et consommé !
Il ne peut donc pas passer au traitement d’une autre requête.

Et, donc, les utilisateurs suivants ne sont pas aidés !


Vous n’êtes pas encore paniqués ?
Et bien, pour le plaisir, je peux en rajouter une petite couche !


Les ressources qui ne sont pas illimitées, c’est valable aussi en dehors de “PHP lui-même”…

Pour ne donner que deux ou trois exemples qui font fréquemment souffrir mes collègues…
Cf slide

Rate-limits: de nombreuses APIs et services dont votre application dépendent se protègent avec des rate-limits. Ils vont rejeter vos requêtes si vous en faites plus de X par seconde !
C’est un fonctionnement normal et attendu, que vous devez prévoir et face auquel vous devez savoir réagir.


J’ai dit que la mémoire, la RAM, est une ressource limitée.

Penchons-nous un peu plus, maintenant, sur la façon dont PHP la manipule.


Déjà, en interne…


En C, un programme qui souhaite obtenir un espace mémoire appelle la fonction malloc().
C’est parfois un appel système, ça peut prendre du temps.

Dans l’autre sens, pour libérer un espace mémoire dont il n’a plus besoin, notre programme appelle la fonction free().


Mais malloc() peut échouer !
Et que fait un programme qui ne parvient pas à obtenir un espace mémoire alors qu’il en a besoin ?
Et bien, au programmeur de penser à gérer le cas, à chaque fois qu’il essaye d’effectuer une allocation !

Essayer de lire ou d’écrire depuis un espace mémoire qui ne vous appartient pas ?
Votre appli va planter. Ou vous venez de créer une faille de sécurité, ça arrive d’ailleurs fort souvent !

Essayer de libérer deux fois un espace mémoire avec la fonction free() ?
Bug classique. Boom l’application.

À l’inverse, si vous ne libérez pas un espace mémoire dont vous n’avez plus besoin, le système ne va pas le récupérer tant que votre application est lancée.
Il ne pourra donc pas l’allouer à un autre programme ni vous le ré-allouer ultérieurement. Votre programme consommera de plus en plus de mémoire : il va souffrir d’une “fuite de mémoire”.


Le moteur de PHP peut utiliser ces fonctions malloc() et free(), il peut demander de la mémoire au système.
Les extensions PHP aussi, d’ailleurs !

C’est une des raisons historiques pour lesquelles php-fpm tue ses processus après quelques centaines de requêtes.
Ca force une libération de mémoire qui aurait éventuellement fuité à cause d’un bug dans PHP ou dans une extension.


Mais, heureusement, PHP est là pour nous simplifier la vie !


Nous n’avons pas à nous soucier de ces fonctions malloc() et free(), nous ne gérons pas la mémoire aussi finement !

Tout ce que nous avons à faire…
Et bien, utiliser de la mémoire, en fait, sans y penser.

Et nous le faisons à chaque fois que nous appelons une fonction, à chaque fois que nous créons une variable, à chaque fois que nous ajoutons où enlevons un élément d’un tableau…


Pour cela, PHP embarque le ZMM : le Zend Memory Manager.

Un de ses rôles est de nous fournir de la mémoire plus rapidement et plus simplement que via la fonction malloc().

Aussi, il implémente la memory_limit.
Une sécurité qui évite qu’une page devenue folle ne consomme trop de mémoire et n’emporte un serveur entier dans sa chute !


Pour être rapide, ZMM ne demande pas de la mémoire au système à chaque fois que votre code en a besoin.

Il demande un gros bloc au début du traitement d’une requête (RINIT) et positionne vos variables dans ce gros bloc, qu’il gère lui-même.

Quand ce bloc ne suffit plus, ZMM l’agrandit — plus que nécessaire, pour ne pas devoir le ré-agrandir juste après pour une autre variable.

Et à la fin du traitement d’une requête, ZMM libère le bloc : les données sont détruites et nous ne subissons pas de fuite de mémoire.


Et quand je parle de gestion de mémoire en PHP, on me demande souvent « et le Garbage Collector, alors ? »

Le “Garbage Collector” arrivé en PHP 5.3 sert uniquement à briser les références circulaires entre variables devenues inutilisées.
Pendant le traitement d’une requête, la mémoire qui était consommée dans ZMM par ces références circulaires peut ainsi être réutilisée.


Je viens de dire que ZMM alloue un bloc mémoire au lancement du traitement d’une requête et le libére à la fin de ce traitement.
Toutes les variables créées depuis PHP pendant le traitement de cette requête sont placées dans ce bloc mémoire.

⇒ La conséquence est que PHP offre une approche “shared-nothing”.


Aucune donnée n’est partagée entre les traitements de requêtes – que ce soit les requêtes traitées en succession par un même processus ou les requêtes traitées en parallèle par plusieurs processus.


La première conséquence, c’est que PHP facilite beaucoup les choses pour les développeurs et développeuses – et c’est sans doute une raison de son succès, notamment comme premier langage pour beaucoup de débutants !
En effet, repartir d’une feuille blanche à chaque traitement de requête est une bénédiction !

Ainsi, nous n’avons pas besoin de fermer une connexion à une base de données, ni les fichiers ouverts, sauf si nous souhaitons libérer ces ressources dès que nous n’en avons plus besoin.
PHP fermera tout ça lui-même à la fin du traitement de chaque requête.

On n’a pas non plus de fuite de mémoire du traitement d’une requête HTTP à une autre.
Il ne reste pas de mémoire allouée “pour rien” et pas libérée.
Et on est sûr de ne plus avoir en mémoire les infos de l’utilisateur A quand on passe au traitement d’une requête de l’utilisateur B !


Cela dit, quand on veut écrire des applications qui répondent en quelques millisecondes, ou traiter du trafic à une certaine échelle, on se rend compte que cette approche shared-nothing n’a pas que des avantages.

En effet, ne pas partager de données entre les traitements des différentes requêtes, ça signifie notamment :

  • Pas de cache DNS
  • Pas de connexion persistante MySQL et autres
  • Pas de connexions persistantes vers les APIs HTTPS. Pensez connexion TCP, handshake TLS…
  • Et au-delà d’établir les connexions, vous devez aussi vous ré-identifier…

C’est une problématique où des extensions PHP peuvent apporter des solutions, puisqu’elles peuvent allouer de la mémoire en dehors du ZMM. Certaines le font, d’ailleurs, par exemple pour fournir des connexions persistantes MySQL.

Mais demander à des développeurs PHP de programmer en C, je crois que ça fait peur.
Et ça introduit un risque supplémentaire (il est plus facile d’introduire certains types de failles, par exemple).

Aussi, installer une extension PHP est plus complexe que lancer composer install


Après tout ça…

Désolé : il n’y a pas de solution magique ni facile.
Il faut bosser et adapter aux situations.

Oui, “ça dépend”.


Vous allez me dire que je caricature mais, quand une application rame ou consomme toutes les ressources à sa disposition, le premier réflexe des devs est souvent d’aller voir les ops et de dire “il faut plus de serveurs”.


Et, oui, allumer plus de ressources est souvent une solution pour redonner de la patate à une application !

Même si vous allez payer plus cher.
Ce qui est peut-être tout à fait acceptable.


Si vous travaillez sur un hébergement historique, où vous achetez des serveurs et les amortissez sur trois ans, vous pouvez provisionner plus de ressources.

Elles ne serviront pas pendant les ¾ du temps ou plus et vous les paierez pour rien, mais elles permettront de tenir face à des pics de charge de moyenne ampleur, le soir ou pendant les soldes ou une autre opération commerciale.


Si vous travaillez sur un hébergement plus moderne, où vous pouvez obtenir temporairement plus de capacité et la rendre quand vous n’en avez plus besoin, vous mettrez sans doute en place un mécanisme d’auto-scaling.


Dans tous les cas, les ressources ne sont pas infinies et elles n’arrivent pas immédiatement.

Sur un hébergement on-prem, il faut jusqu’à plusieurs semaines pour obtenir un serveur.
Dans le cloud, au moins quelques minutes.

Et puis, vous ne pouvez pas toujours prédire un pic de charge…
Et l’auto-scaling, réactif, n’est pas instantané.


Quand les devs disent “on veut plus de serveurs”, les ops ont souvent le réflexe de répondre “optimisez votre code”.


Dans le fond, ils n’ont pas tord : j’ai déjà vu des temps de réponse divisés par 10, par 100, ou même par 1000 en quelques heures de travail !

Ca peut valoir le coup / cout !


Sur des applications à forte charge, en PHP, avec une architecture allant entre le monolithe distribué et les microservices, la ressource la plus limitante est souvent le nombre de processus php-fpm – qui restent occupés à attendre qu’une API ou une base de données, plus lente que d’habitude, réponde.


C’est là où il devient nécessaire de prendre du recul et de penser à l’architecture un peu plus globale qu’à celle d’une unique API.


Il devient aussi indispensable de configurer des timeouts sur tous les appels.
Et ces timeouts doivent être courts !

Si une API répond, en temps normal, en 100 ms et que vous configurez un timeout à 1 seconde, le jour où cette API aura du mal, ses clients vont attendre DIX FOIS PLUS LONGTEMPS qu’en temps normal, 10 fois plus de processus php-fpm vont être consommés pour attendre

Savez-vous comment votre plateforme se comporte, dans ce cas ?
D’expérience : tout va merder !


Ensuite, et si vous configuriez mieux PHP ?

C’est le moment parfait pour associer des connaissances dev de comment l’application fonctionne, ce qu’elle fait.
Et des connaissances ops de comment PHP se configure.


Pour un serveur PHP qui consomme trop de CPU, un réflexe : installer et configurer un cache d’opcodes !

Vous lui donnez de la RAM et il va l’utiliser pour stocker les opcodes et ne plus effectuer la phase de compilation devenue non-nécessaire.
En échange d’une consommation RAM plus élevée, vous allez fortement réduire la consommation CPU de vos serveurs.
Et, aujourd’hui, alors qu’on a atteint les limites de la loi de Moore, réduire la consommation CPU est intéressant !

Selon les applications, on peut diviser par deux la consommation CPU…
Juste en activant un cache d’opcodes !

Et, depuis PHP 5.5, “opcache” est fourni avec PHP !
Vous n’avez donc pas d’excuse, du moins si vous êtes maitre de vos hébergements.


Ensuite, veillez à bien configurer opcache !

Typiquement, si vous ne lui donnez pas suffisamment de mémoire, opcache va passer son temps à compiler, remplir son cache, purger son cache et recommencer, c’est catastrophique !


Pour savoir combien de mémoire allouer…
Si vous avez la moitié d’Internet dans vos dépendances… Et bien, plus.

Plus sérieusement : regardez ses métriques d’utilisation, la vérité est là.

Et re-regardez les de temps en temps, par exemple après avoir fait de nouveaux développements qui ajoutent beaucoup de code PHP.


Une autre approche demande de changer nos habitudes.

Je m’explique.


En PHP, nous avons l’habitude de réfléchir et de programmer de manière très séquentielle : nous faisons les choses les unes après les autres.
Par exemple, une requête SQL, une requête Elasticsearch, un appel d’API, un second appel d’API.

Mais puisqu’on ne fait qu’attendre, en occupant un processus php-fpm pendant ce temps…


Certaines requêtes pourraient être effectuées en concurrence ?

Cela permettrait de consommer un processus php-fpm pendant moins longtemps.
Il serait libèré plus rapidement pour traiter une autre requête entrante.

Aussi, les utilisateurs attendraient moins longtemps.

Bien sûr, la contrepartie, c’est que nous nous complexifions la tâche !

Nous faisons exactement ça chez Bedrock, sur notre projet qui joue le rôle d’API Gateway.


L’approche shared-nothing, où, à chaque requête, on repart de zéro, c’est une sacrée galère lorsqu’on essaye de faire tourner des applications à une certaine échelle !

En effet, jouer encore et encore des résolutions DNS, ouvrir encore et encore des connexions TCP, effectuer encore et encore des handhake TLS, recharger des données depuis un cache et les désérialiser, encore et encore…

Que de temps et de CPU gaspillés !


Et si on écrivait un serveur HTTP en PHP ?

Qu’on lance une fois et qui reste lancé et traite les requêtes entrantes les unes après les autres ?
Voire même, qui traite plusieurs requêtes entrantes en parallèle, en asynchrone ?

Nous pourrions ainsi partager des données, des ressources, entre les traitements des différentes requêtes !
Nous éviterions le principe shared-nothing !

Et bien, c’est le genre de chose que nous pouvons faire avec des frameworks comme AMP ou ReactPHP.


Cela dit, le risque est que si une Fatal Error survient, elle impacte plusieurs traitements de requêtes à la fois.
Donc ça fait peur.

Aussi, travailler en non-shared-nothing

Les devs PHP n’ont pas l’habitude.
Et on peut se poser des questions sur la compabilité des outils, bibliothèques et frameworks.


Enfin, PHP de base ne fournit pas de mécanisme de gestion de threads en userland.

L’extension parallel, qui remplace l’ancienne extension pthreads, permet d’en exploiter, si PHP est compilé avec ZTS activé (option --enable-zts), ce qui n’est généralement pas le cas dans les versions de PHP distribuées via des gestionnaires de paquets.

Vous aurez peut-être à compiler PHP vous-même, ce qui est toujours une expérience intéressante, au moins la première fois.


Je vois l’heure tourner…
Il est donc temps de commencer à réaliser que vous avez (nous avons !) fort à faire !


De quoi a-t-on parlé pendant la grosse demi-heure que nous avons passée ensemble ?


Travailler sur une plateforme, construire un produit, fournir une bonne expérience à nos clients et utilisateurs…

Ca va beaucoup loin que juste écrire du code PHP.
Et je suis convaincu que travailler ensemble, se parler entre personnes de métiers un peu différents, nous aide à fournir des applications de meilleure qualité, qui fonctionnent mieux.

Même si ce mot veut un peu dire 36 choses et même s’il est un peu tordu dans tous les sens, pensez “DevOps”.


Lorsqu’on travaille avec la solution la plus commune pour répondre à des requêtes HTTP, rappelons-nous qu’un processus fpm traite une requête à la fois et consomme jusqu’à un coeur de CPU.

Pour aller plus vite, pas de magie, il faut faire moins de choses ou utiliser un CPU plus rapide.
Et pour traiter plus de requêtes, il faut plus de CPU en parallèle. Avec du scaling horizontal si le trafic varie.


PHP fonctionne avec une approche shared-nothing : chaque requête est indépendante des autres, peu de choses sont partagées.

C’est une énorme force de PHP, qui simplifie énormément l’utilisation du langage.

Le contrecoup est que l’on passe beaucoup de temps et de ressources à effectuer, encore et encore, les mêmes opérations – dont résolutions DNS, handshakes TLS…


Ceci dit, maintenant…


Vu ce dernier point, notamment d’un point de vue performances, on peut se demander si les microservices sont vraiment adaptés ?

Surtout qu’on fait plus souvent face à un monolithe distribué, où des composants d’une application s’appellent en HTTP(S) dans tous les sens, qu’à de vrais microservices qui communiquent en asynchrone.

Sans parler des difficultés et problématiques que posent un système distribué !


Aussi : et si sortir une usine à gaz php + la moitié d’internet à composer installer, ce n’était pas toujours génial ?

Après tout, ajouter du code encore et encore, c’est consommer plus de CPU, plus de RAM…

Et, on l’a dit, les ressources – que ce soit le CPU, la RAM, les matières premières ou l’énergie – ne sont pas illimitées !


Quoi qu’il en soit, mieux comprendre comment PHP fonctionne, je crois que ça aide à mieux développer et mieux héberger des applications.

Rien que penser “les processus php-fpm sont une ressource précieuse et limitée”, c’est un grand pas sur le bon chemin !


Et je ne peux m’empêcher de demander : et si PHP n’était pas toujours le langage le plus adapté ?
Notamment pour des applications distribuées, à fort trafic ?


Nous n’avons pas tellement de temps pour des questions, mais je vais rester ici à côté de la scène pendant quelques minutes, dès que j’aurai laissé la place au prochain speaker.

Après cela, vous pourrez me trouver pendant toute la journée et demain ; je suis souvent à proximité d’une machine à café ;-)

Venez me parler, vous êtes les bienvenus !


Je m’appelle Pascal MARTIN, j’ai pendant longtemps été développeur avec PHP et son écosystème, puis Lead DevOps et je suis désormais Principal Engineer chez Bedrock.

Nous recrutons régulièrement, y compris sur des postes de développement backend en PHP — vous pouvez passer sur notre stand pour discuter.



J’ai rédigé ce pseudo-transcript sans écouter la conférence que j’avais donné, quelques semaines après. Je n’apprends jamais mon discours par cœur et j’ai toujours quelques variations mineures entre deux déroulés.

Si vous préférez le format vidéo, cette conférence est disponible sur la chaine Youtube de l’AFUP :