系统设计:聊天应用

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版,欢迎购买。点击进入详情

目录

了解实时聊天应用程序的架构和系统设计

系统的要求和目标

功能要求

非功能性需求

高层系统设计

贮存

数据模型

1对1聊天的消息表

群聊消息表

消息ID

API设计

1. 发送消息

2. 获取消息

3.上传媒体或文档文件

4. 下载文档或媒体文件

详细设计

WebSocket服务器

服务发现

发送或接收消息

支持群组消息

为什么在我们的聊天应用程序中使用 SQL 数据库:

用户服务

媒体文件

最终设计

非功能性需求

权衡

一致性和可用性之间的权衡

延迟和安全性之间的权衡

资源估算

存储估算

带宽估计

服务器数量估计


了解实时聊天应用程序的架构和系统设计

系统的要求和目标

功能要求

  • 对话:系统应支持用户之间的一对一和群组对话。
  • 确认:系统应支持消息传递确认,例如已发送、已传递和已读。
  • 共享:系统应支持媒体文件的共享,例如图像、视频和音频。
  • 聊天存储:系统必须支持用户离线时聊天消息的持久存储,直至消息成功传递。
  • 推送通知:一旦离线用户的状态变为在线,系统应该能够向其通知新消息。

非功能性需求

  • 低延迟:用户应该能够以低延迟接收消息。
  • 一致性:消息应按照发送顺序传递。此外,用户必须在其所有设备上看到相同的聊天历史记录。
  • 可用性:系统应具有高可用性。然而,一致性比可用性更重要。
  • 安全性:系统必须通过端到端加密确保安全。我们需要确保只有通信双方才能看到消息的内容。中间的任何人,甚至我们作为服务所有者,都不应有权访问。
  • 可扩展性:系统应具有高度可扩展性,以支持每天不断增加的用户和消息数量。

高层系统设计

首先,我们需要了解客户端和服务器如何通信。在聊天系统中,客户端可以是移动应用程序或 Web 应用程序。客户端之间不直接通信。每个客户端都连接到一个聊天服务,该服务支持我们之前讨论的所有功能:

  • 接收来自其他客户端的消息。
  • 为每条消息找到正确的收件人,并将消息转发(传递)给收件人。
  • 如果收件人不在线,则需要在服务器上保留该收件人的消息,直到他们在线为止。

由于 HTTP 是客户端发起的,我们无法真正从服务器向接收者发送消息,因此我们需要考虑用于模拟服务器发起的连接的其他技术:轮询、长轮询和 WebSocket。

  • 轮询是客户端定期向服务器请求数据的方式,会产生大量请求,效率低下。
  • 长轮询,服务器保持连接打开,直到有新数据可用,从而减少请求数量和延迟。
  • WebSocket 是一种双向通信协议,可通过单个长期连接实现客户端和服务器之间的实时通信,从而提供最低的延迟。这是从服务器向客户端发送异步更新的最常见解决方案。

两个客户端之间的逐步通信如下:

  1. 用户A和用户B创建与聊天服务器的通信通道。
  2. 用户A向聊天服务器发送消息。
  3. 当收到消息时,聊天服务器会向用户 A 回复确认。
  4. 如果接收者的状态为离线,则聊天服务器将消息发送给用户B,并将消息存储在数据库中。
  5. 用户 B 向聊天服务器发送确认。
  6. 聊天服务器通知用户A消息已成功发送。
  7. 当用户 B 阅读消息时,应用程序通知聊天服务器。
  8. 聊天服务器通知用户A用户B已阅读消息。

对于客户端-服务器通信WebSocket 优于 HTTP(S) 协议因为 HTTP( S) 不会保持连接打开,以便服务器频繁向客户端发送数据。使用 HTTP(S) 协议时,客户端不断向服务器请求更新,这会占用大量资源并导致延迟。 WebSocket 在客户端和服务器之间维护持久连接。只要数据可用,该协议就会立即将数据传输到客户端。它提供了双向连接,用作将异步更新从服务器发送到客户端的通用解决方案。

其他一切不一定都是 WebSocket。事实上,聊天应用程序的大多数功能(注册、登录、用户配置文件等)都可以使用基于 HTTP 的传统请求/响应方法。让我们看看我们系统的高级组件。聊天系统分为三大类:无状态服务、有状态服务、第三方集成。

