Módulo 3: Gradual Rollout

La rampa: canary 1% → 10% → 50% → 100%

Descripción

Esta lección dibuja la rampa completa que va a acompañar el resto del módulo y ejecuta, por primera vez, rolloutPlan() — el modelo central de todo el rollout gradual—. Dado un conjunto de etapas, cada una con un porcentaje y un criterio de avance, rolloutPlan() recorre la rampa en orden y decide, etapa por etapa, si sigue subiendo o se detiene. Lo vas a correr hoy sobre el caso real de recommendations, con el guardrail de latencia que ya conoces del módulo 1 — y vas a ver, con números, exactamente dónde se frena.

Conexión con el módulo. Esta lección construye la pieza que las lecciones 3 a 7 van a refinar y extender: la lección 3 se enfoca en la primera etapa (el canary) y su tamaño mínimo; la lección 4 formaliza qué significa, en concreto, el criterio de avance que rolloutPlan() usa aquí de forma todavía simple; la lección 5 aplica blastRadius() del módulo 1 a las mismas cuatro etapas; la lección 7 agrega el tiempo mínimo de espera antes de confiar en cada medición. Todas retoman la misma rampa que defines hoy.

Una analogía: el canario en la mina

Antes de que existieran los sensores electrónicos de gas, los mineros bajaban a las minas de carbón con una jaula y un canario adentro. El canario es mucho más sensible que un humano a gases como el monóxido de carbono: si el aire empezaba a envenenarse, el canario dejaba de cantar y caía antes de que un minero notara nada. Ver al canario caído era la señal para salir de inmediato — no después de terminar el turno, no "para ver si mejora": de inmediato. El canario no resolvía el problema del gas. Avisaba a tiempo, con el menor costo posible, antes de que el problema le llegara a alguien que de verdad importaba.

El canary release — la primera etapa de la rampa, el 1% — cumple exactamente ese papel. No arregla el guardrail de latencia de recommendations. Lo que hace es exponer a una porción tan chica de usuarios que, si el problema aparece, el costo de haberlo descubierto ahí es mínimo — y la señal llega mucho antes que si hubieras esperado a ver qué pasaba con el 100% de la base. El resto de la rampa —10%, 50%, 100%— son etapas donde, si el canario ya "cantó bien", subes la apuesta con más confianza en cada paso.

Ejemplo trabajado: rolloutPlan() sobre el rollout de recommendations

La rampa completa tiene cuatro etapas. En cada una, medimos el guardrail de latencia (p95Latency, techo 800ms, ya definido en la guía de métricas) y decidimos si se avanza:

// rolloutPlan: define la rampa de canary -> 10% -> 50% -> 100% y, dado el guardrail
// MEDIDO en cada etapa, decide avanzar o frenar. Se detiene en la primera etapa
// que no cumple su criterio -- las etapas siguientes ni siquiera se evaluan.
// Caso pedagogico: rollout de recommendations en Mercado, con el guardrail de
// latencia (p95, techo 800ms) conocido desde la guia de metricas.
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; // techo de p95Latency en ms, definido en la guia de metricas

