0Pricing
Coding Interview Prep · Leçon

Mise en cache, CDN et équilibrage de charge

Ajoutez des couches de mise en cache Redis, envoyez les ressources statiques vers un CDN et répartissez le trafic entre les réplicas avec des équilibreurs en round-robin et par hachage cohérent.

Mise en cache, CDN et équilibrage de charge est une leçon Coding Interview Prep gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.

Pourquoi la mise en cache est essentielle à grande échelle

La mise en cache stocke des copies des données fréquemment consultées dans une couche de stockage plus rapide, afin que les requêtes ultérieures puissent être traitées sans accéder au stockage sous-jacent plus lent (base de données, API externe). À grande échelle, un petit nombre d’éléments populaires reçoit la grande majorité des requêtes — la règle des 80/20 (principe de Pareto) s’applique souvent : 20 % des éléments représentent 80 % du trafic.

Un cache pouvant contenir en mémoire les 20 % de données les plus consultées peut absorber 80 % de la charge de la base de données. C’est pourquoi l’ajout d’un cache Redis réduit souvent de 70 à 90 % l’utilisation du CPU de la base de données et fait passer la latence p99 de 10 ms à moins de 1 ms pour les requêtes servies par le cache — sans modifier significativement la base de données ni la logique de l’application.

# Demonstrating the 80/20 caching benefit
import random

# Simulate 1000 requests to 100 items with Zipf-like distribution
def zipf_sample(n_items, n_requests):
    access_counts = {}
    weights = [1.0 / (i + 1) for i in range(n_items)]  # Zipf: item 0 most popular
    total = sum(weights)
    probs = [w / total for w in weights]
    for _ in range(n_requests):
        item = random.choices(range(n_items), weights=probs)[0]
        access_counts[item] = access_counts.get(item, 0) + 1
    return access_counts

random.seed(42)
counts = zipf_sample(100, 10000)
top_20_items = sorted(counts, key=counts.get, reverse=True)[:20]
top_20_requests = sum(counts[i] for i in top_20_items)
print(f'Top 20% of items ({20} of 100) handle {top_20_requests/100:.1f}% of requests')

Schéma de mise en cache à la demande (chargement différé)

Le schéma de mise en cache à la demande (également appelé chargement différé) est la stratégie de mise en cache la plus courante. Le code de l’application est chargé de gérer le cache : lors d’une lecture, consultez d’abord le cache. Si la donnée est présente dans le cache, renvoyez-la immédiatement. En cas d’absence dans le cache, récupérez-la depuis la base de données, écrivez-la dans le cache, puis renvoyez-la. Lors d’une écriture, mettez à jour la base de données et invalidez (delete) l’entrée du cache afin que la prochaine lecture la recharge.

Ce schéma garantit que le cache ne contient que des données qui ont effectivement été demandées (sans préchargement inutile) et qu’il reste cohérent avec la base de données grâce à l’invalidation. Le compromis : le premier accès après une absence dans le cache paie le coût total de la base de données (démarrage à froid).

# Cache-aside pattern in Python
class CacheAsideService:
    def __init__(self, db, cache):
        self.db = db
        self.cache = cache   # e.g., Redis client

    def get_user(self, user_id):
        cache_key = f'user:{user_id}'
        # 1. Check cache
        cached = self.cache.get(cache_key)
        if cached:
            return cached    # cache hit
        # 2. Cache miss: fetch from DB
        user = self.db.query('SELECT * FROM users WHERE id=%s', user_id)
        # 3. Write to cache with TTL
        self.cache.set(cache_key, user, ttl=3600)  # 1 hour TTL
        return user

    def update_user(self, user_id, data):
        # 1. Write to DB
        self.db.execute('UPDATE users SET ... WHERE id=%s', user_id, data)
        # 2. Invalidate cache (delete, not update)
        self.cache.delete(f'user:{user_id}')
        # Next read will re-populate cache from DB

print('Cache-aside: READ from cache, miss? load from DB + write cache')
print('         WRITE to DB, then DELETE from cache (invalidate)')

Mise en cache avec écriture immédiate et écriture différée

Écriture immédiate : à chaque écriture, mettez à jour simultanément la base de données et le cache. Le cache contient toujours des données à jour. Compromis : les écritures sont plus lentes (deux opérations) et le cache se remplit de données qui ne seront peut-être plus jamais lues.

