性能测试调研报告
调研日期:2026-02-07 | 状态:Planning
Part 1 — 现状诊断:当前性能测试的问题
现有文件
仅有 1 个文件:backend/tests/performance/payment-load.test.js
结论:该测试完全是模拟的,不测试任何真实代码,价值接近于零。
核心问题分析
| 问题 |
详细说明 |
| 不测真实代码 |
所有"操作"都是 setTimeout(randomDelay),不连数据库、不调 Express 路由、不走中间件链。跟支付代码没有任何关系。 |
| 结果可预测 |
simulateDbOperation(10, 50) → 最大 50ms;断言 P95 < 500ms 永远通过。这是在测试 setTimeout 和 Math.random 是否工作。 |
| 假错误率 |
Math.random() < 0.0001 硬编码 0.01% 失败率,不反映真实系统的错误模式(连接超时、死锁、OOM)。 |
| 覆盖面窄 |
仅模拟支付场景,未覆盖预约(最复杂 API)、报表(最慢查询)、并发冲突检测等真实瓶颈。 |
现有测试代码结构
// 这就是所谓的 "数据库操作"
function simulateDbOperation(minMs, maxMs) {
return new Promise((resolve, reject) => {
const delay = minMs + Math.random() * (maxMs - minMs);
const shouldFail = Math.random() < 0.0001; // 硬编码失败率
setTimeout(() => {
if (shouldFail) reject(new Error('Simulated DB error'));
else resolve();
}, delay);
});
}
// checkout = sleep(10-50) + sleep(1-5) + sleep(5-30) = 最大 85ms
// 然后断言 P95 < 500ms... 永远通过
Part 2 — 系统性能画像:已识别的瓶颈
当前配置一览
架构层面瓶颈
| 严重性 |
瓶颈 |
影响 |
文件位置 |
| 高 |
单进程运行 |
PM2 Fork 模式,无法利用多核 CPU。所有请求排队在单个 Node.js 事件循环 |
ecosystem.config.js |
| 高 |
连接池过小 |
max: 10,高并发时连接耗尽(已出现过生产事故,见 连接池泄漏教训) |
config/database.config.js |
| 中 |
无 gzip 压缩 |
报表等大 JSON 响应未压缩,浪费带宽和传输时间 |
backend/server.js |
| 中 |
内存缓存单进程 |
租户缓存、报表缓存、速率限制器都用 In-Memory Map,进程重启丢失,切 cluster 模式后无法共享 |
middleware/tenant-context.js
services/reports/cache.js |
| 中 |
无慢查询监控 |
无法发现和追踪慢 SQL,报表模块有大量 CTE + 窗口函数 + 多表 JOIN |
services/reports/queries/*.js (6460+ 行查询代码) |
| 低 |
中间件链长 |
10+ 层中间件逐层执行(trace → logger → CORS → tenant → auth → permission → rate-limit),每层都有开销 |
backend/server.js |
数据库查询复杂度
报表模块包含项目中最复杂的 SQL 查询:
| 查询类型 |
SQL 特征 |
风险 |
| 营收报表 (revenue.js) |
多表 JOIN + SUM/AVG/COUNT + 同比/环比(2x 查询) |
数据量大时全表扫描 |
| 客户留存 (retention.js) |
CTE + LAG() 窗口函数 + PARTITION BY + 嵌套子查询 |
计算密集,时间范围越大越慢 |
| 取消分析 (cancellations.js) |
COUNT() FILTER + GROUP BY reason + date_trunc |
相对轻量 |
| 预约冲突检测 |
时间范围重叠查询 + 员工可用性 |
高并发创建预约时热点 |
Part 3 — 行业实践:成熟平台怎么做
成熟 SaaS 平台的性能测试通常分三个层次:
三层测试金字塔
graph TB
A["Layer 3: 浸泡测试 & 混沌工程
长时间低压 + 故障注入
发现内存泄漏、资源耗尽"]
B["Layer 2: API 负载测试
k6 / Artillery / autocannon
真实 HTTP 端点 + 并发加压
发现连接池耗尽、慢查询、吞吐量上限"]
C["Layer 1: 微基准测试
Jest + supertest / 直接函数调用
关键路径性能回归防护
集成到 CI,每次 PR 检查"]
A --> B --> C
style C fill:#d1fae5,stroke:#059669,color:#064e3b
style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
style A fill:#fef3c7,stroke:#d97706,color:#78350f
Layer 1:微基准测试 (Micro-benchmark)
核心理念
在 Jest/Vitest 中测试真实代码路径的性能,设阈值做回归防护。每次 PR 自动运行。
测什么
- 关键 SQL 查询执行时间 — 连真实测试数据库,seed 数据后测量
- Service 层函数吞吐 — 调用 100 次取 P95
- 中间件链开销 — 空请求走完整中间件链的额外延迟
业界案例
- Stripe:对每个支付计算函数设 benchmark,P99 超阈值 CI 直接红
- Shopify:核心结账路径有性能守卫,每次 PR 检查是否引入回归
- Node.js Testing Best Practices(Goldbergyoni 2025):推荐 supertest + 阈值断言
代码示例
// 真正有价值的性能测试
const request = require('supertest');
const app = require('../../server');
describe('Appointment API Performance', () => {
test('GET /api/appointments should respond within 200ms at P95', async () => {
const times = [];
for (let i = 0; i < 50; i++) {
const start = performance.now();
await request(app)
.get('/api/appointments?page=1&limit=20')
.set('Authorization', `Bearer ${testToken}`)
.set('x-tenant-id', 'spa001');
times.push(performance.now() - start);
}
const sorted = times.sort((a, b) => a - b);
const p95 = sorted[Math.ceil(0.95 * sorted.length) - 1];
console.log(`P95: ${p95.toFixed(1)}ms, Avg: ${(times.reduce((a,b) => a+b) / times.length).toFixed(1)}ms`);
expect(p95).toBeLessThan(200);
});
});
Layer 2:API 负载测试 (Load Test)
核心理念
用专业工具对真实 HTTP 端点施加并发压力,发现系统在压力下的真实表现:连接池耗尽、内存泄漏、错误率飙升。
典型场景设计
| 场景名称 |
描述 |
虚拟用户 (VU) |
持续时间 |
关注指标 |
| Smoke |
最低负载,验证端点可用 |
1-2 |
1 min |
无 5xx 错误 |
| Load |
正常业务负载 |
20-30 |
5 min |
P95 < 300ms, 错误率 < 1% |
| Stress |
超出正常负载,找上限 |
50-100 |
3 min |
系统降级但不崩溃 |
| Spike |
突然流量暴增 |
0→100 突增 |
30s 峰值 |
恢复时间 < 30s |
| Soak |
长时间中低负载 |
10 |
30 min |
内存不增长、连接池不泄漏 |
真实用户行为模拟
// k6 场景示例 — 模拟真实用户行为
export default function() {
// 1. 登录获取 JWT
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: 'test.employee@qqnails.com',
password: 'test123'
}), { headers: { 'Content-Type': 'application/json' } });
const token = loginRes.json('token');
// 2. 查看今日预约列表
http.get(`${BASE_URL}/api/appointments?date=2026-02-07`, {
headers: { 'Authorization': `Bearer ${token}`, 'x-tenant-id': 'spa001' }
});
// 3. 创建新预约
http.post(`${BASE_URL}/api/appointments`, JSON.stringify({
guest_id: randomGuestId(),
services: [{ service_id: 1, employee_id: 2 }],
scheduled_date: '2026-02-08',
scheduled_time: randomTimeSlot()
}), { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } });
sleep(1); // 模拟用户思考时间
}
业界标准指标
Layer 3:浸泡测试 & 混沌工程
当前优先级:低
浸泡测试需要稳定的监控基础设施(指标采集、告警)。建议 Layer 1 + Layer 2 就位后再考虑。
- 浸泡测试:中低负载持续 2-4 小时,监控内存趋势、连接池使用率、响应时间是否漂移
- 混沌工程:注入故障(数据库断连、网络延迟、进程 kill),验证恢复能力
- 案例:Netflix Chaos Monkey, Gremlin, AWS Fault Injection Simulator
Part 4 — 工具对比:k6 vs Artillery vs autocannon
| 维度 |
k6 推荐 |
Artillery |
autocannon |
| 开发商 |
Grafana Labs |
Artillery.io |
Fastify 团队 (Matteo Collina) |
| 脚本语言 |
JavaScript (ES6) |
YAML + JS hooks |
CLI + JS API |
| CI 集成 |
极好(exit code + JSON 输出 + thresholds) |
好(JSON reporter) |
一般(需要自己解析) |
| 场景编排 |
强(scenarios, stages, thresholds) |
强(YAML phases) |
弱(只有基础并发参数) |
| 可视化 |
Grafana Cloud k6 / InfluxDB |
自带 HTML 报告 |
终端输出 |
| 学习曲线 |
中(需学 k6 API) |
低(YAML 直观) |
极低 |
| 适用场景 |
全面负载测试、CI 集成、长期监控 |
快速场景测试、API 冒烟测试 |
快速验证单个端点吞吐量 |
| PostgreSQL 扩展 |
有(xk6-pgxpool) |
无 |
无 |
| 安装 |
brew install k6 |
npm install -g artillery |
npm install -g autocannon |
推荐组合
- Layer 1 (微基准):Jest + supertest(已有依赖,零成本)
- Layer 2 (负载测试):k6(JS 脚本、CI 友好、阈值断言、可视化)
- 快速验证:autocannon(一行命令测单个端点)
k6 连接池优化实测数据(业界参考)
| Pool Size |
平均响应时间 |
P95 |
排队请求数 |
说明 |
| 10(默认) |
1,221 ms |
2,100+ ms |
130 |
连接耗尽严重 |
| 20 |
280 ms |
450 ms |
12 |
降 77%,最佳性价比 |
| 50 |
210 ms |
380 ms |
3 |
收益递减,占用资源更多 |
来源:Find Your Optimal PostgreSQL Connection Pool Size with k6
Part 5 — Celoria 性能测试实施计划
Phase 1:微基准测试(替换当前假测试)
目标
在 Jest 中测试真实代码路径的性能,纳入 CI 做回归防护。
测试矩阵
| 测试场景 |
测试方式 |
阈值 |
覆盖的真实瓶颈 |
| 预约列表查询 |
supertest → GET /api/appointments |
P95 < 200ms |
SQL 查询 + 中间件链 |
| 预约冲突检测 |
supertest → POST /api/appointments/check-conflict |
P95 < 100ms |
时间重叠查询性能 |
| 创建预约 |
supertest → POST /api/appointments |
P95 < 500ms |
事务写入 + 多表操作 |
| 营收报表 |
supertest → GET /api/reports/revenue |
P95 < 2000ms |
复杂 JOIN + 聚合 |
| 留存率报表 |
supertest → GET /api/reports/retention |
P95 < 3000ms |
CTE + 窗口函数 |
| 权限检查 |
直接调用 permission service |
P95 < 10ms |
RBAC 查询效率 |
需要的基础设施
- 测试数据库 seed 脚本(生成足够数据量:1000 预约、100 客户、50 员工)
jest.config.performance.js 改为连接真实测试数据库
- CI 中添加性能测试阶段(可选,初期手动触发)
Phase 2:k6 负载测试框架
目标
用 k6 对真实 HTTP 端点施加并发压力,发现系统在并发下的真实上限。
项目结构
backend/tests/performance/
├── k6/
│ ├── smoke.js
│ ├── load.js
│ ├── stress.js
│ ├── spike.js
│ ├── soak.js
│ └── helpers/
│ ├── auth.js
│ ├── data.js
│ └── config.js
├── payment-load.test.js
└── README.md
场景设计
| 场景 |
虚拟用户 |
持续 |
核心断言 |
| 预约查询 |
50 VU |
2 min |
P95 < 300ms |
| 创建预约(含冲突检测) |
20 VU |
2 min |
P95 < 500ms, 0 冲突假阳性 |
| 报表生成 |
10 VU |
3 min |
P95 < 2s |
| 混合场景(真实流量分布) |
30 VU |
5 min |
错误率 < 1% |
| 峰值突增 |
0→100 VU |
1 min |
不崩溃,恢复 < 30s |
npm 脚本集成
// package.json
{
"scripts": {
"perf:smoke": "k6 run backend/tests/performance/k6/smoke.js",
"perf:load": "k6 run backend/tests/performance/k6/load.js",
"perf:stress": "k6 run backend/tests/performance/k6/stress.js",
"perf:soak": "k6 run backend/tests/performance/k6/soak.js",
"perf:all": "npm run perf:smoke && npm run perf:load"
}
}
Phase 3:基于测试结果的针对性优化
原则
不要先优化再测试。先用 Phase 1 + Phase 2 建立 baseline,用数据驱动优化决策。
| 优先级 |
优化项 |
做什么 |
预期收益 |
| P0 |
PM2 Cluster 模式 |
instances: 'max' 或 instances: 4,利用多核 |
吞吐量提升 2-4x |
| P0 |
连接池调优 |
max: 10 → 20-30,用 k6 找最优值 |
响应时间降 50-70% |
| P1 |
gzip 压缩 |
添加 compression Express 中间件 |
传输体积减 60-80% |
| P1 |
慢查询日志 |
PG log_min_duration_statement = 200ms |
可定位慢 SQL |
| P1 |
连接池监控 |
暴露 pool.totalCount/waitingCount 到 /api/monitoring/metrics |
实时可见连接状态 |
| P2 |
报表查询优化 |
添加索引(appointments.scheduled_date, transactions.created_at) |
报表查询提速 |
实施时间线
Week 1 — Phase 1:微基准测试
- 替换
payment-load.test.js,改为 supertest 真实测试
- 创建性能测试 seed 数据脚本
- 编写 3-4 个关键端点的微基准测试
- 建立 baseline 数据
Week 2 — Phase 2:k6 负载测试
- 安装 k6,搭建脚本框架
- 编写 5 个核心场景脚本(smoke / load / stress / spike / soak)
- 集成 npm scripts
- 运行首轮负载测试,记录 baseline
Week 3 — Phase 3:测试驱动优化
- 基于 Phase 2 数据,执行 P0 优化(cluster + 连接池)
- 重跑负载测试,对比优化前后数据
- 执行 P1 优化(gzip + 慢查询日志 + 连接池监控)
- 形成性能基线报告
Part 6 — 不做什么(避免过度工程)
以下是现阶段不需要做的事情
基于 Celoria 当前的业务规模(美甲沙龙 SaaS,每店每天几百笔交易),以下优化属于过度工程:
| 不做的事 |
理由 |
什么时候该做 |
| 引入 Redis |
In-Memory Map 在单实例下够用。上 cluster 模式后如果内存缓存不一致再考虑 |
PM2 Cluster 模式 + 缓存不一致问题出现时 |
| 读写分离 |
当前数据量和流量远不需要。PG 主从复制引入运维复杂度 |
单数据库成为瓶颈(CPU > 70%) |
| 微服务拆分 |
复杂度爆炸,运维成本激增。单体应用在这个规模下完全足够 |
团队 > 10 人,模块需独立部署 |
| 混沌工程 |
需要先有完善的监控和告警基础设施 |
可观测性三支柱完成后 |
| CDN |
API 是动态数据,静态资源已由 Nginx 处理 |
面向全球用户时 |
| 数据库分区 |
表数据量不到百万级别,分区收益极小 |
单表超过 1000 万行 |
参考资料