🔎 Buscar

🏗️ System Design — conceptos y casos

Diseño de sistemas de verdad: estimaciones back-of-the-envelope, escalabilidad, load balancing, caché, CDN, bases de datos a escala, particionado, consistencia, y el framework completo para resolver casos clásicos. Con ejemplos y práctica.

System Design📖 Contenido

System Design — conceptos y casos

System design es la disciplina de diseñar sistemas que aguantan carga, fallos y crecimiento, entendiendo los trade-offs de cada decisión. Es la asignatura que separa a quien «programa endpoints» de quien «diseña sistemas». Se estudia con dos ingredientes: conceptos (los building blocks) y casos (aplicarlos a problemas reales).

📌 La mentalidad del arquitecto
  1. Todo es un trade-off: consistencia vs disponibilidad vs latencia vs coste. 2. Se diseña para el fracaso: los fallos son normales, no excepciones. 3. Los números aproximados (back-of-the-envelope) valen más que la precisión. 4. Se piensa en garantías, no en features. 5. La evolución incremental gana a la revolución.

Paso 0: números y estimaciones (back-of-the-envelope)

Antes de diseñar hay que calcular a mano cuánta carga tendrá el sistema. Estos números aproximados son los que distinguen a un buen ingeniero.

1 segundo          = 10⁶ microsegundos
1 petición HTTP    ≈ 1-10 ms en un servidor
1 consulta SQL     ≈ 1-10 ms
1 lectura de SSD   ≈ 0.1-1 ms
1 lectura de RAM   ≈ 0.1 µs (10.000× más rápida que SSD)
RTT por internet   ≈ 10-100 ms (según distancia)

1 servidor típico: ~10.000-100.000 peticiones/seg (si es simple)
                  ~1.000-10.000 con base de datos detrás

Cálculo de ejemplo — un URL shortener:

Q1: ¿cuántas escrituras por segundo?
    100M URLs nuevas / año ≈ 300K / día ≈ 3-4 / segundo pico

Q2: ¿cuántas lecturas (redirects)?
    10:1 lectura:escritura ≈ 30-40 / segundo

Q3: ¿cuánto almacenamiento en 10 años?
    100M × 10 años = 1.000M URLs ≈ 1.000M × ~500 bytes ≈ 500 GB
    → cabe en un solo disco; no necesitas sharding todavía.

💡 Regla: estima siempre en órdenes de magnitud (10×, 100×, 1000×), no en cifras exactas. Si la respuesta cambia por un factor de 10 según tu estimación, el diseño cambia.

Paso 1: los building blocks (bloques de construcción)

Estos son los componentes estándar con los que se arma todo sistema grande.

Escalabilidad: vertical vs horizontal

Vertical (scale up) Horizontal (scale out)
Qué haces Más CPU/RAM en la misma máquina Más máquinas
Límite Físico (máximo de una máquina) Prácticamente ilimitado
Coste Caro y no lineal Barato por máquina (commodity)
Complejidad Baja Alta (repartir la carga, coordinar)
Ejemplo Pasar de 16 GB a 64 GB RAM Pasar de 1 servidor a 10
Vertical:      [servidor 64GB]          → tope físico
Horizontal:    [sv1] [sv2] [sv3] ...    → +1 máquina = +capacidad

⚠️ La escala horizontal añade el problema del estado: si cada petición puede ir a cualquier máquina, los datos compartidos (sesiones, caché, BBDD) deben vivir fuera de la máquina (Redis, BBDD compartida) o replicarse.

Load balancer (balanceador de carga)

Distribuye las peticiones entre varias máquinas. Es el primer «bloque» de cualquier sistema con más de un servidor.

                    ┌─────────────┐
   Clientes ───────▶│ Load        │──▶ [App 1]
                    │ Balancer    │──▶ [App 2]
                    └─────────────┘──▶ [App 3]
Método Cómo reparte Uso
Round robin A cada uno en turno Simple
Weighted Según capacidad Máquinas distintas
Least connections Al que menos conexiones tiene Cargas variables
IP hash / sticky Misma IP → misma máquina Sesiones locales

El balanceador además hace health checks (saca a las máquinas caídas) y soporta failover (si uno muere, los demás asumen).

💡 Implementaciones: Nginx (HTTP), HAProxy (TCP/HTTP), ELB/ALB (AWS). A nivel DNS, también existe round-robin DNS (devuelve varias IPs). En Kubernetes es el Service + Ingress.

Caché (caching)

Una caché guarda resultados caros para no volver a calcularlos. Es la técnica que más rendimiento da por dólar de esfuerzo.

   Cliente ──▶ [Caché: ¿está?] ──sí──▶ devolver copia (rápido)
                   │ no

               [Backend/BBDD] ──▶ guardar en caché ──▶ devolver

