Módulo 7: Degradación elegante y load shedding

1. Presentación del módulo: fallar suave, no duro

Descripción

Los cinco módulos anteriores te enseñaron a protegerte de una dependencia que falla. Le pusiste un timeout a shipping para que un cuelgue no te agotara los hilos; backoff y jitter al reintento de payments para no matarlo con una avalancha; una idempotency_key para que ese reintento no cobrara dos veces; un circuit breaker para dejar de golpear a un servicio muerto; un bulkhead para que la falla de una dependencia no contaminara a las demás. Cada patrón hizo bien su trabajo. Pero todos dejaron abierta la misma pregunta, y hoy la vamos a responder: cuando la protección funcionó y aun así shipping no responde, ¿qué le contestas al comprador que quería comprar?

Piénsalo con el circuit breaker del módulo 5. El breaker detecta que shipping está muerto y rechaza la llamada en ~0 ms, sin quemar hilos. Perfecto para orders. Pero desde el punto de vista del comprador, ese "rechazo en ~0 ms" no es una respuesta: él no quería saber que shipping está caído, quería comprar. El breaker te protegió a ti; no lo atendió a él. Lo mismo con el bulkhead: aisló la falla de shipping en su compartimento, pero el request del comprador cayó justo en ese compartimento. La protección evitó que la casa se incendiara; ahora hay que decidir qué hacer con la persona que estaba en el cuarto donde saltó el interruptor. Ese "qué hacer" es todo este módulo, y tiene un nombre: degradación elegante.

La idea es tan simple que cuesta creer lo seguido que se ignora: cuando una parte no-esencial falla, entrega una versión reducida del servicio en lugar de un error total. El checkout de Mercado no necesita crear el envío en el mismo instante en que cobra —puede crearlo cinco segundos o cinco minutos después, cuando shipping revive, sin que el comprador lo note—. Así que si shipping está caído, la respuesta correcta no es "error, no se pudo procesar tu compra": es cobrar, confirmar el pedido, y diferir el envío. El comprador se va feliz con su compra; el envío se crea solo, después. Eso es fallar suave. La alternativa —tirar la venta completa porque el servicio que imprime la etiqueta está caído— es fallar duro, y es la respuesta que casi todo el mundo elige sin pensarla, porque "todo o nada" es lo que sale por default cuando no diseñas la degradación a propósito.

La segunda mitad del módulo cambia de enemigo. Hasta aquí la falla venía de afuera (una dependencia caída). Pero a veces el que está en problemas eres : llega tres veces más tráfico del que puedes atender —una promoción, un pico, un bot que se desbocó—. No hay dependencia que degradar; hay que decidir a quién atiendes y a quién no, y hacerlo antes de ahogarte. La respuesta es el load shedding: bajo sobrecarga, rechazar temprano una parte del tráfico —el de baja prioridad primero— para que el núcleo (el cobro) siga sano. Suena brutal, y es exactamente lo contrario: un servidor que intenta atenderlos a todos bajo 3x de sobrecarga no atiende al triple de gente, atiende a casi nadie, porque se ahoga haciendo trabajo que ya nadie espera. Rechazar a tiempo a los que no caben salva a los que sí. Fallar suave con la carga, no duro.

Conexión con el módulo: esta es la lección-mapa. No construimos nada completo todavía; hoy fijamos la intuición (por qué "todo o nada" es casi siempre la respuesta equivocada), las dos analogías (el modo cojera del auto para la degradación, el triage de urgencias para el load shedding), el vocabulario (crítico vs no-crítico, fallback, degrade, shed_load, diferir/encolar, feature toggle) y los dos números faro que recorreremos. La lección 2 mide el corazón: falla dura 0% vs degradación 99.1% en el checkout con shipping caído. La 3 enseña a clasificar dependencias en críticas y no-críticas, la decisión que habilita toda degradación. La 4 es el valor de fallback (qué muestras cuando algo no-crítico cae). La 5 es diferir y encolar el trabajo no-crítico. La 6 son los feature toggles y el kill switch. La 7 pasa al load shedding y la prioridad. Y la 8 te pone a hacer que el checkout de Mercado degrade con tus manos, y a medir el antes y el después.

