状态:实测(2026-10-31)。 第 1 篇中的嵌套集群(3 个 TT Lab 练习虚拟机,KubeVirt v1.9.0)中测得的数据。 策略测试在每个条件下重复 1–3 次,drain 在每个场景下进行一次,并发限制在每个限制下进行一次。编写规则:实测数据为基础,无法测量的标注为“未实测”, 原因未明的解释标注为“推测”。
目录
1. 持续写入内存的虚拟机
第 1 篇中的虚拟机没有执行任何操作。实际中的虚拟机是会占用内存的。预拷贝采用的是“重新发送在发送过程中发生更改的页面”的方式, 因此,如果虚拟机修改内存的速度与发送速度相似,迁移就无法完成。我们亲自复现了这一点。
- 在虚拟机(Fedora,1Gi)内部运行了
stress-ng --vm 1 --vm-bytes 512M --vm-method write64 --vm-keep -t 0。它会不断重复写入相同的 512MB 数据。 - 策略按
MigrationPolicy单独设置并进行迁移。无策略的默认值是带宽不限、completionTimeoutPerGiB: 150,progressTimeout: 150,maxDowntimeMs: 900,并且 autoConverge 和 postCopy 均关闭(VMI 上实际应用的值)。
| 测试 | 负载 | 策略 | 结果 | 耗时 | 虚拟机暂停时间 | 服务中断(新连接 / 保持连接) |
|---|---|---|---|---|---|---|
| light-1 | 128MB | 无 | Failed Live migration has been aborted |
302秒 | 无(在源节点继续运行) | |
| heavy-1 | 512MB | 无 | Failed | 301秒 | 无 | |
| heavy-2 | 512MB | bandwidthPerMigration: 32Mi |
Failed | 301秒 | 无 | |
| heavy-3 | 512MB | allowAutoConverge: true |
Succeeded | 33秒 | 566ms | 2807ms / 2805ms,中断 |
| uffd-3 | 512MB | allowAutoConverge: true |
Succeeded | 36秒 | 547ms | 1604ms / 1603ms,中断 |
| heavy-5 | 512MB | 32Mi + autoConverge | Succeeded | 43秒 | 433ms | 4287ms / 4384ms,中断 |
| heavy-4 | 512MB | allowPostCopy: true |
Failed Postcopy is not supported: Userfaultfd not available: Operation not permitted |
2秒 | 无 | |
| heavy-6 | 512MB | 32Mi + postCopy | Failed(相同原因) | 1秒 | 无 | |
| uffd-1 | 512MB | allowPostCopy: true,节点 vm.unprivileged_userfaultfd=1 |
Succeeded(PostCopy) | 303秒 | 未实测 | 2204ms / 3006ms,中断 |
| uffd-4 | 512MB | 同上 | Succeeded(PostCopy) | 304秒 | 未实测 | 6214ms / 6670ms,中断 |
| uffd-2 | 512MB | 32Mi + postCopy,开启节点设置 | Succeeded(PostCopy) | 306秒 | 未实测 | 6211ms / 6213ms,中断 |
1.1 为什么默认配置无法结束
每隔 5 秒读取一次正在进行的 libvirt 任务信息,发现未收敛的运行在 300 秒内重复了 184–244 次预拷贝。剩余数据量在 15–90MiB 之间浮动,预计发送完毕所需的暂停时间为 1,782–1,913ms。始终未能达到目标(从 300ms 开始,最大 900ms)。虚拟机产生脏页的速度约为每秒 19,000–23,000 页(74–92MiB/s),几乎与发送速度持平。这意味着每次迭代残留的数据量都差不多。
仅持续写入 128MB 也会失败。 在此期间,由于虚拟机在源节点上正常运行,因此没有发生服务中断。相反,由于虚拟机无法被迁移,该节点无法被腾空(第 2 节)。
1.2 各策略汇总
| 策略 | 在 512MB 负载下的表现 | 代价 |
|---|---|---|
| 无(默认) | 无法收敛,约 300 秒后放弃 | 无法腾空节点 |
| 带宽限制 32Mi | 同样放弃 | 设置了限制,但实际传输超出了限制(见下文) |
| autoConverge | 33–43 秒内成功(3/3),暂停 433–566ms | 逐渐降低虚拟机 CPU 速度以减缓脏页产生速率。虚拟机吞吐量下降了多少未进行测量(未实测) |
| postCopy | 成功(3/3),但必须在耗尽预拷贝超时时间后才转入 PostCopy。服务中断 2.2–6.7 秒 | 节点需满足 vm.unprivileged_userfaultfd=1 要求。如果在后拷贝过程中源节点或目标节点宕机,将丢失虚拟机(根据文档说明) |
autoConverge 顾名思义是一个“使其收敛”的机制。当进展受阻时,QEMU 会稍微限制虚拟机的 vCPU,从而降低脏页产生速率。在此负载下,仅凭这一点就将结果从失败扭转为了在 30 秒内成功。
postCopy 则相反。它先在目标端运行虚拟机,尚未传输的页面在虚拟机触及的瞬间从源节点拉取。由于 Linux 的 userfaultfd 用于捕获该瞬间,因此需要节点内核的允许,而 Ubuntu 的默认值是 0(嵌套 worker 也是 0)。开启后虽然成功了,但 KubeVirt 必须在耗尽预拷贝超时时间后才切换到 PostCopy。并且在后拷贝期间,虚拟机的内存分散在两个节点上,因此无论哪一方宕机都会丢失虚拟机。 如果环境中存在高负载的虚拟机,建议首先尝试开启 autoConverge ,这是本次实测得出的先后顺序。
1.3 带宽限制未生效
bandwidthPerMigration: 32Mi 当设置了时,配置已经写入了所有层级。VMI 的应用配置中显示了 32Mi,QEMU 的 max-bandwidth: 33554432,
virsh migrate-getspeed 32 也都已生效。然而,QEMU info migrate 的吞吐量为 630.76Mbps(约 75MiB/s),10 秒内处理的数据量也从 17.316GiB 增至 18.040GiB(约 73.5MiB/s)。传输通过 8 通道 multifd 进行。这与无限制时(86–90MiB/s)几乎相同。
究竟是由于 multifd 与限速的关系,还是 KubeVirt 方面的配置导致的,需要通过关闭 multifd 的对照测试来辨别(未实测)。因此目前建议不要盲目相信“设置限制就能保护网络”,而是应当通过 virsh domjobinfo 来确认实际传输量。顺便一提,用户指南中记载的默认带宽 64MiB/s 是旧值,v1.9.0 的实际默认值是不限制(0)。修复该文档的 PR 已经处于开启状态。
1.4 应用策略的方法
MigrationPolicy 通过命名空间或 VMI 标签来选择目标。以下是在集中了高负载虚拟机的命名空间中仅开启 autoConverge 的示例。
apiVersion: migrations.kubevirt.io/v1alpha1
kind: MigrationPolicy
metadata:
name: busy-vms-auto-converge
spec:
allowAutoConverge: true
selectors:
namespaceSelector:
kubernetes.io/metadata.name: busy-vms
是否成功应用可通过迁移后 VMI 的 status.migrationState.migrationConfiguration 和 status.migrationState.migrationPolicyName 进行确认。
2. 节点腾空(drain)
最常使用动态迁移的场景是在腾空节点时。如果在虚拟机中配置了 evictionStrategy: LiveMigrate , kubectl drain 将在不关闭虚拟机的情况下将其迁移。我们对这一路径进行了四种场景的测试。
| 场景 | 操作内容 | 结果 |
|---|---|---|
| A | 对带有 512MB 负载的虚拟机(LiveMigrate)所在节点执行 drain(超时 600 秒) | drain 失败(600.9 秒)。 Webhook 以 Evacuation in progress: Eviction triggered evacuation of VMI … 拒绝并创建了驱逐迁移,但由于无法收敛,约每 5 分钟放弃一次并重新创建 |
| B | 对磁盘为 RWO 且为 evictionStrategy: LiveMigrate 的虚拟机所在节点执行 drain(超时 120 秒) |
drain 失败(120.8 秒)。 VMI … is configured with an eviction strategy but is not live-migratable 持续拒绝 |
| C | 在 A、B 之后将节点恢复为可调度状态(uncordon) | 驱逐并未停止。 恢复后又产生了两次驱逐迁移,关闭负载后最后一次成功,虚拟机 离开了已恢复的节点 |
| D | 对包含 3 个关闭负载的虚拟机(LiveMigrate)和 1 个(evictionStrategy None)的节点执行 drain(超时 400 秒) | 其中两台在 22 秒·13 秒 内被迁移。设置为 None 的虚拟机在 7.4 秒时关闭,并在 17.9 秒时作为新 VMI 在其他节点上启动(重启)。其余一台由于目标节点的内存请求达到 98% 而 Insufficient memory,5 分钟后失败并重试,最终 drain 400 秒超时 |
2.1 是 Webhook 而非 PDB
在 drain 期间, kubectl get pdb -A 是 No resources found 。虽然用户指南的节点维护文档中写着“为每个 VMI 创建 PodDisruptionBudget”,但 v1.9.0 是通过将 virt-launcher Pod 的驱逐请求发送给 准入 Webhook(virt-launcher-eviction-interceptor.kubevirt.io)来接收的。Webhook 在拒绝驱逐的同时会在 VMI 上留下驱逐标记(status.evacuationNodeName),virt-controller 看到该标记后便会创建驱逐迁移(kubevirt-evacuation-…)。
drain 每隔 5 秒重新请求一次驱逐,当迁移完成且源 Pod 消失时才算通过。针对该文档不一致的问题,我们已提交了修正文档的 PR(user-guide#1051).
因此,drain 是否能完成取决于 该节点上的所有虚拟机是否都能被成功迁移。
- 如果在无法迁移的虚拟机(如 RWO 磁盘等)上设置了
LiveMigrate,drain 将无法结束(场景 B)。对于无法迁移的虚拟机,应将其设为 None 或LiveMigrateIfPossible(LiveMigrateIfPossible基于 API 文档标准,本次未进行测量)。 - 只要存在一个无法收敛的虚拟机,drain 就会一直等待该虚拟机而暂停(场景 A)。第 1 节中的 autoConverge 在这里同样必不可少。
- 目标节点必须有富余的内存(场景 D)。如果只有两个 Worker 节点,当腾空其中一侧时,该侧的所有虚拟机必须全部容纳到另一侧。如果无法容纳,目标 Pod 将处于 Pending 状态 5 分钟然后失败,接着重新创建,并再次失败。
- 驱逐迁移同样遵循并发限制。在场景 D 中,当两台虚拟机占用限制(每个节点 2 个)时,其余一台虚拟机经历了 12 秒的 Pending 状态。
- 在 drain 过程中迁移的 VM,其服务中断时间为:新连接 3607ms,保持连接 3606ms。这比第 1 篇的基础测试略长一点。
2.2 回退后依然残留的驱逐标记
场景 C 是最出乎意料的。由于 drain 失败,我们对节点执行了 uncordon,但 VMI 的 status.evacuationNodeName 标记却依然保留着。virt-controller 看到这个标记后,继续创建了驱逐迁移(执行了两次 uncordon,之后又生成了两次),关闭负载后其中一个成功了,VM 离开了刚刚恢复的节点。也就是说,即使喊出“先停下 drain 并回退”,VM 实际上也仍在搬迁途中。
用于清除此标记的命令是 virtctl evacuate-cancel 。然而,如果使用 v1.9.0 的 virtctl 执行,则会像这样失败。
Internal error occurred: the server rejected our request due to an error in our request
服务器(virt-api)会附加一个“仅当当前正在驱逐的节点名称与请求相同时才清除”的条件,这是因为 v1.9.0 的 virtctl 在请求中没有填入节点名称而导致的漏洞。如果在同一个请求的正文中加入 {"evacuationNodeName": "<노드>"} 并发送,就会以 200 状态码成功清除。该问题已经修复(kubevirt#17008,release-1.9 的 backport #18864),使用
v1.9.1 及以上版本的 virtctl 即可。即便服务器是 v1.9.0,只要 virtctl 是新版本就行。
3. 并发迁移限制
我们将六台 cirros VM 汇聚到一个节点上,然后同时创建了六个 VMIM。我们只修改了 KubeVirt CR 的 parallelOutboundMigrationsPerNode ,而集群限制(parallelMigrationsPerCluster: 5)保持不变。
| 单节点限制 | 直到六台全部完成 | 同时 Running 最大值 | 单次 Running → Succeeded |
|---|---|---|---|
| 1 | 33.9秒 | 1 | 2–6秒 |
| 2(默认) | 22.3秒 | 2 | 3–4秒 |
| 4 | 23.2秒 | 4 | 8–9秒 |
即使将限制从 2 提高到 4,速度也没有加快。因为单个节点能够输出的带宽(约 90MiB/s)被分摊了,导致单次迁移变得相应缓慢。如果是多个小型 VM,将限制从 1 提高到 2 的收益(34 秒 → 22 秒)很明显,再往上就触及了 网络带宽上限。
我们未能测量迁移专用网络(spec.configuration.migrations.network)(未实际测量)。由于嵌套节点只有两个 NIC 且没有 Multus,因此无法构建独立的网络。在这种配置下,迁移、NFS 磁盘和业务流量共享了大约 800Mbps 的带宽。在实际生产环境中,如果 VM 较大或数量较多,应当优先配置专用网络,限制则排在其次。
# KubeVirt CR 의 일부. 비워 두면 위 기본값(클러스터 5, 노드당 2, 대역폭 무제한)
spec:
configuration:
migrations:
parallelMigrationsPerCluster: 5
parallelOutboundMigrationsPerNode: 2
4. 本次测量的局限性
在引用这些数据之前,请先核实相关条件。
- 这是一个嵌套集群。 由于是在虚拟机内部再次运行 KubeVirt,绝对耗时会比物理节点更长。特别是暂停时间和速度,在物理节点上大概率表现更好。
- 网络带宽约为 800Mbps。 如果达到 10GbE 或更高,即使在相同的负载下,预拷贝也能收敛。第 1 节中的“默认配置下失败”正是“脏页产生速度 ≈ 网络带宽”这一条件下的结果。
- VM 较小。 基础测试使用了一个 1Gi 的 Guest,条件测试使用的是 128Mi 的 cirros。我们没有测量几十 GiB 大小的 Guest 所需的时间(未实际测量)。
- 重复次数较少。 基础测试进行了 5 次,策略测试 1–3 次,drain 场景和并发限制测试各 1 次。请将其作为参考案例而非统计分布来阅读。
- 探测程序每次尝试的超时限制为 200ms,因此在没有迁移时偶尔也会记录到短暂的失败。我们仅关注了最长的那段连续区间。
- 未测量项:autoConverge 对 Guest 吞吐量的影响、postCopy 的 Guest 暂停时间(目标端未留存工作信息)、卷迁移、非 masquerade 的网络绑定、迁移专用网络、关闭 multifd 后的带宽对比。
5. 总结:在信任热迁移之前
| 检查项 | 原因(基于第 1、2 篇的实测) |
|---|---|
| 磁盘非 RWX,网络非 masquerade | 否则 Webhook 会从 VMIM 创建阶段就予以拒绝 |
| 将 CPU 模型设置为节点共同支持的命名模型 | host-model 即使显示 True,也会在 5 分钟后失败 |
| 对繁忙的 VM 启用 autoConverge | 默认情况下,即使写入 128MB 也会在 300 秒后放弃,而 autoConverge 仅需 33–43 秒 |
| 若要使用 postCopy,需先配置节点 sysctl | 否则会在 1–2 秒内失败 |
| 带宽限制需通过实际传输量来验证 | 设置了 32Mi 但实际跑出了 75MiB/s |
| 无法迁移的 VM 应设置 evictionStrategy None | 如果是 LiveMigrate,则 drain 无法完成 |
| 目标节点的内存余量 | 若剩余 98%,则每 5 分钟失败并重试一次 |
| v1.9.1 及以上版本的 virtctl | 用于清除 uncordon 后依然残留的驱逐标记 |
| 客户端重新连接 | 服务中断时间比 Guest 暂停时间长好几倍 |
如果您想亲自尝试,TT Lab 的实操课程“KubeVirt 热迁移”(网站路径 /course/kubevirt-live-migration)已准备好与本测量相同的 VM 集合和相同的环境。从选择公共 CPU 模型开始,到迁移并读取暂停时间、读取 RWO 拒绝、再到 drain,一共分为八个步骤。
Titi 的小贴士。 如果在打包行李的时候旁边有个一直把房间弄乱的朋友,那搬家就永远也结束不了。遇到这种情况,只要让这位朋友稍微放慢速度(autoConverge),30 秒就能搞定。而且,取消搬家并不意味着事情就结束了!如果不把贴在门上的“搬家中”便条(evacuationNodeName)撕掉,行李可是会自己跑出去的哦!