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

并排迁移 sandbox

最后更新 查看 MarkdownAgent 设置

在同一个 Worker 中让 Sandbox SDK 1.0 类与 0.12 类并排运行,并在请求第一次到达某个 sandbox 时迁移它。尚未迁移的 sandbox 中的命令和连接会继续运行。当没有 sandbox 再需要 0.12 类时,删除它。

开始之前

  • 你的应用运行 @cloudflare/sandbox 0.12.10,类和绑定名为 Sandbox。
  • 你的 1.0 类用新名称编写,例如 SandboxV1。选择你想保留的名称,因为稍后重命名类是另一次类迁移。有关该类,请参阅 替换 Sandbox 类。
  • 你有一份带有独立 Wrangler 配置文件的预发布 Worker,使用不同的 Worker name,以及每个容器不同的 name。

本页上的代码将 /workspace 作为单个归档从 0.12 容器复制到 1.0 容器。已停止的 0.12 容器没有可复制的文件。归档以 base64 编码在一条 RPC 消息中传输,限制为 32 MiB。对于更大的工作区,请保存 0.12 备份并按 迁移备份 所述转换它。

保留其他迁移页面让你移除的 0.12 导出、变量和密钥,例如 ContainerProxy、SANDBOX_TRANSPORT 和 R2_ACCESS_KEY_ID,直到你删除 0.12 类。0.12 类仍会读取它们。

添加 1.0 类

  1. 安装该包的 1 版本,并在别名 sandbox-0x 下保留 0.12:

    npm i @cloudflare/sandbox sandbox-0x@npm:@cloudflare/[email protected]

    包管理器可能保存版本范围。在 package.json 中将 sandbox-0x 设为 npm:@cloudflare/[email protected],以便 0.12 代码留在该版本。在 0.12 代码中,将每个从 @cloudflare/sandbox 的导入改为 sandbox-0x。

  2. 给 0.12 类添加一个报告其容器是否运行的方法,以及一个返回 1.0 代码读取的存储的方法。如果你的代码从包导出该类,请用子类替换该导出:

    src/index.tsts
    import { getSandbox, Sandbox as SandboxBase } from "sandbox-0x";
    
    // Preview tokens, named tunnels, and outbound rules set at runtime.
    const STORAGE_0X = [
        "portTokens",
        "tunnels",
        "tunnels:meta",
        "OUTBOUND_CONFIGURATION",
    ];
    
    export class Sandbox extends SandboxBase<Env> {
        // A 0.12 container keeps its files only while it runs.
        isRunning(): boolean {
            return this.ctx.container?.running ?? false;
        }
    
        exportStorage(): Promise<Map<string, unknown>> {
            return this.ctx.storage.get(STORAGE_0X);
        }
    }

    如果已有子类,请将常量和这些方法添加到它。

  3. 为 1.0 类创建 Dockerfile.v1。在删除 0.12 类之前保留 0.12 Dockerfile:

    Dockerfile.v1dockerfile
    FROM node:24-trixie-slim
    
    COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim
    
    WORKDIR /workspace
    CMD ["sleep", "infinity"]

    复制步骤需要 tar、gzip 和 base64,此镜像包含它们。添加命令使用的工具。当其他迁移页面向 Dockerfile 添加一行时(例如迁移隧道上的 cloudflared 复制),请将其添加到 Dockerfile.v1。

  4. 将 1.0 类添加到 Wrangler 配置,带有自己的容器、绑定和迁移。保持 Sandbox 条目不变。将 my-worker-sandbox-v1 替换为账户尚未使用的名称:

    {
        "containers": [
            {
                "class_name": "Sandbox",
                "image": "./Dockerfile",
                // Your other 0.12 settings, unchanged.
            },
            {
                "class_name": "SandboxV1",
                "name": "my-worker-sandbox-v1",
                "scheduling_policy": "durable_object",
                "images": {
                    "sandbox": {
                        "dockerfile": "./Dockerfile.v1",
                    },
                },
            },
        ],
        "durable_objects": {
            "bindings": [
                { "class_name": "Sandbox", "name": "Sandbox" },
                { "class_name": "SandboxV1", "name": "SandboxV1" },
            ],
        },
        "migrations": [
            { "new_sqlite_classes": ["Sandbox"], "tag": "v1" },
            { "new_sqlite_classes": ["SandboxV1"], "tag": "v2" },
        ],
    }
    [[containers]]
    class_name = "Sandbox"
    image = "./Dockerfile"
    
    [[containers]]
    class_name = "SandboxV1"
    name = "my-worker-sandbox-v1"
    scheduling_policy = "durable_object"
    
    [containers.images.sandbox]
    dockerfile = "./Dockerfile.v1"
    
    [[durable_objects.bindings]]
    class_name = "Sandbox"
    name = "Sandbox"
    
    [[durable_objects.bindings]]
    class_name = "SandboxV1"
    name = "SandboxV1"
    
    [[migrations]]
    new_sqlite_classes = [ "Sandbox" ]
    tag = "v1"
    
    [[migrations]]
    new_sqlite_classes = [ "SandboxV1" ]
    tag = "v2"

    如果 Worker 在 Wrangler 配置中用 exports 声明类,请在那里添加条目,而不是迁移:

    {
        "exports": {
            "Sandbox": { "type": "durable-object", "storage": "sqlite" },
            "SandboxV1": { "type": "durable-object", "storage": "sqlite" },
        },
    }
    [exports.Sandbox]
    type = "durable-object"
    storage = "sqlite"
    
    [exports.SandboxV1]
    type = "durable-object"
    storage = "sqlite"
  5. 向 1.0 类添加迁移一个 sandbox 的方法。每个名称的复制只运行一次,并存储 moved,以便后续请求跳过它:

    src/index.tsts
    export class SandboxV1 extends DurableObject<Env> {
        // ...
    
        private moving: Promise<void> | null = null;
    
        async moveFrom0x(name: string): Promise<void> {
            // Requests that arrive during the copy wait for the same copy.
            this.moving ??= this.copyFrom0x(name).finally(() => {
                this.moving = null;
            });
            await this.moving;
        }
    
        private async copyFrom0x(name: string): Promise<void> {
            if (await this.ctx.storage.get("moved")) {
                return;
            }
    
            const stub0x = this.env.Sandbox.getByName(name);
            const stored = await stub0x.exportStorage();
            await this.ctx.storage.put(Object.fromEntries(stored));
    
            if (await stub0x.isRunning()) {
                const legacy = getSandbox(this.env.Sandbox, name);
                // Stop the background processes, so no file changes during the copy.
                await legacy.killAllProcesses();
    
                const path = "/tmp/workspace.tar.gz";
                const packed = await legacy.exec(`tar -czf ${path} -C /workspace .`);
    
                if (!packed.success) {
                    throw new Error(packed.stderr);
                }
    
                const archive = await legacy.readFile(path, {
                    encoding: "base64",
                });
    
                await this.ensureRunning();
                const extract = await this.container.exec(
                    ["sh", "-c", "base64 -d | tar -xzf - -C /workspace"],
                    { stdin: new Blob([archive.content]).stream() },
                );
                const result = await extract.output();
    
                if (result.exitCode !== 0) {
                    const stderr = new TextDecoder().decode(result.stderr);
                    throw new Error(stderr);
                }
    
                // destroy() would delete named tunnels and DNS records.
                await legacy.stop();
                await this.ctx.storage.put("moved", true);
                return;
            }
    
            await this.ctx.storage.put("moved", true);
        }
    }

    ensureRunning() 和 container 是 1.0 类中的辅助方法,前者启动容器,后者返回 this.ctx.container。该方法会先复制 0.12 存储,因此即使 0.12 容器已停止,预览 URL、命名隧道和出站规则也会迁移。如果复制或 stop() 失败,该方法会在存储 moved 前抛出异常,下一次请求会重新执行迁移。

    复制过程会保留运行 tar 时的文件状态。killAllProcesses() 会停止 0.12 startProcess() 启动的进程。其他请求通过 0.12 运行的命令仍可能写入文件,因此迁移期间不要向该 sandbox 的 0.12 类发送其他请求。下一步中通过 sandboxFor() 发出的请求会等待迁移完成。

    0.12 stop() 会保留 0.12 类的存储,以及账户中的隧道和 DNS 记录。0.12 destroy() 会删除预览令牌、隧道条目,以及每个命名隧道和对应的 DNS 记录。进程会随 0.12 容器结束,因此请在 1.0 容器中重新启动应用所需的进程,具体请参阅迁移后台进程。若要使用相同的命令,请在调用 killAllProcesses() 前从 legacy.listProcesses() 读取它们。

  6. 让每个请求都经过迁移。将此函数添加到 Worker,并将每个 getSandbox(env.Sandbox, name) 调用替换为 await sandboxFor(env, name):

    src/index.tsts
    async function sandboxFor(env: Env, name: string) {
        const sandbox = env.SandboxV1.getByName(name);
        await sandbox.moveFrom0x(name);
        return sandbox;
    }

    新 sandbox 会在 1.0 类上启动,因为它们的名称没有正在运行的 0.12 容器。

    传入 0.12 使用的名称。如果 Worker 在调用 getSandbox() 时传入 normalizeId: true,请在调用 sandboxFor() 前将名称转换为小写,以便两个类使用该 sandbox 的同一个 0.12 Durable Object。

  7. 生成类型并部署:

    npx wrangler types
    npx wrangler deploy

    部署会报告 0.12 容器应用没有更改。正在运行的 0.12 容器会保留其文件和进程。

检查迁移

向 0.12 容器正在运行且 /workspace 中有文件的 sandbox 发送请求。第一次请求会复制文件并从 1.0 容器返回结果。对同一名称的第二次请求会跳过复制并更快返回。向新名称发送请求,它只会启动 1.0 容器。

删除 0.12 类

  1. 迁移剩余的 sandbox。Worker 无法列出 Durable Objects,因此请使用应用的记录,为每个尚未迁移的 sandbox 发送一次请求。

  2. 移除 0.12 代码。删除 0.12 类、STORAGE_0X、sandbox-0x 导入,以及 1.0 类中的 moving 字段、moveFrom0x() 和 copyFrom0x()。在代码曾调用 sandboxFor() 的地方调用 env.SandboxV1.getByName(name)。同时移除你保留的 0.12 导出、变量和密钥,例如 ContainerProxy、SANDBOX_TRANSPORT 和 R2_ACCESS_KEY_ID。

  3. 从 Wrangler 配置中移除 Sandbox 容器和绑定,并添加删除该类的迁移:

    {
        "migrations": [
            { "new_sqlite_classes": ["Sandbox"], "tag": "v1" },
            { "new_sqlite_classes": ["SandboxV1"], "tag": "v2" },
            { "deleted_classes": ["Sandbox"], "tag": "v3" },
        ],
    }
    [[migrations]]
    new_sqlite_classes = [ "Sandbox" ]
    tag = "v1"
    
    [[migrations]]
    new_sqlite_classes = [ "SandboxV1" ]
    tag = "v2"
    
    [[migrations]]
    deleted_classes = [ "Sandbox" ]
    tag = "v3"

    如果 Worker 在 Wrangler 配置中使用 exports,请改为将该类标记为已删除:

    {
        "exports": {
            "Sandbox": { "type": "durable-object", "state": "deleted" },
            "SandboxV1": { "type": "durable-object", "storage": "sqlite" },
        },
    }
    [exports.Sandbox]
    type = "durable-object"
    state = "deleted"
    
    [exports.SandboxV1]
    type = "durable-object"
    storage = "sqlite"
  4. 移除别名,生成类型并部署:

    npm uninstall sandbox-0x
    npx wrangler types
    npx wrangler deploy
  5. 删除类不会删除其容器应用。Wrangler 根据 Worker 和类名命名该应用,例如 my-worker-sandbox。列出应用以找到它:

    npx wrangler containers list
  6. 删除旧应用。将 <OLD_APPLICATION_ID> 替换为列表中的 ID:

    npx wrangler containers delete <OLD_APPLICATION_ID>

    旧应用会继续运行其容器并为它们计费,直到你删除它。

相关资源

这篇文档对您有帮助吗?