AWS Certified CloudOps Engineer - Associate

Mise en cache avec CloudFront et ElastiCache

Où placer un cache pour qu'il enlève de la charge réelle : la cache key et les règles de TTL qui décident de ce que CloudFront garde à la périphérie, et les stratégies lazy loading, write-through et TTL qui décident de ce qu'ElastiCache garde devant votre base de données.

Intermédiaire 30 minutes 5 Objectifs d'apprentissage
  1. Décider si une charge de travail a besoin d'un cache de périphérie, d'un cache de base de données, ou des deux
  2. Prédire combien de temps CloudFront garde un objet à partir des TTL de la cache policy et des en-têtes Cache-Control de l'origine
  3. Augmenter un cache hit ratio CloudFront en réduisant la cache key, et choisir entre invalidation et noms de fichiers versionnés
  4. Comparer lazy loading, write-through et TTL comme stratégies de remplissage d'ElastiCache et nommer la faiblesse propre à chacune
  5. Lire les métriques CloudWatch d'ElastiCache pour choisir entre monter en gamme, ajouter des réplicas et ajouter des shards

Votre page catalogue exécute une requête qui prend 300 ms et renvoie les mêmes 200 lignes à tous les visiteurs. À 400 requêtes par seconde, la base exécute cette requête 400 fois par seconde et brûle son CPU à produire une réponse qu'elle a déjà produite. La requête n'a aucun défaut. Le problème, c'est que vous recalculez une constante, et le correctif consiste à garder la réponse plus près que la machine qui l'a calculée.

AWS vous propose deux endroits très différents pour la garder, et la compétence 2.1.2 du SOA-C03 nomme les deux : Amazon CloudFront à la périphérie et Amazon ElastiCache à côté de votre application. Ils ne sont pas interchangeables, et se tromper d'endroit revient à ajouter un service qui n'enlève aucune charge.

Deux caches, deux distances

CloudFrontElastiCache
Où il se trouveLes edge locations AWS, près du visiteurDans votre VPC, près de votre application
Ce qu'il stockeDes réponses HTTP entières, indexées par une cache keyCe que votre code y met, indexé par une chaîne
Qui écrit dedansCloudFront, automatiquement, sur un missVotre code applicatif, explicitement
Ce qu'il protègeL'origine et le chemin réseau qui y mèneLa base de données
Coût d'un missUne requête vers l'origineUne requête en base plus une écriture dans le cache
Ce que vous réglezCache policies, TTL, cache keyStratégie, TTL, type de nœud, nombre de shards

La phrase qui tranche : CloudFront est un cache que vous configurez, ElastiCache est un cache que vous programmez. Si les mêmes octets partent vers de nombreux visiteurs en HTTP, CloudFront absorbe ce trafic sans une ligne de code. Si la chose coûteuse est un résultat de requête, un objet de session ou un classement calculé que seule votre application comprend, aucun cache de périphérie ne peut aider, parce que CloudFront ne voit pas à l'intérieur de la réponse et ne sait pas qu'elle est réutilisable.

Les grands systèmes font tourner les deux. CloudFront prend les réponses publiques répétées, ElastiCache prend les lectures internes répétées, et la base ne voit plus que ce qui varie vraiment.

La cache key CloudFront décide de ce qu'est un « hit »

CloudFront range chaque objet sous une cache key. Une requête est un hit seulement si elle produit la même cache key qu'une requête antérieure et que l'objet est encore valide dans cette edge location. Tout le réglage du cache CloudFront revient à une règle : moins il y a de valeurs dans la cache key, plus il y a de hits.

Vous contrôlez la clé avec une cache policy attachée à un cache behavior. La policy dit quels en-têtes, cookies et query strings entrent dans la clé, et elle porte aussi les réglages de TTL.

Les policies managées valent d'être mémorisées, parce qu'elles marquent les deux extrémités de la plage :

Cache policy managéeMin TTLMax TTLDefault TTLContenu de la cache key
CachingOptimized1 s31 536 000 s (365 jours)86 400 s (24 h)Rien, sauf l'en-tête Accept-Encoding normalisé
CachingDisabled0 s0 s0 sRien

CachingOptimized est la réponse par défaut pour les assets statiques. CachingDisabled est la bonne réponse pour tout ce qui est réellement propre à un utilisateur, et elle fonctionne parce que ses trois TTL valent 0, pas parce qu'elle ne transmet rien.

