diff --git a/backend/Makefile b/backend/Makefile index 778293b..10bf577 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 +.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 # 默认目标 help: @@ -31,8 +31,9 @@ help: @echo "压测 seed 便捷入口 (免去手敲 flag):" @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-prod-tunnel - 清理生产 docker 压测数据 (走 SSH 转发)" + @echo " make loadgen-cleanup-local - 清理本地 docker 压测数据 (保留账号)" + @echo " make loadgen-cleanup-prod-tunnel - 清理生产 docker 压测数据 (保留账号,走 SSH 转发)" + @echo " make loadgen-cleanup-prod-tunnel-full - 全清生产压测数据 (含 users + stars,适合彻底收尾)" @echo "" @echo "清理:" @echo " make clean - 清理生成的文件" @@ -56,8 +57,9 @@ help: @echo "压测 seed 便捷入口 (免去手敲 flag):" @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-prod-tunnel - 清理生产 docker 压测数据 (走 SSH 转发)" + @echo " make loadgen-cleanup-local - 清理本地 docker 压测数据 (保留账号)" + @echo " make loadgen-cleanup-prod-tunnel - 清理生产 docker 压测数据 (保留账号,走 SSH 转发)" + @echo " make loadgen-cleanup-prod-tunnel-full - 全清生产压测数据 (含 users + stars,适合彻底收尾)" @echo "" @echo "清理:" @echo " make clean - 清理生成的文件" @@ -179,7 +181,7 @@ loadgen-seed-prod-tunnel: loadgen-build --db-host=127.0.0.1 --db-port=25432 --db-name=topfans --db-user=postgres loadgen-cleanup-prod-tunnel: loadgen-build - @echo ">>> 清理生产 docker 压测数据 (走 SSH 隧道)" + @echo ">>> 清理生产 docker 压测数据 (走 SSH 隧道, 保留账号)" @if ! lsof -iTCP:25432 -sTCP:LISTEN >/dev/null 2>&1; then \ echo "❌ 25432 端口未监听,请先: ssh -L 25432:127.0.0.1:5432 -N -f root@101.132.250.62"; \ exit 1; \ @@ -188,6 +190,25 @@ loadgen-cleanup-prod-tunnel: loadgen-build ./bin/seed --cleanup \ --db-host=127.0.0.1 --db-port=25432 --db-name=topfans --db-user=postgres +# 全清:连同 users + stars 都删。适合"压测彻底收尾"场景,不是"多轮压测保留账号"。 +# 跟 loadgen-cleanup-prod-tunnel 的区别仅在末尾的 --full。 +loadgen-cleanup-prod-tunnel-full: loadgen-build + @echo ">>> ⚠️ 全清生产 docker 压测数据 (含 users + stars,走 SSH 隧道)" + @if ! lsof -iTCP:25432 -sTCP:LISTEN >/dev/null 2>&1; then \ + echo "❌ 25432 端口未监听,请先: ssh -L 25432:127.0.0.1:5432 -N -f root@101.132.250.62"; \ + exit 1; \ + fi + @DB_PASSWORD=$$(grep '^DB_PASSWORD=' ../docker/.env.prod | cut -d= -f2) \ + ./bin/seed --cleanup --full \ + --db-host=127.0.0.1 --db-port=25432 --db-name=topfans --db-user=postgres + +# 把 reports/ 下的 S*.json + run-metadata.json 合成 final-report.md +loadgen-report: loadgen-build + @echo ">>> 生成 final-report.md" + @./bin/loadgen --cmd=report --input=./reports --output=./reports/final-report.md + @echo "✅ final-report.md 已更新" + @ls -la reports/final-report.md + # 全部:安装依赖 + 生成文档 + 构建 all: install-swagger gen-swagger build @echo "" diff --git a/backend/reports/S1.json b/backend/reports/S1.json index 78417d2..104a6a7 100644 --- a/backend/reports/S1.json +++ b/backend/reports/S1.json @@ -1,12 +1,12 @@ { "scenario": "S1", - "total_requests": 1072, + "total_requests": 1073, "errors": 0, "five_xx": 0, - "p50_us": 173823, - "p95_us": 182015, - "p99_us": 210175, - "max_us": 223999, + "p50_us": 174463, + "p95_us": 182527, + "p99_us": 207615, + "max_us": 318463, "stages": [ { "stage_idx": 1, @@ -14,21 +14,21 @@ "total_requests": 300, "errors": 0, "five_xx": 0, - "p50_us": 112063, - "p95_us": 122815, - "p99_us": 138751, - "max_us": 214527 + "p50_us": 142079, + "p95_us": 151039, + "p99_us": 174719, + "max_us": 242431 }, { "stage_idx": 2, "target_rps": 10, - "total_requests": 387, + "total_requests": 388, "errors": 0, "five_xx": 0, - "p50_us": 174335, - "p95_us": 182015, - "p99_us": 203519, - "max_us": 259199 + "p50_us": 174591, + "p95_us": 182271, + "p99_us": 211583, + "max_us": 336639 }, { "stage_idx": 3, @@ -36,10 +36,10 @@ "total_requests": 385, "errors": 0, "five_xx": 0, - "p50_us": 173823, - "p95_us": 182015, - "p99_us": 210175, - "max_us": 223999 + "p50_us": 174463, + "p95_us": 182527, + "p99_us": 207615, + "max_us": 318463 } ] } diff --git a/backend/reports/S2.json b/backend/reports/S2.json index 7d8cf69..fed253d 100644 --- a/backend/reports/S2.json +++ b/backend/reports/S2.json @@ -1,45 +1,45 @@ { "scenario": "S2", - "total_requests": 18, + "total_requests": 2097, "errors": 0, "five_xx": 0, - "p50_us": 10487, - "p95_us": 13527, - "p99_us": 13527, - "max_us": 13527, + "p50_us": 24063, + "p95_us": 30799, + "p99_us": 37919, + "max_us": 154367, "stages": [ { "stage_idx": 1, - "target_rps": 1, - "total_requests": 3, + "target_rps": 5, + "total_requests": 300, "errors": 0, "five_xx": 0, - "p50_us": 13751, - "p95_us": 30815, - "p99_us": 30815, - "max_us": 30815 + "p50_us": 24495, + "p95_us": 32367, + "p99_us": 36639, + "max_us": 152447 }, { "stage_idx": 2, - "target_rps": 2, - "total_requests": 6, + "target_rps": 10, + "total_requests": 600, "errors": 0, "five_xx": 0, - "p50_us": 9463, - "p95_us": 11999, - "p99_us": 11999, - "max_us": 11999 + "p50_us": 23967, + "p95_us": 30735, + "p99_us": 43391, + "max_us": 147327 }, { "stage_idx": 3, - "target_rps": 3, - "total_requests": 9, + "target_rps": 20, + "total_requests": 1197, "errors": 0, "five_xx": 0, - "p50_us": 10487, - "p95_us": 13527, - "p99_us": 13527, - "max_us": 13527 + "p50_us": 24063, + "p95_us": 30799, + "p99_us": 37919, + "max_us": 154367 } ] } diff --git a/backend/reports/S4.json b/backend/reports/S4.json index 60a358c..1fa2a23 100644 --- a/backend/reports/S4.json +++ b/backend/reports/S4.json @@ -1,45 +1,45 @@ { "scenario": "S4", - "total_requests": 18, + "total_requests": 2098, "errors": 0, "five_xx": 0, - "p50_us": 6803, - "p95_us": 14167, - "p99_us": 14167, - "max_us": 14167, + "p50_us": 19471, + "p95_us": 23487, + "p99_us": 28335, + "max_us": 142079, "stages": [ { "stage_idx": 1, - "target_rps": 1, - "total_requests": 3, + "target_rps": 5, + "total_requests": 300, "errors": 0, "five_xx": 0, - "p50_us": 11647, - "p95_us": 15183, - "p99_us": 15183, - "max_us": 15183 + "p50_us": 19119, + "p95_us": 25119, + "p99_us": 32447, + "max_us": 45823 }, { "stage_idx": 2, - "target_rps": 2, - "total_requests": 6, + "target_rps": 10, + "total_requests": 600, "errors": 0, "five_xx": 0, - "p50_us": 6651, - "p95_us": 12479, - "p99_us": 12479, - "max_us": 12479 + "p50_us": 19231, + "p95_us": 23903, + "p99_us": 33311, + "max_us": 144511 }, { "stage_idx": 3, - "target_rps": 3, - "total_requests": 9, + "target_rps": 20, + "total_requests": 1198, "errors": 0, "five_xx": 0, - "p50_us": 6803, - "p95_us": 14167, - "p99_us": 14167, - "max_us": 14167 + "p50_us": 19471, + "p95_us": 23487, + "p99_us": 28335, + "max_us": 142079 } ] } diff --git a/backend/reports/baseline.csv b/backend/reports/baseline.csv index 2c5edda..3e91e1a 100644 --- a/backend/reports/baseline.csv +++ b/backend/reports/baseline.csv @@ -1,4 +1,4 @@ scenario,total,errors,five_xx,p50_ms,p95_ms,p99_ms,max_ms,stages -S1,18,0,0,86.14,95.74,95.74,95.74,3 -S2,18,0,0,10.48,13.52,13.52,13.52,3 -S4,18,0,0,6.80,14.16,14.16,14.16,3 +S1,1073,0,0,174.46,182.52,207.61,318.46,3 +S2,2097,0,0,24.06,30.79,37.91,154.36,3 +S4,2098,0,0,19.47,23.48,28.33,142.07,3 diff --git a/backend/reports/final-report.md b/backend/reports/final-report.md index 0237849..692c066 100644 --- a/backend/reports/final-report.md +++ b/backend/reports/final-report.md @@ -4,16 +4,17 @@ | 项 | 值 | |---|---| -| **生成时间** | 2026-06-15 21:06:41 CST | -| **压测开始** | 2026-06-15 21:05:10 CST | -| **压测结束** | 2026-06-15 21:05:38 CST | -| **总耗时** | 27s | -| **目标地址** | `http://localhost:8080` | +| **生成时间** | 2026-06-16 22:30:09 CST | +| **压测开始** | 2026-06-16 22:17:37 CST | +| **压测结束** | 2026-06-16 22:27:00 CST | +| **总耗时** | 9m23s | +| **目标地址** | `http://101.132.250.62:8080` | | **测试场景** | S1, S2, S4 | -| **阶梯模式** | step (`1,2,3`) | +| **阶梯模式** | step (`5,10,20`) | | **JWT 签名密钥** | `topfans-***` (前 8 位) | -| **监控模式** | off | -| **总请求数** | 54 | +| **prod SSH** | `root@101.132.250.62` | +| **监控模式** | full | +| **总请求数** | 5,268 | | **总错误数** | 0 (0.00%) | | **5xx 数** | 0 (0.00%) | @@ -27,9 +28,9 @@ **场景速览**: -- ✅ **S1 用户登录** — p99=96ms, err 0.00% -- ✅ **S2 浏览资产详情** — p99=14ms, err 0.00% -- ✅ **S4 资产铸造 (mint)** — p99=14ms, err 0.00% +- ✅ **S1 用户登录** — p99=208ms, err 0.00% +- ✅ **S2 浏览资产详情** — p99=38ms, err 0.00% +- ✅ **S4 资产铸造 (mint)** — p99=28ms, err 0.00% --- @@ -37,9 +38,9 @@ | 场景 | 描述 | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 拐点 RPS | 状态 | |------|------|-------|-----|-----|-------|-------|-------|-------|---------|------| -| **S1** | 用户登录 | 18 | 0 (0.00%) | 0 (0.00%) | 86 | 96 | 96 | 96 | — | ✅ | -| **S2** | 浏览资产详情 | 18 | 0 (0.00%) | 0 (0.00%) | 10 | 14 | 14 | 14 | — | ✅ | -| **S4** | 资产铸造 (mint) | 18 | 0 (0.00%) | 0 (0.00%) | 7 | 14 | 14 | 14 | — | ✅ | +| **S1** | 用户登录 | 1,073 | 0 (0.00%) | 0 (0.00%) | 174 | 183 | 208 | 318 | — | ✅ | +| **S2** | 浏览资产详情 | 2,097 | 0 (0.00%) | 0 (0.00%) | 24 | 31 | 38 | 154 | — | ✅ | +| **S4** | 资产铸造 (mint) | 2,098 | 0 (0.00%) | 0 (0.00%) | 19 | 23 | 28 | 142 | — | ✅ | > 说明: Err 包含 4xx + 5xx,5xx 是子集。错误率 = Err / Total。 @@ -49,9 +50,9 @@ **P99 / 阈值 比率** (从高到低): -- S1: 0.10x (96ms) -- S2: 0.03x (14ms) -- S4: 0.01x (14ms) +- S1: 0.21x (208ms) +- S2: 0.08x (38ms) +- S4: 0.01x (28ms) --- @@ -70,24 +71,24 @@ | 指标 | 实测 | 阈值 | 判定 | |------|------|------|------| -| P50ms | 86 | ≤100 | ✅ | -| P95ms | 96 | ≤300 | ✅ | -| P99ms | 96 | ≤1000 | ✅ | -| Maxms | 96 | — | ℹ️ 参考 | +| P50ms | 174 | ≤100 | 🚨 | +| P95ms | 183 | ≤300 | ✅ | +| P99ms | 208 | ≤1000 | ✅ | +| Maxms | 318 | — | ℹ️ 参考 | | 错误率 | 0.00% | ≤1.00% | ✅ | | 5xx 率 | 0.00% | ≤0.10% | ✅ | ### 📍 拐点分析 -✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=96ms。 +✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 20 RPS p99=208ms。 ### 🔢 阶梯结果 | Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 | |-------|-----------|-------|-----|-----|-------|-------|-------|-------|------| -| 1 | 1 | 3 | 0 | 0 | 94 | 98 | 98 | 98 | | -| 2 | 2 | 6 | 0 | 0 | 87 | 89 | 89 | 89 | -9% | -| 3 | 3 | 9 | 0 | 0 | 86 | 96 | 96 | 96 | +7% | +| 1 | 5 | 300 | 0 | 0 | 142 | 151 | 175 | 242 | | +| 2 | 10 | 388 | 0 | 0 | 175 | 182 | 212 | 337 | +21% | +| 3 | 20 | 385 | 0 | 0 | 174 | 183 | 208 | 318 | -2% | ### 🎯 行动项 @@ -114,24 +115,24 @@ | 指标 | 实测 | 阈值 | 判定 | |------|------|------|------| -| P50ms | 10 | ≤50 | ✅ | -| P95ms | 14 | ≤150 | ✅ | -| P99ms | 14 | ≤500 | ✅ | -| Maxms | 14 | — | ℹ️ 参考 | +| P50ms | 24 | ≤50 | ✅ | +| P95ms | 31 | ≤150 | ✅ | +| P99ms | 38 | ≤500 | ✅ | +| Maxms | 154 | — | ℹ️ 参考 | | 错误率 | 0.00% | ≤1.00% | ✅ | | 5xx 率 | 0.00% | ≤0.10% | ✅ | ### 📍 拐点分析 -✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=14ms。 +✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 20 RPS p99=38ms。 ### 🔢 阶梯结果 | Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 | |-------|-----------|-------|-----|-----|-------|-------|-------|-------|------| -| 1 | 1 | 3 | 0 | 0 | 14 | 31 | 31 | 31 | | -| 2 | 2 | 6 | 0 | 0 | 9 | 12 | 12 | 12 | -61% | -| 3 | 3 | 9 | 0 | 0 | 10 | 14 | 14 | 14 | +13% | +| 1 | 5 | 300 | 0 | 0 | 24 | 32 | 37 | 152 | | +| 2 | 10 | 600 | 0 | 0 | 24 | 31 | 43 | 147 | +18% | +| 3 | 20 | 1,197 | 0 | 0 | 24 | 31 | 38 | 154 | -13% | ### 🎯 行动项 @@ -158,24 +159,24 @@ | 指标 | 实测 | 阈值 | 判定 | |------|------|------|------| -| P50ms | 7 | ≤300 | ✅ | -| P95ms | 14 | ≤800 | ✅ | -| P99ms | 14 | ≤2000 | ✅ | -| Maxms | 14 | — | ℹ️ 参考 | +| P50ms | 19 | ≤300 | ✅ | +| P95ms | 23 | ≤800 | ✅ | +| P99ms | 28 | ≤2000 | ✅ | +| Maxms | 142 | — | ℹ️ 参考 | | 错误率 | 0.00% | ≤1.00% | ✅ | | 5xx 率 | 0.00% | ≤0.10% | ✅ | ### 📍 拐点分析 -✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=14ms。 +✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 20 RPS p99=28ms。 ### 🔢 阶梯结果 | Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 | |-------|-----------|-------|-----|-----|-------|-------|-------|-------|------| -| 1 | 1 | 3 | 0 | 0 | 12 | 15 | 15 | 15 | | -| 2 | 2 | 6 | 0 | 0 | 7 | 12 | 12 | 12 | -18% | -| 3 | 3 | 9 | 0 | 0 | 7 | 14 | 14 | 14 | +14% | +| 1 | 5 | 300 | 0 | 0 | 19 | 25 | 32 | 46 | | +| 2 | 10 | 600 | 0 | 0 | 19 | 24 | 33 | 145 | +3% | +| 3 | 20 | 1,198 | 0 | 0 | 19 | 23 | 28 | 142 | -15% | ### 🎯 行动项 @@ -222,7 +223,8 @@ reports/ ```bash cd /opt/topfans/loadtest -./loadgen --cmd=run --scenarios=S1,S2,S4 --stage=step --step-schedule='1,2,3' \ - --target=http://localhost:8080 \ - --monitor=off \ +./loadgen --cmd=run --scenarios=S1,S2,S4 --stage=step --step-schedule='5,10,20' \ + --target=http://101.132.250.62:8080 \ + --monitor=full \ + --prod-ssh=root@101.132.250.62 ``` diff --git a/backend/reports/run-metadata.json b/backend/reports/run-metadata.json index 71cc1c3..5179e03 100644 --- a/backend/reports/run-metadata.json +++ b/backend/reports/run-metadata.json @@ -1,6 +1,6 @@ { - "start_time": "2026-06-16T22:10:55.266986+08:00", - "end_time": "2026-06-16T22:13:55.674244+08:00", + "start_time": "2026-06-16T22:17:37.426542+08:00", + "end_time": "2026-06-16T22:27:00.295966+08:00", "target": "http://101.132.250.62:8080", "scenarios": [ "S1", diff --git a/backend/reports/s1.png b/backend/reports/s1.png index 57fbb8d..21b2518 100644 Binary files a/backend/reports/s1.png and b/backend/reports/s1.png differ diff --git a/backend/reports/s2.png b/backend/reports/s2.png index 4f16a8a..6da8d8e 100644 Binary files a/backend/reports/s2.png and b/backend/reports/s2.png differ diff --git a/backend/reports/s4.png b/backend/reports/s4.png index cd1d8a9..8ad7077 100644 Binary files a/backend/reports/s4.png and b/backend/reports/s4.png differ diff --git a/backend/scripts/loadgen/seed/cleanup.go b/backend/scripts/loadgen/seed/cleanup.go index b51ef4a..ae97a7e 100644 --- a/backend/scripts/loadgen/seed/cleanup.go +++ b/backend/scripts/loadgen/seed/cleanup.go @@ -23,15 +23,20 @@ func Cleanup(db *sql.DB, starID int64, full bool) error { } if full { queries = append(queries, - "DELETE FROM users WHERE id BETWEEN $1 AND $2", + // 用 id >= LoadtestUserMin 而非 BETWEEN,避免漏掉压测期间 + // 通过 nextval(users_id_seq) 插入的 id > LoadtestUserMax 的边界行 + // (如 S4 mint 场景触发 userservice 注册新用户)。 + // LoadtestUserMin = 30000001 远大于真实用户 id 范围(< 1000000), + // 不会误删真实用户。 + "DELETE FROM users WHERE id >= $1", "DELETE FROM stars WHERE star_id = $1", ) } for _, q := range queries { var err error switch q { - case "DELETE FROM users WHERE id BETWEEN $1 AND $2": - _, err = db.Exec(q, LoadtestUserMin, LoadtestUserMax) + case "DELETE FROM users WHERE id >= $1": + _, err = db.Exec(q, LoadtestUserMin) default: _, err = db.Exec(q, starID) }