做性能测试这些年,最常被问的一句话是“系统到底能扛多少并发”。但真到了压测现场,你发现最难的往往不是把TPS顶上去,而是那些此起彼伏的报错、忽高忽低的响应时间、压测机自己先挂掉、测试数据一跑就失效……一堆问题堆在一起,你都不知道该先查哪个。
这篇就聊聊我在性能测试里反复踩过、也反复帮别人排掉的那些问题。不整虚的,全是现场能直接用的排查思路和解决方法。从需求澄清、场景设计、脚本编写,到压测执行、结果分析,一条线串下来,适合刚接触性能测试的测试开发,也适合被压测报告折磨得头疼的运维和开发同学。
1. 先想清楚:性能测试到底在测什么
1.1 需求澄清怎么问
很多性能测试翻车,不是脚本写错,而是需求没问明白。业务方丢过来一句话“系统要支持10万用户”,这10万是注册用户数、日活、还是峰值并发?完全是三个量级。我一般会拿着这几个问题去反推:
- 这个接口/页面预估每秒有多少请求?有没有来源数据(比如线上监控、运营预估、往期大促数据)?
- 峰值持续时间是多久?是持续半小时,还是只有几分钟的脉冲?
- 可接受的响应时间是多少?是99线还是平均响应时间?P95/P99是核心。
- 失败率容忍度是多少?比如核心交易链路要求0.1%以内,非核心接口可以放宽到1%。
- 系统有没有降级预案?压测出来的瓶颈点能不能通过限流、熔断、缓存扛过去?
把这些问完,你的压测才是有的放矢,不是自己猜一个并发数然后埋头造数据。
1.2 场景设计常见的三个误区
第一个误区是只测单接口,不测混合链路。真实用户是打开页面、登录、查询列表、点详情、提交表单,一系列操作串起来的。如果只压单个接口,你测不出整体资源竞争下的真实表现。正确做法是设计业务占比模型:比如登录占10%、查询占50%、下单占20%、支付占20%,按比例混合施压。
第二个误区是直接把并发数当成目标。我见过太多人上来就“我要压5000并发”,结果表现在还没到吗并发用户,连接池先被打满。更好的方式是先小并发跑通,然后阶梯加压(比如每5分钟加50并发),观察TPS拐点。不要一上来就追求最大并发,那是最后一步的极限测试,不是常规性能测试。
第三个误区是忽略前置时间。很多接口要先拿到token、先创建订单才能操作,这些前置步骤如果单独压测,会消耗大量资源。比较好的做法是用真实接口采样数据做参数化,或者单独特化前置数据准备,把前置操作放在压测脚本外面提前准备好,压测时只关心目标操作的性能。
2. 工具选型和环境准备
2.1 压测工具怎么选
没有最好的工具,只有适不适合。如果是HTTP接口,我个人常用某开源压测工具(性能好、上手快,支持脚本编写)。它支持多线程模型,每个线程可以模拟一个用户,还能直接用脚本写复杂逻辑,适合做混合场景。
如果是Java微服务,或者需要压测内部RPC,我也常配合另外几款工具:有的适合压测吞吐量、有的适合做全链路追踪。选型时别迷信“谁比谁更先进”,要看:
- 支持协议:HTTP、HTTPS、WebSocket、TCP、JDBC、MQ……
- 脚本能力:能否参数化、断言、关联、处理签名逻辑
- 监控能力:有没有和监控平台对接的插件
- 分布式施压:单机不够时能否横向扩展压测机
我习惯先把脚本在单机上调通,再上分布式压测集群。不要一开始就上几百台压测机跑,出了问题你都不知道是脚本问题还是环境问题。
2.2 测试环境与生产环境的差距
这是性能测试里最容易被甩锅的环节。测试环境的机器配置、网络链路、缓存情况跟生产差太多,压测结果基本没有参考价值。我遇到过测试环境压出2万TPS,跑上线后只有5千,一查发现生产环境的数据库连接池默认只配了50,测试环境配了500。
我的做法是:
- 先在测试环境压,发现明显问题(死锁、内存泄漏、慢SQL);
- 再在预发/灰度环境压,尽量保证配置和实际运行环境一致;
- 如果生产有条件,选择低峰期做短时压测,直接拿真实流量验证。
另外,测试环境要尽量“干净”。有没有其他定时任务在跑?有没有其他团队在测同一个环境?这些都会污染数据。压测前花10分钟检查一下环境状态,比事后解释强得多。
2.3 数据准备那些坑
数据准备是性能测试的隐形工作量。常见问题:
- 数据量太少,全被缓存命中,测出来的性能虚高。真实系统里数据是有冷热之分的,几百万条记录跟只有一条记录,执行计划完全不同。
- 数据格式不对。比如手机号校验、身份证规则、订单状态机,业务代码里各种if分支,数据一不对就走不到真实分支,等于白测。
- 并发写同一批数据,锁冲突严重,导致性能崩坍。
我的经验是提前准备“压力数据池”:
- 按照业务量级预估数据总量,比如1000万条订单数据,其中近30天的热数据占20%,其余为冷数据;
- 参数化数据要唯一,比如用户名、手机号、订单ID全部用随机生成或从库里批量捞出,写入参数文件;
- 涉及写操作的数据要保证可持续供给,要么定期清理,要么准备一批可重复消费的数据。
3. 压测执行中的常见问题与排查
3.1 脚本参数化与关联问题
脚本报错最多的,不是逻辑不会写,而是用错了变量。我印象里有个模拟项目X的压测,登录接口每次返回一个sessionId,查询接口需要带上它。结果同事直接写死了一个固定的sessionId,跑着跑着session过期,后面的请求全部401。这种问题表面上是脚本错误,本质上是不理解“关联”——后续请求所需的动态值,必须从前一个响应里提取。
解决方法也很直接:
- 第一步,在压测工具里添加正则表达式或JSON提取器,从前置接口响应中提取token/session/orderId;
- 第二步,把提取到的值放入某个变量,后续请求引用该变量;
- 第三步,调试时开启“断言”,对每个关键请求加响应码校验,一旦失败立即定位到哪一步。
参数化也同样重要。并发用户数多时,如果大家都用同一个账号名,服务端会误认为是同一个人反复请求,可能触发风控或串号。正确的做法是把一批测试账号放入CSV文件,脚本随机或顺序读取,模拟不同用户的真实行为。
还有一个很容易忽略的地方:时间戳和随机数。有些接口要生成签名,而签名的原始字段包含当前时间戳。如果你在脚本中固定了时间戳,那跑到后面签名必然过期。解决方式是每次请求重新生成时间戳,并确保压测工具用的时区和服务器一致。
3.2 压测机瓶颈:自己不先挂
我自己刚开始做压测时犯过一个低级错误:压着压着目标系统还好好的,压测工具自己先卡死了,CPU跑到100%,内存飞涨,最后被监控平台连着报警。后来才明白,压测机也是要提前评估的。
如果目标是高并发场景,单台压测机能创建的连接数有限。Linux默认的文件描述符限制是1024,超过这个数,压测工具就会报“Too many open files”。所以压测前一定要调优系统参数:
# 临时调整文件描述符限制 ulimit -n 65535 # 永久生效,写入/etc/security/limits.conf # 格式:* soft nofile 65535 # * hard nofile 65535另外还要注意压测工具自身的内存分配。如果脚本里有大量参数化文件读取,或者每个请求都保存详细响应结果,压测机内存很快就会被打满。建议生产压测时关闭详细日志,只保留结果摘要。能开分布式压测就尽量开,别让一台机器硬扛十万级并发,它真的会先倒。
3.3 响应时间毛刺与抖动
压测曲线跑出来,TPS整体平稳,但响应时间每隔几分钟就出现一个尖峰。这种问题最让人头疼。定位的思路一般是:
- 先看是周期性抖动还是随机抖动。周期性抖动优先怀疑GC、定时任务、日志滚动、备份任务;
- 抓GC日志,看Full GC是否频繁,老年代是否在持续增长;
- 看系统级监控:CPU、内存、磁盘IO、网络流量在同一时间点是否有波动;
- 再查中间件:连接池是否满了、缓存是否过期重建、消息队列是否有积压。
我在某个网关项目里遇到过一次:响应时间每10分钟规律性上涨,后来发现在压测脚本里用了同一个随机数种子,导致每10分钟触发了缓存批量过期的某个逻辑,全部回源数据库。所以不只盯服务端,客户端脚本的逻辑也要回头检查。
3.4 并发数与线程数的换算
大家常把“并发数”挂在嘴边,但压测工具的“线程数”和业务上的“并发用户数”不是一回事。一个线程可以连续跑多个请求,每个请求的响应时间有长有短,所以同一时刻在服务端停留的并发请求数,需要通过公式换算:
假如预期并发用户数为N,平均响应时间为RT秒,单用户思考时间为T秒(用户看完页面再操作的时间间隔),那么需要的压测线程数大概是:
线程数 ≈ N * (RT + T) / RT举个例子:预期并发用户1000人,平均响应时间0.5秒,思考时间1.5秒,那么实际压测线程数 = 1000 * (0.5 + 1.5) / 0.5 = 4000。也就是说,你需要用4000个线程去模拟1000个用户的操作节奏,否则请求会发得比真实场景更密集,服务端压力被放大。
当然,如果只是想测系统极限吞吐,可以直接把思考时间设为0,一步步加线程数直到TPS不再增长。这两种做法的目的不同,别混在一起。
4. 结果分析与瓶颈定位
4.1 从TPS和RT曲线看问题
压测结束后,第一件事不是看平均值,而是看趋势曲线。我会组合看三张图:TPS曲线、响应时间曲线、错误率曲线。
正常状态下,TPS随着线程数增加而上升,响应时间基本稳定,到达某一点后TPS不再增长,此时为系统吞吐上限。如果TPS还在涨,但响应时间同时快速上升,说明系统已经过载,请求在排队。错误率如果在拐点后出现,说明服务端开始拒绝请求或出现超时。
还有两种情况需要特别注意:
- TPS一开始就上不去,但系统资源很低。这时候优先怀疑代码里有串行锁、数据库行锁,或者外部依赖耗时过长。资源很低不代表系统不忙,可能在忙等。
- TPS出现锯齿状波动。这种往往是触发了限流、熔断或自动伸缩策略,系统一会儿拒绝请求一会儿恢复,曲线就会像锯齿。
4.2 资源指标怎么看
服务端的四个基础指标:CPU、内存、磁盘IO、网络带宽。它们是有“正确姿势”的:
- CPU:先看总体使用率,再分核看有没有CPU不均衡。如果只有部分核打满,可能跟NIO线程数配置、队列绑核有关。还要区分是用户态高还是内核态高,内和态高可能是系统调用频繁,比如网络中断和上下文切换。
- 内存:看堆内存使用趋势,如果持续上升并频繁Full GC,基本可以判定有内存泄漏;如果只是大促前缓存预先加载导致,那属于正常。
- 磁盘IO:数据库服务器的磁盘IO利用率超过80%就要警惕了。很多慢SQL不一定慢在查询本身,而是磁盘随机读太多,缓存命中率低。
- 网络带宽:压测大流量场景时最容易忽视,千兆网卡跑满也就100MB/s左右,一旦打满,包会排队,延迟飙升。这里可以用一个简单的估算:如果TPS是1万,平均响应体是50KB,那每秒传输量就有500MB,明显超过单机千兆网卡上限。
4.3 常见的性能瓶颈清单
我把这些年压测中经常遇到的瓶颈类型整理成一张表,方便按图索骥:
| 瓶颈层 | 常见表现 | 典型原因 |
|---|---|---|
| 应用层 | TPS上不去、RT波动 | 同步阻塞调用、线程池太小、锁竞争严重 |
| 数据库层 | CPU高、慢SQL多 | 索引缺失、SQL走了全表扫描、连接池打满 |
| 缓存层 | 命中率低、木刺严重 | key过期集中、缓存穿透/击穿/雪崩 |
| 中间件 | 连接数满、堆积 | 线程池配置不当、消费者处理能力不足 |
| 网络层 | 带宽占满、包重传 | 大报文多、跨机房带宽有限、MTU设置不合理 |
| 系统层 | 上下文切换高、句柄耗尽 | ulimit限制、线程数过高、短连接频繁创建 |
遇到瓶颈时,不要一上来就调数据库。很多问题根源在应用代码:一个串行的远程调用,一个for循环里的多次DB查询,都能让性能跌几个数量级。
5. 常见问题与排查技巧实录
5.1 错误类型速查表
压测执行过程中,错误类型五花八门,这里按出现频率排个序:
| 错误现象 | 常见原因 | 解决方向 |
|---|---|---|
| Connection reset | 服务端主动断开连接,可能是超时、防火墙或负载均衡空闲断开 | 查看服务端日志、调整keepalive设置 |
| 超时(Timeout) | 请求在等待队列中排队太久,或外部依赖响应慢 | 定位超时发生的环节,加大线程池不能解决根本问题 |
| Too many open files | 压测工具或服务端文件句柄耗尽 | 调大系统ulimit,检查是否大量新建连接未关闭 |
| 500/502/503 | 服务端内部异常、网关超时或过载 | 抓对应服务日志,看是代码异常还是负载均衡健康检查失败 |
| 401/403 | token失效、权限不足 | 检查参数化token是否过期,或压测账号是否被风控 |
| 断连(Broken pipe) | 客户端已关闭连接,服务端还在写数据 | 通常由超时导致,先解决超时问题 |
| 断言失败 | 响应内容或状态码不符合预期 | 调试脚本,检查关联值和返回值 |
这里要特别提醒:性能测试中遇到的报错,大部分不是服务端真的坏了,而是压测方式不对。比如线程数压太高导致超时,比如参数化数据用完了导致重复提交,这些要先从脚本侧排查,再找开发。
5.2 定位流程
遇到压测异常,我一般按这个顺序排查,效率最高:
- 先看压测工具的错误日志和汇总统计,确认错误率和错误码分布。
- 再看被压服务器的监控面板,确认时间点是否与错误时间吻合。
- 查应用日志,找异常堆栈。不要只看业务异常,也要看GC日志和线程池拒绝日志。
- 查中间件和数据库慢日志,确认是否存在慢SQL或连接池等待。
- 最后查网络链路,看是否跨网段、是否存在丢包重传。
整个流程的核心是“时间线对齐”。把客户端、服务端、中间件三方的监控拉到同一个时间维度,谁在那个时间点出问题,谁就是嫌疑最大的。
5.3 一批避坑经验
最后分享一些只有实操过才会懂的细节:
- 压测前先跑5分钟小并发预热,让JIT编译和缓存填充先完成,不然第一波数据会虚低。很多新手一上来直接压,看到数据波动大就开始排查,其实是预热不足。
- 不要把失败请求简单设为0容忍。HTTP层成功不代表业务成功,最好在脚本里加业务断言。我有一次压测,接口返回200但业务返回码是“库存不足”,因为测试数据没准备够,压测结果全推翻了。
- 压测时间别太短。至少要跑10-15分钟观察稳定性,很多内存泄漏和连接泄漏要十几分钟才能暴露。
- 全链路压测时,先压下游再压上游。把数据库、缓存、消息队列都压到预期水位,再压入口网关,这样定位问题更清晰。
- 记录压测现场。每次压测前记录:脚本版本、数据量、并发数、线程数、系统参数、环境版本。出问题的时候,翻记录比翻聊天记录靠谱得多。
我在实际项目中感受最深的一点是:性能测试不是一个“跑一次脚本出个报告”的活,而是一个反复验证、持续逼近真相的过程。很多问题藏在你以为不会出问题的脚本逻辑里,藏在没有对齐的时间点里,藏在被忽略的数据准备里。每解决掉一个问题,你对系统运行规律的理解就会深一层,这也正是做性能测试最上瘾的地方。