跳到正文

熔断降级与限流 ​

一句话:限流是「我只接这么多」,熔断是「下游坏了我先不调」;两者都用滑动窗口统计,用拒绝一部分请求换整个系统不被拖死。

适用版本:Sentinel 1.8.x + Spring Cloud Alibaba 2023.0.x(sentinel-spring-cloud-starter 对应版本以官方为准)。Hystrix 已停止开发,仅作对照。

为什么需要 ​

一个下游变慢,调用方的线程/连接会被占满;线程池满后,连不依赖该下游的接口也开始超时。更糟的是重试:一次超时变成三次请求,下游更慢,形成正反馈。

限流和熔断分别在两个位置截断这个循环:

  • 限流在入口/调用方控制速率,避免把自己或下游打满。
  • 熔断在下游持续失败时快速失败,不再等待超时。

核心机制 ​

滑动窗口:统计先要准 ​

限流和熔断的第一步都是「最近一段时间成功多少、失败多少、多慢」。固定窗口在边界有突刺:前 0.9s 没流量,最后 0.1s 来 200 个请求,计数器从 0 开始,全部放行。

Sentinel 用滑动窗口(LeapArray):默认 1s 统计周期切成 2 个桶(sampleCount=2),每 500ms 一个桶;读最近 N 个桶的累计值。桶越多越精确,内存和计算也越多。

Slot Chain:一次资源调用的固定顺序 ​

九个 Slot 是一条顺序固定的链:前四个只负责把数统计出来,后五个依次做判断。

StatisticSlot 记录通过/拒绝/异常/RT,FlowSlot 判断流控,DegradeSlot 判断熔断;任一拒绝就抛 BlockException,由 blockHandler 处理。

这个顺序意味着:流控和熔断共享同一份统计数据,但判断是分开的。

熔断状态机 ​

熔断只有三个状态,关键在于「半开只放探测请求」:打开后不是定时自动恢复,而是靠一次真实请求的结果决定回 CLOSED 还是弹回 OPEN。

Sentinel 1.8.x 的熔断规则有三种:

慢调用比例 grade=0(count 最大 RT,slowRatioThreshold 比例)、异常比例 grade=1(count 0–1)、异常数 grade=2(count 分钟异常数)。

旧代码里的 DEGRADE_GRADE_RT 在 1.8 已不推荐,别再把它当成「RT 阈值」的唯一写法。

隔离:Sentinel 没有线程池隔离 ​

Hystrix 默认把每个依赖放进独立线程池,慢依赖只耗尽自己的池;Sentinel 不做这件事。Sentinel 的「线程数限流」统计的是当前资源占用的线程数,超过阈值就拒绝,请求仍在调用方线程上执行。

所以:

需要「一个慢依赖不影响其他依赖」的舱壁隔离,要用独立线程池/信号量(Resilience4j Bulkhead 或自己包一层);Sentinel 的强项是规则、控制台、滑动窗口统计和系统自适应保护,不是执行隔离。说「Sentinel 用线程池/信号量隔离」是不准确的,它用的是统计 + 拒绝。

系统自适应保护 ​

不是限某个接口,而是保护整个实例:当 Load、CPU、平均 RT、并发线程数超过阈值时,按策略拒绝部分入口流量。它是最后一道防线——业务规则配错了,系统规则兜底。阈值要基于压测,不要拍脑袋。

实战场景 ​

一、重启后限流失效:规则住在 Dashboard 的内存里 ​

现象:createOrder 配了 100 QPS 流控,运行了两周一直生效。某天滚动发布后流量翻倍,限流完全没有拒绝,下游库存服务被打到 P99 8 秒;回看 Dashboard,规则列表是空的。

排查:规则丢失只可能是来源问题,不是算法问题。

