微服务架构下的即时通讯项目,系统联调从来不是把接口一个个调通那么简单。前阵子我带团队完整跑了一轮 IM 系统的联调,从服务拆分、注册中心打通、核心消息链路验证,到用 Jmeter 做高并发压测,最后再把整套环境迁到云上重新验证承载能力,整个过程下来最大的感受是:联调的本质不是“点对点通不通”,而是“整条链路上每一步都经得起反推”。这篇就直接按实际操作顺序写,适合正在做微服务 IM 项目联调的团队参考,也适合打算把测试环境搬上云做压测验证的人借鉴。
1. 项目背景与联调范围界定
1.1 这套微服务 IM 系统到底拆成了什么
这次联调的对象是一个典型的多端 IM 后台,需要同时支撑 App、H5、桌面端在线聊天。按照微服务思路拆完以后,主要服务大致如下:
| 服务名 | 主要职责 | 联调重点关注 |
|---|---|---|
| api-gateway | 统一入口、鉴权转发、限流 | 路由是否准确、token 校验是否生效 |
| auth-service | 登录认证、token 签发与刷新 | 登录接口、刷新 token 逻辑 |
| user-service | 用户资料、好友关系、组织架构 | 好友关系校验、资料查询 |
| chat-service | 会话管理、消息收发、在线状态协调 | 单聊/群聊消息链路 |
| message-service | 消息持久化、离线消息、已读回执 | 消息状态流转、离线拉取 |
| push-service | APNs、厂商推送等离线通知 | 离线消息推送触发 |
| file-service | 图片、视频、文件上传与访问 | 上传鉴权、访问链接有效性 |
| group-service | 群组管理、群成员维护 | 群聊消息与成员变更联动 |
单看表格可能觉得这些服务各干各的,但联调真正考验的是它们之间怎么配合。比如用户发起登录,请求先打到网关,网关从 auth-service 校验身份,拿到 token 后再访问 user-service 拉取资料;发消息的时候,chat-service 要同时协调 message-service、消息队列、push-service 和在线状态服务,任何一环状态对不上,用户立刻就会感知。如果团队里能画出一张微服务架构图,把每个服务的调用方向、消息队列、缓存依赖标清楚,后面联调排查至少能节省一半时间。
1.2 为什么 IM 项目联调比普通 Web 项目更难
普通 CRUD 项目联调失败,最多是接口 500,前端一看报错,后端一看日志,问题通常就锁定了。IM 项目不一样,消息链路长,而且大量状态是实时、分布式的。
长连接不在 HTTP 的连接池里,错误不会在浏览器 Network 面板直接显示;在线状态存在 Redis,不同节点看到的可能不一致;消息顺序、去重、幂等,任何一项没有在联调阶段测到,上线后就是事故。所以联调之前,必须先想清楚边界:这次要验证哪些链路、哪些核心指标,而不是把所有接口都点一遍就算完成。我们当时把范围锁在三块:登录鉴权链路、单聊消息必达链路、离线消息与已读回执链路,性能验证单独放到压测阶段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 联调开始前:把环境和契约先钉死
2.1 测试环境到底用什么跑微服务
我们这次在测试环境没有直接上多节点集群,而是用单节点 k8s 跑整套微服务环境。日常开发时各模块在本地 IDE 里直接跑也没问题,但一旦开始联调,就建议全部容器化部署。原因是单节点 k8s 虽然做不到真正的多机房容灾,但可以把服务发现、配置中心、Pod 重启、日志收集这些分布式问题提前暴露出来。
项目基础能力用了若依微服务这套骨架,用户、权限、日志这些模块开箱即用。IM 相关服务单独注册到同一个 Nacos 实例下,服务间通过 Feign 调用,消息走 RocketMQ。单独跑某个服务看不出毛病,真正把整套环境拉起来之后,问题才会显现。想想看,本地直接连 localhost 的 Redis,跑得再顺也验证不了服务注册、配置拉取、跨节点调用这些微服务基础能力。
2.2 联调前必检清单
列个清单,不满足不要开始点接口:
| 检查项 | 具体要求 |
|---|---|
| 注册中心 | 所有服务注册状态健康,端口与服务名对应 |
| 配置中心 | 环境配置切换正确,数据库、Redis、MQ 地址不是 localhost |
| 中间件 | MySQL 表结构已初始化,Redis、MQ Topic 已创建 |
| 网关路由 | 每个路由指向的服务名与注册中心一致 |
| 接口契约 | 响应统一 JSON,错误码统一,请求头带 traceId |
| 数据准备 | 测试账号、好友关系、群成员数据完整 |
为什么强调这些?联调时最常见的尴尬是:服务在本地能跑,但是注册中心里看不到;或者 A 服务写了一下午,最后发现调的是 B 服务本地启动的另一个端口。把所有服务拉到一个命名空间里,用一套环境跑,才谈得上联调。
2.3 链路追踪和可观测性不能省
无论你用的是若依微服务、Spring Cloud,还是 NestJS 这类 Node.js 技术栈,监控与可观测性实践的核心思路都一样:必须有一个 traceId 贯穿网关、业务服务、消息队列消费端、数据库访问全链路。
我们前一天因为没加 traceId,一个消息丢失问题查了三个小时;加了之后,从入口日志直接能看到消息在哪个服务被丢弃,十分钟内定位。联调阶段就把 traceId 打出来,后面压测定位瓶颈也会轻松很多。日志这块别凑合,至少做到:每个服务接入同一个日志采集平台,输出必须带 traceId 和服务名。没有这个基础,后面做高并发压测基本是瞎子摸象。
3. 核心链路联调实操:从登录到消息必达
3.1 登录与鉴权链路
先调 auth-service 的登录接口,拿到 JWT。联调环境不要用生产密钥,签发密钥放配置中心,不同环境隔离。
bash复制curl -X POST http://gateway/api/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"test01","password":"123456","clientId":"im_app"}'
正常响应会返回 accessToken、refreshToken、expiresIn。拿不到 token 时,先查 auth-service 日志,看是账号不存在、密码错误,还是网关路由没正确转发。
这里有个细节必须坚持:所有联调请求必须走网关,不要绕开网关直连服务。很多团队在本地联调为了省事,直接填服务端口,一旦绕过网关,签名校验、限流、路由这些能力全都没有被验证,联调结果等于白测。
3.2 WebSocket 长连接建立
拿到 token 后,客户端连接网关的 WebSocket 接口,一般会把 token 放在 URL 参数或者 Sec-WebSocket-Protocol 头里。
bash复制wscat -c "ws://gateway/ws?token=xxx"
连接建立后,在 Redis 里应该能看到在线状态键,比如 online:user:1001。如果看不到,优先检查连接建立后是否调用了状态服务上报接口,而不是先怀疑 Redis。
还要确认网关层和 websocket-service 层都做了一遍 token 校验。网关只负责把连接分发到节点,真正的鉴权必须落到 websocket-service,防止有人绕过网关直接连节点端口。联调时最好专门做一个无 token 或过期 token 连接的用例,确认能被拒掉。
3.3 单聊消息发送的完整链路
消息链路是整个联调最核心的部分。我的习惯是先按“能收到消息”来测,再按“消息不丢、不乱序、不重复”来测。
- 客户端 A 发送消息到网关。
- chat-service 保存会话并调用 message-service 落库,消息状态为 SENDING。
- message-service 发送
MESSAGE_SEND事件到 RocketMQ。 - 消费端拿到事件后,从 Redis 查接收方 B 的连接节点。
- 如果 B 在线,通过所在节点推送;如果离线,写入 offline_message 表并触发 push-service 发送离线通知。
- B 收到消息后回 ACK,message-service 把状态更新为 READ。
text复制String receiverNode = redis.get("online:user:" + toUserId);
if (receiverNode != null) {
imServer.sendByUserId(toUserId, messageEnvelope);
} else {
offlineMessageService.save(conversationId, messageEnvelope);
pushService.push(toUserId, messageSummary);
}
为什么用 Redis 存节点 ID 而不是直接存 IP?因为节点重启后,连接映射会变化,存节点 ID 再通过注册中心反查节点地址,可以做到重启无感迁移,客户端也不用重新连接。联调时一定要验证一次节点重启场景,看看连接到旧节点的用户是否需要重连,消息有没有因为这几十秒的空窗期丢失。
3.4 离线消息和消息必达验证
联调时不能只测在线消息,离线消息、多端登录、已读回执都要给到明确结论。我们当时测了一个关键场景:A 给离线的 B 发 10 条消息,B 再上线,应该收到 10 条消息,且历史记录里也是 10 条。
这个场景最容易暴露问题:消息入库和消息队列发送不是同一个事务,DB 写入成功但 MQ 发送失败,会导致服务重启后消息丢失;MQ 消息被重复消费,B 上线会收到重复消息。
我们的做法是:消息表增加一个 send_status 字段,发送前置为 SENDING,消费端处理成功后更新为 SENT,客户端 ACK 后更新为 READ。消费端在 Redis 里用 dup:msgId 做去重,重复消息直接丢弃。测完之后再看一次消息表的状态分布,如果还有大量记录卡在 SENDING,说明消费链路有问题,优先排查 MQ 消费组和连接分配逻辑。
4. 联调中我踩过的坑:问题速查与复盘
4.1 高频问题汇总表
这张表是我从这次联调中整理出来最高频的几类问题,前两个属于协议层配置问题,后四个属于分布式一致性范畴。
| 症状 | 可能原因 | 解决方式 |
|---|---|---|
| 连接建立后 1 分钟被断开 | 心跳超时设置太短,客户端没发心跳 | 统一约定 30s 发 ping,服务端 90s 未收到再断开 |
| 消息发送成功但对方收不到 | MQ Topic 配置不一致,消费组没订阅到 | 用 MQ 控制台查看消息积压和消费情况 |
| 多端登录互相踢 | 同一 userId 抢占连接时逻辑不严谨 | 以设备唯一 ID 维度做连接管理 |
| 消息偶现重复 | 消费端没做去重,ACK 超时后 MQ 重投 | Redis 去重或数据库唯一索引兜底 |
| 在线状态不一致 | 节点本地缓存更新后没同步到 Redis | 状态变更统一走 Redis pub/sub 或共享缓存 |
| 接口偶发超时 | 数据库连接池或 Redis 连接池被打满 | 压测前算好 maxActive 和 maxWait |
4.2 排查链路的时候别一头扎进日志
很多人排查问题喜欢先打开服务日志找 error,这个习惯在微服务环境里效率很低。正确顺序应该是:先看注册中心,确认所有服务是否健康;再看链路追踪,确定耗时集中在哪个环节;最后再看具体服务的日志。
比如消息发送慢,不能只看 message-service 的日志。要先确认是网关转发慢,还是 MQ 消费慢,还是接收方节点推送慢。只盯一个服务,永远查不出来。
另外,连接管理这类问题用 Redis 和消息队列控制台查很快。Redis 里的在线状态键有没有,MQ 控制台里消费组有没有积压,基本一眼就能判断问题出在哪个环节。
4.3 几个值得单独写出来的细节
第一,MQ 消费的幂等不是可选项,必须有。哪怕你确认消息只会被发送一次,也架不住生产环境网络抖动带来的重投。用消息唯一 ID 做去重成本很低,不做就是定时炸弹。
第二,多端登录打掉旧连接前,要确认是新登录还是误重连。客户端网络切换后可能会用同一个 token 重新建立连接,这时候如果把旧连接踢掉,用户会明显感觉到会话不稳。按设备 ID 区分连接,比按 userId 简单粗暴地踢人稳妥得多。
第三,心跳间隔要与服务端超时时间匹配。有的客户端只发业务消息,不发心跳,服务端又设置 60s 超时,结果用户一发呆就被踢下线。联调时一定要把“静默挂机”场景跑一遍。
5. 用 Jmeter 做高并发联调:压力测试和承载能力验证
5.1 压测脚本怎么设计
联调通过不等于能抗住并发。这次我们约定,所有核心接口都要用 Jmeter 脚本压过一轮,并且压测任务不是等联调结束再做,而是边联调边摸底。压测脚本最好由专门负责压测的人维护,避免开发自己压自己容易忽略边界条件。
当时分了三类场景:登录与 token 获取、WebSocket 消息发送、拉取历史与已读回执。每个场景都建了独立的线程组,参数用 CSV 文件管理,模拟不同用户而不是压同一个账号。
| 场景 | 并发用户 | Ramp-up | 循环次数 | 断言 |
|---|---|---|---|---|
| 登录获取 token | 500 | 10s | 100 | 响应 code=0 |
| WebSocket 发送消息 | 200 | 20s | 50 | 收到 ACK |
| 拉取历史与已读回执 | 300 | 15s | 100 | 响应 code=0,耗时小于 800ms |
特别提醒,直接用 Jmeter 默认 HTTP Sample 测 WebSocket 是测不了的,需要装 WebSocket Sampler 插件,或者在工具里用自研扩展。我们用的方案是给每个用户建立一个长连接,再在这个长连接上循环发送消息,这样压出来的数据才贴近真实聊天场景。
5.2 压测结果怎么读
不要只看平均响应时间。即时通讯对长尾延迟特别敏感,用户感知到的卡顿往往是 p99 延迟很高,而不是平均延迟。
我看指标的习惯是:先看错误率,超过 0.1% 就要停;再看 p95 和 p99;最后看服务端 CPU、内存、GC、数据库连接池。把这些放在一张图里对比,很快能看出瓶颈在哪。
举例:第一次压测,500 并发消息发送,网关 CPU 只有 30%,但 message-service 的数据库连接池被打满,导致平均响应时间飙升。一开始都以为是网关问题,排了半天才发现是 DB 连接池配置太小。联调阶段提前发现这种问题,比上线后半夜被人叫起来好一百倍。
5.3 云上环境迁移后的验证思路
我们的测试环境原本在本地单节点 k8s 上跑,后来要把整套迁移到云上 ECS,并且要求尽量不停服、不丢数据。这里头的关键不是 k8s 和 ECS 谁好谁坏,而是迁移后必须重新验证整套服务的承载能力。
数据迁移建议用同步工具先把 MySQL、Redis 的数据拉平,切换前保持旧环境只读,新环境接收写流量,双跑一段时间确认两边数据一致后再正式切流量。服务切完之后,压测人员再用同一套 Jmeter 脚本重新跑一轮,把云上环境的连接数、QPS、错误率跟本地基线对比。
云上环境还要额外检查几项:安全组是否放通了 IM 需要的端口,MySQL 的 max_connections 是否够用,注册中心地址是否需要从内网 IP 换成公网入口。不要以为迁移完接口能通就万事大吉,云上的网络路径和本地不一样,同样的压测脚本跑出来的吞吐量往往会差一大截,这一步不做等于没压。
最后再说一个我在多次联调里养成的习惯:每次联调结束,把接口契约、环境地址、常见问题表更新到团队共享文档,而不是留在聊天记录里。微服务项目越往后拆越细,人的记忆靠不住,文档才是真正的联调资产。这个东西在下次扩服务、招新人、做培训的时候,能帮团队省下大量无效沟通时间。
