Soul网关第7天:使用ZooKeeper同步数据到网关

919 阅读4分钟

一、准备工作

已经到了第7点,可以不用那么啰嗦,简洁一下,准备工作要做的是

  1. 启动zk
  2. 打开soul-admin即控制台(后文用控制台代替)和 soul-bootstrap即网关服务(后文用网关服务代替)配置中的ZooKeeper配置
  3. 启动控制台和网关服务

二、ZooKeeper方式同步数据

先抄一段Soul官方文档

基于 zookeeper 的同步原理很简单,主要是依赖 zookeeper 的 watch 机制,soul-web 会监听配置的节点,soul-admin 在启动的时候,会将数据全量写入 zookeeper,后续数据发生变更时,会增量更新 zookeeper 的节点,与此同时,soul-web 会监听配置信息的节点,一旦有信息变更时,会更新本地缓存。

然后继续上这幅图,依旧从 DataChangedEventDispatcher 类讲起

DataChangedEventDispatcher#onApplicationEvent 方法会遍历注入进来 DataChangedListener 接口的实现类,此处因为打开了zk相关配置,注入了 ZookeeperDataChangedListener bean

1、从监听事件往下看

此处都是实现细节了,举一个具体的发送 Rule 数据,使用的 ZookeeperDataChangedListener#onRuleChanged 方法给节点更新数据

    @Override
    public void onRuleChanged(final List<RuleData> changed, final DataEventTypeEnum eventType) {
        if (eventType == DataEventTypeEnum.REFRESH) {
            final String selectorParentPath = ZkPathConstants.buildRuleParentPath(changed.get(0).getPluginName());
            deleteZkPathRecursive(selectorParentPath);
        }
        for (RuleData data : changed) {
            final String ruleRealPath = ZkPathConstants.buildRulePath(data.getPluginName(), data.getSelectorId(), data.getId());
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(ruleRealPath);
                continue;
            }
            final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getPluginName());
            createZkNode(ruleParentPath);
            //create or update
            upsertZkNode(ruleRealPath, data);
        }
    }

2、从发布事件往上看

又到了昨天的 Spring 框架中的事件发布类 ApplicationEventPublisher,看一下有哪些调用者。

其实依旧是昨天看到的那些,大部分是控制台的 REST 接口更新数据,然后发布事件发送数据给 zkNode。这里有一个ZookeeperDataInit#run方法

public class ZookeeperDataInit implements CommandLineRunner {

    private final ZkClient zkClient;

    private final SyncDataService syncDataService;

    /**
     * Instantiates a new Zookeeper data init.
     *
     * @param zkClient        the zk client
     * @param syncDataService the sync data service
     */
    public ZookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
        this.zkClient = zkClient;
        this.syncDataService = syncDataService;
    }

    @Override
    public void run(final String... args) {
        String pluginPath = ZkPathConstants.PLUGIN_PARENT;
        String authPath = ZkPathConstants.APP_AUTH_PARENT;
        String metaDataPath = ZkPathConstants.META_DATA;
        if (!zkClient.exists(pluginPath) && !zkClient.exists(authPath) && !zkClient.exists(metaDataPath)) {
            syncDataService.syncAll(DataEventTypeEnum.REFRESH);
        }
    }
}

我们可以看到 ZookeeperDataInit 类实现接口 CommandLineRunner,CommandLineRunner 的作用是在 Spring Main方法执行 SpringApplication.run() 之前,执行它的实现类的run方法,适合做一些启动数据加载。我们又看到昨天的老朋友同步全部数据方法 syncDataService.syncAll(DataEventTypeEnum.REFRESH),只不过参数变成了DataEventTypeEnum.REFRESH,为什么是这个参数,答案其实都在 ZookeeperDataChangedListener 处理各种事件的方法里,根据事件类型做不同的处理,REFRESH 这个参数基本上就是全量更新或者删除再全量更新。

3、从客户端看 ZooKeeper 通信

通过依赖的soul-spring-boot-starter-sync-data-zookeeper 可以生成 SyncDataService 接口的实现类 ZookeeperSyncDataService bean,来看一下 ZookeeperSyncDataService 类的构造方法。

