2026-09-05
When your program calls write(), the bytes land in the page cache and the page is marked dirty. They're not on disk. The kernel's writeback subsystem is what eventually flushes them, and its design has a surprising consequence: writeback is per-block-device, not per-file.
Each block device gets a BDI (backing device info) structure. The BDI owns a list of dirty inodes and a dedicated kernel thread — you'll see them as [writeback] or historically flush-8:0 in ps. When dirty pages need to be flushed, that one thread walks the BDI's dirty list and issues I/O for every inode it finds, one after another.
Two knobs drive when it wakes up:
write() call itself blocks and helps flush. This is called throttling.The nasty part: fsync(fd) is documented as "flush this file's data to disk." But the implementation calls filemap_fdatawrite on your file's pages — and then, because the block layer's request queue is shared, your fsync often has to wait behind unrelated dirty pages queued ahead of it on the same BDI. On a busy device where another process just wrote 8 GB of logs, your 4 KB fsync can take tens of seconds.
Real example: a PostgreSQL server sharing a disk with a log-rotation cron. The cron rewrites 6 GB. Postgres's WAL fsync — normally 200 µs on NVMe — spikes to 18 seconds during rotation. The WAL file was tiny; the wait was for foreign dirty pages ahead of it in the BDI's queue. Fix: put WAL on a separate block device (its own BDI, its own writeback thread, its own dirty accounting).
Rule of thumb: on a 64 GB machine with default settings, ~6.4 GB of dirty pages can accumulate before background writeback starts, and ~12.8 GB before your writes stall. At a 500 MB/s SSD, draining 12.8 GB takes ~26 seconds — that's your worst-case fsync() latency for any file on that device. Lower dirty_background_bytes to a fixed small value (e.g., 128 MB) on latency-sensitive systems rather than relying on the percentage default.
Check what's dirty right now with grep -E 'Dirty|Writeback' /proc/meminfo.
