您的位置:首页 > 其它

leveldb源码剖析---版本管理

2017-03-29 13:19 309 查看
所谓的版本,简单地说,指的是leveldb中各个level层的文件信息。显然,随着compaction的进行和新的memtable写入生成新的sstable,版本会不断变化。版本除了记录各层的文件信息外,还记录各层关于compaction的的信息,比如在对于当前版本,最适合进行compaction的level是哪层,以及这层中最适合compaction的是哪个文件等信息。

和版本相关的类主要是以下三个:

class Version

class VersionSet

class VersionEdit

其中version是单独一个版本的信息,VersionSet是版本信息的集合,里面包含多个通过链表组织的版本,VersionEdit主要是记录版本的变化信息。下面我们将逐一介绍这三个类,以及通过观察版本的变化来了解它们的作用以及如何工作。

先来看一下版本(Version)的数据成员



vset_指针是指向包含该版本的版本集合,一般每个leveldb实例中都有一个版本集合,用于管理从启动开始产生的所有版本信息。因为版本集合是通过链表的信息对版本进行管理,因此下面两个数据成员就是链表指针。refs_是一个计数器,用于生命周期管理。再继续就是对应该版本的各个层的文件信息 了,这是一个二维数组,数组中的每个子数组代表一个层,子数组中的每个元素是对应该层的一个文件的元信息。最后面三个成员主要是用于统计当前版本的compaction信息,通过这些信息可以快速判断应该对哪个level进行compaction。

VersionEdit**用于记录对版本的修改**,其中的关键数据成员是deleted_files_和new_files_。deleted_files是一个set,包含在当前版本在此次修改中被删除的文件,new_files_是一个数组,记录此次修改中新增加的文件。

因此通过从当前版本中的删去包含在deleted_files_中的文件,以及添加包含在new_files_中的文件,就可以得到一个新的版本。

leveldb通过VersionSet管理自数据库启动开始的所有版本。通过一个链表将所有版本连接起来,最新的版本位于链表尾部。这个链表是一个双向链表,它的头部用dummy_versions_数据成员标记。VersionSet_中的current_数据成员指向当前数据库的版本信息。VersionSet_中的数据成员compact_pointer_数组主要是标记每个level下次进行compaction时应该选择哪个文件。compact_pointer_里面是一个key数组,比如如果下次进行compaction的是level i,则应该选择level i中第一个key大于compact_pointer_[i]的文件,同时会更新compact_pointer_[i],保证下次compaction会从一个新的文件进行。

VersionSet除了管理版本的文件信息之外,还会管理数据库的缓存,日志信息等等内容。

下面我们通过一个实际的版本变化过程看一下版本管理的工作流程。

触发版本变化的主要有两个地方

将memtable写入磁盘,生成新的sstable

compaction操作

下面我们分别看一下在这两个操作下,版本信息是怎么进行管理的。

memtable写盘

这个过程主要是在DBImpl::CompactMemTable函数中完成

以下我们摘取这个函数的部分代码分析一下

VersionEdit edit;
Version* base = versions_->current();
base->Ref();
Status s = WriteLevel0Table(imm_, &edit, base);
base->Unref();


这里主要是申请了一个VersionEdit变量,并把他传入到WriteLevel0Table函数中,用于记录新生成的sstable文件信息。我们追踪到WriteLevel0Table函数里面就可以看到下面的代码:

edit->AddFile(level, meta.number, meta.file_size,
meta.smallest, meta.largest);


这个函数将新生成的sstable文件的元信息加入到VersionEdit的new_files_中。

再回到CompactMemTable函数:

if (s.ok()) {
edit.SetPrevLogNumber(0);
edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
s = versions_->LogAndApply(&edit, &mutex_);
}


这个就是版本管理的关键了。其中LogAndApply函数将根据前面记录的版本修改信息更新当前版本,得到一个新的版本信息。同时把这个新的版本信息设置成当前VersionSet的current_,并连入版本集合的链表中。

这个函数我们后面再介绍,这里我们只需要知道根据修改信息生成新的版本就可以了。

可以看到,对于CompactMemTable,版本的修改信息很简单,就是在new_files_里面添加一个新的文件元信息。

compaction操作

这个工作主要是在函数DBImpl::BackgroundCompaction中完成。前面我们分析的时候知道,对level和level+1中的文件进行compaction时会有两中情况

当level+1中没有文件和level中的那个待compaction的文件的key范围重合时,此时直接将level中的那个文件移动到level+1中的那个文件即可。

level+1中有文件和level中的那个文件key范围重合。则进行常规的compaction。

下面我们分别从这两种情况看一下版本管理流程。

else if (!is_manual && c->IsTrivialMove()) { //当input[0](level)中只有一个需要compaction的文件,input[1](level+1)中没有需要compaction的文件
// Move file to next level
assert(c->num_input_files(0) == 1);
FileMetaData* f = c->input(0, 0); // 将 f 从 c->level移动到 c->level + 1中即可

c->edit()->DeleteFile(c->level(), f->number);
c->edit()->AddFile(c->level() + 1, f->number, f->file_size,
f->smallest, f->largest);
status = versions_->LogAndApply(c->edit(), &mutex_); //只需要在edit中记录就可以了


对于第一种情况,VersionEdit将level中的那个文件的元信息删除。同时把这个元信息移动到level+1中,最后调用LogAndApply更新当前版本信息。

对于第二种情况,主要是由DoCompactionWork函数中InstallCompactionResults函数的完成。

BackgroundCompaction -> DoCompactionWork -> InstallCompactionResults

下面分析一个InstallCompactionResults函数:

compact->compaction->AddInputDeletions(compact->compaction->edit());
const int level = compact->compaction->level();


前面我们分析过compaction->input数组中包含了本次compaction操作所需要的所有sstable文件信息,同时compaction->output中包含了所有新生成的文件信息,因此我们很自然的想法是将inputs_中的文件信息加入到VersionEdit的deleted_files中,而将outputs中的文件信息加入到它的new_files_中,InstallCompactionResults正是这么做的。上面的AddInputDeletions函数就是将input_的文件信息加入到edit的deleted_files中,而下面的for循环

for (size_t i = 0; i < compact->outputs.size(); i++) {
const CompactionState::Output& out = compact->outputs[i];
compact->compaction->edit()->AddFile(
level + 1,
out.number, out.file_size, out.smallest, out.largest);
}


则将output_中的文件信息加入到new_files数组中。

versions_->LogAndApply(compact->compaction->edit(), &mutex_);


最后一如既往调用LogAndApply函数将修改信息更新到当前版本,生成一个新的版本信息。

下面我们分析一下LogAndApply函数

LogAndApply函数的实现

用一个式子可以完全概括LogAndApply函数的作用:

v = current_ + edit;


其中current是当前版本的信息,edit是版本的修改信息,v是在当前版本上修改得到的新的版本信息。

我们从下面的代码开始看起:

Version* v = new Version(this);
{
Builder builder(this, current_);
builder.Apply(edit);
builder.SaveTo(v);
}


这里先new一个空的版本信息,然后将current_经过edit更新后得到的新版本保存到v中。其实说白了,就是将edit的new_files_中的文件信息添加到current的文件信息中,将edit的deleted_files中的文件信息从current中删除掉,这样就得到了一个新的版本,将这个版本赋值给v。

Finalize(v);// Precomputed best level for next compaction


正如注释所说,这里是计算对于这个新版本的compaction操作信息,比如算出哪个level最适合进行compaction,以备下次compaction的时候使用。所以主要工作就是计算version->compaction_level_和version->compaction_score_。然后会将这个edit写入到manifest文件中。这涉及到日志系统,我们后面再介绍。最后的AppendVersion(v)会将这个新的version设置到当前数据库的versionset中,以及将数据库的当前版本current_也设置为v。

到此,我们基本就把leveldb的版本管理介绍完毕了。

总结

一个健壮的数据库系统,版本信息是必不可少的。leveldb的版本信息主要是记录数据库的文件信息。随着compaction操作和memtable的写盘,数据库中的文件会不断发生变化,因此对于每次可能生成新的sstable文件或者可能删除已有的sstable文件的操作,都必须进行版本更新。这里我们从以上两个操作来跟踪了解了leveldb的版本更新工作流程。leveldb通过versionedit记录版本的修改信息,然后通过LogAndApply函数将这个修改信息更新到当前的版本(current_),生成新的版本信息。leveldb还通过versionSet将从系统启动开始经历的所有版本信息放在一个集合中进行管理,这个集合是用链表支持的,最新的版本总是Append到链表的尾部。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: