本章要解决什么

如何用闪存构建 SSD?如何应对擦除(erase)操作的高昂代价?反复覆盖写会把器件"写坏"(磨损,wear out),如何让设备用得久?

How can we build a flash-based SSD? How can we handle the expensive nature of erasing? How can we build a device that lasts a long time, given that repeated overwrite will wear the device out?

前几章我们一直在和机械硬盘打交道:寻道、旋转、顺序比随机快几百倍。这一章换成另一种持久存储——固态存储(solid-state storage),它没有任何机械部件,由晶体管构成,却和 DRAM 不同,断电后数据不丢。我们关注的技术是 NAND 闪存(NAND-based flash)。

闪存有两个"怪脾气":要写一个页,必须先擦除它所在的整个块(而且擦除很慢);同一个块擦写太多次会磨损报废。SSD 内部的闪存转换层(FTL,flash translation layer)要在这两个限制下,对外假装成一块普通的"按块读写"的磁盘。你会看到,第 43 章日志结构文件系统(LFS)的思想——追加写 + 映射 + 垃圾回收——在这里被原样搬进了设备内部。

核心概念速览

页(page)与块(block)

页约 4KB,是读和编程的单位;块(擦除块)约 128KB–2MB,含几十到几百页,是擦除的单位。

擦除 → 编程 → 读

擦除把整块置为全 1(几毫秒);编程把已擦除页的某些 1 改成 0(几百微秒);读一页只要几十微秒。

页状态 i / E / V

INVALID → 擦除 → ERASED → 编程 → VALID。VALID 页不能再编程,只能等整个块被擦除。

FTL

把主机对逻辑块的读写,翻译成对物理块/页的 read / erase / program;目标是高性能、高可靠。

写放大(write amplification)

FTL 发给闪存的总写入量 ÷ 主机发给 SSD 的写入量。越接近 1 越好。

日志结构 FTL

写总是追加到当前块的下一个空闲页,用映射表记录"逻辑页 → 物理页";覆盖写留下垃圾。

垃圾回收(GC)

挑一个含垃圾页的块,把其中存活页搬到日志末尾,再擦除整块回收空间。

磨损均衡(wear leveling)

让所有块被擦写的次数大致相同,避免少数"热"块先坏掉;代价是额外的数据搬迁。

从比特到块和页

一个单元存几个比特:SLC / MLC / TLC

闪存芯片把一个或多个比特存进一个晶体管:晶体管里困住的电荷量高低被映射成二进制值。

同一个晶体管的"电荷量"区间被切成 2 / 4 / 8 段 —— 段越窄,越难分辨、越慢、越容易出错 SLC · 1 bit 10 MLC · 2 bits 11 高10 偏高 01 偏低00 低 TLC · 3 bits 111000 …8 级 电荷量 最快 · 最贵 · 最耐用 折中 最便宜(容量大)· 最慢
图 1:每个单元存的比特越多,需要区分的电荷水平越多。原书:"SLC chips achieve higher performance and are more expensive."

块和页:闪存的两级组织

闪存芯片由若干 bank(也叫 plane)组成,每个 bank 里有大量单元,按两种不同大小的单位访问:

注意术语!这里的"块"和"页"跟磁盘的块、虚拟内存的页不是一回事。原书提示:"What is a block to you may be a page to someone else"——在闪存语境里,页是读/写的小单位,块是擦除的大单位。

一个 plane(bank)——真实芯片里有数千个块,每块几十到几百页;这里画 3 块 × 4 页(原书 Figure 44.1) 块 0 块 1 块 2 00 01 02 03 04 05 06 07 08 09 10 11 页:读(read)和编程(program)的单位,约 4KB 块:擦除(erase)的单位,128KB–2MB 要改写页 08 中的任何一个比特,都得先把 08~11 整个块擦掉
图 2:页在块之内。"要写块里的一页,必须先擦除整个块"是本章最重要(也最奇怪)的事实。

三种基本操作:读、擦除、编程

闪存芯片支持三种底层操作:读用来读页;擦除和编程配合起来完成写。

