Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 7.6 sur 7.8 · Intermédiaire · 24 min de lecture

Stratégies de mise en cache et leurs pièges

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Les caches rendent les systèmes rapides, et ils peuvent aussi les faire tomber. Dans cette leçon, vous apprendrez comment fonctionne un cache, comment garder ses copies suffisamment fraîches, et comment l’empêcher de lâcher au pire moment.

Imaginez un café très fréquenté. Au lieu de moudre des grains pour chaque commande, le barista garde sur le comptoir un petit bocal de café moulu. La plupart des commandes sont servies à partir du bocal en quelques secondes. Ce n’est que lorsqu’il est vide que quelqu’un va à la réserve. Le bocal est un cache. Il rend le service rapide, et il ménage la réserve. Mais il soulève des questions. Combien de temps le café moulu peut-il attendre avant d’avoir un goût de rassis ? Et que se passe-t-il quand le bocal se vide pendant le coup de feu du matin ? Les caches logiciels soulèvent exactement les mêmes questions.

Un cache conserve une copie des données dans un endroit plus rapide que leur emplacement d’origine. Il réduit la latence, et il protège la base de données, car chaque succès est une requête que la base de données ne voit jamais. Le prix se paie en exactitude. Un cache est une seconde copie des données, et deux copies peuvent diverger. La première question n’est donc pas de savoir quel cache utiliser. C’est de savoir ce qui peut être périmé, dans quelle mesure, et pour qui. Une carte vieille de cinq minutes, ce n’est pas grave. Un solde bancaire vieux de cinq minutes peut l’être.

Le patron sûr par défaut s’appelle cache-aside. Ici, un service Carte l’utilise. Le service interroge d’abord le cache. En cas de succès, la valeur revient directement. En cas de défaut, il lit la base de données, le système de référence, et stocke le résultat dans le cache avec une durée de vie, après laquelle l’entrée expire. Les écritures empruntent un autre chemin. Mettez d’abord à jour la base de données, puis supprimez l’entrée du cache. N’écrivez pas la nouvelle valeur dans le cache. La lecture suivante rate simplement le cache et recharge la valeur à jour. Et si le cache disparaît complètement, le service lit simplement la base de données, plus lentement. C’est pourquoi cache-aside échoue sans danger.

Garder un cache à jour est notoirement difficile. Une boutade attribuée à Phil Karlton dit qu’il n’y a que deux choses difficiles en informatique : l’invalidation de cache et le nommage des choses. Voici pourquoi. Même la suppression à l’écriture laisse une étroite situation de concurrence. Un lecteur rate le cache et lit un prix de neuf livres dans la base de données. Puis il se met en pause, peut-être à cause d’un réseau lent. Pendant ce temps, un écrivain met à jour le prix à onze livres et supprime l’entrée du cache. Jusque-là, tout va bien. Puis le lecteur reprend et stocke neuf livres dans le cache. Le prix périmé reste jusqu’à l’expiration de sa durée de vie. C’est pourquoi les bonnes conceptions combinent les deux : invalider lors des changements, et garder une durée de vie comme filet de sécurité.

La part des consultations auxquelles le cache répond est son taux de succès. Supposons qu’un service Carte reçoive vingt mille lectures par seconde. Le cache répond en une demi-milliseconde, la base de données en huit, et la base de données supporte environ trois mille requêtes par seconde. Sans cache, chaque lecture va à la base de données : presque sept fois ce qu’elle peut soutenir. À un taux de succès de quatre-vingts pour cent, quatre mille requêtes par seconde atteignent encore la base de données. C’est au-dessus de la capacité, donc les files d’attente s’allongent et la latence augmente. À quatre-vingt-quinze pour cent, c’est mille requêtes par seconde. Confortable. À quatre-vingt-dix-neuf pour cent, la latence moyenne change à peine, mais la charge de la base de données est divisée par cinq. Chaque « neuf » de succès supplémentaire divise la charge de la base de données par environ dix. Maintenant, le cache redémarre vide. Les vingt mille requêtes par seconde frappent toutes une base de données dimensionnée pour trois mille, et elle s’effondre. Un cache dont on ne peut pas se passer est une dépendance critique.

