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这样一个极致性能的网关项目。

本文深入解析Soul网关使用Nacos进行数据同步的配置和流程,包括admin启动时的数据同步处理,bootstrap启动时的同步操作,以及在数据变更时的监听与更新机制。此外,还讨论了初次运行时可能遇到的问题和解决方案。
540

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



