try ai
科普
编辑
分享
反馈
  • 写放大

写放大

SciencePedia玻尔百科
核心要点
  • 写放大源于 NAND 闪存中的粒度不匹配问题,由于“先擦除后写入”的限制,小的逻辑更新需要重写大的物理块。
  • 写放大的主要来源是垃圾回收,这是一个后台进程,SSD 在该进程中复制有效数据以回收空间。其严重程度可通过公式 WA=1/(1−u)WA = 1/(1-u)WA=1/(1−u) 量化。
  • 写放大是一个系统级问题,受硬件设计(预留空间)、操作系统(I/O 缓冲、日志记录)、文件系统(写时复制)乃至抽象算法设计的影响。
  • 最小化写放大至关重要,因为它通过降低 I/O 延迟直接提升系统性能,并通过减少闪存单元的磨损显著延长 SSD 的寿命。

引言

在现代计算领域,固态硬盘 (SSD) 以其闪电般的数据访问速度彻底改变了性能。然而,在这种速度之下隐藏着一种代价,即一种被称为写放大的现象,它会悄无声息地降低性能并缩短驱动器的寿命。本文旨在探讨这个至关重要但又常常被误解的概念,解释为何一个简单的数据写入请求会导致驱动器在内部执行多出数倍的工作量。通过理解写放大,我们可以构建更快、更可靠、更持久的系统。

我们的旅程始于第一章“原理与机制”,在这一章中,我们将剖析写放大的根本原因:NAND 闪存奇特的物理特性。我们将探讨垃圾回收的需求如何导致额外的写入,并推导出支配它的那个简单而强大的公式。随后,第二章“应用与跨学科联系”将拓宽我们的视野,揭示写放大并非仅仅是一个硬件怪癖,而是一个系统级的属性。我们将看到,在操作系统、文件系统甚至抽象算法中所做的选择,都可能极大地影响这一关键性能指标,从而展示现代计算堆栈深层次的相互关联性。

原理与机制

要理解写放大,我们必须首先领会一个工程学中的普遍原理:​​粒度不匹配​​问题。从物流到计算机科学,它无处不在。想象一下,你想寄一封信。你不会为这一个信封就雇佣一辆巨大的货运卡车;那将是荒谬的低效。你会使用邮政服务,它将你的信与成千上万封其他信件组合在一起。反之亦然。如果一个系统是为运输货运卡车而建,而你坚持一次只寄一封信,那么你将为每封信支付运输一整辆卡车的代价。

这种现象同样发生在计算机中央处理器 (CPU) 的深层。当 CPU 需要向内存写入一小块数据时——比如一个字节——它通常不会只发送那一个字节。内存系统为批量传输而优化,它以称为​​缓存行​​的固定大小块进行通信,长度可能为 646464 或 128128128 字节。如果 CPU 写入一个字节,硬件通常会通过内存互连发送整个 LLL 字节的缓存行。总线上移动的字节数与 CPU 写入的有用字节数之比,就是一种形式的写放大。在这个简单案例中,对于一个 bbb 字节的写入,放大因子 AAA 为 A=LbA = \frac{L}{b}A=bL​。如果你在一个拥有 64 字节缓存行的系统上写入一个字节,你的写放大就是 64!

工程师们已经开发出一些巧妙的技巧,如​​写合并缓冲区​​,它可以收集多个前往同一缓存行的小写入,并将它们合并为一次总线事务。这是我们的第一条线索:写放大源于粒度不匹配,而减轻它需要对数据进行智能管理。这个故事,在一个更宏大、更戏剧化的尺度上,正是固态硬盘 (SSD) 的故事。

闪存的奇特规则

与之前的磁性硬盘不同,SSD 基于 NAND 闪存,这是一种拥有一套奇特而迷人规则的技术。可以把一个闪存驱动器想象成一种特殊的笔记本。

  1. 你可以将数据写入称为​​页​​的整洁小块中(通常为 4 到 16 KB)。这就像在笔记本的一行上写一个句子。
  2. 你可以随时读取任何你喜欢的页。
  3. 关键在于:​​你不能擦除单个页​​。要擦除任何东西,你必须擦除一个称为​​擦除块​​的巨大块,它可能包含数百个页。这就像一条规则说你不能用橡皮擦掉一个单词;你必须撕掉那个单词所在的整个章节。

