Agent 平台

一个用户一个容器——这个决定管了后面两年

桌面便携方案无法规模化后,转向多租户云端是必然。核心决策:每用户独享容器。这个选择为隔离但代价是资源线性放大。

桌面便携版 OpenClaw 能服务单个客户,但无法规模化——每户都要人工部署,排障要 SSH 上机,更新要走 OTA。转云端多租户是必然的演进。

我在 2026 年 5 月启动 CPA(Cloud Platform Architecture)项目,把这个转变落地。整个设计围绕一个核心决策展开:每个用户一个独立容器

为什么容器隔离必须是 1:1

OpenClaw 是 AI Agent 运行时。用户在 shell 里跑 openclaw chat,这个进程会:

  • 执行用户上传或配置的代码(skill、workflow)
  • 写文件到本地卷(历史记录、缓存、配置)
  • 装依赖(npm install、pip install)

进程级隔离不够。两个用户的代码在同一进程里跑,一个 crash 就拖累另一个;一个恶意脚本可以访问全局状态、篡改其他用户的卷。这是不可接受的。

虚拟机级隔离太重。每个用户启一个 VM,资源成本会上天。

容器是中间地带。OS-level cgroup + namespace + rootfs,隔离干净、开销可控。所以决定是:用户数 = 容器数。这个选择是架构的起点。

后果:资源线性放大

选定 1:1 容器后,宿主机上每一项资源都随用户增长线性放大:

  • 网络地址:Docker overlay network 需要 virtual IP;每容器占一个。从 10.0.0.0/8 分配,上限约 1600 万 —— 看起来充足,实际 Docker 的实现在几千容器时开始出现地址池竞争。
  • 内存:单容器 1 GiB(Node.js 24 + openclaw + adapter + supervisord),100 用户就是 100 GiB。单机 8 核 16G 撑不了多少人。
  • 文件描述符:容器内 openclaw 进程、adapter 进程各占百把个 fd;同时 100 个容器意味着内核全局 fd 表有万级条目。系统默认上限 65536,很容易触线。
  • 存储挂载:每个用户一个持久卷;容器 prepare 时要挂载卷。超过一定数量后,I/O 竞争明显。
  • 升级耗时:滚动升级 100 个容器,每个要拉镜像、启容器、健康检查,总耗时 = 容器数 × 单个启动时间。100 个容器可能要半小时。

这个选择本身没错。错在没有同步建立"每项资源的容量上限是多少用户"这张表。

五周实施节奏

为了在 5 周内把这个架构从 0 到可演示,我把工作按依赖关系排成五个阶段。

Week 1:基础设施与 sub2api 桥接

基础设施决定了后续每周的开发成败。这周的目标是:

  • 搭 monorepo(pnpm + turbo),分离 apps/apiapps/webinfra/docker
  • Postgres + Redis 本地开发环境
  • 反向代理 Caddy,配置 self-signed TLS 与路由
  • 容器镜像骨架(node:24 + supervisord + adapter placeholder)
  • 关键:真机联调 sub2api 管理员 API

最后一项是风险最高的点。sub2api 的管理员 API 文档不完整,我写了一个早期探针脚本逐个 curl 真机上的端点路径,确认了创建子账号、登录、建 key、查用量这四个核心接口。如果这一周没跑通,整个 Week 2 就会卡壳。实际上这次探针一次成功,没有返工。

Week 2:认证、激活码、容器自动供给

注册流程的核心是三段式(create sub2api user → login as that user → create API key as that user),每段用不同的鉴权方式。Week 1 的探针结果直接映射到 Sub2ApiClient 的四个方法。

这周新增 User + ActivationCode + Instance 三个数据库表,以及认证中间件(基于 session cookie 的用户校验)。最关键的工作是 SignupService,它:

  1. 校验激活码
  2. 调 sub2api 三段式开通子账号 + key
  3. 用 libsodium sealed-box 加密 key 和密码
  4. 事务内写入 User 表 + Quota 表 + 消激活码

