Tu aplicación empieza a traccionar. El gráfico de usuarios se dispara, las CPU se ponen al 100 % y la memoria vuela. Un solo servidor deja de ser suficiente. Aquí comienza el trabajo de arquitectura que todos los administradores de sistemas terminamos haciendo tarde o temprano: descubrir el cuello de botella, aliviarlo y repetir.
El diagrama típico (como el de arriba) resume el primer gran salto: un balanceador delante de varios servidores web (capa web) y una base de datos con réplicas (capa de datos). En este artículo repasamos cómo llegar ahí sin romper nada, qué decisiones tomar y qué errores evitar.
1) Dos formas de crecer
| Estrategia | Qué es | Ventajas | Límites / Costes |
|---|---|---|---|
| Escalado vertical | Más CPU/RAM/IO al mismo host | Simple de operar; no cambia la topología | Escala finita, hardware caro; ventanas de mantenimiento más riesgosas |
| Escalado horizontal | Agregar más servidores | Resiliencia y elasticidad, coste por unidad menor | Complejidad: balanceo, sesiones, consistencia, redes |
Regla práctica: comienza verticalmente mientras el ROI sea claro. Cuando el tiempo de respuesta p95 y el uso sostenido de CPU/RAM indican tope físico, pasa a horizontal.
2) El balanceador: quién recibe cada request
El “¿a qué servidor envío cada request?” lo responde un Load Balancer (LB). Dos capas habituales:
- L4 (transporte: TCP/UDP): rápido, ciego al contenido (HAProxy en modo TCP, LVS/IPVS, NLB/ALB en nube).
- L7 (aplicación: HTTP/HTTPS): entiende rutas/cabeceras, puede hacer TLS termination, WAF, routing por path/host (Nginx, HAProxy HTTP, Envoy, Traefik).
Algoritmos comunes
- Round-robin (simple y efectivo).
- Least-connections (mejor cuando las sesiones son desparejas).
- Weighted (para mezclar instancias con tamaños distintos).
- Consistent-hash (útil para sticky por clave o para partición de caché).
Salud y outlier detection
- Health checks (HTTP 200/3xx, TCP, gRPC).
- Circuit breaking y outlier ejection para expulsar instancias con errores intermitentes.
- Time-outs y reintentos con backoff para no amplificar cortes (evita retry storms).
Sesiones: sticky o stateless
- Lo ideal: aplicación stateless (+ sesión en Redis o cookies firmadas).
- Si no puedes (por ahora), usa stickiness (cookie insertada por el LB o IP hash). Acepta que reduce el equilibrio y complica failover.
Ejemplo mínimo con HAProxy (L7)
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs/example.pem
mode http
option httplog
default_backend be_app
backend be_app
mode http
balance leastconn
option httpchk GET /health
http-check expect status 200
server app1 10.0.1.11:8080 check
server app2 10.0.1.12:8080 check
server app3 10.0.1.13:8080 check
Alta disponibilidad del propio LB
- Par activo/backup con VRRP/Keepalived o anycast; en nube, usa ELB/ALB o equivalentes multi-AZ.
- Practica fallos: traslado de VIP, drenado de conexiones y conservación de state (si lo hubiera).
3) El cuello de botella se mueve: la base de datos
Balancear la capa web multiplica el número de clientes concurrentes de la base. Si todas las instancias tiran contra la misma DB, el cuello se traslada allí. Antes de agregar hierro, exprime tres palancas:
- Pool de conexiones y proxies
- Postgres: pgBouncer (modo
transactionpara apps con pooling pobre). - MySQL/MariaDB: ProxySQL.
- Ajusta máx. conexiones; muchas conexiones empeoran el rendimiento.
- Postgres: pgBouncer (modo
- Índices y consultas
- Perfilado (EXPLAIN/ANALYZE), índices compuestos, evitar N+1.
- Columnas “calientes”: mira bloqueos, checkpoint y vacuum (en PG).
- Caché (siguiente sección).
Cuando aun así necesitas más, pasa a replicación:
Replicación: primary + read replicas
- Primary: acepta escrituras;
- Réplicas: atienden lecturas.
Sincronía
- Asíncrona (lo más común): baja latencia, lag posible → eventual consistency.
- Síncrona: confirmación en réplicas, RPO≈0, pero con latencias y riesgo de stall.
Cómo lidiar con el lag (lecturas “desfasadas”)
- Read-your-writes: tras una escritura, dirige lecturas de ese usuario al primary por X segundos o hasta un LSN/GTID específico.
- Consistencia por token: la app adjunta el último LSN que vio; el proxy lee solo en réplicas que lo alcanzaron (PG:
pg_last_wal_replay_lsn). - Rutas claras en el pool:
write→ primary,read→ réplicas; protección para queries que no deben correr en réplicas (transaccionales).
Failover del primary
- Orquestración (Patrón Raft o herramientas como Patroni/repmgr/orchestrator).
- Decide RTO/RPO aceptables; evita split-brain.
4) Caché: tu mejor amiga contra la saturación
Dónde cachear
- CDN/edge (estática y APIs cachables).
- Reverse proxy (Nginx/Envoy) para páginas o fragments.
- Aplicación con Redis/Memcached (baja latencia).
Patrones
- Cache-aside (lazy-loading): la app busca en caché; si no, consulta DB y escribe en caché. Es flexible y el más común.
- Write-through: cuando escribes en DB, también en caché (consistencia mejor, más latencia en el write).
- Write-behind: escribes primero en caché y persistes luego (riesgo si se cae la caché).
Evicción y TTL
- Define TTL sensatos; si la coherencia es crítica, invalida explícitamente (pub/sub, tags).
- Política: LRU/LFU según patrón; evita hot keys sin sharding o pipelining.
Dogpile/stampede
- Evita que cientos de procesos reconstruyan la misma clave: locking (por ejemplo,
SETNXcon vencimiento) o request coalescing en el proxy. - Early refresh: renueva antes del TTL final.
Ejemplo (pseudo cache-aside)
def get_user(id):
key = f"user:{id}"
obj = redis.get(key)
if obj:
return deserialize(obj)
row = db.query("SELECT * FROM users WHERE id=%s", id)
if row:
redis.setex(key, 300, serialize(row)) # TTL 5 min
return row
Lenguaje del código: PHP (php)
5) Cuando ni las réplicas alcanzan: sharding y partición
Si la base (o una tabla) crece y la I/O explota, evalúa particionar:
- Sharding por usuario/tenant (hash/rango): cada shard es una DB más pequeña (con su primary y réplicas).
- Particionado nativo por fecha o rango (Postgres nativo, MySQL partitioning limitado).
- CQRS: separar lecturas y escrituras en modelos diferentes.
- Colas/eventos para trabajo pesado asíncrono (Kafka/RabbitMQ/SQS): idempotencia y Outbox pattern.
Atención: el sharding complica joins, transacciones y operaciones multi-clave. No lo adoptes antes de exprimir réplicas + caché + índices.
6) Resiliencia operativa: timeouts, retries, backoff y circuit breakers
- Timeouts en todas las capas (cliente, LB, app, DB, caché). Sin timeouts, un fallo se convierte en hilo colgado.
- Reintentos con exponential backoff y jitter; nunca reintentes operaciones no idempotentes (o usa claves de idempotencia).
- Circuit breaker: si un servicio falla repetidamente, abre el circuito y evita saturar.
- Rate limiting y WAF en el LB (L7).
- Colas para desacoplar picos (buffer).
7) Observabilidad y capacidad: sin métricas, no hay escalado seguro
Define SLOs (por ejemplo, latencia p95 < 300 ms, error rate < 1 %). Instrumenta:
- LB: RPS, backend errors, outlier ejections, latencias.
- App: latencia p50/p95/p99 por endpoint, thread pool saturation.
- DB: QPS, locks, slow queries, replication lag, conexiones activas.
- Caché: tasa de aciertos, tamaño, evicciones, hot keys.
- Infra: CPU, IOPS, ancho de banda, throttling.
Trazabilidad distribuida (OTel/Jaeger/Tempo) para seguir spans entre LB → app → DB/caché. Dashboards que muestren todas las capas; alertas en tendencia, no solo en umbrales absolutos.
8) Despliegues sin sobresaltos: blue/green y canary
- Blue/Green: dos stacks idénticos; el LB conmuta tráfico de blue a green (y viceversa si hay rollback).
- Canary: enruta 5 % → 25 % → 50 % del tráfico a la nueva versión mientras monitoreas métricas.
- Auto-scaling: policies por CPU/RPS/latencia; cuidado con los picos cortos (usa warm-ups).
9) Seguridad en la arquitectura
- TLS termination en el LB, mTLS en malla interna si el riesgo lo amerita.
- Secret management (no env planos), rotation.
- Least privilege en DB/caché; firewall groups segmentados.
- Backups con RPO/RTO testeados; DR multi-AZ/región.
10) Ruta evolutiva (checklist)
- Vertical: multiplica vCPU/RAM; corrige hotspots de código/queries.
- LB + 2–3 instancias de app; health checks y least-conn.
- Pool de conexiones y proxy (pgBouncer/ProxySQL).
- Caché Redis (cache-aside) + CDN para estáticos.
- Read replicas; routing claro
read/write;read-your-writes. - Observabilidad completa, SLOs, canary deploys.
- Auto-scaling y HA del LB (VRRP/ELB).
- Si hace falta: particionado por rango/tenant; colas para asíncrono.
- DR multi-AZ/región, pruebas de failover regulares.
Un último consejo de guerra
Escalar horizontalmente te da aire, pero cada paso trae nuevos trade-offs: stickiness vs. stateless, latencia vs. consistencia, coste vs. simplicidad. El trabajo real es elegir conscientemente qué sacrificar y medir si la decisión paga. Diseñar sistemas es justamente eso: entender dónde está el cuello de botella hoy y qué necesitas para que mañana tu aplicación siga creciendo sin que producción arda.