Módulo 3: El patrón strangler fig

Fallback y observar las dos rutas

Descripción

Ya sabes desviar tráfico (lección 4) y con qué criterio elegir la ruta (lección 5). Queda la pregunta que decide si la migración avanza con seguridad o a ciegas: ¿cuándo es seguro subir el porcentaje? Subirlo demasiado pronto —cuando el modern aún falla— expone a más usuarios a los problemas; subirlo demasiado tarde alarga la migración sin razón. La respuesta no puede ser una corazonada ("se ve bien, subámoslo"): tiene que ser un criterio con números. Esta lección construye ese criterio a partir de dos piezas: el fallback a fondo y la observabilidad de las dos rutas.

Ya viste el fallback en la lección 4: cuando el modern lanza un error, la request cae al legacy y el usuario no se entera. Aquí lo miramos con más detalle —qué cuesta en latencia, qué mide realmente— y le sumamos su compañero indispensable: observar las dos rutas por separado. El facade, que es por donde pasa todo, cuenta y cronometra cada ruta: cuántas requests atendió el modern con éxito, cuántas cayeron en fallback, cuántas fue el legacy directo, y cuánto tardó cada camino. Con esos números construyes el error_rate de la ruta nueva —el porcentaje de requests enrutadas al modern que terminaron en fallback— que es la señal clave de salud.

Y con el error_rate viene la decisión automatizable: un gate de promoción que dice "sube el porcentaje solo si el error_rate de la ruta nueva está por debajo de un umbral". La lección lo ejecuta con dos versiones del modern: la v1 con el bug de webcam (error_rate 9%, el gate no promueve) y la v2 arreglada (error_rate 0%, el gate sí promueve). El gate convierte "¿subimos?" de una discusión de pasillo a una comparación con un número.

Conexión con el módulo. La lección 4 introdujo el fallback como red; la 5 dio las estrategias de desvío. Esta responde el "cuándo subir" con observabilidad y un gate —independiente de qué estrategia elegiste—. La lección 7 usa este mismo instrumental para el corte final: subir hasta 100 con el gate y retirar el legacy cuando el burn-down llega a cero. Fíjate en la frontera: aquí montamos métricas de las dos rutas para decidir subir el porcentaje dentro del strangler. La disciplina más amplia de medir el progreso de una migración completa —burn-down de llamadas al legacy, fitness functions que impiden que el legacy vuelva a crecer, evitar la migración eterna— es el módulo 7. Aquí, la observabilidad puntual que alimenta la decisión de canary.

Una analogía: el tablero del piloto que decide subir de altitud

Un piloto no sube el avión a la altitud de crucero de un jalón apenas despega. Sube por niveles, y en cada nivel mira el tablero antes de subir al siguiente: presión de la cabina, temperatura de los motores, consumo de combustible, vibración. Si todos los instrumentos están en verde, sube. Si uno está en rojo —un motor recalienta, la presión baja—, se queda en el nivel actual o desciende, y no sube hasta que el instrumento vuelva al verde.

El piloto no decide por cómo "se siente" el vuelo. Decide por lo que dicen los instrumentos. Y tiene reglas duras: "no subir si la temperatura del motor pasa de X". Esa regla es un gate: una condición numérica que debe cumplirse para avanzar. Sin instrumentos, volar a ciegas es una apuesta; con ellos, cada decisión de subir está respaldada por una lectura.

Subir el traffic_percent es subir de altitud. Los instrumentos son la observabilidad de las dos rutas: el error_rate del modern, la latencia, el conteo de fallbacks. Y el gate de promoción es la regla dura del piloto: "no subir el porcentaje si el error_rate de la ruta nueva pasa del umbral". El modern con bug es el motor en rojo: el gate no deja subir hasta que lo arreglas y el instrumento vuelve al verde.

Ejemplo trabajado: observar las dos rutas y el gate de promoción

Vamos a instrumentar el router para que observe las dos rutas por separado, calcule el error_rate del modern y la latencia promedio, y decida con un gate si promover. El modern tiene dos versiones: v1 falla en webcam; v2 lo arregla. Corremos un canary de 10% con cada versión y dejamos que el gate decida. Nota el detalle de latencia: el legacy tarda 35 ms, el modern 12 ms (es más rápido), pero un fallback paga los dos —intenta el modern (12 ms), falla, y luego hace el legacy (35 ms)—, así que el fallback es el camino más lento.

import zlib

LEGACY_MS = 35   # el viejo es mas lento pero solido
MODERN_MS = 12   # el nuevo es mas rapido...

def legacy_catalog(request):
    return {"source": "legacy", "product": request["q"]}

# modern v1 falla en "webcam"; modern v2 ya lo arreglo.
def modern_catalog(request, version):
    if version == 1 and request["q"] == "webcam":
        raise ValueError("modern v1: unhandled out-of-stock path")
    return {"source": "modern", "product": request["q"]}

def bucket(request_id):
    return zlib.crc32(str(request_id).encode()) % 100

def run_canary(requests, traffic_percent, modern_version):
    # Observabilidad: contamos y cronometramos LAS DOS rutas por separado.
    obs = {
        "modern_ok": 0, "fallback": 0, "legacy": 0,
        "modern_ms": 0, "legacy_ms": 0,
    }
    for req in requests:
        if bucket(req["id"]) < traffic_percent:
            try:
                modern_catalog(req, modern_version)
                obs["modern_ok"] += 1
                obs["modern_ms"] += MODERN_MS
            except Exception:
                # El nuevo fallo: intento perdido (12ms) + red del legacy (35ms).
                obs["fallback"] += 1
                obs["modern_ms"] += MODERN_MS
                obs["legacy_ms"] += LEGACY_MS
                legacy_catalog(req)
        else:
            obs["legacy"] += 1
            obs["legacy_ms"] += LEGACY_MS
    return obs

def report(label, obs):
    attempted = obs["modern_ok"] + obs["fallback"]
    err_rate = (obs["fallback"] / attempted * 100) if attempted else 0.0
    total = attempted + obs["legacy"]
    avg_ms = (obs["modern_ms"] + obs["legacy_ms"]) / total
    print(f"{label}")
    print(f"  ruta modern : {obs['modern_ok']:>4} ok, {obs['fallback']:>3} fallbacks"
          f"  -> error_rate {err_rate:5.1f}%")
    print(f"  ruta legacy : {obs['legacy']:>4} directas + {obs['fallback']:>3} por fallback")
    print(f"  latencia avg: {avg_ms:5.1f} ms   (fallback paga {MODERN_MS}+{LEGACY_MS}ms)")
    gate = err_rate <= 1.0
    print(f"  gate de promocion (error_rate <= 1.0%): "
          f"{'PROMOVER' if gate else 'NO PROMOVER - se queda o rollback'}")
    return gate

N = 1000
requests = [{"id": i, "q": "webcam" if i % 10 == 0 else "ssd"} for i in range(1, N + 1)]

print("Observabilidad de las dos rutas y decision de subir el porcentaje\n")
report("Canary 10% con modern v1 (con bug):", run_canary(requests, 10, modern_version=1))
print()
report("Canary 10% con modern v2 (arreglado):", run_canary(requests, 10, modern_version=2))
print("\n  El gate no mira solo 'esta arriba': mira el error_rate de la ruta nueva.")
print("  v1 no pasa (falla en webcam); arreglas -> v2 pasa -> subes a 50%.")

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

Observabilidad de las dos rutas y decision de subir el porcentaje

Canary 10% con modern v1 (con bug):
  ruta modern :   99 ok,  10 fallbacks  -> error_rate   9.2%
  ruta legacy :  891 directas +  10 por fallback
  latencia avg:  32.8 ms   (fallback paga 12+35ms)
  gate de promocion (error_rate <= 1.0%): NO PROMOVER - se queda o rollback

Canary 10% con modern v2 (arreglado):
  ruta modern :  109 ok,   0 fallbacks  -> error_rate   0.0%
  ruta legacy :  891 directas +   0 por fallback
  latencia avg:  32.5 ms   (fallback paga 12+35ms)
  gate de promocion (error_rate <= 1.0%): PROMOVER

  El gate no mira solo 'esta arriba': mira el error_rate de la ruta nueva.
  v1 no pasa (falla en webcam); arreglas -> v2 pasa -> subes a 50%.

Compara los dos bloques, porque la diferencia entre ellos es toda la lección.

Con modern v1 (con bug), la ruta modern atendió 99 requests con éxito y tuvo 10 fallbacks. El error_rate —fallbacks sobre requests enrutadas al modern, 10 / 109— es 9.2%. El gate compara ese 9.2% contra su umbral (1.0%) y decide: NO PROMOVER. No importa que el modern haya atendido 99 requests bien; el 9.2% que falló está muy por encima del umbral, así que el avión no sube: te quedas en 10% (o haces rollback a 0%) y arreglas el bug. Fíjate que el gate no mira si el modern "está arriba y respondiendo"; mira específicamente qué fracción de su tráfico fracasó.

