Quand une application Symfony affiche l’erreur suivante, plusieurs notions se mélangent facilement :
Fatal Error: Not enough shared memory for preloading.
Consider increasing the value for the opcache.memory_consumption directive in php.ini.
Qui reçoit la requête ? Qui exécute PHP ? Que contient OPcache ? Le conteneur de services Symfony est-il mis en mémoire ? Et que change réellement container.no_preload ?
Cet article construit le mécanisme étape par étape, avec un modèle volontairement simple.
Il prolonge deux articles plus anciens du blog :
- Cache Bytecode explique pourquoi PHP transforme le code source en opcodes avant de l’exécuter ;
- Extension OPcache présente l’extension qui conserve ces opcodes pour éviter de les recompiler.
Ici, nous ajoutons les couches manquantes autour d’OPcache : serveur web, PHP-FPM, moteur PHP, autoload Composer, Kernel Symfony, conteneur de services et objets réellement créés.
La chaîne complète, couche par couche
Navigateur
│ requête HTTP
▼
Nginx ou Apache
│ transmet la requête PHP avec FastCGI
▼
PHP-FPM
│ choisit un processus PHP-FPM disponible
▼
Processus PHP-FPM + moteur PHP
│ exécute public/index.php
│
├──────── consulte OPcache
│ pour obtenir du bytecode déjà compilé
│
▼
Autoloader Composer
│ trouve le fichier correspondant à chaque classe demandée
▼
Kernel Symfony
│ traite la requête
▼
Conteneur Symfony
│ construit uniquement les services nécessaires
▼
Objets PHP en mémoire dans le processus PHP-FPM
│ produisent la réponse
▼
Navigateur
Chaque couche a une responsabilité précise :
| Couche | Son rôle | Ce qu’elle ne fait pas |
|---|---|---|
| Nginx ou Apache | Reçoit HTTP, sert les fichiers statiques et transmet PHP | N’exécute pas Symfony |
| PHP-FPM | Maintient les processus PHP et distribue les requêtes | Ne compile pas lui-même le code |
| Moteur PHP | Lit, compile et exécute le PHP | Ne connaît pas la notion de service Symfony |
| OPcache | Partage le bytecode compilé entre les processus PHP-FPM | Ne stocke pas les objets Symfony vivants |
| Autoloader Composer | Associe un nom de classe à son fichier PHP | Ne construit pas les services |
| Kernel Symfony | Pilote le traitement applicatif de la requête | N’est pas OPcache |
| Conteneur Symfony | Sait comment construire les services et leurs dépendances | N’est pas le conteneur Docker |
| Objet service | Exécute la logique métier pendant la requête | N’est pas automatiquement partagé entre tous les processus PHP-FPM |
OPcache se trouve donc au niveau du moteur PHP. Symfony en bénéficie, mais Symfony ne vient pas « lire OPcache » directement : il demande une classe à PHP, et PHP décide d’utiliser le bytecode déjà présent.
Le trajet d’une requête avec PHP-FPM
Dans une architecture classique avec Nginx ou Apache :
Navigateur
↓ requête HTTP
Nginx ou Apache
↓ FastCGI
PHP-FPM
↓ choisit un processus disponible
Processus PHP-FPM
↓ exécute public/index.php
Symfony
↓ construit la réponse
Navigateur
PHP-FPM est un gestionnaire de processus PHP. Il maintient un groupe de processus PHP-FPM et confie chaque requête à un processus disponible :
PHP-FPM
├── processus PHP-FPM 1
├── processus PHP-FPM 2
└── processus PHP-FPM 3
Chaque processus PHP-FPM embarque le moteur PHP. PHP-FPM organise le travail ; le moteur PHP exécute réellement le code.
Pourquoi PHP doit compiler le code
Le processeur n’exécute pas directement le texte d’un fichier PHP. Le moteur PHP commence par le transformer en instructions intermédiaires appelées opcodes, ou bytecode.
Service.php
↓ lecture et analyse
Moteur PHP
↓ compilation
Bytecode PHP
↓ exécution
Résultat
Sans cache, répéter cette compilation à chaque requête gaspillerait du temps.
Le rôle d’OPcache
OPcache conserve le bytecode compilé dans une mémoire partagée. Les processus PHP-FPM peuvent alors réutiliser le même code :
Mémoire OPcache partagée
┌──────────────────────────┐
│ bytecode de Kernel.php │
│ bytecode de Service.php │
│ bytecode de Router.php │
└──────────────────────────┘
▲ ▲ ▲
│ │ │
processus processus processus
PHP-FPM 1 PHP-FPM 2 PHP-FPM 3
Lorsqu’un processus PHP-FPM a besoin d’un fichier PHP :
PHP demande Service.php
│
┌────┴────┐
│ │
présent absent
dans OPcache dans OPcache
│ │
│ └→ lire le fichier → compiler → mettre en cache
│
└──────────────→ exécuter le bytecode
OPcache ne stocke pas les objets métier vivants. Il stocke le code compilé des classes et des fonctions.
Le rôle de Composer dans cette séquence est seulement de retrouver le fichier :
Symfony demande App\Service\ReportService
↓
Composer retrouve src/Service/ReportService.php
↓
PHP demande le code de ce fichier
↓
OPcache fournit le bytecode s'il est déjà disponible
↓
PHP exécute ce bytecode
Trois notions à ne pas confondre
Prenons un service Symfony :
final class ReportService
{
public function generate(): string
{
return 'Rapport prêt';
}
}
Il faut distinguer trois éléments :
OPcache
= code compilé de ReportService
Conteneur de services Symfony
= instructions pour construire ReportService et ses dépendances
Objet ReportService
= instance créée en mémoire quand l'application en a besoin
Le conteneur Symfony ressemble conceptuellement à une usine :
protected function getReportService(): ReportService
{
return new ReportService(
$this->getRepository(),
$this->getLogger(),
);
}
Symfony compile ces instructions dans des fichiers PHP. Ces fichiers peuvent eux aussi bénéficier d’OPcache, puisqu’ils restent du code PHP. Mais OPcache ne contient pas le service déjà construit : l’objet est créé ensuite, dans la mémoire du processus PHP-FPM qui traite la requête.
Bytecode disponible dans OPcache
↓
Le conteneur Symfony exécute new ReportService(...)
↓
Objet créé dans la mémoire du processus PHP-FPM
↓
La méthode generate() est appelée
OPcache et preload ne sont pas la même chose
OPcache fonctionne déjà sans preload. Lorsqu’une classe est rencontrée pour la première fois, PHP la compile puis garde son bytecode en cache pour les requêtes suivantes.
Le preload avance cette opération au démarrage du serveur :
OPcache sans preload
--------------------
Démarrage PHP
↓
Première requête utilisant ReportService
↓
Compilation puis stockage dans OPcache
↓
Requêtes suivantes plus rapides
OPcache avec preload
--------------------
Démarrage PHP
↓
Compilation immédiate de ReportService
↓
Stockage dans OPcache
↓
Première requête déjà prête
Le preload améliore donc surtout le premier chargement, au prix d’une consommation mémoire imposée dès le démarrage.
Il existe cependant une différence importante : une classe préchargée reste disponible jusqu’à l’arrêt du processus serveur. Un script simplement ajouté au cache lors d’une requête suit le cycle normal d’OPcache, qui peut invalider le script ou redémarrer son cache selon sa configuration. Le preload réserve donc de la mémoire de façon beaucoup plus rigide.
Comment Symfony prépare le preload
Lors de la compilation du conteneur, Symfony génère un fichier recensant les classes à précharger. Le nom exact dépend de la version et de l’environnement, mais il ressemble à ceci :
var/cache/prod/App_KernelProdContainer.preload.php
Le fichier config/preload.php, installé par Symfony Flex, charge ensuite cette liste. PHP l’exécute au démarrage lorsque php.ini contient :
opcache.preload=/app/config/preload.php
opcache.preload_user=www-data
La chaîne complète devient :
AU BUILD OU AU WARMUP
Configuration Symfony
↓
Compilation du conteneur de services
↓
Génération du fichier *.preload.php
AU DÉMARRAGE DU SERVEUR PHP
PHP lit opcache.preload dans php.ini
↓
PHP exécute config/preload.php
↓
Le fichier Symfony charge les classes sélectionnées
↓
PHP compile leur code
↓
Le bytecode entre dans OPcache
Symfony choisit la liste ; le moteur PHP réalise la compilation et remplit OPcache.
Pourquoi le démarrage peut échouer
La directive suivante fixe la taille de la mémoire partagée d’OPcache :
opcache.memory_consumption=256
La valeur est exprimée en mégaoctets. Si les classes préchargées ne tiennent pas dans cet espace, PHP peut échouer avant même de servir une requête :
Classes à précharger
↓
Compilation progressive
↓
┌──────────────────────────┐
│ OPcache : 256 Mo │
│ ████████████████████████ │
│ plus aucune place │
└──────────────────────────┘
↓
Not enough shared memory for preloading
↓
Le serveur PHP ne démarre pas
Augmenter la mémoire peut débloquer le démarrage, mais ce n’est pas toujours la meilleure première correction. Une application peut précharger des commandes d’administration, des traitements asynchrones ou des clients d’API rarement utilisés.
Le rôle de container.no_preload
Symfony permet d’exclure une classe du preload sans désactiver son service :
services:
App\Service\MonthlyReportService:
tags:
- container.no_preload
Le résultat est important :
container.no_preload
≠ supprimer le service
≠ interdire OPcache
≠ désactiver la fonctionnalité
container.no_preload
= ne pas compiler obligatoirement cette classe au démarrage
Au premier appel, le fonctionnement normal reprend :
Démarrage
↓
MonthlyReportService absent du preload
↓
Première utilisation
↓
PHP charge et compile la classe
↓
Le bytecode entre dans OPcache
↓
Utilisations suivantes servies depuis OPcache
Le tag reporte donc le coût à la première utilisation. Il ne prive pas les appels suivants du cache.
Cela suppose évidemment qu’il reste de la place dans le cache normal. container.no_preload libère la partie occupée obligatoirement au démarrage ; ce n’est pas une solution magique si l’ensemble du code finit ensuite par saturer OPcache.
Quand tout un fichier de configuration contient des services froids, le tag peut être appliqué avec _defaults :
services:
_defaults:
autowire: true
autoconfigure: true
tags:
- container.no_preload
Il faut rester prudent : Symfony propage aussi container.no_preload dans certains graphes de dépendances. La bonne méthode consiste à mesurer le fichier généré, appliquer le tag à une famille cohérente, reconstruire le cache puis comparer.
Quels services exclure en premier ?
Les meilleurs candidats sont les services rarement utilisés pendant une requête web normale :
- commandes de maintenance ;
- écrans d’administration peu fréquentés ;
- génération de rapports ;
- imports et exports ;
- traitements Messenger ;
- clients d’API lourds utilisés ponctuellement ;
- outils de contrôle qualité ou de réindexation.
Les services utilisés sur presque chaque requête sont de moins bons candidats : routeur, sécurité, rendu Twig, écouteurs globaux ou services nécessaires à la page d’accueil.
Le poids du fichier source n’est pas le seul critère. Une petite classe peut entraîner de nombreuses interfaces, classes parentes et dépendances dans le graphe de preload.
Diagnostiquer avant de modifier
Afficher la configuration active :
php --ini
php -i | grep -E 'opcache.enable|opcache.memory_consumption|opcache.preload'
Observer l’état d’OPcache depuis la même SAPI que le serveur web :
<?php
$status = opcache_get_status(false);
var_dump($status['memory_usage'] ?? null);
Attention : le CLI et le serveur web n’utilisent généralement pas la même instance OPcache. Une commande lancée dans un terminal ne décrit donc pas forcément la mémoire du serveur.
Comparer deux fichiers de preload :
grep -oE "/[^']+\.php" preload-before.php | sort > before.txt
grep -oE "/[^']+\.php" preload-after.php | sort > after.txt
comm -13 before.txt after.txt
Cette comparaison permet d’identifier les classes réellement ajoutées avant d’augmenter la mémoire ou d’exclure des services au hasard.
Et avec FrankenPHP ?
FrankenPHP remplace le couple serveur web traditionnel plus PHP-FPM par un serveur d’applications unique construit sur Caddy :
Architecture PHP-FPM
Navigateur → Nginx/Apache → PHP-FPM → moteur PHP → Symfony
Architecture FrankenPHP
Navigateur → FrankenPHP/Caddy → moteur PHP → Symfony
Il n’y a donc plus de processus PHP-FPM dans l’architecture FrankenPHP. Le moteur PHP et OPcache restent toutefois des notions utiles : FrankenPHP utilise l’interpréteur PHP officiel.
En mode classique, l’application Symfony redémarre logiquement à chaque requête. En mode Worker, Symfony démarre une seule fois et son Kernel reste en mémoire :
FrankenPHP classique
requête 1 → boot Symfony → réponse → nettoyage
requête 2 → boot Symfony → réponse → nettoyage
FrankenPHP Worker
boot Symfony une fois
├── requête 1
├── requête 2
└── requête 3
Le Worker va plus loin qu’OPcache : il ne conserve pas seulement du code compilé, mais aussi l’application démarrée et certains objets entre les requêtes. C’est plus rapide, avec une contrainte supplémentaire : aucun état propre à un utilisateur ne doit survivre dans un service partagé.
Le modèle mental à retenir
OPcache
= bibliothèque de code PHP déjà compilé
Preload
= remplissage anticipé de cette bibliothèque au démarrage
Conteneur Symfony
= usine qui sait construire les services
Service Symfony
= objet construit par cette usine quand il est demandé
PHP-FPM
= gestionnaire des processus PHP dans une architecture classique
FrankenPHP
= serveur d'applications qui remplace serveur web + PHP-FPM
Conclusion
OPcache, preload et conteneur Symfony travaillent ensemble, mais ne stockent pas la même chose. OPcache conserve le bytecode. Le preload décide de charger certaines classes dès le démarrage. Le conteneur Symfony décrit comment construire les services. Enfin, les objets sont créés lorsque l’application en a besoin.
container.no_preload est donc un outil de sélection, pas un interrupteur fonctionnel : le service reste disponible et son code pourra toujours rejoindre OPcache à sa première utilisation. Cette distinction permet de réduire un preload trop ambitieux sans sacrifier les fonctionnalités de l’application.
