共计 8494 个字符,预计需要花费 22 分钟才能阅读完成。
本文案例收录在 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()
。
@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 这些相干的根底属性,还有几个比拟重要的属性如: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!
我是小富~,如果对你有用 在看 、 关注 反对下,咱们下期见~
整顿了几百本各类技术电子书,有须要的同学自取。技术群快满了,想进的同学能够加我好友,和大佬们一起吹吹技术。
电子书地址
集体公众号:程序员内点事,欢送交换