Dónde se pone:

Nivel de caché Dónde Ejemplo
Cliente En el navegador/app HTTP Cache-Control
CDN En el borde de la red CloudFront, Cloudflare, Akamai
Servidor web Antes de la app Nginx proxy_cache
Aplicación En el proceso Diccionario en memoria
Distribuida Fuera de la app Redis, Memcached

Patrones de caché:

  • Cache-aside: la app consulta la caché; si no está (cache miss), lee de la BBDD y la llena. El estándar.
  • Write-through: escribe a la vez en caché y BBDD (consistente, más lento).
  • Write-back: escribe solo en caché y vuelca a BBDD luego (rápido, riesgo de pérdida).
  • Read-through: la propia caché carga de la BBDD al fallar.

⚠️ Invalidación: el problema eterno de la caché es saber cuándo está vieja. Soluciones: TTL (expira sola), invalidación manual al escribir, o versionado de claves. Una caché mal invalidada sirve datos obsoletos (stale reads) — aceptable para feeds, fatal para saldos.

Ratio de acierto (hit rate): % de peticiones servidas desde caché. Un 90%+ ya transforma la carga de tu BBDD.

CDN (Content Delivery Network)

Una CDN es una red de caché geográficamente distribuida: tu contenido (imágenes, JS, HTML) se sirve desde el servidor más cercano al usuario.

Usuario en Madrid ──▶ CDN nodo Madrid (cerca, rápido)
Usuario en Tokio  ──▶ CDN nodo Tokio  (cerca, rápido)
        │ todos sirven el MISMO contenido cacheado del origen

      [Servidor origen]

💡 Reduce latencia (menos distancia) y carga del origen (el contenido estático ni llega a tu servidor). Ideal para assets estáticos e imágenes. No para datos dinámicos personalizados.

Bases de datos a escala

Cuando una sola base de datos no basta, se aplican dos técnicas que ya viste en Fundamentos: BBDD y Distribuidos:

  1. Replicación (copias): separa lecturas de escrituras.
  2. Particionado / sharding (partes): reparte los datos entre nodos.
Replicación:  Escrituras ──▶ [Maestro] ──▶ [Réplica] ──▶ lecturas
                                             [Réplica] ──▶ lecturas

Sharding:     Usuarios 0-1000 ──▶ [Shard A]
              Usuarios 1001-2000 ──▶ [Shard B]
              Usuarios 2001-3000 ──▶ [Shard C]

⚠️ La decisión más difícil: elegir la clave de partición (shard key). Si eliges mal (ej: por país y el 90% está en un país), tienes hot spots y el rendimiento se hunde. Y cambiarla después es carísimo. Piensa: «¿qué consultas son las más comunes y cómo las reparto uniformemente?»

Paso 2: el framework de diseño de un caso

Para resolver cualquier caso de system design, usa este framework de 4 pasos:

1. Requisitos y estimaciones

Antes de dibujar nada: pregunta.

¿Qué hace el sistema? ¿Quiénes lo usan?
¿Cuántos usuarios? ¿Cuántas peticiones por segundo?
¿Lecturas o escrituras? ¿Proporción?
¿Cuántos datos se guardan? ¿Cuánto crecerán en 10 años?
¿Requisitos no funcionales: latencia, disponibilidad, consistencia?

2. Diseño de alto nivel

Dibuja la arquitectura con bloques, sin detalles:

[Cliente] ──▶ [Load Balancer] ──▶ [Servidores API]
                    │                    │
                    ▼                    ▼
               [CDN (estático)]     [Caché (Redis)]


                                      [BBDD primaria]


                                      [Réplicas / colas / workers]

3. Componentes y datos

Detalla cada bloque: esquema de la base de datos, APIs, algoritmos clave.

Tabla urls:
  id (PK) | url_original | short_code (UNIQUE, INDEXADO) | creado_en

API:
  POST /api/url  {url} → {short_code}
  GET  /:short   → 301 redirect a url_original

4. Escalar y fallos

«¿Qué pasa si se duplica el tráfico? ¿Y si se cae un nodo? ¿Y si se cae la BBDD?»

Escalar:  más servidores de app (ya son stateless → solo añadir)
          caché en Redis para lecturas calientes
          réplicas de lectura
Fallos:   balanceador detecta app caída → redirige
          BBDD caída → con réplicas, promoción (failover)
          CDN sigue sirviendo el estático

Caso resuelto: URL shortener

Aplicamos el framework al caso más clásico.

