手把手教你改造 Sentinel Dashboard 实现配置持久化
一. 概述
Sentinel客户端默认情况下接收到 Dashboard 推送的规则配置后,可以实时生效。但是有一个致命缺陷,Dashboard和业务服务并没有持久化这些配置,当业务服务重启后,这些规则配置将全部丢失。
Sentinel 提供两种方式修改规则:
- 通过 API 直接修改 (
loadRules
) - 通过
DataSource
适配不同数据源修改
通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则
手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。
上述 loadRules()
方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。DataSource
接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource
接口是更加可靠的做法。
我们推荐通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource
接口端监听规则中心实时获取变更,流程如下:
DataSource
扩展常见的实现方式有:
- 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
- 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
Sentinel 目前支持以下数据源扩展:
Sentinel开源版在Push模式下只实现了 路径2,也就是Nacos到业务服务之间的规则同步;路径1 Dashboard配置修改写入Nacos并没有实现,在后文中我们会介绍如何修改 Dashboard 源码完成配置的写入。
二. 从 Nacos 加载规则配置
首先,我们先来看看如何使用Sentinel官方提供的 sentinel-datasource-nacos
从Nacos加载规则配置。
第一步:引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.6</version>
</dependency>
第二步:配置规则自动加载
package cn.bigcoder.demo.sentinel.sentineldemo.demos.config;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.alibaba.nacos.api.PropertyKeyConst;
import java.util.List;
import java.util.Properties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
public class SentinelRuleConfiguration implements ApplicationListener<ContextRefreshedEvent> {
private static final String remoteAddress = "10.10.10.12:8848";
// nacos group
private static final String groupId = "SENTINEL_GROUP";
// nacos dataId
private static final String dataId = "sentinel-demo";
private static final String NACOS_NAMESPACE_ID = "SENTINEL";
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, remoteAddress);
// properties.put(PropertyKeyConst.NAMESPACE, NACOS_NAMESPACE_ID);
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(properties, groupId,
dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
}
第三步:往Nacos中写入配置
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import java.util.Properties;
public class NacosConfigSender {
public static void main(String[] args) throws Exception {
final String groupId = "SENTINEL_GROUP";
final String dataId = "sentinel-demo-flow-rules";
// 创建ConfigService实例
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "10.10.10.12:8848");
// 指定namespace
properties.put(PropertyKeyConst.NAMESPACE, "SENTINEL");
final String rule = "[\n"
+ " {\n"
+ " \"resource\": \"GET:/user/getById\",\n"
+ " \"controlBehavior\": 0,\n"
+ " \"count\": 1,\n"
+ " \"grade\": 1,\n"
+ " \"limitApp\": \"default\",\n"
+ " \"strategy\": 0\n"
+ " }\n"
+ "]";
ConfigService configService = NacosFactory.createConfigService(properties);
System.out.println(configService.publishConfig(dataId, groupId, rule));
}
}
执行完后,Nacos中就会出现对应的配置:
第四步:启动项目,验证规则配置是否生效
访问 http://127.0.0.1:8719/getParamRules?type=flow 即可看到业务服务内存中加载到的规则配置
并发执行 /user/getById
接口,可以发现接口被成功限流,1s内的10次请求,只有一次成功。
三. Dashboard存在的问题
使用此方案虽然解决了配置规则配置持久化的问题,但是在Dashboard上修改配置仍然是通过业务服务暴露的接口进行的配置同步。业务服务既可以接收 Nacos 配置变更,又可以接收Dashboard的配置变更,控制台的变更的配置并没有同步到Nacos,应用重启后Sentinel控制台修改的配置仍然会全部丢失:
一个理想的情况是Sentinel控制台规则配置读取至 Nacos 而不是内存,在控制台修改/新增的配置写入Nacos,当Nacos配置发生变更时,配置进而自动同步至业务服务:
当然存储媒介可以根据情况选用别的组件:ZooKeeper, Redis, Apollo, etcd
很可惜的是,阿里官方开源的Sentinel控制台并没有实现将规则配置写入其他中间件的能力。它默认只支持将配置实时推送至业务服务,所以我们在生产环境中想要使用 Sentinel Dashboard 需要自行修改其源码,将其配置同步逻辑改为写入我们所需要的中间件中。
四. 修改Sentinel Dashboard源码
4.1 准备工作
首先通过git拉取下载源码,导入idea工程:
https://github.com/alibaba/Sentinel
本文源码修改基于 Sentinel 1.8.8 版本,所有修改的源码可参考:
https://github.com/bigcoder84/Sentinel
4.1.1 流控规则接口
Sentinel Dashboard的流控规则下的所有操作,都会调用Sentinel-Dashboard源码中的FlowControllerV1类,这个类中包含流控规则本地化的CRUD操作:
在com.alibaba.csp.sentinel.dashboard.controller.v2包下存在一个FlowControllerV2;类,这个类同样提供流控规则的CURD,与V1不同的是,它可以实现指定数据源的规则拉取和发布。
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
//....
}
官方说明:从 Sentinel 1.4.0 开始,我们抽取出了接口用于向远程配置中心推送规则以及拉取规则。
DynamicRuleProvider<T>
: 拉取规则DynamicRulePublisher<T>
: 推送规则以 Nacos 为例,若希望使用 Nacos 作为动态规则配置中心,用户可以提取出相关的类,然后只需在
FlowControllerV2
中指定对应的 bean 即可开启 Nacos 适配
FlowControllerV2依赖两个非常重要的类
- DynamicRuleProvider:动态规则的拉取,从指定数据源中获取控制后在Sentinel Dashboard中展示。
- DynamicRulePublisher:动态规则发布,将在Sentinel Dashboard中修改的规则同步到指定数据源中。
只需要扩展这两个类,然后集成Nacos来实现Sentinel Dashboard规则同步。
4.1.2 需要改造的页面入口
簇点链路:
由于该页面的“流控”配置是对单节点进行配置的,所以理论上该页面的URL是不用改的
流控规则:
上述位置我们都需要改造对应前端代码,使之调用的接口更改为我们新的V2接口上。
4.2 源码改造
4.2.1 在pom.xml文件中去掉test scope注释
这是因为官方提供的Nacos持久化用例都是在test目录下,所以scope需要去除test,需要sentinel-datasource-nacos包的支持。之后将修改好的源码放在源码主目录下,而不是继续在test目录下。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<scope>test</scope>-->
</dependency>
4.2.2 创建Nacos配置
我们采用官方的约束,即默认 Nacos 适配的 dataId 和 groupId 约定如下:
- groupId: SENTINEL_GROUP
- 流控规则 dataId: {appName}-flow-rules,比如应用名为 appA,则 dataId 为 appA-flow-rules
所以不需要修改NacosConfigUtil.java了,但这是展示是为了步骤的完整性。
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "sentinel.nacos")
public class NacosPropertiesConfiguration {
private String serverAddr;
private String groupId = "SENTINEL_GROUP"; // 默认分组
private String namespace;
// 省略 getter/setter
}
然后配置sentinel-dashboar/resources/application.properties中配置nacos配置,以为sentinel.nacos为前缀:
# nacos config server
sentinel.nacos.serverAddr=127.0.0.1:8848
sentinel.nacos.namespace=
sentinel.nacos.group-id=SENTINEL-GROUP
4.2.3 改造NacosConfig,创建NacosConfigService
package com.alibaba.csp.sentinel.dashboard.config;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import java.util.List;
import java.util.Properties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(NacosPropertiesConfiguration.class)
@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);
}
@Bean
public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration)
throws Exception {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());
properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
return ConfigFactory.createConfigService(properties);
}
}
NacosConfig主要做两件事:
1) 注入Convert转换器,将 FlowRuleEntity
使用序列化为JSON字符串,以及将JSON字符串反序列化为 FlowRuleEntity
。
2) 注入Nacos配置服务ConfigService
4.2.4 实现 DynamicRulePublisher 和 DynamicRuleProvider 接口完成配置的持久化和远程加载
在 test 包下,已经有Sentinel官方的实现了,我们只需要将其拷贝至 src
目录下即可:
FlowRuleNacosProvider:用于从Nacos中拉取规则配置。
package com.alibaba.csp.sentinel.dashboard.rule;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.util.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Eric Zhao
* @since 1.4.0
*/
@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 {
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中
package com.alibaba.csp.sentinel.dashboard.rule;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.util.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Eric Zhao
* @since 1.4.0
*/
@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));
}
}
4.2.5 修改FlowControllerV2类将Nacos实现类注入进去
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
// 省略
}
到这里所有流控规则相关的后端接口都已经改造完毕,我们需要接着改造前端页面,将页面请求的接口全部换成V2新接口。
4.2.6 修改前端“流控规则”路由配置(sidebar.html)
找到 resources/app/scripts/directives/sidebar/sidebar.html
文件,该文件是用来渲染左侧路由的:
我们需要将 “流控规则” 路由跳转的页面由 app/views/flow_v1.html
更换为 app/views/flow_v2.html
,因为 flow_v2.html
页面中调用的后端接口全部都是 v2接口。
修改flowV1为flow,去掉V1,这样的话会调用FlowControllerV2接口
<!--<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flowV1({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>-->
<!-- 修改为flow,直接调用FlowControllerV2 -->
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则</a>
</li>
这样就可以通过js跳转至FlowControllerV2了
.state('dashboard.flow', {
templateUrl: 'app/views/flow_v2.html',
url: '/v2/flow/:app',
controller: 'FlowControllerV2',
resolve: {
loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sentinelDashboardApp',
files: [
'app/scripts/controllers/flow_v2.js',
]
});
}]
}
})
4.2.7 修改前端“簇点链路”中流控配置的接口
根据 app/scripts/directives/sidebar/sidebar.html
触点链路路由调用js方法可知,最终路由转发到了 app/views/identity.html
页面:
<!-- app/scripts/directives/sidebar/sidebar.html -->
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.identity({app: entry.app})">
<i class="glyphicon glyphicon-list-alt"></i> 簇点链路</a>
</li>
// app/scripts/app.js
.state('dashboard.identity', {
templateUrl: 'app/views/identity.html',
url: '/identity/:app',
controller: 'IdentityCtl',
resolve: {
loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sentinelDashboardApp',
files: [
'app/scripts/controllers/identity.js',
]
});
}]
}
})
在 app/views/identity.html
页面中,我们需要将“流控”弹窗的保存按钮调用的接口换成V2版本,
addNewFlowRule
方法在 app/scripts/controllers/identity.js
文件中:
$scope.addNewFlowRule = function (resource) {
if (!$scope.macInputModel) {
return;
}
var mac = $scope.macInputModel.split(':');
flowRuleDialogScope = $scope.$new(true);
flowRuleDialogScope.currentRule = {
enable: false,
strategy: 0,
grade: 1,
controlBehavior: 0,
resource: resource,
limitApp: 'default',
clusterMode: false,
clusterConfig: {
thresholdType: 0
},
app: $scope.app,
ip: mac[0],
port: mac[1]
};
flowRuleDialogScope.flowRuleDialog = {
title: '新增流控规则',
type: 'add',
confirmBtnText: '新增',
saveAndContinueBtnText: '新增并继续添加',
showAdvanceButton: true
};
// $scope.flowRuleDialog = {
// showAdvanceButton : true
// };
flowRuleDialogScope.saveRule = saveFlowRule;
flowRuleDialogScope.saveRuleAndContinue = saveFlowRuleAndContinue;
flowRuleDialogScope.onOpenAdvanceClick = function () {
flowRuleDialogScope.flowRuleDialog.showAdvanceButton = false;
};
flowRuleDialogScope.onCloseAdvanceClick = function () {
flowRuleDialogScope.flowRuleDialog.showAdvanceButton = true;
};
flowRuleDialog = ngDialog.open({
template: '/app/views/dialog/flow-rule-dialog.html',
width: 680,
overlay: true,
scope: flowRuleDialogScope
});
};
在这个方法中,会调用 saveFlowRule
方法保存流控规则:
function saveFlowRule() {
if (!FlowService.checkRuleValid(flowRuleDialogScope.currentRule)) {
return;
}
FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) {
if (data.code === 0) {
flowRuleDialog.close();
let url = '/dashboard/flow/' + $scope.app;
$location.path(url);
} else {
alert('失败:' + data.msg);
}
}).error((data, header, config, status) => {
alert('未知错误');
});
}
在这个方法中,会调用 FlowService.newRule
方法发送HTTP请求新建规则,成功后会将页面重定向至 '/dashboard/flow/' + $scope.app
,所以我们需要改两个地方:
-
将FlowService改成V2版本
-
将重定向页面跳转至
'/dashboard/v2/flow/' + $scope.app
4.3.8 重新打包项目
进入 sentinel-dashboard
目录,执行下列命令重新打包:
mvn clean package -Dmaven.test.skip
4.3 测试
可以看到修改后的Dashboard成功将配置写入Nacos中,Nacos配置发生变更,也同时通知了订阅这些配置的客户端,使得业务服务能实时更新流控配置,即使业务服务重启,之前仍能正常从Nacos中拉取配置。
五. 总结
本文详细介绍了如何利用Nacos实现Sentinel Dashboard配置的持久化,解决了业务服务重启后配置丢失的问题。通过以下几个步骤,我们成功实现了配置的动态管理和持久化存储:
- 引入Nacos依赖:在项目中添加了
sentinel-datasource-nacos
依赖,为后续集成打下基础。 - 配置自动加载:通过实现
ReadableDataSource
接口,配置了规则自动从Nacos加载到Sentinel的流程。 - Nacos配置写入:通过编写
NacosConfigSender
类,实现了向Nacos写入配置的功能。 - Dashboard源码改造:针对Dashboard存在的问题,通过修改前后端源码,实现了配置的持久化存储和同步更新。
通过这一系列的改造,我们不仅提高了Sentinel Dashboard的可用性和稳定性,还增强了其在生产环境中的实用性。现在,即使在业务服务重启的情况下,配置也不会丢失,确保了服务的连续性和一致性。
本文只是讲解了“流控规则”持久化的源码修改过程,如果其它模块也有持久化的需求,也可以参考此过程进行相应的源码修改。
本文参考至:
dynamic-rule-configuration | Sentinel (sentinelguard.io)
Sentinel Dashboard(基于1.8.1)流控规则持久化到Nacos——涉及部分Sentinel Dashboard源码改造 - JJian - 博客园 (cnblogs.com)