Nacos配置中心交互模型是 push 还是 pull ?你应该这么回答

>本文案例收录在 https://github.com/chengxy-nds/Springboot-Notebook 大家好,我是小富~ 对于`Nacos`大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。 比如我们今天要讨论的话题,`Nacos`在做配置中心的时候,配置数据的交互模式是服务端推过来还是客户端主动拉的? ![](https://img-blog.csdnimg.cn/20210604073705295.png) 这里我先抛出答案:客户端主动拉的! 接下来咱们扒一扒`Nacos`的源码,来看看它具体是如何实现的? ### 配置中心 聊`Nacos`之前简单回顾下配置中心的由来。 简单理解配置中心的作用就是对配置统一管理,修改配置后应用可以动态感知,而无需重启。 因为在传统项目中,大多都采用静态配置的方式,也就是把配置信息都写在应用内的`yml`或`properties`这类文件中,如果要想修改某个配置,通常要重启应用才可以生效。 但有些场景下,比如我们想要在应用运行时,通过修改某个配置项,实时的控制某一个功能的开闭,频繁的重启应用肯定是不能接受的。 尤其是在微服务架构下,我们的应用服务拆分的粒度很细,少则几十多则上百个服务,每个服务都会有一些自己特有或通用的配置。假如此时要改变通用配置,难道要我挨个改几百个服务配置?很显然这不可能。所以为了解决此类问题配置中心应运而生。 ![配置中心](https://img-blog.csdnimg.cn/202106102227286.png?) ### 推与拉模型 客户端与配置中心的数据交互方式其实无非就两种,要么推`push`,要么拉`pull`。 **推模型** 客户端与服务端建立`TCP`长连接,当服务端配置数据有变动,立刻通过建立的长连接将数据推送给客户端。 优势:长链接的优点是实时性,一旦数据变动,立即推送变更数据给客户端,而且对于客户端而言,这种方式更为简单,只建立连接接收数据,并不需要关心是否有数据变更这类逻辑的处理。 弊端:长连接可能会因为网络问题,导致不可用,也就是俗称的`假死`。连接状态正常,但实际上已无法通信,所以要有的心跳机制`KeepAlive`来保证连接的可用性,才可以保证配置数据的成功推送。 **拉模型** 客户端主动的向服务端发请求拉配置数据,常见的方式就是轮询,比如每3s向服务端请求一次配置数据。 轮询的优点是实现比较简单。但弊端也显而易见,轮询无法保证数据的实时性,什么时候请求?间隔多长时间请求一次?都是不得不考虑的问题,而且轮询方式对服务端还会产生不小的压力。 ### 长轮询 开篇我们就给出了答案,`nacos`采用的是客户端主动拉`pull`模型,应用长轮询(`Long Polling`)的方式来获取配置数据。 额?以前只听过轮询,长轮询又是什么鬼?它和传统意义上的轮询(暂且叫短轮询吧,方便比较)有什么不同呢? **短轮询** 不管服务端配置数据是否有变化,不停的发起请求获取配置,比如支付场景中前段JS轮询订单支付状态。 这样的坏处显而易见,由于配置数据并不会频繁变更,若是一直发请求,势必会对服务端造成很大压力。还会造成推送数据的延迟,比如:每10s请求一次配置,如果在第11s时配置更新了,那么推送将会延迟9s,等待下一次请求。 ![](https://img-blog.csdnimg.cn/2021061010185363.png) 为了解决短轮询的问题,有了长轮询方案。 **长轮询** 长轮询可不是什么新技术,它不过是由服务端控制响应客户端请求的返回时间,来减少客户端无效请求的一种优化手段,其实对于客户端来说与短轮询的使用并没有本质上的区别。 客户端发起请求后,服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。 ![](https://img-blog.csdnimg.cn/20210610101939816.png) ### Nacos初识 为了后续演示操作方便我在本地搭了个`Nacos`。**注意:** 运行时遇到个小坑,由于`Nacos`默认是以`cluster`集群的方式启动,而本地搭建通常是单机模式`standalone`,这里需手动改一下启动脚本`startup.X`中的启动模式。 ![](https://img-blog.csdnimg.cn/2021062311431965.png) 直接执行`/bin/startup.X`就可以了,默认用户密码均是`nacos`。 ![](https://img-blog.csdnimg.cn/20210623113802760.png) ### 几个概念 `Nacos`配置中心的几个核心概念:`dataId`、`group`、`namespace`,它们的层级关系如下图: ![](https://img-blog.csdnimg.cn/20210610224127412.png) `dataId`:是配置中心里最基础的单元,它是一种`key-value`结构,`key`通常是我们的配置文件名称,比如:`application.yml`、`mybatis.xml`,而`value`是整个文件下的内容。 目前支持`JSON`、`XML`、`YAML`等多种配置格式。 ![](https://img-blog.csdnimg.cn/20210610223552112.png?) `group`:dataId配置的分组管理,比如同在dev环境下开发,但同环境不同分支需要不同的配置数据,这时就可以用分组隔离,默认分组`DEFAULT_GROUP`。 `namespace`:项目开发过程中肯定会有`dev`、`test`、`pro`等多个不同环境,`namespace`则是对不同环境进行隔离,默认所有配置都在`public`里。 ### 架构设计 下图简要描述了`nacos`配置中心的架构流程。 客户端、控制台通过发送Http请求将配置数据注册到服务端,服务端持久化数据到Mysql。 客户端拉取配置数据,并批量设置对`dataId`的监听发起长轮询请求,如服务端配置项变更立即响应请求,如无数据变更则将请求挂起一段时间,直到达到超时时间。为减少对服务端压力以及保证配置中心可用性,拉取到配置数据客户端会保存一份快照在本地文件中,优先读取。 ![](https://img-blog.csdnimg.cn/20210614182427299.png) 这里我省略了比较多的细节,如鉴权、负载均衡、高可用方面的设计(其实这部分才是真正值得学的,后边另出文讲吧),主要弄清客户端与服务端的数据交互模式。 >下边我们以Nacos 2.0.1版本源码分析,2.0以后的版本改动较多,和网上的很多资料略有些不同 地址:https://github.com/alibaba/nacos/releases/tag/2.0.1 ### 客户端源码分析 `Nacos`配置中心的客户端源码在`nacos-client`项目,其中`NacosConfigService`实现类是所有操作的核心入口。 说之前先了解个客户端数据结构`cacheMap`,这里大家重点记住它,因为它几乎贯穿了Nacos客户端的所有操作,由于存在多线程场景为保证数据一致性,`cacheMap`采用了`AtomicReference`原子变量实现。 ```java /**  * groupKey -> cacheData.  */ private final AtomicReference > cacheMap = new AtomicReference >(new HashMap<>()); ``` `cacheMap`是个Map结构,key为`groupKey`,是由dataId, group, tenant(租户)拼接的字符串;value为`CacheData`对象,每个dataId都会持有一个CacheData对象。 **获取配置** `Nacos`获取配置数据的逻辑比较简单,先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP请求从远端拉取对应dataId配置数据,并保存到本地快照中,请求默认重试3次,超时时间3s。 ![](https://img-blog.csdnimg.cn/20210701072441625.png) 获取配置有`getConfig()`和`getConfigAndSignListener()`这两个接口,但`getConfig()`只是发送普通的HTTP请求,而`getConfigAndSignListener()`则多了发起长轮询和对dataId数据变更注册监听的操作`addTenantListenersWithContent()`。 ```java @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`对象。 ```JAVA public void addTenantListenersWithContent(String dataId, String group, String content,                                           List 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()`注册监听器。 ![](https://img-blog.csdnimg.cn/20210628190010178.png) `CacheData`也是个出场频率非常高的一个类,我们看到除了dataId、group、tenant、content这些相关的基础属性,还有几个比较重要的属性如:`listeners`、`md5`(content真实配置数据计算出来的md5值),以及注册监听、数据比对、服务端数据变更通知操作都在这里。 ![](https://img-blog.csdnimg.cn/20210630102847716.png) 其中`listeners`是对dataId所注册的所有监听器集合,其中的`ManagerListenerWrap`对象除了持有`Listener`监听类,还有一个`lastCallMd5`字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。 在添加监听的同时会将`CacheData`对象当前最新的md5值赋值给`ManagerListenerWrap`对象的`lastCallMd5`属性。 ```java public void addListener(Listener listener) {     ManagerListenerWrap wrap =         (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)             : new ManagerListenerWrap(listener, md5); } ``` 看到这对dataId监听设置就完事了?我们发现所有操作都围着`cacheMap`结构中的`CacheData`对象,那么大胆猜测下一定会有专门的任务来处理这个数据结构。 ![](https://img-blog.csdnimg.cn/20210701220037843.png) **变更通知** 客户端又是如何感知服务端数据已变更呢? 我们还是从头看,`NacosConfigService`类的构造器中初始化了一个`ClientWorker`,而在`ClientWorker`类的构造器中又启动了一个线程池来轮询`cacheMap`。 ![](https://img-blog.csdnimg.cn/20210628192527953.png) 而在`executeConfigListen()`方法中有这么一段逻辑,检查`cacheMap`中dataId的`CacheData`对象内,MD5字段与注册的监听`listener`内的`lastCallMd5值`,不相同表示配置数据变更则触发`safeNotifyListener`方法,发送数据变更通知。 ```java void checkListenerMd5() {     for (ManagerListenerWrap wrap : listeners) {         if (!md5.equals(wrap.lastCallMd5)) {             safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);         }     } } ``` `safeNotifyListener()`方法单独起线程,向所有对`dataId`注册过监听的客户端推送变更后的数据内容。 ![](https://img-blog.csdnimg.cn/20210628194850513.png) 客户端接收通知,直接实现`receiveConfigInfo()`方法接收回调数据,处理自身业务就可以了。 ```java configService.addListener(dataId, group, new Listener() {     @Override     public void receiveConfigInfo(String configInfo) {         System.out.println("receive:" + configInfo);     }     @Override     public Executor getExecutor() {         return null;     } }); ``` 为了理解更直观我用测试demo演示下,获取服务端配置并设置监听,每当服务端配置数据变化,客户端监听都会收到通知,一起看下效果。 ```java 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`模式做出了服务端实时推送的效果。 ```java 数据变更 receive:我是新配置内容~ true 我是新配置内容~ ``` ### 服务端源码分析 `Nacos`配置中心的服务端源码主要在`nacos-config`项目的`ConfigController`类,服务端的逻辑要比客户端稍复杂一些,这里我们重点看下。 **处理长轮询** 服务端对外提供的监听接口地址`/v1/cs/configs/listener`,这个方法内容不多,顺着`doPollingConfig`往下看。 ![](https://img-blog.csdnimg.cn/2021062314550761.png) 服务端根据请求`header`中的`Long-Pulling-Timeout`属性来区分请求是长轮询还是短轮询,这里咱们只关注长轮询部分,接着看`LongPollingService`(记住这个service很关键)类中的`addLongPollingClient()`方法是如何处理客户端的长轮询请求的。 ![](https://img-blog.csdnimg.cn/20210623145954286.png?) 正常客户端默认设置的请求超时时间是`30s`,但这里我们发现服务端“偷偷”的给减掉了`500ms`,现在超时时间只剩下了`29.5s`,那为什么要这样做呢? 用官方的解释之所以要提前500ms响应请求,为了最大程度上保证客户端不会因为网络延时造成超时,考虑到请求可能在负载均衡时会耗费一些时间,毕竟`Nacos`最初就是按照阿里自身业务体量设计的嘛! ![](https://img-blog.csdnimg.cn/20210623153217273.png) 此时对客户端提交上来的`groupkey`的MD5与服务端当前的MD5比对,如`md5`值不同,则说明服务端的配置项发生过变更,直接将该`groupkey`放入`changedGroupKeys`集合并返回给客户端。 ```Java MD5Util.compareMd5(req, rsp, clientMd5Map) ``` 如未发生变更,则将客户端请求挂起,这个过程先创建一个名为`ClientLongPolling`的调度任务`Runnable`,并提交给`scheduler`定时线程池延后`29.5s`执行。 ```Java ConfigExecutor.executeLongPolling(                 new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag)); ``` 这里每个长轮询任务携带了一个`asyncContext`对象,使得每个请求可以延迟响应,等延时到达或者配置有变更之后,调用`asyncContext.complete()`响应完成。 > asyncContext 为 Servlet 3.0新增的特性,异步处理,使Servlet线程不再需要一直阻塞,等待业务处理完毕才输出响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。 ![](https://img-blog.csdnimg.cn/20210626084513551.png?) `ClientLongPolling`任务被提交进入延迟线程池执行的同时,服务端会通过一个`allSubs`队列保存所有正在被挂起的客户端长轮询请求任务,这个是客户端注册监听的过程。 如延时期间客户端据数一直未变化,延时时间到达后将本次长轮询任务从`allSubs`队列剔除,并响应请求`response`,这是`取消监听`。收到响应后客户端再次发起长轮询,循环往复。 ![处理长轮询](https://img-blog.csdnimg.cn/20210626222414701.jpg) 到这我们知道服务端是如何挂起客户端长轮询请求的,一旦请求在挂起期间,用户通过管理平台操作了配置项,或者服务端收到了来自其他客户端节点修改配置的请求。 怎么能让对应已挂起的任务立即取消,并且及时通知客户端数据发生了变更呢? **数据变更** 管理平台或者客户端更改配置项接位置`ConfigController`中的`publishConfig`方法。 ![](https://img-blog.csdnimg.cn/20210626224923484.png) 值得注意得是,在`publishConfig`接口中有这么一段逻辑,某个`dataId`配置数据被修改时会触发一个数据变更事件`Event`。 ```java ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime())); ``` 仔细看`LongPollingService`会发现在它的构造方法中,正好订阅了数据变更事件,并在事件触发时执行一个数据变更调度任务`DataChangeTask`。 ![订阅数据变更事件](https://img-blog.csdnimg.cn/20210626224154897.png) `DataChangeTask`内的主要逻辑就是遍历`allSubs`队列,上边我们知道,这个队列中维护的是所有客户端的长轮询请求任务,从这些任务中找到包含当前发生变更的`groupkey`的`ClientLongPolling`任务,以此实现数据更变推送给客户端,并从`allSubs`队列中剔除此长轮询任务。 ![DataChangeTask](https://img-blog.csdnimg.cn/20210626232013667.png) 而我们在看给客户端响应`response`时,调用`asyncContext.complete()`结束了异步请求。 ![](https://img-blog.csdnimg.cn/20210701230414744.png) ### 结束语 上边只揭开了`nacos`配置中心的冰山一角,实际上还有非常多重要的技术细节都没提及到,建议大家没事看看源码,源码不需要通篇的看,只要抓住核心部分就够了。就比如今天这个题目以前我真没太在意,突然被问一下子吃不准了,果断看下源码,而且这样记忆比较深刻(别人嚼碎了喂你的知识总是比自己咀嚼的差那么点意思)。 `nacos`的源码我个人觉得还是比较**朴素**的,代码并没有过多炫技,看起来相对轻松。大家不要对看源码有什么抵触,它也不过是别人写的业务代码而已,**just so so**! --- **我是小富~**,如果对你有用**在看**、**关注**支持下,咱们下期见~ 整理了几百本各类技术电子书,有需要的同学自取。技术群快满了,想进的同学可以加我好友,和大佬们一起吹吹技术。  [**电子书地址**](https://github.com/chengxy-nds/Springboot-Notebook)   个人公众号: **程序员内点事**,欢迎交流 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201128210730424.png?)
优秀的个人博客,低调大师

微信关注我们

原文链接:https://blog.51cto.com/u_14787961/2978943

转载内容版权归作者及来源网站所有!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
优质分享Android(本站安卓app)

优质分享Android(本站安卓app)

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat7、8、9(Java Web服务器)

Apache Tomcat7、8、9(Java Web服务器)

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Eclipse(集成开发环境)

Eclipse(集成开发环境)

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。