前言
Nacos 2.x 中抛弃了长轮询模式,改用长连接进行配置同步。
这次就来探探 Nacos 配置中心是如何与Spring Boot 同步变更的。
前置知识
我们在 《『Naocs 2.x』(三) Nacos 服务注册逻辑及通信过程 》的 GRPC调用过程
一小节中,分析过 Nacos Server 与 Nacos Client 的请求与响应过程。
简单地回顾一下,就是根据 Request
的具体类型不同, Nacoe Server 获取到对应的RequestHandler
,进行业务处理,最后把处理结果封装为Response
返回。
我们来看一下 Nacos Cofnig 中 Request 的层级结构 ( 只保留了与此节强相关的子类 ) :
-
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 中大量使用了异步任务与事件机制,初次来看理解有点难度。这篇笔记内容前前后后花费了好几天时间,真让人头疼。