
`allowAutoConverge: true` 可以直接用于普通的 pre-copy 迁移。迁移仍从 pre-copy 开始；这个开关允许 QEMU 在难以收敛时逐步限制 Guest CPU，让脏页产生速度降下来。

本地 RWO PVC 不能直接创建普通 `VirtualMachineInstanceMigration`。KubeVirt 会在准入阶段拒绝：

```text
Cannot migrate VMI, Reason: DisksNotLiveMigratable,
Message: cannot migrate VMI: PVC main-source is not shared,
live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
```

RWO 场景要使用 KubeVirt 的 [VolumeMigration](https://kubevirt.io/user-guide/storage/volume_migration/)：准备位于另一节点的目标 PVC，在 VM 上配置 `updateVolumesStrategy: Migration`，再修改 VM 的卷引用。控制器会创建 VMIM，同时迁移内存、运行状态和磁盘内容。

## 实验环境

实验运行在一台 Ubuntu 主机上的三节点 Kind 集群中：

| 项目 | 值 |
|---|---|
| KubeVirt | v1.9.0 |
| Kubernetes | v1.35.0，`kindest/node:v1.35.0` |
| Kind | v0.31.0 |
| 节点 | 1 个 control-plane，2 个 worker |
| 虚拟化 | `/dev/kvm`，AMD KVM |
| 数据盘 | 2 GiB local PV，`ReadWriteOnce`，源、目标分别固定在两个 worker |
| Guest 负载 | 768 MiB 内存持续写脏、数据盘随机写入并 `fsync`、HTTP 心跳 |
| Host 内核 | 6.8.0-139-generic |

Kind 集群定义、RWO local PV/PVC 和测试 VM 使用下面三份清单。修改 VM 的 `claimName` 会触发 VolumeMigration。

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/00-kind.yaml" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/02-storage.yaml" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/04-vms.yaml" >}}

KubeVirt CR 还需要允许 LiveUpdate 和 LiveMigrate：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/01-kubevirt-config.yaml" >}}

完整实验文件固定在 [k8sdev `fcac8be`](https://github.com/jimyag/k8sdev/tree/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters)。其中 `setup.sh` 创建集群并安装 KubeVirt，`run-profile.sh` 负责切换策略和源、目标 PVC，`results/summary.tsv` 保存汇总结果。

本文的字段范围和默认值以 KubeVirt v1.9.0 的 [`MigrationConfiguration`](https://github.com/kubevirt/kubevirt/blob/v1.9.0/staging/src/kubevirt.io/api/core/v1/types.go#L3545-L3602) 和 [`MigrationPolicySpec`](https://github.com/kubevirt/kubevirt/blob/v1.9.0/staging/src/kubevirt.io/api/migrations/v1alpha1/types.go#L41-L61) 定义为准，行为解释对照官方 [Live Migration](https://kubevirt.io/user-guide/compute/live_migration/)、[Migration Policies](https://kubevirt.io/user-guide/cluster_admin/migration_policies/) 和 [Volume Migration](https://kubevirt.io/user-guide/storage/volume_migration/) 文档。`MigrationPolicy` 当前仍是 `v1alpha1`，接口可能继续变化；`experimental` 下的选项不能当作稳定生产 API。

## 迁移模式不是单选项

KubeVirt 的迁移总是从 pre-copy 开始。`completionTimeoutPerGiB` 先根据 VMI 的迁移数据量计算 acceptable completion time；官方文档中的数据量包括内存和需要复制的 ephemeral disk。超过这个时间后，KubeVirt 再根据 `allowPostCopy` 和 `allowWorkloadDisruption` 决定取消、切 post-copy，还是暂停 Guest 完成复制。

下面六个字段既能作为 KubeVirt CR 的集群默认值，也能由 `MigrationPolicy` 按 namespace/VMI label 覆盖。

| 参数 | v1.9.0 默认值 | 触发条件和作用 | 主要影响 | 本次验证 |
|---|---:|---|---|---|
| `allowAutoConverge` | `false` | pre-copy 难以收敛时，允许 QEMU 逐步限制 Guest CPU，降低 dirty rate | 迁移更容易收敛，但 Guest 可用 CPU 和业务性能会下降；不保证一定成功 | 短窗口失败，长窗口成功；未测量实际 CPU 限制比例 |
| `allowPostCopy` | `false` | acceptable completion time 到达后，允许从 pre-copy 切换到 post-copy | 能迁移高 dirty-rate VM；目标运行依赖源端剩余内存页，网络或源节点故障可能导致 VM 崩溃 | 因 userfaultfd 不可用而失败 |
| `allowWorkloadDisruption` | `false` | 超时后不立即取消；允许 post-copy 时优先切换，否则可以暂停 Guest 完成复制 | 提高完成概率，但可能出现可见停顿；暂停时间取决于剩余数据和带宽 | 短窗口失败，较高带宽用例成功 |
| `bandwidthPerMigration` | `0`，不限制 | 限制每个迁移的数据传输速率，单位是 Kubernetes Quantity/秒 | 值低会延长迁移并占用迁移并发槽位；值高会与业务流量和其他迁移争抢网络 | 4 MiB/s 为 124 秒，64 MiB/s 为 30 秒 |
| `completionTimeoutPerGiB` | 150 秒/GiB | 按 VMI 内存和需复制 ephemeral disk 的大小计算可接受完成时间 | 设置过短会在 auto-converge 尚未起效时取消或回退；设置过长会让不收敛迁移长期占用资源 | 20 秒/GiB 用例在 acceptable completion time 80 秒后失败 |
| `maxDowntimeMs` | 900 毫秒 | `MigrationStallDetection` 启用后，限制切换阶段可接受停机时间；主要用于 `allowWorkloadDisruption: false` 的收敛判断 | 值越高越容易切换，但业务停顿预算也越大；超过预算仍无法切换时迁移失败 | v1.9.0 未开 gate 时被准入拒绝；开 gate 后遇到 Alpha 路径错误 |

因此“标准模式开启 `allowAutoConverge`”是有效组合：仍然是 pre-copy，只是允许通过 CPU throttling 帮助收敛。全部策略都在同一份清单中：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/03-policies.yaml" >}}

这组实验只证明配置进入迁移状态，并在 127 秒内完成。它没有直接测量 Guest 被限制了多少 CPU，不能把“成功”进一步解释成某个确定的降频比例。

## 结果

### 模式与超时

| 用例 | 负载 | 结果 | 用时 | 关键证据 |
|---|---|---:|---:|---|
| 标准 pre-copy | 内存脏页 + 磁盘 `fsync` | 成功 | 125 秒 | 全程 `PreCopy`；首次目标镜像拉取包含在总时长内 |
| Auto-converge，短窗口 | 同上 | 失败 | 约 81 秒 | dirty rate 最高记录到 889 Mbps；acceptable completion time 为 80 秒 |
| Auto-converge，长窗口 | 同上 | 成功 | 127 秒 | 全程 `PreCopy`，策略为 `21-auto-converge-long` |
| Post-copy | 同上 | 失败 | 12 秒 | `Userfaultfd not available: Operation not permitted` |
| Pause，短窗口 | 同上 | 失败 | 约 16 秒 | 日志出现 `Pausing the guest`，但暂停后仍未在窗口内完成 |
| Pause，较高带宽 | 同上 | 成功 | 43 秒 | `PreCopy` 后进入 `Paused`，随后完成 |

汇总数据和准入、失败边界保存在固定提交中：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/results/summary.tsv" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/results/admission-and-boundaries.txt" >}}

Post-copy 的失败不是 KubeVirt 配置没有生效。Host 上的 `vm.unprivileged_userfaultfd=0`，QEMU 在切换 capability 时明确返回 userfaultfd 不可用。我没有为了让结果变绿而修改 Host sysctl；生产环境启用 post-copy 前也应先验证内核、容器权限和故障恢复能力。Post-copy 已经让目标依赖源端剩余内存页，迁移网络中断可能直接造成 VM 崩溃，风险高于 pre-copy。

Pause 回退的语义更直白：允许短暂停止 Guest 写入，再复制剩余内存和磁盘数据。本次 8 MiB/s、短窗口的组合进入 `Paused` 后仍失败；提高到 64 MiB/s 并给出稍长窗口后成功。它可以提高完成概率，但代价就是可见停顿。

### 带宽、压缩和策略优先级

关闭内存脏页负载、保留磁盘 `fsync` 后，4 MiB/s 和 64 MiB/s 的同类往返测试分别用了 124 秒和 30 秒。这个结果只适用于本次稀疏 2 GiB local PV、Guest 数据量与节点性能，不能外推成通用的 4.1 倍性能结论；它能确认 `bandwidthPerMigration` 已经实际限制传输，而不是只写进 CR。

`MigrationPolicy` 同时匹配时，选择器更具体的策略获胜。实验中 `60-specific-policy` 同时匹配 namespace、profile 和 tier，`70-broad-policy` 只匹配 profile，最终 VMI 的 `migrationPolicyName` 是 `60-specific-policy`。

v1.9.0 的 `MigrationPolicy.spec.experimental.compression` 还支持 `none` 和 `zstd`。设置 `zstd` 后迁移在 19 秒内完成，virt-launcher 日志明确记录：

```text
Migration compression enabled: method=zstd
```

这次没有做 CPU 使用量和网络字节数对照，因此只能确认压缩被启用并成功完成，不能宣称它一定更快。

### 并发限制

并发参数只能在 KubeVirt CR 上设置，不能由 MigrationPolicy 覆盖。两个对照 patch 分别隔离集群级上限和单节点迁出上限：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/07-concurrency-cluster-patch.yaml" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/08-concurrency-node-patch.yaml" >}}

