跳至主要內容

【聊天系统】设计核心

holic-x...大约 23 分钟架构系统设计

【聊天系统】设计核心

学习核心

  • 如何设计一个【聊天】系统?(系统的核心功能)
  • 沟通对齐
    • ① 需求分析
      • 1对1:一对一的聊天,消息传递延迟低
      • 群聊:小组聊天(最多100人)
      • 在线状态:支持多端设备,同一账户可以同时登录多个设备
      • 推送通知
    • ② 请求量分析:
    • ③ 精准度分析:
    • ④ 难点分析
      • 聊天系统核心
        • (1)通信方式的选择:从轮询、长轮询、WebSocket 一步步引入话题,结合发送方和接收方的场景进一步分析说明HTTP和WebSocket在不同功能实现时的选择
          • HTTP 无状态协议,可以基于其实现聊天APP的大多数功能(登录、注册、用户资料等)
          • WebSocket 双向通信,用于实现聊天功能(发送端和接收端)
        • (2)聊天系统服务分类:无状态服务、有状态服务、第三方服务集成
          • 无状态服务:用于常见的功能通信(例如登录、注册、用户资料等)
          • 有状态服务:聊天服务(用户客户端构建聊天通信)
          • 第三方服务集成:推送通知
  • 整体设计
    • ① 服务设计(分层设计)
    • ② 存储设计(存储选型)
      • 存储选型:KV store
      • 数据模型:消息表(message_id 主键消息ID)、组消息表(channel_id、message_id 复合主键)
    • ③ 业务设计(业务流程)
  • 要点分析(或难点分析)
    • ① 服务发现:用户登入系统就将相关的登录信息缓存到存储中,并通过服务发现机制将用户与推介最优服务器建立连接
    • ② 消息流
      • 1对1聊天流程:
        • 发消息:用户发送消息到应用服务器,随后服务器将消息转到MQ中,根据目标用户的在线状态来择选不同的操作。如果目标用户在线则发送至该用户连接的服务器上,由该服务器推送给目标用户;如果目标用户离线,则从推送通知(PN)服务器发送推送通知
      • 跨设备信息同步:
        • 每个设备维护核心字段cur_max_message_id,根据用户ID和message_id筛选最新消息并同步
      • 群组聊天流程:组内每个用户有自己的独立的收件箱(MQ消息队列理解为收件箱),假设组内有ABC三个用户
        • A发消息:A发送消息,消息会被推送到BC各自关联的MQ,然后根据BC的在线状态进行处理
        • C收消息:A、B发送的消息会被发送到C关联的收件箱(MQ)中
    • ③ 在线/离线状态
      • 用户登入/登出:
        • (1)用户的登录流程体现在"服务发现"这一个过程,在客户端和实时服务之间建立WebSocket连接后,用户A的在线状态和最后活动时间戳被保存在KV存储中。状态指示器显示用户在登录后处于在线状态
        • (2)当用户注销登录时,会经历下图所示用户注销流程。 KV store 中在线状态变为离线状态。 状态指示器显示用户离线
      • 用户断开连接:引入心跳机制,避免由于网络抖动导致的频繁连接或断开
      • 在线状态输出
        • 状态服务器使用发布-订阅模型,其中每个朋友对都维护一个频道。 当用户A的在线状态发生变化时,将事件发布到三个频道,频道A-B,A-C,A-D。 这三个频道分别由用户 B、C 和 D 订阅。 因此,朋友们很容易获得在线状态更新。 客户端和服务器之间的通信是通过实时 WebSocket 进行的
  • 总结陈述

学习资料

🟢【聊天系统】场景核心

​ 日常生活中几乎每个人都使用聊天应用程序,下述列举比较流行的应用程序:WhatsappFacebook messengerWechatLineGoogle HangoutDiscord。聊天应用程序对不同的人执行不同的功能

🚀【聊天系统】场景实战

1.沟通对齐

​ 此处的沟通对齐方向,主核心方向是需求分析、请求量分析、精准度分析、难点/要点分析,可能还有涉及到其他的一些容量、设计等方面的对齐

① 需求分析

【聊天】系统核心功能?

