Alejandría

Busca en Alejandría

Técnicas

Patrones de resiliencia

Timeouts, reintentos, circuit breakers, bulkheads y degradación controlada para sobrevivir a dependencias que fallan.

01

En pocas palabras

Conjunto de patrones que limitan el impacto de dependencias lentas o caídas para que un fallo local no se convierta en una caída en cascada.

Qué problema resuelve

Un proveedor lento agota hilos y conexiones, las peticiones se acumulan y todo el sistema deja de responder.

Cuándo utilizarlo

Cualquier llamada por red. Ejemplo: la consulta al buró de crédito tarda 30 s y bloquea las solicitudes de todos los clientes.

02

De la idea a la práctica

Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.

Los patrones y lo que resuelven

Patrón Protege contra Idea
Timeout Esperas indefinidas Toda llamada tiene un límite; mejor fallar rápido
Retry con backoff Fallos transitorios Reintentar pocas veces, esperando cada vez más y con jitter
Circuit breaker Insistir sobre una dependencia caída Tras N fallos, cortar llamadas un tiempo y probar después
Bulkhead Que una dependencia consuma todos los recursos Pools separados por dependencia (hilos, conexiones)
Rate limiter Sobrecarga propia o de terceros Limitar peticiones por unidad de tiempo
Load shedding Colapso por exceso de carga Rechazar pronto (503/429) lo que no se puede atender
Fallback Indisponibilidad total Respuesta degradada: caché, valor por defecto, cola diferida

Timeouts y presupuesto de latencia

Fija el timeout a partir de la latencia observada (p. ej. algo por encima del p99) y del presupuesto total del caso de uso. Si la API tiene 2 s de SLO y llama a tres dependencias en serie, ninguna puede tener 5 s de timeout. Propaga un deadline (gRPC lo hace de forma nativa) para que los servicios aguas abajo dejen de trabajar en peticiones que ya nadie espera.

Reintentos sin amplificar el problema

  • Solo sobre errores transitorios (timeouts, 503, 429) y operaciones idempotentes.
  • Backoff exponencial con jitter: espera = random(0, min(tope, base · 2^intento)). Sin jitter, todos los clientes reintentan a la vez.
  • Reintenta en una sola capa. Tres capas con tres reintentos cada una multiplican la carga por 27.
  • Usa un retry budget: no más de, por ejemplo, el 10 % de las peticiones pueden ser reintentos.

Circuit breaker

         fallos ≥ umbral
CERRADO ──────────────────▶ ABIERTO ──(espera)──▶ SEMIABIERTO
   ▲                                                 │
   └──────────── llamadas de prueba con éxito ───────┘
                 (si fallan, vuelve a ABIERTO)

En estado abierto la llamada falla inmediatamente, lo que libera recursos y da tiempo a la dependencia para recuperarse. Ver la implementación en Resilience4j.

Ejemplo: consulta al buró

Decisión Valor ilustrativo
Timeout 3 s (p99 observado: 1,8 s)
Reintentos 1, solo en 503/timeout, backoff 200 ms + jitter
Circuit breaker Abre con 50 % de fallos en 20 llamadas; semiabierto a los 30 s
Bulkhead Máximo 20 llamadas concurrentes al buró
Fallback Solicitud pasa a PENDIENTE_BURO y se reevalúa desde una cola

El fallback es una decisión de negocio: aprobar sin buró no es aceptable, pero dejar la solicitud pendiente y avisar al cliente sí.

Ficha de resiliencia por dependencia

Dependencia Tipo Timeout Reintentos Circuit breaker Bulkhead Degradación
Buró de crédito Externa 3 s 1, solo 503/timeout 50 % en 20 llamadas 20 Solicitud PENDIENTE_BURO
PostgreSQL Interna 2 s 0 (transacción) No Pool 10 Error 503 con Retry-After
Servicio de SMS Externa 2 s 3 con backoff Sí 10 Encolar y reenviar
Motor de scoring Interna 800 ms 1 Sí 30 Reglas básicas + revisión manual

Mantener esta tabla junto al código (o en el ADR correspondiente) obliga a decidir explícitamente qué pasa cuando cada dependencia falla.

Timeouts en cascada

Cliente (timeout 10 s)
  └─ API pública (presupuesto 8 s)
       ├─ Scoring   (800 ms)
       ├─ Buró      (3 s, 1 reintento → máx. ~6,5 s)
       └─ BD        (2 s)

Cada capa debe tener un timeout menor que la que la llama. Si la API pública esperara 30 s al buró, el cliente ya habría abandonado y la API seguiría ocupando un hilo y una conexión por una respuesta que nadie leerá.

Load shedding y concurrencia adaptativa

Cuando la carga supera la capacidad, es mejor rechazar rápido una parte que degradar a todos:

  • Limitar peticiones concurrentes en curso y responder 503 o 429 al superar el límite.
  • Priorizar: atender antes los pagos que las consultas de catálogo.
  • Concurrencia adaptativa: ajustar el límite según la latencia observada (algoritmos tipo AIMD o Vegas, disponibles en librerías como Netflix concurrency-limits).

Probar la resiliencia

# Toxiproxy: insertar 5 s de latencia en la conexión hacia el mock del buró
toxiproxy-cli create -l 0.0.0.0:18080 -u buro-mock:8080 buro
toxiproxy-cli toxic add -t latency -a latency=5000 buro
# Ejecutar la prueba de integración y comprobar: respuesta < 4 s, estado PENDIENTE_BURO,
# circuito abierto tras N fallos y métrica de fallback incrementada
toxiproxy-cli toxic remove -n latency_downstream buro
Experimento Resultado esperado
Latencia alta en una dependencia Timeout y degradación; el resto del servicio sigue sano
Dependencia devuelve 503 continuamente El circuito se abre; no hay tormenta de reintentos
Pico de tráfico 3× Rechazos controlados; latencia de las peticiones aceptadas estable
Caída de una zona El tráfico se redistribuye sin intervención

Empieza en entornos de prueba con Toxiproxy o WireMock y, con madurez, ejecuta game days controlados en producción.

Criterios de implementación

Inventaría las dependencias de cada servicio y define para cada una timeout, reintentos y comportamiento degradado. Mide aperturas del circuito y rechazos como métricas de primera clase. Valida el diseño con experimentos de inyección de fallos (latencia y errores) en un entorno de pruebas antes de confiar en él en producción.

Continúa en las fuentes

Documentación oficial para verificar detalles y explorar más.