Spike S13 · Microsandbox 在 WSL2 上跑通(私有化 Plan B 候选)
状态:✅ 通过(2026-05-20)— msb run alpine 端到端 530ms 冷起(含 OCI image cold load + μVM 启动 + exec + stdout 回收 + tear-down),决策依据 = 推进 OpenSpec change add-microsandbox-private-fallback。
环境
- host:
douglas-wsl(Windows 11 · WSL 2 · kernel 6.6.114-microsoft-standard-WSL2 · Ubuntu 24.04.3 LTS) - 资源:Intel x86_64 · 48 vCPU · 31 GiB RAM · 939 GiB free(同 s12 host)
- CPU virt flags:
vmx× 48 - nested-virt:intel = Y
- /dev/kvm:
crw-rw---- root:kvm已就位 ·kvm_intel模块已加载 - Microsandbox:v0.4.6(
msb-linux-x86_6424.5 MB binary +libkrunfw.so.5.2.121.4 MB) - Tailscale:mirror mode(Windows 端 daemon · WSL eth1 共享 Tailscale tailnet IP 100.116.83.96)
- Node:v22.22.0(≥ 22 是 microsandbox npm SDK 最低要求)
为什么记录这份 spike
add-microsandbox-private-fallback change 决策前,需要把”上游真能跑”+“WSL2 部署有几个坎”这两点先实测确认,避免重蹈 §11.6 spike s12(Cube 因 upstream #159 阻塞)的 ROI 陷阱。
实测踩了 3 个 WSL2 quirk(与 s12 部分重叠,但本 spike 是 Microsandbox 视角的复现),全部固化到 design D5 + spec ADDED Requirement “WSL2 quirk 合规约束”。
端到端 spike 结果
$ sg kvm -c "~/microsandbox-spike/msb --warn run \
10.255.255.254:5000/alpine:3.21 -- echo HelloFromMicroVM"
HelloFromMicroVM
real 0m0.530s # 总耗时 530ms包含:
msb子进程 fork + libkrun init- OCI image manifest + 3.5 MiB layer 从本机 local registry 拉
- erofs rootfs 构建 + μVM cold boot(KVM hardware accel)
- agentd PID 1 init(mount fs + run.exec)
echo执行 + stdout 经 agent socket 回传- sandbox tear-down + 资源回收
健康度对照(vs Cube · s12):
| 维度 | Cube v0.2.2 | Microsandbox v0.4.6 |
|---|---|---|
| GitHub Stars | ~50 (2026-04-20 开源) | 6162 |
| Release 节奏 | 4 个 release / 30 天,含 plugin bug regression | 4-5 个 release / 周 · 当日 PR 即合 |
| 开源 license | Apache-2.0 | Apache-2.0 |
| 维护方 | Tencent · 商业开源早期 | Zerocore AI · YC backed |
| Upstream open issues | #159(plugin loading)跨环境阻塞 OPEN | 59 active,maintainer 当日处理 |
| 我们的 spike 端到端 | △ 协议层 OK · sandbox 真起被 upstream bug 阻塞 | ✅ 真起 |
3 个 WSL2 quirk 完整复盘
Quirk 1 · WSL2 mirror mode loopback 必须用 10.255.255.254
WSL2 在 mirrored networking mode 下,127.0.0.1 映射到 Windows host loopback,但 host loopback 进程(包括 dockerd container port mapping)不通过 127.0.0.1 转发回 WSL 内部进程。
实测:
# 在 douglas-wsl 上 docker run -p 5000:5000 registry:2 之后
$ curl --max-time 5 http://127.0.0.1:5000/v2/ → HTTP 000 t=5.062s (timeout)
$ curl --max-time 5 http://192.168.10.202:5000/v2/ → HTTP 000 t=5.062s (timeout)
$ curl --max-time 5 http://10.255.255.254:5000/v2/ → HTTP 200 t=0.001s ✓
$ curl --max-time 5 http://172.17.0.1:5000/v2/ → HTTP 200 t=0.002s (docker0 bridge IP 也可)含义:
- 所有本机 client → 本机 docker container port 的通信必须用
10.255.255.254(WSL global lo IP,在/etc/resolv.confnameserver 同 IP)或172.17.0.1(docker0 bridge) - 不能用
127.0.0.1、localhost、LAN IP192.168.10.202
对 Microsandbox 部署的影响:
# ✗ msb pull --insecure localhost:5000/... (resolver 走 127.0.0.1 → fail)
# ✗ msb pull --insecure 127.0.0.1:5000/... (5s timeout)
# ✓ msb pull --insecure 10.255.255.254:5000/... (instant)s12 spike 的 cube-api/cubelet 也踩过同一个坎(cube-api 默认监听 127.0.0.1 改为 10.255.255.254)。
Quirk 2 · 用户态 HTTPS 出口被 Tailscale CGNAT 劫持
实测:
# 在 douglas-wsl 上:
$ curl -s -o /dev/null -w "HTTP %{http_code} t=%{time_total}s\n" \
--max-time 10 https://github.com
HTTP 000 t=10.015s
$ curl ... https://registry.npmjs.org → HTTP 000 t=10.018s
$ curl ... https://ghcr.io → HTTP 000 t=10.034s
$ curl ... https://docker.io → HTTP 000 t=10.024s
$ curl ... https://install.microsandbox.dev → HTTP 000 t=15.000s
$ ping -c 2 8.8.8.8 → 50% packet loss
$ getent hosts github.com
198.18.1.79 github.com # ← 198.18.x.x = Tailscale CGNAT 劫持 IP根因:
douglas-wsl 上 DNS 走 10.255.255.254(WSL host loopback)→ Windows 端 systemd-resolved-equivalent → Tailscale MagicDNS 接管 → 返回 CGNAT 假 IP(198.18.0.0/15 范围)。这套机制原本是为 Tailscale magic 主机服务的,但拦了所有 DNS 解析;用户态进程拿到 CGNAT IP 后没路由可达,全部 timeout。
例外:dockerd 本身有 magic path(在 docker network namespace + 自己的 DNS 配置)能正常 pull image。这是 docker pull alpine:3.21 在 spike 中能成功的原因。
对 Microsandbox 部署的影响:
- ✗
curl -fsSL https://install.microsandbox.dev | sh(msb 官方 install 脚本不可用) - ✗
npm install microsandbox(NPM registry HTTP 000) - ✗
msb pull ghcr.io/openspec/task-runner:latest(msb 自己的 HTTPS client 也走系统 DNS)
解法:所有 binary / image / npm package 走 mac-side 中转:
┌─────────────────────────────────────────────────────────────────┐
│ mac (有公网) ──gh release download──▶ msb-linux-x86_64.tar.gz │
│ + docker pull task-runner:latest │
│ + docker save tarball │
│ mac ──ssh ControlMaster scp──▶ douglas-wsl:/tmp/ │
│ │
│ douglas-wsl: │
│ tar xzf /tmp/microsandbox-linux-x86_64.tar.gz │
│ docker load < /tmp/task-runner.tar │
│ docker push 10.255.255.254:5000/task-runner:latest │
│ msb pull --insecure 10.255.255.254:5000/task-runner:latest │
└─────────────────────────────────────────────────────────────────┘包装脚本:scripts/bridge-task-runner-image-to-wsl.sh + scripts/deploy-microsandbox-shim.sh。
Quirk 3 · usermod -aG kvm 不立即生效,需 systemd 注入 group
实测:
$ sudo usermod -aG kvm douglasdong
$ getent group kvm
kvm:x:993:douglasdong # ← /etc/group 已更新
$ groups
douglasdong adm cdrom sudo dip plugdev users docker
# ← 但当前 session group set 没有 kvm(PAM 注入早于 usermod)
$ ~/microsandbox-spike/msb run alpine -- echo hello
Error: failed to open /dev/kvm: Permission denied # ← 当前 session 无权限
$ sg kvm -c "~/microsandbox-spike/msb run alpine -- echo hello"
hello # ← sg 临时切组生效含义:
- 交互式 shell 加 group 后必须
newgrp kvm/sg kvm -c/ 重新登录才生效 - 但 systemd 启的服务进程不走 PAM,可以直接
SupplementaryGroups=kvm注入
对 Microsandbox 部署的影响:
shim 服务的 systemd unit 必须显式声明 group:
[Service]
User=douglasdong
SupplementaryGroups=kvm
Environment=MSB_PATH=/opt/microsandbox-shim/msb
ExecStart=/usr/bin/node /opt/microsandbox-shim/shim.mjs不依赖 douglasdong 的 active session group set。
验证步骤复现(30 分钟内可全跑)
# 1. mac 端下载 binary
gh release download v0.4.6 -R superradcompany/microsandbox \
-p 'microsandbox-linux-x86_64.tar.gz' \
-p 'checksums.sha256' \
-D /tmp/
# 2. mac 端校验 + scp 到 douglas-wsl
cd /tmp && grep microsandbox-linux-x86_64.tar.gz checksums.sha256 | sha256sum -c -
scp -o ControlPath=~/.ssh/cm/douglas-wsl -o ControlMaster=no \
/tmp/microsandbox-linux-x86_64.tar.gz \
/tmp/checksums.sha256 \
douglas-wsl:/tmp/
# 3. douglas-wsl 端解压 + 加 kvm group
ssh -o ControlPath=~/.ssh/cm/douglas-wsl -o ControlMaster=no -T douglas-wsl '
mkdir -p ~/microsandbox-spike ~/.microsandbox/{bin,lib}
cd ~/microsandbox-spike && tar xzf /tmp/microsandbox-linux-x86_64.tar.gz
ln -sf ~/microsandbox-spike/msb ~/.microsandbox/bin/msb
ln -sf ~/microsandbox-spike/libkrunfw.so.5.2.1 ~/.microsandbox/lib/libkrunfw
sudo usermod -aG kvm douglasdong
'
# 4. douglas-wsl 启 local registry + 装 alpine
ssh ... 'docker run -d --restart unless-stopped --name spike-registry -p 5000:5000 registry:2
docker pull alpine:3.21
docker tag alpine:3.21 localhost:5000/alpine:3.21
docker push localhost:5000/alpine:3.21'
# 5. msb cold start
ssh ... 'sg kvm -c "~/microsandbox-spike/msb --warn run \
10.255.255.254:5000/alpine:3.21 -- echo HelloFromMicroVM"'
# 期望输出:HelloFromMicroVM(~500ms 内)与 sandbank 抽象的对位(提前定义)
| sandbank capability | Microsandbox SDK API | 备注 |
|---|---|---|
provider.create() | Sandbox.builder(name).image(...).create() | spawn msb 子进程 |
provider.exec() | sandbox.exec(cmd, args) 同步收 stdout/stderr | |
exec.stream | sandbox.execStream(cmd, args) → AsyncIterable<ExecEvent> | event.kind = stdout / stderr / exited |
provider.destroy() | sandbox.stopAndWait() | 也可以走 msb stop / msb rm CLI |
port.expose | builder .publishPort(host, guest) | |
sleep | msb stop + msb start (detached mode) | |
snapshot | Sandbox.builder(...).snapshot() | beta API |
terminal | sandbox.openShell() | 通过 shim SSE 转发 |
上游 events API(暂未 GA · 不阻塞本期 change)
Microsandbox 计划的 on_event(name, handler) / emit(name, data) host↔guest 结构化 RPC 在 v0.4.6 标注 “coming soon”。我们的 task-runner 内 stage 切换走 PostToolUse hook 写 stdout + shim execStream parse + HTTP callback 到主 API(与 BoxLite/AIO 同模式),不依赖 events API。
后续 follow-up(不在本 change scope)
- F1 Microsandbox 版本升级走单独 change(锁 v0.4.6 baseline · 因为 README 标 beta + 周 release 节奏要求审慎)
- F2 WSL2 出口劫持根治(修 Windows Tailscale daemon 配置)— 不假设修,bridge 脚本保留
- F3 events API GA 后切换 stage 回传到结构化 RPC(减少 stdout parsing 复杂度)
- F4 μVM 长时间运行的 metrics 接入(
msb metrics输出已经是结构化的,可对接 Prometheus)