From 8ccb4e2565fdfa5e9453cb140ab112f707db9ded Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Tue, 16 Jun 2026 23:00:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9A=84=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Makefile | 13 +- backend/scripts/loadgen/README.md | 17 + backend/scripts/loadgen/RUNBOOK.md | 22 +- backend/scripts/loadgen/scripts/README.md | 169 +++++++++ .../scripts/loadgen/scripts/prod_loadtest.sh | 346 ++++++++++++++++++ backend/scripts/loadgen/seed/README.md | 16 + backend/scripts/loadgen/seed/cleanup.go | 13 + 7 files changed, 591 insertions(+), 5 deletions(-) create mode 100644 backend/scripts/loadgen/scripts/README.md create mode 100755 backend/scripts/loadgen/scripts/prod_loadtest.sh diff --git a/backend/Makefile b/backend/Makefile index 10bf577..14a1593 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -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 diff --git a/backend/scripts/loadgen/README.md b/backend/scripts/loadgen/README.md index f3f4150..df0a7e8 100644 --- a/backend/scripts/loadgen/README.md +++ b/backend/scripts/loadgen/README.md @@ -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 diff --git a/backend/scripts/loadgen/RUNBOOK.md b/backend/scripts/loadgen/RUNBOOK.md index ac222e4..081493f 100644 --- a/backend/scripts/loadgen/RUNBOOK.md +++ b/backend/scripts/loadgen/RUNBOOK.md @@ -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 ``` diff --git a/backend/scripts/loadgen/scripts/README.md b/backend/scripts/loadgen/scripts/README.md new file mode 100644 index 0000000..7bfbfa2 --- /dev/null +++ b/backend/scripts/loadgen/scripts/README.md @@ -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-.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) diff --git a/backend/scripts/loadgen/scripts/prod_loadtest.sh b/backend/scripts/loadgen/scripts/prod_loadtest.sh new file mode 100755 index 0000000..d31ce0b --- /dev/null +++ b/backend/scripts/loadgen/scripts/prod_loadtest.sh @@ -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 [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 diff --git a/backend/scripts/loadgen/seed/README.md b/backend/scripts/loadgen/seed/README.md index c3fd46c..c84e113 100644 --- a/backend/scripts/loadgen/seed/README.md +++ b/backend/scripts/loadgen/seed/README.md @@ -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)。 diff --git a/backend/scripts/loadgen/seed/cleanup.go b/backend/scripts/loadgen/seed/cleanup.go index ae97a7e..670e3a1 100644 --- a/backend/scripts/loadgen/seed/cleanup.go +++ b/backend/scripts/loadgen/seed/cleanup.go @@ -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) }