熔断器模式(Circuit Breaker)

2019 年某电商平台的促销活动中,商品详情页因为依赖的推荐服务响应变慢,开始堆积请求。堆积的请求占满了线程池,线程池耗尽导致整个服务无响应。更糟糕的是,商品服务还依赖商品服务本身用于数据校验——商品服务自身的故障,通过调用链路传播回自己。最终,这场原本只需要隔离推荐服务的故障,变成了一场全站宕机。

这就是分布式系统最经典的噩梦:一个服务的故障,是如何拖垮整个系统的?

熔断器模式的核心思想是:不要让故障传播,不要让请求堆积,不要让资源耗尽。 当检测到依赖服务不可用时,熔断器「跳闸」,快速失败,阻止故障蔓延。

熔断器的灵感来自电路的保险丝。当电流过载时,保险丝熔断,切断电路,保护电器不被烧毁。熔断器模式用同样的思路保护系统:检测到故障时,快速熔断,阻止故障传播。

熔断器的三种状态

熔断器有三种状态,通过状态转换来控制流量:

关闭状态(Closed):正常情况,熔断器关闭,所有请求都能通过。如果请求失败数量或比例未达到阈值,熔断器保持关闭。

打开状态(Open):当失败次数超过阈值,熔断器「跳闸」,进入打开状态。此时所有请求直接失败,不会发送到后端服务。

半开状态(Half-Open):经过一段冷却时间后,熔断器进入半开状态,允许部分请求通过。如果这些请求成功,说明后端服务已恢复,熔断器关闭;如果请求继续失败,熔断器重新打开。

stateDiagram-v2
    [*] --> Closed : 初始状态
    Closed --> Open : 失败次数 > 阈值
    Open --> HalfOpen : 冷却时间到期
    HalfOpen --> Closed : 请求成功
    HalfOpen --> Open : 请求失败
    Open --> [*] : 强制重置

熔断器的工作原理

状态转换逻辑

熔断器的状态转换由以下参数控制:

参数说明默认值建议
滑动窗口大小统计失败次数的时间窗口10 秒
失败阈值在滑动窗口内,超过此比例或次数则跳闸50% 或 5 次
打开状态持续时间熔断器打开后,等待多久进入半开状态60 秒
半开允许请求数半开状态下允许通过的请求数3 个

失败率计算

CircuitBreakerMetrics.java
public class CircuitBreakerMetrics {

    private final int slidingWindowSize;
    private final int failureThreshold;
    private final Double failureRateThreshold;

    private final AtomicInteger totalRequests = new AtomicInteger(0);
    private final AtomicInteger failedRequests = new AtomicInteger(0);
    private final Queue<Long> timestamps = new ConcurrentLinkedQueue<>();

    public void recordSuccess() {
        totalRequests.incrementAndGet();
        timestamps.offer(System.currentTimeMillis());
        cleanupOldTimestamps();
    }

    public void recordFailure() {
        totalRequests.incrementAndGet();
        failedRequests.incrementAndGet();
        timestamps.offer(System.currentTimeMillis());
        cleanupOldTimestamps();
    }

    public double getFailureRate() {
        if (totalRequests.get() == 0) {
            return 0.0;
        }
        return (double) failedRequests.get() / totalRequests.get();
    }

    public boolean shouldTrip() {
        if (failureRateThreshold != null) {
            return getFailureRate() >= failureRateThreshold;
        }
        return failedRequests.get() >= failureThreshold;
    }

    private void cleanupOldTimestamps() {
        long cutoff = System.currentTimeMillis() - slidingWindowSize * 1000;
        while (timestamps.peek() != null && timestamps.peek() < cutoff) {
            timestamps.poll();
        }
    }
}

Resilience4j 熔断器实战

Resilience4j 是目前 Java 生态最流行的熔断器库,轻量级、可组合、功能丰富。相比已经停止维护的 Hystrix,Resilience4j 是更好的选择。

基础配置

application.yml
resilience4j:
  circuitbreaker:
    instances:
      userService:
        # 滑动窗口配置
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10
        # 失败率阈值
        failure-rate-threshold: 50
        # 熔断器打开后的等待时间
        wait-duration-in-open-state: 60s
        # 半开状态允许的请求数
        permitted-number-of-calls-in-half-open-state: 3
        # 慢调用阈值
        slow-call-duration-threshold: 2s
        slow-call-rate-threshold: 80
        # 自动从打开状态转为半开
        automatic-transition-from-open-to-half-open-enabled: true

代码使用

UserServiceCircuitBreaker.java
@Service
public class UserServiceCircuitBreaker {

    private final CircuitBreakerRegistry registry;
    private final UserFeignClient userFeignClient;

    public UserServiceCircuitBreaker(CircuitBreakerRegistry registry,
                                    UserFeignClient userFeignClient) {
        this.registry = registry;
        this.userFeignClient = userFeignClient;
    }

    public User getUser(Long userId) {
        CircuitBreaker circuitBreaker = registry.circuitBreaker("userService");

        // 注册事件监听器
        circuitBreaker.getEventPublisher()
            .onStateTransition(event ->
                log.info("熔断器状态变化: {} -> {}",
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState()))
            .onFailureRateExceeded(event ->
                log.warn("熔断器失败率超标: {}", event.getFailureRate()));

        // 使用熔断器包装调用
        return Decorators.ofSupplier(() -> userFeignClient.getUser(userId))
            .withCircuitBreaker(circuitBreaker)
            .withFallback(List.of(Exception.class),
                e -> User.defaultUser(userId))
            .decorate()
            .get();
    }
}

