[leveldb] 4-Recover数据恢复(2)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/simonyucsdy/article/details/81588748

上篇说了LevelDB如何在没有日志的情况下, 恢复(新建)数据库. 这篇开始分析读日志的代码了。在这里,我觉得很有必要区分清楚, log在LevelDB中究竟有几种语义。相当多关于LevelDB的文章都在这里有点含糊不清。

  • 人类可读的日志, 存于"LOG"文件

比如, "2017/06/16-11:09:03.295840 7fffb990d3c0 Recovering log #18". 在代码中是用"Log"函数来触发的, 相关的类是"Logger".

  • 机读二进制日志, 存于".log"文件

这个是真正意义上用于恢复数据的日志. 数据启动时, 如果有没清空的日志, 就说明上次关闭不成功, 须回放一遍.

  • leveldb::log, 这是一个namespace, 用于把二进制数据安全地序列化, 反序列化

::log不仅负责(反)序列化机读日志, VersionEdit在"MANIFEST"文件内也复用了这个组件. 现在提出一个很重要也很常见的问题, 如何保证非原子性的一连串操作的原子性? 有点绕? 来个情景.

数据库现在要开始写Log了, 一条一条又一条, 这时候突然崩溃了. 下次再开, 日志回放的时候, 会得到啥? 形象的说, 这可以叫做"薛定谔的数据库". 最后一条记录处于成功和失败的叠加态, 只有观测的一瞬间才知道. 大部分用户可以容许的是丢日志, 但绝对不容忍错误的日志被当成正常的写入数据库. 比如, 往A账户转入10000W, 这条写到一半, 最后变成了往A账户转入10W...

解决方案大概有两个,

1. sentinel

确保之前的数据都不存在某个特定字符(可能需要转义), 然后在结尾写上这个终止符, 表示顺利完成.

2. checksum

在数据写入完成之后, 再多写一段hash. 再次读取时, 只有hash和内容对上了, 这段数据才是合法的.

sentinel的问题在于如果有来自宇宙的辐射让硬盘/CPU的电路发生比特翻转, 错误的数据还是能被合法地接受. 可能宇宙射线这个太罕见了, 更常见的是硬盘坏道. 还有怎么保证sentinel的唯一性也是个问题.

一般理性的程序员都选择checksum, LevelDB对此有一个高度优化的crc32c hash函数在util/crc32c.cc文件内。

所以, 一条机读日志从内存到硬盘是这样的, 内存对象 => 二进制数组(Slice对象) => leveldb::log切割成小块并打上hash => 写入硬盘。

上篇的Recover函数读到了创建新数据库的位置,接下来继续

  s = versions_->Recover(save_manifest);
  if (!s.ok()) {
    return s;
  }
  SequenceNumber max_sequence(0);

基于LSM Tree的数据库在恢复时一定分两步, 第一是恢复SSTable, 第二是恢复memtable/immemtable. "versions_->Recover"是前者,进入该函数

Status VersionSet::Recover(bool *save_manifest) {
  struct LogReporter : public log::Reader::Reporter {
    Status* status;
    virtual void Corruption(size_t bytes, const Status& s) {
      if (this->status->ok()) *this->status = s;
    }
  };

这段代码再次表明了Google对于虚函数的热爱, "LogReporter"的功能完全可以用函数指针替代. save_manifest在99%的情况下都应该是true, 意为是否要覆写"MANIFEST"文件. 由于"MANIFEST"文件积存着很多VersionEdit, 合并重写对性能总是有好处的.

924-966行:

  Builder builder(this, current_);
  {
    LogReporter reporter;
    reporter.status = &s;
    log::Reader reader(file, &reporter, true/*checksum*/, 0/*initial_offset*/);
    Slice record;
    std::string scratch;
    while (reader.ReadRecord(&record, &scratch) && s.ok()) {
      VersionEdit edit; //先由::log反序列化成slice
      s = edit.DecodeFrom(record);  // 再由类自身从slice反序列化成数据
      if (s.ok()) {
        if (edit.has_comparator_ &&
            edit.comparator_ != icmp_.user_comparator()->Name()) {
          s = Status::InvalidArgument(
              edit.comparator_ + " does not match existing comparator ",
              icmp_.user_comparator()->Name());
        }
      }

      if (s.ok()) {
        builder.Apply(&edit);
      }

这段基本就做了一件事, 不断回放VersionEdit, 然后喂给Builder, 最终apply叠加成崩溃(关闭)之前的Version. 我自己本身是写过KV数据库的, 感觉这些都没什么惊奇的, 有疑惑的可以细看下. Builder可以理解为时光机, 让数据库在不同的时间点无缝迁移.

1010-1014行:

    Version* v = new Version(this);
    builder.SaveTo(v);
    // Install recovered version
    Finalize(v);
    AppendVersion(v);

LevelDB受制于其完成时间远早于C++ 11标准, 个人感觉的非最佳实践:

  • 不用异常

哇... 坑爹啊... 一层套一层的s.ok(). 又因为返回值一定要是状态, 那么函数间数据交互就只能靠指针/引用了.

  • 不用智能指针

然后又加了一个低配引用计数器, 来回手动ref(), unref().

------

下章应该是数据库恢复的最终章, 要写如何恢复memtable/immemtable.

猜你喜欢

转载自blog.csdn.net/simonyucsdy/article/details/81588748