smolvm 实测:一条命令起一台真 VM,0.5 秒从检查点恢复
smolvm 实测:一条命令起一台真 VM,0.5 秒从检查点恢复
要在机器上跑一段自己都不信任的代码,Docker 不是最稳的那一层。它共享宿主内核,内核出一个洞就全出来了。我想要的是每次都能起一台带自己内核的机器,用完直接扔。
smolvm 干的就是这件事:Rust 写的 microVM 运行时,底层是 libkrun + KVM(macOS 走 Hypervisor.framework,Windows 走 WHP),一条命令起机,不需要 Docker daemon,默认断网。我在 Debian 上从装到跑完整过了一遍,下面是可以直接抄的用法、实测数字,以及几个会卡住你的坑。
它还有两个别的运行时没有的能力:把运行中的机器打成单个可执行文件发出去,以及把机器存档、分支、再恢复。
装:两条命令,和国内网络的那个坑
官方安装方式是:
curl -sSL https://smolmachines.com/install.sh | bash
脚本会把运行时装到 ~/.smolvm,在 ~/.local/bin/smolvm 建软链,并且下载 checksums.sha256 校验,哈希对不上直接中止。
我在国内网络上装的时候卡在了 release 下载这一步:release-assets.githubusercontent.com 读到十几兆就断流,报 OpenSSL SSL_read: unexpected eof while reading,重试几次都断在同一个位置。走镜像把包拉下来,再跟官方发布的 checksums.sha256 对一遍就行:
U=https://github.com/smol-machines/smolvm/releases/download/v1.19.2
curl -sSL -o smolvm.tar.gz "https://ghfast.top/$U/smolvm-1.19.2-linux-x86_64.tar.gz"
curl -sSL -o checksums.sha256 "https://ghfast.top/$U/checksums.sha256"
grep linux-x86_64 checksums.sha256 | sha256sum -c - # 必须显示 OK
跨两个镜像源比对同一个 release 的哈希是个便宜的习惯,两个镜像给的哈希一致、并且和官方 checksums 文件一致,才动手解包。另外记住官方安全模型里自己写的一句话:release 目前没有签名,也没有 provenance 证明,安装脚本在拿不到校验文件时会选择继续安装。也就是说校验这件事最终得你自己做。
前置条件只有一条:Linux 下 /dev/kvm 必须可读写。用户不在 kvm 组里就会报权限错,加组或者单独给 ACL 都行:
getfacl /dev/kvm # 看当前权限
sudo usermod -aG kvm $USER
装完的目录结构值得记一下,出问题时第一反应就是去这几个地方看:
~/.smolvm/:包装脚本smolvm、真正干活的smolvm-bin、lib/libkrun.so、lib/libkrunfw.so,以及storage-template.ext4.zst和overlay-template.ext4.zst两个磁盘模板~/.local/share/smolvm/agent-rootfs:guest 里的 agent 根文件系统~/.cache/smolvm/vms/<id>:每台机器的数据目录,smolvm machine data-dir --name <名字>可以直接打印出来
三种跑法
一次性运行。 最像 docker run 的用法,命令退出后整台机器连同改动一起消失:
smolvm machine run --net --image alpine -- uname -a
smolvm machine run --net -it --image alpine -- /bin/sh # 交互式
smolvm machine run --net --image python:3.12-alpine -- python3 script.py
持久机器。 create 只写配置,start 才真正起机,exec 里的改动会落到 overlay 上,跨会话和跨重启都还在:
smolvm machine create --net --name dev --image alpine
smolvm machine start --name dev
smolvm machine exec --name dev -- apk add git
smolvm machine exec --name dev -- git --version # 下次 exec 里依然在
smolvm machine stop --name dev
smolvm machine start --name dev # 重启后 git 还在
smolvm machine delete --name dev
裸机模式。 不带 --image 时直接跑内置的 Alpine rootfs,不拉任何镜像,秒级起来,适合只想要一个干净 Linux 的场景:
smolvm machine create --net --name sandbox
smolvm machine start --name sandbox
smolvm machine exec --name sandbox -- sh -c 'uname -a; cat /etc/os-release | head -1'
每台机器都有个 /workspace,它在存储盘上,exec 之间和 stop/start 之间都保留,是放脚本和产物的默认位置。注意 /tmp、/run、/dev/shm 都是 tmpfs,重启即空,别把要留下的东西放那儿。用 -v /host/dir:/workspace 挂宿主目录时,默认的 workspace 会被让位给宿主目录。
镜像从哪来
--image 认三种写法:
- registry 引用:
alpine、python:3.12-alpine、ghcr.io/you/app:v1、name@sha256:...。裸名字走默认镜像源registry.smolmachines.com - 本地归档:
docker save myapp -o myapp.tar之后--image ./myapp.tar,或者docker save myapp | smolvm machine run --image - -- ./app,也可以直接指一个解开的 rootfs 目录 - 打包产物:
--from app.smolmachine
本地来源这条路对 CI 和内网机器特别有用,我在这台机器上就是靠它绕开了拉镜像的开销。
它不 build 镜像。把 Dockerfile 喂给 --image 会被拒绝,并提示你先 build 再 save。
网络:默认彻底断,出网按需开
默认没有网络,这一点比多数运行时狠。狠到什么程度:不加 --net,连 registry 镜像都建不出来,machine create 直接失败并给你替代方案:
Error: image 'alpine' must be pulled from a registry, but this machine has no network,
so the pull can never succeed. ... To keep the machine network-isolated, supply the
image locally instead: docker save alpine | smolvm machine create --image - ...
我原本以为镜像缓存了就能离线跑,实测不行,create 阶段就拦住了。要走离线路线只能从本地来源或者打包产物进来。
出网的白名单给得很细:
smolvm machine run --net --image alpine -- wget -qO- https://example.com
smolvm machine run --net --allow-host registry.npmjs.org --image alpine -- npm ping
smolvm machine run --net --allow-cidr 10.0.0.0/8 --image alpine -- ...
smolvm machine run --net --allow-host a.com --allow-cidr 10.0.0.0/8 ... # 可以叠加
--allow-host 不只是拦流量,它在 DNS 层就拦。放行 registry.npmjs.org 之后访问 example.com 和 github.com 都直接解析失败(bad address),比连上再拒要干净。被拒的记录可以回头查:
smolvm machine egress-events --name dev
# TIMESTAMP OP DESTINATION
# ... resolve example.com
# ... resolve github.com
默认的网络后端不加虚拟网卡,所以 guest 里看不到 eth0,ping 会说 Network unreachable,哪怕 HTTP 是通的。验证连通性用 wget 或 curl。确实需要一块真网卡(要 ICMP、要自己配地址)时加 --net-backend virtio-net。
隔离到什么程度
guest 里看到的东西:
/proc/version -> Linux version 6.12.95 (root@libkrunfw) ... # 自己的内核,不是宿主的
/dev -> core fd full fuse kmsg ... random tty urandom zero # 没有 /dev/kvm
/home -> 空的,看不到宿主的家目录
也就是说这是一个货真价实的内核边界,guest 里的 root 拿不到宿主的文件系统,也没有嵌套虚拟化。但别把它当多租户方案。官方安全模型写得很清楚:smolvm 的 CLI 和 VMM 进程以调用者的宿主权限运行,宿主账号、libkrun、hypervisor 都在可信计算基里;--volume 挂进去的目录就是明着给 guest 的权限(所以别把机密目录挂进不可信负载);--ssh-agent 虽然不搬私钥,但等于把签名能力借给了这台机器。要防的是 "同一个账号下的其他恶意进程",那得靠宿主层面的账号隔离和 OS 加固,不是靠这个工具。
密钥:注入的是引用,不是存储
smolvm 自己不存密钥,它只认 "引用",在启动那一刻从宿主解析:
# 从宿主环境变量注入(左边是 guest 里的变量名,右边是宿主变量名)
smolvm machine run --net --image alpine \
--secret-env DEMO_TOKEN=DEMO_TOKEN -- sh -c 'echo $DEMO_TOKEN'
# 从宿主文件注入
smolvm machine run --secret-file GCP_CREDS=/abs/creds.json -- ./app
用起来很顺,但威胁模型要看清楚:目标进程的环境里是明文,guest 里的 root 能读 /proc/*/environ。真要密钥永远不进 VM,就用 SSH agent 转发:
smolvm machine run --ssh-agent --net --image alpine -- sh -c 'apk add -q openssh-client && ssh-add -l'
第 1 步就到这里:宿主 SSH agent 负责签名,私钥不跨边界。另外 HTTP API 的请求体和 .smolmachine 打包产物都被当成不可信输入,不允许携带可解析的密钥引用,这是有意为之。
声明式配置:Smolfile
把一台机器的定义写成一个 TOML 文件签进仓库,跟 Dockerfile 一个位置:
image = "python:3.12-alpine"
net = true
cpus = 2
memory = 512
workdir = "/workspace"
init = ["python3 -c \"open('/workspace/built.txt','w').write('init ran')\""]
[network]
allow_hosts = ["pypi.org"]
[auth]
ssh_agent = true
[secrets]
DATABASE_URL = { from_env = "PROD_DB_URL" }
smolvm machine create --name pyvm -s Smolfile # 或 --smolfile <路径>
smolvm machine start --name pyvm
写错的 key 会在 create 阶段直接报错,不会静默忽略,这点比很多配置系统讨喜。init 每次启动以 root 跑一遍,相当于 Dockerfile 的 RUN。CLI 参数优先级高于 Smolfile。
唯一的坑是它和网络默认值会打架:Smolfile 里写 net = false 同时又引用 registry 镜像,create 会失败。要么 net = true,要么把镜像换成本地来源。
检查点、分支、暂停恢复
这是它最有意思的部分,也是唯一让我觉得 "这个东西确实在做别的东西做不到的事" 的地方。
先让机器支持分支:
smolvm machine start --name src --branchable
分支是运行中的机器的写时复制子机,带着源机当前的进程、内存、磁盘继续跑。我在源机里起了一个每秒写文件的计数进程,然后分支:
smolvm machine branch --from src --name child1
# Branched 'src' -> 'child1'. Source continues running. 1.3 s
子机起来以后那个计数进程还在跑,两台机器各自独立地往上数。要一次扇出很多份(RL 采样、多个 agent 并行试同一状态这种场景),配合 smolvm-branch-ready 和 --count 8 --name-prefix worker 批量分支,子机里会拿到 SMOLVM_BRANCH_NAME、SMOLVM_BRANCH_INDEX 这些环境变量。
检查点是把机器(含内存和 CPU 状态)存成一个文件:
smolvm machine checkpoint --name src -o src.checkpoint
# Checkpointed 'src' to src.checkpoint (38 MiB written, 2.644s total, 0.396s source pause)
smolvm machine create --name restored --from src.checkpoint
smolvm machine start --name restored # 0.53 s
源机只停了 0.4 秒,后面打包在后台接着做。恢复出来的机器接着检查点那一刻的进程状态往下跑,我那个计数器就是从存档时的数字继续涨的。38 MiB 存一台带运行中进程的 Alpine,这个体积意味着存档、搬运、冷启动都变得很便宜。
暂停恢复保留内存,也不占额外的存档文件:
smolvm machine pause --name src
smolvm machine resume --name src
暂停中的机器会拒绝 start 和 stop,只能 resume 或者删掉。这一点设计上是对的:不会有人手滑把没保存的执行状态冲掉。
打包成单个可执行文件
把一台机器的环境打成一个可执行文件,对方不用装任何东西:
smolvm machine stop --name dev # --from-vm 要求机器已停止
smolvm pack create --from-vm dev -o ./devpack # 4.2 s -> 45 M 可执行 + 24 M .smolmachine
./devpack run -- git --version # 用完即清
./devpack start && ./devpack exec -- pip install x # 守护模式
也可以从镜像直接打:smolvm pack create --image python:3.12-alpine -o ./python312,然后 ./python312 run -- python3 --version。
产物是两个文件:devpack 是本平台的可执行 stub,devpack.smolmachine 是跨平台的负载(rootfs、OCI 层、存储盘)。把它推到任何 OCI registry 都行,或者直接发给同事。smolvm machine create --name x --from devpack.smolmachine 可以从负载起一台可持久管理的机器,实测 0.34 秒建配置、2.1 秒起机,不碰网络。
这里有个坑我踩了:打包默认不带 /workspace,因为它在存储盘上,而存储盘不在打包范围内。我用 dev 机装的 git 打进了包,/workspace/note.txt 没进去。要连 workspace 一起带走,加 --include-workspace。
文件传输
smolvm machine cp ./script.py myvm:/workspace/script.py
smolvm machine cp myvm:/workspace/out.json ./out.json
我拿 50 MiB 随机数据测了一遍,上传 4.4 秒(约 24 MB/s,含起步爬坡)、下载 2.2 秒(约 23 MB/s),两边 sha256 一致。单次传输上限 4 GiB,再大就该用 --volume 挂目录了。上传是原子写,中断了不会在 guest 里留下半个文件。
用代码驱动:HTTP API 与 SDK
不想 shell 拼命令就开一个本地 API:
smolvm serve start --listen 127.0.0.1:18080
curl -s http://127.0.0.1:18080/api/v1/machines
smolvm serve openapi > openapi.json # 完整的接口描述
常用几个端点:
POST /api/v1/machines 创建
POST /api/v1/machines/:name/start|stop 启停
POST /api/v1/machines/:name/exec 执行(另有 /exec/stream 走 SSE)
PUT /api/v1/machines/:name/files/*path 上传
GET /api/v1/machines/:name/files/*path 下载
POST /api/v1/machines/:name/pause|resume
我按帮助文档里 net: true 的直觉去调创建接口,被拒了。OpenAPI 里这个字段叫 network,不是 net,请求体长这样:
{"name": "apivm", "image": "alpine", "network": true}
改成 network 之后创建、启动、exec(返回 exitCode 加 base64 的 stdout/stderr)、文件上传下载都跑通了。要在自己的进程里直接驱动,还有官方 SDK:npm install smolmachines、pip install smolmachines、cargo add smolmachines,本地和 smol cloud 共用同一套 Machine API。
实测数据
环境:Debian 13、4 vCPU、3.8 GiB 内存的机器,KVM,smolvm 1.19.2。都是单次运行的观测值,不是压测。
| 操作 | 耗时 |
|---|---|
machine create(只写配置) |
0.03 s |
machine run 首次(含拉镜像) |
44 s |
machine run 镜像已缓存 |
14 到 16 s |
machine start 镜像机首次(含拉取) |
13 s |
machine start 已缓存 |
1.2 s(guest 内 /proc/uptime 0.9 s) |
machine start 从 .smolmachine |
2.1 s |
machine start 从 .checkpoint 恢复 |
0.53 s |
stop 后再 start |
1.3 s |
branch 一台运行中的机器 |
1.3 s |
checkpoint(38 MiB) |
2.6 s,源机只停 0.4 s |
pause / resume |
2.3 s / 6.6 s |
pack create --from-vm |
4.2 s(产物 45 M + 24 M) |
machine cp 50 MiB 上传 / 下载 |
4.4 s / 2.2 s,校验一致 |
内存这块值得单独说。默认机型标称 8 GiB,我这台宿主总共只有 3.8 GiB,照样跑起来了,宿主动态提交,machine status 里显示 "0.14 GiB of 7.78 GiB used"。规格里写 512 MiB 的机器,guest 里 free -m 就是 516 MiB。它靠 virtio balloon 做弹性,闲置 vCPU 也在 hypervisor 里睡,所以超卖几乎不花钱。磁盘同理:装完 80 M 运行时加 39 M agent rootfs,三台机器加共享数据 656 M。
踩过的坑
checkpoint的输出文件后缀必须是.checkpoint,写成.ck直接报output must end in .checkpoint。- 从
.smolmachine建出来的机器不能暂停。smolvm machine pause报checkpoint machine: libkrun save failed:,机器卡在pausing状态,resume也救不回来(read checkpoint footer: No such file or directory),只能删掉重建。我用同一台宿主上普通方式建的镜像机试了同样的操作,pause 正常。需要冻结状态就用checkpoint,别对打包出来的机器用 pause。 - 打包默认不含
/workspace,要--include-workspace。 - 没有
--net时,registry 引用建机直接失败,镜像缓存了也不行;离线场景走本地归档或打包产物。 machine run每次都会重新解析镜像,缓存命中也要十几秒。这是 CLI 端到端的时间,不是 VM 启动慢,别拿这个数字当成启动性能。要快就用持久机器(start1.2 秒)或者从打包产物起(2.1 秒)。- HTTP API 的创建字段是
network,不是 CLI 里的--net。 - 默认网络后端没有网卡,
ping一定失败,用curl。 - 挂载只支持目录,不能挂单个文件。
已知限制
- Windows:没有分支、检查点、GPU,网络只有 TSI 后端(TCP/UDP 加端口映射,没有 virtio-net)
- macOS:二进制必须带
com.apple.security.hypervisor权限,官方 release 已经签好了,自己重签或者重新编译会静默丢掉这个权限,之后每次启动都报krun_start_enter returned: -22 - GPU:Vulkan 走 virtio-gpu/Venus,宿主要装 virglrenderer,Windows 不支持
- CUDA:实验性,只覆盖 CUDA Driver API(
cu*那套)。用 nvcc 编译的程序、PyTorch 这类走 Runtime API 的东西不行 - 不是多租户控制面,前面说过了
常见问题
它和 Docker 的区别到底是什么? 边界不一样。容器是一堆 namespace 加共享内核,smolvm 是每台负载一个带自己内核的 VM,但启动方式和 CLI 手感做得很像容器。所以镜像还是 Docker Hub 那套 OCI 镜像,只是运行时不走 Docker daemon。
宿主内存小能跑吗? 能。我就是在 3.8 GiB 的机器上跑默认 8 GiB 机型,靠 balloon 弹性分配,宿主只提交实际用到的部分。
要 root 吗? 不要。要有 /dev/kvm 的读写权限,一般把用户加进 kvm 组就行。
能给一次性的脚本用吗? 最适合的就是这种场景。machine run 把宿主上的一次性依赖换成一个干净的内核,跑完什么都不剩。
去哪继续看
仓库在 smol-machines/smolvm,用户文档在 smolmachines.com/docs,examples/ 目录里有 python、node、docker-in-vm、本地大模型、无头浏览器这些现成例子。装好之后 smolvm --help 本身就是一份写得很细的参考手册,包含完整的 CLI 说明和 Smolfile 字段。
如果你只是想试试,从这一条开始,四十秒之后你就有一台自己的机器了:
smolvm machine run --net --image alpine -- uname -a
评论