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 秒,就能记住整个流程。
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 状态转换的常见问题
最后,我总结几个我在项目中踩过的坑。你遇到了,直接照着改就行。
- 状态震荡:Open 等待时间太短,导致频繁在 Open 和 Half-Open 之间切换。解决办法:增大
waitDurationInOpenState,或者加一个最小等待时间。 - 误判:滑动窗口太小,偶发抖动就触发熔断。解决办法:增大
slidingWindowSize,或者使用基于时间窗口的统计(比如 10 秒内的失败率)。 - Half-Open 试探失败后恢复慢:试探请求失败后,又得等一个完整的
waitDurationInOpenState。解决办法:使用指数退避,第一次等 10 秒,第二次等 20 秒,第三次等 40 秒。
避坑指南:我曾经在一个高并发服务上,把 failureRateThreshold 设成了 30%。结果一次正常的发布上线,因为重启导致的短暂超时,熔断器直接打开了。后来我改成 50%,并且加了 minimumNumberOfCalls(最少请求数)的保护,才解决这个问题。
好了,状态机这部分就讲到这里。记住这张图,记住这三个状态,你的熔断器设计就成功了一半。