Módulo 5: Rollback And Incident Response

Severidad y escalación: quién se despierta

Descripción

El runbook de la lección 4 termina en communicate, y esa comunicación incluyó, casi de pasada, la palabra SEV2. Esta lección le da contenido real a esa etiqueta: severidad — una clasificación explícita de qué tan grave es un incidente, que decide algo muy concreto: a quién se avisa, con qué urgencia, y si eso incluye despertar a alguien a las tres de la mañana. Sin un criterio de severidad, un equipo termina o despertando a todo el mundo por cualquier cosa, o dejando que un incidente serio pase desapercibido hasta la mañana siguiente — los dos extremos son un fallo del mismo tipo: nadie decidió con criterio quién necesitaba enterarse.

Conexión con el módulo. La severidad no cambia qué se hace durante el incidente —el runbook de la lección 4 y el orden de la lección 5 siguen aplicando igual—; cambia la urgencia y el alcance de a quién se involucra. Esta lección construye classifySeverity(), el modelo que decide eso con criterio, y prepara el terreno para la lección 7, donde la comunicación de cierre depende directamente de qué tan severo fue el incidente.

Una analogía: el triage de la sala de emergencias

Una sala de emergencias no atiende a los pacientes en el orden en que llegaron — los clasifica primero. Alguien con una fractura de dedo espera; alguien con dificultad respiratoria severa pasa de inmediato, sin importar quién llegó antes. Esa clasificación —el triage— no depende de cuánto le duele a cada paciente en su propia percepción, ni de cuán ruidosamente pide atención: depende de criterios objetivos, decididos de antemano por gente entrenada, sobre qué tan grave es cada situación y qué tan rápido puede empeorar sin intervención.

Clasificar la severidad de un incidente de software cumple exactamente esa función. No todos los guardrails rotos son iguales: uno puede afectar a toda la base de usuarios con el checkout completamente caído, y otro puede afectar a un porcentaje pequeño con una degradación que tiene un workaround disponible. Tratar a los dos con la misma urgencia —despertar a todo el equipo por igual, o dejar a los dos para el día siguiente— es ignorar la información que el triage está diseñado para capturar.

Ejemplo trabajado: classifySeverity() sobre tres incidentes de Mercado

// classifySeverity: SEV1 (todos despiertos, ya), SEV2 (on-call + lead), SEV3 (horario
// habil, sin pager). El % de usuarios afectados solo no alcanza -- importa tambien si
// hay una funcion central caida y si existe un workaround.
function classifySeverity({ percentUsersAffected, coreFunctionDown, hasWorkaround }) {
  if (coreFunctionDown && !hasWorkaround) {
    return { severity: 'SEV1', paged: 'on-call + incident commander + liderazgo, de inmediato' };
  }
  if (percentUsersAffected >= 0.05 || (coreFunctionDown && hasWorkaround)) {
    return { severity: 'SEV2', paged: 'on-call + tech lead del equipo' };
  }
  return { severity: 'SEV3', paged: 'nadie fuera de horario -- se atiende el siguiente dia habil' };
}

const incidents = [
  {
    label: 'recommendations, rollout 10% (el caso de esta guia)',
    percentUsersAffected: 0.10,
    coreFunctionDown: false, // checkout SIGUE funcionando, solo mas lento
    hasWorkaround: true,     // el kill switch apaga recommendations sin tocar el resto del sitio
  },
  {
    label: 'checkout completo caido, sin forma de procesar pagos',
    percentUsersAffected: 1.00,
    coreFunctionDown: true,
    hasWorkaround: false,
  },
  {
    label: 'un icono roto en el carrusel de recommendations, cosmetico',
    percentUsersAffected: 0.01,
    coreFunctionDown: false,
    hasWorkaround: true,
  },
];

console.log('=== severidad de tres incidentes de Mercado ===\n');
incidents.forEach((i) => {
  const result = classifySeverity(i);
  console.log(i.label);
  console.log('  affected=' + (i.percentUsersAffected * 100) + '%  coreFunctionDown=' + i.coreFunctionDown + '  hasWorkaround=' + i.hasWorkaround);
  console.log('  -> ' + result.severity + '  |  paged: ' + result.paged + '\n');
});

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== severidad de tres incidentes de Mercado ===

recommendations, rollout 10% (el caso de esta guia)
  affected=10%  coreFunctionDown=false  hasWorkaround=true
  -> SEV2  |  paged: on-call + tech lead del equipo

checkout completo caido, sin forma de procesar pagos
  affected=100%  coreFunctionDown=true  hasWorkaround=false
  -> SEV1  |  paged: on-call + incident commander + liderazgo, de inmediato

un icono roto en el carrusel de recommendations, cosmetico
  affected=1%  coreFunctionDown=false  hasWorkaround=true
  -> SEV3  |  paged: nadie fuera de horario -- se atiende el siguiente dia habil

