垃圾回收机制GC——SSD如何在“不能覆写“的世界里清理空间?

垃圾回收机制GC——SSD如何在“不能覆写“的世界里清理空间?

摘要:NAND闪存有一个致命特性——写前必须擦除,且擦除的最小单位是"块"(Block),不能像HDD那样直接覆盖单个扇区。当空闲块不足时,SSD必须在后台执行垃圾回收(Garbage Collection),将有效数据搬出、再擦除整个块。本文深入解析GC的工作原理、触发机制、Victim Block选择策略、前后台冲突,以及TRIM命令如何大幅提升GC效率。


一、为什么SSD需要垃圾回收?

1.1 NAND的"不可覆盖"困境

这是理解GC的核心前提。HDD写入数据时,磁头可以直接覆盖旧数据——想改哪个扇区就改哪个。但NAND闪存完全不同:

┌─────────────────────────────────────────────────────┐ │ HDD vs NAND 写入方式对比 │ ├──────────┬──────────────────┬───────────────────────┤ │ 操作 │ HDD(机械硬盘) │ NAND(闪存) │ ├──────────┼──────────────────┼───────────────────────┤ │ 写入 │ 直接覆盖 │ 只能写空页(空白Page) │ │ 修改 │ 直接覆盖原位置 │ 写入新位置 + 标记旧页无效│ │ 删除 │ 标记删除 │ 标记无效(不立即释放) │ │ 擦除 │ 不需要 │ 按Block整体擦除(几百页) │ │ 覆写前提 │ 无 │ 必须先擦除整个Block │ └──────────┴──────────────────┴───────────────────────┘

关键概念层级:

Block(块)= 擦除的最小单位(通常包含 256~1024 个 Page) Page(页) = 写入/读取的最小单位(通常 4KB~16KB)

也就是说,哪怕你只想修改Block里一个Page的数据,也必须把整个Block的数据搬走、擦除、再重新写入。这就是GC存在的根本原因。

1.2 页的四种状态

在SSD内部,每个Page都处于以下状态之一:

┌──────────────┬──────────────────────────────────┐ │ 页面状态 │ 说明 │ ├──────────────┼──────────────────────────────────┤ │ Free(空闲) │ 从未写过数据,可以直接编程(Program)│ │ Valid(有效)│ 包含最新有效数据,FTL映射表有记录 │ │ Invalid(无效)│ 数据已过期或被更新,但尚未擦除 │ │ Bad(坏块) │ 物理损坏,永久不可用 │ └──────────────┴──────────────────────────────────┘

随着用户不断写入和更新数据,Invalid页会越来越多,Free页越来越少。当Free页耗尽时,SSD就没有空间写入新数据了——GC的作用就是把Invalid页"清理"掉,回收出Free Block


二、垃圾回收的工作原理

2.1 GC的基本流程(四步走)

整个过程可以概括为"挑选→搬移→擦除→归还":

Step 1: 选择一个 Victim Block(受害者块) ↓ 该块中包含大量 Invalid Page Step 2: 将块中仍然 Valid 的 Page 搬到其他 Block ↓ 这个过程叫"数据 rescue(救援)" Step 3: 擦除整个 Victim Block ↓ 所有 Page 恢复为 Free 状态 Step 4: 将回收的 Block 加入 Free Block Pool ↓ 可以接受新的写入请求

用代码逻辑表示:

defgarbage_collect():# Step 1: 选择 Victim Blockvictim=select_victim_block()# 选择策略见下文# Step 2: 搬移有效数据forpageinvictim.pages:ifpage.status==VALID:new_location=allocate_free_page()write_data(new_location,page.data)update_ftl_mapping(page.lpn,new_location)page.status=INVALID# Step 3: 擦除整个 Blockerase_block(victim)# Step 4: 归还到空闲池victim.status=FREE free_block_pool.append(victim)# 统计:这次GC的"成本"valid_pages_moved=count_valid_pages(victim)gc_cost=valid_pages_moved/PAGES_PER_BLOCKreturngc_cost# 越接近0越好

2.2 一个具体的例子

