4、熔断器状态机:Closed、Open、Half-Open 三种状态及转换条件

熔断器的核心,说白了就是一个状态机。它只有三种状态,但能把整个调用链的稳定性管得明明白白。我刚开始接触熔断时,总觉得不就是三个状态来回切嘛,等真在线上跑起来才发现——状态切换的时机和参数,稍微差一点就能让你半夜被电话吵醒。

4.1 三种状态的核心定义

咱们先定个基调,三种状态各自做什么:

  • Closed(关闭):熔断器关闭,正常转发请求。这是系统的默认状态。你想想看,大部分时间它都该待在这儿。
  • Open(打开):熔断器打开,直接拒绝请求(或快速失败)。这时候说明系统出问题了,得赶紧喘口气。
  • Half-Open(半开):熔断器半开,允许少量请求通过,试探一下远端服务有没有恢复。如果成功就切回Closed,失败就再次打开。

嗯,这里要注意:Half-Open 其实是一个“试探窗口”。它不是稳态,而是一个短暂的过渡状态。我记得第一次写熔断器时,把这个状态当成了正常态,结果每过几分钟就让几个请求过去,服务一恢复就被冲垮了。后面会细说为什么。

4.2 状态转换条件

三种状态怎么切?下图一目了然:

熔断器状态机流转图 Closed Open Half-Open 失败计数 ≥ 阈值 超时时间到(如 30 秒) 试探请求成功 试探请求失败 关键转换条件 • Closed → Open: 失败计数(或失败比例)达到阈值,且滑动时间窗口内统计 • Open → Half-Open: 经过预设的超时时间(如 30s),自动进入半开 • Half-Open → Closed / Open: 根据试探请求的结果决定

4.3 关键参数与行为对比

三种状态背后都藏着几个核心参数。我习惯用一张表格把它们列死,上线前逐一确认:

状态 允许请求数 触发条件 退出条件 我踩过的坑
Closed 正常通过 默认 失败率达到阈值(如 50%/10次内) 阈值设太低,偶发失败就熔断,无辜
Open 0(全部拒绝) 失败计数 ≥ 阈值 超时时间到(如 30s) 超时太长,恢复后流量反弹;太短又频繁半开
Half-Open 少量(如 3 个) 超时到达后自动进入 试探全部成功则Closed,任一失败则Open 试探请求数量太多,把刚恢复的服务压垮

核心原则:Closed 状态负责“检测异常”,Open 负责“保护系统”,Half-Open 负责“安全恢复”。三个角色缺一不可,时间窗口和计数才是调优的关键。

4.4 代码骨架:一个简单的状态机实现

我一般不会直接把现成框架的代码贴进来,而是写一个最简思路,帮你理解内核。你看下面这个 Python 伪代码,核心逻辑就几行:

class CircuitBreaker:
    def __init__(self, failure_threshold=5, timeout=30):
        self.state = 'Closed'
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.last_open_time = None

    def call(self, func):
        if self.state == 'Open':
            if time.time() - self.last_open_time > self.timeout:
                self.state = 'Half-Open'
            else:
                raise CircuitBreakerOpenError()

        try:
            result = func()
            if self.state == 'Half-Open':
                self.state = 'Closed'
                self.failure_count = 0
            return result
        except Exception:
            self.failure_count += 1
            if self.state == 'Half-Open':
                self.state = 'Open'
                self.last_open_time = time.time()
            elif self.failure_count >= self.failure_threshold:
                self.state = 'Open'
                self.last_open_time = time.time()
            raise

个人经验:在实际项目中,失败计数往往不是简单的累加,而是滑动时间窗口内的统计(比如最近30秒内的失败次数)。否则某次突发流量后失败计数一直不衰减,服务恢复了还开着熔断,反而影响可用性。

4.5 避坑指南:状态机切换中的三个常见误区

  1. 误区一:Open 状态直接返回错误就行
    我曾经在生产环境也这么干,结果客户端没做重试和降级,所有请求直接抛异常,前端页面全白。说白了,Open 状态下应该用快速失败 + 降级数据,而不是抛异常。
  2. 误区二:Half-Open 试探请求要尽量多
    有个同事把试探数设为 10,结果后端刚恢复 20% 的容量,10 个请求打过去又把服务打垮了。我建议试探请求数控制在 1~3 个,并且优先用关键探活接口。
  3. 误区三:超时时间设个固定值就行
    你想想看,如果远端服务需要 60 秒才能恢复,你却只给了 15 秒的超时,那每次半开都会失败,熔断器会反复在 Open 和 Half-Open 之间抖。最好根据业务特性动态调整,或者干脆用指数退避。

警告:状态机千万别做成“读-写-读”分离的分布式状态,比如两个线程同时修改 state 变量。我曾见过一个项目用 Redis 存熔断器状态,结果网络抖动导致状态被复写,半个集群都挂了。熔断器状态最好保持在本地进程内存中,通过心跳或分布式锁同步决策结果即可。

4.6 配置建议:三个核心参数的初始值

我习惯把参数分为三级:基础、中级、高级。新人可以先按基础配,跑起来再看监控调整:

参数 基础值 说明
失败阈值 5 连续失败 5 次就熔断(滑动窗口建议 30 秒)
超时时间 30 秒 Open 状态持续 30 秒后进入 Half-Open
半开试探数 3 Half-Open 期间最多允许 3 个请求通过

当然,这个基础值不是死规矩。我记得有一次线上密钥服务的某个下游依赖偶尔会慢 5 秒,5 次失败阈值根本防不住,后来改成 50% 失败比例 + 30 秒滑动窗口才稳定下来。你自己调的时候,务必结合业务的 TP99 和 SLA 来定。

最后说一句:状态机本身不难,难的是参数调优和异常场景的覆盖。你只要把三种状态和切换条件刻在脑子里,熔断这块就算拿下了。