​ 市场上有Facebook Messenger、微信和WhatsApp等一对一聊天应用,Slack等专注于群聊的办公聊天应用,Discord等专注于大型群聊和低语音聊天延迟的游戏聊天应用

  • 需求切入点方向
    • 要设计怎样的聊天应用?1对1还是群聊?=》两者都支持
    • 要设计一个手机APP还是Web APP?=》两者都支持
    • 应用程序的规模(创业公司的应用还是大规模应用)=》支持5000万日活跃用户(DAU)
    • 对于小组聊天,小组成员的限制是什么?=》最多100人
    • 对于聊天应用程序而言,有哪些功能是重要的,是否支持附件?=》例如1对1、群聊、在线状态等功能,系统仅支持文本信息
    • 发送信息大小限制?=》文本长度限制少于100000字符
    • 是否要支持端对端加密?=》后续有需要进行深入探讨
    • 聊天记录保存时长?=》永久保存

​ 基于上述分析,需要着重设计一个聊天应用,关注其核心功能:

  • 1对1:一对一的聊天,消息传递延迟低
  • 群聊:小组聊天(最多100人)
  • 在线状态:支持多端设备,同一账户可以同时登录多个设备
  • 推送通知

业务流程分析

② 请求量分析

③ 精准度分析

④ 难点/要点分析

(1)聊天系统核心之【通信方式的选择】

​ 为了开发一个高质量的设计,应该对客户和服务器的通信方式有一个基本的了解。在一个聊天系统中,客户端可以是移动应用程序或Web应用程序。客户端之间并不直接交流。相反,每个客户端都连接到一个聊天服务,它支持上面提到的所有功能,专注于基本操作。聊天服务必须支持以下功能:

  • 接收来自其他客户端的信息
  • 为每条信息找到合适的收件人,并将信息转达给收件人
  • 如果一个收件人不在线,就在服务器上保留该收件人的信息,直到她在线

​ 客户端(发送方和接收方)与聊天之间的关系服务

​ 当客户打算开始聊天时,它使用一个或多个网络协议连接聊天服务。对于一个聊天服务,网络协议的选择很重要(也是需要重点讨论的问题)

​ 对于大多数客户端/服务器应用程序,请求由客户端发起。结合上图示分析,当发送方通过聊天服务向接收方发送消息时,使用HTTP 协议(最常见的 Web 协议)。 在此场景中,客户端打开与聊天服务的 HTTP 连接并发送消息,通知服务将消息发送给接收者。 Keep-Alive 在此处起到关键作用, Keep-Alive 标头允许客户端与聊天服务保持持久连接,还减少了 TCP 握手的次数

​ HTTP 在发送端是一个不错的选择,许多流行的聊天应用程序(例如 Facebook 最初使用 HTTP 来发送消息)

​ 然而,对于接收方的情况就比较复杂了。由于HTTP是由客户发起的,消息被发送到service需要将其转达到对应的接收者,但从服务器发送消息并非易事。多年来,许多技术被用来模拟服务器发起的连接:轮询(Polling)、长轮询(Long polling)和 WebSocket(这些都是在系统设计场景中广泛使用的重要技术

⚽ 轮询

轮询:客户端定期询问服务器是否有消息可用

  • 缺点:
    • 基于轮询频率的不同,轮询的成本可能很高。它可能会消耗宝贵的服务器资源来回答一个大部分时间都没有答案的问题,可以理解为"空转"
image-20250206083402929
⚽ 长轮询

长轮询:客户端保持连接打开,直到实际有新消息可用或者到达超时阈值。一旦客户端收到新消息,它会立即向服务器发送另一个请求,重新启动进程

  • 缺点:

    • 送方和接收方可能不会连接到同一个聊天服务器:基于HTTP的服务器通常是无状态的,如果使用轮回技术进行负载平衡,接收信息的服务器可能没有与接收信息的客户端建立长期轮回连接。

    • 服务器无法很好感知客户端的连接状态:服务器没有很好的方法来判断一个客户是否断开了连接

    • 部分场景效率低下:它的效率很低,如果一个用户不怎么聊天,长时间的轮询仍然会在超时后进行周期性的连接

⚽ WebSocket

WebSocket:WebSocket是一种从服务器向客户端发送异步更新的最常见解决方案

image-20250206083830527

WebSocket 连接是由客户端发起的。它是双向且持久的,以HTTP连接的形式开始,并可通过一些定义明确的握手方式 "升级 "为WebSocket连接。通过这种持久的连接,服务器可以向客户端发送更新。即使有防火墙,WebSocket 连接通常也能工作(因为它们使用80或443端口,这些端口也被HTTP/HTTPS连接所使用)

​ 基于前面的分析,虽然在发送方使用HTTP协议是一种比较通用的方案,但由于WebSocket是支持双向的,因此也可以将其纳入发送端使用考虑,进而构建下述发送/接收核心流程

​ 通过使用WebSocket进行发送和接收,它简化了设计,并使客户端和服务器上的实现更加直接。由于WebSocket连接是持久的,因此有效的连接管理在服务器端至关重要

(2)聊天系统核心之【服务分类】

​ 聊天系统的服务架构被分成三大类:无状态服务、有状态服务和第三方集成,不同的服务架构用于实现相关的功能设计

  • stateless 无状态服务:传统的面向公众的请求/响应服务,用于管理登录、注册、用户资料等(这些是许多网站和应用程序中的常见功能)
    • 无状态服务位于负载均衡器后面,其工作是根据请求路径将请求路由到正确的服务。这些服务可以是单体的,也可以是单独的微服务,可以不需要自己建立许多这样的无状态服务,因为市场上有一些服务可以很容易地被集成
    • 服务发现:用于给客户提供一个可以连接到的聊天服务器的DNS主机名列表
  • stateful 有状态服务:唯一有状态的服务是聊天服务。该服务是有状态的,因为每个客户都与一个聊天服务器保持持久的网络连接
    • 在这个服务中,只要服务器仍然可用,客户通常不会切换到另一个聊天服务器。服务发现与聊天服务密切协调,以避免服务器过载
  • third party 第三方服务集成:参考【通知系统设计】
    • 对于一个聊天应用程序,推送通知是最重要的第三方集成。它是一种在新消息到来时通知用户的方式,即使应用程序没有运行

image-20250206092444235

2.整体设计(架构设计)

① 服务设计(分层设计)

​ 基于上述聊天系统核心分析了通信方式的选择和服务分类,此处进一步基于可扩展性优化结构设计。

​ 在小范围内,上面列出的所有服务都可以放在一台服务器中。即使以上述现有设计的规模,理论上也有可能在一个现代云服务器中处理所有的用户连接。服务器可以处理的并发连接数很可能是限制因素。

​ 在需求场景中,在 100w 并发用户的情况下,假设每个用户连接在服务器上需要10K内存(这是一个非常粗略的数字,非常依赖于语言选择),则只需要大约10GB的内存即可将所有连接保存在一个服务器上。

​ 但如果仅仅提出一种将所有内容都放在一台服务器中的设计,在交流的过程中可能会在对方的脑海中升起一个大大的不好信号。 没有技术专家会在单个服务器中设计这样的规模。由于多种因素,单服务器设计是交易的障碍,单点失败是其中最大的。因此,基于上述起点,还需要考虑如果拆分架构使得其更具备可靠性,进一步得到下述架构

image-20250206095259719

​ 基于上述图示,构建聊天系统架构分析:

  • ① 聊天服务器(chat servers):促进了信息的发送/接收
  • ② 在线服务器(presence servers):管理在线/离线状态
  • ③ API 服务器(API servers):处理一切,包括用户登录、注册、更改资料等
  • ④ 通知服务器(notification servers):发送推送通知
  • ⑤ 数据存储(KV store):存储聊天历史记录

② 存储设计(存储选型)

(1)存储选型分析

​ 基于上述分析完成核心基础服务架构设计,但在技术栈的深处是数据层的选择。基于数据库类型适用场景分析:关系型数据库还是NoSQL数据库?针对不同数据的存储选择,要分析在一个典型的聊天系统中需要存储什么数据?

在一个典型的聊天系统中存在两类数据:

  • ① 通用数据:如用户资料、设置、用户朋友列表。这些数据被存储在强大而可靠的关系数据库中,复制和分片是满足可用性和扩展性要求的常见技术
  • ② 聊天场景限定:第二种是聊天系统特有的,即聊天历史数据(了解读/写模式很重要),其有如下特点
    • 聊天系统的数据量是巨大的(例如数据显示Facebook 和 Whatsapp 每天要处理600亿条信息)
    • 只有最近的聊天记录被频繁访问,用户通常不会查找旧的聊天记录(但也需要提供查询支持)
    • 虽然在大多数情况下都会查看最近的聊天记录,但用户可能会使用需要随机访问数据的功能,如搜索、查看提及内容、跳转到特定的消息等。这些情况应该由数据访问层来支持
    • 一对一聊天应用程序的读写比约为 1:1

​ 基于上述分析可知,选择正确的存储系统,支持所有的使用案例是至关重要的。推荐键值存储(KV store),理由如下:

  • 键值存储允许容易的水平扩展
  • 键值存储为访问数据提供了非常低的延迟
  • 关系型数据库不能很好地处理长尾的数据。当索引变大时,随机访问是很昂贵的
  • 键值存储被其他成熟可靠的聊天应用程序所采用。例如,Facebook 和 Discord 都使用键值存储(Facebook 使用 HBase,而 Discord 使用 Cassandra)
(2)数据模型

​ 数据层最重要的是消息记录的存储,因此设计下述数据模型:

  • 消息表:message_id 作为主键(有助于决定消息的顺序),不能单纯created_at决定消息顺序(因为多条消息可以同时被创建)
  • 群聊消息表:channel_id 和 message_id 作为复合主键(channel_id作为分区键,在群聊天中所有的查询都在一个通道中运行),频道和组在此表示相同的含义

image-20250206100454090

消息ID 的设计:

  • 如何生成message_id:须确保ID唯一、有序(可基于时间排序)

​ 基于此第一时间可能会联想到MySQL的auto_increment,但对于NoSQL数据库而言并没有提供这样的功能。因此思考其他唯一ID的生成方案,例如UUID、Snowflake 序号生成器等。基于本地序号生成器方案的思考,本地意味着ID只在一个组内是唯一的。本地ID发挥作用的原因是,在一对一的信道或一个组的信道内维持消息序列就足够了。与全局ID的实现相比,这种方法更容易实现

③ 业务设计(业务流程)

​ 基于上述架构分析,对于聊天系统,服务发现、消息流、在线/离线状态这些问题还有进一步探讨空间

(1)服务发现

​ 服务发现的主要作用是根据地理位置、服务器容量等标准,为客户推荐最佳的聊天服务器。Apache Zookeeper是一个流行的服务发现开源解决方案。它注册了所有可用的聊天服务器,并根据预定义的标准为客户挑选最佳聊天服务器。下图展示服务发现(Zookeeper)的工作流程

image-20250206102307260

  • ① 用户登录APP(进行登录认证)
  • ② 负载均衡器发送登录请求到API服务
  • ③ 经由后端认证,服务发现为用户A找到最佳的聊天服务器(例如此处选中Chat Server X),返回选中的服务器信息给用户
  • ④ 随后用户通过Web Socket连接到聊天服务器Chat Server X
(2)消息流

核心:了解一个聊天系统的端到端流程(探讨1对1的聊天流程、跨多个设备的信息同步、群组聊天流程)

⚽ 1对1的聊天流程
  • 1 对 1 聊天流程分析(UserA 发送消息给 UserB)
    • ① 用户A向聊天服务器1发送1条聊天信息(此处会通过服务发现为UserA选择一个最佳的聊天服务器建立连接)
    • ② 聊天服务器1从ID生成器中获取message_id(消息ID)
    • ③ 随后聊天服务器1打包好消息发送至MQ(消息同步队列)
    • ④ 消息被存储在KV Store中
    • ⑤ 根据UserB的在线/离线状态执行操作:
      • 如果UserB在线:信息被转发到UserB所连接的聊天服务器2
      • 如果UserB离线:从推送通知(PN)服务器发送推送通知
    • ⑥ 聊天服务器2将消息转发给UserB,UserB和聊天服务器2之间有一个持久的WebSocket连接

image-20250206103506284

⚽ 跨多个设备的信息同步

​ 每个用户有多个设备,结合下图示分析如何在多个设备上同步消息:UserA有3台设备(手机、平板、电脑),当用户A用设备登录聊天应用程序的时候会和聊天服务器1建立一个WebSocket连接(同理手机、平板、电脑无论是哪个设备都是基于此流程),每个设备端都维护着一个cur_max_message_id变量(用于记录设备上最新的消息ID),则可基于此筛选新消息:

  • 收件人ID等于当前登录的用户ID
  • 键值存储中的消息ID大于 cur_max_message_id

​ 基于上述设计,由于每个设备上都维护着各自的cur_max_message_id,因此信息同步很容易(每个设备都可以从KV store中获取到当前登录用户存储的最新信息)

image-20250206104935206

⚽ 群组聊天流程

​ 与一对一的聊天相比,群组聊天的逻辑更加复杂(此处简化ID生成、固化存储的一些细节,专注理解群组聊天的核心流程)。假设有一个包含UserA、UserB、UserC 三位小组成员的小组,分析组聊天发送和接收消息的核心流程。

  • User发送消息:用户消息被复制到每个组员的消息同步队列中,可以理解为每个成员拥有自己的收件箱(消息同步队列)
    • 设计优点:
      • (1)简化了信息同步流程,每个用户只需要检查自己的收件箱即可获取最新消息
      • (2)在群组人较少的时候,每个成员收件箱中存储一份消息副本的成本并不太高
    • 设计不足:
      • 微信使用类似的方法,它将一个群组限制在500个成员。然而,对于拥有大量用户的群组来说,为每个成员存储一份信息副本是不可接受的
  • User接收消息:一个用户可以接收来自多个用户的信息,每个收件人都有一个收件箱(消息同步队列),其中包含来自不同发送者的消息

image-20250206110419563

(3)在线/离线

​ 在线状态指示器是许多聊天应用程序的一个基本功能。通常情况下,可以在用户的个人照片或用户名旁边看到一个绿点(表示用户在线)

​ 在高层设计中,在线服务器负责管理在线状态,并通过WebSocket与客户端进行通信。有几个流程会触发在线状态的变化,此处分析这些状态的变化

⚽ 用户登入/登出

​ 用户的登录流程体现在"服务发现"这一个过程,在客户端和实时服务之间建立WebSocket连接后,用户A的在线状态和最后活动时间戳被保存在KV存储中。状态指示器显示用户在登录后处于在线状态

​ 当用户注销登录时,会经历下图所示用户注销流程。 KV store 中在线状态变为离线状态。 状态指示器显示用户离线

image-20250206111955528

⚽ 用户断开连接

​ 事实上,互联网连接并非完全一致和可靠,因此,必须在设计中解决这个问题。当一个用户从互联网上断开连接时,客户端和服务器之间的持久连接就会丢失。

​ 处理用户断开连接的一个天真的方法是将用户标记为离线,并在连接重新建立时将其状态改为在线。然而,这种方法有一个重大缺陷。用户在短时间内频繁地断开和重新连接到互联网是很常见的。例如,当用户通过隧道时,网络连接可能会打开和关闭。而由于网络抖动导致在每次断开/重新连接时更新在线状态会使存在指标变化得太频繁,导致用户体验不佳

​ 因此引入一个心跳机制来解决这个问题。定期地,一个在线客户端向状态服务器发送一个心跳事件。如果状态服务器在一定时间内收到心跳事件,比如说来自客户端的X秒,那么用户被认为是在线的。否则,它就处于离线状态。

​ 结合图示分析,客户端每5秒向服务器发送一个心跳事件。在发送了3个心跳事件后,客户端被断开连接,并且在x=30秒内没有重新连接(这个数字是任意选择的,以演示逻辑),在线状态被改变为离线。

image-20250206112215038

⚽ 在线状态输出

场景说明:UserA 的好友如何知道状态变化?

​ 结合图示分析:状态服务器使用发布-订阅模型,其中每个朋友对都维护一个频道。 当用户A的在线状态发生变化时,将事件发布到三个频道,频道A-B,A-C,A-D。 这三个频道分别由用户 B、C 和 D 订阅。 因此,朋友们很容易获得在线状态更新。 客户端和服务器之间的通信是通过实时 WebSocket 进行的。

image-20250206112340486

​ 上述设计对小规模的用户群是有效的。例如,微信使用类似的方法,因为它的用户群上限为500人。对于较大的群组,通知所有成员的在线状态是昂贵和耗时的。假设一个群组有100,000个成员。每一个状态变化将产生100,000个事件。为了解决性能瓶颈,一个可能的解决方案是采用限定触发:即只在用户进入群组或手动刷新好友列表时获取在线状态

3.要点分析

​ 此处介绍一个聊天系统架构,它支持1对1的聊天和小群组聊天。WebSocket用于客户端和服务器之间的实时通信。聊天系统包含以下组件:用于实时消息传递的聊天服务器、用于管理在线状态的状态服务器、用于发送推送通知的推送通知服务器、用于聊天历史持久性的键值存储以及用于其他功能的API服务器。

  • 扩展方向补充:

    • ① 扩展聊天应用程序以支持媒体文件,如照片和视频

      • 媒体文件的大小明显大于文本。压缩、云存储和缩略图是值得讨论的话题
    • ② 端到端加密

      • Whatsapp支持信息的端到端加密,只有发件人和收件人可以阅读信息
    • ③ 数据缓存

      • 在客户端缓存信息,可以有效地减少客户端和服务器之间的数据传输
    • ④ 提高加载时间

      • Slack建立了一个地理分布的网络来缓存用户的数据、频道等,以获得更好的加载时间
    • ⑤ 故障处理

      • 聊天服务器错误:可能有数十万,甚至更多的,坚持不懈的连接到一个聊天服务器。如果一个聊天服务器离线,服务发现(Zookeeper)会提供一个新的聊天服务器,让客户建立新的连接。
      • 消息重发机制:重试和排队是重发消息的常用技术

4.总结陈述

  • 深刻总结
  • 要点牵引
  • 收尾请教

🚀实战案例

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3