250 个 actor,轮流住进 8 个 Pod 左边是一大片挂起的 actor,闭着眼睛,每个只是一行记录加一份快照;一条线把其中一个经 ResumeActor 叫醒,放进右边 Kubernetes 节点里 8 个 worker 槽位中的一个空槽,worker-2 和 worker-5 里已经各住着一个睁着眼的 actor。 250 个 actor,轮流住进 8 个 Pod $ kubectl ate create actor my-counter-1 zz zz 挂起:一行记录 + 一份快照,不占 Pod WorkerPool · 8 worker Pods worker-1 worker-2 worker-3 worker-4 worker-5 worker-6 worker-7 worker-8 ResumeActor

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。

所以 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 一样有一个真正的客户机内核2。

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 里9。一台节点上的很多 actor 共用同一份镜像层,各写各的上层。

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

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

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

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

两边走到最后是同一个办法:跑到就绪,拍快照,以后都从快照起。差别在镜像经历了什么。在 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 返回16。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 个请求17。

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

挂起要显式调用 SuspendActor。ateworker 执行 runsc checkpoint,把进程连同内存、根文件系统的改动和挂载的卷存成文件,worker 随即被清理,还给池子。默认的挂起是异步的:快照先留在节点本地,actor 被“钉”在这台节点上,这时如果马上又来一个请求,就回到同一台节点用本地文件恢复;atelet 在后台把快照传到对象存储,之后删掉本地文件、解除钉住,actor 就可以在任何节点上醒来了2。

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

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

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

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

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

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

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

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

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

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

那么,存快照的时候沙箱要不要关机?不用关机,但要停下来。runsc checkpoint 不会让沙箱里的系统走一遍关机流程,而是把所有进程原地冻住,再把内存和文件系统的改动写成文件。ateworker 调用它时没有带 -leave-running 参数,按 runsc 的默认行为,存完以后沙箱就退出了,worker 随即让给别的 actor16。恢复时进程从冻住的那一刻接着往下跑,应用察觉不到自己停过,只是时钟跳过了一段,外面的长连接也已经断开。这很像笔记本合盖休眠:不是关机,内存原样留着,打开盖子接着用;区别是 Substrate 这台“笔记本”合上以后,可以在另一台机器上打开。

也正因为存快照就意味着停下,Substrate 的快照只在挂起时产生。文档和代码里都没有“边运行边拍快照”的接口,也没有定时自动快照(这一点是按挂起流程推断的,没有找到明文说明)。

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

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

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

快照之间的空档

快照只在挂起那一刻存在,两次挂起之间的状态只活在运行中的沙箱里。把一个 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 秒,这个窗口就更紧了。回滚时外部卷不会跟着回滚,于是可能出现内存和根文件系统回到了过去、卷里的数据却停在崩溃那一刻的不一致10。

文档里没有看到 Substrate 会定期自动打快照(这是从文档推断的)。换句话说,状态多久落一次盘、在什么时候落,是上层系统的责任:在智能体空闲、没有进行中的模型调用时挂起,既省资源,也缩短了没有快照保护的时间。

与 Pod、E2B 的对照

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

长时间运行的智能体,状态怎么留下来
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 小时,长期运行的智能体要靠暂停和恢复一段一段地接上20。Substrate 没有连续运行的上限,但挂起的时机、同步还是异步、worker 被收回时要不要抢着保存,都交给了部署它的人,快照也存在自己的对象存储里。

两者都存不住的,是进行中的网络连接。E2B 明确说暂停以后外部客户端的连接都会断开,恢复后要重连20;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。

worker Pod 仍是 Kubernetes 管得着的 Pod

这样分工以后,Kubernetes 并没有被架空。worker Pod 是一个普通的 Pod,不开特权模式,只按需要加了一组 Linux 能力(SYS_ADMIN、NET_ADMIN、SYS_PTRACE 等,runsc 建沙箱、配网络要用),代码注释里说这是在替换早期的特权 worker25。Pod 的资源上限通过 downward API 告诉 ateworker,作为这个 worker 的容量;每个 actor 在 Pod 被委派的 cgroup 下面再开一个子 cgroup,所以 actor 用掉的 CPU 和内存,仍然记在这个 Pod 的账上(这一点是从代码注释推断的,没有在集群里验证)。

也就是说,Kubernetes 看到的是一组有 requests、limits 的 Pod,照常给它们调度、记账、扩缩;它看不到的,只是每个 Pod 里此刻住着哪个 actor。同样在 Kubernetes 上自己拉起沙箱,E2B 又是怎么做的?

六、与 E2B 的对照

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

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

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

区别在单位和与 Kubernetes 的分工

真正的区别有两处。

第一处是默认的单位。 第四章看到,E2B 的沙箱设成超时自动暂停、有请求自动恢复以后,也能像 actor 一样长期挂着、来活就醒。但它的默认值是另一回事:沙箱超时就被删掉,智能体调用 Sandbox.create() 要一台,通过沙箱里的 envd 执行命令、读写文件,像是租一台机器来用20。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 记账。
图 9 · E2B 把整台节点从 Kubernetes 手里接过去;Substrate 把沙箱留在 Pod 里,Kubernetes 管槽位,Substrate 管谁住进去

E2B 那边,每台沙箱节点上只有一个 orchestrator Pod,开着特权模式、共用宿主的网络和 PID namespace,自己 nsenter 进节点准备 hugepage 和 iptables;一台台 Firecracker 虚拟机是它在宿主上的子进程,Kubernetes 一个也看不见27。Kubernetes 在这里只提供一份“哪些节点还活着”的名单。

Substrate 这边,节点上是很多个不特权的 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 管。

图 10 · 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 Pod 仍是不特权、有资源上限的普通 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

  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

  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

  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. 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

  10. 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

  11. 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 ↩

  12. 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 ↩

  13. 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 ↩

  14. 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 ↩

  15. 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 ↩

  16. 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

  17. 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 ↩

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

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

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

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

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

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

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

  25. 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 ↩

  26. 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

  27. 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 ↩