keelOS:背靠 ZFS,虚拟机可以瞬间移动,数据永不丢失!
虚拟机为什么要移动
一台虚拟机一辈子里至少要移动三次:主机升级内核时它得让开;主机换硬件或者买了第二台时它得搬家;盘坏了、误删了、被加密勒索了时它得从备份里回来。三件事看起来不一样,底下是同一个问题:把一台正在跑的机器的全部状态,完整地、一致地放到另一个地方。
"全部状态"比一块盘多:硬盘可能有几块,还有配置、UEFI 变量(启动项就存在里面)、以及内存和设备状态。"一致"是说这些东西必须是同一个时刻的:盘是 10 点的、内存是 10 点零一分的,恢复出来的机器就会把已经写进盘的数据再写一遍,或者以为写过的东西不在。家用和小办公室的机器没有共享存储,没有专门的备份服务器,这件事得靠本地盘自己做对。
别人怎么做
大厂的答案都建在共享存储上:盘在 SAN 或 NFS 里,两台主机都看得见,迁移只用搬内存。家里和小办公室没有共享存储,盘得跟着一起搬。
| 方案 | 盘怎么搬 | 内存怎么搬 | 代价 |
|---|---|---|---|
| ESXi vMotion + Storage vMotion | 边跑边镜像写到新位置,最后切换 | 迭代预拷贝脏页 | 要 vCenter 和授权;两边的 CPU 要兼容 |
| Proxmox 迁移 | ZFS 上 zfs send -R 增量几轮,或者 QEMU 自己的块镜像 |
QEMU 的预拷贝 | 要集群(corosync);单机只能备份/恢复 |
SmartOS vmadm send/receive |
zfs snapshot -r 加 zfs send -R |
不搬:停机、传、对面开机 | 简单可靠;停机时间 = 传盘的时间 |
| vm-bhyve | zfs send -R 存成镜像文件再导入 |
不搬 | 手工;要先关机 |
| Hyper-V | 存储迁移(差分盘边跑边合并) | 预拷贝 | Windows 服务器;家用少 |
共同的限制有两条。一是"一致"的那一刻:盘和内存得在同一个时刻定格,QEMU 和 ESXi 都是在自己的进程里停下 I/O 后才拍盘。二是失败了怎么退:搬到一半网断了、对面盘满了,原来那份得还在、还能开。能把这两条都做对的,都是把盘交给一个自带快照和增量复制的文件系统。keelOS 选的是 ZFS,和 SmartOS 一样的布局,不同的是 keelOS 还能把内存也一起带走。
keelOS 已经做到的
keelOS 是一个很薄的 hypervisor(hyp)加一个 FreeBSD 的 root,guest 用 bhyve 跑。这个结构已经把"移动"的一半做完了:
- 主机升级,虚拟机不停。 guest 的内存在 hyp 的池里,按名字登记,比 root 活得长。root 换内核时只挂起 guest、重新加载自己,然后按名字把内存找回来接着跑。开发机上的实测:17 台 guest(Windows 10 / Server 2022 / XP、FreeBSD、Debian、NetBSD、fnOS,其中一台拿着整块 NVMe 直通)暂停 64 秒后原地继续,TCP 连接不断。这个重新加载到今天为止在真机上做了 118 次。
- 挂起和恢复带内存。 bhyve 的检查点把 vCPU、设备状态和内存写成文件,之后可以从文件里原样恢复。keelOS 自己的 bhyve 补上了 NVMe、virtio-gpu、IDE、PCnet、vTPM 这些设备的检查点,所以 Windows 这些挑剔的 guest 也能挂起。
- 每台 guest 一棵 ZFS dataset 树。 布局照 SmartOS:
zones/vm/guests/<uuid>下面是磁盘(raw 文件或 zvol)、配置、UEFI 变量、固件的副本、检查点文件。一台 guest 的全部状态都在一个地方,zfs snapshot -r一次就能拍下。快照和回滚在开发机上测过:guest 跑着拍快照,改数据,关机回滚,再开机时根盘和 zvol 都回到了拍快照的那一刻。
还没做的是另一半:把这棵树连同检查点一起送到另一台机器上去。
ZFS 给了什么
迁移需要的四样东西 ZFS 都现成有,而且是在文件系统这一层做的,不用管上面是什么 guest。
- 快照是原子的、免费的。
zfs snapshot -r在一个事务组里拍下整棵树:几块盘、配置、UEFI 变量、检查点文件都是同一个时刻的。写时复制意味着拍快照不拷任何数据,毫秒级完成,guest 不用停。 - 增量发送。
zfs send -R -i 上一个快照 这个快照只送两次快照之间改过的块。一台 100 GB 的 guest 第一轮送 100 GB,第二轮只送这几分钟写的几百 MB,第三轮几十 MB。这就是"瞬间"的来源:真正停机时只剩最后一小段。 - 每一块都有校验和。 发送流里的每个块带着校验和,对面收到时核对,写到盘上以后读回来再核对。网络上丢一个比特、盘上坏一个扇区,都会被发现,而不是安静地变成一台启动不了的虚拟机。
- 克隆。 从快照克隆出一台新 guest 不占空间,模板、测试副本、"先试一下这个升级"都是同一个操作。
SmartOS 和 Proxmox 的 ZFS 迁移用的就是这几样。keelOS 多的一块是内存:检查点文件也在这棵树里,所以它跟着盘一起被拍、一起被送。
计划中的迁移流程
把这几样拼起来就是 keelOS 计划里的迁移。它还没写成代码,但每一步用的都是已经在真机上跑过的东西。
关键在第 3 步和第 4 步之间:检查点写完、guest 还停着的时候拍最后一个快照。这时 bhyve 发出的写全部已经落盘,内存里的状态和盘上的状态是同一个时刻的,检查点文件本身就在被拍的 dataset 里。对面收到的是一个完整的、自洽的 guest,用和做检查点时一样的设备参数启动就是了。
停机时间只和两件事有关:内存多大(写检查点),最后一轮里 guest 写了多少。一台 4 GB 的 guest、千兆网络,预计在一分钟上下;盘有 100 GB 还是 2 TB 不影响这个数。同一台机器上换个池,或者备份到另一块盘,是同一个流程去掉最后两步。
数据为什么不丢
"永不丢失"不是一个承诺,是一串设计上的选择,每一条都能指出是谁在兑现。
| 会出什么事 | 谁兑现 | 怎么兑现 |
|---|---|---|
| 迁移到一半网断了、对面盘满了 | ZFS 的快照 | 原地的 guest 在自己的快照上,一直没动过;zfs receive 没收完的那一半不会变成半个 dataset,要么全有要么全无 |
| 传输中翻了一个比特 | 发送流的校验和 | 对面收到时核对,对不上就拒收这一轮 |
| 对面的盘后来坏了一个扇区 | ZFS 的块校验和 | 读到就报错,有镜像或 RAIDZ 就自动修 |
| 对面开机了、但没开好 | 流程的第 6 步 | 原地那份还在,开回来就是了;两边都不会同时跑 |
| 恢复出来的机器盘和内存对不上 | 检查点和快照的顺序 | 快照是在 guest 停着、写全部落盘之后拍的,且检查点文件在快照里 |
| 升级后 guest 起不来,想回到升级前 | 快照 + 回滚 | 每个 dataset 各回滚一次(ZFS 没有递归回滚,开发机上踩过),固件的副本也在 dataset 里,不会因为主机换了固件而对不上 |
有一样东西 ZFS 不管:guest 自己的文件系统在拍快照那一刻是不是干净的。不带内存的快照是"崩溃一致"的,相当于那一刻拔了电:现代文件系统都能从这种状态起来,但数据库可能要回放日志。带内存的快照没有这个问题,因为机器根本没有"重启"过。
现在的状态
到 2026-09-30 为止,已经在真机上跑过的:
- root 重新加载、guest 不停:118 次,最近一次 17 台 guest,含直通设备的。
- 带内存的挂起和恢复:FreeBSD、Linux、Windows 10 / Server 2022 / XP、NetBSD、Haiku,回归测试里每次升级都跑。
- 每台 guest 一棵 dataset,
snapshot -r、逐个 dataset 回滚、TRIM、raw 文件和 zvol 两种盘。
还是计划的:
- 迁移本身:
zfs send的几轮增量、挂起后的最后一轮、对面恢复、成功后才删原地的。两台开发机都在,第一版是命令行,然后进网页。 - 带内存的用户快照(不单指迁移):检查点写完、guest 还停着时拍快照的那个钩子。今天的 bhyve 写完检查点就立刻继续跑,中间没有给磁盘快照留口子,要在 keelOS 自己的 bhyve 里加一小段。
- 定时备份到另一块盘或另一台机器:同一套增量发送,去掉挂起那一步。
- 两边 CPU 不同时带内存迁移能不能恢复:不确定,要测;不行就退到关机迁移,盘的部分不变。
这篇讲的是一个按计划能成的事:难的部分(内存能挂起、一台 guest 的全部状态在一棵树里)已经做完,剩下的是把 ZFS 现成的命令按顺序串起来。做完了会把真机上的停机时间和失败演练的结果补在这里。