操作粒度效果耗时
读(read)页给出页号即可读出,与位置和上一个请求的位置基本无关——闪存是随机访问设备几十 µs
擦除(erase)块把块内每个比特置为 1,销毁整块内容;执行前必须先把块里还有用的数据搬走几 ms(最贵)
编程(program)页在已擦除的页上把某些 1 改成 0,写入页内容几百 µs

最好用页的状态来理解这些操作:页一开始是 INVALID(i);擦除所在块后,块内所有页变成 ERASED(E)——内容被重置,并且可编程;编程一个页后它变成 VALID(V),内容已设定、可以读取。读不改变状态。一个页一旦被编程,想改它的内容只能擦除整个块。

INVALIDi · 初始状态 ERASEDE · 全 1,可编程 VALIDV · 内容已写入 Erase(块) Program(页) Erase(整个块):同块所有页一起回到 E Read:状态不变 Program(已 V 的页) ✗ 出错 原书示例(4 页的块): iiii → EEEE → VEEE → VVEE → EEEE
图 3:闪存页的状态机。关键在右边那条红色虚线——已编程的页不能"原地改写"。
模拟器

一个闪存块:擦除、编程、读

一个含 4 个 8 比特页的块(尺寸是故意缩小的)。试着对已经 VALID 的页再编程;试着在块里还有有用数据时擦除——观察内容和状态如何变化。编程只能把 1 变成 0,所以只有擦除过(全 1)的页才能写入任意内容。
点"载入"或直接操作。

原书的详细示例:块里 4 个页都已 VALID(内容 00011000 11001110 00000001 00111111)。想把页 0 改成 00000011,必须先擦除整个块(4 页全变 11111111),再编程页 0。坏消息是:页 1、2、3 的旧内容全没了!所以覆盖块内任意一页之前,必须先把我们关心的数据搬到别处(内存或另一个闪存块)。擦除的这个性质,将决定整个 SSD 的设计。

闪存的性能与可靠性

器件读(µs)编程(µs)擦除(µs)
SLC25200–3001500–2000
MLC50600–900~3000
TLC~75~900–1350~4500

原书 Figure 44.2:裸闪存的性能特征(数字来自大众媒体 [V12])。

MLC 三种操作的延迟(线性刻度,每格 500µs) 读 50µs 编程 ~750µs 擦除 ~3000µs 60× 15×
图 4:读很快;编程慢一个数量级,而且单元存的比特越多越慢——想要好的写性能,就得并行使用多个闪存芯片;擦除要几毫秒,处理擦除的代价是闪存设计的核心问题。

可靠性:磨损与扰动

闪存是纯硅器件,不像硬盘那样会出现磁头划伤盘面(head crash)这类机械故障。它最主要的问题是:

编程页 2 时,相邻页 1、3 的比特可能被"带偏" 页 0 页 1 页 2 页 3 Program(2) → 邻居可能出现比特翻转 对策:在已擦除的块内按 0 → 1 → 2 → 3 顺序编程 ① ② ③ ④ 磨损:P/E 次数有限(MLC≈1 万,SLC≈10 万)
图 5:扰动和磨损是闪存的两类可靠性问题;后面的日志结构 FTL 天然满足"块内顺序编程"。

从裸闪存到 SSD:FTL

标准的存储接口很简单:按块地址读写 512 字节(或更大)的扇区。SSD 的任务就是在裸闪存芯片之上提供这个标准块接口。

原书的 TIP:定义稳定的接口,两边就能各自创新而保持互操作——文件系统看到的 SSD 和硬盘一样都是"块设备"。但接口过于僵化也有代价,比如 ZFS 就通过重新思考文件系统与 RAID 的分工,得到了更好的整体设计。后面的 trim 接口也是"实现反过来塑造接口"的例子。

SSD 内部有三样东西:若干闪存芯片(持久存储);一些易失性内存(如 SRAM,用来缓存和缓冲数据、存放映射表);以及协调一切的控制逻辑。控制逻辑里最核心的就是 FTL:把对逻辑块的读写请求,变成对物理块、物理页的 read / erase / program 命令。

