feat:修改压缩工具的配置

This commit is contained in:
zerosaturation 2026-06-16 23:00:07 +08:00
parent c6ce7d2d25
commit 8ccb4e2565
7 changed files with 591 additions and 5 deletions

View File

@ -1,7 +1,7 @@
# TopFans Backend Makefile
# 用于简化开发流程
.PHONY: help install-swagger gen-swagger update-swagger start-swagger start-all stop-all clean build run all loadgen-build loadgen-test loadgen-vet loadgen-ci loadgen-seed-local loadgen-seed-prod-tunnel loadgen-cleanup-local loadgen-cleanup-prod-tunnel loadgen-cleanup-prod-tunnel-full loadgen-report
.PHONY: help install-swagger gen-swagger update-swagger start-swagger start-all stop-all clean build run all loadgen-build loadgen-test loadgen-vet loadgen-ci loadgen-seed-local loadgen-seed-prod-tunnel loadgen-cleanup-local loadgen-cleanup-local-full loadgen-cleanup-prod-tunnel loadgen-cleanup-prod-tunnel-full loadgen-report
# 默认目标
help:
@ -32,6 +32,7 @@ help:
@echo " make loadgen-seed-local - seed 写入本地 docker dev (top-fans:15432)"
@echo " make loadgen-seed-prod-tunnel - seed 通过 SSH 端口转发写生产 (127.0.0.1:25432)"
@echo " make loadgen-cleanup-local - 清理本地 docker 压测数据 (保留账号)"
@echo " make loadgen-cleanup-local-full - 全清本地 docker 压测数据 (含 users + stars)"
@echo " make loadgen-cleanup-prod-tunnel - 清理生产 docker 压测数据 (保留账号,走 SSH 转发)"
@echo " make loadgen-cleanup-prod-tunnel-full - 全清生产压测数据 (含 users + stars,适合彻底收尾)"
@echo ""
@ -58,6 +59,7 @@ help:
@echo " make loadgen-seed-local - seed 写入本地 docker dev (top-fans:15432)"
@echo " make loadgen-seed-prod-tunnel - seed 通过 SSH 端口转发写生产 (127.0.0.1:25432)"
@echo " make loadgen-cleanup-local - 清理本地 docker 压测数据 (保留账号)"
@echo " make loadgen-cleanup-local-full - 全清本地 docker 压测数据 (含 users + stars)"
@echo " make loadgen-cleanup-prod-tunnel - 清理生产 docker 压测数据 (保留账号,走 SSH 转发)"
@echo " make loadgen-cleanup-prod-tunnel-full - 全清生产压测数据 (含 users + stars,适合彻底收尾)"
@echo ""
@ -162,11 +164,18 @@ loadgen-seed-local: loadgen-build
--db-host=localhost --db-port=15432 --db-name=top-fans --db-user=postgres
loadgen-cleanup-local: loadgen-build
@echo ">>> 清理本地 docker 压测数据 (top-fans:15432)"
@echo ">>> 清理本地 docker 压测数据 (top-fans:15432,保留账号)"
@DB_PASSWORD=$$(grep '^DB_PASSWORD=' ../docker/.env.local | cut -d= -f2) \
./bin/seed --cleanup \
--db-host=localhost --db-port=15432 --db-name=top-fans --db-user=postgres
# 全清版本:含 users + stars。跟 loadgen-cleanup-prod-tunnel-full 对称。
loadgen-cleanup-local-full: loadgen-build
@echo ">>> ⚠️ 全清本地 docker 压测数据 (top-fans:15432,含 users + stars)"
@DB_PASSWORD=$$(grep '^DB_PASSWORD=' ../docker/.env.local | cut -d= -f2) \
./bin/seed --cleanup --full \
--db-host=localhost --db-port=15432 --db-name=top-fans --db-user=postgres
# --- 本地连生产 (ssh -L 25432 → 生产 docker 5432) ---
# 调用前请确保已建立转发: ssh -L 25432:127.0.0.1:5432 -N -f root@101.132.250.62
loadgen-seed-prod-tunnel: loadgen-build

