「源码学习」Nacos配置中心服务端实现原理

205 阅读4分钟

「源码学习」Nacos配置中心客户端实现原理 上篇文章已通过学习 nacos-client 源码,知道了Nacos客户端是通过长轮询的方式从Nacos服务端获取配置信息,那么Nacos服务端又是如何处理客户端的长轮询请求和通知配置变更事件呢?

源码地址:github.com/alibaba/nac…

长轮询处理

服务端与客户端长轮询交互的主要流程是

  • 判断是不是长轮询,检查配置是否发生变化
  • 如果有变化,马上返回响应,客户端收到响应,完成请求。
  • 如果无变化,检查客户端是否要求马上返回。
  • 如果是,马上返回响应,客户端收到响应,完成请求。
  • 如果不是,启动线程等待客户端指定的超时时间返回。
  • 服务端返回响应,客户端收到,完成请求,随后客户端进入下一次长轮询。

先看一下源码中长轮询处理的方法流程

  1. 从客户端发起长轮询请求任务开始,入口便是监听接口地址:/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());
    }
  1. 根据请求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 + "";
        }
}
  1. 客户端设置的超时时间是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);
  1. 对比客户端请求的配置的DM5和服务端当前的MD5的值是否一致,如果不一致,则已发生变更,直接响应客户端。
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
  1. 如果未发生变更,则将请求挂起,创建 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));
  1. 在 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线程不再需要一直阻塞,等待业务处理完毕才输出响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。

配置变更事件通知

从上面已了解到了服务端如何挂起客户端的长轮询请求,那么服务端又是如何在配置变更的时候,通知挂起的请求呢?

先看一源码中方法调用流程

  1. 客户端通过调用ConfigController中的publishConfig方法实现更新配置,当某个dataId配置变更时,会触发ConfigDataChangeEvent 事件。
ConfigChangePublisher.notifyConfigChange(
                    new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
  1. 在 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;
            }
        });
  1. 在订阅变更事件中,创建了 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));
                    }
                }
  1. 最后,在generateResponse方法中,调用asyncContext.complete(),结束异步请求,完成本次长轮询。
void generateResponse(List<String> changedGroups) {
            if (null == changedGroups) {
                
                // Tell web container to send http response.
                asyncContext.complete();
                return;
            }
}

最后贴一下长轮询处理与配置变更事件通知的交互图


如果文章中有写的不对的地方,感谢大家指出....