virt-report虚拟化社区动态

Libvirt 热迁移的阶段协调与失败恢复

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、Libvirt 热迁移的协调职责

Libvirt 在热迁移中协调两端的资源准备、QEMU 状态传输、目的端接管和源端清理。例如将运行中的 demo 从主机 A 迁到 B,B 需要先准备接收资源,QEMU 随后传输执行状态,管理层最后确认接管结果。确认前的源实例用于保留恢复机会,确认后的清理则避免遗留旧进程与资源。

A、B 各自运行 Libvirt 管理守护进程,迁移期间还各有一个承载 demo 的 QEMU 进程。Libvirt 的“QEMU 驱动”运行在 virtqemud 或单体 libvirtd 内,负责域定义、作业和 QMP 控制;RAM、vCPU 及设备状态属于被它管理的 QEMU / KVM 实例。后面的 Begin、Finish 等函数执行在管理路径,页面传输则由两个 QEMU 完成。

Libvirt 的版本 3 迁移协议将这个过程拆成 Begin、Prepare、Perform、Finish、Confirm 五个阶段。本文沿一台使用共享存储的预拷贝虚拟机追踪成功路径,再讨论客户端断线、完成确认丢失及 postcopy 的恢复边界。内存迭代见QEMU 预拷贝内存迁移与停机时间,这里重点说明各阶段的发起方、执行权归属和失败后的状态核对。

源码版本:Libvirt 12.0.0、QEMU 10.2.0。

二、控制连接与迁移数据通道

控制面携带域 XML、准备请求、阶段结果与迁移 cookie;数据面携带 RAM、CPU 和设备状态。使用原生传输时,两个 QEMU 之间传递状态,Libvirt 负责配置、驱动和观察迁移。控制连接使用 SSH,不代表 QEMU 的数据连接也经过 SSH;迁移数据包含客户机内存,生产环境需另外配置数据通道的 TLS 或受控网络。

协调方式谁推进五个阶段客户端断开后的影响
Managed direct客户端同时连接源端与目的端 Libvirt,交替调用阶段接口协调者消失;依据当前阶段尝试取消、清理,结果不确定时可能保留暂停实例
Managed peer-to-peer(--p2p)客户端向源端提交任务,源端 Libvirt 连接目的端并协调全过程客户端退出不会中止已接管的迁移;源端守护进程及其到目的端的连接仍是依赖

p2p 改变协调责任,--tunnelled 改变状态数据的传输路线,两者应分别理解。隧道模式通过 Libvirt 连接承载数据,减少额外网络入口,也增加拷贝与守护进程参与;并行迁移、postcopy 等能力还受隧道支持限制。p2p 使用源端守护进程的凭据访问 B,客户端能连 B 并不能证明这条连接也能成功。

例如运维电脑通过 SSH 登录 A、B,采用 managed direct;同一迁移改为 p2p 后,后续 Prepare 与 Finish 请求从 A 发往 B。QEMU 的原生状态连接在两种情况下都可以直接从 A 到 B。控制模式与数据传输说明

三、五阶段协议中的资源与执行权交接

以下是普通运行中虚拟机的预拷贝成功路径,未设置迁移后保持暂停的选项。源端在迁移末尾停止 vCPU,传出最后状态;目的端 Finish 验证接收和资源准备结果,再恢复客户机。源端收到 Confirm 成功后才清理原实例。

Libvirt 预拷贝迁移的五阶段协调
客户端调用源端 Begin、目的端 Prepare、源端 Perform、目的端 Finish、源端 Confirm。QEMU 状态流走独立的数据通道。目的端恢复客户机后,源端旧实例保持暂停,直到成功确认后被清理。
客户端调用源端 Begin、目的端 Prepare、源端 Perform、目的端 Finish、源端 Confirm。QEMU 状态流走独立的数据通道。目的端恢复客户机后,源端旧实例保持暂停,直到成功确认后被清理。 打开原图
Mermaid 源码
sequenceDiagram
  accTitle: Libvirt 预拷贝迁移的五阶段协调
  accDescr: Managed direct 模式下,客户端先向源端请求 Begin,再向目的端 Prepare,然后调用源端 Perform。状态数据由两个 QEMU 直接传输。目的端 Finish 确认接收并恢复客户机后,客户端通知源端 Confirm,清理暂停的旧实例。仅描绘普通预拷贝成功路径。
  participant C as 客户端
  participant S as 源 Libvirt
  participant Q as 源 QEMU
  participant R as 目的 QEMU
  participant D as 目的 Libvirt
  C->>S: Begin:检查配置
  S-->>C: 可迁移 XML、cookie
  C->>D: Prepare:创建接收环境
  D->>R: 创建机器,配置 incoming
  Note over R: 接收状态,暂不执行客户机
  D-->>C: 数据地址、cookie
  C->>S: Perform:发起迁移
  S->>Q: QMP migrate
  Q->>R: RAM 全量及脏页迭代
  Note over Q: 停止源 vCPU
  Q->>R: 剩余 RAM、CPU 与设备状态
  Q-->>S: 源侧传输完成
  S-->>C: Perform 结果、cookie
  Note over Q: 保留暂停的旧实例
  C->>D: Finish:确认迁入结果
  D->>R: 检查完成状态,恢复 CPU
  Note over R: 目的端接管客户机执行
  D-->>C: 目标域句柄、cookie
  C->>S: Confirm:确认成功
  S->>Q: 清理旧实例
  S-->>C: 迁移协调完成
阶段主要工作完成后掌握的信息
Begin · 源端校验迁移条件,生成可迁移 XML 与 cookie,建立必要的变更保护得到供目的端创建兼容机器的定义;源客户机继续运行
Prepare · 目的端准备存储、网络、权限与 QEMU,配置 incoming 监听返回可连接的迁移地址及能力信息;有接收实例,客户机尚未接管执行
Perform · 源端配置 QEMU 迁移参数并发起传输,等待源侧阶段完成普通预拷贝成功时源客户机已停;源 QEMU 进程仍保留,等待目的端结果
Finish · 目的端核对迁入结果、刷新设备状态,按 flags 处理持久定义和 CPU 恢复返回目标域句柄或失败;成功后 B 已接管,或按要求保持暂停
Confirm · 源端根据 Finish 结果结束源端作业成功则清理旧实例;失败走恢复分支,postcopy 另行处理

这里有两个不同的“完成”:QEMU 完成状态传输,以及 Libvirt 完成运行位置与资源的交接。Prepare 成功只说明目标能够接收,Perform 成功仍要等待 Finish。类似地,目标客户机已运行也不等于源端清理已结束。

目标持久定义由 --persistent 控制;迁移成功后删除源持久定义由 --undefinesource 控制。默认行为允许源端保留一个已关闭的持久定义。这与源 QEMU 是否仍在执行是两件事,用“源端还能查到名字”判断迁移失败会产生误判。

四、连接失败后的双端状态核对

考虑同一台 demo:B 已加载状态并恢复客户机,但 Finish 的 RPC 响应在网络中丢失。协调者看到的是“调用失败”,B 上实际发生的却是“已开始执行”。若此时直接恢复 A,两个执行流可能同时更新共享磁盘。

固定版本源码在 ddomain == NULL 的分支中明确讨论了这种歧义,并依赖锁管理插件提供安全保护。集群还需要与存储和隔离策略配合:一个只能约束本机进程的锁,无法天然承担跨主机仲裁;网络不通也不能证明对方已停止。Finish 结果丢失的处理与限制

失败位置实例可能处于什么状态恢复前需要确认
Prepare 失败A 通常仍执行,B 只有部分准备资源目标准备清理完成,源端迁移作业已经结束
预拷贝传输失败,目标尚未执行A 可能仍运行,也可能刚进入暂停;B 未完成接收目标未接管,源端状态与后端访问权可以恢复
Perform 完成后协调连接丢失A 已暂停;B 是否已经完成 Finish 可能未知查询 B 的真实运行状态、源端阶段与存储锁,消除双端执行风险
Finish 成功,Confirm 失败B 已接管;A 可能遗留暂停的 QEMU按成功迁移清理源残留;保留暂停不等于应恢复 A

qemuMigrationAnyConnectionClosed() 处理等待下一阶段时的控制连接关闭。遇到 PERFORM3_DONE,它记录“无法确定目标是否启动”的情况,收束相应作业,并不凭断线就恢复旧 vCPU。正常 API 失败、阶段间客户端消失、守护进程重启分别有自己的路径,不能概括成“断线必回滚”。阶段间断连处理

p2p 消除了客户端作为长期协调者的依赖,但无法消除源端守护进程故障、主机故障和两端网络分区。它改善的是任务的归属与存活范围,资源归属判断仍然需要双端证据。

五、postcopy 切换后的失败处理

预拷贝在交接前保留源端完整执行状态,目的端尚未运行时有条件退回源端。postcopy 提前把执行权移到 B,缺失 RAM 页继续从 A 获取;B 的内存和设备状态随后持续变化,A 保留的旧执行点已不再代表最新虚拟机。

