限流、熔断、降级,你真的分得清吗?

★

推荐阅读人群 :Sentinel 只连过控制台、规则重启就丢、Nacos 只拿来存配置文件的后端

一句话导读 :这篇不重复"限流熔断降级是什么",讲它在 Sentinel + Nacos 里怎么落地:一条请求穿过哪几个 Slot、滑动窗口怎么记账、规则从哪来、集群流控的 token 谁发。配置和代码都是能直接跑的。


01 · 三个默认值,同时失效

大促 0 点,订单详情接口 QPS 从 2000 冲到 7600。压测上限是 5000。

雪崩链很标准:连接池 200 个打满 → Tomcat 800 线程占死 → 网关超时 → 前端重试再推一轮 → 依赖详情页的页面全挂。

Sentinel 我们两年前就接了,控制台里流控、熔断都配着。事故后复盘,三个数字全部站不住:

  • 流控阈值 10000 ------ 压测上限 5000,限在 10000。这规则当年在控制台点的,没进 Nacos,后来换集群重建过一次,就再没人碰过
  • 熔断只配了异常比例 ------ 那次故障一个异常都没抛,RT 从 80ms 拖到 4s。异常比例 0%,熔断器全程认为下游健康
  • 热点参数没限 ------ 一个爆款 SKU 吃掉 70% 的详情流量,接口级限流把正常请求一起拒了

Sentinel 默认把规则存在客户端内存里,重启即忘;控制台改的规则,只对当时在线的那批实例生效。

02 · 一条请求穿过什么

Sentinel 的核心不是注解,是责任链。SphU.entry() 之后,请求依次穿过这些 Slot:

bash 复制代码
SphU.entry("orderDetail")
  -> NodeSelectorSlot    builds DefaultNode
  -> ClusterBuilderSlot  merges ClusterNode
  -> StatisticSlot       counts in LeapArray
  -> AuthoritySlot       block/allow list
  -> SystemSlot          load / RT / threads
  -> FlowSlot            flow rules
  -> DegradeSlot         circuit breaker

从左到右:建节点、汇全局、记账、验名单、看系统水位、判流控、判熔断。每个 Slot 只查自己那本规则,通过就下一个,不通过抛 BlockException 直接短路。@SentinelResource 干的事,就是给这次 entry 包一层 AOP。

三个概念对应三个 Slot:

  • 限流 ------ FlowSlot,在入口,依据自己的容量
  • 熔断 ------ DegradeSlot,在调下游处,依据对方的表现
  • 降级 ------ 不在 Slot 里,是 blockHandler / fallback 接住异常后的返回

判断依据不同,参数来源就完全不同:限流看压测报告,熔断看监控曲线,降级看业务方点头。三个数据源凑不齐,别谈"高可用方案"。

03 · 原理:滑动窗口怎么记账

StatisticSlot 往 ClusterNode 里记 QPS、RT、异常数,底层是 LeapArray:把 1 秒切成 N 个桶,滚着用。

bash 复制代码
 intervalInMs = 1000
 sampleCount = 2  ->  one bucket = 500ms
 +--------+--------+
 | slot 0 | slot 1 |
 +--------+--------+
 timeId = now / 500 % 2
  • 写入 ------ 用 timeId 定位桶,桶的 windowStart 对不上就整格重置
  • 读取 ------ 遍历所有桶求和,跨过 1 秒的旧桶自然作废
  • 好处 ------ 任意时刻往前看都是完整 1 秒,不会出现固定窗口那种 0:59 和 1:00 各算满 1000

这也解释了 count 的单位。QPS 模式是每秒放行数,THREAD 模式是并发执行数------后者对慢调用更敏感,下一节用得上。

04 · 限流:QPS 还是线程数

规则怎么写成代码

