TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

KubeVirt Live Migration Actual Measurement 1: The Guest Paused for 0.34 Seconds, But the Service Was Disconnected for 2.4 Seconds

Status: Measured (2026-10-31). These values were measured in a nested cluster with Kubernetes and KubeVirt v1.9.0 set up inside three TT Lab practice VMs. The baseline test was run five times, while the blocking condition and CPU model tests were run once per condition. All numbers are extracted from the raw data left behind for each migration run (VMIM phase records, libvirt job info, probe records). Writing rules: Measured data is the baseline; unmeasured items are marked as 「Unmeasured」; interpretations whose causes could not be verified are marked as 「Estimated」.

Table of Contents

1. Starting with the Conclusion

To fix a node or upgrade the kernel, you need to move the VMs on that node somewhere else. KubeVirt does this via live migration. Without shutting down the guest, memory is copied to another node, paused very briefly, and then resumed on the new node. Reading the documentation makes it sound like "almost no disruption," but when moving it ourselves, we found quite a few conditions.

VMs with heavy memory usage, node draining, and concurrent migration limits will be covered in Part 2.

2. What Live Migration Does

In KubeVirt, a VM is a QEMU process running inside a pod (virt-launcher) on a node. Since a pod cannot be "moved" to another node, KubeVirt performs the migration by spinning up a new pod on the target node, copying the guest state over, and then terminating the original pod . The object that initiates this process is VirtualMachineInstanceMigration(VMIM for short). virtctl migrate simply creates one of these objects.

라이브 마이그레이션: 게스트를 켠 채로 옮기기 부하 없는 1Gi VM, 5회 중앙값(실측) 원본 노드 대상 노드 ① 대상 파드 준비 · 2–4초 게스트 실행 중 virt-launcher 파드 A 새 파드 B 기동 빈 QEMU 가 기다림 ② 사전 복사 · 3–4회 반복, 약 88MiB/s 게스트 계속 실행 바뀐 페이지에 표시 받은 페이지 채움 아직 멈춰 있음 모두 합쳐 456MiB ③ 멈추고 마지막 복사 · 343ms 게스트 멈춤 남은 페이지 + CPU 상태 마지막 조각 받음 남은 것 ④ 대상에서 재개 파드 A 끝남 게스트 실행 파드 IP 는 새것 디스크는 옮기지 않는다 두 파드가 같은 RWX PVC(공유 스토리지)를 붙인다 ②는 1회차에 메모리 전체(0 으로 찬 페이지는 거의 안 보냄), 다음 회차부터는 그사이 바뀐 페이지만 보낸다.
Figure: Four stages of live migration: Target pod preparation, pre-copy, stop-and-copy, and resumption on target
  1. Target pod preparation. virt-controller creates a new virt-launcher pod on the target node. QEMU in this pod waits for an "incoming migration" without a guest. In our measurements, it took 2–4 seconds from creating the VMIM until the target pod was ready. If the target pod cannot be scheduled, it stops here(Sections 5 and 6).
  2. Pre-copy. QEMU on the source side sends the guest memory. Because the guest continues to run during this time, pages that were already sent are modified again. Therefore, the entire memory is sent in round 1, and starting from the next round, only the pages that changed (dirtied) in the meantime are resent. Zero-filled pages are hardly sent, so out of a 1.016GiB guest, the actual amount sent was 456MiB. The iteration repeated 3–4 times at a speed of about 88MiB/s.
  3. Stop-and-copy. Once the remaining amount can be sent within the target time, the guest is paused, and the remaining pages and CPU state are sent. This is when the guest actually pauses, which libvirt records as Total downtime . KubeVirt v1.9 starts the target at 300ms and increases it gradually if progress stalls (upper limit maxDowntimeMs default 900ms).
  4. Resumption on target. The target QEMU resumes the guest, and the original pod is terminated. Disks are not migrated. This is because both pods attach to the same PVC simultaneously, which requires the PVC to be RWX (ReadWriteMany), allowing it to be attached across multiple nodes (Section 5).

Data transmitting the memory flows between the virt-handler pods of the two nodes—that is, through the pod network. Unless a dedicated migration network is provided, it shares the same line as service traffic.

3. Measurement Environment and Method

