2. 核心状态机:Closed、Open、Half-Open 状态详解与状态转换图

熔断器的核心,说白了就是一个三态状态机。我见过不少团队把熔断器当成简单的开关来用,结果线上出了大问题。你想想看,如果只有开和关,那恢复机制怎么设计?所以,Closed、Open、Half-Open 这三个状态,缺一不可。

2.1 状态定义与行为

咱们一个一个来看。每个状态我都结合项目里的实际场景讲,这样你理解起来更直观。

2.1.1 Closed 状态(闭合状态)

这是熔断器的默认状态。说白了就是一切正常,请求照常通过。但注意,Closed 不等于什么都不做。它内部在默默统计失败率。

  • 行为:所有请求正常转发到下游服务
  • 统计:记录最近 N 个请求的成功/失败次数
  • 阈值:当失败率达到设定阈值(比如 50%),触发状态转换

核心参数slidingWindowSize(滑动窗口大小)和 failureRateThreshold(失败率阈值)。

我在项目中遇到过一个问题:滑动窗口设得太小,比如只有 10 个请求。结果一次短暂的网络抖动,10 个请求里失败了 6 个,熔断器直接跳到了 Open 状态。其实下游服务根本没挂,只是偶发超时。嗯,这里要注意,窗口大小要结合你的 QPS 来定。QPS 高的服务,窗口可以设小一点;QPS 低的,窗口得大一些,否则容易误判。

2.1.2 Open 状态(断开状态)

当 Closed 状态下失败率超标,熔断器就切到 Open 状态。这时候,所有请求直接快速失败,不会发往下游。为什么要这么做?

  • 保护下游:防止雪崩。下游已经扛不住了,你再发请求就是火上浇油。
  • 快速失败:客户端不用等超时,直接拿到错误响应,可以走降级逻辑。
  • 节省资源:线程池、连接池不会被耗尽。

Open 状态会持续一段时间,这个时间由 waitDurationInOpenState 控制。我建议这个值不要设得太短,比如 5 秒。我曾经见过一个案例,waitDuration 设了 3 秒,结果下游还没恢复,熔断器就切到 Half-Open,然后又被打了回去。来回震荡,服务一直半死不活。

避坑指南:我曾经把 waitDurationInOpenState 设成 10 秒,但下游是一个慢 SQL 导致的故障,恢复需要 30 秒。结果熔断器在 10 秒后切到 Half-Open,又失败了。后来我改成 waitDuration = 2 * 预估恢复时间,才稳定下来。

2.1.3 Half-Open 状态(半开状态)

这是最巧妙的一个状态。Open 状态结束后,熔断器不会直接回到 Closed,而是先进入 Half-Open。它只放行少量请求,去试探下游是否恢复。

  • 试探请求数:由 permittedNumberOfCallsInHalfOpenState 控制,通常设 3-5 个。
  • 判定逻辑:如果试探请求全部成功,切回 Closed;如果有一个失败,回到 Open。

你想想看,如果没有 Half-Open,直接从 Open 切到 Closed,万一下游还没恢复,那瞬间涌入的请求又会把它打垮。Half-Open 就像一个侦察兵,先派几个人去看看情况。

个人经验:我习惯把 Half-Open 的试探请求数设成 3。为什么是 3?因为 1 个太容易误判,5 个又太多。3 个刚好,既能有统计意义,又不会给下游太大压力。

2.2 状态转换图

下面这张图,是我用 SVG 画的。它清晰地展示了三个状态之间的转换关系。你盯着看 10 秒,就能记住整个流程。

Closed Open Half-Open 失败率超阈值 等待时间结束 试探请求全部成功 试探请求有失败 图例: 失败转换 成功转换 超时转换

2.3 状态转换的关键参数

光知道状态还不够,你得知道怎么调参。我整理了一张表,把每个状态相关的核心参数列出来。这张表我建议你收藏,线上调优时直接翻出来看。