Las dos analogías: el modo cojera y el triage

Este módulo tiene dos ejes, y cada uno tiene su imagen. Guárdalas, porque desarman las dos objeciones más comunes.

Para la degradación: el modo cojera del auto. Tu carro moderno tiene un modo de emergencia que casi nunca ves. Cuando el motor detecta una falla seria —un sensor loco, un sobrecalentamiento—, no se apaga en medio de la autopista y te deja tirado. Entra en limp mode ("modo cojera"): corta la potencia, limita las revoluciones, apaga el aire acondicionado, y te deja llegar a casa o al taller a 40 km/h con la luz de "check engine" encendida. Es una versión reducida del auto —no puedes acelerar, no hay clima— pero es un auto que camina, y eso vale infinitamente más que un auto perfecto que no arranca. El diseñador del auto tomó una decisión deliberada: ante una falla parcial, prefiero darte un servicio degradado que ninguno. Eso es la degradación elegante. El checkout de Mercado con shipping caído entra en modo cojera: te cobra y confirma tu pedido (el motor camina), pero el envío se crea después (el aire acondicionado apagado). Reduce, no se apaga.

Para el load shedding: el triage de urgencias. Llega una ambulancia con diez heridos a una sala de urgencias que tiene capacidad para tres a la vez. La respuesta cómoda —atender a los diez "un poquito" a cada uno— es la que mata a todos: nadie recibe atención suficiente para salvarse, los médicos corren de camilla en camilla sin terminar nada, y el que tenía una hemorragia se desangra mientras le ponen una curita a otro. Por eso las urgencias hacen triage: una enfermera en la puerta clasifica en segundos —este es crítico, entra ya; este puede esperar; este que se raspó la rodilla, a la sala de espera—. Rechazar o posponer a los que no son urgentes no es crueldad: es lo que permite salvar a los que sí lo son. El load shedding es triage para el tráfico: bajo sobrecarga, una decisión rápida en la puerta —este request es del checkout, entra; este es de "productos recomendados", espera o se rechaza— mantiene vivo al núcleo en vez de dejar que todo se ahogue por igual.

Las dos imágenes comparten una moraleja incómoda: a veces la respuesta correcta es dar menos a propósito. Menos servicio (modo cojera) o menos tráfico atendido (triage). El instinto de ingeniero es "hazlo todo, siempre"; la resiliencia madura sabe cuándo reducir deliberadamente es lo que salva al conjunto.

Los dos ejes de un vistazo

Antes de medir nada, quédate con el mapa. El módulo tiene dos mitades que responden a dos preguntas distintas.

                         ¿QUIEN esta en problemas?
                                    |
        +---------------------------+---------------------------+
        |                                                       |
   UNA DEPENDENCIA cae                                    YO estoy sobrecargado
   (shipping muerto)                                      (llega 3x de trafico)
        |                                                       |
   DEGRADACION ELEGANTE                                    LOAD SHEDDING
   "entrega una version reducida"                          "rechaza temprano, protege el nucleo"
        |                                                       |
   +----+---------+----------+-----------+               +------+-------+
   |              |          |           |               |              |
 clasificar    valor de   diferir/    feature          rechazar a    por prioridad
 critico vs    fallback   encolar     toggle /          la capacidad  (nucleo primero)
 no-critico   (leccion 4) (leccion 5) kill switch      (leccion 7)   (leccion 7)
 (leccion 3)                          (leccion 6)

La degradación elegante (lecciones 3-6) responde a una falla externa: una dependencia no-crítica cae, y tú entregas una versión reducida —un valor de fallback, un envío diferido, una función apagada a mano— en vez de un error total. Su requisito previo es saber qué es prescindible (crítico vs no-crítico, lección 3).

El load shedding (lección 7) responde a un problema tuyo: te llega más tráfico del que aguantas, y para no ahogarte rechazas temprano una parte —la de baja prioridad primero— y así proteges al núcleo. Su requisito previo es saber qué tráfico es prescindible (prioridad).

