《How to Write to SSDs》开头有一组很直接的数据:同一块 Samsung PM9A3,数据库占用约 90% 容量时,in-place LeanStore 每次 4 KiB 数据页写入,最终对应 18.85 KiB flash 写入;改成 out-of-place,再加上压缩、页面打包、按失效时间组织回收等优化以后,这个量降到了 2.42 KiB。吞吐也从约 23.8 万 OPS 提高到 47 万 OPS。1

图 1:《How to Write to SSDs》Figure 1 原图。负载为 YCSB-A,Zipf 参数为 0.8;页大小分别为 MySQL 16 KiB、PostgreSQL 8 KiB、LeanStore 4 KiB。左图按各系统的一次数据页写入归一化,论文在这组分析中排除了 WAL 写入。它适合解释写入来源,不能直接当成每次业务 Put 的写放大排行榜。1
2.42 KiB 小于 4 KiB,包含压缩的作用;从 18.85 降到 2.42,也包含数据库自己少写和 SSD 内部少搬两部分。仅给 SSD 发出顺序请求,解释不了这些收益。
数据库知道哪些数据会一起失效,哪些写入在等提交,哪些后台工作可以推迟。怎样把这些信息用起来,才是应用与 SSD 协同设计的具体内容。
1 数据库写入与 FTL 的职责
先固定一个最小模型。业务提交一条更新,数据库可能先追加 WAL(按恢复顺序组织的日志记录),再修改 MemTable 或 buffer pool,之后通过 flush、checkpoint 或 Compaction 把数据写进长期文件。文件系统把文件偏移映射为块地址;SSD 的 FTL 再把 LBA 映射为 NAND 位置。数据库里的一个版本过期、文件系统释放一段空间、FTL 判定一个物理页失效,是不同的事件。
主机控制写入内容、请求大小、并发、逻辑地址和提交时机;常规块接口下,物理放置、NAND 编程和设备 GC 由控制器决定。主机连续写一个文件,可以改善请求组织;它没有获得指定 NAND block 的权限。
这个职责划分会造成信息损失。数据库知道一批 SST 很快会被下一轮 Compaction 替换,控制器看到的却只是不同 LBA 的写入。数据库已经删掉的数据,如果还被旧快照或文件引用,文件系统暂时不能释放;文件系统已经释放的数据,若失效通知没有传下去,设备仍可能保留它。
所以,上层设计需要同时处理持久化、工作量和数据生命周期。持久化决定什么时候可以确认成功;工作量决定交给设备多少读写;生命周期决定以后回收时还得搬多少有效数据。只盯着当前请求的延迟,后两项成本很容易落到下一轮写入上。
2 SSD 的性能约束与数据失效时间
2017 年的《The Unwritten Contract of Solid State Drives》把跨层性能约束整理成五条规则,研究使用应用与文件系统的真实 trace,加上不同 FTL 的模拟分析。2 对今天的数据库,仍可以用下面这张表检查写入形态。
| 论文规则 | 主要影响对象 | 上层需要检查的信息 |
|---|---|---|
| Request Scale | 内部并行度 | 请求大小与在途数 |
| Locality | 映射与数据缓存 | 活跃地址范围 |
| Aligned Sequentiality | 混合映射转换 | 对齐与写入顺序 |
| Grouping by Death Time | GC 有效页搬迁 | 预计失效时刻 |
| Uniform Data Lifetime | 磨损均衡 | 数据存活时长分布 |
表中各规则的适用程度依赖 FTL。尤其是 Aligned Sequentiality,论文对应的是 hybrid FTL 的映射转换成本,不能据此给所有现代 NVMe 设备规定统一的 block 大小。论文还指出,应用层已经分好的数据,经过文件系统与 FTL,仍可能重新混到一起。2
其中最值得上层拿走的是 death time。它指数据被覆盖或丢弃的时刻,和 lifetime,即从写入到失效的时长,需要区分。
构造一个数据库例子:一个对象在 10:00 写入,10:10 失效;另一个在 10:08 写入,也在 10:10 失效。它们的存活时长分别为十分钟和两分钟,但可以在 10:10 一起回收。反过来,两份都能活十分钟的数据,写入时刻相差一小时,也不会一起失效。这个例子只是解释两个概念,没有假定某种数据库或设备已经实现这种放置。
对时序系统,过期时间相近的分区、由同一轮 Compaction 替换的文件,都是可以研究的分组依据。这里仍有附加条件:迟到数据、TTL 变更、快照、备份与恢复保留都可能延后删除。只有逻辑过期时间相近,尚不足以证明底层空间可以同时释放。
按热度分组比较容易做,按实际失效时刻组织回收更贴近 GC 的目标。预测不准时,应该让后续回收修正分组,不能为了维持最初分类,长期保留已经明显失真的布局。
3 数据库写入给 SSD 带来的压力
WAL 的小块追加和频繁同步,容易让提交路径受单次响应时间限制,设备也未必得到足够的并行工作。Group commit 可以让多条更新共用一次日志写入与同步,摊薄提交成本,但等待凑批又会增加前台延迟。3 调整批量时,必须保留原来的持久化条件;O_DIRECT 本身不能代替同步保证。45
LSM 的 flush 和 Compaction 会反复写出相同数据,消耗主机带宽,也给 SSD 增加后续回收工作。后台读写与 WAL、查询共享队列和介质资源,集中执行时可能拉高前台尾延迟。67 把后台任务全部压住也有代价:待整理数据和空间占用持续增长,最终仍会触发写入停顿。8 因此,顺序写的形态、实际写入量和后台能否持续推进要一起看。
数据删除则存在另一种错位。tombstone、快照解除引用、文件删除和设备收到 discard 发生在不同时间;设备尚不知道哪些 LBA 已失效时,仍可能为它们保留空间。即使失效通知已经传到设备,若短命数据与长期有效数据混在同一回收区域,GC 仍须搬走有效部分才能腾出空间。fstrim 可以传递文件系统的空闲范围,但不承诺立即完成 NAND 擦除。9
这些压力最终表现为提交等待、重复写入和有效数据搬迁。《How to Write to SSDs》选择从数据页的写出与回收路径入手:同时改变地址映射、页面布局和 GC 策略,让数据库少写一些,也让 SSD 少搬一些。
4 How to Write to SSDs 的跨层写入设计
4.1 Out-of-place 的地址映射与恢复
固定偏移的覆盖写把逻辑页与存储位置绑定在一起,限制了变长压缩页的布局和按失效时间重新分组。要获得这些放置自由,就需要允许页换位置,同时保证掉电后仍能找到可恢复的版本。1
论文改造的是 B-tree 引擎 LeanStore。先看一个具体例子:逻辑页号 PID 7 的旧内容 v1 放在位置 A,现在要刷出新内容 v2。in-place 的做法是继续向 A 写;out-of-place 则把 v2 写到新的位置 B,再把数据库维护的映射从 PID 7 → A 改成 PID 7 → B。B-tree 仍然引用同一个页号,改变的是这个页号对应的存储位置。1

