soul网关-13-数据同步机制之zookeeper

soul网关的数据同步支持多种方式,如websocket、http长轮询、zookeeper、nacos等。本文就来学习一下soul网关是如何使用zookeeper进行数据同步的。

在分析soul网关源码之前,先来快速了解一下zookeeper相关知识。

zookeeper

zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

docker安装zookeeper

在前面的笔记里面我们已经介绍了如何安装docker for mac,如何用docker安装prometheus,现在就来用docker安装单机版的zookeeper。

  • 1.执行命令docker search zookeeper看一下有没有官方的zookeeper
$ docker search zookeeper
NAME                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
zookeeper                          Apache ZooKeeper is an open-source server wh…   990       [OK]       
jplock/zookeeper                   Builds a docker image for Zookeeper version …   166                  [OK]
wurstmeister/zookeeper                                                             135                  [OK]
……
……
  • 2.执行命令docker pull zookeeper下载zookeeper image
  • 3.docker images看一下下载的image
$ docker images
REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
zookeeper                latest    a90c625feb48   6 days ago    253MB
  • 4.执行命令docker run -d -p 2181:2181 --name myzookeeper --restart always a90c625feb48, 运行zookeeper

  • 5.看一下zookeeper是否已经在docker里面运行,执行docker ps -a

$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS                    PORTS                                                  NAMES
f2c330b15207   a90c625feb48             "/docker-entrypoint.…"   5 seconds ago   Up 4 seconds              2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, 8080/tcp   myzookeeper

  • 6.验证一下zookeeper已经成功运行了,执行docker exec -it f2c330b15207 bash进入。(其中f2c330b15207是上一步查出来的containerid)
$ docker exec -it f2c330b15207 bash
root@f2c330b15207:/apache-zookeeper-3.6.2-bin#
  • 7.输入./bin/zkCli.sh,连接zookeeper

  • 8.输入ls /,看一下输出的内容

[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]

zookeeper常用命令

上文已经成功在docker里面安装并启动了zookeeper,现在简单回顾一下zookeeper的常用命令

  • create /nemonode mydata,在/下面创建一个nemonode节点,并给这个节点设置值为mydata
  • ls -s /nemonode,看一下/nemonode这个节点的信息。
    • cZxid : 创建节点时的事务ID
    • ctime : 创建节点时的时间
    • mZxid : 最后修改节点时的事务ID
    • mtime : 最后修改节点时的时间
    • pZxid : 表示该节点的子节点列表最后一次修改的事务ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID(注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid)
    • cversion : 子节点版本号,子节点每次修改版本号加1
    • dataversion : 数据版本号,数据每次修改该版本号加1
    • aclversion : 权限版本号,权限每次修改该版本号加1
    • ephemeralOwner : 创建该临时节点的会话的sessionID。(如果该节点是持久节点,那么这个属性值为0)
    • dataLength : 该节点的数据长度
    • numChildren : 该节点拥有子节点的数量(只统计直接子节点的数量)
  • get /nemonode获取/nemonode这个节点的数据
  • set /nemonode mychangeddata表示将/nemonode这个节点的数据修改成mychangeddata
  • delete /nemonode表示删除这个节点。(注意,如果删除节点的时候该节点有子节点,那么使用delete命令删不成功,可以使用deleteall命令删除)

soul网关-13-数据同步机制之zookeeper_第1张图片

zkclient

zkclient是一个第三方的库,对zookeeper原生API进行了封装,更加简单易用。

下面列一下zkclient的一些api,先在这里熟悉一下,后面学习soul网关的zookeeper同步机制的时候就没什么障碍了。

每个都有很多API,此处只列举其中一个

  • 创建连接
    • public ZkClient(String zkServers, int sessionTimeout, int connectionTimeout)
  • 创建节点
    • public void createPersistent(String path, boolean createParents)
  • 删除节点
    • public boolean delete(final String path)
    • public boolean deleteRecursive(String path) 可以递归删除节点
  • 获取子节点列表
    • public List getChildren(String path)
  • 读取节点内容
    • public T readData(String path)
  • 更新节点数据
    • public void writeData(String path, Object object)
  • 判断节点是否存在
    • public boolean exists(String path)
  • 监听子节点变化
    • public List subscribeChildChanges(String path, IZkChildListener listener)
  • 监听节点数据变化
    • public void subscribeDataChanges(String path, IZkDataListener listener)

soul网关如何使用zookeeper进行数据同步

好了,上文已经对zookeeper做了基本的了解,下面来看soul网关使用zookeeper进行数据同步。

之前分析websocket同步机制的时候已经了解过,在soul-admin的DataSyncConfiguration这个声明了@Configuration注解的配置类里面,会注入数据同步相关的类。

配置文件里面存在配置soul.sync.zookeeper的话,就会向Spring容器里面注入这两个bean,分别是ZookeeperDataInitZookeeperDataChangedListener。如下图所示。

soul网关-13-数据同步机制之zookeeper_第2张图片

先来看下ZookeeperDataInit,它有个属性是ZkClient类型的。ZkClient来自一个第三方包com.101tec.zkclient,它对zookeeper的原生API进行了封装,更加简单易用。

public class ZookeeperDataInit implements CommandLineRunner {

    private final ZkClient zkClient;

    private final SyncDataService syncDataService;

    ……
    ……
    ……

ZookeeperDataInit实现了CommandLineRunner接口,CommandLineRunner接口会在所有的Spring beans都初始化之后,在SpringApplication.run()之前执行,去执行一段代码块逻辑,这段代码在整个应用生命周期内只会执行一次,使用CommandLineRunner
非常适合在应用程序启动之初进行一些数据初始化的工作。

我们来看下ZookeeperDataInit实现了CommandLineRunner接口的run方法里面具体做了什么。如果/soul/plugin/soul/auth/soul/metaData这三个节点都不存在,那么就调用syncDataService.syncAll方法。

    @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);
        }
    }

我们来看一下SyncDataService,这是一个接口,最终调用的是SyncDataServiceImpl里面的syncAll方法,在这个里面主要就是通过eventPublisher.publishEvent来发送DataChangedEvent事件。即 ConfigGroupEnum.APP_AUTHConfigGroupEnum.PLUGINConfigGroupEnum.SELECTOR, ConfigGroupEnum.RULE, ConfigGroupEnum.META_DATA四种类型数据的DataChangedEvent事件,即全量数据。

 	@Override
    public boolean syncAll(final DataEventTypeEnum type) {
        appAuthService.syncData();
        List pluginDataList = pluginService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, type, pluginDataList));
        List selectorDataList = selectorService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, type, selectorDataList));
        List ruleDataList = ruleService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, type, ruleDataList));
        metaDataService.syncData();
        return true;
    }

DataChangedEvent事件,我们在之前分析websocket同步机制的时候已经知道了。ApplicationEventPublisherApplicationListener是成对的,是spring框架中提供的用于发布事件和监听事件的东西。

DataChangeEventDispatcher数据变更事件分发器是我们之前在websocket同步机制那篇笔记里面提到的,它实现了ApplicationListener接口的onApplicationEvent方法,在onApplicationEvent方法里面,会根据具体的数据类型,调用DataChangedListener的相应的onXXXChanged方法。

再来看一下DataChangedListener接口的实现图,从图中看到ZookeeperDataChangeListener实现了它。

soul网关-13-数据同步机制之zookeeper_第3张图片

