从零到生产:构建百万并发分布式 IM 系统的架构全解
从零到生产:构建百万并发分布式 IM 系统的架构全解如何设计一套真正能落地的分布式即时通讯系统?本文不只讨论“能跑起来”的 Demo,而是从连接接入、消息路由、存储模型、一致性语义、群聊扇出、限流熔断、可观测性、容灾与工程化交付等维度,完整拆解一套可支撑百万长连接、亿级日消息量的生产级 IM 架构。一、为什么 IM 系统难做即时通讯系统表面上只是“发消息”,本质上却是一个典型的高并发分布式系统问题:它同时具备海量长连接、高频小包、状态敏感、强交互实时性的特征。它要求消息在绝大多数场景下“看起来可靠、有序、及时”,但底层网络、服务实例、存储系统、消息队列都天然存在不确定性。它既要满足单聊这种相对简单的点对点通信,也要处理万人群、离线消息、多端同步、撤回、已读回执、历史漫游、推送补偿等复杂业务。很多文章把 IM 架构讲成了“WebSocket + Redis + Kafka + MySQL”的技术拼盘,但真正的难点从来不是组件名词,而是:消息语义怎么定义顺序边界怎么保证路由状态如何维护大群消息如何扇出慢连接如何隔离多端登录怎么同步扩缩容和故障切换时怎么不雪崩如何把这些能力工程化并稳定运行这篇文章会围绕这些核心问题展开。二、业务背景与目标设定假设我们需要为一套企业协同 SaaS 平台构建 IM 能力,业务约束如下:指标目标DAU500 万峰值在线120 万单日消息量2 亿峰值吞吐30 万条/秒消息实时性单聊 P99 300ms,群聊 P99 800ms登录终端Web / iOS / Android / 桌面端可靠性目标不丢消息,允许有限重复,可恢复可用性目标核心链路 99.95%+从业务上看,系统至少要支持:单聊群聊离线消息多端同步消息已读/未读消息撤回在线状态历史消息查询图片/文件消息推送补偿这决定了系统不能只追求吞吐,还必须把一致性语义说清楚。三、先定义语义,再设计架构在 IM 系统里,如果一开始不定义消息语义,后面所有设计都会变形。3.1 必须明确的四个问题1. 消息是否绝对不重复答案通常是:做不到,也没必要。工程上更合理的目标是:服务端提供至少一次投递客户端和存储侧提供幂等去重用户体验上表现为“消息最终只展示一次”这比盲目追求“精确一次”更现实,也更符合大规模分布式系统实践。2. 顺序在哪个范围内保证IM 中通常不追求全局顺序,而是保证:单聊会话内有序群聊在同一会话维度尽量有序跨会话无需有序换句话说,顺序保证的最小粒度是conversationId,而不是整个系统。3. 消息写入和消息投递谁先谁后推荐原则:先落库/落日志,再投递如果在线投递失败,依然可以依赖离线拉取恢复这样系统的“真相源”是存储和日志,而不是连接层内存。4. 客户端以什么为准恢复消息不是按时间戳,而是按会话游标 cursor / seq恢复。时间戳会受时钟漂移影响,游标才适合作为可靠恢复基准。3.2 推荐的消息语义模型对大多数企业 IM,推荐采用下面的语义定义:发送语义:客户端发送成功,表示服务端已接收入队,不代表对端已收到存储语义:服务端先持久化消息,再进行异步在线投递投递语义:至少一次投递展示语义:客户端按msgId幂等去重顺序语义:会话内按seq单调递增展示恢复语义:客户端断线重连后按最后确认的seq拉取增量消息把这些规则先定下来,后续架构设计才有稳定边界。四、生产级分布式 IM 总体架构4.1 总体分层4.2 各层职责层级核心组件职责接入层Connection Gateway维持 WebSocket 长连接、认证、心跳、限流核心业务层Message / Group / Session / Presence消息写入、会话序列生成、群成员解析、状态同步事件层Kafka流量削峰、异步投递、分区顺序、失败重试状态层Redis Cluster在线状态、连接路由、热点会话缓存持久化层MySQL / TiDB消息、会话游标、成员关系、回执信息检索与审计ES / ClickHouse全文搜索、运营分析、审计追踪附件存储MinIO / S3图片、语音、文件4.3 为什么要拆成“接入层 + 核心层”原因非常关键:连接层和业务层扩缩容诉求不同连接层按在线连接数扩业务层按消息吞吐扩连接层更接近网络栈关注 fd、心跳、背压、慢连接业务层更接近状态机关注消息语义、序列号、一致性、群成员关系如果把连接、业务、存储都塞在一个服务里,早期看起来简单,后面几乎一定会在扩容、发布、故障隔离上付出巨大代价。五、关键架构原理拆解5.1 连接管理:如何维护百万长连接长连接系统的第一个核心问题是:如何在分布式集群中准确找到用户当前连接在哪台机器上。路由模型推荐采用两级路由:本地连接池每个 Connection Gateway 维护本地connId - Connection映射全局路由表Redis 中维护userId - routeInfo示例:route:user:1001 - { "gatewayId": "conn-gw-12", "connId": "c-8fa1b2", "deviceId": "ios-001", "lastActiveAt": 1715750000 }为什么 Redis 路由表要带 TTL因为连接是易失状态,网关异常退出时不一定有机会主动清理路由。TTL 可以兜底,避免脏路由长期存在。典型做法:心跳间隔:10 秒路由 TTL:30 秒每次收到客户端心跳时刷新 TTL多端登录怎么处理这取决于业务策略:单端在线:新连接顶掉旧连接多端在线:userId - deviceId - routeInfo同端单实例:同一个deviceType只允许一个活跃连接生产上更常见的是:手机端单实例Web 和桌面端允许并存因此路由模型最好从一开始就支持多设备维度。5.2 消息链路:为什么不能“收到消息就直接推给对方”因为那样消息只存在于内存,一旦服务异常就可能丢失。生产上合理的主链路应该是:Client A - Connection Gateway - Message Service - 分配会话序列号 seq - 持久化消息 - 发送消息事件到 Kafka - Online Dispatcher 查找对端路由 - 投递给目标 Gateway - Client B 收到消息 - Client B ack这条链路的核心思想是:存储是事实来源在线推送是加速路径离线拉取是兜底路径因此,即便在线投递失败,只要消息已经落库,客户端重连或主动同步后仍然能恢复。5.3 顺序性:为什么要按会话分区如果单聊消息的顺序被打乱,用户感知会非常明显。推荐做法是:以conversationId作为 Kafka 分区键一个会话的所有消息进入同一分区消费端按分区顺序处理这样可以保证:同一会话内消息天然有序不同会话之间可以并行处理这也是 IM 系统中“局部有序、全局并行”的经典设计。5.4 群聊扇出:系统真正的压力点群聊的难点不在“存一条消息”,而在“把这条消息发给多少人”。以一个 5 万人大群为例,一条群消息可能引发:5 万条在线投递请求5 万个未读计数更新5 万个离线游标推进若带推送,还会触发成千上万条 push 事件如果处理不当,一个大群就足以把系统打穿。群消息推荐架构建议拆成两个阶段:阶段一:消息入库写一条群消息主记录生成群会话seq