Skip to content
Charles Shao
Go back

性能测试:指标、负载模型、结果解读与容量规划

views

性能测试通常被视为测试团队的职责,但开发工程师同样需要掌握:理解核心指标有助于写出更高效的代码,在联调与排障时也能与测试、运维团队对准同一套评判标准。

TL;DR

Table of contents

Open Table of contents

1. 不同角色眼中的性能

不同角色对”性能”的关注维度各异——理解这一点,有助于在跨团队协作时对准评判标准。

1.1 用户视角

用户衡量性能的唯一标准是响应速度:从点击到页面呈现需要多久?从提交订单到返回结果需要多久?响应慢则直接判定”系统性能差”。

1.2 开发视角

开发人员通常难以直接量化性能数字,需要从架构与代码层面做推断性评估:

  1. 项目是否采用分布式架构?
  2. 是否引入了缓存与消息队列?
  3. 高并发场景是否有针对性处理?
  4. 数据库设计是否合理(索引、分库分表)?
  5. 核心路径的算法复杂度是否经过审查?
  6. 是否存在内存泄漏风险?
  7. Redis 容量与服务器配置是否匹配实际负载?

1.3 测试视角

测试人员依赖性能测试工具输出量化报表,核心关注项通常包括:

  1. 响应时间(RT);
  2. 请求成功率;
  3. 吞吐量;
  4. 错误率与异常分布。

1.4 运维视角

运维人员以基础设施资源利用率为主要视角:服务器 CPU/内存使用是否合理,数据库是否存在资源滥用。随着 DevOps 普及,这一角色与开发、测试的边界已逐渐模糊,但其视角在容量规划与告警阈值设定上仍不可替代。

不同角色看性能:用户关注响应速度,开发关注架构与算法,测试关注 RT/成功率/吞吐,运维关注资源利用率 同一「性能」,用户、开发、测试、运维各自盯的指标不同。

2. 测试前的准备

2.1 摸清业务场景

性能测试最忌无的放矢。 对业务理解不够深刻,很容易把测试重心放在次要路径上,忽略真正的性能瓶颈。

以一个提供邮件发送功能的系统为例:直觉上会先测邮件发送接口,但每天上万次调用的更高频路径往往是用户管理与认证——后者才可能是真正的瓶颈所在。

2.2 用好历史数据

历史流量数据(访问日志、监控指标)可以帮助定位调用最频繁、压力最大的接口,让测试工作有的放矢。这些高频路径通常也是优化收益最大的地方,优化好了往往能带来质的提升。

3. 核心性能指标

3.1 响应时间(RT)

响应时间 RT(Response Time)指用户发出请求到收到系统响应所经历的总时间。

RT 是最直观的性能指标,直接反映系统处理请求的速度,也是用户感知最敏感的指标。

3.2 并发数

并发数指系统在某一时刻同时处理的请求数量。

并发数反映系统的瞬时负载能力。

3.3 QPS 与 TPS

二者的区别:一次页面访问(1 TPS)可能触发多次后端查询(多 QPS)。例如,访问一个页面触发 2 次服务器请求,则产生 1T、2Q。

3.4 吞吐量

吞吐量指系统单位时间内处理的请求总量。

TPS 和 QPS 都是吞吐量的量化形式。请求越消耗资源,系统吞吐能力越低,反之越高。

核心换算公式:

性能指标关系:并发数、QPS 与 RT 可互相换算,辅以 PV/UV/DAU/MAU 活跃度指标 并发、QPS、RT 可互相换算;活跃度另用 PV/UV/DAU/MAU。

一句话QPS ≈ 并发数 / RT——压测报表里三者对不上时,先核查各字段的采样口径。

4. 业务活跃度指标

4.1 PV(Page View)

页面浏览量:统计周期内,用户每打开或刷新一个页面计为 1 次,多次刷新累加计数。从页面打开次数的维度统计访问量。

4.2 UV(Unique Visitor)

独立访客数:统计周期(通常 1 天)内,访问同一站点的去重用户数。同一用户多次访问只计 1 次。从用户个体维度统计规模。

4.3 DAU(Daily Active User)

日活跃用户数。

4.4 MAU(Monthly Active User)

月活跃用户数。

5. 从活跃度推算容量需求

