

KubeVirt 的热迁移看起来像一个动作：把正在运行的虚拟机从一个节点搬到另一个节点。但实际过程同时涉及容器编排平台（Kubernetes，K8s）、KubeVirt、存储、容器网络接口（Container Network Interface，CNI）、开放虚拟网络（Open Virtual Network，OVN）、开放虚拟交换机（Open vSwitch，OVS）和多个控制器。只要其中一个对象没有在正确的时间进入正确的状态，迁移就可能长时间等待，或者表现为「偶尔成功、偶尔失败」。

下面按这次排查中实际遇到的对象和状态展开，先说明各组件的职责，再沿着虚拟机、磁盘、Pod、网络端口和状态机寻找证据。

## 先看结论

这次问题涉及几个层次，现象和原因对应如下：

1. **云盘访问模式决定迁移能否共享存储。** 在使用共享 Ceph 存储、并且迁移期间需要让源节点和目标节点同时访问卷的路径中，新建系统盘和数据盘需要使用多节点读写（ReadWriteMany，RWX）。这项要求取决于存储类型和迁移实现，单节点读写云盘需要单独验证。
2. **KubeVirt 的迁移通过目标侧 Pod 完成。** KubeVirt 会创建目标侧的 `virt-launcher` Pod（虚拟机启动器），并在满足网络、存储和状态条件后，将虚拟机切换到目标节点。
3. **早期的网络失败与 Kube-OVN 的接口垃圾回收有关。** OVS 记录的对象标识是虚拟机实例 ID，但旧代码用错误的 Pod 名或旧标签查询，导致正在初始化的目标接口被误认为孤儿接口并删除。
4. **修复接口匹配后，仍暴露出热插拔卷（hotplug volume）的时序问题。** 目标 Pod 的网络需要在迁移仍处于 `Pending` 时就准备好；如果只在 `Scheduling` 阶段处理，KubeVirt 可能因为卷附加 Pod（attachment Pod）等待网络而永远到不了 `Scheduling`。
5. **「偶尔成功」来自一个很短的时序窗口。** 旧链路中，目标节点有时会短暂地把逻辑端口判定为可用，`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 背后的块存储卷。

```mermaid
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 分属不同对象：

```text
上层任务：面向应用程序编程接口（Application Programming Interface，API）用户，保存任务标识符（Identifier，ID）、状态、完成时间
VMIM：面向 KubeVirt，推进实际的虚拟机迁移
VMI：面向当前运行态，记录虚拟机此刻在哪个节点
virt-launcher：真正承载虚拟机进程的 Pod
```

因此，上层控制器返回成功或任务进入 `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 创建时接起来。

## 这次解决什么问题

这次排查针对两类现象。它们发生在同一条热迁移链路上，触发条件和修复位置不同。

1. **目标 OVS 接口在初始化阶段被清理。** 目标 `virt-launcher` Pod 已经创建，CNI 正在等待 OVS 接口完成绑定。Kube-OVN 的接口垃圾回收却用错误的标识查询活动 Pod，把仍在使用的接口当成残留接口删除，CNI 最后等待超时。
2. **带热插拔卷的迁移长期停留在 `Pending`。** 目标 `virt-launcher` 和卷附加 Pod 都需要网络，网络配置却等到 `Scheduling` 阶段才执行。目标资源等不到网络，VMIM 也等不到目标资源进入下一阶段，形成循环等待。

修复目标很明确：让接口垃圾回收找到正确的活动 Pod，并把迁移网络配置提前到 `Pending` 阶段。这样目标 launcher 和卷附加 Pod 可以先完成网络准备，VMIM 再继续推进；迁移成功时端口切到目标节点，失败时回滚到源节点并清理临时参数。

## 修复前的流程

### 目标接口被清理

```mermaid
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 没有被找到，接口因此进入清理分支。

### 热插拔卷造成循环等待

```mermaid
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

```mermaid
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

```mermaid
%%{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]
```

准备阶段完成后，迁移进入状态流转：

```mermaid
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 和日志为准。此次问题涉及的关键阶段可以简化为：

```mermaid
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` 指向自身的回环，避免渲染时让边标签互相覆盖。

```text
目标 launcher 要网络
    ↓
网络控制器等待 VMIM 进入 Scheduling
    ↓
VMIM 等待目标 launcher 和卷附加 Pod 就绪
    ↓