这种巨大粒度下的“先擦除后写入”限制改变了一切。你不能简单地在同一物理位置用新数据覆盖旧数据。这样做需要擦除整个块,这不仅速度慢,而且会销毁该块中所有其他完好的数据。

解决方案既巧妙又简单:​​异地更新​​。当你想修改一个页时,SSD 的控制器不会动旧数据。相反,它会将新版本的页写入一个全新的、干净的位置,并更新内部的地址簿——​​闪存转换层 (FTL)​​——使其指向这个新位置。旧的页则被简单地标记为“无效”或“陈旧”。

整洁的代价:垃圾回收

这个策略在一段时间内工作得很好。SSD 会愉快地将新数据和更新写入空闲页,留下一串陈旧数据。但驱动器的空间是有限的。迟早,它会充斥着有效数据和陈旧无用数据的混合体,没有大片干净的页可供写入。

为了解决这个问题,SSD 必须执行一项称为​​垃圾回收 (GC)​​ 的内务整理工作。这个过程在概念上非常简单:

  1. FTL 选择一个块进行清理(一个“待回收块”),最好是含有大量陈旧数据的块。
  2. 它从该待回收块中读取所有仍然有效的页。
  3. 它将这些有效的页写入一个新的、已经擦除的块中。
  4. 最后,在所有有效数据被安全地重新安置后,它可以对整个待回收块发出擦除命令,将其转变为一个可用于新写入的、纯净的空闲资源。

在这里,在第 3 步,就隐藏着写放大的主要来源。SSD 被迫进行主机从未请求过的“内部写入”。主机请求写入一页新数据,但为了给它腾出空间,SSD 可能需要复制十页旧的、有效的数据。这就是写放大。

我们可以很漂亮地量化这一点。让我们将一个块的​​利用率​​ uuu 定义为其仍然有效的页所占的比例。如果一个块有 100 个页,其中 20 个是有效的,那么它的利用率是 u=0.2u=0.2u=0.2。当 GC 清理这个块时,它必须执行 20 个页的复制(GC 写入),以回收 80 个页的陈旧空间,并使整个 100 页的块变为空闲。

为了给主机释放每一个单位的空间,GC 必须执行 u/(1−u)u/(1-u)u/(1−u) 次写入。总写入量是主机写入加上这些 GC 写入。这就导出了一个强大而基础的写放大 (WAWAWA) 公式:

WA=主机写入+GC 写入主机写入=11−uWA = \frac{\text{主机写入} + \text{GC 写入}}{\text{主机写入}} = \frac{1}{1-u}WA=主机写入主机写入+GC 写入​=1−u1​

这个简单的方程告诉了我们一切。如果 SSD 能找到几乎为空 (u→0u \to 0u→0) 的块进行清理,写放大就会接近其理想最小值 1。这意味着主机每写入一个字节,SSD 也只写入一个字节。但如果 SSD 被迫清理几乎充满有效数据 (u→1u \to 1u→1) 的块,写放大就会飙升至无穷大。在很大程度上,设计高性能 SSD 的整个博弈,就是最小化被选来进行垃圾回收的块的利用率 uuu 的博弈。

一个系统级的侦探故事:谁在控制放大?

写放大不仅仅是一个底层的硬件怪癖。它是一个系统级的属性,受到从应用程序行为到操作系统策略和 SSD 控制器设计等方方面面的影响。

随机性的混乱

想象一个在一个大文件上到处写入小的、随机数据块的工作负载。从 SSD 的角度来看,这是一场噩梦。这些随机写入分散在许多不同的擦除块中。这会将不同生命周期的数据“混合”在一起,使得在任何给定时间,任何单个块都充满陈旧数据的可能性在统计上变得很低。因此,垃圾回收器只能找到利用率 uuu 很高的块,导致效率低下和高写放大。

这时,操作系统 (OS) 既可以扮演英雄,也可以扮演恶棍。当应用程序写入数据时,操作系统可以使用​​缓冲 I/O​​,在内存的页缓存中收集小的写入。然后,它可以智能地对这些写入进行排序和合并,向 SSD 发送一个更大、更顺序的数据流。这有助于将逻辑上相关的数据在物理上存放在驱动器的一起,增加了它们在相近时间都变为陈旧数据的机会。这会产生可供 GC 使用的低利用率块,从而降低 WAWAWA。

相反,如果应用程序使用​​直接 I/O​​(如 Linux 中的 [O_DIRECT](/sciencepedia/feynman/keyword/o_direct) 标志),它会绕过操作系统缓存,直接将其小的、随机的写入发送给 SSD。这将原始、混乱的访问模式暴露给驱动器,使其失去了任何智能分组的机会,从而导致显著更高的写放大。

喘息空间:预留空间的馈赠

SSD 制造商有一个简单而有效的技巧来对抗写放大:​​预留空间 (OP)​​。他们制造的驱动器所含的物理闪存比他们宣传的要多。一个“1 TB”的驱动器实际上可能包含 1.2 TB 的物理闪存。这个额外的、不可见的空间给了 FTL “周转的余地”。它允许驱动器在被逼到墙角不得不运行垃圾回收之前,吸收更多的写入并积累更多的陈旧数据。有了更大的块池可供选择,FTL 就有更好的机会找到一个利用率 uuu 非常低的待回收块。

这种关系可以被精确建模。写放大既取决于作为更新的写入比例(一个工作负载属性,uuu),也取决于预留空间比率(ρ\rhoρ)。更高的 ρ\rhoρ 给予驱动器更多的自由度,并直接降低最终的写放大。

操作的大脑:闪存转换层

FTL 是 SSD 的板载智能核心。它的设计对写放大有深远的影响。最关键的设计选择之一是其映射表的粒度——这个地址簿将逻辑主机地址转换为物理闪存位置。

  • ​​页级映射​​ FTL 为每个页维护一个条目。这提供了最大的灵活性。要更新一个逻辑页,FTL 可以将新数据写入整个驱动器上的任何空闲物理页。这种细粒度控制非常适合处理随机写工作负载和最小化写放大。缺点呢?映射表会变得非常巨大。对于一个拥有 4 KiB 页的 1 TB 驱动器,这个表可能需要数 GB 的 RAM,这很昂贵。

  • ​​块级映射​​ FTL 通过一次映射整个页块来简化这个问题。这极大地减小了映射表的大小。然而,对于小的随机写入,它会带来灾难性的性能代价。要更新一个逻辑块内的仅仅一个页,FTL 被迫将整个旧块读入其内部内存,修改那一个页,然后将整个更新后的块写入一个新的物理位置。这导致对于每一个小写入,都会产生一个等于块中页数的写放大因子——可能是 128 或 256!

这展示了一个经典的工程权衡:内存开销与性能灵活性。大多数现代高性能 SSD 使用混合方法,但这种根本性的矛盾依然存在。

预知未来:分离热数据和冷数据

最复杂的控制器更进了一步。它们试图预测未来。数据并非都一样;有些数据是“热”的(临时的,可能很快被删除或更改),而另一些数据是“冷”的(归档的,可能会长时间保留)。

如果 FTL 能够区分它们,它就可以执行一种非常强大的优化:​​数据着色​​。它将所有预测为热的数据放在相同的擦除块中。它将所有预测为冷的数据放在其他块中。结果会怎样?“热”块充满了很快就会变陈旧的数据。这些块自然演变成垃圾回收的完美候选者,其利用率 uuu 极低。“冷”块则被搁置一旁,充满了很少被触及的有效数据。

通过优先清理热块,SSD 可以实现大幅降低的平均写放大。这是一个巨大的胜利。当然,这种预测并非没有风险。如果预测错误,或者工作负载突然改变,系统可能会措手不及,被迫清理一个冷块,导致延迟突然大幅飙升。

当层级冲突:抽象的危险