场景:某网站 DAU 为 1200 万,用户日均使用时长 1 小时(3600s),平均 RT 为 0.5s,求并发量和 QPS。

推算容易出错的地方,是把三种「并发」混着用、又用不一致的口径去除 RT。先把定义钉死,再逐层推:

Step 1 — 平均并发(全天摊平):把一天的总在线时长均摊到 86400 秒。

平均并发 = DAU × 使用时长 / 86400
        = 1200万 × 3600 / 86400 = 50万

Step 2 — 真实并发(扣除低峰):用户活跃集中在约 16 小时(57600s),凌晨低峰几乎无流量,按活跃时段折算:

真实并发 = DAU × 使用时长 / 57600
        = 1200万 × 3600 / 57600 = 75万           (= 平均并发 × 1.5)

Step 3 — 峰值并发(一个倍率):在真实并发之上乘以业务峰值倍率(大促/晚高峰,这里取 ):

峰值并发 = 真实并发 × 峰值倍率
        = 75万 × 4 = 300万

Step 4 — 换算 QPS(口径统一:QPS = 并发 / RT,RT 固定 0.5s):每个 QPS 都由它对应的那一档并发除以同一个 RT 得到,不再交叉混用:

活跃期 QPS = 真实并发 / RT = 75万  / 0.5 = 150万 QPS
峰值   QPS = 峰值并发 / RT = 300万 / 0.5 = 600万 QPS

口径一致性:原始推算的坑在于「峰值并发 300 万」却用「真实并发 75 万」去算 QPS——两档并发混用,数字自然对不上。正确做法是先声明峰值倍率作用在哪一档并发上,再让 QPS 与并发同档对应。

以此为基准做容量规划(要按峰值留量,而不是活跃期):若单实例压测饱和点为 QPS 500,安全系数取 0.75,则单实例安全承接 375 QPS;支撑 600 万峰值 QPS 需要 ⌈6000000 / 375⌉ = 16000 台实例——这说明任何真实大型系统都必须叠加缓存(缓存机制)、CDN、读写分离(数据库性能与扩展)、异步削峰(异步与削峰)等手段把「打到应用实例的 QPS」压下来,而非靠堆实例解决。

6. 测试类型:性能 / 负载 / 压力 / 稳定性

性能测试分类:从基准性能验证到负载、压力、稳定性测试的逐级递进 从性能验证到负载、压力、稳定性,压力逐级加码。

6.1 性能测试

在已知基准和明确性能指标的前提下,通过工具模拟负载,验证系统在特定运行条件下是否达到预期能力。属于有明确通过/失败标准的验证型测试。

6.2 负载测试

持续增加请求压力,直至某项资源(缓存、数据库连接、CPU 等)达到饱和或响应时间超出阈值——用于确定系统承载上限。

6.3 压力测试

不考虑资源消耗,持续加压直到系统崩溃,找出系统的极限断点及崩溃模式。

6.4 稳定性测试

在接近真实的负载水平下持续运行较长时间(数小时至数天),验证系统能否稳定运行,排查内存泄漏、连接池耗尽、GC 停顿累积等长期隐患。

7. 设计真实负载模型

从历史日志或 APM 中提取接口调用比例,构造真实的业务混合(API Mix),是压测可信度的基础。一份好的负载模型至少包含:

  1. Think Time(思考时间):真实用户在操作之间有停顿(浏览页面、填写表单)。JMeter 的 Timer 或 Gatling 的 pause 可模拟用户思考时间,不配置时相当于 0 延迟「机器人用户」,会严重高估实际并发,测出的 QPS 没有参考价值。
  2. Ramp-Up(爬坡启动):从 0 逐步增加并发到目标值,观察系统在不同负载档位的行为,而非一次性打满——突然全量打满可能在 JVM 预热前就触发假性超时,误判性能瓶颈。
  3. 接口比例(API Mix):参照生产日志,按真实比例混合读/写/搜索接口。若生产中「商品详情:下单:支付 = 100:10:1」,压测也应保持此比例,而非只测一个 GET 接口。
  4. 数据多样性:请求参数不要使用相同的 ID,否则数据库 Buffer Pool 会把同一条记录反复预热,与生产中均匀分布的访问模式差异极大,掩盖真实的 I/O 瓶颈。

7.1 开放模型 vs 闭合模型:最容易被忽略的选择

