『Naocs 2.x』(八) Nacos Config 配置变更,如何与 Spring Boot 同步

2,071 阅读8分钟

前言

Nacos 2.x 中抛弃了长轮询模式,改用长连接进行配置同步。

这次就来探探 Nacos 配置中心是如何与Spring Boot 同步变更的。

前置知识

我们在 《『Naocs 2.x』(三) Nacos 服务注册逻辑及通信过程 》的 GRPC调用过程一小节中,分析过 Nacos Server 与 Nacos Client 的请求与响应过程。

简单地回顾一下,就是根据 Request的具体类型不同, Nacoe Server 获取到对应的RequestHandler,进行业务处理,最后把处理结果封装为Response返回。

我们来看一下 Nacos Cofnig 中 Request 的层级结构 ( 只保留了与此节强相关的子类 ) :

image-20211113171317751

  • ConfigBatchListenRequest

    Nacos Client 向 Nacos Service ,请求监听一批配置。

  • ConfigQueryRequest

    Nacos Client 向 Nacos Service,查询配置内容。

    ps: 截图少了这个,是AbstractConfigRequest的子类。

  • ConfigChangeNotifyRequest

    Nacos Server 向 Nacos Client ,推送变更配置内容的 Key。

同步配置初始化流程

NacosConfigManager

我们从 NacosConfigManager 说起,一看名字就知道,这个类绝逼持有某些重要的东西。

NacosConfigAutoConfiguration配置类中:

@Bean
public NacosConfigManager nacosConfigManager( NacosConfigProperties nacosConfigProperties) {
    return new NacosConfigManager(nacosConfigProperties);
}

下面进入 NacosConfigManager 中:

NacosConfigManager 持有:ConfigService(配置相关操作)、NacosConfigProperties(Spring Boot 对配置中心的配置)。

public class NacosConfigManager {
    private static ConfigService service = null;
    private NacosConfigProperties nacosConfigProperties;
 
    public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
        this.nacosConfigProperties = nacosConfigProperties;
        createConfigService(nacosConfigProperties);
    }
 
    static ConfigService createConfigService(
            NacosConfigProperties nacosConfigProperties) {
        if (Objects.isNull(service)) {
            // 加锁防止创建了多个NacosConfigManager
            // 可能是为了防止使用者手动创建此类
            synchronized (NacosConfigManager.class) {
                try {
                    if (Objects.isNull(service)) {
                        // 这里是通过反射构造函数创建了 NacosService 的子类
                        // NacosConfigService(Properties properties)
                        service = NacosFactory.createConfigService(
                                nacosConfigProperties.assembleConfigServiceProperties());
                    }
                }
                // …………
            }
        }
        return service;
    }
    // …………
}

NacosConfigService 构造函数

// NacosConfigService # NacosConfigService(Properties properties)
public NacosConfigService(Properties properties) throws NacosException {
    ValidatorUtils.checkInitParam(properties);
    // 初始化 命名空间,放到 properties 中。
    initNamespace(properties);
    // 设置请求过滤器
    this.configFilterChainManager = new ConfigFilterChainManager(properties);
    // 设置服务器名称列表的线程任务
    ServerListManager serverListManager = new ServerListManager(properties);
    serverListManager.start();
    // 重头戏,创建ClientWorker
    this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties);
    // will be deleted in 2.0 later versions
    agent = new ServerHttpAgent(serverListManager);
 
}

ClientWorker 构造函数

public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,final Properties properties) throws NacosException {
 
    this.configFilterChainManager = configFilterChainManager;
    init(properties);
    // 创建 Grpc 请求类
    agent = new ConfigRpcTransportClient(properties, serverListManager);
    // (重要)设置线程任务。该线程任务用于同步配置。
    ScheduledExecutorService executorService = Executors
            .newScheduledThreadPool(ThreadUtils.getSuitableThreadCount(1), r -> {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker");
                t.setDaemon(true);
                return t;
            });
    agent.setExecutor(executorService);
    agent.start();
 
}

ConfigRpcTransportClient

ConfigRpcTransportClient 的父类为ConfigTransportClient

// ConfigTransportClient
public void start() throws NacosException {
    // .......
 
    // 执行内部任务
    startInternal();
}
 
// ConfigRpcTransportClient
// 这个方式启动一线程任务,通过 wthile(true) 方式一直循环。
@Override
public void startInternal() throws NacosException {
    executor.schedule(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    listenExecutebell.poll(5L, TimeUnit.SECONDS);
                    executeConfigListen();
                }
                // …………
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);
}
 
@Override
public void notifyListenConfig() {
    listenExecutebell.offer(bellItem);
}
  • listenExecutebell.poll(5L, TimeUnit.SECONDS);

    获取队列头部元素,如果获取不到则等待5s。Nacos 通过这种方式来控制循环间隔。

    这里需要特别注意,Nacos 通过调用notifyListenConfig()向 listenExecutebell 设置元素的方式,来立即执行executeConfigListen()方法。

    notifyListenConfig() 方法我们在后面还会见到。

到此处同步配置的初始化流程就完成了。我们继续看同步配置的过程。

客户端同步配置

同步配置的逻辑,主要在executeConfigListen();方法中,这段方法比较长。我们分开来看。

CacheData执行判断与分组

// 5 minutes to check all listen cache keys.
// 5 分钟执行一次全量同步。
private static final long ALL_SYNC_INTERNAL = 5 * 60 * 1000L;
 
// 准备两个组:有监听组和无监听组
Map<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);
Map<String, List<CacheData>> removeListenCachesMap = new HashMap<String, List<CacheData>>(16);
// 判断是否到全量同步时间
long now = System.currentTimeMillis();
boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL;
 
// 遍历本地 CacheDataMap。CacheData 保存了配置基本信息,配置的监听器等基础信息。
for (CacheData cache : cacheMap.get().values()) {
    synchronized (cache) {
        //check local listeners consistent.
        // 首先判断,该 cacheData 是否需要检查。也就是如果为 false,必定进行检查。
        // 1.添加listener.default为false;需要检查。
        // 2.接收配置更改通知,设置为false;需要检查。
        // 3.last listener被移除,设置为false;需要检查
        if (cache.isSyncWithServer()) {
            // 执行 CacheData.Md5 与 Listener.md5的比对与设定
            // 如果不相同,则进行监听器的回调。
            cache.checkListenerMd5();
            // 如果还不需要全量同步,就跳过这个 cacheData.
            if (!needAllSync) {
                continue;
            }
        }
 
        if (!CollectionUtils.isEmpty(cache.getListeners())) {
            // 有监听器的放入 listenCachesMap
        } else if (CollectionUtils.isEmpty(cache.getListeners())) {
            // 没有监听器的放入 removeListenCachesMap
    }
}

处理有监听器的 CacheData

// 标志是否有更改的配置
boolean hasChangedKeys = false;
for (Map.Entry<String, List<CacheData>> entry : listenCachesMap.entrySet()) {
    String taskId = entry.getKey();
    List<CacheData> listenCaches = entry.getValue();
    // 构建监听器请求
    ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(listenCaches);
    configChangeListenRequest.setListen(true);
    try {
        // 初始化RpcClient
        RpcClient rpcClient = ensureRpcClient(taskId);
        // 发送请求向 Nacos Server 添加配置变化监听器。
        // 服务端将返回有变化的dataId,group,tenant
        ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(
                rpcClient, configChangeListenRequest);
        if (configChangeBatchListenResponse != null && configChangeBatchListenResponse.isSuccess()) {
            Set<String> changeKeys = new HashSet<String>();
            // 处理有变化的配置
            if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) {
                hasChangedKeys = true;
                for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse
                        .getChangedConfigs()) {
                    String changeKey = GroupKey
                            .getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(),
                                    changeConfig.getTenant());
                    changeKeys.add(changeKey);
                    boolean isInitializing = cacheMap.get().get(changeKey).isInitializing();
                    // 刷新上下文
                    // 此处将请求 Nacos Server ,获取最新配置内容,并触发 Listener 的回调。
                    refreshContentAndCheck(changeKey, !isInitializing);
                }
 
            }
 
            //handler content configs
            for (CacheData cacheData : listenCaches) {
                String groupKey = GroupKey
                        .getKeyTenant(cacheData.dataId, cacheData.group, cacheData.getTenant());
                // 如果返回的 changeKeys 中,未包含此 groupKey。则说明此内容未发生变化。
                if (!changeKeys.contains(groupKey)) {
                    //sync:cache data md5 = server md5 && cache data md5 = all listeners md5.
                    synchronized (cacheData) {
                        if (!cacheData.getListeners().isEmpty()) {
                            // 则将同步标志设为 true
                            cacheData.setSyncWithServer(true);
                            continue;
                        }
                    }
                }
                // 将初始化状态设置 false
                cacheData.setInitializing(false);
            }
 
        }
    } catch (Exception e) {}
}

