配置中心的长轮询

配置中心的长轮询

配置中心是为了解决传统静态配置修改重启应用的问题,Nacos 和 Apollo 都是通过长轮询来实现动态推送的,而不是长连接

推拉模式

数据交互有两种模式:【推模式 push】和【拉模式 pull】

  • 推模式是客户端和服务器建立好长链接,服务器的数据推送到客户端
    • 优点,及时,一旦有数据变更,客户端立马能感知到
    • 缺点,不知道客户端消费能力,可能导致数据堆积
  • 拉模式是客户端主动向服务器发起请求

长轮询与轮询

长轮询和轮询都是拉模式实现的

  • 轮询是指不管服务数据有无更新,客户端每隔顶长时间去请求一次数据

如果配置中心用【轮询】推送,会有以下问题

  • 推送延迟,客户端每隔 5s 拉取一次配置,如果配置变更发生在第 6s,推送的延迟就是 4s
  • 服务端压力,因为配置一般不会发生变化,频繁轮询会造成服务端压力
  • 降低轮询间隔,延迟降低,压力增加,反之亦然

【长轮询】是客户端发起请求,如果服务端数据没有变更,会 hold 住请求,直到服务端数据发生变化,或者等待一定时间超时返回。客户端再次发起下次请求

  • 服务端数据发生变更之后,长轮询结束,立刻返回响应
  • 长轮询间隔一般很长 30s 60s,并且服务端 hold 住连接,不会消耗太多资源

为什么要等待一定时间超时,而不是一直 hold 请求?

  • 连接稳定性,长轮询本质也是 TCP 连接,仅仅依靠 TCP 层很难保证可用性
  • 用户可能随时新增配置监听,所以要在下一次长轮询中加入

配置中心长轮询设计

  1. 客户端发起长轮询,客户端发起一个 http 请求,包含配置中心地址,以及监听的 dataId(定位配置的唯一键)
  2. 服务端监听数据变化,服务端维护 dataId 和长轮询映射关系,如果配置发生变化,服务端会找到对应的连接,在响应体里填入更新的配置内容,如果超时则返回 304
  3. 客户端接收响应,看是 200 还是 304

配置中心在实现长轮询的时候不应该阻塞 Tomcat 业务线程,所以一般采用异步响应方式实现

代码实现

服务端

1
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
// 配置监听接入点
@RequestMapping("/listener")
public void addListener(HttpServletRequest request, HttpServletResponse response) {

String dataId = request.getParameter("dataId");

// 开启异步
AsyncContext asyncContext = request.startAsync(request, response);
AsyncTask asyncTask = new AsyncTask(asyncContext, true);

// 维护 dataId 和异步请求上下文的关联
dataIdContext.put(dataId, asyncTask);

// 启动定时器,30s 后写入 304 响应
timeoutChecker.schedule(() -> {
if (asyncTask.isTimeout()) {
dataIdContext.remove(dataId, asyncTask);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
asyncContext.complete();
}
}, 30000, TimeUnit.MILLISECONDS);
}

// 配置发布接入点
@RequestMapping("/publishConfig")
@SneakyThrows
public String publishConfig(String dataId, String configInfo) {
log.info("publish configInfo dataId: [{}], configInfo: {}", dataId, configInfo);
Collection<AsyncTask> asyncTasks = dataIdContext.removeAll(dataId);
for (AsyncTask asyncTask : asyncTasks) {
asyncTask.setTimeout(false);
HttpServletResponse response = (HttpServletResponse)asyncTask.getAsyncContext().getResponse();
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(configInfo);
asyncTask.getAsyncContext().complete();
}
return "success";
}

长轮询请求 /listener 的时候设置定时器,30s 后写入 304 响应

如果在其中配置变了 /publishConfig 写入变更,并且取消定时任务


配置中心的长轮询
http://showyoubug.cn/2024/07/23/配置中心的长轮询/
作者
Dong Su
发布于
2024年7月23日
许可协议