比 Think Time 更根本的是负载注入模型——它决定了「系统变慢时,压力会不会自动泄掉」。

开放模型 vs 闭合模型:闭合固定并发自带背压,开放恒定到达率会让队列无限堆积 闭合模型系统变慢时到达率自动下降(掩盖过载);开放模型到达率恒定,能暴露真实拐点与尾延迟。

为什么这决定成败:闭合模型 + 0 思考时间会系统性高估容量——因为系统一旦变慢,虚拟用户就”体贴地”少发请求,你永远压不出真实拐点,测得的”最大 QPS”是系统在健康状态下的吞吐,而不是它在真实开放流量冲击下的崩溃点。面向公网/竞价的系统应优先用开放模型压测(k6 的 constant-arrival-rate、Gatling 的 constantUsersPerSec、JMeter 的 Throughput Shaping Timer 或 Concurrency Thread Group + 到达率控制)。

7.2 预热与稳态判定(Warmup & Steady State)

压测的前几分钟数据几乎都不可信,必须丢弃:

8. 解读压测结果

8.1 饱和点与错误预算

压测报告中的吞吐量–延迟曲线通常呈现出明显的**「膝点」(Knee Point / 饱和点)**:在此之前并发增加时 RT 基本平稳,越过此点后 RT 急剧攀升,错误率开始上升。

8.2 P95 / P99 与平均值:尾延迟的数学

平均响应时间(Mean RT)极易掩盖尾部延迟:1000 个请求中有 10 个(1%)耗时 10s、其余 990 个各 1ms,均值仅约 100ms,但 P99 高达 10s。均值对长尾几乎不敏感,而用户真正记住的是那次卡顿。

几个必须建立的直觉:

N = 1   → 1%      命中慢请求
N = 10  → 9.6%
N = 100 → 63.4%   —— 单点 P99 尚可,整页却经常慢

这正是「P99 的服务,用户体验却像 P90」的根因,也是广告竞价并行调用多个下游时必须压尾延迟的原因。

一句话:平均值给你信心,P99 告诉你真相;两者都要看,只看均值是自欺欺人。

8.3 Coordinated Omission:被工具悄悄抹平的尾延迟

即便你盯着 P99,压测工具本身也可能在说谎——这就是 Coordinated Omission(协调遗漏,CO)

问题出在闭合/单连接工具的工作方式:它发出一个请求后阻塞等待响应,才发下一个。一旦服务卡顿(GC、STW、锁竞争)冻结了 140ms,工具就只记录到 1 个 140ms 的慢样本,而这 140ms 里「本应按计划发出」的几十个请求根本没发——它们的延迟被整体遗漏。结果:真实 P99 已经崩到 130ms,工具测出的 P99 却还是漂亮的 10ms。

Coordinated Omission:闭合工具在停顿期不发请求,遗漏了本应到达的负载,尾延迟被系统性抹平 停顿期本应到达的请求(校正后延迟递减:剩余冻结 + 自身)被闭合工具整体遗漏,实测 P99 远好于真实 P99。

如何避免

  1. 用开放模型注入(恒定到达率):请求按计划到达,停顿期堆积的请求会被真实记录(§7.1)。
  2. 用支持 CO 校正的直方图:如 HdrHistogram 的 recordValueWithExpectedInterval() 会按预期间隔回填被遗漏的样本;wrk2、Gatling、k6 的开放模型都内建了这类校正。
  3. 对比「服务端延迟」与「客户端延迟」:若两者背离很大,通常就是排队/CO 在作祟。

8.4 测试环境与生产的差异

压测结果的可信度高度依赖测试环境与生产的相似度:

差异维度常见误差建议
数据量级测试库 10 万行,生产 1 亿行;索引效率截然不同使用生产数据快照或按比例缩放的数据集
缓存预热测试开始时 Redis 空;生产命中率 > 90%压测前先预热,或分别记录冷/热态两组基线
JVM 预热测压开始前几分钟 JIT 尚未充分编译,延迟偏高设置 Ramp-Up 期(5–10 分钟)预热 JVM,再统计指标
网络拓扑测试机与被测服务在同一物理机;生产有跨机房延迟尽量模拟生产网络拓扑,或单独测量网络延迟计入基准

