Reported symptoms
The array holding rbdr-app-01 is lost at 02:14. The guest is restored from
the previous night’s hypervisor image onto replacement storage, powers on,
reaches multi-user.target and answers SSH inside four minutes. The on-call
engineer marks the restore successful and moves to the application.
The postgresql unit for that cluster then fails, within two seconds, every
time. Its log ends with a PANIC naming a checkpoint record the server cannot
locate, and the startup process being killed by signal 6. Three weeks earlier
this same guest passed its monthly restore test with every assertion green.
The first hypothesis on the bridge is that the restore was truncated.
Evidence provided
$ grep -E '^(agent|name|scsi[0-9]|mode|script):' /etc/pve/qemu-server/141.conf /etc/vzdump.conf/etc/pve/qemu-server/141.conf:agent: 0
/etc/pve/qemu-server/141.conf:name: rbdr-app-01
/etc/pve/qemu-server/141.conf:scsi0: rbdr-store:vm-141-disk-0,size=40G
/etc/pve/qemu-server/141.conf:scsi1: rbdr-store:vm-141-disk-1,size=20G
/etc/vzdump.conf:mode: snapshotIllustrative output
Read what is there and what is not. agent: 0 means the hypervisor had nobody
to ask for a filesystem freeze; the grep matched no script: line, so no
pre-backup hook ran either; mode: snapshot means the guest kept serving
traffic throughout. The guest is also not one disk.
$ ssh rbdr-app-01 df -h /var/lib/postgresql/18/main /var/lib/postgresql/18/main/pg_walFilesystem Size Used Avail Use% Mounted on
/dev/sda1 39G 12G 26G 32% /
/dev/sdb1 20G 3.1G 16G 17% /var/lib/postgresql/18/main/pg_walIllustrative output
The data directory rides the root filesystem on scsi0; pg_wal is a separate
volume on scsi1. Nothing on either side of the hypervisor boundary knew a copy
was in progress, and nothing tied the two disk images to one instant. The course
capture reproduces the resulting failure signature deliberately.
$ pg_ctl -D /work/nowal start waiting 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 downThe same capture records the neighbouring case, and it is the more dangerous
one. In PART A the capture ran cp -a $PGDATA /work/naive-copy mid-workload,
with no database involvement at all, and then started the copy. It ran
automatic recovery to completion and served every row.
$ pg_ctl -D /work/naive-copy start waiting 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 : 45000The monthly restore test report is short.
$ cat /srv/rbdr-restore-test/rbdr-app-01-2026-08-01.reportrbdr-restore-test 2026-08-01 vm 141 rbdr-app-01
image restored from rbdr-pbs : ok (18m41s)
guest powered on : ok
guest reached multi-user.target : ok
ssh answered on port 22 : ok
RESULT: PASSIllustrative output
Against that, the platform’s application-consistent path on the same database:
$ recover the base backup to the target time, then count and checksum the business data rows recovered : 50000 (expected 50000)
sum(amount) : 825025000 (expected 825025000)
RECOVERED - row count and business checksum both match the pre-DELETE stateWork the evidence before reading on
- The guest boots and the database does not. Which layer’s copy is under suspicion, and which layer’s restore is not?
- Two virtual disks, one running guest, no freeze. What has to be true for the control file on one disk and the WAL on the other to describe the same instant?
- The capture shows an uncoordinated copy failing and an uncoordinated copy
succeeding. What does that pair tell you about
systemctl is-activeas a restore-test assertion? - The bridge suspects a truncated restore. What single measurement settles that, and where does the number to compare against have to come from?
- Three weeks of a passing monthly test. What exactly was it measuring?
Root cause
The copy was taken while the guest ran, with nothing coordinating it
With agent: 0 and no guest agent, the hypervisor could not ask the guest to
flush and freeze its filesystems, and nothing asked PostgreSQL to bracket the
copy. Without a freeze, nothing makes the scsi0 and scsi1 images correspond
to a single instant of guest I/O.
The restored control file therefore names a checkpoint the restored WAL cannot
supply, which is the invalid checkpoint record and could not locate a valid checkpoint record pair above. PostgreSQL states the boundary plainly: a
file-system-level copy of a running cluster is a valid backup only when it is
an atomic snapshot of the entire cluster, or when it is bracketed by
pg_backup_start and pg_backup_stop and accompanied by every WAL segment
written during the copy. This copy was neither.
The restore test was built to a bar a broken image usually clears
The monthly test restored the image, powered the guest on, waited for
multi-user.target and checked SSH. All four assertions are true of a guest
whose database cannot start, so the test could not detect this fault and
reported that incapability as a pass.
Raising the bar to “the unit is active” would not have been enough. The capture shows an equally uncoordinated copy that started, completed automatic recovery and returned all 45000 rows at exit 0. As the capture puts it: 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.
Resolution
Stop restarting the guest. Each attempt is another automatic recovery pass against the same image, and recovery writes. Copy the restored disks first, so the evidence and the fallback both survive what follows.
Recover the database from a database-aware source, not from the machine image. The capture shows what that path returns: a base backup rolled forward through archived WAL, checked against a recorded property of the business data. If this cluster has no base backup and no WAL archive, say so on the bridge in those words. The image still returns the operating system, the packages and the configuration, which shortens a rebuild - but the cluster data inside it is not a recovery point.
Then close the capture defect before the next window: install and enable the guest agent so the hypervisor can freeze the guest filesystems for the moment of the snapshot, and add a database-native backup with WAL archiving so the database has a recovery path that does not depend on disk-image timing.
Verification
The recovered cluster reaches a consistent recovery state and accepts
connections with no invalid checkpoint record line in its startup log. Read
the log for the absence of the fault, not merely for a running server - the
capture contains a start that succeeded on a copy nobody should trust.
Copy the check the capture performs: not that the server started, but that the data it holds matches an independently recorded property of the business data. Row count and summed amount are read together; a row count alone survives a great deal of damage.
The rewritten restore test connects over the network and diffs a query result against a stored expectation:
RESTORE_HOST=rbdr-restore-app-01
EXPECTED=/srv/rbdr-restore-proof/rbdr-expected.txt
psql -h "$RESTORE_HOST" -U rbdr_check -d orders -Atc \
'select count(*), sum(amount) from orders' > /srv/rbdr-restore-proof/rbdr-actual.txt
diff -u "$EXPECTED" /srv/rbdr-restore-proof/rbdr-actual.txt
The test passes on an empty diff. The boot assertions stay, as preconditions rather than as the verdict. Finally, a backup taken with the guest agent enabled is restored into an isolated target and started, so the capture defect is shown closed rather than assumed closed.
Prevention
Make the restore test assert that the application served. A query result compared against a recorded expectation - not a boot, not an SSH banner, not an active unit. That one change would have surfaced this three weeks before the array failed.
Enable guest coordination, then prove it. Installing the guest agent is the
configuration change; a restore that starts without an invalid checkpoint record is the evidence. Configuration with no restore behind it is an
intention.
Back databases up with database-aware tooling and treat the machine image as operating-system and configuration recovery. A base backup plus an archived WAL stream expresses a recovery point the hypervisor cannot.
Record the size and digest of every image as it is written, and alert on the age of the last restore test that reached the application rather than on the age of the last restore test. The first is what let the bridge discard the truncated-restore hypothesis in minutes; the second is what stopped a green monthly test and an unrecoverable database from looking identical for three weeks.
Write down which guests hold databases. Coordination matters most where somebody has put a stateful service inside a machine the backup schedule treats as generic.