连载中 11/20

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

2026-07-12 · 4628 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