Trois erreurs de cache key coûtent du hit ratio, et chacune a son correctif :

  • La casse et l'ordre dans les query strings. ?parameter1=A et ?parameter1=a sont deux clés différentes, tout comme ?parameter1=a&parameter2=b et ?parameter2=b&parameter1=a. Même objet, jusqu'à quatre entrées de cache. Normalisez une casse et un ordre dans le code qui construit les URL.
  • Transmettre tous les cookies. Pour chaque cookie transmis, CloudFront garde une copie distincte par combinaison de nom et de valeur. Deux cookies à trois valeurs possibles chacun, cela fait jusqu'à 9 copies du même fichier .css. Séparez statique et dynamique dans deux cache behaviors et ne transmettez les cookies que sur le behavior dynamique.
  • Mettre en cache sur User-Agent. Le nombre de valeurs distinctes est énorme, donc cet en-tête désactive le cache en donnant l'apparence d'une configuration qui marche. Si vous avez besoin de réponses adaptées à l'appareil, utilisez plutôt les en-têtes CloudFront dédiés : CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer, CloudFront-Is-SmartTV-Viewer et CloudFront-Is-Tablet-Viewer. Quatre valeurs au lieu de milliers, et vous ne transmettez que ceux qui changent réellement la réponse.

Un dernier levier vit en dehors de la clé. Si la compression n'entre pas en jeu, attachez un custom origin header nommé Accept-Encoding avec une valeur vide. CloudFront retire alors cet en-tête de la cache key au lieu de découper chaque objet en variantes Gzip, Brotli et non compressée.

Combien de temps un objet reste : l'origine propose, la policy décide

C'est la partie que les opérateurs ratent le plus souvent, et elle mérite qu'on la déroule lentement.

Trois réglages vivent dans la cache policy : Minimum TTL, Maximum TTL et Default TTL. L'origine peut de son côté envoyer Cache-Control: max-age, Cache-Control: s-maxage ou un en-tête Expires. Aucun des deux camps ne gagne d'office. L'origine propose une durée et la policy la recadre dans sa plage.

Déroulez avec une policy à Minimum TTL 60, Maximum TTL 86400, Default TTL 3600 :

Réponse de l'origineCloudFront met en cache pendantPourquoi
Cache-Control: max-age=1060 sSous le minimum, donc remonté au Minimum TTL
Cache-Control: max-age=72007200 sDans la plage, donc respecté tel quel
Cache-Control: max-age=3153600086400 sAu-dessus du maximum, donc redescendu au Maximum TTL
Aucun en-tête Cache-Control3600 sRien n'a été proposé, donc le Default TTL s'applique

Quatre détails sur les bords de ce tableau :

  • s-maxage bat max-age à la périphérie. Quand l'origine envoie les deux, CloudFront recadre s-maxage et le navigateur utilise max-age. C'est ainsi qu'on garde un objet une heure à la périphérie et une minute dans le navigateur.
  • Expires est l'option faible. Si l'origine envoie à la fois Cache-Control: max-age et Expires, CloudFront n'utilise que max-age. AWS recommande max-age.
  • Un visiteur ne peut pas forcer un rafraîchissement. CloudFront ignore Cache-Control et Pragma dans les requêtes visiteur, donc un rechargement forcé dans le navigateur ne vide pas la périphérie.
  • Sans max-age et avec un Minimum TTL supérieur à 0, CloudFront retient le plus grand du Minimum TTL et du Default TTL. Avec 60 et 3600, cela donne bien 3600, mais la règle n'est pas « le Default TTL gagne ».

Vient maintenant l'idée fausse qui livre des bugs en production. On suppose volontiers qu'un Cache-Control: no-store venu de l'origine empêche toujours CloudFront de mettre en cache. C'est faux dès que le Minimum TTL de la cache policy dépasse 0. Dans ce cas, CloudFront garde l'objet pendant la durée du minimum alors que l'origine a dit non, et c'est ainsi qu'une page de compte personnalisée est servie au visiteur suivant qui atterrit sur la même edge location. CachingOptimized (Minimum TTL 1 seconde) et la policy Amplify (Minimum TTL 2 secondes) portent cet avertissement dans la documentation AWS. Si un contenu ne doit jamais être mis en cache, la réponse est une policy dont le minimum vaut 0.

