DSN 由 localhost 改 127.0.0.1(gateway + mcp-go):pgx 每新建连接解析一次主机名, 高并发连接池扩容时省掉这层 DNS 开销与一类失败模式。 诚实订正:复测发现 DSN 改完 C=256 仍 0 完成(错误从 lookup localhost 变 dial 127.0.0.1 canceled)。 故 C=256 的「停摆」根因不是 DNS,而是单节点在 256 并发下整体饱和(任务排队 + 每任务 3 次 PG 状态写 + 网关 handler 顶不住),属单节点容量上限、非 bug;C≤128 优雅可用,正解是横向扩。 LOAD_TEST_REPORT §4.3/§6 与 project_analysis 容量实测已据实更新。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.0 KiB
sundynix-agentix 容量压测报告(首版)
日期:2026-06-26 | 环境:单机本地(macOS / Docker 基建)| 撰写:Opus 4.8(基于本次实测,刻意压低水分) 部署相关讨论留待开发完成后另议;本报告只回答一个问题:平台自身扛得住多少、瓶颈在哪。
1. 目的
回答两个一直悬而未决的问题:
- 平台(去掉 LLM 推理本身)的吞吐天花板是多少?单任务固有开销多大?
- 把系统拆成「网关 / 总线 / 调度 / 工具」多服务,到底有没有意义?
2. 方法
- 加压工具:
sundynix-dispatcher/cmd/loadtest。闭环加压(N 个 worker 不停submit → 等收尾 → 再来), 阶梯提升并发,每档跑 10s。 - 完成判定:经 SSE token 流读到收尾(而非轮询状态)。
早期版本用 25ms 轮询,结果把"轮询放大"自身测成了瓶颈(海量 status 请求压垮网关); 改用流后每任务仅 1 条推送连接,量到的才是真实任务管线吞吐。
- 隔离 LLM:dispatcher 开
LLM_FORCE_STUB=1+LLM_STUB_TTFT_MS=0+LLM_STUB_INTERTOKEN_MS=0, 绕开真实模型推理与计费,只压全链路 plumbing:网关 HTTP → NATS/JetStream → 调度消费 → 图执行 → Token 回流(SSE) → PG 状态写。 - 配置:单 dispatcher 实例,
DISPATCHER_CONCURRENCY=64,网关放开限流(RATE_LIMIT_PER_MIN)。
3. 实测数据
| 并发 | 吞吐 (任务/s) | p50 | p95 | max | 说明 |
|---|---|---|---|---|---|
| 1 | 23 | 42ms | 46ms | 54ms | 单任务纯平台开销 ≈ 42ms |
| 2 | 37 | 53ms | 56ms | 70ms | |
| 4 | 54 | 74ms | 77ms | 83ms | |
| 8 | 76 | 103ms | 115ms | 157ms | |
| 16 | 96 | 161ms | 176ms | 257ms | |
| 32 | 112 | 269ms | 345ms | 402ms | 吞吐峰值 |
| 64 | 112 | 557ms | 676ms | 707ms | 饱和:延迟翻倍、吞吐不增 |
| 128 | 102 | 1173ms | 1590ms | 1629ms | 过载,优雅降速(未崩) |
| 256 | 0 | — | — | — | 硬崩(见 §4.3) |
吞吐随并发上升到 ~32 后见顶(~110/s),之后增并发只增延迟不增吞吐 —— 典型饱和曲线。
4. 分析
4.1 单节点平台天花板 ≈ 110 全链路任务/秒
单任务固有开销 ~42ms,由这些串起来:网关收请求 → JetStream 持久化发布(含 fsync)→ 调度消费 → 建图执行 → Token 经 core NATS 回流 + Redis 录制 → 3 次 PG 状态写(submitted / running / done)。
4.2 瓶颈不是数据库连接数,是「每任务多跳」
把 PG 连接池从 25 调到 80,吞吐纹丝不动(仍 ~110/s)。说明瓶颈不在某一处资源,而是 每个任务要走的一长串同步跳数(JetStream 落盘 + 多次 NATS 回写 + Redis 录制 + 多次 PG 写)的累加成本。 要把单节点吞吐再往上抬,得减少每任务的同步跳数(如状态写批量化/异步化),而非简单加资源。
4.3 C=256 的「停摆」:单节点饱和,非 bug
现象:C=256 在 10s 窗口内 0 完成。
初判曾归因于 DSN 用 localhost 致 pgx 每连接 DNS 解析、极端并发下 DNS 查询被取消。
已把 DSN 改为 127.0.0.1(确实消除了 DNS 那一层 + 每连接解析开销,是值得的卫生改进),
但复测 C=256 仍 0 完成 —— 错误从 lookup localhost 变成 dial tcp 127.0.0.1:5432: operation was canceled。
真实根因:单节点在 256 并发下整体饱和 —— 任务排队 + PG 状态写路径(每任务 3 写、池 25)+ 网关 handler/连接 顶不住,单任务延迟超过测试窗口,故窗口内零完成(那些 "canceled" 是窗口到点取消在途请求的尾部噪声)。 这不是要修的 bug,是单节点容量上限的表现:C≤128 优雅降速可用,256 已远超单 dispatcher 的 ~110/s 甜区。 正解是横向扩(加 dispatcher 副本,队列组自动分摊)——正好印证 §4.4 的判断。 单节点要再抬高,则需状态写批量/异步化(降低每任务的同步 PG 跳数)。
4.4 核心判断:平台不是瓶颈,GPU 才是
- 平台固有开销 ~42ms / 任务。
- 真实 LLM 出一轮答案(哪怕自部署 Qwen 32B)是 秒级(几百 ms ~ 数秒)。
- 即 平台开销比 LLM 推理小 1~2 个数量级,淹没在模型延迟里、可忽略。
→ 一个 dispatcher 副本就能喂 ~110 turns/s 的调度能力;而单张 GPU 跑 32B 大约出 几十 turns/s。平台调度能力 > 单 GPU 产能,意味着:
- 平台永远不会先于 GPU 成为瓶颈;
- 要提总吞吐,正确做法是加 dispatcher 副本 + 加 GPU(NATS 队列组自动负载均衡),而非优化平台代码。
5. 架构 / 服务拆分是否成立?—— 成立
本次压测正面验证了"拆服务"的设计前提:
| 设计选择 | 压测给出的判断 |
|---|---|
| 网关 / 调度 / 工具 拆成独立服务 | ✅ 成立。各层可独立横向扩;调度是无状态消费者,加副本即扩吞吐 |
| NATS 队列组做负载均衡 | ✅ 成立。这是"加副本即扩容"的基础,且天花板(110/s)远高于单 GPU 产能 |
| 调度与工具用消息总线解耦 | ✅ 成立。42ms 的总线开销相对 LLM 秒级延迟可忽略,换来的解耦/弹性是划算的 |
| dispatcher 不持有 DB、状态经总线回网关落库 | ✅ 成立。让调度保持无状态、可水平复制 |
一句话:对"喂满自部署 GPU 集群、按 LLM 产能横向扩"这个生产目标,当前拆分是对的。 总线那 ~42ms 的代价,买到了无状态、可水平扩、故障隔离 —— 在 LLM 秒级延迟面前完全值。
但有三个要正视的点(不是"拆错了",是"拆了之后要补齐的")
- 网关目前是单点且偏重(HTTP + 每任务一个 Token 录制 goroutine + 多个订阅者)。横向扩/HA 还没做(P1)。
- PG 是全局共享瓶颈:每任务 3 次状态写。规模再上一个量级时,状态写需要批量/异步化。
- 单机数字 ≠ 集群数字:本报告是单节点天花板;多副本下还需验证 NATS 集群、PG 连接总量、跨副本一致性。
6. 待补(后续)
- ✅ DSN
localhost → 127.0.0.1(已修,消除每连接 DNS 解析;但不解决 §4.3 的单节点饱和)。 - 长稳压测(小时级):看内存 / 文件句柄 / goroutine 是否泄漏。
- 真实 LLM 端到端容量:受在线 provider 限流,需自部署模型后再测。
- 状态写批量/异步化探索(抬高单节点天花板)。
- 多副本 + NATS 集群下的容量复测(验证横向扩是否线性)。
复现
# dispatcher 以 stub 模式起(绕开 LLM),网关放开限流
LLM_FORCE_STUB=1 LLM_STUB_TTFT_MS=0 LLM_STUB_INTERTOKEN_MS=0 DISPATCHER_CONCURRENCY=64 ./sdx-dispatcher
RATE_LIMIT_PER_MIN=1000000 ./sdx-gateway
# 加压(阶梯并发,每档 10s)
go run ./cmd/loadtest -url http://localhost:8080 \
-email <user> -pass <pass> -levels 1,2,4,8,16,32,64,128,256 -dur 10s