Soul网关数据同步
-
为什么需要数据同步?
Soul作为独立的网关系统,有自己的管理后台soul-admin,通过页面操作就可以控制插件数据,选择器,规则数据,元数据,签名数据等等,并且所有插件的选择器,规则都是动态配置,立即生效,不需要重启服务。这样便捷的操作当然需要一套完善的数据同步机制。又考虑到性能问题,不可能每次更改了设置都要通过数据库来获取,所以soul设计了一套数据同步机制,保证了数据始终在JVM的内存中,且是最新的数据,这样也就大大提高了性能。 -
Soul的数据同步原理是什么?
详细的介绍可以参考官方社区的文档【Soul数据同步原理】
我这里概括以下就是用户通过后台页面修改了设置,Soul就会分别将数据同步到数据库,和同步给我们的网关系统。然后我们的网关系统就会更新本地的缓存,当又相应的流量过来的时候,不需要去我们的数据库再去拿数据。对应的就是下面这张图:

-
Soul支持哪些数据同步方式?
到目前为止,Soul支持以下四种数据同步方式:- websocket同步
- zookeeper同步
- http长轮询同步
- nacos同步
今天 我们主要看的是websocket同步方式。
websocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
以上摘抄自百度百科
代码跟踪
基本的概念了解清楚后,接下来就打开源码来学习吧。
因为Soul默认使用的就是websocket方式进行数据同步,所以这里就不再赘述了,不清楚的同学可以参考官方文档【Soul数据同步策略】。
-
启动
soul-admin -
启动网关
soul-bootstrap
启动成功后我们分别再soul-admin的后台和soul-bootstrap的后台分别看到如下log信息。这就说明我们的后台管理系统和网关系统使用websocket建立了连接。2021-01-20 19:57:59.428 INFO 19448 --- [0.0-9095-exec-1] o.d.s.a.l.websocket.WebsocketCollector : websocket on open successful....2021-01-20 19:57:59.411 INFO 19576 --- [ocket-connect-1] o.d.s.p.s.d.w.WebsocketSyncDataService : websocket reconnect is successful..... -
先从这两个log入手,看一下源码,先看
soul-admin的。这里使用的是websocket的OnOpen监听方法,当有新的连接的时候,会把该连接的session放入到一个Set中。private static final Set<Session> SESSION_SET = new CopyOnWriteArraySet<>(); /** * On open. * * @param session the session */ @OnOpen public void onOpen(final Session session) { log.info("websocket on open successful...."); SESSION_SET.add(session); }这里主要处理的是建立连接的时候处理的一些逻辑。
- 如果配置了多个url,通过“,”分割(这个再配置的时候有说明)。
- 创建WebSocketClient,并添加到List中去
- 针对于List中的Client创建连接,并且创建定时任务监视是否处于连接中,如果断开了连接,则尝试重新连接(如果在executor.scheduleAtFixedRate(() -> {})里面打断点,会发现每隔30面进来一次)。
/** * Instantiates a new Websocket sync cache. * * @param websocketConfig the websocket config * @param pluginDataSubscriber the plugin data subscriber * @param metaDataSubscribers the meta data subscribers * @param authDataSubscribers the auth data subscribers */ public WebsocketSyncDataService(final WebsocketConfig websocketConfig, final PluginDataSubscriber pluginDataSubscriber, final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) { String[] urls = StringUtils.split(websocketConfig.getUrls(), ","); executor = new ScheduledThreadPoolExecutor(urls.length, SoulThreadFactory.create("websocket-connect", true)); for (String url : urls) { try { clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers)); } catch (URISyntaxException e) { log.error("websocket url({}) is error", url, e); } } try { for (WebSocketClient client : clients) { boolean success = client.connectBlocking(3000, TimeUnit.MILLISECONDS); if (success) { log.info("websocket connection is successful....."); } else { log.error("websocket connection is error....."); } executor.scheduleAtFixedRate(() -> { try { if (client.isClosed()) { boolean reconnectSuccess = client.reconnectBlocking(); if (reconnectSuccess) { log.info("websocket reconnect is successful....."); } else { log.error("websocket reconnection is error....."); } } } catch (InterruptedException e) { log.error("websocket connect is error :{}", e.getMessage()); } }, 10, 30, TimeUnit.SECONDS); } /* client.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxyaddress", 80)));*/ } catch (InterruptedException e) { log.info("websocket connection...exception....", e); } } -
通过这两个log只是看到了创建WebSocket连接的过程,没有发现数据同步的地方。别着急,我们会发现
WebsocketSyncDataService的所在包里有个handler的包,里面有WebsocketDataHandler,按照猫大人的假设想猜想,应该是在这里进行数据同步的。在里面的executor方法出打个断点,然后去后台点一下数据同步按钮,果不其然就进来了。

