Backend / Infrastructure SWE · System Design

Anthropic 一对一聊天设计:1 亿 DAU、离线补读与跨节点丢消息

按公开面经的 1 亿 DAU、Web 文本聊天约束,推导写入容量、会话索引、序列号、在线/离线路径和 replica lag。

题目重点:按公开面经的 1 亿 DAU、Web 文本聊天约束,推导写入容量、会话索引、序列号、在线/离线路径和 replica lag。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版消息成功定义为持久化写入主存储并取得会话内递增 seq;WebSocket 推送是低延迟通知,不能替代持久记录。
  • 一个 chat 由规范化的两位用户 id 唯一标识;题目没定义多端、删除、编辑、已读回执和存储保留期,先不推断这些功能。
  • 按题目候选人的示例,每 DAU 每天 100 条发送;平均消息长 500B、峰值倍率 5、保留 30 天均为本文额外假设,须随真实业务指标替换。

解题路线

  1. 先估容量:1 亿×100/86400≈115,741 写/秒平均;5 倍峰值约 579k 写/秒。500B 纯正文平均约 50 亿 B/天,30 天约 1500 亿 B,尚未加索引、复制和协议开销。若只扩 WebSocket app server 或 read replica,主库写入瓶颈仍在。
  2. 为每个 chat 维护按 `(chat_id,seq)` 排序的消息表,按 chat_id 分片,消息写入与 seq 分配在同一 shard 的事务/序列机制中完成。另建 `user_chats(user_id,chat_id,last_seen_seq,updated_at)` 索引;用户上线先查此表找会话,再逐会话按 seq 查询未读,避免全库扫 chat。
  3. 发送路径:客户端带稳定 client_message_id → gateway/app server → 以 `(sender_id,client_message_id)` 做可持久化去重 → 写消息并返回 seq → 查 presence 并推送接收方。推送丢失或乱序时,客户端看到 seq 缺口便向主存储补读;接收方离线时上线后按 last_seen_seq 补拉。
  4. 读取分层:老历史可走副本,刚写入/发现缺口的读请求走主 shard 或有明确读屏障的副本,避免 replica lag 让用户误以为消息不存在。presence 可短 TTL、允许瞬时不准,但消息持久化不能依赖 presence。
  5. 突发流量先分别看连接数、写入队列、user_chats 查询和历史加载。可以降级历史回溯/typing/presence 等次要路径,优先保留新消息持久化与补读;缓存只帮热点读或 presence,不能替代消息写入容量。

手动走一遍

A 发送消息 client id=42。主 shard 持久化为 chat C 的 seq=11 后向 A 确认;推送到 B 的 app server 丢失。B 先收到了 seq=12,检测到缺 11,从 C 的主 shard 读取 seq>10 补齐。若 A 超时重试同一 client id,持久化去重表返回原 seq=11,不能再插入 seq=13。

规模与取舍

发送单条消息的主路径期望 O(1) 次索引操作加一次主存储写与可选推送;离线补读 O(c+m),c 为该用户会话数,m 为返回未读消息数。存储 O(消息总数 + user↔chat 关系数 + 幂等键数);实际热点会话、分片迁移、复制倍数和 fanout 可能改变常数及尾延迟。

易错点与追问

容易失分的边界

  • 只按 chat_id 存消息,没建 user→chat 索引,用户上线无法高效找到所有会话。
  • 把推送成功当作持久化成功;跨 app server 丢包时没有补读依据。
  • read replica 落后时仍用它做刚写入消息的缺口回补。
  • 去重只放 app server 本地内存,重试落到别的 server 时产生双写。
  • 把 100 DAU 日发条数、500B 和 5 倍峰值全部说成原题给定;题目只明确 1 亿 DAU,100 条是候选人的容量假设。

继续深挖

  • 群聊若加入,按会话 seq 与成员 fanout 的规模会怎样改变?
  • 某用户有百万会话时,user_chats 如何分页和冷热分层?
  • 跨区域容灾时每会话顺序、写入确认与延迟如何取舍?
✓已复制微信号!