图 2:依据论文 Figure 2 与 §6.2 绘制的简化示意,PID 7、A、B 是解释用的例子。A/B 表示数据库的存储偏移,不是 NAND 地址;图中省略了文件系统与 SSD FTL 的映射。虚线和实线分别表示切换前后的映射,图 c 中的映射提交也不等于客户端事务确认。1
图 c 的顺序很重要:先把 B 中的新页持久化,再提交指向 B 的映射更新。 否则掉电后可能恢复出一个指向未写完整页面的映射。新映射尚未提交时,旧位置 A 仍可供恢复使用,再结合 WAL 重放更新;新映射提交以后,恢复出的映射才能安全地定位到已经持久化的 B。1
因此,增加映射表也增加了恢复责任。论文会记录映射更新,checkpoint 保存映射表和活跃分组历史;恢复时读取 checkpoint、重放后续日志,再根据恢复出的映射重建空间信息。旧位置 A 也要等到引用与恢复都不再需要它时,才能交给数据库 GC 回收。1
这一层间接映射让逻辑页号与存储位置解耦。页写到 B、C 还是别的位置,不必由页号固定下来,后续的压缩、页面打包和按失效时间分组才有了重新安排布局的空间;代价则是维护映射、恢复元数据以及回收旧位置。
图 a 还有一个风险:覆盖写只完成一部分就掉电,A 中可能同时含有新旧内容,这就是 torn page。MySQL 的 doublewrite 和 PostgreSQL 的 WAL full-page image 分别提供恢复所需的页内容;后者开启 full_page_writes 时,checkpoint 后首次修改数据页会把完整页内容记录到 WAL。10 这些机制的写入路径不同,不能把图 1 中的额外写入都解释为每次刷页固定写两份相同的数据文件。
4.2 压缩、页面打包与失效时间分组
可压缩的页面仍按原始大小写出,会消耗更多写入带宽;失效时间混杂的区域在回收时,又要搬迁仍有效的页。这组设计要同时减少写入量和数据库 GC 的搬迁量,并避免压缩后的页面布局增加单页读取次数。1
4.2.1 压缩后的页面打包
压缩降低字节量,也改变空间占用;但压缩后的变长页若横跨读取边界,点查可能需要额外 I/O。论文用 page packing 将压缩页组织到 4 KiB 槽位,使单页读取落在一次 4 KiB 访问内。1 这个设计同时考虑了少写和以后怎样读。这里的 4 KiB 是论文在所测设备上采用的读取组织,并非所有设备都具有相同的内部 NAND page。
4.2.2 新版本的失效预测与放置
Grouping by Death Time(GDT)的分组发生在一个页面版本写出时:先预测这个新版本以后何时会被替换,再把预计失效时间相近的版本放进同一个数据库 zone。 数据库不需要让这些页在同一瞬间过期;目标是在某个 zone 需要回收时,其中尽可能多的旧版本已经失效。1
这里的失效,是指一个磁盘页版本被后继持久化版本替代,不是业务 TTL 到期,也不是 NAND 已经擦除。4.1 的 out-of-place 映射让数据库能够为同一个 PID 的每个新版本另选位置,因此可以在写入前做下面这些安排:
-
预测这个版本的下一次替换。 页头保存最近几次正常持久化写入的 LSN 历史。论文把当前 LSN 加上历史平均写入间隔,得到预计失效时刻 EDT,这里用 LSN 而非墙钟时间作为进度坐标。GC 搬迁不更新这份历史,避免把搬迁频率误当成业务更新频率;首次写入缺乏历史时,先按 B-tree index ID 分组。1
-
为新版本选择 zone。 一批页刷出时,先按相近 EDT 分组并打包,再选择已有页面平均 EDT 最接近的活跃 zone;没有合适的活跃 zone 时,打开新 zone。新内容写进该 zone 的可写位置,并更新 PID 到位置的映射。这里的 zone 是数据库自己管理的存储区域,不要求设备支持 ZNS。1
-
让后续更新使原来的版本先后失效。 这些页以后再次正常刷出时,后继版本会重新选择写入位置,旧位置便不再是 PID 当前指向的版本。如果预测接近实际更新行为,同一个旧 zone 内的版本就会在相近阶段陆续失效,而长时间有效的版本主要留在别的 zone。1

图 3:《How to Write to SSDs》Figure 5 原图,数字表示页面版本的失效 LSN。它用已知失效时刻展示理想放置的效果,实际实现使用预测的 EDT。数据库 zone 不等于 NAND block,也不要求底下必须是 ZNS 设备。1
以图中的例子看,在 LSN 0 写出八个版本时,把预计在 1、2、4、6 失效的四个版本放进 zone 0,把 35、50、60、74 放进另一个 zone。若后续替换符合这组预测,到 LSN 10,zone 0 中的旧版本已经全部失效,回收它时不必复制有效页。它们并没有同时失效,只是在回收之前都已经失效;若把早失效和晚失效的版本混放,每个 zone 到这时都可能还剩有效页需要搬迁。1
预测错误时,GDT 也允许回收尚未完全失效的 zone。GC 选出回收区域后,把其中仍有效的页按 EDT 重新分组、打包,尽量放入 EDT 匹配的开放 zone;没有匹配位置时,论文允许将页留在原区域。只读等不再正常重写的页赋予最大 EDT,按最冷的数据处理。已有的混放不会因开启 GDT 立刻消失,要在正常刷出和必要的 GC 搬迁中逐步纠正;已经失效的内容本身不需要再搬到一起。1
这一节首先减少的是数据库 GC 为腾出空间而复制的有效数据量,不能直接推成 SSD GC 次数下降。PID 从位置 A 改指 B,先改变数据库对 A 的有效性判断;若 A、B 是不同 LBA,SSD 不会仅凭这次写 B 就知道 A 已无效,还要等对应 LBA 被覆盖或收到失效通知。数据库分组怎样进一步减少设备内部搬迁,是下一节要解决的问题。19
4.3 数据库 GC 与设备 GC 的协调
数据库已经按失效时间分组,SSD 仍可能把这些区域的并发写入交错放进同一内部回收单位,导致数据库和设备先后搬迁同一批有效页。要减少设备这一层的写放大,还需让数据库区域的写入与失效方式,配合 SSD 的实际放置和回收粒度。1
对普通 SSD,论文通过受控写入形态估计内部放置的尺度,再约束活跃区域和回收选择。论文用 superblock 表示跨 plane、die 聚合起来的内部追加单位:多个数据库区域同时写入时,控制器可能把它们交错放进同一批 superblock。1
看论文里的简化例子。数据库区域 A、C 同时追加,数据混入一批 superblock;之后 A 被更频繁地重写,原来的 A 页陆续失效,C 页却还有效。设备为回收这些空间,就得搬 C。NoWA 要求当前一组活跃区域全部写满以后才打开新组,并记录分组历史、识别失效频率的不平衡;必要时主动重写较少更新的 C,让原位置中的 A、C 都能失效,目标是在设备触发 GC 前形成完全失效的 superblock。1 主机多写了少量补偿数据,换取后续更少的设备搬迁;实际回收选择还要避免把新的失衡留给下一轮。
并发开放的区域数和每区大小还要一起匹配。论文允许缩小单个数据库 zone,但要求最大开放 zone 数与 zone 大小的乘积,等于推定 SSD GC 单位的大小,或为其整数倍;完整优化组合同时使用 NoWA 和 GC 粒度匹配。1 仅采用补偿写规则,还没有复现这些条件。
设备也可能因调度、负载、NAND 状态、活跃 superblock 限制或磨损均衡重排和迁移数据,因此实际并不保证所有设备、所有状态下都完全消除内部放大。论文 §7.4 的六设备实验中,完整优化组合使 SSD WAF 接近 1;Solidigm D7-P5520 仍为 1.12,论文说明它在顺序负载下也略高于 1。1 模型目标与这组细分实证需要分开理解。
把普通 XFS 上的 SST 文件大小设成论文里的某个区域大小,还没有满足这些条件。文件系统分配、其他进程和 namespace 的写入、并发提交顺序,都可能改变控制器接收到的序列。应用需要控制实际写入顺序与区域开放方式,并用物理写计数验证内部搬迁是否减少;文件大小相同不足以复现论文的放置约束。

