深入 QEMU 热迁移:从状态机到数据平面的全链路剖析
“把一台正在运行的虚拟机从一台主机搬到另一台,还让里面的操作系统浑然不觉——这听起来像魔法,实则是精密的工程。”
引言
实时迁移是 QEMU 最核心的子系统之一。它允许将一个正在运行的虚拟机透明地从源主机迁移到目的主机,停机时间通常控制在几百毫秒以内。整个迁移框架在客户机继续执行的同时,传输完整的 VM 状态——RAM、设备寄存器、CPU 状态等——宛如在飞驰的列车上更换引擎。
本文基于 QEMU 源码中 migration/ 目录的约 30 个源文件,从架构、状态机、数据平面、压缩与加速四个维度,对热迁移系统进行全链路剖析。
一、架构概览:源端与目的端的职责分离
QEMU 的迁移子系统围绕源端(出站迁移)和目的端(入站迁移)之间清晰的职责分离来构建,两者通过一种与传输方式无关的字节流进行连接。
┌─────────────────── Source Host ───────────────────┐ ┌─────────────────── Destination Host ──────────────┐ │ │ │ │ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │ │ MigrationState │ ┌─────────┐ QEMUFile │ │ QEMUFile ┌─────────┐ │ MigrationIncoming│ │ │ │ (migration.c/h) │──│ QIOCh. │════════════════│─────│════════════════│ QIOCh. │──│ State │ │ │ │ │ └─────────┘ │ │ └─────────┘ │ (migration.c/h) │ │ │ │ - to_dst_file │ │ │ │ - from_src_file │ │ │ │ - rp_state │ ←── Return Path ──────────│─────│────────── Return Path ────→ │ - to_src_file │ │ │ └──────────────────┘ │ │ │ - fault_thread │ │ │ │ │ └──────────────────┘ │ └────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘关键数据结构:
- MigrationState:源端核心状态机,持有
to_dst_file(到目的端的文件流)、返回路径线程状态、迁移参数、能力位图、COLO 状态等。 - MigrationIncomingState:目的端核心状态,持有
from_src_file、userfault fd、page request 树、后拷贝信号量等。
两者之间的通信通过QEMUFile抽象进行,底层由QIOChannel提供传输无关的字节流,支持 TCP socket、UNIX socket、exec、RDMA 等多种后端。
二、迁移状态机:精密编排的状态流转
每个迁移实例都经历一系列定义明确的状态推进,通过 QMP 暴露的MigrationStatus枚举进行跟踪。理解这个状态机是理解整个迁移流程的钥匙。
┌──────────┐ │ none │ └────┬─────┘ │ migrate QMP command ┌────▼─────┐ │ setup │ ←── 初始化通道、multifd 线程 └────┬─────┘ │ ┌────▼─────┐ ┌─────│ active │──────┐ │ └────┬─────┘ │ │ │ │ converging postcopy pause-before │ trigger switchover │ │ │ ┌─────▼──┐ ┌────▼──────┐ ┌───▼───────────┐ │wait- │ │postcopy- │ │pause-before- │ │device │ │active │ │switchover │ └───┬────┘ └────┬──────┘ └───┬───────────┘ │ │ │ ▼ ▼ ▼ ┌───────────────────────────────┐ │ completed │ └───────────────────────────────┘ 任何状态 ──→ failing ──→ failed postcopy-active ──→ postcopy-pause ──→ postcopy-recover状态转换的核心逻辑在 migration.c 中定义,migrate_set_state()负责原子地更新状态并触发 QMP 事件通知。
三、预拷贝迁移生命周期
默认的迁移模式是预拷贝(precopy),即在客户机继续运行的同时传输大部分内存。
3.1 主循环:迭代传输脏页
源端迁移线程的入口是 migration_thread(),其核心循环如下:
// 简化后的主循环逻辑while(migration_is_active()){if(urgent||!migration_rate_exceeded(s->to_dst_file)){MigIterateState iter_state=migration_iteration_run(s);// 迭代传输脏页...}thr_error=migration_detect_error(s);// 检测网络故障等urgent=migration_rate_limit();// 速率限制与紧迫页处理}每一轮迭代:
- 扫描脏位图,找出自上次传输以来被修改的内存页
- 传输脏页到目的端
- 检查收敛条件:剩余脏页是否低于
threshold_size(由期望停机时间和实测带宽计算得出) - 若收敛,进入 switchover 阶段;否则继续迭代
3.2 脏页追踪
脏页追踪是预拷贝迁移的基石。QEMU 通过DirtyMemoryBlocksRCU 方案实现高效的脏位图扫描:
- 迁移线程持有 RCU 读锁(而非更重的 ramlist 互斥锁)扫描位图
- RAM 热插拔操作可以并发扩展位图
- 每个 RAMBlock 嵌入了
bmap、receivedmap、clear_bmap、file_bmap等迁移专用位图
3.3 CPU 节流:自动收敛
当脏页产生速度超过传输速度时,QEMU 会启动自动收敛机制——通过 cpu-throttle.c 降低 vCPU 执行频率,迫使客户机产生脏页的速度下降,直至迁移收敛。最大节流比例由max-cpu-throttle参数控制(默认 99%)。
四、后拷贝迁移:用缺页中断换停机时间
后拷贝是一种激进的迁移策略:先快速传输 CPU 状态和非 RAM 设备状态,让 VM 在目的端尽早启动,然后按需拉取内存页。
4.1 核心机制:userfaultfd
后拷贝的核心依赖是 Linux 的userfaultfd系统调用。目的端 QEMU 在迁移开始前:
- 为所有 RAMBlock 注册 userfaultfd 区域
- 启动 fault_thread 监听缺页事件
- 当 VM 在目的端访问尚未传输的页面时,内核暂停该 vCPU 并通知 fault_thread
- fault_thread 通过返回路径向源端发送
MIG_RP_MSG_REQ_PAGES请求 - 源端的 source_return_path_thread() 收到请求后,优先传输被请求的页面
// 目的端请求页面的核心逻辑(简化)intmigrate_send_rp_req_pages(MigrationIncomingState*mis,RAMBlock*rb,ram_addr_tstart,...){WITH_QEMU_LOCK_GUARD(&mis->page_request_mutex){if(!ramblock_recv_bitmap_test_byte_offset(rb,start)){g_tree_insert(mis->page_requested,aligned,(gpointer)1);qatomic_inc(&mis->page_requested_count);}}// 向源端发送 REQ_PAGES 消息...}4.2 后拷贝抢占
后拷贝面临一个经典问题:当源端正在传输后台批量页面时,目的端突然发生紧急缺页,请求的页面可能被排在长队列后面。
解决方案是后拷贝抢占——建立一条独立的紧急通道(postcopy_qemufile_dst),由专门的 postcopy_prio_thread 处理。紧急页面通过这条通道优先传输,避免被批量数据阻塞。
4.3 页面请求的空间局部性优化
QEMU 还利用了空间局部性原理,通过 PageLocationHint 结构,在后拷贝抢占模式下,源端在发送紧急请求页之后,会顺便发送相邻的页面,减少未来的缺页中断次数。
五、Multifd:并行传输引擎
单通道传输在高带宽网络上是瓶颈。Multifd 通过创建 N 条并行通道来充分利用带宽。
5.1 架构
定义在 multifd.h,multifd 在源端创建 N 个发送线程(mig/src/send_%d),在目的端创建 N 个接收线程(mig/dst/recv_%d),通道数由x-multifd-channels参数控制(默认 2)。
Source Destination ┌──────────────┐ ┌──────────────┐ │ Main Thread │═══════════════════│ Main Thread │ ├──────────────┤ ├──────────────┤ │ send_0 │═══════════════════│ recv_0 │ │ send_1 │═══════════════════│ recv_1 │ │ ... │ │ ... │ │ send_N-1 │═══════════════════│ recv_N-1 │ └──────────────┘ └──────────────┘每个 multifd 包的结构定义在 MultiFDPacket_t:
typedefstruct{MultiFDPacketHdr_t hdr;// magic + version + flagsuint32_tpages_alloc;// 最大分配页数uint32_tnormal_pages;// 非零页数uint32_tnext_packet_size;// 数据负载大小uint64_tpacket_num;// 包序号uint32_tzero_pages;// 零页数charramblock[256];// 所属 RAMBlock 名称uint64_toffset[];// 每页的偏移量}MultiFDPacket_t;5.2 可插拔压缩后端
Multifd 的压缩后端是完全可插拔的,每个后端实现为独立文件:
| 后端 | 文件 | 说明 |
|---|---|---|
| 无压缩 | multifd-nocomp.c | 基线,直接传输原始页 |
| zlib | multifd-zlib.c | 通用压缩 |
| zstd | multifd-zstd.c | 高压缩比 |
| Intel QPL | multifd-qpl.c | Intel 硬件加速 |
| Huawei UADK | multifd-uadk.c | 华为硬件加速 |
| Intel QATzip | multifd-qatzip.c | Intel QAT 压缩卡 |
压缩标志通过MULTIFD_FLAG_COMPRESSION_MASK位域标识,接收端根据 flags 分派到对应的解压函数。
六、返回路径:双向通信的生命线
迁移并非单向数据流。目的端需要向源端发送控制消息,例如请求特定页面、确认切换、报告接收位图等。这就是返回路径(Return Path)。
源端的 source_return_path_thread() 专门处理来自目的端的消息,消息类型定义在 mig_rp_message_type:
| 消息类型 | 方向 | 用途 |
|---|---|---|
MIG_RP_MSG_SHUT | DST→SRC | 目的端关闭,不再发送 RP 消息 |
MIG_RP_MSG_PONG | DST→SRC | 响应 PING,确认通道存活 |
MIG_RP_MSG_REQ_PAGES | DST→SRC | 后拷贝期间请求特定页面 |
MIG_RP_MSG_REQ_PAGES_ID | DST→SRC | 带 RAMBlock ID 的页面请求 |
MIG_RP_MSG_RECV_BITMAP | DST→SRC | 回传接收位图 |
MIG_RP_MSG_RESUME_ACK | DST→SRC | 确认已准备好恢复 |
MIG_RP_MSG_SWITCHOVER_ACK | DST→SRC | 确认可以执行切换 |
返回路径的建立通过 open_return_path_on_source() 完成,它从to_dst_file的底层 QIOChannel 获取反向文件句柄,并启动专门的线程。
七、设备状态序列化:VMState 框架
迁移不仅是搬运内存页——设备的内部状态同样需要完整传输。QEMU 通过VMState框架实现设备状态的声明式序列化。
每个可迁移设备通过SaveVMHandlers注册一组回调:
| 回调 | 用途 |
|---|---|
save_setup/load_setup | 初始化迁移传输 |
save_live_iterate | 预拷贝迭代中传输可迭代状态 |
save_live_complete_precopy | 预拷贝最终阶段传输剩余数据 |
has_postcopy | 指示是否支持后拷贝模式 |
save_postcopy_prepare | 为向后拷贝转换做准备 |
load_state | 接收并应用状态数据 |
save_cleanup/load_cleanup | 迁移完成后释放资源 |
VirtIO 设备的迁移尤为复杂,通过多子节 VMState 描述处理(virtio.c 附近的vmstate_virtio),仅当设备状态与默认值不同时才序列化,减少传输量。
八、专用传输与高级特性
8.1 RDMA 迁移
rdma.c 实现了基于 RDMA 的迁移通道,利用远程直接内存访问绕过内核协议栈,在高带宽低延迟网络(如 InfiniBand)上大幅提升传输性能。源端通过 RDMA WRITE 操作直接将页面写入目的端内存,无需目的端 CPU 参与。
8.2 COLO:连续带外活体复制
colo.c 实现了 COLO(COntinuous LOck-step)模式——主备 VM 同步运行,通过比较输出实现近乎零停机的容错。当检测到输出不一致时回滚到上一检查点,适合对可用性要求极高的场景。
8.3 CPR:进程检查点与重启
cpr.c 和 cpr-exec.c 实现了 CPR(CheckPoint and Restart)迁移模式,允许将 VM 状态保存后在同一主机的新 QEMU 进程中恢复,用于 QEMU 升级等场景。
8.4 XBZRLE:增量编码缓存
xbzrle.c 实现了基于 XOR 的增量编码。源端维护一个页面缓存,每次传输时只发送当前页与缓存版本的差异,对于频繁修改相同页面的工作负载(如数据库)效果显著。
九、关键配置参数速查
| 参数 | 默认值 | 说明 |
|---|---|---|
downtime-limit | 300 ms | 最大允许停机时间 |
max-bandwidth | 1 GiB/s | 迁移最大带宽 |
x-multifd-channels | 2 | 并行 multifd 通道数 |
x-multifd-page-count | 16 | 每个 multifd 包的页数 |
x-xbzrle-cache-size | 64 MiB | XBZRLE 增量缓存大小 |
max-cpu-throttle | 99% | 最大 CPU 节流比例 |
十、源码导航地图
migration/ ├── Core State Machine │ ├── migration.c/h — 状态机、主循环、QMP 命令实现 │ ├── options.c/h — 迁移参数解析与校验 │ └── global_state.c — 全局 VM 状态序列化 ├── Transport Layer │ ├── channel.c/h — QIOChannel 迁移通道抽象 │ ├── socket.c/h — TCP/UNIX socket 传输 │ ├── fd.c/h — 文件描述符传输 │ ├── file.c/h — 文件传输 │ ├── exec.c/h — exec 传输 │ ├── rdma.c/h — RDMA 传输 │ └── tls.c/h — TLS 加密传输 ├── RAM Migration │ ├── ram.c/h — RAM 脏页追踪、迭代传输 │ ├── xbzrle.c/h — XBZRLE 增量编码 │ ├── page_cache.c/h — 页面缓存(XBZRLE 用) │ └── dirtyrate.c/h — 脏页速率统计 ├── Multifd Parallel Engine │ ├── multifd.c/h — 多通道并行框架 │ ├── multifd-nocomp.c — 无压缩基线 │ ├── multifd-zlib.c — zlib 压缩 │ ├── multifd-zstd.c — zstd 压缩 │ ├── multifd-qpl.c — Intel QPL 硬件加速 │ ├── multifd-uadk.c — 华为 UADK 加速 │ ├── multifd-qatzip.c — Intel QAT 压缩卡 │ └── multifd-zero-page.c — 零页优化 ├── Postcopy │ ├── postcopy-ram.c/h — 后拷贝 RAM 处理、userfaultfd ├── COLO │ ├── colo.c — COLO 主逻辑 │ ├── colo-failover.c — COLO 故障切换 │ └── multifd-colo.c/h — COLO multifd 集成 ├── CPR │ ├── cpr.c — CPR 公共逻辑 │ ├── cpr-exec.c — CPR exec 模式 │ └── cpr-transfer.c — CPR transfer 模式 ├── Device State │ ├── savevm.c/h — VMState 保存/恢复入口 │ ├── vmstate.c — VMState 描述符处理 │ ├── vmstate-types.c — 常用类型序列化 │ ├── block-active.c — 块设备活跃状态 │ ├── block-dirty-bitmap.c — 块设备脏位图 │ └── vfio.c / vfio-stub.c — VFIO 设备状态迁移 └── Infrastructure ├── qemu-file.c/h — 迁移字节流抽象 ├── migration-stats.c/h — 迁移统计信息 ├── cpu-throttle.c — CPU 节流 └── yank_functions.c — 网络中断安全清理结语
QEMU 的热迁移系统是一个精巧的分布式状态机——它要在源端和目的端之间协调数十 GB 的内存传输、数千个设备的状态序列化、亚秒级的停机时间控制,还要优雅地处理网络中断和硬件故障。理解它的关键在于:
- 状态机是骨架——所有行为都受
MigrationStatus枚举驱动 - 脏页追踪是心脏——决定了什么数据需要传输
- Multifd 是肌肉——提供并行传输的吞吐量
- 后拷贝是利刃——用缺页中断换取极致的停机时间
- VMState 是灵魂——确保设备状态完整一致