Nacos配置中心(V1.3.3 版本)客户端缓存配置动态更新源码分析

645 阅读7分钟

NacosConfigService

NacosConfigService 是客户端操作入口,先查看 NacosConfigService 的构造方法:

public NacosConfigService(Properties properties) throws NacosException {
    ValidatorUtils.checkInitParam(properties);
    String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
    if (StringUtils.isBlank(encodeTmp)) {
        this.encode = Constants.ENCODE;
    } else {
        this.encode = encodeTmp.trim();
    }
    initNamespace(properties);

    this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    this.agent.start();
    this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
  • 初始化了一个 HttpAgent ,内部封装了一些操作 HTTP 请求的方法,实际工作的类是 ServerHttpAgent ,调用 start() 方法最终会执行 ServerListManager.start() :会注册一个 getServersTask 任务,每隔 30s 执行一次来刷新 server list。

  • ClientWorker 客户端工作类。

ClientWorker

进入 ClientWorker 的构造方法:

 public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
            final Properties properties) {
     // 维护 HttpAgent
     this.agent = agent;
     // 初始化配置过滤管理器
     this.configFilterChainManager = configFilterChainManager;

     // 初始化配置: 长轮询超时时间 30s
     init(properties);

     // 初始化一个定时调度的线程池, 跟 Worker 有关
     this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
         @Override
         public Thread newThread(Runnable r) {
             Thread t = new Thread(r);
             t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
             t.setDaemon(true);
             return t;
         }
     });
     
     // 初始化一个定时调度的线程池, 跟长轮询有关
     this.executorService = Executors
         .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
             @Override
             public Thread newThread(Runnable r) {
                 Thread t = new Thread(r);
                 t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                 t.setDaemon(true);
                 return t;
             }
         });

     // 定时调用 checkConfigInfo 这个方法, 首次执行延迟时间为1毫秒, 周期为10毫秒
     this.executor.scheduleWithFixedDelay(new Runnable() {
         @Override
         public void run() {
             try {
                 checkConfigInfo();
             } catch (Throwable e) {
                 LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
             }
         }
     }, 1L, 10L, TimeUnit.MILLISECONDS);
 }

ClientWorker 里,创建了两个线程池:

  • executor 线程池只拥有一个线程,每隔 10ms 会执行一次 checkConfigInfo() 方法。
  • executorService 线程池,从名称看应该是做长轮询的。

checkConfigInfo()

public void checkConfigInfo() {
    // 从缓存中获取监听的配置数量
    int listenerSize = cacheMap.get().size();
    
    // 除以 3000 向上取整, 得到长轮询任务的数量
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    
    // 如果当前长轮询任务熟数量小于上面的结果, 则继续创建
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // 在 executorService 线程池中启动一个 LongPoollingRunnable 任务
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}
  • cacheMap :用来存储监听变更的缓存集合,结构如下:

    /**
     * groupKey -> cacheData.
     */
    private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(
        new HashMap<String, CacheData>());
    

    为保证多线程场景数据一致性,cacheMap 采用了 AtomicReference 原子变量实现。

    cacheMap 是个Map结构,key为 groupKey ,是由 dataId,group,tenant(租户)拼接的字符串;value 为 CacheData 对象,每个 dataId 都会持有一个CacheData 对象。

  • 使用 ClientWorker 构造方法中创建的 executorService 线程池执行 LongPollingRunnable 任务,默认情况下,每个 LongPollingRunnable 任务执行 3000 个监听配置集,超过则启动多个 LongPollingRunnable 任务执行。

  • currentLongingTaskCount 用于保存当前已启动的 LongPollingRunnable 任务数。

LongPollingRunnable

这是一个线程任务,直接进入 run() 方法查看:

@Override
public void run() {

    List<CacheData> cacheDatas = new ArrayList<CacheData>();
    List<String> inInitializingCacheList = new ArrayList<String>();
    try {
        // check failover config
        for (CacheData cacheData : cacheMap.get().values()) {
            if (cacheData.getTaskId() == taskId) {
                // 对任务按批次分类, 把任务 id 相同的数据保存到 cacheDatas
                cacheDatas.add(cacheData);
                try {
                    // 检查本地配置文件
                    checkLocalConfig(cacheData);
                    // 如果 isUseLocalConfigInfo 为 true, 表示缓存和本地配置不一致
                    if (cacheData.isUseLocalConfigInfo()) {
                        // 检查 MD5,通知客户端监听
                        cacheData.checkListenerMd5();
                    }
                } catch (Exception e) {
                    LOGGER.error("get local config info error", e);
                }
            }
        }

        // check server config
        List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
        if (!CollectionUtils.isEmpty(changedGroupKeys)) {
            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
        }

        for (String groupKey : changedGroupKeys) {
            String[] key = GroupKey.parseKey(groupKey);
            String dataId = key[0];
            String group = key[1];
            String tenant = null;
            if (key.length == 3) {
                tenant = key[2];
            }
            try {
                String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                cache.setContent(ct[0]);
                if (null != ct[1]) {
                    cache.setType(ct[1]);
                }
                LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(ct[0]), ct[1]);
            } catch (NacosException ioe) {
                String message = String
                    .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                            agent.getName(), dataId, group, tenant);
                LOGGER.error(message, ioe);
            }
        }
        for (CacheData cacheData : cacheDatas) {
            if (!cacheData.isInitializing() || inInitializingCacheList
                .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                cacheData.checkListenerMd5();
                cacheData.setInitializing(false);
            }
        }
        inInitializingCacheList.clear();

        executorService.execute(this);

    } catch (Throwable e) {

        // If the rotation training task is abnormal, the next execution time of the task will be punished
        LOGGER.error("longPolling error : ", e);
        executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
    }
}

check failover config

先查看第一部分 check failover config,检查本地配置,主要是 checkLocalConfig()checkListenerMd5() 两个方法。

checkLocalConfig()

private void checkLocalConfig(CacheData cacheData) {
    final String dataId = cacheData.dataId;
    final String group = cacheData.group;
    final String tenant = cacheData.tenant;
    // 本地缓存文件路径
    File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);

    // 1. 如果 isUseLocalConfigInfo 为 false,表示不使用本地配置,并且本地缓存文件路径存在
    if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
        // 从本地缓存文件获取配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        // 设置 isUseLocalConfigInfo 为 true
        cacheData.setUseLocalConfigInfo(true);
        
        // 更新 cacheData 文件更新时间和内容
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);

        LOGGER.warn(
            "[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
            agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        return;
    }

    // 2. 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径不存在
    if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
        // 设置 isUseLocalConfigInfo 为 false
        cacheData.setUseLocalConfigInfo(false);
        LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                    dataId, group, tenant);
        return;
    }

    // 3. 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径存在, 且缓存的时间跟文件的更新时间不一致
    if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
        .lastModified()) {
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        // 设置 isUseLocalConfigInfo 为 true
        cacheData.setUseLocalConfigInfo(true);
        
        // 更新 cacheData 文件更新时间和内容
        cacheData.setLocalConfigInfoVersion(path.lastModified());
        cacheData.setContent(content);
        LOGGER.warn(
            "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
            agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
    }
}

检查本地配置,分三种情况:

  • 如果 isUseLocalConfigInfo 为 false,表示不使用本地配置,并且本地缓存文件路径存在,则从本地缓存文件获取配置, 设置 isUseLocalConfigInfo 为 true,并更新 cacheData 文件更新时间和内容。
  • 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径不存在,则设置 isUseLocalConfigInfo 为 false。
  • 如果 isUseLocalConfigInfo 为 true,使用本地配置,并且本地缓存文件路径存在,且缓存的时间跟文件的更新时间不一致,则从本地缓存文件获取配置, 设置 isUseLocalConfigInfo 为 true,并更新 cacheData 文件更新时间和内容。

checkListenerMd5()

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, wrap);
        }
    }
}

遍历添加的监听器,如果发现数据的 md5 值不一致,则发送数据变更通知。

safeNotifyListener()

