virt-report虚拟化社区动态

QEMU 预拷贝迁移的内存迭代与停机控制

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

一、预拷贝迁移与停机控制

预拷贝迁移在虚拟机运行期间复制内存,最后暂停虚拟机,传输剩余脏页及处理器、设备状态。复制期间客户机仍会写入内存,已经传过的页可能需要重传。QEMU 通过脏页跟踪记录这些修改,并根据剩余数据量和传输速率决定切换时机。本文分析这两个过程及其对停机时间的影响。

以 8 GiB 内存、1000 MiB/s 的有效传输速率为例,停机后再复制全部内存,仅数据传输就约需 8.2 秒。预拷贝先在运行期间完成大部分复制,再把停机阶段的剩余量压到可接受范围。下面先看两端如何交接,再跟踪一页被重新写脏后的重传,最后讨论何时停止迭代,以及停机超出目标时应检查什么。

以下分析以 x86 / KVM、普通 RAM 的预拷贝路径为例,假定两端机器配置兼容、存储已就绪,不涉及磁盘复制、VFIO 或机密虚拟机。

源码版本:QEMU 10.2.0、Linux 6.18。

二、运行中复制与停机交接

暂停虚拟机后复制全部内存,可以在停止修改的条件下传送一致状态,停机时间也会包含整个内存传输过程。预拷贝将大部分复制工作移到运行阶段:先建立目的端内存副本,再重复传输发生修改的页,最后暂停客户机并完成状态交接。重复传输增加了流量,同时减少了暂停期间需要处理的数据量。

预拷贝迁移与最终状态交接
运行期间传送内存;暂停后同步余页与处理器、设备状态,目的端完成恢复后继续执行。
运行期间传送内存;暂停后同步余页与处理器、设备状态,目的端完成恢复后继续执行。 打开原图
Mermaid 源码
sequenceDiagram
    accTitle: QEMU 预拷贝迁移的主要时序
    accDescr: 先发送流头及 RAM 布局,再在源端运行时迭代传页;暂停后传最后脏页和 CPU、设备状态。目标端加载完成后才启动。
    participant S as 源端 QEMU
    participant K as 源端 KVM
    participant D as 目标端 QEMU
    Note over S,D: 目标虚机已创建;目标 vCPU 尚未执行
    S->>D: Magic + version + 可选配置校验信息
    S->>K: 启用脏页记录
    S->>D: SECTION_START:RAM 布局等
    loop 源端客户机继续运行
        S->>D: SECTION_PART:待发内存页
        K-->>S: 同步执行期间的脏页记录
        Note over S: 继续发送并评估剩余量
    end
    rect rgb(248, 239, 221)
        Note over S,D: 停机窗口:两端客户机均未执行
        S->>K: 停止源端 vCPU
        S->>S: 静止相关设备 I/O
        K-->>S: 最终脏页同步
        S->>D: SECTION_END:剩余 RAM
        K-->>S: 同步 vCPU 执行状态
        S->>D: SECTION_FULL:vCPU 与设备状态
        S->>D: EOF + 可选 VMDESCRIPTION
        D->>D: 完成装载、状态同步及设备恢复
    end
    D->>D: 允许启动后,恢复目标客户机
    Note over S,D: 源端不能同时恢复运行

Libvirt 协调目的端准备和迁移任务;QEMU 保存、传送和恢复状态;KVM 提供客户机执行及脏页记录能力。目的端 QEMU 边接收边填充内存,但客户机尚未运行。运行阶段得到的副本不要求任意时刻都一致,最终一致性依靠停止修改、最后一次脏页同步和状态交接建立。

暂停后不仅要补齐 RAM,还要保存寄存器、中断控制器、设备队列处理位置等状态。普通预拷贝路径先完成 RAM 等可迭代状态,再同步 vCPU 并保存非迭代状态;目的端恢复完成且允许启动后才执行。字节流的组织方式另见《QEMU 迁移流格式与状态恢复》。

三、脏页位图与页面重传

1. 迁移位图与脏页记录

源端 QEMU 在自己的进程内为各个 RAMBlock维护迁移位图 bmap,表示哪些页还需要发送。初始化时将相应位设为 1,使首次扫描覆盖需要迁移的内存;与此同时开启脏页跟踪,记录运行期间的新修改。初始化代码还会排除符合条件的丢弃页,缩小首次扫描的待传集合。