bash 复制代码
FlowRule r = new FlowRule("orderDetail");
r.setGrade(RuleConstant.FLOW_GRADE_QPS);
r.setCount(560);
// 0 直接拒 1 预热 2 排队
r.setControlBehavior(
    RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(
    Collections.singletonList(r));

三种 controlBehavior 的差别值得单独记:

  • DEFAULT ------ 超了直接拒,最快,绝大多数接口用它
  • WARM_UP ------ 冷启动,阈值从 count/3 爬坡到 count,默认 3 秒。刚发布、缓存还没热的实例必须用它,否则一上线就被打死
  • RATE_LIMITER ------ 匀速排队,每 1000/count 毫秒放一个。注意它会让请求线程等着,别在网关用

QPS 还是 THREAD

  • QPS 模式 ------ 只数请求,不管每个请求多久。下游变慢时 QPS 没超,线程已经占满了
  • THREAD 模式 ------ 数的是"正在执行的并发数",超了直接拒,等于一个免费的信号量隔离
bash 复制代码
r.setGrade(RuleConstant.FLOW_GRADE_THREAD);
r.setCount(200);   // 并发线程数,不是 QPS

结论:入口用 QPS 挡流量,出口用 THREAD 保线程。

阈值怎么算

  1. 压测,找 RT 突然翻倍的那个点------不是 QPS 上不去的点
  2. 单机压出 800 QPS,乘 0.7 得 560,余量留给 GC、慢 SQL、邻居抢 CPU
  3. 再乘实例数,才是集群总量

集群流控的 token 谁发

10 台各限 500,热点实例先撞墙开始拒,旁边 9 台还闲着。要精确就得集中判定,Sentinel 的做法是插一个 Token Server:

bash 复制代码
FlowRule r = new FlowRule("orderDetail");
r.setCount(5000);
r.setClusterMode(true);
ClusterFlowConfig c = new ClusterFlowConfig();
c.setFlowId(1001L);
c.setThresholdType(
    ClusterFlowConfig.THRESHOLD_TYPE_GLOBAL);
r.setClusterConfig(c);

原理很直白:Client 每次请求向 Token Server 要配额,Server 用 ClusterNode 做全局统计并返回剩余量。THRESHOLD_TYPE_GLOBAL 表示 5000 是集群总量,不是单机量。

部署两种形态:

  • embedded ------ 一台业务实例兼做 Server,省机器,但 Server 挂了集群流控退化成单机
  • 独立部署 ------ 单独跑 sentinel-cluster-server,稳定,多一组需要运维的进程

多实例抢 Server 身份需要选主,一般交给 Nacos 注册中心 + 指定实例,别让它们自己抢。

05 · 熔断:慢调用才是主要死法

三种策略

  • 慢调用比例 DEGRADE_GRADE_RT ------ RT 超过 count 毫秒算慢,慢调用占比超过 slowRatioThreshold 就断
  • 异常比例 DEGRADE_GRADE_EXCEPTION_RATIO ------ 异常占比超过 count(0 到 1 之间)
  • 异常数 DEGRADE_GRADE_EXCEPTION_COUNT ------ 统计窗口内异常数超过 count
bash 复制代码
DegradeRule r = new DegradeRule("orderDetail");
r.setGrade(RuleConstant.DEGRADE_GRADE_RT);
r.setCount(800);          // 慢调用阈值 ms
r.setSlowRatioThreshold(0.6);
r.setTimeWindow(10);      // 熔断 10 秒
r.setMinRequestAmount(20);
DegradeRuleManager.loadRules(
    Collections.singletonList(r));

四个参数怎么定

  • count(RT 模式) ------ 取 P99 的 2 到 3 倍,或者取"用户开始重复点击"的时长。800ms 不是标准值,是业务能忍的上限
  • minRequestAmount ------ 默认 5,太小。低频接口 QPS 2,不到 3 秒就凑够 5 个样本,随便抖一下就误断。按 QPS × 10 抬到 20 以上
  • timeWindow ------ 从进 OPEN 起算,到点转 HALF_OPEN 放一个请求探路,成功回 CLOSED,失败回 OPEN
  • statIntervalMs ------ 默认 1000。低流量接口拉到 10000,用时间换样本数

只配异常比例,等于没装熔断

线上最常见的故障形态是"慢而不死":下游没挂,只是每个请求从 80ms 变成 4s。异常数 0,异常比例 0,熔断器全程 CLOSED,线程一个个被占死,直到 Tomcat 打满。

RT 模式就是为这个场景设计的。只配异常比例的熔断,只防住了下游崩溃,没防住下游变慢------而变慢才是常态。

06 · Nacos:规则从哪来

这是整套体系里最容易漏的一环。Sentinel 的规则默认只活在客户端内存里:

  • 应用重启,规则清空,裸奔到下次有人手动点控制台
  • 控制台改规则,只推给当时连着的实例,新扩容的机器拿不到
  • 多实例规则可能不一致,A 机限 560,B 机还是 10000

数据源模式

客户端主动去 Nacos 订阅,规则变了实时推下来:

bash 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 10.0.0.8:8858
        port: 8719
      eager: true
      datasource:
        flow:
          nacos:
            server-addr: 10.0.0.6:8848
            namespace: prod
            group-id: SENTINEL_GROUP
            data-id: order-svc-flow.json
            data-type: json
            rule-type: flow

一个 dataId 只放一类规则。rule-type 支持 flow、degrade、system、authority、param-flow、gw-flow、gw-api-group 七种,熔断就再加一段 degrade 的数据源,结构照抄。

原理:四步

  1. 启动时 NacosDataSource 拉一次配置,反序列化成 List<FlowRule>
  2. 包成 SentinelProperty,注册给 FlowRuleManager
  3. Nacos 侧挂监听器,配置变更回调 → property.updateValue(list)
  4. FlowRuleManager 里的 AtomicReference 换掉旧值,FlowSlot 下次 entry 直接读新的

没有推送失败重试的坑,因为客户端是订阅方,不是被动接收方。这也是数据源模式比控制台推送可靠的根本原因。

Nacos 里存什么

bash 复制代码
[{
  "resource": "GET:/order/{id}",
  "grade": 1,
  "count": 560,
  "controlBehavior": 0,
  "clusterMode": false
}]

命名建议三段定位:namespace 分环境,group 固定 SENTINEL_GROUP,dataId 用 应用名-规则类型.json。别全塞 public 和 DEFAULT_GROUP,出事那天你会找不到规则在哪。

控制台只当看板用

要,但只读。控制台改规则走的是"推给客户端内存",不写 Nacos,重启就丢。改规则走 Nacos 发布,顺便留下变更记录和审批。

版本坑

spring-cloud-starter-alibaba-sentinel 和 sentinel-core 版本必须对齐。Spring Cloud Alibaba 2022.x 对应 Sentinel 1.8.6,2023.x 对应 1.8.7 以上。错位的典型现象是 rule-type: gw-flow 静默不生效,或者启动报 ClassNotFoundException,而日志里只字不提版本。

07 · 兜底、串联与四个打架点

blockHandler 和 fallback 不是一回事

被限流、熔断拒绝时抛的是 BlockException,只进 blockHandler;业务代码里的异常才进 fallback。只写一个,等于漏掉一半场景。

bash 复制代码
@SentinelResource(
    value = "orderDetail",
    blockHandler = "blocked",
    fallback = "businessFallback")
public OrderVO detail(Long id) {
    return orderClient.get(id);
}

// 被限流、熔断拒绝
private OrderVO blocked(Long id,
                        BlockException e) {
    return OrderVO.empty();
}

// 业务异常兜底,只读本地
private OrderVO businessFallback(Long id,
                                 Throwable t) {
    return OrderVO.empty();
}

兜底方法必须同签名、同返回值,只在末尾多一个 BlockException 或 Throwable 参数。参数对不上不会启动报错,运行时才抛 blockHandler not found------这种错往往上线后才发现。

兜底三铁律

兜底方法里不能有远程调用、不能查库、不能抛异常。它必须是最纯粹的本地返回。

反例:fallback 里调 remoteCache.get()。下游挂的时候兜底跟着挂,用户还是看到 500,这个 fallback 白写了。

四个打架点

  • 阈值大于容量 ------ Nacos 里躺着数字,没人验证过它等于压测上限
  • 熔断没配降级 ------ 熔断打开后请求被拒,用户看到错误页。熔断必须和 fallback 成对出现
  • 重试喂饱熔断 ------ Feign 默认重试,一次失败变两次,下游 90% 负载瞬间翻到 180%。先关重试,再开熔断
  • 多层限流叠加 ------ 网关 5000、服务 3000、接口 1000,实际生效的是 1000,另两层是摆设。要么统一到网关,要么分层定职责:网关防刷,服务保命

配齐了还崩,先查重试,再查阈值。

08 · 三句话总结

  1. Sentinel 的核心是 Slot 责任链,不是注解。@SentinelResource 只是给 SphU.entry() 包了层 AOP,判断全在 FlowSlot 和 DegradeSlot 里,LeapArray 负责记账。
  2. RT 模式是主力策略。线上故障多数是慢而不死,只配异常比例等于把熔断器关掉;minRequestAmount 默认 5 太小,按 QPS × 10 抬。
  3. 规则必须离开内存。数据源模式接 Nacos,一个 dataId 管一类规则,控制台只当只读看板------不然重启一次,三道闸全回出厂设置。

写在最后

觉得有用的话,拜托两件事:

  • 点「赞」+「推荐」(在看)
  • 关注「斯维赤」 ------ Java 实战、开发工具、踩坑复盘,持续更新

下期写「超时到底该设多少」------它比熔断基础,也比熔断更容易被随手写死。

我们下期再见

相关推荐
肌肉娃子9 分钟前
红队攻防测试报告 — wechat-e.cfd
后端
陈随易18 分钟前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
海螺_AIHCC24 分钟前
SSE 流式返回中文变问号,问题不在编码
后端
panbook28 分钟前
从零实现一个时序数据库(一):用 Rust、Arrow 和 Parquet 跑通写入与 SQL 查询
后端
Zan30 分钟前
SQLite + SQLx 实战:为什么已经发布的数据库迁移文件,绝对不能再修改?
数据库·后端·面试
微步AICRM36 分钟前
一物一码验真的并发链路设计:码段管理、四级关联与异常判定
后端·架构
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
我叫黑大帅1 小时前
PHP 常驻 Worker 中 MySQL 连接中断问题报告
后端·面试·php