阅读 329

Nacos源码(五)2.0配置中心

前言

1.4版本nacos使用Http短连接+长轮询的方式,客户端发起http请求,服务端hold住请求,当配置变更时响应客户端,超时时间30s。

端口端口偏移量类型实现描述
88480HTTPSpringBoot控制台访问、集群通讯、客户端通讯。
7848-1000gRPCJRaftServerJRaft服务。

2.0版本nacos用gRPC长连接代替了http短连接长轮询。配置同步采用推拉结合的方式。

  • 拉:客户端每隔一段时间,会向服务端重新注册监听,同时如果有配置变更会更新客户端本地配置。

  • 推:服务端在配置变更后,会通知监听在这个配置上的客户端,客户端会重新注册监听并更新配置。

端口端口偏移量类型实现描述
88480HTTPSpringBoot控制台访问。
7848-1000gRPCJRaftServerJRaft服务。
98481000gRPCGrpcSdkServer处理Nacos客户端请求。
98491001gRPCGrpcClusterServerNacos服务端集群通讯。

一、回顾1.4长轮询

回顾1.4版本,在构造ClientWorker时,启动了两个线程服务

  • 一个线程服务,用于检测并提交LongPollingRunnable长轮询任务(2.0废弃)

构造ClientWorker时,会开启一个定时任务,每隔10ms执行一次checkConfigInfo方法。checkConfigInfo判断当前CacheData数量,是否要开启一个长轮询任务。判断依据是,当前长轮询任务数量 < Math.ceil(cacheMap大小 / 3000),则开启一个新的长轮询任务。配置文件不多的情况下,最多也就一个长轮询任务。

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
        final Properties properties) {
     // ...
    // 检测并提交LongPollingRunnable到this.executorService
    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);
}
复制代码
  • LongPollingRunnable长轮询任务负责向服务端发起30s长轮询,服务端检测到配置变更时,推送给客户端。(2.0废弃)

客户端长轮询.png

二、2.0客户端监听配置

2.0之后配置监听不再采用http短连接长轮询的方式,而是改用长连接。

ClientWorker构造时,创建了一个ConfigRpcTransportClient,注入一个线程服务,并执行了它的start方法。

public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
        final Properties properties) throws NacosException {
    this.configFilterChainManager = configFilterChainManager;
    init(properties);
    agent = new ConfigRpcTransportClient(properties, serverListManager);
    ScheduledExecutorService 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");
                    t.setDaemon(true);
                    return t;
                }
            });
    agent.setExecutor(executorService);
    agent.start();
}
复制代码

ConfigRpcTransportClient的start方法里利用外部传入的线程服务,无限循环跑一个配置同步任务

// **ConfigRpcTransportClient**
@Override
public void startInternal() throws NacosException {
    executor.schedule(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    // 等待唤醒,或等待5s
                    listenExecutebell.poll(5L, TimeUnit.SECONDS);
                    // 配置监听
                    executeConfigListen();
                } catch (Exception e) {
                    LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
                }
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);
}
复制代码

先来看一下ConfigRpcTransportClient的属性。

public class ConfigRpcTransportClient extends ConfigTransportClient {
    private final BlockingQueue<Object> listenExecutebell = new ArrayBlockingQueue<Object>(1);
    private Object bellItem = new Object();
    private long lastAllSyncTime = System.currentTimeMillis();
}
复制代码
  • BlockingQueue listenExecutebell:容量为1的阻塞队列,当生产元素时,会唤醒阻塞等待的配置同步任务。
  • Object bellItem:一个普通的Object,用于放入阻塞队列。
  • long lastAllSyncTime:上次全量同步的时间戳。

1、CacheMap分组

接下来看一下这个配置同步任务的处理。当阻塞队列被放入元素或5s超时,执行executeConfigListen方法。

// ClientWorker.ConfigRpcTransportClient
/**
* groupKey -> cacheData.
*/
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(
            new HashMap<String, CacheData>());
