12、Nacos 配置服务服务端源码分析(三)

上篇说到了服务器地址的获取和AsyncRpcTask类,但是有两个重要逻辑dump和服务节点间的消息同步没有分析,本篇就来揭开他们的面纱。

dump方法

/**
 * 将DumpTask添加到任务管理器,它将异步执行。
 */
public void dump(String dataId, String group, String tenant, String tag, long lastModified, String handleIp,
                 boolean isBeta) {
    String groupKey = GroupKey2.getKey(dataId, group, tenant);
    String taskKey = String.join("+", dataId, group, tenant, String.valueOf(isBeta), tag);
    // 添加到dumpTask后异步执行
    dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, tag, lastModified, handleIp, isBeta));
    DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey);
}

上面是将task放入到了TaskManager中,那在哪里执行的呢?

我们看下 构造方法

public DumpService(ConfigInfoPersistService configInfoPersistService, CommonPersistService commonPersistService,
                   HistoryConfigInfoPersistService historyConfigInfoPersistService,
                   ConfigInfoAggrPersistService configInfoAggrPersistService,
                   ConfigInfoBetaPersistService configInfoBetaPersistService,
                   ConfigInfoTagPersistService configInfoTagPersistService, ServerMemberManager memberManager) {
    this.configInfoPersistService = configInfoPersistService;
    this.commonPersistService = commonPersistService;
    this.historyConfigInfoPersistService = historyConfigInfoPersistService;
    this.configInfoAggrPersistService = configInfoAggrPersistService;
    this.configInfoBetaPersistService = configInfoBetaPersistService;
    this.configInfoTagPersistService = configInfoTagPersistService;
    this.memberManager = memberManager;
    this.processor = new DumpProcessor(this);
    this.dumpAllProcessor = new DumpAllProcessor(this);
    this.dumpAllBetaProcessor = new DumpAllBetaProcessor(this);
    this.dumpAllTagProcessor = new DumpAllTagProcessor(this);
    // 创建一个TaskManager
    this.dumpTaskMgr = new TaskManager("com.alibaba.nacos.server.DumpTaskManager");
    // 设置默认的Processor处理
    this.dumpTaskMgr.setDefaultTaskProcessor(processor);

    this.dumpAllTaskMgr = new TaskManager("com.alibaba.nacos.server.DumpAllTaskManager");
    this.dumpAllTaskMgr.setDefaultTaskProcessor(dumpAllProcessor);

    this.dumpAllTaskMgr.addProcessor(DumpAllTask.TASK_ID, dumpAllProcessor);
    this.dumpAllTaskMgr.addProcessor(DumpAllBetaTask.TASK_ID, dumpAllBetaProcessor);
    this.dumpAllTaskMgr.addProcessor(DumpAllTagTask.TASK_ID, dumpAllTagProcessor);

    DynamicDataSource.getInstance().getDataSource();
}

在之前的文章中,我们分析过如果Task没有设置指定的Processor就会用默认的Processor进行处理。在这个TaskManager中,用的就是默认的Processor

@Override
public boolean process(NacosTask task) {
    DumpTask dumpTask = (DumpTask) task;
    String[] pair = GroupKey2.parseKey(dumpTask.getGroupKey());
    String dataId = pair[0];
    String group = pair[1];
    String tenant = pair[2];
    long lastModified = dumpTask.getLastModified();
    String handleIp = dumpTask.getHandleIp();
    boolean isBeta = dumpTask.isBeta();
    String tag = dumpTask.getTag();

    // 构建ConfigDumpEventBuild
    ConfigDumpEvent.ConfigDumpEventBuilder build = ConfigDumpEvent.builder().namespaceId(tenant).dataId(dataId)
        .group(group).isBeta(isBeta).tag(tag).lastModifiedTs(lastModified).handleIp(handleIp);

    if (isBeta) {
        // 如果发布测试版,则转储配置,更新测试版缓存
        // 省略部分代码...
    }
    if (StringUtils.isBlank(tag)) {
        // tag为空的情况
        ConfigInfo cf = configInfoPersistService.findConfigInfo(dataId, group, tenant);
        build.remove(Objects.isNull(cf));
        build.content(Objects.isNull(cf) ? null : cf.getContent());
        build.type(Objects.isNull(cf) ? null : cf.getType());
        build.encryptedDataKey(Objects.isNull(cf) ? null : cf.getEncryptedDataKey());
    } else {
        // tag不为空 省略部分代码...
    }
    // 处理dumpEvent
    return DumpConfigHandler.configDump(build.build());
}

上面的处理逻辑主要是构造了一个ConfigDumpEvent。然后再通过DumpConfigHandler处理。