Las dos mitades comparten el mismo ADN —sacrificar lo secundario para salvar lo esencial— y por eso van juntas en un módulo. La diferencia es qué sacrificas: en la degradación, una función (el envío inmediato); en el shedding, una fracción del tráfico (los requests de baja prioridad).

Los dos números faro

Para que las dos mitades no queden en abstracto, aquí están los números que vas a medir en detalle, uno por cada eje. La semilla es fija y la salida es la real de la simulación en Python.

Eje 1 — degradación (qué esperar). El checkout de Mercado (orderspaymentsshipping) con shipping completamente caído, 2000 checkouts. Salida real de la simulación (Python, semilla fija):

SIM 1  ·  MERCADO checkout: orders -> payments -> shipping
         shipping CAIDO (down)   ·   payments fail_rate=1%   ·   2000 checkouts

  (a) FALLA DURA (todo o nada: si shipping falla, cae el pedido)
      pedidos completados       : 0 / 2000
      success_rate del checkout : 0.0%
      cobros a revertir (huerfanos): 1981

  (b) DEGRADACION (shipping no-critico: se difiere el envio)
      pedidos completados       : 1981 / 2000
      success_rate del checkout : 99.1%
      envios diferidos (encolados) : 1981
      checkouts perdidos por payments (critico): 19

  >> falla dura : 0%  ·  degradacion : 99.1%

Léelo despacio. Con falla dura, shipping caído tira el checkout entero: 0% de éxito, cero ventas. Y peor —fíjate en la última línea de (a)— hay 1981 cobros huérfanos: en el modo duro, payments ya había cobrado cuando shipping falló, así que no solo perdiste la venta, sino que le cobraste al comprador algo que luego tienes que reembolsar. Fallar duro no es neutral; hace daño activo. Con degradación, en cambio, shipping deja de ser un veto: el pedido se completa, el envío se encola para después, y el checkout llega a 99.1%. El único 0.9% que se pierde son los checkouts donde falló payments —la dependencia crítica, la que sí es un veto legítimo (sin cobro no hay pedido)—. La degradación no es magia que salve todo; es la disciplina de dejar que solo lo verdaderamente esencial pueda tumbar la operación.

Eje 2 — load shedding (qué esperar). El mismo Mercado, ahora bajo un pico de 3x de tráfico (capacidad 120/s, llegan 300/s), 60 segundos de sobrecarga. Salida real de la simulación (semilla fija):

SIM 2  ·  MERCADO bajo 3x de sobrecarga  ·  load shedding
         carga normal=100/s  capacidad=120/s  pico=300/s (3x)  deadline=2s

  (a) SIN shedding (se admite todo -> congestion collapse)
      UTILES (goodput) : 300 / 18000   (1.7% de lo ofrecido)
      -> el servidor trabaja al 100% pero casi todo llega tarde

  (b) CON shedding (admission control a la capacidad)
      rechazadas (shed)     : 10800  (60.0%)
      admitidas (served)    : 7200   (40.0%)
      exito de las ADMITIDAS: 100.0%

Este es el número contraintuitivo del módulo. Sin shedding, el servidor acepta las 18000 requests del pico y hace lo "generoso": intentar atenderlas a todas. Resultado: se ahoga. La cola crece más rápido de lo que puede vaciarla, cada request espera tanto que el cliente ya se fue antes de que le toque turno, y el servidor termina gastando el 100% de su capacidad en producir respuestas que ya nadie recoge. Útil: 1.7%. Prácticamente cero. Es el colapso por congestión: trabajar al máximo y entregar casi nada. Con shedding, el servidor hace lo "egoísta": rechaza al instante el 60% que no cabe (10800 requests, con un "vuelve más tarde" barato) y se queda solo con las 7200 que sí puede atender. Resultado: esas 7200 pasan al 100%. Rechazar a tiempo a los que no caben es lo que salva a los que sí. La lección 7 lo mide, y le añade la prioridad: rechazar primero lo secundario (browsing, recomendaciones) para que el núcleo (el checkout) nunca sea el sacrificado.

Por qué esto no lo resuelve lo que ya sabes

