Soul网关源码解析(九):nacos同步数据

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 官网

猜你喜欢

转载自blog.csdn.net/S_powerStone/article/details/113094066
今日推荐