4 - Nos outils4.3 - Les caches

Chapitre 4.3.4
Invalider le cache

Une source de problèmes majeure.
Temps de lecture : 4 minutes


Il y a seulement deux choses difficiles en informatique : l'invalidation du cache et nommer les choses <Phil KARLTON>

Les entrées d'un cache ne sont pas éternelles, un jour ou l’autre les données deviennent obsolètes. L’invalidation regroupe l’ensemble des techniques qui permettent d’indiquer au système de cache qu’une entrée n’est plus valide afin que les données soient renouvelées.

Pour mieux expliquer les difficultés dans la manipulation de cache, reprenons l'exemple du chapitre "Cache applicatif" :

  • Un client ouvre la page pour voir toutes les offres d'abonnement.
  • Le catalogue d'offres est hébergé chez un partenaire qui met 5 secondes à répondre.
  • Nous avons décidé de cacher ses réponses pour réduire le temps de chargement de la page et protéger notre système contre un pic de trafic.
  • Notre site est ouvert en Europe et Amérique.

Jusqu'à présent, nous avons segmenté les offres seulement par continent : tous les Européens voient les mêmes, les Américains en voient d'autres.

Partons du principe que nous avons trois niveaux d'abonnement : basique, intermédiaire, premium.

L'équipe marketing nous demande de développer un système d'éligibilité aux offres en fonction du profil du client :

  • S'il n'est pas abonné, on lui propose les offres standards de chaque niveau.
  • S'il est déjà abonné, on ne veut lui proposer que la possibilité de changer d'offre pour un abonnement supérieur.
  • S'il a demandé la résiliation de son abonnement, il faut essayer de le retenir avec des offres de rétention.
  • Parfois, on veut mettre en avant des offres pour des opérations spéciales : Noël, Saint-Valentin...

Ça démultiplie les possibilités. On pourrait avoir un client intermédiaire qui vient de demander sa résiliation, un client qui n'est pas encore abonné, un client basique qui veut passer vers premium...

Pour savoir comment structurer le cache et identifier les clés et leur contenu, il faut identifier toutes les variantes possibles. Ces profils de clients sont une notion interne à l'entreprise, déterminés par l'équipe marketing. Notre partenaire gestionnaire d'offres n'a aucune connaissance de la situation d'un client. Il est seulement capable de filtrer un peu les offres pour nous dire "voici toutes les offres européennes".

Pour l'exemple, nous continuons à ne pas préchauffer le cache. Chaque fois qu'un client arrive sur la page, nous analysons son profil et nous en déduisons une clé de cache "americain_actuellement_basique_en_cours_de_resiliation". Si Redis ne connait pas la clé, nous interrogeons le partenaire, nous filtrons les offres en fonction du profil et nous les cachons.

La purge manuelle

L'équipe marketing décide, 6 mois plus tard, de changer le prix des offres standards et de rétention du niveau intermédiaire. Les nouveaux prix sont mis à jour dans le système du partenaire. Le partenaire envoie une notification à notre back-end pour prévenir que quelqu'un a modifié une offre.

Pour afficher ces changements aux clients, nous voulons invalider le cache immédiatement pour qu'il se recalcule et prenne en compte les nouvelles données. Nous allons pour ça réaliser une purge manuelle, c'est-à-dire que c'est directement le code qui va appeler le système de cache pour lui demander explicitement de supprimer certaines clés.

Supprimer un cache, c'est comme ouvrir la grille du magasin un matin de soldes. En enlevant la protection, on expose tout le système à un énorme pic de trafic instantané. Le moins on en supprime, le mieux c'est.

Quels profils sont impactés ?

  1. Les non-abonnés.
  2. Les abonnés de niveau basique avec un abonnement en cours.
  3. Les abonnés de niveau basique qui ont demandé leur résiliation.
  4. Les abonnés de niveau intermédiaire qui ont demandé leur résiliation.

Et ce, pour chaque continent. Nous devons donc purger 8 clés.

Problème 1 - Comment identifier ces clés ?

Nous, en tant qu'humains, nous arrivons à identifier ces clés car on sait ce qu'a modifié le marketing. Comment le script codé il y a 6 mois peut comprendre ça ?