8.5 常见压测误区(反模式)

  1. 用闭合模型 + 0 思考时间压公网系统:系统一慢虚拟用户就少发请求,自带背压掩盖真实拐点,测得的容量偏乐观。面向真实到达的系统应改用开放模型。
  2. 只信工具报的 P99:不做 CO 校正时尾延迟被抹平,报告一片绿、线上却在超时(§8.3)。
  3. 只测 Happy Path:单接口 GET /health 返回 200ms,并不等同于真实业务场景;要测完整用户路径(搜索 → 加购 → 结算)。
  4. 未进稳态就下结论:前 5–10 分钟数据因 JIT 未充分编译、缓存/连接池未预热而失真,不应纳入统计(§7.2)。
  5. 把 QPS 当并发用户数:600 万 QPS ≠ 600 万同时在线用户;后者通过 并发数 = QPS × RT 换算(参见第 5 节)。
  6. 测试数据太干净:生产数据库有数亿行且分布不均,全表扫描和慢查询模式与小数据集完全不同。
  7. 不测降级路径:压测时不模拟 Redis 断开、第三方服务超时等异常,导致上线后故障时降级与限流策略未经验证(参见依赖韧性)。
  8. 忽视压测客户端自身瓶颈:单台施压机的 CPU、网卡、端口数(ephemeral port)先打满,测出的是”施压机的上限”而非被测系统的上限——高 QPS 场景需分布式施压。

9. 与容量规划结合

将压测结论与业务预测结合,可以回答「下次大促需要几台服务器」的问题:

示例:压测得出单实例饱和点为 QPS 500(P99 < 300ms),当前实例数为 4 台(总容量 2000 QPS)。业务预测大促峰值为 1500 QPS,安全系数按 1.5× 计算(需要 2250 QPS),则需要 ⌈2250 / 500⌉ = 5 台实例,并预留 1 台弹性扩容缓冲。

容量规划三步走:

  1. 压测获取单实例上限(饱和点 QPS);
  2. 评估峰值需求(历史大促倍率 × 日常平均 QPS);
  3. 按安全系数预留余量(通常 1.5–2×),计算实例数并确认弹性扩缩策略。

10. 常用压测工具

10.1 后端工具

工具特点
JMeterApache 出品,Java 开发,功能全面,图形化界面,开源免费
Gatling基于 Scala,性能高,测试脚本即代码,适合持续集成,开源免费
wrk高性能 HTTP 基准工具,命令行驱动,适合快速测压,开源免费
ab(Apache Bench)轻量,适合单接口快速验证,开源免费
LoadRunner商业工具,功能强大,适合大型企业级场景

10.2 脚本示例:从快速验证到开放模型压测

wrk / wrk2:命令行快速摸底。wrk 是闭合模型(固定连接),存在 CO;压尾延迟建议用 wrk2 并显式指定目标吞吐 -R(开放模型 + CO 校正)。

# wrk:12 线程、400 连接、压 30s,打印延迟分布
wrk -t12 -c400 -d30s --latency http://bidder.internal/health

# wrk2:以恒定 100k QPS 注入(开放模型,避免 Coordinated Omission)
wrk2 -t16 -c1000 -d60s -R100000 --latency \
     -s post_bid.lua https://bidder.internal/rtb/bid

JMeter(CLI 非 GUI 模式):GUI 只用来编辑 .jmx,正式压测一律用命令行(GUI 本身会成为瓶颈)。

# 非 GUI 运行,生成 HTML 报告;用属性覆盖并发/时长,便于 CI 参数化
jmeter -n -t bidding_mix.jmx -l result.jtl -e -o ./report \
       -Jthreads=500 -Jrampup=300 -Jduration=1800

.jmx 里用 Concurrency Thread Group + Throughput Shaping Timer 控制到达率(逼近开放模型),并用 CSV Data Set Config 喂多样化参数,避免单一 ID 反复命中缓存。

Gatling(Scala DSL,脚本即代码,天然适合 CI):用 constantUsersPerSec 即为开放模型。

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BiddingSimulation extends Simulation {
  val httpProtocol = http.baseUrl("https://bidder.internal")

  // 按生产比例混合:商品详情 : 下单 : 支付 = 100 : 10 : 1
  val browse = scenario("browse").exec(
    http("detail").get("/item/#{itemId}")
  ).pause(1.second, 3.seconds) // Think Time

  val feeder = csv("items.csv").random // 参数多样化

  setUp(
    browse.feed(feeder).injectOpen(
      rampUsersPerSec(0).to(2000).during(5.minutes), // 预热爬坡
      constantUsersPerSec(2000).during(30.minutes)   // 开放模型恒定到达率
    )
  ).protocols(httpProtocol)
   .assertions(
     global.responseTime.percentile(99).lt(100), // P99 < 100ms 才算通过
     global.successfulRequests.percent.gt(99.9)
   )
}

