sentinel dashboard扩展

前言

在前面的系列博文里我们已经介绍过了sentinel的dashboard的基础用法,使用dashboard可以详细的监控被保护资源的实际访问统计情况,与为不同服务配置的限流规则。sentinel dashboard的功能显然不止于此,官方留了很多功能的入口供使用者自行扩展,本篇博客抛砖引玉。对dashboard流控数据统计持久化与dashboard动态配置限流规则两方面进行扩展。其它功能入口都可以采用同样的思路进行扩展


限流统计数据持久化

sentinel默认收集到的统计数据是保存在内存中的,且有效时间只有五分钟,即使出了故障我们也无法追查五分钟之前的数据,而当接入sentinel的应用过多的情况下,内存消耗也很巨大(这一点为了实时的在dashboard查看流量趋势可以妥协),所以我们需要将统计的数据持久化下来。

sentinel本质是一个web应用程序,所以他一定有自己的dao层,我们打开浏览器查看获取浏览统计的接口

/metric/queryTopResourceMetric.json

定位到sentinel dashboard的源码,接口位于MetricController类中,这个类里注入了一个dao层的bean

@Autowired
private MetricsRepository<MetricEntity> metricStore;

操作的实体叫MetricEntity,看来这就是我们要找的东西了,它有以下属性,都是与流控统计相关的
com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity

public class MetricEntity {
    private Long id;
    private Date gmtCreate;
    private Date gmtModified;
    private String app;
    /**
     * 监控信息的时间戳
     */
    private Date timestamp;
    private String resource;
    private Long passQps;
    private Long successQps;
    private Long blockQps;
    private Long exceptionQps;

    /**
     * summary rt of all success exit qps.
     */
    private double rt;

    /**
     * 本次聚合的总条数
     */
    private int count;

    private int resourceCode;

    ...
}

MetricRepository是一个接口。我们只需要实现它然后替换controller中它的注入即可达到dao层替换的效果,它有以下三个方法
com.alibaba.csp.sentinel.dashboard.repository.metric.MetricsRepository

public interface MetricsRepository<T> {

    /**
     * Save the metric to the storage repository.
     *
     * @param metric metric data to save
     */
    void save(T metric);

    /**
     * Save all metrics to the storage repository.
     *
     * @param metrics metrics to save
     */
    void saveAll(Iterable<T> metrics);

    /**
     * Get all metrics by {@code appName} and {@code resourceName} between a period of time.
     *
     * @param app       application name for Sentinel
     * @param resource  resource name
     * @param startTime start timestamp
     * @param endTime   end timestamp
     * @return all metrics in query conditions
     */
    List<T> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime);

    /**
     * List resource name of provided application name.
     *
     * @param app application name
     * @return list of resources
     */
    List<String> listResourcesOfApp(String app);
}

MetricsRepository的具体实现就不贴了,选顺手的orm框架实现增删改查即可,需要注意的是这时候容器里MetricsRepository的实现类就不止一个了,继续使用@Autowired注入会出问题,需要使用@Qualifier指定我们自己的实现或者排除对旧的bean的注入。

建表sql参照MetricEntity的属性编写即可,保证MetricEntity的每个属性都被完整记录下来

