Spring Cloud Alibaba入门之Sentinel Dashboard基于Nacos的规则存储和同步

Sentinel Dashboard基于Nacos的规则存储和同步

1、规则管理和推送的原理

1>、详细请参考官方文档:《在生产环境中使用 Sentinel》

2>、官方文档:《动态规则扩展》

3>、也可以参考程序猿DD的相关博客-《Sentinel Dashboard中修改规则同步到Apollo》

2、 下载源码

  通过Github或Gitee下载Sentinel源码,这里选择了v1.8.0版本的源码。其中,

  这里下载的项目包括了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);
    }
}

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/111501746