问津集 #17:Terark-DS——KV 分离后的 WAL 写入、冗余策略与 GC

Terark-DS 处理 KV-separated LSM-tree 部署到存算分离架构后出现的写性能与空间效率问题。KV Separation 让 Compaction 只处理 Key SST,降低了写放大;WAL 与用户写仍要经过远端网络,三副本会放大 Value SST 的存储量与写入流量,GC 也会因为远端读取和逐 Key Lookup 变慢,延后无效 Value 的回收。

论文基于 TerarkDB 与字节跳动的分离存储实现 Terark-DS,把 WAL、Key SST 和 Value SST 交给不同冗余策略,再分别改造 WAL 写入与 GC 数据路径。实验报告,相比同一远端存储上的其他 KV-separated LSM-tree,Terark-DS 的写吞吐提高 20.4%---63.9%;按论文给定的 SSD 单价与 CPU 消耗模型计算,总成本降低 22.7%---58.6%。1

这些数字依赖 Value 大小、批量写规模、25 Gbps 客户端网络和底层存储接口。本文分别讨论 Differentiated Redundancy 如何减少副本流量与容量,Adaptive WAL 如何降低远端写延迟,Network-efficient GC 如何减少无效 Value 传输与 RPC,并核对它们在不同 workload 下的收益边界。

1 存算分离下 KV Separation 的问题与创新点

传统 LSM-tree 把 Key 与 Value 一起放入 SST,Compaction 会反复搬动完整 KV。KV Separation 只在 Key SST 中保留小 KV 与大 Value 的索引,把大 Value 放入独立 Value SST,Compaction 的数据量随之下降。TerarkDB 使用 512 B 作为默认分离阈值。1

具体来看,Key SST 中有两类 Entry:小 KV 直接保存为 <Key, Value>;大 KV 只保存 <Key, FileNo>,其中 FileNo 指向包含该 Value 的 Value SST。Key SST 本身按 Key 有序组织,并带有文件内索引块 IDX。Value SST 同样按 Key 有序,保存完整的大 <Key, Value>,也有自己的 IDX。读取大 KV 时,系统先在 Key SST 中找到 <Key, FileNo>,再进入对应 Value SST,使用 Key 和该文件的内部索引定位 Value。这里的 <Key, FileNo> 是跨文件引用,IDX 用于各自 SST 内部查找,两者不是同一层索引。1

KV Separation 以后,大 Value 不再跟随 Key SST 的普通 Compaction 重写。大 KV 更新时会产生新的 Value 记录,新的 Key SST 记录会指向它所在的 Value SST;删除时,Key SST 中的删除标记会让原来的 Value 失效,但旧数据仍然占着 Value SST 的空间。随着更新和删除持续发生,无效 Value 会不断累积,因此需要 GC 识别仍然有效的 KV,将它们搬到新的 Value SST,再回收旧文件。1

GC 扫描候选 Value SST 时,拿到的是其中曾经写入的 <Key, Value>,单靠这条记录无法判断它是不是该 Key 的当前版本。GC-Lookup 需要使用 Key 查询由 Key SST 构成的 Index LSM-tree,取得当前可见版本。该版本仍然指向候选 Value 时才保留,否则说明它已经被更新或删除淘汰。1

GC 将有效 KV 搬到新的 Value SST 后,Key SST 中原来的 FileNo 又可能随之失效。TerarkDB 使用文件编号继承树记录新旧 Value SST 的关系,让旧引用可以重定向到新文件,避免为一次 GC 立即重写相关 Key SST。1

KV Separation 部署到远端存储以后,Compaction 的写放大下降了,WAL 延迟、副本流量与客户端 NIC 带宽成为新的限制。论文在字节跳动的分离存储上部署 6 个存储节点,使用三副本与 25 Gbps 网络。RocksDB 的远端部署比本地版本损失 34.9%---45.5% 写吞吐;三副本带来的写流量最高占用客户端 NIC 带宽的 87.8%,同步 WAL 又把写延迟增加 44.6%。KV Separation 将远端 TerarkDB 的写吞吐提高到远端 RocksDB 的 3.8 倍,距离本地 TerarkDB 仍然有 44.9% 的差距。1