来看下ZookeeperDataChangeListener里面的onXXXChanged方法,以onAppAuthChanged、onPluginChanged、onSelectorChanged为例。

  • onAppAuthChanged:
    • 如果是删除事件,则删除zookeeper上的相应节点/soul/auth/xxx
    • 如果是其他事件,则(创建)更新相应节点数据
  • onPluginChanged:
    • 如果是删除事件,则删除zookeeper上的相应plugin节点/soul/plugin/xxx以及selector节点/soul/selector/xxx以及rule节点/soul/rule/xxx
    • 如果是其他事件,则(创建)更新相应节点数据
  • onSelectorChanged:
    • 如果是刷新事件,则先删除节点/soul/selector/xxx,再创建节点/soul/selector/xxx,再创建并更新节点/soul/selector/xxx/xx数据
    • 如果是删除事件,则删除节点/soul/selector/xxx
	@Override
    public void onAppAuthChanged(final List changed, final DataEventTypeEnum eventType) {
        for (AppAuthData data : changed) {
            final String appAuthPath = ZkPathConstants.buildAppAuthPath(data.getAppKey());
            // delete
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(appAuthPath);
                continue;
            }
            // create or update
            upsertZkNode(appAuthPath, data);
        }
    }

    @Override
    public void onPluginChanged(final List changed, final DataEventTypeEnum eventType) {
        for (PluginData data : changed) {
            final String pluginPath = ZkPathConstants.buildPluginPath(data.getName());
            // delete
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPathRecursive(pluginPath);
                final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(data.getName());
                deleteZkPathRecursive(selectorParentPath);
                final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getName());
                deleteZkPathRecursive(ruleParentPath);
                continue;
            }
            //create or update
            upsertZkNode(pluginPath, data);
        }
    }

    @Override
    public void onSelectorChanged(final List changed, final DataEventTypeEnum eventType) {
        if (eventType == DataEventTypeEnum.REFRESH) {
            final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(changed.get(0).getPluginName());
            deleteZkPathRecursive(selectorParentPath);
        }
        for (SelectorData data : changed) {
            final String selectorRealPath = ZkPathConstants.buildSelectorRealPath(data.getPluginName(), data.getId());
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(selectorRealPath);
                continue;
            }
            final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(data.getPluginName());
            createZkNode(selectorParentPath);
            //create or update
            upsertZkNode(selectorRealPath, data);
        }
    }

如果一开始zookeeper里面什么都没有,当这个时候只启动了soul-admin的时候,zookeepr里面多了一个/soul节点,/soul节点里面的东西如下:

在这里插入图片描述
soul网关-13-数据同步机制之zookeeper_第4张图片

如果在这个时候,启动了soul-example-http示例项目,那么/soul节点里面的东西如下:

soul网关-13-数据同步机制之zookeeper_第5张图片
可以从上图看到,/soul下面比之前多了/soul/selector/soul/rule节点。由于只有divide插件配置了selector和rule,所以/soul/selector下面多了/soul/selector/divide节点,/soul/rule下面多了/soul/rule/divide节点。/soul/selector/divide下面多了/soul/selector/divide/具体selectorid节点,该节点里面存的就是具体的selector对象。同理,/soul/rule/divide节点下面多了由selectorid-ruleid组成的不同节点,里面存的是具体的rule对象。


看完了soul-admin这半边如何将数据更新到zookeeper里面,在来看下soul-bootstrap这半边是如何获取到最新的数据的。根据第一篇笔记的分析知道源码里面soul-sync-data-center模块是负责数据同步相关的,那我们直接去找soul-sync-data-center/soul-sync-data-zookeeper里面的关键类ZookeeperSyncDataService

ZookeeperSyncDataService的构造函数里面,有三个主要的方法,我们来看下watcherData()watchAppAuth()watchMetaData()方法里面都干了什么

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

在这几个watch方法里面,主要就是调用zkClient.readData读取节点数据放入缓存,调用zkClient.subscribeChildChanges监听节点的子节点变化,调用zkClient.subscribeDataChanges监听节点的数据变化

	private void watcherData() {
        final String pluginParent = ZkPathConstants.PLUGIN_PARENT;
        List 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 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 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);
    }
……
……
……
……

总结一下,soul网关利用zookeeper进行数据同步实际上是利用了zookeeper的watch机制,可以监听节点变化。当soul-admin进行数据更改的时候,soul-bootstrap就可以监听到zookeeper里面的变化。

你可能感兴趣的:(Java,网关)