1. 为什么“并发测试”不是点几下就能出结果的幻觉?
很多人第一次打开 Postman 的 Collection Runner,看到“Iterations”和“Delay”两个输入框,心里就默认:“填个100,点Run,不就模拟100个用户同时访问了吗?”——我去年在给一家电商客户做接口压测支持时,也这么信过。结果跑完发现:所有请求几乎按毫秒级顺序串行发出,响应时间曲线平得像尺子,TPS(每秒事务数)稳定在3.2,而他们生产环境真实峰值是1200+。后来翻日志才发现,Postman 根本没发出去并发请求,它只是用单线程循环执行了100次,每次等上一个结束才发下一个。这根本不是并发,是“假装并发”。
Postman 的本质是一个HTTP调试与协作工具,不是压测平台。它没有内置线程池、连接复用调度器、请求节流控制器或分布式负载生成能力。所谓“并发”,在 Postman 语境里,指的是在单次运行中,以尽可能短的时间间隔发起多个独立请求实例——但它受限于 Node.js 运行时的单线程事件循环、系统 TCP 连接数限制、DNS 解析缓存策略,以及你本地机器的 CPU/内存/网络栈吞吐能力。换句话说,Postman 能帮你验证“接口是否支持并发调用”,但无法真实复现“5000人同时抢券”的流量洪峰。它适合的是:接口健壮性初筛、临界状态触发(如库存扣减竞争)、链路超时配置验证、基础限流策略冒烟测试。如果你的目标是性能基线建模或容量规划,请务必在 Postman 验证通过后,无缝切换到 JMeter 或 k6。这不是功能贬低,而是工具边界认知——就像用 Photoshop 做排版不如 InDesign,不是 PS 不好,是它压根没设计这个使命。
关键词“Postman 并发测试”背后的真实需求,从来不是“怎么让 Postman 变成 JMeter”,而是:“如何用我手头已有的、团队全员都会的 Postman,在开发联调阶段,低成本、高效率地暴露那些只有并发场景才会浮现的缺陷?”——比如数据库死锁、Redis 缓存击穿、全局锁争用、Token 刷新冲突、幂等校验失效。这些缺陷,90% 在单请求测试中完全隐身。所以这篇指南不教你“怎么伪造高并发”,而是带你把 Postman 这把瑞士军刀,拧到它最擅长的螺丝口上:用最小认知成本,撬动最大问题暴露率。
2. 并发能力的底层制约:Node.js 运行时与操作系统的真实瓶颈
要真正理解 Postman 能做什么、不能做什么,必须掀开它的底裤——看它跑在哪。桌面版 Postman 是基于 Electron 构建的,核心逻辑运行在 Chromium 渲染进程 + Node.js 后端进程的混合环境中。Collection Runner 的执行引擎,本质上是 Node.js 的http模块(v18+ 后部分迁移到undici)发起 HTTP 请求。这就决定了它的并发行为,完全受制于 Node.js 的 I/O 模型和操作系统的网络栈。
2.1 Node.js 的连接池与默认并发上限
Node.js 的http.Agent默认启用连接池,maxSockets参数控制每个 host:port 组合最多复用多少个 TCP 连接。默认值是 Infinity——听起来很美?错。这是个陷阱。Infinity 在实践中会被操作系统接管,最终受限于ulimit -n(Linux/macOS 文件描述符上限)和 Windows 的MaxUserPort注册表设置。我在一台 16GB 内存的 MacBook Pro 上实测:ulimit -n默认为 256,意味着即使你设了 1000 次迭代,Postman 最多只能同时维持 256 个活跃 TCP 连接。超出的请求会排队等待空闲 socket,形成事实上的串行化。
你可以用以下命令查自己机器的极限:
# macOS/Linux ulimit -n # Windows (PowerShell) netsh int ipv4 show dynamicport tcp提示:不要盲目调高
ulimit。文件描述符耗尽会导致整个系统网络异常(Chrome 打不开网页、iTerm 连不上 SSH)。安全提升建议:临时提高到 2048(ulimit -n 2048),并确保你的应用代码显式调用agent.destroy()释放连接。
2.2 DNS 解析:那个被所有人忽略的串行瓶颈
更隐蔽的瓶颈在 DNS。Node.js 的dns.lookup()方法是同步阻塞的(尤其在老版本),而dns.resolve()是异步的但默认不启用。Postman 默认使用lookup,这意味着:前100个请求,如果目标域名没进系统 DNS 缓存,它们会排队等第一个 DNS 查询返回,再批量发起 TCP 握手。我曾用 Wireshark 抓包验证过:100次迭代的 Collection Runner,前 3 秒全是 DNS 查询报文,之后才开始发 HTTP。这直接让“并发”变成“分批串行”。
解决方案是强制 Postman 使用异步 DNS 解析。虽然 Postman UI 不提供开关,但你可以通过修改其底层配置生效。找到 Postman 的用户数据目录(macOS:~/Library/Application Support/Postman/,Windows:%APPDATA%\Postman\),编辑config.json,添加:
{ "request": { "dns": { "useAsync": true, "cache": true } } }重启 Postman 后,DNS 查询将并行化,且结果缓存 5 分钟。实测同一套 100 次迭代,DNS 阶段耗时从 3200ms 降至 180ms。
2.3 TCP TIME_WAIT 与端口耗尽:高频短连接的隐形杀手
当你用 Postman 发起大量短连接(即每次请求都新建 TCP 连接,不复用),操作系统会在连接关闭后,将该 socket 置于TIME_WAIT状态,持续 2×MSL(通常 60-120 秒)。这是 TCP 协议保证可靠性的必要机制,但副作用是:本地可用端口被快速占满,新连接失败,报错Error: connect EADDRNOTAVAIL。
验证方法:运行netstat -an | grep TIME_WAIT | wc -l(macOS/Linux)或netstat -an | findstr TIME_WAIT(Windows)。如果数字超过 30000,基本可以判定是端口耗尽。
根本解法有两个:
- 启用 HTTP Keep-Alive:在 Postman 请求的 Headers 中手动添加
Connection: keep-alive,并在 Pre-request Script 中复用 Agent(需脚本支持,见后文); - 调整系统参数(仅限测试机):Linux 下可临时执行
sysctl -w net.ipv4.tcp_tw_reuse=1,允许 TIME_WAIT socket 重用于新连接(需net.ipv4.tcp_timestamps=1开启)。
注意:
tcp_tw_reuse仅适用于客户端主动关闭连接的场景,且存在极小概率的旧数据包混淆风险,严禁在生产服务器启用。这只是帮你跑通本地测试的权宜之计。
3. Collection Runner 的正确打开方式:超越“填数字”的四层配置逻辑
Collection Runner 是 Postman 并发测试的唯一入口,但它的四个核心参数(Iterations, Delay, Data, Environment)绝不是孤立存在的。它们构成一个有机的并发策略控制系统,每一层都解决一类特定问题。我见过太多人只调 Iterations,结果测了个寂寞。
3.1 Iterations:不是“用户数”,而是“请求实例总数”
这是最大的认知误区。“Iterations = 100” 不代表 100 个用户,只代表 Postman 会执行 100 次这个请求(或整个集合)。如果集合里有 5 个请求,100 次迭代就是 500 次 HTTP 调用。真正的“并发度”由 Delay 和系统能力共同决定。
关键技巧:用 Iterations 控制总负载量,而非并发强度。例如,你要验证接口在 1000 次请求下的错误率,就设 Iterations=1000;若想观察 50 并发下的响应时间分布,就设 Iterations=50,并配合极小 Delay(如 1ms),再辅以 Pre-request Script 控制实际并发窗口。
实操心得:永远先用 Iterations=10 做快速冒烟。如果这 10 次都失败,说明接口连基本可用性都不满足,没必要上 1000。我坚持“10次原则”:任何新接口上线前,必须用 Postman 跑通 10 次不同参数组合的并发,覆盖正常、边界、异常三类场景。
3.2 Delay:控制节奏的“节流阀”,而非“休息时间”
Delay 的单位是毫秒,但它的真实含义是“前一个请求发送后,等待多少毫秒,再发送下一个请求”。注意,是“发送后”,不是“响应后”。这意味着:如果 Delay=100ms,而第一个请求耗时 800ms,第二个请求会在 t=100ms 时发出,此时第一个请求还在路上,二者自然并发;但如果 Delay=1000ms,而第一个请求只耗时 200ms,第二个请求就要等到 t=1000ms 才发,中间空等 800ms,彻底串行。
因此,Delay 的设定必须基于你对目标接口 P95 响应时间的预估。公式很简单:
目标并发数 ≈ Delay / P95响应时间例如,P95 响应时间是 200ms,你想模拟 5 并发,Delay 应设为 200ms / 5 = 40ms。实测中,我通常取 P95 的 0.8 倍作为基准 Delay,再微调。
避坑提醒:绝对不要设 Delay=0。Node.js 的事件循环无法真正实现零延迟调度,0 会退化为最小调度粒度(通常 1ms),且极易触发 V8 引擎的垃圾回收暂停,导致请求堆积。最低安全值是 1ms。
3.3 Data 文件:让并发测试从“静态”走向“真实”的关键跃迁
用同一个参数(如user_id=123)发 100 次请求,测的只是“单用户重复操作”,不是“多用户并发”。真正的并发缺陷,往往藏在用户隔离的边界上:库存扣减时 A 用户和 B 用户同时读到剩余 1,都以为能买;优惠券发放时,A 和 B 同时生成了相同 ID 的券。
Data 文件(CSV/JSON)是 Postman 提供的变量注入机制。一个典型的users.csv文件内容如下:
user_id,token,product_id 1001,abc123,prod_a 1002,def456,prod_b 1003,ghi789,prod_a ...在 Collection Runner 中选择此文件,Postman 会为每次迭代分配一行数据,自动替换请求中的{{user_id}}、{{token}}等变量。
高级技巧:动态生成 Data 文件。与其手动写 1000 行 CSV,不如用 Python 脚本生成:
import csv import secrets with open('load_test_data.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['user_id', 'auth_token', 'sku']) for i in range(1000): writer.writerow([ f"user_{i+10000}", secrets.token_urlsafe(16), f"sku_{i % 50}" # 循环 50 个商品,制造热点 ])这样生成的数据具备真实业务特征:用户 ID 分散、Token 唯一、商品 ID 有热点(模拟爆款抢购)。
3.4 Environment:隔离测试环境的“安全气囊”
并发测试必须与生产环境物理隔离。Environment 变量让你一套请求脚本,切换不同 Base URL、API Key、Mock 开关。创建一个load-test环境,设置:
base_url:https://api-staging.yourcompany.comauth_header:Bearer {{staging_token}}enable_mock:true
然后在请求的 Authorization 或 Headers 中引用{{base_url}}和{{auth_header}}。这样,当你要切到预发环境时,只需在 Runner 中换一个 Environment,无需修改任何请求体。
关键经验:在 Environment 中定义一个
concurrent_mode变量,值为true。在 Pre-request Script 中检查它,如果为 true,则自动开启日志埋点或跳过非核心校验,避免测试流量污染监控大盘。这是很多团队忽略的“测试友好性”设计。
4. Pre-request Script 与 Tests:用代码编织并发测试的神经网络
Collection Runner 的 GUI 参数只能解决“发多少、隔多久”的粗粒度问题。真正的并发测试深度,藏在 Pre-request Script(请求发送前执行)和 Tests(响应返回后执行)这两段 JavaScript 代码里。它们是 Postman 的“大脑”,让自动化测试从线性脚本升级为智能探针。
4.1 Pre-request Script:构建并发上下文的“初始化引擎”
Pre-request Script 在每次迭代开始前运行,是注入动态逻辑的最佳位置。以下是三个实战必备模板:
模板1:动态 Token 刷新(防过期)
// 检查 token 是否即将过期(假设有效期 1 小时) const now = Date.now(); const expTime = pm.environment.get("token_expires_at"); if (!expTime || now > (expTime - 300000)) { // 提前 5 分钟刷新 const refreshToken = pm.environment.get("refresh_token"); pm.sendRequest({ url: pm.environment.get("base_url") + "/auth/refresh", method: 'POST', header: { 'Content-Type': 'application/json' }, body: { mode: 'raw', raw: JSON.stringify({ refresh_token: refreshToken }) } }, function (err, res) { if (err) { console.error("Token refresh failed:", err); return; } const data = res.json(); pm.environment.set("access_token", data.access_token); pm.environment.set("token_expires_at", Date.now() + data.expires_in * 1000); }); }模板2:请求 ID 注入(全链路追踪)
// 生成唯一 trace_id,注入到 Header 和请求体,便于后端日志关联 const traceId = `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; pm.environment.set("trace_id", traceId); pm.request.headers.add({ key: 'X-Trace-ID', value: traceId }); // 如果请求体是 JSON,自动注入 if (pm.request.body && pm.request.body.mode === 'raw') { try { const bodyJson = JSON.parse(pm.request.body.raw); bodyJson.trace_id = traceId; // 假设后端约定字段 pm.request.body.raw = JSON.stringify(bodyJson); } catch (e) { // 非 JSON 体,跳过 } }模板3:并发窗口控制(硬核限流)
// 实现真正的 N 并发控制:只允许最多 10 个请求同时在飞 const maxConcurrent = 10; let activeCount = pm.environment.get("active_requests") || 0; if (activeCount >= maxConcurrent) { // 达到上限,主动 delay,模拟排队 const waitMs = Math.floor(Math.random() * 100) + 50; // 50-150ms 随机等待 console.log(`Concurrency limit reached (${maxConcurrent}), waiting ${waitMs}ms`); setTimeout(() => {}, waitMs); // Node.js 中 setTimeout 不阻塞,需用 Promise // 更优解:用 pm.variables.set() 记录 start_time,Tests 中计算等待时间 } pm.environment.set("active_requests", activeCount + 1);4.2 Tests:从“响应成功”到“业务正确”的深度断言
Tests 脚本常被简化为pm.response.to.have.status(200),这在并发测试中远远不够。你需要验证的是:在压力下,业务逻辑是否依然自洽?
断言1:幂等性验证(防止重复扣款)
// 假设接口返回 order_id,且要求 id 全局唯一 const orderId = pm.response.json().order_id; if (orderId) { const history = pm.environment.get("order_ids") || []; if (history.includes(orderId)) { pm.test("Order ID must be unique across concurrent requests", function () { pm.expect(false).to.be.true; // 强制失败 }); } history.push(orderId); pm.environment.set("order_ids", history); }断言2:库存一致性(检测超卖)
// 假设请求体带 sku 和 quantity,响应返回剩余库存 remaining_stock const reqBody = JSON.parse(pm.request.body.raw); const sku = reqBody.sku; const quantity = reqBody.quantity; const resJson = pm.response.json(); const remaining = resJson.remaining_stock; // 从环境变量中读取初始库存(需在测试前预设) const initialStock = pm.environment.get(`initial_stock_${sku}`) || 0; const expectedMin = initialStock - (quantity * pm.iteration.current); // 粗略估算最小值 pm.test(`Stock for ${sku} should not go below ${expectedMin}`, function () { pm.expect(remaining).to.be.at.least(expectedMin - 5); // 允许 5 件误差,因竞争条件 });断言3:响应时间 SLA(服务等级协议)
const responseTime = pm.response.responseTime; const p95Target = 800; // 目标 P95 < 800ms // 将本次响应时间存入数组,用于后续统计 let times = pm.environment.get("response_times") || []; times.push(responseTime); pm.environment.set("response_times", times); // 每 10 次迭代输出一次统计 if (pm.iteration.current % 10 === 0 && times.length >= 10) { const sorted = times.sort((a, b) => a - b); const p95Index = Math.floor(sorted.length * 0.95); const p95Actual = sorted[p95Index]; console.log(`Iteration ${pm.iteration.current}: P95 response time = ${p95Actual}ms`); pm.test(`P95 response time < ${p95Target}ms`, function () { pm.expect(p95Actual).to.be.below(p95Target); }); }经验总结:我把所有 Tests 脚本封装成一个
assertions.js文件,用eval()动态加载(Postman 支持)。这样,当业务规则变更时,只需更新一个 JS 文件,所有集合自动继承新断言。这比在每个请求里复制粘贴代码,维护成本降低 90%。
5. 结果分析与问题定位:从“绿色对勾”到“根因报告”的完整链路
Collection Runner 运行结束后,UI 上显示的“Passed: 100/100”只是假象。真正的价值,在于导出的详细报告和原始日志。我有一套标准化的三步分析法,能在 15 分钟内定位 80% 的并发问题。
5.1 Step 1:导出原始日志,用 Excel 做第一层透视
Runner 结束后,点击右上角 “Export Results” → “Export as CSV”。这个 CSV 包含每行迭代的:序号、请求 URL、响应状态码、响应时间、响应大小、开始时间、结束时间、错误信息(如果有)。
用 Excel 打开,立刻做三件事:
- 排序:按“Response Time”降序排列,找出最慢的 10 个请求;
- 筛选:筛选
Status Code ≠ 200,看错误集中在哪类请求或哪个时间段; - 透视表:行=URL,列=Status Code,值=计数。一眼看出哪个接口错误率最高。
我曾用此法发现一个隐藏 Bug:99% 的请求返回 200,但有 1% 返回 503,且全部集中在/payment/submit接口。进一步查日志,发现是支付网关的连接池耗尽,而这个 503 在 UI 的绿色对勾里完全不可见。
5.2 Step 2:用 Postman Console 捕获实时网络细节
在 Runner 运行前,务必打开 Postman Console(View → Show Postman Console)。Console 会记录每一次请求的完整生命周期:DNS 解析耗时、TCP 连接耗时、TLS 握手耗时、首字节时间(TTFB)、内容下载耗时。
关键指标解读:
- DNS Lookup: > 100ms?说明 DNS 服务器慢或未启用缓存;
- TCP Connection: > 50ms?网络延迟高或目标服务器连接队列满;
- TLS Handshake: > 200ms?证书链长或服务器 TLS 配置不佳;
- TTFB: > 80% 的总响应时间?问题在服务端处理,而非网络。
实操技巧:在 Console 中右键某条慢请求 → “Copy as cURL”,然后粘贴到终端用
curl -w "@curl-format.txt"二次验证(curl-format.txt定义了详细时间字段)。这能排除 Postman 自身渲染开销的干扰,确认是真实网络/服务问题。
5.3 Step 3:结合后端日志,完成根因闭环
前端看到的“慢”,后端可能有完全不同的解释。我要求所有并发测试必须携带X-Test-Run-IDHeader,值为 Runner 的唯一标识(可在 Pre-request Script 中生成pm.environment.set("test_run_id", "load-test-" + Date.now()))。
然后,在后端应用日志中,用grep "X-Test-Run-ID: load-test-171xxxxx"过滤出本次测试的所有日志。重点看:
- 数据库慢查询日志:是否有
SELECT ... FOR UPDATE长时间持有锁? - Redis 日志:
KEYS *命令是否频繁出现(导致主线程阻塞)? - GC 日志:JVM 是否在测试期间频繁 Full GC?
有一次,Postman 显示平均响应 1200ms,Console 显示 TTFB 占 1150ms。后端日志一查,发现所有慢请求都卡在同一个@Transactional方法里,而数据库监控显示Innodb_row_lock_time_avg突增 10 倍。最终定位是库存扣减 SQL 缺少索引,导致行锁升级为表锁。这就是典型的“前端感知慢,根因在 DB”的案例。
最后分享一个血泪教训:永远在并发测试前,用
ab或wrk对目标服务器的/health接口做一次基线探测。如果/health都超时,说明问题在基础设施层(LB 配置、安全组、防火墙),而不是你的业务代码。别在错误的方向上浪费 2 小时。
6. 超越 Postman:当并发测试需要真正规模化时的平滑演进路径
Postman 是完美的起点,但绝不是终点。当你的测试需求从“验证单接口并发健壮性”,进化到“模拟 10 万用户真实行为路径”,就必须引入专业压测工具。我的建议不是推倒重来,而是构建一条平滑的演进管道。
6.1 第一阶段:Postman + Newman(CI/CD 自动化)
Newman 是 Postman 的命令行运行器。它让你把 Collection Runner 的能力,嵌入 Jenkins/GitLab CI 流水线。一个典型的newman-run.sh脚本:
#!/bin/bash # 从 Git 仓库拉取最新 Collection 和 Data 文件 git clone https://your-git/repo.git cd repo # 运行并发测试,导出 HTML 报告 newman run "MyAPI.postman_collection.json" \ --environment="staging.postman_environment.json" \ --globals="globals.postman_globals.json" \ --iteration-data="load_test_data.csv" \ --iteration-count=500 \ --delay-request=20 \ --reporters="html,cli" \ --reporter-html-export="reports/load-test-$(date +%Y%m%d-%H%M%S).html" # 检查失败率,超 5% 则流水线失败 if [ $(grep -c '"failures":\[.*\]' reports/*.html) -gt 0 ]; then exit 1 fi这样,每次代码合并到develop分支,就会自动触发 500 次并发冒烟。问题在集成阶段就被拦截,而不是等到发布前夜。
6.2 第二阶段:Postman Collection → JMeter 脚本(专业压测)
JMeter 功能强大,但学习成本高。好消息是:Postman Collection 可以一键转换为 JMeter.jmx脚本。Postman 官方提供了导出功能(File → Export → Collection v2.1 → Save),然后用开源工具postman-to-jmeter转换:
npm install -g postman-to-jmeter postman-to-jmeter MyAPI.postman_collection.json -o test-plan.jmx转换后的 JMX 文件,保留了所有请求、Headers、Variables、Assertions。你只需在 JMeter 中:
- 添加
Thread Group设置真正的线程数(用户数); - 添加
Constant Throughput Timer控制 TPS; - 配置
Backend Listener将结果实时写入 InfluxDB; - 用 Grafana 看实时压测大屏。
这样,你用 Postman 写的 100% 的业务逻辑,无缝迁移到了专业压测平台,零重复劳动。
6.3 第三阶段:k6(云原生、开发者友好的下一代)
如果你的团队拥抱云原生和 DevOps,k6 是比 JMeter 更现代的选择。它用 JavaScript 编写脚本,语法与 Postman 的 Pre-request/Tests 高度相似,学习曲线极低。一个 k6 脚本示例:
import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 100, // 虚拟用户数 duration: '30s', }; export default function () { const url = __ENV.BASE_URL + '/api/order'; const payload = JSON.stringify({ user_id: __ENV.USER_ID, product_id: __ENV.PRODUCT_ID, trace_id: `k6-${Date.now()}` }); const params = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${__ENV.TOKEN}` } }; const res = http.post(url, payload, params); check(res, { 'is status 200': (r) => r.status === 200, 'response time < 800ms': (r) => r.timings.duration < 800 }); sleep(1); // 每次请求后休眠 1 秒,控制节奏 }启动命令:k6 run --env BASE_URL=https://api-staging.com script.js。k6 的优势在于:轻量(单二进制)、可编程(支持 ES6+)、云原生(官方提供 k6 Cloud 服务)、结果丰富(原生支持 Prometheus 指标导出)。
我的个人实践:团队内部,Postman 用于日常开发联调的并发验证;Newman 用于每日构建的自动化回归;JMeter 用于每月一次的全链路容量压测;k6 用于工程师个人的快速性能探索。四者不是替代关系,而是互补的“测试工具矩阵”。
我在实际使用中发现,最高效的团队,不是追求“一把梭哈”的终极工具,而是建立清晰的“工具分层使用规范”:什么问题用什么工具,边界明确,切换丝滑。Postman 并发测试的价值,不在于它能跑多高,而在于它让每一个开发者,在写完第一行业务代码时,就能随手点一下,验证自己的代码是否经得起“多人同时操作”的考验。这种即时反馈,才是工程效能最真实的护城河。