TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

KubeVirt Live Migration Measurement Part 2: Busy Memory, Node Drain, Concurrent Limits

Status: Measured (2026-10-31). Part 1These values were measured in the same nested cluster (three TT Lab practice VMs, KubeVirt v1.9.0) as . Policy tests were conducted 1–3 times per condition, drain once per scenario, and concurrent limits once per limit. Writing convention: Measured data is the default, unmeasured items are marked as "Unmeasured", and interpretations whose causes could not be verified are marked as "Estimated".

Table of Contents

1. VM with Constant Memory Activity

The VM in Part 1 did nothing. Real VMs use memory. Pre-copy works by "resending pages modified during the transmission", so if the guest's modification rate is similar to the sending rate, it never finishes. We reproduced this scenario.

Test Load Policy Result Duration Guest Downtime Service Disruption (New / Maintained Connections)
light-1 128MB None Failed Live migration has been aborted 302s None (continues running on source)
heavy-1 512MB None Failed 301s None
heavy-2 512MB bandwidthPerMigration: 32Mi Failed 301s None
heavy-3 512MB allowAutoConverge: true Succeeded 33s 566ms 2807ms / 2805ms, Disrupted
uffd-3 512MB allowAutoConverge: true Succeeded 36s 547ms 1604ms / 1603ms, Disrupted
heavy-5 512MB 32Mi + autoConverge Succeeded 43s 433ms 4287ms / 4384ms, Disrupted
heavy-4 512MB allowPostCopy: true Failed Postcopy is not supported: Userfaultfd not available: Operation not permitted 2s None
heavy-6 512MB 32Mi + postCopy Failed(Same reason) 1s None
uffd-1 512MB allowPostCopy: true, node vm.unprivileged_userfaultfd=1 Succeeded(PostCopy) 303s Unmeasured 2204ms / 3006ms, Disrupted
uffd-4 512MB Same Succeeded(PostCopy) 304s Unmeasured 6214ms / 6670ms, Disrupted
uffd-2 512MB 32Mi + postCopy, node setting enabled Succeeded(PostCopy) 306s Unmeasured 6211ms / 6213ms, Disrupted
메모리를 계속 쓰는 VM: 정책별 결과 1Gi 게스트에 512MB 쓰기 부하, 결과가 날 때까지(실측) 기본(정책 없음) 실패 · 301초 대역폭 32Mi 실패 · 301초 autoConverge 성공 3/3 · 33–43초 postCopy, 노드 설정 없음 실패 2/2 · 1–2초 postCopy, 노드 설정 켬 성공 3/3 · 303–306초 0초 100초 200초 300초 왜 기본값으로는 끝나지 않나 게스트가 더럽히는 속도 74–92MiB/s ≈ 보내는 속도 → 매 반복 같은 양이 다시 남아 멈출 순간이 오지 않는다
Figure: Results and duration by policy under memory write load

1.1 Why Default Settings Fail to Complete

Reading ongoing libvirt task information every 5 seconds revealed that the unconverged runs repeated pre-copy 184–244 times over 300 seconds. The remaining data volume fluctuated between 15–90MiB, and the expected downtime required to send this was 1,782–1,913ms. It ultimately failed to reach the target (starting at 300ms and maxing out at 900ms). The guest's dirtying rate was about 19,000–23,000 pages per second (74–92MiB/s), which was nearly identical to the sending rate. The exact same amount remained after every iteration.

Even continuously writing only 128MB resulted in failure. During this time, the guest continued to run normally on the source, so there was no service disruption. However, because the VM could not be migrated, that node could not be drained (Section 2).

1.2 Summary by Policy

Policy Under 512MB Load Cost
None (Default) Failed to converge and gave up after about 300 seconds Node cannot be drained
Bandwidth Limit 32Mi Gave up similarly Limit was set, but actual transfer exceeded the limit (below)
autoConverge Succeeded in 33–43s (3/3), downtime 433–566ms Gradually throttles guest CPU to reduce the dirtying rate. Guest throughput degradation was not measured (Unmeasured)
postCopy Succeeded (3/3), but transitions to PostCopy only after exhausting the pre-copy timeout. Service disruption 2.2–6.7s Requires node vm.unprivileged_userfaultfd=1 . Losing the source or target during post-copy results in losing the guest (per documentation)

autoConverge is, as the name implies, a mechanism to "force convergence". When progress stalls, QEMU throttles the guest vCPUs slightly to lower the dirtying rate. Under this load, this feature alone changed the outcome from failure to success within the 30-second range.

