
做接口自动化回归或者压测的时候jmeter顺序执行和并发执行这两个概念是绕不开的。我见过不少同事拿到一个测试计划上来就把线程数改成50点开始后看着聚合报告一头雾水——响应时间比预期高出一大截请求顺序也乱。其实问题未必出在脚本本身而是在于没有理解jmeter的执行模型一个线程组内多个线程是并行跑的而每个线程内部取样器默认又是从上到下串行执行。这篇文章我会把这两种执行方式怎么配、什么时候用、有哪些坑全部梳理清楚顺便把beanshell断言、参数传递、定时器这些辅助手段也串起来讲。适合有一定jmeter基础、但被并发和顺序问题折磨过的测试同学新手照着步骤走也能跑通。1. 顺序执行与并发执行的底层逻辑1.1 线程组里请求的默认执行规则先画一下jmeter的基本执行模型。线程组是最外层的容器线程数代表模拟的用户数量Ramp-Up Period代表所有线程启动完成所需的时间循环次数代表每个线程执行脚本的遍数。这三个参数决定了你的压测形态但很多人不知道它们背后隐藏的执行规则。规则其实很简单每个线程内部的取样器默认是从上到下顺序执行的。比如线程组里放了A、B、C三个HTTP请求单个线程执行时会先启动A等到A的完整响应返回后再启动B最后执行C。也就是说在同一个线程内部请求之间天然就是“串行”的。但要注意不同线程之间不存在先后关系。如果你把线程数设成10Ramp-Up设成0这10个线程几乎同时启动各自拿着自己的线程副本从上到下执行请求。线程1可能在执行登录线程2可能已经在执行查询线程3甚至可能已经完成了整个链路。所以在结果树里看到的请求顺序往往不是你以为的那种“A执行10次之后再执行B”而是A、B、C三个请求交叉混在一起跑。还有一个容易忽略的点循环次数大于1时顺序执行是“单个线程内的顺序”不是“全局的顺序”。假设线程数2、循环次数5线程1跑完一圈循环后会立刻开始下一圈线程2同时也在跑最终结果是两个线程各自串行但整体上请求还是交叉的。这就是为什么很多人做多用户压测时发现日志里的请求顺序完全不可控。1.2 顺序执行与并发执行的适用场景搞清楚执行模型之后下一个问题就是什么场景用顺序执行什么场景用并发执行。这个判断不能凭感觉而是取决于你要验证的目标。顺序执行的核心目标是业务链路的正确性。典型场景包括登录后创建订单、下单后发起支付、支付成功后查询结果这类接口之间有强依赖前一步返回的数据要作为下一步的入参。这种情况下你需要的就是顺序执行并且通常用单线程跑配合断言检查每一步的状态码和业务字段确保流程没错。并发执行的核心目标是系统在压力下的性能表现。典型场景包括秒杀、抢购、预约抢名额、双11大促等瞬时高并发以及需要持续施压观察TPS和响应时间曲线的接口。这时你要绕开业务链路的顺序依赖或者把链路拆成可并行的独立请求用多线程同时发起。维度顺序执行并发执行核心目标验证链路正确性、参数传递验证吞吐量、响应时间、稳定性线程数通常为1通常大于1Ramp-Up0即可根据压测目标计算典型断言响应码、业务字段、BeanShell逻辑TPS、错误率、响应时间分位数依赖关系强依赖需逐步校验弱耦合避免线程间数据共享大多数真实项目是混合的功能回归阶段用顺序执行脚本验证全链路性能测试阶段再从顺序脚本里抽出关键接口拆出并发执行脚本。不要把两种目的混在一个脚本里否则最后数据解释会非常痛苦。2. 顺序执行的完整实现方案2.1 从最朴素的配置开始单线程加循环控制器如果你想跑一个纯粹的接口链路回归做法非常简单线程组里线程数设为1Ramp-Up设为0循环次数设为1。然后在线程组内按顺序放请求从上到下排好登录请求、创建订单请求、支付请求、订单查询请求。这样执行时就完全按照“一个请求结束后再发下一个”的顺序跑。但只有一个线程跑一次只能验证一条数据路径没法覆盖多组测试数据。这时就需要配合循环控制器。循环控制器的逻辑是它内部的取样器会按照循环次数重复执行并且循环体内仍是顺序执行。我之前常用的组合是线程组循环次数设为1在线程组内放一个“仅一次控制器”把登录请求放进去再放一个“循环控制器”循环次数设成测试数据的组数把业务请求放进去。这样的好处是登录只执行一次后续业务请求按顺序循环多轮不会每次循环都重新登录。注意线程数不要轻易设为大于1。一旦线程数超过1即使循环控制器存在多个线程之间也是并发的。如果你只是想验证多条测试数据最好的做法是循环次数保持1线程组内循环控制器根据数据条数循环或者用CSV数据文件配合“外部数据循环”来实现数据驱动而不是盲目加线程。2.2 跨请求参数传递JSON提取器与用户自定义变量协同顺序执行中最重要的操作就是参数传递。没有参数传递前一个接口返回的数据根本无法驱动后续请求。以最常见的“登录后获取token”为例。登录接口返回的JSON通常是这样的{ code: 0, message: success, data: { token: eyJhbGciOi... } }后续的接口需要在请求头里携带这个token。配置步骤如下在线程组下添加HTTP Header管理器添加Header信息名称填Authorization值先随便写一个占位符。在登录请求上右键添加后置处理器选择JSON提取器。JSON提取器配置变量名填tokenJSON路径表达式填$.data.token匹配编号填1默认值填NOT_FOUND。回到HTTP Header管理器把Authorization的值改为${token}。这里有个关键点要提醒JSON路径表达式一定要和你实际的响应结构对上。很多人复制接口文档里的路径但实际响应里多了一层data路径少写了一层就直接提取失败。提取失败时变量不会报错而是被赋成默认值后续请求带着${token}原文或者NOT_FOUND发出去响应直接4xx/5xx你还得回头逐个排查。排查方法很简单运行一次脚本后在结果树里选中后面的HTTP请求看“请求体”或“HTTP Header”一栏是否已经替换成了真实token。如果仍然是${token}字样说明提取器没生效如果变量名变成了NOT_FOUND说明路径写错或者响应结构不对。2.3 beanshell断言让每一步都按预期走顺序执行场景下的脚本最怕的是“业务流程跑完了但某一步其实是失败的”。比如登录接口返回了401但后面的查询接口因为不需要鉴权仍然返回200你会误以为整条链路正常。所以必须给关键步骤加断言。jmeter自带的响应断言可以检查状态码和响应文本但遇到复杂判断就有点不够用。比如你想同时校验响应码是200、响应文本包含某个业务字段、以及某个字段的数值范围用响应断言要拆成多个断言且可读性差。这时候用BeanShell断言更顺手。BeanShell断言可以放在任意取样器下面作为后置逻辑执行。我常用的模板如下String responseCode prev.getResponseCode(); String responseData prev.getResponseDataAsString(); if (!200.equals(responseCode)) { Failure true; FailureMessage 响应码异常期望200实际 responseCode; } if (!responseData.contains(\success\:true)) { Failure true; FailureMessage 业务字段校验失败响应中缺少 success:true; } String token vars.get(token); if (token null || token.isEmpty() || NOT_FOUND.equals(token)) { Failure true; FailureMessage token提取失败无法继续后续请求; }这段代码里prev是前一个取样器的结果对象vars是当前线程的变量环境。你可以在断言里写分支逻辑一旦某个条件不满足就标记断言失败。断言失败后默认会停止当前线程剩余的取样器执行这正好符合顺序执行的需求前置步骤失败后续步骤根本没必要再跑。提示BeanShell断言里代码抛异常也会导致断言失败而且失败信息在结果树里不一定看得清楚。建议在代码开头用try-catch包住主体逻辑catch块里把异常信息写入FailureMessage这样至少能定位是哪一行代码出的问题。3. 并发执行的压测配置与细节3.1 线程数、Ramp-Up和循环次数怎么搭配并发执行的核心参数是线程数、Ramp-Up和循环次数。很多人对三者的组合理解得很模糊实际压测时全靠蒙。先说Ramp-Up。它的含义是“所有线程从第一个启动到最后一个启动完成的间隔时间”。线程数设为50Ramp-Up设为0意味着50个线程在同一瞬间开始执行这是最极端的瞬时并发。Ramp-Up设为10意味着从第一个线程启动到第50个线程启动完跨度是10秒也就是平均每秒启动5个线程。这种渐变式加压适合观察系统在不同并发量下的表现而不是一上来就砸一个峰值流量。循环次数的作用是让每个线程在启动后反复执行脚本。循环次数为1时脚本跑完一遍线程就结束总请求数就是线程数乘以取样器数量。循环次数设为100意味着每个线程持续执行100轮压测时间会大幅拉长适合做稳定性测试和多久能暴露内存泄漏这类问题。我建议的组合方式是测试目标线程数Ramp-Up循环次数瞬时峰值并发10001阶梯加压观察每秒递增如5/10/205~10秒1长时间稳定性5010持续循环或设定时长关于压测时间的控制jmeter还支持用“恒定吞吐量定时器”或“Duration”属性来限定压测的时长比如压10分钟就自动停止。这个比循环次数更好用因为循环次数无法控制总时长。3.2 同步定时器制造真实并发瞬间现实中的秒杀、抢票场景用户不会均匀地在10秒内发起请求而是“在开闸那一刻同时冲进去”。jmeter的同步定时器就是干这个用的。同步定时器的作用是把线程组里到达该定时器的线程聚合起来达到指定数量后一起释放。配置里有三个关键参数Number of Simulated Users Group by收集多少个线程后一起放行。Timeout in Milliseconds最多等待多少毫秒超时后不再等待未满数量也直接放行。一般放在要并发的那批请求的前面。举个例子。我压一个抢券接口线程数设成100Ramp-Up设为0在抢券请求前面加同步定时器Number of Simulated Users Group by设为100Timeout设为5000。结果就是100个线程全部启动后在同步定时器处集合集合完成后同一瞬间发起100个抢券请求。这里有个大坑如果你把同步定时器的收集数设置得比线程数还大比如线程数50、收集数100那所有线程都卡在定时器里永远等不到100个除非Timeout到了才被强行释放。所以收集数必须小于等于线程数。另外同步定时器会影响聚合报告里的响应时间。因为它把请求真实发起时间延后了所有请求几乎同时到达服务器此时测到的响应时间包含了排队时间不能代表单请求正常耗时。如果你要统计的是“用户实际感知的响应时间”建议分开跑两轮一轮只测并发峰值一轮不做同步定时器单独测接口平均耗时。3.3 事务控制器和常数吞吐量定时器在并发场景中的作用并发场景里只看单个请求的响应时间不够很多时候压的是“整个业务动作”的耗时。比如下单动作可能包含了查库存、生成订单、扣减优惠券三个接口三个接口分别看耗时意义不大合并成一个事务来统计才符合用户感知。事务控制器可以把多个取样器包裹起来聚合报告里就会多出一条以事务控制器名字命名的记录统计的是整个事务从开始到结束的完整时间。配置很简单添加事务控制器把需要合并的请求拖进去勾选Generate parent sample。注意勾选包括定时器、前置/后置处理器的时间这个选项叫Include duration of timer and pre-post processors如果你的场景里前置后置处理耗时较长建议勾上。常数吞吐量定时器则用来控制整体发压速率。比如目标是每秒压50个请求可以在线程组里加一个常数吞吐量定时器Target Throughput设为3000每分钟calculate based on选择all active threads in current thread group。它的作用是动态延时使实际吞吐量逼近设定值。这样系统不会因为瞬间压入太多流量而直接崩掉适合做容量预估和稳定性压测。4. 顺序执行与并发的边界场景与进阶处理4.1 多个线程组之间怎么保证先后顺序默认情况下jmeter的多个线程组是同时启动的线程组之间没有先后顺序。如果你在测试计划里放了线程组A登录和线程组B业务压测A和B会并行执行此时B里的请求可能比A还先发出依赖关系直接被打破。正确的做法有两种。第一种在测试计划面板勾选“独立运行每个线程组”选项让线程组A执行完再执行线程组B。这个选项适合多个线程组之间确实有先后依赖的场景比如先准备数据再执行压测。第二种用setUp Thread Group和tearDown Thread Group。setUp线程组会在正式线程组开始前运行tearDown线程组会在全部线程结束后运行。通常把登录、数据预置、token获取放在setUp里把清理数据放在tearDown里。但这里有一个变量作用域陷阱setUp线程组里定义的变量正式线程组里是取不到的。因为jmeter的变量是线程局部变量线程组A创建了一个变量线程组B根本看不见。跨线程组传递参数要么使用__setProperty设置成全局属性再用${__property(token)}读取要么把token写入文件再由后续线程组读取文件内容。// 在setUp线程组的BeanShell后置处理器中设置全局属性 props.put(token, vars.get(token));// 在正式线程组的Header管理器里读取全局属性 ${__property(token)}这种方式在压测里很实用先跑setUp登录拿到token正式压测线程组直接读取全局属性不用每个压测线程都重新登录一遍。4.2 动态处理Cookie、上传文件与中文乱码顺序执行和并发执行都会遇到Cookie和文件上传的问题这里单独提出来说。Cookie处理方面jmeter的HTTP Cookie管理器默认自动管理Cookie响应里的Set-Cookie会被自动保存后续请求自动携带。这在单线程顺序执行时没问题。但在并发场景下Cookie管理器默认是每个线程独立维护自己的Cookie Store所以线程之间不会串Cookie。如果你发现线程之间Cookie互相污染检查一下是否在Cookie管理器里勾选了“每次反复使用相同Cookie”之类的选项或者手动添加了共享的Cookie值这类配置在多线程下极易造成登录态混乱。上传文件也是常见的顺序执行场景比如先登录再上传头像。jmeter的HTTP请求里切换到Files Upload选项卡填写文件路径、参数名、MIME类型。文件名建议用英文很多后端服务对中文文件名处理不友好出现乱码很难排查。我自己就遇到过中文文件名上传后乱码的情况后来发现是jmeter发送时默认使用ISO-8859-1编码和后端的UTF-8对不上。解决办法是先改文件名再上传或者在HTTP请求里加HTTP Header指定Content-Encoding为UTF-8。还有一个更隐蔽的坑上传文件时请求体被解析成multipart/form-data格式如果你在同一次请求里既想传文件又想传普通参数参数要在HTTP请求的Parameters选项卡里填且Files Upload选项卡里的文件是独立的。顺序执行时上传请求必须放在登录请求之后并且最好在登录请求后加一个断言确认登录成功后再发上传请求否则上传会直接401。4.3 怎么判断顺序执行是否真的生效脚本写完之后可以通过观察结果树里的时间戳来判断请求顺序是否符合预期。结果树会显示每个请求的开始时间和结束时间。如果A请求的结束时间小于B请求的开始时间说明A、B之间确实串行执行如果时间有重叠说明两个请求并行跑了。更直观的办法是在脚本里加一个BeanShell取样器打印当前线程名和时间戳。log.info(Current Thread: Thread.currentThread().getName() at System.currentTimeMillis());然后在jmeter的命令行窗口里查看输出能清楚看到每个请求是在哪个线程、哪个时刻发起的。这样做的好处是你不仅知道线程顺序还能看出不同线程之间的交叉情况对于定位“数组越界”“请求乱序”这类问题很有帮助。我用这个方法定位过一次非常隐蔽的问题线程数设为5时某个依赖前序接口数据的请求频繁报错看结果树时又看不出明显规律。加上时间戳打印后才发现线程1的下单请求还挂在等待响应时线程5已经开始执行支付请求但支付请求依赖的订单号还没生成。本质上是线程并发导致的依赖数据错乱。这让我坚定了“压测脚本必须区分顺序和并发”的结论。5. 常见问题排查与避坑清单5.1 常见问题速查表根据我实际踩过的坑整理一张速查表按现象查原因。现象原因解决方案请求顺序乱了后面的接口先执行线程数大于1多个线程并行执行改为线程数1或使用独立运行线程组后面的请求拿不到token或参数JSON提取器路径写错、提取器位置不对、跨线程组变量不可见核对JSONPath提取器放在请求下一层跨线程组用全局属性并发数上不去TPS低于预期Ramp-Up时间太长、同步定时器卡住等待、固定定时器间隔过大Ramp-Up改为0同步定时器收集数小于等于线程数合理设置定时器beanshell断言报错但接口正常断言代码异常如字段不存在、数据类型不匹配优先校验响应格式用try-catch包裹断言逻辑上传文件中文文件名乱码编码不一致jmeter默认与后端编码不同文件名改英文或配置UTF-8编码聚合报告响应时间异常高同步定时器集合等待时间被计入分离并发与单请求测试分开统计5.2 一个真实联调案例拿我之前做的电商下单链路举个例子流程是登录、创建订单、支付回调。最初我在功能验证阶段直接把线程数设成10Ramp-Up设为0结果脚本跑了不到3分钟就一片报错。查看日志发现同一账号被多个线程同时下单库存反复扣减订单状态也混乱。原因是数据没有隔离且线程并发导致顺序依赖断掉。后来我把脚本改成两套。第一套是回归脚本线程数为1登录用仅一次控制器订单和支付放在事务控制器里每步加beanshell断言校验响应码和关键字段订单号用JSON提取器从上一步响应中提取。这套脚本跑得非常稳定接口报错时能直接定位到具体是哪一步。第二套是压测脚本单独抽取创建订单接口线程数设为50每个线程用独立的测试账号数据通过CSV文件参数化接口前置同步定时器模拟集中下单的场景。整套压测下来TPS曲线和数据分布都合理没有再出现数据串号的问题。这个案例想说明一个道理顺序执行和并发执行不是互斥的它们服务于不同的测试目的。你只需要在写脚本前想清楚“这次验证的到底是什么”然后按照对应的模式去配置大部分问题都能避免。我自己在实际项目里养成一个习惯每次建测试计划前先问自己三个问题——这次跑的是功能流程还是性能验证依赖接口的顺序是否重要目标并发量是瞬时到达还是均匀分布答案清楚了再决定线程数怎么配、同步定时器要不要加、断言写到什么程度。这套思路帮我减少了不少返工特别是压测脚本一改就要全部重新跑。真心建议你也在自己的脚本里加一个beanshell断言顺手做的事能帮你省掉半夜看日志的时间。