上篇说了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.