目标 launcher 仍然没有网络
```

修复的关键是在 `Pending` 阶段提前设置网络迁移所需的参数；到了 `Scheduling` 仍然要保留幂等重试，因为控制器缓存事件可能乱序，第一次处理时节点或对象未完全可见。

`MigrationState` 也可能影响第一次协调。目标 launcher handoff 之前，这个字段可能仍为空，或暂时保留上一轮迁移的状态。`Pending` 和 `Scheduling` 阶段可以直接使用以下两个对象字段推导迁移方向：

```text
源节点：VMI.status.nodeName
目标节点：目标 virt-launcher Pod.spec.nodeName
```

如果目标 launcher 尚未创建，或者源、目标节点的字段暂时为空，控制器应返回错误并通过限速队列重新入队。Pod 创建和调度完成都不保证一定产生新的 VMIM 阶段事件，静默返回会丢失提前配置网络的机会。

## 第一条故障链：目标接口被错误清理

### 什么是接口垃圾回收

Kube-OVN 的 daemon 会定期扫描节点上的 OVS 接口。一个刚创建但还没有完成 OVN 安装的接口，可能是正常初始化中的接口，也可能是上一次 Pod 删除后留下的孤儿接口。垃圾回收（garbage collection，GC）需要在两者之间做判断：

```text
接口尚未 ready
    ├── 对应活动 Pod 仍存在：保留接口，等待初始化
    └── 没有对应活动 Pod：删除接口，清理残留
```

### 迁移时的对象标识并不相同

目标 launcher 的真实 Pod 名称类似：

```text
virt-launcher-vmi-id-随机后缀
```

KubeVirt 网络适配逻辑写入 OVS `external_ids` 的 `pod_name` 使用的是 VMI ID。这两个值分别代表不同对象：

```text
真实 Pod 名：virt-launcher-vmi-id-abc12
OVS pod_name：vmi-id
```

同时，Pod 上的标签也有明确语义：

```text
vmi.kubevirt.io/id=vmi-id
vm.kubevirt.io/name=虚拟机显示名
```

旧逻辑先把 `pod_name` 当作 Pod 名称查询，再用 `vm.kubevirt.io/name=<VMI ID>` 作为兜底。其中 `vm.kubevirt.io/name` 存的是显示名，VMI ID 位于专用的 `vmi.kubevirt.io/id` 标签中，因此两次查询都找不到活动 launcher。GC 于是把正在初始化的目标接口当成孤儿接口删除，CNI 最终报错：

```text
ovs interface ... is not ready after 30s
```

正确的匹配关系是使用 `vmi.kubevirt.io/id`：

```go
selector := labels.SelectorFromSet(map[string]string{
    kubevirtv1.VirtualMachineInstanceIDLabel: podName,
})
```

**OVS 中保存的是 VMI ID，GC 应按 VMI ID 标签寻找 Pod。Pod 名称和虚拟机显示名都不能代替这个 ID。**

### Completed Pod 是原因吗

迁移完成后，源节点上可能留下一个 `Succeeded` 的旧 `virt-launcher` Pod。它需要后续清理，本次接口超时的根因在活动 launcher 的匹配逻辑。

证据有两点：

1. 一台没有历史 Completed Pod 的新虚拟机，第一次迁移仍然出现了相同的接口超时。
2. 手动删除 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 又依赖目标网络，等待被放大，于是循环等待稳定暴露。

修复后的处理思路是：

1. `Pending` 阶段就识别目标 launcher 和目标节点。
2. 在对象暂时还没有出现在控制器缓存时限速重新入队（requeue），让后续事件继续触发处理。
3. 提前设置 OVN 多节点承载参数。
4. `Scheduling` 阶段继续执行同一动作，保证重复事件和乱序事件不会破坏状态。
5. `Succeeded` 时把端口收敛到目标节点，`Failed` 时回滚到源节点，并清理迁移期间的临时参数。

目标 Pod 的选择也需要区分 launcher 和卷附加 Pod。两类 Pod 共享当前 VMIM 的迁移 UID，只按 UID 查询会得到多个结果；目标 launcher 还带有 `kubevirt.io=virt-launcher` 标签，卷附加 Pod 则使用热插拔磁盘标签。控制器应使用迁移 UID 加 `kubevirt.io=virt-launcher` 选择目标 launcher，再读取它的 `spec.nodeName` 作为目标节点：

```text
迁移 UID + kubevirt.io=virt-launcher
        ↓
