当前位置: 首页 > 科技观察

Nacos配置中心交互模型是Push还是Pull?

时间:2023-03-12 03:24:03 科技观察

本文转载自微信公众号《程序员的内幕》,作者为程序员的内幕。转载本文请联系程序员内电史公众号。大家好,我是小付~Nacos大家应该不陌生。我出生在阿里,名声很好。它可以进行动态服务发现和配置管理。这是一个非常有用的工具。但是,使用这项技术的人越多,在面试中被问到的概率就越大。如果仅仅停留在使用层面,那么面试可能会吃大亏。比如我们今天要讨论的话题,Nacos作为配置中心时,配置数据的交互方式是服务端推送还是客户端主动拉取?这里我先给出答案:客户端主动拉!接下来,我们扒一扒Nacos的源码,看看它是如何实现的?在说配置中心的Nacos之前,先简单回顾一下配置中心的由来。配置中心的作用简单理解就是统一管理配置。配置修改后,应用无需重启即可动态感知。因为在传统项目中,大多采用静态配置,即配置信息写在应用程序中的yml或properties等文件中。如果要修改某个配置,通常需要重启应用才能生效。但是在某些场景下,比如我们希望在应用运行时通过修改某个配置项来实时控制某个功能的开启和关闭。频繁重启应用程序肯定是不能接受的。特别是在微服务架构下,我们应用服务拆分的粒度非常细,小到几十个,大到上百个服务,每个服务都会有自己独特或通用的配置。如果这时候我要改通用配置,是不是需要把上百个服务配置一个一个改?显然这是不可能的。因此,为了解决此类问题,配置中心应运而生。配置中心推拉模型客户端和配置中心之间的数据交互其实有两种方式,要么推,要么拉。推送模式客户端与服务器建立TCP长连接,当服务器的配置数据发生变化时,立即通过建立的长连接将数据推送给客户端。优点:长链接的优点是实时性。一旦数据发生变化,变化的数据会立即推送到客户端。对于客户端来说,这个方法比较简单。只需要建立连接就可以接收数据,不需要关心数据是否有变化。处理这种类型的逻辑。缺点:长连接可能因为网络问题不可用,俗称假死。连接状态正常,但实际上已经无法通信了。因此需要一个心跳机制KeepAlive来保证连接的可用性,从而保证配置数据的成功推送。拉取模型客户端主动向服务端发送请求拉取配置数据。常用的方法是轮询,比如每3秒向服务器请求一次配置数据。轮询的优点是实现起来比较简单。但缺点也很明显。轮询不能保证数据的实时性。什么时候申请?我们在长轮询开始时给出了答案。nacos采用客户端主动拉取的拉取模式,采用长轮询(LongPolling)获取配置数据。嗯?我以前只听说过投票。什么是长轮询?它与传统意义上的轮询有何不同(为了便于比较,我们称之为短轮询)?短轮询不管服务器配置数据是否有变化,不断发起请求获取配置。比如支付场景,前面的JS轮询订单支付状态。这样做的缺点很明显,因为配置数据不经常变化,如果一直发送请求,势必会给服务器带来很大的压力。也会造成推送数据的延迟。例如,每10s请求一次配置。如果配置在11s更新,推送会延迟9s等待下一次请求。为了解决短轮询的问题,出现了长轮询方案。长轮询长轮询并不是一项新技术。只是服务端控制响应客户端请求的返回时间,减少客户端无效请求的一种优化方法。其实对于client来说,和shortpolling是不一样的。使用上没有本质区别。客户端发起请求后,服务端不会立即返回请求结果,而是会暂停一段时间请求。如果服务器数据在此期间发生变化,它会立即响应客户端请求。如果没有变化,会等到指定的超时一定时间后才响应请求,客户端重新发起长连接。第一次接触Nacos,为了方便后续的演示操作,在本地搭建了一个Nacos。注意:运行时有一个小坑。由于Nacos默认是集群启动的,而本地构建通常是单机模式,这里需要在启动脚本startup.X中手动更改启动模式。直接执行/bin/startup.X即可,默认用户密码为nacos。几个概念Nacos配置中心的几个核心概念:dataId、group、namespace,它们的层级关系如下:dataId:是配置中心最基本的单元,是一个key-value结构,key通常是我们的配置文件name,如:application.yml,mybatis.xml,value是整个文件的内容。目前支持JSON、XML、YAML等配置格式。group:dataId配置的分组管理。比如在同一个dev环境下开发,但是同一个环境的不同分支需要不同的配置数据。这种情况下可以使用组隔离,默认组为DEFAULT_GROUP。namespace:在项目开发过程中,肯定会有dev、test、pro等多个不同的环境。命名空间是为了隔离不同的环境。默认情况下,所有配置都是公开的。架构设计下图简单描述了nacos配置中心的架构流程。客户端和控制台通过发送Http请求将配置数据注册到服务器,服务器将数据持久化到Mysql。客户端拉取配置数据,批量设置dataId监听,发起长轮询请求。如果服务器配置项发生变化,它会立即响应请求。如果没有数据变化,请求会被暂停一段时间,直到达到超时时间。为了减轻服务器的压力,保证配置中心的可用性,拉取配置数据的客户端会在本地文件中保存一个快照,并先读取。这里我省略了更多的细节,比如认证、负载均衡、高可用设计(其实这部分真的很值得学习,后面会在另一篇文章中讲到),主要是为了理清客户端之间的数据交互和服务器模型。接下来我们分析Nacos2.0.1版本的源码。2.0之后的版本变化较多,与网上很多资料略有不同。地址:https://github.com/alibaba/nacos/releases/tag/2.0.1客户端源码分析Nacos配置中心的客户端源码在nacos-client项目中,其中NacosConfigService实现类是核心所有操作的入口点。说之前,先了解一下客户端的cacheMap数据结构。说到这里,大家应该记住了,因为它几乎贯穿了Nacos客户端的所有操作。由于多线程场景的存在,为保证数据一致性,cacheMap使用AtomicReference原子变量实现。/***groupKey->cacheData.*/privatefinalAtomicReference>cacheMap=newAtomicReference>(newHashMap<>());cacheMap是一个Map结构,key是groupKey,由dataId、group、tenant(租户)拼接而成的字符串;value是一个CacheData对象,每个dataId都会持有一个CacheData对象。获取配置Nacos获取配置数据的逻辑比较简单。首先,获取本地快照文件中的配置。如果本地文件不存在或者内容为空,则通过HTTP请求从远端拉取相应的dataId配置数据,保存在本地快照中,请求默认重试3次,超时时间为3s。获取配置有两个接口,getConfig()和getConfigAndSignListener(),但是getConfig()只是发送普通的HTTP请求,而getConfigAndSignListener()更多的操作是发起长轮询和注册监听dataId数据变化。addTenantListenersWithContent()。@OverridepublicStringgetConfig(StringdataId,Stringgroup,longtimeoutMs)throwsNacosException{returngetConfigInner(namespace,dataId,group,timeoutMs);}@OverridepublicStringgetConfigAndSignListener(StringdataId,Stringgroup,longtimeoutMs,Listenerlistener)throwsNacosException{Stringcontent=getConfig(dataId,group,timeoutMs);worker.addTenantListeners(dataId,group,content,Arrays.asList(listener));returncontent;}注册监听器客户端注册监听器,首先从cacheMap中获取dataId对应的CacheData对象。publicvoidaddTenantListenersWithContent(StringdataId,Stringgroup,Stringcontent,Listlisteners)throwsNacosException{group=blank2defaultGroup(group);Stringtenant=agent.getTenant();//1.获取dataId对应的CacheData,如果没有则向服务器发送一个long轮询请求获取配置CacheDatacache=addCacheDataIfAbsent(dataId,group,tenant);synchronized(cache){//2,为dataId注册数据变化监听cache.setContent(content);for(Listenerlistener:listeners){cache.addListener(listener);}cache.setSyncWithServer(false);agent.notifyListenConfig();}}如果没有,向服务器发起长轮询请求获取配置,默认Timeout时间为30s,将返回的配置数据回填到CacheData对象的内容字段,并使用该内容生成MD5值;然后通过addListener()注册监听器。CacheData也是出现频率非常高的一个类。我们可以看到,除了dataId、group、tenant、content等相关的基本属性外,还有几个比较重要的属性如:listeners、md5(md5是根据content的真实配置数据计算出来的值),以及因为注册监听,数据比对,服务端数据变化通知操作都在这里。其中listeners是为dataId注册的所有监听器的集合。ManagerListenerWrap对象除了持有Listener监听类外,还有一个lastCallMd5字段。这个属性很重要,是判断服务器数据是否发生变化的重要条件。添加监听器时,CacheData对象的最新md5值将赋值给ManagerListenerWrap对象的lastCallMd5属性。publicvoidaddListener(Listenerlistener){ManagerListenerWrapwrap=(listenerinstanceofAbstractConfigChangeListener)?newManagerListenerWrap(listener,md5,content):newManagerListenerWrap(listener,md5);}看到这对dataId的监听设置就搞定了吗?我们发现所有的操作都是围绕着cacheMap结构CacheData对象进行的,那么大胆猜测一定有专门的任务来处理这个数据结构。变更通知客户端如何感知服务器上的数据发生了变化?让我们从头来看。在NacosConfigService类的构造函数中初始化了一个ClientWorker,在ClientWorker类的构造函数中启动了一个线程池来轮询cacheMap。在executeConfigListen()方法中,有这么一段逻辑,检查cacheMap中dataId的CacheData对象,MD5字段和注册的监听监听器中的lastCallMd5值不一样,说明配置数据发生变化,触发safeNotifyListener方法发送数据更改通知。voidcheckListenerMd5(){for(ManagerListenerWrapwrap:listeners){if(!md5.equals(wrap.lastCallMd5)){safeNotifyListener(dataId,group,content,type,md5,encryptedDataKey,wrap);}}}safeNotifyListener()方法单独启动Thread,将更改后的数据内容推送给所有注册监听该dataId的客户端。客户端收到通知,直接实现receiveConfigInfo()方法接收回调数据,处理自己的业务。configService.addListener(dataId,group,newListener(){@OverridepublicvoidreceiveConfigInfo(StringconfigInfo){System.out.println("receive:"+configInfo);}@OverridepublicExecutorgetExecutor(){returnnull;}});为了更直观的理解,我使用testdemo演示下,获取服务器配置并设置监控。每当服务器配置数据发生变化时,客户端监控就会收到通知。一起来看看效果吧。publicstaticvoidmain(String[]args)throwsNacosException,InterruptedException{StringserverAddr="localhost";StringdataId="test";Stringgroup="DEFAULT_GROUP";Propertiesproperties=newProperties();properties.put("serverAddr",serverAddr);ConfigServiceconfigService=NacosFactory.createConfigService(属性);Stringcontent=configService.getConfig(dataId,group,5000);System.out.println(content);configService.addListener(dataId,group,newListener(){@OverridepublicvoidreceiveConfigInfo(StringconfigInfo){System.out.println("数据变化接收:"+configInfo);}@OverridepublicExecutorgetExecutor(){returnnull;}});booleanisPublishOk=configService.publishConfig(dataId,group,"我是新的配置内容~");System.out.println(isPublishOk);Thread.sleep(3000);content=configService.getConfig(dataId,group,5000);System.out.println(content);}结果如预期,当publishConfig数据变化到服务端,客户端可以马上察觉到,我惊呆了,用主动拉取的方式做出服务器的实时推送效果。数据变更接收:我是新的配置内容~true我是新的配置内容~服务端源码分析Nacos配置中心的服务端源码主要在nacos-config项目的ConfigController类中。服务端的逻辑比客户端稍微复杂一些。这里我们重点说一下。处理长轮询服务器提供的监听接口地址/v1/cs/configs/listener。这个方法内容不多。跟着doPollingConfig往下看。服务器根据请求头中的Long-Pulling-Timeout属性区分请求是长轮询还是短轮询。这里我们只关注长轮询部分,接下来看看addLongPollingClient()方法是如何处理客户端的长轮询请求的。普通客户端默认设置的请求超时时间是30s,但是这里我们发现服务端“偷偷”减去500ms,现在超时时间只有29.5s,为什么要这样呢?使用官方解释,所以需要提前500ms响应请求。为了最大程度保证客户端不会因为网络延迟而超时,考虑到请求在负载均衡时可能需要一些时间,毕竟Nacos本来就是按照阿里自己的业务量来设计的!此时将客户端提交的groupkey的MD5与服务器当前的MD5进行比较。如果md5值不一样,说明服务器上的配置项发生了变化。将groupkey放入changedGroupKeys集合中返回给客户端。如果MD5Util.compareMd5(req,rsp,clientMd5Map)没有变化,客户端请求会被挂起。该进程首先创建一个名为ClientLongPolling的调度任务Runnable,提交给调度器定时线程池延迟执行29.5s。ConfigExecutor.executeLongPolling(newClientLongPolling(asyncContext,clientMd5Map,ip,probeRequestSize,timeout,appName,tag));这里,每个长轮询任务都携带了一个asyncContext对象,这样每个请求都可以延迟响应,等待延迟到来或者配置改变后,调用asyncContext.complete()响应完成。asyncContext是Servlet3.0的新特性,异步处理,让Servlet线程不再需要一直阻塞,等待业务处理完再输出响应;容器分配给请求的线程和相关资源可以先释放,减轻系统负担,其响应会Delay,处理完业务或计算后再响应客户端。当ClientLongPolling任务提交到延迟线程池执行时,服务器会通过一个allSubs队列保存所有待处理的客户端长轮询请求任务。这是客户端注册和监控的过程。如果在延迟时间内客户端数据没有变化,则在延迟时间到后,将长轮询任务从allSubs队列中移除,响应请求响应,即取消监听。客户端收到响应后,再次发起长轮询,如此循环往复。处理长轮询,我们知道服务端是如何暂停客户端的长轮询请求的。一旦请求被挂起,用户通过管理平台操作配置项,或者服务端接收到其他客户端节点的配置修改。要求。如何立即取消相应挂起的任务,及时通知客户端数据变化?数据变更管理平台或客户端通过ConfigController中的publishConfig方法变更配置项连接位置。值得注意的是,在publishConfig接口中有这么一段逻辑,当某个dataId的配置数据被修改时,会触发一个数据变更事件Event。ConfigChangePublisher.notifyConfigChange(newConfigDataChangeEvent(false,dataId,group,tenant,time.getTime()));如果你仔细观察LongPollingService,你会发现在它的构造方法中,你只是订阅了数据变化事件,并在该事件触发时执行一个数据变化调度任务DataChangeTask。订阅数据变化事件DataChangeTask的主要逻辑是遍历allSubs队列。上面我们知道,这个队列维护着所有客户端的长轮询请求任务,并从这些任务中找到包含当前变化的groupkey的ClientLongPolling任务。将数据变化推送到客户端,并从allSubs队列中移除这个长轮询任务。DataChangeTask并且当我们查看对客户端的响应时,调用asyncContext.complete()来结束异步请求。结束语以上只是nacos配置中心的冰山一角。其实还有很多重要的技术细节没有提到。我建议你看看源代码。您无需通读源代码。只抓住核心部分。足够的。比如我之前对今天的题目不是很在意,突然被问到的时候我也不确定,于是我果断的看了源码,这样记忆就更深了(别人灌给你的知识是总是不如你咀嚼的东西有趣)。个人认为nacos的源码比较简单。代码不算太花哨,看起来比较简单。看源码不要有任何抗拒,就是别人写的业务代码而已,马马虎虎!