连载中 11/20

熔断判定:错误率、慢调用与统计窗口

2026-07-12 · 4625 阅读 · 0 评论 · 0 赞

难的不是状态机,是诊断标准

上篇把三态骨架搭了起来:CLOSED 睁眼统计、OPEN 快速失败、HALF-OPEN 放行试探。骨架不难,几十行代码的事,真正决定熔断器好坏的是另一个问题——病了怎么判定。判松了是漏诊,下游已经躺平你还在排队等它;判紧了是误诊,一次网络抖动就把半条业务线跳闸。诊断靠两根体温计:错误率与慢调用比例,再配两道防误判的护栏:最小请求数与滑动统计窗口。

第一根体温计:错误率,但什么算「错」

错误率 = 失败请求数 ÷ 总请求数,公式一眼就会,陷阱全藏在分子的定义里:不是所有异常都算病

系统异常才算病:调用超时、连接被拒、连接池耗尽、HTTP 5xx、gRPC UNAVAILABLE——这些说明下游在系统层面出了问题,是熔断器该管的。业务异常不算病:库存不足、余额不够、参数校验失败——下游「清醒地拒绝了你」,服务本身健康得很,熔断器管不着也不该管。

把业务异常混进分子会怎样?大促零点下单,库存不足的业务失败天然冲高,错误率曲线跟着起飞,全公司的熔断器集体跳闸——故障没来,熔断器先把自己人打瘫了。这是熔断误判的头号事故来源。

public boolean isSystemFailure(Throwable e) {
    return e instanceof TimeoutException          // 调用超时
        || e instanceof ConnectException          // 建连被拒
        || e instanceof PoolExhaustedException    // 连接池耗尽
        || e instanceof HttpServerErrorException; // HTTP 5xx
    // 库存不足、余额不够等业务异常:不算,走业务分支
}

第二根体温计:慢调用

只盯错误率会漏掉最重要的前兆——慢。下游 RT 从 50ms 涨到 800ms,一个错误都没抛,但每个请求占线程的时间翻了 16 倍,同样的线程池吞吐只剩 1/16,第 1 篇的雪崩链已经开始转了。等超时异常大量出现再熔断,上游线程已经被拖死一轮。

所以第二根体温计是慢调用比例:RT 超过阈值的调用占总调用数的比例。它配两个参数:慢调用 RT 阈值(多久算慢)与慢调用比例阈值(多少算病)。RT 阈值别拍脑袋,取平时监控 P99 的 1.5~2 倍——P99 本来就是常态尖峰,持续越过它的变慢才值得动手。

小样本陷阱:最小请求数

凌晨四点,统计窗口里进来 2 个请求,失败 1 个——错误率 50%,按阈值早该跳闸。可这两个请求八成来自同一个拨测任务,样本毫无意义。比率只有在样本够多时才是信号,样本少时是噪声。

护栏叫最小请求数(minRequestAmount):窗口内请求总数不足这个数,不做判定,维持 CLOSED。低峰期宁可漏判几秒,绝不能被一两个请求的巧合触发跳闸——低峰跳闸之后的恢复探测,还会反过来打扰刚缓过来的下游。

滑动统计窗口:指标要看「最近」

错误率在多长的时间范围里算?用「上线至今的累计值」毫无意义——上周那场故障的错误数,不该影响今天的判定。指标必须滚动:只看最近一个窗口,旧数据不断滚出去。

实现直接复用限流篇的环形格子滑动窗口——限流统计流量,熔断统计成败与耗时,同一副骨架两副用法。窗口切 10 个桶、每桶 1 秒:请求落进当前桶,10 秒前的桶滚出窗口。

public class SlideStats {
    static final int BUCKETS = 10;               // 10 个桶
    static final long BUCKET_MS = 1000L;         // 每桶 1 秒,整个窗口 10 秒
    final long[] bucketSec = new long[BUCKETS];  // 每桶归属的秒
    final AtomicLongArray total = new AtomicLongArray(BUCKETS);
    final AtomicLongArray fail  = new AtomicLongArray(BUCKETS);
    final AtomicLongArray slow  = new AtomicLongArray(BUCKETS);

    public synchronized void record(long rt, boolean ok, long slowRt) {
        long sec = System.currentTimeMillis() / BUCKET_MS;
        int idx = (int) (sec % BUCKETS);
        if (bucketSec[idx] != sec) {             // 桶里是 10 秒前的旧数据,清零复用
            bucketSec[idx] = sec;
            total.set(idx, 0); fail.set(idx, 0); slow.set(idx, 0);
        }
        total.incrementAndGet(idx);
        if (!ok) fail.incrementAndGet(idx);
        if (rt > slowRt) slow.incrementAndGet(idx);
    }
    public synchronized boolean tripped(double errLimit, double slowLimit, int minReq) {
        long t = 0, f = 0, s = 0;
        for (int i = 0; i < BUCKETS; i++) { t += total.get(i); f += fail.get(i); s += slow.get(i); }
        if (t < minReq) return false;            // 样本不足,宁可不判
        return f * 1.0 / t >= errLimit           // 错误率超标
            || s * 1.0 / t >= slowLimit;         // 或慢调用比例超标
    }
}

窗口多长合适?太短(一两秒)指标抖得厉害,瞬时尖峰就能骗到它;太长(三五分钟)故障恢复半天了指标还没回神,跳闸迟钝、恢复更迟钝。经验值 10 秒到 1 分钟,配合 1 秒一桶让统计平滑滚动。

半开探测与一张参数表

最后是半开态的细节:休眠到期后放几个探测请求?放 1 个最脆——探测请求恰好撞上一次网络抖动,立刻打回 OPEN,恢复期被无限拉长;放 3~5 个,连续成功才闭合,误伤率低得多。Sentinel 与 Resilience4j 都留了这个参数(permitted-number-of-calls-in-half-open-state)。

把本篇的判定参数汇总成表,调参时对着填:

参数含义经验起点
统计窗口错误率、慢调用比例的统计时间范围10s ~ 60s
分桶粒度窗口切多细,越细统计越平滑1 秒一桶
错误率阈值系统异常占比超过即跳闸30% ~ 50%
慢调用 RT 阈值超过此 RT 记为慢调用平时 P99 的 1.5~2 倍
慢调用比例阈值慢调用占比超过即跳闸50% 左右
最小请求数窗口内样本不足则不判定20 ~ 50
熔断休眠时长OPEN 持续多久进半开5s ~ 30s
半开探测数半开态放行的试探请求数3 ~ 5 个

还有一个容易忽略的事实:熔断判定天然是实例级的——每个节点统计自己看到的成败,不需要全局一致,所以手写版只管单实例并不算缺陷,生产框架也是这么干的。参数齐了,但注解、控制台、动态规则这些生产配套还缺着——下篇把 Sentinel 装进 503 咖啡馆,熔断从手艺变成配置。

503

10 年全栈工程师 · 503咖啡馆主理人

#熔断判定#错误率#慢调用#滑动窗口#最小请求数#参数调优

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