Durable Object 将 sandbox 的文件系统保存为 Container snapshot,存储 snapshot ID,并从该 snapshot 启动下一个实例。本例中,sandbox 在 /workspace 中保存 notes 文件。
- 带有 Durable Object 的 Worker,该 Durable Object 使用 Durable Object 调度策略 启动 container。要创建一个,请参阅 运行 Linux 命令。
-
为 Durable Object 添加不活动超时。当重启后的 Durable Object 发现 container 仍在运行时,构造函数会再次设置超时:
src/index.tsts const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; export class MyContainer extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } }然后在存在已存储 snapshot 时从该 snapshot 启动 container:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... private async startSandbox(): Promise<Container> { 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 container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } return container; } }如果无法恢复已存储的 snapshot,请求会失败,错误类似
Snapshot "<SNAPSHOT_ID>" was not found.。在存储的 ID 更改之前,后续请求会以同样方式失败。该方法不会回退到镜像,因为那样 sandbox 会从一个空工作区启动。 -
添加一个方法,确保每次只运行一个操作;再添加一个方法,用于保存文件系统并停止实例:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... private queue: Promise<unknown> = Promise.resolve(); // Runs each operation after the previous one finishes. private exclusive<T>(operation: () => Promise<T>): Promise<T> { const result = this.queue.then(operation); this.queue = result.catch(() => {}); return result; } async save() { return this.exclusive(async () => { const container = await this.startSandbox(); const snapshot = await container.snapshotContainer({ name: "workspace" }); await this.ctx.storage.put("snapshotId", snapshot.id); await container.destroy(); return { snapshotId: snapshot.id, size: snapshot.size }; }); } }快照运行时写入文件的命令可能会成功,但文件会在
destroy()停止实例后消失。exclusive()会让所有访问磁盘的操作等待快照完成,同时让快照等待这些操作结束。通过此方法运行所有此类方法。snapshot 会保存可写根文件系统。它不包含挂载的目录、内存或正在运行的进程。保存前请完成文件写入。关于恢复的实例从什么开始,请参阅 Sandbox 生命周期。
-
添加向文件追加一行并返回该文件的方法,以便检查恢复会带回什么:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async note(line?: string) { return this.exclusive(async () => { const container = await this.startSandbox(); const script = line ? 'mkdir -p /workspace && printf "%s\n" "$1" >> /workspace/notes.txt && cat /workspace/notes.txt' : "cat /workspace/notes.txt"; const process = await container.exec(["sh", "-c", script, "sh", line ?? ""]); const output = await process.output(); return new TextDecoder().decode(output.stdout); }); } } -
向 Worker 添加写入笔记、保存 sandbox 以及读取笔记的路由:
src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\/(notes|snapshot)$/.exec( url.pathname, ); if (!match) { return new Response("Not found", { status: 404 }); } const [, name, action] = match; const sandbox = env.MY_CONTAINER.getByName(name); if (action === "snapshot" && request.method === "POST") { return Response.json(await sandbox.save()); } if (action === "notes" && request.method === "POST") { return new Response(await sandbox.note(await request.text())); } if (action === "notes") { return new Response(await sandbox.note()); } return new Response("Method not allowed", { status: 405 }); }, };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])?)\/(notes|snapshot)$/.exec( url.pathname, ); if (!match) { return new Response("Not found", { status: 404 }); } const [, name, action] = match; const sandbox = env.MY_CONTAINER.getByName(name); if (action === "snapshot" && request.method === "POST") { return Response.json(await sandbox.save()); } if (action === "notes" && request.method === "POST") { return new Response(await sandbox.note(await request.text())); } if (action === "notes") { return new Response(await sandbox.note()); } return new Response("Method not allowed", { status: 405 }); }, } satisfies ExportedHandler<Env>;snapshot 可能包含 sandbox 中写入的凭据或其他敏感文件。请先认证调用方,以免其他人读取或恢复另一个 sandbox 的文件。更多信息请参阅 Sandbox 安全。
-
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
在名为
ada的 sandbox 中写入笔记,保存 sandbox,然后读取笔记。将示例 hostname 替换为 Wrangler 打印的workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada/notes --data "Continue from this checkpoint." curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada/snapshot --request POST curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada/notes保存会返回 snapshot ID 及其字节大小,例如
{"snapshotId":"<SNAPSHOT_ID>","size":123456}。最后一个请求会从 snapshot 启动新实例,笔记会回来:Continue from this checkpoint.
snapshot ID 可以在同一类的任何 Durable Object 中恢复。要将一个 sandbox 的文件复制到新 sandbox,请将该 snapshot ID 存储在新 sandbox 的 Durable Object 中:
-
向 Durable Object 添加返回已存储 snapshot ID 以及存储另一个 ID 的方法:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async snapshotId() { return (await this.ctx.storage.get<string>("snapshotId")) ?? null; } async useSnapshot(snapshotId: string) { return this.exclusive(async () => { // Replacing a running instance would discard files it has not saved. if (this.ctx.container?.running) { return false; } await this.ctx.storage.put("snapshotId", snapshotId); return true; }); } } -
向 Worker 添加将一个 sandbox 复制到另一个的路由:
src/index.tsts if (action === "copy" && request.method === "POST") { const from = url.searchParams.get("from") ?? ""; const snapshotId = await env.MY_CONTAINER.getByName(from).snapshotId(); if (!snapshotId || !(await sandbox.useSnapshot(snapshotId))) { return new Response("Cannot copy", { status: 409 }); } return new Response(null, { status: 204 }); }将
copy添加到路由模式中的操作。复制会收到 snapshot 中的每个文件,因此只在属于同一用户的 sandbox 之间复制。 -
将
ada复制到grace,然后从grace读取笔记:curl "https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/grace/copy?from=ada" --request POST curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/grace/notesContinue from this checkpoint.从此刻起,每个 sandbox 会更改自己的文件。
要丢弃 sandbox 的文件,请销毁其实例并删除已存储的 snapshot ID。下一次请求会从镜像启动:
export class MyContainer extends DurableObject<Env> {
// ...
async reset() {
return this.exclusive(async () => {
if (this.ctx.container?.running) {
await this.ctx.container.destroy();
}
await this.ctx.storage.delete("snapshotId");
});
}
}删除已存储的 ID 不会删除 snapshot,因此复制了它的 sandbox 仍可以恢复它,直到 snapshot 过期。
- 自动保存 sandbox:在 sandbox 因不活动而停止前保存,并在使用期间保存检查点。
- 将目录备份到 R2:将单个目录保存在你自己的 bucket 中,而不是整个文件系统。
- 检查点工作区示例 ↗:可部署的 Worker,用于保存、恢复和复制 sandbox。
- Sandbox 生命周期
- Durable Object 容器 API