Est-ce qu'il faut garder un référentiel des clés ?
Est-ce qu'il faut lier les offres et les clés ?

La stratégie à adopter dépend du logiciel de cache utilisé et ses fonctionnalités.

Redis permet aux applications qui l'utilisent de monter un système de tags à renseigner au moment d'enregistrer une entrée. Nous pourrions par exemple imaginer qu'au moment de stocker les offres pour le profil "americain_actuellement_basique_en_cours_de_resiliation", nous ajoutions des tags expliquant le contenu de la liste d'offres : "americain_basique_retention", "americain_intermediaire_standard", "americain_premium_standard". Quand nous sommes prévenus d'une modification sur l'offre "americain_basique_retention", l'application peut purger d'un coup toutes les entrées liées à ce tag.

Problème 2 - Comment éviter une surcharge ?

Même en les ciblant soigneusement, on prend un gros risque en purgeant ces clés. En l'occurrence, nous nous apprêtons à vider "non-abonné" pour tous les continents, qui est sûrement le profil le plus représenté. Dès que ce sera fait, pendant 5 secondes, le temps que le cache se reconstruise, tous les utilisateurs déclencheront une requête vers le partenaire et maintiendront une connexion ouverte sur nos serveurs. On risque une bousculade, plus connue sous le nom de "cache stampede".

C'est typiquement à ça que sert le préchauffage abordé dans le chapitre précédent. Au lieu de purger la clé et de laisser un client lancer le recalcul, on fait en sorte de directement remplacer les nouvelles données dès que le partenaire nous prévient que quelque chose a changé.

Pour nous faciliter la vie lors du préchauffage et éviter la purge ciblée, nous pourrions imaginer une seconde couche de cache :

  1. Lors d'une notification partenaire, nous préchauffons un cache par continent "toutes_les_offres_americaines".
  2. Grâce à un unique tag "offres_par_profil", nous purgeons d'un coup toutes les variantes.
  3. Le premier client déclenche le recalcul du cache pour son profil "americain_actuellement_basique_en_cours_de_resiliation". Cette fois, c'est très rapide, il n'y a pas d'appel au partenaire, les offres sont déjà disponibles dans "toutes_les_offres_americaines".

En apparence, ça semble plus simple, mais maintenant, il faut faire attention à purger les entrées de cache dans l'ordre.

Autres formes d'invalidation

Parfois, c'est compliqué de directement purger un cache. Soit parce que la technologie ne s'y prête pas, soit parce qu'il y a trop de variantes ou, tout simplement, car aucun événement ne déclenche cette purge.

Par exemple, imaginons qu'un utilisateur se connecte sur le site. Nous allons chercher des informations sur lui dans de multiples sources de données : son historique de paiements, son adresse de livraison... On décide de mettre ce profil en cache pour accélérer sa navigation sur le site et ne pas avoir besoin de récupérer à nouveau ses données à chaque chargement de page.

Tant que le client ne change rien à sa situation, nous n'avons aucune raison de purger le cache.

Mais la mémoire du serveur est limitée, nous ne pouvons pas cumuler des milliers de profils utilisateurs. Pour éviter une saturation, nous mettons en place des stratégies qui invalident automatiquement le cache même sans purge manuelle.

TTL - Time To Live

La plupart du temps, une donnée est invalidée grâce à une date d’expiration définie au moment de sa mise en cache. Selon la technologie, cette expiration peut être exprimée de deux façons : soit sous forme d’une date complète avec fuseau horaire, soit sous forme d’une durée de vie en secondes appelée TTL (Time To Live). Exemple : “mets ce profil utilisateur en cache pendant 600 secondes”.

Chaque technologie gère les données expirées différemment. Redis par exemple, contrairement à une purge manuelle, ne supprime pas immédiatement l'entrée et ne libère donc pas la mémoire. La suppression réelle intervient soit lorsqu’un client tente d’accéder à la clé expirée, soit lors d’un cycle de nettoyage périodique.

Le TTL sert à deux choses :

  • Invalider automatiquement les données qu'on ne sait pas supprimer autrement.
  • Provoquer un renouvellement volontaire d'une ressource.
    Exemple : l'autorisation d'accéder à une vidéo qui doit être renouvelée toutes les 2 minutes pour éviter un piratage.

