ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JMeter 请求重放测试实战:从真实请求到自动化回归

JMeter 请求重放测试实战:从真实请求到自动化回归

做 JMeter 的人,十有八九都是冲着压测去的——开多少线程、跑多少循环、TPS 能打多高。但我这些年用得最多的反而是它最被低估的一个场景:请求重放测试。字面上看就是把真实用户产生过的 HTTP 请求原样再发一遍,听起来很简单,实际做起来却有不少讲究。它和你平时理解的压测完全是两个思路。

说个实际例子:某系统上线新版本后,运营反馈某个查询接口响应时间从 200ms 飙到 3 秒,前后端排查了两天没结论。当时我把线上网关日志里该接口的一段真实请求摘出来,用 JMeter 重放了一遍,立刻发现新版代码在特定请求头组合下会走错逻辑分支,性能退化是次要的,功能都是错的。整个定位过程不到半小时。这类问题靠造数据、写脚本很难碰到,但重放真实请求可以。

这篇文章我会把 JMeter 做请求重放的全流程讲透:怎么拿到真实请求、怎么处理登录态和动态参数、怎么配置线程组和断言、怎么在自动化流程里跑起来,以及我踩过的问题汇总。适合想用 JMeter 做回归验证、问题复现、发布后冒烟测试的测试和开发同学,无论你之前有没有玩过 JMeter 都能照着做。

1. 请求重放到底在解决什么问题——先想清楚再动手

1.1 重放测试和压测是两条路线

压测关注的是“系统能不能扛住 N 并发”,重放关注的是“系统对同一批请求的处理结果是不是还和之前一样”。两者用的工具相同,目标不同,直接导致脚本设计方式完全不同。

压测的核心是制造压力:线程数要高、循环次数要多、聚合报告要好看。重放的核心是保持原真:请求要尽可能和线上真实流量一致,一次算一次,不追求并发。

很多人上来就用压测模板做重放,给一个线程组配 50 线程、循环 10 次,跑完发现接口暴露出各种异常,以为是程序出 bug 了,其实是把重放做成了小规模压测,同一个请求带了 50 份并发,服务端限流或者日志错乱都有可能。重放如果只是为了验证功能正确性,脚本应当尽量保持“一个用户、一条链路、一次完整请求序列”的节奏。

1.2 重放最适合的四个场景

重放绝不是压测的替代品,它有自己不可替代的场合。根据我的经验,以下场景最值得用重放方案。

第一是版本回归验证。发版前截取生产环境核心链路的真实请求,发版后再重放一遍,对比响应结构和耗时。这种方式比手工点点点覆盖面广得多,又能避免构造数据失真。第二是缺陷复现。测试环境复现不了的问题,拿着生产请求往测试环境一放,大概率能复现,比开发猜参数要快。第三是数据迁移核对。数据库迁移或缓存重构后,用旧请求重放,看响应结果是否一致。第四是接口兼容性验证。下游服务升级字段调整后,用历史请求打一遍,看有没有兼容性破坏。

这四个场景核心都是“比较”,不是“施压”。

注意:重放测试请求量通常不大,但它能精准定位“行为变化”。如果你的目标是验证系统在某个真实用户轨迹下表现是否正常,而不是压到极限,那重放是投入产出比最高的手段。

1.3 一次重放和持续重放的区别

重放可以是临时跑一次,比如出了问题拿请求复现一下。但更常用的是持续集成式的重放——每次代码合入后自动把留存的核心请求集跑一遍,当作一种“线上请求回归”。

我建议手上常备一个请求库,把业务核心接口的真实请求保存下来,按模块分文件夹管理。每次发布前用 JMeter CLI 批量重放,发现的差异直接通知相关人员。这个动作看起来不起眼,但实际价值非常高,因为它把“线上行为验证”提前到了发布流程内,避免了很多生产事故。

2. 拿到真实请求的几种方式——录制不是唯一选择

