前言
1.4版本nacos使用Http短连接+长轮询的方式,客户端发起http请求,服务端hold住请求,当配置变更时响应客户端,超时时间30s。
| 端口 | 端口偏移量 | 类型 | 实现 | 描述 |
|---|---|---|---|---|
| 8848 | 0 | HTTP | SpringBoot | 控制台访问、集群通讯、客户端通讯。 |
| 7848 | -1000 | gRPC | JRaftServer | JRaft服务。 |
2.0版本nacos用gRPC长连接代替了http短连接长轮询。配置同步采用推拉结合的方式。
-
拉:客户端每隔一段时间,会向服务端重新注册监听,同时如果有配置变更会更新客户端本地配置。
-
推:服务端在配置变更后,会通知监听在这个配置上的客户端,客户端会重新注册监听并更新配置。
| 端口 | 端口偏移量 | 类型 | 实现 | 描述 |
|---|---|---|---|---|
| 8848 | 0 | HTTP | SpringBoot | 控制台访问。 |
| 7848 | -1000 | gRPC | JRaftServer | JRaft服务。 |
| 9848 | 1000 | gRPC | GrpcSdkServer | 处理Nacos客户端请求。 |
| 9849 | 1001 | gRPC | GrpcClusterServer | Nacos服务端集群通讯。 |
一、回顾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废弃)
二、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、注册监听配置同步
对于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中。
前者映射关系,只是为了控制台展示;
后者映射关系,主要为了之后通过变更的配置项,可以找到订阅的客户端进行通知。
@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版本,服务端通过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通知客户端配置变更。