Une ruée se produit quand une entrée populaire expire, et que des centaines de requêtes ratent le cache au même instant et vont toutes à la base de données. Les parades se combinent bien. Le regroupement des requêtes ne laisse qu’une requête par clé aller chercher la valeur, pendant que les autres attendent son résultat. L’expiration avec gigue aléatoire ajoute une durée aléatoire à chaque durée de vie, pour que des entrées écrites ensemble n’expirent pas ensemble. Stale-while-revalidate sert la valeur expirée à tout le monde pendant qu’une requête en arrière-plan en récupère une nouvelle. Et préchauffer un nouveau cache à partir d’une liste de clés très demandées, avant qu’il ne reçoive du trafic, évite la ruée après un redémarrage.

Enfin, quatre erreurs courantes. Ajouter un cache avant de mesurer : souvent, la vraie solution est un index ou une meilleure requête. N’avoir aucune durée de vie, si bien qu’un seul chemin de code oublié laisse des données périmées pour toujours. Dimensionner la base de données uniquement pour la charge mise en cache, si bien qu’un redémarrage du cache fait tomber toute la plateforme. Et mettre en cache des données personnelles ou financières dans des caches partagés. Une page portant le nom d’un client, mise en cache sous sa seule URL, peut être servie au visiteur suivant.

La section complète compare cinq patrons de mise en cache, et couvre les niveaux de cache, les en-têtes HTTP et l’éviction. Ensuite, dans la section sept point sept, nous verrons l’évolution des schémas. À très vite.

Ce qu’il faut savoir

Pourquoi mettre en cache, et ce que cela coûte

Un cache conserve une copie des données dans un endroit plus rapide ou plus proche que leur emplacement d’origine, afin que les requêtes répétées ne paient pas le prix fort à chaque fois. La section 0.2 a présenté l’idée comme un échange d’espace contre du temps. À l’échelle de l’architecture, l’échange est plus intéressant, car un cache modifie trois qualités à la fois. Il réduit la latence (une consultation de Redis dans le même centre de données prend généralement bien moins d’une milliseconde ; une requête qui joint cinq tables peut prendre des dizaines de millisecondes). Il augmente le débit et protège la base de données, car chaque succès (hit) est une requête que la base de données ne voit jamais. Et il peut améliorer la disponibilité, car un cache capable de servir des données légèrement anciennes continue de fonctionner pendant que la source est lente ou indisponible.

Le prix se paie en exactitude et en complexité. Un cache est une seconde copie des données, et deux copies peuvent diverger. Chaque cache incarne donc une décision sur le degré de péremption autorisé pour une réponse, et pour qui. Une carte de restaurant vieille de cinq minutes est acceptable ; un solde bancaire vieux de cinq minutes, montré à un client qui se demande s’il peut se permettre un paiement, ne l’est pas. Les caches ajoutent aussi un composant qui peut tomber en panne, consomment de la mémoire qui coûte de l’argent et créent de nouveaux modes de défaillance, comme les ruées et les démarrages à froid, qui n’existeraient pas sans eux. La première question n’est donc pas quel cache ? mais qu’est-ce qui peut être périmé, dans quelle mesure, et que se passe-t-il si c’est le cas ?

L’efficacité d’un cache se mesure par son taux de succès : le nombre de succès divisé par le nombre total de consultations. Le taux de succès dépend de l’asymétrie du profil d’accès. Dans la plupart des systèmes grand public, le trafic de lecture est très asymétrique (une petite fraction des restaurants, des produits ou des comptes reçoit l’essentiel des lectures), si bien qu’un cache contenant quelques pour cent des données peut souvent répondre à la plupart des lectures. Un accès aléatoire uniforme, en revanche, se met mal en cache quoi que vous fassiez.

Les cinq patrons classiques

Les patrons de mise en cache diffèrent par l’acteur qui charge le cache, par le moment où les écritures atteignent la source, et donc par le degré de péremption et de durabilité des données. Le tableau les compare ; les paragraphes qui le suivent expliquent où chacun trouve sa place.

