上篇说到了服务器地址的获取和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);
}
}
再看下新增,修改的逻辑
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
双工链路的推送。
回顾下AsyncRpcTask
的run
方法
@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.
}
}
}
在服务节点间的同步有三个主要的逻辑:
节点不健康的情况,采用异步定时的去执行,但是这个定时
并不是严格意义的定时
,因为他会有个延迟的过程,他会随着失败次数的增加,延迟不断加大,不过当达到最大失败次数后,就不会再增加,以一个固定的时间去触发。最大时间间隔是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;
}
如果成员节点是不支持长连接的,那就是以前的老版本,1.X的模式,需要用到http
发送同步请求
如果是支持长连接,是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
源码分析系列进行总结以及写自己的一些感悟,敬请期待。