OPcache sur hébergement mutualisé : pourquoi vos corrections ne s'affichent pas pendant 5 minutes après le déploiement
Après rsync les fichiers PHP sur le serveur sont neufs, mais le navigateur sert l'ancienne version. Cause : OPcache sur mutualisé. Correction via .user.ini en 5 minutes.
C’est l’histoire d’un jour de débuggage qui aurait pu prendre 5 minutes - si j’avais connu plus tôt .user.ini et opcache.validate_timestamps. Resté assis deux heures, je ne comprenais pas pourquoi mes modifs PHP ne s’affichaient pas sur le site. Le fichier en SSH - neuf. Dans le navigateur - vieux. Re-rsync - pareil. Vidé le CDN Beget - n’a pas aidé.
La cause c’est OPcache sur hébergement mutualisé. Voici comment l’attraper et comment le corriger, pour que vous ne perdiez pas d’heures.
En bref
- Sur mutualisé OPcache par défaut ne vérifie pas le mtime - il sert l’ancien bytecode pendant 5-10 minutes après déploiement.
- Symptôme : rsync est passé, le fichier sur le serveur est frais selon
ls -la, mais dans le navigateur le code est vieux.- Correction :
.user.inià la racine du site avecopcache.validate_timestamps=1etopcache.revalidate_freq=0.- Particularité : le premier
.user.iniest pris en compte après 300 secondes (TTL user_ini). Ensuite tout en temps réel.
Comment se manifeste le bug
Je travaillais sur une correction du template des pages métro. J’ai écrit une fonction PHP render_metro_features(), je l’ai poussée par rsync sur l’hébergement mutualisé Beget. J’ai ouvert https://example.ru/uborka-metro-pushkinskaya/ - je vois l’ancienne version. Je pense - cache navigateur. Hard reload (Ctrl+Shift+R), onglet incognito. Pareil.
J’entre en SSH, je fais cat ~/example.ru/public_html/metro.php | grep render_metro_features. La fonction est là, nouvelle version. Donc sur disque le fichier est frais.
Je fais php -l metro.php - pas d’erreurs de syntaxe.
Je fais une requête curl directe au serveur, sans navigateur ni CDN :
curl -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://example.ru/uborka-metro-pushkinskaya/
Renvoie l’ancienne version du HTML. Donc ce n’est ni le navigateur ni le CDN. Embêter le support Beget ‘chez vous quelque chose est cassé’ n’est pas une option - les autres sites sur leur hébergement marchent.
Je suis allé dans phpinfo() via une page de service. Et j’ai vu : opcache.enable On, opcache.validate_timestamps Off, opcache.revalidate_freq 300. Voilà.
OPcache en un paragraphe
Quand vous ouvrez une page PHP, le serveur fait trois choses : il lit le source depuis le disque, parse en opcodes (représentation interne), exécute les opcodes. Le parsing est l’étape la plus chère, jusqu’à 50-70 % du temps de requête. OPcache stocke les opcodes déjà parsés dans la shared memory du serveur. La requête suivante au même fichier saute le parsing - prend les opcodes en mémoire et exécute immédiatement.
Sur mutualisé OPcache est toujours actif. Ça permet au fournisseur de caser trois à cinq fois plus de clients sur un serveur. Inconvénient - un cache invisible côté serveur qu’il faut prendre en compte après chaque déploiement.
Pourquoi le mutualisé ne reset pas le cache tout seul
Il y a deux modes. Premier - opcache.validate_timestamps=1. PHP fait un stat() sur le source à chaque requête, compare le mtime avec celui en cache. Si le fichier a changé - relit et met à jour le cache. Inconvénient - une vérification disque en plus à chaque requête (100 microsecondes sur un site moyen).
Second - opcache.validate_timestamps=0. Aucune vérification. Le cache vit jusqu’à un opcache_reset() explicite ou un redémarrage PHP-FPM. C’est le mode performance max, utilisé sur les services de production sérieux. Inconvénient - il faut reset le cache à la main après déploiement.
Sur mutualisé, opcache_reset() via PHP-CLI n’est pas disponible (souvent dans les fonctions désactivées). Vous n’avez pas accès au redémarrage PHP-FPM. Donc Beget et autres hébergeurs mutualisés font un compromis : validate_timestamps=0, mais revalidate_freq=60-300. Le cache vérifie le mtime non à chaque requête mais une fois toutes les 1-5 minutes. Le code frais est pris en compte dans cette fenêtre 1-5 minutes.
Si pendant cette fenêtre vous déployez et testez une correction - elle est ‘invisible’. Cinq minutes plus tard - elle apparaît subitement. À ce moment-là vous avez déjà poussé la deuxième correction. Et la confusion commence : qu’est-ce qui vient d’où.
Fix via .user.ini
À la racine du site (là où se trouve index.php) vous posez un fichier .user.ini avec deux lignes :
opcache.validate_timestamps=1
opcache.revalidate_freq=0
Ce que ça fait. validate_timestamps=1 - OPcache vérifie le mtime du fichier à chaque requête. revalidate_freq=0 - vraiment à chaque requête, pas moins. Ces deux paramètres dans .user.ini sont supportés (ils ont le scope PHP_INI_PERDIR dans la doc). Globalement sur mutualisé on ne peut pas les changer - mais localement pour son répertoire, si.
Le piège c’est le TTL de .user.ini lui-même. PHP relit ce fichier non à chaque requête mais une fois toutes les 300 secondes par défaut. C’est contrôlé par la config globale user_ini.cache_ttl, qu’on ne peut pas changer. Donc après avoir posé .user.ini sur le serveur pour la première fois, il faut attendre jusqu’à 5 minutes pour que PHP le prenne en compte. Après ce moment - tout en temps réel, aucun délai.
Vérification que c’est pris en compte - ouvrir un script PHP de test avec phpinfo(). Vous devez voir opcache.validate_timestamps Local Value: On. Si encore Off - attendez, ou bien forcez PHP à relire les ini en visitant le site toutes les 30 secondes.
Quoi mettre d’autre dans .user.ini
Puisque vous posez ce fichier - ajoutez d’autres réglages utiles. Minimum pour le backend d’un site :
; OPcache voit les modifs tout de suite
opcache.validate_timestamps=1
opcache.revalidate_freq=0
; Limites d'upload (si formulaires avec photos)
upload_max_filesize=20M
post_max_size=22M
max_input_vars=3000
; Fuseau horaire
date.timezone="Europe/Moscow"
; Sessions - où écrire
session.cookie_httponly=1
session.cookie_samesite="Lax"
Seuls les réglages avec scope PHP_INI_PERDIR ou PHP_INI_USER fonctionneront. Par exemple, memory_limit dans .user.ini est ignoré par Beget - dans le cas général c’est PHP_INI_ALL, mais dans la config mutualisée le fournisseur l’a bloqué. Si vous devez l’augmenter - contactez le support.
Quand .user.ini n’aide pas
Il arrive que vous mettez validate_timestamps=1, 10 minutes passent, le code est toujours vieux. Alors les causes possibles :
- Beget CDN garde une copie du HTML en edge. Si la page n’est pas unique par cookie/paramètre, elle est cachée. Vider via le panneau Beget, section CDN, bouton ‘Vider le cache’. Ou couper le CDN temporairement pendant une demi-heure, voir.
- Le navigateur a caché le HTML. Ouvrez en incognito ou via curl sans en-têtes.
- Vous éditez le mauvais fichier. Il arrive sur mutualisé qu’il y ait un domaine alias - le dossier
~/example.ru/existe physiquement, mais Nginx sert le domaine depuis un autre dossier. Vérification vials -la /var/www/<user>/data/www/, généralement un lien symbolique là. J’ai eu cette histoire - perdu une journée entière. opcache_invalidate()via PHP-CLI est bloqué. Si vous essayez de reset le cache depuis un script cron - silencieusement il ne marche pas. Sur mutualisé cette fonction est généralement dansdisable_functions. Ne reste que le réglage via ini.
Alternative - fichiers PHP versionnés
En dernier recours, si .user.ini n’aide pas à cause des règles du fournisseur, il y a un workaround. Déployer les nouvelles versions PHP avec une version dans le nom de fichier :
metro.php → metro_v2.php
Et dans .htaccess réécrire le routing :
RewriteRule ^uborka-metro-(.+)/?$ metro_v2.php?slug=$1 [L,QSA]
L’ancien metro.php reste dans OPcache, mais personne ne s’y adresse. Les nouvelles requêtes vont sur metro_v2.php, qui n’est pas encore en cache - PHP le parse à neuf. Cinq minutes plus tard OPcache l’attrape aussi, mais à ce moment vous voyez déjà le code frais.
C’est grossier et ne passe pas à l’échelle, mais ça dépanne le moment, le temps de régler les paramètres.
Checklist après déploiement sur mutualisé
- Tout de suite après rsync - ouvrir une page test via curl avec en-têtes
no-cache. Vous voyez la vieille version - c’est OPcache, direction.user.ini. Vous voyez la nouvelle - bien, ensuite vérifiez via navigateur. - Vérifier
phpinfo()une fois par hébergement - connaître les valeurs deopcache.validate_timestampsetopcache.revalidate_freq. Elles diffèrent chez Beget, Reg.ru, Timeweb. - Poser
.user.initout de suite après le premier déploiement. Attendre 5 minutes pour la prise en compte. Ensuite toutes les modifs visibles immédiatement. - Le CDN est une couche de cache séparée. Si vous utilisez Beget CDN ou Cloudflare, après des modifs majeures allez dans leur panneau vider le cache à la main. Ou mettez
Cache-Control: max-age=300dans les en-têtes pour une invalidation rapide.
Le cas complet sur le lancement d’un site fait maison sur mutualisé avec PHP - dans l’article ‘50 jours de SEO en B2B nettoyage’. OPcache c’est un des huit pièges d’hébergement mutualisé sur lesquels j’ai trébuché les premières semaines.
À lire aussi : CLS 0,377 → 0,002 en une journée - une autre histoire de corrections rapides avec gros impact.