平时做服务治理,大家关注最多的是 QPS 限流和并发线程数限流,毕竟这两类规则直接面向业务流量,效果立竿见影。但真正线上跑久了你会发现,很多时候流量没那么大,接口却频繁超时,CPU 被打满,GC 抖动剧烈,这时候再靠 QPS 限流已经救不回来了——因为瓶颈根本不在入口流量,而在系统本身的处理能力。
Sentinel 的系统规则就是为这种场景准备的。它不关心你来了多少请求,而是盯着一组系统维度的指标,其中最硬核、也最常用的就是 CPU 使用率。当 CPU 飙到阈值,Sentinel 会直接切断超出系统承载能力的流量,让入口 QPS 自动贴合机器的实际处理上限,形成一种"自适应限流"的效果。这篇文章我会从系统规则的设计原理讲起,结合 JVM 指标联动、参数配置、源码级别验证和实际踩坑经验,把 CPU 使用率触发限流这件事说透。
1. 系统规则到底是什么:从"限流"到"限系统"
1.1 入口 QPS 限流为什么救不了 CPU 打满
先看一个线上真实案例。某个核心服务,单机峰值 QPS 大约 8000,平时 CPU 在 55% 左右,一切正常。某天业务方做了一个批量任务,将大量消息同时推送到这个服务,瞬间入口 QPS 冲到 12000,CPU 直接飙到 98%。当时服务配置了 Sentinel 的 QPS 限流规则,阈值是 10000,按道理 12000 的流量应该会被拦截掉 2000。
但实际上呢?限流确实生效了一部分,CPU 依然在 90% 以上徘徊。原因很简单:QPS 阈值 10000 本身定得就不合理,因为这台 4C8G 的机器在 8 线程并行处理下,单请求平均耗时已经从平时的 2ms 涨到了 6ms,实际的系统吞吐上限已经掉到了 6000 左右。QPS 限流阈值是静态的,而系统的处理能力是动态的,两者一旦脱节,限流就是个摆设。
这就是系统规则存在的意义。它把保护目标从"流量入口"迁移到"系统资源",你不是超过多少 QPS 就拦,而是机器快扛不住的时候才拦,扛得住就放行。这个思路转变非常关键,尤其是对依赖 JVM 运行的服务来说,CPU、负载、内存这些指标才是真正决定生死的因素。
1.2 系统规则的五种指标位
Sentinel 的系统规则支持五种系统维度的保护指标,控制台配置界面里都能看到:
| 指标 | 含义 | 默认阈值类型 | 适用场景 |
|---|---|---|---|
| Load(仅 Linux/Unix) | 系统 1 分钟平均负载 | 双阈值(核心数+1 等) | 容器或物理机整体负载过高时兜底 |
| CPU 使用率 | 系统 CPU 占用百分比 | 单阈值 | 通用场景,推荐优先配置 |
| RT | 所有入口流量的平均响应时间 | 单阈值(毫秒) | 接口整体变慢时兜底 |
| 线程数 | 所有入口流量的并发线程数 | 单阈值 | 线程池耗尽时保护 |
| 入口 QPS | 所有请求的总 QPS | 单阈值 | 全局流量上限兜底 |
其中 CPU 使用率和 Load 是系统规则里最敏感的指标,因为它们直接反映机器的处理余量。RT 和线程数属于间接指标,往往等它们异常时 CPU 已经先告警了。所以如果你只想配置一条规则,我建议优先配置 CPU 使用率。
需要特别说明的是,系统规则是"全局生效"的,不是针对某个资源。也就是说,一旦规则触发,它对所有经过 Sentinel 的流量都生效,不管你是哪个接口、哪个调用链。这是一把双刃剑——保护面大,但影响面也大,配置阈值必须保守稳妥。
1.3 系统规则与 JVM 指标联动的关系
说到 JVM 指标联动,很多刚接触 Sentinel 的人有个误区,以为 Sentinel 直接读取的是 JVM 的 CPU 使用率。严格来说,Sentinel 的系统规则读取的是操作系统层面的 CPU 使用率,但这两者之间关系极为密切。
一个 Java 服务跑在 JVM 上,JVM 的 GC 线程、业务线程都映射到操作系统线程上执行。当 JVM 发生频繁 Full GC 时,GC 线程会抢占 CPU 时间片,表现出来就是操作系统 CPU 使用率飙升。反过来,如果系统 CPU 使用率持续高位,JVM 的垃圾回收停顿时间也会变长,业务线程拿不到 CPU 时间片,RT 跟着上涨。两者互为因果,所以做系统规则时,一定要结合 JVM 的 GC 情况、线程状态一起分析,不能孤立地只看 CPU 数字。
在实际项目中,我的做法是给 Sentinel 系统规则配一个 JVM 指标监控大盘,把 CPU 使用率、Full GC 次数、老年代内存占用、活跃线程数放在一起看。限流触发时,如果 CPU 和 Full GC 同步飙升,基本可以确认是系统资源瓶颈;如果 CPU 不高但限流频繁,那就要排查规则配置本身的问题了。
2. 系统规则的设计原理与源码级拆解
2.1 系统规则的自适应阈值计算逻辑
Sentinel 的系统规则精髓在于"自适应"。它不是简单地拿当前 CPU 使用率和阈值做比较,而是通过一套滑动窗口采样机制,实时估算系统的最大处理能力,再结合当前系统指标决定是否拦截。
核心原理我拆成三步理解:
第一步,采样。Sentinel 会周期性采集系统指标,默认每秒采集一次 CPU 使用率、Load、RT、线程数、QPS 这五类数据,并保存到一个环形数组中形成时间窗口。这就是为什么控制台上能看到一段连续的指标曲线,而不是离散的点。
第二步,估算最大 QPS。系统在正常运行状态下,Sentinel 会记录每个时间窗口的"通过 QPS"和"RT"数据,然后利用利特尔定律(Little's Law)估算系统在当前资源状态下的最大吞吐量。公式很直观:最大 QPS ≈ 并发线程数 / 平均 RT。当系统资源充足时,这个估算值比较稳定;当 CPU 飙升时,RT 增加,估算出的最大 QPS 就会下降。
第三步,动态拦截。系统规则触发时,Sentinel 会计算当前入口 QPS 与估算最大 QPS 的差值,超出部分直接拒绝。这比静态 QPS 规则聪明的地方在于:它允许流量在系统有余量时"偷偷超阈值",系统紧张时则严格收紧。
2.2 CPU 使用率触发限流的判定过程
具体到 CPU 使用率这个指标,Sentinel 的判定逻辑比想象中简单直接。它有一个CpuUsageSystemRule类,大致处理流程是这样的:
当一个请求通过 Sentinel 的SystemSlot时,Sentinel 会从全局系统状态中取出当前 CPU 使用率,和规则里配置的阈值做比较。如果当前 CPU 使用率超过了阈值,Sentinel 就会进一步检查当前入口 QPS 是否已经超过了估算出的系统最大 QPS。注意,这里不是只要 CPU 超标就直接拒绝,而是要看"流量是否已经超载"。
这个设计我一开始觉得有点绕,后来想明白了:CPU 超标但 QPS 还没达到系统极限,说明系统可能正处于瞬时抖动,此时一刀切拒绝所有请求反而会引发雪崩。只有 CPU 超标且流量确实超出处理能力时,才真正触发限流。这也是 Sentinel 系统规则比普通限流更"温柔"的原因。
判定过程走完后,如果决定拒绝请求,会抛出SystemBlockException,这条异常可以被自定义的BlockExceptionHandler捕获处理,返回预设的提示信息。
2.3 系统规则与 JVM 指标联动的落地机制
这里的"JVM 指标联动"在代码层面是怎么实现的?Sentinel 本身不直接采集 JVM 内存、GC 等指标,它只采集操作系统指标。但 Sentinel 提供了SystemRule的扩展机制,允许开发者自定义系统指标采集器(SystemMetricCollector),把 JVM 的指标接入进来。
我做过的一个方案是这样实现的:自定义一个MetricCollector,用ManagementFactory.getOperatingSystemMXBean()获取 JVM 视角的 CPU 使用率,同时监听GarbageCollectorMXBean获取 Full GC 次数和耗时,每 5 秒写入一个指标存储中。Sentinel 的系统规则加载时会读取这个存储,将 JVM 的 CPU 使用率与操作系统 CPU 使用率做加权平均,作为最终判定依据。
这样做的好处是,能把 JVM 的停顿、GC 导致的 CPU 飙高也纳入保护范围。比如一个服务在 Full GC 时操作系统 CPU 可能只有 50%,但 JVM 内部已经近乎停滞,业务线程大量阻塞。如果只看系统 CPU,Sentinel 不会触发限流,但加上 JVM 指标联动后,就能在 Full GC 期间快速切断流量,保护用户体验。
3. 系统规则 + CPU 使用率的完整配置实操
3.1 环境准备与依赖引入
在动手配置之前,先把环境准备好。Sentinel 有两个核心组成部分:一个是 Java 客户端(接入服务的 SDK),另一个是控制台(可视化配置规则和查看监控)。
我以 Spring Cloud Alibaba 项目为例,引入依赖时需要特别注意版本对齐。我实际项目中用的组合是 Spring Boot 2.7.x + Spring Cloud Alibaba 2021.0.5.0 + Sentinel 1.8.6。Maven 依赖如下:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-parameter-flow-control</artifactId> </dependency>依赖版本千万要对齐,我踩过一次坑:Spring Cloud Alibaba 2021.0.1.0 自带的 Sentinel 是 1.8.0,而我在代码里用了 1.8.6 才有的 API,编译没问题,运行时报NoSuchMethodError,排查了好久才发现是版本冲突。建议在父 POM 中显式声明sentinel-core的版本,覆盖 Spring Cloud Alibaba 的默认值。
接着在application.yml中配置 Sentinel 控制台地址:
spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8080 port: 8719 eager: trueeager: true的意思是应用启动时就主动连接控制台,而不是等第一次流量进来才注册。这个配置在实际联调排查时很有用,否则控制台里看不到服务列表,容易误以为接入失败。
3.2 控制台配置系统规则的关键参数
控制台打开之后,在"系统规则"页面点击"新增系统规则",会看到五个指标位。我推荐一套经过实践验证的初始配置:
CPU 使用率:80%Load:不配置(如果是 Linux 机器,建议配置为核心数 + 1,我是 4 核机器,配 5) 入口 QPS:不配置 RT:不配置 线程数:不配置
为什么 CPU 配 80% 而不是 90%?因为 CPU 使用率是一个瞬时值,线上业务存在毛刺,可能某一秒冲到 95%,下一秒又回落到 30%。如果阈值设得太高,等到 CPU 真的到 95% 才触发限流,此时系统已经严重过载,大量请求已经堆积在队列里,限流带来的保护效果大打折扣。80% 是一个"提前干预"的值,留出 20% 的余量给系统做自我保护。
这里要提醒一句:CPU 阈值是个经验值,不是通用公式。如果你的服务是 IO 密集型,CPU 本来就不高,那 80% 可能永远都触发不了限流;如果是计算密集型,CPU 长期 70% 以上,那 80% 又可能过于敏感。建议先观察一周的监控数据,取 P95 的 CPU 使用率,再加 10~15 个百分点作为阈值。
配置保存后,规则会推送到客户端,不需要重启服务。可以通过控制台的"机器列表"查看推送状态,客户端那一栏会显示规则数量。
3.3 JVM 指标联动监控的自定义实现
控制台自带的监控面板只能看 QPS、RT 这些 Sentinel 指标,看不到 JVM 的 GC、内存、线程情况。所以要实现真正的"JVM 指标联动",需要自己做一个轻量级的监控上报。
我的做法是在服务里加一个定时任务,每 10 秒采集一次 JVM 指标,通过 Sentinel 的MetricExtension扩展点写入。核心代码如下:
@Component public class JvmMetricCollector implements Runnable { private static final int PERIOD_SECONDS = 10; @PostConstruct public void start() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this, 0, PERIOD_SECONDS, TimeUnit.SECONDS); } @Override public void run() { // 获取 JVM 内存和 GC 信息 MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); long usedHeap = heapUsage.getUsed() / (1024 * 1024); long maxHeap = heapUsage.getMax() / (1024 * 1024); List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); long fullGcCount = 0; long fullGcTime = 0; for (GarbageCollectorMXBean gcBean : gcBeans) { if (gcBean.getName().contains("Old") || gcBean.getName().contains("CMS")) { fullGcCount += gcBean.getCollectionCount(); fullGcTime += gcBean.getCollectionTime(); } } // 上报到自定义指标存储,供系统规则联动使用 JvmSystemMetricStore.getInstance().update( usedHeap, maxHeap, fullGcCount, fullGcTime ); } }采集到的 JVM 指标,可以输出到日志、接入 Prometheus,也可以像我这样放入一个单例存储,供后续自定义系统规则读取。这样监控大盘里就能同时看到 CPU 使用率、Full GC 频率和限流触发次数,联动分析事半功倍。
3.4 压测验证:CPU 使用率触发限流的现场实录
配置完成后,一定要做压测验证,不能配置完就算完事。我来说一次真实的压测过程,供你参考。
测试环境是 4C8G 的 Linux 机器,服务是一个简单的查询接口,内部有少量计算逻辑。用压测工具(我用的 JMeter)逐步加压,每 30 秒提升一次并发线程数:从 100 并发开始,依次到 200、400、800、1200。
观察到的现象非常有代表性:
- 100 并发时,接口平均 RT 约 3ms,CPU 使用率 40%,Sentinel 控制在 1000 QPS,全部通过。
- 200 并发时,平均 RT 约 5ms,CPU 使用率 65%,Sentinel 控制到 2000 QPS 左右,依然全部通过。注意,系统规则还没触发,因为 CPU 没到 80%。
- 400 并发时,平均 RT 涨到 9ms,CPU 使用率飙升到 85% 并稳定住。此时 Sentinel 开始触发系统规则,控制台"系统规则"页面的"通过 QPS"曲线明显出现锯齿状,部分请求返回了限流提示。系统将实际吞吐控制在 3500 QPS 左右,CPU 慢慢回落到 70%。
- 800 并发时,CPU 短暂冲到 95%,但 Sentinel 限流快速介入,通过 QPS 依然在 3500 附近,CPU 回落到 75% 左右。大量请求被快速拒绝,没有造成线程堆积。
压测结论:系统规则确实做到了"按系统容量自适应限流"。4C8G 的机器在这个接口逻辑下,稳定吞吐上限大约是 3500 QPS,Sentinel 的 CPU 使用率规则能自动将流量收敛到这个值附近,而不是像静态 QPS 规则那样固定在某个数字上。
这里我特别记录了一个细节:压测刚开始时,CPU 冲到 85% 后 Sentinel 并没有立刻限流,中间大约有 1 秒的延迟。原因在于指标采集是周期性进行的,已经超标的 CPU 要等下一个采样周期才会被识别。在实际线上场景中,这个延迟是可以接受的,但如果你有超高并发瞬时流量的场景,建议辅以并发线程数限流来兜底。
4. 常见问题与排查技巧实录
4.1 配置了系统规则但限流不生效
这是群里被问得最多的问题,明明在控制台配置了 CPU 使用率 80% 的系统规则,压测时 CPU 都 90% 了,却一个限流都没有触发。
排查路径基本固定在三个点上。
第一,确认规则是否成功推送到客户端。在控制台的"机器列表"中点击机器 IP,查看"详细规则"里有没有系统规则。如果没有,检查spring.cloud.sentinel.transport.port端口是否被占用,这个端口用于客户端和控制台之间的心跳通信,一旦被防火墙拦截,规则就推不过去。
第二,确认是否真的走了 Sentinel 的流量路径。如果你的接口没有用@SentinelResource注解,也没有用 Sentinel 适配好的 Web Servlet Filter,那么系统规则虽然存在,但请求根本没经过 Sentinel 的系统槽,自然不会触发限流。检查方式很简单:控制台的"实时监控"里如果看不到这个接口的 QPS 曲线,说明流量没接入。
第三,也是最容易被忽略的点:系统规则触发的条件是"系统过载 + 流量超载"双重判定。如果 CPU 超过 80% 但入口 QPS 并没有达到系统估算的最大 QPS,Sentinel 会判定系统虽然繁忙但还能处理,不触发限流。所以压测时请确认压测流量真的足够大,别用几个并发去测试 CPU 限流,那样永远触发不了。
4.2 系统规则误触发导致可用性下降
有次线上反馈,业务方投诉说某个核心接口大量返回限流提示,但看监控 CPU 只有 75%,距离阈值 80% 还有距离。这就很诡异了。
后来仔细排查,发布了原因:那台机器上部署了多个 Java 服务,Sentinel 采集的是操作系统层面的 CPU 使用率,也就是整台机器的 CPU,不单是你的服务。隔壁服务在做大批量数据迁移,把整机 CPU 拉到了 85%,你的服务 CPU 其实只有 20%,但系统规则一样会触发限流。
这个案例告诉我们,在共享机器上配置系统规则必须非常谨慎。如果机器上还有其他应用,建议把 CPU 阈值适当提高,或者改用 JVM 线程 CPU 使用率作为联动指标。我后来基于ThreadMXBean写了一个只统计当前 JVM 所有线程 CPU 时间的采集器,替换掉操作系统级指标,问题才彻底解决。
4.3 限流触发后请求堆积,RT 不降反升
还有一种典型问题:系统规则确实触发了,但效果不理想,被限流的请求并没有立刻返回,而是堆积起来导致 RT 继续上涨。
原因基本都在BlockExceptionHandler的实现上。默认的限流处理逻辑是直接抛出异常,由框架返回一个默认错误页。但有些团队会自定义限流处理,把被拦截的请求放入一个降级队列进行异步重试。这个降级队列长度如果设置不合理,降级处理本身的执行线程就会抢占 CPU,形成恶性循环。
我的建议是:限流降级逻辑里坚决不要做重试,也不要做复杂的降级组装,只返回一个简单的提示或默认数据。系统都过载了,你再在降级路径里消耗 CPU,等于把限流省下来的资源又花掉了。正确做法是让被限流流量立即失败返回,客户端感知到后自行降级到本地缓存或熔断,这样压力才能真正从机器上卸下来。
4.4 指标联动监控的节奏把握
最后说一个监控节奏的经验。JVM 指标联动的指标采集频率不是越快越好。我见过有人把采集周期设成 1 秒,结果监控线程自身占用了不少 CPU,得不偿失。
对于大多数服务,10 秒采集一次 GC 指标完全够用,因为 Full GC 本身是一个慢变量,不太可能在 1 秒内从 0 飙到需要被介入的数量级。CPU 使用率可以按 1 秒采集,因为 Sentinal 自带的系统采集器就是这个频率。联动的核心观察窗口建议按分钟级别看:Full GC 次数/分钟、平均 RT、触发限流 QPS、CPU 使用率,这四者放在同一个时间轴上,才能看清因果链条。
我在实际使用中发现一个小技巧:把系统规则触发时的"被拦截 QPS"当成一个信号量,也就是"当前机器已经达到处理上限"的信号。如果这个值频繁波动,说明服务容量逼近极限,应该考虑扩容而不是调高阈值。
4.5 系统规则实战中的几条铁律
配置系统规则这段时间,我总结了几条算是铁律的经验,这里一并分享。
阈值宁可低一点也不要高。CPU 使用率触发限流是用来保护系统的,不是用来精确控制流量的,阈值高意味着保护滞后,滞后就意味着已经有请求被杀,只是晚了几秒。
系统规则不要单独使用。CPU 限流属于最后一道防线,前面还是要有 QPS 限流、并发线程限流、熔断规则做分层保护。系统规则触发说明前面几层已经失守或者配置不合理,它是兜底的。
改阈值前先看监控。我做过的所有调整都是基于监控数据来决策的,从不拍脑袋调参数。每一次调整都要记录上下文,包括当时的 QPS、RT、CPU、GC 状态,这样才能形成可复用的调优经验。
我个人在实际操作中的体会是,系统规则 + JVM 指标联动这套组合,最大的价值不是"限流"这个动作本身,而是它倒逼团队把系统容量和 JVM 运行状态真正纳入到稳定性治理的视野里。很多团队上线前压测做得轰轰烈烈,上线后遇到突增流量手忙脚乱,就是缺少一个基于系统实时指标的自适应保护机制。当你把 CPU 使用率触发限流跑通并验证有效之后,后续做扩缩容决策、容量评估都会明显更有底气,因为你手头有了一个能真实反映系统承载力的指标信号。
上面说的这些配置方式和排障经验,都是我从实际项目中一点点磨出来的。你要是刚接入 Sentinel,建议先从 CPU 使用率这一条规则开始,在测试环境完整走一遍压测验证流程,再决定要不要叠加其他指标位。把基础规则跑明白,后面不管做 JVM 联动还是接入自定义指标,都会顺很多。