很多带了一点功能测试经验的同学,第一次听说性能测试时会下意识地问一句:那我多开几个浏览器窗口、多挂几个用户去点接口,不就是在压测了吗?

这个想法的方向没错,但实现层面几乎行不通。把人肉操作当作"并发",第一你很难精确控制"到底同时有多少人在发请求",第二你无法统一记录"每个请求花了多少毫秒",第三你更没法把"100 个用户同时登录"这种场景精确地复现出来。性能测试要的就是"可控、可复现、可度量"——我需要能精确设定并发规模,能稳定地重复执行同一套请求序列,能把每一次请求的响应时间、成功率、吞吐量全部量化落盘,最后还能生成一份可以被团队评审的报告。这就是专门的性能测试工具存在的意义。

而这一篇,我们就把性能测试工具里最主流、也几乎是社区的"默认答案"——Apache JMeter——从安装启动、核心概念,一路讲到参数化、报告解读和常见的坑。全程都是能落地的手把手式讲解,命令、配置一律带逐行注释,看完你就能自己搭一个像样的压测脚本。

对了,先提个醒:凡是正文里出现的"思考题/自测",后面一定紧跟着带详解的答案,别光看题不兑现,那等于没学。

性能测试工具选型:为什么先学 JMeter

你可能会问,压测工具那么多,为什么要从 JMeter 入手?在性能测试的工具版图里,大致分成几类:

  • 开源全能型:以 JMeter、Gatling、k6 为代表。JMeter 是其中资历最老(2000 年前后诞生,Apache 基金会托管)、生态最全、文档最多的一个。它原生就是一个"开放式协议测试器",除了 HTTP/HTTPS,还能测 FTP、JDBC 数据库、JMS、LDAP、TCP 甚至 Java 请求,覆盖的协议很广。
  • 协议类命令行工具:ab(Apache Bench)、wrk、hey 这类,单条命令就能对某个 URL 发起高并发请求,极简、极快,但只擅长"单个地址的原始压力",做不了复杂的业务脚本、参数关联、断言。
  • 商业/云化平台:LoadRunner、Locust 的托管版、云厂商的压测产品等,功能强但往往要钱、要部署、要许可。

这里有个朴素的结论:做接口/协议层面的性能和负载测试,JMeter 是"投入产出比"最高的入门选项。它免费、跨平台(纯 Java,只要装了 Java 哪都能跑)、有图形界面方便搭脚本、有命令行模式适合在 CI 或服务器上跑,而且社区问题几乎都能搜到。先把它学扎实,后面你再去碰 Gatling、LoadRunner,概念是共通的。

JMeter 是什么,跑它需要什么条件

JMeter 是 Apache 软件基金会下的一款、基于 Java 编写的压力测试工具,全称 Apache JMeter。它的本质是一个"跑在 Java 虚拟机(JVM)里的 Java 程序",你喂给它一个描述"怎么测"的脚本文件(后缀 .jmx),它就按脚本里的线程配置、请求细节、断言规则去发请求,并把结果汇总统计出来。

正因为是纯 Java,它对你的机器就只有一个硬性要求:装好 Java 运行环境。具体版本上不少资料口径略有不一,我按比较稳妥的说法给你:

  • JMeter 4.x / 部分 5.x 早期版本:官方标注 Java 8 即可;
  • JMeter 5.5 及以后:官方文档至少要求 Java 8,但社区普遍建议装更新的 LTS 版本(如 Java 11 / 17)。这里务必记住一个经验:JMeter 和你本机 Java 版本不匹配,常常会以启动即报错、界面莫名闪退、插件加载失败等方式表现出来——排查这类问题,第一件事就是去验证 java -version。

(这一点我专门联网核对过:JMeter 官方 building 文档写的是 Java 8,而中文社区有文章主张 5.5 需要 JDK 11+。为避免你踩坑,下面我给出一条"最不容易报错"的路径:装一个 Java 8 或 11 以上的 LTS 即可,别用从来没人验证过的冷门版本组合。)

下载、安装与启动 JMeter

JMeter 的安装不想其他带向导的软件那样"下一步下一步",它的安装本质就是解压即用。我们从两个平台分别过一遍。

第一步:确认 Java 环境

先保证命令行里能敲出 java -version。Windows 上打开 PowerShell,Linux/macOS 打开终端:

# 查看 Java 版本,确认 JMeter 依赖的运行时已就绪
java -version
# 期望输出类似:
# openjdk version "17.0.10" 2024-01-16 LTS ...
# 如果报"不是内部或外部命令"/"command not found",说明没配好 JAVA_HOME 或 PATH,
# 先去安装 JDK,再把 JAVA_HOME 指向 JDK 安装目录,把 %JAVA_HOME%\bin 加进 PATH

第二步:下载二进制包并解压

