RangeReduce 复用 Range Query 已经完成的 I/O、k-way merge、旧版本去重与 Tombstone 过滤,将有效 Entry 同时写回一个更深层的 Sorted Run。这批数据后续做 Compaction 时,不必再完整支付一次读取、归并和无效版本清理成本;重叠查询也可以少读一些文件和旧版本。论文在 RocksDB 8.5.0 上实现原型,报告 Compaction Debt 最高降低 90%、总 Data Movement 最高降低 12%、Space Amplification 最高降低 20%,平均 Range Query 延迟最高降低 18%。1
1 Range Query 的重复归并与 RangeReduce 的创新点
LSM-tree 的 Range Query 需要在每个可能命中 Key Range 的 Sorted Run 上定位 Iterator,读出候选数据,再在内存中执行 k-way merge,过滤被更新、删除或 Tombstone 失效的版本。更新比例越高,一次查询读到的无效数据越多;相同或重叠的 Range Query 再执行一次,这套 I/O、归并与过滤也要再做一次。1
Compaction 随后还会读取其中一部分数据,把相邻 Level 的重叠文件归并并写回。论文把当前 LSM-tree 中间层数据最终合并成一个 Sorted Run 尚需付出的读写工作称为 Compaction Debt。这样来看,同一批 Entry 先被 Range Query 读出并归并,又在后续 Compaction 中重新读出并归并,前一次计算没有改变物理布局。1
RangeReduce 要解决的就是这段重复工作。Range Query 在返回结果前,已经把多个 Run 的 Entry 读入内存,选出最新版本并丢弃旧版本与 Tombstone;系统直接复用归并结果生成 SST,相当于把未来一部分 Compaction 提前放进一次本来就要发生的读取中。收益主要体现在后续 Compaction 的读写量下降,以及相同或重叠 Range Query 少做归并。1

图 1:重复 Range Query 会反复读取并过滤无效版本,后续级联 Compaction 还会再次读写同一批数据。原图来自论文 Figure 2。1
这笔账依赖 workload。更新、删除较多时,查询能够清理更多旧版本;Range Query 较长,或者后续查询与当前范围反复重叠时,一次写回可以摊给更多数据和更多查询。一次性的短 Range Query、Key Range 高度随机、更新很少时,能够省下的旧版本读取有限,写 SST 的成本却立即进入当前查询。还有两类问题:查询在深层只覆盖很大的目标文件时,会重写大量已经位于目标 Level 的数据;查询驱动的归并持续把数据推向最深层以后,LSM-tree 增高又可能触发级联 Compaction。1
论文讨论的直接前序工作是 SuccinctKV。它也用 Range Query 触发 Compaction,但只处理已经饱和的 Level,并通过额外 Meta Block 标记文件中仍然有效的 Key Range。未饱和 Level 中的旧版本继续累积;当较浅层一个文件与下一层大量文件重叠时,查询驱动的 Compaction 又可能重写大量目标层数据。RangeReduce 保持 SST 不可变,也继续使用 RocksDB 原有的查询与 Compaction 路径,把重点放在什么条件下值得写回、写回到哪一层。1
RangeReduce 的四个设计分别卡住一个 bad case:
-
短查询产生小文件:File-Size-Aware Merge-on-Scan。 Range Query 只命中 SST 中的一小段时,直接写回会切出多个碎片文件,同时把大量未命中数据留在原 Level。每个候选 Level 至少要有半个目标 SST 大小的数据命中查询,算法还会合并区间两侧的剩余数据,控制文件数量与填充率。
-
目标 Level 被反复重写:Bounded-Merge。 较浅 Level 只有少量 Entry 命中、下一层却有大量数据命中时,合并后大部分输出原本就在目标 Level,Forward Progress 很低。Bounded-Merge 比较相邻 Level 的命中量,只合并比例达到阈值的连续 Level 区间。
-
最深层饱和后的级联 Compaction:Level-Renaming。 查询驱动的归并会更快地把数据推向底层;最深 Level 饱和、树高增加时,传统做法仍可能搬动大量文件。Level-Renaming 修改现有 Level 编号并增加空的 Level 1,让原有 SST 通过逻辑位置变化获得新的容量上限。
-
写回拖慢当前查询:SLO-Bounded Compaction。 查询复用了读取与归并,生成新 SST 的写入仍是额外成本。系统在执行前估计读写数据量与完成时间;预计超过应用 SLO 时,本次 Range Query 只返回结果,不触发写回。1
2 查询驱动 Compaction 的决策链
2.1 从 Merge-on-Scan 到文件大小约束
最直接的 Merge-on-Scan 会读取所有命中 Range Query 的 Sorted Run,在内存中归并出最新版本,然后把有效 Entry 写到最深的命中 Level。后续查询如果落在相同 Key Range,浅层文件已经被清理,可以从更少的 Run 中读取数据;浅层腾出的空间也能继续吸收 Flush,推迟级联 Compaction。1
这个策略过于积极。Range Query 只命中文件中间一小段时,不可变 SST 会被拆成查询区间之前、区间之内和区间之后几部分;查询区间被推到深层,两侧数据仍需写回原 Level。论文的微实验中,Blind Merge-on-Scan 将 Range Query 的 Read Amplification 最多降低 17%,Compaction Debt 最多降低 78%,同时把总写入量推高了最多 5 个数量级。查询省下来的读,补不了这笔写入。1
File-Size-Aware Merge-on-Scan 增加了两个限制。一个 Level 命中查询的数据量少于目标文件大小的一半时,不参与本轮合并;一组相邻 Level 需要全部通过检查,才能一起向目标层归并。它还把同一 Run 两侧未命中的数据合成一个 SST,避免一次查询在每个 Run 中留下两个小文件。文件数量和平均文件填充率恢复以后,这个版本的查询延迟比 Blind Merge-on-Scan 平均低 10%。问题仍然很明显:论文测试中,其总写入量是 RocksDB 的 167 倍,总 Data Movement 高 31%。1
2.2 Bounded-Merge 的相邻层比例
Bounded-Merge 继续约束目标层的重复写入。对于查询范围 r s t a r t , r e n d r_{start},r_{end} rstart,rend,算法先用每个 SST 的最小 Key、最大 Key 和 Entry 数估计各 Level 的命中数量 T e k T_ek Tek。完整包含的文件直接使用元数据中的 Entry 数;部分重叠的文件执行一次 Compute,读到的 Page 随后会被查询复用。1
一段连续 Level i , j i,j i,j 能够参与本轮 Compaction,需要同时满足:
r k = T e k T e k + 1 ≥ ρ , k = i , ... , j − 1 r_k=\frac{T_ek}{T_ek+1}\geq\rho,\qquad k=i,\ldots,j-1 rk=Tek+1Tek≥ρ,k=i,...,j−1
T e k ⋅ E ≥ M 2 , k = i , ... , j T_ek\cdot E\geq\frac{M}{2},\qquad k=i,\ldots,j Tek⋅E≥2M,k=i,...,j
T e k T_ek Tek 是 Level k k k 中命中本次 Range Query 的 Entry 数, E E E 是平均 Key-Value 大小, M = P ⋅ B ⋅ E M=P\cdot B\cdot E M=P⋅B⋅E 是论文模型中的 Memtable 大小,实验里也对应 4 MB 的目标 SST 大小。第一条约束要求每一对相邻 Level 的命中量比例都不低于 RangeReduceRatio ρ \rho ρ;第二条约束就是前面的半文件阈值。多个区间都满足时,算法选择最深的一段,因为更深层容量更大,通常也保留了更多被浅层版本失效的数据。1
论文在 Size Ratio T = 2 T=2 T=2 到 16 16 16 的配置上扫描 ρ \rho ρ,默认取值为:
ρ = 1 T \rho=\frac{1}{T} ρ=T1
这意味着相邻两层发生查询驱动 Compaction 时,深层命中量最多可以是浅层的 T T T 倍。更小的 ρ \rho ρ 会更积极地清理旧版本,Space Amplification 与 Compaction Debt 更低,总写入量明显增加;更大的 ρ \rho ρ 很少触发查询驱动 Compaction,行为逐渐接近普通 RocksDB。论文在均匀负载的九项指标中认为 1 / T 1/T 1/T 取得了相对稳定的折中。1
2.3 查询执行、Level-Renaming 与 SLO 边界
2.3.1 查询结果与 Compaction 输出复用
RangeReduce 在 RocksDB 主线程中先执行 Level 选择,再初始化各层 Iterator 并移动到查询起点。k-way merge 产生有效 Entry 的同时,结果通过 RocksDB Flush Thread 写回;查询结束后,已经归并到深层的输入文件被标记为待回收。读出的数据只在内存中归并一次,查询结果与 Compaction 输出共用这次工作。1

