相关的参数
level_compaction_dynamic_level_bytes 在 AdvancedColumnFamilyOptions里
Options 继承了 DBOptions和ColumnFamilyOptions
ColumnFamilyOptions 继承了 AdvancedColumnFamilyOptions
// 整个RocksdDB的层级,包含L0,如果 num_levels 为7,层级就是L0至L6。
int num_levels = 7;
// L0层 所有sst文件的大小超过max_bytes_for_level_base 说明可以合并了
uint64_t max_bytes_for_level_base = 256 * 1048576;
// 是否动态计算每一层的容量 下文细谈
bool level_compaction_dynamic_level_bytes = true;
// 当L0层的sst文件数量超过 level0_file_num_compaction_trigger 就说明这一层需要向下合并了
int level0_file_num_compaction_trigger = 4;
// 默认的compaction模式是 kCompactionStyleLevel
// 别的compaction模式 大家自行学习吧
CompactionStyle compaction_style = kCompactionStyleLevel;
// 一次compaction 所涉及的sst的最大size
// 默认是target_file_size_base * 25
// 可以理解为一次compaction 解析的文件大小 最大是64*25MB
// target_file_size_base 默认是64MB
uint64_t max_compaction_bytes = 0;
Compaction的大流程
咱们首先从最大的粒度来理解Compaction的流程,大概分下面6个步骤
1 确定从哪一层开始进行compaction
2 确定从哪个(或哪几个)文件开始
3 选定合并的目标层
4 确定目标层的哪些文件需要参与合并(可能会再次增加源层文件的数量)
5 把这个n个文件的数据 读出来(包含优先级)
6 在目标层生成sst文件,把数据写进去
上面的流程其实可以再细分以下,也就是两个步骤。
1 确定compaction的范围(上面的前四步)
2 执行compaction
这边文章暂时只讲解确定compaction的范围这一步,具体的执行compaction下篇文章讲。
详细拆解每个流程
这次讲解的代码版本是8.11.0。
我们的代码之旅从 LevelCompactionBuilder::PickCompaction开始
确定从哪一层开始进行compaction
这部分可以理解为两个流程
1 先计算各个层级的trigger_size
2 根据每一层sst文件的体积之和除以本层级的trigger_size,来计算到一个分数。
然后就先从分数最高的层进行compaction。
计算trigger_size
先说各层的trigger_size 大小(代码在VersionStorageInfo::CalculateBaseBytes)
假定level_compaction_dynamic_level_bytes 为false
L0 的总大小是 max_bytes_for_level_base 默认256MB
L1 的总大小也是 max_bytes_for_level_base 默认256MB
L2 的总大小是L1* 10,也就是2560MB
L3 的总大小是L2* 10,也就是25600MB
…
确定各层分数
确定各层分数 代码在:VersionStorageInfo::ComputeCompactionScore
L0层的分数就是下面两个值中较大的那个
L0层所有sst的文件总体积除以max_bytes_for_level_base,max_bytes_for_level_base
上文已经说了,默认256MB
L0的文件数量除以level0_file_num_compaction_trigger,level0_file_num_compaction_trigger默认是4
L1层的分数就是L1层所有sst的文件总体积除以上面计算的L1上限,还是256MB
L2层的分数就是L2层所有sst的文件总体积除以上面计算的L2上限,就是2560MB
经过上面的逻辑后
std::vector<int> compaction_level_;
上面vector里面第一个int就是分数最大的层级,第二个int就是分数次大的层级
std::vector<double> compaction_score_;
上面vector里面第一个double就是各个层级最大的分数,第二个double就是各个层级次大的分数
遍历 compaction_level_ 就能按照分数大小 开始compaction了
确定从哪些文件开始进行compaction
下面是需要在源那一层挑选合适的文件。
LevelCompactionBuilder::PickFileToCompact()
默认的思路优先选择和下层key范围重叠率最小的文件去compact(所谓的重叠率就是重叠的size/sst文件的size)。可以降低写放大。
大家可以想想肯定是有个地方,先计算好了这些数据,然后外层直接查询它,来确定到底从哪个文件开始compaction。
相关的代码在:VersionStorageInfo::UpdateFilesByCompactionPri,相关的参数是CompactionPri,默认的值是kMinOverlappingRatio
经过VersionStorageInfo::UpdateFilesByCompactionPri我们已经知道了优先级最高的某一个文件,但是文件是需要扩展的。看下图:
如上5个文件,如果在f3优先级最高,则在compact时同时将{f2,f3,f4} 3个文件向下合并,因为f2、f4中key范围与f3有重叠,因此compact的key范围由[k4,k6]扩展到了[k3,k8]。如果选择的文件中有任何文件在被compact则会终止compact文件选择过程。(至于为什么k4既在f3又在f2,这牵扯到innerKey的问题)
上面扩展重叠的逻辑在CompactionPicker::ExpandInputsToCleanCut。
另外还有一个问题,假定我们是从L0进行合并,那么在SetupOtherL0FilesIfNeeded里面还会在进行一次扩充,把L0层和源文件有交叠的地方再加入源文件里。
确定合并文件的目标层
当参数level_compaction_dynamic_level_bytes 是false的时候,很简单,L0的文件合并到L1,L1的文件合并到L2,L2的合并的L3。
如果打开level_compaction_dynamic_level_bytes,则目标层会从默认的Level 1 变成最高层 Level 6,即最开始Level 0会直接compact到Level 6,如果某次compact后,Level 6大小超过256M(target_file_size_base),假设300M,则base_level向上调整,此时base_level变成Level 5,而Level 5的大小上限是300M/10 = 30M,之后Level 0会直接compact到Level 5,如果Level 5超过30M,假设50M,则需要与Level 6进行compact,compact后,Level 5恢复到30M以下,Level 6稍微变大,假设320M,则基于320M继续调整base_level,即Level 5的大小上限,调整为320M/10 = 32M,随着写入持续进行,最终Level 5会超过256M(target_file_size_base),此时base_level需要继续上调,到Level 4,取Level 5和Level 6当前大小较大者,记为MaxSize,则Level 4的大小上限为MaxSize/100,Level 5的大小上限为Level 4大小上限乘以10,依次类推。
相关代码在VersionStorageInfo::CalculateBaseBytes。
另外一点level_compaction_dynamic_level_bytes只影响L0的目标层,L1依然是合并到L2的。
确定目标层涉及的文件
代码逻辑还是在LevelCompactionBuilder::PickFileToCompact里面。
如前文,先通过ExpandInputsToCleanCut确定了源那一层经过扩展后的文件。
再通过CompactionPicker::GetRange从源层拿到所有文件的最小值和最大值
再通过VersionStorageInfo::GetOverlappingInputs获取目标层里在最大值最小值之间的文件
还有一个问题在SetupOtherInputsIfNeeded里,如果之前扩容了源层的文件,这里还需要再次扩容目标层。
源层目标层的文件都确定了之后,就生成一个compaction结构体,等待调度。
参考资料
https://www.jianshu.com/p/88f286142fb7
https://kernelmaker.github.io/Rocksdb_dynamic