0%

levelDB源码剖析(9)--WAL日志

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:
// 执行的操作就是构造Header,
// 调用WritableFile的append和flush,将record刷到物理磁盘上
Status EmitPhysicalRecord(RecordType type, const char *ptr, size_t length);

WritableFile *dest_; // 可写入的WAL文件
int block_offset_; // 当前的偏移值
uint32_t type_crc_[kMaxRecordType + 1]; // CRC校验

public:
// 构造函数,此时要求dest是空的或存在dest_length长度的内容
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

// 初始化CRC,每个构造函数都调用这个函数
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_);
}

// 构造函数2
Writer::Writer(WritableFile *dest, uint64_t dest_length)
: dest_(dest), block_offset_(dest_length % kBlockSize)
{
InitTypeCrc(type_crc_);
}

// 析构函数
Writer::~Writer() = default;

// 接口,添加一个Record
Status Writer::AddRecord(const Slice &slice)
{
const char *ptr = slice.data(); // 获取record的数据和长度
size_t left = slice.size();

Status s;
// begin表明本条记录是第一次写入,即当前块中第一条记录
bool begin = true;
do
{
// 当前块剩余空间,用于判断头部能否完整写入
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
if (leftover < kHeaderSize)
{
// 如果块剩余空间小于七个字节且不等于0,说明当前无法完整写入数据
// 此时填充\x00,从下一个块写入
if (leftover > 0)
{
static_assert(kHeaderSize == 7, "");
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
}
// 此时块正好写满,将block_offset_置为0,表明开始写入新的块
block_offset_ = 0;
}

// Invariant: we never leave < kHeaderSize bytes in a block.
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;
// end表明该记录是否已经完整写入,即最后一条记录
const bool end = (left == fragment_length);

// 下面判断是哪一种类型
if (begin && end)
{
// 记录为第一条且同时又是最后一条,说明当前是完整的记录,状态为kFullType
type = kFullType;
}
else if (begin)
{
// 只有头部
type = kFirstType;
}
else if (end)
{
// 只有结尾
type = kLastType;
}
else
{
// 既不是头也不是结尾
type = kMiddleType;
}

// 把已经写入的Record部分发射到物理磁盘中
s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;
} while (s.ok() && left > 0);
return s;
}

// 把一条Record发射保存到物理磁盘中
Status Writer::EmitPhysicalRecord(RecordType t, const char *ptr,
size_t length)
{
assert(length <= 0xffff); // Must fit in two bytes
assert(block_offset_ + kHeaderSize + length <= kBlockSize);

// 缓存header并计算出crc
char buf[kHeaderSize];
buf[4] = static_cast<char>(length & 0xff);
buf[5] = static_cast<char>(length >> 8);
buf[6] = static_cast<char>(t);

// 计算crc
uint32_t crc = crc32c::Extend(type_crc_[t], ptr, length);
crc = crc32c::Mask(crc);
EncodeFixed32(buf, crc); // 编码

// 调用文件的相关方法来把header写入磁盘,然后把剩余部分也写入文件
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; // 更新block的偏移位置
return s;
}

其实写入逻辑也是很简单的,就是看一下当前的Block够不够,如果够的话就直接把Record写入并将类型设置为kFullType。如果当前块剩余空间小于7字节(小于header需要的字节数目)就新开一个块,并重复执行上述操作。如果当前块能够写下一部分,就把Record的一部分写入到这个块中,并设置类型为kFirstType,剩余的部分继续执行上述操作,可能会被设置为kMiddleType、kLastType这些类型,直到一条Record写完。

总结

WAL日志部分的写入还是很简单的,这个类的设计的接口其实只有一个,使用起来非常方便。

另外AddRecord这个函数中的循环写入部分也是非常有意思的。