处理无监听器的 CacheData

无监听器的 CacheData 就是,从 Nacos Client 与 Nacos Server 中移除掉原有的监听器。

结尾处理

if (needAllSync) {
    lastAllSyncTime = now;
}
// If has changed keys,notify re sync md5.
// 如果有改变的配置,则立即进行一次同步配置过程。
if (hasChangedKeys) {
    notifyListenConfig();
}

客户端接收服务端推送

当 Nacos Config 配置发生变更时,Nacos Server 会主动通知 Nacos Client。

Nacos Client 在向 Nacos Server 发送请求前,会初始化 Nacos Rpc Client,执行的方法是ConfigRpcTransportClient # ensureRpcClient(String taskId)

private RpcClient ensureRpcClient(String taskId) throws NacosException {
    synchronized (ClientWorker.this) {
        Map<String, String> labels = getLabels();
        Map<String, String> newLabels = new HashMap<String, String>(labels);
        newLabels.put("taskId", taskId);
        RpcClient rpcClient = RpcClientFactory
                .createClient(uuid + "_config-" + taskId, getConnectionType(), newLabels);
        if (rpcClient.isWaitInitiated()) {
            // 初始化处理器,在处理初始化了对 ConfigChangeNotifyRequest 的处理逻辑。
            initRpcClientHandler(rpcClient);
            rpcClient.setTenant(getTenant());
            rpcClient.clientAbilities(initAbilities());
            rpcClient.start();
        }
        return rpcClient;
    }
}
 
// ConfigRpcTransportClient # initRpcClientHandler
// 初始化ConfigChangeNotifyRequest处理逻辑如下
rpcClientInner.registerServerRequestHandler((request) -> {
    if (request instanceof ConfigChangeNotifyRequest) {
        ConfigChangeNotifyRequest configChangeNotifyRequest = (ConfigChangeNotifyRequest) request;
        // ......
        String groupKey = GroupKey
                .getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(),
                        configChangeNotifyRequest.getTenant());
 
        // 获取 CacheData
        CacheData cacheData = cacheMap.get().get(groupKey);
        if (cacheData != null) {
            synchronized (cacheData) {
                // 设置服务器同步标志
                cacheData.getLastModifiedTs().set(System.currentTimeMillis());
                cacheData.setSyncWithServer(false);
                // 立即触发该CacheData的同步配置操作
                notifyListenConfig();
            }
 
        }
        return new ConfigChangeNotifyResponse();
    }
    return null;
});

服务端变更通知

入口

配置变更,是在 Nacos Service 的 Web 页面进行操作的,调用POST /v1/cs/configs接口。

该接口主要逻辑:

  • 更新配置内容
  • 发送配置变更事件
persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false);
ConfigChangePublisher.notifyConfigChange(
        new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));