Los tres incidentes muestran los tres niveles, y vale la pena notar por qué cada uno cae donde cae. El incidente de recommendations —el caso central de esta guía— es SEV2: afecta al 10% de la base, un número por encima del umbral de 5%, pero el checkout sigue funcionando y existe un workaround inmediato (el kill switch). No es SEV1 porque nada crítico está completamente caído sin alternativa. El checkout caído por completo, sin forma de procesar pagos y sin ningún workaround, es el ejemplo claro de SEV1 — la combinación de "función central caída" y "sin workaround" activa la primera condición del modelo, sin importar el porcentaje exacto de usuarios. El ícono roto, afectando apenas al 1% y sin tocar ninguna función central, es SEV3 — algo real, que merece arreglarse, pero no algo que justifique sacar a nadie de su horario normal.

Por qué el modelo no usa solo el porcentaje de usuarios afectados

Fíjate en la primera condición del modelo: coreFunctionDown && !hasWorkaround dispara SEV1 sin mirar percentUsersAffected en absoluto. Esto es intencional, y vale la pena entender por qué. Un porcentaje bajo de usuarios afectados por algo verdaderamente crítico —por ejemplo, un 1% de transacciones de pago que están fallando silenciosamente, sin ningún workaround— puede ser mucho más grave que un porcentaje alto de usuarios viendo una degradación menor con un workaround disponible, como el caso de recommendations. Si el modelo solo mirara el porcentaje, terminaría subestimando incidentes pequeños en alcance pero catastróficos en impacto, y sobreestimando incidentes grandes en alcance pero manejables — exactamente el mismo tipo de error que el módulo 4 ya evitó al no confiar solo en un número aislado sin contexto.

Esto también explica por qué recommendations en rollout 10%, a pesar de afectar a 25,000 personas reales, no es un SEV1: el sistema de recomendaciones tiene un workaround inmediato y de bajo riesgo (el kill switch de la lección 2), y no forma parte de la función central de comprar en Mercado —el checkout sigue procesando pagos con normalidad durante todo el incidente—. Eso no significa que el incidente sea menor o que no importe: significa, específicamente, que no amerita despertar a liderazgo completo a las tres de la mañana — el on-call y el tech lead del equipo son suficientes para responder con el runbook ya construido.

Errores comunes

No definir ningún criterio de severidad, y despertar a todo el equipo por cualquier alerta (o a nadie, por ninguna). Qué pasa: sin un modelo como classifySeverity(), cada persona de guardia decide con su propio criterio si algo amerita escalar — algunos exageran la urgencia de incidentes menores (fatigando al equipo con alertas constantes), y otros subestiman incidentes serios (dejándolos sin atención hasta que empeoran). Por qué pasa: sin criterios explícitos y acordados de antemano, la decisión de "¿esto merece despertar a alguien?" queda a juicio individual, en un momento —de noche, bajo presión— donde el juicio individual es menos confiable. Cómo detectarlo: si dos personas distintas de guardia clasificarían el mismo incidente con severidades distintas, no existe un criterio compartido — solo intuiciones distintas. Cómo corregirlo: como el modelo de esta lección, define los criterios de severidad con el equipo, con calma, antes de que un incidente real los ponga a prueba.

Tratar la severidad como fija desde el momento en que se detecta, sin permitir que cambie. Qué pasa: un incidente se clasifica como SEV2 al inicio y nadie vuelve a evaluarlo, aunque la situación empeore —por ejemplo, si el guardrail roto de recommendations empezara a afectar, inesperadamente, al checkout también—. Por qué pasa: clasificar una vez se siente como una tarea completa, y volver a evaluar en medio de la respuesta puede sentirse como una distracción del trabajo de mitigar. Cómo detectarlo: si un incidente dura mucho tiempo o cambia de alcance, y la severidad asignada al inicio nunca se revisa, es una señal de que el proceso trata la clasificación como un evento único en vez de un estado que se actualiza. Cómo corregirlo: vuelve a correr classifySeverity() (o su equivalente humano) cada vez que la información disponible cambie de forma significativa durante el incidente, no solo al principio.

"Seguir porque el primario gana" también aplica aquí: usar un resultado positivo del experimento para justificar una severidad más baja de la que corresponde. Qué pasa: alguien argumenta que, como recommendations está ganando +18.75% en conversión, el incidente de latencia "no es tan grave" y debería bajarse de SEV2 a SEV3. Por qué pasa: el mismo sesgo que la lección 1 nombró sobre rollbackDecision() reaparece aquí, en otra forma: un resultado positivo grande hace que cualquier problema asociado se sienta menos urgente de lo que realmente es. Cómo detectarlo: si el argumento para bajar la severidad de un incidente menciona el resultado del experimento en vez de las variables reales del modelo —percentUsersAffected, coreFunctionDown, hasWorkaround—, el argumento está mezclando dos preguntas que deberían mantenerse separadas. Cómo corregirlo: como classifySeverity() en esta lección, la severidad se calcula exclusivamente con variables sobre el impacto del incidente en sí — el resultado del experimento que lo originó no es una de esas variables, ni debería serlo.

Ejercicios

