很多人第一次用 JMeter,会先学会发送一个 HTTP 请求,然后把线程数从 1 改成 100。工具确实运行起来了,但这还不能回答一个更实际的问题:100 个用户完成一整段业务时,系统是否仍然可用?

这篇文章用一条脱敏示例链路贯穿全过程:

登录 → 浏览内容列表 → 打开一条详情 → 点赞 → 确认点赞成功

读完并跟做后,你应该能够:

  • 判断一个需求应该做单接口压测,还是业务链压测;
  • 把用户操作拆成 JMeter 测试计划树;
  • 用 CSV、JSON 提取器和变量完成登录态与数据关联;
  • 根据“用户人数 × 一轮耗时”估算业务吞吐,而不是盲填线程数;
  • 先用 1 个用户证明脚本正确,再进入正式负载执行;
  • 分开判断接口成功、业务成功和整条链路完成。

文中的接口、字段和数字都是教学示例,不对应任何真实产品。正式压测前必须确认目标环境、执行时间和容量边界。

从业务目标到压测结论的完整路径

一、先理解:单接口压测与业务链压测差在哪里

单接口压测回答局部问题

例如只压详情查询接口:

GET /api/content/123

它适合回答:

  • 这个查询接口能承受多少请求;
  • 缓存、索引或代码修改后是否变慢;
  • 错误率、响应时间和吞吐量的拐点在哪里。

输入比较固定:接口地址、参数、压力和技术指标。

业务链压测回答用户能否完成一件事

“打开详情并点赞”并不是两个孤立接口。用户通常要先登录,从列表响应中获得内容 ID,打开详情,再携带身份和内容 ID 点赞。

任何一步失败,后面的请求都可能失去业务意义。例如列表没有返回内容 ID,点赞请求却仍发送 ${contentId},HTTP 层也许有响应,但这不是一次有效的用户行为。

维度 单接口压测 业务链压测
测试对象 一个技术入口 一类完整用户行为
参数来源 通常直接构造 经常依赖前序响应
身份状态 可能不需要 通常要登录和隔离账号
节奏 请求速率或并发 还要考虑阅读、切换和分支
成功条件 接口返回正确 每一步正确且到达业务终点
结果分析 按接口汇总 同时看接口、事务和用户链路

判断方法很简单:如果测试目标中出现“用户完成登录、浏览、下单、发布、互动”等动词链,它通常就不再是单接口问题。

二、把模糊需求改写成可验证目标

假设原始需求只有一句话:

测一下点赞业务能不能支持 100 个并发用户。

这句话还不能直接转换成线程组,因为至少缺少:

  • 100 个用户是同时到达,还是逐步进入;
  • 每个用户只点赞一次,还是持续浏览并点赞;
  • 用户两次操作之间是否等待;
  • 测试持续多久;
  • 什么结果算通过;
  • 在哪个环境运行,是否允许产生写入数据。

把它补成一张场景设计卡:

项目 本文示例
测试目标 验证 100 个活跃用户持续浏览并点赞时,关键链路是否稳定
业务链 登录 → 列表 → 详情 → 点赞
用户模型 100 个用户在 60 秒内逐步进入
运行节奏 每轮业务结束后等待约 5 秒再开始下一轮
持续时间 稳定运行 10 分钟
数据 每个线程独立账号,内容 ID 从列表动态取得
业务终点 点赞接口返回成功,且响应中的最终状态为已点赞
观察指标 各接口、整条链路、错误类型及服务端资源
停止条件 错误持续超阈值、业务终点大量不可达或服务端进入危险区间

这里的数字只是示例。真正项目中,应从线上峰值、容量目标、历史监控或业务预测中取得依据。

三、先画用户链路,再列接口和数据依赖

不要一开始就在 JMeter 里添加组件。先用表格说明每一步“需要什么、产生什么、怎样算成功”。

步骤 示例请求 输入 输出给后续步骤 成功依据
登录 POST /api/login CSV 中的账号、密码 accessToken 业务码成功且 Token 非空
内容列表 GET /api/contents accessToken contentId 列表非空且取得可操作 ID
内容详情 GET /api/contents/${contentId} Token、内容 ID 详情状态 业务码成功,返回 ID 与请求一致
点赞 POST /api/contents/${contentId}/like Token、内容 ID 点赞结果 业务码成功且 liked=true

