第二次讲解稿子
第二次讲解稿子¶
大家好,下面由我来进行接下来的讲解。首先让我们来看一下DBImpl::Recover函数的实现,总体来说,它做了以下事情:
-
创建数据库目录;
-
对这个数据库里面的LOCK文件加文件锁。
LevelDB是单进程多线程的,需要保证每次只有一个进程能够打开数据库,方式就是使用了文件锁,如果有其它进程打开了数据库,那么加锁就会失败
-
如果数据库不存在,说明是新打开的DB,那么调用DBImpl::NewDB创建新的数据库;
-
如果是重启DB,调用VersionSet::Recover来读取MANIFEST中每次版本的更改,恢复当前版本信息;
-
根据版本信息,搜索数据库目录,找到关闭时没有写入到SSTable的日志,按日志写入顺序逐个恢复日志数据。
-
DBImpl::RecoverLogFile会创建一个MemTable,开始读取日志信息,将日志的数据插入到MemTable并根据需要调用DBImpl::WriteLevel0Table将MemTable写入到SSTable中
然后我们来具体看一下它的流程。
- 第一步是创建目录,然后尝试获取它的文件锁,失败则返回
- 第二步根据CURRENT文件是否存在以及option参数执行检查。
如果文件不存在且create_if_missing这个参数为真,说明需要新创建数据库,调用函数NewDB()创建即可。
- 第三步调用VersionSet的Recover()函数,就是从文件中恢复数据。如果成功则向下执行第四步。
第四步是函数的重点,尝试从所有比manifest文件中记录的log要新的log文件中恢复。
之所以要进行第四步的理由是:因为前一个版本可能会添加新的log文件,却没有记录在manifest中。最后如果发现有大于原信息记录的log编号的log文件,则需要回放log,更新db数据。回放期间db可能会dump新的level 0文件,因此需要把db元信息的变动记录到edit中返回。
首先,要先找出所有满足比manifest文件记录的log编号更新的log文件。
之前的MANIFEST恢复,会得到版本信息,里面包含了之前的log number,搜索文件系统里的log,如果这些日志的编号 >= 这个log number,那么这些日志都是关闭时丢失的数据需要恢复。这里的代码实现有一个有趣的小细节:
min_log存了原记录的log编号,为什么判断条件里已经有了number>=min_log,还有numberi==prev_log呢?
事实上,函数PrevLogNumber()已经不再用了,这里仅为了兼容LevelDB的老版本。
接下来,因为这里的日志是按顺序存储在logs里面,所以找到log文件后,首先排序,保证按照生成顺序,依次回放log。并把sstable文件的变动追加到edit中返回。
这里有一点需要注意,就是前一版可能在生成该log编号后没有记录在MANIFEST中,所以这里我们手动更新VersionSet中的文件编号计数器。
最后,我们要更新VersionSet的sequence。以上便是DBImpl::Recover函数的实现。
接下来我们要来看一下DBImpl::NewDB函数的实现
第一步要产生DB元信息,设置比较器名称,当前分配日志文件的编号,下一个待分配的文件编号。
这里下一个待分配的文件编号是2,因为1分配给了MANIFEST文件。
第二步要产生MANIFEST文件,将db元信息写入它。
第三步是如果没出问题,就把MANIFEST文件名写入到CURRENT文件中,让CURRENT文件指向这个MANIFEST文件。
总结来看,DBImpl::NewDB出人意料的简单
一个新的数据库没有任何数据,所以不需要日志和SSTable,只需要有一个MANIFEST文件,再包含一些元数据就行了。最后CURRENT要指向新创建的MANIFEST文件。
最后我们看一下VersionSet::Recover函数的实现。概括来说,它会通过读取 CURRENT 文件,找到当前正在使用的 MANIFEST 文件;然后它会读取 MANIFEST 文件,将数据库恢复到上次关闭前的状态,完成了MANIFEST文件的读取和版本的构造。
这样做的原因是:恢复出版本信息后,安装这个版本,那么数据库的元数据就恢复到了关闭时候的状态,这个数据库才准备好了可以读写了。
当然这里要说一下我们恢复的是数据库哪些状态?我们需要知道数据库里有哪些SSTable文件,每个文件处于哪个Level,当前日志的编号等信息。
走进函数的细节,第一步它要读取CURRENT文件的内容,获取当前使用的MANIFEST文件。
在这里需要注意current的文件的格式问题,它正常来说应该以换行符做结尾。然后current中存放的是MANIFEST的文件名,要据此转化为完整路径才能打开
第二步,要读取MANIFEST文件,将里面的VersionEdit读取应用到一个builder里。
具体来说,就是读取 MANIFEST 文件中的每一条 Record,把 Record 解码成一个 VersionEdit,然后调用 builder.Apply(&edit) 将 edit 应用到 builder 中。
读取之后当然要记得关闭文件。
但以上都不是VersionSet::Recover的核心部分。它的第三步是基于 MANIFEST 里的 VersionEdit列表,构造一个新的 Version,这一步尤为重要。这里ReuseManifest检查是否可以继续使用当前的 MANIFEST 文件,那么它检查的标准是什么?
通过进一步的挖掘,我们通过函数间的调用关系可以看到这里主要是通过MANIFEST的大小来判断。如果大于2M,那么就不会重用,而是将当前状态写入到一个新的MANIFEST文件里。这种大文件不重用的方式有什么好处呢?
这样可以避免打开的时候读取太大的MANINFEST,使得打开时间太长。如果它不能继续使用的话,需要把当前的 MANIFEST 进行保存。
我今天讲了两个Recover有关的函数,现在就让我们一起来会看Recover。
数据库每次启动时,都会有一个recover的过程,简要地来说就是利用Manifest信息重新构建一个最新的version。这个过程大概有4步:
1.首先,利用Current文件读取最近使用的manifest文件;
2.接下来,创建一个空的version,并利用manifest文件中的session record依次作apply操作,还原出一个最新的version,注意manifest的第一条session record是一个version的快照,后续的session record记录的都是增量的变化;
3.然后,将非current文件指向的其他过期的manifest文件删除;
4.最后,将新建的version作为当前数据库的version
要整体梳理一下的话,启动数据库时通过Current文件加载Manifset文件,读取Manifest文件完成版本信息恢复。
更新与Compaction的进行,LevelDB会不断增删文件,需要一个文件来记录文件列表,这个列表就是manifest文件。
由于每次启动都会新建一个Manifest文件,因此leveldb当中可能会存在多个manifest文件。DB需要知道最新的manifest,必须将manifest准备好后进行原子切换,这就是CURRENT文件的作用。
结合对代码的讲解,我们能看到CURRENT 文件中存储的是当前正在使用的 MANIFEST 文件。当创建新的 MANIFEST 文件时,LevelDB 会先更新 CURRENT文件,使其指向新的 MANIFEST 文件,然后再将旧的 MANIFEST 文件删除。
若没有 CURRENT 文件,新 MANIFEST 文件创建后,还没来得及删除旧的 MANIFEST 文件。
假如系统此时崩溃了,那么 LevelDB 恢复时就不知道当前正在使用的 MANIFEST 文件是哪个,也就无法恢复到正确的状态。
最后我来谈一个进一步的疑问,就是manifest文件丢失,还能否修复吗?
当leveldb的manifest文件丢失时,所有版本信息也就丢失了,但是本身的数据文件还在。
leveldb提供了Repairer接口供用户进行版本信息恢复。具体的恢复过程如下所示:
1.按照文件编号的顺序扫描所有的sstable文件,获取每个文件的元数据以及最终数据库的元数据(sequence number等);
2.将所有sstable文件视为0层文件(由于0层文件允许出现key重叠的情况,因此不影响正确性);
3.创建一个新的manifest文件,将扫描得到的数据库元数据进行记录
这个方法确实能保证修复,但是问题在于:效率低下
因为需要对整个数据库的文件进行扫描;因为0层文件数量太多导致合并次数过大