Lesson 0016 · 系统设计 · Phase 5 进阶案例 · 细节版

大案例 IV:即时通讯系统(微信/WhatsApp 类)

⏱ 预计 45 分钟 🎯 实时双向系统:连接、顺序、可靠投递三大难题 📖 前置:L8 · L9 · L11
与你的 Mission 的关系

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 一次,超时判定离线)
两个 ID 的分工

client_msg_id(客户端 UUID):重发去重用——超时重传后服务端发现同一 client_msg_id 已收过,返回原结果而不落两条。msg_id + seq(服务端分配):msg_id 全局唯一(Snowflake),seq 是会话内单调递增序号——排序、同步、已读全靠它。

件 ④数据 Schema(Step 3)

字段与设计
messagesmsg_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)

件 ⑨面试官追问预测(提前备好答案)

追问答案要点
「端到端加密怎么做?影响架构吗?」服务器只见密文:不能做服务端内容审核/搜索/转码;密钥管理交给客户端(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 同构)。

§·本周行动

行动任务(约 20 分钟)

在微信上做三个实验并写下推理:① 开飞行模式发消息 → 恢复网络,观察「转圈→红色感叹号→可手动重发」——这是客户端幂等与重传的证据链;② 双设备登录,在一台发消息,观察另一台的同步方式(推送还是拉取?);③ 发一条消息到 200 人群 vs 私聊,感受延迟差异——推断群聊扩散方式。

§·延伸资源

💬 把你的时序图或行动实验结论贴回来,我按面试官方式追问。