资讯动态

JMeter压测WebSocket接口:从环境准备到长连接实战

发布时间:2026/10/20 7:26:38 来源:尧图企业网站定制
做接口测试的同行应该都有类似经历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(fecho:{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 Address127.0.0.1服务端IP或域名Port8765服务端监听端口Protocolws使用加密则填wssPath/服务端路由路径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: 建议从几十秒起步让连接逐步建立避免首轮连接风暴。循环次数每个连接上交互多少轮消息如果是长稳测试可以直接勾选无限循环配合持续时间来控制压测时长。我之前做过一次聊天服务压测一开始线程数直接500Ramp-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 ForbiddenOrigin或鉴权头缺失补全握手请求头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压测本质上是验证服务端在长时间、多连接、高频交互下的真实承载能力多留几组可复现的参数后续调优和回归都会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