多节点执行部署
使用 session 粘性和本地工作区扩展 Executor Worker 节点。
本文给出一套生产可用且不复杂的多节点执行方案:中心 Backend 统一持久化会话、任务队列和 worker 注册状态;每台执行机器运行一个 Executor Manager Worker,通过本机 Docker daemon 动态拉起 Executor 容器,并使用当前机器的本地 WORKSPACE_ROOT。
这个方案不要求 NFS。会话执行期间的工作区只在绑定 worker 本地可用;任务完成后,worker 会把工作区 manifest、文件和 archive 导出到 S3 兼容对象存储,Backend 和前端再通过 S3 读取产物。
架构图
核心原则
Executor Worker是执行节点,不是中心服务。它应该只暴露给 Backend 访问,不要直接暴露到公网。- 每个 worker 必须有唯一且稳定的
EXECUTOR_WORKER_ID。不要复制默认值到多台机器。 WORKSPACE_ROOT是当前 worker 的本地目录,不需要和其他节点一致。- session 首次被某个 worker claim 后会绑定到该 worker,后续 run、停止、清理、会话历史补偿和浏览器流都会回到这个 worker。
- 完成后的工作区文件通过 S3 访问,不依赖本地 worker 是否在线。
调度链路
准备阶段可能包含拉镜像、创建容器、同步 Skills/Plugins/Subagents、下载附件等操作,因此 worker 会在 claim -> start_run 期间续租。只要 worker 在准备阶段仍存活,就不会因为慢启动导致同一个 run 被其他 worker 重新领取。
节点部署
在每台执行机器上使用单独的 worker compose 文件:
docker compose -f docker-compose.worker.yml up -d建议每台 worker 至少配置这些变量:
BACKEND_URL=https://<backend-public-host>
INTERNAL_API_TOKEN=<与 Backend 一致的强随机 token>
CALLBACK_TOKEN=<与 Backend/Executor Manager 一致的强随机 token>
EXECUTOR_WORKER_ID=worker-a
EXECUTOR_MANAGER_PUBLIC_URL=http://<worker-a-host>:8001
WORKSPACE_ROOT=${PWD}/tmp_workspace
S3_ENDPOINT=http://<s3-host>:9000
S3_ACCESS_KEY=<access-key>
S3_SECRET_KEY=<secret-key>
S3_BUCKET=poco第二台 worker 只需要改唯一项:
EXECUTOR_WORKER_ID=worker-b
EXECUTOR_MANAGER_PUBLIC_URL=http://<worker-b-host>:8001
WORKSPACE_ROOT=${PWD}/tmp_workspaceWORKSPACE_ROOT 可以每台机器都写成 ${PWD}/tmp_workspace,因为它表示各自 compose 当前目录下的本地目录,不需要跨节点相同。
网络要求
| 访问方向 | 说明 |
|---|---|
| Worker -> Backend | 注册、心跳、claim、start、fail、回调转发 |
| Backend -> Worker | wake、停止、清理、会话历史补偿、浏览器流代理 |
| Worker -> S3 | 工作区文件、manifest、archive 导出 |
| Backend -> S3 | 前端文件列表和下载 URL 生成 |
| Executor -> Worker | 回调、用户输入请求、浏览器截图上传 |
EXECUTOR_MANAGER_PUBLIC_URL 必须是 Backend 能访问到的 worker 地址。它不需要给最终用户直接访问。
前端用户访问浏览器实时预览时走 Backend WebSocket 代理:
因此对外提供系统访问时,不需要为每个 worker 绑定公网域名。只要 Backend 能访问 worker,前端统一连 Backend 即可。
关键环境变量
| 变量 | 配置位置 | 说明 |
|---|---|---|
EXECUTOR_WORKER_ID | Worker | 每个 worker 唯一且稳定 |
EXECUTOR_MANAGER_PUBLIC_URL | Worker | Backend 访问该 worker 的地址 |
BACKEND_URL | Backend / Frontend / Worker | Backend 公开入口;worker 会访问它,浏览器实时预览 WebSocket 也会使用它,Frontend 仅将它作为兼容回退 |
INTERNAL_API_BASE_URL | Frontend | Frontend 服务端直连 Backend 的内部地址,优先于 BACKEND_URL |
INTERNAL_API_TOKEN | Backend / Worker | 内部 API 鉴权 token,必须一致 |
CALLBACK_TOKEN | Worker | Executor 回调到 worker 的鉴权 token |
WORKSPACE_ROOT | Worker | 当前节点本地工作区目录 |
S3_ENDPOINT / S3_BUCKET | Backend / Worker | Backend 和 worker 都必须可访问同一对象存储 |
TASK_CLAIM_LEASE_SECONDS | Worker | 准备阶段 claim 租约,默认 180 秒 |
MAX_CONCURRENT_TASKS | Worker | 单节点并发上限 |
MAX_EXECUTOR_CONTAINERS | Worker | 单节点 executor 容器总量上限 |
LEGACY_UNBOUND_SESSION_WORKER_ID | Backend | 从单机升级多节点时,历史未绑定 session 的原节点 |
Worker 状态管理
worker 启动后会自动注册并定期心跳。后台管理菜单「执行节点」会显示:
status:online、draining、offline、disabledcapacity/active_runs:节点心跳上报的容量和当前活跃 run 数。capacity来自该 worker 的MAX_CONCURRENT_TASKS,active_runs来自 Executor Manager 当前正在调度的 run 数。running_runs:Backend 数据库中该 worker 已领取或运行中的 run 数。后台「运行负载」列展示的是running_runs / capacity,因此分母是MAX_CONCURRENT_TASKS,不是容器数量。bound_sessions:绑定到该 worker 的未删除 session 数- 心跳时间和 stale 状态
推荐操作方式:
- 扩容:新机器配置唯一
EXECUTOR_WORKER_ID后启动 compose,Backend 会自动发现。 - 临时下线:先切到
draining,等待active_runs=0后再停止容器。 - 禁用节点:切到
disabled。该节点不会接新任务,也不会作为已绑定会话的可路由节点。 - 恢复节点:启动原 worker,并保持原
EXECUTOR_WORKER_ID。如果 worker 已 stale,新的 boot id 可以重新注册。
draining 的含义是“不接新 session,但仍可服务已绑定 session”。这对本地工作区架构很重要,因为历史 session 的文件和运行态仍在原 worker 上。
失败与恢复行为
| 场景 | 行为 |
|---|---|
| worker 在 claim 后、start_run 前崩溃 | claim lease 过期后 run 回到 queued;如果这是新 session 的首次 claim,会清理 session 绑定,其他 worker 可重新领取 |
| worker 准备阶段很慢 | worker 持续 renew claim,避免其他 worker 重复领取 |
| worker 运行中失联 | session 已绑定到该 worker;需要恢复原 worker 才能继续本地工作区会话 |
| 用户删除 session 但 worker 不可用 | Backend 仍会软删 session,清理任务跳过并返回 cleanup error |
| 任务已完成且 workspace export ready | 文件列表和下载从 S3 读取,不依赖 worker 在线 |
| worker_id 被两台机器同时使用 | fresh boot id 冲突会被拒绝,避免同一逻辑节点被两个物理节点抢占 |
从单机升级到多节点
- 选定原单机节点 ID,例如
local-worker。 - Backend 配置:
LEGACY_UNBOUND_SESSION_WORKER_ID=local-worker- 原单机 worker 配置:
EXECUTOR_WORKER_ID=local-worker
EXECUTOR_MANAGER_PUBLIC_URL=http://<old-worker-host>:8001- 新增 worker 使用新的 ID,例如
worker-b、worker-c。 - 观察后台「执行节点」,确认各节点
online,且EXECUTOR_MANAGER_PUBLIC_URL可被 Backend 访问。
历史未绑定 session 会优先回到 LEGACY_UNBOUND_SESSION_WORKER_ID,避免旧本地工作区被路由到新机器。新 session 则按节点可用性和负载选择 worker 并写入绑定。
安全边界
生产部署建议:
- worker
8001端口只允许 Backend 所在网段访问。 - 不要把
INTERNAL_API_TOKEN、CALLBACK_TOKEN使用默认值。 EXECUTOR_MANAGER_PUBLIC_URL使用内网 IP、VPN 地址或私有负载网络地址。- 对外只暴露 Frontend 和 Backend。
- S3/RustFS 控制台不要直接公网暴露;浏览器下载使用 Backend 生成的预签名 URL。
Worker 业务接口按用途分两类鉴权:
- Backend -> Worker:
X-Internal-Token - Executor -> Worker:
X-Callback-Token
健康检查接口可以保持裸露给容器自检使用,但业务写入口、工作区管理、浏览器代理、任务控制和回调入口都必须带 token。
容量规划
先按简单规则估算:
总并发容量 = worker 数量 × MAX_CONCURRENT_TASKS再根据单任务资源消耗调低:
EXECUTOR_CPU_LIMIT:限制单个 executor 容器 CPU。EXECUTOR_MEMORY_LIMIT:限制单个 executor 容器内存。MAX_EXECUTOR_CONTAINERS:限制单节点动态容器总量。一个 session 的多轮 run 通常会复用同一个 executor 容器,用于保留工作区、浏览器状态和运行上下文;同时 worker 也可能暂时保留 warm/persistent 容器以降低下一轮启动成本。因此容器总量不等于当前并发 run 数,通常应满足MAX_EXECUTOR_CONTAINERS >= MAX_CONCURRENT_TASKS,并给多轮会话复用留出余量。TASK_CLAIM_LEASE_SECONDS:节点拉镜像或 staging 慢时调大。
简单区分:
MAX_CONCURRENT_TASKS是「同时干活的槽位数」,决定 worker 一次能并发执行多少个 run,也决定后台负载分母。MAX_EXECUTOR_CONTAINERS是「最多保留的容器池大小」,防止 Docker 中 executor 容器无限增长,不参与节点调度容量计算。
如果任务大量依赖浏览器、编译或大仓库,优先增加 worker 数量,而不是单机无限提高并发。
运维检查清单
- 每个 worker 的
EXECUTOR_WORKER_ID唯一。 - Backend 能访问每个
EXECUTOR_MANAGER_PUBLIC_URL。 - Frontend 能访问
INTERNAL_API_BASE_URL,Worker 能访问BACKEND_URL;用户浏览器能访问 Frontend;Worker 还必须能访问 S3。 - Backend 和所有 worker 使用相同
INTERNAL_API_TOKEN。 - Worker 与动态 executor 使用相同
CALLBACK_TOKEN。 WORKSPACE_ROOT对 worker 容器用户可读写。- worker 端口没有直接暴露到公网。
- 下线前先
draining,不要直接停掉承载活跃 session 的 worker。