从压测 8000 并发直接被打崩,到后面稳定扛住 5 万在线、消息峰值 1.5 万条/秒,中间经历的大大小小的坑和优化,我觉得值得单独拿出来聊一聊。标题里的"即时通讯源码",实际指的是一套基于 Java NIO 自研的 IM 服务端,不是直接用开源框架二次开发的那种,而是把 Netty 通信层、自研协议解析、消息路由、离线存储都拢在一起的完整项目。这套系统上线前的压测表现很差,消息稍微一密集,服务端 CPU 就飙到 90% 以上,内存抖动严重,客户端明显感觉到消息延迟,更别提高峰期还有消息丢失的情况。如果你手头也有一套 IM 源码在调优,或者正在做高并发长连接类的服务,这篇文章里的排查思路和优化手法应该对你有直接用处。
我先把当时定位到的主要问题列出来。三个核心瓶颈,几乎和标题里的三个关键词一一对应:第一,流量洪峰时消息积压严重,消费速度跟不上生产速度;第二,网关层的连接分配策略太粗暴,所有新连接全往一台机器上怼;第三,堆内存里塞了大量 byte[] 和对象副本,GC 频繁到每秒好几次。下面挨个拆开讲。
1. 一上线就被消息洪峰打爆:问题到底出在哪
很多 IM 项目在开发环境跑得好好的,单测、接口测试全部通过,丢到生产环境一上量就趴窝。最典型的场景就是业务方做一次全员推送,或者在某个秒杀节点大量用户同时触发消息。系统卡住的表现通常是:客户端消息发出去没有回执,服务端日志里出现大量"队列已满"的报错,数据库连接池被打满,CPU 飙到 100% 后触发告警。
1.1 先画一张消息流转链路图,把每一环的耗时量化出来
我的习惯是,接手任何性能问题,第一件事不是改代码,而是把完整链路画出来,给每一段加一个耗时采集点。IM 的消息路径大致是这样的:客户端通过 Netty 接入,消息解码后进入业务线程池,然后经过消息校验、关系链检查、消息持久化,最后推送给在线接收方,不在线的写入离线表。
给每一段加上耗时统计后,我发现最离谱的不是 Netty 的编解码,也不是推送逻辑,而是消息持久化这一步。项目中用的是 MySQL 的批量插入,但批量大小固定为 500 条一次,并且每次插入都走的事务提交,也就是每 500 条消息就 COMMIT 一次。在消息高峰期,单台机器的数据库连接池只有 20 个连接,插入一条消息的平均耗时在 15ms 左右,500 条一批就是 7.5 秒。也就是说,上游生产者倒进来 5000 条消息,消费线程组拼死拼活也要好几秒才能落地,消息越积越多,最终触发内存堆积。
这其实是很多 IM 源码里最常见的共性问题——性能差的不是单点能力,而是链路中"木桶最短的那块板"。你通信层再快,落到存储这里变成了串行瓶颈,整体吞吐就上不去。
1.2 为什么说"同步落库"是即时通讯消息链路里最大的败笔
IM 消息在语义上确实需要可靠投递,但"可靠"不等于"每条消息都必须在内存里等数据库返回成功后才往下一个环节走"。如果你认真读一下主流 IM 系统的设计文档,会发现几乎没有一个系统会在业务线程里同步做数据库写入。
原因很简单:数据库写入的耗时方差太大。本地 SSD 上一条简单 INSERT 可能 1ms 就完成,但遇到锁竞争、binlog 刷盘、主从延迟,单条写入飙到 50ms 也不奇怪。一旦你在同步路径上等数据库,你就要为这个不可控的方差买单,线程池被打满、队列积压、内存暴涨都是连锁反应。
当时我做的第一个调整,就是把消息持久化从同步改成异步:业务线程只需要把消息体投递到内存队列就立即返回,由专门的落库线程组批量消费队列,攒够一批或者达到时间阈值再批量写库。这一步改完,单机吞吐从每秒 1200 条直接跳到 5000 条以上,效果立竿见影。
注意:异步化不是把问题消灭,而是把压力从"请求线程"转移到"后台批处理线程"。所以队列必须有界、必须监控积压量、必须设计背压策略。否则内存队列本身就会变成新的瓶颈。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 削峰不是扛峰值,而是削掉"没必要的压力"
很多人对削峰的理解是:加大服务器配置、扩大线程池、提高队列容量,硬扛峰值流量。这是完全错误的思路。削峰的核心是让系统在峰值流量