ConfigDataChangeEvent 监听器

AsyncNotifyService 在初始化时,向事件通知中心添加了监听器。

NotifyCenter.registerSubscriber(new Subscriber() {
 
    @Override
    public void onEvent(Event event) {
        // Generate ConfigDataChangeEvent concurrently
        if (event instanceof ConfigDataChangeEvent) {
            ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
            // ......
            // In fact, any type of queue here can be
            Queue<NotifySingleRpcTask> rpcQueue = new LinkedList<NotifySingleRpcTask>();
            // ....省略代码:把参数包装为 NotifySingleRpcTask 添加到 rpcQueue
            // 把 rpcQueue 包装为 AsyncRpcTask
            if (!rpcQueue.isEmpty()) {
                ConfigExecutor.executeAsyncNotify(new AsyncRpcTask(rpcQueue));
            }
 
        }
    }
 
    @Override
    public Class<? extends Event> subscribeType() {
        return ConfigDataChangeEvent.class;
    }
});

AsyncRpcTask异步任务

AsyncRpcTask #run()

@Override
public void run() {
    while (!queue.isEmpty()) {
        NotifySingleRpcTask task = queue.poll();
        ConfigChangeClusterSyncRequest syncRequest = new ConfigChangeClusterSyncRequest();
        // ... 代码省略:组装 syncRequest 参数。
        if (memberManager.getSelf().equals(member)) {
            if (syncRequest.isBeta()) {
                dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
                        syncRequest.getLastModified(), NetUtils.localIP(), true);
            } else {
                // EmbeddedDumpService.dump()
                dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
                        syncRequest.getTag(), syncRequest.getLastModified(), NetUtils.localIP());
            }
            continue;
        }
      // 以下为 nacos 集群通知,暂时忽略
      // ...
    }
}

接下来继续看 dumpService.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);
    dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, tag, lastModified, handleIp, isBeta));
    DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey);
}

DumpTask异步任务

该异步任务由 TaskManager执行,其在EmbeddedDumpService初始化时,被创建。

实际由TaskManager的父类NacosDelayTaskExecuteEngine执行processTasks()方法:

protected void processTasks() {
        Collection<Object> keys = getAllTaskKeys();
        for (Object taskKey : keys) {
            AbstractDelayTask task = removeTask(taskKey);
            // ....
            NacosTaskProcessor processor = getProcessor(taskKey);
            // ....
            try {
                // ReAdd task if process failed
                if (!processor.process(task)) {
                    retryFailedTask(taskKey, task);
                }
            } catch (Throwable e) {
                getEngineLog().error("Nacos task execute error : " + e.toString(), e);
                retryFailedTask(taskKey, task);
            }
        }
    }

实际上就是根据 taskKey 取到对应的NacosTaskProcessor执行process()方法。

此处 DumpTask 对应的是 DumpProcessor

public boolean process(NacosTask task) {
    final PersistService persistService = dumpService.getPersistService();
    DumpTask dumpTask = (DumpTask) task;
    // ... 省略代码:对 dumpTask 参数赋值
 
    // 构建 ConfigDumpEvent 事件
    ConfigDumpEvent.ConfigDumpEventBuilder build = ConfigDumpEvent.builder().namespaceId(tenant).dataId(dataId)
            .group(group).isBeta(isBeta).tag(tag).lastModifiedTs(lastModified).handleIp(handleIp);
 
    // ... 省略代码:对 build 参数赋值
    return DumpConfigHandler.configDump(build.build());
}

继续进入DumpConfigHandler.configDump(build.build())

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();
    // .... 省略代码
    if (StringUtils.isBlank(event.getTag())) {
        // ... 省略代码
        boolean result;
        if (!event.isRemove()) {
            // 保存配置文件并更新缓存中的 md5 值
            result = ConfigCacheService.dump(dataId, group, namespaceId, content, lastModified, type);
            // ...
        } // ... 省略 else
        return result;
    }
}

