垃圾回收与预留空间(Over-Provisioning)技术探讨
从「无效页占着块却不能被擦除」出发,讲透回收开销的真正来源、预留空间如何把稳态写放大压下来,以及主机侧在这两件事上究竟能做什么 —— 一份给存储方案设计者与调试工程师的技术参考 | 编制日期 2026-10-14 | 版本号 Rev 1.0
几乎所有用过固态存储的人都遇到过同一个现象:盘刚买来很快,用一段时间就变慢,写入延迟开始抖,寿命掉得比预期快。解释往往停留在「主控在做垃圾回收」「要留点预留空间」这种一句话层面,但这句话背后的机制其实没几个人真正拆开过:回收到底在花什么时间?预留空间到底换来了什么?为什么留得越多写放大越低?
这份文档只围绕两件事展开:垃圾回收(Garbage Collection)与预留空间(Over-Provisioning)。它们是同一枚硬币的两面——回收解决「无效页占着块却不能被擦除」的问题,预留空间决定回收时有多少回旋余地。两者有一个共同的分母:写放大。把这三者的关系讲透,前面那些现象就都能推导出来,而不是靠猜。
本文不讨论具体型号的固件实现,也不做任何厂商对比。所有比例、阈值、倍数都只给行业经验区间,正式设计一律以 datasheet 与实测为准。
结论: 垃圾回收的开销不在擦除本身,而在搬迁有效页;预留空间不是浪费容量,而是用一部分标称容量去换更低的稳态写放大与更稳的性能。 二者的定量桥梁是同一个式子:稳态下回收贡献的写放大大致正比于「1 + 有效数据占比的倒数」这一类关系,而有效数据占比又由物理容量与用户容量之比决定——预留空间越大,有效数据占比越低,块被回收时需要搬走的有效页越少,写放大越低。 主机侧能做的只有三件事且作用有限:及时发送丢弃通知、做好分区对齐与容量规划、让写入尽量顺序化。不要高估主机侧的作用,也不要低估「把容量留够」这一件事的杠杆率。
读者路径
- 想搞懂回收为什么非做不可:第 1 章(写擦粒度不一致与无效页积累)+ 第 2 章(一次回收的完整生命周期)。
- 想知道预留空间该留多少:第 3 章(定义与口径)+ 第 4 章(量化关系与边际收益)+ 第 9 章(代价与决策框架)。
- 在调性能抖动 / 写延迟长尾:第 5 章(触发与调度)+ 第 10 章(诊断)+ 附录 C(排查手册)。
- 在做固件或算法选型:第 6 章(牺牲块选择)+ 第 7 章(写放大拆解)。
- 在做主机侧驱动或系统配置:第 8 章(主机能做什么)+ 第 11 章(可观测性)。
- 只想要速查:第 12 章(十条结论与速查表)+ 附录 B(参数速查)。
1. 垃圾回收为什么必须存在
这一章不谈任何算法,只回答一个前置问题:为什么存储器件不能像内存一样原地改写,非要多出一个「垃圾回收」的动作。答案不在软件里,而在 NAND 的物理结构里——写入的最小单位和擦除的最小单位不是同一个东西。后面所有关于预留空间、写放大、性能抖动的讨论,都是从这一条不对称推出来的。
1.1 写入单位与擦除单位不一致
NAND 闪存有三层组织粒度,它们在物理上是包含关系,在操作上却各管一摊。理解这三层,是理解后面所有内容的前提:
- 页(Page):读与写的最小单位。主机下一次读写请求(哪怕只有几百字节),落到 NAND 上也是以「页」为单位完成的一次操作。
- 块(Block):擦除的最小单位。一个块由几十到几百个页组成,具体数量随工艺代际变化,只能查器件手册。
- 擦除组 / 平面(Plane / Die 层面的并行单元):多通道并行与交错操作的组织单位,决定了回收能不能并行做,第 5.5 节会展开。
关键的不对称在于:写可以按页进行,擦除却只能按块进行。这个不对称不是固件设计得不好,而是物理决定的——擦除动作是对整个块的衬底施加一次高压,把块内所有浮栅晶体管的电荷一起放掉,没有「只擦其中一部分」这种操作模式。
| 组织粒度 | 是哪种操作的最小单位 | 典型规模(量级) | 谁能看见它 | 对回收的意义 |
|---|---|---|---|---|
| 页(Page) | 读 / 写 | 几 KB 到十几 KB | 主机与 FTL 都能看见 | 映射表的索引粒度,决定小写放大 |
| 块(Block) | 擦除 | 几十到几百个页 | 只有 FTL 能看见 | 回收的动作对象,决定搬迁开销 |
| 擦除组 / 平面 | 并行与交错 | 多个块 | FTL 与主控调度器 | 决定一次回收能不能并行展开 |
| Die / 通道 | 并发与流水 | 多个平面 | 主控硬件调度 | 决定回收吞吐的上限 |
这张表里最值得记住的一行是第二行。块是擦除的最小单位,意味着任何一次「腾出空间」的动作,都必须以块为颗粒度进行。而主机看到的却是页——这两者之间的落差,全部由 FTL 用一层间接映射来吸收,代价就是回收。
1.2 为什么不能原地更新
既然擦除只能整块,那能不能「把要改的那一页所在的块读出来,擦掉,再把数据连同改动一起写回去」?理论上可以,实际上没人这么做,原因有四条:
- 数据会被一起擦掉:擦除是块级的,块里其余的页在此之前必须已经搬到别处,否则数据直接丢失。也就是说「原地更新」这个动作本身就要求先把整块搬走,等于已经做了一次回收。
- 擦除耗时是写操作的量级倍数:一次块擦除的时间通常是页编程时间的数倍到数十倍(具体倍数查手册,不同代际差异很大)。把它放在每次写请求的路径上,延迟不可接受。
- 擦除有次数上限:每个块的 P/E 次数有限。如果每次改一个字节都要擦一次块,块的寿命会在极短时间内耗尽——这比写放大的问题严重得多。
- 掉电窗口不可接受:读出、擦除、写回这一串动作中间断电,数据处于不可恢复状态。原地更新会让掉电保护的设计复杂到无法落地。
因此所有 NAND 方案的 FTL 都采用同一个策略:异地更新(out-of-place update)。新数据写到另一个已经擦好的页上,映射表把逻辑地址指向新页,旧页原地不动地被标记为「无效」。
异地更新把「擦除」这个昂贵动作从写路径上挪走了,代价是制造了新的垃圾:每次主机改写一个逻辑块,NAND 上就多出一个无效页。这些无效页不能单独消失,只能等它们所在的整个块被擦除时才会消失。于是「制造垃圾」和「清理垃圾」成了两个独立的动作,后者就是垃圾回收。
换句话说:回收不是 FTL 的一个可选优化,而是异地更新这个策略必然带来的配套机制。只要采用异地更新,就一定要有回收;不存在「不用回收的 NAND FTL」。
1.3 无效页的积累
一个块里的每个页,在任意时刻都处于三种状态之一。把这三种状态分清楚,回收的全部逻辑就清楚了:
| 页状态 | 含义 | 还能被读吗 | 还能被写吗 | 什么时候转换 |
|---|---|---|---|---|
| 有效页(Valid) | 映射表当前指向它,主机读这个逻辑地址会读到这里 | 能 | 不能(需先擦除) | 被改写后转为无效 |
| 无效页(Invalid / Stale) | 曾存过数据,但映射表已不再指向它 | 不能直接读 | 不能(需先擦除) | 只能随整块擦除消失 |
| 空闲页(Free / Erased) | 已擦除,可直接写入 | 读出来是全 1 | 能 | 写入后转为有效 |
无效页是这里最特殊的一类:它占用物理空间,却不承载任何有效数据。从主机的视角看这块容量应该还空着,从 NAND 的视角看这个位置已经被占了,谁也用不了,除非把整个块擦掉。
随着主机不断改写数据,无效页会持续增长。如果什么都不做,最终所有块都会变成「有效页 + 无效页」的混合状态,没有任何一个块处于可写状态,盘就写不动了——即使主机侧显示还剩一半容量。这个现象就是「幽灵占用」:主机看到的剩余容量和 NAND 实际能承接的写入量之间出现了偏差。
无效页的积累速度取决于负载的改写密度:
- 纯顺序写满一次:几乎不产生无效页,块写满后一直保持全有效。
- 随机改写热点数据:每次改写都产生一个无效页,积累速度最快。
- 删除文件:如果没有丢弃通知,FTL 并不知道这些页已经没用了,它们仍然是「有效页」,仍然会被搬来搬去——这一点第 8.1 节会展开。
1.4 垃圾回收的定义与目标
把前面的因果链收拢,垃圾回收的定义就很直接了:
垃圾回收(Garbage Collection,GC)是这样一个动作序列——挑选一个含有无效页的块,把其中仍然有效的页搬迁到别处的空闲块,更新映射表,然后把整个块擦除,使其回到可写状态。
这个动作序列同时要达成四个目标,而它们之间是互相拉扯的:
| 目标 | 要什么 | 与谁冲突 | 冲突时的常见取舍 |
|---|---|---|---|
| 回收可用块 | 尽快把块变回可写状态,别让主机写不动 | 与「少搬迁」冲突 | 保证最低空闲块水位优先 |
| 控制写放大 | 每次回收搬走的有效页越少越好 | 与「尽快回收」冲突 | 优先挑无效页比例最高的块 |
| 磨损均衡 | 擦除次数要在块之间摊开 | 与「挑最脏的块」冲突 | 在候选集里加磨损项做折中 |
| 不影响前台 | 回收不要在主机写路径上发生 | 与「尽快回收」冲突 | 后台空闲时提前回收 |
四个目标里,「控制写放大」是本文的主线。因为写放大同时决定了两件事:器件的寿命消耗速度,以及回收动作的频繁程度。而写放大的大小,主要由两个因素决定——回收时块内还剩多少有效页(这由负载决定),以及盘上总共有多少富余空间(这就是预留空间)。
1.5 本文主线
这份文档从头到尾只讲三件事,其余内容都是这三件事的展开或验证:
- 回收的开销不是擦除本身,而是搬迁有效页。擦除一次块的时间是固定的、可预测的;而搬多少页是不确定的,取决于块里还剩多少有效数据。所有关于「为什么变慢」的解释,最后都要回到「搬了多少页」这个量上。
- 预留空间不是浪费容量,而是把稳态写放大压下来的代价交换。多留出来的那部分物理容量降低了整盘的有效数据占比,使得任意一个块被回收时,需要搬走的有效页更少。这是一笔明确的交易:用标称容量换写放大与性能稳定性。
- 主机侧对这两件事都不是无能为力,但能做的事很有限。真正有效的只有三件:及时发送丢弃通知、把分区对齐与容量规划做对、让写入尽量顺序化。不要高估主机侧的作用——它无法改变块的物理粒度,也无法替代预留空间。
第一,本文出现的所有数字都是量级与经验区间,包括预留比例、写放大倍数、水位阈值、搬迁量。它们用来建立直觉和做量级判断,不能当作任何一颗料的规格,正式设计一律以 datasheet 与实测为准。
第二,本文的模型是单颗粒、单控制器的理想模型。多通道并行、多芯片交错、带独立协处理器的主控会带来额外的调度维度,但不改变定义与因果链——只是把「一个块」换成「一组块」,把「一次擦除」换成「一批擦除」。公式依然适用,输入参数要按实际组织粒度重新取值。
图 2 把这个积累过程画成了一个四步的示意。注意第 ④ 步:主机侧认为只用了 4 页,NAND 侧却被占住了 8 页。差的这 4 页就是无效页。当所有块都进入这种状态时,即使主机侧看到的剩余容量还很充裕,盘也写不动了——这就是为什么「容量用满」和「性能塌方」往往同时出现,第 10 章会专门讲这个现象。
2. 垃圾回收的基本流程
这一章把一次完整的回收动作拆成五个步骤讲清楚:什么时候触发、挑哪个块、搬哪些页、什么时候擦、擦完要更新什么。这五步里的每一步都有可选项,不同的固件实现在这五步上的取舍不同,表现出来的性能与寿命特征也不同。理解了这五步,第 5 章的调度策略与第 6 章的选择算法就只是这五步的具体参数化。
2.1 触发条件:空闲块水位
回收不是定时做的,也不是每次写入都做,而是由空闲块的数量触发。FTL 维护一个「已擦除、可直接写入」的块池,当池子里的块数低于某个阈值时,就开始回收。
为什么用「空闲块数」而不是「空闲页数」做判据?因为写入的原子性要求是块级的:一次写入需要一个完整的空闲页,而回收需要一个完整的空闲块来承接搬迁。用页数做水位会在「页数够、块数不够」的情况下误判——页分散在很多块里,但没有一个块是完整空闲的,此时既写不进新数据也做不了回收。
| 水分区间(经验) | 盘的状态 | 该做什么 | 不做会怎样 |
|---|---|---|---|
| 充足(高于高水位) | 回收毫无压力,写入走最短路径 | 后台可以完全不回收,省电 | 无(这是理想状态) |
| 中等(高、低水位之间) | 有余量但不多 | 后台低速回收,维持水位 | 持续下降会进入下一档 |
| 偏低(接近低水位) | 空闲块不多,前台写入开始被拖 | 后台提高回收强度 | 前台写入开始等待回收完成 |
| 告急(低于低水位) | 几乎没有空闲块 | 前台回收,写入与回收串行 | 写延迟显著上升,出现卡顿 |
| 枯竭(无空闲块) | 完全写不动 | 必须先回收才能继续写 | 写入阻塞,表现为掉速或超时 |
阈值本身是个经验值,不同固件差得很多,常见做法是设成「总块数的百分之几」这个量级,并且高、低两个水位之间留出滞回区间,避免在阈值附近反复启停。具体数值没有通用答案,只能实测调整——第 5.2 节会讲怎么定。
2.2 选择牺牲块
触发之后第一步是挑一个被回收的块,行业里一般叫牺牲块(victim block)。最朴素的想法是「挑无效页最多的那个」,因为它需要搬走的有效页最少,写放大最低。这个想法在多数情况下是对的,但它有两个盲区:
- 不考虑磨损:某个块可能一直是「最脏」的那个,反复被挑中,擦除次数远超其他块,寿命提前耗尽。
- 不考虑数据冷热:如果块里剩下的有效页是冷数据,搬走之后它大概率不会再被改写,这是好事;如果搬的是热数据,搬完马上又变脏,等于白搬一次。
这两个盲区分别对应第 6.4 节的磨损均衡协同和第 6.3 节的冷热分离。在基本流程这一章,先把选择动作本身的输入输出讲清楚:
| 选择动作的要素 | 输入 | 输出 | 常见实现 |
|---|---|---|---|
| 候选集 | 所有含无效页的块 | 一个待评估的子集 | 全扫描或按脏度分桶 |
| 评估函数 | 无效页数、擦除计数、数据年龄 | 每块的收益/成本比 | 贪心或成本-收益,见第 6 章 |
| 决策 | 各块的评估值 | 一个牺牲块 | 取最大收益者,或加权折中 |
| 边界处理 | 有没有全无效的块 | 直接擦除、跳过搬迁 | 零搬迁回收,最快的一种 |
2.3 搬迁有效页
这是整个回收过程中开销最大的一步,也是本文反复强调的那句话的来源:回收的成本不在擦除,而在搬迁。
搬迁的动作本身很简单:把牺牲块里每一个仍然有效的页读出来,写进目标空闲块,再把映射表里的对应项指向新位置。设一个块有 N 个页,其中有效页有 V 个,无效页有 I 个(N = V + I),那么这一次回收:
- 产生 V 次读操作:把有效页读进内部缓冲。
- 产生 V 次写操作:把读出来的数据写进新块。
- 产生 1 次擦除操作:把旧块擦掉。
- 净收益:(N - V) 个页的可用空间:也就是被释放出来的无效页数量。
用「净收益」去除「付出的写入量」,就得到这一次回收的写放大贡献:
本次回收的写放大贡献 = 搬迁写入量 / 回收出的可用空间
= V / (N - V)
= 1 / (N/V - 1)
这个式子的含义非常直白:V 越接近 N(块里几乎全是有效页),分母越小,写放大越大,趋近无穷;V 越接近 0(块里几乎全是无效页),写放大趋近 0。举两个极端情况感受一下量级:
| 块内有效页占比 V/N | 回收 1 个块腾出的空间 | 需要搬走的页数 | 本次回收的写放大贡献 | 工程评价 |
|---|---|---|---|---|
| 0.1(很脏) | 0.9 个块 | 0.1 个块 | 约 0.11 | 几乎白赚,回收效率极高 |
| 0.3 | 0.7 个块 | 0.3 个块 | 约 0.43 | 很划算 |
| 0.5 | 0.5 个块 | 0.5 个块 | 1.00 | 搬多少就腾出多少,打平 |
| 0.7 | 0.3 个块 | 0.7 个块 | 约 2.33 | 明显不划算 |
| 0.9(很干净) | 0.1 个块 | 0.9 个块 | 9.00 | 灾难,几乎是在做无用功 |
把表格从上往下看,会得到一个反直觉但极其重要的结论:回收效率完全取决于「块里还剩多少有效数据」,而不是「回收算法有多聪明」。而块里剩多少有效数据,又取决于整盘的平均有效数据占比——盘子越满,每个块的有效页占比就越高,回收就越不划算。
预留空间的全部作用,就是把整盘的平均有效数据占比压低,让块在被挑中回收时,恰好处于表格上半部分那种「很脏」的状态。这不是算法技巧,这是纯粹的容量换效率。理解了这一点,第 4 章的量化关系就是这张表的直接推论。
2.4 擦除与回收
搬迁完成后,牺牲块里所有的有效数据都已经在别处有了副本,此时可以安全地执行擦除。擦除完成,块回到空闲池,本次回收结束。
擦除这一步的特点是时间长、不可打断、期间块不可访问:
| 子步骤 | 耗时量级(相对) | 能否被打断 | 对前台的影响 | 备注 |
|---|---|---|---|---|
| 读有效页 | 与页读取同量级 | 能(可分次读) | 占用读带宽 | 搬迁的读方向 |
| 写有效页到新块 | 与页编程同量级 | 能(可分次写) | 占用写带宽与空闲块 | 搬迁的写方向 |
| 更新映射表 | 极短(内存操作) | 能 | 几乎无 | 但必须原子地持久化 |
| 擦除块 | 数倍到数十倍于页编程 | 不能 | 块在此期间不可访问 | 是延迟尖峰的来源之一 |
| 块状态回收入池 | 极短 | 能 | 无 | 擦除完成后的记账 |
注意一个容易被忽略的细节:擦除期间该块不可访问。如果此时主机恰好要读这个块里的某个页(搬迁完成前的瞬间),请求会被挂起直到搬迁完成。这就是写入延迟出现长尾的一个来源——第 10.2 节会从诊断角度再讲一遍。
另外,擦除是有次数上限的。每回收一次,牺牲块的 P/E 计数加一。回收越频繁,寿命消耗越快,这就是为什么「写放大」既是性能指标也是寿命指标——它同时出现在两个预算表里。
2.5 元数据更新
搬迁与擦除之后,有一批元数据必须同步更新,否则数据就找不到了。这一步耗时极短,但正确性要求最高——它是掉电保护的核心关注点。
| 元数据类型 | 更新时机 | 必须持久化的原因 | 写不下去会怎样 |
|---|---|---|---|
| 逻辑到物理映射表 | 每搬一个页更新一项 | 否则读回来的数据是旧的 | 数据错乱 |
| 块状态表 | 擦除完成后 | 否则空闲块池可能给出未擦除的块 | 写入失败或数据损坏 |
| 块擦除计数 | 每次擦除后 | 磨损均衡依赖它 | 磨损失衡,寿命提前耗尽 |
| 有效页计数 | 每次搬迁后 | 牺牲块选择依赖它 | 选错块,写放大恶化 |
| 坏块表 | 擦除失败时 | 否则反复使用一个坏块 | 数据丢失风险 |
元数据本身也要写在 NAND 上(掉电不能丢),而写元数据本身又会产生少量额外的写入——这一项通常很小,但在写放大的精细拆解里仍然占一格,见第 7.1 节。
2.6 一次回收的完整生命周期
把上面五步串起来,一次完整的回收就是这样一条时间线。理解这条时间线,后面讨论「前台回收还是后台回收」「要不要限速」才有共同语言。
把这条时间线用一句话概括:回收的全部工作,就是在为「腾出一个块」这个目标付出 V 次读 + V 次写 + 1 次擦除的代价,而收益只有 (N - V) 个页的空间。当 V 很大的时候,这笔交易极其不划算;而当盘上有充足的预留空间时,块往往在变得「很脏」之前就被回收了,V 自然就小。
| 阶段 | 主要动作 | 是否占用前台带宽 | 失败时的后果 | 可以优化的方向 |
|---|---|---|---|---|
| ① 触发 | 检查空闲块水位 | 否 | 不触发就没有回收 | 提前后台触发,见第 5 章 |
| ② 选择 | 评估候选块 | 否(读元数据) | 选到坏块,白做一次 | 更好的评估函数,见第 6 章 |
| ③ 搬迁 | 读旧页 + 写新页 | 是(读写都占) | 数据丢失风险 | 降低 V:预留空间、冷热分离 |
| ④ 改表 | 持久化映射与计数 | 轻微 | 映射错乱 | 批量提交、日志化 |
| ⑤ 擦除 | 整块擦除 | 是(块不可用) | 块变坏块 | 挑质量好的块,避开高温窗口 |
| ⑥ 回池 | 记账并归还 | 否 | 空闲池泄漏 | 无,纯记账 |
回收的五个步骤里,只有「搬迁」这一步的开销是可变的,其余都是固定开销。而搬迁量 V 又完全由「块里还剩多少有效数据」决定。所以所有降低回收开销的手段,归根结底都是在降低 V —— 或者让平均而言 V 更小。预留空间就是在整盘层面降低 V 的那个手段。
3. 什么是预留空间
这一章给「预留空间」下一个可计算的严格定义,并把它和几个容易混淆的概念分开:备用区、坏块替换池、厂商隐藏容量。定义清楚之后,第 4 章才谈得上量化——因为「预留 7%」这句话在不同口径下指的是两个不同的数,差了将近四分之一。
3.1 定义与口径
预留空间(Over-Provisioning,OP)最常被使用的定义是:
OP = (C_raw - C_user) / C_user
其中:
C_raw = NAND 上真实存在的可擦除容量(物理容量 / 裸容量)
C_user = 主机可以访问到的容量(用户容量 / 标称容量)
这个式子的分母是用户容量,不是物理容量。这一点必须记住,因为另一种口径同样常见:
| 口径 | 定义式 | 同一盘上的数值 | 谁在用 | 换算关系 |
|---|---|---|---|---|
| 口径一(本文默认) | OP = (C_raw - C_user) / C_user | 偏大(例:28%) | 企业级规格、性能讨论 | OP = OP_raw / (1 - OP_raw) |
| 口径二 | OP_raw = (C_raw - C_user) / C_raw | 偏小(例:22%) | 部分厂商材料、容量说明 | OP_raw = OP / (1 + OP) |
「这盘预留了 28%」和「这盘预留了 22%」说的可能是同一颗盘。评审会上如果不先对齐分母,后面的写放大估算、寿命预算会全部错位。
本文统一使用口径一(除以用户容量)。如果你手上的材料用的是口径二,先按上面的式子换算过来再代入本文的公式。
3.2 物理容量与用户容量的差
物理容量和用户容量为什么会不相等?因为中间有好几层扣减。把这些扣减逐层列出来,才知道「预留空间」到底是哪一层:
| 层次 | 从物理容量里扣掉了什么 | 主机能看见吗 | 算不算预留空间 | 备注 |
|---|---|---|---|---|
| 第一层 | 出厂坏块与替换块池 | 看不见 | 不算 | 属于可靠性冗余,不是 OP |
| 第二层 | 固件代码区、映射表区、日志区 | 看不见 | 不算 | 属于固件开销,是必需品 |
| 第三层 | ECC 校验与元数据附加区 | 看不见 | 不算 | 每个页都要带,随页大小走 |
| 第四层 | 厂商设定的固定预留(保底 OP) | 看不见 | 算 | 出厂就划定,用户改不了 |
| 第五层 | 主机未分配的空闲空间 | 看得见(显示为可用空间) | 算(等效) | 不写满就等于临时预留 |
这里有一个非常实用、也非常容易被忽略的结论:「没写满」在物理上等价于「预留了空间」。一块 100 GB 的盘,如果主机只往里写了 60 GB,剩下 40 GB 从未被写过,那些块处于已擦除状态,FTL 可以拿它们当空闲块用——效果和出厂预留 40 GB 是一样的。
反过来说:一旦盘被写满过一次,这层等效预留就永久消失了。所有块都含过数据,之后再怎么删文件(如果没有丢弃通知),FTL 也认为那些块是满的。这就是第 10 章要讲的「越用越慢」的物理根源。
3.3 预留空间的三种来源
实际能拿到的预留空间来自三个地方,它们的可控性完全不同:
| 来源 | 怎么来的 | 量级(经验) | 用户能控制吗 | 稳定性 |
|---|---|---|---|---|
| 厂商固定预留 | 出厂时把物理容量划出一部分不给用户 | 客户端到企业级不等,档次越高越多 | 不能,出厂即固定 | 永久有效 |
| 单位换算差额 | 十进制标称与二进制实际之间的差 | 约百分之几的量级 | 不能,行业惯例 | 永久有效 |
| 主机主动留出 | 分区时少分一部分,或用主机工具留出空间 | 完全由用户决定 | 能,且随时可改 | 取决于使用习惯 |
三种来源里,只有第三种是设计者能主动调整的,也是第 8.4 节「主机管理的预留空间」要讲的内容。第一种和第二种在拿到器件时就已经确定了,选型时能做的只有「选哪一档」。
需要提醒的是:单位换算差额这一项经常被人误当成预留空间全部。它确实存在,但量级只有几个百分点,对高写入负载来说远远不够。真正起决定作用的是厂商固定预留与主机主动留出这两项。
3.4 与备用区的区别
「预留空间」和「备用区(spare area / over-head)」是完全不同的两件事,混用会让容量账算错:
| 对比项 | 预留空间 OP | 备用区 / 冗余区 |
|---|---|---|
| 存在位置 | 整块的富余容量,以块为单位 | 每个页内部的附加字节,以字节为单位 |
| 用途 | 给回收搬迁用,降低写放大 | 存 ECC 校验码与元数据,保证数据正确 |
| 能不能省 | 能省,但写放大与性能会恶化 | 不能省,是数据可靠性的必需品 |
| 主机能感知 | 能,表现为标称容量变小 | 不能,完全隐藏在页结构内 |
| 与寿命的关系 | 越大越省寿命(写放大低) | 越大越可靠(纠错能力强) |
| 典型规模 | 用户容量的百分之几到几十个百分点 | 每页几十到几百字节(随页大小与工艺) |
一句话区分:备用区是「为了让数据读得对」,预留空间是「为了让盘写得动、写得久」。两者都占用物理容量,但作用机理毫不相干,不能在容量预算里互相抵扣。
还有一个容易混的概念是坏块替换池:它也是以块为单位的富余容量,但用途是替换出厂坏块与使用中出现的新坏块,属于可靠性冗余,不能拿来当回收的周转空间——用掉了就没有替换余量了。
3.5 预留空间从哪里来
把上一节的结论收拢成一张可执行的清单。做方案时按这个顺序问自己,就能知道这颗盘实际有多少回旋余地:
- 出厂固定预留是多少?查规格书或问原厂,客户端盘与企业级盘在这一项上通常差一个档次。
- 单位换算带来多少?十进制标称与二进制实际之间的差,量级几个百分点。
- 固件开销吃掉多少?映射表、日志、固件镜像都要占块,小容量盘占比更高。
- 主机打算再留多少?这是唯一能主动加的一项,也是成本最高的一项(要牺牲可用容量)。
- 负载会不会写满?写满一次,第 2 节说的等效预留就归零,前面四项里真正生效的只剩前三项。
在没有规格书可查的情况下,可以用「标称容量 × (1 + 预估 OP) 约 物理容量」反推,再用实测的稳态写放大去校验:如果实测写放大明显高于同档位盘的典型区间,通常说明实际预留比预估的少,或者负载的随机度比预估的高。这个交叉验证方法在第 11.5 节会展开。
4. 预留空间如何影响回收与写放大
这是全文的核心章节。第 2 章已经得到「一次回收的写放大贡献 = V / (N - V)」,本章把这句话从单次回收推到稳态:整盘的平均有效数据占比由预留空间决定,而稳态写放大又由平均有效数据占比决定。预留空间与写放大之间因此存在一条可以直接画出来的曲线,且这条曲线是单调下降、边际递减的。
4.1 预留越大写放大越低的原因
因果链只有四步,但每一步都必须想清楚:
- 预留空间越大 → 整盘平均有效数据占比 u 越低。设物理容量
C_raw、用户容量C_user,主机写满用户容量时,整盘有效数据占比u = C_user / C_raw = 1 / (1 + OP)。OP 越大,u 越小。 - 平均有效数据占比越低 → 任意一个块被挑中回收时,块内的有效页越少。这是统计意义上的:整盘只有 u 比例的页是有效的,那么一个随机块的期望有效页数就是
N × u。 - 块内有效页越少 → 搬迁量 V 越小。这一步是第 2.3 节的结论。
- V 越小 → 回收的写放大贡献 V / (N - V) 越低。收工。
把 1 代入 4,就得到稳态下的那条关系式(严格来说是上界,见下一节):
稳态写放大的上界: WA_max = 1 / (1 - u) = (1 + OP) / OP
OP = 0.07 -> WA_max 约 15.3
OP = 0.15 -> WA_max 约 7.7
OP = 0.28 -> WA_max 约 4.6
OP = 0.50 -> WA_max 约 3.0
这个式子假设的是最坏情况:写入完全随机、毫无局部性、且盘被写满。真实负载几乎总是有热点局部性的——被反复改写的数据集中在少数块上,那些块会变得很脏,回收它们非常划算,于是实测写放大远低于上界。
所以正确的用法是:用上界做保守设计(最坏情况能不能接受),用实测值做验收(这颗盘实际表现如何)。两者不要混用,也不要把上界当成「这颗盘的写放大就是 15」——那会导出荒谬的寿命结论。
4.2 稳态下的量化关系
把不同 OP 档位代进上式,得到下面这张表。请注意看「上界」和「典型实测区间」两列的差别——差的就是局部性带来的收益。
| OP(口径一) | 有效数据占比 u | 写放大上界 (1+OP)/OP | 典型实测区间(经验) | 适用负载形态 |
|---|---|---|---|---|
| 0%(无预留) | 1.00 | 趋于无穷 | 不可控,可能到几十 | 不可用于随机写 |
| 7% | 0.935 | 15.3 | 2 到 6 | 客户端轻负载、有局部性 |
| 15% | 0.870 | 7.7 | 1.5 到 3.5 | 客户端主流、混合负载 |
| 28% | 0.781 | 4.6 | 1.3 到 2.5 | 企业级入门、写密集 |
| 50% | 0.667 | 3.0 | 1.2 到 2.0 | 写密集、抖动敏感 |
| 100% | 0.500 | 2.0 | 1.1 到 1.5 | 极端写密集、短寿命换性能 |
这张表有两个用法:
- 正向:已知预留多少,估算最坏写放大,再估算寿命够不够。
- 反向:已知寿命目标与写入量,反推需要多少写放大,再反推需要多少预留。
反向用法在选型时更常见,第 9.2 节会给出完整的反推步骤。此处先强调一点:写放大同时决定性能与寿命,所以这张表既是性能表也是寿命表。
4.3 突发与稳态的差别
上界公式描述的是稳态——盘已经被写满、且持续被随机改写很长时间之后的状态。但实际负载往往不是稳态的,把这两种状态分开看非常重要:
| 状态 | 盘的实际情况 | 写放大表现 | 性能表现 | 持续时间 |
|---|---|---|---|---|
| 初始态 | 大量块从未写过,处于已擦除状态 | 接近 1,几乎无回收 | 最快,延迟最低 | 直到盘被写满一次 |
| 突发态 | 短时间内大量写入,空闲块快速消耗 | 暂时不高,但水位急剧下降 | 先快后慢,出现拐点 | 突发持续期间 |
| 过渡态 | 后台回收追赶不上消耗 | 逐步上升 | 抖动开始出现 | 数十秒到数分钟量级 |
| 稳态 | 所有块都含过数据,回收持续进行 | 稳定在上界与实测值之间 | 稳定但低于初始态 | 长期 |
| 劣化态 | 接近写满且无丢弃通知 | 显著高于稳态 | 明显掉速、延迟长尾 | 直到容量释放 |
这里最值得注意的现象是「初始态的假象」:新盘测出来的性能非常好,因为大量块是干净的、根本不需要回收。但这个状态不可持续——盘被写满一次之后就永远回不去了(除非做一次全盘擦除类操作)。所以:
验收之前必须先把盘「写满一次」,让它进入稳态,再测性能。行业里的稳态性能测试流程(先把盘顺序写满、再施加随机写负载一段时间、然后开始采样)就是为这个目的存在的。
如果跳过这一步,验收得到的数字会显著好于实际长期表现,投产之后才会暴露问题。这是存储选型里最常见、代价也最高的一类误判。
4.4 边际收益递减
把 4.2 节那张表画成曲线,会看到一个非常明确的形状:OP 从 0 加到 7% 时,写放大上界从无穷掉到 15,收益巨大;OP 从 28% 加到 50% 时,只从 4.6 掉到 3.0,收益很小。这就是边际收益递减。
| OP 区间 | 上界变化 | 单位预留换来的收益 | 还值得加吗 | 判断 |
|---|---|---|---|---|
| 0% → 7% | 无穷 → 15.3 | 极大 | 必须加 | 不加根本不能用 |
| 7% → 15% | 15.3 → 7.7 | 很大 | 强烈建议 | 性价比最高的一档 |
| 15% → 28% | 7.7 → 4.6 | 中等 | 看负载 | 写密集场景值得 |
| 28% → 50% | 4.6 → 3.0 | 较小 | 只在特定场景 | 抖动敏感或极端写密集 |
| 50% → 100% | 3.0 → 2.0 | 很小 | 几乎不值得 | 容量成本太高 |
递减的原因从公式看一目了然:(1 + OP) / OP = 1/OP + 1。OP 越小,1/OP 这一项变化越剧烈;OP 越大,它被稀释得越厉害。当 OP 已经很大时,再往上加只能把那条「1」以外的部分再压一点点。
因此「预留越多越好」是错误的。正确的判断是:先看负载的写密集程度与抖动敏感度,找到边际收益开始变平的那个拐点,把预留定在它附近。第 9.5 节会给一个决策框架。
4.5 典型档位与效果量级
把行业里常见的几档预留和它们的实际效果放在一起,作为选型的起点。再次强调:这些都是量级与经验区间,不是任何一颗料的规格。
| 档位 | 典型 OP | 面向的场景 | 为什么这么定 | 代价 |
|---|---|---|---|---|
| 客户端入门 | 约 7% 上下 | 日常办公、轻负载 | 容量敏感,成本优先 | 随机写密集时掉速明显 |
| 客户端主流 | 约 15% 上下 | 混合读写、日常重度使用 | 容量与性能的折中点 | 标称容量少一档 |
| 企业级入门 | 约 28% 上下 | 读多写少、有写突发 | 保证稳态性能不塌 | 每 GB 成本上升 |
| 企业级写密集 | 约 50% 上下或更高 | 日志、数据库、缓存 | 抖动必须压到很低 | 容量成本大幅上升 |
| 主机自定义 | 由分区方案决定 | 特殊负载与定制系统 | 按实测反馈调整 | 需要自己维护与验证 |
一个常被问到的问题是「能不能靠主机少分区来替代厂商预留?」答案是物理上可以,工程上不推荐完全替代:厂商预留是出厂就划定的、不会被误用;而主机留出的空间依赖使用习惯,一旦有人把分区建满,预留就消失了。稳妥做法是两者叠加——出厂预留打底,主机再根据需要加一层。
图 5 把这条曲线画了出来。请特别注意曲线的形状——它在左侧几乎是垂直下降的。这意味着:从「几乎没有预留」到「留够十几个百分点」这一段,是投入产出比最高的区间;再往上加,曲线迅速变平。
预留空间与写放大之间通过「整盘有效数据占比」这座桥连接起来:OP 决定 u,u 决定回收时的平均搬迁量 V,V 决定写放大。这条链条上没有任何魔法,全是算术。理解了这条链,就能自己推导任何一组输入下的结果,而不必去背「某某档位要留多少」这种结论。