另一半问题:出去的调用
限流篇解决的是「进来的流量」怎么少接点。但第 1 篇雪崩链里还有另一半:会员服务调用订单服务,订单病了(RT 5 秒),会员的线程一个个阻塞在等待上——上游不是被流量打死的,是被自己「礼貌的等待」拖死的。此时重试是叩得更响,扩容是给病人搬椅子,都不对症。对症的机制是熔断器(Circuit Breaker):发现下游病了,就停止叩门,请求直接快速失败——家里的空气开关早就想明白了这件事,电流异常直接跳闸,断电保电路。
三个状态,一台断路器
熔断器的全部智慧浓缩在三个状态里:
错误率/慢调用超阈值
┌──────────────────────────────┐
│ ▼
[CLOSED 关闭] [OPEN 打开]
▲ │
│ 探测成功 │ 休眠时间到
│ ▼
└──────[HALF-OPEN 半开] ◄────┘
探测失败 ──────► 回到 OPENCLOSED(闭合):正常态,请求全部通过,同时持续统计下游的错误率与慢调用比例——断路器在闭合时也睁着眼。OPEN(打开):统计指标越过阈值,断路器跳闸,之后所有请求立即失败(走降级逻辑),不再发出网络调用——上游线程瞬间解脱,下游获得喘息。HALF-OPEN(半开):跳闸持续一段「休眠时间」后进入试探态,放行少量探测请求;探测成功说明下游恢复,断路器闭合回 CLOSED,全线恢复;探测失败则重新跳闸回 OPEN,继续休眠。
手写一个迷你熔断器
状态机不复杂,核心代码几十行,自己写一个便于理解骨架:
public class MiniBreaker {
enum State { CLOSED, OPEN, HALF_OPEN }
private volatile State state = State.CLOSED;
private final AtomicInteger failures = new AtomicInteger();
private volatile long openedAt = 0;
private static final int THRESHOLD = 50; // 失败 50 次跳闸
private static final long SLEEP_MS = 10_000; // 休眠 10 秒
public boolean allow() {
if (state == State.OPEN) {
if (System.currentTimeMillis() - openedAt > SLEEP_MS) {
state = State.HALF_OPEN; // 休眠到期,进入试探
return true;
}
return false; // 熔断中,快速失败
}
return true;
}
public void record(boolean success) {
if (success) { failures.set(0); state = State.CLOSED; }
else if (failures.incrementAndGet() >= THRESHOLD) {
state = State.OPEN; // 连续失败,跳闸
openedAt = System.currentTimeMillis();
}
}
}真实生产不会用这么简的版本(缺滑动统计窗口、最小请求数、并发控制),下一版框架轮到 Sentinel 与 Resilience4j,但状态机的骨架就是这个三态,框架只是把「怎么判定病了」做得更科学。
熔断保护的是谁
一个常被问反的问题:熔断是保护上游还是下游?答案是都保护,但先保护上游。对上游:跳闸后请求不再发出,线程不再阻塞,上游自己的资源(线程池、连接)不再被拖垮——这是第 1 篇连坐链的直接解药。对下游:叩门声停了,故障中的下游获得喘息空间去恢复(GC 能跑完、连接池能清空、磁盘 IO 能跟上),而不是在愈来愈多的请求里窒息到底。熔断的休眠时间就是下游的 ICU 时间。
与限流、重试的分工
| 机制 | 管什么 | 动作 | 触发依据 |
|---|---|---|---|
| 限流 | 进来的流量 | 主动拒绝超额 | 预设阈值(静态) |
| 熔断 | 出去的调用 | 停止调用下游 | 实时健康指标(动态) |
| 重试 | 瞬时失败 | 再试一次 | 可重试的错误类型 |
三者各司其职且互相配合:重试要尊重熔断状态(Open 时不重试),限流的触发率与熔断的跳闸事件都要进监控。熔断器「怎么判定病了」有一整门学问——错误率、慢调用比例、统计窗口、最小请求数,下一篇把判定标准拆开细讲。
评论 (0)