1. 从零搭建在线聊天平台的性能测试方案
聊天平台这个项目,我前前后后测了将近一个月。先说结论:在线聊天平台和普通业务系统最大的区别在于连接模型——大多数系统是“请求-响应”,聊天平台是“长连接+消息推送”,这也决定了测试思路必须推倒重来。普通的HTTP压测脚本根本不能直接套用,必须从协议层、连接管理、消息路由三个维度重新设计。
我这次负责的在线聊天平台,核心功能包括单聊、群聊、消息撤回、离线消息拉取,以及在线状态展示。技术栈是Spring Boot搭建的WebSocket服务端,消息中间件用RabbitMQ做集群,在线状态放在Redis,数据库存历史消息。压测环境是三台8核16G的云服务器,JMeter版本是5.6.3,用到了它最新的WebSocket插件和HTML报告模板。
在做测试方案之前,我花了整整两天梳理业务链路。聊天消息从A用户发出,到B用户收到,中间要经过:WebSocket连接建立 → 客户端发送消息帧 → 服务端鉴权 → 消息写入RabbitMQ → 消费者从队列拉取 → 通过连接会话推送至B用户 → B客户端回执确认。这里面任何一环出现瓶颈,都会表现为消息延迟或者掉线。
这里我踩了第一个坑:一开始我只关注WebSocket的推送链路,忽略了RabbitMQ的消费能力。结果压测时发现WebSocket连接数上去了,但消息大量积压在队列中,消费者处理不过来,表现为“消息发出去但对方收不到”。后来我在JMeter里面加了RabbitMQ消费者性能的独立测试,才把这个瓶颈暴露出来。
我需要澄清的是,测试报告的呈现方式也很关键。JMeter 5.6.3的HTML报告升级之后,比老版本的CSV+图表工具好用太多,但前提是要弄清每个指标在聊天场景下的业务含义,这一点后面我会专门讲。
2. 测试方案设计:聊天场景不同于普通Web系统
2.1 为什么不能直接套用HTTP压测方案
传统的HTTP压测,核心指标是并发用户数和事务响应时间。一个典型登录接口,1000个线程同时发起HTTP请求,服务器返回200,这个事情就完了。但聊天平台完全不是这个逻辑,它的核心矛盾在于:
- 连接是长久的:用户连上WebSocket后,可能几小时都不断开,服务器保持的连接数和实际活跃用户数是两码事。
- 消息是双向的:A发消息给B,B的客户端会收到推送,这个推送是由服务器主动发起的,JMeter既要模拟发送方,也要验证接收方是否真的收到。
- 状态是实时的:在线状态、未读消息数、群成员列表,这些数据在消息到达时可能同步更新,对Redis和数据库产生额外的读写压力。
所以我的测试设计拆成了四条线:连接建立与保持、消息上行吞吐、消息下行推送、离线消息拉取。四条线分开测,再组合起来验证混合场景。
2.2 混合场景的拟定方法
我的做法是先做单场景基准测试,拿到每个环节的最大能力值,再设计混合场景。基准数据大致是这样的:
| 场景 | 并发连接数 | 消息大小 | 预期TPS | 实际瓶颈点 |
|---|---|---|---|---|
| 单聊发送 | 2000 | 256B | 800 | RabbitMQ消费能力 |
| 群聊广播 | 500群×50人 | 512B | 1500 | 广播扇出带宽 |
| 离线拉取 | 500 | 100KB含历史 | 120 | MySQL分页查询 |
| 混合场景 | 3500连接 | 256B~1KB | 1000 | 连接状态同步 |
这里我必须补充说明一个做测试方案时容易忽略的点:在线聊天平台的“并发用户数”和“并发连接数”不是同一个概念。用户可能连上了WebSocket,但半天不发消息;也可能在多个终端登录,产生多路连接。所以定义测试模型的时候,要明确“在线连接数”“活跃发送数”“广播扇出倍数”三个参数的关系。比方说3500个连接里,实际同时发消息的可能只有800个,但如果群里有一个人发消息,50个成员都要收,消息扇出就是1:50,上行TPS 200,下行推送TPS就是10000。这种一压就爆的模型,在测试计划里必须先算清楚。
2.3 场景建模需要业务数据支撑
做场景建模不能拍脑袋。我找开发要了线上聊天业务的历史数据分析,最后确认了一个比例:单聊消息占65%,群聊消息占30%,系统通知占5%。群聊平均成员数45人,用户平均在线时长40分钟,峰值集中在上午10点和晚上9点。有了这些业务参数,JMeter里的线程组设置、循环次数、思考时间(等到时间,就是think time)就能落到实处,而不是随便填数字。
3. JMeter 5.6.3压测在线聊天平台的核心配置
3.1 环境准备和依赖安装
JMeter 5.6.3要求JDK 8以上,我用的JDK 17。插件方面,测试WebSocket必须要装WebSocket Sampler插件。有两种选择:一种是JMeter插件管理器(Plugins Manager)里直接搜索WebSocket Sampler安装,另一种是下载JMeter WebSocket Samplers项目的jar包放到lib/ext目录。我推荐第一种,插件管理器还能顺带把依赖的包一起装好,省得缺了某个类导致启动报错。
验证插件是否安装成功的方法:启动JMeter,在“选项”菜单里看到“Plugins Manager”选项,并且在“添加-取样器”菜单里能看到“WebSocket Sampler”相关的选项,就说明装成功了。
3.2 WebSocket连接建立与消息发送的配置
JMeter脚本里首先要配置一个WebSocket连接,这是所有消息测试的基础。我在测试计划里用一个“WebSocket Connection”取样器建立连接,填入聊天平台的WebSocket地址,比如ws://chat.example.com/ws?token=xxx。这里有个细节:聊天平台的鉴权通常放在连接阶段的URL参数或Header里,JMeter里可以直接添加Header管理器,把Authorization: Bearer xxx带上去,也可以在URL后拼token参数。
建立连接之后,发送消息用“WebSocket Sampler”的Binary或Text格式。聊天消息一般是JSON格式的文本帧,我在取样器里填入:
{ "type": "chat", "msgId": "uuid-xxx", "from": "user_1001", "to": "user_2002", "content": "hello, this is a test message", "timestamp": 1710000000000 }这里必须提醒一句:msgId一定要用JMeter的内置函数动态生成,否则消息内容一样,服务端可能做去重或幂等处理,影响真实压测效果。我用的函数是${__UUID()}或${__time(yyyyMMddHHmmssSSS,)}拼在消息里。
发送消息之后,需要验证服务端确实把消息推给了接收方。这一步很多人会漏掉。我的做法是再加一个“WebSocket Single Read Sampler”,专门读取服务端推送回来的消息帧,然后用断言(断言,就是Assertion)检查返回的内容里是否包含预期的msgId或接收方ID。这样才能确定消息是真的走通了链路,而不是“发出去就算成功”。
3.3 群聊广播的测试配置
群聊场景里,一个人发的消息要广播给群内所有成员。如果测试脚本里对每个群成员都单独建立一个WebSocket连接,资源开销会非常大。我的做法是:
- 用CSV文件维护群成员账号列表,每组数据里包含群ID和成员ID。
- 用“线程组”模拟群内成员,每线程代表一个成员,线程数等于群人数。
- 群主发言的消息,通过单独的发送线程发出,但目标群ID在CSV变量中引用。
- 接收端用断言校验是否收到本群的消息类型,并验证群ID匹配。
这种方案模拟了真实业务流程,但又不需要真正给每个用户配一台“手机”。实际上,我在JMeter中让每个线程同时建立两个WebSocket会话,一个会话用来接收本群的消息(订阅群组频道),另一个会话用来主动发送消息(代表用户发言)。这样就同时覆盖了上行和下行。
3.4 心跳机制和断线重连的验证
在线聊天平台通常都有心跳机制,客户端每隔一段时间发送一个Ping帧,服务端回Pong帧,用来维持连接和检测死链。JMeter的WebSocket插件里提供了Ping/Pong帧的支持,但有一点要特别注意:JMeter默认的“连接超时”和“读取超时”设置,以及单次取样器的执行时间,都要大于心跳间隔。否则测试线程会认为连接已经断了,主动关闭WebSocket,影响长连接测试的准确性。
我在测试计划里专门写了“长连接稳定性”的一组测试:120个线程持续连线运行12小时,每30秒发送一次心跳,观察连接断开重连次数。JMeter里可以记录连接建立和断开的日志,测试结束后通过日志统计掉线率。实际跑下来,发现服务端有一个内存泄漏问题:连接关闭后,会话映射表里的记录没有及时清除,导致内存缓慢增长,第7小时开始出现大量连接超时。这个问题就是长连接稳定性测试的价值所在。
4. 用JMeter 5.6.3生成测试报告:命令与指标解读
4.1 报告生成命令写法
JMeter 5.6.3不需要再用老版本的Listener去保存CSV然后手动用Excel画图。直接命令行跑完之后,用内置的jmeter -g命令把结果文件转成HTML报告,很方便。我执行的命令是这样的:
jmeter -n -t chat_platform_test.jmx -l result.jtl -e -o output_report参数说明:
-n:非GUI模式,压测必须用命令行跑,不能开着JMeter界面压,窗口渲染会占用系统资源。-t:指定测试计划文件。-l:保存原始结果到JTL文件,后面生成报告要用的。-e:测试结束后立即生成报告。-o:指定输出目录,这个目录必须不存在或为空,否则会报错。
如果是已经跑完了,只有JTL文件,想重新生成一份HTML报告,可以单独执行:
jmeter -g result.jtl -o output_report4.2 聊天场景下报告指标怎么看
报告生成之后,会有一个index.html,打开就能看到整体情况。但这里我必须提醒:JMeter默认的聚合报告指标在聊天场景里有些需要特殊解读。
吞吐量(Throughput):表示每秒完成的请求数。在聊天场景里,发送消息的上行请求和接收消息的下行推送是分开统计的。我关注的是“上行消息发送TPS”和“下行消息接收TPS”。如果一份报告里吞吐量很高,但消息接收方验证失败率也很高,那说明消息丢了,吞吐量再高也没有意义。
响应时间(Response Time):JMeter报告中有平均值、中位数、90%和99%分位。聊天场景里最关键的指标是99%分位,也就是最差的那1%用户感知到的延迟。我设定的目标是99%分位消息延迟不超过2000ms,平均值不超过800ms。如果平均值看起来不错,但99%分位突然飙到5000ms以上,说明存在长尾延迟,一般和GC停顿或线程池排队有关。
错误率(Error%):这里我要专门讲一下WebSocket测试里一个容易误判的点——如果连接没建立成功,JMeter会直接报错;但如果消息发出去了,服务器没有返回预期响应,单靠默认的响应码判断不出来。所以我给WebSocket Single Read取样器加了JSR223断言,里面用Groovy脚本检查返回的消息内容。
活跃线程数(Active Threads):这个图表能看出来测试过程中真实在跑的线程数,如果线程数掉下去了,说明连接断了,JMeter在重连或者线程退出。
4.3 报告中的图表怎么用来定位瓶颈
JMeter 5.6.3的HTML报告里,我最常用的几个图是:
- Over Time图:响应时间和TPS随时间变化的趋势。我用来定位“是不是某个时间点开始性能劣化”,如果是,就回去看那个时间点对应什么操作。
- Throughput vs Threads:随着并发线程增加,吞吐量是线性增长还是到达峰值后回落。如果峰值后回落,说明系统已经有瓶颈。
- Latency vs Request:散点分布。如果图上出现一条斜向上的“尾巴”,说明响应时间在请求增加时持续拉长,典型的排队效应。
这些图组合起来看,能快速判断瓶颈在客户端、服务端还是中间件。有一次线上压测,响应时间全部超时,检查JMeter所在机器的CPU和网络后发现是压测机带宽打满了,撑不住每秒几万个消息。这个血泪教训提醒我:压测机的配置不能比服务器低太多,否则报告在分析上毫无参考价值。
5. 测试结果分析与性能调优记录
5.1 第一轮测试结果和瓶颈定位
第一轮测试结果出炉,情况不太好看。基础指标如下:
| 指标 | 目标值 | 第一轮实测 | 状态 |
|---|---|---|---|
| 消息上行TPS | 1000 | 623 | 未达标 |
| 端到端响应时间99分位 | ≤2000ms | 3480ms | 未达标 |
| 错误率 | ≤1% | 2.3% | 未达标 |
| 持续稳定连接数 | 3500 | 2890 | 未达标 |
看到结果之后,我第一反应是服务器配置问题,但检查CPU和内存发现使用率不高。接着查了RabbitMQ监控面板,发现消息队列的消费速率基本跑满,队列堆积严重。换句话说,WebSocket网关能接收消息,但消息投递到RabbitMQ之后,消费者消费不过来,导致端到端延迟变大。
这个问题的直接原因有两个:一是消费者线程池配置太小,核心线程数只有10;二是一次性批量拉取消息的条数设得太小,导致消费者频繁去Broker拉取,吞吐量上不去。开发调整了配置之后,消费能力从600 TPS提升到1100 TPS。
5.2 混合场景下的长连接稳定性问题
第二轮测试主要跑混合场景。结果暴露了两个新问题:
- 连接数达到3000以上时,服务端握手开始超时:打开TCP全连接队列的溢出统计,发现
ListenOverflows一直在涨。这是因为应用层的连接处理是同步阻塞模型,大量并发握手时,进程来不及调用accept,系统内核的accept队列被占满。最终方案是调整操作系统层网络参数,并把应用层连接处理改成异步非阻塞模式。 - 群消息扇出时CPU飙高:群聊广播会对每个在线成员遍历一遍连接表,连接表是全局锁保护的HashMap,并发一大就锁竞争激烈。开发把连接存储改成分段锁的ConcurrentHashMap,并引入本地缓存和批量推送合并,CPU的锁竞争问题才缓解下来。
5.3 最终达标数据
经过两轮调优之后,最终测试数据长这样:
| 指标 | 目标值 | 最终实测 | 状态 |
|---|---|---|---|
| 消息上行TPS | 1000 | 1052 | 达标 |
| 端到端响应时间99分位 | ≤2000ms | 1780ms | 达标 |
| 错误率 | ≤1% | 0.42% | 达标 |
| 持续稳定连接数 | 3500 | 3700 | 达标 |
| 群聊广播扇出TPS | 1500群消息/秒 | 1680 | 达标 |
这个结果证明了问题基本聚焦在消费能力和锁竞争上。聊天平台的性能测试,重点不是单机压出多高的TPS,而是看系统性瓶颈在哪里,以及调优之后整个链路能不能稳定扛住业务峰值。
6. 测试执行过程中的踩坑记录与排查技巧
6.1 JMeter本身制造的压力瓶颈
刚开始压测时,我用的JMeter单机跑5000个线程连接WebSocket,结果JMeter这一侧先崩了:内存溢出,连接建立超时。排查后发现,JMeter每建一个WebSocket连接,都会在内存里维护会话状态,5000个会话对Heap压力非常大,默认的1GB堆内存完全不够。
调整方案是:在jmeter启动脚本里修改JVM_ARGS,堆内存调到4GB,并且启用G1垃圾回收器:
export JVM_ARGS="-Xms4g -Xmx4g -XX:+UseG1GC"同时每台压测机只跑2000个连接,三台压测机组成分布式压测集群,用JMeter的Controller-Agent模式协调。还有一个必须注意的细节:Agent和Controller之间的通信结果汇总也会占用带宽,压测机之间最好走内网,不要经过公网转发,否则测试数据本身就会成为延迟来源。
6.2 WebSocket的消息帧大小和编码问题
在线聊天平台消息内容支持图片、表情、文件链接,消息体的JSON大小从几百字节到几KB不等。我最初统一用256字节的小消息压测,后来改成混合大小之后,TPS立刻掉了一截。原因是服务端对消息体做了Base64转码和敏感词过滤,大消息的处理时间明显增长。
测试脚本里我用了CSV文件配置不同大小的消息体,按比例随机选择,尽量逼近线上真实情况。关于编码还有一个小经验:JMeter里发送TextFrame时要注意字符编码,默认UTF-8没问题,但如果发送二进制内容(比如图片消息的Base64字符串),要用Binary Frame,并且把内容转换好,否则服务端解析会报错。
6.3 服务器内核参数的调整
聊天平台连接数要撑到几千甚至上万,Linux系统默认的文件描述符限制是不够的。压测开始前,我就把系统参数调整了:
# 单个进程可以打开的最大文件描述符数量 ulimit -n 65535 # 系统总文件描述符限制 sysctl -w fs.file-max=100000 # TCP连接重用和快速回收 sysctl -w net.ipv4.tcp_tw_reuse=1 sysctl -w net.ipv4.ip_local_port_range="1024 65000"这些参数在服务器端配置好,连接受限的问题就不会在压测中途冒出来干扰测试结论。JMeter这一侧的机器也同样要调,否则压力源先瓶颈了,后面的测试数据就完全没有意义。
6.4 离线消息拉取场景的数据库瓶颈
离线消息拉取的压测和普通Web压测不一样,需要模拟“用户重新登录后拉取所有未读消息”的动作。这个动作最终落在MySQL的一个大分页查询上:
SELECT * FROM chat_message WHERE user_id = ? AND msg_id > ? ORDER BY msg_id ASC LIMIT 500并发拉取一上来,这条SQL直接把数据库CPU干到100%。问题出在联合索引上没有包含user_id和msg_id。开发调整索引之后,同样的场景从120 TPS提升到400 TPS。这里我想再提醒一句:测试报告中如果只关注TPS,容易忽略数据库慢查询日志里藏着的杀手。我在每轮测试之后,都会同步收集MySQL慢查询日志和Redis的慢命令日志,两个一起对照分析,往往能定位到意想不到的瓶颈。
7. JMeter 5.6.3新特性实测感受与报告优化细节
7.1 新版本报告模板比老版强在哪
JMetter 5.6.3的HTML报告模板,比5.1之前的版本清爽不少。最直观的感受是三类信息放在首页就能看到:
- APDEX(应用性能指数):反映用户对系统性能的满意程度,0到1之间,越接近1越好。我做聊天平台设置APDEX阈值为“满意2000ms,容忍5000ms”,最终结果显示0.92,属于良好区间。
- 统计摘要表:直接列出样本数、平均值、中位数、90%、95%、99%分位和错误率,一眼就能看清楚。
- 时间线图表:把吞吐量、响应时间、活跃线程数按时间维度展示,方便拉长时间轴看整体趋势。
7.2 让报告更贴合聊天场景的几个配置
默认报告里的“事务名称”比较简陋,显示的可能是“WebSocket Sampler”这样的名字。为了让报告更有业务含义,我在脚本里给每个取样器取了有意义的名字,比如“发送单聊消息”“拉取离线消息”“验证群聊推送”,报告里能直接看出来每类操作的性能参数。
另一个优化项是在用户自定义变量里管理服务器的地址、端口、并发数,报告里的标签页能显示测试参数组合。这样每次压测生成报告后,稍微看一眼标签就知道这次跑的是“5000连接-256B消息-60分钟”还是“3500连接-1KB消息-30分钟”。这些细节能让报告在团队里流转的时候少很多解释成本。
7.3 报告分享时容易被忽略的上下文信息
测试报告不只是给测试组自己看的,还要给开发、产品和运维看。我习惯在报告目录里额外放一个README,写清楚本次压测的环境配置(几台服务器,每台规格)、JMeter版本、插件版本、业务场景设定(在线用户数、消息大小比例、群聊人数)、关键结论和调优建议。否则单看一张吞吐量图表,外行根本不知道为什么第一轮失败第二轮成功。
这里我想强调一下:很多团队拿到了JMeter的HTML报告,看完就扔了,三个月后再来复盘,环境参数早就忘了,报告等于白做。补上一个简单的环境说明文件,花费不了几分钟,后期省下的沟通成本非常大。
8. 关于消息可靠性和测试报告深度的补充
8.1 压测中发现的可靠性问题
在线聊天平台除了性能指标,还有两个隐藏的可靠性指标必须测:消息不丢失和消息不重复。我在压测过程中发现,当RabbitMQ出现短暂不可用时,客户端会重发消息,但服务端缺少幂等处理机制,导致同一消息被写入多次,接收方出现重复消息。
解决方案是让开发在服务端加一层基于msgId的去重表,压测脚本里的msgId用UUID生成,刚好能用唯一性约束去验证去重逻辑是否生效。测试报告里我单独增加了一个“消息去重验证”小节,包含压测期间产生的重复消息数量、去重表大小、去重耗时等数据。
8.2 测试报告里应该包含哪些性能结论
一份完整的在线聊天平台测试报告,我习惯分成六个部分:
- 测试概述(背景、范围、环境)
- 测试模型设计(业务比例、连接模型、场景设计)
- 执行结果(吞吐量、延迟分位、错误率、稳定性)
- 性能瓶颈分析(每个瓶颈的定位过程、根因、调优措施)
- 调优前后对比(用表格列出各类指标的前后变化)
- 风险与建议(剩余风险、扩展性建议、监控告警建议)
第六部分往往最容易被忽略。我的建议是测试报告不能只报“通过”,还要给出“什么时候需要扩容”“哪些参数需要持续监控”等建议。比如这次压测结论里我明确写了:当在线连接数达到5000时,需要将RabbitMQ消费者实例扩到至少4个;当端到端延迟P99超过3000ms时,首先检查消息队列堆积,其次检查数据库慢查询。这样运维同事拿到报告后可以直接落地到监控告警规则里,而不是干看着一堆数据。
8.3 测试数据资产化
最后我建议:压测的JTL原始文件、JMeter脚本、压测机配置、调优前后参数快照,都要归档保存。我经历过好几次线上出问题,回去翻测试报告时发现脚本已经从磁盘上清掉了,只能凭记忆复现,耗时还不一定准确。JMeter脚本本身就是测试资产,加上用5.6.3版本生成的测试报告自带时间戳和指标快照,归档价值很高,千万别清理。每轮压测后的原始数据都按日期命名归档,后期做版本对比和容量规划的时候直接调出来用,比自己临时造数据靠谱得多。