有时,两个好主意结合起来可能会产生一个坏结果。考虑在一个 SSD 上运行​​日志结构文件系统 (LFS)​​——一种本身就使用异地更新的文件系统。LFS 有自己的“清理器”进程,这本质上是软件层面的垃圾回收。它从碎片化的“段”中读取有效数据,并将其写入新的、干净的段中,从而向底层设备生成一个写入流。

SSD 对此毫不知情,它看到的是来自 LFS 清理器的这些写入。这些数据,从 LFS 的角度来看,按定义是长寿命的,但对 SSD 来说,却像是一股新的写入流。SSD 控制器将这些数据写入自己的块中。现在,你就有了两个层次的垃圾回收在相互作用。放大因子会相乘:Atotal=ALFS×ASSDA_{\mathrm{total}} = A_{\mathrm{LFS}} \times A_{\mathrm{SSD}}Atotal​=ALFS​×ASSD​。一个看似无害、写放大为 5 的 LFS 运行在一个写放大为 5 的 SSD 上,可能导致总的、灾难性的系统放大达到 25。这是一个深刻的教训,说明系统中的抽象层如何可能产生意想不到的破坏性相互作用。

底线:我们为何追求因子为一

在经历了现代存储复杂机制的这段旅程之后,有人可能会问:这一切到底为什么重要?为什么对一个抽象的比率如此执着?答案有两部分,都至关重要。

首先是​​性能​​。写放大不仅仅是一个内部记账指标;它对应用程序延迟有直接、切实的影响。每一次额外的物理写入都会使驱动器的内部资源保持繁忙。当应用程序发出一个写入请求时,它可能不仅要等待设备服务其请求,还要等待设备完成一些正在进行的垃圾回收工作。这种增加的延迟表现为 CPU 执行中的间隙,减慢了整个系统。更低的 WA 意味着一个更敏捷、响应更快的系统。

其次,也许更深刻的是​​耐久性​​。闪存单元并非永生。在耗尽之前,它只能承受有限次数的编程/擦除周期——对于消费级 SSD 来说,通常是几千次。每一次写入,无论是来自主机还是来自内部垃圾回收器,都会消耗掉这些宝贵的周期之一。

这种关系简单得残酷。设 CCC 为驱动器的物理容量, EEE 为其闪存单元的耐久性(以周期计)。能够物理写入到闪存中的总数据量是 C×EC \times EC×E。因此,主机在驱动器生命周期内可以写入的总数据量,即其​​写入的太字节数 (TBW)​​ 额定值,受到写放大的限制:

TBW=C×EWATBW = \frac{C \times E}{WA}TBW=WAC×E​

这个方程 是写放大的最终、严酷的真相。它是你驱动器寿命的一个反向乘数。将你的写放大减半,实际上就将你的 SSD 耐久性翻倍。追求降低这一个数字,就是追求设备不仅更快,而且拥有更长、更可靠的生命。这是一个完美的例子,说明了理解深刻、基本的原理如何让我们能够构建更好、更高效的系统。

应用与跨学科联系

既然我们已经探索了闪存芯片奇妙的内部世界,包括它的页、块以及由闪存转换层管理的持续整理工作,你可能会倾向于认为写放大纯粹是一个硬件层面的问题,一个设备怪癖,应由设备工程师来解决。但这样做就只见树木,不见森林了。真相远比这有趣得多。写放大不仅仅是硬件问题;它是一个系统性现象,一个在计算机系统每一层都产生回响的涌现属性。它是一种税,不仅由硅片征收,还由操作系统、文件系统,甚至我们用来组织数据的算法本身征收。要真正理解它,就需要踏上一段穿越整个宏伟、互联的现代计算堆栈的旅程。

操作系统的“写入税”

让我们从操作系统 (OS) 内部开始我们的旅程,它是管理计算机资源的总操纵师。操作系统在性能、可靠性和功能之间不断地做出权衡。这些决策中,有许多是出于好意,却创造了它们自己隐藏的“写入税”。

想象一下,你正在写一份重要文件,突然断电了。你肯定希望重启后,你的文件系统不会变成一团乱码。为防止此类灾难,许多文件系统采用一种称为​​日志记录​​的技术。在对主文件结构进行任何更改之前,操作系统首先将预期更改的描述写入一个特殊的日志或日记中。这就像飞行员在起飞前提交飞行计划一样。首先,写入日志;然后,进行实际更改。这确保了如果在操作中途发生崩溃,系统可以在重启时读取日志,并完成或撤销该操作,从而恢复到一致的状态。

