← Tous les cours / Le moteur PHP — du code source à l'exécution Cours
00 — VUE D'ENSEMBLE

PHP n'est pas magique

Repartir d'une feuille blanche à chaque requête, ne jamais gérer la mémoire à la main : PHP te cache énormément de choses. Comprendre ce qu'il fait sous le capot, c'est ce qui sépare une appli qui tient la charge d'une appli qui s'écroule.

Tu écris $total = $a + $b; et ça marche. Tu ouvres une connexion à ta base, tu oublies de la fermer, et tout va bien quand même. Tu remplis un tableau avec des milliers d'objets, tu ne libères jamais rien à la main, et pourtant la mémoire ne fuit pas d'une requête à l'autre. PHP te rend la vie tellement facile qu'on finit par croire qu'il n'y a pas de machine en dessous.

C'est exactement comme une voiture à boîte automatique. Tant que tu roules en ville, tu n'as jamais à penser au moteur. Mais le jour où tu attaques une côte raide avec la remorque chargée, comprendre ce qui se passe sous le capot change tout : tu sais pourquoi ça force, ce que tu peux faire, et quand il faut s'arrêter avant la surchauffe.

🔑 L'idée à garder pendant tout le cours : un processus PHP est une ressource limitée et précieuse. Rien que prendre conscience de ça, c'est déjà un grand pas.

Ce cours suit le voyage d'une seule requête à l'intérieur du moteur. On part de ton code source, on le voit se transformer en instructions que le processeur comprend, on regarde comment la mémoire est prêtée puis rendue, et on termine sur la conséquence la plus marquante de tout ça : à la fin de chaque requête, PHP efface tout et repart de zéro.

À chaque étape, la même question revient : qu'est-ce que ça consomme ? Car le CPU, la RAM, les connexions réseau, les processus disponibles — rien de tout ça n'est infini. Et c'est en comprenant le moteur qu'on arrête de répondre « il nous faut plus de serveurs » par réflexe.

Le fil rouge en cinq escales
La SAPI et le cycle de vie · la compilation en opcodes · OPcache · la mémoire (ZMM & ramasse-miettes) · le modèle share-nothing et le prix des ressources.

Un mot sur le périmètre. Ce cours regarde l'intérieur du moteur pendant le traitement d'une requête. La question voisine — comment PHP arrive à traiter plusieurs requêtes en même temps, avec PHP-FPM, les pools de processus et les runtimes modernes — est traitée à fond dans le cours PHP — Concurrence & Runtimes. Les deux se complètent : ici on plonge dans la machine, là-bas on regarde la flotte de machines.

01 — SAPI & CYCLE DE VIE

La SAPI et le cycle de vie du moteur

Le moteur de PHP ne parle pas tout seul au monde extérieur. Entre lui et ta requête, il y a toujours une couche d'adaptation — la SAPI — et un cycle de vie en cinq temps.

Le cœur de PHP — le moteur, le Zend Engine — ne sait pas, à lui tout seul, ce qu'est une requête HTTP, une ligne de commande ou un cron. Il sait exécuter du code. Pour le brancher au monde réel, il faut une couche d'adaptation entre lui et le contexte dans lequel il tourne. Cette couche s'appelle une SAPI.

SAPI — Server API
L'adaptateur qui reçoit une demande d'exécution venue de l'extérieur, la met en forme, et la transmet au moteur. C'est elle qui change selon l'environnement : cli en ligne de commande, fpm derrière un serveur web, le module Apache historique, ou la SAPI embed quand PHP est intégré dans un autre programme.

Pense à la prise électrique. L'appareil (le moteur) est toujours le même ; ce qui change d'un pays à l'autre, c'est l'adaptateur qu'on met au bout du câble. La SAPI, c'est cet adaptateur : le moteur ne change pas, mais selon que tu l'appelles en CLI ou via PHP-FPM, l'entrée et la sortie ne sont pas branchées de la même façon.

