【IOTDB】recovery

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任务,结束后删除日志文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值