🏆【Alibaba中间件技术系列】「Nacos技术专题」配置中心加载原理和配置实时更新原理分析(中)

1,253 阅读8分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

官方资源

nacos.io/zh-cn/docs/…

带着问题去思考

  • 客户端长轮询的响应时间会受什么影响
  • 为什么更改了配置信息后客户端会立即得到响应
  • 客户端的超时时间为什么要设置为30s
  • 带着以上这些问题我们从服务端的代码中去探寻结论。

Nacos之配置中心

  • 动态配置管理是 Nacos的三大功能之一,通过动态配置服务,可以在所有环境中以集中和动态的方式管理所有应用程序或服务的配置信息。
  • 动态配置中心可以实现配置更新时无需重新部署应用程序和服务即可使相应的配置信息生效,这极大了增加了系统的运维能力。

动态配置

Nacos的动态配置的能力,看看 Nacos是如何以简单、优雅、高效的方式管理配置,实现配置的动态变更的,接下来来了解下 Nacos 的动态配置的功能。

客户端动态化配置机制

Nacos 的客户端维护了一个长轮询的任务,去检查服务端的配置信息是否发生变更,如果发生了变更,那么客户端会拿到变更的 groupKey 再根据 groupKey 去获取配置项的最新值即可。

客户端去发请求,询问服务端我所关注的配置项有没有发生变更,如果间隔时间设置的太长的话有可能无法及时获取服务端的变更,如果间隔时间设置的太短的话,那么频繁的请求对于服务端来说无疑也是一种负担。

如果客户端每隔一段长度适中的时间去服务端请求,而在这期间如果配置发生变更,服务端能够主动将变更后的结果推送给客户端,这样既能保证客户端能够实时感知到配置的变化,也降低了服务端的压力。

客户端长轮询

客户端长轮询的部分,也就是LongPollingRunnable中的checkUpdateDataIds 方法,该方法就是用来访问服务端的配置是否发生变更的,该方法最终会调用如下图所示的方法:

http请求操作

客户端是通过一个http的post 请求去获取服务端的结果的,并且设置了一个超时时间:30s。一般来讲:客户端足足等了29.5+s,才请求到服务端的结果,然后客户端得到服务端的结果之后,再做一些后续的操作,全部都执行完毕之后,在 finally 中又重新调用了自身,也就是说这个过程是一直循环下去的。

长轮询执行逻辑

客户端向服务端发起一次请求,最少要29.5s才能得到结果,当然啦,这是在配置没有发生变化的情况下。如果客户端在长轮询时配置发生变更的话,该请求需要多长时间才会返回呢,在客户端长轮询时修改配置。

未获得到修改数据的操作触发返回

获得到了修改数据操作立刻触发返回

服务端controller

上面说到了客户端发送的 http 请求中可以知道,请求的是服务端的 /v1/cs/configs/listener 这个接口,com.alibaba.nacos.config.server.controller.ConfigController.java,在 ConfigController 类中,如下图所示:

服务端是通过springMVC对外提供的 http 服务,对 HttpServletRequest 中的参数进行转换后,然后交给一个叫 inner 的对象去执行。inner 对象是 ConfigServletInner 类的实例,com.alibaba.nacos.config.server.controller.ConfigServletInner.java

该方法是一个轮询的接口,除了支持长轮询外还支持短轮询的逻辑。再次进入 longPollingService 的 addLongPollingClient 方法,如下图所示:

com.alibaba.nacos.config.server.service.LongPollingService.java

该方法主要是将客户端的长轮询请求添加到某个东西中去:服务端将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行。

服务端拿到客户端提交的超时时间后,又减去了 500ms 也就是说服务端在这里使用了一个比客户端提交的时间少 500ms 的超时时间,也就是 29.5s,看到这个 29.5s 我们应该有点兴奋了。

PS:这里的 timeout 不一定一直是 29.5,当 isFixedPolling() 方法为 true 时,timeout 将会是一个固定的间隔时间,这里为了描述简单就直接用 29.5 来进行说明。

接下来我们来看服务端封装的 ClientLongPolling 的任务到底执行的什么操作,如下图所示:

com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling.java

