Recovery
1.整体流程
入口:org.apache.iotdb.db.service.IoTDB#setUp
在服务启动时,注册完基础服务后,开始恢复数据,并进行一些初始化工作,供后续读写操作使用。
... ...
// 注册RPC服务
if (IoTDBDescriptor.getInstance().getConfig().isEnableRpcService()) {
registerManager.register(RPCService.getInstance());
}
... ...
// 初始化StorageEngine,等待恢复完成
while (!StorageEngine.getInstance().isAllSgReady()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
初始化StorageEngine,开始恢复工作。
private StorageEngine() {
// 该目录为: /data/system/schema/storage_groups
systemDir = FilePathUtils.regularizePath(config.getSystemDir()) + "storage_groups";
// 如果开启分区,则需要从配置中获取指定的分区键
if (!enablePartition) {
// 未开启则不分区
timePartitionInterval = Long.MAX_VALUE;
} else {
initTimePartition();
}
FileUtils.forceMkdir(SystemFileFactory.INSTANCE.getFile(systemDir));
// 升级相关:主要为记录的文件更新,检查是否有老版本的数据
UpgradeUtils.recoverUpgrade();
// 开始恢复
// 将isAllSgReady置为false,Begin-Recovery-Pool线程提交recoverAllSgs恢复任务
recover();
}
恢复工作主要为初始化VirtualStorageGroupManager,通过vsgm获取各分区下的sgp进行读写。
每个StorageGroup对应一个VirtualStorageGroupManager,而一个VirtualStorageGroupManager管理多个VirtualStorageGroup,每个VirtualStorageGroup对应一个StorageGroupProcessor
// 恢复每个storageGroup
private void recoverStorageGroupProcessor(List<Future<Void>> futures) {
// 从元数据MTree中获取所有storageGroupNode
List<StorageGroupMNode> sgNodes = IoTDB.metaManager.getAllStorageGroupNodes();
// 便利sg node
for (StorageGroupMNode storageGroup : sgNodes) {
futures.add(
recoveryThreadPool.submit(
() -> {
// 为当前的sg初始化vsgm
VirtualStorageGroupManager virtualStorageGroupManager =
processorMap.computeIfAbsent(
storageGroup.getPartialPath(), id -> new VirtualStorageGroupManager());
// 恢复该vsgm
virtualStorageGroupManager.recover(storageGroup);}));
}
}
// vsgm开始恢复
public void recover(StorageGroupMNode storageGroupMNode) {
// 按partitioner逐个遍历
// 这里的partitioner取自参数virtual_storage_group_num,即一个sg对应几个vsg
// 对应数据目录/data/sequence/${sg_name}/${0~N},参数默认为1
for (int i = 0; i < partitioner.getPartitionCount(); i++) {
int cur = i;
Thread recoverThread =new Thread(new Runnable() {
public void run() {
StorageGroupProcessor processor = null;
// 为当前的vst初始化一个sgp
processor =StorageEngine.getInstance()
.buildNewStorageGroupProcessor(
storageGroupMNode.getPartialPath(),
storageGroupMNode,
String.valueOf(cur));
// 记录该processor到对应的partitioner id
virtualStorageGroupProcessor[cur] = processor;
}
});
threadList.add(recoverThread);
recoverThread.start();
}
}
在buildNewStorageGroupProcessor进行初始化sgp时,主要分为三部分:tsFile数据文件恢复,更新版本文件,合并任务恢复。
综上,恢复任务是在服务启动,并注册基础服务后,开始恢复。恢复过程主要是从元数据MTree中获取storageGroup节点,按节点初始化各sg对应的vsgm,vsgm管理该sg下的所有vsg,并初始化各vsg对应的sgp,通过sgp恢复数据文件,及合并任务。
2. 恢复数据文件
在sgp的recover中,首先恢复数据文件。恢复数据文件时,先恢复seq再恢复unseq;恢复过程中对每个vsg的分区下文件,逐个恢复。
例如,存在文件/data/sequence/root.a/0/123/a.tsfile,该文件对应的sg name为root.a,只有一个vsg,id为0,分区键为123;恢复文件时,以vsg下一个分区为单位,按文件进行恢复。
private void recover() throws StorageGroupProcessorException {
// getAllFiles获取seq目录中,svg及其下一级目录中的所有文件
// 1. 获取svg下的所有文件,如果.temp及.merge对应的文件存在,则直接删除,否则rename
// 例如:存在文件a.tsfile及a.tsfile.merge,则删除a.tsfile.merge,否则将a.tsfile.merge更名为a.tsfile
// 2. 判断vsg下一级的目录,是否为upgrade,是则放入upgrade集合,否则同1操作
// 3. 为每个文件初始化TsFileResource
// 最终返回:seqFile的TsFileResource -> upgradeFile的TsFileResource
Pair<List<TsFileResource>, List<TsFileResource>> seqTsFilesPair =
getAllFiles(DirectoryManager.getInstance().getAllSequenceFileFolders());
// seqFile的TsFileResource
List<TsFileResource> tmpSeqTsFiles = seqTsFilesPair.left;
... ... // unseq与seq相同,省略
// 按分区键切分为:分区键 -> seqFile的TsFileResource
// 切分时,通过文件的绝对路径获取
// 如:/data/sequence/root.a/0/123/a.tsfile,获取的分区键则为123
Map<Long, List<TsFileResource>> partitionTmpSeqTsFiles =
splitResourcesByPartition(tmpSeqTsFiles);
// 按分区进行恢复
for (List<TsFileResource> value : partitionTmpSeqTsFiles.values()) {
recoverTsFiles(value, true);
}
... ... // 版本文件更新,及合并任务的恢复过程
}
分区下的文件,逐个开始恢复;通过文件名判断该文件是否已经merge过,为真且文件完整,则该文件可以直接封口,不再读写;如果文件没有合并过,需要检查该文件是否为该分区下的最后一个文件,并且是否可以继续读写,如果可以继续使用,则初始化该文件的tsFileProcessor,供后续使用,否则文件封口,该文件结束恢复。
// 恢复单个tsfile
private void recoverTsFiles(List<TsFileResource> tsFiles, boolean isSeq) {
for (int i = 0; i < tsFiles.size(); i++) {
// 遍历获取单个的tsfile
TsFileResource tsFileResource = tsFiles.get(i);
// 初始化一个TsFileRecoverPerformer,为后续恢复过程做准备
TsFileRecoverPerformer recoverPerformer = new TsFileRecoverPerformer(...);
RestorableTsFileIOWriter writer;
// 如果该文件merge过,说明文件可以正常封口,不需要继续读写,也不需要replay wal
// 文件名结构:${timestamp}-${version}-${merge_count}-{unseq_merge_count}.tsfile
if (TsFileResource.getMergeLevel(tsFileResource.getTsFile().getName()) > 0) {
// 恢复文件
writer = recoverPerformer.recover(...);
// 如果文件有损坏,恢复、记录
if (writer.hasCrashed()) {
tsFileManagement.addRecover(tsFileResource, isSeq);
} else {
// 文件正常,直接封口
tsFileResource.setClosed(true);
tsFileManagement.add(tsFileResource, isSeq);
}
continue;
} else {
// 如果没有合并过,则恢复完成后,还需要考虑能否继续读写,并replay wal
writer = recoverPerformer.recover(...);
}
// 如果不是最后一个文件,或者该文件不能再写,则直接封口
if (i != tsFiles.size() - 1 || !writer.canWrite()) {
tsFileResource.setClosed(true);
} else if (writer.canWrite()) {
if (isSeq) {
// 最后一个文件可以写,那么初始化这个文件的tsFileProcessor,供后续的继续使用
tsFileProcessor = new TsFileProcessor(...);
if (enableMemControl) {
... ... // 内存控制模块,计算使用大小
}
} else {
... ... // unseq file初始化对应的tsFileProcessor
}}}}
在recoverPerformer.recover()中,进行了文件自检,即检查文件的头部和尾部等标识信息是否正常,如果正常,则文件完整不需要恢复,恢复对应的.resource文件后返回即可;如果文件损坏,则需要按chunk进行数据恢复,并且replay wal,结束恢复任务(文件的自检及chunk恢复等涉及文件结构部分的代码略)。
// 文件内容校验及恢复
public RestorableTsFileIOWriter recover(...) {
File file = FSFactoryProducer.getFSFactory().getFile(filePath);
// 获取当前文件信息,初始化RestorableTsFileIOWriter,并进行自检恢复
RestorableTsFileIOWriter restorableTsFileIOWriter = new RestorableTsFileIOWriter(file);
// 如果文件没有损坏
if (!restorableTsFileIOWriter.hasCrashed()) {
// 恢复对应.resource文件
// 如果.resource文件存在,则反序列化内容,获取对应的timeIndex及planIndex
// 如果文件不存在,则读取该tsfile的metadata,更新tsfileResource,序列化写入.resource文件
recoverResource();
return restorableTsFileIOWriter;
}
// 如果文件有损坏,更新lasttime,避免wal中记录的数据时间和tsfile文件中最后一个chunkgroup有交集
recoverResourceFromWriter(restorableTsFileIOWriter);
if (needRedoWal) {
// 恢复wal中记录的操作
redoLogs(restorableTsFileIOWriter, supplier);
MultiFileLogNodeManager.getInstance().deleteNode();
}
return restorableTsFileIOWriter;
}
// 文件的自检及恢复
public RestorableTsFileIOWriter(File file) throws IOException {
// 如果该文件不存在,则以此为文件为头,重新开始
// 文件存在,则需要检查文件是否完整,是否可以继续写入
if (file.exists()) {
try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getAbsolutePath(), false)) {
// 文件自检,返回值为:FILE_NOT_FOUND、INCOMPATIBLE_FILE、COMPLETE_FILE、${truncatedSize}
// truncatedSize记录文件可正常读到,并按metadata能恢复的位置;其他类型均为负数
// 文件快速过检标准:头部为"tsFile"+${version},尾部为"tsFile",文件长度大于这两部分的和
truncatedSize = reader.selfCheck(knownSchemas, chunkGroupMetadataList, true);
// 文件正常过检,则可以封口,关闭该文件
if (truncatedSize == TsFileCheckStatus.COMPLETE_FILE) {
crashed = false;
canWrite = false;
out.close();
} else if (truncatedSize == TsFileCheckStatus.INCOMPATIBLE_FILE) {
// 文件标识信息有问题,文件已经被损坏,抛异常
out.close();
throw new NotCompatibleTsFileException(
String.format("%s is not in TsFile format.", file.getAbsolutePath()));
} else {
// 文件已按chunk读取并恢复,将该位置的后续数据丢弃
crashed = true;
canWrite = true;
out.truncate(truncatedSize);
}}}}
3. 恢复合并任务
在合并时,会记录合并任务的操作日志,合并结束后删除日志文件。seq合并时走compaction逻辑,日志文件为.compaction.log;unseq合并到seq时走merge逻辑,日志文件为merge.log;由于合并部分的代码正在重构,逻辑发生变更,故此处仅大致说明合并恢复的过程,暂不详细解释。
配置默认在重启时,不再继续上次的合并,需要通过恢复日志记录的信息,进行部分恢复及数据清除操作。
private void recover() throws StorageGroupProcessorException {
... ... // 版本文件更新,及tsfile的恢复过程
// 获取merge.mods文件,该文件位于vsg的下一级
File mergingMods = SystemFileFactory.INSTANCE.getFile(storageGroupSysDir, MERGING_MODIFICATION_FILE_NAME);
// 如果mods文件存在,需要读取该文件内容,获取修改的操作,将modify操作更新到具体的文件内容中
if (mergingMods.exists()) {
this.tsFileManagement.mergingModification = new ModificationFile(mergingMods.getPath());
}
// 初始化RecoverMergeTask
RecoverMergeTask recoverMergeTask = new RecoverMergeTask(...);
recoverMergeTask.recoverMerge(
IoTDBDescriptor.getInstance().getConfig().isContinueMergeAfterReboot());
// 如果参数continue_merge_after_reboot配置为true,则重启时,需要从上次合并失败的位置开始恢复合并
// 默认为false,即重启前合并异常,则丢弃合并数据
if (!IoTDBDescriptor.getInstance().getConfig().isContinueMergeAfterReboot()) {
mergingMods.delete();
}
// 恢复compaction
recoverCompaction();
}
merge任务主要分为四个阶段:NONE、MERGE_START、ALL_TS_MERGED、MERGE_END;通过merge.log,获取到上次merge执行状态:
NONE:merge任务开没有开始,恢复时直接删掉merge.log日志文件;
MERGE_START:已经选出了要合并的时间序列,删除对应的.merge文件,删除日志文件;
ALL_TS_MERGED:已经合并结束,如果读取last position不是文件尾部,则直接从该位置截断,并清理merge相关文件;
MERGE_END:已经全部结束,cleanup再次检查(理论上不应该存在此阶段);
compaction任务则读取compaction.log后,检查target file是否完整(targetFile为要合并的目标文件名),如果完整则关闭,跳过本次compaction,否则重新提交一个compaction任务,结束后删除日志文件。