Servir du périmé volontairement

Deux directives échangent un peu de fraîcheur contre de la latence et contre la survie à une panne d'origine :

Cache-Control: max-age=3600, stale-while-revalidate=600, stale-if-error=86400
  • Pendant la première heure, CloudFront sert depuis le cache normalement.
  • Ensuite, stale-while-revalidate=600 autorise CloudFront à rendre immédiatement la copie périmée au visiteur pendant qu'il va chercher une version fraîche en arrière-plan, jusqu'à 10 minutes.
  • stale-if-error=86400 autorise CloudFront à continuer de servir la copie périmée jusqu'à 24 heures si l'origine est injoignable ou renvoie une erreur 5xx.

Les deux sont plafonnées par le Maximum TTL de la cache policy, la valeur la plus faible l'emportant. Passé le Maximum TTL, l'objet a quitté la périphérie quoi qu'aient demandé les directives.

Invalidation ou noms de fichiers versionnés

Quand il faut sortir un contenu du cache avant son expiration, vous avez deux options, et AWS recommande celle vers laquelle les gens se tournent en second.

L'invalidation retire les fichiers des caches de périphérie tout de suite. Les 1 000 premiers chemins d'invalidation par mois sont gratuits par compte AWS, toutes distributions confondues, et vous payez au chemin au-delà. Un chemin contenant un joker * compte pour un seul chemin quel que soit le nombre de fichiers retirés, donc /* est une unité facturable unique. Le joker doit être le dernier caractère ; un astérisque ailleurs est traité littéralement.

Deux pièges d'invalidation gâchent un déploiement :

  • Les query strings font partie du chemin. Si la cache key contient les query strings, /images/logo.jpg n'invalide pas /images/logo.jpg?v=2. Utilisez /images/logo.jpg*.
  • Les chemins de répertoire demandent les deux formes. Si vos URL ne sont pas cohérentes sur le slash final, invalidez /images et /images/.

Les noms de fichiers versionnés consistent à livrer app.a3f91c.js au lieu d'écraser app.js. AWS recommande cette approche en premier pour les contenus qui changent souvent, et la raison ne se limite pas au coût. Une invalidation vide CloudFront mais ne fait rien contre la copie posée dans le navigateur du visiteur ou dans un proxy d'entreprise ; un nouveau nom de fichier change l'URL, donc tous les caches de la chaîne font un miss. Cela rend aussi le rollback trivial et les access logs lisibles, puisque la ligne de log nomme la version servie.

Origin Shield est l'autre levier structurel. Il ajoute une couche de cache supplémentaire devant l'origine, pour que toutes les couches CloudFront, edge locations et regional edge caches, convergent vers un seul emplacement. L'origine sert alors une requête par objet au lieu d'une requête par regional cache.

ElastiCache : le cache qu'il faut programmer

CloudFront se remplit tout seul. ElastiCache non : votre code décide de ce qui entre, quand, et pour combien de temps. AWS documente trois stratégies, et l'examen teste le mode d'échec de chacune plutôt que sa définition.

Le lazy loading écrit dans le cache seulement après un miss.

get_customer(customer_id)
    customer_record = cache.get(customer_id)
    if (customer_record == null)
        customer_record = db.query("SELECT * FROM Customers WHERE id = {0}", customer_id)
        cache.set(customer_id, customer_record)
    return customer_record

Seules les données demandées entrent dans le cache, et une panne de nœud est survivable : un nœud neuf et vide renvoie encore des réponses justes, simplement plus lentement, pendant que les miss le remplissent. Le prix, ce sont trois allers-retours par miss (lecture du cache, requête en base, écriture du cache) et des données périmées, puisque rien ne met le cache à jour quand la base change en dessous.

Le write-through écrit dans le cache à chaque écriture en base.

save_customer(customer_id, values)
    customer_record = db.query("UPDATE Customers WHERE id = {0}", customer_id, values)
    cache.set(customer_id, customer_record)
    return success

Les données en cache ne sont jamais périmées, et la latence supplémentaire tombe sur les écritures, là où les utilisateurs la tolèrent mieux. Le prix est l'image inversée du lazy loading : des données manquantes sur tout nouveau nœud, puisqu'un nœud neuf ne contient rien tant que les lignes correspondantes ne sont pas réécrites, et du cache churn, puisque vous mettez en cache des écritures que personne ne lira peut-être jamais.

Ajouter un TTL à chaque écriture est ce qui fait fonctionner le couple. Un TTL borne la péremption d'une entrée chargée paresseusement et évince les entrées write-through que personne ne lit. Le motif de production, c'est les trois ensemble : write-through pour la justesse, lazy loading pour la résilience, TTL pour borner les deux.

cache.set(customer_id, customer_record, 300)   # expire dans 5 minutes

Un TTL ne garantit jamais la fraîcheur. Il garantit un plafond de péremption, ce qui est une promesse différente et bien plus tenable. Dites-le en revue de conception, plutôt que de laisser « on met en cache 5 minutes » se faire entendre comme « le cache est correct ».

Choisir le moteur, et pourquoi cela change vos options de scaling

ElastiCache prend en charge Memcached, Valkey et Redis OSS. Le moteur choisi détermine quelles actions de scaling existent, donc ce n'est pas une question de préférence.

MemcachedValkey / Redis OSS (cluster mode disabled)Valkey / Redis OSS (cluster mode enabled)
Types de donnéesChaînes et objets simplesComplexes (listes, hashes, sets, sorted sets)Complexes
Multi-threadOuiNonNon
Réplication (read replicas)NonOuiOui
Automatic failoverNonOptionnelObligatoire
Partitionnement des donnéesOui, côté clientNonOui
Online reshardingNonNonOui
Backup et restoreClusters à nœuds : nonOuiOui

Prenez Memcached quand vous voulez le modèle le plus simple possible, de gros nœuds multicœurs et du cache d'objets sans plus. Prenez Valkey ou Redis OSS quand vous avez besoin de réplication, de failover, de persistance, de sorted sets ou de pub/sub. La conséquence pratique : un cluster Memcached ne se répare pas en ajoutant des réplicas, puisqu'il n'en a pas. Ses seuls leviers sont un type de nœud plus gros ou plus de nœuds.

Lire les métriques, puis choisir l'action de scaling

Les questions de scaling ElastiCache sont en général des questions de diagnostic. Une métrique vous dit quelle ressource manque, et la ressource plus le moteur vous donnent l'action.

MétriqueCe qu'elle signifieCe vers quoi elle pointe
CPUUtilizationLe CPU au niveau de l'hôteSur les petits nœuds, le plafond de la charge
EngineCPUUtilizationL'usage du cœur moteur uniqueLe vrai signal sur les nœuds à 4 vCPU ou plus
EvictionsDes clés retirées pour faire de la placePas assez de mémoire pour le working set
SwapUsage et FreeableMemoryLa pression mémoireFreeableMemory sous 100 Mo, ou SwapUsage au-dessus de FreeableMemory, veut dire que le nœud est en difficulté
ReplicationLagLe retard d'un réplica sur le primaireLes lectures sur réplica renvoient des données anciennes
TrafficManagementActiveUne valeur de 1 veut dire qu'ElastiCache throttle les commandes entrantesLe nœud est sous-dimensionné pour la charge

Le seuil de CPU piège beaucoup de monde, donc travaillez-le. Valkey et Redis OSS exécutent le moteur sur un seul thread, donc un nœud peut être complètement saturé pendant que le CPU de l'hôte affiche une valeur très inférieure à 100 %. AWS donne le calcul : fixez le seuil à 90 divisé par le nombre de cœurs. Sur un nœud à 2 cœurs, cela fait 45 %. Un cluster à 46 % de CPU avec une latence qui grimpe n'est pas sain et tranquille, il est cloué. Sur les types de nœuds à 4 vCPU ou plus, utilisez EngineCPUUtilization et la division disparaît. Memcached est multi-thread, donc son seuil est bien autour de 90 %.

Une fois la ressource identifiée, l'action découle du moteur et de la charge :

  • Lecture intensive au-dessus du seuil, Valkey ou Redis OSS : ajoutez des read replicas.
  • Écriture intensive, cluster mode disabled : montez en gamme de nœud. Il n'y a qu'un primaire, donc plus d'écritures demandent un primaire plus gros.
  • Écriture intensive, cluster mode enabled : ajoutez des shards, ce qui répartit les écritures sur plus de nœuds primaires.
  • N'importe quelle pression, Memcached : type de nœud plus gros, ou plus de nœuds.

Comment le scaling se produit réellement

En cluster mode enabled, l'online resharding change le nombre de shards pendant que le cluster continue de servir les requêtes. Ajouter des shards augmente la capacité en lecture et en écriture, en retirer réduit le coût, et le rebalancing égalise l'espace de clés entre les shards existants. Trois limites à connaître : les nouveaux shards reçoivent le nombre de nœuds du plus petit shard existant, vous ne pouvez pas fixer les espaces de clés par shard en ligne (cela demande le chemin offline par backup et restore), et les clés dont les éléments dépassent 256 Mo après sérialisation ne sont pas migrées, ce qui peut laisser des shards déséquilibrés. Avant de retirer des shards, ElastiCache vérifie que les shards restants peuvent contenir les données et annule l'opération plutôt que de perdre des clés.

Le scaling vertical par type de nœud est aussi une opération en ligne pour Valkey et Redis OSS. Le resharding offline, la voie backup et restore, est celui qui coupe le service, et vous acceptez cette coupure seulement pour ce qu'il permet en une seule manœuvre : changer le type de nœud, la version du moteur, le nombre de réplicas par shard et les espaces de clés.

ElastiCache Serverless supprime la décision sur les shards. Il suit le CPU, la mémoire et le réseau en continu et ajoute des shards de lui-même, et vous surveillez deux métriques au lieu d'une liste de nœuds : BytesUsedForCache pour le stockage et ElastiCacheProcessingUnits (ECPU) pour le calcul. Vous pouvez plafonner les deux pour borner le coût, à condition de comprendre ce qu'un plafond produit à la limite : atteindre le maximum de stockage fait évincer les clés porteuses de TTL en LRU puis renvoyer des erreurs de mémoire insuffisante, et atteindre le maximum d'ECPU fait throttler les requêtes. AWS recommande une alarme CloudWatch à 75 % de tout maximum que vous fixez, pour l'apprendre avant vos utilisateurs.

Conseils pour l'examen

  • « La même réponse à beaucoup de visiteurs en HTTP » désigne CloudFront. « Un résultat de requête coûteux réutilisé par l'application » désigne ElastiCache. Un énoncé qui décrit une base sous pression de lecture ne parle pas de la périphérie.
  • Devant des chiffres de TTL, faites le recadrage : un max-age sous le Minimum TTL remonte au minimum, au-dessus du Maximum TTL il redescend au maximum, et le Default TTL ne s'applique que si l'origine n'envoie aucun max-age. Les mauvaises réponses reprennent le dernier nombre cité.
  • « L'origine envoie no-store et du contenu périmé est quand même servi » désigne une cache policy dont le Minimum TTL dépasse 0, pas un défaut de CloudFront.
  • 1 000 chemins d'invalidation gratuits par mois et par compte, et un chemin avec joker compte pour un. Si l'énoncé insiste sur des mises à jour fréquentes, la réponse attendue est en général les noms de fichiers versionnés, pas plus d'invalidations.
  • Toute option qui augmente le cache hit ratio en ajoutant des en-têtes, des cookies ou des query strings à la cache key est fausse. Les hits viennent d'une clé plus petite.
  • Le lazy loading autorise des données périmées et survit aux nœuds vides. Le write-through est toujours frais et échoue sur les nœuds vides. Le TTL est ce qui rend viable de faire tourner les deux. Rattachez le symptôme à la stratégie, pas la définition.
  • Valkey et Redis OSS sont mono-thread : le seuil d'alarme CPU vaut 90 divisé par le nombre de cœurs, et EngineCPUUtilization est le signal propre sur les gros nœuds.
  • Memcached n'a ni réplicas, ni failover, ni cluster mode. Toute réponse qui propose l'un des trois pour un cluster Memcached est fausse sur la seule frontière du moteur.
  • Lecture intensive veut dire réplicas, écriture intensive en cluster mode disabled veut dire un nœud plus gros, écriture intensive en cluster mode enabled veut dire plus de shards.

La règle à emporter : nommez ce qui se répète avant de nommer le service. Les réponses HTTP répétées appartiennent à la périphérie, les résultats de requêtes répétés appartiennent à la mémoire à côté de l'application, et ce qui varie réellement à chaque requête n'appartient qu'à la base. Cette dernière catégorie est celle que le cache ne sauve pas, et c'est là que commence la leçon suivante, sur la mise à l'échelle de la base relationnelle elle-même.