1. 背景介绍
1.1 为什么会有数据同步?
原因:由于网关是流量请求的入口,在微服务架构中承担了非常重要的角色,网关高可用的重要性不言而喻。在使用网关的过程中,为了满足业务诉求,经常需要变更配置,比如流控规则、路由规则等等。因此,网关动态配置是保障网关高可用的重要因素。那么,Soul 网关又是如何支持动态配置的呢?soul作为网关,为了提供更高的响应速度,所有的配置都缓存在JVM的Hashmap中,每次请求都走的本地缓存,速度非常快,目前soul支持websocket、zookeeper、http、nacos四种数据同步方式。【摘自】soul官网介绍
2. 数据同步原理图
2.1 数据同步流程
下图展示了 Soul 数据同步的流程,Soul 网关在启动时,会从从配置服务同步配置数据,并且支持推拉模式获取配置变更信息,并且更新本地缓存。而管理员在管理后台,变更用户、规则、插件、流量配置,通过推拉模式将变更信息同步给 Soul 网关,具体是 push 模式,还是 pull 模式取决于配置。关于配置同步模块,其实是一个简版的配置中心。【摘自】soul官网介绍
2.2 数据同步流程细化图
如下图所示,soul-admin 在用户发生配置变更之后,会通过 ApplicationEventPublisher 发出配置变更通知,由 DataChangedEventDispatcher 处理该变更通知,然后根据配置的同步策略(http、weboscket、zookeeper),将配置发送给对应的事件处理器,如果是 websocket 同步策略,则将变更后的数据主动推送给 soul-web,并且在网关层,会有对应的 WebsocketCacheHandler 处理器处理来处 admin 的数据推送。【摘自】soul官网介绍
3. 示例运行
3.1 启动数据库
mysqld --console
3.2 Soul-Admin加上数据同步配置
soul:
database:
dialect: mysql
init_script: "META-INF/schema.sql"
#打开websocket数据同步
sync:
websocket:
enabled: true
3.3 运行Soul-Admin
运行SoulAdminBootstrap
3.4 Soul-Bootstrap加上websocket同步方式配置
soul :
file:
enabled: true
corss:
enabled: true
dubbo :
parameter: multi
#打开websocket数据同步
sync:
websocket :
urls: ws://localhost:9095/websocket
3.5 运行Soul-Bootstrap
运行SoulBootstrapApplication
3.6 运行Soul-Example-http
运行Soul-Example-http工程,在启动时往soul-admin注册选择器规则数据,为后续源码分析铺底数据。
4. 源码分析
4.1 回顾请求转发处理核心类
在之前分析http、dubbo、springcloud等不同渠道接入soul后,请求转发都会经过AbstractSoulPlugin类进行选择器的选择以及选择器规则的匹配,主要代码如下:
public abstract class AbstractSoulPlugin implements SoulPlugin {
......
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
String pluginName = named();
// 获取插件配置
final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
if (pluginData != null && pluginData.getEnabled()) {
// 获取选择器
final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
.......
// 获取规则
final SelectorData selectorData = matchSelector(exchange, selectors);
.......
}
return chain.execute(exchange);
}
......
}
4.2 找出缓存数据操作核心类
从上述代码可以看出,Soul-Bootstrap在处理请求转发时,有三部分数据是从缓存中获取的:插件配置数据、选择器数据、规则数据,这些数据都是从 BaseDataCache中获取的,下面看看其核心代码:
public final class BaseDataCache {
private static final BaseDataCache INSTANCE = new BaseDataCache();
/**
* pluginName -> PluginData.
*/
private static final ConcurrentMap<String, PluginData> PLUGIN_MAP = Maps.newConcurrentMap();
/**
* pluginName -> SelectorData.
*/
private static final ConcurrentMap<String, List<SelectorData>> SELECTOR_MAP = Maps.newConcurrentMap();
/**
* selectorId -> RuleData.
*/
private static final ConcurrentMap<String, List<RuleData>> RULE_MAP = Maps.newConcurrentMap();
public void cachePluginData(final PluginData pluginData) {
Optional.ofNullable(pluginData).ifPresent(data -> PLUGIN_MAP.put(data.getName(), data));
}
public void removePluginData(final PluginData pluginData) {
Optional.ofNullable(pluginData).ifPresent(data -> PLUGIN_MAP.remove(data.getName()));
}
public PluginData obtainPluginData(final String pluginName) {
return PLUGIN_MAP.get(pluginName);
}
public void cacheSelectData(final SelectorData selectorData) {
Optional.ofNullable(selectorData).ifPresent(this::selectorAccept);
}
public void removeSelectData(final SelectorData selectorData) {
Optional.ofNullable(selectorData).ifPresent(data -> {
final List<SelectorData> selectorDataList = SELECTOR_MAP.get(data.getPluginName());
Optional.ofNullable(selectorDataList).ifPresent(list -> list.removeIf(e -> e.getId().equals(data.getId())));
});
}
public List<SelectorData> obtainSelectorData(final String pluginName) {
return SELECTOR_MAP.get(pluginName);
}
public void cacheRuleData(final RuleData ruleData) {
Optional.ofNullable(ruleData).ifPresent(this::ruleAccept);
}
public void removeRuleData(final RuleData ruleData) {
Optional.ofNullable(ruleData).ifPresent(data -> {
final List<RuleData> ruleDataList = RULE_MAP.get(data.getSelectorId());
Optional.ofNullable(ruleDataList).ifPresent(list -> list.removeIf(rule -> rule.getId().equals(data.getId())));
});
}
public List<RuleData> obtainRuleData(final String selectorId) {
return RULE_MAP.get(selectorId);
}
}
4.3 进一步找出数据同步入口程序
从BaseDataCache的代码中可以得知以下信息:
- 方法的命名可以大致看出基本都是对插件配置数据、选择器配置数据、规则配置数据进行的增删改查操作;
- cacheSelectData和removeRuleData应该就是对应的更新和删除操作
下面来看下,cacheSelectData和removeRuleData方法到底在哪些地方被调用,使用IDEA快捷键(Alt+F7)查看用哪些地方进行了使用,对结果信息筛选后,排除了BaseDataCache类本身的调用和一些Junit测试类的调用,只有CommonPluginDataSubscriber这个类进行了调用,如下图所示:
这个类应该就是数据同步的核心类了,下面看看这个类的核心代码,如下:
public class CommonPluginDataSubscriber implements PluginDataSubscriber {
.......
@Override
public void onSubscribe(final PluginData pluginData) {
subscribeDataHandler(pluginData, DataEventTypeEnum.UPDATE);
}
@Override
public void unSubscribe(final PluginData pluginData) {
subscribeDataHandler(pluginData, DataEventTypeEnum.DELETE);
}
public void onSelectorSubscribe(final SelectorData selectorData) {
subscribeDataHandler(selectorData, DataEventTypeEnum.UPDATE);
}
@Override
public void unSelectorSubscribe(final SelectorData selectorData) {
subscribeDataHandler(selectorData, DataEventTypeEnum.DELETE);
}
@Override
public void onRuleSubscribe(final RuleData ruleData) {
subscribeDataHandler(ruleData, DataEventTypeEnum.UPDATE);
}
@Override
public void unRuleSubscribe(final RuleData ruleData) {
subscribeDataHandler(ruleData, DataEventTypeEnum.DELETE);
}
private <T> void subscribeDataHandler(final T classData, final DataEventTypeEnum dataType) {
Optional.ofNullable(classData).ifPresent(data -> {
if (data instanceof PluginData) {
PluginData pluginData = (PluginData) data;
if (dataType == DataEventTypeEnum.UPDATE) {
// 插件配置数据的更新
BaseDataCache.getInstance().cachePluginData(pluginData);
Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.handlerPlugin(pluginData));
} else if (dataType == DataEventTypeEnum.DELETE) {
// 插件配置数据的删除
BaseDataCache.getInstance().removePluginData(pluginData);
Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.removePlugin(pluginData));
}
} else if (data instanceof SelectorData) {
SelectorData selectorData = (SelectorData) data;
if (dataType == DataEventTypeEnum.UPDATE) {
// 选择器数据的更新
BaseDataCache.getInstance().cacheSelectData(selectorData);
Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.handlerSelector(selectorData));
} else if (dataType == DataEventTypeEnum.DELETE) {
// 选择器数据的删除
BaseDataCache.getInstance().removeSelectData(selectorData);
Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.removeSelector(selectorData));
}
} else if (data instanceof RuleData) {
RuleData ruleData = (RuleData) data;
if (dataType == DataEventTypeEnum.UPDATE) {
// 规则数据的更新
BaseDataCache.getInstance().cacheRuleData(ruleData);
Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.handlerRule(ruleData));
} else if (dataType == DataEventTypeEnum.DELETE) {
// 规则数据的删除
BaseDataCache.getInstance().removeRuleData(ruleData);
Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.removeRule(ruleData));
}
}
});
}
}
可以看到,核心处理方法subscribeDataHandler被自身的onSubscribe、onSelectorSubscribe、onRuleSubscribe等方法调用,所以接着再通过alt+F7看看这些方法是被谁调用的,以onSubscribe为例查看调用关系,如下图:
从图中可以看到,基本上由以下四个模块调用:
- soul-sync-data-http : PluginDataRefresh
- soul-sync-data-nacos : NacosCacheHandler
- soul-sync-data-websocket : PluginDataHandler
- soul-sync-data-zookeeper : ZookeeperSyncDataService
到这里,可以知道应该重点关注soul-sync-data-websocket模块的PluginDataHandler这个类是如何处理的,包括doRefresh、doUpdate和doDelete方法。
5. 总结
从以上的分析可以得知,从源码的角度分析websocket数据同步,需要深入了解一下几个方面:
- BaseDataCache : 配置缓存,提供更新、删除、查询;
- CommonPluginDataSubscriber : 对BaseDataCache的封装,提供更新和删除;
- soul-sync-data-websocket模块PluginDataHandler类是如何对;CommonPluginDataSubscriber这个类进行调用从而实现数据同步的。