阿里面试Nacos配置中心交互模型是push还是pull原理解析

目录
  • 引言
  • 配置中心
  • 长轮询
  • Nacos初识
  • 几个概念
  • 客户端源码分析
  • 结束语

引言

对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。

比如我们今天要讨论的话题,Nacos在做配置中心的时候,配置数据的交互模式是服务端推过来还是客户端主动拉的?

这里我先抛出答案:客户端主动拉的!

接下来咱们扒一扒Nacos的源码,来看看它具体是如何实现的?

配置中心

Nacos之前简单回顾下配置中心的由来。

简单理解配置中心的作用就是对配置统一管理,修改配置后应用可以动态感知,而无需重启。

因为在传统项目中,大多都采用静态配置的方式,也就是把配置信息都写在应用内的ymlproperties这类文件中,如果要想修改某个配置,通常要重启应用才可以生效。

但有些场景下,比如我们想要在应用运行时,通过修改某个配置项,实时的控制某一个功能的开闭,频繁的重启应用肯定是不能接受的。

尤其是在微服务架构下,我们的应用服务拆分的粒度很细,少则几十多则上百个服务,每个服务都会有一些自己特有或通用的配置。假如此时要改变通用配置,难道要我挨个改几百个服务配置?很显然这不可能。所以为了解决此类问题配置中心应运而生。

配置中心

推与拉模型

客户端与配置中心的数据交互方式其实无非就两种,要么推push,要么拉pull

推模型

客户端与服务端建立TCP长连接,当服务端配置数据有变动,立刻通过建立的长连接将数据推送给客户端。

优势:长链接的优点是实时性,一旦数据变动,立即推送变更数据给客户端,而且对于客户端而言,这种方式更为简单,只建立连接接收数据,并不需要关心是否有数据变更这类逻辑的处理。

弊端:长连接可能会因为网络问题,导致不可用,也就是俗称的假死。连接状态正常,但实际上已无法通信,所以要有的心跳机制KeepAlive来保证连接的可用性,才可以保证配置数据的成功推送。

拉模型

客户端主动的向服务端发请求拉配置数据,常见的方式就是轮询,比如每3s向服务端请求一次配置数据。

轮询的优点是实现比较简单。但弊端也显而易见,轮询无法保证数据的实时性,什么时候请求?间隔多长时间请求一次?都是不得不考虑的问题,而且轮询方式对服务端还会产生不小的压力。

长轮询

开篇我们就给出了答案,nacos采用的是客户端主动拉pull模型,应用长轮询(Long Polling)的方式来获取配置数据。

额?以前只听过轮询,长轮询又是什么鬼?它和传统意义上的轮询(暂且叫短轮询吧,方便比较)有什么不同呢?

短轮询

不管服务端配置数据是否有变化,不停的发起请求获取配置,比如支付场景中前段JS轮询订单支付状态。

这样的坏处显而易见,由于配置数据并不会频繁变更,若是一直发请求,势必会对服务端造成很大压力。还会造成推送数据的延迟,比如:每10s请求一次配置,如果在第11s时配置更新了,那么推送将会延迟9s,等待下一次请求。

为了解决短轮询的问题,有了长轮询方案。

长轮询

长轮询可不是什么新技术,它不过是由服务端控制响应客户端请求的返回时间,来减少客户端无效请求的一种优化手段,其实对于客户端来说与短轮询的使用并没有本质上的区别。

客户端发起请求后,服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。

Nacos初识

为了后续演示操作方便我在本地搭了个Nacos注意: 运行时遇到个小坑,由于Nacos默认是以cluster集群的方式启动,而本地搭建通常是单机模式standalone,这里需手动改一下启动脚本startup.X中的启动模式。

直接执行/bin/startup.X就可以了,默认用户密码均是nacos

几个概念

Nacos配置中心的几个核心概念:dataIdgroupnamespace,它们的层级关系如下图:

dataId:是配置中心里最基础的单元,它是一种key-value结构,key通常是我们的配置文件名称,比如:application.ymlmybatis.xml,而value是整个文件下的内容。

目前支持JSONXMLYAML等多种配置格式。

group:dataId配置的分组管理,比如同在dev环境下开发,但同环境不同分支需要不同的配置数据,这时就可以用分组隔离,默认分组DEFAULT_GROUP

namespace:项目开发过程中肯定会有devtestpro等多个不同环境,namespace则是对不同环境进行隔离,默认所有配置都在public里。

架构设计

下图简要描述了nacos配置中心的架构流程。

客户端、控制台通过发送Http请求将配置数据注册到服务端,服务端持久化数据到Mysql。

客户端拉取配置数据,并批量设置对dataId的监听发起长轮询请求,如服务端配置项变更立即响应请求,如无数据变更则将请求挂起一段时间,直到达到超时时间。为减少对服务端压力以及保证配置中心可用性,拉取到配置数据客户端会保存一份快照在本地文件中,优先读取。

这里我省略了比较多的细节,如鉴权、负载均衡、高可用方面的设计(其实这部分才是真正值得学的,后边另出文讲吧),主要弄清客户端与服务端的数据交互模式。