源端宿主内核中的 KVM 通过 dirty bitmap 或 dirty ring 向 QEMU 提供客户机写脏记录;QEMU 内存子系统汇总记录,再由 migration_bitmap_sync()合入自己的迁移位图。原来为 0、此次变为 1 的页增加待传数量,已经为 1 的页不会重复计数。同一页在两次发送之间反复写入,合入位图后仍只需安排一次重传。目的端接收页面内容并更新本机 RAM,源端这份待发位图只用于安排传输。位图合并、同步入口

对象与关键字段保存的内容主要操作
RAMBlock.idstr / host / used_length块名、源端宿主起始地址与有效长度按块内偏移定位页面;流中用块名匹配目的端内存
RAMBlock.bmap按目标页索引的待发送集合,1 表示需要发送初始化置位、脏页同步重新置位、发送前清位
RAMBlock.clear_bmap延迟清除底层脏日志所用的块标记,一个比特可覆盖多页同步后标记需要清除的块,在读页前恢复其写脏跟踪
PageSearchStatus.block / page / complete_round扫描所在内存块、页索引与是否完成回绕限速返回时保存进度,下次迭代继续扫描
页编号             P0 P1 P2 P3 P4 P5 P6 P7
当前 bmap           0  0  1  0  0  0  0  0
本次同步的脏页       0  1  1  0  0  0  0  0
合并后的 bmap       0  1  1  0  0  0  0  0   # P2 已待传,只新增 P1
处理 P1 并准备发送   0  0  1  0  0  0  0  0   # 先恢复跟踪,再清待传位并读页
P1 再次被写入       后续脏页同步将 P1 重新标成 1

bmap记录发送调度需求,不代表目的端已确认收到;网络中的数据还可能尚未装载。块内第 i 个目标页的内存地址为 host + i × 目标页大小。它与 RAMBlock.offset在 QEMU 全局 RAM 地址空间中的位置也应区分。RAMBlock 字段、扫描状态

2. 页面重传与跟踪顺序

继续看上面的 P1:待传位清零后,页面经过发送、接收和装载,目的端得到一份副本。客户机再次修改 P1,底层脏页跟踪便记录这次写入;修改也可能发生在上一份副本尚在传输时。下次同步把 P1 重新标为 1,扫描再次选中它,新的页内容随后覆盖目的端旧副本。同一页由“待传 → 已安排发送 → 再次待传”反复切换,直到最终同步完成。

migration_bitmap_clear_dirty()在发送前处理底层脏标记的清除,再清除迁移位图中的该页。相关页的跟踪必须在读取、发送之前重新有效:若发送之后才清标记,两者之间的新写入就可能被一并清除。手动 dirty-log 模式允许按块推迟清除操作,前提是仍在读页之前恢复跟踪。清除顺序、页面发送

运行阶段的页复制与客户机写入可能交错,因此单次复制不保证得到整页的原子快照。期间发生修改的页会再次同步。最后暂停客户机并静止相关 I/O 后,ram_save_complete()再次汇总脏页并发送剩余内容,形成恢复所需的最终内存。用户态设备或 DMA 的写入还需要对应的脏页记录路径,KVM 脏页日志的覆盖范围取决于写入来源。

3. 扫描轮次与发送批次

ram_save_iterate()从上次扫描位置继续寻找待传页;达到限速或时间预算时返回,下次调用再继续发送。外层为一次调用写出一个 SECTION_PART,一次完整扫描可以跨越多个 PART。扫描位置、位图同步次数和 PART 数分别描述扫描进度、记录收集与传输批次。扫描游标、迭代回调

四、带宽与写脏速率决定收敛

用一个简化的轮次模型可以说明重传的代价。设内存量为 M,第 n 轮待传量为 Rn,有效内存传输速率为 B,按脏页粒度去重后的新增数据速率为 D。在速率稳定、没有压缩和其他开销的近似下:

本轮耗时      Tₙ ≈ Rₙ / B
下一轮待传量  Rₙ₊₁ ≈ min(M, D × Tₙ)
                    = min(M, Rₙ × D / B)

例如 M 为 8 GiB、B 为 1000 MiB/s、D 为 256 MiB/s,首轮用约 8.2 秒,产生约 2 GiB 待重传页;第二轮用约 2.1 秒,又留下约 537 MiB。各轮剩余量约为 8192 → 2097 → 537 → 137 → 35 MiB,下方演示采用这组初始参数。D/B 越接近 1,每轮减少得越少;达到或超过 1 时,这个模型就不能靠继续迭代收敛。实际负载的热页集合、突发写入、扫描顺序和带宽变化,会使曲线偏离几何级数。