Cinq temps, toujours dans le même ordre

Quel que soit l'environnement, le moteur suit un cycle de vie en cinq phases. Deux d'entre elles encadrent toute la durée de vie du programme ; deux autres encadrent chaque requête ; au milieu, ton code s'exécute enfin.

MINIT
RINIT
ton code
RSHUTDOWN
MSHUTDOWN

MINIT (module init) joue une fois, au démarrage du programme PHP : le moteur et chaque extension installée s'initialisent. RINIT (request init) prépare ce qui sert à la requête en cours. Puis ton code tourne. RSHUTDOWN referme la requête et libère ce qui lui était propre. Enfin MSHUTDOWN éteint le moteur à l'arrêt du programme.

Le détail qui change tout : une fois, ou à chaque requête ?

En ligne de commande, le cycle complet se joue une seule fois : MINIT, une requête, MSHUTDOWN, et le processus meurt. Relancer php script.php mille fois, c'est rejouer MINIT mille fois — coûteux si tu boucles.

Derrière un serveur web avec PHP-FPM, c'est plus malin. MINIT ne joue qu'une seule fois, au lancement du worker. Ensuite, le worker boucle : RINIT, ton code, RSHUTDOWN — encore et encore, pour chaque requête — et MSHUTDOWN n'arrive qu'à l'extinction du worker. C'est ce qui explique que le bootstrap de ton framework (autoloader, conteneur de services, configuration) soit rejoué à chaque requête : il vit dans la fenêtre RINIT → RSHUTDOWN, pas dans MINIT.

💡 Le piège des extensions : les phases MINIT et MSHUTDOWN sont jouées pour chaque extension chargée, et RINIT / RSHUTDOWN aussi, à chaque requête. Activer trente extensions « au cas où » revient à payer trente petits frais d'initialisation à chaque requête. N'active que ce dont tu te sers réellement.

Beaucoup de fonctionnalités que tu crois « natives » sont en fait fournies par des extensions, écrites en C (ou dans un langage compilé compatible). Certaines vivent dans le code source de PHP lui-même — le répertoire ext/ du dépôt php-src ; d'autres ont leur propre version et leur propre rythme, comme Xdebug. Et beaucoup ne sont que de la « colle » vers des bibliothèques système (cURL, GD, OpenSSL…). C'est d'ailleurs une explication aux incohérences de nommage qu'on reproche à PHP : il reflète des bibliothèques écrites par des gens différents qui ne se sont jamais parlé.

Côté tuyauterie : tes utilisateurs parlent HTTP, mais PHP-FPM parle FastCGI. Ils ne se comprennent pas directement. On intercale donc un serveur web (Nginx, Apache…) qui traduit l'HTTP de l'utilisateur en FastCGI pour PHP-FPM, et inversement. Tout ce volet — pools de workers, processus master, modes static / dynamic / ondemand — est détaillé dans PHP — Concurrence & Runtimes.

Simulateur — le cycle de vie en action

Choisis un mode, puis envoie des requêtes. Observe ce qui se rejoue à chaque fois… et ce qui ne joue qu'une seule fois.

MINIT
RINIT
ton code
RSHUTDOWN
MSHUTDOWN
Requêtes traitées : 0 MINIT joué : 0 fois
02 — DU CODE AUX OPCODES

De ton code aux opcodes

Ton processeur ne comprend pas le PHP. Avant la moindre exécution, ton code traverse une chaîne de compilation qui le transforme en instructions élémentaires.

Ton serveur ne sait pas lire du PHP. Son processeur exécute des instructions élémentaires, et le PHP que tu écris en est très loin. Entre les deux, il y a une chaîne de compilation — invisible, mais payée à chaque requête.