假设一个Block包含8个Page,其中5个已经无效(用户更新了数据),3个仍然有效:

GC 前: ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │Valid │Invalid│Valid │Invalid│Invalid│Valid │Invalid│Invalid│ │ 旧A │ 旧B │ 旧C │ 旧D │ 旧E │ 旧F │ 旧G │ 旧H │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ Step 1-2: 搬移3个Valid页到新位置 ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ →搬走 │ │ →搬走 │ │ │ →搬走 │ │ │ │ 旧A │ 旧B │ 旧C │ 旧D │ 旧E │ 旧F │ 旧G │ 旧H │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ Step 3: 擦除整个Block ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ Free │ Free │ Free │ Free │ Free │ Free │ Free │ Free │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ Step 4: 加入 Free Block Pool ✅

💡关键观察:GC并不是"免费"的——它需要额外搬移有效数据,产生内部写入。搬移的页越多,GC的"成本"越高。这就引出了GC策略的核心问题:如何选择Victim Block,使得搬移成本最低?


三、Victim Block选择策略

GC策略的好坏直接影响SSD的性能和寿命。业界有几种经典策略:

3.1 Greedy(贪心策略)

规则:选择Invalid页最多(即Valid页最少)的Block。

Block A: 7 Valid + 1 Invalid → GC成本 = 7/8 = 87.5% ❌ 太高 Block B: 2 Valid + 6 Invalid → GC成本 = 2/8 = 25% ✅ 贪心选中 Block C: 4 Valid + 4 Invalid → GC成本 = 4/8 = 50%
优点缺点
每次GC回收效率最高忽略了页的"年龄"(冷热)
搬移数据量最少可能导致热数据被反复搬移
实现简单长期来看WAF不一定最优

3.2 FIFO(先进先出策略)

规则:选择最早被写入的Block(最"老"的Block)。

Block A: 写入时间 T=100s, 5 Invalid → 最老,优先回收 Block B: 写入时间 T=500s, 3 Invalid Block C: 写入时间 T=800s, 7 Invalid
优点缺点
实现极其简单可能选中Valid页很多的Block
不会遗漏老数据GC效率不稳定

3.3 Cost-Benefit(收益成本策略)

规则:综合考虑Block的Invalid比例和"年龄",选择收益/成本比最高的。

Benefit = (Invalid Pages / Total Pages) × (当前时间 - 最后修改时间) Cost = Valid Pages(需要搬移的代价) Score = Benefit / Cost → 选最高的

这是理论上最优的策略,但计算开销较大。

3.4 策略对比总结

┌──────────────┬──────────┬──────────┬──────────────┐ │ 策略 │ GC效率 │ 实现复杂度 │ 适用场景 │ ├──────────────┼──────────┼──────────┼──────────────┤ │ Greedy │ ★★★★ │ ★★ │ 消费级SSD │ │ FIFO │ ★★ │ ★★★★★ │ 轻量级固件 │ │ Cost-Benefit │ ★★★★★ │ ★★ │ 企业级SSD │ │ 混合策略 │ ★★★★★ │ ★★★ │ 主流方案 │ └──────────────┴──────────┴──────────┴──────────────┘

📌实际固件中的做法:现代SSD通常采用混合策略——先用Greedy快速筛选候选集,再用Cost-Benefit做精细排序。部分厂商还会结合温度感知(Temperature-aware GC),将冷数据Block优先回收。


四、前台GC与后台GC

4.1 两种GC模式

┌─────────────────────────────────────────────────────────────┐ │ GC 的两种执行模式 │ ├──────────────────────┬──────────────────────────────────────┤ │ 前台 GC │ 后台 GC │ │ (Synchronous GC) │ (Asynchronous GC) │ ├──────────────────────┼──────────────────────────────────────┤ │ Free Block 耗尽时 │ SSD 空闲时自动执行 │ │ 被迫停止主机写入 │ 不影响主机正常读写 │ │ 主机感知到延迟 │ 用户完全无感知 │ │ 性能严重下降 │ 提前准备空闲块,避免前台GC │ │ "SSD变卡"的元凶 │ 高端SSD的核心竞争力 │ └──────────────────────┴──────────────────────────────────────┘

