状态机设计:熔断器三态转换逻辑与半开状态探活机制
熔断器的核心,说白了就是一个状态机。
我做了这么多年风控系统,见过太多人把熔断器想得太复杂。其实它就三个状态:Closed(关闭)、Open(打开)、Half-Open(半开)。嗯,就这么简单。但真正难的是——这三个状态之间怎么转?转的时机对不对?转错了会出什么后果?
三态转换的核心逻辑
先看一张图,这是我个人习惯用的状态转换图,清晰明了:
Closed → Open:什么时候打开?
Closed 状态下,所有请求正常通过。但系统会默默统计失败率。
我在项目中遇到过一个问题:阈值设得太低。有一次线上业务波动,失败率短暂冲到 15%,结果熔断器直接打开了。其实那只是网络抖动,根本不需要熔断。后来我学乖了——阈值要结合业务容忍度来设。
核心参数:
- 失败率阈值:比如 50%,超过就熔断
- 滑动窗口大小:统计最近 N 秒的数据
- 最小请求数:请求太少时不触发熔断,避免误判
举个例子:
// 伪代码:判断是否要熔断
func shouldTrip(stats *WindowStats) bool {
// 请求太少,不触发
if stats.TotalRequests < minRequestThreshold {
return false
}
// 失败率超过阈值,触发
return stats.FailureRate >= failureRateThreshold
}
你想想看,如果最小请求数设成 1,那一个请求失败就直接熔断了——这显然不合理。我建议最小请求数至少设到 10 以上。
Open → Half-Open:什么时候半开?
Open 状态下,所有请求直接拒绝,返回快速失败。但总不能一直拒绝下去吧?
所以需要一个超时时间。比如 30 秒后,自动进入 Half-Open 状态。
我的经验:
超时时间不要设成固定值。我曾经踩过坑——固定 30 秒,结果下游服务恢复需要 40 秒,那 10 秒的窗口期所有请求都白探活了。后来我改成指数退避:第一次 10 秒,第二次 20 秒,第三次 40 秒……
// 伪代码:计算下次探活时间
func nextRetryTimeout(retryCount int) time.Duration {
baseTimeout := 10 * time.Second
return baseTimeout * time.Duration(1 << uint(retryCount))
}
Half-Open 探活机制:最关键的环节
Half-Open 是熔断器最精妙的设计。说白了,就是放一个探针进去试试水。
这个状态只允许有限数量的请求通过。比如只放 3 个请求过去。如果都成功了,就切回 Closed;如果有一个失败,立刻回到 Open。
注意:
探活请求的数量一定要控制好。我曾经见过一个系统,半开状态下放了 100 个请求过去——结果下游服务还没恢复,直接被打挂了。这叫「探活风暴」。建议探活数不超过 5 个。
探活的具体实现:
// 伪代码:半开状态探活逻辑
type CircuitBreaker struct {
state State
halfOpenCount int // 已放行的探活请求数
maxHalfOpenReqs int // 最大探活请求数
successCount int // 探活成功数
requiredSuccess int // 需要连续成功数
}
func (cb *CircuitBreaker) allowRequest() bool {
switch cb.state {
case Closed:
return true
case Open:
return false
case HalfOpen:
// 探活请求数没超,就放行
if cb.halfOpenCount < cb.maxHalfOpenReqs {
cb.halfOpenCount++
return true
}
return false
}
return false
}
func (cb *CircuitBreaker) onRequestComplete(success bool) {
if cb.state != HalfOpen {
return
}
if success {
cb.successCount++
// 连续成功达到阈值,切回 Closed
if cb.successCount >= cb.requiredSuccess {
cb.resetToClosed()
}
} else {
// 任何一个探活失败,立刻回到 Open
cb.resetToOpen()
}
}
状态转换的完整流程
| 当前状态 | 触发条件 | 下一状态 | 说明 |
|---|---|---|---|
| Closed | 失败率 >= 阈值 | Open | 开始拒绝请求 |
| Open | 超时时间到 | Half-Open | 允许少量探活请求 |
| Half-Open | 探活全部成功 | Closed | 恢复正常 |
| Half-Open | 探活有失败 | Open | 继续熔断 |
避坑指南
我曾经踩过几个坑,分享给你:
- 状态转换的原子性:多线程环境下,状态转换必须加锁。我见过一个系统,两个线程同时判断「应该切到 Open」,结果状态被覆盖了。用 CAS 或者锁来保证。
- 探活请求的超时:探活请求本身也要设超时。如果下游服务一直不返回,探活请求会卡住,导致探活数被耗尽。
- 不要用全局状态:每个业务线、每个接口最好有独立的熔断器。我见过有人用一个全局熔断器,结果一个接口挂了,整个系统都熔断了——那叫「熔断雪崩」。
总结一下:
熔断器的三态转换,核心就三句话:
- Closed 下失败多了 → Open
- Open 等一会儿 → Half-Open
- Half-Open 探活成功 → Closed,失败 → Open
记住这个循环,熔断器你就掌握了 80%。剩下的 20% 是参数调优和边界处理。