Skip to main content
RunBook Academy

LinuxXLI · Storage PerformanceFilesystem tuning

Writeback and filesystem tuning - dirty pages, mount options and fstrim

Advanced⏱ ~20 minfstrimfindmntiostat

What you'll learn

  • Read Dirty and Writeback in /proc/meminfo and relate them to the vm.dirty_* limits
  • Explain why the percentage-based dirty limits behave badly on large-memory hosts
  • Choose atime and journal mount options from the workload rather than from a tuning guide
  • Compare inline discard against periodic fstrim and justify the default

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11

Not yet marked complete on this device.

The previous lesson established that a large share of storage latency is added above the block layer. This one is about the two places it is added most often: the writeback path, and the filesystem’s own decisions about journalling, timestamps and discard.

Every knob here has a defensible default. The point is not to change them - it is to know which one to reach for when a measurement says something is wrong, and to know what each change costs.

Writeback: the dirty page limits

A buffered write() makes a page dirty and returns. Writeback drains dirty pages to the device in the background. Two thresholds govern it:

sysctl vm.dirty_background_ratio vm.dirty_ratio \
       vm.dirty_expire_centisecs vm.dirty_writeback_centisecs
vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
  • dirty_background_ratio - at this fraction of available memory dirty, kernel writeback threads start flushing in the background. The application is not affected.
  • dirty_ratio - at this fraction, the writing process is blocked until writeback catches up. This is the throttle.
  • dirty_expire_centisecs - a dirty page older than this (30 s here) becomes eligible for writeback regardless of the thresholds.
  • dirty_writeback_centisecs - how often the writeback threads wake to check (5 s here).

Watch the live values:

grep -E '^(Dirty|Writeback|MemAvailable):' /proc/meminfo
Dirty:            418204 kB
Writeback:         12288 kB
MemAvailable:   43221108 kB

Dirty climbing toward dirty_ratio of MemAvailable while Writeback stays small means the device cannot keep up and throttling is imminent.

Measure the effect on both sides. The device side:

iostat -x 5 12

Look for wkB/s becoming steadier rather than arriving in bursts, and r_await on the same device losing its spikes. The application side is whatever your workload reports as write latency - the two must be checked together, because smoothing writeback can slightly reduce peak sequential throughput while substantially improving read latency, and only you know which one your service is judged on.

Mount options

findmnt -no TARGET,FSTYPE,OPTIONS -t ext4,xfs
/ ext4 rw,relatime

atime

Every read of a file updates its access timestamp, which is a write. The three behaviours:

  • strictatime - update on every access. A read-heavy workload turns into a write workload. Almost never wanted.
  • relatime - the default. Update only if the existing atime is older than mtime/ctime, or older than a day. Cheap, and keeps atime useful enough for “has this file been read recently” tooling.
  • noatime - never update. Cheapest.

noatime is the classic first recommendation in tuning guides, and on a modern kernel relatime has already removed most of the cost. Measure before assuming: on a mail spool or a large static file cache with millions of reads it is still worth having; on a database whose data lives in a handful of large files it changes essentially nothing, because those files are read constantly and their atime is already fresh.

# /etc/fstab
UUID=b0434124-e13c-4dff-9006-5abf403266fe / ext4 defaults,noatime 0 1

Some applications do read atime - mutt uses it to detect new mail, and some backup and tiering tools use it to find cold data. Check before setting noatime on a filesystem you do not own.

ext4 journal mode

data=ordered     default. Metadata journalled; data written before
                 its metadata commits. Protects against reading
                 stale blocks after a crash.
data=writeback   metadata journalled, data ordering not enforced.
                 Faster; a crash can expose stale block contents
                 in files that were being extended.
data=journal     data AND metadata journalled. Everything written
                 twice. Slowest, strongest.

data=writeback is the one that appears in tuning guides with the caveat omitted. The risk is not filesystem corruption - the filesystem stays consistent - it is that after a crash a file extended just before the crash can contain whatever was previously in those blocks, which may be another file’s data. On a host handling data belonging to more than one tenant, that is a security consideration and not only a durability one.

commit=nrsec controls how often the journal commits (5 seconds by default). Raising it batches more work per commit and reduces f/s in iostat, at the cost of widening the crash window by the same amount.

XFS

XFS has fewer knobs by design and its defaults are good. The two worth knowing are logbsize=256k, which enlarges the in-memory log buffer and helps metadata-heavy workloads, and allocsize=, which sets the speculative preallocation size for streaming writes. noatime applies the same way. Do not go looking for an XFS equivalent of data=writeback; there is not one, because XFS never journals file data.

Discard: inline or batched

An SSD needs to be told which blocks are no longer in use, or its garbage collector works against stale data and write amplification climbs. Two ways to tell it.

Inline discard - the discard mount option - issues a TRIM as part of every delete. It sounds efficient and often is not: on many devices a discard is a slow, synchronous command, so file deletion becomes slow and the discards arrive in a stream of tiny ranges the device handles poorly.

Batched discard - fstrim - walks the free space and issues large discard ranges in one pass. This is the default on every mainstream distribution, delivered as a timer:

systemctl cat fstrim.timer
[Unit]
Description=Discard unused filesystem blocks once a week
Documentation=man:fstrim
ConditionVirtualization=!container
ConditionPathExists=!/etc/initrd-release

[Timer]
OnCalendar=weekly
AccuracySec=1h
Persistent=true
RandomizedDelaySec=100min

[Install]
WantedBy=timers.target

Note what that unit does with everything from the scheduling part: Persistent=true so a host that was off catches up, RandomizedDelaySec=100min so a fleet does not trim simultaneously, and AccuracySec=1h because a weekly maintenance job does not need to fire at a precise instant.

Check it is enabled and see what it reclaims:

systemctl list-timers fstrim.timer
sudo fstrim -av
/boot: 743.6 MiB (779616256 bytes) trimmed on /dev/sda1
/: 412.1 GiB (442495008768 bytes) trimmed on /dev/sda2

Read-ahead, checked against the workload

cat /sys/block/sda/queue/read_ahead_kb
8192

Read-ahead is how far past a detected sequential pattern the kernel reads. A large value helps sequential scans - backups, analytics table scans - and hurts random-access workloads, where it fills page cache with data that will never be read and consumes device bandwidth doing it.

Confirm which kind of workload you have before changing it:

iostat -x 5 3

Compare rareq-sz - the average read request size in kB - against the filesystem block size. Requests much larger than the block size mean the kernel is merging sequential reads and read-ahead is working. Requests at or near the block size mean random access, and raising read-ahead will make things worse.

Persist any change with a udev rule rather than an rc.local echo, so devices added later inherit it - the pattern is in the queue depth lesson.

Knowledge check

Knowledge check · 5 questions

  1. Q1. Why are the percentage-based dirty limits a poor fit for a 512 GB host?

  2. Q2. Setting vm.dirty_bytes leaves vm.dirty_ratio in effect as a second, independent limit.

  3. Q3. What is the actual risk of mounting ext4 with data=writeback?

  4. Q4. Which observations would justify running fstrim with --dry-run before the real thing? Select all that apply.

  5. Q5. Which iostat field tells you whether read-ahead is doing useful work?

Passing score: 75%. Answers are checked in this browser.