Item Value
External Three TT Lab practice VMs (4 vCPU, 4GiB each, Ubuntu 24.04, nested virtualization). 1 control plane, 2 workers. The two workers are on physical servers with different CPU generations.
Internal Kubernetes kubeadm 1.36.4, containerd 2.3.5, Flannel v0.28.9
KubeVirt·CDI KubeVirt v1.9.0 (libvirt 11.10.0 in virt-launcher, QEMU 10.1.0, multifd 8 channels), CDI v1.66.0
Shared Storage Control plane kernel NFS + csi-driver-nfs 4.13.4, one StorageClass (RWX). local-path-provisioner v0.0.32 for RWO comparison
Guest Fedora 44 (1 vCPU, 1Gi memory, 6GiB root RWX, masquerade), cirros (128Mi) for conditional tests
Line Between Workers iperf3 10 sec × 3: 790–799Mbps (management network), 764–772Mbps (pod network)

There are three measurement methods.

The time taken to set up the environment is also noted: VM set preparation 276s, csi-driver-nfs 52s, KubeVirt Available 122s, CDI Available 91s, Fedora base disk retrieval 193s. We hit a snag once during CDI retrieval: if mountPermissions is not provided to csi-driver-nfs, the volume folder becomes root 0755, causing the importer pod (uid 107) to fail with permission denied in an endless restart loop. Since NFS does not change ownership via fsGroup, we resolved this by providing mountPermissions: "0777" to the StorageClass.

4. Baseline Measurement: 1Gi VM Without Load, Five Times

A Fedora VM (1Gi, RWX, masquerade, evictionStrategy: LiveMigrate) was round-tripped five times between the two workers. No migration policy was set (defaults used). The CPU was fixed to the named model Skylake-Client-v4 , which both workers could support. Section 6 explains why this was necessary.

Run VMIM Creation → Succeeded libvirt Elapsed Guest Pause Sent Amount Speed Iterations New Connection Outage Maintained Connection
1 11.4s 6680ms 413ms 459MiB 86.2MiB/s 4 2406ms 2205ms, dropped
2 9.0s 6652ms 490ms 456MiB 87.1MiB/s 4 2205ms 2005ms, dropped
3 10.3s 6262ms 343ms 455MiB 89.3MiB/s 3 2607ms 2605ms, dropped
4 9.1s 6400ms 338ms 456MiB 88.3MiB/s 4 1204ms 1203ms, dropped
5 10.1s 6208ms 342ms 456MiB 90.1MiB/s 4 2806ms 3007ms, dropped
Median 10.1s 6400ms 343ms 456MiB 88.3MiB/s 4 2406ms 2205ms, 5/5 dropped

It took about 10 seconds from creating the VMIM until completion, of which actual copying (libvirt elapsed time) took 6.4 seconds. The remainder consists of scheduling and spinning up the target pod (1–2s), target preparation (0–2s), and post-completion state cleanup.

4.1 Why a 0.34-Second Pause Becomes a 2.4-Second Outage

게스트는 0.34초 멈췄는데 서비스는 2.4초 끊겼다 부하 없는 1Gi VM 5회, 중앙값(범위), 실측 0초 1초 2초 3초 게스트 멈춤 libvirt 가 잰 값 343ms (338–490) 새 연결 무응답 Service 너머 탐침 2.4초 (1.2–2.8) 왜 다른가: masquerade 에서는 게스트가 파드 뒤에 숨는다 옮기기 전 클라이언트 Service 파드 A IP a → NAT → 게스트 TCP 연결을 맺어 둠 옮긴 뒤 클라이언트 Service 파드 B IP b → NAT → 게스트 엔드포인트가 B 로 바뀌어야 닿음 파드 A 가 사라지면 A 에 맺은 연결도 사라진다 유지하던 TCP 연결은 5회 모두 끊겼고, 다시 맺으면 됐다
Figure: Comparison of guest pause 343ms and new connection unresponsiveness 2.4s, illustration of pod replacement in masquerade

In masquerade , KubeVirt's default network binding, the guest is hidden behind a pod. The address visible from the outside is the IP of the virt-launcher pod, which passes traffic to the guest via NAT inside the pod. However, as seen in Section 2, migration is the process of creating a new pod and moving the workload there , so upon completion, the pod wrapping the guest changes and the pod IP also changes.

Therefore, you should not design under the assumption that "it will not disconnect because it is a live migration." You must ensure that clients re-establish disconnected connections and endure failures lasting a few seconds through retries. We did not measure the same values in bindings that are not masquerade (such as a separate network attached via Multus) (unmeasured).

5. Conditions That Prevent Migration

The VMI status includes LiveMigratable conditions. If False, the reason is provided alongside, and if you attempt to create a VMIM, the admission webhook rejects creation itself. We actually tested this by creating a cirros VM for each condition.

