ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Jmeter不同参数并发测试实战:参数化与线程组配置详解

Jmeter不同参数并发测试实战:参数化与线程组配置详解 1. 不同参数并发到底在测什么需求拆解与测试目标很多刚接触Jmeter的同学做接口压测时习惯性地写死一组参数然后疯狂加线程数。比如压测一个查询订单接口所有线程都传同一个订单号跑到最后看聚合报告发现错误率不高、TPS也挺好看就把报告丢给开发说“接口性能没问题”。但真实线上环境里每个用户查询的订单都不一样请求参数的差异会直接影响接口的响应路径、缓存命中率、数据库查询速度。用同一组参数压出来的结果和真实场景其实是两回事。不同请求参数并发核心要解决的是三件事。第一确保每一个线程也就是模拟的每一个用户能够拿到独立且有效的参数而不是所有人抢同一份数据第二在参数各不相同的前提下请求能够在时间维度上尽量同时打到服务端形成真正的并发压力第三压测过程中能够快速区分“参数导致的业务失败”和“服务端真正的性能瓶颈”避免被错误率数字误导。这三个点任何一个处理不好压测结果的可信度都会大打折扣。举一个我实际接触过的例子。某个订单查询接口压测时把线程数拉到200固定参数跑得很平稳。后来改成用1000个真实订单号做参数并发同样的线程配置接口的p99从300毫秒直接飙到2秒多。原因很简单固定参数下第一次查询后结果进了缓存后面的请求全走缓存换成不同参数后大量请求穿透到数据库走实时查询缓存和数据库的压力完全不一样。这就是不同参数并发存在的意义——它测出来的才是接口在真实负载下的表现。这篇文章适合谁看一类是刚接触Jmeter接口测试能跑通单个脚本但不知道怎么处理多用户不同参数的新手另一类是已经在做压测但发现自己的并发结果总被开发质疑“参数不对结果无效”的测试工程师。我会从参数准备、脚本设计、并发模型搭建、问题排查到结果分析把整个链路讲透并且给出可以直接照做的配置和脚本。在做所有事情之前建议先把Jmeter版本统一到5.x最新稳定版JDK用8以上版本。不要用太古老的Jmeter版本很多参数化组件和HTTP配置项的默认行为在旧版本里有差异网上教程鱼龙混杂版本不对会平白增加很多排查成本。2. 参数数据的准备与加载让每个线程拿到属于自己的参数2.1 CSV参数化最稳妥的按行分配方案Jmeter里实现不同参数并发最基础也是最好用的方式就是CSV Data Set Config。它的工作原理很简单你准备一个CSV文件每一行是一组参数Jmeter在运行时会按行读取数据然后赋值给脚本里对应的变量。每一个线程组里的线程在发送请求前会从CSV里取一行数据作为本次请求的参数。这里有一个关键点需要先搞清楚CSV文件里的数据量最好大于等于“线程数 × 循环次数”的总数否则会出现同一个线程不同循环里重复使用参数或者多个线程共用参数的情况。当然Jmeter也提供了数据用尽后的处理策略选项这个后面会详细说但正常情况下数据量够用是前提。举个例子我要压测一个查询接口接口需要同时传入两个参数userId和orderId。CSV文件就这么准备user_id,order_id 10001,SO20240301001 10002,SO20240301002 10003,SO20240301003CSV Data Set Config的配置项里有几个坑需要特别注意。文件名路径建议写绝对路径写相对路径时每次运行前都要确认工作目录是不是Jmeter的bin目录这个是新手最常踩的坑我在实际工作中甚至见过同事因为路径问题导致参数一直没加载进去压了一天全是无效请求。变量名称那一栏填写你希望Jmeter生成的变量名多个变量用英文逗号分隔比如上面这个场景填user_id,order_id。分隔符默认是英文逗号如果你的CSV文件用的是其他分隔符需要对应修改。是否允许带引号这个选项建议使用默认的False除非你的CSV文件里字段值本身包含逗号并且用引号包裹了否则不要乱开。接下来是重头戏共享模式Sharing mode。这个选项直接决定不同线程之间怎么读CSV文件它有三个值All threads、Current thread group、Current thread。默认值是All threads意思是所有线程组的所有线程共享同一个CSV读取游标。在多线程并发场景下这个模式会导致两个问题一是多个线程可能读到同一行数据因为线程调度本身是随机的文件指针的移动和线程取数之间没有严格的锁机制二是如果脚本里有多个线程组它们会互相消费参数导致某个线程组的参数数据被另一个线程组抢走。针对“不同请求参数并发”这个场景我通常选择Current thread group。在这种模式下同一个线程组内的线程共享一个游标不同线程组之间各自独立读取。好处是数据分配可控坏处是如果一个线程组内的线程数大于CSV行数数据就会重复使用需要配合Recycle on EOF选项来处理。Recycle on EOF和Stop thread on EOF这两个选项控制的是数据读完之后的行为。Recycle on EOF为True时数据读完了会从头再读Stop thread on EOF为True时数据读完了当前线程直接停止。我的建议是做压测时如果CSV数据量够用两个都设为False数据读完后线程会报错停止这本身就是一个信号提醒你数据量准备不足。如果数据量确实有限但并发量又大可以Recycle on EOF设为True、Stop thread on EOF设为False但要清楚数据会重复使用这时就要在业务层面接受“部分请求参数重复”的现实。CSV文件准备好之后在HTTP请求里用${user_id}和${order_id}占位符引用变量即可。比如查询接口的Path是/api/v1/order/${order_id}POST请求的JSON body里写{userId:${user_id}}这些都是Jmeter最基本的变量引用方式不需要额外引入插件。2.2 函数与随机变量适用场景和局限性CSV是主方案但有的时候你会发现手里的数据不是现成的CSV文件而是需要现场生成的。这时可以用Jmeter自带的函数来生成参数。比较常用的有__Random、__counter、__time这几个。${__Random(10001,99999)}会在每次调用时随机生成一个10001到99999之间的整数适合生成ID类参数。但要注意的是随机生成的数据不代表有效比如接口期望userId是数据库里真实存在的随机生成的数字大概率查不到数据请求会失败。所以__Random更适合那些不校验数据合法性的接口或者用于生成一些范围参数。${__counter(TRUE,)}是Jmeter的计数器函数第二个参数传TRUE表示每个线程独立计数FALSE表示所有线程共享计数。比如要生成用户ID从10001开始递增的数据可以配合__counter做拼接1000${__counter(TRUE,)}不过这种方式生成的数据格式控制比较弱而且计数器无法直接从某个特定数据源里取数。__time通常用于生成时间戳参数格式可以自由指定比如${__time(yyyy-MM-dd HH:mm:ss,)}。这个在压测一些带时间范围的接口时很好用但也要注意如果多个线程在同一秒内调用__time生成的时间戳是完全相同的这在某些对时间唯一性有要求的场景下会出问题。从我的实际经验来看CSV几乎能覆盖90%以上的场景。函数的真正适用场景是参数本身没有业务含义、不需要数据预置、生成逻辑简单。一旦参数之间有关联关系比如userId对应的orderId或者用户名对应的密码函数就无能为力了必须走CSV或者后面的动态参数方案。2.3 动态参数处理加密、签名、时间戳有些接口不会老老实实接受你传的明文参数它会在参数里加签名、时间戳、token等动态内容。这在一线接口压测里非常常见不做处理的话请求可能直接在网关层被拦掉。以签名参数为例。假设接口要求请求体里带一个sign字段算法是md5(userId orderId timestamp secretKey)。这个计算过程如果用CSV静态数据是做不到的因为timestamp是实时生成的。这时需要在取样器之前加一个JSR223预处理脚本。在JSR223预处理脚本里语言选择Groovy脚本逻辑大致是import java.security.MessageDigest def userId vars.get(user_id) def orderId vars.get(order_id) def timestamp System.currentTimeMillis() / 1000 def rawStr userId orderId timestamp your_secret_key MessageDigest md MessageDigest.getInstance(MD5) byte[] digest md.digest(rawStr.getBytes(UTF-8)) def sign new BigInteger(1, digest).toString(16).padLeft(32, 0) vars.put(timestamp, timestamp.toString()) vars.put(sign, sign)脚本执行完后在HTTP请求的Body Data里引用${timestamp}和${sign}就可以了。这里要强调一点如果接口对时间戳有效期有校验比如5分钟内有效压测中途因为暂停、调整参数导致脚本重启timestamp会自动重新生成不会有问题。但如果签名算法里还包含了随机数nonce就需要在脚本里生成并保存逻辑是一样的。另外关心JMeter处理MD5加密的朋友也可以直接用Jmeter内置的__digest函数完成这类需求它能算MD5、SHA-1、SHA-256等常见摘要样例${__digest(MD5,${user_id}${order_id}${timestamp}secretKey,,,)}。不过函数方式只适用于参数在请求前已经确定的情况如果先决逻辑复杂还是建议用Groovy脚本可维护性更高。3. 并发模型的搭建从线性启动到真正同时打过去3.1 线程组配置线程数、Ramp-Up、循环次数的合理设置参数数据准备好了接下来就是怎么让请求“并发”起来。Jmeter里的并发模型是基于线程组实现的每个线程模拟一个虚拟用户线程数就是并发用户数。但很多人的理解有一个偏差以为线程数设为100请求就会同时发出。实际上线程组里的请求不是同时发出的。Jmeter的线程调度有一个关键参数叫Ramp-Up Period它表示线程启动的持续时长。比如线程数100Ramp-Up设为10秒Jmeter会在10秒内均匀启动100个线程平均每100毫秒启动一个。假设每个线程只发一次请求那么第一个请求和最后一个请求之间相差接近10秒这远远谈不上“并发”。要让请求真正同时打到服务端有两个方向。一是把Ramp-Up调成0让所有线程在同一时刻启动。二是设置循环次数让线程在启动后持续发请求在某一瞬间自然形成并发波峰。在实际压测中我更倾向于第二种——压测从来不是为了制造一个瞬间尖峰而是为了让系统持续处于高负载下运行一段时间这才能暴露连接池、线程池、数据库连接数这些资源瓶颈。关于线程组里几个参数的搭配我常用的思路是参数取值思路说明线程数按目标并发数设置即同时活跃的用户数不等于每秒请求数Ramp-Up目标并发数÷5左右经验值线性爬坡避免瞬时冲击导致连接堆积循环次数根据压测时长反推例如需要持续压测3分钟按预估TPS估算循环次数或勾选永远并配合持续时间持续时间压测时长更推荐用时长来控制循环次数在长时间压测中不够灵活勾选了“永远”并设置持续时间的话Continuously运行模式下线程会持续发请求直到时间到。这个模式下Jmeter还会比较均匀地把请求分布在整个测试周期内不会因为某一线程提前跑完就掉负载。这个配置方式适合跑回归压测如果你想快速看结果也可以固定循环次数跑一轮数据量不用太大能看出趋势就够了。3.2 集合点定时器制造真正的请求波峰如果你确实需要模拟“100个用户同时点击”这种场景比如秒杀、抢购、考试报名瞬间那就要用到Synchronizing Timer也就是常说的集合点。它做的事情很直接让指定数量的线程到达这个位置后停下等待等其他线程也到了然后一起放行。集合点配置有两个参数Number of Simultaneous Users to Group触发释放的线程数和 Timeout in Milliseconds超时时间。前者表示凑齐多少个线程后放行后者表示最多等多久。一个容易踩的坑就藏在这里——如果你设置的组释放线程数大于线程组实际线程数那这些线程会一直等直到超时时间到了才释放而超时时间内没有达到设定数量说明这个场景本身就不合理结果参考价值很低。我自己的做法是集合点线程数等于线程组线程数超时时间设置为线程组Ramp-Up时间的2到3倍这样既不会因为个别线程启动慢导致死等也不会让策略形同虚设。另外集合点不要放在每个请求前面。一个线程组里如果有多个HTTP请求最好只在核心请求前加一个集合点避免多个集合点之间互相排队把压测节奏搞乱。需要提醒的是集合点本质上是“人为制造并发尖峰”它测的是系统在瞬时冲击下的表现不是系统在持续负载下的表现。如果你的接口本身是常规业务接口没有明显的高峰期特征那集合点不是必需项单纯用线程组加循环次数的方式反而更贴近真实流量模型。3.3 恒定吞吐量定时器与QPS控制还有一种情况你要的不是“用户数并发”而是“每秒固定请求数”。比如目标是一秒打500个请求持续10分钟观察服务端在这个QPS下的表现。这时用线程数来压往往不准因为线程数对应的只是活跃用户数实际QPS取决于接口响应速度。接口快QPS就高接口慢QPS就低。控制QPS的利器是Constant Throughput Timer。它在每个线程的请求之间计算当前整体吞吐量如果低于目标值就适当延迟下一个请求的发送时间从而让整体TPS稳定在设定值附近。配置只需要两个关键项Target Throughput填写目标每秒请求数Calculate Throughput based on选择计算模式常见选“All active threads in current thread group”。这个定时器没有想象中那么精确尤其是在并发线程数较多、目标QPS较高时它只能做到统计意义上的“接近”误差通常在±5%到±10%之间。做精确的QPS控制更专业的工具是分布式压测平台或者专门支持流量模型的工具Jmeter适合快速验证。另外要注意这个定时器的计算模式不同Target Throughput的单位含义也不同选错了数值会差一个量级。选“All active threads in current thread group”时填写的Target Throughput就是整个线程组的总TPS目标不用除以线程数这个细节经常有人搞反。4. 压测中的常见坑与排查链路从错误日志入手4.1 CSV参数并发读取导致的参数错乱不同参数并发最典型的坑就是CSV文件的并发读取问题。场景很常见100个线程CSV文件里有1000条数据跑完后在服务端日志里发现有大量请求参数对不上号userId明明是AorderId却是B的——数据完全错乱了。这个问题的根因在于CSV Data Set Config的默认共享模式All threads。当多个线程同时到达CSV读取点时Jmeter的文件指针移动不是一个原子操作会导致两个线程在同一时刻拿到同一行或者交错读取从而出现参数错配。在压测请求频率较低时问题不明显但一旦并发数上来问题就会集中暴露。排查路径是这样的第一在测试计划里加一个Debug Sampler把当前线程的变量打印到查看结果树里对比请求参数和CSV原始数据是否对应。第二确认CSV Data Set Config的共享模式是不是All threads如果是改成Current thread group。第三检查是否有多个CSV Data Set Config引用了同一个文件这会导致文件被多个组件分别读取游标各自独立也会产生参数乱序。我自己处理这个问题时最直接的做法是在CSV文件里增加一列校验位比如把userId和orderId的拼接值再做一次摘要然后在JSR223断言里校验请求参数是否匹配。这一步虽然增加了脚本复杂度但在数据量大、排查困难时会省非常多的时间尤其是当你需要向开发证明“请求参数确实错了”时这个证据链是无可辩驳的。4.2 并发数上不去连接数限制与本机端口耗尽还有一个非常容易出现的问题——并发数提到一定量级后Jmeter报错开始增多错误信息大多是Connection refused或者Address already in use。此时很多人的第一反应是服务端挂了但实际情况往往是压测机自己先撑不住了。先说Address already in use。HTTP请求是基于TCP的Windows和Linux系统上主动断开TCP连接的一方会进入TIME_WAIT状态端口会被占用一段时间默认是60到120秒。Jmeter在高并发下会大量创建TCP连接如果这些连接在请求结束后没有复用而是频繁建立和断开很快本机的可用端口就会耗尽新连接就无法建立表现就是Address already in use。解决办法有几条路。一是开启HTTP请求的KeepAlive让连接复用不要让每个请求都新建TCP连接。默认情况下HTTP请求的KeepAlive是勾选状态但很多人在请求头部手动加了Connection: close反而把连接复用电给关掉了这个细节容易被忽视。二是调整操作系统的可用端口范围Linux下修改/etc/sysctl.conf里的net.ipv4.ip_local_port_range同时调低net.ipv4.tcp_fin_timeout的值缩短TIME_WAIT的回收时间Windows下在注册表里修改TCP时间戳和端口范围这个操作需要重启系统一般不建议在压测中途进行。三是采用分布式压测让多台压测机分担压力这个问题后面细说。Connection refused则大概率是服务端的连接数被打满了。服务端的tomcat、netty、或者数据库连接池都有上限当并发请求超过服务端处理的极限新连接会被直接拒绝。这个问题的排查手段是看服务端日志、网络连接数、线程池状态而不是在Jmeter这边死磕。通过CSV做不同参数并发时服务端的连接数打满可能比固定参数压测更容易出现因为大量不同参数请求会触发更复杂的业务逻辑单请求处理时间变长连接占用时间也变长连接池的并发压力自然更大。4.3 断言与响应校验你看到的错误率可能不准关于错误率我想先说一个结论聚合报告里的Error%并不代表接口的业务失败率它只代表HTTP层面或断言层面的失败。很多压测报告之所以被质疑正是因为这个数字没有反映真实情况。比如一个接口业务正常时HTTP状态码是200业务异常时也返回200但响应体里的code字段是50001。如果你的脚本里没有加响应断言聚合报告会认为所有请求都成功了Error%显示0%但这个接口的实际失败率可能高得吓人。不同参数并发时这个问题会被放大——因为参数一变化触发业务异常的几率比固定参数高得多大量请求返回“订单不存在”“用户无权限”“数据已过期”等业务错误而你的报告数字完全看不出来。正确的做法是给HTTP请求加响应断言或者更灵活一点用JSON提取器取出响应里的code字段然后和预期值做比较。JSON提取器的配置很简单Post-Processor里添加JSON ExtractorVariable Names填biz_codeJSON Path Expressions填$.code然后在响应断言里判断这个biz_code等于0假设0表示成功。这样错误率统计的就是真实业务失败率。此外在压测不同参数并发时建议把“查看结果树”在调试阶段打开确认几个代表性的请求返回了预期结果正式压测时再把它关掉因为查看结果树会保存所有请求的响应数据非常消耗内存开着它做500并发以上的压测压测机先挂了这不是玩笑。5. 结果数据怎么看从聚合报告到瓶颈定位5.1 聚合报告的核心指标与正确读法压测跑完第一件事是看聚合报告。报告里那一堆指标很多人的习惯是只看Average平均响应时间和Throughput吞吐量然后得出结论“平均响应时间500毫秒TPS 200还行”。但只看平均值是非常危险的特别是不同参数并发的场景下参数的差异会让响应时间分布变得非常不均衡平均值会掩盖大量尾部延迟问题。正确读法是重点关注这几项90% Line、95% Line、99% Line的错误率。90% Line的意思是90%的请求响应时间都在这个值以内99% Line同理。如果Average是500毫秒但99% Line是2秒说明有1%的请求非常慢这部分慢请求极有可能就是某些特定参数触发了慢查询、大事务或外部依赖调用。不同参数并发时99% Line的波动通常比固定参数压测时更明显这正是你需要的信号——它告诉你接口在不同数据下的性能差异有多大。另外要警惕误差范围。如果聚合报告里同一组压测配置跑三次结果差异超过20%那这个测试本身的可重复性就存疑。通常原因是压测机自身资源不足、网络波动、或者请求参数分布不均匀。我遇到的多半是参数分布问题比如80%的参数命中缓存20%的参数穿透到数据库这会导致整体结果取决于缓存命中率而不是服务端的真实处理能力。5.2 通过参数分类对比定位性能瓶颈聚合报告只是第一步更深度的分析是把压测结果按参数维度拆分来看。同一个接口不同参数的响应时间往往差异巨大。这种差异在平均值里被抹平了但不代表问题不存在。一个典型场景接口按城市编码查门店列表。北京、上海的数据量大查询和序列化耗时高小城市数据量小响应快。如果你在参数里混入了不同规模的数据聚合报告的平均值会是一条“看不出问题”的曲线。这时需要做的是把CSV按数据规模分组分别压测对比两个场景的响应时间分布和服务端资源占用才能定位到“大数据量参数导致SQL慢查询”这类根因。这个思路在实战中很有效。我有一次压测一个列表接口发现总体表现正常但日志里时不时出现几条超时请求。把CSV数据按数据量拆分后才发现超时全部集中在某个特定城市的参数上——那批订单数量远大于其他城市导致接口内部的聚合计算成了性能瓶颈。如果没有按参数分类对比这个结论几乎不可能拿到。具体操作上不建议在同一个测试计划里加多个线程组分别跑不同参数而是建议跑多轮每轮只替换CSV文件这样数据的隔离性好也不会因为多个线程组并行而互相干扰。多轮对比时保证线程数、Ramp-Up、循环次数一致这样结果才是可比的。5.3 单机压不动的时候分布式压测与资源规划当并发数目标很高比如3000以上单台压测机很容易先成为瓶颈。这不是服务端的问题而是压测机自身的线程调度、内存、网络栈已经撑不住了。这时就需要分布式压测——一个Master节点控制多个Slave节点每个Slave跑一部分线程汇总上报测试结果。分布式压测坑不少我提三个最关键的。第一所有Slave的Jmeter版本、JDK版本必须一致否则各种莫名其妙的兼容性问题会让你怀疑人生。第二所有Slave机器的时间最好同步否则聚合报告里各Slave上报的时间戳对不上做时序分析时会乱。第三最关键的一点不同参数并发时如果使用CSV参数化需要确保每个Slave读到不重复的数据。一个常见做法是在CSV里增加一列标识按Slave序号拆分CSV每个Slave用独立的CSV文件或者在脚本里用${__machineName}结合计数器生成参数把数据范围人为拆开。分布式压测的网络开销也不容忽视。Master和Slave之间要同步控制命令和汇总测试结果如果压测机数量太多Master本身可能成为通信瓶颈。一个Master控制10个以内的Slave是比较稳妥的配置再往上建议考虑分层架构但这已经超出Jmeter本身的能力范畴了属于压测平台的架构设计问题。最后提一下单机上跑压测时适当调大Jmeter的堆内存是很有必要的。默认的-Xmx1g在500并发以上时经常出现内存溢出启动前建议把堆内存调到4G以上这个直接在jmeter/bin目录下的jmeter脚本或者jmeter.bat里修改HEAP参数就行。
返回列表