docs/multi-tenant/runtime-resource-audit-2026-07-28.md
日期:2026-07-28 至 2026-07-29
审查分支:
feat/multi-tenants,审查起点 32abbb636f4455e965141d8d209b359dbfbb5aaefeat/multi-tenants,审查起点 0cddf3c2bea5939c67b71e488a719e9903c28d17本轮已覆盖 LangBot Core、Plugin Runtime 和 Box Runtime 的主要常驻对象、后台任务、队列、网络客户端、进程生命周期及数据库连接池。本轮定位到的攻击者可控或历史累积状态均已补充容量、超时、淘汰或确定性清理边界;修正后的高基数探针没有观察到随历史请求继续增长的活跃缓存。按本轮“代码审查 + 本地可重复测试”的验收口径,审查已经完成,未发现仍未处理的严重内存泄漏或 CPU 抢占路径。该结论不等于证明任意生产负载下不存在资源问题。
最终 Cloud 拓扑和生产环境不是本轮完成条件。代码级审查、跨仓全量测试和仓库 Dockerfile 构建的 Linux/cgroup v2 探针已经通过,但当前状态仍不能单独作为 Cloud 生产激活批准。完整的环境侧剩余清单见 Cloud v2 仍待验证事项。其中与本轮资源审查直接相关、上线前还必须完成的项目包括:
--privileged 的 private cgroup namespace 都不满足条件。1d65ed301a6afc52150a998043f73cd6032c8162,本提交集中的 LangBot
pyproject.toml 和 uv.lock 已精确钉住该提交。最终镜像仍需按待验证清单记录并核对实际安装版本。Application.shutdown() 使用 contextlib.suppress 却未导入 contextlib 的问题。原行为会在实际资源关闭分支直接 NameError,阻断后续 Plugin、Box、HTTP 和数据库释放。make_app() 在任一启动 stage 或初始化失败时会关闭尚未返回给 main() 的半构建 Application;Telemetry、Box、Tool、Platform、Vector 和 HTTP manager 在初始化前即挂到 Application,避免初始化中途失败后清理器无法发现已经创建的连接、会话、子进程或后台任务。(instance, workspace, generation) 管理 host task 和 session;任务完成会从注册表移除,代次推进会取消旧 host task 并关闭旧 session,reload/shutdown 会先清空现有运行时,避免 completed task 和旧代连接永久驻留。aclose() 再传播错误。Application.dispose() 只允许一个可追踪 shutdown task;重复的信号、窗口关闭或调用方清理不会铺开多个并行停机流程。TaskCapacityError 已下沉到无 Application/controller 依赖的纯错误模块。原来的 HTTP 过载异常路径会在特定冷启动导入顺序下触发 TaskManager/controller 循环导入,把应返回的 429 变成框架 500。aiohttp.ClientSession 却不关闭的问题;现在 runner 的 aclose() 会确定性关闭底层 API client。lbp publish 现在用上下文管理器关闭插件包上传文件;原实现的成功、API 错误和 HTTP 错误返回路径都会遗留文件句柄。wait() 回收子进程;Windows worker 的原生进程路径也进入相同的 finally 清理契约,避免取消安装、停机或超时后留下孤儿进程。OSError 也会关闭 Runtime 和 reaper。WebSocket 控制模式端口绑定失败会退出并交给编排器重启,不再在没有任何可用 RPC/health 端口时永久等待;stdio 模式仍允许仅 relay 绑定失败后继续控制通道。max_concurrent_restarts 个 supervisor
能持有冷却计时器/状态等待,其他 installation 睡眠在同一个 semaphore FIFO;
probe 状态变更在调用者取消时仍会完成,避免 half-open 永久占用。/proc,并流式删除遗留 session 目录;不再为每个
遗留目录重复扫描全部进程或先把全部目录物化到内存。(instance, workspace, generation) O(1) scope
counter 做准入,不再为每个新 launcher 扫描全实例 buffer。global sandbox 不进入 TTL 索引,managed process 被禁用时进程索引为空;session 创建、周期 reaper、状态和 /healthz 因此不会随全部持久租户数线性扫描。测试把总 session 字典替换为禁止迭代的映射,仍能完成第二个持久 session 创建、reap、status 和 health。ConnectionClosed 类型,不再因库顶层未导出 exceptions 属性而在错误路径二次失败。O(log N) 或仅消费已过期前缀,旧堆项会忽略并按活跃状态的有界倍数压缩。detail_truncated。默认分页 1,000、
export 10,000、detail 2,000,绝对上限分别为 5,000、50,000、10,000。date_trunc、SQLite 使用 strftime 在数据库中聚合,并只返回
最近 1,000 个时间桶(绝对上限 10,000)。模型分组复用分页上限并在 SQL 中按 token
排序、限制;两类结果都返回显式的 *_truncated 标志。limit + 1 字节,S3 body 在成功、
超限和异常分支都会关闭;默认单对象 10 MiB、绝对上限 64 MiB。所有 scoped load
以及 WebSocket attachment 都经过同一边界,写入也不能产生当前实例无法安全读取的对象。repeat_seed('') 空输入无限循环。regex 引擎:最多 64 个 pattern、单 pattern 1,024 字符、输入 1 MiB、单次总匹配 CPU 预算 50 ms,并在线程中执行。超时、非法正则和替换放大均失败关闭;灾难性 (a+)+$ 回归在 1 ms 测试预算内被中断。read/write/edit/glob/grep 文件工具移出事件循环并继承 Workspace 阻塞预算。目录列举、递归 walk、grep 文件/总字符、单行、pattern、结果和 regex CPU 均有硬上限;glob 只用固定大小最小堆保留最新 100 项,不再先把全部命中路径驻留内存。Box 内执行的 glob/grep 脚本同样限制命中集合、扫描量和正则时间。storage.s3.max_concurrency=16,可通过实例配置和环境变量覆写。StreamReader limit 或堵塞子进程。11.270s。resource-maintenance 调度器。调度器先等待首个 interval,不与启动加载争抢资源;
同一到期周期只执行一次 active Workspace discovery,然后按 Workspace 串行运行
有界 job,单 Workspace 失败不跳过其他 Workspace。默认相同的一小时周期由此从
三次全租户发现和三个同时唤醒的任务收敛为一次发现和一个任务。mcp.lifecycle_concurrency=16,支持 MCP__LIFECYCLE_CONCURRENCY 覆写并硬性限制最大 128。初始加载不再先为每个 server 创建一个等待 semaphore 的 task,而是由一个可取消 dispatcher 每批最多物化 lifecycle_concurrency 个子 task;同时去掉了 ORM server/config 的双份临时列表,避免大量租户启动时集中占用 CPU、内存、socket 和文件句柄。asyncio.to_thread() executor 统一改为硬有界线程池。默认最多同时运行 8 个阻塞调用、排队 128 个,达到容量后立即抛出 admission 错误,不再使用 Python 默认 ThreadPoolExecutor 的无界工作队列保留任意数量的请求对象、Future 和闭包。每个可信 Workspace 的 running + queued 默认再限制为 4,并强制配置值不超过 worker 数的一半,避免单个租户先提交一整批同步工作占满全部 worker/FIFO 队列。Core 使用 system.blocking_executor.max_workers/max_pending/max_inflight_per_scope,原生支持对应的 SYSTEM__BLOCKING_EXECUTOR__* 覆写;SDK 进程使用 LANGBOT_BLOCKING_EXECUTOR_MAX_*,并分别限制全局最大值为 64/4096。/healthz 返回同一份无凭据、无租户标识的聚合 JSON,Box /readyz 同时附带资源快照。24 小时门禁默认拒绝缺失或停止的 monitor、超过 1 秒的 recent max、超过 250 ms 的尾段 recent p95 及 sample counter 回退。RequestContext、公开 bot 的 RuntimeBot、公开对象 key 中经 binding fence 验证的 Workspace、Platform/TaskManager 的 ExecutionContext,以及 SDK 入站 ActionContext 建立,不接受调用方伪造的租户 header。公开 webhook、公开对象下载、Dashboard/Embed WebSocket、普通 HTTP handler、Platform adapter 和 detached tenant task 均已覆盖。容量拒绝在 Core HTTP 路径返回稳定的 429,health/debug counter 分开报告 global 与 scope rejection。system:authentication 阻塞作用域。Cloud 本身仍禁用本地密码登录。count/find/sort 改成线性扫描,
PIL image 使用显式关闭,压缩步长为零时也能终止。max_workers、max_total_cpus / max_cpus 和 max_total_memory_mb / max_memory_mb 的最小值约束。plugin.worker.require_hard_limits=true;cgroup v2 delegation 不可用时拒绝启动。memory.max 和 memory.swap.max=0。修复前,48 MiB 沙盒可以把强制提交的 128 MiB 页面换出并正常退出,形成宿主 swap 抢占;修复后同一探针以 exit 137 被 cgroup 杀死。/healthz 改为 /readyz,使 backend 或 managed-mode 隔离检查失败时不会把 Pod 加入就绪流量。requirements.txt 和插件
manifest.yaml 都使用 limit + 1 有界读取,manifest 额外限制为 1 MiB。global session、零 managed process。database.postgresql 新增并校验 pool_size、max_overflow、pool_timeout_seconds、pool_recycle_seconds;默认最大连接数为 10 + 10。pool_size + max_overflow 的绝对上限为 100,timeout/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 和告警核对。monitoring.query_limits 配置并支持原生环境变量覆写,但始终
受代码绝对上限约束;cleanup 的每表批次数和 Storage 每轮文件数同样采用实例配置加
绝对上限。时间序列默认/绝对上限为 1,000/10,000 个数据库聚合桶,模型分组复用分页
上限。提高这些值必须计入 V-08/V-09 的数据库 CPU 与 Core RSS 容量曲线。| 验证项 | 结果 |
|---|---|
LangBot Ruff + git diff --check | 通过 |
Plugin SDK Ruff + git diff --check | 通过 |
| LangBot 全量测试(含 unit/integration/Box/E2E) | 2855 passed, 33 skipped |
| Plugin SDK 全量测试 | 1328 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 门禁解析/采样/判定单元测试 | 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 通过,12.559s |
| Core 5,000 个 populated Workspace 三代容量探针(使用当前本地 SDK 分支) | 当前复跑通过,最大替换耗时比 1.405 |
| Plugin SDK 双阶段资源探针 | audit 通过,11.270s |
两个仓库新增了可重复执行的历史 churn 探针,Core 另有 populated Workspace 三代替换探针:
# LangBot Core
PYTHONPATH=../langbot-plugin-sdk/src uv run python scripts/runtime_resource_probe.py --scale audit --json
# LangBot Core:5,000 个带代表性资源的 Workspace
PYTHONPATH=../langbot-plugin-sdk/src uv run python scripts/workspace_runtime_capacity_probe.py --scale audit --json
# LangBot Core:真实 PostgreSQL 16 + RLS populated Workspace 启动
TEST_POSTGRES_URL=postgresql+asyncpg://... \
LANGBOT_PG_CAPACITY_WORKSPACES=1000 \
uv run pytest \
tests/integration/persistence/test_migrations_postgres.py::TestPostgreSQLTenantRuntime::test_populated_cloud_startup_is_linear_and_task_bounded \
-q -W error::ResourceWarning --log-cli-level=INFO
# langbot-plugin-sdk
uv run python scripts/runtime_resource_probe.py --scale audit --json
Core audit 每个阶段执行 10,000 个空 Workspace 的真实 Model/Plugin manager 加载与 reconcile、25,000 次 Query、2,500 次 session churn、10,000 个限流身份、5,000 个 task 和 2,500 次 WebSocket churn。第一、第二阶段的保留状态完全一致:
0。0,历史 scope counter 100。200。10,000。200。200。1 / 1 / 6;使用当前本地 SDK 分支的复跑中,第二阶段相对第一阶段 RSS 增长 2,228,224 bytes、tracemalloc current 增长 344,669 bytes,总耗时 12.559s。Session 淘汰改为 Workspace 索引和最小堆后,同一 audit 工作量相对此前 16.150s 明显下降。Populated Workspace audit 为 5,000 个 Workspace 各加载一个 Provider、LLM、Embedding、Rerank、Pipeline、Bot、KnowledgeBase 和 MCP session,然后全部推进两个 generation:
5,000,不存在按历史 generation 增长。10,000 个全部收到确定性关闭;weak reference 断言旧代对象可被回收。1 / 1 / 6;使用远端精确钉住 SDK 的当前复跑中,第三阶段相对第二阶段 RSS 增长 1,245,184 bytes,tracemalloc current 仅增长 2,061 bytes。1.893s / 2.549s / 2.659s,最大替换耗时比为 1.405,未随历史代次出现 CPU 退化。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 创建/删除。第一、第二阶段的保留状态完全一致:
0。5,000;Workspace generation record 为有界的 10,000,没有等待者时 generation event 为 0。0。1 / 7;当前复跑第二阶段相对第一阶段 RSS peak 增长 2,637,824 bytes、tracemalloc current 增长 289,746 bytes,总耗时 11.270s。耗时增加来自本轮把大协议消息的 JSON/Pydantic、UTF-8 编码、分片和拼接移入有界线程池;结构状态和第二阶段 tracemalloc 增量保持平稳。第二轮反向静态审查另外枚举了 Core 的 50 个显式 task 创建点和 204 个线程、阻塞调用及子进程调用点,以及 SDK 的 28 个显式 task 创建点和 62 个线程、阻塞调用及子进程调用点。第三轮独立复核继续从高基数定时器、目录遍历、准入全表扫描和取消竞态反推,新增关闭了 Plugin restart 冷却唤醒群、MCP idle 数据库轮询、nsjail orphan 的 O(session × process) 启动扫描、message aggregation 的 O(buffer) 准入及 Skill inode/文本列表边界。显式 task 均具有持有者、完成回调或 finally 回收路径;所有生产入口在第一次 asyncio.to_thread() 前安装有界默认 executor。Core、Plugin Runtime 和 Box 的公开 /healthz(Box /readyz 亦同)会输出各自的 aggregate runtime/resource counter 和 event-loop lag,供 soak 对比活跃量、pending、累计 capacity rejection 与调度延迟;不输出 debug key、控制 token、租户或插件身份。Plugin Runtime 的授权 debug info 复用同一资源快照,避免公开/私有指标语义漂移。
真实 PostgreSQL populated 启动门禁会先通过 release migration 创建最新 schema,再用无 BYPASSRLS 的临时 Cloud Runtime 角色启动。每个 Workspace 都含九类代表性资源,测试会走实际的 instance discovery、tenant UoW、启动 binding 快照和 Model/Platform/Pipeline/RAG/MCP/Plugin 加载路径:
6.178s,进程 CPU 3.026s。12.109s,进程 CPU 5.967s;相对此前 500 Workspace 的墙钟比为 1.960。model_providers、llm_models、embedding_models、rerank_models、bots、legacy_pipelines、knowledge_bases、mcp_servers、plugin_settings 九张表的 SELECT 次数均精确等于 Workspace 数,没有重复的全租户发现或超线性资源扫描。ResourceWarning 模式通过。探针要求第二阶段的结构状态与第一阶段精确相等,并对第二阶段 RSS 与 tracemalloc 增长设置失败阈值。macOS 的 RSS 来源是 getrusage peak,因此这里验证的是峰值增量边界而非“当前 RSS 回落”;最终 Linux 24 小时 soak 仍需采集 current RSS/PSS 和 cgroup memory.current。
LangBot 全量测试的 33 个 skip 中,22 个是默认全量运行未提供 PostgreSQL/pgvector 而跳过的集成用例,10 个是未提供 Valkey,另 1 个是可选环境的 collection skip;真实 PostgreSQL 相关路径已由上表单独运行覆盖。Plugin SDK 的 26 个 warning 为现有 Pydantic v2 deprecation 与 aiohttp AppKey 建议;没有失败、未关闭资源或资源上限降级。Core 当前全量产生 194 个既有第三方/兼容性 warning;ResourceWarning 和 PytestUnraisableExceptionWarning 仍由 pytest 配置提升为错误,本轮没有此类泄漏告警。
Linux Runtime 探针使用上述镜像并只读挂载本地最新 SDK 源码:
false,严格 readiness 按预期失败关闭。--privileged + private cgroup namespace:namespace、mount、network 通过,但 cgroup v2 delegation 为 false,仍按预期不能进入 Cloud ready。true,nsjail namespace、mount、network 和 cgroup v2 均通过;硬文件系统与 inode quota 继续报告 false。cpus=0.1 的 1.0 秒 process-CPU busy loop 实际耗时 9.13s;memory_mb=48 下逐页提交 128 MiB 以 exit 137 终止;pids_limit=8 下批量 fork 返回 EAGAIN。这些结果验证了 CPU、memory+swap 和 PID 的实际内核执行路径。scripts/cloud_runtime_soak.py 后,在同一 Linux 镜像的独立容器中设置 --cpus 0.5 --memory 256m --memory-swap 256m --pids-limit 128,工具从目标 cgroup 读回 quota 50000/100000 usec、memory 268435456 bytes、swap 0 bytes 和 PID 128。最终复跑中,32 MiB 子负载退出后的 4 秒冷却尾段 memory.current 稳健增长和斜率均为 0,平均 CPU 0.00132 cores,OOM、memory pressure、PID max 和 throttle delta 均为 0,最终 verdict 为 pass。这只是采集器/判定器自检,不替代最终 24 小时生产候选运行。/healthz 均返回相同聚合 JSON,event-loop monitor 为 running,且正文不含 debug key。采集器显式绕过进程级 HTTP proxy 后,对控制端口执行 6 秒短时 endpoint gate:无失败,观测到的 recent max/p95 均为 2.233 ms,verdict 为 pass。/healthz 与 /readyz 均返回 event-loop、blocking executor、session/process/task 聚合快照;monitor 为 running、样本持续增长,两个端点观测到的 recent max 均为 2.265 ms。SIGINT 后 aiohttp、Runtime、reaper 与 monitor 走统一清理路径并正常退出。最终 24 小时命令、运行位置、阈值语义、负载矩阵和产物要求见 LangBot Cloud 24 小时资源 Soak 门禁。该工具默认把任一健康失败、OOM/memory pressure、PID limit、CPU throttling 超阈值、blocking executor rejection、冷却尾段内存持续增长或空闲 CPU 过高判为失败;生产运行必须使用 --require-hard-limits。
至少需要监控并告警:
global_rejected_total 和 scope_rejected_total;pending 持续不归零或 rejection 增长都应告警。生产 soak 应覆盖租户突发登录、批量插件 reconcile、插件崩溃重启、WebSocket 断连、Box 并发执行、PG pool 饱和和应用 SIGTERM;持续运行至少 24 小时,并验证负载停止后 RSS、task、socket、文件和子进程数量回到稳定基线。