Una reacción natural es "pero yo ya tengo breaker y bulkhead, ¿no basta?". Vale la pena cerrar esa puerta, porque marca la frontera exacta de por qué la degradación es una herramienta nueva.

El circuit breaker (M5) y el bulkhead (M6) son mecanismos de protección: deciden cómo tratar la llamada fallida —rechazarla rápido, aislarla en su pool—. Ninguno decide qué valor entregar en su lugar. El breaker convierte una espera de 2 segundos en un CircuitOpenError de ~0 ms; pero un CircuitOpenError que sube hasta el comprador sigue siendo una pantalla de error. La degradación toma ese error y lo convierte en una respuesta útil: "pedido confirmado, envío en camino". Son capas distintas —el breaker protege el recurso (los hilos de orders), la degradación protege la experiencia (la venta del comprador)— y se apilan: primero el breaker corta rápido, luego la degradación responde con gracia. De hecho, la degradación es lo que hace aceptable al breaker: sin un plan de degradación, "el breaker está abierto" solo significa "ahora fallo rápido en vez de lento", y fallar rápido a secas no le sirve al comprador. Con degradación, "el breaker está abierto" significa "difiero el envío y sigo vendiendo".

El load shedding, por su parte, ataca un caso que ningún patrón anterior toca: la sobrecarga de entrada. Timeout, retry, breaker y bulkhead asumen que el problema está en una dependencia. Pero cuando el problema es que a ti te llega 3x de tráfico, no hay dependencia que arreglar —hay demasiada demanda para tu oferta—. El único patrón que responde a eso es rechazar parte de la demanda a tiempo, y ese es el load shedding. Es la pieza que faltaba para un cuadro completo de resiliencia: sabes sobrevivir a las fallas de tus partes (M2-M6) y ahora sabes sobrevivir a tu propio éxito (un pico que te desborda).

La conclusión es que este módulo cubre las dos preguntas que los anteriores dejaron abiertas: qué respondo cuando algo no-crítico falla (degradación) y a quién dejo de atender cuando me desbordo (shedding). Por eso no sustituye a nada de lo anterior —lo completa—, y por eso el capstone del módulo 8 los combina todos.

Errores comunes

Creer que degradar es "bajar la calidad" o "hacer trampa". Qué pasa: alguien oye "entrega una versión reducida" y lo interpreta como conformarse con menos, como una excusa para hacer las cosas a medias. Por qué pasa: "reducido" suena a peor. Cómo detectarlo: si tu instinto es "no, el sistema debe funcionar completo o no funcionar", tienes el estándar equivocado para un sistema distribuido. Cómo corregirlo: recuerda el modo cojera —un auto que te lleva a casa a 40 km/h no es "hacer trampa", es la diferencia entre llegar y quedarte tirado—. En un sistema distribuido siempre hay algo caído o lento (módulo 1); exigir "todo perfecto o error total" garantiza el error total. Degradar no baja tu estándar: lo hace realista. El estándar es "el comprador completa su compra", no "todos los microservicios respondieron a la vez".

Confundir "protegerse de la falla" con "responder a la falla". Qué pasa: se pone un breaker y un bulkhead y se da el trabajo por terminado. Por qué pasa: el checkout ya no se cae, así que "parece" resuelto. Cómo detectarlo: pregúntate qué ve el comprador cuando shipping está caído. Si la respuesta es "una pantalla de error rápida", protegiste a orders pero no atendiste al comprador. Cómo corregirlo: el breaker y el bulkhead son la mitad; la degradación es la otra. Un CircuitOpenError que llega hasta el usuario es una falla dura con menos latencia, no una degradación. El objetivo no es fallar rápido, es fallar suave.

Pensar que el load shedding es "rendirse" o "perder clientes a propósito". Qué pasa: se resiste a rechazar tráfico porque "cada request es un cliente" y rechazar suena a perder ventas. Por qué pasa: rechazar el 60% parece tirar el 60% del negocio. Cómo detectarlo: si tu argumento es "hay que atenderlos a todos", mide qué pasa cuando lo intentas. Cómo corregirlo: la aritmética del triage. Sin shedding, "atenderlos a todos" bajo 3x entrega 1.7% útil —pierdes al 98%—; con shedding proteges el 40% al 100% y, con prioridad, el núcleo entero. No rechazas para perder clientes: rechazas a los que de todos modos no ibas a poder atender, para no arrastrar contigo a los que sí. El 60% rechazado no era negocio que tenías; era negocio que el colapso te iba a quitar igual, más el otro 40%.