Con modern v2 (arreglado), la misma ruta atendió las 109 requests enrutadas, cero fallbacks. El error_rate es 0.0%, por debajo del umbral, y el gate decide: PROMOVER. Ahora sí puedes subir a 50%. El único cambio entre los dos bloques fue arreglar el caso webcam; el error_rate lo reflejó de inmediato, y el gate convirtió esa mejora en un permiso automático de avanzar.

Mira también la latencia. En los dos casos ronda los 32-33 ms, dominada por el legacy (que aún atiende el 89% del tráfico a 35 ms cada uno). El comentario del código señala el costo escondido: cada fallback paga 12 + 35 = 47 ms —más que ir directo al legacy—, porque intenta el modern, falla, y luego hace el legacy. Con v1 hay 10 fallbacks caros; con v2, cero. Es un costo pequeño aquí, pero a porcentajes altos con un modern que falla mucho, la latencia del fallback se nota: es otra razón para no subir el porcentaje con un modern que aún falla.

Profundización: qué observar, y el gate como contrato

La observabilidad de un strangler no es "logs por si acaso": son unas pocas métricas específicas que responden preguntas concretas de la migración.

Metrica              Pregunta que responde              Decision que alimenta
───────────────────  ─────────────────────────────────  ──────────────────────
error_rate (modern)  el nuevo, ¿funciona bien?           subir / no subir el %
fallback count       el nuevo, ¿tiene huecos?            ¿esta completo?
legacy calls         ¿cuanto trafico aun toca el viejo?  burn-down (leccion 7)
latencia por ruta    el nuevo, ¿es mas rapido/lento?     ¿vale la pena migrar?

El error_rate es la métrica de salud: fallbacks sobre requests enrutadas al modern. Un error_rate alto dice "el modern falla mucho, no subas". El conteo de fallbacks en términos absolutos, a 100%, dice si el modern está completo (fallback 0 = ningún hueco). Las llamadas al legacy miden cuánto tráfico aún depende del viejo —el burn-down de la lección 7—. Y la latencia por ruta valida que la migración vale la pena: si el modern fuera más lento que el legacy, quizás no querrías migrar (o querrías arreglar la latencia antes de seguir).

El gate de promoción es un contrato entre la migración y la seguridad: "solo avanzo si estas condiciones numéricas se cumplen". En el ejemplo el gate es una sola condición (error_rate <= 1.0%), pero en la práctica combina varias:

promover si:
    error_rate(modern) <= 1.0%           # el nuevo no falla mas de lo tolerable
    y latencia(modern) <= latencia(legacy) * 1.5   # no es dramaticamente mas lento
    y observado durante >= 1 hora        # no fue un pico momentaneo

Lo valioso del gate es que quita la emoción de la decisión. Sin gate, subir el porcentaje es una discusión ("yo lo veo bien" / "yo esperaría más") que suele ganar quien tiene más prisa o más rango. Con gate, la decisión la toma el número: si el error_rate está bajo el umbral durante el tiempo requerido, se sube; si no, no. Y funciona en las dos direcciones: el mismo gate que autoriza subir puede disparar un rollback automático —bajar el traffic_percent a 0— si el error_rate se dispara después de subir. Ese rollback es la máxima expresión del fallback: no solo cada request tiene su red (el fallback), sino que el nivel de tráfico entero puede retroceder si la ruta nueva se degrada. Reversibilidad en cada paso.

Errores comunes

Subir el porcentaje por corazonada, sin métricas. Qué pasa: el equipo sube el traffic_percent porque "lleva unos días y no ha habido quejas". Por qué pasa: sin observabilidad de las dos rutas, "no ha habido quejas" es lo único que hay, y se confunde con "está sano". Cómo detectarlo: no hay un error_rate ni un conteo de fallbacks en el tablero; la decisión de subir se toma en una reunión, no a partir de un número. Cómo corregirlo: instrumenta las dos rutas antes de subir el primer escalón, y define un gate explícito (error_rate bajo X, durante Y tiempo). "No ha habido quejas" es peligroso porque el fallback esconde los fallos del modern —los usuarios no se quejan justamente porque el legacy los atendió—; sin mirar el error_rate, no ves que el modern está fallando bajo la superficie. La ausencia de quejas no es salud; el error_rate bajo, sí.

