NAND Flash FTL 深度实战:闪存翻译层、磨损均衡与垃圾回收的工程原理
现代 SSD 的核心魔法不在于 NAND 硅片本身,而在于控制器固件中的 FTL(Flash Translation Layer,闪存翻译层)。FTL 将原始的 NAND 闪存"伪装"成可靠的块设备,隐藏了其最反直觉的物理约束——不能覆盖写,必须先擦除再写入。本文将深入 FTL 的工程实现,从映射算法到磨损均衡,从垃圾回收到写放大控制,逐层剖析这个嵌入在每一块 SSD 里的微型存储操作系统。
一、NAND Flash 的物理约束:为什么需要 FL
NAND 闪存的读写擦操作存在三个根本性不对称:
| 操作 | 粒度 | 延迟 | 寿命影响 |
|---|---|---|---|
| 读取(Read) | Page(4KB/8KB/16KB) | ~25μs | 极小 |
| 编程(Program) | Page(4KB/8KB/16KB) | ~200μs | 有磨损 |
| 擦除(Erase) | Block(256~1024 Pages) | ~3ms | 严重磨损 |
三个致命约束由此产生:
-
擦除粒度 ≠ 写入粒度:擦除操作以 Block 为单位(通常 256~1024 个 Page),但写入以 Page 为单位。如果文件系统在同一个 Block 中的某个 Page 更新数据,控制器必须把整个 Block 读出来,擦除,再把修改后的数据写回去。
-
写前擦除(Erase-before-Write):NAND 晶体管只能将比特从 1 写为 0,不能反向。把 0 变回 1 唯一方式是擦除整个 Block。这意味着原地更新(In-Place Update)在 NAND 闪存上是不可能的。
-
有限的擦写次数(P/E Cycles):每个 Block 有擦写次数上限——SLC 约 100K 次,MLC 约 10K 次,TLC 约 3K 次,QLC 约 1K 次。超过后 Block 变为坏块。
FTL 的存在就是为了解决这三个约束,向上层文件系统和块层呈现一个"可以任意覆盖写的普通块设备"的假象。
二、FTL 映射算法:从简单到工业级
2.1 Page-Level Mapping(页级映射)
最直觉的映射方式:维护一个从逻辑页号(LPN)到物理页号(PPN)的映射表。
写入逻辑页 LPN=42 到 Block=3, Page=7:
┌───────────────────────────────┐
│ LPN 42 → PPN 0x03007 │
│ LPN 43 → PPN 0x01012 │
│ LPN 44 → PPN 0x05021 │
│ ... │
└───────────────────────────────┘
优点:灵活的写放大使能、无需 Block 对齐,GC 时可以选择有效页最少的 Block。
缺点:映射表太大。1TB SSD 有 ~256M 个 Page(4KB/Page),每个映射项 4 字节,需要 1GB 映射表。这放不下在 SSD 的有限 DRAM 里(通常只有 1~4GB DRAM,还要用于数据缓存)。
早期 SSD(Intel X25-M 等)用 Page-Level Mapping,现代消费级 SSD 已经很少纯用这种方式。
2.2 Block-Level Mapping(块级映射)
将 SSD 分成逻辑块和物理块一一对应,偏移量固定:
LPN = (逻辑块号 × 每块页数) + 页内偏移
PPN = (物理块号 × 每块页数) + 页内偏移
优点:映射表极小(1TB ÷ 128MB/Block = 8K 条目,~32KB DRAM)。
缺点:任何 Page 更新都要按 Block 粒度重演,写放巨大。现代 SSD 不单独使用。
2.3 Hybrid Log-Block Mapping(工业主流)
结合了两者优势——用 Log Block(日志块)接收随机写,用 Data Block 存放冷数据:
┌─────────────────────────────────────────┐
│ FTL 混合映射架构 │
├─────────────────────────────────────────┤
│ │
│ Data Blocks: 固定映射,顺序填满 │
│ ┌──┐┌──┐┌──┐┌──┐ │
│ │D0││D1││D2││D3│ ... │
│ └──┘└──┘└──┘└──┘ │
│ │
│ Log Blocks: 随机写缓冲块 │
│ ┌──┐┌──┐ │
│ │L0││L1│ ← 写满后与 Data Block 交换 │
│ └──┘└──┘ │
│ │
│ 1 个 Data Block 对应 N 个 Log Block │
│ Switch Merge: 直接交换,零数据复制 │
│ Partial Merge: 有效页搬回,触发 GC │
│ │
└─────────────────────────────────────────┘
这是三星、WD、SK Hynix 等主流 SSD 采用的方案。写请求先写到 Log Block(类似日志结构文件系统),当 Log Block 耗尽时与对应的 Data Block 合并。如果 Data Block 在该 Log Block 段内没有数据,Switch Merge(直接交换)几乎是零成本操作。
2.4 DFTL(Demand-based Page-level Mapping)
三星提出的折中方案:全页级映射表存在 NAND 中,但将热密集部分(Cached Mapping Table, CMT)缓存在 DRAM:
# DFTL 地址翻译伪代码
def ftl_read(lpn):
# 检查 CMT(DRAM 缓存映射表)
if lpn in cmt:
ppn = cmc[lpn]
return nand_read(ppn)
# CMT 未命中 → 从 NAND 加载映射页
mapping_page_addr = get_mapping_page(lpn)
mapping_page = nand_read(mapping_page_addr)
# 腾出 CMT 空间(LRU 淘汰)
evicted = cmt_evict_lru()
if evicted.dirty:
nand_write(evicted.to_mapping_page())
# 加载新的映射页到 CMT
cmt_load(mapping_page)
ppn = cmt[lpn]
return nand_read(ppn)
DFTL 的关键洞察是:虽然全局映射表太大,但工作集(一段时间内访问的 Page 集合)通常可以放进有限 DRAM。这和操作系统的虚拟内存工作集理论如出一辙。
三、垃圾回收(Garbage Collection):FTL 的"暗物质"成本
3.1 为什么需要 GC
因为 NAND 不能覆盖写,每次更新一个 Page,旧版本的物理页就变成了无效页(Invalid Page)。这些无效页占据空间但无法直接释放(必须擦除整个 Block 才能回收空间)。当空闲 Block 低于阈值时,GC 必须行动。
3.2 GC 成本模型
GC 成本 = 读取有效页数 × 读取延迟 + 写入有效页数 × 编程延迟 + 擦除延迟
写放大系数(WA) = 实际写入 NAND 的数据量 / 主机写入的数据量
理想 WA = 1.0(无 GC 开销)
实际消费级 SSD WA = 1.5 ~ 5.0
企业级 SSD(大 OP) WA = 1.1 ~ 2.0
3.3 Victim Selection 策略
选择哪个 Block 做 GC 回收直接影响写放大和延迟:
# 贪心选择:选无效页最多的 Block
def gc_select_victim_greedy(free_blocks):
return max(free_blocks, key=lambda b: b.invalid_count)
# 贪心+磨损平衡:避免选擦除次数偏离过大的 Block
def gc_select_victim_balanced(free_blocks, avg_erase_count):
candidates = [b for b in free_blocks if b.erase_count <= avg_erase_count * 1.2]
if not candidates:
candidates = free_blocks
return max(candidates, key=lambda b: b.invalid_count)
# 成本-效益模型(Cost-Benefit,学术常用)
def gc_select_victim_cost_benefit(free_blocks, current_time):
def cost_benefit(block):
age = current_time - block.last_write_time
invalid_ratio = block.invalid_count / block.total_pages
# 回收收益 = 无效比例 / (有效比例 × 衰老因子)
return (invalid_ratio * age) / ((1 - invalid_ratio) * 2)
return max(free_blocks, key=cost_benefit)
成本-效益模型(Cost-Benefit)的直觉:无效比例越高,回收越划算;Block 越老(last_write_time 越久),越不值得因为少量冗余而多搬移有效页。乘以 2 的分母是因为搬移一个有效页需要"读+写"两次 NAND 操作。
3.4 Foreground GC vs Background GC
现代 SSD 采用分层策略:
┌────────────────────────────────────────────────────┐
│ GC 触发策略 │
├────────────────────────────────────────────────────┤
│ │
│ Level 0: 空闲块 > 高水位 → 不 GC │
│ │
│ Level 1: 空闲块 < 高水位 → Background GC 启动 │
│ 利用空闲带宽回收无效页,不阻塞主机 IO │
│ │
│ Level 2: 空闲块 < 低水位 → Foreground GC │
│ 暂停主机写入,强制 GC 释放块 │
│ 此时延迟急剧上升(GC stall) │
│ │
│ Level 3: 空闲块耗尽 → 写入阻塞,系统卡顿 │
│ 最坏情况,应尽量避免 │
│ │
└────────────────────────────────────────────────────┘
这是为什么企业级 SSD 在高负载下会突然延迟飙升的本质原因——Foreground GC Stall。Intel DC S3700、三星 PM1733 等通过充足的 Over-Provisioning 延缓 Level 2/3 的触发。
四、磨损均衡(Wear Leveling):均匀消耗每一颗 Block
4.1 核心矛盾
NAND 闪存中不同 Block 如果擦除次数不均,少数 Block 会提前抵达寿命终点变成坏块,而其他 Block 还有大量寿命未用完。磨损均衡的目标是让所有Block的擦写次数趋于一致。
4.2 Dynamic Wear Leveling(动态磨损均衡)
只针对热数据(频繁更新的数据):
写请求到达时:
- 优先使用擦除次数最少的空闲 Block
- 避免在同一个 Block 上连续覆写
- Data Block 交换时选择擦除次数相差大的 Block 交换
动态磨损均衡的问题:冷数据(很少修改的文件,如操作系统二进制、归档数据)永远不参与均衡,Block 的擦除次数仍会极度偏斜。
4.3 Static Wear Leveling(静态磨损均衡)
定期将冷数据迁移到磨损较重的 Block,为冷数据所在的年轻 Block 腾出空间用于热数据:
def static_wear_leveling_check(blocks, erase_count_avg, threshold=0.5):
"""周期性运行(如每小时一次,由 FTL 固件定时器触发)"""
young_blocks = [b for b in blocks if b.erase_count < erase_count_avg * threshold]
for young_block in young_blocks:
if young_block.valid_count == 0:
continue # 空块不需要处理
# 找一个擦除次数显著更高的块作为目标
old_blocks = [b for b in blocks
if b.erase_count > young_block.erase_count * 2
and b.free_pages >= young_block.valid_count]
if old_blocks:
target = min(old_blocks, key=lambda b: b.erase_count)
# 将年轻块中的冷数据搬到年老块
migrate_block(young_block, target)
erase(young_block) # 释放年轻块回到空闲池
4.4 磨损均衡的代价
迁移冷数据本身是一次写操作,消耗 NAND 寿命。因此磨损均衡需要在"均衡度"和"额外写放大"间取得平衡。企业级 SSD 通常允许 5%~10% 的擦写次数差异,不做绝对平均。
五、Write Amplification 的数学建模
理解写放大是理解 SSD 性能的关键。我们来推导一个稳态模型:
设:
- OP = Over-Provisioning 比例 = (物理容量 - 用户容量) / 用户容量
- W = 主机写入量(单位:Page)
- G = GC 额外搬移的有效页数(单位:Page)
则:
写放大 WA = (W + G) / W = 1 + G/W
在稳态下,每次 GC 回收一个 Block 释放的空间需要能吸收后续主机写入。
设一个 Block 有 B 个 Page,GC 时无效页比例为 f(fraction of invalid pages),
则从每个 Block 回收的空间为 B × f。
回收一个 Block 需要搬移的有效页数 = B × (1 - f)
如果 GC 消耗一个 Block 的空间来吸收 Δ 的主机写入(这些写入又会产生新的无效页):
G = B × (1 - f)
而 Δ = B × f 的空间被释放
换算写放大:
WA = 1 + (1-f) / f × B / (被 GC 吸收的写)
更简洁的公式(假设 GC 吸收所有写入):
WA = 1 / (1 + OP)
验证:
OP = 0.28 (28% OP,常见于企业级SSD)
WA = 1 / (1 + 0.28) = 0.78... → 这不对,应为倒数关系
修正:在随机写入场景下:
WA ≈ 1 / (2 × OP) (小 OP,重度随机写)
WA ≈ 1 (大 OP,顺序写入极少触发 GC)
实际上不同工作负载下的写放大大不相同:| 工作负载 | 28% OP 企业级 | 7% OP 消费级 | |---|---|---| | 顺序写(Steam) | 1.0 | 1.0 | | 轻度随机(4KB 70/30 RW) | 1.2 ~ 1.5 | 2.0 ~ 3.0 | | 重度随机(100% 随机写) | 2.0 ~ 3.0 | 5.0 ~ 10.0 | | FIO 稳态随机写 | 1.5 ~ 2.0 | 3.0 ~ 5.0 |
六、Over-Provisioning:性能的"货币"
Over-Provisioning(OP)——SSD 物理容量中用户不可见的部分——是 FTL 的生命线。OP 空间越大,GC 开销越小,磨损均衡越从容,稳态性能越稳定。
6.1 OP 的来源
┌──────────────────────────────────────────────────┐
│ 2TB 企业级 SSD 的 OP 空间组成(示例) │
├──────────────────────────────────────────────────┤
│ │
│ NAND 物理总容量: 2048GiB │
│ 用户可用容量: 1920GiB (≈1.89TiB) │
│ → 标称 OP: 6.25% │
│ │
│ + 二进制/十进制转换: ~7% 隐藏 │
│ (NAND 厂商标 256Gbit = 256×10^9 bytes │
│ OS 看 = 256×10^9 / 2^30 ≈ 238.4 GiB) │
│ │
│ + 固件保留: 坏块池、SA 区、FTL 元数据 │
│ │
│ 实际可用 OP: 20%+ │
│ (企业级 SSD 通常标称 28% 物理 OP) │
│ │
│ 512GB 消费级 SSD: │
│ NAND 物理: 512GB │
│ 标称 OP: ~7%(28.8GB 不可见) │
│ GC 压力极大,稳态性能波动严重 │
│ │
└──────────────────────────────────────────────────┘
6.2 Host-Managed OP
Linux 的 blkdiscard 和 NVMe Format 命令可以缩小用户可见容量,相当于增大 OP:
# 查看当前 OP
nvme id-ctrl /dev/nvme0 | grep -E "tncap|unip"
# 缩小 Namespace 增大 OP(不可逆操作,数据会丢失)
nvme format /dev/nvme0 --nsid=1 --lbaf=2 --ses=0
# lbaf=2 选择 4K+8B 元数据格式的 LBA
# 或使用更大 OP:
nvme format /dev/nvme0 --nsid=1 --lbaf=2 --ses=1 --pil=1
企业级场景经常把 2TB 的盘格式化到 1.6TB 使用,将 OP 从 28% 提升到 35%,写放大从 2.5 降到 1.5。
七、坏块管理与寿命终止
7.1 坏块的来源
- 出厂坏块(出厂 MB):NAN 片出厂时标记的坏块,每片有 2%~5% 的出厂坏块率
- 运行时坏块:擦写次数超过 P/E Cycles 限制后,Block 出现不可纠正的 bit 错误(UBER 超过 ECC 能力)
- 运行时干扰错误:读干扰(Read Disturb)、编程干扰(Program Disturb)导致相邻 Cell 比特翻转
7.2 ECC 纠错能力的演化
随着制程微缩和 Cell 密度提升,NAND 的原始误码率(RBER)急剧升高:
| NAND 类型 | RBER(原始误码率) | 所需 ECC |
|---|---|---|
| SLC | 10^-9 | 简单 BCH |
| MLC (3xnm) | 10^-6 | BCH-40 |
| TLC (1xnm) | 10^-3 | LDPC硬判决+软判决 |
| QLC (1xnm+) | 10^-2 | LDPC软判决迭代10+次 |
现代 SSD 的 LDPC(低密度奇偶校验)软判决延迟可达~100~300μs,是 SSD 读延迟的主要组成部分之一。当某 Block 的误码率超过 LDPC 纠错能力时,它被标记为坏块,数据已无法恢复。
7.3 寿命终止(End-of-Life, EOL)判断
SMART 属性中的 Percentage Used 和 Media and Data Integrity Errors 是 EOL 监控指标:
# 查看 SSD 寿命使用百分比
nvme smart-log /dev/nvme0 | grep percentage_used
# 查看剩余可用备用块(低于阈值触发警报)
nvme smart-log /dev/nvve0 | grep available_spare
# NVMe 标准定义:
# Available Spare < Available Spare Threshold → 触发 Critical Warning
# 此时建议立即备份数据并更换 SSD
八、主机端与 FTL 的协作
8.1 TRIM / Deallocate
文件系统通过 TRIM(ATA)或 Deallocate(NVMe)命令通知 FTL:某些逻辑块不再包含有效数据。这让 FTL 提前标记无效页,减少 GC 时的有效页搬移:
# 手动 TRIM(适用于 ext4/xfs)
fstrim -v /mnt/nvme_ssd
# 查看 discard 挂载选项
mount | grep discard
# -o discard: 实时 TRIM(可能引入延迟波动)
# fstrim 定期: 批量 TRIM,推荐企业级配置
无 TRIM 时的 GC 灾难:假设文件系统删除了一个 1GB 文件但没有 TRIM,FTL 不知道这 1GB 的 LPN 已无效。当 GC 选中包含这些 Page 的 Block 时,仍然把它们当作有效页搬回新 Block——白白增加了写放大。这就是为什么早期 SSD(无 TRIM 支持)越用越慢。
8.2 ZNS(Zoned Namespace)SSD:将 FTL 部分卸载到主机
ZNS SSD 将 NAND Block 按 Zone 分区,每个 Zone 必须顺序写入,完全绕过 Flash Block 的 GC 和磨损均衡。主机文件系统负责垃圾回收:
┌───────────────────────────────────────────────┐
│ ZNS SSD 架构 │
├───────────────────────────────────────────────┤
│ │
│ 传统 SSD: 主机 → FTL → NAND │
│ FTL 隐藏所有物理细节 │
│ 主机完全不知道块状态 │
│ │
│ ZNS SSD: 主机 ←[zone 状态查询]→ 控制器 │
│ 控制器只做磨损均衡和坏块替换 │
│ 主机负责 GC、顺序写入约束 │
│ 写放大≈1.0(无 FTL GC) │
│ │
│ 主机栈:F2FS / ZoneFS + libnvme → ZNS 设备 │
│ │
└───────────────────────────────────────────────┘
ZNS 的核心优势是写放大大幅降低(1.0~1.2 vs 传统 2~5),代价是主机软件复杂度高。CERN 在 ZFS-on-ZNS 场景中实测吞吐量提升 2~3 倍。
九、性能剖析与优化实践
9.1 稳态性能优于突发性能
SSD 的标称"最高速"(Burst Performance)仅在 SLC Cache / 空闲状态时可达。真正的考验是稳态性能(Steady State Performance)—— SLC Cache 用尽、GC 正在运行时:
# SNIA SSSI PTS 标准稳态测试步骤:
# 1. 安全擦除 SSD(恢复到出厂性能状态)
# 2. 双遍顺序预写:写入 2 倍 SSD 容量,消除 SLC Cache 增益
# 3. 稳态判定:连续 5 轮测试结果变异系数 < 10%
# 4. 在稳态下测量 IOPS/延迟/吞吐量
# 实际测试:
# 标称 3.5GB/s 读取的 NVMe SSD 稳态随机写可能只有 1.2GB/s
# 标称 500K IOPS 的 SSD 稳态 4K 随机写可能只有 80K IOPS
9.2 延迟百分位:Tail Latency 的 GC Stall 来源
# fio 测试 P99.99 延迟
fio --name=randwrite --ioengine=io_uring \
--direct=1 --bs=4k --iodepth=32 \
--numjobs=1 --rw=randwrite --size=100G \
--runtime=300 --time_based \
--percentile_list=50:75:90:95:99:99.9:99.99:99.999 \
--filename=/dev/nvme0n1
典型消费级 SSD 在稳态下 P99 延迟可能是平均延迟的 5-10 倍。这是因为少数写请求触发了 Foreground GC Stall。
9.3 文件系统选择影响
不同的文件系统对 FTL 施加的写放大不同:
| 文件系统 | 写放大特征 | 对 FTL 的压力 |
|---|---|---|
| ext4 (默认) | Journal 双重写,随机化程度中 | 中等 |
| F2FS | 日志结构设计,顺序化写入(对 SSD 友好) | 低 |
| XFS | Journal 小,分配组并发度高 | 中-高 |
| Btrfs | Cow + 校验和,写时复制放大写 | 高 |
| ZoneFS | 直接暴露 Zone 给应用,无 GC(配合 ZNS) | 最低 |
F2FS 的"日志结构多日志"设计将不同类型的数据(node/meta/data)分散到不同 Segment,天然减少 GC 时的有效页搬移,是最适合传统 FTL SSD 的文件系统之一。
十、FTL 的未来方向
-
计算型存储(Computational Storage):在 SSD 控制器内嵌 ARM/RISC-V 核,运行应用代码。三星 SmartSSD 在控制器内运行 Spark SQL 扫描下推,减少 90% 的数据搬运。
-
机器学习辅助 FTL:用 LSTM/Transformer 预测 Block 的温度(冷热分类),优化 Victim Selection。学术论文(如 "DeepFlash")展示了比 Cost-Benefit 更好的预测精度。
-
开放通道 SSD(Open-Channel SSD):完全移除 FTL,主机直接管理 NAND Block。LG/ LightNVM 曾推动此方向,但因主机软件栈复杂度高,目前几乎被 ZNS 替代。
-
CXL-SSD 融合:CXL.mem 协议让更多设备共享内存语义,未来 SSD 的 FTL 可能和 CXL 内存控制器融合,控制器直接以内存语义访问 NAND 而不是块语义。
总结
FTL 是 SSD 中最复杂的固件子系统之一,它是 NAND 闪存物理世界和上层逻辑世界之间的桥梁。理解 FTL 的工程实现——映射算法、垃圾回收、磨损均衡、写放大——对存储系统工程师至关重要:
- 选盘时一定要看稳态性能而非标称突发速度
- OP 空间是 SSD 的"寿命余量",别用完
- TRIM 必须开启,否则 GC 放大写
- 监控 SMART 的 Available Spare 和 Percentage Used
- 追求极致写性能考虑 ZNS SSD + F2FS 组合
- 消费级 SSD 易爆 SLC Cache,高负载场景选企业级
FTL 本质上是一个运行在 1~4GB DRAM 上的微型存储操作系统,它让我们可以像使用硬盘一样使用 NAND 闪存——这是现代计算基础设施中最不起眼却最不可或缺的工程之一。

发表评论 取消回复