一句话结论:kubelet 不直接创建容器,它只通过 CRI(Container Runtime Interface)下达指令;真正干活的是高级运行时(containerd / CRI-O)和低级运行时(runc / kata-agent / runsc)。沙盒选型的本质,就是在决定「谁来接 kubelet 的 CRI 调用」以及「容器进程最终跑在什么隔离边界里」。对具身智能公司而言,仿真训练、Agent 工具调用、强隔离 VM、真机设备接入这四类负载对应完全不同的运行时组合——不存在一个「万能沙盒」能同时满足。

这篇是 具身智能仿真场景的沙盒选型 的下层视角。上篇回答「该选哪个沙盒」,这篇回答「沙盒插在运行时栈的哪一层、跟 kubelet 怎么耦合」。

本文的架构描述逐条核对自 Kubernetes 官方文档、containerd 源码与文档、Kata/gVisor/Firecracker 官方文档与仓库。核对中发现原稿有十余处需要更正,已在文中以 ⚠️ 标注。


一、先立骨架:kubelet 之下的运行时栈分层

Kubernetes 的容器运行时体系是明确的两层结构,这个分层决定了后面所有场景的讨论框架:

  • 高级运行时:对接 kubelet 的 CRI gRPC 接口,负责镜像管理、容器生命周期编排、存储快照、网络(CNI 调用)。代表是 containerd(内部的 cri 插件)和 CRI-O。
  • 低级运行时:高级运行时通过 OCI Runtime Spec 调用它,负责真正创建/销毁容器进程。传统上是 runc(namespace + cgroups + seccomp),沙盒场景下则可能是 Kata(VM 内的 kata-agent)、gVisor 的 runsc、或 Firecracker 的 guest agent。

说明:K8s 官方文档本身不使用「高级/低级运行时」这两个词,该分层是 CNCF 生态的通行表述——「Low-level Container Runtime: OCI-compliant implementations that can receive a runnable filesystem (rootfs) and a configuration file (config.json) to execute isolated processes… such as runc, crun, youki, gvisor, and kata. High-level Container Runtime: responsible for the transport and management of container images, unpacking the image, and passing it off to the low-level runtime…」

kubelet 与 containerd 的协作流程(对照 containerd 官方 CRI 架构文档):

  1. kubelet 通过 CRI RuntimeService 调 containerd 的 cri 插件创建 Pod sandbox;
  2. cri 插件创建网络命名空间并调 CNI 配置网络;
  3. cri 插件创建并启动 pause 容器(sandbox 容器);
  4. kubelet 通过 CRI ImageService 拉取业务镜像;
  5. kubelet 再调 cri 在 Pod 内创建并启动业务容器。

⚠️ 两处需要精确化:

  1. CNI 不是 kubelet 调的,是 CRI 实现(containerd 的 cri 插件)调的。 K8s 的 CRI 网络规范原文:「Kubelet expects the runtime shim to manage pod’s network life cycle… RunPodSandbox must set up pod’s network.」
  2. pause 容器属于 sandbox 创建阶段,不是之后单独的一步。 默认实现里 sandbox 就是 pause 容器,它在 RunPodSandbox 内完成。

containerd 与低级运行时之间是 shim v2 协议:containerd 不直接 fork/exec 运行时引擎,而是通过长驻 shim 进程的 ttRPC socket 下发指令。

⚠️ 更正(这条原稿的对比不准确):原稿写「containerd 为每个 pod sandbox 启动一个 shim 进程,通过 socket 长连接,而不是每次操作 fork 一个 runtime 二进制」。

两个问题:

  1. 「每个 pod sandbox 一个 shim」是 io.containerd.runc.v2 这个 shim 实现的选择,不是 containerd 的强制语义。 containerd 官方原文:「containerd does not know or care about whether the shim to container relationship is one-to-one, or one-to-many. It is entirely up to the shim to decide.」而通用 Runtime v2 模型的表述是「containerd launches a shim process for each container」。是 runc.v2 shim 主动按 io.kubernetes.cri.sandbox-id 标签聚合,才让同一个 Pod 的所有容器共用一个 shim。
  2. 「不再 fork runtime 二进制」不成立——长驻 shim 本身在每次 create/start/stop 时仍会 exec runc。准确的表述是「containerd 不直接调用运行时引擎,而是通过长驻 shim 的 ttRPC socket 下发指令」。

这个分层的关键含义是:沙盒技术插在哪一层,决定了它跟 kubelet 的耦合方式。gVisor 和 Kata 插在 shim 层(containerd 换个 runtime handler 就能切换);Firecracker 可以走 firecracker-containerd 的 control plugin 路径,也可以走独立 shim 路径;而 AgentENV / CubeSandbox 这类自带控制平面的系统干脆绕过 kubelet,自建调度。


二、场景一:裸金属容器仿真训练

适用场景:Isaac Lab / MuJoCo Warp / Genesis 大规模 RL rollout,GPU 密集,容器内直接跑 CUDA / PhysX / RT Core 负载,不需要 VM 级隔离。

  • 高级运行时:containerd(标准配置),配 nydus-snapshotter 做镜像按需加载;
  • 低级运行时:runc(io.containerd.runc.v2 shim)。

GPU 不走虚拟化,通过 DRA driver 或 NVIDIA Device Plugin 注册到 kubelet 的设备管理器。DRA(K8s 1.34 GA,feature gate 在 1.35 起 locked)比旧 Device Plugin 多了拓扑感知:

DRA 的完整链路:scheduler 的 DynamicResources 插件把 Pod 匹配到 ResourceSlice 描述的硬件上并写回 ResourceClaim → kubelet 通过 gRPC 调 driver 的 NodePrepareResources → driver 返回 CDI device ID → containerd 消费。

⚠️ 更正一处措辞:原稿写「NodePrepareResources 生成 CDI spec」。实际上 driver 返回的是 CDI device ID(NodePrepareResourcesResponse 里的 repeated string cdi_device_ids);CDI spec 文件是厂商 driver 的 spec 生成器写进 /etc/cdi 或 /var/run/cdi 的,containerd 读取并注入。两者不是一回事。

CDI(Container Device Interface) 的全称与归属核实无误,仓库在 CNCF 的 cncf-tags 组织下。但有一个版本差异要知道:

⚠️ containerd 1.7 默认 enable_cdi = false(需手动开),2.0 起 CDI 默认启用——所以 2.0 的 config 参考里已经不再出现这个开关。跨版本排查设备注入问题时会踩到。

几个关键细节:

  • HAMi 提利用率的路径是替换节点上的 NVIDIA device plugin(它本身就是 k8s-device-plugin 的 fork)。官方前置条件文档要求:把 GPU Operator 的 devicePlugin.enabled=false、关掉节点上其他 NVIDIA device plugin,避免 nvidia.com/gpu 被重复注册。切分粒度是 nvidia.com/gpumem(MiB)和 nvidia.com/gpucores(百分比),由 libvgpu.so 在 CUDA API 层做容器内软隔离。但时间片无故障隔离,一个 rollout OOM 可能波及共享同卡的其他副本。
  • nydus-snapshotter 是 containerd 的外部插件进程(containerd-nydus-grpc),以 [proxy_plugins] 的 type = "snapshot" 身份接入,实现的是 remote snapshotter 接口。它的完整机制见 containerd Snapshotter 接口深度解析。
  • 断点续跑靠仿真器自身的 save_state(Isaac Lab、MuJoCo、Genesis 都支持),不靠虚拟化层快照——GPU 状态无法被 hypervisor 快照,这是仿真沙盒与通用沙盒最本质的差别。

三、场景二:Agent 工具调用沙盒

适用场景:具身 Agent 训练中的工具调用(代码执行、文件操作、API 调用),CPU 密集、生命周期短(秒到分钟)、高并发 fanout、不需要 GPU。

  • 高级运行时:containerd,配 firecracker 或 gvisor runtime handler,由 Kubernetes SIG 的 agent-sandbox(Sandbox CRD + controller + webhook)统一管理生命周期与策略;
  • 低级运行时:按隔离强度分三档。

(1)gVisor runsc(containerd-shim-runsc-v1):Sentry 作为用户态内核拦截系统调用,Gofer 代理文件系统访问。开销在 syscall 密集型负载明显——官方原文:「This cost will principally impact applications that are system call bound」,纯 CPU 负载几乎无开销。部署最简单。

⚠️ 更正:原稿写「Sentry 与 Gofer 之间用 9P 协议」。现行协议是 LISAFS,9P 是历史实现。gVisor 官方文档:「Gofer instances communicate with their respective sentry using the LISAFS protocol.」仓库 pkg/lisafs/README.md 解释了换的原因:「Historically, gVisor used a custom extension of the 9P2000.L protocol… 9P proved to be chatty in certain situations, inducing a lot of RPCs. LISAFS aims to be more economical.」(主仓库已无 pkg/9p 目录。)

(2)独立 firecracker-shim(社区实现 PipeOpsHQ/firecracker-shim):无中间 daemon,containerd 直接调 shim,shim 启动 Firecracker VMM,VM 内 fc-agent 再调 runc 跑容器。标准 CNI 可用,VM 池化预热后冷启动 <50ms,单 VM 内存开销 64—128MB。

这一条的组件名(fc-agent + 调 runc)核实属实——README 架构图是 fc-agent → runc → container,源码 cmd/fc-agent/main.go 注释 “manages containers using runc”,常量 runcBinary="/usr/bin/runc"。

⚠️ 但项目本身要打个折扣:该仓库 2026-01 创建、约 8 star,很新很小,可以叫「社区实现」,不宜当成熟方案推荐。而且它自己文档里内存开销有 64—128MB 与 ~10MB 两个口径并存(前者是整机 VM 占用、后者是相对 runc 的增量),引用时要注明。

(3)firecracker-containerd(AWS 官方):shim + 编译进 containerd 的 control plugin。

⚠️ 更正:原稿写它是「daemon 架构,容器管理通过单独的 gRPC 接口而非标准 OCI shim」。实际是:

  • 它是「A control plugin compiled in to the containerd binary」(需要专门构建的 containerd 二进制)+ 「A runtime implementing both the control API and the V2 runtime API, implemented as an out-of-process shim communicating over ttrpc」;
  • 容器/任务生命周期走的正是标准 V2 runtime(shim)模型;那个 control plugin(FC-Control)只管 VM 级 API(StartVM 等),不接管容器管理;
  • 通信是 ttrpc 不是 gRPC。

维护状态也要说准:未归档、未标记废弃(GitHub API archived: false,2026-10 仍有提交),近一年以依赖升级为主、无重大功能开发——是「明显放缓」,不是「停止维护」。

这个场景最容易混淆的一点:AgentENV 和 CubeSandbox 不在这个拓扑里。它们自带控制平面——CubeSandbox 的 CubeMaster 做集群调度、Cubelet 管节点上沙盒实例的生命周期、CubeShim 实现 containerd shim v2 接口做 VM 启动与 vsock 通信——K8s 只承载外层 Pod,内层 microVM 的调度不走 kubelet 的 CRI。

⚠️ 更正一处技术基座:原稿写「CubeHypervisor 基于 Cloud Hypervisor」。官方原文是「CubeHypervisor… The lightweight VMM built on RustVMM + KVM」。Cloud Hypervisor 只出现在致谢里(“Special thanks to Cloud Hypervisor, Kata Containers, virtiofsd…”)——是灵感血缘,不是直接基座。

kubelet 对内层 microVM 是不感知的——它只看到外层 Pod 的 requests/limits,内层 VM 的内存、VMM 开销、文件缓存、快照写入都不在 K8s 计量里,容量规划需要单独评估。


四、场景三:Kata VM 强隔离(含 GPU 直通)

适用场景:多租户不可信负载、需要 VM 级内核隔离、或需要 VFIO 设备直通(整卡 GPU、RDMA NIC)的场景。

  • 高级运行时:containerd(或 CRI-O),配 kata runtime handler;
  • 低级运行时:kata-runtime(shim v2 形态的 containerd-shim-kata-v2),其下可选 QEMU / Cloud Hypervisor / Firecracker / Dragonball 作为 VMM 后端。

Kata 2.x 相对 1.x 的架构变化是本质性的:不再有独立的 kata-proxy 和 kata-shim 进程,而是每个 pod sandbox(VM)一个 containerd-shim-kata-v2 进程。官方 history 文档的对比表:

1.x 2.x
shim 进程 multiple per container 1 per VM(hosting any number of containers)
proxy 进程 1 per container connection 0
独立 kata-shim 1 0

这个 per-sandbox/VM 粒度意味着:一个 Pod 里的所有容器共享同一个 microVM 和同一个内核。

补两个精度:shim 实现的是 containerd shimv2 API(与 OCI runtime CLI「similar… in terms of the way the container lifecycle is split into different verbs」,但不完全等同 OCI runtime spec);与 guest 内 kata-agent 的通信是 ttRPC over VSOCK,I/O 流也走这条协议。

存储走 virtio-fs,是 virtio-9p 的替代——官方原文:「As of the 2.0 release of Kata Containers, virtio-fs is the default filesystem sharing mechanism… provides significant performance and POSIX compliance improvements over 9pfs.」virtiofsd 守护进程在 host 侧运行,每个 VM 一个。(runtime-rs 里 9pfs 已被移除。)

关键细节:

  • Kata 与 GPU 的关系是「两件可分离的事」:默认 QEMU + KVM 全 virtio 设备模型不带 GPU;开启 VFIO 直通后每 VM 独占一张物理卡,密度降到一卡一 guest,但拿到接近裸金属的性能。
  • Kata 硬性要求 KVM,不支持纯软件翻译(TCG)——官方:「All supported hypervisors use KVM as the underlying hardware virtualization interface on Linux」;「Kata Containers requires nested virtualization or bare metal」。云 VM 上嵌套虚拟化不可用时,AgentENV 提供 PVM(shadow paging)模式作为替代(标记为 EXPERIMENTAL,需自装 PVM 内核),但那已经离开 Kata 路径。
  • 选 VMM 后端的权衡:

⚠️ 更正:原稿写「CLH 是当前推荐默认」。官方默认/推荐是 Dragonball——virtualization.md 原文:「For Most Users: Use the default Dragonball VMM with the Kata Containers Rust runtime.」配置表里标 configuration-dragonball.toml (default)。Dragonball 是「a special hypervisor created by the Ant Group that runs in the same process as the Rust-based containerd shim」——零 IPC 开销是它的卖点。

后端 定位
Dragonball 官方默认,Kata 内置 Rust VMM,与 shim 同进程
QEMU 功能最全(VFIO、热插拔、多架构);官方称它是 NVIDIA GPU 支持最好的后端
Cloud Hypervisor 性能与兼容性平衡
Firecracker 最轻,但设备模型极简

用 Kata 跑 Firecracker 后端是官方支持的,但有明确限制:无 virtio-fs、无设备热插拔、无 VFIO/直通、无 CPU/内存热插拔、CRI API 支持有限。所以「用 Kata 跑 Firecracker」得到的是 K8s 原生集成,失去的正是 Firecracker 设备极简性带来的那部分优势;反之直接用独立 firecracker-shim 得到轻量但失去 Kata 的多 hypervisor 抽象——这是两条不同的集成路径,不是谁替代谁。

⚠️ 更正一处硬件支持表述:原稿写「Kata 官方硬件支持列表明确包含 Nvidia GPU、FPGA、RDMA、SR-IOV」。核实结果:GPU / SR-IOV / RDMA 有据(设计需求文档:「The Kata Containers runtime MUST provide containers with secure pass through for generic devices such as GPUs, SRIOV, RDMA, QAT」),但 FPGA 无据——全仓库检索只在某内核配置文件里出现过一次 # CONFIG_FPGA is not set,没有任何官方文档把它列为支持。而且官方并没有一份统一的「硬件支持列表」文档,出处是设计需求文档。

Kata、gVisor、runc 可以在同一集群以不同 RuntimeClass 共存(官方专门有 “Mixing VM based and namespace based runtimes” 一节),Pod spec 里 runtimeClassName 一个字段切换。RuntimeClass 机制 1.20 GA。


五、场景四:真机设备接入

