在上篇教程中我们分别介绍了如何将Sentinel的限流规则存储到Nacos中。同时这套整合方案存在一个不足之处:不论采用什么配置中心,限流规则都只能通过Nacos界面来完成修改才能得到持久化存储,而在Sentinel Dashboard中修改限流规则虽然可以生效,但是不会被持久化到配置中心。而在这两个配置中心里存储的数据是一个Json格式,当存储的规则越来越多,对该Json配置的可读性与可维护性会变的越来越差。所以,下面我们就来继续探讨这个不足之处,并给出相应的解决方案。

代码实现

下面直接来看看如何实现的具体改造步骤,这里参考了Sentinel Dashboard源码中关于Nacos实现的测试用例。但是由于考虑到与Spring Cloud Alibaba的结合使用,略作修改。
第一步:修改pom.xml中的sentinel-datasource-nacos的依赖,将test注释掉,这样才能在主程序中使用

  1. <dependency>
  2. <groupId>com.alibaba.csp</groupId>
  3. <artifactId>sentinel-datasource-nacos</artifactId>
  4. <!--<scope>test</scope>-->
  5. </dependency>

第二步:找到resources/app/scripts/directives/sidebar/sidebar.html中的这段代码:

  1. <li ui-sref-active="active" ng-if="!entry.isGateway">
  2. <a ui-sref="dashboard.flowV1({app: entry.app})">
  3. <i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a>
  4. </li>

更换为

<!-- dashboard.flow 更换到V2接口-->
<li ui-sref-active="active" ng-if="!entry.isGateway">
  <a ui-sref="dashboard.flow({app: entry.app})">
    <i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a>
</li>

第三步:在com.alibaba.csp.sentinel.dashboard.rule包下新建一个nacos包,用来编写针对Nacos的扩展实现。

@Configuration
public class NacosConfig {

    @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 {
        Properties properties = new Properties();
        //1.6.~中properties.put(PropertyKeyConst.SERVER_ADDR, "localhost");即可
        properties.put(PropertyKeyConst.SERVER_ADDR, "localhost:8848");
//        properties.put(PropertyKeyConst.NAMESPACE, "130e71fa-97fe-467d-ad77-967456f2c16d");
        return ConfigFactory.createConfigService(properties);
    }
}

注意:在1.8.~的SentinelDashboard中 如果出现Incorrect serverAddr address : localhost, example should like ip:port or domain:port 异常,需要加入端口号。

如果用到了namespace隔离环境,可以在nacosConfigService方法中再加入配置,比如:properties.put(PropertyKeyConst.NAMESPACE, “130e71fa-97fe-467d-ad77-967456f2c16d”);
第五步:实现Nacos的配置拉取。

@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter<String, List<FlowRuleEntity>> converter;

    public static final String FLOW_DATA_ID_POSTFIX = "-sentinel";
    public static final String GROUP_ID = "DEFAULT_GROUP";

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        String rules = configService.getConfig(appName + FLOW_DATA_ID_POSTFIX, GROUP_ID, 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}
  • getRules方法中的appName参数是Sentinel中的服务名称。
  • configService.getConfig方法是从Nacos中获取配置信息的具体操作。其中,DataId和GroupId分别对应客户端使用时候的对应配置。具体如下:

    spring.cloud.sentinel.datasource.ds.nacos.groupId=DEFAULT_GROUP
    spring.cloud.sentinel.datasource.ds.nacos.dataId=${spring.application.name}-sentinel
    

    第六步:实现Nacos的配置推送。

    @Component("flowRuleNacosPublisher")
    public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
    
      @Autowired
      private ConfigService configService;
      @Autowired
      private Converter<List<FlowRuleEntity>, String> converter;
    
      public static final String FLOW_DATA_ID_POSTFIX = "-sentinel";
      public static final String GROUP_ID = "DEFAULT_GROUP";
    
      @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 + FLOW_DATA_ID_POSTFIX, GROUP_ID, converter.convert(rules));
      }
    }
    
  • 这里的大部分内容与上一步中的实现一致。主要就是Nacos中存储配置的DataId和GroupId不要弄错。

第七步:修改com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2中DynamicRuleProvider和DynamicRulePublisher注入的Bean,改为上面我们编写的针对Nacos的实现:

@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

第八步:如果大家启动后,欣喜的发现未生效,是因为前端的改动还未打包。

  1. 安装 node npm gulp环境
  2. 在webapp目录下执行 npm install
  3. 若报错 gulp 报错:ReferenceError:primordials is not defined 在pagejson同级目录下新建 npm-shrinkwrap.json 重新执行 npm install
  4. 执行 gulp build
  5. 可以观察到 webapp/resources/dist里面文件更新了,我们再访问sentinel试试image.png

可以看到,url上面的版本已经更换成了v2

  1. 在sentinel上修改流控规则配置, 查看nacos配置。image.png

可以看到修改成功了


启动应用拉取nacos配置时,异常处理

convert error: convert 0 rules but there are 1 rules from datasource. RuleClass: FlowRule

  1. 从nacos拉取配置的时候会进行规则转换,我们看看converRule里面

image.png

  1. nacos上的规则 ruleStr如下

    {"app":"sentinel-dashboard-nacos",
    "gmtModified":1625204049062,
    "resource":"/server",
    "grade":1,"controlBehavior":0,
    "count":5.0,"id":4,
    "limitApp":"default",
    "strategy":0,
    "clusterMode":false}
    

    image.png

  2. 错误提示如下,表示未知的 “app”属性

    com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "app"
    (class com.alibaba.csp.sentinel.slots.block.flow.FlowRule), not marked as ignorable 
    (12 known properties: "warmUpPeriodSec", "refResource", "rater", "strategy", "limitApp", 
    "grade", "maxQueueingTimeMs", "clusterConfig", "clusterMode", "resource", "count", "controlBehavior"])
    at [Source: (String)"{"app":"sentinel-dashboard-nacos","gmtModified":1625204049062,"
    resource":"/server","grade":1,"controlBehavior":0,"count":5.0,"id":4,
    "limitApp":"default","strategy":0,"clusterMode":false}"; line: 1, column: 9]
    (through reference chain: com.alibaba.csp.sentinel.slots.block.flow.FlowRule["app"])
    
  3. 解决方法 : 修改sentinel代码,推送到nacos上的无用属性不再序列化为json

    public class FlowRuleEntity implements RuleEntity {
     @JSONField(serialize = false)
     private Long id;
     @JSONField(serialize = false)
     private String app;
     @JSONField(serialize = false)
     private String ip;
     @JSONField(serialize = false)
     private Integer port;
     private String limitApp;
    ......
     @JSONField(serialize = false)
     private Date gmtCreate;
     @JSONField(serialize = false)
     private Date gmtModified;
    
  4. 可以对比一下上文中的配置

image.png

  1. 同样的集群配置里面也会出现属性不匹配情况

    public class ClusterFlowConfig {
    ......
     @JSONField(serialize = false)
     private long resourceTimeout = 2000;
    
     @JSONField(serialize = false)
     private int resourceTimeoutStrategy = RuleConstant.DEFAULT_RESOURCE_TIMEOUT_STRATEGY;
    
     @JSONField(serialize = false)
     private int acquireRefuseStrategy = RuleConstant.DEFAULT_BLOCK_STRATEGY;
    
     @JSONField(serialize = false)
     private long clientOfflineTime = 2000;