无状态服务是传统的面向公众的请求/响应服务,用于管理登录、注册、用户配置文件等。它们位于负载均衡器后面,负载均衡器的作用是路由根据请求路径向正确的服务发出请求。这些服务可以是整体的或单独的微服务。我们不需要自己构建许多无状态服务,因为市场上有可以轻松集成的服务。我们将深入讨论的一项服务是服务发现。它的主要工作是向客户端提供客户端可以连接到的聊天服务器的 DNS 主机名列表。

有状态服务唯一的有状态服务是聊天服务。该服务是有状态的,因为每个客户端都维护与聊天服务器的持久网络连接。在此服务中,只要服务器仍然可用,客户端通常不会切换到另一个聊天服务器。服务发现与聊天服务紧密配合,以避免服务器过载。

第三方集成用于推送通知,以便在新消息到达时通知用户,即使应用未运行也是如此。

贮存

我们需要考虑使用哪种类型的数据库:关系数据库还是NoSQL。在典型的聊天系统中,我们将有两种类型的数据。第一个是通用数据,例如用户个人资料、设置和用户好友列表。这些数据应该存储在可靠的关系数据库中。我们当然需要实现复制和分片来满足可用性和可扩展性要求。

第二个是聊天系统特有的:聊天历史数据。了解读/写模式很重要。

  • 对于聊天系统来说,数据量非常巨大(Facebook 每天有 600 亿条消息)。
  • 仅经常访问最近的聊天记录。用户通常不会查找旧聊天记录。
  • 尽管在大多数情况下都会查看最近的聊天历史记录,但用户可能会使用需要随机访问数据的功能,例如搜索、查看您的提及、跳转到特定消息等。这些情况应该得到数据访问层的支持。
  • 对于 1 对 1 聊天应用程序来说,读写比率约为 1:1。

所以我认为键值存储非常适合这里,因为:

  • 键值存储允许轻松水平扩展。
  • 键值存储提供非常低的数据访问延迟。
  • 关系数据库不能很好地处理数据的长尾。当索引变大时,随机访问的成本很高。
  • 键值存储已被其他经过验证的可靠聊天应用程序采用。例如,Facebook Messenger 和 Discord 都使用键值存储。

数据模型

1对1聊天的消息表

主键是message_id,它有助于决定消息的顺序。我们不能依靠created_at来决定消息顺序,因为可以同时创建两条消息。

群聊消息表

复合主键为(channel_idmessage_id )。 主键是数据库表中每一行的唯一标识符。复合主键由两个或多个列组成,它们一起唯一地标识一行。在本例中,复合主键由两列组成:channel_id 和 message_id.通道和组在这里代表相同的含义。 channel_id 是分区键,因为群聊中的所有查询都在通道中操作。

消息ID

message_id 负责消息的顺序。

  • ID 必须是唯一的。
  • ID 应可按时间排序,这意味着新行的 ID 高于旧行。

我们如何才能实现这两项保证?我首先想到的是MySql中的“auto_increment”关键字。然而,NoSQL 数据库通常不提供这样的功能。第二种方法是使用全局 64 位序列号生成器,例如 Snowflake。最后的方法是使用本地序列号生成器。本地意味着 ID 仅在组内是唯一的。本地 ID 起作用的原因是在一对一通道或组通道内维护消息序列就足够了。与全局 ID 实现相比,这种方法更容易实现。

API设计

1. 发送消息

此 API 用于通过对 /messages API 端点进行 POST API 调用,将文本消息从发送方发送到接收方。一般来说,发件人和收件人的 ID 是他们的电话号码。

sendMessage(sender_ID、reciever_ID、类型、文本=无, media_object< a i=4>=无,文档=无 )
  • sender_ID → 发送消息的用户的唯一标识符。
  • reciever_ID → 接收消息的用户的唯一标识符。
  • type → 表示发送方发送的是媒体文件还是文档(默认消息类型为文本)。
  • text → 包含必须作为消息发送的文本。
  • media_object → 根据类型参数定义。它代表要发送的媒体文件。
  • 文档 → 要发送的文档文件。

2. 获取消息

通过该API调用,用户可以在离线后上线时获取所有未读消息。

getMessage(user_Id)
  • user_id → 代表必须获取所有未读消息的用户的唯一标识符。

3.上传媒体或文档文件

我们可以通过 uploadFile() API 上传媒体文件,方法是执行 POSTAPI 端点发出请求。成功的响应将返回一个转发给接收者的 ID。假设可上传媒体的最大文件大小为 16 MB,而文档的限制为 100 MB。/v1/media 向