postCopy works in the opposite direction. It starts running the guest on the target immediately, and any pages not yet transferred are fetched from the source the moment the guest accesses them. Because Linux's userfaultfd catches those moments, the node kernel must permit it, and Ubuntu's default was 0 (nested workers were also 0). Enabling it led to success, but KubeVirt waited until the pre-copy timeout expired before switching to PostCopy. Furthermore, since the guest's memory is split across both nodes during post-copy, the guest is lost if either node dies. For environments with busy VMs, trying autoConverge first is the order recommended by these measurements.

1.3 Bandwidth Limits Were Not Respected

bandwidthPerMigration: 32Mi When setting , the configuration was present across all layers. Both the applied VMI setting of 32Mi and QEMU's max-bandwidth: 33554432, virsh migrate-getspeed 32 were recorded. However, QEMU's info migrate throughput was 630.76Mbps (approx. 75MiB/s), and the amount processed over 10 seconds went from 17.316GiB to 18.040GiB (approx. 73.5MiB/s). Transmission occurred over 8 multifd channels. This is nearly identical to when there is no limit (86–90MiB/s).

Whether this is due to the relationship between multifd and speed limits or a KubeVirt-side configuration requires a control test with multifd disabled to distinguish (Unmeasured). Therefore, we currently recommend not trusting that "setting a limit protects the network" and instead verifying actual transfer volumes using virsh domjobinfo . For reference, the default bandwidth of 64MiB/s stated in the user guide is an old value, and the actual default in v1.9.0 was unlimited (0). A PR to fix the documentation is already open.

1.4 How to Apply Policies

MigrationPolicy selects targets by namespace or VMI label. This is an example of enabling only autoConverge in a namespace packed with busy VMs.

apiVersion: migrations.kubevirt.io/v1alpha1
kind: MigrationPolicy
metadata:
  name: busy-vms-auto-converge
spec:
  allowAutoConverge: true
  selectors:
    namespaceSelector:
      kubernetes.io/metadata.name: busy-vms

You can verify whether it was applied by checking the VMI's status.migrationState.migrationConfiguration and status.migrationState.migrationPolicyName after migration.

2. Node Drain

Live migration is most frequently used when draining nodes. Placing evictionStrategy: LiveMigrate on a VM causes kubectl drain to move the VM without shutting it down. We tested this path across four scenarios.

노드 비우기(drain)가 VM 을 옮기는 길 KubeVirt v1.9.0, evictionStrategy: LiveMigrate(실측) kubectl drain 파드 축출 요청, 5초마다 다시 승인 웹훅이 축출을 거절 PDB 가 아니다. VMI 에 축출 표시를 남김 축출 마이그레이션 생성 동시 한도(노드당 2)를 따른다 성공 원본 파드가 사라져 다음에 통과 실패하면 다시, 또 다시 수렴 못 함: 약 5분마다 포기 받는 노드 메모리 부족: 5분 Pending 뒤 실패 옮길 수 없는 VM: 끝없이 거절 실측: 한 대 22초, 다른 한 대 13초 되돌려도(uncordon) 축출 표시가 남는다 status.evacuationNodeName 이 그대로라 마이그레이션이 계속 생기고, 결국 VM 이 되돌린 노드를 떠났다 virtctl evacuate-cancel (v1.9.1 이상 virtctl)
Figure: The path drain takes to move a VM: eviction request, admission webhook rejection and eviction migration, success and failure, and eviction flags persisting after reversion
Scenario Target Result
A Drain node with a VM under 512MB load (LiveMigrate) (timeout 600s) Drain failed (600.9s). Webhook rejects with Evacuation in progress: Eviction triggered evacuation of VMI … and creates an eviction migration, but fails to converge, giving up and recreating it roughly every 5 minutes
B Drain node with a VM having an RWO disk and evictionStrategy: LiveMigrate (timeout 120s) Drain failed (120.8s). VMI … is configured with an eviction strategy but is not live-migratable Continues rejecting with
C Uncordon node after scenarios A and B Eviction did not stop. Eviction migrations were created twice more even after reverting; when the load was stopped, the final one succeeded and the VM left the reverted node
D Drain node with three unloaded VMs (LiveMigrate) and one (evictionStrategy None) (timeout 400s) Two were migrated in 22s and 13s . The None VM was shut down at 7.4s and booted as a new VMI on another node at 17.9s (reboot). The remaining one hit Insufficient memorybecause the receiving node's memory request was at 98%, failing after 5 minutes and retrying until drain timed out at 400s

2.1 It is a Webhook, Not a PDB

