# 后端服务压测工具 (loadgen) > 给阿里云单机 (4G/2C) TopFans 后端微服务用的压测 + 数据准备工具集。 > 凌晨 02:00-06:00 业务低峰执行,数据物理隔离 `star_id=999900`。 --- ## 📚 文档地图 | 文档 | 用途 | 谁要看 | |------|------|--------| | **README.md** (本文) | 工具集概览 + 5 分钟入门 | 所有人 | | [RUNBOOK.md](RUNBOOK.md) | 凌晨压测**一步一步**操作手册 | on-call 工程师 | | [REPORT_GUIDE.md](REPORT_GUIDE.md) | 压测报告**怎么读** + 瓶颈定位 + 行动项模板 | 看报告的工程师 / TL | | [seed/README.md](seed/README.md) | seed 工具细节 (数据准备) | 第一次跑压测的人 | --- ## 🧰 工具集概览 ``` loadgen/ ├── seed/ # 数据准备 CLI (生成 1000 个测试用户 + 资产 + JWT) ├── loadgen/ # 压测主程序 (7 个场景,6 维熔断,带 reporter) ├── monitor/ # 监控栈 (Prometheus + Grafana,可选) ├── recover/ # 紧急灭火 (一键停 + 数据库恢复) ├── scripts/ # 部署到 prod 的辅助脚本 └── reports/ # 跑测产出 (gitignore,scp 拉回本地) ``` ### 核心 CLI: `bin/seed` + `bin/loadgen` | 命令 | 作用 | |------|------| | `./bin/seed` | 灌测试数据 → `users.csv` + 数据库 | | `./bin/seed --cleanup` | 清理测试数据 (保留 1000 用户) | | `./bin/seed --cleanup --full` | 全部删掉 (账号本身) | | `./bin/seed --reset-tokens` | 只重签 JWT (跨周压测用) | | `./bin/loadgen --cmd=preflight` | 7 项开压前检查 | | `./bin/loadgen --cmd=run --scenarios=S1` | 跑场景 | | `./bin/loadgen --cmd=report` | 生成 markdown 报告 + PNG 图表 | ### 7 个场景 | ID | 场景 | 默认 RPS | 写/读 | 关键 API | |----|------|---------|------|---------| | S1 | Login | 15 | 写(轻) | `POST /api/v1/auth/login` | | S2 | Read | 250 | 读 | `GET /api/v1/assets/{id}` | | S3 | Like | 50 | 写(轻) | `POST/DELETE /api/v1/social/assets/{id}/like` | | S4 | Mint | 1-5 | **写(重)** | `POST /api/v1/assets/mints/precreate` | | S5 | Dashboard | — | 读聚合 | (dashboard 聚合) | | S6 | Ranking | 300 | 读 | `GET /api/v1/rankings/hot` | | S7 | Place | 1-5 | **写(重)** | (摆展事务) | --- ## 🚀 5 分钟入门 (本地 docker) ```bash # 1. 编译 (Linux prod 部署用,本地 darwin 直接 go build) cd backend make loadgen-build # 2. 准备数据 (需要本地 docker postgres) cd scripts/loadgen/seed # 生成 bcrypt 哈希 (与 tokens.go 硬编码的 "Test@123" 匹配) python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \ > loadtest_bcrypt.txt # 跑 seed (用本地 docker 的 env) DB_PASSWORD=123456 \ JWT_SECRET=topfans-secret-key-local-dev-only \ /Users/liulujian/Documents/code/TopFansByGithub/backend/bin/seed \ --db-name=top-fans --db-host=localhost --db-port=15432 --db-user=postgres # 3. 复制 users.csv 到 backend 目录 cp users.csv ../../../users.csv # 4. 开压前检查 cd ../../../ # = backend JWT_SECRET=topfans-secret-key-local-dev-only \ ./bin/loadgen --cmd=preflight --target=http://localhost:8080 # 5. 烟雾测试 (30 秒,1 RPS) JWT_SECRET=topfans-secret-key-local-dev-only \ ./bin/loadgen --cmd=run --scenarios=S1 --stage=baseline --rps=1 --duration=30s \ --target=http://localhost:8080 --monitor=off # 6. 生成报告 JWT_SECRET=topfans-secret-key-local-dev-only \ ./bin/loadgen --cmd=report --input=./reports --output=./reports/final-report.md open reports/final-report.md # macOS ``` --- ## 🔨 编译 ```bash cd backend make loadgen-build # 编译 seed + loadgen 到 bin/ make loadgen-test # 单元测试 (23 个) make loadgen-vet # go vet make loadgen-ci # vet + test + build (CI 单步) ``` 手动编译 (Linux prod): ```bash GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/seed ./scripts/loadgen/seed/ GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/loadgen ./scripts/loadgen/loadgen/ ``` --- ## 🛡️ 安全设计 ### 数据隔离 所有测试数据用 `star_id = 999900` 物理隔离,**不影响**真实业务 star_id (87, 88, 91, 93, 94, 95)。 ### CLAUDE.md 序列重置 seed 工具末尾自动同步所有相关表的 PG 序列(避免后续 GORM 插入报 duplicate key)。 ### 凌晨窗口 执行窗口:**02:00 - 06:00** 业务低峰。 紧急灭火: `recover/emergency-stop.sh` 一键停 + `restore-from-backup.sh` 5-8min 还原。 ### 6 维红线熔断 (自动停) | # | 红线 | 阈值 | 数据源 | |---|------|------|--------| | R1 | 客户端错误率 | > 5% 持续 30s | loadgen HDR | | R2 | 客户端 P99 | > 3000ms 持续 30s | loadgen HDR | | R3 | 5xx 比例 | > 10% 持续 10s | loadgen status | | R4 | PG 连接数 | > 42 持续 30s | metrics-feed | | R5 | 磁盘空闲 | < 5GB 持续 30s | metrics-feed | | R6 | OOM 事件 | 瞬时触发 | metrics-feed | --- ## 📊 报告产出 跑完 + `--cmd=report` 后,`reports/` 下: ``` reports/ ├── S1.json # 原始数据 (含 stages) ├── S2.json ├── S4.json ├── baseline.csv # Excel 友好的汇总 ├── s1.png # RPS / P99 / Error 曲线 ├── s2.png ├── s4.png └── final-report.md # ← 主要看这个 ``` `final-report.md` 包含: 1. **总览表** (所有场景一行一个,7 列) 2. **每个场景的 ⚠️ 拐点 RPS** (自动算:第一个 p99 涨 >50% 的 stage) 3. **阶梯结果表** (每 stage 的 RPS / p50 / p95 / p99 / err / 5xx) 4. **PNG 曲线图** (RPS / P99 / Error 三条线) 详细读法见 [REPORT_GUIDE.md](REPORT_GUIDE.md)。 --- ## 🧪 测试状态 ``` seed: 5/5 PASS loadgen/lib: 16/16 PASS scenarios: 2/2 PASS TOTAL: 23/23 PASS ``` --- ## 📁 完整目录 ``` backend/scripts/loadgen/ ├── README.md # ← 你在这里 ├── RUNBOOK.md # ← 凌晨压测操作手册 ├── REPORT_GUIDE.md # ← 报告怎么读 ├── seed/ # 数据准备工具 │ ├── main.go # CLI 入口 │ ├── stars.go users.go profiles.go assets.go │ ├── slots_and_exhibits.go friendships.go │ ├── tokens.go sequences.go cleanup.go │ ├── seed_test.go # 单元测试 │ ├── loadtest_bcrypt.txt # Test@123 哈希 (与 tokens.go 匹配) │ └── README.md ├── loadgen/ # 压测主程序 │ ├── main.go # CLI 入口 │ ├── preflight.go verify.go # 7 项开压前检查 + 压后验证 │ ├── lib/ # 核心库 │ │ ├── csv.go # users.csv 解析 │ │ ├── client.go # HTTP client │ │ ├── hdr.go # 延迟直方图 + per-stage 计数 │ │ ├── log.go ramp.go # 日志 + 阶梯调度 │ │ ├── circuit.go # 6 维熔断 │ │ ├── ssh_metrics.go # prod server metrics 抓取 │ │ ├── config.go │ │ └── *_test.go # 16 个测试 │ ├── scenarios/ # 7 个场景 │ │ ├── s1_login.go │ │ ├── s2_read.go │ │ ├── s3_like.go │ │ ├── s4_mint.go # 支持多 stage │ │ ├── s5_dashboard.go │ │ ├── s6_ranking.go │ │ ├── s7_place.go │ │ ├── common.go # doRequest + DefaultBaseURL │ │ ├── scenarios.go # 注册表 │ │ ├── helpers.go │ │ └── scenarios_test.go │ └── reporter/ # 报告生成 │ ├── json.go # RunReport + StageReport │ ├── csv.go # baseline.csv │ ├── plot.go # PNG 曲线 (gonum) │ ├── markdown.go # final-report.md │ └── knee.go # KneeRPS 自动算 ├── monitor/ # 监控栈 (可选) │ ├── sample.sh # 后台采样到 metrics-feed.jsonl │ ├── docker-compose.monitor.yml │ ├── prometheus.yml │ └── grafana-dashboards/ # 4 个预置面板 ├── recover/ # 紧急灭火 │ ├── emergency-stop.sh │ └── restore-from-backup.sh ├── scripts/ # prod 辅助 │ ├── mint_reset.sh # S4 之间的 mint 数据清理 │ └── prod_seed.sh # 一键跑 seed (读 prod env) └── reports/ # 跑测产出 (gitignore) ``` --- ## 详细设计 - **设计文档**: `docs/superpowers/specs/2026-06-12-load-testing-design.md` - **实施计划**: `docs/superpowers/plans/2026-06-12-load-testing.md` - **seed 工具说明**: [seed/README.md](seed/README.md)