De la source aux opcodes, en quatre étapes
Le tokenizer (lexer) découpe ton fichier en une suite de jetons. Le parser assemble ces jetons en un arbre syntaxique abstrait (AST). Le compilateur parcourt cet arbre et produit des opcodes. Enfin, la machine virtuelle du Zend Engine exécute ces opcodes.
code source
jetons
AST
opcodes
VM Zend

L'étape de l'AST n'a pas toujours existé : elle est arrivée avec PHP 7. Avant, le parser produisait les opcodes directement, sans représentation intermédiaire. Séparer l'analyse de la syntaxe de la génération des opcodes a rendu le moteur plus propre et a ouvert la porte à de nombreuses optimisations à la compilation.

Un opcode, c'est une instruction simple, dans l'esprit de l'assembleur. Si tu en as déjà fait, le dépaysement sera léger. Voici à quoi ressemble, de façon illustrative, ce que produit une ligne toute bête :

// ton code PHP
echo "Bonjour " . $nom;

// les opcodes produits (forme simplifiée)
CONCAT   ~0  "Bonjour ", $nom   // concatène, range le résultat dans ~0
ECHO     ~0                       // envoie ~0 sur la sortie
RETURN   1

Chacun de ces opcodes correspond, derrière, à des dizaines, des centaines, parfois des milliers de lignes de C dans le moteur. L'opcode qui réalise un echo s'appelle par exemple ZEND_ECHO ; chaque opcode porte un numéro interne, mais ce numéro dépend de la version de PHP, alors ne t'y attache pas.

🔍 Conseil de terrain : quand tu ne comprends pas un comportement de PHP, va lire le code source du moteur. C'est plus accessible qu'on ne le croit — des parenthèses, des accolades, des points-virgules, presque du PHP. On y comprend souvent l'origine d'un comportement bizarre. (Et si la doc était fausse, propose une correction.)

Le coût caché

Voici le point qui va nous occuper toute la section suivante. Par défaut, cette compilation est refaite à chaque exécution d'un script. Si ton serveur reçoit un million de requêtes par jour, il compile un million de fois le même code — alors que ton code, lui, ne change pas un million de fois par jour. C'est un gaspillage de CPU pur et simple. Heureusement, on peut l'éviter complètement.

03 — OPCACHE

OPcache : ne plus recompiler

Recompiler le même code à chaque requête, c'est brûler du CPU pour rien. OPcache garde les opcodes en mémoire partagée — et c'est souvent le gain de performance le plus facile de ta vie.

On vient de le voir : recompiler le même code à chaque requête, c'est payer encore et encore pour un résultat identique. La parade est simple et redoutablement efficace : compiler une fois, garder les opcodes en mémoire, et les réutiliser pour toutes les requêtes suivantes. C'est le rôle d'OPcache.

OPcache
Une extension qui stocke les opcodes compilés dans une mémoire partagée entre tous les workers. À la première requête, le script est compilé puis mis en cache ; aux requêtes suivantes, le moteur saute toute la phase de compilation et exécute directement les opcodes en cache.

Elle est incluse dans PHP depuis la version 5.5. Autrement dit, si tu es maître de ton hébergement, tu n'as aucune excuse pour ne pas l'activer. Le gain est souvent spectaculaire : il n'est pas rare de voir la consommation CPU des serveurs divisée par deux rien qu'en activant un cache d'opcodes — sans toucher une ligne d'application.

⚠️ Le piège du cache trop petit : si tu ne donnes pas assez de mémoire à OPcache, le cache se remplit, OPcache le purge, recompile, se remplit à nouveau… et tu te retrouves dans une boucle catastrophique qui coûte plus cher que pas de cache du tout. Regarde les métriques d'utilisation d'OPcache pour dimensionner la mémoire, et reviens les vérifier quand tu ajoutes beaucoup de code.

Les réglages qui comptent

