系统设计面试笔记 第 12 章

第 12 章 设计聊天系统

简介

聊天系统(Chat System)支持用户之间的实时消息传递。本章重点设计一款聊天应用,包含以下功能:

  • 一对一聊天
  • 群聊(最多 100 人)
  • 在线状态指示
  • 多设备支持
  • 推送通知

该系统的目标是支撑 5000 万日活跃用户(DAU),并永久保存聊天记录。


第 1 步:理解问题

需求

  1. 功能:
    • 一对一聊天和群聊(最多 100 名成员)。
    • 文本消息(最多 100,000 个字符)。
    • 在线/离线状态指示。
    • 支持多设备。
    • 推送通知。
  2. 规模: 按 5000 万 DAU 进行设计。
  3. 存储: 永久保存聊天记录。

第 2 步:高层设计

通信协议

  1. 发送方: 使用 HTTP 发送消息,并利用持久连接(persistent connection)提高效率。

    基础设计

  2. 接收方:

    • 轮询(Polling):

      • 客户端定期询问服务器是否有可用的消息。
      • 由于请求频繁且大多是冗余的,效率低下。

        轮询

    • 长轮询(Long Polling):

      • 保持连接打开,直到有消息到达。
      • 对不活跃的用户而言效率低下。

        长轮询

    • WebSocket:

      • 一种用于实时通信的双向持久连接,发送和接收消息都选用它。
      • 使用 WebSocket(ws)协议来发送和接收消息。

        WebSocket


组件

高层架构 高层架构
  1. 无状态服务(Stateless Services):
    • 处理注册、登录和用户资料管理。
    • 与服务发现(service discovery)集成,为客户端推荐最合适的聊天服务器。
  2. 有状态服务(Stateful Services):
    • 聊天服务器维护持久的 WebSocket 连接。
    • 负责消息的投递和同步。
  3. 第三方集成:
    • 推送通知服务向用户通知新消息。
    • 通知的实现请参考“通知系统”一章。

设计

客户端与一台聊天服务器维持一个持久的 WebSocket 连接,用于实时消息传递。

高层设计
  • 聊天服务器负责消息的发送和接收。
  • 在线状态服务器(presence server)管理在线/离线状态。
  • API 服务器处理其他所有事务,包括用户登录、注册、修改资料等。
  • 通知服务器发送推送通知。
  • 最后,键值存储(key-value store)用于存储聊天记录。选择键值存储作为聊天记录数据的数据库,原因如下:
    • 它便于水平扩展。
    • 键值存储访问数据的延迟非常低。
    • 关系型数据库不擅长处理数据的长尾(long tail)。当索引变得 很大时,随机访问的代价很高。
    • 其他经过验证、可靠的聊天应用也采用了键值存储。例如 Facebook Messenger 和 Discord。

以下是一对一聊天和群聊的数据模型。

  • 主键是消息 ID,它用于确定消息的顺序。
  • 对于群聊,复合主键是 (channel_id, message_id)。
    • ID 可以使用 Snowflake 这样的全局 64 位序列号生成器生成。
    • 更好的方法是使用本地序列号生成器。本地是指 ID 只在一个群组内唯一。
    • 本地 ID 之所以可行,是因为只需在一对一频道或群组频道内保持消息顺序就足够了。

      一对一聊天设计
      群聊设计

第 3 步:深入设计

服务发现

Zookeeper
  • 服务发现的主要作用是根据地理位置、服务器容量等条件, 为客户端推荐最合适的聊天服务器。

  • 使用 Apache Zookeeper,根据地理位置和服务器容量等条件分配聊天服务器。

  • 确保负载的高效分配,并将延迟降到最低。

消息流程

一对一聊天

  1. 用户 A 向聊天服务器 1 发送一条消息。
  2. 聊天服务器 1 分配一个唯一的消息 ID,并将消息存储到键值存储中。
  3. 如果用户 B 在线,消息会被转发到与其维持着持久 WebSocket 连接的聊天服务器 2。
  4. 如果用户 B 离线,则发送一条推送通知。

群聊

群聊流程
  • 消息会被复制到群组中每个接收者各自的收件箱中。
  • 这简化了同步,但对于较大的群组来说代价高昂。
  • 在接收方,一个接收者可以收到来自多个用户的消息。每个接收者 都有一个收件箱(消息同步队列),其中包含来自不同发送者的消息。

消息同步

许多用户拥有多台设备。我们需要在各设备之间同步消息。 每台设备都维护一个名为 cur_max_message_id 的变量,用于记录该设备上最新的 消息 ID。满足以下两个条件的消息被视为 新消息:

消息同步
  • 接收者 ID 等于当前登录用户的 ID。
  • 键值存储中的消息 ID 大于 cur_max_message_id

在线状态

  1. 心跳机制(Heartbeat Mechanism):

    心跳机制

    • 客户端定期向在线状态服务器发送心跳,表明自己在线。
    • 如果在某个阈值时间内(例如 x = 30)没有收到心跳,该用户就会被标记为离线。
  2. 扇出模型(Fanout Model):

    在线状态扇出

    • 在线状态的更新通过发布-订阅(publish-subscribe)模型推送给好友,其中每一对好友维护一个频道。
    • 当用户 A 的在线状态发生变化时,它会把事件发布到三个频道:频道 A-B、A-C 和 A-D。
    • 这三个频道分别由用户 B、C 和 D 订阅,他们由此收到在线状态的更新。
    • 上述设计对于小型用户群组是有效的。

其他考虑因素

可扩展性

  • 水平扩展: 随着用户数量增长增加服务器。
  • 负载均衡: 将流量均匀地分发到各服务器。
  • 缓存: 降低数据库负载并改善延迟。

错误处理

  • 重试机制: 通过重试和排队来处理消息投递失败。
  • 服务器故障: 发生故障时,利用服务发现分配新的服务器。

未来扩展

  1. 媒体支持: 增加对照片和视频的处理,包括压缩和云存储。
  2. 端到端加密(End-to-End Encryption): 确保消息的隐私。
  3. 客户端缓存: 减少数据传输以提升性能。
  4. 缩短加载时间: 使用地理上分布式的缓存网络。