public static boolean configDump(ConfigDumpEvent event) {
    final String dataId = event.getDataId();
    final String group = event.getGroup();
    final String namespaceId = event.getNamespaceId();
    final String content = event.getContent();
    final String type = event.getType();
    final long lastModified = event.getLastModifiedTs();
    final String encryptedDataKey = event.getEncryptedDataKey();
    if (event.isBeta()) {
        // beta版本,省略部分代码...
    }
    if (StringUtils.isBlank(event.getTag())) {
        // 处理特殊的dataId
        if (dataId.equals(AggrWhitelist.AGGRIDS_METADATA)) {
            AggrWhitelist.load(content);
        }
        if (dataId.equals(ClientIpWhiteList.CLIENT_IP_WHITELIST_METADATA)) {
            ClientIpWhiteList.load(content);
        }
        if (dataId.equals(SwitchService.SWITCH_META_DATAID)) {
            SwitchService.load(content);
        }

        boolean result;
        // 非删除配置事件
        if (!event.isRemove()) {
            // 配置缓存服务dump配置信息
            result = ConfigCacheService
                .dump(dataId, group, namespaceId, content, lastModified, type, encryptedDataKey);

            if (result) {
                // 记录日志
                ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(),
                                                ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified,
                                                content.length());
            }
        } else {
            // 删除配置事件,移除配置缓存
            result = ConfigCacheService.remove(dataId, group, namespaceId);

            if (result) {
                ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(),
                                                ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0);
            }
        }
        return result;
    } else {
        // tag不为空的处理,省略部分代码...
    }
}

因为ConfigDumpEvent分为了两类事件,一类是新增或更新的事件,另一类是删除的事件,对于这两种事件是不同的两种处理方式。

首先看下删除的逻辑

public static boolean remove(String dataId, String group, String tenant) {
    final String groupKey = GroupKey2.getKey(dataId, group, tenant);
    // 获取写锁
    final int lockResult = tryWriteLock(groupKey);
    // 如果数据不存在了
    if (0 == lockResult) {
        DUMP_LOG.info("[remove-ok] {} not exist.", groupKey);
        return true;
    }
    // 获取写锁失败了
    if (lockResult < 0) {
        DUMP_LOG.warn("[remove-error] write lock failed. {}", groupKey);
        return false;
    }
    try {
        // 移除配置
        if (!PropertyUtil.isDirectRead()) {
            DiskUtil.removeConfigInfo(dataId, group, tenant);
        }
        CACHE.remove(groupKey);
        // 发布本地配置变动通知
        NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
        return true;
    } finally {
        // 释放写锁
        releaseWriteLock(groupKey);
    }
}
  1. 获取写锁
  2. 移除配置信息
  3. 发布本地配置变动通知

再看下新增,修改的逻辑

public static boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs,
                           String type, String encryptedDataKey) {
    String groupKey = GroupKey2.getKey(dataId, group, tenant);
    CacheItem ci = makeSure(groupKey, encryptedDataKey, false);
    ci.setType(type);
    // 获取写锁
    final int lockResult = tryWriteLock(groupKey);
    assert (lockResult != 0);
    // 获取锁失败
    if (lockResult < 0) {
        DUMP_LOG.warn("[dump-error] write lock failed. {}", groupKey);
        return false;
    }

    try {
        // 计算配置信息的md5值
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
        // 如果这个事件滞后了则不处理了
        if (lastModifiedTs < ConfigCacheService.getLastModifiedTs(groupKey)) {
            DUMP_LOG.warn("[dump-ignore] the content is old. groupKey={}, md5={}, lastModifiedOld={}, "
                          + "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey),
                          lastModifiedTs);
            return true;
        }
        if (md5.equals(ConfigCacheService.getContentMd5(groupKey)) && DiskUtil.targetFile(dataId, group, tenant).exists()) {
            // 配置信息的内容一致不需要保存到磁盘
            DUMP_LOG.warn("[dump-ignore] ignore to save cache file. groupKey={}, md5={}, lastModifiedOld={}, "
                          + "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey),
                          lastModifiedTs);
        } else if (!PropertyUtil.isDirectRead()) {
            // 非单节点非本地存贮则需要保存一份到磁盘
            DiskUtil.saveToDisk(dataId, group, tenant, content);
        }
        // 更新md5值,并发布本地配置变动事件
        updateMd5(groupKey, md5, lastModifiedTs, encryptedDataKey);
        return true;
    } catch (IOException ioe) {
        DUMP_LOG.error("[dump-exception] save disk error. " + groupKey + ", " + ioe);
        if (ioe.getMessage() != null) {
            String errMsg = ioe.getMessage();
            if (NO_SPACE_CN.equals(errMsg) || NO_SPACE_EN.equals(errMsg) || errMsg.contains(DISK_QUATA_CN) || errMsg
                .contains(DISK_QUATA_EN)) {
                // Protect from disk full.
                FATAL_LOG.error("磁盘满自杀退出", ioe);
                System.exit(0);
            }
        }
        return false;
    } finally {
        // 释放写锁
        releaseWriteLock(groupKey);
    }
}

