Repository navigation
feat(vm): live migration for Cloud Hypervisor VMs - #303
Merged
Merged
Conversation
vm migrate capture/receive/abort build on the CH diff snapshot series. Disk deltas come from comparing reflinked extents, so a round costs what the guest changed. Transfer, ownership and round count stay with the caller. A generated VM ID could never collide, so prereserveVM rolled back on a failed reservation. With --id that rollback would delete the live record of the VM holding the ID, so it goes.
A receive killed after launch left a VMM on the run dir, and a retry with the same --id started a second one on the same socket. The retry now runs the stale-create reclaim first.
An abort of an old series resumed a guest paused for a newer one, which would split the VM across two nodes. A new baseline now removes older directories and abort acts only while its directory exists, keeping it until the guest resumes so a retry can finish. A capture killed between the memory and disk writes left rounds out of step that only surfaced at the receive. Each round now checks first. The envelope follows the record each round, since a NIC added mid-series does not end the series.
SnapshotSequence releases the ops lock before PersistSnapshotDir reads its directory, so sweeping every snapshot-* dir could delete a snapshot save in progress. Series directories get their own prefix and the baseline removes only those; the shared prefix keeps them under the GC sweep.
A short delta already fails at CopyN, and a missing series directory already fails before the pause at stampFiles.
| if err != nil { | ||
| return err | ||
| } | ||
| defer out.Close() //nolint:errcheck |
Fold the series file copy into cloneSnapshotFiles, which drops a classifier type. Replace fileStamp with os.FileInfo. ValidID stops compiling a regexp at every process start for a rarely used flag. The per-round envelope write moves before the pause, so its fsync leaves the downtime window. Remove comments the names and tests already carry, including those fix commits added. vm start resuming a received guest is the normal path, so the orphaned pause warning no longer blames an interrupted capture.
writeRoundDelta commits the delta and then renames the next reference. A kill between the two leaves rounds in step on a stale reference, so a range that went hole, data, hole compares equal and the replay keeps the old bytes. The residual next reference marks that window; the check runs before the pause and takes the existing path: end the series, resume the guest, delete the directory. A delta after a final round has the same stale reference. It takes a vm start outside the documented flow, so the docs state the rule.
This was referenced Nov 4, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
cocoon vm migrate capture|receive|abort, which moves a running Cloud Hypervisor VM to another node with its processes and memory intact. Cocoon captures and restores. The caller moves the files, picks the round count and decides which node owns the VM. Firecracker returnsErrMigrateUnsupported.cocoon vm migrate abort my-vm --series 123gives up at any point before that. The contract is indocs/snapshots.md, section Live Migration.How it works
35b098c65260or newer, which addsPUT /vm.end-snapshot-series. An older VMM answers 404 on the first capture and nothing changes.migrate. The guest gets fresh NICs like a clone, so it needs the post-clone network setup.Hot-path cost: no new I/O on claim, clone, restore, snapshot or create. Clone gains a flag read, a branch and one string check per snapshot file, about 33 ns per clone of 5 files in a benchmark.
snapshotVMmarshals a small struct instead of a map.Behaviour changes outside the feature
prereserveVMno longer callsRollbackCreatewhen the reservation fails. A failed reservation writes nothing, and with--idthat rollback would delete the live record holding the ID.FinalizeClonetakes the metering reason. Clones still emitclone.Test plan
Real cross-host runs between two bare-metal nodes (AMD EPYC 9Y24, XFS), kit torn down afterwards:
--no-direct-io: sha256 matches with a 3.7 GiB final deltacopyandmmap;ondemandis rejected by CHqemu-img checkis clean, cold boot reaches login. The hash differs once the VMM opens the file, because the dirty bit makes it rebuild refcountsvm startorabortrecovers the guest, a killed delta fails its retry and discards the series, a killed receive retries with one VMMvm netup and down,vm disk attachanddetach,snapshot save, clone,vm restore,vm hibernateand wake,vm stop,e2fsck -fnreturning 0 on reflinked copies of both disks, cold boot, guest reboot, then another migration with matching sha256Timings with a 2 GiB guest under light load: baseline capture about 0.3 s (8 GiB about 0.7 s), final capture about 0.13 s, receive about 0.26 s,
vm startabout 0.12 s. Guest downtime was not measured directly: the runs hashed both disks during the pause. The sum with a 16 MB final round over a 57 MB/s link is about 1.4 s.On
7395cde6these were rerun: migrate and migrate back, default balloon, restore modes, old series ID, SIGKILL during a delta, NIC swap and mid-series NIC add, truncated delta, and the lifecycle set. Heavy writes, the qcow2 overlay, SIGKILL during baseline and receive, and the three refusals last ran on earlier commits of this branch.95921b85came after that run and has no hardware run of its own. It refuses a delta when a capture was killed between committing a disk delta and moving its reference. A unit test plants that residue; no real kill was injected into the window.Gates on
7395cde6:make lint0 issues on linux and darwin,make fmt-checkandaslclean,go test -race ./...passes in a Linux container.Not tested: TSC across hosts of different frequency, nested virtualization, Windows guests, a real orchestrator as the caller, network connectivity after a cold boot, reboot or restore of a migrated VM, and the same lifecycle set on a qcow2 overlay beyond stop and cold boot. Firecracker's refusal has a unit test only. All runs used the same
root_dirandrun_dirpaths on both nodes.Known limits
vm clone --from-diron a series directory pairs the last memory with baseline disks. The docs forbid it; there is no guard.gc, like any clone failure. Reading the code, the existing cgroup sweep collects it;gcwas not run in these tests.TestStartSequenceRelaunchesWhenTheScannedVMMDiedBeforeTheLockis flaky on the base commit too (1 of 60 under load); this branch does not touch that path.