250 个 actor,轮流住进 8 个 Pod 左边是一个装着 8 个槽位的 WorkerPool,几个槽里住着睁着眼的蓝色 actor;右边铺开一大片闭着眼睡觉的 actor,望不到头,其中一个刚被叫醒,沿着一道弧线跳进了 WorkerPool 的空槽。

Agent Substrate 是 GitHub 上 agent-substrate 组织下的一个开源项目,代码的版权头写的是 Google LLC。它的 README 里有一段演示视频,标题下面只有一句话:一个 Agent Substrate 集群,把大约 250 个有状态的 actor 复用在 8 个物理 Pod 上1。

照着 README 在本地 kind 集群里装好以后,创建一个 actor、给它发请求,只要这几行:

Terminal window
kubectl ate create actor my-counter-1 -a ate-demo-counter --template counter
kubectl port-forward -n ate-system svc/atenet-router 8000:80
curl -X POST -H "ate-target-actor: ate-demo-counter/my-counter-1" -i http://localhost:8000/

kubectl ate 是个 kubectl 插件,可 create actor 创建出来的东西,用 kubectl get pods 是看不到的。这个计数器演示的 WorkerPool 只声明了 3 个 Pod,actor 却可以建很多个。那 actor 到底是什么,不跑的时候它在哪?说它“有状态”,计数器数到一半被挂起,醒来还接着数,这听上去和 E2B 把沙箱拍成快照再恢复是一回事,两者又是什么关系?

我们先看清它是什么、长什么样,再分别拿它和 Kubernetes、E2B 对照。

一、Agent Substrate 是什么

智能体的负载特点

Agent Substrate 的架构文档从一个观察开始:智能体这类负载非常“突发”,大部分时间在等,等模型返回、等工具跑完、等人回复,真正干活的时间很短,等待的时间却没有上限。它们又经常执行不可信的代码,只能放进沙箱,一个用户一个实例,于是数量非常多2。

如果每个智能体是一个常驻的 Pod,大部分 Pod 在大部分时间里什么都不做,却一直占着内存和 Pod 配额。最直觉的办法是空闲时把 Pod 删掉、用到时再建,可 Kubernetes 建一个 Pod 要经过调度、拉镜像、启动应用,社区自己的目标是 p99 不超过 5 秒,这还不算拉镜像3。对一个只跑几百毫秒就又开始等待的智能体来说,这个延迟太长了。

actor 和 worker

Agent Substrate 的回答是把“智能体”和“Pod”拆开。文档里把智能体这类负载叫做 actor(不一定真是 AI 智能体,只要是“大部分时间空闲、有状态”的负载都算),把预先拉起、等着接活的 Pod 叫做 worker。actor 空闲时打一个快照,把 worker 腾出来;请求来了,再把快照恢复到任意一个空闲的 worker 里2。actor 多、worker 少,这就是开头那个“250 个跑在 8 个上”的来历。

能这样做,前提是沙箱支持挂起和恢复。Agent Substrate 要求每个 actor 都必须跑在沙箱里,明确不支持普通 Linux 容器1。默认的沙箱是 gVisor:它是 Google 做的一个用户态内核,拦下应用的系统调用、在用户态自己实现,应用碰不到宿主的内核;它的运行时叫 runsc,自带 checkpoint 和 restore,能把整个进程树连同内存存成文件再恢复出来。另一种选择是 Kata Containers 加 Cloud Hypervisor 的轻量虚拟机2。

项目对自己的定位是“没什么主见的运行时”,不是智能体 SDK:它跑的是普通的 OCI 镜像,镜像里是什么框架都行。Google 的 Agent Executor 和 CNCF 沙箱项目 kagent 都把它用作底座1。项目还在 1.0 之前,架构文档开头就写着“其中很多部分还是设想,尚未实现”2。

整体架构

装好以后,集群里多出来的东西可以分成四块,画在一起是这样:

Agent Substrate 的整体架构 控制面有 atenet-router、ate-api-server 和存 ActorTemplate、Actor、Worker 记录的 PostgreSQL;Kubernetes API 里只有 WorkerPool、Deployment、SandboxConfig。节点上有三个 worker Pod,各自运行 ateworker-gvisor,其中两个 Pod 里有 runsc 沙箱分别跑着 my-counter-1 和 my-counter-2,一个空闲;atelet 负责在节点上恢复沙箱,并和对象存储之间搬运快照。router 经 atunnel 隧道把请求送进 my-counter-1 所在的 Pod。 Substrate 控制面 · ate-system Kubernetes API 节点 · node-a 对象存储 · GCS / S3 Restore 隧道 · atunnel :8443 atenet-router Envoy · ext_proc ate-target-actor: …/my-counter-1 ate-api-server 生命周期 · 调度 PostgreSQL template counter actor my-counter-1 worker ×3 WorkerPool counter Deployment ×3 SandboxConfig Pod worker-1 ateworker-gvisor 空闲 · 等分配 Pod worker-2 ateworker-gvisor runsc 沙箱 my-counter-1 Pod worker-3 ateworker-gvisor runsc 沙箱 my-counter-2 atelet DaemonSet · 拉镜像 · 组 OCI bundle · 搬快照 Pod 由 Kubernetes 按 WorkerPool 拉起;actor 跑在 Pod 里 ateworker 起的沙箱中。 不在跑的 actor 不占 Pod,只剩数据库里的一行和存储里的快照。 挂起的 actor 状态都存在这里 上传 / 下载快照 快照 counter · golden my-counter-1 my-counter-3 …
图 1 · Kubernetes 里只有 worker 的池子,actor 的记录和快照都在 Substrate 自己手里

控制面在 ate-system 命名空间里。ate-api-server 是核心,它对外提供 gRPC 接口,管 actor 的创建、挂起、恢复和删除,也负责决定 actor 放进哪个 worker;状态全部存在 PostgreSQL 里。atenet-router 是流量入口,一个带外部处理器(ext_proc)的 Envoy:它读请求头 ate-target-actor: <atespace>/<actor>,发现目标 actor 没在跑,就先让控制面把它恢复起来,再把请求转过去2。

Kubernetes API 里只放三类东西。WorkerPool 声明一个 worker Pod 的池子,被 atecontroller 调和成一个普通的 Deployment;SandboxConfig 记录沙箱用哪个版本的 runsc 或虚拟机内核。描述 actor 长什么样的 ActorTemplate(镜像、资源、快照配置),和 Actor、Worker 这些记录一样,都不是 Kubernetes 对象,而是存在 PostgreSQL 里2。

节点上,每个 worker Pod 里跑着一个 ateworker,它替节点上的 atelet 调用 runsc,在 Pod 内部建出、挂起、恢复一个个沙箱,还带着一个 atunnel,接住 router 打过来的隧道,把请求转进沙箱。atelet 以 DaemonSet 的形式每个节点一个,负责拉镜像、组装 OCI bundle,以及在节点和对象存储之间搬运快照2。

对象存储里放着快照。创建一个 ActorTemplate 时,系统会临时冷启动一次这个模板,跑到就绪后拍一个快照,叫 golden snapshot;之后这个模板的每个 actor 第一次被唤醒,都从它恢复。actor 每次被挂起,又会存下它自己的最新快照4。PostgreSQL 和对象存储(GCS 或兼容 S3 的存储)都由部署的人自己准备,本地用 kind 安装时会顺带装上 PostgreSQL 和 rustfs1。

所以 kubectl ate create actor 只是往 PostgreSQL 里写了一行,状态是“已挂起”,指向模板的 golden snapshot,不占任何 worker2。actor 真正跑起来,要等第一个请求到达。不过在讲唤醒之前,先看看这份 golden snapshot 是从哪里来的:模板里那个镜像,是怎么变成 gVisor 沙箱里的进程的?

二、镜像与构建

模板里写的是一个普通镜像

上一章说 actor 都跑在 gVisor 沙箱里。那模板里的镜像,要不要专门为 gVisor 准备?看仓库里的演示就知道。让几个 Claude Code 轮流复用 worker 的那个演示,镜像的 Dockerfile 是这样的(省略了版权头和两行 ENV)5:

FROM node:20-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code@latest
COPY run.sh /run.sh
RUN chmod +x /run.sh
ENTRYPOINT ["/run.sh"]

这就是一个普通的 Debian 系镜像,拿去 docker run 也能跑。ActorTemplate 里写的,也只是镜像地址、启动命令、资源和快照配置,和 Pod 模板差不多4。那 gVisor 在哪里?它和 Ubuntu、CentOS 是同一类东西吗?

发行版、镜像和内核

要回答这个问题,得先把三样东西分开。装在一台机器上的 Ubuntu 或 CentOS 是一个完整的发行版:底下是 Linux 内核,上面是 glibc、包管理器(apt 或 yum)、systemd 这些用户态的东西。ubuntu:24.04、node:20-slim 这类容器镜像只打包了用户态,里面没有内核,通常也没有 systemd。镜像要跑起来,总得借一个内核。

gVisor 不是镜像,也不是发行版,它就是那个被借来的“内核”。它是一个用 Go 写、跑在用户态的应用内核,实现了一套类 Linux 的系统调用接口;镜像里的进程发出系统调用,由 gVisor 的 Sentry 接住、自己处理,不交给宿主内核6。镜像面对的仍然像是一个 Linux 内核,所以同一个 node:20-slim 不用改就能跑。

Docker 和 E2B 正好是另外两种借法。把同一个镜像放进三者里看:

同一个镜像,三种借内核的方式 三列分别是 Docker(runc)、Agent Substrate(gVisor)和 E2B(Firecracker),每列自上而下是进程 /run.sh、镜像 node:20-slim 的用户态,再往下是内核。Docker 没有自己的内核,系统调用直接交给宿主内核;Substrate 由 gVisor 的 Sentry 在用户态实现系统调用;E2B 先给镜像追加 envd、systemd、sshd,再在 Firecracker 虚拟机里跑自己的 Linux 内核 vmlinux-6.1.158。三列底下是同一个宿主 Linux 内核。 Docker · runc Agent Substrate · gVisor E2B · Firecracker 进程 /run.sh 进程 /run.sh 进程 /run.sh 镜像 node:20-slim 用户态:glibc · apt · npm 镜像 node:20-slim 用户态:glibc · apt · npm 镜像 node:20-slim 用户态:glibc · apt · npm 镜像原样使用,不加 init 镜像原样使用,不加 init E2B 追加的一层 envd · systemd · sshd 镜像里没有内核 系统调用直接交给宿主内核 Sentry · gVisor 应用内核 Go 写的,在用户态实现系统调用 客户机 Linux 内核 vmlinux-6.1.158 虚线:这一层并不存在 宿主只看到 Sentry 发出的调用 中间隔着 KVM 硬件虚拟化 宿主 Linux 内核(每台节点只有这一个)
图 2 · 同一个镜像只有用户态:Docker 借宿主的内核,gVisor 在用户态顶替内核,E2B 带上一个真正的客户机内核

Docker(底下是 runc)不借别的内核,容器里的进程直接对宿主内核发系统调用,和这台机器上所有容器共用一个内核,这在《E2B 和 Docker 是什么关系》里做过实验。E2B 走的是另一头:每个沙箱是一台 Firecracker 虚拟机,里面跑着 E2B 自己编的 Linux 内核7。gVisor 夹在中间:它像虚拟机一样给每个沙箱一个独立的内核,却不模拟硬件,本身就是宿主上的一组普通进程,文档说它可以看成“合在一起的客户机内核和虚拟机监控器”6。

代价是兼容性。gVisor 只实现了 Linux 系统调用的一个子集,缺的大多是已有调用的替代版本,比如 io_uring;每个版本都会跑 Python、Java、Node.js、PHP、Go 的回归测试,常见的语言运行时基本都能用8。依赖特殊内核特性的程序,只有放进 gVisor 实际跑一遍才知道。Substrate 也支持另一族沙箱,Kata 加 Cloud Hypervisor 的轻量虚拟机,选了它,就和 E2B 一样有一个真正的客户机内核,节点也得暴露 /dev/kvm29。

Substrate:镜像原样跑在 gVisor 上

既然镜像不用改,Substrate 也就没有自己的构建服务。镜像用 docker build 或者 ko 在你自己的机器或 CI 里构建,推到镜像仓库,Substrate 只在用到的时候去拉。从模板到一份可以恢复的快照,两边的流程放在一起看:

从镜像到第一份快照 上一行是 Agent Substrate 创建 ActorTemplate 时的流程:镜像在用户自己的机器或 CI 里用 docker build 或 ko 构建并推到仓库(这一步不在 Substrate 里);atelet 拉镜像、各层解进节点缓存、overlayfs 叠成 rootfs;临时的 golden actor 原样运行镜像,等 wakeupProbe 通过或再等 20 秒;按 preferredFidelity 挂起并存为 Tag,新 actor 从它恢复。下一行是 E2B 构建模板:Dockerfile 在 SDK 里解析成步骤;template-manager 拉镜像、追加 envd 层、压平进 ext4、装上 systemd 和 sshd;在 Firecracker 虚拟机里经 envd 执行 RUN,开机进 systemd,跑 start 命令到就绪;暂停并拍下 memfile 和 rootfs 快照,之后创建沙箱就是恢复它,约 80 毫秒。 Agent Substrate · 创建 ActorTemplate 时 虚线:这一步不在 Substrate 里做 E2B · 构建模板时 构建镜像 docker build / ko 在你自己的机器或 CI 推到镜像仓库 atelet 拉镜像 各层解进节点缓存 overlayfs 叠成 rootfs 写入进私有 upper golden actor 镜像原样,不加 init wakeupProbe 通过 没有探针再等 20 秒 挂起,存为 Tag preferredFidelity 临时 actor 随后删掉 新 actor 从这里恢复 模板写成代码 Dockerfile 在 SDK 里 解析成构建步骤 交给 E2B 去构建 template-manager 拉镜像,加 envd 层 压平进 ext4 磁盘 装上 systemd、sshd Firecracker 里构建 RUN 经 envd 执行 开机进 systemd 跑 start 命令到就绪 暂停,拍快照 memfile + rootfs 创建沙箱 = 恢复它 约 80 毫秒
图 3 · 两边最后都是“跑到就绪、拍快照”;Substrate 不碰镜像,E2B 要先把镜像改造成一块能开机的磁盘

Substrate 这一行,拉镜像发生在节点上。atelet 用 go-containerregistry 直接从仓库拉,不经过 Docker 守护进程,各层解压进节点上的镜像缓存。它给每个 actor 准备一个 OCI bundle,却不往里面铺文件,只建好 rootfs、upper、work 三个空目录,再写一份叠层说明;actor 运行前,ateworker 用 overlayfs 把缓存里的镜像层作为只读的下层叠起来,actor 自己写的东西都落在它私有的 upper 里10。一台节点上的很多 actor 共用同一份镜像层,各写各的上层。这里用到了 overlayfs 在 Linux 6.5 才加入的 lowerdir+ 挂载选项,所以节点内核得是 6.5 以上11。

启动命令照搬 Pod 的规矩:模板写了 command,就覆盖镜像的 ENTRYPOINT,同时丢掉 CMD;只写 args,就只覆盖 CMD。环境变量以模板里的为先,镜像里的其次10。Substrate 不往镜像里加 init,也不装 systemd,沙箱里跑的就是镜像本来的程序。

接着是 golden snapshot。创建 ActorTemplate 时,Substrate 先建一个临时的 golden actor,像普通 actor 一样唤醒它,等它就绪:每个容器都声明了 wakeupProbe 的,探针通过就算就绪,否则再多等 20 秒。然后按模板的 preferredFidelity 把它挂起,把快照拷成一个以模板 UID 命名的 Tag,最后删掉这个临时 actor12。之后这个模板的新 actor 第一次醒来,都从这个 Tag 恢复,拉镜像和程序启动都省掉了。

E2B:先把镜像改造成一台能开机的机器

E2B 的沙箱有自己的内核,光有用户态不够,镜像得先变成一块能开机的磁盘,所以构建这件事它要自己来做。Dockerfile 在 SDK 里就被解析成一串模板步骤,交给 E2B 的构建服务执行13。构建服务同样用 go-containerregistry 拉镜像,在上面追加一层,放进 envd 等 E2B 自己的程序14;再把各层压平,整个拷进一块 ext4 磁盘15;然后按 /etc/os-release 认出发行版,给它装上 systemd、openssh-server 这些开机和登录要用的包16。Dockerfile 里的 RUN 不在 Docker 里执行,而是在一台真正开了机的 Firecracker 虚拟机里经 envd 执行。最后跑起模板的启动命令,等它就绪,暂停虚拟机拍下快照;之后创建沙箱就是恢复这份快照,官方的说法是约 80 毫秒17。

两边走到最后是同一个办法:跑到就绪,拍快照,以后都从快照起。差别在镜像经历了什么。在 Substrate 里,镜像原封不动,只是下面换了一个“内核”;在 E2B 里,镜像被补成了一台带内核、systemd 和 sshd 的小机器,更接近一台装了 Ubuntu 的虚拟机。和 Docker 放在一起:

同一个镜像,在三者里的待遇
DockerAgent Substrate(gVisor)E2B
内核宿主内核,和其他容器共用gVisor 的 Sentry,在用户态实现系统调用自己的 Linux 内核,跑在 Firecracker 虚拟机里
镜像怎么用按层解开,overlayfs 叠成根文件系统追加 envd 层,压平进一块 ext4 磁盘
init不加,直接跑镜像的程序装上 systemd,开机后再起 envd 和启动命令
启动命令镜像的 ENTRYPOINT、CMD按 Pod 规则,模板的 command 可以覆盖模板里另写 start 命令
谁来构建docker build 等工具,在自己的机器或 CI 里E2B 的构建服务,RUN 在虚拟机里执行
第一次启动冷启动从 golden snapshot 恢复从构建时的快照恢复
兼容性原生 Linux系统调用是子集,常见语言运行时都测过原生 Linux,里面还能再装 Docker

Docker 和 Substrate 对镜像的处理几乎一样,差别只在下面的内核;E2B 则把镜像当成一台机器的系统盘来装。

图 4 · Substrate 原样使用镜像、换掉内核;E2B 改造镜像、带上内核

镜像准备好了,golden snapshot 也拍好了,可新建的 actor 还是挂起状态。一个请求是怎么把它叫醒的?

三、actor 的唤醒与挂起

唤醒:一路同步到能用

请求到了 atenet-router,ext_proc 先把它停住,调用 ate-api-server 的 ResumeActor。如果 actor 已经在跑,这次调用直接返回它所在 worker 的地址,router 照着转发,这是绝大多数请求走的路。

actor 是挂起状态时,ResumeActor 要做一串事情:先在 PostgreSQL 里给这个 actor 拿一个租约,防止两个请求同时恢复它;找出它最新的一份完整快照;按模板的资源和 worker 选择器挑一个还有空位的 worker,用乐观并发写进数据库,抢不到就以 15 毫秒起步指数退避再试;然后让那个节点上的 atelet 取回快照,由 ateworker 执行 runsc restore;最后把 actor 标成运行中,带着 worker 的 IP 返回18。router 拿到这个地址,才经 atunnel 的 mTLS 隧道把停着的请求送进 actor 的 80 端口。

actor 的唤醒和挂起 唤醒:atenet-router 停住请求,ate-api-server 的 ResumeActor 依次拿租约、在 PostgreSQL 里抢 worker 槽位、让 atelet 取快照、执行 runsc restore、把 actor 标成 RUNNING,全程同步,返回后 router 才经隧道转发请求。挂起:SuspendActor 执行 runsc checkpoint,worker 立即释放,快照先留在节点本地并把 actor 钉在该节点,atelet 在后台上传完成后删除本地文件、解除钉住。 唤醒 · 请求到达 atenet-router 挂起 · SuspendActor(默认异步) router 停住请求 租约 按 actor 加锁 AssignWorker PG 里抢槽位 atelet 取回快照 runsc restore 进程接着跑 RUNNING 带 worker IP ResumeActor 返回 → 经 atunnel 隧道转给 actor :80 实线全是同步调用:ResumeActor 返回的那一刻,actor 已经在运行, router 这才把停住的请求转进去。 槽位全满:请求停车重试,最多 5 s SuspendActor 上层系统调用 runsc checkpoint 内存 · rootfs · 卷 本地快照 钉在 node-a 后台上传 对象存储 删本地文件 解除钉住 这一步之后 worker 就空出来了 这期间再被唤醒:回 node-a 用本地文件恢复
图 5 · 唤醒是一次同步调用,返回时 actor 已在运行;挂起先在本地落盘,worker 马上空出来,上传放到后台

在超卖的系统里,“挑一个还有空位的 worker”是可能失败的:一阵流量涌来,池子里的槽位会被瞬间占满,ResumeActor 返回“没有 worker 放得下”。这种拥挤通常只持续一眨眼,别的 actor 很快就会被挂起,所以 router 默认不直接回 503,而是把请求“停车”,退避重试,最多等 5 秒,停车场最多容纳 1024 个请求19。

挂起:先落本地,再慢慢上传

挂起要显式调用 SuspendActor。ateworker 执行 runsc checkpoint:它不让沙箱里的系统走关机流程,而是把所有进程原地冻住,再把内存、根文件系统的改动和挂载的卷写成文件。ateworker 调用它时没有带 -leave-running 参数,按 runsc 的默认行为,存完沙箱就退出,worker 随即被清理,还给池子18。这很像笔记本合盖休眠:不是关机,内存原样留着,打开盖子接着用;区别是这台“笔记本”合上以后,可以在另一台机器上打开。

默认的挂起是异步的:快照先留在节点本地,actor 被“钉”在这台节点上,这时如果马上又来一个请求,就回到同一台节点用本地文件恢复;atelet 在后台把快照传到对象存储,之后删掉本地文件、解除钉住,actor 就可以在任何节点上醒来了2。

计数器演示里的 actor 挂起再醒来,内存里的计数接着往下数,说明这份快照连进程内存都存下了。一个要跑几天的智能体,它的状态是不是都能这样保住?

四、长时间运行的状态保持

一个智能体如果要跑几天,它身上的状态远不止计数器里的一个数。对话的上下文和中间变量在进程内存里,它用 pip install 装的工具落在根文件系统上,它改到一半的代码和 SQLite 数据库在工作目录里,它也许还连着外部的数据库和一条正在等模型返回的网络连接。挂起和唤醒解决的是“不跑的时候不占资源”,可对一个长期运行的智能体来说,更要紧的是另一个问题:这些状态分别存在哪里,什么时候存下来,出了什么事会丢?

我们先看 Kubernetes 的 Pod 怎么回答这个问题,再看 Substrate,最后和 E2B 放在一起比较。

Pod 的做法:只有卷能活下来

Pod 里的进程一旦重启,内存就没了;容器的可写层也跟着容器走,容器重启,pip install 装上的东西就没了。emptyDir 卷能挺过容器重启,但 Pod 一删也被清空;只有 PersistentVolume 能活过 Pod 本身20。所以 Kubernetes 的模型是“无状态的进程 + 有状态的卷”:应用自己负责把要留下的东西写进卷或者数据库,重启以后再从里面把内存状态重建出来。

Kubernetes 并非完全没有碰过内存。从 1.30 起,kubelet 有一个默认开启的 beta 接口 POST /checkpoint/{namespace}/{pod}/{container},可以把一个运行中的容器连同全部内存页打成一个 tar 包,放在节点的 /var/lib/kubelet/checkpoints 下面。但文档对它的定位是“搬到别的机器上调试”,Kubernetes 自己的 API 里没有对应的“从这个包恢复”21。放在 Pod 里的智能体,要想让对话跨重启延续,只能自己把上下文序列化进外部存储。

Substrate 的做法:整个沙箱按档位存成快照

Substrate 把这件事从应用手里接了过来:挂起时由 runsc checkpoint 把整个沙箱存下来,应用不用知道自己被挂起过。存多少由模板的 preferredFidelity 决定,分三档,每一档包含它下面的所有层4:

  • VOLUMES:只存模板里声明的 durableDir 卷。醒来时容器从镜像重新启动,卷里的数据还在。
  • ROOTFS:再加上根文件系统的改动。醒来时还是重新启动进程,但装过的包都在。
  • MEMORY:再加上进程内存,醒来就是挂起前那一刻。计数器演示用的就是这一档。

durableDir 卷是 Substrate 里专门放“应用数据”的地方,它的内容进每一份快照,用法类似 Kubernetes 的 emptyDir,但能跟着 actor 搬到任何节点。还有一类外部卷走 CSI,按 actor 动态创建、随生命周期挂载卸载,它们从不进快照,目前还是预览功能12。

档位只是上限。快照里的内存是在某个模板、某个沙箱运行时版本下拍的,恢复时条件变了,内存就用不上:actor 换了模板,恢复时只还原卷里的数据、重新启动进程;找不到运行时兼容的 worker,也会退到 VOLUMES 档冷启动12。所以内存更像一层“能省掉启动时间的缓存”,真正不能丢的数据应该放在 durableDir 里。

快照的数量也有规矩。每个 actor 只保留一份外部快照,下一次挂起成功就把上一份删掉。想留住某一刻,要在挂起后给它打一个 Tag,Tag 自己持有一份拷贝,还能拿来创建新的 actor,相当于从这一刻分叉12。

快照之间的空档

快照只在挂起那一刻存在,两次挂起之间的状态只活在运行中的沙箱里。把一个 actor 默认的异步挂起前后画成时间线,就能看出哪些时刻是有风险的:

异步挂起前后,actor 的状态存在哪里 进程内存、根文件系统改动和 durableDir 卷三层一起变化:运行中只在沙箱里;SuspendActor 之后只有 node-a 上的本地快照;约 5 分钟后上传到对象存储,两边都有;约 30 分钟后删除本地文件、解除钉住,只剩对象存储。外部 CSI 卷始终在存储后端,不进快照。 一个 actor 的状态在哪 · 默认的异步挂起 运行中 SuspendActor 约 5 min 上传完 约 30 min 解除钉住 进程内存 根文件系统改动 durableDir 卷 外部卷 · CSI 沙箱里 只在 node-a node-a + 对象存储 只在对象存储 沙箱里 只在 node-a node-a + 对象存储 只在对象存储 沙箱里 只在 node-a node-a + 对象存储 只在对象存储 一直在存储后端:不进快照,RevertActor 也不回滚它 SuspendActor 到上传完成之间,快照只有 node-a 上这一份,节点没了它也跟着没了。 运行中出事更早:只能回到上一次挂起留下的快照,之后的改动全丢。
图 6 · 状态只在挂起时落盘,落盘后还要过一段时间才离开节点;外部卷是唯一不跟着快照走的一层

最常见的风险是 worker Pod 被收回:滚动升级、缩容、节点排空,或者 Spot 实例被回收。这时 ateworker 会给每个 actor 主进程发 SIGTERM,从那一刻起有 30 分钟,但 Substrate 不会替 actor 挂起,得由驱动它的上层(通常是智能体的 harness)在这段时间里调用一次同步的 SuspendActor。没来得及挂起的 actor 被杀掉,变成 CRASHED,上一次快照之后的东西全部丢失,只能用 RevertActor 回到那份快照。Spot 实例默认只给 15 秒,这个窗口就更紧了。回滚时外部卷不会跟着回滚,于是可能出现内存和根文件系统回到了过去、卷里的数据却停在崩溃那一刻的不一致12。

存快照要让沙箱停下来,文档和代码里也都没有边运行边拍快照、或者定期自动打快照的接口(这是从文档和挂起流程推断的)。换句话说,状态多久落一次盘、在什么时候落,是上层系统的责任:在智能体空闲、没有进行中的模型调用时挂起,既省资源,也缩短了没有快照保护的时间。

与 Pod、E2B 的对照

E2B 也有一整套对应的能力。暂停默认连内存和文件系统一起存,也可以只存文件系统;暂停的沙箱无限期保留,直到显式删除;可以设成超时自动暂停、有请求或 SDK 调用时自动恢复2223。命名快照可以一对多地创建新沙箱24,独立于沙箱的卷还在内测25。内存镜像恢复不了时,可以选择只用磁盘重新启动26。三者放在一起:

长时间运行的智能体,状态怎么留下来
Kubernetes PodAgent SubstrateE2B
进程内存重启即丢;checkpoint 只留归档,不管恢复MEMORY 档存进快照暂停默认存,恢复约 1 秒
根文件系统随容器重启丢失ROOTFS 档及以上暂停、快照都存
工作数据PersistentVolumedurableDir,进每一份快照磁盘随暂停保存
沙箱外的卷PersistentVolumeCSI 外部卷(预览),不进快照Volumes(内测)
存的时候停不停checkpoint 给运行中的容器拍一份副本进程冻住,存完沙箱退出、让出 worker暂停,沙箱停止运行
谁决定什么时候存应用自己写上层显式挂起,同步或异步显式暂停,或超时自动暂停
不跑时保留多久—到下一次挂起替换为止;Tag 长期保留无限期,直到显式删除
连续运行上限无无;worker 被收回时 30 分钟内要挂起Pro 24 小时,Hobby 1 小时
内存用不上时—只恢复卷数据,重新启动可选只用磁盘重启
从某一刻分叉—Tag 创建新 actor快照创建新沙箱

Pod 把状态留给应用自己管;Substrate 和 E2B 都把整个沙箱存下来,差别在由谁、在什么时候按下保存。

图 7 · Pod 只保住卷,Substrate 和 E2B 连内存一起保住

最大的分界线在 Pod 和另外两者之间:Pod 只保证卷,内存和根文件系统要应用自己序列化;Substrate 和 E2B 都把整个沙箱拍成快照,应用不用改。Substrate 和 E2B 之间,差别不在能存什么,而在谁来按保存键、存下的东西归谁管。E2B 是托管服务,暂停由 SDK 调用或超时触发,存好的快照交给 E2B 保管,沙箱最多连续跑 24 小时,长期运行的智能体要靠暂停和恢复一段一段地接上22。Substrate 没有连续运行的上限,但挂起的时机、同步还是异步、worker 被收回时要不要抢着保存,都交给了部署它的人,快照也存在自己的对象存储里。

两者都存不住的,是进行中的网络连接。E2B 明确说暂停以后外部客户端的连接都会断开,恢复后要重连22;Substrate 的 router 按请求重新找 actor,请求级别的 HTTP 流量不受影响,但跨越一次挂起的长连接同样指望不上。

挂起、恢复、状态保持,Agent Substrate 里频繁发生的这些事情,没有一样经过 Kubernetes 的 API。它明明装在 Kubernetes 上,每个 worker 也都是 Pod。它和 Kubernetes 到底是怎么分工的?

五、与 Kubernetes 的分工

异步管 worker,同步管 actor

在《E2B 和 Docker 是什么关系》里我们看过 Kubernetes 创建 Pod 的方式:apiserver 把期望写进 etcd 就返回,Pod 什么时候真正跑起来,由各个组件 watch 接力慢慢收敛,调用方要自己再去等。它是异步的,不管拉起。

Agent Substrate 的分工正好沿着这条线切开。Kubernetes 只负责 worker,也就是那些长期运行、很少变化的 Pod:拉起它们,节点坏了在别处重建,按负载扩缩,套上 NetworkPolicy,选什么样的节点。actor 进出 worker 是另一件事,项目的目标是每秒上千次,调用方要的是“返回时就能用”,所以由 Substrate 自己的控制面同步完成2。

Kubernetes 和 Substrate 的两层分工 上层是 Substrate:约 250 个 actor 记录,正在运行的 my-counter-1、my-counter-2 分别占着 worker-2、worker-5 两个槽位,挂起的 actor 只是记录。下层是 Kubernetes:WorkerPool 经 Deployment、kube-scheduler 和 kubelet 异步拉起 8 个 worker Pod,每个 Pod 对应上层的一个 Worker 记录。 Substrate · 同步:请求来了才放进槽位,返回时已在运行 ResumeActor 占槽 · SuspendActor 让槽 下面一行是 Worker 记录,一个 Pod 一行 my-counter-1 my-counter-2 my-counter-3 my-counter-4 my-counter-5 … 共约 250 个 actor 虚线:挂起,只是一行记录 空闲 counter-1 空闲 空闲 counter-2 空闲 空闲 空闲 worker-1 worker-2 worker-3 worker-4 worker-5 worker-6 worker-7 worker-8 WorkerPool replicas: 8 Deployment kube-scheduler · kubelet Pod ×8 Kubernetes · 异步:写下期望,各组件 watch 接力 这一层很少变:扩缩容、换版本、节点坏了重建 Pod。
图 8 · Kubernetes 只负责把槽位(worker Pod)准备好,actor 进出槽位是 Substrate 自己的同步操作

为什么不干脆让每个 actor 就是一个 Pod,挂起时删掉、唤醒时重建?架构文档列了四条理由:空闲的 Pod 也在占资源;apiserver 擅长在许多控制器之间异步调和,但不擅长存数以百万计的对象,也扛不住大量写入;调度一个 Pod 要等几个异步过程收敛,还要拉镜像,几秒的延迟对只跑几百毫秒的负载不可接受;PersistentVolume 也不是为百万个卷高速挂载卸载设计的2。所以 actor、Worker 这些高频变化的记录放在 PostgreSQL,只把 WorkerPool 这种低频的基础设施配置留在 Kubernetes,让平台团队还能用熟悉的 RBAC、审计和策略去管它2。

Kubernetes 承担的四件事

这样切开以后,Kubernetes 在 Substrate 里做的事并不止“建几个 Pod”。把代码里用到 Kubernetes API 的地方找出来,Substrate 的四个组件各靠它完成一件事:

Substrate 对 Kubernetes 的依赖 左边四个 Substrate 组件各有一根线连到右边 Kubernetes API 里的对象:atecontroller 把 WorkerPool 调和成 Deployment,产生 worker Pod;ate-api-server watch 带 app=atelet 标签的 Pod,按节点找到 atelet;atelet 用集群内凭据 watch 本节点的 worker Pod、WorkerPool 和 SandboxConfig;podcertcontroller 通过 PodCertificateRequest 这个 beta API 给组件签发 mTLS 证书。下方是不依赖 Kubernetes 的部分:存 actor 和模板记录的 PostgreSQL、存快照的对象存储、可以单独接在 Docker 上的 runsc,单独拿出来只是零件。 Substrate 组件 Kubernetes API 里的对象 atecontroller 建出 worker:把 WorkerPool 调和成 Deployment ate-api-server 找节点:按节点名索引 atelet 的 Pod atelet 备运行时:盯着本节点的 worker 和沙箱配置 podcertcontroller 发身份:给各组件签 mTLS 证书 WorkerPool → Deployment CRD,产生一个个 worker Pod Pod app=atelet DaemonSet 的 Pod,带着所在节点名 Pod · WorkerPool · SandboxConfig 用集群内凭据 watch PodCertificateRequest beta API,只能在建集群时打开 不靠 Kubernetes 的部分 单独拿出来只是零件 PostgreSQL actor 和模板的记录 对象存储 快照 runsc 可以单独接 Docker
图 9 · 四个组件各靠 Kubernetes API 完成一件事;剩下的 PostgreSQL、对象存储和 runsc 单独拿出来拼不出 Substrate

第一件是建出槽位。worker 来自 WorkerPool 这个 CRD,由 atecontroller 调和成 Deployment,worker 的容量也是经 Pod 的 downward API 告诉 ateworker 的27。

第二件是找到节点。ate-api-server 在 Kubernetes 里 watch 带 app=atelet 标签的 Pod,按节点名建索引,要和哪台节点上的 atelet 通话,就从这里查地址28。

第三件是让节点知道该准备什么。atelet 用集群内的凭据连 API server,watch 本节点上的 worker Pod、WorkerPool 和 SandboxConfig,据此给每个 worker 准备沙箱运行时29。runsc 本身也是随 SandboxConfig 下发、在 worker Pod 里运行的,节点上不用预装 gVisor4。

第四件是发放身份。组件之间互相认证用的 mTLS 证书,连同 ate-api-server 登录 PostgreSQL 的证书,都由 podcertcontroller 通过 Kubernetes 的 PodCertificateRequest 签发,信任根则经 ClusterTrustBundle 分发30。这两个 API 到 Kubernetes 1.36 还没有默认打开:本地的 kind 集群要在创建时打开对应的特性开关;GKE 上要么用 1.36 并在建集群时打开 beta API,事后再开不会生效,只能重建,要么直接用默认就提供它们的 1.37 以上版本9。

这四件事都写在组件的代码里,不是可以换掉的部署选项,所以 Substrate 离不开 Kubernetes。它的安装方式也只有 kind(在 Docker 里跑一个 Kubernetes)、GKE 和自带的集群,README 写的支持范围是 Kubernetes 最新的稳定版和它前一个小版本1。能单独拿出来用的只有下面那一层:runsc 本身就是一个 OCI 运行时,可以直接接在 Docker 上6,但那只是单个沙箱的存和取;actor 的名字、请求来了先停住、在一群 worker 之间挑槽位,这些 Substrate 自己的东西都搭在 Kubernetes 之上。

worker Pod 仍是 Kubernetes 管得着的 Pod

Kubernetes 也没有因此被架空。worker Pod 是一个普通的 Pod,不开特权模式,代码注释里说这是在替换早期的特权 worker。不过 runsc 要建用户命名空间、切换根目录、跟踪应用进程,worker 还要给每个 actor 配网络,所以它以 root 运行,加了 SYS_ADMIN、NET_ADMIN、SYS_PTRACE 等 13 项 Linux 能力,AppArmor 也设成了 Unconfined,因为默认配置不许它挂载27。这超出了 Kubernetes Pod 安全标准里 baseline 一级允许的范围,worker 所在的命名空间要按 privileged 一级放行(这是按标准推断的,项目文档里没有直接写)。

Pod 的资源上限通过 downward API 告诉 ateworker,作为这个 worker 的容量;每个 actor 在 Pod 被委派的 cgroup 下面再开一个子 cgroup,所以 actor 用掉的 CPU 和内存,仍然记在这个 Pod 的账上(这一点是从代码注释推断的,没有在集群里验证)。也就是说,Kubernetes 看到的是一组有 requests、limits 的 Pod,照常给它们调度、记账、扩缩;它看不到的,只是每个 Pod 里此刻住着哪个 actor。

对集群的要求

把这一章和前面几章碰到的条件收在一起,就是 Substrate 对一个集群的要求。它不改 Kubernetes 本身,也不要求换容器运行时,但其中有一条只能在建集群时决定:

跑 Substrate 的集群要满足什么
层面要求不满足会怎样
API打开 PodCertificateRequest 和 ClusterTrustBundle 两个证书 API安装卡在等待 ClusterTrustBundle;GKE 1.36 只能在建集群时打开,事后补不上,要重建;1.37 起默认提供
节点内核Linux 6.5 及以上overlayfs 的 lowerdir+ 从 6.5 才有,拼不出 actor 的根文件系统
安全策略worker Pod 以 root 运行,加 13 项 Linux 能力(含 SYS_ADMIN),AppArmor 设为 Unconfined集群的 Pod 安全准入若按 baseline 或 restricted 执行,worker Pod 建不出来(按 Pod 安全标准推断)
节点参数单节点上千个沙箱时,调大邻居表和 inotify 上限实测约 800 到 1000 个沙箱时 ARP 表溢出、inotify 耗尽
集群外一个 PostgreSQL,一个 GCS 或 S3 兼容的对象存储没有地方存 actor 记录和快照;本地 kind 安装会一并装上 PostgreSQL 和 rustfs
可选节点暴露 /dev/kvm只影响轻量虚拟机那一族沙箱,gVisor 不需要

前三行和 PostgreSQL、对象存储缺一不可;其中证书 API 那一条只能在建集群时定下来。

图 10 · Substrate 不改 Kubernetes,但要一个打开了证书 API、内核够新、允许高权限 worker 的集群

挂起、恢复这些高频的事绕开了 Kubernetes,Substrate 本身却是牢牢装在 Kubernetes 上的。同样在 Kubernetes 上自己拉起沙箱,E2B 又是怎么做的?

六、与 E2B 的对照

同一个问题,同一个核心办法

先说结论:两者在代码和组织上没有关系,Substrate 的仓库里也没有提到 E2B。它们是同一个问题的两种产品形态。

问题是一样的:给智能体一个隔离得住不可信代码、又能很快拿到手的执行环境。核心办法也一样:不冷启动,而是从快照恢复。E2B 在构建模板时把应用跑到就绪再拍快照,创建沙箱就是恢复这份快照31;Substrate 在创建 ActorTemplate 时拍 golden snapshot,actor 第一次醒来也是恢复它。连挂起的做法都很像:E2B 暂停沙箱时先在本地存下差异,异步上传,恢复时优先回到原节点31;Substrate 的异步挂起也是先留在本地、钉住节点、后台上传。两者也都绕开 Kubernetes,用同步调用走热路径。

区别在单位和与 Kubernetes 的分工

真正的区别有两处。

第一处是默认的单位。 第四章看到,E2B 的沙箱设成超时自动暂停、有请求自动恢复以后,也能像 actor 一样长期挂着、来活就醒。但它的默认值是另一回事:沙箱超时就被删掉,智能体调用 Sandbox.create() 要一台,通过沙箱里的 envd 执行命令、读写文件,像是租一台机器来用22。Substrate 的 actor 生来就是一个长期存在、有名字的服务:它的地址是 atespace/name,带着自己的 SPIFFE 身份证书,创建出来就是挂起状态;actor 里没有 envd 这样的代理,跑的就是你的镜像自己的 HTTP 服务,请求经 router 直接打到它的端口上1。前者是一台可以暂停的机器,后者是一个能缩到零、带着内存状态醒来的服务。

第二处是和 Kubernetes 的分工。 两者都是“Kubernetes 拉起一层长期运行的东西,自己在里面同步拉起沙箱”,差别在 Kubernetes 看到的粒度。把一台节点画出来就很清楚:

同一台节点上的 E2B 和 Substrate E2B 的沙箱节点上,kubelet 只看见一个 orchestrator Pod,它开着特权、hostNetwork、hostPID,Firecracker 虚拟机是它在宿主上的子进程,在 Pod 之外。Substrate 的节点上,kubelet 看见 atelet 和多个不特权的 worker Pod,每个 Pod 有自己的资源上限,gVisor 沙箱在 Pod 内部。 E2B · 沙箱节点 Substrate · 节点 kubelet 看见 1 个 Pod Pod orchestrator privileged true · nsenter hostNetwork / hostPID true hugepages-2Mi 4Gi 沙箱在 Pod 之外:宿主上的子进程 Firecracker sb-7f3a · envd Firecracker sb-91c2 · envd Firecracker sb-c04e · envd 整台节点交给 orchestrator,Kubernetes 看不见沙箱。 kubelet 看见 atelet 和 N 个 worker Pod atelet DaemonSet Pod worker-1 privileged false limits 1 CPU · 1Gi 沙箱在 Pod 里 runsc my-counter-1 Pod worker-2 privileged false limits 1 CPU · 1Gi 沙箱在 Pod 里 runsc my-counter-2 … 沙箱留在 Pod 里,Kubernetes 仍按 Pod 记账。
图 11 · E2B 把整台节点从 Kubernetes 手里接过去;Substrate 把沙箱留在 Pod 里,Kubernetes 管槽位,Substrate 管谁住进去

E2B 那边,每台沙箱节点上只有一个 orchestrator Pod,开着特权模式、共用宿主的网络和 PID namespace,自己 nsenter 进节点准备 hugepage 和 iptables;一台台 Firecracker 虚拟机是它在宿主上的子进程,Kubernetes 一个也看不见32。Kubernetes 在这里只提供一份“哪些节点还活着”的名单;E2B 的服务本身也和调度器无关,单机版 Embed 用 Compose 就能跑起来,Kubernetes 对它只是可选的部署平台31。

Substrate 这边,Kubernetes 是地基,节点上是很多个不特权的 worker Pod,每个都有自己的资源上限,gVisor 沙箱就在 Pod 内部。Kubernetes 不知道某个 Pod 里此刻住着哪个 actor,但它知道每个槽位多大、在哪、还在不在,扩缩容、网络策略、节点选择都照常起作用。代价是一个沙箱的大小受 worker Pod 的规格约束,计数器演示的模板注释里就提醒,actor 的资源要按 worker 的容量来定。

其他几处差别,放在一张表里:

E2B 与 Agent Substrate
E2BAgent Substrate
交付形态托管的云服务,基础设施代码也开源开源软件,装进自己的 Kubernetes 集群
基本单位沙箱:默认超时即删,可设成自动暂停、自动恢复actor:长期存在,有名字和身份,大部分时间挂起
怎么用SDK 经沙箱里的 envd 执行命令、读写文件HTTP 请求带 ate-target-actor,打到 actor 自己的服务
隔离Firecracker 虚拟机默认 gVisor,也可选 Cloud Hypervisor 虚拟机
和 Kubernetes每节点一个特权 Pod,沙箱在 Pod 外许多不特权的 worker Pod,沙箱在 Pod 里

两者共享“从快照恢复、同步拉起”这个核心;分歧在于默认交出去的是一台机器还是一个常驻的服务,以及节点还归不归 Kubernetes 管。

图 12 · E2B 和 Agent Substrate 的主要差别

七、回到开头的问题

现在可以回答开头那个数字了。250 个 actor 跑在 8 个 Pod 上,是因为任何时刻真正在运行的 actor,最多只有 worker 槽位那么多;其余的 actor 都处在挂起状态,在系统里只是 PostgreSQL 里的一行记录,加上对象存储里的一份快照。请求来了,router 把它停住,控制面挑一个空槽,atelet 把快照恢复进去,请求再接着往里走;actor 闲下来,被挂起,槽位又空出来给别人。这和 kubectl get pods 看不到 actor 是同一件事:Kubernetes 只管那 8 个槽位。

这套设计的代价,文档自己也写得很清楚:挂起和恢复做到这个规模,就变成了一个数据管理问题,数以百万计的快照要存、要搬,还要知道每个 actor 最新的状态在哪台机器上;再造一个高可用的控制面也不是小工程;actor 在 worker 之间不停地搬来搬去,监控和排错都会更难2。快照还和沙箱运行时的版本绑在一起,SandboxConfig 会把恢复所需的 runsc 版本钉进快照清单2。README 里“恢复小于 400 毫秒、每秒 1500 次以上激活”的数字是项目自述,北极星目标是 p95 100 毫秒,眼下还没到。

小结

  1. Agent Substrate 是什么? 一个装在 Kubernetes 上的智能体运行时。它把大量大部分时间空闲的 actor 复用到少量预先拉起的 worker Pod 上,空闲时打快照腾出 worker,请求来了再恢复。
  2. actor 不跑的时候在哪? 在 PostgreSQL 里的一行记录和对象存储里的一份快照里,不占任何 Pod。每个沙箱都是 gVisor 或轻量虚拟机,不支持普通容器。
  3. 镜像怎么变成沙箱? 模板里是普通的 OCI 镜像,只有用户态、没有内核。gVisor 不是镜像,而是在用户态顶替内核的运行时,镜像原样跑在它上面。Substrate 不管构建:atelet 拉镜像、overlayfs 叠成根文件系统,创建模板时跑到就绪拍下 golden snapshot。E2B 要给镜像补上 envd 和 systemd、压进 ext4,在 Firecracker 里构建再拍快照。
  4. 一个请求怎么叫醒它? router 停住请求,ResumeActor 同步地拿租约、抢 worker 槽位、恢复快照,返回时 actor 已在运行,router 再经隧道转发。挂起则先落本地、后台上传。
  5. 和 Kubernetes 怎么分工? Kubernetes 异步地管 worker Pod 这些低频变化的槽位,Substrate 同步地管 actor 进出槽位这种高频操作。建出 worker、找到节点、同步节点、发放身份这四件事都靠 Kubernetes API,所以它离不开 Kubernetes;集群要在创建时打开两个证书 API,节点内核 6.5 以上,并允许以 root 运行、带一组 Linux 能力的 worker Pod。
  6. 长时间运行的状态怎么保住? Pod 只保住卷,内存和根文件系统要应用自己序列化。Substrate 挂起时按 VOLUMES、ROOTFS、MEMORY 三档把整个沙箱存成快照,必须留下的数据放 durableDir;状态只在挂起时落盘,两次挂起之间、异步上传完成之前都有丢失的窗口,挂起时机由上层决定。E2B 的暂停、快照、卷与之一一对应,但它是托管服务,沙箱最多连续跑 24 小时。
  7. 和 E2B 什么关系? 没有代码关系,核心办法相同:从快照恢复,绕开 Kubernetes 同步拉起。区别在于 E2B 默认交出的是一台可暂停的机器、orchestrator 接管整台节点;Substrate 的 actor 是常驻的服务、沙箱留在 Pod 里。

一句话概括:E2B 是交给智能体的一台可以暂停的机器,Agent Substrate 是让一群智能体轮流住进 Kubernetes 槽位里的调度层。

参考资料