两个辅助 VM 同时变更 RWO PVC 时，`parallelMigrationsPerCluster: 1`、单节点上限为 3 的实验观测到 `max_running=1`。反向迁移时改成集群上限 3、`parallelOutboundMigrationsPerNode: 1`，同样只有一个 VMIM 进入 `Running`。两轮结束后都恢复为 3。

### 取消 VolumeMigration

VolumeMigration 不能照搬普通 VMIM 的取消方式。正确动作是把 VM 的卷集合精确恢复到迁移前的值。实验在 VMIM 进入 `Running` 后把 `claimName` 从目标 PVC 改回源 PVC，VMIM 进入 `Failed`，VM 仍使用原 PVC 并留在原节点。

失败恢复还要检查 `targetNodeDomainReadyTimestamp`。如果目标端 Domain 已经启动，但 VMIM 最终标为失败，KubeVirt 文档要求保留目标卷并走手工恢复，不能机械地切回源卷。

## 参数分层与验证边界

KubeVirt v1.9.0 的参数分成三个层次。

### KubeVirt CR 与 MigrationPolicy 都支持

以下字段可以在集群级设置，也可以由策略按 namespace/VMI label 覆盖：

- `allowAutoConverge`
- `allowPostCopy`
- `allowWorkloadDisruption`
- `bandwidthPerMigration`
- `completionTimeoutPerGiB`
- `maxDowntimeMs`