Écriture différée (écriture en retour) : lors d’une écriture, mettez à jour uniquement le cache, puis transférez les données vers la base de données de manière asynchrone. Les écritures sont ainsi extrêmement rapides, mais une perte de données est possible si le cache tombe en panne avant le transfert. Cette stratégie est utilisée pour les charges comportant beaucoup d’écritures lorsque certaines pertes de données sont acceptables (par exemple, les compteurs de vues et les analyses).

# Write-through vs Write-behind comparison
strategies = {
    'Cache-aside (Lazy)': {
        'read':  'Check cache; miss => DB + populate cache',
        'write': 'Write DB; delete from cache (invalidate)',
        'consistency': 'Strong (invalidation ensures freshness)',
        'write_latency': 'Fast (one DB write)',
        'risk': 'Cache stampede on popular key expiry',
    },
    'Write-through': {
        'read':  'Always check cache; miss => DB',
        'write': 'Write DB AND cache atomically',
        'consistency': 'Strong (cache always has latest)',
        'write_latency': 'Slower (two writes per operation)',
        'risk': 'Cache polluted with rarely-read data',
    },
    'Write-behind': {
        'read':  'Check cache; miss => DB',
        'write': 'Write cache only; async flush to DB',
        'consistency': 'Eventual (flush may be delayed)',
        'write_latency': 'Very fast (cache write only)',
        'risk': 'Data loss if cache crashes before flush',
    },
}
for name, info in strategies.items():
    print(f'\n{name}:')
    for k, v in info.items(): print(f'  {k}: {v}')

Politiques d’expulsion du cache

Lorsque le cache est plein, la politique d’expulsion détermine quelle entrée supprimer. Les politiques les plus courantes sont les suivantes :

  • LRU (la moins récemment utilisée) : expulse l’entrée qui n’a pas été consultée depuis le plus longtemps. Cette politique est performante pour les charges présentant une localité temporelle. Elle est utilisée par Redis par défaut.
  • LFU (la moins fréquemment utilisée) : expulse l’entrée consultée le moins souvent. Elle est préférable lorsque certains éléments restent populaires en permanence, mais que LRU ne permet pas de le prendre en compte.
  • FIFO : expulse l’entrée insérée en premier. C’est simple, mais peu performant pour les charges de travail web classiques.
  • Aléatoire : expulse une entrée choisie au hasard. Cette politique est étonnamment compétitive face à LRU en pratique pour les caches de très grande taille.
# Implementing LRU cache
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = OrderedDict()  # maintains insertion/access order

    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)   # mark as recently used
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)   # evict LRU (oldest)

cache = LRUCache(3)
for k, v in [('a',1),('b',2),('c',3)]:
    cache.put(k, v)
print('Get a:', cache.get('a'))   # 1 (a now most recently used)
cache.put('d', 4)                  # evicts 'b' (LRU)
print('Get b:', cache.get('b'))   # -1 (evicted)
print('Get c:', cache.get('c'))   # 3

Réseaux de diffusion de contenu (CDN)

Un CDN est un réseau géographiquement distribué de serveurs périphériques (points de présence, PoPs) qui mettent en cache des contenus statiques et dynamiques à proximité des utilisateurs finaux. Au lieu que la requête de chaque utilisateur soit acheminée vers un serveur d’origine situé dans un même centre de données, les nœuds périphériques du CDN fournissent le contenu depuis le PoP le plus proche — réduisant la latence d’environ 200 ms (d’un continent à l’autre) à environ 5 ms (depuis un PoP proche).

Les CDN sont essentiels pour les ressources statiques (images, CSS, JS), la diffusion vidéo (segments HLS) et, de plus en plus, les réponses d’interface de programmation ainsi que le HTML généré par le serveur. Le CDN vérifie son cache périphérique ; en cas d’échec du cache, il récupère le contenu depuis l’origine et le met en cache pour les requêtes suivantes.

# CDN architecture flow
cdn_flow = [
    'User requests https://example.com/image.jpg',
    'DNS resolves to the nearest CDN PoP (e.g., Frankfurt for EU users)',
    'CDN edge checks its local cache:',
    '  HIT:  Return cached image directly (5ms latency)',
    '  MISS: Fetch from origin server (e.g., AWS S3 in us-east-1)',
    '        Cache image at edge with Cache-Control: max-age=86400',
    '        Future requests for this image served from edge (HIT)',
    'Cache-Control headers control CDN behaviour:',
    '  max-age=31536000 s-maxage=31536000  -- cache 1 year',
    '  no-cache                             -- always revalidate',
    '  private                              -- CDN must not cache (user-specific)',
]
for step in cdn_flow:
    print(step)