要重放,先得有请求。最理想的是从线上环境拿到真实流量。我不太建议手工在测试环境点一遍然后录下来,那样录出来的请求大多是用测试数据打出来的,真实性和线上差距不小。下面几个方式我按推荐程度排个序。

2.1 方式一:从网关日志或流量平台导出 HAR

现在稍微正规一点的系统,前面都会挂网关或接入流量平台。网关的访问日志里通常记录了完整的请求行、请求头、请求体。导出这些记录后,整理出目标接口的样本,转成 HAR 格式(HTTP Archive 格式),JMeter 可以直接导入 HAR 文件生成测试计划。

没有流量平台的,可以在 Nginx 访问日志里加一段 log_format,把 $request_body 打出来。别小看这一步,那条日志在关键时刻能救命。我在某项目里就是靠 Nginx 日志里的 request_body 抓到了用户报错的完整请求体,从而在测试环境稳定复现了线上问题。

2.2 方式二:JMeter HTTP 代理服务器录制

JMeter 自带录制功能,本质是一个 HTTP 代理。浏览器把流量代理到 JMeter 监听的端口,JMeter 记录下所有经过的请求,生成测试脚本。

具体步骤:

  1. 在 JMeter 中新建“测试计划”,添加线程组。
  2. 在工作台或测试计划下添加“HTTP(S) 测试脚本记录器”(原本叫 HTTP 代理服务器)。
  3. 端口设置成 8080,目标控制器选到线程组。
  4. 浏览器或系统代理指向 127.0.0.1:8080。
  5. HTTPS 请求需要先导出 JMeter 的 CA 证书安装到本机受信任的根证书列表。
  6. 点击启动按钮,然后在浏览器里操作被测系统各个页面,操作完成后停止录制。

录出来的脚本通常比较“脏”——静态资源请求一大堆,还有 Cookie、Referer 等头信息。做重放之前要过滤:在记录器里配置“排除模式”,把 .js、.css、.png、.ico、.woff 这些静态资源排除掉;也可以在录制后手工删掉多余的取样器。

2.3 方式三:从浏览器开发者工具手动提取

如果只是复现一两个接口,浏览器 F12 是最快的。在 Network 面板里找到目标请求,右键选择“Copy as HAR”,把内容保存成文件,再用 JMeter 打开。不需要完整录制,精准提取单个请求链即可。

用 HAR 导入时要注意,JMeter 对 HAR 里的多项内容只做基础映射:URL、方法、请求头、请求体会保留,但一些动态逻辑如 JS 生成的头、WebSocket 连接无法生成。导入后需要手工检查和调整。

2.4 拿到请求后的清理三件事

无论用哪种方式,拿到原始请求后都必须做三件事,否则重放效果会大打折扣。

第一,过滤非必要请求。图片、字体、埋点、统计类请求在重放时一概去掉,它们只会带来噪音。第二,区分“必须原样”和“每次变化”的字段。固定不变的路径、参数原样保留;带有时间戳、随机数、签名、Token 的字段需要做参数化或关联处理。第三,去除可能影响重放结果的头字段。比如 Referer 和 Origin 需要保留,因为它们可能触发服务端校验;而 Accept-Encoding 可以删掉,避免响应内容是压缩格式导致结果可读性差。

经验:真正决定重放效果的不是录制的完整性,而是清理的彻底性。静态资源堆在脚本里只会拖慢执行速度,动态参数处理不好则会直接导致重放失败。我见过太多人录完直接用,结果跑到第 3 步就报 401,其实就是没处理登录态。

3. 让重放脚本具备“可重复性”——动态参数和关联是关键

3.1 为什么重放会失败:动态值在作怪

一个用户登录系统后,服务端通常会下发 Token 或 Session ID,后续每个接口请求都带着这个凭证。如果你录制时把当时的 Token 写死在脚本里,过一会儿 Token 过期了,重放自然失败。

