先交代一句:这个标题里的“系统规则与 JVM 指标联动”,很多人看到第一反应是“这不就是 Sentinel 加一个阈值吗”,但实际上 CPU 使用率触发限流这件事,牵扯到 JMX 指标来源、容器环境的取数偏差、系统规则在调用链里的优先级、以及 Nacos 动态下发后如何验证生效。我自己在生产和压测环境里把这条路完整走过一遍,中间踩了好几个坑,这篇就把整个链路掰开讲清楚,包括能直接抄走的配置样例。
先说个真实场景。当时线上有个结算服务,QPS 长期稳定在 200 左右,流控规则也按 300 设置了,按理说还有不少余量。结果某天大促预演,流量才爬到 260,服务就开始大面积超时,紧接着一堆 Full GC 告警。打开监控一看 CPU 使用率 100%,但 Sentinel 控制台里的 QPS 限流一个都没触发。原因很直接:QPS 不高不代表系统安全,请求贵不贵重看耗时不看数量,一个重接口的代价可能是轻接口的几十倍。从那以后我就把系统规则当成第一道防线来用,QPS 限流反而退到第二位。
这种场景适合所有人参考:Java 后端负责人、SRE、中间件维护者,只要你手上有 Sentinel 并且被“接口 QPS 不高但系统打满”这种问题困扰过,下面的内容都用得上。
1. 为什么QPS限流拦不住“重请求”:系统规则解决的不是流量而是水位
先理清概念。Sentinel 有三类主流规则:流控规则(FlowRule)、熔断降级规则(DegradeRule)、系统规则(SystemRule)。前两类都是资源维度的,你给某个接口配 QPS 阈值,它就只管这个接口。系统规则不一样,它管的是整个 JVM 进程所在系统的“健康水位”。
1.1 一个典型的线上事故:QPS 不高,服务先挂了
我复盘一下当时那个事故的完整因果链。流量从 200 爬到 260,表面看没有超过阈值,但仔细观察每个请求的耗时分布,发现有个对账接口的平均响应时间从 80ms 涨到了 1200ms。原因是大促期间数据量翻倍,这个接口里的一个深分页查询把数据库连接池占满了,连接不够用之后,其他轻量接口的请求也全部排队等连接,整体 RT 飙升,JVM 里的业务线程大量阻塞,GC 压力随之增大,CPU 快速打满。
如果只靠 QPS 限流,100 个请求和 260 个请求对我那个服务的影响完全不同。事故里的重接口占比只有 10%,但贡献了 80% 的 CPU 消耗。这种场景下,正确的保护思路不是在“进来多少流量”这个维度上设置上限,而是在“系统 CPU 还能扛住多少新流量”这个维度上做准入控制。
1.2 系统规则与普通流控规则的差异对照
为了说明白这个差异,我直接列一个对比表,你对比着看印象会更深。
| 对比项 | 流控规则(FlowRule) | 系统规则(SystemRule) |
|---|---|---|
| 控制维度 | 单个资源 / 某个接口 | 整个 JVM 所在环境 |
| 核心指标 | QPS、并发线程数 | CPU 使用率、系统负载、整体 RT、整体线程数、入口 QPS |
| 拦截粒度 | 针对该资源的流量 | 经过 Sentinel 埋点链路的全部流量 |
| 适用场景 | 流量均匀、请求成本稳定 | 请求成本差异大、系统水位优先 |
| 规则数量 | 每个接口可单独配置 | 全局只有一套(通常 2~3 条) |
| 触发后的异常 | FlowException | SystemBlockException |
这里注意一点:系统规则不是拿来替代流控规则的,它是前置防线。流控规则管的是“单个接口能不能再放流量进来”,系统规则管的是“整个系统还允不允许新流量进来”。两者配合,才能既照顾局部又照顾整体。
1.3 五种系统阈值类型里,为什么 CPU 使用率最适合做第一道防线
Sentinel 系统规则支持五种阈值类型:系统负载(Load)、CPU 使用率、平均 RT、并发线程数、入口 QPS。很多人第一次接触会全配,但我的建议是:优先保 CPU 使用率。
原因有三点。第一,CPU 使用率是所有指标的“最终汇聚点”,RT 上涨、线程阻塞、GC 频繁,最终都会反映到 CPU 上;第二,CPU 指标的取值范围是 0~1,语义直观,阈值设置简单,不像系统负载那样还要考虑机器核数换算;第三,CPU 取数跨平台稳定,而系统负载在 Windows 上经常拿不到准确值(getSystemLoadAverage 在 Windows 下返回 -1),一旦用了 LOAD 规则,Windows 开发机上验证和 Linux 生产机上表现不一致,排查起来很恶心。
所以后面所有的配置样例和踩坑记录,我都是围绕 CPU 使用率这个阈值类型展开的。
2. CPU使用率怎么变成限流信号:一条从操作系统到Sentinel的取数链路
这个标题里提到的“JVM 指标联动”,核心就在这一节。搞懂取数链路,你就知道为什么 CPU 限流会有延迟、为什么会跟容器环境“打架”、以及规则被触发时日志该怎么看。
2.1 指标来源是 JMX 的 OperatingSystemMXBean
Sentinel 拿 CPU 使用率,走的不是额外的 agent 采集,也不是你自己上传监控数据,它直接调了 JVM 自带的 JMX 接口。具体来说,是com.sun.management.OperatingSystemMXBean#getSystemCpuLoad()这个方法,返回一个 0~1 的浮点数,代表系统最近一小段时间内的 CPU 使用率。
拆开看这个类名你就明白了:com.sun.management是 JDK 自带的扩展管理包,OperatingSystemMXBean是 JVM 暴露出来的操作系统信息接口。Sentinel 运行在同一个 JVM 进程内,通过进程内的 MXBean 查询就能拿到数据,不需要额外开 JMX 远程端口,也不需要部署监控 agent。这一点是它的设计优势:轻量、零额外组件。
顺带说一句,这里经常有人把“JVM 指标”理解成“JVM 内存指标”。实际上广义的 JVM 指标至少包括四类:CPU(OperatingSystemMXBean)、内存(MemoryMXBean 和内存池)、GC(GarbageCollectorMXBean)、线程(ThreadMXBean)。Sentinel 系统规则里用到的 CPU、RT、线程数,本质上都来自这一类 JVM 运行时指标。所谓“联动”,就是让流控决策直接建立在这些指标之上,而不是只盯着流量数字。
2.2 每秒采样机制:为什么 CPU 触发会有一两秒的“迟钝感”
Sentinel 内部有一个系统状态监听器,SystemStatusListener,它维护了一个每秒执行一次的定时统计任务。每次执行时,它会把getSystemCpuLoad()的结果、当前系统负载、平均 RT、线程数等指标刷新到内存里,供SystemSlot做实时判断。
这里有一个非常容易误解的点:getSystemCpuLoad()返回的不是“这一瞬间的 CPU 使用率”,而是操作系统统计的时间窗口内的平均值。不同实现下这个窗口从几十毫秒到几百毫秒不等,再叠加 Sentinel 每秒拉取一次的逻辑,最终的效果就是:CPU 真的打上去之后,Sentinel 的拦截动作通常会有 1~2 秒的延迟。
这个延迟是刻意留下的。想象一下如果做成毫秒级实时判断,CPU 使用率出现一个 200ms 的尖峰就触发限流,那系统规则会频繁误伤正常流量。1 秒左右的采样间隔天然起到“平滑毛刺”的作用,只对那些持续超过阈值的压力产生响应。
2.3 系统 CPU 与进程 CPU:一个很多人搞错的容器取数问题
OperatingSystemMXBean里面其实有两个方法:getSystemCpuLoad()和getProcessCpuLoad()。前者是整个操作系统的 CPU 使用率,后者才是当前 JVM 进程自身的 CPU 使用率。Sentinel 取的是前者。
这两个指标在物理机上差别不大,因为物理机通常只跑一个 Java 服务。但到了容器环境,差别就大了。
我在压测环境里遇到过一个经典问题:容器分配了 2 核,宿主机是 32 核。压测时容器内 CPU 已经打满 200%,但 Sentinel 系统规则始终不触发,因为getSystemCpuLoad()读到的是宿主机的整体 CPU 使用率,32 核的宿主机跑多少业务都很难超过 50%。换句话讲,按这一版 JDK 的取数行为,Sentinel 截获的是“整个宿主机”被谁打满了,而不是“我这个容器”被打满了。
这个问题在不同 JDK 版本、不同容器配置下的表现并不一致,新一点的 JDK 在容器环境中会部分感知 cgroup 的 CPU 配额,但行为仍然跟具体版本强相关。所以如果你在容器里用系统规则,上线前一定要做一次实测,确认一个事实:压力打满容器时,Sentinel 到底能不能通过系统规则拦截住流量。如果发现不能,要么升级 JDK 版本,要么放弃 CPU 阈值,改用入口 QPS 这类应用层指标兜底。
2.4 从指标到拦截:SystemSlot 在调用链里的位置决定了它的优先级
Sentinel 的流量处理不是一个大方法做完的,而是一条由多个 Slot 组成的责任链。系统规则对应的 Slot 叫做SystemSlot,它的位置在普通流控规则(FlowSlot)之前。
这个位置关系直接决定了一件事:当系统水位超标时,Sentinel 会优先触发系统规则,而不是先看某个接口的 QPS 限流。也就是说,系统规则的优先级高于普通限流规则。
这个设计是有道理的。系统已经濒临过载,这时候不能再给任何接口放水,哪怕单个接口的 QPS 还没到阈值,整体也不该继续接新请求。如果你在排查问题时发现:明明 QPS 远没到阈值却出现拦截日志,那大概率就是系统规则先动手了,日志里会出现SystemBlockException,跟FlowException区分开。
关于日志,被系统规则拦下来的请求会写进${user.home}/logs/csp/sentinel-block.log,里面会明确标注SystemBlockException以及当前 CPU 使用率。这个文件是排查系统规则问题时的第一排查入口。
3. 从配置到生效:CPU阈值设置、Nacos动态下发与验证
配置这块我按三条路径来讲:纯代码方式适合本地和单元测试,控制台方式适合临时调整,Nacos 方式适合生产环境动态管理。前两种简单,重点说 Nacos。
3.1 代码方式:一分钟内配好 CPU 阈值
先说最直接的 API 方式,适合快速验证逻辑。
import com.alibaba.csp.sentinel.slots.system.SystemRule; import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; public class SystemRuleQuickStart { public static void initCpuRule() { SystemRule rule = new SystemRule(); // 系统规则是全局的,resource 传空串即可 rule.setResource(""); // limitType = 1 表示 CPU 使用率 rule.setLimitType(SystemRule.LIMIT_TYPE_CPU); // 0.8 表示 CPU 使用率达到 80% 时触发限流 rule.setCount(0.8); SystemRuleManager.loadRules(Collections.singletonList(rule)); } }这里提醒一句:count的取值是 0~1 的浮点数,0.8 代表 80%。有人习惯性写成 80,结果就是规则永远不生效,因为 CPU 使用率永远不可能达到 80(因为在代码比较里,0.x 永远小于 80,即永远不触发)。这个细节踩的人不在少数。
3.2 控制台方式:适合临时调整
如果你搭建了 Sentinel 控制台,操作路径是:控制台左侧菜单找到“系统规则”,点击“新增系统规则”,指标类型选择“CPU”,阈值填0.8,然后保存。
控制台方式的优点是直观、无需发版,但它依赖控制台和客户端的连通性,而且控制台的规则存储在内存里,服务重启就丢了。所以生产环境我一般不建议用控制台做长期规则管理,它更适合应急调整。
3.3 Nacos 动态下发:生产环境的正确打开方式
Nacos 方案是目前社区里用得最多、也最适合生产的方式。配置里加依赖和 datasource 配置,Nacos 侧放 JSON 格式的规则,客户端启动时会自动拉取,Nacos 配置变更后客户端也会实时感知并更新。
依赖部分:
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.8.7</version> </dependency>Spring Boot / Spring Cloud 配置部分:
spring: application: name: settlement-service cloud: sentinel: datasource: system-rules: nacos: server-addr: ${NACOS_ADDR} dataId: sentinel-system-rules groupId: SENTINEL_GROUP rule-type: systemNacos 配置中心里新建一个配置,dataId 和 groupId 与上面保持一致,配置格式用 JSON,内容如下:
[ { "resource": "", "limitType": 1, "count": 0.8 } ]字段说明:resource传空串表示全局规则;limitType为 1 表示 CPU 使用率(0=系统负载,1=CPU,2=平均 RT,3=并发线程数,4=入口 QPS);count是对应阈值。
这里有个版本细节:不同版本的 Sentinel 对系统规则 JSON 字段命名有差异,老版本用grade标识阈值类型,新版本用limitType。你用 Nacos 下发之前,先确认一下自己项目里的 sentinel-core 版本,在控制台手动配置一条规则,然后观察客户端日志里打印出来的规则对象是哪个字段,再按对应字段写 JSON。我因为这个字段差异,曾经在测试环境发了三遍配置都没生效,最后查版本才定位到。
3.4 阈值怎么定:别照抄网上的 0.8
CPU 阈值定多少,没有统一答案,但有一条经验值链路可以参考。
先看你的部署规格。物理机或容器分配了多少核,决定了 CPU 使用率的“安全水位”。单核实例在 80% 时已经几乎没有余力消化突发流量,8 核实例在 80% 时还剩 1.6 核的算力,两者能承受的新增流量完全不是一个量级。
再看业务的响应时间要求。核心交易链路的服务,建议阈值放到 0.5~0.6,给系统留足余量,宁可提前拦截一部分非核心流量,也不能让核心接口的 RT 劣化;离线任务型服务、对 RT 不敏感的服务,可以放到 0.75~0.8。
我的习惯是先设一个偏保守的值,比如 0.7,然后压测观察:CPU 接近 70% 时,Sentinel 开始拦流量,拦完之后 CPU 是否回落、业务错误率是否归零。如果拦截后 CPU 快速回落(说明拦截有效且系统确实过载了),这个阈值就是合理的;如果拦截后 CPU 还在缓慢上升(说明桶里面已经积压了大量请求,CPU 下降有滞后),那就把阈值继续往下调,比如 0.6。这个“压测-观察-回调”的过程,比任何理论值都可靠。
3.5 验证规则确实生效:压测加日志对照法
配置完之后别急着上生产,先做一次验证,确认规则真的在拦截。
最直接的验证方法四步走。第一步,起一台压测机,对目标接口施压,逐渐增加并发。第二步,打开${user.home}/logs/csp/sentinel-block.log,观察有没有SystemBlockException出现。第三步,用top命令对照观察 CPU 使用率是否稳定在阈值附近。第四步,看服务本身的 QPS 曲线,确认被拦截后吞吐不再继续上涨。
我遇到过一个很迷惑的情况:压测到 CPU 95% 也没看到SystemBlockException。排查后发现不是规则没生效,而是压测的请求没有经过 Sentinel 埋点。系统规则只能拦截“经过 Sentinel 资源链路”的流量,如果一个接口的入口压根没加@SentinelResource注解、也没走 Sentinel 的包装链路,那系统规则对它基本是睁一只眼闭一只眼。这个问题放到下一节详细说。
4. 压测与线上踩坑记录:CPU限流的失效与误伤
配置从“写了”到“生效”,中间隔着几条深坑。我整理了几个亲身踩过的案例,每个都附上排查链路。
4.1 误伤案例:低配实例上的 0.3 阈值把高峰期流量拦没了
有一段时间我给一个 2C4G 的网关服务设了 CPU 阈值 0.3,理由是“网关不该吃太多 CPU,设低一点更安全”。结果上线第一个高峰期,大量正常请求被拦截,业务方直接炸毛。
复盘原因:2 核实例上 CPU 使用率很容易因为一次 GC 或者一批定时任务冲到 30% 以上,而 0.3 的阈值没有给系统留出“瞬时尖峰”的缓冲空间。GC 引发的 CPU 波动本身并不代表系统过载,但阈值太低就会把这些正常波动当成过载信号。
教训是:CPU 阈值不能只看总量,还要看实例规格和波动幅度。小规格实例的日常 CPU 波动本来就比大规格实例剧烈,阈值至少要高于平稳运行时的 CPU 均值 20 个百分点以上。我后来把网关的阈值从 0.3 调回 0.7,误拦情况立刻消失。
4.2 失效案例:容器环境里读到的是宿主机 CPU
这个在第二章节里已经埋了伏笔。当时有个容器化部署的服务,容器配置 4 核,宿主机 48 核。压测容器内 CPU 到 400%(4 核满载),Sentinel 的系统规则阈值配的 0.8,压了十分钟一次系统限流都没有。
排查过程分三步。第一步,检查规则确实加载成功——调用SystemRuleManager.getRules()打印出来,规则在里面;第二步,看sentinel-record.log里系统状态监听器打印的实时 CPU 值——发现记录的 CPU 一直是 0.2 左右,这跟容器内top显示的 400% 明显对不上;第三步,去宿主机上看整体负载,确实不高。确认是getSystemCpuLoad()拿到的是宿主机视角的 CPU 使用率。
这个案例最终的处理不是升级 JDK,而是调架构:容器场景下放弃用系统 CPU 作为触发条件,改用“入口 QPS + 平均 RT”的组合规则来兜底。入口 QPS 一旦达到峰值且平均 RT 开始上涨,就触发拦截,效果比 CPU 规则更可控。
4.3 不生效案例:接口根本没接入 Sentinel 埋点
还有一个失效场景很隐蔽:系统规则配好了,Nacos 也下发了,压测也测了,但sentinel-block.log里就是干干净净。最后一行一行查代码才发现,被测的那个接口入口没有@SentinelResource注解,也没有用 Sentinel 的SphU.entry()包裹。
系统规则虽然叫“系统规则”,但它不是部署在操作系统层面的防火墙。它是一个嵌入在应用调用链里的保护器,只对流经 Sentinel 的请求生效。如果某个接口绕过了 Sentinel 链路,那它就完全不受系统规则管制。
所以接入系统规则前,先梳理一遍你的核心入口:哪些接口走了 Sentinel 埋点。只有至少把核心入口全部纳入埋点,系统规则才有意义。埋点方式一般就是@SentinelResource注解或者SphU.entry("resourceName"),确保所有重要的外部流量入口都经过这个链路。
4.4 多规则并存时的“与”逻辑:任一超限都拦截
生产环境里通常不会只配一条系统规则。比如你既配了 CPU 使用率阈值,又配了入口 QPS 阈值,那这两个规则之间是什么关系?
答案是“与”逻辑:任何一条规则超限,都会触发拦截。也就是说,即使 CPU 使用率完全正常,只要入口 QPS 超过了另一条规则设置的阈值,请求照样会被SystemBlockException拦住。
这个设计合理,但容易造成排查误解。我有一次看到生产环境大量拦截日志,第一反应是 CPU 打满了,结果top一看 CPU 才 40%。查完才发现是入口 QPS 的阈值被人调低了,跟 CPU 一点关系都没有。建议排查时先看sentinel-block.log里SystemBlockException的详细描述,它会明确写出是哪种阈值类型触发的拦截,而不是自己凭感觉猜。
4.5 完整排查链路总结
如果你遇到了“配置了系统规则但行为不符合预期”的问题,下面的链路可以按顺序走:
- 确认规则确实加载:控制台或代码里调用
SystemRuleManager.getRules()查看当前规则列表,没有规则一切都是空谈。 - 确认系统状态确实超阈值:看
sentinel-record.log里的实时 CPU 值,或通过 JMX 手动调用一次getSystemCpuLoad()对比。 - 确认请求确实经过了埋点链路:检查接口有没有
@SentinelResource注解,或代码里有没有SphU.entry()。 - 确认拦截日志:看
${user.home}/logs/csp/sentinel-block.log里有没有SystemBlockException,以及它描述的阈值类型。 - 对照操作系统视角:用
top或sar看系统 CPU 使用率,跟 Sentinel 记录的数值做对比,排除容器取数偏差。 - 最后检查优先级问题:如果还有流控规则、熔断规则同时配置,看是不是其他规则先拦截了,日志里异常类型会区分
FlowException、DegradeException和SystemBlockException。
这条链路我每次排查系统规则问题都会完整走一遍,走完基本 90% 的问题都能定位。
5. 生产环境把CPU联动限流用好:阈值参考与规则组合
规则生效之后,剩下的是怎么把系统规则融入整个稳定性体系。这里我分享几个生产实践中的组合思路。
5.1 不同场景的阈值参考
| 部署规格 | 业务特点 | 建议 CPU 阈值 | 说明 |
|---|---|---|---|
| 2C4G 小规格 | 对 RT 敏感 | 0.5 | 小规格实例波动大,阈值不能太低,但也要留足余量 |
| 2C4G 小规格 | 离线任务 / 容忍 RT | 0.6 | 任务型服务可适当提高吞吐优先 |
| 4C8G 常规规格 | 对 RT 敏感 | 0.6 | 常规规格建议 0.5~0.6 |
| 8C16G 大规格 | 核心交易链路 | 0.7 | 核数多,单一请求不变时 70% 仍有较大余力 |
| 容器多实例 | 无法确定取数行为 | 暂不使用 CPU 规则 | 用入口 QPS + 平均 RT 代替 |
再次强调:这是起步参考值,不是标准答案。每次上线前都要做一轮压测,观察拦截点附近的 CPU 实际水位,再决定微调。
5.2 系统规则和流控规则、熔断规则的分工
正确姿势是把三类规则放在不同层级。系统规则在最前面,负责“全局水位”保护;流控规则在中间层,负责“单个入口”的 QPS 上限;熔断规则在最后层,负责“下游已经坏了”时的快速失败。
举一个完整的例子。某个下单接口:
- 系统规则:CPU 使用率阈值 0.6,全局生效。
- 流控规则:下单接口 QPS 上限 1000,防止瞬时流量打穿下游。
- 熔断规则:下游库存服务调用错误率超过 50% 时熔断 10 秒。
CPU 正常情况下,系统规则完全不参与,流控规则按接口维度管 QPS;一旦系统 CPU 逼近 60%,系统规则抢先拦截,连带着把其他所有接口的流量一起降下来。这个组合在面对大促流量突刺时效果非常明显。
5.3 告警联动:别等被拦截才开始反应
系统规则是应急手段,不是日常手段。如果生产环境经常出现SystemBlockException,说明你已经长期在过载边缘运行了,这时候正确的做法不是调高阈值,而是扩容或者优化热点接口。
所以我建议在系统规则之外,另配一套监控告警:CPU 使用率达到阈值的 80% 时发出 warning 告警,达到阈值的 100% 时发出 critical 告警。这样当 CPU 开始逼近阈值时,值班同学就能介入处理,而不是等 Sentinel 真正开始拦截流量,线上业务已经出现大量报错了。
5.4 关于恢复期的预期管理
最后说一个很多人忽略的细节:系统规则触发后,恢复不是瞬时的。
因为 CPU 使用率是采样计算出来的,超限状态下拦截了大量流量后,系统 CPU 会逐渐回落。但 JVM 里已经产生的大量对象还在等 GC 回收,某些线程的锁竞争也还在持续,所以 CPU 回落到阈值以下需要一段时间。在这个回落期间,Sentinel 会继续拦截流量。这就导致一个现象:CPU 打满触发拦截后,流量被砍掉一大半,但系统依然“缓”了几十秒才完全恢复。
这不是 Bug,是正常逻辑。你要做的是在系统规则的阈值下方预留足够的缓冲,让 CPU 在接近阈值时就开始拦截,而不是等它完全越过阈值才动手。换句话说,阈值选择要保守,恢复期的“惯性”也要算进去。
我在实际项目里还有一个个人习惯:每次改动系统规则阈值,都会顺手更新 Nacos 配置中心的描述,写上改动日期、原因、压测依据。几个月后回看,这些记录能少踩很多重复的坑。系统规则的威力不在于它多复杂,而在于配置的时候你对系统水位有多了解。