Caching, CDN'er og load balancing
Tilføj Redis-cachelag, send statiske aktiver til et CDN, og fordel trafikken mellem replikaer med round-robin- og consistent-hashing-loadbalancere.
Caching, CDN'er og load balancing er en gratis Forberedelse til kodeinterviews-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Forberedelse til kodeinterviews, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.
Hvorfor cachelagring er afgørende i stor skala
Cachelagring gemmer kopier af data, der tilgås ofte, i et hurtigere lagerlag, så fremtidige forespørgsler kan besvares uden at ramme det langsommere bagvedliggende lager (database, ekstern API). Ved stor skala modtager et lille antal populære elementer langt størstedelen af forespørgslerne — 80/20-reglen (Pareto-princippet) gælder ofte: 20 % af elementerne står for 80 % af trafikken.
En cache, der kan have de 20 % mest efterspurgte elementer i hukommelsen, kan absorbere 80 % af belastningen på databasen. Derfor reducerer tilføjelsen af en Redis-cache ofte databasens CPU-forbrug med 70–90 % og sænker p99-latensen fra 10 ms til under 1 ms ved cachetræf — uden væsentlige ændringer af databasen eller applikationslogikken.
# 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')Cache-aside-mønsteret (doven indlæsning)
Cache-aside-mønsteret (også kaldet doven indlæsning) er den mest almindelige cachestrategi. Applikationskoden er ansvarlig for at administrere cachen: ved læsning kontrolleres cachen først. Ved et cachetræf returneres resultatet straks. Ved et cachemiss hentes data fra databasen, skrives til cachen og returneres derefter. Ved en skrivning opdateres databasen, og cacheposten ugyldiggøres (slettes), så næste læsning opdaterer den.
Dette mønster sikrer, at cachen kun indeholder data, der faktisk er blevet efterspurgt (ingen unødvendig forudindlæsning), og at den forbliver konsistent med databasen via ugyldiggørelse. Afvejningen er, at den første adgang efter et cachemiss betaler den fulde databaseomkostning (kold start).
# 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)')Write-through- og write-behind-cachelagring
Write-through: ved hver skrivning opdateres både databasen og cachen synkront. Cachen indeholder altid aktuelle data. Afvejning: skrivninger er langsommere (to operationer), og cachen fyldes med data, der måske aldrig læses igen.
Write-behind (write-back): ved en skrivning opdateres kun cachen; data skrives asynkront til databasen senere. Det gør skrivninger ekstremt hurtige, men medfører risiko for datatab, hvis cachen svigter, før dataene er skrevet til databasen. Bruges til skriveintensive arbejdsbelastninger, hvor et vist datatab er acceptabelt (f.eks. visningstællere og analyse).
# 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}')Politikker for fjernelse fra cachen
Når cachen er fuld, afgør politikken for fjernelse, hvilken post der skal fjernes. De mest almindelige politikker er:
- LRU (mindst nyligt brugte): fjern den post, der ikke er blevet tilgået i længst tid. Fungerer godt ved arbejdsbelastninger med tidsmæssig lokalitet. Bruges som standard af Redis.
- LFU (mindst hyppigt brugte): fjern den post, der er blevet tilgået færrest gange. Bedre til arbejdsbelastninger, hvor nogle elementer er permanent populære, men LRU ikke ville opfange det.
- FIFO: fjern den først indsatte post. Enkelt, men giver dårlig ydeevne ved typiske webarbejdsbelastninger.
- Tilfældig: fjern en tilfældig post. Er overraskende konkurrencedygtig med LRU i praksis ved meget store caches.
# 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')) # 3Indholdsleveringsnetværk (CDN'er)
Et CDN er et geografisk distribueret netværk af edge-servere (tilstedeværelsespunkter, PoP'er), der cachelagrer statisk og dynamisk indhold tæt på slutbrugerne. I stedet for at hver brugers forespørgsel skal hele vejen til en oprindelsesserver i ét datacenter, leverer CDN-edgeknuder indholdet fra den nærmeste PoP — hvilket reducerer latenstiden fra ~200 ms (på tværs af kontinenter) til ~5 ms (til en nærliggende PoP).
CDN'er er afgørende for statiske ressourcer (billeder, CSS, JS), videostreaming (HLS-segmenter) og i stigende grad API-svar og serverrenderet HTML. CDN'et tjekker sin edge-cache; ved et cache-miss henter det indholdet fra oprindelsesserveren og cachelagrer det til fremtidige forespørgsler.
# 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')Belastningsfordeling: Fordeling af trafik
En belastningsfordeler fordeler indgående forespørgsler på flere bagvedliggende servere, så ingen enkelt server bliver en flaskehals. Den sørger også for høj tilgængelighed: Hvis en server fejler, dirigerer belastningsfordeleren automatisk trafikken til raske servere (sundhedstjek hvert 5. til 30. sekund).
Belastningsfordelere fungerer på forskellige OSI-lag: Lag 4 (transport — dirigerer efter IP/port og er meget hurtigt) og lag 7 (applikation — dirigerer efter URL-sti, headere og cookies, hvilket muliggør mere intelligent dirigering). AWS ALB, Nginx og HAProxy er almindelige belastningsfordelere på lag 7. AWS NLB er en belastningsfordeler på lag 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"]}')Konsistent hashing: Tilføjelse og fjernelse af noder
Konsistent hashing løser problemet med at omfordele cache-nøgler, når servere tilføjes eller fjernes. Ved naiv modulo-hashing (server = hash(key) % n) omfordeler en ændring af n næsten alle nøgler — hvilket udløser en cache-storm. Konsistent hashing afbilder både nøgler og servere på en ring; hver nøgle betjenes af den nærmeste server med uret. Når en server tilføjes, omfordeles kun nøglerne mellem den nye server og dens forgænger — omkring 1/n af alle nøgler.
Virtuelle noder (vnodes) forbedrer fordelingen af belastningen: Hver fysisk server tildeles flere positioner på ringen, så nøglerne fordeles mere jævnt, selv når der kun er få servere.
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-storm og løsninger
En cache-storm (eller tordnende flok) opstår, når et populært cache-element udløber, og mange samtidige forespørgsler rammer et cache-miss på samme tid, så databasen oversvømmes med den samme forespørgsel. Løsninger:
- Mutex/lås: Kun én forespørgsel beregner værdien; resten venter
- Sandsynlighedsbaseret tidligt udløb: Kort før TTL udløber, afgør en forespørgsel tilfældigt, om cachen skal opdateres, så samtidigt udløb forhindres
- Forældet under genvalidering: Servér straks forældet indhold, mens cachen opdateres asynkront
- Baggrundsopdatering: En separat proces opdaterer populære nøgler, før de udløber
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')Ugyldiggørelse af CDN-cache
Cache-invalidering er berømt for at være vanskeligt: „Der er kun to vanskelige problemer i datalogi: cache-invalidering og navngivning af ting.“ Når indhold ændres på oprindelsesserveren, skal CDN-edgeknuderne levere den nye version. Strategier:
- TTL-baseret udløb: Lad indholdet udløbe naturligt (enkelt, men med et vindue med forældet indhold)
- URL-versionering: Indlejrer en indholdshash i URL'en (f.eks.
main.a3f2b.js); nyt indhold = ny URL, så ingen invalidering er nødvendig - Tømning via CDN-API: Tøm URL'er eksplicit via et API-kald efter udrulning (hurtigt, men kræver integration med CDN-API'et)
# 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')Arkitektur: Det hele samlet
Et fuldt skaleret webapplikationslag bruger alle tre teknikker sammen: Belastningsfordeling fordeler trafikken, CDN'et absorberer statiske og cachelagrede API-forespørgsler, og Redis cachelagrer dynamiske data. Databasen ser kun cache-misses — typisk 5-20 % af forespørgslerne.
Et typisk forespørgselsforløb for en læsetung API er: bruger → DNS → CDN-edge (cache-hit: leveres straks) → CDN-miss → belastningsfordeler → pulje af applikationsservere → Redis-cache (cache-hit: svar på 1 ms) → Redis-miss → database (10-50 ms) → svar cachelagres i Redis + eventuelt CDN'et → bruger. Hvert lag reducerer databasebelastningen betydeligt.
# 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%')Interviewtips: Cachelagring og belastningsfordeling
Når du taler om cachelagring i et interview om systemdesign, skal du altid komme ind på: hvad der skal cachelagres (populære data, beregninger, der er dyre at udføre), hvor der skal cachelagres (browser, CDN, applikation, cache til databaseforespørgsler), hvornår cachen skal ugyldiggøres (ved skrivning, ved TTL-udløb eller via baggrundsopdatering) og hvilke konsistensgarantier der er acceptable. En cache indfører et konsistensvindue — vær tydelig om det.
For belastningsfordeling bør du nævne valget af algoritme, sundhedstjek, sticky sessioner (om nødvendigt) og om horisontal skalering af tilstandsløse applikationsservere er mulig. Hvis applikationen har tilstand (WebSocket-forbindelser, sessioner), skal du beskrive, hvordan denne tilstand håndteres på tværs af replikaer.
# 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}')Hurtigt tjek
Afprøv din forståelse af begreberne i Data Structures & Algorithms — Coding Interview Prep fra denne lektion.
Lektionsopsamling
I denne lektion lærte du, at cachelagring gemmer populære data i hurtige hukommelseslag (Redis, CDN) for at håndtere hovedparten af læsningerne og reducere databasebelastningen, at cache-aside er det mest almindelige mønster — et cache-miss betyder, at data indlæses fra databasen, et cache-hit betyder, at data returneres med det samme, og ved skrivning slettes data fra cachen, og at konsistent hashing fordeler cache-nøgler på tværs af noder, så tilføjelse eller fjernelse af noder kun omfordeler ~1/n af nøglerne i stedet for alt. I næste lektion designer vi en hastighedsbegrænser og et Twitter-feed, så du kan anvende alle begreberne inden for systemdesign i komplette problemer.
Lær Forberedelse til kodeinterviews med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 90
- Lektioner
- 360
Ofte stillede spørgsmål
Er lektionen “Caching, CDN'er og load balancing” gratis?
Ja — hele teksten til “Caching, CDN'er og load balancing” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Forberedelse til kodeinterviews-kurset, skal du opgradere til CoddyKit PRO. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Caching, CDN'er og load balancing”?
Tilføj Redis-cachelag, send statiske aktiver til et CDN, og fordel trafikken mellem replikaer med round-robin- og consistent-hashing-loadbalancere. Du øver dig i Forberedelse til kodeinterviews med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Forberedelse til kodeinterviews?
Der kræves ingen tidligere erfaring. Forberedelse til kodeinterviews på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.
Hvor lang tid tager lektionen “Caching, CDN'er og load balancing”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Forberedelse til kodeinterviews-lektion?
Ja. Alle Forberedelse til kodeinterviews-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Framework til systemdesigninterviews
- Skalerbar datalagring: SQL eller NoSQL
- Caching, CDN'er og load balancing
- Design af ratebegrænser og Twitter-feed