我做过不少压测也见过不少压测翻车的现场。印象最深的一次是某系统压到 2000 TPS 就再也上不去研发说代码没问题运维说配置没问题最后我把应用容器的线程池从默认值调到 400TPS 直接翻倍。从那以后我就明白性能测试从来不是拿工具拉一个并发、再等一个数字报表那么简单。它至少要覆盖目标、工具、环境、脚本、监控、结果、复盘多个视角否则一次压测很可能变成一次数据造假的彩排。这篇不打算写任何一款工具的官方文档式教学而是把性能压测整个链路里我踩过的坑、验证过的经验、沉淀下来的方法按多个视角一次性说透。无论你是刚接触压测的测试同学还是被性能问题追着跑的接口开发或是想给系统做个全面体检的运维应该都能从中找到能直接用的东西。1. 目标视角压测之前先想清楚要验证什么1.1 性能测试本质上是为了回答四个问题很多人一提到压测就默认是“把并发拉高看系统什么时候挂”但实际工作中性能验证的目标远不止这一种。为了避免大家拿着一套脚本走天下我把常见诉求拆成了四类每一类的结论形式和执行方式完全不同。第一类是基准测试也叫基线测试。它的目标是确定系统在特定配置、特定业务量下“原本”的表现通常用少量并发甚至 1 个用户去跑得到一组响应时间、吞吐量的基准值。这类测试最大的价值在于给后续调优和回归提供参照物没有基线后面所有“优化了 30%”的说法都是自说自话。第二类是负载测试目标是验证在预期的正常负载下系统能不能稳定满足 SLA。比如业务高峰期 5000 人同时在线峰值 TPS 大概是 800那就模拟这个负载去跑一段时间看各项指标是否达标。它回答的是“够不够用”的问题。第三类是压力测试目标是找到系统的上限和崩溃点。通常会让并发或吞吐持续加大直到系统出现错误率上升、响应时间恶化的拐点然后记录这个拐点在哪。它回答的是“能撑到什么时候”的问题用于容量规划、限流阈值设定和应急预案制定。第四类是稳定性测试也叫疲劳测试。目标是以一定的负载持续运行数小时甚至数天观察内存是否泄漏、连接是否耗尽、性能是否逐渐劣化。很多系统能扛住 1 小时的压力却扛不住 8 小时的温水煮青蛙这类测试就是我们日常说的“跑长测”我最怕听到的一句话就是“系统没问题只是跑了 20 分钟就停了”。除了这四类还有尖峰测试模拟突发流量瞬间暴涨的场景比如整点秒杀、定时任务触发瞬间。这类测试关注的不是能不能压死系统而是系统被瞬间冲高后能否自愈、能否通过限流和熔断保住核心链路。我的建议是接到压测需求后先不要急着写脚本而是问一句这次的目标是验证容量、找上限、确认稳定还是对比调优效果目标不一样并发模型、压测持续时间、监控重点全部都不一样。把这件事搞清楚整个过程就成功了一半。1.2 指标不是只看吞吐和平均响应时间压测报告里最常见的两个数字是 TPS 和平均响应时间但这两者单独拿出来都非常有迷惑性。TPS每秒事务数衡量的是系统的吞吐能力QPS每秒查询数经常和它混用。严格来说一个事务可能包含多个查询比如“下单”事务包含库存查询、订单写入、支付调用等多个内部操作。在制定目标时一定要先把口径定清楚到底 TPS 是指完整的业务流程数还是指单个接口的请求数。口径不统一研发和测试对同一个数字的理解可能完全不同。响应时间方面我强烈建议把平均响应时间放到次要位置重点看百分位指标。一次压测中大部分请求在 100ms 内返回但少量请求卡了 3 秒平均值可能只有 150ms看起来非常健康实际用户体验已经崩了。更准确的做法是看 TP95、TP99也就是 95%、99% 的请求响应时间落在什么范围内。P99 最能反映长尾延迟而长尾延迟往往就是性能问题爆发的信号。并发用户数和系统承受的压力也不是直接等号。10000 人在线不等于 10000 并发在一个典型业务系统中在线用户可能只有 5% 到 10% 同时处于操作状态而这部分里真正在发请求的又会打一个折扣。有一个实用的估算公式利特尔定律并发数 每秒请求数 × 平均响应时间。如果目标 QPS 是 1000平均响应时间是 100ms那么系统实际承载的并发大约是 1000 × 0.1 100。很多不懂的人一上来就把并发线程数拉到 2000其实系统根本不需要也不应该这么扛。1.3 压测目标必须是可以验收的书面约定压测之前我会逼着需求方把目标写成一条一条可量化的语句而不是笼统地说“性能要好”。一个标准的性能目标包含四要素。吞吐目标核心接口 TPS 需要达到多少延迟目标TP95、TP99 不能超过多少毫秒成功率目标错误率上限是多少一般不超过 0.1%资源水位目标压测期间 CPU、内存、磁盘 IO 平均占用不高于多少举个例子订单提交接口“在 800 并发下TPS ≥ 1500TP99 ≤ 500ms错误率 ≤ 0.1%压测持续 1 小时应用服务器 CPU 平均 ≤ 70%数据库 CPU 平均 ≤ 50%。”这样一份目标书既能让研发清楚优化方向也能在最终评审时避免两边对着完全不同的数据吵架。2. 工具与环境视角结果可信度从准备阶段就决定了2.1 主流压测工具到底怎么选每个团队都有自己的压测工具偏好这很正常但选型不应该只看“用过的人多”而要结合自己的协议、脚本复杂度、分布式需求和团队技能来决定。我用一张表把常见工具的核心差异拉出来工具脚本语言并发模型分布式支持学习成本最擅长的场景JMeter图形化 少量脚本/BeanShell线程模型支持多施压机中低复杂业务链路、多协议混合场景wrkLua基于事件驱动的高性能模型有限低单接口快速压测、开发自测k6JavaScript基于 Go 的高并发模型支持云/集群中脚本化、CI/CD 集成场景LocustPython协程模型支持中复杂自定义协议、需要 Python 生态GatlingScalaActor 模型支持中高对性能要求高的 DSL 场景选型核心要看三点。第一协议是否覆盖。纯 HTTP 接口哪个工具都行但涉及 WebSocket、gRPC、自定义 TCP 二进制协议时JMeter 可以通过插件扩展Locust 可以直接用 Python 写客户端而 wrk 这种轻量工具就很难做。第二场景复杂度。单接口、链路短、参数少的场景用 wrk 这种轻量工具效率极高多接口、有依赖、需要动态取值、有业务比例配比的场景JMeter 这类重工具反而更省心。第三报告与集成能力。k6 和 Gatling 对自动化流水线支持更友好JMeter 则靠庞大的插件生态弥补了原生报告不够好看的问题。2.2 我的组合打法JMeter 为主wrk 为辅在团队里我没有让所有人都用一种工具而是按场景配置了“重型”和“轻型”两套方案。正式压测、链路复杂、需要留痕的项目我基本都用 JMeter。原因很实际社区生态完整、团队里每个人都能看懂测试计划、官方的聚合报告和 HTML 报告可以直接存档遇到问题容易排查。但 JMeter 有个致命弱点就是以线程模型来模拟并发线程开多了之后施压机本身会先吃不消所以真到高并发场景不能指望一台笔记本跑出准确结果。日常开发自测和快速验证瓶颈我强烈推荐 wrk。它用事件驱动代替线程单机就能打出很高的并发写一个 Lua 脚本几十行就能完成带自定义参数的压测。它跑出来的延迟分布非常直观适合在开发环境先摸摸底。这里有一个我在 JMeter 上踩过的大坑千万不要开着图形界面的监听器跑压测。GUI 模式本身会消耗本机资源监听器里的结果树如果开启“保存响应数据”还会把每一步的完整响应写到磁盘压测结束时施压机可能已经成了系统瓶颈。正确的做法是用命令行模式执行测试期间只开启必要的输出配置跑完再用结果文件生成 HTML 报告。命令行示例jmeter -n -t order_stress.jmx -l result.jtl -e -o html_report如果并发规模很大比如单机压到 3000 线程还达不到目标就需要分布式压测。多台施压机通过 agent 模式配合调度但要注意同步参数、确保每台 agent 的 JDK 版本、JMeter 版本、脚本和数据文件完全一致否则同一场景在不同施压机上跑出来的数据会有明显偏差合并统计结果时误差会被放大。2.3 测试环境与测试数据是压测结果可信度的地基环境问题是我见过压测结果无法复现的主要原因。很多团队直接用开发环境压数据量极少代码逻辑也不完全一致压出来的数据除了证明“系统能跑”没有其他意义。最理想的做法是准备一个独立于日常开发的预发环境规格尽可能贴近生产。重点不是一模一样而是以下关键点不能差太多应用实例数和对应的注册中心流量比例、数据库配置和连接池上限、缓存分片和容量、消息队列的消费者实例数。如果生产是 8 个应用实例测试环境至少要有 2 到 3 个实例否则单实例的线程池和连接池压力放大效果无法体现。测试数据同样重要。数据库中如果只有几万行数据和几千万行数据时相同 SQL 可能走向完全不同的执行计划压测结果可能差出好几倍。我的习惯是至少准备与生产核心表数据量为 50% 到 100% 的测试数据并且数据分布要模拟真实业务。比如用户 ID 要覆盖低、中、高不同热度订单状态要有多种类型不然很容易遇到“压测全是缓存命中”的虚假繁荣。还要重点处理数据隔离和幂等。压测会不断写入数据如果脚本里的手机号、订单号固定不变第一次能成功第二次可能就触发唯一索引或幂等校验直接报错。我通常在参数里拼接时间戳和随机数做前缀并在请求头加一个自定义标签例如x-perf-flag: 1这样后续去掉压测数据时只要按这个标记清理即可不影响正常业务数据。3. 场景与脚本视角把用户真实行为翻译成压测请求3.1 场景模型设计要遵循渐进原则很多人拿到 JMeter 就直接把线程组的并发数拉到 500这种方式我极度不推荐。合理的压测场景设计应当从轻到重、从单点到链路循序渐进这样每个阶段都能留下可解释的数据。我的习惯分三步走。第一步先做单接口基础测试。用一个线程组、少量并发跑 5 分钟验证脚本本身的正确性确保参数、断言、数据都能通过同时拿到单接口的基线和大概的吞吐上限。第二步按业务比例混合接口压测。把登录、列表查询、详情、提交订单等接口放在同一个线程组或事务控制器里通过加权比例模拟真实用户操作。第三步做阶梯加压也就是 ramp-up。比如初始 50 并发每 5 分钟增加 50同时记录每一阶梯的 TPS、RT、错误率这样能非常清楚地看到从哪个并发点开始出现拐点。这里有一个很容易忽略的细节思考时间不能设置成 0。把思考时间设成 0相当于所有用户在同一瞬间抢系统资源这属于尖峰测试的玩法不是真实负载。真实用户操作之间会有停顿这个停顿对系统的压力是重要的缓冲。我会根据业务特点设置 100ms 到 500ms 不等的思考时间既保证压力足够又能接近真实流量。3.2 参数化和关联脚本的真实性决定了结果的有效性压测脚本里最影响真实性的就是参数处理。如果所有请求都使用同一个 token、同一个用户 ID、同一个商品 ID那压测出来的结果至少有两个问题一是热点数据缓存会让后面的请求全部命中缓存系统负载远低于实际二是并发场景下同一个用户共享会话可能出现资源竞争导致的非典型错误。参数化最常用的是 CSV 数据集把一批测试用户、商品、订单号放在文件里让每个线程循环取不同值。另一个是动态关联遇到登录返回的 token、创建订单生成的订单号这类前后依赖需要使用正则表达式或 JSON 提取器把返回值取出来传给后续请求。拿订单提交流程举例先请求创建订单接口从响应中提取orderId再把orderId带入支付接口的请求体里这个过程就是关联。还有一个让很多人忽略的点对随机数据要有一个清醒的认识。不是所有字段都必须随机但标识性字段、账号类字段、空间查询的坐标字段等最好都要覆盖不同取值。比如列表查询接口如果每次都用同一个 keyword 查询数据库可能走了缓存或者命中同一索引段压测结果对搜索引擎或数据库优化没有任何参考价值。3.3 脚本里的隐形杀手断言、监听器和事务定义写脚本时我还见过不少“压测没到量工具先挂了”的情况问题往往出在三个地方。第一断言过重。每个接口都加了响应体包含校验每次请求都要做字符串匹配引擎额外承担大量解析工作。我的原则是压测脚本里断言只保留最关键的成功标志比如状态码 200 或业务码为 0其余细枝末节的字段校验放到自动化测试去做压测脚本只负责制造压力。第二监听器滥用。保存日志、保存全量响应、开启结果树这些功能都会产生巨大的 IO 开销。线上压测我通常只保留一个简单的聚合结果写入文件跑完一次性生成报告。第三事务边界定义错误。JMeter 里的“事务”默认是单个 Sampler但在真实业务里一次下单操作可能包含创建订单、支付、库存扣减多个步骤。要在事务控制器里把这几个 Sampler 包起来统计出整个业务链路的 TPS 和响应时间而不是只看单个接口。4. 监控视角压测时不看监控结果就是盲人摸象4.1 四层监控体系应用、中间件、数据库、操作系统压测过程中压力客户端给出的 TPS 和错误率只是结果真正能帮我们定位原因的是过程中各层的运行状态。我习惯把监控分成四层缺一层都不够完整。应用层要看 JVM 的堆内存、GC 次数和停顿时间、活跃线程数、Tomcat/容器连接线程池的使用情况。盈利性监控可以用 APM 工具比如 SkyWalking 之类的开源链路追踪系统但哪怕是临时用jstat -gcutil pid 1000这种命令盯一下 Full GC 次数也比什么都不看强。中间件层要盯缓存命中率、连接数、队列积压量。比如 Redis 的INFO stats可以看到 keyspace_hits 和 keyspace_misses消息队列的 consumer 堆积情况直接反映了消费能力是否跟上生产。数据库层是最容易忽略也最能暴露问题的一层。重点看慢查询数量变化、连接数是否快速增加、锁等待次数、InnoDB 缓冲池命中率以及当前活动的 SQL 是什么。命令可以先从SHOW FULL PROCESSLIST开始但这只能看到瞬间状态如果要连续观察用performance_schema.events_statements_summary_by_digest按 SQL 摘要聚合分析更可靠。操作系统层关注 CPU、内存、磁盘 IO 和网络。vmstat 1可以看 CPU 的 us/sy/wa 占比iostat -x 1可以看到磁盘利用率sar -n DEV 1看网卡流量。这些数据的抓取频率建议在压测期间保持 5 秒一次可以和压测脚本的阶梯并发度对齐方便事后做关联分析。4.2 分链路追踪一次请求的时间都花在哪里压测出现响应时间恶化时第一步不是调代码而是搞清楚这 500ms 到底消耗在哪一段链路上了。一次完整的用户请求可能经历网关、应用、下游服务、缓存、数据库每一跳都可能成为瓶颈。如果有链路追踪系统直接看 trace 的时间分布即可。如果没有最笨也最有效的办法是在应用代码里对关键调用点打点计时。比如发现某接口响应时间从 200ms 涨到 1200ms可以通过分段时间拆解看出是本地排队时间长、下游服务调用慢还是数据库查询变慢。我处理过一类非常典型的案例某接口 TP99 从 300ms 突然涨到 3 秒但从应用日志看接口本身的逻辑执行时间只有 40ms。后来一查是应用线程池被打满了大量请求在线程池队列里排队等待而排队的时间完全没算在接口处理时间内。这就是典型的“线程池耗尽”问题压力根本没打到逻辑层全堵在入口。没有合适的监控视角这类问题很难定位。4.3 资源利用率的正确解读饱和度比百分比更重要很多人看到 CPU 利用率 60% 就觉得没问题但 60% 的数值含义在单核和多核系统上完全不同。更关键的是CPU 较高未必是坏事反而可能说明系统在充分干活真正要警惕的是“CPU 不高但吞吐已经上不去”那往往意味着瓶颈在锁、连接池或者磁盘 IO。判断饱和度的三个实用信号一是请求排队数或线程池活跃数触顶比如 Tomcat 线程池打满大量请求都在等待二是数据库连接池用满报connection timeout之类的错误三是磁盘 IO 利用率长时间在 80% 以上伴随iowait升高。看到这些信号就算系统 TPS 还没下降也已经到了容量上限附近继续加压就是在放大延迟。5. 分析与调优视角从数字报表到真实瓶颈5.1 学会看响应时间分布和 TPS 曲线的形态拿到压测报告后第一眼要看的是 TPS 和响应时间随并发变化的关系曲线而不是看单个最大值。正常情况下随着并发从低到高TPS 会先呈线性增长然后增速放缓进入平台期最后出现下降。平台期的起点就是系统的合理容量点而下降点往往是资源耗尽或排队加剧的表现。响应时间曲线也一样前期平缓到了某个并发点后会快速抬头甚至垂直上升这个拐点就是需要重点记录的阈值。如果 TPS 在某个并发下突然断崖下跌同时错误率暴增大概率是触发了某个保护机制比如限流、熔断或连接池拒绝。如果 TPS 没有下降但响应时间持续升高通常说明请求在排队系统还在尽力处理但已经积压了大量任务。这些形态对应的问题和调优方向完全不同拿到曲线后先判断属于哪种形态再动手。5.2 常见底层瓶颈线程池、连接池、GC、锁很多性能问题无非是几种经典的底层原因在轮番上演。线程池过小是最高频的瓶颈。容器默认线程池参数往往保守当并发超过线程池容量时新请求只能在队列里排队。这类问题的特征是CPU 利用率不高、TPS 提不上去、响应时间持续恶化解决方法大多是调大 maxThreads 到合理值但调太大也会导致上下文切换开销增加通常按每核 200 到 300 线程做初步估算后实测调整。连接池耗尽也是经典问题。数据库连接池、Redis 连接池、HTTP 客户端连接池任何一个打满都会导致获取连接超时。这类问题的特征是应用日志频繁出现获取连接超时或等待连接异常同时数据库或对应中间件的当前连接数等于池上限。解决方向是调大池大小但更要排查是否有连接泄漏比如某个异常分支没有归还连接。GC 引起的性能问题往往呈周期性的响应时间尖刺。我见过一个系统大多数请求响应时间 200ms但每隔几秒就有一个请求冲到 2 秒以上监控 GC 后发现每次 Full GC 正好对应尖刺。这时候优先处理的是对象分配速率和堆内存参数而不是盲目优化业务代码。锁竞争比较隐蔽但一旦出现就是压测结果惨不忍睹。热点数据加锁、synchronized 代码块包含耗时操作、分布式锁长时间持有都会导致并发能力急剧下降。通过线程 dump 经常可以看到大量线程处于 BLOCKED 或 WAITING 状态。5.3 一个完整调优循环的实操记录我通常会用“压测—分析—调优—再压测”的循环确保每一次改动都能用数据说话。之前有个某接口的性能问题初始压测 200 并发下TPS 只有 300TP99 高达 1300ms。第一轮分析后发现应用线程池满负荷但数据库和 CPU 压力都很低于是把应用线程池从默认值调大TPS 提升到 800TP99 降到 700ms。第二轮发现数据库慢查询明显增多检查发现压测数据里某个表的索引没有命中补上联合索引后再压TPS 稳定在 1300TP99 降到 420ms。整个过程每轮控制变量只改一个东西递进很快。这里最关键的是控制变量。如果同一轮里既调了线程池又改了数据库索引还重启了服务最后出了问题都分不清是哪一步生效的。我的习惯是每轮只做一处变更同时保证脚本、数据、并发、环境完全一致压出来的差异才可信。6. 常见问题与复盘视角那些压测翻车现场6.1 最容易导致结果废掉的六个坑我把这几年压测过程中遇到过的问题集中梳理了一下其中以下六条是出现频率最高、对结果影响最大的。第一施压机被打满。单台机器跑 JMeter 高并发时施压机本身的 CPU 或网络先到瓶颈测出来的结果根本反映不了服务器真实能力。判断方法是压测结束后看施压机的资源消耗如果 CPU 连续超过 80%就要考虑扩容施压机或用分布式模式。第二测试数据过于单一。固定账号命中缓存、固定订单号触发幂等、单一商品 ID 导致数据库行锁竞争都会让结果失真或出现假错误。第三预热缺失。应用刚启动时 JIT 还没有充分编译连接池还是空的直接高并发压测会让首批请求承担大量初始化开销表现偏慢。正式压测前先用较低并发跑 5 到 10 分钟做预热让系统进入稳定状态。第四压测流量混淆。重要环境压测没有做流量标记结果压测数据和线上数据混在一起事后清理困难还可能误伤线上统计报表。我在压测请求头加标记之后这个问题基本绝迹。第五并发模型不符合真实场景。所有接口的 Think Time 都设 0所有接口权重一样算出来的结果和用户真实行为完全对不上。这个在前面章节已经展开说过。第六压测环境变更未冻结。压测期间研发合入了新代码、运维改了连接池参数、DBA 建立了新索引都会让结果失去可比性。压测前发一个“环境冻结通知”让相关人员在压测期间不做变更能避免大量扯皮。6.2 稳定性测试接下来的独特看点短时间高并发压测通过不代表系统能安稳跑一天。稳定性测试需要额外关注几个短测不会暴露的指标。内存泄漏是最隐蔽的。如果压测过程中 TPS 平稳但堆内存使用率一路爬升每次 GC 后内存回收幅度逐渐减小十有八九是某处对象被错误持有不释放。这类问题用jmap -histo:live pid看对象增长趋势很有帮助。连接泄漏也是稳定性问题大户。每次请求都会新开连接但不关闭最终所有连接池被占满系统卡死。监控连接数随时间缓慢增长时要检查代码里所有获取连接的路径是否都有 finally 释放逻辑。日志和临时文件膨胀同样值得警惕。如果应用打印了 DEBUG 级别的日志稳定测试跑 12 小时下来日志文件可能膨胀到几十 GB磁盘写满后系统会进入半死状态。稳定性测试期间必须对磁盘使用率做实时告警。6.3 压测过程的操作纪律最后聊几条压测现场的操作纪律是多年踩坑换来的。第一压测开始前发一个明确的通知包含压测时间窗口、涉及的系统、入口地址、预期并发、受影响的上下游让大家有个心理预期。我曾经遇到过压测和另一个团队的定时任务撞车两边同时加压结果互相干扰数据完全没法看。第二压测执行要有“紧急止损”路径。如果压测过程中发现监控指标异常尤其是有依赖的中间件严重告警立即降并发或停止压测不要为了数据硬扛。提前写好事后停止的脚本可以在 30 秒内把一个高并发场景彻底停掉。第三每次压测的配置文件、脚本文件、数据文件、结果文件、监控截图一并归档。几个月后再有人问“上次压测结论是什么”能快速调用当时的环境和脚本比靠记忆靠谱得多。写在最后做了这么多年性能压测我的体会有两点。一是性能测试不是一次性任务而是一个需要反复迭代的过程系统每经历一次大版本变更、架构调整、数据量增长之前压测出的结论就可能不再成立把压测扎进日常研发流程里比一年做一次单点突击更有价值。二是压测最大的敌人不是系统太弱而是数据不透明。只要监控数据、脚本数据、环境数据都能完整保留任何一次性能问题都能快速定位到具体原因。最后再分享一个小技巧压测前我会把这次要验证的核心指标和预期值写成一页纸贴在压测执行的终端旁边。每次阶梯加压结束先看这页纸上的指标有没有达成再去做复杂分析。这样既不会迷失在大量图表里也能保证每一步都是朝着最开始定下来的目标推进。哪怕最终压测结果不理想这一页纸也能成为和研发、产品对齐问题的最有力证据。
阅读完成 · 觉得有帮助?