rocksdb写放大_RocksDB零基础学习(三) Level Style Compaction

本文详细介绍了RocksDB的写放大、读放大和空间放大概念,重点讨论了Level Style Compaction,包括其原理、算法以及如何降低写放大的策略。文章还涵盖了compaction的两种风格——Leveled和Tiered,以及RocksDB中使用的Level Style Compaction的优化,如minor和major compaction,以及如何选择参与compaction的文件。
摘要由CSDN通过智能技术生成

Amplification Factors

在开始介绍compaction之前,我想先介绍下什么是“写放大”,“读放大”和“空间放大”,为什么要说这些呢?了解了这几个概念后,后面在介绍compaction的时候,会更能理解每个算法的设计。

  • Write amplification is the multiple of bytes written by the database to bytes changed by the user. Since some LSM trees rewrite unchanging data over time, write amplification can be high in LSM trees.
  • Read amplification is how many bytes the database has to physically read to return values to the user, compared to the bytes returned. Since LSM trees may have to look in several places to find data, or to determine what the data’s most recent value is, read amplification can be high.
  • Space amplification is how many bytes of data are stored on disk, relative to how many logical bytes the database contains. Since LSM trees don’t update in place, values that are updated often can cause space amplification.

Compactions

  1. 用于去除相同key的多条记录。当我们对一个key进行多次update/delete时,RocksDB会产生多条记录。Compactions发生于数据转移的过程中,例如memtable转为Level-0 sstfile时,Level-0 file 转为Level-1file时。
  2. Level-0 的kv 会在memtable flush的时候,排序存储,Compactions的时候,也会对kv 进行排序后,生成新的下一个level的文件。
  3. SortedRun

compaction有个概念叫sorted run。什么叫sorted run呢?暂时可以理解为一组有序的data,在RocksDB里,可以理解为一组排好序的SST file,后文会提到,对于RocksDB,sorted runs 以意味着什么。

compaction algorithm

我们提到的compaction算法有2种,leveled和tired

  1. Leveled

可以简单的认为,数据是分层存储的,每次compaction 会从Level n中取几个文件,与level n+1中的文件进行合并。

15faa8c99b8255c4f8420f337ee0c121.png
图1 Compaction between two levels in LevelDB(source:Towards Accurate and Fast Evaluation of Multi-Stage Log-Structured Designs)
  • LSM tree 是一个层次序列,每一层都是一个“sorted run”,也就是说,每一层的数据都是有序的,可以按排序范围,将数据分为多个文件。Ln+1 level 会比Ln 大很多,我们把相邻level的大小比率称为“fanout”,当一个LSM tree的每个fanout都相同时,写放大可以达到最小化。在最差的情况在,每一层的写放大等于该层的fanout。原始LSM的 compaction 指的是all-to-all,而levelDB和rocksDB则采用的是some-to-some,Ln的部分数据与Ln+1的部分来合并。
  • 以读放大和写放大来换取空间减小。通常Leveled的写放大比tired的要严重,但某些场景下,它还是有自己的优势。
  1. 在顺序写(key-order inserts)的场景下,rocksDB 的各种优化已经尽可能减小写放大
  2. skewed writes,即只有一小部分key是可能被更新的(此处,我也不太理解)。

2. Tired

对于tired算法而言,我们的处理对象是sorted runs。如下图所示,每一个黑框就是一个sorted run

2438b35cb0244edddd7104048b0d4a47.png
图2 Tired &Leveled source:https://stratos.seas.harvard.edu/files/stratos/files/monkeykeyvaluestore.pdf

Tiered compaction将每一个level 看为多个sorted run。Ln的每一个sorted run都比Ln-1的大N倍,这个N类似与Leveled compaction的fanout 。将Ln的sorted runs合并为Ln+1的一个sorted run。当进行Ln->Ln+1 compaction时,Compaction不会去写或者读Ln+1的文件。这种方式的写放大为1,远小于fanout。

Tiered compaction 通过牺牲空间放大和读放大来减少写放大。

Leveled+Tired

  1. Tiered+Leveled相比上述两种算法,具有较小的写放大,和较小的空间放大。
  2. tiered for the smaller levels and leveled for the larger levels
  3. 在rocksDB Level Style 是用的这种算法。比如memtable 可以通过配置max_write_buffer_number来设置多个sorted runs,但只有一个是可写的,其他都是只读。memtable flush 类似与tiered compaction,immutable memtable flush至L0后,L0并不会对这个sorted run 进行改动,可以认为L0是tiered

Compaction Styles

下面我们会介绍RocksDB常用的compaction style,Leveled 和Universal。首先,我们先了解minor Compaction和major compaction。

  • minor Compaction,就是把memtable中的数据导出到SSTable文件中;
  • major compaction就是合并不同层级的SSTable文件

FIFO Style Compaction 是第三种compaction style。会直接删除旧数据,可用于缓存。另外,RocksDB支持自定义Compaction方式。

Level Style Compaction(leveled compaction)

199f09e71378e86ac52a4620d13be2a5.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

