跳转至

BoxLite Kubernetes 实战:microVM 启动、代码执行与磁盘恢复

BoxLite 把微型虚拟机(microVM)的创建和使用封装成 SDK。应用可以启动一个独立 Linux 环境,在里面运行 Python、读写文件,再结束环境。对于需要执行模型生成代码的 Agent,这是一条值得验证的实现路线:应用管理任务,BoxLite 提供带独立内核的执行环境。

这次将 BoxLite 0.10.7 部署到一个已有 Kubernetes 测试集群,在单个具备 KVM 能力的节点上完成实际验证。缓存镜像后的启动中位数为 736.9 ms,已启动环境执行短 Python 的中位数为 32.1 ms;四实例并发、文件隔离、执行期限,以及承载 Pod 重建后的磁盘恢复均完成验证。实验也发现了恢复场景中一个值得注意的 SDK 指标口径问题。

文中的地址、节点名、镜像仓库和集群标识已脱敏。配套示例与测量数据可用于复现。

1. Kubernetes、BoxLite 与 guest 各负责什么

本次使用的是 BoxLite 的嵌入式 Python SDK。Kubernetes 管理一个承载应用的 Pod;该应用进程调用 BoxLite,通过 libkrun 和 KVM 启动多个 microVM。每个 microVM 内再运行 OCI 镜像提供的用户空间。OCI 是 Open Container Initiative,本文所用的 Python 镜像仍是常见的容器镜像格式。项目说明

flowchart TB
  A["实验客户端 / Agent 应用"] --> B["Kubernetes Pod<br/>Python + BoxLite SDK"]
  B --> C["microVM A<br/>独立 Linux 内核 + Python"]
  B --> D["microVM B<br/>独立 Linux 内核 + Python"]
  B --> E["KVM / libkrun<br/>宿主机虚拟化能力"]
  B --> F["PVC<br/>镜像缓存、元数据与 guest 磁盘"]
  B --> G["实验指标导出器<br/>VictoriaMetrics → Grafana"]

因此,Kubernetes 中的一个 Pod 可以对应多个 Box。每个 Box 不会自动成为独立的 Pod、Service 或 Kubernetes 自定义资源。节点选择、应用副本和外层资源限额由 Kubernetes 管理;Box 的创建、命令执行、停止和删除由 SDK 管理。

这一点也解释了它与 OpenKruise Agents 的区别:后者将预热池、领取和生命周期放进 Kubernetes 控制器与自定义资源模型;BoxLite 的嵌入式路径更接近应用内部的虚拟机运行库。两者的能力层次不同,不能仅用启动耗时排出高低。

1.1 这次 Kubernetes 的作用在哪里

本轮将 Kubernetes 作为外层的应用管理平台。节点已经选定,实际验证的是在这个节点上运行 BoxLite,而非让 Kubernetes 在多个节点之间为每个 Box 选择位置。

Kubernetes 提供的能力 本次实际使用方式
应用部署与节点选择 用 Deployment 管理 SDK 应用,通过节点选择器限定到具备 KVM 的测试节点
应用进程重建 重建承载 Pod,再由应用重新打开原有 Box 的磁盘
外层资源管理 对承载容器配置 CPU、内存请求与限额
存储与访问 PVC 保存 BoxLite home,Service 与 port-forward 提供实验入口
监控接入 复用已有 VictoriaMetrics、Grafana 与采集资源

例如,一个 Pod 内启动四个 Box,Kubernetes 仍然只看到一个 Pod。本次 Pod 请求 1 CPU / 2 GiB,容器限制 4 CPU / 8 GiB;四个 Box 各配置 1 vCPU / 512 MiB,不会自动形成四份独立的 Kubernetes 资源请求。每个 Box 的资源分配、数量上限和宿主机限额,需要 BoxLite 或上层平台继续管理。

从依赖关系看,这组单节点 SDK 实验也可以在直接提供 KVM 的 Linux 环境中完成。使用 Kubernetes 的收益是接入已有运维体系;它不会让 Box 自动获得独立的 Kubernetes 调度、故障迁移和任务续跑能力。BoxLite 在本轮直接使用节点 KVM,现有 KubeVirt 虚拟机是同节点上的其他工作负载。

1.2 BoxLite 有没有自己的调度控制面

有,但需要部署官方平台组件。

嵌入式 SDK 管理本地运行时;官方仓库的 apps/ 另有 API 控制面、Runner 执行服务及 Console 等组件。API 接收创建请求并选择 Runner,一个 Runner 再管理多个 Box。该平台也可运行在云虚拟机上,不以 Kubernetes 为必需依赖。官方平台架构

核对 v0.10.7 的 RunnerService,实例放置流程为:

  1. 筛选 READY、允许调度、没有进入排空状态,并达到可用性评分阈值的 Runner。
  2. 根据请求中的区域、实例类别和排除列表进一步过滤。
  3. 按可用性评分降序取最多十个候选,再随机选择一个;没有候选时返回无可用 Runner 的错误。

可用性评分会使用 Runner 上报的 CPU、内存、磁盘和已分配资源等数据。这是平台内部的 Runner 选择逻辑,不能直接理解为 kube-scheduler 的 Pod 调度,也不能仅凭评分筛选就认定已为每次请求完成严格的资源预占。Runner 调度实现

1.3 两层调度怎样配合,本轮覆盖到哪一层

如果将完整 BoxLite 平台部署到 Kubernetes,可以形成两层放置关系:Kubernetes 决定 Runner Pod 位于哪个节点,BoxLite API 决定新 Box 分配给哪个 Runner,然后由 Runner 通过本机 KVM 启动 guest。Runner Pod 是 Kubernetes 的管理对象,Box 是 BoxLite 平台的管理对象。

本轮只部署嵌入式 SDK 应用,没有部署官方 API、Console 和多 Runner 控制面。

后文的四实例并发属于单个应用 Pod 内的执行验证;官方调度流程来自源码核对,不能算作已经完成的跨节点调度实测。

要继续评估共享沙箱平台,下一轮应至少部署两个独立 Runner,验证容量不足、排空、故障状态与任务分配,并为各 Runner 使用独立的 runtime home 和存储。增加应用 Pod 副本、共享同一个 BoxLite home,不能替代这项控制面验证。

2. 实际部署环境

项目 本次配置
Kubernetes / 容器运行时 1.30.4 / containerd 1.7.14,Linux amd64
宿主机内核 / cgroup 5.10.134 / cgroup v1
BoxLite 0.10.7;源码提交 e140b56b4c6195292cbcf26de76c406e71ab56f4
Python 客户端 Python 3.12,使用发布的 Linux wheel
guest 用户空间 Python 3.12 slim OCI 镜像,经内部镜像缓存加载
单个 microVM 1 vCPU、512 MiB guest 内存
最大测试并发 4 个 microVM,90 秒 CPU 任务
承载容器 请求 1 CPU / 2 GiB,限制 4 CPU / 8 GiB
存储 10 GiB、ReadWriteOnce 本地卷,挂载到 /data
访问与监控 localhost port-forward;VictoriaMetrics 采集,Grafana 展示

版本锁定在 2026-10-18 发布的 v0.10.7。部署只落到一个选定节点,沿用已有容器运行时和网络,没有安装节点 DaemonSet,也没有调整现有 KubeVirt 配置。实验前后检查,节点上原有五台虚拟机均处于 Running。版本记录

2.1 设备与权限

节点已具备可用的 /dev/kvm。实验容器使用特权模式,显式挂载 /dev/kvm、/dev/fuse 和 /dev/vhost-vsock。这是为验证兼容性选择的部署基线,与上游容器化部署示例的方向一致;本轮没有完成最小权限部署验证。部署模式

KVM 设备可见只是前提,实际启动成功才是验证结果。首个 guest 返回的内核版本为 6.12.87,宿主机为 5.10 系列;后续两个 Box 的 boot_id 也不同。这些观测共同证明任务进入了独立 guest,而不只是承载 Pod 中的普通子进程。

2.2 两次镜像拉取不能混为一谈

第一层由 kubelet/containerd 拉取实验应用镜像;第二层由 BoxLite 自己拉取 guest OCI 镜像并准备磁盘。节点已经缓存某个容器镜像,不代表 BoxLite 的镜像缓存也已经就绪。

首轮 guest 镜像准备失败。进一步请求仓库 HTTPS 接口后,确认实验镜像内没有信任该仓库使用的自签证书。第二次尝试仅针对这个仓库设置 ImageRegistry(skip_verify=True),随后启动成功。此项属于实验中的明确例外;正式接入应配置可信证书链,而不是把跳过校验作为默认配置。Python SDK 需要显式传入 registry options,不会自动读取 CLI 的配置文件。镜像仓库配置

3. 从代码启动一个真实 microVM

普通公共镜像环境下,基本调用如下:

import asyncio
import boxlite

async def main():
    runtime = boxlite.Boxlite(boxlite.Options(home_dir="/data/runtime"))
    try:
        async with boxlite.SimpleBox(
            image="docker.io/library/python:3.12-slim",
            runtime=runtime,
            cpus=1,
            memory_mib=512,
        ) as box:
            result = await box.exec(
                "python", "-c", "print(sum(range(101)))", timeout=20
            )
            print(result.exit_code, result.stdout)
    finally:
        await runtime.shutdown()

asyncio.run(main())

本次实际返回退出码 0 和结果 5050。第一次成功启动的客户端耗时为 5.466 秒,其中包含 guest 镜像准备与初始化;它不是纯内核启动时间。SDK 中的 guest_boot_duration_ms 此次为 null,没有将其填成零或用于绘制启动性能图。

为了让执行结果可以直接查看,配套示例提供了一个读取真实 JSON 记录的实验页:

BoxLite 实验页展示缓存启动、命令执行、隔离、超时、并发和磁盘恢复的实际记录

这张图是本文配套的 SDK 实验页。BoxLite 上游确实提供官方 Console,位于 apps/dashboard,但官方 Console 依赖 API、认证、数据库及 Runner 等控制面。本轮验证的是嵌入式 SDK 路径,没有将 mock 模式截图作为部署证据,也没有把实验页冒充官方控制台。官方 Console 说明

4. 启动与重复执行测到了什么

启动计时从调用 SimpleBox.start() 前开始,到它返回为止,随后再执行一次结果校验。镜像缓存保留,每轮 Box 为新建;上一轮结束后停止并自动删除。

测试 样本数 中位数 最小值 最大值
guest 镜像已缓存后的 start() 10 736.9 ms 713.8 ms 1,427.2 ms
已启动 Box 中执行短 Python 20 32.1 ms 31.2 ms 71.8 ms

第二行在同一个已启动 Box 中重复执行 print(sum(range(101))),每次均校验退出码和输出。它包含命令通道和 Python 进程启动开销,不包含大模型推理、复杂依赖导入或数据下载。

这些结果适合回答“准备一个小型 Python 执行环境,延迟大致在哪个量级”。样本不足以描述生产尾延迟,也不能与使用不同节点、不同预热策略的沙箱平台直接横向比较。

测试脚本的首轮还出现过一次调用顺序错误:停止 Box 后再读取 SimpleBox.id,当前 SDK 会拒绝该操作。修复为停止前保存 ID 后,独立记录第二轮结果。原始失败记录保留,表中的十次、二十次来自修复后的完整测试,没有把首轮未完成的调用算成成功样本。

5. 文件、超时与并发验证

5.1 文件隔离与回传

同时启动 A、B 两个 Box,在 A 的 /root/isolation.txt 写入随机标记。B 中该文件不存在,而且两个 guest 的 boot_id 不同。随后将本地文件复制到 A,再复制回承载应用,对回传内容校验 SHA-256,结果一致。

这验证了本轮默认文件系统下的分离和文件传输链路。共享目录、显式卷挂载和网络策略会改变资源可见性,需要在实际应用配置中另外验证。

5.2 期限到了,环境还能继续使用吗

guest 中运行一段原计划休眠 20 秒的 Python,忽略 SIGALRM,执行时配置 timeout=2。实际 2.011 秒返回,退出码为 -15;随后在同一个 Box 中执行第二条命令,正常返回 alive。

因此,此次期限通过命令退出结果判断,而不是等待 Python SDK 抛出超时异常。当前 SDK 的超时调用可能返回非零退出码的 ExecResult,调用者必须检查结果。上游超时测试

5.3 四个环境同时运行

同时创建四个 Box,每个配置 1 vCPU / 512 MiB,在 guest 中连续执行 SHA-256 CPU 循环 90 秒。首轮并发执行阶段耗时 90.610 秒,四个任务均返回正常退出码和有效计数;补采资源曲线的一轮耗时 90.354 秒,同样四个任务全部完成。

这里没有将哈希循环包装成 Agent 吞吐分数。它的用途是让四个 guest 同时持续工作,观察承载容器的资源变化以及结束后的回收情况。

6. Pod 重建后,究竟恢复了什么

本轮采用 auto_remove=False 保留 guest 磁盘,将完整 BoxLite home 放在 PVC 中,而不是只保存某个工作目录。

实际步骤为:

  1. 创建命名 Box,在 /root/marker.txt 写入随机标记,保存 Box ID 与 guest boot_id。
  2. 停止 Box,确认准备阶段完成。
  3. 对承载 Deployment 执行 rollout restart,确认 Pod UID 已改变。
  4. 在新 Pod 中使用同一 PVC、同一 home 和同一 Box 名,配置 reuse_existing=True 启动。
  5. 检查 Box ID、文件内容和新的 guest boot_id,随后停止并删除该验证实例。

结果是 Box ID 相同、文件内容相同、guest boot ID 不同;重新启动耗时 712.3 ms。

这说明保留下来的是磁盘与实例元数据,guest 重新启动了。原进程、Python 内存变量和打开的网络连接不能靠这一过程继续运行。如果 Agent 希望从某个业务步骤接着做,还需要在应用层持久化任务状态和已完成步骤。

本次 PVC 是本地卷,因此验证覆盖同一节点上的 Pod 重建。节点磁盘损坏、迁移到另一节点和存储灾难恢复不在这个结果内。

7. 资源曲线,以及一个 SDK 计数陷阱

四实例并发与结束后的真实监控曲线,SDK 推导数量在恢复后显示为三

图中数据来自实验时的实际采集:

观测项 结果与口径
承载容器 CPU 峰值 4.005 核;容器自身 cgroup 累计 CPU 时间的 30 秒速率
承载容器工作集峰值 764,030,976 字节,约 728.6 MiB;usage 减去 inactive file cache
功能检查记录 第二轮 8 个检查点通过,包含持久化准备与恢复、一次并发复测
结束后 SDK 推导数量为 0,容器内检查未发现 KVM 文件描述符残留

资源导出器读取当前容器的 cgroup 挂载根,没有读取整机根 cgroup。曲线包含承载应用与它运行的 microVM 进程;它不是单个 guest 的“已用内存”,也不能用 512 MiB × 4 与它直接做减法。guest 配置内存与宿主机实际驻留、文件缓存的口径不同。

图中还有一个容易忽略的现象:实际并发为四,但 SDK 数量曲线的平台是三。核对 v0.10.7 实现,num_running_boxes 由当前 runtime 进程的 created - stopped - failed 计数推导,并非实时枚举活跃实例。这个进程先恢复了一个旧 Box,又停止了它;结合计数实现,能够解释后续新建四个 Box 时少一个的现象。这是基于实验与源码的判断,不是对所有版本的结论。运行时指标实现

因此保留了原曲线,并将标题明确标为 SDK 推导数量。实际并发与退出状态同时由四个独立命令的结果验证;生产容量和计费不应只依赖这个推导值,尤其要覆盖恢复与重复启动场景。

本轮使用自建导出器桥接 SDK 和容器 cgroup 数据,再通过新增的 VMServiceScrape 采集;这不表示 BoxLite Python SDK 自带一个 Prometheus HTTP 端点。原有监控采集配置保持不变。

8. cgroup v1 能跑,不等于每个 Box 的宿主机限额都已生效

测试节点使用 cgroup v1。v0.10.7 的 jailer 资源控制代码检查 cgroup v2;建立 cgroup 失败时,调用路径记录警告并继续启动。因此本轮能够执行 microVM,但不能据此声称每个 Box 的宿主机 CPU、内存、进程数限额都已经建立。cgroup 检测、继续执行的处理路径

这里有三层不同的配置:guest 的 1 vCPU / 512 MiB、承载容器的 4 CPU / 8 GiB,以及 BoxLite 尝试建立的每实例宿主机 cgroup。前两层的配置存在,不能代替第三层的验证。

如果继续评估多租户生产部署,下一步应放到 cgroup v2 环境验证每实例限额、恶意进程与内存压力下的回收,同时收紧可信 launcher 的权限。特权 Pod 本身属于平台信任边界,guest 的虚拟化隔离不能自动消除 launcher 被滥用的风险。

9. 如何复现与查看

配套目录提供 Dockerfile、清单渲染器、完整测试脚本、只读记录页和监控配置。将示例中的节点、存储类与镜像替换为自己的环境:

docker build --platform linux/amd64 -t registry.example.com/lab/boxlite:0.10.7 \
  examples/boxlite-kubernetes
docker push registry.example.com/lab/boxlite:0.10.7

python3 examples/boxlite-kubernetes/render.py \
  --test-node worker-lab --storage-class local-lvm \
  --image registry.example.com/lab/boxlite:0.10.7 \
  --output .local/boxlite-lab

kubectl create namespace boxlite-lab --dry-run=client -o yaml | kubectl apply -f -
kubectl apply --dry-run=server -f .local/boxlite-lab/install.json
kubectl apply -f .local/boxlite-lab/install.json
kubectl -n boxlite-lab rollout status deployment/boxlite-lab
kubectl -n boxlite-lab port-forward service/boxlite-lab 18940:9108

浏览器打开 http://127.0.0.1:18940/ 查看记录。另一个终端提交测试并检查状态:

curl -X POST http://127.0.0.1:18940/api/suite
curl http://127.0.0.1:18940/api/state

202 Accepted 只表示已接收。等待 busy=false 后逐项检查记录,确认 persistence_prepare 成功,再重建 Pod、重新建立 port-forward,并提交 /api/persistence-verify。需要资源曲线时,安装示例目录中的采集对象与看板,然后提交 /api/resource-run。渲染器不修改 CNI、宿主机运行时或集群现有监控 release。

实验页和采集端口共用一个 Service,只用于受控测试环境;不要直接作为面向多个租户的公网入口。生产平台还需实现调用认证、任务配额、生命周期所有权和故障后的清理协调。

对 Agent 应用而言,这轮验证已经打通了“启动独立执行环境—运行代码—保存磁盘—重建后再次使用”的链路。继续扩展时,重点应放在每实例资源治理、任务恢复语义和监控正确性,而不是把一次 start() 成功当作整个平台的验收。