
虚拟机热迁移要解决的问题是：在业务仍然运行的情况下，把一个 VM 从源节点移动到目标节点，并尽量把不可服务时间压到很短。这个能力常用于节点维护、负载均衡、硬件故障规避和集群升级。

如果只看上层命令，热迁移像是一次 `virtctl migrate` 或一次平台侧调度动作。但真正的迁移链路是分层协作：

1. QEMU 负责保存和恢复虚拟机运行状态，包括内存、CPU 和设备状态。
2. Libvirt 负责封装 QEMU 迁移接口，协调源端与目标端的迁移生命周期。
3. KubeVirt 把这套能力放进 Kubernetes，用 CRD、Pod 和控制器管理迁移。
4. Kube-OVN 这类网络插件在网络层补齐 Pod 重建带来的 IP、MAC 和连接连续性问题。

这篇文章按这个顺序拆开看。

<!--more-->

## QEMU 负责迁移什么

QEMU 文档把迁移描述为「把一个正在运行的 guest 从一台机器移动到另一台机器」。

它依赖 QEMU 已经具备的 save/load 能力：源端保存 guest 的设备状态，目标端再按相同配置加载这些状态。

对于 live migration，QEMU 允许 guest 在大部分状态传输期间继续运行，只在最后一小段状态传输时暂停 guest。这个框架在 [QEMU Migration framework](https://www.qemu.org/docs/master/devel/migration/main.html) 里有明确说明。

可以把 QEMU 这一层理解成三类状态的搬迁：

1. CPU 状态，例如寄存器、执行位置、中断相关状态。
2. 内存状态，也就是 guest RAM。
3. 设备状态，例如 virtio 设备、定时器、磁盘控制器和网卡设备模型中的状态。

其中最难的是内存。因为 VM 在迁移时还在运行，guest 会持续写内存，已经发到目标端的页可能又被源端改掉，这些被修改的页就是迁移里常说的 dirty page。

## Pre-copy 与 Post-copy

QEMU/KVM 体系里常见的内存迁移策略是 pre-copy 和 post-copy。KubeVirt 的用户文档也用这三类策略解释迁移行为：pre-copy、post-copy 和 auto-converge。

Pre-copy 的重点是「先复制，后切换」。源端 VM 在大部分迁移时间里继续运行，QEMU 多轮追赶新产生的脏页，最后再短暂停机完成切换。

```mermaid
graph TD
    A["启动迁移"] --> B["目标端创建 VM"]
    B --> C["源端继续运行"]
    C --> D["第一轮传输内存页"]
    D --> E["多轮传输新产生的脏页"]
    E --> F{"剩余脏页是否足够少"}
    F -- "否" --> E
    F -- "是" --> G["暂停源端 VM"]
    G --> H["传输最后脏页和 CPU/设备状态"]
    H --> I["目标端 VM 继续运行"]
    I --> J["清理源端 VM"]
```

Pre-copy 是默认策略，也更适合多数生产场景。它先把内存页从源端复制到目标端；复制期间源端 VM 继续运行，所以还会产生新的脏页。

QEMU 会多轮复制这些脏页，直到剩余状态足够少，再短暂停止源端 VM，复制最后一批状态，然后让目标端继续运行。

QEMU 文档提到 live migration 期间 guest 只需要在最后一段状态传输时停止，通常不可响应时间会依赖很多条件；KubeVirt 文档也说明 pre-copy 是默认策略，适合大多数场景。

Pre-copy 的优点是失败处理相对简单：在切换前，源端仍然保有完整 VM 状态，迁移可以取消或回退。

缺点是它依赖迁移收敛。如果 guest 的 dirty rate 很高，内存被改写的速度超过网络复制速度，迁移就会长时间追不上。

Post-copy 的重点是「先切换，后补页」。目标端 VM 会在内存尚未全部迁完时先运行，访问缺失页面时再从源端按需拉取。

```mermaid
graph TD
    A["启动迁移"] --> B["目标端创建 VM"]
    B --> C["传输必要 CPU/设备状态和少量内存"]
    C --> D["目标端 VM 先运行"]
    D --> E["后台继续迁移剩余内存页"]
    D --> F{"访问页面是否已在目标端"}
    F -- "是" --> G["直接访问目标端内存"]
    F -- "否" --> H["触发缺页"]
    H --> I["目标端向源端请求该页"]
    I --> J["源端发送缺失页面"]
    J --> K["目标端恢复访问"]
    E --> L["所有内存页迁移完成"]
    K --> L
    L --> M["清理源端 VM"]
```

Post-copy 是为高 dirty rate 场景准备的策略。

QEMU 的 [Postcopy 文档](https://www.qemu.org/docs/master/devel/migration/postcopy.html) 说明，post-copy 会在所有内存传输完成前启动目标端 CPU；如果目标端访问了尚未迁移的页面，QEMU 会把这个 fault 转换成向源端请求页面。

这样做的好处是迁移流量和耗时有上界，同一个内存页通常不需要因为反复变脏而重复传输。

代价是风险更高：post-copy 阶段 VM 状态分布在源端和目标端，一侧失败都可能导致 guest 丢失。

Auto-converge 不是第三种完整迁移算法，而是让 pre-copy 更容易收敛的手段。KubeVirt 文档解释为：当 high dirty rate 让迁移难以收敛时，auto-converge 通过 throttle guest CPU 来降低内存改写速度，从而提高迁移完成概率。

## Libvirt 做了什么

QEMU 提供底层迁移能力，但上层系统通常不会直接拼 QMP 命令管理整个生命周期。Libvirt 的作用是把这些能力封装成稳定 API，并负责源端、目标端和 QEMU 之间的协调。

典型流程可以抽象成这样：

1. 上层组件调用 Libvirt migration API 发起迁移。
2. 源端和目标端 Libvirt 协商迁移参数、目标 URI、TLS、磁盘迁移等配置。
3. 目标端按源端 VM 配置准备 incoming QEMU。
4. Libvirt 驱动 QEMU 执行 pre-copy 或 post-copy。
5. 迁移完成后，目标端 VM 接管运行，源端 VM 被停止或清理。

Libvirt 的迁移 flags 直接反映了这些能力边界。例如 [Libvirt domain API](https://libvirt.org/html/libvirt-libvirt-domain.html) 中定义了：

| 参数 | 作用 |
| --- | --- |
| `VIR_MIGRATE_NON_SHARED_DISK` | 迁移完整磁盘镜像以及内存，适用于非共享磁盘场景。 |
| `VIR_MIGRATE_COMPRESSED` | 压缩迁移数据，减少网络传输量。 |
| `VIR_MIGRATE_AUTO_CONVERGE` | 启用让 live migration 最终收敛的算法，通常会降低 guest 运行速度，避免内存变化快于传输速度。 |
| `VIR_MIGRATE_POSTCOPY` | 允许 post-copy，但迁移仍会先按普通模式启动，需要再切到 post-copy。 |

这也是为什么讨论热迁移时不能只看“是否支持迁移”。更关键的问题是：是否共享存储、是否允许 post-copy、是否能接受 CPU throttle、是否能接受压缩带来的 CPU 开销，以及网络带宽是否足够。

## KubeVirt 如何把迁移放进 Kubernetes

KubeVirt 没有重写 QEMU/Libvirt 的迁移算法。它做的是把传统虚拟化迁移放进 Kubernetes 的对象模型和 Pod 生命周期里。

在 KubeVirt 中，每个 VMI 运行在一个 `virt-launcher` Pod 中。热迁移发生时，KubeVirt 会在目标节点创建新的 `virt-launcher` Pod，并把 VM 状态从源 Pod 迁到目标 Pod。

用户可以通过创建 `VirtualMachineInstanceMigration` 对象触发迁移，也可以用 `virtctl migrate` 发起迁移；KubeVirt 官方的 [Live Migration 文档](https://kubevirt.io/user-guide/compute/live_migration/) 明确说明了这两种入口。

```mermaid
graph TD
    A["virtctl migrate / VMIM"] --> B["KubeVirt controller 观察迁移对象"]
    B --> C["目标节点创建 virt-launcher Pod"]
    C --> D["源端 virt-launcher / Libvirt 发起迁移"]
    D --> E["迁移内存、设备状态，必要时迁移磁盘块"]
    E --> F["VMI.status 更新迁移状态"]
    F --> G["迁移完成后清理源端 Pod"]
```

KubeVirt 这一层还有几个重要约束。

首先是存储。官方文档写得很直接：使用 PVC 的 VM 默认要求 PVC 具备 `ReadWriteMany` 访问模式才能 live migrate；如果不是共享存储，就可能需要 block migration，也就是把磁盘块也迁过去。

文档里的 `Migration Method` 也区分了 `LiveMigration` 和 `BlockMigration`：前者表示只复制实例内存，后者表示某些 VMI 磁盘也需要从源端复制到目标端。

其次是网络。KubeVirt 文档列出的限制包括：pod network binding 使用 bridge interface type 时不允许 live migration；迁移还要求 `virt-launcher` Pod 里的 `49152`、`49153` 端口可用；源端和目标端 Pod 的 primary network interface 名称也要一致。

再次是集群级保护。KubeVirt 默认会限制迁移并发和带宽，避免迁移把集群资源打满。官方文档给出的默认值包括：

| 配置 | 默认值 | 含义 |
| --- | --- | --- |
| `parallelMigrationsPerCluster` | `5` | 集群内同时迁移数量上限。 |
| `parallelOutboundMigrationsPerNode` | `2` | 单节点同时作为源端迁出的数量上限。 |
| `bandwidthPerMigration` | `64Mi` | 单个迁移的带宽限制。 |
| `allowAutoConverge` | `false` | 是否允许自动收敛。 |
| `allowPostCopy` | `false` | 是否允许 post-copy。 |

这些默认值说明了 KubeVirt 对热迁移的定位：它不是越快越好，而是要在迁移完成率、业务扰动和集群稳定性之间做约束。

如果不同 VM 对迁移的要求差异很大，不应该只靠全局配置硬扛。

KubeVirt 还提供了 [Migration Policies](https://kubevirt.io/user-guide/cluster_admin/migration_policies/)，可以按 namespace label 或 VMI label 匹配一组 VM，分别覆盖带宽、auto-converge、post-copy、超时等迁移配置。

官方文档也提醒这个 API 是 `v1alpha1`，意味着它适合做精细化控制，但使用前要确认当前集群版本的 API 稳定性。

一个典型用法是：普通业务 VM 走全局默认策略；内存写入很频繁的 VM 单独匹配一条 policy，允许 auto-converge；对可用性要求高但能接受 post-copy 风险的少量 VM，再单独开启 post-copy。

## 迁移网络可以独立配置

源稿里提到了迁移流量可以走 Host 网络或 CNI 网络。

更准确地说，KubeVirt 官方文档提供的是「使用不同网络进行迁移」的能力：管理员可以创建一个专用的 `NetworkAttachmentDefinition`，再在 KubeVirt CR 的 `spec.configuration.migrations.network` 指向这个网络。

这样迁移流量就可以走独立网络，而不是和 Kubernetes 默认网络混在一起。

```yaml
apiVersion: kubevirt.io/v1
kind: Kubevirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  configuration:
    migrations:
      network: migration-network
```

这个能力适合几个场景：

1. 迁移流量很大，希望隔离业务网络和迁移网络。
2. 节点有独立迁移网卡，希望把迁移带宽和延迟做得更可控。
3. 集群安全策略不希望迁移流量直接暴露在默认通道上。

需要注意的是，独立迁移网络不是免费的能力。

KubeVirt 文档要求每个节点至少有第二块网卡，并且这些网卡所在网络彼此互通；如果没有 DHCP，还需要类似 whereabouts 这样的 IPAM 能力。

也就是说，它适合规划过网络拓扑的生产集群，而不是一个只改开关就能解决所有问题的通用优化。

## 迁移前应该检查什么

热迁移的入口很简单，但迁移失败通常不是入口问题，而是 VMI 在启动时就已经不满足迁移条件。

KubeVirt 会在 VMI status 中计算是否可迁移；官方文档也说明，迁移请求会拒绝 non-LiveMigratable 的 VMI。

实际排查时，可以先看这几类信息：

```bash
# 看 VMI 条件，重点关注 LiveMigratable
kubectl get vmi <vmi-name> -n <namespace> -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" "}{.reason}{"\n"}{end}'

# 看迁移方法，LiveMigration 表示主要迁移内存，BlockMigration 表示还需要复制磁盘块
kubectl get vmi <vmi-name> -n <namespace> -o jsonpath='{.status.migrationMethod}{"\n"}'

# 看完整状态和事件
kubectl describe vmi <vmi-name> -n <namespace>
```

如果 `LiveMigratable=False`，优先检查下面几项：

| 检查项 | 关注点 |
| --- | --- |
| PVC access mode | 使用 PVC 的 VM 是否满足 `ReadWriteMany`，或者当前场景是否允许 block migration。 |
| 网络 binding | 是否使用了 KubeVirt 默认不允许迁移的 pod bridge binding，或者是否有 Kube-OVN/OVN-Kubernetes 这类插件补齐网络迁移能力。 |
| 迁移端口 | `virt-launcher` Pod 中 `49152`、`49153` 是否被其他用途占用。 |
| 节点网络 | 源节点与目标节点的迁移网络是否互通，独立迁移网络是否在所有节点都可用。 |
| 设备模型 | 是否挂载了当前 QEMU/KubeVirt 组合不能迁移的设备，例如某些 passthrough 设备。 |

触发迁移后，可以用 VMIM 和 VMI 状态跟踪过程：

```bash
virtctl migrate <vmi-name> -n <namespace>

kubectl get vmim -n <namespace>
kubectl describe vmim <migration-name> -n <namespace>
kubectl describe vmi <vmi-name> -n <namespace>

# 需要取消时
virtctl migrate-cancel <vmi-name> -n <namespace>
```

这比只看 `virtctl migrate` 的返回值更可靠。`virtctl migrate` 成功通常只代表迁移对象提交成功，不代表目标 Pod 调度、Libvirt 迁移、网络切换都已经成功。

## 迁移策略怎么选

可以把策略选择先简化成两个问题：

1. VM 的 dirty rate 会不会高到 pre-copy 难以收敛？
2. 业务能不能接受迁移策略带来的额外扰动或风险？

对应到 KubeVirt 配置，可以这样取舍：

| 场景 | 优先策略 | 原因 |
| --- | --- | --- |
| 常规业务 VM，内存写入不激烈 | Pre-copy | 默认策略，失败时更容易取消或回退。 |
| 迁移偶尔不收敛，但业务能接受短时间 CPU 降速 | Pre-copy + auto-converge | 通过 throttle guest CPU 降低 dirty rate，换取迁移完成率。 |
| 高 dirty rate，维护窗口有限，且可以接受 post-copy 风险 | Post-copy | 目标端先运行，迁移更容易完成；但 post-copy 阶段任一侧失败都有更高风险。 |
| 大量 VM 同时维护迁移 | 限制并发和带宽 | 避免迁移流量、目标节点资源和存储 I/O 被打满。 |
| 固定 IP / 长连接敏感 VM | 网络插件配合迁移优化 | 只迁移内存和磁盘不够，还要保证 IP、MAC 和连接路径连续。 |

KubeVirt 的 `allowWorkloadDisruption` 也要谨慎理解。

官方文档说明，当迁移超过可接受完成时间时，如果允许 workload disruption，控制器可以优先让迁移完成：允许 post-copy 时会切到 post-copy；不允许 post-copy 时则可能暂停对应 VMI。

它不是一个“无损加速”开关，而是在完成迁移和避免业务扰动之间做选择。

因此，生产环境更稳妥的做法是：

1. 全局配置保持保守，控制并发、带宽和超时。
2. 用 `MigrationPolicy` 对特殊 VM 做 label 级覆盖。
3. 先在同类业务上压测 dirty rate、迁移耗时和网络抖动，再扩大范围。
4. 对数据库、消息队列这类状态服务，把应用层 HA 和备份恢复方案作为兜底，而不是完全依赖热迁移。

## Kube-OVN 解决的是网络连续性

到这里为止，QEMU/Libvirt/KubeVirt 主要解决的是 VM 状态迁移和迁移生命周期。

网络还有一个独立问题：VM 被包装在 Pod 里，迁移时源 Pod 和目标 Pod 会发生切换。

如果网络插件不能保留或平滑切换 IP、MAC 和流量路径，业务连接就可能中断。

Kube-OVN 针对 KubeVirt live migration 做了专门处理。

它的 [Live Migration 文档](https://kubeovn.github.io/docs/v1.14.x/en/kubevirt/live-migration/) 先列出了 KubeVirt 在网络迁移上的几个挑战。

默认情况下，KubeVirt 不支持 bridge 网络模式 VM 的 live migration；KubeVirt 主要处理内存和磁盘迁移，没有针对网络迁移做特定优化。

如果 VM IP 改变或网络中断，就无法做到 seamless live migration。

Kube-OVN 的做法是让迁移过程中的源 Pod 和目标 Pod 在一段时间内共享网络身份，并通过 OVS 控制流量。

```mermaid
graph LR
    A["KubeVirt 创建目标 Pod"] --> B["Kube-OVN 识别迁移目标 Pod"]
    B --> C["目标 Pod 复用源端口网络信息"]
    C --> D["配置流量复制"]
    D --> E["目标端口临时禁用，避免流量混乱"]
    E --> F["KubeVirt 同步 VM 内存"]
    F --> G["源端 VM 停止处理流量"]
    G --> H["目标端 VM 激活"]
    H --> I["Libvirt 发送 RARP"]
    I --> J["清理源 Pod 和复制规则"]
```

这里的关键不是简单地让两个 Pod 拿到同一个 IP/MAC。真正避免冲突的是数据面时序和端口状态控制：

1. 预热阶段，源端仍然处理业务流量。
2. 目标端复用源端网络信息，但端口处于临时禁用状态，不对外主动发包。
3. OVS 复制或引导流量，为最后切换缩短网络收敛时间。
4. 切换阶段，源端停止处理流量，目标端激活。
5. Libvirt 发送 RARP，帮助网络设备刷新二层可达性。

Kube-OVN 文档给出的测试结论是：网络中断时间可以控制在 0.5 秒内，TCP 连接因为重试机制不会中断。

这个说法需要按层次理解：

| 层次 | 现象 |
| --- | --- |
| 二层/数据面 | 切换源端口、目标端口和刷新路径时，仍可能有短暂抖动。 |
| UDP/ICMP | 可能观察到少量瞬时丢包。 |
| TCP/SSH | 只要抖动小于 TCP 超时阈值，连接通常不会被重置；VM 内核里的连接状态也随内存状态迁到目标端。 |

开启方式也在 Kube-OVN 文档中给出：在 VM 模板上增加注解。

```yaml
metadata:
  annotations:
    kubevirt.io/allow-pod-bridge-network-live-migration: "true"
```

这个能力适合固定 IP、长连接敏感、不能接受秒级断连的 VM，例如数据库、中间件和状态服务。

但它仍然依赖具体 Kube-OVN 版本、KubeVirt 版本、网络拓扑和业务协议特征，不能简单等同于所有协议、所有负载都绝对零丢包。

## 迁移中看哪些指标

KubeVirt 官方 metrics 文档列出了多组迁移指标，适合把“迁移很慢”拆成几个可观测问题。

| 指标 | 用途 |
| --- | --- |
| `kubevirt_vmi_dirty_rate_bytes_per_second` | 观察 guest 当前 dirty rate。 |
| `kubevirt_vmi_migration_dirty_memory_rate_bytes` | 观察迁移期间内存变脏速度。 |
| `kubevirt_vmi_migration_memory_transfer_rate_bytes` | 观察内存迁移传输速度。 |
| `kubevirt_vmi_migration_data_remaining_bytes` | 观察剩余待迁移数据是否收敛。 |
| `kubevirt_vmi_migration_failed` / `kubevirt_vmi_migration_succeeded` | 判断迁移结果。 |
| `kubevirt_vmi_migrations_in_running_phase` | 观察当前运行中的迁移数量。 |
| `kubevirt_vmi_non_evictable` | 发现设置了 live migration eviction strategy 但不可迁移的 VM。 |

这些指标可以帮助判断根因：

1. `data_remaining` 长时间不下降，同时 dirty rate 接近或超过 transfer rate，通常是 pre-copy 不收敛。
2. `transfer_rate` 很低，要看迁移网络、TLS、压缩、目标节点 CPU 和存储 I/O。
3. running phase 迁移数量过多，要先收紧并发，而不是继续调高单个迁移带宽。
4. `non_evictable` 出现时，节点维护前就应该处理 VM 的存储、网络或设备限制，否则 drain 时会卡住。

对于网络连续性，也不要只看迁移是否 `Completed`。更接近业务体验的验证方式是，在迁移期间同时观察：

```bash
# VM 内或外部客户端持续看丢包和 RTT
ping <vm-ip>

# 长连接类业务可以保持 SSH、数据库连接或应用连接
ssh <vm-ip>

# 集群侧观察迁移对象和事件
kubectl get vmim -A
kubectl get events -n <namespace> --sort-by=.lastTimestamp
```

如果迁移完成但业务连接断了，问题多半不在 QEMU 状态迁移本身。

更常见的位置是网络 binding、CNI 数据面切换、RARP/ARP 收敛或上层协议超时设置。

## 分层看待风险

热迁移不是单点能力，而是一组条件同时满足后的结果。排查或设计时，可以按层看风险。

| 层次 | 主要风险 | 常见缓解方式 |
| --- | --- | --- |
| QEMU | 设备状态不可迁移、dirty rate 过高、post-copy 失败风险 | 控制设备模型，评估 dirty rate，谨慎开启 post-copy。 |
| Libvirt | 参数选择不当、非共享磁盘迁移成本高、TLS/URI 配置错误 | 明确迁移 flags，区分共享存储和块迁移。 |
| KubeVirt | PVC 不满足 RWX、bridge 网络限制、迁移并发过高 | 使用 LiveMigratable 条件、配置并发和带宽上限、规划迁移网络。 |
| CNI/Kube-OVN | IP/MAC 切换、二层收敛、连接抖动 | 使用支持 KubeVirt 迁移优化的网络插件，验证 RARP/流表切换行为。 |

所以，一个 VM 能不能热迁移，不能只看 `virtctl migrate` 是否能提交成功。更可靠的检查顺序是：

1. VMI 是否被标记为 `LiveMigratable`。
2. 存储是共享存储迁移，还是需要 block migration。
3. 网络 binding 是否在当前 KubeVirt/CNI 组合下支持迁移。
4. 业务 dirty rate 是否会导致 pre-copy 不收敛。
5. 迁移并发、带宽、超时和 post-copy/auto-converge 策略是否符合业务风险偏好。

## 小结

虚拟机热迁移的核心不是「把一个进程搬到另一台机器」，而是多层状态的一致切换。

QEMU 负责保存和恢复 VM 的 CPU、内存、设备状态；Libvirt 把 QEMU 的迁移能力封装成可管理的生命周期。

KubeVirt 用 Kubernetes CRD、Pod 和 controller 把迁移放进集群调度与状态机；Kube-OVN 则补上 Pod 网络切换时的 IP、MAC 和连接连续性问题。

这几层分别解决不同问题，也分别引入不同约束。

理解这些边界后，再看热迁移失败、迁移不收敛、网络短断、TCP 连接是否会断，就不容易把所有问题都归因到一个「热迁移支持不好」上。

