性能测试脚本编写全攻略:从设计到压测排障
在性能测试这个行当里摸爬滚打久了你会发现一个残酷的事实压测工具谁都会装LoadRunner、JMeter、Locust这些名字大家张口就来但真正决定一次压测是“走过场”还是“能定位问题”的往往不是工具本身而是你手里的那份测试脚本。很多刚接触性能测试的测试同学很容易把“编写测试脚本”理解成“用工具录制一段操作”。等真上了生产环境才发现录出来的脚本跑得比乌龟还慢或者压测时服务器直接报错查下来是脚本里带了大量无关请求把本该测的接口流量稀释得一塌糊涂。今天这篇实战笔记我就结合自己做过的几个项目把性能测试脚本编写这件事从头到尾捋一遍——脚本到底要写成什么样、每一步在干什么、常见的坑在哪里。内容不会太偏理论基本都是能直接抄作业的实操经验适合正在做接口压测、或者刚接手性能测试任务的朋友参考。1. 性能测试脚本的设计思路先搞清楚脚本在模拟什么很多新手写脚本上手就录录完就压压完出报告满屏的响应时间、TPS看着很专业但一深问就说不出所以然。根子在于没想明白一个问题性能测试脚本到底在模拟什么1.1 脚本的本质是“用户行为模型”不是“操作序列”在正式动手写脚本之前要先建立一个认知框架脚本里的每一个请求都代表真实用户的一次操作脚本的整体结构则代表业务场景中一部分用户的完整使用路径。它不是在模拟“一个人点了两次登录按钮”而是在模拟“某个时间段内有成百上千个用户同时在登录系统”这一客观事实。举个例子。你测的是一个电商系统的下单接口。如果把脚本设计成“一个线程循环调用下单接口”那压力模型就是“所有用户都在疯狂下单”这在业务上几乎不成立。真实场景里有的用户在下单前要先浏览商品有的要先把商品加入购物车还有一部分用户登录到一半就流失了。把这些比例关系融进脚本压测结果才有参考价值。所以在写第一行脚本前我的习惯是先做一件事打开系统的业务日志统计各核心操作的真实占比。比如某次项目中我发现“浏览商品详情”这个大动作占了全天流量的60%“加购”占25%“下单”只占8%剩下的都是登录、搜索、优惠券查询等。那脚本的请求配比就该大致往这个比例上靠。1.2 脚本的三个层次单接口脚本、业务链路脚本、混合场景脚本根据测试目的不同脚本设计有三个递进的层次我建议刚上手的朋友先把这三个概念区分清楚单接口脚本只针对一个接口做压力测试比如单独压测“登录接口”或“查询余额接口”。这种脚本最简单通常用于验证某个接口的性能基线或者在性能问题出现时快速定位瓶颈在哪个环节。业务链路脚本把用户的一次完整业务操作串起来比如“登录→查询商品→添加购物车→提交订单→支付”。这种脚本模拟的是用户真实路径测试结果最能反映用户的实际体验但脚本复杂度也成倍上升因为涉及参数传递、依赖关系处理。混合场景脚本把多个链路脚本按比例混合在一起模拟完整的业务流量模型。比如一个压测场景里有60%的人在看商品25%的人在加购15%的人在提交订单。这是生产环境压测最常用的一种方式也是性能脚本设计的终极形态。明确了要模拟什么要压测到哪一层再去动手写脚本思路会清晰很多。接下来我们就聊具体的脚本要素。2. 核心脚本要素拆解参数化、关联、断言、思考时间如果说脚本设计是骨架那参数化、关联、断言、思考时间这四个要素就是血肉。任何一个要素处理不好压测结果都会失真。2.1 参数化让脚本从“一个人反复用”变成“很多人一起用”参数化是性能测试脚本里最先要处理的问题。直白说就是要让每次请求的数据都不一样。很多人第一次写登录脚本时直接写死了账号密码一跑压测成百上千个并发用户全用同一个账号在登录。这样做有两个后果一是服务端如果有单账号令牌互踢逻辑后面的用户会把前面的用户顶下线TPS曲线会呈现诡异的锯齿形二是不少系统有防爆破机制同一个账号请求太频繁会直接触发验证码或者账号锁定压测很快就压不下去了。所以参数化的原则很简单凡是能变化的业务数据尽量都让它变。用户的手机号、银行卡号、订单号、商品ID全都可以作为参数化的对象。参数化的方式常见的有几种。第一种是准备一个独立的数据文件由脚本逐行读取。比如一批真实脱敏后的测试账号密码压测时每个线程取其中一行这种数据真实度高最接近生产情况。第二种是用随机函数拼接生成比如手机号用固定前缀加随机数这种方式适合批量造数但要注意随机数据可能撞库或不符合系统校验规则比如身份证校验位、地区编码用随机数容易生成一堆无效值白白浪费请求。还有一点特别容易忽略参数化数据的总量要大于脚本里并发数的至少2到3倍否则一路压到底会发现数据循环使用前后请求数据重复逻辑上跟“同一批人反复操作”没什么区别。2.2 关联把上一个请求的“返回值”变成下一个请求的“输入”关联是性能测试脚本编写中最难啃的一块也是区别“录脚本”和“写脚本”的分水岭。什么意思呢还是说电商下单。一个完整的下单流程客户端先请求一个“创建订单”接口服务端返回一个订单号然后客户端再拿这个订单号去请求“确认支付”接口。如果你录完脚本不处理直接回放你会发现订单号是写死的第二次跑这个脚本时那个订单号在系统里早就支付过了接口直接返回“订单不存在”。解决办法就是关联从创建订单接口的响应报文里用正则表达式或者JSON解析把订单号提取出来动态传给下一个请求。我在实际项目里处理关联时遇到过特别头疼的情况。有一次压测一个金融服务平台的申请流程中间有个“风控预审”环节服务端返回的数据不是固定字段而是根据用户资质动态生成的一串加密字符串长度和格式都不确定。用普通正则根本提不出来最后只能让开发同事协助确认了返回结构改用JSONPath提取才解决。所以做关联时有两个经验很重要优先从返回报文里选长度和格式都相对稳定的字段作为关联目标别去关联时间戳这类每次必然变化的值。如果响应体是JSON格式优先用JSONPath去取值比写正则靠谱得多。正则一遇到换行符、转义字符排查起来会让你绝望。2.3 断言不是压测的附庸是数据质量的守门员很多人在功能测试里熟门熟路一写性能脚本就把断言给忘了。心里想的是“反正就是看响应时间嘛服务器没报错就行”。但性能测试里响应快不等于正确。想象一下一个极端场景你压测的接口因为代码异常所有请求都返回了一段固定的错误提示这个错误提示体量极小网络传输时间短接口的响应时间测出来非常快。如果你没做断言这份压测报告的TPS和响应时间指标全是虚的。只有做了断言发现绝大部分请求的响应里根本没有包含预期的成功标识才能知道系统已经全线报错了。我做断言时通常分两层响应状态码层HTTP状态码是否为200、4xx或5xx的占比是多少。业务逻辑层返回报文里是否有业务成功标志比如success: true、状态码为0等。业务层的断言尤其关键因为有些服务从HTTP层看是200但业务层已经因为各种校验失败返回了错误码千万别让这些无效请求污染了压测数据。断言不是越多越好。断言太多会给性能压测客户端带来额外的解析开销严重时压测机本身的CPU先跑满了结果全废。我的习惯是只对关键成功标志做1到2个核心断言既保证数据有效又不给压测机太大负担。2.4 思考时间模拟人的“手速”而不是机器的“最大马力”这一步可能是新手最容易忽略的性能测试脚本要素。没有思考时间的脚本相当于让每个虚拟用户像机关枪一样不停地发请求压出来的结果是“系统能扛住多大流量冲击”的上限值而不是“真实用户使用时系统表现如何”。真实的人类用户操作是有节奏的点开一个页面要看看填完表单要想想提交之后要等页面响应。这些停顿时间就叫思考时间。如果压测目标是在线业务系统脚本里一定要加合理的思考时间。如果压测目标是单纯的数据处理接口可以考虑不加思考时间以最大吞吐量为目标。思考时间设多长合适没有标准答案最靠谱的做法还是回到第一节说的“业务日志”。查清楚两个操作之间的平均间隔时间再去设定。之前我在一个OA系统的压测中发现用户在打开待办列表后平均停留12秒才会点进详情页而另一个项目里审批操作几乎是无缝衔接。同样是办公系统思考时间差异巨大。3. 完整实操从零编写一套登录下单的链路压测脚本前面铺垫了不少思路这部分我们直接进入实操用大家最常用的场景——用户从登录到下单的完整链路——来演示脚本怎么一步步写出来并解释每一步背后的考量。工具上以JMeter为例因为它是目前社区里最普及、上手门槛也最低的开源压测工具但思路同样可以平移应用到其他工具上。3.1 环境准备与脚本结构设计在写脚本之前先准备好压测环境。域名用测试环境的地址数据库要有一批干净的测试数据尤其是商品库存、用户余额这种前置条件。如果测试数据不足或者脏数据过多压测过程中报错不断脚本调通了也是虚的。脚本结构我习惯于这样规划线程组定义并发用户数和循环次数。配置元件放公共的请求头、参数化的数据文件、HTTP请求默认值。Sampler具体的HTTP请求比如登录请求、创建订单请求、支付请求。断言对每个请求结果做校验。监听器查看压测结果并导出报告。这套结构里每个元素在后续都有对应动作。3.2 登录请求与登录态处理登录是最常见的起点。在JMeter里新增一个线程组命名为“登录下单链路”然后在下面添加配置元件“HTTP请求默认值”把协议、域名、端口填进去这样后续每个请求就不用重复填这些公共信息了。添加一个HTTP请求方法选POST路径填登录接口的地址比如/api/v1/auth/login。请求体一般是JSON格式需要传用户名和密码。在设计时我们要把用户名密码做参数化。在测试计划里添加一个配置元件“CSV Data Set Config”指向一个准备好的用户数据文件文件里每行是一组账号密码。线程组里设置10个并发用户、循环10次这样就会从数据文件里取到不同账号去执行登录。这里有一个很关键的逻辑登录接口执行后服务端会返回一个令牌或会话ID。后续的下单请求必须带着这个凭证服务端才会认为当前用户已登录。所以要在登录请求下添加一个后置处理器“JSON Extractor”从登录响应里把令牌字段提取出来命名为token然后在下单请求里通过${token}引用它。很多不熟悉会话机制的朋友在这一步会踩坑后面压测时大量请求被拦截返回401排查半天才发现是登录态没带上。3.3 POST请求体的动态拼接与业务数据关联下一步进入核心业务链路。登录成功后需要执行的下单流程通常分为两步先创建订单再支付订单。创建订单的请求路径可能是/api/v1/order/create请求体里需要商品ID、数量、收货地址等。这里就会出现参数化和关联同时用到的场景商品ID从数据文件里读取确保不同用户下单的商品尽量不重复避免并发争抢同一个商品库存的极端情况。订单号从创建订单的响应里提取作为下一步支付请求的关键参数。具体的操作是在创建订单请求下添加一个JSON提取器提取响应体里的订单编号存为变量orderId。再添加支付请求路径是/api/v1/order/pay请求体里引用${orderId}。到这一步整个链路脚本的核心逻辑就已经跑通了。但这里还有两个细节值得展开说一下处理不好会影响数据的有效性。第一个细节创建订单后可能要校验状态是否已变为“待支付”那就在请求后加一个响应断言检查返回的订单状态字段是否为预期值。如果断言失败说明该用户的本次操作是无效的后续支付请求也没必要再发了可以在断言失败后设置线程停止节省压测资源。第二个细节商品库存是有限的而压测会反复执行下单操作。库存一旦被耗尽后续请求就会报“库存不足”影响压测数据的纯度。我之前的做法是在压测开始前批量造几万条库存数据把库存上限设置得远高于压测期间的消耗总量。如果是数据库比较敏感的项目可以在每次循环时动态补充库存但这需要有数据库操作权限操作起来更麻烦一些。3.4 真实压测数据下的脚本验证脚本写完之后千万别直接上大并发开压。这里我强烈建议分三步走第一步单线程单次运行先看这条链路能不能顺利跑通响应断言是否都通过。这里重点关注的是“功能正确性”相当于验证脚本本身没问题。第二步单线程多循环运行跑个5到10次看参数化数据是否会被重复使用关联字段在多次循环中是否还能正确提取。如果用的是CSV数据文件还要确认指针是否会自动回到文件开头。第三步小规模并发验证比如10个用户并发跑2分钟观察服务端的报错日志和压测客户端的断言通过率。如果这里一切正常再正式进入预设的并发阶梯。我发现不少刚接触性能测试的朋友一上来直接就用最高并发压结果脚本本身的问题和系统瓶颈混在一起出了问题根本分不清是脚本写错了还是系统真的扛不住。脚本没有经过小规模验证压测结果就是一笔糊涂账。4. 细节打磨这些配置项决定了压测的成败边界脚本主体跑通了并不代表就可以交付压测报告了。性能测试脚本的很多坑藏在那些容易被忽略的细枝末节里。这一节我把几个影响面最大、也最常见的配置项单独拿出来讲。4.1 线程数、Ramp-Up时间与循环次数的合理组合线程组的核心配置是三件事线程数、Ramp-Up时间、循环次数。很多人直接填“1000个线程”瞬间启动这其实是暴力压测的思路模拟的是“系统突然被1000个用户同时撞进来”的冲击场景。除非你有明确的突发流量需求否则更符合真实业务的设置方式是在Ramp-Up时间内逐步把并发用户数加载起来。比如线程数设置100Ramp-Up时间设置50秒那么平均每0.5秒就有一个新用户进来。这样系统负载是慢慢上升的曲线更平滑也更接近真实用户从零开始逐渐增加的过程。同时你也可以在压测过程中观察TPS和响应时间的变化趋势判断系统性能是否稳定。循环次数方面如果设置为“无限”那就必须勾选“调度器”设置持续时间。跑固定时间比跑固定次数更容易控制压测节奏也方便在压测中途调整并发。我在大部分压测中都倾向于设置为时长比如持续压测15分钟以保证数据量足够进行后续分析。4.2 超时设置与连接复用默认情况下JMeter的HTTP请求超时时间可能是无限等待。在生产环境压测时一旦服务端出现阻塞请求会一直挂着线程得不到释放新的请求进不来最终压力全部堆在压测客户端本地测试结果惨不忍睹。我的习惯是给每个HTTP请求做如下超时设置连接超时设为3000毫秒响应超时设为5000毫秒。如果服务端响应超过这个时间说明接口基本上已经处于不健康状态了让事务快速失败并记录错误反而比无限等待更有分析价值。连接复用这块也值得关注。压测过程中如果每个请求都新建一条TCP连接连接建立的握手开销会占据大量成本。JMeter默认使用HTTP连接池可以复用连接但要在“HTTP请求默认值”中勾选正确的连接设置。如果被测系统的服务端配置了长连接上限过高的并发会导致连接排队这时候就要考虑调整连接超时或者压测机的连接数限制。4.3 集合点要不要用怎么用集合点是从负载测试里传下来的一个概念简单说就是所有虚拟用户在同一个时刻一起触发请求模拟“瞬间爆发”的流量比如秒杀场景。JMeter里通过“定时器”中的“同步定时器”来实现用来保证并发线程组中的用户在同一时刻起跑。但很多项目里集合点是被误用的。就拿常见的登录压测来说如果每个线程组里的用户都设置了一个同步定时器所有用户在压测开始时会同时发起登录请求。这在真实业务场景里很少见因为真实用户登录是有先后顺序的。同时刷新导致的结果是服务端瞬间被打满而后续每个用户的操作却错开了这样得到的数据并不具有普适性。那什么时候该用集合点比如一个抢购活动预定在10点整开始大量用户在10点整同时点抢购按钮这种场景是可以用集合点的。如果用不到这类业务场景建议不要随便加加了会让压测请求分布极度不均匀对整体TPS的影响也非常明显。4.4 压测机的性能监控这一条算是一个经验教训。早期我做压测时只盯着被测系统的服务器指标压到高并发时发现TPS上不去了以为是被测系统到了瓶颈。排查半天发现是压测机的CPU已经100%网络带宽也满了请求全堵在本地压测结果完全没有参考意义。所以压测脚本调好之后务必同步监控压测机自身的资源使用情况。尤其是用JMeter做高并发压测时单台压测机的线程数不建议开得过高通常超过1000个线程时就要考虑分布式压测了。JMeter本身是基于Java的在高并发下线程调度的开销比较大如果你要压的量级超过单机能力不要硬撑直接把压测任务拆到多台施压机上去再汇总结果。5. 常见问题与排查技巧实录前面几节基本把脚本编写的完整过程讲透了。最后这部分我整理几个自己项目里真实遇到过的、很有代表性的脚本问题按症状、原因、排查路径、解决办法四段式来写方便你在自己的压测中快速对照。问题现象常见原因排查方法解决方案压测开始后大量请求报401/403登录态没有正确传递查看登录响应提取的token变量是否为空打印请求头内容检查JSON提取器表达式确认参数名引用与请求头命名一致TPS曲线呈锯齿状周期性跌到0参数化账号互踢或验证码触发查看服务端日志确认是否有账号冲突记录增加账号池规模为每个线程分配独立账号并禁止循环复用响应成功率很高但业务失败率也高断言只检查HTTP状态码未检查业务码对比响应报文中的业务状态字段与实际数据表记录增加业务级断言校验业务成功状态码压测客户端CPU先跑到100%线程数开得过大压测机资源耗尽查看施压机CPU、内存、网络带宽使用分布式压测或降低单机线程数/改用吞吐量控制的压测模式压测过程中响应时间越来越长脚本循环次数过多数据增长导致接口查询变慢查看数据库扫描行数、锁等待状态必要时定时清理压测产生的脏数据或减少循环中重复查询动作关联提取的字段有时有值有时为空响应报文中的字段在某些条件下不存在打印响应报文分析空值出现的触发条件扩展关联提取器的默认值并对空值数据进行单独断言排除除了表格里这些还有一个几乎每次都遇到的小坑压力测试跑完以后数据库里堆了一堆测试数据。比如下单链路脚本跑了一小时订单表里多了几十万条垃圾订单待支付、已支付、已取消的都有。这些垃圾数据会让下一次压测的环境不再“干净”严重的会拖慢后续测试环境的接口响应速度。所以一定要在压测结束后执行数据清理动作或者把压测环境设计成可随时回滚的快照模式。还有一个非常隐蔽但影响巨大的点就是“压力模型失真”。很多脚本在编写时开发给的接口文档只包含URL和参数格式根本不会告诉你哪个接口在内部调了别的服务、哪个接口有缓存策略。这种“文档和实现不一致”的情况会让脚本里的请求实际走了完全不同的调用链。我之前遇到过查询类接口文档写的是查数据库压测时才发现加了Redis缓存前几次请求慢后面请求全走缓存TPS数据一直在逐步上涨。如果不看服务端的链路追踪日志很容易误判系统的真实处理能力。所以脚本跑通后建议先看一次服务端的链路追踪如果有的话确认请求确实走到了预期的业务逻辑分支再来开压。6. 压测前的最后自检清单脚本编写接近尾声最后再用一份自检清单来兜底。这算是我自己每次压测前都会过一遍的操作清单防止低级失误在正式压测时爆发。提示以下自检项建议在每次压测前一天完成压测当天再改脚本是大忌。参数化数据是否充足数据总量是否大于并发数乘循环数的需求总量数据格式是否符合业务校验逻辑。关联字段是否稳定所有提取器都试跑过多次循环确认没有空值或错值。断言是否有效用失败场景验证过断言是否会拦截请求避免断言写歪了全都通过却没有实际拦截能力。思考时间是否符合业务实际数值是在日志统计基础上设定的不是拍脑袋写的。超时设置是否合理连接超时和响应超时都配置了明确数值没有无限等待。脚本是否经过小规模验证从单线程到小并发逐步验证过断言通过率接近100%。压测机资源是否充足CPU、内存、带宽满足本次压测需求必要时已规划好分布式压测方案。测试数据是否提前造好库存、账号、业务前置数据准备齐全。压测后数据清理方案是否明确脚本结束动作或数据库清理脚本都已准备好。服务端监控是否就绪被压服务器的CPU、内存、磁盘、网络监控已确认开启。这套清单看起来琐碎但每一条都在项目里真实换过教训。尤其是最后一条服务端监控我见过太多人脚本写得漂亮压测时却忘了开被压服务器的监控压测一结束所有性能数据只有接口响应时间没有系统资源数据连最基本的瓶颈分析都做不了。性能和脚本之间的对应关系瞬间就断了。脚本调到这里该准备的都准备好了。剩下的就是启动压测、收集数据、分析报告那些是另一个话题了。最后再分享一个小体会性能测试脚本的编写本质上是一个“理解业务、表达业务、校验业务”的过程。当你拿到一个接口能立刻想到它的真实用户群体是什么样的操作习惯这个脚本就已经成功了一大半。