4. FTL磨损均衡算法:让每一块闪存都活到老
磨损均衡,说白了就是让SSD里所有的闪存块“同生共死”。
我刚开始做FTL的时候,觉得这玩意儿不就是个计数嘛。后来踩了坑才明白——磨损均衡做不好,SSD的寿命可能缩水90%以上。
4.1 磨损均衡的目标
闪存块有擦写寿命上限,比如SLC约10万次,TLC只有1000-3000次。你想想看,如果某些块被频繁擦写,而另一些块几乎不动,那先死的块就会拖垮整个盘。
磨损均衡的核心目标就两个:
- 最大化SSD整体寿命:让所有块的擦写次数尽可能接近
- 避免局部过早失效:防止“热点块”先挂掉
我见过一个项目,因为没做磨损均衡,某块被写了50万次,其他块才写了500次。结果那块先挂了,整个盘直接变砖。嗯,教训深刻。
理想状态:所有块的擦写次数差异 ≤ 20%。实际工程中,能做到30%以内就算合格。
4.2 动态磨损均衡
动态磨损均衡,就是在写数据的时候,挑擦写次数少的块来用。
它的逻辑很简单:
// 伪代码:动态磨损均衡的块选择
uint32_t select_block_for_write() {
uint32_t min_erase_count = UINT32_MAX;
uint32_t selected_block = 0;
for (int i = 0; i < total_blocks; i++) {
if (block_is_free(i) && block_erase_count[i] < min_erase_count) {
min_erase_count = block_erase_count[i];
selected_block = i;
}
}
return selected_block;
}
动态均衡的好处是实现简单,开销小。但它有个致命缺陷——只处理“写”的过程,不处理“静置”的数据。
举个例子:你有个日志文件,天天被追加写入。动态均衡会尽量把新数据写到冷块上。但那些已经存了系统文件、照片的块,可能几年都不动一次。它们的擦写次数永远是0。
这就导致了冷热数据分化——热块被反复擦写,冷块永远闲着。
我的经验:动态均衡适合消费级SSD,因为用户数据随机性强,冷热分布相对均匀。但企业级场景,光靠动态均衡远远不够。
4.3 静态磨损均衡
静态磨损均衡,就是把那些“睡大觉”的冷数据挪个窝,让冷块也出来干干活。
它的核心逻辑:
- 找到擦写次数最少的冷块(存着长期不动的数据)
- 把冷块上的数据迁移到热块(擦写次数多的块)
- 把冷块释放出来,用于后续写入
你可能会问:把数据从冷块搬到热块,不是增加了写放大吗?
没错,静态均衡是用写放大换寿命均衡。这是一个典型的trade-off。
// 伪代码:静态磨损均衡的触发
void static_wear_leveling() {
uint32_t max_erase = get_max_erase_count();
uint32_t min_erase = get_min_erase_count();
// 当最大最小擦写次数差距超过阈值时触发
if ((max_erase - min_erase) > STATIC_LEVELING_THRESHOLD) {
Block *cold_block = find_coldest_block(); // 擦写最少且存着冷数据
Block *hot_block = find_hottest_block(); // 擦写最多
migrate_data(cold_block, hot_block); // 冷数据搬到热块
erase(cold_block); // 释放冷块
}
}
注意:静态均衡不能太频繁。我曾经在一个项目里把阈值设得太低,结果系统一直在搬数据,性能掉得没法看。建议阈值设在10%-20%的寿命范围内。
4.4 磨损均衡与垃圾回收的协同
这里有个关键问题:垃圾回收和磨损均衡,到底谁听谁的?
我个人的习惯是:垃圾回收负责“找块”,磨损均衡负责“选块”。
具体来说:
- 垃圾回收:从所有块中选出“回收价值最高”的候选块(有效数据最少、擦写次数适中)
- 磨损均衡:在垃圾回收的候选块中,进一步筛选出“擦写次数最少”的块
两者协同的决策流程:
// 伪代码:垃圾回收与磨损均衡协同
Block *select_block_for_gc() {
// Step 1: 垃圾回收筛选候选块(按有效数据量排序)
Block *candidates[GC_CANDIDATE_COUNT];
find_gc_candidates(candidates, GC_CANDIDATE_COUNT);
// Step 2: 磨损均衡从候选中选擦写最少的
Block *best = candidates[0];
for (int i = 1; i < GC_CANDIDATE_COUNT; i++) {
if (candidates[i]->erase_count < best->erase_count) {
best = candidates[i];
}
}
return best;
}
下面这张图展示了完整的协同流程:
从流程图可以看出:
- 正常写入:动态均衡直接选空闲块,开销最小
- 空闲块不足:触发垃圾回收,回收时静态均衡介入选块
- 回收完成后:释放的块进入空闲池,供后续写入使用
我的建议:不要把磨损均衡和垃圾回收做成两个独立的模块。把它们合并成一个“块管理器”,统一决策。这样代码更简洁,也更容易调优。
4.5 实际工程中的避坑指南
我踩过的坑,分享给你:
- 阈值设得太激进:曾经把静态均衡阈值设到5%,结果系统一直在搬数据,性能掉了40%。后来调到15%,世界清净了。
- 没考虑写放大:静态均衡每搬一次数据,写放大就+1。如果SSD本身写放大是3,加上静态均衡可能到5。要提前算好。
- 冷热数据识别不准:别只看时间戳。我见过一个数据库,数据写入后3个月才被读取一次,但中间被标记为“热数据”。建议用访问频率+最后访问时间综合判断。
重要提醒:磨损均衡不是越均衡越好。过度均衡会导致写放大失控,反而缩短寿命。建议设定一个“均衡窗口”——比如最大最小擦写次数差不超过20%就停止均衡。
好了,磨损均衡这块就聊到这儿。记住一句话:动态均衡保下限,静态均衡提上限,两者协同才是王道。
公众号:蓝海资料掘金营,微信deep3321