但看看发生了什么!为了更改一条元数据,系统现在执行了两次写入:一次写入日志,一次写入最终位置。这是文件系统自身为可靠性而创建的一种写放大形式,而且这发生在请求到达 SSD 自己的 FTL 之前。然后,硬件放大再将这个软件层面的重复放大。

操作系统在产生写入的习惯上可能更加微妙。考虑文件上的“最后访问时间”或 atime。每次你仅仅读取一个文件,一些文件系统就觉得有必要通过更新文件的元数据来记录这一事件。仔细想一想:一个读操作触发了一个写操作!对于一个每秒处理数千次文件读取的繁忙服务器来说,这会产生一场微小元数据写入的风暴,每一次写入都在加剧底层 SSD 的磨损。难怪注重性能的系统管理员经常禁用此功能,用 noatime 选项挂载他们的文件系统。这直接承认了这个看似无害的功能会带来真实的物理成本。

也许操作系统写入税最戏剧性的例子来自其内存管理。你的计算机拥有有限的快速物理内存 (RAM)。当你运行太多程序时,操作系统会使用你的部分存储驱动器作为“交换空间”——内存的溢出区。当操作系统需要释放 RAM 时,它可能会取一个最近未使用的内存“页”,并将其写出到交换空间。如果该页已被修改(我们称之为“脏”页),它就必须被写入磁盘。

现在,将此与我们的 SSD联系起来。每次一个脏页被换出,都是对交换文件的一次逻辑写入,然后 FTL 会将其放大。在一个内存压力很大的系统中,操作系统可能会开始花费几乎所有时间疯狂地换入换出页面,这是一种称为​​颠簸​​的灾难性状态。在这种状态下,计算机运行缓慢如牛,不是因为 CPU 在忙于有用的工作,而是因为它在不断地等待 I/O 系统。交换设备上的写放大于此火上浇油。它增加了写出每个脏页所需的时间,使 I/O 瓶颈更加严重,并将系统更深地推入颠簸的死亡螺旋。一个聪明的操作系统甚至可能变得“闪存感知”,设计其页面置换策略以优先换出干净页(不需要写回)而非脏页,以此来减轻 SSD 的负担。这是软件适应底层硬件物理特性的一个绝佳例子。

放大的层级:文件系统和 RAID

从核心操作系统向上看,我们发现存储系统本身的结构就会引入更多的放大层。例如,现代文件系统经常使用一种称为​​写时复制 (CoW)​​ 的技术。CoW 文件系统不是原地覆盖数据,而是将修改后的数据写入新位置,然后更新指针以指向这个新位置。这提供了许多出色的功能,比如能够对文件系统进行即时“快照”。

然而,它有一个隐藏的成本。磁盘上的数据通常以称为区段的大型连续块进行管理。如果你的应用程序对一个文件做了一个微小的、1 字节的更改,而这个字节恰好位于一个数兆字节的大区段中间,一个 CoW 文件系统可能被迫将整个旧区段复制到一个新位置,并包含你那 1 字节的更改。这里的放大因子可能非常巨大,代表了用户请求的逻辑更改与文件系统执行的物理 I/O 之间的巨大差异。

这种乘法效应在大型存储系统中变得更加明显。考虑一个 ​​RAID 5​​ 阵列,这是一种组合多个驱动器以防止单个驱动器故障的常用方法。它通过在多个驱动器上条带化数据并存储奇偶校验信息来工作。当你对一个 RAID 5 阵列执行一次小写入时,控制器不能只写入新数据。它必须读取旧数据,读取旧奇偶校验,计算新奇偶校验,然后写入新数据和新奇偶校验。这个“读-修改-写”序列意味着,对于来自主机的每一次逻辑写入,系统都会向驱动器执行两次物理写入。这就是 RAID 级别的写放大,通常称为“写惩罚”。