这张表解决了业务链的核心问题:每个变量从哪里来,失败后能不能继续,哪个响应证明整条链真正完成。

四、把链路映射成 JMeter 测试计划树

本文示例使用下面的结构:

Test Plan
├── User Defined Variables
│ └── thinkTimeMs = ${__P(thinkTimeMs,5000)}
└── Thread Group:浏览并点赞
├── CSV Data Set Config:users.csv
├── HTTP Request Defaults
├── HTTP Header Manager
├── Once Only Controller:用户初始化
│ └── HTTP Request:登录
│ ├── JSON Extractor:accessToken
│ └── JSON JMESPath Assertion / Response Assertion
└── Loop Controller:持续业务
├── Transaction Controller:一次完整业务
│ ├── HTTP Request:获取内容列表
│ │ ├── JSON Extractor:contentId
│ │ └── JSON JMESPath Assertion
│ ├── If Controller:contentId 有效
│ │ ├── HTTP Request:获取内容详情
│ │ └── HTTP Request:点赞
│ │ └── JSON JMESPath Assertion:liked = true
│ └── If Controller:contentId 无效
│ └── Flow Control Action:结束当前线程或本轮
└── Flow Control Action:暂停后再开始下一轮

逻辑控制器用于改变子元素的执行顺序、次数和选择方式;断言的作用域取决于它在测试树中的位置。Apache JMeter 官方文档建议把只针对某个请求的断言放在该 Sampler 下,避免误作用到其他请求。Apache JMeter:Elements of a Test Plan

为什么登录放在 Once Only Controller 中

线程组循环时,普通请求会每轮重复执行。登录通常代表一个虚拟用户建立一次身份,因此放到 Once Only Controller 中,让它在该线程首次迭代时执行。

注意:这只能解决“同一线程只登录一次”。如果登录线程、后台线程和业务线程分属不同线程组,变量不能自动跨线程共享,需要另行设计用户状态存储和同步。

为什么业务链外面套 Transaction Controller

列表、详情和点赞各自需要接口指标,同时我们还想知道“一轮完整业务”耗时。Transaction Controller 可以生成事务汇总样本,但这个汇总样本不是一个真实 HTTP 接口,分析时不要和接口吞吐混在一起。

五、按顺序搭建 JMeter 组件

第 1 步:配置线程组,但先只用 1 个用户

新建 Thread Group,调试阶段填写:

Number of Threads: 1
Ramp-up Period: 1
Loop Count: 1

先证明一个用户能完整跑通。直接从 100 个线程开始,会把脚本错误、数据冲突和系统性能问题混在一起。

正式运行时,再把值改成 Property:

Number of Threads: ${__P(threads,1)}
Ramp-up Period: ${__P(rampUp,1)}
Duration: ${__P(duration,60)}

正式持续运行时,线程组仍只进入 1 次,在内部增加持续循环的 Loop Controller,并启用线程组的调度器、用 duration 控制总时长。这样每个线程只领取一次账号和登录一次,重复的是业务控制器,而不是整个线程组。

这些值通过 Property 暴露后,可以在不同环境使用 -Jthreads=... 传参,不必反复修改 JMX。

第 2 步:用 CSV 给每个线程分配账号

准备教学示例 users.csv:

user_001,example_password_001
user_002,example_password_002

CSV Data Set Config 可配置:

Filename: ${__P(dataDir,.)}/users.csv
Variable Names: username,password
Recycle on EOF: False
Stop thread on EOF: True
Sharing mode: All threads

请求体中引用:

{
"username": "${username}",
"password": "${password}"
}

如果计划运行 100 个并发用户,却只有 10 个账号,就必须先决定是否允许账号复用。对于会修改用户状态的业务,盲目复用账号可能产生会话覆盖、幂等冲突或限流,结果不能直接解释为系统容量问题。

JMeter 官方最佳实践也给出了用 CSV Data Set 为不同线程提供独立登录数据的方式。Apache JMeter:Best Practices

第 3 步:设置公共地址和 Header

在 HTTP Request Defaults 中填写:

Protocol: https
Server Name: ${__P(host,test.example.com)}
Connect Timeout: ${__P(connectTimeout,5000)}
Response Timeout: ${__P(responseTimeout,10000)}

登录成功后,JSON 提取器得到 accessToken。随后在 Header Manager 中引用:

Authorization: Bearer ${accessToken}
Content-Type: application/json

不要把真实 Token 写在 JMX 中。Token 是运行时数据,应该由登录响应生成。

第 4 步:提取动态内容 ID

假设列表响应为:

{
"code": 0,
"data": {
"items": [
{"id": "content_001", "canLike": true}
]
}
}

在列表请求下添加 JSON Extractor:

Names of created variables: contentId
JSON Path expressions: $.data.items[0].id
Default Values: NOT_FOUND

后面的详情路径写成:

/api/contents/${contentId}

为什么要设置 NOT_FOUND?因为提取失败时,如果没有清晰的默认值,后续请求可能携带空字符串、未解析变量,甚至沿用上一轮旧值。明确的哨兵值更容易被条件控制器拦截和在结果中定位。

第 5 步:失败时不要继续发送无效请求

在详情和点赞外层增加 If Controller,示意条件为:

${__groovy(vars.get('contentId') && vars.get('contentId') != 'NOT_FOUND')}

如果 ID 无效,应根据场景规则执行一种明确动作:

  • 当前列表为空属于允许情况:跳过本轮并记录原因;
  • 数据不足使测试失去意义:停止当前线程;
  • 列表接口本应返回数据:将本轮标记失败。

不要让脚本继续请求 /api/contents/NOT_FOUND,否则报告中的错误主要来自脚本,而不是被测系统。

第 6 步:添加三层成功判断

业务链不能只检查 HTTP 200。

  1. 通信成功:连接正常、状态码符合预期、响应能解析;
  2. 业务成功:例如 code=0,详情 ID 与请求 ID 一致;
  3. 链路完成:点赞响应中 liked=true,到达本轮终点。

如果系统采用最终一致性,写入成功后,列表或详情不一定立即反映新状态。此时应把“写请求成功”和“稍后读取确认”作为两个判断,并设置有界等待或轮询,不能无限重试,也不能用即时读取失败直接否定写入。

第 7 步:加入真实但可控的思考时间

真实用户在列表和详情页会停留。没有任何等待的 100 个线程,可能变成不断循环的请求机器。

如果要让每轮结束后等待 5 秒,可以在线程内部的 Loop Controller 末尾添加 Flow Control Action,选择暂停并填写:

Duration: ${thinkTimeMs}

如果要模拟“打开详情前阅读列表”,也可以给详情请求添加 Constant Timer。JMeter 的 Timer 会在其作用域内的 Sampler 执行前等待,放在线程组下会影响多个请求,因此必须先明确等待发生在哪个用户动作上。正式场景可以根据真实行为分布改用随机定时器,但要记录取值依据。

六、线程数不是吞吐量:做一次简单估算

假设稳定阶段有 100 个并发用户,每一轮业务平均包含:

列表请求 1 秒
详情请求 1 秒
点赞请求 1 秒
用户思考 5 秒

一轮约 8 秒。用闭合用户模型做粗略估算:

业务迭代吞吐 ≈ 并发用户数 ÷ 平均一轮耗时
≈ 100 ÷ 8
≈ 12.5 次业务/秒

每轮有 3 个业务请求,因此请求吞吐粗略约为:

12.5 × 3 ≈ 37.5 个请求/秒

这只是设计阶段的近似值,不是保证值。系统响应变慢后,一轮耗时会增长,闭合模型中的吞吐也会变化;接口并行、重试和后台请求也会改变结果。

这个计算至少能帮助我们识别一个错误:100 个线程不等于 100 RPS。

七、按照四个阶段验证,不要直接正式开压

阶段 1:单用户、单轮调试

临时添加 View Results Tree,只运行 1 个线程、1 轮,逐项检查:

  • CSV 是否读取到当前账号;
  • 登录 Token 是否非空;
  • 列表是否提取到内容 ID;
  • 详情和点赞是否使用同一个 ID;
  • 断言是否能故意识别错误响应;
  • 业务事务是否到达终点。

View Results Tree 适合脚本调试,不适合正式负载执行。JMeter 官方建议正式负载测试使用 CLI 模式,并尽量减少 Listener 以降低负载机资源开销。Apache JMeter:Getting Started

阶段 2:少量用户、多轮验证

例如使用 2~5 个测试账号运行几轮,重点发现:

  • 不同线程是否串用 Token;
  • 数据能否重复消费;
  • 上一轮变量是否污染下一轮;
  • 账号耗尽时是否正确停止;
  • 写操作是否产生冲突。

