即时通讯系统设计 (WhatsApp/微信)

即时通讯(IM)是现代互联网最核心的应用场景之一。WhatsApp 日处理 400 亿条消息,微信日活跃用户超 10 亿。设计一个支撑亿级用户的 IM 系统,考验的是对网络协议、消息可靠性、存储架构和分布式系统的综合理解。

一、核心需求分析

1.1 功能需求

  • 一对一消息:用户 A 发消息给用户 B,B 实时收到
  • 群聊消息:消息广播至群内所有在线成员
  • 在线状态:展示用户在线/离线/最后在线时间
  • 历史消息:新设备登录后可拉取历史消息
  • 已读回执:消息已投递/已读状态同步

1.2 非功能需求

  • 低延迟:消息端到端延迟 < 100ms(体感实时)
  • 高可靠:消息不丢、不重、不乱序
  • 高并发:支持亿级同时在线用户
  • 跨设备:手机、PC、Web 同时在线,消息同步

二、通信协议选择

2.1 为什么不用 HTTP 短轮询?

传统的 HTTP 请求-响应模式不适合 IM:

  • 客户端必须不断发请求检查新消息,浪费带宽和电量
  • 延迟高:轮询间隔决定了消息送达的上限
  • 服务端无法主动推送

2.2 长连接方案对比

方案 协议 优点 缺点
WebSocket TCP 全双工 浏览器原生支持、双向通信、连接复用 需要维护连接状态
TCP 私有协议 自定义 性能最优、包体精简 客户端实现成本高
MQTT 发布-订阅 适合 IoT、QoS 分级 不直接支持群聊语义

推荐:移动端用 WebSocket + Protobuf 序列化,Web 端直接用 WebSocket。

2.3 WebSocket 连接管理

1
2
3
4
5
6
7
8
9
10
11
客户端                          服务端
│ │
├──── WebSocket 握手 ──────────→│
│ (HTTP Upgrade) │
├←──── 101 Switching ──────────┤
│ │
├──── 心跳 PING ───────────────→│ 每 30s
├←──── PONG ───────────────────┤
│ │
├──── 发送消息 (protobuf) ─────→│
├←──── ACK ────────────────────┤

关键设计决策:

  • 心跳间隔:30 秒发送一次 PING,60 秒未收到 PONG 判定连接断开
  • 断线重连:指数退避延迟(1s → 2s → 4s → 8s,最大 60s)
  • 连接迁移:用户切换网络时,携带 last_event_id 在新连接上继续收取未 ACK 的消息

三、消息可靠投递

3.1 消息生命周期

1
2
3
发送方 → 网关 → 消息队列 → 接收方
│ │ │ │
存本地 鉴权 持久化 推送+ACK

每条消息的投递状态:

1
2
SENT → DELIVERED → READ
(已发送) (已送达) (已读)

3.2 离线消息处理

当接收方离线时:

  1. 消息存入接收方的离线消息队列(Redis List / MySQL)
  2. 接收方上线后,从 last_event_id 开始拉取
  3. 拉取完成后,标记这些消息为 DELIVERED

消息 ID 设计:使用 Snowflake 算法生成全局递增 ID(64 位,毫秒级时间戳 + 机器 ID + 序列号),保证同一会话内消息有序。

3.3 消息去重

由于网络重试,同一条消息可能被投递多次。通过以下机制保证幂等:

机制 说明
客户端生成 msg_id 发送方生成 UUID 作为消息唯一标识
服务端去重 收到消息后,Redis SETNX msg:{msg_id} 检查是否已处理
接收方去重 接收方本地维护已接收消息 ID 集合,丢弃重复消息

四、群聊消息分发

4.1 写扩散 vs 读扩散

群聊的核心难题:一条消息要送达成千上万个群成员。

方案 做法 优点 缺点
写扩散 发消息时为每个群成员写一条收件箱记录 读简单、延迟低 大群写入放大严重(万人群 = 万次写入)
读扩散 消息只写一份到群的 timeline 写简单、存储省 每个成员上线都读群消息,读压力大
混合策略 小群写扩散 + 大群读扩散 平衡性能 实现复杂

WhatsApp 的做法:小群(< 100 人)写扩散,大群(≥ 100 人)读扩散。

4.2 实现细节

小群写扩散流程