@Override
public void executeConfigListen() {
    // taskId - CacheData(有listener)
    Map<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);
    // taskId - CacheData(无listener)
    Map<String, List<CacheData>> removeListenCachesMap = new HashMap<String, List<CacheData>>(16);
    long now = System.currentTimeMillis();
    // 当前时间 - 上次全量同步时间 >= 5min,需要执行全量同步
    boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL;
    for (CacheData cache : cacheMap.get().values()) {
        synchronized (cache) {
            if (cache.isSyncWithServer()) {
                // 1. 对于已经同步的配置,再次校验cacheData和listener的md5一致性
                cache.checkListenerMd5();
                // 如果距离上次全量同步时间小于5分钟,不会对配置做任何同步处理
                if (!needAllSync) {
                    continue;
                }
            }

            // 统计有listener的配置
            if (!CollectionUtils.isEmpty(cache.getListeners())) {
                if (!cache.isUseLocalConfigInfo()) {
                    List<CacheData> cacheDatas = listenCachesMap.get(String.valueOf(cache.getTaskId()));
                    if (cacheDatas == null) {
                        cacheDatas = new LinkedList<CacheData>();
                        listenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);
                    }
                    cacheDatas.add(cache);
                }
            }
            // 统计没有listener的配置
            else if (CollectionUtils.isEmpty(cache.getListeners())) {
                if (!cache.isUseLocalConfigInfo()) {
                    List<CacheData> cacheDatas = removeListenCachesMap.get(String.valueOf(cache.getTaskId()));
                    if (cacheDatas == null) {
                        cacheDatas = new LinkedList<CacheData>();
                        removeListenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);
                    }
                    cacheDatas.add(cache);
                }
            }
        }

    }
    boolean hasChangedKeys = false;
    // 2. 处理有Listener的cacheData,发起listen请求
    if (!listenCachesMap.isEmpty()) {
        // ...
    }
    // 3. 移除监听
    if (!removeListenCachesMap.isEmpty()) {
        // ...
    }
    if (needAllSync) {
        lastAllSyncTime = now;
    }
    // 4. 如果有配置发生变更,再次立即触发executeConfigListen
    if (hasChangedKeys) {
        notifyListenConfig();
    }
}
复制代码

根据是否存在Listener,将cacheMap中的CacheData分为两个Map,一个有Listener的,一个是没有Listener的。对于前者需要注册监听,对于后者需要移除监听。(cacheMap是由configService.addListener用户注册而产生的,见第一章)

// ClientWorker.ConfigRpcTransportClient#executeConfigListen
// taskId - CacheData(有listener)
Map<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);
// taskId - CacheData(无listener)
Map<String, List<CacheData>> removeListenCachesMap = new HashMap<String, List<CacheData>>(16);
复制代码

此外,全量同步5分钟内(needAllSync=fasle),CacheData.isSyncWithServer=true的情况下,这部分CacheData是不会参与注册监听逻辑的,不会统计到两个Map中,此时只会校验CacheData.md5与Listener.md5的一致性,触发Listener。

// 当前时间 - 上次全量同步时间 >= 5min,需要执行全量同步
boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL;
for (CacheData cache : cacheMap.get().values()) {
  synchronized (cache) {
    if (cache.isSyncWithServer()) {
      // 1. 对于已经同步的配置,再次校验cacheData和listener的md5一致性
      cache.checkListenerMd5();
      // 如果距离上次全量同步时间小于5分钟,不会对配置做任何同步处理
      if (!needAllSync) {
        continue;
      }
    }
    // ...
  }
  // ...
复制代码

这个isSyncWithServer代表server.md5==client.CacheData.md5==client.listeners.md5,表示CacheData对应配置已经同步。也就是说,全量同步5分钟后,所有CacheData都要重新注册监听

2、注册监听配置同步

注册监听.png

对于listenCachesMap,调用gRPC接口注册监听。

// ClientWorker.ConfigRpcTransportClient#executeConfigListen
// 2. 处理有Listener的cacheData,发起listen请求
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 {
        // 每个taskId对应一个RpcClient #1
        RpcClient rpcClient = ensureRpcClient(taskId);
        // gRPC 注册监听 #2
        ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(
                rpcClient, configChangeListenRequest);
        // gRPC返回结果处理 #3
        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();
                    // 查询最新配置,落snapshot,刷新CacheData中的配置
                    // 对于非初始化的配置,通知所有监听器
                    refreshContentAndCheck(changeKey, !isInitializing);
                }

            }
            // 对于没有配置变化的配置,设置syncWithServer=true,减少同步配置的开销
            for (CacheData cacheData : listenCaches) {
              String groupKey = GroupKey
                .getKeyTenant(cacheData.dataId, cacheData.group, cacheData.getTenant());
              if (!changeKeys.contains(groupKey)) {
                synchronized (cacheData) {
                  if (!cacheData.getListeners().isEmpty()) {
                    cacheData.setSyncWithServer(true);
                    continue;
                  }
                }
              }
              // 设置所有cacheData为非初始化
              cacheData.setInitializing(false);
            }
        }
        
    } catch (Exception e) {
            //ignore
    }
}
复制代码

