394 lines
11 KiB
Markdown
394 lines
11 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.0 ⭐ 推荐:本地一键压测 (无需 SSH 到生产机)
|
|
|
|
> **新方式**:`scripts/prod_loadtest.sh` 一站式脚本,在本地跑,自动建 SSH 隧道/读凭据/调 seed+loadgen+cleanup。
|
|
> 比"SSH 进去跑 prod_seed.sh"更安全、更可复现、更适合 CI。
|
|
|
|
```bash
|
|
cd /Users/liulujian/Documents/code/TopFansByGithub
|
|
|
|
# 一键完整流程 (up → backup → seed → preflight → loadgen → report → cleanup-all → down)
|
|
./backend/scripts/loadgen/scripts/prod_loadtest.sh pipeline \
|
|
--scenarios=S1,S2,S4 --stage=step --step-schedule='5,10,20' --duration=60s
|
|
```
|
|
|
|
子命令、状态查询、故障排查:见 [scripts/README.md](scripts/README.md)。
|
|
|
|
---
|
|
|
|
### 2.1 (旧方式) SSH 到 prod 后跑 prod_seed.sh
|
|
|
|
> ⚠️ **仅作为应急 / 兼容路径**。如果生产机没法 SSH 隧道、新流程出问题时,可以用这个。
|
|
|
|
```bash
|
|
ssh root@101.132.250.62
|
|
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"
|
|
```
|