主机文件系统 读/写 LBA SSD 接口逻辑Interface Logic 闪存控制器运行 FTLFlash Controller 内存(SRAM)映射表 · 缓存/写缓冲 Flash Flash Flash Flash Flash Flash 多个芯片并行 → 高带宽 read / erase / program
图 6:基于闪存的 SSD 逻辑结构(仿原书 Figure 44.3)。FTL 运行在控制器里,映射表放在控制器旁边的内存中。

FTL 的设计目标:

糟糕的方案:直接映射

最简单的 FTL 叫直接映射(direct mapped):读逻辑页 N 就读物理页 N。写逻辑页 N 就麻烦了:FTL 要先读出 N 所在的整个块,然后擦除这个块,最后把旧页和新页一起重新编程。

初始:块内 4 页都有效 ① 读出其余 3 页 ② 擦除整个块 ③ 编程 4 页 A B C D A B C D 内存:B C D E E E E A' B C D 几毫秒;块的 P/E 计数 +1 主机只写了 A',闪存写了 4 页 主机写 1 页 = 读 3 页 + 擦除 1 块 + 编程 4 页 → 写放大 = 块内页数 真实块有 64~256 页:写放大可达几十到几百倍,比有机械寻道的硬盘还慢;热点逻辑页所在的块会被反复擦写,很快磨损报废
图 7:直接映射 FTL 每次覆盖写都要"读-改-写"整块。

直接映射在性能和可靠性上都很糟:性能上,每次写都要读整块(贵)、擦除(很贵)、再编程(贵),写放大与每块页数成正比;可靠性上,如果文件系统元数据或用户数据被反复覆盖,同一个块就被反复擦写,很快磨损坏掉——直接映射把磨损的控制权完全交给了客户端负载。

日志结构 FTL

因此今天大多数 FTL 都是日志结构(log structured)的——这个思想既可以用在存储设备里(本章),也可以用在设备之上的文件系统里(第 43 章 LFS)。写逻辑块 N 时,设备把它追加(append)到当前正在写的块的下一个空闲页;为了之后能读 N,设备维护一张映射表(mapping table),记录每个逻辑块所在的物理地址。

原书示例:SSD 有若干 16KB 的块,每块 4 个 4KB 的页;客户端依次执行 Write(100)=a1、Write(101)=a2、Write(2000)=b1、Write(2001)=b2。一开始所有页都是 INVALID,所以 FTL 决定把第一个写放到物理块 0 时,必须先擦除块 0;随后按顺序编程页 0、1、2、3,并记下 100→0, 101→1, 2000→2, 2001→3。读逻辑块 100 时,查表得到物理页 0,直接读即可。

和直接映射相比:擦除只是偶尔发生,昂贵的读-改-写完全消失;FTL 还可以把写分散到所有页上(磨损均衡)。下面的模拟器可以逐步重现原书的每一张状态图。

模拟器

FTL 模拟器:直接映射 vs 日志结构(仿 ssd.py)

输入请求序列(逻辑页),逐步观察每个请求被翻译成哪些 erase / program / read。绿色 = 存活的 VALID 页;红色删除线 = 已是垃圾(VALID 但映射表不再指向它);虚线框 = ERASED;灰色 = INVALID。每个块下面的橙色条是擦除次数(磨损)。
请求序列:w逻辑页:内容(内容可省略)、r逻辑页 读、t逻辑页 trim、gc块号 手动回收某块
(ssd.py 的默认值)
i INVALID E ERASED V 存活 V 垃圾(旧版本/已 trim) 本步写入/读取 本步读出的源页 当前追加块

断电后内存里的映射表就没了,这对"持久"存储设备是不可接受的。最简单的办法是在每个页旁边的带外区域(OOB,out-of-band)记录一些映射信息(比如"这一页存的是逻辑块 100")。重启时扫描所有 OOB 区重建映射表——但大容量 SSD 全盘扫描很慢,高端设备会用更复杂的日志和检查点(checkpoint)技术加速恢复(参见第 42 章崩溃一致性、第 43 章 LFS)。

日志结构并非没有代价。原书指出两个缺点:① 覆盖写产生垃圾,需要垃圾回收,过多的 GC 会推高写放大、降低性能;② 内存中的映射表很大,设备越大需要的内存越多。下面两节分别讨论。

垃圾回收(Garbage Collection)

