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還提供了對熱點參數限流配置,集羣限流配置等其他限流規則配置的入口,根據自己的需要對其按照同樣的方式進行擴展即可。

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