容器编排改用 Docker Swarm overlay network(相比 host 网络,overlay 能自动处理 DNS + virtual IP)。Instance 表存 containerId(swarm service name)、host(overlay VIP)。

实现过程中没有太多惊喜,主要是工程量 —— 从密码哈希、session 管理、错误处理到审计日志,都是标准的 SaaS 后端配方。

Week 3:聊天与 Skill 管理

这是最接近业务核心的一周。chatProxy 绕过了还没装进镜像的 openclaw daemon,直接把用户消息转给 sub2api 的 OpenAI 兼容端点 /v1/chat/completions。流式响应逐个 token 推回浏览器(SSE)。

Skill 管理因为 openclaw 本身的 API 还没定型,只能做到 L1 —— 在 DB 存每个用户的 skill enable/disable 状态,用户点按钮立即保存。实际生效要等 Week 5 才行。

这周前端加了 React 聊天页(Vercel AI SDK 处理流式解析)+ Markdown 渲染(remark-gfm 支持表格和任务列表)。

Week 4:高级终端与用量看板

用户在浏览器里打开 xterm 网页终端,直接连到容器内的 ttyd(PTY bridge)。Fastify WebSocket 插件处理双向透传。

关键改动是从 overlay VIP 换成 --publish mode=host。宿主 18000+ 端口一对一映射到容器 8080(adapter)和 7681(ttyd)。这样 cpa-api(跑在 systemd,不在 swarm 里)能用 127.0.0.1:PORT 直接访问容器,不依赖 overlay 网络。

用量看板聚合 sub2api 数据(API 调用次数、token、费用)+ 本地 Message 表的消息分桶。每天一条记录,前端 recharts 折线图展示趋势。

镜像升到 v0.3,真正装了 npm install -g openclaw。supervisord 配 autostart=false,避免 daemon 启动失败拖累整个容器。用户在 ttyd shell 里手动 openclaw chat 按需启动。

Week 5:生产加固

三个合规页面(用户协议、隐私、违法举报)+ 注册强制勾选。Postgres 每日自动备份(pg_dump + 30 天保留)+ Docker volume 每周快照。Uptime Kuma 监控 4 个关键 endpoint。admin 后台一键给用户充值 sub2api 余额(前提是上游支持 admin recharge API;如不支持则通知手动充值)。

Vite manualChunks 拆分 bundle —— 把 markdown、charts、xterm 库分离成独立 chunk,首屏只加载核心 main.js,减少初始 load 时间。

预留的隐性成本

这个设计交付时看起来很完整,但后续运维中,瓶颈会逐个浮现。

用户数从 0 增到几百时,每项资源都还有富余,看不出问题。但到一定规模,某一个资源先耗尽 —— 比如 overlay network 的 fd 表、或者宿主文件系统的 inode、或者滚动升级时的累积启动时间。现象不是"平台容量满了",而是"个别用户偶发异常""某个用户的容器起不来""部分 ws 连接超时"。

排查这类问题很痛苦,因为单看一个用户的日志是完全正常的,必须从全局资源池的角度看。比如文件描述符耗尽时,表现是某个容器内的 adapter 进程打不开 socket 连接,但这个进程本身的 CPU 和内存都在正常范围。

好消息是,这个 1:1 容器的决策本身没有反悔的必要。隔离需求是真实的,成本是值得的。错的只是在架构敲定时,没有同步做"容量规划表"—— 明确每 100 用户会消耗多少网络地址、文件描述符、内存碎片、存储 IOPS,以及对应的监控告警阈值。

这些细节会在后续的故障复盘中逐一显露。


后续演进

这个五周MVP是 Agent 平台因果链的起点。后续 05-22、05-25、06-01、06-03、07-26 的六篇文章,逐段展开宿主资源如何一个接一个触线,以及每次的应对方案。从网络隔离、文件描述符争抢、存储竞争,到最终的多区域容灾,整条线索都是这个 1:1 容器决策的直接后果。

星野的头像

星野 XINGYE

一个人维护 AI 平台的工程师。这里记录 63 篇复盘:18 份故障档案、OTA、架构演进与工作流。