后复制迁移

后复制用于处理无法收敛,或收敛过慢的迁移。 优点是迁移流量和耗时存在上界;缺点是在后复制阶段,任意一端故障都会导致客户机丢失。

目标端在内存尚未全部传完时启动 CPU。 访问尚未到达的页面会触发缺页,QEMU 将其转换为向源端请求页面。

后复制可以与普通预复制结合:如果预复制在指定时间内未完成,就切换到后复制。

启用后复制

迁移开始前,在源端和目标端监视器执行:

migrate_set_capability postcopy-ram on

随后按普通方式启动迁移,最初仍采用预复制。执行:

migrate_start_postcopy

即可切换到后复制。可以在迁移刚开始时或之后任意时刻执行,迁移结束后执行也无害。

内部实现

状态机

状态依次为 ADVISE、DISCARD、LISTEN、RUNNING、END,见 postcopy_state。

  • Advise

    只要启用了后复制,迁移开始时就设置,即使尚未发出切换命令。 目标端检查操作系统支持,并准备适用于后复制的 RAM 映射。 缺少系统支持时会尽早失败。 由收到 POSTCOPY_ADVISE 触发。

  • Discard

    收到首个 discard 命令后进入。 执行前通过 madvise 禁用大页,避免后复制阶段生成新大页, 并拆分包含待丢弃页面的大页。

  • Listen

    数据包首个 POSTCOPY_LISTEN 命令切换到该状态,并启动监听线程接管迁移流的页面接收。 主线程继续处理数据包。 此时对 RAM 启用缺页检测,Linux 使用 userfault 机制。

  • Running

    POSTCOPY_RUN 使目标端同步全部状态,启动 CPU 和 I/O 设备。 主线程完成数据包处理后恢复类似普通预复制完成后的行为, 但不能执行正常迁移结束时的清理。

  • End

    监听线程退出并清理迁移状态,迁移完成。

设备传输

加载设备状态可能访问尚未到达的客户机 RAM,触发需要源端解决的缺页。 因此设备加载期间,迁移流必须仍能接收页面。 为此,在加载前必须先完整读取设备数据,释放流供页面使用。 实现方式是把设备数据打包成一个可一次读取的整体。

源端行为

进入后复制前,迁移流与普通预复制相同, 只在开头增加 postcopy advise,通知目标可能切换。 切换时,源端先发送页面丢弃数据,再构造包含以下内容的数据包:

  • postcopy listen 命令。

  • 设备状态:与预复制相同的若干节,但不包含 RAM 等可以后复制的设备。

  • postcopy run 命令。

数据包作为 CMD_PACKAGED 的数据部分发送,内部格式与主迁移流相同。

后复制期间,源端像预复制一样扫描脏页并主动发送。 但收到目标缺页请求后,会从请求位置重新开始扫描, 使请求页及其后续页面尽早到达,期待这些相邻页也很快被访问。

目标端行为

初始与预复制相同,单线程读取迁移流。 postcopy advise 和 discard 改变 RAM 管理方式,但不影响流处理。

------------------------------------------------------------------------------
                        1      2   3     4 5                      6   7
主   -----DISCARD-CMD_PACKAGED ( LISTEN  DEVICE     DEVICE DEVICE RUN )
线程                               |       |
                                   |     (页面请求    )
                                   |        \___
                                   v            \
监听线程:                          --- 页面 -- 页面 -- 页面 -- 页面 -- 页面 --

                                   a   b        c
------------------------------------------------------------------------------
  • 收到 CMD_PACKAGED(1)时:

    将数据包全部读入内存,即图中括号部分。 主线程递归调用 qemu_loadvm_state_main 处理内容(2), 包括命令(3、6)和设备(4 等)。

  • 收到数据包首条 postcopy listen(3)时:

    启动线程 a 接管迁移流,主线程继续加载数据包。 监听线程既接收普通后台页 b,也接收设备加载缺页(5)请求返回的页 c, 使主线程得以继续。

  • 数据包最后的 RUN(6):

    启动目标 CPU。 数据包结束(7)后,主线程恢复正常工作,不再参与迁移; 监听线程继续接收页面,直到结束。