private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
                                final String md5, final ManagerListenerWrap listenerWrap) {
    final Listener listener = listenerWrap.listener;

    // 启一个线程, 向注册了监听的客户端推送变更后的数据内容
    Runnable job = new Runnable() {
        @Override
        public void run() {
            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader appClassLoader = listener.getClass().getClassLoader();
            try {
                if (listener instanceof AbstractSharedListener) {
                    AbstractSharedListener adapter = (AbstractSharedListener) listener;
                    adapter.fillContext(dataId, group);
                    LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                }
                // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                Thread.currentThread().setContextClassLoader(appClassLoader);

                ConfigResponse cr = new ConfigResponse();
                cr.setDataId(dataId);
                cr.setGroup(group);
                cr.setContent(content);
                configFilterChainManager.doFilter(null, cr);
                String contentTmp = cr.getContent();
                // 回调 listener 的 receiveConfigInfo() 方法
                listener.receiveConfigInfo(contentTmp);

                // compare lastContent and content
                if (listener instanceof AbstractConfigChangeListener) {
                    Map data = ConfigChangeHandler.getInstance()
                        .parseChangeData(listenerWrap.lastContent, content, type);
                    ConfigChangeEvent event = new ConfigChangeEvent(data);
                    ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                    listenerWrap.lastContent = content;
                }

                listenerWrap.lastCallMd5 = md5;
                LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                            listener);
            } 
            // 省略部分...
        }
    };

    final long startNotify = System.currentTimeMillis();
    try {
        if (null != listener.getExecutor()) {
            listener.getExecutor().execute(job);
        } else {
            job.run();
        }
    } 
    // 省略部分...
}

safeNotifyListener() 方法单独起线程,向所有对 dataId 注册过监听的客户端推送变更后的数据内容。客户端接收通知,直接实现 receiveConfigInfo() 方法接收回调数据,处理自身业务就可以了。

check server config

再看第二部分 check server config ,检查服务配置。

checkUpdateDataIds()

从服务端获取配置变化了的列表,返回:dataId、group、tenant

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
    StringBuilder sb = new StringBuilder();
    for (CacheData cacheData : cacheDatas) {
        // 找到 isUseLocalConfigInfo 为 false 的缓存数据
        if (!cacheData.isUseLocalConfigInfo()) {
            // 把需要检查的配置项,拼接成一个字符串: dataId、group、MD5
            sb.append(cacheData.dataId).append(WORD_SEPARATOR);
            sb.append(cacheData.group).append(WORD_SEPARATOR);
            if (StringUtils.isBlank(cacheData.tenant)) {
                sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
            } else {
                sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
                sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
            }
            if (cacheData.isInitializing()) {
                // It updates when cacheData occours in cacheMap by first time.
                inInitializingCacheList
                    .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
            }
        }
    }
    boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}

checkUpdateConfigStr

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);
        // 向服务端发送 http 请求: /v1/cs/configs/listener
        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();
}

getServerConfig()

public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
    throws NacosException {
    String[] ct = new String[2];
    if (StringUtils.isBlank(group)) {
        group = Constants.DEFAULT_GROUP;
    }

    HttpRestResult<String> result = null;
    try {
        Map<String, String> params = new HashMap<String, String>(3);
        if (StringUtils.isBlank(tenant)) {
            params.put("dataId", dataId);
            params.put("group", group);
        } else {
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("tenant", tenant);
        }
        // 发送 http 请求:/v1/cs/configs
        result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
    } catch (Exception ex) {
        String message = String
            .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
                    agent.getName(), dataId, group, tenant);
        LOGGER.error(message, ex);
        throw new NacosException(NacosException.SERVER_ERROR, ex);
    }

    switch (result.getCode()) {
        case HttpURLConnection.HTTP_OK:
            // 保存本地配置
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
            ct[0] = result.getData();
            if (result.getHeader().getValue(CONFIG_TYPE) != null) {
                ct[1] = result.getHeader().getValue(CONFIG_TYPE);
            } else {
                ct[1] = ConfigType.TEXT.getType();
            }
            return ct;
       // 省略部分...
    }
}
  • 根据配置变化了的列表从服务端获取配置,发送 HTTP 请求:/v1/cs/configs
  • 默认超时时间 3s
  • 保存本地配置快照

总结

Nacos 客户端缓存配置动态更新主要流程:

  • ClientWorker 种会创建两个线程池,executor 线程池只有一个线程,会定时 10ms 执行一次 checkConfigInfo()executorService 执行长轮询任务。
  • 对本地缓存配置分批次拆分,每个批次处理 3000 个配置集,超过则启动多个 LongPollingRunnable 任务执行,由 executorService 线程池来执行 LongPollingRunnable 任务。
  • 先把每一个批次的缓存配置和本地文件配置对比:
    • 如果不一致,则更新 isUseLocalConfigInfo 为 true,比较 MD5 值不一致则向客户端发送数据变更通知(Listener)。
    • 如果一致,则远程请求服务端检查变化了的配置项。返回 dataId、group、tenant
  • 客户端收到变更的配置列表,再从服务端获取最新配置。
  • 将最新配置保存到本地配置快照。