Ejercicios

Ejercicio 1 — Ubica cada patrón en el mapa. Para cada situación, di si la herramienta correcta es degradación elegante o load shedding, y por qué: (a) reviews está caído y la página de producto muestra "reseñas no disponibles" pero carga igual; (b) es Black Friday, llega 5x del tráfico normal, y el sistema rechaza los requests de "productos que también te pueden gustar" para que el checkout no se caiga; (c) shipping está muerto y el checkout confirma el pedido encolando el envío para después; (d) un pico de bots satura la API y el balanceador devuelve 503 a una fracción del tráfico.

Ver solución
  • (a) Degradación elegante. El problema es una dependencia externa caída (reviews); la respuesta es entregar una versión reducida de la página (sin reseñas) en vez de un error. Es exactamente el "valor de fallback / omitir" de la lección 4.
  • (b) Load shedding (por prioridad). El problema es tuyo —te llega 5x de tráfico— y la respuesta es rechazar tráfico de baja prioridad (recomendaciones) para proteger el núcleo (checkout). Es la lección 7.
  • (c) Degradación elegante (diferir/encolar). Dependencia externa caída (shipping); respuesta reducida (pedido confirmado, envío diferido) en vez de tirar la venta. Lección 5.
  • (d) Load shedding. Sobrecarga de entrada; se rechaza una fracción del tráfico (503) para proteger la capacidad. Lección 7. (Con prioridad sería aún mejor: rechazar primero a los bots o al tráfico secundario, no al azar.)

La clave que los separa: pregúntate quién está en problemas. ¿Una dependencia? → degradación. ¿Yo, por exceso de demanda? → load shedding.

Ejercicio 2 — Lee los cobros huérfanos. En la simulación de degradación, la falla dura reporta "1981 cobros huérfanos". (a) Explica de dónde salen esos cobros y por qué la falla dura es peor que simplemente 0% de éxito. (b) En la versión degradada, ese número es 0: ¿por qué? (c) ¿Qué te dice esto sobre el orden en que un checkout debería llamar a payments y a shipping?

Ver solución

(a) En la falla dura, el checkout llama a payments primero (cobra) y luego a shipping (crea el envío); si shipping falla, se cae el pedido entero. Pero para entonces payments ya cobró. De los 2000 checkouts, 1981 pasaron el cobro (payments tiene 1% de falla) y luego chocaron con shipping caído: esos 1981 cobros quedaron huérfanos —cargos reales por pedidos que no existen—. Es peor que 0% de éxito porque 0% ya es no vender nada; encima le cobraste a 1981 personas algo que ahora tienes que detectar y reembolsar, con el costo operativo, los reclamos y la desconfianza que eso genera. Fallar duro no es neutral: crea trabajo y daño.

(b) En la versión degradada, cuando shipping falla el pedido no se cae: se completa y el envío se difiere. Así que el cobro de payments corresponde a un pedido real y confirmado —no hay nada que revertir—. El cobro y el pedido quedan consistentes; solo el envío queda pendiente (y se crea después). Cero huérfanos.

(c) Sugiere una regla de diseño: haz lo crítico e irreversible (cobrar) lo más tarde y lo más protegido posible, y solo cuando lo demás ya está asegurado. Si shipping se puede diferir, ni siquiera debería poder tumbar un cobro ya hecho. Una opción aún mejor es que payments sea la última llamada síncrona y todo lo no-crítico se difiera después del cobro exitoso —así nunca cobras algo que otro paso pueda invalidar—. La idempotencia del módulo 4 y la degradación de este se dan la mano aquí.

Ejercicio 3 — El límite de la aritmética del triage. Un compañero, convencido por el número del load shedding, propone: "si rechazar el 60% protege al 40% al 100%, rechacemos siempre el 60%, incluso en tráfico normal —así siempre estamos al 100%—". Explica por qué esto es un error, y cuál es la condición exacta bajo la cual el load shedding ayuda y bajo la cual solo tira negocio.

