资讯动态

FPGA卸载LSM引擎compaction:软硬件协同设计详解

发布时间:2026/10/17 23:38:56 来源:尧图企业网站定制
读这篇 FPGA-Accelerated Compactions for LSM-based Key-Value Store 的过程中我一直在想一个问题为什么这类系统论文读完之后给人的印象往往比论文里那一串性能数字更持久我的答案是它把一个长期被后台任务掩盖的“成本中心”摆到了台面上并且给出了一条工程上可执行的路——用 FPGA 卸载 LSM-based Key-Value Store 里最吃 CPU 的 compaction 过程。这篇博文就按我自己的理解把论文的背景、核心设计、关键细节、性能收益和适用边界完整拆一遍中间会夹带一些我在存储系统上做 profiling 和软硬件协同设计时踩过的坑希望能给同行一些参考。1. 为什么这篇论文值得读compaction 是 LSM 引擎的隐形耗电大户1.1 从一次普通写入说起LSM 树的日常工作先拉齐一下基础概念不然后面讲卸载方案容易飘。一个典型的 LSM-based Key-Value Store写入路径大概是这样的用户写入先落到内存里的 memtable一个内存中的可排序结构比如跳表或者有序哈希同时为了故障恢复还会写一份 WAL 日志threshold 到达之后memtable 被冻结并转储成不可变的有序文件落到第 L0 层L0 之下的每一层都有容量上限后台线程定期的把上层选出的文件与下一层 key 范围有重叠的文件做一次多路归并生成新的下层文件后删除旧文件。整个过程就是 compaction。一句话概括 compaction它本质上就是文件级别的 k 路归并排序外加数据筛选和旧版本回收。用个生活化的类比memtable 是前台收银台临时攒的小票盒子盒子满了就得搬到仓库仓库里的货都是按编号排好序的箱子但不同批次搬进来的箱子之间并不保证全局有序于是定期需要在仓库里“并箱”——把几个箱子按编号合并成更大的箱子让仓库更规整。这个“并箱”动作就是 compaction。为什么要反复做这件事因为 LSM 树的核心规律是写的快、查的慢。为了让读路径尽量少扫描文件它必须保证每一层的 key 范围相对有序、文件数量可控同时通过层级合并把旧版本、被删除的记录真正从磁盘上清走。也就是说compaction 不是可有可无的优化而是维持查询效率、回收存储空间、控制放大效应的刚需。任何 LSM 引擎只要持续写入compaction 就停不下来。1.2 compaction 那张 CPU 账单到底有多厚很多人第一次看 LSM 引擎的 CPU profiling 时都会意外前台读写请求消耗的 CPU 通常不大真正烧核的往往是那些后台 compaction 线程。我见过线上服务器一个写密集实例里4 到 8 个核长期被 compaction 占满主业务请求还在挤剩下的核延迟波动一下子就上来了。这里有几个关键词值得一提放大类型含义典型代价写放大一次逻辑写入最终触发多少实际磁盘写入LSM 引擎通常在 10~30 之间读放大一次查询需要读多少个文件/块与层数、文件重叠度相关空间放大旧版本和被删数据占用的额外空间依赖 compaction 的回收效率写放大 20 意味着写入 1GB 逻辑数据SSD 上实际落盘可能超过 20GB这 20GB 里大部分都是 compaction 反复重写出来的中间结果。每一次重写都要执行这样的动作从源文件里读数据块在内存里做多路归并逐 key 比较、决定保留还是丢弃、按目标 key 范围重新组织然后写入新文件。这几步里逐 key 的比较和内存搬移是纯 CPU 开销数据块越大、参与归并的文件越多CPU 账单就越厚。还有一个容易被忽略的点多路归并的访问模式对 CPU 缓存特别不友好。每个输入文件都维护一个游标需要从多个块里各取一小段数据参与比较这种“多个流交替消费”的模式会让缓存换入换出很频繁有效带宽远低于理论内存带宽。结果就是即使核数够多单核的归并吞吐也很难拉上去多核并行又会引发内存带宽竞争和锁开销。所以用通用 CPU 去硬扛 compaction本质是在用一套“万能但每步都有额外损耗”的工具去执行一份“完全可以定制成流水线”的固定工作。1.3 FPGA 为什么在这种场景里能插一脚FPGA 不是万能硬件但它正好擅长这一件事按数据通路的形状定制执行逻辑。CPU 是通用流水线每条指令都要经过取指、译码、执行、访存这些固定步骤FPGA 则可以把比较器、缓冲、归并逻辑直接用硬件描述语言部署在可编程逻辑里数据从 DMA 进来之后流式经过归并单元不需要每字节都过一次通用 CPU 流水线。如果用一句话总结论文的动机就是compaction 的归并过程非常规整k 路输入、逐 key 比较、顺序输出这是一个可以被流水线化的流式任务非常适合从 CPU 卸载到 FPGA。相比之下memtable 的操作太轻量卸载收益小flush 只是一次简单排序硬件加速空间有限前台读路径延迟敏感不适合引入 DMA 和中断等不确定性。所以论文选择 compaction 作为加速点非常聚焦也足够有分量。2. 论文核心思路软硬件分工让 FPGA 接管多路归并2.1 总体架构一张卡、一条 PCIe 链路、一个任务队列论文描述的系统从顶层看是典型的软硬件协同架构。主机侧运行的是常规的 LSM 引擎可以把它想象成一个 LevelDB/rocksdb 类的开源引擎保持原有的读写接口、WAL、memtable 和文件格式。硬件侧是一块 FPGA 加速卡通过 PCIe 与主机相连卡上至少有三个核心部分DMA 引擎负责与主机内存交换数据归并核心负责执行真正的 k 路归并片上缓冲BRAM 或者板载存储用来平滑数据流的速率波动。关键是两者之间的接口。论文里最核心的抽象是在任务描述符task descriptor层面定义了一个“compaction 卸载协议”。软件侧把一次 compaction 要做的所有事情打包成一个描述结构丢给设备设备执行完把结果写回主机内存并通知软件。对软件来说这类似于把一次“函数调用”变成了“远程过程调用”只不过远端不是另一台机器而是同一台服务器里的一块 FPGA 卡。我从论文里读出的第一原则是“硬件不替做决策只替做苦力”。策略层面比如哪些文件参与合并、产生的目标文件的 key 范围、快照序号是多少、旧版本能回收多少——这些仍然由软件侧决定。FPGA 只负责把决策变成实际的数据搬运、比较、归并和写出。这种分工降低了系统的耦合度也是这套方案能被工程接受的重要原因。2.2 软件侧设计尽量不惊动原有引擎任何想落地的卸载方案最忌让软件侧伤筋动骨。论文的做法是在软件侧引入一个“compaction offload 层”把原本由后台线程执行的归并逻辑替换为“打包任务描述符 提交任务队列 等待完成通知 回收旧文件”这几步。描述符大致长这样我根据自己的工程经验还原的一个示意结构不代表论文原样struct compaction_desc { uint64_t src_segment_id[8]; // 参与合并的源文件列表 uint32_t src_segment_cnt; // 源文件数量 uint64_t dst_segment_id; // 目标文件标识 uint64_t snapshot_seq; // 快照序号决定哪些旧版本可被回收 uint64_t key_range_low; // 本任务负责的 key 范围下界 uint64_t key_range_high; // 本任务负责的 key 范围上界 uint32_t flags; // 压缩、校验、同步标志 };有了这个描述符原有引擎的 compaction 调度器不再需要关心归并的细节它只关心上层选好了哪些文件、目标 key 范围是什么、做完之后怎么切换元数据。而 FPBA 侧做完归并之后软件侧的收尾动作跟原来的软件 compaction 完全一致把旧文件从层级结构中移除把新文件登记到对应层然后释放空间。这保证了卸载方案对外行为的一致性也让测试和验证变得相对简单。值得强调的是软件侧不能把所有任务一股脑塞给设备就算完。论文里虽然没有详细展开但在工程上必须配套一个“任务队列水位管理”设备侧缓冲有容量上限软件要能够感知队列拥堵在设备满时停止下发新任务避免数据堆积导致任务超时或者 DMA 失败。这个水位管理是整个系统的隐形骨架也是我在后面第 3 章会重点展开的部分。2.3 硬件侧归并单元一个为字节流而生的比较阵列FPGA 内部的归并单元是论文里最吸引我的部分。它的核心是一个 k 路归并器从 k 个输入流中各取当前记录比较器阵列同时对比这些记录的 key每个周期选出其中最小的那条记录输出。因为有多路输入同时参与比较FPGA 的归并吞吐理论上可以做到每周期输出一条记录这比 CPU 上一条条循环比较要快得多。有一个细节需要处理LSM 的 key 是变长的。FPGA 寄存器天生适合做定宽比较面对变长 key 就比较麻烦。常见的做法是在进入比较器阵列之前先经过一个 key 解析模块把变长 key 的长度信息提取出来然后按“长度对齐 内容逐段比较”的方式设计状态机如果再做细一点还可以把 key 的哈希结果放在高比特位做一个快速过滤减少逐字节比较的次数。归并单元的输出侧还要考虑压缩。很多 LSM 引擎默认会对数据块做压缩存储如果 FPGA 直接处理压缩后的数据比较之前必须先解压输出之后还得重新压缩。论文层面我理解第一版大概率只针对未压缩或者简单编码方式做了加速压缩算法下沉到 FPGA 属于后续演进方向因为这个工作量非常大。能想清楚“第一刀切到哪里”很重要这点我会在最后一章的工程判断里再讲。3. 论文关键细节调度、流水线与一致性设计3.1 任务调度多并发场景下的优先级与背压真实系统里compaction 不是孤零零的。L0 层达到阈值时可能有两个文件合并进 L1L1 的某个 key 区间又同时需要和 L2 合并还有新的 memtable flush 正在路上。与此同时用户读写请求还在共享磁盘带宽。这意味着 FPGA 必须能同时服务多个归并任务任务之间还要有优先级不能只按 FCFS 傻跑。从系统设计的角度设备侧会有一个小型的任务调度器决定当前哪几个归并任务占用归并阵列。调度策略通常要考虑几点L0 往 L1 的 compaction 往往是写路径卡顿的罪魁祸首优先级最高同一层不同任务之间key 范围重叠度高的优先合并这样能更快降低读放大与此同时用户 IO 请求的带宽需要被显式保留不能让后台任务把磁盘带宽全部抢走。优先级调度一旦引入背压backpressure就不可避免。硬件缓冲满了怎么办软件还在不断下发任务怎么办我的经验是一定要在任务队列上做水位线软件端维护一个“在途任务上限”达到上限就暂停提交等设备侧的完成队列反馈后再继续。宁可让软件侧暂时等一等也不能让硬件侧的缓冲溢出。这个道理跟网络交换机里的流控完全一样本质都是“上游减速换取系统稳定”。3.2 三阶段流水线读、合并、写要互相掩护compaction 全链路大致是三段Source Read - Merge - Sink Write。如果只把注意力放在 FPGA 的归并速度上很容易踩坑——因为最终吞吐往往不是由最快的归并单元决定而是由最慢的那一段决定。我见过不少软硬件协同方案第一版都犯同一个毛病Merge 在 FPGA 上跑得飞快但软件侧的 DMA 缓冲区只给了一个Source Read 在读下一批数据时Merge 必须停下来等整个流水线退化成串行执行。真正的做法是要给每个阶段配上双缓冲或者多缓冲让 Source Read 在填缓冲 A 的时候Merge 消费缓冲 B 的内容同时 Sink Write 把上一轮输出写走流水线阶段硬件/软件承担者缓冲要求Source Read软件侧发起读设备侧 DMA 接收至少双缓冲避免读等合并MergeFPGA 归并单元输入输出缓冲独立容量足够容纳多个数据块Sink WriteFPGA/软件发起写DMA 写回目标位置输出缓冲可排空不能阻塞归并用接力搬砖来类比更直白如果仓库门口只留一块砖的位置第一个人放完砖之前第二个人只能站着等只要多留几块砖的位置两个人就可以一直在动。论文能跑出好看的性能数字我认为很大一部分功劳不是归并单元本身而是它把三段工序合理流水线化了IO 时间被 Merge 时间成功隐藏掉。这个思路同样适用于软件侧线程池设计。即使在纯软件环境里把 compaction 拆成“预读线程 归并线程 写回线程”用队列连接阶段也能比单线程边读边合并快不少。先学会这一层再去看 FPGA 方案会觉得豁然开朗。3.3 变长 key、压缩和失败恢复这几个硬骨头变长 key 比较前面已经提到了补充一点如果比较器设计得不好地址对齐和长度判断本身就会成为新的吞吐瓶颈所以硬件设计里通常会把 key 按定长前缀拆开比较完成后再进内容比较。这跟 CPU 上做字符串比较时先比长度、再比内容的思路一脉相承。压缩和校验也值得一提。如果源文件是压缩存储归并前必须解压如果目标文件要求压缩归并后还得重新压。很多引擎还会按块计算 checksum写入时校验。这些环节如果全部留在 CPU 上那么即便归并单元加速得再快CPU 仍然要承担大量数据搬运和计算收益会被明显稀释。论文的完整方案理想状态下应该把解压/压缩也一并卸载但这确实是不小的工作量。以我的判断第一版很可能只对特定编码方式、或者只对纯归并过程做加速其余交给软件优先打通全链路收益。失败恢复是另一个必须提前设计、否则上线就翻车的环节。compaction 最怕做到一半出故障旧文件删了、新文件没写完系统就进入不一致状态。规范做法是输出文件先写到一个临时位置完整落盘后才通过元数据切换让新文件可见旧文件才能删除。放在 FPGA 卸载场景里硬件侧还要额外支持“任务断点状态记录”比如当前任务已经写到了第几个块、写出的 key 范围到哪里这样系统重启后可以决策是重做还是续跑而不是从零再来。4. 性能收益、成本与适用边界4.1 论文报告了哪些收益又该怎么理解论文里报告的实验按我读到的内容来看覆盖了典型的写密集和读写混合 workload。整体结论是正向的持续写入场景下吞吐有提升CPU 占用大幅下降compaction 延迟收敛得更快用户请求的长尾延迟也有改善。这里我想特别解释一个很多读者容易误解的地方写吞吐提升并不是因为单次写入变快了而是因为后台 compaction 不再侵占 CPU 核前台请求获得了更稳定的资源。举个例子64 核服务器上如果原来有 4~8 个核被后台 compaction 烧满那么这些核被卸载到 FPGA 之后相当于把一批计算资源“还给”了业务路径。p99 延迟的改善同理它主要来自资源争用的消除而不是硬件本身的速度奇迹。所以读这类论文别只盯着“FPGA 比 CPU 快了多少倍”要看到端到端的系统收益CPU 占用下降、IO 路径更平稳、调度更可预期。这些指标对在线系统的重要性往往超过峰值吞吐那一两个百分点。4.2 硬件成本和功耗不算便宜但算得过来账FPGA 加速板卡的成本不能忽略一块中高端板卡的价格比一块 CPU 还贵功耗也通常在三五十瓦以上。但如果从整体来看这笔账需要换一个角度不是“一块卡替代一颗 CPU”而是“一块卡卸载掉了原本需要 4~8 个核持续运转的后台任务”对应的功耗节省是整个服务器 CPU 功耗的下降同时业务侧还获得了更稳的延迟。工程上还有一个容易被低估的成本开发。FPGA 的设计调试周期比纯软件长得多。软硬件接口调试、DMA 描述符错误、中断丢失、边界情况下缓冲溢出每一项都可能耗掉以周计的时间。所以在评估这套方案的性价比时一定要把“团队有没有 FPGA 开发能力”算进去。如果没有那论文里的思路初期阶段更适合先在软件里做流水线化模拟而不是直接买卡。4.3 什么负载适合什么负载别硬上我把适用场景整理成一张判断清单个人经验挂在后面适合加速的负载特征不适合加速的负载特征写多读少compaction 占总 CPU 比例高读多写少compaction 很少触发任务粒度大文件级别合并小任务高频DMA/中断开销摊不掉多个 compaction 可以并发复用硬件单任务极其短小硬件加速比小于 1后台任务导致延迟抖动明显CPU 资源本身过剩一个实用的预判方法先做 profiling统计 compaction 相关线程的 CPU 占用比例。如果长期低于 10%~20%卸载价值不大如果超过 30%那这套方案就值得认真考虑。别在没画 profiling 之前就决定架构这是我在多个项目里吃过亏之后总结出来的原则。5. 局限、改进方向与我的工程判断5.1 工程落地的几个硬门槛第一道门槛是开发门槛前面已经说过第二道是中断与轮询的取舍。设备任务完成通知如果全部走中断在高频小任务场景下 CPU 会被干扰得很难受一次中断响应本身的代价可能是几千周期。工程上通常会用完成队列轮询 中断合并的方式大部分时间 CPU 主动轮询完成队列长时间空闲时才靠中断唤醒。这个细节对实际加速上限影响非常大但论文图表里一般看不到。第三道门槛的一致性要求我在 3.3 已经提到这里不重复了。第四道门槛其实是部署形态很多系统并没有独立 FPGA 卡的位置供电、散热、机箱空间都要考虑。现在很多服务器已经转向 DPU/智能网卡形态如果把同样的卸载逻辑放到 DPU 上部署可行性会好很多。论文的硬件形态是 FPGA 卡但它的任务抽象和调度思想完全可以直接迁移到 DPU。5.2 从这篇论文延伸出去的方向说几个我觉得有价值的后续演进方向。软硬件接口层面可以把任务描述符标准化成一种“后台存储操作卸载协议”。数据块引用、快照序号、key 范围这些信息统一描述之后软件引擎和硬件加速器可以松耦合甚至多个引擎共享同一块硬件资源。功能层面compaction 的归并只是第一步布隆过滤器更新、范围删除Delete Range的落盘编码、校验计算、更高效的压缩算法都是可以继续下沉到硬件的工作。越往后面走CPU 在后台任务上需要做的事情就越少系统留给前台请求的空间就越充足。还有一条路线值得关注用软件做“卸载模拟器”。在没有 FPGA 板卡的情况下把 compaction 抽象成描述符队列用独立的线程池消费描述符先验证调度策略、背压设计和流水线拆分是否合理等真正拿到硬件再替换执行引擎。这条路径成本低、收获稳定我认为对许多团队来说是最值得先做的一步。5.3 几点个人体会我没有参与这个系统的实现但读完之后最大的感受是软硬件协同的成败往往不在于归并单元设计得多精巧而在于系统建模有没有做对。把一次 compaction 从“一个函数”变成“一条流水线”给每个阶段标上缓冲深度、期望带宽、允许时延然后再去谈卸载策略很多问题会自己浮出水面。反过来如果连软件的流水线都没理清楚就直接上 FPGA大概率会把软件里的串行瓶颈原封不动搬进硬件。另一个体会是读论文不要只读主结果一定要问一句这个收益到底是从哪一段省出来的。如果 write offload 只是把归并计算变快了但数据搬运还是老路径收益就不会大只有读-并-写三段真正重叠起来才能解释论文里那些好看的整体指标。理解了这一点后续无论做软件优化还是硬件卸载思路都会清楚很多。最后再分享一个小技巧写这类系统的代码前先把“谁等谁”画清楚。比如软件等硬件完成硬件等软件释放缓冲两边都有可能互等一旦互等就是死锁。我在实际项目里调试过这种问题最后往往是靠超时机制和任务级状态机解决的而不是靠现场翻代码。这个教训我觉得比任何性能数字都更值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