跳到主要内容

即时通信系统设计

即时通信案例的范围是什么?​

  • 功能: 一对一聊天、最多 100 人的群聊及图片、视频等文件分享,100 人是本题假设而非真实产品上限;
  • 质量目标: 高可用、低延迟和可扩展,在线消息与历史消息都应有明确交付语义;
  • 扩展需求: 已发送、已送达、已读回执,最近在线时间,以及设备离线时的推送;
  • 设计边界: 原文未展开端到端加密、多设备密钥同步等能力,不能因此称本文是完整 WhatsApp 安全架构;

怎样估算消息量与媒体存储?​

  • 假设: 5000 万 DAU,每人每天给 4 人各发 10 条消息,5% 附带媒体;文本按 100 B、媒体按 100 KB;
项目计算结果
每日消息5000 万 × 4020 亿
平均消息写入20 亿 / 8640023148/s,原文约 24K/s
媒体文件20 亿 × 5%1 亿/日
消息记录20 亿 × 100 B200 GB/日
媒体内容1 亿 × 100 KB10 TB/日
十年逻辑存储10.2 TB × 365037.23 PB,原文粗估约 38 PB
平均入口10.2 TB / 86400118 MB/s,约 120 MB/s
  • 估算边界: 消息写入数不等于投递次数,群聊扇出、多设备、回执、心跳和复制都增加负载;
  • 保留期: 十年全量保存是容量假设,不能同时默认媒体下载后立即删除,应先选定实际保留规则;
  • 连接容量: DAU 不等于同时在线人数,还需估算峰值在线连接和每条连接资源成本;

数据模型如何关联用户、会话和消息?​

表关键数据关系
usersid、name、phoneNumber用户身份
chats / groupsid、name私聊或群聊容器
users_chatsid、userID、chatID用户与私聊多对多
users_groupsid、userID、groupID用户与群聊多对多
messagesid、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 丢失、重复消息和成员移除后仍尝试读取等场景;