全面解析:五种文件选择策略的原理与实战调优)
数据库KV存储嵌入式数据库存储【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址https://gitcode.com/gh_mirrors/ro/rocksdb点击查看免费下载RocksDB 的 Level-based Compaction 在层级文件数量超限时需要决定“优先压缩哪一个文件”这个选择策略由options.compaction_priCompactionPri枚举控制直接关系到写放大Write Amplification、空间放大Space Amplification与热数据局部性。本文以官方博客《Option of Compaction Priority》为核心骨架结合当前仓库 advanced_options.h 中完整枚举定义与 version_set.cc 的排序实现系统讲解各优先级模式的设计动机、适用场景、源码级实现差异与调优建议帮助读者在写密集、热 key 覆盖、大量删除等典型负载下做出正确的选型。一、背景Level-based Compaction 中的“选文件”问题Level-based Compaction 是 RocksDB 最常用的压缩风格它是 LevelDB 压缩算法的改进版。其基本思想是数据按多个层级组织每一层的目标大小呈指数增长除特殊的 Level 0 之外每一层都按 key 区间切分成多个 SST 文件。当某一层的大小超过目标值时就从中挑选一个或多个文件将其与下一层的重叠文件合并merge到下一层。在这里“挑选哪个文件去压缩”是一个关键问题LevelDB 只有一个压缩线程总是以**轮询round-robin**方式挑选文件RocksDB 实现了多线程压缩通过从同一层挑选多个文件并行压缩来提升吞吐因此必须摆脱 LevelDB 的简单挑选方式转而提供可配置的策略。为此RocksDB 引入options.compaction_pri选项用不同的算法决定从同一层中优先挑选哪些文件进行压缩。之所以需要多种算法供选择是因为挑选文件时需要权衡多个相互冲突的因素目前还没有一种算法能够自动平衡所有场景因此把这个选择权暴露给用户。二、CompactionPri枚举五种模式一览在当前的仓库代码中CompactionPri枚举定义于 include/rocksdb/advanced_options.h共包含五种模式原博客发表于 2016 年时只有前三种后两种为后续演进新增枚举值值官方注释要点核心语义kByCompensatedSize0x0按删除数补偿后的大小排序略微优先选择较大的文件默认值kOldestLargestSeqFirst0x1优先压缩最新更新最旧的文件适合只在小区间更新热 key 的场景kOldestSmallestSeqFirst0x2优先压缩最久未压缩到下一层的区间key 空间随机更新时写放大更优kMinOverlappingRatio0x3优先压缩与下一层重叠比例最小的文件多数场景可优化写放大被标记marked for compaction的文件优先kRoundRobin0x4维护一个游标记录上次压缩文件的后继 key 区间按轮询方式遍历该层所有文件该选项通过 options/cf_options.cc 注册为ImmutableCFOptions的标准可配置项类型为OptionType::kCompactionPri因此既可以在代码中直接赋值也可以通过 OPTIONS 文件、SetOptions等方式配置。下面按照原博客的脉络深入剖析每一种策略背后的权衡。三、写放大视角kOldestSmallestSeqFirst3.1 为什么文件密度影响写放大在估计写放大时通常假设 key 在每一层内均匀分布但现实往往并非如此——即使更新在整个 key 区间上均匀发生。例如当把一个文件从 L 层压缩到 L1 层时会在 L 层留下一个空洞hole此后新进来的压缩会逐步把数据填回空洞但在一段时间内该区域的密度仍然偏低。挑选密度最低的文件压缩到下一层的代价更高因为该文件在下一层会有更多重叠文件需要重写更多数据。原博客给出了一组直观数字假设一个文件大小为 100MB若它在 L2 层与 8 个 L3 文件重叠需要重写约 800MB 数据才能把它压到 L3若它与 12 个 L3 文件重叠则需要重写约 1200MB——同样是 100MB 的文件多花了 50% 的写量。该分析忽略了下一层的 key 密度因为下一层该区间覆盖 N 个文件单个空洞只影响写放大的 1/N。如果所有更新均匀分布LevelDB 的做法其实能优化写放大因为它挑选的文件所覆盖的区间上一次被压缩到下一层的时间最久该区间从后续压缩中积累 key 的时间最长密度最高。3.2 kOldestSmallestSeqFirst 的机制RocksDB 为此设计了kOldestSmallestSeqFirst总是挑选覆盖最旧更新的文件这样的区间通常也是该层中密度最高的 key 区间。从源码看这一策略的实现非常直接。在 version_set.cc 的UpdateFilesByCompactionPri中该模式对每层文件按smallest_seqno升序排序case kOldestSmallestSeqFirst: std::sort(temp.begin(), temp.end(), [](const Fsize f1, const Fsize f2) - bool { return f1.file-fd.smallest_seqno f2.file-fd.smallest_seqno; }); break;文件的最小序号smallest_seqno越小说明该文件覆盖的数据写入越早、越陈旧该区间被后续压缩填充的时间越长密度越高。排序结果写入files_by_compaction_pri_[level]压缩挑选器即按此顺序取文件。3.3 适用场景如果你的写入在整个 key 空间上均匀分布并且希望降低写放大官方建议设置为options.compaction_pri rocksdb::kOldestSmallestSeqFirst;四、优化小工作集热 keykOldestLargestSeqFirst4.1 热 key 场景下的问题前面的分析假设更新均匀分布在全部 key 空间上但许多实际用例中只有一小部分 key 被频繁更新其他 key 区间非常冷。此时让热 key 区间尽量停留在浅层不被压到更深层对写放大和空间放大都有好处。原博客的例子假设数据库中只有 key 150–160 被更新其他 key 很少更新L1 层共有 20 个 key。此时我们希望 key 150–160 全部留在 L1 层因为当下一次 L0→L1 压缩到来时只是覆盖已有 keyL1 大小不增长因此不会触发 L1→L2 的进一步压缩反之如果先把 key 150–155 压到 L2那么下一次 L1→L2 压缩会使 L1 大小增长、超过目标值从而引发更多压缩产生更多写入。4.2 kOldestLargestSeqFirst 的机制kOldestLargestSeqFirst专门优化上述场景挑选最新更新最旧的文件——即该区间最长时间没有新数据进入通常就是最冷的区间。先把最冷区间压走把热区间留在当前层。源码实现同样清晰version_set.cc 按largest_seqno升序排序case kOldestLargestSeqFirst: std::sort(temp.begin(), temp.end(), [](const Fsize f1, const Fsize f2) - bool { return f1.file-fd.largest_seqno f2.file-fd.largest_seqno; }); break;largest_seqno越小代表该文件接收的最新写入越早越可能是冷数据。4.3 适用场景如果你的使用模式是在小区间内反复覆盖已有 key典型的热 key 更新负载官方建议尝试options.compaction_pri rocksdb::kOldestLargestSeqFirst;五、尽早丢弃删除标记kByCompensatedSize默认值5.1 删除标记过多带来的问题如果一个文件包含大量删除标记delete markers / tombstones会拖慢对该区域的迭代性能——因为即使是要忽略这些已删除 key也必须遍历它们。此外越早把删除的 key 压缩到最后一层磁盘空间就越早被回收因此对空间效率也有利。5.2 默认策略 kByCompensatedSize 的机制RocksDB 的默认压缩优先级kByCompensatedSize正是考虑了删除标记场景当文件中的删除数超过插入数时该文件被选中压缩的可能性更大删除数超出插入数越多被压缩的可能性越大。该优化的目的是避免数据库中有很大比例数据被删除时出现最差的空间效率和查询性能。源码中的排序依据是compensated_file_size删除补偿后的大小见 version_set.cc// Comparator that is used to sort files based on their size // In normal mode: descending size bool CompareCompensatedSizeDescending(const Fsize first, const Fsize second) { return (first.file-compensated_file_size second.file-compensated_file_size); }文件的实际大小会因其中删除标记的数量而被补偿放大删除越多、补偿后大小越大排序越靠前从而越先被压缩。因为需要按补偿后大小做部分排序该模式使用std::partial_sort只对前VersionStorageInfo::kNumberFilesToSort个文件排序见 version_set.cc以减少开销。5.3 何时保持默认如果你的数据库存在大量删除操作或者不确定负载特征保留默认的kByCompensatedSize是稳妥选择——它兼顾了空间回收与删除区间的查询性能。六、压缩过滤器Compaction Filter的效率问题通常用户使用压缩过滤器CompactionFilter来清理旧数据、释放空间。挑选文件进行压缩的方式会影响空间回收效率。截至原博客发布时RocksDB 还没有专门为压缩过滤器场景设计的压缩优先级。官方在内部场景中的解决思路是另辟蹊径由一个外部服务检查所有 SST 文件的修改时间mtime一旦发现某个文件过于陈旧就通过DB::CompactFiles()强制压缩这一个文件。这样做可以为数据经过压缩过滤器提供时间上界——保证任何数据在陈旧到一定程度后一定会被过滤器处理并回收空间。如果你也有类似需求可以参考这一做法使用 db/compaction/compaction_picker_level.cc 中同样支持的FilesMarkedForCompaction机制被标记的文件在 version_set.cc 的排序比较器中被优先或者直接调用CompactFiles()主动触发单文件压缩。七、源码视角排序如何驱动文件挑选理解了五种模式的语义后再看一眼整体机制会让理解更完整。UpdateFilesByCompactionPriversion_set.cc是核心入口它有以下关键行为仅对 Level-based 生效当compaction_style_为kCompactionStyleNone、kCompactionStyleFIFO或kCompactionStyleUniversal时直接返回不需要该排序最高层不参与排序for (int level 0; level num_levels() - 1; level)因为最后一层永远不会再被压缩每层生成一个优先级索引排序结果存入files_by_compaction_pri_[level]供 compaction_picker_level.cc 的LevelCompactionBuilder::PickFileToCompact按序取用Level 0 的特殊处理Level 0 的文件之间允许相互重叠未完全排序因此即使配置为kRoundRobinLevel 0 也回退到按smallest_seqno排序即kOldestSmallestSeqFirst行为见 version_set.cc。这解释了为什么默认的kByCompensatedSize是补偿后大小降序而不是简单的文件大小降序——删除标记是最需要优先处理的负资产。八、调优建议与选型总结综合原博客的官方建议与当前仓库的枚举注释可以给出如下选型决策参考全新场景、无从下手时官方建议从kOldestSmallestSeqFirst开始尝试——注意它不是默认值默认是kByCompensatedSize保持默认是出于向后兼容的原因写放大敏感 更新均匀分布选择kOldestSmallestSeqFirst热 key 小区间覆盖更新选择kOldestLargestSeqFirst大量删除、空间回收优先保持默认kByCompensatedSize通用调优进阶当前仓库注释建议优先尝试kMinOverlappingRatio——它直接以与下一层重叠比例最小为准则多数情况下能优化写放大追求公平性与可预测性kRoundRobin以游标方式轮询遍历整个层适合不希望出现饿死某些文件、希望压缩均匀铺开的场景。如果你有更好的文件挑选思路也欢迎实现并基准测试benchmark后提交 PR这正是 RocksDB 将策略开放为枚举选项的初衷——社区持续在探索更优的平衡点。九、进一步阅读枚举完整定义与官方注释include/rocksdb/advanced_options.h排序实现UpdateFilesByCompactionPri与各比较器db/version_set.cc文件挑选主流程LevelCompactionBuilderdb/compaction/compaction_picker_level.cc选项注册与序列化options/cf_options.cc压缩优先级相关的测试与验证db/compaction/compaction_picker_test.cc、db/db_compaction_test.cc压缩过滤器的完整 APIinclude/rocksdb/options.h赞分享数据库KV存储嵌入式数据库存储【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址https://gitcode.com/gh_mirrors/ro/rocksdb点击查看免费下载相关推荐OkGo请求优先级Priority类与任务调度策略OkGo请求优先级Priority类与任务调度策略 你是否曾遇到过APP中多个网络请求同时发起时重要数据加载缓慢而非关键广告却抢占资源的情况OkGo框架后端移动开发解决API调用拥堵Feign请求优先级调度的Fair与Priority策略全解析解决API调用拥堵Feign请求优先级调度的Fair与Priority策略全解析 你是否遇到过API调用高峰期请求拥堵、关键业务被非核心请求阻塞的问题Fei后端API设计RocksDB 预设压缩字典Preset Dictionary Compression原理、架构与调优实践RocksDB 预设压缩字典Preset Dictionary Compression原理、架构与调优实践 导读 本文围绕 RocksDB 的压缩字典预设数据库KV存储嵌入式数据库存储创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考