DirectiveRôle
opcache.memory_consumptionTaille de la mémoire partagée allouée au cache (en Mo). À régler d'après tes métriques, pas au hasard.
opcache.validate_timestampsSi activé, PHP vérifie la date de modification des fichiers pour recompiler si besoin. En production, on le désactive et on vide le cache au déploiement.
opcache.max_accelerated_filesNombre maximum de fichiers gardés en cache. Trop bas, et une partie de ton code n'est jamais mise en cache.

Depuis PHP 8, OPcache embarque en plus un JIT (just-in-time) capable de traduire certains opcodes en code machine natif. Le gain est réel sur du calcul intensif, plus discret sur les applications web classiques qui passent surtout leur temps à attendre des bases de données et des API.

Simulateur — avec et sans cache

Active ou non OPcache, puis envoie une rafale de requêtes. Regarde le CPU consommé et le nombre de compilations. Essaie aussi le mode « cache trop petit » pour voir la boucle infernale.

CPU cumulé
Requêtes : 0 Compilations : 0 Unités CPU : 0
04 — MÉMOIRE & ZMM

La mémoire : malloc, free et le ZMM

En C, gérer la mémoire à la main est un champ de mines. PHP te protège avec son propre gestionnaire — le Zend Memory Manager — qui loue un gros bloc, le gère, puis le rend d'un coup.

En C, le langage dans lequel le moteur de PHP est écrit, on gère la mémoire à la main. Quand un programme a besoin d'espace, il appelle malloc ; quand il a fini, il appelle free. Sur le papier, c'est simple. En pratique, c'est un champ de mines.

D'abord, malloc est un appel système : il prend du temps, et il peut échouer. C'est alors à toi de gérer le cas — bon courage. Ensuite, la moindre erreur se paie cash : lire ou écrire dans un espace qui ne t'appartient pas fait planter le programme ; libérer deux fois le même espace avec free le fait planter aussi ; et si tu oublies de libérer un espace dont tu n'as plus besoin, le système ne peut pas le récupérer — c'est une fuite mémoire, et ton application gonfle jusqu'à l'étouffement.

La bonne nouvelle, c'est que PHP te débarrasse de tout ça. Au quotidien, tu utilises la mémoire sans y penser : chaque variable, chaque élément ajouté à un tableau, chaque appel de fonction manipule de la mémoire, et PHP s'en occupe pour toi. Pour ça, il embarque son propre gestionnaire : le Zend Memory Manager.

ZMM — Zend Memory Manager
Le gestionnaire de mémoire interne de PHP. Plutôt que d'appeler malloc à chaque petite allocation, il loue un gros bloc au système au début de la requête, puis distribue la mémoire à l'intérieur lui-même. Quand le bloc devient trop petit, il l'agrandit — un peu plus que nécessaire, pour que la variable suivante n'ait pas à repayer. Et à la fin de la requête, il libère le bloc entier d'un coup.

L'analogie : au lieu d'aller à la banque retirer 10 € à chaque fois que tu as une dépense, tu retires une grosse somme d'un coup et tu piochs dedans. C'est bien plus rapide, et à la fin de la journée tu rends tout ce qui reste.

memory_limit : le garde-fou

C'est aussi le ZMM qui applique memory_limit, la limite de mémoire par requête définie dans ta configuration. Si un script tente de dépasser cette limite, le ZMM déclenche une erreur fatale et interrompt la requête. C'est volontaire : mieux vaut tuer une page devenue folle que laisser un serveur entier s'écrouler. Ne règle donc pas cette limite à une valeur démesurée « pour être tranquille ».

💀 Et si on ignore la limite ? Si un processus consomme plus que ce que le système d'exploitation lui accorde, c'est l'OS qui tranche : il tue le processus avec un OOM kill. Ce n'est ni Linux ni Kubernetes qui sont « méchants » — ils se protègent pour éviter qu'une application n'emporte la machine entière dans sa chute.