ensureRpcClient方法,确保每个taskId公用一个RpcClient,而1.4是每个taskId一个LongPollingRunnable任务。

// ClientWorker.ConfigRpcTransportClient#ensureRpcClient
// 每个taskId对应一个RpcClient #1
private synchronized RpcClient ensureRpcClient(String taskId) throws NacosException {
    Map<String, String> labels = getLabels();
    Map<String, String> newLabels = new HashMap<String, String>(labels);
    newLabels.put("taskId", taskId);
    // 一个ClientWorker一个taskId 对应一个 RpcClient
    // 每个RpcClient有自己的线程池
    RpcClient rpcClient = RpcClientFactory
            .createClient("config-" + taskId + "-" + uuid, getConnectionType(), newLabels);
    if (rpcClient.isWaitInitiated()) {
        initRpcClientHandler(rpcClient);
        rpcClient.setTenant(getTenant());
        rpcClient.clientAbilities(initAbilities());
        rpcClient.start();
    }
    return rpcClient;
}
// RpcClientFactory#createClient
public static RpcClient createClient(String clientName, ConnectionType connectionType, Map<String, String> labels) {
  String clientNameInner = clientName;
  synchronized (clientMap) {
    if (clientMap.get(clientNameInner) == null) {
      RpcClient moduleClient = null;
      if (ConnectionType.GRPC.equals(connectionType)) {
        moduleClient = new GrpcSdkClient(clientNameInner);
      }
      moduleClient.labels(labels);
      clientMap.put(clientNameInner, moduleClient);
      return moduleClient;
    }
    return clientMap.get(clientNameInner);
  }
}
复制代码

接下来请求Nacos服务端9848端口,注册监听,这里不会像1.4Server端会hold住请求,这里会立即返回

// gRPC 注册监听 #2
ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(rpcClient, configChangeListenRequest);
复制代码

服务端ConfigChangeBatchListenResponse会返回md5已经发生变更的配置项,refreshContentAndCheck方法会查询服务端最新配置,并更新snapshot文件和CacheData。这里getServerConfig与1.4一样,只不过换成了gRPC;checkListenerMd5也与1.4一样,比对CacheData中的md5和Listener中的md5,如果发生变化会触发监听。

// gRPC返回结果处理 #3
private void refreshContentAndCheck(String groupKey, boolean notify) {
    if (cacheMap.get() != null && cacheMap.get().containsKey(groupKey)) {
        CacheData cache = cacheMap.get().get(groupKey);
        refreshContentAndCheck(cache, notify);
    }
}

// 刷新CacheData中的配置,并通知监听器
private void refreshContentAndCheck(CacheData cacheData, boolean notify) {
    try {
        String[] ct = getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L, notify);
        cacheData.setContent(ct[0]);
        if (null != ct[1]) {
            cacheData.setType(ct[1]);
        }
        cacheData.checkListenerMd5();
    } catch (Exception e) {
        LOGGER.error();
    }
}
复制代码

由于5s后阻塞队列会超时,会重新触发executeConfigListen方法走上述逻辑,为了减少频繁配置全量同步带来的开销,这里对于服务端与客户端配置不存在差异的CacheData标记为isSyncWithServer=true,表示客户端与服务端配置已经一致,5分钟内不需要参与全量同步。

3、移除监听

对于没有监听器的CacheData,这里会调用和注册监听一样的gRPC接口注销监听,区别是入参ConfigBatchListenRequest里的listen参数为false,表示注销监听