View File

@ -54,6 +54,23 @@ loadgen/
---
## ⭐ 推荐入口:prod_loadtest.sh 一键脚本
> **如果你的目标是打生产压测(无论 prod 还是本地 docker),优先用 [`scripts/prod_loadtest.sh`](scripts/prod_loadtest.sh)**
> 子命令模式覆盖了全部流程,凭据从 `docker/.env.prod` 读,避免明文泄露。
```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)。
---
## 🚀 5 分钟入门 (本地 docker)
```bash

View File

@ -86,13 +86,29 @@ scp loadtest_bcrypt.txt root@101.132.250.62:/opt/topfans/loadtest/
## 2. 数据准备 (T0 = 02:00)
### 2.1 SSH 到 prod
### 2.0 ⭐ 推荐:本地一键压测 (无需 SSH 到生产机)
> **新方式**:`scripts/prod_loadtest.sh` 一站式脚本,在本地跑,自动建 SSH 隧道/读凭据/调 seed+loadgen+cleanup。
> 比"SSH 进去跑 prod_seed.sh"更安全、更可复现、更适合 CI。
```bash
ssh root@101.132.250.62
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
```
### 2.2 一键跑 seed (生产数据灌入)
子命令、状态查询、故障排查:见 [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
```

View File

@ -0,0 +1,169 @@
# scripts/ — 部署到生产机的辅助脚本
> 这些脚本不需要在生产机上跑。`prod_loadtest.sh` 是一站式包装,
> 把所有 SSH 隧道、seed、loadgen、preflight、report、cleanup 操作
> 串成子命令,本地直接调,目标始终是生产 DB / 生产网关。
---
## 📜 脚本清单
| 脚本 | 用途 | 谁要跑 |
|------|------|--------|
| **[prod_loadtest.sh](prod_loadtest.sh)** | 一站式压测:子命令模式 (`up` / `seed` / `loadgen` / `report` / `cleanup-all` / `pipeline` 等) | on-call、想跑压测的工程师 |
| [prod_seed.sh](prod_seed.sh) | ⚠️ 旧脚本:只能在生产机上跑 (SSH 进去后 `bash prod_seed.sh`) | 不推荐使用,统一改用 prod_loadtest.sh |
| [mint_reset.sh](mint_reset.sh) | mint 数据重置(每跑完一个 S4 stage 调一次) | loadgen S4 场景内部调用,不用手跑 |
---
## 🚀 快速开始 (prod_loadtest.sh)
### 一键完成整个压测流程
```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
```
### 细粒度控制(推荐)
```bash
SCRIPT=./backend/scripts/loadgen/scripts/prod_loadtest.sh
# 1. 建 SSH 隧道
$SCRIPT up
# 2. 备份生产 DB
$SCRIPT backup
# 3. 灌 1000 测试用户 + 23k 行数据
$SCRIPT seed
# 4. 开压前 7 项检查
$SCRIPT preflight
# 5. 跑压测 (所有 loadgen 参数透传)
$SCRIPT loadgen \
--scenarios=S1,S2,S4 \
--stage=step --step-schedule='5,10,20' \
--duration=60s
# 6. 生成 final-report.md
$SCRIPT report
# 7. 全清压测数据 (含 users + stars,序列同步)
$SCRIPT cleanup-all
# 8. 关隧道
$SCRIPT down
```
### 查看状态(任何时候)
```bash
$SCRIPT status
# == SSH 隧道 ==
# ✅ 本地 25432 端口转发在跑 (PID: 12345)
# == 生产 DB 容器 ==
# topfans-postgres Up 22 hours (healthy) 0.0.0.0:5432->5432/tcp
# == 最近 3 次备份 ==
# pre-loadtest-20260616-2151.sql 1.2M
# == 本地 reports/ ==
# S1.json / S2.json / S4.json / final-report.md
```
---
## 📋 子命令速查
| 子命令 | 说明 | 副作用 |
|--------|------|--------|
| `up` | 建 SSH 隧道 `本地 25432 → 生产 5432` | 启动 `ssh -f -N -L 25432:...` |
| `down` | 关 SSH 隧道 | 杀掉 ssh 进程 |
| `status` | 隧道/容器/备份/reports 状态 | 只读 |
| `backup` | 备份生产 DB → `/opt/topfans/backups/pre-loadtest-<ts>.sql` | 写生产机磁盘 |
| `seed` | 灌 1000 users + 23k 行 (通过隧道写生产 DB) | 写生产 DB |
| `preflight` | 7 项开压前检查 | 只读 |
| `loadgen [args]` | 跑压测,所有参数透传给 `./bin/loadgen --cmd=run` | 打生产网关 + 写 reports/ |
| `report` | 把 reports/ 下的 S*.json 合成 final-report.md | 写 reports/final-report.md |
| `cleanup` | 清理压测数据(**保留** users + stars) | 写生产 DB(删 rows) |
| `cleanup-all` | 全清(**含** users + stars,序列同步重置) | 写生产 DB(删 rows + setval) |
| `pipeline [args]` | `up → backup → seed → preflight → loadgen → report → cleanup-all → down` | 一条龙,loadgen args 透传 |
---
## 🔧 环境变量(可选覆盖)
```bash
PROD_HOST=root@101.132.250.62 # 默认
TUNNEL_PORT=25432 # 默认
BACKEND_DIR=... # 默认脚本所在目录的 ../../..
```
例:测不同端口:
```bash
TUNNEL_PORT=35432 $SCRIPT up
TUNNEL_PORT=35432 $SCRIPT seed
```
---
## 🛡️ 设计要点
### 1. SSH 端口转发为啥必要?
- 生产机 `topfans-postgres` 容器是 PostgreSQL **18.3**
- 本地 `pg_dump`**17.4** → 版本不兼容
- 解决:seed/cleanup 走 `docker exec topfans-postgres pg_dump` 容器内跑 (版本匹配)
- `prod_loadtest.sh` 把这个细节封装在 `seed` / `cleanup` / `cleanup-all`
### 2. 凭据从哪来?
- 所有子命令从 `docker/.env.prod``DB_PASSWORD` / `JWT_SECRET`
- 不在命令行明文泄露
- 调用方看不到 .env.prod 文件(只要 prod_loadtest.sh 内部读)
### 3. 数据安全
- `--cleanup`(默认):保留 1000 users,只清资产(支持"多轮压测")
- `--cleanup-all`:**含** users + stars(适合"压测彻底收尾")
- 序列同步:cleanup 末尾会 `setval()` 所有相关表的 sequence(避免后续 GORM 报 duplicate key)
### 4. 为什么不用 prod_seed.sh?
- `prod_seed.sh` 需要 SSH 到生产机跑,**有歧义**(以为是 prod-only 工具)
- `prod_loadtest.sh` 全部在本地,**语义清晰**(本地调,目标生产)
- 保留 `prod_seed.sh` 是为了向后兼容(已经在生产机上跑过的人),但 README/工具集都优先推荐 `prod_loadtest.sh`
---
## 🔍 故障排查
### "本地 25432 端口未监听"
`$SCRIPT up` 建隧道。
### "DB_PASSWORD 未找到"
检查 `docker/.env.prod` 是否存在并有 `DB_PASSWORD=` 行。
### preflight ③ backup 报 < 50MB
生产库本身小(几十 MB),RUNBOOK 红线是历史经验值,不是阻断错误。
### loadgen 报 `circuit breaker tripped!`
`loadgen-*.log``🚨 circuit breaker tripped!` 上下文:
- 如果错误率/p99 正常,可能是 `metrics-feed.jsonl` 缺失导致误 trip(已修复:`latestServer` 改 nil 判断)
- 如果是真有错,看报告里 `S*.json``errors` 字段
### SSH 隧道被挂起或断
```bash
$SCRIPT down
$SCRIPT up
```
---
## 📂 相关文件
- 上层: [../README.md](../README.md) | [../RUNBOOK.md](../RUNBOOK.md) | [../REPORT_GUIDE.md](../REPORT_GUIDE.md)
- seed 工具: [../seed/README.md](../seed/README.md)
- reports 输出: [../../reports/](../../reports/) (gitignore)

View File

@ -0,0 +1,346 @@
#!/bin/bash
# ===================================================================
# prod_loadtest.sh — 本地一键压测生产环境
# ===================================================================
# 设计目的: 不用 SSH 上生产机,本地直接调 seed/loadgen/cleanup
# 打生产 DB(走 SSH 端口转发)和生产网关(走公网)
#
# 架构:
# 1. SSH 端口转发: 本地 25432 → 生产机 docker 5432
# (绕开 host pg_dump 17.4 vs 容器 PG 18.3 版本不匹配问题)
# 2. seed/loadgen/cleanup: 直接在本地跑,通过环境变量/flag 传生产凭据
#
# 用法:
# ./prod_loadtest.sh <command> [args...]
#
# Commands:
# up 建立 SSH 端口转发 (本地 25432 → 生产 5432)
# down 关闭 SSH 端口转发
# status 查看隧道/DB/容器状态
# backup 备份生产 DB 到 /opt/topfans/backups/
# preflight 跑 7 项 preflight 检查
# seed 灌 1000 测试用户 + 23k 行数据
# loadgen [args...] 跑压测,所有参数透传给 ./bin/loadgen
# 例: ./prod_loadtest.sh loadgen --scenarios=S1,S2,S4 \
# --stage=step --step-schedule='5,10,20' \
# --duration=60s
# report 把 reports/ 下的 S*.json 合成 final-report.md
# cleanup 清理压测数据(保留 users + stars,适合多轮压测)
# cleanup-all 全清(含 users + stars,适合彻底收尾)
# pipeline [args...] 一键: up → backup → seed → preflight → loadgen
# → report → cleanup-all → down(loadgen args 透传)
# help 显示本帮助
#
# 环境变量(可覆盖):
# PROD_HOST 生产 SSH 地址 (默认 root@101.132.250.62)
# TUNNEL_PORT 本地转发端口 (默认 25432)
# BACKEND_DIR backend 绝对路径 (默认脚本所在目录的 ../../.. 即 backend/)
#
# 典型工作流(交互):
# ./prod_loadtest.sh up
# ./prod_loadtest.sh backup
# ./prod_loadtest.sh seed
# ./prod_loadtest.sh preflight
# ./prod_loadtest.sh loadgen --scenarios=S1,S2,S4 --stage=step \
# --step-schedule='5,10,20' --duration=60s
# ./prod_loadtest.sh report
# ./prod_loadtest.sh cleanup-all
# ./prod_loadtest.sh down
#
# 一键完成:
# ./prod_loadtest.sh pipeline --scenarios=S1,S2,S4 --stage=step \
# --step-schedule='5,10,20' --duration=60s
# ===================================================================
set -euo pipefail
# ===== 路径与默认值 =====
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="${BACKEND_DIR:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
REPO_ROOT="$(cd "$BACKEND_DIR/.." && pwd)"
PROD_HOST="${PROD_HOST:-root@101.132.250.62}"
TUNNEL_PORT="${TUNNEL_PORT:-25432}"
PROD_DB_NAME="topfans"
PROD_DB_USER="postgres"
ENV_PROD="$REPO_ROOT/docker/.env.prod"
# ===== 颜色输出 =====
if [[ -t 1 ]]; then
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[0;33m'
BLUE=$'\033[0;34m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
else
RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; RESET=""
fi
info() { echo "${BLUE}>>>${RESET} $*"; }
ok() { echo "${GREEN}${RESET} $*"; }
warn() { echo "${YELLOW}⚠️${RESET} $*"; }
err() { echo "${RED}${RESET} $*" >&2; }
fatal() { err "$*"; exit 1; }
# ===== 通用工具 =====
read_env() {
# 读 docker/.env.prod 里的 key,值通过 stdout 返回
local key="$1"
if [[ ! -f "$ENV_PROD" ]]; then
fatal "$ENV_PROD 不存在"
fi
grep "^${key}=" "$ENV_PROD" | cut -d= -f2
}
ensure_built() {
# 跑命令前确保 bin/seed bin/loadgen 已编译
if [[ ! -x "$BACKEND_DIR/bin/seed" || ! -x "$BACKEND_DIR/bin/loadgen" ]]; then
info "编译 seed + loadgen → $BACKEND_DIR/bin/"
(cd "$BACKEND_DIR" && make loadgen-build >/dev/null)
fi
}
require_tunnel() {
if ! lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
fatal "本地 $TUNNEL_PORT 端口未监听,先跑: $0 up"
fi
}
# ===== 子命令:up =====
cmd_up() {
if lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
ok "隧道已存在 (PID: $(lsof -t -iTCP:$TUNNEL_PORT -sTCP:LISTEN), port $TUNNEL_PORT)"
return 0
fi
info "建立 SSH 隧道: 本地 $TUNNEL_PORT$PROD_HOST:5432"
ssh -o StrictHostKeyChecking=no \
-o ServerAliveInterval=30 \
-L "$TUNNEL_PORT":127.0.0.1:5432 \
-N -f "$PROD_HOST"
sleep 1
if lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
ok "隧道已建立 (PID: $(lsof -t -iTCP:$TUNNEL_PORT -sTCP:LISTEN))"
else
fatal "隧道建立失败"
fi
}
# ===== 子命令:down =====
cmd_down() {
if ! lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
ok "隧道已关闭 (或没建过)"
return 0
fi
local pid
pid=$(lsof -t -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN)
kill "$pid" 2>/dev/null || true
sleep 1
if lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
warn "普通 kill 没关掉,试 kill -9"
kill -9 "$pid" 2>/dev/null || true
fi
ok "隧道已关闭 (PID: $pid)"
}
# ===== 子命令:status =====
cmd_status() {
echo "${BOLD}== SSH 隧道 ==${RESET}"
if lsof -iTCP:"$TUNNEL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
ok "本地 $TUNNEL_PORT 端口转发在跑 (PID: $(lsof -t -iTCP:$TUNNEL_PORT -sTCP:LISTEN))"
else
warn "本地 $TUNNEL_PORT 端口转发没建"
fi
echo ""
echo "${BOLD}== 生产 DB 容器 ==${RESET}"
ssh "$PROD_HOST" "docker ps --filter name=topfans-postgres --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
echo ""
echo "${BOLD}== 最近 3 次备份 ==${RESET}"
ssh "$PROD_HOST" "ls -lh /opt/topfans/backups/ 2>/dev/null | tail -3" || warn "无备份或 backups 目录不存在"
echo ""
echo "${BOLD}== 本地 reports/ ==${RESET}"
ls -lh "$BACKEND_DIR/reports/"*.md "$BACKEND_DIR/reports/"*.json 2>/dev/null | tail -5 || warn "无 reports"
}
# ===== 子命令:backup =====
cmd_backup() {
local ts
ts=$(date +%Y%m%d-%H%M)
local backup_path="/opt/topfans/backups/pre-loadtest-${ts}.sql"
info "备份生产 DB → $backup_path"
local db_pass
db_pass=$(read_env DB_PASSWORD)
ssh "$PROD_HOST" "DB_PASSWORD=\$(grep '^DB_PASSWORD=' /opt/topfans/docker/.env.prod | cut -d= -f2)
mkdir -p /opt/topfans/backups
docker exec -e PGPASSWORD=\"\$DB_PASSWORD\" topfans-postgres \
pg_dump -U postgres -d topfans -Fc > $backup_path
ls -lh $backup_path"
ok "备份完成"
}
# ===== 子命令:seed =====
cmd_seed() {
require_tunnel
ensure_built
# bcrypt 文件(seed 用)
local bcrypt_file="$BACKEND_DIR/loadtest_bcrypt.txt"
if [[ ! -f "$bcrypt_file" ]]; then
info "生成 $bcrypt_file (匹配 tokens.go 硬编码密码 'Test@123')"
python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" > "$bcrypt_file"
fi
info "seed: 1000 users + 23k 行 (写生产 DB 走 SSH 隧道)"
local db_pass
db_pass=$(read_env DB_PASSWORD)
(
cd "$BACKEND_DIR"
DB_PASSWORD="$db_pass" \
JWT_SECRET=$(read_env JWT_SECRET) \
./bin/seed \
--db-host=127.0.0.1 --db-port="$TUNNEL_PORT" \
--db-name="$PROD_DB_NAME" --db-user="$PROD_DB_USER" \
--db-password="$db_pass"
)
ok "seed 完成"
}
# ===== 子命令:preflight =====
cmd_preflight() {
ensure_built
local db_pass
db_pass=$(read_env DB_PASSWORD)
info "跑 preflight (7 项检查)"
(
cd "$BACKEND_DIR"
DB_PASSWORD="$db_pass" \
JWT_SECRET=$(read_env JWT_SECRET) \
./bin/loadgen --cmd=preflight \
--target=http://101.132.250.62:8080 \
--prod-ssh="$PROD_HOST"
)
}
# ===== 子命令:loadgen =====
cmd_loadgen() {
require_tunnel
ensure_built
if [[ $# -eq 0 ]]; then
fatal "loadgen 需要参数,例: $0 loadgen --scenarios=S1,S2,S4 --stage=step"
fi
local db_pass
db_pass=$(read_env DB_PASSWORD)
info "loadgen ${*}"
(
cd "$BACKEND_DIR"
DB_PASSWORD="$db_pass" \
JWT_SECRET=$(read_env JWT_SECRET) \
./bin/loadgen --cmd=run \
--target=http://101.132.250.62:8080 \
--prod-ssh="$PROD_HOST" \
"$@"
)
}
# ===== 子命令:report =====
cmd_report() {
ensure_built
info "生成 final-report.md"
(
cd "$BACKEND_DIR"
./bin/loadgen --cmd=report \
--input=./reports \
--output=./reports/final-report.md
)
ok "final-report.md 已更新: $BACKEND_DIR/reports/final-report.md"
}
# ===== 子命令:cleanup =====
cmd_cleanup() {
require_tunnel
ensure_built
local db_pass
db_pass=$(read_env DB_PASSWORD)
warn "清理压测数据(保留 users + stars,适合多轮压测)"
(
cd "$BACKEND_DIR"
DB_PASSWORD="$db_pass" \
./bin/seed --cleanup \
--db-host=127.0.0.1 --db-port="$TUNNEL_PORT" \
--db-name="$PROD_DB_NAME" --db-user="$PROD_DB_USER" \
--db-password="$db_pass"
)
ok "cleanup 完成"
}
# ===== 子命令:cleanup-all =====
cmd_cleanup_all() {
require_tunnel
ensure_built
local db_pass
db_pass=$(read_env DB_PASSWORD)
warn "⚠️ 全清压测数据(含 users + stars,序列同步重置)"
(
cd "$BACKEND_DIR"
DB_PASSWORD="$db_pass" \
./bin/seed --cleanup --full \
--db-host=127.0.0.1 --db-port="$TUNNEL_PORT" \
--db-name="$PROD_DB_NAME" --db-user="$PROD_DB_USER" \
--db-password="$db_pass"
)
ok "cleanup-all 完成"
}
# ===== 子命令:pipeline =====
cmd_pipeline() {
info "=== pipeline: 1/8 up ==="
cmd_up
info "=== pipeline: 2/8 backup ==="
cmd_backup
info "=== pipeline: 3/8 seed ==="
cmd_seed
info "=== pipeline: 4/8 preflight ==="
cmd_preflight || warn "preflight 有 FAIL,继续 (③ backup <50MB 预期会 FAIL)"
info "=== pipeline: 5/8 loadgen ==="
cmd_loadgen "$@" || warn "loadgen 出错,继续到 cleanup"
info "=== pipeline: 6/8 report ==="
cmd_report || warn "report 生成失败,继续"
info "=== pipeline: 7/8 cleanup-all ==="
cmd_cleanup_all || warn "cleanup 失败"
info "=== pipeline: 8/8 down ==="
cmd_down
ok "pipeline 完成"
}
# ===== 帮助 =====
cmd_help() {
sed -n '3,50p' "$0"
}
# ===== 入口 =====
if [[ $# -eq 0 ]]; then
cmd_help
exit 1
fi
cmd="${1:-help}"
shift || true
case "$cmd" in
up) cmd_up ;;
down) cmd_down ;;
status) cmd_status ;;
backup) cmd_backup ;;
seed) cmd_seed "$@" ;;
preflight) cmd_preflight ;;
loadgen) cmd_loadgen "$@" ;;
report) cmd_report ;;
cleanup) cmd_cleanup ;;
cleanup-all) cmd_cleanup_all ;;
pipeline) cmd_pipeline "$@" ;;
help|-h|--help) cmd_help ;;
*) err "未知子命令: $cmd"; cmd_help; exit 2 ;;
esac

View File

@ -4,6 +4,22 @@
---
## ⭐ 推荐入口: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)。

View File

@ -20,6 +20,14 @@ func Cleanup(db *sql.DB, starID int64, full bool) error {
"DELETE FROM friendships WHERE star_id = $1",
"DELETE FROM assets WHERE star_id = $1",
"DELETE FROM fan_profiles WHERE star_id = $1",
// 通知系统 (2026-06-16 加的) — 压测期间 user 关注 loadtest star
// 触发了 1000 条 notification + 1000 条 notification_stats。
// 用 OR 同时覆盖 user_id 和 star_id 两条线索,保证清干净。
// 必须在 fan_profiles 删完之后跑(避免 fan_profiles FK 锁)。
// 注意: lib/pq 的 $1 占位符是按"unique 编号"算的,不能重复 $1 —
// 重复 $1 会让 pq 期望 1 个参数,我们传 2 个就报 got 2/requires 1。
"DELETE FROM notifications WHERE user_id IN (SELECT id FROM users WHERE id >= $1) OR star_id = $2",
"DELETE FROM notification_stats WHERE user_id IN (SELECT id FROM users WHERE id >= $1) OR star_id = $2",
}
if full {
queries = append(queries,
@ -37,6 +45,11 @@ func Cleanup(db *sql.DB, starID int64, full bool) error {
switch q {
case "DELETE FROM users WHERE id >= $1":
_, err = db.Exec(q, LoadtestUserMin)
case "DELETE FROM notifications WHERE user_id IN (SELECT id FROM users WHERE id >= $1) OR star_id = $2",
"DELETE FROM notification_stats WHERE user_id IN (SELECT id FROM users WHERE id >= $1) OR star_id = $2":
// notifications / notification_stats 用 LoadtestUserMin 删 user 维度,
// 用 starID 删 star 维度。$1 / $2 顺序必须跟 SQL 出现顺序一致。
_, err = db.Exec(q, LoadtestUserMin, starID)
default:
_, err = db.Exec(q, starID)
}