Dernier point pour boucler la boucle avec la section précédente : les extensions PHP peuvent appeler malloc directement, en dehors du ZMM. Si l'une d'elles oublie un free, la fuite survit à la requête. C'est l'une des raisons historiques pour lesquelles on configure souvent PHP-FPM (via pm.max_requests) pour recycler ses workers après un certain nombre de requêtes : on tue le processus, un neuf le remplace, et toute fuite éventuelle repart à zéro. Par défaut ce recyclage est désactivé (valeur 0) ; on le règle souvent à quelques centaines.

Simulateur — le bloc du ZMM

Ajoute des variables et regarde le bloc se remplir, puis s'agrandir d'un coup quand il déborde. Termine la requête pour tout libérer — ou dépasse memory_limit et provoque l'erreur fatale.

Bloc loué par le ZMM 0 / 0 unités · limite 100
05 — RAMASSE-MIETTES

Le compteur de références et le GC

PHP n'est pas un langage « garbage collecté » au sens classique. Il compte des références — et le ramasse-miettes ne sert qu'à un seul cas tordu : les cycles.

On entend souvent que « PHP a un garbage collector ». C'est vrai, mais c'est trompeur, parce qu'il ne fait pas du tout ce qu'on imagine. Le vrai mécanisme qui gère la mémoire de tes variables, c'est le comptage de références.

Comptage de références
Chaque valeur partageable (chaîne, tableau, objet…) garde un compteur du nombre de variables qui pointent vers elle. Quand une variable disparaît, le compteur baisse. Dès qu'il tombe à zéro, plus personne n'utilise la valeur : elle est libérée immédiatement, sans attendre.

Petite précision technique utile : depuis PHP 7, ce n'est pas la variable (le zval) qui porte le compteur, mais la valeur elle-même quand elle est de type comptable — chaîne, tableau, objet, ressource. Les valeurs simples (entier, flottant, booléen, null) ne sont jamais comptées : elles sont juste copiées, c'est moins cher.

Copy-on-write : on ne copie qu'au dernier moment

Le comptage de références permet une optimisation élégante. Quand tu écris $b = $a; sur un gros tableau, PHP ne duplique rien : les deux variables pointent vers la même valeur, dont le compteur passe à 2. La copie réelle n'a lieu qu'à l'instant où l'une des deux est modifiée.

📄 L'image juste : c'est une photocopieuse paresseuse. Tant que tout le monde se contente de lire la feuille, on partage l'original. Dès que quelqu'un veut écrire dessus, on lui en fait une photocopie pour lui tout seul — c'est la « séparation ». Tu manipules des gros tableaux sans le coût de la copie, tant que tu ne les modifies pas.

Le trou : les références circulaires

Le comptage de références a un angle mort. Imagine deux objets qui se pointent mutuellement : $a référence $b, et $b référence $a. Même quand tu fais unset($a) et unset($b), chacun est encore référencé par l'autre. Leurs compteurs ne tombent jamais à zéro. Plus personne ne peut les atteindre depuis ton code, mais ils restent en mémoire : c'est une fuite.

C'est exactement — et uniquement — pour ce cas qu'existe le ramasse-miettes, arrivé avec PHP 5.3.

Le collecteur de cycles
Quand le compteur d'une valeur baisse sans atteindre zéro, elle devient une racine « peut-être en cycle » et part dans un tampon (le root buffer, 10 000 entrées par défaut). Quand ce tampon est plein, le collecteur se déclenche : il parcourt ces racines pour repérer les groupes d'objets qui ne s'accrochent qu'entre eux, et les libère.

D'où la formule à retenir : PHP n'est pas un langage « garbage collecté » au sens d'un Java ou d'un Go. PHP libère la mémoire principalement par comptage de références, en temps réel ; le ramasse-miettes n'est qu'un filet de sécurité pour les cycles.

Simulateur — refcount et cycle

Crée deux objets, observe leurs compteurs, puis fabrique une référence circulaire. Fais unset : tu verras que sans cycle la mémoire part tout de suite, mais qu'avec un cycle il faut lancer le GC.

