levelDB - 04. 通过 CompactMemTable 了解版本控制
整个文章其实并没有讲解太多地细节方面的事情,主要是理一下大致的逻辑, 大致存什么, 和有关的对象的粗略介绍,主要是为了建立对各个对象的直观认识,知道它是什么,我觉得对后面更细节的分析也是有一定帮助的
1. 如何调用到 CompactMemTable()
上一部分我们讲到了 MaybeScheduleCompaction(), 他的逻辑是比较清晰的,就是检测是否需要进行 compaction,具体的条件是在监测到
- imm != nullptr : 需要 minor compaction
- manual_compaction != nullptr
- versions_->NeedsCompaction() : 需要进行 major compacation
这三种情况中的满足其中一种就 env_->Schedule(&DBImpl::BGWork, this); 调度后台 compaction 的线程,将 &DBImpl::BGWork 作为线程函数
&DBImpl::BGWork 实际上内部就是 reinterpret_cast<DBImpl*>(db)->BackgroundCall(); 调用对应的 BackgroundCall() 方法,如果要问为什么不直接调用,这是因为 &DBImpl::BGWork 是一个 static 方法,即一个普通的函数指针,而 BackgroundCall() 是一个类成员方法,是需要对象去调用的
而 BackgroundCall() 的逻辑也是很清晰的,主要就是
- 调用 BackgroundCompaction();进行 compaction
- MaybeScheduleCompaction()
- background_work_finished_signal_.SignalAll();
在第 2 步:因为上一次的 compaction 可能造成其中一个 level 的 file 变得过多,所以我们要再次检查是否需要 compaction,在 compaction 的时候也因此设计了很多细节来防止这样的连锁反应造成 compaction 时间过长
在第 3 步:compaction 完成后要通知其他线程,就像是 MakeRoomForWrite 可能就在 waiting,要唤醒它
最后,在 BackgroundCompaction() 部分,我们可以看见
if (imm_ != nullptr) {
  CompactMemTable();
  return;
}
正如在 MakeRoomForWrite() 情况五中,如果发现了需要进行 minor compaction,就将 imm_ = mem_, 于是我们正式调用 CompactMemTable() 方法
2. CompactMemTable() 逻辑
按照一个粗略的逻辑,都是会根据 immutable table 产生一个 level0 的文件,然后要将其更新到版本中,在实现中,使用了一个 VersionEdit 对象,把 CompactMemTable() 简化了一下
void DBImpl::CompactMemTable() {
  ...
  // Save the contents of the memtable as a new Table
  VersionEdit edit;
  Version* base = versions_->current();
  ...
  Status s = WriteLevel0Table(imm_, &edit, base);
  ...
  // Replace immutable memtable with the generated Table
  if (s.ok()) {
    edit.SetPrevLogNumber(0);
    edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
    s = versions_->LogAndApply(&edit, &mutex_);
  }
  ...
  imm_ = nullptr;
  ...
}
简单的来说就是
- WriteLevel0Table(imm_, &edit, base)将对应的 imm_ 创建一个 level0 文件,并且设置好一个- VersionEdit对象
- 然后就是应用 VersionEdit 对象来对 versions_ 进行修改 versions_->LogAndApply(&edit, &mutex_);
这里我们就可以简单地理解成为说 VersionEdit 就是统一了一下 versions_ 要修改的数据,以 VersionEdit 为单位进行修改
2.1 WriteLevel0Table()
正如方法名所说的那样,这个方法很明显就是会通过 MemTable 创建一个 level0 文件,然后从之前的可以知道是要将其修改统一到 VersionEdit 中,那具体是如何统一的?存的数据又是什么?
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  ...
  FileMetaData meta;
  meta.number = versions_->NewFileNumber();
  ...
  Iterator* iter = mem->NewIterator();
  ...
  Status s;
  {
    mutex_.Unlock();
    s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
    mutex_.Lock();
  }
  ...
  edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
                meta.largest);
  ...
}
那其实从上面的代码中,一个直观的印象就是 BuildTable() 创建了一个 table, 通过遍历参数 iter 遍历 memetable 创建对应的表, 然后在 edit->AddFile() 就相当与是把这个文件添加了,之后 versions_ 也就通过 edit 中添加的数据,对应添加到自己上
但是同时这个代码也会显得很奇怪,至少当时我看的时候是比较迷惑的,我的理解是首先创建一个 file,那应该是会有对应的文件描述符的,一般也是返回文件描述符,然后使用文件描述符进行一些操作的 (比如保存? 【狗头】)
但是你看见 BuildTable 返回的是 Satus, 然后可能有修改的就是传了引用的 FileMetaData,而且在 edit 进行 AddFile 的时候也是和 meta 相当有关的,以及非常可疑的 meta.number = versions_->NewFileNumber();
然后我看了一下对应的 BuildTable 方法,发现在创建 table 的时候是使用了 db_name_ 和 meta.name 用作 file name, 然后就明白了。实际上存的话肯定是不能是文件描述符的,因为文件描述符是进程用来标识一个文件的,实际上我们是存储文件名来保存版本的,因为这是持久化的,找到文件的方法,整个问题就通顺了,同时在 Version 中的对文件的保存就是以 FileMetaData 存的。
对应的 edit->AddFile 就是在 VersionEdit 中有 std::vector<std::pair<int, FileMetaData>> new_files_, 这里就是存对应 (level, FileMetaData) 数据对,表示在 level 层要增加的文件
2.2 versions_->LogAndApply()
然后我们简单看看是如何将 edit 修改到 versions_ 的
Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) {
  ...
  Version* v = new Version(this);
  {
    Builder builder(this, current_);
    builder.Apply(edit);
    builder.SaveTo(v);
  }
  
  ...
  // Install the new version
  AppendVersion(v);
  ...
}
额,我把很大的一部分都删除了... 主要的逻辑就是通过 edit 创建一个新的 Version,然后将这个 Version 添加
然后这里主要又涉及到一个新的对象 Builder, 我们就简单地将他理解成为是创建 Version 的对象,然后 VersionEdit 就相当与是保存了要修改的数据,感觉有点像是将修改的数据和创建 Version 逻辑分开的感觉,由不同的对象负责... 个人感觉...
然后粗略地感受一下 builder.Apply() 方法和 builder.SaveTo() 方法
// Apply all of the edits in *edit to the current state.
void Apply(const VersionEdit* edit) {
  // Update compaction pointers
  ...
  // Delete files
  ...
  // Add new files
  for (size_t i = 0; i < edit->new_files_.size(); i++) {
    const int level = edit->new_files_[i].first;
    FileMetaData* f = new FileMetaData(edit->new_files_[i].second);
    f->refs = 1;
    ...
    levels_[level].added_files->insert(f);
  }
}
// Save the current state in *v.
void SaveTo(Version* v) {
  BySmallestKey cmp;
  cmp.internal_comparator = &vset_->icmp_;
  for (int level = 0; level < config::kNumLevels; level++) {
    // Merge the set of added files with the set of pre-existing files.
    // Drop any deleted files.  Store the result in *v.
    ... 
    MaybeAddFile(v, level, *base_iter); or MaybeAddFile(v, level, added_file);
    ...
    ...
  }
}
这两个部分也是省略了很多的细节,这里的主要理解就是,在 Apply 当中,会在 Builder 中有 levels 数组,效果就是将 VersionEdit 中存的 <level, FileMetaData> pair 的形式整理以 level 分层保存增加的文件,删除的文件等等。
然后在 SaveTo 中 base_ 是 Version* current, 这里就是将 builder 中整理好的以 level 分层的文件和 current version 结合产生新的 Version v; 内部的逻辑其实就是调用 MaybeAddFile() 将对应的文件 push_back 到 Version v 当中的 files_[level] 当中
最后就是将其 AppendVersion(v)添加到 VersionSet 中,然后粗略的理解 VersionSet 和 Version 的话,就是 Version 本身是以一个双链表的形式存储 Version 的版本链,然后 VersionSet 就更多是存一些和整版本链有关的信息,而 Version 就是存当前 Version 的信息,最直观的就是每一个 level 有的 FileMetaData ...
3. 总结:关键逻辑与相关对象

对应在 VerisonSet::Builder 中的对 FileMetaData 的比较器和 LevelStaus 如下
// Helper to sort by v->files_[file_number].smallest
struct BySmallestKey {
  const InternalKeyComparator* internal_comparator;
  bool operator()(FileMetaData* f1, FileMetaData* f2) const {
    int r = internal_comparator->Compare(f1->smallest, f2->smallest);
    if (r != 0) {
      return (r < 0);
    } else {
      // Break ties by file number
      return (f1->number < f2->number);
    }
  }
};
typedef std::set<FileMetaData*, BySmallestKey> FileSet;
struct LevelState {
  std::set<uint64_t> deleted_files;
  FileSet* added_files;
};
 
                    
                 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号