配置中心的长轮询
配置中心是为了解决传统静态配置修改重启应用的问题,Nacos 和 Apollo 都是通过长轮询来实现动态推送的,而不是长连接
推拉模式
数据交互有两种模式:【推模式 push】和【拉模式 pull】
- 推模式是客户端和服务器建立好长链接,服务器的数据推送到客户端
- 优点,及时,一旦有数据变更,客户端立马能感知到
- 缺点,不知道客户端消费能力,可能导致数据堆积
- 拉模式是客户端主动向服务器发起请求
长轮询与轮询
长轮询和轮询都是拉模式实现的
- 轮询是指不管服务数据有无更新,客户端每隔顶长时间去请求一次数据
如果配置中心用【轮询】推送,会有以下问题
- 推送延迟,客户端每隔 5s 拉取一次配置,如果配置变更发生在第 6s,推送的延迟就是 4s
- 服务端压力,因为配置一般不会发生变化,频繁轮询会造成服务端压力
- 降低轮询间隔,延迟降低,压力增加,反之亦然
【长轮询】是客户端发起请求,如果服务端数据没有变更,会 hold 住请求,直到服务端数据发生变化,或者等待一定时间超时返回。客户端再次发起下次请求
- 服务端数据发生变更之后,长轮询结束,立刻返回响应
- 长轮询间隔一般很长 30s 60s,并且服务端 hold 住连接,不会消耗太多资源
为什么要等待一定时间超时,而不是一直 hold 请求?
- 连接稳定性,长轮询本质也是 TCP 连接,仅仅依靠 TCP 层很难保证可用性
- 用户可能随时新增配置监听,所以要在下一次长轮询中加入
配置中心长轮询设计
- 客户端发起长轮询,客户端发起一个 http 请求,包含配置中心地址,以及监听的 dataId(定位配置的唯一键)
- 服务端监听数据变化,服务端维护 dataId 和长轮询映射关系,如果配置发生变化,服务端会找到对应的连接,在响应体里填入更新的配置内容,如果超时则返回 304
- 客户端接收响应,看是 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);
dataIdContext.put(dataId, asyncTask);
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
写入变更,并且取消定时任务