$a → objet A refcount : 0
$b → objet B refcount : 0
Root buffer : 0 racine(s) en attente
06 — SHARE-NOTHING

Share-nothing : repartir de zéro

Parce que le ZMM libère tout à la fin de chaque requête, rien ne survit d'une requête à l'autre. C'est la plus grande force de PHP — et sa plus grande limite.

On a vu que le ZMM libère son bloc entier à la fin de chaque requête. Tire le fil de cette idée et tu tombes sur la propriété la plus marquante de PHP : rien ne survit d'une requête à l'autre. Toutes les variables, toutes les données, tout ce que ton code a construit — effacé. La requête suivante repart d'une feuille blanche.

Share-nothing
Aucune donnée n'est partagée entre les traitements de requêtes : ni entre deux requêtes traitées l'une après l'autre par le même processus, ni entre des requêtes traitées en parallèle par plusieurs processus. Chaque requête vit dans sa propre bulle, et la bulle éclate à la fin.

Pourquoi c'est génial

Cette propriété est sans doute l'une des grandes raisons du succès de PHP, en particulier comme premier langage. Repartir de zéro à chaque requête, ça simplifie énormément la vie : pas de fuite mémoire à traquer sur la durée, pas besoin de fermer religieusement chaque connexion ou chaque fichier, et surtout aucun risque que les données de l'utilisateur A traînent encore en mémoire quand tu traites l'utilisateur B. Tu te trompes dans une variable globale ? Tant pis, elle disparaît à la fin de la requête. Le sol est nettoyé entre chaque client.

Pourquoi ça coûte cher

Le revers apparaît dès qu'on cherche la performance ou qu'on encaisse beaucoup de trafic. Ne rien partager, ça veut dire pas de cache DNS, pas de connexion persistante vers ta base de données, pas de connexion persistante vers les API que tu appelles. À chaque requête, le premier appel vers un service rouvre une connexion TCP, refait un handshake TLS, se ré-authentifie… et tout ça prend du temps et brûle des ressources, encore et encore.

🔁 Le gâchis à l'échelle : rejouer la résolution DNS, rouvrir une connexion TCP, refaire un handshake TLS, recharger des données depuis un cache et les désérialiser — à chaque requête, pour les mêmes destinations. Multiplié par des millions de requêtes, c'est une montagne de CPU et de temps dépensés à refaire ce qu'on vient de faire.

Les parades existent, mais aucune n'est gratuite. Côté infrastructure, on intercale souvent un composant qui maintient un pool de connexions ouvertes (un proxy de base de données, par exemple), pour que PHP n'ait plus à les rouvrir. Côté PHP, certaines extensions allouent de la mémoire en dehors du ZMM et peuvent donc conserver des choses d'une requête à l'autre : c'est ainsi que l'extension MySQL propose des connexions persistantes. Mais ça veut dire du code C — plus risqué, et plus pénible à installer qu'un simple composer require.

La parade la plus radicale consiste à sortir carrément du modèle : faire tourner un serveur PHP au long cours qui reste vivant et partage des données entre les requêtes. On en parle dans la synthèse, et le sujet est creusé dans PHP — Concurrence & Runtimes.

Simulateur — deux requêtes, une bulle qui éclate

Traite deux requêtes successives. Sans connexion persistante, chacune refait DNS + TCP + TLS. Active la persistance (via extension) et regarde la deuxième requête réutiliser ce qui existe déjà.

Temps cumulé : 0 ms
07 — RESSOURCES LIMITÉES

Les ressources ne sont pas illimitées

CPU, RAM, bande passante, connexions, processus FPM : tout est compté. Et la ressource la plus critique, c'est souvent celle qu'on oublie — le processus PHP lui-même.