Comparaison des patrons de mise en cache

PatronFonctionnement des lecturesFonctionnement des écrituresPoints fortsRisques
Cache-aside (chargement paresseux)L’application consulte le cache ; en cas de défaut (miss), elle lit la base de données et stocke le résultatL’application écrit dans la base de données, puis supprime (ou met à jour) l’entrée du cacheSimple, ne met en cache que ce qui est utilisé, une panne du cache se traduit seulement par des lectures plus lentesLa première lecture après expiration est lente ; des situations de concurrence peuvent laisser des entrées périmées ; ruées
Read-throughL’application interroge le cache ; la bibliothèque de cache charge elle-même depuis la base de données en cas de défautGénéralement combiné avec des écritures write-through ou cache-asideLa logique de chargement est à un seul endroitMêmes problèmes de péremption et de ruée ; la couche de cache doit connaître la source de données
Write-throughComme read-through ou cache-asideL’écriture va dans le cache et dans la base de données de façon synchrone avant l’accusé de réceptionLe cache n’est jamais plus ancien que la base de données pour les données écrites à travers luiChaque écriture paie les deux latences ; met en cache des données qui ne seront peut-être jamais lues
Write-behind (write-back)Servies depuis le cacheL’écriture est confirmée dès qu’elle est dans le cache, puis vidée vers la base de données plus tard, souvent par lotsÉcritures très rapides ; absorbe les pics ; regroupe le travail de la base de données par lotsPerte de données si le cache tombe en panne avant le vidage ; retard de la base de données ; ordre et conflits
Refresh-aheadServies depuis le cacheNe concerne pas les écritures : les entrées très demandées sont rechargées peu avant leur expirationLes éléments populaires n’expirent jamais sous les yeux des utilisateursRechargements inutiles d’éléments qui n’auraient pas été lus ; nécessite des statistiques d’accès

Cache-aside est le choix par défaut pour de bonnes raisons : il est facile à comprendre et échoue sans danger, car si le cache disparaît, l’application lit simplement la base de données (plus lentement). Read-through déplace la même logique dans une bibliothèque ou un produit de cache, ce qui est plus net, mais ne change rien au comportement. Write-through convient aux données lues peu après leur écriture, comme la session d’un utilisateur ou son panier. Write-behind est le plus risqué : il fait du cache le système de référence temporaire, si bien qu’il ne convient que lorsque perdre les dernières secondes d’écritures est acceptable (compteurs de vues, événements analytiques, scores de jeu) ou lorsque le cache lui-même est répliqué et durable. N’utilisez jamais write-behind pour de l’argent. Refresh-ahead est une optimisation pour un petit ensemble de clés très demandées, comme la liste des restaurants de la page d’accueil dans une grande ville animée.

Cache-aside en pratique

Cache-aside : à la lecture, le service consulte le cache, charge depuis la base de données en cas de défaut et stocke le résultat ; à l’écriture, il met à jour la base puis supprime l’entrée en cacheService Cartecode applicatifCacheex. RedisBDsystème de réf.1 GET clé2 hit : valeur5 SET clé, TTL3 miss : SELECT4 ligneÉCRITUREa UPDATE lignedans la BDb DELETE clédans le cachelecture suivanterate, lit lavaleur neuve
Les lectures remplissent le cache paresseusement en cas de défaut ; les écritures modifient d’abord la base de données, puis suppriment la copie en cache au lieu de l’écraser.
import json, random

TTL = 300  # seconds