ClientLongPolling 被提交给 scheduler 执行之后,实际执行的内容可以拆分成以下四个步骤:

  1. 创建一个调度的任务,调度的延时时间为 29.5s。
  2. 将该 ClientLongPolling 自身的实例添加到一个 allSubs 中去。
  3. 延时时间到了之后,首先将该 ClientLongPolling 自身的实例从 allSubs 中移除。
  4. 获取服务端中保存的对应客户端请求的 groupKeys 是否发生变更,将结果写入 response 返回给客户端。

allSubs 对象,该对象是一个 ConcurrentLinkedQueue 队列,ClientLongPolling 将自身添加到队列中。

调度任务

服务端对客户端提交上来的 groupKey 进行检查,如果发现某一个 groupKey 的 md5 值还不是最新的,则说明客户端的配置项还没发生变更,所以将该 groupKey 放到一个 changedGroupKeys 列表中,最后将该 changedGroupKeys 返回给客户端。对于客户端来说,只要拿到 changedGroupKeys 即可。

服务端数据变更

服务端直到调度任务的延时时间到了之前,ClientLongPolling 都不会有其他的任务可做,所以在这段时间内,该 allSubs 队列肯定有事情需要进行处理。

在客户端长轮询期间,更改了配置之后,客户端能够立即得到响应。

服务端数据变更接口

调用的请求,可以很容易的找到该请求对应的 url为:/v1/cs/configs 并且是一个 POST 请求,具体的方法是 ConfigController 中的 publishConfig 方法,如下图所示:

修改配置后,服务端首先将配置的值进行了持久化层的更新,然后触发了一个 ConfigDataChangeEvent 的事件,fireEvent 的方法:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.java

fireEvent 方法实际上是触发的 AbstractEventListener 的 onEvent 方法,而所有的 listener 是保存在一个叫 listeners 对象中的。

被触发的 AbstractEventListener 对象则是通过 addEventListener 方法添加到 listeners 中的,找到 addEventListener 方法在何处被调用的,就知道有哪些 AbstractEventListener 需要被触发 onEvent 回调方法了。

可以找到是在 AbstractEventListener 类的构造方法中,将自身注册进去了,如下图所示:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener.java

可以看到 AbstractEventListener 所有的子类中LongPollingService。当我们从 dashboard 中更新了配置项之后,实际会调用到 LongPollingService 的 onEvent 方法。

回到 LongPollingService 中,查看一下 onEvent 方法,如下图所示:

com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask.java

发现当触发了 LongPollingService 的 onEvent 方法时,实际是执行了一个叫 DataChangeTask 的任务,应该是通过该任务来通知客户端服务端的数据已经发生了变更,我们进入 DataChangeTask 中看下具体的代码,如下图所示:

遍历 allSubs 的队列

遍历 allSubs 的队列,该队列中维持的是所有客户端的请求任务,需要找到与当前发生变更的配置项的 groupKey 相等的 ClientLongPolling 任务

往客户端写响应数据

丢i与ClientLongPolling 任务后,只需要将发生变更的 groupKey 通过该 ClientLongPolling 写入到响应对象中,就完成了一次数据变更的 “推送” 操作了

如果 DataChangeTask 任务完成了数据的 “推送” 之后,需要将原来等待执行的调度任务取消掉了,这样就防止了推送操作写完响应数据之后,调度任务又去写响应数据。

可以从 sendResponse 方法中看到,确实是这样做的:

http请求本来就是无状态的,所以没必要也不能将超时时间设置的太长,这样是对资源的一种浪费。

与此同时服务端也将该请求封装成一个调度任务去执行,等待调度的期间就是等待 DataChangeTask 主动触发的,如果延迟时间到了 DataChangeTask 还未触发的话,则调度任务开始执行数据变更的检查,然后将检查的结果写入响应对象,如下图所示:

总结结论:

  1. Nacos 客户端会循环请求服务端变更的数据,并且超时时间设置为30s,当配置发生变化时,请求的响应会立即返回,否则会一直等到 29.5s+ 之后再返回响应
  2. Nacos 客户端能够实时感知到服务端配置发生了变化。
  3. 实时感知是建立在客户端拉和服务端“推”的基础上,但是这里的服务端“推”需要打上引号,因为服务端和客户端直接本质上还是通过 http 进行数据通讯的,之所以有“推”的感觉,是因为服务端主动将变更后的数据通过 http 的 response 对象提前写入了。