Log-Structured File Systems

Journaling accepted the double-write cost to keep the old in-place layout. In 1991 Mendel Rosenblum and John Ousterhout asked a more radical question: what if writing to the log was not the overhead but the entire file system? Their Log-Structured File System (LFS) treats the whole disk as one enormous append-only log. Nothing is ever overwritten in place; every new or changed block — data and metadata — is simply appended to the end of the log in one long sequential stream.

The motivation was a hardware trend that is even truer today. Disk seeks barely improved while bandwidth and RAM sizes exploded. Big RAM caches absorb most reads, so the disk's job becomes dominated by writes — and the fastest thing a disk (or an SSD) can do is a large sequential write, with no seeking. LFS is built to make every write sequential. Buffer thousands of small random updates in memory, then flush them as one giant contiguous segment, and you convert a seek-bound workload into a bandwidth-bound one. The ideas that felt exotic in 1991 now run inside every SSD and every flash-aware file system.

Buffer into segments, append forever

LFS never issues a small write to the disk. It accumulates dirty blocks in an in-memory segment (typically 512 KB–several MB) and writes the segment out whole, sequentially, once it is full. Because a block's location changes every time it is rewritten, the classic problem appears: where is a file's inode now? In a normal file system inodes live at fixed spots; in LFS they wander down the log with everything else.

Reads still work: consult the checkpoint region → find the imap → find the inode → find the block. The imap is small and heavily cached, so in practice reads cost about the same as any other file system. Writes, meanwhile, are always one big sequential append — the point of the whole design.

The catch: garbage collection

Appending forever has an obvious problem — you run out of disk. When you overwrite block 5 of a file, LFS writes a new version further down the log and the old version becomes dead garbage, still occupying space in an old segment. Over time segments become a patchwork of live and dead blocks. To keep going, LFS must reclaim that space with a cleaner (garbage collector / segment cleaning):

This is LFS's Achilles heel. Cleaning is extra I/O that competes with real work, and its cost depends on utilisation u — the fraction of a segment that is still live. Cleaning a segment that is u live yields only (1-u) of a segment's worth of free space, but you must read and rewrite the u live part. When segments are nearly full of live data, cleaning thrashes. LFS therefore tries to clean cold (rarely-changing, so stably-live or stably-dead) segments and leave hot ones to accumulate more deaths before touching them.

Simulate a cleaning pass

The model below holds several segments, each a mix of live and dead blocks. The cleaner scans them, copies the live blocks into a new compact segment, and reports how much space it reclaimed and how much I/O the copying cost — the tension at the heart of LFS.

// Segment cleaning. Each segment is an array of blocks; a block is "dead" if it was overwritten later. const SEG = 8; // blocks per segment const segments: boolean[][] = [ // true = live, false = dead [true, false, false, true, false, false, true, false], [false, false, true, false, false, true, false, false], [true, true, false, false, false, false, false, false], ]; let liveCopied = 0, deadReclaimed = 0; const newSegment: string[] = []; segments.forEach((seg, s) => { const live = seg.filter((b) => b).length; const dead = SEG - live; liveCopied += live; deadReclaimed += dead; seg.forEach((b, i) => { if (b) newSegment.push(`seg${s}:blk${i}`); }); const u = ((live / SEG) * 100).toFixed(0); console.log(`segment ${s}: utilisation ${u}% -> copy ${live} live, reclaim ${dead} dead`); }); console.log(`\nlive blocks copied forward (the cleaning cost): ${liveCopied}`); console.log(`free blocks reclaimed: ${liveCopied + deadReclaimed} - ${liveCopied} = ${deadReclaimed}`); console.log(`new compact segment holds: ${newSegment.length} live blocks`);

Flash cannot overwrite a byte in place: you can write a fresh page, but to re-use a page you must erase its whole surrounding block (hundreds of pages) first, and each cell tolerates only a limited number of erasures. So the SSD's controller runs a Flash Translation Layer that does exactly what LFS does — write every update to a fresh location, keep a map from logical to physical address, and run a garbage collector to reclaim pages whose data has been superseded, spreading erasures evenly to wear-level. Rosenblum and Ousterhout designed LFS for spinning disks whose seeks were slow; the design turned out to be the native shape of flash. This is why "write amplification" and "over-provisioning" — LFS cleaning concepts — are the vocabulary of every SSD datasheet, and why flash-friendly file systems like F2FS are explicitly log-structured.

A frequent misconception is that a log-structured file system reads fast because everything is "in order." The opposite can be true. LFS optimises write throughput by turning random writes into one sequential stream; it says nothing kind about read layout. In fact a file that was written a few blocks at a time over its lifetime ends up scattered across many segments down the log, so reading it back sequentially may require hopping all over the disk — the very fragmentation LFS created by never overwriting in place. LFS bets that big RAM caches absorb most reads so this rarely bites, and that write speed is what's scarce. When that bet is wrong (large files read cold, sequentially), LFS read performance can disappoint. Know which side of the trade you are optimising.

A bonus: crash recovery almost for free

Because LFS only ever appends, crash recovery is easy — a pleasant side effect of the design. After a crash, LFS starts from the last checkpoint and rolls forward through the segments written since, incorporating the complete ones and discarding a torn tail. There is no whole-disk \texttt{fsck} and no separate journal, because the log is the journal. This is the same insight the copy-on-write file systems of the next lesson push even further: if you never overwrite live data, consistency stops being something you bolt on and becomes a property of the layout itself.