磁盘的文件组合成多个Level。Level 0保存了最新的数据,越高level的block 保存了越旧的数据。在L0,不同的文件可能会出现相同的key(因此,每次Get()需要遍历L0的每个文件),但是L1和更高level的文件中,不同的文件不会出现相同的key。除L0外,每个Level的数据按key的顺序分为一个个SST file。每个SST file内部的key都是有序的。

c7cbc6fd9ead346acbdd028bee531a4f.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

所有非0的Level都有target sizes,Compaction的目的就是把每层的数据控制在target size下。每一层的target size通常是指数递增的。

每一次compaction,会从Ln中取一部分文件与Ln+1的重叠文件进行合并。运行在不同Level或者不同key 范围的compaction可以并发执行。

L0->L1的compaction是最棘手的。L0可能覆盖key的整个范围。当L0->L1运行时,L1的所有file都参与进来,这样L1->L2的compaction就不能执行。如果L0->L1太慢,可能整个系统在长时间内都只存在这一个compaction操作。并且L0->L1是单线程任务

  1. Minor compaction:
    1. 按照immutable memtable中记录由小到大遍历,并依次写入一个level 0 的新建SSTable文件中,写完后建立文件的index 数据
  2. Major compaction:

过程:

1542a497746d509c9e92e6bc08681980.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)
    1. 当Level 0的文件数到达了level0_file_num_compaction_trigger 就会触发compaction。通常,我们会选择L0全量的file参与compaction,因为它们大都是相互重叠的。
        1. 当完成L0→L1的compaction后,可能会导致L1到达它的target size

218b1b09db5c928dc5ba05842d7532ae.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

2. 在这种情况下,我们将从L1中选择至少一个文件,并将其与L2的重叠范围合并。结果文件将放置在L2中:在这种情况下,我们将从L1中选择至少一个文件,并将其与L2的重叠范围合并。结果文件将放置在L2中:

fb1384c0c5ede83c0bba20c2f307737f.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

如果L1→L2的Compaction 导致L2到达它的target size,则我们继续之前的操作:选取一个file,与下一个level key range重叠的files compaction。

c9b43046f4c8ddf056b7d1fc80758d3c.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

3. then

172a3d3cd46d277c0f1a32a6c9b1b841.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

多个compaction可以同时进行,最大并行数由max_background_compactions确定:

a2a837feb49e80c9694878920c884b85.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

有个特殊的场景是L0→L1是单线程的,不能并行compaction,这样在很多场景下,L0→L1的compaction操作会成为整个服务的性能瓶颈。为解决这个问题,RocksDB引入max_subcompactions,可以如下图所示,将一个file 分块compaction

281fde2bd6b62e911cee0b4da9a93729.png
(source:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction)

which level to compact

上面提到了compaction的条件是某一个level的文件达到了target size,就会触发compaction。那么如果有多个level 都达到了compaction,我们要选择哪一个level 先compaction呢,答案是计算 level score