4.2 为什么"用久了SSD会变慢"?

很多用户反映SSD用了一段时间后明显变慢,GC是重要原因之一:

全新 SSD: Free Block 充足 → 所有写入直接落入空闲页 → 无需GC → 速度飞快 使用一段时间后: Free Block 减少 → 写入时需要等待GC回收 → 前台GC触发 → 延迟飙升 典型延迟变化: ┌──────────┬────────────────┐ │ 场景 │ 写入延迟 │ ├──────────┼────────────────┤ │ 全新状态 │ 20-50 μs │ │ 轻度使用 │ 50-100 μs │ │ 重度使用 │ 200-500 μs │ │ 空间耗尽 │ 1000+ μs │ ← 前台GC + 磨损均衡同时进行 └──────────┴────────────────┘

4.3 后台GC的触发时机

聪明的固件会在SSD空闲时"偷偷"执行GC:

触发条件(典型): ① 主机无 I/O 请求超过阈值(如 50ms 空闲) ② Free Block 数量低于高水位线(如 < 20% 总块数) ③ SSD 温度低于阈值(避免高温下GC加剧发热) ④ 电池/超级电容供电充足(防止GC中途断电丢数据) 退出条件: ① 收到新的主机 I/O 请求 → 立即暂停GC,优先服务主机 ② Free Block 恢复至高水位线以上 ③ 温度超过安全阈值
时间轴示意: 主机I/O: ████░░░░░░░░████░░░░████████░░░░░░ 后台GC: ░░░░▓▓▓▓▓▓▓░░░░░▓▓░░░░░░░░▓▓▓▓▓▓ █ = 主机写入/读取 ▓ = 后台GC ░ = 空闲

💡关键洞察:好SSD和差SSD的差距,很大程度上体现在后台GC的调度策略上。高端固件能让用户几乎感受不到GC的存在,而低端固件则频繁触发前台GC,导致"卡顿"。


五、TRIM命令:GC的效率倍增器

5.1 没有TRIM时的问题

当用户在操作系统中删除文件时:

传统流程(无TRIM): 用户删除文件 ↓ 操作系统: 在文件系统中标记"此文件已删除" ↓ 但是!SSD完全不知道哪些数据已经不需要了 ↓ 结果: GC时,SSD必须把所有"看起来有效"的页都当作有效数据搬移 ↓ 大量本可以丢弃的数据被反复搬移 → 写放大严重 ↑↑↑

5.2 TRIM的工作原理

TRIM(在NVMe中称为Dataset Management的Deallocate)是操作系统主动告诉SSD"这些数据不需要了"的机制:

带TRIM的流程: 用户删除文件 ↓ 操作系统: 标记文件已删除 + 向SSD发送TRIM命令 ↓ SSD固件: 将对应的逻辑页标记为 Invalid ↓ GC时: 这些页直接被跳过,不需要搬移! ↓ 擦除效率大幅提升,写放大显著降低 ✅

5.3 TRIM的实际效果

┌────────────────────────────┬────────────────────┐ │ 场景 │ GC 写放大因子 │ ├────────────────────────────┼────────────────────┤ │ 无 TRIM,空间紧张 │ WAF ≈ 5-10x │ │ 无 TRIM,空间充裕 │ WAF ≈ 2-4x │ │ 有 TRIM,稳态运行 │ WAF ≈ 1.1-1.5x │ │ 有 TRIM + Over-Provisioning│ WAF ≈ 1.0-1.2x │ └────────────────────────────┴────────────────────┘

5.4 确保TRIM生效的配置

Linux系统

# 检查SSD是否支持TRIMlsblk--discard/dev/nvme0n1# 检查fstrim定时器是否启用systemctl status fstrim.timer# 手动执行TRIM(建议定期运行)sudofstrim-v/# 挂载时启用discard(实时TRIM,但有性能开销)# /etc/fstab 中添加 discard 选项UUID=xxx / ext4 defaults,discard01# 更好的方案:使用systemd定时器定期批量TRIM# 无需mount选项discard,减少实时开销systemctlenablefstrim.timer systemctl start fstrim.timer

