Sentinel在生产环境的真实落地:为什么我们最终放弃了Dashboard?

10 阅读13分钟

写在前面

用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依赖版本,结果发现:

  1. Dashboard的代码和Nacos 1.x深度耦合
  2. 升级后各种API都不兼容
  3. 改起来工作量巨大,风险也高

整整折腾了两天,最后我放弃了。

柳暗花明:直接用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生效,需要:

  1. 在请求中加上来源标识(比如某个Header)
  2. 实现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

这种设计有几个好处:

  1. 职责清晰 - 每个Slot只干一件事
  2. 易于扩展 - 用的是SPI机制,可以自定义Slot
  3. 性能好 - 链式调用,开销很小

滑动窗口算法

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)

  • 优点: 统一在网关处理,业务应用不用关心
  • 缺点: 网关成为单点,而且有些场景需要业务层限流
  • 实现复杂度: 中等

我们的选择:单机限流足够了

经过评估,我们最终选择了单机限流,原因是:

  1. 我们用了负载均衡 - Nginx会自动把流量均匀分配到各个实例
  2. 限流不是绝对精确 - 总限流300还是330,其实差别不大,只要不让后端服务被打垮就行
  3. 简单可靠 - 不依赖额外组件,出问题的概率更小
  4. 性能更好 - 不需要网络调用,延迟更低

如果真的需要精确的分布式限流,我会考虑用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是个好组件,核心设计很优雅,但周边生态确实有些问题。

这次实践的经历让我明白:

  1. 不要盲目相信官方文档 - 有些重要信息文档里根本不提
  2. 不要过度追求完美方案 - 够用就行,简单可靠最重要
  3. 多看源码,少走弯路 - 很多问题在源码里都能找到答案
  4. 技术选型要考虑长期维护 - 一个不更新的项目,未必是坏事,但也要提前做好准备

如果你也在用Sentinel,或者准备引入限流组件,希望这篇文章能给你一些参考。

当然,每个公司的场景不一样,我们的方案未必适合你。比如:

  • 如果你们的流量分布很不均匀,可能需要分布式限流
  • 如果你们有专门的中间件团队,可以考虑改造Dashboard
  • 如果对限流精度要求很高,可能需要用Redis方案

技术没有银弹,只有最合适的。

如果你在使用Sentinel的过程中遇到了什么问题,或者有更好的实践经验,欢迎在评论区讨论。我也还在摸索中,很多地方理解得可能不够深入,一起交流进步。

另外,如果你对限流的其他方案感兴趣(比如Redis+Lua、Nginx令牌桶、或者自研限流组件),我后面可以单独写一篇文章深入对比一下。


希望这篇文章对你有帮助,点个赞再走呗~