CubeSandbox 热迁移实战指南:Cloud Hypervisor 本地迁移与嵌套虚拟机迁移全流程解析
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
导读
本文以 CubeSandbox 仓库中 hypervisor/docs/live_migration.md 为骨架,完整讲解 Cloud Hypervisor 虚拟化层提供的两种虚拟机热迁移(Live Migration)方案:同机本地迁移(Local Migration,适用于 VMM 无感热升级)与嵌套虚拟机迁移(Nested-VM Migration,适用于宿主机内再套一层的虚拟化场景)。你将掌握ch-remote的send-migration/receive-migration命令用法、--local本地模式与socat跨机桥接两种传输路径,并理解其背后基于快照(Snapshot)与内存脏页日志(Dirty Log)的迁移协议实现,可直接在当前仓库源码环境中复现与验证。
适用前提说明:文中所有命令均以当前仓库
hypervisor/目录下 Cloud Hypervisor 代码为准,示例镜像路径(如~/workloads/vmlinux、focal.raw)为文档约定,请替换为你实际的内核与磁盘镜像路径。
一、理解 Cloud Hypervisor 的迁移架构
在动手执行命令之前,先建立一个源码级的整体认知:热迁移并非把整个虚拟机"复制"过去,而是暂停—快照—传输—恢复的组合。当前仓库通过独立的 vm-migration crate 定义了迁移的抽象层,其核心内容位于 vm-migration/src/lib.rs:
Pausabletrait:提供pause()/resume(),所有可迁移组件先暂停再快照,随后恢复;Snapshottabletrait:提供snapshot()/restore(),将组件状态序列化为Snapshot(组件 ID + 子树 + 数据段);Transportabletrait:提供send()/recv(),负责把快照传输到指定 URL(Unix socket、TCP 或本地文件);Migratabletrait:组合以上能力,并额外定义start_dirty_log()、stop_dirty_log()、dirty_log()、start_migration()、complete_migration()五个钩子——前三个用于脏页追踪(迁移过程中持续记录被修改的内存页),后两个用于收尾阶段(最后一次增量传输并完成切换)。
快照数据本身以SnapshotDataSection(段 ID + 序列化 JSON 状态)组织,各设备、CPU、内存控制器都作为可迁移组件把自己的状态挂到以 VM 为根的快照树上。这套抽象在 vmm/src/migration.rs 中被具体化:内存与配置以state.json、config.json文件形式落盘(见SNAPSHOT_STATE_FILE、SNAPSHOT_CONFIG_FILE常量),并通过url_to_path()校验目标 URL 必须是file://前缀的目录。
迁移协议的传输细节定义在 vm-migration/src/protocol.rs,协议采用命令字 + 长度头(Request { command, padding, length })的消息交换,命令包括Start、Config、State、Memory、Complete、Abandon、MemoryFd。下一节的两种迁移场景,本质就是这套协议在不同传输通道上的两次具体演练。
二、本地迁移(Local Migration):适用于 VMM 热升级
2.1 场景与原理
本地迁移指源 VM 与目标 VM 运行在同一台宿主机上。典型用途是VMM 的 Live Upgrade:旧版本 VMM 承载的虚拟机不断业务,通过迁移"换乘"到新版本 VMM 进程中,实现虚拟化层本身的无感升级。
同机场景下走的是协议中的"Local version":源码注释(vm-migration/src/protocol.rs)明确说明本地模式通过Unix socket 跨进程传递文件描述符(FD)来交付内存——源侧发送memory fd command(携带 u16 内存槽位 ID 与内存 FD),目标侧直接接管该内存区域,避免了真正的数据拷贝,这也是send-migration命令带--local标志的根源。集成测试 hypervisor/tests/integration.rs 中也可看到,本地模式要求源 VM 内存以size=4G,shared=on方式启动(共享内存才能传递 FD 并共享物理页)。
2.2 完整操作步骤
第一步:启动源 VM(宿主机上,注意内存必须带shared=on):
$ target/release/cloud-hypervisor \ --kernel ~/workloads/vmlinux \ --disk path=~/workloads/focal.raw \ --cpus boot=1 --memory size=1G,shared=on \ --cmdline "root=/dev/vda1 console=ttyS0" \ --serial tty --console off --api-socket=/tmp/api1参数要点:
--memory size=1G,shared=on:shared=on是本地迁移的硬性前提,源与目标通过共享内存传递 FD;--api-socket=/tmp/api1:为 VM 打开控制通道,后续ch-remote全部命令经由该 Unix socket 下发。
第二步:启动目标 VM(同宿主机、同一工作目录,空壳实例):
$ target/release/cloud-hypervisor --api-socket=/tmp/api2目标 VM 启动时不指定任何 guest 配置,因为配置会随迁移从源侧整体送达——这正是协议第 4 步Config命令的作用。
第三步:目标 VM 进入接收就绪状态:
$ target/release/ch-remote --api-socket=/tmp/api2 receive-migration unix:/tmp/sockunix:/tmp/sock是本机迁移通道,两端(源与目标)通过该 Unix socket 完成协议握手。
第四步:源 VM 发起迁移:
$ target/release/ch-remote --api-socket=/tmp/api1 send-migration --local unix:/tmp/sock--local标志决定走 FD 传递路径而非内存数据拷贝路径。命令执行完毕且无报错后:目标 VM 继续运行,源 VM 被优雅终止(gracefully terminated)。
2.3 命令行背后的实现
send-migration与receive-migration是ch-remote内建的两个子命令,实现在 hypervisor/src/bin/ch-remote.rs:
receive_migration_api_command()(L347-L358):把receiver_url封装成VmReceiveMigrationData,通过 HTTPPUT请求打到 VMM 的receive-migration端点;send_migration_api_command()(L360-L376):把destination_url与local布尔标志封装成VmSendMigrationData,PUT 到send-migration端点。
HTTP 端点侧,在 vmm/src/api/http/http_endpoint.rs 中,ReceiveMigration与SendMigration两个 action 被分别路由到vm_receive_migration()与vm_send_migration(),由此进入 VMM 内部的迁移调度。也就是说,一条ch-remote命令的实际调用链是:
ch-remote (CLI 解析/参数封装) → HTTP PUT /api/v1/vm.send-migration (Unix socket 上) → http_endpoint 路由 → vm_send_migration() → vm-migration crate 的 Migratable 组件协同三、嵌套虚拟机迁移(Nested-VM Migration)
3.1 场景与拓扑
嵌套迁移适用于宿主机上先跑两级虚拟化的场景:外层 VM(VM 1 / VM 2)各是内层嵌套 VM 的"宿主",嵌套源 VM 运行在 VM 1 的 guest OS 内,嵌套目标 VM 运行在 VM 2 的 guest OS 内,迁移数据需要穿过外层网络。由于两个嵌套 VM 不在同一个物理地址空间,无法直接传 FD,因此这里走的是标准协议路径——通过socat把 Unix socket 桥接到 TCP,再借助外层网络转发。
整体拓扑如下:
宿主机(Host) ├── VM 1(外层,ip=192.168.101.1) │ └── 嵌套源 VM(api-socket=/tmp/api1, ip=192.168.100.1) └── VM 2(外层,ip=192.168.102.1) └── 嵌套目标 VM(api-socket=/tmp/api2)3.2 准备外层虚拟机(VM 1 与 VM 2)
两个外层 VM 都需要挂载一块共享的嵌套 guest 镜像(focal-nested.raw),通过额外的 virtio-blk 设备暴露给内层。文档还额外附了一块 1MB 的 dummy 镜像tmp.img(可选,用于测试数据一致性):
$ head -c 1M < /dev/urandom > tmp.img # 创建测试用 dummy 镜像(可选)启动 VM 1(带三块磁盘,供内层源 VM 使用):
$ sudo /target/release/cloud-hypervisor \ --serial tty --console off \ --cpus boot=1 --memory size=512M \ --kernel vmlinux \ --cmdline "root=/dev/vda1 console=ttyS0" \ --disk path=focal-1.raw path=focal-nested.raw path=tmp.img \ --net ip=192.168.101.1启动 VM 2(同样带三块磁盘,focal-2.raw是 VM 2 自己的系统盘):
$ sudo /target/release/cloud-hypervisor \ --serial tty --console off \ --cpus boot=1 --memory size=512M \ --kernel vmlinux \ --cmdline "root=/dev/vda1 console=ttyS0" \ --disk path=focal-2.raw path=focal-nested.raw path=tmp.img \ --net ip=192.168.102.1注意:外层 VM 需以
sudo运行,且两块外层 VM 挂载的focal-nested.raw必须是同一份文件——迁移的本质是把内层 VM 的状态原样搬到另一台"容器",磁盘数据必须两端共享。
3.3 启动嵌套源 VM(VM 1 内部)
在 VM 1 的 guest OS 中,把外层暴露的块设备/dev/vdb(嵌套系统盘)与/dev/vdc(dummy 盘)直接作为嵌套 VM 的磁盘:
vm-1:~$ sudo ./cloud-hypervisor \ --serial tty --console off \ --memory size=128M \ --kernel vmlinux \ --cmdline "console=ttyS0 root=/dev/vda1" \ --disk path=/dev/vdb path=/dev/vdc \ --api-socket=/tmp/api1 \ --net ip=192.168.100.1配置嵌套 guest 网络(使其可通过外层网络被 VM 2 内访问):
vm-1:~$ sudo ip addr add 192.168.101.2/24 dev ens4 vm-1:~$ sudo ip link set up dev ens4 vm-1:~$ sudo ip r add default via 192.168.101.1可选:运行一个持续 I/O 的 guest 负载用于验证迁移无中断。以下脚本在嵌套源 VM 的 guest OS 内循环对/dev/vdb做 md5sum 校验——只要两次校验结果一直相等就持续打印"equal",一旦迁移过程导致数据损坏,会跳出循环打印"not equal":
#/bin/bash # 在嵌套源 VM 的 guest OS 内执行 input="/dev/vdb" result=$(md5sum $input) tmp=$(md5sum $input) while [[ "$result" == "$tmp" ]] do echo "equal" tmp=$(md5sum $input) done echo "not equal" echo "result = $result" echo "tmp = $tmp"迁移过程中控制台应持续输出"equal",直到迁移完成后该负载仍在目标 VM 上继续运行——这就是"业务不中断"最直观的验证。
3.4 启动嵌套目标 VM(VM 2 内部)
vm-2:~$ sudo ./cloud-hypervisor --api-socket=/tmp/api2配置网络并验证与嵌套源 VM 互通(迁移走 TCP,必须保证两端可达):
vm-2:~$ sudo ip addr add 192.168.102.2/24 dev ens4 vm-2:~$ sudo ip link set up dev ens4 vm-2:~$ sudo ip r add default via 192.168.102.1 vm-2:~$ ping 192.168.101.2 # 应当成功排障提示:若 ping 失败,请检查宿主机的 iptables 规则——
FORWARD链策略为DROP是 Docker 的默认配置,会阻断外层 VM 之间的转发流量,需调整为ACCEPT或放行对应网段。
3.5 建立迁移通道并执行迁移
由于两个嵌套 VM 通过各自的unix:/tmp/sock*管理迁移通道,需要先用socat把 Unix socket 转成 TCP,让数据穿越外层网络。
嵌套目标侧(VM 2 内):先让 VMM 进入接收就绪状态,再用 socat 监听 TCP 6000 端口并把数据转发到本机 Unix socket:
vm-2:~$ sudo ./ch-remote --api-socket=/tmp/api2 receive-migration unix:/tmp/sock2 vm-2:~$ sudo socat TCP-LISTEN:6000,reuseaddr UNIX-CLIENT:/tmp/sock2嵌套源侧(VM 1 内):先用 socat 把本机 Unix socket 桥接到对端 VM 2 的 TCP 6000 端口,再发起迁移:
vm-1:~$ sudo socat UNIX-LISTEN:/tmp/sock1,reuseaddr TCP:192.168.102.2:6000 vm-1:~$ sudo ./ch-remote --api-socket=/tmp/api1 send-migration unix:/tmp/sock1注意此处不带--local——跨嵌套环境不是同机 FD 传递,走标准协议,内存数据通过 socat 桥接的 TCP 流按Memory命令 + 内存范围表(GPA, size)逐批传输。
迁移完成后:嵌套目标 VM 继续运行测试负载,嵌套源 VM 被优雅终止;控制台上 md5 校验脚本应持续输出"equal"不中断。
四、迁移协议的工作流程:源码级拆解
两份场景跑完后,值得把 vm-migration/src/protocol.rs 中注释的协议流程完整展开,它解释了"为什么命令顺序必须是先 receive 后 send":
标准版本(嵌套迁移适用):
1. 源与目标建立连接(Unix socket 或 TCP,建立方式不在协议范围内) 2. 源 → 目标:Start 命令 3. 目标 → 源:OK 响应(可接收状态数据) 4. 源 → 目标:Config 命令 + 配置数据(长度=配置数据长度) 5. 目标 → 源:OK 响应(可接收内存数据) 6. 源 → 目标:Memory 命令 + (GPA, size) 范围表 + 对应内存数据 7. 目标 → 源:OK 响应(可继续接收) 8..(n-4) 重复 6、7 直到源侧内存发送完毕 (n-3) 源 → 目标:State 命令 + 状态数据 (n-2) 目标 → 源:OK 响应 (n-1) 源 → 目标:Complete 命令 n. 目标 → 源:OK 响应本地版本(同机迁移适用):流程基本一致,唯一差异在第 6/7 步——源发送的是MemoryFd命令 + u16 内存槽位 ID + 内存 FD,目标直接接管 FD 对应的共享内存,省去整段内存拷贝。
协议还内置了双向取消机制:目标侧随时可发Error响应中止,源侧随时可发Abandon请求放弃,保证迁移失败时两端能干净回退。
配合 vm-migration/src/lib.rs 中Migratable::dirty_log()返回的MemoryRangeTable与start_dirty_log/stop_dirty_log,整个迁移循环可归纳为:
- 启动脏页日志,VM 继续运行(停机时间接近零);
- 迭代传输内存页,每次只传上次之后新脏的页;
- 脏页量收敛后暂停 VM,传输最后一次增量 + 设备/CPU 状态;
- 目标侧恢复,源侧优雅退出。
五、迁移能力与测试验证
当前仓库的迁移功能并非孤例文档,hypervisor/tests/integration.rs中包含一整套可运行的集成测试,覆盖了多种迁移变体,可作为功能边界与回归验证依据(integration.rs):
| 测试函数 | 验证内容 |
|---|---|
test_live_migration_basic | 基础热迁移(标准协议路径) |
test_live_migration_local | 本地 FD 传递迁移(--local) |
test_live_migration_numa/test_live_migration_numa_local | 带 NUMA 拓扑的迁移 |
test_live_migration_watchdog/..._watchdog_local | watchdog 设备在迁移中的状态保持 |
test_live_migration_balloon/..._balloon_local | virtio-balloon 内存气球设备迁移 |
test_live_migration_ovs_dpdk/..._ovs_dpdk_local | OVS-DPDK 网络后端下的迁移 |
测试内部的公共实现_test_live_migration()(L10065-L10109)展示了与文档一致的参数规范:源 VM 以 2 vCPU / max 4 vCPU 启动,本地模式内存固定size=4G,shared=on,非本地模式无需shared=on。若要在本仓库复现文档流程,可按测试同样的方式组织源/目标 VM 的启动参数。
六、关键注意事项与常见坑
综合文档与源码,实战中需重点留意以下几点:
- 本地迁移必须
shared=on:FD 传递依赖共享内存,源 VM 若未加shared=on,send-migration --local将无法走通(见 integration.rs 两种模式对内存参数的分支处理)。 - 目标 VM 保持"裸启动":目标进程不带
--kernel/--disk等配置,所有 guest 配置在协议Config阶段由源侧下推,切勿在目标侧自行指定。 - 嵌套迁移需共享磁盘:
focal-nested.raw必须同时暴露给外层 VM 1 和 VM 2,且内容一致;/dev/vdb、/dev/vdc的块设备直通方式要求外层设备编号与内层配置严格对应。 - 跨外层网络要开转发:Docker 默认把 iptables
FORWARD链设为DROP,嵌套场景的ping与 TCP 迁移流量都可能被静默丢弃,优先检查宿主机 iptables。 - socat 是嵌套场景的必经桥梁:
socat TCP-LISTEN:6000 UNIX-CLIENT:/tmp/sock2与socat UNIX-LISTEN:/tmp/sock1 TCP:...必须先于对应侧的迁移命令建立,且两端端口/路径严格配对。 --local标志仅限同机:跨主机或跨嵌套 VM 的迁移走标准协议路径,不要携带--local。
七、总结
本文以 hypervisor/docs/live_migration.md 的两套实操流程为主线,完成了从"照命令跑"到"理解原理"的闭环:本地迁移借助--local的 FD 传递实现同机零拷贝切换,适用于 VMM 热升级;嵌套迁移借助socat的 Unix→TCP 桥接实现跨虚拟化层的数据搬运,适用于二级虚拟化环境。两者共享同一套基于 vm-migration crate 的快照-传输-恢复协议,只是传输层实现不同。想要进一步深入,可以从 vm-migration/src/protocol.rs 的协议注释出发,结合 vmm/src/migration.rs 的落盘实现与 integration.rs 的测试用例,逐层验证迁移的每个阶段。
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考