print('\nCDN providers: Cloudflare, AWS CloudFront, Fastly, Akamai')

Répartition de charge : distribution du trafic

Un répartiteur de charge distribue les requêtes entrantes entre plusieurs serveurs applicatifs, empêchant qu’un seul serveur ne devienne un goulot d’étranglement. Il assure également la haute disponibilité : si un serveur tombe en panne, le répartiteur achemine automatiquement le trafic vers les serveurs sains (vérifications d’état toutes les 5 à 30 secondes).

Les répartiteurs de charge fonctionnent à différentes couches de l’OSI : la couche 4 (transport — acheminement selon l’adresse IP et le port, très rapide) et la couche 7 (application — acheminement selon le chemin de la ressource, les en-têtes et les cookies, ce qui permet un routage plus intelligent). AWS ALB, Nginx et HAProxy sont des répartiteurs courants de couche 7. AWS NLB est un répartiteur de couche 4.

# Load balancing algorithms
algorithms = {
    'Round Robin': {
        'how': 'Rotate through servers in sequence',
        'best_for': 'Stateless servers with similar capacity',
        'weakness': 'Does not account for server load or response time',
    },
    'Weighted Round Robin': {
        'how': 'Round robin but servers with more capacity get more requests',
        'best_for': 'Heterogeneous server fleet',
        'weakness': 'Static weights; does not adapt to runtime load',
    },
    'Least Connections': {
        'how': 'Send to server with fewest active connections',
        'best_for': 'Long-lived connections (WebSocket, streaming)',
        'weakness': 'More complex tracking of connection state',
    },
    'Consistent Hashing': {
        'how': 'Hash request key (user_id, session) to server',
        'best_for': 'Sticky sessions, cache locality per server',
        'weakness': 'Uneven distribution if hash space is not balanced',
    },
    'Random': {
        'how': 'Choose server at random',
        'best_for': 'Simple stateless workloads',
        'weakness': 'No guarantee of load balance in short windows',
    },
}
for alg, info in algorithms.items():
    print(f'{alg}: {info["how"]}')

Hachage cohérent : ajout et retrait de nœuds

Le hachage cohérent résout le problème de redistribution des clés du cache lorsque des serveurs sont ajoutés ou retirés. Avec le hachage modulo naïf (server = hash(key) % n), la modification de n remappe presque toutes les clés — ce qui provoque une avalanche du cache (« stampede »). Le hachage cohérent associe les clés et les serveurs à un anneau ; chaque clé est servie par le serveur le plus proche dans le sens horaire. L’ajout d’un serveur ne remappe que les clés situées entre le nouveau serveur et son prédécesseur — environ 1/n de l’ensemble des clés.

Les nœuds virtuels améliorent la répartition de la charge : chaque serveur physique reçoit plusieurs positions sur l’anneau, de sorte que les clés sont réparties plus uniformément, même avec un petit nombre de serveurs.

import hashlib
import bisect

class ConsistentHashRing:
    def __init__(self, replicas=100):
        self.replicas = replicas      # virtual nodes per server
        self.ring = {}
        self.sorted_keys = []

    def add_server(self, server):
        for i in range(self.replicas):
            key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
            self.ring[key] = server
            bisect.insort(self.sorted_keys, key)

    def remove_server(self, server):
        for i in range(self.replicas):
            key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
            del self.ring[key]
            self.sorted_keys.remove(key)

    def get_server(self, item):
        key = int(hashlib.md5(item.encode()).hexdigest(), 16)
        idx = bisect.bisect(self.sorted_keys, key) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

ring = ConsistentHashRing()
for s in ['server-1', 'server-2', 'server-3']:
    ring.add_server(s)
for item in ['user:1', 'user:2', 'product:abc', 'session:xyz']:
    print(f'{item} => {ring.get_server(item)}')

Cache stampede et solutions

Une cache stampede (ou ruée collective) se produit lorsqu’une entrée de cache très consultée expire et que de nombreuses requêtes simultanées ne trouvent plus le cache, saturant la base de données avec la même requête. Solutions :

  • Verrou d’exclusion mutuelle/verrou : une seule requête calcule la valeur ; les autres attendent
  • Expiration anticipée probabiliste : légèrement avant le TTL, une requête choisit random (aléatoirement) de rafraîchir le cache, ce qui empêche une expiration simultanée
  • Contenu obsolète pendant la revalidation : fournir immédiatement le contenu obsolète tout en actualisant le cache de manière asynchrone
  • Actualisation en arrière-plan : un processus distinct actualise les clés populaires avant leur expiration
