Sentinel系列之集成Nacos实现规则同步持久化

  • Post author:
  • Post category:其他


这个标题是什么意思呢,原因是上一篇章:使用维护在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接口的数据都会自动加载出来,那就证明整合完成并且成功了



版权声明:本文为qq_38377525原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。