Interpretar "el modern responde" como "el modern está sano". Qué pasa: el monitoreo dice que el servicio modern está arriba (responde al health check) y el equipo concluye que puede subir el porcentaje. Por qué pasa: un health check de "¿estás vivo?" es fácil y común, y se confunde con "¿estás funcionando bien?". Cómo detectarlo: el modern está "verde" en el monitoreo de disponibilidad pero su error_rate de negocio (fallbacks) es alto. Cómo corregirlo: la métrica que importa para promover no es "el proceso responde", es "qué fracción de las requests reales atendió con éxito". Un modern que está arriba pero falla en el 9% de las requests (como la v1) no está sano para subir. Mide el error_rate sobre el tráfico real que enrutas, no la disponibilidad del proceso. El avión puede tener los motores encendidos y aun así uno recalentando.

Ignorar la latencia del fallback a porcentajes altos. Qué pasa: el equipo mira solo el error_rate y no la latencia, y sube el porcentaje con un modern que falla en cierto caso. Por qué pasa: mientras el fallback atienda las requests, "funcionan", así que la latencia extra pasa desapercibida. Cómo detectarlo: la latencia promedio sube a medida que subes el porcentaje, porque cada fallback paga el intento del modern más el legacy. Cómo corregirlo: recuerda que un fallback es el camino más lento (modern + legacy), no el más rápido. Si el modern falla en un caso frecuente, subir el porcentaje aumenta la fracción de requests que pagan la doble latencia. El error_rate y la latencia se leen juntos: un error_rate del 9% no solo significa "9% de fallos ocultos", también significa "9% de requests que tardan el doble". Arreglar el modern baja las dos métricas a la vez.

Ejercicios

Ejercicio 1 — Calcula el error_rate y decide. Un canary al 20% observa: modern_ok=180, fallback=20, legacy=800. El gate promueve si el error_rate del modern es ≤ 1.0%. (a) ¿Cuántas requests se enrutaron al modern? (b) ¿Cuál es el error_rate? (c) ¿El gate promueve? Justifica.

Ver solución

(a) Se enrutaron al modern 180 + 20 = 200 requests: las 180 que tuvo éxito más las 20 que fallaron y cayeron en fallback. Eso es el 20% de las 1000, consistente con traffic_percent=20.

(b) El error_rate es fallbacks sobre requests enrutadas al modern: 20 / 200 = 10.0%.

(c) El gate NO promueve. El error_rate (10.0%) está muy por encima del umbral (1.0%). Aunque el modern atendió 180 requests con éxito, 1 de cada 10 de las que le tocaron falló y cayó al legacy —un modern con un hueco grande—. Subir el porcentaje expondría a más usuarios a la doble latencia del fallback y acercaría el sistema al límite de lo que el fallback puede tapar. La decisión correcta es quedarse en 20% (o rollback a 0%), encontrar qué caso está fallando el modern, arreglarlo, y volver a medir. El gate solo abre cuando el error_rate cae bajo el umbral.

Ejercicio 2 — El fallback esconde y revela. El texto dice que "no ha habido quejas" es peligroso como criterio. (a) Con el modern v1 (error_rate 9.2%), ¿por qué los usuarios no se quejaron a pesar de que el modern falló 100 veces? (b) ¿Qué habría pasado sin fallback? (c) ¿Qué métrica te avisa del problema que las quejas no te avisan?

Ver solución

(a) Porque el fallback atendió esos 100 fallos con el legacy. Cada vez que el modern falló en webcam, el try/except mandó la request al legacy, que la resolvió correctamente. Desde la perspectiva del usuario, no hubo error: recibió su respuesta (del legacy, un poco más lenta, pero correcta). El fallback hizo su trabajo de red de seguridad, y por eso nadie se quejó. La ausencia de quejas no significa que el modern esté sano —significa que el fallback está funcionando—.

(b) Sin fallback, esos 100 fallos del modern habrían sido errores 500 visibles para los usuarios cuyas requests el modern no supo atender. Ahí sí habría quejas —pero al costo de exponer el bug a usuarios reales—. El fallback convierte fallos visibles en fallos invisibles; buena noticia para los usuarios, pero implica que no puedes confiar en las quejas para detectar problemas.

(c) El error_rate (o el conteo de fallbacks) te avisa de lo que las quejas esconden. El error_rate de 9.2% dice, con un número, que el modern está fallando en casi 1 de cada 10 requests que le tocan —aunque ningún usuario lo haya sentido—. Por eso el gate se basa en el error_rate y no en las quejas: la métrica ve lo que el fallback oculta a los usuarios. Observar la ruta nueva por dentro es la única forma de saber que está sana cuando el fallback esconde sus fallos.