1
2
3
4
1. 用户发消息 → 查群成员列表
2. 对每个成员:插入一条消息到成员的收件箱队列
3. 对在线成员:直接推送消息
4. 对离线成员:消息留在收件箱中等待上线拉取

大群读扩散流程

1
2
3
4
1. 用户发消息 → 写入群的 timeline(MySQL 表)
2. 成员打开群聊 → 从 timeline 拉取最新消息
3. 在线成员实时收到通知(有新消息),但不推送消息内容
4. 客户端决定是否立即拉取还是等用户主动查看

五、存储架构

5.1 消息存储

数据类型 存储方案 说明
在线消息 Redis Stream / Kafka 实时投递,TTL 短(24h)
离线消息 Redis List + MySQL 离线消息先存 Redis,定期归档到 MySQL
历史消息 MySQL + 冷热分离 近 7 天热数据,更早的数据归档到对象存储
附件/图片 对象存储 (S3/OSS) + CDN 缩略图、原图分别存储

5.2 用户状态

1
2
3
4
5
Redis Hash:
user:{user_id}
status: "online" | "offline"
last_seen: 1625097600
device_tokens: ["token1", "token2"]

在线状态通过心跳维护。定期(每 30s)更新 last_seen 时间戳,超过 60 秒未更新标记为离线。

六、系统架构总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                   ┌──────────────┐
│ CDN │ (图片/文件)
└──────┬───────┘

客户端 ←── WebSocket ──→ [接入层网关集群]

┌──────┴───────┐
│ 消息队列 │ (Kafka/RocketMQ)
└──────┬───────┘

┌─────────────┼─────────────┐
│ │ │
[消息处理] [状态服务] [存储服务]
(去重/路由) (在线/离线) (MySQL+Redis)
│ │ │
└─────────────┼─────────────┘

[推送服务]
(WebSocket Push + APNs/FCM 离线推送)

七、扩展性考量

  • 接入层无状态:WebSocket 网关只负责连接管理和协议转换,业务逻辑下沉到消息处理层
  • 按用户 ID 分片:消息队列按 user_id 哈希分区,保证同一用户的消息有序处理
  • 消息 ID 递增:Snowflake 生成全局递增 ID,客户端用 ID 判断消息顺序
  • 多机房部署:用户就近接入,消息通过专线同步到异地机房
  • 容量评估:假设 1 亿用户、人均日发送 50 条消息,日均 50 亿条消息,峰值 QPS ≈ 50亿 / 86400 × 1.5(峰值系数) ≈ 8.7 万 QPS

7.2 WhatsApp 的架构哲学

WhatsApp 后端能支撑 400 亿日消息量,依靠几个核心设计决策:

每连接一个 Erlang 进程,无连接池。传统后端用连接池复用 TCP 连接,WhatsApp 反其道而行——每个用户连接映射为一个轻量级 Erlang 进程(约 300 字节),不池化、不多路复用。这在其他语言下是不可行的(Java 线程需 ~1MB 栈),但 BEAM 虚拟机的进程模型让”百万进程一台机器”成为现实。

全异步消息传递。所有内部通信都是异步的——进程发送消息后立即继续处理下一件事,不等待回复。这避免了传统同步调用中的线程阻塞问题。

单向复制。数据从主数据中心单向复制到备用数据中心。备用节点只读,不处理写入。当主数据中心宕机时,备用节点中的进程状态会自动重新创建(Erlang Supervisor 树的设计),而非依赖复杂的主备切换。

ETS 内存表 + 直写缓存。WhatsApp 后端的许多集群使用 ETS(Erlang Term Storage,内存共享表)作为内部数据存储。配合直写缓存模式,读操作走内存表,写操作同时更新内存表和持久化存储——用空间换时间,极致优化读取延迟。

八、小结

即时通讯系统的核心挑战是消息可靠性(不丢不重不乱序)和实时性(毫秒级端到端延迟)。关键架构决策包括:WebSocket 长连接、消息 ID 去重、小群写扩散/大群读扩散的分发策略、以及 Redis + MySQL 的分层存储。WhatsApp 通过 Erlang/OTP 构建的分布式 Actor 模型天然适合这种场景,而微信则选择了 C++ 协同程序 + 同步 RPC 的技术路线——两种方案都在亿级规模上证明了各自的可行性。