Sentinel规则动态推送:Nacos数据源与Dashboard的持久化方案
Sentinel规则动态推送:Nacos数据源与Dashboard的持久化方案
适读人群:使用Sentinel做限流熔断的后端工程师 | 阅读时长:约24分钟 | Spring Boot 3.2 / Sentinel 1.8.6
开篇故事
用过Sentinel的人应该都经历过这个痛苦:在Dashboard上配好了限流规则,服务一重启,规则全没了。每次发版都要重新在Dashboard上把规则配一遍,配错了还要回滚。上了生产的第一天我就被这个坑坑了,差点在全公司同事面前出洋相。
那是一次大促备战,我在Dashboard把核心接口的QPS限流配置好,以为万事大吉。凌晨搞完了服务升级,第二天早上活动开始,发现所有限流配置都没了,服务在接受大流量冲击没有任何保护。好在活动初期流量还没上来,紧急手动重配了规则才没出大问题。
但是手动重配是不够的,真正生产可用的Sentinel必须把规则持久化,重启后自动加载,在Dashboard修改后实时推送到所有实例。这就需要引入Nacos作为规则的持久化数据源,同时改造Dashboard让它把规则写到Nacos而不是只推给内存。今天把这套完整方案写出来。
一、核心问题分析
Sentinel的规则持久化有三种模式,很多人分不清楚:
模式一:原始模式(不持久化)。Dashboard直接通过HTTP API把规则推给Sentinel客户端,存在内存里。服务重启就丢失,这是默认模式,只适合本地开发。
模式二:Pull模式。应用定时从外部数据源(文件、数据库)拉取规则。规则会持久化,但有延迟,且每个实例都需要有访问数据源的权限,管理麻烦。
模式三:Push模式(生产推荐)。规则存储在外部配置中心(Nacos),Sentinel客户端监听Nacos变更,实时获取最新规则。Dashboard改造后把规则写入Nacos,通过Nacos推送到所有实例,实现配置变更秒级生效。
生产环境必须用Push模式。但Push模式需要改造Sentinel Dashboard的源码,这是很多团队卡住的地方。今天把这个改造方案完整讲清楚。
二、原理深度解析
2.1 三种持久化模式对比
2.2 Push模式完整数据流
2.3 Sentinel数据源架构
三、完整代码实现
3.1 项目依赖
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel Nacos数据源 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.6</version>
</dependency>
<!-- Nacos客户端 -->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>3.2 application.yml配置Nacos数据源
Spring Cloud Alibaba对Sentinel的Nacos数据源有自动配置支持,直接在yml里配置即可:
spring:
application:
name: order-service
cloud:
sentinel:
transport:
dashboard: sentinel-dashboard:8080
port: 8719
# 开启饥饿加载,避免首次请求触发限流规则初始化
eager: true
datasource:
# 限流规则
flow:
nacos:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:prod}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-flow-rules
data-type: json
rule-type: flow
# 熔断降级规则
degrade:
nacos:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:prod}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-degrade-rules
data-type: json
rule-type: degrade
# 系统保护规则
system:
nacos:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:prod}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-system-rules
data-type: json
rule-type: system
# 热点参数限流规则
param-flow:
nacos:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:prod}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-param-flow-rules
data-type: json
rule-type: param-flow
# 授权规则
authority:
nacos:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: ${NACOS_NAMESPACE:prod}
group-id: SENTINEL_GROUP
data-id: ${spring.application.name}-authority-rules
data-type: json
rule-type: authority3.3 Nacos中的限流规则格式
在Nacos控制台创建DataID为order-service-flow-rules的配置,内容为JSON数组:
[
{
"resource": "/api/order/create",
"limitApp": "default",
"grade": 1,
"count": 100,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
},
{
"resource": "/api/order/query",
"limitApp": "default",
"grade": 1,
"count": 500,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]字段说明:
grade: 1表示QPS限流,0表示并发线程数限流strategy: 0直接限流,1关联限流,2链路限流controlBehavior: 0快速失败,1Warm Up,2排队等待
3.4 在业务代码中使用Sentinel注解
package com.laozhang.sentinel.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderService {
/**
* 创建订单,配置了Sentinel限流保护
* value: 资源名,对应Nacos中rules里的resource字段
* blockHandler: 限流时的处理方法
* fallback: 业务异常时的降级方法
*/
@SentinelResource(
value = "/api/order/create",
blockHandler = "createOrderBlock",
fallback = "createOrderFallback"
)
public OrderResult createOrder(CreateOrderRequest request) {
// 业务逻辑
return processOrder(request);
}
/**
* 限流/熔断处理
* 注意:方法签名必须和原方法一致,并在末尾加BlockException参数
*/
public OrderResult createOrderBlock(CreateOrderRequest request, BlockException ex) {
log.warn("订单创建被限流,userId={}", request.getUserId());
return OrderResult.fail("系统繁忙,请稍后重试");
}
/**
* 业务异常降级处理
*/
public OrderResult createOrderFallback(CreateOrderRequest request, Throwable t) {
log.error("订单创建异常降级,userId={}", request.getUserId(), t);
return OrderResult.fail("服务暂时不可用");
}
/**
* 热点参数限流示例
* 对userId参数做热点限流,防止单个用户刷接口
*/
@SentinelResource(value = "queryOrderByUser")
public List<Order> queryOrdersByUser(
@com.alibaba.csp.sentinel.annotation.ParamFlowException String userId,
int pageNum,
int pageSize
) {
return orderRepository.findByUserId(userId, pageNum, pageSize);
}
private OrderResult processOrder(CreateOrderRequest request) {
// 实际业务逻辑
return new OrderResult();
}
}3.5 改造Sentinel Dashboard(关键步骤)
Dashboard改造是整个方案最复杂的部分。核心思路是:在Dashboard里引入Nacos客户端依赖,然后重写FlowControllerV2等Controller,让它在修改规则时同时写入Nacos。
以下是改造的核心代码(基于Sentinel Dashboard 1.8.6源码):
// 在sentinel-dashboard模块的pom.xml中添加
// <dependency>
// <groupId>com.alibaba.nacos</groupId>
// <artifactId>nacos-client</artifactId>
// <version>2.3.2</version>
// </dependency>
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 限流规则发布器:将规则写入Nacos
* 替换默认的推送到客户端内存的发布器
*/
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
// 将规则序列化后写入Nacos
String dataId = app + NacosConfigUtils.FLOW_DATA_ID_POSTFIX;
configService.publishConfig(
dataId,
NacosConfigUtils.GROUP_ID,
JSON.toJSONString(rules)
);
}
}package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 限流规则提供者:从Nacos读取规则
*/
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String rules = configService.getConfig(
appName + NacosConfigUtils.FLOW_DATA_ID_POSTFIX,
NacosConfigUtils.GROUP_ID,
3000
);
if (rules == null || rules.trim().isEmpty()) {
return new ArrayList<>();
}
return JSON.parseArray(rules, FlowRuleEntity.class);
}
}package com.alibaba.csp.sentinel.dashboard.rule.nacos;
public class NacosConfigUtils {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-flow-rules";
public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules";
}3.6 Nacos配置Service Bean
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class NacosConfig {
@Value("${nacos.server-addr}")
private String serverAddr;
@Value("${nacos.namespace}")
private String namespace;
@Value("${nacos.username:}")
private String username;
@Value("${nacos.password:}")
private String password;
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
properties.setProperty("serverAddr", serverAddr);
properties.setProperty("namespace", namespace);
if (!username.isEmpty()) {
properties.setProperty("username", username);
properties.setProperty("password", password);
}
return ConfigFactory.createConfigService(properties);
}
}四、生产配置与调优
4.1 Sentinel全局配置
spring:
cloud:
sentinel:
# 流控模式配置
flow:
cold-factor: 3 # Warm Up冷启动因子,默认3
# 熔断器相关
circuit-breaker:
stat-interval-ms: 1000 # 统计窗口大小
# 日志配置
log:
dir: /var/log/sentinel
switch-pid: true4.2 规则数据的Nacos目录规划
Nacos命名空间: prod
组: SENTINEL_GROUP
DataID命名规范:
{serviceName}-flow-rules # 限流规则
{serviceName}-degrade-rules # 熔断降级规则
{serviceName}-system-rules # 系统保护规则
{serviceName}-param-flow-rules # 热点参数规则
{serviceName}-authority-rules # 授权规则五、踩坑实录
坑一:Sentinel Dashboard默认不持久化,服务重启规则全丢。
这是最基础的坑,开篇故事就是这个。一定要在引入Sentinel之初就把Nacos持久化方案搭好,不要等到生产出了问题再改造,改造成本很高。
坑二:Nacos数据源配置正确但规则没生效,排查发现是Nacos namespace填错了。
Sentinel的Nacos数据源namespace要填UUID格式的命名空间ID,和其他Nacos配置一样。但有个特殊情况:如果namespace是public命名空间,不要填"public"这个字符串,要填空字符串或者不填,否则找不到配置。
坑三:改造Dashboard后,规则写入Nacos了,但客户端没收到更新。
排查发现是DataID格式不一致。Dashboard写入Nacos时用的DataID是${app}-flow-rules,但客户端配置里用的是${spring.application.name}-flow-rules。如果Dashboard里的app名称和Spring应用名不一样,就会对不上。
解决方案是确保注册到Dashboard的应用名(通过spring.cloud.sentinel.transport.dashboard连接时的应用名)和spring.application.name保持一致。
坑四:热点参数限流规则在重启后不生效。
热点参数限流规则比普通限流规则多了一个限制:@SentinelResource的value属性(资源名)必须和Nacos中规则的resource字段完全一致,包括大小写。而且热点参数的参数索引也要正确,参数索引从0开始计数。
配好之后,建议先在本地通过curl测试一下规则是否生效,再部署到生产。
六、总结
Sentinel的Push模式持久化方案是生产环境的必选。核心三步:引入Nacos数据源依赖,在yml里配置各类规则的DataID,以及改造Dashboard让规则变更写入Nacos。改造Dashboard是最复杂的部分,但只需要做一次,之后规则就能在重启后自动恢复,在Dashboard修改后实时推送到所有实例。
