fix(cloud): enforce instance capacity ceilings

This commit is contained in:
Junyan Qin
2026-07-29 13:45:58 +08:00
parent e52d6880f5
commit c89e6f3bd2
25 changed files with 1062 additions and 87 deletions
@@ -81,6 +81,9 @@ uv run python scripts/cloud_runtime_soak.py \
- 健康接口缺少 event-loop monitor、monitor 未持续运行,或其 sample counter 回退。
- blocking executor rejection counter 增长。
- Plugin Runtime restart circuit 的累计打开次数增长。
- Core 目录 active Workspace、最近 snapshot/delta Workspace 或 membership 基数
超过各自配置上限,或 PostgreSQL `checked_out` 超过配置 pool 容量;相关 current/max
指标只出现一半或 max 非法也失败。
负载结束后的冷却尾段还必须满足:
@@ -16,8 +16,9 @@
## 1. 当前已形成的交付基线
- LangBot Core 全量 `2811 passed, 33 skipped`Plugin SDK 全量
`1317 passed`;两仓 Ruff、`git diff --check` 已通过。
- LangBot Core 全量 `2833 passed, 33 skipped`Plugin SDK 全量
`1325 passed`,闭源适配器 `40 passed`,Space Go 全量测试通过;三仓格式、
静态检查和 `git diff --check` 已通过。
- Plugin Runtime 和 Box Runtime 的公开健康接口、event-loop lag 与有界
blocking executor 指标已经过真实进程短时验证。
- 仓库 Dockerfile 构建的 Linux/cgroup v2 短时探针已证明 CPU、memory、
@@ -25,12 +26,29 @@
- PostgreSQL 16 + RLS 的 1,000 Workspace 真实启动测试,以及 5,000
Workspace 三代替换合成探针已通过。
- Core 已精确钉住 Plugin SDK 提交
`c7893f8f94e83cb09ebcc98e25490fa699b0fb69`。最终验证必须使用包含该提交的
`7c0b9827ed8597a1c84151b83fcf6307934fd944`。最终验证必须使用包含该提交的
Core、Plugin Runtime 和 Box Runtime 镜像,不能混用旧 SDK。
- 独立资源复核已经移除 Cloud MCP 每会话 5 秒查询执行绑定的轮询,改由签名目录
投影提交后向一个合并回收任务发布代次变化;工具与资源调用前后仍使用数据库
execution fence。Plugin restart 冷却等待者、MCP 投影回收、消息聚合 buffer/scope
均已纳入健康快照和 soak 归零门禁。
- 单实例目录现在有一致的操作容量契约:Space 在注册事务内通过 PostgreSQL
advisory lock 串行执行 active Workspace check-and-createSpace 全量快照只返回
active Workspace,并在查询阶段限制 Workspace/membership 数量;闭源适配器限制
解压后的 HTTP 响应字节和签名目录基数;Core 在持有目录投影行锁的事务内再次
COUNT active Workspace,超限时整批回滚且不推进 cursor。任何一层都不截断权威数据。
- Core Cloud PostgreSQL pool 的 `pool_size + max_overflow` 有绝对上限 100
Cloud runtime 连接默认强制 60 秒 statement/idle-transaction timeout 和 5 秒
lock timeoutpool 使用量、超时累计数与目录 active/max 基数进入 `/healthz`
Box Runtime 的 session、process、admission record、RPC 文件和 completed retention
配置也有不可被实例配置放大的绝对上限。
- Space 的 concurrent registration 容量准入已在一次性 PostgreSQL 16 上真实执行:
两个 Account 同时争用最后一个 Workspace 槽位时,精确一个事务成功、一个事务
得到 capacity error,最终 active Workspace 数为 1。active-only snapshot 与
archived delta tombstone 的同一真实 PostgreSQL 集成流程也通过。
- Core Cloud manager 已连接一次性 PostgreSQL 16,并从 `pg_settings` 读回
`statement_timeout=60000ms``lock_timeout=5000ms`
`idle_in_transaction_session_timeout=60000ms`;测试结束后引擎已显式 dispose。
以上结果是进入生产候选验证的前提,不是 SaaS 上线批准。
@@ -107,6 +125,12 @@
- Core、Plugin Runtime、Box Runtime 的不可变镜像 digest
- LangBot 与 SDK commit
- `data/config.yaml` 的非敏感摘要和所有环境变量覆写;
- Space 的 `CLOUD_V2_MAX_DIRECTORY_WORKSPACES` 必须与 Core
`cloud.directory.max_active_workspaces`
`cloud.directory.max_snapshot_workspaces` 一致;Space 的 membership 上限必须与
Core `cloud.directory.max_snapshot_memberships` 一致;
- Core 的 PostgreSQL pool、statement/lock/idle-transaction timeout,以及 Plugin
Runtime/Box Runtime 的全部实例级资源上限;
- PostgreSQL migration revision
- Cloud Adapter、Space control plane 和 workload 的版本。
@@ -176,6 +200,10 @@ PostgreSQL/pgvector 和代表性 Workspace 配置分布,测量:
- 单实例可批准的 Workspace、活跃 Bot、plugin worker 和 sandbox 上限。
容量上限必须写入生产配置与告警,不能只保留在测试报告中。
代码中的默认值和绝对上限只是失控配置的最后防线,不等于生产容量结论。V-08 必须
根据最终镜像的真实曲线把 Space 与 Core 的匹配上限调到已验证容量以内;如果最终
批准值高于当前默认 1,000 active Workspace,必须重新执行目录启动、故障恢复和
24 小时门禁。
### V-0924 小时资源 soak
@@ -193,8 +221,12 @@ PostgreSQL/pgvector 和代表性 Workspace 配置分布,测量:
最后至少保留 30 分钟无测试流量冷却。任一健康失败、OOM/memory pressure、
PID limit、blocking executor rejection、超阈值 CPU throttling/event-loop lag、
目录 `active_workspaces > max_active_workspaces`、数据库 pool 使用量超过配置容量、
冷却尾段内存持续增长,或 Plugin restart `gate_waiters`、MCP 投影回收、
消息聚合 buffer/scope 等临时 gauge 不回落都判为失败。
标准 soak 工具已自动比较目录 active/最近批次与各自配置上限,并比较 PostgreSQL
`checked_out` 与配置 pool 容量;名为 `core` 的标准 endpoint 缺少任一容量指标、
current/max 只出现一半、数值非法或任一样本越界都会直接失败。
必须归档:
@@ -270,3 +270,36 @@ This log records implementation choices made while delivering the Workspace arch
- Decision: In multi-Workspace Cloud mode, Space OAuth may refresh tokens only for an active `cloud_projection` Account whose Space subject UUID and normalized email exactly match. It cannot create an Account, relink by email, mutate directory identity, or choose a Workspace.
- Redirect boundary: When Cloud v2 configures its Core public URL, Space issues authorization codes only to that exact callback origin or the explicitly retained legacy managed-Pod domain. Community installations remain dynamic OAuth clients because they cannot pre-register with the public Space service: the consent screen displays their hostname, and only HTTPS or loopback HTTP with the fixed callback path is accepted. Remote HTTP, userinfo, fragments, arbitrary callback paths, and unrecognized query parameters always fail closed.
- Reason: Space is the SaaS identity and directory authority, but Core remains the authentication enforcement point. Projected-only matching avoids split-brain identities; redirect allowlisting prevents bearer authorization-code exfiltration.
## 2026-07-29
### Directory capacity is one instance-level admission contract
- Decision: Space and Core share an explicitly configured operational ceiling for active
Workspace and snapshot membership cardinality. Space serializes new personal-Workspace
creation across replicas with a PostgreSQL transaction advisory lock and rejects the
registration transaction before the limit is crossed. Core independently counts the
projected active Workspace set while holding the per-instance directory projection row
lock. Exceeding any limit rolls back the complete projection and does not advance its
cursor; neither side truncates authoritative data.
- Snapshot boundary: A full snapshot is current desired state and contains active
Workspaces only. Archived Workspace revisions are retained as bounded, targeted deltas
so Core can validate and apply monotonic tombstones without every bootstrap carrying
unbounded history.
- Memory boundary: Space bounds database result cardinality before signing. The closed
adapter bounds decompressed response bytes before JSON/JWS parsing and validates
Workspace/membership cardinality before entitlement-cache fan-out. Core schema models
have absolute list ceilings, validate duplicates in one pass, and bulk-read Accounts in
bounded chunks rather than issuing two serial queries per Account. Manifest and
entitlement responses have smaller endpoint-specific byte ceilings; entitlement refresh
validates and releases batches of at most 16 raw responses instead of retaining the
entire directory fan-out.
- Operations boundary: Core health exposes aggregate active/max directory cardinality and
PostgreSQL pool occupancy/timeouts. The default active Workspace ceiling is 1,000, with
a hard code ceiling of 5,000; this is a safety stop, not a production capacity claim.
Space and Core configuration must match, and the approved production value comes from
the real V-08 capacity curve plus the V-09 24-hour soak.
- Reason: One LangBot instance should admit new tenants at the cheapest data-only boundary,
while still preventing legitimate registration growth or a malformed control-plane
response from causing an unbounded startup allocation, database connection storm, or
CPU spike.
@@ -17,7 +17,7 @@
1. 在最终 Cloud 部署权限和 cgroup 拓扑下重复 nsjail、namespace 和 delegated cgroup v2 的 CPU、内存、swap、PID、文件句柄验证。本轮一次性 Linux 容器已经证明代码路径可工作,但普通容器和仅 `--privileged` 的 private cgroup namespace 都不满足条件。
2. 为 Cloud Box 提供并验证硬文件系统 quota provider。普通 nsjail bind mount 不能证明总字节数和 inode 硬配额,当前严格 readiness 按设计会失败关闭。
3. 使用最终生产配置分布继续做容量测试,并据此确定单实例 Workspace placement 上限。本轮真实 PostgreSQL 16 + RLS 启动测试已经覆盖 1,000 个各带 Provider、三类 Model、Bot、Pipeline、KnowledgeBase、MCP 和 Plugin setting 的 Workspace,启动加载耗时和 SQL 次数保持线性;5,000 Workspace 的合成三代替换探针也证明旧运行时会释放。仓库已新增可同时采集 Core/Plugin/Box HTTP、进程树和 cgroup v2 的 24 小时门禁工具,并在受 CPU、memory、swap、PID 硬限制的 Linux 容器中完成短时自检;但最终生产候选拓扑的 24 小时运行仍未执行。测试中的 fake adapter/requester/Plugin handler 仍不能替代真实平台 SDK、外部连接池和插件进程的容量数据;合法活跃租户本身仍会线性占用内存。
SDK 已先行发布到分支提交 `c7893f8f94e83cb09ebcc98e25490fa699b0fb69`,本提交集中的 LangBot
SDK 已先行发布到分支提交 `7c0b9827ed8597a1c84151b83fcf6307934fd944`,本提交集中的 LangBot
`pyproject.toml``uv.lock` 已精确钉住该提交。最终镜像仍需按待验证清单记录并核对实际安装版本。
## 覆盖范围
@@ -99,6 +99,17 @@ SDK 已先行发布到分支提交 `c7893f8f94e83cb09ebcc98e25490fa699b0fb69`
- Dify 待提交表单、用户 Space OAuth state 和 Cloud launch JTI replay cache 改为带 revision 校验的最小过期堆;Space credits 使用按时间有序的 LRU/TTL 队列。原实现会在每次攻击者可触发的请求上扫描整个历史缓存并在满容量时再次线性寻找最旧项;现在过期回收为摊销 `O(log N)` 或仅消费已过期前缀,旧堆项会忽略并按活跃状态的有界倍数压缩。
- Cloud launch JTI cache 达到 4,096 个仍有效 token 时失败关闭,不再为了接纳新 token 淘汰仍有效的 replay 记录;否则攻击者可以在容量满后重放被提前遗忘的合法签名。
- Entitlement resolver 现在跟随 Cloud directory 的权威 Workspace 活跃集合。全量目录投影会丢弃已 fenced/removed Workspace 的历史 entitlement snapshotdelta 批量更新不会对每个变化重复扫描;provider 请求进行中发生目录撤销时,返回前的第二次 active fence 会阻止旧结果重新写回缓存。
- Cloud directory 的签名响应、Workspace、membership 和实例 active Workspace
均新增可配置操作上限与绝对上限。闭源适配器按流读取响应并在 JSON/JWS 解析前
拒绝超过 32 MiB 默认值的解压后正文;Manifest/entitlement/event endpoint 再
分别限制为 256 KiB、256 KiB 和 2 MiB。entitlement 刷新最多并发并驻留 16 个
原始响应,逐批验证成小型 snapshot 后释放;Core 在目录投影行锁保护的事务内检查最终
active 数量,超限时回滚 Workspace、Account、membership、inbox 和 cursor,不会
截断权威数据或让并发副本各自越过最后一个容量槽。
- Space 全量目录只投影 active Workspace,历史 archived Workspace 只在最多 100 个
目标的签名 delta 中作为 tombstone 返回。注册创建新个人 Workspace 前通过
PostgreSQL transaction advisory lock 串行执行全局 active 数量准入;达到上限
返回 503,避免多个 Space 副本同时观察到最后一个空位。
### CPU 和事件循环保护
@@ -141,12 +152,23 @@ SDK 已先行发布到分支提交 `c7893f8f94e83cb09ebcc98e25490fa699b0fb69`
- 仓库 Docker Compose/Kubernetes 示例显式下发 Core、Plugin Runtime 和 Box Runtime 的 blocking executor 上限;Kubernetes Box readiness probe 从仅报告进程存活的 `/healthz` 改为 `/readyz`,使 backend 或 managed-mode 隔离检查失败时不会把 Pod 加入就绪流量。
- 相同 digest 的已验证代码和依赖环境可只读共享,每个 installation 的 home/tmp/data 和进程独立。
- Box session、managed process、completed process、admission record 和 RPC 文件均有实例级上限;Cloud entitlement 仍限制每个合资格 Workspace 一个 `global` session、零 managed process。
- Box Runtime 对上述实例级配置再增加不可放大的硬上限:session 5,000、managed
process 1,024、completed process 10,000、admission record 250,000、RPC 单文件
100 MiB、completed retention 86,400 秒。初始化与远程 INIT 对错误类型、负数和
超上限均失败关闭,错误动态更新不会留下部分生效的 limit。
### PostgreSQL
- Cloud 强制 PostgreSQL 业务库、共享 pgvector 和允许的固定向量维度。
- pgvector Cloud 模式复用业务数据库的同一个 AsyncEngine,不创建第二个连接池。
- `database.postgresql` 新增并校验 `pool_size``max_overflow``pool_timeout_seconds``pool_recycle_seconds`;默认最大连接数为 `10 + 10`
- `pool_size + max_overflow` 的绝对上限为 100timeout/recycle 也有绝对上限;
Cloud runtime 的 asyncpg 连接默认设置 60 秒 statement timeout、5 秒 lock
timeout 和 60 秒 idle-in-transaction timeout,并分别限制最大 300/60/300 秒。
一次性 release migration 不继承这些短 runtime timeout。
- `/healthz` 输出 pool 配置容量、checked-in/out、overflow、pool admission timeout
累计数和 SQL timeout 配置;目录同时输出 active/max 与最近批次
Workspace/membership 数,供生产 soak 和告警核对。
- Application shutdown 显式 dispose 业务引擎;standalone pgvector 仅关闭自己拥有的引擎。
- PersistenceManager 提供统一异步 shutdown;Cloud 常驻进程的启动失败、正常停机和一次性 release migration 的成功/异常路径都会释放数据库引擎。真实 PG catalog 测试还覆盖了“入口已经关闭后测试再次复用 manager 会重开 pool”的第二生命周期,严格资源告警模式下无 asyncpg socket/transport 遗留。
@@ -155,6 +177,10 @@ SDK 已先行发布到分支提交 `c7893f8f94e83cb09ebcc98e25490fa699b0fb69`
- 优先 fail closed 或淘汰最老的 idle cache,不允许攻击者控制的历史 key 无限驻留。
- 插件依赖准备选择实例级串行化,以稳定 CPU/磁盘峰值;代价是批量安装耗时增加。
- PostgreSQL 使用一个显式有界共享连接池;未拆分 pgvector pool。
- 单实例目录的默认 active/full-snapshot Workspace 上限均为 1,000membership
上限为 20,000,签名响应上限为 32 MiB;绝对上限分别为 5,000、100,000 和
64 MiB。Space 和 Core 必须配置为同一操作上限,生产值只能根据 V-08 容量曲线
向下调整或在重跑全部门禁后提高。
- 第三方 runner 采用 1 MiB 单结果、16 MiB 单流总量和 100,000 个同步/异步事件的统一实例级安全上限;超限请求失败关闭。
- S3 默认允许 16 个并发阻塞调用,最大配置值 128;在没有独立 worker service 的前提下限制线程池排队和上游连接压力。
- MCP 生命周期默认并发 16、最大 128;该限制统一约束实例启动时的 session host 峰值和 shutdown 批次,不允许租户配置单独放大。
@@ -171,19 +197,22 @@ SDK 已先行发布到分支提交 `c7893f8f94e83cb09ebcc98e25490fa699b0fb69`
| --- | --- |
| LangBot Ruff + `git diff --check` | 通过 |
| Plugin SDK Ruff + `git diff --check` | 通过 |
| LangBot 全量测试(使用远端精确钉住的新 SDK,含 unit/integration/Box/E2E | `2811 passed, 33 skipped` |
| Plugin SDK 全量测试 | `1317 passed` |
| LangBot 全量测试(使用远端精确钉住的新 SDK,含 unit/integration/Box/E2E | `2833 passed, 33 skipped` |
| Plugin SDK 全量测试 | `1325 passed` |
| Space Go 全量测试与闭源 Cloud Adapter 测试 | Go `go test ./...` 通过;Adapter `40 passed` |
| Space PostgreSQL 16 Cloud v2 目录与并发容量准入 | 通过;两个注册并发争用最后一个槽位时 `1 success / 1 capacity rejection / 1 active Workspace` |
| Core PostgreSQL 16 Cloud runtime server timeout | 真实连接从 `pg_settings` 读回 `60000ms / 5000ms / 60000ms` 的 statement/lock/idle-transaction timeout,并显式 dispose |
| 真实 PostgreSQL 16 + pgvector 迁移/RLS/发布测试(严格资源告警) | `22 passed` |
| 真实 PostgreSQL 16 + RLS populated Cloud 启动容量 | 500 Workspace `6.178s / CPU 3.026s`;当前 1,000 Workspace 复跑 `12.109s / CPU 5.967s` |
| 较早 Core Dockerfile Linux 镜像构建与 `regex` 导入 | 通过,image SHA `8893a14053df`;该镜像使用旧 SDK pin,已失效,最终候选必须重建 |
| `ResourceWarning` + `PytestUnraisableExceptionWarning` 全量门禁 | Core 与 SDK 均通过,并已固化到 pytest 配置 |
| Plugin SDK Box 专项测试(含全局扫描回归保护) | `669 passed` |
| Docker Compose 渲染、Compose/Kubernetes YAML 解析与 diff 检查 | 通过 |
| Cloud soak 门禁解析/采样/判定单元测试 | `25 passed` |
| Cloud soak 门禁解析/采样/判定单元测试 | `27 passed` |
| Core/Plugin SDK event-loop monitor 专项测试 | 两仓各 `7 passed`,包含真实 50 ms scheduler stall |
| Cloud soak Linux 硬限制短时自检 | 通过;CPU `0.5`、memory+swap `256 MiB`、PID `128` 均从 cgroup v2 读回,冷却尾段 verdict `pass` |
| Core 双阶段历史 churn 资源探针(使用精确钉住的新 SDK) | audit 通过,`11.895s` |
| Core 5,000 个 populated Workspace 三代容量探针(使用精确钉住的新 SDK) | audit 通过,最大替换耗时比 `1.435` |
| Core 5,000 个 populated Workspace 三代容量探针(使用精确钉住的新 SDK) | 当前复跑通过,最大替换耗时比 `1.382` |
| Plugin SDK 双阶段资源探针 | audit 通过,`9.407s` |
两个仓库新增了可重复执行的历史 churn 探针,Core 另有 populated Workspace 三代替换探针:
@@ -220,9 +249,9 @@ Populated Workspace audit 为 5,000 个 Workspace 各加载一个 Provider、LLM
- 三个阶段的活跃 provider/model、pipeline、bot、knowledge 和 MCP registry 均精确维持 `5,000`,不存在按历史 generation 增长。
- 到第三阶段,前两代的 requester、Bot adapter 和 MCP session 各 `10,000` 个全部收到确定性关闭;weak reference 断言旧代对象可被回收。
- event-loop task、线程和文件描述符保持 `1 / 1 / 6`;使用远端精确钉住 SDK 的当前复跑中,第三阶段相对第二阶段 RSS 增长 `1,261,568 bytes`tracemalloc current 仅增长 `510 bytes`
- 初始/第一次替换/第二次替换分别耗时 `1.804s / 2.420s / 2.588s`,最大替换耗时比为 `1.435`,未随历史代次出现 CPU 退化。
- macOS RSS sample 从初始的 `154,484,736` 增至第一阶段 `368,050,176`、第二阶段 `388,939,776` 和第三阶段 `390,201,344 bytes`;第二次替换只比第一次替换增加约 1.20 MiB,但“合法活跃租户资源的线性容量”仍必须作为 placement 容量输入。这里使用轻量 fake adapter/requester,不应把第一阶段约 204 MiB 增量外推为生产每租户成本。
- event-loop task、线程和文件描述符保持 `1 / 1 / 6`;使用远端精确钉住 SDK 的当前复跑中,第三阶段相对第二阶段 RSS 增长 `1,245,184 bytes`tracemalloc current 仅增长 `2,061 bytes`
- 初始/第一次替换/第二次替换分别耗时 `1.660s / 2.151s / 2.296s`,最大替换耗时比为 `1.382`,未随历史代次出现 CPU 退化。
- macOS RSS sample 从初始的 `154,648,576` 增至第一阶段 `368,181,248`、第二阶段 `389,087,232` 和第三阶段 `390,332,416 bytes`;第二次替换只比第一次替换增加约 1.19 MiB,但“合法活跃租户资源的线性容量”仍必须作为 placement 容量输入。这里使用轻量 fake adapter/requester,不应把第一阶段约 204 MiB 增量外推为生产每租户成本。
Plugin SDK audit 每个阶段执行 25,000 次 loopback RPC、5,000 次安装 binding 激活/撤销、10,000 个 Workspace generation 更新和 2,500 次带 Workspace 上下文的 Box session 创建/删除。第一、第二阶段的保留状态完全一致: