跳到主要内容

控制面:沙箱生命周期与编排器 REST API

30 秒导读: 一个 E2B「沙箱」是云上一台可秒起、可暂停、可恢复的微型虚拟机。谁来下命令创建它、暂停它、杀掉它?就是本章讲的控制面(control plane)——SDK 里的 SandboxApi 类把「创建 / 查询 / 设超时 / 暂停 / 恢复 / 快照 / 删除」这些对沙箱整体的操作,翻译成对 E2B 编排器(orchestrator,负责调度真实机器的中央服务) 的 REST 请求。你在沙箱里面跑命令、读写文件是另一回事——那是数据面,见 数据面连接


1. 这是什么(先分清两个平面)

E2B 的 SDK 内部有两条完全不同的通信线路,初学最容易混:

平面管什么跟谁说话本 shelf 章节
控制面沙箱这台机器的一生:建、查、暂停、恢复、超时、快照、销毁E2B 编排器的公有 REST API(api.e2b.dev本章
数据面沙箱里面发生的事:跑命令、开 PTY、读写文件、监听目录沙箱内的守护进程 envd(gRPC/HTTP)ch02·ch03·ch04

一句话直觉: 把控制面想成遥控器——按「开机 / 待机 / 关机」这些整机按钮;把数据面想成开机后你在这台机器里敲的每一条命令。遥控器不关心机器里在跑什么,它只调度机器本身。

本章聚焦控制面。 主角是一个文件、一个类:

  • packages/js-sdk/src/sandbox/sandboxApi.ts 里的 SandboxApi 类——控制面所有动作的入口。
  • packages/js-sdk/src/api/index.ts 里的 ApiClient——把动作变成一次真正的 HTTP 请求。

用起来什么样(下面是真实 SDK 调用,感受一下「一生」):

// 示意,非源码 —— 展示控制面的典型时间线
const sandbox = await Sandbox.create({ timeoutMs: 60_000 }) // 建:POST /sandboxes
console.log(sandbox.sandboxId) // 拿到 ID,控制面认它

await Sandbox.setTimeout(sandbox.sandboxId, 300_000) // 续命:POST .../timeout
await Sandbox.pause(sandbox.sandboxId) // 暂停:POST .../pause(存内存快照)

const again = await Sandbox.connect(sandbox.sandboxId) // 恢复:POST .../connect(自动 resume)
await Sandbox.kill(sandbox.sandboxId) // 销毁:DELETE /sandboxes/{id}

读者读完本章应能回答:这些方法各打哪个 REST 端点?暂停时「存不存内存」有什么后果?哪些生命周期选项能组合、哪些是非法的?


2. 顶层全景(控制面怎么转)

一次控制面调用,从 SDK 到编排器,只穿过三层:

你的代码
│ Sandbox.create() / .pause() / .kill() ...

┌─────────────────────────────────────────────┐
│ SandboxApi (sandbox/sandboxApi.ts) │ ← 控制面「动作层」
│ · 组请求体 body(生命周期规则在这里校验) │
│ · 挑对 HTTP 方法 + 端点路径 │
│ · 收响应 → 映射成 SandboxInfo / 抛错 │
└───────────────┬─────────────────────────────┘
│ client.api.POST('/sandboxes', {body})

┌─────────────────────────────────────────────┐
│ ApiClient (api/index.ts) │ ← 「传输层」
│ · openapi-fetch,路径/参数/响应全类型安全 │
│ · 注入鉴权头 X-API-KEY、默认 metadata 头 │
│ · handleApiError:HTTP 状态码 → 错误类 │
└───────────────┬─────────────────────────────┘
│ HTTPS

E2B 编排器 REST API (api.e2b.dev)

每层一句话职责:

部件干什么在哪
SandboxApi控制面所有静态方法;组 body、选端点、校验生命周期、解析响应sandbox/sandboxApi.ts:703
ApiClient包一个 openapi-fetch 客户端,配好 baseUrl / 鉴权头 / 错误钩子api/index.ts:68
schema.gen.tsopenapi-typescript 从 OpenAPI 规范生成paths/components 类型api/schema.gen.ts
metadata.ts默认请求头(langpackage_versionsdk_runtime 等遥测/版本字段)api/metadata.ts:8
Paginator游标分页抽象基类,读 x-next-token 响应头翻页paginator.ts:24
handleApiErrorHTTP 状态码 → SandboxError 子类的统一映射api/index.ts:26

控制面动作 → REST 端点全表(核实过行号与路径):

SandboxApi 方法HTTP编排器端点语义
createSandbox :1119POST/sandboxes建一台新沙箱
connectSandbox :1206POST/sandboxes/{sandboxID}/connect连接(暂停态则自动恢复
getInfo :756GET/sandboxes/{sandboxID}查单台信息
getMetrics :840GET/sandboxes/{sandboxID}/metrics查 CPU/内存/磁盘指标
setTimeout :906POST/sandboxes/{sandboxID}/timeout改超时(续命/减寿)
updateNetwork :946PUT/sandboxes/{sandboxID}/network原子替换出网规则
pause :982POST/sandboxes/{sandboxID}/pause暂停(存快照)
createSnapshot :1040POST/sandboxes/{sandboxID}/snapshots打持久快照
kill :714DELETE/sandboxes/{sandboxID}销毁
deleteSnapshot :1091DELETE/templates/{templateID}删快照(快照即模板)
listSnapshots :1079GET/snapshots列快照(分页)
SandboxPaginator.nextItems :1267GET/v2/sandboxes列沙箱(分页)

两个易踩的「不对称」:列沙箱走的是 /v2/sandboxes(新版分页端点),而不是 /sandboxes;删快照走的是 /templates/{templateID} 而不是 /snapshots/...——因为在 E2B 里,一个快照本质上就是一个模板(见 模板构建)。


3. 核心原理(逐个机制)

3.1 传输层:一层类型安全的 REST 客户端

SandboxApi 自己不碰 fetch。它拿到一个 ApiClient,通过 client.api.POST(...) 这样的调用发请求。这层薄封装的价值在于编译期就把 REST 契约钉死

思路: OpenAPI 规范(spec/)是唯一事实源。跑 make codegenopenapi-typescript 把它编译成 schema.gen.ts 里的 paths 类型;ApiClient 再用 openapi-fetchcreateClient<paths> 把这份类型套上去。结果是——路径字符串、路径参数、请求体、响应体全部有类型,写错端点或字段直接编译不过。

真实构造只有一处,api/index.ts:89

this.api = createClient<paths>({
baseUrl: config.apiUrl,
fetch: createApiFetch(config.proxy),
headers: {
...defaultHeaders, // metadata.ts 的遥测/版本头
...(config.apiKey && { 'X-API-KEY': config.apiKey }), // 控制面鉴权:API Key
...(config.accessToken && { Authorization: `Bearer ...` }),
...config.headers,
},
querySerializer: { array: { style: 'form', explode: false } }, // 数组参数序列化
})

两个要点:

  • 鉴权。 控制面用账号级的 X-API-KEYe2b_ 开头的密钥)。这跟数据面用的、临时的 envdAccessToken/trafficAccessToken两套 token——后者是 create/connect 的响应里带回来的(sandboxApi.ts:1197-1203),专供连 envd 用,见 ch02
  • 默认头即遥测。 defaultHeadersmetadata.ts:8)把 lang: 'js'package_version(读自 package.jsonversion)、sdk_runtimesystem 等塞进每个请求,让编排器知道是哪个 SDK / 运行时 / 版本在调它。

密钥格式在客户端就先验一道(validateApiKeyapi/index.ts:17):必须匹配 /^e2b_[0-9a-f]+$/,否则抛 AuthenticationError——错的密钥不用等服务器打回来。

3.2 生命周期语义:create 时的三个开关与运行时校验

这是全章最需要讲透的一节。Sandbox.create 可以配一个 lifecycle,决定沙箱到点(超时)之后怎么办。涉及三个概念:

概念含义body 字段
onTimeout超时动作:'pause'(暂停)还是 'kill'(销毁)autoPause(布尔)
keepMemory若暂停,存不存内存(全内存快照 vs 仅文件系统)autoPauseMemory
autoResume暂停后是否允许被入站流量自动唤醒autoResume.enabled

思路:这三个开关不是自由组合——有些组合物理上讲不通,SDK 在发请求前就用 InvalidArgumentError 拦下,而不是让编排器返回一个含糊的错误。createSandbox 顶部(sandboxApi.ts:1129-1155)先把 onTimeout 归一化,再跑三道校验:

// 示意,非源码 —— 三道前置校验的逻辑
const action = onTimeout is string ? onTimeout : onTimeout.action // 'pause' | 'kill'
const keepMemory = 取 onTimeout.keepMemory,默认 true
const autoResume = lifecycle.autoResume ?? false

if (hasKeepMemory && action !== 'pause') throw // ① keepMemory 只配 pause
if (autoResume && action !== 'pause') throw // ② autoResume 只配 pause
if (!keepMemory && autoResume) throw // ③ 仅文件系统快照无法被流量唤醒

对应的真实报错原文(sandboxApi.ts:1139-1155)逐字说明了为什么

  • "onTimeout.keepMemory is only allowed when action is 'pause'."——kill 是销毁,谈不上「存不存内存」。
  • "autoResume can only be true when onTimeout action is 'pause'."——被销毁的沙箱没有可恢复的东西。
  • "autoResume: true is not a valid value when keepMemory: false ..."——见下条 3.3,仅文件系统快照没有内存可原地恢复,只能靠 connect() 显式冷启动,无法被流量透明唤醒。

合法组合速查(其余组合在 create 时即报错):

onTimeout.actionkeepMemoryautoResume结果到点后
kill(默认)false直接销毁
pausetrue(默认)false暂停(全内存快照),需手动 connect 唤醒
pausetruetrue暂停(全内存快照),入站流量自动唤醒
pausefalsefalse暂停(仅文件系统),connect冷启动
pausefalsetrue❌ 抛错③
kill传了 keepMemory❌ 抛错①
killtrue❌ 抛错②

校验通过后,三个开关被翻译进请求体(sandboxApi.ts:1157-1169):

const body = {
templateID: template,
timeout: timeoutToSeconds(timeoutMs), // 注意:SDK 收毫秒,编排器要秒
autoPause: action === 'pause',
autoPauseMemory: action === 'pause' ? keepMemory : undefined,
autoResume: { enabled: autoResume },
// ... metadata / envVars / secure / network / volumeMounts
}

类型 + 运行时双保险。 SandboxOnTimeoutsandboxApi.ts:210)是个判别式联合类型,在 TS 里 action: 'kill'keepMemory 直接编译报错;但 JS 调用方绕过类型,所以 createSandbox 又在运行时重查一遍(注释见 :1126-1128)。类型管 TS 用户,运行时管 JS 用户。

最后一道闸:envd 版本。 create 成功后还要比对返回的 envdVersion——低于 0.1.0 就地 kill 掉并抛 TemplateErrorsandboxApi.ts:1190-1195),因为旧守护进程配不上新 SDK 的数据面协议。

3.3 pause:全内存快照 vs 仅文件系统快照

pausesandboxApi.ts:982)POST 到 /sandboxes/{sandboxID}/pause,body 里只有一个布尔:

body: { memory: opts?.keepMemory ?? true } // sandboxApi.ts:995-997

这一个布尔决定了两种截然不同的暂停:

keepMemory: true ── 全内存快照 ──▶ 冻结整机(含 RAM、运行中的进程、打开的连接)
恢复 = 原地解冻,进程还在跑

keepMemory: false ── 仅文件系统 ──▶ 只存磁盘,丢掉内存状态
恢复 = 冷启动(reboot),进程/连接全丢

这正是 3.2 里校验③的物理根源:没有内存快照,就没有「原地唤醒」这回事——只能从磁盘重新开机。所以「仅文件系统 + autoResume」是自相矛盾的,必须靠 connect() 显式冷启动。

pause 的返回值也讲究:true = 真的暂停了;遇到 HTTP 409 说明沙箱本就已暂停,此时返回 false 而不是抛错(sandboxApi.ts:1005-1008)——把「已经是目标状态」当成幂等成功。

betaPausesandboxApi.ts:1021)是 pause 的历史别名,已 @deprecated,直接转调 pause

3.4 connect:连上暂停的沙箱会自动恢复

Sandbox.connect(sandboxId) 的语义是「连到一台已存在的沙箱」,但它顺手做了恢复:如果那台沙箱是暂停态,连接动作本身就把它唤醒。

实现走 connectSandboxsandboxApi.ts:1206),POST 到 /sandboxes/{sandboxID}/connect,body 带一个新的 timeout

// sandbox/index.ts connect:353 —— 公有方法只是薄薄一层
const sandbox = await SandboxApi.connectSandbox(sandboxId, opts) // index.ts:367
return new this({ sandboxId, sandboxDomain, envdAccessToken, ... })

关键点:

  • 恢复是编排器侧的行为,SDK 只发一个 /connect。注意 OpenAPI 里另有 /sandboxes/{sandboxID}/resume 端点,但当前 SDK 的 connect 走的是 /connect,恢复语义由这一个端点承载。
  • 连不到就抛 SandboxNotFoundError,报错文案特意写成 "Paused sandbox ... not found"sandboxApi.ts:1228),提示你连的很可能是台已暂停/已过期的沙箱。
  • connect 的成功响应会带回新的 envdAccessToken / trafficAccessTokensandboxApi.ts:1236-1242),供数据面重新连 envd——控制面「恢复」完,数据面才能接着用。
  • keepMemory:false 存下来的仅文件系统快照,connect唯一的恢复入口(因为它无法被流量自动唤醒),且恢复即冷启动。

3.5 游标分页:Paginator + x-next-token

列沙箱(/v2/sandboxes)和列快照(/snapshots)可能返回很多条,用游标分页(cursor pagination):服务器每页在响应头 x-next-token 里给一个「下一页的书签」,客户端带着它请求下一页,直到没有 token。

抽象基类 Paginatorpaginator.ts:24)把这套簿记收口——子类只需实现 nextItems 去真正发请求:

// paginator.ts:58 —— 翻页状态只在这里更新
protected updatePagination(response: Response) {
this._nextToken = response.headers.get('x-next-token') || undefined
this._hasNext = !!this._nextToken // 没有下一页 token = 没有更多了
}

典型用法是 while (hasNext) 循环:

// 示意,非源码
const pages = Sandbox.list() // 返回 SandboxPaginator,不立刻请求
while (pages.hasNext) {
const batch = await pages.nextItems() // 每次一页,内部更新 nextToken/hasNext
console.log(batch)
}

两个具体子类:

  • SandboxPaginatorsandboxApi.ts:1258)——nextItems GET /v2/sandboxes,还负责把 query.metadata 逐对 encodeURIComponent 后拼成查询串(:1272-1282),并支持按 staterunning/paused)过滤。
  • SnapshotPaginatorsandboxApi.ts:1336)——nextItems GET /snapshots,可按 sandboxId 过滤来源沙箱。

两者都在拿到响应后调 this.updatePagination(res.response):1304:1369)翻页,再把原始 schema 对象映射成整洁的 SandboxInfo / SnapshotInfo

3.6 错误映射:HTTP 状态码 → SandboxError 子类

控制面每个方法的收尾都是同一套路:先挑出已知状态码,再交给 handleApiError 兜底

逐方法的特判(在调 handleApiError 之前):

状态码处理例子
404视方法而定:kill/deleteSnapshot 返回 false(幂等删除);getInfo/setTimeout/getMetrics/updateNetworkSandboxNotFoundErrorsandboxApi.ts:736:772
409pause:已暂停 → 返回 falsesandboxApi.ts:1005

兜底映射 handleApiErrorapi/index.ts:26)——所有方法共用:

HTTP抛出
2xx(response.ok不抛,返回 undefined
401AuthenticationError("Unauthorized, please check your credentials")
429RateLimitError("Rate limit exceeded")
其它非 2xx传入的 errorClass(默认 SandboxError),消息为 "{status}: {message}"

一个值得记的细节(api/index.ts:34-38):openapi-fetch 在响应 Content-Length: 0 时会把 error 留成 undefined,所以这里response.response.ok(看状态码)判断成败,而不是看 error 存不存在——否则一个空 body 的 500 会被误判成成功。

和数据面错误映射的区别。 本章 handleApiError控制面侧、openapi-fetch 的响应对象上做的映射;数据面(envd)用的是 gRPC/connect 那套 code → 错误类的映射,是另一条链路,见 ch02。两者都收敛到 errors.ts 里同一棵 SandboxError 继承树(errors.ts:13),但触发点和输入形状不同。


4. 巧妙之处(可借鉴)

  • 契约即类型,规范即事实源。 端点、参数、请求/响应体全从 OpenAPI 规范生成(schema.gen.ts),ApiClientcreateClient<paths> 套上,写错端点直接编译不过。改协议只改 spec/make codegen,多语言 SDK 一起同步——见 一份 spec,多个 SDK
  • 非法生命周期组合在客户端就拦。 三道 InvalidArgumentErrorsandboxApi.ts:1139-1155)把「keepMemory 配 kill」「autoResume 配 kill」「autoResume 配仅文件系统快照」挡在发请求之前,报错文案直接解释物理原因,比等服务器 400 友好得多。
  • 幂等语义藏在返回值里。 kill/deleteSnapshot 对 404 返回 falsepause 对 409 返回 false——「东西不在了」或「已经是目标状态」被当成成功而非异常,调用方不必包 try/catch。
  • 分页簿记抽象成基类。 Paginatorx-next-token 的读取与 hasNext 收在一处(paginator.ts:58),新列表端点只写 nextItems 即可,不重复实现翻页。
  • 用状态码而非 error 字段判失败。 handleApiError 绕开 openapi-fetch 对空 body 非 2xx 响应留空 error 的坑(api/index.ts:34-38)。

5. 边界与局限

  • 控制面 ≠ 数据面。 本章不覆盖连 envd、URL 推导、流的生命周期(ch02),也不覆盖命令/PTY(ch03)、文件系统(ch04)、模板构建(ch05)。
  • 单位换算易错。 SDK 对外一律毫秒timeoutMs),编排器要——createSandbox/setTimeout/connectSandbox 都靠 timeoutToSeconds 转换(sandboxApi.ts:921:1162:1222)。绕过 SDK 直接打 REST 时别忘了。
  • debug 模式短路。 config.debug 为真时,kill 直接返回 truegetMetrics 返回 []create/connect 返回假 ID 而不发请求(sandboxApi.ts:720:846index.ts:359),用于本地开发,不代表真实编排器行为。
  • 快照即模板。 deleteSnapshot/templates/{templateID}createSnapshot 返回的 snapshotId 可直接喂给 Sandbox.create——快照和模板在编排器眼里同源(ch05)。
  • 列沙箱固定走 /v2/sandboxes SandboxPaginator 用的是 v2 分页端点,不是 /sandboxes;老代码若直接调 /sandboxes 列表得不到游标分页。

6. 代码地图(导航索引)

主题文件符号
控制面动作总入口packages/js-sdk/src/sandbox/sandboxApi.tsSandboxApi
创建 + 生命周期校验packages/js-sdk/src/sandbox/sandboxApi.tsSandboxApi.createSandbox
恢复(连接暂停沙箱)packages/js-sdk/src/sandbox/sandboxApi.tsSandboxApi.connectSandbox
暂停(内存/文件系统快照)packages/js-sdk/src/sandbox/sandboxApi.tsSandboxApi.pause
销毁packages/js-sdk/src/sandbox/sandboxApi.tsSandboxApi.kill
查信息 / 指标 / 超时 / 网络packages/js-sdk/src/sandbox/sandboxApi.tsgetInfo · getMetrics · setTimeout · updateNetwork
快照 增/删/列packages/js-sdk/src/sandbox/sandboxApi.tscreateSnapshot · deleteSnapshot · listSnapshots
超时动作类型(判别式联合)packages/js-sdk/src/sandbox/sandboxApi.tsSandboxOnTimeout · SandboxLifecycle
类型安全 REST 客户端packages/js-sdk/src/api/index.tsApiClient
HTTP 状态码 → 错误类packages/js-sdk/src/api/index.tshandleApiError
密钥格式校验packages/js-sdk/src/api/index.tsvalidateApiKey
生成的 OpenAPI 类型packages/js-sdk/src/api/schema.gen.tspaths · components
默认请求头 / 版本packages/js-sdk/src/api/metadata.tsdefaultHeaders · version
游标分页基类packages/js-sdk/src/paginator.tsPaginator.updatePagination
沙箱 / 快照分页器packages/js-sdk/src/sandbox/sandboxApi.tsSandboxPaginator · SnapshotPaginator
错误继承树packages/js-sdk/src/errors.tsSandboxError · SandboxNotFoundError · InvalidArgumentError
公有 create/connect 门面packages/js-sdk/src/sandbox/index.tsSandbox.create · Sandbox.connect