「源码学习」Nacos配置中心客户端实现原理 上篇文章已通过学习 nacos-client 源码,知道了Nacos客户端是通过长轮询的方式从Nacos服务端获取配置信息,那么Nacos服务端又是如何处理客户端的长轮询请求和通知配置变更事件呢?
长轮询处理
服务端与客户端长轮询交互的主要流程是
- 判断是不是长轮询,检查配置是否发生变化
- 如果有变化,马上返回响应,客户端收到响应,完成请求。
- 如果无变化,检查客户端是否要求马上返回。
- 如果是,马上返回响应,客户端收到响应,完成请求。
- 如果不是,启动线程等待客户端指定的超时时间返回。
- 服务端返回响应,客户端收到,完成请求,随后客户端进入下一次长轮询。
先看一下源码中长轮询处理的方法流程
- 从客户端发起长轮询请求任务开始,入口便是监听接口地址:/v1/cs/configs/listener,位于com.alibaba.nacos.config.server.controller.ConfigController 类。
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, signType = SignType.CONFIG)
public void listener(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
String probeModify = request.getParameter("Listening-Configs");
if (StringUtils.isBlank(probeModify)) {
LOGGER.warn("invalid probeModify is blank");
throw new IllegalArgumentException("invalid probeModify");
}
probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
Map<String, String> clientMd5Map;
try {
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
// do long-polling
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
- 根据请求header中的 Long-Pulling-Timeout 区分是不是长轮询请求。
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
// Long polling.
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
}
- 客户端设置的超时时间是30s,但是Nacos服务端为了保证客户端不会因网络延迟造成超时,提前了0.5s响应请求,所以实际的超时时间是29.5s。
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
- 对比客户端请求的配置的DM5和服务端当前的MD5的值是否一致,如果不一致,则已发生变更,直接响应客户端。
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
- 如果未发生变更,则将请求挂起,创建 ClientLongPolling 线程。
// Must be called by http thread, or send response.
final AsyncContext asyncContext = req.startAsync();
// AsyncContext.setTimeout() is incorrect, Control by oneself
asyncContext.setTimeout(0L);
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
- 在 ClientLongPolling 线程中启动 ConfigExecutor.scheduleLongPolling 延时任务,线程延时29.5s后执行。
public void run() {
// 1. 创建调度任务,延时时间为29.5s
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(() -> {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// 3.调度任务结束后,从allSubs中移除
boolean removeFlag = allSubs.remove(ClientLongPolling.this);
if (removeFlag) {
if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
//4.判断配置是否变更,返回响应给客户端
List<String> changedGroups = MD5Util.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} else {
LogUtil.DEFAULT_LOG.warn("client subsciber's relations delete fail.");
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}
}, timeoutTime, TimeUnit.MILLISECONDS);
//2.将当前ClientLongPolling加入allSubs
allSubs.add(this);
}
在处理长轮询中,有一个AsyncContext对象,它发挥了什么作用呢?
AsyncContext 为 Servlet 3.0新增的特性,异步处理,使Servlet线程不再需要一直阻塞,等待业务处理完毕才输出响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。
配置变更事件通知
从上面已了解到了服务端如何挂起客户端的长轮询请求,那么服务端又是如何在配置变更的时候,通知挂起的请求呢?
先看一源码中方法调用流程
- 客户端通过调用ConfigController中的publishConfig方法实现更新配置,当某个dataId配置变更时,会触发ConfigDataChangeEvent 事件。
ConfigChangePublisher.notifyConfigChange(
new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
- 在 LongPollingService 初始化的时候,在构造器里面,订阅了 LocalDataChangeEvent 数据变更事件。
// Register LocalDataChangeEvent to NotifyCenter.
NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
// Register A Subscriber to subscribe LocalDataChangeEvent.
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// Ignore.
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
- 在订阅变更事件中,创建了 DataChangeTask 数据变更线程任务。allSubs中是所有挂起的客户端的长轮询请求任务,DataChangeTask 内主要的任务是迭代allSubs队列,从这些任务中,检索出变更过的groupKey的ClientLongPolling 任务,将响应数据返回给客户端。
ConfigCacheService.getContentBetaMd5(groupKey);
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// If published tag is not in the beta list, then it skipped.
if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
continue;
}
// If published tag is not in the tag list, then it skipped.
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // Delete subscribers' relationships.
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
RequestUtil
.getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
"polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
- 最后,在generateResponse方法中,调用asyncContext.complete(),结束异步请求,完成本次长轮询。
void generateResponse(List<String> changedGroups) {
if (null == changedGroups) {
// Tell web container to send http response.
asyncContext.complete();
return;
}
}
最后贴一下长轮询处理与配置变更事件通知的交互图
如果文章中有写的不对的地方,感谢大家指出....