ShenYu网关源码阅读(十七)Nacos数据同步解析-Bootstrap端

865 阅读6分钟

简介

    基于上篇:Soul网关源码阅读(十五)Zookeeper数据同步-Bootstrap端,这篇我们要研究下Nacos数据同步原理

概览

    Nacos数据同步和zookeeper数据同步非常相似,都是通过监听数据变化的手段来进行实现的,而且从Debug的数据接收情况来看,形式上表现的也是增量更新的

    但猫大人推荐的同步方式是websocket和zookeeper,原因是他们能增量更新,并没有提到nacos。在提到HTTP长轮询的时候,提了以后Nacos的实现,则猜测Nacos内部实现的方式是长轮询,所以不推荐Nacos数据同步方式。但这只是猜测,需要后面研究下Nacos才能验证,这里先大胆猜一下

    通过分析,Nacos的数据同步方式流程基本如下:

  • 1.构造Nacos相关服务
  • 2.启动监听目前的五种数据类型
  • 3.收到改动的时候调用相应的subscribe进行数据更新

    Nacos的数据更新代码有些令人疑惑,在数据变化收到数据的时候,先进行unSubscribe,再onSubscribe,相当的令人不解

    而且Nacos好像收不到数据删除通知

    具体解析过程请看源码Debug环节

示例运行

    2021.1.23日之前的Soul主分支版本Nacos同步存在问题,运行需要参考上篇:Soul网关源码阅读(十五)Zookeeper数据同步-Bootstrap端,进行配置,配置运行后,我们进入源码Debug环节

源码Debug

启动配置跟踪

    首先寻找切入点,在之前的Soul网关源码阅读(十二)数据同步初探中,我们知道了Nacos数据同步的入口类是:

  • soul-sync-data-nacos : NacosCacheHandler

    我们找到相应的类,其构造函数如下,我们看到了熟悉的Subscribe关键字,通过调用他们实现本地缓存的更新

public class NacosCacheHandler {
    public NacosCacheHandler(final ConfigService configService, final PluginDataSubscriber pluginDataSubscriber,
                             final List<MetaDataSubscriber> metaDataSubscribers,
                             final List<AuthDataSubscriber> authDataSubscribers) {
        this.configService = configService;
        this.pluginDataSubscriber = pluginDataSubscriber;
        this.metaDataSubscribers = metaDataSubscribers;
        this.authDataSubscribers = authDataSubscribers;
    }

    protected void updatePluginMap(final String configInfo) {
        try {
            // Fix bug #656(https://github.com/dromara/soul/issues/656)
            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);
        }
    }
}

    我们在上面的构造函数上打上断点,看看其调用栈

    我们来到下面这个类,我们看到了其继承了NacosCacheHandler,构造好相应的数据后,start启动,在其函数中,我们看到了和zookeeper监听非常相似的函数,我们猜测这个函数就是监听的函数。依据前面的经验,在里面应该进行了一些初始化的工作后进行变化监听,我们后面具体调试看看。继续在构造函数上打上断点,再往上看看

public class NacosSyncDataService extends NacosCacheHandler implements AutoCloseable, SyncDataService {

    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);
        watcherData(SELECTOR_DATA_ID, this::updateSelectorMap);
        watcherData(RULE_DATA_ID, this::updateRuleMap);
        watcherData(META_DATA_ID, this::updateMetaDataMap);
        watcherData(AUTH_DATA_ID, this::updateAuthMap);
    }

    @Override
    public void close() {
        LISTENERS.forEach((dataId, lss) -> {
            lss.forEach(listener -> getConfigService().removeListener(dataId, GROUP, listener));
            lss.clear();
        });
        LISTENERS.clear();
    }
}

    在上面的构造函数打上断点后,我们跟踪调用栈,来到熟悉的Spring配置,这里配置了Nacos相关的东西,构造nacosSyncDataService的时候启动了Nacos监听

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));
    }

    @Bean
    public ConfigService nacosConfigService(final NacosConfig nacosConfig) throws Exception {
        Properties properties = new Properties();
        if (nacosConfig.getAcm() != null && nacosConfig.getAcm().isEnabled()) {
            properties.put(PropertyKeyConst.ENDPOINT, nacosConfig.getAcm().getEndpoint());
            properties.put(PropertyKeyConst.NAMESPACE, nacosConfig.getAcm().getNamespace());
            properties.put(PropertyKeyConst.ACCESS_KEY, nacosConfig.getAcm().getAccessKey());
            properties.put(PropertyKeyConst.SECRET_KEY, nacosConfig.getAcm().getSecretKey());
        } else {
            properties.put(PropertyKeyConst.SERVER_ADDR, nacosConfig.getUrl());
            properties.put(PropertyKeyConst.NAMESPACE, nacosConfig.getNamespace());
        }
        return NacosFactory.createConfigService(properties);
    }

    @Bean
    @ConfigurationProperties(prefix = "soul.sync.nacos")
    public NacosConfig nacosConfig() {
        return new NacosConfig();
    }
}

