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

自动保存 sandbox

最后更新 查看 MarkdownAgent 设置

Durable Object 中的 alarm 会为空闲 sandbox 保存 Container snapshot 并停止它。sandbox 使用期间,alarm 也会保存检查点。下一次请求会从最后一个 snapshot 启动 sandbox。

前提条件

在停止前保存 sandbox

  1. 在 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 会保留其最近一次保存的文件。

  2. 在 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。

  3. 添加保存 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 或移除检查点。

  4. 添加返回 sandbox 状态的方法:

    src/index.tsts
    async status() {
        return {
            running: this.ctx.container?.running ?? false,
            snapshotId: (await this.ctx.storage.get<string>("snapshotId")) ?? null,
        };
    }
  5. 向 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 安全。

  6. 部署 Worker:

    npx wrangler deploy
  7. 在名为 ada 的 sandbox 中写入文件。将示例 hostname 替换为 Wrangler 打印的 workers.dev URL:

    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada --json '{"argv":["sh","-c","mkdir -p /workspace && echo hello > /workspace/notes.txt"]}'
  8. 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
    }

相关资源

这篇文档对您有帮助吗?