// ClientWorker.ConfigRpcTransportClient#executeConfigListen
for (Map.Entry<String, List<CacheData>> entry : removeListenCachesMap.entrySet()) {
    String taskId = entry.getKey();
    List<CacheData> removeListenCaches = entry.getValue();
    ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(removeListenCaches);
    configChangeListenRequest.setListen(false);
    try {
        RpcClient rpcClient = ensureRpcClient(taskId);
        boolean removeSuccess = unListenConfigChange(rpcClient, configChangeListenRequest);
        if (removeSuccess) {
            for (CacheData cacheData : removeListenCaches) {
                synchronized (cacheData) {
                    if (cacheData.getListeners().isEmpty()) {
                        ClientWorker.this
                                .removeCache(cacheData.dataId, cacheData.group, cacheData.tenant);
                    }
                }
            }
        }

    } catch (Exception e) {
        LOGGER.error("async remove listen config change error ", e);
    }
}
复制代码

4、唤醒监听任务

通过往阻塞队列listenExecutebell中放入元素,可以唤醒配置同步任务。

// ClientWorker.ConfigRpcTransportClient#notifyListenConfig
@Override
public void notifyListenConfig() {
    listenExecutebell.offer(bellItem);
}
复制代码

当listenExecutebell阻塞队列中有元素时,配置同步任务会被唤醒。那么什么时候会唤醒配置同步任务呢?

场景一:配置同步任务执行中,服务端监听响应ConfigChangeBatchListenResponse中包含变更的配置。

// ClientWorker.ConfigRpcTransportClient#executeConfigListen
// 4. 如果有配置发生变更,再次立即触发executeConfigListen
public void executeConfigListen() {
    // ...
    if (hasChangedKeys) {
        notifyListenConfig();
    }
}
复制代码

场景二:新增Listener时,需要立即同步配置。注意也会把整个CacheData标记为syncWithServer=false,强制执行配置同步,保证双端数据一致。

// ClientWorker
public void addListeners(String dataId, String group, List<? extends Listener> listeners) {
    group = null2defaultGroup(group);
    CacheData cache = addCacheDataIfAbsent(dataId, group);
    synchronized (cache) {
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
        cache.setSyncWithServer(false);
        agent.notifyListenConfig();

    }
}
复制代码

场景三服务端发送ConfigChangeNotifyRequest请求,表示某个配置发生变更,需要客户端执行配置同步。

// ClientWorker.ConfigRpcTransportClient#initRpcClientHandler
private void initRpcClientHandler(final RpcClient rpcClientInner) {
    rpcClientInner.registerServerRequestHandler((request) -> {
        if (request instanceof ConfigChangeNotifyRequest) {
            ConfigChangeNotifyRequest configChangeNotifyRequest = (ConfigChangeNotifyRequest) request;
            String groupKey = GroupKey
                    .getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(),
                            configChangeNotifyRequest.getTenant());

            CacheData cacheData = cacheMap.get().get(groupKey);
            if (cacheData != null) {
                cacheData.setSyncWithServer(false);
                notifyListenConfig();
            }
            return new ConfigChangeNotifyResponse();
        }
        return null;
    });
}
复制代码

其他场景不一一列举了,总之当客户端需要感知配置变更时,就会唤醒配置同步任务。

三、2.0服务端处理监听请求

服务端ConfigChangeBatchListenRequestHandler负责处理ConfigBatchListenRequest。

@Component
public class ConfigChangeBatchListenRequestHandler extends RequestHandler<ConfigBatchListenRequest, ConfigChangeBatchListenResponse> {
    @Autowired
    private ConfigChangeListenContext configChangeListenContext;

