前言
rocksdb有多种compaction策略,默认的compaction策略应该为leveled compaction,我们本次分析leveled compaction机制。本次分析主要涉及到几个问题:
- compaction何时被触发
- compaction具体流程
- compaction如何与mvcc机制联动
前置知识
- 一个库的SST文件有多个
- 每个SST文件都属于某一层
- 除了第0层的SST文件,其余层的SST文件之间都是有序的
- 除了第0层以外,其余层的大小都是固定的。compaction的目标是将这些level的数据大小限制在阈值之下。level的大小通常呈指数级增长。
如何做compaction--宏观层面
当L0层的文件数据达到level0_file_num_compaction_trigger,compaction会被触发,L0层的文件将会merge到L1层。由于L0层的SST文件之间是无序的,所以在做compaction时,一般来说会将L0层的所有文件全部merge到L1层。
当做完L0到L1层的compaction后,L1层的SST文件大小可能会超过配置的阈值,此时需要在L1层挑选一个SST文件,merge到L2层。至于挑选哪个文件,rocksdb有具体的策略。
接下来L2,L3,...层的SST文件大小也可能会超过配置的阈值,那么重复上面的过程即可。
rocksdb支持多个compaction任务并发进行。
由于L0层的SST文件之间是无序的,所以L0层到L1层的compaction是无法直接并发的,这可能会成为瓶颈。rocksdb提供了一个参数,该参数可以使得L0层到L1层的compaction被分割成sub-compact, sub-compact之间是可以并发的。
Compaction Picking
所谓Compaction Picking主要涉及到两个问题:
- 当多个level的SST文件总size都达到了阈值,那应该优先对哪个level做compact
- 对于一个level内的多个SST文件,应该优先选择哪个文件做compact
挑选哪个level
为了确定不同level做compact的优先级,rocksdb给每个level赋予一个score,得分越高的level,其做compact的优先级更高。每层的score计算方式如下:
- 对于非L0层的层,score的计算方式为,level总SST的大小除以该level对应的要做compact的阈值
- 对于L0层的score,计算方式为max(L0层所有文件的格式/level0_file_num_compaction_trigger, L0层所有SST文件总大小-max_bytes_for_level_base)
挑选哪些SST文件
- 挑选得分最高的level,记为Lb作为将要做compact的输出level
- compact后输出的SST文件应该位于Lb+1, 记为Lo
- 在Lb层找到优先级最高的SST文件,该文件将要被compact,如果该文件已经在一个compact_job中了,或者该文件的KeyRange对应Lo层对应的文件已经在一个compact_job中,则跳过该文件,选择下一个优先级最高的文件,重复当前步骤,直到直到一个候选文件,将文件加入compaction inputs集合中。
- 扩展inputs集合,直到inputs集合中的所有SST文件,有一个清晰的(clean out)边界。举个例子,假设Lb层有5个SST文件, 如下所示。如果inputs集合最开始包含的文件是f3, 因为f3的a4与f2的a4重合,f3的a6与f4的a6重合,所以还需要将f2和f4加入inputs集合。这么做的原因应该是为了便于读同一个key的多版本数据。
f1[a1 a2] f2[a3 a4] f3[a4 a6] f4[a6 a7] f5[a8 a9]
- 检查inputs集合中的SST文件是否有与正在做compact的文件重合,如果有重合且并且manual compaction不可用,则abort当前的compaction pick job。
- 在Lo层找到与inputs集合中所有SST文件有交错的SST文件,然后按照步骤4扩展,如果这些文件有正在做compact的,则abort当前的compaction pick job,否则,将这些文件放入output_level_inputs集合。
- 一个可选的优化做法就是,如果扩展inputs集合中的SST文件,但是不会导致output_level_inputs集合的SST文件增加,那么就扩展之,举个例子, 如果当前f2在inputs集合中,f6在output_level_inputs集合中,那么可以将f3也加入inputs集合中,因为将f3加入inputs集合中,我们并不会损失什么。
Lb: f1[B E] f2[F G] f3[H I] f4[J M]
Lo: f5[A C] f6[D K] f7[L O]
- inputs集合中的SST文件和output_level_inputs集合中的SST文件,就是此次compact的candidate文件。
如何做compaction--代码层面
主要调用栈:
- BGWorkCompaction
- BackgroundCallCompaction
BackgroundCallCompaction函数调用BackgroundCompaction,完成compaction主流程,另外,还会调用FindObsoleteFiles,清理过期文件。 - BackgroundCompaction
BackgroundCompaction这个函数比较长,我们主要分析主干,细枝末节先放弃。主流程:
- 从全局队列,compaction_queue_,获取等待做compact的cf
- 调用PickCompaction,用于挑选需要做compaction的文件,具体的信息记录在Compaction对象中
- 尽管当前的compact未开始执行,但是从一个level挑选一些SST文件做compact后,此时该level的score就发生了变化。这里,rocksdb会再次检测该cfd是否需要做compact,如果需要的话,那么将该cfd再次加入全局compact队列
- 当挑选完参与本次compact的SST文件列表后,接下来构造一个CompactionJob对象,整个compact的核心逻辑都封装在这个对象中
- 调用ompaction_job.Run(),执行主要的compact动作
- 调用compaction_job.Install, 猜测和全局的Version管理相关,待核实。
- 做一些善后工作,return
- CompactionJob::Run
这个函数的主要工作是调用ProcessKeyValueCompaction,ProcessKeyValueCompaction执行完,compact的大部分工作就完成了。另外有一点需要关注的地方,就是执行完ProcessKeyValueCompaction函数后,会生成一些output SST,对于这些SST的信息,该函数将其保存到一个hashmpa中(TablePropertiesCollection)。 - ProcessKeyValueCompaction
这个函数很长,而且涉及到的模块很快,所以比较复杂,我们还是一样只分析主干:
- 调用MakeInputIterator构建一个迭代器记为input,这个迭代器可以迭代所有参与compaction的SST文件
- 如果用户指定了compaction filter,那么则初始化,否则compaction filter为null
- 如果用户指定了merge operator,那么则初始化,否则可以忽略
- 调整input迭代器定位到起始位置
- 构建CompactionIterator,CompactionIterator又是一个大杂烩,包括了input迭代器,当前系统的快照信息,compaction filter,merge filter等一些成员。ompaction的过程就是迭代input sst文件,并对kv对做处理的过程,CompactionIterator就封装了处理逻辑
- 迭代CompactionIterator,对于每一个CompactionIterator的输出,都将其写入output文件中,迭代的过程中,如果output文件写满,则切换文件。
compact中的Iterator
一般来说,Compaction的Input涉及两层数据的合并,对于涉及到的每一层数据:如果是level-0,对level-0的每一个sstable文件建立一个Iterator, 因为Level-0的sstable之间存在Overlap。如果不是level-0,对该 level 的所有sstable文件建立一个TwoLevelIterator。
所谓TwoLevelIterator指的是,我们遍历的数据有两层,第二层依赖于第一层。举个例子:对于同一个level的一个SST集合来说,如果我们要遍历这个SST集合中的所有kv对,那么需要首先确定要遍历哪个SST文件,然后要确定要遍历SST文件的哪个位置。对于TwoLevelIterator:
first_level_iter_: 表示该层所涉及到Compaction 的 file 元信息组成的 LevelFileNumIterator
second_level_iter_: 表示当前所使用的数据 Block 构成的 Iterator
它们的组合将这一层所有sstable 的数据连接起来,构成了一个整体的Iterator。这样,对于Compaction所用到的所有sstable文件,都建立了与之相关联的Iterator。最后将建立的所有Iterator合并,构成一个整体性的MergeIterator。这就是VersionSet::MakeInputIterator的操作过程。
LevelFileNumIterator
LevelFileNumIterator的内容为该层所有文件的元信息:LevelFilesBrief,以下是 LevelFileNumIterator 中两个主要的函数实现:
Slice key() const override
{
assert(Valid());
return flevel_->files[index_].largest_key;
}
Slice value() const override
{
assert(Valid());
auto file_meta = flevel_->files[index_];
current_value_ = file_meta.fd;
return Slice(reinterpret_cast<const char*>(t_value_), sizeof(FileDescriptor));
}
key():表示的是当前index的sstable的largest_key。
value():表示的是是个由当前sstable文件的FileDescriptor信息,即包含其file number 和 file size。
TwoLevelIterator
TwoLevelIterator为一个两层结构的迭代器,其类成员包括两个IteratorWrapper:
IteratorWrapper first_level_iter_;
IteratorWrapper second_level_iter_;
IteratorWrapper是对InternalIterator封装的,引入这个类的原因是为了缓冲key,避免多次调用虚函数带来的开销。上述的LevelFileNumIterator即为此处的first_level_iter,当TwoLevelIterator初始化时,它会调用自身的InitDataBlock()函数。InitDataBlock函数根据自身的state和first_level_iter_.value(),构建第二层Iterator。
整体上来看,这个TwoLevelIterator内容为这一层的所有文件数据,第一层Iterator:表示这一层所有文件的元信息;第二层Iterator:表示当前文件的数据信息。以下是TwoLevelIterator的Key-Value成员函数的代码,可以看出,它的Key-Value即表示当前sstable Iterator的当前Key-Value:
virtual Slice key() const override
{
assert(Valid());
return second_level_iter_.key();
}
virtual Slice value() const override
{
assert(Valid());
return second_level_iter_.value();
}
Next() 操作定位下一个Key-Value,如果当前Block已结束,second_level_iter_移动到下一个Block
void TwoLevelIterator::Next()
{
assert(Valid());
second_level_iter_.Next();
SkipEmptyDataBlocksForward();
}
void TwoLevelIterator::SkipEmptyDataBlocksForward()
{
while (second_level_iter_.iter() == nullptr
|| (!second_level_iter_.Valid() && !second_level_iter_.status().IsIncomplete()))
{
// Move to next block
if (!first_level_iter_.Valid())
{
SetSecondLevelIterator(nullptr);
return;
}
first_level_iter_.Next();
InitDataBlock();
if (second_level_iter_.iter() != nullptr)
{
second_level_iter_.SeekToFirst();
}
}
}
MergingIterator
MergingIterator用于将参与本次compaction涉及到的所有子迭代器聚合在一起,构建成一个MergingIterator。所谓的子迭代器指的参与此次compaction的所有L0层的每个SST文件的迭代器,以及参与此次compaction的非L0层的每一层的迭代器,至于为什么这么构建前文已经有论述。
IteratorWrapper* current_;
autovector<IteratorWrapper, kNumIterReserve> children_;
MergerMinIterHeap minHeap_;
从结构上来说,一个MergingIterator由一个或多个childr组成,每一个child也是一个Iterator,所有的子迭代器通过堆的数据结构组织,MergingIterator维护了一个小顶堆,用于持续向外输出当前剩余数据的最小值,其实维护小顶堆的过程就是解决多数组topk问题的过程。
MergingIterator的Key和Value成员函数均返回的是当前子迭代器current_的Key与Value:
virtual Slice key() const override
{
assert(Valid());
return current_->key();
}
virtual Slice value() const override
{
assert(Valid());
return current_->value();
}
current_ 默认为堆顶的child,即表示数值最小的child。在Compaction过程中,这些children表示的是某一个sstable(level-0层)或者某一层sstable数据。
对于MergingIterator的迭代过程,以下是Next() 成员函数的部分节选:
virtual void Next() override
{
current_->Next();
if (current_->Valid())
{
minHeap_.replace_top(current_);
}
else
{
minHeap_.pop();
}
current_ = CurrentForward();
}
MergingIterator的Next一般来说表示的就是current_的Next,但是当current_迭代结束之后,就会取下一个child作为current。
从Compaction的角度来说,也就是MergingIterator的Next操作也就是涉及到所有sstable数据中,Key从小到大顺序遍历的过程(direction=kForward)
CompactionIterator
其实compaction的过程就是迭代input sst文件,并对kv对做处理的过程,CompactionIterator就封装了处理逻辑,其主要数据成员包括:
InternalIterator* input_;
MergeHelper* merge_helper_;
std::vector<SequenceNumber>* snapshots_;
const CompactionFilter* compaction_filter_;
input_用于遍历所有参与这次compaction的SST文件。
merge_helper_封装了用户定义的merge operator,用于在迭代SST文件时处理merge操作。
snapshots_记录了此次compaction时当前系统存在的snapshot,因为一个key在SST中可能存在多个版本,根据snapshots_可以确定哪些版本的值已经不被用户可见并且可以丢弃了。举个例子,对于下面的kv对序列,假设系统存在两个snapshot,key1-v2这个数据,无论如何也不会被用户读到了,所以compaction时就可以丢弃掉。
compaction_filter_记录了用户定义的compaction filter,输出数据时会被调用。
CompactionIterator有几个关键函数:
- NextFromInput是compaction filter关键函数。该函数读取input_迭代器当前指向的key和value,然后根据该key是否满足某些条件,做出一些动作,比如当前指向的key是否和上一个key相同,如果相同则保留当前版本(高版本)的数据。比如当前指向的key的类型为kTypeSingleDeletion或者为kTypeDeletion或者为kTypeMerge,都会根据响应的操作符语义做出一些处理。
- Next函数用于获取下一个可以输出的key和value,所谓可以输出指的是key和value可以写入output SST文件中。Next函数的整个逻辑比较简单:
Next() {
input_->Next();
NextFromInput();
}
配置参数
- level0_file_num_compaction_trigger: 当L0的个数超过这个值时,会触发compaction
- max_bytes_for_level_base: L1的SST文件总大大小阈值,L2层SST文件总大小阈值为max_bytes_for_level_base*ratio, L3层,L4层,以此类推
- target_file_size_multiplier: 下一层单个SST文件是上一层单个SST文件大小的几倍
- max_bytes_for_level_multipliers: 下一层SST文件的总大小最大值是当前层SST文件总大小最大值的几倍
- num_levels: LSM树最多有多少层