下边我们以Nacos 2.0.1版本源码分析,2.0以后的版本改动较多,和网上的很多资料略有些不同 地址:

https://github.com/alibaba/nacos/releases/tag/2.0.1

客户端源码分析

Nacos配置中心的客户端源码在nacos-client项目,其中NacosConfigService实现类是所有操作的核心入口。

说之前先了解个客户端数据结构cacheMap,这里大家重点记住它,因为它几乎贯穿了Nacos客户端的所有操作,由于存在多线程场景为保证数据一致性,cacheMap采用了AtomicReference原子变量实现。

/**
 * groupKey -> cacheData.
 */
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(new HashMap<>());
 

cacheMap是个Map结构,key为groupKey,是由dataId, group, tenant(租户)拼接的字符串;value为CacheData对象,每个dataId都会持有一个CacheData对象。

获取配置

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

获取配置有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()注册监听器。

CacheData也是个出场频率非常高的一个类,我们看到除了dataId、group、tenant、content这些相关的基础属性,还有几个比较重要的属性如:listenersmd5(content真实配置数据计算出来的md5值),以及注册监听、数据比对、服务端数据变更通知操作都在这里。

其中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对象,那么大胆猜测下一定会有专门的任务来处理这个数据结构。

变更通知

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

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

而在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注册过监听的客户端推送变更后的数据内容。

客户端接收通知,直接实现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往下看。

服务端根据请求header中的Long-Pulling-Timeout属性来区分请求是长轮询还是短轮询,这里咱们只关注长轮询部分,接着看LongPollingService(记住这个service很关键)类中的addLongPollingClient()方法是如何处理客户端的长轮询请求的。

正常客户端默认设置的请求超时时间是30s,但这里我们发现服务端“偷偷”的给减掉了500ms,现在超时时间只剩下了29.5s,那为什么要这样做呢?

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

此时对客户端提交上来的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线程不再需要一直阻塞,等待业务处理完毕才输响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。

ClientLongPolling任务被提交进入延迟线程池执行的同时,服务端会通过一个allSubs队列保存所有正在被挂起的客户端长轮询请求任务,这个是客户端注册监听的过程。

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


处理长轮询

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

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

数据变更

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

值得注意得是,在publishConfig接口中有这么一段逻辑,某个dataId配置数据被修改时会触发一个数据变更事件Event

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

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


订阅数据变更事件

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


DataChangeTask

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

结束语

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

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

以上就是阿里面试Nacos配置中心交互模型是push还是pull原理解析的详细内容,更多关于Nacos配置中心交互模型的资料请关注我们其它相关文章!

(0)

相关推荐

  • 聊聊SpringBoot整合Nacos自动刷新配置的问题

    目录 目的 环境 pom 配置文件 代码 日志 测试 目的 Nacos作为SpringBoot服务的注册中心和配置中心. 在NacosServer中修改配置文件,在SpringBoot不重启的情况下,获取到修改的内容. 本例将在配置文件中配置一个 cml.age=100 的配置项,程序中编写一个方法读取配置文件,并通过 Get--->/test/age 接口提供给浏览器访问. 若配置文件中的 age 修改为 200 ,不用重新启动程序,直接访问 /test/age 接口,将获取到最新的值 200

  • Nginx实现Nacos反向代理的项目实践

    目录 1.win10安装Nginx 1.1 windows系统启动和停止的命令 2.win10安装nacos 2.1 搭建三台nacos步骤 1.win10安装Nginx nginx下载地址 nginx: download 下载后解压,进入bin目录,根据你的系统执行相应的命令 1.1 windows系统启动和停止的命令 启动 start nginx.exe 终止 nginx.exe -s stop //停止nginx nginx.exe -s reload //重新加载nginx nginx.

  • Spring的@Value如何从Nacos配置中心获取值并自动刷新

    目录 @Value从Nacos配置中心获取值并自动刷新 Nacos属性值自动刷新 1.@NacosValue获取最新值 2.@Value获取最新值 @Value从Nacos配置中心获取值并自动刷新 在使用Nacos作为配置中心时,除了@NacosValue可以做到自动刷新外,nacos-spring-context:0.3.4版本是支持@Value获取Nacos配置中心的值,并动态刷新的,该功能是Spri依靠ngValueAnnotationBeanPostProcessor类来实现: @Ove

  • nacos配置中心远程调用读取不到配置文件的解决

    目录 nacos配置中心远程调用读取不到配置文件 下面引用官方文档的一段内容 读取不到nacos配置中心内容的坑 这是出错的配置 这是正确的配置 nacos配置中心远程调用读取不到配置文件 1.由于公司要求,对nacos做了命名空间的配置.(这个开始我步入了迷途....) 为了更好的区分配置文件,我们创建了两个命名空间,dev和test,在学习的时候,我们并未创建这些命名空间,所以一直正常使用,创建命名空间后我们无法读取到配置文件. 那么添加命名空间后需要在配置文件中支持命名空间,类似你创建了g

  • springboot读取nacos配置文件的实现

    目录 首先,Nacos 的配置文件如下 第一种方式来解析 第二种方式来解析 SpringBoot 注册服务到 Nacos 上,由 Nacos 来做服务的管理.在 Nacos的配置列表中,管理着服务的配置文件.SpringBoot 有两种方式来读取配置文件的内容,一种是写配置文件类 @ConfigurationProperties ,一种是使用 @Value 注解. 首先,Nacos 的配置文件如下 ### 配置文件使用 yml 格式, 也可以使用 properties 格式,最终 yml 格式会

  • 阿里面试Nacos配置中心交互模型是push还是pull原理解析

    目录 引言 配置中心 长轮询 Nacos初识 几个概念 客户端源码分析 结束语 引言 对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现.配置管理,非常好用的一个工具.然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏. 比如我们今天要讨论的话题,Nacos在做配置中心的时候,配置数据的交互模式是服务端推过来还是客户端主动拉的? 这里我先抛出答案:客户端主动拉的! 接下来咱们扒一扒Nacos的源码,来看看它具体是如何实现的? 配置中心 聊N

  • Nacos配置中心搭建及动态刷新配置及踩坑记录

    目录 一.Nacos配置中心介绍 二.配置中心搭建 1.配置中心启用 2.创建配置文件 3.工程配置 三.试验结果 四.踩坑经历 五.总结 近期有用到阿里的开源配置中心及注册中心nacos,特此记录并分享一些学习笔记及配置踩坑点. 一.Nacos配置中心介绍 从架构图上可以知道,Nacos提供了两种服务,一种是用于服务注册.发现的Naming Service,一种是用于配置中心.动态配置的Config Service,而他们底层均由core模块来支持. 外层提供OpenAPI供客户端使用,并提供

  • SpringBoot使用Nacos配置中心的实现

    本文介绍SpringBoot如何使用阿里巴巴Nacos做配置中心. 1.Nacos简介 Nacos是阿里巴巴集团开源的一个易于使用的平台,专为动态服务发现,配置和服务管理而设计.它可以帮助您轻松构建云本机应用程序和微服务平台. Nacos基本上支持现在所有类型的服务,例如,Dubbo / gRPC服务,Spring Cloud RESTFul服务或Kubernetes服务. 尤其是使用Eureka注册中心的,并且担心Eureka闭源的开发者们,可以将注册中心修改为Nacos,本文主要介绍Naco

  • spring cloud如何集成nacos配置中心

    目录 spring cloud集成nacos配置中心 一.添加依赖 二.添加bootstrap.yml配置文件 三.添加远程配置 nacos作为SpringCloud配置中心 一.背景介绍 二.项目实战 三.总结 spring cloud集成nacos配置中心 一.添加依赖 <properties> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <nacos.version>0.9

  • Nacos配置中心集群原理及源码分析

    目录 Nacos集群工作原理 配置变更同步入口 AsyncNotifyService AsyncTask 目标节点接收请求 NacosDelayTaskExecuteEngine ProcessRunnable processTasks DumpProcessor.process Nacos作为配置中心,必然需要保证服务节点的高可用性,那么Nacos是如何实现集群的呢? 下面这个图,表示Nacos集群的部署图. Nacos集群工作原理 Nacos作为配置中心的集群结构中,是一种无中心化节点的设计

  • SpringCloud Nacos配置中心管理超详细讲解

    目录 一.Nacos配置管理 1.1 统一配置管理 1.1.1在nacos中添加配置文件 1.1.2 从微服务拉取配置 1.2 配置热更新 1.2.1 方式一 1.2.2 方式二 1.3 配置共享 一.Nacos配置管理 Nacos除了可以做注册中心,同样可以做配置管理来用 1.1 统一配置管理 当微服务部署越来越多,达到数十,数百时,逐个修改微服务配置就会很麻烦,且容易出错.我们需要一种统一配置管理方案,可以集中管理所有实例的配置. Nacos一方面更可以将配置集中管理另一方面在配置变更时,及

  • 详解Nacos配置中心的实现

    目录 基础配置 pom文件 YML文件 配置配置中心文件 Nacos中的匹配规则 Nacos作为配置中心-分类配置 Nacos的图形化管理界面 三种方案加载配置 DataID方案 Group方案 Namespace方案 基础配置 新建module:cloudalibaba-config-nacos-client3377 pom文件 版本号已经由父工程控制 <?xml version="1.0" encoding="UTF-8"?> <project

  • Nacos配置中心的配置文件的匹配规则及说明

    目录 Nacos配置中心配置文件的匹配 1.配置微服务配置中心文件 2.常用细节 3.从一个配置中心加载多个配置集 Nacos配置中心配置不成功问题 1.扫描包 2.关于配置nacos 3.为何没有热加载? 4.配置好为何还是没有热加载? 5.另一个坑 6.如何打开项目中的nacos Nacos配置中心配置文件的匹配 详情见nacos的官方文档 1.配置微服务配置中心文件 在 Nacos Spring Cloud 中,数据集(Data Id) 的配置完整格式如下: ${prefix}-${spr

随机推荐