topfans/backend/scripts/loadgen/RUNBOOK.md
2026-06-15 20:10:56 +08:00

367 lines
10 KiB
Markdown

# RUNBOOK — 凌晨压测执行手册
> **目标读者**:负责 prod 凌晨压测的 on-call 工程师
> **执行窗口**:02:00 - 06:00 (业务低峰)
> **预计总耗时**:1.5 - 4 小时 (按场景数)
> **风险等级**:🟡 中 (会写 23k+ 测试数据,但物理隔离 star_id=999900)
---
## 0. 前置检查 (T-1 天)
### 0.1 确认 prod 状态
```bash
# SSH 到 prod
ssh root@101.132.250.62
# 确认 prod 网关正常
curl -sS http://localhost:8080/health
# 期望: {"service":"top-fans-gateway","status":"ok"}
# 确认磁盘空间 > 10GB (R5 红线需要)
df -h /opt
# 期望: Avail > 10G
```
### 0.2 确认阿里云快照 < 24h
- 登录 ECS 控制台 → 实例 → 磁盘与镜像 → 快照
- 必须有 < 24h 的快照,**否则不要开压**
- 没有的话先手动触发:实例 更多 磁盘和镜像 创建快照
### 0.3 备份数据库
```bash
ssh root@101.132.250.62
mkdir -p /opt/topfans/backups
pg_dump -h localhost -U postgres topfans > /opt/topfans/backups/pre-loadtest-$(date +%Y%m%d-%H%M).sql
ls -lh /opt/topfans/backups/pre-loadtest-*.sql
# 期望: 文件 > 50MB
```
---
## 1. 上传/确认工具 (T-30min)
### 1.1 确认工具已上传到 prod
```bash
ssh root@101.132.250.62
ls -la /opt/topfans/loadtest/
# 必须看到:
# seed (二进制)
# loadgen (二进制)
# loadtest_bcrypt.txt
# scripts/prod_seed.sh
# README.md
# reports/ (空目录)
```
如果文件缺失,本地重新上传:
```bash
# 本地 (从 backend 目录)
cd /Users/liulujian/Documents/code/TopFansByGithub/backend
# 重新编译
make loadgen-build
# 上传
scp bin/seed bin/loadgen root@101.132.250.62:/opt/topfans/loadtest/
scp scripts/loadgen/seed/loadtest_bcrypt.txt root@101.132.250.62:/opt/topfans/loadtest/
scp scripts/loadgen/scripts/prod_seed.sh root@101.132.250.62:/opt/topfans/loadtest/scripts/
ssh root@101.132.250.62 "chmod +x /opt/topfans/loadtest/{seed,loadgen} /opt/topfans/loadtest/scripts/prod_seed.sh"
```
### 1.2 重新生成 bcrypt 哈希 (如果你改了密码策略)
```bash
# 本地
cd backend/scripts/loadgen/seed
# 生成与 tokens.go 硬编码密码 (默认 "Test@123") 匹配的哈希
python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \
> loadtest_bcrypt.txt
# 上传覆盖
scp loadtest_bcrypt.txt root@101.132.250.62:/opt/topfans/loadtest/
```
---
## 2. 数据准备 (T0 = 02:00)
### 2.1 SSH 到 prod
```bash
ssh root@101.132.250.62
```
### 2.2 一键跑 seed (生产数据灌入)
```bash
cd /opt/topfans/loadtest
bash scripts/prod_seed.sh
```
**这一步骤会做什么**:
- `/opt/topfans/docker/.env.prod` DB_PASSWORD + JWT_SECRET
- 插入 star_id=999900 测试明星 (1 )
- 插入 1000 个测试用户 (mobile 19900000001 - 19900001000)
- 插入 1000 fan_profile + crystal
- 插入 5000 assets
- 插入 3000 booth_slots + 2000 exhibitions
- 插入 10000 friendships
- **重置所有相关表的 PG 序列** (CLAUDE.md 规范,避免后续 GORM 插入报 duplicate key)
- 1000 JWT,写到 `users.csv`
**预计耗时**:30 - 60
**预期输出**:
```
✓ stars seeded
✓ 1000 users seeded
✓ 1000 fan_profiles + crystal seeded
✓ 5000 assets seeded
✓ 3000 booth_slots + 2000 exhibitions seeded
✓ 10000 friendships seeded
✓ sequences reset
✅ users.csv written: 1000 rows
✅ seed + tokens completed
```
---
## 3. 开压前 7 项检查 (T0+1min)
```bash
cd /opt/topfans/loadtest
./loadgen --cmd=preflight --target=http://localhost:8080
```
**预期全部 PASS**:
```
✓ ① Gateway /health HTTP 200
✓ ② SSH to prod (省略,如不需要 server metrics)
✓ ③ pg_dump backup > 50MB (你的备份)
✓ ④ 阿里云快照 < 24h (人工确认)
✓ ⑤ prod 磁盘空闲 > 10GB free > 10G
✓ ⑥ users.csv 1000 rows rows=1000
✓ ⑦ JWT_SECRET set set
ALL CHECKS PASSED — 可以开压
```
**如果有 FAIL**:见 "附录 A: 故障排查"
---
## 4. 烟雾测试 (T0+2min) — 强烈推荐
> 这一步只花 30 秒,但能提前发现 90% 的集成问题,省后面 1 小时排错
```bash
cd /opt/topfans/loadtest
JWT_SECRET=$(grep '^JWT_SECRET=' /opt/topfans/docker/.env.prod | cut -d= -f2) \
./loadgen --cmd=run --scenarios=S1 --stage=baseline --rps=1 --duration=30s \
--target=http://localhost:8080 --monitor=off 2>&1 | tee reports/smoke-s1.log
```
**预期**:
```
📊 S1: total=30 err=0 5xx=0 p99=200ms stages=1
✅ loadgen done. total=30 err=0 fiveXX=0
```
**判定**:
- total=30, err=0 进入正式压测
- total < 30 跑挂了, `reports/smoke-s1.log`
- err > 0 → auth/JWT 问题,检查 `users.csv` 和 JWT_SECRET
---
## 5. 正式压测 (T0+3min)
### 5.1 选择策略
**Plan B 推荐** (S1 + S2 + S4,~1.5 小时):
```bash
cd /opt/topfans/loadtest
export JWT_SECRET=$(grep '^JWT_SECRET=' /opt/topfans/docker/.env.prod | cut -d= -f2)
export PROD_SSH=root@101.132.250.62
# === 场景 1: Login (02:05-02:30, 25min) ===
./loadgen --cmd=run --scenarios=S1 \
--stage=step --step-schedule='2,5,10,15,20' \
--duration=5m --target=http://localhost:8080 \
--monitor=full --prod-ssh=$PROD_SSH \
--inter-scenario-pause=0s 2>&1 | tee reports/s1.log
# 预期: 5 个 stage,每 stage 5min,p99 应随 RPS 阶梯上升
# === 场景 2: Read (02:35-03:00, 25min) ===
./loadgen --cmd=run --scenarios=S2 \
--stage=step --step-schedule='10,30,60,100,150' \
--duration=5m --target=http://localhost:8080 \
--monitor=full --prod-ssh=$PROD_SSH \
--inter-scenario-pause=0s 2>&1 | tee reports/s2.log
# === 场景 4: Mint (03:05-03:30, 25min, 写重,保守) ===
./loadgen --cmd=run --scenarios=S4 \
--stage=step --step-schedule='1,2,3,5' \
--duration=5m --target=http://localhost:8080 \
--monitor=full --prod-ssh=$PROD_SSH \
--inter-scenario-pause=0s 2>&1 | tee reports/s4.log
```
**Plan A 全量** (S1-S7,~3.5 小时):
```bash
# S1-S7 全部跑,S4/S7 写重场景保守
SCENARIOS="S1,S2,S3,S4,S5,S6,S7"
SCHEDULES_BY_SCENARIO='{"S1":"2,5,10,15,20","S2":"10,30,60,100,150","S3":"5,15,30,50","S4":"1,2,3,5","S5":"5,10,20,40","S6":"20,50,100,150","S7":"1,2,3,5"}'
# (目前 loadgen 一次只支持一个 schedule,需要跑 7 次)
```
### 5.2 每个场景跑完后做什么
1. 检查 `reports/{scenario}.log` 末尾的 `📊`
2. 记录 total / err / 5xx / p99 / stages
3. 如果 `🚨 circuit breaker tripped` 触发,**立即停**,见附录 B
---
## 6. 生成报告 (T+1min)
```bash
cd /opt/topfans/loadtest
./loadgen --cmd=report --input=./reports --output=./reports/final-report.md
```
**产出**:
```
reports/
├── S1.json
├── S2.json
├── S4.json
├── baseline.csv # Excel 可直接打开
├── s1.png # RPS/P99/Error 曲线图
├── s2.png
├── s4.png
└── final-report.md # 人看的报告
```
---
## 7. 收尾 (T+2min)
### 7.1 拉报告到本地
```bash
# 本地
mkdir -p ~/Desktop/loadtest-report-$(date +%Y%m%d)
scp -r root@101.132.250.62:/opt/topfans/loadtest/reports/* ~/Desktop/loadtest-report-$(date +%Y%m%d)/
```
### 7.2 决定是否清理测试数据
| 情况 | 动作 |
|------|------|
| 数据分析完,后续不需要 | `./seed --cleanup --full` |
| 数据还要保留做下一轮 | `./seed --cleanup` (保留 1000 用户,清理关联数据) |
| 只是 JWT 过期 | `./seed --reset-tokens --jwt-secret=$JWT_SECRET` |
| **生产事故** | `./seed --cleanup --full` + 立即回滚,见附录 C |
### 7.3 (可选) 关闭监控后台采样
```bash
# 如果你启动了 monitor/sample.sh,杀掉
ssh root@101.132.250.62 "pkill -f 'monitor/sample.sh'"
```
---
## 8. 报告分析 (T+30min,白天)
`REPORT_GUIDE.md` — 教你怎么读 `final-report.md`,定位瓶颈,写行动项。
---
## 附录 A: 故障排查
### A.1 preflight FAIL: users.csv 不存在
**原因**: 上次 seed 没跑成功
**修复**: `cd /opt/topfans/loadtest && bash scripts/prod_seed.sh`
### A.2 preflight FAIL: 阿里云快照 < 24h
**原因**: 没备份
**修复**: 在 ECS 控制台手动建快照,等就绪后重跑 preflight
### A.3 烟雾测试 FAIL: 大量 4xx
**原因**: JWT_SECRET 不匹配 / users.csv 过期
**修复**:
```bash
# 1. 确认 JWT_SECRET
grep '^JWT_SECRET=' /opt/topfans/docker/.env.prod
# 2. 重签 token (数据保留)
./seed --reset-tokens --jwt-secret=$JWT_SECRET
# 3. 重跑
./loadgen --cmd=run --scenarios=S1 --stage=baseline --rps=1 --duration=30s \
--target=http://localhost:8080 --monitor=off
```
### A.4 烟雾测试 FAIL: 大量 5xx
**原因**: 网关/服务挂了
**修复**: 先看 `docker ps` 确认服务在,`curl /health` 确认网关活
---
## 附录 B: Circuit Breaker 触发 (🚨)
如果出现 `🚨 circuit breaker tripped!`,**立即**:
1. **Ctrl+C** 停止当前 loadgen (会 graceful shutdown,等待当前请求完成)
2. 立即判断:
- 5xx > 10% 持续 10s → 服务有问题,见附录 C
- 仅客户端错率高 → 测试问题,可能是 step 跳太猛
3. **降低 RPS 重跑****改天再试**
---
## 附录 C: 紧急灭火 (production 被打挂了)
**判定**: 服务真实报错(不是测试客户端问题),prod 用户受影响。
**立即执行** (按顺序,每步 30s 内):
```bash
ssh root@101.132.250.62
# 1. 停 loadgen + 监控
pkill -f 'bin/loadgen'
pkill -f 'monitor/sample.sh'
# 2. 清测试数据 (1 秒)
cd /opt/topfans/loadtest
./seed --cleanup --full
# 3. 重启服务 (让 prod 回到 baseline)
cd /opt/topfans/docker
docker-compose -f docker-compose.prod.yml --profile prod restart
# 4. (最严重情况) 从备份还原
bash /opt/topfans/loadtest/recover/restore-from-backup.sh
# 输入 backup 文件路径,预计 5-8 分钟
```
**事后**:
- 写事故复盘
- 修压测发现的 bug
- 调整 step schedule (下一次更保守)
---
## 附录 D: 常用 cheat sheet
```bash
# 查看 loadtest 进程
ssh root@101.132.250.62 "ps aux | grep -E '(loadgen|sample)' | grep -v grep"
# 看实时日志
ssh root@101.132.250.62 "tail -f /opt/topfans/loadtest/reports/*.log"
# 看 metrics feed
ssh root@101.132.250.62 "tail -f /opt/topfans/loadtest/metrics-feed.jsonl"
# 测一下网关还活着
ssh root@101.132.250.62 "curl -sS http://localhost:8080/health"
```