远端 GC 也会继续推高空间放大。100 GB 初始数据持续以 200 MB/s 更新 1800 秒以后,本地 TerarkDB 的 Space Amplification 为 1.51,远端版本上升到 1.96,RocksDB 则约为 1.12。GC 的 Read 与 Lookup 阶段中,网络分别占总延迟的 28.4% 与 22.2%;回收周期变长,三副本继续放大尚未清理的 Value SST。1

图 1:论文对本地/分离存储、传统 LSM-tree/KV Separation 的设计空间定位。右侧雷达图是论文作者的定性概括,并非 Figure 11 一类受控实验。原图来自论文 Figure 1。1

Terark-DS 对上述问题分别采用三项方案:

  1. Differentiated Redundancy。 WAL、Key SST 和 Value SST 使用 Quorum、三副本与 RS(4:2) Erasure Coding,按访问粒度与容量占比分配保护方式。

  2. Adaptive WAL Writing。 小 Batch 串行写入,大 Batch 拆成 Parent WAL 与多个 Sub-WAL 并行落到远端,减少 WAL 在写入关键路径上的等待。

  3. Network-efficient GC。 GC 先读取 Key 并批量判断有效性,只按需读取仍然有效的 Value,再通过本地化 Lookup 与自适应预读降低 RPC 次数。

图 2:WAL、Key SST、Value SST 与 GC 在 Client Node 和分离存储之间的路径。原图来自论文 Figure 5。1

2 差异化冗余、Adaptive WAL 与 Network-efficient GC

2.1 文件访问模式与差异化冗余

Terark-DS 先把 KV-separated LSM-tree 中的文件分成三类。WAL 位于写入关键路径,生命周期短,只有恢复时才会被大块顺序读取;Key SST 容量小,却参与每次 Point Query、Compaction 和 GC-Lookup;Value SST 保存大 Value,容量占比最高,读取频率更低,I/O 粒度也更大。在论文的 Mixed-8K 负载中,Key SST 的访问频率比 Value SST 高 46.8%。1

WAL 使用三副本 Quorum。一次写入同步落到两个副本即可提交,第三个副本异步补齐;恢复或一致性读取查询两个副本。Key SST 继续使用三副本,避免小 I/O 触发 EC 的跨节点读取与解码。Value SST 使用 RS(4:2),4 个数据块加 2 个校验块,存储与写入流量从三副本的 3 倍降到 1.5 倍。1

论文使用 QPS/Storage Cost 比较三种冗余策略,数值越高,表示相同存储成本下能够提供的读吞吐越高。模型假设请求串行执行,QPS 与平均读延迟成反比;SST 写入通常是大块顺序 I/O,不作为策略选择的主要差异。结果可以拆成 4 点:1

  1. 全三副本。 Key SST 和 Value SST 都保存 3 份,读延迟最低,存储成本为原始数据量的 3 倍。Value SST 占据主要容量时,这部分副本成本很高。

  2. 全 EC。 Key SST 和 Value SST 都使用 RS(4:2),存储成本降到原始数据量的 1.5 倍。问题是每次 Point Query 都要访问 Key SST,小 I/O 会承担跨节点读取与 EC 解码延迟。

  3. 混合策略。 Key SST 使用三副本,Value SST 使用 EC,存储成本为 3 D k + 1.5 D v 3D_k+1.5D_v 3Dk+1.5Dv。Key SST 保留低延迟读取,大 Value 承担 EC 的空间收益。与全三副本相比,这个策略在论文模型中始终有更高的 Cost-effectiveness;Key SST 越小,优势越接近 2 倍。

  4. 策略边界。 α = D k / D v \alpha=D_k/D_v α=Dk/Dv 表示 Key SST 与 Value SST 的容量比。论文把三副本下的小 I/O 读延迟作为基准 1。同样是小 I/O,EC 的读延迟为 1.3,记为 K s K_s Ks;EC 下的大 I/O 读延迟为 1.5,记为 K l K_l Kl。将这两个数据代入模型,当 α < 0.1 \alpha<0.1 α<0.1 时,混合策略优于全 EC; α \alpha α 增大到 RocksDB 一类 Key SST 占比较高的形态后,全 EC 节省的 Key SST 空间会占上风。

因此,Differentiated Redundancy 的关键前提是 Key SST 足够小,Value SST 才是容量主体。它不是一个脱离 KV 分布的固定最优策略。

