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 avec opcache.validate_timestamps=1 et opcache.revalidate_freq=0.
  • Particularité : le premier .user.ini est 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 via ls -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 dans disable_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é

  1. 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.
  2. Vérifier phpinfo() une fois par hébergement - connaître les valeurs de opcache.validate_timestamps et opcache.revalidate_freq. Elles diffèrent chez Beget, Reg.ru, Timeweb.
  3. Poser .user.ini tout de suite après le premier déploiement. Attendre 5 minutes pour la prise en compte. Ensuite toutes les modifs visibles immédiatement.
  4. 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=300 dans 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.

Questions fréquentes

C'est quoi OPcache et pourquoi il est sur mutualisé ?
OPcache c'est un cache intégré de bytecode PHP compilé. Quand vous ouvrez une page, PHP parse la source en opcodes (représentation interne), puis exécute. OPcache stocke les opcodes déjà parsés dans la shared memory du serveur : la requête suivante saute l'étape de parsing. Ça économise 30-70 % de temps CPU par requête. Sur les hébergements mutualisés OPcache est généralement activé par défaut, ce qui permet au fournisseur de caser plus de clients sur un même serveur.
Pourquoi le mutualisé ne prend pas mes modifications tout de suite ?
Par défaut OPcache sur mutualisé est configuré avec `validate_timestamps=0` - il ne vérifie pas si le fichier a changé sur disque. Ça donne le max de performance, mais demande un reset manuel après déploiement. Sur mutualisé vous n'avez pas accès à `opcache_reset()` en CLI ni au redémarrage PHP-FPM. Habituellement `revalidate_freq=60` ou `300` est configuré - le cache se revérifie tout seul toutes les 1-5 minutes. Avant ce moment, le serveur sert le vieux code.
En quoi `.user.ini` diffère de `php.ini` ?
`php.ini` c'est la config globale du serveur, intouchable sur mutualisé. `.user.ini` c'est une config locale au répertoire, que vous pouvez poser à la racine de votre site. PHP la relit automatiquement avec un TTL par défaut de 300 secondes (contrôlé par `user_ini.cache_ttl` dans php.ini). Dans `.user.ini` un sous-ensemble de réglages est supporté - ceux avec `PHP_INI_PERDIR` ou `PHP_INI_USER` dans la doc. Pour OPcache marchent `opcache.validate_timestamps`, `opcache.revalidate_freq`, `opcache.enable`.
La vitesse du site va-t-elle baisser avec `validate_timestamps=1` ?
Elle baisse, mais peu. OPcache fait un `stat()` sur le fichier source à chaque requête et compare le mtime avec celui en cache. Sur SSD c'est quelques microsecondes par fichier. Sur un site de 50 fichiers PHP l'overhead est 100-200 microsecondes. Face à une requête SQL (5-20 ms) et à la latence réseau (50-200 ms) c'est invisible. L'argument contre `validate_timestamps=0` sur mutualisé : vous perdez des heures à débugger 'pourquoi mes modifications ne marchent pas'.
Quels autres caches en dehors d'OPcache empêchent de voir le code frais ?
Plusieurs couches. Cache navigateur via `Cache-Control` et `Expires` - généralement quelques heures sur HTML, jusqu'à un an sur CSS/JS avec versions. Cache CDN - Beget CDN ou Cloudflare gardent une copie en edge de 5 minutes à 7 jours. Cache Nginx sur le mutualisé - généralement jusqu'à 5 minutes. Sessions cookies - si la page est personnelle, ne confondez pas cache et session périmée. URLs versionnées comme `/css/main.css?v=20260511` gèrent les modifs CSS/JS ; le fix OPcache de cet article gère les modifs PHP.