再比如表单里常见的 CSRF Token,每次刷新页面都会变化。还有订单号、时间戳、UUID,这类值如果写死,重放时服务端就会认为参数非法。这个问题不解决,脚本跑一次行,第二次就挂。

解决思路分两种:一种是把动态值提取出来,从上一个请求的响应中获取,再注入到下一个请求中,这叫关联;另一种是参数化,把动态值替换成变量,每次从 CSV 文件或函数中取值。

3.2 关联的两种常用提取器

JMeter 里最常用的是“正则表达式提取器”和“JSON 提取器”。

正则提取器适合响应是 HTML 或非结构化文本的场景。比如登录接口返回的 JSON 里有个"token":"abc123"字段,可以在登录请求下添加后置处理器“正则表达式提取器”,配置:

  • 引用名称:token
  • 正则表达式:"token":"([^"]+)"
  • 模板:$1$
  • 匹配数字:1

这样后续请求的 HTTP Header 里写Bearer ${token},就能动态取值。

如果响应是 JSON,推荐用 JSON 提取器,写法更直观。比如提取data.tokens.access_token,就配置 JSONPath 表达式为$.data.tokens.access_token,引用名称设为 accessToken,后面直接${accessToken}引用。

注意:关联没有“一键完成”的银弹。你必须先跑通一遍录制脚本,看到哪个请求失败,再倒推它失败是不是因为某个动态值没关联。有一个笨但有效的方法:把每个请求的响应都加上“查看结果树”查看,跑一遍,从第一个失败请求开始逐个排查。

3.3 用 CSV 参数化重放多组数据

如果需要重放不同用户的请求,不一定要录制多条脚本,把用户信息放进一个 CSV 文件,用 CSV 数据文件配置读取。

配置要点:

  • 文件名指向本地或 CI 机器上的 CSV 文件
  • 变量名称和后续请求的参数名对应,比如 mobile, password
  • 分隔符用英文逗号
  • 线程组里有几个线程就读取几行,如果要循环读取记得配置“共享模式”

这里有一个容易被坑的点:线程组如果循环次数大于 CSV 行数,默认情况下会继续循环读取 CSV 文件的内容,等于重复使用同一批数据。重放时如果接口有幂等校验,重复请求会被拦截,所以要么给 CSV 多准备几行数据,要么把线程组的循环次数控制好。

3.4 时间戳和随机数的处理

有些接口要求参数里有时间戳,比如timestamp=1699999999。直接在 JMeter 里用${__time(/1000,)}函数生成即可,它会取当前时间的秒级时间戳。如果需要毫秒级,用${__time(,)}。

随机数用${__Random(1000,9999,rand)}生成。要注意随机数也是会重复的,在单次重放周期内一般够用,但如果接口对唯一性要求很高,建议用${__UUID()}生成 UUID。

4. 重放脚本的几个核心配置——线程组、超时和断言

4.1 线程组参数怎么定

重放脚本里,我把线程组通常设为:

  • 线程数:1 或 2
  • 循环次数:1
  • 调度器:不勾选

原因很简单,重放是要按真实用户行为顺序执行请求,而不是让多个虚拟用户同时发起相同请求。如果只是在测试环境做功能回归,1 个用户完全足够;如果希望重放时能稍微带一点并发验证,也最多开到 2~3,再大就变味了,会引入并发因素,问题不好定位。

Ramp-Up Period 保持默认 1 秒即可。如果线程数大于 1,可以在 Ramp-Up 里稍微拉开间隔,比如 2 个线程设成 5 秒,模拟先后启动的效果。

4.2 HTTP 请求默认值减少重复配置

如果一个测试计划里要重放同一域名的多个接口,给每个取样器都填一遍域名和端口纯属浪费。直接在测试计划下添加“配置元件 → HTTP 请求默认值”,填上协议、服务器名称或 IP、端口号。后面的 HTTP 请求取样器就只需要填路径和请求体。

这个配置还有个额外好处:当环境切换时,只改一处默认值就能把整个脚本指向新环境。比如从测试环境切到预发环境,只改服务器名称即可。

