El simulacro: el job pesado del 31 sube la CPU a 90% durante 4 min; el paso 1 pregunta "¿el USUARIO lo sufre?" → el p95 de endpoints plano, el error rate plano → NO page (la alerta de CPU no existe: la 46 la puso en ticket por síntoma-vs-causa). El criterio: página el SÍNTOMA del usuario; la causa en ticket con dash. El pánico de "¡CPU 90%!" muere en el paso 1.
Las 5 respuestas del incidente: (¿quiénes?) los checkouts de todos los tenants desde 14:22 (el log por tenant confirma: no es uno); (¿dónde?) el endpoint del checkout (el dash lo separa); (¿desde cuándo?) 14:22 (la anotación del deploy); (¿qué cambió?) la PR del prefetch (el diff); (¿cuánto impacto?) p95 2.8 s, ~180 checkouts afectados, budget del 46 quemándose a burn 9 — el impacto cuantificado es el que prioriza la mitigación.
Ejercicio 2 — El incidente
Las mitigaciones medidas: (a) flag: FEATURE_SEAT_PREFETCH=false + reload del settings cache (30 s) → p95 base en ~4 min; NADA se pierde (el flag solo apaga el prefetch). (b) rollback: 11 s de switch (41) + re-deploy del azul + los otros 2 features del deploy FUERA de prod (el rollback es el martillo: revierte todo). (c) scale-out: 3 réplicas más → el p95 baja a 1.9 s (disfraza: la contención en Redis sigue; la factura sube para siempre). La decisión: flag por quirúrgico y reversible; rollback el plan B si el flag no existe (lección: TODO feature de riesgo nace con su flag, la 27/41).
El guard del regreso: la revisión del 50 con la checklist "¿la optimización toca un recurso limitado? ¿qué pasa bajo rate limit/contención?" habría preguntado "¿el prefetch respeta el rate limiter?" — y el test del 36 con la mezcla bajo el límite lo mediría. La lección del postmortem: las "optimizaciones" entran por la puerta del review con el mismo rigor que las features (el caché mal puesto del 38 es el antipatrón más común de rendimiento).
Ejercicio 3 — La reproducción
El dataset anonimizado: 200 tokens de request con UUIDs sintéticos (mismo patrón de llegada: la tasa del 14:22), 1 evento con 20k asientos, el token del rate limiter agotado. Sin email/nombre/IP real (la 23: el patrón es el dato; la identidad, no).
El test rojo (integración + carga en miniatura):
python
@pytest.mark.django_db(transaction=True)
def test_prefetch_bajo_rate_limit_degrada_checkout(rate_limiter, evento, meseta_local):
rate_limiter.agota("prefetch") # el estado del incidentewithself.assertLess( # el p95 de 30 checkouts
p95(lambda: checkout(client, evento)), 0.8):
...
# FAILED: p95 = 2.71 s (la firma del incidente)
El fix elegido: el prefetch ASYNC (el 29: la disponibilidad "recién vista" se refresca en background, el request sirve del estado consistente) + el caché del 38 con single-flight para el caso sync. Test verde (p95 0.42 s) + meseta del 36 verde. El test de regresión: test_prefetch_no_bloquea_checkout_bajo_rate_limit — el nombre ES el postmortem en miniatura.
Ejercicio 4 — El postmortem
El documento (extracto):
markdown
# Postmortem: degradación del checkout — 2027-09-28 14:22 UTC
Impacto: 180 checkouts con p95 2.8 s (48 min), budget del 46: -12% del mes. Sin pérdida de dinero.
Timeline (UTC):
14:20 deploy a1b2c3d (PR "optimización disponibilidad": prefetch síncrono)
14:22 p95 checkout cruza 800 ms; alerta page del burn rate
14:31 trace+log localizan el span seat_availability_prefetch bajo rate limit
14:36 FEATURE_SEAT_PREFETCH=false → p95 base a las 14:40
Causas: raíz — el prefetch síncrono compite por el rate limit del Redis bajo contención.
Contribuyentes: (1) la PR entró como "optimización" sin test de carga; (2) el feature
sin flag en el primer deploy; (3) la revisión sin checklist de recursos limitados.
Acciones: [las 4 del §4 con issue/dueño/fecha]
El test blameless: la primera versión decía "el junior desplegó sin flag" → reescrito: "el despliegue no exige flag para features nuevas (el pipeline no lo pregunta)" — el sistema cambia (la política del 41), la persona no se marca. El tono cambia: el documento pasa de juicio a ingeniería.
Las acciones: P1 el test de regresión (protege HOY), P2 el canary automático del 41 (protege el próximo), P3 el runbook del rate_limited (protege al on-call). La sin-issue es un deseo: las 4 quedaron con issue, dueño y fecha — el postmortem sin acciones cerradas es una confesión sin penitencia.
Ejercicio 5 — El on-call sostenible
La métrica de madurez: incidente A (el del §2): MTTR 18 min (diagnóstico manual, sin runbook); game day B (Redis): MTTR 11 min (runbook de la 46); hallazgo C (drift del 44): 4 min (el plan lo caza). La tendencia baja EXPLICA la inversión: el módulo 10 compró minutos de MTTR con logs (45), métricas (46) y protocolo (47) — el MTTR es la métrica que justifica la observabilidad a los que facturan el tiempo.
El oncall.md final (2 páginas): protocolo de 5 pasos con comandos → las 5 alertas page con runbook (burn checkout, sagas, outbox, heartbeat, 5xx) → checklist del postmortem con la regla blameless → reglas del on-call de 1 (MOC, blackout de cambios durante incidente, recuperación agendada). El test del documento: el tú de las 3 a.m. lo sigue sin pensar — y el tú de la semana 30 lo agradece porque el sistema aprendió de cada incidente.
Resumen del profesor
Confirmar → acotar → mitigar → localizar → explicar: el usuario primero, la causa después, el ego nunca.
El incidente se resuelve con la cadena dash→trace→log→diff→flag; la mitigación quirúrgica (flag) vence al martillo (rollback) si la feature nació con flag.
El postmortem blameless con acciones cerradas es el único mecanismo por el que el sistema aprende; el MTTR descendente es la prueba.