uploadFile(file_type,文件)
  • file_type → 通过 API 调用上传的文件类型。
  • file → 包含通过 API 调用上传的文件。

4. 下载文档或媒体文件

downloadFile(user_id, file_id)
  • user_id → 将下载文件的用户的唯一标识符。
  • file_id → 文件的唯一标识符。它是在通过uploadFile()API 调用上传文件时生成的。 downloadFile()API 调用通过此标识符下载媒体文件。客户端可以通过向服务器提供文件名来找到file_id

详细设计

WebSocket服务器

一个 WebSocket 服务器肯定不足以处理数十亿台设备,因此应该有足够的服务器来处理这个问题。这些服务负责为每个在线用户提供一个端口。因此,我们需要一个 WebSocket 管理器,它基本上位于数据存储集群 (Redis) 之上。

服务发现

此外,我们还需要根据客户的地理位置、服务器容量等为客户推荐最好的聊天服务器。Apache Zookeeper 是一种流行的开源服务器。它注册所有可用的聊天服务器,并根据预定义的标准为客户端选择最佳的聊天服务器。

1. 用户A尝试登录应用程序。

2. 负载均衡器向API服务器发送登录请求。

3. 后端对用户进行身份验证后,服务发现为用户 A 找到最佳的聊天服务器(服务器 2),并将服务器信息返回给用户 A。

4. 用户A通过WebSocket连接到聊天服务器2。

发送或接收消息

WebSocket 服务器还需要与另一个服务通信,即消息服务。消息服务基本上是数据库集群顶部的消息存储库。它充当与数据库交互的其他服务的数据库接口。它从数据库中存储和检索消息,并在特定时间(我们可以设置)后删除它们。并且,它公开 API 以通过各种过滤器接收消息,例如用户 ID、消息 ID 等。

现在,如果用户 A 想要向用户 B 发送消息。由于我们有多个 WebSocket 服务器,这些用户可以连接到不同的服务器。那么这是如何工作的:

  1. 用户A与其所连接的相应WebSocket服务器进行通信。
  2. 与用户 A 关联的 WebSocket 服务器识别用户 B 通过 WebSocket 管理器连接到的 WebSocket。如果用户 B 在线,WebSocket 管理器会向用户 A 的 WebSocket 服务器响应用户 B 已与其 WebSocket 服务器连接。
  3. 同时,WebSocket服务器将消息发送到消息服务并存储在数据库中(以防用户B离线)。因此,要处理的消息的驱逐策略将是先进先出(在这种情况下这非常有意义)。当消息传递到接收者时,它们将从数据库中删除。
  4. 现在,用户A的WebSocket服务器已经有了用户B与自己的WebSocket服务器连接的信息。两个用户都与 WebSocket 管理器通信以查找彼此的 WebSocket 服务器。
  5. 如果用户B离线,消息将保留在数据库中。每当他们上线时,所有发送给用户 B 的消息都会通过推送通知传递。否则,这些消息将在 30 天后永久删除。

如果两个用户之间存在连续对话,则会对 WebSocket 管理器进行多次调用。为了最大限度地减少延迟并减少这些调用的数量,我们可以向每个 WebSocket 服务器添加一个缓存,如下所示:

  • 如果两个用户都连接到同一服务器,则可以避免对 WebSocket 管理器的调用。
  • 它还可以缓存有关哪个用户连接到哪个 WebSocket 服务器的最近对话的信息。

我们还应该考虑过期策略——WebSocket 服务器应该缓存信息多长时间?如果用户断开连接并连接到另一台服务器,缓存中的数据将变得过时。

在这种情况下,信息将在 WebSocket 管理器中更新,而 WebSocket 管理器将验证 WebSocket 服务器使用的缓存中的数据,并将更新的数据发送到相应的缓存。因此,缓存中的信息将保留在那里,直到收到来自 WebSocket 管理器的无效信号。

支持群组消息

WebSocket 服务器不跟踪组,因为它们只跟踪活动用户。但在群组中,一些用户可能在线,而另一些用户可能离线。我们需要考虑组消息的其他组件,负责将消息传递给组中的每个用户:

  • 群组消息处理程序
  • 群消息服务
  • 消息队列*Kafka

