Alibaba Nacos配置中心动态感知原理
# 前言
Nacos提供两大核心功能,服务注册发现,配置中心。对应Nacos的架构图,分别是Naming Service和Config Service,其中Config Service是实现配置中心的核心模块。实现了版本管理、灰度发布、监听管理、推送轨迹等功能。针对配置中心,当我们通过控制台或API修改配置之后,客户端能动态获取到修改后的配置,那么配置中心是如何实现动态感知的呢?
# 动态监听之Pull和Push
当Nacos Config Server上的配置发生变化时,需要让相关的应用程序感知到配置的变化,这就需要客户端对感兴趣的配置实现监听。那么客户端是如何实现配置变更实时更新的呢?
一般来说,客户端与服务端的交互无非两种:Pull模式和Push模式,一个是客户端主动拉取,一个是服务端主动推送。
Pull模式:服务端和客户端之间需要维护长连接,客户端多的情况下耗内存、需要心跳机制检测连接状态。
Push模式:客户端需要定时拉取数据,不能保证实时性,服务端长时间不更新的情况下,定时任务为无效更新,浪费资源。
# Nacos的Pull模式
Nacos采用的是Pull模式,不过不是简单的Pull,而是一种长轮询机制。结合Pull和Push两者的优势,客户端采用长轮询的方式发起Pull请求,检查服务配置消息是否发生变化,如果更新,客户端会根据变更的内容获取最新配置信息。
所谓的长轮询,就是客户端发起Pull请求之后,服务端如果发生配置变更则立即返回,如果服务端和客户端配置是保持一致的,那么服务端会“Hold”住这个请求,在指定时间内不返回结果,直到这段时间内配置发生变化。这个长连接默认超时时间是30s。
服务端收到请求后,先检查配置是否发生变化,如果没变化,则设置一个定时任务,延期29.5s执行,并且把当前的客户端长轮询连接加入allSubs队列。这里有两种方式触发连接返回。
- 等待29.5s自动触发检查机制,无论是否发生变化,都会返回。
- 在29.5s内,通过Nacos控制台或者API的形式对配置进行了修改,会触发
ConfigDataChangeEvent
事件。
# Nacos的动态感知
前面我们已经知道客户端通过长轮询请求来获取配置变更,但是定时任务是延迟执行的,那并没有达到实时的目的,当通过控制台或者API修改配置时,那Nacos是如何实时动态更新的呢?
# LongPollingService 监听事件类
LongPollingService
继承AbstractEventListener
,AbstractEventListener
是事件抽象类,它有一个onEvent
抽象方法,而LongPollingService
实现了这个方法
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// ignore
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
2
3
4
5
6
7
8
9
10
11
LongPollingService
可以看到LocalDataChangeEvent
事件,这个事件是服务端的配置数据发生变化时发布的一个事件。onEvent
方法中通过线程池来执行一个DataChangeTask
任务。
# DataChangeTask线程
DataChangeTask
是一个线程,实现了Runnable
接口,对应的run()
如下:
class DataChangeTask implements Runnable {
@Override
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// 如果beta发布且不在beta列表直接跳过
if (isBeta && !betaIps.contains(clientSub.ip)) {
continue;
}
// 如果tag发布且不在tag列表直接跳过
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // 删除订阅关系
LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
(System.currentTimeMillis() - changeTime),
"in-advance",
RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
"polling",
clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
}
}
DataChangeTask(String groupKey) {
this(groupKey, false, null);
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {
this(groupKey, isBeta, betaIps, null);
}
DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {
this.groupKey = groupKey;
this.isBeta = isBeta;
this.betaIps = betaIps;
this.tag = tag;
}
final String groupKey;
final long changeTime = System.currentTimeMillis();
final boolean isBeta;
final List<String> betaIps;
final String tag;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- 遍历allSubs中的客户端长轮询请求。
- 比较每个客户端长轮询请求携带的groupKey,如果服务端变更的配置和客户端请求关注的配置一致,则直接返回,这里调用
clientSub.sendResponse()
方法返回。
# 总结
好了,最后整理下nacos实时动态感知的流程如下:
- 客户端通过长轮询的方式发起Pull请求服务端获取配置变更;
- 服务端判断如果是长轮询请求,对比数据的MD5,如果发生变化则直接返回,否则通过延迟任务执行
ClientLongPolling
线程; - 配置中心修改配置后,会发布
ConfigDataChangeEvent
事件; EventDispatcher
触发事件,通知监听者。LongPollingService
就是监听者之一。- 监听者通过线程池开启定时线程,遍历客户端的所有长轮询的请求, 通过groupKey匹配到对应请求,直接返回。