2.2 Batch 大小驱动的 WAL 串并行切换

远端 WAL 不能继续依赖本地 Page Cache 隐藏同步写延迟。Terark-DS 先扩大 Group Commit,论文示例为 512 KB,再把较大的 Log Batch 切成 4 个 Segment:一个写入 Parent WAL,其余写入 3 个 Sub-WAL。Parent WAL 保存 Group IDSegment IDOffsetLength 的映射,保证恢复时能够按原始顺序重组 Batch。1

并行写也有调度成本。论文测得每个 Segment 16 KB 是一个实用阈值,因此小于 64 KB 的 Group 串行写入 Parent WAL;更大的 Group 才拆成 4 份并行执行。对允许异步刷 WAL 的 workload,Terark-DS 还提供默认 1 MB 的 Log Buffer,缓冲区溢出或显式 Flush 时才持久化。这个路径改变了持久化时机,不能与每次写都要求同步持久化的 Group Commit 混在同一个耐久性口径下比较。1

图 3:小 Group 串行写入,大 Group 分段进入 Parent WAL 与 Sub-WAL。原图来自论文 Figure 8。1

恢复时,系统先从 Manifest 找到 Parent WAL,读取其中的 Segment Mapping,再由多个 Worker 并行取回 Sub-WAL 的区间,按 Segment ID 重组记录。每个 Mapping Entry 使用 2 B 保存 Group/Segment ID,Offset 与 Length 各 8 B;论文将这部分开销判断为相对 WAL 大小可以忽略,但没有给出完整恢复时延与故障注入结果。1

2.3 Key-first GC 与 RPC 合并

Index LSM-tree 指由各层 Key SST 组成的 Key 侧 LSM-tree。它保存小 KV 和大 KV 的 <Key, FileNo>,查询一个 Key 时会返回当前可见版本。它不是单个 SST 内部用于定位 Entry 的 IDX 索引块。

原始 TerarkDB 的 GC 可以展开成 4 个动作:1

  1. 选择候选文件。 GC 选出需要回收的 Value SST。文件中混有仍然有效的 KV,以及已经被更新或删除的旧 KV。

  2. GC-Read。 GC 从远端存储批量读取候选 Value SST 中完整的 <Key, Value>,传到 Client 或 GC Worker。

  3. GC-Lookup。 对每个候选 Key 查询远端 Index LSM-tree,取得该 Key 的当前版本。当前版本与候选记录匹配时标记为 Valid,已经被更新或删除的版本标记为 Invalid。

  4. GC-Write。 GC 将 Valid KV 批量写入新的 Value SST,记录新旧文件的映射关系,随后回收旧文件。

远端部署主要慢在第 2、3 步。GC-Read 在判断有效性之前已经传输了完整 Value,无效 Value 到达 Client 后才被丢弃,直接浪费网络带宽;GC-Lookup 又要为每个 Key 单独访问远端 Index LSM-tree,大量串行 RPC 把网络往返延迟累积起来。1

Terark-DS 沿用同样的 4 步,但改了第 2---4 步的数据路径:1

  1. 选择候选文件。 这一步没有变化,GC 仍然先选出需要回收的 Value SST。

  2. GC-Read 只读取 Key。 Terark-DS 在 Value SST 内部进一步分离 Key 与 Value。GC 先批量读取候选 Key,不再把对应的大 Value 一起传到 Client。这个改动称为 On-Demand Value Fetching。

  3. GC-Lookup 批量判断有效性。 GC 一次处理一批候选 Key,输出由 0 和 1 组成的有效性 Bitmap,其中 1 表示 Valid,0 表示 Invalid。GC 在计算节点执行时,Flat Index Cache 作为 Key SST 索引数据的只读内存缓存,让大部分查询可以在本地批量完成。GC 在独立 Worker 上执行时不依赖这份缓存,Terark-DS 为这条路径维护了一棵辅助 LSM-tree,也就是 Invalid Tree。它不保存 Value,也不保存所有 Key 的当前映射,只保存后台 Compaction 已经确认失效的那部分 Key。Worker 把整批候选 Key 通过一次 RPC 交给 Invalid Tree,由它返回其中的失效 Key,再生成有效性 Bitmap,不再逐 Key 查询完整的远端 Index LSM-tree。这一步对应 Batched and Localized GC-Lookup。

  4. 按 Bitmap 读取 Value,再执行 GC-Write。 Bitmap 标出仍然有效的 Value,但这些 Value 在文件中可能并不连续,逐段读取仍会产生很多 RPC。Adaptive Readahead 会合并相距小于 16 KB、合并后有效数据占比不低于 80% 的区间,单个窗口最多扩到 2 MB。例如 V1V3V4V5 有效而 V2 无效时,系统可以一次读取 V1---V5,多读一个 V2,少发一次远端请求。最终只有有效 KV 会写入新的 Value SST。