Windows系统

# 检查TRIM是否启用(默认开启)fsutil behavior query DisableDeleteNotify# 如果返回 0,表示TRIM已启用# 如果返回 1,手动启用:fsutil behaviorsetDisableDeleteNotify 0# 手动优化(等同于TRIM)Optimize-Volume-DriveLetter C-ReTrim-Verbose

⚠️重要提醒:RAID阵列中的TRIM支持取决于控制器和驱动。Intel RST、Windows Storage Spaces通常支持,但某些硬件RAID卡可能不透传TRIM命令,需要特别确认。


六、GC与磨损均衡、FTL的协作关系

GC不是孤立运行的,它和磨损均衡(WL)、FTL(闪存转换层)紧密协作:

┌──────────────────────────────────────────────────┐ │ FTL 核心模块 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ 地址映射 │ │ 磨损均衡 │ │ 垃圾回收 GC │ │ │ │ (L2P) │←→│ (WL) │←→│ │ │ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ └──────────────┼───────────────┘ │ │ ↓ │ │ ┌──────────────┐ │ │ │ 块管理器 │ │ │ │ (Block Mgr) │ │ │ └──────┬───────┘ │ │ ↓ │ │ ┌──────────────┐ │ │ │ NAND 驱动 │ │ │ │ (NAND Flash) │ │ │ └──────────────┘ │ └──────────────────────────────────────────────────┘

三者协作时的典型冲突:

┌──────────────────────────────────────────────────┐ │ 资源竞争场景 │ ├──────────────────────────────────────────────────┤ │ │ │ GC 需要: 空闲块来搬移数据 + NAND带宽来擦除 │ │ WL 需要: 空闲块来搬移数据 + NAND带宽来写入 │ │ 主机写入: 空闲块来接受新数据 + NAND带宽来编程 │ │ │ │ → 三者共享同一块NAND,带宽和空间都有限 │ │ → 固件必须在三者之间做出调度权衡 │ │ → 这就是为什么固件开发是SSD最核心的技术壁垒 │ └──────────────────────────────────────────────────┘

七、当日知识点小结

知识点关键要点
GC存在的根本原因NAND写前必须擦除,且擦除单位是Block(非Page)
GC四步流程选择Victim Block → 搬移Valid页 → 擦除Block → 归还Free Pool
Victim Block策略Greedy(效率优先)、FIFO(简单)、Cost-Benefit(最优但复杂)
前台GC vs 后台GC前台触发=性能灾难;后台执行=用户无感知,是高端SSD的标志
TRIM的作用操作系统主动告知SSD哪些数据已无效,大幅降低GC搬移量
TRIM配置Linux用fstrim.timer,Windows默认启用,RAID需确认透传
GC与WL/FTL协作三者共享NAND资源,调度权衡是固件核心壁垒
SSD变慢的原因Free Block不足→前台GC频繁触发→写入延迟飙升

🤔 思考题

  1. 一块SSD有1000个Block,每个Block 256个Page。当SSD进入稳态(Steady State)时,假设平均每个Block有60%的页是无效的。如果使用Greedy策略,GC一次平均需要搬移多少个Valid页?如果改用随机选择策略,GC一次平均需要搬移多少个页?两者的GC效率差距有多大?

  2. 在Linux系统中,为什么社区更推荐用fstrim.timer定期批量TRIM,而不是在/etc/fstab中启用discard挂载选项?从性能、I/O模式和SSD寿命的角度分析。

  3. 假设一块消费级SSD的OP(Over-Provisioning)比例从默认的7%提升到28%(类似企业级),分析这对GC频率、写放大、可用容量和SSD寿命分别有什么影响。是否存在"最佳OP"的平衡点?

🏷️ 推荐标签

SSD固态硬盘垃圾回收Garbage CollectionNAND闪存TRIM写放大FTL存储技术固件算法


作者持续更新中,关注获取每日SSD硬核知识 👆