磨损均衡算法对比
动态均衡 / 静态均衡 / 全局与混合均衡 —— 把擦除次数均匀摊到所有块上的三层算法、三项代价与选型边界 | 编制日期 2026-10-14 | 版本号 Rev 1.0
几乎每一份存储器件的技术简介都会写一句「内置磨损均衡」,但它从来没说清楚:均衡到底均衡了什么、均衡到什么程度、为此付出了多少额外写入。于是现场就出现两类典型困惑——一类是器件标称寿命远没到,实际却提前出现坏块与写入失败;另一类是开了均衡之后写放大明显变差,性能与寿命反而一起下滑。
这两类困惑的根子是同一个:擦除次数天然不会自己均匀分布。热数据反复改写少数块,冷数据长期压着一批几乎不参与磨损的块,差距会随时间单调拉大。磨损均衡不是「让器件更耐用」,而是把有限的总擦除预算从用不完的地方搬到不够用的地方。
这份文档把这件事拆成三层算法来讲:动态均衡只管挑空闲块,静态均衡主动搬迁低磨损块上的冷数据,全局与混合均衡把两者融合并放大到段与区域粒度。三条主线之外,给出统一的代价框架、参数起点、可观测性指标与失效排查路径。所有数值均为行业经验区间,以正式 datasheet 与实测为准。
结论: 磨损均衡的本质只有一句——把擦除次数均匀摊到所有块上,让整盘寿命由「所有块的平均磨损」决定,而不是由「磨损最重的那一个块」决定。 三层算法的分工因此非常清楚:动态均衡只在空闲块里挑擦除计数最低的那个来承接新写入,冷数据块永远不会被它迁移,这是它最大的失效场景;静态均衡主动把低磨损块上的冷数据搬到高磨损块上,用额外迁移量换均衡度;全局与混合均衡把两者融合,并把管理粒度放大到段与区域,用更粗的粒度换更小的表。 三者共同的核心权衡是「均衡效果 vs 迁移开销 vs 表规模」,这三项不可兼得——要更高的均衡度就得付出更多迁移写入,要把表做小就得接受更粗的粒度与更低的均衡度。选型不是挑「最好的算法」,而是挑在你的负载形态下代价最合适的那一个。
读者路径
- 只想搞懂为什么磨损会不均:第 1 章,重点看 1.2 热冷数据与 1.3 磨损度量。
- 要理解三层算法的差别:第 3 章(动态)、第 4 章(静态)、第 5 章(全局与混合),这是本文主干。
- 要做选型:第 6 章(横向对比与选型矩阵)+ 第 11.1 节(决策树)。
- 要调参数:第 8 章(阈值、批量、周期、冷热窗口)与第 9 章(度量)。
- 现场出问题了:第 10 章(误区与失效模式)+ 附录 C(排查手册)。
- 只想要速查表:第 12 章与附录 B 就够了。
1. 磨损为什么天然不均匀
磨损均衡之所以存在,前提只有一个:如果不加干预,擦除次数在各块之间一定是不均匀的,而且差距会随时间单调拉大。这一章先把「为什么会不均」讲透,再定义「用什么量来描述不均」,最后给出磨损均衡能做到与做不到的边界。后面所有算法的比较,都建立在这一章的三个事实之上。
1.1 写入分布不均匀的根源
一个很自然的想象是:主机写了多少数据,NAND 上就均匀摊掉多少磨损。这个想象在两处同时出错。
- 第一处:写入量按地址加权,不是按容量均摊。擦除次数是「有多少次写入落到了这块上」的累积结果。写入落在哪块由逻辑地址决定,而逻辑地址的访问频率在不同区间之间可以相差几个数量级。容量占一半的区间,完全可能只吃掉个位百分点的写入。
- 第二处:擦除的单位是块,写入的单位是页。一次擦除覆盖整个块,但触发这次擦除的可能只是块里某几页的改写。于是「少量热点页」就能拖着「整块」反复擦写。
把这两处合起来,写入分布不均的根源可以归成四类,它们几乎总是同时出现:
| 根源 | 典型形态 | 空间分布特征 | 时间分布特征 | 对擦除次数的直接影响 |
|---|---|---|---|---|
| 元数据反复改写 | 文件系统元数据、目录项、超级块、分配位图 | 集中在极少数固定逻辑区 | 高频、持续、贯穿全生命周期 | 把少数块推到远高于均值的计数 |
| 日志与记账循环写 | 日志、计数器、掉电记账区、磨损计数本身 | 集中在环形或固定区域 | 周期性、可预测 | 形成稳定且长期存在的热点 |
| 固件与只读资源 | 固件镜像、字库、配置、只读资源包 | 占容量大头 | 写一次后长期不再改写 | 对应块长期停在低计数,几乎不参与磨损 |
| 随机小写命中窄区间 | 小粒度随机写、数据库页更新、参数区 | 命中少数区间 | 突发、抖动大 | 在窄区间内制造局部高计数块 |
这四类里,前两类制造「热端」,第三类制造「冷端」,第四类介于两者之间并放大抖动。热端与冷端同时存在,才是磨损不均的完整形态——只看到热端会误以为「把热数据摊开就行」,而真正让均衡失效的往往是冷端那批一动不动的块。
把磨损不均归因为「器件质量问题」或者「颗粒一致性差」是最常见的误判。颗粒之间确实存在耐久差异,但在正常工作负载下,负载分布带来的计数差距通常远大于颗粒本身的耐久离散。也就是说:同一批器件在不同的写入模型下,寿命可以差出很多倍,而这个差别主要来自负载,不是来自器件。
1.2 热数据与冷数据
描述写入分布最常用的切分是热数据与冷数据。这条切分不是按数据类型分的,而是按「改写频率」分的:
- 热数据:改写频率高,逻辑地址被反复命中。它可能只占容量的很小一部分,却吃掉大部分擦除次数。
- 冷数据:写入之后长期不再改动。它通常占容量的大头,但擦除计数长期停在写入时的水平。
- 温数据:介于两者之间,改写频率随业务阶段变化。实际系统里真正的中间态往往比两端更难处理,因为它会「变热」也会「变冷」。
从工程经验看,冷数据在容量占比上经常是大头量级(常见说法是占到容量的五到七成,具体取决于设备类型与使用阶段),而它吃掉的擦除次数往往只是一小部分;热数据则相反。这个「容量占比与磨损占比倒挂」的现象,是后面所有均衡算法存在的前提:如果两者不倒挂,均匀摊开就没有意义。
| 维度 | 热数据 | 温数据 | 冷数据 |
|---|---|---|---|
| 改写频率 | 高,可能每秒到每分钟量级 | 中等,随业务阶段波动 | 极低,写入后长期不变 |
| 容量占比 | 常为小头 | 中等 | 常为大头 |
| 磨损占比 | 常为大头 | 中等 | 常为小头 |
| 擦除计数走势 | 持续领先并拉大差距 | 跟随均值上下移动 | 长期停在低位 |
| 对均衡的意义 | 是迁移的主要目标源 | 判定最难,容易误判 | 是静态均衡要腾挪的对象 |
| 典型例子 | 元数据、日志、计数器 | 用户增量数据 | 固件、只读资源、归档 |
这里有一个必须提前说清的点:冷热是动态的。今天的冷数据可能因为一次固件升级变成温数据,明天的热数据也可能因为业务切换彻底冷下来。因此冷热判定不能只看「最后一次写入时间」,而要看一段时间窗口内的改写次数——窗口多长,就是第 8.4 节要讨论的参数。
1.3 磨损度量的定义
在谈「均不均」之前,必须先有可计算的量。工程上最少需要下面几个量,它们的定义要严格区分:
| 量 | 含义 | 怎么算 | 用途 | 注意点 |
|---|---|---|---|---|
| 擦除计数 | 单个块累计被擦除的次数 | 每次成功擦除后加一 | 均衡的直接输入 | 必须持久化,掉电不能丢 |
| 耐久上限 | 块在规格上可承受的擦除循环数 | 来自器件规格书 | 归一化分母 | 随颗粒类型与代际差异很大 |
| 磨损度 | 已消耗寿命的比例 | 擦除计数 / 耐久上限 | 横向比较不同块 | 分子分母必须同源 |
| 剩余寿命 | 还能承受的擦除次数 | 耐久上限 - 擦除计数 | 坏块与替换决策 | 不是时间的直接换算 |
| 均值 | 全部块擦除计数的算术平均 | 求和 / 块数 | 整盘磨损水平 | 被极值拉偏时需配合中位数看 |
| 极差 | 最高计数与最低计数之差 | 最大值 - 最小值 | 最直观的均衡指标 | 只看两个块,易受异常值误导 |
| 标准差 | 计数分布的离散程度 | 对偏差平方求平均再开方 | 分布的稳定性 | 对少量离群块不敏感 |
| 分位数 | 把分布切成若干段的边界值 | 排序后按位置取值 | 看长尾与冷端 | 比极差更能反映整体形态 |
第一,分子分母必须同源:算磨损度时,擦除计数与耐久上限要来自同一颗粒类型与同一口径,不能拿一类颗粒的计数除以另一类的上限。
第二,均值的口径要写清楚:是「所有块的算术平均」,还是「只统计在用块」,还是「按容量加权」。三种口径在冷端占比高时差别很大,混用会得出自相矛盾的结论。
第三,分布类指标必须说明统计范围:是否包含坏块、是否包含替换块、是否包含保留区。这些块数量虽少,却往往是极值所在,包含与否会显著改变极差。
这几个量里,极差与标准差是描述「均不均」的主力,均值描述「磨到什么程度」,磨损度描述「还剩多少」。它们回答的是三个不同问题,混用会直接导致误判:均值很低不代表均匀(可能是一半块为零、一半块已接近上限),极差很大也不代表马上要坏(可能最高值离上限还很远)。
还需要区分两个容易混淆的口径:主机侧写入量与 NAND 侧写入量。磨损由后者决定,而用户体验由前者决定,两者之间的比值就是写放大系数。任何「按写入量估算寿命」的算法,都必须先把主机侧换算成 NAND 侧,否则会系统性低估磨损。
1.4 磨损不均的后果
磨损不均本身不是故障,它是一种把寿命提前消耗掉的效率损失。后果按严重程度递进:
- 整盘寿命由最重的块决定。只要有一个块先触顶失效,可用容量就开始掉,哪怕其余块的剩余寿命还很充裕。这部分「用不完的寿命」是纯损失。
- 可用容量随时间衰减。随着坏块与替换块增加,可提供给主机的容量逐步下降,而且下降往往不是线性的。
- 写放大随之抬高。可用空间变紧会迫使垃圾回收更频繁、更激进,进一步增加 NAND 侧写入,形成正反馈。
- 数据保持裕量被吃掉。擦除次数高的块,电荷保持能力通常更差;同一批数据落在高计数块上, retention 风险更大。
- 性能抖动变大。回收压力上升会带来更长的写停顿,表现为偶发的长尾延迟而不是平均性能下降。
- 一致性风险。如果计数表与实际物理状态不一致,可能把数据搬到已经接近寿命末期的块上,把本来可控的问题变成不可控。
- 评估失真。如果只用整盘均值或总写入量来估算剩余寿命,会系统性高估——因为真正先失效的是分布右端那一小撮块,而不是均值所代表的「典型块」。
最后一条值得单独强调:寿命评估必须用分布,不能用均值。用均值估算出来的寿命,是「把所有块的寿命借给彼此之后」的理想值,而磨损不均恰恰意味着这种借贷做不到。两者之间的差额,就是磨损均衡要挽回的目标,也是不做均衡时白白损失掉的那部分。
| 后果 | 直接表现 | 为什么会这样 | 能否靠均衡挽回 |
|---|---|---|---|
| 寿命提前触顶 | 少数块先失效,整盘剩余寿命充裕 | 寿命由最高计数块决定 | 能,这是均衡的主战场 |
| 可用容量衰减 | 可用容量随使用时间下降 | 坏块与替换消耗物理容量 | 部分能,取决于坏块原因 |
| 写放大抬高 | 主机写不变,NAND 写上升 | 回收变频繁、搬迁叠加 | 不能,均衡本身还会抬高 |
| 保持能力变差 | 高计数块上数据出错率上升 | 耐久与保持特性相关 | 间接能,通过摊平计数 |
| 长尾延迟变差 | 偶发长时间写停顿 | 擦除与搬迁占用带宽 | 不能,属于均衡的代价侧 |
| 计数与物理不一致 | 计数表失真导致误判 | 掉电、记账遗漏、更新不同步 | 不能,属于实现正确性问题 |
1.5 磨损均衡能做什么、不能做什么
把边界划清楚,比记住算法细节更重要。磨损均衡是一个分配优化,不是一个寿命创造机制。
| 能做 | 做到什么程度 | 不能做 | 为什么不能 |
|---|---|---|---|
| 把擦除次数摊得更均匀 | 把极差与标准差压下去 | 凭空增加总擦除预算 | 总预算由颗粒与容量决定 |
| 让冷端块参与磨损 | 通过主动搬迁实现 | 在不搬迁的前提下搬冷数据 | 冷数据不动,计数就不会涨 |
| 降低由磨损不均导致的寿命损失 | 显著,是主要收益 | 降低由写入量本身决定的磨损 | 写入量是需求侧决定的 |
| 改善高计数块的保持风险 | 通过摊平计数间接改善 | 修复已经物理损坏的块 | 物理损坏不可逆 |
| 配合回收减少无效擦除 | 有协同空间 | 替代垃圾回收 | 两者解决不同问题 |
| 让整盘寿命接近平均水平 | 这是均衡的目标上限 | 让整盘寿命超过平均水平 | 平均就是上限 |
磨损均衡只能把已有的擦除预算重新分配,不能增加预算总量。它的收益上限是「让整盘寿命等于所有块的平均寿命」,而代价是为此付出的额外迁移写入与表空间。因此均衡做得越激进,收益增长越慢、代价增长越快——这个非线性关系是所有参数选择的底层约束。
1.6 本文主线
全文围绕一条主线展开,后面所有章节都是它的展开:
磨损均衡的本质是把擦除次数均匀摊到所有块上。
三层算法的分工:动态均衡只管「挑空闲块」,冷数据块永远不被迁移,这是它最大的失效场景;静态均衡主动搬迁低磨损块上的冷数据,用额外迁移量换均衡度;全局与混合均衡把两者融合并放大到段与区域粒度。
核心权衡是「均衡效果 vs 迁移开销 vs 表规模」,三者不可兼得。
沿着这条主线,本文的推进顺序是:第 2 章先把问题形式化(合格判据、三项代价、信息范围、分工、粒度),第 3~5 章分别讲三层算法,第 6 章横向对比,第 7 章讲与其他机制的耦合,第 8 章讲参数,第 9 章讲度量,第 10 章讲误区与失效,第 11 章给工程清单,第 12 章收口。
把图 1 的四条来源和图 2 的分布形态放在一起看,会得到两个可直接用于设计的判断:
- 判断一:只看均值一定会误判。均值描述的是「总共磨了多少」,但决定寿命的是分布的长尾。两个均值相同的器件,一个分布紧凑、一个长尾很长,寿命可以完全不同。
- 判断二:冷端才是动态均衡的盲区。图 2 里冷端那批块计数长期不动,任何「只在空闲块里挑」的策略都无法让它们参与磨损。要让它们参与,只有一条路——把上面的数据搬走,把热数据换进来。
2. 磨损均衡的基本问题与约束
在比较具体算法之前,需要先把问题形式化:「均衡」到底要均到什么程度才算合格?为此要付出什么?手上有多少信息可用?谁来做?在多粗的粒度上做?这五个问题不回答清楚,任何算法比较都会退化成口号之争。
2.1 均衡到什么程度算合格
「均匀」是一个理想极限:所有块擦除次数完全相等。工程上既达不到,也没必要达到。真正要回答的是允许多大的离散度。
可用三类判据来描述,它们适合不同的场合:
- 极差判据:最高计数与最低计数之差,控制在均值的某个比例内。最直观,但只看两个极端点。
- 标准差判据:计数的标准差与均值的比值(离散系数)落在某个区间内。反映整体分布,对少数离群块不敏感。
- 分位数判据:例如把最高分位与最低分位的差值、或者中位数与最高分位的比值作为门槛。兼顾整体与长尾,是实际系统里最实用的一种。
从经验上看,一个运行良好的块级均衡,通常能把离散度压到「远小于 1」的量级(即标准差明显小于均值),并把极差控制在均值的一倍以内;而完全不做静态均衡的系统,极差常常能拉到均值的数倍。这些都是量级性参考,具体门槛必须结合器件的耐久上限与目标寿命反推,以正式 datasheet 与实测为准。
| 判据 | 计算方式 | 优点 | 缺点 | 适合谁 |
|---|---|---|---|---|
| 极差 | 最大值 - 最小值 | 一眼看懂,便于现场判断 | 只看两点,易被异常值误导 | 现场快速体检 |
| 离散系数 | 标准差 / 均值 | 反映整体,无量纲可比 | 对少量离群块不敏感 | 算法效果评估 |
| 分位数差 | 高分位 - 低分位 | 兼顾整体与长尾 | 需要先排序,计算量略大 | 控制器内部触发判据 |
| 触顶预警 | 最高计数 / 耐久上限 | 直接对应寿命风险 | 不反映分布形态 | 坏块与替换决策 |
| 冷端占比 | 低计数块数 / 总块数 | 直接暴露动态均衡盲区 | 阈值依赖冷热定义 | 判断是否需要静态均衡 |
还需要明确一个容易混淆的点:合格不等于最优。把离散度从 0.5 压到 0.3 可能需要翻倍的迁移量,而寿命收益可能只有几个百分点。因此工程上更常用的做法是设定一个目标区间,达到即止,而不是一味追求最小离散度。这个「达到即止」的门槛就是第 8.1 节的触发阈值。
还有一点:合格判据要随磨损阶段变化,不能从头到尾用同一个数字。在寿命早期,各块计数都很低,此时极差的绝对值小是自然的,判断意义不大,应该看相对离散度;到寿命后期,各块计数都接近上限,此时即使相对离散度不大,绝对极差也可能已经足以让一部分块先触顶,应该同时看绝对极差与触顶接近度。同一个判据贯穿全寿命期,是很多「到后期突然失效」的根因。
2.2 三项代价:迁移写入、表空间、擦除成本
磨损均衡不是免费的。它同时付三项代价,而且这三项彼此牵制。
| 代价 | 是什么 | 为什么会付 | 主要受什么影响 | 典型表现 |
|---|---|---|---|---|
| 迁移写入 | 把数据从源块读出再写入目标块 | 均衡必须移动数据才能改变归属 | 迁移批量、触发频率、冷热判定准确度 | 写放大系数上升 |
| 表空间 | 擦除计数表、冷热标记、位图等元数据 | 记账粒度越细,条目越多 | 记账粒度、块数、是否需要历史窗口 | SRAM 占用与掉电保存开销 |
| 擦除成本 | 腾空旧块所需的擦除动作 | 搬迁后旧块必须回收才能复用 | 搬迁批量、是否合并到垃圾回收 | 带宽占用与写停顿变长 |
三项代价之间的关系可以概括成三条:
- 均衡度与迁移写入正相关。要更高的均衡度,就要搬得更频繁、搬得更多。边际收益递减,边际代价递增。
- 表规模与均衡度正相关。记到块级才能识别块间差异;记到段级,段内差异就被平均掉了,看不见也就均衡不了。
- 擦除成本与迁移写入正相关,但可以被摊销。把均衡搬迁与垃圾回收合并执行,一次擦除服务两个目的,是降低这项代价的主要手段(见第 5.3 与第 7.1 节)。
2.3 信息的可获得范围
算法能做什么,首先取决于它能看到什么。信息范围决定了均衡能力的上限,这一点经常被忽略:很多「算法效果不好」的问题,实质上是「算法看不到足够的信息」。
| 信息项 | 控制器侧能否拿到 | 主机侧能否拿到 | 拿不到时的后果 |
|---|---|---|---|
| 每块擦除计数 | 能,且是它自己维护的 | 通常不能直接拿到 | 无法自建均衡 |
| 坏块与替换状态 | 能 | 通常不暴露 | 可能把数据搬到风险块 |
| 逻辑地址到物理块的映射 | 能 | 看不到,映射在器件内部 | 无法判断数据落在哪 |
| 冷热属性(改写频率) | 能,靠访问统计 | 能,且更懂业务语义 | 控制器侧要靠窗口猜 |
| 主机逻辑上的数据重要性 | 不能 | 能 | 无法做语义级冷热分层 |
| 未来写入意图 | 不能 | 部分能(业务可预测) | 只能被动响应,无法提前布局 |
| 空闲块池状态 | 能 | 看不到 | 无法自己挑块 |
这张表直接解释了为什么磨损均衡几乎总是放在控制器侧:擦除计数、映射关系、空闲块池这三项核心信息只有控制器有。主机侧能做的主要是减少无效写入、做冷热分层、避免放大,这属于「少制造问题」,而不是「解决问题」。
2.4 主机侧与控制器侧的分工
两侧各有能做与不能做的事,把分工讲清楚可以避免大量无效的尝试。
| 职责 | 主机侧 | 控制器侧 | 说明 |
|---|---|---|---|
| 维护擦除计数 | 不做 | 必做 | 控制器专有信息 |
| 挑空闲块承接写入 | 不做 | 必做 | 动态均衡的核心动作 |
| 搬迁冷数据 | 不直接做 | 必做 | 静态均衡的核心动作 |
| 冷热识别 | 可做语义级 | 做统计级 | 两侧互补,主机更准 |
| 减少元数据放大 | 可做 | 部分可做 | 主机侧的收益通常更大 |
| 合并小写、对齐写入 | 可做 | 可做 | 降低写放大,间接利于寿命 |
| 预留空间与容量规划 | 可做 | 部分可做 | 决定回收压力 |
| 保证掉电一致性 | 配合 | 必做 | 计数表必须可靠持久化 |
主机侧负责「少写、写对」,控制器侧负责「摊平」。主机侧最有价值的两件事是:减少元数据类的高频小改写,以及把真正冷的大块数据(固件、资源包)尽量一次性写入、之后不再触碰。这两件事的收益往往比纠结用哪种均衡算法更大,而且不需要任何器件内部信息。
2.5 均衡粒度:页、块、段、区域
粒度是磨损均衡最容易被忽视、但影响最大的一个设计变量。它同时决定表规模、可达均衡度、迁移代价三件事。
- 页级:以页为记账与轮转单位。粒度最细,但擦除的单位是块而不是页,页级记账无法解决块间差异,主要用于块内页间的写指针轮转。
- 块级:以块为记账单位。这是主流粒度,表规模与块数成正比,能直接识别块间差异,绝大多数均衡算法在这一层工作。
- 段级:以若干块组成的段为记账单位。表规模缩小一个量级,代价是段内差异被平均掉,段内冷热不均仍然存在。
- 区域级:以更大的区域(如若干段组成的大区)为记账单位。表最小,适合超大容量器件,只能保证区域之间的大致均衡。
| 粒度 | 记账条目量级 | 能识别的差异 | 表规模 | 迁移代价 | 典型适用 |
|---|---|---|---|---|---|
| 页级 | 页数量级 | 块内页间 | 极大 | 低 | 块内写指针轮转 |
| 块级 | 块数量级 | 块间 | 中 | 中 | 主流器件 |
| 段级 | 段数量级 | 段间 | 小 | 中到高 | 大容量与嵌入式控制器 |
| 区域级 | 区数量级 | 区间 | 很小 | 高 | 超大容量、内存受限 |
选择粒度的经验规则是:在内存与表规模允许的范围内,尽量选最细的一档。因为粒度一旦选粗,段内/区内的差异就永久性地看不见了,后面再怎么调参数都补不回来。反过来,粒度选细带来的表规模压力,可以通过压缩计数位宽、分层记账(粗粒度常驻内存、细粒度按需加载)来缓解,这属于可实现性问题,而不是原理性障碍。
症状一:粒度选得太粗。表现为表很小、内存很松,但极差长期压不下去,且怎么调参数都改善有限。原因是段内或区内的差异根本看不见,算法无从下手。这种情况下调参数没有用,只能把粒度做细。
症状二:粒度选得太细。表现为均衡效果不错,但内存吃紧、掉电保存时间变长、极端情况下出现换入抖动。这种情况下可以考虑分层记账:粗粒度常驻、细粒度按需,而不是简单地把粒度整体调粗——后者会把已经获得的均衡能力一起丢掉。
3. 动态磨损均衡
动态均衡是三层算法里最轻的一层,也是唯一不产生任何额外数据搬迁的一层。它的全部动作只有一句:当需要一个新的空闲块来承接写入时,从空闲块池里挑擦除计数最低的那一个。这一章讲它的原理、实现结构、效果边界、代价与适用场景——重点是它的失效场景,因为那正是静态均衡存在的理由。
3.1 原理:只挑擦除计数最低的空闲块
动态均衡的工作时机非常明确:分配新块的那一刻。平时它什么都不做,不扫描、不统计、不搬迁。
它的判定规则也只有一条:在当前空闲块池里,比较各块的擦除计数,取最小的那个分配出去。这条规则的效果是——计数低的块会被优先用起来,计数高的块会在池里多待一会儿,于是各块的使用机会被拉平。
理解这条规则,有两个关键词必须抓住:
- 关键词一:被动。它只在「需要新块」时被触发。没有写入、没有回收、没有开新块的需求时,它完全不动。
- 关键词二:候选集只有空闲块。这是它的力量所在,也是它的致命限制:已经存放着数据的块(尤其是冷数据块)永远不在候选集里,因此永远不会被它选中、不会被它腾挪。
从效果上看,动态均衡解决的是「新写入往哪放」的问题,不解决「已经放好的数据要不要动」的问题。前者是分配问题,后者是布局问题,两者的难度完全不同。
| 要素 | 动态均衡的做法 | 带来的结果 |
|---|---|---|
| 触发时机 | 需要分配新块时 | 完全被动,无空闲调度开销 |
| 候选集 | 仅空闲块池 | 已占用块不参与,冷端不受影响 |
| 比较依据 | 块的擦除计数 | 计数低的优先被用 |
| 是否搬迁数据 | 不搬迁 | 不产生额外写入 |
| 是否修改映射 | 不改已有映射 | 无额外映射更新开销 |
| 是否额外擦除 | 不额外擦除 | 不额外消耗寿命 |
| 实现复杂度 | 低 | 小容量器件也能做 |
3.2 实现结构:有序链表与最小堆
原理只有一句话,但要做得快,需要一个合适的数据结构。核心操作是三个:取计数最小的块(分配时)、块计数加一后重新归位(擦除后)、新块加入池(回收后)。
常见的三种实现结构各有取舍:
| 结构 | 取最小 | 更新位置 | 插入 | 内存开销 | 适合场景 |
|---|---|---|---|---|---|
| 无序数组 + 线性扫描 | 线性级 | 常数级 | 常数级 | 最小 | 块数很少、分配不频繁 |
| 按计数分桶的链表数组 | 接近常数级 | 常数级 | 常数级 | 中等,随计数档位数增长 | 计数范围有限、档位可枚举 |
| 最小堆 | 常数级取堆顶 | 对数级 | 对数级 | 小,一个数组即可 | 块数多、分配频繁 |
| 平衡二叉搜索树 | 对数级 | 对数级 | 对数级 | 较大,有指针开销 | 需要同时支持范围查询 |
工程上最常见的组合是「分桶链表 + 少量堆」:把擦除计数按档位分桶(例如每若干次一档),每个桶挂一条空闲块链表;分配时从最低的非空桶取头节点,块计数加一后挂到新档位的桶尾。这种结构的优点是三个核心操作都接近常数时间,且不需要为每个块维护指针节点,内存开销可控。缺点是桶内不做排序,同一档位内的块按挂入顺序使用,牺牲了一点精度换来了实现简洁。
需要更高精度时,可以在桶内再挂一个小顶堆,或者把档位做细。代价是内存与维护复杂度上升。
把擦除计数当作「每次擦除就无条件加一」来维护是不够的。至少要处理三件事:一、计数必须持久化,掉电后要能恢复,否则整张表失真;二、计数的更新必须与擦除动作原子绑定,擦除成功才加,失败不加;三、替换块与坏块要从候选集里剔除,否则会挑到不可用块。这三件事里任何一件出错,都会让算法「看起来在跑,实际上在乱跑」。
3.3 效果边界与失效场景
动态均衡的效果边界,完全由它的候选集决定。把它能覆盖与不能覆盖的范围列清楚:
| 场景 | 动态均衡是否有效 | 原因 | 后果 |
|---|---|---|---|
| 写入均匀分布 | 有效 | 各块被轮到的机会接近 | 计数分布保持紧凑 |
| 热点集中在少数逻辑区 | 部分有效 | 热端在空闲池内被轮转 | 热块被摊开,但仍领先 |
| 存在大量只读冷数据 | 基本无效 | 冷块不在候选集,永不参与 | 冷端计数长期停在低位 |
| 容量接近写满 | 效果下降 | 空闲池小,可选范围窄 | 轮转空间被压缩 |
| 写入量极小 | 看不出效果 | 总擦除次数本来就少 | 不做也无妨 |
| 一次性写入后长期静置 | 完全无效 | 没有新分配动作发生 | 分布定格在写入时的形态 |
把这些场景归纳成一句话:动态均衡只对「被写进去的数据」有效,对「已经躺在里面不动的数据」无效。第 1.2 节说过,冷数据往往占容量的大头——这就意味着动态均衡在最需要起作用的那部分容量上,恰恰完全不起作用。
这是它最大的失效场景,也是最容易被误判的场景:因为从外部看,动态均衡一直在工作(分配、计数更新、空闲池轮转都在跑),所有可观测的动作都正常,但冷端与热端的差距仍在持续拉大。「算法在跑」不等于「算法有效」,这个区别在第 10 章会再展开。
3.4 代价分析
动态均衡的代价在三层算法里是最低的,但并不是零。把它的代价拆成三项来看(沿用第 2.2 节的框架):
| 代价项 | 动态均衡的表现 | 说明 |
|---|---|---|
| 迁移写入 | 基本为零 | 它只改变「新数据写到哪」,不搬动已有数据 |
| 表空间 | 需要整张擦除计数表 | 块级记账,条目数与块数同量级 |
| 擦除成本 | 不额外增加 | 擦除次数由写入量决定,与挑哪个块无关 |
| 额外开销 | 每次分配多一次比较 | 分桶或堆结构下开销很小 |
| 掉电保护开销 | 计数表需持久化 | 这是主要的实现代价 |
换句话说,动态均衡的代价主要是一次性的实现代价(一张计数表 + 一个有序结构 + 持久化机制),而不是持续的运行代价。一旦实现完成,它运行起来几乎不增加 NAND 侧写入。这也是为什么它几乎是所有带控制器的存储器件的标配——投入产出比太高了,不做反而奇怪。
但正因为它便宜,也容易出现「只做动态均衡就以为万事大吉」的误判。这个误区在第 10.1 节单独讨论。
3.5 适用场景
动态均衡适合以下几类情况,判断标准是冷数据占比低、或者总写入量相对容量很小:
| 适用情况 | 判断依据 | 理由 |
|---|---|---|
| 小容量器件,写入量相对充足 | 总写入次数远未接近耐久上限 | 不做均衡也够用,做了更稳 |
| 数据基本都会周期性更新 | 冷数据占比低 | 动态轮转已能覆盖大部分块 |
| 内存与算力严重受限 | 无法承受搬迁与复杂表 | 动态均衡是唯一可选项 |
另有两类情形不列入上表:「只读或近只读场景」的写入集中在初始化阶段,分布定格,动态均衡无从发挥,属于不适合的情形;「作为其他均衡的基础层」则不是独立适用项——动态层负责分配,上层机制负责布局。
动态均衡是必要条件,不是充分条件。任何一套完整的磨损均衡都应该包含它(因为它的代价极低、收益确定),但任何一套只有它的方案,在存在大量冷数据的场景里都会失效。判断是否需要往上加一层,看的是「冷数据占多少」而不是「写入有多热」。
最后补一条经常被问到的实务问题:动态均衡会不会让性能变差?答案是基本不会。它只在分配时刻多一次比较,而比较的代价在分桶或堆结构下极小;它不改变写入路径的长度,也不产生额外 I/O。因此如果发现开启动态均衡后性能明显下降,问题大概率不在均衡本身,而在计数表的持久化策略——比如每次擦除后都立刻写一次元数据到 NAND,那就把一次内存操作变成了真实写入。这类开销属于实现问题,可以通过延迟合并提交来消除。
4. 静态磨损均衡
静态均衡是三层算法里唯一主动制造数据搬迁的一层。它的核心动作是:把擦除计数低的块上存放的冷数据,搬到擦除计数高的块上,从而把低计数块腾出来交给热数据使用。这一章讲它的原理、冷数据怎么识别、什么时候触发、搬多少、以及它带来的新风险。核心权衡一句话:用额外的迁移写入,换更高的均衡度。
4.1 原理:主动搬迁低磨损块上的冷数据
先解释「静态」这个名字:它指的不是「静态数据」,而是「让静态(不常变)的数据也参与进来」。这一点经常被读反。
它的操作方向值得特别说明,因为很容易搞反:
- 源块是擦除计数低的块(上面放的是冷数据,所以计数低)。
- 目标块是擦除计数高的块(它已经被磨得比较多了)。
- 搬迁之后:冷数据被放到高计数块上(冷数据在写入后不再改动,放上去之后这块就不会继续被磨),而腾出来的低计数块被回收进空闲池,接下来会被热数据优先使用。
于是发生了一次「交换」:冷数据替热数据承担了已经消耗掉的那部分磨损,热数据拿到了新鲜的块。从整盘看,冷端计数被抬起来,热端计数上升变慢,极差被压缩。
这个交换之所以成立,靠的是冷数据的性质——它搬过去之后不会再被频繁改写。如果把热数据搬到高计数块上,等于把最热的数据放到最旧的块上,是纯粹帮倒忙。
| 环节 | 选谁 | 判据 | 做错会怎样 |
|---|---|---|---|
| 源块 | 低擦除计数且存冷数据的块 | 计数低 + 长时间无改写 | 误搬热数据会打断业务写入 |
| 目标块 | 高擦除计数的块 | 计数高于均值一定幅度 | 选到中位块则收益很小 |
| 搬迁内容 | 源块上的全部有效页 | 有效页位图 | 漏搬会造成数据丢失 |
| 搬迁后源块 | 擦除后进入空闲池 | 无有效页残留 | 残留旧页会占位 |
| 映射更新 | 逻辑地址改指新物理块 | 地址映射表 | 漏改会读到旧位置的废数据 |
4.2 冷数据的识别方法
静态均衡成败的关键,几乎全在「什么叫冷」这个判定上。判错了,搬的就是热数据,代价付了收益没有。
常见的识别方法按信息源分成三类:
| 方法 | 依据 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 最后写入时间 | 距上次写入的时间长度 | 实现最简单,一个时间戳 | 无法区分「写了很久但马上要热」 | 粗判,作为前置过滤 |
| 窗口内改写次数 | 固定时间窗口内的改写次数 | 能反映近期活跃度 | 窗口长度敏感,需调参 | 主流做法 |
| 逻辑地址热度表 | 按逻辑区间统计访问频率 | 与业务语义相关性好 | 需要额外表与维护开销 | 控制器有能力时 |
| 主机显式提示 | 主机告知哪些数据为冷 | 最准确 | 需要接口支持,通用性差 | 有专用接口时 |
| 推断式标记 | 结合写入大小、对齐、地址分布 | 不需要额外接口 | 误判率较高 | 作为辅助信号 |
工程上最常用的是窗口内改写次数:给每个块(或每个逻辑区间)维护一个计数器,在一个滑动窗口内统计被改写的次数,次数低于阈值的判为冷。窗口长度与计数阈值是两个需要调的参数,见第 8.4 节。
还需要一个容易被忽略的补充判据:块的擦除计数本身。一个块如果计数很低且被判定为冷,它是理想的搬迁源;但如果一个块计数很低却判定为热,说明它只是刚被换上来的新块,不应该马上搬走。因此实际的源块选择通常是两个条件的交集:计数低于某个下限,且冷度高于某个门槛。
4.3 触发时机与阈值
静态均衡不能一直跑(代价太高),也不能不跑(冷端锁死)。触发时机的设计就是在这两者之间找节奏。
| 触发方式 | 判据 | 优点 | 缺点 | 典型适用 |
|---|---|---|---|---|
| 极差触发 | 最高计数与最低计数之差超过阈值 | 直接对准目标指标 | 对个别异常块敏感 | 通用 |
| 离散度触发 | 标准差与均值之比超过阈值 | 反映整体分布 | 计算量略大 | 有统计能力时 |
| 冷端占比触发 | 低计数块占比超过阈值 | 直接暴露静态均衡的必要性 | 依赖冷热定义 | 冷数据明确的场景 |
| 周期性触发 | 每隔固定时间或固定写入量执行一次 | 节奏可控,便于预算 | 可能与负载不同步 | 负载平稳时 |
| 空闲窗口触发 | 检测到空闲时段就执行一批 | 不影响前台性能 | 空闲不足时会被饿死 | 有明显空闲的设备 |
| 寿命预警触发 | 最高计数接近耐久上限一定比例 | 风险导向,优先级最高 | 触发时可能已经偏晚 | 作为兜底触发 |
实际系统往往是「周期 + 条件」的组合:每隔一段时间检查一次,只有当条件(极差或离散度)成立时才真正执行搬迁。这样既避免了无意义的搬迁,也避免了条件长期成立却一直不动作。
阈值怎么定,见第 8.1 节。这里只给一个方向性建议:触发阈值应该以「距离耐久上限还有多少」为锚,而不是以绝对值。同样的极差,在寿命早期无所谓,在寿命后期就是要命的。
4.4 迁移量的代价
静态均衡的代价是每次搬迁都要真金白银地写一遍。一次搬迁的完整开销包括:
| 开销项 | 是什么 | 与什么成正比 | 能否优化 |
|---|---|---|---|
| 读出源块有效页 | 把源块上还有效的数据读出来 | 源块有效页数量 | 能,选有效页少的块做源 |
| 写入目标块 | 把读出的数据写进目标块 | 有效页数量 | 不能,是物理必需 |
| 擦除源块 | 腾空源块以便复用 | 搬迁次数 | 能,与回收合并执行 |
| 更新映射 | 逻辑地址指向新位置 | 搬迁页数 | 能,批量提交 |
| 更新计数与冷热表 | 维护元数据 | 搬迁块数 | 能,批量提交 |
| 掉电保护写入 | 把上述变更持久化 | 变更条目数 | 部分能,合并日志 |
把这些开销折算成写放大系数的增量,是评估静态均衡代价最实用的方法:把一段时间内的搬迁写入量,除以同一段时间内的主机写入量,就得到均衡带来的写放大增量。经验上,一个调得比较好的静态均衡,这个增量应该明显小于 1;如果做到 1 以上,说明搬得太凶,需要收敛。
控制迁移量的三个主要手段:
- 限批量:每次触发只搬固定数量的块,剩下的等下一次。这是最直接有效的手段(见第 8.2 节)。
- 挑有效页少的源块:源块上有效页越少,读出与写入的量越小。优先搬那些「大部分页已经失效、只剩少量有效页」的冷块。
- 与垃圾回收合并:如果某个块本来就要被回收,顺手把它的有效页搬到高计数块上,一次擦除服务两个目的(见第 5.3 节)。
这三个手段里,「挑有效页少的源块」最容易被忽略,但收益往往最明显。原因是搬迁成本几乎全部由有效页数决定:一个只剩几页有效数据的冷块,搬起来几乎不花钱;而一个装满有效数据的冷块,搬一次等于重写一整个块。因此源块选择应该把「有效页数少」作为首要排序键,把「冷度」与「计数低」作为过滤条件,而不是反过来。很多迁移量居高不下的案例,根子就在这个排序键上。
4.5 效果与风险
静态均衡把均衡度推上一个台阶,但也引入了一组动态均衡没有的风险。这些风险不是「实现不好才会有」,而是机制本身自带的,只能通过参数与工程手段压制,不能根除。
| 风险 | 机理 | 表现 | 缓解手段 |
|---|---|---|---|
| 误搬热数据 | 冷热判定出错 | 刚搬走又被改写,双重写入 | 加窗口、加确认期、搬后观察 |
| 搬迁抖动 | 同一批数据被反复搬来搬去 | 迁移量居高不下,均衡度不改善 | 搬迁后设冷却期,禁止短期内再选为源 |
| 写放大恶化 | 搬迁量占比过高 | NAND 侧写入远超主机侧 | 限批量、与回收合并 |
| 前台性能抖动 | 搬迁占用带宽与擦除时间 | 偶发长尾延迟 | 放到空闲窗口,降低单次批量 |
| 掉电一致性风险 | 搬迁中途掉电 | 源块与目标块状态不一致 | 原子提交、日志、可回滚 |
| 计数表失真 | 计数更新与擦除不同步 | 均衡基于错误信息决策 | 原子绑定、掉电恢复校验 |
不是「搬得不够」,而是「搬了但搬错了」。搬错比不搬更糟:不搬只是冷端继续锁死,寿命按原样走;搬错则同时付出迁移写入的代价,又没有换来任何均衡收益,甚至因为把热数据放到高计数块上而加速了热块的磨损。因此冷热判定的准确度,比搬迁的执行效率更重要——宁可搬得慢,不可搬得错。