Nacos配置中心服务端处理源码

服务端收到客户端的配置变更请求查询的长轮训请求之后,服务端怎么来处理这个长轮训呢?

上文中讲到了配置更新的整个原理及源码,我们知道客户端会有一个长轮训的任务去检查服务器端的配置是否发生了变化,如果发生了变更,那么客户端会拿到变更的 groupKey 再根据 groupKey 去获取配置项的最新值更新到本地的缓存以及文件中,那么这种每次都靠客户端去请求,那请求的时间间隔设置多少合适呢?

如果间隔时间设置的太长的话有可能无法及时获取服务端的变更,如果间隔时间设置的太短的话,那么频繁的请求对于服务端来说无疑也是一种负担,所以最好的方式是客户端每隔一段长度适中的时间去服务端请求,而在这期间如果配置发生变更,服务端能够主动将变更后的结果推送给客户端,这样既能保证客户端能够实时感知到配置的变化,也降低了服务端的压力。 我们来看看nacos设置的间隔时间是多久.

长轮训的概念

客户端发起一个请求到服务端,服务端收到客户端的请求后,并不会立刻响应给客户端,而是先把这个请求hold住,然后服务端会在hold住的这段时间检查数据是否有更新,如果有,则响应给客户端,如果一直没有数据变更,则达到一定的时间(长轮训时间间隔)才返回。

长轮训典型的场景有: 扫码登录、扫码支付。

nacos 动态配置隐藏redis_服务端

客户端长轮训

在ClientWorker这个类里面,找到checkUpdateConfigStr这个方法,这里面就是去服务器端查询发生变化的groupKey。

/**
     * Fetch the updated dataId list from server.
     *通过长轮训的方式,从远程服务器获得变化的数据进行返回
     * @param probeUpdateString       updated attribute string value.
     * @param isInitializingCacheList initial cache lists.
     * @return The updated dataId list(ps: it maybe null).
     * @throws IOException Exception.
     */
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {

    Map<String, String> params = new HashMap<String, String>(2);
    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
    Map<String, String> headers = new HashMap<String, String>(2);
    headers.put("Long-Pulling-Timeout", "" + timeout);

    // told server do not hang me up if new initializing cacheData added in
    if (isInitializingCacheList) {
        headers.put("Long-Pulling-Timeout-No-Hangup", "true");
    }

    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    }

    try {
        // In order to prevent the server from handling the delay of the client's long task,
        // increase the client's read timeout to avoid this problem.
		//超时时间,由客户端定
        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
        //发起长轮询
        HttpRestResult<String> result = agent
            .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
                      readTimeoutMs);

        if (result.ok()) {
            setHealthServer(true);
            return parseUpdateDataIdResponse(result.getData());
        } else {
            setHealthServer(false);
            LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(),
                         result.getCode());
        }
    } catch (Exception e) {
        setHealthServer(false);
        LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
        throw e;
    }
    return Collections.emptyList();
}

这个方法最终会发起http请求,注意这里面有一个readTimeoutMs的属性,

HttpRestResult<String> result = agent
    .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
              readTimeoutMs);

readTimeoutMs是在init这个方法中赋值的,默认情况下是30秒,可以通过configLongPollTimeout进行修改

private void init(Properties properties) {
	//默认timeout是30000
    timeout = Math.max(ConvertUtils.toInt(properties.getProperty(PropertyKeyConst.CONFIG_LONG_POLL_TIMEOUT),
                                          Constants.CONFIG_LONG_POLL_TIMEOUT), Constants.MIN_CONFIG_LONG_POLL_TIMEOUT);

    taskPenaltyTime = ConvertUtils
        .toInt(properties.getProperty(PropertyKeyConst.CONFIG_RETRY_TIME), Constants.CONFIG_RETRY_TIME);

    this.enableRemoteSyncConfig = Boolean
        .parseBoolean(properties.getProperty(PropertyKeyConst.ENABLE_REMOTE_SYNC_CONFIG));
}

所以从这里得出的一个基本结论是

客户端发起一个轮询请求,超时时间是30s。 那么客户端为什么要等待30s才超时呢?不是越快越好吗?

客户端长轮训的时间间隔

我们可以在nacos的日志目录下$NACOS_HOME/nacos/logs/config-client-request.log文件

2019-08-04 13:22:19,736|0|nohangup|127.0.0.1|polling|1|55|0
2019-08-04 13:22:49,443|29504|timeout|127.0.0.1|polling|1|55
2019-08-04 13:23:18,983|29535|timeout|127.0.0.1|polling|1|55
2019-08-04 13:23:48,493|29501|timeout|127.0.0.1|polling|1|55
2019-08-04 13:24:18,003|29500|timeout|127.0.0.1|polling|1|55
2019-08-04 13:24:47,509|29501|timeout|127.0.0.1|polling|1|55

可以看到一个现象,在配置没有发生变化的情况下,客户端会等29.5s以上,才请求到服务器端的结果。然后客户端拿到服务器端的结果之后,在做后续的操作。

如果在配置变更的情况下,由于客户端基于长轮训的连接保持,所以返回的时间会非常的短,我们可以做个小实验,在nacos console中频繁修改数据然后再观察一下

config-client-request.log的变化

2019-08-04 13:30:17,016|0|in-advance|127.0.0.1|polling|1|55|example+DEFAULT_GROUP
2019-08-04 13:30:17,022|3|null|127.0.0.1|get|example|DEFAULT_GROUP||e10e4d5973c497e490a8d7a9e4e9be64|unknown
2019-08-04 13:30:20,807|10|true|0:0:0:0:0:0:0:1|publish|example|DEFAULT_GROUP||81360b7e732a5dbb37d62d81cebb85d2|null
2019-08-04 13:30:20,843|0|in-advance|127.0.0.1|polling|1|55|example+DEFAULT_GROUP
2019-08-04 13:30:20,848|1|null|127.0.0.1|get|example|DEFAULT_GROUP||81360b7e732a5dbb37d62d81cebb85d2|unknown

nacos 动态配置隐藏redis_客户端_02

服务端的处理

分析完客户端之后,随着好奇心的驱使,服务端是如何处理客户端的请求的?那么同样,我们需要思考几个问题

  • 客户端的长轮训响应时间受到哪些因素的影响
  • 客户端的超时时间为什么要设置30s

客户端发送的请求地址是:/v1/cs/configs/listener 找到服务端对应的方法

ConfigController

nacos是使用spring mvc提供的rest api。这里面会调用inner.doPollingConfig进行处理

/**
 * The client listens for configuration changes.
 * 服务端提供配置修改监听的请求入口
 */
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
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)) {
        throw new IllegalArgumentException("invalid probeModify");
    }

    probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

    Map<String, String> clientMd5Map;
    try {
        // 客户端会传递多个dataId
        clientMd5Map = MD5Util.getClientMd5Map(probeModify);
    } catch (Throwable e) {
        throw new IllegalArgumentException("invalid probeModify");
    }

    // do long-polling
    //调用inner.doPollingConfig进行处理
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

doPollingConfig

这个方法中,兼容了长轮训和短轮询的逻辑,我们只需要关注长轮训的部分。再次进入到longPollingService.addLongPollingClient

/**
 * 轮询接口.
 * 这个方法中,兼容了长轮训和短轮询的逻辑,我们只需要关注长轮训的部分。再次进入到longPollingService.addLongPollingClient
 */
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 + "";
    }

     // ------------------------------以下为短轮询的逻辑-------------------------------------------
    // Compatible with short polling logic.
    // else 兼容短轮询逻辑
    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

    // Compatible with short polling result.
    // 兼容短轮询result
    String oldResult = MD5Util.compareMd5OldResult(changedGroups);
    String newResult = MD5Util.compareMd5ResultString(changedGroups);

    String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
    if (version == null) {
        version = "2.0.0";
    }
    int versionNum = Protocol.getVersionNumber(version);

    // Befor 2.0.4 version, return value is put into header.
    /**
         * 2.0.4版本以前, 返回值放入header中
         */
    if (versionNum < START_LONG_POLLING_VERSION_NUM) {
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
    } else {
        request.setAttribute("content", newResult);
    }

    Loggers.AUTH.info("new content:" + newResult);

    // Disable cache.
    // 禁用缓存
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
    response.setHeader("Cache-Control", "no-cache,no-store");
    response.setStatus(HttpServletResponse.SC_OK);
    return HttpServletResponse.SC_OK + "";
}

longPollingService.addLongPollingClient

从方法名字上可以推测出,这个方法应该是把客户端的长轮训请求添加到某个任务中去

  • 获得客户端传递过来的超时时间,并且进行本地计算,提前500ms返回响应,这就能解释为什么客户端响应超时时间是29.5+了。当然如果isFixedPolling=true的情况下,不会提前返回响应
  • 根据客户端请求过来的md5和服务器端对应的group下对应内容的md5进行比较,如果不一致,则通过generateResponse将结果返回
  • 如果配置文件没有发生变化,则通过scheduler.execute 启动了一个定时任务,将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行
