4、熔断器状态机:Closed、Open、Half-Open 三种状态及转换条件
熔断器的核心,说白了就是一个状态机。它只有三种状态,但能把整个调用链的稳定性管得明明白白。我刚开始接触熔断时,总觉得不就是三个状态来回切嘛,等真在线上跑起来才发现——状态切换的时机和参数,稍微差一点就能让你半夜被电话吵醒。
4.1 三种状态的核心定义
咱们先定个基调,三种状态各自做什么:
- Closed(关闭):熔断器关闭,正常转发请求。这是系统的默认状态。你想想看,大部分时间它都该待在这儿。
- Open(打开):熔断器打开,直接拒绝请求(或快速失败)。这时候说明系统出问题了,得赶紧喘口气。
- Half-Open(半开):熔断器半开,允许少量请求通过,试探一下远端服务有没有恢复。如果成功就切回Closed,失败就再次打开。
嗯,这里要注意:Half-Open 其实是一个“试探窗口”。它不是稳态,而是一个短暂的过渡状态。我记得第一次写熔断器时,把这个状态当成了正常态,结果每过几分钟就让几个请求过去,服务一恢复就被冲垮了。后面会细说为什么。
4.2 状态转换条件
三种状态怎么切?下图一目了然:
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 避坑指南:状态机切换中的三个常见误区
- 误区一:Open 状态直接返回错误就行
我曾经在生产环境也这么干,结果客户端没做重试和降级,所有请求直接抛异常,前端页面全白。说白了,Open 状态下应该用快速失败 + 降级数据,而不是抛异常。 - 误区二:Half-Open 试探请求要尽量多
有个同事把试探数设为 10,结果后端刚恢复 20% 的容量,10 个请求打过去又把服务打垮了。我建议试探请求数控制在 1~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 来定。
最后说一句:状态机本身不难,难的是参数调优和异常场景的覆盖。你只要把三种状态和切换条件刻在脑子里,熔断这块就算拿下了。