Nacos/nɑ:kəʊs/脱胎于阿里巴巴内部的 Config Server、VIPServer 和 Diamond,成长于多年双十一的洪峰考验,沉淀了简单易用、稳定可靠、性能卓越的核心竞争力。于 2018 年正式开源,其核心特性有:服务发现、动态配置管理 和 动态 DNS 服务。
笔者所在的云网络控制台团队使用 Nacos 来做配置管理,那么以 Nacos 为代表的配置中心究竟解决了哪些痛点问题呢?配置,作为代码如影随形的小伙伴,伴随着应用的整个生命周期,一般有三种形式:1) 硬编码,配置项通过类字段来承载;可以暴露 API 实现动态变更,但配置变更是发生在堆内存中的,没有持久化,一旦应用重启,配置项会回退到代码中的默认值;此外,如果有多个应用实例,自然需要逐一调用 API 变更配置,运维成本可想而知,极其蛋疼。2) 配置文件,配置项通过配置文件来承载,如 Spring 中的 properies、yaml 文件;配置文件解决了持久化的问题,但配置变更需要登录机器手动维护外部配置文件,然后重启应用,运维成本依然很高。3) 配置表,配置项保存在数据库中的配置表内;这种形式将配置从应用中抽离出来进行集中管理,可以有效地降低运维成本;但需要额外的定时任务拉取变更后的配置项,不够优雅。这么一合计,关于配置的痛点问题也就呼之欲出了,分别是:动态变更、持久化和运维成本。

笔者在 2017 年曾参与集团委派的 Prometheus 调研项目,在一次赴京汇报中,一位博士大佬问我:Prometheus Server 与一众 Exporter 是如何进行数据交互的?我回答道:Exporter 会不断拉取 Prometheus Server 中的 metrics;接着大佬追问:为什么选用拉模式,拉模式与推模式有何区别呢?当时回答的比较片面。的确,数据交互有两种模式:Push (推模式) 和 Pull (拉模式)。推模式指的是客户端与服务端建立长链接后,服务端向客户端推送数据;这种方式实时性高,但如果客户端的数据消费能力较弱,而且没有相应地反压 (Back Pressure) 机制,那十有八九会导致客户端数据积压。拉模式指的是客户端主动向服务端发起数据拉取的请求;其优点是客户端将拉取的节奏掌握自己手中,不会导致数据积压,但实时性会比较差。
无论是在 Nacos 1.X 亦或是 2.X 中,Config Server 与 Config Client 针对动态配置项的交互模型均是基于Pull模式的。但在 1.X 版本中,Config Client 并没有与 Config Server 建立所谓的长链接 (Long Connection),而是通过 长轮询 (Long Polling) 来模拟长链接。接下来,让我们一起来简单地学习下长轮询在 Nacos 动态配置管理中的落地思路。
1 长轮询
在长轮询中,当服务端收到客户端的请求后,服务端会一直挂起链接 (Connection),直到服务端有可以响应给客户端的数据,才会关闭链接,然后客户端再次发起请求,周而复始 ··· 废话不多说,上车,噢不,上图!