D 按脏页粒度统计:修改一页中的少量字节,也可能需要重传整页;同一统计周期内反复修改同一页会被合并。B 使用当前路径处理内存的有效速率,与网卡标称带宽可能有明显差异。压缩或零页编码启用后,还需分别统计逻辑内存字节数与链路字节数。

内存迭代与停机时间

调整传输带宽、脏页速率或停机目标,观察剩余内存何时达到切换条件,以及减少迭代会怎样改变停机阶段的工作量。

  1. 首次复制8192 MiB(8 GiB)内存,以 1000 MiB/s 传输需 8.192 秒;期间以 256 MiB/s 产生约 2097.152 MiB 待重传脏页。
  2. 迭代重传继续重传,待传量约按 2097.152 → 536.871 → 137.439 → 35.184 MiB 下降;完成 4 轮运行中复制后,低于 100 MiB 的切换阈值。
  3. 暂停并完成最终内存传输约需 35.184 毫秒,加上示例中的状态保存传输与恢复共 40 毫秒,模型停机时间约为 75.184 毫秒。

模型先完成首次复制,再按完整轮次评估切换:本轮耗时=待传量 ÷ 有效带宽;下一轮脏页量=min(初始内存,脏页速率 × 本轮耗时)。脏页速率按页面去重,同轮重复写入同一页只计一页。模型的一轮扫描可对应 QEMU 中多个 SECTION_PART。计算使用固定速率,不含压缩与并行通道;播放速度独立于模拟用时,估算结果仅适用于上述模型,QMP downtime 与业务中断时间需实际测量。

可以先保持内存量不变,只提高写脏速率,观察曲线从快速下降变为缓慢下降;再提高带宽,比较相同停机目标下需要的轮次。最后降低停机目标:阈值变小,通常需要多迭代,但链路和设备处理能力没有因此改变。这三组操作分别对应写入压力、传输能力和切换策略。

增加迭代通常能减少暂停后的剩余工作,但也会延长迁移、消耗网络与主机资源。QEMU 根据预计暂停期间的工作量判断切换时机,剩余量达到阈值后即可进入完成阶段,无需等待脏页归零。

五、从剩余量估算停机时间

1. 从时间目标换算剩余量阈值

migration_update_counters()使用近期观测带宽,或配置的 avail-switchover-bandwidth,计算切换阈值。固定版本中的关键表达式为:

s->threshold_size = expected_bw_per_ms * migrate_downtime_limit();

随后 migration_iteration_run()先读取各状态处理器报告的剩余量估计;当它低于阈值时,再请求更精确的剩余量。对 RAM 来说,后一步会重新同步脏页,避免只看已经过时的位图。最终剩余量不超过阈值,并且切换条件满足,才进入完成阶段。启用 switchover-ack时,还需目的端确认。阈值、判断逻辑、精确剩余量

2. 停机目标与实际停机时间

剩余内存除以带宽,只解释暂停阶段的一部分。实际还要停止 vCPU 和相关 I/O、汇总最终脏页、提取与序列化 CPU 和设备状态、完成传输,并在目的端完成装载、存储激活和恢复执行。其中部分发送与接收可以重叠,不能简单把所有函数耗时相加。设备状态、带宽突变、线程调度及同步等待都可能让实际停顿超过目标。

参数或指标含义
downtime-limit用于切换决策的时间目标,单位为毫秒;实际停机还受设备与传输收尾影响。
expected-downtime迁移活动期间对未来停机的估计,随脏页同步和带宽估计更新。
downtime成功完成后报告的停机统计;需结合源端计时点与启用的协议理解。
total-time整个迁移过程的耗时,包含运行期间的迭代,不能与停机时间混用。

在本文的预拷贝实现中,源端在 migration_stop_vm()调用停止操作前开始计时,在源端完成处理时结束;是否等待返回通道也影响这个区间。因此 QMP 的 downtime不是一次直接测量“源端最后一条客户机指令到目的端第一条指令”的跨主机计时。业务侧还会受到网络切换、连接重试和应用调度影响,应另行测量请求中断与尾延迟。计时开始、完成流程、计时结束

六、源码:迭代与最终状态保存

迁移线程负责判断是否继续迭代,RAM 处理器负责维护待传集合,两者通过状态保存回调连接。初始化、迭代和最终保存的主要调用关系如下:

migration_thread
  qemu_savevm_state_setup
    ram_save_setup → ram_init_bitmaps
  migration_iteration_run
    pending_estimate / pending_exact → 必要时同步脏页
    qemu_savevm_state_iterate
      ram_save_iterate → ram_find_and_save_block
        ram_save_host_page → ram_save_target_page
  migration_completion_precopy
    migration_stop_vm
    qemu_savevm_state_complete_precopy
      ram_save_complete → 最终脏页同步、发送余页
      complete_precopy_non_iterable → 同步 vCPU、保存设备

