
接手这个商城项目的性能测试说实话一开始我是有点心里打鼓的。上个月线上刚出过一次事故活动预热刚开始半小时首页接口的响应时间从平时300ms直接飙到3秒用户点加购按钮转圈半天没反应订单量肉眼可见往下掉。老板复盘的时候拍板性能测试必须系统化落地这个任务就落到了我头上。这个系列我会完整记录整个过程从项目背景拆解、测试场景建模到JMeter脚本落地、压测执行再到最后的瓶颈定位与调优验证。如果你是刚转测试没多久、第一次被迫上压测的临时选手或者正在为面试准备性能测试项目经验这篇内容应该能帮你少走不少弯路。我先说明一点这套操作不是教科书式流程而是我在真实业务里反复折腾出来的思路。教科书会告诉你性能测试有负载测试、压力测试、稳定性测试、容量测试这些分类当然没错但到了具体项目里最难的不是区分这些名词而是回答三个问题被测系统长什么样、业务链路哪条最重要、测到什么程度算达标。这三个问题不解决后面所有工具操作都是空中楼阁。1. 商城项目到底测什么先看清系统和业务再动手1.1 我接到的是什么样的商城这个商城不是那种简单的演示项目而是一套真实在运行的电商系统包含用户端H5、用户端小程序、管理后台三块。用户端核心功能有首页商品推荐、商品详情、购物车、订单确认、支付下单、个人中心这几个模块管理后台则是商品管理、库存管理、订单处理和营销配置。技术上整体是前后端分离架构前端部署在CDN和Nginx上后端按微服务拆分核心服务包括用户服务、商品服务、订单服务、库存服务、支付服务和营销服务。数据层用了MySQL存业务主数据Redis扛缓存和分布式会话另外还有一套MQ做订单创建和库存扣减的异步解耦。这里有个关键点性能测试不是对整个系统无差别打压而是要顺着真实用户的业务路径去打。所以我拿到项目的第一件事不是打开JMeter乱压一通而是先梳理清楚这个商城的核心交易链路。1.2 先回答三个问题再设计测试我在给团队做测试方案评审时反复强调一个观点性能测试的需求方不是测试经理而是业务方。所以测试启动前的需求澄清会非常关键。我和产品、研发、运维拉了一次会确认了三件事预期并发规模运营给的预估是活动高峰期同时在线用户数约5万但这5万不会同一秒全部点进来。通过日常埋点数据反推核心接口的峰值QPS大约在2000到3000之间。核心业务链路从用户浏览首页开始到查看商品详情、加购物车、确认订单、提交支付再到支付回调这是完整的下单交易链路。其中商品详情和提交订单接口是整个系统的咽喉。性能底线业务方给出的指标是核心接口TP99响应时间不超过1000ms95线不超过500ms压测期间不能出现订单丢失或库存超卖。这三个问题定了测试范围才真正清晰。后续所有脚本设计、场景比例、指标阈值都是围绕这三个答案展开的。如果一开始不把这些确认清楚测试做到一半再改方向那才是真正的时间黑洞。2. 全链路场景拆解哪些接口必须压哪些可以先放2.1 从首页到支付回调核心链路其实只有六跳这个商城看起来功能很多但真正决定交易成败的接口就那么几个。我在设计场景时把用户行为拆成了下面这张接口清单接口模块核心接口请求方式是否核心链路说明用户模块登录、获取用户信息POST/GET是依赖度高几乎每个请求都带商品模块首页推荐、商品详情、搜索GET是流量入口点击量最大购物车加购、购物车列表POST/GET是用户行为密集订单模块确认订单、提交订单POST是核心中的核心涉及库存扣减支付模块发起支付、支付回调POST是涉及第三方对接敏感度高营销模块秒杀/优惠券领取GET/POST否本次活动可选压我在实际压测时把营销模块的秒杀接口先放了一放因为这次活动的秒杀是独立的流量入口而且和主交易链路耦合度相对低。不是说它不重要而是性能测试要分优先级一步一步来一次想把所有接口都压明白最后往往一个都没压透。2.2 场景比例是从用户行为数据里来的不是拍脑袋很多测试同学喜欢按经验分配场景比例比如登录10%、加购20%、下单10%然后就把脚本跑起来。但真正的业务比例应该来自真实用户行为分析如果商城没有埋点平台至少也要从后端日志里统计接口的调用次数占比。我们当时的做法是从Nginx访问日志里拉取了过去一个月的接口调用数据统计出各类接口的占比浏览类接口首页推荐 商品详情 搜索约占65%用户操作类登录 购物车增删查约占25%交易类订单确认 提交订单 支付约占10%这组数据直接决定了我压测脚本里每个事务的并发占比。比如我把全链路场景跑起来时浏览类请求的并发线程数是交易类的好几倍而不是平均分配。这个细节对压测结果的可信度影响巨大因为如果你的脚本里下单占比比真实业务高压出来的系统瓶颈很可能是被放大或扭曲的。2.3 压测数据准备是整个项目最脏最累的活场景拆解完了紧接着就是准备测试数据。很多人压测跑出来的报告不对问题往往出在数据上而不是工具上。我这次踩了一个比较经典的坑商品库存不足导致下单失败。库存是放在Redis里的压测脚本里下单的商品ID用的是同一个SKU并发跑起来以后这个SKU的库存很快被扣光后续所有下单请求全部返回库存不足。聚合报告里的错误率立刻飙升我被吓了一跳还以为是代码有Bug最后定位发现是测试数据本身有问题。正确做法是准备多组商品数据每组SKU的库存量远大于压测总量并且在脚本里对商品ID做参数化让请求均匀分布到不同的商品上。同时要保证测试库里的用户数据、商品数据、订单数据量级和生产环境基本一致比如用户表至少几十万条商品表几万条。如果测试库就几千个用户那数据库索引优化不优化根本测不出来因为全表扫描在几千条数据上也很快。3. 性能指标在商城场景里的落地解读别被术语唬住3.1 响应时间的二五八定律在我们项目里怎么定大家常听到响应时间有个二五八定律200ms以内用户基本无感500ms以内可以接受超过800ms用户就会明显感觉到卡顿。这其实是一个比较泛化的经验值真实项目里要结合业务场景调整。比如商品详情页属于高频浏览型接口用户对延迟比较敏感我们的目标定在TP99小于500msTP95小于300ms。而提交订单属于低频强操作型接口用户按了提交按钮以后会有一个预期的等待周期所以TP99可以放宽到1000ms以内。这背后其实是用户心理学不同操作对延迟的容忍度完全不同。还有一个关键问题TP99比平均响应时间重要得多。平均响应时间最容易被极端值掩盖比如100个请求里99个是100ms1个是10秒平均下来也就200ms左右看起来很快但实际上已经有1%的用户在经历灾难级的体验大促场景下这1%可能就是几千上万个订单损失。3.2 TPS和并发用户数这两个概念最容易搞反并发用户数和TPS每秒事务数是完全不同的两个概念但我在面试候选人时发现至少有一半的人分不清。简单说并发用户数是系统同时容纳的在线操作人数TPS是系统每秒能处理的事务数量。两者的关系可以用一个公式表示TPS 并发用户数 / 业务平均响应时间秒举个例子如果系统有1000个并发用户每个用户完成一次加购操作平均需要0.5秒那么加购接口的TPS理论上限就是1000 / 0.5 2000。当然这是理论值实际会受网络带宽、数据库锁、连接池等瓶颈影响通常达不到这个上限。所以我在设计压测场景时不是直接设置1000个线程就完事而是先按目标TPS倒推核心接口目标TPS是2000平均响应时间假设500ms那至少需要负载机的并发线程数就是2000 乘以 0.5 1000。这个推算过程能帮你校验测试工具的施压能力够不够也能帮你理解压测结果的合理性。3.3 错误率的口径要统一这个细节很坑错误率的统计口径在不同工具里不太一样JMeter默认以Assertion断言结果来判定请求是否成功而LoadRunner是看HTTP状态码和事务定义。这个差异在实际工做中很坑。比如一个接口响应了HTTP 200状态码但业务实际返回的是库存不足或用户未登录这种情况下LoadRunner默认会记为成功而JMeter如果没写断言也会记为成功结果就是错误率被严重低估。我在脚本里给关键业务接口都加了响应体内容断言比如提交订单成功后会返回订单号字段我就断言响应体中包含这个字段。这样业务失败但HTTP成功的情况才能被正确识别出来。另一个口径问题是超时算不算错误。像JDBC Connection Timeout、Socket Timeout这类异常JMeter会算进错误中HTTP层面的504网关超时也会算。我以为跟开发对齐超时判定标准也很重要否则你报一个5%错误率开发说请求不是返回200了吗两边就从技术讨论变成了扯皮现场。4. JMeter脚本落地关联、参数化和断言一个都不能少4.1 登录态的Cookie与Token处理商城用的是前后端分离架构用户登录后后端会返回一个Token后续带Token的请求会被识别为登录态。而H5端还有一个前置登录流程有的接口需要先通过登录接口获取Cookie和Token。这时候脚本里必须做关联。具体的做法是先跑一次登录请求然后用JSON Extractor从登录响应中提取token字段再把这个token通过Beanshell或JSR223脚本写入到后续请求的Header中。这里有个使用JMeter的常见误区很多人直接用JMeter自带的HTTP Cookie Manager以为只要加了这个组件就能自动维护Cookie。对于纯Cookie会话的登录是够用的但Token场景下Cookie Manager并不会自动提取和传递业务Token必须手动做关联。我做了一个自定义的JSR223 PostProcessor在登录请求结束后提取Token存到vars变量里然后在后续请求的HTTP Header Manager里引用。// JSR223 PostProcessor放在登录请求下面 import org.json.JSONObject; String response prev.getResponseDataAsString(); JSONObject obj new JSONObject(response); String token obj.getJSONObject(data).getString(token); vars.put(auth_token, token);4.2 商品ID和用户ID的参数化策略参数化这块我吃过亏刚开始图省事直接用一个CSV文件里面放几十个用户在循环跑。跑了一段时间发现数据库连接数和QPS都比预期低很多最后定位发现是因为用户太少Redis里那个用户的缓存一直被命中完全没打到数据库压测结果根本不能反映真实情况。所以参数化的核心目标是让测试数据尽量接近真实分布。我当时做了两层参数化用户参数化CSV文件里准备了5000个测试用户按用户ID|密码|昵称格式排列线程组用CSV Data Set Config按行读取。不同的线程取到不同的用户模拟不同用户同时在线。商品参数化从数据库里查出一批真实商品ID写入另一个CSV加购和下单时从里面随机取一个值避免所有请求打同一个商品。这里要注意一个JMeter使用技巧线程组的循环次数和CSV文件的条数要匹配好。如果你有5000条用户数据但压测计划是100个线程循环100次那么每线程会取到5条数据这是正常的但如果CSV只有10条数据循环100次数据就会重头再来一遍这时要勾选CSV Data Set Config里的Recycle on EOF选项并处理好Stop thread on EOF的勾选状态否则线程跑到文件末尾可能直接报错停止。4.3 断言怎么设置才能在误报和漏报之间找平衡断言设置太松业务失败被当成成功性能报告失真断言太严一点非关键字段波动就报错又会产生大量无效错误拖慢压测。我在商城项目里是分级设计的基础断言所有接口HTTP响应码为200用Response Assertion加Response Code 200。同时加上Response Message OK防止代理层返回200但页面是错误页。业务断言核心交易接口提交订单成功后判断响应是否包含orderId支付回调接口判断是否包含success。这类业务字段的关键在于选返回值稳定且唯一的标识千万不要去判断成功这种通用词因为提示文案改一个字就会大量误报。超时断言用Duration Assertion给核心接口设置最大耗时阈值比如提交订单不能超过3秒。超过即失败这样能把慢请求直接标记出来方便后续做耗时分布分析。提示压测场景里的断言不是越多越好每个断言都会消耗额外的处理器资源。给所有接口只做HTTP断言给核心业务链路加上业务断言和耗时断言这是性价比最高的配置方式。5. 压测执行与企业监控边压边看才能发现真问题5.1 单机压测跑满了负载机那就上分布式执行压测之前要先算一下负载机够不够用。JMeter的线程本身会消耗一定CPU和内存当你把并发线程开到800以上单台负载机频繁出现GC停顿这时候测试结果里会混入负载机自身性能瓶颈的干扰。我这次的目标TPS 2000到3000并发线程需要1000以上于是用了3台JMeter负载机来做分布式压测。分布式压测有个容易踩的坑在Windows上启动jmeter-server被防火墙拦截导致Agent连接不上。如果是在云上压测还要注意JMeter Server和Master之间要开放特定端口默认是1099加上RMI动态端口。更稳妥的做法是直接用SSH隧道或者干脆用Docker方式部署JMeter Agent避免RMI端口问题。分布式模式下还有一个细节各Agent的取样结果会汇集到Master汇总如果Master内存不足聚合报告可能丢失部分数据。我的做法是控制每个Agent上的线程数不超过500并且把modeStandard改成modeStrippedBatch或modeStatistical减少回传数据量实测数据完整性明显提升。5.2 服务器监控要盯三个维度少盯一个都容易误判压测执行过程中我不只是盯JMeter的聚合报告面板更重要的是盯后端的服务器监控。我总结了三个必须盯的维度一是应用层的线程池和连接池状态。Tomcat线程池如果活跃线程数接近最大值说明应用处理不过来数据库连接池如果活跃连接数持续高位说明数据库成为瓶颈。当时我观察到订单服务活跃线程数从50涨到200而且一直降不下来这就是线程堆积的信号同时数据库连接池也被占满说明瓶颈在数据库这一层。二是中间件的关键指标。Redis的命中率如果低于80%说明缓存策略可能有问题大量请求穿透到数据库MQ的堆积数量如果持续增长说明消费端处理速度跟不上生产端会导致订单状态延迟更新。三是系统资源层面的指标。CPU、内存、磁盘I/O、网络带宽。这里有个常见误区CPU高不一定代表有瓶颈CPU高可能只是业务计算密集关键是看CPU是否被打满且伴随响应时间上涨反过来CPU没满但TPS上不去往往是锁、连接池、队列这些并发控制点在卡脖子这是性能分析中最值得注意的一种情况。5.3 不要上来就跑峰值阶梯加压找拐点很多测试新手喜欢一上来就把并发拉到最高跑出个峰值TPS很高兴但这个数字没有太大价值因为你不知道系统的极限到底在哪里也没有办法告诉开发你要优化到什么程度。更实用的做法是阶梯加压从100并发开始每次增加50或100每个梯度的持续跑2到3分钟观察TPS、响应时间、错误率的变化。比如我当时跑出的拐点曲线大致是并发200时TPS 800响应时间150ms并发400时TPS 1500响应时间280ms并发600时TPS 1900响应时间480ms并发800时TPS 2100但响应时间直接飙到了1200ms。从这个数据能明确看出系统的性能拐点在并发600左右再往上加并发TPS的增幅明显放缓而响应时间急剧恶化。这个拐点就是系统容量规划和限流降级策略的重要依据。运营那边的活动预热方案就是根据这个拐点设计了削峰填谷的排队策略。6. 一次真实瓶颈定位TPS上不去但CPU不满问题到底卡在哪6.1 现象拐点之后TPS停滞但服务器资源像没吃饱在阶梯加压的第二轮优化验证中我把订单服务的并发线程从400提升到800预期TPS能翻倍但奇怪的是TPS牢牢卡在2100左右再也上不去而订单服务所在的4核8G机器CPU只有45%数据库服务器CPU也才60%。资源明明没用满为什么TPS上不去这个现象用大白话说就是系统饿住了不是累住了。就像一条流水线工位上的工人CPU并没有全部在干活但产品请求就是流不下去一定有什么地方在限流。6.2 排查链路连接池、锁、线程状态逐个看我按从外到内的顺序排查第一步看网络层确认带宽没有打满内网延迟正常。排除网络问题后第二步看数据库用show processlist查看数据库连接和慢查询。结果发现大量会话状态是Waiting for table metadata lock和Sleep看起来并不算慢但连接数已经逼近上限。再看慢查询日志发现一条订单表的INSERT语句平均执行时间从5ms涨到了80ms。第三步重点查数据库连接池参数。订单服务用的是HikariCP连接池配置的maximum-pool-size是50。我用SHOW STATUS LIKE Threads_connected一看连接数稳定在50不涨了。这时候基本能断定问题出在连接池被占满。再往下追一层查了下订单服务日志发现了大量HikariPool-1 - Connection is not available, request timed out after 30000ms。到这里根因已经很明确连接池上限太小加上每个事务持有连接的时间太长导致连接被借光申请连接的请求排队超时。6.3 验证与调优效果确认根因后我和开发做了一次数据库连接池参数调优maximum-pool-size从50调到150connection-timeout从30000ms缩短到5000ms让等不到连接的请求快速失败而不是长时间排队minimum-idle从10调到30避免连接池在高并发下重新建连的抖动顺手优化了INSERT语句把单条插入改为批量插入ap日事务提交的持有连接时间降了下来调整后重新跑同样的阶梯加压拐点从并发600后移到并发1100最高TPS从2100提升到3200并且TP99稳定在600ms以内。这个提升幅度不算夸张但完全验证了连接池被耗尽导致系统饥饿的推断。这个案例让我印象很深因为它提醒了我一个思路性能优化的优先级不是看哪里代码写得慢而是先找哪里把并发卡死了。像连接池大小、线程池大小、队列容量这些并发上限参数往往比单个接口的SQL优化更能决定整体吞吐量。7. 压测报告里最容易被打回的几个参数建议你提前避坑7.1 别只给一张聚合报告截图要有对比我第一次给业务方看压测报告时直接丢过去一张JMeter聚合报告截图结果被产品问了一句所以这说明了什么当场哑住。正确的做法是通过对比来体现性能状态。我后来把报告整理成了三组对比调优前与调优后的核心指标对比、目标值与实测值的差距对比、不同并发梯度下TPS和响应时间的变化趋势对比。每张表格下面加一段结论性描述比如500并发下TP99超时时间是目标值的2倍主要瓶颈为数据库连接池耗尽这样一句人话而不是只有一堆数字。7.2 吞吐量数据要标注数据口径报告里最容易引发争议的是TPS到底是多少。因为TPS的统计口径有几种按HTTP请求数统计的TPS、按业务事务统计的TPS、按包含重试和额外接口的TPS。我在JMeter里通过Transaction Controller来统一定义一次完整交易的范围从点击提交订单到收到订单ID的整个交互算一个事务而不是散落地统计每个HTTP请求的独自TPS。报告里每条TPS数据后面我都会标注本TPS统计口径为完整交易事务不含登录态获取请求这样一句注释。别嫌麻烦这句注释可能帮你省掉一次跨部门沟通会。7.3 性能调优建议要给人能动手的方案报告最后都会写优化建议但我见太多报告写的是建议优化接口性能建议排查慢SQL这种建议等于没写。我在报告里的每个问题后面都会给出可落地的调整参数比如问题数据库连接池上限过低导致高并发下请求等待。建议将HikariCP的maximum-pool-size上调至150connection-timeout下调至5000ms需结合数据库实例规格验证。问题商品列表接口未使用缓存导致数据库压力过大。建议对热点商品列表结果做缓存缓存过期时间120s使用Redis的List结构存储商品摘要数据。这类建议研发看完能直接排期估算不用再拉一轮会去掰扯怎么改的问题。8. 写在收尾性能测试这件事真正值钱的是判断力跑完整个商城项目的性能测试我最深的感受是JMeter的操作、指标的理解、参数的含义这些网上教程到处都有一个月就能学会。真正值钱的是你拿到一堆数据之后能不能看出系统的问题在哪以及敢不敢给出就是这个参数要调的判断。这种判断力没有捷径只能靠一轮一轮在真实项目里踩坑、定位、验证。如果你正准备在自己的项目里落地性能测试我给你三个务实建议第一先花两天时间把业务和系统架构摸透这比学任何工具都值得第二第一次压测不要追求峰值跑一轮阶梯加压摸清拐点就够用了第三压测过程中一定要同步盯服务器监控只看JMeter面板你永远找不到根因。商城项目这个系列的后续我会继续写详细的JMeter脚本组件设计、分布式压测搭建和瓶颈定位实战欢迎继续关注。