关于 SSD,每一位开发者都应该知道的一些知识
基于闪存的 Solid-State Drives (SSDs) 固态硬盘 已经在很大程度上取代了磁盘,成为标准的存储介质。从程序员的角度来看,固态硬盘和磁盘看起来非常相似:两者都是持久性的,通过文件系统和系统调用实现基于页面(如 4KB)的访问,并且具有大容量。
然而,也有一些重要的区别,如果想达到最佳的 SSD 性能,这些区别就变得很重要。周所周知,SSD 更加复杂,如果只是简单地认为它们是快速的磁盘,那么它们的性能表现会显得相当难以理解。这篇文章的目的是让大家了解为什么 SSD 会有这样的性能表现,这有助于构建能够充分利用它们的软件。请注意,我讨论的是 NAND 闪存,而不是英特尔 Optane 内存,后者具有不同的特性)。
驱动器(Drives)不是磁盘(Disks)
固态硬盘经常被称为磁盘,但这是一种误导,因为它们在半导体上存储数据,而不是在机械磁盘上。要从一个随机块中读取或写入,磁盘必须机械地将其磁头移动到正确的位置,这需要 10ms 的时间。相比之下,从固态硬盘中进行随机读取,需要大约 100us–快 100 倍。这种低读取延迟就是为什么系统从固态硬盘启动要比从磁盘启动快得多的原因。
并行性
磁盘和固态硬盘的另一个重要区别是,磁盘有一个磁头,只在顺序访问时表现良好。相比之下,固态硬盘由几十个甚至几百个闪存芯片(“并行单元”)组成,可以同时访问。
固态硬盘透明地将较大的文件以页为单位在闪存芯片上存储,而硬件预取器则确保顺序扫描能够利用所有可用的闪存芯片。然而,在闪存层面上,顺序读取和随机读取之间没有太大的区别。事实上,对于大多数固态硬盘来说,随机读取页面也有可能实现利用几乎全部的带宽。要做到这一点,必须同时安排数百个随机 IO 请求,以保持所有闪存芯片的工作。这可以通过启动大量线程或使用异步 IO 接口(如 libaio 或 io_uring)来实现。
写入
写入时,事情变得更加有趣,例如,如果我们测试下写入延迟,我们可以测量到低至10us的结果–比读快 10 倍。然而,延迟看起来如此之低,是因为固态硬盘在易失性 RAM 上进行了写入缓存。NAND 闪存的实际写入延迟约为 1ms - 比读取慢 10 倍。在消费级固态硬盘上,这可以通过在写完后发出 sync/flush 来衡量,以确保数据在闪存上的持久性。在大多数数据中心/服务器固态硬盘上,写延迟无法直接测量:sync/flush 将立即完成,因为即使在断电的情况下备用电池依旧可以保证写缓存的持久性。
为了在相对较高的写入延迟下实现高的写入带宽,写使用了与读相同的技巧:他们同时访问多个闪存芯片。因为写缓存可以异步写入页面,所以甚至没有必要同时安排那么多的写入来获得良好的写入性能。然而,写入的延迟不可能总是被完全掩盖:例如,由于写入占用闪存芯片的时间是读的10倍,写入会导致对同一闪存芯片的读取有明显的尾部延迟。
Out-Of-Place 写入
我们对 SSD 的理解忽略了一个重要的事实:NAND 闪存的页面不能被覆盖。页的写入只能在事先已被擦除的块内按顺序进行。这些擦除块的大小为多 MB,由数百个页面组成。在一个新的固态硬盘上,所有的块都已被擦除,人们可以直接开始写入新的数据。
然而,更新页面就不那么容易了。如果只是为了就地覆盖一个页面而擦除整个区块,那就太浪费了。因此,固态硬盘通过将新版本的页面写到一个新的位置来执行页面更新。这意味着逻辑和物理页面地址是解耦的。存储在 SSD 上的映射表将逻辑(软件)地址转换为物理(闪存)位置。这个组件也被称为 Flash Translation Layer(FTL)。
例如,让我们假设我们有一个 SSD,有 3 个擦除块,每个有 4 个页面。对 P1, P2, P0, P3, P5, P1 页的一连串写入可能会导致以下物理 SSD 状态:
Block 0 | P1 (old) | P2 | P0 | P3 |
Block 1 | P5 | P1 | → | |
Block 2 |
这时,我们已经没有空闲的擦除块了(尽管从逻辑上来说,应该还有空间)。在写入另一个页面之前,SSD 首先要擦除一个块。在这个例子中,对于垃圾回收器来说,最好是擦除第 0 块,因为只有其中的一个页面还在使用。在擦除第0块后,我们为 3 个写页腾出了空间:
Block 0 | P1 (old) | P2 (old) | P0 | P3 (old) |
Block 1 | P5 | P1 (old) | P3 | P4 |
Block 2 | P7 | P1 | P6 | P2 |
写入放大与超额配置
为了垃圾回收 block 0,我们不得不物理地移动 P0 页,尽管在逻辑上该页没有发生什么。换句话说,对于闪存 SSD 来说,物理(闪存)写入的数量通常高于逻辑(软件)写入的数量。两者之间的比例被称为写入放大(WA)。在我们的例子中,为了给 0 区块的 3 个新页面腾出空间,我们不得不移动 1 个页面。因此,我们有 4 个物理写,3 个逻辑写,也就是说,写放大率为 1.33。
高写入放大率会降低性能,并减少闪存的使用寿命。写入放大率有多大,取决于访问模式和固态硬盘的满度。大的连续写入有低的写入放大率,而随机写入是最糟糕的情况。
让我们假设我们的 SSD 被填充到 50%,我们进行随机写入。在稳定状态下,无论我们在哪里擦除一个区块,该区块的大约一半的页面仍然在使用,并且平均要被复制。因此,50% 的填充系数的写入放大率是 2。一般来说,填充系数 f 的最坏情况下的写入放大率是 1/(1-f)。
f | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 0.95 | 0.99 |
WA | 1.11 | 1.25 | 1.43 | 1.67 | 2.00 | 2.50 | 3.33 | 5 | 10 | 20 | 100 |
因为在填充系数接近 1 的情况下,写入放大率会变得不合理地高,大多数固态硬盘都有隐藏的备用容量。这种超额配置通常为总容量的 10-20%。当然,通过创建一个空的分区并永远不向其写入,也很容易增加更多的超额配置。
总结
目前,固态硬盘已经变得相当便宜,而且它们具有非常高的性能。例如,三星 PM1733 服务器固态硬盘每 TB 价格约为 200 EUR ,并承诺提供接近 7GB/s 的读取和 4GB/s 的写入带宽。实际上,要实现如此高的性能,需要了解 SSD 的工作原理,这篇文章就是描述了闪存 SSD 最重要的底层机制。
我尽量让这篇文章简短,要想了解更多,这篇教程 是一个不错的起点。最后,由于 SSD 已经变得如此之快,操作系统的 I/O 堆栈往往成为性能瓶颈。Linux 的实验结果可以在这篇的 CIDR 2020论文 中找到。