CephCVI · RBD BackupRBD Backup
Incremental export with export-diff
What you'll learn
- Take an incremental export
- Maintain the snapshot chain it depends on
- Reconstruct a full image from a chain
- Handle a broken chain
Prerequisites
None — start here.
Verified against Ceph Tentacle 20.2.x · Ceph Squid 19.2.x (supported previous) · cephadm matches the verified Ceph release · podman 4.x · csi-rbd and csi-cephfs current · RBD / CephFS / RGW current (matches Ceph release) · Linux kernel 5.15+ (5.10 minimum) · Ubuntu 24.04 LTS (Ceph host baseline) · Debian 12 (Bookworm) (Ceph host baseline) · Rocky Linux / RHEL / AlmaLinux 9.x (Ceph host baseline) · Proxmox VE 9.x (cross-course integration) · Kubernetes 1.31+ (cross-course integration) · 2026-08-18
Why this matters in production
Full exports do not fit nightly windows past a certain size, and incremental export is what makes off-cluster backup sustainable.
Taking an incremental
# Substitute your own values before running:
POOL=rbd-vms
IMAGE=vm-disk-01
# the base, once
rbd snap create "$POOL/$IMAGE@base"
rbd export-diff "$POOL/$IMAGE@base" /backups/image01-base.diff
# Substitute your own values before running:
POOL=rbd-vms
IMAGE=vm-disk-01
# each subsequent run
rbd snap create "$POOL/$IMAGE@$(date +%Y%m%d)"
rbd export-diff --from-snap base \
"$POOL/$IMAGE@20260818" /backups/image01-20260818.diff
ls -lh /backups/image01-*.diff
The diff contains only the objects that changed between the two
snapshots, which for a typical VM is a small fraction of the image.
# Substitute your own values before running:
POOL=rbd-vms
IMAGE=vm-disk-01
# chained: each night's diff is from the previous night
rbd export-diff --from-snap 20260817 \
"$POOL/$IMAGE@20260818" /backups/image01-20260818.diff
| Strategy | Diff size | Restore complexity |
|---|---|---|
| Always from base | grows over time | base + one diff |
| Chained from previous | small and constant | base + every diff in order |
| Periodic new base | resets both | base + diffs since that base |
Chained diffs are smallest and make restore depend on every link. A
weekly full or new base bounds that dependency.
Maintaining the chain
The snapshot named in --from-snap must still exist on the source image.
Removing it breaks the next incremental.
POOL=rbd-vms
IMAGE=vm-disk-01
rbd snap ls ${POOL}/${IMAGE}
# Substitute your own values before running:
POOL=rbd-vms
IMAGE=vm-disk-01
# retain at least the snapshot the next incremental will reference
rbd snap ls "$POOL/$IMAGE" --format json | python3 -c '
import sys,json
s = sorted(json.load(sys.stdin), key=lambda x: x["id"])
print("newest (next --from-snap):", s[-1]["name"] if s else "none")
print("removable:", [x["name"] for x in s[:-2]])'
# Substitute your own values before running:
POOL=rbd-vms
IMAGE=vm-disk-01
# remove older ones, keeping the most recent two
rbd snap ls "$POOL/$IMAGE" --format json | python3 -c '
import sys,json
s = sorted(json.load(sys.stdin), key=lambda x: x["id"])
for x in s[:-2]: print(x["name"])' | \
while read snap; do rbd snap rm "$POOL/$IMAGE@$snap"; done
Reconstructing
# create the target and apply the base
SIZE=100G
POOL=rbd-vms
rbd create --size ${SIZE} ${POOL}/image01-restored
rbd import-diff /backups/image01-base.diff ${POOL}/image01-restored
POOL=rbd-vms
# then every diff, in order
for d in $(ls -1 /backups/image01-2026*.diff | sort); do
echo "applying $d"
rbd import-diff "$d" "$POOL/image01-restored" || { echo "FAILED at $d"; break; }
done
POOL=rbd-vms
rbd snap ls "$POOL/image01-restored"
rbd info "$POOL/image01-restored"
import-diff refuses to apply a diff whose starting snapshot is not
present on the target, which is what makes out-of-order application
fail loudly rather than silently corrupting.
A broken chain
| Break | Recovery |
|---|---|
| A diff file lost or corrupt | restore to the last good diff; take a new full export |
The --from-snap snapshot removed on source | take a new full export and start a new chain |
| A diff applied out of order | import-diff refuses; apply in order |
| The target diverged | restore to a fresh image, not the diverged one |
# verify the chain applies before relying on it
SIZE=100G
POOL=rbd-vms
rbd create --size ${SIZE} ${POOL}/chain-test
for d in $(ls -1 /backups/image01-*.diff | sort); do
rbd import-diff "$d" ${POOL}/chain-test || { echo "CHAIN BROKEN at $d"; break; }
done
rbd rm ${POOL}/chain-test
Quiz
Knowledge check · 4 questions
Q1. Why take a periodic new full export when using chained diffs?
Q2. Diffs can be applied to the target in any order.
Q3. Set up incremental backup for an RBD image.
A 4 TiB image cannot complete a full export in the nightly window. The team wants incrementals.
Q4. What must remain on the source image for the next incremental to work?
Passing score: 75%. Answers are checked in this browser.
Production discipline
Alert on any failed nightly export-diff — a silent gap breaks every
subsequent diff and only surfaces during a restore. Take a new full base
periodically to bound both the restore step count and how much a single
lost file invalidates.
Cross-course references
- Kubernetes: incremental volume snapshots carry the same chain dependency
- Linux: incremental backup chains fail at their weakest link, silently until restore