阶段 3:阶梯升压

不要从 1 个用户直接跳到目标峰值。可以按经过评审的台阶逐步提高,在每个稳定阶段观察:

  • P90/P95/P99 响应时间;
  • 错误率及错误类型;
  • 业务终点成功率;
  • 应用、数据库、缓存和下游资源;
  • JMeter 负载机自身 CPU、内存和网络。

阶段 4:正式场景

脚本和少量数据验证通过后,才进入约定的持续时间与目标负载。正式执行使用 CLI,例如:

jmeter -n \
-t business-like.jmx \
-Jhost=test.example.com \
-Jthreads=100 \
-JrampUp=60 \
-Jduration=600 \
-JdataDir=./data \
-l result.jtl \
-j jmeter.log

示例域名不能直接用于真实执行。请把目标、负载和数据文件替换为已经授权的测试环境配置。

八、结果应该怎样读

假设一次示例执行得到:

观察项 示例现象 还不能直接得出的结论
列表接口 P95 上升 从 300 ms 增至 1.2 s 不能只凭此判断数据库是瓶颈
点赞错误率 3% 主要错误为业务码失败 不能把所有失败都算作服务器 5xx
业务事务成功率 94% 部分用户未到达点赞终点 不能只看三个接口的平均错误率
负载机 CPU 95% JMeter 本身接近上限 不能继续把吞吐下降归因于目标系统

分析顺序可以是:

  1. 负载是否按设计产生,JMeter 执行端是否健康;
  2. 失败是脚本、数据、网络、业务拒绝还是服务端异常;
  3. 哪个接口首先恶化,是否影响整条业务;
  4. 服务端监控和日志是否提供同一时间段的证据;
  5. 本次结果能回答原始目标中的哪一部分。

只有客户端 JTL,没有服务端资源和日志证据时,可以描述现象,但不要武断宣布瓶颈根因。

九、五个最常见的错误设计

错误 1:把多个接口排在一起就叫业务链

修正:为每一步写清输入、输出、失败行为和业务终点。

错误 2:所有线程共用一个写账号

修正:账号与线程建立明确映射;无法隔离时,先确认共享账号是否会改变业务含义。

错误 3:提取失败后仍继续请求

修正:设置明确默认值,用条件控制器阻止空 ID、旧 ID 和未解析变量进入后续请求。

错误 4:只断言 HTTP 200

修正:同时验证业务码、关键字段和最终业务状态。

错误 5:GUI 中带着大量 Listener 正式开压

修正:GUI 用于构建与调试,正式压测使用 CLI,并只保存分析所需字段。JMeter 官方最佳实践也明确建议负载执行时使用 CLI、减少 Listener 和不必要结果数据。Apache JMeter:Best Practices

十、一份可以直接填写的业务压测模板

1. 要回答的问题:
2. 目标环境与授权时间:
3. 用户角色:
4. 完整业务链:
5. 每一步的输入、输出和成功依据:
6. 登录与初始化次数:
7. 动态参数来源:
8. 账号与业务数据规模:
9. 并发用户、升压和持续时间:
10. 每轮思考时间与后台频率:
11. 接口成功标准:
12. 业务终点标准:
13. 服务端观察指标:
14. 停止条件:
15. 数据清理方式:
16. 已知限制:

十一、执行前检查清单

  • 我能用一句话说明本次测试要回答什么
  • 每个请求都能对应到真实用户动作
  • 每个动态变量都有来源、默认值和失败处理
  • 登录不会在每轮被意外重复
  • 账号数、线程数和数据复用规则一致
  • 单用户单轮已经到达业务终点
  • 多用户运行时不会串用身份或旧变量
  • 线程数、思考时间和预期吞吐做过估算
  • 同时定义了接口成功、业务成功和链路完成
  • 正式执行使用 CLI,并监控负载机自身资源
  • 已约定停止条件、数据清理和结果解释边界

写在最后

业务压测不是“在 JMX 里放更多请求”,而是把一类真实用户行为变成可重复、可计算、可验证的测试模型。

从今天的示例可以记住一条最重要的路径:

先定义业务终点,再拆接口和数据依赖;先让一个用户正确完成,再让很多用户按真实节奏重复;最后用 JTL、日志和服务端监控共同解释结果。

做到这一步,JMeter 才不只是请求发生器,而是在执行一条可信的业务压测场景。

参考资料