接着原书的例子:逻辑块 100 和 101 再次被写入,内容为 c1、c2。它们被写到下一个空闲页(物理页 4、5)——在此之前设备必须先擦除块 1;映射表更新为 100→4, 101→5。现在问题来了:物理页 0 和 1 虽然状态仍是 VALID,里面却是 100、101 的旧版本——垃圾(garbage)。

找出垃圾块(也叫死块,dead block)并回收的过程叫垃圾回收。基本流程很简单:

  1. 找到一个含有垃圾页的块(例如块 0:页 0、1 是死的,页 2、3 存活);
  2. 读出其中的存活页(页 2、3);
  3. 把存活页写到日志末尾(页 6、7);
  4. 擦除这个块,供以后写入。

GC 怎么知道一页是死是活?每个块要在某处记下"每一页存的是哪个逻辑块"(类似 LFS 的段摘要)。GC 查映射表:块 0 存的是 100、101、2000、2001,映射表是 100→4, 101→5, 2000→2, 2001→3——页 2、3 仍被映射表指着,是活的;页 0、1 没有被指着,可以回收。

可见 GC 可能很昂贵:要读、要重写存活数据。最理想的回收对象是只含死页的块——直接擦除就能复用,无需任何数据搬迁。

硬盘的接口基本只有 read 和 write(外加缓存刷新)。对于日志结构的 SSD(以及任何维护"逻辑→物理"灵活映射的设备),多一个 trim 操作很有用:它告诉设备某个地址(范围)上的块已被删除,设备不必再维护它的映射,GC 时可以直接把对应物理页当垃圾回收,而不用傻傻地搬迁。对硬盘来说 trim 几乎没用,因为它的地址映射是静态的。这是"实现反过来塑造接口"的好例子。在上面的模拟器里试试 t逻辑页。

为了降低 GC 代价,一些 SSD 会预留空间(overprovision):多装一些闪存容量,这样清理就可以推迟、放到后台、在设备空闲时做;更多容量也带来更多内部带宽用于清理,不影响客户端感知到的带宽。本章的 C 实验会定量展示预留比例与写放大的关系。

映射表的大小

日志结构的第二个代价是映射表可能非常大:每个 4KB 页一个表项。原书的例子:1TB 的 SSD,每 4KB 页一个 4 字节表项,光映射表就要 1GB 内存!所以纯页级 FTL 不现实。

计算器

映射表要多大?

改容量、页大小、块大小,比较页级映射与块级映射的内存开销。

块级映射(block-based mapping)

一种思路是每个块只存一个指针,映射信息缩小为原来的 Sizeblock⁄Sizepage。这类似于虚拟内存里用更大的页:VPN 位数更少、偏移量更长。逻辑地址被切成"块号(chunk number)+ 偏移(offset)"两部分:每块 4 页时偏移占 2 位,其余高位是块号。

原书例子:逻辑块 2000~2003(内容 a、b、c、d)位于物理块 1(物理页 4~7)。页级映射要 4 个表项 2000→4, 2001→5, 2002→6, 2003→7;块级映射只需一个 500→4(2000~2003 的块号都是 500,偏移 0~3)。读 2002:块号 500 → 查表得 4 → 加偏移 2 → 物理页 6,读出 c。

计算器

块级映射的地址翻译

映射表只有一项 500 → 4(每块 4 页)。输入逻辑块地址,看 FTL 如何拆出块号和偏移。

