即时通信系统设计
即时通信案例的范围是什么?
- 功能: 一对一聊天、最多 100 人的群聊及图片、视频等文件分享,100 人是本题假设而非真实产品上限;
- 质量目标: 高可用、低延迟和可扩展,在线消息与历史消息都应有明确交付语义;
- 扩展需求: 已发送、已送达、已读回执,最近在线时间,以及设备离线时的推送;
- 设计边界: 原文未展开端到端加密、多设备密钥同步等能力,不能因此称本文是完整 WhatsApp 安全架构;
怎样估算消息量与媒体存储?
- 假设: 5000 万 DAU,每人每天给 4 人各发 10 条消息,5% 附带媒体;文本按 100 B、媒体按 100 KB;
| 项目 | 计算 | 结果 |
|---|---|---|
| 每日消息 | 5000 万 × 40 | 20 亿 |
| 平均消息写入 | 20 亿 / 86400 | 23148/s,原文约 24K/s |
| 媒体文件 | 20 亿 × 5% | 1 亿/日 |
| 消息记录 | 20 亿 × 100 B | 200 GB/日 |
| 媒体内容 | 1 亿 × 100 KB | 10 TB/日 |
| 十年逻辑存储 | 10.2 TB × 3650 | 37.23 PB,原文粗估约 38 PB |
| 平均入口 | 10.2 TB / 86400 | 118 MB/s,约 120 MB/s |
- 估算边界: 消息写入数不等于投递次数,群聊扇出、多设备、回执、心跳和复制都增加负载;
- 保留期: 十年全量保存是容量假设,不能同时默认媒体下载后立即删除,应先选定实际保留规则;
- 连接容量: DAU 不等于同时在线人数,还需估算峰值在线连接和每条连接资源成本;
数据模型如何关联用户、会话和消息?
| 表 | 关键数据 | 关系 |
|---|---|---|
| users | id、name、phoneNumber | 用户身份 |
| chats / groups | id、name | 私聊或群聊容器 |
| users_chats | id、userID、chatID | 用户与私聊多对多 |
| users_groups | id、userID、groupID | 用户与群聊多对多 |
| messages | id、userID、chatID 或 groupID、type、content、sentAt、deliveredAt、seenAt | 消息归属一个会话 |
- 通道约束: 消息只关联一种通道,必须验证发送者是成员,不能信任请求传入的 userID;
- 群回执修正: 单个 deliveredAt/seenAt 不足以表达每位成员的状态,需要按消息与接收者保存回执,或保存每人的读到序号;
- 存储选择: 用户与成员关系可用关系库,消息可按会话访问模式评估 Cassandra 等,数据所有权按服务责任划分;
查询与发送 API 应约定什么?
getAll(userID) → Chat[] | Group[]
getMessages(userID, channelID, cursor?, limit?) → MessagePage
sendMessage(userID, channelID, clientMessageID, message) → messageID
joinGroup(userID, channelID) → result
leaveGroup(userID, channelID) → result
- 原文接口: 提供列表、获取消息、发送和入退群,返回 boolean 的发送接口在此补充稳定 messageID 以支持回执与去重;
- 分页: 使用会话内稳定序号或复合游标查询历史,不一次加载整个群聊历史;
- 成员控制: 加入群需要按邀请或群规则授权,不能仅知道 channelID 就获得读取权限;
- 重试: clientMessageID 在发送者范围内唯一,服务器重复收到请求时返回同一个消息结果;
核心服务如何分配职责?
| 服务 | 主要职责 |
|---|---|
| 用户服务 | 身份和用户资料 |
| 聊天服务 | 会话、实时连接与消息路由 |
| 在线状态服务 | 心跳、最近活动与状态过期 |
| 媒体服务 | 上传、处理及访问授权 |
| 通知服务 | 离线设备推送 |
- 服务通信: HTTP 或 gRPC 根据接口需要选择,实例定位使用服务发现,概念见 服务发现;
- 拆分时机: 群管理责任或负载增长后可以从聊天服务拆出群服务,不必把所有功能第一天都独立部署;
- 网关: 统一路由、认证和限流,但需要实际验证 WebSocket 支持、连接超时与容量,不能假定任意网关兼容所有协议;
一条在线消息怎样可靠到达接收方?
发送者 → WebSocket → 聊天服务 → 校验与消息持久化
↓ 可靠投递任务
接收者 ← 目标连接节点 ← 按用户和设备查找在线连接
- 连接选择: 双向聊天适合 WebSocket;长轮询可作兼容方案,SSE 更适合单向推送,机制见 实时通信;
- 路由状态: 共享状态保存用户或设备对应的连接节点,连接断开或迁移时需及时更新;
- 服务端确认: 持久化成功后再确认“已发送”,写库后投递任务发布失败需要可恢复的补发机制;
- 顺序: 会话内分配稳定序号,客户端按序组装和去重,不要求所有会话共享全局顺序;
- 断线恢复: 客户端携带最后收到的序号补取缺失消息,TCP 连接恢复不会自动补齐业务历史;
已发送、已送达和已读分别代表什么?
- 已发送: 服务端已按约定接受并持久化消息,而非只是客户端调用 send 成功;
- 已送达: 接收设备返回应用层 ACK,服务端更新相应接收者的 deliveredAt;
- 已读: 用户打开或实际查看会话后,客户端发出已读事件,更新 seenAt 或读取游标;
- 幂等回执: 回执可能重复或迟到,应只允许状态前进,避免旧事件把已读降回未读;
- 多设备语义: 需要决定任一设备已读还是所有设备送达,不能把单个时间戳默认解释为全部完成;
心跳怎样维护最近在线时间?
- 心跳: 客户端周期性报告存活,在线状态服务保存 lastSeen 与过期时间,超时后判定离线;
- 精度权衡: 心跳更频繁可提高状态新鲜度,但增加网络、电量和写入负载;
- 故障解释: 未收到心跳可能是网络中断,不一定代表用户主动离线,状态是近似判断;
- 存储: Redis 或 Memcached 可保存短期状态,若需要长期最近在线记录还应定义持久化策略;
基于用户动作的在线判断有什么不同?
- 活动更新: 以最近业务动作时间作为 lastSeen,一段时间没有动作后显示离线或不活跃;
- 原文示例: 30 秒无活动是演示阈值,需要按产品体验调整;
- 权衡: 减少额外心跳,但正在安静阅读的用户可能被误判,需要区分“有连接”和“最近有操作”;
离线通知如何通过平台推送送达?
聊天服务发现收件人离线 → 通知任务队列
通知服务 → 按设备平台选择 FCM / APNs → 设备通知
- 队列作用: 缓冲设备推送任务,让推送服务故障不阻塞消息持久化,并允许独立重试;
- 设备元数据: 维护平台与推送令牌,必要时支持邮件或短信,并处理失效设备令牌;
- 保证边界: 队列已确认不代表设备已经展示通知,FCM/APNs 的返回也不能替代消息读取回执;
- 模式修正: 平台推送与发布订阅并不互斥,可以用主题扇出到各类队列,再由通知 worker 调用外部平台;
- 复用边界: 后续社交和出行案例只引用此通知流程,不再重复解释平台推送机制;
媒体怎样上传并被授权访问?
- 存储路径: 媒体服务验证上传权限,将文件保存到对象存储,消息内只保留受控引用及必要元数据;
- 内容处理: 按需要压缩、生成缩略图或转码,降低存储和下载费用;
- 分发: CDN 加速媒体交付,私聊或私有群文件仍必须有访问控制,不能把永久公开 URL 当作权限方案;
- 原文实现选项: S3、Azure Blob、Cloud Storage,也列出 HDFS 和 GlusterFS,选择取决于文件访问与运维需求;
- 历史说法: 原文提到真实产品的下载后删除行为,本题只把它作为保留策略选项,不当作当前产品保证;
历史消息增长后如何分片与缓存?
- 分片: 优先围绕 channelID 与时间范围组织消息,让同一页历史尽量在少数分区内完成;
- 大会话: 时间分桶可避免一条会话无限增长,热点群仍需单独控制,哈希并不能消除单键热点;
- 缓存: 历史消息和热点会话页较适合缓存,最新回执与成员权限需要更明确的新鲜度边界;
- 淘汰与回源: 可用 LRU 作为初始策略,未命中按受控路径读取数据库,原理见 缓存;
- 容量: 原文的“缓存 20% 日流量”只能当待验证假设,应按唯一消息和实际访问窗口规划;
故障时如何保护消息与在线连接?
- 服务副本: 多实例与健康路由降低单点,连接节点失败后客户端重连并通过游标补消息;
- 数据副本: 为消息持久性和查询可用性配置复制,但副本延迟不能导致刚发送的消息永久消失;
- 缓存故障: 在线映射可由重连重建,历史消息缓存可回源,二者恢复路径不同;
- 入口故障: 网关需要冗余和连接容量预算,集中重连还需限速与退避;
- 消息基础设施: Kafka、NATS 等可提供持久事件能力,但不能自动替应用实现端到端恰好一次或群回执;
- 验证: 覆盖写后断线、投递前崩溃、ACK 丢失、重复消息和成员移除后仍尝试读取等场景;