ARTICLE DETAIL

资讯详情

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

JMeter同步定时器(集合点)实战:原理、配置与常见坑

JMeter同步定时器(集合点)实战:原理、配置与常见坑 做性能压测这些年我见过太多看起来没问题一上线就出事的项目。最典型的一种情况JMeter报告里TPS很漂亮、平均响应时间很低、错误率是0结果活动一上线用户一拥而入系统直接卡死。复盘的时候翻脚本才发现——你压的并发根本不是并发。今天这篇文章我把JMeter里最容易被人忽视却至关重要的一个组件——同步定时器Synchronizing Timer也就是大家常说的集合点掰开揉碎讲清楚。从一个真实压测案例出发说清它的原理、配置、实战用法和坑保证你看完能直接用在自己的性能测试脚本里。1. 线程组默认行为解密为什么你的压测不是真并发1.1 从线程启动到请求发出中间隔着一整个世界很多刚接触JMeter的人有一个误解我在线程组里设置了100个线程Ramp-Up Period填了0启动测试的那一刻这100个用户就同时把请求打到服务器上了。但真实情况完全不是这样。JMeter的线程组只是负责创建100个独立的线程每个线程创建之后会立刻执行自己管辖范围内的Sampler。听起来很同时但这里有一个关键的时间差100个线程的创建、启动、各自执行前置逻辑比如从CSV读取用户数据、处理参数化、执行前置BeanShell脚本都需要时间。哪怕Ramp-Up是0JMeter也会在极短的时间内逐个创建线程这个极短可能只有几十毫秒但几十毫秒对高性能接口来说已经足够让先启动的线程把请求发完并收到响应了。我做过一个实测本地起了一个简单的Spring Boot接口逻辑就是打印当前时间戳并返回。JMeter里配置100个线程、Ramp-Up为0、循环1次不加任何定时器。结果服务端实际收到请求的时间跨度大约在80到150毫秒之间。也就是说号称同时并发100用户真实到达服务器的请求是分布在100多毫秒时间内逐个过来的。对于平均响应时间在50毫秒以内的接口这100多个请求几乎不会在服务端产生真正的排队效应。你测出来的结果本质上更像100个用户依次快速点击而不是100个用户同时按下按钮。1.2 一个小实验用响应数据验证到达时间差要让这个问题变得直观可以在脚本里加一个非常简单的操作在HTTP请求里添加一个参数值来自${__time()}函数然后到聚合报告或查看结果树里对比每个样本的时间戳。举个例子接口原本不需要参数但为了观察到达时间我们给它额外加一个query参数比如_t${__time()}。跑一次100线程的压测把结果导出后你会发现每个请求的_t值相差几十毫秒甚至几百毫秒分布是杂乱的而不是集中在同一个毫秒。这个实验看似简单但它是理解集合点价值的关键。你只有亲眼看到并发实际上是错峰到达才会明白为什么有些压测报告和线上表现差距那么大真实用户做秒杀、抢购、考试报名时大家都在等待同一个开放时刻那个瞬间流量洪峰才是系统真正需要承受的压力。而你用默认线程组压出来的流量形态是斜坡式爬升或者均匀散弹根本模拟不出那种瞬间冲击。1.3 集合点概念的来源与定位集合点这个叫法最早来自LoadRunner英文叫Rendezvous直译就是会合点。它的作用很直白让所有虚拟用户在执行到某个位置时停下来等待等凑够指定数量后再一起继续往下走。JMeter里对应的组件叫同步定时器Synchronizing Timer逻辑上就是干同一件事。它解决的问题正是上面说的那条时间差让一批线程在发起请求前排队集合集合完毕后再统一放行从而在服务端制造一道真正的流量洪峰。这里需要明确一个定位同步定时器不是用来模拟用户行为的它是用来模拟极端瞬间并发的。如果你压测的目标是验证系统在平稳负载下的长期稳定性那同步定时器不但没用反而有害。但如果你要验证的是秒杀、抢券、考试报名、抢票这类尖峰流量场景它就必不可少了。2. 同步定时器工作机制与参数拆解2.1 底层机制线程的起步枪同步定时器的内部逻辑其实很简单任何工程师都能理解它维护一个计数器每个线程到达定时器时计数器加一然后线程进入阻塞状态等待计数器达到你设定的目标值。一旦达到目标值所有阻塞的线程被同时唤醒继续执行下一个步骤。举个例子你设置了集合5个用户后释放那么前4个到达的线程会全部挂起等待第5个线程一到5个线程被同时放行几乎在同一毫秒内向下执行。如果设置了超时时间那么即使一直没凑够数量到时间后已经到达的线程也会被强制释放避免线程永远等下去。这个机制的本质是在JMeter的线程层模拟了一种屏障Barrier。在Java并发编程里CyclicBarrier或CountDownLatch干的就是这个事。理解了这一点你就能推断出很多使用细节比如在同步定时器之后添加、且依赖前置请求返回值的逻辑比如正则表达式提取器、JSON提取器在执行时不会出问题因为定时器只是阻塞和放行并不清空上下文。2.2 Number of Simulated Users to Group by一个必须动脑子的参数这个参数是所有困惑的源头之一。官方文档给的解释是每次释放的虚拟用户数量但实际使用时你必须结合线程组的总线程数来设计。假设线程组里配置了50个线程这里也填50那么这50个线程全部到达同步定时器之后会一起被放行形成最大规模的一次性冲击。如果你填的是10那意味着每凑够10个线程就释放一次50个线程会产生5波小团体并发每波之间有一个微小的间隔因为后面一批线程从创建到到达定时器还需要一点点时间。但这里有一个新手最容易踩的坑如果你填的数量大于线程组的总线程数比如线程组只有50个线程参数填了100那么这50个线程到达定时器后会一直等下去永远凑不够100。如果不设置超时时间所有线程会无限阻塞压测直接卡死。所以我通常的建议是如果要用同步定时器模拟一次性的绝对峰值参数值和线程组线程数保持一致如果想模拟多波次冲击——比如每波100人总共500人分5波——那就在线程组里让500个线程循环5次同时把定时器的集合数设为100。这样每轮循环都会等100个线程到齐后放行循环5次正好模拟5波冲击。2.3 Timeout in milliseconds设置不当的后果这个参数决定线程在同步定时器里最长等待多少毫秒。填0表示不限制也就是说会无限等下去。实际工作中我强烈建议不要填0。原因很简单压测环境经常有各种不稳定因素线程组里的某几个线程可能因为前置脚本异常、网络DNS解析慢等问题延迟到达结果大部分线程干等着时间一长你以为系统崩了其实只是定时器在饿等。但填了超时时间也不是万事大吉因为超时后的行为是放行当前已到达的线程这和集合到目标数量再放行是两种完全不同的压力形态。举个例子你配置了集合100个用户、超时3000毫秒。如果某个循环里由于上游原因只有80个线程到达那么3秒后这80个线程会被释放并继续执行这一波的压力就打了折扣。因此超时时间的设置原则是要略大于正常情况下所有线程从启动到走到定时器所需的时间而不是拍脑袋随便填。我一般会在测试计划里先跑一次不压力的调试比如只跑几个线程通过查看结果树里样本的时间戳估算出线程到达定时器的大致时延再乘以1.5到2倍作为超时值。比如正常情况下50个线程全部到达定时器需要200毫秒超时时间我会填500毫秒留足余量。2.4 放置位置对作用范围的影响同步定时器的作用范围由它的位置决定。如果你把它放在某个Sampler的下面作为子节点那么只有那一个Sampler会受它约束如果你把它放在某个逻辑控制器下面那它只对那个控制器范围内的Sampler生效如果你把它放在线程组下面、和HTTP请求平级那么所有线程组里的请求都会先经过它。这个位置问题在实际工作中很有讲究。举个例子压测一个下单流程一个线程组里包含登录、查询库存、创建订单三笔请求。如果你的目标是把创建订单的瞬间流量打满那就应该把同步定时器放在创建订单这个Sampler的子节点下而不是放在线程组层级。如果你放在线程组层级那么所有线程都会在登录之前就集合一次。这会导致什么登录请求全部同时到达后面的查询库存、创建订单反而因为各自的处理速度不同而错峰了。你想压的是订单接口结果压力全压在了登录接口上测试目标直接偏离。还有一种常见操作把同步定时器放在仅一次控制器里配合循环次数来使用可以让每个线程只在第一轮循环时集合后续循环正常执行。这种组合在先同时登录然后各自持续操作的场景里很实用。3. 登录接口5用户真正同时并发实战3.1 测试计划结构与脚本搭建下面用一个最常见的场景——登录接口完整走一遍配置流程。这个例子对应很多人在网上搜的用JMeter测试5个用户并发登录但我会在这里加入同步定时器让这5个用户的登录请求真正同时到达服务端。测试计划结构非常简单测试计划线程组5个线程Ramp-Up 0循环1次HTTP请求login同步定时器集合5个用户超时2000毫秒察看结果树聚合报告注意同步定时器放在了登录请求的下面作为子节点。这个放置方式是官方推荐的做法之一同步定时器会拦截它所属的Sampler在Sampler执行之前先执行同步逻辑。也就是说5个线程执行到登录请求时会先在同步定时器处集合集合完成后5个线程同时发起登录请求。3.2 关键配置清单线程组部分线程数5Ramp-Up Period秒0循环次数1这里Ramp-Up填0是为了让5个线程尽可能快地创建完毕减少线程创建本身带来的时间差。不过前面说过即便填0线程创建也需要几毫秒到几十毫秒不等所以最终还是要靠同步定时器来消除这个差距。HTTP请求部分以POST登录接口为例协议http服务器名称或IP192.168.1.100按实际环境填写端口号8080HTTP请求方法POST路径/api/login消息体数据{username:test_${__threadNum},password:123456}这里用${__threadNum}做了简单的参数化5个线程分别用test_1到test_5登录方便在结果里区分是哪个线程发起的请求。真实场景中一般会用CSV文件准备5组真实用户数据的用户名密码。同步定时器部分Number of Simulated Users to Group by5Timeout in milliseconds20005个线程、集合5个意味着这一轮只需要一次集合2000毫秒的超时设了个保险即使某个线程创建延迟了也不会出现无限等待。3.3 结果分析如何判断真的同时到达测试跑完后光看聚合报告是不够的。聚合报告只会给你平均值、吞吐量、错误率这些汇总指标它无法告诉你5个请求是否真的同时到达。要验证集合效果需要换一个视角。最直观的办法是在登录请求里加一个时间戳参数比如_t${__time()}或者在查看结果树里展开每个样本查看响应头里的Date字段。同时发起请求的话这几个请求的服务端接收时间应该非常接近差异通常在1到2毫秒以内。我从实际执行的结果里摘几组时间戳做个示例线程12025-01-12 14:30:02.103 线程22025-01-12 14:30:02.104 线程32025-01-12 14:30:02.103 线程42025-01-12 14:30:02.105 线程52025-01-12 14:30:02.104有没有发现1毫秒的差距基本可以忽略这5个请求是同一瞬间打到服务端的。而如果不加同步定时器同样的配置跑出来的时间戳可能会分布在20到50毫秒内。这中间的差别就是你压测结果可信度的差距。3.4 聚合报告里那些指标的解读拿到了聚合报告重点看以下几项Samples一共发了多少个请求。这个值应该等于请求名对应的执行次数。Average平均响应时间。加了同步定时器后这个值通常会比不加时略高因为服务端在同一时刻接收了5个并发请求它们之间产生了排队效应。Throughput吞吐量即每秒完成的请求数。注意这个值在5个用户并发一次后很快就测完了所以对于一个只发一次请求的测试来说吞吐量参考意义有限——这只说明功能上能同时接受5个连接远远达不到容量评估的程度。Error %错误率。在登录接口并发场景下如果出现非200响应码或断言失败需要优先看是不是服务端连接池、数据库连接数不够。这里要刻意提醒一句5个用户并发登录只是一个教学级的验证场景它不是容量测试。真正做容量评估时你会需要50、100、500甚至更多的集合数并且结合阶梯加压来看系统在哪个并发量级开始性能恶化。同步定时器在其中扮演的角色是制造出每个阶梯上的真实并发浪头而不是让请求缓慢均匀地撒过去。4. 同步定时器与复杂压测场景的组合玩法4.1 组合阶梯线程组渐进加压下的精准同步点很多性能测试项目要求先低压、再中压、后高压的阶梯式加压策略目的是找到系统的性能拐点。单独用同步定时器的话每一波请求都必须凑够固定数量才释放形态太硬不适合做渐进式观察。我的做法是把同步定时器和阶梯线程组可以用插件Ultimate Thread Group也可以用自带的Stepping Thread Group思路手动实现配合使用。在阶梯线程组的每个阶梯周期内让同步定时器按当前波次人数来集合。举个例子整个测试计划一共300个线程循环3次同步定时器设置集合数为100超时2000毫秒。那么第一轮循环里100个线程凑齐后立刻形成一次100并发冲击第二批100个线程再凑齐后形成第二次第三批同理。如果你在脚本里配合使用了Ultimate Thread Group的延迟启动就能做成每过30秒来一波100并发的节奏用来观察系统在面对周期性尖峰时的恢复能力。这种用法在秒杀活动每隔半小时放一批券这类业务模型里非常贴切。每一个集合点就是一场微型秒杀服务端能否在每波冲击后恢复正常比单纯看极限TPS更贴近业务真相。4.2 叠加断言与提取器集合点后置动作要小心同步定时器只是控制何时放行它不管放行之后你的请求依赖什么数据。如果你在登录请求后面提取了Token下一个请求要带着Token去查询用户信息那么在集合点并发场景下这部分逻辑是安全的。因为同步定时器阻塞和放行发生在Sampler执行之前放行后每个线程依然走自己的运行上下文。但我遇到过一种情况某个用户在登录请求里用了正则表达式提取器提取token然后把token通过${token}传给下一个请求。在低并发时一切正常一旦用同步定时器制造高并发瞬时冲击某些线程会因为服务端处理超时没有返回正确的token导致后续请求报错。这种错误非常容易和被测系统问题混淆。排查时要先在查看结果树里确认失败请求的上一跳是否成功再判断是不是并发导致服务端出现异常。另外如果断言里写了响应体包含某某字段在高并发时也要注意服务端在尖峰压力下可能返回的是限流提示比如系统繁忙而不是正常业务响应。此时断言失败会大量出现这不是JMeter脚本问题而是被测系统本身触发了限流恰恰说明压测达到了效果应该把推断重点放在系统从哪个并发量级开始限流上。4.3 配合while控制器做条件集合还有一个高阶用法值得提一下同步定时器和while控制器配合可以做条件集合。举个例子你要压测一个抢购接口但抢购的前提是用户先登录。有些用户登录可能失败密码错误、账号被锁定这些失败的线程如果继续走到抢购请求本身是不合法的压力来源。解决办法是在线程组里让所有线程先走登录把登录结果保存到一个自定义变量里然后用while控制器判断如果登录失败就一直重新登录最多重试N次登录成功后才进入抢购请求。在抢购请求下挂一个同步定时器这样到达集合点的每一个线程都是已经完成前置条件的合法用户集合点压出来的数据才干净。这个组合写起来也不复杂核心逻辑是在登录请求后面添加一个BeanShell断言或JSR223断言用vars.put(loginStatus, success)或failure记录登录状态用while控制器包裹登录请求条件是${loginStatus} ! success在while循环里给登录请求设置最大重试次数防止死循环登录成功后进入抢购接口抢购接口下挂同步定时器。这样整个脚本的健壮性会提升一个档次尤其适合压测数据质量不高的环境。很多测试项目用的是生产环境脱敏数据里面本来就有一批失效账号如果你不做这层过滤同步定时器一放行一批原本就会登录失败的线程同时打到抢购接口上得出的响应时间数据根本不可信。4.4 动态数据与集合点的冲突处理热词里出现了动态验证码这里简单提一盏灯如果你的登录接口有动态验证码通常的解法是加一个前置处理器去请求验证码接口或读取数据库里的验证码缓存把它填到登录参数里。这个逻辑和同步定时器不冲突但要注意一点验证码的获取也是有耗时的。当一个线程去获取验证码、另一个线程还没开始的时候你得保证获取验证码这个前置步骤不在同步定时器之后。我一般把获取验证码放在同步定时器之前、或者放在BeanShell预处理里让所有线程先把验证码准备好然后到集合点集合再一把冲出去。否则可能出现一种极端情况5个线程同时到达登录请求结果某个线程的验证码还没填进去请求已经发出去了服务端直接报验证码错误你又得花半天时间去排查是脚本问题还是系统问题。5. 同步定时器使用中的高频坑与完整排查链路5.1 坑一超时时间设成0导致的无限期阻塞这个前面提了一嘴这里展开说。有人在配置同步定时器时看到Timeout默认是0以为0就是立即超时结果恰恰相反0表示永不超时。我接过一个压测支持压测过程中发现线程数到了但请求迟迟没有发出去服务端网关日志里完全没有流量。查了半天最后发现是同步定时器的集合数设了100线程组却只有50个线程超时又是0。那50个线程到达定时器后一直等永远等不到100整个压测就僵住了。这种问题在分布式压测场景里更隐蔽因为你可能有多台施压机每台施压机上的线程组线程数是50你以为总共有150个线程但同步定时器的集合是按节点独立计算的——每台机器各自等各自的永远凑不满设定值。结论很明确集合数绝不能大于单台施压机上实际到达定时器的线程数超时时间不要填0哪怕设一个大一点的值也好给压测留一个兜底释放的出口做分布式压测时每个Jmeter Agent节点上的线程数必须大于等于集合数。5.2 坑二Ramp-Up时间过长导致集合点形同虚设线程组的Ramp-Up Period是在多少秒内创建完所有线程。如果你有50个线程Ramp-Up填了10秒那么线程并不是同时创建的而是每200毫秒创建一个。这种情况下同步定时器虽然设置的是集合50个用户后释放但第1个线程和第50个线程之间本身就差了10秒哪怕你在同步定时器里设置了比较长的超时时间前49个线程也会一直等等到第50个线程姗姗来迟后再一起被放行。这就产生一个问题那50个请求确实是同一秒发出去的但前面的线程等了很久会有大量的线程等待时间被计入响应时间吗答案是不会。同步定时器的等待时间不会计入Sampler的响应时间它只影响整个测试的节奏和时长。但如果你在定时器之后还有依赖前置步骤的提取器、以及后续的思考时间整个测试耗时会被拉得很长浪费压测机资源。我的建议是用同步定时器时Ramp-Up尽量设成0或一个很小的数比如1秒内让线程快速创建完毕并到达集合点这样同步的效果才真实。如果你非要模拟用户慢慢登录后同时操作的场景那可以用阶梯线程组去控制启动时间而不是用Ramp-Up。5.3 坑三聚合报告里看不出线程等待时间但你能通过时间戳定位很多人在压测结束后只看聚合报告的平均响应时间如果发现平均响应时间比平时高了就以为服务端性能不行。其实在用了同步定时器的场景里响应时间包含的是请求发出到收到响应的时间不包含等待集合的时间。如果服务器真的响应变慢了说明是并发冲击导致服务端处理不过来这个响应时间升高是有意义的。但如果某些线程在集合点等待时超时被释放此时系统实际压力小于预期响应时间可能反而低了——这就是因为超时释放导致压测结果失真。怎么定位方法很简单结合查看结果树的样本时间戳如果发现同一批次请求的起始时间戳差异比较大超过集合正常误差范围就可以怀疑是超时释放而不是真正的集合到达。5.4 坑四同步定时器对压测机自身的资源消耗同步定时器本质上是让一批线程进入阻塞等待再统一唤醒。这种阻塞-唤醒操作在系统级是重量级的尤其是当你的集合点并发数很大比如5000线程同时集合时JMeter所在机器的CPU和内存会产生一个明显的瞬时峰值。我见过一个极端的例子压测机配置一般线程组3000线程、集合数3000、超时设为0线程同时被唤醒后JMeter瞬间的内存占用和GC停顿长达数秒导致后面的请求没有及时发出压测结果出现断崖式下跌的假象。如果你要在单台施压机上集合同步大量线程请确保压测机本身有足够的CPU和内存JMeter的堆内存HEAP也要调大。一般建议在jmeter.bat或jmeter启动脚本里把HEAP-Xms4g -Xmx4g这类参数设置好别用默认的1G。分布式压测时尽量让每台机器的集合数控制在合理范围内比如单机500到1000个不要动不动就单机集合几千个线程。5.5 坑五同步定时器和事务控制器叠加导致的计时偏差如果你的压测脚本里用了事务控制器Transaction Controller来统计登录查询下单整个链路的响应时间那么同步定时器尽量不要放在事务控制器内部除非你有明确的目的。原因在于事务控制器的计时逻辑它默认会计算整个事务树内所有Sampler的执行总耗时。如果同步定时器放在事务控制器内部且它造成了线程等待那么——严格来说——同步定时器的等待时间在某些版本里会被计算进事务耗时导致你测出来的整体事务响应时间出现一个固定的大额偏移看起来像接口本身很慢其实是线程在等待其他用户到齐。正确做法是把同步定时器放在事务控制器外层的Sampler上或者放在事务控制器里第一个请求之前但要在Generate parent sample配置上做控制。更简单的建议如果你压测的是单接口完全不需要事务控制器直接加同步定时器就好如果压测的是业务流程把同步定时器放在你想制造尖峰的那个具体请求上而不是放在流程外层。6. 从集合点到容量评估一个完整的压测执行与验证思路多花一点篇幅说下压测结果的验证思路。很多人做完压测拿到报告就算交差了但我一向认为性能测试报告里缺少验证并发是否真实发生这一环报告的结论就站不住脚。我自己整理过一套验证流程每次压测完都会照着做一遍第一看服务端日志。如果服务端打印了请求接收日志可以按照时间戳精确统计每一秒接收了多少请求是否有某个毫秒出现瞬时请求暴涨。真正通过集合点施加的压力大概率会在某一个极短时间窗口内出现请求数的尖峰。第二看中间件指标。Nginx和网关的TPS曲线如果出现陡峭的柱状凸起说明流量形态是阶段脉冲式的与集合点一致。如果曲线是平滑上升的即使脚本里配置了同步定时器也可能因为超时释放等方式导致没有形成真正的尖峰。第三看数据库的慢查询日志和连接数曲线。在集合点尖峰到达的那一刻数据库活动的连接数会形成一个明显的波峰如果波峰没出现要么是中间件缓冲了流量要么是请求并没有同时到达数据库。第四反推数据。如果你用Kibana或者Prometheus监控了接口的P99和P999响应时间在集合点压力下P999通常会出现明显恶化因为同一批请求同时到达后在服务端产生排队。如果P99和P999几乎没变化你就该怀疑压力是不是真的集中到达了。讲这些是想说明一点同步定时器是制造尖峰的工具但它不是证明尖峰发生的工具。你要在压测目标系统里找到对应证据才能确认脚本有效、结果可信。这套验证思路在做云上环境迁移后的容量验证这类项目时尤其重要。比如把整套微服务从自建环境迁到云上ECS后压测人员的任务是用配套JMeter脚本做高并发测试验证云上环境的承载能力。这个过程中如果只是想当然地线程组配了个并发数就开跑没有同步定时器去制造真正的并发瞬时冲击很难验证出云上环境在高流量峰值的表现。反过来如果加了同步定时器但又没有做上述验证那么你可能得到一个系统性能不错的错误结论上线后才发现扛不住真实秒杀。做性能测试这么些年我对同步定时器的态度经历了三个阶段第一个阶段觉得它麻烦动不动就把线程等挂了第二个阶段觉得它是压测神器所有并发场景都要加现在到了第三个阶段——它只是一个工具关键是你得搞清楚自己的压测目标是什么。如果你的场景是秒杀、抢购、考试报名那就放心用它把集合数和超时时间按本文的方法配好做好结果验证如果你的场景只是评估系统日常负载下的稳定性那就别乱加让它保持简单。最后分享一个我踩过几次坑之后养成的习惯每一次压测前都在测试计划里单独加一个只有10个线程的调试用线程组先用同步定时器跑一遍最小规模的并发验证确认集合点的行为符合预期后再跑正式的大规模压测。这个调试组就像发动机点火前的盘车花三分钟能帮你省下后面排查脚本问题浪费的一上午。
返回列表