对应到原始 GC 的两个慢点:第 2 步通过只读 Key 避免传输无效 Value,第 3 步通过本地缓存或一次批量 RPC 减少远端 Lookup,第 4 步再合并零散的 Value 读取请求。

图 4:GC 从读取完整 KV、逐 Key Lookup,变成先读 Key、本地化批量 Lookup,再按有效区间预读 Value。原图来自论文 Figure 9。1

这套 GC 同时增加了 Value SST 内部布局、Flat Index Cache 与 Invalid Tree。论文报告 Flat Index Cache 与原有 Block Cache 共享内存,节点失败只需要重新 Warm Up;Invalid Tree 由后台 Compaction 更新。缓存占用、Warm Up 期间的尾延迟与 Invalid Tree 恢复过程没有进入后面的端到端成本表。1

3 实验配置、组件收益与证据边界

3.1 ByteStore Testbed 与三类 KV 分布

实验使用字节跳动的分离存储。6 个存储节点各配置双路 Intel Xeon Platinum 8336C、3 块 Intel SSD 与一张 25 Gbps Mellanox ConnectX-5 IB RNIC,3 个节点同时承担 MetaServer,全部节点承担 ChunkServer;负载生成器运行在同配置的独立节点。系统为 Debian 10、Linux 5.4。1

论文设计了三类负载:

  1. Mixed-8K。 小 Value 在 100---512 B 均匀分布,大 Value 固定 16 KB,两者比例 1:1,用来模拟增量更新与数据页混合的云数据库负载。

  2. Fixed-16K。 所有 Value 固定 16 KB,代表 Feature Store 与状态存储中的大 Value。

  3. Pareto-1K。 Value 服从广义 Pareto 分布,小、中 Value 占主导,用来接近文件系统元数据负载。

每轮默认先写入 100 GB,再执行 300 GB Update、100 GB Read 与 4000 万次 Scan,Scan Length 在 2---1000 之间均匀分布。各系统统一使用 512 B 分离阈值、最多 4 个 128 MB Memtable、256 MB Value SST、128 MB Key SST、1 GB Block Cache、32 个前台线程与 16 个后台线程。对照组是适配同一分离存储的 RocksDB、BlobDB 和未优化 TerarkDB;默认三副本,带 -EC 的版本使用 RS(4:2)。1

3.2 写吞吐收益与小 Value 边界

图 5:Insert、Update、Point Read 与 Scan 的吞吐,纵轴相对 RocksDB 归一化。每个 Panel 中标出的绝对数值是 RocksDB 基线。原图来自论文 Figure 11。1

Insert 测试中,Terark-DS 达到 RocksDB 的 2.72---6.70 倍,相比 BlobDB 与 TerarkDB 提高 20.4%---63.9%;Update 达到 RocksDB 的 2.10---6.02 倍,相比另外两个 KV Separation 基线提高 21.3%---62.6%。Fixed-16K 的收益最大,Pareto-1K 明显缩小,说明大 Value 比例同时决定 KV Separation、EC 降流量和 WAL Batch 并行能贡献多少。1

Point Read 在 Mixed-8K 与 Pareto-1K 上分别提高 32.1% 与 25.7%,论文将其归因于 Flat Index Cache 减少 Index Lookup RPC。Scan 的结果就比较丑陋了:Mixed-8K 与 Fixed-16K 下,几种 KV-separated LSM-tree 大致相当;Pareto-1K 下,Value Locality 被打散,Terark-DS 的 Scan 吞吐低于不做分离的 RocksDB,只与其他 KV Separation 方案接近。1

