1. 项目概述为什么我们需要为WebSocket构建专门的压测方案在当今的实时应用生态中WebSocket协议早已不是新鲜事物。从在线聊天室、实时协作文档到股票行情推送、在线游戏和物联网设备控制WebSocket凭借其全双工、低延迟的通信能力成为了构建实时交互功能的首选协议。然而当我们的应用从“能用”走向“好用”特别是面临高并发用户访问时一个核心问题就浮出水面这套基于WebSocket的实时系统到底能承受多大的压力它的性能瓶颈在哪里这正是我过去几年在多个实时项目中反复遇到的挑战。很多团队会用JMeter对HTTP接口进行压测但面对WebSocket尤其是基于STOMPSimple/Streaming Text Oriented Messaging Protocol子协议的应用时常常感到无从下手。STOMP在WebSocket之上提供了一种基于帧的、类似消息队列的通信模式广泛应用于Spring Boot等框架的WebSocket实现中。对它的压测不仅仅是建立连接、发送消息那么简单还涉及到连接的生命周期管理、订阅/发布模式、心跳维持以及消息路由的验证。直接使用JMeter内置的HTTP请求采样器来模拟WebSocket是行不通的因为协议底层完全不同。而网络上零散的教程要么只讲基础的WebSocket插件使用要么过于理论化缺乏一个从环境搭建、脚本编写到结果分析、瓶颈定位的完整实战指南。因此我决定结合多次实战经验整理出一套基于JMeter与STOMP协议的高并发WebSocket压测方案。这套方案的目标很明确不仅要告诉你每个按钮怎么点更要讲清楚背后的设计逻辑、参数设置的依据以及压测过程中那些容易踩坑的细节让你能真正构建出贴合业务场景、数据可信的压测脚本。2. 核心工具选型与环境准备工欲善其事必先利其器。构建一个可靠的压测方案工具链的选择是第一步。这里我们选择JMeter作为压测引擎的核心并围绕它搭建必要的插件生态。2.1 为什么是JMeter WebSocket Samplers插件JMeter作为一款老牌的开源性能测试工具其优势在于强大的可扩展性、丰富的监听器用于结果收集与分析以及易于分布式部署的能力。对于WebSocket压测JMeter本身并不原生支持因此我们需要借助第三方插件。在众多插件中JMeter WebSocket Samplers是社区活跃、功能相对完善的一个选择。它提供了从连接建立、消息发送/接收、心跳测试到连接关闭的全套采样器能够较好地模拟一个WebSocket客户端的行为。选择它主要基于以下几点考量协议覆盖全面支持标准的WebSocket协议WS和加密的WSS能满足大多数生产环境需求。STOMP协议友好虽然插件不直接识别STOMP帧但STOMP是基于文本的协议我们可以通过插件发送纯文本格式的STOMP命令帧从而实现对STOMP over WebSocket的压测。资源开销可控插件的采样器在设计上避免了为每个连接创建额外线程这对于模拟成千上万个并发连接至关重要能更真实地反映服务端资源消耗。集成度高完美融入JMeter的测试元件体系可以方便地使用JMeter的定时器、前置/后置处理器、断言和监听器构建复杂的测试逻辑。注意除了WebSocket Samplers也有其他插件如WebSocket Plugin by Maciej Zaleski。但根据我的实测前者在连接稳定性、高并发下的内存管理以及结果树的展示上更胜一筹特别是在处理二进制帧和复杂的断言场景时。2.2 环境搭建详细步骤这里我们假设你已经在本地安装了Java环境JDK 8或11并配置好了JMeter建议使用5.4或以上版本。下面是从零开始搭建压测环境的实操步骤。第一步下载与安装WebSocket Samplers插件访问插件的GitHub发布页面或可靠的Maven仓库下载最新版本的JAR文件。例如jmeter-websocket-samplers-1.2.10.jar。将下载的JAR文件复制到你的JMeter安装目录下的lib/ext文件夹中。这是JMeter加载第三方插件的标准路径。重启JMeter。这是关键一步不重启插件不会生效。第二步验证插件安装成功启动JMeter后可以通过以下方式验证右键点击“测试计划” - “添加” - “配置元件”查看列表中是否出现了WebSocket Binary Frame Filter,WebSocket Ping/Pong Frame Filter等新增项。右键点击“线程组” - “添加” - “取样器”查看列表中是否出现了WebSocket Open Connection,WebSocket request-response Sampler等系列采样器。如果能看到这些新增项说明插件安装成功。第三步准备被压测服务示例为了后续脚本编写和调试我们需要一个目标服务。这里用一个简单的Spring Boot STOMP WebSocket服务作为示例。你可以在本地快速启动一个。// 一个简单的Spring Boot STOMP WebSocket配置类 Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic); // 客户端订阅地址前缀 registry.setApplicationDestinationPrefixes(/app); // 客户端发送消息到服务端的地址前缀 } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws).setAllowedOriginPatterns(*).withSockJS(); } }// 一个简单的消息处理控制器 Controller public class GreetingController { MessageMapping(/hello) // 客户端发送到 /app/hello SendTo(/topic/greetings) // 服务端广播到 /topic/greetings public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(100); // 模拟一点处理延迟 return new Greeting(Hello, message.getName() !); } }使用./mvnw spring-boot:run启动服务它将在本地8080端口提供一个WebSocket端点ws://localhost:8080/ws并支持STOMP子协议。3. JMeter压测脚本核心逻辑与架构设计直接开始添加采样器很容易让脚本变得混乱且难以维护。一个健壮的压测脚本应该有清晰的结构和逻辑。我们的目标是模拟大量用户连接WebSocket服务器进行STOMP协议的握手、订阅频道、发送消息并接收广播。3.1 测试计划整体结构设计一个典型的STOMP over WebSocket压测脚本其JMeter测试计划结构应如下所示测试计划 (Test Plan) ├── 线程组 (Thread Group: 模拟用户组) │ ├── HTTP信息头管理器 (HTTP Header Manager: 设置Upgrade头等) │ ├── WebSocket Open Connection (建立WS连接) │ ├── WebSocket request-response Sampler (发送STOMP CONNECT帧) │ ├── WebSocket request-response Sampler (发送STOMP SUBSCRIBE帧) │ ├── 循环控制器 (Loop Controller: 模拟持续交互) │ │ ├── 固定定时器 (Constant Timer: 控制消息发送频率) │ │ ├── WebSocket request-response Sampler (发送STOMP SEND帧) │ │ └── WebSocket Single Read Sampler (可选读取特定响应) │ ├── WebSocket request-response Sampler (发送STOMP DISCONNECT帧) │ └── WebSocket Close (关闭底层WS连接) ├── 察看结果树 (View Results Tree: 调试用) ├── 聚合报告 (Aggregate Report: 主要性能指标) └── 每秒事务数 (Transactions per Second: 监控TPS)设计逻辑解析线程组定义了并发用户数线程数、循环次数和启动延迟。这是控制并发压力的总开关。HTTP信息头管理器至关重要。WebSocket连接始于一个HTTP Upgrade请求。我们需要在这里设置Connection: Upgrade和Upgrade: websocket头。对于STOMP通常还需要设置Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp来协商使用STOMP子协议。连接与协议握手先通过WebSocket Open Connection建立TCP层面的WebSocket连接然后立即通过WebSocket request-response Sampler发送STOMP的CONNECT帧完成应用层的协议握手。必须等待CONNECT帧的成功响应通常是CONNECTED帧后才能进行后续操作否则服务端会拒绝后续帧。订阅与发布发送SUBSCRIBE帧来订阅一个目的地如/topic/greetings。之后在循环控制器内通过定时器控制节奏反复发送SEND帧到应用目的地如/app/hello并接收服务端广播到订阅目的地的MESSAGE帧。连接清理压测结束时应发送STOMPDISCONNECT帧然后调用WebSocket Close采样器关闭底层WebSocket连接。这是良好的“公民”行为避免服务端积累大量僵尸连接。3.2 关键配置参数深度解析在配置各个采样器时理解每个参数的意义是写出有效脚本的关键。WebSocket Open Connection:Server name or IP: 目标服务器地址。压测时建议使用IP避免DNS解析带来的额外开销和不稳定性。Port: WebSocket服务端口。Path: WebSocket端点路径如/ws。注意这是建立WebSocket连接的路径不是STOMP的目的地。Connection timeout: 建立TCP连接的超时时间。在高并发场景下服务端如果来不及处理连接可能会排队。这个值不宜过短建议设置为5000-10000毫秒。Read timeout:这个参数在此采样器中容易被误解。它并非指读取数据的超时而是指等待WebSocket握手完成的超时时间。保持默认或稍大即可。WebSocket request-response Sampler:Connection: 务必选择use existing connection复用之前Open Connection建立的连接。每个虚拟用户线程应该维护自己的长连接。Request data: 这里填入我们要发送的STOMP帧。例如一个CONNECT帧CONNECT accept-version:1.2 host:localhost ^注意帧末尾的空行和NULL字符^在JMeter中可以用\0表示。STOMP帧以NULL字符结束。Response timeout: 等待响应的超时时间。对于CONNECT帧必须设置一个合理的值如3000ms并添加断言来确保握手成功否则后续步骤必然失败。关于STOMP帧的构造STOMP帧是纯文本的结构为COMMAND\\nheader1:value1\\nheader2:value2\\n\\nBody\\0。在JMeter中我们需要在“请求数据”框中精确构造这个格式。对于无Body的命令如CONNECT、SUBSCRIBEBody部分为空但最后的NULL字符必不可少。4. 实战构建一个完整的STOMP高并发压测脚本现在让我们一步步构建一个模拟1000个用户同时在线并持续进行消息收发的压测脚本。4.1 第一步创建线程组与全局设置新建一个Test Plan保存为stomp_websocket_stress.jmx。右键Test Plan-Add-Threads (Users)-Thread Group。Number of Threads (users): 1000 模拟1000个并发用户Ramp-up period (seconds): 60 在60秒内逐步启动这1000个线程避免对服务造成瞬时冲击Loop Count: Forever 勾选Infinite通过后续的调度器或手动停止来控制压测时长可选添加User Defined Variables配置元件定义一些变量如server_host,server_port,websocket_path方便后续维护。4.2 第二步配置HTTP头与建立连接右键Thread Group-Add-Config Element-HTTP Header Manager。添加以下两个关键的HeaderConnection: UpgradeUpgrade: websocketSec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp告知服务器客户端支持的STOMP版本右键Thread Group-Add-Sampler-WebSocket Open Connection。Protocol: WS 如果是生产环境WSS则选WSSServer name or IP:${server_host}或localhostPort:${server_port}或8080Path:${websocket_path}或/wsConnection timeout:10000Read timeout:5000将此采样器命名为01_WS_Open。右键Thread Group-Add-Sampler-WebSocket request-response Sampler。Connection:use existing connectionRequest data:CONNECT accept-version:1.2 host:localhost \0注意在JMeter的文本框中直接输入\0即可代表NULL字符Response timeout:3000将此采样器命名为02_STOMP_Connect。为连接添加断言这是保证脚本健壮性的关键。右键点击02_STOMP_Connect采样器 -Add-Assertions-Response Assertion。测试字段Text Response模式匹配规则Contains要测试的模式添加CONNECTED。因为成功的STOMP连接服务器会回复一个CONNECTED帧。如果断言失败这个采样器会被标记为失败帮助我们快速定位是认证问题还是服务不可用。4.3 第三步实现订阅与消息循环添加WebSocket request-response Sampler用于订阅。Connection:use existing connectionRequest data:SUBSCRIBE id:sub-${__threadNum} # 使用线程号生成唯一的订阅ID避免冲突 destination:/topic/greetings \0Response timeout:2000命名为03_STOMP_Subscribe。注意STOMP的SUBSCRIBE帧服务器通常不会回复内容性的响应但协议层面是成功的。这里设置超时主要是防止网络问题导致的无限等待。右键Thread Group-Add-Logic Controller-Loop Controller。我们将在这个控制器内模拟用户持续发送消息。Loop Count: 100 每个用户发送100条消息可根据需要调整或设为Forever在Loop Controller下首先添加一个Constant Timer。Thread Delay (milliseconds):1000每个用户每秒发送一条消息模拟中等频率的交互在定时器后添加WebSocket request-response Sampler用于发送消息。Connection:use existing connectionRequest data:SEND destination:/app/hello content-type:application/json {name: User_${__threadNum}_${__counter(,TRUE)}} \0这里我们发送一个JSON格式的Body内容包含线程号和循环计数器便于在服务端日志或结果中区分消息来源。Response timeout:3000命名为04_STOMP_Send_Message。关键且易错点处理异步响应。服务端处理完/app/hello的请求后会向/topic/greetings广播一条MESSAGE帧。由于我们是订阅者这个广播消息会通过WebSocket连接推送过来。JMeter的request-response Sampler在发送SEND帧后会立即尝试读取一个响应帧。如果服务端处理速度很快广播的MESSAGE帧可能先于SEND帧的其他响应到达从而被这个Sampler读到导致断言混乱。解决方案在SEND之后使用一个WebSocket Single Read Sampler来专门读取并消耗掉这个广播消息。在Loop Controller内04_STOMP_Send_Message之后添加WebSocket Single Read Sampler。Connection:use existing connectionResponse timeout:2500略小于SEND的响应超时确保逻辑顺序命名为05_Read_Broadcast_Message。可以为此Sampler添加一个Response Assertion检查响应中是否包含MESSAGE或Hello,等关键字以验证消息广播的正确性。4.4 第四步连接断开与监听器配置在Thread Group的最后Loop Controller之后添加断开连接的采样器。首先发送STOMP DISCONNECT帧添加WebSocket request-response Sampler。Connection:use existing connectionRequest data:DISCONNECT\\n\\n\\0Response timeout:1000命名为06_STOMP_Disconnect。然后关闭底层WebSocket连接添加WebSocket CloseSampler。Connection:use existing connection命名为07_WS_Close。添加监听器回到Test Plan层级添加监听器来收集结果。View Results Tree主要用于调试阶段查看每个请求和响应的详情。在高并发压测时务必禁用或删除它因为它会消耗大量内存和存储严重影响JMeter自身性能。Aggregate Report核心监听器提供所有采样器的平均值、中位数、90%百分位、95%百分位、99%百分位、吞吐量TPS和错误率等关键性能指标。Summary Report与聚合报告类似提供更简洁的摘要。Response Times Over Time(需要安装Custom Thread Groups插件包中的jpgc - Response Times Over Time)可视化响应时间随时间的变化趋势。Active Threads Over Time(同样来自插件包)可视化并发用户数随时间的变化。4.5 第五步参数化与数据准备为了模拟更真实的场景避免所有用户行为完全一致我们需要引入参数化。CSV数据文件创建一个user_data.csv文件包含用户名、用户ID等信息。username,userId Alice,1001 Bob,1002 Charlie,1003 ...在Test Plan下添加CSV Data Set Config配置元件。Filename: 指向你的user_data.csv文件路径。Variable Names:username,userIdDelimiter:,Recycle on EOF?:True如果线程数多于数据行则循环使用Stop thread on EOF?:FalseSharing mode:All threads所有线程共享同一份文件按顺序读取修改04_STOMP_Send_Message采样器中的请求数据使用变量SEND destination:/app/hello content-type:application/json {name: ${username}_${userId}} \05. 执行压测与结果深度分析脚本构建完成后不要急于用上千线程直接开压。科学的压测应该遵循“循序渐进”的原则。5.1 压测执行策略单用户调试将线程数设为1循环1-2次运行脚本。在View Results Tree中逐一检查每个采样器是否成功特别是STOMP CONNECT和响应断言。确保整个流程能走通。低并发验证将线程数增加到10-50循环10次左右。观察聚合报告中的错误率是否为0。同时监控被压测服务器的CPU、内存、网络连接数等基础资源使用情况确保脚本逻辑和服务器在低负载下正常。阶梯式增压这是发现性能拐点的关键方法。可以使用JMeter的Stepping Thread Group(需安装插件) 或通过多个不同的线程组配合定时器来实现。例如0秒启动50用户每60秒增加50用户直到达到目标500用户并持续压测5分钟。观察在并发用户数增加的过程中响应时间特别是90%百分位或95%百分位和吞吐量TPS的变化曲线。当TPS不再增长甚至下降而响应时间急剧上升时就找到了系统的性能瓶颈点。稳定性测试耐力测试以系统预估的最大并发用户数或略低于瓶颈点的并发数进行长时间如30分钟到2小时的持续压测。观察系统在长期压力下内存是否有泄漏、响应时间是否平稳、错误率是否会随时间推移而升高。5.2 核心性能指标解读压测结束后Aggregate Report是我们分析的主要依据样本数 (Samples)总共发出的请求数。平均值 (Average)平均响应时间。注意这个值容易被极值影响参考价值有限。中位数 (Median)50%的请求响应时间低于此值。能更好地反映“典型”用户体验。90%百分位 (90% Line)90%的请求响应时间低于此值。这是最重要的指标之一它反映了绝大多数用户的体验。例如90% Line为200ms意味着90%的用户在200ms内得到了响应。95%百分位 / 99%百分位 (95% Line / 99% Line)对体验要求极高的场景如金融交易需要关注。99% Line飙升可能意味着有少量请求遇到了严重阻塞如垃圾回收。最小值/最大值 (Min/Max)响应时间的范围。最大值异常高需要排查。异常% (Error %)失败请求的百分比。必须接近0%。任何非零的错误率都需要逐一分析原因查看结果树或.jtl日志。吞吐量 (Throughput)单位时间秒内处理的请求数即TPS。这是系统处理能力的直接体现。在压力增加时TPS曲线会先上升后趋于平缓甚至下降。接收/发送 KB/秒网络带宽使用情况。5.3 常见问题与排查技巧实录在实际压测中你几乎一定会遇到各种问题。以下是我踩过的一些坑和解决方案问题1大量WebSocket Open Connection失败错误信息包含Address already in use或Connection refused。原因分析高并发下客户端JMeter机器端口耗尽。每个TCP连接需要一个本地端口默认范围有限。解决方案调整JMeter机器内核参数(Linux/macOS)临时增加本地端口范围。sudo sysctl -w net.ipv4.ip_local_port_range1024 65535 sudo sysctl -w net.ipv4.tcp_tw_reuse1 sudo sysctl -w net.ipv4.tcp_tw_recycle1 # 注意在较新内核中此参数可能已废弃使用连接池不适用WebSocket是长连接不能像HTTP那样用连接池复用端口。但JMeter的WebSocket插件本身会复用TCP连接一个线程一个连接。分布式压测这是解决单机资源包括端口瓶颈的根本方法。在多台JMeter Slave机器上运行压测由一台Master控制。问题2压测运行一段时间后JMeter自身报OutOfMemoryError: Java heap space错误并崩溃。原因分析监听器尤其是View Results Tree会保存所有请求的响应数据内存急剧增长。解决方案正式压测时禁用或删除View Results Tree。增加JMeter的JVM堆内存编辑JMeter启动脚本jmeter或jmeter.bat找到HEAP设置。# 在jmeter脚本中修改 JVM_ARGS-Xms4g -Xmx8g -XX:MaxMetaspaceSize512m将结果直接写入文件使用Simple Data Writer监听器将结果写入CSV格式的.jtl文件对内存消耗极小。压测结束后再用Aggregate Report等监听器导入.jtl文件进行分析。问题3服务端返回的STOMP帧格式不正确导致断言失败或读取错乱。原因分析可能是网络问题导致帧不完整或者服务端实现与STOMP协议规范有细微出入。更常见的是我们前面提到的“异步响应读取错位”问题。解决方案仔细检查View Results Tree中失败请求的原始响应数据与STOMP协议规范对比。强化脚本的容错性对于WebSocket Single Read Sampler可以将其Response timeout设得稍短并勾选Ignore Read Fault如果插件提供此选项或者在其后添加一个If Controller判断读取是否超时然后决定是重试还是标记为警告。确保“发送-读取”逻辑配对这是解决异步消息错位的核心。坚持“一个SEND后跟一个专门读取广播MESSAGE的Single Read”的模式。如果业务逻辑是一个请求会触发多个广播则需要对应数量的Single Read。问题4压测时吞吐量TPS上不去但服务器资源CPU、内存利用率很低。原因分析瓶颈可能不在服务端而在客户端JMeter或网络。JMeter单机性能瓶颈单个JMeter实例能模拟的并发连接数和发送速率有限。网络延迟或带宽限制特别是如果JMeter与服务器不在同一局域网。脚本中存在不必要的等待例如定时器时间设置过长或者Response timeout设置过长导致线程阻塞。解决方案进行分布式压测使用多台JMeter Slave。优化JMeter脚本减少不必要的断言和监听器使用JSR223 Sampler配合Groovy脚本处理复杂逻辑可能比大量GUI元件更高效。调整超时时间在保证业务逻辑正确的前提下适当缩短Response timeout。监控JMeter所在机器的资源使用top或htop查看CPU使用nethogs查看网络带宽。如果JMeter自身CPU跑满那就是它的极限了。问题5如何验证消息的顺序和完整性场景模拟聊天室需要确保用户发送的消息都能被所有订阅者按顺序收到。解决方案这超出了基础压测的范围属于正确性验证。可以在脚本中实现消息染色在每个发送的消息Body中加入全局唯一的序列号如${__threadNum}_${__time()}_${__Random(1000,9999)}和发送者ID。在WebSocket Single Read Sampler后添加JSR223 PostProcessor使用Groovy脚本解析收到的广播消息提取序列号和发送者ID将其记录到一个全局的共享数据结构如ArrayList或写入外部文件。注意线程安全。压测结束后分析检查记录的消息看是否有丢失、重复或顺序错乱。这个方案对JMeter性能有影响仅适用于小规模并发验证逻辑。构建高并发WebSocket压测方案是一个系统工程从工具选型、脚本设计到执行分析和问题排查每一步都需要严谨细致。这套基于JMeter和STOMP协议的方案经过多个线上项目的锤炼被证明是可靠且高效的。记住压测的最终目的不是“压垮”系统而是通过模拟真实负载提前发现系统的性能边界和潜在缺陷为容量规划、性能优化和稳定性保障提供坚实的数据支撑。在具体实践中务必结合你的实际业务逻辑和架构特点对这份指南进行灵活的调整和深化。