1. Requisitos: crear URLs cortas (write-heavy es poco, read-heavy sí), redirects rápidos (menos de 50 ms), sin colisiones.

2. Alto nivel:

[Cliente] ──▶ [LB] ──▶ [API servers] ──▶ [Redis caché]
                                        ──▶ [BBDD PostgreSQL]

3. Datos: tabla urls con short_code único e indexado. La búsqueda por short_code es O(log n) por el índice.

4. Generación del código corto: dos opciones clásicas.

-- Opción A: ID autoincremental → codificar en base62
-- id=1000000 → base62 → "4c92" (más corto que decimal)
-- 6 caracteres base62 = 62^6 ≈ 56.800 millones de combinaciones

-- Opción B: hash de la URL
SELECT substr(md5(random()::text), 1, 7);  -- aleatorio, con retry si colisiona

Escalar: los redirects son lecturas → se cachean en Redis (TTL largo, el 99% de los redirects son de URLs populares). La BBDD se replica para lecturas. Si el tráfico crece mucho, se sharding por short_code (hash).

Caso resuelto: chat (sistema de mensajería)

1. Requisitos: mensajes en tiempo real, historial, multiusuario.

2. Alto nivel:

[Cliente] ──▶ [WebSocket / Long-poll] ──▶ [Chat server] ──▶ [Cola] ──▶ [Worker] ──▶ [BBDD]


                                                         [Persistencia]

3. Decisiones clave:

  • Tiempo real: WebSocket para el canal activo (bidireccional, sin polling). Alternativas: Server-Sent Events (unidireccional), long-polling (compatibilidad).
  • Orden de mensajes: cada conversación tiene un conversation_id; el orden se garantiza con un número de secuencia por conversación (no global).
  • Presencia: estado en Redis (TTL que se renueva con heartbeats).
  • Persistencia: tabla mensajes (id, conversation_id, user_id, texto, creado_en) con índice en conversation_id.
  • Notificaciones push cuando el usuario no está conectado (FCM/APNs).

4. Escalar:

- Chat servers stateless → añadir más detrás del LB
- El canal WebSocket se enruta con sticky sessions o un "presence registry"
  (Redis con mapa conversation → server)
- Historial viejo → a almacenamiento frío (archivo/S3) o columna
- Sharding por conversation_id para millones de conversaciones

Catálogo de casos clásicos

Practica resolviendo estos (por dificultad):

Caso Conceptos que entrena
URL shortener BBDD, base62, caché, redirección
Rate limiter Algoritmos de límites (token bucket, sliding window), Redis
News feed Push vs pull, fanout, caché, ranking
Chat WebSocket, orden, presencia, colas
Búsqueda (search engine) Inverted index, ranking, crawling
Video streaming CDN, streaming adaptativo (HLS/DASH), segmentos
Notificaciones Colas, deduplicación, workers
Key-value store Replicación, sharding, consistencia, LSM/B-tree
Ticketing (reservas) Consistencia fuerte, locks, transacciones, overbooking
Uber / maps Geo-indexing, eventos, sincronización

Terminología que debes dominar

Término En una frase
Throughput Operaciones por segundo (QPS/TPS)
Latencia Tiempo de una operación
Escalabilidad Capacidad de crecer con la carga
Disponibilidad (SLA) % de tiempo que el sistema responde (99.9% = ~8.7h/año caído)
Load balancer Reparte carga entre servidores
Caché Copia de datos caros de recalcular
Hit rate % de peticiones servidas por caché
CDN Caché distribuida geográficamente
Replicación Copias de los datos (lecturas/escrituras separadas)
Sharding / particionado Cada nodo guarda una parte
Shard key La clave que decide a qué partición va cada dato
Fanout Multiplicar un dato a muchos destinos (feed)
Idempotencia Repetir la operación da el mismo resultado (clave para colas)
Back-of-the-envelope Estimación a mano, aproximada
Cold / hot data Datos poco usados / muy usados

Práctica propuesta

  1. Calcula back-of-the-envelope para un sistema de tweets: usuarios, tweets/día, lecturas/seg, almacenamiento en 10 años.
  2. Dibuja en papel la arquitectura de URL shortener sin mirar la solución. Luego compárala con la de arriba.
  3. Implementa un rate limiter con token bucket en tu lenguaje (Redis si lo tienes a mano).
  4. Monta una caché Redis delante de un endpoint tuyo y mide la mejora con ab (ApacheBench) o wrk.
  5. Explica a alguien (o por escrito) la diferencia entre replicación y sharding con un dibujo.
  6. Elige una shard key para: usuarios, mensajes, pedidos. Justifica cada elección.

Para profundizar

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.