TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

KubeVirt 动态迁移实测 1:虚拟机暂停了 0.34 秒,但服务中断了 2.4 秒

状态:实测 (2026-10-31)。 这是在包含三台 TT Lab 实验 VM 的嵌套集群中构建了 Kubernetes 和 KubeVirt v1.9.0 环境下测得的数据。 基础测试进行五次,阻塞条件和 CPU 模型测试每个条件进行一次。所有数字均提取自每次迁移保存的原始数据(VMIM 阶段记录、libvirt 操作信息、探测记录)。编写规则:以实测数据为基础,无法测量的数据标记为“未实测”,原因未确认的解释标记为“推测”。

目录

1. 从结论开始

若要修补节点或升级内核,必须将该节点上的虚拟机迁移到其他地方。KubeVirt 是通过 动态迁移来完成此操作的。在不关闭虚拟机的情况下, 将内存复制到另一个节点,短暂暂停后在新节点上继续运行。只读文档时会觉得“几乎不会中断”,但亲自迁移后发现 限制条件相当多。

内存密集型虚拟机的处理、节点排空(drain)以及并发迁移限制将在 第二篇中介绍。

2. 动态迁移的工作原理

在 KubeVirt 中,虚拟机是运行在节点上 Pod (virt-launcher) 内部的 QEMU 进程。由于 Pod 无法被“直接迁移”到另一个节点,KubeVirt 会选择 在目标节点上启动一个新的 Pod,将虚拟机状态复制过去,然后终止原来的 Pod 方式转移。负责启动这个过程的对象是 VirtualMachineInstanceMigration(简称 VMIM)。 virtctl migrate 也只会创建一个这样的对象。

라이브 마이그레이션: 게스트를 켠 채로 옮기기 부하 없는 1Gi VM, 5회 중앙값(실측) 원본 노드 대상 노드 ① 대상 파드 준비 · 2–4초 게스트 실행 중 virt-launcher 파드 A 새 파드 B 기동 빈 QEMU 가 기다림 ② 사전 복사 · 3–4회 반복, 약 88MiB/s 게스트 계속 실행 바뀐 페이지에 표시 받은 페이지 채움 아직 멈춰 있음 모두 합쳐 456MiB ③ 멈추고 마지막 복사 · 343ms 게스트 멈춤 남은 페이지 + CPU 상태 마지막 조각 받음 남은 것 ④ 대상에서 재개 파드 A 끝남 게스트 실행 파드 IP 는 새것 디스크는 옮기지 않는다 두 파드가 같은 RWX PVC(공유 스토리지)를 붙인다 ②는 1회차에 메모리 전체(0 으로 찬 페이지는 거의 안 보냄), 다음 회차부터는 그사이 바뀐 페이지만 보낸다.
图:热迁移的四个阶段:准备目标 Pod、预拷贝、停止并最终拷贝、在目标上恢复
  1. 准备目标 Pod。 virt-controller 在目标节点上创建一个新的 virt-launcher Pod。该 Pod 中的 QEMU 在没有 Guest 的情况下等待“即将到来的迁移”。 在实际测量中,从创建 VMIM 到目标 Pod 准备就绪花费了 2–4 秒。 如果无法调度目标 Pod,则在此处停止(第 5、6 节)。
  2. 预拷贝(pre-copy)。 源端的 QEMU 发送 Guest 内存。由于 Guest 仍在持续运行,它会修改已经发送过的页面。因此,第 1 轮会发送全部内存,从第 2 轮开始,只重新发送期间发生更改(变脏)的页面。几乎不会发送全为 0 的页面,因此对于 1.016GiB 的 Guest,实际发送量为 456MiB。重复了 3–4 次,速度约为 88MiB/s。
  3. 停止并最终拷贝(stop-and-copy)。 当能够在目标时间内发送剩余的数据量时,停止 Guest 并发送剩余页面和 CPU 状态。此时是 Guest 实际停止的时间,libvirt 会将其记录为 Total downtime 。KubeVirt v1.9 将目标时间从 300ms 开始,如果进度受阻则会逐渐增加(上限 maxDowntimeMs 默认 900ms)。
  4. 在目标上恢复。 目标 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 网络)