-
然后就可以跟踪调试了,这个类比较简单,我们每次进来会执行
executor方法,这个方法里面胡根据我们的type进行不同的处理,根据代码就可以看出有五种不同的处理,分别是PluginDataHandler,SelectorDataHandler,RuleDataHandler,AuthDataHandler,MetaDataHandler。private static final EnumMap<ConfigGroupEnum, DataHandler> ENUM_MAP = new EnumMap<>(ConfigGroupEnum.class); /** * Instantiates a new Websocket data handler. * * @param pluginDataSubscriber the plugin data subscriber * @param metaDataSubscribers the meta data subscribers * @param authDataSubscribers the auth data subscribers */ public WebsocketDataHandler(final PluginDataSubscriber pluginDataSubscriber, final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) { ENUM_MAP.put(ConfigGroupEnum.PLUGIN, new PluginDataHandler(pluginDataSubscriber)); ENUM_MAP.put(ConfigGroupEnum.SELECTOR, new SelectorDataHandler(pluginDataSubscriber)); ENUM_MAP.put(ConfigGroupEnum.RULE, new RuleDataHandler(pluginDataSubscriber)); ENUM_MAP.put(ConfigGroupEnum.APP_AUTH, new AuthDataHandler(authDataSubscribers)); ENUM_MAP.put(ConfigGroupEnum.META_DATA, new MetaDataHandler(metaDataSubscribers)); } /** * Executor. * * @param type the type * @param json the json * @param eventType the event type */ public void executor(final ConfigGroupEnum type, final String json, final String eventType) { ENUM_MAP.get(type).handle(json, eventType); }然后根据eventType判断调用哪个方法。这里可以看到目前有三种方法
doRefresh,doUpdate,doDelete@Override public void handle(final String json, final String eventType) { List<T> dataList = convert(json); if (CollectionUtils.isNotEmpty(dataList)) { DataEventTypeEnum eventTypeEnum = DataEventTypeEnum.acquireByName(eventType); switch (eventTypeEnum) { case REFRESH: case MYSELF: doRefresh(dataList); break; case UPDATE: case CREATE: doUpdate(dataList); break; case DELETE: doDelete(dataList); break; default: break; } } } -
因为我们是在
divide插件页面
执行的操作,可以看到这里的type分别是PLUGIN,SELECTOR,RULE,也就是分别调用了PluginDataHandler.doUpdate(),SelectorDataHandler.doRefresh(),RuleDataHandler.doRefresh() -
接下来再看一下这三个方法是怎么处理的,因为每个DataHandler的处理逻辑都一样,所以这里就拿
SelectorDataHandler这一个为例子看一下。private final PluginDataSubscriber pluginDataSubscriber; @Override public List<SelectorData> convert(final String json) { return GsonUtils.getInstance().fromList(json, SelectorData.class); } @Override protected void doRefresh(final List<SelectorData> dataList) { pluginDataSubscriber.refreshSelectorDataSelf(dataList); dataList.forEach(pluginDataSubscriber::onSelectorSubscribe); } @Override protected void doUpdate(final List<SelectorData> dataList) { dataList.forEach(pluginDataSubscriber::onSelectorSubscribe); } @Override protected void doDelete(final List<SelectorData> dataList) { dataList.forEach(pluginDataSubscriber::unSelectorSubscribe); } -
接着跟着调试走,就会来到了我们通用的一个数据同步处理的方法中。可以看到Soul通过
UPDATE和DELETE两种操作类型完成了数据的更新。并且可以看出我们的数据都是放在了BaseDataCache这样一个容器中,该容器针对于不同的插件类型和不同的操作类型,提供了如下几个方法用来做数据操作。cachePluginData//更新指定插件的数据缓存removePluginData//删除指定插件的数据缓存cacheSelectData//更新指定选择器的数据缓存removeSelectData// 删除指定选择器的数据缓存cacheRuleData// 更新指定规则的数据缓存removeRuleData// 删除指定规则的数据缓存
@Override public void onSelectorSubscribe(final SelectorData selectorData) { subscribeDataHandler(selectorData, DataEventTypeEnum.UPDATE); } @Override public void unSelectorSubscribe(final SelectorData selectorData) { subscribeDataHandler(selectorData, 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)); } } }); } -
知道了数据存在哪,接下来就看一下我们的容器是怎么存数据的。
-
我们的容器定义了四个属性,
INSTANCE:作为全局的一个整体数据容器使用,PLUGIN_MAP,SELECTOR_MAP,RULE_MAP:使用ConcurrentMap分别存储插件数据,选择器数据和规则数据。那么它的update和delete就很简单了,对于的就是Map的put和remove方法。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();
小结
OK,到此为止我们知道了我们网关中数据是保存在哪的了,对于插件,选择器以及Rule的数据,放在一个BaseDataCache的容器中,并且分别用Map存储。
对于元数据单独放在一个MetaDataCache的容器中,也是使用Map存储。
当数据发生变化,或者我们在后台管理画面点了同步数据的按钮时,就会触发我们数据同步的方法。这个方法的流程大致如下:
- 所有操作调用
WebsocketDataHandler的executor方法,这里需要把操作类型和数据类型以及数据详细信息传递过来,这里的数据信息是以json传递过来的,使用com.google.gson进行数据解析。 - 然后就是根据不同的数据类型和操作类型调用实际的操作方式,即往map里put数据或者remove数据。
本文深入探讨Soul网关的数据同步机制,重点讲解WebSocket同步方式。内容包括数据同步的需求、原理、支持的方式,以及WebSocket同步的启动、连接管理和数据处理过程,揭示了数据如何在内存中实时更新并保证高性能。
267

被折叠的 条评论
为什么被折叠?



