ShenYu网关源码阅读(十五)Zookeeper数据同步-Bootstrap端

547 阅读7分钟

Soul网关源码阅读(十五)Zookeeper数据同步-Bootstrap端


简介

    本篇文章对Soul网关Bootstrap的Zookeeper数据同步进行探索

示例启动

环境配置

    使用docker启动mysql和zk

docker run -dit --name zk -p 2181:2181 zookeepe
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest

Soul-Admin配置

    启动时候,我们先把Soul-admin和Soul-Bootstrap的Websocket同步给关了,开启HTTP同步,配置大致如下:

    Soul-Admin的HTTP同步方式配置,暂时把其他同步方式给注释关闭

  sync:
      zookeeper:
          url: localhost:2181
          sessionTimeout: 5000
          connectionTimeout: 2000

Soul-Bootstrap配置

    Soul-Bootstrap的Http同步方式配置,暂时把其他同步方式给注释关闭

soul :
    file:
      enabled: true
    corss:
      enabled: true
    dubbo :
      parameter: multi
    sync:
        zookeeper:
             url: localhost:2181
             sessionTimeout: 5000
             connectionTimeout: 2000

启动失败

    启动Soul-admin、Soul-Bootstrap、Soul-Example-HTTP

    http://localhost:9195/dubbo/findById?id=1 ,访问链接失败

    进行debug的时候发现没有数据,只用变更的时候会发数据过来,重启了试试不行

    感觉可能是bug,但是首先排除下自己的代码版本问题和自己的使用问题,问了问群里的老哥,发现是自己的使用问题

启动修复

    解决的办法是拉一次自己的代码,保证是最新的,然后删除zookeeper里面的soul数据,重启Admin和Bootstrap,重新访问,成功

    下面是zk图形化客户端,用起来比价方便,win也可以使用(win下第一次启动可能显示不全,关闭再启动即可),下面是搜索到的安装教程和下载链接:

源码查看

    我们直接定位到Soul网关源码阅读(十二)数据同步初探中的zk同步的入口类: ZookeeperSyncDataService

    稍微用过zk的,就可以看到熟悉的watch,监听数据的变化,从下面的代码大致可以看出监听我前面分析的五种数据:插件数据、选择器数据、规则数据、AppAuth数据、Metadata数据

public class ZookeeperSyncDataService implements SyncDataService, AutoCloseable {

    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 watcherAll(final String pluginName) {
        watcherPlugin(pluginName);
        watcherSelector(pluginName);
        watcherRule(pluginName);
    }
}

    我们在构造函数上打上断点,看看调用栈,发现是和HTTP同步差不多的,如下代码所示,可以看到使用Spring方式启动

public class ZookeeperSyncDataConfiguration {
   
    @Bean
    public SyncDataService syncDataService(final ObjectProvider<ZkClient> zkClient, final ObjectProvider<PluginDataSubscriber> pluginSubscriber,
                                           final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) {
        log.info("you use zookeeper sync soul data.......");
        return new ZookeeperSyncDataService(zkClient.getIfAvailable(), pluginSubscriber.getIfAvailable(),
                metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
    }

    @Bean
    public ZkClient zkClient(final ZookeeperConfig zookeeperConfig) {
        return new ZkClient(zookeeperConfig.getUrl(), zookeeperConfig.getSessionTimeout(), zookeeperConfig.getConnectionTimeout());
    }

}

    简单的调试和看了看,发现zk的同步方式和websocket一样还是比较简单的,结构也是非常的清晰,这里就不做过多的介绍了,就说明下其核心逻辑,代码如下:

    构造函数中启动多所有数据的监听,并进行五种树节点初始化

    需要注意理解的是对列表数据的监听(监听增删之类),监听列表中单个数据的变化,代码中的监听基本都是这种套路,注意理解即可

public class ZookeeperSyncDataService implements SyncDataService, AutoCloseable {

    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的会有点疑惑,这里就简单介绍下自己的理解:

    上面分析中提到的监听其核心是:发布-订阅

    zk客户端连接上zk服务端,当服务端的数据发送变化的时候,服务端会发送变化的数据给客户端,客户端收到变化的数据后进行操作

    这些变化的数据可以是目录(上面提到的列表数据),也可以是具体单个文件(列表中具体数据)

    理解了这个,大致就能理解zk同步的核心了

    总结下zookeeper的同步流程大致如下:

  • 1.读取初始数据进行初始化
  • 2.对读取的初始化数据进行监听
  • 3.读取变化的数据进行本地缓存更新,并对其进行监听

Soul网关源码分析文章列表

Github

掘金