const stages = [
  { percent: 0.01, label: 'canary 1%', measured: { p95Latency: 720 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.10, label: 'rollout 10%', measured: { p95Latency: 910 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.50, label: 'rollout 50%', measured: { p95Latency: null }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 1.00, label: 'rollout 100%', measured: { p95Latency: null }, advanceIf: (m) => m.p95Latency <= ceiling },
];

console.log('=== rolloutPlan: la rampa de recommendations en Mercado (techo p95=' + ceiling + 'ms) ===\n');
const plan = rolloutPlan(stages);
plan.forEach((s) => {
  const measuredLabel = s.measured.p95Latency === null ? 'n/a' : s.measured.p95Latency + 'ms';
  console.log(s.label.padEnd(14) + 'p95Latency=' + String(measuredLabel).padStart(6) + '  -> ' + s.decision);
});

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

=== rolloutPlan: la rampa de recommendations en Mercado (techo p95=800ms) ===

canary 1%     p95Latency= 720ms  -> ADVANCE
rollout 10%   p95Latency= 910ms  -> HOLD
rollout 50%   p95Latency=   n/a  -> NOT_REACHED
rollout 100%  p95Latency=   n/a  -> NOT_REACHED

⚠️ Los valores de p95Latency por etapa (720ms en el canary, 910ms en el 10%) son un caso pedagógico: modelan que un canary de baja escala no siempre reproduce con la misma magnitud una regresión que sí aparece con más tráfico concurrente. El número 910ms reutiliza, a propósito, el mismo valor que la guía de métricas midió en el experimento original — la misma regresión, ahora vista en una etapa más grande de la rampa.

Fíjate en las últimas dos filas: rollout 50% y rollout 100% aparecen como NOT_REACHED, no como HOLD ni como ningún otro veredicto sobre su propio desempeño. rolloutPlan() nunca llegó a evaluarlas — se detuvo en rollout 10% y ahí se quedó. Esa es la propiedad más importante del modelo: la rampa no es una lista de cuatro chequeos independientes que corren todos a la vez; es una secuencia, donde cada etapa solo se evalúa si la anterior avanzó. El canary al 1% pasó limpio (720ms, bajo el techo). El 10% no — y ahí termina, por ahora, el rollout de recommendations.

Por qué la rampa se detiene, y no "sigue viendo qué pasa"

Nota algo deliberado en el diseño de rolloutPlan(): cuando una etapa da HOLD, la función no intenta la siguiente etapa "a ver si mejora". Se detiene ahí. Esto no es un detalle técnico menor — es la diferencia entre una rampa real y una lista de casillas que se marcan sin consecuencia. Si rolloutPlan() siguiera evaluando rollout 50% y rollout 100% de todos modos, el resultado de esas filas no significaría nada: nunca se llegó a exponer a esa gente, porque la decisión correcta, al ver 910ms en la etapa anterior, es frenar antes de seguir subiendo.

Esto también explica por qué la rampa tiene cuatro etapas y no dos (1% y 100%, directo). Cada etapa intermedia es una oportunidad de detectar el problema con menos gente expuesta que la siguiente. Si la rampa fuera solo 1% → 100%, y el canary al 1% hubiera pasado limpio (como de hecho pasó, en 720ms), el siguiente paso habría sido exponer a toda la base — sin el paso intermedio del 10%, que es exactamente donde este rollout reveló el problema real. Más etapas no es burocracia: es más oportunidades de que el "canario" avise antes de que el problema le llegue a todo el mundo.

Errores comunes

Saltar de canary a 100% sin etapas intermedias, "porque el canary ya pasó". Qué pasa: al ver que el canary al 1% dio 720ms —limpio, bajo el techo— alguien propone ir directo al 100%, saltándose el 10% y el 50%. Por qué pasa: "el canary pasó" se siente como suficiente evidencia, y cada etapa intermedia parece una demora innecesaria si la primera ya salió bien. Cómo detectarlo: la propuesta sobre la mesa es "canary limpio, prendámoslo para todos" sin mencionar ninguna etapa entre el 1% y el 100%. Cómo corregirlo: como muestra el ejemplo de hoy, el problema de recommendations no apareció en el canary — apareció recién en el 10%, con más tráfico concurrente. Sin esa etapa intermedia, el rollout habría llegado directo al 100% con el guardrail roto y sin ninguna advertencia previa.

Tratar las etapas NOT_REACHED como si fueran etapas ya "validadas" o "pendientes de aprobación" en algún sentido positivo. Qué pasa: alguien lee la tabla de salida de rolloutPlan() y asume que rollout 50% y rollout 100% están "en cola", listas para avanzar en cuanto se resuelva el problema de la etapa anterior. Por qué pasa: NOT_REACHED se parece visualmente a un estado neutral, y es fácil leerlo como "todavía no le tocó su turno" en vez de "el rollout, tal como está, nunca llegó ahí". Cómo detectarlo: se habla de esas etapas como si tuvieran datos propios ("a ver qué tal se porta el 50%"), cuando measured.p95Latency en esas filas es literalmente null. Cómo corregirlo: NOT_REACHED significa cero información sobre esa etapa — ni buena ni mala. No hay nada que "avanzar" ahí hasta que la etapa anterior deje de estar en HOLD.

Diseñar una rampa con una sola etapa intermedia entre el canary y el 100%, pensando que "menos pasos es más simple". Qué pasa: alguien propone 1% → 100% directo, o 1% → 50% → 100%, argumentando que menos etapas significa un proceso más rápido y más fácil de coordinar. Por qué pasa: cada etapa intermedia implica coordinación —alguien tiene que revisar los números y decidir— y menos etapas parece, a primera vista, menos trabajo. Cómo detectarlo: al comparar la rampa propuesta con la de cuatro etapas de este módulo, el salto entre etapas consecutivas es mucho más grande (por ejemplo, de 1% a 50% es un salto de 49 puntos porcentuales, contra 1% a 10%, un salto de 9). Cómo corregirlo: la lección 5 de este módulo pone número exacto a por qué los saltos grandes son peligrosos —cuánta gente nueva queda expuesta en cada transición—. Por ahora, basta con notar que cada etapa que quitas de la rampa es una oportunidad menos de que el canario avise antes de tiempo.

Ejercicios

Ejercicio 1 — Cambia el resultado de una etapa y vuelve a trazar la rampa. Supón que, en vez de 910ms, la etapa rollout 10% hubiera medido 780ms (bajo el techo de 800ms). Sin correr el código, dibuja la tabla completa de rolloutPlan() con este nuevo dato — ¿qué etapas avanzan, cuáles se detienen, y cuáles quedan como NOT_REACHED?

Ver solución

Con p95Latency: 780 en rollout 10% (780 <= 800, cumple el criterio), la fila pasa a ADVANCE. Como ninguna etapa anterior se detuvo, rolloutPlan() sigue evaluando: rollout 50% y rollout 100% siguen teniendo measured.p95Latency: null en este ejemplo, así que con los datos tal como están en el código, ambas seguirían mostrando el resultado de s.advanceIf(s.measured) evaluado sobre null — y null <= 800 es true en JavaScript, así que, tal como está escrito el ejemplo, avanzarían igual. Esto revela algo importante que no es un error del modelo sino una limitación de los datos de este ejercicio: rolloutPlan() solo es tan confiable como los datos measured que le das. En un rollout real, cada etapa necesita su propia medición real antes de evaluarse — nunca null — exactamente el tema de la lección 4 (criterios de avance) y la lección 7 (esperar a tener datos suficientes antes de mirar).

Ejercicio 2 — Diseña una rampa con más etapas. El equipo de Mercado, después de ver que el problema apareció justo en el salto de 1% a 10%, propone agregar una etapa intermedia: 1% → 5% → 10% → 50% → 100%. ¿Qué gana el equipo con esta etapa adicional? ¿Qué pierde?

Ver solución

Gana: una oportunidad más de detectar el problema con menos gente expuesta — si la regresión de latencia empieza a manifestarse gradualmente con más tráfico concurrente (como sugiere que apareciera en 10% y no en 1%), es posible que ya sea visible en 5%, antes de llegar a 10%. Eso reduce, otra vez, el radio de impacto del descubrimiento. Pierde: tiempo total del rollout — cada etapa nueva significa una revisión más, y probablemente un tiempo mínimo de espera más (dwell time, lección 7) antes de poder avanzar. La decisión de cuántas etapas usar es, en el fondo, un balance entre velocidad de lanzamiento y qué tan fina quieres que sea la detección temprana — no existe un número "correcto" universal, depende de cuánto le cueste al negocio cada semana adicional de rollout contra cuánto le costaría un incidente no detectado a tiempo.

Ejercicio 3 — Explica el modelo con tus propias palabras. Un compañero de equipo, sin haber leído esta lección, pregunta: "¿por qué rolloutPlan() no evalúa las cuatro etapas de una sola vez y me dice el resultado de todas?" Escribe la respuesta que le darías, en 2-3 frases.

Ver solución

Una respuesta posible: "Porque las etapas no son independientes — son secuenciales. No tiene sentido preguntar '¿el 50% de rollout está bien?' si nunca expusiste al 50% de la gente, porque te detuviste antes en el 10%. rolloutPlan() evalúa una etapa a la vez, en orden, y en cuanto una da HOLD, deja de evaluar las siguientes — porque, en la realidad, esas etapas siguientes ni siquiera llegaron a ejecutarse. El resultado NOT_REACHED es exactamente eso: no hay datos que evaluar, porque el rollout nunca llegó ahí."

Resumen y siguiente paso

En esta lección ejecutaste rolloutPlan(), el modelo central del módulo, sobre las cuatro etapas de la rampa de recommendations: el canary al 1% avanzó limpio (720ms), pero el 10% rompió el techo de latencia (910ms) y ahí se detuvo la rampa — el 50% y el 100% quedaron como NOT_REACHED, sin ningún dato que los respalde. Viste por qué la rampa evalúa etapa por etapa, en secuencia, y por qué detenerse a tiempo en una etapa intermedia es exactamente el punto de tener más de dos escalones entre el canary y el 100%.

Antes de avanzar deberías poder: explicar con tus propias palabras la diferencia entre HOLD y NOT_REACHED; y ejecutar rolloutPlan() a mano dado un conjunto de etapas y sus mediciones.

La lección 3 se detiene en la primera etapa de la rampa — el canary — y responde una pregunta que esta lección dejó abierta: ¿por qué empezar con un porcentaje tan chico, y qué pasa cuando ese porcentaje es demasiado chico para que el guardrail dé alguna señal confiable?

Recursos

  • Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. Describe el patrón de un canary con múltiples etapas, cada una con una población más grande que la anterior — la base formal de la rampa de cuatro etapas de esta lección. En inglés.
  • Martin Fowler (bliki), "CanaryRelease" — martinfowler.com/bliki/CanaryRelease.html. La definición de referencia de canary release como técnica para reducir el riesgo, exponiendo primero a un subconjunto chico antes de exponer a toda la infraestructura. En inglés.