状态 参数名 说明 我建议的初始值
Closed slidingWindowSize 滑动窗口大小,统计最近多少个请求 100(高QPS) / 20(低QPS)
failureRateThreshold 失败率阈值,超过则触发 Open 50%
Open waitDurationInOpenState 在 Open 状态停留的时间 30 秒(根据下游恢复时间调整)
Half-Open permittedNumberOfCallsInHalfOpenState 允许的试探请求数 3
halfOpenMaxFailureRate Half-Open 状态下的失败率阈值(部分实现支持) 50%

核心原则:参数不是一成不变的。我建议你上线前先做一次压力测试,看看下游的恢复时间曲线,再决定 waitDurationInOpenState 的值。别拍脑袋设个 10 秒,然后线上出问题再改。

2.4 状态转换的代码示例

下面是一个简化版的状态机实现。我用的是 Java 伪代码,但逻辑在任何语言里都一样。你注意看状态转换的条件判断。

public class CircuitBreaker {
    private State state = State.CLOSED;
    private int failureCount = 0;
    private int requestCount = 0;
    private long openTime = 0;
    
    // Closed 状态下的阈值
    private static final int FAILURE_THRESHOLD = 5;
    private static final int REQUEST_THRESHOLD = 10;
    // Open 状态等待时间
    private static final long WAIT_DURATION = 30000; // 30秒
    
    public boolean allowRequest() {
        switch (state) {
            case CLOSED:
                return true;
            case OPEN:
                // 检查是否过了等待时间
                if (System.currentTimeMillis() - openTime > WAIT_DURATION) {
                    state = State.HALF_OPEN;
                    return true; // 放行试探请求
                }
                return false;
            case HALF_OPEN:
                // 只放行有限数量的请求
                return requestCount < 3; // 最多放行3个
            default:
                return false;
        }
    }
    
    public void recordFailure() {
        failureCount++;
        requestCount++;
        
        if (state == State.HALF_OPEN) {
            // 试探请求失败,回到 Open
            state = State.OPEN;
            openTime = System.currentTimeMillis();
            resetCounts();
        } else if (state == State.CLOSED) {
            // 检查是否达到阈值
            if (failureCount >= FAILURE_THRESHOLD && 
                requestCount >= REQUEST_THRESHOLD) {
                state = State.OPEN;
                openTime = System.currentTimeMillis();
                resetCounts();
            }
        }
    }
    
    public void recordSuccess() {
        requestCount++;
        
        if (state == State.HALF_OPEN) {
            // 试探请求成功,回到 Closed
            state = State.CLOSED;
            resetCounts();
        }
    }
}

这段代码我简化了很多,但核心逻辑都在。你注意看 recordFailure() 方法里的判断:Half-Open 状态下只要有一个失败,立刻回到 Open。这是为了防止试探请求把下游再次打垮。

个人习惯:我在生产环境里,会把 Half-Open 的试探请求数设成 3,并且每个试探请求之间加一个 200ms 的间隔。为什么?因为如果 3 个请求同时发出去,下游可能瞬间又扛不住了。加个间隔,给下游一点喘息的时间。

2.5 状态转换的常见问题

最后,我总结几个我在项目中踩过的坑。你遇到了,直接照着改就行。

  1. 状态震荡:Open 等待时间太短,导致频繁在 Open 和 Half-Open 之间切换。解决办法:增大 waitDurationInOpenState,或者加一个最小等待时间。
  2. 误判:滑动窗口太小,偶发抖动就触发熔断。解决办法:增大 slidingWindowSize,或者使用基于时间窗口的统计(比如 10 秒内的失败率)。
  3. Half-Open 试探失败后恢复慢:试探请求失败后,又得等一个完整的 waitDurationInOpenState。解决办法:使用指数退避,第一次等 10 秒,第二次等 20 秒,第三次等 40 秒。

避坑指南:我曾经在一个高并发服务上,把 failureRateThreshold 设成了 30%。结果一次正常的发布上线,因为重启导致的短暂超时,熔断器直接打开了。后来我改成 50%,并且加了 minimumNumberOfCalls(最少请求数)的保护,才解决这个问题。

好了,状态机这部分就讲到这里。记住这张图,记住这三个状态,你的熔断器设计就成功了一半。