性能测试调研报告

调研日期:2026-02-07  |  状态:Planning

目录
Part 1 — 现状诊断:当前性能测试的问题 Part 2 — 系统性能画像:已识别的瓶颈 Part 3 — 行业实践:成熟平台怎么做 Part 4 — 工具对比:k6 vs Artillery vs autocannon Part 5 — Celoria 性能测试实施计划 Part 6 — 不做什么(避免过度工程)

Part 1 — 现状诊断:当前性能测试的问题

现有文件

仅有 1 个文件backend/tests/performance/payment-load.test.js

结论:该测试完全是模拟的,不测试任何真实代码,价值接近于零。

核心问题分析

问题 详细说明
不测真实代码 所有"操作"都是 setTimeout(randomDelay),不连数据库、不调 Express 路由、不走中间件链。跟支付代码没有任何关系。
结果可预测 simulateDbOperation(10, 50) → 最大 50ms;断言 P95 < 500ms 永远通过。这是在测试 setTimeoutMath.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 — 系统性能画像:已识别的瓶颈

当前配置一览

1
PM2 实例数(Fork 模式)
10
连接池 max 连接数
5s
连接池空闲超时
20MB
Request Body 限制
10+
中间件层数
0
gzip / Redis / 负载均衡

架构层面瓶颈

严重性 瓶颈 影响 文件位置
单进程运行 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 自动运行。

测什么

业界案例

代码示例

// 真正有价值的性能测试
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); // 模拟用户思考时间
}

业界标准指标

< 200ms
API 响应 P95(列表查询)
< 500ms
API 响应 P95(写入操作)
< 2s
API 响应 P95(报表查询)
< 1%
正常负载错误率

Layer 3:浸泡测试 & 混沌工程

当前优先级:低
浸泡测试需要稳定的监控基础设施(指标采集、告警)。建议 Layer 1 + Layer 2 就位后再考虑。

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
推荐组合

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 查询效率

需要的基础设施

Phase 2:k6 负载测试框架

目标
用 k6 对真实 HTTP 端点施加并发压力,发现系统在并发下的真实上限。

项目结构

backend/tests/performance/ ├── k6/ │ ├── smoke.js # 1-2 VU, 验证端点可用 │ ├── load.js # 30 VU, 正常业务负载 │ ├── stress.js # 100 VU, 找到上限 │ ├── spike.js # 0→100 突增, 恢复测试 │ ├── soak.js # 10 VU × 30min, 内存泄漏 │ └── helpers/ │ ├── auth.js # JWT 登录获取 │ ├── data.js # 测试数据生成器 │ └── config.js # 环境配置 (URL, tenant) ├── 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:微基准测试
Week 2 — Phase 2:k6 负载测试
Week 3 — Phase 3:测试驱动优化

Part 6 — 不做什么(避免过度工程)

以下是现阶段不需要做的事情

基于 Celoria 当前的业务规模(美甲沙龙 SaaS,每店每天几百笔交易),以下优化属于过度工程:

不做的事 理由 什么时候该做
引入 Redis In-Memory Map 在单实例下够用。上 cluster 模式后如果内存缓存不一致再考虑 PM2 Cluster 模式 + 缓存不一致问题出现时
读写分离 当前数据量和流量远不需要。PG 主从复制引入运维复杂度 单数据库成为瓶颈(CPU > 70%)
微服务拆分 复杂度爆炸,运维成本激增。单体应用在这个规模下完全足够 团队 > 10 人,模块需独立部署
混沌工程 需要先有完善的监控和告警基础设施 可观测性三支柱完成后
CDN API 是动态数据,静态资源已由 Nginx 处理 面向全球用户时
数据库分区 表数据量不到百万级别,分区收益极小 单表超过 1000 万行

参考资料