Leveldb 源码详解系列之八: 版本(Version)

Version 是 leveldb 在磁盘上的文件 level 结构的抽象, 也是访问磁盘文件的隘口. 也就是说, 要读取磁盘上的文件, 必须要经过 Version. 要针对磁盘上的数据库做一些优化或者统计, 也可以通过 Version 来实现.

下面通过读写来了解下 Version 的作用.

被用户高频使用的 DBImpl::Get 方法就会用到 Version. 当在内存中的 memtables 找不到 key 时, 就需要去查询文件. 此时, 当前 Version 开始起作用.

下面代码删掉了与我们这里要介绍内容关联不大的部分.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) {
  bool have_stat_update = false;
  Version::GetStats stats;

  // 根据 user_key 和快照对应的序列号构造一个 internal_key
  LookupKey lkey(key, snapshot);
  // 先查询内存中与当前 log 文件对应的 memtable
  if (mem->Get(lkey, value, &s)) {
  } else if (imm != nullptr && imm->Get(lkey, value, &s)) {
    // 查不到来待压实的 memtable 去查询
  } else {
    // 查不到再逐 level 去 sstable 文件查找, 这时候就用到了 Version current
    s = current->Get(options, lkey, value, &stats);
    have_stat_update = true;
  }

  // 每次查询完都要检查下是否有文件查询次数已经达到最大需要进行压实了.
  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction();
  }
  return s;
}

上面 current 就是当前 Version.

方法末尾的判断目的是记录本次扫盘对压实施加的影响, 如果最底层文件读取次数超出上限则进行压实, 关于压实请看之前文章介绍.

没有. leveldb 文件一旦落盘便不可更改.

Version 不单独存在, 由 VersionSet 负责维护.

leveldb 在启动时会构造一个 VerionSet, 后者会读取磁盘上的 MANIFEST 文件构造 Version.

leveldb 在打开数据库时会调用 Recover() 读取磁盘上的文件, 其中就有构造 Version 所需要的 MANIFEST 文件:

1
2
3
4
5
6
7
8
Status DB::Open(const Options& options, const std::string& dbname,
                DB** dbptr) {
  ...
  // 读取 current 文件, manifest 文件, sstable 文件和 log 文件恢复数据库.
  // 如果要打开的数据库不存在, Recover 负责进行创建.
  Status s = impl->Recover(&edit, &save_manifest);
  ...
}

impl->Recover 做相关工作调用的是 VersionSet 的对应方法:

1
2
3
4
5
6
7
8
9
Status DBImpl::Recover(VersionEdit* edit, bool *save_manifest) {
  ...
  // 该方法负责从最后一个 MANIFEST 文件解析内容出来与当前 Version
  // 保存的 level 架构合并保存到一个
  // 新建的 Version 中, 然后将这个新的 version 作为当前的 version.
  // 参数是输出型的, 负责保存一个指示当前 MANIFEST 文件是否可以续用.
  s = versions_->Recover(save_manifest);
  ...
}

VersionSet::Recover() 解析 MANIFEST 文件每一行, 反序列化为 VersionEdit, 然后将其组装为其当前 Versioncurrent_:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 该方法负责从最后一个 MANIFEST 文件解析内容出来与当前 Version 
// 保存的 level 架构合并保存到一个
// 新建的 Version 中, 然后将这个新的 version 作为当前的 version.
Status VersionSet::Recover(bool *save_manifest) {
  // 读取 CURRENT 文件获取 MANIFEST 文件名称
  Status s = ReadFileToString(env_, CurrentFileName(dbname_), &current);
  // 构造 MANIFEST 文件路径
  std::string dscname = dbname_ + "/" + current;
  SequentialFile* file;
  s = env_->NewSequentialFile(dscname, &file);

  // 解析 MANIFEST 文件内容反序列化为 VersionEdit
  Builder builder(this, current_);
  {
    log::Reader reader(file, &reporter, true/*checksum*/, 0/*initial_offset*/);
    // 循环读取 MANIFEST 文件日志, 每一行日志就是一个 VersionEdit
    while (reader.ReadRecord(&record, &scratch) && s.ok()) {
      VersionEdit edit;
      // 将 record 反序列化为 version_edit
      s = edit.DecodeFrom(record);

      // 将 VersionEdit 保存到 VersionSet 的 builder 中, 
      // 后者可以一次性将这些文件变更与当前 Version 合并构成新 version.
      if (s.ok()) {
        builder.Apply(&edit);
      }
    }
  }
  // 至此解析 MANIFEST 文件结束, 根据其保存的全部文件变更创建新的 Version
  if (s.ok()) {
    Version* v = new Version(this);
    // 将当前 version 和 builder 的 level 架构合并放到新的 v 中
    builder.SaveTo(v);
    // 将 v 作为当前 version
    AppendVersion(v);
  }

  return s;
}

至此, leveldb 的 Version 构造完成.

每当有文件变更时(这一般由普通写操作和压实操作触发), 相关变更(如文件新增或删除)会被记录到 VersionEdit. VersionEdit 可以看作一个 on-fly 的 Version. 它会记录 db 运行过程中删除的文件列表和新增的文件列表. VersionSet 会将新的 VersionEdit 与当前 Version 合并构造一个新的 Version 服务于 leveldb 用户. VersionEdit 可以看作是 Version 的增量更新, 全量+增量=新全量.

具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 1, 将参数 *edit 内容(可看作当前针对 level 架构的增量更新)
// 和当前 version 内容合并构成一个新的 version;
// 2, 然后将这个新 version 内容序列化为一条日志写到新建的 manifest 文件;
// 3, 同时将该 manifest 文件名写入 current 文件;
// 4, 最后把新的 version 替换当前 version.
Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) {
  // 新建一个 Version 用于合并当前 Version 和 VersionEdit 保存的增量更新
  Version* v = new Version(this);
  {
    // 将当前 VersionSet 及其 current version 作为输入构建一个新的 Builder
    Builder builder(this, current_);
    // 将新的 VersionEdit 与 current version 内容合并
    builder.Apply(edit);
    // 将 Builder 内容输出到 Version v 中
    builder.SaveTo(v);
  }

  // Initialize new descriptor log file if necessary by creating
  // a temporary file that contains a snapshot of the current version.
  // 如有必要通过创建一个临时文件来初始化一个新的文件描述符, 这个临时文件包含了当前 Version 的一个快照
  std::string new_manifest_file;
  Status s;
  // 如果 MANIFEST 文件指针为空则新建一个
  if (descriptor_log_ == nullptr) {
    // manifest 文件又叫 descriptor 文件
    s = env_->NewWritableFile(new_manifest_file, &descriptor_file_);
    if (s.ok()) {
      descriptor_log_ = new log::Writer(descriptor_file_);
      // 将当前 Version 保存的 level 架构信息保存到一个新 VersionEdit 
      // 中后将其序列化到 MANIFEST 文件.
      s = WriteSnapshot(descriptor_log_);
    }
  }

  // Unlock during expensive MANIFEST log write
  {
    mu->Unlock();
    // 将通过参数传入的 VersionEdit 记录到 manifest 文件. 
    // 这个地方相对于上面的 snapshot 相当于是一个增量.
    if (s.ok()) {
      std::string record;
      edit->EncodeTo(&record);
      s = descriptor_log_->AddRecord(record);
    }
    mu->Lock();
  }

  // 将基于当前 Version 和增量 VersionEdit 构建的新 version 
  // 作为当前 version. 它是最新的 level 架构.
  AppendVersion(v);

  return s;
}

—end—