Circuit Breaker (Resilience4j)
Guía definitiva de Circuit Breakers en Spring Boot con Resilience4j
📖 ¿Quieres ver este documento más hermoso? Haz clic aquí para la versión HTML con diseño oscuro/claro, tabla de contenidos lateral y más.
1. Introducción a la Resiliencia en Sistemas Distribuidos
Section titled “1. Introducción a la Resiliencia en Sistemas Distribuidos”En arquitecturas de microservicios las llamadas remotas caen por debajo de tu control y un fallo parcial puede propagarse en cascada por todo el clúster.
- [Usuario] ──> [Servicio Edge] ──> [Servicio Pedidos] ──> [Pasarela de Pago LENTA/CAÍDA]
- │ │
- └─(Hilos Bloqueados)───└─(Hilos Agotados esperando timeout)
Fallos en Cascada y Agotamiento del Pool de Hilos
Section titled “Fallos en Cascada y Agotamiento del Pool de Hilos”Cuando un servicio aguas abajo es lento, los hilos del cliente quedan bloqueados. Bajo carga eso deriva rápidamente en Thread Pool Exhaustion. El servicio cliente deja de responder incluso para tráfico que no depende del sistema roto.
Para construir sistemas resilientes hay que diseñar para el fallo y fallar rápido (Fail Fast).
La diferencia no está en si esa transacción específica se completa, sino en qué le pasa a todo el sistema mientras persiste la falla.
- Escenario A: SIN Circuit Breaker: un servicio lento inmoviliza hilos por timeout. Bajo carga, el pool se agota y el sistema entero deja de responder.
- Escenario B: CON Circuit Breaker: los primeros fallos abren el disyuntor a OPEN. La llamada se rechaza en milisegundos, se ejecuta un fallback y el resto del sistema sigue operando.
2. El Patrón Circuit Breaker
Section titled “2. El Patrón Circuit Breaker”El patrón Circuit Breaker envuelve llamadas remotas y monitoriza fallos. Funciona como un fusible eléctrico: corta la conexión con un servicio en fallo para proteger al sistema que consume ese servicio.
- CLOSED: las llamadas siguen fluyendo. Si se supera el umbral, pasa a OPEN.
- OPEN: las llamadas se rechazan de inmediato y se ejecuta un fallback.
- HALF-OPEN: se permiten pocas llamadas de prueba; si fallan regresa a OPEN, si tienen éxito vuelve a CLOSED.
Ejemplos del mundo real
Section titled “Ejemplos del mundo real”E-Commerce: Pedido -> Inventario
Section titled “E-Commerce: Pedido -> Inventario”- CLOSED: inventario responde en 50 ms y la compra se procesa con normalidad.
- OPEN: el inventario falla. El disyuntor salta a OPEN y ejecuta un fallback: guardar el pedido y posponer el cobro.
- HALF-OPEN: pasado el tiempo de espera, se prueban pocas compras. Si todas tienen éxito, el circuito vuelve a CLOSED.
App de Transporte: Cotización -> Google Maps
Section titled “App de Transporte: Cotización -> Google Maps”- CLOSED: consulta exitosa en 100 ms. Tarifa y ruta presentadas al instante.
- OPEN: la API devuelve error 403. El disyuntor abre y el fallback calcula distancia en línea recta.
Streaming: Home -> Motor de Recomendaciones
Section titled “Streaming: Home -> Motor de Recomendaciones”- CLOSED: respuesta en 150 ms con filas personalizadas.
- OPEN: el motor se satura. El disyuntor abre y sirve una caché de contenido popular.
3. Evolución de los Circuit Breakers en Spring: de Hystrix a Resilience4j
Section titled “3. Evolución de los Circuit Breakers en Spring: de Hystrix a Resilience4j”Netflix Hystrix fue referencia durante años, pero quedó deprecado en favor de soluciones más ligeras como Resilience4j.
¿Por qué se deprecó Hystrix?
- Diseño monolítico y acoplamiento fuerte a RxJava.
- Introducía overhead de CPU por pools de hilos dedicados.
- Sin soporte oficial para Spring Boot 3.
| Característica | Netflix Hystrix | Resilience4j |
|---|---|---|
| Paradigma | Reactivo | Funcional / Lambdas (Java 8+) |
| Dependencias | Pesadas | Ligeras y modulares |
| Spring Boot 3 | No soportado | Soporte de primer nivel |
| Memoria | Moderada / Alta | Extremadamente baja |
4. Ventanas Deslizantes en Resilience4j
Section titled “4. Ventanas Deslizantes en Resilience4j”Resilience4j evalúa el comportamiento reciente mediante una ventana deslizante, que puede ser:
- COUNT_BASED: solo considera las últimas
Nllamadas. - TIME_BASED: considera solo las llamadas dentro de los últimos
Nsegundos.
Si la ventana no es deslizante, éxitos antiguos diluyen fallos recientes. La ventana deslizante decide usando únicamente el presente inmediato.
- Cinta transportadora (Count-Based): buffer circular con las últimas llamadas. Una nueva llamada reemplaza a la más antigua.
- Cubos de tiempo (Time-Based): buckets parciales dentro de la ventana temporal. El bucket más viejo se descarta conforme avanza el tiempo.
5. Configuración Inicial
Section titled “5. Configuración Inicial”Configuremos una aplicación Spring Boot moderna con Resilience4j.
<!-- Resilience4j Spring Boot Starter --><dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version></dependency>
<!-- Spring Boot Starter AOP --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>resilience4j: circuitbreaker: configs: default: slidingWindowSize: 100 slidingWindowType: COUNT_BASED minimumNumberOfCalls: 10 failureRateThreshold: 50 slowCallRateThreshold: 75 slowCallDurationThreshold: 2000ms waitDurationInOpenState: 15000ms permittedNumberOfCallsInHalfOpenState: 5 automaticTransitionFromOpenToHalfOpenEnabled: true instances: paymentServiceBreaker: baseConfig: default slidingWindowSize: 20 failureRateThreshold: 406. Implementación Declarativa
Section titled “6. Implementación Declarativa”Resilience4j intercepta métodos anotados con @CircuitBreaker mediante Spring AOP.
@Servicepublic class PaymentService {
private final RestTemplate restTemplate;
@CircuitBreaker(name = "paymentServiceBreaker", fallbackMethod = "processPaymentFallback") public String processPayment(String transactionId, double amount) { String externalUrl = "https://api.external-payment.com/v1/charge"; return restTemplate.postForObject(externalUrl, new PaymentRequest(transactionId, amount), String.class); }
public String processPaymentFallback(String transactionId, double amount, Throwable exception) { if (exception instanceof CallNotPermittedException) { return "PAYMENT_SERVICE_UNAVAILABLE (Fallo Rápido)"; } return "PAYMENT_PENDING (Almacenado localmente para procesamiento diferido)"; }}Reglas de firmas fallback: el método debe estar en la misma clase, aceptar los mismos parámetros más
Throwablefinal, y devolver el mismo tipo.
7. Configuración Avanzada
Section titled “7. Configuración Avanzada”No todas las excepciones representan fallos de infraestructura. Por ejemplo, las de negocio 4xx.
resilience4j.circuitbreaker: instances: orderServiceBreaker: recordExceptions: - org.springframework.web.client.HttpServerErrorException - java.util.concurrent.TimeoutException ignoreExceptions: - org.springframework.web.client.HttpClientErrorExceptionEventos de transición de estado:
@Configurationpublic class CircuitBreakerEventListener { @PostConstruct public void registerListeners() { registry.getAllCircuitBreakers().forEach(breaker -> { breaker.getEventPublisher() .onStateTransition(event -> log.error("Transición: {}", event.getStateTransition())); }); }}8. Uso Programático
Section titled “8. Uso Programático”La envoltura programática evita los límites del proxy de Spring AOP.
@Servicepublic class ProgrammaticPaymentService { private final CircuitBreaker circuitBreaker;
public String processPayment(String txId, double amount) { Supplier<String> paymentCall = () -> executePayment(txId, amount); Supplier<String> decorated = CircuitBreaker.decorateSupplier(circuitBreaker, paymentCall);
return Try.ofSupplier(decorated) .recover(throwable -> "FALLBACK_TRIGGERED") .get(); }}9. Observabilidad y Métricas
Section titled “9. Observabilidad y Métricas”Spring Boot Actuator y Micrometer exponen métricas del estado de los circuit breakers.
resilience4j_circuitbreaker_state: estado actual.resilience4j_circuitbreaker_calls: llamadas por resultado.resilience4j_circuitbreaker_failure_rate: tasa de fallos.
10. Estrategias de Prueba
Section titled “10. Estrategias de Prueba”Verifica transiciones de estado con pruebas de integración robustas.
@SpringBootTestclass PaymentServiceIntegrationTest { @Autowired private PaymentService paymentService;
@Test void testCircuitBreakerTrips() { when(restTemplate.postForObject(any(), any(), any())) .thenThrow(new RuntimeException("Network Error"));
for (int i = 0; i < 11; i++) { paymentService.processPayment("tx", 10.0); }
assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); }}11. Buenas Prácticas
Section titled “11. Buenas Prácticas”- Evita autollamadas: invocar un método
@CircuitBreakerdesde la misma clase elude el proxy AOP. - Ajusta timeouts HTTP: mantén timeouts agresivos para que el circuito no quede retenido por llamadas lentas.