SwadhyAI Notes

All System Design notes

System Design · OS Storage Foundations

Filesystem Internals for System Design

Every distributed store eventually parks bytes on a local volume. Understanding , , , and is how you explain why a “disk write” is not one syscall and why databases obsess over flush order.

This is the single-machine foundation underneath chunkservers, KV data directories, and log volumes. It is not GFS/HDFS architecture and not a cloud file-product case study. Those build on these primitives—or deliberately bypass them with direct I/O.

Byte journey

1

Dentries resolve names to inode numbers.

2

Find which blocks hold the file.

3

RAM absorbs hits; dirty pages wait.

4

Journal + data hit stable storage.

What actually happens

System design interviews care about filesystems when estimating disk behavior, arguing for/against storing many tiny files, explaining fsync latency, or justifying why a storage engine uses direct I/O. The mental model is layered: application → VFS → concrete FS (ext4, XFS, NTFS, APFS) → block device → media.

Crash consistency is the recurring theme. Metadata structures (bitmaps, inode tables, directory trees) must not be half-updated. Journals or copy-on-write schemes make recovery bounded. Data durability is a separate promise you opt into with barriers.

Inodes, blocks, and extents

An does not contain the path. Multiple (hard links) can reference one inode. Symlinks are a different inode type storing a path string. Device nodes, sockets, and FIFOs are also inode types—useful when you see “files” that are not byte streams.

Classic Unix used direct, single-, double-, and triple-indirect pointers. Large files paid many metadata reads. Modern maps describe contiguous runs compactly, which matches how chunkservers and log files want to stream.

plaintext
inode (conceptual):
  mode, uid, gid, size, timestamps, nlink
  extent_tree: [(phys_block=1000, len=8), (phys=5000, len=4), ...]

directory "logs":
  ("app.log", inode=42)
  ("app.log.1", inode=43)

unlink("app.log"):
  remove dentry; nlink--
  if nlink==0 and no open fds: schedule free of blocks + inode

Internal fragmentation:
  1 byte file still occupies ≥ 1 block (plus inode)
  → millions of tiny files waste space and inode table slots