异步调用支持

AsyncCircuitBreaker.java
@Service
public class AsyncCircuitBreaker {

    private final CircuitBreakerRegistry registry;

    public CompletableFuture<User> getUserAsync(Long userId) {
        CircuitBreaker circuitBreaker = registry.circuitBreaker("userService");

        return Decorators.ofCallable(() -> {
                return userFeignClient.getUserAsync(userId);
            })
            .withCircuitBreaker(circuitBreaker)
            .withFallback(List.of(Exception.class),
                e -> CompletableFuture.completedFuture(User.defaultUser(userId)))
            .decorate()
            .get();
    }
}

熔断器 vs 重试 vs 超时

这三个模式经常一起使用,但作用不同:

模式作用使用场景
超时防止无限等待所有远程调用都应该设置超时
重试处理瞬时故障网络抖动、临时不可用
熔断器防止故障传播依赖服务持续不可用

三者配合使用

ResiliencePattern.java
@Service
public class ResiliencePattern {

    private final CircuitBreakerRegistry circuitBreakerRegistry;

    public Result callService() {
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("backendService");
        Retry retry = Retry.of("backendService", RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.ofMillis(500))
            .retryExceptions(Exception.class)
            .intervalFunction(IntervalFunction.ofExponentialBackoff(500, 2))
            .build());

        return Decorators.ofSupplier(() -> backendService.call())
            .withRetry(retry)
            .withCircuitBreaker(circuitBreaker)
            .withTimeout(Duration.ofSeconds(3))
            .withFallback(List.of(Exception.class),
                e -> Result.fallback())
            .decorate()
            .get();
    }
}

为什么需要三个一起用?

  • 超时是基础保障,防止请求无限等待
  • 重试处理瞬时故障,比如网络抖动时重试一下就好了
  • 熔断器处理持续故障,当服务真的挂了,快速失败,不要白白消耗资源

熔断器与舱壁隔离的配合

熔断器保护的是「调用链」,舱壁模式(Bulkhead)保护的是「资源池」。两者配合使用,可以实现更全面的容错保护。

flowchart TB
    subgraph Caller["调用方"]
        ThreadPool[线程池 A]
    end

    subgraph Bulkhead["舱壁隔离"]
        Pool1[独立线程池 1]
        Pool2[独立线程池 2]
        Pool3[独立线程池 3]
    end

    subgraph CB["熔断器"]
        CB1[熔断器 1]
        CB2[熔断器 2]
        CB3[熔断器 3]
    end

    subgraph Services["后端服务"]
        S1[服务 1]
        S2[服务 2]
        S3[服务 3]
    end

    ThreadPool --> Pool1 & Pool2 & Pool3
    Pool1 --> CB1
    Pool2 --> CB2
    Pool3 --> CB3
    CB1 --> S1
    CB2 --> S2
    CB3 --> S3

舱壁 + 熔断器配置

application.yml
resilience4j:
  bulkhead:
    instances:
      userService:
        maxConcurrentCalls: 10
        maxWaitDuration: 100ms
  circuitbreaker:
    instances:
      userService:
        sliding-window-size: 10
        failure-rate-threshold: 50
BulkheadWithCircuitBreaker.java
public Result callService() {
    Bulkhead bulkhead = Bulkhead.of("userService",
        BulkheadConfig.custom().maxConcurrentCalls(10).build());
    CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("userService");

    return Decorators.ofSupplier(() -> backendService.call())
        .withBulkhead(bulkhead)
        .withCircuitBreaker(circuitBreaker)
        .withFallback(List.of(BulkheadFullException.class),
            e -> Result.degraded())
        .withFallback(List.of(Exception.class),
            e -> Result.fallback())
        .decorate()
        .get();
}

常见问题与反模式

熔断器阈值设置不当

阈值太高,熔断器反应迟钝,故障期间大量请求失败;阈值太低,熔断器频繁跳闸,正常波动也会触发熔断。

正确做法:根据业务容忍度和历史数据调整阈值。初期可以设高一些,观察一段时间后下调到合理值。

所有服务共用一个熔断器

把所有依赖服务的调用放在同一个熔断器里,一个服务故障会导致所有服务都熔断。

正确做法:每个依赖服务有独立的熔断器。user-service 的熔断器跳闸,不影响 order-service 的调用。

熔断器状态不监控

熔断器打开了,但没人知道,直到用户开始大量投诉。

正确做法:监控熔断器状态变化,将状态变化纳入告警。最好把熔断器状态暴露在监控面板上。

忽略了降级逻辑

熔断器打开后返回什么?如果没有好的降级逻辑,可能会返回空数据或者抛出异常,反而造成二次故障。

正确做法:提前设计好降级方案。熔断器打开时,返回缓存数据、默认值或友好的错误提示。

适用场景

应该使用熔断器

  • 调用外部服务(HTTP、RPC、数据库)
  • 调用链路复杂,存在故障传播风险
  • 服务 SLA 要求高,需要快速失败而不是超时等待

暂不需要熔断器

  • 单体应用,内部调用不需要熔断器
  • 调用链简单清晰,故障影响范围可控
  • 超时已经足够处理故障场景

熔断器是微服务架构的必备容错机制。但熔断器只是手段,不是目的。真正重要的是:在熔断器打开时,你的后备方案是什么?