Ver solución

El error es aplicar la medicina de la sobrecarga a un paciente sano. El load shedding ayuda solo cuando la demanda supera la capacidad. En el pico de 3x (300/s ofrecidos, 120/s de capacidad), rechazar el excedente es gratis en clientes reales: esos requests de todas formas no se iban a atender —el colapso se los iba a llevar igual, junto con los que sí cabían—. Rechazar el 60% no pierde negocio; salva el 40% que si no colapsaría también.

Pero en tráfico normal (100/s, por debajo de la capacidad de 120/s), rechazar el 60% sería tirar 60 requests/s que sí podías atender perfectamente. Ahí el shedding no protege nada —no hay sobrecarga de la cual proteger— y solo destruye negocio: rechazas clientes que ibas a servir bien.

La condición exacta: el shedding es correcto cuando y solo cuando admitir un request más degradaría el servicio de los ya admitidos (es decir, cuando estás en o por encima de tu capacidad). Por eso el load shedding real no es un porcentaje fijo, sino una decisión dinámica que se activa según una señal de carga (profundidad de cola, latencia, CPU) y se apaga cuando la carga baja. Un shedding "siempre al 60%" es tan absurdo como un triage que rechaza pacientes en una sala de urgencias vacía.

De reconocer el problema a construir la solución

En esta lección viste las dos preguntas que los módulos anteriores dejaron abiertas y que este cierra: qué le respondes al usuario cuando algo no-crítico falla (degradación elegante) y a quién dejas de atender cuando te desbordas (load shedding). Viste las dos analogías que las gobiernan —el modo cojera del auto (reduce, no se apaga) y el triage de urgencias (sacrifica lo secundario para salvar lo crítico)—, el mapa de los dos ejes, y los dos números faro: 0% vs 99.1% degradando el checkout, y 1.7% vs núcleo protegido con shedding.

Antes de avanzar deberías poder: explicar por qué "todo o nada" es casi siempre la respuesta equivocada en un sistema distribuido; distinguir "protegerse de la falla" (breaker, bulkhead) de "responder a la falla" (degradación); y decir, ante una situación, si el problema pide degradación (una dependencia cayó) o load shedding (yo me desbordé).

Lo que sigue es dejar de anunciar y medir. La lección 2 monta la simulación del checkout de Mercado con shipping caído y pone las dos versiones lado a lado —falla dura y degradación— para que veas, en código y en números, la diferencia entre 0% y 99.1%, y por qué la falla dura además deja 1981 cobros que hay que reembolsar. Ese contraste es el corazón del módulo, y el punto de partida de todo lo que construirás después.

Recursos

  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software, 2ª ed. (Pragmatic Bookshelf, 2018) — la fuente canónica. Sus stability patterns incluyen "Fail Fast" y la idea de degradación elegante (graceful degradation): un sistema que entrega valor reducido en vez de fallar por completo. El libro entero es la referencia de este ecosistema. En inglés.
  • Betsy Beyer et al. (eds.), Site Reliability Engineering: How Google Runs Production Systems (O'Reilly, 2016), capítulo 21, "Handling Overload" — sre.google/sre-book/handling-overload. La referencia sobre load shedding y el colapso por congestión: por qué un servidor sobrecargado que acepta todo entrega casi cero goodput, y cómo el shedding y el graceful degradation lo evitan. Gratis y en inglés.
  • Netflix Technology Blog, "Making the Netflix API More Resilient" — netflixtechblog.com/making-the-netflix-api-more-resilient-a8ec62159c2d. El caso real que popularizó los fallbacks como estrategia de degradación: cuando un servicio de personalización cae, Netflix devuelve resultados no personalizados en vez de una página en blanco. En inglés.
  • Marc Brooker (AWS), "Fault Tolerance and Graceful Degradation" — en el Amazon Builders' Library, aws.amazon.com/builders-library. Colección de artículos de ingenieros de AWS sobre tolerancia a fallas, degradación y control de carga en producción. Gratis y en inglés.