soul admin 和 soul bootstrap 同步数据的时机
从昨天的文章中可以看出 soul admin 和 soul bootstrap 之间数据同步的触发的类型主要有两类行为:
- soul admin 中对 soul bootstrap 运行时数据 (app_auth, plugin, selector, rule 等) 进行了更改,由 soul admin 侧的事件机制触发数据的同步
- soul bootstrap 主动触发数据同步,如:WebSocket 客户端和服务端完成了握手行为 (WebSocket 协议应用层的属于) 之后由 soul bootstrap 端主动触发数据的同步
从源码分析 soul 网关如何依赖 zookeeper 完成数据同步
同步机制
soul admin 利用 zookeeper 同步数据的原理是使用了 zookeeper 的 watcher 机制, zookeeper 的 watcher 流程如下:
- 客户端向服务端的某个节点路径上注册一个 watcher,客户端同时会在本地 watcher manager 中存储特定的watcher
- 当发生节点数据或者节点子节点变化时,服务端会通知客户端节点变化信息,客户端收到通知后,会调用 watcher 回调函数
在 soul 网关中, soul admin 中集成了 zookeeper 客户端, 在 soul admin 中的数据类型变更数据处理流程中和 zookeeper server 交互; soul bootstrap 模块可配置启用通过 zookeeper 同步数据, 通过 zookeeper 同步数据的功能被集成在了模块
soul-spring-boot-starter-sync-data-zookeeper 中, 这个模块中有注册 watcher 到 zookeeper 节点路径上的逻辑; 通过上述的流程完成数据的同步。
soul admin 侧的流程解析
soul admin 工程中,在 Spring 事件总线机制中完成 soul 网关数据同步事件监听的类是: org.dromara.soul.admin.listener.DataChangedListener, zookeeper
完成数据同步的实现类为: org.dromara.soul.admin.listener.zookeeper.ZookeeperDataChangedListener:
public class ZookeeperDataChangedListener implements DataChangedListener {
private final ZkClient zkClient;
public ZookeeperDataChangedListener(final ZkClient zkClient) { this.zkClient = zkClient; }
@Override public void onAppAuthChanged(final List<AppAuthData> changed, final DataEventTypeEnum eventType) { for (AppAuthData data : changed) { final String appAuthPath = ZkPathConstants.buildAppAuthPath(data.getAppKey()); if (eventType == DataEventTypeEnum.DELETE) { deleteZkPath(appAuthPath); continue; } upsertZkNode(appAuthPath, data); } }
@SneakyThrows @Override public void onMetaDataChanged(final List<MetaData> changed, final DataEventTypeEnum eventType) { for (MetaData data : changed) { final String metaDataPath = ZkPathConstants.buildMetaDataPath(URLEncoder.encode(data.getPath(), "UTF-8")); if (eventType == DataEventTypeEnum.DELETE) { deleteZkPath(metaDataPath); continue; } upsertZkNode(metaDataPath, data); } }
@Override public void onPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) { for (PluginData data : changed) { final String pluginPath = ZkPathConstants.buildPluginPath(data.getName()); 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; } upsertZkNode(pluginPath, data); } }
@Override public void onSelectorChanged(final List<SelectorData> 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); upsertZkNode(selectorRealPath, data); } }
@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); upsertZkNode(ruleRealPath, data); } }
private void createZkNode(final String path) { if (!zkClient.exists(path)) { zkClient.createPersistent(path, true); } }
private void upsertZkNode(final String path, final Object data) { if (!zkClient.exists(path)) { zkClient.createPersistent(path, true); } zkClient.writeData(path, data); }
private void deleteZkPath(final String path) { if (zkClient.exists(path)) { zkClient.delete(path); } }
private void deleteZkPathRecursive(final String path) { if (zkClient.exists(path)) { zkClient.deleteRecursive(path); } } }
|
在 zookeeper 中 plugin 节点, selector 节点, rule 节点的路径有一定的关联,关联关系如下:
| 路径类型 |
路径规则 |
| AppAuthData |
/soul/auth/{appKey} |
| MetaData |
/soul/meta/{path} |
| PluginData |
/soul/plugin/{pluginName} |
| SelectorData |
/soul/selector/{pluginName}/{selectorId} |
| RuleData |
/soul/rule/{pluginName}/{selectorId}-{ruleId} |
zookeeper 路径的构造方法在: org.dromara.soul.common.constant.ZkPathConstants 中。
所以在 ZookeeperDataChangedListener 类的 onPluginChanged,
onSelectorChanged, onRuleChanged 方法会存在对 zookeeper 路径的复合操作。
soul admin 利用 zookeeper 同步数据到 soul bootstrap 侧依赖的是 soul admin 中监听数据变化事件的处理中的实现类 ZookeeperDataChangedListener
对 zookeeper 中持久节点的数据节点操作完成不同类型数据的操作来完成不用的 DataEventType 的操作。
soul bootstrap 侧的流程剖析
soul bootstrap 集成模块 soul-spring-boot-starter-sync-data-zookeeper 并且在 Spring 容器启动配置中加入相应的配置来开启 zookeeper 配置。
配置 zookeeper 数据同步的关键 Configuration
@Configuration @ConditionalOnClass(ZookeeperSyncDataService.class) @ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url") @EnableConfigurationProperties(ZookeeperConfig.class) @Slf4j 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()); }
}
|
- 配置会被 Spring 容器执行的条件是: classpath 中存在
ZookeeperSyncDataService 类并且 PropertySource 中存在配置 soul.sync.zookeeper.url
ZookeeperSyncDataConfiguration 配置类中注册的 SyncDataService 为通过 zookeeper 实现 watcher 机制的关键类, ZookeeperSyncDataConfiguration 的构造函数中完执行了:
- 从 zookeeper 读取插件信息, 选择器信息, 规则信息。
- 注册 watcher 到插件节点路径, 选择器节点路径, 规则节点路径; 当发生节点数据或者节点子节点变化时,服务端会通知客户端节点变化信息,完成数据的同步。
总结
soul 利用了 zookeeper 路径节点的 watcher 机制来实现 soul admin 到 soul bootstrap 的数据同步, 结合昨天分析的通过 WebSocket 同步数据, 可以看到一些设计模式的灵活运用, 比如: 观察者模式, 策略模式等, 值得细细学习。