当你用 SSD 构建一个 RAID 5 阵列时,你会得到一个复合的灾难。主机发出一次写入。RAID 控制器将其变为两次写入。然后,每个 SSD 上的 FTL 会接收其传入的写入,并因垃圾回收而进一步放大它。总放大是 RAID 级别因子和 FTL 级别因子的乘积。这完美地说明了来自系统不同独立层的开销如何相乘以产生一个惊人的巨大总体效应。唯一的出路是理解整个系统,例如通过增加 SSD 的预留空间来给 FTL 更多的周转空间以吸收放大的工作负载。

同样的原则也适用于云端。虚拟化中的一个常见做法是让数十个虚拟机 (VM) 共享一个单一的、只读的基础操作系统镜像。然后每个 VM 将其更改写入一个独立的、个人的写时复制增量磁盘。所有这些增量磁盘可能都存在于一个复杂的日志结构文件系统 (LFS) 上,该文件系统本身具有与 SSD 的 FTL 在精神上相似的垃圾回收例程。结果是一个令人眼花缭乱的写放大效应堆栈:COW 层增加了元数据开销,而 LFS 清理过程由于需要重新复制来自许多生命周期不相关的不同 VM 的活动数据而放大了写入。分析这样的系统是一堂大师课,让你看到单个组件如何相互作用以产生复杂的、系统范围的行为。

算法的基石

到目前为止,我们已经看到放大源于硬件物理、操作系统策略和文件系统架构。但这个兔子洞还要更深。写放大被编织进了我们用来组织信息的数据结构和算法的本质之中。

想一想数据库。其核心通常是一个像 ​​B-tree​​ 这样的数据结构,这是一种为基于磁盘的存储而优化的特殊搜索树。数据存储在节点中,节点对应于磁盘上的页。当你从 B-tree 中删除一个条目时,你可能会导致一个节点“下溢”——即其条目数少于所需的最小数量。为了解决这个问题,算法可能会将下溢的节点与其兄弟节点合并。这个合并操作是一个单一的逻辑事件,但在物理上,它需要将至少两个节点(合并后的节点及其父节点)重写到磁盘。由于每个节点都是一个完整的页,一个微小的键删除可能会在树中级联,导致多次昂贵的整页重写。算法本身的逻辑就产生了写放大。

这段旅程可能看起来有点令人沮丧,好像我们系统的每一层都在合谋更快地耗尽我们的驱动器。但它以一个令人惊讶且充满深刻之美的故事结尾。这就是​​缓存无关算法​​的故事。

这些是理论家们设计的算法,旨在在具有内存层次结构(例如,缓存和 RAM)的计算机上达到最佳效率,但有一个神奇的特点:算法不知道层次结构的参数,如缓存大小或块传输大小。它是“无关的”。其中最著名的此类算法之一是 mergesort 的递归版本。

现在,考虑一下当你在真实的 SSD 上运行这个对物理世界一无所知的纯理论算法时会发生什么。Mergesort 通过反复将已排序的数据段合并成更长的已排序数据段来工作。它产生的写入,从本质上讲,是长的、优美的、顺序的数据流。而一个 SSD 上的日志结构 FTL 最擅长处理什么样的工作负载呢?长的、顺序的数据流!当顺序写入空闲空间时,FTL 可以简单地一个接一个地填满擦除块,写放大因子接近理想值 111。

这是一个了不起的发现。一个在抽象数学世界中设计的、对页、擦除块或闪存一无所知的算法,结果却几乎完美地适应了硬件的物理现实。其内在结构无需刻意就已经是“闪存友好的”了。它不需要为 SSD 进行“优化”,因为其固有的优雅已经使其与设备的操作和谐一致。

这就是写放大的终极教训。它不是某个需要修复的孤立 bug。它是一条线,将最抽象的算法理论层面与最具体的固态物理层面连接起来。它告诉我们,要构建真正高效的系统,我们不能孤立地思考各个层次。我们必须将计算机视为一个整体,一个由相互作用的部分组成的交响乐,其中算法的选择可能会产生一直延伸到硅芯片内电子之舞的后果。在理解这些深层联系的过程中,我们不仅能获得更好的性能,还能更深刻地欣赏计算固有的美和统一性。