QEMU MultiFD 的任务分发与跨通道同步
一、并行传输带来的顺序问题
MultiFD 将迁移数据分派给多个工作线程,通过独立通道并行发送和接收。当单线程的页面处理速度限制吞吐时,这种拆分可以利用更多 CPU;主流继续负责迁移阶段与同步。并行之后,各通道的到达顺序不再统一,需要在同一页可能再次发送之前建立同步边界,防止旧内容晚到而覆盖新内容。
本文分析普通 socket 迁移中未压缩 RAM 数据的分发与同步。示例采用默认同步方式:在 RAM 扫描轮次边界通过主流的 RAM_SAVE_FLAG_MULTIFD_FLUSH 协调数据通道。旧机器兼容模式、文件迁移及压缩路径在后文分别说明。
源码版本:QEMU 10.2.0。
内存迭代与停机切换见《QEMU 预拷贝迁移的内存迭代与停机控制》;主迁移流的记录格式见《QEMU 迁移流格式与状态恢复》。
二、主流控制与多通道传页
1. 主迁移流与数据通道
主迁移流继续承担流头、配置、RAMBlock 布局、状态分段和同步控制;RAM 页面可以交给额外的 MultiFD 通道。发送工作线程属于源端 QEMU 进程,接收工作线程属于目的端 QEMU 进程,它们与客户机 vCPU 的数量没有一一对应关系。每条通道各有发送线程和接收线程,同一通道内保持字节顺序,不同通道可以同时推进,抵达顺序由各自的处理和传输进度决定。
源端先把同一 RAMBlock 的页偏移收集成批,再寻找空闲发送线程。目的端根据包头中的 RAMBlock 名称与偏移数组,把数据写入对应内存位置。数据批次因而可以在不同通道上独立处理,无须按地址从低到高抵达。
并行通道必须在必要的边界上重新建立顺序。假设页面 P 在上一轮发送了旧内容,下一轮又被写脏并发送新内容:如果两次传输走不同通道,旧内容晚到就可能覆盖新内容。同步点保证上一组传输完成装载之后,接收端才越过相应边界。
通道 0:上一轮 P=v1 ───────→ 装载 v1 → SYNC
通道 1:上一轮其他页面 → SYNC → 等待屏障 → 下一轮 P=v2
主流: FLUSH ─────────────→ 等全部通道到齐 → 放行下一轮
屏障建立的顺序:装载上一轮 v1 → 装载下一轮 v2
2. 两端各有一个完成条件
- 发送端:工作线程完成已有任务,并按要求把 SYNC 包写入各自通道,主线程才能继续发出主流同步标记。
- 接收端:主流读到同步标记后,等待全部数据通道读到 SYNC;各接收线程此前已经处理了该通道上排在 SYNC 前的页面。
发送函数返回只说明本地写入完成,数据仍可能位于 socket 缓冲区或网络中。发送端无需为每个同步点等待一条远端确认消息;跨通道的接收顺序由目的端屏障保证。同步请求定义、接收端屏障
三、RAM 批次的分发与装载
以下示例先传输 16 页,再更新其中 8 页并传输一次。每批用两页展示任务边界。先观察正常进度下多个通道同时工作,再把通道 1 的接收进度调慢:其余通道可以先处理完本轮数据,却要在 SYNC 等待;主流只有等全部通道到齐才放行下一轮。这个等待正是并行传输为保持页面版本顺序付出的代价。
并行页传输与同步屏障
源端分发页面,数据通道并行接收,主迁移流在 FLUSH 处等待全部通道完成。P00—P15 为页编号,v1 / v2 为内容版本。
源端页内容
目的端页内容
- 源端为各通道分派同一 RAMBlock 的页面批次。
- 各发送线程处理完任务后,在本通道写入 SYNC;主线程等本地线程完成后写出主流 FLUSH。
- 目的端数据通道依次装载页面,收到 SYNC 后等待。
- 目的端主流收到 FLUSH,并收齐全部通道的 SYNC,随后释放接收线程。
启用 JavaScript 后,可逐步观察页面版本、通道延迟和两端的同步状态。
每步为逻辑时刻,不代表毫秒或带宽;每批用两页代表实际批次。延迟施加在通道 1 的接收端,通道内仍严格有序。示例只推进两组 RAM 传输,不包含 CPU / 设备状态保存与客户机启动;切换参数会重置过程。
1. 页面批次的线程分配
multifd_queue_page() 收集同一个 RAMBlock 的页偏移。当前批次满了,或扫描进入另一个 RAMBlock 时,它把已有批次交给 multifd_send()。同步前也会提交尚未满的批次。默认批量上限按 MULTIFD_PACKET_SIZE = 512 KiB 计算;采用 4 KiB 页时可容纳 128 个页偏移。入队实现、批量大小
分发函数等待 channels_ready,从上次位置之后循环寻找 pending_job == false 的线程。交接通过交换数据对象指针完成,随后以 release 语义发布任务并唤醒线程。页面并没有在入队时整体复制成一份不可变快照;普通未压缩路径由工作线程准备 iovec 并执行写入,迁移期间的新写入仍由脏页跟踪和后续轮次处理。
| 源端对象 | 关键字段 | 操作与所有权 |
|---|---|---|
MultiFDPages_t | block / offset[] / num / normal_num | 收集同一 RAMBlock 的页偏移,区分普通页与零页 |
MultiFDSendData | type / u.ram | 主线程把 RAM 批次对象交换给选中的工作线程 |
MultiFDSendParams | data / pending_job / pending_sync / sem / sem_sync | 每线程一个当前任务;工作线程完成后归还可用槽,另处理同步请求 |
MultiFDPacket_t | ramblock / offset[] / normal_pages / flags 等线格式字段 | 发送端编码包头,接收端校验后定位页面;本地指针不写入通道 |
主线程批次:block = pc.ram, offset = [0x2000, 0x7000], num = 2
↓ 等待空闲发送线程,交换 data 指针
线程 1:pending_job = true → 构造包头和 iovec → 写通道 → pending_job = false
↓ 同一通道的字节顺序
接收线程:ramblock 名称匹配目的 RAMBlock
offset[0] → 目的 host + 0x2000
offset[1] → 目的 host + 0x7000
本地任务对象保存 RAMBlock 指针,通道包头保存 RAMBlock 名称及块内偏移。两端的宿主虚拟地址可以不同,只要名称、内存布局和偏移检查匹配,就能把内容放到对应页面。包、页批次与线程对象
以批次中的 0x2000 页为 P:线程 1 写完这一批后清除 pending_job,表示自己的任务槽可以再次使用;目的端收到包后才把 P 装入当地内存。下一轮 P 又变脏时,主线程可能把它交给另一个空闲线程。任务槽、网络传输和页版本因此是三个连续进度,下一节的屏障负责在接收端保持版本顺序。
2. 通道数据包格式
MultiFD 通道先交换初始化信息,包括 magic = 0x11223344、版本、虚拟机 UUID 和通道 ID。后续 RAM 包携带批次元数据与页内容。它有自己的包格式,与主迁移流的 START / PART / END section 分层处理。
magic · version · flags
批次信息pages_alloc · normal_pages
next_packet_size · packet_num
zero_pages · reserved
内存定位ramblock[256]
offset[]
普通页内容page 0 · page 1 · …
整数按协议规定转换成大端序。普通页的偏移排在 offset[] 前部,零页偏移位于其后;零页由目的端填零,无需携带整页内容。packet_num 是包序号,接收线程按各自通道直接装载页面,并不会先汇集全部通道再按包序号排序。包结构、页接收和头部解析
四、SYNC、FLUSH 与通道同步
1. 发送端:完成任务,再排入 SYNC
multifd_ram_flush_and_sync() 先提交残留页批次,随后调用 multifd_send_sync_main(MULTIFD_SYNC_ALL)。主线程为每个工作线程设置 pending_sync 并唤醒它,再逐个等待完成信号。
工作线程先处理 pending_job,再处理同步请求。发送 SYNC 使用与页面相同的有序通道,因此接收端不会先读到 SYNC、再读到它前面的页内容。全部线程完成这一步后,主线程写出主流的 RAM_SAVE_FLAG_MULTIFD_FLUSH。
2. 接收端:汇合所有通道
MultiFD 接收线程读到 SYNC 时,先通知公共同步信号量,再阻塞于自己的 sem_sync。主流处理函数 ram_load() 读到 FLUSH 后进入 multifd_recv_sync_main(),收齐全部通道的通知,然后释放这些接收线程。
Mermaid 源码
sequenceDiagram
accTitle: MultiFD 主流与数据通道的同步时序
accDescr: 源端主线程提交剩余批次,各发送线程完成页面写入并排入 SYNC 后报告本地完成。主线程发送 FLUSH;目的端等待所有通道装载前序页面并读到 SYNC,然后放行接收线程。示例选择 FLUSH 先到的时序。
participant S as 源端主线程
participant W as 源端发送线程
participant R as 目的端接收线程
participant D as 目的端主线程
Note over W,R: 每个数据通道各有一对线程;通道内有序
S->>W: 提交残留批次 pending_job
S->>W: 设置 pending_sync = ALL,唤醒
W->>W: 完成已有页面任务的通道写入
W->>W: 在同一通道写入 SYNC 包
W-->>S: sem_sync:本地写入完成
Note over S,W: 等全部发送线程;无须远端逐轮确认
S-)D: 主迁移流:MULTIFD_FLUSH
Note over S: 可以继续分发下一轮任务
Note over W,R: 图示 FLUSH 先到;页面和 SYNC 仍可能在途
W-)R: 各数据通道:前序页面 → SYNC
R->>R: 装载前序页面,再读到 SYNC
R->>D: 公共 sem_sync:本通道到达
Note over R: 阻塞于本线程 sem_sync
D->>D: 收齐全部通道的到达通知
D->>R: 释放各线程 sem_sync
R->>R: 才可装载后续轮次页面
Note over R,D: SYNC 先到时也在同一屏障等待 FLUSH
FLUSH 可能早于某条数据通道上的 SYNC 到达,某些 SYNC 也可能先于 FLUSH 到达。两种情况都由同一个汇合条件处理。接收线程在屏障处暂停,可以防止后续轮次的数据提前覆盖当前阶段仍在使用的状态。
本页交互为了逐步查看,等目的端完成同步后才生成下一组任务。实际源端完成本地同步并发出 FLUSH 后即可继续工作,下一组数据可以在通道中排队;目的端接收线程仍须通过屏障才能继续装载。同步发起、发送线程、接收线程等待
3. RAM EOS 与同步点
默认配置下,RAM EOS 结束本次 RAM section 载荷,专门的 FLUSH 则触发跨通道同步;两者用途不同。一轮 RAM 扫描可能跨越多个 PART,不能把 PART 的数量当作预拷贝轮数。使用旧机器兼容配置时,接收端在每个 RAM EOS 上同步,发送端也必须配对发送各通道的 SYNC。RAM 标记分派
五、源码:任务交接与同步屏障
| 环节 | 入口与关注点 |
|---|---|
| 收集页面 | multifd_queue_page():同一 RAMBlock、批次容量、未满批次的提交。 |
| 交接任务 | multifd_send():空闲线程、数据对象交换与 acquire / release。 |
| 发送工作 | multifd_send_thread():准备包头与 iovec、发送、归还任务槽,再处理同步。 |
| 页装载 | multifd_nocomp_recv():构建目的地址 iovec,读取普通页并处理零页。 |
| 同步两端 | multifd_recv_sync_main():等待所有接收线程,再统一释放。 |
源码中的本地同步和远端同步由 MultiFDSyncReq 区分。MULTIFD_SYNC_LOCAL 只等本地工作线程;MULTIFD_SYNC_ALL 还要求各通道写出 SYNC,并与主流同步标记配合。mapped-ram 文件迁移使用不同的数据定位与同步方式,不能把其完成条件直接套用到网络迁移。
六、实现演进与性能取舍
1. 从串行处理到并行批次
当页扫描、编码和发送集中在一个路径上时,CPU 处理能力可能先于网络带宽成为限制。MultiFD 把批次处理分给工作线程,让发送、接收及可选压缩获得并行机会。QMP 的 multifd-channels 参数自 4.0 提供,压缩方法参数自 5.0 提供;具体收益取决于 CPU、内存带宽、网络及负载。
2. 从逐 section 同步到扫描轮次同步
早期协议在每个 RAM section 结束时同步全部通道。主流为了限速和迭代调度产生多个 PART 时,也会反复触发屏障,已经空闲的通道需要等待其他通道。QEMU 8.1 引入按完整 RAM 扫描轮次同步的方式,并增加 FLUSH 标记,把跨通道同步与 section 结束解耦。旧机器兼容设置仍保留逐 section 行为。同步粒度调整提交、8.0 兼容属性
同步减少后,工作线程可以连续处理更多批次,但协议仍需要在页面可能跨轮重复发送及阶段切换的位置建立顺序。QEMU 10.2.0 的 multifd_ram_sync_per_section() 与 multifd_ram_sync_per_round() 明确区分两种路径。演进改变的是同步频率和标记位置,顺序约束始终存在。
3. 通道数与性能测量
增加通道会增加线程、连接和缓冲区,也可能提高内存访问竞争。当网卡带宽已经耗尽,更多线程不能增加物理链路容量;当目的端解压或写内存较慢时,发送端的并行度也无法单独解决瓶颈。一个明显较慢的通道还会拉长屏障等待。
评估时先区分两种结果:增加通道后吞吐上升,说明先前的处理路径存在可利用的并行空间;吞吐不变而线程 CPU 或屏障等待增加,则应检查网络、内存带宽或慢通道。结合 multifd_send_sync_main_wait、multifd_recv_sync_main_wait 等 trace 事件,再比较总迁移时间和停机时间。交互中的逻辑步数只展示顺序,不是性能测量。