During drain, kubectl get pdb -A was No resources found . Although the user guide's node maintenance documentation states that it "creates a PodDisruptionBudget per VMI", v1.9.0 receives eviction requests for the virt-launcher pod via an admission webhook(virt-launcher-eviction-interceptor.kubevirt.io). The webhook rejects the eviction while leaving an eviction flag (status.evacuationNodeName) on the VMI, which virt-controller observes to create an eviction migration (kubevirt-evacuation-…). Drain re-requests eviction every 5 seconds, and completes successfully once migration finishes and the source pod disappears. A documentation PR has been submitted to fix this discrepancy (user-guide#1051).

Therefore, whether a drain finishes depends on whether all VMs on that node are successfully migrated.

2.2 Eviction Label Remaining Even After Reverting

Scenario C was the most unexpected. The drain failed, so the node was uncordoned, but the VMI's status.evacuationNodeName label remained as is. Seeing this label, the virt-controller continued creating eviction migrations (created twice after uncordoning twice), and when the load was turned off, one of them succeeded and the VM left the node just reverted. Even if you think, "Let's just stop the drain and revert," the VM is essentially still in the middle of moving.

The command to clear this label is virtctl evacuate-cancel . However, when executed with v1.9.0 virtctl, it failed like this.

Internal error occurred: the server rejected our request due to an error in our request

The server (virt-api) enforces a condition that "it is cleared only when the node name currently being evicted matches the request," which is a bug caused by v1.9.0 virtctl failing to include the node name in the request. Sending the same request with the body {"evacuationNodeName": "<노드>"} cleared it with a 200 status code. Since this has already been fixed (kubevirt#17008, release-1.9 backport #18864), virtctl v1.9.1 or higher should be used. Even if the server is v1.9.0, only virtctl needs to be updated.

3. Concurrent Migration Limits

After gathering six cirros VMs onto a single node, six VMIMs were created all at once. Only the parallelOutboundMigrationsPerNode in the KubeVirt CR was changed while the cluster limit (parallelMigrationsPerCluster: 5) was left as is.

Per-node limit Until all six finish Maximum Running simultaneously One Running → Succeeded
1 33.9s 1 2–6s
2 (default) 22.3s 2 3–4s
4 23.2s 4 8–9s

Raising the limit from 2 to 4 did not make it faster. Since a single node's available bandwidth (approx. 90MiB/s) is shared, each operation becomes slower accordingly. If there are many small VMs, the gain from increasing from 1 to 2 (34s → 22s) is significant, but beyond that, the network bandwidth is the bottleneck.

The migration-dedicated network (spec.configuration.migrations.network) was not measured (unmeasured). The nested node had only two NICs and lacked Multus, so a separate network could not be created. In this configuration, migration, NFS disks, and service traffic all shared the same approximately 800Mbps line. In production, if VMs are large or numerous, a dedicated network comes first, and limits come second.

# KubeVirt CR 의 일부. 비워 두면 위 기본값(클러스터 5, 노드당 2, 대역폭 무제한)
spec:
  configuration:
    migrations:
      parallelMigrationsPerCluster: 5
      parallelOutboundMigrationsPerNode: 2

4. Limitations of This Measurement

Please verify the conditions before using these numbers.

5. Conclusion: Before Trusting Live Migration

Checklist Reason (Actual measurements from Parts 1 & 2)
Disk is not RWX, network is not masquerade Otherwise, the webhook rejects VMIM creation from the start
CPU model set to a baseline model shared by nodes host-model may look True but fails after 5 minutes
autoConverge for busy VMs Default gives up after 300s even with 128MB writes; autoConverge takes 33–43s
Node sysctl settings required to use postCopy Without them, it fails in 1–2s
Bandwidth limits verified via actual transfer volume Configured 32Mi, but 75MiB/s was transmitted
VMs that cannot be moved have evictionStrategy None If LiveMigrate, the drain does not complete
Target node memory headroom If at 98%, it fails and retries every 5 minutes
virtctl v1.9.1 or higher To clear the eviction label remaining even after uncordoning
Client reconnection Service interruption is several times longer than guest downtime

If you want to try it yourself, TT Lab's hands-on course "KubeVirt Live Migration" (site path /course/kubevirt-live-migration) is prepared with the same VM set and setup as these measurements. It consists of eight steps, ranging from choosing a common CPU model, moving and reading downtime, reading RWO rejections, to draining.

TT's Note: If a friend keeps making a mess while packing, the move will never end. In that case, gently slowing that friend down (autoConverge) finishes the move in 30 seconds. And canceling the move doesn't mean it's over. If you don't remove the "Moving" note (evacuationNodeName) attached to the door, your belongings might end up moving out on their own!

Sign in to like

Comments

No comments yet.

Sign in to leave a comment