测量方法有三种。

一度構築にかかった時間も記録しておきます。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 秒的卡顿

게스트는 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회 모두 끊겼고, 다시 맺으면 됐다
图:Guest 暂停 343ms 与新连接无响应 2.4 秒的比较,masquerade 中 Pod 变更的示意图

在 KubeVirt 的默认网络绑定 masquerade 中,Guest 隐藏在 Pod 背后。从外部看到的地址是 virt-launcher Pod 的 IP,并通过 Pod 内的 NAT 转发给 Guest。然而,正如第 2 节中所见,迁移是 创建一个新 Pod 并进行迁移 的工作,因此结束后,包裹 Guest 的 Pod 会发生变化,Pod IP 也会改变。

因此,不能抱着“因为是热迁移所以不会中断”的想法进行设计。 必须让客户端重新建立断开的连接,并能够通过重试 来承受几秒钟的失败。对于非 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)来处理。然而, 在像隔离网络(封闭网络)这样预先将镜像放入节点的环境中存在一个陷阱。

此行为已作为 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 无法启动)

这里可以得出三个结论。

  1. LiveMigratable 不会计算 CPU 兼容性。 host-model VM 的状态虽然是 True,但没有可供迁移的位置,目标 Pod 在 Pending 状态持续 5 分钟后以 unschedulable pod … timeout period exceeded 结束。
  2. host-model 的目标条件是“与源相同的 host-model 名称 + 源节点的所有必需功能”。 即使名称相同,如果缺少一个功能也不行。并且 host-model 名称仅仅是“最接近的名称模型”,并不代表代系。两个代系不同的主机获得了相同的名称。
  3. host-passthrough 只检查厂商。 因此,它甚至成功迁移到了代系不同的节点。由于当前的客户机处于能够看到目标上没有的功能(如ddpd-u·rfds-no 等)的状态,如果是有实际使用该功能的程序,就会存在风险。passthrough 只能在 CPU 完全相同的节点之间使用。

由于用户指南中未整理此关系,我们提交了一份添加该章节的文档 PR(user-guide#1052).

6.1 在混合硬件环境中会发生什么

我们应用相同的规则(上述 host-model 选择器经实测确认的规则),对混合了四个 Intel 节点和一个 AMD 节点的实际节点标签进行了计算。如果去掉节点名称,按配对类型进行归类,结果如下。

섞인 노드: CPU 설정별로 옮길 수 있는 쌍 두꺼운 칸은 중첩 클러스터 실측, 나머지는 노드 라벨로 계산 host-model (기본값) 이름 모델 공통 모델 지정 host- passthrough 같은 CPU 기능 라벨이 같음 ✓ 옮겨짐 ✓ 옮겨짐 ✓ 안전 host-model 이름만 같음, 기능 1–3개 차이 ✗ 5분 뒤 실패 ✓ 옮겨짐 ⚠ 옮겨짐, 위험 다른 세대 인텔 기능 8–15개 차이 ✗ 대상 없음 ✓ 공통 모델로 ⚠ 스케줄됨, 위험 인텔 ↔ AMD 공급사가 다름 ✗ 대상 없음 △ 옛 모델뿐 ✗ 대상 없음 ✗ 대상 파드가 뜨지 못한다. LiveMigratable 은 그래도 True 로 보인다. ⚠ 공급사만 보고 옮긴다. 게스트가 대상에 없는 기능을 보고 있었다. △ 공통 이름 모델이 IvyBridge 이하뿐이라 AVX2 를 잃는다. 다른 세대 인텔끼리의 공통 모델 예: Skylake-Client-v4
图:混合节点中按 CPU 配置可迁移的节点对:CPU 相同、仅 host-model 名称相同、Intel 代系不同、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 名称相同的两台机器”这一行。

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 功能相同)”的地址。如果是只有名字相似的小区,到了之后才发现根本没路,所以请提前对好地图。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论