SD System Design In Depth
0%
lesson system-designstorage-engines

Storage engine design constraints

Module 2.3: Storage Engine Internals & Blob Systems Updated Sep 17, 2026
📄 Cheat Sheet
#03 - Database Storage: Files & Pages ✸ Neon Database Talk (CMU Intro to Database Systems)
CMU Database Group #03 - Database Storage: Files & Pages ✸ Neon Database Talk (CMU Intro to Database Systems) Watch on YouTube ↗

Andy Pavlo builds the storage hierarchy, sequential-vs-random I/O and why a DBMS manages its own pages instead of trusting the OS: the exact constraints this unit covers.

0:00 / --:--

Under the Hood: Hardware Hierarchy & Kernel I/O Constraints

A storage engine is the low-level software component responsible for persisting, indexing, and retrieving records from physical non-volatile storage. Storage engine design is fundamentally constrained by the physical physics and kernel interfaces of modern computer architectures:

  • Memory Latency Gap: L1 CPU Cache (~1ns) vs DRAM (~100ns) vs NVMe SSD (~10-100µs) vs HDD seek (~5-10ms). Accessing disk is 1,000x to 100,000x slower than accessing main memory.
  • Sequential vs Random I/O: On mechanical HDDs, random I/O incurs a physical actuator arm seek and platter rotation penalty (~100 IOPS, ~1MB/s). On NVMe SSDs, flash memory cannot be overwritten in place; cells must be written in 4KB/16KB pages and erased in 2MB/8MB blocks. Random writes trigger severe SSD controller garbage collection and high Write Amplification Factors (WAF). Sequential writes achieve over 5GB/s on PCIe Gen4 NVMe.
  • Linux Page Cache & VFS: Standard POSIX write() writes to the OS kernel page cache (DRAM dirty pages). Durability requires calling fsync() or fdatasync() to flush physical drive write buffers to non-volatile NAND cells. Some engines (InnoDB when configured with O_DIRECT, ScyllaDB) bypass the page cache using O_DIRECT and manage their own buffer pools, while others such as PostgreSQL deliberately use buffered I/O plus fsync.

The Storage Engine I/O Pipeline

flowchart TD App["Application Thread"] -->|"1. write(fd, buf, len)"| VFS["Virtual File System (VFS)"] VFS -->|"2. Dirty Page"| PageCache["Kernel Page Cache (DRAM)"] PageCache -->|"3. Periodic flusher / fsync()"| BlockLayer["I/O Scheduler & Block Device"] BlockLayer -->|"4. NVMe PCIe Queue"| Controller["SSD Controller Flash Translation Layer (FTL)"] Controller -->|"5. Erase Block & Program Page"| FlashNAND[("Physical NAND Flash Chips")]

Under the Hood: fsync vs fdatasync vs O_DIRECT

Ensuring ACID durability without catastrophic latency spikes requires precise systems programming:

  • fsync(int fd): Requests persistence of a file's modified data and metadata required by the filesystem's durability contract. Publishing a newly created or renamed filename may also require synchronizing the containing directory; a file fsync alone does not make that directory entry durable.
  • fdatasync(int fd): Requests persistence of data and metadata required to retrieve it, such as a changed file size, while it may omit unrelated metadata. Whether it saves I/O depends on the filesystem, workload, and device.
  • O_DIRECT and O_DSYNC solve different problems. O_DIRECT requests direct I/O with filesystem- and device-specific alignment constraints; O_DSYNC changes write-completion semantics and does not itself bypass the page cache. Engines that use direct I/O must manage buffering, prefetching, and eviction explicitly.

Worked example: why group commit exists

Suppose an OLTP service needs 20,000 committed transactions per second, and each commit must survive power loss. Assume a device where one fdatasync takes about 1 ms (typical for consumer NVMe without power-loss protection; enterprise drives with capacitors can be far faster, a spinning disk is 5-10 ms).

  • One flush per commit: at most about 1,000 commits per second per log. The target is missed by 20x.
  • Group commit: commits that arrive while a flush is in progress wait and are flushed together. If each flush carries 25 commits, 1,000 flushes per second yields 25,000 commits per second. Each commit waits roughly one flush interval, trading a little latency for a lot of throughput.

PostgreSQL, MySQL InnoDB and RocksDB all implement variants of this: the log is a single sequential file, and the durability point is the flush, not the write.

Buffered vs direct I/O

ApproachWho cachesStrengthCostUsed by
Buffered I/O plus fsyncOS page cache (plus DB buffers)Simple; kernel readahead and write-back for freeDouble caching; eviction and write-back timing controlled by the kernelPostgreSQL
Direct I/O (O_DIRECT)The engine's own buffer poolPredictable memory use, no double caching, engine controls evictionAlignment rules; engine must implement prefetch and write-back itselfInnoDB (commonly configured), ScyllaDB, optional in RocksDB
Memory-mapped filesOS page cache via page faultsVery little code; zero-copy readsLittle control over flush order and eviction; I/O errors surface as signals; stalls on page faultsLMDB (reads); CMU lecture argues against it for general DBMSs

Failure modes worth knowing

  • fsync error handling: in 2018 PostgreSQL developers found that on Linux a failed fsync could drop the dirty pages, so a retried fsync reported success while data was lost. PostgreSQL now treats fsync failure as fatal and recovers from the WAL.
  • Volatile device caches: a drive that acknowledges flushes without persisting them (or with write caching and no power-loss protection) breaks every guarantee above. Engines rely on the device honoring flush or FUA commands.
  • Torn pages: a 16 KB database page is larger than the atomic write unit of most devices, so a crash can leave half a page. InnoDB's doublewrite buffer and PostgreSQL's full-page writes exist for this reason.
Check yourself Question 1 of 3
Durable new files recall
From: Under the Hood: fsync vs fdatasync vs O_DIRECT

An engine creates a new SSTable file, writes it, calls fsync on the file descriptor and returns success. The machine then loses power, and after reboot the file does not exist. What was missing?

Architecture Challenge: B-Tree vs LSM-Tree Write Amplification
Advanced
Context / Scenario: Workload: 500,000 writes/sec with sequential vs random I/O constraints on NVMe SSDs.
Problem: Why do high-throughput write-heavy stores (like Cassandra and RocksDB) prefer append-only LSM-trees over traditional B-Trees?
A LSM-Trees eliminate disk storage entirely by keeping everything in RAM.
B LSM-Trees do not require compaction or background sorting.
C B-Trees require random page writes and in-place updates, causing severe write amplification and lock contention compared to append-only sequential writes.
D B-Trees cannot support range scans.

Key Takeaways

Storage engines exist to bridge the 100,000x latency gap between DRAM and persistent physical disk.
Flash storage cannot overwrite in place; random writes cause internal erase block garbage collection and wear.
Many engines use fdatasync() to skip unneeded metadata flushes, and some (InnoDB, ScyllaDB) use O_DIRECT to avoid double-caching, while PostgreSQL relies on buffered I/O plus fsync.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue