Módulo 6: Postmortems And Iterating

Iterar sobre el ganador: arreglar, relanzar, medir de nuevo

Descripción

La lección anterior dejó el ship del segundo ciclo descrito en una sola frase: "llamada rediseñada a modo asíncrono + cache; se relanza por la misma rampa". Esta lección ejecuta ese ship en detalle. Con los tres action items del postmortem ya resueltos —la llamada al motor de recomendaciones ya no bloquea, existe cache para las recomendaciones más solicitadas, el criterio de avance de la rampa ya exige una prueba de carga—, recommendations vuelve a pasar por exactamente la misma rampa que el módulo 3 diseñó: canary 1% → 10% → 50% → 100%. La pregunta de esta lección es simple de plantear y satisfactoria de contestar: ¿el arreglo funcionó?

Conexión con el módulo. Esta lección reutiliza rolloutPlan() sin cambiarle una línea respecto al módulo 3 — el mismo patrón que ya viste con guardrailWatch(), isEnabled() y buildPostmortem() a lo largo de esta guía: una función se construye una vez, y se reusa cada vez que el caso lo pide, sin reescribirla. Lo único que cambia entre el rollout original (módulo 3, lección 8) y este relanzamiento son los datos medidos en cada etapa — la lógica de decisión es idéntica.

Una analogía: la segunda prueba de manejo, después del ajuste

Cuando un fabricante de autos detecta un problema en la prueba de pista —digamos, que los frenos se calientan más de lo esperado en curvas cerradas repetidas—, no descarta el modelo entero ni empieza el diseño desde cero. Ajusta el sistema de frenos —un compuesto distinto, un disco más grande, una ventilación mejor— y vuelve a correr exactamente la misma batería de pruebas, en el mismo orden, bajo las mismas condiciones. No inventa una pista nueva para la segunda prueba, ni se salta pasos porque "ya aprendió la lección" — corre la misma rampa de pruebas, ahora con el arreglo puesto, precisamente porque esa rampa fue la que reveló el problema la primera vez, y es la mejor herramienta disponible para confirmar que el arreglo funciona en las mismas condiciones que lo expusieron.

El relanzamiento de recommendations es exactamente esa segunda prueba de pista. La rampa no cambia —sigue siendo canary 1% → 10% → 50% → 100%, con el mismo techo de latencia de 800ms—; lo que cambia es que, esta vez, el sistema que la atraviesa ya tiene el arreglo puesto.

Ejemplo trabajado: rolloutPlan() sobre el relanzamiento de recommendations

// rolloutPlan: SIN cambios respecto al modulo 3. Recorre las etapas EN
// ORDEN y decide ADVANCE/HOLD segun el criterio de cada una.
function rolloutPlan(stages) {
  const results = [];
  let halted = false;
  for (const s of stages) {
    if (halted) { results.push({ ...s, decision: 'NOT_REACHED' }); continue; }
    const decision = s.advanceIf(s.measured) ? 'ADVANCE' : 'HOLD';
    results.push({ ...s, decision });
    if (decision === 'HOLD') halted = true;
  }
  return results;
}

const ceiling = 800; // el mismo techo de siempre; no cambio con el fix