    @Override
    @TpsControl(pointName = "ConfigListen")
    @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
    public ConfigChangeBatchListenResponse handle(ConfigBatchListenRequest configChangeListenRequest, RequestMeta meta) throws NacosException {
        String connectionId = StringPool.get(meta.getConnectionId());
        String tag = configChangeListenRequest.getHeader(Constants.VIPSERVER_TAG);

        ConfigChangeBatchListenResponse configChangeBatchListenResponse = new ConfigChangeBatchListenResponse();
        for (ConfigBatchListenRequest.ConfigListenContext listenContext : configChangeListenRequest.getConfigListenContexts()) {
            String groupKey = GroupKey2.getKey(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant());
            groupKey = StringPool.get(groupKey);

            String md5 = StringPool.get(listenContext.getMd5());

            if (configChangeListenRequest.isListen()) {
                // 把connectionId -> key 和 key -> md5的关系保存在服务端
                configChangeListenContext.addListen(groupKey, md5, connectionId);
                // 校验配置是否已经发生变更,如果是的话,将变更的groupKey加入响应报文
                boolean isUptoDate = ConfigCacheService.isUptodate(groupKey, md5, meta.getClientIp(), tag);
                if (!isUptoDate) {
                    configChangeBatchListenResponse.addChangeConfig(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant());
                }
            } else {
                configChangeListenContext.removeListen(groupKey, connectionId);
            }
        }
        return configChangeBatchListenResponse;
    }
}
复制代码

对于注册监听请求,长连接(connectionId)<->groupKey<->客户端md5的映射关系,和groupKey<->connectionId集合的映射关系保存到ConfigChangeListenContext中。

前者映射关系,只是为了控制台展示; 监听查询.png

后者映射关系,主要为了之后通过变更的配置项,可以找到订阅的客户端进行通知。

@Component
public class ConfigChangeListenContext {
    /**
     * groupKey-> connection set.
     */
    private ConcurrentHashMap<String, HashSet<String>> groupKeyContext = new ConcurrentHashMap<String, HashSet<String>>();
    
    /**
     * connectionId-> group key set.
     */
    private ConcurrentHashMap<String, HashMap<String, String>> connectionIdContext = new ConcurrentHashMap<String, HashMap<String, String>>();
    public synchronized void addListen(String groupKey, String md5, String connectionId) {
        // 1.add groupKeyContext
        Set<String> listenClients = groupKeyContext.get(groupKey);
        if (listenClients == null) {
            groupKeyContext.putIfAbsent(groupKey, new HashSet<String>());
            listenClients = groupKeyContext.get(groupKey);
        }
        listenClients.add(connectionId);
        
        // 2.add connectionIdContext
        HashMap<String, String> groupKeys = connectionIdContext.get(connectionId);
        if (groupKeys == null) {
            connectionIdContext.putIfAbsent(connectionId, new HashMap<String, String>(16));
            groupKeys = connectionIdContext.get(connectionId);
        }
        groupKeys.put(groupKey, md5);
    }
}
复制代码

此外,和1.4长轮询逻辑一样,如果发现客户端md5与服务端md5不一致的时候,服务端会返回不一致的groupKey。

// ConfigChangeBatchListenRequestHandler.handle
// 校验配置是否已经发生变更,如果是的话,将变更的groupKey加入响应报文
boolean isUptoDate = ConfigCacheService.isUptodate(groupKey, md5, meta.getClientIp(), tag);
if (!isUptoDate) {
  configChangeBatchListenResponse.addChangeConfig(listenContext.getDataId(), listenContext.getGroup(), listenContext.getTenant());
}
复制代码

对于移除监听,服务端仅仅是移除了上面ConfigChangeListenContext中保存的映射关系。

四、2.0服务端配置发布

Nacos服务端的本地配置和内存配置都更新完成后,会发布LocalDataChangeEvent事件。

1.4版本服务端配置更新流程.png

1.4版本,服务端通过LongPollingService处理LocalDataChangeEvent;

2.0版本Http长轮询的逻辑还在,没有删除,但是2.0版本客户端,对应服务端的LocalDataChangeEvent事件处理器是RpcConfigChangeNotifier

@Component(value = "rpcConfigChangeNotifier")
public class RpcConfigChangeNotifier extends Subscriber<LocalDataChangeEvent> {
    public RpcConfigChangeNotifier() {
        NotifyCenter.registerSubscriber(this);
    }
    @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);
    }
}
复制代码

configDataChanged从监听上下文ConfigChangeListenContext中获取groupKey对应的所有监听connectionId,再通过ConnectionManager获取connectionId对应Connection gRPC长连接。最后构建ConfigChangeNotifyRequest同步配置请求参数,提交RpcPushTask到其他线程服务处理,不阻塞其他事件处理。

