阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)

zhyilo
发布于 2022-6-1 16:49
浏览
0收藏

 

获取配置

Nacos获取配置数据的逻辑比较简单,先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP请求从远端拉取对应dataId配置数据,并保存到本地快照中,请求默认重试3次,超时时间3s。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
获取配置有getConfig()和getConfigAndSignListener()这两个接口,但getConfig()只是发送普通的HTTP请求,而getConfigAndSignListener()则多了发起长轮询和对dataId数据变更注册监听的操作addTenantListenersWithContent()。

@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
    return getConfigInner(namespace, dataId, group, timeoutMs);
}

@Override
public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener)
        throws NacosException {
    String content = getConfig(dataId, group, timeoutMs);
    worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
    return content;
}

注册监听

客户端注册监听,先从cacheMap中拿到dataId对应的CacheData对象。

public void addTenantListenersWithContent(String dataId, String group, String content,
                                          List<? extends Listener> listeners) throws NacosException {
    group = blank2defaultGroup(group);
    String tenant = agent.getTenant();
    // 1、获取dataId对应的CacheData,如没有则向服务端发起长轮询请求获取配置
    CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
    synchronized (cache) {
        // 2、注册对dataId的数据变更监听
        cache.setContent(content);
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
        cache.setSyncWithServer(false);
        agent.notifyListenConfig();
    }
}

如没有则向服务端发起长轮询请求获取配置,默认的Timeout时间为30s,并把返回的配置数据回填至CacheData对象的content字段,同时用content生成MD5值;再通过addListener()注册监听器。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
CacheData也是个出场频率非常高的一个类,我们看到除了dataId、group、tenant、content这些相关的基础属性,还有几个比较重要的属性如:listeners、md5(content真实配置数据计算出来的md5值),以及注册监听、数据比对、服务端数据变更通知操作都在这里。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
其中listeners是对dataId所注册的所有监听器集合,其中的ManagerListenerWrap对象除了持有Listener监听类,还有一个lastCallMd5字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。

在添加监听的同时会将CacheData对象当前最新的md5值赋值给ManagerListenerWrap对象的lastCallMd5属性。

public void addListener(Listener listener) {
    ManagerListenerWrap wrap =
        (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
            : new ManagerListenerWrap(listener, md5);
}

看到这对dataId监听设置就完事了?我们发现所有操作都围着cacheMap结构中的CacheData对象,那么大胆猜测下一定会有专门的任务来处理这个数据结构。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
变更通知

客户端又是如何感知服务端数据已变更呢?

我们还是从头看,NacosConfigService类的构造器中初始化了一个ClientWorker,而在ClientWorker类的构造器中又启动了一个线程池来轮询cacheMap。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
而在executeConfigListen()方法中有这么一段逻辑,检查cacheMap中dataId的CacheData对象内,MD5字段与注册的监听listener内的lastCallMd5值,不相同表示配置数据变更则触发safeNotifyListener方法,发送数据变更通知。

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
        }
    }
}

safeNotifyListener()方法单独起线程,向所有对dataId注册过监听的客户端推送变更后的数据内容。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
客户端接收通知,直接实现receiveConfigInfo()方法接收回调数据,处理自身业务就可以了。

configService.addListener(dataId, group, new Listener() {
    @Override
    public void receiveConfigInfo(String configInfo) {
        System.out.println("receive:" + configInfo);
    }

    @Override
    public Executor getExecutor() {
        return null;
    }
});

为了理解更直观我用测试demo演示下,获取服务端配置并设置监听,每当服务端配置数据变化,客户端监听都会收到通知,一起看下效果。

public static void main(String[] args) throws NacosException, InterruptedException {
    String serverAddr = "localhost";
    String dataId = "test";
    String group = "DEFAULT_GROUP";
    Properties properties = new Properties();
    properties.put("serverAddr", serverAddr);
    ConfigService configService = NacosFactory.createConfigService(properties);
    String content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
    configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
            System.out.println("数据变更 receive:" + configInfo);
        }
        @Override
        public Executor getExecutor() {
            return null;
        }
    });

    boolean isPublishOk = configService.publishConfig(dataId, group, "我是新配置内容~");
    System.out.println(isPublishOk);

    Thread.sleep(3000);
    content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
}

结果和预想的一样,当向服务端publishConfig数据变化后,客户端可以立即感知,愣是用主动拉pull模式做出了服务端实时推送的效果。

数据变更 receive:我是新配置内容~
true
我是新配置内容~

