LinuxXLI · Storage PerformanceFilesystem tuning
Writeback and filesystem tuning - dirty pages, mount options and fstrim
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
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
Q1. Why are the percentage-based dirty limits a poor fit for a 512 GB host?
Q2. Setting vm.dirty_bytes leaves vm.dirty_ratio in effect as a second, independent limit.
Q3. What is the actual risk of mounting ext4 with data=writeback?
Q4. Which observations would justify running fstrim with --dry-run before the real thing? Select all that apply.
Q5. Which iostat field tells you whether read-ahead is doing useful work?
Passing score: 75%. Answers are checked in this browser.