How to Write SSD

我们之前介绍了 SSD 的基本知识,但是描述的非常基本。这里基本没有涉及到一些 SSD 的接口的部分。这一篇文章介绍一下:

  1. SSD 的一些接口,包括 GC 的一些原理
  2. 如何 (友好地)去写 SSD

旧接口和原理介绍

如果你还记得 https://blog.mwish.me/2026/06/03/SSD-Internals/ 的内容的话,你就会回忆起来,SSD 单个 Block 写入的并发是受限的,我们通常会有 Die/Plane 级别的并行。我们下面可以介绍一下 SuperBlock 的概念:

SSD 为了打满多通道的读写带宽,会从每个 Channel、每个 Chip 上各取一个擦除块,把它们绑定成一个更大的逻辑块,这个逻辑块就叫超级块。这里他们可以当作逻辑上的一个或者一组 Block,顺序写入,一起 GC。

img

如果你还记得 https://blog.mwish.me/2025/07/01/Architecture-and-Design-of-the-Linux-Storage-Stack-Block-Layer/ 的内容的话,bio 层会组织给下面发送的块请求。块设备会有 /dev/nvme0n1 这样的名,然后这里面大小就是块设备对外暴露的 LBA 长度之类的。块层包装了读写,也能处理下发 TRIM 之类的命令,一些这篇文章会介绍的 STREAM 之类的概念也能被放在块层来处理。用户层也可以通过 fstrim(8)之类的命令去发送 Trim 的请求给对应的挂载的 fs,或者直接在块设备上 TRIM。这里会 hint SSD 说这段连续的 LBA 可以不用了。不过 Trim 的时间可能会比较长。

需要知道的是,bio 层支持了很多 NVMe 和 SSD 有关的新 Feature:

img

但也有很多块层不一定支持的命令,新一些的系统或者类似的行为可能需要

  1. io_uring Passthru, 这块可以参考论文 I/O Passthru: Upstreaming a flexible and efficient I/O Path in Linux
  2. 字符设备 passthrough, 给设备 ioctl 发送相关命令,是绕过文件系统的裸盘应用
  3. 有的未来可能有上层的支持,即块层等支持之后暴露给上层的接口。

img

我们可以以接受度之类的视角来看这些,一些新的内容通常就需要 io_uring passthru 或者 SPDK 等方式来处理了,TRIM 之类的广为流传的则可以在系统中找到已有的上层暴露接口。

减少写放大的方案

我们在之前的文章中知道,SSD 要 GC 单位至少是一个 Block,而这一篇文章我们学到了,这个单位也可能会是一个 SuperBlock。这里有一个想法是,如果一个 GC 单元中,用户知道数据有几种不同的生存期,所以写入的数据中,数据要么不 GC,要么可以尽量一起被 GC,这样就能大大减少 GC 的时候,重写这里有用的 Page 的开销。下面有种种方案来优化这一流程。

  1. 提供某种 hint,表示写入的数据会聚在一起 (FDP/Stream)。需要注意的是,这些方案一般默认需要修改的代码比较少,如果什么都不改,和正常 SSD 可以一样的处理,可能这里会 assign 一些默认的流或者 RUH (后文辉介绍这些概念,先当成 assign 全局的大默认 id 即可,不做任何隔离/hint)。
  2. 把 GC 单元和内部一些逻辑交给上层处理 ( ZNS / FDP ),下面只处理 SSD 容错的一些东西。这里如果要靠谱的用上一般需要一些对应代码的适配。

img

Stream 模式如上图,通过给 SSD 提供一些 Write Hint ( RocksDB 也有相关的代码,搜索 write_hint 即可),给流加入一些 stream_id,然后这里会把流 ID assign 给不同的 SuperBlock 来写。这里如上图所示。

Stream 进入了 NVMe 标准,但因为没有任何主流厂商真正交付可用的硬件实现,也没有已知的真实业务在使用,所以没啥人接着用了。但这套东西很多优势被吸收到了下面的 FDP 中。

img

FDP 和 Stream 很相似,但它允许:

  1. 配置 Handle (RUH) 来写入
  2. 回收的单元为 RU,RU 虽然在有些文档中被推荐为和 superblock 对应,但它只要是 A set of NAND blocks on an SSD 就行
  3. 回收单元为 RG,有的地方默认为 1 RG(整盘一起回收),不过也可以独立配置

这样,可以有不同的 RUH 来写入,每种 RU 对应一种类型的写入流,比如说:

  1. 元数据频繁更改,对应一种 RUH;数据对应一种
  2. 数据会有 TTL catagory,1天内对应一种,1-3天对应一种,长期 living 对应一种

这样既能切分租户写入隔离,也能提供不同的生存期。

img

ZNS 可以把盘空间切分为不同大小的 GC 和写入单元 Zone,上层知道这个信息并写入。ZenFS 就是这个场景的使用者。SMR 的机械硬盘也有类似的逻辑。

