【leveldb】Compact(二 十 三):Major Compaction

在这里插入图片描述

上一篇我们介绍了进行Major Compaction所需要的对象数据实体Compaction。对整个Compact流程的介绍可点此Compact触发流程
本片我们来着重介绍下Compact的Major Compaction流程。


Major Compaction的作用
01节省磁盘空间

我们知道leveldb的存储结构用的是LSM数据结构,增、删、改操作都是顺序追加操作,也就是不会去真正的去修改数据或者删除数据,而是写入操作,只是设置对应的标志。这样的话同一个key可能会存多条分布在不同的SSTable文件中,这明显是冗余的。通过Major Compaction可以删除过期无效的key,减少磁盘空间占用。

02提高读效率

我们知道level-0层的SSTable文件之间是无序的,这样查找一个key时就需要遍历所有的文件,这显然是低效的,如果leveldb的每一层SSTable文件都是无序的,那查找的时候就是灾难了。但是通过major Compaction,leveldb从level-1层开始,每层的SSTable文件之间都是有序的。这样在查找一个key的时候,必要的话只有在level-0层需要遍历所有的SSTable,在level-1及以上层只需通过二分搜索快速定位到某个SSTable文件,然后再去细查此文件即可。

源码分析

之前文章已经讲解了怎么查找用于进行Major Compaction的SSTable文件及数据,不清楚的可以点此Compaction数据产生。主要通过以下两个方法生存:

 Compaction* c;
 - c = versions_->CompactRange(m->level, m->begin, m->end);
 - c = versions_->PickCompaction();

Major Compaction分两种:

  • 直接将level层文件移动到level+1层。
  • 循环遍历所有待Compact的key,保留有效key,丢弃无效key。
01移动Compact

即简单的移动文件到下一层来达到合并文件的效果。

满足此流程的条件如下:

  • 非手动Compact;
  • 待Compact的level层只有一个文件,level+1层没文件且grandparents层总的文件大小未超过阈值MaxGrandParentOverlapBytes()。
//通过方法IsTrivialMove()来判断是不是可以简单的移动文件到下一层
//来达到合并文件的效果。但是又要避免因将文件移动到下一层导致与
//下下一层即grandparent层有太多的重叠数据进而导致compact下一层时
//压力太大。
//判断是否可行的方法如下:
//1、inputs_[0]层只有一个文件、inputs_[1]层没文件。
//2、grandparents层总的文件大小未超过阈值MaxGrandParentOverlapBytes()。
bool Compaction::IsTrivialMove() const {
  const VersionSet* vset = input_version_->vset_;
  // Avoid a move if there is lots of overlapping grandparent data.
  // Otherwise, the move could create a parent file that will require
  // a very expensive merge later on.
  return (num_input_files(0) == 1 && num_input_files(1) == 0 &&
          TotalFileSize(grandparents_) <=
              MaxGrandParentOverlapBytes(vset->options_));
}

<!通过移动文件达到Compact效果的流程>
if (!is_manual && c->IsTrivialMove()) {
    // Move file to next level
	//1.如果当前压缩文件指出TrivialMove()条件,
    //2.则直接在VersionEdit中,记录下level层删除此文件,
    //  level + 1 添加此新文件。
    //3.将此VersionEdit应用出新的Version。

    assert(c->num_input_files(0) == 1);
    FileMetaData* f = c->input(0, 0);
    c->edit()->DeleteFile(c->level(), f->number);
    c->edit()->AddFile(c->level() + 1, f->number, f->file_size, f->smallest,
                       f->largest);
    status = versions_->LogAndApply(c->edit(), &mutex_);
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    VersionSet::LevelSummaryStorage tmp;
    Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n",
        static_cast<unsigned long long>(f->number), c->level() + 1,
        static_cast<unsigned long long>(f->file_size),
        status.ToString().c_str(), versions_->LevelSummary(&tmp));
  } 
02正常Compact

此流程就是遍历所有需要待Compact的SSTable文件中的key,有效的key就写入到新的SSTable文件中,无效的key就丢弃。写完之后删除掉无用的SSTable。详细的说明已在代码中注释。

