性能测试入门——为什么你的接口跑压测和真实场景差距这么大
性能测试入门——为什么你的接口跑压测和真实场景差距这么大
适读人群:后端开发工程师、测试工程师、技术团队负责人 | 阅读时长:约14分钟 | 核心价值:彻底搞懂压测数据失真的根本原因,建立正确的性能测试心智模型
那个让我印象最深刻的生产故障
2019年双十一前两周,我们团队做了一次全量接口压测,结果非常好看:核心下单接口在200并发下TPS稳定在1800,P99响应时间148ms,错误率0。
所有人松了一口气,Leader说"没问题了,上线吧"。
11月11日零点,活动开始后8分钟,订单服务集体超时。
告警大面积涌入,Grafana上的P99曲线像火箭一样飞上去——从120ms直接冲到8400ms,TPS从1800骤降到60出头,大量请求开始返回500。
那一夜我们排查到凌晨4点。最后定位到三个原因:
第一,压测时我们用的是固定的10个测试用户ID,这10个用户的数据常驻在MySQL的Buffer Pool里,根本没有触发真实的磁盘IO。真实流量里有几十万用户ID,缓存命中率从98%跌到23%,查询时间从1ms涨到60ms。
第二,压测环境的数据库只有50万条订单记录,生产是2.8亿条。索引在小数据量下跑得很漂亮,数据量大了之后B+树层数增加,索引效率直接下降。
第三,我们忽略了购物车服务和库存服务的联动。压测只打了下单接口,但真实下单要调用库存扣减,库存服务在高并发时有锁竞争,我们的压测压根没覆盖这条链路。
这次事故让我彻底理解了一件事:压测结果好看,不代表系统真的能扛住生产流量。 压测和真实场景之间的差距,背后有明确的技术原因,不是玄学。
为什么压测数据会失真
1. 数据规模差异
这是最常见也最容易被忽视的原因。
压测环境的数据库通常只有几十万条记录,生产可能是几亿条。表面上看索引都建了,SQL执行计划也是走索引,但数据量不同时,MySQL优化器的选择、Buffer Pool的命中率、IO读取量都会有本质差异。
一个真实的例子:同一条SELECT * FROM orders WHERE user_id = ? AND status = 1 ORDER BY create_time DESC LIMIT 20,在50万条数据时执行时间是0.8ms,在2亿条数据时执行时间是47ms。差距接近60倍,而这条SQL在压测环境里看起来完全正常。
解法: 压测数据库至少要达到生产数据量的30%以上。如果做不到,至少要用生产数据的随机抽样,保证数据分布与生产一致。
2. 热点数据与缓存命中率
压测脚本里如果使用固定的参数值(比如固定用户ID、固定商品ID),这些数据在压测开始后几秒内就会全部进入Redis缓存和MySQL Buffer Pool。
后续所有请求直接命中缓存,响应时间极短,TPS高得离谱。但真实流量里用户ID是随机分布的,缓存冷启动、缓存失效、缓存穿透这些问题在压测里根本不会出现。
我见过一个案例:商品详情接口压测TPS达到12000,P99是8ms。上线之后真实流量TPS只有3000,P99跑到340ms。原因就是压测只用了100个商品ID,这100个商品的数据根本没离开过缓存。
解法: 压测参数必须用随机化、足量的测试数据。用户ID至少准备生产用户量的10%,商品ID覆盖主流量商品。JMeter用CSV Data Set Config参数化,k6用共享数组随机取。
3. 链路不完整
很多团队做压测喜欢只压核心接口,不管上下游依赖。这样压出来的数据完全不反映真实情况。
一个下单接口,内部可能调用了:
- 用户服务(查询用户信息)
- 商品服务(查询商品价格)
- 库存服务(扣减库存,有分布式锁)
- 优惠券服务(核销优惠券)
- 风控服务(请求三方风控)
如果只压下单接口,但Mock掉了所有下游,压测数据完全是假的。真实流量里库存锁竞争、优惠券并发核销、风控服务的响应时间,都会成为瓶颈。
解法: 核心链路要做全链路压测,不能Mock关键依赖。至少要把有状态操作(写库存、写DB、调用三方)都纳入压测链路。
4. 思考时间(Think Time)缺失
真实用户不会在拿到上一个请求的响应之后立刻发下一个请求。他们要看页面、填表单、做选择,平均间隔可能是2-10秒。
而压测脚本默认是收到响应立即发下一个请求,这导致压测时每个线程的RPS远高于真实用户。
举个数字:假设你的测试用1000个线程,平均响应时间200ms,没有Think Time,每秒TPS是5000。但真实1000个并发用户,每人有3秒Think Time,实际TPS是250。差了20倍。
这种差距会导致你以为系统能扛5000 TPS,实际上真实场景下1000真实用户就能把系统压垮。
解法: 根据业务日志统计真实用户的操作间隔时间,在压测脚本里加入相应的Think Time。JMeter用Constant Timer或Gaussian Random Timer,k6用sleep()。
5. 连接池与线程池预热
压测一开始,JVM是冷的,连接池没有建立连接,各种缓存都是空的。真实流量来临时,服务通常已经运行了一段时间,各种连接和缓存都是热的。
如果压测从第1秒开始计数,这段冷启动期的数据会把整体指标拉低,不能真实反映系统的稳态性能。
解法: 压测开始后前1-2分钟的数据不计入最终结果,等系统进入稳定状态后再开始统计。JMeter可以用Ramp-Up Period让线程逐步加到目标数,避免瞬间冲击。
性能测试的正确认知框架
理解了失真原因,我们来建立一个正确的性能测试认知框架。
四种核心测试类型
基准测试(Baseline) 单线程或极低并发,测接口的原始响应时间。比如1个线程跑100次,统计P50/P95/P99。这个数据是后续所有分析的基础——如果基准就很慢,说明代码本身有问题,跟并发无关。
负载测试(Load Test) 模拟预期的正常业务量,持续运行一段时间(通常30分钟-2小时),验证系统在正常负载下能否稳定运行,关注指标是TPS稳定性、P99变化趋势、错误率。
压力测试(Stress Test) 逐步加大并发,找到系统的性能拐点和最大承载能力。当TPS开始下降、P99开始发散、错误率开始上升时,前一个负载级别就是系统的上限。
稳定性测试(Soak Test) 用正常负载持续压测4-24小时,主要检查内存泄漏、连接泄漏、慢慢积累的超时等长时间运行才会暴露的问题。很多团队跳过这个,结果服务上线后跑几天就OOM。
性能测试指标怎么看
不要只看平均响应时间,这个指标非常有迷惑性。
假设有100个请求,99个都在10ms内完成,1个花了10000ms,平均值是109ms。听起来还不错?但那1%的用户等了10秒。
正确的方式是看百分位数:
- P50(中位数):50%的请求在这个时间内完成,代表典型用户体验
- P95:95%的请求的响应时间上限,大部分用户的最差体验
- P99:99%的请求的响应时间上限,SLA通常基于这个指标
- P999:1000个请求里最慢那1个,用来发现极端慢请求
一般来说,接口SLA的定义方式是:P99 < 500ms,P999 < 2000ms,TPS > 1000,错误率 < 0.1%。
压测环境的最低要求
如果你的压测环境和生产差距太大,测出来的数据没有任何参考价值。
最低要求:
- 数据库数据量达到生产的30%,且数据分布与生产一致
- JVM堆大小、线程池配置、连接池配置与生产一致
- 测试数据参数化,不用固定ID
- 网络环境接近生产(不要在本地开发机上压生产数据库)
- 压测机与被压测服务之间的网络延迟 < 1ms
踩坑实录
坑1:Ramp-Up设太短导致误报瓶颈
现象: 200线程并发压测,TPS在前30秒内持续在200左右,远低于预期的1500。测试同学报告"系统性能有严重问题"。
原因: JMeter的Ramp-Up Period设置了30秒,200线程在30秒内逐步启动,平均每秒启动6.7个线程。前30秒线程数根本没达到200,TPS当然低。
解法: Ramp-Up设置要根据实际需求来。如果模拟突增流量,设短一点(5-10秒)。如果模拟正常增长,设1-2分钟。压测统计时间从Ramp-Up结束后才开始计算。
坑2:数据库连接池耗尽不报错
现象: 压测时TPS稳定在800,但监控里发现应用服务器CPU只有15%,数据库CPU也只有20%,明显还有余量,但TPS就是上不去。
原因: 数据库连接池上限设的是10,所有线程都在等待连接。HikariCP默认等待超时是30秒,所以没有立刻报错,而是大家都在排队,导致TPS上不去但也不超时。
解法: 压测时要同时监控连接池的active/idle/pending指标。HikariCP可以通过JMX或metrics端点暴露,一旦pending数量大于0就说明连接池是瓶颈,需要调大maximumPoolSize。
坑3:JVM GC在压测中期爆发
现象: 压测进行到第18分钟时,P99突然从180ms跳到2300ms,持续约40秒后恢复正常。重复三次,每次间隔大约18分钟。
原因: JVM堆设置了8G,但Xmn(新生代)只设了512M。每隔一段时间触发Full GC,Stop-The-World暂停了40秒。
解法: 压测期间必须监控JVM GC日志。-Xloggc:/tmp/gc.log -XX:+PrintGCDetails。新生代大小一般设为堆的1/3到1/2,大对象直接进老年代的阈值也要合理设置。
一个最简单的正确压测流程
说了这么多理论,给一个实际可操作的流程:
第一步:准备测试数据
- 准备10万+随机用户ID,写入CSV文件
- 准备1万+随机商品/资源ID
- 确认测试DB数据量接近生产
第二步:基准测试(1个线程,持续5分钟)
- 记录P50/P95/P99,这是你的基线
第三步:负载测试(目标并发,持续30分钟)
- 前5分钟不计入统计(预热)
- 监控TPS、P99、错误率、CPU、内存、GC、连接池
第四步:压力测试(阶梯加压)
- 每5分钟增加50并发
- 找到TPS拐点
第五步:写结果报告
- 对比基准、负载测试、压力测试三组数据
- 标注瓶颈点和建议总结
压测和真实场景差距大,核心原因就这几个:测试数据不真实、链路不完整、缺少Think Time、环境配置不一致。
不要在压测数据漂亮时松懈,要问自己:数据参数化了吗?链路覆盖完整吗?数据量接近生产吗?连接池和JVM配置对齐了吗?
只有把这些坑都踩过、都补上,压测才能真正告诉你系统能扛多少。
下一篇我们进入JMeter实战,从线程组到监听器,把工具用起来。