Tout ce qu'on a vu jusqu'ici consomme quelque chose. Et la grande leçon de Pascal Martin, c'est qu'aucune de ces ressources n'est illimitée — même en PHP, où on a tendance à l'oublier.

Le CPU

Le CPU se dépense à deux endroits : la compilation (qu'OPcache supprime presque entièrement) et l'exécution de ton code. Combien ? Ça dépend de ce que tu écris. Quand PHP attend une réponse SQL ou une API distante, il ne consomme quasiment pas de CPU — il poireaute. À l'inverse, du chiffrement, du redimensionnement d'images, le chargement d'un énorme fichier YAML ou la sérialisation JSON d'un objet gigantesque que tu ne devrais pas renvoyer en entier : ça brûle. Et souviens-toi qu'un processus PHP consomme au maximum un cœur à la fois ; comme on en lance plusieurs en parallèle, ils se partagent les quelques cœurs de la machine.

La RAM, et tout le reste

La mémoire, on l'a vue : les variables, le processus lui-même, les opcodes en cache. Dépasse, et c'est l'OOM kill. Mais la liste des ressources comptées est bien plus longue : la bande passante réseau (une appli qui stockait d'énormes objets dans Redis a déjà saturé tout le réseau d'un serveur), le nombre de connexions ouvertes, les descripteurs de fichiers, l'espace et le débit disque… Chacune peut devenir le goulot qui fait tout coincer.

La ressource qu'on oublie : le processus PHP-FPM

Voici la plus sournoise. Avec PHP-FPM, chaque processus ne traite qu'une seule requête à la fois. Quand une requête dure longtemps — typiquement parce que PHP attend une base ou une API lente — ce processus est immobilisé. Pendant ce temps, les nouvelles requêtes n'ont plus de processus libre pour les accueillir, et les utilisateurs finissent par prendre une erreur.

Le piège du traitement après réponse : on est souvent tenté de répondre vite à l'utilisateur, puis de faire le gros du travail « après » (envoi d'e-mail, calcul lourd) — par exemple via kernel.terminate dans Symfony. Sauf que le processus PHP-FPM reste occupé pendant ce travail. Tu as répondu au premier utilisateur, mais tu n'as pas répondu à tous ceux qui attendaient un processus libre. Ça semble marcher sur ton poste de dev, et ça s'écroule en production sous charge. Le bon réflexe : sortir ce travail de la requête, vers une file de messages.

Les limites en cascade

Pour encaisser plus, on lance plus de processus en parallèle. Mais on ne peut pas en lancer une infinité : chacun consomme de la mémoire et peut occuper un cœur. Et au-delà de PHP, les limites s'enchaînent : ta base a un nombre maximum de connexions, Redis accepte un certain nombre de nouvelles connexions par seconde, les NAT gateways du cloud aussi, et toutes les API externes posent des rate limits qui rejettent tes requêtes au-delà d'un certain débit.

Et donc : des timeouts courts

C'est ici que les timeouts deviennent vitaux. Configure-les sur toutes tes requêtes sortantes, et fais-les courts. L'exemple de Pascal Martin est limpide : une API qui répond d'habitude en 100 ms, sur laquelle tu mets « un peu de marge » avec un timeout à 1 seconde. Le jour où cette API galère, tes clients attendent dix fois plus longtemps qu'en temps normal — donc dix fois plus de processus PHP-FPM occupés à attendre. Et quand tu consommes dix fois plus de processus que d'habitude, en général, tout se met à merder.

Simulateur — l'amplification des timeouts

Choisis l'état de l'API en aval et la durée du timeout, puis lance une vague de requêtes. Regarde combien de workers se retrouvent bloqués à attendre, et combien de requêtes sont rejetées faute de worker libre.

API en aval
Timeout
Servies : 0 Rejetées : 0
08 — SYNTHÈSE

Aller plus loin que « plus de serveurs »

Pas de solution magique. Mais quand tu sais comment PHP fonctionne, tu disposes de bien plus d'un levier que « rajouter des serveurs ».