图 2:RangeReduce 先用 Level 元数据决定合并范围,再让 Range Query 的 k-way merge 结果同时服务查询返回与 SST 写回。原图来自论文 Figure 7。1
2.3.2 扩层时的级联 Compaction
RangeReduce 加快最深 Level 饱和以后,传统扩层过程可能形成下面这条链路:1
-
查询结果下沉。 RangeReduce 将一次 Range Query 命中的多层数据归并到最深的候选 Level。浅层文件被清理,最深 Level L L L 也会更快接近容量上限。
-
增加新的最深层。 Level L L L 饱和以后,LSM-tree 增加一个空的 Level L + 1 L+1 L+1。传统 LSM-engine 不会一次搬完 Level L L L,通常先选择一个或多个 SST 向下迁移,等 Level L L L 再次达到阈值后继续处理下一批。
-
后续 Flush 连续溢出。 增加 Level L + 1 L+1 L+1 没有改变上面各层的容量阈值。新的 Buffer Flush 进入 Level 1 后,可能让 Level 1 超过容量并触发到 Level 2 的 Compaction;如果 Level 2 也接近满载,这次 Compaction 会继续传播到 Level 3,最终到达新的最深层。一次 Flush 连续触发多层 Compaction,就是级联 Compaction。
-
文件迁移变成实际重写。 Level L + 1 L+1 L+1 为空时,部分 SST 可以通过修改文件归属完成 Trivial Move。随着新 Level 中出现文件,后续迁移更容易遇到重叠 Key Range,需要重新读取、归并并写出 SST,Read Amplification 与 Write Amplification 都会升高。
Level-Renaming 在 Level L L L 饱和时将现有 Level 编号整体加一:原来的 Level L L L 变成 Level L + 1 L+1 L+1,Level L − 1 L-1 L−1 变成 Level L L L,依此类推,再在顶部增加一个空的 Level 1。这个过程只改变逻辑层级,已有 SST 不需要先搬到新位置。
若本文将 Level i i i 的容量上限记为 C i C_i Ci,相邻 Level 的 Size Ratio 为 T T T,改名后的容量为:
C i + 1 = T ⋅ C i C_{i+1}=T\cdot C_i Ci+1=T⋅Ci
原本接近满载的数据只占新容量的大约 1 / T 1/T 1/T,空的 Level 1 也可以先吸收 T T T 次 Buffer Flush。各层在扩层后同时获得新的容量余量,后续 Flush 不会立即从 Level 1 一路触发到最深层。1
2.3.3 SLO-Bounded Compaction
查询驱动 Compaction 仍然把写入带进了查询路径。SLO-Bounded Compaction 使用有效读写带宽、唯一 Key 数、平均 Selectivity 和 Entry 大小估计本次读写量,估计值超过 SLO 就跳过 Compaction。这个 Gate 只能限制可以提前估算的数据量,实际设备排队、k-way merge 的 CPU 开销和 Garbage Collection 抖动并未进入模型。1
3 实验配置、对照基线与结果边界
3.1 单机 RocksDB 原型与默认负载
论文在 RocksDB 8.5.0 上集成 RangeReduce,对照原生 RocksDB 与同样基于 RocksDB 的 SuccinctKV。实验服务器使用 Intel Xeon Gold 6240R 2.40 GHz、192 GB 内存、1 TB SSD 和 Ubuntu 20.04,负载由 KVBench 与 Tectonic 生成。1
默认配置先写入 1 GB 唯一 Key,再交错执行 1 GB 更新和 9000 次 Range Query;Key 与 Value 分别为 16 B 和 112 B,平均 Selectivity 为 0.1,Memtable 与目标 SST 大小为 4 MB,Size Ratio 为 6。论文分别测试 Key Range 均匀随机的查询,以及每 100 次出现一组相同查询的重叠负载。1

图 3:Figure 8 增加随机 Range Query 数量,比较 Compaction Debt、Space Amplification、Data Movement 与吞吐;Figure 9 将 Size Ratio 从 2 调到 10,比较 Compaction 工作量、查询读写量、平均延迟与 Space Amplification。原图来自论文 Figure 8、Figure 9。1
随机 Range Query 数增加以后,RangeReduce 相比 SuccinctKV 的 Compaction Debt 最多降低 85%,相比 RocksDB 的总 Data Movement 最多降低 12%,Space Amplification 最多改善 20%。Size Ratio 从 2 增加到 10 时,RangeReduce 用于层间 Compaction 的 Data Movement 比 RocksDB 最多低 50%,比 SuccinctKV 最多低 25%;Range Query 读取开销比 RocksDB 低 10%---15%。1
3.2 更新、长短查询混合与 YCSB-E
更新密集测试先写入 420 万条唯一 Entry,随后执行五个 Epoch,每个 Epoch 包含 42 万次更新与 100 次 Selectivity 为 0.1 的 Range Query。RangeReduce 平均比 RocksDB 少读 16.9%,查询快 5.4%;相比 SuccinctKV 少读 12.4%,查询快 5.9%。Compaction Debt 分别比 RocksDB 和 SuccinctKV 低 82% 与 85%,相对 SuccinctKV 的 Space Amplification 改善 19%,总 Data Movement 低 9%。1
论文还构造了九个 Epoch 的混合负载:唯一写入、更新、一次 Selectivity 为 0.25 的长 Range Query、1000 次落在同一范围内的短查询、随机长短查询、空与非空 Point Query,再回到写入和混合查询。前面的长查询已经完成大范围归并以后,后续短查询从更少的数据中读取;RangeReduce 的短查询比 RocksDB 快 35%、比 SuccinctKV 快 24%,读取量分别低 30% 与 20%。Point Query 延迟与两个基线基本相当。1

图 4:一次长 Range Query 之后执行重叠短查询,并在后续 Epoch 混入不同 Selectivity 的 Range Query、Point Query、Insert 与 Update。原图来自论文 Figure 11。1
短而随机的 Range Query 收益小得多。YCSB-E 风格实验预写 210 万条唯一 Entry 与 210 万次更新,再交错执行 9.5 万次约返回 100 条记录的 Range Query 和 5000 次更新。RangeReduce 的早期查询先出现延迟尖峰,稳定后比 RocksDB 最多快 18%,相比 SuccinctKV 只快 1.1%;每次查询读取量分别低 6.8% 与 7.4%。1

图 5:约返回 100 条记录的短 Range Query 中,RangeReduce 初期承担写回开销,后续延迟和读取量下降;相对 SuccinctKV 的延迟差距已经很小。原图来自论文 Figure 14。1
3.3 结果不能直接外推到云上 Compaction
论文标题和机制讨论的是通用 LSM Compaction,实测范围仍然很窄:单机 RocksDB、单块本地 SSD,扩展性实验把数据库从 1 GB 增加到 8 GB。没有分布式副本、共享存储、对象存储、远端 Compaction Worker,也没有网络与多租户资源竞争。1
论文给出的 10 TB 数据集、AWS/GCP/Azure 四年成本最多降低约 45% 的结果来自成本模型投影,不是三家云上的端到端部署测试。这个数字依赖 12% Data Movement 与接近理想 Space Amplification 能否在更大数据量、远端存储计费和长期更新下继续成立。1
另一个边界是延迟口径。论文主要报告平均 Range Query 延迟,部分时序图给出滚动窗口的 P5---P95;SLO-Bounded Compaction 没有单独给出 P99、误判率或限速设备下的组件实验。把 Compaction 写回放进查询路径以后,尾延迟、并发查询间的 Flush 竞争以及失败后的清理成本都还需要验证。
最后,完整 RangeReduce 同时启用了 Bounded-Merge 与 Level-Renaming。前者决定本次查询要不要归并哪些 Level,后者改变 LSM-tree 增高时的级联 Compaction 行为。论文给了 Bounded-Merge 的组件对照,但摘要中的最高收益来自完整系统,不能全部算在查询复用这一个机制上。1
4 总结
RangeReduce 最直接的优势是复用 Range Query 已经完成的 I/O、k-way merge、旧版本去重和 Tombstone 过滤。将这批有效 Entry 写入更深层以后,后续 Compaction 可以少读、少归并一部分数据,相同或重叠范围的查询也能少访问一些 Run。长 Range Query、查询范围反复重叠,以及更新和删除较多的负载,更容易覆盖本次写回成本。1
它的代价也很明确:写 SST 的开销会立即进入当前查询。短而随机的 Range Query 很难复用这次整理结果;更新和删除很少时,可清理的旧版本有限;查询只覆盖一小段 Key Range,但目标 Level 中与之重叠的 SST 很大时,还可能为了少量有效数据重写大量已有数据。查询驱动的归并持续把数据推向最深层后,也可能使 LSM-tree 增高并触发额外的级联 Compaction。论文中的 File-Size-Aware Merge-on-Scan、Bounded-Merge、Level-Renaming 和 SLO-Bounded Compaction 分别在限制这些开销,但触发限制或直接跳过写回时,RangeReduce 的收益也会随之下降。1
所以,RangeReduce 是否值得执行,最终取决于未来省下的查询与 Compaction 工作,能否覆盖当前查询新增的写回。它更适合可复用范围较大、旧版本较多的 workload;对短、随机、低更新且很少重叠的 workload,普通 Range Query 更便宜。