Footnotes

  1. Agent Substrate READMEGitHub · agent-substrate/substrate收藏github.com本文读的是 2026-10-18 的提交 ddb4665;What is Agent Substrate、Performance & Multiplexing、Quickstart 各节https://github.com/agent-substrate/substrate/blob/main/README.md ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Agent Substrate ArchitectureGitHub · agent-substrate/substrate收藏github.comProblem Statement、Relationship to Kubernetes、API Resource Models、Actor Lifecycle 各节;开头注明“Much of this architecture is aspirational”https://github.com/agent-substrate/substrate/blob/main/docs/architecture.md ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15

  3. Pod startup latency SLI/SLOKubernetes SIG Scalabilitygithub.com ↗无状态 Pod,不含拉镜像和 init 容器,p99 ≤ 5 shttps://github.com/kubernetes/community/blob/master/sig-scalability/slos/pod_startup_latency.md ↩

  4. Agent Substrate GlossaryGitHub · agent-substrate/substrate收藏github.comSnapshots 一节:VOLUMES、ROOTFS、MEMORY 三档和 Golden Snapshothttps://github.com/agent-substrate/substrate/blob/main/docs/glossary.md ↩ ↩2 ↩3 ↩4

  5. claude-code-multiplex 演示的 DockerfileGitHub · agent-substrate/substrategithub.com ↗FROM node:20-slim,装 Claude Code,ENTRYPOINT ["/run.sh"]https://github.com/agent-substrate/substrate/blob/main/demos/claude-code-multiplex/workload/Dockerfile ↩

  6. What is gVisor?gVisor 文档收藏gvisor.devhttps://gvisor.dev/docs/ ↩ ↩2 ↩3

  7. fc-kernels:E2B 给沙箱编的内核GitHub · e2b-dev/infra收藏github.com默认内核版本 vmlinux-6.1.158 在 packages/shared/pkg/featureflags/flags.go 的 DefaultKernelVersionhttps://github.com/e2b-dev/infra/tree/main/firecracker/fc-kernels ↩

  8. gVisor CompatibilitygVisor 文档收藏gvisor.devWhat works? 一节:系统调用 ABI 的子集、io_uring、语言运行时回归测试https://gvisor.dev/docs/user_guide/compatibility/ ↩

  9. setup-gcp:创建集群GitHub · agent-substrate/substrategithub.com ↗

  10. Create Cluster 一节的警告:PodCertificateRequest、ClusterTrustBundle,GKE 1.36 只能建集群时打开、1.37 起默认;节点 sysctl;kind 的特性开关见 hack/create-kind-cluster.sh
  11. https://github.com/agent-substrate/substrate/blob/main/tools/setup-gcp/README.md ↩ ↩2

  12. oci.go:atelet 为 actor 准备 OCI bundleGitHub · agent-substrate/substrategithub.com ↗prepareOCIDirectory、resolveActorEnv、resolveProcessArgs;overlayfs 挂载在 internal/imagecache/bundle_linux.gohttps://github.com/agent-substrate/substrate/blob/main/cmd/atelet/oci.go ↩ ↩2

  13. bundle_linux.go:用 overlayfs 组装 actor 的根文件系统GitHub · agent-substrate/substrategithub.com ↗mountOverlay 注释:最低内核 Linux 6.5(lowerdir+)https://github.com/agent-substrate/substrate/blob/main/internal/imagecache/bundle_linux.go ↩

  14. Agent Substrate API Configuration GuideGitHub · agent-substrate/substrate收藏github.comSnapshot 字段(actorTemplateUid、sandboxRuntime 的降级规则)、Eviction、SuspendActor、Snapshot lifetime、RevertActor;外部卷见 docs/csi-volumes.mdhttps://github.com/agent-substrate/substrate/blob/main/docs/api-guide.md ↩ ↩2 ↩3 ↩4 ↩5

  15. dockerfileParser.ts:把 Dockerfile 翻译成模板步骤GitHub · e2b-dev/E2B收藏github.com本文读的是提交 62a17df;handleInstruction、IGNORED_INSTRUCTIONS、applyStartCmdhttps://github.com/e2b-dev/E2B/blob/main/packages/js-sdk/src/template/dockerfileParser.ts ↩

  16. rootfs.go:在镜像上追加 E2B 自己的一层GitHub · e2b-dev/infra收藏github.comCreateExt4Filesystem、additionalOCILayers;镜像配置里只保留 Env 见 phases/base/builder.gohttps://github.com/e2b-dev/infra/blob/main/packages/orchestrator/pkg/template/build/core/rootfs/rootfs.go ↩

  17. oci.go:从 OCI 镜像到 ext4GitHub · e2b-dev/infra收藏github.comGetPublicImage、ToExt4、createExport、unpackRootfs;叠层在 core/filesystem/ext4.go 的 MountOverlayFShttps://github.com/e2b-dev/infra/blob/main/packages/orchestrator/pkg/template/build/core/oci/oci.go ↩

  18. provision.sh:把容器镜像补成能开机的系统GitHub · e2b-dev/infra收藏github.com按 /etc/os-release 识别发行版;各发行版要装的包在 phases/base/distro/distro.go;默认用户在 phases/finalize/configure.shhttps://github.com/e2b-dev/infra/blob/main/packages/orchestrator/pkg/template/build/phases/base/provision.sh ↩

  19. How it worksE2B 文档收藏docs.e2b.dev“load the sandbox in ~80ms any time later with all the processes already running”https://docs.e2b.dev/template/how-it-works ↩

  20. workflow_resume.go:ResumeActor 工作流GitHub · agent-substrate/substrate收藏github.comResumeActor 依次调用 loadActorForResume、ensureWorkerAssigned、ensureAteletRestored、finalizeRunning;runsc 的参数在 cmd/ateworker-gvisor/runsc.gohttps://github.com/agent-substrate/substrate/blob/main/cmd/ateapi/internal/controlapi/workflow_resume.go ↩ ↩2

  21. Request Parking (atenet router)GitHub · agent-substrate/substrate收藏github.com--parked-request-budget 默认 5 s,--parked-request-max 默认 1024https://github.com/agent-substrate/substrate/blob/main/docs/request-parking.md ↩

  22. VolumesKubernetes 文档收藏kubernetes.ioemptyDir 一节:Pod 从节点上移除时,emptyDir 里的数据被永久删除;容器崩溃不会移除 Podhttps://kubernetes.io/docs/concepts/storage/volumes/ ↩

  23. Kubelet Checkpoint APIKubernetes 文档收藏kubernetes.ioBeta since v1.30,默认开启;归档写到 /var/lib/kubelet/checkpointshttps://kubernetes.io/docs/reference/node/kubelet-checkpoint-api/ ↩

  24. Sandbox persistenceE2B 文档收藏docs.e2b.dev暂停存内存和文件系统、无限期保留、Auto-pause、Network、Limitations(Pro 24 小时、Hobby 1 小时)https://docs.e2b.dev/sandbox/persistence ↩ ↩2 ↩3 ↩4

  25. Auto-resume on requestE2B 文档收藏docs.e2b.devhttps://docs.e2b.dev/sandbox/auto-resume ↩

  26. Sandbox snapshotsE2B 文档收藏docs.e2b.devhttps://docs.e2b.dev/sandbox/snapshots ↩

  27. VolumesE2B 文档收藏docs.e2b.dev内测(private beta)https://docs.e2b.dev/volumes ↩

  28. Reboot on resumeE2B 文档收藏docs.e2b.devhttps://docs.e2b.dev/sandbox/resume-without-memory ↩

  29. workerpool_apply.go:WorkerPool 生成的 PodGitHub · agent-substrate/substrate收藏github.comworkerSecurityContext 和 workerGvisorCapabilities;每个 actor 的 cgroup 见 internal/ateworker/cgroup/actor.gohttps://github.com/agent-substrate/substrate/blob/main/cmd/atecontroller/internal/controllers/workerpool_apply.go ↩ ↩2

  30. informer.go:ate-api-server 按节点索引 ateletGitHub · agent-substrate/substrategithub.com ↗AteletInformer watch app in (atelet) 的 Pod,按 spec.nodeName 建索引;WorkerPool、SandboxConfig 的 lister 在 cmd/ateapi/main.gohttps://github.com/agent-substrate/substrate/blob/main/cmd/ateapi/internal/controlapi/informer.go ↩

  31. worker_sandbox_syncer.go:atelet 同步本节点的 workerGitHub · agent-substrate/substrategithub.com ↗文件头注释:接 Pod、WorkerPool、SandboxConfig 三个 informer;集群内凭据见 cmd/atelet/main.go 的 rest.InClusterConfig()https://github.com/agent-substrate/substrate/blob/main/cmd/atelet/worker_sandbox_syncer.go ↩

  32. podcertcontroller:三个 Pod 证书签发者GitHub · agent-substrate/substrategithub.com ↗servicedns、podidentity、postgres 三个 signer,基于 PodCertificateRequest APIhttps://github.com/agent-substrate/substrate/blob/main/cmd/podcertcontroller/main.go ↩

  33. E2B Infrastructure ArchitectureGitHub · e2b-dev/infra收藏github.comOrchestrator、Sandbox creation、Pause and resume 各节https://github.com/e2b-dev/infra/blob/main/docs/ARCHITECTURE.md ↩ ↩2 ↩3

  34. E2B Embed on one Kubernetes nodeGitHub · e2b-dev/infragithub.com ↗orchestrator 所在 Pod 用 hostNetwork、hostPID 和特权容器,nsenter 进节点https://github.com/e2b-dev/infra/blob/main/embed/kubernetes/README.md ↩