Quand une application rame ou consomme toutes les ressources de son serveur, le premier réflexe est presque toujours le même : aller voir les ops et réclamer « plus de serveurs ». Et parfois, c'est la bonne réponse. Si chaque utilisateur servi te rapporte de l'argent, payer plus de capacité pour en servir davantage est tout à fait raisonnable.

Mais ajouter des serveurs a ses limites. Les ressources ne sont ni illimitées ni instantanées : sur de l'hébergement classique, un nouveau serveur peut prendre des semaines ; dans le cloud, l'autoscaling met des secondes à des minutes — et il n'est pas instantané, alors qu'un pic de charge, lui, ne se laisse pas toujours prévoir. Sans compter que ça coûte. C'est pour ça que la réponse des ops — « optimisez votre code » — n'a souvent pas tort : on voit régulièrement des temps de réponse divisés par dix, par cent, parfois par mille, en quelques heures de travail.

Les leviers, quand on connaît le moteur

LevierCe que ça soulage
OPcache bien dimensionnéLe CPU : plus de recompilation à chaque requête.
Timeouts courts partoutLes processus FPM : on n'immobilise pas le pool à attendre.
Requêtes sortantes en concurrenceLe temps d'occupation : lancer SQL, Elasticsearch et API en parallèle plutôt qu'en file indienne.
Travail lourd hors requêteLe pool : déléguer à une file de messages au lieu d'occuper un worker.
Moins de code, moins de dépendancesCPU et RAM : « la moitié d'internet en dépendance » se paie à chaque requête.

Sortir du share-nothing

Le levier le plus profond, c'est de remettre en cause le modèle lui-même. Plutôt que de tout refaire à chaque requête, on peut écrire un serveur PHP au long cours qui reste vivant, traite les requêtes les unes après les autres (ou en parallèle, en asynchrone), et partage des données entre elles : connexions ouvertes, caches en mémoire, configuration déjà chargée. C'est ce que permettent des projets comme ReactPHP, AMPHP ou FrankenPHP.

🔑 Le compromis : hors du share-nothing, une erreur fatale peut impacter plusieurs requêtes à la fois, et toutes les bibliothèques ne sont pas prévues pour ce paradigme. C'est un autre monde, à embrasser en connaissance de cause. Le détail est dans PHP — Concurrence & Runtimes.

Une question revient toujours : « et le multithreading ? » En PHP, il n'y a pas de multithreading côté code applicatif par défaut. L'extension parallel (qui a remplacé l'ancienne pthreads) peut aider, mais elle exige une option de compilation spéciale (un build ZTS) qui n'est généralement pas activée — il faut donc recompiler PHP soi-même. À vivre au moins une fois.

Ce qu'il faut retenir

Le moteur en une phrase
Ton code est compilé en opcodes (qu'OPcache met en cache), exécuté par la VM Zend, sa mémoire est prêtée puis rendue d'un bloc par le ZMM à chaque requête — d'où le share-nothing, qui simplifie tout mais oblige à tout refaire à chaque fois.

Un processus PHP-FPM traite une requête à la fois et occupe jusqu'à un cœur. Pour aller plus vite : faire moins de choses, ou des CPU plus rapides. Pour traiter plus de requêtes : plus de cœurs, en scaling horizontal. Pas de magie — mais une fois que tu sais ça, tu choisis tes leviers au lieu de les subir.

Et le mot de la fin de Pascal Martin tient en deux idées. D'abord, va lire le code source de PHP quand un comportement t'intrigue : c'est plus abordable qu'il n'y paraît, et instructif. Ensuite, construire un bon produit va bien plus loin qu'écrire du code PHP : parle avec les gens qui hébergent tes applications. C'est en croisant le regard de ceux qui connaissent l'appli et de ceux qui connaissent les serveurs qu'on va vraiment plus loin.