KubeVirt 热迁移为什么会偶发失败:从云盘、OVN 到状态机
KubeVirt 的热迁移看起来像一个动作:把正在运行的虚拟机从一个节点搬到另一个节点。但实际过程同时涉及容器编排平台(Kubernetes,K8s)、KubeVirt、存储、容器网络接口(Container Network Interface,CNI)、开放虚拟网络(Open Virtual Network,OVN)、开放虚拟交换机(Open vSwitch,OVS)和多个控制器。只要其中一个对象没有在正确的时间进入正确的状态,迁移就可能长时间等待,或者表现为「偶尔成功、偶尔失败」。
下面按这次排查中实际遇到的对象和状态展开,先说明各组件的职责,再沿着虚拟机、磁盘、Pod、网络端口和状态机寻找证据。
先看结论
这次问题涉及几个层次,现象和原因对应如下:
- 云盘访问模式决定迁移能否共享存储。 在使用共享 Ceph 存储、并且迁移期间需要让源节点和目标节点同时访问卷的路径中,新建系统盘和数据盘需要使用多节点读写(ReadWriteMany,RWX)。这项要求取决于存储类型和迁移实现,单节点读写云盘需要单独验证。
- KubeVirt 的迁移通过目标侧 Pod 完成。 KubeVirt 会创建目标侧的
virt-launcherPod(虚拟机启动器),并在满足网络、存储和状态条件后,将虚拟机切换到目标节点。 - 早期的网络失败与 Kube-OVN 的接口垃圾回收有关。 OVS 记录的对象标识是虚拟机实例 ID,但旧代码用错误的 Pod 名或旧标签查询,导致正在初始化的目标接口被误认为孤儿接口并删除。
- 修复接口匹配后,仍暴露出热插拔卷(hotplug volume)的时序问题。 目标 Pod 的网络需要在迁移仍处于
Pending时就准备好;如果只在Scheduling阶段处理,KubeVirt 可能因为卷附加 Pod(attachment Pod)等待网络而永远到不了Scheduling。 - 「偶尔成功」来自一个很短的时序窗口。 旧链路中,目标节点有时会短暂地把逻辑端口判定为可用,
ovn-installed=true只保持几毫秒;CNI 恰好轮询命中时就成功,错过后则等待 30 秒超时。
一次成功迁移只能证明当前输入和当前时刻的链路成功。其他网络类型、云盘类型、历史实例和失败路径仍需单独验证。
参与者分别做什么
先把名词放到同一张图里。这里的「节点」是 K8s 节点(Node);KubeVirt 的虚拟机对象是虚拟机(VirtualMachine,VM),运行态对象是虚拟机实例(VirtualMachineInstance,VMI),真正承载虚拟机进程的是 Pod。持久卷(PersistentVolume,PV)和持久卷声明(PersistentVolumeClaim,PVC)分别代表集群中的存储卷和对存储卷的使用请求。图中的 Ceph RBD 指 Ceph 的 RADOS 块设备(RADOS Block Device,RBD),它是 PV/PVC 背后的块存储卷。
flowchart LR
User[用户发起迁移请求] --> VMIM[虚拟机实例迁移资源]
VMIM --> VirtController[KubeVirt virt-controller]
VirtController --> TargetPod[目标 virt-launcher Pod]
TargetPod --> CNI[CNI ADD]
CNI --> KOVN[Kube-OVN]
KOVN --> OVN[OVN 逻辑交换机端口]
OVN --> OVS[目标节点 OVS 接口]
TargetPod --> Attach[卷附加 Pod]
Attach --> PVC[PVC/PV/Ceph RBD]
Kubernetes
K8s 负责调度 Pod、挂载卷、调用 CNI,并保存各种资源对象。它本身不知道虚拟机内存复制的细节。
KubeVirt
KubeVirt 让 K8s 能够管理虚拟机。它把虚拟机表示为 VM,把运行态表示为 VMI。VMI 通常由一个名为 virt-launcher-... 的 Pod 承载;virt-launcher 可以理解为「虚拟机启动器」,其 Pod 内运行虚拟机进程。
virt-launcher 属于承载虚拟机进程的专用 Pod。它里面启动了快速模拟器(Quick Emulator,QEMU)和基于内核的虚拟机(Kernel-based Virtual Machine,KVM)等虚拟机进程,Pod 的网络和卷是否准备完成,会直接影响虚拟机迁移是否能够继续。
VMIM
虚拟机实例迁移资源(VirtualMachineInstanceMigration,VMIM)是一次迁移任务对应的 K8s 自定义资源。它记录源 VMI、目标节点和迁移阶段。上层控制器的迁移任务与 VMIM 分属不同对象:
| |
因此,上层控制器返回成功或任务进入 Running,都不表示迁移已经完成。必须继续观察 VMIM 的终态、VMI 的节点、目标 Pod 的状态以及磁盘和网络。
CNI、Kube-OVN、OVN 和 OVS
- CNI 是 K8s 的容器网络接口。Pod 创建时,kubelet 会调用 CNI 的
ADD操作,为 Pod 创建网络。 - Kube-OVN 是 K8s 到 OVN 的连接层,负责把 Pod 网络请求转换为 OVN 和 OVS 配置。
- OVN 是逻辑网络控制平面。它用逻辑交换机端口(Logical Switch Port,LSP)描述一个逻辑网络端点。
- OVS 是节点上的虚拟交换机。目标 Pod 在节点上创建的本地 OVS 接口,必须先与 OVN 的逻辑端口和流表对应起来,才能真正转发流量。
这些组件的职责分别是:OVN 决定「这个逻辑端口应该接入哪里」,OVS 在节点上提供「实际的虚拟网卡」,CNI 负责把这条链路在 Pod 创建时接起来。
这次解决什么问题
这次排查针对两类现象。它们发生在同一条热迁移链路上,触发条件和修复位置不同。
- 目标 OVS 接口在初始化阶段被清理。 目标
virt-launcherPod 已经创建,CNI 正在等待 OVS 接口完成绑定。Kube-OVN 的接口垃圾回收却用错误的标识查询活动 Pod,把仍在使用的接口当成残留接口删除,CNI 最后等待超时。 - 带热插拔卷的迁移长期停留在
Pending。 目标virt-launcher和卷附加 Pod 都需要网络,网络配置却等到Scheduling阶段才执行。目标资源等不到网络,VMIM 也等不到目标资源进入下一阶段,形成循环等待。
修复目标很明确:让接口垃圾回收找到正确的活动 Pod,并把迁移网络配置提前到 Pending 阶段。这样目标 launcher 和卷附加 Pod 可以先完成网络准备,VMIM 再继续推进;迁移成功时端口切到目标节点,失败时回滚到源节点并清理临时参数。
修复前的流程
目标接口被清理
flowchart TD
A[创建目标 launcher] --> B[CNI ADD]
B --> C[创建 OVS 接口]
C --> D{找到活动 Pod?}
D -->|错误查询| E[查找失败]
E --> F[删除初始化接口]
F --> G[CNI 等待 30 秒]
G --> H[迁移失败]
OVS 的 pod_name 保存的是 VMI ID,旧查询却把它当成真实 Pod 名称,兜底 selector 也使用了错误的标签语义。活动 launcher 没有被找到,接口因此进入清理分支。
热插拔卷造成循环等待
flowchart TD
A[VMIM Pending] --> B[等待 Scheduling]
B --> C[才配置目标网络]
C --> D[目标 launcher 等网络]
C --> E[卷附加 Pod 等网络]
D --> F[launcher 未 Ready]
E --> G[attachment 未 Ready]
F --> H[VMIM 无法推进]
G --> H
H --> B
没有热插拔卷时,目标 launcher 可能很快完成准备,Pending 持续时间较短。热插拔卷增加了卷附加 Pod 这条等待路径,网络配置过晚就会把循环等待固定下来。
修复后的流程
接口匹配改为按 VMI ID
flowchart TD
A[创建目标 launcher] --> B[CNI ADD]
B --> C[创建 OVS 接口]
C --> D[读取 VMI ID]
D --> E[按 VMI ID 查 Pod]
E --> F[保留活动接口]
F --> G[ovn-installed=true]
G --> H[网络就绪]
修复后,GC 使用 vmi.kubevirt.io/id 查找 Pod。目标 launcher 能被正确识别,初始化中的 OVS 接口会继续等待 OVN 完成绑定。CNI 轮询到 ovn-installed=true 后,目标 Pod 才进入后续流程。
网络配置提前到 Pending
%%{init: {"layout": "dagre", "flowchart": {"nodeSpacing": 16, "rankSpacing": 24, "wrappingWidth": 140}}}%%
flowchart LR
A[VMIM Pending] --> B[识别目标 launcher]
B --> C[设置迁移网络参数]
C --> D[目标 launcher 网络 Ready]
D --> E[创建卷附加 Pod]
E --> F[卷附加 Pod Ready]
准备阶段完成后,迁移进入状态流转:
flowchart TB
G[进入 Scheduling] --> H[进入 Scheduled]
H --> I[复制内存]
I --> J[切换到目标节点]
J --> K{迁移结果}
K -->|成功| L[Succeeded]
L --> N[清理临时参数]
K -->|失败| M[回滚源节点]
M --> O[Failed]
O --> P[清理临时参数]
修复后的协调逻辑在 Pending 阶段识别目标节点,提前设置 requested-chassis 和 activation-strategy。目标 launcher 网络 Ready 后,KubeVirt 才会创建卷附加 Pod;卷附加 Pod Ready 后,VMIM 才能从 Pending 进入 Scheduling,再从 Scheduling 进入 Scheduled。对象暂时还没有出现在控制器缓存(informer 缓存)时,控制器会重新入队;进入 Scheduling 后再次执行同一动作,保证事件乱序和重复处理不会破坏状态。迁移成功时,端口收敛到目标节点并清理临时参数;迁移失败时,端口回滚到源节点,再清理临时参数。
为什么云盘要用 RWX
云盘的访问模式是迁移能否成立的基础。
- 单节点读写(ReadWriteOnce,RWO)表示卷可以被一个节点以读写方式挂载。
- 多节点读写(ReadWriteMany,RWX)表示卷可以被多个节点同时以读写方式挂载。
- 单 Pod 读写(ReadWriteOncePod,RWOP)比 RWO 更严格,限制为一个 Pod 使用。
热迁移期间,源 virt-launcher 还没有退出,目标 virt-launcher 已经开始创建。在上述共享存储路径中,如果系统盘和数据盘仍是只能由源节点访问的 RWO,目标 Pod 就无法完成挂载;即使内存复制和网络都正常,迁移也不能安全切换。本结论只适用于上述共享存储路径;本地盘或专门的卷迁移(Volume Migration)方案需要单独验证,适用条件也不同。
验收需要覆盖完整的云盘生命周期,至少包括以下路径:
- 随实例创建的系统盘和数据盘;
- 独立云盘的挂载、卸载和重新挂载;
Running与Shutdown两种实例状态;- 系统盘和数据盘的在线、离线扩容;
- 快照、恢复、派生盘和系统盘重装;
- 实例删除时的云盘保留、级联删除和独立云盘删除语义。
每一步都要检查多层状态是否一致:上层控制器记录的磁盘容量、存储后端卷、PV、PVC,以及虚拟机内部看到的容量。API 返回 200 或 204 只表示请求被接受或处理完成,不自动代表这些异步对象已经收敛。
热迁移的状态机
本地热迁移从 Unset 进入 Pending。去中心化迁移还可能先进入 WaitingForSync 或 Synchronizing,完成 VMI 同步后再回到 Pending。所有本地热迁移都会经过 Pending。没有热插拔卷时,目标 launcher 创建后,Pending 通常很快进入 Scheduling,所以这个阶段不容易被观察到。带热插拔卷时,目标 launcher 必须先完成网络准备并进入 Ready,KubeVirt 才会创建卷附加 Pod;卷附加 Pod 也 Ready 后,Pending 才能继续推进。
Scheduling 仍然有自己的前置条件:目标 launcher 必须 Ready;存在热插拔卷时,卷附加 Pod 也必须 Ready。条件满足后,VMIM 进入 Scheduled,接着经过 PreparingTarget、TargetReady 和 Running,最后进入 Succeeded 或 Failed。
不同 KubeVirt 版本的字段和内部阶段可能略有区别,排查时应以实际 VMIM 和日志为准。此次问题涉及的关键阶段可以简化为:
flowchart TD
Start((开始)) --> Unset[Unset]
Unset -->|本地迁移已接受| Pending[Pending]
Unset -->|目标端等待同步| WaitingForSync[WaitingForSync]
Unset -->|源端执行同步| Synchronizing[Synchronizing]
WaitingForSync -->|VMI 同步完成| Pending
Synchronizing -->|VMI 同步完成| Pending
Pending --> CreateLauncher[创建目标 launcher]
CreateLauncher --> Pending
Pending --> PrepareHotplug[准备热插拔卷]
PrepareHotplug --> Pending
Pending -->|launcher 和卷就绪| Scheduling[Scheduling]
Scheduling -->|launcher 和卷 Ready| Scheduled[Scheduled]
Scheduled --> PreparingTarget[PreparingTarget]
PreparingTarget -->|目标端准备完成| TargetReady[TargetReady]
TargetReady -->|开始复制| Running[Running]
Running -->|completed=true| Succeeded[Succeeded]
Running -->|failed=true| Failed[Failed]
Pending -->|失败| Failed
Scheduling -->|失败| Failed
Scheduled -->|失败| Failed
PreparingTarget -->|失败| Failed
TargetReady -->|失败| Failed
Succeeded --> End((结束))
Failed --> End
Unset 表示 VMIM 已创建,但迁移还没有进入具体阶段。WaitingForSync 表示目标端等待 VMI 同步,Synchronizing 表示源端执行 VMI 同步;这两个分支完成后都会回到 Pending。
Pending 表示迁移对象已经存在,控制器正在创建目标 launcher、准备网络和热插拔卷。图中的「创建目标 launcher」和「准备热插拔卷」会回到 Pending,表示这些动作尚未满足进入下一阶段的条件。Scheduling 表示迁移前置条件已满足,Scheduled 表示目标 launcher 和卷附加 Pod 已经 Ready,可以进入目标端准备。状态名描述的是 VMIM 的迁移阶段,Pod 的 Pending、Running 等状态需要单独观察。
| VMIM 状态 | 迁移过程中的含义 |
|---|---|
Unset | VMIM 已创建,迁移还没有进入具体阶段。 |
WaitingForSync | 目标端等待 VMI 同步完成。 |
Synchronizing | 源端执行 VMI 同步。 |
Pending | 创建目标 launcher,并准备网络和热插拔卷。 |
Scheduling | 迁移前置条件已满足,KubeVirt 正在推进目标端调度。 |
Scheduled | 目标 launcher 已 Ready;存在热插拔卷时,卷附加 Pod 也已 Ready。 |
PreparingTarget | 目标端接收迁移准备工作。 |
TargetReady | 目标端已经准备好,可以开始内存复制。 |
Running | 执行内存复制和最终切换。 |
Succeeded | 迁移完成,端口收敛到目标节点并清理临时参数。 |
Failed | 迁移失败,端口回滚到源节点并清理临时参数。 |
在热插拔卷场景中,目标 launcher 和卷附加 Pod 可能都在等待网络;如果网络配置只在后面的 Scheduling 阶段发生,就会形成循环等待:
当目标资源暂时还没有准备好时,控制器会继续把 VMIM 留在 Pending,等待后续事件或重试。图中省略了 Pending 指向自身的回环,避免渲染时让边标签互相覆盖。
| |
修复的关键是在 Pending 阶段提前设置网络迁移所需的参数;到了 Scheduling 仍然要保留幂等重试,因为控制器缓存事件可能乱序,第一次处理时节点或对象未完全可见。
MigrationState 也可能影响第一次协调。目标 launcher handoff 之前,这个字段可能仍为空,或暂时保留上一轮迁移的状态。Pending 和 Scheduling 阶段可以直接使用以下两个对象字段推导迁移方向:
| |
如果目标 launcher 尚未创建,或者源、目标节点的字段暂时为空,控制器应返回错误并通过限速队列重新入队。Pod 创建和调度完成都不保证一定产生新的 VMIM 阶段事件,静默返回会丢失提前配置网络的机会。
第一条故障链:目标接口被错误清理
什么是接口垃圾回收
Kube-OVN 的 daemon 会定期扫描节点上的 OVS 接口。一个刚创建但还没有完成 OVN 安装的接口,可能是正常初始化中的接口,也可能是上一次 Pod 删除后留下的孤儿接口。垃圾回收(garbage collection,GC)需要在两者之间做判断:
| |
迁移时的对象标识并不相同
目标 launcher 的真实 Pod 名称类似:
| |
KubeVirt 网络适配逻辑写入 OVS external_ids 的 pod_name 使用的是 VMI ID。这两个值分别代表不同对象:
| |
同时,Pod 上的标签也有明确语义:
| |
旧逻辑先把 pod_name 当作 Pod 名称查询,再用 vm.kubevirt.io/name=<VMI ID> 作为兜底。其中 vm.kubevirt.io/name 存的是显示名,VMI ID 位于专用的 vmi.kubevirt.io/id 标签中,因此两次查询都找不到活动 launcher。GC 于是把正在初始化的目标接口当成孤儿接口删除,CNI 最终报错:
| |
正确的匹配关系是使用 vmi.kubevirt.io/id:
| |
OVS 中保存的是 VMI ID,GC 应按 VMI ID 标签寻找 Pod。Pod 名称和虚拟机显示名都不能代替这个 ID。
Completed Pod 是原因吗
迁移完成后,源节点上可能留下一个 Succeeded 的旧 virt-launcher Pod。它需要后续清理,本次接口超时的根因在活动 launcher 的匹配逻辑。
证据有两点:
- 一台没有历史 Completed Pod 的新虚拟机,第一次迁移仍然出现了相同的接口超时。
- 手动删除 Completed Pod 后,反向迁移仍然出现同样的 CNI 和 OVN 问题。
因此要区分两种动作:
- 删除已完成 Pod 是资源清理;
- 修正活动 launcher Pod 的匹配逻辑才是避免误删初始化接口的修复。
终态过滤需要区分 Running、Pending、Succeeded、Failed 和正在删除的 Pod,并结合 Pod 所在节点判断接口归属。状态过滤属于独立的清理问题,不属于本次活动接口误删的核心修复。
第二条故障链:热插拔卷让 Pending 停留更久
什么是热插拔卷
普通卷通常在 Pod 创建时就由 kubelet 一起挂载。热插拔卷允许虚拟机在运行过程中动态添加或移除卷。KubeVirt 会为这类卷创建额外的卷附加 Pod,让卷先在目标节点完成 attach,再交给虚拟机使用。
因此迁移期间可能同时看到:
- 源节点上的 Running launcher;
- 目标节点上的 Pending launcher;
- 一个或多个用于卷附加的卷附加 Pod;
- 仍处于
Pending的 VMIM。
看到 VMIM 没有进入 Scheduling,不能立刻判断 KubeVirt 没有处理迁移。要继续检查目标 launcher、卷附加 Pod、PVC/PV、CNI 事件和 VMI 的 activePods。
为什么只处理 Scheduling 不够
网络优化逻辑最初只在迁移进入 Scheduling 后配置目标节点的网络。对于没有热插拔卷的实例,launcher 可能很快 Ready,状态推进速度足够快,这个时序问题不明显;加入热插拔卷后,卷附加 Pod 又依赖目标网络,等待被放大,于是循环等待稳定暴露。
修复后的处理思路是:
Pending阶段就识别目标 launcher 和目标节点。- 在对象暂时还没有出现在控制器缓存时限速重新入队(requeue),让后续事件继续触发处理。
- 提前设置 OVN 多节点承载参数。
Scheduling阶段继续执行同一动作,保证重复事件和乱序事件不会破坏状态。Succeeded时把端口收敛到目标节点,Failed时回滚到源节点,并清理迁移期间的临时参数。
目标 Pod 的选择也需要区分 launcher 和卷附加 Pod。两类 Pod 共享当前 VMIM 的迁移 UID,只按 UID 查询会得到多个结果;目标 launcher 还带有 kubevirt.io=virt-launcher 标签,卷附加 Pod 则使用热插拔磁盘标签。控制器应使用迁移 UID 加 kubevirt.io=virt-launcher 选择目标 launcher,再读取它的 spec.nodeName 作为目标节点:
| |
OVN 多节点承载解决什么问题
迁移期间,源和目标节点会分别有一个本地 OVS 接口,但它们映射到同一个 OVN 逻辑端口。这里的 OVN 多节点承载(multi-chassis)表示迁移期间允许多个节点暂时准备同一个逻辑端口。
北向数据库(Northbound Database,NB)中的对象类型是 Logical_Switch_Port,南向数据库(Southbound Database,SB)中对应的端口绑定对象是 Port_Binding。NB 和 SB 保存的是两个数据库对象,共同描述同一个逻辑网络端点。LSP 是 Logical_Switch_Port 的常用简称,通常指 NB 中的逻辑交换机端口;对应的 SB 记录是 Port_Binding。
NB 与 SB 的对应关系如下:
| |
这里复用一个逻辑端口,源节点和目标节点各自准备本地 OVS 接口。
常见的两个参数语义如下:
requested-chassis=source,target:把源节点作为主要 chassis,同时把目标节点加入允许承载该逻辑端口的 chassis 集合。chassis可以理解为 OVN 中代表节点的承载位置标识。它为迁移期间的双端准备提供条件。activation-strategy=rarp:目标端即使已经准备好,也不要在虚拟机真正切换前立即转发业务流量;收到目标虚拟机发出的 RARP(Reverse Address Resolution Protocol,反向地址解析协议)后再激活,降低迁移切换前的流量风险。
requested-chassis 决定「哪些节点可以准备这个逻辑端口」,activation-strategy 决定「目标端什么时候开始接管转发」。两个参数职责不同。
ovn-installed=true 为什么会让问题看起来随机
ovn-installed=true 可以理解为:本地 OVS 接口已经完成与 OVN 端口的绑定,并且相应流表已经被 ovs-vswitchd 接受。CNI 会周期性检查这个标志;在本次观测中,检查间隔约为 500 ms,最长等待 30 s。
修复前的日志中出现过这样的顺序:
| |
这个 true 窗口非常短,两个 ovn-controller 可能因为端口归属和请求配置的变化反复执行协调循环(reconcile)。所以:
- CNI 轮询恰好落在几毫秒窗口内,立即认为网络已准备好,迁移继续;
- CNI 没有命中窗口,等待 30 秒后报
not ready after 30s; - 下一次重试又可能遇到不同的时序,表现为「重试后成功」。
因此,「偶尔成功」只表示观察者偶尔命中了一个暂态状态。确认成功还需要检查目标 Pod Ready、VMIM 继续推进、VMI 节点切换以及业务网络可用。
证据边界需要明确:日志能证明 ovn-installed 出现后又被删除,但同一条 Removing iface ... ovn-installed 日志不足以单独证明删除由 can_bind=false 触发。另一个可能路径是 OVN 替换上一轮安装状态。区分具体原因时,需要同时保存 NB、SB、OVS 接口、chassis 和 controller 日志快照。
一次完整的排查路径
先确认实验环境
排查时使用明确的 kubeconfig。每条 Kubernetes 命令都应指定 kubeconfig,并记录集群、节点、Kubernetes 版本、KubeVirt 版本、存储类型和网络实现:
| |
OVN/浮动 IP(Floating IP,FIP)网络与 Calico/macvlan 网络需要分开验证,结果分别记录;非 OVN 环境不要带 OVN/FIP 专用迁移注解。
排查时按下面的证据链观察迁移:
| |
如果某一层卡住,就在这一层收集日志和对象快照,不要直接跳到下一层下结论。
扩容也要检查异步收敛
验收范围还应包括在线扩容。扩容可能停在 Resizing:上层控制器仍显示旧容量,但底层 PV/PVC 已经变成新容量,负责协调的控制器又因为找不到 Running 的 launcher Pod 而持续返回错误。
根因同样可能是标签语义混淆:控制器用 vm.kubevirt.io/name=<实例 ID> 查找 Pod,但实际这个标签保存的是实例显示名;稳定的实例 ID 在另一个专用标签中。这个问题与 OVN GC 的标签错误属于同一类排查教训:
标签名相似不代表语义相同。必须查看写入端、读取端和真实对象,依据字段语义选择 selector。
后端已接收扩容请求,仍需确认上层控制器、PV、PVC 和虚拟机内部都完成扩容。验收需要把异步阶段逐层对齐,否则可能出现「底层已经变大、上层任务仍失败」的半收敛状态。
修复和验证应该怎么写
一次可靠的回归至少需要包含以下对照:
| 层次 | 应验证的内容 |
|---|---|
| API | 创建、查询、取消、列表过滤、重复请求和并发操作保护 |
| 存储 | 系统盘/数据盘、RWX、挂载、扩容、快照、恢复、重装 |
| KubeVirt | VMIM 阶段、目标 launcher、卷附加 Pod、VMI 节点切换 |
| 网络 | CNI ADD、OVN LSP、requested-chassis、ovn-installed 和业务连通性 |
| 清理 | 成功、失败、取消后的 VMIM、Pod、接口和迁移参数 |
| 兼容性 | OVN/FIP、Calico/macvlan、旧实例和不同存储类型 |
对于 Kube-OVN 的接口 GC,单元测试至少要覆盖:
- 活动
Runninglauncher,接口应保留; Pendinglauncher,接口应保留;Succeeded/Failedlauncher,接口可被判断为终态残留;- 同一个 VMI 同时存在旧
SucceededPod 和新RunningPod; - Pod 正在删除时的行为;
- VMI ID 标签匹配与错误显示名标签不匹配。
同一环境还应做严格 A/B:先运行修复前镜像,记录失败路径;再只替换修复镜像,保持 VM、网络、存储和迁移方向一致。仅在新环境中成功 12 次,能说明修复版本通过了这组实验,但不能单独证明修复前一定能稳定复现,也不能证明所有重试分支都覆盖。
最后总结
KubeVirt 热迁移是一条跨组件的异步控制链,API 调用只是启动入口。理解它需要关注以下几个方面:
- 把上层任务、VMIM、VMI、Pod、PVC/PV、OVN LSP 和 OVS 接口当成不同对象分别观察。
- 把
Pending当作「某个前置条件尚未满足」,继续查等待关系和控制器事件。 - 区分 Pod 名、VMI ID、虚拟机显示名和标签值,先确认真实对象,再写 selector。
- 看到「偶尔成功」时优先检查时序和暂态状态,不要把一次成功当成正确性证明。
- 把资源创建、异步收敛、失败清理和兼容性矩阵都纳入验收。
本次排查最终得到两条故障链:Kube-OVN 依据错误标识误删活动接口;热插拔卷让迁移在 Pending 阶段停留,暴露出网络配置过晚形成的循环等待。前一条解释接口 GC 误删,后一条解释修正 GC 后迁移仍可能在 CNI 和 VMIM 之间等待。