4.3 超时设置必须显式控制

重放时最忌一个请求挂起半天不动,整个脚本卡死在那里。在 HTTP 请求默认值里把超时时间设成明确值:

  • 连接超时:3000ms
  • 响应超时:5000ms

连接超时设太短不行,比如跨网段访问时握手超过 3 秒是常事,会把正常请求误判失败。响应超时设 5 秒是一个比较均衡的经验值,如果接口本身响应就是秒级,再调大一些。重放的目的是判断系统“通不通、对不对”,不是统计长尾耗时,超时后快速结束请求让脚本跑完,比一直死等有价值得多。

注意:重放时如果响应超时设得过大,整个脚本可能因为一个慢接口拖到几分钟,严重拖慢发布验证节奏。我一般会把“遇到超时直接标记失败并继续”这条思路贯穿始终,让它快速跑完,输出失败清单,而不是中断在第一个异常上。

4.4 断言怎么配置才不误报

断言是对响应结果做校验,重放后没断言等于没检查。常用三种:

响应断言可以检查响应文本是否包含某个业务关键字,比如登录接口响应里包含"success":true,或者检查响应码是否为 200。如果只是检查响应码,你可能会漏掉很多业务异常——服务端完全可以在返回 200 的同时在 JSON 里携带"code": 50001的错误码。所以响应断言里我习惯勾选“响应文本”,用“包含”匹配关键业务字段。

JSON 断言用来对 JSONPath 提取的字段做判断,例如某个字段值是否符合期望,适合响应是纯 JSON 的接口。

持续时间断言检查请求耗时上限,比如设定 3000ms,超过即失败。这个在重放时可以辅助识别性能异常退化。

经验之谈:断言宁可多配,不要少配。很多人只加响应码断言,结果重放全绿,实际业务结果一塌糊涂。最少也要加一个“响应文本包含业务成功标志”的断言。我之前就见过接口由于 NPE 返回了 500 但响应体里没有业务字段的情况,这时响应码断言其实也能抓出问题;但更常见的是 200 + 业务失败,这时候没有文本断言就是全盲。

5. 从手动重放到自动化回归——CLI 跑法和报告解读

5.1 在命令行中执行重放脚本

GUI 模式适合调试脚本,跑回归验证一定要用 CLI 模式。JMeter 5.x 及以上在 Windows 上执行 bin/jmeter.bat,Linux 或 macOS 执行 bin/jmeter.sh。

一条典型命令:

jmeter -n -t replay.jmx -l result.jtl -e -o report/

参数含义:

  • -n:非 GUI 模式
  • -t:指定测试计划文件
  • -l:输出结果文件(JTL 格式)
  • -e:生成 HTML 报告
  • -o:报告输出目录

如果需要覆盖脚本里的变量,比如切换环境地址,还可以追加-Jhost=192.168.1.100,脚本里用${__P(host,127.0.0.1)}读取。

CLI 模式跑完后,重点是看两样东西:result.jtl 中标记为 failure 的请求,以及 HTML 报告里的错误率、平均响应时间。重放测试的错误率理论上应该是 0,任何失败都必须解释原因。

5.2 把重放塞进发布流水线

在持续集成中执行重放测试,不需要复杂的复杂配置,一条命令搞定。在流水线里加个步骤:

jmeter -n -t pipeline-replay.jmx -JbaseUrl=https://pre-release.example.com -l target/replay-result.jtl -e -o target/report

然后再加一步检查 JTL 文件里有没有失败记录。可以参考以下 shell 命令片段:

if grep -q ",false," target/replay-result.jtl; then echo "Replay test found failures." exit 1 fi

这个方案的优点是:不需要专门的测试平台,任何一个能运行命令的构建节点都能跑。请求库里的 jmx 文件入库,每次发布自动触发重放,比起开发自测更加客观稳定。

5.3 重放报告的对比分析