`MigrationPolicy` 另外暴露两组实验参数。默认值和算法含义来自 v1.9.0 的 [`StallDetectorOptions`](https://github.com/kubevirt/kubevirt/blob/v1.9.0/staging/src/kubevirt.io/api/core/v1/types.go#L3420-L3487) 定义。

| 字段 | v1.9.0 默认值 | 作用与影响 | 本次验证 |
|---|---:|---|---|
| `experimental.compression` | 未设置 | `none` 关闭压缩，`zstd` 在 multifd 通道启用 Zstandard；通常以额外 CPU 开销换取更少的传输字节 | `zstd` 生效并在 19 秒内完成；未做 CPU 和网络字节对照 |
| `stallDetector.stallMargin` | 4% | 将剩余字节与历史最佳值比较时的容差；容差大会更容易把当前状态判定为停滞或局部最小值 | 进入 Alpha 路径后失败，未单独验证 |
| `stallDetector.ewmaAlpha` | `0.4` | 迁移带宽 EWMA 平滑因子，范围是 `(0, 1]`；值越高，估算越看重最近样本 | 同上 |
| `stallDetector.stallProgressTimeout` | 40 秒 | 跟踪最小剩余字节、判定停滞的滑动窗口 | 同上 |
| `stallDetector.switchoverTimeout` | 60 秒 | 触发 stop-and-copy 或 post-copy 后允许的切换时间；超时则中止迁移 | 同上 |
| `stallDetector.precopyPossibleFactor` | `1.5` | 估算停机时间可超出 `maxDowntimeMs` 的最大倍数，仍可尝试软 stop-and-copy；超过后中止 | 同上 |
| `stallDetector.patienceWindowDecayFactor` | `0.5` | 每次放宽最佳剩余字节基线后，缩短下一个等待窗口的倍数 | 同上 |
| `stallDetector.searchLocalMinima` | `true` | 延后收敛动作，等待剩余字节接近已观测的局部最小值；为 `false` 时可在检测到停滞后立即动作 | 同上 |
| `stallDetector.completionTimeoutFactor` | `2` | 放大基于 `completionTimeoutPerGiB` 计算的时间预算，用于判断强制切换是否还来得及完成 | 同上 |

本次启用 `MigrationStallDetection` 后，RWO block migration 在 pause 用例中返回 `value of ewmaBandwidthBps not set!`。这是一条 Alpha 路径上的实测失败，不应作为生产配置示例。

### 只能在 KubeVirt CR 设置

下面的字段只能在 KubeVirt CR 设置。表中的默认值来自 v1.9.0 源码定义；实验为了隔离变量，把并发基线改成了 3/3，并在对应用例中临时覆盖。

| 字段 | v1.9.0 默认值 | 能做什么、何时触发 | 影响和风险 | 本次验证 |
|---|---:|---|---|---|
| `parallelMigrationsPerCluster` | 5 | 限制整个集群同时处于运行阶段的迁移数 | 太低会排队；太高会同时消耗网络、CPU、存储吞吐和目标节点资源 | 设置为 1 后，两台 VM 同时迁移时 `max_running=1` |
| `parallelOutboundMigrationsPerNode` | 2 | 限制单个源节点同时迁出的 VM 数 | 避免维护或故障转移时把单节点网络、CPU 打满；也可能延长 drain 时间 | 设置为 1 后，同一源节点只有一个迁移进入 `Running` |
| `progressTimeout` | 150 秒 | 剩余数据连续完全没有进展达到该时间后，把迁移视为卡死并取消；只要出现进展，计时就会重置 | 它处理的是“完全卡住”，不是 dirty rate 上下波动导致的不收敛；过短可能误杀短时网络抖动 | 设置为 5 秒并做断流实验，但未隔离出单一计时器结果 |
| `utilityVolumesTimeout` | 150 秒 | VMIM 在 `Pending` 阶段等待 containerDisk、cloud-init 等 utility volume detach；超时仍未释放则标记 `Failed` | 只影响迁移准备阶段，不是内存或磁盘复制超时 | 字段进入迁移配置；未故意制造 utility volume detach 卡住 |
| `unsafeMigrationOverride` | `false` | 兼容性检查认为迁移对 Guest 不安全时，允许继续创建迁移 | 只绕过 KubeVirt 检查，不会让不兼容 CPU、设备或存储突然具备迁移能力，可能造成 Guest 故障 | 保持 `false`，只验证 API 和状态传播 |
| `disableTLS` | `false` | `true` 会关闭 KubeVirt 迁移代理上的额外 TLS 保护 | v1.9.0 明确将其视为高风险配置；迁移内存和磁盘数据失去该层加密，必须依赖可信迁移网络 | `true` 时迁移成功，实验后恢复 `false` |
| `network` | 未设置，使用 Pod 网络 | 指定迁移数据使用的 CNI 网络名称 | 可以隔离业务和迁移流量，但所有节点必须具备一致、可达的 Multus/NAD 配置 | Kind 环境没有第二迁移网络，未做行为验证 |
| `matchSELinuxLevelOnMigration` | `false` | 默认把目标 virt-launcher 的 SELinux level 固定为源端 level；设为 `true` 后由 CRI 为目标随机分配 level | 随机 level 能减少同节点 category 共享，但不自动处理 SELinux level 的 RWX 卷可能导致迁移失败 | Host 无可用 SELinux 环境，未做行为验证 |
| `nodeDrainTaintKey` | `kubevirt.io/drain` | 指定哪一个 taint key 表示节点进入 drain | 依赖已弃用的 node taint 功能；key 与维护流程不一致时不会按预期触发迁移 | API 已验证；RWO VolumeMigration 用例未覆盖节点 drain |

`progressTimeout` 这组结果值得保守解释。设置为 5 秒后，阻断控制和数据端口会让迁移失败，但直接原因是 libvirt client socket closed；只阻断两个数据端口 20 秒，迁移恢复后仍然成功。实验没有证明“连续 5 秒没有网络字节就一定失败”，也没有否定该参数。它说明迁移栈还有 proxy、keepalive、QEMU 进度上报等状态，必须用源端 virt-launcher 日志确认真正触发者。

### VMIM 自身字段

`VirtualMachineInstanceMigration.spec` 的完整字段定义见 v1.9.0 [`VirtualMachineInstanceMigrationSpec`](https://github.com/kubevirt/kubevirt/blob/v1.9.0/staging/src/kubevirt.io/api/core/v1/types.go#L1778-L1810)。

| 字段 | 能做什么 | 限制与影响 | 本次验证 |
|---|---|---|---|
| `vmiName` | 指定要迁移的 VMI | VMI 必须存在于 VMIM 所在 namespace | VolumeMigration 由控制器填写，多轮成功和失败用例均覆盖 |
| `addedNodeSelector` | 在 VM 原有 `nodeSelector` 或 `nodeAffinity` 上增加目标节点约束 | 只能收紧约束；key 冲突时保留 VM 上的值，不能用它绕过 VM 的限制 | 直接 VMIM 在调度前先被 RWO 磁盘校验拒绝，未进入节点选择 |
| `priority` | 控制器内部区分 `system-critical`、`user-triggered` 和 `system-maintenance` 迁移 | 不是 Kubernetes PriorityClass 名称，普通用户不能直接设置 | 提交 `system-cluster-critical` 被准入拒绝：`only virt-controller is allowed to set priority field` |
| `sendTo` | 把该 VMIM 标记为去中心化迁移的源端；包含 `migrationID` 和目标 synchronization controller 的 `connectURL` | 需要预先在目标集群建立 VM 和必要资源，两端 `migrationID` 必须匹配 | 未覆盖 |
| `receive` | 把该 VMIM 标记为去中心化迁移的目标端；提供与源端匹配的 `migrationID` | 按官方 [decentralized live migration](https://kubevirt.io/user-guide/compute/decentralized_live_migration/) 流程配置目标 VM、同步地址和集群间 CA 信任 | 未覆盖 |

## 怎么选

生产基线仍应从 pre-copy 开始，并为 `bandwidthPerMigration`、集群并发和单节点迁出并发设置明确上限。

持续高 dirty-rate 的工作负载可以先尝试 `allowAutoConverge: true`。它保留 pre-copy 的故障边界，但会牺牲 Guest 性能；监控应同时观察迁移时长、Guest CPU、业务延迟和 dirty rate。只把 `completionTimeoutPerGiB` 调得很短，auto-converge 还没来得及起作用，迁移就可能先被取消。

如果业务接受短暂停顿，可以考虑 `allowWorkloadDisruption: true` 的 pause-and-copy 回退。Post-copy 应当放在更严格的环境验证之后：userfaultfd、迁移网络可靠性和中断恢复缺一不可。

最后，不要只看到 VMIM `Running` 或目标 Pod 已创建就判定成功。本次验收以 VMIM `Succeeded`、VMI 节点切换、VM 卷引用切换和 Guest 心跳持续为准；失败用例还要确认 VM 回到源节点和源 PVC，或者进入文档规定的手工恢复分支。

## 复现实验

实验使用七个脚本。`setup.sh` 负责环境，`run-*.sh` 执行用例，`monitor-vmim.sh` 和 `collect-migration.sh` 采集状态与证据。

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/setup.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/run-profile.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/run-concurrency.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/run-cancel.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/run-progress-timeout.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/monitor-vmim.sh" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/fcac8be06c7a3323df008dc5156b761f8576f6c0/kubevirt-migration-parameters/collect-migration.sh" >}}

按下面的顺序执行。`setup.sh` 使用独立 kubeconfig，不覆盖默认 context。

```bash
git clone https://github.com/jimyag/k8sdev.git k8sdev-kubevirt-migration
cd k8sdev-kubevirt-migration
git checkout fcac8be06c7a3323df008dc5156b761f8576f6c0
cd kubevirt-migration-parameters

# 创建三节点 Kind 集群、安装 KubeVirt，并部署 RWO PVC 与测试 VM。
./setup.sh

# 运行单个策略；第三个参数是结果目录。
./run-profile.sh migration-main standard results/01-standard
./run-profile.sh migration-main auto-converge-long results/07-auto-converge-long

# 分别验证集群级和单节点迁出并发。
./run-concurrency.sh 07-concurrency-cluster-patch.yaml results/12-concurrency-cluster
./run-concurrency.sh 08-concurrency-node-patch.yaml results/13-concurrency-node

# 验证恢复原卷集合会取消正在运行的 VolumeMigration。
./run-cancel.sh results/14-cancel
```

实验完成后删除专用集群：

```bash
kind delete cluster --name kv-migration-params
```

参数定义与行为边界可继续对照 KubeVirt 的 [Live Migration](https://kubevirt.io/user-guide/compute/live_migration/)、[Migration Policies](https://kubevirt.io/user-guide/cluster_admin/migration_policies/) 和 [API Reference](https://kubevirt.io/api-reference/main/definitions.html)。

