RocksDB的Compaction : Leveled Compaction 和 Universal Compaction


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”等待多个大小接近的排序结果,然后把它们合并到一起。

img

  • 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通常指数增加。

level_targets

当L0的文件数量到达level0_file_num_compaction_trigger,合并(compaction)就会被触发,L0的文件会被合并进L1。通常我们需要把所有L0的文件都选上,因为他们通常会有交集:
在这里插入图片描述

合并过后,可能会使得L1的大小超过目标大小:

在这里插入图片描述

这个时候,我们会选择至少一个文件,然后把它跟L2有交集的部分进行合并。生成的文件会放在L2:
在这里插入图片描述

如果结果仍旧超出下一层的目标大小,我们重复之前的操作 —— 选一个文件然后把它合并到下一层:

https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l1_compaction.png

然后

在这里插入图片描述

如果有必要,多个合并会并发进行:

https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/multi_thread_compaction.png
最大同时进行的合并数由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

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据Valgrind提供的信息,可以得出以下分析: 这段Valgrind信息表示在程序运行结束时,有24字节的内存块是明确丢失的。这是在294条记录中的第68条记录。 这个内存块的分配是通过`operator new`函数进行的,具体是在`vg_replace_malloc.c`文件的`operator new(unsigned long, std::nothrow_t const&)`函数中进行的。这个函数用于分配内存,并且使用了`std::nothrow_t`参数,表示在分配失败时不抛出异常。 这个内存块的丢失发生在`libstdc++.so.6.0.19`库文件中的`__cxa_thread_atexit`函数中。这个函数是C++标准库中的一个线程退出钩子函数,用于在线程退出时执行清理操作。 进一步跟踪,这个内存块的丢失是在`librocksdb.so.6.20.3`库文件中的`rocksdb::InstrumentedMutex::Lock()`函数中发生的。这个函数是RocksDB数据库引擎的一个锁操作函数,用于获取互斥锁。 在调用堆栈中,可以看到这个内存块丢失是在RocksDB数据库引擎的后台合并线程(Background Compaction)中发生的。具体是在`rocksdb::DBImpl::BackgroundCallCompaction()`和`rocksdb::DBImpl::BGWorkCompaction()`函数中进行的合并操作。 最后,从调用堆栈中可以看到,这个内存块的丢失是在后台线程中发生的。这是在`librocksdb.so.6.20.3`库文件中的`rocksdb::ThreadPoolImpl::Impl::BGThread()`和`rocksdb::ThreadPoolImpl::Impl::BGThreadWrapper()`函数中执行的。 综上所述,根据Valgrind的信息分析,这段代码中存在一个明确的内存泄漏问题,24字节的内存块在后台合并线程中丢失。需要进一步检查代码,确保在合适的时机释放这些内存块,以避免资源泄漏和潜在的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值