// RpcConfigChangeNotifier
public void configDataChanged(String groupKey, String dataId, String group, String tenant, boolean isBeta,
        List<String> betaIps, String tag) {
    // 从注册监听的上下文中,获取groupKey对应的所有监听客户端connectionId
    Set<String> listeners = configChangeListenContext.getListeners(groupKey);
    if (!CollectionUtils.isEmpty(listeners)) {
        for (final String client : listeners) {
            // 通过connectionId获取实际gRPC长连接
            Connection connection = connectionManager.getConnection(client);
            if (connection == null) {
                continue;
            }
            // ...
            // 构建同步配置请求参数
            ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant);
            // 为了不阻塞其他事件处理,这里提交一个任务到其他线程池处理
            RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp,
                    connection.getMetaInfo().getAppName());
            push(rpcPushRetryTask);
        }
    }
}
复制代码

push方法里面分为三个分支。

如果Task已经超过重试次数50次,ConnectionManager会关闭对应长连接;

如果ConnectionManager里connectionId对应连接还存在,正常提交Task,每次任务执行失败后会做延迟补偿,延迟时间 = 失败次数 * 2 秒;

如果连接已经不存在,不做任何处理。

// RpcConfigChangeNotifier
private void push(RpcPushTask retryTask) {
    ConfigChangeNotifyRequest notifyRequest = retryTask.notifyRequest;
    if (retryTask.isOverTimes()) { // 重试超过50次
        connectionManager.unregister(retryTask.connectionId);
        return;
    } else if (connectionManager.getConnection(retryTask.connectionId) != null) {
        ConfigExecutor.getClientConfigNotifierServiceExecutor()
                .schedule(retryTask, retryTask.tryTimes * 2, TimeUnit.SECONDS);
    } else {
        // client is already offline,ingnore task.
    }

}
复制代码

RpcPushTask是RpcConfigChangeNotifier的内部类,run方法主要是处理容错逻辑

  • 如果限流,重新提交Task
  • 如果RPC失败,重新提交Task
class RpcPushTask implements Runnable {
    @Override
    public void run() {
        tryTimes++;
        // 如果限流,重新提交Task
        if (!tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH, connectionId, clientIp)) {
            push(this);
        } else {
            // gRPC请求客户端 发送ConfigChangeNotifyRequest
            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);
                    // 如果失败,重新提交Task
                    push(RpcPushTask.this);
                }
            }, ConfigExecutor.getClientConfigNotifierServiceExecutor());

        }
    }
}
复制代码

对应客户端的逻辑见2.4唤醒监听任务的场景三。

总结

2.x配置中心主要的改动在于引进长连接代替了短连接长轮询

客户端改动点:

改动点一:

1.x每3000个CacheData,客户端会开启一个LongPollingRunnable长轮询任务;2.x每3000个CacheData,客户端会开启一个RpcClient,每个RpcClient与服务端建立一个长连接。

改动点二:

客户端增加定时全量拉取配置的逻辑。

在1.x中,Nacos配置中心通过长轮询的方式更新客户端配置,对于客户端来说只有配置推送;

在2.x中支持客户端定时同步配置,所以2.x属于推拉结合的方式。

拉:每5分钟,客户端会对全量CacheData发起配置监听请求ConfigBatchListenRequest,如果配置md5发生变更,会同步收到变更配置项,发起ConfigQuery请求查询实时配置。

:服务端配置变更,会发送ConfigChangeNotifyRequest请求给与当前节点建立长连接的客户端通知配置变更项。

服务端改动点:

改动点一:

由于2.x使用长连接代替长轮询,监听请求ConfigBatchListenRequest不会被服务端hold住,会立即返回。服务端只是将监听关系保存在内存中,方便后续通知。

groupKey和connectionId的映射关系,方便后续通过变更配置项找到对应客户端长连接;connectionId和groupKey的映射关系,只是为了控制台展示。这些关系保存在服务端的ConfigChangeListenContext单例中。

改动点二

对应改动点一,1.x需要通过groupKey找到仍然在进行长轮询的客户端AsyncContext;2.x是通过groupKey找到connectionId,再通过connectionId找到长连接,发送ConfigChangeNotifyRequest通知客户端配置变更。

文章分类
后端
文章标签