*Kafka 是一个开源分布式事件流平台,用于构建实时数据管道和流应用程序。它旨在实时处理大量数据。它提供了一个发布-订阅消息系统,允许应用程序以容错、可扩展且可靠的方式发送和接收消息。 Kafka 基于分布式架构,由多个节点或代理组成,这些节点或代理协同工作形成一个集群。生产者可以向 Kafka 主题发送消息,消费者可以从这些主题中读取消息。 Kafka还支持流处理,可以实时处理数据流。 Kafka在业界被广泛用于构建实时数据管道和流式应用程序,它已成为大数据生态系统中必不可少的工具。

用户 A 希望向具有某个唯一 ID 的组发送消息 - 例如 Group/A:

  1. 由于用户 A 连接到 WebSocket 服务器,因此它向 Group/A 的消息服务发送消息。
  2. 群组消息处理程序与群组服务通信以检索群组/A用户的数据。
  3. 消息服务将消息连同有关该组的其他特定信息发送到 Kafka。该消息保存在那里以供进一步处理。在 Kafka 术语中,一个组可以是一个主题,发送者和接收者可以分别是生产者和消费者。
  4. 现在,群组服务将每个群组中用户的所有信息保存在系统中。它拥有每个组的所有信息,包括用户 ID、组 ID、状态、组图标、用户数量等。该服务驻留在 MySQL 数据库集群之上,具有按地理位置分布的多个辅助副本。 Redis 缓存服务器还用于缓存来自 MySQL 服务器的数据。地理分布的副本和 Redis 缓存都有助于减少延迟。
  5. 最后,组消息处理程序遵循与 WebSocket 服务器相同的流程,并将消息传递给每个用户。

为什么在我们的聊天应用程序中使用 SQL 数据库:

  • 结构化数据:聊天服务需要结构化数据来管理用户对话、用户个人资料和消息历史记录。 SQL 数据库提供了一种结构化的数据组织方法,使其易于查询和管理。
  • 一致性:我们的服务还要求数据存储和检索的一致性,以确保消息可靠且一致地传递。即使多个用户同时访问,SQL 数据库也能保证数据的一致性。
  • 可扩展性:随着用户和消息数量的不断增加,轻松扩展的能力非常重要。 SQL 数据库提供了跨多个服务器和节点 *分片数据的能力,从而更容易水平扩展。< /span>
  • 可靠性:我们的聊天应用需要可靠且 24/7 可用。

* 分片(水平扩展) 是添加更多服务器的做法。分片将大型数据库分成更小、更容易管理的部分,称为分片。每个分片共享相同的架构,但每个分片上的实际数据对于该分片来说是唯一的。用户数据根据用户ID分配到数据库服务器。每当您访问数据时,都会使用哈希函数来查找相应的分片。在我们的示例中,使用 user_id % 4 作为哈希函数。如果结果等于 0,则使用分片 0 来存储和获取数据。如果结果等于 1,则使用分片 1。同样的逻辑也适用于其他分片。

用户服务

媒体文件

通常,WebSocket 服务器是轻量级的,不支持繁重的逻辑,例如处理媒体文件的发送和接收。所以我们需要添加另一个服务——资产服务,它将负责发送和接收媒体文件。压缩和加密的文件将被发送到资产服务以将文件存储在 blob 存储上。如果资产服务收到对某些特定内容的大量请求,则内容会加载到 CDN 上。

对于发送媒体文件:

  1. 首先应在设备端对其进行压缩和加密。
  2. 压缩和加密的文件随后会发送到资产服务,以将文件存储在*blob 存储上。资产服务分配一个与发件人关联的 ID。资产服务还可以为每个文件提供一个哈希值,以避免 Blob 存储上的内容重复。例如,如果用户想要上传 Blob 存储中已有的图像,则不会上传该图像。相反,相同的 ID 会转发给接收者。
  3. 资产服务通过消息服务将媒体文件的ID发送给接收者。接收方使用 ID 从 Blob 存储下载媒体文件。
  4. 如果资产服务收到对某些特定内容的大量请求,则内容会加载到 CDN 上。

* Blob 存储 是非结构化数据的存储解决方案。我们可以在那里存储照片、音频、视频或其他多媒体项目。每种类型的数据都存储为 blob。它遵循平面数据组织模式,其中没有目录、子目录等。它由具有称为一次写入多次读取 (WORM) 的特定业务需求的应用程序使用,该需求规定数据只能写入一次,并且任何人都无法更改它。 Blob 存储被 YouTube、Netflix、Facebook 等广泛使用。

最终设计

非功能性需求

我们对此设计的非功能性要求是低延迟、一致性、可用性和安全性。让我们想想如何在我们的系统中实现这些要求:

低延迟:我们可以在各个级别最大限度地减少系统的延迟:

  • 我们可以通过地理上分布的 WebSocket 服务器以及与其关联的缓存来做到这一点。
  • 我们可以在 MySQL 数据库集群之上使用 Redis 缓存集群。
  • 我们可以使用 CDN 来频繁共享文档和媒体内容。

一致性: 系统还借助严格排序的 FIFO 消息队列,提供消息的高度一致性。但是,消息的排序需要 Sequencer(分配唯一序列号或时间戳的组件或算法)为每条消息提供 ID。此 ID 号可帮助系统识别消息发送的顺序,即使消息到达时顺序不正确。对于离线用户,Mnesia 数据库将消息存储在队列中。用户上线后,消息将按顺序发送。

可用性:如果我们有足够的 WebSocket 服务器并跨多个服务器复制数据,系统就可以实现高可用性。当用户由于 WebSocket 服务器中的某些故障而断开连接时,会话将通过负载均衡器与不同的服务器重新创建。此外,消息按照主从复制模型存储在Datastore集群(Mnesia常用于消息系统)上,提供高可用性和持久性。

安全性:系统还提供端到端加密机制,确保用户之间的聊天安全。

可扩展性:由于高性能工程(意味着如果我们的系统是使用高性能工程原理设计和开发的),可扩展性可能不是一个重大问题。然而,我们提出的系统是灵活的,因为随着负载的增加或减少,可以添加或删除更多服务器。

权衡

一致性和可用性之间的权衡

根据*CAP定理,系统可以在网络分区时提供其中之一或另一个。显然,在我们的系统中,消息的正确排序至关重要。否则,用户之间交流的信息的上下文可能会发生显着变化。因此,我认为如果发生网络分区,我们系统的可用性可能会受到影响。

* CAP 定理(也称为 Brewer 定理)指出,分布式数据库系统只能保证这三个特征中的两个:一致性、可用性和分区容错性。

延迟和安全性之间的权衡

低延迟是系统设计中为用户提供实时体验的重要因素。然而,另一方面,如果不加密,通过我们的聊天应用程序共享信息或数据可能会不安全。缺乏适当的安全机制会使数据容易受到未经授权的访问。因此,我们可以接受优先考虑消息安全传输和低延迟的权衡。例如,在涉及多媒体的通信的情况下,在发送方设备上近乎实时地加密它们并在接收方上解密它们可能会给设备带来负担,从而导致延迟。

资源估算

我们需要估计存储容量、带宽和服务器数量来支持如此大量的用户和消息。

存储估算

例如,WatsUp 每天共享的消息超过 1000 亿条,我们根据这个数字来估算存储容量。假设每条消息平均占用 100 字节,我们的服务器只会将消息保留 30 天。因此,如果用户在这些天内没有连接到服务器,这些消息将从服务器中永久删除。

1000 亿/天 * 100 字节 = 10 TB/天

30 天的存储容量约为:

30 * 10 TB/天 = 300 TB/月

除了聊天消息之外,我们还有媒体文件,每条消息占用超过 100 字节。我们还必须存储用户的信息和消息的元数据——例如时间戳、ID 等。在此过程中,我们还需要加密和解密来实现安全通信(因此我们需要存储加密密钥和相关元数据)。因此,准确地说,我们每月需要超过 300 TB,但为了简单起见,我们还是坚持每月 300 TB 这个数字。

带宽估计

由于我们的服务每天将获取 10TB 数据,因此我们需要将其除以 86400(一天中的秒数),这将为我们提供传入带宽< /span>926 Mb/s。

10 TB / 86400 ≈   116 MBps

为了简单起见,我们暂时忽略图像、视频、文档等媒体内容。否则,我们将把整个采访都花在这个上面。

我们还需要相同数量的传出带宽,因为来自发送方的相同消息需要传递到接收方:< /span>

总带宽: 116 * 2 = MBps232 

服务器数量估计

让我们开始估计服务器数量。假设我们的系统在单个服务器(例如 WhatsApp)上处理大约 1000 万个连接,每天的总连接数为 20 亿:

N  个服务器= 个连接总数 个连接每个 服务器 N /天每  
2十亿 / 10百万 = 200 a> 服务器
  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值