Resiliencia en Sistemas Distribuidos
Timeout, retry, circuit breaker, idempotencia, bulkhead, rate limiting, backpressure, load shedding, dead letter queue, saga y degradación controlada: los mecanismos explícitos que necesita un sistema distribuido para convivir con fallos parciales.
Un sistema distribuido cambia por completo el modelo de fallos. En un proceso local, una función puede devolver un resultado o lanzar una excepción. En una interacción remota:
flowchart LR A["Service A"] --> N["Network"] --> B["Service B"]
puede suceder que la request nunca llegue, que llegue dos veces, que el Service B procese la operación pero la respuesta se pierda, que el Service B tarde demasiado o que solo una parte del sistema esté fallando. Por eso, una arquitectura distribuida necesita mecanismos explícitos de resiliencia: Timeout, Retry, Circuit Breaker, Idempotencia, Bulkhead, Rate Limiting, Backpressure, Load Shedding, Dead Letter Queue, Saga y Graceful Degradation.
Timeout
Una dependencia nunca debería poder bloquear recursos indefinidamente:
sequenceDiagram participant P as Payment participant Pr as Provider P->>Pr: request Note over P,Pr: ... P--xPr: timeout
Pero:
Timeout no significa que la operación no haya ocurrido.
Este detalle es crítico:
sequenceDiagram participant C as Client participant Pr as Provider C->>Pr: charge() Note over Pr: charges card Pr--xC: response lost
El cliente ve un timeout. La tarjeta ya fue cobrada.
Retry
Ante fallos transitorios:
flowchart LR R["Request"] --> F1["Failure"] --> Rt1["Retry"] --> F2["Failure"] --> Rt2["Retry"]
Un retry robusto suele considerar backoff exponencial, jitter, cantidad máxima de intentos y un presupuesto de tiempo.
Retry + operación no idempotente
Esto es peligroso:
sequenceDiagram participant C as Client participant Pr as Provider C->>Pr: ChargeCard() Note over C,Pr: timeout C->>Pr: retry ChargeCard() Note over Pr: cobrado dos veces
Por eso Timeout, Retry e Idempotencia deben diseñarse juntos.
Circuit Breaker
Permite dejar de enviar requests a una dependencia que está fallando:
stateDiagram-v2 [*] --> CLOSED CLOSED --> OPEN: supera el umbral de fallos OPEN --> HALF_OPEN: probe tras cooldown HALF_OPEN --> CLOSED: probe exitoso HALF_OPEN --> OPEN: probe falla
Cuando está OPEN, el sistema puede fallar rápidamente en lugar de acumular timeouts y seguir saturando la dependencia.
Idempotencia
Supongamos:
POST /payments
Idempotency-Key: abc123
En la primera request, el provider crea el payment pero la respuesta se pierde. En la segunda request, con el mismo Idempotency-Key, el sistema reconoce que esa operación ya fue procesada y devuelve el payment existente en lugar de crear uno nuevo:
flowchart LR K["Idempotency-Key: abc123"] --> E["Existing Payment"]
Es fundamental para payments, orders, webhooks y message consumers.
At-Least-Once Delivery
Muchos sistemas de mensajería utilizan semántica de entrega at-least-once:
sequenceDiagram participant Broker participant Consumer Broker->>Consumer: message Note over Consumer: process Note over Consumer: crash before ack Broker->>Consumer: message (reenviado) Note over Consumer: message duplicated
Por eso una estrategia práctica es combinar at-least-once con un idempotent consumer.
Bulkhead
Aísla recursos:
flowchart TD App["Application"] --> PP["Payment Pool"] App --> OP["Order Pool"] App --> NP["Notification Pool"]
Si Notifications se degrada, no debería consumir todos los recursos disponibles para Payments.
Rate Limiting
Controla el flujo entrante:
flowchart LR C["Client"] --> RL["Rate Limiter"] RL -->|allowed| A["Backend"] RL -->|rejected| R["429"]
Protege contra clientes abusivos, picos de tráfico, sobrecarga y clientes que generan tráfico descontrolado de forma accidental. Entre las implementaciones más conocidas están token bucket y leaky bucket.
Backpressure
Supongamos un producer a 20k msg/s y un consumer a 5k msg/s. La diferencia genera acumulación:
flowchart LR P["Producer<br/>20k msg/s"] --> Q["Queue<br/>(creciendo)"] --> C["Consumer<br/>5k msg/s"]
Backpressure consiste en hacer que el sistema controle esa diferencia en lugar de aceptar trabajo indefinidamente.
Load Shedding
Cuando el sistema no puede procesar todo:
flowchart TD L["Incoming Load"] --> O["Overloaded System"] O -->|Critical| Pr["Process"] O -->|Optional| Re["Reject"]
Es preferible rechazar algunas requests antes que permitir que todo el sistema colapse.
Dead Letter Queue
flowchart LR Q["Queue"] --> C["Consumer"] -->|failure + retries agotados| DLQ["DLQ"]
La DLQ permite inspeccionar mensajes, corregir problemas, reprocesarlos más tarde y evitar que un mensaje defectuoso bloquee la cola de forma permanente.
Saga
Cuando una operación de negocio atraviesa varios servicios, no tenemos una única transacción ACID. Podemos implementar una Saga:
flowchart LR O["Create Order"] --> P["Authorize Payment"] --> I["Reserve Inventory"] --> S["Create Shipment"]
Si Inventory falla, se dispara una cadena de acciones compensatorias:
flowchart LR F["Reserve Inventory ✘"] --> RP["Release Payment"] --> CO["Cancel Order"]
Graceful Degradation
No todos los componentes deberían tener el mismo nivel de criticidad:
flowchart TD Ch["Checkout"] --> Pay["Payment — critical"] Ch --> Ord["Order — critical"] Ch --> Rec["Recommendation — optional"]
Si Recommendations está caído, Checkout sigue disponible y solo se pierde la funcionalidad de Recommendations. La arquitectura mantiene disponible lo crítico.
Cómo analizar un fallo
Un enfoque práctico es recorrer una serie de preguntas:
flowchart TD D["Dependency fails"] --> Q1["¿La request hace timeout?"] Q1 --> Q2["¿Deberíamos hacer retry?"] Q2 --> Q3["¿Es seguro el retry?"] Q3 --> Q4["¿Debería abrirse el circuit?"] Q4 --> Q5["¿Puede haber procesamiento duplicado?"] Q5 --> Q6["¿Qué pasa con los mensajes en cola?"] Q6 --> Q7["¿Se necesita compensación?"] Q7 --> Q8["¿Qué experimenta el usuario?"]
Herramientas
Dependiendo de la plataforma: Resilience4j, Envoy, Istio, AWS API Gateway, AWS SQS, Kafka, RabbitMQ, Kubernetes health probes. Las herramientas implementan mecanismos. No reemplazan el diseño de los modos de fallo.
Ninguno de estos mecanismos sirve si no se verifica que funcionan. Un circuit breaker que nunca se abrió en un entorno controlado es una hipótesis, no una protección: hace falta ejercitarlo con performance testing bajo carga y con fallas inyectadas a propósito.