图 4:《How to Write to SSDs》Figure 12 原图。Buffer Manager 提供页面与失效时间估计,I/O Interface 完成压缩、分组和打包,Space Manager 管理映射与回收区域,GC 的读写重新经过同一条路径。这里增加的地址映射、GC 和恢复职责,都属于取得收益的实现成本。1
论文在每轮前重置设备映射状态,持续运行到累计数据库写入至少达到设备容量的四倍,并在可比写入量之后统计最后一小时。1 这些条件把初始化、持续回收和统计窗口纳入了对照;判断写入设计的收益,也需要覆盖其产生的后台搬迁,不能只观察刚开始写入的阶段。
5 Streams、FDP 与 ZNS 的放置与回收方案
普通块接口上的 NoWA 通过约束写入顺序和区域开放方式,减少设备内部的混放。另一条路径是让主机直接向设备表达分组或回收意图。Streams、FDP 和 ZNS 都与这个问题有关,但交给主机的控制权不同。《How to Write to SSDs》也实现了 FDP 与 ZNS 适配,同一套页面映射和生命周期组织可以连接不同的设备接口。1
5.1 Streams 的写入分类与混放控制
Streams 要解决的是:设备只看到一串 LBA 写入,不知道哪些数据很快会被覆盖,哪些还要保留很久。主机在写请求中附带 Stream ID,把预计失效时间接近的数据归到同一流;支持这一接口的 SSD 据此分开放置,尽量让同一回收区域中的数据一起失效,减少 GC 时要复制的有效内容。2014 年的 multi-streamed SSD 论文已在原型上验证这种思路。11
在数据库中,可以先把频繁轮换的日志、短命 SST 与长期冷数据作为候选类别,再依据实际删除和覆盖行为调整。文件类型只是分类起点:同为 SST,不同层级、快照和更新分布下的失效时间也可能差很多。可用流数量受设备限制,分类需要利用这些有限资源。
具体的物理地址映射、擦除和 GC 仍由 FTL 完成。Stream ID 传递的是数据分组信息;它不表示 I/O 优先级,也不让主机直接指定某个 NAND block。11
5.2 FDP 的回收单元与放置标识
FDP(Flexible Data Placement)继续保留按 LBA 随机覆盖写的接口,同时向主机暴露 RU(Reclaim Unit,回收单元)等放置资源。写请求携带 Placement Identifier,其中的放置句柄经 namespace 映射选择 RUH(Reclaim Unit Handle),回收组标识再选中该 RUH 在对应组中的当前 RU;写满后由控制器切换到新的 RU。主机选择的是放置关系,不是永久固定的 NAND 块。1213
数据库可以把预计共同失效的一组页或文件,持续写向相应的放置资源。《How to Write to SSDs》的实现给每个数据库 zone 保存一个放置标识,将 zone 大小设为 RU 大小,并将同时开放的 zone 数限制在可用 RUH 数量以内,使不同活跃区域的写入进入不同资源。在这些条件下,论文通过 FDP 避免区域之间的交错混放,不再需要 NoWA 的补偿写。1
FDP 的收益来自分类与设备回收单位的配合。它没有要求应用像 ZNS 那样按 zone 写指针追加、再显式 reset;FTL 和设备内部 GC 仍然存在。12 分组在 GC 后能否继续隔离,还取决于 RUH 类型:Initially Isolated 允许 GC 在同一回收组内合并来自不同 Initially Isolated RUH 的数据,Persistently Isolated 则要求搬迁后仍保持对应 RUH 的隔离。13 因而要同时核对设备能力、放置资源数量和物理写量,不能把添加标识本身当作优化完成。
5.3 ZNS 的顺序写与整区回收
ZNS 把回收组织变成了明确的接口约束。namespace 被划分为 zone,允许随机读;写入需要沿 zone 的写指针推进,重新使用该 zone 前必须 reset。更新旧数据时,上层写到新位置并维护映射;如果一个 zone 还有有效数据,就先迁出这些数据,再释放整个 zone。14
这使数据库能够直接决定哪些数据共同占用一个可回收区域。例如,把预计一同删除的 SST 放到相同的 zone 组,等所有引用消失后整区 reset,就能减少回收时的有效数据搬迁。ZNS 论文中的 ZenFS 已利用 RocksDB 的 write-lifetime hint 安排文件的 zone 放置;对生命周期的判断由数据库提供,ZenFS 存储后端负责把它落实到设备区域。14
上层也因此承担 zone 分配、开放与活跃数量限制、剩余容量管理、有效数据迁移,以及映射和回收状态的崩溃恢复。SSD 继续负责介质管理与可靠性,一个 zone 也不应直接理解为单个 NAND block。15 如果主机没有为迁移预留空闲区域和后台预算,改用 ZNS 后仍会因空间回收跟不上而停顿。
Streams 传递分类,FDP 将分类关联到回收资源,ZNS 则把顺序写与整区释放交给主机组织。三者都需要数据库先识别哪些数据可以一起失效,并确认这些分类或管理命令能够穿过实际文件系统、块层与驱动到达设备。
6 结束语
数据库按记录、页和文件判断数据是否有效,SSD 按自己的物理单位写入和回收空间。两边的粒度与时序不一致,一次小更新就可能引出整页重写、数据库搬迁和设备搬迁。数据库结合 SSD 做结构设计,要减少的是完成同一批业务更新所需的总工作,同时控制这些工作怎样进入前台的等待路径。
从本文的几种设计,可以归纳出以下过程:
-
先固定持久化与可见性条件。 明确何时可以确认事务成功、哪些旧版本仍被引用、页数据和地址映射按什么顺序持久化。这些条件决定能合并哪些提交、何时能释放旧位置,也是批量写、异地更新和回收共同遵守的约束。
-
分别设计更新、读出和回收的单位。 业务更新的粒度不必等于磁盘请求或空间回收的粒度。压缩和页面打包可以减少字节,但还要让点查维持合理的 I/O 次数;地址间接层可以允许重新布局,也会增加内存和恢复成本。引擎已有页映射、文件版本或 value 引用时,应先利用这些结构中的信息。
-
让布局跟随数据的实际失效过程。 根据更新历史、文件替换和过期规则安排分组,再让 GC 修正预测错误。旧版本解除引用以后,要及时把失效信息传下去。若在普通块接口上采用 NoWA,需约束实际写入序列与区域开放方式;Streams、FDP 可以传递分类,ZNS 可以直接组织区域重用。选择哪种接口,取决于应用能控制哪些信息、愿意承担多少回收职责。
-
为提交和回收共同分配资源。 批量与并发决定当前怎样使用设备,flush、Compaction 和 GC 决定后续是否仍有空间可写。前台尾延迟达标的同时,后台积压不能持续增长,回收也要及时补充后续写入所需的空闲空间;不能靠长期推迟后台工作获得一段好看的低延迟。
-
在持续更新与故障恢复中验证收益。 固定持久化语义、有效数据量和负载,联合比较主机写量、物理写量、读放大、CPU、空间与尾延迟。观察要覆盖多轮回收;新增映射、分配和 GC 元数据则要经过崩溃恢复验证。只降低数据库写放大或设备 WAF,都不足以证明整体设计更好。
《How to Write to SSDs》把这些关系放进了同一条写入路径:压缩后的页怎样读取,异地写入后怎样定位,页面怎样共同失效,两层 GC 又怎样少做重复搬迁。可迁移的是这些设计问题及其相互约束;具体 zone 大小、并发数和补偿策略,需要在自己的引擎与设备上重新验证。
7 四篇系列总结
四篇文章围绕同一次数据库 I/O,逐步把观察范围从介质扩展到上层设计:
-
《从一到无穷大 #101:SSD 的工作原理------从 NAND 到数据库读写》解释 NAND 的写入与擦除粒度、FTL、GC 和内部并行,说明主机的一次逻辑写入为什么会变成更多物理工作。
-
《从一到无穷大 #102:Linux IO 路径------从系统调用到 NVMe 驱动》沿文件、bio、request 与 NVMe 命令区分对象和完成位置,用 eBPF 观察等待发生在哪一段,也说明哪些阶段无法仅靠主机探针继续拆分。
-
《从一到无穷大 #103:SSD 的使用率与性能余量------从 %util 到数据库延迟》把完成率、实际在途量和响应分布放在同一负载对照中,判断路径是否受限,再用 CPU、链路、温控和写放大等指标解释原因。
%util接近 100% 只能提供活跃程度信息,不能替代这组判断。 -
《从一到无穷大 #104:面向 SSD 的数据库写入设计------从 FTL 到应用协同》进一步改变交给设备的工作:减少重写,组织数据生命周期,并协调提交、放置与回收。
落到一次具体的慢写,可以先定位线程在等什么,再测出对应负载的吞吐与延迟关系,最后决定应减少哪一层的工作,或改变哪一种数据布局和回收规则。介质原理给出约束,主机路径提供观测,实验确定收益是否成立,数据库结构则决定这些收益能否在持续运行中保留下来。
8 参考资料
1 Bohyun Lee, Tobias Ziegler, Viktor Leis. How to Write to SSDs. PVLDB 19(7):1469--1483, 2026
4 Linux man-pages. fsync(2), fdatasync(2)
5 Linux man-pages. open(2), O_DIRECT 与 O_SYNC
6 RocksDB. RocksDB Tuning Guide
10 PostgreSQL 18 Documentation. WAL Configuration
11 Jeong-Uk Kang et al. The Multi-streamed Solid-State Drive. HotStorage 2014
12 NVM Express. Flexible Data Placement: State of the Union. August 2023
14 Matias Bjørling et al. ZNS: Avoiding the Block Interface Tax for Flash-based SSDs. USENIX ATC 2021
15 Zoned Storage. SSDs with NVMe Zoned Namespace (ZNS) Support