做接口测试的同行应该都有类似经历:HTTP接口压测做得再熟,一碰到WebSocket接口,习惯性套用HTTP那套去压,结果不是连接压根没建立起来,就是压出来的数字自己都不敢信。WebSocket接口和普通HTTP接口最大的区别在于它是常驻连接,一条连接建立后会持续收发消息,测试目标从“每秒能处理多少个请求”变成了“同时能维持多少连接、消息推送延迟有多大、服务端能不能稳稳扛住高频双向通信”。这篇文章就围绕JMeter基础——使用JMeter测试WebSocket接口这条主线,把从环境准备、插件安装、最小脚本、参数化到现场排查的完整链路梳理一遍。适合两类人:刚接手聊天、推送、行情类系统的测试新手,以及想用现成工具把长连接压测跑起来、但不想从零造轮子的开发或测试同学。
1. 为什么压测WebSocket不能套用HTTP那套思维
1.1 从HTTP短连接到WebSocket长连接:测试目标变了
HTTP的压测模型简单直接:客户端发一个请求,服务端返回一个响应,这个事务结束,连接关闭或者回到连接池待命。被测系统的核心能力是“单位时间内能处理多少请求”,所以大家习惯看TPS、平均响应时间、错误率,这套模型对应的是典型的请求-响应式业务。
WebSocket不一样。它先通过HTTP Upgrade机制完成一次握手,服务端返回101状态码之后,连接就保持在TCP层,双方随时可以互发消息。连接一旦建立,服务端可以主动推送,客户端也可以连续发多条消息而不用每次重建连接。这带来两个直接后果。
第一个后果:压测的重点不再是“每秒处理多少请求”,而是“同时能保持多少在线连接”。很多推送服务、聊天服务、实时行情服务的瓶颈恰恰出在连接数上。连接多了,服务端的内存、文件描述符、心跳维护逻辑、消息路由分发全部承压,HTTP那套“打请求”的模型根本模拟不出这种状态。
第二个后果:消息延迟成了关键指标。HTTP接口关心单次请求耗时,WebSocket场景更关心一条消息从服务端推出来到客户端收到,延迟是多少,特别是95分位、99分位。最典型的场景是室内聊天室:几百人在线,一个人发消息,其他人都要收到广播,服务端能不能把广播延迟控制在可接受范围内,不压测根本说不清楚。
所以,拿到一个WebSocket接口测试任务,先别急着打开JMeter找插件,要把测试目标拆出来。目标不同,后面的Sampler选型和线程组设计会完全不同。我一般把目标拆成三类:第一类测连接建立能力,关注每秒能成功建立多少连接、连接数上限在哪;第二类测双向消息吞吐,关注单位时间内客户端和服务端互发消息的总量;第三类测推送延迟,关注从服务端发起到客户端收到的时间分布。
1.2 JMeter介入WebSocket测试的底层方式
JMeter本身只能处理HTTP、JDBC、FTP这类“发起一次请求、拿到一次响应”的协议。WebSocket之所以能用JMeter测,靠的是第三方插件,原理上它做了这样几件事:
插件里的WebSocket相关Sampler负责替虚拟用户维护一条真实存在的TCP连接。这个连接不是JMeter框架自动管的那种连接池连接,而是每个线程私有的长连接。插件组件会在握手成功后,把连接对象和当前线程绑定,后续的发送、读取、关闭都在这条连接上进行。
插件常见组件按功能分成几类:
WebSocket Open Connection:负责建立连接,可以单独用来压连接数。WebSocket Single Write Sampler:负责发送一条文本或二进制消息,不等待响应。WebSocket Single Read Sampler:负责读取一条消息,可以设置读取超时。WebSocket Sampler:这是互斥型取样器,发送一条消息并等待对应的响应,类似HTTP的请求-响应模式。WebSocket Ping/Pong Sampler:发送心跳帧。WebSocket Close:关闭连接。
理解这套组件,比直接记参数重要得多。举个例子:你想模拟一个客户端持续接收服务端推送的场景,正确做法是连接建立之后,用WebSocket Single Read Sampler循环读取;如果你用WebSocket Sampler去等响应,服务端只是广播而不是针对你的消息回一条,等待就不可能正常结束,大概率触发超时。
JMeter对WebSocket的能力边界也得说清楚:它能模拟协议交互,但业务上的消息编解码、会话状态维持,必须靠JMeter变量和BeanShell断言辅助处理。它不会自动理解你的业务协议是JSON还是Protobuf,这些要靠脚本设计者自己处理。
1.3 先想清楚要测什么,再选组件
我见过不少同行把WebSocket Sampler当成HTTP Sampler用,循环一百次发订阅消息,结果压出来的根本不是长连接行为。下面这张表是我在实际项目中总结的组件选型对照,照着选基本不会跑偏。
| 测试目标 | 推荐组件 | 主要观察指标 |
|---|---|---|
| 并发连接建立能力 | WebSocket Open Connection + Close | 每秒建立连接数、失败率 |
| 双向消息吞吐 | WebSocket Sampler(请求-响应型) | 请求数/秒、响应延迟分位 |
| 服务端主动推送延迟 | WebSocket Single Read Sampler 循环 | 消息到达间隔、读取超时次数 |
| 长稳运行 | 连接建立后按真实节奏读写 | 内存占用、连接泄漏、消息积压 |
| 心跳维持 | WebSocket Ping/Pong Sampler | 心跳成功率、Pong返回时间 |
还有一个容易被忽略的问题:你要压的是“连接数”还是“消息数”。如果需求是“支持一万在线用户”,那你得先用Open Connection把所有线程的连接都建立起来,让连接保持住,再叠加少量消息交互去模拟真实用户行为。如果一上来就并发发一百条消息,连接数可能只有几十条,压测结论和需求完全对不上。
2. 环境准备:JDK、JMeter版本与Sampler插件安装
2.1 先定版本组合,别让环境拖后腿
WebSocket测试插件对JMeter版本和JDK版本有要求,版本组合不对,最常见的结果是插件管理器里搜不到插件,或者Sampler装上了但启动报错。我用的组合是JDK 11加JMeter 5.6以上的版本,这套组合兼容性好,插件社区跟进也及时。
JDK版本有个原则:能用11就不要用8,能用17再看插件是否兼容。原因很简单,JDK 8对一些新协议和证书套件的支持偏旧,WebSocket测试经常要连wss接口,SSL握手相关的兼容性问题在JDK 8上出现的概率更高。再说JMeter本身对高并发压测的内存管理一直不太友好,新版本改进了不少,没必要为了追求“稳定”死守老版本。
启动JMeter之前,建议把堆内存调大一点。默认的JVM参数在连接数上来之后会频繁触发Full GC,导致采样结果出现奇怪的尖峰。我一般在jmeter.bat或者jmeter.sh头部找一个叫HEAP的参数,改成类似-Xms1g -Xmx2g的水平。注意这是启动参数,不是测试计划里的设置,很多人会找错地方。
另外要强调一点:压测执行时不要用GUI模式,GUI只用来编辑和调试脚本。正式压测用命令行执行,否则JMeter自身渲染界面会消耗大量CPU和内存,数据根本不准。后面章节我会给出命令行用法。
2.2 安装插件管理器与WebSocket Samplers
安装WebSocket测试能力,最省事的方式是通过JMeter插件管理器。操作步骤如下:
- 先确认JMeter的lib/ext目录下有没有
jmeter-plugins-manager相关的jar包,没有的话需要单独下载。 - 把插件管理器jar包放进
$JMETER_HOME/lib/ext目录,重启JMeter。 - 启动后菜单栏会出现
Options下的Plugin Manager入口。 - 在插件管理器中切换到
Available Plugins标签,搜索WebSocket Samplers。 - 勾选后点击
Apply Changes and Restart JMeter,等待自动下载并重启。
插件管理器会自动处理依赖,基本不需要手动补包。需要注意:安装过程要联网,如果你的压测环境是内网隔离,得提前下载好插件jar包和依赖包,手动放到lib/ext目录下。手动安装有一个不小的坑:jar包版本和JMeter版本不匹配,启动时日志会报类加载异常或者缺少某个类,排查起来比联网安装麻烦得多。所以我建议能联网的环境都用插件管理器,离线环境再考虑手动方案,而且手动方案要严格按插件说明的版本对应关系来准备。
2.3 插件安装后的自检清单
每次装完插件,我习惯先跑一个三连检查,确认环境OK再动测试计划。第一步,重启JMeter后在日志窗口里看有没有ERROR级别的报错,特别是和websocket、plugin相关的异常。第二步,在“添加Sampler”菜单里找一下WebSocket相关组件,至少能看到WebSocket Sampler和WebSocket Open Connection两个入口。第三步,用一个最小连接测试验证插件能真正建立连接,这一步等下一章的本地服务端准备好后顺手就验了。
不要跳步。我见过有人在真实被测系统上一边压测一边排查插件问题,环境变量和业务问题搅在一起,浪费了大半天。先花十分钟把插件自检做干净,后面所有调试都会顺畅很多。
3. 从零跑通一个最小WebSocket压测脚本
3.1 先搭一个可控的本地WebSocket服务端
刚开始接触WebSocket压测,不要直接去压真实业务系统。真实系统协议复杂、鉴权流程长,出了问题你很难判断是脚本写错还是环境限制。我的习惯是先搭一个本地WebSocket服务端,几百行代码都用不上,一个回显服务就够。
我这里用一个Python的websockets库做示例,核心代码非常简单:收到一条文本消息,原样加上前缀返回。这段代码的目的不是实现业务,而是给压测脚本提供一个稳定、可控、可以随时看日志的靶子。
import asyncio import websockets async def echo(websocket): async for message in websocket: print(f"收到: {message}") await websocket.send(f"echo:{message}") async def main(): async with websockets.serve(echo, "127.0.0.1", 8765): print("WebSocket服务已启动 ws://127.0.0.1:8765") await asyncio.Future() asyncio.run(main())保存成ws_server.py,命令行执行python ws_server.py,服务就起来了。如果你用Node那套生态,也可以找一个类似能力的库,效果一样。关键是服务端代码自己能看懂,出现问题能立刻在终端看到收到的消息内容。
这个服务端选127.0.0.1和8765端口有两个原因:一是本机回环不会受防火墙干扰,二是端口不容易和常用HTTP测试环境冲突。如果你之后要测试局域网内的真实服务,只需要把地址换成目标地址,但脚本调试阶段用本机是最省心的。
3.2 Sampler核心配置:连接、握手与第一帧消息
打开JMeter,先创建一个测试计划,名字随便起。接着添加一个线程组,线程数先设成1,循环次数设成1。这时候不要急着加负载,我们要的是“一次能跑通”。
然后在这个线程组下添加WebSocket Sampler。这个组件是请求-响应型,适合验证最基本的“发送一条、接收一条”链路。打开配置面板,需要填的关键项如下:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| Server Address | 127.0.0.1 | 服务端IP或域名 |
| Port | 8765 | 服务端监听端口 |
| Protocol | ws | 使用加密则填wss |
| Path | / | 服务端路由路径 |
| Connection Timeout (ms) | 3000 | 握手超时 |
| Read Timeout (ms) | 5000 | 等待响应超时 |
| Request Data | {"action":"ping"} | 发送的文本帧内容 |
这里的Request Data是真正打进WebSocket连接的文本帧内容。在回显服务下,它会原样返回并加上echo:前缀。有些插件版本会直接叫Message或Payload,不用纠结字段名,知道它对应的是“要发送的正文内容”就行。
配置完成后,添加一个查看结果树监听器,然后点运行。如果一切正常,Sampler显示绿色,响应正文能看到类似echo:{"action":"ping"}这样一串内容。
这里有个细节值得注意:JMeter插件帮你完成了HTTP Upgrade握手过程,你看不到单独握手请求。但你可以从服务端终端的日志里验证握手确实发生了——服务端会打印收到{"action":"ping"},说明连接建立并完成了一轮交互。
3.3 用监听器和断言确认链路真的通了
只看Sampler变绿还不够,至少要看两样东西:响应正文和响应时间。
查看结果树里,选中刚才那次Sample,右边会显示请求数据、响应数据和采样时间。把响应数据和本地服务端返回内容对照一下,确认是整个业务消息都回来了,而不是只收到一个空帧。这里特别容易踩坑:服务端如果返回的是二进制帧,你在查看结果树里看到的可能是一堆乱码或者空白,因为工具默认按文本渲染。遇到这种情况,不要急着下结论说接口不通,先用服务端日志确认消息实际有没有发出去。
链路通了以后,再考虑断言。一个极简的断言可以这样加:添加响应断言,在“测试模式”里选择“包含”,测试字符串填echo:。这样做的好处是让失败自动暴露:如果连接建立成功但消息内容不对,断言会标红,避免后面做并发压测时错误被淹没。
我反复强调这一步,是因为很多新手跳过了断言直接跑压力,结果压测报告看起来全绿,实际上服务端一直在返回错误帧,只是JMeter不关心内容。断言不是可选项,是压测脚本的基本配置。
4. 把脚本升级成可交付的压测脚本:断言、参数化与并发模型
4.1 请求数据参数化:从硬编码JSON到CSV变量
最小化脚本跑通后,接着要做的是让脚本更像真实业务。真实场景下,每个用户的登录态、订阅的资源、发送的消息内容都不一样,把所有消息写死成同一个JSON,压出来的结果没有参考价值。
JMeter参数化的基本手段是变量和CSV数据文件。先用CSV Data Set Config读取一批模拟数据,比如每行代表一个用户ID和订阅房间号:
user_01,room_1001 user_02,room_1002 user_03,room_1003在线程组下添加CSV Data Set Config,填好文件路径、变量名(比如uid和room),然后Sampler的Request Data可以改写成:
{"action":"subscribe","user":"${uid}","room":"${room}"}这样做的好处是每次迭代会尝试从CSV里取下一行数据填充到消息里。JMeter变量的替换发生在发送之前,变量名放在双引号字符串中间会正常展开。需要注意,JSON里如果变量值本身包含引号,会造成JSON解析问题,所以CSV数据里尽量不要放特殊字符,或者在生成数据时就做好转义。
CSV还有个隐藏问题:当线程数大于CSV行数时,会按“是否循环”配置决定从头读还是报错。我一般在循环配置里选择EOF后循环,避免压测跑到一半因为数据耗尽而大面积失败。
4.2 响应断言与后置提取的正确打开方式
WebSocket消息的响应断言比HTTP接口更容易出错,原因是响应内容往往不是稳定的全量返回,而是包含了时间戳、随机ID、推送内容等动态字段。如果直接用整串匹配,可能第一次能过,第二次就因为时间戳变了而失败。
更好的做法是结合后置处理器提取关键字段,再做断言。比如回显服务返回{"type":"echo","content":"..."},我会用JSON提取器取$.type,然后断言这个值等于echo。JMeter自带的JSON提取器在Post Processors菜单里,配置上写JSONPath表达式,变量名可以任意取。这样断言准确且不受内容变化影响。
对二进制帧要特别小心。有些WebSocket接口服务端返回Protobuf、MessagePack这类二进制负载,文本断言完全无法匹配。这种情况下要么在Sampler上配置按文本解析(如果插件允许),要么把断言的用途降级为“确保收到了非空帧”,真正的业务校验放在服务端压测前的功能验证阶段做。
4.3 线程组设计:模拟多少客户端,连接就占多少资源
这是整个压测脚本设计里最容易被误解的一环。JMeter里每启动一个线程,就代表一个独立的虚拟用户,这个用户建立的WebSocket连接是独立且独占的。所以在线程组里把线程数设成1000,意味着同时有1000条WebSocket连接到服务端,不是“1000个请求”。
线程数、Ramp-Up Period、循环次数这三个参数,组合起来决定了你施加的压力模型:
- 线程数:目标并发连接数,要根据线上用户量或服务端设计容量来定。
- Ramp-Up: 建议从几十秒起步,让连接逐步建立,避免首轮连接风暴。
- 循环次数:每个连接上交互多少轮消息,如果是长稳测试,可以直接勾选无限循环,配合持续时间来控制压测时长。
我之前做过一次聊天服务压测,一开始线程数直接500,Ramp-Up设了1秒,结果服务端还没把连接处理完,客户端已经报了几百个连接超时。后来把Ramp-Up拉长到60秒,总连接数一样,但建立过程平稳,服务端处理得很好。这个参数往往比线程数本身更能反映真实用户到来节奏。
还有一个容易忽略的点:操作系统文件句柄和临时端口。每个WebSocket连接至少消耗一个文件描述符,大量的客户端连接从同一台压测机发起,端口号和句柄上限很快就会触顶。单机压测连接数不宜盲目追求上万,那是分布式压测的范畴。
4.4 多业务场景的组织方式:订阅、推送、心跳分开建模
真实的长连接业务从来不是只有一种消息类型。用户连上之后,第一件事通常是订阅某个频道,然后服务端持续推送,客户端每隔一段时间发心跳维持连接。把这些行为全塞进一个Sampler的循环里,脚本会变得复杂且难维护。
我的做法是拆线程组。一个线程组负责“建立连接并完成订阅”,里面的Sampler按顺序执行:先Open Connection,再发订阅消息,然后进入心跳循环;另一个线程组负责“只读推送”,连接建立后循环执行Single Read Sampler,专门度量推送到达情况。两个线程组同时跑,才模拟出一个完整的业务面。
注意线程组之间是并行执行的,要让订阅完成后再开始大量推送,可以用JMeter的setUp Thread Group或者添加“等待”逻辑。实际项目中我把“订阅”放在setup线程组,“推送读取”放在主线程组,再通过一个外部标志控制启动时机,效果稳定很多。这样组织脚本,出了问题也容易定位——哪条链路失败,直接看对应线程组的错误记录就行。
5. 压测现场高频问题与完整排查链路
5.1 连不上:从握手状态码开始逐层排查
压测现场最常见的故障就是“连接失败”,错误信息五花八门,但根因大部分集中在几个固定环节。我的排查链路是这样的:
第一步,用朴素客户端手动连一次目标服务,确认服务本身正常。这一步排掉“服务挂了”“地址错了”“端口被防火墙挡了”这些基础问题。第二步,检查JMeter的协议和端口配置,ws和wss别搞混,端口别写成8080之类。
第三步,看握手错误的具体表现。JMeter里连接失败通常会直接报ConnectException或者握手响应非101。如果服务端返回了403,多半是缺少必要的请求头或鉴权信息,比如Origin、Authorization。有些WebSocket服务只接受携带特定子协议或Cookie的握手请求,这种情况可以在Sampler的握手请求配置里加自定义请求头。
第四步,查证书。wss接口如果没有正确导入证书,JMeter日志里会出现SSL握手失败的堆栈信息。处理办法是在JMeter系统属性里指定TrustStore,路径必须是jmeter安装目录可访问的位置,填错了启动都会受影响。
| 现象 | 可能原因 | 检查动作 |
|---|---|---|
| ConnectException Connection refused | 端口不通或服务未启动 | 手动客户端连通性测试 |
| 握手响应非101 | 服务端拒绝了Upgrade | 查看服务端日志和请求头 |
| 403 Forbidden | Origin或鉴权头缺失 | 补全握手请求头 |
| SSL握手失败 | 证书未信任 | 导入TrustStore |
| 超时但服务端收到连接 | 并发连接数超限 | 检查服务端连接上限 |
5.2 断言通过但全在报错:错把错误帧当正常响应
这种问题比“连不上”更容易迷惑人。现象是压测报告里的错误率很高,但点开查看结果树,明明Sampler是绿色,断言也过了。
出现这种情况,有八成的可能是“错误信息本身就是一条正常的文本帧”。比如服务端返回{"status":"error","code":4001},这一帧在网络层面是正常收到的,Sampler当然不报错,但如果断言只写了“响应非空”,错误帧照样能顺利通过。
所以排查这种假通过,要做两件事:第一,抽样查看实际响应内容,尤其是错误响应格式长什么样;第二,把断言的判定逻辑从“非空”改成“业务成功标志正确”。比如服务端成功时返回"status":"ok",断言就应该匹配"ok",而不是匹配某个通用字段。
这里需要养成一个习惯:每轮压测结束后,不只盯着错误率,还要抽几台服务端日志看业务状态分布。JMeter只能证明网络交互完成了,证明不了业务逻辑成功,业务成功与否必须靠服务端记录来交叉验证。
5.3 并发跑高后大规模超时:盯着连接建立的瞬间
压测进行到一半,错误率突然飙升,大多是超时类错误。很多人的第一反应是“服务端不行了”,但根据我的经验,先别急着甩锅给被测系统,先检查自己脚本的连接行为。
如果你把WebSocket Open Connection放在了循环体内部,每次迭代都重新建立连接,那么并发一旦上来,所有线程同时做握手,服务端要处理的是巨大的“新建连接”洪峰,而不是业务消息。这种压法测出来的是握手容量,不是业务容量。
正确的做法是控制连接建立节奏:要么连接只建立一次,后续循环只发消息;要么把Ramp-Up拉长,让新建连接错开。另外,读取超时也可能被误判为服务端故障。如果服务端某个时间段不推送消息,而客户端Single Read Sampler设置的超时只有2秒,压测期间出现正常的间歇窗口,就会大量超时。这时候要看的是“消息实际到达间隔”分布,而不是单纯调大超时。
还有一个排查点是客户端本身。单机跑的连接数过高,操作系统端口和句柄耗尽后,JMeter也会报连接失败或超时,这在日志里和网络不通非常像。排查时看一下压测机自身的TCP连接数、端口占用数和文件句柄数,别把压测机的瓶颈算到服务端头上。
5.4 压测结束后的资源回收与数据确认
压测跑完之后,很多脚本会直接停掉,这埋了一个隐患:如果测试计划里没有显式关闭WebSocket连接,JMeter进程退出后,服务端那些还保持着的连接会经历一个自然的超时回收过程。短线连接问题不大,但如果做的是长稳大并发测试,服务端side的残留连接会直接影响下一轮压测的基线。
我的做法是在测试计划里加一个tearDown Thread Group,里面放一个WebSocket Close组件,确保每个虚拟用户跑完测试计划后都主动关闭连接。不加这个组件,不是说一定会泄漏,但服务端的连接回收日志会变得很难看,你分不清哪些是业务残留,哪些是测试残留。
数据确认也不能只看JMeter面板。命令行模式压测时,用-l参数指定结果日志文件,压测结束后用脚本或Excel透视表统计错误率、平均响应时间、90/95/99分位数。这里要提醒一点:如果压测中间调整过脚本或重启过服务,最好把日志分段保存,不要混在一起统计,否则结论会失真。
6. 把WebSocket压测固化到我日常流程中的几个习惯
这套流程走下来,我慢慢养成了几个固定习惯,对测试质量提升很明显。第一个习惯是任何WebSocket压测都先有一个“单线程冒烟”步骤,哪怕是压测过无数遍的老接口,也先跑一遍确认环境没变,再上并发。第二个习惯是压测前记录服务端连接数基线,压测后立刻再记录一次,两次数据对比能快速发现连接泄漏或回收延迟。
第三个习惯是所有Sampler都配明确的断言,宁可断言写严格导致前期调试费劲,也不要后期拿着全绿的假数据交差。压测报告的结论必须建立在“业务消息正确往返”的基础上,不然数字再漂亮也可能误导项目决策。第四个习惯是压测机的资源占用也要监控,我自己吃过这个亏,一度以为是服务端扛不住,结果发现压测机端口先耗尽了。
最后分享一个小技巧:在压测执行时,我会在命令行里加上-Jthreads=...这类参数,把线程数、持续时间抽成外部属性,脚本里引用${__P(threads,100)}。这样不需要每次改脚本,也能快速做多组不同压力级别的对比实验。WebSocket压测本质上是验证服务端在长时间、多连接、高频交互下的真实承载能力,多留几组可复现的参数,后续调优和回归都会轻松很多。