import time, threading, random

# Probabilistic early expiry (XFetch algorithm)
class ProbabilisticCache:
    def __init__(self):
        self._cache = {}

    def get(self, key, ttl, recompute_fn, beta=1.0):
        if key in self._cache:
            value, expiry, delta = self._cache[key]
            # XFetch: decide to refresh early with probability proportional to delta/TTL
            remaining = expiry - time.time()
            if remaining > 0:
                early_refresh_score = delta * beta * (-1) * (remaining / ttl)
                if random.random() > (1 - early_refresh_score):  # simplified
                    pass  # could trigger async refresh here
                return value
        # Cache miss or expired
        start = time.time()
        value = recompute_fn()
        delta = time.time() - start          # computation time
        expiry = time.time() + ttl
        self._cache[key] = (value, expiry, delta)
        return value

print('XFetch: refresh probabilistically before expiry based on computation cost')
print('High-cost computations => refresh earlier to avoid stampede')
print('Low-cost computations => refresh closer to TTL')

Invalidation du cache CDN

L’invalidation du cache est réputée particulièrement difficile : « Il n’y a que deux problèmes difficiles en informatique : l’invalidation du cache et le nommage des choses. » Lorsque le contenu change à l’origine, les nœuds périphériques du CDN doivent fournir la nouvelle version. Stratégies :

  • Expiration fondée sur le TTL : laisser le contenu expirer naturellement (simple, mais avec une fenêtre de contenu obsolète)
  • Versionnement des adresses : intégrer le hachage du contenu dans l’adresse (par ex. main.a3f2b.js) ; nouveau contenu = nouvelle adresse, aucune invalidation nécessaire
  • Purge du CDN via une interface de programmation : purger explicitement les adresses via un appel call après le déploiement (rapide, mais nécessite l’intégration à l’interface de programmation du CDN)
# Cache invalidation strategies for CDN/browser
strategies = [
    {
        'name': 'Long TTL + URL versioning (best for static assets)',
        'example': '<script src="/app.a3f2b1c.js"></script>',
        'ttl': 'Cache-Control: max-age=31536000 (1 year)',
        'how': 'Content hash in filename; new deploy = new URL; old URL cached forever (OK)',
    },
    {
        'name': 'Short TTL (for frequently changing content)',
        'example': '/api/v1/config',
        'ttl': 'Cache-Control: max-age=60 (1 minute)',
        'how': 'Simple; content is at most 60s stale; no invalidation needed',
    },
    {
        'name': 'CDN API purge (for news / social media)',
        'example': '/news/breaking-story.html',
        'ttl': 'Cache-Control: s-maxage=3600',
        'how': 'On publish, call CDN.purge(url); edge serves new version immediately',
    },
]
for s in strategies:
    print(f'{s["name"]}:')
    print(f'  Example: {s["example"]}')
    print(f'  TTL: {s["ttl"]}')
    print(f'  Strategy: {s["how"]}\n')

Architecture : assemblage de l’ensemble

Une couche d’application web entièrement dimensionnée utilise les trois techniques ensemble : la répartition de charge distribue le trafic, le CDN absorbe les requêtes statiques et les requêtes d’interface de programmation pouvant être mises en cache, et Redis met en cache les données dynamiques. La base de données ne reçoit que les requêtes qui ne trouvent pas leur contenu dans le cache — généralement 5 à 20 % des requêtes.

Flux typique d’une requête pour une interface de programmation à dominante lecture : utilisateur → DNS → périphérie du CDN (succès du cache : fournie immédiatement) → échec du cache du CDN → répartiteur de charge → grappe de serveurs applicatifs → cache Redis (succès : réponse en 1 ms) → échec du cache Redis → base de données (10 à 50 ms) → réponse mise en cache dans Redis + éventuellement dans le CDN → utilisateur. Chaque couche réduit considérablement la charge de la base de données.

# Request flow with cache hit rates
request_flow = [
    ('Browser Cache',       '10%',  '0ms',   'Browser caches GET responses per Cache-Control'),
    ('CDN Edge Cache',      '60%',  '5ms',   'CloudFront/Fastly caches cacheable API responses'),
    ('Load Balancer',        None,  '1ms',   'Routes to healthy app server replica'),
    ('App Server',           None,  '2ms',   'Business logic, auth check'),
    ('Redis Cache',         '25%',  '1ms',   'Caches computed data, hot DB rows'),
    ('Database Read Replica','5%',  '10ms',  'Cache miss: query read replica'),
    ('Database Primary',    '0.1%', '15ms',  'Cache+replica miss: query primary (rare for reads)'),
]
print(f'{'Layer':30s} {'Hit Rate':10s} {'Latency':10s} {'Notes'}')
print('-'*80)
for layer, hit_rate, latency, note in request_flow:
    hr = hit_rate if hit_rate else '-'
    print(f'{layer:30s} {hr:10s} {latency:10s} {note}')
