JMeter WebSocket压测实战:IM长连接瓶颈定位与优化复盘
这阵子给某项目组的IM消息系统做了一次完整的 WebSocket 压测从最底层的 ChatMessage.proto 字段梳理开始一直到输出压测报告并推动服务端完成两个瓶颈优化前前后后跑了将近两周。这中间踩了不少坑也有很多值得记录的判断和取舍。今天就把整个流程完整复盘一遍从协议定义、工具选型、脚本设计到真正的压测执行和报告解读全部串起来讲清楚。如果你正在准备对自己家的IM或任何长连接消息系统做压测这篇文章应该能帮你少走不少弯路。多说一句这篇文章的侧重点不在“怎么把JMeter跑起来”这种入门教学而是“在真实IM这种复杂消息场景里怎么用JMeter把WebSocket压测做对”。里面涉及的一些参数、写法、排查思路都是我实际跑过验证过的可以直接拿去参考。1. 项目背景与压测目标拆解1.1 压测对象是条什么链路先交代一下被测系统的背景。这是一个面向移动端的 IM 消息系统核心链路是客户端通过 WebSocket 接入消息网关网关做连接管理、鉴权、心跳然后把消息投递到后端消息服务后端服务负责消息路由、离线存储、回执生成最后把消息推送回目标客户端。这里说的“客户端”实际是很多个业务端包括App、小程序、H5它们的共性都是走同一个 WebSocket 网关接入。这次压测的重点落在两个地方。第一个是网关接入层关心的是同时能撑住多少条 WebSocket 长连接连接建立的速度怎么样连接断开重连是否平滑。第二个是单条消息从“客户端发送上行”到“目标客户端收到下行”这条主链路的吞吐能力包括上行消息接收、消息路由转发、下行消息推送整个过程在服务端怎么流转、耗时多少、瓶颈在哪。聊压测之前一定得先把被测对象搞清楚不然压了半天也不知道压的是什么东西。我这次是先拉着服务端同学对齐了核心链路把网关、路由、存储、推送四个环节的数据流图画出来再决定压测脚本要模拟哪些行为。后面做报告的时候你会发现没有这个环节对齐你拿到的TPS再高也没有人认。1.2 为什么用 JMeter 来做这个事很多团队压 WebSocket 会先想到自己写压测工具甚至用代码起几百个协程去怼服务端。这种做法不是不行但有几个现实问题脚本没有统一编排结果没有统一统计线程和连接的管理要自己处理出问题以后不好排查。而且一旦需要模拟带登录态的用户、带参数的消息内容自己写工具的复杂度就上来了。JMeter 的好处在于它是一个成熟的压测框架线程模型、采样器、断言、监听器、报告输出都是现成的。它原生支持 HTTPWebSocket 则需要通过插件扩展。我们这次用的就是 JMeter 5.x 配合 WebSocket Sampler 插件再加上 JSR223 Sampler 来跑自定义的 protobuf 序列化逻辑。整个压测脚本全部跑在 JMeter 里最后的聚合报告直接出图表服务端同学看得懂也不用来回扯皮。选择 JMeter 还有一个很实际的原因JMeter 的 JSR223 Sampler 允许直接写 Java 代码这意味着我可以把一个“模拟真实客户端”的完整逻辑塞进去——创建 protobuf 消息、签发 token、计算序列号、维护心跳全部在 Sampler 里完成。WebSocket 插件负责的是连接和收发帧自定义逻辑负责的是业务模拟。这个组合在灵活性上已经足够覆盖大多数 IM 压测场景。1.3 压测指标是怎么定义的压测报告里如果没有明确的指标定义那做出来的报告就是一堆数字堆在一起没有说服力。所以在一开始我们就定好了这次要关注的几个核心指标。连接能力网关能够维持的最大 WebSocket 长连接数以及连接建立的速率每秒能完成多少次握手。消息下行 TPS单位时间内目标客户端实际收到的消息条数这是用户感知最直接的指标。消息处理耗时从客户端发出消息到服务端下发回执之间的耗时通常会看 RT 的 P50、P90、P99 分位数。错误率包括连接失败率、消息发送失败率、回执超时率。任何压测报告里错误率必须单独呈现不能只看TPS。服务端资源连接数、CPU、内存、GC、线程池活跃度这些指标用来定位瓶颈发生在哪一层。指标定完以后我们把它和业务指标做了一次映射。比如当时业务方的要求是“高峰期在线用户 5 万每人每 10 秒产生一条消息”那倒推下来消息下行 TPS 至少在 5000 以上再考虑到峰值波动我们目标定在 8000 TPS 才算安全。这个数字后来成了压测是否通过的基准线。2. 压测环境与工具链准备2.1 JMeter 版本与 WebSocket 插件选型JMeter 版本的选择上我建议直接用 5.4 以上的稳定版本JDK 用 11 或 17。不要小看这两个基础选型后面跑长时间压测的时候JDK 8 在默认 GC 参数下很容易出现 Full GC 导致压测机抖动直接影响结果。我们一开始就是在某台 JDK 8 的压测机上跑跑 20 分钟以后 JMeter 的 GC 越来越频繁后面换了 JDK 17 才稳定下来。WebSocket 插件的选型是另一个关键点。JMeter 本身不带 WebSocket Sampler需要从插件管理器安装 websocket-sampler 插件。这个插件有两个核心组件WebSocket Open Connection负责建立连接和 WebSocket Request Response Sample负责收发消息帧。另外还有 WebSocket 心跳定时器之类的小工具。安装完成后JMeter 的采样器列表里会出现 WebSocket 相关组件就可以正常使用了。有一个值得注意的细节WebSocket Sampler 插件在默认情况下发的是文本帧如果要发二进制帧protobuf 场景必须显式指定请求数据为二进制并勾选对应的选项。这个细节放到后面讲报文构造的时候再展开。2.2 压测环境规划和参数设计压测环境我分两个维度来说。第一个是服务器端。被测服务部署在测试环境配置是 8C16GJava 服务使用 Netty 作为 WebSocket 的底层通信框架。测试环境的网络是内网千兆压测机和被测服务之间没有经过外网这样可以排除公网带宽和延迟的干扰专注于验证服务自身的处理能力。第二个是压测端。JMeter 本身是 Java 应用默认启动参数经常不够用尤其是跑长连接场景时每个 WebSocket 连接在 JMeter 侧都要占用一个线程线程一多默认堆大小极易出问题。我这边是直接修改了 JMeter 的启动参数把堆内存调到 4G同时明确禁用 GC 日志之外的额外输出避免压测机自己成为瓶颈。这里强调一下线程数和连接数的关系。JMeter 的 WebSocket Sampler 是每个线程维护一个 WebSocket 连接所以“1000 个线程”就等于“1000 条连接”不是“1000 并发请求”。这条对应关系在做连接数压测的时候非常重要。你要测 2 万连接JMeter 就要起 2 万个线程这对压测机是很大的压力。我们在设计时考虑了两台压测机做分布式压测每台跑 1 万线程才把连接数推到 2 万这个量级。2.3 服务端监控与埋点准备压测不只是看 JMeter 这边的数字服务端必须有一套监控手段。我们这次用到的服务端观测手段包括JDK 自带工具jstack 看线程状态、jmap 看堆内存、jstat 看 GC 情况。操作系统层面top 和 vmstat 看 CPU 和内存。连接数观察因为网关用的是 Netty可以通过自定义的 Metrics 接口实时获取当前连接数、消息处理数、线程池活跃度。为什么要把监控做在前面因为压测遇到性能瓶颈时你只能靠服务端指标去定位是 CPU 计算密集、内存不足、线程池打满还是连接队列溢出。没有这些数据你会陷入“TPS 上不去但不知道问题在哪”的尴尬局面。我们实际压测中遇到的三个关键瓶颈全部是靠服务端监控定位出来的这个后面详细讲。2.4 关于测试数据的准备压测之前还有一个很容易被忽略的环节准备足够多的测试用户和会话数据。IM 场景里压测需要使用真实的用户 ID、token、房间号如果全部用同一个用户服务端的单用户串行处理逻辑会直接把 TPS 卡死结果完全失真。我这边准备了 2 万个测试用户提前导入到服务的用户表里。每个用户有一个唯一的 user_id对应的 token 也是预先签发好的。另外准备了 1000 个模拟群聊房间每个房间有 50 个成员用于模拟群消息广播场景。这些数据放在 CSV 文件里压测脚本启动时按线程读取保证每个模拟用户都有独立的身份。3. ChatMessage.proto 报文解析与 JMeter 消息构造3.1 ChatMessage.proto 的关键字段拆解聊到 IM 消息系统协议定义是所有事情的起点。我们的 ChatMessage.proto 定义在很多核心模块间共用是一个标准的 protobuf 消息体。它的简化结构大致是这样的syntax proto3; package im; message ChatMessage { string message_id 1; string from_user 2; string to_user 3; string room_id 4; uint32 msg_type 5; int32 content_type 6; bytes content 7; mapstring, string extra 8; int64 timestamp 9; uint32 seq 10; int32 ack_status 11; }看起来字段不多但每个字段怎么填、能不能复用、会不会冲突都直接影响压测脚本的编写。message_id 是全局唯一消息ID压测时必须保证不重复否则服务端会有去重逻辑直接丢弃重复消息。我们采用时间戳加线程编号加自增序列号的方案确保同一线程和不同线程之间都不会冲突。from_user 和 to_user 是从 CSV 参数化出来的前者是当前线程的用户后者根据场景不同可能是另一个用户单聊或一个房间 ID群聊。room_id 只在群聊场景下有意义单聊时留空。msg_type 用于区分消息类型例如文本消息、图片消息、系统消息。压测时一般固定使用文本消息也就是 msg_type1。content_type 表示内容格式固定在 body 里放一段有代表性的文本内容。extra 是一个 map用来携带扩展属性压测时可以塞一个固定的设备信息不需要太复杂。timestamp 和 seq 这两个字段要单独说明。timestamp 是客户端生成消息的时间戳服务端会拿它和服务器时间做差来计算消息延迟。压测构造消息时timestamp 不能是固定的必须取当前时间否则延迟统计完全不准。seq 是客户端发送序列号同一个连接上的 seq 必须严格递增这是我们脚本里最容易写错的地方之一。ack_status 用于回执状态压测发消息时置为 0表示待确认收到回执后由服务端改写。3.2 在 JMeter 中序列化 protobuf 消息的两种思路确定了协议结构就要解决一个实际问题JMeter 怎么把 protobuf 消息发出去。第一条思路是使用二进制帧手工拼字节。第二条思路是加载 protobuf 的 jar 包在 JSR223 Sampler 里用 Java 代码构造消息体然后序列化。我的建议是直接用第二种。手工拼二进制字节极易出错而且消息字段一增加就要重新改字节偏移可维护性极差压测脚本本身就会变成一个事故隐患。用 JSR223 Sampler 之前需要先把 protobuf-java 的 jar 包放到 JMeter 的 lib/ext 目录下同时把你自己的 ChatMessageProto 编译出来的 Java 类也打包成 jar 放进去。然后 JSR223 Sampler 的语言选择 Java脚本里直接引用生成的 Builder 类来构造消息。下面是用 JSR223 Sampler 构造 ChatMessage 的参考写法import im.ChatMessageProto.ChatMessage; import java.time.Instant; import java.util.UUID; String messageId UUID.randomUUID().toString().replace(-, ) _ vars.get(threadNum) _ (counter.incrementAndGet()); String fromUser vars.get(from_user); String toUser vars.get(to_user); String roomId vars.get(room_id); long now Instant.now().toEpochMilli(); ChatMessage msg ChatMessage.newBuilder() .setMessageId(messageId) .setFromUser(fromUser) .setToUser(toUser) .setRoomId(roomId) .setMsgType(1) .setContentType(1) .setContent(ByteString.copyFromUtf8(im-load-test-message)) .setTimestamp(now) .setSeq(seq.incrementAndGet()) .setAckStatus(0) .build(); byte[] bytes msg.toByteArray(); vars.putObject(request_payload, bytes);注意几个容易出错的地方。第一vars.putObject 存的是 Object后面 WebSocket Sampler 要读取 byte[] 类型的数据不能存成 String否则插件会按文本帧发送服务端反序列化直接失败。第二message_id 的生成必须保证在跨线程场景下依然唯一用 UUID 加线程编号加自增计数是最稳妥的方案。第三seq 的递增不能依赖单线程私有变量要在 JSR223 里使用全局原子变量否则多个采样器并发写同一个字段会乱序。3.3 二进制帧怎么交给 WebSocket Sampler消息构造完成以后下一步是把它交给 WebSocket Sampler 发送。在 JMeter 里WebSocket Sampler 的请求数据框支持从变量读取。如果上面 JSR223 Sampler 已经把 byte[] 存到了 request_payload那么 WebSocket Sampler 的 Request Data 可以填写 ${request_payload}并将数据类型设置为 Binary。WebSocket Open Connection 组件也需要相应的配置。连接地址是 ws://测试环境网关地址/ws路径里带上 token 参数网关通过 token 识别用户身份。连接超时一般设 10 秒防止连接建立阶段卡住导致线程堆积。这里要特别提醒一点如果 WebSocket Sampler 的 Response Data 需要读取服务端返回的内容需要把 Read Response 设置为 true并设置一个合理的 Read Timeout。IM 场景中服务端返回消息不是即时的客户端发一条消息后可能要等几毫秒甚至几十毫秒才能收到回执。Read Timeout 如果太短会出现大量“等待回执超时”的误报如果太长又会让采样器长时间占用线程。我们最终按业务指标把 Read Timeout 设为 5 秒这样既不会误报也不会卡线程。4. JMeter WebSocket 压测脚本设计与实现4.1 压测场景的脚本结构怎么搭一个完整的 IM 压测脚本不能只有一个 WebSocket Sampler 来回发消息它至少得包含获取身份数据、建立连接、发送登录消息、等待登录回执、发送业务消息、接收消息、维持心跳、统计结果。这么一长串逻辑在 JMeter 里更像一个小型工作流。我设计的 Thread Group 结构大概是这样的配置元件CSV Data Set Config负责读取 user_id、token、room_id 等参数。JSR223 预处理根据当前线程的用户信息构造登录报文。WebSocket Open Connection建立连接。JSR223 Sampler发送登录消息并设置读超时等待登录回执。WebSocket Request Response Sampler发送业务消息二进制帧。WebSocket 心跳定时器定时发送心跳消息保持连接活跃。断言校验回执中的 message_id 是否与发送的消息匹配。第 4 步和第 5 步的区别要理解清楚。登录消息是一次性的业务消息是循环发起的所以它们在脚本里的位置不同采样器的循环次数控制也不同。登录消息放在 Thread Group 的 Setup 线程组里执行一次业务消息放在主循环线程组里反复执行。4.2 参数化与会话数据的管理长连接压测里最怕的就是把所有用户压成一个用户。CSV Data Set Config 的配置很简单但要注意 Sharing Mode 要设置为 Current Thread这样每个线程会从 CSV 文件里取不同行而不是所有线程共用一行。我们准备的 CSV 是三列user_id、token、room_id1000 个用户的数据量非常小JMeter 加载完全没压力。这里我要分享一个实践细节登录态的 token 不一定需要真的通过 HTTP 接口获取。压测场景下可以先离线批量生成一批 token 写入 CSV或者编写一个 JSR223 预处理脚本在本地按规则生产 token。真实 IM 系统的登录可能依赖短信验证码、外部认证服务压测时如果每条线程都走完整登录流程压测机的资源会被大量无关逻辑消耗。我们当时是直接把 token 生成逻辑做成一个小的 Java 工具类放到 lib 里压测脚本里通过调用工具类快速生成 token既模拟了真实登录态又不拖慢压测脚本。4.3 连接与登录阶段的关键时序WebSocket 和 HTTP 不一样连接建立以后不是马上就可以发业务消息的。我们的网关要求客户端首先要发一条 login 消息服务端返回 login_ack 后才能收发普通消息。这个时序在压测脚本里必须严格遵守否则服务端直接断开连接。所以我在每个线程的逻辑里做了两件事。第一发完 login 消息后强制采样器等待服务端回执确认拿到 login_ack 才继续第二判断回执内容如果回执里的 result_code 不为 200就记录一次连接失败并且终止当前线程防止带病连接继续压测污染数据。你可能会问为什么要这么严格因为如果有一批连接没完成握手就进入业务逻辑服务端会把这些连接标记为异常连接它们并不会真正参与消息收发。最后压测报告里的连接数会很高但实际有效请求很低TPS 指标完全失真。我在第一次压测时就因为这个吃了亏后来直接把这一步写成了一个断言才解决。4.4 心跳机制的模拟WebSocket 长连接场景下心跳必不可少。大多数 IM 服务端会设置一个空闲超时时间比如 60 秒内没有收到客户端消息就断开连接。压测脚本里如果不发心跳跑几分钟后所有连接都会被服务端主动断开。JMeter 的 WebSocket 插件里带了一个 WebSocket Ping Pong Sample可以直接发 WebSocket 协议层的 ping 帧。但实际 IM 系统的心跳往往不是协议层 ping而是应用层的心跳消息也就是向服务端发送一条 msg_type0 的业务消息。因此我们是在 JSR223 Sampler 里循环构造心跳消息用固定 Rate Timer 控制每 30 秒发送一次。使用定时器时有另外一个陷阱JMeter 的定时器作用于它所在的 Sampler 之前如果把它放在业务消息 Sampler 后面会拖慢业务消息的发送节奏导致 TPS 被压测脚本自身的定时器卡住。正确做法是让心跳消息单独走一个逻辑不要和业务消息混在同一个采样器流程里。我这边是把心跳放在 WebSocket Sampler 成功之后的子采样器里通过循环控制器每分钟执行一次保证它不影响主流程的吞吐。4.5 单聊、群聊、广播三种压测模式IM 系统的压测不能只测“大家都往同一个房间里发消息”这一种情况。我们根据业务场景把消息发送模式分成了三种单聊模式每个线程用户给一个固定的对方用户发私聊消息适合验证点对点消息链路的数据隔离和实时性。群聊模式每个线程用户向自己所在的群聊房间发消息房间内其他成员都会收到适合验证消息广播、扇出逻辑。公屏模式所有用户都在同一个大房间里发消息相当于集中广播对网关的下行推送压力最大。三种模式的压测数据不能混在一起否则报告没法解释。我是在 JMeter 里建了三个独立的测试计划通过命令行参数选择执行哪个场景。单聊模式跑出的 TPS 通常是三种里最高的因为消息只推给一个人公屏模式的 TPS 虽然看着高但每一条消息要被广播给大量连接服务端实际转发压力要大得多。这个差异在最终报告里一定要写清楚不能只看上行 TPS 就下结论。5. 压测执行过程与真实瓶颈案例5.1 执行策略先温压再阶梯加压压测执行不是一上来就开 2 万线程。我的标准流程是先做一轮小规模温压确认脚本没有问题。比如先用 100 个线程跑 5 分钟观察 JMeter 日志有没有连接失败、断言失败、超时告警。如果这轮就有问题直接停下来修脚本不要浪费后面的时间。确认脚本稳定后再进入正式的阶梯加压。我按 500、1000、2000、5000、10000 用户五档递增每档持续 10 分钟记录稳定阶段的指标。阶梯加压的好处是能够看到系统从“容量充足”到“接近瓶颈”再到“突破瓶颈”的完整变化过程。报告里如果能画出这样的曲线读者一眼就能看出拐点在哪。每档压测结束以后我会留两分钟观察服务端指标是否回落再启动下一档。如果服务端资源一直没有回落说明可能存在资源泄漏这时候继续加压会把系统压挂数据也没有意义。5.2 第一次瓶颈连接数上来以后握手失败率飙升第一个真实瓶颈出现在连接数压到 10000 以上的时候。现象是 JMeter 里出现了大量 WebSocket 连接失败错误信息是连接超时。但看服务端 CPU 和内存都在正常范围说明问题不在服务端处理能力上而是连接接入层的某些配置到达了上限。用 jstack 抓取服务端线程状态后发现大量 Netty 工作线程阻塞在同步等待新的连接 channel 上。进一步查下来问题出在服务端 TCP 连接的 accept 队列过短。Linux 系统里每个监听 socket 有一个 accept 队列用来暂存尚未被应用层 accept 的连接队列满了以后内核会拒绝新的连接请求。默认队列长度往往只有 128对普通服务足够但对 10000 并发连接来说完全不够。解决办法很直接修改 TCP backlog 参数同时调整 Netty 的 boss 线程组处理连接的配置。我们把 backlog 调到 4096 后重新压测连接失败率直接降到 0。这个案例很有典型意义IM 长连接场景下服务端代码本身的线程池可能没打满但底层连接队列是第一个瓶颈。5.3 第二次瓶颈CPU 没满但 TPS 上不去第二个瓶颈比较隐蔽。压测到 2000 用户单聊消息场景时服务端 CPU 使用率只有 55% 左右但消息处理 TPS 始终上不去稳定在 3000 左右。这时候 CPU 没满内存正常线程池也没打满但吞吐就是卡住不动。我们怀疑是某个流程里的串行锁或单线程队列拖住了整体吞吐。用 jstack 连续抓了几次线程栈发现一个现象有一个业务线程池里的线程几乎全部处于 BLOCKED 状态等待的锁对象指向同一个地方。查看代码后发现消息发送模块里一个用来做全局计数的 ConcurrentHashMap 操作被包在了一个 synchronized 块里而且这个锁的作用范围覆盖了消息序列化之后实际上锁内代码是很短的但因为并发量大锁竞争导致线程大部分时间都在等待。去掉了不必要锁保护把计数操作改成原子类之后同样压力下 TPS 从 3000 提升到了 7500 左右CPU 使用率也上到了 80% 以上。这个案例的启发很大压测发现了自己在生产环境很难发现的并发瓶颈这是压测最大的意义之一。5.4 第三次瓶颈长时间压测发生内存泄漏第三个问题是长时间压测跑出来的。我们在 10000 连接的场景下连续压了 30 分钟服务端内存开始缓慢增长最终触发了 OOM。查 JVM 堆 dump 后定位到消息网关在处理一条消息时把消息体放入了本地缓存但不是按 message_id 做过期淘汰而是简单地只增不删导致慢速积压。定位到具体的业务原因后修复也比较快。但我想强调的是压测不能只跑十几分钟就收工。很多内存类问题、连接泄漏类问题都需要长稳测试才能暴露。我们后面的每次版本发布前都会保留一个“10000 连接 8 小时长稳”的压测用例专门用来排查稳定性问题。5.5 压测报告怎么看指标对照与拐点判断这部分是很多同学问得最多的。JMeter 的聚合报告给了一堆字段哪些值得看Average、Median、90% Line、99% Line这些是响应时间关键分位数。重点看 99% Line它反映的是长尾延迟。如果 P99 明显比 P50 高出一个数量级说明存在少数消息被卡在某个慢路径上比如等待锁、等待队列。Throughput单位时间完成的请求数。IM 场景里一般看 messages/sec。Error %错误率。连接失败、断言失败、超时都算错误错误率超过 1% 基本可以判定当前档位不支持。连接数曲线如果服务端能维持的长期稳定连接数和 JMeter 线程数一致说明连接能力没有问题如果压测中途连接数掉线大概率是心跳或者空闲超时。报告里需要包含的一个关键内容是指标拐点。比如随着用户数从 1000 涨到 5000TPS 几乎线性增长说明系统还处于有余力的状态继续增加到 8000TPS 增长变缓P99 开始明显抬升这时候就说明接近瓶颈了再往上加用户TPS 开始下降P99 突跳这意味着已经过载。报告里把这些拐点写清楚服务端同学才会真正重视压测结果。6. 常见问题与排查技巧实录6.1 WebSocket 连接频繁被服务端断开现象压测运行一段时间后JMeter 里的连接出现大量断开重连。原因大多是心跳超时。服务端如果规定 60 秒内没有任何消息就断开连接客户端必须在 60 秒之内至少发一次心跳。需要注意这里的“任何消息”包含 WebSocket 协议层的 ping 帧吗很多系统是不包含的必须发应用层业务心跳。排查时用 Wireshark 抓包看连接断开前是否有心跳帧一目了然。6.2 JMeter 压测机负载过高导致结果失真跑 10000 线程以上时压测机可能先扛不住。CPU 打满、JVM 频繁 GC都会导致采样器本身变慢进而拖低压测结果。遇到这种情况优先考虑分布式压测把线程分配到多台压测机上。如果只有一台机器可以减少不必要的断言和监听器尤其是图形化监听器非常消耗资源。6.3 protobuf 字段构造错误导致消息丢死服务端解析 protobuf 消息失败时不会直接报错而是会记录一条 WARN 日志然后丢弃消息。JMeter 这边看到的现象是发了消息但收不到回执。排查方法是打开服务端的 protobuf 解析日志或者先用一个简单的 Java 客户端验证构造的字节能否被正常解析。最容易出错的是 message_id 重复和 seq 乱序这两个字段必须保持严格唯一和严格递增。6.4 JMeter 自身出现 OutOfMemoryJMeter 默认堆内存只有 1G跑长连接压测很容易 OOM。修改 JMeter 安装目录下的 jmeter 启动脚本把 JVM 堆参数改成export HEAP-Xms4g -Xmx4g -XX:MaxMetaspaceSize512m还有一点容易被忽略JMeter 的日志和临时文件会持续写入磁盘长期压测时建议把结果存储改为 CSV 而不是 XML因为 XML 写入的体积是 CSV 的十倍以上。6.5 采样器超时时间和实际网络时延不匹配如果你把 Read Timeout 设成了 1 秒而服务端在高峰期处理消息需要 1.5 秒那么大量正常消息会被误判为超时。唯一的办法是参考服务端的历史延迟分布来设置超时宁可设置 5 秒或者更宽松也不要为了追求低超时导致误报。6.6 一点额外的经验压测完别急着删脚本每次压测完成以后我会把测试计划、参数文件、服务端监控数据、报告原文归档到一个单独目录。下次做性能优化或者版本升级以后用同一套脚本重新跑才能做到“同样条件下对比优化前后”的公平比较。这一点看起来不复杂但很多团队没做到导致每次压测之间没有可比性之前的指标都白跑了。经过这几轮折腾我个人最大的体会是IM 消息系统的压测真正难的不是把 JMeter 配起来而是如何让模拟流量尽可能地贴近真实业务。协议结构、连接行为、心跳时序、数据规模任何一个环节失真最后报告里的数字就都成了自嗨。把 ChatMessage.proto 里的每个字段吃透把 JMeter 脚本打造成一个能真正模拟真实客户端行为的工具再去谈 TPS、RT 和瓶颈优化整个压测这件事才算真正有了价值。