数据初始化与监听

    下面我们回过头来看看,我们取消所有的断点,将断点打在下面start函数中,看看watcherData的具体逻辑

public class NacosSyncDataService extends NacosCacheHandler implements AutoCloseable, SyncDataService {

    public void start() {
        watcherData(PLUGIN_DATA_ID, this::updatePluginMap);
        watcherData(SELECTOR_DATA_ID, this::updateSelectorMap);
        watcherData(RULE_DATA_ID, this::updateRuleMap);
        watcherData(META_DATA_ID, this::updateMetaDataMap);
        watcherData(AUTH_DATA_ID, this::updateAuthMap);
    }
}

    打上断点重启后,我们进入到watcherData内部

    初看我们可能有点懵,一个函数通用调用就监听了我们之前提到的五种数据变化

    通过跟踪调试,发现其是靠上面函数中的类型和传入的函数进行区分,从而达到一个函数通用调用监听五种数据的,应该是其内部的实现机制

    大致是根据传入的数据类型id,调用相应的处理函数,这里知晓其大意即可

public class NacosCacheHandler {

    protected void watcherData(final String dataId, final OnChange oc) {
        Listener listener = new Listener() {
            @Override
            public void receiveConfigInfo(final String configInfo) {
                // 在这里没有发现属性的updatePluginMap之类的
                oc.change(configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        };
        // 这么通过调试,发现触发了初始化操作,从全量的同步一次数据
        oc.change(getConfigAndSignListener(dataId, listener));
        // 这里大致看出是添加进监听列表中之类的
        LISTENERS.getOrDefault(dataId, new ArrayList<>()).add(listener);
    }

    protected interface OnChange {
        void change(String changeData);
    }
}

    我们在下面的更新插件数据的函数中打上断点,发现在进行第一次启动初始化和修改Admin后台管理界面的插件数据后,都会走到下面的函数:

public class NacosCacheHandler {

    protected void updatePluginMap(final String configInfo) {
        try {
            // Fix bug #656(https://github.com/dromara/soul/issues/656)
            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);
        }
    }
}

    从上面函数大意中,我们大意看出:这是个进行插件信息更新的函数,当收到数据的时候,将其序列化,然后调用相应的subscribe

    但是我们发现了一个奇怪的逻辑,它先调用删除的逻辑,然后调用更新的逻辑,如前面我们所知,在插件数据进行更新的时候,他们会最终都在走入下面的subscribeDataHandler函数逻辑

    而进行更新和删除无非就是对本地缓存中的Map进行操作,而在上面的先un再on的操作下,unSubscribe删除数据是不起作用的

    而且更新数据也不用先删除再添加,直接put替换掉原理的数据即可,不用先删除

    也就是这个unSubscribe可能是无效和多余的

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);
    }
    
    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) {
                    // 最终都会进行Map的更新操作
                    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 {
                ......
            }
        });
    }
}

    而且通过进一步的Debug和分析,发现Nacos似乎不能监听到数据的删除事件,那就是说如果数据失效没有用了,在Bootstrap没有重启的情况下,这些无效的数据会一直占用内存

    如果是上面分析的那样,那这个Nacos同步机制就有些问题了

总结

    这篇大致分析了Nacos的大致数据同步原理,知道同步流程大致如下:

  • NacosSyncDataConfiguration : Nacos启动配置
  • NacosSyncDataService
    • 1.初始化读取全量数据进行本地缓存刷新
    • 2.启动数据变化监听
    • 3.接收变化数据,调用相应的subScribe进行相应的更新

    同时我们发现可能还存在的问题:

  • 1.无法接收到数据删除数据
  • 2.在数据变化监听处理函数中,unSubscribe函数可能是无用和多余的