Cache par invalidation avec Symfony

Le cache par invalidation Késako ?

Le cache par invalidation est une technique qui permet de contrôler la mise à jour du cache en fonction des changements de contenus.

On utilise le terme “cache par invalidation” par opposition au “cache par expiration” qui consiste à mettre une durée de vie (Time To Live) sur les réponses.

Un double objectif

La mise en place de ce mécanisme va permettre au serveur d’origine de servir un nouveau contenu une seule fois, chaque demande suivante bénéficiera du cache. On remplit ainsi deux objectifs :

  • Diminuer le trafic des serveurs à l’origine
  • Augmenter la fraîcheur des données reçues par les utilisateurs

How to

Préquis :

  • Un projet Symfony >= 2.8
  • Varnish

Nous avons utilisé le bundle FOSHttpCache, rapidement installé avec la documentation. Ce bundle permet de gérer les entêtes Cache-Control, différentes méthodes d’invalidation de cache : PURGE, REFRESH, BAN. Ici nous n’utiliserons que la méthode BAN.

La configuration

On installe le bundle

Config bundle

Fos_http_cache:
   # On active les tags, fonctionnalité dont nous aurons besoin
   tags:
       enabled: true
   proxy_client:
       Varnish:
           Http:
        # La liste des serveurs varnish
               servers:
                   - http://awesome-varnish

 

Configuration Varnish

sub vcl_recv {
   if (req.method == "BAN") {
       if (!client.ip ~ purge) { # Un filtre ip pour éviter toute purge non autorisée
           return (synth(405, "Not allowed"));
       }

       if (req.http.X-Cache-Tags) {
           ban("obj.http.X-Cache-Tags ~ " + req.http.X-Cache-Tags
           );
       }

       return (synth(200, "Banned"));
   }
}

sub vcl_deliver {
# On cache ces infos au public
   unset resp.http.X_Bubble_App;
   unset resp.http.X-Cache-Tags;
}

 

Implémentation des tags sur les réponses

Attention à partir du moment ou vous taguez vos pages en fonction des ressources utilisées, il est important de le faire pour chaque ressource sans en oublier une seule, si ce n’est pas le cas, une modification sur une ressource non taguée n'entraînera pas la mise à jour de la page !

Par exemple, la page d’accueil du site du Tour de France requiert tout ces tags :

X-Cache-Tags:
sites,page-1,flashinfo-fr,menu-menu-fr,stages-millesime-11819,team-image-jersey,teams,rankingTypes-millesime-11819,rankingTypes-ite-millesime-11819,rankingTypes-ije-millesime-11819,menu-top-fr,socialBlock-menu-fr,slider-fr,news-1276049,image-11439,news-1275985,image-11128,video-3717,image-11129,news-1275991,image-11130,livenews-fr,news-liv-fr,news-int-fr,news-flm-fr,millesime-11819,pushCollection-2,news-fr,social-twitter-fr,videoGalleries-fr,push-42,push-447,push-449,competitor-354446,competitor-image-profil-354446,competitor-354549,competitor-image-profil-354549,competitor-354539,competitor-image-profil-354539,competitor-354464,competitor-image-profil-354464,competitor-354509,team-image-jersey-49159,competitor-354529,competitor-image-profil-354529,millesime-image-jersey-11818,millesime-image-sponsor-11818,lastMillesimeWithCompetitor,socialBlock-5,partnerBlock-fr-default,menu-pre_footer-fr,menu-footer-fr

 

Le bundle expose un service fos_http_cache.http.symfony_response_tagger qui vous permettra d’ajouter des tags très simplement.

Un exemple simple dans un controller qui permettrait l’affichage d’une actualité :

$news = $this->newRepository->find($id);
$this->responseTagger->addTags(["news-{$news>getId()}"]);

 

Note : Si vous utilisez des services pour cloisonner la récupération des données avant de les envoyer à la vue, injecter le service symfony_response_tagger et ajoutez les tags associées aux données que vous manipulez, en regroupant la gestion des tags et la récupération des données liées, vous évitez de perdre le fil.

Implémentation de l’invalidation

Le bundle expose un service fos_http_cache.cache_manager qui vous permettra d’invalider les contenus en fonctions des tags avec la méthode invalidateTags.

Plusieurs solutions s’offrent à vous :

  • Utiliser ce service dans un EventListener Doctrine pour invalider les tags liés aux données modifiées.
  • Utiliser ce service dans vos controllers ou services qui gèrent les modifications des données.
  • Si vous utilisez Sonata, dans les fonctions postPersist/postUpdate des classes Admins.

Plusieurs cas a considérer :

Vos tags doivent prendre en compte TOUTES les ressources utilisées pour afficher la page :

  • Les ressources associées à un identifiant unique peuvent utiliser des tags "simples". Un tag de la forme : <type>-<identifiant> serait suffisant.

Exemple : pour la news 125 : news-125

  • Les ensembles de ressources identifiées par un paramètre particulier devraient utiliser un format plus adapté, contenant ce paramètre. Un tag de la forme : <type>-<paramètre>-<valeur> est le plus approprié.

