设计聊天系统

几乎所有人都用过聊天应用,如

WhatsApp、Facebook Messenger、WeChat、Line、Discord

理解问题

搞清楚设计的聊天应用类型,有一对一的聊天应用 WhatsApp,也有专注于群聊的办公聊天应用Slack,还有 专注于大型群组互动和低延时语音对话的游戏聊天应用 Discord。

  1. 即支持一对一聊天,也支持群聊
  2. 假设支持5000万的日活用户
  3. 群组人数上限100人
  4. 一对一聊天、群聊和在线状态显示,系统只支持文本消息
  5. 消息大小限制,文本长度小于 100000 个字符
  6. 暂时不需要端到端加密
  7. 聊天记录需要保存多久?永远

高层级设计

聊天服务必须支持下面的功能:

图12-2

网络协议的选择很重要。

发送消息,如果使用 HTTP keep alive请求头使用减少TCP握手次数,发送端使用HTTP也还行, 如 Facebook Messenger一开始是用HTTP协议来发送消息的。

接收端比较复杂,很多技术被用于模拟服务器发起链接,包括轮询、长轮询、WebSocket。

轮询

周期性询问服务器是否有新消息的技术,轮询的开销可能很大,取决于轮询的频率。

图12-3

长轮询

轮询的效率较低,长轮询有时是更好的选择

图12-4

长轮询中,客户端保持链接处于打开状态,直到有新消息可用活达到超时阈值, 服务器才返回,一旦客户端接收新消息,就会立刻将另一个请求发送给服务器,重新开始流程。

WebSocket

图12-5

WebSocket连接由客户端发起,双向且持续。

WebSocket被选择为客户端和服务器之间的主要通信协议,但注意, 聊天系统中的其他部分不一定非得用WebSocket,实际上大部分功能 注册、登录、 获取用户个人信息 使用的都是传统的 HTTP 协议。

有无状态服务

无状态服务是传统的面向大众的请求响应服务,用于管理登录、注册、用户个人信息等。

无状态服务位于负载均衡器之后,负载均衡器根据请求路径把请求路由到正确的服务上, 服务可以是单体应用或者独立的微服务。

有状态服务,在聊天系统中,唯一有状态服务是聊天服务,因为每个客户端都维持了一个和 聊天服务器的持久连接。只要聊天服务器依然可用,客户端通常不会将连接切换到另一个聊天服务器上。 服务发现和聊天服务密切合作,可以避免服务器过载。

图12-7

第三方集成

在聊天应用中,推送通知是与第三方服务集成的最重要的功能。

可扩展性

一个服务器可以处理的并发连接数量,最有可能成为限制因素, 假设场景有100万并发用户,假设每个用户连接需要10KB内存,那么 只需要约10GB内存就可以在服务器上保存所有连接。

合理的将服务进行分离解耦。

图12-8

存储

用什么类型的数据库?

典型的聊天数据系统中,有两种类型的数据,第一种通用数据 如用户个人信息、设置、 用户的好友列表等,可以存储在健壮且可靠的关系型数据库中。

聊天系统特有的数据,聊天历史数据,Facebook Messenger 和 WhatsApp 每天处理600亿条信息, 只有最近的聊天记录会被经常访问,用户通常不会查找很早之前的聊天记录。但用户也会使用 搜索、查看自己被提及的消息,跳转特定的消息等。

推荐使用键值存储,键值存储能轻松地进行横向扩展、访问延时很低、关系型数据库不能 很好地处理长尾数据 当索引变得很大时 随机访问的开销很大,Facebook Messenger 用的 是HBase、Discord用的是Cassandra。

数据模型

使用键值存储来作为存储层,最重要的数据是消息数据,探讨消息数据的组织和存储方式。

一对一聊天的消息表,

图12-9

群聊的消息表,复合主键是 channel_id message_id

图12-10

消息ID,必须是唯一的,应该可以按时间排序,意味着新创建的行的ID值更大。可以用 Snowflake。

服务发现

服务发现的主要功能是,基于地理位置、服务器性能等条件来推荐对于客户端来说最佳的 聊天服务器。

Apache Zookeeper是一个流行的解决方案,它注册所有可以用的聊天服务器,然后基于 预先设定的条件来为客户端选择最佳的聊天服务器。

图12-11

一对一聊天消息流

当用户A发消息给用户B时发生了什么

图12-12
  1. 用户A发一条消息给聊天服务器1
  2. 聊天服务器1从ID生成器获取一个消息ID
  3. 聊天服务器1将消息发送给消息同步队列
  4. 将消息存储在键值存储中
  5. 如果用户B在线,消息将被转发到用户B连接的聊天服务器2上
  6. 如果用户B不在线,一个推送通知将被发给通知服务器
  7. 聊天服务2转发消息给用户B

多个设备之间同步消息

很多用户拥有多个设备,如何在多个设备之间同步消息

图12-13

没个设备都维护了一个叫,cur_max_message_id 的变量,用于追踪设备上最新消息的ID。每个设备的 cur_max_message_ud 是不同的,每个设备都可以从键值存储中获取各自的新消息, 因此消息同步变得容易。

群聊消息流

当用户A发送一条消息,假设群里有 ABC 三个用户,来自用户A的消息被复制到每个群成员 的消息同步队列中。消息同步队列想象成收件箱,每个收件人都有一个。

图12-14

微信使用了类似的方法,将群成员数限制为500人。如果群成员非常多,那么为每个成员 存储消息副本是不可接受的。

在接受端,一个接受者可以收到来自多个用户的消息。

图12-15

显示在线状态

当客户端和实时服务之间建立WebSocket连接后,用户在线状态和最后活跃时间戳存在 键值存储中。

图12-16

用户退出时,在线状态在键值存储中修改为离线。

用户连接断开,引入心跳机制,在线客户端定期发送一个心跳时间给在线状态服务器,如果 在线状态服务器在一定时间内收到了来自客户端的心跳事件,用户就被认为是在线的,否则被视为离线。

在线状态广播

用户A的好友怎么知道用户A在线状态的变化呢?

在线状态服务器使用发布订阅模型,为每对好友维护一个频道,当用户A的在线状态改变时, 向每个频道发送在线改变信息,频道订阅者就是自己的好友。这种设计对小用户群和好友是有效的, 如果好友或加入群太多,这将是个灾难。

一种可能的解决方案是只有当用户进群或手动刷新好友列表时才获取在线状态。