CREATE TABLE `sentinel_metric` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `gmt_create` bigint(20) DEFAULT NULL,
  `gmt_modified` bigint(20) DEFAULT NULL,
  `app` varchar(100) DEFAULT NULL,
  `timestamp` bigint(20) DEFAULT NULL,
  `resource` varchar(100) DEFAULT NULL,
  `pass_qps` bigint(20) DEFAULT NULL,
  `success_qps` bigint(20) DEFAULT NULL,
  `block_qps` bigint(20) DEFAULT NULL,
  `exception_qps` bigint(20) DEFAULT NULL,
  `rt` double(10,0) DEFAULT NULL,
  `count` bigint(20) DEFAULT NULL,
  `resource_code` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `app_idx` (`app`) USING BTREE,
  KEY `resource_idx` (`resource`) USING BTREE,
  KEY `timestamp_idx` (`timestamp`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=69 DEFAULT CHARSET=utf8

动态配置限流规则

sentinel dashboard的前台界面提供了流控规则配置的入口,可以对接入sentinel dashboard的每个实例进行流控规则的配置,但是其实开源版本这个功能的实现只是一个壳子,所谓的流控规则是保存在dashboard本地的并没有对具体的实例生效,但是既然提供了入口,我们就可以通过sentinel的动态规则刷新机制来对其进行扩展。

同样找到流控配置的入口,增加,删除,编辑分别对应/v1/flow/rule、/v1/flow/delete.json、/v1/flow/save.json三个接口,他们都在同一个controller中
com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1

@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {

    private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);

    @Autowired
    private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
    @Autowired
    private AuthService<HttpServletRequest> authService;

    @Autowired
    private SentinelApiClient sentinelApiClient;

又看到了一个repository,这里当然可以选择替换它的实现来做到动态刷新,这里只是为了演示没必要实现接口的所有方法,所以使用了更简单的方式,在本地内存和动态数据源分别维护了一份相同的流控规则。

sentinel支持多种数据源,由于之前的示例rpc框架一直使用的都是dubbo,这里为了方便读者自己试验部署使用同一套zk作为数据源,数据格式使用解析方便的json。

现在我们开始先对dashboard进行改造,首先注入zk的java客户端bean,这里还是偷懒通过静态代码块的方式初始化了一个Curator客户端

    {
        zkClient = CuratorFrameworkFactory.newClient(remoteAddress, new ExponentialBackoffRetry(SLEEP_TIME, RETRY_TIMES));
        zkClient.start();
    }

使用Curator客户端需要引入相关依赖,注意下载下来的dash board源码会默认引入sentinel-datasource-nacos的依赖,会和某些版本Curator的guava依赖冲突,排除相关依赖或者直接删除nacos的依赖即可

		<dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.10.0</version>
        </dependency>

数据源准备完成后就是对接口的改造了,一个一个来。

/v1/flow/rule
首先是新增接口,保持原逻辑不变的同时加入对远端数据源的配置代码
com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1#apiAddFlowRule

    @PostMapping("/rule")
    public Result<FlowRuleEntity> apiAddFlowRule(HttpServletRequest request, @RequestBody FlowRuleEntity entity) {
        ...
         // 刷新zk
        String groupId = entity.getApp();
        String dataId = "sentinel-flow/rule";
        String rule = "[\n"
                + "  {\n"
                + "    \"resource\": \"" +  entity.getResource() + "\",\n"
                + "    \"controlBehavior\":" + entity.getControlBehavior() +",\n"
                + "    \"count\":" + entity.getCount() + ",\n"
                + "    \"grade\":" + entity.getGrade() + ",\n"
                + "    \"limitApp\": \""+ entity.getLimitApp() +"\",\n"
                + "    \"strategy\":" + entity.getStrategy() + "\n"
                + "  }\n"
                + "]";
        // zk的path由两部分组成,一部分是根据不同服务区分的groupId,dataId部分固定,说明这个path是用来存放流控规则的
        String path = getPath(groupId, dataId);
        Stat stat = null;
        try {
            stat = zkClient.checkExists().forPath(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 如果之前服务没有配置过限流规则则生成一个新的限流数组,放入第一条规则,反序列化后存入对应的path下
        if (stat == null) {
            try {
                zkClient.create().creatingParentContainersIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                zkClient.setData().forPath(path, rule.getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 将新的流控规则存入对应的path下,同时保证不影响已存在的规则
                byte[] oldRuleData = zkClient.getData().forPath(path);
                List<FlowRule> oldRules = JSON.parseObject(new String(oldRuleData), new TypeReference<List<FlowRule>>() {});
                rule = rule.substring(1, rule.length()-1);
                FlowRule newRule = JSON.parseObject(rule, FlowRule.class);
                oldRules.add(newRule);
                zkClient.setData().forPath(path, JSON.toJSONString(oldRules).getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return Result.ofSuccess(entity);
    }

com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1#getPath

 	private static String getPath(String groupId, String dataId) {
        String path = "";
        if (groupId.startsWith("/")) {
            path += groupId;
        } else {
            path += "/" + groupId;
        }
        if (dataId.startsWith("/")) {
            path += dataId;
        } else {
            path += "/" + dataId;
        }
        return path;
    }

/v1/flow/save.json
com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1#updateIfNotNull
在不影响原逻辑的情况下加入

@PutMapping("/save.json")
    public Result<FlowRuleEntity> updateIfNotNull(HttpServletRequest request, Long id, String app,
                                                  String limitApp, String resource, Integer grade,
                                                  Double count, Integer strategy, String refResource,
                                                  Integer controlBehavior, Integer warmUpPeriodSec,
                                                  Integer maxQueueingTimeMs) {
        ...
        // 同时更新zk里的限流规则
        String groupId = entity.getApp();
        String dataId = "sentinel-flow/rule";
        String path = getPath(groupId, dataId);
        Stat stat = null;
        try {
            stat = zkClient.checkExists().forPath(path);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (stat != null) {
            byte[] oldRuleData;
            try {
                oldRuleData = zkClient.getData().forPath(path);
                // 找出旧的规则集合
                List<FlowRule> oldRules = JSON.parseObject(new String(oldRuleData), new TypeReference<List<FlowRule>>() {});
                List<FlowRule> newRules = Lists.newCopyOnWriteArrayList(oldRules);
                for (FlowRule flowRule : oldRules) {
                    // 对同名资源的保护规则同时更新
                    if (flowRule.getResource().equals(entity.getResource())) {
                        newRules.remove(flowRule);
                        FlowRule newRule = new FlowRule();
                        BeanUtils.copyProperties(entity, newRule);
                        newRules.add(newRule);
                    }
                }
                zkClient.setData().forPath(path, JSON.toJSONString(newRules).getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
            logger.info("publish flow rules fail after rule update");
        }
        return Result.ofSuccess(entity);
    }

/v1/flow/delete.json
com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1#delete
同样在不影响原逻辑的情况下加入对远端数据源的刷新

 	@DeleteMapping("/delete.json")
    public Result<Long> delete(HttpServletRequest request, Long id) {
    	...
    	 // 同时删除zk里的限流规则
        String groupId = oldEntity.getApp();
        String dataId = "sentinel-flow/rule";
        String path = getPath(groupId, dataId);
        Stat stat = null;
        try {
            stat = zkClient.checkExists().forPath(path);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (stat != null) {
            byte[] oldRuleData;
            try {
                oldRuleData = zkClient.getData().forPath(path);
                List<FlowRule> oldRules = JSON.parseObject(new String(oldRuleData), new TypeReference<List<FlowRule>>() {});
                List<FlowRule> newRules = Lists.newCopyOnWriteArrayList(oldRules);
                for (FlowRule flowRule : oldRules) {
                    // 如果有多条对相同资源的规则会一并删除
                    if (flowRule.getResource().equals(oldEntity.getResource())) {
                        newRules.remove(flowRule);
                    }
                }

                if (newRules.size() == 0) {
                    // 如果删除的是最后一条规则,直接清除path
                    zkClient.delete().forPath(path);
                } else {
                    zkClient.setData().forPath(path, JSON.toJSONString(newRules).getBytes());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return Result.ofSuccess(id);
    }

对dashboard改造完了接下来就是对资源提供方的改造,只需要修改获取规则的方式为从zk动态刷新即可。资源提供方需要引入相关依赖

		<dependency>
		    <groupId>com.alibaba.csp</groupId>
		    <artifactId>sentinel-datasource-zookeeper</artifactId>
		    <version>x.y.z</version>
		</dependency>
    private void loadRulesFromZk() throws Exception {
        String host="127.0.0.1:2181";
        String path="/dubbo_provider/sentinel-flow/rule";
        ZookeeperDataSource<List<FlowRule>> zkDataSource = new ZookeeperDataSource<>(host, path, flowRuleListParser);
        FlowRuleManager.register2Property(zkDataSource.getProperty());
    }

注意这里我为了演示方便zk path是写死了的,实际上与dashboard的逻辑对应,dubbo_provider对应的其实是这个服务的appId( -Dproject.name=dubbo_provider ),而sentinel-flow是固定死的dataId用于标识,实际生产中appId需要改为从环境变量变量中获取。

测试
重新打包sentinel dashboard然后注册上dubbo-provider分别进行增加,修改,删除规则的测试,结果都符合配置
在这里插入图片描述
这样对简单流控规则的控制台配置改造就完成了,dashboard还提供了对热点参数限流配置,集群限流配置等其他限流规则配置的入口,根据自己的需要对其按照同样的方式进行扩展即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章