不改一行源码,实现 sentinel-dashboard 所有配置支持 apollo 持久化
# sentinel-dashboard apollo 定制版
sentinel-dashboard-apollo 是从官方 Sentinel (opens new window) fork 的 dashboard 定制版,支持所有配置持久化到 apollo。
- github:https://github.com/fengjx/Sentinel (opens new window)
- gitee:https://gitee.com/fengjx/Sentinel (opens new window)
- 使用文档:http://blog.fengjx.com/sentinel-dashboard-apollo-wiki (opens new window)
- spring-boot(cloud) 整合 demo: https://github.com/fengjx/sentinel-dashboard-demo (opens new window)
# sentinel-dashboard 为什么需要定制
Sentinel (opens new window) 是阿里巴巴开源的流量治理组件。功能非常齐全,包括了:请求过滤、降级、限流、流量监控等功能。如果对 sentinel 还不是很了解可以查看官方文档:https://sentinelguard.io/zh-cn/docs/basic-api-resource-rule.html (opens new window)
虽然 sentinel 的设计非常优秀,基本上满足了流量治理的所有需求,但是 sentinel-dashboard(管理后台)的配置都是存储在内存,在服务重启后就会丢失。所以 sentinel 目前是不具备在生产环境上使用的。即使 sentinel 客户端是支持了从 apollo、consul、etcd、eureka、nacos、redis、spring-cloud-config、zookeeper 读取配置,但是如果不使用 dashboard,直接手动修改配置的话,官网也没有提供详细的参数配置文档,想知道哪些参数可配置,需要自己查看源码,使用上非常不友好。
而这个问题早在 2020 年就有人提出来了(github issue) dashboard 配置持久化功能,但是官方至今(2022-07)依然没有实现这个功能。
issue
https://github.com/alibaba/Sentinel/issues/1759 (opens new window)
https://github.com/alibaba/Sentinel/issues/2179 (opens new window)
值得一提的是,阿里云的商业版 sentinel-dashboard 是有这个功能的。并且在 test 代码中可以看到有对应持久化实现的。所以这很明显官方并不想在开源版实现这个功能,需要我们自己去实现。这其中的原由已经非常明显了。
# 方案选型
目前已经实现的组件中,sentinel 客户端已经支持:
- apollo
- consul
- etcd
- eureka
- nacos
- redis
- spring-cloud-config
- zookeeper
以最小化改动原则,我们可以从上面其中一个作为持久化存储方案,否则就需要自己再开发一套客户端同步数据组件。
这里我选择 apollo,理由是:apollo 作为配置中心,有丰富的配置功能,与其他方案如 nacos 都要完善和稳定许多。而其他如 redis、zookeeper 在数据排查方面都不是太方便。
# 源码分析
sentinel-dashboard 的源码结构非常简单。后端使用 spring-boot,前端使用 angular1。
我们打开浏览器抓包工具,在界面上操作增删改查对应配置,就可以知道对应的接口是多少,然后通过接口路径找到对应的 Controller,继续往下跟踪就可以知道完整的处理流程了。
例如:新增网关流控规则的接口是 /gateway/flow/new.json
通过分析不难发现,不管是什么配置,对应增删改查的接口路径都是类似的。
sentinel 规则总共有 7 中类型,都实现了 RuleEntity
接口
我们需要实现的也是将这7种数据类型持久化到 apollo。
从 sentinel 的架构设计上可以知道分为 sentinel 客户端(也就是我们的应用)和 sentinel-dashboard(管理后台)。
通过分析 FlowControllerV1
源码,可以知道配置读写都是通过 SentinelApiClient
来完成的。
读数据:通过
SentinelApiClient
请求客户端,拉取配置,然后更新到内存写数据:先保存到内存,然后调用
SentinelApiClient
将请求同步到客户端
# 改造实现
对于在生产环境中使用 Sentinel,官网文档 (opens new window)中给我们介绍了几种模式。通过上面源码分析的流程实现的就是原始模式,我们的改造方案是要实现推模式。
对于改造方案,如果做过这方面调研的同学,找到的资料基本上都是只实现了流量控制规则持久化,而剩下其他 6 中规则并没有实现持久化,包括姚秋辰(姚半仙)老师在极客时间上的专栏《Spring Cloud 微服务项目实战》第 20 章节Sentinel 实战:如何接入 Nacos 实现规则持久化? (opens new window) 中的方案也只是把流控规则
配置做了持久化。大家可以自己搜索一下,这里不再赘述。
以上方案都存在几个不足。
- 只实现了流控规则持久化
- 需要修改源码(包括前端代码),不放面后续滚动升级
- 如果 7 中类型数据都做持久化的,那需要修改的地方会比较多
通过上面源码分析可以知道,其实数据拉取和推送都是通过SentinelApiClient
的 fetchXXX(拉取数据)和 setXXX, modifyXXX(推送数据)方法来实现的,所以我们只要把对应的方法改成从 apollo 拉取数据和将数据推送到 apollo 上就可以了,
因为 SentinelApiClient
没有定义接口,所以要在不改变源码的情况下改变它的默认行为,就要通过aop来实现了。
下面是实现网关流控规则读写 apollo 的示例代码。
@Aspect
@Component
public class SentinelApiClientAspect {
private static final Logger LOG = LoggerFactory.getLogger(SentinelApiClientAspect.class);
@SuppressWarnings("PMD.ThreadPoolCreationRule")
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(
new NamedThreadFactory("sentinel-dashboard-api-aspect"));
@Resource
private DynamicRuleStoreFactory factory;
@Pointcut("execution(public * com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient.fetchGatewayFlowRules(..))")
public void fetchGatewayFlowRulesPointcut() {
}
@Pointcut("execution(public * com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient.modifyGatewayFlowRules(..))")
public void modifyGatewayFlowRulesPointcut() {
}
/**
* 拉取网关流控规则配置
*/
@Around("fetchGatewayFlowRulesPointcut()")
public Object fetchGatewayFlowRules(final ProceedingJoinPoint pjp) throws Throwable {
return fetchRulesWithCompletableFuture(pjp, RuleType.GW_FLOW);
}
/**
* 推送网关流控规则配置
*/
@Around("modifyGatewayFlowRulesPointcut()")
public Object modifyGatewayFlowRules(final ProceedingJoinPoint pjp) throws Throwable {
return publishRules(pjp, RuleType.GW_FLOW);
}
// 中间省略了部分代码,完整代码可以从 github 查看
private Object fetchRules(ProceedingJoinPoint pjp, RuleType ruleType) throws Throwable {
DynamicRuleStore<?> dynamicRuleStore = factory.getDynamicRuleStoreByType(ruleType);
if (dynamicRuleStore == null) {
return pjp.proceed();
}
Object[] args = pjp.getArgs();
String app = (String) args[0];
return dynamicRuleStore.getRules(app);
}
private CompletableFuture<Object> fetchRulesWithCompletableFuture(ProceedingJoinPoint pjp, RuleType ruleType) {
return CompletableFuture.supplyAsync(() -> {
try {
return fetchRules(pjp, ruleType);
} catch (Throwable e) {
throw new RuntimeException("fetch rules error: " + ruleType.getName(), e);
}
}, EXECUTOR);
}
@SuppressWarnings("unchecked")
private boolean publishRules(ProceedingJoinPoint pjp, RuleType ruleType) {
DynamicRuleStore<RuleEntity> dynamicRuleStore = factory.getDynamicRuleStoreByType(ruleType);
Object[] args = pjp.getArgs();
String app = (String) args[0];
List<RuleEntity> rules = (List<RuleEntity>) args[3];
try {
dynamicRuleStore.publish(app, rules);
return true;
} catch (Exception e) {
LOG.error("publish rules error", e);
return true;
}
}
private CompletableFuture<Void> publishRulesWithCompletableFuture(ProceedingJoinPoint pjp, RuleType ruleType) {
return CompletableFuture.runAsync(() -> publishRules(pjp, ruleType), EXECUTOR);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
对应 apollo 读写数据的代码在 test 包下已经有了,拿过来稍加改动就可以了
完整的代码实现可以在 github 上查看:https://github.com/fengjx/Sentinel (opens new window),整个改动没有修改一行源码,只是新增了一些类,方便后续升级不会引起代码冲突。
# 改造后的效果
dashboard 配置
apollo 配置
# 升级&版本维护
本项目从 sentinel 官方 github 仓库 fork,只针对 dashboard 模块进行修改,保持与官方发布版本同步修改,版本对应关系
Sentinel | sentinel-dashboard-apollo | 说明 |
---|---|---|
branch - master | branch: dashboard/apollo/master | 保持最新版本与官方 master 同步 |
tag - 1.8.4 | branch: dashboard/apollo/1.8.4 | 从官方发布的 tag checkout 出来进行修改 |
tag - 1.8.4 | tag: dashboard/apollo/v1.8.4 | 修改完成后发布tag |