Step 1 - 明确需求

一些可供参考的需求讨论点:

  1. 哪种类型的聊天系统,一对一还是群聊?=>都支持
  2. 移动端 or 网页端 or both?=> both
  3. 用户规模多大?适用于初创公司还是大公司?=> 日活按5000万计算
  4. 群聊人数有限制吗?=> 最多100人
  5. 需要支持哪些特性?支持附件吗?=>一对一,群聊,上线提示,只支持文本。
  6. 消息大小有限制吗?=>单条不超过1万字
  7. 需要端对端加密吗?=>当前不需要,后续可能需要
  8. 聊天记录需要保持多久?=>永久保存

接下来以下面的需求作为系统设计的目标:

  • 一对一低延时聊天系统
  • 支持最多100人的群聊
  • 上线提示
  • 多平台同时线支持。
  • 消息通知推送。
  • 日活5000万。

Step 2 - 概要设计

客户端与客户端不直接通信,而是通过聊天服务进行通信,聊天服务需要支持以下特性:

  • 从客户端接收消息
  • 将消息发送给接收者
  • 如果接收者不在线,则将消息暂存起来,直到接收者上线

客户端上线时需要与聊天服务建立连接,这里涉及到连接协议的问题。对于客户端来说,使用HTTP协议是一个常见的选择,客户端使用HTTP协议向服务端发起请求,并且将聊天内容和接收者附加在HTTP消息中,聊天服务器收到HTTP请求后会将消息转发给接收者。客户端和服务器之间使用保活维持聊天连接,减少TCP握手次数。

使用HTTP协议对于客户端来说很方便,但是对于服务端来说就比较复杂了,因为HTTP请求只能由客户端发起,这就导致服务端无法主动发起会话。有很多针对服务端主动发起会话的技术,包括轮询,长轮询,WebSocket。

轮询

指客户端循环检测服务器是否有新消息。根据循环的间隔,轮询可能非常占用资源,导致系统效率低。

长轮询

针对轮询系统效率低的问题,发展出了长轮询。长轮询是指客户端向查询服务器端是否有新消息时,不立即返回,而是等服务器有消息,或是超时之后再返回,这种方式有以下缺点:

  • 发送者和接收者可能不在同一个聊天服务器上。HTTP协议通常是无状态的,如果使用轮转的方式做负载均衡,则有可能发送者和接收者不在同一个聊天服务器上。
  • 服务器无法判断客户端是否下线。
  • 效率不高。如果用户发送聊天信息不频繁,那么维护长轮询也会导致周期性的重连。

WebSocket

WebSocket是最常见的用于服务端向客户端发起连接的手段。WebSocket由客户端发起,支持双向连接和会话保持。WebSocket从HTTP请求开始,通过握手协议升级成WebSocket通信。升级成WebSocket后,客户端和服务端就可以进行双向通信了。即使是在有防火墙的情况下,WebSocket也可以很好的工作,因为它和HTTP/HTTPS使用相同的端口,而这两个端口在服务器上一般是开放的。由于WebSocket本身支持双向通信,所以客户端也不需要使用HTTP协议了,客户端和服务器都可以使用WebSocket协议进行通信。

总体设计

整个系统包含三部分:无状态服务,有状态服务,第三方服务。服务端和客户端之间的双向连接采用WebSocket实现,但是像其他功能,比如登录,注册,获取用户配置等还是可以使用传统HTTP基于请求/响应模型进行实现。

无状态服务

用于实现传统的面向公众的请求/响应服务,比如注册,登录,获取用户配置等。

通过负载均衡将请求导向不同的服务接口来实现无状态服务。这些服务接口可以是一个整体,也可以是微服务。这里的大部分服务都可以使用现成的商业服务。需要讨论的一点是服务发现,它的主要工作是给客户端提供一系列可用的聊天服务器的DNS主机名。

有状态服务

只包含聊天服务。客户端与聊天服务器需要保持长连接,并且只要当前聊天服务器可用,客户端就不会切换服务器。服务发现功能可用于协调聊天服务以避免服务器过载。

第三方集成

最重要的第三方集成组件是消息推送,用于通知用户有新消息到达。

可扩展性

先忽略单点故障问题,从单服务器设计开始,如下:

客户端通过WebSocket和聊天服务器保持实时通信。

  • 聊天服务器负责消息接收和转发。
  • 上线服务器负责管理上下线。
  • API服务器负责处理诸如用户登录,注册,修改配置等请求。
  • 推送服务器负责推送通知。
  • 最后,键值数据器用于存储聊天历史记录,当离线用户上线时会收到之前的会话消息。

存储

当前的设计已经包含了服务器,并且服务可运行,也集成了第三方组件。接下来要处理的是数据存储问题。首先是该使用哪种类型的数据库:关系型 or 非关系型?在处理这个问题之前,我们先分析一下数据类型和读写模式。

聊天系统中存在两种典型的数据。一种是通用数据,比如用户配置,好友列表,这些数据要存储在关系型数据库里,通过复制和分片保证可用性和扩展性。第二种是聊天历史记录,需要关注读写模式:

  • 对于聊天系统来说,数据是无穷的。
  • 只有最近的聊天数据才会频繁用到,用户通常不会翻看很久以前的聊天记录。
  • 虽然只有最近的聊天数据才会频繁用到,但用户也可能随机获取以前的聊天记录,比如搜索聊天关键字,或者跳转到某天的聊天记录。
  • 对于一对一聊天来说,读写比例为1:1。

