方案cicd

sentinel之dashboard改造

2021-08-30  本文已影响0人  bear_small

前言

在sentinel控制台设备的规则信息默认都是存储在内存当中,无论是重启了 sentinel 的客户端还是 sentinel 的控制台,所设置的规则都会丢失。如果要线上环境使用,那肯定是不行,解决方法也两种:

  • 一是使用阿里云付费版本,即 AHAS Sentinel ,好处是不用踩坑(==坑很多==)
  • 二是如题所示,自己改造dashboard客户端集成数据源

见官方文档 在生产环境中使用 Sentinel

找过很多关于集成的相关文章,基本都是仿照官网给的限流规则的例子来做的,如果仅仅按照官网案例实现,那是绝对不能用于线上环境去使用的。好了,废话不多说,开始我们的改造之旅吧。

可查看改造源码文件toBearShmily / sentinel-dashboard-nacos-1.8.0

环境版本相关说明

  1. 自定义数据源:nacos-1.4.1
  2. sentinel-dashboard版本:sentinel-1.8.0
  3. 通过推模式持久化规则数据到nacos

拉取 sentinel 源码文件

拉取源码,地址:alibaba/Sentinel ,导入idea,注意拉取的版本,我使用的是1.8.0。主要与你自己项目集成的版本,可见官方说明版本说明

修改点

package.png
@Configuration
public class NacosConfig {
        @Value("${sentinel.datasource.nacos.server-addr:localhost:8848}")
        private String serverAddr;
        @Value("${sentinel.datasource.nacos.namespace:public}")
        private String namespace;
        @Bean
        public ConfigService nacosConfigService() throws Exception {
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
            properties.put(PropertyKeyConst.NAMESPACE, namespace);
            return ConfigFactory.createConfigService(properties);
        }
}

此处获取配置中的nacos配置,初始化nacos中config模块(ConfigService)操作的依赖bean,并注入当前容器

增加如下规则配置项:

/**
 * add cc for `degrade,authority,system`
 */
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules";
public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules";
public static final String GETWAY_API_DATA_ID_POSTFIX = "-gateway-api-rules";
public static final String GETWAY_FLOW_DATA_ID_POSTFIX = "-gateway-flow-rules";
package com.alibaba.csp.sentinel.dashboard.rule.nacos;

import com.alibaba.csp.sentinel.dashboard.util.JSONUtils;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 定义 Nacos数据源 推送,拉取操作
 * @Author sisyphus
 * @Date 2021/8/25 15:11
 * @Version V-1.0
 */
public interface CustomDynamicRule<T> {

    /**
    *@Author sisyphus
    *@Description 远程获取规则-nacos数据源
    *@Date 2021/8/25 17:24
    *@Param [configService, appName, postfix, clazz-反序列化类]
    *@return java.util.List<T>
    **/
    default List<T> fromNacosRuleEntity(ConfigService configService, String appName, String postfix, Class<T> clazz) throws NacosException {
        AssertUtil.notEmpty(appName, "app name cannot be empty");
        String rules = configService.getConfig(
                genDataId(appName, postfix),
                NacosConfigUtil.GROUP_ID,
                3000
        );
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return JSONUtils.parseObject(clazz, rules);
    }
    
    /**
     * @title setNacosRuleEntityStr
     * @description  将规则序列化成JSON文本,存储到Nacos server中
     * @author sisyphus
     * @param: configService nacos config service
     * @param: appName       应用名称
     * @param: postfix       规则后缀 eg.NacosConfigUtil.FLOW_DATA_ID_POSTFIX
     * @param: rules         规则对象
     * @updateTime 2021/8/26 15:47 
     * @throws  NacosException 异常
     **/
    default void setNacosRuleEntityStr(ConfigService configService, String appName, String postfix, List<T> rules) throws NacosException{
        AssertUtil.notEmpty(appName, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        String dataId = genDataId(appName, postfix);

        //存储,推送远程nacos服务配置中心
        boolean publishConfig = configService.publishConfig(
                dataId,
                NacosConfigUtil.GROUP_ID,
                printPrettyJSON(rules)
        );
        if(!publishConfig){
            throw new RuntimeException("publish to nacos fail");
        }
    }

    /**
    *@Author sisyphus
    *@Description 组装nacos dateId
    *@Date 2021/8/25 16:34
    *@Param [appName, postfix]
    *@return java.lang.String
    **/
    default String genDataId(String appName, String postfix) {
        return appName + postfix;
    }

    /**
    *@Author sisyphus
    *@Description 规则对象转换为json字符串,并带有格式化
    *@Date 2021/8/25 17:19
    *@Param [obj]
    *@return java.lang.String
    **/
    default String printPrettyJSON(Object obj) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return JSON.toJSONString(obj);
        }
    }
}

