Aller au contenu

Cache

Un cache devant les origines ne décide rien : il ne bloque pas, il ne note pas, il ne défie personne. Il garde une réponse déjà produite et la rend plus vite, pendant que le reste de la chaîne continue de travailler. C'est pour cela qu'il est posé derrière le WAF et derrière le décideur, jamais devant.

Pourquoi un opérateur, et pas un Deployment dans un manifeste

Un moteur de cache n'est pas un pot de yaourt : sa politique est un fichier, ce fichier change, et ce changement doit être appliqué sans perdre la mémoire chaude. Écrit à la main, un Deployment Varnish impose de redémarrer le pod à chaque retouche de durée de vie, donc de jeter tout ce qui était en cache au moment précis où l'on voulait l'améliorer.

L'opérateur prend la politique comme une ressource, rend le VCL, le publie, et laisse le moteur recharger à chaud. Il publie aussi ce que le moteur a réellement servi, parce qu'une console qui affiche un taux de succès inventé ne sert à rien.

Deux ressources, deux rôles

VarnishCache est le cache : une image, une mémoire, une politique. Il vit sa vie et peut servir plusieurs sites.

EdgeSite est le site. Il dit seulement s'il passe par un cache, et lequel :

spec:
  cache:
    enabled: true
    name: edge-cache

Tant que le cache n'est pas prêt, la route du site reste directe. C'est volontaire : un site ne doit pas tomber parce qu'un cache démarre. La bascule se fait quand le cache dit qu'il sert, et le site repasse en direct si on lui retire son cache.

La vérité est croisée : le cache nomme ses sites, le site nomme son cache. Si les deux ne se répondent pas, rien ne se monte. Une faute de frappe dans une liste ne peut donc pas envoyer un domaine vers les origines d'un autre.

La politique

apiVersion: edge.exoledge.io/v1alpha1
kind: VarnishCache
metadata:
  name: edge-cache
  namespace: edge
spec:
  image: docker.io/library/varnish:7.6
  memoryMiB: 192
  ttlSeconds: 60
  graceSeconds: 30
  sites:
    - name: demo
  rules:
    - prefix: /app
      ttlSeconds: 0
    - prefix: /assets
      ttlSeconds: 3600
      stripCookies: true
  purgeSecretRef:
    name: cache-purge
    key: token

Les routes viennent des EdgeSite nommés, avec leurs poids : le cache choisit l'origine exactement comme le bord l'aurait fait, et une origine à poids zéro ne reçoit rien, pas même du cache.

Les règles disent ce qui a le droit d'être gardé. Une règle à durée nulle n'est pas un oubli : c'est un refus de stocker, et la requête part directement. Le préfixe le plus long gagne.

stripCookies retire le Set-Cookie avant de stocker. Sans lui, une réponse qui pose un cookie n'est jamais partageable, ce qui est le comportement par défaut et le bon : une page qui dépend d'une session n'a rien à faire dans un cache partagé.

Sans stripCookies, toute requête portant un cookie traverse sans être stockée. Une requête authentifiée aussi, toujours.

Qui en profite vraiment

Une requete qui porte un cookie n'est pas stockee, sauf si la regle du prefixe demande stripCookies. Comme le navigateur qui a franchi le defi porte sa clearance, le cache sert surtout les clients sans cookie : moteurs de recherche verifies, appels d'API, ressources statiques declarees partageables.

C'est un choix, pas un oubli : un cache qui stocke les reponses d'une session finit par servir la page d'un visiteur a un autre. La consequence est a connaitre avant de promettre un taux de cache eleve : il porte sur la part du trafic qui n'a pas de session, et cette part doit etre mesuree, pas supposee.

Preuve de fonctionnement, depuis le cluster, sans passer par le defi :

1. requete  : X-Cache: MISS
2. requete  : X-Cache: HIT
3. purge /  : varnishadm ban.list montre req.url == "/"
4. requete  : X-Cache: MISS

Ce que le moteur ne stocke jamais

  • une réponse d'erreur d'origine (5xx), ou partielle (206) ;
  • une réponse que l'origine interdit de stocker (no-store, private) : l'origine a le dernier mot ;
  • une méthode autre que GET et HEAD.