Ejercicio 1 — Cambia una sola variable. Si el incidente de recommendations en rollout 10% hubiera ocurrido en rollout 1% (canary), con solo 2,500 usuarios expuestos (percentUsersAffected: 0.01) y el resto de las variables iguales, ¿qué severidad devolvería classifySeverity()? ¿Por qué tiene sentido ese cambio?

Ver solución

Con percentUsersAffected: 0.01, coreFunctionDown: false, y hasWorkaround: true, ninguna de las dos primeras condiciones se cumple (coreFunctionDown es false, y 0.01 >= 0.05 es false), así que el resultado sería SEV3 — no SEV2. Tiene sentido: el mismo tipo de problema, con el mismo workaround disponible, afecta a muchísima menos gente en la etapa canary que en rollout 10%. Esto ilustra por qué la rampa gradual de los módulos 2 y 3 no solo limita el radio de impacto de un bug — también limita, de forma natural, la severidad del incidente que ese bug puede generar si se detecta a tiempo, en una etapa temprana.

Ejercicio 2 — Encuentra el caso límite. ¿Qué severidad devolvería classifySeverity() para un incidente con percentUsersAffected: 0.05 exacto (ni más ni menos), coreFunctionDown: false, hasWorkaround: true? Revisa con cuidado el operador usado en la condición del modelo.

Ver solución

SEV2. La condición es percentUsersAffected >= 0.05, con un >= que incluye el valor exacto de 0.05 — así que un incidente que afecta a exactamente el 5% de los usuarios cae en SEV2, no en SEV3. Este es un buen ejercicio para notar algo importante sobre cualquier modelo con umbrales: los casos límite (exactamente 0.05, ni un poco más ni un poco menos) dependen del operador exacto usado (>= contra >), y vale la pena revisarlo explícitamente en vez de asumir cuál de los dos lados del límite corresponde a cada categoría.

Ejercicio 3 — Explica el triage sin usar la palabra "severidad". En dos o tres frases, explica a alguien nuevo en el equipo por qué Mercado no despierta a todo el equipo de liderazgo cada vez que algo se rompe, y tampoco espera hasta el día siguiente para atender cualquier incidente. Puedes usar la analogía de la sala de emergencias.

Ver solución

Un ejemplo de respuesta: "Hacemos lo mismo que una sala de emergencias con sus pacientes: no atendemos todo con la misma urgencia, ni ignoramos todo por igual hasta el día siguiente. Miramos qué tan grave es cada problema —¿está afectando algo esencial como los pagos? ¿tenemos una forma rápida de contenerlo sin esperar?— y con esa información decidimos si hace falta despertar a alguien de inmediato o si puede esperar tranquilamente hasta la mañana. Eso evita dos problemas: agotar al equipo con alertas por cosas menores, y dejar pasar algo serio sin que nadie se entere a tiempo." La idea central: la clasificación no es sobre qué tan molesto se siente un problema — es sobre qué tan rápido necesita intervención antes de que empeore.

Resumen y siguiente paso

En esta lección construiste classifySeverity(), el modelo que clasifica un incidente en SEV1, SEV2 o SEV3 según si una función central está caída, si existe un workaround, y qué porcentaje de usuarios está afectado — y decide, con ese resultado, a quién se avisa y con qué urgencia. Sobre el incidente real de recommendations en rollout 10%, la clasificación fue SEV2: serio, con on-call y tech lead involucrados, pero sin necesidad de despertar a liderazgo completo, porque el checkout sigue funcionando y el kill switch ofrece un workaround inmediato.

Antes de avanzar deberías poder: explicar por qué el modelo evalúa coreFunctionDown antes que el porcentaje de usuarios afectados; identificar el error de dejar que un resultado positivo del experimento influya en la severidad asignada; y clasificar un incidente nuevo, dado su alcance y sus condiciones, en uno de los tres niveles.

Ya tienes las cuatro piezas: la decisión (rollbackDecision()), el runbook, el orden correcto (mitigar antes de diagnosticar), y la severidad. La lección 7 cierra el ciclo con la pieza que falta: ¿cómo se mide, con un número concreto, qué tan rápido fue realmente la respuesta a este incidente — y cómo se comunica ese cierre?

Recursos

  • PagerDuty Incident Response Documentation, "Severity Levels" — response.pagerduty.com/before/severity_levels. La referencia directa de la industria sobre cómo definir niveles de severidad de antemano, y por qué las definiciones genéricas deben adaptarse a números concretos de cada organización — exactamente lo que classifySeverity() hace con sus umbrales. En inglés.
  • Atlassian, "Understanding incident severity levels" — atlassian.com/incident-management/kpis/severity-levels. Describe el esquema SEV1/SEV2/SEV3 con criterios similares a los de esta lección: alcance del impacto, disponibilidad de un workaround, y si una función central está afectada. En inglés.
  • Google SRE Book, Capítulo 14, "Managing Incidents" — sre.google/sre-book/managing-incidents. Describe el sistema de comando de incidentes de Google, donde la severidad determina explícitamente qué roles se activan y con qué urgencia — el mismo principio detrás del campo paged de classifySeverity(). En inglés.