Ejercicio 3 — Diseña el gate. Vas a definir el gate de promoción para el canary del catalog de Mercado. (a) Escribe, en prosa o pseudocódigo, tres condiciones que el gate debería exigir antes de subir el porcentaje. (b) ¿Por qué incluir una condición de tiempo ("observado durante al menos N minutos") y no solo de error_rate? (c) ¿Cómo usarías el mismo gate para un rollback automático?

Ver solución

(a) Tres condiciones razonables:

promover si:
    error_rate(modern) <= 1.0%                    # el nuevo casi no falla
    y p95_latencia(modern) <= p95_latencia(legacy) * 1.5   # no mucho mas lento
    y observado durante >= 30 minutos con trafico real     # no fue un pico

La primera protege la correctitud (el modern atiende bien); la segunda, el rendimiento (el modern no degrada la experiencia); la tercera, la estadística (los números son de una muestra suficiente, no de un instante afortunado).

(b) Porque un error_rate bajo en un instante puede ser suerte: quizás mediste justo en un minuto tranquilo, o antes de que llegara el tráfico del caso que el modern no maneja. La condición de tiempo exige que el modern se mantenga sano durante un periodo con tráfico real, cubriendo la variedad de requests que llegan a lo largo de media hora (incluidos los picos y los casos raros). Un buen momento no basta; hace falta un buen rato. Sin la condición de tiempo, promoverías con base en una foto en vez de una película.

(c) El mismo gate, invertido, dispara el rollback: si después de subir el porcentaje el error_rate supera el umbral (o la latencia se dispara) durante un periodo corto, el sistema baja automáticamente el traffic_percent —a un escalón anterior o directo a 0—. Es la reversibilidad del strangler llevada al nivel de tráfico: no solo cada request tiene su fallback, sino que el nivel de canary entero retrocede si la ruta nueva se degrada. El gate se vuelve un termostato de dos sentidos: sube cuando hay salud sostenida, baja cuando hay degradación sostenida.

Resumen y siguiente paso

En esta lección construiste el criterio para saber cuándo subir el porcentaje: la observabilidad de las dos rutas y el gate de promoción. Viste, con el piloto que sube de altitud mirando el tablero, que la decisión de avanzar no es una corazonada sino una lectura de instrumentos. Y lo ejecutaste: instrumentaste el router para contar y cronometrar las dos rutas, calculaste el error_rate del modern, y dejaste que un gate decidiera —con el modern v1 (error_rate 9.2%) el gate no promovió; tras arreglarlo, con v2 (error_rate 0%), el gate promovió—. Y viste el costo escondido del fallback: es el camino más lento (modern + legacy), otra razón para no subir el porcentaje con un modern que falla.

Antes de avanzar deberías poder: nombrar las métricas clave de un strangler y qué decisión alimenta cada una; calcular el error_rate del modern y aplicarlo contra un umbral; explicar por qué "no ha habido quejas" y "el modern responde" no son criterios de salud; y diseñar un gate con condiciones de error_rate, latencia y tiempo, que sirva también para rollback.

La lección 7 hace la fase que casi nadie completa: el corte final y retirar el legacy. Con el gate llevas el traffic_percent hasta 100, observas el burn-down de llamadas al legacy bajando hacia cero, y —cuando llega a cero— aplicas el gate de retiro: apagas y borras el legacy. Vas a ver por qué "retirar" significa borrar el código, no comentarlo ni dejarlo "por si acaso", y por qué la migración eterna —dos sistemas prendidos para siempre— es el fracaso silencioso que este último paso existe para evitar.

Recursos

  • Martin Fowler, "CanaryRelease" — martinfowler.com/bliki/CanaryRelease.html. La observación del canary y las métricas que deciden si expandir la nueva versión o hacer rollback. El marco conceptual del gate de esta lección. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 y cap. 9 ("As an Organization Grows") — el rol del monitoreo del proxy para decidir cuándo avanzar en el desvío y cómo detectar que el servicio nuevo se degrada. En inglés.
  • Chris Richardson, "Pattern: Strangler application" — microservices.io/patterns/refactoring/strangler-application.html. La necesidad de monitorear el servicio nuevo y mantener la vieja implementación como respaldo mientras se gana confianza. En inglés.
  • Pete Hodgson, "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. La sección sobre gestión de toggles y cómo se usan para hacer rollback rápido de una funcionalidad que se degrada, sin desplegar código nuevo. En inglés.