JMeter 线程组与场景设计¶
场景设计错,后面所有调优都是白费。这一篇按“误用现场 → 参数语义 → 四类场景 → 计时器与控制器的真实行为 → 检查清单”展开,环境为 JMeter 5.6.3,插件为 jpgc Standard Set 2.2。
一、四个真实误用现场¶
1. Ramp-up 填 0,500 线程瞬间打满¶
线程组:Threads=500,Ramp-Up=0,Loop=10。启动 3 秒后服务端开始 502,jmeter.log 里成片出现:
报告错误率 43%,业务方直接判定“系统只能扛 200 并发”。实际是 500 个 TCP 连接在一瞬间挤进 accept 队列,内核 somaxconn 溢出,跟业务处理能力没关系。
2. 循环次数选了 Forever,场景停不下来¶
非 GUI 跑了一夜,第二天 jtl 涨到 80 GB,报告一直生成不了。因为 Loop Count = Forever 且没勾调度器,线程永远不会自然结束。非 GUI 模式必须用 bin/shutdown.sh(优雅)或 bin/stoptest.sh(强制)收尾,直接 Ctrl+C 只会杀掉前台进程,报告目录可能不完整。
3. 把线程数当 TPS 报给业务¶
“我们压了 500 并发,所以系统能扛 500 TPS。”实测稳态 TPS 只有 180。原因是每个线程一次迭代要发 4 个请求、平均 RT 260ms、思考时间 1s,单线程每秒只能完成 0.79 次迭代。
4. 换台机器打开 jmx 就报类找不到¶
jmx 里用了 jpgc 的 Ultimate Thread Group,新机器没装插件。JMeter 不会提示“缺插件”,只会把这个元件静默丢弃,场景跑出来线程数完全不对。
二、线程组参数的真实语义¶
| 参数 | 真实含义 | 常见误解 |
|---|---|---|
| Number of Threads | 虚拟用户数(线程数),不是 TPS,也不是并发请求数 | 当成 TPS |
| Ramp-Up Period | 用多少秒把所有线程启动完,间隔 = Ramp-Up / 线程数 | 以为是“延迟多少秒开始” |
| Loop Count | 每个线程执行多少轮迭代 | 以为 Forever 也会自己停 |
| Same user on each iteration | 每轮是否复用同一线程上下文(Cookie、变量、缓存) | 以为只影响报告里的用户名 |
| Delay Thread creation until needed | 线程首次执行时才创建,降低空闲内存 | 以为等于延迟启动 |
| Scheduler / Duration | 到点后请求线程停止,正在跑的采样器会先结束 | 以为到点立刻断 |
| Startup delay | 线程组启动前等待的秒数 | 与 Ramp-Up 混用 |
几个必须记住的换算:
线程启动速率 = Number of Threads / Ramp-Up (个/秒)
单线程迭代周期 = 事务耗时 + 思考时间 + Pacing
单线程 TPS ≈ 1 / 迭代周期
总 TPS ≈ 线程数 × 单线程 TPS
Ramp-Up=0 表示“立刻全部启动”,不是“不启动”。Ramp-Up 小于单次迭代耗时,第一批线程还没跑完、最后一批已经进来,实际并发会堆到接近线程总数,这时的曲线是“一波流”,不能用来判断稳态容量。
三、线程数、并发、TPS 三者不是一回事¶
用利特尔法则串起来:
算例对比:
| 线程数 | 平均 RT | 思考时间 | 单线程周期 | 理论 TPS |
|---|---|---|---|---|
| 1 | 100 ms | 0 | 0.1 s | 10 |
| 100 | 200 ms | 0 | 0.2 s | 500 |
| 100 | 200 ms | 1 s | 1.2 s | 83 |
| 500 | 200 ms | 1 s | 1.2 s | 417 |
所以“500 线程”在带 1 秒思考时间的场景里只对应 417 TPS 的理论上限,实际还会被服务端、数据库、压测机打折。报容量时必须同时给出:线程数、思考时间、Pacing、实测 TPS、RT 分位数,缺一项结论就不可复现。
四、Ramp-Up 与 TPS 曲线的关系¶
Ramp-Up 期间并发从 0 线性升到 N,TPS 也近似线性上升。设稳态 TPS 为 S,Ramp-Up 为 R,总时长 D,则报告里的平均 TPS:
算例:稳态 500 TPS,Ramp-Up 600 s,Duration 1800 s。
聚合报告显示的 417 不是系统能力,是被 Ramp-Up 摊薄的结果。判定方法:
- 在“Transactions per Second”图里截取平台段(前后各去掉 Ramp-Up 和下降段)作为稳态 TPS。
- Ramp-Up 建议 ≥ 单次迭代耗时的 2 倍,且 ≥ 目标线程数 / 每秒可启动线程数(普通 PC 约 20–50 个/秒)。
- 稳定性场景的 Ramp-Up 单独计算,不占总时长比例过大;8 小时稳定性测试用 10 分钟爬坡是合理的,30 分钟爬坡就会污染统计。
五、四类场景的参数设计表¶
| 场景 | 目的 | 线程数 | Ramp-Up | 持续 | 循环 | 通过标准 |
|---|---|---|---|---|---|---|
| 基准 | 单用户基线、脚本正确性 | 1 | 1 s | 5–10 分钟 | 20 | 全链路 0 错误,拿到单请求 RT |
| 容量/梯度 | 找拐点、算单机容量 | 50→100→200→400 | 每档 60–120 s | 每档 10 分钟 | 由调度器控制 | TPS 线性增长,P95 不超阈值 |
| 稳定性 | 泄漏、连接池、GC | 目标的 70%–80% | 10 分钟 | 8–24 小时 | Forever + Duration | 错误率 < 0.1%,内存无单向增长 |
| 尖峰 | 秒杀、开抢、突增流量 | 目标峰值 | 5–10 s | 3–5 分钟 | 3–5 轮 | 不雪崩,限流生效,RT 可恢复 |
梯度场景不要用一个线程组反复改参数,直接用 jpgc 的 Stepping Thread Group 一次跑完,报告里能按时间段切片。
六、Synchronizing Timer:真正的瞬时并发¶
普通线程组即使 Ramp-Up 很小,线程也是陆续启动的。要让 200 个线程“同一毫秒”发请求,只能靠集合点。
| 字段 | 语义 | 坑 |
|---|---|---|
| Number of Simulated Users to Group by | 每凑够多少个线程释放一次;填 0 表示当前线程组的全部线程 | 填得比线程数大 → 永远等不齐 |
| Timeout in milliseconds | 等待上限,0 表示无限等 | 生产脚本必须设,否则一个线程卡死整组 |
<SyncTimer guiclass="TestBeanGUI" testclass="SyncTimer"
testname="Synchronizing Timer - buy" enabled="true">
<intProp name="groupSize">200</intProp>
<longProp name="timeoutInMs">30000</longProp>
</SyncTimer>
使用要点:
- 放在事务开始之前,不要放在事务控制器内部,否则集合等待时间会被计入事务 RT。
- 集合点制造的是瞬时并发峰值,不代表 TPS。1000 线程集合一次只产生一波请求,Hits per Second 图上是一个尖刺,不是持续吞吐。
- 集合点必须放在 Loop 内层时,每一轮都会重新集合;放在 Loop 外层只会集合一次。
- 线程数不能被 groupSize 整除时,最后一批会等到 Timeout 才释放,报告尾部会出现一批超时样本。
七、恒定吞吐:CTT 与 PTT 的区别¶
7.1 Constant Throughput Timer¶
最容易踩的坑是单位:Target throughput 是“每分钟样本数”,不是每秒。想要 10 TPS 就填 600。
| 字段 | 取值 | 说明 |
|---|---|---|
| Target throughput | 600 | 每分钟 600 个采样器 = 10 TPS |
| Calculate Throughput based on | All active threads (shared) | 多线程共享一个目标;选 This thread only 则是每线程各 600/分钟 |
它靠“请求前 sleep”限速,公式近似:
因此有三个硬限制:
- 并发不足时达不到目标(它只能减速,不能加速)。
- 响应时间抖动大时,实际 TPS 会在目标上下漂移,实测偏差 5%–15%。
- 它不控制请求的瞬时分布,短窗口内的 TPS 仍然不均匀。
7.2 Precise Throughput Timer¶
5.x 内置,不用装插件。关键字段是 Target throughput (in samples per minute)、Throughput period (seconds)、Test duration (seconds)。它按批次计算每个样本的预定发送时刻,长时间运行时更接近目标,且能配置成随机到达(近似泊松分布),比 CTT 更适合“模拟真实用户不均匀到达”。
| 维度 | Constant Throughput Timer | Precise Throughput Timer |
|---|---|---|
| 单位 | 样本/分钟 | 样本/分钟(配合周期与总时长) |
| 精度 | 粗,受 RT 影响 | 细,按计划时刻投放 |
| 分布 | 均匀减速 | 可均匀也可随机 |
| 必需参数 | 目标值 + 计算基准 | 目标值 + 周期 + 测试时长 |
| 适用 | 快速限速、稳定性场景 | 精确复现业务 TPS 曲线 |
配置示例(目标 12 TPS,跑 30 分钟):
Target throughput (in samples per minute) = 720
Throughput period (seconds) = 60
Test duration (seconds) = 1800
两个 Timer 都只对同作用域内的采样器生效,挂在事务控制器外才能限制其内部所有请求。
八、吞吐量控制器:把业务比例做进脚本¶
| 模式 | 行为 | 注意 |
|---|---|---|
| Percent Executions | 按百分比随机决定是否进入 | 每个线程独立计算,短时间比例偏差大 |
| Total Executions | 执行满指定次数后不再进入 | 多线程下计数口径要实测校准 |
混合场景配比示例(浏览 60%、加购 20%、下单 15%、支付 5%):
Thread Group 200 threads, ramp-up 120s, duration 1800s
├─ Transaction Controller "browse" ← Throughput Controller 60%
├─ Transaction Controller "cart" ← Throughput Controller 20%
├─ Transaction Controller "order" ← Throughput Controller 15%
└─ Transaction Controller "pay" ← Throughput Controller 5%
注意百分比是“进入次数”的比例,不是“成功请求数”的比例。下单接口错误率高时,实际成功的业务比例会明显偏离设计值,报告里要按成功样本重新算。
九、事务控制器:统计口径的分水岭¶
| 选项 | 作用 | 误用后果 |
|---|---|---|
| Generate parent sample | 报告只显示事务总耗时,子请求仍记录在 jtl | 不勾选时报告里全是零散接口,看不到端到端 RT |
| Include duration of timer and pre/post processors | 事务时间包含 Timer、前后置处理器 | 勾选后思考时间被算进 RT,P95 虚高 |
| 嵌套事务 | 父子都计入样本 | 聚合报告样本数翻倍,TPS 虚高 |
判定标准:性能报告里的 TPS 必须和业务口径一致。一个下单事务包含 4 个接口,如果父子都统计,报告 TPS 会是真实下单数的 5 倍。压测前先确认“1 TPS 指的是 1 个事务还是 1 个 HTTP 请求”。
事务名里不要带 ${__threadNum} 之类的变量,否则聚合报告会出现几百行不同名字的事务,无法汇总。
十、setUp / tearDown 线程组¶
- setUp 在每个测试计划里最多一个,先于所有普通线程组执行;tearDown 最后执行。
- 典型用途:setUp 登录拿全局 token、准备测试数据;tearDown 清理订单、删除临时用户。
- setUp 里的变量不会传给普通线程组(线程隔离)。必须落文件、写属性或写数据库:
- setUp 失败不会自动终止主线程组,JMeter 仍会继续跑。必须在主线程组开头加断言或
If Controller检查 token 是否存在,否则整场压测都在用空 token。 - tearDown 在线程组被强制停止时可能不执行,清理逻辑不要只依赖它。
十一、jpgc 阶梯线程组¶
安装方式:Options → Plugins Manager → Available Plugins → 搜索 jpgc - Standard Set → 安装后重启 JMeter。
11.1 Stepping Thread Group¶
| 字段 | 含义 |
|---|---|
| This group will start | 最终线程总数 |
| First wait for | 第一批线程启动前等待 |
| Then start | 首批启动多少线程 |
| Next, add | 每阶梯增多少线程 |
| Threads every | 每阶梯间隔多久 |
| Using ramp-up | 每批线程用多久启动完 |
| Then hold load for | 到顶后保持多久 |
| Finally, stop | 每批停止多少线程 / 间隔 |
这套参数天然对应“梯度加压找拐点”,比手改普通线程组五次可靠。
11.2 Ultimate Thread Group¶
每一行是一条独立的线程曲线(Start Threads、Initial Delay、Startup Time、Hold Load For、Shutdown Time),可以拼出“上升—保持—下降—再上升”的波浪形,适合模拟一天内的流量波动。行与行之间是叠加关系,不是替换关系。
打开 jmx 报 CannotResolveClassException: kg.apc.jmeter.threads.UltimateThreadGroup,只有一个原因:目标机器没装 jpgc 或版本不兼容。不要靠手改 XML 绕过,装了插件再打开。
十二、常见误用表¶
| 误用 | 直接后果 | 正确做法 |
|---|---|---|
| Ramp-Up = 0 | 瞬时连接风暴,502/Connection refused | 按 20–50 线程/秒设置 |
| Loop = Forever 且无 Duration | 场景不结束,jtl 无限增长 | Forever 必须配 Scheduler |
| 线程数当 TPS | 容量结论虚高 2–5 倍 | 用 TPS = 线程数 / 迭代周期 折算 |
| 所有场景共用一个线程组 | 参数互相污染 | 每个场景独立 jmx 或独立线程组 |
| 集合点放在事务内 | RT 包含集合等待,P95 失真 | 放在事务开始前 |
| CTT 目标填 10(以为是 TPS) | 实际 10 样本/分钟,压了个寂寞 | 填 600 |
| 事务控制器父子都统计 | TPS 翻倍 | 明确统计口径,报告注明 |
| 稳定性测试不设 Ramp-Up | 前 10 分钟全是错误,污染错误率 | 单独爬坡 10 分钟 |
| 压测机上开着查看结果树 | 压测机 CPU 先满,TPS 上不去 | 非 GUI + 关闭监听器 |
| 用 GUI 跑正式压测 | GUI 自身开销大,结果不可信 | jmeter -n -t ... -l ... -e -o ... |
非 GUI 启动命令:
-o 指定的目录必须为空,否则启动直接失败;每次重跑前先删目录,不要复用上一次的 report。
十三、场景设计检查清单¶
- 场景类型明确(基准/容量/稳定性/尖峰),不是“压一下看看”
- 线程数由目标 TPS 与迭代周期反算,不是拍脑袋
- Ramp-Up ≥ 单次迭代耗时 × 2,且未造成启动瞬间错误
- Forever 场景都配了 Duration,并准备了 shutdown 脚本
- 思考时间、Pacing 与生产一致,报告里写明了取值
- 集合点只用在需要瞬时并发的接口,且设置了 Timeout
- 限速用 CTT/PTT 时确认单位是“每分钟”
- 事务控制器统计口径与业务一致,报告注明 1 TPS 的定义
- setUp 产物通过属性/文件传递,并有存在性校验
- 正式压测用非 GUI,jtl 与 report 目录独立,环境版本已记录