2 Asynchronous Servlet
在 Servlet 3.0 之前,Servlet 完全遵循同步阻塞I/O模型,这意味着一个 HTTP 请求对应一个 Servlet Container 线程,即每一个 HTTP 请求都是在各自线程上下文中完成处理的,这也正是ThreadLocal的用武之地。Servlet 3.0 引入了一种异步处理请求的能力,即Asynchronous Servlet。为什么要有 Asynchronous Servlet 呢?Tomcat 线程池默认最多有 200 个工作线程,由于每一个线程都会消耗服务器的硬件资源,也就是说线程数量肯定是有上限的,一旦业务逻辑又臭又长,那么在并发请求较多的场景,必然会出现线程池中已经没有多余的线程来处理 HTTP 请求的情形,因为线程都在忙,没有办法返回线程池中,这就是经典的线程饥饿问题。怎么办呢?Asynchronous Servlet 的核心思想是将 Servlet Container 中的工作线程与复杂耗时的业务逻辑相解耦,试图通过另一个线程池来承载这些耗时逻辑,使得工作线程可以快速地返回到 Servlet Container 线程池中,以处理新的 HTTP 请求。尽管工作线程已经返回到 Servlet Container 线程池中,但 Servlet Container 并不会断开与客户端的链接,否则怎么给客户端响应数据啊,它会一直挂起客户端链接,最终由 Servlet Container 之外的业务线程池中的线程来响应客户端。
Asynchronous Servlet 用起来很简单,首先通过 HttpServletRequest 的startAsync()方法初始化AsyncContext实例,同时标志着当前 Servlet Container 中的工作线程会尽早返回到线程池中;然后将 AsyncContext 实例传递到业务线程池中的线程;最后,当复杂的计算逻辑完成后,业务线程池中的线程调用 AsyncContext 的complete()方法实现对客户端的响应。
虽然 Servlet 3.0 对请求的处理是异步的,但是面向
ServletInputStream和ServletOutputStream的 I/O 操作却依然是阻塞的,想象一下,如果请求体的体量很大,Servlet Container 中的工作线程必然面临不必要的等待,这是另一个影响工作线程尽早返回到 Servlet Container 线程池的因素。为了解决这个问题,Servlet 3.1 又引入了Non-blocking I/O,主要是向 ServletInputStream 和 ServletOutputStream 中注册ReadListener、WriteListener回调接口。
笔者总感觉 Asynchronous Servlet 思路有点怪怪的,难道重新开了一个业务线程就不会造成业务线程的饥饿问题吗?此外,Asynchronous Servlet 与基于长轮询的客户端 Pull 方案简直绝配啊,有没有?
3 Nacos 配置管理之长轮询源码解读
Nacos 2.X 针对动态配置的交互方案进行了升级,通过 GRPC 来真正地实现长链接,进一步压榨通信效率。因此,笔者这里只能基于 Nacos 1.X 进行源码解读。首先,查看一下标签信息,然后基于 1.4.3 标签创建一同名本地分支。
git tag --list
git checkout -b 1.4.3 1.4.3 (git branch 1.4.3 1.4.3)
3.1 跑起来
理解开源项目源码最快的方式无疑是跑起来,那么如何在 Intellij Idea 中单独将 nacos-config 模块跑起来呢?
3.1.1 配置启动参数
-Dnacos.standalone=true -Dnacos.functionMode=config
3.1.2 改造 AuthConfig
AuthConfig 位于 nacos-core 模块中,将其头顶上的@Configuration注解拿掉即可。
3.1.3 移除 Spring Security 依赖
nacos-config 模块依赖于 nacos-core 模块,而 nacos-core 模块引入了 Spring Security,去掉它可以方便咱们调试。
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>nacos-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</exclusion>
</exclusions>
</dependency>
3.1.4 追加 application.properties 配置文件
在 nacos-config 模块中追加 application.properties 配置文件,内容如下:
server.servlet.contextPath=/nacos
server.port=8848
启动吧~
3.2 Config Client 如何发起长轮询
如果希望 Config Client 动态拉取到 Config Server 中的变更配置项,则需要向 Config Client 注册监听器 (Listener) 以用于回调。示例如下:
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
properties.put("namespace", tenant);
ConfigService configService = NacosFactory.createConfigService(properties);
configService.addListener(dataId, group, new AbstractListener() {
@Override
public void receiveConfigInfo(String configInfo) {}
});
Nacos长轮询机制解析

本文深入剖析了Nacos配置中心如何利用长轮询机制实现客户端与服务端之间的高效通信,重点介绍了客户端如何发起长轮询请求及服务端如何处理这类请求。
最低0.47元/天 解锁文章
1689

被折叠的 条评论
为什么被折叠?



