Table des matières :
- Workers FrankenPHP : ce qui change réellement par rapport à PHP-FPM
- Pourquoi PrestaShop n’est pas naturellement « worker-safe »
- Compatibilité en pratique : ce qui tient, ce qui casse, ce qu’on isole
- Stabiliser des workers FrankenPHP : rotation, limites mémoire, et reset applicatif
- Un protocole de validation réaliste (sinon vous ne verrez pas les bugs)
- Ce qu’on déploie en production quand on veut dormir la nuit : architecture hybride
FrankenPHP est intéressant dès qu’on veut sortir du triptyque « Nginx/Apache + PHP-FPM » et retrouver un modèle app server plus proche de ce qu’on fait en Go/Java : process longs, workers, TLS et HTTP/3 gérés au même endroit. Le problème, côté PrestaShop, c’est que le cœur et l’écosystème modules/overrides ont été conçus historiquement pour l’architecture share-nothing (un état, une requête) des SAPIs web classiques. En worker, cet axiome saute.
L’objectif ici n’est pas de “vendre” FrankenPHP : c’est de cadrer ce qui est réellement compatible, ce qui cassera, et comment stabiliser des workers en production sans transformer votre boutique en banc de test permanent. Pour la suite, je pars sur PrestaShop 8.1/8.2 et PrestaShop 9.x, PHP 8.2/8.3 (cf. vos contraintes de compatibilité : https://www.expertise-prestashop.fr/2026/07/23/prestashop-9-versions-php-recommandees-et-incoherences-de-documentation/) et un déploiement Linux moderne (Debian 12 / Ubuntu 24.04).
Ce qu’il faut garder en tête dès le départ :
- FrankenPHP “sans worker” peut déjà simplifier la stack (Caddy, TLS auto, HTTP/3) tout en restant proche d’un modèle de requêtes “isolées”.
- FrankenPHP “avec workers” est un changement d’exécution applicative : on ne parle plus d’un simple “remplacement de FPM”, mais d’un mode où votre code doit être resettable et observé comme un service long-running.
Workers FrankenPHP : ce qui change réellement par rapport à PHP-FPM
En mode “worker”, FrankenPHP exécute du PHP dans des processus longs qui traitent plusieurs requêtes successives dans la même VM PHP, sans recréer le contexte d’exécution comme le ferait une requête web “standard”. C’est le point clé : la mémoire, les statiques, certains handles et caches “vivants” persistent tant que le worker n’est pas recyclé.
À ne pas confondre avec PHP-FPM : les processus FPM sont eux aussi long-lived côté OS, mais PHP réinitialise l’état d’exécution à chaque requête (superglobales, liste des fichiers inclus, etc.). Autrement dit, avec FPM, vous avez un processus long, mais un request lifecycle très cadré ; avec un worker FrankenPHP, vous vous rapprochez d’une boucle applicative qui doit explicitement “nettoyer” entre deux requêtes.
Table de lecture rapide (ce qui change vraiment) :
| Sujet | PHP-FPM (modèle web classique) | FrankenPHP worker (process long) |
|---|---|---|
| Cycle de vie PHP | init/handle/cleanup par requête | boucle applicative, cleanup à votre charge (ou via framework) |
require_once |
“une fois par requête” | “une fois par worker” (risque de bootstrap partiel) |
| Statiques / singletons | réinitialisés implicitement (dans la plupart des cas) | peuvent persister et “déteindre” d’une requête à l’autre |
| Connexions DB / sockets | souvent recréées (ou pool côté driver) | peuvent rester ouvertes longtemps (bénéfice perf + risques réseau) |
| Déploiement | reload FPM / rotation simple | attention aux workers “anciens” qui continuent de servir après release |
Ce changement de modèle impacte des détails concrets que beaucoup de code PHP « web classique » ignore : une inclusion require_once devient réellement globale au worker (les fichiers inclus une fois ne se ré-exécutent plus), les caches statiques peuvent croître sans jamais redescendre, et des ressources (sockets, connexions BD, descripteurs de fichiers) peuvent rester ouvertes “trop longtemps”. Le gain de perf vient précisément de cette persistance (moins de bootstrap, moins d’IO, plus de cache chaud), mais c’est aussi la source de la plupart des instabilités.
Sur ce point, la doc PHP est très claire sur un sujet connexe (connexions persistantes) : « Persistent connections are not closed at the end of the script » (PHP Manual, PDO) — ce qui résume bien le changement de mentalité à avoir avec un process long, même si vous n’utilisez pas explicitement ATTR_PERSISTENT : dans un worker, la fin du script ne signifie pas “fin du monde”.
Deux implications très opérationnelles en e-commerce (souvent visibles dès les premiers tests) :
- Risque “multi-tenant accidentel” : une valeur restée en mémoire (langue, devise, shop context, client) peut contaminer une autre requête si elle est stockée dans un singleton non réinitialisé.
- Risque de dérive lente : pas forcément d’erreur immédiate, mais une montée progressive du RSS et/ou des latences (p95/p99) jusqu’à un redémarrage violent.
Pour le contexte FrankenPHP/Caddy (TLS auto, HTTP/3, configuration), vous avez déjà une base sur le site : https://www.expertise-prestashop.fr/2026/07/29/frankenphp-serveur-dapplication-php-avec-caddy-http-3-et-https-auto/. Ici, on reste focalisé sur stabilité des workers et compatibilité PrestaShop.
Pourquoi PrestaShop n’est pas naturellement « worker-safe »
PrestaShop (comme la plupart des CMS e-commerce PHP historiques) s’appuie massivement sur : singletons (Context::getContext(), Db::getInstance()), caches statiques, variables globales, constantes, et une quantité non négligeable d’effets de bord à l’initialisation. En PHP-FPM, ces choix sont “tolérables” parce que l’état est implicitement jeté à la fin de la requête. En worker, ces objets peuvent survivre et fuir d’une requête à la suivante, surtout si un module a stocké de l’état dans des propriétés statiques pour “optimiser”.
Concrètement, ce qui pose problème en worker sur une boutique PrestaShop “réelle” (modules + overrides) :
Contextet ses dépendances : client, panier, devise, langue, boutique (multi-boutique), employee (BO). Si une extension “met en cache” un objetCustomerouCartdans une statique, vous créez un mélange de sessions.- Smarty et caches applicatifs : certains plugins ou optimisations (minification, compilation, caches maison) gardent des références qui ne devraient vivre que le temps d’une requête.
- Hooks et ordre d’exécution : un module peut initialiser un état lors d’un hook “précoce” (par exemple un client HTTP, un SDK paiement, un logger), puis réutiliser cet état sans tenir compte des paramètres de la requête suivante.
- Événements “one-shot” : des initialisations qui supposent qu’elles seront rejouées à chaque requête (ex. recharger une configuration depuis DB), mais qui ne se rejouent plus si le code a été inclus une fois et ne repasse plus par le même chemin.
Le point le plus violent, en pratique, c’est la combinaison bootstrap + require_once. Beaucoup d’entrées PrestaShop (FO/BO) incluent une chaîne de fichiers qui n’ont jamais été écrits pour être rejoués N fois dans le même process. Or en worker, si votre handler ré-exécute un front controller qui s’appuie sur des require_once, une partie du “boot” n’aura lieu qu’une seule fois, et vous vous retrouvez rapidement avec : configuration partiellement initialisée, hooks mal rechargés, contexte client qui “déteint”, et des caches internes incohérents.
Mini-scénario typique (vu en préprod) :
un module de personnalisation stocke la devise courante dans une propriété statique “pour éviter un appel à Context”. Après quelques centaines de requêtes, une fiche produit en EUR est servie avec des montants en GBP (ou l’inverse) pour un autre client. En PHP-FPM, ce bug peut rester invisible car l’état est jeté ; en worker, il devient intermittent et donc difficile à diagnostiquer.
À ça s’ajoute l’écosystème : overrides, modules legacy, modules Symfony, scripts d’import, etc. Même si le core était rendu strictement “resettable”, il suffit d’un module qui conserve des références (ex : un client HTTP, un logger custom, une instance Doctrine/DBAL, un cache maison) et vous retombez sur des comportements non déterministes. Quand on parle de compatibilité FrankenPHP + PrestaShop, il faut comprendre : compatibilité du core + compatibilité de vos modules + compatibilité de vos overrides.
Enfin, un détail souvent sous-estimé : PrestaShop fait des choses “lourdes” par requête (Smarty, génération de CSS/JS, reconstruction de contexte, indexation, calculs de prix). Le worker peut améliorer le temps de bootstrap, mais il peut aussi amplifier les effets de memory bloat si vos pages déclenchent des caches statiques volumineux (catalogues, règles panier, combinaisons). Si votre enjeu principal est la perf front, commencez par cadrer la base (SQL, caches, Redis, CWV) via : https://www.expertise-prestashop.fr/2026/07/27/prestashop-performance-optimiser-gros-catalogues-et-core-web-vitals/ et https://www.expertise-prestashop.fr/2026/07/13/redis-prestashop-configurer-le-cache-sur-vps-ou-serveur-dedie/.
Compatibilité en pratique : ce qui tient, ce qui casse, ce qu’on isole
En production, la stratégie la moins risquée consiste à découpler : utiliser FrankenPHP (Caddy + PHP intégré) en mode “classique” pour la boutique (comportement proche d’un SAPI web standard), et réserver les workers à des endpoints explicitement conçus pour être long-running. Typiquement : une API Symfony (Admin API / API Platform), un microservice PHP interne, ou un module exposant un endpoint Symfony propre, sans dépendances legacy non resettable.
Ce découplage est aussi une manière de réduire le blast radius : si un worker dérive, vous perdez un segment (ex. /api) mais pas tout le checkout.
Un découpage simple (et souvent suffisant) :
| Zone | Exemples | Mode conseillé | Pourquoi |
|---|---|---|---|
| FO legacy | /, catégories, fiches produit, panier, checkout |
“classique” (sans worker) | beaucoup de legacy + hooks + Smarty + état implicite |
| BO | /admin |
souvent “classique” au début | mélange legacy + Symfony selon versions/modules |
| Endpoints maîtrisés | /api, webhooks, endpoints techniques Symfony |
worker si reset Symfony et code contrôlé | code plus “framework”, services resettable |
| Jobs lourds | imports, sync, réindexation | CLI supervisée | évite timeouts, état web, limites mémoire |
Ce n’est pas “moins bien” : c’est juste aligné avec la réalité du code existant. Et si votre objectif est la perf sur pics de trafic, le gain le plus robuste vient souvent d’architecture/caches (CDN, Redis, Varnish, HA) plutôt que d’un changement de SAPI. Voir : https://www.expertise-prestashop.fr/2026/07/10/prestashop-pics-de-trafic-architecture-cloud-scalable-et-haute-disponibilite/.
Pour PrestaShop lui-même, l’endroit où le “worker mode” est le plus réaliste, c’est tout ce qui passe par le noyau Symfony et son container (BO moderne, API d’administration si activée). Symfony a un concept explicite de reset entre requêtes, via des services réinitialisables. La doc Symfony décrit ce mécanisme (services taggés kernel.reset, ResetInterface, etc.), précisément pour les process longs.
À l’inverse, ce qui a tendance à casser vite en worker : Front Office legacy, contrôleurs legacy, exécution de hooks modulaires non maîtrisés, et tout ce qui dépend de caches statiques dont la taille est proportionnelle au catalogue ou à la navigation utilisateur. Le symptôme typique n’est pas forcément une erreur immédiate : c’est une dérive lente. Au bout de 5 000–50 000 requêtes, vous observez un RSS mémoire qui monte, des temps de réponse qui s’allongent, puis un 503/502 parce que le worker est tué par l’OOM killer ou qu’il devient trop lent.
La décision “compatibilité” doit donc être formulée comme une question opérationnelle : quels chemins URL et quelles features passent en worker, et comment vous limitez le blast radius ? Un bon réflexe, en contexte e-commerce (et particulièrement sur des boutiques françaises/UE soumises à des exigences de disponibilité et de conformité), est de faire passer en worker uniquement des routes “techniques” qui ne manipulent pas directement du rendu FO, et de garder le checkout sur une voie éprouvée.
Enfin, les scripts lourds (imports, sync, jobs) sortent du web et passent en CLI (supervisé), cf. https://www.expertise-prestashop.fr/2026/07/29/import-catalogue-prestashop-pipeline-api-first-automatise-et-supervise/.
Stabiliser des workers FrankenPHP : rotation, limites mémoire, et reset applicatif
Le principe de base, en worker, c’est d’assumer qu’un process long doit être recyclé. Vous cherchez la stabilité, pas l’uptime infini d’un process PHP. En pratique, vous définissez une politique : recycler après N requêtes ou quand la mémoire dépasse X. FrankenPHP fournit des mécanismes de workers gérés côté serveur (voir la doc officielle), mais même sans option “magique”, vous pouvez encadrer via supervision (systemd, Docker) et redémarrages contrôlés.
- Doc FrankenPHP (repo + docs) : https://github.com/dunglas/frankenphp et https://frankenphp.dev/
Une politique “safe par défaut” (à ajuster après mesures) ressemble souvent à :
- Recyclage par requêtes : 1 000 à 10 000 requêtes/worker (commencer bas en préprod, monter si stable).
- Garde-fou mémoire (RSS) : 256–512 MiB/worker selon la taille du code et le trafic (catalogues lourds → plutôt bas au début).
- Recyclage sur dérive latence : si p95/p99 dégrade dans le temps à trafic stable, c’est un signal de fuite ou de fragmentation.
Deuxième point : OPcache et chargement des classes. En workers, OPcache reste utile (il l’est déjà en FPM), mais l’optimisation se joue différemment : vous avez moins d’intérêt à “minimiser le bootstrap” si le kernel/worker reste chaud, mais vous avez besoin d’éviter les comportements bizarres en cas de déploiement (fichiers modifiés, classes rechargées partiellement). Sur les paramètres OPcache, gardez une configuration cohérente avec votre stratégie de release (atomique, symlink, blue/green). Si vous voulez une base de réglages structurée, partez de : https://www.expertise-prestashop.fr/2026/07/23/php-opcache-parametres-recommandes-pour-optimiser-les-performances/.
Troisième point : reset applicatif. Si vous utilisez un handler Symfony (c’est le seul cas raisonnable côté PrestaShop pour du worker “propre”), vous devez vous assurer que les services non stateless sont réinitialisés. Un pseudo-squelette de worker (à adapter à votre kernel PrestaShop/Symfony) ressemble à ceci :
<?php
// worker.php (exemple conceptuel)
require __DIR__.'/vendor/autoload.php';
$kernel = new AppKernel('prod', false);
$kernel->boot();
while (frankenphp_handle_request(function () use ($kernel) {
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
// Important en process long : reset entre requêtes
if ($kernel->getContainer()->has('services_resetter')) {
$kernel->getContainer()->get('services_resetter')->reset();
}
})) {
// boucle
}
Le nom exact des fonctions et services dépend de votre stack, mais la logique reste : handle → send → terminate → reset.
Deux points à vérifier côté “reset”, parce qu’ils expliquent 80% des bugs intermittents :
- Les services qui gardent des buffers (logger, mailer, HTTP clients, clients paiement) : si vous les instanciez une fois, assurez-vous qu’ils sont stateless ou explicitement reset.
- Les caches “applicatifs” (tableaux statiques, map d’objets, etc.) : un cache non borné en worker devient une fuite mémoire.
Si vous essayez de faire la même chose avec le FO legacy PrestaShop, vous retomberez vite sur le piège require_once + état global. À ce stade, le “fix” n’est plus un réglage serveur : c’est un chantier de refactor.
Quatrième point : connexions base de données. En worker, vous gardez potentiellement une connexion ouverte longtemps. C’est performant, mais vous devez gérer les cas “réseau” (failover, redémarrage MariaDB, wait_timeout). Dans un core PrestaShop non conçu pour ça, préparez un plan : soit vous forcez une reconnexion par requête (perte de perf, mais stable), soit vous implémentez un mécanisme de ping/reconnect sur erreur HY000/2006 côté couche DB (override contrôlé). Et si vous migrez MySQL/MariaDB, verrouillez aussi vos paramètres InnoDB/collations pour éviter des surprises pendant les tests : https://www.expertise-prestashop.fr/2026/07/29/prerequis-systeme-migration-mysql-vers-mariadb-innodb-et-collation-utf-8/.
Cinquième point : fuites mémoire et fragmentation. PHP a un GC, mais il ne corrige pas les mauvaises pratiques (références cycliques, gros tableaux statiques, caches qui grossissent sans limite). En worker, surveillez : RSS, memory_get_usage(true), nombre de classes chargées, et le nombre de descripteurs fichiers.
Checklist “diagnostic dérive” (simple, mais efficace) :
- RSS par PID :
ps -o pid,rss,cmd -C caddy(ou le binaire qui porte FrankenPHP) et suivre l’évolution. - FD (file descriptors) :
ls /proc/<pid>/fd | wc -l(un nombre qui monte peut signaler un leak sockets/fichiers). - Erreurs DB : “server has gone away”, timeouts, “too many connections”.
- Latences : p95/p99 qui dérivent alors que p50 reste correct → souvent contention ou GC plus fréquent.
Si vous voyez une dérive, recyclez plus agressivement et identifiez les hotspots (Blackfire, Xdebug en environnement de dev). Pour la mise en place de Xdebug 3 : https://www.expertise-prestashop.fr/2026/07/29/xdebug-installer-et-activer-xdebug-3-pour-php-cli-et-web/ et https://www.expertise-prestashop.fr/2026/07/14/xdebug-vs-code-configurer-le-debogage-php-en-local/.
Un protocole de validation réaliste (sinon vous ne verrez pas les bugs)
Valider FrankenPHP en mode worker sur un e-commerce ne se fait pas avec “ça répond sur /”. Il vous faut au minimum un soak test (durée longue) et un load test (charge réaliste). Un test court (5 minutes) vous dira que c’est “OK” ; un test long (2–12 heures) vous dira si les workers dérivent. Concrètement : simulez navigation (catégories, fiches produit, panier, checkout), BO (listing commandes), et endpoints modules (paiement, tracking). Mesurez p50/p95/p99, taux d’erreur, et memory RSS par worker.
Une approche pragmatique (qui évite les faux positifs) :
- Étape 1 — Soak test à faible charge : 1 à 5 VU (virtual users) pendant 2–4h, mais couvrant beaucoup de parcours. Objectif : détecter fuite mémoire/FD et bugs de contamination de contexte.
- Étape 2 — Load test : montée progressive (ramp-up) jusqu’à une charge proche de vos pics habituels, sur 20–40 minutes. Objectif : observer la stabilité (502/503), les verrous DB, et la latence.
- Étape 3 — Test de “release” : déployer une nouvelle version (même mineure) en plein test pour vérifier le comportement des workers (code ancien/nouveau, OPcache, rotation).
Pour exécuter des tests reproductibles, un outil courant et robuste est k6 (Grafana Labs), notamment pratique en CI : https://k6.io/docs/
En parallèle, mettez l’observabilité au niveau attendu : logs applicatifs, logs PHP, logs serveur, et alertes. Les 503 et 502 sont typiquement le premier signal visible quand un worker part en vrille. Pour un diagnostic structuré côté serveur : https://www.expertise-prestashop.fr/2026/07/14/erreur-http-503-diagnostic-serveur-logs-et-ressources/ et pour la collecte côté boutique : https://www.expertise-prestashop.fr/2026/07/15/prestashop-monitoring-derreurs-logs-php-mysql-javascript-et-alertes-e-mail/.
Une règle opérationnelle utile : définissez un seuil de recyclage basé sur des métriques (ex : si RSS > 512 MiB ou après 5 000 requêtes), puis comparez la stabilité et les perfs vs PHP-FPM sur le même dataset. Si vous ne constatez pas de gain significatif (par exemple, une baisse mesurable de TTFB sur pages non cachées, ou une réduction CPU), n’insistez pas : vous êtes probablement limité ailleurs (SQL, IO, cache).
Enfin, prévoyez un rollback sans drame. L’idéal reste un déploiement blue/green avec bascule au reverse proxy (HAProxy, Caddy, etc.). Sur ce point, gardez une approche “incrémentale” (migrer un chemin URL à la fois) plutôt qu’un big bang : https://www.expertise-prestashop.fr/2026/07/23/migration-prestashop-audit-technique-et-plan-incremental-blue-green/ et, si vous êtes déjà sur HAProxy, https://www.expertise-prestashop.fr/2026/07/13/haproxy-reverse-proxy-terminaison-tls-rate-limiting-et-supervision-prometheus/.
Ce qu’on déploie en production quand on veut dormir la nuit : architecture hybride
Sur une boutique PrestaShop existante avec modules, la trajectoire la plus robuste est hybride : FrankenPHP pour simplifier la stack et profiter de Caddy (TLS, HTTP/3, config), mais workers uniquement là où vous avez un code explicitement compatible process long (Symfony propre, services resettable, pas de legacy non maîtrisé). Vouloir forcer tout le FO legacy en worker “parce que ça va plus vite sur le papier” revient souvent à déplacer le problème : vous gagnez du bootstrap et vous perdez en déterminisme.
Une architecture hybride “propre” ressemble souvent à :
- Un service FrankenPHP “classique” pour FO/BO (comportement proche d’un serveur PHP standard, faible risque fonctionnel).
- Un service FrankenPHP “workers” pour
/apiet endpoints techniques (même infra, mais surface contrôlée). - Un reverse proxy (ou Caddy lui-même selon votre design) qui route par préfixe d’URL, et qui permet une bascule rapide en cas d’incident.
Si votre objectif est la perf globale, investissez d’abord dans : cache HTTP/CDN, cache applicatif Redis, tuning MariaDB, et réduction des requêtes N+1 (surtout sur gros catalogues). Les gains sont plus stables et mieux connus dans l’écosystème PrestaShop. Si votre objectif est l’industrialisation (Docker, immutable infra), FrankenPHP s’intègre bien, mais dimensionnez correctement votre infra (CPU/RAM/IO) : https://www.expertise-prestashop.fr/2026/07/29/vps-pour-docker-criteres-techniques-et-ressources-recommandees/ et, pour du lourd, https://www.expertise-prestashop.fr/2026/07/17/serveurs-dedies-infogeres-e-commerce-nvme-redis-varnish-et-haute-disponibilite/.
Dans cette approche, votre “checklist” de compatibilité FrankenPHP/PrestaShop devient simple et actionnable :
- Isoler les chemins worker-safe (et assumer que le FO legacy ne l’est pas par défaut).
- Recycler agressivement (requêtes, mémoire, latence).
- Reset Symfony entre requêtes (services resetter, services taggés
kernel.reset). - Monitorer mémoire/FD/latences (et alerter avant l’incident).
- Rollback prêt (bascule blue/green, routage par URL).
Le reste (rendre le FO legacy entièrement worker-safe) est possible, mais c’est un chantier comparable à une modernisation profonde — et ça se planifie comme tel, avec audit de code, refactor mesurable et dette technique traitée comme une contrainte produit, pas comme un tweak serveur (voir : https://www.expertise-prestashop.fr/2026/07/21/dette-technique-symfony-profiling-blackfire-et-refactoring-mesurable/).