但块级映射对小写(small write,小于一个物理块)很不友好。客户端写 2002(新内容 c')时,FTL 必须读出 2000、2001、2003,再把四个逻辑块一起写到新位置(物理块 2,页 8~11),更新表项为 500→8,然后块 1 可以擦除复用。一次 4KB 的写引起了 3 次读和 4 次编程——物理块往往是 256KB 甚至更大,这种写放大太严重了。

混合映射(hybrid mapping)

为了兼顾灵活写入和较小的映射开销,许多现代 FTL 使用混合映射:

关键是让日志块数量保持很少。FTL 要定期检查日志块,把它们"转换"成只需一个块指针就能表示的块。根据块中内容不同,有三种合并方式 [KK+02]:

模拟器

混合映射的三种合并:switch / partial / full

逐步观察日志表和数据表如何变化,以及每种合并需要多少额外的读、编程、擦除。

页映射 + 缓存

另一种更简单的办法:仍然用页级映射,但只把映射表的活跃部分缓存在内存中 [GY+09]。如果负载只访问一小部分页,它们的映射都在内存里,性能好且内存开销小;如果内存装不下工作集,每次访问都要先多一次闪存读把映射取进来;更糟的是,腾位置时若被淘汰的映射是脏的(还没持久化),还要额外写一次。好在大多数负载具有局部性——这和 TLB、页缓存的道理完全一样。

磨损均衡(Wear Leveling)

多次擦除/编程会磨损闪存块,因此 FTL 要尽量把这些工作均匀分摊到设备的所有块上,让它们差不多同时"寿终正寝",而不是几个"热门"块很快报废。

日志结构本身已经能较好地分散写负载,垃圾回收也有帮助。但有一种情况它们都无能为力:某个块装满了长期不被覆盖的冷数据——GC 永远不会回收它(里面没有垃圾),它也就得不到公平的写负载份额。解决办法:FTL 必须定期把这类块中的存活数据全部读出、重写到别处,让这个块重新可写。这种迁移增加了写放大、降低了性能——为了让所有块磨损速度一致,只能付出额外 I/O。

没有磨损均衡:冷数据块从不被擦除 静态磨损均衡:搬走冷数据 热块:快被写坏 冷数据块:擦除 0 次 各块擦除次数接近 → 一起老化 柱高 = 块的擦除次数(示意)。右边付出的代价:搬迁冷数据带来的额外读和编程(写放大上升)
图 8:磨损均衡的目标是"整体寿命最长",而不是"写放大最低"。C 实验的 cold90 负载给出了真实数字。

SSD 的性能与成本

SSD 没有机械部件,在"随机访问"这一点上更像 DRAM。和硬盘相比差别最大的是随机读写:硬盘每秒只能做几百次随机 I/O,SSD 则好得多。原书 Figure 44.4 给出了三款 SSD 和一款顶级硬盘的数据:

设备随机读 MB/s随机写 MB/s顺序读 MB/s顺序写 MB/s
Samsung 840 Pro SSD103287421384
Seagate 600 SSD84252424374
Intel SSD 335 SSD39222344354
Seagate Savvio 15K.3 HDD22223223
随机读 随机写 顺序读 顺序写
图 9:SSD vs HDD(MB/s,原书 Figure 44.4)。随机 I/O 差距巨大,顺序 I/O 差距小得多。

从表中可以读出几点:

  1. 随机 I/O 差距最惊人:SSD 随机读写有几十到几百 MB/s,这块"高性能"硬盘只有约 2 MB/s(还是向上取整的)。
  2. 顺序性能差距小得多:如果只需要顺序性能,硬盘依然是不错的选择。
  3. SSD 的随机读反而不如随机写:原因正是日志结构设计——它把随机写变成了顺序写。
  4. SSD 的顺序与随机 I/O 之间仍有差距,所以前面为硬盘设计文件系统的许多技巧(如 FFS 的局部性)对 SSD 仍然适用。

成本

既然 SSD 这么快,为什么没有完全取代硬盘?答案是每单位容量的成本。原书写作时([A15]):250GB 的 SSD 约 150 美元,即每 GB 60 美分;1TB 的硬盘约 50 美元,即每 GB 5 美分——相差 10 倍以上。所以:看重性能(尤其随机读)选 SSD;要存海量数据的数据中心会倾向于硬盘;也可以混合使用——少量 SSD 存"热"数据,大量硬盘存"冷"数据。只要价格差距存在,硬盘就不会消失。

先预测再验证

在 FTL 模拟器里点"原书:日志 FTL + GC",停在第 5 个请求 w100:c1 之前。预测:c1 会被写到哪个物理页?这一步之前需要擦除哪个块?执行后映射表是什么?
看答案
块 0 已写满(页 0~3),所以 FTL 需要一个新块:块 1 还处于 INVALID 状态,必须先擦除块 1,然后把 c1 编程到物理页 4。映射表变成 100→4, 101→1, 2000→2, 2001→3,物理页 0(a1)变成垃圾。再写 c2 后页 5 = c2,页 1 也变成垃圾,此时页 6、7 为 E,块 2 仍为 i——与原书图一致。
继续:执行 gc0 回收块 0 之前,预测 GC 需要多少次闪存读、编程和擦除?回收后映射表和块 0 的状态是什么?
看答案
块 0 中页 2(2000:b1)、页 3(2001:b2)存活,页 0、1 是垃圾。GC 读 2 次、编程 2 次(写到页 6、7)、擦除 1 次。之后映射表为 100→4, 101→5, 2000→6, 2001→7,块 0 四页全为 E,块 1 为 c1 c2 b1 b2——正是原书 44.8 节最后那张图。
(改编自 Homework 第 4 题)用 ssd.py 的默认时间(擦除 1000µs、编程 40µs、读 10µs),估算原书示例全部 7 个请求(含 gc0)的总耗时,再看模拟器的统计核对。
看答案
擦除 3 次(块 0 初次、块 1、GC 时块 0)= 3000µs;编程 4 + 2 + 2 = 8 次 = 320µs;读 2 次(GC 迁移)= 20µs;合计 3340µs。擦除占了绝大部分时间。写放大 = 8 次编程 ÷ 6 次主机写 ≈ 1.33。
(改编自 Homework 第 5 题)点"直接映射:反复覆盖":请求是 w0 w1 w2 w3 然后对逻辑页 0 覆盖写 4 次。先预测直接映射下擦除、编程、读各多少次,写放大多少;再点"同样请求 → 日志 FTL"对比。
看答案
直接映射:第一个 w0 要擦除块 0(1 次擦除、1 次编程),w1~w3 目标页已擦除,直接编程(3 次)。之后每次覆盖 w0 都要读 3 页、擦除 1 次、编程 4 页,4 次共读 12、擦 4、编 16。合计擦除 5、编程 20、读 12,写放大 20/8 = 2.5(块越大越糟)。日志 FTL:8 个写依次追加到块 0、块 1,只擦除 2 次、编程 8 次、读 0 次,写放大 1.0。模拟器下方的对比表会同时列出两种 FTL 和"理想 SSD"的数字。
(改编自 Homework 第 6、7 题)点"随机负载 + 自动 GC",然后取消勾选"自动 GC"再点"应用"。预测会发生什么?再把高水位 G 设为 5、低水位 g 设为 4 并重新勾选,结果如何?
看答案
关闭 GC 后,日志不断追加。一开始 FTL 偶尔还能碰到"只剩垃圾页"的块直接擦除复用,但很快每个块都至少含一个存活页,找不到能擦除的块,后面的写入失败(红色"没有可用的块";种子 1 下有 7 个请求失败)——对应 homework 里"不开 GC 跑大负载"。开启 GC 后,每当使用中的块数 ≥ G,就挑存活页最少的块回收,直到 ≤ g,写入一直成功;代价是出现 GC 读和 GC 编程(种子 1 下共迁移 12 页),写放大 > 1。但对比表里直接映射要擦除 35 次,日志 FTL 只要 16 次。
用"映射表要多大?"计算器:2TB 的 SSD、8KB 页、4 字节表项,页级映射表多大?改用 1MB 块的块级映射呢?
看答案
页级:241 / 213 = 228 项 × 4B = 1GB。块级:241 / 220 = 221 项 × 4B = 8MB,缩小了 128 倍(= 每块页数),但代价是小写要读-改-写整块。

动手实验:C 语言 FTL 模拟器

在 labs/44-ssd/ 里有一个几百行的 C 程序 ftlsim.c,实现了三种 FTL:direct(直接映射)、log(页映射日志结构 + 贪心 GC,预留 10% 空间)、log+wl(再加静态磨损均衡)。闪存为 256 块 × 64 页;先把逻辑空间顺序写满一遍,再测 20 万次写。四种负载:顺序覆盖、均匀随机、80/20 热点、cold90(只改写 10% 的逻辑页,其余是冷数据)。

cd labs/44-ssd && make && ./ftlsim -op

示例输出(模拟是确定性的,你的机器上应得到相同数字):

workload  FTL         host      prog   erase      read     WA      wear  time(s)
seq       direct    200000  12800000  200000  12600000  64.00   768/832  10830.0
seq       log       200000    200000    3125         0   1.00      0/14    159.4
seq       log+wl    200000    200000    3125         0   1.00      0/14    159.4
random    direct    200000  12800000  200000  12600000  64.00   708/882  10830.0
random    log       200000   1010167   15785    810167   5.05     56/69    845.5
random    log+wl    200000   1010167   15785    810167   5.05     56/69    845.5
hot80/20  direct    200000  12800000  200000  12600000  64.00  158/3310  10830.0
hot80/20  log       200000   1052361   16444    852361   5.26     47/91    881.2
hot80/20  log+wl    200000   1049956   16407    849956   5.25     53/83    879.2
cold90    direct    200000  12800000  200000  12600000  64.00    0/7978  10830.0
cold90    log       200000    246984    3861     46984   1.23      0/84    199.2
cold90    log+wl    200000    722492   11290    522492   3.61     37/68    601.9

预留空间比例 vs 写放大(log FTL,均匀随机写)
      OP       WA    erase
      5%     9.82    30683
      7%     7.11    22211
     10%     5.05    15785
     15%     3.44    10747
     20%     2.63     8230
     28%     1.95     6108
     40%     1.45     4546
     50%     1.24     3868

怎么读这些数字:

自测题

1. NAND 闪存中"擦除"操作的单位是什么?
擦除以块为单位(128KB–2MB),把块内所有比特置 1;读和编程以页为单位。"要写一页必须先擦整块"正是闪存最大的怪癖。
2. 某个页已经是 VALID 状态,想把它的内容改掉,闪存芯片要求怎么做?
编程只能把 1 变成 0,已编程的页不能再次编程;唯一改变其内容的方法是擦除整个块。擦除会销毁同块其他页,所以要先搬走有用数据。
3. 直接映射(direct mapped)FTL 的写放大大致与什么成正比?
每次写都要读出整块、擦除、再把整块页重新编程——主机写 1 页,闪存写 P 页(P = 每块页数)。原书:"severe write amplification (proportional to the number of pages in a block)"。
4. 日志结构 FTL 中,逻辑块 100 被覆盖写后,它原来所在的物理页处于什么情况?
原书:"physical pages 0 and 1, although marked VALID, have garbage in them"。单独一页无法被擦除,必须等 GC 处理整个块。
5. 1TB 的 SSD、4KB 页、每页一个 4 字节表项,页级映射表需要多少内存?
240 / 212 = 228 个页,每个 4 字节 = 230 字节 = 1GB——这就是纯页级 FTL 不实用的原因。
6. 原书 Figure 44.4 中,SSD 的随机写带宽反而高于随机读,主要原因是?
编程本身比读慢一个数量级,所以原因不在芯片;日志结构设计把随机写转换成顺序写,提高了性能。
7. 对 GC 来说,最理想的回收对象是?
全是死页的块可以直接擦除复用,无需搬迁任何数据,GC 代价最低。全是存活页的块回收了也腾不出空间。
8. trim 操作对日志结构 SSD 的主要意义是?
trim 只是"通知",不强制立即擦除。对静态映射的硬盘几乎没用;对维护灵活映射的 SSD 很有用。
9. 混合映射中,partial merge 相比 switch merge 的额外代价来自哪里?
switch merge 时日志块已完整,只需改一个块指针;partial merge 只覆盖了部分页,必须把其余页(如 1002、1003)读出并写入日志块,增加写放大。
10. 关于磨损均衡(wear leveling),下列说法正确的是?
冷数据块里没有垃圾,GC 永远不会选它,它就分不到擦写负载;FTL 必须定期把其中数据搬走——原书明确说这会增加写放大、降低性能。

常见误区与小结

与后续章节的联系:第 45 章"数据完整性"会讨论设备可能以各种方式悄悄出错(包括闪存的比特翻转),以及校验和如何检测它们;而本章的日志结构、映射与垃圾回收思想和第 43 章 LFS 一脉相承——同一个想法,既可以放在文件系统里,也可以放在设备固件里。