选择合适的存储系统用于保存聊天历史至关重要,这里推荐使用键值数据库,原因如下:

  • 适合水平扩展
  • 延时低
  • 适合随机获取数据,当数量增大时,关系型数据库在随机查找方面开销巨大
  • 键值数据库已经应用在其他成功的聊天应用上,比如Facebook messenger和Discord。

数据模型

在确定使用键值数据库后,接下来是设计数据模型。

一对一的聊天消息表

使用如下的设计,主键是消息id,用于确定消息的顺序。不能仅靠创建时间来确定消息顺序,因为有概率两条消息被同时创建。

群聊的消息表

使用<频道id,消息id>来作为主键。

消息ID

消息ID兼具保证消息排序功能,为了保证消息有序,消息ID要满足下面两个特性:

  • 必须唯一
  • 必须按时间自增,也就是新的消息ID比老的消息ID要大。

一般的处理方法是使用数据库的自增主键,但是非关系型数据库一般不提供这个特性。

第二种方式是使用全局的64位序列号发生器,比如雪花算法,限制是每秒钟不可能生成太多的ID,以及并发访问问题。

最后的方式是使用本地的序列号发生器。本地意味着所有的ID只在当前环境里是唯一的,但是这也能满足需求,因为只需要保证在单个聊天频道里的消息ID唯一就行了,不同的聊天频道的消息ID可以重复。

Step 3 - 详细设计

重点探讨一下服务发现,消息流,上下线提示。

服务发现

基于服务端负责以及地理位置等信息向客户端推荐最适合的服务器。Apache Zookeeper是一个很流行的开源服务发现工具。它负责注册所有的聊天服务器,以及基于预定义的规则从服务器中选出最合适的一个。

工作流程如下:

  1. 用户A尝试登录APP
  2. 负载均衡将请求发送给API服务器
  3. 认证成功后,服务发现找出最合适的聊天服务器,这里是服务器2,将该服务器的信息返回给用户A。
  4. 用户A通过WebSocket连接聊天服务器2。

消息流

搞清楚端到端情况下的消息流,这里讨论一对一消息流,消息多端同步,以及群聊。

一对一消息流

流程如下:

  1. 用户A发送一条消息给聊天服务器1。
  2. 聊天服务器1从ID生成器中获取一个消息ID。
  3. 聊天服务器将消息发送给消息同步队列。
  4. 消息存储在键值服务器中。
  5. 如果用户B在线,将消息转发用户B所在的聊天服务器2。
  6. 如果用户B不在线,则给推送服务器推送一条消息。
  7. 聊天服务器2通过持久的WebSocket将消息转发给用户B。

消息多端同步

这里用户A有两个设备:手机和笔记本电脑。当用户通过手机登录时,手机APP和聊天服务器1之间建立一个持久的WebSocket连接。同样,在用笔记本登录时,笔记本电脑也和服务器1有一个持久的WebSocket连接。

每个设备上都维护一个cur_max_message_id变量,用于表示该设备上当前最新的消息ID。检查键值数据库中的消息,如果有消息ID比设备上的cur_max_message_id变量更大,并且消息的接收者ID与当前登录的用户ID一致时,将这条消息同步给当前设备。

上面的消息多端同步方式本质就是增量同步。

小规模群聊消息流

流程:

  1. A发送一条消息
  2. 将A发送的消息同步到B和C的消息同步队列
  3. B和C从各自的消息队列中取出A的消息

优点:

  • 简化了消息流,因为每个用户只需要检查自己的收集箱就可以拿到最新消息
  • 当群聊规模较小时,将消息存储在每个成员的收集箱里成本可以接收

当群聊规模巨大时,为每个成员存储消息备份将占用巨大的资源,这时可以采用下面的设计,这个设计的关键是接收者主动从其他成员的聊天服务器上拉取消息:

上下线提示

用于提示用户上下线,比如上线后在头像上附加一个绿点标识。

用户登录

当用户登录后,用户的登录状态以及last_active_at时间戳都被保存到键值数据库中,上下线提示服务器指示用户在线。

用户登出

将键值数据库中的用户状态改为离线,上下线提示服务器提示下线。

用户异常掉线

用于处理用户异常掉线的情况。这里直接将用户设置成离线状态并不合适,因为用户有可能稍后就连接上来了,如此一来上下线提示服务器就会频繁提示上下线。

这里引入一个心跳机制,用户在上线后周期性地向上下线提示服务器发送心跳,服务器通过心跳来判断用户是否在线,如果心跳超时一定时间或次数,则认为用户掉线。

在线状态扇出

用于将用户已掉线的消息推送给用户的朋友列表,设计如下,通过消息队列来实现,这种方式在小规模时很有效,但规模较大时,将占用大量资源。

在规模较大时,一种方案是只在用户登录或者手动刷新好友列表时检查一次在线状态。

Step 4 - 总结

以下几点可供扩展讨论:

  • 扩展该系统以支持媒体文件,比如图片和视频。由于媒体文件通常较大,数据压缩,云存储,缩略图是可供讨论的点。
  • 端到端加密。
  • 客户端本地缓存数据。
  • 通过分布式服务缩短加载时间。
  • 错误处理:
    • 单个聊天服务器可能需要应对成千上万个客户端连接,如果服务器出现错误,则需要通过服务发现功能将新服务器的地址发送给客户端,以重连建立聊天连接
    • 消息重传机制。重传加队列处理是常用的处理办法。

























  • 无标签