重放一般都会跑两轮:发布前一轮,发布后一轮。跑完之后对比两份报告,重点看三个方面。

平均响应时间是否量级变化。如果发布前 300ms,发布后 3s,基本可以断定新版本引入了性能问题,即使功能正确也需要关注。错误数是否有新增。如果发布前为 0,发布后出现非零失败,需要逐条排查是请求本身问题还是程序问题。响应体结构是否一致。这个用肉眼对比不方便,建议在两个请求后各加一个“响应断言”,断言同一个业务关键字必须存在,这样结构变化会被自动标记。

我见过比较典型的例子:新版本把接口返回的字段名从user_name改成了username,老客户端直接解析失败。这种问题靠压测很难发现,重放配合断言却能在上线前敏锐地抓到。

6. 我踩过的重放测试的坑——踩雷实录和排查清单

6.1 录制时没有排除静态资源导致脚本慢得离谱

最初做录制时,我图省事没加排除模式,结果录出来一个页面请求带着 60 多个 JS/CSS 请求。重放完一个流程要三四分钟,大部分时间花在毫无意义的静态资源加载上。后来我学乖了,录制前先配好排除模式:.*\.js.*、.*\.css.*、.*\.png.*、.*\.ico.*,脚本干净很多。

排除正则放记录器里的“排除模式”列表,支持正则表达式。简单规则:静态资源后缀统一写成.*\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|ttf)(\\?.*)?$。

6.2 登录态失效导致大面积 401

某次做线上请求回归,第一批请求全部通过,第二批开始全部 401。开始怀疑是服务端问题,后来才想到录制时的 Cookie 是临时的,几小时后过期。从那以后,我录制的脚本里只要涉及用户体系的流程,都会先跑一次登录请求,用正则提取器把返回的 Token 提取出来,加到后面所有请求的 Header 里。

如果业务系统用的是 Header 里带 Token 的方式,在登录请求下添加一个“正则表达式提取器”或“JSON 提取器”,提取后把变量写到 HTTP 请求默认值的“HTTP Header Manager”里即可。注意“HTTP Header Manager”要放在取样器前面,变量引用${token}会自动替换。

6.3 时间戳参数不更新导致签名校验失败

有些接口的签名机制会把时间戳参与签名计算,比如sign = md5(appkey + token + timestamp)。如果你录的是完整的 sign,重放时时间戳一变,sign 就不对了。

常见处理办法是:用 JMeter 函数生成当前时间戳,替换掉签名串里的 timestamp 部分,但 sign 本身没法动态算——因为签名算法在服务端或 App 内部。这种情况下我有一个建议:不要重放带完整签名的接口,而是重放签名校验之后的业务逻辑接口。如果非要从签名接口开始,就需要单独写一个 BeanShell 或 JSR223 脚本模拟签名计算,工作量会大一些。

6.4 中文响应乱码导致断言失效

响应体里有中文内容时,JMeter 默认按 ISO-8859-1 解码,断言里的中文字符匹配不上。解决办法是在 HTTP 请求取样器的“响应编码”里指定 UTF-8,或者在测试计划里加一个 JSR223 前置处理器设置默认编码:

System.setProperty("file.encoding", "UTF-8")

这行代码要放在“测试计划 → 配置元件”。需要注意,file.encoding 修改未必对所有场景生效,最稳妥还是在取样器层面配置响应编码为 UTF-8。

6.5 重放后响应正常但数据库无数据

这是最隐蔽的一类问题:接口返回 200,断言也通过,但数据库里没有产生数据。后来发现是重放时用了历史请求里的 ID,服务端判断主键冲突,直接返回“已存在”或者静默跳过。

遇到这种情况,要把断言加细:除了响应文本,还要断言数据库层面的落库数量。JMeter 可以通过 JDBC 请求验证数据库结果,也可以在响应断言里检查返回的数据 ID 是否是新生成的。

6.6 常见问题速查表