这些回调连续推进同一份迁移状态:setup 建立待传位图;iterate 消费它,并保留当前 RAMBlock 与扫描位置;因限速返回后,下一次 iterate 从保存的位置继续。进入 complete 时,源端已经停止修改客户机状态,最后一次同步把 P1 等尚有变化的页补入集合,再发送完毕。回调返回只是把控制权交回迁移框架,位图和扫描进度仍由迁移对象保存。

savevm_ram_handlers注册了初始化、迭代、完成和装载回调。暂停阶段的最终保存流程先完成可迭代处理器,再通过 cpu_synchronize_all_states()读取所需的 CPU 状态,并保存各非迭代处理器。CPU 与具体设备的保存次序由处理器注册顺序决定。

在源宿主机上,可对正在迁出的 Libvirt 测试域执行只读查询;将 demo 替换为实际域名:

virsh -c qemu:///system qemu-monitor-command demo --pretty '{"execute":"query-migrate"}'

返回的 ram.remaining 是剩余字节数;同一 ram 对象中的 dirty-pages-rate、dirty-sync-count、pages-per-second 分别反映写脏速率、同步次数和页传输速率。页速率须结合 page-size 换算,字段是否出现取决于迁移阶段。按固定间隔保存多次查询,才能判断剩余量的变化;一次查询仅提供当前快照。RAM 统计字段、查询接口

需要进一步区分“剩余量降不下来”和“暂停后的处理太慢”时,可结合 migration_bitmap_sync_end、migrate_pending_exact 和 vmstate_downtime_checkpoint 等 trace 事件。前者对应运行中的收敛过程,后者还需要观察停机后的设备保存和传输收尾。

七、并行、限速与后拷贝的取舍

预拷贝的主要开销来自内存传输、脏页跟踪与重传,以及暂停阶段的状态保存和恢复。不同优化机制针对其中的不同环节,也各自需要额外的计算、内存或同步资源。

1. 传输能力不足:并行化

当单一路径的页面处理或传输成为瓶颈时,MultiFD 使用多个工作线程和通道处理内存;QAPI 的 multifd能力自 QEMU 4.0 提供。它有机会提高有效 B,但会增加线程、缓冲区和同步开销;若已受网络总带宽或内存带宽限制,增加通道不会按比例提速。MultiFD 实现、能力版本

多通道的任务分发和同步过程见《QEMU MultiFD 的任务分发与跨通道同步》。

2. 写脏速度过高:从统一限速到按 vCPU 限制

QEMU 1.6 的 auto-converge通过限制客户机 CPU 执行降低写脏速度。这会降低迁移期间的业务性能,也会影响写内存较少的 vCPU。QEMU 8.1 的 dirty-limit利用 KVM dirty ring 观察各 vCPU 的写脏活动,对超过配额的 vCPU 施加等待。它依赖相应 KVM 能力,并以受限 vCPU 的执行时间换取较低的写脏速率;设备写入仍需由相应路径处理。自动收敛、Dirty limit 设计

3. 跟踪本身有开销:缩短清除与读页的间隔

获取脏页、清除日志与重新写保护本身也有成本。手动 dirty-log 保护允许更接近读页时清除相应范围,减少过早重新保护带来的重复工作;仍须满足本篇介绍的“先恢复跟踪,再读取页面”顺序。dirty ring 进一步改变记录的收集方式,其回收、满环与内存顺序集中在脏页日志专篇。KVM 手动保护的设计说明

4. 减少流量与控制收尾成本

零页识别避免发送整页零值;XBZRLE 在适合的重传场景中编码与已缓存页面的差异;压缩以 CPU 工作换取链路字节减少。实际收益取决于页面内容、缓存命中和处理能力。内存剩余量已经很小而停机仍长时,需要进一步分析设备保存、目的端装载和 I/O 交接的耗时。放宽停机目标可以更早切换,也会把更多工作留在暂停阶段。

后拷贝允许目的端在内存尚未传完时开始执行,剩余页面由后台传输,并在访问缺失页时优先请求。它减少了预拷贝阶段的重复传输,同时引入远程缺页延迟和两端状态依赖。预拷贝与后拷贝的选择还需结合业务延迟要求、网络稳定性和故障恢复条件。后拷贝设计

缺页请求、页面安装与恢复执行的过程见《QEMU 后拷贝迁移的缺页处理与页面传输》。

八、参考资料