WAL日志的格式与构建
源码位置与说明
db/log_format.h : 定义了RecordType和一些常量
db/log_writer.h db/log_writer.cc :
主要实现Writer::AddRecord,写一条记录到日志中
db/log_reader.h db/log_reader.cc :
主要实现Reader::ReadRecord,读取一条日志记录
LevelDB写入一个kv时,都会先向日志里写入一条记录,这种日志一般称为WAL,也就是Write
Ahead
Log。这种日志最大的作用就是将对磁盘的随机写转换成了顺序写。当故障宕机时,可以通过WAL进行故障恢复。控制每次WAL写入磁盘的方式,可以控制最多可能丢失的数据量。
WAL里的内容实际就是内存里MemTable内容的持久化,当一个MemTable写满后,开启一个新的MemTable时,也同时会开启一个新的WAL,当MemTable被Dump到磁盘后,相应的WAL可以被删除。
WAL的格式很简单,由一系列32KB的Block组成,当然最后一个块可能是不满的,正在写入中。
而每个Block包含连续的Record,其中每个Record的格式为crc+长度+类型+value。一条记录可能全部写到一个块上,也可能跨几个块。且其中每个Record可以是不同的类型的,有以下几种类型。
- kZeroType:为预分配的文件保留。
- kFullType:表示一条记录完整地写到了一个块上。
- kFirstType:表示该条记录的第一部分。
- kMiddleType:表示该条记录的中间部分。
- kLastType:表示该条记录的最后一部分。
可能感觉为什么需要设置这么多Record类型,原因很简单。因为WAL是由Block组成的,注意这个Block不是前面SSTable中的Block类,这里的Block指的是一种逻辑上的结构,为了便于读取,WAL中的Block大小都是固定的,为kBlockSize = 32768
。而每条Record的大小不是固定的,因此可能会出现当前Block中放不下插入的Record,所以需要将Record进行分块插入到不同的Block中,这个类型就是为了标识出这条Record是不是被拆分之后的结果,如果是的话,那么这条Record位于未被拆分的Record中的哪部分。
这部分可以在doc/log_format.md
中找到
源码部分
我们先来看一下头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Writer { private: Status EmitPhysicalRecord(RecordType type, const char *ptr, size_t length);
WritableFile *dest_; int block_offset_; uint32_t type_crc_[kMaxRecordType + 1];
public: explicit Writer(WritableFile *dest); Writer(WritableFile *dest, uint64_t dest_length); Writer(const Writer &) = delete; Writer &operator=(const Writer &) = delete; ~Writer();
Status AddRecord(const Slice &slice); };
|
其实这个Writer接口还是非常简单的,只有一个AddRecord函数可以用来添加日志。我们来看一下它的具体实现是什么样的。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
|
static void InitTypeCrc(uint32_t *type_crc) { for (int i = 0; i <= kMaxRecordType; i++) { char t = static_cast<char>(i); type_crc[i] = crc32c::Value(&t, 1); } }
Writer::Writer(WritableFile *dest) : dest_(dest), block_offset_(0) { InitTypeCrc(type_crc_); }
Writer::Writer(WritableFile *dest, uint64_t dest_length) : dest_(dest), block_offset_(dest_length % kBlockSize) { InitTypeCrc(type_crc_); }
Writer::~Writer() = default;
Status Writer::AddRecord(const Slice &slice) { const char *ptr = slice.data(); size_t left = slice.size();
Status s; bool begin = true; do { const int leftover = kBlockSize - block_offset_; assert(leftover >= 0); if (leftover < kHeaderSize) { if (leftover > 0) { static_assert(kHeaderSize == 7, ""); dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover)); } block_offset_ = 0; }
assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
const size_t avail = kBlockSize - block_offset_ - kHeaderSize; const size_t fragment_length = (left < avail) ? left : avail;
RecordType type; const bool end = (left == fragment_length);
if (begin && end) { type = kFullType; } else if (begin) { type = kFirstType; } else if (end) { type = kLastType; } else { type = kMiddleType; }
s = EmitPhysicalRecord(type, ptr, fragment_length); ptr += fragment_length; left -= fragment_length; begin = false; } while (s.ok() && left > 0); return s; }
Status Writer::EmitPhysicalRecord(RecordType t, const char *ptr, size_t length) { assert(length <= 0xffff); assert(block_offset_ + kHeaderSize + length <= kBlockSize);
char buf[kHeaderSize]; buf[4] = static_cast<char>(length & 0xff); buf[5] = static_cast<char>(length >> 8); buf[6] = static_cast<char>(t);
uint32_t crc = crc32c::Extend(type_crc_[t], ptr, length); crc = crc32c::Mask(crc); EncodeFixed32(buf, crc);
Status s = dest_->Append(Slice(buf, kHeaderSize)); if (s.ok()) { s = dest_->Append(Slice(ptr, length)); if (s.ok()) { s = dest_->Flush(); } } block_offset_ += kHeaderSize + length; return s; }
|
其实写入逻辑也是很简单的,就是看一下当前的Block够不够,如果够的话就直接把Record写入并将类型设置为kFullType。如果当前块剩余空间小于7字节(小于header需要的字节数目)就新开一个块,并重复执行上述操作。如果当前块能够写下一部分,就把Record的一部分写入到这个块中,并设置类型为kFirstType,剩余的部分继续执行上述操作,可能会被设置为kMiddleType、kLastType这些类型,直到一条Record写完。
总结
WAL日志部分的写入还是很简单的,这个类的设计的接口其实只有一个,使用起来非常方便。
另外AddRecord这个函数中的循环写入部分也是非常有意思的。