源端页面位图

与预复制基本相同,每一位表示对应页需要发送。 预复制期间,CPU 写脏页面时会更新位图。 后复制期间源 CPU 已停止,不应再产生脏页,发送相应页面后清除位即可。

后复制特性

恢复后复制

后复制的错误处理不同于预复制。 发生网络等错误时,虚拟机数据分布在两端,无法简单地使迁移失败并回退。 两端会进入暂停状态,需要恢复阶段才能继续。

通常按以下流程恢复:

  • 网络故障时,两端进入 POSTCOPY_PAUSED。

  • 网络恢复,或提供新网络后,在目标端发送 QMP migrate-recover,建立新通道。

  • 源端发送带 resume=true 的 migrate, 进入 POSTCOPY_RECOVER_SETUP,尝试重新建立通道。

  • 连接成功后,两端进入 POSTCOPY_RECOVER,通过握手同步虚拟机状态。 例如,在断网瞬间发送、途中丢失的页面必须重新发送。

  • 同步完成后,两端回到 POSTCOPY_ACTIVE,继续迁移。

迁移暂停期间,虚拟机逻辑上仍可运行,访问已到达目标的页面不受影响。 但访问缺失页的线程会等待,可能一直阻塞到恢复完成。

影响也取决于客户机配置。例如启用异步缺页时,客户机可以主动调出等待页面的线程。

大页支持

后复制支持 hugetlbfs 内存,但要求:

  1. 目标 Linux 内核支持大页 userfault。

  2. 两端 RAMBlock 使用相同页大小。

  3. 大页不足时,-mem-path /dev/hugepages 可能回退到普通 RAM,违反上一条件。 使用 -mem-prealloc 强制分配大页。

  4. 谨慎选择大小。2 MB 通常适合;1 GB 可能有问题, 因为通过 10 Gbps 链路传输约需一秒,完整页面到达前目标线程一直阻塞。

共享内存支持

需要 QEMU 以及共享内存的其他进程显式支持, userfault 能处理的共享内存类型也有限制。

Linux 支持 /dev/shm 和 hugetlbfs, 但后者没有等效的 madvise(MADV_DONTNEED),某些配置可能受影响。

QEMU 的 vhost-user 支持具备后复制能力的客户端, tests/ 中的 vhost-user-bridge 及 DPDK 已作相应适配。

客户端需要打开 userfaultfd,为映射区域注册 userfault, 再将描述符和地址映射表交给 QEMU。 映射表用于把客户端缺页地址转换为 RAMBlock 和偏移。 QEMU 将描述符加入后复制缺页线程,代客户端请求页面, 页面到达后执行 wake,使客户端继续。

备注

未来希望改进:
  1. 让 QEMU 无需了解客户端地址空间。

  2. 避免页面到达后仍需 QEMU 调用 ufd-wake。

已有客户端也可以增加支持:
  1. 按上述方式注册 userfault,并与后复制阶段协调。 vhost-user 在现有控制通道增加了消息。

  2. 找出可能因访问客户机内存而阻塞的线程,并理解影响。 例如,持锁访问内存时,其他等待该锁的线程也会阻塞。

抢占模式

QEMU 8.0 引入 postcopy preempt, 将目标明确缺页请求的紧急页面放到独立抢占通道,而非在后台迁移通道排队。 关注缺页延迟的用户应启用此功能,默认未启用。

阻塞时间统计

Blocktime 表示 vCPU 因缺页处于可中断睡眠的时间。 目标端既计算所有 vCPU 阻塞时间的重叠指标,也分别计算各 vCPU 的时间。 在目标监视器启用:

migrate_set_capability postcopy-blocktime on

通过 QMP query-migrate 获取。 postcopy-blocktime 表示所有 vCPU 的重叠阻塞时间, postcopy-vcpu-blocktime 列出各 vCPU 的阻塞时间。