Ejercicio 1 — El script
- Los dos p95 (cifras de referencia del entorno del curso): runserver ~850 ms (un proceso síncrono, sin pooling, modo debug-ish: mide el servidor de desarrollo), gunicorn 4 workers ~210 ms. La primera medición no era del sistema: era del juguete de desarrollo. El runserver existe para iterar HTML, no para sostener tráfico — la prueba de carga contra él es un dato de nada.
- El check de corrección:
check(detalle, { "schema del detalle": (r) => r.status === 200
&& typeof r.json().seats_available === "number" });Sin corrección, un endpoint que responde 500 rápido "gana" la prueba de carga: los thresholds mezclan velocidad y corrección, y checks>0.999 obliga a que el sistema conteste BIEN bajo presión.
Ejercicio 2 — El checkout
1-2. La meseta (200 VUs, 5 min, gunicorn+PG):
http_req_duration: p95=380ms p99=720ms ✓ thresholds
http_req_failed: 0.02% ✓
rate_409: 1.8% de los intentos de reserva ← contención normal (dos clickers, mismo asiento)
checks: 99.98% ✓Los 409 (3.6 de cada 200 intentos) son el sistema decidiendo limpiamente; el check de k6 los acepta y el Counter los registra — la tasa de 409 es DATO de contención (si se dispara al 30%, la mezcla es irreal o hay un bug de disponibilidad: los asientos que se muestran libres ya no lo están, la 12/32).
- Arena vs evento pequeño: p95 reserva en arenas 2.1 s vs 290 ms en pequeños. La causa: el UPDATE de los asientos del mismo evento contiende (el lock de la 10 serializa las filas del mismo evento) y el planificador (09) recorre 20k filas de asiento. Conclusión: la capacidad NO es un número único — es por evento. La partición por evento (55) y el índice
(event_id, status)son la respuesta; el "N concurrentes global" es la suma de las capacidades por evento con la mezcla.
Ejercicio 3 — El cuello
- El top de pg_stat_statements en la meseta:
total_time calls query
1.8e6 ms 4200 UPDATE seats SET status=$1 WHERE id IN (...) AND status='FREE'
4.2e5 ms 91000 SELECT ... FROM events WHERE uuid=$1
...El UPDATE explica el 74% del tiempo de BD: es el cuello. Las queries del listado (cacheadas, 12) ni aparecen: la intuición "el listado es lo lento" era marketing interno.
- El pool:
pg_stat_activitymuestra 38 conexionesidle in transactionen el pico (el patrón "abrir transacción, llamar a la pasarela, commitear" de la 10/32 mantiene el lock Y la conexión durante 1.2 s de latencia remota) — el p95 del checkout coincide con los momentos de espera de conexión. El finding: separar la TX local del remoto (la saga ya lo hace: cobrar fuera de la TX, 32) y ajustar CONN_MAX_AGE/pool (39). Dos hallazgos por el precio de un SELECT.
- Las 3 mesetas con delay de pasarela: 0/200/500 ms → p99 checkout 720/1100/1600 ms. El incremento es ~1:1 con el delay: la pasarela añade su latencia SIN saturar tu CPU. Conclusión honesta: puedes comprometer p95 de tu PARTE (BD+app), no de la cadena completa — por eso el SLO se escribe por componente y el breaker de la 54 protege al usuario del tercero lento.
Ejercicio 4 — La curva
- El pico de 400: degradación elegante si: p95 sube a 610 ms (no a 5 s), rate_409 sube al 4.2%, errores reales <0.3%, CPU <85%. Colapso sería: timeouts en cadena (el pool agotado bloquea todo), OOM del contenedor, errores 500 crecientes. El umbral de diseño del 55: la pendiente de la curva de latencia — degradación elegante tiene pendiente suave; el cuello duro (pool/lock) la tiene vertical.
- La descarga: la línea base volvió en 40 s, pero el log del worker mostró 1.2k tareas Celery acumuladas en el pico (los emails/webhooks de las reservas del pico) drenando 6 min más. El finding: la cola ABSORBE el pico (bien) pero el job de expiración (31) corriendo durante el drenaje contiende con las reservas nuevas (el skip_locked (10) lo resolvió sin corrupción). La capacidad de la cola y del worker es PARTE del número de capacidad: "soporta 200" incluye que los emails salgan en <5 min post-pico.
- El documento de capacidad (
load/results/2027-09-28.md):
# Capacidad TicketFlow — 2027-09-28 (commit a1b2c3d)
Hardware: staging 2 vCPU/4GB app, PG 16 2 vCPU, Redis compartido dev.
Seed: 50k eventos (200 arenas 20k asientos), 100k usuarios.
Escenario: mezcla 70/20/9/1, think 0.5-2s, meseta 200 VUs × 5m.
Resultados: p95 380ms / p99 720ms global; reserva p95 2.1s en arenas.
Capacidad declarada: 200 concurrentes mixtos ≈ 12k reservas/min.
Pico 400: degradación elegante (409 4.2%, sin 500s). Recuperación: 40s.
Hallazgos: UPDATE masivo de asientos (74% del tiempo BD); idle-in-tx con pasarela.
Mejoras medidas: bulk UPDATE → p95 reserva arenas 1.4s. Siguiente: partición por evento.15 líneas que valen más que una hora de reunión: hardware, seed, escenario, números, hallazgos, siguiente paso.
Ejercicio 5 — La mejora medida
- El before/after:
ANTES: UPDATE ... WHERE id IN (12 ids) × 4200 llamadas → 1.8e6 ms total
DESPUÉS: bulk_update en un solo statements + índice (event_id, status) → 5.4e5 ms
p95 reserva arenas: 2.1s → 1.4s (meseta 200) p95 global: 380 → 290 ms- La capacidad post-mejora: la meseta limpia sube a ~280 VUs (+40%): la mejora tocó el cuello EXACTO (medido, no adivinado). Siguiente: partición por evento (55) porque el cuello remanente es la contención POR EVENTO (las arenas), no global — el dato del ejercicio 2 lo señala.
- La decisión de producto: con 280 concurrentes medidos y el pico previsto de 400, dos caminos: (a) admitir por turnos al checkout (23: el rate limiter por evento como "puerta" de pre-venta) — costo de producto: UX de espera; costo de infra: ~0; (b) escalar a 3 réplicas (55): costo ~3× infra, PERO el lock por evento NO se reparte (la contención es por filas del mismo evento): escalar réplica ayuda al browse, NO al checkout de la arena llena. La respuesta correcta: (a) para la arena (el cuello es la fila), réplicas para el browse — la mezcla de decisión sale de los números, no del miedo.
Resumen del profesor
- Define los SLOs antes (p95/p99/errores/corrección) y cuenta el 409 como éxito: sin criterio previo, la carga es teatro.
- El cuello se localiza con pg_stat_statements + pg_stat_activity + desglose por endpoint; la latencia remota se mide aparte y no se arregla con servidores.
- La capacidad es un documento con fecha (hardware, seed, escenario) que alimenta decisiones de producto e infra — y cada mejora se re-mide con la misma corrida.