k6(JavaScript,云原生友好)constant-arrival-rate 是开放模型的标准写法,内建 CO 校正。

import http from "k6/http";
import { check } from "k6";

export const options = {
  scenarios: {
    rtb_open_model: {
      executor: "constant-arrival-rate", // 开放模型:恒定到达率
      rate: 100000,          // 100k 次/秒
      timeUnit: "1s",
      duration: "30m",
      preAllocatedVUs: 2000, // 预分配足够 VU,不足会告警而非降速
      maxVUs: 5000,
    },
  },
  thresholds: {
    "http_req_duration{expected_response:true}": ["p(99)<100", "p(99.9)<150"],
    http_req_failed: ["rate<0.001"],
  },
};

export default function () {
  const res = http.post("https://bidder.internal/rtb/bid", openRtbPayload());
  check(res, { "status is 200": r => r.status === 200 });
}

读取 P99 / 换算容量:拿到分位数报表后,用 Little’s Law 反推并核对容量。例如 k6 汇总里 p(99)=92ms、恒定 100k QPS,则稳态平均在途请求:

# 由压测报表反推:并发 = QPS × RT,核对与实测并发是否一致
qps = 100_000
p50_rt_s = 0.018      # 18ms
p99_rt_s = 0.092      # 92ms
avg_rt_s = 0.025      # 25ms(均值,用于 Little's Law)

concurrency = qps * avg_rt_s          # = 2500 在途请求
print(f"稳态并发 ≈ {concurrency:.0f}")

# 单机安全容量:饱和点 QPS × 安全系数
sat_qps, safety = 3200, 0.75
usable = sat_qps * safety             # = 2400 QPS/实例
peak_qps = 6_000_000
import math
print(f"支撑峰值需实例数 = {math.ceil(peak_qps / usable)}")  # = 2500

口径提醒:报表里的 P99 必须来自开放模型 + CO 校正的采样,否则 并发 = QPS × RT 反推出的数字看似自洽,实则建立在被抹平的尾延迟上。

10.3 前端工具

  1. Fiddler:HTTP 抓包与请求调试工具,可修改请求/响应数据,Web 调试利器。
  2. HttpWatch:HTTP 请求录制与分析工具,适合前端页面性能诊断。

11. 性能优化自检清单

性能优化的前提是定位瓶颈——先通过测试数据确认问题所在,再针对性地优化,避免盲目调优。以下是常用自检项:

  1. 热点数据是否已引入缓存?(参见 缓存机制
  2. 系统架构本身是否存在单点或串行瓶颈?入口流量是否已均衡分摊?(参见 负载均衡
  3. 是否存在死锁或长事务阻塞?连接池是否够用?(参见 连接与 I/O 模型
  4. 是否存在内存泄漏(JVM 环境下尤其需要关注)?
  5. 数据库索引设计是否合理?是否存在全表扫描或需分库分表?(参见 数据库性能与扩展
  6. 热点路径是否引入了异步削峰或批量合并?(参见 异步与削峰批处理与请求合并
  7. 过载路径是否有限流、超时、熔断、降级兜底,并在压测中验证过?(参见 依赖韧性

12. 小结

性能测试的价值,不在于跑出一个漂亮的 QPS 数字,而在于用可信的方法回答”系统在真实压力下会怎样、能撑多少”。回到全篇:

至此,高性能系统设计系列十篇收束:从负载均衡分摊入口流量,到缓存冷热分离数据库扩展CDNNginx削减与就近处理,再到异步削峰连接与 I/O 模型批处理与请求合并榨干单机效率——性能测试则是贯穿始终的验证闭环:所有优化最终都要用可信的压测数据来证明它真的有效。


views
Share this post on:

Previous Post
JMeter 压测实战:组件模型、线程组、CLI 执行与 HTML 报告
Next Post
批处理与请求合并:攒批、削频与吞吐优化