/**
     * 从方法名字上可以推测出,这个方法应该是把客户端的长轮训请求添加到某个任务中去
     * 1. 获得客户端传递过来的超时时间,并且进行本地计算,提前500ms返回响应,这就能解释为什么客户端响应超时时间是29.5+了。当然如果isFixedPolling=true的情况下,不会提前返回响应
     * 2. 根据客户端请求过来的md5和服务器端对应的group下对应内容的md5进行比较,如果不一致,则通过generateResponse将结果返回
     * 3. 如果配置文件没有发生变化,则通过scheduler.execute 启动了一个定时任务,将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行
     * @param req              HttpServletRequest.
     * @param rsp              HttpServletResponse.
     * @param clientMd5Map     clientMd5Map.
     * @param probeRequestSize probeRequestSize.
     */
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
                                 int probeRequestSize) {
    //str表示超时时间,也就是timeout
    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
    String tag = req.getHeader("Vipserver-Tag");
    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
    /**
     * 提前500ms返回响应,为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动  add delay time for LoadBalance
       为了避免客户端请求超时,将客户端传递过来的超时时间缩短一定时间,默认500ms,保证能在客户端请求超时前进行返回
    */
    // 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);
    if (isFixedPolling()) {// 如果是固定的长轮询请求,则仅仅重新设置超时时间,不做其他操作
        timeout = Math.max(10000, getFixedPollingInterval());
        // Do nothing but set fix polling timeout.
    } else {
        long start = System.currentTimeMillis();
        // 找到需要更新的配置的key的列表
        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
        if (changedGroups.size() > 0) {
            // 通过该方法直接返回数据,结束长轮询
            generateResponse(req, rsp, changedGroups);
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                                    changedGroups.size());
            return;
        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                                    changedGroups.size());
            return;
        }
    }
    String ip = RequestUtil.getRemoteIp(req);

    // Must be called by http thread, or send response.
    // 一定要由HTTP线程调用,否则离开后容器会立即发送响应
    //得到AsyncContext实例之后,就会先释放容器分配给请求的线程与相关资源,然后把把实例放入了一个定时任务里面;等时间到了或者有配置变更之后,调用complete()响应完成
    final AsyncContext asyncContext = req.startAsync();

    // AsyncContext.setTimeout() is incorrect, Control by oneself
    // AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
    asyncContext.setTimeout(0L);
	// 开启一个长轮询线程
    ConfigExecutor.executeLongPolling(
        new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

这里的主要逻辑:

1.从请求中获取参数,并重新设置请求过期时间,保证在客户端请求超时前进行返回

2.如果isFixedPolling()==true,则重新设定超时时间,不做其他的操作

3.如果isFixedPolling()==false,则会直接比较一次配置信息,如果配置有变更,则直接返回给客户端,不进行长轮询。

4.如果配置没有修改,但是noHangUpFlag == true,表示该请求不需要挂起,那么就直接返回。

5.如果上面的流程都没有将请求返回,那么就会通过req这个客户端过来的请求创建一个AsyncContext,并通过线程执行ClientLongPolling任务。

nacos 动态配置隐藏redis_客户端_03

ClientLongPolling

接下来我们来分析一下,clientLongPolling到底做了什么操作。或者说我们可以先猜测一下应该会做什么事情

  • 这个任务要阻塞29.5s才能执行,因为立马执行没有任何意义,毕竟前面已经执行过一次了
  • 如果在29.5s+之内,数据发生变化,需要提前通知。需要有一种监控机制

基于这些猜想,我们可以看看它的实现过程

从代码粗粒度来看,它的实现似乎和我们的猜想一致,在run方法中,通过scheduler.schedule实现了一个定时任务,它的delay时间正好是前面计算的29.5s。在这个任务中,会通过MD5Util.compareMd5来进行计算

那另外一个,当数据发生变化以后,肯定不能等到29.5s之后才通知呀,那怎么办呢?我们发现有一个allSubs的东西,它似乎和发布订阅有关系。那是不是有可能当前的clientLongPolling订阅了数据变化的事件呢?

class ClientLongPolling implements Runnable {
    @Override
    public void run() {
        // 调度一个延时执行的任务,在这段延时的时间内,会监听配置的修改,如果触发了事件,会从这里获取allSubs队列中找到相应的长轮询连接,并返回
        asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
            @Override
            public void run() {
                try {
                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

                    // Delete subsciber's relations.
                    /**
                         * 从队列中删除当前长轮询连接实例
                         */
                    allSubs.remove(ClientLongPolling.this);

                    if (isFixedPolling()) {
                        // 如果是固定的长轮询,则通过md5比较,找到需要更新的配置的key
                        LogUtil.CLIENT_LOG
                            .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                  RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                  "polling", clientMd5Map.size(), probeRequestSize);
                        //客户端传递过来的CacheData中的md5和服务端的md5值进行比较
                        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);
                    }
                } catch (Throwable t) {
                    LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                }

            }
			// 延迟执行的时间由前面传入,timeoutTime=29.5s
        }, timeoutTime, TimeUnit.MILLISECONDS);
		 // 将当前长轮询连接ClientLongPolling放到allSubs队列中
        allSubs.add(this);
    }
}

