⚖️ Infraestructura y orquestación
El patrón de despliegue de sistemas reales: load balancers (L4/L7), réplicas y failover de bases de datos, connection pooling, caché distribuida, orquestación de contenedores, service discovery y escalado automático. Teoría, configuración y práctica.
Infraestructura y orquestación
Cuando tu sistema pasa de «un servidor» a «varios», aparece un conjunto de piezas que sostienen la escala: balanceadores de carga, réplicas de base de datos, caché compartida y orquestación de contenedores. Esta página reúne todo lo que debes saber de ese nivel, con sus referencias. La teoría conceptual está en System Design: conceptos y Distribuidos; aquí ves el cómo operarlo.
[CDN (estático)]
│
[Clientes] ──▶ [Load Balancer] ──▶ [App 1] ──▶ [Caché Redis]
│ [App 2] │
│ [App 3] ▼
└─────────────────────▶ [BBDD primaria]
│ replicación
▼
[BBDD réplicas (lecturas)]Toda pieza de este diagrama tiene alternativas y trade-offs; abajo ves cada una con detalle.
Load balancer (balanceador de carga)
Ya viste los conceptos en System Design. Aquí, el detalle operativo que separa al que lo configura del que lo entiende.
L4 vs L7
| L4 (transporte) | L7 (aplicación) | |
|---|---|---|
| Qué mira | IP + puerto (TCP/UDP) | La petición completa (URL, headers, cookies) |
| Velocidad | Muy rápida (no inspecciona el contenido) | Más lenta (tiene que leer el tráfico) |
| Capacidades | Balanceo simple, NAT, TLS passthrough | Routing por URL, sticky sessions, rate limit, compresión |
| Ejemplos | HAProxy (L4), AWS NLB, iptables | Nginx, HAProxy (L7), AWS ALB, Traefik |
💡 Regla práctica: L4 para rendimiento puro (balanceo de BBDD, gRPC, WebSocket); L7 cuando necesitas decidir por contenido (routing
/apivs/admin, auth, caching).
Configuración real con Nginx (L7)
upstream backend {
least_conn; # al que menos conexiones tenga
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080;
server 10.0.0.3:8080 backup; # solo si los demás caen
}
server {
listen 443 ssl;
server_name api.misitio.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Sticky sessions: cuándo sí y cuándo no
upstream backend {
ip_hash; # misma IP → misma máquina
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
⚠️ Las sticky sessions (misma IP/sesión → misma máquina) son un parche. Si una máquina cae, su usuario se pierde. La solución correcta en producción: sesiones fuera de la máquina (Redis) o JWT stateless — así cualquier instancia atiende a cualquiera (patrón stateless del 12-factor).
Health checks y failover
El balanceador vigila a los servidores y saca a los que no responden:
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
# Health check activo con una ruta dedicada
location /healthz {
proxy_pass http://backend;
}
App 2 muere → el LB deja de mandarle peticiones (health check falla)
→ App 1 y App 3 absorben la carga
→ al volver, el LB lo detecta y lo reintroduce
💡 El endpoint
/healthzes el contrato entre la app y la infraestructura: debe responder 200 rápido cuando la app está sana, y no-200 (o no responder) cuando no puede servir. Incluye la BBDD y las dependencias críticas en el check.
Réplicas y failover de bases de datos
La teoría está en Distribuidos: replicación. Aquí, el patrón operativo real.
El patrón estándar: líder + réplicas de lectura
Escrituras ──▶ [Primaria] ──replicación──▶ [Réplica 1] ──▶ lecturas
[Réplica 2] ──▶ lecturas
# PostgreSQL: crear una réplica (streaming replication)
pg_basebackup -h primaria -D /var/lib/postgresql/15/replica -R -X stream
# Y en primaria, los archivos de config que lo activan
# postgresql.conf (primaria): wal_level = replica, max_wal_senders = 10
| Configuración | Qué permite |
|---|---|
| Réplicas de lectura | La app manda SELECT a réplicas, INSERT/UPDATE a la primaria |
| Failover | Si la primaria cae, una réplica se promueve a primaria |
| Read-after-write | El usuario que escribe debe leer su propia escritura (routing fino) |
⚠️ El problema de las réplicas: con replicación asíncrona (la común), una réplica puede ir unos milisegundos por detrás. Si tu app hace
INSERTy luegoSELECTinmediato del mismo dato, la réplica puede no tenerlo aún (read-after-write). Soluciones: leer la escritura de la primaria, o usar replicación síncrona para ese dato crítico.
Failover: qué pasa cuando cae la primaria
Primaria cae
→ una réplica se promueve (Promote / pg_ctl promote / tools)
→ la app debe apuntar a la nueva primaria
→ el antiguo réplica vuelve como réplica de la nueva
💡 Automatizar el failover: herramientas como Patroni, Repmgr o el RDS Multi-AZ de AWS lo hacen solos (con consenso). A mano,
pg_ctl promotepromueve una réplica. La decisión de quién se promueve y cuándo (para no tener dos primarias = split-brain) es un problema de consenso — lo viste en Raft.
Connection pooling
Cada conexión a BBDD es cara (proceso, memoria, handshake). Un pooler reutiliza un número fijo de conexiones entre miles de clientes.
[App ×100] ──▶ [PgBouncer (pool de 50)] ──▶ [PostgreSQL]
(100 apps comparten 50 conexiones)
# pgbouncer.ini
[databases]
mydb = host=primaria port=5432
[pgbouncer]
pool_mode = transaction # la conexión se libera al acabar la transacción
max_client_conn = 1000
default_pool_size = 50
⚠️ El error clásico: sin pooling, 1000 apps × 10 conexiones = 10.000 conexiones → la BBDD se ahoga (“too many connections”). El pooler es la pieza que hace que el número real de conexiones sea manejable.
Caché distribuida: Redis a escala
La teoría de caché está en System Design. Operativamente, Redis es el estándar.
Redis cluster vs replicación
Replicación Redis: [Master] ──▶ [Replica] (alta disponibilidad, lectura)
Redis Cluster: 3 masters × 2 réplicas (datos repartidos + HA)
| Patrón | Qué resuelve |
|---|---|
| Master-replica | HA y lecturas (un master, réplicas) |
| Sentinel | Fallover automático del master |
| Cluster | Sharding: los datos se reparten entre masters (hash slots) |
| Redis on Flash / persistencia | No perder datos (AOF/RDB, o elige Redis solo para caché) |
💡 Regla de diseño: si los datos se pueden reconstruir (caché, sesión, rate limit) → Redis sin demasiada persistencia, todo rápido. Si son la fuente de verdad → mejor la BBDD; Redis no debe ser la única copia de nada crítico.
Orquestación de contenedores
La teoría está en DevOps: contenido. Aquí, el patrón completo de orquestación.
La jerarquía de abstracción
Dockerfile (imagen)
→ docker run (un contenedor)
→ Docker Compose (varios servicios en un host)
→ Kubernetes (clúster: muchos hosts, despliegue, escala, red, HA)
Kubernetes: el patrón declarativo
┌───────────────────────────────────────────────┐
│ Control plane │
│ ┌────────┐ ┌───────────┐ ┌──────────┐ │
│ │ API │ │ Scheduler │ │ etcd │ │
│ │ server │ │ │ │ (Raft) │ │
│ └────────┘ └───────────┘ └──────────┘ │
└──────────┬────────────────────────────────────┘
│
┌──────────▼────────────────────────────────────┐
│ Worker nodes │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ kubelet │ │ kube-proxy│ │ pods │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────┘
Los objetos clave que definen la orquestación:
| Objeto | Qué hace | Declara |
|---|---|---|
| Deployment | “Quiero N réplicas” | replicas: 3 |
| Service | IP estable + balanceo a los pods | selector por labels |
| Ingress | Entrada HTTP externa (rutas, TLS) | host + path → service |
| ConfigMap / Secret | Configuración / secretos | datos |
| HPA | Escalado automático por CPU/métricas | target + mín/máx |
# deployment + service + hpa: el patrón completo
apiVersion: apps/v1
kind: Deployment
metadata:
name: miapp
spec:
replicas: 3
selector:
matchLabels: { app: miapp }
template:
metadata:
labels: { app: miapp }
spec:
containers:
- name: miapp
image: miapp:1.4.2
ports: [{ containerPort: 3000 }]
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
---
apiVersion: v1
kind: Service
metadata:
name: miapp
spec:
selector: { app: miapp }
ports: [{ port: 80, targetPort: 3000 }]
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: miapp
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: miapp }
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
💡 Declarativo: tú escribes “el estado deseado” (3 réplicas, auto-escalar hasta 20 al 70% CPU). Kubernetes hace el trabajo de llegar ahí y mantenerlo: si un pod muere, crea otro; si la carga sube, el HPA añade réplicas; si baja, las quita.
Service discovery: cómo se encuentran los servicios
Sin discovery, la app no sabría a qué IP llamar:
K8s: Service → nombre DNS estable (miapp.svc.cluster.local)
→ kube-proxy enruta a los pods detrás
Otras: Consul / etcd / AWS Cloud Map / ECS Service Discovery
→ registro + DNS + health checks
Mi app: GET http://miapp.svc.cluster.local/api/usuarios
│ (DNS resuelve al Service, que balancea a los pods)
▼
[pod miapp-abc12] [pod miapp-def34] [pod miapp-ghi56]
Escalado automático
| Nivel | Quién escala | Cómo |
|---|---|---|
| Apps | HPA (K8s) / ASG (AWS) | Por CPU, memoria, métricas custom o cola |
| Caché | Redis Cluster | Añadir shards / réplicas |
| BBDD | Réplicas de lectura | Añadir réplicas para lecturas; escalado vertical para escrituras |
| Colas | Worker pool | Por profundidad de la cola (KEDA, SQS autoscaling) |
⚠️ El cuello de botella final: puedes escalar apps y réplicas de lectura casi infinitamente, pero las escrituras de la BBDD siempre tienen un límite (una primaria). Cuando se satura la escritura, ahí entran sharding, particionado por tenant, o colas asíncronas para descargar la primaria.
Cheatsheet
| Concepto | En una frase |
|---|---|
| L4 / L7 | Balanceo por IP/puerto / por contenido de la petición |
| Health check | El check que decide si una instancia sirve o no |
| Sticky session | Misma IP → misma máquina (evitar en producción) |
| Failover | Promoción de una réplica cuando la primaria cae |
| Connection pooling | Reutilizar N conexiones entre muchos clientes |
| Read replica | Copia que atiende lecturas |
| Read-after-write | La réplica puede ir por detrás de la escritura |
| Redis cluster | Sharding + HA de la caché |
| Orquestación | Gestionar contenedores a escala (K8s) |
| Deployment / Service / Ingress | Réplicas / IP estable / entrada HTTP |
| HPA | Escalado automático horizontal |
| Service discovery | Encontrar servicios por nombre, no por IP |
| Escala horizontal | Más instancias tras un balanceador |
| Split-brain | Dos primarias a la vez (catástrofe) |
Práctica propuesta
- Monta Nginx como L7 load balancer delante de 2 instancias de una app tuya. Añade
/healthzy verifica que al matar una app, el LB deja de mandarle tráfico. - Crea réplicas de PostgreSQL (líder + 2 réplicas) y enruta lecturas a réplicas, escrituras a la primaria. Provoca el failover.
- Añade PgBouncer delante y observa cómo las conexiones reales a la BBDD bajan de 100 a 20.
- Monta Redis con replicación y luego Redis Cluster con 3 masters. Compara.
- Despliega tu app en Kubernetes con Deployment (3 réplicas) + Service + HPA. Lanza carga con
wrky observa el auto-escalado.
Para profundizar
- System Design: conceptos: load balancing, caché, CDN, replicación y sharding (teoría).
- DevOps: contenido: Docker, Kubernetes, CI/CD, monitoreo (cómo operar).
- Nginx de verdad: load balancing y reverse proxy configurados línea a línea.
- Docker de verdad: imágenes y contenedores.
- Fundamentos: distribuidos: replicación, particionado y consenso (teoría base).
- System Design: distribuidos de élite: Raft, Kafka, los sistemas reales.
- Infraestructura moderna (recursos) y Bases de datos a fondo (recursos).
- Cloud en profundidad (recursos).