适用场景:机械臂、遥操作设备、摄像头等真机硬件的调度与数据回流,以及数据工厂流水线的高吞吐网络。

  • 高级运行时:containerd 标准配置;
  • 低级运行时:runc(不需要沙盒增强,但要配合设备注入)。

真机接入不走 RuntimeClass,走 kubelet 的设备与网络扩展点:

  • 设备层:自定义 Device Plugin(或 DRA driver)把机械臂/CAN 总线/摄像头注册为 Extended Resource 或 ResourceClaim。旧 Device Plugin 只能静态整数分配;DRA 支持按属性匹配、拓扑对齐、与其他 DRA 资源(GPU、高速 NIC)联合调度。dra-driver-cpu 这类项目甚至用 DRA 替代 kubelet 内置的 CPUManager——官方明确「The kubelet’s CPUManager and this DRA driver are mutually incompatible, and only one can be enabled at a time on any given node」。
  • 网络层:sriov-network-device-plugin 发现并注册 SR-IOV VF / PF / auxiliary(SF) 三类设备;Multus 作为 meta-plugin 给 Pod 挂多个网络接口。

关键细节:

  • Multus 不是独立 CNI,是 CNI 多路复用器——官方原文:「Multus acting as a “meta-plugin”, a CNI plugin that can call multiple other CNI plugins」,且要求先装一个默认网络 CNI。

⚠️ SR-IOV 这条要分模式说(原稿说得太绝对)。SR-IOV CNI 有两种驱动模式:

模式 VF 绑定 是否绕过内核网络栈
kernel driver(默认/最小可用配置) 内核驱动,进入 Pod netns 作为内核网卡 否,走内核栈
DPDK userspace(uio/vfio) vfio-pci / uio,Pod 拿到 PCIe 设备节点 是,数据面在用户态

所以「绕过内核网络栈、Pod 直接拿到 VF 的 PCIe 设备节点」只在 DPDK/vfio 模式成立。另外「延迟微秒级」是通行描述,但我没找到官方量化的原文,宜标为量级参考。

NetworkPolicy 那条也要补:标准 K8s NetworkPolicy 由主 CNI 在默认接口(eth0)上实施,secondary 网络(含 SR-IOV)确实不在标准 NetworkPolicy 覆盖范围内;但不是「完全管不到」——可以用上游的 MultiNetworkPolicy CRD 单独给 secondary 接口加策略。

  • kubelet 的 Topology Manager 在这里发挥作用:从各 Hint Provider 收集 NUMA hint 并对齐,scope 取 container(默认)或 pod,policy 取 none(默认)/ best-effort / restricted / single-numa-node。对齐对象是 CPU(经 CPU Manager)、内存/大页(Memory Manager)、设备(Device Manager)。GPU 和 SR-IOV NIC 不是 Topology Manager 直接识别,而是其 device plugin 在 Allocate 里返回 TopologyInfo、经 Device Manager 参与对齐。

六、全场景对比

维度 裸金属容器仿真 Agent 工具调用沙盒 Kata VM 强隔离 真机设备接入
高级运行时 containerd(标准) containerd + agent-sandbox controller containerd(RuntimeClass=kata) containerd(标准)
低级运行时 runc runsc / fc-agent+runc / kata-agent kata-runtime(shim v2,per VM) runc
与 kubelet 耦合点 CRI + Device Manager + CDI CRI + RuntimeClass CRI + RuntimeClass + VFIO CRI + Device Manager + Multus
GPU 路径 物理直访 / HAMi 时间片 / DRA 切片 无(CPU 密集) VFIO 整卡直通 不涉及
沙盒边界 namespace + cgroups + seccomp 用户态内核 / microVM microVM + 独立内核 namespace + cgroups
存储增强 nydus-snapshotter 按需加载 guest 内文件系统 / Kata virtio-fs virtio-fs(virtiofsd 在 host) 标准 overlayfs
启动延迟 秒级 <50ms(VM 池化预热后) 分钟级 秒级
适合负载 RL 仿真、CUDA 训练 代码执行、工具调用 多租户、不可信代码 真机调度、数据流水线
不适合 强隔离多租户 GPU 负载 高密度 GPU 仿真 强隔离

七、选型决策要点

第一,不要找一个万能沙盒,找一组 RuntimeClass。 上面四个场景可以在同一集群共存:仿真 Pod 用默认 runtime、Agent 工具调用 Pod 用 firecracker 或 gvisor、多租户 Pod 用 kata、真机 Pod 用默认 runtime + 自定义 device plugin。K8s 的 RuntimeClass 机制(1.20 GA)就是为这种混合负载设计的。

第二,区分「标准 RuntimeClass 路径」与「自带控制平面路径」。 firecracker-containerd、独立 firecracker-shim、Kata+Firecracker 都走 kubelet → CRI → containerd → shim 链路,K8s 调度、NetworkPolicy、metrics 全部原生生效;AgentENV / CubeSandbox 这类系统自带 Master/Scheduler/Node 组件,K8s 只承载外层 Pod,内层 microVM 的生命周期、容量、网络策略都要独立建设。前者运维负担小但灵活性受限,后者需要平台团队持续投入——没有银弹。

第三,GPU 负载先定 GPU 路径,再定沙盒路径。 仿真主流是裸金属容器 + 设备插件直访;强隔离多租户才考虑 VFIO 直通(Kata);MIG-backed vGPU 是「想保留 VM 形态又要硬件隔离」的折中;virtio-gpu(Venus/VirGL)只翻译图形 API,不覆盖 CUDA,仿真栈直接排除。GPU 路径定了,沙盒技术选型的可行集就自动收敛了——这是具身场景与通用容器场景最大的不同。

第四,快照与断点续跑是分层解决的。 仿真 rollout 的状态恢复靠仿真器 save_state(GPU 状态 hypervisor 快照不了);Agent 工具调用沙盒的暂停恢复靠 microVM snapshot(CPU + 内存状态)。两者的快照机制不通用,混用会掉坑。

一句话总结:kubelet 之下,高级运行时管「怎么调度和编排」,低级运行时管「跑在什么隔离边界里」;具身智能公司的沙盒架构不是选一个技术,而是按负载类型把 runc、gVisor、Firecracker、Kata 各归其位,用 RuntimeClass 和 Device Plugin 把它们编成一个整体的调度拓扑。


相关阅读

核实说明:本文架构描述逐条核对自一手来源——Kubernetes 官方文档(CRI、RuntimeClass、Topology Manager、Device Plugin、DRA)与 feature gate 定义、containerd 官方文档与源码(docs/runtime-v2.md、docs/sandbox-api.md、cri/docs/architecture.md、docs/containerd-2.0.md)、CRI proto、CDI 规范仓库、Kata Containers 官方仓库(docs/design/architecture/history.md、virtualization.md、hypervisors.md、storage.md、各 how-to)、gVisor 官方文档与 pkg/lisafs/README.md、Firecracker 官方仓库与 FAQ、AWS firecracker-containerd 仓库、HAMi / nydus-snapshotter / Multus / SR-IOV CNI / sriov-network-device-plugin / dra-driver-cpu / CubeSandbox 各项目 README。

原稿更正汇总:CNI 的调用方(containerd 非 kubelet)、pause 容器归属阶段、shim 的 per-pod 粒度是 runc.v2 的实现选择而非 containerd 语义、「不再 fork runtime 二进制」不准确(shim 仍 exec runc)、DRA driver 返回的是 CDI device ID 而非「生成 CDI spec」、CDI 默认值随 containerd 版本变化、gVisor 的 Sentry↔Gofer 协议是 LISAFS 非 9P、firecracker-containerd 的架构描述(shim + control plugin,ttrpc 非 gRPC,容器走标准 V2 runtime)、Kata 默认 VMM 是 Dragonball 非 Cloud Hypervisor、Kata 硬件支持列表中的 FPGA 无据、SR-IOV「绕过内核网络栈」仅 DPDK/vfio 模式成立、NetworkPolicy 可用 MultiNetworkPolicy 覆盖、CubeHypervisor 是 RustVMM + KVM 非「基于 Cloud Hypervisor」。另外为 PipeOpsHQ/firecracker-shim 标注了项目成熟度与文档口径不一致。