案例拆解:典型 IM 架构的取舍
架构选型没有标准答案,只有在给定约束下的取舍。本章拆四个公开可查的典型形态:单机网关加 Redis 路由、多机房就近接入、大群的消息扩散,以及自研长连接与开源协议、云服务之间的分界。文中所有数值都是量级示意,用于说明推导方式,不对应任何厂商的内部数据;涉及具体实现细节时以公开资料为准。
案例 A:单机网关 + Redis 路由
约束:DAU 十万到百万之间,峰值 CCU 十万级,团队三到五人,SLA 99.9% 即可。
客户端 ──长连接──▶ 接入网关 N 台(无状态:连接、心跳、编解码、限流)
│ 内网 RPC
逻辑服务(收发、会话、群成员、投递编排)
│
Redis(路由表 + 会话缓存 + 未读数)|MySQL(消息、会话、关系)|MQ(落库与扇出)
| 决策点 | 选择 | 代价 | 什么时候必须换 |
|---|---|---|---|
| 路由表 | Redis 单集群,key 为 uid+device 指向网关实例 | 容量与故障域都集中在 Redis | 连接数超 300 万或内存吃紧 → 按 uid 分片 |
| 消息落库 | 同步写 MySQL 成功后才回 ACK | 写延迟进 P99,主库压力大 | 写入 QPS 超 2 万 → 先入 MQ 异步落库,同时补上客户端补偿逻辑 |
| 群聊 | 读扩散,成员各自按会话拉取 | 在线的即时性差 | 群规模超 200 且要求即时 → 在线广播 + 对活跃成员写扩散 |
| 多端同步 | 每设备一条同步游标 | 存储与回执量随设备数线性增长 | 设备数超 3 → 合并为会话级游标 |
| 在线态 | Redis 存最后一跳心跳时间 | 高频写,需合并写 | 心跳频率提高 → 改本地聚合后批量上报 |
| 部署 | 单机房多可用区 | 机房级故障不可用 | 出现跨地域用户或 SLA 高于 99.9% → 多机房 |
要点:这个形态比想象中耐用。无状态网关加 Redis 路由表是最省心的组合,真正的瓶颈通常出现在 MySQL 写入与 Redis 单集群内存上,而不是网关转发;等到这两处告急再谈分片与多机房,能省下大量复杂度。
案例 B:多机房就近接入
约束:用户跨地域、CCU 百万级、SLA 99.95% 以上、有数据合规要求。
┌── GSLB:按地域与健康度解析,外加接口下发的接入点列表 ──┐
华东用户 ──▶ 单元 A(网关 + 逻辑 + Redis 路由分片 + MySQL 分片)
华南用户 ──▶ 单元 B(独立一套,单元内闭环)
海外用户 ──▶ 单元 C(延迟与合规双重考虑)
跨单元投递:接收方不在本单元 → 走消息总线转发,而不是直接写对方连接
| 决策点 | 选择 | 代价 | 什么时候必须换 |
|---|---|---|---|
| 单元划分 | 按 uid 哈希固定归属,用户不迁移 | 扩容要成对加单元,粒度粗 | 单单元容量不足 → 单元内再分片 |
| 会话归属 | 按发起方 uid 所在单元 | 双方跨单元时多一跳转发 | 跨单元消息占比超 30% → 引入会话归属中心 |
| 路由表 | 单元内自治,不做跨机房实时复制 | 跨单元投递依赖消息总线 | 需要全局在线态 → 加一层只读的在线汇总,接受秒级延迟 |
| 消息数据 | 一个会话只落在一个单元,避免双写冲突 | 该单元故障时会话不可写 | 要求会话级容灾 → 会话内主备单元 + 异步复制 |
| 推送 | 在用户归属单元发起厂商推送 | 单元故障时推送中断 | 推送通道独立部署,避免与 IM 单元同生共死 |
| 切换 | GSLB 摘除 + 客户端换接入点 | 切换窗口内连接重建有风暴 | 平滑性不够 → 客户端多接入点并发探测 |
关键结论:多机房的核心不是“部署两套”,而是把“谁是权威”写清楚。路由、会话、数据各有一个权威归属,其他副本只读或异步;否则并发写冲突会在故障切换的瞬间集中爆发,表现为“同一条消息两个版本”“两个单元都认为用户在线”。
案例 C:大群的消息扩散
| 维度 | 写扩散(fan-out on write) | 读扩散(fan-out on read) | 分层混合 |
|---|---|---|---|
| 扩散时机 | 发送时写 N 份收件箱 | 接收方拉取时按会话查 | 小群写扩散,大群读扩散 + 在线广播 |
| 存储放大 | N 倍(N 为群人数) | 1 倍 | 按群规模分档 |
| 发送耗时 | 随成员数上升 | 与成员数无关 | 与成员数无关 |
| 读耗时 | 一次读收件箱,快 | 需合并多个会话 | 在线快,离线需合并 |
| 一致性 | 每份收件箱各自维护位点,容易漏推 | 单一真相,天然一致 | 复杂,必须把分档规则写死 |
| 离线补拉 | 直接读收件箱位点 | 按会话位点增量拉取 | 两者并用 |
| 适用群规模 | 500 人以内 | 千人以上 | 全规模 |
超大群的分层做法:
1 万 ~ 10 万人群:
1) 发送只落一份消息(读扩散),同时向“当前在线成员”广播
2) 在线成员靠广播即时展示,离线成员靠上线后按位点拉取
3) 广播限速与合并:同一会话 200ms 内的多条消息合并成一次下发
4) 非活跃会话(成员 7 天未打开)不下发正文,只更新未读数与摘要
5) 超出投递预算时降级为“有新消息”提示,正文由客户端拉取(见 28 章)
两个容易踩的坑:读扩散下“未读数”与“会话最后一条摘要”必须能低成本算出来,否则群列表会退化成 N 次聚合并成为新瓶颈;群成员变更后扩散范围必须重建,否则新成员看不到历史、退群成员仍在收消息。取舍判据很直接:只要“群成员数 × 消息条数”超出存储预算或发送延迟预算,就该切读扩散;反之写扩散换来的是最简单的读路径。
案例 D:自研长连接、开源协议与云服务
| 方案 | 适合的场景 | 换来什么 | 代价 |
|---|---|---|---|
| 自研长连接(WebSocket/TCP + 自定义协议) | 需要精确控制协议、成本与多端同步语义 | 协议可裁剪、单位成本可控、优化自由度高 | 连接治理、推送、监控、容灾都要自建,人力是最大成本 |
| 基于 MQTT(公开规范) | 弱网、设备侧混合场景、已有 MQTT 生态 | 成熟的 QoS 分级、遗嘱消息、订阅模型 | 面向主题而非人与会话,会话、历史、已读等业务语义要自己补 |
| 基于 XMPP(公开规范) | 需要联邦、与第三方互通 | 标准化扩展协议族齐全 | 以 XML 流为核心,移动端流量与实现成本偏高 |
| 云厂商 IM 服务 | 快速上线、团队小、需要全球节点 | 免运维,自带推送与合规节点 | 长期单位成本与定制自由度受限,业务逻辑仍需自研 |
| 长连接 + 厂商推送组合 | 移动端为主的消费级产品 | 后台保活问题交给系统通道 | 推送延迟与到达率受厂商策略影响,需做多通道兜底 |
选型要回答六个问题:协议是否必须自己掌控、有没有海外合规要求、团队能否长期维护连接层、每个在线连接每月的成本能否接受、厂商推送通道要不要自建、将来迁移的退出成本有多大。
通用取舍清单
| 维度 | 常见两个选项 | 判断依据 |
|---|---|---|
| 连接归属 | 全局路由表 vs 单元内自治 | 用户是否跨地域、SLA 是否高于 99.9% |
| 消息落库 | 同步落库后 ACK vs 先入队列后异步落库 | 能否接受“ACK 后短暂不可见”,客户端是否有补偿能力 |
| 群消息 | 写扩散 vs 读扩散 | 群规模分布与存储预算 |
| 在线态 | Redis 集中存 vs 网关本地聚合上报 | 查询频率与一致性要求 |
| 多端同步 | 每设备游标 vs 会话级游标 | 平均设备数与回执链路的压力 |
| 推送 | 厂商通道 vs 自建通道 | 用户所在平台与到达率要求 |
常见踩坑
- 把“在线人数”当成权威数据做运营决策:长连接数与真实活跃数可能差数倍(见 01 章)。
- 用 MySQL 存路由表:写放大与主从延迟会直接表现为“消息找不到人”。
- 群成员变更后不重建扩散范围:新成员看不到历史,退群成员继续收消息。
- 大群实时广播不限速:一个群就能打满整个接入集群的出口带宽。
- 多机房双写路由表却没有冲突解决:切换时出现“两个在线单元”。
- 把 MQ 当投递主链路:MQ 故障即消息不可用,且重试时的顺序与丢失语义很难控。
- 过早自研:在没有明确的成本或协议诉求前就自建连接层,最后维护不动,反而拖慢业务。
小结:四个案例的共同点是“先明确约束,再选形态”:小规模用无状态网关加 Redis 路由最划算,规模上来后再谈多机房,而多机房的关键是给路由、会话与数据指定唯一权威;群聊则按群规模分档,500 人以内写扩散、千人以上读扩散、超大群用在线广播加分档降级;连接层自研与采购的分界,取决于协议掌控力、单位成本与团队长期维护能力,三者缺一就不要自研。