# 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 ``` **脚本会先做这些预检查** (任一失败立即退出,不会写数据): 1. `/opt/topfans/docker/.env.prod` 存在 + `DB_PASSWORD` / `JWT_SECRET` 非空 2. `seed` 二进制 + `loadtest_bcrypt.txt` 都存在 3. 如果 `psql` 可用,会先 `SELECT 1` 验证能连到 `localhost:5432/topfans` **然后**打印连接信息 + 删除/重建摘要,要求输入 `y` 确认才执行。 **这一步骤会做什么**: - 显式传所有 DB 参数 (`--db-host=localhost --db-port=5432 --db-name=topfans --db-user=postgres`),不依赖 seed 默认值 - 插入 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) > ⚠️ **重要 flag**: `--inter-scenario-pause` (默认 `0s`) > - prod 凌晨窗口直接连跑,**不加这个 flag 或显式写 `=0s`** > - 旧版本默认是 15 分钟,如果你升级前用过请确认 ### 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" ```