Sentinel Dashboard基于Nacos的规则存储和同步
1、规则管理和推送的原理
1>、详细请参考官方文档:《在生产环境中使用 Sentinel》
2>、官方文档:《动态规则扩展》
3>、也可以参考程序猿DD的相关博客-《Sentinel Dashboard中修改规则同步到Apollo》
2、 下载源码
通过Github或Gitee下载Sentinel源码,这里选择了v1.8.0版本的源码。其中,
- Github地址:https://github.com/alibaba/Sentinel
这里下载的项目包括了Sentinel项目的全部模块,我们选择我们需要的Sentinel Dashboard模块即可,然后导入开发工具即可。
3、 修改源码
为了区分,我这里把项目重命名为sentinel-dashboard-nacos。
3.1、首先修改pom文件
主要修改artifactId的值和注释掉sentinel-datasource-nacos依赖的scope,并移除nacos-api依赖。如下所示:
//省略其他内容……
<artifactId>sentinel-dashboard-nacos</artifactId>
//省略其他内容……
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<scope>test</scope>-->
</dependency>
//省略其他内容……
<!--
//该依赖版本引入的有问题,注释掉后,会自动加载指定的版本
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-api</artifactId>
<version>1.8.0</version>
<scope>compile</scope>
</dependency>
-->
3.2、 增加Nacos相关配置类
在v1.8.0这个版本中,在单元测试包中,已经实现了Nacos配置需要的类,代码位置如下所示:
在java目录下,包com.alibaba.csp.sentinel.dashboard.rule下面,创建nacos子包,并把四个类复制到这里然后修改类名和其中的方法。其中,NacosConfigUtil类不做任何修改。
首先NacosConfig类,不修改类名,只修改内容,如下:
@Configuration
public class NacosConfig {
//增加一个参数,在application.properties中增加spring.nacos.server-addr=192.168.1.8:8848定义,用来定义nacos的访问路径
@Value("${spring.nacos.server-addr}")
private String serverAddr;
@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() throws Exception {
if(StringUtils.isEmpty(serverAddr)){//不配置时,使用默认配置
return ConfigFactory.createConfigService("localhost");
}else{//使用自定义配置,这里其实还可以定义其他参数,比如encode、namespace等,详细可以参考NacosConfigService类
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
return ConfigFactory.createConfigService(properties);
}
}
}
然后修改NacosFlowRuleApiProvider类,首先修改了类的名称(可以不修改),然后修改@Component注解的值,其他可以选择性修改,如下:
@Component("nacosFlowRuleApiProvider")
public class NacosFlowRuleApiProvider 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);
}
}
然后修改NacosFlowRuleApiPublisher类,和前面一样。首先修改了类的名称(可以不修改),然后修改@Component注解的值,其他可以选择性修改,如下:
@Component("nacosFlowRuleApiPublisher")
public class NacosFlowRuleApiPublisher 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));
}
}
3.3、修改FlowControllerV2类
把ruleProvider和rulePublisher两个变量注入我们前面新定义的两个实现类(修改注解@Qualifier的值即可),如下:
@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
修改为:
@Autowired
@Qualifier("nacosFlowRuleApiProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("nacosFlowRuleApiPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
3.4、修改sidebar.html页面
把sidebar.html页面中的如下代码:
<a ui-sref="dashboard.flowV1({app: entry.app})">
修改为下面代码:
<a ui-sref="dashboard.flow({app: entry.app})">
文件路径,修改内容,如下图所示:
这样修改,实际上是使用了FlowControllerV2作为处理器,通过app.js中的如下代码可以看出:
.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',
]
});
}]
}
})
而原来的代码则是,通过FlowControllerV1作为处理器,
.state('dashboard.flowV1', {
templateUrl: 'app/views/flow_v1.html',
url: '/flow/:app',
controller: 'FlowControllerV1',
resolve: {
loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sentinelDashboardApp',
files: [
'app/scripts/controllers/flow_v1.js',
]
});
}]
}
})
至此,Sentinel Dashboard-nacos修改就完成了。
4、 启动Nacos配置中心
启动配置中心,具体请参考前面的内容。这里Nacos配置中心的地址是:http://192.168.1.8:8848/nacos,用户名密码:nacos/nacos。
5、启动Sentinel Dashboard
通过开发工具直接启动Sentinel Dashboard即可,使用默认的端口号8080。访问地址:http://localhost:8080/#/dashboard,用户名密码:sentinel/sentinel。
6、 建立测试应用
这里基于前面的nacos-service项目进行验证和测试。
6.1、修改pom文件,引入相关依赖
<!--引入sentinel的依赖-->
<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>
<version>1.5.2</version>
</dependency>
6.2、修改bootstrap.properties配置文件,增加如下配置:
spring.cloud.sentinel.transport.client-ip=192.168.1.87
spring.cloud.sentinel.transport.port=8722
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.datasource.ds.nacos.server-addr=192.168.1.8:8848
spring.cloud.sentinel.datasource.ds.nacos.groupId=SENTINEL_GROUP
spring.cloud.sentinel.datasource.ds.nacos.dataId=${spring.application.name}-flow-rules
spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow
其中,
- spring.cloud.sentinel.transport.client-ip 需要被监听应用的IP,当客户端有多个网卡时需要配置
- spring.cloud.sentinel.transport.port 需要监听应用的Port
- spring.cloud.sentinel.transport.dashboard:sentinel dashboard的访问地址
- spring.cloud.sentinel.datasource.ds.nacos.server-addr:nacos的访问地址
- spring.cloud.sentinel.datasource.ds.nacos.groupId:nacos中存储规则的groupId,上述的“SENTINEL_GROUP”配置是由NacosConfigUtil类中的变量GROUP_ID定义的
- spring.cloud.sentinel.datasource.ds.nacos.dataId:nacos中存储规则的dataId,上述配置使用了当前应用名称+"-flow-rules"进行配置,其中后缀也是在NacosConfigUtil类中定义,由FLOW_DATA_ID_POSTFIX变量定义。
- spring.cloud.sentinel.datasource.ds.nacos.rule-type:该参数用来定义存储的规则类型。由com.alibaba.cloud.sentinel.datasource.RuleType枚举类定义,可选值有:flow、degrade、param-flow、system等。
6.3、启动项目,并验证
1>、访问http://localhost:8001/service接口,频繁刷新,这个时候没有限制。
2>、进入Sentinel Dashboard中,进行流控规则配置,如下所示:
主要添加流控配置的按钮,关于“回到单机页面”的配置,后续再分析。
3>、这个时候,再访问http://localhost:8001/service接口,频繁刷新,会出现异常报错。同时,可以登录Nacos配置中心,会发现多了一个配置项,如下所示:
说明在Sentinel Dashboard中配置的规则已经再nacos配置中心进行了存储。
4>、然后,在配置中心,修改qriver-nacos-server-flow-rules配置项,再为/test增加一个流控配置,这个时候去刷新Sentinel Dashboard配置,会发现多了一个/test的规则,然后通过频繁刷新http://localhost:8001/test接口,会发现频繁访问异常,说明配置生效了。
7、 修改FlowControllerV1类
通过前面的修改,我们就实现了规则的动态修改和基于Nacos的存储了。但是前面提到的FlowControllerV1类是干什么用的,在Sentinel Dashboard中,流控规则中“回到单机页面”按钮是什么作用呢?其实在修改sidebar.html页面中,我们提到了FlowControllerV2和FlowControllerV1两个类,其中默认使用了FlowControllerV1类,我们前面通过修改sidebar.html变成了使用FlowControllerV2类。其中,流控规则中“回到单机页面”按钮也是回到FlowControllerV1处理器的单机配置。我们尝试使用单机配置,会发现没有实现规则的动态修改和基于Nacos的存储,如果为了实现单机的规则的动态修改和基于Nacos的存储,就需要修改FlowControllerV1类(在实际的过程中,修改FlowControllerV2类就可以了),具体方法如下:
首先,和FlowControllerV2类中类似,引入ruleProvider和rulePublisher,并注释sentinelApiClient。
然后,修改apiQueryMachineRules方法
再,修改publishRules方法
最后,修改使用publishRules方法的位置。完整的代码如下:
@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("nacosFlowRuleApiProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("nacosFlowRuleApiPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
// @Autowired
// private SentinelApiClient sentinelApiClient;
@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result<List<FlowRuleEntity>> apiQueryMachineRules(@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
try {
// List<FlowRuleEntity> rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port);
// rules = repository.saveAll(rules);
List<FlowRuleEntity> lastRules = new ArrayList<>();
List<FlowRuleEntity> rules = ruleProvider.getRules(app);
for(FlowRuleEntity flow : rules){
if(ip.equals(flow.getIp()) && port.equals(flow.getPort())){
lastRules.add(flow);
}
}
rules = repository.saveAll(lastRules);
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying flow rules", throwable);
return Result.ofThrowable(-1, throwable);
}
}
private <R> Result<R> checkEntityInternal(FlowRuleEntity entity) {
if (StringUtil.isBlank(entity.getApp())) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isBlank(entity.getIp())) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (entity.getPort() == null) {
return Result.ofFail(-1, "port can't be null");
}
if (StringUtil.isBlank(entity.getLimitApp())) {
return Result.ofFail(-1, "limitApp can't be null or empty");
}
if (StringUtil.isBlank(entity.getResource())) {
return Result.ofFail(-1, "resource can't be null or empty");
}
if (entity.getGrade() == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (entity.getGrade() != 0 && entity.getGrade() != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got");
}
if (entity.getCount() == null || entity.getCount() < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
if (entity.getStrategy() == null) {
return Result.ofFail(-1, "strategy can't be null");
}
if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
if (entity.getControlBehavior() == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
int controlBehavior = entity.getControlBehavior();
if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
if (entity.isClusterMode() && entity.getClusterConfig() == null) {
return Result.ofFail(-1, "cluster config should be valid");
}
return null;
}
@PostMapping("/rule")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<FlowRuleEntity> apiAddFlowRule(@RequestBody FlowRuleEntity entity) {
Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(null);
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
entity.setLimitApp(entity.getLimitApp().trim());
entity.setResource(entity.getResource().trim());
try {
entity = repository.save(entity);
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS);
publishRules(entity.getApp(), entity.getIp(), entity.getPort());
return Result.ofSuccess(entity);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e);
return Result.ofFail(-1, e.getMessage());
}
}
@PutMapping("/save.json")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<FlowRuleEntity> apiUpdateFlowRule(Long id, String app,
String limitApp, String resource, Integer grade,
Double count, Integer strategy, String refResource,
Integer controlBehavior, Integer warmUpPeriodSec,
Integer maxQueueingTimeMs) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "id " + id + " dose not exist");
}
if (StringUtil.isNotBlank(app)) {
entity.setApp(app.trim());
}
if (StringUtil.isNotBlank(limitApp)) {
entity.setLimitApp(limitApp.trim());
}
if (StringUtil.isNotBlank(resource)) {
entity.setResource(resource.trim());
}
if (grade != null) {
if (grade != 0 && grade != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got");
}
entity.setGrade(grade);
}
if (count != null) {
entity.setCount(count);
}
if (strategy != null) {
if (strategy != 0 && strategy != 1 && strategy != 2) {
return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got");
}
entity.setStrategy(strategy);
if (strategy != 0) {
if (StringUtil.isBlank(refResource)) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
entity.setRefResource(refResource.trim());
}
}
if (controlBehavior != null) {
if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) {
return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got");
}
if (controlBehavior == 1 && warmUpPeriodSec == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && maxQueueingTimeMs == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
entity.setControlBehavior(controlBehavior);
if (warmUpPeriodSec != null) {
entity.setWarmUpPeriodSec(warmUpPeriodSec);
}
if (maxQueueingTimeMs != null) {
entity.setMaxQueueingTimeMs(maxQueueingTimeMs);
}
}
Date date = new Date();
entity.setGmtModified(date);
try {
entity = repository.save(entity);
if (entity == null) {
return Result.ofFail(-1, "save entity fail: null");
}
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS);
publishRules(entity.getApp(), entity.getIp(), entity.getPort());
return Result.ofSuccess(entity);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Error when updating flow rules, app={}, ip={}, ruleId={}", entity.getApp(),
entity.getIp(), id, e);
return Result.ofFail(-1, e.getMessage());
}
}
@DeleteMapping("/delete.json")
@AuthAction(PrivilegeType.WRITE_RULE)
public Result<Long> apiDeleteFlowRule(Long id) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
try {
repository.delete(id);
} catch (Exception e) {
return Result.ofFail(-1, e.getMessage());
}
try {
//publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(5000, TimeUnit.MILLISECONDS);
publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort());
return Result.ofSuccess(id);
} catch (Throwable t) {
Throwable e = t instanceof ExecutionException ? t.getCause() : t;
logger.error("Error when deleting flow rules, app={}, ip={}, id={}", oldEntity.getApp(),
oldEntity.getIp(), id, e);
return Result.ofFail(-1, e.getMessage());
}
}
// private CompletableFuture<Void> publishRules(String app, String ip, Integer port) {
// List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
// return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules);
// }
private void publishRules(String app, String ip, Integer port) throws Exception {
List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
rulePublisher.publish(app, rules);
}
}