TechCorp ya ve lo que pasa (06-01, 06-02) y sobrevive a los fallos (06-03). Queda la tercera pregunta que Marta lleva haciendo desde 01-05: el Black Friday en que el catálogo recibió ×20 tráfico y la tienda estuvo 50 minutos sin vender. En el monolito la única palanca era "una máquina más grande"; en Kubernetes tenemos réplicas, autoescalado y la posibilidad de escalar solo el servicio que lo necesita. Pero escalar no es gratis ni automático: hay que saber qué hace escalable a un servicio, cómo se le dice al clúster que crezca, cómo se comprueba antes con pruebas de carga y, sobre todo, dónde están los cuellos de botella que ninguna réplica arregla (la base de datos, un event loop bloqueado, un pool mal dimensionado). Esta lección recorre esas capas de fuera adentro.
Contenido
- Escalado vertical y horizontal: qué hace escalable a un servicio
- Autoescalado en Kubernetes: HPA para
servicio-catalogo - Escalar consumidores por longitud de cola: KEDA para
servicio-inventario requests,PodDisruptionBudgety el escalado del clúster- El caso Black Friday: dimensionar y comprobar con k6
- Caché: Redis en Catálogo y caché HTTP en el gateway
- Pools de conexiones: la cuenta que hay que hacer
- Consultas, índices y el event loop de Node
- Escalar la mensajería: colas competidoras y orden
- Escalar la base de datos: réplicas de lectura y particionado
- Tabla "síntoma → dónde mirar → remedio"
- Escalado vertical y horizontal: qué hace escalable a un servicio
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| Qué es | Más CPU/memoria al mismo proceso | Más réplicas del mismo proceso |
| En Kubernetes | Subir resources.limits (05-02) |
Subir replicas / HPA |
| Límite | El tamaño del nodo; Node.js usa un solo hilo, así que más CPU apenas ayuda | El estado compartido (BD, colas) |
| Coste del cambio | Reinicio del pod | Ninguno si el servicio es "escalable" |
| Cuándo | Bases de datos, procesos con estado | Servicios HTTP y consumidores sin estado |
Un servicio es horizontalmente escalable cuando dos réplicas se comportan igual que una y no se estorban. La plantilla de 04-02 ya lo garantiza, y conviene recordar por qué:
- Sin estado en el proceso. Nada en memoria que una segunda réplica necesite: ni sesiones (el JWT de 07-01 viaja con la petición), ni cachés "de verdad" (Redis, apartado 6), ni ficheros locales. La caché en memoria de la plantilla (
Mapcon TTL) es tolerable solo si perderla no rompe nada. - Conexiones limitadas y conocidas. Cada réplica abre su pool de
pg(apartado 7) y su canal de RabbitMQ; N réplicas = N pools. Si no se cuenta, la BD se queda sin conexiones antes que el servicio sin CPU. - Arranque y parada rápidos y limpios.
readinessProbe(05-02) para no recibir tráfico antes de tiempo;SIGTERM(06-03) para no perder peticiones al reducir réplicas. - Trabajos periódicos que toleran réplicas. El relay del outbox y el vigilante (06-03) usan
FOR UPDATE SKIP LOCKEDpara que dos réplicas no se pisen; sin eso, escalar Pedidos duplicaría eventos.
- Autoescalado en Kubernetes: HPA para
servicio-catalogo
servicio-catalogoEl HorizontalPodAutoscaler observa una métrica y ajusta replicas del Deployment entre un mínimo y un máximo. Para Catálogo, el servicio que multiplica ×20:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: servicio-catalogo
namespace: techcorp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: servicio-catalogo
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # % sobre resources.requests.cpu
- type: Pods
pods:
metric:
name: http_requests_per_second # métrica personalizada servida por Prometheus Adapter
target:
type: AverageValue
averageValue: "150" # peticiones/s por pod
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- { type: Percent, value: 100, periodSeconds: 60 } # como mucho duplicar cada minuto
scaleDown:
stabilizationWindowSeconds: 300 # esperar 5 min antes de reducir
policies:
- { type: Pods, value: 2, periodSeconds: 60 }Línea a línea:
scaleTargetRef: elDeploymentde 05-02. A partir de ahora no se fijareplicasen el manifiesto (el HPA lo pisaría); Kustomize deja el campo fuera y el HPA manda.minReplicas: 2por disponibilidad (un pod puede morir en cualquier momento);maxReplicas: 20es el techo de coste y también protege a MongoDB de recibir 200 conexiones.- CPU al 60 %: el porcentaje se calcula sobre
resources.requests.cpu(100m en 05-02), no sobrelimits. Conrequests: 100m, el HPA añade réplicas cuando la media supera 60m. Por eso requests debe reflejar el consumo real en régimen normal (apartado 4). - Métrica personalizada
http_requests_per_second: viene de lahttp_requests_totalde 06-01 a través del Prometheus Adapter, que expone consultas PromQL como métricas de la APIcustom.metrics.k8s.io. Su configuración (fragmento):
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources: { overrides: { namespace: { resource: namespace }, pod: { resource: pod } } }
name: { matches: "^(.*)_total$", as: "${1}_per_second" }
metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'Convierte http_requests_total en http_requests_per_second por pod con rate de 2 minutos. Con dos métricas, el HPA calcula las réplicas necesarias para cada una y toma la mayor.
behavior: sin él, el HPA reacciona con las políticas por defecto. Aquí subimos rápido (duplicar por minuto: en Black Friday no hay tiempo) y bajamos despacio (5 minutos de estabilización, 2 pods por minuto) para evitar el flapping con picos cortos.
Se comprueba con kubectl get hpa -n techcorp (TARGETS 45%/60%, 80/150) y kubectl describe hpa servicio-catalogo muestra cada decisión. En 05-04 escalábamos a mano con kubectl scale; ese comando sigue sirviendo para un ajuste puntual, pero el HPA lo revertirá en el siguiente ciclo (15 s).
- Escalar consumidores por longitud de cola: KEDA para
servicio-inventario
servicio-inventarioInventario no recibe HTTP: consume inventario.pedidos. Su carga no se ve en CPU hasta que ya va tarde; se ve en rabbitmq_queue_messages_ready. El HPA no habla RabbitMQ, pero KEDA (Kubernetes Event-Driven Autoscaling) sí: crea y gestiona un HPA por debajo a partir de scalers para colas, Prometheus, cron, etc.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: servicio-inventario
namespace: techcorp
spec:
scaleTargetRef:
name: servicio-inventario
minReplicaCount: 2
maxReplicaCount: 10
cooldownPeriod: 120
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: inventario.pedidos
mode: QueueLength
value: "50" # objetivo: 50 mensajes pendientes por réplica
authenticationRef:
name: keda-rabbitmq-auth # TriggerAuthentication que apunta al Secret pedidos-rabbitmq (05-02)mode: QueueLength, value: 50: KEDA quiereréplicas = ceil(mensajes_listos / 50); con 400 mensajes esperando pasa a 8 réplicas.cooldownPeriod: 120: dos minutos con la cola vacía antes de reducir.authenticationRefreutiliza las credenciales delSecretde RabbitMQ; no se ponen en el YAML.- KEDA también puede escalar a cero (
minReplicaCount: 0) para trabajos esporádicos; para Inventario mantenemos 2 porque el primer mensaje no debe esperar un arranque.
Escalar consumidores tiene un efecto sobre el orden de los mensajes que se trata en el apartado 9, y un límite claro: más réplicas de Inventario significa más carga sobre su PostgreSQL. maxReplicaCount: 10 sale de la cuenta del apartado 7.
requests, PodDisruptionBudget y el escalado del clúster
requests, PodDisruptionBudget y el escalado del clústerTres piezas del equipo de Plataforma que hacen posible lo anterior:
resources.requestsreales. El scheduler coloca pods según requests; el HPA calcula porcentajes sobre ellos. Sirequests.cpu: 100mpero el servicio usa 300m en reposo, el HPA verá "300 %" y escalará al máximo sin motivo; si usa 20m, nunca escalará. Se ajustan mirandocontainer_cpu_usage_seconds_total(06-01) del p50 en horario normal, ylimitscon margen para picos (05-02: 100m/500m para Pedidos; Catálogo pasa a 200m/1000m tras las pruebas del apartado 5).PodDisruptionBudget: al reducir réplicas, vaciar un nodo (kubectl drain) o actualizar el clúster, Kubernetes puede matar varios pods a la vez. El PDB pone un suelo:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: servicio-catalogo, namespace: techcorp }
spec:
minAvailable: 1 # o maxUnavailable: 1 para Deployments grandes
selector: { matchLabels: { app: servicio-catalogo } }Con esto, un drain que dejaría a Catálogo con cero pods espera a que otro nodo lo levante. Va en la base de Kustomize de todos los servicios con minReplicas ≥ 2.
- Cluster Autoscaler (o Karpenter): cuando el HPA pide 20 réplicas y no caben en los nodos, los pods quedan
Pending; el autoescalador del clúster añade nodos (y los quita al bajar la carga). Es responsabilidad de Plataforma/proveedor; para los equipos de servicio basta saber que "réplica pendiente durante minutos" significa que el clúster está creciendo, y que tarda 2-4 minutos: por eso Catálogo entra en Black Friday conminReplicassubido a 6 (un overlay de Kustomize temporal), no confiando solo en la reacción.
- El caso Black Friday: dimensionar y comprobar con k6
Datos de 01-05: ~3.000 pedidos/día en un día normal (≈0,03/s de media, con picos de 0,5/s); Catálogo unas 200 peticiones/s en hora punta. En Black Friday: catálogo ×20 (4.000/s) y pedidos ×3 (picos de 1,5/s, unos 130 pedidos/minuto).
Dimensionar es medir cuánto aguanta una réplica y dividir. Para eso están las pruebas de carga con k6, en el repositorio de cada servicio bajo pruebas/carga/:
// servicio-catalogo/pruebas/carga/catalogo.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 200 }, // rampa hasta 200 usuarios virtuales
{ duration: '5m', target: 200 }, // meseta
{ duration: '2m', target: 1000 }, // pico ×5 sobre la meseta
{ duration: '5m', target: 1000 },
{ duration: '2m', target: 0 } // bajada
],
thresholds: {
http_req_duration: ['p(95)<300'], // p95 por debajo de 300 ms
http_req_failed: ['rate<0.01'], // menos del 1 % de errores
checks: ['rate>0.99']
}
};
const IDS = ['p-501,p-777', 'p-501', 'p-777,p-812,p-903'];
export default function () {
const ids = IDS[Math.floor(Math.random() * IDS.length)];
const res = http.get(`${__ENV.URL_BASE}/v1/productos?ids=${ids}`, { headers: { 'X-Request-Id': `k6-${__VU}-${__ITER}` } });
check(res, { 'estado 200': (r) => r.status === 200, 'tiene productos': (r) => r.json('productos').length > 0 });
sleep(0.5);
}stagesdescribe la forma del tráfico: rampas y mesetas, con un pico que simula la avalancha. Cada usuario virtual (VU) ejecuta la función en bucle con medio segundo de pausa: 1.000 VU ≈ 2.000 peticiones/s si el servidor responde al instante.thresholdsconvierte la prueba en pasa/no pasa: son los mismos objetivos que el panel de Grafana de 06-01 pinta en rojo (300 ms) y que 06-05 formalizará como SLO. Se ejecuta en CI contra staging (k6 run -e URL_BASE=https://staging.api.techcorp.example pruebas/carga/catalogo.js) desde unJobdel pipeline de 05-03, en una etapa manual antes de campañas.X-Request-Idcon prefijok6-: los logs de la prueba se distinguen (y se pueden excluir) en Loki.
Cómo leer el resumen de k6:
http_req_duration..............: avg=48ms min=6ms med=31ms max=2.1s p(90)=110ms p(95)=240ms
✓ { expected_response:true }.: p(95)<300
http_req_failed................: 0.32% ✓ 41 ✗ 12760
http_reqs......................: 12801 1830/s
vus_max........................: 1000p(95)=240mscon1830/ssobre 2 réplicas ⇒ una réplica sostiene ~900 peticiones/s dentro del objetivo. Para 4.000/s hacen falta al menos 5; con margen ×2 para el HPA y el ruido,minReplicas: 6en la campaña ymaxReplicas: 20de techo. Conrequests.cpu: 200m, la CPU al 60 % se cruza mucho antes que las 150 peticiones/s por pod, así que la métrica de CPU será la que escale en la práctica.- Un
max=2.1saislado conp(95)sano suele ser un arranque de pod o un GC; se comprueba en la traza (06-02) antes de preocuparse. - Si el p95 no baja al añadir réplicas, el cuello está detrás: MongoDB, red o el gateway. Ahí entran los apartados siguientes.
- Pedidos ×3 son 1,5 pedidos/s: sus 2 réplicas sobran de largo; lo que hay que vigilar en Pedidos es la saga (RabbitMQ, Inventario, Pagos), no HTTP.
- Caché: Redis en Catálogo y caché HTTP en el gateway
La forma más barata de servir 4.000 peticiones/s es no calcularlas. GET /v1/productos?ids= en 03-01 ya devuelve Cache-Control: public, max-age=30; ahora añadimos caché en dos capas:
Cache-aside en productosServicio con Redis. Antes de ir a MongoDB se mira Redis; si falta, se lee, se guarda con TTL y se devuelve:
// servicio-catalogo/src/servicios/productosServicio.js
function crearProductosServicio({ repositorio, redis, ttlSegundos = 30, logger }) {
async function obtenerPorIds(ids) {
const claves = ids.map((id) => `producto:${id}`);
const cacheados = await redis.mget(claves); // una sola ida y vuelta
const resultado = new Map();
const faltan = [];
ids.forEach((id, i) => (cacheados[i] ? resultado.set(id, JSON.parse(cacheados[i])) : faltan.push(id)));
if (faltan.length) {
const leidos = await repositorio.buscarPorIds(faltan);
const pipeline = redis.pipeline();
for (const p of leidos) { resultado.set(p.id, p); pipeline.set(`producto:${p.id}`, JSON.stringify(p), 'EX', ttlSegundos); }
await pipeline.exec();
logger.debug({ aciertos: ids.length - faltan.length, fallos: faltan.length }, 'caché de productos');
}
return ids.map((id) => resultado.get(id)).filter(Boolean);
}
return { obtenerPorIds };
}mgetypipelineagrupan las operaciones: una petición con 3 ids hace 2 idas y vuelta a Redis, no 6.- El TTL de 30 s coincide con el
max-ageHTTP: nadie ve un precio más viejo de lo que ya prometíamos. - Invalidación por evento: Catálogo consume su propio evento
producto.actualizado(publicado cuando el equipo de Experiencia de compra cambia un producto) y ejecutaredis.del('producto:p-501'); así un cambio de precio se ve al instante y no a los 30 s. Un contadorcache_aciertos_total{resultado="acierto|fallo"}(06-01) mide la eficacia: en Catálogo, con 30 s de TTL, se espera > 95 % de aciertos. - Si Redis no responde, se salta la caché (timeout de 50 ms y
catchque logueawarny va a MongoDB): la caché nunca puede ser causa de fallo (06-03).
¿Rompe esto la regla de Marta de 04-01 ("dos motores de base de datos, PostgreSQL y MongoDB, y punto")? No: Redis aquí es una caché, no una base de datos: no guarda nada que no esté en MongoDB, se puede vaciar en cualquier momento (FLUSHDB) y el servicio sigue funcionando. La regla es sobre dónde vive la verdad, no sobre qué procesos hay en el clúster. Redis se despliega con Helm en techcorp (una instancia con réplica, 512 Mi) y se configura con REDIS_URL en el ConfigMap.
Caché HTTP en el gateway y CDN. Traefik (o un CDN delante) puede cachear respuestas public, max-age=30 de GET /v1/productos por URL: el 90 % de las lecturas de un producto popular en Black Friday ni siquiera llega a Catálogo. Requiere que la clave de caché sea estable (los ids ordenados: el cliente de 04-04 ya los ordena) y que las respuestas personalizadas (GET /v1/pedidos, con Authorization) lleven Cache-Control: private, no-store.
- Pools de conexiones: la cuenta que hay que hacer
Cada réplica abre un pool de conexiones a PostgreSQL. La cuenta que hay que hacer antes de subir maxReplicas:
conexiones a PostgreSQL = réplicas × tamaño del pool × procesos por réplica (+ relay + vigilante + jobs)Para servicio-pedidos: pg.Pool({ max: 10 }) × 2 réplicas + relay y vigilante en el mismo pool = 20 conexiones. Con maxReplicas: 10 serían 100 solo de Pedidos, y PostgreSQL trae max_connections = 100 por defecto para todos los servicios que compartan instancia (Clientes, Pagos, Inventario tienen bases separadas pero, en dev, el mismo servidor). Opciones:
- Bajar
maxdel pool: para un servicio Node con 100 ms por consulta, 10 conexiones sostienen ~100 consultas/s por réplica; suele sobrar. Pedidos pasa amax: 5. - Poner un pooler delante (PgBouncer en modo transacción): 200 conexiones de aplicación se multiplexan en 20 reales. Es lo habitual con muchas réplicas; Plataforma lo añade cuando algún servicio supera 8 réplicas.
- Configurar timeouts del pool (
connectionTimeoutMillis: 2000,idleTimeoutMillis: 30000) para que la espera por una conexión sea un error rápido (06-03), no un cuelgue. - Vigilar la saturación (USE, 06-01):
prom-clientpuede exponerpg_pool_esperando(pool.waitingCount) ypg_pool_activas; una cola de espera creciente con CPU baja es la firma de un pool pequeño.
Lo mismo aplica al driver de MongoDB (maxPoolSize, 100 por defecto: demasiado para 20 réplicas → 20) y a los canales de RabbitMQ (uno por consumidor y uno para publicar, no uno por mensaje).
- Consultas, índices y el event loop de Node
Escalar réplicas no arregla una consulta lenta: la multiplica. Herramientas por orden de uso:
EXPLAIN ANALYZE en PostgreSQL. La consulta de obtener(id) de Pedidos (04-04) hace LEFT JOIN a clientes_ref (la copia local de datos del cliente que mantiene el consumidor pedidos.clientes):
EXPLAIN ANALYZE
SELECT p.*, c.nombre AS cliente_nombre
FROM pedidos p LEFT JOIN clientes_ref c ON c.id = p.cliente_id
WHERE p.id = 'ped-88213';Nested Loop Left Join (cost=0.71..16.76 rows=1 width=180) (actual time=0.041..0.043 rows=1 loops=1)
-> Index Scan using pedidos_pkey on pedidos p (actual time=0.022..0.023 rows=1 loops=1)
Index Cond: (id = 'ped-88213'::text)
-> Index Scan using clientes_ref_pkey on clientes_ref c (actual time=0.010..0.010 rows=1 loops=1)
Planning Time: 0.180 ms
Execution Time: 0.071 msDos Index Scan: perfecto. Lo que hay que temer es un Seq Scan sobre pedidos en la consulta de listado por cliente (WHERE cliente_id = $1 ORDER BY creado_en DESC), que a 3.000 pedidos/día en un año recorre un millón de filas: CREATE INDEX pedidos_cliente_creado_idx ON pedidos (cliente_id, creado_en DESC) y la paginación por cursor de 03-01 (WHERE (creado_en, id) < ($cursor…)) usan el mismo índice y mantienen el coste constante aunque la tabla crezca. La extensión pg_stat_statements dice qué consultas consumen más tiempo total; es lo primero que se mira cuando la BD está alta de CPU.
Índices en MongoDB. asegurarIndices() de 04-02 crea { _id } (implícito) y { categoria: 1, nombre: 1 }; db.productos.find({...}).explain('executionStats') debe mostrar IXSCAN, no COLLSCAN, y totalDocsExamined cercano a nReturned.
Un solo hilo. Node atiende miles de conexiones con un hilo porque nunca lo bloquea; una operación síncrona de 100 ms (el calcularRecomendaciones que la traza de 06-02 destapó, un JSON.parse de 20 MB, bcrypt síncrono, una regex catastrófica) detiene todas las peticiones de esa réplica durante 100 ms. La métrica nodejs_eventloop_lag_seconds (06-01) lo delata; el remedio es hacerlo asíncrono, sacarlo a un evento, o, si es cómputo puro e imprescindible, moverlo a worker_threads (mención: un pool de hilos para tareas CPU-intensivas; en TechCorp no hay ninguna).
Detalles que suman: compression() en Express para respuestas grandes (listados de productos: 60-80 % menos bytes), keep-alive en los clientes HTTP (fetch de Node 20 lo hace por defecto: evita un handshake TCP por petición entre Pedidos y Catálogo), no serializar cuerpos gigantes en eventos (pedido.creado lleva ids y cantidades, no el catálogo entero) y prefetch de RabbitMQ acorde al coste del mensaje: 10 para Inventario (consultas rápidas), 2-3 para Pagos (llamadas de segundos al PSP).
- Escalar la mensajería: colas competidoras y orden
Varias réplicas de Inventario consumiendo inventario.pedidos es el patrón de consumidores competidores: RabbitMQ reparte los mensajes en round-robin entre los consumidores conectados, cada mensaje se entrega a uno, y el rendimiento crece casi linealmente hasta que la BD dice basta. Es lo que KEDA explota en el apartado 3.
El precio es el orden: dos mensajes del mismo pedido (pedido.creado y, un segundo después, pedido.cancelado por un cliente arrepentido) pueden ir a réplicas distintas y procesarse en orden inverso. Cómo se trata en TechCorp:
- Diseñar los consumidores para desorden: la máquina de estados de Pedidos ignora transiciones imposibles (
pago.confirmadosobreCANCELADOno hace nada, 06-03) y cada evento llevaocurrido_enpara descartar los más antiguos que el estado actual. Es la solución que ya tenemos y basta para el 99 % de los casos. - Particionar por clave cuando el orden es imprescindible: el exchange
x-consistent-hashde RabbitMQ (o Kafka con particiones porpedidoId, 03-02) envía todos los mensajes de un mismopedidoIda la misma cola/consumidor. Más complejo; TechCorp no lo necesita hoy. - El caso opuesto: un solo consumidor garantiza orden pero no escala; solo para colas de muy bajo volumen.
Escalar el propio RabbitMQ: en producción, clúster de 3 nodos con quorum queues (x-queue-type: quorum, replicadas por Raft: un nodo caído no pierde la cola) en lugar de las clásicas; lo declara Plataforma en el chart de 05-02 y no cambia el código.
- Escalar la base de datos: réplicas de lectura y particionado
Los servicios se escalan solos; la base de datos es el techo. Palancas, de menor a mayor esfuerzo:
- Menos consultas: caché (apartado 6), pools correctos (7), índices (8). Resuelve la mayoría de los casos.
- Réplicas de lectura: PostgreSQL con streaming replication; el
PedidoRepositoriousa dos pools (PEDIDOS_DB_URLpara escrituras y la saga,PEDIDOS_DB_LECTURA_URLpara las vistas de consulta del CQRS de 02-05:GET /v1/pedidos?clienteId=). Precio: retraso de replicación de milisegundos a segundos; unGET /v1/pedidos/{id}justo después delPOSTdebe ir al primario o aceptarRetry-After(03-01 ya devuelve 202 yLocation, así que el cliente espera). En un proveedor gestionado es una casilla. - Particionado (sharding, mención): repartir
pedidospor rango de fecha (PARTITION BY RANGE (creado_en), mensual, para archivar) o por hash decliente_identre varias instancias. TechCorp lo evaluará cuando la tabla supere decenas de millones de filas; hoy es innecesario. MongoDB tiene sharding nativo por clave, con la misma advertencia. - Escalado vertical de la instancia: la palanca legítima para las bases de datos: más CPU, RAM y IOPS. Cuesta dinero, no complejidad.
Regla: primero medir (pg_stat_statements, latencia de consultas en las trazas de 06-02), luego elegir la palanca más barata que resuelva el síntoma.
- Tabla "síntoma → dónde mirar → remedio"
| Síntoma (métrica de 06-01) | Dónde mirar | Remedio probable |
|---|---|---|
p95 de http_request_duration_seconds sube con la carga y CPU del pod > 80 % |
kubectl top pod, HPA |
Más réplicas: HPA con requests bien puestos |
p95 sube pero CPU baja y pg_pool_esperando > 0 |
Pool de pg, pg_stat_activity |
Ampliar pool o PgBouncer; revisar consultas lentas |
nodejs_eventloop_lag_seconds > 0,1 s |
Traza con huecos (06-02), código síncrono | Eliminar la operación bloqueante; worker_threads si es cómputo |
rabbitmq_queue_messages_ready{queue="inventario.pedidos"} crece sin parar |
Réplicas de Inventario, su BD | KEDA / más consumidores; comprobar que la BD de Inventario aguanta |
cache_aciertos_total{resultado="fallo"} alto en Catálogo |
Redis (¿caído? TTL muy bajo), invalidaciones excesivas | Revisar TTL, tamaño de Redis, evento producto.actualizado |
| BD al 100 % de CPU con réplicas de servicio ociosas | pg_stat_statements, EXPLAIN ANALYZE |
Índices, caché, réplica de lectura |
Pods Pending al escalar |
kubectl describe pod ("Insufficient cpu") |
Cluster Autoscaler; ajustar requests; subir minReplicas antes de la campaña |
| Muchos 429 en el gateway con servicios sanos | Rate limit del gateway (03-04) | Subir el límite para clientes legítimos; el 429 está haciendo su trabajo |
| Latencia sube tras un despliegue con la misma carga | Comparar por version (06-01), traza |
Regresión de rendimiento: revertir el canary (05-04) y perfilar |
Errores Comunes y Consejos
- HPA sobre un
Deploymentconreplicasfijado en Git. Argo CD (05-03) y el HPA se pelean: uno pone 2, el otro 8, cada minuto. Quitarreplicasdel manifiesto (oignoreDifferencesen Argo). requestsinventados. Conrequests.cpu: 1000mpara un servicio que usa 50m, el HPA nunca supera el 5 % y jamás escala; con 10m, escala al máximo enseguida. Medir.- Escalar el servicio y olvidar la base de datos. 20 réplicas × 10 conexiones = 200 >
max_connections. La cuenta del apartado 7 antes de subir el máximo. - Caché sin invalidación ni TTL. Precios viejos durante horas. TTL siempre; evento de invalidación cuando importe.
- Caché que se convierte en dependencia. Si Redis cae y el servicio falla, la caché ha dejado de ser caché. Timeout corto y fallback a la BD.
- Pruebas de carga contra producción o desde el portátil. Los resultados no significan nada (o tiran producción). Staging con datos y tamaño realistas, generador de carga dentro del clúster o en una máquina dedicada.
- Fijarse en la media.
avg=48msesconde un p95 de 240 y un p99 de 900. Percentiles siempre. - Añadir réplicas para arreglar una consulta sin índice. La BD empeora. Índice primero.
- Consejo: hacer la prueba de k6 antes de cada campaña y guardar el resumen junto al tag de la imagen; comparar con la anterior detecta regresiones.
- Consejo: cada servicio documenta su "hoja de capacidad": peticiones/s por réplica al p95 objetivo,
requests/limits, tamaño del pool,min/maxdel HPA y la fecha de la última prueba de carga.
Ejercicios
Ejercicio 1: dimensionar Pedidos y su base de datos
Una réplica de servicio-pedidos sostiene 40 POST /v1/pedidos/s al p95 < 500 ms con pg.Pool({ max: 5 }). Para el Black Friday se esperan picos de 1,5 pedidos/s de HTTP más el consumidor de la saga y el relay. Propón minReplicas/maxReplicas, calcula las conexiones máximas a PostgreSQL y di si hace falta PgBouncer.
Ejercicio 2: KEDA para Notificaciones
servicio-notificaciones consume notificaciones.pedidos y envía correos a través de un proveedor que admite 20 envíos/s por conexión. Escribe el ScaledObject con valores justificados y explica qué límite impide subir maxReplicaCount sin más.
Ejercicio 3: diagnóstico
Durante la prueba de k6, el p95 de Catálogo pasa de 90 ms con 200 VU a 1,2 s con 1.000 VU; el HPA ha subido a 12 réplicas, la CPU media de los pods es del 25 % y cache_aciertos_total{resultado="acierto"} es del 40 %. ¿Dónde está el cuello de botella y qué dos cosas comprobarías primero?
Soluciones
Ejercicio 1
1,5 pedidos/s es el 4 % de la capacidad de una réplica: minReplicas: 2 (disponibilidad, no capacidad) y maxReplicas: 4 sobra con holgura para picos y para el trabajo asíncrono. Conexiones: 4 réplicas × 5 = 20 al primario, más el Job de migraciones ocasional (05-02) y el vigilante/relay que comparten el pool: ~20-22. Con max_connections = 100 compartidas por Pedidos, Clientes, Pagos e Inventario, Pedidos consume una quinta parte: aceptable, no hace falta PgBouncer todavía. Se anota en la hoja de capacidad que si maxReplicas supera 8 o se añaden más servicios a la misma instancia, se revisa.
Ejercicio 2
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: servicio-notificaciones, namespace: techcorp }
spec:
scaleTargetRef: { name: servicio-notificaciones }
minReplicaCount: 1 # las notificaciones toleran segundos de espera; 1 basta en reposo
maxReplicaCount: 5
cooldownPeriod: 300
triggers:
- type: rabbitmq
metadata: { protocol: amqp, queueName: notificaciones.pedidos, mode: QueueLength, value: "100" }
authenticationRef: { name: keda-rabbitmq-auth }value: 100 porque un correo tarda ~50 ms y 100 mensajes se vacían en 5 s por réplica; no hay prisa de negocio. El límite real es el proveedor de correo: 20 envíos/s por conexión y probablemente una cuota global; 5 réplicas × 20 = 100/s, que ya está cerca de la cuota contratada. Subir maxReplicaCount sin subir la cuota solo produciría 429 del proveedor (que el consumidor trataría como transitorio y enviaría a notificaciones.pedidos.reintento, 06-03). El límite viene de la dependencia externa, no del clúster.
Ejercicio 3
Réplicas al 25 % de CPU y latencia disparada: el cuello no es Catálogo, está detrás. La pista es la caché: 40 % de aciertos es muy bajo para un TTL de 30 s con productos populares; el 60 % de las peticiones bajan a MongoDB. Comprobar primero (1) Redis: ¿está respondiendo o el timeout de 50 ms está saltando (warn en Loki "caché no disponible") de modo que todo va a la BD?; ¿los ids llegan en distinto orden y generan claves distintas? (aquí las claves son por producto, así que sería la ejecución del pipeline); (2) MongoDB: db.currentOp(), CPU de la instancia y explain de buscarPorIds: con _id indexado debería ser rápido; si la instancia está saturada de conexiones (12 réplicas × maxPoolSize 100 = 1.200), ahí está el problema: bajar maxPoolSize a 20 y, si es Redis, arreglarlo antes de volver a lanzar la prueba. Añadir más réplicas no ayudaría; probablemente empeoraría.
Conclusión
Escalar en TechCorp deja de ser "una máquina más grande" para convertirse en un conjunto de decisiones medibles. Un servicio de la plantilla es escalable porque no guarda estado, limita sus conexiones y arranca y para con limpieza; el HorizontalPodAutoscaler de servicio-catalogo (2-20 réplicas, CPU al 60 % y http_requests_per_second vía Prometheus Adapter, con behavior asimétrico) y el ScaledObject de KEDA para servicio-inventario sobre inventario.pedidos deciden las réplicas; requests reales, PodDisruptionBudget y el Cluster Autoscaler sostienen esas decisiones; k6 (pruebas/carga/catalogo.js, p(95)<300) las comprueba antes del Black Friday. Debajo, el rendimiento se gana con caché cache-aside en Redis (TTL 30 s, invalidación por producto.actualizado, nunca dependencia), pools de conexiones que caben en max_connections, índices comprobados con EXPLAIN ANALYZE, un event loop sin bloqueos, consumidores competidores que toleran el desorden y réplicas de lectura para las vistas de consulta. Tenemos observabilidad, resiliencia y capacidad; lo que falta es acordar qué es "ir bien" y qué hacer cuando no: cuánta latencia y cuántos errores aceptamos, cuándo debe sonar el teléfono de la guardia y cómo se gestiona y se aprende de un incidente. Ese es el cierre del módulo: SLOs, alertas y gestión de incidentes.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
