![图片[1]-Sentinel 性能影响深度分析:到底损耗了多少性能? - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/cover-2119.png)
本文导读
在引入任何一个框架之前,开发者都会关心一个问题:它对性能的影响有多大?
Sentinel 作为一个服务保护框架,需要嵌入到每个请求的调用链路中,它的性能表现直接关系到系统的整体吞吐量。官方文档给出的结论是:
“引入 Sentinel 带来的性能损耗非常小,只有在业务单机量级超过 25W QPS 的时候才会有一些显著的影响(5%~10% 左右),单机 QPS 不太大的时候损耗几乎可以忽略不计。”
这个结论靠谱吗?Sentinel 又是怎么做到低损耗的?
本文将从以下几个维度深入分析 Sentinel 的性能:
- Sentinel 的性能优化手段:从源码看 Sentinel 在性能上做了哪些努力
- JMH 基准测试入门:快速掌握 Java 基准测试工具 JMH 的使用
- Sentinel 性能基准测试:实际测试看看 Sentinel 的性能表现
- 实际使用建议:如何在项目中最大程度降低 Sentinel 的影响
通过本文,你不仅能了解 Sentinel 的性能,还能学会 JMH 基准测试的方法。
一、Sentinel 在性能方面的努力
评价一个框架的性能,不能只看最终的测试结果,更要看它在设计和实现上为性能做了哪些努力。Sentinel 在性能优化上可谓下足了功夫。
1.1 滑动窗口的性能优化
指标统计是 Sentinel 的核心功能,也是最频繁的操作。在滑动窗口的实现上,Sentinel 做了多层优化。
1. 循环复用 Bucket
滑动窗口采用”时间窗口 + Bucket”的结构,通过循环复用 Bucket 来减少对象的创建和销毁。Bucket 一旦创建就不会被销毁,只是不断被重置和复用。
2. LongAdder 优化并发统计
统计指标数据时,使用 LongAdder 而非 AtomicLong 来统计请求成功数、失败数、总耗时等指标。
LongAdder 的优势:在高并发场景下,通过将竞争分散到多个 Cell 中,减少了 CAS 操作的冲突,从而提升了并发更新的吞吐量。
3. 定时任务维护时间戳
Sentinel 通过定时任务递增时间戳,而不是每次都调用 System.currentTimeMillis() 获取。
为什么要这么做?
System.currentTimeMillis()是一个 native 方法,调用开销相对较大- 在高 QPS 场景下,每次请求都调用一次的累积开销不容小觑
- 用定时任务维护时间戳,虽然牺牲了一点精度,但换来了性能提升
精度换取性能,这是 Sentinel 在性能优化上的一个典型思路。
1.2 读多写少场景下的 Map 选择
在 Sentinel 源码中,你会看到很多这样的代码模式:
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
chain = SlotChainProvider.newSlotChain();
// 创建新的 HashMap
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<>(
chainMap.size() + 1);
// 复制旧数据
newMap.putAll(chainMap);
// 插入新数据
newMap.put(resourceWrapper, chain);
// 整体替换
chainMap = newMap;
}
}
}
return chain;
为什么不用 ConcurrentHashMap?
答案是:读多写少的场景下,普通 Map 反而更快。
以 ProcessorSlotChain 的创建为例:
- 只有在资源第一次被访问时才会创建(写操作)
- 之后的所有访问都是读操作
- 假设有 10 个接口,应用启动后很快都被访问过了,之后这个 Map 就几乎不会再有写操作
既然没有写操作,就不需要加锁,也不需要 ConcurrentHashMap 的同步开销。用普通的 HashMap,在读操作上性能更好。
写操作的实现方式:
- 加锁(synchronized)
- 创建新的 HashMap
- 复制所有旧数据 + 新数据
- 用新 Map 整体替换旧 Map
这就是典型的写时复制(Copy-On-Write)思想,虽然写操作开销大,但读操作完全无锁,非常适合读多写少的场景。
1.3 流量效果控制的取舍
在流量效果控制上,Sentinel 也做了一些性能优先的取舍。
1. 匀速限流的精度取舍
RateLimiterController(匀速限流控制器)有一个隐含的限制:最大支持约 1000 QPS 的匀速限流。
为什么是 1000?
- 因为 Sentinel 使用的时间戳最小单位是毫秒
- 以毫秒为单位,1 秒钟最多有 1000 个时间点
- 所以理论上每秒最多只能精确控制 1000 个请求通过
这也是为什么很多人觉得 Sentinel 的匀速限流”不那么准”——比如限制每 5ms 一个请求,但实际上可能 5ms 内通过了好几个请求。
这是因为 Sentinel 并没有严格控制并发下的排队计时,而是用更轻量的方式实现。对于大多数业务场景来说,这种精度已经足够了,完全没必要为了极致的精确而牺牲性能。
2. 冷启动的令牌更新频率
WarmUpController(冷启动控制器)的实现也有性能考量:
- 不是每个请求都更新令牌桶
- 而是每秒只生产一次令牌
- 生产令牌后,一次性扣减上一秒通过的请求数(官方称之为”令牌自动掉落”)
这样做的好处是:
- 减少了 CAS 更新令牌桶的次数
- 降低了对应用性能的影响
- 虽然精度稍差,但对于冷启动场景完全够用
从这些细节可以看出,Sentinel 的设计哲学是:在保证功能可用的前提下,尽最大可能降低自身对应用性能的影响。
二、JMH 基准测试工具入门
在进行性能测试之前,我们先来了解一下 Java 领域最权威的基准测试工具:JMH。
2.1 什么是 JMH
JMH(Java Microbenchmark Harness)是 OpenJDK 官方提供的 Java 微基准测试工具。它的优势在于:
- 可信度高:由 JVM 团队开发维护,深知 JVM 的各种优化机制
- 结果准确:自动处理预热、GC、JIT 编译等影响因素
- 功能强大:支持吞吐量、平均耗时、采样时间等多种测试模式
2.2 引入依赖
在项目中添加 JMH 的依赖:
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</dependency>
</dependencies>
1.23 是撰写本文时的最新版本,建议使用最新稳定版。
2.3 常用注解详解
JMH 的使用主要依靠注解,下面是最常用的几个注解。
@Benchmark
声明一个 public 方法为基准测试方法。每个被 @Benchmark 标注的方法对应一个基准测试用例。
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 要测试的代码
}
}
@BenchmarkMode
指定基准测试的模式,即测试哪些指标。常用模式:
| 模式 | 说明 |
Mode.Throughput |
吞吐量:每秒执行多少次操作 |
Mode.AverageTime |
平均耗时:每次操作平均耗时 |
Mode.SampleTime |
采样耗时:随机采样,统计 p50、p99 等 |
Mode.All |
所有模式都测一遍 |
@BenchmarkMode(Mode.AverageTime)
@Benchmark
public void testMethod() {
// 测试平均耗时
}
@Measurement
配置测量参数:
| 参数 | 说明 |
iterations |
测量次数(轮次) |
time |
每轮测量的持续时间 |
timeUnit |
时间单位 |
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Benchmark
public void testMethod() {
// 测量 5 轮,每轮 1 秒
}
@Warmup
配置预热参数。JVM 有 JIT 编译,代码执行多次后会被优化,所以需要先预热再测量,结果才准确。
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Benchmark
public void testMethod() {
// 预热 5 轮,再测量 5 轮
}
注意区分:iterations 是”轮次”,不是”方法执行次数”。每轮持续 time 时间,在这段时间内方法会被不断调用,调用次数取决于方法本身的耗时。
@OutputTimeUnit
指定输出结果的时间单位。如果方法很快,建议用微秒或纳秒,更直观。
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Benchmark
public void testMethod() {
// 结果以纳秒为单位输出
}
@Fork
指定 fork 多少个子进程来执行测试。每个进程独立运行,避免相互影响。
@Fork(1)
@Benchmark
public void testMethod() {
// 只用 1 个进程测试
}
@Threads
指定用多少个线程并发执行测试方法。
@Threads(2)
@Benchmark
public void testMethod() {
// 2 个线程并发执行
}
公共配置
如果一个类中有多个基准测试方法,且配置相同,可以把注解写在类上,所有方法共享配置:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {
@Benchmark
public void testMethod1() {
// 使用类上的配置
}
@Benchmark
public void testMethod2() {
// 使用类上的配置
}
}
2.4 @State:对象共享范围
@State 注解用于指定类中字段的共享范围:
| 范围 | 说明 |
Scope.Thread |
每个线程有独立的实例(线程隔离) |
Scope.Benchmark |
所有线程共享同一个实例 |
Scope.Group |
同一线程组内共享 |
举个例子,测试 Gson 和 Jackson 的性能对比:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark) // 所有线程共享
public class JsonBenchmark {
private GsonParser gsonParser = new GsonParser();
private JacksonParser jacksonParser = new JacksonParser();
@Benchmark
public void testGson() {
gsonParser.fromJson(jsonString, JsonTestModel.class);
}
@Benchmark
public void testJackson() {
jacksonParser.fromJson(jsonString, JsonTestModel.class);
}
}
如果换成 @State(Scope.Thread),那么每个线程都会有自己独立的 gsonParser 和 jacksonParser 实例。
2.5 @Param:参数化测试
@Param 注解可以为测试方法指定多组参数,JMH 会为每组参数分别执行测试。
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Threads(2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
public class JsonBenchmark {
private GsonParser gsonParser = new GsonParser();
private JacksonParser jacksonParser = new JacksonParser();
@Param({
"{\"name\":\"zhangsan\",\"age\":20}",
"{\"name\":\"zhangsan\",\"age\":20,\"address\":\"...\"}",
// 更长的 JSON...
})
private String jsonStr;
@Benchmark
public void testGson() {
gsonParser.fromJson(jsonStr, JsonTestModel.class);
}
@Benchmark
public void testJackson() {
jacksonParser.fromJson(jsonStr, JsonTestModel.class);
}
}
测试结果会分别输出每组参数对应的性能数据,方便对比不同输入下的性能表现。
2.6 非注解方式
除了注解,还可以通过代码方式配置:
public class BenchmarkTest {
@Test
public void test() throws RunnerException {
Options options = new OptionsBuilder()
.include(JsonBenchmark.class.getSimpleName())
.exclude("testJackson")
.forks(1)
.threads(2)
.timeUnit(TimeUnit.NANOSECONDS)
.warmupIterations(5)
.warmupTime(TimeValue.seconds(1))
.measurementIterations(5)
.measurementTime(TimeValue.seconds(1))
.mode(Mode.AverageTime)
.build();
new Runner(options).run();
}
}
注解方式和代码方式本质上是一样的,运行时注解会被解析成配置对象。注解方式更简洁,代码方式更灵活。
2.7 运行方式
JMH 有几种常见的运行方式:
- 打 jar 包在服务器运行:最准确,贴近生产环境
java -jar my-benchmarks.jar - IDEA 中写单元测试运行:开发调试方便
- IDEA JMH Plugin 插件:最方便,直接点方法上的运行按钮
- 插件名:JMH Plugin
- 安装后在
@Benchmark方法旁会出现运行按钮
建议:开发阶段用插件快速验证,最终结果在服务器上测试,避免本机环境的干扰。
三、Sentinel 性能基准测试
了解了 JMH 之后,我们来看看 Sentinel 自身的基准测试结果。
3.1 测试设计
要衡量 Sentinel 对性能的影响,需要两组数据做对比:
- 对照组:不使用 Sentinel 保护的业务方法
- 实验组:使用 Sentinel 保护的业务方法
两者的吞吐量之差,就是 Sentinel 带来的性能损耗。
以下是 Sentinel 官方提供的基准测试类简化版:
@Fork(1)
@Warmup(iterations = 10)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
public class SentinelEntryBenchmark {
@Param({"25", "50", "100", "200", "500", "1000"})
private int length;
private List<Integer> numbers;
@Setup
public void prepare() {
numbers = new ArrayList<>();
for (int i = 0; i < length; i++) {
numbers.add(ThreadLocalRandom.current().nextInt());
}
}
// 对照组:纯业务逻辑
@Benchmark
@Threads(8)
public void doSomething() {
Collections.shuffle(numbers);
Collections.sort(numbers);
}
// 实验组:用 Sentinel 保护
@Benchmark
@Threads(8)
public void doSomethingWithEntry() {
Entry e0 = null;
try {
e0 = SphU.entry("benchmark");
doSomething();
} catch (BlockException e) {
// ignore
} finally {
if (e0 != null) {
e0.exit();
}
}
}
}
测试说明:
doSomething:模拟业务方法,做 shuffle + sort 操作doSomethingWithEntry:用 Sentinel 包装业务方法- 8 个线程并发,吞吐量模式,预热 10 轮
@Setup方法在每轮测试前执行初始化,类似 JUnit 的@Before
3.2 测试结果
对照组(无 Sentinel):
Result "com.alibaba.csp.sentinel.benchmark.SentinelEntryBenchmark.doSomething":
300948.682 ±(99.9%) 33237.428 ops/s [Average]
(min, avg, max) = (295869.456, 300948.682, 316089.624), stdev = 8631.655
CI (99.9%): [267711.254, 334186.110] (assumes normal distribution)
- 平均:约 30.1 万 ops/s
- 最小:约 29.6 万 ops/s
- 最大:约 31.6 万 ops/s
实验组(有 Sentinel):
Result "com.alibaba.csp.sentinel.benchmark.SentinelEntryBenchmark.doSomethingWithEntry":
309934.827 ±(99.9%) 98910.540 ops/s [Average]
(min, avg, max) = (280835.799, 309934.827, 337712.803), stdev = 25686.753
CI (99.9%): [211024.287, 408845.366] (assumes normal distribution)
- 平均:约 31.0 万 ops/s
- 最小:约 28.1 万 ops/s
- 最大:约 33.8 万 ops/s
OPS = Operations Per Second,每秒操作数,相当于 QPS。
3.3 结果分析
从测试数据来看:
- 无 Sentinel:平均约 30.1 万 ops/s
- 有 Sentinel:平均约 31.0 万 ops/s
- 差异:约 3% 左右
等等,有 Sentinel 反而更快?这显然不合理。原因可能是:
- 测试的业务方法本身比较重(shuffle + sort)
- Sentinel 的开销相对于业务逻辑可以忽略不计
- 测试误差范围内的波动
但至少说明一点:在超过 28 万 QPS 的场景下,Sentinel 的性能影响也只有约 3%,甚至在误差范围内。
而实际项目中,单个服务节点所有接口加起来的总 QPS 很难达到 25 万。QPS 越低,Sentinel 的相对影响就越小。
3.4 注意事项
这个测试结果是理想情况下的参考值,实际情况可能不同:
- 没有配置任何限流规则:配置规则后会有额外的规则匹配开销
- 只有一个资源:资源越多,规则匹配的开销越大
- 调用链路深度为 1:嵌套调用越深,Sentinel 的调用次数越多
所以:以实际项目中的压测结果为准。
但无论如何,Sentinel 的性能开销对于绝大多数业务场景来说都是可以接受的。
四、总结与实践建议
4.1 Sentinel 性能优化手段回顾
| 优化点 | 实现方式 | 效果 |
| 滑动窗口复用 | 循环复用 Bucket | 减少对象创建销毁 |
| 并发统计 | LongAdder | 高并发下统计更快 |
| 时间戳缓存 | 定时任务维护 | 减少 System.currentTimeMillis() 调用 |
| 读多写少 Map | 写时复制 + 普通 Map | 读操作完全无锁 |
| 匀速限流 | 毫秒级精度 | 牺牲精度换性能 |
| 冷启动令牌 | 每秒更新一次 | 减少 CAS 操作 |
4.2 实际使用建议
虽然 Sentinel 的性能已经很好了,但在使用时还是可以注意一些细节,进一步降低影响:
1. 合理控制资源数量
- 资源名不要用动态生成的字符串(如带 ID 的 URL)
- 使用 UrlCleaner 将 RESTful URL 归一化
- 资源越多,规则匹配和统计的开销越大
2. 合理配置规则数量
- 只给核心接口配置规则
- 利用系统自适应限流做全局兜底,不用每个接口都配
- 规则越多,每次请求的规则遍历开销越大
3. 注意调用链路深度
- 避免过深的嵌套 SphU.entry 调用
- 每次 entry/exit 都有开销,链路越深累积开销越大
4. 选择合适的流控效果
- 快速失败性能最好(默认)
- 匀速排队会有线程 sleep 的开销
- 冷启动有令牌桶计算开销
5. 结合实际压测
- 上线前压测,评估 Sentinel 对系统的实际影响
- 关注高并发场景下的 CPU 和内存占用
- 对比开启/关闭 Sentinel 时的吞吐量差异
4.3 最终结论
Sentinel 的性能表现:
- 低 QPS 场景(几万以内):损耗几乎可以忽略不计
- 中 QPS 场景(十万级):损耗在 1%~3% 左右
- 高 QPS 场景(25 万以上):损耗约 5%~10%
对于绝大多数业务系统来说,Sentinel 带来的性能损耗远远小于它所提供的价值。服务保护是一项必要的投入,而 Sentinel 已经把这项投入的成本降到了很低的水平。
全文总结
到这里,我们的 Sentinel 深入理解系列就接近尾声了。从最初的服务雪崩问题,到滑动窗口、责任链模式、SPI 机制,再到限流、熔断、系统自适应限流、热点参数限流、自定义扩展、动态数据源、框架适配、集群限流……我们一起走完了 Sentinel 核心实现的完整旅程。
Sentinel 值得我们学习的不仅是功能,还有它的设计思想:
- 扩展性:SPI 机制、Slot 责任链、动态数据源
- 性能优先:各种细节上的性能考量和取舍
- 解耦设计:统计、限流、熔断各模块职责清晰
- 框架适配:利用各框架的扩展点实现低侵入集成
希望通过这个系列,你不仅学会了 Sentinel 的使用和原理,更能从中汲取优秀框架的设计思路,应用到自己的项目中。
最后一篇番外,我们将聊聊 Sentinel 1.8.0 版本在熔断降级上的新特性,看看 Sentinel 是如何持续进化的。












请登录后查看评论内容