
我打算从存储系统架构师的角度谈谈如何用好 SSD。本文中的 " 存储系统 ",指的是全闪阵列,或者全闪分布式块。
以前我做 zStorage 的时候,遇到一个问题:性能测试时间长了,写入 SSD 的数据量大了,写性能就下降。当时我猜测,多半是 SSD 后台启动了 GC(垃圾收集),导致了写性能下降。没有证据,也就只能是猜测。从去年开始,我花了很多时间学习 SSD,现在我可以肯定地说,就是 SSD 的后台 GC 影响了写性能。
本文不打算详细讲解 " 为什么 SSD 需要做 GC",也不讲 "SSD 怎么做 GC",这些内容网上有很多,不了解的读者自行百度或 Deepseek,推荐 Deepseek。本文重点讲那些以前我不了解的、但对存储系统有影响的 SSD 特性。
SSD 的写比读性能差,随机写比顺序写差很多。现在采用 PCIe Gen5 x4 的 NVMe SSD,主机接口带宽可以达到 15.7GB/s,顺序读可以达到 14.5GB/s,要去掉一些命令开销。详见下表:

上表可以看出,4KB 随机写的 PCIe 带宽利用率只能跑到 15%。很明显,PCIe 接口不是瓶颈,那瓶颈在什么地方?
实际上,SSD 的写性能瓶颈在 NAND Flash 上。现在企业 SSD 中最常用的 TLC Nand Flash,一般要求一次性写入同一个 WL 的 3 个页,如果每个页 8KB,那么 3 个页就是 24KB。写入这 3 个 8KB 页需要花大约 700us。也就是说,一个 Flash 芯片的写性能只能达到 24KB / 700us = 35MB /s,如果要达到 10GB/s 性能,一个 SSD 需要配置大约 300 个 Nand Flash 芯片。看网上的 SSD 照片,似乎没有那么多 Flash 芯片?
实际上,一般单个 Flash 芯片内部会封装很多个独立的 Die(切割好的硅片),每个 Die 内部还有多个 Plane,多个 Plane 可以一起执行写命令。也就是说,实际上不是 300 个芯片,而是 300 个 Plane。按照每个 die 容量 1Tb 计算,15.36TB 的 SSD 大约需要配置 140 个 die,每个 Die 里面有 2 个 Plane,顺序写性能大约就是 10GB/s。为什么读性能那么高?因为读一个 Flash 页只需要 40us。那么,又为什么随机写性能(带宽利用率 14%)比顺序写性能(70%)低那么多?
随机写性能低的主要原因是 GC 导致的写放大(WAF)很高。如果是顺序写,那么 Nand Flash 上数据也是被顺序覆盖掉的,一个 GC 回收单元里面所有 Flash 数据都被一起覆盖掉了,虽然还有 GC 流程,但是一个 GC 回收单元里面的所有页面都被覆盖掉了,没有有效数据页了,也就不需要拷贝数据了,所以没有写放大(WAF = nand_write_bytes / host_write_bytes = 1.0)。如果是 4KB 随机写,一个 GC 回收单元内部还有很多有效数据页,这些数据需要被复制到 Nand Flash 的新位置,才能删除 GC 回收单元。这个复制过程,会引入写放大。如果用 fio 做 4kb 随机写测试,有些型号的 SSD 在极限情况下写放大(WAF)可以达到 4.5 甚至 5.0,也就是 nand flash 里面写入 5TB 数据,只有 1TB 是主机写入 SSD 盘的,另外写入的 4TB 数据是 GC 复制流程产生的。
例如,上表中 SSD 顺序写性能 10GB/s,4KB 随机写性能 52 万 IOPS,那么这个型号的 SSD 在 4KB 随机写极限情况下写放大就是 WAF = 10*1E9 / ( 52 * 1E4 * 4096 ) = 4.7。GC 导致的写放大不仅仅造成随机写性能低,还会影响 SSD 的寿命。因为导致 Nand Flash 芯片磨损的主要因素是擦写循环的次数,也就是往 Nand Flash 写入的数据量。
存储系统是否有办法降低写放大?一种容易想到的办法是:把对 SSD 的随机写,全部改成顺序写。对这个想法,我的观点是:没必要为了减小 SSD 写放大这么做。如果你的系统本来就是追加写(Append-Only)的设计,那么对 SSD 自然就是顺序写,例如:RocksDB 或者 ZFS 这种追加写的系统,不需要专门去做什么设计。如果存储系统是原地写(Update-In-place)的设计,改成追加写确实可以减小 SSD 内部的写放大,但是不能减小整个系统端到端的写放大,也就是写入 Nand Flash 数据量并没有减少。因为改成对 SSD 追加写,存储系统就要增加 GC 流程,也就是把写放大从 SSD 内部搬到了存储系统软件中。从更上层应用角度看,整体写放大并没有减少。
上面做法也有另外一个好处,就是存储系统可以决定什么时候启动 GC 流程。如果你知道什么时候业务压力低,就在业务压力低的时候启动 GC 流程,这样可以把 GC 对性能的负面影响降低到最小。SSD 内部 GC 流程,总是倾向于尽量晚一点启动,因为晚一点启动 GC 流程,就会有更多的数据页被覆盖,这样需要复制的有效页就少一点,写放大也就会小一点。在我看来,这个好处并不够大,不值得为了这个把存储系统改为追加写。
另一个需要注意的问题是:如果你决定改为追加写模式,那么一定要做彻底一点,追加写数据块的长度要尽量大。
一般来说,现在企业 SSD 内部都会在 Flash 芯片之间做 RAID,这样一旦发生 LDPC 无法纠正的错误(例如 die 失效),就可以通过 RAID 来恢复数据。企业 SSD 的 GC 回收单元是 Super Block,构成一个 RAID 组所有 die 上对应的 Block 共同构成一个 Super Block,这个 Super Block 中有一个 Block 是用来存放 parity 的,其他 Block 用来存放用户数据。一个 Super Block 会被 GC 流程整体回收擦除。RAID 导致了 GC 回收单元的尺寸相当大,例如物理 Flash 芯片的 Block 大小是 64MB,如果构成一个 RAID 的 Super Block 中有 300 个 Block,Super Block 的大小 64MB * 300 = 19 GB。如果存储系统改为追加写之后,存储系统使用 8MB 的回收单元做 GC,对于 19GB 大小 GC 回收单元的 SSD 来说,这个上层存储系统依然是随机写,不是顺序写。SSD 做 GC 的时候,一个 19GB 大小的回收单元中,会有很多 8MB 的空洞,也会有很多 8MB 的有效数据。SSD 的 GC 依然需要做数据复制,无法起到减小 SSD 写放大的作用。
除了顺序写可以降低 WAF 之外,TRIM 也能起到显著降低 WAF 的作用。当 GC 选中一个 Super Block 进行回收的时候,如果这个 Super Block 中有很多数据页实际上是无效的,但是还没有被新数据覆盖,也没有 TRIM 命令把它删除掉,那么 GC 流程就必须把它当作有效数据页进行复制。如果有 TRIM 命令把这个页面标记为无效,GC 流程就不需要复制它了,就减小了写放大。Linux 的 ext4 文件系统的 mount 命令有一个参数 -o discard,mount 的时候加了这个参数就会给 SSD 下发 TRIM 命令。
除了顺序写和 TRIM 之外,增加 OP 空间(Over Provisioning)页能减小 WAF。因为 OP 空间大了,GC 流程就可以晚一些启动,SSD 就可以先处理更多的写和 TRIM 命令,被 GC 选中回收的 super block 中的有效页面就更少,需要复制的页面更少,WAF 就会更小。
还有一个能降低写放大的方法是 FDP。存储系统先预测哪些数据寿命短,哪些数据寿命长,然后通过 FDP 把预测的数据寿命告诉 SSD。SSD 在写入数据的时候,把寿命相同的数据放到一个 Super Block 中,这样当某个 Super Block 被选中回收的时候,里面需要复制的有效数据页就非常少,WAF 就会比较小。
很多企业级 SSD 内部的 FTL 粒度是 4KB 的,也就是一个 FTL 表项指向一个 4KB 的 Nand Flash 物理地址。如果存储系统下发的写操作里面有很多小于 4KB 的写请求,例如 512Bytes 的,那么 SSD 就需要先把 4KB 数据读出来,修改 512B,然后再把 4KB 写回去。这也会影响性能,另外也造成了另外一种写放大。但如果是 512B 的连续写,例如写 WAL 日志,这是不影响的。因为企业 SSD 内部有备电电容,主机写到 SSD 的 512B 数据,会被暂时放到内存中,凑齐 4KB 之后再写 Flash。也就是说,存储系统应该尽量避免不足 4KB 的随机写。另外,现在市面上有些超大容量(30TB、60TB、…)QLC SSD 的 FTL 粒度不是 4KB,可能是 16KB 或者更大的,对它们要避免不足 16KB 的随机写。
有些存储系统会在数据写入 SSD 之后,立刻读出做个校验,以确保数据被正确写入了。对于 SSD 来说,这个做法很不好。因为如果立刻读出 Nand Flash 刚写入的数据,此时 Nand 处于不稳定状态,误码率会比较高,这可能会影响性能,也可能导致 SSD 内部读电压调整算法的误动作。存储系统应该尽量避免写入数据之后立刻读。
SSD 是否会产生坏道?是的,不过 SSD 产生坏道的原因跟 HDD 不一样。现在的 TLC/QLC Nand Flash 介质的误码率是非常高的,有些甚至可以达到 10E-4,也就是一个 4KB 数据页写入 Nand Flash 之后,再读出来的时候,几乎必然有几个 bits 是错的。这些错误就是靠 LDPC 算法和 RAID 算法来恢复的,但是 SSD 并不能保证数据一定能被恢复。遇到无法恢复的情况,就只能把这个 LBA 地址的数据标记为坏道。跟 HDD 的坏道不一样的是,这个坏道 LBA 地址再重写一遍,状态就恢复了,就不再是坏道了。
我搞 zStorage 的时候,曾经被一个问题困扰:如果读 SSD 的时候遇到了坏道,是否意味着这个 SSD 里面还有很多坏道?是否需要对这个 SSD 做一次全盘扫描?是否需要把这个 SSD 从系统中踢掉?实际上,NVMe 标准提供了一个命令 LBA status,这个命令可以把这个 SSD 的所有坏道都返回给主机,主机只需要对这些坏道数据做恢复并重写就可以了,不需要对整个 SSD 做全盘扫描。因为全盘扫描相当耗费时间,并且影响性能。SSD 自己也会在启动内部任务在后台做扫描,并且把无法恢复的数据块标记出来。当然,存储系统每隔一段时间做一次全盘扫描,也是有必要的,毕竟自己才是最可靠的。如果充分利用 LBA status 这个命令,这个全盘扫描的时间间隔可以长一点。
还有一点是,工作温度对 SSD 的影响很大,温度升高的时候,Nand Flash 的误码率会升高,这会影响 SSD 的性能,因为需要花更多时间来恢复数据。另外,工作温度对 SSD 的寿命也有负面影响,高温会加速 Flash Cell 的隧穿氧化层老化。
我能想到的,就这么多了。
本文不代表任何组织。文章内容也许有错误,欢迎批评指正。


