文章目录
Compaction(称为合并,或者压缩),在LSM中,指的是把数据从Ln层,合并到Ln+1层,把重复的旧的数据删除。
Compaction算法
主要两个类型的LSM树合并算法:
Leveled
-
leveled 合并,这个是RocksDB的默认策略
leveled 算法的特点是以读放大和写放大为代价最小化空间放大。
LSM-tree 可以看作是包含若干 level 的序列,每个 level 是仅包括1个 sorted run。相邻 level 的大小之比通常被我们称为 fanout(扇出),当不同 level 之间的 fanout 相同时,LSM-tree 的写放大最小。compaction 选择 L(n) 的数据,与原有 L(n+1) 的数据进行合并,得到新的 L(n+1) 数据。每次 compaction 的最大写放大系数等同于 fanout。
Tiered
-
一种替代合并策略,有时候被叫做“size tiered”或者“tiered”
Tiered合并通过增加读放大和空间放大,来最小化写放大 。
LSM-tree 依然可以看作是包含若干 level 的序列,每个 level 包括 N 个 sorted run。L(n) 的 sorted run 大小是 L(n-1) 的 N 倍。compaction 通常选择 L(n) 的数据合并得到新的 sorted run 输出到 L(n+1),但并不与 L(n+1) 的已有数据进行合并。每次 compaction 的最大写放大系数是 1。
两种算法的主要差别在于,leveled合并倾向于更加频繁的把小的排序结果合并到大的里面,而“tiered”等待多个大小接近的排序结果,然后把它们合并到一起。
-
Tiered+Leveled
Tiered+Leveled会有比leveled更小的写放大,以及比teired更小的空间放大。
Tiered+Leveled实现方式是一种混合实现,在小的层使用tiered,在大的层使用leveld。具体哪一层切换tiered和leveled可以非常灵活。
RocksDB中的Leveled合并也是Tiered+Leveled。一个memtable落盘过程类似于tiered合并——memtable的输出在L0构建一个新的排序结果并且不需要读/重写L0上已经存在的排序结果。根据level0_file_num_compaction_trigger的配置,L0可以有多个sort run,所以L0是Teired的。 其它层为Leveled。
RocksDB的Compaction
Rocksdb常用的Compaction模式有两种:Leveled、Universal(tiered算法)。
Leveled Compaction
所有非0层都有 target sizes。合并的目的是限制这些层的数据大小。target sizes通常指数增加。
当L0的文件数量到达level0_file_num_compaction_trigger,合并(compaction)就会被触发,L0的文件会被合并进L1。通常我们需要把所有L0的文件都选上,因为他们通常会有交集:
合并过后,可能会使得L1的大小超过目标大小:
这个时候,我们会选择至少一个文件,然后把它跟L2有交集的部分进行合并。生成的文件会放在L2:
如果结果仍旧超出下一层的目标大小,我们重复之前的操作 —— 选一个文件然后把它合并到下一层:
然后
如果有必要,多个合并会并发进行:
最大同时进行的合并数由max_background_compactions控制。
which level to compact
当多个层触发合并条件,RocksDB需要选择哪个层先进行合并。对每个层通过下面方式生成一个分数:
- 对于非0层,分数是当前层的总大小除以目标大小。如果已经有文件被选择进行合并到下一层,这些文件的大小不会算入总大小,因为他们马上就要消失了。
- 对于level0,分数是文件的总数量,除以level0_file_num_compaction_trigger,或者总大小除以max_bytes_for_level_base,这个数字可能更大一些。(如果文件的大小小于level0_file_num_compaction_trigger,level 0 不会触发合并,不管这个分数有多大)
我们比较每一层的分数,分数高的合并优先级更高。
subcompaction
level 0 由memtable flush而来, 每个文件的key range会有重叠,不同于其他level,无法并行compaction。因此提出 subcompaction(次合并)来实现另一种并行。
通过一系列操作,将level 0到level 1的一次compaction按照合理的key range划分成为互不覆盖,互不影响的多个subcompaction,并交给多个子线程并行去做,不同的子线程compaction结果输出到不同的文件中,等所有子线程完成自己的compaction后,主compaction线程进行结果整理合并,最终完成本次compaction。
https://blog.51cto.com/u_15057819/2647639
Universal Compaction
Universal合并方式是一种合并方式,面向那些需要用读放大和空间放大,来换取更低的写放大的场景。
通常认为 tiere 算法提供更好的写放大表现,但是读放大会变糟糕。凭直觉来看:在tiered存储,每当一个更新被合并,他倾向于从小的sorted run移动到一个大很多的sorted run。每次合并都可能会指数级接近最后那个最大的sorted run。然而在leveled合并,一次更新更加可能通过一个小的排序结果,被合并进一个更大的排序结果,而不是直接作为一个较小的排序结果的一部分保存起来。
最坏的情况下,sorted run数量会比leveled多很多。这会导致读数据的时候有更高的IO和更高的CPU消耗。
尽管如此,RocksDB仍旧提供了“tiered”家族的算分,Universal合并。用户可以再leveled合并无法处理想要的写速率的时候,可以尝试这种合并方式。
使用 Universal Compaction 的 RocksDB 实例,可以看作是在时间上将数据划分到不同的 sorted run,每个 sorted run 在时间上互不交叠。compaction 仅仅针对在时间上相邻的 sorted run 进行,其输出的时间范围是原输入的时间范围的组合。
Compaction Picking Algorithm
假设我们有 sorted run
R1, R2, R3, ..., Rn
R1有DB最新的更新,而Rn有最老的DB更新。 sorted run 按照数据时间顺序排序。由于 compaction 输出与输入的时间范围相同,compaction 操作不会改变 sorted run 的排序。
前置条件
Compaction 触发前置条件:n >= options.level0_file_num_compaction_trigger,即 sorted run 的数量达到阈值options.level0_file_num_compaction_trigger。 (参数中的file并不准确,应该为sorted run)
如果前置条件满足了,还有四个条件。满足其中任意一个都可以触发一个合并:
1、由Space Amplification触发的合并
如果预计的空间放大比例(size amplification ratio)大于options.compaction_options_universal.max_size_amplification_percent / 100,所有的文件会被合并为一个sorted run。
计算公式如下:
size amplification ratio = (size(R1) + size(R2) + ... size(Rn-1)) / size(Rn)
以 options.compaction_options_universal.max_size_amplification_percent = 25,options.level0_file_num_compaction_trigger = 1 的配置参数为例:
1
1 1 => 2
1 2 => 3
1 3 => 4
1 4
1 1 4 => 6
1 6
1 1 6 => 8
1 8
1 1 8
1 1 1 8 => 11
1 11
1 1 11
1 1 1 11 => 14
1 14
1 1 14
1 1 1 14
1 1 1 1 14 => 18
2、由Individual Size Ratio触发的合并
size_ratio_trigger = (100 + options.compaction_options_universal.size_ratio) / 100
我们从R1开始,如果size(R2) / size(R1) <= size_ratio_trigger, 那么(R1,R2)被合并到一起。我们以此继续决定R3是不是可以加进来。如果size(R3) / size(R1+r2) <= size_ratio_trigger,R3应该被包含,得到(R1,R2,R3)。然后我们对R4做同样的事情。我们一直用所有已有的大小总和,跟下一个排序结果比较,直到size_ratio_trigger条件不满足。
以 options.compaction_options_universal.size_ratio = 0,options.level0_file_num_compaction_trigger = 5 参数配置为例:
1 1 1 1 1 => 5
1 5 (no compaction triggered)
1 1 5 (no compaction triggered)
1 1 1 5 (no compaction triggered)
1 1 1 1 5 => 4 5
1 4 5 (no compaction triggered)
1 1 4 5 (no compaction triggered)
1 1 1 4 5 => 3 4 5
1 3 4 5 (no compaction triggered)
1 1 3 4 5 => 2 3 4 5
3、由 number of sorted runs 触发的合并
如果sorted runs的数量达到了options.level0_file_num_compaction_trigger,但是没有触发前面提到的size amplification 或者space amplification trigger。那么RocksDB将尝试进行 sorted run 数量不超过options.compaction_options_universal.max_merge_width 的 compaction,以使得 sorted run 数量少于 options.level0_file_num_compaction_trigger。
4、由 age of data 触发的合并
对于Universal Compaction,基于时间的compaction策略是最高优先级的,如果我们尝试compaction,一定会先检查时间条件,如果有文件存在的时长大于options.periodic_compaction_seconds,RocksDB将会从旧到新来挑选sorted runs 来compaction,直到某个更新的sorted runs 正在被compaction,这些文件将会被合并到最底层。
FIFO Compaction
FIFO Style Compaction 是最简单的合并策略。很适合用于保存低开销的事件日志数据(例如查询日志)。当文件总大小超过配置值 CompactionOptionsFIFO::max_table_files_size (默认值为 1GB) 时,最早的 SST 文件将会被删除。
使用FIFO时,所有的文件都位于 L0,因此SST文件过多,导致查询性能急剧下降。
开启 CompactionOptionsFIFO.allow_compaction 参数,可以触发 L0 IntraCompaction,每次至少选取 level0_file_num_compaction_trigger 个 SST 文件进行合并,从而减少文件数量。
FIFO Compaction with TTL
FIFO Compaction with TTL 在 FIFO compaction 的基础之上,提供 SST 文件级别的过期删除功能。当 SST 的最新的 key 存在时间超过 mutable_cf_options.ttl,则该 SST 文件将会在 TTL compaction 中被删除。
参考:
https://github.com/facebook/rocksdb/wiki/Compaction
https://www.jianshu.com/p/333ec61051f0
https://zhuanlan.zhihu.com/p/165137544
https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/Compaction.md