VM Disk and Network LiveMigratable Reason and Message (Verbatim) VMIM
RWO NFS StorageClass's RWO PVC False DisksNotLiveMigratable: PVC … is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode) Creation rejected
local-path local-path RWO PVC False Same message Creation denied
hostDisk containerDisk + hostDisk False DisksNotLiveMigratable: cannot migrate VMI with non-shared HostDisk Creation denied
bridge RWX disk, in pod network bridge False InterfaceNotLiveMigratable: cannot migrate VMI which does not use masquerade or a migratable plugin to connect to the pod network Creation denied
containerDisk (received as ctr) To the node, ctr images pull pre-pulled image True Failed 15 minutes and 4 seconds after Scheduling
containerDisk (pulled via crictl) the same image crictl pull after pulling again with True Succeeded

5.1 RWO is blocked even if the storage is NFS

KubeVirt does not check whether the storage can actually be attached across multiple nodes, but only the PVC access mode We will see. Even if sharing is originally possible because the storage is NFS, if the PVC was created as RWO, it cannot be migrated. When inspecting the actual environment as read-only, we found that the existing VM was exactly in this case. The storage's StorageProfile can provide RWX, but the code creating the VM specified the PVC as RWO, resulting in all LiveMigratable: False being generated. To make migration possible, the root disk must be recreated as an RWX PVC (by shutting down the VM and cloning/changing it to RWX, or using volume migration in v1.9). Volume migration was not measured this time (unmeasured). The discussion on access modes and volume modes is covered in Part 3 until the student VM starts.

5.2 containerDisk and ctr Pitfalls

containerDisk (where the disk is received as a container image) can be migrated. It is handled via block migration (BlockMigration), which sends the write layer along with it. However, in environments where images are pre-loaded onto nodes, such as in air-gapped networks, there was a pitfall.

