Skip to Content

Chapitre 15.3 — FrankenPHP worker mode

Partie 15 : Expert — Chapitre 3/9 Version de référence : Symfony 7.4 / FrankenPHP.

⏱️ TL;DR

  • Modèle classique PHP-FPM : une requête = un bootstrap complet de l’application (puis on jette tout). Simple, sûr, mais coûteux.
  • FrankenPHP worker mode : l’application est bootstrappée une seule fois et réutilisée pour de nombreuses requêtes → gros gain de perf (proche de Node).
  • Symfony réinitialise l’état entre les requêtes (via runtime/frankenphp-symfony) pour éviter les fuites.
  • Précaution absolue : les services doivent être sans état (rappel Partie 5.1). Un état stocké dans une propriété de service fuit d’une requête à l’autre.
  • On redémarre les workers périodiquement (max requêtes) pour contenir les fuites mémoire.
  • Pour un dev Next.js : c’est le passage du modèle « serverless/CGI » (process jetable) au modèle « serveur long-running » (node server.js) — d’où des perfs comparables, mais avec l’isolation d’état de PHP en prime.

🎯 Objectifs

  • Comprendre la différence FPM vs worker mode.
  • Mettre en place FrankenPHP worker avec Symfony.
  • Éviter les fuites d’état et de mémoire.
  • Savoir quand l’utiliser.

1. FPM vs worker : le changement de modèle

En FPM, chaque requête paie le bootstrap : charger l’autoloader, construire le conteneur, initialiser les bundles… puis tout est jeté. En worker mode, ce bootstrap se fait une fois, et l’application reste en mémoire pour servir les requêtes suivantes — le coût par requête chute fortement.

2. Mettre en place le worker

FrankenPHP + le composant Symfony Runtime :

composer require runtime/frankenphp-symfony

On configure l’environnement pour utiliser le runtime FrankenPHP, et FrankenPHP lance un script worker qui boucle :

// public/worker.php (schéma simplifié — Runtime le gère pour vous) // bootstrap du Kernel une fois $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); $handler = function () use ($kernel) { $request = Request::createFromGlobals(); $response = $kernel->handle($request); // réutilise le kernel bootstrappé $response->send(); $kernel->terminate($request, $response); // Symfony réinitialise l'état "resettable" entre les requêtes }; // FrankenPHP appelle $handler pour chaque requête, dans le même process

Le composant Runtime + le bridge Symfony gèrent la réinitialisation des services entre les requêtes (ceux qui implémentent ResetInterface sont remis à zéro — le profiler, la sécurité, le logger de requête…).

3. Le danger n°1 : la fuite d’état

En FPM, un état accidentellement stocké dans un service disparaît à la fin de la requête (tout est jeté). En worker, le service survit entre les requêtes — donc un état résiduel fuit :

final class CurrentUserContext { private ?User $user = null; // ❌ DANGER en worker mode public function set(User $user): void { $this->user = $user; } public function get(): ?User { return $this->user; } // renvoie l'user d'une AUTRE requête ! }

En worker, si la requête A pose $user et que la requête B (même process) lit get() sans avoir posé le sien, elle obtient l’utilisateur de A — une fuite de données critique.

⚠️ Piège fondamental : les services doivent être sans état (rappel Partie 5.1). En worker mode, cette discipline devient critique de sécurité, pas juste de propreté. Ne stockez jamais d’état par requête dans une propriété de service. Si vous devez tenir un « contexte de requête », utilisez un service qui implémente ResetInterface (Symfony le réinitialise entre les requêtes) ou récupérez l’utilisateur via Security (qui, lui, est correctement réinitialisé). Tester en worker mode révèle ces bugs que FPM masquait.

4. Gérer la mémoire

Un worker est un process long-running : la mémoire peut croître (caches internes, fuites de bibliothèques tierces). On limite le nombre de requêtes par worker avant redémarrage :

# Caddyfile / config FrankenPHP (schéma) frankenphp { worker { file ./public/index.php num 4 # 4 workers max_requests 1000 # redémarre chaque worker après 1000 requêtes } }

Le redémarrage périodique contient les fuites (le nouveau process repart propre) — la même idée que --time-limit pour les workers Messenger (Partie 12.2).

5. Quand l’utiliser

  • Idéal : API à fort trafic (le gain de bootstrap se cumule sur chaque requête), microservices, backend d’un front Next.js très sollicité.
  • Moins critique : petit site, back-office peu chargé (FPM suffit, plus simple à opérer).
  • Prérequis : un code sans état rigoureux (services stateless) et des tests en worker mode pour débusquer les fuites.

💡 Pour un dev Next.js : vous connaissez la différence entre serverless (fonction froide/chaude, jetable) et serveur long-running (node server.js qui garde l’état). Le worker mode fait passer Symfony du premier au second — d’où les perfs proches de Node. Mais Symfony conserve l’isolation par requête (réinitialisation), évitant les fuites d’état global qui guettent un serveur Node mal codé. C’est « le meilleur des deux » : perf du long-running + sûreté de l’isolation.

✏️ Exercices

Exercice 1. Expliquez pourquoi le worker mode est plus rapide que PHP-FPM.

✅ Solution

En FPM, chaque requête bootstrappe entièrement l’application (autoloader, conteneur, bundles) puis jette tout — ce coût est payé à chaque requête. En worker mode, ce bootstrap se fait une seule fois au démarrage du process, et l’application reste en mémoire pour servir de nombreuses requêtes. Le coût fixe par requête (bootstrap) disparaît, d’où un débit bien supérieur (proche de Node).

Exercice 2. Pourquoi un état stocké dans une propriété de service est-il dangereux en worker mode ?

✅ Solution

Parce que le service survit entre les requêtes (même process réutilisé). Un état par requête (ex. l’utilisateur courant) stocké dans une propriété persiste : la requête suivante peut lire l’état d’une requête précédentefuite de données (voir les infos d’un autre utilisateur). En FPM ce bug est masqué (tout est jeté après chaque requête) ; en worker il devient une faille. Les services doivent être sans état (ou implémenter ResetInterface).

Exercice 3. Pourquoi limiter max_requests par worker ?

✅ Solution

Parce qu’un worker long-running peut voir sa mémoire croître (caches internes, fuites de libs tierces). En le redémarrant après N requêtes, le process repart propre, ce qui contient les fuites mémoire — la même logique que --time-limit pour les workers Messenger. C’est un compromis entre le gain du long-running et la maîtrise de la mémoire.

🧠 Quiz de révision

1. Différence FPM vs worker mode ?

FPM bootstrappe à chaque requête (puis jette). Worker mode bootstrappe une fois et réutilise l’app → bien plus rapide.

2. Comment Symfony évite-t-il les fuites entre requêtes en worker ?

Il réinitialise l’état des services « resettable » (ResetInterface) entre chaque requête, via le bridge Runtime FrankenPHP.

3. Quel est le piège principal du worker mode ?

Les fuites d’état : un état par requête stocké dans une propriété de service persiste → fuite de données. Les services doivent être sans état.

4. Pourquoi redémarrer les workers périodiquement ?

Pour contenir les fuites mémoire d’un process long-running (repartir propre après N requêtes).

5. Pour quel type d’application le worker mode brille-t-il ?

Les API à fort trafic (le gain de bootstrap se cumule), avec un code sans état rigoureux.


Prochain chapitre : 15.4 — Déploiement & CI/CD — mettre BookLab en production proprement.