本文案例收录在 https://github.com/chengxy-nd...
大家好,我是小富~
对于Nacos
大家应该都不太生疏,出身阿里名声在外,能做动静服务发现、配置管理,十分好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在应用层面,那面试可能要吃大亏。
比方咱们明天要探讨的话题,Nacos
在做配置核心的时候,配置数据的交互模式是服务端推过来还是客户端被动拉的?
这里我先抛出答案:客户端被动拉的!
接下来咱们扒一扒Nacos
的源码,来看看它具体是如何实现的?
配置核心
聊Nacos
之前简略回顾下配置核心的由来。
简略了解配置核心的作用就是对配置对立治理,批改配置后利用能够动静感知,而无需重启。
因为在传统我的项目中,大多都采纳动态配置的形式,也就是把配置信息都写在利用内的yml
或properties
这类文件中,如果要想批改某个配置,通常要重启利用才能够失效。
但有些场景下,比方咱们想要在利用运行时,通过批改某个配置项,实时的管制某一个性能的开闭,频繁的重启利用必定是不能承受的。
尤其是在微服务架构下,咱们的应用服务拆分的粒度很细,少则几十多则上百个服务,每个服务都会有一些本人特有或通用的配置。如果此时要扭转通用配置,难道要我挨个改几百个服务配置?很显然这不可能。所以为了解决此类问题配置核心应运而生。
推与拉模型
客户端与配置核心的数据交互方式其实无非就两种,要么推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
配置核心的几个外围概念:dataId
、group
、namespace
,它们的层级关系如下图:
dataId
:是配置中心里最根底的单元,它是一种key-value
构造,key
通常是咱们的配置文件名称,比方:application.yml
、mybatis.xml
,而value
是整个文件下的内容。
目前反对JSON
、XML
、YAML
等多种配置格局。
group
:dataId配置的分组治理,比方同在dev环境下开发,但同环境不同分支须要不同的配置数据,这时就能够用分组隔离,默认分组DEFAULT_GROUP
。
namespace
:我的项目开发过程中必定会有dev
、test
、pro
等多个不同环境,namespace
则是对不同环境进行隔离,默认所有配置都在public
里。
架构设计
下图简要形容了nacos
配置核心的架构流程。
客户端、控制台通过发送Http申请将配置数据注册到服务端,服务端长久化数据到Mysql。
客户端拉取配置数据,并批量设置对dataId
的监听发动长轮询申请,如服务端配置项变更立刻响应申请,如无数据变更则将申请挂起一段时间,直到达到超时工夫。为缩小对服务端压力以及保障配置核心可用性,拉取到配置数据客户端会保留一份快照在本地文件中,优先读取。
这里我省略了比拟多的细节,如鉴权、负载平衡、高可用方面的设计(其实这部分才是真正值得学的,后边另出文讲吧),次要弄清客户端与服务端的数据交互模式。
下边咱们以Nacos 2.0.1版本源码剖析,2.0当前的版本改变较多,和网上的很多材料略有些不同
地址:https://github.com/alibaba/na...
客户端源码剖析
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()
。
@Overridepublic String getConfig(String dataId, String group, long timeoutMs) throws NacosException { return getConfigInner(namespace, dataId, group, timeoutMs);}@Overridepublic 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这些相干的根底属性,还有几个比拟重要的属性如:listeners
、md5
(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
队列,上边咱们晓得,这个队列中保护的是所有客户端的长轮询申请工作,从这些工作中找到蕴含以后产生变更的groupkey
的ClientLongPolling
工作,以此实现数据更变推送给客户端,并从allSubs
队列中剔除此长轮询工作。
而咱们在看给客户端响应response
时,调用asyncContext.complete()
完结了异步申请。
结束语
上边只揭开了nacos
配置核心的冰山一角,实际上还有十分多重要的技术细节都没提及到,倡议大家没事看看源码,源码不须要通篇的看,只有抓住外围局部就够了。就比方明天这个题目以前我真没太在意,忽然被问一下子吃不准了,果决看下源码,而且这样记忆比拟粗浅(他人嚼碎了喂你的常识总是比本人咀嚼的差那么点意思)。
nacos
的源码我集体感觉还是比拟奢侈的,代码并没有过多炫技,看起来绝对轻松。大家不要对看源码有什么冲突,它也不过是他人写的业务代码而已,just so so!
我是小富~,如果对你有用在看、关注反对下,咱们下期见~
整顿了几百本各类技术电子书,有须要的同学自取。技术群快满了,想进的同学能够加我好友,和大佬们一起吹吹技术。
电子书地址
集体公众号: 程序员内点事,欢送交换