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 只承诺落库而不承诺已读。