目标 virt-launcher Pod
        ↓
target = Pod.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 的对应关系如下：

```text
NB: Logical_Switch_Port（LSP）
             ↓ 对应
SB: Port_Binding
             ├── 源节点 OVS 接口
             └── 目标节点 OVS 接口
```

这里复用一个逻辑端口，源节点和目标节点各自准备本地 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`。

修复前的日志中出现过这样的顺序：

```text
目标节点短暂 claim 逻辑端口
    ↓
流表安装完成
    ↓
设置 ovn-installed=true
    ↓ 约 6～9 ms 后
删除 ovn-installed
```

这个 `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 版本、存储类型和网络实现：

```bash
kubectl --kubeconfig /path/to/ovn-kubeconfig get vmi -A
kubectl --kubeconfig /path/to/ovn-kubeconfig get vmim -A
kubectl --kubeconfig /path/to/ovn-kubeconfig get pods -A -o wide
```

OVN/浮动 IP（Floating IP，FIP）网络与 Calico/macvlan 网络需要分开验证，结果分别记录；非 OVN 环境不要带 OVN/FIP 专用迁移注解。

排查时按下面的证据链观察迁移：

```text
上层迁移任务
    ↓
VMIM 创建与阶段
    ↓
源/目标 VMI 与 virt-launcher Pod
    ↓
PVC/PV attach 和挂载
    ↓
CNI ADD、OVN LSP、OVS 接口
    ↓
目标 Pod Ready
    ↓
VMIM Succeeded/Failed
    ↓
上层任务终态与操作锁释放
```

如果某一层卡住，就在这一层收集日志和对象快照，不要直接跳到下一层下结论。

## 扩容也要检查异步收敛

验收范围还应包括在线扩容。扩容可能停在 `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，单元测试至少要覆盖：

- 活动 `Running` launcher，接口应保留；
- `Pending` launcher，接口应保留；
- `Succeeded`/`Failed` launcher，接口可被判断为终态残留；
- 同一个 VMI 同时存在旧 `Succeeded` Pod 和新 `Running` Pod；
- Pod 正在删除时的行为；
- VMI ID 标签匹配与错误显示名标签不匹配。

同一环境还应做严格 A/B：先运行修复前镜像，记录失败路径；再只替换修复镜像，保持 VM、网络、存储和迁移方向一致。仅在新环境中成功 12 次，能说明修复版本通过了这组实验，但不能单独证明修复前一定能稳定复现，也不能证明所有重试分支都覆盖。

## 最后总结

KubeVirt 热迁移是一条跨组件的异步控制链，API 调用只是启动入口。理解它需要关注以下几个方面：

1. 把上层任务、VMIM、VMI、Pod、PVC/PV、OVN LSP 和 OVS 接口当成不同对象分别观察。
2. 把 `Pending` 当作「某个前置条件尚未满足」，继续查等待关系和控制器事件。
3. 区分 Pod 名、VMI ID、虚拟机显示名和标签值，先确认真实对象，再写 selector。
4. 看到「偶尔成功」时优先检查时序和暂态状态，不要把一次成功当成正确性证明。
5. 把资源创建、异步收敛、失败清理和兼容性矩阵都纳入验收。

本次排查最终得到两条故障链：Kube-OVN 依据错误标识误删活动接口；热插拔卷让迁移在 `Pending` 阶段停留，暴露出网络配置过晚形成的循环等待。前一条解释接口 GC 误删，后一条解释修正 GC 后迁移仍可能在 CNI 和 VMIM 之间等待。

### 参考资料

- [KubeVirt Live Migration](https://kubevirt.io/user-guide/compute/live-migration/)
- [KubeVirt API Reference](https://kubevirt.io/api-reference/)
- [Kubernetes Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
- [Kubernetes Container Network Interface](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)
- [OVN controller documentation](https://www.ovn.org/support/dist-docs/ovn-controller.8.html)
- [Kube-OVN PR #7415：热插拔卷迁移阶段的网络处理](https://github.com/kubeovn/kube-ovn/pull/7415)
- [Kube-OVN Issue #7416：KubeVirt 热迁移网络状态推进问题](https://github.com/kubeovn/kube-ovn/issues/7416)

