2e3cb8105d
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>
126 lines
7.0 KiB
Markdown
126 lines
7.0 KiB
Markdown
# 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 的「停摆」:单节点饱和,非 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 秒级延迟面前完全值。
|
||
|
||
### 但有三个要正视的点(不是"拆错了",是"拆了之后要补齐的")
|
||
1. **网关目前是单点且偏重**(HTTP + 每任务一个 Token 录制 goroutine + 多个订阅者)。横向扩/HA 还没做(P1)。
|
||
2. **PG 是全局共享瓶颈**:每任务 3 次状态写。规模再上一个量级时,状态写需要批量/异步化。
|
||
3. **单机数字 ≠ 集群数字**:本报告是单节点天花板;多副本下还需验证 NATS 集群、PG 连接总量、跨副本一致性。
|
||
|
||
---
|
||
|
||
## 6. 待补(后续)
|
||
- ✅ DSN `localhost → 127.0.0.1`(已修,消除每连接 DNS 解析;但不解决 §4.3 的单节点饱和)。
|
||
- 长稳压测(小时级):看内存 / 文件句柄 / goroutine 是否泄漏。
|
||
- 真实 LLM 端到端容量:受在线 provider 限流,需自部署模型后再测。
|
||
- 状态写批量/异步化探索(抬高单节点天花板)。
|
||
- 多副本 + 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 <user> -pass <pass> -levels 1,2,4,8,16,32,64,128,256 -dur 10s
|
||
```
|