YCSB 延续了这个边界。Fixed-16K 的 YCSB-A 中,Terark-DS 比未优化 TerarkDB 高 45.2%;Pareto-1K 下,大多数负载仍比其他 KV Separation 基线高 14.4%---44.1%,Scan 密集的 YCSB-E 则落后于 RocksDB。1

3.3 Differentiated Redundancy、WAL 与 GC 的组件结果

Differentiated Redundancy 相比全三副本将 Insert/Update 吞吐提高 22.7%---32.0%。全 EC 在 Mixed-8K 与 Pareto-1K 的 Point Read 分别下降 12.9% 与 15.2%,混合策略把高频小 I/O 留在三副本 Key SST,读吞吐接近全三副本。Pareto-1K 的 Scan 仍然会访问 EC Value SST,这个退化没有被冗余策略消除。1

在同步 Group Commit 路径上,Adaptive WAL 对 Mixed-8K 与 Fixed-16K 的写吞吐提高 6.1%---19.6%,WAL 延迟下降 5.1%---17.4%;Pareto-1K 的平均 Batch 只有 16.9 KB,系统基本回退到串行模式,吞吐变化很小。Fixed-16K 与 Mixed-8K 的平均 Batch 分别为 263 KB 和 135 KB,能够覆盖拆分与线程调度成本。1

GC 空间实验重新载入 100 GB 数据,对 Mixed-8K、Fixed-16K 按 200 MB/s、Pareto-1K 按 100 MB/s 连续更新 1800 秒。三副本条件下,Terark-DS 比 RocksDB 少用 30.4%---40.1% 空间,比 BlobDB 与 TerarkDB 少 51.9%---77.3%;只比较打开 Differentiated Redundancy 与 Adaptive WAL、尚未优化 GC 的版本,Network-efficient GC 自身再减少 12.0%---28.1%。1

图 6:持续限速更新 1800 秒后的实际空间占用。左图采用三副本,右图的对照基线采用 RS(4:2);Terark-DS 使用差异化冗余。原图来自论文 Figure 16。1

与 RocksDB-EC 相比,Terark-DS 仍然占用更多空间,因为 KV Separation 的 Value 回收存在延迟;对应收益是 Mixed-8K 下 Insert 与 Read 吞吐分别达到 RocksDB-EC 的 3.43 倍与 1.43 倍。论文的成本表假设数据保留一个月,按字节跳动分离存储实际使用的本地 SSD 单价计算 Storage Cost,再用 300 GB Put/Get 期间的 CPU 使用量计算执行成本。Terark-DS 的合计成本为 0.4106 美元,其他方案为 0.5313---0.9912 美元,于是得到 22.7%---58.6% 的降幅。1

这是一套固定数据量、保留周期和资源单价下的相对模型,不是包含网络设备、故障修复、缓存内存和工程维护成本的完整云账单。端到端测试也没有纳入 Figure 1 中的 CaaS-LSM、MirrorKV 或 Disaggregated RocksDB,20.4%---63.9% 的标题数字对应 BlobDB、TerarkDB 等同 Testbed 基线。1

论文已经公开代码与测试脚本。2 仓库说明,完整优化目前主要依赖字节跳动 ByteStore;本地文件系统只能生效 GC 相关优化,Differentiated Redundancy 与 Adaptive WAL 需要分离存储提供对应接口。其他存储后端需要实现 bytestore_fs/mock.cc 中的初始化与读写接口。因此,公开仓库可以检查 Terark-DS 的 RocksDB/TerarkDB 改动,缺少 ByteStore 集群时无法独立复现论文的完整结果。2

4 总结

Terark-DS 根据 WAL、Key SST 和 Value SST 的访问模式分别选择远端存储策略。WAL 需要控制提交延迟并支持恢复,Key SST 需要低延迟小 I/O,Value SST 更关注容量与大块吞吐,GC 则要减少无效 Value 传输和 RPC 次数。统一三副本会增加 Value SST 的容量与流量,全 EC 又会提高 Key SST 的小 I/O 延迟,因此论文分别使用 Quorum、三副本和 EC,并单独改造 WAL 与 GC 路径。

