状态:实测 (2026-10-31)。 这是在包含三台 TT Lab 实验 VM 的嵌套集群中构建了 Kubernetes 和 KubeVirt v1.9.0 环境下测得的数据。 基础测试进行五次,阻塞条件和 CPU 模型测试每个条件进行一次。所有数字均提取自每次迁移保存的原始数据(VMIM 阶段记录、libvirt 操作信息、探测记录)。编写规则:以实测数据为基础,无法测量的数据标记为“未实测”,原因未确认的解释标记为“推测”。
目录
1. 从结论开始
若要修补节点或升级内核,必须将该节点上的虚拟机迁移到其他地方。KubeVirt 是通过 动态迁移来完成此操作的。在不关闭虚拟机的情况下, 将内存复制到另一个节点,短暂暂停后在新节点上继续运行。只读文档时会觉得“几乎不会中断”,但亲自迁移后发现 限制条件相当多。
- 虚拟机暂停和服务中断是不同的数字。 在五次无负载的 1Gi VM 迁移中,libvirt 测得的暂停时间中位数为 343ms,但通过 Service 发起的新连接 中位数为 2.4 秒无响应,并且原有的 TCP 连接在五次测试中全部中断。
- 无法迁移的虚拟机比想象中要多。 如果磁盘是 RWO,即使存储是 NFS 也会被阻断,local-path、hostDisk 以及 pod 网络 bridge 也会被阻断。
containerDisk 可以迁移,但如果在节点上通过
ctr拉取了镜像,等待 15 分钟后会失败。 LiveMigratable: True不会检查 CPU。 默认的 host-model 会要求目标节点具备源节点的所有 CPU 特性,因此即使是看起来名称相同的 节点,也会因为一两个特性的差异而在 5 分钟后失败。在硬件混杂的环境中, 具名 CPU 模型是正确的解决方法。
内存密集型虚拟机的处理、节点排空(drain)以及并发迁移限制将在 第二篇中介绍。
2. 动态迁移的工作原理
在 KubeVirt 中,虚拟机是运行在节点上 Pod (virt-launcher) 内部的 QEMU 进程。由于 Pod 无法被“直接迁移”到另一个节点,KubeVirt 会选择
在目标节点上启动一个新的 Pod,将虚拟机状态复制过去,然后终止原来的 Pod 方式转移。负责启动这个过程的对象是
VirtualMachineInstanceMigration(简称 VMIM)。 virtctl migrate 也只会创建一个这样的对象。
- 准备目标 Pod。 virt-controller 在目标节点上创建一个新的 virt-launcher Pod。该 Pod 中的 QEMU 在没有 Guest 的情况下等待“即将到来的迁移”。 在实际测量中,从创建 VMIM 到目标 Pod 准备就绪花费了 2–4 秒。 如果无法调度目标 Pod,则在此处停止(第 5、6 节)。
- 预拷贝(pre-copy)。 源端的 QEMU 发送 Guest 内存。由于 Guest 仍在持续运行,它会修改已经发送过的页面。因此,第 1 轮会发送全部内存,从第 2 轮开始,只重新发送期间发生更改(变脏)的页面。几乎不会发送全为 0 的页面,因此对于 1.016GiB 的 Guest,实际发送量为 456MiB。重复了 3–4 次,速度约为 88MiB/s。
- 停止并最终拷贝(stop-and-copy)。 当能够在目标时间内发送剩余的数据量时,停止 Guest 并发送剩余页面和 CPU 状态。此时是 Guest 实际停止的时间,libvirt 会将其记录为
Total downtime。KubeVirt v1.9 将目标时间从 300ms 开始,如果进度受阻则会逐渐增加(上限maxDowntimeMs默认 900ms)。 - 在目标上恢复。 目标 QEMU 继续运行 Guest,源 Pod 终止。 不迁移磁盘。 这是因为两个 Pod 会同时挂载同一个 PVC,因此 PVC 必须是能够跨多个节点同时挂载的 RWX(ReadWriteMany)(第 5 节)。
传输内存数据的流量流经两个节点的 virt-handler Pod 之间,即 Pod 网络。如果不提供专门的迁移专用网络,它将与服务流量共用同一条线路。
3. 测量环境与方法
| 项目 | 值 |
|---|---|
| 外部 | 三台 TT Lab 实验虚拟机(各 4 vCPU·4GiB,Ubuntu 24.04,嵌套虚拟化)。1 个控制平面,2 个 Worker。两个 Worker 位于不同 CPU 代际的物理服务器上 |
| 内部 Kubernetes | kubeadm 1.36.4, containerd 2.3.5, Flannel v0.28.9 |
| KubeVirt·CDI | KubeVirt v1.9.0(virt-launcher 内的 libvirt 11.10.0, QEMU 10.1.0, multifd 8 通道), CDI v1.66.0 |
| 共享存储 | 控制平面的内核 NFS + csi-driver-nfs 4.13.4, 一个 StorageClass(RWX)。用于 RWO 对比的 local-path-provisioner v0.0.32 |
| Guest | Fedora 44(1 vCPU, 内存 1Gi, 根目录 6GiB RWX, masquerade), 条件测试使用 cirros(128Mi) |
| Worker 间网络 | iperf3 10秒 × 3:790–799Mbps(会话网络), 764–772Mbps(Pod 网络) |
测量方法有三种。
- VMIM 阶段:每 0.5 秒记录一次 VMIM 的阶段(Pending → Scheduling → Scheduled → PreparingTarget → TargetReady → Running → Succeeded)。
- libvirt 测量的值: finishedの後、対象のPodから
virsh domjobinfo --completedで経過時間、停止時間(Total downtime)、送信量、繰り返し回数を読み取りました。 実際に適用された設定は、VMI のstatus.migrationState.migrationConfigurationから読み取りました。 - 外部から見た切断: ゲスト内にエコーサーバー(
ncat -lk 7777 --exec /bin/cat)を立ち上げ、コントロールプレーンから VMI を選択する ClusterIP Service に対して 20ms ごとにリクエストを送信しました。毎回新しく確立する接続(新規接続)と、1つの接続を維持し続ける接続(持続接続)の2種類があり、試行ごとに 200ms 以内に応答がない場合は失敗としてカウントします。 「切断」は、最も長い連続失敗区間です。
一度構築にかかった時間も記録しておきます。VMセットの準備 276秒、csi-driver-nfs 52秒、KubeVirt Available 122秒、CDI Available 91秒、Fedoraベースディスクの
取得 193秒でした。CDIの取得で一度引っかかりましたが、csi-driver-nfs に mountPermissions を指定しないとボリュームフォルダーが root 0755 になり、取得
Pod(uid 107)が permission denied で無限に再起動します。NFS は fsGroup で所有権を変更してくれないため、StorageClass に mountPermissions: "0777" 解答了此问题。
4. 基本实测:无负载的 1Gi VM 五次
Fedora VM(1Gi, RWX, masquerade, evictionStrategy: LiveMigrate)在两个 Worker 之间往返了五次。未设置迁移策略(默认值)。CPU 固定为两个 Worker 能够共同支持的处理器型号 Skylake-Client-v4 。为什么必须这样做,我们将在第 6 节中看到。
| 次 | VMIM 创建 → Succeeded | libvirt 耗时 | 客户机暂停 | 已发送量 | 速度 | 重复 | 新连接断开 | 保持连接 |
|---|---|---|---|---|---|---|---|---|
| 1 | 11.4秒 | 6680ms | 413ms | 459MiB | 86.2MiB/s | 4 | 2406ms | 2205ms,已断开 |
| 2 | 9.0秒 | 6652ms | 490ms | 456MiB | 87.1MiB/s | 4 | 2205ms | 2005ms,已断开 |
| 3 | 10.3秒 | 6262ms | 343ms | 455MiB | 89.3MiB/s | 3 | 2607ms | 2605ms,卡顿 |
| 4 | 9.1秒 | 6400ms | 338ms | 456MiB | 88.3MiB/s | 4 | 1204ms | 1203ms,卡顿 |
| 5 | 10.1秒 | 6208ms | 342ms | 456MiB | 90.1MiB/s | 4 | 2806ms | 3007ms,卡顿 |
| 中位数 | 10.1秒 | 6400ms | 343ms | 456MiB | 88.3MiB/s | 4 | 2406ms | 2205ms,5/5 卡顿 |
从创建 VMIM 到结束大约花费了 10 秒,其中实际复制(libvirt 耗时)为 6.4 秒。其余时间包括调度并启动目标 Pod 的时间(1–2秒)、准备目标环境的时间(0–2秒),以及结束后清理状态的时间。
4.1 为什么 0.34 秒的暂停会变成 2.4 秒的卡顿
在 KubeVirt 的默认网络绑定 masquerade 中,Guest 隐藏在 Pod 背后。从外部看到的地址是 virt-launcher Pod 的 IP,并通过 Pod 内的 NAT 转发给 Guest。然而,正如第 2 节中所见,迁移是 创建一个新 Pod 并进行迁移 的工作,因此结束后,包裹 Guest 的 Pod 会发生变化,Pod IP 也会改变。
- 维持的连接断开的原因。 原来经由 Pod A 建立的 TCP 连接的 NAT 状态位于 Pod A 的网络内部。当 Pod A 终止时,该状态也会随之消失。五次保持连接均因超时而断开,重新建立的连接则成功了。
- 新连接有一段时间仍然无处可去的原因(推测)。 在 Service 的 Endpoint 从 Pod A 转移到 Pod B 并反映到各节点的规则中之前,即使暂停结束,新连接也无法触达。在探针记录中,失败区间在 VMIM 显示为 Succeeded 的大约 1 秒前开始,并持续了之后的 1–3 秒。除去 0.34 秒的暂停时间,其余时间被认为是用于 Endpoint 切换的时间,但并未单独记录 Endpoint 更改的时间(未实测)。
因此,不能抱着“因为是热迁移所以不会中断”的想法进行设计。 必须让客户端重新建立断开的连接,并能够通过重试 来承受几秒钟的失败。对于非 masquerade 的绑定(例如通过 Multus 附加的独立网络等),未测量相同的值(未实测)。
5. 无法迁移的条件
VMI 的状态包含 LiveMigratable 条件。如果为 False,则会同时显示原因,并且如果尝试创建 VMIM,准入 Webhook 将直接拒绝创建。我们使用 cirros VM 实际创建了各种条件进行了测试。
| VM | 磁盘与网络 | LiveMigratable | 原因与消息(原文) | VMIM |
|---|---|---|---|---|
| RWO | NFS StorageClass 的 RWO PVC | False | DisksNotLiveMigratable: PVC … is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode) |
拒绝创建 |
| local-path | local-path RWO PVC | False | 相同消息 | 拒绝创建 |
| hostDisk | containerDisk + hostDisk | False | DisksNotLiveMigratable: cannot migrate VMI with non-shared HostDisk |
拒绝创建 |
| bridge | RWX 磁盘,pod 网络使用 bridge | False | InterfaceNotLiveMigratable: cannot migrate VMI which does not use masquerade or a migratable plugin to connect to the pod network |
拒绝创建 |
| containerDisk(通过 ctr 拉取) | 预先拉取到节点 ctr images pull 上的镜像 |
True | 在 Scheduling 状态 15 分 4 秒后 Failed | |
| containerDisk(通过 crictl 拉取) | 使用 crictl pull 重新拉取相同镜像后 |
True | Succeeded |
5.1 即使存储是 NFS,RWO 也会被阻止
KubeVirt 不会检查存储实际上是否可以挂载到多个节点,而只检查 PVC 的访问模式 。即使存储是 NFS、本来可以共享,如果 PVC 被创建为 RWO,也无法迁移。通过仅读取方式检查实际环境后发现,现有的 VM 正是这种情况。存储的 StorageProfile 可以提供 RWX,但创建 VM 的代码将 PVC 编写为 RWO,因此全部都是 LiveMigratable: False 。为了能够迁移,必须将根磁盘重新创建为 RWX PVC(关闭 VM 并复制到 RWX 进行更换,或者使用 v1.9 的卷迁移)。这次没有对卷迁移进行测量(未实测)。关于访问模式和卷模式的内容,请参见
直到学生 VM 启动(第三篇)。
5.2 containerDisk 与 ctr 的陷阱
containerDisk(将磁盘作为容器镜像接收的方式)可以迁移。它通过连同写入层一起发送的块迁移(BlockMigration)来处理。然而,
在像隔离网络(封闭网络)这样预先将镜像放入节点的环境中存在一个陷阱。
- 在节点上通过
ctr拉取或通过ctr images import导入的镜像,在 CRI 中的存储摘要(repoDigests)为空,并且源 Pod 的imageID仅剩sha256:<설정 digest>。 - 为了将目标 Pod 的镜像固定为“与源镜像相同的摘要”,KubeVirt 会直接附加
imageID的 sha256 值来创建<저장소>@sha256:<값>。由于该值是存储中不存在的引用,目标 Pod 会陷入NotFound·ImagePullBackOff,而 VMIM 会在 15 分 4 秒 内一直停留在 Scheduling 状态并最终失败。 - 通过
crictl pull(CRI 路径)重新拉取相同镜像并填充 repoDigests 后新启动的 VM 成功完成了迁移。
此行为已作为 Issue 提交给上游(kubevirt#19694)。在此之前,建议通过 crictl pull 拉取预置镜像,或者在导入后确认 crictl inspecti 是否已填充 repoDigests。
5.3 达到失败状态所需的时间
根据受阻环节的不同,等待时间会有很大差异。Webhook 拒绝的条件可以立即知晓,但如果无法调度目标 Pod,则需要等待 5 分钟;而如果目标 Pod 已启动但无法拉取镜像,则需要等待 5 分钟后才会变为 Failed。如果不希望等待,可以通过删除 VMIM 来取消。 15 分钟 后才会变成 Failed。如果不希望等待,删除 VMIM 即可取消。
6. CPU 模型:哪些节点之间可以迁移
客户机认为它会继续使用最初启动时看到的 CPU 功能。如果迁移到的节点上缺少该功能,客户机可能会崩溃,因此 KubeVirt 会在目标 Pod 上附加
节点选择器,以便仅调度到功能匹配的节点上。附加到各个节点的 CPU 标签(cpu-model.node.kubevirt.io/…,
cpu-feature.node.kubevirt.io/…, cpu-model-migration.node.kubevirt.io/…)由 virt-handler 的节点贴标器(Node Labeller)负责打上。
由于两个 Worker 位于 CPU 代系不同的 Intel 主机(family 6 的 model 151 和 170)上,因此可以在相同条件下直接进行测试。节点贴标器为两个 Worker 附加了
相同的 host-model 名称,但必需的功能有所不同。其中一侧独有 rfds-clear,而另一侧独有 ddpd-u·rfds-no 。
| CPU 配置 | LiveMigratable | 附加到目标 Pod 的选择器(实测) | 结果 |
|---|---|---|---|
名称模型 Skylake-Client-v4 |
True | cpu-model.node.kubevirt.io/Skylake-Client-v4 |
Succeeded |
| host-model(默认值) | True | cpu-model-migration.node.kubevirt.io/<원본의 host-model 이름> + 源节点的 27 个必需功能 + 厂商 |
目标 Pod Unschedulable, 正好 5 分钟后 Failed |
| host-passthrough | True | cpu-vendor.node.kubevirt.io/Intel 仅有一个 |
Succeeded,迁移到代系不同的节点 |
| 任何节点都无法提供的模型 | cpu-model.node.kubevirt.io/<그 모델> |
VMI 本身停留在 Scheduling 状态(VM 无法启动) |
这里可以得出三个结论。
LiveMigratable不会计算 CPU 兼容性。 host-model VM 的状态虽然是 True,但没有可供迁移的位置,目标 Pod 在 Pending 状态持续 5 分钟后以unschedulable pod … timeout period exceeded结束。- host-model 的目标条件是“与源相同的 host-model 名称 + 源节点的所有必需功能”。 即使名称相同,如果缺少一个功能也不行。并且 host-model 名称仅仅是“最接近的名称模型”,并不代表代系。两个代系不同的主机获得了相同的名称。
- host-passthrough 只检查厂商。 因此,它甚至成功迁移到了代系不同的节点。由于当前的客户机处于能够看到目标上没有的功能(如
ddpd-u·rfds-no等)的状态,如果是有实际使用该功能的程序,就会存在风险。passthrough 只能在 CPU 完全相同的节点之间使用。
由于用户指南中未整理此关系,我们提交了一份添加该章节的文档 PR(user-guide#1052).
6.1 在混合硬件环境中会发生什么
我们应用相同的规则(上述 host-model 选择器经实测确认的规则),对混合了四个 Intel 节点和一个 AMD 节点的实际节点标签进行了计算。如果去掉节点名称,按配对类型进行归类,结果如下。
| 节点对 | 根据标签看到的差异 | host-model | 名称模型 | host-passthrough |
|---|---|---|---|---|
| CPU 相同的两台机器 | cpu-feature 标签完全相同 |
可行 | 可行 | 可行(安全) |
| 仅 host-model 名称相同的两台机器 | 在源节点的必需功能中,有 1–3 个功能在目标上缺失(rfds-clear 一个,或 ddpd-u·rfds-no·vmx-any-errcode) |
两侧均被阻止 | 可通过公共模型实现 | 可调度但有风险 |
| 代系不同的 Intel | 一侧缺少 12 个 AVX-512 系列功能,另一侧缺少 waitpkg·bhi-ctrl 等 8 个功能 |
两侧均被阻止 | 可通过公共模型实现 | 可调度但有风险 |
| Intel 与 AMD | 彼此相差 15–22 个(从虚拟化扩展到 vmx·svm 均不相同) |
被阻止 | 公共模型仅限旧型号 | 被阻止(厂商不同) |
- 使用 host-model 只能在 CPU 完全相同的配对之间进行。 在五台机器中,这样的配对只有一组。“属于同一产品系列所以应该没问题”的想法并不适用,差异往往是一两个诸如安全缓解措施或预取器控制之类不易察觉的功能位。
- 如果指定名称模型,适用范围就会变广。 四台 Intel 机器可以共同提供的名称模型有 31 个,其中有像
Skylake-Client-v4·Skylake-Client-noTSX-IBRS·Broadwell-noTSX-IBRS·Haswell-noTSX-IBRS这样能够保持 AVX2 的模型。包括 AMD 在内的五台机器共同能提供的模型只有IvyBridge·SandyBridge·Westmere·Nehalem·Penryn(包含各-v1)等 10 个,因此会失去 AVX2。如果混合了不同的厂商,就需要在“能够迁移”与“客户机性能”之间进行权衡。 - 实际上,除了硬件之外还有其他需要考虑的因素。内存较小的节点、通过设备直通(Passthrough)绑定的节点应该从接收方中排除,这样可用的配对就会进一步减少。
本表基于标签计算。在嵌套测试中实际得到确认的只有“仅 host-model 名称相同的两台机器”这一行。
6.2 如何在自己的节点上进行确认
通过比较两个节点的 CPU 功能标签,可以提前知道 host-model VM 是否能够被成功迁移。
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 기능 같음"
两个节点能够共同提供的名称模型是所有以 cpu-model.node.kubevirt.io/ 开头的标签的交集。要在混合节点中迁移 VM,可以在
spec.domain.cpu.model 中填写其中一个,或者通过 KubeVirt CR 的 spec.configuration.cpuModel 来更改集群默认值。
7. 总结
| 检查项目 | 原因(本文的实测结果) |
|---|---|
| PVC 访问模式是否为 RWX | 如果是 RWO,即使存储是 NFS,Webhook 也会拒绝 |
| Pod 网络绑定是否为 masquerade(或支持迁移的插件) | 如果是 bridge,则会拒绝 |
containerDisk 镜像是否未使用 ctr 拉取 |
拉取目标不存在的 digest 时,会在 15 分钟后失败 |
| 是否指定了 CPU 模型 | host-model 即使显示 True 也会在 5 分钟后失败,passthrough 则会冒着风险成功 |
| 客户端是否会进行重新连接和重试 | 即使暂停时间仅为 0.34 秒,新连接也会中断 2.4 秒,且保持连接会断开 |
本文的实测结果可以在 TT Lab 的实践课程“KubeVirt 热迁移”(网站路径 /course/kubevirt-live-migration)中进行实际跟随演练。你将使用相同的 VM 集合与版本,依次经历共享存储、KubeVirt、选择通用 CPU 模型、读取迁移与暂停时间、读取 RWO 拒绝情况以及 drain 顺序。如果你对 KubeVirt 的架构还不熟悉, 直到学生 VM 启动与 阅读 KubeVirt 源代码
系列文章将为你奠定基础。
TT 的话。 搬家期间客人等在门外的时间是 0.34 秒,但客人却觉得门铃有 2.4 秒按不响。因为家里的地址(Pod IP)变了!而且搬家卡车只能去“同一个小区(CPU 功能相同)”的地址。如果是只有名字相似的小区,到了之后才发现根本没路,所以请提前对好地图。