IM 系统全景:需求、指标与架构分层

IM(Instant Messaging,即时通讯)系统对外的承诺只有两条:消息不丢、消息不乱序,其余目标(快、省机器、能横向扩)都建立在这两条之上。它和普通 Web 服务的根本差别在于:服务端必须长期持有数百万条长连接,而任何一次发布、扩容、机房故障都会直接表现为"用户掉线"。

需求边界

先把范围钉死,否则设计会在细节里跑偏。本教程聚焦以下需求:

  • 单聊(C2C)与群聊(C2G)消息收发,支持文本、图片、语音、文件,以及引用与撤回;
  • 在线态(presence):对方是否在线、最后活跃时间;
  • 多端登录(multi-device):手机、桌面、Web 同时在线,消息在各端之间保持一致;
  • 离线消息:不在线时消息落库,上线后按位点补拉;
  • 已读回执(read receipt)与未读数(unread count)。

明确不做的部分同样重要:音视频通话的信令与媒体传输、端到端加密的密钥协商、内容审核的模型实现。这些各有独立课题,混进来只会让架构图变成大杂烩。

指标与口径

"高并发 IM"必须先翻译成数字,否则无法判断架构是否够用。真实项目里最容易扯皮的是口径,所以每一项都写清定义:

指标口径定义目标示例
并发连接数(CCU)同一时刻已鉴权且心跳未超时的连接数峰值 300 万
单机连接数单个接入进程持有的连接数≤ 8 万
上行消息速率每秒进入逻辑层的合法消息条数峰值 5 万/s
端到端延迟发送方收到 ACK 到接收方收到消息的时间差TP99 < 200ms
送达率在线用户中成功收到消息的比例≥ 99.99%
推送延迟离线消息交给厂商推送通道的耗时TP99 < 2s
可用性单聊发送接口月度可用性99.95%

注意区分"在线"与"可达":长连接还在,不代表用户看得见消息(进程卡死、手机休眠、网络半开)。送达率统计的是 ACK 回执,不是"我以为发出去了"。

容量估算口径

估算的用途不是算准,而是找出最先扛不住的那一环。以 DAU 1000 万的产品为例:

连接数:DAU 1000 万 × 峰值在线率 30% = 300 万 CCU
        预留 30% 冗余(发布、重连风暴) → 按 400 万规划容量
        单机 8 万连接 → 至少 50 台接入机,跨 2 个可用区部署

消息量:人均上行 40 条/日 → 4 亿条/日
        日均 = 4 亿 / 86400 ≈ 4600 条/s
        峰值系数取 10(早晚高峰)→ 峰值约 5 万条/s

存储:单条消息含索引约 500B → 4 亿 × 500B ≈ 200 GB/日
        保留 180 天 → 约 36 TB,必须分片 + 冷热分离

连接带宽:心跳 30s 一次、帧 24B → 约 0.8 B/s/连接
        300 万连接 ≈ 2.4 MB/s,可忽略;带宽与 CPU 的消耗大头在
        TLS 握手与断线重连,而不是心跳本身

三条结论:连接是内存与文件描述符(file descriptor)问题,消息是分区与顺序问题,存储是分片与成本问题。三种瓶颈对应完全不同的解法,千万不要用同一套手段去治。

架构分层

IM 服务端可以切成四层,依赖方向严格自上而下,不允许反向调用:

        ┌───────────────┐                ┌───────────────┐
 移动端 │  接入层 gateway │                │  接入层 gateway │ Web/桌面
 ──────▶│  持有长连接     │                │  持有长连接     │◀──────
        └───────┬───────┘                └───────┬───────┘
                │       内网 RPC / 消息总线        │
        ┌───────▼────────────────────────────────▼───────┐
        │              逻辑层 logic(无状态 × N)           │
        │  鉴权校验 / 会话管理 / 消息处理 / 投递编排         │
        └───┬──────────────────┬───────────────────┬──────┘
            │                  │                   │
   ┌────────▼───────┐  ┌───────▼────────┐  ┌───────▼────────┐
   │ 路由层 route    │  │ 异步管道 MQ     │  │ 存储层 storage  │
   │ Redis 路由表    │  │ 削峰 / 落库 / 扇出│  │ MySQL+Redis+OSS │
   └────────────────┘  └────────────────┘  └────────────────┘

各层的职责与失败影响决定了运维策略:

层状态职责扩容方式挂掉的影响
接入层有状态维护连接、心跳、编解码、限流加机器 + 客户端重连该机用户全部掉线,需重连
逻辑层无状态鉴权、会话、消息处理、投递编排无脑加机器请求失败可重试,用户无感
路由层有状态记录 uid/设备到接入机的映射按 uid 分片扩容新消息无法定位接收方
存储层有状态消息、会话、关系、离线数据分片 + 冷热分离影响面最大,需降级读

有状态的两层(接入、路由)是 IM 与普通微服务最不一样的地方:它们扩容时不是"多起几个实例就行",而要处理存量连接的迁移与路由表的收敛。

一条消息的完整链路

理解分层最好的方式是跟一条消息走完全程:

发送方               接入层           逻辑层          路由层        存储/队列
  │  send(msg) ────────▶│                 │              │              │
  │                     │── 上行转发 ────▶│              │              │
  │                     │                 │─ 鉴权+校验 ──│              │
  │                     │                 │─ 生成 msgID ─┼─ 写消息 ────▶│
  │◀──── ACK(msgID,seq) ┤◀── 返回 ACK ────│              │              │
  │                     │                 │─ 查接收方路由▶│              │
  │                     │◀─ 下发指令 ─────│              │              │
  │                     │  投递 hold 30s  │              │              │
  │                     │─ 收到对端 ACK ─▶│─ 标记已送达 ─▶│              │

关键点:ACK 是对"落库成功"的确认,不是对"对方已读"的确认。发送方拿到 ACK 即可清掉本地待发队列,接收方已读状态由回执单独驱动。这个区分决定了弱网下会不会出现"消息重发风暴"。

本篇五章的边界

后面 25 章各自解决一个具体问题,本篇只负责把地基铺平:

章解决的问题明确不展开
01 全景需求、指标口径、分层模型任何具体实现细节
02 架构四层职责、依赖方向、扩容单位路由表的具体结构(06)
03 连接连接生命周期、设备模型、在线态语义路由表的 Redis 落地(06)
04 协议帧格式、字段设计、版本兼容消息 ID 与序列号规则(08)
05 鉴权令牌、设备指纹、多端登录策略频控与风控(24)

工程注意点

  • 不要用"在线人数"做业务决策:长连接数与真实活跃数可能差 3 倍以上,指标定义要落到"心跳 90s 内有效"这种可验证的条件上。
  • 先定延迟预算再写代码:端到端 200ms 的预算建议拆成接入 10ms、逻辑 50ms、路由 20ms、存储 80ms,剩下 40ms 留给重试与抖动。没有预算的优化都是猜。
  • 估算要写进文档:把"单机 8 万连接""峰值 5 万/s""200 GB/日"写在设计文档里,压测时才有验收标准(26 章)。
  • 容量口径统一到峰值系数:所有 QPS 都注明"日均 × 倍数",否则同一份数据会被不同的人算出十倍差距。

小结:IM 系统的设计从需求边界与指标口径开始,把"高并发"翻译成 CCU、上行速率、端到端延迟、送达率与存储日增这些可验证的数字;架构上切成有状态的接入层与路由层、无状态的逻辑层、可分的存储层,并牢记连接、消息、存储三类瓶颈各有各的解法,ACK 只承诺落库而不承诺已读。

笔记加载中…