soul网关的数据同步支持多种方式,如websocket、http长轮询、zookeeper、nacos等。本文就来学习一下soul网关是如何使用zookeeper进行数据同步的。
在分析soul网关源码之前,先来快速了解一下zookeeper相关知识。
zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
在前面的笔记里面我们已经介绍了如何安装docker for mac,如何用docker安装prometheus,现在就来用docker安装单机版的zookeeper。
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]
……
……
docker pull zookeeper
下载zookeeper imagedocker 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
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]
上文已经成功在docker里面安装并启动了zookeeper,现在简单回顾一下zookeeper的常用命令
create /nemonode mydata
,在/
下面创建一个nemonode
节点,并给这个节点设置值为mydata
ls -s /nemonode
,看一下/nemonode
这个节点的信息。
get /nemonode
获取/nemonode
这个节点的数据set /nemonode mychangeddata
表示将/nemonode
这个节点的数据修改成mychangeddata
delete /nemonode
表示删除这个节点。(注意,如果删除节点的时候该节点有子节点,那么使用delete命令删不成功,可以使用deleteall命令删除)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)
好了,上文已经对zookeeper做了基本的了解,下面来看soul网关使用zookeeper进行数据同步。
之前分析websocket同步机制的时候已经了解过,在soul-admin的DataSyncConfiguration
这个声明了@Configuration
注解的配置类里面,会注入数据同步相关的类。
配置文件里面存在配置soul.sync.zookeeper
的话,就会向Spring容器里面注入这两个bean,分别是ZookeeperDataInit
和ZookeeperDataChangedListener
。如下图所示。
先来看下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_AUTH
,ConfigGroupEnum.PLUGIN
,ConfigGroupEnum.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同步机制的时候已经知道了。ApplicationEventPublisher
和ApplicationListener
是成对的,是spring框架中提供的用于发布事件和监听事件的东西。
DataChangeEventDispatcher
数据变更事件分发器是我们之前在websocket同步机制那篇笔记里面提到的,它实现了ApplicationListener
接口的onApplicationEvent
方法,在onApplicationEvent
方法里面,会根据具体的数据类型,调用DataChangedListener
的相应的onXXXChanged
方法。
再来看一下DataChangedListener
接口的实现图,从图中看到ZookeeperDataChangeListener
实现了它。
来看下ZookeeperDataChangeListener
里面的onXXXChanged
方法,以onAppAuthChanged、onPluginChanged、onSelectorChanged为例。
/soul/auth/xxx
/soul/plugin/xxx
以及selector节点/soul/selector/xxx
以及rule节点/soul/rule/xxx
/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-example-http
示例项目,那么/soul
节点里面的东西如下:
可以从上图看到,/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里面的变化。