因此,postcopy 网络中断后的首选恢复方向是保留两端、恢复迁移连接并继续补页。Libvirt 使用 VIR_MIGRATE_POSTCOPY_RESUME 区分恢复请求,重新协调 Begin Resume、Prepare Resume 等阶段。目标或源端所需内存已经丢失时,这条恢复路径也无法凭空重建状态。恢复入口

源码中的 Confirm 会识别失败的 postcopy,保留迁移状态而跳过普通源端恢复;目标 Finish 的失败分支同样区分清理目标与保留失败的 postcopy。完成交接后,把旧端手工 cont 等同于回滚,会丢弃目标已执行的变化并带来存储一致性风险。目标错误分支

六、迁移阶段记录与结果传递的源码

managed direct 的主线位于 virDomainMigrateVersion3Full()。它先调用 Begin,再用返回的 XML 与 cookie 调用 Prepare,将目标地址传给 Perform,最后依次调用 Finish 和 Confirm。Prepare 失败时,若 Begin 已建立变更保护,也要调用取消性质的 Confirm 结束源端作业。Perform 失败则通知 Finish 清理目标,保留最初错误以免被后续清理覆盖。客户端五阶段控制

结构或变量记录什么解决的问题
vm->job 的 asyncJob 与 phase迁入或迁出作业,以及 Begin、Perform 完成等阶段约束并发操作,断连后按最后阶段选择处理路径
qemuMigrationCookie身份、锁、能力、网络、块迁移及统计等可选信息在阶段之间交换管理元数据;RAM 与寄存器仍由 QEMU 状态流传输
cancelled、retcode 与目标域句柄本阶段结果及后续清理意图将 Perform 结果交给 Finish,再将 Finish 结果交给 Confirm

cookie 由调用方转交,QEMU 驱动解析所需字段。它记录两端已交换的信息;前面 Finish 响应丢失的例子仍需要额外核对实际运行状态与锁。qemuMigrationSrcPerformPeer2Peer3() 在源端守护进程内执行协调逻辑,使阶段推进不再依赖客户端持续在线。cookie 结构、p2p 入口

执行切换集中在两处。qemuMigrationDstFinishFresh() 等待迁入完成、刷新设备状态,再调用 qemuProcessStartCPUs();qemuMigrationSrcConfirmPhase() 成功时调用源端完成清理,失败时检查 postcopy 状态,再决定是否恢复原运行状态。这两处把“加载成功”与“允许谁继续执行”联系起来。目标放行 CPU、源端确认与恢复

七、从阶段记录判断成功与恢复边界

测试时至少保存同一 UUID 的双端状态、迁移作业统计与事件时间。下面的只读命令在具备 A、B 访问权限的管理机上执行,连接 URI 明确选择被查询的宿主;示例假定两端域名均为 demo。活动作业查询和已完成统计应结合双端运行状态解读:

virsh -c qemu+ssh://host-a/system domstate demo --reason
virsh -c qemu+ssh://host-b/system domstate demo --reason
virsh -c qemu+ssh://host-a/system domjobinfo demo
virsh -c qemu+ssh://host-a/system domjobinfo demo --completed --keep-completed
virsh -c qemu+ssh://host-b/system domjobinfo demo --completed --keep-completed

domjobinfo 查询不到作业时,可能是任务尚未建立、已经结束或统计已被消费;--completed 查询保留的完成信息,--keep-completed 避免本次读取后删除统计。默认只显示活动与成功完成的作业,检查失败任务时还可加 --anystats。应把结果与同一 UUID、迁移阶段和时间对应,查询失败本身不足以判定迁移成功或目标已停止。domjobinfo 查询语义

成功路径应能串起“源传输结束 → 目标恢复 → 源进程清理”;保留源持久定义时,A 上的域可以继续显示为关闭。业务探测还需确认网络与应用恢复,迁移作业完成时间、客户机停止执行时间和业务不可用时间要分别记录。

机制演变的重点是扩大可确认的边界:早期较少阶段的协议更早放弃源实例,版本 3 增加 Begin 与 Confirm,把配置检查和最终清理分开;可扩展参数 API 又让新能力在原有阶段中协商;p2p 将协调责任移入守护进程。它们减少特定失败窗口,也增加阶段状态和恢复分支。可靠性应以“在哪个阶段失败,谁还持有可用状态,如何排除双端执行”来评价。

八、参考资料