定位层取证据判定
规则来源FlowRuleManager.getRules() 打印,或 curl localhost:8719/getRules?type=flow(Sentinel 命令端口)空列表 → 没有加载到任何规则
谁写的规则Dashboard 上的规则是临时推送,默认不落盘只在控制台点过 → 重启即丢
加载时机启动日志里 dynamic rule loaded, size=N没有这行 → 数据源没生效
数据源配置spring.cloud.sentinel.datasource.*.nacos 的 rule-type、data-idrule-type 漏写 → 拉到了但不解析
资源名一致性规则里的 resource 与 @SentinelResource(value=...)/URL 资源名字不同 → 规则在但永不命中
实例数变化单机阈值 × 实例数是否已超下游容量扩容后总阈值漂移,看似“限流失效”

本例命中第 2 行与第 6 行:规则只在控制台配过,没有配置中心数据源;同时上周从 4 实例扩到 8 实例,单机 100 QPS 的隐含总量从 400 变成 800。

对症做法:

yaml
# 1. 规则放 Nacos,应用启动主动拉取;控制台改规则要写回配置中心
spring:
  cloud:
    sentinel:
      datasource:
        flow:
          nacos:
            server-addr: ${NACOS_ADDR}
            data-id: order-service-flow-rules
            group-id: SENTINEL_GROUP
            rule-type: flow
        degrade:
          nacos:
            server-addr: ${NACOS_ADDR}
            data-id: order-service-degrade-rules
            group-id: SENTINEL_GROUP
            rule-type: degrade
      eager: true          # 启动即初始化,不等第一个请求才注册资源
java
// 2. 规则为空就拒绝上线:把“没有规则”变成启动失败,而不是静默裸奔
@PostConstruct void assertRulesLoaded() {
    if (FlowRuleManager.getRules().isEmpty()) {
        throw new IllegalStateException("sentinel flow rules empty: datasource not ready");
    }
}

// 3. 单机阈值随实例数变化的问题,用集群流控或按“总量/实例数”推导并留档

eager: true 常被忽略:懒加载下资源在第一个请求到达时才注册,控制台看不到资源,规则也就无从命中。

复盘要点:治理能力的“配置存在”不等于“运行时生效”。把三件事做成指标并告警:规则条数(与配置中心比对,变少即告警)、block 计数(长期为 0 说明规则没命中)、半开与打开次数。规则变更走发布流程并带审计,Dashboard 只作为观察入口,见 Nacos 与 Sentinel 的规则持久化。

二、熔断打开后不再闭合 ​

现象:下游风控服务抖动 1 分钟,risk.check 资源熔断打开。风控 20 秒后已完全恢复,但网关侧持续返回降级结果长达 25 分钟,人工把规则删掉才恢复。

排查:闭合依赖“半开时探测成功”,探测本身可能被同样的慢拖住。

定位层取证据判定
熔断时长DegradeRule.timeWindow 实际值设成 300s 就会“看起来一直没恢复”
是否进入半开Dashboard 状态曲线 / metrics 里 block 与 pass 的变化一直全 block → 从未进半开
探测请求结果半开窗口里那条请求的下游 RT探测本身超时 → 立刻弹回 OPEN 并重新计时
统计门槛minRequestAmount=20、statIntervalMs=10000降级期间流量极低,凑不满 20 次请求,比例永远算不出来
规则类型grade=0 慢调用比例 / 1 异常比例 / 2 异常数慢调用比例配成 count 是 RT 阈值,不是时间
下游真实状态风控服务自身的 RT 与错误率下游仍未恢复 → 熔断正确,问题在别处

本例的链条是:降级把流量抽走 → 半开窗口里唯一一次探测正好赶上下游 GC(2s)→ 探测失败重新计时 → 熔断反复延长,形成“越降级越难恢复”。

对症做法:

java
DegradeRule r = new DegradeRule("risk.check");
r.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
r.setCount(0.5);
r.setStatIntervalMs(10_000);
r.setMinRequestAmount(5);        // 降级期流量少,门槛要能被满足(经验值,需校准)
r.setTimeWindow(15);             // 15s 后半开探测,而不是 300s
// 半开恢复模式在部分版本可配(DegradeRule 的恢复相关字段),启用前先核对当前版本源码
DegradeRuleManager.loadRules(List.of(r));
java
// 半开探测要有独立超时与短路:探测请求 300ms 无响应即视为失败,不拖长恢复窗口
@SentinelResource(value = "risk.check", blockHandler = "riskFallback")
public RiskResult check(RiskReq req) {
    return riskClient.check(req);     // 该 client 的 readTimeout 独立配成 300ms
}
public RiskResult riskFallback(RiskReq req, BlockException e) {
    metrics.blocked("risk.check", e.getClass().getSimpleName());
    return RiskResult.conservative(req);   // 降级要给出可用的保守结果
}

降级结果本身要设计:“保守通过 + 事后复核”还是“拒绝 + 提示稍后”,取决于业务能不能容忍误放。熔断只负责止损,下游的 RT 问题仍需修复。

复盘要点:恢复路径必须被演练,而不是等真实故障验证。演练口径:注入下游 30% 错误率 60 秒 → 观察熔断打开 → 撤销注入 → 断言在 timeWindow + 5s 内恢复满流量;不能在预期时间内闭合的规则就是配置错误。规则阈值统一放配置中心,半开探测的独立超时写进客户端配置。

三、以为 Sentinel 做了隔离,结果全站一起慢 ​

现象:报表导出接口偶发 30 秒。给 report.export 配了“线程数限流 = 10”,预期只影响导出接口,实际高峰期商品详情、下单也被拖慢,Tomcat 线程 200 全满。

排查:先确认隔离发生在哪里。

定位层取证据判定
执行线程慢请求与正常请求的线程名是否都是 http-nio-8080-exec-*同一个池 → 没有舱壁,只是统计后拒绝
线程数限流语义RuleConstant.FLOW_GRADE_THREAD 的行为统计当前资源占用线程数,超过就 BlockException;通过的请求仍在调用方线程执行
是否命中阈值block 计数是否上升上升说明规则生效,但请求仍占用容器线程
慢的来源慢请求的栈:JDBC、导出文件生成、下游 RPC30 秒级占用必然来自阻塞操作
资源命名URL 资源与注解资源是否同一资源名字不同 → 配了规则也不统计

结论要说准:Sentinel 的做法是统计 + 拒绝,不是 Hystrix 那种线程池隔离。要“一个慢依赖不影响其他接口”,得自己做舱壁。

对症做法:

java
// 1. 真正的隔离:慢接口走独立有界线程池,容器线程只做提交与超时
private final ThreadPoolExecutor reportPool = new ThreadPoolExecutor(
        8, 8, 60, SECONDS, new ArrayBlockingQueue<>(50),
        new NamedThreadFactory("report"), new AbortPolicy());

public String export(ExportReq req) {
    try {
        return reportPool.submit(() -> doExport(req)).get(20, SECONDS);  // 超时即放弃等待
    } catch (RejectedExecutionException e) {
        return "报表排队已满,请稍后重试";                 // 队列满时给出确定语义
    } catch (TimeoutException e) {
        metrics.timeout("report.export");
        return "报表生成超时";                             // 注意:任务仍在跑,需可取消
    }
}

// 2. 线程数限流继续留着,作为“进入该资源的并发上限”,与独立池形成两层
// 3. 或用 Resilience4j Bulkhead/TimeLimiter,语义更明确(以团队依赖现状选择)

超时之后任务不会自动停止,长任务要有内部取消点(分页导出时每页检查中断标志),否则只是把用户等待变成了后台资源泄漏。

复盘要点:这道题的判据是“线程名”——如果所有接口共用容器线程池,就没有隔离可言。把慢接口(导出、批量、上传)统一走独立线程池写进规范,并给出每个池的容量与队列上限;同时保留 Tomcat 线程数的告警,它是“有没有被别的东西偷走”的直接指标。见 企业级可观测性与故障定位。

最小上手 ​

依赖:

xml
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yaml
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
      eager: true

代码方式配规则(Sentinel 1.8.x API):

java
FlowRule flow = new FlowRule("createOrder");
flow.setGrade(RuleConstant.FLOW_GRADE_QPS);
flow.setCount(100);                         // 100 QPS
FlowRuleManager.loadRules(List.of(flow));

DegradeRule degrade = new DegradeRule("createOrder");
degrade.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
degrade.setCount(0.5);                      // 50% 异常触发
degrade.setTimeWindow(10);                  // 熔断 10s
degrade.setMinRequestAmount(20);            // 至少 20 次请求才统计
degrade.setStatIntervalMs(10_000);          // 统计窗口 10s
DegradeRuleManager.loadRules(List.of(degrade));
java
@SentinelResource(value = "createOrder",
                  blockHandler = "onBlock",
                  fallback = "onFallback")
public Order create(OrderDTO dto) { ... }

public Order onBlock(OrderDTO dto, BlockException e) {
    return Order.rejected("busy");
}

验证:压到 100 QPS 以上,观察 BlockException 计数和 Dashboard 的 block 曲线;把下游人为变慢,确认熔断打开后请求快速失败,而不是等超时。

生产动作 ​

  • 规则持久化:Dashboard 推送的规则默认只在内存,应用重启即丢。生产用 Nacos/Apollo 数据源,启动时主动拉取:
yaml
spring:
  cloud:
    sentinel:
      datasource:
        flow:
          nacos:
            server-addr: 127.0.0.1:8848
            data-id: order-service-flow-rules
            group-id: SENTINEL_GROUP
            rule-type: flow
  • 启动顺序:数据源初始化失败时,应用可能「没有规则」直接上线;把规则拉取失败做成启动失败或显式告警。
  • blockHandler vs fallback:blockHandler 处理 BlockException(限流/熔断),签名要带 BlockException;fallback 处理业务异常,可用 exceptionsToIgnore 排除不该降级的异常。两者别混用。
  • 阈值来源:先压测拿到容量,再按容量的 70%–80% 设阈值;没有基线的阈值只是数字。
  • 集群流控:单机规则在实例数变化时总阈值会漂移;需要全局 QPS 时用集群流控,并接受 Token Server 的可用性成本。
  • 监控:block 比例、熔断打开次数、规则条数与配置中心比对;规则数变少说明加载或持久化断了。

排障路径:

  • 重启后限流失效 → 查规则来源是不是 Dashboard 内存推送 → 改为配置中心数据源。
  • 熔断一直不恢复 → 查 timeWindow、半开探测是否成功、下游是否仍慢 → 不要盲目调大阈值。
  • 规则不生效 → 查资源名是否与 @SentinelResource/URL 资源一致 → 查规则类型(flow/degrade)是否匹配。

边界 ​

  • 不是线程池隔离:需要舱壁隔离时,Sentinel 不能替代独立线程池/信号量。
  • Dashboard 不是生产存储:它适合观察和临时调规则,不适合作为规则唯一来源。
  • 规则粒度是资源:资源名不一致,规则就不生效;URL 资源和注解资源的命名要统一。
  • 阈值没有银弹,也别用它掩盖下游问题:系统自适应需要压测基线;熔断是止损不是修复,下游该扩容/该优化仍要做。

讲清楚 ​

限流和熔断都是「用拒绝换稳定」:限流控制进入系统的速率,熔断在下游持续失败时快速失败。Sentinel 用滑动窗口统计最近 1s 的通过数、异常数、RT,按 Slot Chain 依次判断授权、系统保护、热点参数、流控、熔断,任一不通过就抛 BlockException。它没有 Hystrix 那种线程池隔离,所谓「线程数限流」只是统计当前线程数后拒绝,不会把请求切到独立池子;需要舱壁隔离要另找方案。生产上最关键的不是阈值,而是规则持久化——Dashboard 内存里的规则一重启就没了。

把一门技术讲透到既懂也会用