Durable Object 中的 alarm 会为空闲 sandbox 保存 Container snapshot 并停止它。sandbox 使用期间,alarm 也会保存检查点。下一次请求会从最后一个 snapshot 启动 sandbox。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
-
在
src/index.ts顶部定义 Durable Object 检查 sandbox 的频率、sandbox 可以在没有请求的情况下持续多久,以及使用中的 sandbox 多久保存一次:src/index.tsts const CHECK_INTERVAL_MS = 60 * 1000; const IDLE_LIMIT_MS = 10 * 60 * 1000; const CHECKPOINT_INTERVAL_MS = 15 * 60 * 1000; const INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;不活动超时比检查间隔更长,因此实例会在检查之间继续运行。如果检查停止,超时仍会停止实例,sandbox 会保留其最近一次保存的文件。
-
在
MyContainer中添加构造函数。当重启后的 Durable Object 发现容器仍在运行时,构造函数会重新设置非活动超时,避免实例在下次检查前停止:src/index.tsts constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } }然后替换
exec()。如果存在已存储的快照,该方法会从快照启动新实例、安排首次检查并记录活动:src/index.tsts busy = 0; async exec(argv: string[]) { const container = this.ctx.container; if (!container) { throw new Error("No container is configured"); } if (!container.running) { const snapshotId = await this.ctx.storage.get<string>("snapshotId"); container.start({ ...(snapshotId ? { containerSnapshot: { id: snapshotId } } : { image: "cloudflare/debian-trixie" }), entrypoint: ["sleep", "infinity"], enableInternet: false, }); await this.ctx.storage.put("savedAt", Date.now()); } await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); if ((await this.ctx.storage.getAlarm()) === null) { await this.ctx.storage.setAlarm(Date.now() + CHECK_INTERVAL_MS); } this.busy++; await this.ctx.storage.put("lastActivity", Date.now()); try { const process = await container.exec(argv); const output = await process.output(); return { stdout: new TextDecoder().decode(output.stdout), exitCode: output.exitCode, }; } finally { this.busy--; await this.ctx.storage.put("lastActivity", Date.now()); } }savedAt保存文件最近一次保存或恢复的时间。busy统计正在运行的命令,Durable Object 不会在命令运行时保存或停止 sandbox。进行中的请求会将 Durable Object 保留在内存中,因此busy不需要存储。如果无法恢复已存储的 snapshot,start()会失败。要处理该失败,请参阅 使用 snapshot 保存并恢复 sandbox。 -
添加保存 snapshot 的方法,以及保存并停止空闲 sandbox、同时为使用中的 sandbox 保存检查点的 alarm handler:
src/index.tsts async save() { const startedAt = Date.now(); const snapshot = await this.ctx.container!.snapshotContainer({ name: "auto", }); await this.ctx.storage.put({ snapshotId: snapshot.id, savedAt: startedAt }); } async alarm() { const container = this.ctx.container; if (!container?.running) { return; } const lastActivity = (await this.ctx.storage.get<number>("lastActivity")) ?? 0; const savedAt = (await this.ctx.storage.get<number>("savedAt")) ?? 0; const idle = this.busy === 0 && Date.now() - lastActivity > IDLE_LIMIT_MS; const checkpoint = this.busy === 0 && lastActivity > savedAt && Date.now() - savedAt > CHECKPOINT_INTERVAL_MS; if (idle || checkpoint) { try { await this.save(); } catch (error) { // Keep the instance and try again at the next check. console.error("Saving the sandbox failed", error); await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); await this.ctx.storage.setAlarm(Date.now() + CHECK_INTERVAL_MS); return; } } const activeDuringSave = this.busy > 0 || (await this.ctx.storage.get<number>("lastActivity")) !== lastActivity; if (idle && !activeDuringSave) { await container.destroy("idle"); return; } await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); await this.ctx.storage.setAlarm(Date.now() + CHECK_INTERVAL_MS); }保存快照需要几秒钟,期间实例会继续运行命令。如果保存期间有请求到达,闹钟不会停止实例。后续快照会保存保存期间写入的文件。如果保存失败,处理程序会让实例继续运行并安排下次检查,以便重试。如果部署中断保存,运行时会再次运行闹钟。
检查点会限制当 alarm 以外的原因停止 sandbox 时(例如检查停止后的不活动超时)sandbox 会丢失的内容。sandbox 会恢复最后一个检查点,不含之后的更改。
Durable Object 只有一个 alarm。如果你的类已有
alarm()handler,请将空闲和检查点检查合并进去,并将 alarm 设为任何检查所需的最早时间。更多信息请参阅 Alarms。snapshot 会保存可写根文件系统。它不包含挂载的目录、内存或正在运行的进程。在保存期间写入文件的后台进程可能只把该文件的一部分留在 snapshot 中。
每次保存都会创建新的 snapshot。Durable Object container API 没有删除 snapshot 的方法。snapshot 会在创建或最近一次恢复后 30 天过期。要保存更少 snapshot,请增大
CHECKPOINT_INTERVAL_MS或移除检查点。 -
添加返回 sandbox 状态的方法:
src/index.tsts async status() { return { running: this.ctx.container?.running ?? false, snapshotId: (await this.ctx.storage.get<string>("snapshotId")) ?? null, }; } -
向 Worker 添加在具名 sandbox 中运行命令并返回其状态的路由:
src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)$/.exec(url.pathname); if (!match) { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName(match[1]); if (request.method === "GET") { return Response.json(await sandbox.status()); } const { argv } = (await request.json()) as { argv: string[] }; return Response.json(await sandbox.exec(argv)); }, };snapshot 可能包含 sandbox 中写入的凭据或其他敏感文件。请先认证调用方,并为每个用户提供独立的 sandbox 名称。更多信息请参阅 Sandbox 安全。
-
部署 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
在名为
ada的 sandbox 中写入文件。将示例 hostname 替换为 Wrangler 打印的workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada --json '{"argv":["sh","-c","mkdir -p /workspace && echo hello > /workspace/notes.txt"]}' -
11 分钟内不要向
ada发送请求,然后读取其状态:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada{ "running": false, "snapshotId": "<SNAPSHOT_ID>" }sandbox 保存了 snapshot 并停止。下一次请求会从 snapshot 启动新实例。读取该文件:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada --json '{"argv":["cat","/workspace/notes.txt"]}'{ "stdout": "hello\n", "exitCode": 0 }
- 使用 snapshot 保存并恢复 sandbox:在应用决定时保存、复制 sandbox,以及重新开始 sandbox。
- Sandbox 生命周期:snapshot 保存什么,以及恢复的实例从什么开始。
- sandbox 停止时运行代码
snapshotContainer()