性能、负载与内存
🔵 Agent OS 在负载下的表现:能承受多少并发请求、如何限制过载、每个会话占用多少内存,以及如何复现这些数据。另外还会在同一测试台上与其他 agent 服务器进行对比。
文中的数字来自一个特定的测试台(见 §34.2),仅作为规划时的参考,而非您硬件上的保证。
34.1 什么限制了负载
Agent OS 不是负载均衡器,也不自己做推理:LLM 是外部提供商,而内核只是规划对它的调用。因此「天花板」由三件事决定:内核限制、过载时的行为,以及持久化的成本。
| 杠杆 | 位置 | 默认值 | 作用 |
|---|---|---|---|
kernel.concurrency_limit |
server.yaml |
3 |
到提供商的并发 LLM 流上限(内核信号量)。多余的轮次进入队列。 |
kernel.max_in_flight_turns |
server.yaml |
0(关闭) |
HTTP 边缘的准入闸门:不超过 N 个「在途」轮次;超出则立即 429 Too Many Requests + Retry-After。被拒绝的请求不会触碰会话,也不会取消已启动的轮次。 |
kernel.session_ttl_secs |
server.yaml |
300 |
无活动的会话在 TTL 后被杀死;清理器每 session_cleanup_interval_secs 运行一次(60)。限制长时间空闲下的内存增长。 |
一个会话——一个活动轮次。 如果同一会话收到第二个请求,它会取消前一个(last-wins),而不是排队。这可以防止同一会话内的竞态。
持久化。 存储会写入轮次数据:JSONL 日志器、EventDb(events.db)、消息存储(messages.db)和访问日志(access.db)。它们全部被放到带批处理的后台写入线程中(每批一个事务),因此不会阻塞 async 路径;但在非常高的负载下,它们的线程会开始相互竞争(见 §34.3)。JSONL 是可选的层;DuckDB 数据库(事件/转录/访问)承载主要功能。
运行时该看什么(GET /metrics,Prometheus):
agentos_in_flight_turns、agentos_admission_limit、agentos_admission_rejected_total—— 过载和 429;agentos_active_sessions、agentos_active_processes—— 工作集大小;agentos_event_db_write_wait_ms、agentos_message_store_queue、agentos_access_log_queue、agentos_event_db_dropped_total—— 写入器拥塞;agentos_turn_duration_ms、agentos_tokens_prompt_total/_completion_total—— 延迟和 token 消耗。
34.2 如何复现测量
来自仓库的测试台(需要 mock_llm + loadgen,构建 release-fast):
cargo build -p agent_web_bot --profile release-fast \
--bin agent-os --example mock_llm --example loadgen
./scripts/loadtest.ps1 # свип 1,2,4,8,16,32,64
./scripts/loadtest.ps1 -Sweep 128,256,1000 -DurationSecs 15
- Mock 提供商(
crates/host/examples/mock_llm.rs)提供 OpenAI 兼容的 SSE,可配置MOCK_TTFT_MS、MOCK_TPS、MOCK_TOKENS和错误注入;在/stats上显示max_concurrent(证明内核没有超过限制)。 - 负载生成器(
crates/host/examples/loadgen.rs)通过 SSE 测量 TTFT 和轮次总时间,--sessions == --concurrency,每个 worker 有自己的会话。 - 测试台配置:
testing/config-scale*(无持久化 / 完整持久化)、max_in_flight_turns: 0、concurrency_limit: 1000。
下文数字的测试台配置:mock TTFT=2000ms, 200 tok/s, 8 токенов(轮次 ≈ 2.04 s),18 个逻辑 CPU,mock + 服务器 + 负载生成器在同一台机器上,每个点 12–15 s。最好在干净的机器上测量:无关进程和累积的数据库会明显扭曲结果。
34.3 吞吐量
Agent OS 无持久化(纯调度器 + HTTP)——内核的「天花板」:
| c | rps | total p50 | TTFT p50 | 错误 |
|---|---|---|---|---|
| 1 | 0.53 | 2135 ms | 2028 ms | 0 |
| 4 | 2.17 | 2126 ms | 2027 ms | 0 |
| 16 | 8.61 | 2140 ms | 2029 ms | 0 |
| 64 | 33.4 | 2138 ms | 2040 ms | 0 |
| 128 | 68.3 | 2151 ms | 2045 ms | 0 |
| 256 | 128.9 | 2163 ms | 2065 ms | 0 |
| 1000 | 459.5 | 2215 ms | 2090 ms | 0 |
延迟几乎持平: 从 c=1 到 c=1000,TTFT 从 2028 增长到 2090 ms,轮次总时间从 2135 增长到 2215 ms。内核本身的开销 ≈ TTFT +30 ms、轮次 +95 ms,且几乎不依赖于竞争。只要 CPU 和 permit 足够,吞吐量就线性增长。
Agent OS 带 DuckDB 数据库(EventDb + 消息存储 + 访问日志;JSONL 关闭):
| c | rps | total p50 | TTFT p50 | 备注 |
|---|---|---|---|---|
| 1 | 0.53 | 2142 ms | 2025 ms | — |
| 4 | 2.15 | 2150 ms | 2032 ms | — |
| 16 | 8.58 | 2149 ms | 2033 ms | — |
| 64 | 34.3 | 2146 ms | 2045 ms | — |
| 128 | 68.4 | 2150 ms | 2047 ms | — |
| 256 | 136.2 | 2159 ms | 2059 ms | — |
| 1000 | 451.0 | 2135 ms | 2055 ms | message_store_queue 至 7k;event_db_dropped 0 |
在约 1000 个并发轮次以内,数据库只消耗约 2% 的吞吐量(451 vs 459),且不影响延迟。超过约 3000 后,吞吐量会撞上相互竞争的 DuckDB 写入器(事件 + 转录 + 访问),消息存储队列溢出,部分事件在过载下被丢弃(event_db_dropped)——如果您要维持数千个带完整历史的并发会话,请考虑 retention/分片。
34.4 内存
我们测量服务器进程的两个量:WorkingSet(常驻内存,包括代码/DLL 的共享页)和 Private(私有内存)。在 Windows 上是 Get-Process;在 Linux 上是 /proc/<pid>/smaps_rollup 中的 VmRSS 和 Private_Dirty。
| 状态 | WorkingSet | Private |
|---|---|---|
| 无存储,启动(steady) | 31 MB | 9 MB |
| 带 DuckDB 数据库,启动 | 108 MB | 98 MB |
| 无存储,约 1450 个会话 | 218 MB | — |
| 带 DuckDB 数据库,约 1470 个会话 | 385 MB | — |
- 关于「启动时 70 MB」。 内核启动时的私有内存只有 ~9 MB;WorkingSet 30–70 MB 主要是共享的、按需分页的二进制文件页(
agent-os.exe~62 MB)和duckdb.dll(~35 MB),它们随代码调用而加载,操作系统可以回收。这是固定的、不增长的开销,而不是泄漏。 - 真正的固定成本是 DuckDB:启用数据库会提升 private 内存从约 9 MB 增至约 98 MB(缓冲管理器,即使无负载)。 因此「70 MB」不是正确的参照:应计算 DuckDB 基础占用(约 100 MB)加上 每会话增量。
- 每会话增量 ≈ 0.13 MB(无存储)/ 0.19 MB(含数据库), 且保持线性:一个会话 = 一个核心进程 + 工作上下文。
- 内存不会立即释放:会话存活至
session_ttl_secs(默认 300 s)。长时间空闲后,清理器会将 RSS 回收到基础水平。 - 转录和事件存储在磁盘上(DuckDB),而非内存中;RSS 增长是 进程/会话状态,而非历史记录。
34.5 过载行为(429)
启用 kernel.max_in_flight_turns > 0(例如在
concurrency_limit: 3 时使用 8)——过载便不再堆积队列:
- 超出限制的请求立即收到
429+Retry-After,开销很低; - 已接受的请求执行到底,没有
5xx和 SSE 中断; - 被拒绝的请求不会取消同一会话中已启动的轮次;
- 负载回落后恢复只需数秒。
建议:在生产环境中始终启用 admission 门控,并将
concurrency_limit 按提供商配额设置(以避免触发外部
429/超时)。
34.6 与 opencode 的对比
相同测试台、相同 mock(TTFT=2000 ms,8 个 token),每 worker 一个会话。
opencode(opencode serve v1.18.32)通过自定义提供商 @ai-sdk/openai-compatible 指向同一 mock;
测量的是完整轮次时间
(POST /session/:id/message)。
重要——公平对比。 opencode 将会话/转录持久化到 SQLite,因此应将其与 Agent OS 含数据库(EventDb + message store + access;JSONL 关闭)对比,而非纯核心。此外,opencode 服务器是 单用户 coding agent 的服务器(会话绑定到项目, 无 admission 控制),而 Agent OS 是多租户聊天/agent 服务器。对比 关注的是扩展特性,而非产品对等性。
吞吐量(Agent OS 含数据库 vs opencode)
| c | Agent OS rps | opencode rps | Agent OS p50 | opencode p50 | opencode 错误 |
|---|---|---|---|---|---|
| 1 | 0.53 | 0.50 | 2142 ms | 2229 ms | 0 |
| 4 | 2.15 | 2.00 | 2150 ms | 2285 ms | 0 |
| 16 | 8.58 | 6.67 | 2149 ms | 2791 ms | 0 |
| 64 | 34.3 | 16.0 | 2146 ms | 4386 ms | 0 |
| 128 | 68.4 | 21.3 | 2150 ms | 6611 ms | 0 |
| 256 | 136.2 | 36.2 | 2159 ms | 11883 ms | 142 |
| 512 | — | 挂起 | — | >27 s | 大量 |
| 1000 | 451.0 | — | 2135 ms | — | — |
- c=1 时的每轮开销:Agent OS ≈ 95 ms,opencode ≈ 190 ms。
- Agent OS 将延迟保持平稳直至 1000;opencode 随 c 增长 以延迟为代价(c=128 → p50 6.6 s,c=256 → 11.9 s),且无 admission 门控, 在 256 时出现错误,在 512 时拒绝服务。
- 在延迟相当的情况下,吞吐量差距为一个数量级 (c=128 时 68 vs 21 rps;高 c 时 451 vs <40)。
内存
| Agent OS(无存储) | Agent OS(DuckDB 数据库) | opencode | |
|---|---|---|---|
| 基础(启动,WorkingSet) | 31 MB | 108 MB | 208–336 MB(Node,因 GC 波动) |
| 基础(private) | 9 MB | 98 MB | — |
| 每会话增量 | ≈ 0.13 MB | ≈ 0.19 MB | ≈ 0.09 MB(空) |
| ~1500 会话(WorkingSet) | 218 MB | 385 MB | 负载下 500–680 MB |
- opencode 的固定基础占用更高(运行时本身),每会话增量 相当。在实际负载下,opencode 在数百会话时观察到 550–680 MB, 且不像带 TTL 清理器的 Agent OS 那样乐于归还内存。
- 估算 Agent OS 的 RAM:`~9 MB(核心)+ ~100 MB(DuckDB 数据库,若启用)
- 0.13–0.19 MB × 并发会话数`。
对比结论
Agent OS 针对竞争性多租户负载而优化:平稳延迟、 吞吐量线性增长、429 保护和 TTL 清理。opencode 服务器在此角色下 退化:延迟线性增长,过载时出现错误/挂起且无 保护。这在意料之中:它的任务不同(面向单用户的交互式 coding agent), 而非承载数千并发会话的服务器。
34.7 实践建议
- 提供商限制:
concurrency_limit= 您的 API 密钥 实际能承受的并发流数。这是主要杠杆。 - 始终启用 admission:在生产环境中使用
max_in_flight_turns > 0,以便 过载时快速返回429,而非队列和超时。 - 持久化——按需:JSONL 可以关闭(
jsonl_dir: ""); DuckDB 数据库用于转录/事件,但这会带来约 100 MB 固定 内存,且是高负载下的主要瓶颈。 - 会话 TTL:将
session_ttl_secs保持在合理范围——这是空闲时 归还内存的手段。 - RAM 规划:
~ (100 MB 数据库,若启用) + 0.13–0.19 MB × 并发会话数,外加峰值余量;「启动 70 MB」主要是 可回收的代码页,而非工作内存。 - 监控
/metrics:in_flight_turns、admission_rejected_total、 写入队列(message_store_queue、access_log_queue)、event_db_dropped_total和turn_duration_ms。 - 回归:保留 baseline 数值并在 CI 中运行
scripts/loadtest.ps1(参见 16-benchmarking);当 p95/RSS 增长超过约 20–25% 时告警。