1155 lines
56 KiB
Markdown
1155 lines
56 KiB
Markdown
# 后端服务压力测试设计
|
||
|
||
> **状态**:设计已确认 ✅
|
||
> **创建时间**:2026-06-12
|
||
> **作者**:Claude
|
||
> **目标**:为部署在阿里云单机(`101.132.250.62`,4G/2C,docker-compose)的 TopFans 后端微服务,设计一套可执行、可恢复、可重复的压力测试方案。覆盖容量评估、SLA 基线、稳定性验证、破坏性测试四类目标。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [背景与目标](#1-背景与目标)
|
||
2. [关键约束与已验证事实](#2-关键约束与已验证事实)
|
||
3. [总体方案与架构](#3-总体方案与架构)
|
||
4. [测试数据准备](#4-测试数据准备)
|
||
5. [场景设计与 RPS 梯度](#5-场景设计与-rps-梯度)
|
||
6. [执行计划与时间盒](#6-执行计划与时间盒)
|
||
7. [监控指标与判停红线](#7-监控指标与判停红线)
|
||
8. [风险控制与回滚](#8-风险控制与回滚)
|
||
9. [产出物清单](#9-产出物清单)
|
||
10. [不在范围(YAGNI)](#10-不在范围yagni)
|
||
11. [后续步骤](#11-后续步骤)
|
||
12. [附录 A:术语表](#附录-a术语表)
|
||
13. [附录 B:被测接口路径速查](#附录-b被测接口路径速查)
|
||
|
||
---
|
||
|
||
## 1. 背景与目标
|
||
|
||
### 1.1 背景
|
||
|
||
TopFans 平台当前部署在阿里云单机 `101.132.250.62`,使用 `docker/docker-compose.prod.yml` 启动 11 个容器(gateway + 9 个 Dubbo 微服务 + PostgreSQL + Redis)。
|
||
|
||
项目目前处于早期阶段(生产数据规模:89 users / 91 fan_profiles / 223 assets),尚未做过任何系统化的压力测试。团队对系统在当前 4G/2C 资源配置下的**真实承载能力**、**接口性能瓶颈**、**长时间运行的稳定性**均缺乏量化数据。
|
||
|
||
### 1.2 目标(四象全要)
|
||
|
||
| 目标 | 含义 | 产出 |
|
||
|---|---|---|
|
||
| **G1 容量评估** | 找出每个核心接口的拐点 RPS(错误率/P99 突破阈值前的最大稳态 RPS) | 每场景一个"拐点 RPS"数字 + 瓶颈分析 |
|
||
| **G2 SLA 基线** | 建立每个接口的 P50/P95/P99 延迟基线,供后续回归对比 | `baseline.csv` + HDR 直方图 |
|
||
| **G3 稳定性** | 在安全水位下跑 30 分钟,验证无内存/连接池/goroutine 泄漏 | 时序图 + 资源增长率 |
|
||
| **G4 破坏性** | 推至拐点 2-3 倍 RPS,验证降级与自动恢复 | 恢复时间 + 异常日志摘录 |
|
||
|
||
### 1.3 非目标(明确不做)
|
||
|
||
- 不做端到端业务正确性测试(这是 E2E 的工作)
|
||
- 不做安全/渗透测试
|
||
- 不做前端性能测试
|
||
- 不做单元/集成测试
|
||
- 不做 AI Chat WebSocket 压测(独立场景,本轮跳过)
|
||
- 不做活动榜单/星册等次要接口压测(本轮聚焦核心 7 场景)
|
||
|
||
---
|
||
|
||
## 2. 关键约束与已验证事实
|
||
|
||
### 2.1 环境约束
|
||
|
||
| 项 | 现状 |
|
||
|---|---|
|
||
| 部署位置 | 阿里云 ECS `101.132.250.62`(**华东 1 / 杭州**,101.132.x.x 段;root SSH) |
|
||
| 资源 | **4G RAM / 2 CPU**(生产单机,无 staging) |
|
||
| 容器资源限额 | gateway 300M/0.5C,各微服务 100-200M/0.5C,PG 400M(max 100 connections),Redis 256M |
|
||
| 入口 | 公网 `http://101.132.250.62:8080`(无 nginx 反代,gateway 直接对外) |
|
||
| 部署脚本 | `docker/deploy.sh`(参考用法见脚本头部注释) |
|
||
|
||
⚠️ **PG 内存配置冲突(必须在 preflight 阶段处理)**:
|
||
- PG 容器限额 400M,但 `POSTGRES_MAX_CONNECTIONS=100`
|
||
- 每个 PG backend 进程典型占用 5-10MB(work_mem + stack + plan cache,不含 shared_buffers)
|
||
- 100 connections × 7MB ≈ **700MB > 400M 容器限额**
|
||
- → R4 红线 85 connections 之前,PG 进程很可能已被 cgroup OOM Killer 杀掉
|
||
- → R6 OOM 红线如果检测不准(详见 §7.3 修复说明),会出现"PG 突然消失但红线没触发"
|
||
- **处置方案**(任选其一):
|
||
- 方案 A(推荐):第一轮压测前**手动**把 `POSTGRES_MAX_CONNECTIONS` 降到 50(preflight 仅做**检测+报错+给出 SQL 命令**,不自动改 PG 配置,因为 `max_connections` 修改需要重启 PG 才生效,不能 reload)。手动步骤:
|
||
```bash
|
||
# 改 docker-compose.prod.yml POSTGRES_MAX_CONNECTIONS=50
|
||
# 重启 postgres 容器(约 30s 停机)
|
||
docker-compose -f docker-compose.prod.yml restart postgres
|
||
# 验证
|
||
docker exec "$PG_CONTAINER" psql -U postgres -c "SHOW max_connections;"
|
||
```
|
||
- 方案 B:把 PG 容器 limits.memory 提到 1024M,max_connections 不变(同样需重启 PG)
|
||
- 方案 C:接受现状,把 R4 阈值从 85 调到 50(无停机,但风险还在)
|
||
|
||
### 2.2 数据库约束(来自本地 `top-fans` 库 schema 调研)
|
||
|
||
**库名差异(重要)**:
|
||
- 本地 docker:`top-fans`(带横线)
|
||
- prod docker-compose 配置:`topfans`(无横线)
|
||
- → seed/loadgen 工具的 DSN 必须参数化,**开工前先 ssh 到 prod 跑 `\d+ <key_tables>` 验证 schema 与本地一致**
|
||
|
||
**核心表与隐含约束**:
|
||
|
||
| 表 | 关键约束 |
|
||
|---|---|
|
||
| `users` | mobile 唯一(仅 deleted_at IS NULL);id BIGSERIAL |
|
||
| `stars` | star_id BIGSERIAL;identity_id 唯一 |
|
||
| `fan_profiles` | `(user_id, star_id)` 唯一;`(star_id, nickname)` 唯一;多了 `experience`/`revenue_boost_bps` 字段(vs GORM model) |
|
||
| `assets` | id BIGSERIAL;外键 owner_uid → users.id, star_id → stars.star_id |
|
||
| `asset_likes` | **`exhibition_id NOT NULL`**;唯一约束 `(user_id, asset_id, exhibition_id)`;**点赞必须依附 exhibition** |
|
||
| `exhibitions` | `uk_asset WHERE deleted_at IS NULL`:每个 asset 同一时刻只能上一个展位 |
|
||
| `booth_slots` | `is_enabled` 默认 false,**seed 时必须显式置 true** |
|
||
| `mint_orders` | OrderID 是 UUID(字符串主键),不需序列重置 |
|
||
|
||
**`auto_users` 表(避坑)**:
|
||
存在一张 `auto_users` 表(主键序列名 `auto_like_users_id_seq` 暴露原始用途:**自动点赞机器人元数据表**)。压测数据**绝不写入 auto_users**,避免被业务侧的自动点赞调度器扫到产生不可控背景流量。
|
||
|
||
### 2.3 业务约束
|
||
|
||
**铸造成本指数翻倍(关键约束)**:
|
||
|
||
`mint_cost_config` 当前配置:
|
||
|
||
```
|
||
第1次:2 第2次:4 第3次:8 第4次:16 第5次:32
|
||
第6次:64 第7次:128 第8次:256 第9次:512 第10次:1024
|
||
累计 = 2046 水晶/用户
|
||
```
|
||
|
||
**含义**:
|
||
- 单测试账号最多压 10 次铸造(之后 `mint_cost_config` 查空报错)
|
||
- → 铸造场景采用 **轮转重置策略**:每 200 秒一轮,跑完 reset 水晶+次数+订单,再开下一轮(详见 §5.4)
|
||
|
||
**JWT secret**:
|
||
- 本地:`topfans-secret-key-local-dev-only`
|
||
- prod:`topfans-secret-key-please-change-in-production`
|
||
- seed 工具复用 `backend/pkg/jwt.GenerateToken()`,secret 通过 `--jwt-secret` 参数注入,**不要 commit 到 repo**
|
||
|
||
### 2.4 已确认决策汇总
|
||
|
||
| 决策项 | 选择 |
|
||
|---|---|
|
||
| 压源位置 | 同地域阿里云 ECS(4G/2C 按量付费,~5 元/天) |
|
||
| 监控形态 | 可配置三档:`--monitor=off/lite/full`,默认 `lite` |
|
||
| 工具栈 | **方案 D**:自研 Go 二进制 `loadgen` + `seed` |
|
||
| 业务规模预期 | 早期项目摸底,无业务量预设 |
|
||
| 核心场景 | S1 登录、S2 资产读、S3 点赞、S4 铸造、S5 看板、S6 多维榜单、S7 上架 |
|
||
| seed 运行位置 | prod 服务器本地(避免 PG 公网暴露) |
|
||
| schema 验证 | 开工前 ssh prod 跑 diff |
|
||
| 铸造场景节奏 | 轮转重置 |
|
||
| RPS 阶梯策略 | 每场景独立阶梯(详见 §5.6) |
|
||
| 压测分轮 | 分两轮:探索 ~2h + 修复 + 验证 ~4h |
|
||
| 第一轮混合场景 | 不做(数据驱动后再定比例) |
|
||
| 6 维红线阈值 | 维持设计原值(详见 §7.3) |
|
||
| 实时仪表 | stderr 行模式(不做 TUI) |
|
||
| Grafana 面板 | 4 个(整机/容器/PG/业务) |
|
||
|
||
---
|
||
|
||
## 3. 总体方案与架构
|
||
|
||
### 3.1 架构总览
|
||
|
||
```
|
||
┌──────────────────────────┐ ┌────────────────────────────────────┐
|
||
│ 压力机 (同地域 ECS) │ 公网 HTTP │ prod 101.132.250.62 (4G/2C) │
|
||
│ │ ─────────► │ │
|
||
│ loadgen (Go binary) │ │ docker-compose.prod.yml │
|
||
│ ├ scenarios/*.go │ │ ├ gateway:8080 (REST) │
|
||
│ ├ lib/{ramp,circuit, │ │ ├ userservice/socialservice/... │
|
||
│ │ hdr,csv,client} │ │ └ postgres + redis │
|
||
│ ├ users.csv (1000 行) │ │ │
|
||
│ └ reports/run-*/ │ │ /opt/topfans/loadtest/ │
|
||
│ │ ssh tunnel │ ├ seed binary │
|
||
│ loadgen-watcher (ssh) │ ─────────► │ ├ sample.sh (后台) │
|
||
│ │ │ ├ metrics-feed.jsonl │
|
||
│ │ │ ├ emergency-stop.sh │
|
||
│ │ │ └ restore-from-backup.sh │
|
||
└──────────────────────────┘ └────────────────────────────────────┘
|
||
▲ ▲
|
||
│ 报告生成(事后) │ 可选: docker-compose.monitor.yml
|
||
▼ ▼
|
||
./reports/run-YYYYMMDD-HHMMSS/ cAdvisor + node/pg/redis-exporter
|
||
*.json *.csv *.md *.svg + Prometheus + Grafana (端口 3000)
|
||
```
|
||
|
||
### 3.2 为什么选这个架构
|
||
|
||
| 决策 | 理由 |
|
||
|---|---|
|
||
| **不在被压机器跑压源** | 资源争抢:loadgen 在 200-1000 goroutine 并发下吃 0.5-1 CPU、300-800MB,会污染容量数据 30-50%;localhost 走 lo 不经过物理 NIC,路径与真实用户不同;docker stats 自己也会被压慢,监控失真。**注意区分**:spec §4.2 的"1000 测试用户"是用户池规模(数据),loadgen 实际并发 goroutine 数由 RPS × 平均 RT 决定(200 RPS × 100ms RT = 20 并发 goroutine)。 |
|
||
| **同地域 ECS** | 跨公网压会把家宽延迟/丢包混进 P99,污染结论;**压源 ECS 与 prod 同在华东 1(杭州)**,内网/同骨干 RTT 2-5ms |
|
||
| **不绕过 gateway** | 真实业务路径就从 gateway 入,绕过测不出 gateway 自身瓶颈 |
|
||
| **可配置监控(off/lite/full)** | 摸容量拐点时 lite 干扰最小(< 30M);定位慢查询时 full(~400M/0.3C)值得开 |
|
||
| **方案 D 自研 Go 工具** | Go 后端团队熟,可复用 `pkg/jwt`/`pkg/types`;单二进制部署;逻辑可灵活定制(如铸造轮转) |
|
||
|
||
### 3.3 工程目录结构
|
||
|
||
```
|
||
backend/scripts/loadgen/
|
||
├── seed/
|
||
│ ├── main.go # 入口,解析 --jwt-secret/--db-dsn/--reset
|
||
│ ├── stars.go # INSERT star_id=999900
|
||
│ ├── users.go # INSERT 1000 users (bcrypt 'Test@123')
|
||
│ ├── profiles.go # INSERT fan_profiles + 充值 2200 水晶
|
||
│ ├── slots_and_exhibits.go # INSERT booth_slots (is_enabled=true) + exhibitions
|
||
│ ├── assets.go # INSERT 5000 assets (status=1)
|
||
│ ├── friendships.go # INSERT 10000 friendships
|
||
│ ├── tokens.go # 复用 pkg/jwt 预签 token,写 users.csv
|
||
│ ├── sequences.go # 重置所有相关表的序列(CLAUDE.md 规范)
|
||
│ ├── cleanup.go # DELETE WHERE star_id=999900(强校验)
|
||
│ └── README.md
|
||
├── loadgen/
|
||
│ ├── main.go # 入口,解析 --scenario/--rps/--vus/--duration/--monitor
|
||
│ ├── lib/
|
||
│ │ ├── ramp.go # 阶梯调度器:[]Stage{rps, duration}
|
||
│ │ ├── circuit.go # 6 维红线判停(详见 §7.3)
|
||
│ │ ├── hdr.go # HdrHistogram-go 封装
|
||
│ │ ├── csv.go # users.csv 加载到内存
|
||
│ │ ├── client.go # http.Transport(MaxConnsPerHost=500)
|
||
│ │ ├── ssh_metrics.go # ssh tail metrics-feed.jsonl
|
||
│ │ └── log.go # stderr 行仪表 + 全量 events.jsonl
|
||
│ ├── scenarios/
|
||
│ │ ├── s1_login.go
|
||
│ │ ├── s2_read.go
|
||
│ │ ├── s3_like.go
|
||
│ │ ├── s4_mint.go # 含 reset SQL 调度
|
||
│ │ ├── s5_dashboard.go
|
||
│ │ ├── s6_ranking.go
|
||
│ │ └── s7_place.go
|
||
│ ├── reporter/
|
||
│ │ ├── json.go # raw 时序
|
||
│ │ ├── csv.go # 聚合表
|
||
│ │ ├── plot.go # gonum/plot 三联图
|
||
│ │ └── markdown.go # report.md 生成
|
||
│ ├── preflight.go # §8 7 项 sanity check
|
||
│ └── verify.go # 压测后数据完整性校验
|
||
├── monitor/
|
||
│ ├── sample.sh # 被压机后台采样
|
||
│ ├── docker-compose.monitor.yml # cAdvisor+exporters+Prom+Grafana
|
||
│ └── grafana-dashboards/ # 4 个预置面板 JSON
|
||
├── recover/
|
||
│ ├── emergency-stop.sh # 一键熔断
|
||
│ └── restore-from-backup.sh # pg_dump 还原
|
||
└── reports/ # gitignore,跑测产出
|
||
└── run-YYYYMMDD-HHMMSS/
|
||
```
|
||
|
||
### 3.4 loadgen CLI 完整命令与 flag 表(终审修复 D3)
|
||
|
||
> 散落在 §4.5/§5.6/§6.1/§7.5/§8.5 的命令在此处统一定义。实施时**严格按这张表**生成 CLI。
|
||
|
||
| 子命令 | 用途 | flag | 默认值 |
|
||
|---|---|---|---|
|
||
| `loadgen seed --prod` | 在 prod 本地跑 seed | `--jwt-secret <secret>` 必填 | - |
|
||
| | | `--db-host <host>` | `localhost` |
|
||
| | | `--db-name <name>` | `topfans` |
|
||
| | | `--db-password <pw>` 或读 `$DB_PASSWORD` | - |
|
||
| | | `--reset` 删旧测试数据后重新 seed | false |
|
||
| | | `--reset-tokens` 只重签 token,不动数据 | false |
|
||
| `loadgen seed cleanup` | 清理测试数据 | `--keep-baseline` 保留 1000 testers + 资产 | false |
|
||
| | | `--full` 全删(含 stars/users/profiles) | false |
|
||
| `loadgen run` | 跑压测主流程 | `--scenarios <S1,S2,...>` 复数 ,逗号分隔 | - |
|
||
| | | `--stage <baseline\|step\|soak\|stress>` | `step` |
|
||
| | | `--rps <N>` 单 RPS 模式 | - |
|
||
| | | `--vus <N>` 最大 VU 数 | 自动 |
|
||
| | | `--duration <30m>` 单阶段时长 | 按 §5.3 |
|
||
| | | `--inter-scenario-pause <15m>` | `15m` |
|
||
| | | `--monitor <off\|lite\|full>` | `lite` |
|
||
| | | `--prod-ssh <user@host>` | - |
|
||
| | | `--target <url>` 被压 gateway 入口 | `http://101.132.250.62:8080` |
|
||
| `loadgen preflight` | 开压前 7 项检查 | `--target <url>` | - |
|
||
| | | `--prod-ssh <user@host>` | - |
|
||
| `loadgen verify` | 压后数据完整性校验 | `--prod-ssh <user@host>` | - |
|
||
| `loadgen report` | 从 raw data 生成 Markdown 报告 | `--input <dir>` | - |
|
||
| | | `--output <md path>` | `./report.md` |
|
||
|
||
**命名约定(实施时强制)**:
|
||
- 多场景一律用 `--scenarios=S1,S2,...`(复数,逗号分隔)
|
||
- 单场景用 `--scenarios=S4`(不再设 `--scenario` 单数 flag,避免歧义)
|
||
|
||
---
|
||
|
||
## 4. 测试数据准备
|
||
|
||
### 4.1 数据隔离策略("压测沙盒")
|
||
|
||
所有压测数据归到一个独立的 `star_id`,物理隔离真实业务:
|
||
|
||
| 维度 | 隔离值 | 业务真实值 |
|
||
|---|---|---|
|
||
| star_id | **999900** | 87, 88, 91, 93, 94, 95(6 个) |
|
||
| 测试手机号前缀 | **199000xxxxx**(11 位) | 13x/15x/17x... |
|
||
| 测试 user_id 区间 | **30000001 ~ 30001000** | max=110 |
|
||
| 测试 asset_id 起点 | **`MAX(assets.id) + 1000` 动态分配** | max=303 |
|
||
| 测试昵称 | `loadtest_<n>` | (任意中文/英文) |
|
||
| 测试资产名 | `loadtest_asset_<userId>_<n>` | (任意) |
|
||
|
||
### 4.2 数据规模
|
||
|
||
| 资源 | 数量 | 设计依据 |
|
||
|---|---|---|
|
||
| 测试用户 | 1000 | 200 并发 × 5 倍余量 |
|
||
| 每用户 crystal_balance 初始 | 2200 | ≥ 2046(10 次铸造累计)+ 缓冲 |
|
||
| 每用户预铸资产 | 5 | asset_id 从 `MAX+1000` 起。**用途分配**:资产 1-2 用于 seed 预上架(S3 点赞依赖),资产 3-5 留作未上架库存供 S7 上架/卸下轮转使用 |
|
||
| 每用户 booth_slots | 3(is_enabled=true) | 默认配额;slot_index=1,2 给 seed 预上架,slot_index=3 留给 S7 压测 |
|
||
| 每用户 exhibitions | 2(用 slot 1, 2) | "可点赞资产池"(S3 依赖) |
|
||
| 每用户测试好友 | 10 | S2/S6 部分查询走好友关系 |
|
||
| 总 INSERT 行数 | ~25,000 | users 1k + profiles 1k + assets 5k + slots 3k + exhibits 2k + friendships 1w + stars 1 |
|
||
|
||
### 4.3 Seed 执行流程
|
||
|
||
```sql
|
||
BEGIN;
|
||
|
||
-- 时间戳占位:ts = extract(epoch from now())*1000 (毫秒,与项目时间戳约定一致)
|
||
-- 实际 seed 程序中由 Go 侧 time.Now().UnixMilli() 计算
|
||
|
||
-- 1. 测试明星
|
||
INSERT INTO stars (star_id, name, identity_id, is_active, created_at, updated_at)
|
||
VALUES (999900, 'loadtest_star', 'loadtest_star', true, ts, ts)
|
||
ON CONFLICT (star_id) DO NOTHING;
|
||
|
||
-- 2. 1000 测试用户(password_hash 用 bcrypt('Test@123') 离线生成一次复用)
|
||
INSERT INTO users (id, mobile, password_hash, is_active, created_at, updated_at) VALUES
|
||
(30000001, '19900000001', '$2a$10$<bcrypt_hash>', true, ts, ts),
|
||
...
|
||
ON CONFLICT (id) DO NOTHING;
|
||
|
||
-- 3. fan_profiles + 充值
|
||
INSERT INTO fan_profiles (user_id, star_id, nickname, crystal_balance, slot_limit, is_active, created_at, updated_at) VALUES
|
||
(30000001, 999900, 'loadtest_1', 2200, 3, true, ts, ts), ...;
|
||
|
||
-- 4. assets(从 MAX(id)+1000 起步,避免与真实数据撞主键)
|
||
WITH base AS (SELECT COALESCE(MAX(id), 0) + 1000 AS start FROM assets)
|
||
INSERT INTO assets (id, owner_uid, star_id, name, cover_url, info, status, like_count, is_active, created_at, updated_at, grade)
|
||
SELECT start + n, owner_uid, 999900, 'loadtest_asset_'||owner_uid||'_'||idx, '<PLACEHOLDER_OSS_URL>', 'loadtest', 1, 0, true, ts, ts, 1
|
||
FROM base, generate_series(1, 5000) n, ...;
|
||
|
||
-- 5. booth_slots(每 user 3 个,is_enabled=true)
|
||
INSERT INTO booth_slots (host_profile_id, user_id, star_id, slot_index, is_enabled, created_at, updated_at) ...;
|
||
|
||
-- 6. exhibitions(每 user 把 asset 1,2 上架到 slot 1,2)
|
||
INSERT INTO exhibitions (asset_id, slot_id, host_profile_id, occupier_uid, occupier_star_id, start_time, expire_at, created_at, updated_at)
|
||
SELECT a.id, s.slot_id, fp.id, a.owner_uid, 999900, ts, ts + 4*3600*1000, ts, ts
|
||
FROM assets a
|
||
JOIN fan_profiles fp ON a.owner_uid=fp.user_id AND fp.star_id=999900
|
||
JOIN booth_slots s ON s.host_profile_id=fp.id AND s.slot_index IN (1,2)
|
||
WHERE a.star_id=999900 AND a.name LIKE 'loadtest_%';
|
||
|
||
-- 7. friendships(双向;status 默认 'accepted',唯一约束 (user_id, friend_id, star_id))
|
||
INSERT INTO friendships (user_id, friend_id, star_id, status, intimacy, created_at, updated_at)
|
||
SELECT a.id, b.id, 999900, 'accepted', 0, ts, ts
|
||
FROM users a, users b
|
||
WHERE a.id BETWEEN 30000001 AND 30001000
|
||
AND b.id BETWEEN 30000001 AND 30001000
|
||
AND a.id != b.id
|
||
AND ((a.id - 30000000) + 1) % 10 = ((b.id - 30000000) % 10)
|
||
ON CONFLICT (user_id, friend_id, star_id) DO NOTHING;
|
||
-- 上述 JOIN 条件生成约 10000 行双向好友关系
|
||
|
||
COMMIT;
|
||
|
||
-- 8. ⚠️ CLAUDE.md 强制:重置所有相关表的序列
|
||
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
|
||
SELECT setval('fan_profiles_id_seq', (SELECT MAX(id) FROM fan_profiles));
|
||
SELECT setval('assets_id_seq', (SELECT MAX(id) FROM assets));
|
||
SELECT setval('booth_slots_slot_id_seq', (SELECT MAX(slot_id) FROM booth_slots));
|
||
SELECT setval('exhibitions_id_seq', (SELECT MAX(id) FROM exhibitions));
|
||
SELECT setval('stars_star_id_seq', (SELECT MAX(star_id) FROM stars));
|
||
SELECT setval('asset_likes_id_seq', (SELECT COALESCE(MAX(id), 0) FROM asset_likes));
|
||
SELECT setval('friendships_id_seq', (SELECT MAX(id) FROM friendships));
|
||
SELECT setval('crystal_transaction_records_id_seq', (SELECT COALESCE(MAX(id), 0) FROM crystal_transaction_records));
|
||
```
|
||
|
||
### 4.4 JWT Token 预签发
|
||
|
||
```go
|
||
// backend/scripts/loadgen/seed/tokens.go
|
||
import "github.com/topfans/backend/pkg/jwt"
|
||
|
||
func GenerateTokensForLoadtest(users []TestUser, jwtSecret string) error {
|
||
jwt.SetSecret(jwtSecret) // 从命令行参数注入
|
||
|
||
csvFile, _ := os.Create("users.csv")
|
||
defer csvFile.Close()
|
||
writer := csv.NewWriter(csvFile)
|
||
writer.Write([]string{"phone", "password", "user_id", "star_id", "jwt_token", "asset_ids", "exhibition_ids"})
|
||
|
||
for _, u := range users {
|
||
token, err := jwt.GenerateToken(u.UserID, 999900, time.Now().UnixMilli())
|
||
if err != nil { return err }
|
||
writer.Write([]string{
|
||
u.Mobile, "Test@123",
|
||
strconv.FormatInt(u.UserID, 10),
|
||
"999900", token,
|
||
joinInt64Slice(u.AssetIDs, ";"), // 工具函数:strings.Builder + strconv.FormatInt
|
||
joinInt64Slice(u.ExhibitionIDs, ";"),
|
||
})
|
||
}
|
||
return writer.Flush()
|
||
}
|
||
```
|
||
|
||
⚠️ **`users.csv` 加入 `.gitignore`**,包含 token 不能 commit。
|
||
|
||
⚠️ **`<PLACEHOLDER_OSS_URL>`** 需要替换为 OSS 上真实存在的占位图 URL(seed 执行前先上传一张 `loadtest-placeholder.png` 到 OSS 并填入)。**上传方式**:用项目现有接口 `POST /api/v1/assets/oss/upload-signature` 获取签名 → PUT 文件到 OSS 的 `loadtest/loadtest-placeholder.png` 路径 → 拷贝返回的可访问 URL 填入 seed 工具的 `LOADTEST_PLACEHOLDER_URL` 常量。
|
||
|
||
### 4.5 数据清理策略
|
||
|
||
```bash
|
||
# 删压测产生的写入(保留 1000 个测试账号 + 资产,下次复用)
|
||
loadgen seed cleanup --keep-baseline
|
||
|
||
# 全删(包括账号本身)
|
||
loadgen seed cleanup --full
|
||
```
|
||
|
||
**cleanup 安全校验**:
|
||
|
||
```go
|
||
const LoadtestStarID = 999900
|
||
|
||
func cleanup(db *sql.DB, starID int64) error {
|
||
if starID != LoadtestStarID {
|
||
return errors.New("safety: cleanup only accepts loadtest star_id 999900")
|
||
}
|
||
queries := []string{
|
||
"DELETE FROM asset_likes WHERE star_id = $1",
|
||
"DELETE FROM exhibitions USING fan_profiles fp WHERE exhibitions.host_profile_id=fp.id AND fp.star_id = $1",
|
||
"DELETE FROM booth_slots WHERE star_id = $1",
|
||
"DELETE FROM mint_orders WHERE star_id = $1",
|
||
"DELETE FROM crystal_transaction_records WHERE star_id = $1",
|
||
// assets / fan_profiles / users / stars: 按 --keep-baseline / --full 决定
|
||
}
|
||
for _, q := range queries {
|
||
if _, err := db.Exec(q, starID); err != nil { return err }
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 场景设计与 RPS 梯度
|
||
|
||
### 5.1 7 个场景一览
|
||
|
||
| ID | 场景 | 接口 | 依赖 | 预期瓶颈 | 基线 P95 目标 |
|
||
|---|---|---|---|---|---|
|
||
| S1 | 登录+鉴权 | `POST /auth/login` + `GET /me/profile` | mobile + password | UserService bcrypt 验密 / PG users 查询 / JWT 签发 CPU | < 200ms |
|
||
| S2 | 资产读 | `GET /assets/me/items?page=1` + `GET /assets/:id` 随机 | 预签 token + asset_ids | PG `idx_assets_owner_star` 索引 / Gateway→Dubbo→Asset 三跳 | < 100ms |
|
||
| S3 | 点赞 | `POST /social/assets/:id/like` + `DELETE` | 必须用 seed 上架的 asset_id(依附 exhibition) | PG `asset_likes` 唯一约束 / `assets.like_count` 行锁 | < 150ms |
|
||
| S4 | 资产铸造 | `POST /assets/mints/precreate` → `POST /assets/mints` | 水晶 ≥ cost + 未达 10 次硬上限 | PG 事务(扣余额+写订单) / `crystal_transaction_records` 流水 | < 500ms |
|
||
| S5 | 数据看板 | 7 个 `GET /dashboard/*` 串行 | 当前用户 token | PG 物化视图 / 多表 JOIN / Statistic 服务 | < 800ms(用户会话端到端)|
|
||
| S6 | 多维榜单 | `GET /rankings/{hot,original}?dimension={displaying,month,total}&star_id={87,88,93,999900}` | 24 种参数组合循环(2 接口 × 3 dimension × 4 star_id) | PG 排序+LIMIT / Asset 服务 Redis 缓存命中率 | < 250ms |
|
||
| S7 | 上架展位 | `POST /galleries/place` + `DELETE /galleries/slots/:slot_id/asset` | slot_index=3 留作压测槽位 | PG `exhibitions.uk_asset` 唯一 / booth_slots 状态机 | < 200ms |
|
||
|
||
### 5.2 度量定义(避免歧义)
|
||
|
||
**RPS 在本文档中的统一含义**:除非特别说明,"RPS" = **后端请求/秒(backend QPS)**,即 loadgen 实际发出的 HTTP 请求计数。
|
||
|
||
**S5 看板场景特例**:S5 一次"用户会话" = 7 个 `/dashboard/*` 串行请求。S5 的 RPS 阶梯(§5.3)以**用户会话视角**给出,对应后端 QPS = 阶梯值 × 7。例如 S5 阶梯 "20 RPS" 表示**20 个并发用户会话/秒,对应 140 backend QPS**。S5 的 P95/P99 也按"用户会话端到端"度量(即 7 个串行请求的总耗时)。
|
||
|
||
**S6 榜单场景特例**:S6 一次"用户行为" = 1 次请求(不串行)。S6 的 RPS = 后端 QPS。
|
||
|
||
**所有红线判定基于"后端 QPS 视角"**:§7.3 R1/R2/R3 的错误率和延迟红线统计的是 events.jsonl 单请求事件,**不分场景类型一律按原子请求计**。这意味着 S5 在 20 RPS(会话)= 140 QPS 时如果有 1.4 QPS 错误(5%),R1 触发。
|
||
|
||
### 5.3 4 阶段 RPS 梯度
|
||
|
||
> **⚠️ 两轮拆分(P0-1 修复)**:本节描述 4 阶段**完整 cycle 模型**。**第一轮(探索)只跑阶段 1+2**(baseline + step,225 min),目标是找到拐点;**阶段 3 稳定性 + 阶段 4 破坏性 + §5.5 混合场景全部推到第二轮(验证)**。第二轮窗口独立计算(详见 §6.3)。
|
||
>
|
||
> | 阶段 | 第一轮 | 第二轮 |
|
||
> |---|---|---|
|
||
> | 1. Baseline | ✅ 跑 | (已有数据,跳过) |
|
||
> | 2. Step Ramp-up | ✅ 跑 | (已有拐点,跳过;如修复后想对比,可重跑) |
|
||
> | 3. Soak(30min × N) | ❌ 不跑 | ✅ 跑(在已知拐点的 60% 安全水位) |
|
||
> | 4. Stress(5min × N) | ❌ 不跑 | ✅ 跑(拐点 × 2-3 倍) |
|
||
> | 混合场景 | ❌ 不跑 | ✅ 跑(按数据驱动比例) |
|
||
|
||
#### 阶段 1 · 基线 (Baseline)
|
||
- 目标:拿到"无并发干扰"的 P50/P95/P99
|
||
- 执行:每场景 1 VU × 1 RPS × 3 min
|
||
- 产出:`baseline.csv`
|
||
|
||
#### 阶段 2 · 容量阶梯 (Step Ramp-up) ← 找拐点
|
||
- 目标:每场景独立 6 阶爬升,找到错误率/P99 突破阈值的 RPS
|
||
- 每阶 2 分钟
|
||
|
||
**每场景独立的 RPS 阶梯**:
|
||
|
||
| 场景 | RPS 阶梯(每阶 2min) | 预估拐点 |
|
||
|---|---|---|
|
||
| S1 登录 | 2 → 5 → 10 → 15 → 25 → 40 | ~15 |
|
||
| S2 资产读 | 20 → 50 → 100 → 200 → 400 → 700 | ~250 |
|
||
| S3 点赞 | 5 → 10 → 20 → 40 → 80 → 150 | ~50 |
|
||
| S4 铸造 | 5 → 10 → 20 → 30 → 50 → 80 | ~30 |
|
||
| S5 看板 | 2 → 5 → 10 → 20 → 35 → 60 | ~20 |
|
||
| S6 榜单 | 20 → 50 → 100 → 200 → 400 → 700 | ~300 |
|
||
| S7 上架 | 5 → 10 → 20 → 40 → 80 → 150 | ~50 |
|
||
|
||
每场景跑到红线(错误率>5% 持续 30s 或 P99>3s 持续 30s)自动停止。
|
||
|
||
#### 阶段 3 · 稳定性 (Soak) ← 找泄漏
|
||
- 目标:取阶段 2 拐点的 60% 作为"安全水位",跑 30 分钟
|
||
- 关注:goroutine / 内存增长 / PG connections / 慢查询累积
|
||
- S1/S2/S3/S5/S6/S7 各跑一次 30min;S4 见 §5.4
|
||
|
||
#### 阶段 4 · 破坏性 (Stress) ← 验证降级
|
||
- 目标:拐点 × 2-3 倍,跑 5 分钟
|
||
- 观察:Gateway 503 / Dubbo 超时熔断 / PG 连接拒绝 / OOM Kill / 撤压后恢复时间
|
||
|
||
### 5.4 铸造场景特殊处理:每阶 reset 轮转
|
||
|
||
铸造单用户硬上限 10 次(mint_cost_config 只有 10 行):1000 用户共 10,000 次配额。
|
||
|
||
**为什么不能裸跑阶梯**(B1 自审发现):
|
||
|
||
S4 阶梯 5→10→20→30→50→80 RPS(每阶 120s),累计请求量 = (5+10+20+30+50+80) × 120 = **23,400 次** >> 10,000 配额。
|
||
|
||
如果不每阶 reset,第 50 RPS 阶(累计 8400 次 + 120s × 50 = 14,400 次)就会大面积"用户已达 10 次"业务报错;80 RPS 阶(累计 16,800 次起)几乎 100% 失败 → **R3 5xx 红线触发,但实际不是系统拐点,是配额耗尽**,拐点测不出来。
|
||
|
||
**正确节奏(每阶单独 reset)**:
|
||
|
||
```
|
||
S4 阶梯每阶 = "压 120s → 暂停 → reset → 下一阶"
|
||
|
||
阶段 1: 5 RPS × 120s = 600 req → reset
|
||
阶段 2: 10 RPS × 120s = 1200 req → reset
|
||
阶段 3: 20 RPS × 120s = 2400 req → reset
|
||
阶段 4: 30 RPS × 120s = 3600 req → reset
|
||
阶段 5: 50 RPS × 120s = 6000 req → reset
|
||
阶段 6: 80 RPS × 120s = 9600 req → reset
|
||
(每阶都在 10000 配额内,余量充足)
|
||
```
|
||
|
||
**稳定性阶段同样轮转**(取 50 RPS 安全水位):
|
||
|
||
```
|
||
[T+0] 开始压 50 RPS
|
||
[T+200s] loadgen 暂停(1000 用户 × 10 次 = 10000 / 50 RPS = 200s)
|
||
[T+201s] ssh prod 执行 reset SQL(约 5-10s)
|
||
[T+210s] 继续下一轮
|
||
...
|
||
循环 9 轮 ≈ 30 分钟
|
||
```
|
||
|
||
**Reset SQL(P0-2 修复:包装成可执行脚本 + PGPASSWORD)**:
|
||
|
||
`scenarios/s4_mint.go` 在每阶/每轮结束后**通过 ssh 远程触发 prod 上的 `mint_reset.sh`**:
|
||
|
||
```bash
|
||
# /opt/topfans/loadtest/scripts/mint_reset.sh(部署到 prod)
|
||
#!/bin/bash
|
||
set -e
|
||
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
[ -z "$PG_CONTAINER" ] && { echo "❌ 找不到 postgres 容器"; exit 1; }
|
||
export PGPASSWORD="${DB_PASSWORD:-postgres123}"
|
||
|
||
docker exec -i -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans <<'EOF'
|
||
BEGIN;
|
||
DELETE FROM user_mint_count WHERE star_id = 999900;
|
||
DELETE FROM mint_orders WHERE star_id = 999900;
|
||
UPDATE fan_profiles SET crystal_balance = 2200 WHERE star_id = 999900;
|
||
DELETE FROM crystal_transaction_records WHERE star_id = 999900;
|
||
DELETE FROM assets WHERE star_id = 999900 AND name LIKE 'loadtest_mint_%';
|
||
COMMIT;
|
||
-- 序列不重置(mint_orders 主键是 UUID;assets 新建的会被上面 DELETE 清掉,序列空洞 OK)
|
||
EOF
|
||
echo "✅ mint reset 完成"
|
||
```
|
||
|
||
`scenarios/s4_mint.go` 调用方式(伪代码):
|
||
```go
|
||
func resetMintData() error {
|
||
cmd := exec.Command("ssh", prodSSH, "bash /opt/topfans/loadtest/scripts/mint_reset.sh")
|
||
return cmd.Run()
|
||
}
|
||
```
|
||
|
||
**铸造场景资产命名约定(重要)**:
|
||
|
||
S4 压测调用 `POST /assets/mints` 时,**请求 body 的 `name` 字段必须以 `loadtest_mint_` 开头**(如 `loadtest_mint_30000001_round3_42`),这样 reset SQL 的 `name LIKE 'loadtest_mint_%'` 才能精准清理,不会误删 seed 阶段预铸的 `loadtest_asset_` 资产。
|
||
|
||
`scenarios/s4_mint.go` 构造请求时强制按此前缀生成 name。
|
||
|
||
### 5.5 不做的(YAGNI)
|
||
|
||
- **混合场景**:第一轮不做。理由:早期项目业务比例还没成型,凭直觉给比例会产出"看似科学但实际是猜的"数字。等单场景拐点出齐,按业务相对调用频率回推 DAU 上限。
|
||
- **WebSocket(AI Chat)**:独立的长连接压力模型,本轮不压。
|
||
- **活动榜单 / 星册接口**:聚焦核心 7 场景。
|
||
|
||
### 5.6 场景间隔离缓冲与总时长(B8 自审修复)
|
||
|
||
每个场景跑完不能立刻切下一个。必须等:
|
||
|
||
| 缓冲项 | 最少耗时 | 原因 |
|
||
|---|---|---|
|
||
| TIME_WAIT 释放 | 120-240s | Linux `tcp_fin_timeout=60`,200 VU 的 TIME_WAIT 堆积要分钟级散去;不等会让下一场景压力机连接抖动 |
|
||
| 连接池回收 | 60-120s | Dubbo client / Go http.Transport idle conn + PG `idle_in_transaction_session_timeout`;前场景占用的 PG 连接要释放完 |
|
||
| Redis 缓存散热 | 60-300s | 前场景预热的热点 key 退场;不等会让下一场景"缓存命中率虚高" |
|
||
| PG WAL 消化 | 30-60s | 前场景的写操作 WAL 还在异步落盘 |
|
||
| cleanup SQL | 30-60s | S4 还要 §5.4 reset |
|
||
| 人眼判断 / 短报告 | 5-10min | 看本场景是否需要进入下一场景,还是要重跑 |
|
||
|
||
**结论**:每场景之间至少 **8-12 min 缓冲**。
|
||
|
||
修正后的第一轮总时长(终审修复,明细可对账):
|
||
|
||
| 阶段 | 单次耗时 | 次数 | 小计 | 累计 |
|
||
|---|---|---|---|---|
|
||
| 开场(seed + monitor 启动) | 2 min | 1 | 2 min | 2 min |
|
||
| Baseline(每场景 1 RPS) | 3 min | 7 场景 | 21 min | 23 min |
|
||
| Baseline 场景间短 buffer | 1 min | 6 间隔 | 6 min | 29 min |
|
||
| 阶梯(6 阶 × 2min/阶) | 12 min | 7 场景 | 84 min | 113 min |
|
||
| 阶梯场景间长 buffer | 15 min | 6 间隔 | 90 min | 203 min |
|
||
| 收尾(cleanup + verify + 释放) | 22 min | 1 | 22 min | 225 min |
|
||
| **合计** | | | | **225 min = 3h 45min** |
|
||
|
||
→ §6.1 第一轮窗口:**02:00-06:00**(含 preflight 在 01:30-02:00 之外)。第二轮稳定性 + 破坏性约 4 小时(含混合场景)。
|
||
|
||
`loadgen run --scenarios=S1,S2,S3,S4,S5,S6,S7 --inter-scenario-pause=15m` 自动在场景间插入 pause。
|
||
|
||
---
|
||
|
||
## 6. 执行计划与时间盒
|
||
|
||
### 6.1 第一轮(探索压测)
|
||
|
||
#### Day 1:环境与数据准备(白天,6-7 小时,分两个时段)
|
||
|
||
| 时段 | 动作 |
|
||
|---|---|
|
||
| 上午 1h | ssh prod 跑 `\d+ users fan_profiles assets asset_likes exhibitions booth_slots mint_cost_config` 对照本地,生成 `prod-vs-local-schema-diff.md` |
|
||
| 上午 2h | 编写 `seed/` Go 工具,本地 docker-compose 联调跑通 |
|
||
| **下午 14:00-14:05(业务低峰,P0-5)** | **应用 §2.1 方案 A(必做)**:改 `docker-compose.prod.yml` 把 `POSTGRES_MAX_CONNECTIONS` 从 100 改到 50,`docker-compose -f docker-compose.prod.yml restart postgres`(约 30s 停机)。验证:`docker exec ... psql -c "SHOW max_connections;"` 返回 50 |
|
||
| 下午 1h | 阿里云控制台开 ECS(**华东 1 杭州 同地域**,4G/2C 按量付费) |
|
||
| 下午 1h | 编译 `seed` + `loadgen` 二进制;scp 到压力机 + scp seed 二进制和 `mint_reset.sh` 到 prod `/opt/topfans/loadtest/` |
|
||
| **晚上 23:00(次低峰)** | **预演 dry run**:用 `--monitor=off` 跑 5 min mini baseline(10 RPS × 1min × 7 场景),验证 preflight/seed/cleanup 链路通畅 |
|
||
|
||
#### Day 1 当晚 / 02:00 - 06:00(即 Day 2 凌晨):第一次正式压测窗口
|
||
|
||
> **P0-3 对账:本表逐分钟与 §5.6 总时长表对账**。02:00 开场到 05:45 收尾结束 = 225 min;06:00 为余量缓冲(容纳意外延迟、监控停止、scp 报告等收尾动作)。
|
||
|
||
| 时间 | 持续 | 动作 |
|
||
|---|---|---|
|
||
| 01:30 | 25 min | preflight 检查(详见 §8.5;含 §8.2 防线 1 的 7 项 + max_connections=50 验证) |
|
||
| 01:55 | 5 min | pg_dump 备份完成(pg_dump 跑了 ~3 min,加 buffer) |
|
||
| 02:00 | 1 min | ssh prod 跑 `loadgen seed --prod` |
|
||
| 02:01 | 1 min | ssh prod 启动 `monitor.sh` 后台采样 |
|
||
| 02:02 | 21 min | 压力机跑 **baseline**(每场景 1 RPS × 3min,7 场景,**场景间 1min 短 buffer**) |
|
||
| 02:29 | 84 min | 压力机跑 **step 阶梯**(每场景 6 阶 × 2min,7 场景) |
|
||
| 02:29-05:35 | +90 min | **场景间 15min 长 buffer × 6 间隔**(与阶梯交错执行;阶梯第 1 场景跑完 12min → 15min buffer → 第 2 场景 → ... → 第 7 场景结束) |
|
||
| 05:35 | 5 min | ssh prod 停 monitor.sh + 收尾监控 |
|
||
| 05:40 | 5 min | `loadgen seed cleanup --keep-baseline` |
|
||
| 05:45 | 5 min | `loadgen verify`(详见 §8.5) |
|
||
| 05:50 | 10 min | 压力机拉回报告目录 + 释放 ECS(如 1 周内不再压) |
|
||
| **06:00** | — | **窗口结束,prod 完全可用** |
|
||
|
||
> **加和验证**:开场 2 + baseline 27 + step+buffer 174(84+90 交错) + 收尾 25 = **228 min ≈ §5.6 表 225 min**(3 min 容差在 §6.5 允许内)。窗口实际 240 min,留 12 min 应急余量。
|
||
|
||
#### Day 3:分析与决策
|
||
|
||
- 读取 `report-round1.md`
|
||
- Review 会议:哪些瓶颈是**配置可修**(bcrypt cost、PG max_connections、连接池),哪些是**代码必改**(N+1、缺索引、锁粒度),哪些**接受现状**
|
||
|
||
### 6.2 修复期(Day 3 - Day 14)
|
||
|
||
如有必要,按 review 结论改代码:调 bcrypt cost、加索引、加缓存、调 Dubbo 超时。每个修复**打 tag 部署一次**便于第二轮对比。
|
||
|
||
### 6.3 第二轮(验证压测)
|
||
|
||
**触发条件**:第一轮报告交付 + 团队 review 后,**决定是否修复瓶颈**:
|
||
- 如果第一轮拐点已经满足业务预期 → **第二轮可跳过**
|
||
- 如果改了代码/配置 → 第二轮只压**改动影响的场景**
|
||
|
||
**第二轮内容**(详见 §5.3 拆分表):
|
||
- 阶段 3 稳定性:30min × 修改了的场景
|
||
- 阶段 4 破坏性:5min × 修改了的场景
|
||
- 混合场景:15min(按 §5.5 数据驱动的比例)
|
||
|
||
**第二轮时长估算**(修改了 N 个场景的情况):
|
||
- 稳定性:30min × N + 15min × (N-1) 缓冲
|
||
- 破坏性:5min × N + 5min × (N-1) 缓冲
|
||
- 混合:15min + 30min 收尾
|
||
- N=3 时约 **3h**;N=7(全部场景)时约 **6.5h**(要拆 2 个凌晨窗口)
|
||
|
||
仍在凌晨 02:00-06:00 窗口(必要时第二天 02:00-06:00 续)。
|
||
|
||
⚠️ **JWT 重签(终审修复 C1)**:`pkg/jwt` 的 `TokenExpiration = 7 * 24 * time.Hour`(7 天)。第一轮 seed 时签的 token 7 天后过期。如第二轮在第一轮 7 天后才跑,必须**第二轮开压前 30 min 重跑**:
|
||
|
||
```bash
|
||
# prod 服务器上
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
loadgen seed --reset-tokens --jwt-secret="${JWT_SECRET}" --db-host=localhost --db-name=topfans
|
||
# --reset-tokens 仅重新签发 users.csv 里 1000 个 token,不动数据
|
||
```
|
||
|
||
### 6.4 时间承诺
|
||
|
||
- 第一轮端到端:从今天起 **5 个工作日内**完成报告
|
||
- 第二轮端到端(如触发):第一轮报告交付后 **2 周内**
|
||
|
||
### 6.5 中止/回退原则
|
||
|
||
| 情况 | 动作 |
|
||
|---|---|
|
||
| 压测期间 prod 错误率持续 > 10% | loadgen 自动 SIGINT,ssh 跑 `emergency-stop.sh` |
|
||
| 监控仪表提示磁盘满(< 5GB) | 立即 stop |
|
||
| 用户半夜投诉 | 立即 stop,第二天 review 是否窗口选错 |
|
||
| 任何阶段 reset SQL 失败 | 不进入下一阶段,保留现场 |
|
||
| preflight 任何一项 fail | 拒绝启动主压测 |
|
||
|
||
---
|
||
|
||
## 7. 监控指标与判停红线
|
||
|
||
### 7.1 监控分层(4 个采集源 × 3 个聚合粒度)
|
||
|
||
```
|
||
压力机侧(loadgen 自身)
|
||
├ 单请求级:start_ts/end_ts/rt_ms/status/scenario/vu_id → events.jsonl
|
||
├ 10 秒滑窗:actual_rps/p50/p95/p99/error_rate → 10s.csv + stderr
|
||
└ 全局 HDR 直方图:每场景 .bin 文件,事后还原任意百分位
|
||
|
||
被压机侧 · monitor.sh(lite 模式,默认)
|
||
├ docker stats(每 5s,所有容器 CPU/MEM/NET/IO)
|
||
├ pg_stat_activity(每 5s)
|
||
├ pg_stat_statements top 10(每 30s)
|
||
├ redis INFO(每 5s)
|
||
└ uptime + df -h(每 5s)
|
||
|
||
被压机侧 · Prometheus(full 模式,可选)
|
||
├ cadvisor(容器维度,scrape 5s)
|
||
├ node-exporter(OS 层)
|
||
├ postgres-exporter(连接/lock/cache hit/replication)
|
||
└ redis-exporter(内存/命令/客户端)
|
||
|
||
业务侧探针(可选)
|
||
└ Gateway /health 每 1s 探活(外部 SLA 视角)
|
||
```
|
||
|
||
### 7.2 实时仪表盘(stderr 行模式)
|
||
|
||
每秒一行:
|
||
|
||
```
|
||
[02:15:34] S3 like | target= 50 actual= 48.3 | p50=42 p95=180 p99=520 | err=0.2% | vu=5 conn_err=0
|
||
```
|
||
|
||
每分钟汇总:
|
||
|
||
```
|
||
═══════════════════════════════════════════════════════════════
|
||
[02:16:00] SCENARIO=S3 STAGE=stage3-20rps ELAPSED=01:58
|
||
Requests: 5,820 (97.0/s avg)
|
||
Errors: 12 (0.2%)
|
||
Latency p50: 42ms p95: 198ms p99: 612ms max: 2.1s
|
||
Connection: active=5 refused=0 timeout=0
|
||
Status: 2xx=99.8% 4xx=0.1% 5xx=0.1%
|
||
═══════════════════════════════════════════════════════════════
|
||
```
|
||
|
||
### 7.3 6 维红线(任一触发即停)
|
||
|
||
| # | 红线 | 触发条件 | 数据源 | 周期 |
|
||
|---|---|---|---|---|
|
||
| R1 | 客户端错误率 | `error_rate > 5%` 持续 30s | loadgen 滑窗 | 5s |
|
||
| R2 | 客户端 P99 | `p99 > 3000ms` 持续 30s | loadgen HDR | 5s |
|
||
| R3 | 5xx 比例 | `5xx_rate > 10%` 持续 10s | loadgen status | 5s |
|
||
| R4 | PG 连接数 | `pg_active > 85`(max 100 的 85%) | monitor.sh + pg_stat_activity | 5s |
|
||
| R5 | 磁盘空间 | `disk_free < 5GB`(B9 修复:原 2GB 太低,PG WAL 在写密集场景会快速膨胀 2-3GB) | monitor.sh + df -h | 30s |
|
||
| R6 | 容器 OOM | `docker events --filter event=oom` 收到事件 **或** `docker inspect --format='{{.State.OOMKilled}}'` 返回 true **或** 容器 RestartCount 较基线增加 | monitor.sh + docker events 流式订阅 | 1s(事件触发)|
|
||
|
||
⚠️ **不要用 `ExitCode=137` 检测 OOM**(自审 B5):
|
||
- 137 = 128 + SIGKILL,**`docker stop` 超时也会触发**,无法区分 OOM/手动 kill
|
||
- `restart: always` 把容器自动拉起后,`docker inspect` 看到的是新实例的 ExitCode=0,**OOM 痕迹消失**
|
||
- 正确做法:`docker events --filter event=oom` 提供实时事件流;`State.OOMKilled` 字段是 docker daemon 直接记录的 OOM 标志
|
||
|
||
`lib/circuit.go` 主循环每 5s 检查;触发后向主进程发 SIGINT,loadgen 优雅退出。
|
||
|
||
### 7.4 Grafana 4 面板(--monitor=full)
|
||
|
||
| 面板 | 内容 |
|
||
|---|---|
|
||
| 1. 整机概览 | CPU / Memory / Network / Disk IO 时序 + load1/5/15 |
|
||
| 2. 容器维度 | 每容器 CPU% / Memory 对比限额 + 重启次数(捕捉 OOM) |
|
||
| 3. PG 健康 | active connections vs max / longest transaction / cache hit ratio / locks / WAL 增长 |
|
||
| 4. 业务指标 | loadgen RPS / Error / P99(Prometheus 从 loadgen `:9091/metrics` pull) |
|
||
|
||
loadgen 暴露 `:9091/metrics`,被 Prometheus pull。
|
||
|
||
### 7.5 报告生成(事后)
|
||
|
||
```bash
|
||
loadgen report --input ./reports/run-20260613-0200/ --output ./report.md
|
||
```
|
||
|
||
自动:
|
||
- 从 `*.hdr` 还原任意百分位(**注**:HdrHistogram precision=3 时 P99 以下可信,**P99.9+ 误差 10-30%**,仅作参考;如需准确 P99.9,preflight 阶段将 precision 提到 4-5 位但内存 4×)
|
||
- `gonum/plot` 画每场景 RPS-Latency-Error 三联图(SVG)
|
||
- 表格列出每场景拐点 RPS
|
||
- 摘录 prod 监控的高负载片段
|
||
- 自动写"建议"节:哪些场景接近瓶颈、哪些有空间
|
||
|
||
---
|
||
|
||
## 8. 风险控制与回滚
|
||
|
||
### 8.1 崩溃形态与恢复时间矩阵
|
||
|
||
| # | 形态 | 概率 | 影响 | 自动恢复 | 恢复时间 | 数据风险 |
|
||
|---|---|---|---|---|---|---|
|
||
| 1 | 容器 OOM Killed | 🔴 高 | 单服务短时不可用 | ✅ `restart: always` | 10-30s | 无 |
|
||
| 2 | PG 连接打满 100 | 🟡 中 | 整站新请求拒绝 | ✅ idle 释放 | 停压后 30s | 无 |
|
||
| 3 | PG 慢查询雪崩 / 死锁 | 🟡 中 | 整站 hang | ⚠️ 半自动 | 30-90s | 无 |
|
||
| 4 | TIME_WAIT 端口耗尽 | 🟡 中 | 新连接失败 | ✅ 60s 释放 | 60s | 无 |
|
||
| 5 | 磁盘满 | 🟢 低 | 整站 5xx | ❌ 手动 rm logs | 2-5min | 极小 |
|
||
| 6 | 整机 OOM(PG 进程被 cgroup kill) | 🟡 中(**原 🟢 错算,§2.1 PG 400M/100conn 冲突让概率提高**) | 整站宕机 | ✅ + WAL replay | 60-120s | 无 |
|
||
| 7 | 数据污染真实数据 | 🔴 极低 | 业务异常 | ❌ 备份还原 | 5-10min | **中-高** |
|
||
| 8 | 阿里云硬件故障 | ⚪ 极罕 | 数据丢失 | ❌ 快照还原 | 30min-1h | 高 |
|
||
| 9 | **压力机自身 GC 暴涨**(B9 新增) | 🟡 中 | P99 数据失真,记的延迟里有 30-50% 是 loadgen 自己 | ❌ 需重测 | 重测整轮 | 无(但结果作废) |
|
||
| 10 | **PG WAL 写满**(B9 新增) | 🟡 中(200 VU × 30min 点赞写 5w 行) | 整库变只读 | ❌ checkpoint + rm 旧 WAL | 5-15min | 无 |
|
||
| 11 | **Docker bridge NAT 表满**(B9 新增) | 🟢 低(200 VU × 30min × 无 keep-alive ≈ 36w NAT 条目) | 新连接失败 | ✅ keep-alive 缓解 | 立即(开 keep-alive) | 无 |
|
||
| 12 | **PG 锁等待雪崩**(B9 新增) | 🟡 中(S3 点赞行锁 + S7 唯一约束) | 假装连接耗尽,根因是锁 | ❌ pg_terminate_backend 杀长事务 | 30-90s | 无 |
|
||
| 13 | **SSH tunnel 断开监控失效**(B9 新增) | 🟡 中(凌晨阿里云抖动) | R4/R5/R6 静默失效 | ⚠️ autossh 自愈 | 5-30s | 无 |
|
||
|
||
**新增 R7 红线(压源自检)**:
|
||
|
||
| R7 | 压源自身延迟漂移 | loadgen 内部计算 `client_p99_自测 > 0.3 × target_p99` 持续 30s | loadgen 内部钩子 | 5s | 触发说明压力机已成为瓶颈,数据不可信,需重测或上分布式压源 |
|
||
|
||
### 8.2 三道熔断防线
|
||
|
||
**防线 1:压测前必做(不做就不开压)**
|
||
|
||
> **⚠️ 容器名注意(B7 自审修复)**:以下命令中的 `topfans-postgres` 来自 `docker-compose.prod.yml:41` 的 `container_name:` 字段,但实际启动后名字可能因 docker-compose 版本/项目目录前缀不同而异(如 `topfans_postgres_1`)。**所有脚本应用动态查找代替硬编码**:
|
||
> ```bash
|
||
> PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
> ```
|
||
> 下文示例为可读性保留 `topfans-postgres` 名称,**实施时统一替换为 `$PG_CONTAINER`**。
|
||
|
||
> **⚠️ PGPASSWORD 注入(终审修复)**:`docker exec ... psql -U postgres` 在容器内仍走 `pg_hba.conf` 鉴权,prod 配置默认要求密码(`.env.prod` 里 `DB_PASSWORD=postgres123`)。所有 psql 命令前必须 `export PGPASSWORD`,否则鉴权失败。
|
||
|
||
```bash
|
||
# 脚本头部统一注入(防线 1 / restore / snapshot / reset SQL 共用)
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
[ -z "$PG_CONTAINER" ] && { echo "❌ 找不到 postgres 容器"; exit 1; }
|
||
export PGPASSWORD="${DB_PASSWORD:-postgres123}" # 与 .env.prod 一致
|
||
|
||
# 数据库逻辑备份
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" pg_dump -U postgres -d topfans \
|
||
-f /opt/topfans/backups/pre-loadtest-$(date +%Y%m%d-%H%M).sql
|
||
|
||
# 阿里云控制台拍快照(5-10 分钟,手动操作)
|
||
# ECS → 实例详情 → 云盘 → 创建快照
|
||
|
||
# 磁盘检查
|
||
df -h | grep -E "/$|/opt" # 需要 ≥ 15GB 空闲(终审修复:原 10GB 在 30min soak 场景 WAL 膨胀下余量不够)
|
||
|
||
# PG 连接基线
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans \
|
||
-c "SELECT count(*) FROM pg_stat_activity;"
|
||
|
||
# PG 内存约束自检(B4 自审修复 + 终审 #6 澄清)
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans \
|
||
-c "SHOW max_connections;"
|
||
# 如果显示 100 但容器 limit=400M,preflight 报错并退出,提示运维手动跑 §2.1 方案 A
|
||
```
|
||
|
||
**防线 2:压测中自动熔断**
|
||
|
||
loadgen 6 维红线(详见 §7.3)+ monitor.sh 旁路监控。任一红线触发,loadgen 主进程收 SIGINT 优雅退出。
|
||
|
||
**防线 3:手动一键灭火(`emergency-stop.sh`)**
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# /opt/topfans/loadtest/recover/emergency-stop.sh
|
||
|
||
pkill -9 loadgen 2>/dev/null
|
||
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
export PGPASSWORD="${DB_PASSWORD:-postgres123}"
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans -c "
|
||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||
WHERE state != 'idle' AND now() - query_start > interval '10 seconds'
|
||
AND usename = 'postgres';
|
||
"
|
||
|
||
cd /opt/topfans/docker
|
||
docker-compose -f docker-compose.prod.yml restart
|
||
|
||
sleep 30
|
||
curl -f http://localhost:8080/health || echo "⚠️ Gateway 仍未恢复"
|
||
```
|
||
|
||
### 8.3 备份兜底(唯一不可恢复路径的保险)
|
||
|
||
```bash
|
||
# /opt/topfans/loadtest/recover/restore-from-backup.sh
|
||
# 用法:bash restore-from-backup.sh /opt/topfans/backups/pre-loadtest-20260613-0200.sql
|
||
|
||
BACKUP_FILE=$1
|
||
[ -f "$BACKUP_FILE" ] || { echo "备份文件不存在"; exit 1; }
|
||
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
export PGPASSWORD="${DB_PASSWORD:-postgres123}"
|
||
|
||
# 停所有应用层(动态查找 topfans 相关容器)
|
||
docker ps --filter 'name=topfans-' --format '{{.Names}}' \
|
||
| grep -v postgres | grep -v redis | xargs -r docker stop
|
||
|
||
# 删库重建
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -c "DROP DATABASE topfans;"
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -c "CREATE DATABASE topfans;"
|
||
|
||
# 还原
|
||
docker exec -i -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans < "$BACKUP_FILE"
|
||
|
||
# 启动
|
||
cd /opt/topfans/docker
|
||
docker-compose -f docker-compose.prod.yml --profile prod up -d
|
||
|
||
sleep 30
|
||
curl http://localhost:8080/health
|
||
```
|
||
|
||
实测还原时间:~150MB DB → **5-8 分钟**。
|
||
|
||
### 8.4 测试数据污染的额外保护
|
||
|
||
| 层 | 机制 |
|
||
|---|---|
|
||
| 1 隔离 star_id | 所有写入强制带 `star_id=999900`,与真实业务 star_id=87~95 物理隔离 |
|
||
| 2 cleanup 强制 WHERE | 所有清理 SQL 必须含 `WHERE star_id=999900`,代码层 reject 没有 WHERE 的语句 |
|
||
| 3 本地完整跑一遍 | 用 docker-compose.local.yml 跑一次 seed + 压测 + cleanup,验证不影响 star_id≠999900 数据 |
|
||
|
||
### 8.5 preflight 与 verify
|
||
|
||
#### Preflight(开压前自动检查)
|
||
|
||
```
|
||
loadgen preflight --target http://101.132.250.62:8080 --prod-ssh root@101.132.250.62
|
||
```
|
||
|
||
7 项检查:
|
||
|
||
```
|
||
✓ ① Gateway /health 返回 200
|
||
✓ ② SSH 到 prod 成功
|
||
✓ ③ pg_dump 备份文件存在 (size > 50MB)
|
||
✓ ④ 阿里云快照在 24h 内创建(人工确认或 ECS API 检查)
|
||
✓ ⑤ prod 磁盘空闲 > 10GB
|
||
✓ ⑥ users.csv 加载 OK,1000 行
|
||
✓ ⑦ JWT_SECRET 与 prod 一致(用 1 个 token 调 /me/profile 验证返回 200)
|
||
─────────────────────────────────────────
|
||
ALL CHECKS PASSED — 可以开压
|
||
```
|
||
|
||
任一项 fail,loadgen 拒绝启动主压测。
|
||
|
||
#### Verify(压测后自动验证)
|
||
|
||
```
|
||
loadgen verify --prod-ssh root@101.132.250.62
|
||
```
|
||
|
||
检查:
|
||
|
||
- diff pre-snapshot vs post-snapshot(真实用户数据未变)
|
||
- `SELECT count(*) FROM mint_orders WHERE star_id != 999900 AND created_at > <压测开始时间>` == 0
|
||
- PG 连接数已回落到基线
|
||
- 所有容器 Restart Count 未增加(除非压崩)
|
||
- 磁盘空闲恢复(cleanup 后日志已清)
|
||
|
||
任一项 fail → 走 emergency-stop + restore。
|
||
|
||
### 8.6 prod 状态打点
|
||
|
||
```bash
|
||
# pre-test-snapshot.sh / post-test-snapshot.sh
|
||
PG_CONTAINER=$(docker ps --filter 'name=postgres' --format '{{.Names}}' | grep -v exporter | head -1)
|
||
export PGPASSWORD="${DB_PASSWORD:-postgres123}"
|
||
docker exec -e PGPASSWORD="$PGPASSWORD" "$PG_CONTAINER" psql -U postgres -d topfans -c "
|
||
SELECT
|
||
(SELECT count(*) FROM users) AS users,
|
||
(SELECT count(*) FROM assets) AS assets,
|
||
(SELECT count(*) FROM asset_likes) AS likes,
|
||
(SELECT count(*) FROM mint_orders) AS mints,
|
||
(SELECT sum(crystal_balance) FROM fan_profiles WHERE star_id != 999900) AS real_user_crystal_total,
|
||
NOW() AS snapshot_at;
|
||
"
|
||
```
|
||
|
||
**`real_user_crystal_total` 若变化 = 数据污染告警**。
|
||
|
||
### 8.7 临时关闭外部告警(如有)
|
||
|
||
```bash
|
||
# D6 自审修复:项目当前未确认是否接入告警系统,本节为条件式
|
||
# 1. 检查是否有 webhook 配置
|
||
grep -rEn "webhook|alert|dingding|wechat|feishu|lark|slack" \
|
||
/opt/topfans/docker/.env.prod /opt/topfans/docker/docker-compose.prod.yml 2>/dev/null
|
||
|
||
# 2a. 如果有 webhook,临时改为本地黑洞地址
|
||
# 例如 sed -i.bak 's|ALERT_WEBHOOK=.*|ALERT_WEBHOOK=http://127.0.0.1:9999/null|' .env.prod
|
||
# 完成后 systemctl 或 docker-compose restart 让配置生效
|
||
# 压测结束后 mv .env.prod.bak .env.prod 恢复
|
||
|
||
# 2b. 如果 grep 没有命中(项目当前状态),本节跳过
|
||
|
||
# 3. 通知运维:今晚 02:00-06:00 压测,告警别理
|
||
```
|
||
|
||
### 8.8 不可触碰的红线(人工铁律)
|
||
|
||
| 红线 | 处置 |
|
||
|---|---|
|
||
| 没 pg_dump 就不开压 | preflight 检查 |
|
||
| 白天/工作时段不开压 | 时间窗口 02:00-06:00 |
|
||
| cleanup SQL 没有 `WHERE star_id=999900` 不执行 | seed/cleanup 代码层强校验 |
|
||
| 压测期间发现真实用户投诉立刻停 | emergency-stop |
|
||
| 第一轮没出报告前不开第二轮 | 防止盲压 |
|
||
|
||
### 8.9 关于 IP 白名单(决定不做)
|
||
|
||
考虑过在 gateway 加临时 middleware,让真实用户走 503 维护页只让压力机 IP 进入。**决定不做**,理由:
|
||
|
||
- 早期项目凌晨 02:00-06:00 DAU 接近 0,撞上概率极低
|
||
- 加 middleware 要重新部署 gateway,反而引入新风险
|
||
- 万一压测被熔断了,真实用户却被白名单拦了 = 把 prod 自己锁死
|
||
|
||
靠时间窗口 + 6 维红线自动判停就够。
|
||
|
||
---
|
||
|
||
## 9. 产出物清单
|
||
|
||
### 9.1 第一轮交付
|
||
|
||
```
|
||
docs/loadtest/round1/
|
||
├── prod-vs-local-schema-diff.md # Day 1 schema 对照报告
|
||
├── seed-validation.log # Day 1 联调记录
|
||
├── report-round1.md # 主报告(模板见 §9.2)
|
||
├── monitoring/
|
||
│ ├── sample-YYYYMMDD-0200.log # 服务端采样原始
|
||
│ ├── docker-stats.csv # CPU/内存时序
|
||
│ ├── pg-slow-queries.txt # pg_stat_statements top 20
|
||
│ └── grafana-screenshots/ # 仅 --monitor=full 时
|
||
└── raw-data/
|
||
├── baseline-*.json # 每场景基线
|
||
├── step-*.json # 每场景阶梯
|
||
└── hdr-histograms.bin # 二进制 HDR(可重放)
|
||
```
|
||
|
||
### 9.2 `report-round1.md` 模板
|
||
|
||
> ⚠️ 以下数字均为**格式示例**,不是真实压测结果。真实数字由 `loadgen report` 自动从 raw data 生成。
|
||
|
||
```markdown
|
||
# 第一轮压测报告 - 2026-MM-DD
|
||
|
||
## 摘要(示例值)
|
||
- 总耗时: 1h45min
|
||
- 测试场景: 7
|
||
- 拐点小结(示例): S1=15RPS, S2=350RPS, S3=60RPS, S4=25RPS, S5=18RPS, S6=400RPS, S7=70RPS
|
||
- 主要瓶颈(示例): bcrypt cost (S1), PG aggregation (S5)
|
||
- 建议: 优先改 S1/S5
|
||
|
||
## 各场景详情
|
||
(每场景一节:RPS 曲线、错误率曲线、Top 慢查询、容器 CPU/内存峰值)
|
||
|
||
## 系统观察
|
||
- 整机 CPU 峰值: 95%(发生在 S2=700 RPS 时)
|
||
- PG connections 峰值: 78/100(发生在 S5=60 RPS 时)← 接近上限
|
||
- Redis 内存峰值: 180MB / 256M
|
||
- 容器 OOM: 无
|
||
|
||
## 后续行动
|
||
- [ ] 改 bcrypt cost 10 → 8(S1 拐点提高 ~3x)
|
||
- [ ] 给 statistic_mv 加 refresh 调度(S5 拐点提高 ~2x)
|
||
- [ ] 调 PG max_connections 100 → 150(S5 紧迫)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 不在范围(YAGNI)
|
||
|
||
| 项 | 不做的原因 |
|
||
|---|---|
|
||
| 分布式追踪(OpenTelemetry / Jaeger) | 早期摸底用不上 |
|
||
| 业务级埋点(每接口单独 metrics) | 加业务代码代价大 |
|
||
| 全链路日志聚合(ELK) | Loki/Promtail 都嫌重 |
|
||
| APM(New Relic / DataDog) | 商业产品,超出范围 |
|
||
| TUI 实时仪表盘 | stderr 行模式更可靠 |
|
||
| 第一轮混合场景 | 业务比例还没成型,凭直觉给比例没意义 |
|
||
| WebSocket 压测 | 独立的长连接模型,本轮不做 |
|
||
| IP 白名单 middleware | 凌晨窗口 DAU 接近 0,引入新风险得不偿失 |
|
||
| 端到端业务正确性测试 | E2E 的工作 |
|
||
| 前端性能测试 | 范围外 |
|
||
| 安全/渗透测试 | 范围外 |
|
||
| 活动榜单/星册接口 | 聚焦核心 7 场景 |
|
||
|
||
---
|
||
|
||
## 11. 后续步骤
|
||
|
||
1. **本设计文档复核** ← 当前节点
|
||
2. 调用 `writing-plans` skill 生成实施计划
|
||
3. Day 1: schema diff + seed 工具开发 + 压力机准备
|
||
4. Day 2: 凌晨 02:00-06:00 第一次正式压测
|
||
5. Day 3: review 报告与决策
|
||
6. 修复期(可选)
|
||
7. 第二轮压测(验证)
|
||
|
||
---
|
||
|
||
## 附录 A:术语表
|
||
|
||
| 术语 | 含义 |
|
||
|---|---|
|
||
| **VU** | Virtual User,并发虚拟用户数 |
|
||
| **RPS** | Requests Per Second,每秒请求数 |
|
||
| **P50/P95/P99** | 延迟分位数(中位数 / 95% / 99% 的请求在此值以下完成) |
|
||
| **HDR** | High Dynamic Range Histogram,高精度低开销直方图,用于准确算百分位 |
|
||
| **拐点 RPS** | 错误率/P99 突破阈值前的最大稳态 RPS |
|
||
| **安全水位** | 拐点 × 60%,作为稳定性测试的目标 RPS |
|
||
| **Soak Test** | 稳定性测试,长时间维持中等负载找泄漏 |
|
||
| **Stress Test** | 破坏性测试,超出极限验证降级与恢复 |
|
||
| **MTTR** | Mean Time To Recover,平均恢复时间 |
|
||
| **影子表** | 与生产表结构相同但物理分离的副本(本方案用 `star_id` 隔离代替) |
|
||
|
||
---
|
||
|
||
## 附录 B:被测接口路径速查
|
||
|
||
| 场景 | Method | 路径 | 主要 body/query |
|
||
|---|---|---|---|
|
||
| S1 | POST | `/api/v1/auth/login` | `{mobile, password}` |
|
||
| S1 | GET | `/api/v1/me/profile` | (Header: Authorization) |
|
||
| S2 | GET | `/api/v1/assets/me/items?page=1&page_size=20` | |
|
||
| S2 | GET | `/api/v1/assets/:asset_id` | |
|
||
| S3 | POST | `/api/v1/social/assets/:asset_id/like` | (需 asset 在 exhibition 中) |
|
||
| S3 | DELETE | `/api/v1/social/assets/:asset_id/like` | |
|
||
| S4 | POST | `/api/v1/assets/mints/precreate` | `{material_url, name, info, ...}` |
|
||
| S4 | POST | `/api/v1/assets/mints` | `{order_id}` |
|
||
| S5 | GET | `/api/v1/dashboard/today-overview` | |
|
||
| S5 | GET | `/api/v1/dashboard/income-curve` | |
|
||
| S5 | GET | `/api/v1/dashboard/exhibition-summary` | |
|
||
| S5 | GET | `/api/v1/dashboard/like-income-by-level` | |
|
||
| S5 | GET | `/api/v1/dashboard/top-assets` | |
|
||
| S5 | GET | `/api/v1/dashboard/level-distribution` | |
|
||
| S5 | GET | `/api/v1/dashboard/upgrade-progress` | |
|
||
| S6 | GET | `/api/v1/rankings/hot?dimension={displaying,month,total}&star_id={87,88,93,999900}&page=1&page_size=10` | dimension 值已通过代码核实(ranking_service.go:63-72),仅这 3 个 |
|
||
| S6 | GET | `/api/v1/rankings/original?...` | |
|
||
| S7 | POST | `/api/v1/galleries/place` | `{slot_id, asset_id}` |
|
||
| S7 | DELETE | `/api/v1/galleries/slots/:slot_id/asset` | |
|
||
|
||
---
|
||
|
||
*— 设计文档结束 —*
|