CubeSandbox 热迁移实战指南:Cloud Hypervisor 本地迁移与嵌套虚拟机迁移全流程解析
2026/9/16 19:14:43 网站建设 项目流程

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-remotesend-migration/receive-migration命令用法、--local本地模式与socat跨机桥接两种传输路径,并理解其背后基于快照(Snapshot)与内存脏页日志(Dirty Log)的迁移协议实现,可直接在当前仓库源码环境中复现与验证。

适用前提说明:文中所有命令均以当前仓库hypervisor/目录下 Cloud Hypervisor 代码为准,示例镜像路径(如~/workloads/vmlinuxfocal.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.jsonconfig.json文件形式落盘(见SNAPSHOT_STATE_FILESNAPSHOT_CONFIG_FILE常量),并通过url_to_path()校验目标 URL 必须是file://前缀的目录。

迁移协议的传输细节定义在 vm-migration/src/protocol.rs,协议采用命令字 + 长度头(Request { command, padding, length })的消息交换,命令包括StartConfigStateMemoryCompleteAbandonMemoryFd。下一节的两种迁移场景,本质就是这套协议在不同传输通道上的两次具体演练。

二、本地迁移(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=onshared=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/sock

unix:/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-migrationreceive-migrationch-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_urllocal布尔标志封装成VmSendMigrationData,PUT 到send-migration端点。

HTTP 端点侧,在 vmm/src/api/http/http_endpoint.rs 中,ReceiveMigrationSendMigration两个 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()返回的MemoryRangeTablestart_dirty_log/stop_dirty_log,整个迁移循环可归纳为:

  1. 启动脏页日志,VM 继续运行(停机时间接近零);
  2. 迭代传输内存页,每次只传上次之后新脏的页;
  3. 脏页量收敛后暂停 VM,传输最后一次增量 + 设备/CPU 状态;
  4. 目标侧恢复,源侧优雅退出。

五、迁移能力与测试验证

当前仓库的迁移功能并非孤例文档,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_localwatchdog 设备在迁移中的状态保持
test_live_migration_balloon/..._balloon_localvirtio-balloon 内存气球设备迁移
test_live_migration_ovs_dpdk/..._ovs_dpdk_localOVS-DPDK 网络后端下的迁移

测试内部的公共实现_test_live_migration()(L10065-L10109)展示了与文档一致的参数规范:源 VM 以 2 vCPU / max 4 vCPU 启动,本地模式内存固定size=4G,shared=on,非本地模式无需shared=on。若要在本仓库复现文档流程,可按测试同样的方式组织源/目标 VM 的启动参数。

六、关键注意事项与常见坑

综合文档与源码,实战中需重点留意以下几点:

  1. 本地迁移必须shared=on:FD 传递依赖共享内存,源 VM 若未加shared=onsend-migration --local将无法走通(见 integration.rs 两种模式对内存参数的分支处理)。
  2. 目标 VM 保持"裸启动":目标进程不带--kernel/--disk等配置,所有 guest 配置在协议Config阶段由源侧下推,切勿在目标侧自行指定。
  3. 嵌套迁移需共享磁盘focal-nested.raw必须同时暴露给外层 VM 1 和 VM 2,且内容一致;/dev/vdb/dev/vdc的块设备直通方式要求外层设备编号与内层配置严格对应。
  4. 跨外层网络要开转发:Docker 默认把 iptablesFORWARD链设为DROP,嵌套场景的ping与 TCP 迁移流量都可能被静默丢弃,优先检查宿主机 iptables。
  5. socat 是嵌套场景的必经桥梁socat TCP-LISTEN:6000 UNIX-CLIENT:/tmp/sock2socat UNIX-LISTEN:/tmp/sock1 TCP:...必须先于对应侧的迁移命令建立,且两端端口/路径严格配对。
  6. --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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询