Buforowanie, CDN-y i równoważenie obciążenia
Dodawać warstwy buforowania Redis, przenosić zasoby statyczne do CDN-u oraz rozdzielać ruch między repliki za pomocą modułów równoważenia obciążenia round-robin i consistent hashing
Buforowanie, CDN-y i równoważenie obciążenia to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Dlaczego buforowanie jest niezbędne przy dużej skali
Buforowanie przechowuje kopie często używanych danych w szybszej warstwie pamięci, dzięki czemu przyszłe żądania mogą zostać obsłużone bez odwoływania się do wolniejszego magazynu źródłowego (bazy danych lub zewnętrznego API). Przy dużej skali niewielka liczba popularnych elementów otrzymuje zdecydowaną większość żądań — często ma zastosowanie reguła 80/20 (zasada Pareta): 20% elementów odpowiada za 80% ruchu.
Pamięć podręczna, która mieści w pamięci najczęściej używane 20% danych, może przejąć 80% obciążenia bazy danych. Dlatego dodanie pamięci podręcznej Redis często zmniejsza użycie CPU bazy danych o 70–90% i skraca opóźnienie p99 z 10 ms do mniej niż 1 ms w przypadku trafień do pamięci podręcznej — bez znaczących zmian w bazie danych ani logice aplikacji.
# 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')Wzorzec cache-aside (leniwe ładowanie)
Wzorzec cache-aside (nazywany również leniwym ładowaniem) jest najczęściej stosowaną strategią buforowania. Kod aplikacji odpowiada za zarządzanie pamięcią podręczną: podczas odczytu najpierw sprawdza pamięć podręczną. W przypadku trafienia do pamięci podręcznej natychmiast zwraca wynik. W przypadku braku danych w pamięci podręcznej pobiera dane z bazy, zapisuje je w pamięci podręcznej, a następnie zwraca wynik. Podczas zapisu aktualizuje bazę danych i unieważnia (usuwa) wpis w pamięci podręcznej, aby następny odczyt go odświeżył.
Ten wzorzec zapewnia, że pamięć podręczna zawiera tylko dane, o które rzeczywiście poproszono (bez niepotrzebnego wstępnego ładowania), i pozostaje spójna z bazą danych dzięki unieważnianiu. Kompromisem jest to, że pierwszy dostęp po braku danych w pamięci podręcznej wiąże się z pełnym kosztem odczytu z bazy danych (zimny 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)')Buforowanie write-through i write-behind
Write-through: przy każdym zapisie synchronicznie aktualizuje się zarówno bazę danych, jak i pamięć podręczną. Pamięć podręczna zawsze zawiera aktualne dane. Kompromis: zapisy są wolniejsze (dwie operacje), a pamięć podręczna zapełnia się danymi, które mogą już nigdy nie zostać odczytane.
Write-behind (write-back): podczas zapisu aktualizuje się wyłącznie pamięć podręczną, a dane są później asynchronicznie zapisywane w bazie danych. Dzięki temu zapisy są niezwykle szybkie, ale istnieje ryzyko utraty danych, jeśli pamięć podręczna ulegnie awarii przed wykonaniem zapisu. Rozwiązanie to stosuje się w obciążeniach z przewagą zapisów, w których pewna utrata danych jest akceptowalna (np. liczniki wyświetleń, analityka).
# 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}')Zasady usuwania wpisów z pamięci podręcznej
Gdy pamięć podręczna jest pełna, zasada usuwania wpisów decyduje o tym, który wpis zostanie usunięty. Najczęściej stosowane zasady to:
- LRU (Least Recently Used): usuwa wpis, do którego najdawniej uzyskano dostęp. Dobrze sprawdza się w obciążeniach charakteryzujących się lokalnością czasową. Jest domyślnie używana przez Redis.
- LFU (Least Frequently Used): usuwa wpis, do którego uzyskiwano dostęp najmniejszą liczbę razy. Lepiej sprawdza się w obciążeniach, w których niektóre elementy są stale popularne, ale LRU nie potrafiłoby tego uchwycić.
- FIFO: usuwa najdawniej dodany wpis. Jest prosta, ale zapewnia słabą wydajność w typowych obciążeniach internetowych.
- Random: usuwa losowy wpis. W praktyce przy bardzo dużych pamięciach podręcznych jest zaskakująco konkurencyjna wobec LRU.
# 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')) # 3Sieci dostarczania treści (CDN)
CDN to geograficznie rozproszona sieć serwerów brzegowych (punktów obecności, PoP), które buforują treści statyczne i dynamiczne w pobliżu użytkowników końcowych. Zamiast kierować żądanie każdego użytkownika do serwera źródłowego w jednym centrum danych, węzły brzegowe CDN dostarczają treść z najbliższego punktu obecności — zmniejszając opóźnienie z około 200 ms (przy połączeniu między kontynentami) do około 5 ms (w przypadku pobliskiego punktu obecności).
CDN-y są niezbędne do obsługi zasobów statycznych (obrazów, CSS, JS), strumieniowego przesyłania wideo (segmentów HLS), a coraz częściej także odpowiedzi API i kodu HTML renderowanego po stronie serwera. CDN sprawdza swój cache brzegowy; przy braku trafienia pobiera treść ze źródła i zapisuje ją w cache na potrzeby przyszłych żądań.
# 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ównoważenie obciążenia: rozdzielanie ruchu
Moduł równoważenia obciążenia rozdziela przychodzące żądania między wiele serwerów backendowych, zapobiegając przeciążeniu pojedynczego serwera. Zapewnia również wysoką dostępność: jeśli jeden serwer ulegnie awarii, moduł równoważenia obciążenia automatycznie kieruje ruch do sprawnych serwerów (kontrole stanu co 5–30 sekund).
Moduły równoważenia obciążenia działają na różnych warstwach modelu OSI: warstwie 4 (transportowej — kierowanie na podstawie adresu IP i portu, bardzo szybkie) oraz warstwie 7 (aplikacji — kierowanie na podstawie ścieżki URL, nagłówków i plików cookie, co pozwala na bardziej inteligentne trasowanie). AWS ALB, Nginx i HAProxy to popularne moduły równoważenia obciążenia warstwy 7. AWS NLB jest modułem równoważenia obciążenia warstwy 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"]}')Haszowanie spójne: dodawanie i usuwanie węzłów
Haszowanie spójne rozwiązuje problem ponownego rozdzielania kluczy cache po dodaniu lub usunięciu serwerów. W naiwnym haszowaniu modulo (server = hash(key) % n) zmiana n powoduje ponowne przypisanie niemal wszystkich kluczy — prowadząc do lawiny żądań do cache. Haszowanie spójne umieszcza zarówno klucze, jak i serwery na pierścieniu; każdy klucz jest obsługiwany przez najbliższy serwer zgodnie z ruchem wskazówek zegara. Dodanie serwera powoduje ponowne przypisanie tylko kluczy znajdujących się między nowym serwerem a jego poprzednikiem — około 1/n wszystkich kluczy.
Węzły wirtualne (vnodes) poprawiają rozkład obciążenia: każdemu serwerowi fizycznemu przypisuje się wiele pozycji na pierścieniu, dzięki czemu klucze są rozłożone bardziej równomiernie nawet przy małej liczbie serwerów.
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)}')Lawina żądań do cache i rozwiązania
Lawina żądań do cache (lub efekt tłumu) występuje, gdy popularny wpis w cache wygasa, a wiele współbieżnych żądań jednocześnie nie znajduje go w cache, zalewając bazę danych tym samym zapytaniem. Rozwiązania:
- Mutex/lock: tylko jedno żądanie oblicza wartość, a pozostałe czekają
- Probabilistyczne wcześniejsze wygasanie: krótko przed upływem TTL żądanie losowo decyduje o odświeżeniu cache, zapobiegając jednoczesnemu wygaśnięciu
- Stale-while-revalidate: natychmiast udostępnia nieaktualną treść, jednocześnie asynchronicznie odświeżając cache
- Odświeżanie w tle: oddzielny proces odświeża popularne klucze przed ich wygaśnięciem
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')Unieważnianie cache CDN
Unieważnianie cache jest słynne ze swojej trudności: 'Istnieją tylko dwa trudne problemy w informatyce: unieważnianie cache i nazywanie rzeczy.' Gdy treść zmienia się w źródle, węzły brzegowe CDN muszą udostępniać nową wersję. Strategie:
- Wygasanie na podstawie TTL: pozwól treści wygasnąć naturalnie (proste rozwiązanie, ale pozostawia okno nieaktualności)
- Wersjonowanie URL: umieść hash treści w URL (np.
main.a3f2b.js); nowa treść oznacza nowy URL, więc unieważnianie nie jest potrzebne - Czyszczenie za pomocą API CDN: jawnie usuń adresy URL za pomocą wywołania API po wdrożeniu (szybkie rozwiązanie, ale wymaga integracji z API 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')Architektura: połączenie wszystkich elementów
W pełni skalowalna warstwa aplikacji internetowej korzysta ze wszystkich trzech technik: równoważenie obciążenia rozdziela ruch, CDN przejmuje obsługę statycznych i cacheowalnych żądań API, a Redis buforuje dane dynamiczne. Baza danych obsługuje tylko żądania, przy których nie ma trafienia w cache — zazwyczaj 5–20% wszystkich żądań.
Typowy przepływ żądania dla API z przewagą odczytów: użytkownik → DNS → węzeł brzegowy CDN (trafienie w cache: natychmiastowa odpowiedź) → brak trafienia w CDN → moduł równoważenia obciążenia → pula serwerów aplikacji → cache Redis (trafienie: odpowiedź w 1 ms) → brak trafienia w Redis → baza danych (10–50 ms) → odpowiedź zapisana w Redis + opcjonalnie w CDN → użytkownik. Każda warstwa znacząco zmniejsza obciążenie bazy danych.
# 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%')Wskazówki rekrutacyjne: cache i równoważenie obciążenia
Podczas omawiania cache w wywiadzie dotyczącym projektowania systemów należy zawsze uwzględnić: co buforować (często używane dane, kosztowne obliczenia), gdzie buforować (przeglądarka, CDN, aplikacja, cache zapytań do bazy danych), kiedy unieważniać (przy zapisie, po wygaśnięciu TTL lub za pomocą odświeżania w tle) oraz jakie gwarancje spójności są akceptowalne. Cache wprowadza okno niespójności — należy wyraźnie je określić.
W przypadku równoważenia obciążenia należy wspomnieć o wyborze algorytmu, kontrolach stanu, sesjach sticky (jeśli są potrzebne) oraz o tym, czy możliwe jest horyzontalne skalowanie bezstanowych serwerów aplikacji. Jeśli aplikacja przechowuje stan (połączenia WebSocket, sesje), należy wyjaśnić, jak ten stan jest zarządzany między replikami.
# 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}')Szybki test
Proszę sprawdzić swoją znajomość koncepcji Data Structures & Algorithms — Coding Interview Prep z tej lekcji.
Podsumowanie lekcji
W tej lekcji dowiedzieli się Państwo, że: cache przechowuje często używane dane w szybkich warstwach pamięci (Redis, CDN), aby przejąć większość odczytów i zmniejszyć obciążenie bazy danych, cache-aside to najczęściej używany wzorzec — brak trafienia oznacza wczytanie danych z bazy, trafienie oznacza natychmiastowy zwrot danych, a zapis oznacza usunięcie ich z cache oraz haszowanie spójne rozdziela klucze cache między węzły, dzięki czemu dodanie lub usunięcie węzłów powoduje ponowne przypisanie tylko około 1/n kluczy zamiast wszystkich. W następnej części zaprojektujemy ogranicznik liczby żądań i kanał Twittera, aby zastosować wszystkie koncepcje projektowania systemów w kompleksowych zadaniach.
Często zadawane pytania
Czy lekcja „Buforowanie, CDN-y i równoważenie obciążenia” jest bezpłatna?
Tak — pełny tekst „Buforowanie, CDN-y i równoważenie obciążenia” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Co nauczysz się w „Buforowanie, CDN-y i równoważenie obciążenia”?
Dodawać warstwy buforowania Redis, przenosić zasoby statyczne do CDN-u oraz rozdzielać ruch między repliki za pomocą modułów równoważenia obciążenia round-robin i consistent hashing Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?
Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Buforowanie, CDN-y i równoważenie obciążenia”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?
Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Schemat rozmowy technicznej o projektowaniu systemów
- Skalowalne przechowywanie danych: SQL a NoSQL
- Buforowanie, CDN-y i równoważenie obciążenia
- Projektowanie ogranicznika przepustowości i kanału Twittera