print('\nResult: DB sees ~5% of requests; Redis sees ~25%; CDN absorbs 60%; browser 10%')

Conseils d’entretien : mise en cache et répartition de charge

Lorsque vous abordez la mise en cache lors d’un entretien de conception de système, traitez toujours les points suivants : quoi mettre en cache (données très consultées, calculs coûteux), où mettre en cache (navigateur, CDN, application, cache des requêtes de base de données), quand invalider le cache (lors d’une écriture, à l’expiration du TTL ou au moyen d’une actualisation en arrière-plan) et quelles garanties de cohérence sont acceptables. Un cache introduit une fenêtre de cohérence : indiquez-la explicitement.

Pour la répartition de charge, mentionnez le choix de l’algorithme, les vérifications d’état, les sessions persistantes si nécessaire, ainsi que la possibilité d’une mise à l’échelle horizontale des serveurs applicatifs sans état. Si l’application possède un état (connexions WebSocket, sessions), expliquez comment cet état est géré entre les répliques.

# Caching design questions checklist
cache_checklist = [
    'What data to cache? (read-heavy, expensive to compute, rarely updated)',
    'Cache layer: client-side / CDN / app-level / DB query cache?',
    'Cache invalidation strategy: TTL / event-driven / write-through?',
    'Eviction policy: LRU / LFU?',
    'Cache key design: ensure uniqueness, avoid hotspots',
    'Consistency window: acceptable staleness in seconds?',
    'Cache stampede prevention: mutex / stale-while-revalidate?',
    'Cache capacity: how much RAM needed for hot set?',
]
lb_checklist = [
    'Layer 4 vs Layer 7: routing by IP or by URL/headers?',
    'Algorithm: round robin / least-connections / consistent hashing?',
    'Health checks: interval, failure threshold, recovery',
    'Session stickiness: needed? Use cookie-based affinity or external session store',
    'Auto-scaling: scale out when CPU > 70%; scale in when < 30%',
]
print('Cache checklist:')
for item in cache_checklist: print(f'  [ ] {item}')
print('\nLoad balancer checklist:')
for item in lb_checklist: print(f'  [ ] {item}')

Vérification rapide

Vérifiez votre compréhension des concepts des structures de données & algorithmes — préparation à l’entretien de programmation — présentés dans cette leçon.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris : la mise en cache stocke les données très consultées dans des couches de mémoire rapides (Redis, CDN) afin d’absorber la majorité des lectures et de réduire la charge de la base de données, le modèle avec lecture depuis le cache est le plus courant — un échec signifie charger depuis la base de données, un succès signifie renvoyer immédiatement, et une écriture signifie supprimer du cache, et le hachage cohérent répartit les clés du cache entre les nœuds, de sorte que l’ajout ou le retrait de nœuds ne remappe qu’environ 1/n des clés au lieu de tout remapper. Ensuite, nous concevrons un limiteur de débit et un fil Twitter afin d’appliquer tous les concepts de conception de système à des problèmes de bout en bout.

Questions Fréquemment Posées

La leçon « Mise en cache, CDN et équilibrage de charge » est-elle gratuite ?

Oui — le texte complet de « Mise en cache, CDN et équilibrage de charge » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Mise en cache, CDN et équilibrage de charge » ?

Ajoutez des couches de mise en cache Redis, envoyez les ressources statiques vers un CDN et répartissez le trafic entre les réplicas avec des équilibreurs en round-robin et par hachage cohérent. Tu pratiques Coding Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Coding Interview Prep ?

Aucune expérience préalable n'est requise. Coding Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.

Combien de temps prend la leçon « Mise en cache, CDN et équilibrage de charge » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Coding Interview Prep ?

Oui. Chaque leçon Coding Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Cadre d’entretien de conception de systèmes
  2. Stockage de données évolutif : SQL ou NoSQL
  3. Mise en cache, CDN et équilibrage de charge
  4. Concevoir un limiteur de débit et un fil Twitter
← Retour à Coding Interview Prep