Spindle的新microVM引擎
摘要
Spindle推出了一个新的microVM引擎,每个工作流在其独立的轻量级虚拟机中运行,支持NixOS配置、Docker构建以及缓存以实现快速后续运行。
<p><a href="https://lobste.rs/s/ybcofm/spindle_s_new_microvm_engine">评论</a></p>
查看缓存全文
缓存时间: 2026/06/30 03:32
# Spindle 的新 microVM 引擎
来源: https://blog.tangled.org/spindle-microvm/
Spindle 获得了第二个引擎:`microvm`。每个工作流都拥有一台独立的小型虚拟机,一个完整且真实的环境,你可以在其中做任何事情。这是对 Nixery 引擎的升级,同时保持完全兼容,因此如果你已经有一个正常工作的 Nixery 工作流,只需将 `nixery` 改为 `microvm`,它就能运行!有趣的部分在于 NixOS 镜像:你可以直接从工作流文件中配置机器。你可以做以下几件事:
你可以启动服务:
``
services:
postgresql:
enable: true
ensureDatabases: ["spindle-workflow"]
ensureUsers:
- name: spindle-workflow
ensureDBOwnership: true
``
你可以构建 Docker 容器:
``
virtualisation:
docker: true
steps:
- name: "do the thing!"
command: docker build ...
``
你也可以使用非 NixOS 镜像:
``
image: alpine
steps:
- name: install golang
command: apk add go
``
第二次运行速度也很快,因为它会积极缓存:你的依赖项、服务以及在 microVM 内构建的任何其他 Nix 派生都会被推送到 spindle 的 Nix 缓存中,这样下次需要它们的工作流就不必重新构建了。更多信息请见下文 [关于 Nix 缓存的双向机制](https://blog.tangled.org/spindle-microvm/#the-nix-cache-both-ways)。
与 tangled 中的其他一切一样,整个系统都可以自行托管,因此你可以在自己的硬件上运行带有 microVM 引擎的 spindle(参见 [自行托管指南](https://docs.tangled.org/spindles.html#self-hosting-guide))。如果你想要更完整的示例,文档中也有 [示例](https://docs.tangled.org/spindles.html#recipes)。
## 什么是 microVM [#](https://blog.tangled.org/spindle-microvm/#whats-in-a-microvm)
microVM 就是一种去除大部分无聊部分的虚拟机。没有 BIOS,没有 PCI 总线需要探测,没有模拟显卡,也没有普通 QEMU 机器拖带的任何缓慢的遗留组件。你得到的是 virtio 设备,其他几乎什么都没有,这意味着它启动非常快,占用内存非常少。目前我们只支持 QEMU 作为运行器,但引擎的编写方式使得其他运行器(例如 firecracker)可以稍后插入进来。
在虚拟机内部,有一个我们称之为 agent 的小型软件。Spindle 从不通过 SSH 连接或“从外部”运行命令;相反,agent 在启动时立即通过 vsock 回连到 spindle,发送问候,然后工作流的每一步都作为消息发送给它。agent 以非特权用户身份运行命令,回传标准输出和标准错误,并报告退出码。
主机端部分位于 `spindle`(https://tangled.org/tangled.org/core/tree/master/spindle/engines/microvm/agent.go),客户端是一个名为 `shuttle`(https://tangled.org/tangled.org/core/tree/master/shuttle)的小型 Rust 二进制文件。(`shuttle` 实现了 `agentproto`(https://tangled.org/tangled.org/core/tree/master/spindle/),这是 `spindle` 使用的协议。从技术上讲,任何人都可以实现这个协议,假设副作用保持不变,你也可以拥有自己的 agent!)
## 两种镜像 [#](https://blog.tangled.org/spindle-microvm/#two-kinds-of-images)
你可以启动两种“风格”的镜像,它们针对相当不同的人群。第一种是 **NixOS 镜像**。这些是有趣的:因为整个虚拟机都是使用 Nix 构建的,你可以直接从工作流文件中配置它。诸如 `dependencies`、`services`、`virtualisation`(例如 Docker)、`registry` 和 `caches` 之类的东西都直接写在 YAML 中,而 guest agent 在任何步骤运行之前构建并激活该配置。如果我们之前已经构建过完全相同的基座加配置,spindle 可以直接将存储路径交给 guest 来实现(从 `spindle` 配置的任何缓存中获取),而不是重新构建它,因此第二次运行很快。
第二种是**非 NixOS 镜像**,目前仅指 Alpine,但可以是任何镜像。这里你没有工作流级别的 NixOS 配置(因为没有 NixOS 可供配置),但如果镜像中恰好存在 Nix(就像我们的 Alpine 镜像那样),它仍然可以与 spindle 的 Nix 缓存正常通信。
## 一个 NixOS 工作流示例 [#](https://blog.tangled.org/spindle-microvm/#an-example-nixos-workflow)
如果你以前使用过 spindle,这看起来会很熟悉:它与你已经知道的清单相同,只是多了几个 NixOS 镜像能理解的额外键。下面是一个工作流,它需要 Postgres 用于测试,以及 Docker 用于构建镜像:
``
# .tangled/workflows/test.yaml
engine: microvm
when:
- event: ["push", "pull_request"]
branch: ["master"]
image: nixos
dependencies:
- go
- github:nixos/nixpkgs#hello
registry:
nixpkgs: github:nixos/nixpkgs/nixos-unstable
caches:
https://nix-community.cachix.org: "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
services:
postgresql:
enable: true
ensureDatabases: ["spindle-workflow"]
ensureUsers:
- name: spindle-workflow
ensureDBOwnership: true
virtualisation:
docker: true
steps:
- name: run tests
environment:
PGHOST: /run/postgresql
command: |
docker build -t app .
psql -c "select 1"
go test ./...
``
新的每个键都有其作用:
- **`dependencies`** 是你的步骤可以使用的包。它们被放入一个 `mkShellNoCC` 开发 shell 中,每个步骤运行前都会 source 该 shell,因此你得到的是整个 stdenv 环境(包括像 `pkg-config` 连接 `PKG_CONFIG_PATH` 这样的设置钩子),而不仅仅是裸二进制文件。这意味着你可以使用像 `openssl` 这样的依赖,并轻松编译 `openssl-sys` Rust crate!像 `go` 这样的裸名称会在 nixpkgs 中查找(与 Nixery 相同),但你也可以使用 `flakeref#attr` 语法指向任何 flake,因此 `github:nixos/nixpkgs#hello` 直接从该 flake 拉取 `hello`。
- **`registry`** 是重新映射全局引用的方式。这里我们将 `nixpkgs` 固定到 `nixos-unstable`,因此上面的裸 `go` 从 unstable 解析。你可以以同样的方式为你的 flake 设置别名(`myflake: github:me/x`,然后在 `dependencies` 中使用 `myflake#tool`)。
- **`caches`** 是二进制缓存 URL 到其受信任公钥的映射。它们被连接到读取代理(见下文)中,因此 guest 可以从中替换预先构建的路径,而不是从头开始构建所有内容。
`services` 和 `virtualisation` 是有趣的部分:它们被直接传递给 NixOS,因此任何你能在 NixOS 配置中写的内容都可以在这里写。`services.postgresql.enable` 会在任何步骤运行之前启动 Postgres。由于步骤以 `spindle-workflow` 用户身份运行,使用 `ensureDBOwnership` 命名一个以该用户命名的数据库是实现数据库工作的简单途径——Postgres 对等认证将 unix 用户直接映射到匹配的角色,因此 `psql` 通过套接字连接时不需要密码,也不需要额外设置(这种名称匹配是 NixOS 对 `ensureDBOwnership` 的要求;如果你想要一个不同名称的数据库,你需要自己授予访问权限)。
`virtualisation.docker: true` 是 `virtualisation.docker.enable = true` 的简写,它会在 VM 内为你提供一个真正的 Docker 守护进程。到你的第一步运行时,Postgres 已经在监听,Docker 套接字也已就绪,无需任何边车操作,它只是机器的一部分。(只要有 `enable` 选项的地方,`true` 就可以作为 `.enable = true` 的简写,因此大多数“只需打开”的服务都是一行即可!)
## 架构 [#](https://blog.tangled.org/spindle-microvm/#the-architecture)
### 双向 Nix 缓存 [#](https://blog.tangled.org/spindle-microvm/#nix-cache-both-ways)
Spindle 通过运行在主机上的两个代理与它的 Nix 缓存通信,因此 guest 永远不需要凭据或直接网络访问来访问它。与 agent 类似,它们使用 vsock 与 spindle 通信。读取代理将查询分发到配置的 substituter 以及你在工作流中列出的任何缓存,因此当 guest 需要实现一个存储路径时,它询问代理,代理去获取。请求同时发送到读取缓存,因此最先响应的那个获胜。
上传代理则相反:在 guest 内构建的任何路径都会被推送回 spindle 的 Nix 缓存(如果配置了的话),这样下次需要它的工作流就不必再次构建了。任何在配置的读取缓存上已经存在的路径将不会被上传。当 agent 报告已构建的路径时,它们会被排队并在后台上传,同时工作流的其余部分继续运行,因此上传与工作重叠,而不是阻塞工作。如果在 VM 拆除时仍有上传正在进行,工作流会等待直到所有上传完成。
Spindle 可以配置为使用 `http`、`ssh-ng` 或 `ssh` URL 作为上传到的二进制缓存,因此例如 `ssh-ng://localhost` 只会将路径上传到运行 spindle 的机器上的本地 Nix 存储!`ssh-ng` 和 `ssh` 要求 PATH 中存在 Nix,以便 spindle 可以使用 `nix copy` 上传到它们,但如果你使用的是支持 `http` 的二进制缓存(例如 [ncps](https://github.com/kalbasit/ncps)),则不需要 Nix。
### 构建镜像 [#](https://blog.tangled.org/spindle-microvm/#building-the-images)
镜像构建使用 Nix 完成。对于 NixOS,我们依赖 [microvm.nix](https://github.com/microvm-nix/microvm.nix) 并在其上添加我们自己的部分(精简内核模块、配置用户等)。对于 Alpine,有一个较小的 Nix 定义来获取内核、initrd 和内核模块,设置一个在启动时配置机器的 init 脚本,复制我们想要的依赖项(`nix`、`git` 等),并将整个根文件系统压缩为 squahfs。
不过,这些都不*必须*使用 Nix。就 spindle 而言,只要满足以下几点,镜像就是有效的:存在一个 guest agent(实现 `agentproto`)并在启动时启动,存在一个 `spindle-workflow` 用户,并且工作目录设置在 `/workspace`。你可以用任何你喜欢的方式构建它。
### 寻找镜像 [#](https://blog.tangled.org/spindle-microvm/#finding-an-image)
每个构建好的镜像都会在其产物旁边附带一个 `spec.json`。spec 是整个契约:内核、initrd 和只读存储磁盘的位置,启动参数,分配的内存和 vCPU 数量,运行步骤的 shell,可写卷,网络接口,以及运行器特定的旋钮(机器类型、CPU、额外的 QEMU 参数)。NixOS 镜像还带有一个 `baseConfigHash`,标识内置的基本配置(这是 `nixosSystem.config.system.build.toplevel.outPath` 的哈希值)。
工作流使用顶层的 `image` 键选择镜像。名称与磁盘上的内容逐字匹配,我们查找名为 `` 的目录,其中包含 `spec.json`,然后回退到平面的 `\.json`。这里好的性质是解析*仅*依赖于名称和磁盘上的内容,从不依赖于执行解析的主机,因此相同的工作流在每个 spindle 上解析到相同的镜像。如果运维人员将多种架构并存,他们可以将其命名为 `nixos-x86_64`、`alpine-aarch64` 等(后缀只是名称的一部分,没有特殊处理)。如果你希望例如 `nixos` 也能工作,你可以简单地将 `nixos` 符号链接到 `nixos-x86_64`。
在启动之前,我们会双重检查引用的文件实际存在,并且主机拥有我们需要的工具:用于卷的 `mkfs.ext4`,用于 spec 架构的 QEMU 二进制文件,`/dev/kvm` 和 `/dev/vhost-vsock`,以及如果镜像需要网络,则需要 `ip`/`mount`/`slirp4netns`/`unshare` 工具链。
### 工作流的生命周期 [#](https://blog.tangled.org/spindle-microvm/#the-life-of-a-workflow)
一个工作流经历几个阶段:被解析并解析其镜像,等待一个插槽,进行设置,运行步骤,然后拆除所有内容。等待步骤非常重要。每个镜像声明它需要多少内存、多少 vCPU 和多少磁盘空间,一个工作流必须从资源调度器获取一个插槽才能启动任何东西。调度器是工作守恒的,具有老化机制和按用户公平性,因此一个人提交一百个作业不会饿死其他人,并且如果存在适合预算的工作,插槽不会空闲。
一旦获得插槽,我们进行设置。Spindle 为 guest 分配一个随机的 vsock CID 并注册到 agent hub。它创建每个工作流的工作目录,启动两个缓存代理(前面描述过),一个 DNS 代理通过主机解析并过滤掉私有/特殊用途地址,然后创建 VM:可写卷变为稀疏文件并格式化为 ext4,存储磁盘以只读方式附加,QEMU 以 `-sandbox on`、`-nodefaults`、无显示、无监视器等启动,串行(启动时)/`virtio_console` 输出到日志文件,并带有一个 QMP 套接字用于控制。
然后我们等待机器启动。我们轮询 QMP 直到 QEMU 报告 guest 正在运行,然后等待 agent 通过 vsock 从我们期望的 CID 发来的握手。agent 告诉我们它的协议和版本,spindle 发回作业 ID、受信任的缓存公钥以及缓存和 DNS 代理端口。从那里开始,步骤一个个运行,作为 `$shell -lc `,以非特权工作流用户身份在 `/workspace/repo` 中运行,带有正确的环境和任何解锁的秘密。
如果工作流激活了一个 NixOS 配置,并且我们已经构建了完全相同的基座加配置,那么激活步骤可以实现一个缓存的顶层存储路径,而不是重新构建。无论哪种方式,无论是从头构建配置还是拉取缓存的顶层,该输出都会在发生时直接流入激活步骤的日志中,因此你可以看到闭包进入,而不是盯着空白屏幕想知道是否发生了什么事情。
超时是协作的:我们从工作流超时计算出一个截止时间并发送给 guest,我们这边有一点宽限,以便 guest 有机会自己报告超时,而不是我们直接将其拉出机器。如果 VM 在步骤进行中崩溃,我们会将串行和 QEMU 日志的尾部输出到步骤的标准错误中,因为“guest agent connection lost: EOF” 在凌晨 2 点读到是完全没有用的……。
拆除无论工作流是通过、失败还是超时都是一样的:排空任何待处理的 Nix 缓存上传,要求 agent 关机,等待 QEMU 退出(回退到 QMP `system_powerdown`,如果它固执的话最后杀掉),然后关闭代理并删除工作目录。
### 锁定网络 [#](https://blog.tangled.org/spindle-microvm/#locking-down-the-network)
一个可以访问主机本地网络的 VM 是一个可以访问它不该访问的东西的 VM。因此 QEMU 根本不运行在主机的网络命名空间中。我们首先 `unshare` 到新的用户、网络和挂载命名空间。在该命名空间内,一个小型包装器绑定挂载一个指向 `127.0.0.1` 的 resolv.conf,这样 QEMU 内置的 slirp DNS 不会被使用,然后在 exec QEMU 之前为每个特殊用途的 IP 范围(RFC 6890,因此包括私有网络、链路本地、回环等)安装黑洞路由。`slirp4netns` 随后提供命名空间的出站互联网连接,并带有 `--disable-host-loopback`、sandbox 和 seccomp 全部开启。QEMU 在该命名空间*内部*运行,guest 的网卡连接到 QEMU 自己的内置用户模式网络。因此来自 guest 的每个数据包经过两跳:guest → QEMU 的 slirp → 命名空间的 `slirp4netns` → 互联网。Guest 永远不会看到主机的网络,主机的网络也永远不会看到 guest。所有这些都不需要任何特权!
Guest DNS 不使用任一层 slirp。Guest 的 `/etc/resolv.conf` 指向 `127.0.0.1:53` 上的 shuttle,shuttle 通过 vsock 将 DNS 数据包转发到主机端的 DNS 代理。该代理通过主机的实际解析器解析,并过滤掉任何指向私有或特殊用途地址的答案,因此 guest 流量只能到达外部世界,永远无法到达主机或其本地网络上的任何东西。
### 预算和 cgroups [#](https://blog.tangled.org/spindle-m
相似文章
MicroVMs:运行具有完整生命周期控制的隔离沙箱
AWS Lambda 宣布推出 MicroVMs,这是一种新的无服务器计算原语,提供隔离、有状态的执行环境,具备虚拟机级别的隔离和近乎瞬时的启动,由 Firecracker 驱动。
开源适用于 Docker 和 gVisor 沙箱的 MicroVM 运行时
Aerol AI 开源了一个与 Docker 和 gVisor 沙箱兼容的 MicroVM 运行时,其设置和启动速度比现有方案更快。
在 NixOS 上使用 microvm.nix 的编码代理虚拟机
一篇技术指南,介绍如何在 NixOS 上使用 microvm.nix 创建临时虚拟机,以便在无法访问个人文件的情况下安全运行编码代理。
我们逆向工程了 Docker Sandbox 未文档化的 MicroVM API
一个团队逆向工程了 Docker Sandboxes 使用的未文档化的 MicroVM API,并构建了开源 Sandbox Agent SDK,用于在微虚拟机中编排 AI 编码代理,以实现安全的不可信代码执行。
@gerhart_x: "NVX 是一个超轻量级 micro-VM 沙箱,用于在硬件强制隔离下运行不受信任的工作负载。它基于……"
Microsoft 开源了 NVX,这是一个超轻量级的 micro-VM 沙箱,用于在硬件强制隔离下运行不受信任的工作负载,基于 OpenVMM 构建,支持以 Linux 作为客户机,并兼容 KVM、MSHV 和 Windows Hypervisor Platform。