1. 并发数的本质:先搞清楚你到底在压什么
1.1 为什么我说“并发数”这个词被用烂了
做性能测试的朋友第一次上手 JMeter,往往第一句就会问:“并发数在哪设置?”然后打开线程组,把“线程数”填成 1000,以为这就是 1000 并发。一年前我也是这么干的,直到被组长指着聚合报告里的 TPS 数字问了一句“你的并发呢?”我才发现自己根本没答上来。
先说结论:JMeter 里的线程数只是并发用户的模拟载体,它不等于真实的并发请求数。并发数以“线程数”的形式配置,但实际打到服务端的“并发压力”是由线程数、循环次数、Think Time、Ramp-Up、以及每个线程内 Sampler 的数量共同决定的。稍不注意,你设置了 1000 线程,实际同一时刻可能只有 200 个请求在线上,剩下 800 个线程都处于等待响应的状态。
所以本文不打算只告诉你“线程数填多少”这种浅层答案。我会把并发数的计算逻辑、常见负载模型、阶梯加压方法、以及 JMeter 自身限制全部拆开来讲。你可以把它当成一篇从“会填参数”到“会设计压测场景”的过渡文章。
1.2 线程数、用户数、并发请求数:三者不是一回事
很多刚接触性能测试的人会把下面三个概念混在一起,其实它们差异非常大:
- 用户数:指系统需要承载的总注册用户或活跃用户规模,比如 10 万注册用户。这个数字主要用于估算,不直接等于压测负载。
- 线程数:JMeter 中每个线程模拟一个虚拟用户。你设置 100 个线程,就是同时有 100 个虚拟用户在执行脚本动作。
- 并发请求数:某个瞬间真正到达服务端的请求数量。这个数字取决于线程数和请求耗时。
通俗地解释:你去银行办业务,大厅有 100 个座位(线程数),但真正同时趴在窗口办业务的人(并发请求数)不会超过柜台数量。如果每笔业务需要 3 分钟,那么座位再多,同一时刻办理中的业务也就是柜台数的量级。假设银行有 10 个窗口,哪怕 100 个人在大厅坐着排队,客户眼中的“并发”最多也只有 10 笔在进行中。这时候你说“我有 100 并发”,客户肯定不会认。
放在 JMeter 里也一样:线程组设置了 100 线程,线程 1 发起请求后等待响应,如果响应时间为 500ms,而脚本里每个线程做完一次请求马上进入下一轮,那这 100 个线程可能在任意时刻都有 100 个请求在途。但如果每个线程内部加了 2 秒的固定定时器,那么同一时刻平均在途的请求数就低于 100,真实并发会大幅缩水。
1.3 吞吐量与 RPS:衡量并发效果的核心指标
要判断并发设置到底有没有生效,最终看的是RPS(Requests Per Second,每秒请求数)或TPS(Transactions Per Second,每秒事务数)。并发数只是手段,吞吐量才是可观测的结果。
吞吐量和并发数之间存在这样一个关系:
RPS ≈ 并发线程数 / 平均响应时间
举个例子:设置 500 个线程,每个请求平均响应时间是 0.5 秒,那么理论吞吐量 ≈ 500 / 0.5 = 1000 RPS。如果把响应时间压到 0.25 秒,其它不变,吞吐量就会跑到约 2000 RPS。反过来,如果你发现线程数已经加到 2000 了,RPS 却一直稳定在 3000 左右不动,那基本可以判断瓶颈不在线程数上,而是被测服务或压测机自身到了上限。
这也就是为什么很多资深测试员做压测时,并不盲目追求“线程数越大越威风”。他们更关心负载模型下的RPS 曲线是否和目标一致。本文后续会给出两种实现路径:一种是直接设线程数,另一种是按目标 RPS 来反推并发量。
2. 线程组的参数明细:手把手把并发数配置到位
2.1 线程组里的几个核心字段到底怎么填
打开 JMeter 的线程组,你会看到线程数、Ramp-Up 时间和循环次数三个最基本的设置。很多教程只会让你填数,但不会告诉你这三个数字的内在逻辑。
线程数对应的是虚拟用户的规模。具体填多少,取决于你的测试目标。如果是做容量测试,通常把线程数设成预期峰值的 1.5 到 2 倍,留出余量;如果是做稳定性测试,线程数则设置到恰好支撑目标 RPS 的水平,然后持续跑数小时。
Ramp-Up 时间是 JMeter 在多少秒内启动完全部线程。默认是 0,表示立即同时启动所有线程。在演示、压测集群时,Ramp-Up 设成 0 做突袭式压测是有意义的,能测出服务在瞬间高并发下的表现。但如果模拟真实用户逐步进场,最好把 Ramp-Up 时间设成一个非零值,比如每 5 秒增加 50 个线程。
循环次数决定了每个线程执行多少次请求。如果只做一次冒烟测试,填 1 即可;做持续压测,可以直接勾选“永远”,再配合持续时间来控制。我个人的经验:循环次数不推荐填一个很大的固定值(比如 999999),因为这样压测时长完全不可控。更规范的做法是把“循环次数”设为永远,然后用“持续时间”来控制压测结束时间,这样配合命令行跑自动化更舒服。
2.2 调度器(Scheduler)的用法与典型场景
线程组面板最下方的“调度器配置”,很多人以为只是设置总运行时长,实际上它包含两个关键字段:持续时间和启动延迟。
- 持续时间:从测试开始到测试结束的总时长,单位秒。配合“循环次数=永远”使用,可以在固定时间内完成压测。
- 启动延迟:JMeter 等待多少秒后开始发起请求,适合等待服务就绪的情况。
典型的用法是:做一个 15 分钟的稳定性测试,线程数设为 500,Ramp-Up 设为 120 秒,循环次数永远,持续时间 900 秒。这样 JMeter 在 120 秒内把 500 个线程逐步拉满,然后在满并发下继续跑 13 分钟,总共历时 15 分钟。这个模型比“一开跑就 500 个线程同时打”更贴近真实系统的用户增长曲线,也更容易观察服务在扩容过程中的表现。
2.3 循环次数、Think Time 和请求耗时的组合策略
同样一个“500 线程”的配置,由于循环次数和定时器不同,产生的压力完全不一样。设 500 个线程,循环 1 次,一共只有 500 个请求;设 500 个线程,循环永远 + 持续 5 分钟,那请求数就看每个线程的并发执行速度,可能数千甚至数万。
这里面还有个很容易被忽略的点——请求耗时。如果被测接口非常快,比如 3ms 返回,那 500 个线程跑出来非常高的 RPS,服务端可能先被压爆。如果接口很慢,比如 3 秒才返回,那 500 个线程同一时刻在途请求也可能达到 500,压力并不小。
进阶玩法是在线程组下面加一个“固定定时器(Constant Timer)”,模拟真实用户阅读页面、填表单的思考时间。吞吐量会明显下降,但这更接近真实行为。做压力测试和容量测试时,建议不加或加很小的 Think Time,充分探底;做真实用户模拟时,Think Time 是必备元素。
3. 阶梯加压:让并发数从“一刀切”变成“渐增曲线”
3.1 为什么要阶梯加压
直接上 1000 个线程做压测,得到的结果往往是一堆错误和超时,但你分不清服务是从哪个并发段开始崩的。阶梯加压的核心目的就是:让并发数按既定节奏逐级增加,找到系统的拐点。
拐点是指服务端开始出现明显性能恶化的并发边界。比如并发 200 时响应时间 50ms,并发 400 时 80ms,并发 600 时突然涨到 600ms 且错误率抬升,那这就是容量规划里的关键数据。没有阶梯加压,你就拿不到这种曲线。
用现实生活中的例子理解:考一个人的负重能力,你不会直接往他肩上压 100 公斤,那样只会出现两个结果——要么咬牙扛住了,要么直接受伤,但你不知道他 60 公斤和 80 公斤时的状态差异。分级加量才能测出真实阈值。
3.2 用 Stepping Thread Group 插件实现阶梯负载
JMeter 原生线程组不支持按时间段自动增加线程数,但可以通过 JMeter Plugins 扩展实现。最常用的是jp@gc - Stepping Thread Group,在插件管理器里搜索安装即可使用。
Stepping Thread Group 的关键设置如下:
- Start Threads Count:起始线程数。
- First Wait For:测试开始后等待多久才开始加压。
- Then Start Threads Count:每级增加多少线程。
- Then Wait For:每级加压后保持多久。
- Threads Increment Per Step:每级增加的线程数。
- Ramp-Up Time Per Step:每级增加线程所花的时间。
- Hold Load For:全部线程启动后保持负载多长时间。
举个例子:把 Start Threads Count 设为 50,每级增加 50,每级保持 60 秒,总线程数到达 500 后保持 10 分钟。这样你会得到一条非常平滑的阶梯曲线,可以在聚合报告或监听器里清楚看到每个并发阶段的响应时间波动。
3.3 Concurrency Thread Group + 吞吐量定时器的组合用法
另一个更专业的组合是Concurrency Thread Group配合Throughput Shaping Timer。Concurrency Thread Group 可以设置目标并发数,并通过定时器来控制 RPS 变化。
这套组合的最大价值是:并发数和吞吐量之间的映射关系不再靠手动算,而是由定时器直接控制。你可以设定“前 60 秒每秒 100 次,接下来 120 秒每秒 300 次……”这种更精细的 RPS 曲线,常用在容量规划场景。
安装Throughput Shaping Timer后,在定时器配置里添加一个时间段列表,每一行代表一个时间区间内的目标 TPS。JMeter 会自动调整请求发起频率来贴合这些目标值。跑完之后,监听器里能直接对比目标曲线和实际曲线,非常直观。
3.4 原生功能做渐变:Threads 逐步增加的替代方案
如果你不方便装插件,JMeter 原生也能模拟简单阶梯。实现思路是用多个线程组,每个线程组设置不同的线程数和启动延迟,再通过“SetUp Thread Group”或者使用“命令行参数动态指定负载”的方式分别执行。
比如建 4 个线程组:第一个线程组 100 线程,启动延迟 0 秒,跑 1 分钟;第二个线程组 200 线程,启动延迟 60 秒,跑 1 分钟;第三个线程组 300 线程,启动延迟 120 秒,跑 1 分钟。测试计划整体总时长 3 分钟,就能得到一个粗糙的阶梯效果。
用这种方法的人较少,因为管理和维护都不方便,且多线程组之间资源竞争难以精确控制。如果你要上线生产级压测任务,还是推荐插件方案。
4. 目标并发:压测的真实目标其实是这样算出来的
4.1 先把目标并发算出来
很多人设并发数凭感觉:先跑 200,不行跑 500,再不行跑 1000。但专业的性能测试在写脚本之前,就应该先定好目标。
目标并发量的计算依赖 RA 数据(业务预估)和接口容量。最简化的公式有两类:
按在线用户数估算:目标并发 = 在线用户数 × 同时操作比例
比如某产品峰值在线用户 2 万人,同时操作比例按 5% 估算,那目标并发 = 1000。
按目标 TPS 反推:目标并发 = 目标 TPS × 平均响应时间
比如你想让系统支撑 5000 TPS,而接口平均响应时间是 100ms,那么并发 = 5000 × 0.1 = 500。这个数值可以作为初始线程数的参考。
这里要强调“初始”两个字。实际压测中,线程数和 TPS 不是完全线性的关系,因为线程多了以后,CPU 调度、网络连接复用、服务端线程池饱和都会让吞吐量上升变慢,最终进入平缓期。这个平缓期对应的并发数,就是系统的有效容量上限。
4.2 用 Throughput Shaping Timer 设定精确 TPS
如果你已经明确目标 TPS 而不是目标线程数,强烈建议直接用 Throughput Shaping Timer。它的逻辑很简单:无论你后台线程组设成多少线程,它都会通过动态调节请求发起速率把实际 TPS 控制在目标范围内。
这种工作方式的好处是:你不用担心“线程数设少了导致 TPS 不够”,只要线程序数留够余量,定时器会自行调整实际发压速率。用大白话说,线程数是“可用的手”,定时器是“节流阀”。
配置示例:
- 阶段 1:0-300 秒,目标 TPS 200
- 阶段 2:300-600 秒,目标 TPS 500
- 阶段 3:600-900 秒,目标 TPS 1000
跑出来的结果直接和业务容量目标对照,省去了很多人为调参的过程。
4.3 通过 CSV 和参数化实现数据驱动并发
并发数设置还有一个容易被忽略的维度:业务数据隔离。真实场景中,并发用户不会都用同一个账号、同一个商品 ID。如果所有线程打同一个接口、传同样的参数,JMeter 会命中服务端缓存,测出来的结果虚高。
我的建议是:为并发测试准备一组参数化数据,存成 CSV,通过 CSV Data Set Config 分发给每个线程。例如 5000 个用户账号、5000 个商品 ID,在 1000 并发下每个线程取一条,保证请求的数据不重叠。
具体做法:CSV 配置中设置“共享模式”为All threads,每次迭代自动读取下一行。如果需要每个线程独立的数据段,可以改用Current thread模式,CSV 行数只要大于线程数即可。
4.4 真实“并发用户”场景模型示例
最后补充一个偏业务的建模思路。假设我们测一个电商秒杀接口,业务要求 1 分钟内承受 10 万次下游调用,每次调用平均耗时 200ms。
那么目标 TPS = 100000 / 60 ≈ 1667。平均并发 = 1667 × 0.2 ≈ 334。为了使压测充分且通过波动验证容量,线程数建议设置为平均并发的 2 倍左右,即 600-700,并设置合理的 Ramp-Up 时间。
这里体现的思路是:并发数设置不是拍脑袋,而是业务目标、接口平均耗时和资源冗余三个因素共同推导的结果。
5. 限制你并发数上不去的,很可能不是被测系统
5.1 谁能拦住并发数的一堵隐墙
压测过程中最尴尬的事情不是被测系统扛不住,而是压测机自己先怂了。你设置 2000 线程,JMeter 报“无法创建线程”或者大量连接超时,你以为是服务挂了,结果发现是发起压测的这台 4G 内存机器先顶不住。
JMeter 是纯 Java 应用,高并发下需要大量堆内存存储线程状态和响应数据。默认可用的堆内存通常只有 256MB-512MB,跑 500 以上线程就开始频繁 Full GC,TPS 出现周期性掉坑。这个问题在笔记本电脑上非常常见。
5.2 JVM 堆与 JMeter 内存配置
JMeter 启动前会读取jmeter启动脚本里的HEAP参数。默认值是-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m,在大规模压测时远远不够。
我通常会把内存调成这样:
export HEAP="-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m"如果你的机器内存有 8GB,建议给 JMeter 分配 4GB 堆内存在。如果压测机内存不足,直接换更高配置的机器跑压测,而不是在小内存机器上硬扛。
判断 JMeter 是否内存不足,可以开 JConsole 或者观察压测日志里的 GC 相关输出。如果发现 GC 频率高、压测过程 TPS 出现周期性归零,多半是内存问题。
5.3 操作系统文件句柄与端口耗尽
JMeter 的每个线程通常对应到一个 Socket 连接。高并发时会有大量 TCP 连接建立和关闭,Linux 系统默认的文件描述符上限(ulimit -n)往往是 1024,超过这个数,Socket 创建就会失败。
压测前把文件描述符上限调高:
ulimit -n 65535同时还要留意 TIME_WAIT 状态堆积导致的端口耗尽问题。短连接压测时,客户端端口被大量 TIME_WAIT 状态占用,新连接无法建立。可以通过调整/etc/sysctl.conf来缓解:
net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 0 fs.file-max = 100000修改完执行sysctl -p让配置生效。这一步做不好,你会发现线程数怎么加,RPS 都卡在某个数值上不去,而且错误日志里全是连接超时。
5.4 压测前的自我检查清单
综合上面说的,我建议你在每次开启大规模压测之前,先对 JMeter 压测机做一个“体检”:
- 内存是否足够支撑设定线程数?建议堆内存不低于并发线程数 × 1MB。
- JVM 参数是否已经调整?
ulimit -n是否足够大,建议 65535 以上。- 压测机 CPU 是否已经跑满?如果压测机 CPU 已经接近 100%,产物数据已经失真。
- 压测计划中是否关闭了不能写入结果文件的“查看结果树”监听器?因为高并发写文本日志也是个大坑。
- 是否用命令行模式而非 GUI 模式执行压测?GUI 模式会拖慢压测速度。正确姿势是:
jmeter -n -t test.jmx -l result.jtl -e -o report6. 并发设置完之后:怎么判读结果、怎么调优
6.1 错误率与响应时间的合理边界
并发数设置正确后,结果到底怎么看?先看错误率,再看响应时间分布,最后看吞吐量。
错误率不是越低越好,而是要看业务约定。常规标准是:错误率 < 0.1%,部分核心链路要求 0%。如果错误率超过 1%,一般视为服务不可用,压测就该暂停并查日志。
响应时间要看的不是平均值,而是百分位值。聚合报告里的90% Line、95% Line、99% Line才有参考意义。平均值很容易被极端值带偏,比如 10 个请求里一个超时 8 秒,其余 9 个 50ms,平均响应时间会被拉到 800ms 多,看起来性能极差,但实际绝大多数用户体验很好。真正的性能承诺一般以 99% Line 为准。
6.2 聚合报告和监听器里到底该看什么
聚合报告(Aggregate Report)里值得关注的字段,我一般按这个顺序看:
- Sample:样本数,看总请求量是否够大。样本太少,结果不具备参考意义。
- Error%:错误率,先过第一道红线。
- Median:中位响应时间,反映一般用户体验。
- 90% Line / 99% Line:反映长尾用户受到的体验。
- Throughput:每秒请求数,直接对应 TPS。
- Received KB/sec / Sent KB/sec:网络带宽。如果这个数值接近网卡上限,说明网络成了瓶颈。
TPS 和响应时间是一对矛盾指标。在系统资源打满之前,增加并发通常会带来 TPS 上升和响应时间上升。当系统进入过载阶段后,TPS 开始持平或下降,而响应时间会快速抬升。你把这两个指标画成两条曲线,交叉点附近的区域就是系统的“甜点区”。
6.3 典型的并发设置失误与排查链路
我见过不少团队压测结果忽上忽下,最后发现不是被测系统的问题。常见失误集中在下面几条:
脚本中开了“查看结果树”且勾选“保存响应数据”。这会让 JMeter 把每个请求的响应体都写到内存和磁盘,IO 开销巨大,压测机反而成为瓶颈。排查方式:压测时用命令行跑,不打开结果树,结果用聚合报告汇总。
线程数设了,但循环次数为 1。这会让压测在极短时间内释放全部请求后立刻结束,你根本没跑到稳定状态。排查方式:用-l result.jtl存结果,看时间戳分布,观察是否覆盖了完整的加压阶段。
Ramp-Up 时间设得过长或过短。Ramp-Up 设 0 秒,1000 线程瞬间全部启动,对服务端的连接建立冲击很大;Ramp-Up 设 300 秒,线程数增长太慢,短时间压测内可能没达到目标并发就结束了。排查方式:结合线程活跃图,观察峰值线程是否确实到了设定值。
没有做数据隔离。500 个线程都用同一个用户 Token 打接口,服务端可能有缓存,也可能因为同一用户并发导致限流。排查方式:协议日志里看是否出现缓存命中、限流错误或者唯一性校验失败。
6.4 并发数不是越大越好:我的真实体会
最后聊点个人体会。很多刚入行的人会陷入“并发数攀比”的误区,好像测出了 2000 并发就很厉害。实际上,性能测试的意义在于找出系统在当前架构、当前配置下的真实容量和风险,并发数只是工具参数,不是成绩单。
我经历过一个项目,初始压测目标定为 1000 并发。我们用阶梯加压发现,系统在 400 并发时响应时间还算平稳,到 600 并发时 99% 响应时间直接飙到 3.8 秒,触发大量超时。后来把服务端的线程池、连接池和数据库连接池重新调配之后,同样的 600 并发,99% 响应时间降到了 800ms 以下。整个过程,JMeter 的线程数并没有改变,改变的是对并发背后资源的理解。
所以如果你读完这篇文章只记住一句话,我希望是:并发数的设置只是压测的第一步,真正有价值的,是理解并发背后的资源瓶颈,并通过阶梯式压测找到系统的拐点。