# 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
```
