写在前面
用Sentinel也有一段时间了,回头看看当初的一些决策,真是有点哭笑不得。今天就来聊聊我们团队在使用Sentinel时踩过的几个坑,希望能帮大家少走点弯路。这不是什么高大上的架构设计,就是真实的踩坑经历。
事情是这样的
上个月老板突然找我:"老王啊,咱们的订单接口最近QPS涨得有点猛,经常把下游服务打挂。你研究一下怎么加个限流?"
我一想,这不是常见需求吗?立马开始技术选型:
- Guava RateLimiter - 太简单了,功能不够
- Resilience4j - 看起来不错,但国内用的人少
- Sentinel - 阿里开源的,Spring Cloud Alibaba官方推荐,就它了!
说干就干,撸起袖子开始搞。
初体验:Dashboard真香!
一开始我按照官方文档,很快就把Sentinel集成进来了:
项目环境:
- Spring Boot: 3.0.2
- JDK: 17
- Spring Cloud Alibaba: 2022.0.0.0
- Sentinel: 1.8.6
添加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置文件:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
然后启动Sentinel Dashboard,在浏览器打开 http://localhost:8080,看到界面的那一刻,我真的觉得太香了!
可视化配置限流规则,点点鼠标就搞定:
- 资源名:test_flow_rule
- 阈值类型:QPS
- 单机阈值:1
- 流控效果:快速失败
测试一下,效果完美!
# 第一次请求
curl http://localhost:8081/test_flow_rule
# 返回: {"code":200,"msg":"success"}
# 快速刷新第二次
curl http://localhost:8081/test_flow_rule
# 返回: Blocked by Sentinel (flow limiting)
我心想:这也太简单了吧,半小时搞定!赶紧给老板汇报,老板也很满意。
然后我就高高兴兴下班了。
第一个坑:配置第二天就丢了!
第二天早上来公司,照例先看一下监控。咦,怎么限流规则没生效?
赶紧打开Dashboard一看,好家伙,昨天配置的规则全没了!
我当时就慌了,难道是Dashboard挂了?重启了一下,规则还是空的。
开始疯狂查日志、查文档,终于在官方文档的某个角落里看到一句话:
Dashboard中的配置仅保存在内存中,重启后会丢失。如需持久化,请使用动态规则数据源。
What?!这么重要的信息为什么不放在显眼的地方!
好吧,那就持久化呗。文档说可以持久化到Nacos、Apollo、Zookeeper等。我们项目已经在用Nacos了,那就用Nacos吧。
尝试改造Dashboard
我去GitHub翻了一下Sentinel Dashboard的源码,发现默认确实只存内存。网上有一些改造教程,说可以把Dashboard的配置推送到Nacos。
我心想:这不就是加几个API调用吗?应该不难。
于是开始动手改造。
然后我就发现了一个致命问题:
Dashboard依赖的Nacos版本是1.4.1,而我们项目用的是Nacos 2.x!
这有什么问题?问题大了!
Nacos从2.0开始,底层通信协议从HTTP改成了gRPC。这意味着Dashboard的那套HTTP调用方式,在Nacos 2.x上根本行不通!
我尝试升级Dashboard的Nacos依赖版本,结果发现:
- Dashboard的代码和Nacos 1.x深度耦合
- 升级后各种API都不兼容
- 改起来工作量巨大,风险也高
整整折腾了两天,最后我放弃了。
柳暗花明:直接用Nacos配置
后来我静下心来想:既然Dashboard这条路走不通,那能不能直接用代码从Nacos读配置?
翻了一下Sentinel的文档,还真有这个功能!而且实现起来比改造Dashboard简单多了。
添加Nacos数据源依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
配置Nacos数据源:
spring:
cloud:
sentinel:
datasource:
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow
在Nacos中配置限流规则:
[
{
"resource": "test_flow_rule",
"count": 1.0,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
]
重启应用,限流规则生效了!而且在Nacos中修改配置,应用立即就能生效,根本不需要重启。
这才是正确的姿势啊!
此时的架构是这样的:
graph LR
A[业务应用] -->|读取规则| B[Nacos]
C[开发人员] -->|配置规则| B
A -->|限流拦截| D[Sentinel]
style B fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#bbf,stroke:#333,stroke-width:2px
Dashboard?什么Dashboard?不需要了!
第二个坑:limitApp怎么都不生效!
解决了持久化问题,我开始研究Sentinel的一些高级功能。
看到文档里说,可以通过limitApp参数来针对不同的调用方进行限流。这个功能挺好啊,我们有好几个系统会调用同一个接口,如果能针对不同系统分别限流就完美了。
于是我在Nacos配置里加了limitApp参数:
[
{
"resource": "test_flow_rule",
"count": 1.0,
"grade": 1,
"limitApp": "order-service", // 只限制来自order-service的调用
"strategy": 0,
"controlBehavior": 0
}
]
重启应用,测试...咦,怎么完全不生效?
无论是order-service调用还是其他服务调用,都没有被限流。这是什么情况?
Debug源码,找到真相
没办法,只能Debug源码了。我花了整整一天时间,跟着代码一行一行看。
终于在FlowSlot这个类里找到了答案:
// Sentinel的源码逻辑
String origin = context.getOrigin();
if (rule.getLimitApp().equals(origin)
|| rule.getLimitApp().equals(RuleConstant.LIMIT_APP_DEFAULT)) {
// 匹配成功,执行限流
}
原来Sentinel是从context中获取调用方信息,然后和limitApp进行匹配。
那这个origin是从哪来的呢?继续跟代码:
// 在RequestOriginParser中
String origin = parseOrigin(request);
if (StringUtil.isNotEmpty(origin)) {
ContextUtil.enter(resourceName, origin);
}
问题找到了!
Sentinel需要从HTTP请求中解析出调用方信息,但默认实现是返回空字符串。我们的请求里根本就没有这个信息,所以永远匹配不上!
要让limitApp生效,需要:
- 在请求中加上来源标识(比如某个Header)
- 实现
RequestOriginParser接口来解析这个标识
但这对我们的场景来说太重了。各个系统都要改代码加Header,改造成本太高。
放弃limitApp
最后我的解决方案很简单:去掉limitApp配置,改用resource粒度的限流。
[
{
"resource": "test_flow_rule",
"count": 1.0,
"grade": 1,
"limitApp": "default", // 改回default
"strategy": 0,
"controlBehavior": 0
}
]
虽然不能针对不同调用方限流了,但至少能用。而且对于我们的场景来说,统一限流其实也够了。
有时候,不要追求完美的方案,够用就行。
第三个问题:版本兼容真的很头疼
这个问题其实从一开始就在踩,只是当时没意识到。
Sentinel的版本管理真的很混乱:
- Spring Boot有很多版本
- Spring Cloud有很多版本
- Spring Cloud Alibaba有很多版本
- Sentinel本身也有很多版本
这几个版本之间的对应关系,官方文档几乎没有说明!
我一开始用的版本组合:
- Spring Boot 3.0.2
- Spring Cloud 2022.0.0
- Spring Cloud Alibaba 2022.0.0.0
- Sentinel 1.8.6
结果启动时各种奇怪的报错:
- ClassNotFoundException
- NoSuchMethodError
- 依赖冲突
我在网上搜了半天,找了一堆文章,发现大家用的版本都不一样,而且很多文章都过时了。
最后是靠反复尝试,一点点调整版本号,才找到一个能用的组合。
整整浪废了一天时间!
这种感觉就像在黑暗中摸索,真的很让人抓狂。
关于Sentinel的现状:更新很慢了
用了一段时间Sentinel后,我发现一个问题:这个项目更新得非常慢。
去GitHub上看了一下:
- 最近几年的版本更新频率越来越低
- 很多issue都没人回复
- 一些明显的bug也没修复
这让我开始思考:Sentinel还适合长期使用吗?
后来和几个做中间件的朋友聊,他们说阿里内部现在已经不怎么用Sentinel了,更多的是用自研的限流组建。开源的Sentinel基本处于"维护状态",不会有大的功能更新。
这也能解释为什么Dashboard的设计这么鸡肋,为什么文档这么不完善。
不过话说回来,对于我们的场景,Sentinel的现有功能已经够用了。它不更新,反而说明这套方案比较稳定。
Sentinel的优点(终于可以说说好话了)
虽然踩了这么多坑,但客观来说,Sentinel确实有它的优点。
在研究这些问题的过程中,我花了不少时间看Sentinel的源码。不得不说,Sentinel的核心设计真的很优雅。
SlotChain的设计
Sentinel的核心是一个SlotChain,请求会依次经过一系列的Slot:
graph LR
A[请求] --> B[NodeSelectorSlot]
B --> C[ClusterBuilderSlot]
C --> D[StatisticSlot]
D --> E[SystemSlot]
E --> F[AuthoritySlot]
F --> G[FlowSlot]
G --> H[DegradeSlot]
style D fill:#afa,stroke:#333,stroke-width:2px
style G fill:#faa,stroke:#333,stroke-width:2px
style H fill:#faa,stroke:#333,stroke-width:2px
每个Slot职责单一:
- 统计相关(4个): NodeSelectorSlot、ClusterBuilderSlot、LogSlot、StatisticSlot
- 规则判断(4个): SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot
这种设计有几个好处:
- 职责清晰 - 每个Slot只干一件事
- 易于扩展 - 用的是SPI机制,可以自定义Slot
- 性能好 - 链式调用,开销很小
滑动窗口算法
Sentinel的限流算法用的是滑动窗口,而不是简单的计数器。
graph TD
A[1秒钟] -->|分成| B[2个时间窗口]
B --> C[500ms窗口1]
B --> D[500ms窗口2]
C --> E[计数: 3]
D --> F[计数: 2]
E --> G[总计: 5]
F --> G
style G fill:#faa,stroke:#333,stroke-width:2px
这个设计比固定窗口更精准,能有效避免"突刺"问题。
代码也写得很清晰,可读性很好:
// 伪代码展示核心逻辑
public class LeapArray<T> {
// 滑动窗口数组
private AtomicReferenceArray<WindowWrap<T>> array;
public WindowWrap<T> currentWindow() {
long timeId = System.currentTimeMillis() / windowLengthInMs;
int idx = (int)(timeId % array.length());
// 获取或创建当前窗口
WindowWrap<T> old = array.get(idx);
if (old == null || !old.isTimeInWindow(time)) {
// 创建新窗口
}
return old;
}
}
降级的状态机设计
Sentinel的降级功能用了一个状态机,包含三个状态:
stateDiagram-v2
[*] --> CLOSED: 初始状态
CLOSED --> OPEN: 触发降级条件
OPEN --> HALF_OPEN: 恢复时间到
HALF_OPEN --> CLOSED: 探测成功
HALF_OPEN --> OPEN: 探测失败
note right of CLOSED: 正常放行
note right of OPEN: 熔断所有请求
note right of HALF_OPEN: 允许少量请求探测
这个设计和Hystrix很像,但实现更轻量。
看完源码之后,我有个想法:如果以后要自研限流组件,可以直接把Sentinel的核心逻辑抽取出来用。
整套代码写得很规范,SPI扩展机制也很完善,完全可以作为自研限流功能的底座。
分布式限流:真的需要吗?
在研究Sentinel的过程中,我也看了文档里提到的分布式限流方案。理论上,Sentinel可以通过Token Server实现集群限流。
但说实话,文档写得很模糊,源码里也没找到Token Server的具体实现。而且即使实现了,还要额外维护一个Token Server,增加了复杂度。
后来我想了想,对于我们的场景,真的需要分布式限流吗?
单机限流 vs 分布式限流
假设我们有3个应用实例,每个实例限流100 QPS:
单机限流:
- 每个实例独立限流100 QPS
- 总体可承受 3 × 100 = 300 QPS
- 如果流量分布不均,可能某个实例被打满,其他实例还有余量
分布式限流:
- 所有实例共享一个限流配额300 QPS
- 需要引入Redis或者Token Server
- 增加了网络开销和单点风险
graph TB
subgraph 单机限流
A1[实例1<br/>限流100]
A2[实例2<br/>限流100]
A3[实例3<br/>限流100]
end
subgraph 分布式限流
B1[实例1]
B2[实例2]
B3[实例3]
R[Redis/Token Server<br/>总限流300]
B1 -.->|请求令牌| R
B2 -.->|请求令牌| R
B3 -.->|请求令牌| R
end
style R fill:#faa,stroke:#333,stroke-width:2px
对比一下常见的分布式限流方案:
方案1: Redis + Lua脚本
- 优点: Redis本身就在用,不用引入新组件
- 缺点: 增加Redis压力,而且Redis挂了限流就失效
- 实现复杂度: 中等
方案2: Nginx限流
- 优点: 在网关层限流,保护后端服务
- 缺点: 限流逻辑在Nginx,不够灵活,修改配置要reload
- 实现复杂度: 低
方案3: Sentinel集群限流
- 优点: 和Sentinel无缝集成
- 缺点: 文档不完善,需要额外维护Token Server
- 实现复杂度: 高
方案4: 网关层限流(如Spring Cloud Gateway)
- 优点: 统一在网关处理,业务应用不用关心
- 缺点: 网关成为单点,而且有些场景需要业务层限流
- 实现复杂度: 中等
我们的选择:单机限流足够了
经过评估,我们最终选择了单机限流,原因是:
- 我们用了负载均衡 - Nginx会自动把流量均匀分配到各个实例
- 限流不是绝对精确 - 总限流300还是330,其实差别不大,只要不让后端服务被打垮就行
- 简单可靠 - 不依赖额外组件,出问题的概率更小
- 性能更好 - 不需要网络调用,延迟更低
如果真的需要精确的分布式限流,我会考虑用Redis + Lua,而不是Sentinel的Token Server。
因为Redis我们本来就在用,运维成本低。而Token Server还要单独部署和监控,增加复杂度。
我们的最终方案
经过这一番折腾,我们最终确定的方案是:
1. 不使用Dashboard
- 优点:减少依赖,不需要开8719端口
- 缺点:没有可视化界面(但我们也用不上)
2. 使用Nacos持久化配置
- 规则配置在Nacos中
- 应用启动时自动加载
- 支持在线动态修改
3. 单机限流模式
- 不使用分布式限流(Token Server)
- 每个实例独立限流
- 配置简单,够用
4. 不依赖Sentinel的统计功能
- Sentinel的统计不持久化
- 我们用自己的日志和监控系统
整体架构:
graph TB
subgraph 应用集群
A1[应用实例1<br/>限流:100QPS]
A2[应用实例2<br/>限流:100QPS]
A3[应用实例3<br/>限流:100QPS]
end
B[Nacos配置中心]
C[运维人员]
D[监控系统]
C -->|修改限流规则| B
B -->|推送配置| A1
B -->|推送配置| A2
B -->|推送配置| A3
A1 -->|上报指标| D
A2 -->|上报指标| D
A3 -->|上报指标| D
style B fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#afa,stroke:#333,stroke-width:2px
检验配置是否加载成功:
添加Actuator依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置暴露端点:
management:
endpoints:
web:
exposure:
include: "*"
访问: http://localhost:8081/actuator/sentinel
能看到规则信息就说明加载成功了。
几点建议
经过这次折腾,我总结了几条使用Sentinel的建议:
1. 生产环境不要用Dashboard
理由:
- 配置不持久化,容易丢
- 需要改造才能对接Nacos 2.x,成本高
- 多开一个端口(8719),增加复杂度
直接用Nacos配置更靠谱。
2. 不要迷信所有功能都要用
比如limitApp、分布式限流这些功能,看起来很酷,但实际场景中可能用不上。
根据实际需求选择功能,不要过度设计。
3. 版本选择要谨慎
强烈建议:
- 先去Spring Cloud Alibaba的官方文档查版本对应表
- 如果文档不全,去GitHub的issues里搜
- 实在不行,找个靠谱的开源项目,看他们用什么版本
不要盲目用最新版,稳定性更重要。
4. 读一读源码
Sentinel的源码真的值得一看:
- 代码质量高,注释清晰
- 设计模式用得好
- 核心逻辑不复杂,容易理解
如果要深度使用,建议花几天时间看看核心代码。
5. 做好监控和日志
Sentinel本身的统计功能比较弱,而且不持久化。
建议:
- 自己记录被限流的日志
- 把限流指标上报到监控系统
- 定期分析限流数据,调整阈值
写在最后
Sentinel是个好组件,核心设计很优雅,但周边生态确实有些问题。
这次实践的经历让我明白:
- 不要盲目相信官方文档 - 有些重要信息文档里根本不提
- 不要过度追求完美方案 - 够用就行,简单可靠最重要
- 多看源码,少走弯路 - 很多问题在源码里都能找到答案
- 技术选型要考虑长期维护 - 一个不更新的项目,未必是坏事,但也要提前做好准备
如果你也在用Sentinel,或者准备引入限流组件,希望这篇文章能给你一些参考。
当然,每个公司的场景不一样,我们的方案未必适合你。比如:
- 如果你们的流量分布很不均匀,可能需要分布式限流
- 如果你们有专门的中间件团队,可以考虑改造Dashboard
- 如果对限流精度要求很高,可能需要用Redis方案
技术没有银弹,只有最合适的。
如果你在使用Sentinel的过程中遇到了什么问题,或者有更好的实践经验,欢迎在评论区讨论。我也还在摸索中,很多地方理解得可能不够深入,一起交流进步。
另外,如果你对限流的其他方案感兴趣(比如Redis+Lua、Nginx令牌桶、或者自研限流组件),我后面可以单独写一篇文章深入对比一下。
希望这篇文章对你有帮助,点个赞再走呗~