Path lookup and caches

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart TD
    Open["open(/var/log/app.log)"] --> Cache{dentry cache hit?}
    Cache -->|yes| Inode[Resolve inode]
    Cache -->|no| Dir[Read directory blocks]
    Dir --> Ent[Match name → inode #]
    Ent --> Inode
    Inode --> Perm[Check permissions]
    Perm --> File[Return file descriptor]
    File --> Read[read → page cache → blocks]

The and dentry cache are why “create 10 million files in one directory” becomes a control-plane nightmare for local disks and for DFS namenodes alike. Shard directories, use packed containers (tar, SSTables, object layouts), or a dedicated metadata store when cardinality explodes.

Page cache, writeback, and fsync

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    participant App as Application
    participant VFS as VFS / page cache
    participant J as Journal
    participant Disk as Data blocks
    App->>VFS: write(buf)
    VFS-->>App: returns (often cached)
    App->>VFS: fsync
    VFS->>J: commit metadata + needed data ordered
    J->>Disk: writeback data blocks as required by mode
    J-->>VFS: durable barrier complete
    VFS-->>App: fsync returns

A successful write() usually updates the . Dirty pages flush later via writeback. forces the durable point the application means by “committed to disk.” Renaming a file over an old one is a common atomic publish pattern—but only if the data was fsynced before the rename, and directory entries are made durable as required on that OS.

plaintext
Safe-ish replace pattern (simplified):
  fd = open("file.tmp")
  write(fd, data)
  fsync(fd)
  close(fd)
  rename("file.tmp", "file")   // atomic namespace update on same FS
  fsync(directory_fd)          // on some systems, required for crash safety

Without fsync:
  power loss → empty/truncated file or old bytes
  yet the app may have already ACKed the user

Storage engines that need predictable control use O_DIRECT to bypass the page cache, manage their own block cache, and issue explicit flushes. That avoids double caching and surprise writeback storms—at the cost of reimplementing caching wisely.

Journaling and crash consistency

Mode (ext4 vocabulary)What is journaledRisk / cost
data=writebackMetadataFastest; data may appear before metadata ordering guarantees
data=ordered (common default)Metadata; data written before related metadataBalanced; still need fsync for app durability
data=journalData + metadataSafest/slowest; doubles data write amp through journal

Copy-on-write filesystems (btrfs, ZFS-style) take a different approach: write new blocks, then atomically swing root pointers. Checksums and snapshots fall out naturally; fragmentation and free-space management become the hard parts. For interviews, say “journal vs COW” as two families of crash consistency, then pick based on workload.

Allocation, fragmentation, and SSD realities

Delayed allocation lets the FS wait until writeback to pick physical blocks, improving contiguity for large streaming writes. on HDDs shows up as collapsed sequential throughput. On SSDs, random vs sequential matters less for seeks but write amplification, queue, and queue matter more—especially if the FS and the device both garbage-collect.

Why tiny files hurt

Inode tables, directory entries, and minimum block occupancy dominate. Metadata IOPS explode. Prefer packing (one file many records), a KV/LSM store, or an object API with multipart only when objects are large.

When FUSE appears in designs

is a bridge to present remote or virtual storage as paths. Great for compatibility; measure latency. Hot data planes usually use native clients (HDFS client, S3 SDK, engined open) instead of FUSE on every read.

Quotas, sparse files, and holes

Sparse files logically large but physically small confuse capacity planning. Quotas may count logical or physical usage depending on FS. Always verify which metric your monitoring scrapes.

What system designers should take upward

Chunkserver / data dir

  • Few large files beat millions of tiny ones
  • Checksum in userspace; do not trust disk silently
  • fsync policies define acknowledged durability
  • XFS/ext4 tuning for large sequential allocation

Database / KV engine

  • WAL + fsync group commit on a dedicated volume
  • Optional O_DIRECT to own the cache
  • Place WAL and data on devices with clear failure domains
  • Monitor writeback and journal commit latency
plaintext
Back-of-envelope that uses FS reality:
  100k creates/sec of empty files:
    each create → journal metadata + dentry + inode
    HDD metadata IOPS cannot sustain this
    SSD helps but inode/dentry lock & allocator still bind

  1 GB/s sequential write of large extents:
    often device bandwidth bound if allocation stays contiguous
    fragmentation or COW amplification can cut this sharply

Memory-mapped files and concurrency

Reads through mmap still use the page cache. Writes dirty pages that must be msync'd (or equivalent) for durability—the same fsync lesson under another name. Concurrent writers need explicit synchronization; the filesystem does not serialize your store's logical transactions just because two processes mapped the same path.

Locking belongs in the application or engine: file locks (flock, POSIX locks) are advisory on many systems and do not substitute for a WAL protocol. Distributed systems that share a network filesystem (NFS) inherit weaker cache-coherency assumptions—another reason chunkservers prefer local disks.

plaintext
When mmap helps in SD contexts:
  + read-only SSTable / index files with warm page cache
  + shared read-mostly lookup tables in one process tree

When to avoid as primary write path:
  - unclear msync barriers vs crash windows
  - multi-writer mutation without an engine-level log
  - remote FS mounts with surprising consistency

Trade-offs to weigh

Buffered I/O vs direct I/O

Page cache absorbs bursts and simplifies apps; direct I/O gives engines control and avoids double caching.

Journal mode safety vs write amp

Full data journaling shrinks corruption windows but can halve effective throughput.

Many files vs packed layouts

POSIX convenience versus metadata scalability—the same lesson namenodes learn at cluster scale.

FUSE compatibility vs native clients

Mountable paths help humans and legacy tools; native RPCs win on hot paths.

Choosing and explaining filesystem internals

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

Files are inodes plus data blocks/extents; names live in directory entries that point at inodes.

The page cache makes write() asynchronous; fsync defines the durability barrier applications actually need.

Journals or COW provide crash-consistent metadata recovery; tiny-file workloads are metadata-bound.

When it belongs

You estimate local disk behavior under a storage node.

You justify packing, LSM files, or object layouts instead of millions of small files.

You explain why databases manage their own caches and flush orders.

How it fails

Acknowledging data before fsync and losing it on power loss.

Metadata meltdown from extreme file cardinality in one directory.

Writeback storms or double caching fighting the storage engine.

Trade-offs to weigh

Durability latency versus throughput (fsync frequency, journal mode).

Kernel cache convenience versus engine-controlled direct I/O.

POSIX file trees versus packed/containerized layouts for scale.

Common misunderstandings

Assuming close() equals durable persistence.

Confusing local FS journals with distributed commit protocols.

Blaming “disk is slow” when the bottleneck is metadata operations.

Wrap-up

Filesystem internals give you the vocabulary for everything that later becomes “a chunk on a chunkserver” or “an SSTable in a data directory.” Master inode vs name, cache vs fsync, and journal vs COW, and your distributed storage answers stop floating above the hardware.

Key takeaway

Paths resolve through dentries to inodes to extents. Durability is fsync-shaped. Metadata scale and crash consistency dominate real storage-node design long before raw sequential GB/s looks hard.