Contents

微服务雪崩自救指南:从代码到架构的Circuit Breaker熔断模式实战

一个真实的故事

上周五晚上11点,我们线上某个核心链路突然超时告警。排查发现,下游一个用户服务响应变慢(P99 从 50ms 飙到 8s),导致上游所有调用它的服务线程池被耗尽,最终整条链路雪崩。

最终根因:用户服务依赖的 Redis 实例内存打满了。

但真正的问题不是 Redis 挂了,而是系统没有任何熔断机制。

今天聊的就是如何用 Circuit Breaker 模式从根源上防止这种雪崩。

Circuit Breaker 到底是什么?

Circuit Breaker 的核心思想来自电路中的保险丝——当下游服务异常过多时,自动"断开"调用,快速失败,避免问题扩散。

它有三个状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
                    超过阈值
  ┌──────────┐  ───────────►  ┌──────────┐
  │  CLOSED  │                │   OPEN   │
  │ (正常放行) │                │ (拒绝请求) │
  └──────────┘                └──────────┘
       ▲                            │
       │     探测成功,恢复           │  超过等待时间
       └────────────────────────────┘
              ┌──────────┐
              │HALF-OPEN │
              │(试探性放行) │
              └──────────┘
  • CLOSED:正常状态,所有请求正常通过,同时统计失败率
  • OPEN:熔断状态,所有请求直接快速失败(不再调用下游)
  • HALF-OPEN:探测状态,放少量请求试探下游是否恢复,成功则回到 CLOSED,失败则回到 OPEN

方案一:Resilience4j(推荐,轻量级)

Spring Boot 2 时代的 Hystrix 已经停止维护,Resilience4j 是目前 Java 生态最主流的选择。

依赖配置

1
2
3
4
5
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# application.yml
resilience4j:
  circuitbreaker:
    instances:
      user-service:
        slidingWindowSize: 10          # 统计窗口大小:最近10次调用
        minimumNumberOfCalls: 5         # 至少调用5次才开始计算失败率
        failureRateThreshold: 50        # 失败率超过50%触发熔断
        waitDurationInOpenState: 30s    # OPEN状态持续30秒后进入HALF-OPEN
        permittedNumberOfCallsInHalfOpenState: 3  # HALF-OPEN状态允许3次探测调用
        automaticTransitionFromOpenToHalfOpenEnabled: true

代码使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
@Slf4j
public class UserServiceClient {

    private final RestTemplate restTemplate;

    public UserServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
    public UserDTO getUser(Long userId) {
        return restTemplate.getForObject(
            "http://user-service/api/users/" + userId, 
            UserDTO.class
        );
    }

    // 熔断时的降级逻辑
    public UserDTO getUserFallback(Long userId, Throwable t) {
        log.warn("User service 熔断降级, userId={}, reason={}", 
                 userId, t.getMessage());
        // 返回缓存数据或默认值
        return UserDTO.builder()
            .id(userId)
            .name("服务暂不可用")
            .fromCache(true)
            .build();
    }
}

组合使用:熔断 + 限流 + 重试

Resilience4j 的优势在于可以自由组合:

1
2
3
4
5
6
7
8
9
@CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
@RateLimiter(name = "user-service")  // 额外加限流
@Retry(name = "user-service")        // 额外加重试
public UserDTO getUserWithAllProtection(Long userId) {
    return restTemplate.getForObject(
        "http://user-service/api/users/" + userId, 
        UserDTO.class
    );
}

方案二:阿里 Sentinel(适合复杂流控场景)

如果你已经在用 Spring Cloud Alibaba 生态,Sentinel 是更自然的选择。它除了熔断,还内置了流量控制、热点参数限流、系统自适应保护等功能。

核心配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Sentinel 通过定义规则来控制
@PostConstruct
public void initDegradeRule() {
    DegradeRule rule = new DegradeRule("user-service")
        .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
        .setCount(0.5)              // 慢调用比例阈值
        .setSlowRatioThreshold(1000) // 慢调用 RT 阈值(ms)
        .setTimeWindow(30)           // 熔断时长(秒)
        .setMinRequestAmount(5)      // 最小请求数
        .setStatIntervalMs(10000);   // 统计窗口(ms)

    DegradeRuleManager.loadRules(List.of(rule));
}