这段代码其实就两个逻辑,一个是执行一个延时调度任务,一个是将当前长轮询连接放入allSubs队列中。
这里看调度的任务的逻辑。
如果isFixedPolling()==true,则会对配置进行比较,如果有变化,则返回变化的配置的key,其他则返回null,表示没有需要更新的配置。

那么这里有个问题,当长轮询任务在等待调度的时候,如果配置发生了变化,是如何动态告知客户端的呢?其实显然猜想的到,肯定有地方会进行事件的监听,当配置更改时,发布监听的事件,从而能够实时的动态告知客户端配置的变化

allSubs

allSubs是一个队列,队列里面放了ClientLongPolling这个对象。这个队列似乎和配置变更有某种关联关系

/**
     * allSubs是一个队列,队列里面放了ClientLongPolling这个对象。这个队列似乎和配置变更有某种关联关系
     * ClientLongPolling subscibers.
     * 长轮询订阅关系
     */
final Queue<ClientLongPolling> allSubs;

nacos 动态配置隐藏redis_nacos 动态配置隐藏redis_04

那这个时候,我的第一想法是,先去看一下当前这个类的类图,发现LongPollingService集成了Event,事件监听?果然没猜错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SJKZy4Rj-1612253537072)(https://i.loli.net/2020/10/20/dwJZUc8AhKm6FW2.png)]

LongPollingService.onEvent

这个事件的实现方法中

  • 判断事件类型是否为LocalDataChangeEvent
  • 通过scheduler.execute执行DataChangeTask这个任务
/** 
*  初始化的时候会注册监听
*/
public LongPollingService() {
    // 初始化存放客户端长轮询连接的队列
    allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();

    ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);

    // Register LocalDataChangeEvent to NotifyCenter.
    // 将LocalDataChangeEvent这个事件注册到NotifyCenter中
    NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);

    // Register A Subscriber to subscribe LocalDataChangeEvent.
	// 注册一个LocalDataChangeEvent事件的订阅
    NotifyCenter.registerSubscriber(new Subscriber() {
        @Override
        public void onEvent(Event event) {
            if (isFixedPolling()) {
                // Ignore.
            } else {
                //如果是LocalDataChangeEvent事件,添加到DataChangeTask任务中.
                if (event instanceof LocalDataChangeEvent) {
                    LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                    // 当触发LocalDataChangeEvent事件的时候,会调度DataChangeTask任务。
                    ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                }
            }
        }

        @Override
        public Class<? extends Event> subscribeType() {
            return LocalDataChangeEvent.class;
        }
    });

}

通过LongPollingService的构造方法,发现该类在初始化的时候,会注册一个LocalDataChangeEvent事件的监听,当收到LocalDataChangeEvent事件后会调度DataChangeTask任务。

DataChangeTask.run

从名字可以看出来,这个是数据变化的任务,最让人兴奋的应该是,它里面有一个循环迭代器,从allSubs里面获得ClientLongPolling

最后通过clientSub.sendResponse把数据返回到客户端。所以,这也就能够理解为何数据变化能够实时触发更新了。

该方法里会遍历allSubs这个队列,并从里面找到当前触发的配置变更事件对应的那个长轮询连接,然后将该连接从allSubs队列中去除,并且通过clientSub.sendResponse(Arrays.asList(groupKey));方法直接返回。
而在clientSub.sendResponse(Arrays.asList(groupKey));这个方法里,会判断如果当前这个长轮询连接的asyncTimeoutFuture延时调度任务还没取消,则会将该任务取消,因为配置已经变更,并且要立刻返回,继续执行延时任务没有意义。

class DataChangeTask implements Runnable {