Purge

La purge est la seule écriture acceptée par le cache, et elle exige un jeton :

curl -X PURGE -H "X-Purge-Token: <jeton>" https://demo.guillaume.ink/une-page

Sans jeton configuré, la purge répond 405 : fermée. Avec un jeton et le mauvais en-tête, 403. Un cache qu'un inconnu peut vider à distance est un moyen de faire tomber le site par un simple en-tête.

Le jeton est généré par OpenTofu, jamais écrit dans un manifeste :

kubectl -n edge get secret cache-purge -o jsonpath='{.data.token}' | base64 -d

Le CRS n'autorisait que GET, HEAD, POST et OPTIONS : PURGE était refusé en 403 par le WAF avant d'atteindre le cache. La méthode est désormais dans tx.allowed_methods, et c'est le cache qui décide, pas le pare-feu.

Rechargement à chaud

varnishd ne relit pas son VCL de lui-même. Le conteneur démarre donc le moteur, puis surveille l'empreinte du fichier : quand elle change, il charge la nouvelle politique et l'active, sans redémarrer. Vérifiable dans le conteneur :

varnishadm -n /var/lib/varnish vcl.list
available   auto   warm   0   boot
active      auto   warm   2   cfg_1

Une politique modifiée prend effet en une minute environ : le temps que le ConfigMap propage le fichier, puis cinq secondes de surveillance. Le pod ne change pas de nom, la mémoire chaude est conservée.

L'empreinte de la politique est publiée dans status.vclHash, et l'opérateur ne la pose pas sur le modèle de pod : une annotation sur le pod ferait redémarrer le cache à chaque changement de politique, ce qui est exactement ce qu'on veut éviter.

Ce que le cache raconte

Chaque réponse porte son verdict, sans outil :

x-cache: HIT

et le compteur est publié dans status : hits, misses, objects, hitRatio. La console les lit dans l'écran Répartition de charge, avec la mémoire, la durée de vie et l'empreinte de la politique.

Ces chiffres viennent du moteur lui-même, lus par varnishstat dans le pod. Le cache n'expose pas de port de statistiques : ce serait une porte de plus au bord pour un chiffre.

Origin tombee : ce que voit le visiteur

Quand plus aucune origine ne repond, Varnish fabrique lui-meme la reponse. Par defaut il sert sa page d'erreur : « Backend fetch failed », « Guru Meditation », un XID, et le nom du logiciel en clair. Trois problemes d'un coup : le visiteur lit un message technique, la marque du client disparait derriere celle du cache, et la pile est annoncee a qui sait la lire.

Le VCL rendu definit donc vcl_synth et vcl_backend_error : un 503 avec Retry-After: 60, Cache-Control: no-store et une page autonome, sans police distante ni script. Elle s'affiche pendant une panne reseau, pas seulement pendant une panne de site.

Le 502 reste volontairement intouche : il signifie que l'origine a repondu quelque chose d'invalide, et le masquer derriere une page de maintenance ferait perdre le seul signal utile a l'exploitant.

La page est une chaine longue VCL (synthetic({"..."})). Elle ne doit contenir aucun guillemet double : il fermerait la chaine au milieu du document et la compilation echouerait en silence, le cache gardant l'ancien VCL. Un test fige cette regle.

Limites assumées

  • Un seul réplica, et c'est un choix. Deux caches devant les mêmes origines divisent le taux de succès, chacun ayant sa propre mémoire froide. Le cache n'est pas la haute disponibilité, il est l'accélération ; la passerelle reste le point unique.
  • Mémoire seule. Le stockage est dans la RAM allouée, pas sur disque : un redémarrage vide le cache. C'est acceptable pour un cache, pas pour un magasin.
  • Pas de purge par étiquette. On purge une URL, pas un ensemble.
  • Pas de cache pour les réponses qui varient. Pas de Vary négocié, pas d'ESI : ce sont des fonctionnalités à ajouter consciemment, pas des réglages à exposer.
  • Le cache ne filtre pas. Il ne remplace ni le WAF ni le décideur, et il ne doit jamais être placé devant eux : il servirait alors une réponse bloquée, ou l'inverse.