1. Java 后端为什么迟早要自己压一次接口刚接手一个 Java 服务的时候大家关注的都是功能对不对、单测绿不绿、Sonar 扫描有没有告警。直到某次上线前的容量评审产品问了一句这个接口大促能扛住多少并发会议室里瞬间安静。这时候才想起来代码写得再漂亮也没人真正拿并发去撞过它。JMeter 就是在这个时间点被翻出来的工具——它不解决业务问题它解决的是这套服务到底能撑住多少的问题。对做 Java 的人来说它属于那种平时躺在工具箱底部、关键时刻必须拿得出手的东西。这篇文章面向的是有点 Java 基础、准备给自家接口做一轮压力测试的同学也包括面试前想把这部分知识点补齐的人。我会从环境安装一路讲到脚本编写、加压策略、报告解读把我在实际项目里踩过的坑尽量都摆出来。压力测试这个话题在网上教程很多但大多停在点一下启动按钮看聚合报告这一层真正决定测试结果可信度的细节往往藏在那些没人讲的配置项和加压方式里。1.1 单次调用 50 毫秒不代表能扛住 100 个并发本地用 Postman 调一次接口返回 50 毫秒很多人据此觉得这个接口性能不错。这个结论只对一个人访问成立。一旦并发量上去链路上每一个共享资源都会变成排队点数据库连接池默认可能只有 10 个连接Tomcat 的工作线程池有上限Redis 连接池有上限日志框架的异步队列有容量甚至连对象分配速率上去之后 GC 的频率都会变化。这些因素在单次调用里全部被隐藏了。压测的价值就在于把这些排队点暴露出来。你在 1 个并发下看到的 50 毫秒到了 100 并发可能变成 800 毫秒错误率还可能爬到 5%而这种劣化通常不是线性的——它会在某个拐点突然跳变。找到这个拐点比记住任何一个响应时间数字都重要。JMeter 做的就是把并发量变成可控变量让你能一段一段往上加观察系统在哪个位置开始喘不过气。1.2 压测工具那么多JMeter 的位置在哪命令行工具有 ab、wrk、hey脚本化框架有 Locust、Gatling云上还有各种 SaaS 压测服务。JMeter 的特点是基于 Java 写的、跨平台、图形界面、协议支持广、插件生态大。它支持 HTTP、HTTPS、JDBC、JMS、FTP装上插件之后还能压 MQTT、gRPC这点是 ab 和 wrk 完全比不了的。对 Java 团队来说它的另一个优势是不用为压测单独学一门语言——虽然 Beanshell 和 JSR223 里写的是脚本但整体心智模型对你来说是熟悉的。工具协议覆盖上手成本资源消耗适合场景JMeter广插件扩展低图形界面偏高Java 进程复杂业务链路、多协议混合ab仅 HTTP极低低单接口快速摸底wrk仅 HTTP中需写 Lua很低高并发纯 HTTP 打点LocustHTTP 为主中需写 Python中复杂场景且团队熟悉 PythonGatlingHTTP 为主中需写 Scala DSL中持续集成中的性能回归这张表的意思不是JMeter 最强而是JMeter 最省心。你要压的是一个带登录态、要传 JSON、要校验返回体、还要顺带查一下数据库写入是否成功的业务接口JMeter 的图形界面能让你在半小时内搭出场景换成代码框架可能要磨半天。它不适合的是极限压榨单机 QPS 的场景那种活交给 wrk 更合适。提示JMeter 图形界面只用来开发和调试脚本真正跑压测一律用命令行非 GUI 模式。图形界面本身会消耗大量内存和 CPU会把压测机自己的资源吃光导致结果完全失真。2. 装完 JMeter 别急着点启动下载和安装这一环看起来最简单实际上新手卡住的位置多半在这里。JMeter 是绿色包解压就能用但能启动和能跑出可信结果之间还差好几个配置。尤其是内存参数默认配置下跑几百并发十有八九会看到命令行里报 OutOfMemoryError然后你会以为是 JMeter 不行其实是默认堆太小。2.1 JDK 版本、JAVA_HOME 与启动脚本的关系JMeter 5.6 之后的版本要求 Java 8 或更高用 Java 17 这类 LTS 版本也没问题。真正容易出问题的是启动脚本找 java 的方式Windows 下 jmeter.bat 会优先用JAVA_HOME指向的 JDK找不到才去 PATH 里翻。如果你机器上装了三四个 JDKPATH 里第一个是某个老旧版本启动时就会报版本不兼容。我习惯的做法是单独给压测环境配一个 JDK 目录在启动脚本最上方显式设死# Linux: 编辑 bin/jmeter在文件开头附近加上 export JAVA_HOME/opt/jdk-17 export PATH$JAVA_HOME/bin:$PATH这样无论系统默认 JDK 是什么版本JMeter 用的都是你指定的那一份。脚本调试阶段用图形界面把 jmeter.bat 发个快捷方式到桌面就行服务器上跑压测直接调bin/jmeter脚本。2.2 内存参数改哪里改多大JMeter 的堆参数写死在启动脚本里别去动jmeter.properties那个文件里管的是行为配置不是 JVM 参数。找这几行# bin/jmeter (Linux/macOS) : ${HEAP:-Xms1g -Xmx1g -XX:MaxMetaspaceSize256m}默认 1G 堆对于简单 HTTP 脚本、几百并发是够的但如果脚本里挂了一堆监听器、每个请求都要保存响应数据、还用 JSR223 写复杂逻辑内存涨得很快。我一般会按下面的节奏调调试阶段保持 1G够用改大了反而拖慢启动。命令行压测、并发 500 以内-Xms2g -Xmx2g初始堆和最大堆设成一样避免运行中反复扩容。并发上千或脚本复杂-Xms4g -Xmx4g同时确认压测机物理内存还剩得下这么多别让操作系统开始换页。GC 参数可以加-XX:UseG1GC在压测这种持续高分配的场景下比默认收集器表现平稳。改完堆之后一定要先跑一轮低并发的冒烟确认脚本在非 GUI 模式下能出报告再往上加并发。2.3 中文乱码和 HTTPS 证书这两件事响应体里出现乱码绝大多数情况是没设编码。打开bin/jmeter.properties把这两行打开sampleresult.default.encodingUTF-8 languagezh_CN第一行决定 JMeter 用什么编码解析响应内容第二行只管界面语言。只改第二行不改第一行是你会在网上看到的最常见的一个误导。HTTPS 的处理分两种情况。被压的站点用自签名证书时JMeter 的 HTTP 取样器默认是不做严格校验的一般能直接跑通。真正需要额外动作的是录制脚本这个场景要让 JMeter 作为本地代理去抓浏览器的请求你得把它的根证书装进浏览器或系统信任列表路径在bin/ApacheJMeterTLS.crt。装完之后别忘了用完就把代理关掉不然浏览器上不了网——这个坑我见过不止一个人在排查网络问题。3. 一个能直接抄的 HTTP 接口压测脚本脚本结构这件事网上的教程往往一上来就让你拖元件拖完说这样就好了但没解释为什么这个顺序、为什么有些元件要放在线程组外面。我按自己搭脚本的实际顺序讲一遍你照着做基本不会走弯路。3.1 测试计划和线程组三个数字填多少新建测试计划右键添加线程用户→ 线程组。线程组有三个核心参数线程数就是并发用户数每个线程独立执行一遍你定义的流程。Ramp-Up 时间秒所有线程在多少秒内启动完毕。填 10 表示 100 个线程分摊在 10 秒内陆续启动平均每 0.1 秒起一个。循环次数每个线程执行多少遍。勾选永远就靠调度器控制时长。这三个数字最有讲究的是 Ramp-Up。很多人直接填 0想模拟瞬间 100 并发结果 JMeter 在一瞬间创建 100 个线程压测机自己先卡住前几秒的数据全是噪声。合理的做法是让 Ramp-Up 至少留出 1 到 2 秒的爬坡除了专门测雪崩场景一般不需要 0 秒启动。循环次数方面如果要做 10 分钟的稳定性测试就把循环次数设为永远再勾选线程组下方的调度器填好持续时间。3.2 配置元件先行请求默认值、信息头、Cookie搭脚本的正确顺序是先放配置元件再放取样器。理由很简单配置元件是给同一个作用域下的所有取样器打底你先放它后面加接口的时候就不用每个都重复填 IP 和端口。HTTP 请求默认值填服务器名、端口、协议。所有 HTTP 请求里这三项留空就会继承它。将来换环境改一处就行。HTTP 信息头管理器放Content-Type: application/json、token这类公共头。注意这个元件不能跨线程组共享每个线程组放一份。HTTP Cookie 管理器有登录态的接口必备。它放在线程组下登录请求返回的 Set-Cookie 会被自动记住并带到后续请求上。这里有一个非常容易踩的坑信息头管理器的作用域。如果你把它放在测试计划根节点所有线程组都会继承如果放在某个 HTTP 取样器的子节点它只对那一个取样器生效。我见过有人把带 token 的头管理器误放到单个取样器下结果其他接口全部 401排查了半天以为是服务端问题。3.3 CSV 参数化与共享模式的坑想模拟不同用户登录就得参数化。用配置元件 → CSV Data Set Config准备一个users.csvusername,password user001,pass001 user002,pass002元件里填文件名、变量名username,password分隔符逗号。剩下的关键是共享模式这一项共享模式行为适用场景All threads全测试计划共享一个文件指针各线程各取一行不重复一次登录压测数据不重复使用Current thread group每个线程组独立读文件多线程组各自有一套数据Current thread每个线程独立读完整文件同一批数据要在每个并发用户里循环Identifier按线程组名字标识共享特殊编排场景选错共享模式会出现所有并发用户都在用同一个账号的情况这会让你的测试结果偏乐观因为服务端可能对这个账号有缓存。真正的并发压测应该用 All threads让每个线程拿到不同用户顺便还能顺带测出登录接口本身的性能。注意如果线程数大于 CSV 行数文件读完后 JMeter 会从头开始循环读默认行为如此。所以 CSV 至少要准备和并发数相当的行数不然还是会出现账号复用。3.4 关联取值JSON 提取器和正则的取舍有依赖的接口必须做关联。比如登录返回{token:abc123,userId:9527}后续请求要在头里带 token。做法是在登录请求下加后置处理器 → JSON 提取器变量名tokenJSON Path 表达式$.token匹配数字1取第一个匹配默认值留空或填一个明显错误的值方便定位失败JSON 提取器比正则表达式好用太多只要接口返回的是标准 JSON 就优先用它可读性和稳定性都好。正则在返回的是 HTML 或者结构不规整的文本时才上场。另外一个细节提取不到值的时候后续请求会带着字面量${token}发出去而不是报错。所以要么给默认值要么在后续请求上加断言否则你会看到一堆莫名其妙的失败请求还以为是接口挂了。3.5 断言别让 500 悄悄算成成功不加断言的 JMeter 脚本只看 HTTP 状态码就判定成功。有些服务返回 200 但 body 里是{code:500,msg:系统异常}这种情况 JMeter 会记为成功你的错误率报告就是假的。至少加两层第一层是响应断言检查 HTTP 响应码等于 200第二层是JSON 断言检查$.code等于 0 或者$.success等于 true。JSON 断言在 JMeter 4.0 之后是内置的不用装插件。如果业务逻辑更复杂比如要校验响应时间、要跨请求做校验用 JSR223 断言语言选 Groovy比 Beanshell 快得多官方也推荐// 校验响应码和业务码同时校验响应时间不超过 1 秒 def code vars.get(code) if (!0.equals(code)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(业务码异常: code) } if (prev.getTime() 1000) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(响应过慢: prev.getTime() ms) }prev是当前取样器的结果对象vars是线程变量。把这个断言放在线程组级别所有取样器都会继承。记住一条BeanShell 能用但新脚本一律用 JSR223 Groovy前者的解释执行开销在几百并发下会明显拖慢压测机。3.6 定时器常数吞吐量和集合点定时器决定请求之间的节奏不加定时器的线程会疯狂发请求测出来的不是业务真实压力。常用的两个常数吞吐量定时器按每分钟采样数控制发送速率。想压到每秒 500 次请求就填 30000单位是每分钟。这个定时器适合需要精确控制 TPS 的验收测试。同步定时器Synchronizing Timer模拟集合点等齐 N 个线程再一起发。用来制造瞬时峰值比如秒杀场景。填模拟用户组的数量为 100超时时间 5000 毫秒批次满了或者超时就放行。定时器的作用域和配置元件一样按树形结构继承放在线程组下面会影响组内所有取样器。如果你只想给某一个请求加节流就把它挂到那个取样器下。刚上手的人常常把定时器随便一放结果发现吞吐量怎么都上不去其实是自己把节奏限死了。4. 从能跑到测得准加压策略和执行方式脚本能跑通只是起点。同一份脚本用不同的加压方式跑结论可能完全相反。这一节讲的是怎么让测试结果具备参考价值。4.1 先定验收指标再动手写脚本我见过太多人先写脚本再想指标最后拿着一堆数字不知道该怎么判断。正确顺序是反过来先明确这次压测要回答什么问题。常见的验收口径有几种指标类型典型口径说明吞吐目标目标 TPS ≥ 2000按业务峰值换算出来的响应时间P95 ≤ 300msP99 ≤ 800ms用分位数而不是平均值错误率 0.1%包含业务码异常稳定性连续 2 小时指标不劣化关注内存泄漏和连接泄漏算出目标 TPS 的方式很简单日均请求量 × 峰值系数 ÷ 有效时间。比如日均 200 万次调用峰值系数取 3集中在 8 小时那么2000000 × 3 ÷ (8 × 3600) ≈ 208TPS。这个算法很粗糙但比拍脑袋填 1000 并发靠谱得多。4.2 阶梯加压、固定并发和稳定性测试的区别三种加压方式回答三个不同的问题别混用阶梯加压用来找拐点。用插件管理器装上 Custom Thread Groups用 Stepping Thread Group 配置成每 30 秒加 50 个线程一直加到 500观察 TPS 和响应时间曲线在哪里开始拐弯。拐点位置就是这个系统的容量水位。这个测试最有价值因为你能拿到一条完整的性能曲线而不是孤立的一个数据点。固定并发用来做验收。已知目标并发是 300那就用普通线程组固定 300 个线程跑 10 分钟看 P95 和错误率是否达标。这种跑法简单直接适合上线前的容量确认。稳定性测试用来抓泄漏。用 30% 到 50% 的容量水位连续跑 2 到 8 小时重点观察内存曲线是否单调上升、响应时间是否缓慢劣化、数据库连接数是否只增不减。很多隐藏问题只有长时间跑才会现形比如连接池没归还、本地缓存无限增长。4.3 命令行执行与 HTML 报告生成调好脚本之后保存为.jmx文件传到压测机上执行jmeter -n -t /opt/test/login.jmx \ -l /opt/test/result.jtl \ -e -o /opt/test/report \ -Jthreads300 -Jrampup30参数含义逐条解释一下-n是非 GUI 模式-t指定脚本-l是结果数据文件-e -o表示测试结束后自动生成 HTML 报告到指定目录。后缀的-J是给脚本传系统属性脚本里用${__P(threads,100)}就能读到这样同一份脚本可以在不同并发下复用不用每次改脚本。有个坑必须提醒-o指定的目录必须不存在或者为空。目录里已经有文件时会直接报错退出很多人第一次用会以为是参数写错了。另外-l的 jtl 文件也是一个很占空间的东西几千并发跑十分钟可能就是几百兆磁盘要提前规划。4.4 分布式压测什么时候值得上单台压测机的瓶颈一般出现在网络和 CPU 上大概能压出几千 TPS。再往上就得用分布式一台控制机加若干执行机。配置步骤不复杂——执行机跑jmeter-server控制机的jmeter.properties里写remote_hostsip1:1099,ip2:1099启动时用-R指定执行机列表。但分布式有几个隐藏成本很多人第一次踩会懵执行机的 JMeter 版本和插件必须和控制机完全一致CSV 数据文件必须每台机器上都有一份且内容一致所有机器的时钟必须同步对时不准会导致结果时间戳错乱控制机只是协调者它自己不产生压力。另外控制机汇总结果时会有网络开销执行机数量多的时候控制机反而可能成为瓶颈。5. 报告里的数字怎么看才不被误导跑完第一次压测你会得到一份聚合报告和一个 HTML 报告。这两份东西里的字段理解偏一个结论可能就反了。5.1 聚合报告字段的准确口径聚合报告的每一行对应一个取样器名字字段含义需要精确理解Average平均响应时间所有请求响应时间的算术平均。Median中位数50% 的请求响应时间低于这个值。90% Line / 95% Line / 99% Line分位数。90% Line 等于 200ms意思是 90% 的请求在 200ms 内返回剩下 10% 比它慢。Min / Max最小和最大响应时间。Max 受单个异常请求影响极大别拿它当指标。Throughput每秒完成的请求数含失败的请求。它和 TPS 概念接近但不完全等价。Error %断言失败的请求占比。有个容易搞混的点Throughput 和并发数不是一回事。300 个并发可能只压出 200 TPS也可能压出 3000 TPS取决于服务端处理能力。并发数是你施加的压力Throughput 是系统实际吐出的结果后者才是容量指标。5.2 平均响应时间是所有指标里最容易骗人的如果 100 个请求里有 99 个是 10ms1 个是 5000ms平均值是 60ms看起来还行。但对真实用户体验来说那 1 个慢请求才是问题。这就是为什么现在大家看 P95、P99 而不看平均值。我自己判断一个系统是否健康习惯看三个数的组合关系P99 和 P95 的比值、P95 和 Average 的比值。如果 P99 是 P95 的两倍以上说明有明显的长尾通常是锁竞争或者缓存击穿的表现如果 P95 和 Average 都很低但 Max 特别高那可能只是偶发的 GC 或者网络抖动不一定是系统性问题。这些判断需要结合服务端监控一起看光看 JMeter 报告会得出错误结论。5.3 把 JMeter 结果和服务端监控对齐看压测报告只是一个侧面。要和 GC 日志、数据库慢查询、连接池活跃数、Redis 命中率这些指标对时间轴看才能定位瓶颈在哪。比较典型的几个对应关系JMeter 响应时间抖动的时刻服务端如果同时有 Full GC那问题在内存和 GC。TPS 涨不上去但 CPU 没打满多半是某个线程池或者连接池被打满了请求在排队。错误率突然上升且伴随Connection refused通常是服务端连接队列溢出也就是 accept 队列满了。我通常会在压测开始前把服务端的监控面板打开放在另一个屏幕上压测过程中盯曲线出问题立刻截屏。事后对着 JMeter 的时间戳去翻监控数据成本高得多。6. 我踩过的坑和对应的处理办法前面讲的是方法这一节讲讲我实际被卡住过的地方。这些都是教程里不太会写、但一定会遇上的问题。6.1 压测机先扛不住的情况有一次压到 800 并发TPS 怎么都上不去我还以为是服务端有瓶颈查了两个小时才发现压测机的 CPU 已经 100%load 到了 30。压测机自己成了瓶颈测出来的数据全是假的。判断方法很简单压测过程中同时在压测机上开一个top看 JMeter 进程的 CPU 占用和系统 load。如果 CPU 超过 80%或者 load 超过核数的 2 倍先考虑换机器或者减少监听器。另外网络也要看用sar -n DEV 1观察网卡带宽有没有跑满千兆或万兆。压测机和服务端如果在同机房网络带宽一般是够的如果跨机房带宽和延迟都会成为干扰因素这时候要在报告里注明。6.2 端口耗尽和 TIME_WAIT 堆积高并发下压测机容易出现大量 TIME_WAIT 状态的连接最后报Cannot assign requested address。原因是每个请求都新建了 TCP 连接短时间消耗掉了几万个本地端口。解决办法有两个方向第一个方向是让 JMeter 复用连接。检查 HTTP 取样器的实现方式是不是 HttpClient4然后在jmeter.properties里调整连接复用时间httpclient4.time_to_live60000这个值控制连接的空闲存活时间调大之后连接可以重复使用端口消耗会大幅下降。第二个方向是调整压测机的内核参数扩大可用端口范围和加快 TIME_WAIT 回收。这些参数属于系统层面的调优需要结合实际情况评估不要照搬网上的文章直接改生产机器。6.3 监听器把磁盘写爆了新手最容易犯的错是在脚本里挂查看结果树和聚合报告这两个监听器然后跑非 GUI 压测。结果树会把每个请求的完整请求体和响应体都记下来几万请求下去就是几个 G 的文件磁盘直接爆掉压测也随之中断。正确做法是调试阶段保留结果树正式压测前全部删掉只用-l参数写 jtl 文件需要看详情时再事后用-g参数从 jtl 生成 HTML 报告。同时在jmeter.properties里确认这几项是 falsejmeter.save.saveservice.response_datafalse jmeter.save.saveservice.samplerDatafalse jmeter.save.saveservice.requestHeadersfalse jmeter.save.saveservice.responseHeadersfalse只保留必要字段jtl 文件的体积能小一个数量级。6.4 被测服务其实是被缓存扛住的有一次压测结果非常漂亮单机 3000 TPS响应时间稳定在 20ms。上线之后到了真实流量下却频繁超时。回头查才发现压测用的参数一直是同一批商品 ID服务端缓存命中率接近 100%等于一直在压缓存没有真正打到数据库。避免这个假象的办法是让参数具备足够的离散度。用 CSV 准备几千条不同的 ID让每次请求拿到不同的数据或者在压测过程中主动把缓存清掉一部分观察性能变化。更彻底的做法是同时压一个缓存命中场景和一个缓存穿透场景两个结果都记录下来。真实容量应该以更差的那个为准。6.5 分布式执行时的同步与数据一致问题分布式压测里最烦人的问题是数据不一致。控制机上的 CSV 和执行机上的版本不同步会导致部分请求用的账号不存在执行机时区或者时钟有偏差会让结果时间戳错位聚合时排序全乱。我的做法是把 CSV、JMX 脚本、插件包打包成一个版本化的目录用配置管理工具统一分发到所有执行机压测开始前先跑一轮 10 并发的冒烟确认所有执行机都能正常出结果同时把执行机的 NTP 同步打开确保时钟偏差在毫秒级。最后再分享一个我现在的习惯每次压测都会把脚本、jtl、HTML 报告、服务端监控截图和一份简短的结论文档放进同一个目录归档文件名带上日期和版本号。半年后再有人问这个接口当时压出多少你不用翻聊天记录直接找目录就行。压测这件事的价值不只在跑的那几个小时更在于你留下了什么可以对照的东西。