Sentinel Dashboard

Sentinel 提供了一个可视化的 Dashboard,可以在运行时动态调整规则,不需要重启服务:

1
2
3
4
# 启动 Sentinel Dashboard
java -Dserver.port=8080 \
     -Dcsp.sentinel.dashboard.server=localhost:8080 \
     -jar sentinel-dashboard-1.8.8.jar

方案三:手写一个简易熔断器

理解原理比依赖框架更重要。下面是一个不到 80 行的完整实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class CircuitBreaker {

    public enum State { CLOSED, OPEN, HALF_OPEN }

    private final int failureThreshold;
    private final long openDurationMs;
    private final int halfOpenMaxCalls;

    private volatile State state = State.CLOSED;
    private AtomicInteger failureCount = new AtomicInteger(0);
    private volatile long lastFailureTime = 0;
    private AtomicInteger halfOpenCalls = new AtomicInteger(0);

    public CircuitBreaker(int failureThreshold, 
                          long openDurationMs, 
                          int halfOpenMaxCalls) {
        this.failureThreshold = failureThreshold;
        this.openDurationMs = openDurationMs;
        this.halfOpenMaxCalls = halfOpenMaxCalls;
    }

    public <T> T execute(Supplier<T> action, Supplier<T> fallback) {
        switch (state) {
            case OPEN:
                if (System.currentTimeMillis() - lastFailureTime > openDurationMs) {
                    state = State.HALF_OPEN;
                    halfOpenCalls.set(0);
                } else {
                    return fallback.get();
                }
                break;
            case HALF_OPEN:
                if (halfOpenCalls.get() >= halfOpenMaxCalls) {
                    return fallback.get();
                }
                break;
        }

        try {
            T result = action.get();
            onSuccess();
            return result;
        } catch (Exception e) {
            onFailure();
            return fallback.get();
        }
    }

    private void onSuccess() {
        if (state == State.HALF_OPEN) {
            if (halfOpenCalls.incrementAndGet() >= halfOpenMaxCalls) {
                state = State.CLOSED;
                failureCount.set(0);
            }
        } else {
            failureCount.set(0);
        }
    }

    private void onFailure() {
        lastFailureTime = System.currentTimeMillis();
        if (state == State.HALF_OPEN) {
            state = State.OPEN;
        } else if (failureCount.incrementAndGet() >= failureThreshold) {
            state = State.OPEN;
        }
    }

    public State getState() { return state; }
}

使用示例:

1
2
3
4
5
6
CircuitBreaker cb = new CircuitBreaker(5, 30_000, 3);

String result = cb.execute(
    () -> restTemplate.getForObject("/api/users/1", String.class),
    () -> "{\"name\": \"fallback\"}"
);

生产环境的几个关键参数

参数 推荐值 说明
slidingWindowSize 10~20 太小波动大,太大反应慢
failureRateThreshold 40%~60% 过低会误熔断,过高保护不及时
waitDurationInOpenState 15~60s 取决于下游恢复速度
minimumNumberOfCalls 5~10 避免少量调用触发误判

最佳实践

  1. 每个下游服务独立一个 CircuitBreaker 实例,不要共用——否则一个服务挂了会连累所有服务被熔断
  2. Fallback 必须有实际意义——返回缓存数据、默认值或降级逻辑,不要直接返回 null
  3. 配合监控使用——接入 Prometheus + Grafana,实时观察熔断状态和失败率
  4. 渐进式上线——先在非核心链路灰度,确认参数合理后再推广到核心链路

小结

微服务架构下,没有熔断的调用链就是定时炸弹。Resilience4j 适合大多数 Java 项目的轻量级方案,Sentinel 适合需要复杂流控和动态规则的场景,而手写熔断器则能帮你真正理解原理。

回到开头的故事:我们在用户服务调用链路上加了 Resilience4j 熔断后,即使下游再出问题,上游最多影响 30 秒,之后自动降级到缓存数据,用户几乎无感知。

防御性编程不是悲观主义,而是对用户负责。