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 严重磨损

三个致命约束由此产生:

  1. 擦除粒度 ≠ 写入粒度:擦除操作以 Block 为单位(通常 256~1024 个 Page),但写入以 Page 为单位。如果文件系统在同一个 Block 中的某个 Page 更新数据,控制器必须把整个 Block 读出来,擦除,再把修改后的数据写回去。

  2. 写前擦除(Erase-before-Write):NAND 晶体管只能将比特从 1 写为 0,不能反向。把 0 变回 1 唯一方式是擦除整个 Block。这意味着原地更新(In-Place Update)在 NAND 闪存上是不可能的。

  3. 有限的擦写次数(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 坏块的来源

  1. 出厂坏块(出厂 MB):NAN 片出厂时标记的坏块,每片有 2%~5% 的出厂坏块率
  2. 运行时坏块:擦写次数超过 P/E Cycles 限制后,Block 出现不可纠正的 bit 错误(UBER 超过 ECC 能力)
  3. 运行时干扰错误:读干扰(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 的未来方向

  1. 计算型存储(Computational Storage):在 SSD 控制器内嵌 ARM/RISC-V 核,运行应用代码。三星 SmartSSD 在控制器内运行 Spark SQL 扫描下推,减少 90% 的数据搬运。

  2. 机器学习辅助 FTL:用 LSTM/Transformer 预测 Block 的温度(冷热分类),优化 Victim Selection。学术论文(如 "DeepFlash")展示了比 Cost-Benefit 更好的预测精度。

  3. 开放通道 SSD(Open-Channel SSD):完全移除 FTL,主机直接管理 NAND Block。LG/ LightNVM 曾推动此方向,但因主机软件栈复杂度高,目前几乎被 ZNS 替代。

  4. 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 闪存——这是现代计算基础设施中最不起眼却最不可或缺的工程之一。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部