【深入理解SpringCloud微服务】Sentinel规则持久化实战
Sentinel规则持久化实战
- Sentinel规则推送模式
- 原始模式
- pull(拉模式)
- push(推模式)
- 实现拉模式
- 实现推模式
Sentinel规则推送模式
原始模式
原始模式由Sentinel控制台直接推送规则到Sentinel客户端,Sentinel客户端将规则保存到内存中。这种模式是最简单的,不需要做任何改造,但是一旦Sentinel客户端重启,所有的规则都会丢失。
pull(拉模式)
拉模式由Sentinel客户端主动轮询规则管理中心,更新规则到内存中。这个规则管理中心可以是关系型数据库或者文件,因此这种模式已具备了规则持久化的能力,但是实时性无法保证(因为是定时轮询的)。
拉模式要扩展并注册写数据源WritableDataSource和读数据源ReadableDataSource。当Sentinel客户端接收到Sentinel控制台推送的规则时,将规则更新到内存的同时,会通过WritableDataSource将规则持久化到规则管理中心。而ReadableDataSource则负责定时轮询规则管理中心,当规则管理中心发生规则变更时,则更新到内存。
push(推模式)
推模式由规则管理中心主动将变更的规则推送给Sentinel客户端,Sentinel客户端通过注册监听器的方式监听规则管理中心推送的规则变更通知。此时的规则管理中心就不能是关系型数据库或文件了,而是nacos或zookeeper等具备主动通知能力的配置中心。
推模式需要扩展并注册ReadableDataSource,ReadableDataSource通过监听器实时监听配置中心推送的规则变更通知,更新到内存。
此时有一个问题,在Sentinel控制台上更新的规则,在配置中心没有变化。
解决这个问题有两个办法。
第一种办法是:Sentinel客户端注册一个WritableDataSource,当Sentinel客户端收到Sentinel控制台推送的新增或更新的规则时,通过WritableDataSource发布规则到配置中心。
第二种办法是修改Sentinel控制台的源码,Sentinel控制台不和Sentinel客户端直接通讯,而是把在控制台上新增或修改的规则保存到配置中心,由配置中心通知Sentinel客户端。同时Sentinel控制台也不再通过Sentinel客户端提供的API拉取规则,而是从配置中心读取。
实现拉模式
引入Maven依赖(如果引入的是spring-cloud-starter-alibaba-sentinel则不需要,里面已经包含了):
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
<version>1.8.5</version>
</dependency>
通过实现InitFunc接口,并重新init方法进行读数据源和写数据源的注册,Sentinel内部会通过SPI机制加载InitFunc的实现类并执行init方法。
/**
* 实现InitFunc接口,Sentinel内部会通过SPI机制加载
* @author huangjunyi
* @date 2024年4月21日 下午1:48:09
* @desc
*/
public class FlowRuleFileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
// 定位本地流控规则配置文件路径
ClassLoader classLoader = getClass().getClassLoader();
String flowRulePath = URLDecoder.decode(classLoader.getResource("FlowRule.json").getFile(), "UTF-8");
System.out.println("flowRulePath: " + flowRulePath);
// 注解读数据源到RuleManager
FileRefreshableDataSource<List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(
flowRulePath, source -> JSON.parseObject(source,
new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
// 注册写数据源到通讯(transport)模块的WritableDataSourceRegistry
WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath,
t -> JSON.toJSONString(t));
WritableDataSourceRegistry.registerFlowDataSource(wds);
}
}
/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc:
com.huangjunyi1993.config.FlowRuleFileDataSourceInit
准备一个本地的流控规则配置文件
/resources/FlowRule.json:
[
{
"resource": "/my",
"controlBehavior": 0,
"count": 2,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]
这样就完成了。
此时的拉模式是通过定时轮询本地配置文件实现的:
我们在Sentinel控制台上配置的规则,就会持久化到文件中。比如我们新增一条流控规则:
在文件中就可以看到持久化的规则:
修改文件的规则:
控制台也会发生变化:
调用接口,验证流控规则是否生效:
实现推模式
引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.5</version>
</dependency>
这个在Sentinel的源码包的demo模块中也有示例,我们拉过来直接改一改:
public class NacosDataSourceInit implements InitFunc {
// nacos server ip
private static final String remoteAddress = "localhost:8848";
// nacos group
private static final String groupId = "Sentinel_Demo";
// nacos dataId
private static final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule";
private static void loadRules() {
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
@Override
public void init() throws Exception {
loadRules();
}
}
/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc:
com.huangjunyi1993.config.NacosDataSourceInit
在nacos上创建配置文件:
启动应用并验证:
这种是硬编码的配置模式,其实还有更简单的。如果我们引入的是spring-cloud-starter-alibaba-sentinel,那么我们只要在bootstrap.yml配置nacos地址
/resources/bootstrap.yml
server:
port: 8888
spring:
application:
name: demo
cloud:
nacos:
# 配置nacos地址
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 控制台地址
dashboard: 127.0.0.1:8080
port: 8719
datasource:
nacos-flow-rule:
nacos:
# nacos地址、dataId、groupId
server-addr: 127.0.0.1:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
rule-type: flow
data‐type: json
management:
endpoints:
web:
exposure:
include: '*'
由于我们配置的dataId是我们的应用名,因此nacos上的dataId要与应用名一致:
这种方式就更加简单了。
启动应用并验证:
此时我们在Sentinel控制台上修改规则配置时,发现nacos上的配置没有发生变化。因此我们接下来解决这个问题,我们使用修改Sentinel控制台的方式实现,我们可以参考sentinel-dashboard下的/src/test目录下的例子进行改造。
NacosConfig中配置NacosConfigService,用于控制台与nacos通讯:
@Configuration
public class NacosConfig {
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
// 配置NacosConfigService,用于控制台与nacos通讯
@Bean
public ConfigService nacosConfigService() throws Exception {
return ConfigFactory.createConfigService("localhost:8848");
}
}
新增FlowRuleNacosProvider,用于控制台从nacos拉取规则配置:
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
// 通过configService从nacos拉取规则配置
String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, 3000);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
新增FlowRuleNacosPublisher,用于控制台把规则配置发布到nacos:
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
// 发布规则配置到控制台
configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, converter.convert(rules));
}
}
对com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1进行改造:
注释掉SentinelApiClient,然后引入FlowRuleNacosProvider和FlowRuleNacosPublisher。
原先使用SentinelApiClient拉取规则配置的地方改成使用FlowRuleNacosProvider,原先使用SentinelApiClient发布规则配置的地方改成使用FlowRuleNacosPublisher。
改造前的sentinel-dashboard是这样:
改造后的sentinel-dashboard是这样:
流控规则就改造完了,改动和添加的几个文件如下:
其余类型的规则也是参照这个套路修改即可。
然后在nacos上创建一个配置文件,按照NacosConfigUtil指定的命名规则,Data Id的格式为{app}-flow-rules,group则是SENTINEL_GROUP。
然后我们的应用程序的bootstrap.yml文件配置的dataId和groupId也要改成相应格式:
spring:
...
cloud:
...
sentinel:
...
datasource:
nacos-flow-rule:
nacos:
...
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
...
启动控制台并验证:
发现从nacos拉取到了流控规则,我们修改流程规则:
流控规则更新到控制台:
最后还有个问题,如果热点参数流程也按照这个套路来改的话,会导致其失效。原因是因为Sentinel控制台发布到nacos的格式是按照ParamFlowRuleEntity的格式推的,而Sentinel从nacos拉取到热点参数规则时,是解析成ParamFlowRule的。
一种办法是修改Sentinel客户端的converter,让它能成功的从ParamFlowRuleEntity格式的json解析成ParamFlowRule。
另一种办法是修改Sentinel控制台的converter。当发布规则到nacos时,从ParamFlowRuleEntity格式的对象转成ParamFlowRule格式的json;当从nacos拉取规则时,从ParamFlowRule格式的json转成ParamFlowRuleEntity格式的对象。