FlowRuleNacosProvider 代码

package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.CustomDynamicRule;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;

/**
 * @author Eric Zhao
 * @since 1.4.0
 */
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>>, CustomDynamicRule<FlowRuleEntity> {

    @Autowired
    private ConfigService configService;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        AssertUtil.notEmpty(appName, "app name cannot be empty");
        return fromNacosRuleEntity(configService, appName, NacosConfigUtil.FLOW_DATA_ID_POSTFIX, FlowRuleEntity.class);
    }
}

FlowRuleNacosPublisher 代码

package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.CustomDynamicRule;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;

/**
 * @author Eric Zhao
 * @since 1.4.0
 */
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>>, CustomDynamicRule<FlowRuleEntity> {

    @Autowired
    private ConfigService configService;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        setNacosRuleEntityStr(configService, app, NacosConfigUtil.FLOW_DATA_ID_POSTFIX, rules);
    }
}

此处实现与官网案例存在不同,两个类都实现了自定义接口CustomDynamicRule,将数据源操作的动作抽象为默认接口,后续如果需要其他数据源的操作,可直接在接口中增加操作方法即可, 其余规则对应操作类与以上类似,仅仅泛型对应匹配的规则类型不同(对应AuthorityRuleEntity , DegradeRuleEntity , ParamFlowRuleEntity , SystemRuleEntity , ApiDefinitionEntity , GatewayFlowRuleEntity

我全部使用新建类,并未在原有类做更改,全部在包 v2下,如图:

1630056096708.png

接下来还是以flow为例进行相关说明

说明:该类的作用,摒弃掉所有内存存储操作

问题

Sentinel Dashboard官方版本中支持创建DynamicRuleProvider和DynamicRulePublisher来和外部数据源通信,但是仅仅增加这两个类的实现并不够,使用下来发现的问题

  1. 新建RuleEntity的时候,ID是从代码中的AtomicLong变量获取的,每次这个变量都是从0开始计数,也就意味着每次重启之后ID都重新计数,这在使用内存存储rule的时候没有问题,但是一旦有外部数据源,这地方逻辑就不对了。
  2. 新建RuleEntity之后,会将当前所有的RuleEntity发布到外部数据源,如果是从资源列表页(请求链路或簇点链路)直接创建规则,那么这时候还没从外部数据源加载已存在的rule(只有访问对应的规则页面的list接口才会从远程加载),当前rule创建完成之后发布到外部数据源的时候,只会把刚创建的这个发布出去,导致之前存在的rule被覆盖掉。
  3. 在原有的各个Controller的list方法中,在从外部加载rule之后,会调用repository的saveAll方法(就是InMemoryRuleRepositoryAdapter的saveAll方法),在该方法中会清除所有的rule,这相当于内存中同时只能有一个app的rule集合存在。

改动

  1. 不再使用InMemoryRuleRepositoryAdapter的各个实现类作为repository,仅使用外部数据源,即InDataSourceRuleStore
  2. 增加NacosConfigNacosConfigUtil,作为和Nacos通信的基础类
  3. 增加rule > nacos包的以下类(以ProviderPublisher结尾),用于各类Rule和外部数据源交互
  4. 增加InDataSourceRuleStore类,该类提供了findById、list、save、update、delete方法用于和外部数据源交互,提供了format方法,用于格式化从外部数据源获取到的数据,提供了merge方法用于在update时做数据整合
  5. 修改controller类,继承InDataSourceRuleStore类,不再使用原有的repository进行存储,使用InDataSourceRuleStore定义方法操作,同时修改注入的DynamicRuleProviderDynamicRulePublisher实现
 @RestController
 @RequestMapping(value = "/v2/flow")
 public class FlowControllerV2 extends InDataSourceRuleStore<FlowRuleEntity>{
 
     private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class);
 
     @Autowired
 //    @Qualifier("flowRuleDefaultProvider")
     @Qualifier("flowRuleNacosProvider")
     private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
     @Autowired
 //    @Qualifier("flowRuleDefaultPublisher")
     @Qualifier("flowRuleNacosPublisher")
     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);*/
             List<FlowRuleEntity> rules = this.list(ruleProvider, app);
             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());*/
             this.save(rulePublisher, ruleProvider, entity);
         } 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);*/
         FlowRuleEntity oldEntity = this.findById(ruleProvider, entity.getApp(), 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());
         */
         entity.setId(id);
         /*Date date = new Date();
         entity.setGmtCreate(oldEntity.getGmtCreate());
         entity.setGmtModified(date);*/
         Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
         if (checkResult != null) {
             return checkResult;
         }
         try {
             /*entity = repository.save(entity);
             if (entity == null) {
                 return Result.ofFail(-1, "save entity fail");
             }*/
             return this.update(rulePublisher, ruleProvider, entity);
         } catch (Throwable throwable) {
             logger.error("Failed to update flow rule", throwable);
             return Result.ofThrowable(-1, throwable);
         }
     }
 
     @DeleteMapping("/rule/{id}")
     @AuthAction(PrivilegeType.DELETE_RULE)
     public Result<Long> apiDeleteRule(@PathVariable("id") Long id, @RequestParam("app") String app) {
         if (id == null || id <= 0) {
             return Result.ofFail(-1, "Invalid id");
         }
         if (StringUtils.isEmpty(app)) {
             return Result.ofFail(-1, "Invalid app");
         }
         /*FlowRuleEntity oldEntity = repository.findById(id);
         if (oldEntity == null) {
             return Result.ofSuccess(null);
         }*/
 
         try {
             /*repository.delete(id);*/
             return this.delete(rulePublisher, ruleProvider, id, app);
         } catch (Exception e) {
             return Result.ofFail(-1, e.getMessage());
         }
     }
 
     @Override
     protected void format(FlowRuleEntity entity, String app) {
         entity.setApp(app);
         if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) {
             entity.setId(entity.getClusterConfig().getFlowId());
         }
         Date date = new Date();
         entity.setGmtCreate(date);
         entity.setGmtModified(date);
         entity.setLimitApp(entity.getLimitApp().trim());
         entity.setResource(entity.getResource().trim());
     }
 
     @Override
     protected void merge(FlowRuleEntity entity, FlowRuleEntity oldEntity) {
         entity.setApp(oldEntity.getApp());
         entity.setIp(oldEntity.getIp());
         entity.setPort(oldEntity.getPort());
         Date date = new Date();
         entity.setGmtCreate(oldEntity.getGmtCreate());
         entity.setGmtModified(date);
     }
 }

注释部分为原有代码

最后可能有人需要这个util

package com.alibaba.csp.sentinel.dashboard.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class JSONUtils {
    public static <T> String toJSONString(Object object) {
        try {
            return new ObjectMapper().writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return new ObjectMapper()
                .getTypeFactory()
                .constructParametricType(collectionClass, elementClasses);
    }

    public static <T> List<T> parseObject(Class<T> clazz, String string) {
        JavaType javaType = getCollectionType(ArrayList.class, clazz);
        try {
            return (List<T>) new ObjectMapper().readValue(string, javaType);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

改造过程碰到问题颇多,感谢 github-franciszhao 和官方文档 在生产环境中使用 Sentinel 提供相关帮助

谢谢大家关注,点个赞呗~
如需转载请标明出处,谢谢~~

上一篇 下一篇

猜你喜欢

热点阅读