public static void updateMd5(String groupKey, String md5, long lastModifiedTs, String encryptedDataKey) {
    CacheItem cache = makeSure(groupKey, encryptedDataKey, false);
    if (cache.md5 == null || !cache.md5.equals(md5)) {
        cache.md5 = md5;
        cache.lastModifiedTs = lastModifiedTs;
        NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
    }
}

这里与删除逻辑不同的是,需要比较md5值,不一致可能会有个磁盘存储的处理。

LocalDataChangeEvent留给大家自行分析。主要分为两部分,一部分逻辑是1.X的长轮训的推送,另一部分是2.X的RPC双工链路的推送。

服务节点间的消息同步

回顾下AsyncRpcTaskrun方法

@Override
public void run() {
    while (!queue.isEmpty()) {
        // 省略自身的处理逻辑
        if (memberManager.hasMember(member.getAddress())) {
            boolean unHealthNeedDelay = memberManager.isUnHealth(member.getAddress());
            if (unHealthNeedDelay) {
                ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
                                                  task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
                                                  0, member.getAddress());
                // 可延迟的处理,因为是不健康的节点,不知道什么时候能恢复
                asyncTaskExecute(task);
            } else {
                if (!MemberUtil.isSupportedLongCon(member)) {
                    // 不支持长连接,则走1.X的异步发送http请求
                    asyncTaskExecute(
                        new NotifySingleTask(task.getDataId(), task.getGroup(), task.getTenant(), task.tag,
                                             task.getLastModified(), member.getAddress(), task.isBeta));
                } else {
                    // 支撑走RPC的方式
                    try {
                        configClusterRpcClientProxy
                            .syncConfigChange(member, syncRequest, new AsyncRpcNotifyCallBack(task));
                    } catch (Exception e) {
                        MetricsMonitor.getConfigNotifyException().increment();
                        asyncTaskExecute(task);
                    }
                }

            }
        } else {
            //No nothig if  member has offline.
        }
    }
}

在服务节点间的同步有三个主要的逻辑:

  1. 节点不健康的情况,采用异步定时的去执行,但是这个定时并不是严格意义的定时,因为他会有个延迟的过程,他会随着失败次数的增加,延迟不断加大,不过当达到最大失败次数后,就不会再增加,以一个固定的时间去触发。最大时间间隔是500ms + 7 * 7 * 1000ms

    private static int getDelayTime(NotifyTask task) {
        int failCount = task.getFailCount();
        int delay = MIN_RETRY_INTERVAL + failCount * failCount * INCREASE_STEPS;
        if (failCount <= MAX_COUNT) {
            task.setFailCount(failCount + 1);
        }
        return delay;
    }
    
  2. 如果成员节点是不支持长连接的,那就是以前的老版本,1.X的模式,需要用到http发送同步请求

  3. 如果是支持长连接,是Rpc的方法发送同步请求

这里我们主要分析Rpc的方式

public void syncConfigChange(Member member, ConfigChangeClusterSyncRequest request, RequestCallBack callBack)
    throws NacosException {
    // 异步处理
    clusterRpcClientProxy.asyncRequest(member, request, callBack);
}

public void asyncRequest(Member member, Request request, RequestCallBack callBack) throws NacosException {
    // 获取成员的rpc客户端
    RpcClient client = RpcClientFactory.getClient(memberClientKey(member));
    if (client != null) {
        // 异步处理
        client.asyncRequest(request, callBack);
    } else {
        throw new NacosException(CLIENT_INVALID_PARAM, "No rpc client related to member: " + member);
    }
}

ConfigChangeClusterSyncRequestHandler找到处理逻辑

public ConfigChangeClusterSyncResponse handle(ConfigChangeClusterSyncRequest configChangeSyncRequest,
            RequestMeta meta) throws NacosException {
    if (configChangeSyncRequest.isBeta()) {
        // beta情况,省略部分代码...
    } else {
        // 本机的dump服务
        dumpService.dump(configChangeSyncRequest.getDataId(), configChangeSyncRequest.getGroup(),
                         configChangeSyncRequest.getTenant(), configChangeSyncRequest.getLastModified(), meta.getClientIp());
    }
    return new ConfigChangeClusterSyncResponse();
}

调用到其他节点,其他节点也是执行dump服务,然后通知和本机连接的客户端,通知他们进行配置更新。

总结

本篇分析了dump方法和节点间的数据同步。节点间的消息通信后,还是利用了dump方法比较缓存的中的配置信息md5。发现变动后,通知到客户端的监听器进行配置的更新。下一篇我会最整个Nacos源码分析系列进行总结以及写自己的一些感悟,敬请期待。

你可能感兴趣的:(Nacos,源码分析,java,分布式,中间件)