Exemple : pour la liste des news de l'année 2019 : news-year-2019

  • Les ressources futures. Si vous affichez une liste d’actualité en fonction de leur date de publication, il faut prendre en compte la prochaine actualité dont la date de publication est dans le futur. La date de publication de cette prochaine actualité sera la date à laquelle votre réponse devra expirer.

Exemple: Au premier février, j’affiche 2 actualités, mais j’ai une actualité N°3 à afficher le 4 février, ma réponse aura donc une durée de vie maximale de 3 jours, à ce moment une nouvelle réponse sera demandée qui affichera cette actualité N°3.

  • Les ressources "communes", utilisées sur chaque page du site (header, footer, social bar, langues, ...) nécessitent de purger tout le site à chaque changement. Pour gérer ces cas, le plus simple est d’ajouter un tag all, sur chacune des pages et de purger ce tags si une de ces ressources était modifiée.

Quelques conseils

  • Commencer petit puis améliorer : Rien ne vous empêche de commencer sans tag et de purger votre site à chaque modification de contenu avec un listener global sur les évènements Doctrine (ou autre selon votre situation). Vous pouvez ensuite ajouter la gestion des tags en commençant par les ressources les plus fréquemment mises à jour.
  • Les entêtes de réponse Http ont une limite de taille, veillez à ne pas les dépasser avec un nombre de tag trop important.

Apache et Varnish possèdent une limite par défaut de 8Ko par l’ensemble des headers d’une réponse http, pour cet exemple on atteint 1.5Ko de données, il reste donc de la marge, mais à ne pas négliger :

Cache-Control: max-age=61, public, s-maxage=23
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=UTF-8
Date: Mon, 21 Jan 2019 08:24:16 GMT
Edge-control: cache-maxage=60s, downstream-ttl=5s public
ETag: "e6220c321f3c835ef96eafd579ff6b53-gzip"
Keep-Alive: timeout=60, max=1000
Server: Apache/2.4.25 (Debian)
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-App-Bubble: xxxxxxxxxxx
X-App-Reverse-Proxy-TTL: 2592000
X-App-Reverse-Proxy-TTL-Debug: 2019/02/20 09:24:17
X-Cache-Tags: sites,page-1,flashinfo-fr,menu-menu-fr,stages-millesime-11819,team-image-jersey,teams,rankingTypes-millesime-11819,rankingTypes-ite-millesime-11819,rankingTypes-ije-millesime-11819,menu-top-fr,partnerBlock-fr-default,socialBlock-menu-fr,slider-fr,news-1276049,image-11439,news-1275985,image-11128,video-3717,image-11129,news-1275991,image-11130,livenews-fr,news-liv-fr,news-int-fr,news-flm-fr,millesime-11819,pushCollection-2,news-fr,social-twitter-fr,videoGalleries-fr,push-42,push-447,push-449,competitor-354446,competitor-image-profil-354446,competitor-354549,competitor-image-profil-354549,competitor-354539,competitor-image-profil-354539,competitor-354464,competitor-image-profil-354464,competitor-354509,team-image-jersey-49159,competitor-354529,competitor-image-profil-354529,millesime-image-jersey-11818,millesime-image-sponsor-11818,lastMillesimeWithCompetitor,socialBlock-5,menu-pre_footer-fr,menu-footer-fr
X-Release: 145_5d5351a4681c1a6f30a6f0765ed3ead3a377a651

 

Qu’est ce que ça donne ?

 

Graphique : Requêtes aux serveurs d’origine par minutes

 

Actions :

  • Premier trait bleu : Remplacement du cache par invalidation par un cache par expiration de 60 secondes et purge des serveurs varnish.
  • Deuxième trait bleu : Réactivation du cache par invalidation et purge des serveurs varnish.
  • Première zone rouge : Crawl du site N°1
  • Deuxième zone rouge: Crawl du site N°2

Observations :

  • A chaque purge des serveurs varnish, on observe un pic de requêtes conséquence directe de l’invalidation du cache.
  • Entre la désactivation et la réactivation du cache par invalidation, on constate une hausse du nombre de requêtes.
  • Après la réactivation du cache par invalidation, le nombre de requêtes descend peu à peu au fur et à mesure que les requêtes sont mises en cache.
  • Au premier crawl, on voit la hausse importante du nombre de requêtes, malgré que le cache par invalidation soit activé, très peu de pages du site sont en cache.
  • Au deuxième crawl, on voit que la quasi totalité des requêtes ont été répondues par les serveurs de cache, ne restent que les pages en 404 et les pages qui ne peuvent être cachées (réseaux sociaux).

Conclusion

La mise en place d’un cache par invalidation, même simple, devrait avoir un effet immédiat sur votre charge à l’origine, toutefois attention quand vous implémenterez des tags adaptés à l’utilisation de vos ressources, le moindre oubli pourra entraîner la mise en cache à vie d’une page sans que les utilisateurs puissent la mettre à jour.

 

Ajouter un commentaire