This behavior has been reported as an issue upstream (kubevirt#19694). Until then, it is safe to pull pre-loaded images via crictl pull or verify that repoDigests has been populated via crictl inspecti after import.

5.3 Time Until Failure

The waiting time varies greatly depending on where it gets blocked. Conditions rejected by the webhook are known immediately, but if the target pod fails to schedule, it becomes Failed after 5 minutes, and if the target pod starts but fails to pull the image, it becomes Failed only after 15 minutes . To avoid waiting, you can cancel it by deleting the VMIM.

6. CPU Model: Which Nodes Can Be Migrated Between

Guests assume that they continue using the CPU features they had when first booting. If the destination node lacks those features, the guest may crash, so KubeVirt attaches a node selectorto the target pod to schedule it only on nodes with matching features. The CPU labels attached to each node (cpu-model.node.kubevirt.io/…, cpu-feature.node.kubevirt.io/…, cpu-model-migration.node.kubevirt.io/…) are applied by virt-handler's node labeller.

Since the two workers were on Intel hosts with different CPU generations (family 6, model 151 and 170), we were able to test the exact conditions directly. The node labeller assigned the same host-model nameto both workers, but the required features differed. One side had rfds-clear, and the other side had ddpd-u·rfds-no .

CPU configuration LiveMigratable Selector attached to target pod (measured) Result
Named Model Skylake-Client-v4 True cpu-model.node.kubevirt.io/Skylake-Client-v4 Succeeded
host-model (default) True cpu-model-migration.node.kubevirt.io/<원본의 host-model 이름> + 27 required features of the source node + vendor Target pod Unschedulable, Failed exactly 5 minutes later
host-passthrough True cpu-vendor.node.kubevirt.io/Intel Only one Succeeded, moved to a node of a different generation
A model that no node can provide cpu-model.node.kubevirt.io/<그 모델> The VMI itself stays in Scheduling (VM does not start)

There are three takeaways here.

  1. LiveMigratable does not calculate CPU compatibility. The host-model VM was True, but there was no place to move it, and the target pod stayed Pending for 5 minutes and unschedulable pod … timeout period exceeded ended with .
  2. The target condition for host-model is "same host-model name as the source + all required features of the source node". Even if the names are the same, if it lacks even a single feature, it will not work. And since the host-model name is merely the "closest named model," it does not indicate the generation. Two hosts of different generations received the same name.
  3. host-passthrough only checks the vendor. Therefore, it was moved even to a node of a different generation. Since this guest was checking features not present on the target (ddpd-u·rfds-no , etc.), it would have been dangerous if it were a program actually using those features. Passthrough must only be used between nodes with completely identical CPUs.

Since this relationship is not documented in the user guide, I submitted a documentation PR to add that section (user-guide#1052).

6.1 What Happens with Mixed Hardware

We applied the same rule (the rule verified empirically by the host-model selector above) to the labels of actual nodes consisting of four Intel nodes and one AMD node, and calculated the results. Excluding node names and grouping by pair type, it looks like this.

섞인 노드: CPU 설정별로 옮길 수 있는 쌍 두꺼운 칸은 중첩 클러스터 실측, 나머지는 노드 라벨로 계산 host-model (기본값) 이름 모델 공통 모델 지정 host- passthrough 같은 CPU 기능 라벨이 같음 ✓ 옮겨짐 ✓ 옮겨짐 ✓ 안전 host-model 이름만 같음, 기능 1–3개 차이 ✗ 5분 뒤 실패 ✓ 옮겨짐 ⚠ 옮겨짐, 위험 다른 세대 인텔 기능 8–15개 차이 ✗ 대상 없음 ✓ 공통 모델로 ⚠ 스케줄됨, 위험 인텔 ↔ AMD 공급사가 다름 ✗ 대상 없음 △ 옛 모델뿐 ✗ 대상 없음 ✗ 대상 파드가 뜨지 못한다. LiveMigratable 은 그래도 True 로 보인다. ⚠ 공급사만 보고 옮긴다. 게스트가 대상에 없는 기능을 보고 있었다. △ 공통 이름 모델이 IvyBridge 이하뿐이라 AVX2 를 잃는다. 다른 세대 인텔끼리의 공통 모델 예: Skylake-Client-v4
Figure: Movable node pairs by CPU configuration in mixed nodes: Same CPU, same host-model name only, different Intel generations, Intel and AMD
Node Pair Difference Seen in Labels host-model Named Model host-passthrough
Two with identical CPUs cpu-feature Labels are completely identical Supported Supported Supported (Safe)
Two with only the same host-model name 1–3 source required features missing on the target (rfds-clear one, or ddpd-u·rfds-no·vmx-any-errcode) Blocked on both sides Supported via common model Scheduled but risky
Intel of different generations 12 AVX-512 family features on one side, and waitpkg·bhi-ctrl and 8 others missing on the other Blocked on both sides Supported via common model Scheduled but risky
Intel and AMD Differ by 15–22 features (ranging from virtualization extensions to vmx·svm ) Blocked Common model is legacy only Blocked (different vendors)

This table is based on label calculations. The only thing verified in the nested testing is the single row for "two with only the same host-model name."

6.2 How to Check on Your Nodes

By comparing the CPU feature labels of two nodes, you can predict in advance whether a host-model VM can be migrated.

diff <(kubectl get node A -o json | jq -r '.metadata.labels|keys[]|select(startswith("cpu-feature."))') \
     <(kubectl get node B -o json | jq -r '.metadata.labels|keys[]|select(startswith("cpu-feature."))') \
  && echo "A·B CPU 기능 같음"

The named models that two nodes can provide together are the intersection of labels starting with cpu-model.node.kubevirt.io/ . For a VM to be migrated in a mixed node setup, you write one of them in spec.domain.cpu.model , or change the cluster default via the spec.configuration.cpuModel of the KubeVirt CR.

7. Conclusion

Item to Check Reason (Empirical finding in this post)
Is the PVC access mode RWX? If it is RWO, the webhook rejects it even if the storage is NFS.
Is the pod network binding masquerade (or a migratable plugin)? If it is bridge, it is rejected.
Was the containerDisk image added as ctr ? It pulls a digest missing on the target and fails after 15 minutes.
Was a CPU model specified? host-model fails after 5 minutes even if it appears True, and passthrough succeeds dangerously.
Does the client reconnect and retry? Even with a 0.34-second pause, new connections drop for 2.4 seconds and persistent connections are severed.

The empirical tests in this post can be followed step-by-step in TT Lab's hands-on course "KubeVirt Live Migration" (site path /course/kubevirt-live-migration). You follow the same VM set and setup to go through shared storage, KubeVirt, choosing a common CPU model, observing migration and downtime, observing RWO rejection, and draining. If KubeVirt's architecture is new to you, the series Until Student VMs Start Upand Reading KubeVirt Source Code will serve as a foundation.

TT's closing word. During the move, the guest waited at the door for 0.34 seconds, but the guest reported that the doorbell didn't work for 2.4 seconds. Because the house address (pod IP) changed! And the moving truck only goes to "neighborhoods with the same CPU features." Neighborhoods that only sound similar by name turn out to have no roads only upon arrival, so make sure to align your maps in advance.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment