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

在后台运行 server

最后更新 查看 MarkdownAgent 设置

启动服务器,使其持续运行到容器停止,例如开发服务器、隧道或代码内核。与作业不同,不会有任何进程等待服务器退出。Durable Object 会在首次使用时启动服务器,等待服务器响应,并在每个新容器中重新启动服务器。

对于会运行到完成的进程(例如构建或 Agent 任务),请参阅 运行后台进程。如果 sandbox 只运行一个 server 且没有你需要保留的文件,可以将该 server 作为主进程,如 预览 Web 应用 所示。主进程退出时,实例会停止,其文件也会随之结束。

前提条件

运行 server

  1. 添加用于启动、检查和停止 server 的 shell 脚本和函数:

    src/index.jsjs
    const SERVERS = "/var/lib/servers";
    const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
    
    // Defines current(), which reads the process ID in a directory into $pid
    // when the process started in this instance.
    const CURRENT = `current() {
        read -r pid boot 2>/dev/null <"$1/pid" &&
            [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ]
    }`;
    
    // Starts the server unless it already runs. The server runs in its own
    // process group, and writes its process ID and output to its directory.
    const SERVE = `${CURRENT}
    dir=$1; shift
    current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0
    mkdir -p "$dir" && rm -f "$dir/pid"
    setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\
        "$dir" "$@" >"$dir/log" 2>&1`;
    
    // Prints the end of the log and exits with 0 when the server has exited.
    const EXITED = `${CURRENT}
    current "$1" || exit 1
    kill -0 "$pid" 2>/dev/null && exit 1
    tail -n 20 "$1/log"`;
    
    // Stops the process group, with SIGKILL if the server still runs after
    // 10 seconds. Once the server exits, removes its process ID.
    const STOP = `${CURRENT}
    current "$1" || exit 0
    kill -s TERM -- "-$pid" 2>/dev/null
    timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" ||
        kill -s KILL -- "-$pid" 2>/dev/null
    while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done
    rm -f "$1/pid"`;
    
    async function startServer(container, dir, argv, options = {}) {
    	// Ignoring the output lets the server outlive this request.
    	await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], {
    		...options,
    		stdout: "ignore",
    		stderr: "ignore",
    	});
    }
    
    async function serverExited(container, dir) {
    	const check = await container.exec(["sh", "-c", EXITED, "sh", dir]);
    	const output = await check.output();
    
    	return output.exitCode === 0
    		? new TextDecoder().decode(output.stdout)
    		: undefined;
    }
    
    async function waitForServer(container, dir, port, timeoutMs) {
    	const server = container.getTcpPort(port);
    	const deadline = Date.now() + timeoutMs;
    
    	while (Date.now() < deadline) {
    		try {
    			const response = await server.fetch("http://server/", {
    				signal: AbortSignal.timeout(1_000),
    			});
    			await response.body?.cancel();
    			return server;
    		} catch {
    			const log = await serverExited(container, dir);
    
    			if (log !== undefined) {
    				throw new Error(`The server exited: ${log}`);
    			}
    
    			await scheduler.wait(500);
    		}
    	}
    
    	throw new Error("The server did not answer in time");
    }
    
    async function stopServer(container, dir) {
    	const stop = await container.exec(["sh", "-c", STOP, "sh", dir]);
    	const exitCode = await stop.exitCode;
    
    	if (exitCode !== 0) {
    		throw new Error(`Stopping the server failed with exit code ${exitCode}`);
    	}
    }
    src/index.tsts
    const SERVERS = "/var/lib/servers";
    const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
    
    // Defines current(), which reads the process ID in a directory into $pid
    // when the process started in this instance.
    const CURRENT = `current() {
        read -r pid boot 2>/dev/null <"$1/pid" &&
            [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ]
    }`;
    
    // Starts the server unless it already runs. The server runs in its own
    // process group, and writes its process ID and output to its directory.
    const SERVE = `${CURRENT}
    dir=$1; shift
    current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0
    mkdir -p "$dir" && rm -f "$dir/pid"
    setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\
        "$dir" "$@" >"$dir/log" 2>&1`;
    
    // Prints the end of the log and exits with 0 when the server has exited.
    const EXITED = `${CURRENT}
    current "$1" || exit 1
    kill -0 "$pid" 2>/dev/null && exit 1
    tail -n 20 "$1/log"`;
    
    // Stops the process group, with SIGKILL if the server still runs after
    // 10 seconds. Once the server exits, removes its process ID.
    const STOP = `${CURRENT}
    current "$1" || exit 0
    kill -s TERM -- "-$pid" 2>/dev/null
    timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" ||
        kill -s KILL -- "-$pid" 2>/dev/null
    while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done
    rm -f "$1/pid"`;
    
    type ServerOptions = { cwd?: string; env?: Record<string, string> };
    
    async function startServer(
        container: Container,
        dir: string,
        argv: string[],
        options: ServerOptions = {},
    ): Promise<void> {
        // Ignoring the output lets the server outlive this request.
        await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], {
            ...options,
            stdout: "ignore",
            stderr: "ignore",
        });
    }
    
    async function serverExited(
        container: Container,
        dir: string,
    ): Promise<string | undefined> {
        const check = await container.exec(["sh", "-c", EXITED, "sh", dir]);
        const output = await check.output();
    
        return output.exitCode === 0
            ? new TextDecoder().decode(output.stdout)
            : undefined;
    }
    
    async function waitForServer(
        container: Container,
        dir: string,
        port: number,
        timeoutMs: number,
    ): Promise<Fetcher> {
        const server = container.getTcpPort(port);
        const deadline = Date.now() + timeoutMs;
    
        while (Date.now() < deadline) {
            try {
                const response = await server.fetch("http://server/", {
                    signal: AbortSignal.timeout(1_000),
                });
                await response.body?.cancel();
                return server;
            } catch {
                const log = await serverExited(container, dir);
    
                if (log !== undefined) {
                    throw new Error(`The server exited: ${log}`);
                }
    
                await scheduler.wait(500);
            }
        }
    
        throw new Error("The server did not answer in time");
    }
    
    async function stopServer(container: Container, dir: string): Promise<void> {
        const stop = await container.exec(["sh", "-c", STOP, "sh", dir]);
        const exitCode = await stop.exitCode;
    
        if (exitCode !== 0) {
            throw new Error(`Stopping the server failed with exit code ${exitCode}`);
        }
    }

    如果目录中的服务器已在运行,SERVE 不会启动新进程;而没有服务器的新容器则会启动一个。Durable Object 重启而容器仍在运行时,也适用此行为。

    进程 ID 文件还会记录 boot ID,每个实例中该值都会变化。snapshot 会恢复该文件,新实例会复用相同的进程 ID,因此恢复的进程 ID 可能属于无关进程。current() 会忽略来自其他实例的进程 ID。

    setsid 会在新的进程组中启动 server,因此 STOP 也会停止该 server 启动的进程。运行 SERVE 的 shell 会留在 container 中直到 server 退出。不要改用 & 启动 server:shell 启动后台作业时会忽略 SIGINT。

    STOP 会在服务器退出后移除进程 ID,因此紧接着在同一目录启动的服务器不会显示为已退出。忽略 SIGTERM 的服务器会在 10 秒后收到 SIGKILL。

    waitForServer() 将任何 HTTP 响应视为就绪。如果 server 先退出,它会抛出包含日志末尾的错误。日志会一直增长直到 server 停止,因此请保持繁忙 server 的输出较小,或将其发送到挂载的 bucket。

  2. 向 Durable Object 添加构造函数。当重启后的 Durable Object 发现 container 仍在运行时,它会再次设置不活动超时:

    src/index.tsts
    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),
                );
            }
        }
    }

    然后添加先启动 server 再向其发送请求、以及停止它的方法:

    src/index.tsts
    const WEB = `${SERVERS}/web`;
    const WEB_PORT = 8080;
    const WEB_SERVER = [
        "node",
        "--eval",
        `require("node:http")
            .createServer((req, res) => res.end("hello from the server\\n"))
            .listen(${WEB_PORT})`,
    ];
    
    export class MyContainer extends DurableObject<Env> {
        // ...
    
        // The server start that requests in progress share
        private starting?: Promise<Fetcher>;
    
        async fetchServer(request: Request): Promise<Response> {
            const container = this.ctx.container;
    
            if (!container) {
                throw new Error("No container is configured");
            }
    
            if (!container.running) {
                container.start({
                    image: "cloudflare/debian-trixie",
                    entrypoint: ["sleep", "infinity"],
                    enableInternet: false,
                });
                await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
            }
    
            this.starting ??= startServer(container, WEB, WEB_SERVER)
                .then(() => waitForServer(container, WEB, WEB_PORT, 30_000))
                .finally(() => {
                    this.starting = undefined;
                });
            const server = await this.starting;
            return server.fetch(request);
        }
    
        async stopWeb(): Promise<void> {
            const container = this.ctx.container;
    
            if (container?.running) {
                await stopServer(container, WEB);
            }
        }
    }

    同时到达的请求会共用一次启动操作。服务器记录进程 ID 之前,startServer() 就会返回,因此在这段时间内,第二次 SERVE 会启动另一个服务器。第二个服务器会因端口冲突而失败,并覆盖进程 ID 文件,导致 STOP 无法停止第一个服务器。服务器响应后,每个请求都会再次运行 SERVE;服务器仍在运行时不会启动新进程,服务器已退出时则会重新启动。

    正在运行的 server 不会让 container 保持运行。通过 Durable Object 到达 server 的请求会像其他请求一样让它保持运行。不活动超时结束时,container 会停止,server 也随之停止。更多信息请参阅 Sandbox 生命周期。

  3. 向 Worker 添加调用这些方法的路由:

    src/index.jsjs
    export default {
    	async fetch(request, env) {
    		const url = new URL(request.url);
    
    		if (url.pathname !== "/server") {
    			return new Response("Not found", { status: 404 });
    		}
    
    		const sandbox = env.MY_CONTAINER.getByName("sandbox");
    
    		if (request.method === "DELETE") {
    			await sandbox.stopWeb();
    			return new Response(null, { status: 202 });
    		}
    
    		// getTcpPort() accepts only http:// URLs.
    		const forward = new Request("http://container/", request);
    		return sandbox.fetchServer(forward);
    	},
    };
    src/index.tsts
    export default {
        async fetch(request: Request, env: Env): Promise<Response> {
            const url = new URL(request.url);
    
            if (url.pathname !== "/server") {
                return new Response("Not found", { status: 404 });
            }
    
            const sandbox = env.MY_CONTAINER.getByName("sandbox");
    
            if (request.method === "DELETE") {
                await sandbox.stopWeb();
                return new Response(null, { status: 202 });
            }
    
            // getTcpPort() accepts only http:// URLs.
            const forward = new Request("http://container/", request);
            return sandbox.fetchServer(forward);
        },
    } satisfies ExportedHandler<Env>;
  4. 部署你的 Worker:

    npx wrangler deploy
  5. 向 server 发送请求。将示例 hostname 替换为 Wrangler 打印的 workers.dev URL:

    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server

    第一个请求会启动 container 和 server,并等到 server 应答:

    hello from the server
  6. 停止 server,然后发送另一个请求:

    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server --request DELETE
    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server

    DELETE 请求会以 202 响应。下一个请求会再次启动 server 并打印相同响应。

相关资源

这篇文档对您有帮助吗?