继续进入ConfigCacheService.dump():

public static boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs,
        String type) {
    // ...
    try {
        final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
 
        if (md5.equals(ConfigCacheService.getContentMd5(groupKey))) {
            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);
        }
        updateMd5(groupKey, md5, lastModifiedTs);
        return true;
    } // ...
 
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));
    }
} 

LocalDataChangeEvent 监听器

RpcConfigChangeNotifier 是 LocalDataChangeEvent 的监听器:

@Override
public void onEvent(LocalDataChangeEvent event) {
    String groupKey = event.groupKey;
    boolean isBeta = event.isBeta;
    List<String> betaIps = event.betaIps;
    String[] strings = GroupKey.parseKey(groupKey);
    String dataId = strings[0];
    String group = strings[1];
    String tenant = strings.length > 2 ? strings[2] : "";
    String tag = event.tag;
    configDataChanged(groupKey, dataId, group, tenant, isBeta, betaIps, tag);
}
 
public void configDataChanged(String groupKey, String dataId, String group, String tenant, boolean isBeta,
        List<String> betaIps, String tag) {
    // 获取变更配置对应的客户端
    Set<String> listeners = configChangeListenContext.getListeners(groupKey);
    // ....
    int notifyClientCount = 0;
    for (final String client : listeners) {
        // 根据客户端获取连接
        Connection connection = connectionManager.getConnection(client);
        // ...
        // 构造请求
        ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant);
        // 构造任务
        RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp,
                connection.getMetaInfo().getAppName());
        // 发送请求
        push(rpcPushRetryTask);
        notifyClientCount++;
    }
    Loggers.REMOTE_PUSH.info("push [{}] clients ,groupKey=[{}]", notifyClientCount, groupKey);
}
 
private void push(RpcPushTask retryTask) {
    ConfigChangeNotifyRequest notifyRequest = retryTask.notifyRequest;
    if (retryTask.isOverTimes()) {
        // 请求超时,移除该连接
        connectionManager.unregister(retryTask.connectionId);
    } else if (connectionManager.getConnection(retryTask.connectionId) != null) {
        // first time :delay 0s; sencond time:delay 2s  ;third time :delay 4s
        // 重试机制
        ConfigExecutor.getClientConfigNotifierServiceExecutor()
                .schedule(retryTask, retryTask.tryTimes * 2, TimeUnit.SECONDS);
    } else {
        // client is already offline,ingnore task.
    }
}

发送请求

发送请求的逻辑在RpcPushTask # run()中:

public void run() {
    tryTimes++;
    if (!tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH, connectionId, clientIp)) {
        // 如果 tps 受限,自旋等待 tps 控制放开。
        push(this);
    } else {
        // 发送请求
        rpcPushService.pushWithCallback(connectionId, notifyRequest, new AbstractPushCallBack(3000L) {
            @Override
            public void onSuccess() {
                tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_SUCCESS, connectionId, clientIp);
            }
 
            @Override
            public void onFail(Throwable e) {
                tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_FAIL, connectionId, clientIp);
                Loggers.REMOTE_PUSH.warn("Push fail", e);
                push(RpcPushTask.this);
            }
 
        }, ConfigExecutor.getClientConfigNotifierServiceExecutor());
    }
}

小结

Nacos 2.x 中弃用了 长轮询 模式,采用 长连接 模式。

  • Nacos Config Client 每 5 分钟进行一次全量比对。
  • Nacos Config Server 有配置发生变化时,发布LocalDataChangeEvent,监听器监听到该事件,即开始向 Nacos Config Client 发送 ConfigChangeNotifyRequest。Nacos Config Client 感到到有配置发生变化,向 Nacos Config Server 发送 ConfigQueryRequest 请求最新配置内容。

Nacos 中大量使用了异步任务与事件机制,初次来看理解有点难度。这篇笔记内容前前后后花费了好几天时间,真让人头疼。