LevelDB源码5:WAL

技术 · 2022-07-05

源码:

log_writer.cc。

为了不丢失数据,需要持久化的功能。把数据持久化到磁盘上,最常用的技术就是日志技术,也就是当修改数据时,先把对数据的修改写到磁盘上,然后在MemTable里做修改。因为日志记录了每个操作,只要对日志进行重放便可以恢复MemTable,这样就做到了数据库实例崩溃、宕机或者停机维护的时候数据不丢失。

这种日志技术在数据库里面很常用(Redis里的Aof,Innodb里的Redo Log都是这样的技术),一般称为WAL (Write Ahead Log),正如名字一样,就是在写入前先写入日志的意思。

Put操作也不能简单的写入到 memtable,而是要先写日志、再写 memtable,都更新完成后再返回写入成功,write-ahead logging 名字也是由此而来。

因为日志的写入都是Append的,也就是顺序写,所以写磁盘也是顺序写,虽然磁盘的随机写效率比较低,但是顺序写效率还是很高的,所以就算加入了日志,写效率还是很高,依然可以满足写多的场景。

另外可以通过控制日志写同步的策略在性能和可靠性之间做折中:

  • 每次写入都做一次sync,可靠性最高,不会丢数据,但是性能最低;
  • 每次写入,但是不sync,数据库崩溃不会丢数据,但是机器崩溃会丢数据,性能高;
  • 每次写入,不sync,但是每1s做一次sync,数据库崩溃不会丢数据,但是机器崩溃丢最多1s的数据,性能较高。

有了MemTable和WAL,就有了一个功能比较完备的数据库了,类似于一个简化版的Redis。然而这又引入了新的问题:

  • 如果日志量很大,每次重启数据库时,恢复的时间非常长;
  • 内存的容量是有限的,所以当数据量太大,MemTable的大小超过内存容量时,就没法写入数据了。

这时候不仅仅需要将日志写入到磁盘,也需要定时将MemTable的镜像写入磁盘,并且清空MemTable。

leveldb 里的日志写操作主要由leveldb::log::Writer完成。log::Writer类主要负责:接收待写入数据,组织数据格式,调用成员变量完成数据真正写入到文件系统。

日志的格式

例如当前 block 只有10个字节,而用户调用了AddRecord('HelloWorld'),数据会分为两次写入,type 分别为 kFirstType kLastType:

**************  block N  **************
...
|crc 3 kFirstType|hel|
************** block N+1 **************
|crc 7 kLastType|loWorld|
...

可以看到先尝试写入hel填满该 block,标记这次写入为kFirstType,总共占用 10 个字节。然后写入loWorld到下一个 block,标记为kLastType.

如果这个 block 写不下,那么就标记为kMiddleType.

type 是一个枚举值:

enum RecordType {
  // Zero is reserved for preallocated files
  kZeroType = 0,

  kFullType = 1,

  // For fragments
  kFirstType = 2,
  kMiddleType = 3,
  kLastType = 4
};

如果 block 剩余的空间不足7 bytes,写不下 header,那么就补\0填满。

简而言之:一个block只有32k,来一条数据时,能放得下就放,放不下就能放多少算多少。剩下的放入下个blck,其中用type来标记是第几块。

在 level 中,db_impl.cc会构造log::Writer对象并且写入数据。

WriteBatchInternal::Contents(updates)数据格式图里的 data 部分,这些究竟包含了什么?可以参考写入与读取流程这篇笔记。

//WriterBatch写入log文件,包括:sequence,操作count,每次操作的类型(Put/Delete),key/value及其长度
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
bool sync_error = false;
if (status.ok() && options.sync) {
  //log_底层使用logfile_与文件系统交互,调用Sync完成写入
  status = logfile_->Sync();
  if (!status.ok()) {
    sync_error = true;
  }
}
//写入文件系统后不用担心数据丢失,继续插入MemTable
if (status.ok()) {
  status = WriteBatchInternal::InsertInto(updates, mem_);
}
LevelDB
Theme Jasmine