前言
在前面的系列博文裏我們已經介紹過了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還提供了對熱點參數限流配置,集羣限流配置等其他限流規則配置的入口,根據自己的需要對其按照同樣的方式進行擴展即可。