现象可能原因排查方向
脚本跑几步后全部 401 / 403登录态失效或 Token 未关联检查登录请求响应里的 Token 提取是否正确,Header 引用变量是否生效
响应超时网络代理残留或环境不通检查 JMeter 所在机器是否需要配置代理,检查目标环境白名单
断言失败但接口响应码是 200断言匹配串不合适打印响应内容,确认业务成功字段是否存在
中文乱码响应编码未指定 UTF-8在取样器里配置响应编码,或 JSR223 中设置编码
请求顺序乱了HTTP 请求取样器默认并行执行勾选线程组里的“同一线程上的取样器按顺序执行”
参数化数据重复CSV 文件行数不够线程循环次数加数据行数,或调整线程循环次数

7. 重放测试脚本的结构模板——直接抄作业

这里我给你一个可以直接扩展的 JMeter 测试计划骨架,按照这个结构组织脚本,维护成本低很多。

测试计划(名称:replay-订单链路) ├── 测试片段: 登录获取Token(可选) ├── 线程组: 重放主流程(线程数=1,循环=1) │ ├── HTTP请求默认值(协议=https, 服务器=${host}) │ ├── HTTP信息头管理器(Authorization: Bearer ${token}) │ ├── CSV数据文件配置(用户数据.csv) │ ├── 取样器1: 登录 │ │ └── 正则表达式提取器: token │ ├── 取样器2: 查询订单列表 │ │ └── 响应断言: 包含"orderId" │ ├── 取样器3: 创建订单 │ │ ├── 时间戳函数: ${__time(/1000, timestamp)} │ │ ├── 响应断言: 包含"success" │ │ └── JSON提取器: orderId │ └── 取样器4: 按orderId查询订单详情 │ └── 响应断言: 包含"status": "PAID" └── 监听器: 查看结果数(调试用), 简单数据写入器

把 host、端口、超时时间都做成 JMeter 属性,这样所有环境共用同一个 jmx,切换环境只需要传不同的 -J 参数。脚本文件本身保持不变,配合流水线使用即方便又不易出错。

8. 重放之后的校验——不能只看请求是否成功

重放测试容易让人觉得“请求返回 200 就行”,实际上远远不够。我曾经重放一个下单链路,所有接口返回 200,但订单状态一直停留在 INIT,问题出在新版本改了消息队列的 topic,下单请求虽然落库了,后续的支付回调消费不到。

所以在重放之后,一定要做结果闭环校验。常用手段是:

  1. 最后追加一个查询接口,断言关键业务字段进入预期状态。
  2. 若系统有对账或审计接口,重放结束后再拉一次数据核对。
  3. 在响应断言里同时校验“响应码 + 业务码 + 关键数据字段”。

把这个闭环纳入你的重放脚本模板,不要贪图“快速跑完”。重放快的意义在于尽早发现问题,而不是让一个问题隐藏到上线后才爆发。

提示:如果你负责的系统有事务性链式调用,重放后务必检查整条链路的终态,不要只看中间某一个接口的响应。这是重放脚本设计中最容易被忽略的一环。

9. 最后分享一点实际经验

请求重放测试,说到底是把线上真实请求变成一份“可重复执行的黑盒用例”。它不追求复杂的测试设计,核心价值在于真实性和可对比性。我个人的习惯是,每次重放之前先问自己三个问题:请求来源是不是真实流量?动态值处理干净没有?校验断言能不能发现业务异常?这三个问题都过了,重放结果才有参考意义。

另外提一个提高效率的小技巧:请求库里的历史请求不要只留一份,尽量给每个核心接口留存多个采样,分散在不同的参数组合之间。时间长了,这个库就是最有价值的“行为基线”。每次版本变更后跑一遍这个基线,系统表现是否退化一目了然。

如果你还没用过 JMeter 做重放测试,强烈建议从一条核心链路开始。录一段,清一下,配好 Token 关联,加一个响应断言,然后 CLI 跑一遍。做完这条,你就能体会到为什么它比手工回归靠谱得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进