Módulo 13 · Resiliencia y recuperación/Lección 78
Patrones de resiliencia: reintentos, idempotencia y degradación
Tiempos de espera, reintentos con espera exponencial, idempotencia, circuit breaker, aislamiento y estabilidad estática: cómo evitar que una falla pequeña se vuelva una caída total.
Las caídas grandes rara vez empiezan grandes. Una base de datos se pone lenta, los clientes esperan sin límite, reintentan todos a la vez, los hilos se agotan y el servicio de arriba también cae. La falla original era pequeña; la reacción la volvió total.
Los patrones de esta lección cortan esa cadena. Complementan la redundancia de alta disponibilidad: tener copias no sirve si una falla se contagia a todas.
Tiempos de espera y reintentos
Pon siempre un tiempo de espera en toda llamada remota: para conectar y para recibir la respuesta. Una llamada sin límite deja un hilo ocupado mientras la dependencia esté mal, y cuando se agotan los hilos tu servicio deja de responder aunque esté sano. Para elegir el valor, mira la latencia de la dependencia en un percentil alto, como p99,9, y decide cuántas falsas alarmas toleras.
Reintenta solo lo transitorio: un 503, un throttling, una conexión cortada. Un 400 o un 403 va a fallar igual la segunda vez. Y espera entre intentos con espera exponencial (cada pausa el doble de larga, con un tope) más jitter (un componente aleatorio para que mil clientes no reintenten en el mismo milisegundo).
intentos_max = 3 # incluye el primero
base = 100 ms ; tope = 5 s
para intento en 1..intentos_max:
respuesta = llamar(dependencia, timeout = 2 s, clave_idempotencia = K)
si respuesta es éxito: devolver respuesta
si el error no es transitorio (400, 403, 404): fallar ya
espera = aleatorio(0, min(tope, base * 2^intento)) # jitter completo
dormir(espera)
fallar con el último error # y que otra capa decida, no que reintenteEl peligro es la tormenta de reintentos. Si cinco capas reintentan tres veces cada una, la base de datos del fondo recibe hasta 3⁵ = 243 veces la carga original justo cuando está débil. La regla del Amazon Builders' Library («Timeouts, retries, and backoff with jitter») es reintentar en un solo punto de la cadena y limitar los reintentos con una cubeta de fichas (token bucket).
Los SDK de AWS ya hacen esto por ti. El modo standard reintenta los errores transitorios y de throttling con espera exponencial y jitter, y tiene una cuota de reintentos: si fallan demasiadas llamadas seguidas, deja de reintentar y devuelve el error. El modo adaptive además frena el envío cuando detecta throttling; sirve cuando atacas un solo recurso. No envuelvas el SDK en otro ciclo de reintentos: multiplicas.
Idempotencia: repetir sin efectos dobles
Reintentar solo es seguro si la operación es idempotente: hacerla dos veces deja el mismo resultado que hacerla una. Leer es idempotente. «Cobrar 50 dólares» no lo es: si la respuesta se perdió en la red y reintentas, cobras dos veces.
La solución es una clave de idempotencia. El cliente genera un identificador único por intención («el pago del pedido 8812») y lo manda en cada intento. El servidor guarda la clave junto con el resultado, en la misma operación atómica. Si llega otra vez la misma clave, devuelve el resultado guardado sin repetir el cobro. Si llega la misma clave con otros parámetros, responde con error: es otra intención.
AWS usa este patrón en sus propias APIs: RunInstances de EC2 acepta un ClientToken, y los SDK lo generan y lo reutilizan en los reintentos. En tu código, una escritura condicional en DynamoDB que solo guarda la clave si no existía es una forma simple de lograrlo. También lo necesitas al consumir colas: una cola estándar de SQS entrega al menos una vez, así que el mismo mensaje puede llegar dos veces.
Circuit breaker, aislamiento y control de carga
Un circuit breaker envuelve las llamadas a una dependencia y tiene tres estados. Cerrado: las llamadas pasan y se cuentan los errores. Abierto: superado un umbral, deja de llamar y falla de inmediato, sin esperar el timeout. Medio abierto: después de un rato deja pasar unas pocas llamadas de prueba; si salen bien, se cierra. Así no gastas hilos en una dependencia caída y le das aire para recuperarse.
El aislamiento (bulkhead) separa recursos para que una parte no consuma todo. Un grupo de conexiones para el proveedor de pagos y otro para el de correos: si correos se cuelga, los pagos siguen. En Lambda, la concurrencia reservada hace dos cosas: garantiza capacidad a una función crítica y le pone techo para que no sature una base de datos. Con valor 0, la función deja de procesar: un interruptor de emergencia.
A mayor escala, la arquitectura de celdas divide el sistema en copias completas e independientes, cada una con una parte de los clientes: una celda rota afecta solo a los suyos. El shuffle sharding va más allá: a cada cliente le asigna una combinación distinta de servidores, así dos clientes casi nunca comparten la misma suerte. Route 53 lo usa: cada dominio recibe 4 de 2.048 servidores de nombres virtuales, y hay unos 730 mil millones de combinaciones posibles (Amazon Builders' Library, «Workload isolation using shuffle-sharding»).
Cuando llega más carga de la que puedes atender, es mejor rechazar rápido a algunos que atender lento a todos. Throttling limita cuánto puede pedir cada cliente: API Gateway usa una cubeta de fichas por cuenta, etapa, método y, con planes de uso, por cliente, y responde 429 Too Many Requests al excederse. El descarte de carga (load shedding) es la versión del servidor: si está al límite, responde un error barato de inmediato en vez de aceptar trabajo que no terminará a tiempo.
Degradación controlada y colas como amortiguador
No todas las dependencias son igual de importantes. La degradación controlada convierte dependencias duras en blandas: si el servicio de recomendaciones cae, la tienda muestra los productos más vendidos desde una caché. Si el inventario exacto no responde, muestras «disponible» con la última cifra conocida. Lo no crítico se apaga con un interruptor (un feature flag) que alguien pueda mover durante el incidente sin desplegar código.
Las colas absorben picos: el frente acepta el pedido, lo deja en SQS y los trabajadores lo procesan a su ritmo. Lo viste en desacoplar. Ojo con el límite: una cola con millones de mensajes viejos no es resiliencia, es una deuda. Vigila la edad del mensaje más antiguo y decide qué hacer con trabajo que ya no tiene sentido procesar.
Estabilidad estática: no depender del plano de control
| Criterio | Plano de control | Plano de datos |
|---|---|---|
| Qué hace | Crea, modifica y borra recursos | Hace el trabajo del día a día con los recursos que ya existen |
| Ejemplo en EC2 | Lanzar una instancia: buscar servidor, crear la interfaz de red, preparar el volumen | Enrutar paquetes y leer o escribir en el volumen de una instancia que ya corre |
| Complejidad | Más piezas, más probable que falle | Más simple, diseñado para mayor disponibilidad |
| En un incidente | No cuentes con él | Es lo que debe seguir funcionando |
Un sistema tiene estabilidad estática cuando sigue funcionando aunque una dependencia falle, sin tener que cambiar nada. El ejemplo clásico: en vez de esperar a que se caiga una AZ para lanzar instancias en las otras, ya tienes la capacidad puesta. Con tres AZs, AWS sobreaprovisiona un 50 %: cada zona trabaja a cerca del 66 % y, si una cae, las otras dos cargan todo sin lanzar nada nuevo. Auto Scaling repone después, sin prisa.
El riesgo del diseño contrario es que justo durante una falla grande el plano de control está bajo presión: todos quieren lanzar instancias a la vez en las zonas sanas. Si tu recuperación depende de crear recursos, depende de la parte más frágil. Lo mismo aplica entre regiones: el cambio de tráfico debe usar mecanismos del plano de datos, como verás en failover y failback.
Para llevar esto a decisiones de recuperación, sigue con RPO y RTO y con multirregión y multinube.
Quiz
Pon a prueba lo que aprendiste
0 de 5 correctas · 5 sin responder
1.Un servicio llama a un proveedor externo sin timeout. El proveedor se cuelga y, minutos después, tu servicio deja de responder aunque su CPU está baja. ¿Qué pasó?
2.Una API de pagos recibe la misma petición dos veces porque el cliente reintentó tras un timeout. ¿Qué evita el cobro doble?
3.Tras una caída breve de la base de datos, todos los clientes reintentan exactamente cada 1 segundo y la base no logra recuperarse. ¿Qué falta?
4.Una aplicación corre en tres AZs con Auto Scaling, dimensionada al 100 % de la carga total entre las tres. ¿Por qué no es estáticamente estable ante la caída de una AZ?
5.Una función Lambda que procesa una cola satura la base de datos con demasiadas conexiones en los picos. ¿Qué ajuste aplica el patrón de aislamiento?