topfans/backend/scripts/loadgen/RUNBOOK.md
2026-06-15 21:12:59 +08:00

10 KiB

RUNBOOK — 凌晨压测执行手册

目标读者:负责 prod 凌晨压测的 on-call 工程师 执行窗口:02:00 - 06:00 (业务低峰) 预计总耗时:1.5 - 4 小时 (按场景数) 风险等级:🟡 中 (会写 23k+ 测试数据,但物理隔离 star_id=999900)


0. 前置检查 (T-1 天)

0.1 确认 prod 状态

# 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 备份数据库

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

ssh root@101.132.250.62
ls -la /opt/topfans/loadtest/
# 必须看到:
#   seed           (二进制)
#   loadgen        (二进制)
#   loadtest_bcrypt.txt
#   scripts/prod_seed.sh
#   README.md
#   reports/       (空目录)

如果文件缺失,本地重新上传:

# 本地 (从 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 哈希 (如果你改了密码策略)

# 本地
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

ssh root@101.132.250.62

2.2 一键跑 seed (生产数据灌入)

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)

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 小时排错

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)

⚠️ 重要 flag: --inter-scenario-pause (默认 0s)

  • prod 凌晨窗口直接连跑,不加这个 flag 或显式写 =0s
  • 旧版本默认是 15 分钟,如果你升级前用过请确认

5.1 选择策略

Plan B 推荐 (S1 + S2 + S4,~1.5 小时):

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 小时):

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

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 拉报告到本地

# 本地
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 (可选) 关闭监控后台采样

# 如果你启动了 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 过期 修复:

# 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 内):

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

# 查看 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"