264 lines
8.7 KiB
Markdown
264 lines
8.7 KiB
Markdown
# seed - 压测数据准备工具
|
|
|
|
> 给 prod 凌晨压测灌 1000 个测试用户 + 资产 + JWT,数据用 `star_id=999900` 物理隔离。
|
|
|
|
---
|
|
|
|
## ⭐ 推荐入口:prod_loadtest.sh
|
|
|
|
> **如果你目标是打生产压测,优先用 [`../scripts/prod_loadtest.sh`](../scripts/prod_loadtest.sh) 一站式脚本**。
|
|
> 它会把下面的所有步骤(SSH 隧道/seed/preflight/loadgen/report/cleanup)封装成子命令,
|
|
> 凭据从 `docker/.env.prod` 自动读,避免明文泄露。
|
|
|
|
```bash
|
|
# 一键完整流程
|
|
./../scripts/prod_loadtest.sh pipeline --scenarios=S1,S2,S4 \
|
|
--stage=step --step-schedule='5,10,20' --duration=60s
|
|
```
|
|
|
|
详见 [../scripts/README.md](../scripts/README.md)。
|
|
|
|
---
|
|
|
|
## 一句话总结
|
|
|
|
跑 `./seed`,数据库里多出 1000 个用户 + 5000 个 assets + 2000 个 exhibitions,本地多出 `users.csv` (含 JWT)。
|
|
|
|
---
|
|
|
|
## 编译
|
|
|
|
```bash
|
|
cd backend
|
|
go build -o bin/seed ./scripts/loadgen/seed/
|
|
# 或
|
|
make loadgen-build
|
|
```
|
|
|
|
---
|
|
|
|
## 在 prod 上跑 (凌晨 T0 = 02:00,推荐)
|
|
|
|
```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 (插入 23k 行测试数据,直接写入 docker 里的 `topfans-postgres`)
|
|
3. 自动重置 PG 序列 (CLAUDE.md 规范)
|
|
4. 写 `users.csv` (含 1000 个 JWT)
|
|
|
|
**预计耗时**:30-60 秒
|
|
|
|
> ⚠️ **为什么必须在生产机上跑**:`seed` 默认值是 `localhost:5432/topfans`,
|
|
> 这正好等于生产机上 docker 暴露的 `5432:5432` + `POSTGRES_DB=topfans`。
|
|
> 从本地 Mac 跑默认值会**连到你本机的 Postgres**(可能根本不是 topfans 库),
|
|
> 后续 `users.csv` 拿去打生产网关会全部 401。详见下一节。
|
|
|
|
---
|
|
|
|
## 从本地 Mac 连到生产 DB (不 ssh 进生产机)
|
|
|
|
只适合本地紧急补 seed、或者压测脚本调试。**首选仍是上一节 ssh 进生产机**。
|
|
|
|
### 方式 A:SSH 端口转发 (推荐,改动最小)
|
|
|
|
```bash
|
|
# 1. 建立转发:本地 25432 → 生产机 docker 5432
|
|
ssh -L 25432:127.0.0.1:5432 -N -f root@101.132.250.62
|
|
|
|
# 2. 跑 seed (从 docker/.env.prod 拿密码)
|
|
cd /Users/liulujian/Documents/code/TopFansByGithub
|
|
DB_PASSWORD=$(grep '^DB_PASSWORD=' docker/.env.prod | cut -d= -f2) \
|
|
JWT_SECRET=$(grep '^JWT_SECRET=' docker/.env.prod | cut -d= -f2) \
|
|
go run ./backend/scripts/loadgen/seed \
|
|
--db-host=127.0.0.1 \
|
|
--db-port=25432 \
|
|
--db-name=topfans \
|
|
--db-user=postgres
|
|
|
|
# 3. 用完记得关转发
|
|
pkill -f 'ssh -L 25432'
|
|
```
|
|
|
|
### 方式 B:从本地直连 docker 容器 (生产机已开 5432 端口时)
|
|
|
|
如果生产机 `topfans-postgres` 已经通过 `ports: - "5432:5432"` 暴露到外网,
|
|
也可以直接走公网,但**强烈不建议** (生产 PG 暴露公网本身就有风险):
|
|
|
|
```bash
|
|
DB_PASSWORD=$(grep '^DB_PASSWORD=' docker/.env.prod | cut -d= -f2) \
|
|
JWT_SECRET=$(grep '^JWT_SECRET=' docker/.env.prod | cut -d= -f2) \
|
|
go run ./backend/scripts/loadgen/seed \
|
|
--db-host=101.132.250.62 \
|
|
--db-port=5432 \
|
|
--db-name=topfans \
|
|
--db-user=postgres
|
|
```
|
|
|
|
---
|
|
|
|
## 在本地 docker 跑 (开发联调)
|
|
|
|
```bash
|
|
cd backend/scripts/loadgen/seed
|
|
|
|
# 1. 生成 bcrypt 哈希 (与 tokens.go 硬编码的 "Test@123" 匹配)
|
|
python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \
|
|
> loadtest_bcrypt.txt
|
|
|
|
# 2. 跑 seed (假设本地 docker postgres 在 15432)
|
|
cd /Users/liulujian/Documents/code/TopFansByGithub/backend
|
|
DB_PASSWORD=123456 \
|
|
JWT_SECRET=topfans-secret-key-local-dev-only \
|
|
./bin/seed \
|
|
--db-name=top-fans \
|
|
--db-host=localhost \
|
|
--db-port=15432 \
|
|
--db-user=postgres
|
|
```
|
|
|
|
**注意**: `loadtest_bcrypt.txt` 必须在 seed 二进制运行的**当前目录**(代码用相对路径读)。
|
|
|
|
---
|
|
|
|
## 命令行参数
|
|
|
|
```
|
|
./bin/seed --help
|
|
|
|
Usage of ./bin/seed:
|
|
-cleanup # 跑清理 (默认保留 1000 users)
|
|
-cleanup-star-id int # 要清的 star_id (默认 999900, 防止误删)
|
|
-full # 配合 -cleanup: 也删用户和 stars
|
|
-reset # 删旧数据再 seed (隐含 --cleanup 行为)
|
|
-reset-tokens # 只重签 JWT (数据保留)
|
|
-jwt-secret string # JWT 密钥 (默认 $JWT_SECRET)
|
|
-db-host string # PG host (默认 localhost)
|
|
-db-port int # PG port (默认 5432)
|
|
-db-name string # PG 数据库 (默认 topfans,即 prod 默认;
|
|
# 本地 docker dev 用 top-fans,需显式 --db-name=top-fans)
|
|
-db-user string # PG user (默认 postgres)
|
|
-db-password string # PG 密码 (默认 $DB_PASSWORD)
|
|
```
|
|
|
|
### 三种部署对应的 DB 参数速查表
|
|
|
|
| 部署 | host | port | dbname | user | password |
|
|
|------|------|------|--------|------|----------|
|
|
| **生产机直跑** (ssh 进去) | `localhost` | `5432` | `topfans` | `postgres` | `$DB_PASSWORD` (从 `docker/.env.prod` 读) |
|
|
| **本地连生产** (SSH 端口转发) | `127.0.0.1` | `25432` | `topfans` | `postgres` | `$DB_PASSWORD` (从 `docker/.env.prod` 读) |
|
|
| **本地 docker 联调** (宿主机 postgresql-database-1) | `localhost` | `15432` | `top-fans` | `postgres` | `123456` (从 `docker/.env.local` 读) |
|
|
|
|
> **默认值设计意图**:`localhost:5432/topfans` 是给"在生产机上直接跑"设计的,
|
|
> 因为生产 docker 把 5432 暴露到宿主机 5432。从本地 Mac 跑时**必须显式覆盖
|
|
> `--db-host` / `--db-port` / `--db-name`**,否则会连到本机 Postgres。
|
|
|
|
---
|
|
|
|
## 三种"清理"模式对比
|
|
|
|
| 命令 | 删 stars | 删 users | 删 assets/exhibits | 用途 |
|
|
|------|---------|---------|-------------------|------|
|
|
| `./seed --cleanup` | ❌ | ❌ | ✅ | 压完一轮,清理资产但保留账号 |
|
|
| `./seed --cleanup --full` | ✅ | ✅ | ✅ | 全部清,下次重新 seed |
|
|
| `./seed --reset` | ❌ | ❌ | ✅ | 等同 `--cleanup`(保留用户) |
|
|
| `./seed --reset-tokens` | ❌ | ❌ | ❌ | 只重新签 JWT,数据不动 |
|
|
|
|
**典型流程**:
|
|
```bash
|
|
# 第 1 轮压测 (02:00-03:00)
|
|
./seed # 灌数据
|
|
./loadgen --cmd=run --scenarios=S1,S2,S4 # 压测
|
|
./seed --cleanup # 压完清理资产
|
|
|
|
# 第 2 轮压测 (下周,JWT 过期了)
|
|
./seed --reset-tokens --jwt-secret=$JWT_SECRET # 只重签 JWT
|
|
./loadgen --cmd=run --scenarios=S1,S2,S4 # 复测
|
|
|
|
# 完全重来 (例如改了用户模型)
|
|
./seed --cleanup --full # 全删
|
|
./seed # 重新灌
|
|
```
|
|
|
|
---
|
|
|
|
## 数据规模
|
|
|
|
| 表 | 行数 | 备注 |
|
|
|----|------|------|
|
|
| `stars` | +1 | star_id=999900 |
|
|
| `users` | +1000 | mobile 19900000001 ~ 19900001000 |
|
|
| `fan_profiles` | +1000 | 每个 user 一个 |
|
|
| `crystal_transaction_records` | +1000+ | 初始水晶 |
|
|
| `assets` | +5000 | 每个 user ~5 个 |
|
|
| `booth_slots` | +3000 | |
|
|
| `exhibitions` | +2000 | |
|
|
| `friendships` | +10000 | |
|
|
| **TOTAL** | **~23k 行** | |
|
|
|
|
---
|
|
|
|
## 关键设计
|
|
|
|
### 1. star_id 隔离
|
|
所有测试数据用 `star_id = 999900`,**不影响**真实业务 (87, 88, 91, 93, 94, 95)。
|
|
|
|
### 2. PG max_connections = 50
|
|
prod 已将 `POSTGRES_MAX_CONNECTIONS` 从 100 调到 50,避免被测试数据耗尽连接池。
|
|
|
|
### 3. CLAUDE.md 序列重置
|
|
seed 末尾自动 `setval()` 所有相关表的 sequence,避免后续 GORM 插入报 duplicate key。
|
|
|
|
### 4. JWT 7 天过期
|
|
跨周第二轮压测前需 `--reset-tokens` 重签。
|
|
|
|
### 5. bcrypt 哈希与密码硬编码
|
|
- `tokens.go` 硬编码密码为 `"Test@123"`(写到 users.csv 的 password 列)
|
|
- `loadtest_bcrypt.txt` 是这个密码的 bcrypt(cost=10) 哈希
|
|
- 二者必须匹配,否则 login 会报 500
|
|
|
|
---
|
|
|
|
## 常见问题
|
|
|
|
### Q: 跑完 seed 但 login 报"密码错误"?
|
|
A: `loadtest_bcrypt.txt` 没匹配上 `Test@123`。
|
|
```bash
|
|
python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \
|
|
> loadtest_bcrypt.txt
|
|
./seed --cleanup --full && ./seed
|
|
```
|
|
|
|
### Q: 想换密码怎么办?
|
|
A: 同时改两个地方:
|
|
1. `tokens.go` 的 `u.Mobile, "Test@123"` → 你的密码
|
|
2. `loadtest_bcrypt.txt` 重新生成
|
|
|
|
### Q: "loadtest_bcrypt.txt: no such file or directory"?
|
|
A: seed 用相对路径读这个文件,必须在 seed 目录跑(或者把文件 cp 到当前目录)。
|
|
|
|
### Q: --reset 没生效,users 还是旧的?
|
|
A: 因为 `--reset` 等同 `--cleanup`(保留用户)。要删用户用 `--cleanup --full`。
|
|
|
|
---
|
|
|
|
## 单元测试
|
|
|
|
```bash
|
|
cd backend
|
|
go test ./scripts/loadgen/seed/ -v
|
|
```
|
|
|
|
5 个测试:
|
|
- `TestMobileNumbering`: mobile 编号正确性
|
|
- `TestSequenceMapping`: loadtestSeqs 映射
|
|
- `TestPKColumnMapping`: pkColumns 映射(关键 stars/star_id, booth_slots/slot_id)
|
|
- `TestCleanupRejectsInvalidStarID`: cleanup 拒绝非 loadtest star_id
|
|
- `TestJoinInt64`: CSV 序列化辅助函数
|
|
|
|
**测试状态**: 5/5 PASS
|