概览

log 相关的功能在如下文件中

  • db/log_format.h,这是里格式的定义
  • db/log_reader.h,log_reader.cc
  • db/log_writer.h,log_writer.cc

一个日志块大小固定为 32K
每个日志记录格式如下(4字节的checksum、2字节长度、1字节类型)

类型包括:

  • kZeroType,预留
  • kFullType,单个块可以存放下
  • kFirstType,跨多个块,这是第一个
  • kMiddleType,跨多个块,中间的
  • kLastType,跨多个块,最后一个

如下是 3条 记录,其中 A、C是 full类型,B跨了多个块

每个文件块的组成格式,用go描述如下:

1
2
3
4
5
6
block := record* trailer?
record :=
  checksum: uint32     // crc32c of type and data[] ; little-endian
  length: uint16       // little-endian
  type: uint8          // One of FULL, FIRST, MIDDLE, LAST
  data: uint8[length]

读写入过程

写入

核心逻辑位于 db/log_writer.cc 中

1
Status Writer::AddRecord(const Slice& slice) 

例子

  • 假设当前已经写入了 1000字节,再写一条 500字节的日志,需要额外增加7个字节(4个字节checksum、2字节长、1字节类型)
  • 写入之后,文件块的写入偏移量就变成1507字节
  • 加入写入新记录为 31755字节,写入后偏移量为 32762字节(1000字节开始偏移量 + 31755新内容长度 + 7字节头部)
  • 此时只剩 6字节空余,对于小于 7字节的空余情况,需要全部填充 ‘\0’

另一个例子

  • 假设写入 50000字节,当前已经写入了 1000字节
  • 先写 7字节头部,再写 31761字节,此时会将剩余的块全部填满
  • 再写第二个块,仍然是先写 7字节头部,再写 18239字节数据部分

写入流程

  • AddRecord,在 do-while中,先计算出剩余的字节量,如果少于 7字节则全部填充 0
  • 之后计算出数据部分(去掉 7字节头部后),需要的空间,如果小于剩余空间,则为 FULL类型,否则是FIRST类型
  • 调用EmitPhysicalRecord,将数据写入磁盘
  • EmitPhysicalRecord 中,先计算出头部7字节,填充4字节的CRC部分(这里用的是变长编码)
  • 然后向磁盘写入 7字节头部、日志数据部分、最后flush,最后计算新的 offset
  • 继续之前的 AddRecord,也就是do-while部分,此时计算剩余的 部分是否 < 块的可用长度,是为LAST类型,否则为MIDDLE类型
  • 继续不停的写,直到数据写完退出循环

读取

读取相关的逻辑位于

  • db/log_reader.h
  • db/log_reader.cc

核心逻辑是:

1
bool Reader::ReadRecord(Slice* record, std::string* scratch)

主要逻辑

  1. 调用 ReadPhysicalRecord,从物理文件中读取数据块
  2. ReadPhysicalRecord 中会从物理文件中读取 32K数据,再解析头部(长度和类型,以及CRC)
  3. 如果解析成功则将数据封装成 Slice指针(此变量作为函数参数传进来的),并返回 type
  4. 如果是 FULL 类型,将结果赋予 record指针返回
  5. FIRST、MIDDLE 则放入 scratch(临时的std::string缓存)中,LAST则封装成Slice返回
  6. 这里还会处理 读取失败,块损坏等情况,并记录到 Reporter 对象中

写日志的触发

这是在 db/db_impl.cc 中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 函数
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates)
...
// 触发 log,检查是否要同步 flush 数据到磁盘
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }

Log文件记录的内容如下:

  • 序列号(sequenceNumber):占8个字节,表示该批次的序列号
  • 个数(count):占4个字节,表示该批次有多少键-值对
  • 类型(type):0x0表示删除该批次键-值对,0x1表示新增键-值对

恢复

当 log 对应的 mem-table已经写入磁盘后,log就可以删除了
如果启动时发现有 log 则需要做对应的恢复处理
恢复的实现在: db/db_impl.cc 中
函数为:

1
2
3
Status DBImpl::RecoverLogFile(uint64_t log_number, bool last_log,
                              bool* save_manifest, VersionEdit* edit,
                              SequenceNumber* max_sequence)

触发过程是:

1
DB:Open()  -->  DBImpl::Recover()  -->  DBImpl::RecoverLogFile()

流程:

  1. 创建 log::Reader,然后调用 ReadRecord()读取一个日志块
  2. 创建 MemTable(如果为null的话),并将刚刚读取的日志记录,批量写入到 MemTable 中
  3. 生成SequenceNumber,并更新 max_sequence
  4. 如果 MemTable 大小超过阈值,则写入到 LevelDB 0 中
  5. 之后检查是否要重用上面的 log, options_.reuse_logs
  6. WriteLevel0Table,获取迭代器 mem->NewIterator(),然后将其写入
  7. 写完之后,还会更新 VersionEdit 文件

参考