服务保障 Sentinel
项目默认没有接入 Sentinel。yudao-spring-boot-starter-protection (opens new window) 组件本身也不包含 Sentinel,它提供的是基于 Redis(Redisson)的 《请求限流(RateLimiter)》、《幂等性》、《分布式锁》。
需要在网关、服务中按 QPS 限流,按慢调用、异常比例熔断,并且在运行时通过 Nacos 动态调整规则时,可以基于 Sentinel (opens new window) + Spring Cloud Alibaba 按需接入,实际使用的依赖版本见《3.3 添加依赖》小节:
- 只有接入的服务(示例是
gateway-server、system-server)才会创建 Sentinel 资源。没有规则的资源只统计、不拦截,其它服务不受影响。 - 规则保存在 Nacos 中,服务启动时读取、运行时监听变更,Nacos 是规则的唯一来源,参见《4.5 规则持久化》小节。
- 被 Sentinel 阻断时,网关、服务都返回 HTTP 429 或 503,以及
CommonResult格式的 JSON;Feign 只在显式配置了fallbackFactory的客户端上降级,项目已有的 Feign 客户端行为不变。
关于社区的 PR
PR #138 (opens new window)(网关、服务集成 Sentinel,规则持久化到 Nacos)、PR #154 (opens new window)(Feign 统一 fallback)提供了很多思路,感谢参与讨论的同学!本文的材料借鉴了其中的网关接入、Nacos 持久化方式,但没有合并这两个 PR:规则改为严格的 JSON,被阻断时返回 CommonResult;Feign 保持 Sentinel 原生的 SentinelFeign,不替换全局的 Feign.Builder,也不为所有 Feign 客户端注入默认的 fallback,业务错误码仍然由调用方的 getCheckedData() 处理。
# 1. 方案说明
# 1.1 与 @RateLimiter 的区别
两者解决的问题不同,可以同时使用:
@RateLimiter | Sentinel | |
|---|---|---|
| 计数位置 | Redis,多个实例共享同一个计数,是全局的配额 | 每个 JVM 进程的内存,阈值是单机的,参见《4.6 单机阈值》小节 |
| 限流维度 | 时间窗口内最多 N 次,可以按全局、用户、IP、服务节点、Spring EL 表达式分别计数 | 按资源的 QPS 或并发线程数,支持快速失败、预热、匀速排队;网关可以按路由、API 分组 |
| 熔断降级 | 不支持 | 慢调用比例、异常比例、异常数 |
| 规则 | 注解的属性,修改后需要重新发布 | 与代码分离,保存在 Nacos 中,运行时修改即时生效 |
| 外部依赖 | Redis | 无,Dashboard、Nacos 用于监控和规则管理 |
| 被限制时 | 抛出 ServiceException,HTTP 200 + code = 429 | 示例返回 HTTP 429/503 + CommonResult,参见《4.7 阻断响应与前端》小节 |
简单来说:「每个用户每分钟只能发送 1 次短信」这类业务配额,使用 @RateLimiter;「保护服务实例不被流量压垮、下游变慢时快速失败」这类流量防护,使用 Sentinel。
# 1.2 整体结构
curl / 前端
│ GET /admin-api/system/sentinel-demo/**
▼
gateway-server(网关)
SentinelGatewayFilter:按 API 分组 system-sentinel-demo-api、路由 system-admin-api 统计,执行 gw-flow 规则
被阻断 → DemoSentinelGatewayConfiguration 的 BlockRequestHandler:HTTP 429/503 + CommonResult
│
▼
system-server(调用方)
SentinelWebInterceptor:URL 资源 /admin-api/system/sentinel-demo/flow,执行 flow 规则
被阻断 → DemoSentinelConfiguration 的 BlockExceptionHandler:HTTP 429/503 + CommonResult
DemoSentinelController#feign
└── DemoSentinelInfraApi#echo(SentinelFeign):Feign 资源 GET:http://infra-server/rpc-api/infra/sentinel-demo/echo,执行 degrade 规则
调用失败、被阻断 → DemoSentinelInfraApiFallbackFactory:返回 CommonResult 错误码
│
infra-server(被调用方,未接入 Sentinel)
DemoSentinelRpcController#echo:mode = ok 正常、slow 慢调用、biz-error 业务错误
Nacos(命名空间 dev,分组 SENTINEL_GROUP):4 个规则配置,服务启动时读取、运行时监听
Sentinel Dashboard(可选):实时监控;在其中修改的规则只是临时的,不会写回 Nacos
# 1.3 限流、熔断与 Feign 降级
这三者经常被混在一起,作用并不相同:
① 限流(FlowRule、网关的 GatewayFlowRule):保护被调用方。单位时间内超过阈值的请求直接拒绝。网关限流在请求进入服务之前,服务限流保护单个服务实例。
② 熔断(DegradeRule):保护调用方。下游的慢调用比例、异常比例超过阈值时,在熔断时长内直接拒绝调用,不再等待下游超时;熔断时长结束后,放行一个探测请求,成功则恢复,失败则继续熔断。示例在 system-server 中,为调用 infra-server 的 Feign 资源配置熔断规则。
③ Feign 降级(fallbackFactory):决定调用失败、或者被限流熔断阻断后,调用方的代码得到什么结果。没有配置 fallback 时,原来的异常照常抛出;配置后,返回 fallback 的结果。
也就是说,熔断决定「要不要调用下游」,fallback 决定「不调用、或调用失败时返回什么」,两者是独立的。
# 1.4 示例
示例的接口都在 system-server 中,需要登录,通过网关访问:
| 接口 | 规则所在服务 | 资源 | 规则 | 被阻断时 |
|---|---|---|---|---|
GET /admin-api/system/sentinel-demo/gateway-flow | gateway-server | API 分组 system-sentinel-demo-api | 限流,QPS 2 | HTTP 429,code = 429 |
GET /admin-api/system/sentinel-demo/flow | system-server | URL /admin-api/system/sentinel-demo/flow | 限流,QPS 2 | HTTP 429,code = 429 |
GET /admin-api/system/sentinel-demo/feign?mode=ok | system-server | Feign GET:http://infra-server/rpc-api/infra/sentinel-demo/echo | 熔断,慢调用比例 | fallback 的错误码:HTTP 200,code = 503 |
另外,gateway-server 还为路由 system-admin-api(即 /admin-api/system/**)配置了 QPS 200 的限流规则,演示按 routeId 限流。正常访问不会触发,用于《3.6 启动验证》中动态修改规则。
# 2. 获取集成材料
集成材料在项目的 script/sentinel/ (opens new window) 目录。目前只有 master-jdk17 分支提供这些材料,master、master-jdk25 分支请先从 master-jdk17 分支获取,同步到对应分支后,也可以直接从当前分支获取。材料按下面三个分支的 API 编写,接入步骤相同,差异如下:
| 分支 | JDK | Spring Boot | Spring Cloud Alibaba | Sentinel | 差异 |
|---|---|---|---|---|---|
master | 8 | 2.7.18 | 2021.0.6.2 | 1.8.6 | DemoSentinelConfiguration 的 BlockExceptionHandler 需要修改,见《3.5 复制示例代码》小节 |
master-jdk17 | 17 | 3.5.15 | 2025.0.0.0 | 1.8.9 | 无 |
master-jdk25 | 25 | 4.1.2 | 2025.1.0.0 | 1.8.9 | 使用 Feign 的服务(例如说 system-server、infra-server)只有 Pageable、没有 DataWebProperties 时,按条件添加 dependencies-jdk25.xml,见《3.3 添加依赖》小节 |
Sentinel 的版本由项目已有的 Spring Cloud Alibaba BOM 管理,不需要、也不建议单独指定。master-jdk25 分支,Sentinel 官方没有声明对 Spring Cloud Gateway 5、Spring Framework 7 的适配,接入后以实际的启动、请求验证为准。
目录中的文件及用途如下:
| 文件 | 说明 | 目标位置 |
|---|---|---|
dependencies.xml | Maven 依赖片段,分为 2 段 | 第 1 段:yudao-gateway/pom.xml、yudao-module-system-server/pom.xml;第 2 段:只添加到 yudao-gateway/pom.xml |
dependencies-jdk25.xml | 仅 master-jdk25 分支按条件补充的 spring-boot-data-commons 依赖片段 | 满足条件时,添加到对应服务(例如说 system-server、infra-server)的 pom.xml,gateway-server 不适用 |
sentinel.yaml | 按服务分为两段的配置片段 | 分别合并到 gateway-server、system-server 的 application-local.yaml 等配置文件,或 Nacos 配置 |
nacos/*.json | 4 个规则文件,文件名就是 Nacos 中的 Data ID | 在 Nacos 中创建同名配置 |
src/main/java/.../gateway/sentineldemo/ | 网关被阻断时的 BlockRequestHandler | yudao-gateway/src/main/java/ 的同名包 |
src/main/java/.../system/sentineldemo/ | 配置类、Feign 客户端及其 fallbackFactory、示例接口 | yudao-module-system-server/src/main/java/ 的同名包 |
src/main/java/.../infra/sentineldemo/ | 被调用的 RPC 接口,模拟慢调用、业务错误 | yudao-module-infra-server/src/main/java/ 的同名包 |
# 3. 接入步骤
友情提示:以 `gateway-server` 网关 + `system-server` 调用 `infra-server` 为例子。
示例代码的包名分别是 cn.iocoder.yudao.gateway.sentineldemo、cn.iocoder.yudao.module.system.sentineldemo、cn.iocoder.yudao.module.infra.sentineldemo,在对应服务的扫描范围内。接入其它服务时,需要将包名改到对应服务的 base-package 下。
# 3.1 启动 Sentinel Dashboard(可选)
Sentinel Dashboard (opens new window) 用于查看实时的 QPS、阻断次数,以及资源名,不是必须的:不启动 Dashboard,Nacos 中的规则照常生效。
从 Sentinel 的 Releases (opens new window) 下载 1.8.x 版本的 sentinel-dashboard-1.8.10.jar,它是独立的 Spring Boot 应用,与项目使用的 JDK 版本无关。设置好 SENTINEL_DASHBOARD_PASSWORD 环境变量后启动:
java -Dserver.address=127.0.0.1 -Dserver.port=8858 \
-Dsentinel.dashboard.auth.username=sentinel \
-Dsentinel.dashboard.auth.password="${SENTINEL_DASHBOARD_PASSWORD}" \
-jar sentinel-dashboard-1.8.10.jar
-Dserver.address=127.0.0.1:只在本机监听,服务与 Dashboard 运行在同一台机器上。-Dserver.port=8858:Dashboard 默认使用 8080 端口,改为 8858,避免与其它程序冲突。sentinel.dashboard.auth.*:Dashboard 的登录账号,默认是sentinel/sentinel,不要在共享环境中使用默认密码。
浏览器访问 http://127.0.0.1:8858 (opens new window) 登录。服务启动时设置 YUDAO_SENTINEL_DASHBOARD=127.0.0.1:8858 环境变量,才会连接 Dashboard,参见《3.4 添加配置》小节。
# 3.2 在 Nacos 中创建规则
在 Nacos 控制台中,选择服务使用的命名空间(项目默认是 dev,即 spring.cloud.nacos.config.namespace),创建下面 4 个配置。配置内容就是 script/sentinel/nacos/ 目录下同名文件的内容,配置格式选择 JSON:
| Data ID | Group | 使用的服务 | 说明 |
|---|---|---|---|
gateway-server-gw-api-group-rules.json | SENTINEL_GROUP | gateway-server | 自定义 API 分组 |
gateway-server-gw-flow-rules.json | SENTINEL_GROUP | gateway-server | 网关流控规则 |
system-server-flow-rules.json | SENTINEL_GROUP | system-server | 限流规则 |
system-server-degrade-rules.json | SENTINEL_GROUP | system-server | 熔断规则 |
注意事项:
- Data ID 包含
.json后缀,与文件名完全相同;Group 是SENTINEL_GROUP,与服务自己的配置(DEFAULT_GROUP)分开。 - 命名空间要选择服务实际使用的那个。配置中填写的是命名空间的 ID,项目文档中
dev命名空间的 ID、名字都是dev;两者不同时,以 ID 为准。 - 内容必须是严格的 JSON,不能包含
//注释、结尾多余的逗号。解析失败时,该配置的规则不会生效。各字段的含义,参见《4. 使用说明》。
# 3.3 添加依赖
① 将 dependencies.xml 第 1 段的依赖,复制到 yudao-gateway、yudao-module-system-server 两个服务的 pom.xml 中:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
② 将 dependencies.xml 第 2 段的依赖,只复制到 yudao-gateway 的 pom.xml 中:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
说明:
- 都不写版本,由项目已有的 Spring Cloud Alibaba BOM 管理,对应的 Sentinel 版本见《2. 获取集成材料》小节。
spring-cloud-starter-alibaba-sentinel中的 Servlet 适配,只在 Servlet 应用中生效,它依赖的servlet-api、spring-webmvc是provided的,不会给网关引入spring-boot-starter-web等 Servlet 组件。可以通过mvn dependency:tree -pl yudao-gateway -Dincludes=org.springframework:spring-webmvc确认。infra-server只提供被调用的 RPC 接口,不需要添加上面的 Sentinel 依赖;master-jdk25分支它是否需要 ③ 的依赖,按 ③ 的条件单独判断。
③ 仅 master-jdk25 分支(Spring Boot 4.1.2 + Spring Cloud OpenFeign 5.0.2):适用于每个使用 Feign 的服务,包括已有、新增的 Feign 客户端,例如说 system-server(新增了 DemoSentinelInfraApi)、infra-server(已有 TenantCommonApi 等);gateway-server 不使用 Feign,不适用。该服务启动时的 classpath 中有 org.springframework.data.domain.Pageable,却没有 spring-boot-data-commons 模块的 org.springframework.boot.data.autoconfigure.web.DataWebProperties 时,OpenFeign 不会创建任何 Encoder,启动报 No bean found of type interface feign.codec.Encoder for xxx(xxx 是 Feign 客户端的名字,例如说 demoSentinelInfraApi)。此时,才将 dependencies-jdk25.xml 中的依赖添加到该服务的 pom.xml,版本由 Spring Boot BOM 管理:
<!-- 仅 master-jdk25 分支,且使用 Feign 的服务只有 Pageable、没有 DataWebProperties 时添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-data-commons</artifactId>
</dependency>
这个条件与 Sentinel 无关,不要无条件添加:已经有该模块、或者没有 Pageable 时都不需要,以每个服务实际的 classpath 为准;master、master-jdk17 分支不使用该文件。
# 3.4 添加配置
sentinel.yaml 按服务分为两段,分别合并到对应服务的 application-local.yaml 配置文件中。spring.cloud.sentinel 合并到已有的 spring.cloud 下,不要重复写一个 spring.cloud。
① gateway-server:
spring:
cloud:
sentinel:
eager: true # 启动时就初始化 Sentinel、连接 Dashboard;默认 false,第一次请求时才初始化
transport:
dashboard: ${YUDAO_SENTINEL_DASHBOARD:} # Sentinel Dashboard 地址,例如说 127.0.0.1:8858;为空时不连接 Dashboard,规则照常生效
port: 8719 # Sentinel 的命令端口,没有鉴权,只能在内网访问;同一台机器的多个服务需要不同端口,被占用时 Sentinel 会自动递增
datasource:
gw-api-group: # 自定义 API 分组:定义哪些请求路径属于一个 API,供 gw-flow 规则按 API 限流
nacos:
server-addr: ${spring.cloud.nacos.server-addr}
namespace: ${spring.cloud.nacos.config.namespace}
username: ${YUDAO_NACOS_USERNAME:${spring.cloud.nacos.username:}} # Nacos 开启鉴权时需要;未设置环境变量时,使用服务自己的 Nacos 账号
password: ${YUDAO_NACOS_PASSWORD:${spring.cloud.nacos.password:}}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-gw-api-group-rules.json # 即 gateway-server-gw-api-group-rules.json
data-type: json
rule-type: gw-api-group
gw-flow: # 网关流控规则:按 routeId(resourceMode = 0)或自定义 API 分组(resourceMode = 1)限流
nacos:
# ... server-addr、namespace、username、password、group-id 与 gw-api-group 相同
data-id: ${spring.application.name}-gw-flow-rules.json # 即 gateway-server-gw-flow-rules.json
data-type: json
rule-type: gw-flow
为了篇幅,上面省略了 gw-flow 中与 gw-api-group 相同的配置项,完整内容见 sentinel.yaml 的第一段,复制时不要遗漏。
② system-server:Sentinel 的配置与 gateway-server 类似,只是命令端口改为 8720,数据源改为 flow(rule-type: flow,Data ID system-server-flow-rules.json)、degrade(rule-type: degrade,Data ID system-server-degrade-rules.json)。另外,在配置文件的顶层开启 Sentinel 对 Feign 的支持:
feign:
sentinel:
enabled: true # 开启 Sentinel 对 Feign 的支持:当前服务的【所有】Feign 客户端都由 Sentinel 原生的 SentinelFeign 创建,每个方法是一个资源
完整内容见 sentinel.yaml 的第二段。注意,配置项是 feign.sentinel.enabled,不是 spring.cloud.openfeign 下的配置项。
注意事项:
namespace、group-id、data-id必须与《3.2 在 Nacos 中创建规则》小节创建的配置一致。data-id通过${spring.application.name}拼接,服务名改变时,Nacos 中的 Data ID 也要一起修改。- 每种规则(
rule-type)在一个服务中只配置一个数据源:同一种规则的多个数据源,只有最后注册的那个生效。 - 不要配置
spring.cloud.sentinel.scg.fallback.*:它会覆盖DemoSentinelGatewayConfiguration中返回CommonResult的处理器。 - 不需要为
gateway-server手动设置csp.sentinel.app.type:spring-cloud-alibaba-sentinel-gateway启动时会自动设置为11(Spring Cloud Gateway),Dashboard 按网关应用展示「API 管理」、网关流控规则,参见《5.2》小节。 - 如果服务的配置放在 Nacos 中,同样合并到 Nacos 中该服务的配置(例如说
gateway-server-local.yaml)。
账号、地址通过环境变量传入,不要把真实密码写入配置文件、提交到 Git:
| 环境变量 | 说明 | 默认值 | 使用的服务 |
|---|---|---|---|
YUDAO_SENTINEL_DASHBOARD | Sentinel Dashboard 的地址 | 空,不连接 Dashboard | 两个服务 |
YUDAO_NACOS_USERNAME | 读取规则的 Nacos 账号,需要有该命名空间的读权限 | 服务自己的 spring.cloud.nacos.username | 两个服务 |
YUDAO_NACOS_PASSWORD | 读取规则的 Nacos 密码 | 服务自己的 spring.cloud.nacos.password | 两个服务 |
# 3.5 复制示例代码
① 将 src/main/java 下的 cn.iocoder.yudao.gateway.sentineldemo 包,复制到 yudao-gateway/src/main/java 的同名目录:
DemoSentinelGatewayConfiguration:注册BlockRequestHandlerBean。spring-cloud-alibaba-sentinel-gateway会将它注册到GatewayCallbackManager,网关被阻断时,由优先级最高的SentinelGatewayBlockExceptionHandler调用,返回 HTTP 429/503 +CommonResult,不会进入网关的GlobalExceptionHandler。
② 将 cn.iocoder.yudao.module.system.sentineldemo 包,复制到 yudao-module-system-server/src/main/java 的同名目录:
DemoSentinelInfraApi:调用infra-server的 Feign 客户端,使用独立的contextId,并通过fallbackFactory显式指定降级。DemoSentinelInfraApiFallbackFactory:调用失败、被阻断时,返回CommonResult错误码,参见《4.4 Feign 降级》小节。DemoSentinelConfiguration:通过@EnableFeignClients(clients = DemoSentinelInfraApi.class)单独声明 Feign 客户端,不修改已有的RpcConfiguration;将fallbackFactory注册成 Bean;注册 URL 资源被阻断时的BlockExceptionHandler,它对system-server的所有 URL 资源生效。controller/admin/DemoSentinelController:示例接口。放在controller.admin包下,才会添加/admin-api前缀。
③ 将 cn.iocoder.yudao.module.infra.sentineldemo 包,复制到 yudao-module-infra-server/src/main/java 的同名目录:
DemoSentinelRpcController:RPC 接口/rpc-api/infra/sentinel-demo/echo,在infra-server已有安全配置放行的/rpc-api/infra/**下。mode=slow时等待 500 毫秒再返回,mode=biz-error时抛出业务异常。
示例代码只使用 Java 8 语法和构造方法注入,不直接引用 javax、jakarta 包。只有 master 分支(Sentinel 1.8.6 的 sentinel-spring-webmvc-adapter)需要修改 DemoSentinelConfiguration 的 BlockExceptionHandler:包名不同,handle 方法也没有 resourceName 参数:
// master-jdk17、master-jdk25 分支
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
return (request, response, resourceName, ex) -> { ... };
// master 分支:修改为
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
return (request, response, ex) -> { ... };
# 3.6 启动验证
按 Nacos →(可选)Dashboard → infra-server → system-server → gateway-server 的顺序启动。
下面的请求都通过网关(48080 端口)访问。Authorization: Bearer test1 是项目的模拟登录:local 环境默认开启 yudao.security.mock-enable,test + 用户编号 1 即可模拟登录,其它环境需要使用登录接口返回的真实访问令牌;tenant-id: 1 是租户编号。
① 确认规则已经加载。命令端口只在本机访问,返回的 JSON 中能看到《3.2》小节创建的规则:
curl -s 'http://127.0.0.1:8719/gateway/getApiDefinitions' # gateway-server 的 API 分组
curl -s 'http://127.0.0.1:8719/gateway/getRules' # gateway-server 的网关流控规则
curl -s 'http://127.0.0.1:8720/getRules?type=flow' # system-server 的限流规则
curl -s 'http://127.0.0.1:8720/getRules?type=degrade' # system-server 的熔断规则
返回 [] 时,参见《5.1 修改了 Nacos 中的规则,没有生效》小节排查。
② 网关限流:连续请求 5 次 /gateway-flow,每秒只有 2 个请求通过,其余被网关拒绝,不会到达 system-server:
for i in 1 2 3 4 5; do
curl -s -w ' HTTP %{http_code}\n' -H 'Authorization: Bearer test1' -H 'tenant-id: 1' \
'http://127.0.0.1:48080/admin-api/system/sentinel-demo/gateway-flow'
done
{"code":0,"msg":"","data":"gateway-flow"} HTTP 200
{"code":0,"msg":"","data":"gateway-flow"} HTTP 200
{"code":429,"msg":"请求过于频繁,请稍后重试",...} HTTP 429
{"code":429,"msg":"请求过于频繁,请稍后重试",...} HTTP 429
{"code":429,"msg":"请求过于频繁,请稍后重试",...} HTTP 429
输出仅供参考,字段顺序、是否包含 "data":null 以实际的 JSON 配置为准。限流按 1 秒的滑动窗口统计,第一个请求较慢(例如说网关首次校验令牌)时,可能有 3 个请求通过,稍等 1 秒再执行一次即可。
③ 服务限流:把地址换成 /flow,结果与 ② 相同,只是 HTTP 429 由 system-server 的 BlockExceptionHandler 返回。也可以直接请求 system-server 的 48081 端口验证,不经过网关。
④ Feign 熔断降级:
URL='http://127.0.0.1:48080/admin-api/system/sentinel-demo/feign'
H1='Authorization: Bearer test1'; H2='tenant-id: 1'
# 1. 连续 3 次慢调用,每次约 0.5 秒:慢调用比例 100%,超过 50%,熔断 10 秒
for i in 1 2 3; do curl -s -w ' HTTP %{http_code}\n' -H "$H1" -H "$H2" "$URL?mode=slow"; done
# 2. 熔断期间,即使 mode=ok,也不会调用 infra-server,直接返回 fallbackFactory 的错误码
curl -s -w ' HTTP %{http_code}\n' -H "$H1" -H "$H2" "$URL?mode=ok"
# 3. 10 秒后,第一个请求作为探测放行,成功且不慢,熔断恢复
sleep 10; curl -s -w ' HTTP %{http_code}\n' -H "$H1" -H "$H2" "$URL?mode=ok"
# 4. 业务错误:infra-server 返回业务错误码,不会进入 fallback,也不计入熔断的统计
curl -s -w ' HTTP %{http_code}\n' -H "$H1" -H "$H2" "$URL?mode=biz-error"
| 步骤 | 结果 |
|---|---|
| 1 | 3 次都成功:code = 0,data = "slow",HTTP 200 |
| 2 | code = 503,msg = "infra-server 暂时不可用,请稍后重试",HTTP 200。fallback 返回的错误码由 getCheckedData() 抛出 ServiceException,再由全局异常处理器返回 |
| 3 | code = 0,data = "ok" |
| 4 | code = 400,msg = "模拟 infra-server 的业务错误":业务错误码原样返回给前端,不会被 fallback 替换 |
还可以停止 infra-server 后请求 mode=ok:同样返回 code = 503,并且 system-server 的日志中打印 [buildErrorResult][调用 infra-server 失败] 及完整的异常,便于排查。
⑤ 动态修改规则:在 Nacos 中,将 system-server-flow-rules.json 的 count 从 2 改为 5 并发布,不需要重启服务,再执行一次 ③,每秒可以通过 5 个请求;curl -s 'http://127.0.0.1:8720/getRules?type=flow' 中的 count 也变为 5.0。按 routeId 的规则同理:将 gateway-server-gw-flow-rules.json 中 system-admin-api 的 count 临时改为 1,所有 /admin-api/system/** 的请求每秒只能通过 1 个,验证完成后记得改回。
⑥(可选)Dashboard:设置了 YUDAO_SENTINEL_DASHBOARD 时,在 Dashboard 中可以看到 gateway-server、system-server 两个应用,「簇点链路」中列出了上面的资源名,「实时监控」中可以看到通过、拒绝的 QPS。
验证完成后,可以删除 DemoSentinelController、DemoSentinelInfraApi、DemoSentinelInfraApiFallbackFactory、DemoSentinelRpcController 等示例类,以及 Nacos 中的示例规则;两个 BlockExceptionHandler、BlockRequestHandler 可以保留,作为自己业务的阻断响应。
# 4. 使用说明
# 4.1 网关限流:路由与 API 分组
网关的规则有两种资源:
- 路由(
resourceMode = 0):资源名是网关配置中路由的id,即spring.cloud.gateway.server.webflux.routes[].id(master分支是spring.cloud.gateway.routes[].id),例如说system-admin-api对应/admin-api/system/**。 - 自定义 API 分组(
resourceMode = 1):资源名是gw-api-group中的apiName,由一组请求路径组成,可以跨路由,粒度比路由更细。
一个请求可能同时属于一个路由、多个 API 分组,它们的规则都会生效,任意一个超过阈值就会被拒绝。
gateway-server-gw-api-group-rules.json 的字段:
| 字段 | 说明 |
|---|---|
apiName | API 分组的名字,与 gw-flow 规则的 resource 一致 |
predicateItems[].pattern | 请求路径,匹配的是网关收到的原始路径,例如说 /admin-api/system/sentinel-demo/gateway-flow |
predicateItems[].matchStrategy | 匹配方式:0 精确匹配,1 前缀匹配(例如说 /admin-api/system/sentinel-demo/**),2 正则匹配 |
gateway-server-gw-flow-rules.json 的字段:
| 字段 | 说明 |
|---|---|
resource | 路由的 id,或者 API 分组的 apiName |
resourceMode | 0 路由,1 自定义 API 分组 |
grade | 阈值类型:1 QPS,0 并发线程数 |
count | 阈值,单个网关实例的 |
intervalSec | 统计窗口的时长,单位:秒,默认 1 |
controlBehavior | 流控效果:0 快速失败,2 匀速排队(配合 maxQueueingTimeoutMs,单位:毫秒) |
burst | 快速失败时,额外允许的突发请求数 |
paramItem | 可选,按客户端 IP、Host、请求头、URL 参数、Cookie 分别计数,参见 Sentinel 的 《网关流量控制》 (opens new window) 文档 |
按客户端 IP 计数时,Sentinel 取的是网关看到的连接地址:网关在 Nginx 等反向代理之后时,所有请求的 IP 都是代理的地址,需要改为按 X-Forwarded-For 等请求头计数,并且只信任代理设置的请求头。
# 4.2 服务限流:URL 资源与 @SentinelResource
① URL 资源:引入 spring-cloud-starter-alibaba-sentinel 后,Sentinel 自动为 Spring MVC 的每个接口创建资源,资源名是匹配到的路径模式,包含 /admin-api、/app-api、/rpc-api 等前缀,路径变量保持原样,例如说 /admin-api/system/user/get、/admin-api/system/xxx/{id}。资源名可以在 Dashboard 的「簇点链路」中确认。
spring.cloud.sentinel.http-method-specify 默认是 false,资源名不带 HTTP 方法;master-jdk17、master-jdk25 分支的 Sentinel 1.8.9 中,webmvc-v6x 适配开启它时添加的方法前缀有问题(Sentinel 1.8.10 修复),建议保持默认值。
② @SentinelResource:需要保护 Service 方法等非 URL 的资源时,在方法上添加 @SentinelResource 注解,Spring Cloud Alibaba 已经注册了它的切面。用法参见单体项目的 《限流熔断》 (opens new window) 文档。
system-server-flow-rules.json 的字段:
| 字段 | 说明 |
|---|---|
resource | 资源名 |
limitApp | 调用来源,default 表示不区分 |
grade | 阈值类型:1 QPS,0 并发线程数 |
count | 阈值,单个服务实例的 |
strategy | 流控模式:0 直接,1 关联,2 链路 |
controlBehavior | 流控效果:0 快速失败,1 预热(配合 warmUpPeriodSec,单位:秒),2 匀速排队(配合 maxQueueingTimeMs,单位:毫秒) |
clusterMode | 是否集群限流,保持 false,参见《4.6 单机阈值》小节 |
# 4.3 熔断规则
system-server-degrade-rules.json 的字段:
| 字段 | 说明 |
|---|---|
resource | 资源名 |
grade | 熔断策略:0 慢调用比例,1 异常比例,2 异常数 |
count | 慢调用比例时,是最大响应时间(单位:毫秒),超过即为慢调用;异常比例时,是比例阈值(0.0 ~ 1.0);异常数时,是异常的个数 |
slowRatioThreshold | 慢调用比例的阈值(0.0 ~ 1.0),只在慢调用比例时有效 |
minRequestAmount | 统计时长内的最小请求数,请求数不足时不会熔断 |
statIntervalMs | 统计时长,单位:毫秒 |
timeWindow | 熔断时长,单位:秒。到期后放行一个探测请求,成功则恢复,失败则继续熔断 |
哪些情况会被统计为「异常」,需要注意:
- Feign 资源:只有调用本身失败才算,例如说超时、连接失败、没有可用的实例、HTTP 状态码不是 2XX。项目的 RPC 接口会将业务异常转换成 HTTP 200 的
CommonResult错误码,Sentinel 不会把它当作下游故障,这是期望的行为:业务错误不应该导致熔断。 - URL 资源:项目的全局异常处理器会把异常转换成
CommonResult返回,是否计入异常与 Sentinel 的版本有关。需要按异常比例、异常数熔断时,建议在 Service 方法上使用@SentinelResource,并通过exceptionsToIgnore = ServiceException.class排除业务异常。 - 下游变慢、但不报错时,异常比例无法发现,使用慢调用比例更合适,示例即是如此。
# 4.4 Feign 降级
① 开启 feign.sentinel.enabled 后,当前服务的所有 Feign 客户端,都由 Sentinel 原生的 SentinelFeign 创建,每个方法是一个资源,资源名是「HTTP 方法:http://服务名 + 路径」,例如说 GET:http://infra-server/rpc-api/infra/sentinel-demo/echo。项目已有的 Feign 客户端没有配置 fallback:调用失败时,原来的异常照常抛出,行为与接入前相同。
② 只有在 @FeignClient 上显式配置了 fallbackFactory(或 fallback)的客户端,才会降级:
@FeignClient(name = ApiConstants.NAME, contextId = "demoSentinelInfraApi",
fallbackFactory = DemoSentinelInfraApiFallbackFactory.class)
public interface DemoSentinelInfraApi { ... }
- 优先使用
fallbackFactory,它能拿到失败的原因(Throwable cause);同时配置时,fallback优先。 fallbackFactory必须注册成 Bean,Sentinel 通过FeignClientFactory按类型查找它,找不到时启动报错。- 它要实现接口的所有方法。接口的
default方法不是 Sentinel 资源,它调用的其它方法仍然会经过 Sentinel。
③ fallback 的写法,参考 DemoSentinelInfraApiFallbackFactory:
- 返回
CommonResult错误码,不要抛出异常:调用方通过getCheckedData()或checkError()抛出ServiceException,由全局异常处理器返回给前端。master分支的 Spring Cloud Alibaba 2021.0.6.2 还会把 fallback 中抛出的异常包装成AssertionError。 - 不要伪造成功的结果:例如说创建订单、扣减库存等写操作,fallback 返回成功会让调用方误以为已经执行。只有「查询失败时返回空列表」这类业务可以接受的场景,才返回降级的数据。
- 提示中不要包含原始的异常信息,避免泄漏下游的地址、报错等;但对 Sentinel 阻断以外的异常,要打印完整的错误日志,不要吞掉。
- 只对返回
CommonResult的方法这样处理。不是CommonResult的方法,需要按其返回类型单独设计,不要一概返回null。
④ 调用方必须检查结果:
return success(demoSentinelInfraApi.echo(mode).getCheckedData());
不检查时,调用方会把失败当作成功,拿着为 null 的 data 继续执行。
⑤ 为 Feign 资源配置限流、熔断规则时,该客户端一定要配置 fallback。没有 fallback 时,Sentinel 抛出的 BlockException 是受检异常,接口方法没有声明,会变成 UndeclaredThrowableException,由全局异常处理器返回「系统异常」。
为什么不为所有 Feign 客户端注入默认的 fallback?
统一的默认 fallback 需要替换 Feign.Builder 或 InvocationHandlerFactory,与 Sentinel 原生的 SentinelFeign 冲突:要么 Sentinel 的统计、熔断失效,要么默认 fallback 失效。并且,它很难正确处理所有的返回类型、default 方法和业务错误码:把业务错误码也当作失败去降级,会改变调用方原有的语义。所以,本文只在需要降级的客户端上显式配置。
# 4.5 规则持久化:Nacos 是规则的唯一来源
规则的流转如下:
- 修改:在 Nacos 中修改规则配置并发布,服务通过 Nacos 的监听即时收到,整体替换该服务这一类型的规则,不需要重启。Nacos 的历史版本可以用于回滚。
- 启动:服务启动时从 Nacos 读取一次。Nacos 中没有该配置时,没有规则,等创建后再收到推送。
- Dashboard:官方 Dashboard 修改规则时,只是通过命令端口推送到当前这台实例的内存,不会写回 Nacos。下次 Nacos 中的配置变更、或者服务重启时,就会被 Nacos 中的规则覆盖;有多个实例时,也只修改了其中一台。
所以,Dashboard 只用于查看监控、确认资源名,以及紧急情况下临时调整;需要长期生效的规则,一定要修改 Nacos 中的配置。要让 Dashboard 的修改直接写入 Nacos,需要自行改造 Dashboard 的源码,本文不提供。
# 4.6 单机阈值
规则中的 count 是单个实例的阈值,Sentinel 在每个实例的内存中分别计数:
- 网关部署 3 个实例时,
count = 2的规则,整体每秒最多可以通过约 6 个请求;服务同理。 - 实例数变化(扩缩容、滚动发布)时,整体的上限随之变化,需要按「整体阈值 ÷ 实例数」估算
count。
需要全局、精确的配额时(例如说每个用户每分钟只能调用 N 次),使用基于 Redis 的 《请求限流(RateLimiter)》。Sentinel 的集群流控(Token Server)需要额外的部署和运维,本文不使用,规则中的 clusterMode 保持 false。
# 4.7 阻断响应与前端
| 场景 | HTTP 状态码 | 响应 |
|---|---|---|
| 网关、服务被限流 | 429 | {"code": 429, "msg": "请求过于频繁,请稍后重试"} |
| 网关、服务的 URL 资源被熔断 | 503 | {"code": 503, "msg": "服务暂时不可用,请稍后重试"} |
| Feign 被限流等普通阻断,进入 fallback | 200 | {"code": 429, "msg": "请求过于频繁,请稍后重试"},与项目其它的业务错误码一样 |
| Feign 被熔断或调用失败,进入 fallback | 200 | {"code": 503, "msg": "infra-server 暂时不可用,请稍后重试"},与项目其它的业务错误码一样 |
前端的表现:
yudao-ui-admin-vben:HTTP 状态码不是 2XX 时,弹出响应中的msg,即「请求过于频繁,请稍后重试」。yudao-ui-admin-vue3:HTTP 状态码不是 2XX 时,弹出通用的请求失败提示及状态码,不读取msg。
如果希望所有前端都按业务错误码处理,可以把 BlockRequestHandler、BlockExceptionHandler 中的状态码改为 200。代价是网关、Nginx 的访问日志和监控中,无法再通过 HTTP 状态码区分被限流的请求。
# 4.8 安全注意事项
- 命令端口(示例的 8719、8720)没有鉴权,可以查询、修改规则,只能在本机或内网访问,通过防火墙、安全组限制,不要对公网开放。
- Dashboard 修改默认的账号密码,只在内网访问。
- Nacos 中的规则配置,有写权限的人都可以修改,按命名空间控制权限;读取规则的账号只需要读权限。
- 本文不使用 Sentinel 的授权规则(黑白名单):它依据的调用来源,通常取自客户端可以伪造的请求头,不能替代 Spring Security 的认证、鉴权。
- 本文也没有启用系统自适应规则、热点参数规则、集群流控,需要时参见 Sentinel 的官方文档,并自行验证。
# 5. 常见问题
# 5.1 修改了 Nacos 中的规则,没有生效
按顺序排查:
- 命名空间:创建配置时选择的命名空间,与服务的
spring.cloud.nacos.config.namespace是否一致;配置中填写的是命名空间的 ID。 - Group、Data ID:是否是
SENTINEL_GROUP,Data ID 是否与sentinel.yaml中拼接的结果完全一致,包括.json后缀、服务名。 - JSON 格式:是否包含注释、结尾多余的逗号。可以先用 JSON 格式化工具校验,再发布。
- 鉴权:Nacos 开启鉴权时,是否设置了有读权限的账号。
rule-type:例如说网关的规则是gw-flow、gw-api-group,不是flow。- 资源名:是否与实际的资源名一致,可以在 Dashboard 的「簇点链路」中确认;
count等字段不合法的规则会被 Sentinel 忽略。
然后通过《3.6 启动验证》的 ① 查看服务内存中实际的规则。Sentinel 自己的日志在 ${user.home}/logs/csp/ 目录下,sentinel-record.log 中记录了规则的加载。
# 5.2 Dashboard 中,网关显示为普通的应用,没有「API 管理」菜单
当前的 Spring Cloud Alibaba 网关适配器(三个分支的 spring-cloud-alibaba-sentinel-gateway)启动时会自动将 csp.sentinel.app.type 设置为 11(Spring Cloud Gateway),不需要手动添加启动参数。出现这个现象时,依次排查:
- 适配器是否接入:
yudao-gateway的pom.xml是否添加了《3.3 添加依赖》小节的spring-cloud-alibaba-sentinel-gateway;通过《3.6 启动验证》的 ① 请求gateway/getRules,能返回网关规则,说明网关适配已经生效。 - Dashboard 的版本与连通性:是否使用 1.8.x 版本的 Dashboard(例如说《3.1》小节的 1.8.10),
gateway-server是否连接上了 Dashboard,参见《5.8 Dashboard 中看不到应用》小节。 - 实际注册的应用类型:Dashboard 在内存中按应用名保存应用,应用类型取自该应用第一次注册的机器,之后的注册、移除机器都不会更新它。同名应用之前以普通应用注册过时,核对是否遗留了旧的应用类型;必要时在可控的测试环境中重启 Dashboard,等
gateway-server重新注册后再确认。 - 初始化顺序:若 Sentinel 的
SentinelConfig在网关自动配置之前已经被提前初始化,需要结合实际注册的应用类型、初始化顺序排查。
它只影响 Dashboard 的展示,不影响规则的执行。
# 5.3 Dashboard 中修改的规则,重启后丢失,或者过一会儿被还原
这是预期的行为:Dashboard 的修改只保存在实例的内存中,不会写回 Nacos,参见《4.5 规则持久化》小节。需要长期生效的规则,修改 Nacos 中的配置。
# 5.4 被阻断时,返回的是 Blocked by Sentinel 文本,而不是 CommonResult
- 网关:
DemoSentinelGatewayConfiguration是否在cn.iocoder.yudao.gateway包下被扫描到;是否配置了spring.cloud.sentinel.scg.fallback.*,它会覆盖BlockRequestHandler。 - 服务:
DemoSentinelConfiguration是否被扫描到;master分支是否按《3.5 复制示例代码》小节修改了BlockExceptionHandler的包名。
# 5.5 Feign 调用失败时,没有进入 fallback
- 是否配置了
feign.sentinel.enabled: true。没有开启时,OpenFeign 会忽略fallbackFactory,失败时直接抛出异常。 fallbackFactory是否注册成了 Bean。- 下游返回的是不是业务错误码:HTTP 200 的
CommonResult错误码不是调用失败,不会进入 fallback,由调用方的getCheckedData()处理,参见《4.4 Feign 降级》小节。
# 5.6 为 Feign 资源配置规则后,返回「系统异常」
该 Feign 客户端没有配置 fallback,BlockException 变成了 UndeclaredThrowableException,参见《4.4 Feign 降级》小节的 ⑤。
# 5.7 报错 No bean found of type interface feign.codec.Encoder
例如说 IllegalStateException: No bean found of type interface feign.codec.Encoder for demoSentinelInfraApi,出现在 master-jdk25 分支使用 Feign 的服务(例如说 system-server、infra-server)启动时。按《3.3 添加依赖》小节的 ③ 确认该服务只有 Pageable、没有 DataWebProperties 后,为该服务添加 dependencies-jdk25.xml 中的 spring-boot-data-commons 依赖。可以通过 mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-data-commons 确认是否已经存在。
# 5.8 Dashboard 中看不到应用
- 是否设置了
YUDAO_SENTINEL_DASHBOARD环境变量,地址、端口是否正确。 - 没有开启
eager时,服务收到第一个请求后才会连接 Dashboard。 - Dashboard 需要反过来访问服务的命令端口拉取监控数据:服务在 Docker 等环境中时,通过
spring.cloud.sentinel.transport.client-ip设置 Dashboard 能够访问到的 IP,并开放对应的端口。
延伸阅读:《芋道 Spring Cloud Alibaba 服务容错 Sentinel 入门》 (opens new window)。