这个标题是什么意思呢,原因是上一篇章:使用维护在Nacos中的一个限流Json来做Sentinel限流规则的持久化,这里有的同学就会问了:不是已经实现了吗?为啥还要再说一遍,其实啊,Nacos在上个篇章做了持久化,那他在跟Sentinel做集成的时候,大家想想,Nacos充当什么角色呢?我们平常开发,说到数据持久化,第一时间会想到谁?不出意外应该都是MySQL,所以,在上述的关系里面,Nacos也充当是一个“数据库”的角色,但有一个风险,我单独写一下:
所有的限流规则都持久化到了Nacos,这句话没问题,但有问题的是,我们在Nacos控制台上直接修改了限流规则并且同步了,但由于项目开发人员过多呢,就会出现一个问题:人员信息不同步,什么意思呢,就是,有些开发可能并不知道限流规则都在Nacos上维护,默认会去Dashboard上直接操作了,这样,就会导致很难管理的问题,而且Nacos的定位是注册中心和配置中心,如果直接操作Nacos来做Sentinel的规则持久化多多少少有点不太合适!
那上述的问题怎么解决呢?解决思路很简单,那就是:修改Sentinel源码,在Dashboard上维护限流规则同时直接调用Nacos服务,同步到Nacos上面即可,不用维护两套(先维护Nacos,再维护Sentinel Dashboard)
同步实现
1、源码先知道
Sentinel控制台在做流控规则的时候,都会调用SentinelDashboard源码中的一个名叫:FlowControllerV1的接口类,因为流控规则的所有操作,调用的接口都是如图所示:
这个路径对应的就是FlowControllerV1这个类,但是同时还存在一个FlowControllerV2的接口类,这个类主要提供的是流控规则的CURD,这两者有啥区别呢?
和V1不同之处在于:V2可以实现指定数据源的规则拉取和发布
反正Sentinel也是Java代码,那就废话不多数,直接上代码:
@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result<List<FlowRuleEntity>> apiQueryMachineRules(@RequestParam String app) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
try {
List<FlowRuleEntity> rules = ruleProvider.getRules(app);
if (rules != null && !rules.isEmpty()) {
for (FlowRuleEntity entity : rules) {
entity.setApp(app);
if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) {
entity.setId(entity.getClusterConfig().getFlowId());
}
}
}
rules = repository.saveAll(rules);
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 (entity == null) {
return Result.ofFail(-1, "invalid body");
}
if (StringUtil.isBlank(entity.getApp())) {
return Result.ofFail(-1, "app can't be null or empty");
}
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(value = AuthService.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());
} catch (Throwable throwable) {
logger.error("Failed to add flow rule", throwable);
return Result.ofThrowable(-1, throwable);
}
return Result.ofSuccess(entity);
}
@PutMapping("/rule/{id}")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<FlowRuleEntity> apiUpdateFlowRule(@PathVariable("id") Long id,
@RequestBody FlowRuleEntity entity) {
if (id == null || id <= 0) {
return Result.ofFail(-1, "Invalid id");
}
FlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofFail(-1, "id " + id + " does not exist");
}
if (entity == null) {
return Result.ofFail(-1, "invalid body");
}
entity.setApp(oldEntity.getApp());
entity.setIp(oldEntity.getIp());
entity.setPort(oldEntity.getPort());
Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(id);
Date date = new Date();
entity.setGmtCreate(oldEntity.getGmtCreate());
entity.setGmtModified(date);
try {
entity = repository.save(entity);
if (entity == null) {
return Result.ofFail(-1, "save entity fail");
}
publishRules(oldEntity.getApp());
} catch (Throwable throwable) {
logger.error("Failed to update flow rule", throwable);
return Result.ofThrowable(-1, throwable);
}
return Result.ofSuccess(entity);
}
@DeleteMapping("/rule/{id}")
@AuthAction(PrivilegeType.DELETE_RULE)
public Result<Long> apiDeleteRule(@PathVariable("id") Long id) {
if (id == null || id <= 0) {
return Result.ofFail(-1, "Invalid id");
}
FlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
try {
repository.delete(id);
publishRules(oldEntity.getApp());
} catch (Exception e) {
return Result.ofFail(-1, e.getMessage());
}
return Result.ofSuccess(id);
}
private void publishRules(/*@NonNull*/ String app) throws Exception {
List<FlowRuleEntity> rules = repository.findAllByApp(app);
rulePublisher.publish(app, rules);
}
}
其中啊,看一下最上面的俩Qualifier注解修饰的对象:
- DynamicRuleProvider:动态规则的拉取,从指定的数据源里面获取数据并回显在Sentinel控制台上
- DunamicRulePublisher:动态规则的发布,将Sentinel控制台上操作的数据,同步到指定的数据源上
所以,我们要想实现,入口都走Sentinel控制台的话,就要从这俩对象入手
1、修改Sentinel Dashboard源码
我这里使用的是Sentinel1.8.1版本,其实差不多
1、先将sentinel-datasource-nacos一来的scope去掉(maven的声明周期就不说了)
2、进入resources/app/scripts/directives/sidebar这个目录,找到sidebar.html文件,全局搜一下“dashboard.flowV1”,将V1去除:去除之后就会调用FlowControllerV2中的CURD的接口
3、在com.alibaba.csp.sentinel.dashboard.rule下创建一个nacos包,创建一个配置类,定义nacos的基本属性,代码如下:
@ConfigurationProperties(prefix = "sentinel.nacos") public class NacosPropertiesConfiguration { private String serverAddr; private String dataId; private String groupId="DEFAULT_GROUP"; private String namespace; public String getServerAddr() { return serverAddr; } public void setServerAddr(String serverAddr) { this.serverAddr = serverAddr; } public String getDataId() { return dataId; } public void setDataId(String dataId) { this.dataId = dataId; } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } }
4、创建一个Nacos的配置类NacosConfiguration,代码如下:
@EnableConfigurationProperties(NacosPropertiesConfiguration.class) @Configuration public class NacosConfiguration { @Bean public Converter<List<FlowRuleEntity>,String> flowRuleEntityEncoder(){ return JSON::toJSONString; } /** * @Description: 注入一个转换器,将FlowRuleEntity转换成FlowRule,以及上下两个方法的正反向转换 * @Author: caobing * @Date: 2022/2/25 21:18 * @Param: [] * @Return: com.alibaba.csp.sentinel.datasource.Converter<java.lang.String,java.util.List<com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity>> */ @Bean public Converter<String,List<FlowRuleEntity>> flowRuleEntityDecoder(){ return s->JSON.parseArray(s,FlowRuleEntity.class); } /*** * @Description: 注入Nacos配置服务ConfigSerice * @Author: caobing * @Date: 2022/2/25 22:09 * @Param: [config] * @Return: com.alibaba.nacos.api.config.ConfigService */ @Bean public ConfigService nacosConfigService(NacosPropertiesConfiguration config) throws NacosException { Properties properties=new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR,config.getServerAddr()); properties.put(PropertyKeyConst.NAMESPACE,config.getNamespace()); return ConfigFactory.createConfigService(properties); } }
5、再定义一个常量类
public class NacosConstants { public static final String DATA_ID_POSTFIX="-sentinel-flow"; public static final String GROUP_ID="DEFAULT_GROUP"; }
6、实现动态从Nacos配置中心获取流控规则
@Service public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>>{ private static Logger logger=LoggerFactory.getLogger(FlowRuleNacosProvider.class); @Autowired private NacosPropertiesConfiguration nacosPropertiesConfiguration; @Autowired private ConfigService configService; @Autowired private Converter<String,List<FlowRuleEntity>> converter; /** * @Description: 通过ConfigService.getConfig方法从 NacosConfigServer中读取指定配置信息,通过converter转化为FlowRule规则 * @Author: caobing * @Date: 2022/2/25 22:41 * @Param: [appName] * @Return: java.util.List<com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity> */ @Override public List<FlowRuleEntity> getRules(String appName) throws Exception { String dataId = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString(); String rules=configService.getConfig(dataId,nacosPropertiesConfiguration.getGroupId(),3000); if(StringUtils.isEmpty(rules)){ return new ArrayList<>(); } return converter.convert(rules); } }
7、创建一个流控规则发布类
这个类,是为了在Sentinel控制台修改规则时,调用次类将数据持久化到nacos上,代码如下
@Service public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> { @Autowired private NacosPropertiesConfiguration nacosPropertiesConfiguration; @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, "appName connot be empty"); if (rules == null) { return; } String dataId = new StringBuilder(app).append(NacosConstants.DATA_ID_POSTFIX).toString(); configService.publishConfig(dataId, nacosPropertiesConfiguration.getGroupId(), converter.convert(rules)); } }
8、修改FlowControllerV2类
将上面配置的两个类引入进来,拉取和发布用这两个类实现即可,修改@Qualifier中的参数即可,代码如下:
@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;
9、在yml配置文件中添加Nacos的配置信息
sentinel.nacos.serverAddr=127.0.0.1:8848 sentinel.nacos.namespace= sentinel.nacos.group-id=DEFAULT_GROUP
10、使用mvn clean package打包运行jar即可,记住哈,是全局打包
2、规则数据同步
在上述步骤中,我们定义了一个常量类,其中data-id定义为了-sentinel-flow结尾,所以,我们集成nacos的时候,也需要以此结尾,如图:
![]()
3、启动服务
启动Sentinel Dashboard和集成工程服务,启动nacos控制台
1、在Sentinel控制台进入流控规则>新增流控规则,如图:
![]()
点击新增的时候,调试接口看一下调用的接口是不是V2的
2、新增之后的数据之后,再去Nacos控制台看一下配置列表,会发现一个
![]()
这个就是我们在Sentinel控制台新增的规则持久化配置,我们看一下详情
![]()
关于json的字段这里就不做陈述解释了,然后重启Sentinel,会发现流控规则中走V2接口的数据都会自动加载出来,那就证明整合完成并且成功了