Backup & DRIV · Consistency, Integrity and Proof of RestorabilityConsistency
Filesystem consistency: open files, buffered writes and quiescing
What you'll learn
- Trace a write from the application through the page cache, the journal and the device cache to durable storage
- Choose between fsync, sync and fsfreeze for a given consistency gap
- Identify the hazards a file-level walk of a live filesystem cannot represent
- Apply the quiesce, snapshot, release, copy-from-the-snapshot sequence to a running service
Prerequisites
Verified against restic 0.19.1 · BorgBackup 1.4.5 · rclone 1.75.0 · MinIO (S3-compatible object storage) RELEASE.2025-09-07T16-13-09Z · OpenZFS 2.4.1 · LVM2 2.03.31(2) · btrfs-progs 6.17.1 · PostgreSQL 18.6 · pgBackRest 2.59.1 · Kubernetes (k3s) and etcd k3s v1.36.3+k3s1, etcd 3.7.1 · Velero 1.18.2 · Docker Engine 29.7.2 · Proxmox Backup Server (documentation only) 4.0.10-1 · Ubuntu (host baseline) 26.04 LTS · 2026-08-28
Crash-consistent and application-consistent, the distinction the previous
lesson drew, is a statement about what a copy contains. This lesson is about
where the missing part was sitting at the instant the copy was taken. Between
the moment an application calls write() and the moment those bytes are
durable, the data passes through several staging areas that a block-level
snapshot cannot see into and that a file-level walk sees into only by accident.
Knowing which layer holds what, when your backup fires, is the whole of
filesystem consistency.
Four layers between write() and durable storage
A successful write() does not mean the data is on a disk. It means the kernel
has copied the bytes into the page cache and marked those pages dirty. The
call returns immediately. Writeback happens later — when kernel threads get
round to it, when memory pressure forces it, or when something explicitly asks
for it. Until then the only copy of that data is in volatile RAM.
Below the page cache sits the filesystem journal. Its job is narrower than most people assume: it records metadata changes (and, in data-journalling modes, the data too) so that after an unclean shutdown the filesystem can be replayed into a structurally valid state. Journal replay guarantees that directories, inodes, extents and free-space maps agree with each other. It guarantees nothing about whether the contents of your files form a coherent application state. A filesystem can be perfectly clean and hold a half-written record.
Below that sits the device write cache. The fsync(2) manual page says
that flushing a file to the disk device includes writing through or flushing a
disk cache if present, and that the call blocks until the device reports the
transfer complete — but the same page records that implementations in older
kernels and lesser-used filesystems do not know how to flush disk caches, and
that there the caches must be disabled with hdparm(8) or sdparm(8) to
guarantee safe operation.
And then there is the block layer, where LVM, ZFS, Btrfs, a SAN array and a cloud volume service all take their snapshots. A snapshot at that layer is taken beneath the filesystem and beneath the page cache. It contains precisely the blocks the device has been handed. Everything still dirty in RAM is absent. Everything the application has buffered in its own memory and not yet written is absent twice over.
That is why an unassisted block snapshot produces a crash-consistent image and nothing better. It is, byte for byte, what a power cut at that instant would have left behind, and whether it is usable depends on what the filesystem journal and the application’s own recovery can make of it.
Three hazards a walk of a live filesystem cannot represent
The temptation, when a snapshot layer is unavailable, is to copy harder: tar,
rsync, cp -a over the live tree, perhaps twice, perhaps with a checksum
pass. Three hazards defeat all of it.
A file rewritten in place while the walk is reading it. A tree walk reads files one at a time over seconds or hours. Any file modified after the walk started and before the walk reached it is captured in its new state; any file modified while the walk is partway through is captured as a mixture of old and new — a state that never existed on the origin at any instant. The resulting archive records a normal-looking size and mtime for it.
The measured version of this is a plain copy of a running PostgreSQL data directory, taken with no involvement from the database, while a workload ran.
$ pg_ctl -D /work/naive-copy startwaiting for server to start.... done
server started
>>> exit code: 0
2026-08-28 13:34:37.611 UTC [94] LOG: database system was interrupted; last known up at 2026-08-28 13:34:36 UTC
2026-08-28 13:34:37.613 UTC [94] LOG: database system was not properly shut down; automatic recovery in progress
2026-08-28 13:34:37.613 UTC [94] LOG: redo starts at 0/17615F8
2026-08-28 13:34:37.634 UTC [94] LOG: invalid record length at 0/256A8D8: expected at least 24, got 0
2026-08-28 13:34:37.634 UTC [94] LOG: redo done at 0/256A8B0 system usage: CPU: user: 0.01 s, system: 0.00 s, elapsed: 0.02 s
2026-08-28 13:34:37.639 UTC [88] LOG: database system is ready to accept connections
rows readable from the naive copy : 45000
rows in the live database : 45000It started. It ran crash recovery, exactly as it would after a power cut, and
happened to have every WAL record it needed inside the copy. It returned the
same 45000 rows as the live database. And none of that established that the
copy was a transaction-consistent image of any instant — cp walked the tree
over several seconds while pages were being written underneath it. The capture
puts the danger plainly: a practice that usually appears to work, and has no
defined failure signal when it does not, is more dangerous than one that fails
loudly.
Files that must agree, captured at different instants. A service’s state is rarely one file. A database’s data files and its write-ahead log must correspond. When those live on separate volumes and only one volume is captured, the parts disagree. This arrangement is common: data on one volume, WAL on another, and a snapshot schedule covering the data volume.
$ pg_ctl -D /work/nowal startwaiting for server to start.... stopped waiting
pg_ctl: could not start server
Examine the log output.
>>> exit code: 1
2026-08-28 13:34:37.879 UTC [132] LOG: creating missing WAL directory "pg_wal/archive_status"
2026-08-28 13:34:37.879 UTC [132] LOG: creating missing WAL directory "pg_wal/summaries"
2026-08-28 13:34:37.879 UTC [132] LOG: invalid checkpoint record
2026-08-28 13:34:37.879 UTC [132] PANIC: could not locate a valid checkpoint record at 0/2F20158
2026-08-28 13:34:37.941 UTC [126] LOG: startup process (PID 132) was terminated by signal 6: Aborted
2026-08-28 13:34:37.941 UTC [126] LOG: terminating any other active server processes
2026-08-28 13:34:37.942 UTC [126] LOG: shutting down due to startup process failure
2026-08-28 13:34:37.943 UTC [126] LOG: database system is shut downThis failure is loud, which is the lucky case. The checkpoint record telling the server where redo must begin lived in WAL the copy never contained, so start-up panicked and stopped. The dangerous variant is the one where both volumes are captured, but a few seconds apart — nothing panics, the service starts, and the disagreement between the two halves surfaces later as corruption nobody can date.
Open files with no name. A process can hold an open descriptor on a file
that has already been unlinked. The data is still readable through the
descriptor and the blocks are still allocated, but the file has no entry in any
directory. A tree walk cannot see it at all, because a walk enumerates
directory entries. A block-level snapshot does capture the blocks, but it
captures them under an inode with a link count of zero that nothing in the
snapshot holds open — the exact condition a filesystem’s orphan handling exists
to clean up. Either way the data is gone from the copy, and the only symptom
beforehand is that df and du disagree about how full the filesystem is. Log
files rotated while a daemon still holds the old descriptor are the everyday
version.
fsync, sync and fsfreeze close different parts of the gap
None of those hazards is closed by copying more carefully. Three mechanisms narrow the distance between the page cache and the block layer, and they are not interchangeable.
fsync(2) acts on a single file descriptor and is called by the application
itself. It is the precise tool: the process that knows which file
matters, and in what order relative to others, asks for that one to be made
durable. A database’s durability guarantee is built out of fsync calls
in a specific sequence, and nothing outside the application can issue them
correctly, because nothing outside it knows the ordering.
sync is the blunt system-wide instrument: flush every dirty page belonging to
every filesystem. It closes the page-cache gap for data that has already been
written, at one instant. What it does not do is stop anything: the moment
it returns, writers are dirtying pages again, and a snapshot a quarter of a
second later meets a fresh gap. sync before a snapshot is better than nothing
and is not a consistency mechanism.
fsfreeze is the one that gives you a window instead of an instant. In the
words of its manual page it halts any new access to the filesystem and creates
a stable image on disk, and it holds that state until the matching fsfreeze -u
releases it.
# fsfreeze -f /srv/dataThree properties of fsfreeze matter operationally. Its argument is a mount
point, not an arbitrary directory, so it affects every writer on that
filesystem and not only the service you care about. It is not universal either:
the manual page lists btrfs, ext2/3/4, f2fs, jfs, nilfs2, reiserfs and xfs as
the filesystems supporting the freeze feature, and notes the list may be
incomplete — so on a filesystem outside that list the freeze step cannot be
assumed to exist, and the design has to hold consistency elsewhere. And the
same page records
that fsfreeze is unnecessary for device-mapper devices, because device-mapper
and therefore LVM automatically freeze a filesystem on the device when a
snapshot creation is requested. If your snapshot mechanism is LVM, the freeze is
already happening; the thing that is still missing is the application.
Quiesce, snapshot, release, then copy from the snapshot
The three mechanisms above assemble into one sequence, and it is the sequence rather than any single mechanism that closes all three hazards.
Quiesce first. Ask the application, in its own protocol, to reach a state it
can be restarted from and to hold there. This is pg_backup_start for
PostgreSQL, a read lock plus flush for other engines, a guest-agent freeze hook
inside a virtual machine, a writer framework on Windows. The application
defines what consistent means; the storage layer cannot.
Snapshot second, with the filesystem frozen — explicitly with fsfreeze,
or implicitly by device-mapper if you are on LVM. The snapshot is an instant,
not an interval, which is what removes the rewritten-in-place hazard. If the
service spans several volumes, every one of them must be captured in the same
operation, or you have manufactured the second hazard yourself.
Release third, and treat it as mandatory rather than cleanup: a freeze blocks every writer on the filesystem, so an error between freeze and thaw is an outage.
MNT=/srv/data
SNAP_CMD=/usr/local/sbin/take-volume-snapshot
# Register the thaw before taking the freeze: an error in between must not
# leave the mount point frozen against every writer on the system.
trap 'fsfreeze -u "$MNT"' EXIT
sync
fsfreeze -f "$MNT"
"$SNAP_CMD" "$MNT"
fsfreeze -u "$MNT"
trap - EXIT
Copy from the snapshot, finally — never from the live filesystem. This is the step that converts a brief freeze window into an unhurried copy, however long it takes, because the snapshot does not change while it is read. The LVM capture demonstrates the property the whole pattern rests on: a snapshot taken at 09:00 still served the 09:00 ledger after an operator truncated the file on the origin at 09:30.
$ md5sum /mnt/snap/orders.csvorigin orders.csv now:
ORDER-9999,0.00
snapshot orders.csv still:
ORDER-1001,4500.00
ORDER-1002,1250.00
md5 read from snapshot : 9eb4e2ad8e08e1dcaaf87ababab964b0
md5 recorded at 09:00 : 9eb4e2ad8e08e1dcaaf87ababab964b0
MATCH - the snapshot still holds the 09:00 ledgerThat stable view is the one thing a snapshot genuinely gives you, and it is what makes a long copy meaningful. What makes it restorable is the quiesce before it.
Production discipline
- Involve the application before you involve the storage. A quiescing hook that returns success is evidence about application state; a snapshot with no hook is evidence only about block state. If a service has no quiesce mechanism, record that its backups are crash-consistent rather than pretending otherwise.
- Capture every volume a service spans in one operation. Data on one
volume and WAL on another, snapshotted separately, produced
PANIC: could not locate a valid checkpoint record at 0/2F20158on PostgreSQL 18.6 — and the silent version, where both are captured seconds apart, is worse. - Guarantee the thaw before you take the freeze. Register the release path first, so that a failure in the snapshot step cannot leave a mount point frozen against every process writing to it.
- Copy from the snapshot, never from the live tree. The snapshot does not move under the reader; the live filesystem is what produced the mixed-state file in the first place.
- Prove the copy starts, then prove it is right. The naive copy in this lesson started, completed crash recovery and returned 45000 rows. Starting is the weakest possible evidence, and it is the evidence most restore tests stop at.
Cross-course references
- Linux for Production Sysadmins — Part XIV (Filesystems) covers journal modes, writeback and mount-time recovery in the depth this lesson treats as a single layer; you need it to reason about what a freeze had actually flushed before the snapshot was taken.
- PostgreSQL for Production Sysadmins — Part XII (WAL, Checkpoints and
Crash Recovery) explains why the checkpoint record in
pg_walis what makes a data directory startable at all, which is precisely what the missing-WAL capture above could not supply. - Proxmox VE for Production Operators — Part IX (Virtual Machines) covers the guest-agent path that runs freeze and thaw hooks inside a guest, which is the quiescing sequence of this lesson implemented from one layer below the guest filesystem.
Quiz
Knowledge check · 5 questions
Q1. A block-layer snapshot is taken of a volume holding a running application, with no involvement from the application and no freeze. What does the snapshot contain?
Q2. A cluster keeps its data directory on one volume and pg_wal on another, and only the data volume is snapshotted. What happened when that copy was started?
Q3. Running `sync` immediately before a snapshot makes that snapshot application-consistent.
Q4. A backup walks a live filesystem tree with tar. Which of these does the resulting archive fail to represent correctly? Select all that apply.
Q5. A service is snapshotted nightly with LVM and no application involvement. State the two changes that would move it from crash-consistent to application-consistent, in order.
Passing score: 75%. Answers are checked in this browser.