Caché, CDN y balanceo de carga
Añada capas de caché con Redis, envíe los recursos estáticos a una CDN y distribuya el tráfico entre réplicas mediante balanceadores de carga round-robin y de hashing consistente.
Caché, CDN y balanceo de carga es una lección gratuita de Coding Interview Prep en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Coding Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Coding Interview Prep incluye 4 lecciones en total.
Por qué la caché es esencial a gran escala
La caché almacena copias de los datos a los que se accede con frecuencia en una capa de almacenamiento más rápida, de modo que las solicitudes futuras puedan atenderse sin acceder al almacén subyacente más lento (la base de datos o una API externa). A gran escala, un pequeño número de elementos populares recibe la mayoría de las solicitudes; a menudo se aplica la regla 80/20 (principio de Pareto): el 20 % de los elementos representa el 80 % del tráfico.
Una caché que pueda contener en memoria el 20 % de los elementos más solicitados puede absorber el 80 % de la carga de la base de datos. Por eso añadir una caché de Redis suele reducir entre un 70 % y un 90 % el uso de CPU de la base de datos y disminuir la latencia p99 de 10 ms a menos de 1 ms en los aciertos de caché, sin cambiar significativamente la base de datos ni la lógica de la aplicación.
# 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')Patrón cache-aside (carga diferida)
El patrón cache-aside (también llamado carga diferida) es la estrategia de caché más habitual. El código de la aplicación se encarga de gestionar la caché: al leer, compruebe primero la caché. Si hay un acierto de caché, devuelva el resultado inmediatamente. Si hay un fallo de caché, obtenga los datos de la base de datos, escríbalos en la caché y devuélvalos. Al escribir, actualice la base de datos e invalide (elimine) la entrada de la caché para que la siguiente lectura la actualice.
Este patrón garantiza que la caché solo contenga datos que se hayan solicitado realmente (sin precarga innecesaria) y que se mantenga coherente con la base de datos mediante la invalidación. El compromiso es que el primer acceso después de un fallo de caché asume el coste completo de la base de datos (arranque en frío).
# 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)')Caché de escritura directa y escritura diferida
Escritura directa: en cada escritura, actualice la base de datos y la caché de forma síncrona. La caché siempre contiene datos actualizados. El compromiso es que las escrituras son más lentas (dos operaciones) y que la caché se llena de datos que quizá no vuelvan a leerse.
Escritura diferida: al escribir, actualice solo la caché y vuelque los datos en la base de datos de forma asíncrona más tarde. Esto hace que las escrituras sean extremadamente rápidas, pero conlleva riesgo de pérdida de datos si la caché falla antes del volcado. Se utiliza en cargas de trabajo con muchas escrituras en las que se acepta cierta pérdida de datos (por ejemplo, contadores de visualizaciones y análisis).
# 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}')Políticas de expulsión de la caché
Cuando la caché está llena, la política de expulsión decide qué entrada eliminar. Las políticas más habituales son:
- LRU (menos recientemente utilizada): expulsa la entrada a la que no se ha accedido durante más tiempo. Funciona bien con cargas de trabajo que presentan localidad temporal. Redis la utiliza de forma predeterminada.
- LFU (menos frecuentemente utilizada): expulsa la entrada a la que se ha accedido menos veces. Es mejor para cargas de trabajo en las que algunos elementos son populares de forma permanente, algo que LRU no captaría.
- FIFO: expulsa la entrada insertada hace más tiempo. Es sencilla, pero ofrece un rendimiento bajo en las cargas de trabajo web habituales.
- Aleatoria: expulsa una entrada al azar. En la práctica, resulta sorprendentemente competitiva frente a LRU en cachés muy grandes.
# 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')) # 3Redes de distribución de contenido (CDN)
Una CDN es una red distribuida geográficamente de servidores perimetrales (puntos de presencia, PoPs) que almacenan en caché contenido estático y dinámico cerca de los usuarios finales. En lugar de que la solicitud de cada usuario viaje a un servidor de origen en un único centro de datos, los nodos perimetrales de la CDN sirven el contenido desde el PoP más cercano — lo que reduce la latencia de unos 200 ms (entre continentes) a unos 5 ms (PoP cercano).
Las CDN son esenciales para recursos estáticos (imágenes, CSS, JS), la transmisión de vídeo (segmentos HLS) y, cada vez más, las respuestas de API y el HTML renderizado en el servidor. La CDN comprueba su caché perimetral; si se produce un fallo de caché, recupera el contenido del origen y lo almacena en caché para futuras solicitudes.
# 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')Equilibrio de carga: distribución del tráfico
Un equilibrador de carga distribuye las solicitudes entrantes entre varios servidores backend, evitando que un único servidor se convierta en un cuello de botella. También proporciona alta disponibilidad: si un servidor falla, el equilibrador de carga enruta automáticamente el tráfico a los servidores que funcionan correctamente (comprobaciones de estado cada 5-30 segundos).
Los equilibradores de carga funcionan en distintas capas del modelo OSI: capa 4 (transporte — enruta según la IP y el puerto, con gran rapidez) y capa 7 (aplicación — enruta según la ruta de URL, los encabezados y las cookies, lo que permite un enrutamiento más inteligente). AWS ALB, Nginx y HAProxy son equilibradores de carga habituales de capa 7. AWS NLB es un equilibrador de carga de capa 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"]}')Hashing consistente: adición y eliminación de nodos
El hashing consistente resuelve el problema de redistribuir las claves de caché cuando se añaden o eliminan servidores. En el hashing por módulo ingenuo (server = hash(key) % n), cambiar n reasigna casi todas las claves, lo que provoca una estampida de caché. El hashing consistente asigna tanto las claves como los servidores a un anillo; cada clave es atendida por el servidor más cercano en el sentido de las agujas del reloj. Añadir un servidor solo reasigna las claves situadas entre el nuevo servidor y su predecesor: aproximadamente 1/n de todas las claves.
Los nodos virtuales (vnodes) mejoran la distribución de la carga: cada servidor físico recibe varias posiciones en el anillo, de modo que las claves se distribuyen de forma más uniforme incluso cuando hay pocos servidores.
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)}')Estampida de caché y soluciones
Una estampida de caché (o efecto de manada) se produce cuando una entrada de caché popular caduca y muchas solicitudes simultáneas no encuentran la entrada en la caché, inundando la base de datos con la misma consulta. Soluciones:
- Mutex/bloqueo: solo una solicitud calcula el valor; las demás esperan
- Expiración anticipada probabilística: poco antes de que venza el TTL, una solicitud decide aleatoriamente actualizar la caché, evitando que caduque de forma simultánea
- Stale-while-revalidate: sirve inmediatamente el contenido obsoleto mientras actualiza la caché de forma asíncrona
- Actualización en segundo plano: un proceso independiente actualiza las claves populares antes de que caduquen
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')Invalidación de la caché de CDN
La invalidación de caché es famosa por su dificultad: «Solo hay dos problemas difíciles en informática: invalidar la caché y poner nombre a las cosas». Cuando el contenido cambia en el origen, los nodos perimetrales de la CDN deben servir la nueva versión. Estrategias:
- Caducidad basada en TTL: dejar que el contenido caduque de forma natural (sencillo, pero con una ventana de contenido obsoleto)
- Versionado de URL: insertar el hash del contenido en la URL (por ejemplo,
main.a3f2b.js); contenido nuevo = URL nueva, sin necesidad de invalidación - Purga mediante la API de CDN: purgar explícitamente las URL mediante una llamada a la API después de la implementación (rápido, pero requiere integrar la API de la 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')Arquitectura: integración de todos los componentes
Una capa de aplicación web a escala completa utiliza las tres técnicas conjuntamente: el equilibrio de carga distribuye el tráfico, la CDN absorbe las solicitudes de recursos estáticos y de API que se pueden almacenar en caché, y Redis almacena en caché los datos dinámicos. La base de datos solo recibe los fallos de caché — normalmente entre el 5 y el 20 % de las solicitudes.
Un flujo de solicitud típico para una API con muchas lecturas es: usuario → DNS → nodo perimetral de CDN (acierto de caché: se sirve inmediatamente) → fallo de CDN → equilibrador de carga → grupo de servidores de aplicaciones → caché de Redis (acierto: respuesta de 1 ms) → fallo de Redis → base de datos (10-50 ms) → respuesta almacenada en la caché de Redis + CDN opcional → usuario. Cada capa reduce considerablemente la carga de la base de datos.
# 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%')Consejos para entrevistas: caché y equilibrio de carga
Al hablar del almacenamiento en caché en una entrevista de diseño de sistemas, aborde siempre: qué almacenar en caché (datos de acceso frecuente, cálculos costosos), dónde almacenar en caché (navegador, CDN, aplicación, caché de consultas de la base de datos), cuándo invalidar (al escribir, cuando vence el TTL o mediante una actualización en segundo plano) y qué garantías de consistencia son aceptables. Una caché introduce una ventana de consistencia: sea explícito al respecto.
En cuanto al equilibrio de carga, mencione la elección del algoritmo, las comprobaciones de estado, las sesiones sticky (si son necesarias) y si es posible escalar horizontalmente los servidores de aplicaciones sin estado. Si la aplicación mantiene estado (conexiones WebSocket, sesiones), explique cómo se gestiona ese estado entre las réplicas.
# 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}')Comprobación rápida
Compruebe su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección ha aprendido que: el almacenamiento en caché guarda los datos de acceso frecuente en capas de memoria rápida (Redis, CDN) para absorber la mayoría de las lecturas y reducir la carga de la base de datos, cache-aside es el patrón más habitual: un fallo implica cargar desde la base de datos, un acierto implica devolver el resultado inmediatamente y una escritura implica eliminarlo de la caché, y el hashing consistente distribuye las claves de caché entre los nodos, de modo que añadir o eliminar nodos solo reasigna aproximadamente 1/n de las claves en lugar de reasignarlas todas. A continuación, diseñaremos un limitador de solicitudes y un feed de Twitter para aplicar todos los conceptos de diseño de sistemas en problemas integrales.
Preguntas frecuentes
¿La lección «Caché, CDN y balanceo de carga» es gratis?
Sí — el texto completo de «Caché, CDN y balanceo de carga» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Coding Interview Prep, actualiza a CoddyKit PRO. El curso de Coding Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Caché, CDN y balanceo de carga»?
Añada capas de caché con Redis, envíe los recursos estáticos a una CDN y distribuya el tráfico entre réplicas mediante balanceadores de carga round-robin y de hashing consistente. Practicas Coding Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar Coding Interview Prep?
No se requiere experiencia previa. Coding Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.
¿Cuánto tiempo toma la lección «Caché, CDN y balanceo de carga»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de Coding Interview Prep?
Sí. Cada lección de Coding Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Marco de la entrevista de diseño de sistemas
- Almacenamiento de datos escalable: SQL frente a NoSQL
- Caché, CDN y balanceo de carga
- Diseñar un limitador de solicitudes y un feed de Twitter