服务端源码分析
Nacos配置中心的服务端源码主要在nacos-config项目的ConfigController类,服务端的逻辑要比客户端稍复杂一些,这里我们重点看下。

处理长轮询

服务端对外提供的监听接口地址/v1/cs/configs/listener,这个方法内容不多,顺着doPollingConfig往下看。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
服务端根据请求header中的Long-Pulling-Timeout属性来区分请求是长轮询还是短轮询,这里咱们只关注长轮询部分,接着看LongPollingService(记住这个service很关键)类中的addLongPollingClient()方法是如何处理客户端的长轮询请求的。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
正常客户端默认设置的请求超时时间是30s,但这里我们发现服务端“偷偷”的给减掉了500ms,现在超时时间只剩下了29.5s,那为什么要这样做呢?

用官方的解释之所以要提前500ms响应请求,为了最大程度上保证客户端不会因为网络延时造成超时,考虑到请求可能在负载均衡时会耗费一些时间,毕竟Nacos最初就是按照阿里自身业务体量设计的嘛!

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
此时对客户端提交上来的groupkey的MD5与服务端当前的MD5比对,如md5值不同,则说明服务端的配置项发生过变更,直接将该groupkey放入changedGroupKeys集合并返回给客户端。

MD5Util.compareMd5(req, rsp, clientMd5Map)

如未发生变更,则将客户端请求挂起,这个过程先创建一个名为ClientLongPolling的调度任务Runnable,并提交给scheduler定时线程池延后29.5s执行。

ConfigExecutor.executeLongPolling(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

这里每个长轮询任务携带了一个asyncContext对象,使得每个请求可以延迟响应,等延时到达或者配置有变更之后,调用asyncContext.complete()响应完成。

“asyncContext 为 Servlet 3.0新增的特性,异步处理,使Servlet线程不再需要一直阻塞,等待业务处理完毕才输出响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。
 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
ClientLongPolling任务被提交进入延迟线程池执行的同时,服务端会通过一个allSubs队列保存所有正在被挂起的客户端长轮询请求任务,这个是客户端注册监听的过程。

如延时期间客户端据数一直未变化,延时时间到达后将本次长轮询任务从allSubs队列剔除,并响应请求response,这是取消监听。收到响应后客户端再次发起长轮询,循环往复。

阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区

 处理长轮询

到这我们知道服务端是如何挂起客户端长轮询请求的,一旦请求在挂起期间,用户通过管理平台操作了配置项,或者服务端收到了来自其他客户端节点修改配置的请求。

怎么能让对应已挂起的任务立即取消,并且及时通知客户端数据发生了变更呢?

数据变更

管理平台或者客户端更改配置项接位置ConfigController中的publishConfig方法。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
值得注意得是,在publishConfig接口中有这么一段逻辑,某个dataId配置数据被修改时会触发一个数据变更事件Event。

ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

仔细看LongPollingService会发现在它的构造方法中,正好订阅了数据变更事件,并在事件触发时执行一个数据变更调度任务DataChangeTask。

阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区

 订阅数据变更事件

DataChangeTask内的主要逻辑就是遍历allSubs队列,上边我们知道,这个队列中维护的是所有客户端的长轮询请求任务,从这些任务中找到包含当前发生变更的groupkey的ClientLongPolling任务,以此实现数据更变推送给客户端,并从allSubs队列中剔除此长轮询任务。

阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区

 DataChangeTask

而我们在看给客户端响应response时,调用asyncContext.complete()结束了异步请求。

 阿里面试这样问:Nacos配置中心交互模型是push还是pull ?(二)-鸿蒙开发者社区
结束语
上边只揭开了nacos配置中心的冰山一角,实际上还有非常多重要的技术细节都没提及到,建议大家没事看看源码,源码不需要通篇的看,只要抓住核心部分就够了。就比如今天这个题目以前我真没太在意,突然被问一下子吃不准了,果断看下源码,而且这样记忆比较深刻(别人嚼碎了喂你的知识总是比自己咀嚼的差那么点意思)。

nacos的源码我个人觉得还是比较朴素的,代码并没有过多炫技,看起来相对轻松。大家不要对看源码有什么抵触,它也不过是别人写的业务代码而已,just so so!

 
我是小富~,如果对你有用在看、关注支持下,咱们下期见~

 

 

文章转自公众号:程序员小富

分类
标签
已于2022-6-1 16:49:48修改
收藏
回复
举报
回复
    相关推荐