# node-vmm：感觉像创建进程一样简单的 VM 隔离


## 关于容器的那个令人安心的谎言

现代开发中有一个大家都默许的体面假象，因为它很方便：我们假装 Docker 容器既快又轻量。如果你的参照系是 2005 年裸机的配置过程，那确实如此。但随着我的工具需要越来越频繁——而且越来越动态地——隔离工作负载，我开始感到一种拖累：在我和我真正想要的东西之间，横亘着一个庞大的外部引擎。

我想要的是某种在人体工程学上*感觉*像在 Node.js 中调用 `child_process.spawn()` 的东西，但它能提供一台完整虚拟机的硬件级隔离。不是共享命名空间的容器。我想要一个真正的内核——或者至少是一个微内核——在隔离环境中运行。最重要的是，我希望暂停和恢复足够快，快到 VM 内部的 HTTP API 能够响应客户端，并让人感觉这只是普通的网络延迟。没有"解冻"的延迟。

[node-vmm](https://github.com/misaelzapata/node-vmm) 就是这么来的。而正如这类事情通常的走向一样，这个想法一直很简单，直到我一头撞上了原生 hypervisor 的现实。

## 用艰难的方式绕过 QEMU

几乎每一个需要跨平台虚拟化的项目都会本能地求助于 QEMU。它就是瑞士军刀。但 QEMU *很庞大*。从 Node 中封装 QEMU 本可以在一周内解决跨平台问题——却会彻底葬送延迟和轻量的目标。

所以我换了一种做法：直接与每个操作系统的原生 hypervisor API 对话，用 C++ 通过 N-API 绑定到 Node。结果是一套被拆分为三个完全独立世界的架构，它们在最底层不共享任何代码：

1. **Linux**：直接对 KVM 发起 `ioctl` 调用。一个带着某种粗犷优雅的文件——`native/kvm/backend.cc`。
2. **Windows**：Windows Hypervisor Platform（WHP）。这一个是真的折磨人。WHP 丢给你一个赤裸的虚拟 CPU，然后说祝你好运自己去组装主板，于是我不得不从零开始模拟 APIC、定时器和 UART 端口。
3. **macOS / Apple Silicon**：Hypervisor.framework（HVF）。熬了好几个深夜之后，我意识到最干净的路径不是假装成 x86，而是使用 ARM64 机器配置（基于 virt）来保持一切快速且原生。

把这三者隐藏在 TypeScript 中一个单一的 `interface NativeRunConfig` 之后并不简单。Virtio 的 MMIO 中断布局是事情变得棘手的地方。在 KVM 上，设备的内存步长很干净——0x1000。在 Windows 上，我不得不把它们更紧凑地塞在 0x200，以避免与 ACPI 表重叠。最终这个抽象层稳住了，任何引入这个库的开发者永远都不必去想这些。

## 解雇中间人：没有 Docker Engine 的 OCI

每个现代工具都需要运行镜像。显而易见的做法是桥接到本地 Docker socket。这违反了我不使用重量级引擎的规则。

所以我写了 `oci.ts`——一个用 TypeScript 实现的完整 OCI（Open Container Initiative）registry 客户端。它解析 manifest，协商 token，逐层拉取 tar.gz blob，直接把它们注入到一个 VM 可以即时挂载的 ext4 rootfs 中。通过在本地解码镜像并在不接触 dockerd 的情况下挂载它来引导 `node:22-alpine`，从实践层面改变了"即时"的含义。在那些默认不安装 `mkfs.ext4` 的架构上，我们会优雅地回退到 WSL2 或 Homebrew，而不会打断流程。

## 顿悟时刻：作为违背物理定律之黏合剂的 SharedArrayBuffer

1 到 3 秒的冷启动时间还可以接受。我真正的执念是让被暂停的进程在一个网络请求的延迟内恢复——亚 100 毫秒。

VM 暂停/恢复的传统做法是冻结 CPU，把 RAM 和中断状态序列化到磁盘，唤醒时再恢复。对我的需求而言，这慢了好几个数量级。另一个选项是在 Node.js 主线程和管理 hypervisor 的 Worker 线程之间传递消息。问题在于：Node 在事件循环之上的消息传递桥会引入延迟抖动和停顿。

灵感来自思考现代游戏引擎是如何渲染的：**SharedArrayBuffer** 与 Atomics 的结合。

我实现了一个小巧的结构化缓冲区，带有用于 `CONTROL_COMMAND`、`CONTROL_STATE` 和控制台的槽位——TypeScript 主线程和 C++ Worker 线程都能原子地读取它，无需昂贵的锁，也无需通过 V8 进行任何消息序列化。

当我想暂停一台 VM 时，TS 线程会原子地往缓冲区里写入一个 `1`。KVM/WHP Worker 在它某次微观的 VM-exit 期间，检查那个共享字节，然后直接停止 vCPU 的执行——而不拆除机器的内存基础设施。VM 并不在磁盘上。它仍然在 hypervisor 中活着，只是睡着了，不消耗任何周期。

里面的 Fastify 或 Express 服务器会恢复并在 **5 到 50 毫秒**内解析一个挂起的 `GET /`。与商业 hypervisor 的做法相比，这是个不起眼的小把戏。但对于启动隔离环境而言，它带来的人体工程学收益一点也不小。

这个项目还没完成。从冷快照完整恢复 RAM 仍是我借助 dirty-page 跟踪在追求的目标，我已经把它接入了代码的基础结构中。但到目前为止，我已经得到了我当初想要构建的东西：一台真正虚拟机毫不妥协的隔离，被隐藏在某种看起来、运行起来、消亡起来都像我终端里又一个进程那样轻松的东西里。