去 JMeter 官方下载页(https://jmeter.apache.org/download_jmeter.cgi)拿 Binaries(二进制发行版)下的压缩包,Windows 用 .zip,Linux/macOS 用 .tgz。以 5.5 为例,解压后你会看到一个 apache-jmeter-5.5 目录,里面几个关键目录我们先认一下:

  • bin/:启动脚本所在处,Windows 是 jmeter.bat,Linux/macOS 是 jmeter;还有 jmeter-server(分布式压测用)、jmeter.properties(配置文件)。
  • lib/:JMeter 依赖的第三方 Java 库。
  • lib/ext/:JMeter 的核心 jar 和协议实现,我们后面装插件就是把 jar 丢进这里。
  • docs/ 与 printable_docs/:HTML 版官方手册。

第三步:启动 JMeter

启动有两种方式。一种是"方式一",直接双击运行 bin/jmeter.bat(Linux 下执行 bin/jmeter)。另一种是"方式二"(我推荐):把 bin 目录加进系统环境变量 PATH,之后在任何目录直接敲 jmeter 就能启动。Windows 下设置环境变量的命令如下(Linux 用 export 同理):

# 1. 配置 JAVA_HOME,指向你的 JDK 安装目录
set JAVA_HOME=C:\Program Files\Java\jdk-17
# 2. 把 JMeter 的 bin 目录临时加进 PATH(想长期生效请去系统环境变量里配置)
set PATH=%PATH%;D:\tools\apache-jmeter-5.5\bin
# 3. 直接敲 jmeter 启动图形界面
jmeter

启动后弹出的图形界面(GUI)就是 JMeter 的"搭建工作台"。这里必须讲一个很多人一开始不知道、却极其重要的使用纪律:

  • GUI 模式拿来建脚本、调试脚本、看小规模结果——够用且直观;
  • 真正的压测(尤其是高并发、长时间、线上环境)必须用命令行(非 GUI)跑。因为 GUI 本身要绘制界面、渲染数据,会额外吃掉 CPU 和内存,而 JMeter 的每个线程在 JVM 里是真实的 Java 线程,GUI 绘图表和真实压测抢资源,结果会偏小;更别提几百上千个线程时,GUI 图形界面往往会卡死。所以业界默认规矩是:能用命令行,就不开 GUI。

顺便把界面语言切成中文

JMeter 默认界面可能是英文。想改成中文,打开 apache-jmeter-5.5/bin/jmeter.properties,找到 language 这一行(通常被注释着),改成 zh_CN 并去掉行首的注释号,然后重启 JMeter:

# jmeter.properties 中,找到 language 配置项并改为 zh_CN
language=zh_CN

改完后界面按钮、元件名就变成中文,配合下面的讲解你能更快对上号。

五分钟跑通第一个压测:JMeter 基本使用流程

JMeter 的一切都围绕一个叫**测试计划(Test Plan)**的概念展开,它是整个脚本的根节点,下面长出一棵树结构:线程组 → 取样器/定时器/断言/监听器等。你第一次上手,按照下面这六步就能跑通一个最小压测。

  1. 启动 JMeter,默认会自带一个空的"测试计划"根节点。
  2. 在"测试计划"下右键添加一个线程组(线程组是什么,下一节专讲,先记住它是"并发模型"的容器)。
  3. 在"线程组"下右键添加一个 HTTP 取样器(取样器 Sampler 就是"我到底要发什么请求"的最小执行单元)。
  4. 在 HTTP 取样器里填写请求数据:协议、服务器 IP 或域名、端口、方法(GET/POST)、路径、参数或消息体。
  5. 在"线程组"下右键添加监听器,最基础的是"查看结果树"(先看单条请求对不对)。
  6. 点工具栏**绿色三角"启动"**运行,到"查看结果树"里看每条请求的响应状态。

把这六步串起来,一个"用 1 个线程、循环 1 次、打一个 HTTP 接口"的压测脚本就跑通了。接下来我们把这棵树上每一个关键节点都拆开讲透。

元件的两种法则:作用域与执行顺序

理解了树状结构,就会引出 JMeter 里最容易让人迷糊的两个概念:元件的"作用域"和"执行顺序"。

在 JMeter 里,装进测试计划树上的每一个东西(线程组、取样器、配置元件、定时器、断言、监听器……)都统称元件(Element)。先说作用域:一个元件的"影响范围",由它在树状结构里所处的父/子位置决定,规则很简单——一个配置型或断言型元件,只对它"自己下面"的那些子节点生效。举例:你在某个 HTTP 取样器下面挂了一个"HTTP 请求默认值",那么这个默认值就只影响这一个取样器;而你把它挂在线程组下面,那它就会平等地作用于这个线程组里的所有取样器。所以元件摆在哪一级,决定了它能"罩住"多少节点。

再说执行顺序。初学者最容易犯的错,是误以为"树上排在上面的先执行、排下面的后执行"。JMeter 的执行顺序和树上的视觉排列顺序无关,它有自己固定的优先级规则,大致从高到低是:

  1. 配置元件(如 HTTP 请求默认值、CSV 数据文件设置、用户定义的变量)——最先执行,因为后面的取样器要在发起请求前先拿到这些参数;
  2. 前置处理器(如"用户参数"、作用于请求发出之前的处理);
  3. 定时器(如同步定时器:在当前节点执行前先"等待/集合");
  4. 取样器(真正的请求本体);
  5. 后置处理器(如 JSON 提取器:取样器执行完、拿到响应之后,再从响应里提取数据供后续使用);
  6. 断言(对取样器结果做校验,判断本次请求到底算成功还是失败);
  7. 监听器(把结果收集、统计、画图,比如查看结果树、聚合报告)。

这里有个关键补充:取样式器本身不依赖周围其他元件也能独立执行,作用域问题是针对"配置/断言/提取"这类"附属型"元件而言的;而像"用户参数的变量到底在请求前准备好了没有""提取器提取到的值,断言和下一个请求能不能用到",全都由上面这条优先级顺序决定。记住一个口诀:配(置)→前(置)→迟(定时器)→样(取样器)→后(置)→判(断言)→听(监听器),JMeter 的实际执行就基本按这条链走。

线程组:并发模型的舵手

**线程组(Thread Group)**是整个压测脚本里最核心的"并发容器"。在 JMeter 的语义里,一个线程就是模拟的一个用户。你设 100 个线程,就是模拟 100 个'用户'在并发操作。所以"线程数"直接决定了"你要造多大的并发压力"。

线程组界面上有几个关键配置,一一说明:

  • 线程数(Number of Threads / users):总的线程数量,也就是模拟用户数。注意它和"每秒并发数"不是一回事,这点我们放到后面"线程数与真实并发"的坑里详细说。

  • Ramp-up 时间(秒):从第一个线程启动到最后一个线程启动所花的时间。如果把 Ramp-up 设成 0,意思是所有线程"一瞬间全部就绪",这通常会在压测刚开始的几秒形成一股冲击波,把服务器打个措手不及;而把 Ramp-up 设成一个合理的值(例如 100 个线程用 60 秒逐步起来),能更平滑地模拟"用户逐渐变多"的真实场景。业界有一个朴素的反推方法:Ramp-up ≈ 线程数 × 单条事务平均耗时 / 期望的每秒新增用户数,先用它当起点再调。

  • 循环次数(Loop Count):每个线程把脚本从头到尾跑多少遍。两个选项:

    • 勾选"永远"(Infinite):线程会一直循环跑下去,这时候必须配合调度器使用,否则测试永远不会停下来;
    • 指定固定次数:例如每个线程循环 10 次,那么总请求量 ≈ 线程数 × 循环次数。

    下方还有"调度器(Scheduler)"区域,两个常用字段:

    • 持续时间(Duration):整个测试跑多久。设了它,无论循环多少次,到时间就停;
    • 启动延迟(Startup Delay):脚本先"休眠"指定秒数,再正式开始发请求。

线程组这一层还决定了每个线程是独立的:每个线程有自己独立的 Cookie 存储、自己的计数器位置,这为后面"会话隔离""参数化"打下了基础。

<!-- 线程组的 .jmx 简化示意(完整文件里字段更全,这里抓主干) -->
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="登录压测">
  <!-- 线程数:100 个模拟用户 -->
  <intProp name="ThreadGroup.num_threads">100</intProp>
  <!-- Ramp-up:60 秒内把 100 个线程全部拉起来 -->
  <intProp name="ThreadGroup.ramp_time">60</intProp>
  <!-- 是否启用调度器:false 表示不用调度器 -->
  <boolProp name="ThreadGroup.scheduler">false</boolProp>
  <!-- 若启用调度器,这只总时长(秒) -->
  <stringProp name="ThreadGroup.duration">600</stringProp>
  <!-- 若启用调度器,这是启动前的延迟(秒) -->
  <stringProp name="ThreadGroup.delay">0</stringProp>
  <!-- 是否无限循环:true 表示永远循环,必须配调度器 -->
  <stringProp name="ThreadGroup.loop">true</stringProp>
</ThreadGroup>

HTTP 取样器:把一次请求说清楚

在完成了"人"(线程)的配置后,下一步就是要告诉他们"做什么动作"——这就是取样器(Sampler)。取样器是 JMeter 中真正"发出请求、拿到结果"的最小执行单元。最常用的是 HTTP 取样器(HTTP Sampler),它负责按照 HTTP 协议,向服务器发出一个具体请求并等待响应。

填好一个 HTTP 取样器,本质就是把一次 HTTP 请求的所有要素写全。关键字段如下:

  • 协议(Protocol):http 或 https。不写默认走 http。
  • 服务器名称或 IP:你打算压测的主机,可以是 www.example.com 或 127.0.0.1。
  • 端口号(Port Number):HTTP 默认 80,HTTPS 默认 443;如果是别的自定义端口(比如本机测试服务开了 8080),记得填上。
  • 方法(Method):GET、POST、PUT、DELETE……跟你接口实际约定的方法保持一致。
  • 路径(Path):接口的 URL 路径部分。路径里也可以直接带查询串(如 /api/login?from=web)。
  • 内容编码(Content Encoding):默认走 ISO 国际标准,但对中文支持不友好。遇到中文参数/返回值乱码,改成 utf-8。
  • 参数(Parameters / Body Data):
    • GET 请求通常把参数拼在路径里,或填在"Parameters"表格中;
    • POST 请求的 JSON/表单参数,一般放到**消息体数据(Body Data)**里,比如 {"username":"tom","password":"123"}。

这里有个新手常踩的点:POST 接口如果后端要求 JSON 请求体,你却把参数填在 URL 上,很可能被服务器 400 或拒绝。参数放哪,取决于接口约定,不是"随便放"。

<!-- HTTP 取样器 .jmx 示意 -->
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="登录接口">
  <stringProp name="HTTPSampler.protocol">http</stringProp>
  <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
  <stringProp name="HTTPSampler.port">8080</stringProp>
  <stringProp name="HTTPSampler.method">POST</stringProp>
  <stringProp name="HTTPSampler.path">/api/login</stringProp>
  <stringProp name="HTTPSampler.connect_timeout">5000</stringProp>
  <stringProp name="HTTPSampler.response_timeout">10000</stringProp>
  <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
  <!-- 消息体数据:POST 的 JSON 请求体 -->
  <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
    <collectionProp name="Arguments.arguments">
      <elementProp name="" elementType="HTTPArgument">
        <stringProp name="Argument.value">{"username":"tom","password":"123"}</stringProp>
      </elementProp>
    </collectionProp>
  </elementProp>
</HTTPSamplerProxy>

查看结果树:从微观看一次请求的成败

跑脚本时,你总需要一个"看得见"的窗口来确认"我这一步到底发出去了没有、服务器回了什么"。监听器(Listener)就是干这个的:它负责收集取样器的执行结果,并把它展示、统计或落盘。其中最基本的一个监听器是查看结果树(View Results Tree)——它逐条展示每个取样器的详细结果。

在查看结果树里,选中某一条请求你会看到很关键的三类信息:

  • 取样器结果(Sampler result):一行核心统计,包含:
    • Thread Name:这条请求来自哪个线程组、哪个线程,格式通常是"线程组名 + 序号",例如 登录压测 1-3;
    • Sample Time:一次请求从发出到拿到完整响应的总耗时(毫秒);
    • Load Time:响应时间(对 HTTP 而言与 Sample Time 表现一致);
    • Latency:从发出请求到收到"第一个字节"的时间,能体现网络往返与服务器首批响应速度;
    • Response Code:HTTP 状态码,如 200、400、500;
    • Response Message:状态码对应的文本,如 OK。
  • 请求(Request):这次请求带出去的请求头、请求体、参数等,方便核对"我这下发出去的到底是什么"。
  • 响应(Response Data):服务器返回的响应头 + 响应体,可以在这里看接口真正返回了什么 JSON。

值得强调:查看结果树适合"小规模调试",绝不适合"高强度压测"。因为它是逐条把所有请求的详细内容都存进内存并画在树上,请求一多,内存和界面绘制都会被拖垮。正式压测时请把它从线程组里摘掉,改用命令行跑,把结果写进文件。

压测真实系统时,很多接口需要"带着会话/登录态"才能访问。一个接口回投放了 Set-Cookie,后续请求必须带上这个 Cookie,服务器才知道"你已登录"。JMeter 用一个专门元件来模拟浏览器的 Cookie 行为:HTTP Cookie 管理器(HTTP Cookie Manager)。

它的行为很像浏览器:如果某个 HTTP 请求的响应里带了 Cookie,Cookie 管理器会自动把它存起来,并在之后对同一站点的所有请求自动携带。关键点在于:每个线程都有自己独立的"Cookie 存储区",也就是说,用 Cookie 管理会话的网站,你用 100 个线程去压,这 100 个线程各自维护一套会话,互不串台。这正好模拟了"100 个不同用户各自登录"的场景。Cookie 缓存配置通常选 standard(标准)或 compatibility(兼容)。

另一个高频元件是 HTTP 请求默认值(HTTP Request Defaults)。当一个压测脚本里有几十个取样器,而它们的协议、IP、端口(甚至公共的请求头)都一样时,挨个填既啰嗦又容易改漏。把这些公共项抽到"请求默认值"里,下面那些取样器只要填自己独特的路径和方法即可;将来换服务器环境,只改这一个节点就行。它上面挂的位置决定作用域——挂在线程组下,则罩住整个线程组。

<!-- HTTP 请求默认值:公共协议/IP/端口抽在这里,子取样器可省略 -->
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="公共服务器配置">
  <stringProp name="HTTPSampler.protocol">http</stringProp>
  <stringProp name="HTTPSampler.domain">api.example.com</stringProp>
  <stringProp name="HTTPSampler.port">443</stringProp>
</ConfigTestElement>

参数化(一):为什么登录压测不能只有一套账号

在讲具体元件前,先把这个动机讲透,因为它贯穿性能测试始终。假设你要压测"登录接口",假设你在 HTTP 取样器里写死了一个 username=tom、password=123。这会有两个大问题:

  1. 真实性不足:真实系统是成千上万不同用户登录,你用同一个账号反复登录,很多系统会有"同一账号重复登录""单点登录互踢""账号被临时锁定"等机制,导致压出的数据失真;
  2. 测试可控性差:你想知道系统在"大量不同用户"下表现如何,写死一套账号根本模拟不了。

这就是**参数化(Parameterization)**要解决的问题——把脚本里"写死的值"变成"运行时从外面取的值"。JMeter 里常用的参数化手段有:用户定义的变量(适合全局统一管理、个数少)和 CSV 数据文件设置(适合从文件批量读取大量参数,也就是我们主要要讲的)。我们先看"用户定义的变量"。

用户定义的变量

**用户定义的变量(User Defined Variables)**是一个配置元件,作用是定义一组"全局可引用"的变量,在脚本里用 ${变量名} 的方式引用。它适合那些"需要在多个取样器里统一用、改一处全局生效"的参数,比如测试环境的 Host、公共的 token、统一的 appid。

# 使用示例:在用户定义的变量里定义 host = api.example.com
# 然后在 HTTP 取样器的"服务器名称"一栏填 ${host}
# 将来换成预发环境,只需把 host 的值改成 pre.example.com,所有引用处一次性变更

它的一个特点是作用域明确:挂在测试计划下全局生效,挂在某个线程组下则只在这个线程组内生效。如果某个参数"只想要在固定的场景里用、不想影响其他脚本",就把"用户定义的变量"放进对应那个线程组/作用域里去——这也正是作用域概念的实际应用。

参数化(二):CSV 数据文件设置——真正的批量用户来源

当测试需要"大量不同的用户",比如几百个 username/password 对时,手动定义几百个变量显然不现实。这时候就轮到 CSV 数据文件设置(CSV Data Set Config) 登场。它是参数化压测里最常用、也最重要的一个配置元件。

它的工作方式不复杂:JMeter 逐线程、逐行地从一个 CSV 文件里读取数据,把读到的每一列赋给我们指定的变量名,供取样器用 ${变量名} 引用。读取方向是"从上往下依次取"。配置它需要理解几个字段:

  • 文件名(Filename):CSV 文件的绝对路径。强烈建议写绝对路径,因为相对路径的解释依赖于 JMeter 的启动目录,在命令行压测时很容易找不到文件。
  • 文件编码(File Encoding):强烈建议 UTF-8,尤其当 CSV 里含中文(比如中文用户名)时。
  • 变量名称(Variable Names):CSV 每一列对应的变量名,多个变量用逗号分隔。比如 CSV 有两列,这里填 username,password。顺序与 CSV 的列顺序一一对应。
  • 是否忽略首行(Ignore First Line):CSV 第一行如果是表头(如 username,password),勾上 True 就跳过它,只读数据行。
  • 分隔符(Delimiter):必须和 CSV 文件里的列分隔符一致,默认英文逗号 ,。若 CSV 文件实际用的是分号或制表符,这里要对应改成 ; 或(制表符用一个字符表示)。
  • 遇到文件结束符再次循环(Recycle on EOF):
    • 选 True:数据不够时,从头再取一遍,实现"循环复用";
    • 选 False:数据不够时不再循环,此时必须再勾选"遇到文件结束符停止线程(Stop thread on EOF)",否则线程会因为取不到数据而报错。
  • 线程共享模式(Sharing Mode):控制多个线程如何共享这份数据(如所有线程共享同一条指针、或每个线程独立从头读取),默认"所有线程共享"。
<!-- CSV 数据文件设置 .jmx 示意 -->
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="登录用户数据">
  <!-- 数据源文件:绝对路径,含一列或两列 -->
  <stringProp name="filename">D:\data\users.csv</stringProp>
  <!-- 文件编码:UTF-8,防中文乱码 -->
  <stringProp name="fileEncoding">UTF-8</stringProp>
  <!-- 变量名:CSV 第一列给 username,第二列给 password -->
  <stringProp name="variableNames">username,password</stringProp>
  <!-- 首行是表头,要跳过 -->
  <boolProp name="ignoreFirstLine">true</boolProp>
  <!-- 列分隔符:英文逗号,须与 CSV 实际分隔符一致 -->
  <stringProp name="delimiter">,</stringProp>
  <!-- 数据不够时是否从头循环 -->
  <boolProp name="recycle">true</boolProp>
  <!-- 若选到文件尾,是否停止线程(recycle 为 false 时务必勾上,否则报错) -->
  <boolProp name="stopThreadOnEOF">false</boolProp>
</CSVDataSet>

配套地,你需要在磁盘上准备一个 users.csv,形如:

username,password
user01,pass01
user02,pass02
user03,pass03

然后在登录取样器里把写死的账号改成 ${username}、${password},再配合把线程数调到与账号数量匹配(让每个线程取到不同的账号),压测时系统就会用这批账号去登录。这里藏着一个参数化压测的黄金法则:要压出"真实多用户"的负载,就让"线程数"和"可用测试账号数"落在同一天平上,否则要么账号复用失真,要么有线程拿不到账号直接失败。

JSON 提取器:让后一个接口用上前一个接口的返回值

真实业务几乎都是"接口链路":先登录拿 token,再拿 token 去查数据,再把拿到的 id 去下单。在 JMeter 里,如何把"上一个接口的响应值"动态交给"下一个接口当参数"?这就要靠 后置处理器 家族的 JSON 提取器(JSON Extractor)。

因为现代的 HTTP API 十个里有八个返回的是 JSON,而 JMeter 又内置了 JSONPath(一种在 JSON 文档里定位元素的查询语言,正则表达式之于字符串,就相当于 JSONPath 之于 JSON),所以 JSON 提取器成为接口关联(即接口之间传参)用得最多的手段。它的执行时机在"取样器返回响应之后、下一个取样器发出之前"——符合我们前面讲的"后置处理器先于下一个取样器"的顺序。

用法:在某一个 HTTP 取样器上右键"后置处理器 → JSON 提取器",然后配置:

  • JSON Path Expressions:用 JSONPath 表达式选中你想取的值,例如取所有 blogId:$..blogId;取第一个元素的 blogId:$.[0].blogId。
  • Variable Names:把取到的值存到哪个变量名,供后面用 ${变量名} 引用。语法与"用户定义的变量"一致。
  • Default Values:当 JSONPath 没取到值时的兜底默认值,建议给个显眼的占位,方便你在结果里一眼看出"这条没取到",而不至于把空值默默传给下一个接口。
<!-- JSON 提取器 .jmx 示意:从响应里取第一个 blogId,存进变量 blogId -->
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取 blogId">
  <!-- 取到的值存入的变量名 -->
  <stringProp name="JSONPostProcessor.variable_names">blogId</stringProp>
  <!-- JSONPath 表达式:$..[0] 表示取第一个元素的 blogId -->
  <stringProp name="JSONPostProcessor.jsonPathExprs">$..[0].blogId</stringProp>
  <!-- 取不到时使用的默认值 -->
  <stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
</JSONPostProcessor>

后续的"查看博客内容"接口,只要在路径或参数里引用 ${blogId},就会自动带上上一个接口返回的真实 id。这一步就是我们常说的"接口关联"——它让 JMeter 从"打个孤立的接口"升级成"跑一条真实业务链"。

JSONPath 常用操作符留给你的速查表(出处是 JsonPath 官方文档):

操作符含义
$根元素
@当前元素
*通配符,所有节点
..深度搜索,选择所有符合条件的节点
.<name>子元素
['<name>' (, '<name>')]括号表示子元素或子元素列表
[<number> (,<number>)]数组索引或索引列表
[start:end]数组切片
[?(<expression>)]过滤器,表达式求值为布尔值

例如对于一个博客列表 JSON,$..blogId 取出所有 blogId;$.[0].blogId 取第一个元素的 blogId。前者适合"想遍历全部",后者适合"拿单个来用"。

JSON 断言:响应码 200 不等于请求成功

初学者常有一个误解:接口返回了 200,就代表这次压测里的请求是"成功的"。真相对吗?大多数接口即使业务失败(比如密码错误、库存不足、参数非法),只要 HTTP 层处理了,就还是返回 200,错误信息藏在响应体里。那么聚合报告里如果只看"错误率",就会把一大批"HTTP 200 但业务失败"的请求误判为成功——这是压测数据造假最常见的一种。

你可能会说:那是功能测试的事,性能测试还关心这个?恰恰要关心。因为如果业务失败率高,响应往往比正常路径更快或更慢,直接污染"响应时间"和"吞吐量"这两个核心指标。所以性能脚本里同样要加断言(Assertion),来按业务规则判定本次请求到底算不算成功。

JMeter 里的断言,就是"给取样器结果再补一道校验,判定它是 PASS 还是 FAIL"的元件。常用的是 JSON 断言(JSON Assertion),用法和 JSONPath 一脉相承:配置一个 JSONPath 表达式来定位响应体里某个字段,再用期望值去比对。

配置它的几个关键开关(含几个容易踩的细节):

  1. "Additionally assert value"(是否断言值):
    • 不勾选:只检查"这个字段在不在响应里"(存在性校验),不看它的值;
    • 勾选后:必须再填"Expected Value"把期望值写出来。
  2. "Match as regular expression"(按正则匹配):
    • 不勾选:期望值必须完整精确匹配,少一个字符都判断言失败;
    • 勾选:期望值可以是部分关键词,能正则匹配上就算通过。拿不准时,通常勾正则 + 写关键词更抗"响应体多点多余字段"的情况。
<!-- JSON 断言 .jmx 示意:校验响应里的 code 字段是否精确等于 200 -->
<JSONAssertion guiclass="JSONAssertionGui" testclass="JSONAssertion" testname="校验业务码">
  <stringProp name="JSONAssertion.jsonPath">$.code</stringProp>
  <!-- 是否断言值:true 表示要比较具体值 -->
  <boolProp name="JSONAssertion.expectation">true</boolProp>
  <!-- 期望值 -->
  <stringProp name="JSONAssertion.expectedValue">200</stringProp>
  <!-- 按正则匹配:true 时只要求能匹配上关键词 -->
  <boolProp name="JSONAssertion.regx">true</boolProp>
</JSONAssertion>

$.code 里的 $ 就是上一节说的"根元素",.code 是取 code 这个子字段。加好断言后,聚合报告里的"错误率"就不再是纯粹看 HTTP 状态了,而是真正代表"HTTP 层或业务层判定失败"的请求占比。

同步定时器(集合点):让请求真正"同时"压下去

现在到了性能测试里一个很有意思、也最容易出错的概念:如何让一堆请求真正"同时"打过去。

你可能以为:100 个线程,就自动是 100 个请求在同一毫秒发出。并不是。 线程们各自按自己的节奏跑——先发的线程已经在等响应了,后发的线程可能还没启动;哪怕同时启动,各自的准备时间、网络握手时间也参差不齐。这样一来,你看到的其实是"错落开"的请求,而不是瞬间齐发。

同步定时器(Synchronizing Timer)就是来解决这个问题的,它也常被称作"集合点(rendezvous point)"。它的作用,直观理解就像红绿灯:大家先到路口等,谁先到谁红灯下等着,等"攒够指定数量"(或者超时到了),绿灯才放行,所有人一起出发。业界有个比喻:现实中过马路,有人先到有人后到,但必须等绿灯所有人才能一起过人行道——同步定时器就是那个红绿灯。

在 JMeter 里给取样器前面加一个同步定时器,配两个参数:

  • Number of Simulated Users to Group(同时释放的线程数):攒够多少个线程就一起放行。通常设成等于线程数,表示"所有线程都到位后一并启动",营造瞬间高峰压力。
  • Timeout in Milliseconds(超时,毫秒):如果等太久始终凑不齐,超过这个时间就"有多少放多少",避免一直干等导致线程阻塞。建议给一个合理的超时阈值。
<!-- 同步定时器 .jmx 示意:攒够 100 个线程同时放行 -->
<SynchronizingTimer guiclass="SynchronizingTimerGui" testclass="SynchronizingTimer" testname="登录集合点">
  <!-- 达到多少个线程就一起释放 -->
  <integerProp name="SynchronizingTimer.numUsers">100</integerProp>
  <!-- 超时:等满这个毫秒数后,即使没凑齐也先放行 -->
  <integerProp name="SynchronizingTimer.timeout">30000</integerProp>
</SynchronizingTimer>

这里必须给你泼一点冷水,划清边界:集合点能让请求"几乎同时从压测机发出",却不能保证"同时到达服务器"。因为从压测机到服务器之间还有网络传输,请求在路上、在服务器排队队列里的先后仍可能有毫秒级差异。所以严谨的说法是:同步定时器"较真实地模拟并发的发起"——它让瞬间压力更大、更贴近真实的高并发冲击,但它不是魔术,别指望它做到绝对零延迟的同时到达。理解了这个边界,你才不会被压出来的数据骗到。

事务控制器:把多个接口打包成一个"业务动作"

压测业务链路时,我们真正关心的往往不是"单个接口快不快",而是"用户完成一整个操作快不快"。比如"下单"这一步,后端其实要依次调:验证库存 → 创建订单 → 扣减金额 → 返回订单号。前面聊关联时我们把这些接口串起来了,但问题来了:报告里每个接口各记各的响应时间,我该怎么看"整件事"花了多久?

这就轮到 事务控制器(Transaction Controller)。它的作用一句话讲清:把它下面嵌套的所有取样器(以及取样器之间的耗时)视作一个整体,作为一个"事务"来度量总耗时。不加事务控制器时,一个取样器(一个接口)就是一个事务;加了它,下面一串接口就被"打包"成一个大事务,报告里会出现这个事务的总时间。

<!-- 事务控制器 .jmx 示意:把"登录+查数据+下单"打包成一个下单事务 -->
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="下单全流程">
  <!-- 是否把响应时间以外的思考/定时时间也算进事务耗时 -->
  <boolProp name="TransactionController.includeTimers">true</boolProp>
  <HTTPSamplerProxy testclass="HTTPSamplerProxy" testname="验证库存"/>
  <HTTPSamplerProxy testclass="HTTPSamplerProxy" testname="创建订单"/>
  <HTTPSamplerProxy testclass="HTTPSamplerProxy" testname="扣减金额"/>
</TransactionController>

为什么它重要?因为性能测试尤其看重"端到端的业务时延"。有时候单个接口平均 50ms,但用户整条链路要走 1.2 秒——后者才是真实的体验。用事务控制器度量整条链,你的报告才反映"真实用户体验",而不是被拆碎的单接口数字。

JMeter 插件与梯度压测(Stepping Thread Group)

标准的 JMeter 线程组是"固定线程数 + ramp-up"这种比较朴素的模型。但真实企业压测里,我们常想要的不是"一下子上 1000 个线程",而是像阶梯一样,每到一个台阶稳定一会儿,再加一批——即逐步加压,方便观察系统"在哪个并发量开始劣化"。这种场景就需要 JMeter 插件(JMeter Plugins) 来扩展能力。

先装插件管理器

JMeter 官方并不内置全部插件,需要先装一个"插件管理器(Plugins Manager)"。常规做法:

  1. 从 https://jmeter-plugins.org/install/Install/ 下载 plugins-manager.jar;
  2. 把它放进 JMeter 的 lib/ext 目录;
  3. 重启 JMeter。之后 GUI 右上角会多出一个"小蝴蝶"样的图标,点击即可打开插件管理器;
  4. 通过插件管理器去勾选安装其他插件(比如梯度线程组、更多监听器),点 "Apply Changes and Restart JMeter" 就会自动下载所需插件并重启生效。
# 把插件管理器 jar 放进 JMeter 的核心扩展目录后重启即可
# (Windows 下路径请换成你实际的 JMeter 安装目录)
cp plugins-manager.jar D:/tools/apache-jmeter-5.5/lib/ext/
# 然后重启 JMeter,右上角出现"小蝴蝶"图标即安装成功

Stepping Thread Group:梯度压测线程组

装上插件后,在线程组里就能看到多出一些选项,其中企业压测最常用的就是 Stepping Thread Group(梯度压测线程组)。它的关键配置项正好对应"阶梯加载"的各个阶段:

  • This group will start:总共要启动多少个线程(相当于最终的总线程数)。
  • First, wait for:真正的请求开始前,先等待多少秒(常设 0)。
  • Then start:第一阶段一开始先上多少个线程(常设 0,表示不瞬间起,而从 0 起步)。
  • Next, add N threads:每一个"台阶"新增多少个线程。
  • every X seconds:每隔多少秒走一个台阶,即上一次新增线程完成后再隔多久加下一批。
  • using ramp-up:每批线程的启动时长;若设为 5 秒,表示每新增一批线程用 5 秒拉起。
  • then hold load for:所有线程全部到位后,保持满负载持续运行多长时间。
  • finally, stop N threads every X seconds:测试结束阶段,每多少秒释放多少个线程(例如"5 个 / 1 秒",就是持续负载结束后每 1 秒释放 5 个线程,做平缓退场)。

把"先小批量 → 逐个台阶加上去 → 满负载保持 → 再阶梯释放"串起来,就是标准的梯度压测。它能帮你在一条测试里看到"系统在哪个并发台阶出现响应时间拐点、错误率爬升、吞吐量见顶",是定位容量与瓶颈的利器(常配合我们后面讲的三大指标来分析)。

常见监听器:观察性能的实时窗口

压测跑起来后,如何"看"结果,是监听器的职责。我们统一讲三个最常用的:

  • 聚合报告(Aggregate Report):压缩到一个表格里,展示每个请求的样本数、平均响应时间、中位数、90%/95%/99% 分位、最小/最大、错误率、吞吐量等。这是压测结果分析的主力,下一节专门展开。
  • Response Times Over Time(响应时间随时间变化):一张折线图,横轴是运行时间、纵轴是响应时间(毫秒)。它让你直观看到"随着加压,响应时间在什么时刻出现突变/波动"。如果某个时间点响应时间突然飙升,往往意味着系统在那个时刻撞上了瓶颈、GC(垃圾回收)抖动、或某台节点告警。聚合报告给的是"总体统计",这图给的是"时间走势"——两者互补。
  • Transactions per Second(每秒事务数,TPS):反映吞吐量的实时走势,横轴时间、纵轴每秒完成的事务数。TPS 越高说明系统处理能力越强;图里通常红色代表成功(通过的 TPS),绿色代表失败(失败的 TPS)。观察"TPS 到某个值后不再上升甚至掉头向下",往往就是容量顶点了。

这里的口径沿用课件的两分法:Response Times Over Time 看"响应时间的实时平均与走向",TPS 监听器看"吞吐量的实时变化与瓶颈拐点",两者从不同维度反映同一场压测。专业地讲,"TPS"是指每秒事务数,即客户机向服务器发起请求、服务器完成响应的这一整个动作在一秒内完成的件数,它直接刻画系统同一时间处理业务的最大能力。

聚合报告:把关键项一个一个读懂

**聚合报告(Aggregate Report)**是一张核心表格,对你整个压测里每一个命名不相同的请求,各输出一行统计。逐列把它们讲清楚——这也是面试和实战里考得最实的一块。注意:同名请求会在聚合报告里被合并成一行,所以给取样器的命名一定要有意义,否则分析时你都不知道数据属于谁。

聚合报告列含义(单位:ms 的为毫秒)
Label取样器(或事务)的名称,即这一行统计的是谁
# Samples这个请求的总样本数。线程数 × 循环次数 即大致总请求量
Average平均响应时间。注意它容易被"极少数特别慢的请求"拉高,不能只看它
Median(中位数)把响应时间排序后,处于正中间的那个值,即 50% 分位:一半请求比它快、一半比它慢
90% Line90% 分位:90% 的请求耗时不超过这个值
95% Line95% 的请求耗时不超过这个值
99% Line99% 的请求耗时不超过这个值,看"长尾里的最差体验"
Min所有样本中最小的响应时间
Max所有样本中最大的响应时间
Error %错误率 = 判定失败(HTTP 或断言失败)的请求数 / 总样本数
Throughput吞吐量,默认是"每秒完成的请求数"(Request/Second),数值小时 JMeter 也可能按每分钟显示,读的时候留意单位
Received KB/sec每秒从服务器接收的数据量
Sent KB/sec每秒向服务器发送的数据量

为什么不能只看 Average,非要看分位?——百分位的意义

举个例子你就懂了。假设一场压测里有两个请求:99 个请求都只要 0.1 秒,唯独 1 个请求卡了 100 秒。平均响应时间被拉高到了大约 1 秒多——可实际上绝大多数用户的体验是极好的 0.1 秒。反过来,如果 99 个请求都要 2 秒、1 个要 0.1 秒,平均只有约 2 秒,又掩盖了"几乎所有用户都很慢"的真相。平均值对"少数极端值"非常敏感,用它当唯一依据会得出完全跑偏的结论。

所以我们要引入百分位(percentile/分位数):把这段时间内所有样本的响应时间从小到大排序,位于"第 p% 位置"的那个值,就是第 p 百分位,它表示"有 p% 的请求比这个值快(耗时不超过这个值)"。于是:

  • Median(50% 分位) 描述"典型用户"的体验;
  • 90% Line(90% 分位)、95%/99% Line 描述"绝大多数用户、乃至最倒霉那批用户"的体验。90% line 是性能指标里被提及最多的一个:它规避了平均值的欺骗性,比 Average 更可靠地代表"大多数用户的真实体感"。

所以你在前端页面上常看到的要求——"接口 90% 响应时间 ≤ 500ms",意思就是"90% 的请求要在 500ms 内返回"。判定是否达标,看 90% Line(以及更高分位)比看 Average 更有说服力。

关于"THT"以及读表的正确姿势

有相当数量的课件/物料在讲聚合报告时,会把它概括成"Avg / Min / 90% line / THT"这几个词。这里要负责任地跟你说明一句:在 JMeter 聚合报告的标准列里,并没有一个叫 THT 的列。我核对过官方文档与主流社区资料,标准列就是上面表格那 11 项。上下文语境里,"THT"通常指的是**吞吐量(Throughput)或每秒事务数(TPS/总吞吐)**一类的能力指标——你看,早早地就在两处地方见到"吞吐量"这个词了,它要么挂在 Throughput 列(请求数/秒),要么出现在"事务吞吐"的语境里。因此当你再看到别人把报告概括成"Avg/Min/90%/THT"时,他心里想表达的每一个词,你上面的表里全都能对上:平均、最小、90% 分位、吞吐量。这样无论按哪种资料去读,你都不会被术语绕晕。

读表建议遵循"先横后纵、先整体后长尾"的顺序:

  1. 先看Error%(错误率):不为 0 就要先排查"是 HTTP 失败还是断言失败",把基础搞干净再谈性能;
  2. 再看 90% Line / Average:判断"大多数用户"快不快;
  3. 最后看 99% Line 和 Max:判断长尾里有没有"极慢的请求"被系统无情暴露——99% 分位或 Max 异常偏高,往往是慢查询、锁等待、GC 停顿的典型信号。

命令行压测与 HTML 报告生成

前面一直强调"正式压测要用命令行"。JMeter 提供了很简洁的一套命令行参数,把"跑压测 + 生成一个可视化 HTML 报告"一步到位。这就是课件里那条核心命令的出处,我们把每个参数用行注释拆开:

# 命令行无界面压测,并把结果写成 jtl 日志、同时生成 HTML 报告
jmeter -n -t test_plan.jmx -l result.jtl -e -o report_dir
#        ↑                                  ↑           ↑
#   -n: 无图形化运行(non-GUI),正式压测必须用这个
#   -t: 指定被测脚本文件(.jmx)
#   -l: 把每条请求的运行信息写入 jtl 日志文件(后面可复用去生成报告)
#   -e: 测试结束时基于 jtl 生成 HTML 报告
#   -o: 指定 HTML 报告的输出目录

逐参数解释一遍:

  • -n:non-GUI,无图形界面运行。理由我们说过——省内存、避免 GUI 抢资源。
  • -t:被运行的脚本文件,即你在 GUI 里搭好并保存的 .jmx。
  • -l:将运行结果写入日志文件,后缀惯例是 .jtl(JMeter Test Log)。这个 jtl 文件是"过程数据",里面一行一条请求的明细,可以用来随时重新生成报告,也可以事后导入 GUI 的聚合报告里复盘。
  • -e:生成测试报告。它会读取 -l 指定的 jtl,产出一个 HTML 报告。
  • -o:指定 HTML 报告的输出目录。

这里有两个必须刻进脑子的坑:

  1. -o 指定的输出目录必须是"不存在,或已存在但为空"。如果目标目录里已经有非空文件,JMeter 会直接报错。所以你在命令行里那条命令之前,先把这个目录清空(或换一个全新目录)。
  2. -l 的 jtl 文件如果已经存在,JMeter 默认是"追加写入"的。这意味着旧数据会和新数据混在一起,报告可能被历史脏数据污染。所以每次正式压测,建议也先删掉旧的 jtl(或用新的 jtl 文件名)。这条与"输出目录要空"是同一类"先清理再运行"的纪律。
# 干净的压测/报告生成循环(Linux/Mac 用 rm,Windows PowerShell 用 Remove-Item)
rm -rf report_dir        # 1. 删掉旧的输出目录(确保为空)
rm -f result.jtl         # 2. 删掉旧的 jtl(避免追加脏数据)
jmeter -n -t test_plan.jmx -l result.jtl -e -o report_dir
#                         3. 执行压测并生成报告

命令结束后,报告目录里会生成一个 index.html,用浏览器双击打开,你就能看到一份完整的性能报告,通常包含整体统计、APDEX(应用性能指数)评分、请求各项指标、响应时间分布、不同线程下的表现等。这份 HTML 报告,才是一份可以发给团队的、说服力足够的正式交付物。后续若想持续观察,也可以配合"Backend Listener"把结果实时推送到监控存储,这个话题我们点到为止。

性能分析:通过三大指标定位问题

工具拿到了、报告生成了,最后一步也是最关键的一步,是对着报告"做诊断"。性能分析不外乎围绕三大指标展开:响应时间、错误率(可靠性)、吞吐量。逐一说清楚。

指标一:响应时间

响应时间就是"一个请求从发出到收到响应"所需的时间。如果响应时间超过了预期要求,基本可以判定系统到了瓶颈。

分析要点:要能说清楚"是在多少个线程(多高并发)时开始超标的"。这才是定位价值所在——单纯说"响应时间慢了"没有意义,得指出"在并发 500 时响应时间从 300ms 涨到 2s",开发和架构才有方向。

响应时间变高的常见原因分两类:

  • 系统本身不稳定:时快时慢,响应时间大幅波动,往往指向资源争抢、GC 停顿、依赖的服务偶发变慢;
  • 随着并发压力加大而逐渐变慢:响应时间随并发近似线性爬升,多半是服务器算力/连接池/数据库等"吞吐能力"到达上限,排队时间被拉长——这是最典型的"瓶颈"形态。

指标二:错误率(可靠性)

错误率(Error %) 考察的是高并发场景下系统能否稳定正常处理业务。业界对可靠性指标要求很高,常见目标是"99.99% 可靠"甚至更高的"SLA(服务等级协议)"要求——超大规模系统常以"几个 9"来衡量全年可用时长与失败容忍度。

错误率变高的原因,通常从这几层去查:

  • 接口请求本身报错:如参数错误、依赖的系统返回 5xx,先把业务和链路层的错揪出来;
  • 服务器无法继续处理:达到瓶颈——代码写得不好(如内存泄漏导致内存耗尽)、硬件资源(CPU/内存/磁盘)耗尽;
  • 后端自我保护机制被触发:限流、熔断、降级。这三个词是微服务高并发场景的高频词,也常一并被考。

顺便把熔断和降级这对兄弟给你讲清楚,因为它们是"错误率升高"背后最常见的元凶,也是面试常客:

  • 熔断(Circuit Breaking):为了防止"某个下游服务的故障"拖垮整个系统。设想电商收银时同时调微信支付和支付宝,某天微信支付失败率突然飙升、超时严重,系统就可以"临时把微信支付这个渠道熔断掉",把流量切到健康的支付宝——牺牲一个支路,保住整体不为它所累。名字里的"断路"借用了电路保险丝的概念:电流过大就跳闸,保护整个电路。
  • 降级(Degradation):主动关闭一些非核心功能,把资源让给核心功能。经典例子:某次大型视频平台故障时,用户页面拿不到真实昵称,就默认显示"腾讯用户"这类兜底名称——用兜底展示换业务可用性,这就是降级。

区分这对概念就记一句话:熔断是"把坏掉的路断掉、别让它拖垮全局";降级是"主动砍掉非核心保核心"。前者是被故障触发,后者常是预案性的主动取舍。

指标三:吞吐量

吞吐量(Throughput) 指单位时间内系统能处理的请求/事务数量,聚合报告里通常是"请求数/秒"。原则很简单:吞吐量越大,性能越好;吞吐量相对稳定甚至变低,说明系统可能已经摸到性能瓶颈。

吞吐量的变化规律,可以对照着读:

  • 波动很剧烈:系统性能不稳定,大概率有偶发的资源争抢、依赖抖动、或压测机本身成为瓶颈;
  • 随并发慢慢变高、趋于稳定:和并发量强相关,规则是"并发量小于系统吞吐潜力时,增多并发,吞吐量也同步增加"——因为此时系统还有余力处理更多请求;
  • 慢慢变低、并发也减少:两个方向——要么是压测快结束、并发主动降低;要么是系统已卡顿,响应时间变慢,导致单个线程能发起的请求数变少,吞吐量随之被拖下来。这种"受响应时间反噬而下降的吞吐量",往往是系统已重度过载的强烈信号。

综合起来,一个熟练的性能工程师看报告时的动作是固定的:先看错误率排除脏数据,再看 90% 分位和平均值判断体感,再看 99% 分位找长尾,最后用吞吐量的走势判断"现在蹲在拐点前还是拐点后",从而定位瓶颈的确切位置。

几个绕不开的坑:本机远程、线程不等于并发、报告口径

最后,集中讲三个高频坑。它们几乎决定了你的压测"数据可信不可信",值得单独拎出来掰开揉碎。

坑一:压测本机 vs 压测远程

很多人在本机起一个服务,又在本机用 JMeter 去压它——比如 JMeter 就装在 127.0.0.1 指向的那台机器上。这在大规模压测里是个严重的错误,原因有两类:

  1. 资源竞争(最致命):压测本身要消耗 CPU 和内存(JMeter 每个线程都是 JVM 里的真实线程、每个连接都是真实 TCP 连接)。当压测机和被测服务器跑在同一台机器上时,压测在跟被压的服务器抢 CPU/内存/网络/文件句柄。结果就是:不是服务器真的慢,而是被压测机拖慢了;或者服务器根本没被压满,瓶颈其实出在压测机自己身上。你统计出的响应时间、吞吐量全都失真。
  2. 网络层失真:本机请求几乎没有网络延迟,无法体现真实用户从外网访问的往返时间(RTT);而且本机回环(loopback)接口的吞吐能力远高于真实网卡,会给你一个"虚高"的吞吐量假象。

所以压测的纪律是:压测机(压力发生器)和被压服务器要分机部署,且压测机资源要留足余量。压测机本身也最好是多核、内存足够,必要时用多台压测机做分布式压测(JMeter 的 jmeter-server + -R 远程调用)来分摊压力。记住一句:你压测的数字反映的是"压测机 + 服务器"这条链路的联合表现,谁变成瓶颈,数字就归谁。

坑二:线程数不等于真实并发

再看线程组那里埋下的伏笔。在 JMeter 里"100 个线程"绝不等于"100 个请求同时在进行"。为什么?

因为一个线程的典型节奏是:发出一个请求 → 等它返回 → 再做点什么(思考时间、下一个取样器)→ 再发。所以在任何一瞬,同时"刚好在途"的请求数(真正的瞬时并发)大约 = 线程数 × (单请求耗时 / 完整循环周期)。如果你给每个线程加了"思考时间",或者一个循环里有多个顺序执行的取样器,那么同刻在途的请求会远少于线程数。反过来,如果想让并发更贴近线程数,就要把思考时间设小、把 Ramp-up 收敛,并用同步定时器(集合点)让线程"同时释放"。

还有一个更细的坑:Ramp-up 设太短,会造成"起跑瞬间的尖峰"而非"稳定并发"。比如 1000 个线程 Ramp-up 设 1 秒,前 1 秒服务器可能被一波齐射打得措手不及,之后才回落到稳态——这与你想要描述的"稳定 1000 并发"根本不是一回事。所以解读报告时,务必想清楚:你看到的并发曲线,到底是"稳定并发的状态量"还是"瞬间冲击+回落的瞬态"。

坑三:报告里的百分位口径(percentile)别被平均骗了

这个坑在"聚合报告"一节已经铺过,这里把它和"结果可信性"挂钩再强调一次。读任何报告的响应时间,先确认你看的是分位还是均值。

  • 平均值(Average)易被极端值拉偏;
  • 中位数/90%/99% 分位才反映"大多数用户"和"长尾用户"的真实体感。

同时注意:百分位不是你随便眯一眼"把最后一个值当 100%",它对样本量有要求。样本过少(比如只发了几个请求),分位数就没有统计意义。正规做法是压测样本量要足够大、时长要足够长,此时 90%/99% 分位才有参考价值。此外,写进验收标准、写进 SLA 的响应时间要求,要明确写清"是第几分位、什么口径下达标"(比如"90% 分位 ≤ 500ms"),否则评审时就会因为"你说平均 300ms 达标、我说实话说 99% 的人都 2s"而吵起来——本质是没对齐统计口径。

坑四:参数化做不好,压出来的是假数据

这个坑我在参数化那一节已经埋了,这里把因果闭环讲透。参数化不只是"为了模拟不同用户",更是为了保证压测数据的真实性。 常见翻车点:

  • 一套账号反复登录:触发系统的去重/锁号/单点互踢逻辑,制造出大量"非业务的失败",错误率虚高,响应时间也被污染;
  • 账号数量不够线程数:CSV 配了 recycle=false 却又没勾"停止线程",结果线程取不到数据直接报错;或者配了 recycle=true 全程复用,又退回到"一套账号"的老路;
  • 环境变量/数据对不上:CSV 里的字段名你写的是 username,password,取样器引用却写 ${user},引用不到会静默取默认值或空串,请求都打在中国式错误上。

所以参数化的三条自检:

  1. 账号(或其他参数)的数量,要 >= 线程数,让每个线程都有独一份可用的账密;
  2. 引用名和 CSV 的 variableNames 严格一致,别张冠李戴;
  3. 文件编码、分隔符、首行是否跳过,要和真实 CSV 文件对齐,尤其中文和特殊分隔符。

把这四条坑全部避开,你的压测结果才谈得上"能用来做决策"。

思考题与详解

课堂讲完了,来兑现承诺——下面这些思考题,每道后面都跟着详细解答,先自己想想再对答案。

思考题 1:为什么说"正式压测必须用命令行而非 GUI 模式来跑"?请从资源占用和结果可信度两个角度解释。

答案与详解

GUI 模式一方面要花 CPU/内存去绘制界面、渲染取样器结果树、刷新图表,这些开销和真实的压测请求抢同一个 JVM 的资源;另一方面 JMeter 的线程本质是 JVM 里的真实 Java 线程,加上 GUI 后,压测机能承担的负载会明显下降。在资源有限的压测机上,"多余的 GUI 开销"直接导致你能施加的最大压力变小,或者把本就该属于服务器的资源消耗掉,让测得的数据失真(尤其"压测本机 vs 远程"那个坑,叠加 GUI 会更严重)。而命令行模式几乎只有纯请求逻辑在跑,资源利用率高得多,也更适合无人值守地在服务器/CI 上长期运行。所以纪律是:GUI 搭脚本、命令行跑真压。

思考题 2:聚合报告里出现"Average 是 500ms,但 90% Line 是 1.2s"这种情况,说明了什么?该以哪个为准判断是否达标?

答案与详解

说明少数请求很快、把平均值拉低了,而大多数请求其实偏慢。平均数是所有样本加总再除总数,它会被很小的值拽下来;而 90% 分位代表"90% 的请求耗时不超过这个值",更能反映大多数用户的真实体验。判断达标应该以分位为准(比如业务要求 90% 分位 ≤ Xms 就看 90% Line),并且补一个"旁证"——99% 分位和 Max,看看长尾里是不是还有更夸张的慢请求。总之,只看 Average 下结论,很容易被极端值骗过去。想让曲线更稳,可以检查是不是有极少数特别慢的请求拖住了 99% 分位并在 99% 分位那个位置做优化。

思考题 3:在线程组里设了"线程数 = 100,Ramp-up = 0,循环次数 = 1",压测登录接口。这能不能代表"100 个用户真正同时并发登录"?如果不能,它实际是什么形态?

答案与详解

不能直接代表"同时并发"。Ramp-up=0 只是让 100 个线程在同一时刻"启动"(被创建出来),但每个线程从创建、初始化、建立 TCP 连接、发第一个请求这几步的完成时刻仍有先后,所以请求是"在极短的一小段时间内陆续发出",而不是绝对的同一毫秒。要更接近"同时齐发",需要在登录取样器前面加同步定时器(集合点),设置"同时释放的线程数 = 100",让线程先聚齐再一并放行。即便如此,也如正文所说,请求"同时从压测机发出"仍不等于"同时到达服务器"。所以严谨的说法是:线程数 Ramp-up=0 + 同步定时器,能非常接近地模拟"同时并发",但仍有毫秒级的边缘误差,分析数据时心里要留这个余量。

思考题 4:压测脚本里你给登录接口加了一个 JSON 断言,校验响应体里 $.code 是否等于 "200"。运行后发现错误率很高,查看结果树里却发现 HTTP 状态码是 200。请推断可能的原因。

答案与详解

这恰恰就是"HTTP 200 不等于业务成功"的写照。最可能的原因:业务实际失败时,接口依旧返回 HTTP 200,但响应体里的 code 字段不是 "200",而是非 0 的业务错误码(比如 "50001")。此时 HTTP 层状态码正常,但断言比对不通过,取样器被判定为失败,于是错误率升高。另一个可能是 JSONPath 写错了(比如实际结构不是 $.code 而是 $.data.code),导致取到的值是默认值/default 或空,比对失败。排查顺序:先在查看结果树里看真实响应体,确认 code 字段的实际路径和取值;再回头核对 JSON 断言的表达式、期望值、正则开关是否正确(不勾正则时期望值必须完整精确匹配)。

思考题 5:你要压"下单"这条业务链,它依次调用 库存校验、创建订单、扣款三个接口。你会怎么设计脚本,才能既看清每个接口,又能看清"整个下单"花了多久?

答案与详解

两个层面叠加:一是用 JSON 提取器把上一个接口的返回值(如 token、商品 id)动态传给下一个接口,让三个接口串成一条真实业务链;二是把这条业务链包进一个 事务控制器(Transaction Controller),让控制器把三个接口的总耗时作为一个整体统计。这样:聚合报告里,三个接口各自有一行(用它们各自的取样器名区分),可以看每个环节谁慢;事务控制器又多出一行"下单全流程",看整体端到端耗时。双份信息都有,既做了分环节诊断,又度量了真实用户体验。别忘了给三个取样器取能区分的名字,否则同名会被聚合并成一行。


到这里,我们从"性能测试为什么要专门工具"讲起,把 JMeter 从安装启动、测试计划树、线程组、HTTP 取样器、监听器一路拆到参数化(用户变量 + CSV)、接口关联(JSON 提取器)、业务校验(JSON 断言)、并发强化(同步定时器/事务控制器)、能力扩展(插件与梯度压测),再到聚合报告逐列解读、命令行压测与 HTML 报告生成,最后用响应时间/错误率/吞吐量三大指标把"怎么用数据做诊断"串了起来,还专门掰开了本机压测、线程不等于并发、分位口径、参数化这四颗雷。

你可以回头看一眼这条主线:先学会造压力(线程组 + 取样器 + 定时器),再用参数化和关联让压力贴近真实业务,再加断言保证统计的是"真成功",最后用聚合报告和三大指标把数据读成结论。这套链条走通了,你再遇到 Gatling、LoadRunner、云压测平台,看到的都是同一个骨架,只是换了个皮。

一个朴素的收尾建议:别急着一次吃透全部。今天就去跑通"最小压测 + 一个 CSV 参数化 + 一份 HTML 报告",再把登录链路的响应时间曲线和聚合报告亲自盯一遍。性能测试最需要的从来不是记住多少元件,而是"亲手压、敢读数、能较真"——把这篇文章里的命令敲进你自己的机器跑一遍,那种"数据开始说话"的感觉,比任何文档都来得实在。

对了,别忘了最开始那句话:任何想不清楚的术语,大胆回去翻对应的那一节,术语第一次出现的地方我都给了定义。压测的路很长,先把这第一步踩实了再说。