11.2 熔断器

响应式熔断器原理与实现:Resilience4j CircuitBreakerOperator Reactor 适配、CLOSED/OPEN/HALF_OPEN 状态机、失败率阈值与熔断时长配置、半开状态下的试探恢复、以及熔断事件监听与指标暴露。

熔断器解决什么问题

当下游服务故障时,如果不加保护:

  • 请求不断超时,线程/连接被占满
  • 雪崩效应:故障服务拖垮上游调用方
  • 恢复后瞬间涌入的请求再次击垮服务

熔断器(Circuit Breaker)在故障率达到阈值时快速失败,给下游恢复时间。

Resilience4j 熔断器

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-reactor</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
</dependency>

配置与使用

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator;

@Configuration
public class ResilienceConfig {

    @Bean
    public CircuitBreaker orderServiceBreaker() {
        io.github.resilience4j.circuitbreaker.CircuitBreakerConfig config =
            io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.custom()
            .failureRateThreshold(50)           // 失败率 > 50% 触发熔断
            .waitDurationInOpenState(Duration.ofSeconds(30))  // 熔断持续 30 秒
            .permittedNumberOfCallsInHalfOpenState(10)  // 半开时允许 10 个试探请求
            .slidingWindowSize(100)             // 统计最近 100 次调用
            .minimumNumberOfCalls(20)           // 至少 20 次调用才开始算失败率
            .slowCallRateThreshold(80)          // 慢调用 > 80% 也触发熔断
            .slowCallDurationThreshold(Duration.ofSeconds(2))  // > 2s 算慢调用
            .build();

        return CircuitBreaker.of("order-service", config);
    }
}

minimumNumberOfCalls 容易被忽略:它的默认值是 100(恰好等于默认窗口大小),低流量服务可能永远凑不够样本,熔断器一直不打开。把它设成 20,表示窗口内至少 20 次调用后才评估失败率。注意它是固定默认值而不是随窗口推导:如果缩小 slidingWindowSize 却没同步调整它,最少调用数会大于窗口容量,窗口永远凑不够评估样本,熔断器等于失效。默认窗口是 COUNT_BASED;改成 TIME_BASED 时 slidingWindowSize 的单位变成秒。maxWaitDurationInHalfOpenState 默认是 0(一直等),如果半开试探请求卡住不返回,熔断器会停在 HALF_OPEN,建议设一个上限(如 5 秒)。

使用:

@Service
public class OrderClient {
    private final CircuitBreaker breaker;
    private final WebClient webClient;

    public Mono<String> getOrders() {
        return webClient.get().uri("/api/orders")
            .retrieve()
            .bodyToMono(String.class)
            .transformDeferred(CircuitBreakerOperator.of(breaker))
            .onErrorResume(CallNotPermittedException.class, err ->
                Mono.just("{\"error\":\"Circuit breaker open\"}"));
    }
}

状态机

CLOSED(关闭)        OPEN(打开)        HALF_OPEN(半开)
    │                    │                    │
    │ 失败率 < 50%       │ 快速失败           │ 允许有限请求
    │ 正常通过           │ 所有请求被拒       │ 检查成功率
    │                    │                    │
    │ 失败率 >= 50%      │ 等待 30s           │ 成功 → CLOSED
    └───────────────────→├───────────────────→┤ 失败 → OPEN

半开状态

OPEN 状态等待 30 秒后进入 HALF_OPEN:

  • 允许 10 个 试探请求通过
  • 这 10 个请求的失败率低于 50%(即成功率高于 50%),回到 CLOSED
  • 否则回到 OPEN,再等待 30 秒

这个设计防止服务「刚恢复就被压垮」。

事件监听

breaker.getEventPublisher()
    .onStateTransition(event ->
        System.out.println("Breaker " + breaker.getName()
            + " transitioned from " + event.getStateTransition().getFromState()
            + " to " + event.getStateTransition().getToState()))
    .onError(event ->
        System.out.println("Breaker " + breaker.getName() + " recorded error"))
    .onSuccess(event ->
        System.out.println("Breaker " + breaker.getName() + " recorded success"));

配合 Micrometer(第 10.1 节)暴露指标:

resilience4j_circuitbreaker_state{name="order-service",state="closed"} 1
resilience4j_circuitbreaker_state{name="order-service",state="open"} 0
resilience4j_circuitbreaker_state{name="order-service",state="half_open"} 0
resilience4j_circuitbreaker_calls_total{name="order-service",kind="failed"} 42
resilience4j_circuitbreaker_calls_total{name="order-service",kind="successful"} 158

这些指标来自 resilience4j-micrometer(还需在 CircuitBreakerRegistry 上注册 TaggedCircuitBreakerMetrics),不是引入 circuitbreaker 模块就自动出现。state 是指标标签而不是数值枚举:当前状态对应的序列为 1,其余为 0。

与重试的配合

熔断器和重试的组合顺序很重要:

webClient.get().uri("/api/orders")
    .retrieve()
    .bodyToMono(String.class)
    .transformDeferred(CircuitBreakerOperator.of(breaker))  // 内层:每次真实尝试都计数
    .retryWhen(Retry.backoff(2, Duration.ofMillis(100))
        .filter(err -> !(err instanceof CallNotPermittedException)))  // 外层:重试
    .subscribe();

链上的顺序决定谁能看到谁:retryWhen 写在后,它就是外层操作符,每次重试都会重新订阅内层的熔断器,所以熔断器统计的是每一次真实尝试。反过来(retryWhen 在前、熔断器在后),熔断器只会看到整条重试链的最终结果,失败率按「逻辑请求」而不是「尝试次数」计算,触发会更慢(一次「重试两次后成功」在内层记 2 失败 1 成功,在外层只记 1 成功)。

重试必须排除 CallNotPermittedException,否则熔断器已经打开时,重试还会反复发起注定被拒的调用,放大无效流量。

事件驱动服务的特殊考虑

事件驱动架构中,熔断器通常用于同步调用(如 WebClient HTTP 请求)。对于异步消息消费,使用消费者组分区和死信队列代替熔断:

正常队列 → 消费者处理
    失败 N 次 → 死信队列(DLQ)
    人工/自动重试 DLQ

小结

参数含义建议值
failureRateThreshold触发熔断的失败率50%
waitDurationInOpenState熔断后等待时间30–60s
permittedNumberOfCallsInHalfOpenState半开试探请求数5–10
slidingWindowSize统计窗口100
minimumNumberOfCalls计算失败率前的最少调用数20(默认等于窗口大小)
slowCallRateThreshold慢调用熔断阈值80%
slowCallDurationThreshold慢调用定义2s

下一节 11.3 限流 :令牌桶、漏桶、Redis 分布式限流与 Resilience4j RateLimiter。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南