Soul网关源码解析(九):nacos同步数据
Soul网关源码解析(九):nacos同步数据nacos同步数据数据同步配置admin启动时同步处理bootstrap启动时同步处理遇到的问题小结参考
nacos同步数据
数据同步配置
与前面的websocket,zookeeper相同,http长连接需要admin与bootstrap两边都要配置,admin和bootstrap都需要如下配置
soul : sync: nacos: url: localhost:8848 namespace: 1c10d748-af86-43b9-8265-75f487d20c6c
bootstrap另外需要增加nacos的maven依赖
<!--soul data sync start use nacos--> <dependency> <groupId>org.dromara</groupId> <artifactId>soul-spring-boot-starter-sync-data-nacos</artifactId> <version>${project.version}</version> </dependency>
admin启动时同步处理
同样的,先读取数据同步的配置NacosConfiguration,这里会构造一个NacosConfigService对象,基于NacosConfigService对象创建NacosDataChangedListener。在页面操作时,会触发PluginController.updatePlugin接口,在service层调用ApplicationEventPublisher对象,该对象广播其管理的ApplicationListener集合,这里就有soul的DataChangedEventDispatcher对象,该对象管理者soul中各种数据同步的监听器,其中就有前面创建的NacosDataChangedListener。
以插件数据变动举例,首先会根据插件id从nacos里获取一把数据,这个id对应nacos里的dataId概念,另外所有的数据在nacos里共用一个Group(DEFAULT_GROUP),让缓存数据与nacos里的保持一致;接着会根据事件类型,进行相应处理,比如说是更新事件,则将新传入的数据更新到缓存中。
#DataSyncConfiguration @Configuration @ConditionalOnProperty(prefix = "soul.sync.nacos", name = "url") @Import(NacosConfiguration.class) static class NacosListener { @Bean @ConditionalOnMissingBean(NacosDataChangedListener.class) public DataChangedListener nacosDataChangedListener(final ConfigService configService) { return new NacosDataChangedListener(configService); } } #NacosConfiguration @EnableConfigurationProperties(NacosProperties.class) public class NacosConfiguration { @Bean @ConditionalOnMissingBean(ConfigService.class) public ConfigService nacosConfigService(final NacosProperties nacosProp) throws Exception { Properties properties = new Properties(); if (nacosProp.getAcm() != null && nacosProp.getAcm().isEnabled()) { // Use aliyun ACM service properties.put(PropertyKeyConst.ENDPOINT, nacosProp.getAcm().getEndpoint()); properties.put(PropertyKeyConst.NAMESPACE, nacosProp.getAcm().getNamespace()); // Use subaccount ACM administrative authority properties.put(PropertyKeyConst.ACCESS_KEY, nacosProp.getAcm().getAccessKey()); properties.put(PropertyKeyConst.SECRET_KEY, nacosProp.getAcm().getSecretKey()); } else { properties.put(PropertyKeyConst.SERVER_ADDR, nacosProp.getUrl()); properties.put(PropertyKeyConst.NAMESPACE, nacosProp.getNamespace()); } return NacosFactory.createConfigService(properties); } } #DataChangedEventDispatcher public void onApplicationEvent(final DataChangedEvent event) { for (DataChangedListener listener : listeners) { switch (event.getGroupKey()) { case APP_AUTH: listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType()); break; case PLUGIN: listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType()); break; case RULE: listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType()); break; case SELECTOR: listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType()); break; case META_DATA: listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType()); break; default: throw new IllegalStateException("Unexpected value: " + event.getGroupKey()); } } } #NacosDataChangedListener public void onPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) { updatePluginMap(getConfig(PLUGIN_DATA_ID));//这里和下面的REFRESH,MYSELF有重复的感觉 switch (eventType) { case DELETE: changed.forEach(plugin -> PLUGIN_MAP.remove(plugin.getName())); break; case REFRESH: case MYSELF: Set<String> set = new HashSet<>(PLUGIN_MAP.keySet()); changed.forEach(plugin -> { set.remove(plugin.getName()); PLUGIN_MAP.put(plugin.getName(), plugin); }); PLUGIN_MAP.keySet().removeAll(set); break; default: changed.forEach(plugin -> PLUGIN_MAP.put(plugin.getName(), plugin)); break; } publishConfig(PLUGIN_DATA_ID, PLUGIN_MAP); } private void updatePluginMap(final String configInfo) { JsonObject jo = GsonUtils.getInstance().fromJson(configInfo, JsonObject.class); Set<String> set = new HashSet<>(PLUGIN_MAP.keySet()); for (Entry<String, JsonElement> e : jo.entrySet()) { set.remove(e.getKey()); ??? PLUGIN_MAP.put(e.getKey(), GsonUtils.getInstance().fromJson(e.getValue(), PluginData.class)); } PLUGIN_MAP.keySet().removeAll(set); }
bootstrap启动时同步处理
bootstrap启动后,同样会读取nacos配置,并创建NacosSyncDataService,创建过程中会开启监听,这里以插件监听说明,使用dataid从nacos里获取一把数据,然后先尝试删除该插件缓存的数据,再更新。然后建立dataid与监听集合的Map关系,当对应的dataid发生数据变动,则会进入监听器的receiveConfigInfo函数内进行相关的处理。
#NacosSyncDataConfiguration @Configuration @ConditionalOnClass(NacosSyncDataService.class) @ConditionalOnProperty(prefix = "soul.sync.nacos", name = "url") @Slf4j public class NacosSyncDataConfiguration { @Bean public SyncDataService nacosSyncDataService(final ObjectProvider<ConfigService> configService, final ObjectProvider<PluginDataSubscriber> pluginSubscriber, final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) { log.info("you use nacos sync soul data......."); return new NacosSyncDataService(configService.getIfAvailable(), pluginSubscriber.getIfAvailable(), metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList)); } ... } #NacosSyncDataService protected static final Map<String, List<Listener>> LISTENERS = Maps.newConcurrentMap(); public NacosSyncDataService(final ConfigService configService, final PluginDataSubscriber pluginDataSubscriber, final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) { super(configService, pluginDataSubscriber, metaDataSubscribers, authDataSubscribers); start(); } public void start() { watcherData(PLUGIN_DATA_ID, this::updatePluginMap); ... } protected void watcherData(final String dataId, final OnChange oc) { Listener listener = new Listener() { @Override public void receiveConfigInfo(final String configInfo) { // 当admin发生变化,bootstrap监听器会收到,并进入这里 oc.change(configInfo); } @Override public Executor getExecutor() { return null; } }; // 这里根据dataId从nacos拿数据,接着将拿到的配置数据调用updatePluginMap oc.change(getConfigAndSignListener(dataId, listener)); // 这个getOrDefault在查不到dataId的value时,会返回一个空数组 // 这里建立一个dataId与listener的映射关系 LISTENERS.getOrDefault(dataId, new ArrayList<>()).add(listener); } #NacosCacheHandler protected void updatePluginMap(final String configInfo) { try { List<PluginData> pluginDataList = new ArrayList<>(GsonUtils.getInstance().toObjectMap(configInfo, PluginData.class).values()); pluginDataList.forEach(pluginData -> Optional.ofNullable(pluginDataSubscriber).ifPresent(subscriber -> { // 这里先将这个插件之前缓存数据清除,然后再插入新数据 subscriber.unSubscribe(pluginData); subscriber.onSubscribe(pluginData); })); } catch (JsonParseException e) { log.error("sync plugin data have error:", e); } } #CommonPluginDataSubscriber @Override public void unSubscribe(final PluginData pluginData) { subscribeDataHandler(pluginData, DataEventTypeEnum.DELETE); } @Override public void onSubscribe(final PluginData pluginData) { subscribeDataHandler(pluginData, DataEventTypeEnum.UPDATE); } 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) { ... } else if (data instanceof RuleData) { ... } }); }
遇到的问题
在启动admin之后,开启了monitor插件,但未配置selector和rule时,启动bootstrap,会运行报错,NacosCacheHandler.updateSelectorMap的入参configInfo为null,这是由于NacosSyncDataService的watcherData方法从nacos里拿数据时,由于未配置数据,nacos里是空的值。而在配置之后,nacos会先放在缓存,然后在客户端获取时,创建一个快照文件放到客户端。所以要正确运行nacos的同步功能,需要先在admin那边开启插件,并给插件配置选择器和规则,另外元数据和认证信息也需要。
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException { group = null2defaultGroup(group); ParamUtils.checkKeyParam(dataId, group); ConfigResponse cr = new ConfigResponse(); cr.setDataId(dataId); cr.setTenant(tenant); cr.setGroup(group); // 优先使用本地配置 String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant); ... try { // 这里从 String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs); cr.setContent(ct[0]); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); return content; } catch (NacosException ioe) { ... } ... // 从本地获取快照内容(本地文档) content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant); cr.setContent(content); configFilterChainManager.doFilter(null, cr); content = cr.getContent(); return content; }
小结
本小节以nacos配置为起点,介绍如何配置nacos进行同步数据;接着介绍了admin启动时与nacos的交互,然后在bootstrap启动时会从nacos拉取信息,并在admin发生变化时,通过监听器收到并更新自己的内存,最后简单说明了初次运行nacos同步数据时遇到的问题,以及debug找到问题的点,实际是nacos在配置未发生变动时,内部是没有数据,那么在bootstrap拉取时,是不会有值,抛出空指针。希望能帮到你,初识soul这样一个极致性能的网关项目。