VM Incremental Backup (CBT-Based)#
Overview#
KubeVirt's CBT-based incremental backup lets you capture only changed disk blocks since the last checkpoint, using libvirt's native backup API backed by QCOW2 dirty bitmaps. It is gated by the IncrementalBackup feature gate (Alpha, v1.6.0) and requires Changed Block Tracking to be active on the VM.
Core CRDs:
VirtualMachineBackup— initiates and tracks a single backup run (full or incremental)VirtualMachineBackupTracker— persistsLatestCheckpointacross backup runs, enabling incremental chains
Backup modes :
| Mode | Mechanism |
|---|---|
| Push | libvirt writes directly to QCOW2 files on the attached target PVC |
| Pull | libvirt exposes data over an NBD Unix socket; a tunnel proxies it to the backup consumer |
Key source files:
| File | Purpose |
|---|---|
pkg/storage/cbt/backup.go | VMBackupController — backup lifecycle orchestration |
pkg/virt-launcher/virtwrap/storage/backup.go | libvirt integration: BackupBegin, checkpoint create/redefine |
pkg/storage/cbt/backup_target_pvc.go | Utility volume attach/detach for the backup target PVC |
pkg/storage/backend-storage/backend-storage.go | Persistent-state PVC creation and sizing |
Backup Lifecycle#
The VMBackupController.sync() drives the state machine :
-
Prerequisites — checked in
checkPrerequisites():- VM exists and VMI is running
- CBT state ==
Enabledon the VMI - No live migration in progress
CheckpointRedefinitionRequiredis not pending on the tracker- Backup target PVC exists and is filesystem mode (not block)
-
Full vs. incremental —
isIncrementalBackup()returnstruewhen!ForceFullBackupand the tracker has a non-emptyLatestCheckpoint.Name. When incremental,backupOptions.Incrementalis set to the checkpoint name . -
Start —
startBackup()attaches the target PVC as a utility volume, then issues aBackupgRPC command to virt-launcher . -
virt-launcher execution —
backup():- Calls
generateDomainBackup()to produce backup and checkpoint XML - Checkpoint name format:
{backupName}-{timestamp}(e.g.vmbackup-fedora-2026-07-04_15-41-59) - Each disk with a data store gets
Checkpoint="bitmap"in the checkpoint XML - Optionally freezes the guest filesystem (quiesce) before calling
dom.BackupBegin()
- Calls
-
Completion —
HandleBackupJobCompletedEvent()handles the libvirtDomainEventJobCompletedevent, writing success/failure into the metadata cache. The controller then callsupdateBackupTracker()to advanceLatestCheckpointwith the new checkpoint name and disk info .
Checkpoint Lifecycle and VM Restart Recovery#
libvirt checkpoint metadata lives only in the running domain — a VM restart wipes it. KubeVirt works around this with checkpoint redefinition (added in PR #16448):
Normal flow:
- On backup completion,
updateBackupTracker()stores the checkpoint name and per-disk volume info in theVirtualMachineBackupTrackerstatus . - On the next backup run, the controller reads
LatestCheckpoint.Nameand passes it to libvirt as the incremental base.
After VM restart:
- The CBT state transition sets
CheckpointRedefinitionRequired = trueon the tracker. - The tracker reconciliation queue detects the flag and calls the
RedefineCheckpointREST/gRPC endpoint . - In virt-launcher,
RedefineCheckpoint():- Queries QEMU via QMP
query-named-block-nodesfor current dirty bitmaps on each disk file - Builds checkpoint XML only for disks that still have the bitmap
- Issues
DOMAIN_CHECKPOINT_CREATE_REDEFINE | REDEFINE_VALIDATEto libvirt
- Queries QEMU via QMP
- On success:
CheckpointRedefinitionRequiredis cleared; pending backups proceed. - On corrupt/invalid bitmap (e.g.
ERR_CHECKPOINT_INCONSISTENT): checkpoint metadata is cleared, a warning event is emitted, and the next backup falls back to full .
Backups using a tracker source are blocked (initializing state) until redefinition completes .
Utility Volumes: Backup Target PVC Isolation#
The backup target PVC (where libvirt writes the backup output) is attached to the virt-launcher pod via the UtilityVolumes mechanism (PR #15922), gated by UtilityVolumes (Alpha, v1.7.0) .
Why utility volumes? They let privileged KubeVirt service accounts hot-attach a PVC directly to the virt-launcher pod without exposing it as a VM disk. The attachment is out-of-band from the VM's own storage, so backup output goes to a separate PVC and never appears inside the guest.
How backup uses them:
attachBackupTargetPVC()patchesvmi.spec.utilityVolumesto add aUtilityVolumeof typev1.Backuppointing to the user-provided PVC .detachBackupTargetPVC()removes the entry after the backup completes or is aborted, verified viacleanupVMIState().- The controller checks attachment readiness via
backupTargetPVCAttached()(hotplug-mounted phase) before sending the start command.
Constraints:
- Target PVC must be a filesystem PVC; block mode is rejected .
- Utility volumes are not migrated with the VMI; live migration waits up to 150 s for them to detach .
- Only manageable by KubeVirt service accounts (privileged API).
Persistent-State PVC: Sizing for CBT#
CBT stores one QCOW2 overlay file per VM disk at subPath=cbt/ on the VM's persistent-state PVC (the same PVC used for TPM state and persistent EFI). The base PVC size is hardcoded at PVCSize = "10Mi" .
The problem (issue #18352): 10 Mi is not enough for multi-disk VMs or accumulated checkpoint bitmaps. A real-world 3-disk VM observed these overlay sizes:
rootdisk.qcow2: 4.6 Mi (30 Gi virtual, 4 bitmaps)disk-black-worm-63.qcow2: 3.3 Mi (20 Gi virtual, 4 bitmaps)disk-coral-hummingbird-84.qcow2: partial/failed (creation error)
With the PVC at 93% utilization, adding a third disk's overlay fails with No space left on device, which puts the VM into a CrashLoopBackOff / CBT Initializing loop .
The fix (PR #18470, open): createPVC() checks cbt.HasCBTStateEnabled() and adds CBTBackendStateOverhead = "1Gi" to the requested storage when CBT is active. The 1 Gi default is described as a conservative value for common configurations; finer-grained sizing and bitmap retention policies are planned for future PRs .
Current workarounds (before PR #18470 merges):
- Manually expand the persistent-state PVC to 256 Mi–1 Gi.
- Configure a CDI storage profile with
cdi.kubevirt.io/minimumSupportedPvcSizelarge enough — the PVC creation already labels the PVC withLabelApplyStorageProfile = "true"so the CDI webhook can override size .
IsBackendStorageNeeded() returns true for VMs that have CBT state initializing or enabled, in addition to those with persistent TPM or EFI, so the persistent-state PVC is created whenever CBT is active.