第 12 章 设计聊天系统
简介
聊天系统(Chat System)支持用户之间的实时消息传递。本章重点设计一款聊天应用,包含以下功能:
- 一对一聊天
- 群聊(最多 100 人)
- 在线状态指示
- 多设备支持
- 推送通知
该系统的目标是支撑 5000 万日活跃用户(DAU),并永久保存聊天记录。
第 1 步:理解问题
需求
- 功能:
- 一对一聊天和群聊(最多 100 名成员)。
- 文本消息(最多 100,000 个字符)。
- 在线/离线状态指示。
- 支持多设备。
- 推送通知。
- 规模: 按 5000 万 DAU 进行设计。
- 存储: 永久保存聊天记录。
第 2 步:高层设计
通信协议
-
发送方: 使用 HTTP 发送消息,并利用持久连接(persistent connection)提高效率。
接收方:
-
轮询(Polling):
- 客户端定期询问服务器是否有可用的消息。
-
由于请求频繁且大多是冗余的,效率低下。
-
长轮询(Long Polling):
- 保持连接打开,直到有消息到达。
-
对不活跃的用户而言效率低下。

-
WebSocket:
- 一种用于实时通信的双向持久连接,发送和接收消息都选用它。
-
使用 WebSocket(ws)协议来发送和接收消息。
组件
- 无状态服务(Stateless Services):
- 处理注册、登录和用户资料管理。
- 与服务发现(service discovery)集成,为客户端推荐最合适的聊天服务器。
- 有状态服务(Stateful Services):
- 聊天服务器维护持久的 WebSocket 连接。
- 负责消息的投递和同步。
- 第三方集成:
- 推送通知服务向用户通知新消息。
- 通知的实现请参考“通知系统”一章。
设计
客户端与一台聊天服务器维持一个持久的 WebSocket 连接,用于实时消息传递。
- 聊天服务器负责消息的发送和接收。
- 在线状态服务器(presence server)管理在线/离线状态。
- API 服务器处理其他所有事务,包括用户登录、注册、修改资料等。
- 通知服务器发送推送通知。
- 最后,键值存储(key-value store)用于存储聊天记录。选择键值存储作为聊天记录数据的数据库,原因如下:
- 它便于水平扩展。
- 键值存储访问数据的延迟非常低。
- 关系型数据库不擅长处理数据的长尾(long tail)。当索引变得 很大时,随机访问的代价很高。
- 其他经过验证、可靠的聊天应用也采用了键值存储。例如 Facebook Messenger 和 Discord。
以下是一对一聊天和群聊的数据模型。
- 主键是消息 ID,它用于确定消息的顺序。
- 对于群聊,复合主键是 (channel_id, message_id)。
- ID 可以使用 Snowflake 这样的全局 64 位序列号生成器生成。
- 更好的方法是使用本地序列号生成器。本地是指 ID 只在一个群组内唯一。
-
本地 ID 之所以可行,是因为只需在一对一频道或群组频道内保持消息顺序就足够了。
第 3 步:深入设计
服务发现
-
服务发现的主要作用是根据地理位置、服务器容量等条件, 为客户端推荐最合适的聊天服务器。
-
使用 Apache Zookeeper,根据地理位置和服务器容量等条件分配聊天服务器。
- 确保负载的高效分配,并将延迟降到最低。
消息流程
一对一聊天
- 用户 A 向聊天服务器 1 发送一条消息。
- 聊天服务器 1 分配一个唯一的消息 ID,并将消息存储到键值存储中。
- 如果用户 B 在线,消息会被转发到与其维持着持久 WebSocket 连接的聊天服务器 2。
- 如果用户 B 离线,则发送一条推送通知。
群聊
- 消息会被复制到群组中每个接收者各自的收件箱中。
- 这简化了同步,但对于较大的群组来说代价高昂。
- 在接收方,一个接收者可以收到来自多个用户的消息。每个接收者 都有一个收件箱(消息同步队列),其中包含来自不同发送者的消息。
消息同步
许多用户拥有多台设备。我们需要在各设备之间同步消息。 每台设备都维护一个名为 cur_max_message_id 的变量,用于记录该设备上最新的 消息 ID。满足以下两个条件的消息被视为 新消息:
- 接收者 ID 等于当前登录用户的 ID。
- 键值存储中的消息 ID 大于 cur_max_message_id
在线状态
-
心跳机制(Heartbeat Mechanism):
- 客户端定期向在线状态服务器发送心跳,表明自己在线。
- 如果在某个阈值时间内(例如 x = 30)没有收到心跳,该用户就会被标记为离线。
-
扇出模型(Fanout Model):
- 在线状态的更新通过发布-订阅(publish-subscribe)模型推送给好友,其中每一对好友维护一个频道。
- 当用户 A 的在线状态发生变化时,它会把事件发布到三个频道:频道 A-B、A-C 和 A-D。
- 这三个频道分别由用户 B、C 和 D 订阅,他们由此收到在线状态的更新。
- 上述设计对于小型用户群组是有效的。
其他考虑因素
可扩展性
- 水平扩展: 随着用户数量增长增加服务器。
- 负载均衡: 将流量均匀地分发到各服务器。
- 缓存: 降低数据库负载并改善延迟。
错误处理
- 重试机制: 通过重试和排队来处理消息投递失败。
- 服务器故障: 发生故障时,利用服务发现分配新的服务器。
未来扩展
- 媒体支持: 增加对照片和视频的处理,包括压缩和云存储。
- 端到端加密(End-to-End Encryption): 确保消息的隐私。
- 客户端缓存: 减少数据传输以提升性能。
- 缩短加载时间: 使用地理上分布式的缓存网络。