Le TTL doit être bien pensé selon l'utilisation de la donnée.
Un délai trop court rend le cache inutile.
Un délai trop long, lui, augmente le risque de surcharger la mémoire.

En général, on applique des TTL longs sur les données qu'on sait pouvoir supprimer manuellement si besoin. Pour les couches plus près des clients, nous favoriserons à l'inverse des TTL courts. Ces couches intermédiaires vont régulièrement venir se mettre à jour sur nos serveurs, où elles seront alimentées par des couches de cache long. Entre le navigateur, le CDN, un reverse proxy et le cache applicatif, on croise bien souvent des chaînes de 3 à 4 couches qui s'alimentent les unes à partir des autres.

En-têtes HTTP

Dans les systèmes de cache des navigateurs et des caches intermédiaires, l’équivalent du TTL s’appelle souvent "max-age". Dès qu'une ressource (image, page, script…) a atteint son max-age, ils vont tenter de la mettre à jour auprès de la couche située juste au-dessus dans la chaîne. Pour éviter de télécharger la ressource entière si elle n’a pas changé, ils envoient une question du type : "voici l’empreinte ou la date de dernière modification que je connais, est-ce toujours à jour ?".

Le système en amont compare alors ces informations avec la version réelle qu’il possède. S’il détecte un changement, il renvoie la ressource complète. Sinon, il répond simplement un petit message indiquant "c’est encore valide", évitant un transfert inutile.

Toute cette mécanique repose sur des en-têtes intégrés au protocole HTTP, le protocole de communication utilisé sur le web. Nous y reviendrons plus en détail dans la partie consacrée au réseau.

Versionnement

Parfois, nous avons besoin de faire évoluer le format des données en cache. Par exemple, avant nous ne stockions que le nom des offres, maintenant nous décidons de stocker un descriptif complet. Lors de la mise en production du code, il ne sera plus capable de lire correctement les données déjà stockées dans le cache : ça risque de provoquer des erreurs lors de l'exécution.

Pour que le code n'aille pas chercher ces clés de cache obsolètes, nous leur ajoutons un numéro de version. À la place de "americain_basique_retention", nous aurons plutôt quelque chose comme "v3:americain_basique_retention". Nous changeons ce numéro de version dans le nouveau code qui passe en production.

Ce système a néanmoins plusieurs inconvénients :

  • La clé "v3:americain_basique_retention" n'est pas invalidée. Si rien n'est fait, elle restera en vie jusqu'à la fin de son TTL. Pendant ce temps, le nouveau code crée et utilise "v4:americain_basique_retention". La mémoire du serveur est mise à rude épreuve avec ces deux versions stockées en même temps.
  • D'un point de vue du code, c'est comme si on supprimait toutes les clés d'un coup et qu'il devait tout recalculer. Comme on l'a vu précédemment, sans préchauffage, c'est très dangereux !

Le versionnement n'est pas seulement utilisé pour qu'une nouvelle version remplace une ancienne lors de mise en production. Il est indispensable si plusieurs variantes du code lui-même tournent en production. C'est typiquement le cas d'un back-end appelé par les différentes versions d'une application mobile. Les clients ne mettant pas tous à jour leur application en même temps, le back-end doit garder la rétrocompatibilité pour s'assurer du bon fonctionnement de toutes les anciennes versions.

Nettoyage

Le nettoyage, ou éviction, correspond aux règles que le cache applique quand la mémoire disponible devient insuffisante. Plutôt que de planter, le système supprime certaines clés selon une stratégie configurable.

Les politiques les plus courantes sont :

  • LRU (Least Recently Used) : supprime les clés les moins utilisées récemment. C’est le comportement par défaut de Redis et Memcached.
  • LFU (Least Frequently Used) : privilégie les données consultées souvent, utile quand certaines clés sont populaires mais peu récentes.
  • TTL-based : supprime d’abord les entrées les plus proches de leur expiration naturelle.
  • Random : supprime des clés aléatoirement. C’est utile lorsque les données ont toutes la même importance et que leur perte ponctuelle est sans conséquence.

Le choix de la politique dépend du rôle du cache et de son utilisation métier.