def get_menu(restaurant_id):
    key = f"menu:{restaurant_id}:v3"            # version the key with the data shape
    cached = cache.get(key)
    if cached is not None:
        return json.loads(cached)                # hit
    with cache.lock(f"lock:{key}", timeout=5):  # coalesce: one loader per key
        cached = cache.get(key)                  # someone may have filled it meanwhile
        if cached is not None:
            return json.loads(cached)
        menu = db.load_menu(restaurant_id)       # miss: go to the system of record
        jitter = random.randint(0, TTL // 10)    # spread expiry times out
        cache.set(key, json.dumps(menu), ex=TTL + jitter)
        return menu

def update_price(restaurant_id, item_id, price):
    db.update_price(item_id, price)              # 1. change the system of record
    cache.delete(f"menu:{restaurant_id}:v3")     # 2. invalidate, do not overwrite
Cache-aside avec une clé versionnée, un verrou par clé pour regrouper les défauts concurrents, une expiration avec gigue aléatoire, et une invalidation par suppression après l’écriture en base de données.

Durée de vie et invalidation

Il y a deux façons d’empêcher un cache de servir des données qui ont changé. La première est une durée de vie (TTL) : chaque entrée expire après une période fixe, ce qui borne la péremption sans aucune coordination. Un TTL de cinq minutes sur les cartes signifie qu’un changement de prix est visible partout en moins de cinq minutes. La seconde est l’invalidation explicite : quand la source change, quelque chose supprime ou met à jour la copie en cache. L’invalidation donne des données plus fraîches, mais exige que chaque chemin de code qui modifie les données connaisse chaque cache qui les contient, et c’est exactement ce qui se dérègle à mesure que les systèmes grandissent. Dans les systèmes distribués, l’invalidation est souvent pilotée par des événements : le service Carte publie MenuChanged, et chaque service qui en détient une copie supprime son entrée. La plupart des conceptions professionnelles combinent les deux : invalider lors des changements pour la fraîcheur, et garder un TTL comme filet de sécurité pour l’invalidation qui aura été manquée.

La difficulté est résumée par une boutade attribuée à Phil Karlton, de Netscape : Il n’y a que deux choses difficiles en informatique : l’invalidation de cache et le nommage des choses. C’est difficile parce que l’invalidation est un problème de systèmes distribués déguisé. L’écriture en base de données et la suppression dans le cache sont deux opérations sur deux systèmes sans transaction commune ; l’une peut donc réussir pendant que l’autre échoue, et des opérations issues de requêtes différentes peuvent s’entrelacer.

Deux règles évitent la plupart des problèmes. D’abord, lors d’une écriture, mettez à jour la base de données, puis supprimez l’entrée du cache ; n’écrivez pas la nouvelle valeur dans le cache. Deux écrivains concurrents qui mettent chacun le cache à jour peuvent terminer dans l’ordre inverse de leurs commits en base de données et laisser l’ancienne valeur en cache indéfiniment, alors qu’une suppression ne peut causer qu’un défaut supplémentaire. Ensuite, acceptez que même la suppression à l’écriture laisse une étroite situation de concurrence, illustrée ci-dessous, et bornez-la avec un TTL. L’article de Facebook de 2013 sur le passage à l’échelle de memcache décrit des baux (leases) pour ce cas : le cache remet un jeton au client qui a subi le défaut, et refuse l’écriture ultérieure de ce client si la clé a été supprimée entre-temps.

La situation de concurrence du SET périmé

Concurrence en cache-aside : un lecteur lent stocke une vieille valeur après qu’un écrivain a supprimé l’entrée, qui reste périmée jusqu’à l’expiration du TTLLecteur RÉcrivain WDans le cacheGET menu:17 : miss(rien)SELECT : prix £9UPDATE prix à £11DELETE menu:17(rien)SET menu:17 = £9£9 périmé jusqu’à TTLR en pause(GC, réseau lent)
Supprimer à l’écriture réduit la fenêtre de données périmées sans la fermer ; un TTL borne la durée de survie de la valeur périmée.

Ruées et effet de troupeau

Une ruée sur le cache (cache stampede) se produit quand une entrée populaire expire, ou qu’un nœud de cache redémarre vide, et que des centaines de requêtes concurrentes ratent toutes le cache au même instant. Chacune va à la base de données pour calculer la même valeur, et la base de données, dimensionnée pour le taux de défaut normal du cache, est submergée. Des réponses plus lentes font s’accumuler davantage de requêtes, et la surcharge peut se propager en cascade. Le phénomène voisin de l’effet de troupeau (thundering herd) désigne de nombreux clients en attente réveillés en même temps pour se disputer une seule ressource. Facebook a indiqué que, pour un ensemble de clés particulièrement sujettes à ce problème, les baux ont fait passer le pic de requêtes à la base de données de 17 000 à 1300 par seconde.

Les parades se combinent bien. Le regroupement des requêtes (request coalescing, aussi appelé single flight) ne laisse qu’une requête par clé aller chercher la valeur à la source, pendant que les autres attendent son résultat ; au sein d’un processus, c’est une table de futures en cours ; entre processus, un verrou court dans le cache, comme dans le code ci-dessus. L’expiration avec gigue aléatoire (jitter) ajoute une durée aléatoire à chaque TTL afin que des entrées écrites ensemble (après un déploiement ou un vidage du cache) n’expirent pas ensemble. Stale-while-revalidate sert la valeur expirée à tout le monde pendant qu’une requête en arrière-plan en récupère une nouvelle, ce qui supprime entièrement le pic de latence quand une légère péremption est acceptable. L’expiration anticipée probabiliste (Vattani, Chierichetti et Lowenstein, VLDB 2015) fait décider à chaque lecteur, au hasard et avec une probabilité qui augmente à l’approche de l’expiration, de rafraîchir l’entrée en avance, de sorte qu’une requête la rafraîchit généralement avant qu’elle n’expire réellement. Enfin, le préchauffage d’un nouveau cache à partir d’une liste de clés très demandées, avant qu’il ne reçoive du trafic, évite la ruée du démarrage à froid après un redémarrage.

Exemple commenté

Exemple commenté : ce que le taux de succès fait à la latence et à la charge de la base de données

Le service Carte de la plateforme de livraison de repas reçoit 20 000 lectures de carte par seconde au pic du soir. Une consultation du cache prend 0,5 ms ; une requête à la base de données prend 8 ms. En cas de défaut, le service paie les deux (8,5 ms) et stocke le résultat de façon asynchrone. La base de données peut soutenir environ 3000 requêtes de ce type par seconde avant que la latence ne grimpe en flèche.

FormuleLatence moyenne = 0,5 + (1 − h) × 8 ms. Charge de la base de données = (1 − h) × 20 000 requêtes par seconde.
Aucun cache8 ms en moyenne ; 20 000 requêtes par seconde, presque sept fois ce que la base de données peut soutenir.
Taux de succès 80 %0,5 + 0,2 × 8 = 2,1 ms ; 4000 requêtes par seconde : encore au-dessus de la capacité, donc les requêtes s’accumulent dans la file d’attente de la base de données et la latence augmente.
Taux de succès 95 %0,5 + 0,05 × 8 = 0,9 ms ; 1000 requêtes par seconde : confortable.
Taux de succès 99 %0,5 + 0,01 × 8 = 0,58 ms ; 200 requêtes par seconde.
De 95 % à 99 %La latence moyenne ne s’améliore que de 0,32 ms, mais la charge de la base de données est divisée par cinq, de 1000 à 200 requêtes par seconde.
Le cluster de cache redémarre videLe taux de succès tombe près de 0 % pendant un moment : 20 000 requêtes par seconde frappent une base de données dimensionnée pour 3000. Sans regroupement, préchauffage ni limitation de débit, la base de données s’effondre.
PercentilesÀ 95 % de succès, 5 % des requêtes prennent 8,5 ms, donc le p95 est à la limite et le p99 vaut 8,5 ms, pas 0,9 ms.

Le taux de succès compte bien plus pour la base de données que pour la latence moyenne : chaque « neuf » de succès supplémentaire divise la charge de la base de données par environ dix.

Un cache sans lequel le système ne peut pas fonctionner n’est plus une optimisation ; c’est une dépendance critique, et sa défaillance doit être prévue comme la section 2.5 le prévoit pour toute autre.

Les moyennes flattent les caches ; comme l’a montré la section 2.4, ce sont les défauts qui définissent la queue de distribution.

Les niveaux de cache

Les caches existent à chaque niveau entre l’utilisateur et le disque, et une requête reçoit sa réponse du premier niveau qui dispose d’une copie utilisable. Le navigateur garde un cache privé contrôlé par des en-têtes HTTP. Un réseau de diffusion de contenu (CDN) garde des copies partagées dans des emplacements en périphérie (edge), près des utilisateurs, idéal pour les images, les scripts et les réponses d’API publiques comme les cartes. Un reverse proxy à l’origine (Nginx, Varnish) peut mettre en cache des réponses entières pendant quelques secondes, ce qu’on appelle le microcaching, qui peut absorber un pic soudain. L’application met en cache des objets calculés, soit dans le processus (le plus rapide, mais chaque instance a sa propre copie et elles divergent), soit dans un cache partagé comme Redis ou Memcached. La base de données met en cache des pages en mémoire (les shared buffers de PostgreSQL, le buffer pool d’InnoDB) et peut précalculer des résultats coûteux dans des vues matérialisées.

Plus on est loin des données, plus l’économie par succès est grande et plus l’invalidation est difficile. Vous pouvez supprimer une clé de Redis en une milliseconde ; vous pouvez purger un CDN, plus lentement ; vous ne pouvez pas du tout atteindre le navigateur d’un client, si bien que le max-age que vous envoyez est une promesse avec laquelle vous devrez vivre. Les longues durées de vie dans le navigateur sont donc réservées aux fichiers dont l’URL change quand leur contenu change (app.3f9a1c.js), et les courtes, ou la revalidation, à tout le reste.

Où vivent les caches

Cinq niveaux de cache, du navigateur à la mémoire de la base de données via un CDN, un reverse proxy et un cache applicatif, avec les contrôles de chaque niveauNavigat.cache privéCDN edgepartagé, près util.Reverse proxypartagé, origineApplicationen processus ou RedisBDbuffer pool, vuesUne requête ne va vers la droite qu’aussi loin que nécessaire ; chaque niveau qui répond épargne tous les niveaux derrière luicôté utilisateur : latence faible, plus dur à invalidercôté données : plus simple à tenir à jourCache-Control,ETags-maxage,API purgeTTL par route,microcachingdesign clés,TTL, évictiongéré par la BDelle-même
Chaque niveau qui répond à une requête épargne tous les niveaux derrière lui ; les niveaux proches de l’utilisateur économisent davantage mais sont plus difficiles à invalider.

En-têtes de cache HTTP

HTTP dispose d’un modèle de cache complet, défini dans la RFC 9111 (juin 2022), et bien l’utiliser peut supprimer de la charge avant même qu’elle n’atteigne vos serveurs. Cache-Control: max-age=60 permet à n’importe quel cache de réutiliser une réponse pendant 60 secondes ; s-maxage remplace cette valeur pour les caches partagés comme les CDN ; private interdit aux caches partagés de la stocker (utilisez-le pour tout ce qui est personnel) ; no-cache signifie, de façon déroutante, stocke-la, mais vérifie auprès du serveur avant chaque réutilisation ; et no-store signifie n’en garde jamais de copie, le bon réglage pour les soldes et les relevés bancaires. Les extensions stale-while-revalidate et stale-if-error (RFC 5861, 2010) appliquent au niveau HTTP les idées de ruée et de disponibilité vues plus haut.

Quand une copie en cache est périmée, le client n’a pas besoin de la télécharger à nouveau. Un validateur lui permet de demander si quelque chose a changé : le serveur envoie un ETag (un identifiant de version opaque) ou une date Last-Modified avec la réponse, et le client envoie plus tard If-None-Match ou If-Modified-Since. Si rien n’a changé, le serveur répond 304 Not Modified sans corps, ce qui économise de la bande passante et souvent l’essentiel du travail du serveur. Ces requêtes conditionnelles sont définies dans la RFC 9110. L’en-tête Vary indique aux caches quels en-têtes de requête (comme Accept-Language) produisent des réponses différentes, afin qu’une carte en français ne soit jamais servie à une requête en anglais.

GET /restaurants/17/menu HTTP/1.1
Host: api.example-food.test

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"

... 18 KB of menu JSON ...

# 100 seconds later the copy is past max-age plus the stale-while-revalidate
# window, so the browser must revalidate before using it:
GET /restaurants/17/menu HTTP/1.1
If-None-Match: "menu-17-v42"

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"

# A bank balance, by contrast, must never sit in any cache:
Cache-Control: no-store
Une réponse de carte publique que les navigateurs peuvent réutiliser pendant 60 secondes et les CDN pendant 300, revalidée à faible coût avec un ETag ; les données financières personnelles utilisent no-store.

Éviction, dimensionnement et risques de cohérence

Un cache est plus petit que les données qu’il couvre ; quand il est plein, il doit donc évincer quelque chose. La politique moins récemment utilisé (LRU) évince l’entrée restée inutilisée le plus longtemps et fonctionne bien quand l’usage récent prédit l’usage futur. La politique moins fréquemment utilisé (LFU) évince l’entrée la moins souvent utilisée et résiste aux parcours ponctuels (un rapport nocturne qui touche chaque restaurant une fois viderait un cache LRU de ses entrées réellement très demandées). Les caches de production approximent les deux : Redis, par exemple, échantillonne quelques clés et évince le meilleur candidat parmi elles, et propose des politiques comme allkeys-lru, allkeys-lfu et volatile-ttl. Sa politique par défaut, noeviction, rejette les écritures quand la mémoire est pleine, ce qui convient à Redis utilisé comme stockage, mais pas comme cache ; choisissez donc la politique délibérément.

Au-delà de la péremption, les caches introduisent des risques de cohérence plus subtils. Un utilisateur qui change son adresse de livraison puis voit l’ancienne, parce que la lecture provient d’un cache qu’une autre instance n’avait pas encore invalidé, subit une rupture de la garantie de lecture de ses propres écritures (section 7.4) ; une solution courante consiste à contourner le cache pour les écritures récentes de l’utilisateur lui-même. Les caches internes au processus de nombreuses instances divergent entre eux, si bien que des requêtes consécutives acheminées vers des instances différentes peuvent montrer une valeur qui revient en arrière. Mettre en cache une erreur ou un résultat vide sans TTL court transforme une brève panne en une longue. Mettre en cache des données qui mêlent plusieurs utilisateurs (une page portant le nom du client, mise en cache sous la seule URL) divulgue des données personnelles au visiteur suivant, une catégorie bien connue d’incidents de sécurité. Enfin, les clés de cache doivent inclure tout ce qui change la réponse : locataire (tenant), langue, devise, permissions et version du format des données.

Vérifiez votre compréhension

Pourquoi une écriture doit-elle supprimer l’entrée du cache plutôt que d’y placer la nouvelle valeur ?

Parce que deux écrivains concurrents peuvent terminer leurs mises à jour du cache dans l’ordre inverse de leurs commits en base de données, laissant l’ancienne valeur en cache ; une suppression peut au pire causer un défaut supplémentaire, qui recharge la valeur actuelle.

Une consultation du cache coûte 1 ms et une lecture en base de données 10 ms. Quelle est approximativement la latence moyenne à un taux de succès de 90 % avec cache-aside ?

1 + 0,1 × 10 = 2 ms (chaque requête paie la consultation ; une sur dix paie aussi la lecture en base de données).

Quelle est la différence entre Cache-Control: no-cache et no-store ?

no-cache permet à un cache de stocker la réponse, mais l’oblige à la revalider auprès du serveur avant chaque réutilisation ; no-store interdit d’en garder la moindre copie.

Pourquoi faire passer le taux de succès de 99 % à 99,9 % peut-il compter, alors que la latence moyenne change à peine ?

Parce que la charge de la base de données est proportionnelle au taux de défaut, qui passe de 1 % à 0,1 %, soit dix fois moins de requêtes atteignant la base de données.

Suivant · 7.7Évolution des schémasModifier les schémas de bases de données, d’API et d’événements pendant que le système continue de fonctionner : rétrocompatibilité, compatibilité future et compatibilité complète, expansion-contraction, outils de migration, backfills de grande ampleur et registres de schémas pour Avro et Protocol Buffers.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Notre API de recherche de restaurants est lente aux heures de pointe. Comment décider s’il faut ajouter un cache, où le placer et quel TTL utiliser ? »