public ZookeeperSyncDataService(final ZkClient zkClient, final PluginDataSubscriber pluginDataSubscriber,
                                    final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
        this.zkClient = zkClient;
        this.pluginDataSubscriber = pluginDataSubscriber;
        this.metaDataSubscribers = metaDataSubscribers;
        this.authDataSubscribers = authDataSubscribers;
        watcherData();
        watchAppAuth();
        watchMetaData();
    }

    private void watcherData() {
        final String pluginParent = ZkPathConstants.PLUGIN_PARENT;
        List<String> pluginZKs = zkClientGetChildren(pluginParent);
        for (String pluginName : pluginZKs) {
            watcherAll(pluginName);
        }
        zkClient.subscribeChildChanges(pluginParent, (parentPath, currentChildren) -> {
            if (CollectionUtils.isNotEmpty(currentChildren)) {
                for (String pluginName : currentChildren) {
                    watcherAll(pluginName);
                }
            }
        });
    }

    private void watcherAll(final String pluginName) {
        watcherPlugin(pluginName);
        watcherSelector(pluginName);
        watcherRule(pluginName);
    }

    private void watcherPlugin(final String pluginName) {
        String pluginPath = ZkPathConstants.buildPluginPath(pluginName);
        if (!zkClient.exists(pluginPath)) {
            zkClient.createPersistent(pluginPath, true);
        }
        cachePluginData(zkClient.readData(pluginPath));
        subscribePluginDataChanges(pluginPath, pluginName);
    }

    private void watcherSelector(final String pluginName) {
        String selectorParentPath = ZkPathConstants.buildSelectorParentPath(pluginName);
        List<String> childrenList = zkClientGetChildren(selectorParentPath);
        if (CollectionUtils.isNotEmpty(childrenList)) {
            childrenList.forEach(children -> {
                String realPath = buildRealPath(selectorParentPath, children);
                cacheSelectorData(zkClient.readData(realPath));
                subscribeSelectorDataChanges(realPath);
            });
        }
        subscribeChildChanges(ConfigGroupEnum.SELECTOR, selectorParentPath, childrenList);
    }

    private void watcherRule(final String pluginName) {
        String ruleParent = ZkPathConstants.buildRuleParentPath(pluginName);
        List<String> childrenList = zkClientGetChildren(ruleParent);
        if (CollectionUtils.isNotEmpty(childrenList)) {
            childrenList.forEach(children -> {
                String realPath = buildRealPath(ruleParent, children);
                cacheRuleData(zkClient.readData(realPath));
                subscribeRuleDataChanges(realPath);
            });
        }
        subscribeChildChanges(ConfigGroupEnum.RULE, ruleParent, childrenList);
    }

    private void watchAppAuth() {
        final String appAuthParent = ZkPathConstants.APP_AUTH_PARENT;
        List<String> childrenList = zkClientGetChildren(appAuthParent);
        if (CollectionUtils.isNotEmpty(childrenList)) {
            childrenList.forEach(children -> {
                String realPath = buildRealPath(appAuthParent, children);
                cacheAuthData(zkClient.readData(realPath));
                subscribeAppAuthDataChanges(realPath);
            });
        }
        subscribeChildChanges(ConfigGroupEnum.APP_AUTH, appAuthParent, childrenList);
    }

    private void watchMetaData() {
        final String metaDataPath = ZkPathConstants.META_DATA;
        List<String> childrenList = zkClientGetChildren(metaDataPath);
        if (CollectionUtils.isNotEmpty(childrenList)) {
            childrenList.forEach(children -> {
                String realPath = buildRealPath(metaDataPath, children);
                cacheMetaData(zkClient.readData(realPath));
                subscribeMetaDataChanges(realPath);
            });
        }
        subscribeChildChanges(ConfigGroupEnum.APP_AUTH, metaDataPath, childrenList);
    }

代码贴的有点多,其实就是订阅 ZooKeeper 各种目录,获取各类数据的更新,也就是在网关启动的时候做好订阅 ZooKeeper 各类目录。

网关服务通过 ZooKeeper 获取数据的代码很少,只有 ZookeeperSyncDataService 一个类。

三、网关本地缓存什么样

昨天和今天对于网关服务获取数据,都只是讲到得到了数据,并没有接着往下讲得到数据后再怎么处理。代码结构不负责,只是有点多。最终都是到了各个订阅类的静态常量 Map 里。主要有三大类 Map 缓存,都已经在 ZookeeperSyncDataService 的属性里定义了。

  • PluginDataHandler 插件类数据,Soul使用的各类插件数据

  • MetaDataSubscriber 元数据类,接入需要元数据的RPC框架,例如Dubbo、SOFA、Tars

  • AuthDataSubscriber 权限类,其实我也没细看