这套设计适合大 Value 占主要容量、Point Query 频繁、客户端 NIC 与远端 RPC 已经进入瓶颈,并且底层存储能够按文件类型提供 Quorum、Replication 与 EC 的系统。小 Value 主导时,EC 的空间收益变小,Adaptive WAL 很难形成足够大的 Batch,KV Separation 还会伤害 Scan Locality。论文的 Pareto-1K 已经把这部分代价暴露出来。

这些方案也增加了实现与移植成本。Terark-DS 需要存储层理解文件类型并提供多种冗余接口,WAL Recovery 需要识别 Parent/Sub-WAL,GC 又引入 Flat Index Cache、Invalid Tree 和新的 Value SST 布局。公开实现对 ByteStore 的依赖说明,这三项优化并不是把参数调好就能移植到任意对象存储。2

GC 部分的收益很大一部分来自原始远端路径本来就比较粗糙:完整 Value 先传到 Client,确认失效后再丢弃;每个 Key 又单独查询远端 Index LSM-tree。Terark-DS 先读 Key、批量判断,再按需读取有效 Value,确实减少了无效数据传输和逐 Key RPC,但也把原来一次完整 KV 读取拆成了 Key 读取、有效性判断与 Value 读取。Batched Lookup 和 Adaptive Readahead 用来抵消新增的网络跳数。论文的组件实验主要比较持续更新 1800 秒后的实际存储量,Network-efficient GC 相比未优化 GC 的版本减少了 12.0%---28.1% 的空间占用,却没有直接给出两者的端到端 GC 完成时延。因此,我暂时无法从论文数据确认单轮 GC 到底快了多少,甚至是否一定更快;还需要在相同垃圾比例下比较 GC 完成时间、网络请求数以及前台读写的 P99。1

GC 在这里首先解决的是回收变慢导致的空间堆积,空间降下来以后,论文的整体成本模型才会更好。到了云上,我的第一反应是把 GC 调度得更频繁。提高频率不会让单次 GC 执行得更快,但可以缩短旧 Value 的积压时间,也有机会把一次较大的回收拆成更多次较小的回收。这个思路可以和论文方案叠加:先降低每轮 GC 传输的数据量与 RPC 数量,再利用省下来的网络和 I/O 预算提高执行频率。当然,GC 虽然不在用户读写的同步关键路径上,仍会争用网络、存储 I/O、CPU 和缓存,能否不影响前台请求需要单独测试。

论文没有测试节点失效时的 Quorum 写、并行 WAL 恢复时延、EC Repair、缓存冷启动、网络分区与多租户带宽争用。总成本的 22.7%---58.6% 也只覆盖论文模型中的 Storage、Put 与 Get。存算分离 LSM-tree 的存储接口还需要表达 WAL、Key SST 和 Value SST 的访问特征,冗余、网络写入与 GC 才能分别决策。实际系统更容易复用的是这种按数据角色选择策略的方法;论文中的最高倍数仍然取决于 Value 分布、网络与底层存储实现。

这篇论文也挺有意思。至少从论文公开材料看,我没有看到一个非做不可的业务刚需,它更像团队围绕现有产品中的基础问题形成研究课题,做出结果后再反哺存储引擎。能够投入人力处理这种不那么直接由业务推动、却可以长期改善基础能力的问题,本身就说明团队还有做系统研究的冗余人力空间。不错的一篇工程论文。

5 参考资料

1 Terark-DS: A High-Performance and Storage-Efficient Key-Value Separation Storage Engine on Disaggregated Storage

2 SZ-NPE/terark-ds

相关推荐
StarRocks_labs2 天前
StarRocks 存算分离架构下的大规模实时导入优化实践
starrocks·flink·sstable·compaction·tablet·主键索引·存算分离架构
李兆龙的博客2 天前
问津集 #15:RangeReduce——查询驱动 Compaction 收益
compaction
arvin_xiaoting5 个月前
OpenClaw学习总结_I_核心架构_6:Compaction详解
学习·系统架构·学习总结·ai agent·compaction·openclaw
私房菜7 个月前
Linux内存管理(81):compact_zone 详解
linux·compaction·kcompactd·proactive·compact_zone
StarRocks_labs9 个月前
从小文件困局到“花小钱办大事”:StarRocks 存算分离批量导入优化实践
数据库·starrocks·compaction·memtable·本地磁盘 spill
zincooo3 年前
HBase之Compaction
大数据·hbase·compaction