SLC Cache / 伪 SLC 与性能稳态
编制日期 2026-10-14 | 版本号 Rev 1.0
存储技术探讨 · 性能层 | 同系列另有三篇讲颗粒与控制器:《NAND 存储单元与 3D NAND》讲单元类型与堆叠如何决定物理代价,《NAND 干扰机制与数据保持》讲可靠性,《FTL 映射与写放大》讲控制器如何翻译块设备语义。本篇讲性能:缓存买到的那段写入窗口究竟有多长、窗口用完之后会发生什么,以及为什么用短样本测出来的数字不能信。
SLC Cache 买到的是一段写入窗口,不是一段持续带宽。窗口长度由缓存容量与写入速率之比决定,与总写入量无关;窗口一旦用完,性能必然回落到器件的真实水平。任何标称峰值,只要测试样本没有穿透稳态,都是无效数据。
报告总结
面向做 SD NAND / SPI NAND / 并行 NAND 方案设计与验证的工程师,回答六个问题:
- 缓存到底在缓冲什么——把「低页写与高页写的代价差」量化成时延与写放大的相对关系,而不是笼统地说「SLC 比 XLC 快」。
- 伪 SLC 是不是更好的方案——它的收益(不必为全容量付 SLC 代价)与代价(切换本身要花时间、要搬数据)如何权衡,切换策略如何决定稳态上限。
- 窗口到底有多长——缓存老化如何让有效容量低于标称容量,冷启动填充消耗掉多少预算,稳态该用什么方法判定。
- 满缓存之后发生了什么——写操作走向哪几条路径,读放大与写放大如何耦合,稳态时延为什么比冷态高一个量级。
- 缓存带来什么新风险——回写碎片如何加剧垃圾回收,缓存区自身的磨损如何累积,掉电瞬间数据可能处在哪些状态。
- 怎么测、怎么排查、怎么选型——样本长度、负载形状、混合比对稳态数据的影响,失效现象的决策顺序,以及设计检查清单。
全文不写具体 MB/s 与 IOPS 承诺,写量级一律用相对关系表达;涉及命令名与机制的地方以正式 datasheet 与规范为准。凡是给出数字的地方,都是为了让读者建立量级直觉,不是可以直接抄进规格书的指标。
- 同系列 · NAND 存储单元与 3D NAND:晶体管与阵列。数据存在哪、怎么存进去、每单元几个比特。给出 SLC/MLC/TLC/QLC 与 2D/3D 的正交关系。
- 同系列 · NAND 干扰机制与数据保持:单元间干扰。读干扰、写干扰、保持期漏电怎么影响可靠性。解释缓存区老化与数据保持的交互。
- 同系列 · FTL 映射与写放大:控制器固件。块设备语义怎么落到物理约束上,代价如何量化。本篇的碎片、回收、稳态都是写放大的延伸。
- 本篇 · SLC Cache 与性能稳态:性能与时延。缓存买到什么、窗口多长、稳态怎么测怎么判。把前三层的物理代价换算成用户可感知的时延。
读者路径
一、SLC / MLC / TLC / QLC 说的是每个单元存几个比特,2D / 3D 说的是单元怎么排布。这是两个正交维度,不是一条从低到高的进化链——一颗3D TLC 器件同时既是 TLC 也是 3D,不存在「3D 比 SLC 更先进」这种说法。二、SLC Cache 的收益是时间上集中的,它的代价是把写放大推高,这两个方向必须一起看,只看速度会得到错误的器件印象。三、本文所有数值都是量级与相对关系,任何要写进规格书的数字都必须以对应型号的正式 datasheet 与实测报告为准。
1. SLC Cache 要解决什么问题
要理解 SLC Cache,先要接受一个前提:NAND 的写入代价不是常数。同一颗器件,写不同的页、写到不同的层、缓存里有没有空间,代价可以差出一个量级。缓存做的事,就是把一段高速写入窗口放在前面,让大部分日常写入落在这段窗口里。窗口是有限的,用完就没了。
1.1 三维堆叠带来的写入劣化
平面 NAND 时代,一颗die 上横向排列的单元数量是有限的,单元之间距离足够大,干扰相对可控。把单元改成立体堆叠之后,垂直方向的层数从个位数涨到几十层甚至更多,单元之间的沟道间距急剧缩小。这件事对写入的直接后果有三个,而且三者叠加。
- 编程脉冲数上升。单元之间的耦合更强,阈值电压分布变宽,一次编程很难把目标电压打准,只能靠更多的增量编程脉冲反复逼近、每次都验证。一次页编程的耗时因此被拉长,而且这种拉长与数据内容相关——从全1 变成全 0 或反过来,脉冲数可能明显不同。
- 擦除电压分布变宽。层数越多,堆叠方向的电场分布越不均匀,擦除时不同位置的擦除裕量差异变大。为了确认擦干净,控制器需要更多轮验证,进一步拉长擦除时间。
- 读干扰与保持期恶化。单元间距变小,读取一个单元对邻居的扰动更强;层数增加也让保持期漏电问题更突出。这部分属于可靠性范畴,本篇不展开,但它会以「后台刷新」「读重试」的形式侵占带宽,间接影响稳态时延。
关键结论是:三维堆叠让NAND 的写入从「一个确定的代价」变成了「一个分布」。厂商规格书上的写入时间通常是典型负载下的值,实际时延会有波动。这正是缓存有存在价值的物理前提——如果每次写入都严格等长,缓存就没意义了,控制器直接排队即可。
| 劣化来源 | 物理机制 | 对写入时延的影响 | 对控制器的要求 | 与缓存的关系 |
|---|---|---|---|---|
| 编程脉冲数上升 | 单元耦合增强、阈值分布变宽 | 页编程时间拉长,且随数据内容波动 | 更精细的增量编程与验证策略 | 波动被缓存吸收,窗口内时延更平坦 |
| 擦除裕量变宽 | 堆叠方向电场分布不均 | 块擦除时间拉长,验证轮次增加 | 更保守的擦除判定与重试 | 回写批量进行可摊薄擦除开销 |
| 读干扰增强 | 横向与纵向间距同时缩小 | 后台读重试与刷新占用通道 | 读计数与后台刷新调度 | 刷新与回写抢通道,稳态抖动来源之一 |
| 保持期恶化 | 漏电路径增多 | 表现为后台数据搬移 | 定期扫描与刷新 | 老化的成因之一,见第 4 章 |
| 页尺寸变大 | 同容量下页数减少 | 小块写的读改写代价上升 | 写合并与写缓冲 | 缓存把大量小块写合并成整页写 |
这五条里,只有最后一条是缓存能直接消除的;其余四条缓存最多是「推迟」或「吸收波动」。把这一点讲清楚,后面对缓存的评价标准才不会跑偏。
1.2 低页写入与高页写入的代价差
三维堆叠还有一个特性是平面时代没有的:同一块里,不同的页写入代价不一样。这是多层栅极堆叠的直接后果。
在把电荷送进单元这件事上,不同的栅极层面对应的物理路径不同:靠近沟道的那一层(俗称低页区)电荷进出最容易;越靠上的字线离字线驱动端越远,字线压降与驱动条件相对越差(具体机理以器件实现为准)。结果就是——写低页快,写高页慢,而且这个差距在堆叠层数增加时会被放大。
厂商的应对策略是写排序:固件主动把顺序写映射到低页区,把随机写、覆盖写、更新写放到高页区。这是一套纯固件的调度,不增加任何成本,收益却很直接。理解这一点很重要,因为它意味着:
- 「顺序写比随机写快」不只是因为随机写碎片多,还因为顺序写可以被引导到代价更低的页区。
- 一旦负载从顺序变成随机,即使总量不变、块区分布不变,时延也会变化——因为可用的低页区被消耗掉了。
- 缓存相当于在数据区之上多出一层「全部按最优方式写」的存储空间,等于把高页区的竞争挪出了关键路径。
| 对照项 | 低页区(靠沟道侧) | 高页区(远离沟道侧) | 对使用者的含义 |
|---|---|---|---|
| 单次写入耗时 | 较短,是设计上的快路径 | 较长,且随层数增加而恶化 | 随机写天然吃亏,与碎片无关的那部分 |
| 擦写耐受度 | 通常优于同器件平均水位 | 通常劣于同器件平均水位 | 两类页混用时按差的算寿命预算 |
| 典型分配策略 | 顺序写、大块连续写优先占用 | 随机写、覆盖写、更新写落在此处 | 顺序与随机混跑会让高页区提前成为瓶颈 |
| 缓存介入后的变化 | 缓存区内全部按最优方式写 | 数据区回写时仍要面对高页代价 | 回写批量大小直接影响高页区的压力 |
| 与容量的关系 | 低页区是有限资源,先用完 | 高页区承接剩余全部负载 | 负载越随机,低页区消耗越快 |
这一节的内容在姊妹篇《NAND 存储单元与 3D NAND》里展开过(单元类型、每单元比特数与堆叠方式的关系),本篇只引用结论,不再重复物理细节。之所以在这里重新提起来,是因为它是理解缓存收益来源的第一块拼图:缓存的收益,一部分来自「SLC 比 XLC 快」,另一部分来自「顺序写比随机写快」,而缓存对这两部分都有帮助。
1.3 缓存作为缓冲层的思路
把前面两节拼起来,缓存的思路就清楚了。NAND 的写入代价本来就有高低之分:SLC 页比 XLC 页快,低页区比高页区快,顺序写比随机写快。主机负载里这两类写是混在一起的,控制器无法控制主机要写什么,但可以控制数据先写到哪一层去。
于是缓存的做法是:在正常数据区之外,额外准备一小块空间,这块空间用代价最低的方式写。主机写过来,先写进这块空间,写完就算完成,主机不用等。盘里在后台慢慢地把这些数据搬回数据区,搬的时候可以合并、可以排序、可以挑低页区优先,代价被摊薄。
缓存不是让写变快,而是把写变慢这件事推到后台去,并且让后台的慢变得可以忍受。主机侧看到的是窗口内的快;代价记在后台账上,以碎片、回收压力和寿命的形式出现。
图1 画的是最典型的一条曲线。横轴是累计写入量,以缓存容量为一个单位;纵轴是写入时延的相对倍数。曲线在前段几乎水平(缓存窗口期),到缓存被写满之后迅速抬升,然后稳定在一个平台上(稳态)。图里还画了两条参照线:一条是无缓存基线(始终按 XLC 直写),一条是缓存容量更大的情形——它的平台更高,也就是稳态比小缓存更慢,但平台出现得更晚。
缓存容量越大,稳态性能越差。更大的缓存让你在更长的写入量范围内享受冷态时延,但缓存里的数据最终还是要按 XLC 写回数据区,而更大的缓存意味着更多数据被强制在回写那一刻落到更不利的页区。用加大缓存来改善稳态,方向是反的。缓存容量该按「工作集大小」来定,不是按「想让它慢多少」来定。
无缓存基线那条线也需要注意:它比缓存的稳态平台要高一些(时延更小),因为没有回写就没有额外的碎片和回收。这说明在持续超长写入的场景里,缓存未必是净收益——它把一部分负载变成了回写负担。缓存真正划算的场景是「写入是间歇的、总量有限的」,或者「系统大部分时间在读」。
1.4 缓存能改善什么、不能改善什么
把缓存的收益与代价分开列清楚,比任何「某方案性能提升多少」的说法都有用。下表左列是改善项,右列是明确不改善甚至恶化项。
| 场景 / 指标 | 缓存的作用 | 机理 | 需要注意的地方 |
|---|---|---|---|
| 冷启动阶段的批量写入 | 明显改善 | 全部落在缓存区,不触发回写与回收 | 这部分收益最容易被误当成持续能力 |
| 随机小块写 | 窗口期内明显改善 | 多笔小块写被合并成整页写 | 窗口用完后回到原点,收益归零 |
| 混合负载的时延稳定性 | 窗口期内抖动变小 | 回写被摊到后台,前台看到的是平坦曲线 | 满缓存后抖动回升,甚至比无缓存更抖 |
| 持续写入的长期带宽 | 不改善,甚至变差 | 回写带来的碎片与回收占用通道 | 大缓存会让稳态平台更高 |
| 写放大 | 不改善,通常变差 | 同一份数据先写缓存再写数据区,多一次写 | 寿命核算必须把这段额外写计进去 |
| 数据保持特性 | 不改善 | 缓存本身不改变单元的保持特性 | 老化反而会让有效缓存容量低于标称 |
| 随机读性能 | 基本不参与 | 缓存优化的是写路径 | 读优化靠的是别的机制,不要指望缓存 |
| 掉电数据完整性 | 不改善,引入新风险 | 未回写数据在缓存区,掉电即丢 | 一致性设计必须专门处理,见第 8 章 |
表里有三行值得展开。
「持续写入的长期带宽」不改善——这是全文最重要的一条。缓存是缓冲区,不是加速器。缓冲区在读满之后必须清空,清空的过程本身就是原来那笔开销,只是被延后了。如果负载是「一直写、永远写不完」,缓存贡献为零,甚至因为多了一次写而略微为负。
「写放大」通常变差——同一份数据被写了两遍:一次进缓存(按SLC 写),一次回写进数据区(按 XLC 写)。如果统计口径是「主机写了 1 个单位,盘里实际写了几次」,缓存至少让这个数字加一。寿命预算必须按加一之后的口径算。
「随机读性能」不参与——很多选型表里把缓存和读性能放在一起比较,这是分类错误。缓存优化的是写路径,读路径的优化靠的是读缓存、读重试、坏块管理这些别的机制。评估缓存时只用写负载去测,读负载测不出缓存的好坏。
1.5 稳态与冷态的差别
冷态与稳态是本文反复使用的两个词,定义必须先固定下来,否则后面所有讨论都会含混。
- 冷态(缓存窗口期):缓存还有可用空间,主机写入全部落在缓存区,不触发回写,不触发由回写引起的前台垃圾回收。此时测到的时延与吞吐,主要反映的是 SLC 编程速度加上接口开销,与数据区的真实写入代价无关。
- 过渡期:缓存已满,回写开始,前台写与后台回写争抢同一套资源,垃圾回收被频繁触发。这段时延抬升最快,波动也最大。
- 稳态(缓存耗尽后):缓存不再起作用,写入全部按数据区的真实代价进行,性能稳定在一个不再继续恶化的平台上。这个平台才是产品的真实性能。
三者的分界点是缓存被写满的时刻,而不是「写了多少 GB」。缓存容量和写入速率的比值决定窗口长度,同一颗器件在不同负载下窗口长度完全不同。
用一段没穿透稳态的样本测出漂亮数字,把它当作器件的性能承诺,然后在上线后发现真实业务(写入量更大、更连续)远远达不到。判断方法很简单:看测试期间缓存是否已经写满。缓存容量和写入总量处于同一量级时,几乎可以肯定稳态还没到。
「稳态」这个词还有一个易混的近邻:稳态磨损。它指器件在寿命末期、擦写累积到接近 P/E 上限时的性能状态,跟本文说的「缓存耗尽后的稳定平台」完全是两回事。前者取决于你写了多少,后者取决于缓存多大。JEDEC 对客户端与企业级 SSD 的稳态负载定义通常已经隐含了寿命末期的条件,本篇不再展开,详见姊妹篇《TBW / DWPD 寿命预算》。
1.6 本文要回答的问题与主线
全文收敛到一个判断:SLC Cache 买到的是写入窗口,不是持续带宽。围绕这个判断,全文按下面这条主线推进。每一条给出问题、它为什么重要,以及回答它的是哪一章。
- 缓存缓冲的是什么?:不弄清这一点,就会期待缓存改善它根本改善不了的指标;第 1 章 1.1 ~ 1.4。
- 缓存怎么组织、回写怎么发生?:决定缓存能撑多久、回写带来多少额外写;第 2 章。
- 伪 SLC 值不值得?:它省下了SLC 代价,但切换本身要付时间和数据位置的账;第 3 章。
- 窗口到底有多长?:窗口长度决定业务能享受多久好性能;第 4 章。
- 窗口用完之后发生什么?:这是产品的真实性能,也是唯一有效的验收口径;第 5 章。
- 缓存和垃圾回收怎么互相影响?:这是缓存唯一明确的负面方向,必须一起权衡;第 6 章。
- 三条产品线上分别怎么做?:实现代价、启动影响、可选形态都不同;第 7 章。
- 掉电时数据在哪?:缓存引入了一个新的易失位置,这是新增风险;第 8 章。
- 怎么测才算数?:样本长度不足是最普遍的性能数据造假方式;第 9 章。
- 负载形状如何决定稳态?:同样的总写入量,不同形状的表现差很多;第 10 章。
- 出问题了怎么查?:缓存相关的现象极易被误判成器件失效;第 11 章。
- 要不要用、用多大?:把前面的机制转成可执行的选型判据与清单;第 12 章。
顺带说明两处刻意的取舍。文中不写 SLC/MLC/TLC/QLC 与 2D/3D 的物理机制,那是《NAND 存储单元与 3D NAND》的内容,重复会稀释主线;文中不展开 FTL 映射与写放大的完整推导,那是《FTL 映射与写放大》的内容,本篇只在需要时引用结论。另外,文中所有量级表达都用相对关系(「处于同一量级」「高一个数量级」),不给具体 MB/s 与 IOPS 承诺——因为性能是由负载形状、控制器实现、温度、寿命阶段共同决定的,任何脱离条件的数字都没有意义。
2. SLC 缓存的工作机制
上一章说明了缓存缓冲的是什么,本章把缓存内部拆开看:它占哪些物理空间、一次写进来之后经历了哪些步骤、什么时候被搬走、搬到哪里、搬走之前如果掉电会怎样。读完本章,缓存从一个性能概念变成一个可以画在纸上的数据结构。
2.1 缓存区的组织形式
缓存区不是一块独立的物理区域,它是从正常数据区里划出来的一部分块。划的方式决定了缓存的容量是否精确、是否可回收、是否与数据区的主机可用容量冲突。
常见的三种组织形式如下。第一种是固定分区:出厂或初始化时划出一段连续的物理块作为缓存区,容量固定,映射上标为不可用。实现最简单、行为最可预测,但两个缺点很实在——一是容量必须按最坏情况设定,负载轻的时候白白占用;二是这部分空间在缓存不用的时候也拿不回来。
第二种是动态分区:缓存块和数据块混在同一池子里,用元数据标记哪些块当前充当缓存。空间利用率高、容量可以按负载调整,代价是控制器必须维护更复杂的块状态机,出错面更大。
第三种是伪 SLC(整盘 pSLC):不划出专门的缓存区,而是把全盘(或某个分区)动态地在 SLC 模式与 XLC 模式之间切换。它的收益与代价在第 3 章专门讨论。
| 组织形式 | 物理空间来源 | 容量可变性 | 实现复杂度 | 对可用容量的影响 | 典型落点 |
|---|---|---|---|---|---|
| 固定 SLC 分区 | 划出的连续块,标记为不可用 | 固定,初始化后不变 | 低,状态清晰 | 用户可用容量永久减少一块 | 追求行为可预测的方案 |
| 动态块池 | 与数据块混用,靠元数据标记 | 可按负载调整 | 中高,块状态机复杂 | 容量占用随负载浮动 | 负载波动大的通用方案 |
| 伪 SLC(整盘 pSLC) | 全盘或整分区动态切换 | 由模式状态决定 | 高,切换要搬数据 | 切换期间有短暂不可用 | 容量利用率优先的方案 |
| 日志块式 | 少量专用日志块,合并后并入数据区 | 固定,块数少 | 中,需要合并逻辑 | 影响小 | 以块为合并单位的方案 |
| 无缓存 | 不划任何空间 | 不适用 | 最低 | 无额外占用 | 写入量小或以读为主的场景 |
无论哪种形式,缓存区都有一个共同点:它对主机是不可见的。主机写逻辑地址,永远不直接落在缓存对应的物理位置上;映射表负责把逻辑块翻译到当前真正的物理位置。所以「缓存在哪」是控制器内部的实现细节,不是主机能观察到或配置的接口参数。
2.2 写入路径:日志结构还是直写
缓存有两种根本不同的写入组织方式,选哪一种决定了后面所有行为的形态。
日志结构(log-structured)是 SLC Cache 的默认形态:每次写都往缓存区末尾追加一个新页,完全不管逻辑上是不是在覆盖同一块。缓存区满了就整体或按批回写,回写时再根据逻辑块号把属于同一逻辑块的新旧版本合并,只保留最新的一份。这套结构的优点是写入路径极短——分配、编程、更新映射,三步结束,完全没有读操作参与。缺点是回写时的合并需要额外读,而且会产生碎片。
直写(write-through)是缓存只做缓冲不做累积:数据既写缓存也立刻写数据区,缓存只起「让主机先返回」的作用。这套结构几乎没有合并开销,但写放大直接翻倍,缓存区的容量优势几乎完全丧失,只剩下「提前返回」这一个作用。直写适合的场景是:写入量不大但对数据落地时间有硬要求。
| 对照项 | 日志结构(追加式) | 直写(write-through) | 选择依据 |
|---|---|---|---|
| 写入路径长度 | 分配 → 编程 → 改映射,三步 | 分配 → 编程缓存 → 编程数据区 → 改映射 | 路径越短越能兑现「窗口内很快」 |
| 写放大 | 增加一次回写写 | 增加一整次同量写,倍数更高 | 日志结构在大写入量下明显更省 |
| 回写是否需要读 | 需要,合并同一逻辑块的新旧版本 | 不需要 | 读放大主要由合并产生 |
| 碎片来源 | 回写合并时旧版本被覆盖丢弃 | 缓存块与数据区同时变脏 | 日志结构的碎片更集中 |
| 数据落地时间 | 不确定,取决于回写时机 | 写完缓存即已落地 | 有持久化时延要求时选直写 |
| 掉电风险面 | 缓存区未回写数据全部易失 | 同样易失,但数据区已有副本 | 直写的一致性负担略轻 |
| 适合的负载 | 间歇写入、总量有限 | 小写入量、强持久化要求 | 按负载与可靠性要求共同决定 |
图2 把日志结构的一次写入画成了五步:主机写请求进来,分配器从空闲块池取一个干净块,按 SLC 方式编程写入缓存区,回写调度器按策略把脏页搬回数据区,按 XLC 方式落盘。下面的三个支撑结构——空闲块池、回写队列、元数据区——任何一个缺失,缓存都无法工作。底下三个方块是这条路径上的三类判断,写到这里可以先记住它们,第 8 章会回到这三点讲一致性。
2.3 缓存命中与回写
「命中」这个词在缓存语境里容易被误解。SLC Cache 的命中不是「主机读到了想要的数据」,而是主机要写的那个逻辑块,此刻恰好有一份有效副本留在缓存区里。读路径上根本用不到 SLC Cache。
一次写入在缓存里的生命周期可以分成这几个状态,控制器必须精确跟踪每一个缓存页处在哪一态:
- 干净(clean):缓存页里的数据与数据区的最新副本一致,可以随时被覆盖或丢弃。这类页不占回写负担。
- 脏(dirty):缓存页里的数据比数据区新,必须回写才能丢弃。脏页数量就是缓存水位线。
- 回写中(in-flight):正在被搬到数据区的页。这段时间里它既不能被覆盖也不能被丢弃,是一致性设计里最敏感的状态。
- 回写完成(flushed):数据已落到数据区,映射已更新,缓存页可以安全释放或转为干净。
「缓存满」这个说法其实不够精确。真正的判据是脏页数量达到阈值,而不是分配不到缓存块。干净页可以随便覆盖,脏页必须先回写。所以在水位线以下,缓存分配永远成功且很快;一旦脏页攒到阈值,新来的写就要排队等回写腾出空间——这就是时延开始抬升的原因。
因为触发回写的是水位线阈值,而阈值通常设在容量的大部分位置(而不是百分之百)。从阈值触发到水位真正顶满,中间还有一段缓冲,这段时间里性能已经开始退化但还没到底。稳态的起点比「缓存标称容量用完」要早。
2.4 回写时机:合并写还是立即写
回写策略决定了稳态抖动的幅度,是缓存调优里收益最明显的一个参数。它回答的问题是:脏页攒到什么时候、由谁来搬。
写满即回写:水位一到就触发,把当前所有脏页一次搬走。实现最简单,回写延迟可预测,代价是形成明显的「锯齿」——搬完立刻又攒,攒完又搬,前台写在这两个动作之间忽快忽慢。
后台合并写:不等到水位线,而是持续在后台按批次搬运,把水位压在低位。写合并更充分(同一逻辑块的多版本更容易凑齐),碎片更少,写放大更低,但回写占用的通道时间更分散,前台写的时延分布更平滑但平均值可能略高。
定时回写:按固定时间片触发,与写入负载无关。适合周期性写入的负载(例如定时上报的终端),对突发负载反而最不友好。
三者的选择有一条实用经验:负载越接近「持续不断」,越应该选后台合并写,因为持续负载下写满即回写会形成规律性的强抖动,而平滑的稳态对上层应用更友好;负载越是「一阵一阵」,写满即回写的可预测性优势越明显。
| 对照项 | 写满即回写 | 后台合并写 | 定时回写 |
|---|---|---|---|
| 触发条件 | 脏页水位达到阈值 | 后台调度持续按批搬运 | 固定时间片到期 |
| 时延稳定性 | 锯齿状,抖动明显 | 平滑,抖动最小 | 周期性波动,与负载无关 |
| 写合并充分度 | 低,一批里同逻辑块版本少 | 高,版本容易凑齐 | 中 |
| 写放大 | 较高 | 较低 | 中 |
| 碎片产生量 | 多 | 少 | 中 |
| 空闲带宽利用 | 差,回写时才用 | 好,持续利用空闲窗口 | 差 |
| 适合的负载 | 间歇、突发型写入 | 持续、长时间写入 | 周期性写入 |
| 实现复杂度 | 低 | 中,需后台调度与限流 | 低 |
表里有一行容易被忽略:空闲带宽利用。后台合并写之所以在持续负载下表现更好,很大程度上是因为它把回写摊进了「本来也没有前台写」的那些时间片。如果负载本身就是满负荷的,后台能利用的空闲窗口很少,后台合并写的优势就会缩水——这时候真正要靠的是减小缓存的必要性(第 12 章)或改变负载形状(第 10 章),而不是调回写策略。
2.5 读路径:缓存未命中怎么办
先明确一件事:读路径不经过 SLC Cache。缓存区里放的是尚未回写的新写入数据,主机读的是逻辑块的最新值。如果最新值已经在数据区(正常情况),直接读数据区;如果最新值还在缓存区(同一个逻辑块被反复覆盖、数据区还是旧版本),那就必须先合并出最新值再返回。
后一种情况是真正需要处理的:它在同一逻辑块被连续多次覆盖时出现。缓存区里堆着这个逻辑块的多个版本,数据区里是最早的那个。主机读它的时候,控制器要么把缓存区的版本合并出来(读放大),要么就地做一次读改写把它敲回数据区(写放大)。两种做法都有代价,而且都发生在关键路径上。
实践中有三种处理方式,按代价从小到大排列:
- 穿透读:直接读数据区的旧版本。因为它可能是旧的,这个选择只在「逻辑块只被写过一次」的常见情形下安全;一旦同一逻辑块被再次覆盖而最新值还在缓存区,就必须读合并,否则返回脏数据。
- 读合并(read-merge):把数据区版本与缓存区版本在控制器内合并,返回最新值,不产生额外写。代价是控制器要处理跨区域的数据拼接,控制器侧要缓存也要有临时空间。
- 读改写(read-modify-write):读出旧版本,与新数据合并后一起写回数据区,同时更新映射。代价是产生一次额外写,收益是把最新值立刻固化到数据区,后续读都不用再合并。
写合并做得好,缓存区的脏版本会被及时合并掉,读合并就不会发生;反过来,如果为了降低写放大而过度延后回写,脏版本在缓存里堆得更高,读合并的触发概率反而上升。所以「回写越少越好」是错的,回写与读合并之间存在直接权衡。
要评估这个开销,最直接的办法是读写混合测试:在持续写入的同时按比例插入读,观察时延与吞吐随混合比的变化曲线。注意必须把读也做成穿透缓存窗口的持续读,否则测的又是冷态。读写混合比的影响在第 9 章 9.4 与第 10 章 10.4 会再展开。
2.6 缓存元数据的管理
缓存区里放的是数据,但真正让它运转起来的是元数据:哪些缓存页是脏的、回写队列排到哪了、逻辑块到物理页的映射是什么、元数据本身写到哪了。缓存带来的绝大部分复杂度都集中在元数据上,而不是数据上。
缓存元数据通常包含四类内容,每一类都有各自的更新频率与可靠性要求:
- 脏位图(dirty bitmap):一个 bit 对应一个缓存页,标记它是否需要回写。这是最频繁更新的元数据,每发生一次写入就可能改一个 bit。
- 映射表(mapping table):逻辑块到物理页号的翻译。这部分与普通 FTL 映射共用,但缓存引入了一个额外的跳转层:逻辑块可能当前指向缓存页,也可能指向数据区页,切换时要更新。
- 回写队列(flush queue):记录待回写页的顺序与批次边界。它必须能容忍中断——回写过程中掉电,队列里的状态要能被恢复。
- 模式状态(mode state):伪 SLC 方案下当前处于哪种模式、哪些块还没切换完成。这一项是伪 SLC 独有的,也是伪 SLC 一致性设计最脆弱的地方。
元数据本身的写入有两个特殊要求:原子性(一次更新要么全生效要么完全不生效,不能只改一半)与先于数据提交(描述数据状态的那条记录必须先落,数据才允许生效)。这两条要求是第 8 章掉电一致性的全部基础。
图3 把本章的内容收成三个正交维度。维度一是缓存形态(固定 SLC 区、伪 SLC 动态切换、无缓存),决定缓存占多大空间、怎么来;维度二是回写时机(写满即回写、后台合并写、定时回写),决定稳态抖动的形状;维度三是数据位置(脏数据全在缓存、已合并进数据区、元数据与数据分处两处),决定掉电时数据可能处在哪些状态。
图下方那段话是本章最需要带走的一句:这三个维度是独立的,单看每一个维度器件往往都是合格的,真正的故障几乎都发生在交叉处。第 8 章会用一个具体的交叉场景(伪 SLC 切换 + 后台合并写 + 掉电)把这条讲透,第 11 章的排查顺序也按这个框架组织。
3. 伪SLC:动态页类型切换
固定 SLC 缓存区要划出专用物理块,这块容量对所有负载都是一样的:轻负载用不到,重负载不够用。伪 SLC(也叫动态页类型切换、pSLC、Flexible SLC)换了个思路——不划专用区,而是让同一批块按需在 SLC 模式与 XLC 模式之间切换。本章讲清它的收益、它的机制,以及它必须付的那笔账。
3.1 伪 SLC 的基本思路
伪 SLC 的核心只有一句话:让全盘(或某个分区)在需要的时候整体变成 SLC 模式,写完再切回去。它不划出专用块,而是把「整盘可编程为 SLC」当成一种临时状态。
这个思路的背景是固定分区方案的两个硬伤。第一是容量按最坏情况设定:为了保证重负载下窗口够长,缓存区必须划得足够大;而在轻负载下这部分空间永远用不上,白白从用户容量里扣掉。第二是缓存区用不掉就是死的:如果这一轮业务只写了缓存区容量的三分之一,剩下的三分之二既不能给用户用(标记为不可用),也不能被缓存逻辑有效利用(没有脏页)。
伪 SLC 把这个问题解决了:全盘都是可写的,缓存区不再是一个物理隔断,而是控制器在任意时刻对「当前有多少块处于 SLC 模式」这一状态的管理结果。轻负载时只把一小部分块切成 SLC 模式,重负载时切成更大比例,用多少占多少。
固定 SLC 区:一块物理空间,永久以 SLC 方式编程,容量固定。伪 SLC:同一批物理块,按需切换编程模式,容量随负载浮动。无缓存:全程按 XLC 编程,没有 SLC 阶段。三者的差别不只是「有没有 SLC」,而是「SLC 这个状态由谁决定、什么时候撤销」。
- SLC 状态的归属:固定在一块专用物理区。由控制器动态分配,可变。不存在 SLC 状态。
- 容量决定因素:初始化时设定,之后不变。由负载与策略决定,浮动。不适用。
- 轻负载下的浪费:未用到的缓存容量被闲置。未用到的部分不切成 SLC,不浪费。无浪费也无收益。
- 重负载下的表现:窗口受限于固定容量。可按需扩大 SLC 范围。无窗口。
- 额外代价:占用物理空间。切换本身要花时间、要重写。无额外代价。
- 实现复杂度:低。高,需模式状态机与切换调度。最低。
- 对寿命的影响:缓存区磨损集中但不均。磨损在全盘摊开,更均衡。无额外磨损。
- 典型落点:容量确定、负载可预估的产品。负载波动大、需要弹性的产品。以读为主或写入量极小的场景。
最后一条值得留意:伪 SLC 在寿命维度上其实有额外收益。因为 SLC 模式只在需要时短暂存在,被切换成 XLC 的块会和普通数据块一样参与磨损均衡,磨损在全盘摊得更开,不会出现「缓存区那一小块先写穿」的情况。这是伪 SLC 相对固定分区的隐性优势,但代价是切换的复杂度与停顿。
3.2 多级缓存的概念
伪 SLC 常常和「多级」这个词一起出现,但它有两种完全不同的含义,必须分清,否则容量算法会算错。
第一种含义是多级 SLC 模式(multi-level SLC):不只有「SLC 模式」和「XLC 模式」两档,而是设计若干个中间档位。每一档对应不同的每单元比特数——比如最激进的档接近 SLC,最保守的档接近 TLC。分档的好处是可以让 SLC 比例连续可调,负载轻时用低档(保守),负载重时用高档(激进);代价是每增加一档,验证工作量与模式状态复杂度都上一个台阶。
第二种含义是多级存储层次(multi-level cache):控制器内部除了 NAND 之外还有 DRAM(SDRAM / eMMC 的确有),形成「DRAM 缓存 → SLC 区 → 数据区」三级结构。这和伪 SLC 是两个正交的东西——多级存储层次解决的是「写 DRAM 比写 SLC 还快」,伪 SLC 解决的是「SLC 状态怎么分配」。
本文档聚焦第一种。两种含义在产品文档里经常混用,看到「多级缓存」四个字要先确认它指的是哪一层。
| 档位设置 | SLC 强度 | 适用负载 | 切换开销 | 验证工作量 |
|---|---|---|---|---|
| 两档(SLC / XLC) | 全有或全无 | 负载特征单一、可预估 | 切换一次覆盖全部块,开销集中 | 低,状态少 |
| 三档(多一档中间态) | 可粗调 | 负载波动中等 | 切换可分批进行,开销可摊 | 中,需验证每档的边界行为 |
| 多档(细粒度连续可调) | 可细调 | 负载波动大、追求弹性 | 切换频繁,摊薄后开销上升 | 高,档间过渡态最易出错 |
| 不分档(全程 XLC) | 无 | 以读为主或写入量极小 | 无 | 最低 |
每增加一档,就多一组「从A 档切到 B 档」时需要验证的边界行为,尤其是切换瞬间掉电的恢复逻辑。档位设计真正的约束不是技术能力,而是验证矩阵的规模。绝大多数量产方案在两档或三档之间取平衡,只有极少数面向特定场景的产品才会做更细的划分。
3.3 模式切换的触发条件
切换什么时候发生,由控制器策略决定。常见的触发条件有四类,它们可以组合使用,但组合越多越难调。
- 脏页水位触发:SLC 模式区域的脏页占比达到阈值。这是伪 SLC 里最常用的触发条件,因为脏页水位直接对应「SLC 区域的写入压力」。它的缺点是阈值难定:定低了切换频繁,定高了切换时一次性要处理的数据量太大,停顿明显。
- 空闲块水位触发:全盘空闲块数量低于阈值时,把一部分块切入 SLC 模式以缓解分配压力。这个条件的意义在于——空闲块不足往往意味着后台回收跟不上,此时用 SLC 模式争取时间。
- 周期触发:按固定周期或按累计写入量间隔切换。实现最简单,行为最可预测,适合负载特征稳定的场景。
- 空闲窗口触发:只在检测到「一段时间没有前台写」时才切换。这是四种里对性能影响最小的,因为它把切换成本藏在业务空隙里。缺点是如果业务一直不停,切换就永远排不上队,缓存实际上处于不可用状态。
四个条件的关系不是并列的。水位类条件决定「什么时候必须切」,空闲窗口条件决定「什么时候能切」。实际策略通常是:水位到了就标记为「待切换」,然后等空闲窗口真正执行。如果空闲窗口迟迟不来,就必须在「等下去」和「牺牲一次前台性能」之间选一个——这个选择直接决定了稳态时延的上限。
| 触发条件 | 判据 | 优点 | 缺点 | 调参要点 |
|---|---|---|---|---|
| 脏页水位 | SLC 区域脏页占比达阈值 | 直接对应写入压力,响应及时 | 阈值难定,过低则频繁 | 阈值取容量的一定比例,随负载校准 |
| 空闲块水位 | 全盘空闲块数低于阈值 | 缓解分配压力,避免写不进去 | 空闲块少往往已说明回收乏力 | 与预留区大小联动评估 |
| 周期触发 | 按时间或写入量间隔 | 行为可预测,易验证 | 与真实负载不同步时浪费切换 | 周期要覆盖典型业务节律 |
| 空闲窗口触发 | 检测到一段时间无前台写 | 对前台性能影响最小 | 业务不停时永远排不上 | 空闲判定窗口的长度设置 |
| 组合策略 | 水位触发 + 窗口执行 | 兼顾必要性与代价 | 两个参数互相影响,调参更难 | 先定水位,再定窗口,最后联调 |
3.4 切换过程的数据位置问题
切换动作本身很简单——把一批块从 SLC 模式改成 XLC 模式。但这两个模式编程出来的页在物理上是不可混用的:一个按 SLC 写出来的页,和一个按 XLC 写出来的页,落到同一个块里,构成的是一个混排的块。这个块后续还能不能正常擦写、能不能正常读,取决于器件与控制器的处理方式。
所以真正的问题不是「怎么切」,而是「正在被写成 SLC 的那批数据,在切换那一刻放在哪里」。三种处理方式:
- 等它回写完再切:把 SLC 区域内所有脏页先按 XLC 写回数据区,全部落盘后再切换。语义最干净,切换点是一个明确的安全边界,掉电恢复逻辑最简单。代价是切换前的等待时间比较长,而且这段时间里 SLC 区域不能接受新写。
- 切完再回写:先把块切成 XLC 模式,再把原本在 SLC 区的脏页按 XLC 写回去。切换点更早,但切换瞬间有一批数据处于「物理模式已变、内容还没重写」的状态,掉电恢复必须能识别并处理这个中间态。
- 分批流水切换:把待切换的块分成若干批,一批一批地切,中间穿插正常读写。停顿时间最短,但状态机最复杂——任何时刻都有一部分块是 SLC、一部分是 XLC,一致性设计必须同时覆盖两种状态的组合。
「数据位置」维度的答案,在切换过程中不是「在缓存区」或「在数据区」这种二选一,而是随切换阶段变化,并且在分批流水方案下同时存在两种状态。如果一致性设计只覆盖了「等它回写完再切」这一种,遇上分批方案就会崩。第 8 章会用一个完整的掉电点矩阵把这些中间态列清楚。
3.5 伪 SLC 与固定 SLC 的差异
把两种方案放在同一张表上逐项对比,差异比「伪 SLC 更好」或「固定 SLC 更简单」这种笼统说法清楚得多。
| 对比维度 | 固定 SLC 分区 | 伪 SLC 动态切换 | 哪一方更优取决于 |
|---|---|---|---|
| 容量利用率 | 低,未用到的缓存容量被闲置 | 高,用多少占多少 | 负载波动程度 |
| 窗口可预测性 | 高,窗口长度由固定容量直接决定 | 中,由策略决定,需要实测标定 | 是否需要可预测的窗口 |
| 冷态时延 | 两者接近,SLC 编程本身就快 | — | |
| 稳态时延 | 低,回写压力集中在固定容量 | 高,切换开销要摊进稳态 | 切换频率与空闲窗口充足度 |
| 实现复杂度 | 低,状态少,边界清晰 | 高,模式状态机 + 切换调度 | 团队验证能力与项目周期 |
| 掉电恢复难度 | 低,状态二值,边界清楚 | 高,中间态多,需覆盖流水切换 | 是否采用分批流水方案 |
| 磨损分布 | 不均,缓存区磨损集中 | 较均匀,全盘摊开 | 寿命预算是否有余量 |
| 对控制器资源的要求 | 低 | 高,切换期间需要额外的调度开销 | 控制器的算力与缓存配置 |
| 适合的负载 | 特征稳定、可预估 | 波动大、需要弹性 | 产品定义阶段能否预估负载 |
表里两行留空的地方值得说明:冷态时延两者基本一致。伪 SLC 不会让冷态更快——它在冷态阶段同样是按 SLC 模式写,收益完全一样。伪 SLC 改善的是容量利用与稳态的适应能力,不是冷态速度。任何宣称「伪 SLC 性能更好」的说法,如果不区分冷态与稳态,都是没有信息量的。
另一个高频误解是「伪 SLC 因为不需要划专用区,所以没有容量损失」。这不对——切换期间受影响块要重写,重写期间设备不可用,这段时间是实实在在的容量与性能损失,只是形式不是「永久扣掉一块空间」,而是「周期性付出停顿」。
3.6 切换的时机代价
切换的代价可以用一个很直观的量来表达:切换占空比——单位写入量里,用于切换的时间占比。这个比值直接决定了伪 SLC 相对无缓存方案还有多少净收益。
推导过程不复杂,但结论值得记住。设缓存能提供的窗口对应的写入量为 W(由 SLC 区域容量决定),切换一次的成本是 C(重写受影响块所需的时间当量)。如果每积累 W 的写入就需要切换一次,那么切换占空比约为 C / W。当 C 相对于 W 很小(大批量切换、切换代价低),伪 SLC 明显优于无缓存;当 C 接近或超过 W,切换吃掉全部收益,伪 SLC 退化成「一直切换、一直停顿」,比无缓存还差。
要提高伪 SLC 的净收益,只有三条路:把 C 变小(分批切换,让一次切换只处理少量块)、把 W 变大(在触发水位与切换之间多攒一些写入)、提高空闲窗口的命中率(让切换尽量落在业务空隙)。第一条属于实现,第二条属于策略,第三条属于负载——而负载是产品定义时就固定的,通常最难改。
实践中最常见的问题是只优化了 W 而忽略了 C:把触发水位调得很高,看起来窗口更长了,但每次切换要处理的数据量也更大,停顿时间随之上升,用户体感反而更差。正确的做法是同时观察窗口长度与切换停顿的绝对时长,找那个让「窗口收益 - 切换停顿」最大的点,而不是让窗口最长的点。
- 低水位 + 立即切换:短。小(每次只处理少量块)。切换频繁,累计停顿可观。负载突发、允许频繁小切换。
- 高水位 + 立即切换:长。大(一次处理大量块)。单次停顿明显,用户感知差。对单次停顿不敏感的后台写入。
- 高水位 + 空闲窗口执行:长。中(成本被摊进空隙)。净收益通常最好。负载有自然间歇的典型业务。
- 低水位 + 空闲窗口执行:短。小。窗口太短,收益有限。轻度缓存需求,低成本实现。
- 不分档、不切换:无。无。退化为无缓存方案。写入量极小或以读为主。
4. 缓存的填充与老化
前两章讲了缓存是什么、怎么工作。本章把注意力放到「时间」这个维度:缓存从空到满要多久、窗口到底有多长、容量为什么会随使用时间下降、以及怎么判断一块盘已经进入稳态。这一章的内容直接决定了性能测试该怎么设计。
4.1 缓存容量与性能的关系
缓存容量与性能的关系不是线性的,这一点必须先说清楚。很多人直觉上认为「缓存翻倍,窗口翻倍」,这只在窗口长度远小于测试样本时成立。
窗口长度由一个比值决定:缓存容量 / 写入速率。写入速率越高,同样的缓存容量撑的时间越短;反之越慢。所以容量与性能的真实关系是:容量决定窗口的绝对长度,而性能曲线上「从冷态到稳态的过渡段有多长」由这个绝对长度决定。
由此推出两条实践结论。
第一,判断缓存够不够,要看业务的工作集大小,而不是看容量数字。如果一次业务事务写入的数据量与缓存容量处于同一量级,那么缓存几乎只能覆盖事务的最初一小段,剩下的全部落在稳态。反过来,如果业务是长时间间歇写入,缓存容量即使不大也能持续命中。
第二,加大容量会同时改善「体验」和恶化「稳态」。第 1 章图1 里那条「平台更高」的曲线就是这件事的量化表达:更多数据被强制在回写那一刻落到更不利的页区,稳态时延上升。这两件事必须一起权衡,只看前半段会做出错误决策。
- 很小(工作集的零头):短,冷态一闪而过。影响很小,基线接近无缓存。极小的写入突发。几乎享受不到缓存收益。
- 中等(与工作集同量级):覆盖事务初段,之后进入稳态。略高于无缓存基线。间歇型、小事务型负载。误把初段当成稳态来评估。
- 较大(数倍于工作集):覆盖多轮事务,窗口明显变长。明显高于无缓存基线。持续写入型负载。稳态平台抬升,寿命消耗增加。
- 很大(接近全容量):覆盖绝大部分写入过程。显著抬升,切换与回写开销突出。以读为主的混合负载。把缓存变成了稳态的负担。
其中最容易踩坑的一种情形是:缓存容量与工作集同量级时,业务跑完前几笔事务性能很好,之后掉下来。测试人员如果只跑了几笔事务就采数,拿到的是冷态数据;如果跑完全部业务,拿到的是混合数据(前半冷态、后半稳态)。只有穿透稳态的样本才是有效的,这条会在第 9 章展开。
4.2 冷启动阶段的缓存填充
冷启动到稳态之间有一段过渡,叫填充阶段。这段时间的性能表现有一个特点:比冷态差,但比稳态好,而且抖动最大。
填充阶段发生了什么,按顺序是这样:
- 脏页开始累积。写入正常落进缓存,脏页数持续上升,此时还没有回写,性能与冷态一致。
- 回写首次触发。脏页达到阈值,后台开始搬数据。前台写此时仍然很快,但通道利用率开始下降。
- 回写与前台写争抢。这是填充阶段最难受的一段:后台回写要读要写,前台写也要写,两者在同一套资源上排队。时延开始被拉长,而且拉长的幅度随脏页水位上升而变化,所以抖动最明显。
- 水位进入稳态区间。回写的吞吐与新脏页的产生速率达到平衡,水位稳定在高位,回写变成常态化的后台活动,前台写的时延也稳定下来——这就是稳态。
这段时间里同时存在「已回写完成」和「正在回写中」两类页,元数据更新最频繁,回写队列处于最长状态。如果掉电发生在这一段,恢复逻辑要处理的状态组合最多。第 8 章的掉电点矩阵会把这一段单独列出来。
4.3 老化过程与有效容量衰减
缓存容量在规格书上是标称值,但实际能用的部分随时间下降。这就是老化。它的成因有三个,重要性递减:
- 保留块被消耗:为了应对坏块与磨损,控制器要预留一部分块作为保留(OP)。随着使用推进,保留块被逐步启用,可用于正常分配的块减少。
- 坏块替换:使用中新增的坏块要用备用块替换,每次替换都消耗一个备用块。备用块池是有限的,用完之后只能降低预留比例或降低性能。
- 保持期风险:长期不通电的块,数据保持能力下降。为了保证数据可靠,控制器需要定期读取校验、必要时刷新重写。这部分不减少可用容量,但会占用通道,间接影响稳态时延。
三个来源的共同效果是:标称容量不变,有效容量下降,窗口随之变短。这一点在规格书里通常不会体现,但它对验收有直接影响——一块新盘和一个用了一段时间的盘,缓存窗口长度可能差出不少,而它们的稳态性能是一样的。
| 老化来源 | 消耗什么 | 对有效容量的影响 | 对窗口的影响 | 可否通过使用方式缓解 |
|---|---|---|---|---|
| 保留块启用 | 预留的 OP 块 | 线性减少 | 窗口按比例缩短 | 不可,只能靠初始预留量覆盖 |
| 坏块替换 | 备用块池 | 阶梯式减少 | 窗口阶梯式缩短 | 不可,取决于器件本身 |
| 保持期刷新 | 通道时间而非容量 | 不减少 | 不缩短,但时延上升 | 可,避免长期不通电即可 |
| 静态磨损均衡搬移 | 通道时间 | 不减少 | 不缩短 | 可,冷热分离可减少搬移 |
| 后台回收 | 通道时间 | 不减少 | 不缩短 | 可,给足空闲窗口 |
老化是必然发生的,它的深度由寿命预算决定:预算越大,被消耗到寿命末段时老化越明显。所以「窗口长度」本质上是一个随寿命消耗而变化的量,规格里写一个固定的窗口长度只在寿命早期成立。如果一个产品需要在整个寿命周期内保持稳定的窗口表现,那么窗口长度应当按寿命末段的状态来设计和验收,而不是按新盘来设计。
4.4 稳态的判定方法
稳态判定听起来是个简单问题——看曲线变平就完了——但实际操作中有三个坑。下面给出四种可用的判定方法,按可靠性排序。
| 判定方法 | 做法 | 可靠性 | 局限 | 推荐场合 |
|---|---|---|---|---|
| 写入量穿透法 | 确保累计写入量达到缓存容量的若干倍后再取数 | 高 | 需要知道缓存容量 | 性能验收、竞品对比 |
| 时延曲线拐点法 | 画出时延随写入量变化的曲线,取拐点之后的数据 | 中 | 拐点位置受采样粒度影响 | 研发阶段定位窗口长度 |
| 稳态斜率法 | 取曲线末段的线性拟合斜率,斜率接近零即为稳态 | 高 | 需要较长的样本 | 长跑测试、寿命测试 |
| 方差收敛法 | 观察时延的方差不再增大即为稳态 | 中 | 受负载波动干扰 | 负载波动大的场景 |
三个实践中的坑:
- 采样粒度太粗。如果每 10% 采一个点,拐点附近的三四个点会被平均掉,看起来曲线是平滑上升而不是「先平后升再平」。正确做法是把采样粒度加密到拐点附近能分辨的尺度。
- 负载在变。如果测试期间负载本身在变化(比如越测越重),时延的上升可能是负载造成的而不是缓存耗尽造成的。对照做法是同时记录负载参数。
- 把方差收敛当成稳态。方差变小不代表时延水平稳定,一个缓慢上升但很平滑的曲线方差也很小。判定稳态必须看均值是否还在上升。
连续三个等长区间内,写入时延的区间均值之差都小于第一个区间均值的百分之五,且趋势不显著向上——满足这两条,可以认为进入稳态。这条标准不需要预先知道缓存容量,适合在验证阶段快速判断。
4.5 预热手段
预热的动机很直接:既然冷态只存在于系统刚上电的那段时间,那就在正式业务开始之前人为地把缓存填起来,让业务从一开始就在稳态区间运行。
常见的预热手段有三种,强度递减:
- 厂商预置命令:控制器提供一个专用的预热命令,内部按既定策略填充缓存区而不写入用户数据。这是最应该优先使用的方式,因为它不会消耗用户的寿命预算(预热写的是厂商预留区),也不会产生用户可见的垃圾数据。
- 写入填充文件:主机写一个足够大的临时文件把缓存填满,然后删除。实现简单,但会消耗寿命预算,而且如果文件没有正确删除(TRIM 没有下发、文件系统保留了元数据),会留下垃圾数据。
- 业务模拟加载:直接跑一遍真实业务的镜像加载过程。效果最接近真实场景,但耗时最长,且在业务本身就是低频写入时收益有限。
预热能做什么、不能做什么,第 3 章图6 底部已经给过一句话总结:预热能跳过冷态,不能降低稳态平台。这个限制必须讲清楚,因为很多方案会把「加预热」当成性能问题的解法,而它解决的是另一个问题。
- 厂商预置命令:不消耗用户预算。否。短。高,优先使用。
- 写入临时文件后删除:消耗,与真实写入同等计费。有,依赖 TRIM 与文件系统配合。中。中,验证阶段可用。
- 真实业务镜像加载:消耗,与真实写入同等计费。否。长。中,贴近真实但成本高。
- 反复切换模式:消耗,且切换本身有额外写。否。中。低,对多数场景是负收益。
用写文件的方式预热时,如果只写了数据但没有正确删除,缓存虽然被填满了,但用户可用容量少了一大块,而且这部分空间在文件系统层面可能还是「已分配」的。结果是:预热达到了效果,但留下了比预热本身更严重的问题。用这种方式预热必须完整走一遍「写入 → 删除 → TRIM 下发 → 确认释放」的流程。
4.6 容量与性能的折中
把本章的内容收到一处:缓存容量的选取是一个三方折中,而不是「越大越好」或「够用就好」这么简单。
| 折中维度 | 加大容量的收益 | 加大容量的代价 | 决策依据 |
|---|---|---|---|
| 用户体验 | 窗口更长,冷态覆盖更多业务 | 稳态平台抬升,掉速来得更晚但更痛 | 业务写入模式与用户对时延的敏感度 |
| 寿命预算 | 磨损在全盘摊开,分布更均匀 | 缓存区写入量增加,回写总写量上升 | 目标寿命与写入负载的量级关系 |
| 可用容量 | 伪 SLC 形态下容量利用率更高 | 固定分区形态下直接占用可用空间 | 产品定位与容量档位规划 |
| 实现复杂度 | 策略可更简单(窗口够长,不敏感) | 状态更多(伪 SLC 切换、调参空间大) | 项目周期与验证资源 |
| 一致性风险 | 切换频率可降低,中间态减少 | 分批切换需要更复杂的一致性设计 | 产品对掉电一致性的等级要求 |
从这张表能看出一条实用经验:当业务写入量与缓存容量处于同一量级时,缓存带来的净收益很小,而它引入的复杂度是固定的。这种情况下不加缓存、或者用小缓存只吸收最尖锐的突发,往往是更合理的选择。这一判据在第 12 章会展开成可执行的选型清单。
最后强调本章的一个核心结论,它是整篇文档反复用到的那句话的具体化:窗口长度不是一个常数。它随缓存容量变化、随写入速率变化、随老化程度变化、随回写策略变化。任何在文档里写「缓存提供 X 容量窗口」而不标注这些前提的说法,都是不完整的。
5.1 稳态写入的真实路径
稳态下缓存不再提供任何缓冲,主机的一次写入要自己承担 XLC 的全部代价。这条路径可以拆成五步,每一步都在消耗时间或消耗寿命预算。
- 分配数据区空闲块。控制器要在数据区里找一个空闲块。稳态下空闲块紧张,这个分配本身可能触发一次回收才能完成。
- 读合并(如果需要)。如果这个逻辑块的数据区旧版本仍然有效(意味着这个逻辑块被覆盖写过),必须先读出来才能丢弃。
- 按 XLC 编程新页。这是最贵的一步,耗时与页型(高页区更贵)和数据内容相关。
- 旧页失效。更新映射,旧页变成垃圾。有效页比例随之下降。
- 触发或等待回收。垃圾堆积到一定程度必须回收,回收要搬移有效页,产生额外写与额外读。
把上面五步的时间加总,稳态时延约等于分配等待 + 读合并 + XLC 编程 + 回收排队。其中回收排队通常是最大的可变项,因为它取决于系统里有多少别的写操作在竞争。这解释了一个常见现象:同一块盘在「只有我一个人在写」和「盘上还有别的后台任务在跑」两种情况下,稳态时延可以差出很多——差别不在盘本身,在竞争。
| 步骤 | 稳态下的耗时量级 | 受什么影响 | 能否优化 | 优化手段 |
|---|---|---|---|---|
| 分配空闲块 | 短,但可能隐含等待回收 | 空闲块水位、预留区大小 | 部分 | 加大预留区、降低碎片 |
| 读合并 | 中,取决于是否需要 | 逻辑块是否被覆盖写 | 部分 | 提高写合并度、减少覆盖写 |
| XLC 编程 | 长,且是主要项 | 页区位置、数据内容、器件代际 | 有限 | 引导顺序写到低页区 |
| 旧页失效 | 极短 | 无 | 否 | — |
| 回收排队 | 可变,最大项 | 并发写压力、回收策略 | 部分 | 空闲窗口回收、限流 |
5.2 缓存满时写操作走向
缓存满了不等于写不进去。控制器至少有三条路可以走,选择哪一条决定了稳态时延的形状。
最常见的是排队等回写:新来的写挂起,等控制器把对应的缓存脏页回写完,腾出空间后再分配。这条路径的时延等于「回写耗时 + 本次写入耗时」,是三条里时延最长但写放大最低的。
第二条是绕过缓存直写数据区:新数据直接分配到数据区,完全不管缓存。这条路径时延中等,但它放弃了合并的机会——原来可以在缓存里合并的多次写,现在每次都要单独落盘,碎片直接产生。
第三条是合并等待:把针对同一逻辑块的多次写合并后再回写。这条路径写放大最低,但要读旧版本,读放大在这里产生,而且如果后续没有同块写,合并等待就变成了纯粹的延迟。
- 排队等回写:高。低。中。少。默认路径,多数情况最稳妥。
- 绕过缓存直写:中。高。无。多。缓存全满且回写跟不上时。
- 合并等待:中到高。最低。高。最少。同一逻辑块被反复覆盖写。
- 直接返回失败:仅在极端耗尽时,不应作为常规策略。
如果控制器的策略是「缓存满就拒绝写入」,那么业务在持续写入时必然在某一点崩溃。稳态的正确含义是「变慢但正确」,不是「拒绝服务」。一个缓存方案如果在满缓存下会返回错误,那它就不是一个稳态方案,而是一个只在短时突发下可用的方案。
5.3 读放大与写放大的耦合
图8 画的是本章最重要的一条因果链。它的起点是回写,终点是前台时延,中间每一步都在放大代价。这条链的意义在于:缓存在改善时延的同时,正在损害寿命。这两个方向必须一起权衡。
把这件事量化一下比较好理解。设缓存带来的写放大增量是 1(同一份数据写两遍),而无缓存方案的写放大是 1。那么加上缓存之后,主机写1 单位数据,盘里实际写 2 单位,寿命预算按 2 计。如果一个产品的寿命预算是按 1 设计的,那么引入缓存之后实际寿命只有标称的一半。
当然实际情况没这么极端,因为缓存通常只在窗口期生效,稳态期并不会写两遍。真正需要算的是整个生命周期内的平均写放大:窗口期内约等于 2,稳态期内约等于 1,把两者按写入量加权平均,才是寿命核算该用的数字。
| 阶段 | 缓存状态 | 每次主机写对应的实际写 | 读放大来源 | 寿命消耗 |
|---|---|---|---|---|
| 冷态 | 未满 | 约 1 次(全部进缓存) | 无 | 低 |
| 过渡期 | 接近满 | 约 2 次(缓存 + 回写) | 合并时的读 | 高 |
| 稳态 | 已耗尽 | 约 1 次以上(含回收搬移) | 回收前的搬移读 | 中到高 |
| 全生命周期加权 | 混合 | 介于 1 与 2 之间,需实测 | 混合 | 按加权值核算 |
表里最后一行是实践中最容易出错的地方。不能因为「稳态下不写两遍」就认为缓存不影响寿命——过渡期的双写是真实发生的,它消耗的 P/E 次数不会因为后面进入稳态而退还。正确的做法是按生命周期累计写入量做加权,而不是只看某个稳态时刻的瞬时状态。
5.4 稳态下的平均时延
稳态时延不是一个数,而是一个分布。评估它至少要看三个维度:平均值、抖动(方差或高分位)、以及尾延迟(最慢的那部分)。
- 平均时延:反映整体吞吐能力。它对回写策略不太敏感,对回收压力最敏感。
- 抖动:反映体验的稳定性。它对回写策略最敏感——写满即回写会产生明显的锯齿,后台合并写会平滑得多。
- 尾延迟:反映最坏情况。它主要由回收触发决定:一次前台 GC 会让一批写同时等待,尾延迟直接被拉到毫秒甚至十毫秒量级。
这三者的取舍很典型:追求平均时延会倾向于加大合并、减少读;追求尾延迟会倾向于把回收摊开、避免前台阻塞。两者方向相反,必须根据产品定位选。
- 平均时延:回收压力、预留区大小。加大预留区、降低碎片。只看平均值会掩盖严重抖动。
- 抖动(方差):回写策略、回写批量。改后台合并写、加大批量。平均值好看但体验很差的情况很常见。
- 尾延迟(高分位):是否发生前台 GC。把回收摊到后台、避免前台阻塞。尾延迟差但平均值不差,容易漏测。
- 吞吐:通道利用率、并发度。提高并发、合理调度。吞吐高不代表时延可接受。
一个常见的产品事故是:实验室里测平均时延很漂亮,曲线很平,上线后业务反馈「偶尔卡一下」,最后定位到满缓存下的前台 GC。这种问题只看平均值永远发现不了,因为被卡住的那批写在平均里被稀释掉了。验收指标里至少要有一个高分位时延指标(比如 99 分位),并且要在满缓存状态下测。
5.5 稳态性能与标称性能的差距来源
规格书上的写入速度是峰值,它对应的是冷态、是顺序写、是块对齐、是页区最有利的位置、是寿命早期。这五个前提里任何一个不成立,实测值都会明显低于标称。
| 差距来源 | 标称值的前提 | 实测值的现实 | 差距量级 | 能否消除 |
|---|---|---|---|---|
| 缓存窗口 | 缓存充足,全程命中 | 窗口耗尽后按XLC 写 | 通常最大的一项 | 不能,只能推迟 |
| 负载形状 | 块对齐的大块顺序写 | 小块随机写、写合并需求高 | 一到两个数量级 | 部分,靠主机侧对齐 |
| 页区位置 | 顺序写引导到低页区 | 随机写消耗低页区后落到高页区 | 中等 | 部分,靠写排序策略 |
| 并发度 | 满队列深度、无排队 | 队列深度不足时通道空转 | 中等 | 能,靠主机侧调优 |
| 寿命阶段 | 寿命早期,误码率低 | 寿命末期,编程脉冲变多、读重试增加 | 中等 | 不能 |
| 温度 | 常温 | 高温下编程与擦除都变慢 | 视热设计而定 | 部分,靠散热 |
| 后台负载 | 无其他任务 | 文件系统日志、TRIM、回收同时进行 | 波动大 | 部分,靠限流 |
把差距按大小排序,「缓存窗口」通常是最大的一项,这正是本文反复强调的。原因也很直白:其他几项差距是「小幅度打折」,缓存窗口是「从打折变成另一个价格」——冷态与稳态之间的差距往往比所有其他因素加起来还大。
这也解释了一个现象:为什么有些产品在实验室里跑分很漂亮,量产后被大量投诉。跑分用的是短样本的顺序写,量产的负载是长样本的混合写,两者根本不是同一个工况。
5.6 稳态被打破的典型事件
稳态不是永久的。以下事件会让已经进入稳态的设备短暂回到「更好」或「更差」的状态,排查性能问题时必须逐条排除。
- 大文件集中写入:碎片骤增,回收压力上升。写入期间。看时延曲线是否与写入时段重合。易复现。
- 大量文件删除:TRIM 下发使旧块可回收,回收量激增。删除后一段时间。对比删除前后的时延与写入量。易复现。
- 文件系统日志回写:后台写入挤占通道。周期性尖峰。观察时延是否周期性尖峰。易复现。
- 掉电重启:回到冷态,性能变好。一个窗口期。重启后立刻测,窗口明显变好。易复现。
- 温度变化:高温使编程与擦除变慢。与环境相关。对照温度记录。视环境。
- 后台刷新(保持期):占用通道,时延上升。后台周期性。无前台写时也能观测到。较易复现。
- 预留区被消耗:可用块减少,回收更频繁。随寿命推进。查空闲块水位是否持续下降。不可快速复现。
其中最有用的一条是掉电重启:重启会让设备回到冷态,缓存重新为空。如果发现设备性能变差后重启立刻恢复,那么问题几乎可以确定在稳态路径上(回收压力、碎片、预留区),而不是器件本身。这是一个成本极低、区分度极高的排查手段,第 11 章的决策顺序里会把它放在前面。
6.1 回写产生的碎片
碎片产生的机制在第 3 章讲过原理,这里讲量化。回收的搬移量约等于块内有效页数,所以有效页比例决定了一次回收的代价。这个比例有一个经验规律:当有效页比例高于某个阈值时,回收是划算的;低于阈值时,回收频繁但不划算,控制器通常会选择延后。
缓存让碎片产生的时点变得更集中。原本均匀分布的写入,在缓存窗口内被攒成大批,然后一次性回写——这批回写在很短时间内产生大量失效页。这有两个后果:
- 空闲块消耗速度出现脉冲。稳态下空闲块水位不是平滑下降,而是阶梯式下降。
- 回收压力出现脉冲。水位一下降就触发集中回收,前台写被迫排队。
这两个脉冲叠加,就是第 5 章说的「满缓存下时延抖动」的物理来源。
| 碎片形态 | 成因 | 有效页比例 | 回收代价 | 缓解手段 |
|---|---|---|---|---|
| 均匀老化 | 顺序覆盖写,旧页逐个失效 | 缓慢下降 | 低且平稳 | 无需处理 |
| 时间集中 | 缓存攒批后一次性回写 | 阶梯式下降 | 脉冲式,可预测 | 摊薄回写批次 |
| 空间集中 | 同一区域被反复覆盖 | 局部骤降 | 局部回收压力极大 | 写均衡、打散热点 |
| 伪碎片 | 数据有效但版本过旧 | 比例不高 | 低 | 版本回收机制 |
6.2 缓存与垃圾块的绑定
有一个容易忽略的耦合:缓存块的消耗速度与垃圾块的产生速度是绑定的。原因在于它们竞争同一批空闲块。
具体来说,缓存需要一个空闲的、已擦好的块来承接新的写入;垃圾回收需要一批空闲块来安置搬移出来的有效页。两者抢同一批块。所以当缓存脏页快速累积、需要大量回写时,回收能拿到的块就少了;反过来当回收正在密集搬运时,缓存能拿到的块也少了。
这个耦合在稳态下会形成一个微妙的平衡:脏页水位高→ 需要回写 → 回写产生垃圾 → 需要回收 → 回收消耗空闲块 → 空闲块不足 → 缓存分配也紧张 → 写得更慢。整个系统在「变慢但不至于停」的边界上运行。
缓存区与数据区不应该共用同一个预留区。如果共用,缓存的写入压力会挤占数据区的预留,导致回收提前启动,表现为「明明还有容量,稳态时延却上来了」。合理的做法是给缓存单独留一部分预留,或者在容量规划时明确把缓存的占用算进去。
- 空闲块总量:干净块承接新写。空闲块安置搬移页。两边都分配不到,写入停滞。加大总预留。
- 空闲块时效:块要马上能用。块可以等。缓存抢占回收的块。为缓存预留专属块。
- 通道时间:前台写要快。后台搬移要通道。前台被后台挤占。把回收限制在空闲窗口。
- 预留区归属:缓存老化要消耗预留。坏块替换要消耗预留。预留被提前耗尽。分别核算两者的预留需求。
6.3 动态负载下的回收压力
前面讲的都是写入强度不变的情况。真实负载是动态的:有时候猛写,有时候停;有时候大块顺序,有时候小块随机。这种波动会让回收压力变得难以预测。
最难处理的是负载由顺序突然转为随机的情况。顺序写时低页区被高效利用、碎片少、回收轻;一旦转为随机写,低页区迅速消耗完,写入被迫落到高页区,同时碎片大量产生,回收压力陡增。此时缓存如果已经耗尽,就没有缓冲手段了,性能会断崖式下降。
| 负载变化 | 对缓存的影响 | 对回收的影响 | 性能表现 | 预警信号 |
|---|---|---|---|---|
| 顺序→随机 | 窗口内影响不大,窗口后低页区耗尽 | 碎片骤增,回收频繁 | 稳态时延断崖式上升 | 时延曲线出现拐点 |
| 低负载→高负载 | 窗口迅速被消耗 | 回收压力缓升,可预测 | 先短暂变好再变差 | 窗口缩短 |
| 高负载→低负载 | 重新积累窗口 | 压力下降 | 性能回升 | 时延回落 |
| 稳态→大量删除 | 无直接影响 | TRIM 使大量块可回收 | 短时回收量激增 | 删除操作后的时延尖峰 |
| 持续不变 | 窗口固定消耗 | 压力稳定 | 性能稳定在平台 | 曲线平坦 |
表里第二行有个反直觉的地方:负载变重时性能可能先变好。原因是负载轻的时候缓存可能一直用不满,数据一直留在缓存区不被回写,一旦负载变重,缓存被快速利用,反而把低页区的优势用上了。所以「性能变好」不一定是好事——它可能只是意味着缓存快被用完了。
6.4 缓存区本身的磨损
缓存区自己的磨损是一个容易被忽略的成本。缓存区里的每个块都被高频反复写入——一个循环周期内所有块各被写一次,然后再来一轮。这个速率通常高于数据区的平均磨损速率,因为数据区里的块写完一次之后往往要等很久才会被再次覆盖。
后果有两个。第一,缓存区会先于数据区出现高磨损块,触发更多坏块替换,消耗预留块。第二,如果磨损不均,缓存区的有效容量会不均匀地下降——不是所有块同时变差,而是部分块先不行,导致可用的「干净且低磨损」的块越来越少。
| 磨损维度 | 数据区 | 缓存区 | 谁更差 | 应对方式 |
|---|---|---|---|---|
| 单位时间 P/E 次数 | 低,写完一次间隔长 | 高,循环写入 | 缓存区 | 循环指针 + 磨损均衡 |
| 磨损分布 | 按数据热度自然分布 | 若随机分配则高度集中 | 取决于策略 | 循环或磨损感知分配 |
| 高磨损块出现时间 | 晚 | 早 | 缓存区 | 为缓存单独预留替换块 |
| 对预留块的消耗 | 主要来自坏块 | 坏块 + 磨损双重消耗 | 缓存区 | 分别核算,必要时隔离 |
6.5 循环缓存与磨损均衡
循环分配是缓存区磨损均衡的经典做法:维护一个写指针,绕着缓存区一圈一圈推进,每个块被均匀使用。它的实现代价很低(一个指针),效果很直接。
但循环分配有一个副作用值得注意:它破坏了「刚回写完的块」与「新写入的块」之间的空间分离。循环推进意味着刚刚被回写过(因此腾空)的块,很可能马上又被分配到新的写入。这样一来,写入会在缓存区里形成一条越来越长的「新旧交替」序列,局部碎片程度上升。
更完善的方案是磨损感知分配:维护每个块的磨损计数,分配时优先选磨损最低的块。这能把老化摊得最平,但需要一张磨损计数表,占用控制器的存储资源,并且每次分配都要查表,分配路径变长。
| 分配策略 | 磨损均匀性 | 碎片程度 | 控制器开销 | 推荐场合 |
|---|---|---|---|---|
| 随机分配 | 差 | 中 | 最低 | 不推荐,仅最简单的方案 |
| 循环分配 | 好 | 略高 | 低(一个指针) | 容量紧张的中小容量方案 |
| 循环 + 保留间隔 | 好 | 低 | 低(指针 + 简单判据) | 较优的折中,推荐 |
| 磨损感知分配 | 最好 | 低 | 高(磨损表 + 查表) | 大容量、长寿命要求高的方案 |
如果控制器资源紧张,「循环 + 保留间隔」通常是性价比最高的折中:用循环指针保证磨损均匀,同时规定「刚被回写过的块要隔开N 个位置才能再次分配」,用一条简单规则换回大部分的碎片抑制收益,实现代价几乎为零。
6.6 缓存耗尽与预留区的关系
本章最后一节讲一个容易被当成「器件故障」的现象:盘还有空间,但性能突然变差。
原因通常是预留区被消耗到了临界点。当空闲块(包括预留)低于某个阈值时,控制器必须做选择:要么频繁回收(性能下降),要么回收得更激进(可能触发前台 GC,时延尖峰),要么降低某些后台活动(数据保持刷新被推迟,长期可靠性风险)。三种选择都会牺牲某一个方面的性能或可靠性。
判断方法很简单:查空闲块水位。如果水位已经很低,那么性能下降不是缓存的问题,是容量预留的问题。解决办法不是加大缓存,而是加大预留区或者减少写入量。
| 空闲块水位区间 | 系统状态 | 缓存表现 | 回收表现 | 应对策略 |
|---|---|---|---|---|
| 充裕 | 健康 | 窗口充足 | 回收轻松 | 正常 |
| 中等 | 正常 | 窗口缩短,回写变频繁 | 回收压力上升 | 监控,暂不干预 |
| 偏低 | 紧张 | 窗口很短,缓存形同虚设 | 回收被迫前台执行 | 加大预留区,减少写入 |
| 接近耗尽 | 临界 | 缓存完全不起作用 | 时延尖峰明显,可能返回错误 | 必须扩容或改变负载 |
缓存对寿命与回收的影响是单向的负面:它不会缓解写放大,只会让写放大更高、碎片更集中、回收更早被触发。因此在评估一个缓存方案时,时延改善和寿命消耗必须放在同一张表里比较——只看时延就接受一个写放大明显上升的方案,通常会在寿命后期付出代价。第 12 章会把这条原则落成具体的检查项。
7.1 内置控制器方案的典型实现
内置控制器方案(SD NAND、eMMC、UFS)把 FTL 和缓存逻辑都放在封装内的控制器里,主机看到的是一个纯粹的块设备。这条线上的缓存有三个典型特征。
第一,缓存放在同一 die 上,不占封装面积之外的成本,容量按块数划分。第二,控制器有片上 SRAM 甚至片外 DRAM,可以做多级缓存(DRAM 缓存 → SLC 区 → 数据区),这是另外两条线做不到的。第三,主机完全无法感知缓存的存在,容量、策略、开关全部由控制器固件决定。
第三个特征对产品设计既是优点也是限制。优点是体验一致:同一颗料在不同主机上表现相同,不需要每家主机都调一遍。限制是失去了按产品定制缓存策略的能力——如果某个应用场景需要更长的窗口或更平滑的时序,只能靠选不同的料来解决。
- 缓存位置:与数据区同die,按块划分。不增加封装成本。容量按块数计量,换算时要考虑页块大小。
- 元数据存放:片上 SRAM 或预留的元数据块。元数据写入极频繁,需要低时延。元数据区本身要纳入掉电保护设计。
- 多级结构:DRAM 缓存 + SLC 区 + 数据区。片上资源允许,成本可控。级数越多,一致性状态组合越多。
- 策略调整:由固件与厂商预设。主机不可见。无法按主机负载定制,只能靠选型。
- 容量设置:初始化时按分区或全盘确定。需要主机侧配合初始化流程。分区大小会影响缓存的实际可用量。
7.2 SPI NAND 的缓存形态
SPI NAND 是外挂式接口,主机通过 SPI 直接驱动。这条线上「缓存」有两种截然不同的含义,必须分清:器件内部的 SLC Cache(如果这颗料有)和主机侧的写缓存(软件实现)。
器件内部的 SLC Cache 与内置控制器方案里的缓存机制相同,区别只在于它由器件自己管理,主机发命令时完全不知道。这带来一个实际问题:主机无法判断当前是否处于窗口期,所以测试时必须自己想办法穿透稳态(比如写入远超缓存容量的数据)。
主机侧的写缓存是另一回事:主机在内存里先攒一批写,攒够了再批量下发。这层缓存能合并小写、吸收突发,但它不改变盘里的写入代价,只改变下发节奏。很多 SPI NAND 方案的性能提升实际上来自这层主机缓存,而不是器件的 SLC Cache——评估时必须把两者分开,否则会高估器件本身的能力。
| 缓存层次 | 由谁管理 | 对性能的作用 | 对寿命的作用 | 验证方式 |
|---|---|---|---|---|
| 器件内SLC Cache | 器件内部固件 | 窗口内降低写入时延 | 增加一次回写,P/E 上升 | 写入远超缓存容量后对比 |
| 主机写缓存 | 主机软件 | 合并小写,吸收突发 | 把多次逻辑写合成一次物理写,P/E 下降 | 改缓存大小观察批量大小变化 |
| 主机读缓存 | 主机软件或系统 | 降低读时延 | 无直接影响 | 与写无关,单独评估 |
| DRAM 写缓冲 | 主机或控制器 | 缓冲突发写 | 同主机写缓存 | 调整缓冲深度观察 |
把「主机写缓存带来的提升」当成「器件 SLC Cache 带来的提升」。两者的验证方法完全不同:关掉主机写缓存再测一次,如果性能大幅下降,说明之前的提升主要来自主机缓存而不是器件;反之如果几乎不变,说明器件缓存才是主力。这个对照实验成本极低,但能避免选型时的严重误判。
7.3 并行 NAND 的缓存形态
并行 NAND 指多通道、多 die 并行的方案(uFS、eMMC 高速档、企业级 SSD 形态)。这条线的特点是带宽优先,缓存的设计目标与前两条线不同:不是为了降低单次写时延,而是为了在高并发下把通道填满。
常见的缓存形态是以控制器侧 DRAM 为主的结构:NAND 阵列负责大容量存储,DRAM 负责吸收写入与维护映射表。DRAM 的访问时延比 NAND 低一到两个数量级,因此它天然是「高速层」。在这种结构里,SLC 模式区域的作用从「降低时延」变成了「减少 NAND 侧的写入次数」——两者目标不同,评估口径也不同。
另一个重要差异是并发。多条通道并行工作时,单次写时延可能并不比单通道方案更慢(因为通道之间可以流水),但聚合带宽高得多。这意味着评估并行方案时,单次时延的参考价值下降,聚合吞吐与尾延迟的参考价值上升。用单次写时延去比较并行方案与单通道方案,是没有意义的。
- 主要评估指标:窗口内时延 + 稳态时延。窗口内时延 + 稳态时延。聚合吞吐 + 尾延迟。并行方案看聚合,单线看单次。
- 并发的影响:有限,主机侧并发低。有限,主机侧并发低。极关键,通道数决定上限。并发不足则通道空转。
- DRAM 的角色:可选,做多级缓存。通常无外挂 DRAM。核心,映射表与写缓冲都在里面。决定容量与成本。
- SLC 模式区的角色:降低时延。降低时延。减少 NAND 侧写次数。目标不同,评估口径不同。
- 缓存容量单位:块。块或字节。字节。跨线比较必须先换算。
- 一致性责任边界:控制器全责。主机与器件分担。控制器全责。掉电定位难度不同。
7.4 无缓存方案的存在场景
缓存这么好,为什么还有无缓存的方案?因为在某些场景下,缓存是纯粹的负担。四种典型情况:
- 写入量极小:整个生命周期写入的数据量都没有超过缓存容量,那么缓存从头到尾都用不满,等于白白占用了容量。典型的例子是只写少量配置数据的设备。
- 以读为主:缓存只优化写路径,读路径根本不走缓存。对只读或读远多于写的负载,缓存零收益。
- 一次性批量写完就结束:比如固件烧录、设备出厂初始化。这类负载的特点是「写完就不再写」,缓存的窗口期覆盖了整个任务,收益等于「整个任务都按 SLC 写」,看起来很好——但如果任务是烧录大容量镜像,这个收益会被「缓存被写满后还要回写」抵消大半。
- 极小容量:容量小到缓存只能占百分之几,此时窗口短到没有实际意义,实现复杂度却一分不少。
| 场景 | 为什么缓存无收益或收益很小 | 更好的选择 | 判据 |
|---|---|---|---|
| 生命周期写入量远小于缓存容量 | 缓存永远用不满,容量被闲置 | 无缓存,把容量还给用户 | 比较全生命周期写入量与缓存容量 |
| 读远多于写 | 读路径不走缓存 | 把资源投给读优化 | 统计读写比 |
| 一次性小量写入 | 窗口覆盖全任务,但绝对收益有限 | 无缓存,简化控制器 | 单次写入量是否超过缓存容量 |
| 极小容量器件 | 窗口短到无意义 | 无缓存 | 缓存占容量的比例是否过低 |
| 大容量一次性烧录 | 窗口收益被回写抵消,寿命多耗一倍 | 无缓存,或用顺序写优化 | 任务结束后是否还在意时延 |
| 持续写入的长生命周期设备 | 窗口很快耗尽,收益归零 | 缓存,但要按稳态核算寿命 | 写入持续时间与总量 |
如果一个方案在纸面上「有缓存」但你找不到任何一段真实业务能完整用满这个缓存,那这个缓存大概率不该加。缓存的价值完全来自「窗口被用掉的那部分写入」,如果业务永远用不满窗口,那部分价值就永远不会被兑现。
7.5 缓存对控制器复杂度的影响
缓存不是一个「加个模块」的事情。它会同时增加状态空间、调度路径和验证矩阵三方面的工作量。这三项是分开算的,因为它们落在不同的人力上。
- 状态空间:干净/脏/回写中/已落盘 四态,加上伪SLC 的模式状态。状态组合爆炸,中间态覆盖不全。减少并行状态,采用顺序切换。
- 调度路径:回写调度、回收调度、前台写调度的三方博弈。前台被后台无限期饿死。给后台设限流与配额。
- 元数据:脏位图、映射表、回写队列、模式状态四类元数据。元数据本身的原子性与掉电保护。统一元数据格式,减少分散写入。
- 异常处理:回写中断、切换失败、回收失败。异常路径的恢复逻辑难以验证。异常必须可重入,重试必须幂等。
- 验证矩阵:每个维度两两组合 × 每个掉电点。验证规模随维度指数增长。用状态覆盖度工具辅助,而非穷举。
其中一条建议值得展开:不要靠穷举来覆盖状态组合。缓存的状态组合数是乘法增长的,三个正交维度各取三态就已经有二十七种组合,再加上掉电点,穷举不可行。正确的做法是按状态覆盖度来设计用例——确保每一种状态转移都至少被覆盖一次,而不是枚举所有路径。
另一个常被低估的成本是异常路径。正常路径好测,异常路径(回写被打断、切换失败、回收失败)才是真正的工作量所在。设计阶段就要确认:这些异常是可重入的吗?重试是幂等的吗?如果不是,一致性设计就有洞。
7.6 缓存对启动时间的影响
启动时间是缓存最容易被忽视的一个影响面。缓存对启动的影响是双向的:它让启动阶段的批量写变快,也让某些场景变慢。
变快的一面很直观:启动时要加载固件、拷贝镜像、写引导记录,这些写入全部发生在窗口期,时延比稳态低得多。对于启动时间敏感的嵌入式设备,这个收益相当可观——有缓存和没缓存的启动时间可能差出可观的比例。
变慢的一面出现在重启之后。掉电重启会清空缓存(缓存区不回写就丢了),重启后第一批写又是冷态。如果系统频繁重启,或者有反复重启的异常处理逻辑,缓存的收益会被反复的「窗口重建」吃掉。极端情况下,重启风暴会让系统永远处在冷态。
| 启动场景 | 缓存的作用 | 对启动时间的影响 | 注意事项 |
|---|---|---|---|
| 冷启动首次加载 | 窗口期全命中 | 显著变快 | 收益最大的一次 |
| 正常重启后加载 | 缓存被清空,重新走窗口 | 与冷启动相当 | 每次重启都重新付一次窗口成本 |
| 异常重启风暴 | 缓存反复清空 | 可能变慢,系统一直在冷态 | 需要限制重启频率 |
| 启动后立即大量写 | 窗口被快速消耗 | 启动本身快,但后续立刻掉速 | 启动时序不要与业务写同时进行 |
| 启动校验与刷新 | 后台刷新占用通道 | 可能与启动写竞争 | 把刷新避开启动窗口 |
测启动时间时必须明确三个条件:盘是新盘还是用过的盘、是否做过预热、启动期间是否有其他后台任务在跑。这三者都会显著影响结果。不注明条件的启动时间数字,在跨方案比较时是不可用的。
8.1 缓存中的数据尚未落盘
先明确风险的来源。主机写一笔数据并收到完成返回时,这笔数据可能处在三个位置之一:
- 在缓存区的脏页里:已写入 NAND,但还没回写到数据区。这是日志结构缓存的常态。
- 正在回写途中:数据区已有新版本但元数据还没更新,或者反过来。这是回写过程中的瞬间状态。
- 已经在数据区:回写完成,映射已更新。这是最安全的状态。
掉电发生在状态 1,数据会丢——这是缓存设计上的固有行为,不是缺陷。它对应的是「主机收到完成返回之前的数据」,本来就必须保证。关键是要分清哪些数据丢了是可接受的,哪些是不可接受的。
判断标准是主机的语义约定:已经向主机返回「写完成」的数据,在掉电后必须还在。没有返回完成的写,掉电后丢失是允许的。这条标准听起来很清楚,但实现上极容易出错,因为控制器的「返回完成」和「数据真正安全落盘」之间存在时间差——这个时间差就是一致性的全部难点。
- 写未返回完成:在缓存脏页。丢失。可接受。主机需重试。
- 写已返回完成,数据在缓存:在缓存脏页。丢失(不可接受)。不可接受。控制器必须先保证落盘。
- 回写中,数据区新版本已写:数据区。旧版本残留,元数据未更新。不可接受,需恢复逻辑修正。控制器。
- 回写完成,元数据未更新:数据区。数据在但映射指向旧页。不可接受,需元数据恢复。控制器。
- 回写完成,元数据已更新:数据区。正常。可接受。
8.2 掉电点与数据状态矩阵
图12 把掉电点按「数据位置」与「元数据提交状态」两个维度列成矩阵。这张矩阵的价值在于:它能穷举一致性设计必须覆盖的全部情形,比按时间轴想象掉电点要可靠得多。
矩阵里最需要注意的不是「安全」的那些格子,而是「不一致」的那些格子——它们表示掉电后盘上留下了一个「数据在、但映射不对」的状态。这类状态不会立刻报错,而是在很久之后读到这个逻辑块时才暴露,排查成本极高。
矩阵里 P4(伪 SLC 切换中)之所以整行都不安全,是因为切换过程中的状态组合最多:部分块已切、部分块未切、部分数据在 SLC 区、部分数据在 XLC 区。如果掉电,重启后控制器必须能识别出「这是一次没做完的切换」,然后决定是回滚还是继续。把切换设计成可回滚的,比设计成可续做的要简单得多。
| 掉电点 | 典型触发场景 | 最难的部分 | 恢复策略 |
|---|---|---|---|
| P1 数据编程中 | 任意写入过程中 | 编程中断可能留下半个页 | 该页作废,不更新映射 |
| P2 元数据提交前 | 写已完成,返回前 | 必须保证已返回的数据不丢 | 按「元数据先于数据」规则判定 |
| P3 回写中 | 后台回写过程 | 数据区新旧版本并存 | 以元数据为准,丢弃未提交的版本 |
| P4 伪SLC 切换中 | 模式切换过程 | 状态组合最多 | 回滚到切换前的确定状态 |
| P5 元数据提交后 | 回收完成过程 | 旧页状态判定 | 按映射表清理无效页 |
8.3 元数据掉电的原子性要求
一致性设计有八成的复杂度集中在元数据上。数据本身反而简单——数据页要么写成功要么写失败,判定很明确;元数据是多个字段的组合状态,判定就复杂了。
元数据必须满足两条硬要求。
第一条是原子性:一次状态更新要么全部生效,要么完全不生效。最常见的实现方式是双副本 + 校验——写两份相同内容,重启时选校验通过的那份。这样即使写到一半掉电,也能通过校验判定出哪份是完整的。
第二条是提交顺序:元数据必须先于它所描述的数据落盘。也就是说,记录「数据在位置 X」这条元数据,要先写成功,才能把 X 处的数据认定为有效。如果顺序反了,掉电后就会出现「元数据说数据在 X,但 X 处的数据其实没写完」的不一致。
- 脏位图:极高,每次写入都可能改。必须原子,否则会误判或漏判。按块聚合更新,减少落盘频率。一位一写,掉电后位图与实际不符。
- 映射表:每次写入必改。必须原子,且先于数据提交。双副本 + 校验,或日志式增量更新。先写数据后改映射。
- 回写队列:每次回写必改。必须原子,且能容忍中断。记录边界标记而非全量状态。队列状态与实际进度不一致。
- 模式状态(伪SLC):切换时改。必须原子,且可回滚。双副本记录切换前后状态。单副本,掉电后无法判断切换进度。
一个隐蔽的性能问题是:元数据写入如果太频繁,会成为隐藏的性能瓶颈。每次主机写都可能改一个脏位图 bit,如果每个 bit 都单独落盘,元数据写入量会和数据写入量同量级,性能与寿命都会被拖垮。正确做法是按块聚合并延迟落盘——在内存里累积到一定量或一定时间再批量更新。但这又带来了新的原子性要求:批量更新的边界必须与数据状态严格对应,否则聚合本身就成了不一致的来源。
8.4 恢复流程与保守策略
掉电之后,控制器要在重启时把盘恢复到一致状态。恢复流程的设计原则只有一条:宁可多丢一点,不可错一点。
这就是「保守策略」的核心含义——恢复时宁可把状态往更旧的一侧拉,也不要往更新的方向推。因为把状态拉旧,代价是丢一点最近的数据(主机可以重试);把状态推新,代价是返回错误的数据(主机无法察觉,无法重试)。
一个典型的恢复流程有五步:
- 校验元数据完整性:检查双副本是否有一份校验通过。没有则盘不可用。
- 加载映射表与位图:把元数据读进内存,重建盘的状态认知。
- 丢弃未提交的脏页:所有在缓存区但元数据未记录为已提交的数据,一律丢弃。它们对应的逻辑块回到旧版本。
- 处理未完成的切换:如果是伪 SLC 且检测到切换未完成,回滚到切换前的确定状态。
- 重建可用块池:重新扫描坏块与空闲块,重建分配池。
| 恢复步骤 | 做什么 | 保守取向 | 可能的后果 | 耗时影响 |
|---|---|---|---|---|
| 校验元数据 | 选一份完整副本 | 两份都无效则拒绝挂载 | 盘不可用,需返修 | 短 |
| 加载状态 | 读元数据到内存 | 以旧副本为准而非新副本 | 可能丢失最近一次更新 | 中,取决于元数据量 |
| 丢弃脏页 | 清理缓存区未提交数据 | 宁可丢不可错 | 主机需重试最近写 | 短 |
| 回滚切换 | 恢复到切换前状态 | 回滚而不是续做 | 切换前的工作白做 | 中 |
| 重建块池 | 扫描坏块与空闲块 | 多扫一些,慢但安全 | 启动变慢 | 长,通常是恢复的主要耗时 |
保守恢复通常意味着重启后要做更多扫描工作,启动时间会变长。对嵌入式设备来说,重启时间本身就是一项指标——一个「为了安全要扫全盘」的设计,可能在实验室里没问题,在要求快速重启的场合却不可接受。所以恢复流程的设计要同时考虑正确性与启动时间,常见做法是「快速恢复 + 后台完整扫描」:启动时只做最小必要的一致性处理,完整的块池重建放到启动后后台完成。
8.5 缓存与掉电保护的协同
很多器件已经带了掉电保护机制(掉电检测电容、掉电中断、写保护)。缓存引入之后,这些机制与缓存的关系需要重新理清,否则会出现「保护机制反而帮了倒忙」的情况。
需要理清的三点:
- 掉电检测的时机。掉电保护通常在电压掉到某个阈值时触发一个中断,控制器用它来「抢救」关键数据(通常是映射表)。缓存存在时,抢救的对象要不要包括缓存脏页?答案是通常不包括——因为缓存脏页的量可能很大,抢救它的时间不够。设计上必须明确:抢救范围只包含元数据,不包含缓存数据。
- 回写中掉电的处理。如果掉电发生在回写途中,抢救机制不应该尝试「接着回写」,而应该放弃这次回写,保留旧版本。回写是可以重试的非关键操作。
- 元数据的抢救优先级。如果缓存压力大、元数据更新频繁,掉电时抢时间的压力更大,元数据更新应当更保守(批量更大、间隔更长),而不是更激进。
- 掉电抢救范围:缓存脏页增加了要抢救的对象。只抢救元数据,明确放弃缓存数据。试图抢救全部缓存,浪费时间。
- 回写可重试性:回写变成一个可能被中断的长期操作。回写设计成幂等,可安全放弃重试。假设回写总能完成。
- 元数据更新频率:缓存使元数据更新更频繁。加大批量,延长间隔,降低抢时间压力。为了实时性高频更新元数据。
- 写保护窗口:窗口期内数据在缓存,掉电即丢。明确告知主机窗口期内的持久化语义。让主机误以为窗口期内已持久化。
窗口期不等于持久化。主机如果认为「写返回成功 = 数据已安全落盘」,那么在窗口期内掉电就会丢数据。控制器与主机之间必须有一致的语义约定:如果缓存方案选择「返回成功但允许丢」,那必须在上层协议或文档里说明;如果选择「返回成功即保证持久化」,那控制器就必须对所有写做同步落盘(这等于放弃缓存的时延收益)。这个约定必须在需求阶段就定清楚,不能留给实现去猜。
8.6 常见的不一致形态
最后把实际遇到的不一致现象归类。它们在症状上很像,但根因完全不同,混在一起排查会浪费时间。
| 不一致形态 | 症状 | 触发条件 | 根因 | 定位方法 |
|---|---|---|---|---|
| 映射指向失效页 | 读到旧数据,且是某次写入之前的版本 | 元数据与数据提交顺序颠倒 | 数据先落、元数据后更 | 对比该逻辑块的版本序列 |
| 脏页被误判为干净 | 写入丢失,但无报错 | 位图更新失败被忽略 | 位图原子性不足 | 对账位图与实际数据 |
| 回写队列与实际不符 | 回写遗漏或重复 | 队列更新未与回写同步 | 队列提交顺序错误 | 检查队列边界标记 |
| 切换进度未知 | 重启后盘长时间挂载不上 | 伪 SLC 切换中掉电 | 模式状态不可回滚 | 看模式元数据是否双副本 |
| 坏块表不一致 | 写入指向坏块,写失败 | 坏块标记未落盘 | 坏块表未纳入元数据保护 | 对账坏块表与物理状态 |
| 预留区提前耗尽 | 性能突降但容量尚有剩余 | 缓存与数据区共用预留 | 预留核算未覆盖缓存占用 | 查空闲块水位与预留配置 |
验证掉电一致性不需要穷举所有掉电点。关键在于覆盖每一种「元数据提交状态」与「数据位置」的组合——也就是图12 矩阵里的每一行。在每一行里挑一个代表性掉电点做验证,就能覆盖绝大多数真实故障。这比随机掉电测试有效得多,也比穷举省时间。随机掉电可以作为补充手段,用来发现设计者没想到的路径,但它不能替代矩阵验证。
9.1 标称峰值与实测值的差距
规格书上的写入速度对应一组非常具体的条件:块对齐的大块顺序写、缓存窗口内、队列深度拉满、页区最有利、寿命早期、常温。这些条件在真实业务里几乎不可能同时成立。
所以看到实测值低于标称值时,第一反应不应该是「器件有问题」,而应该是「条件不一致」。把条件逐项对齐,比反复测更有价值。
- 缓存状态:窗口内,持续命中。窗口耗尽后按 XLC 写。实测偏低。写入量远超缓存容量。
- 负载形状:块对齐顺序大块。小块随机或混合。实测偏低。用真实负载形状测,不用合成大块。
- 队列深度:拉满。默认或较低。实测偏低。确认并提升并发数。
- 分区与格式:整盘、已格式化。小分区、未格式化。实测波动大。统一分区大小与格式化状态。
- 寿命阶段:新盘早期。已用一段时间。实测偏低。同一批样品统一初始状态。
- 温度:常温。高温环境。实测偏低。记录测试温度,必要时做温度对照。
- 后台负载:空闲。有文件系统日志与后台任务。实测波动大。关闭无关后台任务后复测。
9.2 测试样本必须穿透稳态
这是本章最重要的一节。图13 画的是同一份负载在不同样本长度下测得的性能:样本越长,测得值越低,而且下降不是线性的——在样本覆盖缓存容量的那个点上有一个明显的拐点。
判断样本是否足够的方法很简单:把样本长度翻倍再测一次,如果数值明显下降,说明上一次还在窗口内,样本不够。反复翻倍直到数值稳定,那才是稳态值。这个「倍增法」不需要预先知道缓存容量,是最实用的做法。
| 样本长度 | 测得值 | 数据有效性 | 能得出什么结论 | 不能得出什么结论 |
|---|---|---|---|---|
| 小于缓存容量 | 偏高 | 无效 | 缓存工作正常 | 任何关于稳态的结论 |
| 约为缓存容量的 1~2 倍 | 接近真实值 | 基本有效 | 稳态性能量级 | 精确的稳态值,仍有波动 |
| 数倍于缓存容量 | 真实 | 有效 | 稳态性能与抖动 | 寿命末期的表现 |
| 覆盖整个寿命周期 | 真实且偏低 | 有效 | 寿命末期稳态表现 | — |
用「循环覆盖同一个大文件」来做写入测试,看起来写入了远超缓存容量的数据,但因为每次写的是同一个逻辑块,缓存里的版本会不断被合并,回写量很小,测出来的是一个非常乐观的稳态值——它没有反映随机覆盖写下的真实碎片压力。验证稳态时应当用持续写入不同的数据(比如多个文件轮流写),而不是反复覆盖同一个文件。
9.3 顺序与随机负载的差异
顺序写与随机写的差距远大于一般的直觉。差距来自两个叠加的来源:页区分配(第 1 章讲过,顺序写可以被引导到低页区)与碎片与回收(随机写产生更多失效页,回收更频繁)。
这两者的比重随负载的「随机度」变化。所以比较不同方案时,随机度必须作为一个受控变量固定下来,否则数字不可比。
- 大块顺序写:接近最优。可引导到低页区。低。相对最小,因顺序写本就快。确认是否被优化算法特殊照顾。
- 小块顺序写:中等。部分可引导。中。较明显,合并收益大。注意块对齐,验证合并效果。
- 大块随机写:偏高。低页区消耗快。偏高。窗口内明显,窗口后回落。看低页区耗尽的时间点。
- 小块随机写:最高。低页区消耗最快。最高。窗口内最明显,稳态最差。最严苛的负载,适合做对比基准。
如果要为某个产品选一个「代表性」的基准负载,建议选「持续的小块随机写」,而不是顺序写。理由是:顺序写下各方案的差距较小,容易让评审产生「差异不大、可以接受」的错觉;而小块随机写是差距最大的场景,在最严苛条件下仍然可接受的方案,在真实负载下才不会出问题。
9.4 读写混合比的影响
纯写测试和纯读测试都不能代表真实负载。真实负载总是读写混合,而混合比的变化会显著改变缓存的表现。
原因有两点。第一,读会挤占通道:读虽然不消耗 P/E,但占用总线时间,读的比例越高,写能分到的通道时间越少。第二,读会触发读合并:如果被读的逻辑块在缓存区里还有更新版本,控制器必须先做读合并才能返回,这是一次关键路径上的额外开销。
所以稳态测试应当包含读。建议至少测三档混合比(比如纯写、七三混合、对半混合),观察性能随混合比变化的曲线形状——曲线形状比某一点的具体数值更能说明方案的特性。
| 混合比(读:写) | 写通道时间占比 | 读合并触发概率 | 缓存的净收益 | 典型场景 |
|---|---|---|---|---|
| 0 : 10(纯写) | 最高 | 低 | 最明显 | 日志写入、镜像烧录 |
| 2 : 8 | 较高 | 中 | 明显 | 带状态回读的业务写入 |
| 5 : 5 | 中等 | 中到高 | 中等 | 一般数据库负载 |
| 8 : 2 | 较低 | 高 | 较小 | 读多写少的应用 |
| 10 : 0(纯读) | 无写入 | 不适用 | 零 | 只读场景,本就不需要缓存 |
9.5 冷启动与热身的处理
冷启动数据与热态数据是两种完全不同的东西,混在一起报是自欺欺人。但也不能简单地「只用热态」——因为很多应用确实关心的就是冷启动的表现(比如设备开机后的第一笔写入)。
正确的做法是分开测、分开报,并且明确每个数字对应的是哪种状态。
| 测试状态 | 如何构造 | 关注什么 | 常见误用 | 报告要求 |
|---|---|---|---|---|
| 冷态 | 新盘或重启后立即测 | 窗口内的时延与吞吐 | 当成器件性能 | 必须标注「冷态」 |
| 过渡态 | 写入量在窗口附近 | 拐点位置、抖动开始点 | 当成稳态 | 必须标注「过渡」 |
| 稳态 | 样本数倍于缓存容量后 | 平均时延、抖动、尾延迟 | 样本不足导致误判 | 必须标注样本长度 |
| 预热后 | 用预置命令或专用流程 | 系统实际体验 | 预热写入被误算为用户写入 | 必须说明预热方式与消耗 |
| 寿命末期 | 接近 P/E 预算上限 | 最差情况的表现 | 用新盘数据承诺末期表现 | 必须标注磨损阶段 |
用「写文件预热」得到的数据里,包含了预热本身的写入量。如果报告里不区分「预热写入」和「业务写入」,那么性能数字和寿命数字都会被污染——预热写入消耗了寿命,却可能被算进用户的业务写入里。所有预热相关的数字都必须单独标注,包括预热消耗的写入量与时间。
9.6 测试方法的常见陷阱
本章最后一节把实践中最容易出错的地方集中列出。这些错误的共同特点是:测完之后数字很好看,但这个数字不能用来做任何决策。
| 陷阱 | 做法 | 为什么数字无效 | 正确做法 |
|---|---|---|---|
| 样本太短 | 写入量小于缓存容量就取数 | 测的是窗口,不是产品 | 用倍增法确认样本已穿透稳态 |
| 反复覆盖同一文件 | 循环写同一个大文件 | 合并率高,碎片压力未体现 | 持续写不同数据 |
| 分区未对齐 | 用未对齐的分区或裸写 | 每写都有跨块开销 | 分区对齐后测,或明确标注不对齐 |
| 并发度不足 | 用单线程或低并发测 | 通道填不满,测的是排队能力 | 拉满并发并确认 |
| 忽略后台负载 | 在有文件日志、TRIM 的环境测 | 波动被放大或缩小 | 统一后台环境,或明确标注 |
| 忽略温度 | 高温下测却不记录 | 结果不可复现 | 记录温度,必要时做温度对照 |
| 只报平均值 | 只报平均时延 | 掩盖抖动与尾延迟 | 同时报高分位时延 |
| 不区分冷热态 | 把冷态与稳态数据混在一张表 | 两个不同的工况无法比较 | 分开测、分开报 |
第一,负载描述:块大小、读写比、并发数、随机度。第二,初始状态:新盘还是用过的盘、是否预热、预热方式。第三,样本长度:累计写入量与缓存容量的比值。第四,稳态取哪一段:拐点之后哪一段数据被用来计算指标。第五,指标种类:平均时延、高分位时延、吞吐,三者都要有。第六,环境条件:温度、后台负载状态。缺任何一项,两个方案的数字就不可比。
10.1 顺序大块写入
顺序大块写入是对缓存最友好的负载,也是最容易造成误判的负载——因为它在冷态和稳态下的表现差距相对最小。
它友好的原因有三层:可被引导到低页区(第 1 章)、碎片产生少(覆盖同一段连续空间,旧页逐个失效)、写合并天然充分(连续写很少需要合并,因为没有中间版本)。
它容易造成误判的原因也在这里:用顺序写测出来的性能,掩盖了方案在随机负载下的真实差距。如果一个方案在顺序写下和无缓存方案差不多,但在随机写下差距很大,那么用顺序写做选型依据就会选错。
- 时延:冷稳差距最小。SLC 与 XLC 的差距被页型优势掩盖。不能用它测出缓存的真实收益。
- 写放大:接近最优。写合并充分,回收搬移少。看起来比实际更省。
- 页区利用:可全程引导到低页区。顺序分配天然有序。掩盖了随机写的页区劣势。
- 碎片:少。连续覆盖,旧页规律失效。回收压力被低估。
- 典型场景:镜像烧录、大文件顺序写;属于一次性任务,收益窗口有限。
10.2 随机小块写入
随机小块写入是对缓存最苛刻的负载,也是最应该作为基准的负载。
它苛刻的原因:写合并需求高但不一定凑得齐(多笔写落在不同逻辑块上,合并不了)、低页区消耗快(无法引导顺序,页区被快速消耗)、碎片产生密集(随机分布导致失效页均匀散布在整个块里)。
这三点叠加的结果是:缓存的窗口收益最明显,窗口之后的回落也最剧烈。这个「先好后坏」的曲线形状在小块随机写下最清晰。
| 维度 | 随机小块写的表现 | 原因 | 对方案评估的意义 |
|---|---|---|---|
| 时延 | 冷稳差距最大 | SLC 优势被页区劣势抵消 | 最能体现缓存的真实价值 |
| 写放大 | 最高 | 合并凑不齐,回收搬移多 | 最能暴露方案的写放大问题 |
| 页区利用 | 低页区迅速耗尽 | 无法引导顺序分配 | 考验页区调度策略 |
| 碎片 | 密集且分散 | 随机分布导致失效均匀散布 | 考验回收策略 |
| 读合并 | 触发概率高 | 同一逻辑块被反复覆盖 | 考验读路径设计 |
要为一个产品选代表性基准负载,选持续的小块随机写,理由是它是最苛刻的场景。在最苛刻条件下仍然可接受的方案,在真实负载下不会出问题。反过来,用顺序写做基准容易得到「差异不大」的错觉,从而放过一个在随机写下问题严重的方案。
10.3 大量文件覆盖
大量文件覆盖是很常见的一类负载(日志轮转、配置更新、缓存文件刷新),它的特点是写入总量不大但覆盖频繁。
这类负载对缓存的影响是双面的。有利的一面:缓存的窗口可能刚好覆盖整个写入过程,享受全程 SLC 编程。不利的一面:覆盖写会产生大量旧版本,缓存里的版本堆积会触发读合并,而读合并是关键路径上的额外开销。
还有一个容易被忽略的机制:文件系统层面的覆盖往往伴随删除。删除如果正确下发了 TRIM,旧块会被标记为可回收,碎片问题会缓解;如果没有下发(比如文件系统没有启用 TRIM,或者用了日志式文件系统),旧块只能等自然回收,碎片持续累积。
- 频繁覆盖小文件:窗口可能覆盖全程,但读合并多。同逻辑块多版本堆积。缩短覆盖间隔,减少版本堆积。
- 文件轮转(新建+删除):有利,TRIM 缓解碎片。TRIM 使旧块可回收。确保 TRIM 正确下发。
- 日志式文件系统:碎片回收依赖内部机制。日志块与主数据区的分离。调整日志块大小与刷写策略。
- 配置区频繁更新:写入量小,缓存基本用不满。工作集小。这类场景缓存收益有限。
10.4 数据库日志类负载
数据库日志是典型的小写入、高频次、强顺序的组合负载,它的稳态表现有一个独特特征:平均时延不错,但尾延迟很差。
原因在于日志写入会触发周期性的大动作:日志缓冲刷写、检查点、索引更新。每一次这样的动作都会造成一批写同时落盘,与前台的小写入争抢通道,产生时延尖峰。
缓存对这类负载的作用是「削峰」:把一部分写入提前吸收到窗口期,让周期性尖峰不那么尖锐。但如果缓存容量相对于日志写入量太小,削峰效果有限;如果太大,稳态平台抬升又会影响平均时延。
| 观测项 | 典型表现 | 成因 | 验收要点 |
|---|---|---|---|
| 平均时延 | 较好 | 小写入本身代价低 | 关注稳态均值是否可接受 |
| 尾延迟 | 较差,周期性尖峰 | 检查点、日志刷写等批量动作 | 必须测高分位,尖峰才是问题所在 |
| 写入量 | 不大但持续 | 工作集小,缓存可能长期用不满 | 确认缓存容量与日志量的关系 |
| 抖动 | 周期性 | 批量动作的节奏 | 观察抖动周期与业务周期的关系 |
表里「写入量不大但持续」这一行值得展开:这类负载可能是缓存收益最有限的场景之一。如果整个生命周期的日志写入量都没有超过缓存容量,那么缓存从头到尾用不满,用户看到的是「一直很快」,但这个「快」不是因为缓存起了作用,而是因为本来就没有多少写入量。把这种情况误判为「缓存很有效」是最危险的结论——一旦业务量增长,性能会断崖式下跌,而那时再排查就晚了。
10.5 稀疏写入与去重类负载
稀疏写入(大量逻辑块只有少量数据被写)与去重(相同数据只存一份)是代价最高的一类负载,原因是有效数据占比低。
对 NAND 来说,写入量按物理页计而不是按逻辑数据量计。稀疏写入意味着大量逻辑块只占了物理上一个页的一部分,剩下的空间要么空着,要么被后续的无关数据填上但那个逻辑块再也用不到。这直接导致物理页的有效利用率低,回收时需要搬移的数据比例高。
去重更极端:逻辑上写入了大量数据,物理上只存了一小份。缓存在这里几乎帮不上忙——缓存按物理页工作,去重之后需要缓存的物理数据量远小于逻辑写入量,窗口很快就用满了。
- 稀疏写(少量数据占大逻辑块):高。有限。缓存按物理页工作,逻辑空隙不省成本。主机侧做稀疏感知的分配。
- 完全重复的数据:取决于去重率。很小。物理写入量远小于逻辑量,窗口很快用满。在主机侧去重,减小下发量。
- 写一次就长期不读:中等。窗口期内有效。窗口期正好覆盖写入过程。不需要额外处理。
- 大对象顺序写:低。有效。顺序写本身代价就低。对齐块大小即可。
如果产品宣传的容量是去重后的有效容量,而写入统计是去重前的逻辑写入量,那么这两个数字放在一起算窗口长度会得出完全错误的结论。窗口是按物理数据算的,所以评估去重场景下的缓存收益时,必须用去重后的物理写入量。这类口径错误在实际项目里并不罕见。
10.6 负载形状比数据量更重要
本章的结论可以压成一句话:两个写入总量相同的负载,代价可以完全不同。原因是缓存的作用点在「形状」上,不在「总量」上。
具体地说,缓存能改善的是页型分配、写入合并、时延平坦度这三件事,它们全部与写入的分布方式有关,与写入的绝对量无关。而缓存不能改善的是物理写入总量,它只会让这个总量在时间上更集中。
| 只写总量相同,形状不同 | 顺序大块 | 随机小块 | 覆盖写 | 稀疏去重 |
|---|---|---|---|---|
| 冷态时延 | 低 | 低 | 低 | 低 |
| 稳态时延 | 较低 | 高 | 较高 | 最高 |
| 写放大 | 低 | 高 | 中 | 最高 |
| 碎片产生 | 少 | 多 | 中 | 多 |
| 回收压力 | 低 | 高 | 中 | 高 |
| 缓存收益的兑现程度 | 低(本就快) | 最高 | 中 | 低(窗口很快满) |
表里最后一行是本章最实用的一条:缓存收益的兑现程度,在顺序写和稀疏去重这两类负载上是最低的。原因分别是「本来就快」和「窗口很快被用满」。所以判断一个产品该不该用缓存时,要看它的负载形状落在哪一列,而不是看它写了多少数据。
11.1 性能随时间持续下降
症状:设备刚出厂时性能正常,使用一段时间后持续变慢,且不会自己恢复。
这类问题有三个可能的原因,优先级从高到低:预留区被消耗导致空闲块不足、磨损进入后期导致编程脉冲变多与读重试增加、坏块累积导致替换消耗预留。三者都会随时间单调恶化,符合「持续下降且不恢复」的特征。
- 空闲块水位:明显偏低。正常。正常。
- 重启后表现:不变差。不变差。不变差。
- 编程耗时:正常。明显变长。正常。
- 读重试次数:正常。明显增加。可能增加。
- 新盘同条件对比:性能接近。性能明显更好。性能接近。
- 增长速度:较慢。很慢。很慢。
如果新盘在同样的负载下性能明显更好,说明是磨损后期效应;如果新盘和老盘表现接近,说明是容量或预留问题(因为老盘的预留已经被消耗了一部分)。这个对比实验能在一小时内把范围缩小一半,比逐项查参数快得多。
11.2 掉电后偶发校验失败
症状:正常运行时数据正确,掉电重启后偶发校验失败,且失败是零星的、不集中在特定区域。
「偶发」这两个字是关键。如果每次掉电都失败,那是设计缺陷;如果偶发失败,说明一致性设计覆盖不全——某些掉电点没有被正确处理。
排查方向是第 8 章的矩阵:把失败的逻辑块对应到图12 矩阵里的具体格子,看那一格的状态组合是否被恢复逻辑覆盖。重点关注两个位置:P3(回写进行中)与P4(伪SLC 切换中),它们的状态组合最多,最容易漏。
| 定位步骤 | 做什么 | 看到什么说明什么 | 对应机制 |
|---|---|---|---|
| 记录失败 LBA | 把失败的逻辑块号记下来 | 集中在某区域说明是局部问题 | 碎片与块的对应关系 |
| 查是否在窗口期内掉电 | 对照掉电时刻的缓存水位 | 窗口期内掉电丢失是设计行为 | 日志结构的固有语义 |
| 查回写进度 | 看掉电时该逻辑块是否在回写 | 在回写中说明覆盖不全 | 第 5 章稳态路径 |
| 查元数据完整性 | 校验映射表与位图 | 校验失败说明元数据非原子 | 第 8 章元数据要求 |
| 查模式状态 | 伪 SLC 方案查切换记录 | 切换未完成说明切换不可回滚 | 第 3 章切换代价 |
11.3 满缓存下时延抖动
症状:平均时延可以接受,但时延曲线出现规律性的锯齿,高分位时延明显偏高。
最可能的原因是回写策略是「写满即回写」。这种策略下,回写一旦触发就是一批密集的写,与前台写争抢通道,形成锯齿。解决办法是改成后台合并写,把回写摊到空闲窗口。
如果已经用了后台合并写仍然有抖动,就要查是不是空闲窗口不够——如果业务一直不停,后台就没有可利用的空隙,回写只能挤进前台写的通道。这种情况下调策略没用,要从负载侧解决(第 12 章 12.3)。
- 锯齿周期与写入量相关:写满即回写触发批量回写。看抖动周期与缓存容量的关系。改后台合并写。
- 锯齿周期与时间相关:定时回写策略。看抖动周期是否固定。按负载节律调整周期。
- 无规律的高分位尖峰:前台 GC。查空闲块水位是否偏低。加大预留、把回收摊到后台。
- 持续偏高无尖峰:回收压力整体过大。看碎片率与有效页比例。加大预留、优化回写合并。
11.4 缓存区提前失效
症状:使用时间不长,但缓存区的可用容量明显下降,窗口比预期短得多。
这个现象的根因通常不是「缓存坏了」,而是分配策略把磨损集中了。如果采用随机分配,少数块会被反复写入先到达磨损阈值,触发坏块替换,消耗预留块,导致可用块减少。
另一个容易被忽略的原因是缓存与数据区共用预留区。缓存区老化消耗的预留块本来是留给数据区坏块替换的,缓存把它用掉之后,数据区的坏块替换就没有预留可用,表现为「盘还有容量但写不进去」。
| 可能根因 | 识别方式 | 确认手段 | 解决方向 |
|---|---|---|---|
| 随机分配导致磨损集中 | 个别块磨损计数异常高 | 读磨损计数表 | 改循环分配或磨损感知分配 |
| 缓存与数据区共用预留 | 数据区预留被提前耗尽 | 查两区的预留配置 | 给缓存单独预留 |
| 缓存容量设置过小 | 窗口从一开始就短 | 核对容量配置与工作集 | 调整容量档位 |
| 后台任务持续占用 | 空闲窗口从未被利用 | 查后台活动日志 | 协调后台任务时序 |
11.5 磨损计数异常
症状:磨损计数的增长速率与预期不符,或者某些块的计数明显偏离平均值。
先要明确一点:缓存区的磨损计数高于数据区是正常现象,不是故障。缓存区的工作方式决定了它的每个块被高频反复写入,计数高是设计使然。真正需要排查的是「分布异常」而不是「数值偏高」。
- 缓存区整体均匀高于数据区:缓存的正常工作方式。否。不需要处理。
- 缓存区内个别块远高于平均:分配策略集中了磨损。是。改循环或磨损感知分配。
- 数据区出现局部热点:业务写入有热点。视情况。静态磨损均衡搬移冷数据。
- 计数增长速率突然变化:负载形状改变。需确认。对照业务变更记录。
- 计数溢出或归零:计数器位数不够。是。检查计数器宽度与处理逻辑。
11.6 排查决策顺序
前面五节给了具体现象的排查方法,但真实故障中更常见的情况是「现象不典型」——既不完全像 A 又不完全像 B。这时候需要一个顺序,而不是一堆并列的检查项。
推荐的顺序是五步,每一步都能把范围砍掉一大半:
- 分类现象:性能类还是一致性类?性能类再分「持续下降」还是「间歇抖动」。分类错了,后面全错。
- 验证掉电相关性:是否只在掉电重启后出现?是则转一致性矩阵排查(第 8 章),否则转性能路径。
- 做重启实验:重启后性能是否明显变好?是则说明问题在稳态路径(回收、碎片、预留),不是器件本身。
- 查空闲块水位:水位偏低则是容量预留问题;水位正常则继续往下。
- 复核测试口径:样本是否穿透稳态、负载形状是否真实、冷热态是否混报。到这一步还没定位,问题多半在测试方法而不是器件。
| 步骤 | 做什么 | 耗时 | 能砍掉的范围 | 结果指向 |
|---|---|---|---|---|
| 1 分类现象 | 判断性能类或一致性类 | 几分钟 | 约一半 | 决定后续走哪条排查线 |
| 2 掉电相关性 | 有无掉电才出现 | 半小时 | 约三成 | 一致性矩阵 vs 性能路径 |
| 3 重启实验 | 重启后性能是否变好 | 一小时 | 约两成 | 稳态路径 vs 器件本身 |
| 4 空闲块水位 | 查水位与预留配置 | 半小时 | 约一成 | 容量预留问题 |
| 5 复核测试口径 | 样本长度、负载形状、冷热态 | 一天 | 剩余部分 | 测试方法问题 |
因为它成本极低而区分度极高:重启一次、跑一遍同样的负载,几小时内就能得到结论。而且它直接验证了本文的核心判断——如果重启后性能明显变好,说明器件本身没问题,问题出在稳态路径上。这一步能把「器件质量」与「固件策略」这两类完全不同的根因彻底分开,避免在错误的方向上耗时间。
12.1 是否需要缓存的判据
判断要不要缓存,用三个问题按顺序问,任何一个答「是」就可以考虑加:
- 全生命周期写入量是否显著超过缓存容量?如果不超过,缓存永远用不满,价值无法兑现。
- 负载形状是否让缓存有机会改善时延?如果以顺序大块写为主,缓存收益有限(本来就快);如果是随机小块写或大量覆盖写,收益最明显。
- 业务能否接受写放大上升带来的寿命代价?如果寿命预算很紧,缓存带来的额外写可能让实际寿命不达标。
三个问题都答「否」时,不加缓存通常是更好的选择——省下的复杂度、验证成本与写放大是实打实的收益。
- 一次性大文件烧录:是。否(顺序写)。否(双写浪费)。不加缓存,对齐块大小即可。
- 间歇业务写入:是。是。是。加小容量固定 SLC 区。
- 持续日志写入:是。是。需核算。加缓存,按稳态核算寿命。
- 读多写少的应用:否。否。不加缓存,投给读优化。
- 容量极小的器件:否(占比太低)。是。是。不加缓存,收益不抵复杂度。
- 负载波动大的通用盘:是。是。是。伪 SLC 弹性方案。
12.2 缓存容量的选取
容量选取的原则只有一条:按工作集大小定,不按期望性能定。容量应当覆盖典型业务事务或典型业务周期的写入量,而不是「越大越快」的想法。
三条具体的约束:
- 下界:至少要能覆盖一次典型业务事务的写入量,否则连一次业务都撑不完,缓存等于没有。
- 上界:不超过稳态可接受的额外写放大。容量越大,稳态平台越高,这个上界由寿命预算与性能要求共同决定。
- 占比:不宜占据总容量的过大比例,否则轻负载下白白闲置。占比过高是固定分区方案最常见的资源浪费。
| 约束 | 方向 | 定得不当的后果 | 如何核算 |
|---|---|---|---|
| 覆盖典型业务事务 | 下界 | 窗口比一次业务还短,等于没缓存 | 统计典型事务的写入量 |
| 不超过寿命预算允许 | 上界 | 稳态平台过高,寿命不达标 | 按加权写放大核算寿命 |
| 不占过大容量比例 | 占比 | 轻负载下容量闲置 | 对比业务写入量与总容量 |
| 预留区要独立核算 | 预留 | 缓存与数据区抢预留,早期耗尽 | 分别核算两者的预留需求 |
| 老化后仍够用 | 寿命末期 | 寿命末期窗口过短 | 按寿命末段的有效容量核算 |
第 1 章图1 已经用曲线说明:缓存越大,稳态平台越高。加大缓存改善的是「体验」,恶化的是「稳态」与「寿命」。如果目标是让稳态性能更好,方向恰恰是减小缓存的必要性(减少写入量、优化负载形状),而不是加大它。这两件事方向相反,是本章最需要记住的一点。
12.3 主机侧能否改善稳态表现
好消息是:主机侧确实有相当多的手段可以改善稳态,而且这些手段往往比调整器件更便宜。
| 主机侧手段 | 原理 | 对稳态的改善 | 代价 |
|---|---|---|---|
| 块对齐写 | 避免每次写跨块,减少读改写 | 显著,减少合并开销 | 需要应用配合改造 |
| 减少覆盖写 | 少产生中间版本,降低碎片 | 显著,直接改善回收压力 | 需要改写入模式 |
| 正确下发 TRIM | 让文件系统声明无效块 | 显著,减少无效数据 | 依赖文件系统支持 |
| 批量提交 | 把多次小写攒成一次大写 | 中等,改善合并度 | 增加延迟 |
| 降低写入量 | 从源头减少物理写 | 最显著,且改善寿命 | 可能需要改业务流程 |
| 错峰安排批量写 | 让业务低谷期承担批量动作 | 中等,为后台留出空闲窗口 | 需要业务侧配合调度 |
表里「降低写入量」这一项的效果最强但常被忽略。它的逻辑很简单:稳态的代价来自物理写入量,减少物理写入量就是直接减少稳态代价。很多项目把精力全花在调器件参数上,却没认真统计过自己的业务到底写了多少物理数据——这个账应该先算清楚。
12.4 保守模式与性能模式的取舍
很多方案提供一个「性能模式」与一个「保守模式」的开关。这两个模式的差别通常不只是调几个参数,而是设计取向的根本分歧。
性能模式倾向于:窗口更大、后台更激进、元数据更新更频繁(保证持久化快)、磨损均衡更积极。保守模式倾向于:窗口更小、后台更保守、元数据更新更稀疏(减少写入)、优先保寿命。
取舍的关键是应用场景的容错能力:如果业务能容忍偶发的写入失败或需要重启恢复,保守模式更合适;如果业务对时延敏感且能接受寿命缩短,性能模式更合适。没有普遍最优的选择,只有与场景匹配的选择。
- 缓存容量:较大。较小。业务对时延的敏感度。
- 回写策略:后台积极合并。写满即回写或延后回写。稳态抖动的容忍度。
- 元数据更新:频繁,保证持久化快。稀疏,减少写入量。掉电一致性的要求强度。
- 磨损均衡:积极,牺牲部分寿命换性能。保守,优先摊平磨损。寿命预算的余量。
- 后台活动:较多,占用通道。较少,让前台优先。业务高峰期的分布。
- 典型适用:时延敏感、寿命预算有余量。寿命紧张、可容忍偶发异常。产品定位。
12.5 设计检查清单
下面这份清单把全文的要求收敛成可逐项核对的条目。建议在设计评审与量产验证两个节点各过一遍。
需求与选型阶段
| # | 检查项 | 判定标准 | 对应章节 |
|---|---|---|---|
| 1 | 业务写入量已统计 | 有全生命周期写入量的估算,且与缓存容量做过比较 | 12.1 |
| 2 | 负载形状已确认 | 明确是顺序、随机、覆盖还是稀疏,不用「总量」代替 | 10.6 |
| 3 | 缓存必要性已论证 | 三个判据逐条回答,不是凭感觉决定 | 12.1 |
| 4 | 容量选取有依据 | 按工作集定,且核对了寿命末期是否仍够用 | 12.2 |
| 5 | 一致性语义已与主机对齐 | 窗口期是否算已持久化,需求阶段就写清楚 | 8.5 |
设计与实现阶段
- 6:元数据原子性有方案。更新要么全生效要么不生效,有具体实现手段。8.3。
- 7:元数据先于数据提交。提交顺序在代码里可验证,不是靠约定。8.3。
- 8:回写可重入且幂等。回写被打断后重试不会产生重复数据。7.5。
- 9:异常路径已覆盖。回写失败、切换失败、回收失败都有恢复逻辑。7.5。
- 10:切换可回滚。伪 SLC 方案在切换中掉电能回到确定状态。3.4。
- 11:后台活动有限流。回收与回写不会无限期占用前台通道。7.5。
- 12:预留区独立核算。缓存与数据区分别核算预留需求。6.2。
验证与交付阶段
| # | 检查项 | 判定标准 | 对应章节 |
|---|---|---|---|
| 13 | 掉电矩阵已覆盖 | 图 12 矩阵的每一行都有对应用例 | 8.2 |
| 14 | 性能样本穿透稳态 | 用倍增法确认样本已足够长 | 9.2 |
| 15 | 负载形状真实 | 用真实业务的负载,不用总量相同的合成负载 | 10.2 |
| 16 | 包含读写混合 | 至少测三档混合比,观察曲线形状 | 9.4 |
| 17 | 报了高分位时延 | 至少有一个 99 分位指标,且在满缓存下测 | 5.4 |
| 18 | 冷态与稳态分开报 | 报告里明确标注每个数字对应的状态 | 9.5 |
| 19 | 寿命按加权写放大核算 | 含缓存带来的额外写,不是按稳态瞬时值 | 5.3 |
| 20 | 预热消耗单独标注 | 预热写入量与时间不混入业务数据 | 9.5 |
把这十二章压缩成一句话:SLC Cache 买到的是一段写入窗口,不是一段持续带宽;窗口用完之后,性能一定回落到真实水平,所以任何标称峰值只要测试样本没有穿透稳态,都是无效数据。
这句话的实践含义是:评估存储性能时,先问「这个数字测的是哪一段」,再问「这个数字是多少」。顺序不能颠倒——因为如果测的是窗口内,那么无论数字多漂亮,它描述的都是一个不会长期存在的状态。
A.1 缓存形态
- 内置控制器(SD NAND / eMMC / UFS):固定 SLC 分区或伪 SLC。同一 die 上划出的块。块数。控制器固件与初始化流程。
- SPI NAND:器件内 SLC Cache 或主机侧写缓存。器件内块,或主机内存 / 外挂 DRAM。块数或字节。器件固件,或主机软件。
- 并行 NAND(多通道多die):DRAM 缓存为主,SLC 区为辅。控制器侧 DRAM,或 die 内部分块。字节。控制器固件。
- 无缓存方案:无。不适用。不适用。不适用。
A.2 容量档位
| 容量档位 | 相对总容量的占比量级 | 覆盖的工作集 | 窗口长度 | 主要代价 |
|---|---|---|---|---|
| 极小 | 百分之几以下 | 极小的写入突发 | 短到无实际意义 | 实现复杂度与收益不匹配 |
| 小 | 百分之几到十以内 | 一次典型业务事务 | 覆盖单次业务 | 轻负载下闲置 |
| 中 | 百分之十到二十 | 多轮业务事务 | 覆盖多轮业务 | 稳态平台开始抬升 |
| 大 | 二十以上 | 持续写入的长阶段 | 覆盖大部分写入过程 | 稳态平台明显抬升,寿命消耗增加 |
上表给的是量级区间,不是推荐值。具体占比要按第 12 章的方法核算:下界取典型业务事务的写入量,上界取寿命预算与性能要求共同允许的最大值,占比上限则由「轻负载下能容忍多少闲置」决定。任何不说明工作集的容量推荐都是不完整的。
A.3 回写策略
| 回写策略 | 触发条件 | 时延稳定性 | 写放大 | 适用负载 |
|---|---|---|---|---|
| 写满即回写 | 脏页水位到阈值 | 锯齿明显 | 较高 | 间歇、突发型 |
| 后台合并写 | 后台调度持续按批 | 最平滑 | 较低 | 持续、长时间写入 |
| 定时回写 | 固定时间片 | 周期性波动 | 中等 | 周期性写入 |
| 空闲窗口回写 | 检测到业务空隙 | 平滑且代价低 | 较低 | 有自然间歇的业务 |
A.4 对启动的影响
- 冷启动首次加载:窗口期全命中。变快。收益最大的一次。
- 正常重启后加载:缓存清空,重走窗口。与冷启动相当。每次重启重付窗口成本。
- 异常重启风暴:缓存反复清空。可能变慢。限制重启频率。
- 启动后立即大量写:窗口快速消耗。启动快但后续掉速。错开启动时序与业务写。
- 启动期校验与刷新:后台刷新占通道。可能竞争。刷新避开启动窗口。
B.1 负载分类
| 负载类别 | 典型业务 | 写入特征 | 写入量量级 |
|---|---|---|---|
| 顺序大块写 | 镜像烧录、视频录制、大文件拷贝 | 连续、单向、块对齐 | 一次任务量大但一次性 |
| 随机小块写 | 文件系统元数据、小文件更新 | 随机、多目标、频繁 | 总量中等,持续 |
| 大量文件覆盖 | 日志轮转、缓存文件刷新 | 重复覆盖同一批逻辑块 | 总量小,频率高 |
| 数据库日志 | 事务日志、检查点 | 小写入高频,混合批量动作 | 总量不大但持续 |
| 稀疏与去重 | 快照、备份、分布式存储 | 有效占比低,重复度高 | 逻辑量大,物理量小 |
| 读多写少 | 内容分发、只读数据 | 以读为主 | 写入量极小 |
B.2 命中特征
- 顺序大块写:快。小。低(本来就快)。
- 随机小块写:很快。很大。最高。
- 大量文件覆盖:快。中到大。中。
- 数据库日志:较快。中。中,但尾延迟受益明显。
- 稀疏与去重:较快。大。低(窗口按物理量很快用满)。
- 读多写少:无差别。无差别。零。
B.3 稳态表现
| 负载类别 | 稳态时延 | 写放大 | 碎片与回收压力 | 主要风险 |
|---|---|---|---|---|
| 顺序大块写 | 较低 | 低 | 低 | 用顺序写测会高估方案 |
| 随机小块写 | 高 | 高 | 高 | 寿命消耗超出预算 |
| 大量文件覆盖 | 中到高 | 中 | 中 | 读合并开销被低估 |
| 数据库日志 | 中(尾延迟差) | 中 | 中 | 尖峰导致业务超时 |
| 稀疏与去重 | 最高 | 最高 | 高 | 容量口径错误导致误判 |
| 读多写少 | 低 | 低 | 低 | 误以为缓存有效 |
B.4 优化手段
| 负载类别 | 最有效的优化手段 | 次要手段 | 不建议做的事 |
|---|---|---|---|
| 顺序大块写 | 块对齐、加大并发 | 适当加大缓存 | 为它专门加缓存 |
| 随机小块写 | 减少写入总量、块对齐 | 加大预留区、优化合并 | 只调回写策略 |
| 大量文件覆盖 | 正确下发 TRIM、减少覆盖 | 错峰批量提交 | 指望缓存解决 |
| 数据库日志 | 错峰安排检查点、加大预留 | 缓存削峰 | 只看平均时延 |
| 稀疏与去重 | 主机侧去重、减小下发量 | 稀疏感知的分配 | 用逻辑写入量算窗口 |
| 读多写少 | 把资源投给读优化 | 适当缓存突发写 | 加缓存期待改善读 |
C.1 症状分类
| 大类 | 子类 | 典型表现 | 首查方向 |
|---|---|---|---|
| 性能类 | 持续下降 | 随时间单调变慢,不自恢复 | 空闲块水位、磨损阶段 |
| 性能类 | 间歇抖动 | 时延锯齿或周期性尖峰 | 回写策略、前台 GC |
| 性能类 | 突然劣化 | 某个时间点之后断崖式变差 | 业务变更、预留耗尽 |
| 一致性类 | 掉电后校验失败 | 重启后零星读到错误数据 | 元数据原子性与提交顺序 |
| 一致性类 | 容量异常 | 可用容量少于标称 | 预留区配置、老化程度 |
| 一致性类 | 写入失败 | 写到一定量后写不进去 | 空闲块耗尽、坏块表 |
C.2 定位步骤
| 步骤 | 动作 | 输出 | 分支 |
|---|---|---|---|
| 第一步 | 确定症状大类(性能 / 一致性) | 一个大类 | 分两条线走 |
| 第二步 | 一致性线:确认是否只在掉电后出现 | 是 / 否 | 是则进矩阵,否则查常态校验 |
| 第三步 | 性能线:做重启实验 | 重启后是否变好 | 变好则查稳态路径 |
| 第四步 | 查空闲块水位与预留配置 | 水位值 | 偏低则查容量预留 |
| 第五步 | 水位正常则复核测试口径 | 样本长度与负载形状 | 口径有问题则重测 |
| 第六步 | 以上都排除后查磨损阶段 | 新旧盘对比 | 新盘明显更好则查寿命末期效应 |
C.3 易误判场景
| 易误判现象 | 看起来像 | 实际是 | 区分方法 |
|---|---|---|---|
| 性能差 | 器件坏了 | 测试样本太短,测的是窗口 | 用倍增法加长样本 |
| 性能差 | 缓存无效 | 负载形状用错了,顺序写看不出差异 | 换成小块随机写复测 |
| 容量少 | 坏块太多 | 预留区被缓存老化消耗 | 查预留配置与水位 |
| 偶发校验失败 | NAND 质量问题 | 一致性设计覆盖不全 | 对照掉电矩阵找缺失格 |
| 重启后变慢 | 重启有副作用 | 窗口重建,掉电恢复扫描耗时 | 对比重启前后恢复时长 |
| 寿命不达标 | 器件寿命不足 | 写放大被低估,没算缓存的额外写 | 按加权写放大重算 |
C.4 处置建议
| 根因类别 | 短期处置 | 长期处置 | 验证方式 |
|---|---|---|---|
| 测试口径问题 | 重新做测试 | 把口径写进测试规范 | 倍增法确认样本足够 |
| 负载形状问题 | 换真实负载复测 | 在需求阶段明确负载形状 | 对比不同形状的曲线 |
| 预留区不足 | 加大预留 | 分别核算缓存与数据区的预留 | 查水位回升情况 |
| 回写策略不当 | 改后台合并写 | 把策略纳入产品配置 | 对比抖动曲线 |
| 一致性覆盖不全 | 补齐矩阵用例 | 按矩阵逐行设计用例 | 掉电测试无失败 |
| 寿命预算不足 | 缩短设计寿命或加大容量 | 按加权写放大重算预算 | 按新预算核算 P/E |
D.1 存储术语
| 术语 | 解释 | 在本文中的作用 |
|---|---|---|
| SLC / MLC / TLC / QLC | 每单元分别存1、2、3、4 个比特 | 决定单次写入的编程代价;与2D/3D 正交 |
| 2D / 3D NAND | 单元平面排列 / 垂直堆叠 | 决定堆叠层数与干扰特性;与单元类型正交 |
| 低页区 / 高页区 | 靠沟道侧 / 远离沟道侧的栅极层 | 顺序写可被引导到低页区,写入更快 |
| 伪 SLC(pSLC) | 同一批块按需在 SLC 与 XLC 模式间动态切换 | 第 3 章主题:省容量代价,付切换代价 |
| 固定 SLC 区 | 划出专用物理块永久以 SLC 方式编程 | 第 2 章主题:容量固定,行为可预测 |
| 日志结构(log-structured) | 写追加到新位置,合并时只保留最新版本 | 缓存的默认写入组织方式 |
| 写合并 | 把多次逻辑写合成一次物理写 | 降低写放大与碎片,依赖同一逻辑块的版本凑齐 |
| 回写 / 折叠 | 把缓存中的数据搬回数据区并落盘 | 窗口结束的动作,决定稳态何时到来 |
| 脏页 | 内容比数据区更新的缓存页 | 脏页数量即缓存水位线 |
| 有效页比例 | 块内有效页数占块内总页数的比例 | 决定一次垃圾回收的搬移代价 |
| 写放大(WA) | 主机写 1 单位,盘里实际写的量 | 缓存会让它上升,寿命核算必须包含 |
| 读放大 | 为了读 1 单位实际读了多少 | 主要来自写合并与垃圾回收的搬移前读 |
| 预留区(OP) | 不对主机开放的块,用于坏块替换与磨损均衡 | 与缓存共用时会提前耗尽 |
| 窗口 / 写入窗口 | 缓存能提供高速写入的那段写入量区间 | 全文核心概念:买到的是窗口不是带宽 |
| 稳态 | 缓存耗尽后性能稳定所处的平台 | 唯一有效的验收与核算口径 |
| 老化 | 缓存有效容量随使用时间下降 | 由保留块消耗、坏块替换等造成 |
D.2 性能指标
| 指标 | 含义 | 常见误用 | 正确用法 |
|---|---|---|---|
| 写入时延 | 单次写操作从提交到完成的耗时 | 只报平均值,掩盖抖动 | 同时报平均与高分位 |
| 吞吐 | 单位时间完成的写量 | 用峰值代表长期表现 | 必须标注是否穿透稳态 |
| 窗口内时延 | 缓存未耗尽时的时延 | 当成器件性能承诺 | 标注为冷态数据 |
| 稳态时延 | 缓存耗尽后的时延 | 样本不够时误当作稳态 | 用倍增法确认 |
| 抖动 / 方差 | 时延的波动程度 | 只看均值 | 作为独立指标验收 |
| 尾延迟(高分位) | 最慢那部分写的时延 | 样本量不足时不稳定 | 满缓存状态下测 |
| 写放大 | 实际写量与主机写量之比 | 只算稳态瞬时值 | 按生命周期加权 |
| 样本长度 | 测试写入量相对缓存容量的倍数 | 不记录这个数字 | 报告中必须标明 |
D.3 缩写速查
| 缩写 | 全称 / 含义 | 备注 |
|---|---|---|
| SLC / MLC / TLC / QLC | Single / Multi / Triple / Quad Level Cell | 每单元比特数,与 2D/3D 正交 |
| pSLC | pseudo SLC,伪 SLC | 动态页类型切换方案 |
| FTL | Flash Translation Layer,闪存翻译层 | 块设备语义到物理约束的翻译 |
| WA | Write Amplification,写放大 | 缓存会使其上升 |
| RA | Read Amplification,读放大 | 主要来自合并与回收搬移 |
| GC | Garbage Collection,垃圾回收 | 碎片的主要来源与清理者 |
| OP | Over Provisioning,预留空间 | 缓存与数据区应分别核算 |
| P/E | Program / Erase,编程与擦除次数 | 寿命的基本单位 |
| TBW / DWPD | Total Bytes Written / 每日写入量 | 寿命预算的两种口径 |
| TRIM | 声明逻辑块不再使用的命令 | 能显著降低无效数据堆积 |
| SLC Cache | SLC 缓存区 | 本文主题:买到的是窗口 |
| WAF / WEU | 写放大因子 / 有效利用率 | 评价方案时与时延一起看 |
| SPD | SSD Performance Decay | 固态盘固有性能衰减 |
| FTL 恢复时间 | 掉电后重建映射所需时间 | 保守策略的主要代价 |
「SLC / MLC / TLC / QLC」说的是每单元存几个比特,「2D / 3D」说的是单元怎么排布。这是两个正交维度——一颗 3D TLC 器件同时既是 TLC 也是 3D,不存在「3D 比 SLC 更先进」这样的说法。把这两条线混成一条进化链,是存储领域最常见的概念错误之一。相关内容在姊妹篇《NAND 存储单元与 3D NAND》中展开,本文只引用结论。
本文档的性能量级、代价关系与排查方法适用于 NAND 类存储器件的通用讨论,具体型号的参数以对应厂商的正式 datasheet 与固件版本说明为准。凡涉及寄存器、位域与具体数值的实现细节,均以正式文档为准。