不改一行源码,实现 sentinel-dashboard 所有配置支持 apollo 持久化

592 阅读5分钟

sentinel-dashboard apollo 定制版

sentinel-dashboard-apollo 是从官方 Sentinel fork 的 dashboard 定制版,支持所有配置持久化到 apollo。

sentinel-dashboard 为什么需要定制

Sentinel 是阿里巴巴开源的流量治理组件。功能非常齐全,包括了:请求过滤、降级、限流、流量监控等功能。如果对 sentinel 还不是很了解可以查看官方文档:sentinelguard.io/zh-cn/docs/…

虽然 sentinel 的设计非常优秀,基本上满足了流量治理的所有需求,但是 sentinel-dashboard(管理后台)的配置都是存储在内存,在服务重启后就会丢失。所以 sentinel 目前是不具备在生产环境上使用的。即使 sentinel 客户端是支持了从 apollo、consul、etcd、eureka、nacos、redis、spring-cloud-config、zookeeper 读取配置,但是如果不使用 dashboard,直接手动修改配置的话,官网也没有提供详细的参数配置文档,想知道哪些参数可配置,需要自己查看源码,使用上非常不友好。

而这个问题早在 2020 年就有人提出来了(github issue) dashboard 配置持久化功能,但是官方至今(2022-07)依然没有实现这个功能。

github.com/alibaba/Sen… github.com/alibaba/Sen…

值得一提的是,阿里云的商业版 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,继续往下跟踪就可以知道完整的处理流程了。

01.sentinel-dashboard-api.png

例如:新增网关流控规则的接口是 /gateway/flow/new.json

通过分析不难发现,不管是什么配置,对应增删改查的接口路径都是类似的。

sentinel 规则总共有 7 中类型,都实现了 RuleEntity 接口

我们需要实现的也是将这7种数据类型持久化到 apollo。

从 sentinel 的架构设计上可以知道分为 sentinel 客户端(也就是我们的应用)和 sentinel-dashboard(管理后台)。

通过分析 FlowControllerV1 源码,可以知道配置读写都是通过 SentinelApiClient 来完成的。

  • 读数据:通过 SentinelApiClient 请求客户端,拉取配置,然后更新到内存

    01.sentinel-dashboard-rule-fetch.png

  • 写数据:先保存到内存,然后调用 SentinelApiClient 将请求同步到客户端

    01.sentinel-dashboard-rule-save.png

改造实现

对于在生产环境中使用 Sentinel,官网文档中给我们介绍了几种模式。通过上面源码分析的流程实现的就是原始模式,我们的改造方案是要实现推模式。

对于改造方案,如果做过这方面调研的同学,找到的资料基本上都是只实现了流量控制规则持久化,而剩下其他 6 中规则并没有实现持久化,包括姚秋辰(姚半仙)老师在极客时间上的专栏《Spring Cloud 微服务项目实战》第 20 章节Sentinel 实战:如何接入 Nacos 实现规则持久化? 中的方案也只是把流控规则配置做了持久化。大家可以自己搜索一下,这里不再赘述。

以上方案都存在几个不足。

  1. 只实现了流控规则持久化
  2. 需要修改源码(包括前端代码),不放面后续滚动升级
  3. 如果 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);
    }
}

对应 apollo 读写数据的代码在 test 包下已经有了,拿过来稍加改动就可以了

完整的代码实现可以在 github 上查看:github.com/fengjx/Sent…,整个改动没有修改一行源码,只是新增了一些类,方便后续升级不会引起代码冲突。

改造后的效果

dashboard 配置

01.sentinel-dashboard-demo.png

apollo 配置

01.sentinel-dashboard-apollo-settings.png

升级&版本维护

本项目从 sentinel 官方 github 仓库 fork,只针对 dashboard 模块进行修改,保持与官方发布版本同步修改,版本对应关系

Sentinelsentinel-dashboard-apollo说明
branch - masterbranch: dashboard/apollo/master保持最新版本与官方 master 同步
tag - 1.8.4branch: dashboard/apollo/1.8.4从官方发布的 tag checkout 出来进行修改
tag - 1.8.4tag: dashboard/apollo/v1.8.4修改完成后发布tag

相关文档