大案例 IV:即时通讯系统(微信/WhatsApp 类)
IM 是面试出场率前五的题,也是细节密度最高的一类:它同时考验长连接、消息顺序、可靠投递、群聊扩散四个深水区。本课起采用「九件套」细节格式——比之前的案例课多出 API 定义、数据 Schema、完整时序、失败模式与面试官追问预测。
件 ①需求表(Step 1)
| 类别 | 内容 |
|---|---|
| 功能(MVP) | ① 1v1 文本消息;② 群聊(上限 500 人);③ 在线状态;④ 已读回执与未读数;⑤ 离线消息;⑥ 多设备同步。明确不做:音视频通话、端到端加密细节(提一句扩展)、朋友圈类动态 |
| 非功能 | 在线消息投递延迟 < 200ms;不能丢消息(宁重不丢,客户端去重);同一会话内严格有序;5 亿 DAU |
| 关键洞察 | IM 与 L3 的「无状态」原则正面冲突:长连接天然有状态(连接在哪台网关上)。解法是分层——接入层有状态、业务层无状态,这是本题第一得分点 |
件 ②容量估算(Step 2,全程算式)
| 指标 | 算式 | 架构暗示 |
|---|---|---|
| 消息量 | 5 亿 DAU × 40 条/天 = 200 亿条/天 ÷ 86400 ≈ 23 万条/s;峰值 ×3 ≈ 70 万条/s | 写读双高 → 存储按写优化(LSM 类),MQ 削峰 |
| 并发长连接 | 5 亿 DAU × 高峰在线比例 60% ≈ 3 亿并发连接;单网关 50 万连接(fd/内存/CPU 瓶颈)→ 约 600 台网关 | 连接数是第一瓶颈,网关只管收发、不做业务 |
| 存储 | 200 亿 × 500B ≈ 10 TB/天 → 年 3.6 PB | 冷热分层;按会话分片,历史消息转归档存储 |
| 推送扇出 | 大群 500 人 × 群消息占比 10% → 扇出写峰值可达 百万级/s | 群聊必须分「写扩散/读扩散」(件 ⑥) |
件 ③API 定义(Step 3)
# 业务面(客户端 → 业务层,可走任意网关)
POST /messages # 发消息
body: {conv_id, client_msg_id, type, content}
resp: {msg_id, seq} # 服务端分配的全局 ID + 会话内序号
GET /sync?device_seq= # 多设备增量拉取(断线重连也走这里)
POST /read # 上报已读 {conv_id, last_read_seq}
# 数据面(网关 ⇄ 客户端,长连接私有协议)
C → S: SEND {client_msg_id, conv_id, ...}
S → C: ACK {client_msg_id, msg_id, seq} # 发送确认
S → C: PUSH {msg_id, conv_id, seq, payload} # 下行推送
C → S: PUSH_ACK {msg_id} # 接收确认(可靠投递的关键)
C → S: HEARTBEAT(30s 一次,超时判定离线)
client_msg_id(客户端 UUID):重发去重用——超时重传后服务端发现同一 client_msg_id 已收过,返回原结果而不落两条。msg_id + seq(服务端分配):msg_id 全局唯一(Snowflake),seq 是会话内单调递增序号——排序、同步、已读全靠它。
件 ④数据 Schema(Step 3)
| 表 | 字段与设计 |
|---|---|
messages | msg_id, conv_id, sender_uid, seq, type, content, created_at。按 conv_id 哈希分片(同会话消息落同一分片 → 会话内顺序天然保证,L6 分片键对齐原则) |
inbox(用户收件箱) | user_id, conv_id, last_msg_seq, unread_count, last_read_seq。按 user_id 分片——「我的消息列表」一次查齐 |
route(在线路由,Redis) | user_id → {gateway_id, device_list, last_active}。TTL = 心跳超时 ×2,过期即视为离线 |
conv_seq(会话序号,Redis/专用服务) | conv_id → 当前最大 seq(INCR 原子分配;号段模式批量预取,L8 发号器同构) |
件 ⑤一条消息的完整时序(Step 3,背下来)
发送方 A 网关 Ga 消息服务 MQ(conv分区) 投递服务 接收方 B
│ SEND ────────▶│ │ │ │ │
│ 路由校验 │ │ │ │
│ │──业务请求─────▶│ │ │ │
│ │ ①conv_seq 原子 +1 = seq │ │ │
│ │ ②写 messages(本会话分片)│ │ │
│ │ ③发事件───────────────▶ │ │ │
│ ACK{seq} ◀────│◀───立即返回────│ (对发送方:到此结束,<50ms) │ │
│ │ │ 消费──▶│ ④查 route: B 在线? │
│ │ │ │ 在线 → 经 Gb PUSH ──────▶│
│ │ │ │ B 回 PUSH_ACK → 投递完成 │
│ │ │ │ 离线 → inbox 更新 last_msg_seq
│ │ │ │ + 系统级推送(APNs) │
▼(B 上线后)sync(last_ack_seq) ──▶ 拉取缺口消息(幂等,按 seq 补齐)
注意时序里的两个「即时返回」:发送方拿到 ACK 就算成功(后续投递异步),投递失败靠 PUSH_ACK 超时重传——这就是「宁重不丢 + 幂等去重」的落地(L7 at-least-once 的教科书应用)。
件 ⑥关键组件选型对比
| 决策点 | 选项 A | 选项 B | 结论与代价 |
|---|---|---|---|
| 群聊扩散 | 写扩散:群消息写入每个成员收件箱(读快) | 读扩散:成员拉群 timeline(写简单) | 小群写扩散、超大群读扩散(L11 推拉混合的空间版)。写扩散代价:500 人群一条消息 500 写;读扩散代价:聚合延迟 |
| seq 分配 | Redis INCR 实时分配 | 号段批量预取 + 内存分配 | 号段模式:DB 压力降 3 个数量级;代价:号段切换瞬间重启会浪费一段 ID(可接受,seq 只要求单调不要求连续) |
| 在线状态 | 心跳实时写 Redis(准实时准确) | 被动判定:TTL 过期才算离线 | TTL 判定(TTL=心跳×2):写压力大减;代价:真实掉线后 ~60s 内仍显示在线——多数产品可接受 |
| 消息存储 | MySQL 分库分表 | LSM 类 KV(RocksDB/HBase) | 70 万写/s + 时间有序追加 → LSM 写优化占优;代价:读放大与 compaction 抖动,靠缓存(L5 热会话)对冲 |
件 ⑦失败模式与应对(Step 4)
| 故障 | 应对 |
|---|---|
| 网关宕机(3 亿连接抖动) | 客户端指数退避重连(L10 抖动防重连风暴);重连后走 sync(last_seq) 补缺口——连接可丢,seq 序号是真相之源 |
| seq 服务单点 | 号段预取 = 无状态分片;号段服务再挂 → 按机器 ID 段隔离分配(Snowflake 变体),宁可 seq 有空洞不可停止服务 |
| 上线风暴(断网恢复,百万设备同时 sync) | sync 限流 + 排队;离线消息条数封顶(如 500 条,超出引导到历史页);兜底推送合并成一条「你有 N 条新消息」 |
| 重复消息轰炸(恶意/bug 重传) | client_msg_id 去重窗口(Redis 24h)+ 发送端频控(L10 用户级令牌桶) |
| 跨机房容灾 | 用户就近接入;会话数据按 conv_id 归属机房异步复制(最终一致,L9);机房级故障 → 路由切换 + 全量 sync 补齐 |
件 ⑧演进路线(v1 → v2)
- v1(0–100 万用户):单机房、WebSocket 网关 + MySQL + Redis route 表,写扩散到 200 人群——够用且简单;
- v2(百万 → 千万):MQ 引入削峰、messages 按 conv_id 分库分表、seq 号段化;
- v3(亿级):网关集群 + 路由中心、LSM 存储替换 MySQL、超大群读扩散、多机房就近接入、离线归档到对象存储。
件 ⑨面试官追问预测(提前备好答案)
| 追问 | 答案要点 |
|---|---|
| 「端到端加密怎么做?影响架构吗?」 | 服务器只见密文:不能做服务端内容审核/搜索/转码;密钥管理交给客户端(Signal 协议族);架构上主要影响检索与合规链路 |
| 「消息撤回怎么做?」 | tombstone:发撤回指令消息(特殊 type),客户端把原消息替换为「已撤回」占位;seq 继续单调——撤回也是一条消息 |
| 「图片/视频怎么发?」 | 走 L13 管线:对象存储 + CDN,消息体只带 URL+缩略图;大文件分片上传、断点续传 |
| 「已读回执能伪造吗?」 | 能(客户端可信问题),产品上提供「关闭已读」开关;服务端只如实转发上报 |
| 「为什么不用 HTTP 轮询?」 | 轮询延迟与开销不可接受(3 亿连接 × 每 2s 轮询 = 1.5 亿 QPS 的纯开销);长连接是 IM 的物理前提 |
§·随堂检测
§·检索练习
盖住全部内容:① 默写一条消息从 A 到 B 的完整时序(含两个 ACK 时机);② 写出「分层化解有状态矛盾」的架构表述;③ 群聊写扩散/读扩散的选择标准。
核对要点
① SEND →(seq 分配+落库+发 MQ)→ ACK 给发送方(异步结束)→ 消费 → 查路由 → 在线 PUSH + 等 PUSH_ACK(超时重传);离线 → inbox + 系统推送。② 「接入层有状态(连接路由),业务层无状态(任何实例可处理),状态收敛到 route 表与 seq」。③ 成员数 ≤ 阈值(~500)写扩散;超大群读扩散 + 活跃度过滤(L11 同构)。
§·本周行动
在微信上做三个实验并写下推理:① 开飞行模式发消息 → 恢复网络,观察「转圈→红色感叹号→可手动重发」——这是客户端幂等与重传的证据链;② 双设备登录,在一台发消息,观察另一台的同步方式(推送还是拉取?);③ 发一条消息到 200 人群 vs 私聊,感受延迟差异——推断群聊扩散方式。
§·延伸资源
- 首选精读:The WhatsApp Architecture Facebook Bought For $19 Billion——Erlang + 单机百万连接的真实数字[1]
- 对照答案:Alex Xu Vol.1「Design a Chat System」章[2]
- 相关课回看:L7(at-least-once)、L11(扩散模式)、L10(重连风暴)
- 下一课 → Lesson 0017 搜索自动补全