程序员L札记
2022/10/07阅读:83主题:橙心
改造sentinel-dashboard1.8.5持久化配置到Nacos2.1.1
前言
sentinel-dashboard官方未提供动态数据源的支持,需要自行整合。官方虽未提供实现,但是却提供了整合样例代码,从而实现dashboard与nacos的互通。
以下是官方文档的描述:
目前控制台的规则推送也是通过 规则查询更改 HTTP API 来更改规则。这也意味着这些规则仅在内存态生效,应用重启之后,该规则会丢失。
以上是原始模式。当了解了原始模式之后,我们非常鼓励您通过 动态规则 并结合各种外部存储来定制自己的规则源。我们推荐通过动态配置源的控制台来进行规则写入和推送,而不是通过 Sentinel 客户端直接写入到动态配置源中。在生产环境中,我们推荐 push 模式,具体可以参考:在生产环境使用 Sentinel。
注:若要使用集群流控功能,则必须对接动态规则源,否则无法正常使用。您也可以接入 AHAS Sentinel 快速接入全自动托管、高可用的集群流控能力。
Sentinel 同时还提供应用维度规则推送的示例页面(流控规则页面,前端路由为 /v2/flow),用户改造控制台对接配置中心后可直接通过 v2 页面推送规则至配置中心。Sentinel 抽取了通用接口用于向远程配置中心推送规则以及拉取规则:
-
DynamicRuleProvider : 拉取规则(应用维度) -
DynamicRulePublisher : 推送规则(应用维度)
用户只需实现 DynamicRuleProvider 和 DynamicRulePublisher 接口,并在 v2 的 controller 中通过 @Qualifier 注解替换相应的 bean 即可实现应用维度推送。我们提供了 Nacos 和 Apollo 的示例,改造详情可参考 应用维度规则推送示例。
官方文档为我们指明了方向,下面我们就动手改造sentinel dashboard。
准备
-
先通过 Github 拉取 Sentinel 源码,GitHub 地址:https://github.com/alibaba/Sentinel -
导入到IDEA中

-
进入 sentinel-dashboard 模块,所有的修改都在该模块下。
开始
后端代码调整
1 . 修改pom.xml
<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!-- <scope>test</scope>-->
</dependency>
找到以上依赖,将scope删除或者注释掉
-
修改配置文件
找到配置文件 application.properties 文件,在末尾添加一下配置:
sentinel.nacos.enable=true
sentinel.nacos.server-addr=localhost:8848
sentinel.nacos.namespace=
sentinel.nacos.username=nacos
sentinel.nacos.password=nacos
sentinel.nacos.group-id=SENTINEL-GROUP
注意:如果需要将规则持久化到nacos的publish命名空间下,此处的填空就好,不要写publish,否则会导致,控制台无法查询到配置信息
-
调整包结构
在com.alibaba.csp.sentinel.dashboard.rule包下创建nacos包,并在该包下根据以下结构新建包
├─auth #存放授权规则相关类
├─degrade #存放降级规则相关类
├─flow #存放限流规则相关类
├─gateway #存放网关限流规则相关类
│ ├─api
│ └─flow
├─param #存放热点规则相关类
└─system #存放系统规则相关类
如下图:

-
增加配置

如上图所示,将test包下的NacosConfigUtil、NacosConfig、FlowRuleNacosPublisher、FlowRuleNacosProvider复制到刚刚新建的nacos包下,如下图所示:

-
增加SentinelNacosProperties文件
在com.alibaba.csp.sentinel.dashboard.rule.nacos包下创建SentinelNacosProperties.java文件,代码如下:
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sentinel.nacos")
@ConditionalOnProperty(name = "sentinel.nacos.enable", havingValue = "true")
public class SentinelNacosProperties {
private String serverAddr;
private String namespace;
private String username;
private String password;
private String groupId = "SENTINEL_GROUP";
public String getServerAddr() {
return serverAddr;
}
public void setServerAddr(String serverAddr) {
this.serverAddr = serverAddr;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
}
-
修改NacosConfig文件
修改后的代码如下:
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Properties;
/**
* @author Eric Zhao
* @since 1.4.0
*/
@Configuration
public class NacosConfig {
//====================流控规则 Converter====================
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
//====================降级规则 Converter====================
@Bean
public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
return s -> JSON.parseArray(s, DegradeRuleEntity.class);
}
//====================热点规则 Converter====================
@Bean
public Converter<List<ParamFlowRuleEntity>, String> paramsRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<ParamFlowRuleEntity>> paramsRuleEntityDecoder() {
return s -> JSON.parseArray(s, ParamFlowRuleEntity.class);
}
//====================系统规则 Converter====================
@Bean
public Converter<List<SystemRuleEntity>, String> systemRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<SystemRuleEntity>> systemRuleEntityDecoder() {
return s -> JSON.parseArray(s, SystemRuleEntity.class);
}
//====================授权规则 Converter====================
@Bean
public Converter<List<AuthorityRuleEntity>, String> authRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
Converter<String, List<AuthorityRuleEntity>> authRuleEntityDecoder() {
return s -> JSON.parseArray(s, AuthorityRuleEntity.class);
}
//====================网关API限流规则 Converter====================
@Bean
public Converter<List<ApiDefinitionEntity>, String> apiDefinitionEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<ApiDefinitionEntity>> apiDefinitionEntityDecoder() {
return s -> JSON.parseArray(s, ApiDefinitionEntity.class);
}
//====================网关限流规则 Converter====================
@Bean
public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService(SentinelNacosProperties sentinelNacosProperties) throws Exception {
Properties properties = new Properties();
// nacos 地址
properties.put(PropertyKeyConst.SERVER_ADDR, sentinelNacosProperties.getServerAddr());
// 命令空间
properties.put(PropertyKeyConst.NAMESPACE, sentinelNacosProperties.getNamespace());
// nacos 账号
properties.put(PropertyKeyConst.USERNAME, sentinelNacosProperties.getUsername());
// nacos 账号密码
properties.put(PropertyKeyConst.PASSWORD, sentinelNacosProperties.getPassword());
return ConfigFactory.createConfigService(properties);
}
}
请自行与源文件做对比
-
修改NacosConfigUtil文件
修改后的代码如下:
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
/**
* @author Eric Zhao
* @since 1.4.0
*/
public final class NacosConfigUtil {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
public static final String SYS_DATA_ID_POSTFIX = "-system-rules";
public static final String AUTH_DATA_ID_POSTFIX = "-auth-rules";
public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";
/**
* cc for `cluster-client`
*/
public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
/**
* cs for `cluster-server`
*/
public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";
public static final String GATEWAY_FLOW_DATA_ID_POSTFIX = "-gateway-flow-rules";
public static final String GATEWAY_API_DATA_ID_POSTFIX = "-gateway-api-rules";
private NacosConfigUtil() {}
}
-
在前面创建的auth包下创建以下两个类:
-
AuthorityRuleNacosProvider 用于从nacos拉取配置 -
AuthorityRuleNacosPublisher 用于控制台向nacos推送配置
参照上面拷贝的FlowRuleNacosProvider和FlowRuleNacosPublisher文件中的代码创建以上两个文件,完整的代码如下:
@Component("authRuleNacosProvider")
public class AuthorityRuleNacosProvider implements DynamicRuleProvider<List<AuthorityRuleEntity>> {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorityRuleNacosProvider.class);
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<AuthorityRuleEntity>> converter;
@Override
public List<AuthorityRuleEntity> getRules(String appName) throws Exception {
String rules = configService.getConfig(appName + NacosConfigUtil.AUTH_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, 3000);
LOGGER.info("get auth rule from nacos, rules : {}", rules);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
@Component("authRuleNacosPublisher")
public class AuthorityRuleNacosPublisher implements DynamicRulePublisher<List<AuthorityRuleEntity>> {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorityRuleNacosPublisher.class);
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<AuthorityRuleEntity>, String> converter;
@Override
public void publish(String app, List<AuthorityRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
String convertedRule = converter.convert(rules);
LOGGER.info("sentinel dashboard publisher auth rules : {}", convertedRule);
configService.publishConfig(app + NacosConfigUtil.AUTH_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, convertedRule, ConfigType.JSON.getType());
}
}
-
依次创建降级规则、网关规则、热点规则、系统规则等文件
代码大同小异,不同点就是Converter<List<AuthorityRuleEntity>, String> converter
中的entity不同,具体的可以回看NacosConfig中的配置,另一个就是data-id不同。这里就不一一罗列代码了。
最终的代码结构如下图:

-
FlowControllerV2
修改 com.alibaba.csp.sentinel.dashboard.controller.v2包下的FlowControllerV2,内容如下:
// @Autowired
// @Qualifier("flowRuleDefaultProvider")
// private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
// @Autowired
// @Qualifier("flowRuleDefaultPublisher")
// private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
注释掉原来的ruleProvider和rulePublisher,引入我们自己创建的。
-
AuthorityRuleController
修改 com.alibaba.csp.sentinel.dashboard.controller包下的AuthorityRuleController,内容如下:
// 增加以下代码
@Autowired
@Qualifier("authRuleNacosProvider")
private DynamicRuleProvider<List<AuthorityRuleEntity>> ruleProvider;
@Autowired
@Qualifier("authRuleNacosPublisher")
private DynamicRulePublisher<List<AuthorityRuleEntity>> rulePublisher;
@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result<List<AuthorityRuleEntity>> apiQueryAllRulesForMachine(@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app cannot be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip cannot be null or empty");
}
if (port == null || port <= 0) {
return Result.ofFail(-1, "Invalid parameter: port");
}
if (!appManagement.isValidMachineOfApp(app, ip)) {
return Result.ofFail(-1, "given ip does not belong to given app");
}
try {
// 注释掉下面一行代码
// List<AuthorityRuleEntity> rules = sentinelApiClient.fetchAuthorityRulesOfMachine(app, ip, port);
// 增加以下代码
List<AuthorityRuleEntity> rules = ruleProvider.getRules(app);
// 尤其注意增加以下if逻辑,不然运行时会报错,这里踩的坑
if (rules != null && !rules.isEmpty()) {
for (AuthorityRuleEntity entity : rules) {
entity.setApp(app);
}
}
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying authority rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private boolean publishRules(String app, String ip, Integer port) {
List<AuthorityRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
// 注释掉这一行
// return sentinelApiClient.setAuthorityRuleOfMachine(app, ip, port, rules);
// 增加以下代码
try {
rulePublisher.publish(app, rules);
return true;
} catch (Exception e) {
return false;
}
}
需要增加以及改动的代码,已经在代码上加了注释,请仔细阅读。
-
同理修改其他几个controller

如图所示,需要另行修改其他几个controller,改动的位置大致相同,分别注入对应的provider和publisher,请自行修改。
前端代码修改
-
sidebar.html
找到webapp/resources/app/scripts/directives/sidebar/sidebar.html文件,找到以下代码:
<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>
修改为:
<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>
-
flow_service_v1.js
找到webapp/resources/app/scripts/services/flow_service_v1.js文件,找到以下代码:
this.newRule = function (rule) {
var param = {
resource: rule.resource,
limitApp: rule.limitApp,
grade: rule.grade,
count: rule.count,
strategy: rule.strategy,
refResource: rule.refResource,
controlBehavior: rule.controlBehavior,
warmUpPeriodSec: rule.warmUpPeriodSec,
maxQueueingTimeMs: rule.maxQueueingTimeMs,
app: rule.app,
ip: rule.ip,
port: rule.port
};
return $http({
url: '/v1/flow/rule',
data: rule,
method: 'POST'
});
};
修改为:
this.newRule = function (rule) {
var param = {
resource: rule.resource,
limitApp: rule.limitApp,
grade: rule.grade,
count: rule.count,
strategy: rule.strategy,
refResource: rule.refResource,
controlBehavior: rule.controlBehavior,
warmUpPeriodSec: rule.warmUpPeriodSec,
maxQueueingTimeMs: rule.maxQueueingTimeMs,
app: rule.app,
ip: rule.ip,
port: rule.port
};
return $http({
url: '/v2/flow/rule',
data: rule,
method: 'POST'
});
};
将url由v1修改为v2
-
identity.js
找到webapp/resources/app/scripts/controllers/identity.js文件,找到以下代码:
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('未知错误');
});
}
修改为:
function saveFlowRule() {
if (!FlowService.checkRuleValid(flowRuleDialogScope.currentRule)) {
return;
}
FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) {
if (data.code === 0) {
flowRuleDialog.close();
let url = '/dashboard/v2/flow/' + $scope.app;
$location.path(url);
} else {
alert('失败:' + data.msg);
}
}).error((data, header, config, status) => {
alert('未知错误');
});
}
-
flow_v2.html 找到webapp/resources/app/views/flow_v2.html文件,找到以下代码:
<a class="btn btn-default-inverse" style="float: right; margin-right: 10px;" ui-sref="dashboard.flowV1({app: app})">
回到单机页面
</a>
将其注释掉或删掉。
至此sentinel dashboard持久化规则到nacos改造完毕,实现了双向同步。改造的过程中,省略了一部分代码,是因为几乎类似,为了节省篇幅,相信聪明的你,一定会自我完成的,如果无法独立完成,请在下方评论区留言哦。
欢迎关注我的公众号:程序员L札记

作者介绍