diff --git a/LOAD_TEST_REPORT.md b/LOAD_TEST_REPORT.md new file mode 100644 index 0000000..f7ad74b --- /dev/null +++ b/LOAD_TEST_REPORT.md @@ -0,0 +1,117 @@ +# sundynix-agentix 容量压测报告(首版) + +> 日期:2026-06-26 | 环境:单机本地(macOS / Docker 基建)| 撰写:Opus 4.8(基于本次实测,刻意压低水分) +> 部署相关讨论留待开发完成后另议;本报告只回答一个问题:**平台自身扛得住多少、瓶颈在哪。** + +--- + +## 1. 目的 + +回答两个一直悬而未决的问题: + +1. 平台(去掉 LLM 推理本身)的吞吐天花板是多少?单任务固有开销多大? +2. 把系统拆成「网关 / 总线 / 调度 / 工具」多服务,到底有没有意义? + +--- + +## 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 硬崩的根因(易修) +网关日志:`failed to connect ... lookup localhost: operation was canceled`。 +DSN 用了主机名 `localhost`,pgx 每新建一条连接都做一次 DNS 解析;极端并发下连接抖动 → 大量 DNS 查询 +在请求超时里被取消 → 雪崩。**修法简单**:DSN 改 `127.0.0.1`(免 DNS)+ 连接复用调优。属配置级修复,非架构问题。 + +### 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 秒级延迟面前完全值。 + +### 但有三个要正视的点(不是"拆错了",是"拆了之后要补齐的") +1. **网关目前是单点且偏重**(HTTP + 每任务一个 Token 录制 goroutine + 多个订阅者)。横向扩/HA 还没做(P1)。 +2. **PG 是全局共享瓶颈**:每任务 3 次状态写。规模再上一个量级时,状态写需要批量/异步化。 +3. **单机数字 ≠ 集群数字**:本报告是单节点天花板;多副本下还需验证 NATS 集群、PG 连接总量、跨副本一致性。 + +--- + +## 6. 待补(后续) +- 长稳压测(小时级):看内存 / 文件句柄 / goroutine 是否泄漏。 +- 真实 LLM 端到端容量:受在线 provider 限流,需自部署模型后再测。 +- DSN `localhost → 127.0.0.1`(顺手修,消除 §4.3 崩溃)。 +- 状态写批量/异步化探索(抬高单节点天花板)。 +- 多副本 + NATS 集群下的容量复测。 + +--- + +## 复现 + +```bash +# 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 -pass -levels 1,2,4,8,16,32,64,128,256 -dur 10s +```