// La MISMA rampa del modulo 3, corrida de nuevo despues del fix del
// postmortem (llamada asincrona + cache al motor de recomendaciones).
const relaunchPlan = [
  { percent: 0.01, label: 'canary 1%', measured: { p95Latency: 674 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.10, label: 'rollout 10%', measured: { p95Latency: 738 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.50, label: 'rollout 50%', measured: { p95Latency: 762 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 1.00, label: 'rollout 100%', measured: { p95Latency: 781 }, advanceIf: (m) => m.p95Latency <= ceiling },
];

console.log('=== Re-lanzamiento de recommendations, despues del fix (techo p95=' + ceiling + 'ms) ===\n');
const results = rolloutPlan(relaunchPlan);
results.forEach((s) => {
  console.log(s.label.padEnd(14) + 'p95=' + String(s.measured.p95Latency).padStart(4) + 'ms  -> ' + s.decision);
});

const allAdvanced = results.every((s) => s.decision === 'ADVANCE');
console.log('\nLa rampa completa avanzo sin ningun HOLD: ' + allAdvanced);

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

=== Re-lanzamiento de recommendations, despues del fix (techo p95=800ms) ===

canary 1%     p95= 674ms  -> ADVANCE
rollout 10%   p95= 738ms  -> ADVANCE
rollout 50%   p95= 762ms  -> ADVANCE
rollout 100%  p95= 781ms  -> ADVANCE

La rampa completa avanzo sin ningun HOLD: true

Compara este resultado con el del módulo 3: ahí, la misma rampa se detenía en rollout 10% con p95Latency=910ms, muy por encima del techo. Esta vez, la etapa de 10% mide 738ms — bajo el techo, y de hecho más bajo que el número que había en canary la primera vez (910ms en la etapa de 10% del intento original, contra 738ms ahora). El arreglo no solo evitó que se repitiera el problema — dejó margen real: incluso en rollout 100%, con toda la base expuesta, p95Latency llega a 781ms, todavía 19ms por debajo del techo. La rampa completa avanza, etapa por etapa, sin un solo HOLD — la primera confirmación concreta de que los tres action items del postmortem (lección 3) atacaron la causa correcta.

Profundización: por qué reusar la MISMA rampa, y no diseñar una nueva "más cuidadosa"

Podría parecer, a primera vista, que después de un incidente conviene diseñar una rampa más lenta para el relanzamiento —etapas más chicas, más tiempo de espera entre cada una— como forma de "tener más cuidado" la segunda vez. Vale la pena resistir ese impulso, y la razón es la misma que explica por qué el fabricante de autos de la analogía no inventa una pista nueva: la rampa original ya demostró que funciona para detectar el problema exacto que apareció —se detuvo en 10% la primera vez, exactamente donde debía—. Diseñar una rampa distinta para el relanzamiento no la hace más segura; la hace distinta, lo que significa que ya no se sabe con la misma certeza cómo se hubiera comportado ante el problema original.

Esto no significa que la rampa nunca deba cambiar — de hecho, uno de los tres action items del postmortem (lección 3) fue, precisamente, agregar una prueba de carga al criterio de avance, un cambio real a la rampa. La diferencia es que ese cambio está justificado por un factor contribuyente específico, documentado en el postmortem, no por una sensación general de "vamos más despacio por si acaso". Cambiar la rampa sin una razón trazable a un factor contribuyente es, en el fondo, el mismo tipo de decisión sin evidencia que el módulo 1 ya advirtió contra hacer con umbrales de guardrail: se ve prudente, pero no está anclada en nada concreto.

Errores comunes

Relanzar sin haber confirmado que los tres action items del postmortem ya están cerrados. Qué pasa: el equipo, ansioso por recuperar el valor de negocio de recommendations, relanza la rampa antes de que el rediseño de la llamada, el cache, o la prueba de carga en el criterio de avance estén completamente implementados. Por qué pasa: la presión de negocio por un resultado que ya se sabe que gana (+18.75% en conversión) es real, y esperar a que los tres action items cierren se siente como una demora costosa. Cómo detectarlo: si al preguntar "¿ya está desplegado el cache de recomendaciones?" la respuesta es "todavía no, pero debería estar bien igual", el relanzamiento se está haciendo con la causa raíz solo parcialmente resuelta. Cómo corregirlo: los tres action items del postmortem existen porque los tres factores contribuyentes juntos causaron el incidente — relanzar con solo uno o dos resueltos es apostar a que los factores restantes no vuelvan a combinarse de la misma forma, sin ninguna evidencia real de que eso sea cierto.

Interpretar que la rampa avanzó limpia como "ya no hace falta seguir vigilando". Qué pasa: al ver allAdvanced: true, el equipo desactiva el dashboard de monitoreo del módulo 4, asumiendo que un relanzamiento exitoso significa que el riesgo desapareció por completo. Por qué pasa: cuatro etapas seguidas en ADVANCE se sienten como una confirmación definitiva, y es tentador tratar "pasó la rampa" como "ya no hay nada que vigilar nunca más". Cómo detectarlo: si nadie puede decir cuál fue el p95Latency de recommendations la semana pasada —solo el dato del día del relanzamiento—, el monitoreo se abandonó demasiado pronto. Cómo corregirlo: guardrailWatch() del módulo 4 no fue una herramienta de un solo uso para el rollout — sigue siendo la disciplina correcta para cualquier feature en producción, indefinidamente, aunque con menor urgencia una vez que el riesgo conocido ya se contuvo.

Confundir "la latencia se sostuvo" con "el ganador ya está confirmado del todo". Qué pasa: el equipo celebra el relanzamiento exitoso y cierra el caso de recommendations por completo, sin conectar este resultado con la pregunta que la lección 5 dejó explícitamente abierta: ¿el lift de conversión se sostiene, o era efecto novedad? Por qué pasa: la latencia era el problema visible y dramático —el que causó el incidente—, así que resolverla se siente como resolver "el problema" en general, dejando de lado que el guardrail y la métrica primaria son preguntas distintas. Cómo detectarlo: si el reporte de este relanzamiento no menciona ningún plan para volver a medir checkoutConversion en las semanas siguientes, el ciclo del loop ship → measure → learn quedó incompleto en la parte de measure. Cómo corregirlo: esta lección resuelve el guardrail (p95Latency), no la métrica primaria (checkoutConversion) — la lección 7 construye exactamente el chequeo que falta para cerrar esa segunda pregunta.

Ejercicios

Ejercicio 1 — Compara los dos rollouts, número por número. Usando los datos del módulo 3 (canary 720ms, rollout 10% 910ms) y los de esta lección (canary 674ms, rollout 10% 738ms), calcula cuánto bajó p95Latency en la etapa de 10%, en milisegundos y en porcentaje.

Ver solución

La bajada en milisegundos: 910 - 738 = 172ms. En porcentaje: 172 / 910 * 100 ≈ 18.9% de reducción respecto al valor original que rompió el guardrail. Vale la pena notar algo interesante: el valor de canary también bajó (de 720ms a 674ms, una reducción de 46ms, ≈6.4%), aunque en canary el guardrail nunca se había roto — el arreglo (llamada asíncrona + cache) mejoró la latencia en todas las etapas, no solo en la que había fallado, porque atacó una causa que afecta a cualquier volumen de tráfico, solo que se volvía crítica recién a partir de cierto volumen.

Ejercicio 2 — Simula un relanzamiento que todavía no está listo. Supón que solo se implementó el cache (uno de los tres action items), pero la llamada al motor de recomendaciones sigue siendo bloqueante. Con esos datos parciales, la latencia en rollout 10% mejora, pero solo baja a 830ms (todavía sobre el techo de 800ms). ¿Qué decisión daría rolloutPlan() en esa etapa, y qué error de los "Errores comunes" de esta lección ilustra este escenario?

Ver solución

Con p95Latency: 830 y ceiling: 800, la condición 830 <= 800 es false, así que rolloutPlan() marcaría esa etapa como HOLD, deteniendo la rampa exactamente igual que en el intento original —aunque con un valor más bajo que antes (830ms contra 910ms), sigue rompiendo el guardrail—. Este escenario ilustra el primer error común: relanzar con solo una parte de los action items resueltos puede mejorar el número sin resolver el problema por completo, y el guardrail —que no negocia con "mejoró, aunque no lo suficiente"— lo detecta igual que la primera vez.

Ejercicio 3 — Defiende la decisión de reusar la misma rampa. Un compañero de equipo propone diseñar una rampa completamente nueva para el relanzamiento, con etapas más pequeñas (0.5% → 5% → 25% → 100%) "para ser más conservadores después de lo que pasó". En 2-3 frases, explica por qué reusar la rampa original, con el ajuste puntual del criterio de avance (la prueba de carga), es preferible a diseñar una rampa nueva desde cero.

Ver solución

Una respuesta razonable: "La rampa original (canary 1% → 10% → 50% → 100%) ya demostró que funciona para detectar exactamente el tipo de problema que tuvimos — se detuvo en la etapa correcta la primera vez. Diseñar una rampa nueva no la hace más segura, la hace distinta, y perdemos la certeza de que se comporta igual de bien ante el mismo tipo de regresión. El único cambio que sí está justificado es el que ya identificamos en el postmortem: agregar la prueba de carga al criterio de avance — un ajuste puntual con una causa documentada, no un rediseño completo basado en una sensación general de cautela."

Resumen y siguiente paso

En esta lección ejecutaste la iteración concreta sobre el ganador: reutilizando rolloutPlan() sin cambios, recommendations relanzado —con la llamada al motor de recomendaciones ya asíncrona, con cache, y con el criterio de avance ya corregido— atravesó las cuatro etapas de la misma rampa del módulo 3 sin un solo HOLD, con p95Latency sosteniéndose entre 674ms y 781ms en todas las etapas, muy por debajo del techo de 800ms. Confirmaste que los tres action items del postmortem atacaron la causa correcta, y viste por qué reutilizar la rampa original —con su único ajuste documentado— es preferible a diseñar una nueva "por las dudas".

Antes de avanzar deberías poder: explicar por qué reusar la misma rampa del relanzamiento tiene más valor que diseñar una nueva; distinguir "el guardrail se sostuvo" de "el ganador ya está confirmado del todo"; y anticipar por qué la pregunta sobre checkoutConversion sigue sin contestar, aunque p95Latency ya esté resuelto.

La lección 7 cierra esa pregunta pendiente: ahora que la latencia dejó de ser un riesgo, ¿el lift de conversión se sostiene semana a semana, o era, en parte, efecto novedad — el entusiasmo inicial de los usuarios frente a algo nuevo, que se desvanece con el tiempo?

Recursos

  • Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. La misma referencia del módulo 3, relevante aquí porque confirma por qué correr la misma batería de canary/rollout después de un fix, en vez de una nueva, es la práctica recomendada para validar una corrección con la misma rigurosidad que detectó el problema. En inglés.
  • Charity Majors, "Deploys Are the WRONG Way to Change User Experience" — honeycomb.io/blog/deploys-wrong-way-change-user-experience. El mismo artículo citado en el módulo 1, relevante aquí porque su argumento central —desacoplar deploy de release— es lo que permite relanzar un feature ya arreglado sin un nuevo despliegue de infraestructura, solo cambiando el estado del flag. En inglés.
  • LaunchDarkly, "What Is Progressive Delivery All About?" — launchdarkly.com/blog/what-is-progressive-delivery-all-about. Contexto de industria sobre por qué las mismas rampas graduales que protegen un primer lanzamiento son la herramienta correcta para validar una iteración posterior sobre el mismo cambio. En inglés.