    @Override
    public void run() {
        try {
            ConfigCacheService.getContentBetaMd5(groupKey);
            // 遍历allSubs队列,
            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.
                    // 如果beta发布且不在beta列表直接跳过
                    if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                        continue;
                    }

                    // If published tag is not in the tag list, then it skipped.
                    // 如果tag发布且不在tag列表直接跳过
                    if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                        continue;
                    }

                    getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                    // 将当前触发了事件的长轮询连接从allSubs中剔除
                    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);
                    // 直接发送返回信息,并且会把延时调度任务当前的asyncTimeoutFuture结束
                    clientSub.sendResponse(Arrays.asList(groupKey));
                }
            }
        } catch (Throwable t) {
            LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
        }
    }

    DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {
        this(groupKey, isBeta, betaIps, null);
    }

    DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {
        this.groupKey = groupKey;
        this.isBeta = isBeta;
        this.betaIps = betaIps;
        this.tag = tag;
    }

    final String groupKey;

    final long changeTime = System.currentTimeMillis();

    final boolean isBeta;

    final List<String> betaIps;

    final String tag;
}

sendResponse

void sendResponse(List<String> changedGroups) {

    // Cancel time out task.
    if (null != asyncTimeoutFuture) {
        //取消延时任务
        asyncTimeoutFuture.cancel(false);
    }
    generateResponse(changedGroups);
}

generateResponse

void generateResponse(List<String> changedGroups) {
    if (null == changedGroups) {

        // Tell web container to send http response.
        asyncContext.complete();
        return;
    }

    HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();

    try {
        final String respString = MD5Util.compareMd5ResultString(changedGroups);

        // Disable cache.
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-cache,no-store");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().println(respString);
        asyncContext.complete();
    } catch (Exception ex) {
        PULL_LOG.error(ex.toString(), ex);
        asyncContext.complete();
    }
}

那么接下来还有一个疑问是,数据变化之后是如何触发事件的呢? 所以我们定位到数据变化的请求类中,在ConfigController这个类中,找到POST请求的方法

找到配置变更的位置, 发现数据持久化之后,会通过EventDispatcher进行事件发布EventDispatcher.fireEvent 但是这个事件似乎不是我们所关心的时间,原因是这里发布的事件是ConfigDataChangeEvent, 而LongPollingService感兴趣的事件是LocalDataChangeEvent.

后来我发现,在Nacos中有一个DumpService,它会定时把变更后的数据dump到磁盘上,DumpService在spring启动之后,会调用init方法启动几个dump任务。然后在任务执行结束之后,会触发一个LocalDataChangeEvent 的事件

在ConfigCacheService中,只要涉及到config配置信息的修改的,都会发布LocalDataChangeEvent事件.

/**
 * Update md5 value.
 *
 * @param groupKey       groupKey string value.
 * @param md5            md5 string value.
 * @param lastModifiedTs lastModifiedTs long value.
 */
public static void updateMd5(String groupKey, String md5, long lastModifiedTs) {
    CacheItem cache = makeSure(groupKey);
    if (cache.md5 == null || !cache.md5.equals(md5)) {
        cache.md5 = md5;
        cache.lastModifiedTs = lastModifiedTs;
        // 发布LocalDataChangeEvent事件
        NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
    }
}

总结

  • 客户端发起长轮训请求
  • 服务端收到请求以后,先比较服务端缓存中的数据是否相同,如果不同,则直接返回
  • 如果相同,则通过schedule延迟29.5s之后再执行比较
  • 为了保证当服务端在29.5s之内发生数据变化能够及时通知给客户端,服务端采用事件订阅的方式来监听服务端本地数据变化的事件,一旦收到事件,则触发DataChangeTask的通知,并且遍历allStubs队列中的ClientLongPolling,把结果写回到客户端,就完成了一次数据的推送
  • 如果 DataChangeTask 任务完成了数据的 “推送” 之后,ClientLongPolling 中的调度任务又开始执行了怎么办呢?
    很简单,只要在进行 “推送” 操作之前,先将原来等待执行的调度任务取消掉就可以了,这样就防止了推送操作写完响应数据之后,调度任务又去写响应数据,这时肯定会报错的。所以,在ClientLongPolling方法中,最开始的一个步骤就是删除订阅事件

所以总的来说,Nacos采用推+拉的形式,来解决最开始关于长轮训时间间隔的问题。当然,30s这个时间是可以设置的,而之所以定30s,应该是一个经验值。

nacos 动态配置隐藏redis_config_05