b125a9ab3b4e31d256103b20eef166cb.png
leveled style compaction(source:https://www.slideshare.net/meeeejin/rocksdb-compaction/3)
  • 对非0的level,首先我们会给每一个level 算Level Score: level date size (排除正在进行compaction的部分)除以target size。
  • 对于Level 0,Level Score等于所有file 数处以level0_file_num_compaction_trigger,或者data size 除以max_bytes_for_level_base,这个值通常更大。如果file number 小于level0_file_num_compaction_trigger,L0→L1的compaction绝不会发生。

然后比较每个level的score,选择分数最高的一个来优先Compaction

which file to compaction

  1. 通过level sore 选择发生compaction的level n
  2. 确定Level n+1
  3. 在Level n通过优先级配置项(后面详述)选择参与compaction的第一个文件 SST file - fk,加入inputs
  4. 将fk 周围的其他SST file也加入inputs,除非这个file 与 inputs 中的文件有“清晰”的边界,如何理解“清晰”的边界呢?
    1. 比如我们有 以下5个文件
  f1[a1 a2] f2[a3 a4] f3[a4 a6] f4[a6 a7] f5[a8 a9]

如果我们选择了f3 进行compaction,那么我们也将同样把f2和f4一起加入inputs.因为f2~f5是连续的。为什么会这样做呢,原因在于,SST file里,存储的“key” 是InternalKey,而InternalKey是由UserKey+seqNo+key type构成。类似如下(例子摘自):

InternalKey(user_key="Key1", seqno=10, Type=Put)    | Value = "KEY1_VAL2"
InternalKey(user_key="Key1", seqno=9,  Type=Put)    | Value = "KEY1_VAL1"
InternalKey(user_key="Key2", seqno=16, Type=Put)    | Value = "KEY2_VAL2"
InternalKey(user_key="Key2", seqno=15, Type=Delete) | Value = "KEY2_VAL1"
InternalKey(user_key="Key3", seqno=7,  Type=Delete) | Value = "KEY3_VAL1"
InternalKey(user_key="Key4", seqno=5,  Type=Put)    | Value = "KEY4_VAL1"

所以,可能同一个user key 会以不同的InternalKey形式存储在不同的文件中。如果发生compaction,我们需要把这些user_key相同的InternalKey都加入Inputs

5.检查Ln+1中,与Inputs 重叠的SST file,是否有正在compaction的,如果有正在compaction的SST file,就尝试手动compaction,如果没有找到可以手动compaction的,就直接停止此次compaction。

6. 在Ln+1 的重叠文件中,执行第4步,将符合条件的SST file加入output_level_inputs。

7.有个可选的优化项,我们看看能不能在不改变output_level_inputs 的SST file数的前提下,去增加Ln的Input数,举个例子:

 Lb: f1[B E] f2[F G] f3[H I] f4[J M]
 Lo: f5[A C] f6[D K] f7[L O]

如果f2是被选中的幸运file,根据f2,我们查找到Ln+1 中,加入output_level_inputs 的是f6,然后我们反过来再到Ln查找。发现f3 ,f4 是和f6 key range重叠的,并且不会引入新的file 到output_level_inputs。这个时候,我们可以把input(f2,f3,f4)output_level_inputs(f6)compaction,否则,我们跳过这一步。

8.至此位置,pick file 的过程结束了

现在,我们来看看第3步里提到的,"根据优先级配置项选择的第一个sst file",中的优先级配置项指的什么。

  • kOldestSmallestSeqFirst 选择最晚更新(oldest update)的文件

这个选项是为了减少“写放大”。我们这样想,如果Ln的一个file和Ln+1的几个file发生了compaction,那么Ln原来的file就“消失”了,也就是出现了一个数据“hole”,或者理解为一段密度比较低的key range,又或者可以理解为,出现了key range的“断层”。

即使Ln-1和Ln 发生compaction后,这个“hole”会填充新的file,但依然相对其他的key range ,密度会更低,也就是Ln新生成的sst file,相较于同层的其他的sst file,key range 会更大一些。也就是,Ln+1 层中,与它重叠的SST file 的个数可能更多,compaction的文件个数就会更多,写放大更明显。这样讲解有点“空”。我们看下图

339727f8e829bf712e7fb63860d2d8e5.png

每个黑框都是一个SST file,黑框里的数字代表key range,假设我们的数据,在每个key range是均匀分布的,每个SST file的data size 是近似相等的。

d872a00ac20abb284ddeb3ec43f4a21b.png

Level b的第二个文件,与Level b+1的两个文件compaction后,Level b 出现了一个“hole”。

abfbf5f3027c774f61b5ca984131d0ec.png

如果Level b-1发生compaction, Level b 原先空洞的地方出现了新的SST file。那么这个新file 与Level b+1 重叠的文件,大概率比同层其他的SST file 重叠的文件要大。

所以。如果我们想在某一层选择参与compaction的SST file。选择更旧的文件。它的key range 可能会更小。引起的output_level_inputs文件可能会更少,也就间接减小了“写放大”。

因此,如果我们的写入场景中,key的更新范围较为均匀,可以选择配置options.compaction_pri = kOldestSmallestSeqFirst 来减少写放大。

  • kOldestLargestSeqFirst 选择最晚更新的文件

上一个分析的假设,是基于更新的key的分布是均匀的。然而,很多时候,我们的分布是不均匀的,也就是说会出现“热key”。这种情况下,我们应该尽量保证这些经常被更新的key,在上层,为什么呢?

思考一下,如果level 1 包含的key range[150,160]是经常被更新的key,且Level 1 有20个key。那么当Level 0->Level 1 compaction的时候。两个level 中有大量重叠的key 会合并,compaction 结束后,Level 1的data size 不会变化很多,这样不会引发Level 1 -> Level 2的compaction。这样,就减少了“写放大”。

在这个配置下,我们会选择这一层的某一个文件,它的data的更新时间是最旧的。这样,我们就能把新数据留在这一层了。

  • kByCompensatedSize选择删除标记最多的文件(Default)

如果一个文件的删除文件数大于insert次数,那么它就有可能被选中compaction,这个比例越大,被选中的可能就越大。

这样做的目的是,我们进行查询时,依然需要遍历这些被删除的key,而如果这种删除操作很多的情况下,会浪费我们大量的查询效率。其次,如果删除标记很多,我们的空间效率也很低,空间被浪费去存储大量失效的key。

  • 外部压缩

除了以上三种,你也可以选择自己扫描DB ,如果发现某个SST file很旧了,可以call DB::CompactFiles() 来压缩单个文件。

  • kMinOverlappingRatio

在翻查源码的时候,看到从4.8后新增了一个新的选项,成为了新的推荐配置选项。这个配置会计算每个file 与下一层 Level 文件的 覆盖程度。然后挑选一个ratio 最小的 。

里面的原理是:

  1. 找出Level n+1 与当前文件存在key range 重叠的files, 把文件的file_size都加起来算一个总值overlapping_bytes
  2. 然后计算ratio = overlapping_bytes * 1024 /当前文件的compensated_file_size ,存到一个map中,然后按ratio 排序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值