//Compact工作。
//1.将待Compact的input[0]、input[1]文件创建为迭代器访问
//2.Seek到迭代器的最小key,并开始循环访问这些key并进行Compact。
//3.在循环的过程中,如果有immutable,那就优先处理进行下Compact。
//4.判断下当前准备Compact的文件对应的key是否需要提前停止Compact,并将当前的文件落地为SSTable。
Status DBImpl::DoCompactionWork(CompactionState* compact) {
  const uint64_t start_micros = env_->NowMicros();
  int64_t imm_micros = 0;  // Micros spent doing imm_ compactions

  Log(options_.info_log, "Compacting %d@%d + %d@%d files",
      compact->compaction->num_input_files(0), compact->compaction->level(),
      compact->compaction->num_input_files(1),
      compact->compaction->level() + 1);

  assert(versions_->NumLevelFiles(compact->compaction->level()) > 0);
  assert(compact->builder == nullptr);
  assert(compact->outfile == nullptr);
  if (snapshots_.empty()) {
    compact->smallest_snapshot = versions_->LastSequence();
  } else {

	//如果某个快照被外部使用(GetSnapShot),这个快照对应的SnapShotImpl对象会被放
	//在SnapshotList中(也就是sequence_number被保存下来了),Compaction的时候
    //遇到可以清理的数据,还需要判断要清理数据的seq_number不能大于这些快照中的
	//sequence_number,否则会影响夸张数据。
    compact->smallest_snapshot = snapshots_.oldest()->sequence_number();
  }

  Iterator* input = versions_->MakeInputIterator(compact->compaction);

  // Release mutex while we're actually doing the compaction work
  mutex_.Unlock();

  //这里input迭代器的循环遍历是每次取迭代器中最小的key,
  //key是指InternalKey,key比较器是InternalKeyComparator,
  //InternalKey比较方式是对InternalKey中的User_Key按BytewiseComparator来比较。
  //在User_Key相同的情况下,按SequenceNumber来比较,SequenceNumber值大的是小于SequenceNumber值小的,
  //所以针对abc_456_1、abc_123_2,abc_456_1是小于abc_123_2的。针对我们对同一个user_key的操作,
  //最新对此key的操作是小于之前对此key的操作的。
  input->SeekToFirst();
  Status status;
  ParsedInternalKey ikey;
  std::string current_user_key;
  bool has_current_user_key = false;
  SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
  while (input->Valid() && !shutting_down_.load(std::memory_order_acquire)) {

    // Prioritize immutable compaction work
    //检查并优先compact存在的immutable memtable。
    if (has_imm_.load(std::memory_order_relaxed)) {
      const uint64_t imm_start = env_->NowMicros();
      mutex_.Lock();
      if (imm_ != nullptr) {
        CompactMemTable();
        // Wake up MakeRoomForWrite() if necessary.
        background_work_finished_signal_.SignalAll();
      }
      mutex_.Unlock();
      imm_micros += (env_->NowMicros() - imm_start);
    }

    Slice key = input->key();

	//如果当前InternalKey与grandparent层产生overlap的size超过阈值,
	//那就停止当前SSTable检查,直接落地并停止遍历。
    if (compact->compaction->ShouldStopBefore(key) &&
        compact->builder != nullptr) {
      status = FinishCompactionOutputFile(compact, input);
      if (!status.ok()) {
        break;
      }
    }

    // Handle key/value, add to state, etc.
	//确定当前key是否要丢弃
    bool drop = false;
    if (!ParseInternalKey(key, &ikey)) {
	  //解析key失败。
      //针对解析失败的key,这里不丢弃,直接存储。
	  //目的就是不隐藏这种错误,存储到SSTable中,
	  //便于后续逻辑去处理。
      // Do not hide error keys
      current_user_key.clear();
      has_current_user_key = false;
      last_sequence_for_key = kMaxSequenceNumber;
    } else {
	 //解析InternalKey成功。
	 
      if (!has_current_user_key ||
          user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) !=0) {
		  //前后检测的两个InternalKey不一样,
		  //那就记录这个首次出现的key,
		  //并将last_sequence_for_key设置为最大。
        // First occurrence of this user key
        current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
        has_current_user_key = true;
        last_sequence_for_key = kMaxSequenceNumber;
      }

	  //如果前后两个key不一样,last_sequence_for_key会被赋值为kMaxSequenceNumber,
      if (last_sequence_for_key <= compact->smallest_snapshot) {

		//进入此逻辑,说明last_sequence_for_key不是最大值kMaxSequenceNumber,
		//也就是当前这个key的user_key(是一个比较旧的userkey)和上一个key的user_key是相同的。
		//所以这里就直接丢弃。
        // Hidden by an newer entry for same user key
        drop = true;  // (A)
      } else if (ikey.type == kTypeDeletion &&
                 ikey.sequence <= compact->smallest_snapshot &&
                 compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
		//如果这个InternalKey满足一下三个条件,则可以直接丢弃。
		//1.是个Deletionkey。
		//2.sequence <= small_snaphshot。
		//3.当前compact的level是level-n和level-n+1,
		//  如果在level-n+1以上的层已经没有此InternalKey对应的user_key了。
		//基于以上三种情况可删除。
		//为什么要此条件(IsBaseLevelForKey)判断呢?
		//举个例子:
		//如果在更高层,还有此InternalKey对应的User_key,
		//此时你把当前这个InternalKey删除了,那就会出现两个问题:
		//问题1:再次读取删除的key时,就会读取到老的过期的key(这个key的type是非deletion),这是有问题的。
		//问题2:再次合并时,但这个key(这个key的type是非deletion)首次被读取时last_sequence_for_key会设置为kMaxSequenceNumber,
		//      这样就也不会丢弃。
		//以上两个问题好像在更高层的也就是旧的此key的所有userkey的type都是是delete的时候好像是没问题的,
		//但这毕竟是少数,原则上为了系统正常运行,我们每次丢弃一个标记为kTypeDeletion的key时,
	    //必须保证数据库中不存在它的过期key,否则就得将它保留,直到后面它和这个过期的key合并为止,合并之后再丢弃

        // For this user key:
        // (1) there is no data in higher levels
        // (2) data in lower levels will have larger sequence numbers
        // (3) data in layers that are being compacted here and have
        //     smaller sequence numbers will be dropped in the next
        //     few iterations of this loop (by rule (A) above).
        // Therefore this deletion marker is obsolete and can be dropped.
        drop = true;
      }

      last_sequence_for_key = ikey.sequence;
    }
#if 0
    Log(options_.info_log,
        "  Compact: %s, seq %d, type: %d %d, drop: %d, is_base: %d, "
        "%d smallest_snapshot: %d",
        ikey.user_key.ToString().c_str(),
        (int)ikey.sequence, ikey.type, kTypeValue, drop,
        compact->compaction->IsBaseLevelForKey(ikey.user_key),
        (int)last_sequence_for_key, (int)compact->smallest_snapshot);
#endif

	//写入不丢弃的key
    if (!drop) {

	  //1.打开SSTable文件
      // Open output file if necessary
      if (compact->builder == nullptr) {
        status = OpenCompactionOutputFile(compact);
        if (!status.ok()) {
          break;
        }
      }

	  //对于要写入的key,从input_取出时,
	  //应该是当前整个input_中最小的key(此处应该就体现了迭代器封装的好处了)。
	  //也就是说写入到SSTable是升序的。
      if (compact->builder->NumEntries() == 0) {
        compact->current_output()->smallest.DecodeFrom(key);
      }
      compact->current_output()->largest.DecodeFrom(key);
      compact->builder->Add(key, input->value());

	  //待生成的SSTable已超过特定阈值,那么就将此SSTable文件落地。
      // Close output file if it is big enough
      if (compact->builder->FileSize() >=
          compact->compaction->MaxOutputFileSize()) {
        status = FinishCompactionOutputFile(compact, input);
        if (!status.ok()) {
          break;
        }
      }
    }

    input->Next();
  }

  if (status.ok() && shutting_down_.load(std::memory_order_acquire)) {
    status = Status::IOError("Deleting DB during compaction");
  }
  if (status.ok() && compact->builder != nullptr) {
    status = FinishCompactionOutputFile(compact, input);
  }
  if (status.ok()) {
    status = input->status();
  }
  delete input;
  input = nullptr;

  CompactionStats stats;
  stats.micros = env_->NowMicros() - start_micros - imm_micros;
  for (int which = 0; which < 2; which++) {
    for (int i = 0; i < compact->compaction->num_input_files(which); i++) {
      stats.bytes_read += compact->compaction->input(which, i)->file_size;
    }
  }
  for (size_t i = 0; i < compact->outputs.size(); i++) {
    stats.bytes_written += compact->outputs[i].file_size;
  }

  mutex_.Lock();
  stats_[compact->compaction->level() + 1].Add(stats);

  if (status.ok()) {
    status = InstallCompactionResults(compact);
  }
  if (!status.ok()) {
    RecordBackgroundError(status);
  }
  VersionSet::LevelSummaryStorage tmp;
  Log(options_.info_log, "compacted to: %s", versions_->LevelSummary(&tmp));
  return status;
}

总结

看到一些对major Compaction流程介绍的文章说当待Compact的key与grandparents层的SSTable文件存在overlap的大小太大的时候会结束本次Compact,这里说明下应该是结束对当前SStable的写入,重新创建一个新的SSTable文件来存此key。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值