ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JMeter实战:一万并发串联接口压测的完整思路与配置

JMeter实战:一万并发串联接口压测的完整思路与配置 年前接到一个活动项目的压测任务场景很典型一万名用户同时去请求两个活动接口而且这两个接口还是串联的——第二个接口必须用到第一个接口的返回结果。这类场景在互联网业务里太常见了要么是参与活动领奖→用奖品兑换权益要么是下单→支付接口之间有先后依赖关系压测时如果只测单个接口根本没法反映真实业务链路的承载能力。我做压测这些年最深的体会是技术难度往往不在工具操作上而在场景建模和问题定位上。一万并发看着只是在线程组里填个10000但真要跑起来压测机本身、网络连接、服务端资源、接口关联、数据准备任何一个环节掉链子结果都没法看。这篇就把这次压测的完整思路、配置步骤、关联实现和排查过程捋一遍给后续做类似活动接口压测的朋友一个可以直接抄的作业。1. 压测前期先把业务逻辑和场景模型理清楚1.1 一个典型的活动业务串联场景拆解这次要压的活动是典型的两步走用户可以参与活动系统返回一个参与记录编号和对应的奖品兑换码紧接着用户拿着这个记录编号和兑换码去进行奖品核销。两个接口分别对应两次HTTP请求第二个请求的入参完全依赖第一个请求的响应数据。我一般接到这种有串联关系的压测需求第一步不是打开JMeter而是先和开发把接口文档对清楚第一个接口返回的JSON里哪个字段是第二个接口需要的这个字段在响应体的什么路径下第二个接口需要的是query参数、header还是request body这个决定了提取后的变量在什么地方引用。两个接口之间是否有超时限制比如用户必须在几秒内完成兑换这个会影响压测时两个请求之间要不要加固定延时。这次的情况是第一个接口返回类似这样{ code: 0, msg: success, data: { recordId: R20240501001, prizeCode: PRIZE888 } }第二个接口在请求体里需要传recordId和prizeCode这两个字段。那整个压测链路就非常清晰了第一个请求发出后从响应里抠出这两个字段的值作为第二个请求的入参。这就是JMeter里常说的接口关联实现方式不外乎JSON提取器、正则表达式提取器复杂一点的会用BeanShell或JSR223脚本但能简单就简单后面细说。1.2 一万并发该怎么理解不是简单填个10000很多人一看到一万名用户同时请求脑子里的第一反应就是线程数填10000然后把循环次数设为1点下启动就完事了。这其实是新手最容易踩的坑。首先要搞清楚业务上的同时请求到底是什么概念。用户不会真的在同一微秒内全部点按钮而是有一个到达过程。所以压测时我们要模拟的不是瞬间到达而是某个时间段内的稳态并发——即系统在持续承受一万在线用户不断发起请求的压力。JMeter里控制这个行为的关键参数一个是线程数另一个是Ramp-Up Period启动延迟时间。Ramp-Up Period指的是所有线程在多长时间内全部启动完毕。如果设置60秒那一万线程就是大约每秒启动167个线程10000÷60。这样做的好处是给服务端一个预热的过程避免一开机就一万请求砸过去直接把服务打崩根本测不出真实的瓶颈拐点。但如果业务场景就是要模拟极端瞬间冲击比如整点秒杀那Ramp-Up可以缩短到几秒甚至填0表示立即并发不过这种情况下压测机自身的调度压力也最大。我的建议是初次压测用阶梯加压不要一上来就拉满一万。先500、1000、3000、5000、8000、10000这样分阶段跑每个阶段观察响应时间和错误率的变化找到性能拐点。这比一次性跑完拿到一份全红的报告要有意义得多。1.3 压力模型与数据准备一万个用户同时跑每个用户如果都用相同的数据请求很容易被服务端的缓存或者幂等校验干扰。比如第一万个用户请求参与的接口结果发现和前一个用户拿到的是同一个活动名额那接口内部可能直接返回已被领取错误率就会虚高。所以在压测之前必须把测试数据准备好。这次活动接口用户维度上最核心的是用户ID和手机号。我让开发和测试环境导出了一万个真实脱敏用户数据做成了CSV文件每行一个用户大概长这样uid,phone U10001,13800000001 U10002,13800000002 ...在JMeter里用CSV Data Set Config参数化让每个线程从文件里按顺序或随机取一条数据。这样做的好处是每个虚拟用户都有独立身份更贴近真实场景也能避免因为数据重复造成的业务逻辑误判。这里有个细节要注意CSV Data Set Config放在线程组下、HTTP请求之前默认是每个线程取一行。如果想要随机分配用户可以把共享模式改成每次取样或者用随机顺序。但如果是严格模拟一个用户一次请求顺序取更合理因为后面还要统计每个用户的操作成功率。2. JMeter核心配置线程组、默认请求与连接参数2.1 线程组这样配置才不会把压测机打垮JMeter的每个线程在操作系统层面就是一个真正的线程一万个线程意味着你要开出一万条并发执行流。这对压测机本身的CPU和内存是有硬性要求的不是随便一台电脑就能扛住的。我在压测前第一件事是调大JMeter的JVM堆内存。默认的JMeter启动脚本给的内存很小跑几百线程还行跑一万线程分分钟OutOfMemoryError。修改方式很简单找到JMeter安装目录下的bin/jmeter.batWindows或者jmeterLinux编辑文件里的堆内存参数# Windows: jmeter.bat set HEAP-Xms6g -Xmx8g -XX:MaxMetaspaceSize2g # Linux: 通过环境变量覆盖 export HEAP-Xms6g -Xmx8g -XX:MaxMetaspaceSize2g这里注意-Xms和-Xmx最好是同一个值一次性把内存申请到位避免运行过程中频繁扩容触发GC停顿。如果压测机内存不太够至少保证4G起步因为除了线程栈空间JMeter还需要保存每个请求的响应数据如果开启了结果树监听的话。线程组的配置这次我用的参数是线程数10000Ramp-Up Period120秒每秒约83个线程启动循环次数1次调度器不勾选压测时长通过实际脚本运行控制循环次数我设置为1原因是这次关注的是一万用户的完整操作成功率而不是单个用户反复刷接口。如果要测长时间稳定性可以把循环次数设大或者勾选永远配合调度器的持续时间使用。跑一万线程时压测机在非GUI模式下大约要消耗8G左右内存CPU也会跑到多核满载。所以强烈建议用命令行模式执行压测不要在GUI界面里挂着跑大并发GUI模式本身就要消耗大量资源还会因为界面刷新影响压测数据的稳定性。我是这样执行的jmeter -n -t activity_pressure_test.jmx -l result.jtl -e -o report/-n表示非GUI模式-t指定脚本-l输出原始结果文件-e和-o是生成HTML报告用的压测完直接就能看到完整的图表分析。2.2 HTTP请求默认值和超时参数一万并发下HTTP请求的配置越统一越好管理。我习惯在测试计划下先添加一个HTTP请求默认值HTTP Request Defaults把公共信息一次性配好协议http服务器名称或IP被测环境地址端口号8080内容编码UTF-8连接超时3000毫秒响应超时10000毫秒超时参数一定要配而且不能配得太大。我之前见过有同事把响应超时设为0表示无限等待结果服务端出现局部故障时线程全部挂在等待上后续请求越积越多最后把压测机自己搞崩了。设置合理的超时能让线程快速释放比如连接超时3秒、响应超时10秒一旦服务端处理不过来请求就会以超时错误的形式记录下来错误率指标才能反映真实情况。同时在请求里需要加的公共头也通过HTTP信息头管理器统一设置比如Content-Type: application/json、Authorization等。这里有一个很容易被忽略的点如果活动接口需要登录态JMeter怎么处理一般有两种方案一种是在压测前先跑一次登录接口用正则或JSON提取器拿到token再加到后面的请求头里另一种是让开发在测试环境统一放开鉴权或者提供一个万能测试token。我这次用的是第二种开发给了一个测试环境的固定token省去了登录步骤压力更聚焦在活动接口自身。2.3 阶梯加压从1000慢慢爬到10000直接在线程组里写死10000线程虽然也能跑但问题是一旦服务端在3000并发时就扛不住了你得到的结果只有一堆超时至于瓶颈到底在哪、什么时候开始恶化完全看不出来。所以我日常压测更喜欢用阶梯加压这次也不例外。如果用JMeter自带的线程组想实现阶梯效果有两个办法一是做多个线程组分别设置1000、3000、5000、8000、10000然后用启动时间错开二是直接装一个jmeter-plugins-manager通过它安装Custom Thread Groups插件里面有现成的Stepping Thread Group逐步加压线程组和Ultimate Thread Group终极线程组。Ultimate Thread Group的配置方式很直观可以一行一行地定义线程池每一行都包含启动时间、线程数、持续时间、停机时间。我这次的配置大致是线程数延迟启动持续时间停机时间10000秒60秒10秒300060秒60秒10秒5000120秒60秒10秒8000180秒60秒10秒10000240秒120秒20秒这样跑下来的结果里每个阶段的错误率和响应时间都分布在不同的时间窗口对照聚合报告可以明显看到从5000并发开始平均响应时间开始拉高到8000时错误率上升到10000时出现超时瓶颈就一目了然了。3. 接口关联实现把第一个接口的返回值交给第二个接口3.1 JSON提取器和正则提取器的选型这是串联接口压测的核心环节。在JMeter里做接口关联最常用的两种后置处理器就是JSON提取器JSON Extractor和正则表达式提取器Regular Expression Extractor。到底用哪个我的选择标准很简单接口返回的是标准JSON结构优先用JSON提取器因为JSONPath的语义清晰、容错性好如果接口返回的是非标准结构比如夹杂着各种描述文本的HTML、或者JSON嵌在字符串里或者响应体太大、结构不固定那就退回用正则表达式提取器。这次两个接口返回的都是标准JSON我直接用的JSON提取器配置简单可读性也强。但正则提取器我也一并讲了因为实际工作中总会碰到一些返回结构比较奇葩的接口到时候就知道多会一种方案有多省事了。3.2 具体配置步骤含表达式写法在第一个HTTP请求参与活动接口上右键添加后置处理器选择JSON提取器。这里我给两个字段各建一个JSON提取器也可以在一个提取器里一次提取多个变量后者更高效。单个字段的提取变量名称recordIdJSONPath表达式$.data.recordId匹配编号1默认值NOTFOUND再添加一个JSON提取器提取prizeCode配置同上JSONPath表达式改成$.data.prizeCode。如果嫌多个提取器麻烦JMeter也可以在一个JSON提取器里同时提取多个变量。在变量名称和JSONPath表达式两栏里用分号分隔比如变量名称recordId;prizeCode JSONPath表达式$.data.recordId;$.data.prizeCode这样一次请求就能同时拿到两个变量。注意两边的数量必须一一对应写错了提取就会失败。我把默认值统一设置成一个容易发现的字符串比如NOTFOUND这样一旦提取失败第二个接口的请求体里会出现明显的NOTFOUND字样从请求日志或者结果树里一眼就能看到关联有没有生效。如果是用正则表达式提取器配置方式是这样的变量名称recordId正则表达式recordId:\s*([^])模板$1$匹配编号1默认值NOTFOUND这里的正则含义是匹配recordId:后面紧跟的引号字符串括号里的部分就是我们要抓取的内容。需要注意正则里的转义JMeter对双引号和反斜杠的处理有时候会让人头大测试完多看一眼提取结果总没错。3.3 关联的调试与验证配置完提取器先不要急着跑一万并发。我会加一个调试取样器Debug Sampler放在第二个请求之前然后先跑1个线程打开查看结果树观察提取到的变量值。Debug Sampler会用JSR223脚本的形式把当前线程上下文里的变量全部打印出来如果你看到这样的输出recordIdR20240501001 prizeCodePRIZE888说明关联成功这时候再把Debug Sampler禁用掉正式跑压测。注意Debug Sampler在正式压测时一定要禁用或删除因为每跑一次线程它都要把所有变量打一遍大量IO操作会把压测机的性能拖垮影响结果准确性。第二个接口的请求体里直接引用变量就行请求体大概是这样的{ uid: ${uid}, recordId: ${recordId}, prizeCode: ${prizeCode} }引用变量就是${变量名}这种格式JMeter会在运行时自动替换成实际值。这里有一个细节很关键提取器必须挂在第一个HTTP请求下面作为它的子节点而且在取样器顺序上要排在第二个HTTP请求之前。JMeter线程组里是顺序执行的第一个接口跑完提取器立刻处理响应结果把变量存到当前线程的变量池里紧接着第二个接口才能拿到变量。如果你的提取器放错位置第二个接口拿到的永远是NOTFOUND因为线程跑到第二个接口的时候提取器压根还没执行。4. 参数化、断言与监听器让压测数据真实可分析4.1 CSV参数化模拟一万用户身份前面提到过这次压测准备了带一万个用户信息的CSV文件。在JMeter里做参数化的标准组件是CSV Data Set Config具体配置如下文件名/path/to/users.csv文件编码UTF-8变量名称uid,phone分隔符,是否允许带引号False遇到文件结束符继续循环 或 停止线程线程共享模式当前线程组线程共享模式这里有个坑值得说下。默认配置下CSV文件由所有线程共享读取每个线程拿到的行是顺序错开的如果选择当前线程每个线程会从头开始读文件容易造成数据重复。我的建议是共享模式保持默认然后在请求里只引用uidphone用不到就不引用。不过CSV里多列数据并不会影响读取因为JMeter是按变量名来索引的。如果测试环境没有那么多真实用户也可以用JSR223取样器动态生成随机数据来模拟比如用${__Random(1,10000)}生成随机ID或者用${__time(yyyyMMddHHmmss)}生成不重复的操作流水号。但要注意这种方式生成的随机数据如果业务端有唯一性校验很可能产生大量冲突所以有真实账号池的时候优先用CSV实在没有才用函数生成。4.2 响应断言配置压测时只看HTTP状态码是远远不够的。如果接口内部抛了业务异常响应码可能还是200但响应体里的code字段变成了非0。这时候如果没有断言这些请求会被当成成功请求统计错误率被严重低估报告也就失去了参考价值。我这次为两个接口分别添加了响应断言Response Assertion逻辑是响应文本中包含指定业务成功标识。活动接口的成功标识是code:0所以断言配置为响应字段响应文本模式匹配规则包含测试模式code:0这样只要接口返回值里包含code为0的字段请求就算通过如果业务失败比如返回活动已结束或库存不足断言就会失败错误率立刻体现出来。还有一点建议不要在正式压测时把查看结果树监听器挂在测试计划里。我在调试阶段会开着查看结果树确认关联和断言配置正确但正式压测前一定把它禁用。原因很简单结果树会把每个请求的完整报文写到内存里一万并发下这个监听器自己就能把堆内存吃满导致压测提前OOM。4.3 监听器与指标解读这次压测用了两种结果输出方式一是聚合报告Aggregate Report直接看关键指标的汇总二是生成HTML报告方便后续和团队一起复盘。聚合报告里我最关心的几个指标是Samples总请求数。如果线程数是10000每个线程发两个请求Samples应该是20000左右如果少太多说明有请求没发出去或者被压测机卡住了。Error%错误率。这里需要看是业务报错还是连接超时结合断言结果判断。Average平均响应时间。活动接口一般要求P95小于1秒平均响应时间不能定得太死因为不同接口逻辑复杂程度不同。Throughput吞吐量单位是请求数/秒代表系统每秒能处理多少请求。不过聚合报告也有个短板它不直接给TP99这种分位数。如果想知道99%的请求在多少毫秒内完成需要用汇总报告的插件版本比如p99/p90列或者直接在HTML报告中查看响应时间百分位图。这次跑完我导出的HTML报告里能直接看到中位数、90%响应时间、99%响应时间活动场景用户体感更接近这种分位数指标而不是平均响应时间。配套地我还会在压测过程中同步监控服务端的CPU、内存、数据库连接池和慢SQL情况因为接口响应变慢往往不是接口本身的问题而是下游的数据库或者Redis扛不住了。只有把压测端的TPS指标和服务端的系统指标放在一起看才能锁定真正的瓶颈。5. 在压测现场常遇问题与排查实录5.1 压测机自身瓶颈排查一万线程不是所有机器都能扛的。我第一次跑的时候压测机是一台4核8G的云主机结果跑到6000线程左右的时候JMeter报了OutOfMemoryError压测被迫中断。后来我把堆内存调大再观察压测机的CPU发现已经打满了线程上下文切换严重客户端本身成了瓶颈。这个问题的解决方案是上JMeter分布式压测也就是大家常说的agent模式。一台主控机Controller负责调度脚本多台从机Agent负责执行实际压力。比如两台从机每台跑5000线程总压力还是10000但每台机器的负担直接减半。分布式压测的配置不算复杂从机上启动jmeter-server主控机的jmeter.properties里配置remote_hosts把从机IP和端口默认1099加进去然后脚本通过远程启动选项分发到各从机执行。但有几个细节必须注意CSV文件里的用户数据每台从机都要有一份且路径一致否则从机读不到文件会直接报错。从机的系统时间尽量同步否则时序数据会乱。主控机不产生压力只做调度和结果汇聚所以主控机的配置可以比从机低一些但别低太多。如果实在没有多台机器做分布式也可以试着用JMeter的线程组配置做单机极限压测但要做好心理准备结果可能受限于客户端资源。我的判断标准是单机压到CPU超过80%测出来的数据就不太可信了优先考虑扩展从机。5.2 服务端性能瓶颈与连接数问题去掉压测机自身问题后最常遇到的另一类问题是服务端连接耗尽。有一次压测过程中服务端的Tomcat直接拒绝连接错误日志里大把的Connection refused。排查了一下Tomcat默认的maxThreads是200数据库连接池默认配置也不大一万并发远超它本身能承受的连接数上限。这种问题从压测的角度不能算是测试脚本的问题但对压测执行者来说必须能判断出来并推动服务端调优。常见的调优方向包括调整Web容器线程池大小比如Tomcat的maxThreads调整为2000并同步调整acceptCount。调整数据库连接池Druid或HikariCP的最大连接数要匹配实际并发避免连接池排队。开启连接复用HTTP请求头里加上Connection: keep-alive减少TCP握手开销。调整操作系统层面参数Linux上服务端需要适当调大文件描述符默认1024肯定不够至少要设到65535压测机上也要避免TIME_WAIT端口堆积。说到TIME_WAIT压测机上如果大量出现java.io.ioexception: error writing to server这种报错基本上就是TCP连接建立不过去了。排查方法是压测中在压测机上执行netstat -ant | grep TIME_WAIT | wc -l如果数量达到几万说明本地临时端口号已经排光了。Linux系统可以这样调整# 调整临时端口范围 sysctl -w net.ipv4.ip_local_port_range1024 65535 # 开启连接复用 sysctl -w net.ipv4.tcp_tw_reuse1注意这些是压测机上的优化生产服务器或者被测环境不建议随意开启tw_reuse有可能引入连接数据混乱的问题。5.3 关联失效的几类原因接口关联这块最容易出的问题有三类这里整理成一个速查表方便排查现象可能原因排查方法第二个接口请求体里全是NOTFOUNDJSON提取器的JSONPath表达式写错先用单线程查看结果树观察提取结果变量没提取到但正则表达式看着没问题响应体过大正则匹配到第一个就停了检查匹配编号和贪婪匹配写法一部分用户成功、一部分失败业务数据差异导致响应结构不同打开结果树对比成功和失败响应体断言报错但接口返回正常响应文本里字符串格式不一致比如code和0之间有空格用包含模式而非等于放宽模式匹配规则分布式从机上关联全部失败CSV文件路径或编码不一致确认每台从机的文件实际存在且内容一致还有一个非常隐蔽的问题如果两个接口在同一个线程组里但JMeter的循环次数大于1提取器会反复执行并把变量覆盖。如果第一个接口的返回结果不是每次都是新数据第二个接口用到的可能是上一轮的旧值。所以串联接口的压测场景循环次数通常设为1或者确保每次循环的数据互不干扰。这个踩过一次印象特别深。5.4 分布式压测的规划最后说说这次一万并发在分布式压测下的分配方案。活动接口的单请求处理时间大约在几十毫秒到几百毫秒之间单机跑一万线程至少需要16G内存和8核CPU。我这次是用3台从机来分担每台从机分到3333个线程主控机负责统一的脚本分发和结果汇总。不过需要提醒的是在分布式模式下每个从机都在输出自己的压测结果最后汇聚到主控端时主控端的磁盘IO可能成为瓶颈。所以我一般让每台从机把结果写到本地压测结束后再用脚本汇总成一份完整的聚合报告而不是全部实时传回主控机。小技巧但在大规模压测时很好用。从性能分析的角度这次压测结束后我拿到了几个关键数据系统的最大TPS大概是多少、在哪个并发量级开始劣化、错误率是否有突刺、两个接口之间的吞吐量差异有多大。这些数据最终汇成一份压测报告给团队和运维做容量评估参考后面加机器扩节点也有数据支撑。小结之外的几句实操体会这类串联接口的高并发压测我做了不止一次最想提醒后来人的是三件事第一关联提取器一定要先小规模调通不要拿一万线程去赌提取表达式有没有写对第二压测机资源要提前评估内存、端口、文件描述符这些基础环境配置比测试脚本更早暴露问题第三任何一次压测都要保留原始结果文件别压完就删后面复盘和对比调优全靠它。最后再分享一个细节跑完压测后不要只盯着平均值和错误率多看一眼响应时间的90%、99%分位活动接口的用户体感往往取决于最差的那一批请求能不能扛住。把P99压下去比单纯把平均值做低对业务的意义要大得多。
返回列表