LFS:The Design and Implementation of a Log-Structured File System(论文笔记)
发布于:
论文:The Design and Implementation of a Log-Structured File System(Rosenblum, Ousterhout,SOSP 1991 / 1992 扩展版)
LFS(Log-Structured File System)的经典点在于:它把文件系统写入路径重构成“只追加”的日志,把随机写压缩成顺序写,换取更高的写带宽;同时用 cleaner(清理/压缩) 在后台回收空间。它也是“日志结构 + 后台合并”这一类系统设计(LSM、现代对象存储、某些 copy-on-write FS)的源头之一。
本文的工程化关注点:
- 写优化到底解决了什么瓶颈:为什么小文件写在传统 FFS 上会被放大?
- 代价在哪里:cleaning 的读写放大、尾延迟抖动来自哪里?
- 关键机制:segment、segment summary、inode map、checkpoint、cleaner 策略。
全文不使用 Mermaid,图用 ASCII 画法,保证构建稳定。
1. 传统文件系统的写放大:小更新为何慢
典型的“就地更新(in-place update)”文件系统(如 FFS)在小写场景会遇到系统性放大:
- 数据块写:写数据本身;
- 元数据写:inode 更新、间接块更新、位图更新、目录项更新;
- 同步语义:为了崩溃一致性,元数据写可能需要强制落盘或依赖日志/排序。
于是一个“写 4KB 文件”的动作,可能触发多次随机写。磁盘在随机写下的带宽远低于顺序写,最终表现为吞吐不稳定、延迟拉长。
LFS 的一句话动机:既然随机写慢,那就把写变成顺序追加。
2. LFS 的核心抽象:把磁盘当成循环日志
2.1 Segment:日志的物理组织
- Segment:磁盘上顺序追加的“大块”,例如 512KB/1MB/几MB。
- 写入时把数据块与元数据块打包成一个 segment,顺序落盘。
Disk as a log:
+--------------------------------------------------------------+
| seg0 | seg1 | seg2 | seg3 | ... | segK | free ... |
+--------------------------------------------------------------+
^
|
tail (append point)
2.2 写路径:数据与元数据一起顺序写
简化写流程:
write(file, offset, data):
1) allocate new blocks in the log
2) write new data blocks to log tail
3) write updated inode (and indirect blocks) to log tail
4) update inode map (in-memory), later flushed as part of log
关键点:不修改旧位置的块,而是写新版本,并通过映射把“最新位置”指向新块。
3. 关键元数据:inode map 与 checkpoint
3.1 inode map(imap):inode 编号到磁盘地址
传统 FS:inode 固定位置(inode table)。
LFS:inode 随日志移动,固定位置消失;需要 imap 维护:
imap[inode_number] -> disk_address_of_latest_inode
为了让 imap 自身不成为随机写热点:
- imap 也被切成块(imap blocks)
- imap blocks 作为日志的一部分顺序写入
- 内存中维护最新版本,周期性落盘
3.2 checkpoint:崩溃恢复入口
磁盘上保留一个(或两个交替)checkpoint region,记录:
- 最新 checkpoint 时间点的 imap blocks 地址
- log tail 的位置等关键信息
Checkpoint region:
- pointer to imap block #0
- pointer to imap block #1
- ...
- log tail position
崩溃恢复时:
1) 读 checkpoint region 得到 imap 的入口 2) 从 log tail 附近扫描 segment summary,找到 checkpoint 之后的新写入,重建内存状态
4. Segment Summary:为什么需要“每段摘要”
因为日志里混杂了数据块、inode、imap block 等,清理与恢复需要知道“每个块是什么、属于谁、对应文件哪个偏移”。
所以每个 segment 带一个 summary:
Segment:
[segment summary][block1][block2]...[blockN]
summary entry:
- type: data / inode / imap / ...
- inode number
- file block number (for data)
- version / timestamp (optional)
它承担两类职责:
- recovery:扫描 segment 时快速识别有效块;
- cleaning:判断块是否仍是“最新版本”,还是已被更新淘汰。
5. Cleaning(清理/压缩):LFS 的代价与灵魂
5.1 为什么一定需要 cleaner
日志只追加会把旧版本留在盘上;随着写入推进,空间会耗尽。要回收空间,需要识别“过期块”,把仍然有效的块搬走,释放整段空间。
这和 LSM 的 compaction 本质类似:后台合并 + 回收旧版本。
5.2 Cleaner 的基本流程
clean():
1) select victim segments (old segments to clean)
2) read victim segments sequentially
3) for each block in segment:
if block is still live (latest):
copy it to the log tail (write new segment)
else:
drop it
4) mark victim segments as free
5.3 “live 判断”怎么做:版本验证
对于某个数据块(inode=i,file_block=b,addr=A):
- 通过 imap 找到最新 inode 地址
- 读 inode,得到 file_block b 的最新数据块地址 A’
- 若 A == A’,则该块仍 live;否则已过期
这一步看似会引入随机读;工程上会做批量化/缓存/把验证信息放在 summary 中降低成本。
6. Segment 选择策略:利用率与年龄
Cleaner 不是随便挑段;挑得不好会导致:
- 搬运活数据太多 → 写放大过大
- cleaning 跟不上前台写 → tail 逼近 head,系统抖动
论文里常见的启发式:结合 segment utilization(活数据比例)与 age(冷热)。
直觉:
- 低利用率段:可回收空间多,搬运少,性价比高。
- 冷数据段:不容易再被覆盖,搬运一次后可长时间不动,减少二次搬运。
可以用“成本/收益”类指标排序(示意):
benefit ~= (1 - utilization) * segment_size
cost ~= utilization * segment_size + read_cost
pick segments with high benefit/cost
7. LFS 的性能画像:什么时候快,什么时候抖
7.1 快的场景
- 小文件创建/追加写:把大量随机元数据写变成顺序日志写;
- 高写入并发:顺序带宽更容易吃满;
- 元数据密集:目录操作、fsync 频繁(假设 checkpoint 与写合并得当)。
7.2 抖的场景(cleaning 主导)
当系统进入“cleaning-heavy”状态:
- 前台写产生新日志
- 后台 cleaning 产生更多写(搬运 live data)
若 live data 比例高(例如覆盖率低、数据很冷),cleaning 的写放大可能接近或超过 2x、3x,导致:
- 写带宽被 cleaner 吞掉
- 前台延迟抬升,甚至出现 stall(没有足够干净 segment)
工程上往往需要:
- 给 cleaner 设置带宽上限/低优先级
- 预留足够 free segments(类似 LSM 预留空间)
- 做冷热分离(把冷数据聚集到少数段)
8. 崩溃一致性与恢复:checkpoint + roll-forward
LFS 的一致性设计偏“数据库式”:
- checkpoint 提供一个一致性快照点
- 崩溃后从 checkpoint 向前扫描日志(roll-forward)恢复到最新状态
恢复路径示意:
boot:
1) read checkpoint region -> imap roots + log tail
2) scan forward from last checkpoint to tail
3) apply segment summary entries to rebuild latest mappings
工程提醒:
- checkpoint 刷盘频率是性能与恢复时间的权衡;
- checkpoint 需要双写或校验,避免自身损坏导致无法启动;
- scan 的上界取决于“checkpoint 之后写入量”,会影响重启时长。
9. 与现代系统的对应关系
LFS 的思想在很多系统里以不同形式出现:
- LSM-Tree:memtable + WAL + SST + compaction,本质是“日志结构 + 后台合并”
- Copy-on-Write FS(ZFS/btrfs):写新块 + 指针更新;通过事务组/树结构维护一致性
- 对象存储/块存储:append-only + GC/compaction 的组合常见
差异在于:
- LFS 的地址空间是“文件系统块”,元数据组织与 POSIX 语义绑定;
- LSM 的 key-space 更适合做“版本淘汰”与“范围合并”;
- CoW FS 更强调树结构一致性与快照/克隆语义。
10. 工程实践清单(面向实现/运维)
10.1 设计层面的 checklist
- segment size 与写聚合策略:是否能稳定吃到顺序带宽?
- cleaner 策略:是否能在最坏情况下跟上写入?
- free segment 水位:何时触发 backpressure?
- checkpoint 频率:恢复时间是否可接受?
10.2 观测指标
- log 写入带宽、尾部推进速度
- cleaner 读带宽、写带宽、搬运率(live bytes copied / bytes cleaned)
- segment utilization 分布(冷热是否集中)
- stall 次数/时长(因无干净 segment)
- 重启恢复扫描时间
11. 小结
LFS 解决了“随机小写吞吐不稳定”的根问题:把写路径改为顺序追加;同时把代价转移到后台 cleaner。它的经典性来自于:
- 清晰的系统重构:数据 + 元数据一起日志化;
- 一套完整的恢复机制:checkpoint + roll-forward;
- 对 cleaning 代价的正面讨论:利用率/年龄驱动的选择策略。
工程视角下,LFS 的关键风险点也同样明确:cleaning 写放大 + 尾延迟抖动。这在后续几十年的存储系统里不断重演,成为“日志结构类系统”的长期主题。