文件结构¶
磁盘上的文件被分成多层进行组织。我们叫他们 Level-1, Level-2,等等,或者简单的 L1,L2,等等。一个特殊的层,Level-0(L0),会包含刚从内存 memtable 落盘的数据。每个层(除了 Level0)都是一个独立的排序结果
在每一层(除了 Level-0),数据被切分成多个 SST 文件。
每一层都是一个排序结果,因为每个 SST 文件中的 key 都是排好序的(参考 基于块的表格式)。如果需要定位一个 key,我们先二分查找所有文件的起始和结束 key,定位哪个文件有这个 key,然后二分查找具体的文件,来定位 key 的位置。总的来说,就是在该层的所有 key 里面进行二分查找。
所有非 0 层都有目标大小。压缩的目标是限制这些层的数据大小。大小目标通常是指数增加。
压缩 (compaction)¶
当 L0 的文件数量到达 level0_file_num_compaction_trigger,压缩 (compaction) 就会被触发,L0 的文件会被合并进 L1。通常我们需要把所有 L0 的文件都选上,因为他们通常会有交集:
压缩过后,可能会使得 L1 的大小超过目标大小:
这个时候,我们会选择至少一个文件,然后把它跟 L2 有交集的部分进行合并。生成的文件会放在 L2:
如果结果仍旧超出下一层的目标大小,我们重复之前的操作 —— 选一个文件然后把它合并到下一层:
然后
然后
如果有必要,多个压缩会并发进行:
最大同时进行的压缩数由 max_background_compactions 控制。
然而,L0 到 L1 的压缩不能并行。在某些情况,他可能变成压缩速度的瓶颈。在这种情况下,用户可以设置 max_subcompactions 为大于 1。在这种情况下,我们尝试进行分片然后使用多线程来执行。
压缩文件选择¶
当多个层触发压缩条件,RocksDB 需要选择哪个层先进行压缩。对每个层通过下面方式生成一个分数:
- 对于非 0 层,分数是当前层的总大小除以目标大小。如果已经有文件被选择进行压缩到下一层,这些文件的大小不会算入总大小,因为他们马上就要消失了
- 对于 level0,分数是文件的总数量,除以 level0_file_num_compaction_trigger,或者总大小除以 max_bytes_for_level_base,这个数字可能更大一些。(如果文件的大小小于 level0_file_num_compaction_trigger,level 0 不会触发压缩,不管这个分数有多大)
我们比较每一层的分数,分数高的压缩优先级更高。
具体那个一个文件会被压缩会在 选择层压缩文件 中解释。
层的目标大小¶
1 level_compaction_dynamic_level_bytes 为 false¶
如果 level_compaction_dynamic_level_bytes 为 false,那么层目标这样决定: L1 的目标是 max_bytes_for_level_base,然后 Target_Size(Ln+1) = Target_Size(Ln) * max_bytes_for_level_multiplier * max_bytes_for_level_multiplier_additional[n]
。max_bytes_for_level_multiplier_additional 默认全为 1。
举个例子,如果 max_bytes_for_level_base = 16384,max_bytes_for_level_multiplier = 10 ,max_bytes_for_level_multiplier_additional 没有被设置,那么 L1,L2,L3 与 L4 层的大小分别是 16384, 163840, 1638400, 以及 16384000。
2 level_compaction_dynamic_level_bytes 为 true¶
最后一层的目标大小 (层数 -1) 总是层的实际大小。剩下的 Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier
。如果哪一层的大小小于 max_bytes_for_level_base / max_bytes_for_level_multiplier
,我们不会填充他。这些层会保持为空,所有 L0 的压缩会跳过这些层,直接到第一个符合大小的层。
举个例子,如果 max_bytes_for_level_base 为 1GB,num_levels=6,最后一层的实际大小为 276GB,那么 L1-L6 层的目标大小经分别为 0,0,0.276GB,2.76GB,27.6GB 以及 276GB。
这是为了保证一个稳定的 LSM 树结构,如果 level_compaction_dynamic_level_bytes 为 false,就无法保证。举个例子,在前面的例子中:
我们可以保证 90% 的数据存储在最后一层,9% 的数据在倒数第二层。这样会有许多好处。
TTL¶
如果没有人更新一个文件中 key,那么这个文件将不会进入压缩流程,而这个文件就会存活很久。例如,在某些特定场景,key 会被“软删除” —— 把数值设置为空而不是直接使用 Delete 删除。这个“已经删除”的 key 范围可能不会有任何新的写入了,这样,这个数据就会在 LSM 里面待很长时间,浪费空间。
一个动态 ttl 列族选项被用于解决这个问题。文件(或者说,数据)老于 TTL 的数据在没有其他后台任务的时候会被定时压缩。这会让数据进入常规的压缩流程,摆脱无用的老数据。对于所有不是最底层的,比 ttl 要新的数据,以及所有在最底层老于 ttl 的数据,这还有一个(好的)副作用。注意这会导致更多的写因为 RocksDB 会调度更多的压缩。