跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

使用 snapshot 保存并恢复 sandbox

最后更新 查看 MarkdownAgent 设置

Durable Object 将 sandbox 的文件系统保存为 Container snapshot,存储 snapshot ID,并从该 snapshot 启动下一个实例。本例中,sandbox 在 /workspace 中保存 notes 文件。

前提条件

保存并恢复 sandbox

  1. 为 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 会从一个空工作区启动。

  2. 添加一个方法,确保每次只运行一个操作;再添加一个方法,用于保存文件系统并停止实例:

    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 生命周期。

  3. 添加向文件追加一行并返回该文件的方法,以便检查恢复会带回什么:

    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);
            });
        }
    }
  4. 向 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 安全。

  5. 部署你的 Worker:

    npx wrangler deploy
  6. 在名为 ada 的 sandbox 中写入笔记,保存 sandbox,然后读取笔记。将示例 hostname 替换为 Wrangler 打印的 workers.dev URL:

    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.

复制 sandbox

snapshot ID 可以在同一类的任何 Durable Object 中恢复。要将一个 sandbox 的文件复制到新 sandbox,请将该 snapshot ID 存储在新 sandbox 的 Durable Object 中:

  1. 向 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;
            });
        }
    }
  2. 向 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 之间复制。

  3. 将 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/notes
    Continue from this checkpoint.

    从此刻起,每个 sandbox 会更改自己的文件。

重新开始 sandbox

要丢弃 sandbox 的文件,请销毁其实例并删除已存储的 snapshot ID。下一次请求会从镜像启动:

src/index.tsts
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 过期。

相关资源

这篇文档对您有帮助吗?