img

我们某种意义上可以把这里当成 SSD 厂商和用户(FS 和更上层用户)妥协的过程。

FDP 和 CacheLib FDP 改造

这项工作似乎主要由三星的成员完成,核心优化来自 SSD 的 SOC/LOC 的大小对象区分,这两种对象写放大/生存期不同,配置不同 RUH 即可。

img

How to Write SSD 的实践

TUM 的论文引入了一系列实用的方法在 LeanStore 这个 Btree 存储上

img

Out-of-place write

人人都用 LLAMA 有没有懂的?这里核心是每次写用新地址,然后老地址 + WAL == 新地址的,充当一份数据写的 Double-write Buffer。同时,这里写入的时候可以 assign 一个新的 LBA,为后续优化做准备。

Page Packing

和 EROFS 的思路很像。但没有那么细,核心问题是 out-of-place write 之后可以引入 (offset, length],然后一起 IO。但我个人觉得这个效果不一定好,反正我一直认为这东西做到上层或许会好些。

img

DEATHTIME-BASED GC

这里需要注意的一点是,这个地方因为是 Out-of-place write,所以 DB 需要自己负责 GC Old Block。一般的 LLAMA 之类的 Log-based System 本身有比较完善的 GC 了,参考:https://arxiv.org/abs/2005.00044

但这篇论文观点比较有意思,因为这个地方是 Btree Page GC,一个 Btree 对应的页面可能会反复写,所以对于 Btree 而言,能比较好有一个 Death time 估计:

  1. 记录 Page 最近的 n 次写入 timestamp,维护一个 write history ( WH ),根据 current_lsnWH,线性估计下次写入的时间(EDT)
  2. EDT 类似的会 grouping 到一起写,再选择平均 EDT 最匹配的活跃 zone 写入
  3. GC greedily selects victim zones until their cumulative invalid pages free the space of one zone. 然后写入对应的 zone,注意 ages that are never rewritten (e.g., read-only pages) are assigned the maximum EDT to be treated as the coldest data.
  4. 在 GC 结束后,给不同满度的 zone ( may be full, partially full, or empty ) 不同的 EDT

SSD 适配

对 ZNS 这里尽量 DB Zone 大小 == ZNS Zone 大小,如下图:

img

对普通 SSD 和 FDP 就比较好玩了,这里先要测出对应 RU 的大小。FDP 可以直接拿到 RU 大小,我们重点看 SSD:

  • 通过类 ZNS 写模式推断:单活跃 zone 下,逐步增大 zone 大小,当 SSD WAF 首次降到 1 时,对应的 zone 大小就是内部 GC 单元的上界。实验测得企业级 SSD 的 GC 单元通常在 4~8GB,论文建议 32GB 作为通用安全上界。

img

写入的时候,这里对 FDP,可以尝试给每个 DB Zone 一个 id,取模拿到对应的 RUH。

这里核心在论文提出了普通 SSD 减少写放大的 NoWA 方式:

  1. Active Group 是 NoWA 机制中用于管理并发写流的逻辑调度单元,定义为:同一时间段内,数据库并发打开、同时执行追加写入的一组 Zone 集合。
  2. 论文认为,写放大来自于同时打开多组 Active Zone 的话,SSD 写入的时候,实际对物理空间写入是 Multiplexing 的,可能会写到各处。所以同一个 EDT 应该尽量只对应一个 Active Zone ( When multiple zones are appended concurrently, their write streams interleave across different superblocks [100]. For example, if the DBMS writes to zones A and C at the same time (active group G1(A, C) in Figure 8), writes from both zones become mixed, scattering their data across two superblocks. This multiplexing effect [92] arises because standard SSDs cannot distinguish writes from zones A and C. )
  3. 因为 (1),所以这里如果不同 Zone 生存期不一样,活得久的会影响早死的回收。这里比如 Fig 10 发现有的组写的少,可能会把 C/D 中部分补偿写(重写 zone 来拖下水),来换得一样的 GC 时间。这里会尝试拉同一个 Active Group 的下水。本质上是用户写放大换 SSD 写放大。

img

讲道理这段我看的不是很过瘾,因为这个策略看着水很深,CW(D) 都不知道怎么进行,make sense 是 make sense,但感觉没有很简单?

Reference

  1. Introduction to Flexible Data Placement https://www.snia.org/educational-library/introduction-flexible-data-placement-new-era-optimized-data-management-2026
  2. Storage Abstractions for SSDs: The Past, Present, and Future https://dl.acm.org/doi/pdf/10.1145/3708992
  3. Towards Efficient Flash Caches with Emerging NVMe Flexible Data Placement SSDs https://dl.acm.org/doi/abs/10.1145/3689031.3696091
  4. How to Write to SSDs https://arxiv.org/abs/2603.09927
  5. https://github.com/sg20180546/FDP-awesome-paper