Skip to content

十二、设计聊天系统

在这一章中,我们将探索一个聊天系统的设计。几乎每个人都使用聊天应用。图 12-1 显示了市场上一些最流行的应用程序。

A picture containing flower  Description automatically generated

聊天应用为不同的人执行不同的功能。明确具体的要求是非常重要的。例如,当面试官想到的是一对一的聊天时,你不希望设计一个专注于群聊的系统。重要的是探究特征需求 。

步骤 1 -了解问题并确定设计范围

就聊天应用的设计类型达成一致至关重要。在市场上,有像 Facebook Messenger、微信和 WhatsApp 这样的一对一聊天应用程序,像 Slack 这样专注于群聊的办公聊天应用程序,或者像 Discord 这样专注于大型群体互动和低语音聊天延迟的游戏聊天应用程序。

当面试官让你设计一个聊天系统时,第一组澄清性问题应该明确她到底在想什么。至少,弄清楚你应该专注于一对一聊天还是群聊应用。你可能会问的一些问题如下:

候选人 :我们要设计什么样的聊天应用?1 对 1 还是基于小组?

面试官 :应该支持 1 对 1 和群聊。

考生 :这是手机 app 吗?还是一个 web app?还是两者都有?

面试官 :都有。

候选人 :这个 app 的规模有多大?创业 app 还是海量规模?

面试官 :应该支持 5000 万日活跃用户(DAU)。

候选人 :群聊,群成员限制是多少?

面试官 :最多 100 人

候选人 :聊天 app 有哪些重要的功能?它能支持附件吗?

面试官 : 1 对 1 聊天,群聊,在线指标。系统只支持短信。

候选人 :邮件大小有限制吗?

面试官 :是的,文字长度要小于 10 万字。

候选人 :需要端到端加密吗?

采访者 :现在不需要,但是如果时间允许的话,我们会讨论的。

候选人 :聊天记录要保存多久?

面试官 :永远。

在本章中,我们将重点设计一个像脸书信使这样的聊天应用,重点是以下功能:

低交付延迟的一对一聊天

小型群聊(最多 100 人)

在线状态

多设备支持。同一个账号可以同时登录多个账号。

T2】推送通知

就设计规模达成一致也很重要。我们将设计一个支持 5000 万 DAU 的系统。

第二步——提出高水平的设计并获得认同

为了开发高质量的设计,我们应该对客户机和服务器如何通信有一个基本的了解。在聊天系统中,客户端可以是移动应用程序,也可以是 web 应用程序。客户端之间不直接通信。相反,每个客户端连接到一个聊天服务,它支持上面提到的所有功能。让我们把注意力集中在基本操作上。聊天服务必须支持以下功能:

接收其他客户端的消息。

为每条消息找到合适的收件人,并将消息转发给收件人。

如果收件人不在线,将该收件人的邮件保存在服务器上,直到她在线。

图 12-2 显示了客户端(发送者和接收者)和聊天服务之间的关系。

A screenshot of a cell phone  Description automatically generated

当客户端想要开始聊天时,它使用一个或多个网络协议连接聊天服务。对于聊天服务来说,网络协议的选择很重要。让我们和面试官讨论一下这个问题。

对于大多数客户机/服务器应用程序,请求是由客户机发起的。对于聊天应用程序的发送方来说也是如此。在图 12-2 中,当发送者通过聊天服务向接收者发送消息时,它使用了久经考验的 HTTP 协议,这是最常见的 web 协议。在这个场景中,客户端打开一个与聊天服务的 HTTP 连接并发送消息,通知服务将消息发送给接收者。为此,保持活动是有效的,因为保持活动头部允许客户端保持与聊天服务的持久连接。它还减少了 TCP 握手的次数。HTTP 是发送方的一个很好的选择,许多流行的聊天应用程序如脸书[1]最初使用 HTTP 发送消息。

然而,接收端要复杂一些。因为 HTTP 是客户端发起的,所以从服务器发送消息并不简单。多年来,许多技术被用来模拟服务器发起的连接:轮询、长轮询和 WebSocket。这些都是在系统设计面试中广泛使用的重要技术,所以让我们来检查一下它们。

投票

如图 12-3 所示,轮询是一种客户端定期询问服务器是否有消息可用的技术。根据轮询频率的不同,轮询的成本可能会很高。回答一个大多数情况下答案为“否”的问题可能会消耗宝贵的服务器资源。

A screenshot of a cell phone  Description automatically generated

长轮询

因为轮询可能是低效的,下一步是长轮询(图 12-4)。

A screenshot of a cell phone  Description automatically generated

在长轮询中,客户端保持连接打开,直到有新消息可用或达到超时阈值。一旦客户端收到新消息,它会立即向服务器发送另一个请求,重新开始这个过程。长轮询有一些缺点:

发送者和接收者不能连接到同一个聊天服务器。基于 HTTP 的服务器通常是无状态的。如果使用循环法进行负载平衡,接收消息的服务器可能不会与接收消息的客户端建立长轮询连接。

服务器无法判断客户端是否断开。

效率低下。如果用户聊天不多,长轮询仍然会在超时后进行定期连接。

WebSocket

WebSocket 是从服务器向客户端发送异步更新的最常见的解决方案。图 12-5 显示了它是如何工作的。

A close up of text on a white background  Description automatically generated

WebSocket 连接由客户端发起。它是双向的和持久的。它最初是一个 HTTP 连接,可以通过一些定义明确的握手“升级”为 WebSocket 连接。通过这种持久连接,服务器可以向客户机发送更新。即使有防火墙,WebSocket 连接通常也能工作。这是因为它们使用 HTTP/HTTPS 连接也使用的端口 80 或 443。

前面我们说过,在发送端,HTTP 是一个很好的协议,但是由于 WebSocket 是双向的,没有很强的技术理由不把它也用于接收。图 12-6 显示了 WebSockets (ws)是如何用于发送方和接收方的。

A close up of a map  Description automatically generated

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

高层设计

刚才我们提到,WebSocket 被选为客户端和服务器之间双向通信的主要通信协议,需要注意的是,其他一切都不一定是 WebSocket。事实上,聊天应用程序的大多数功能(注册、登录、用户资料等)都可以使用传统的 HTTP 请求/响应方法。让我们深入了解一下系统的高级组件。

如图 12-7 所示,聊天系统分为三大类:无状态服务、有状态服务和第三方集成。

A close up of a map  Description automatically generated

无状态服务

无状态服务是传统的面向公众的请求/响应服务,用于管理登录、注册、用户档案等。这些是许多网站和应用程序的共同特征。

无状态服务 位于负载均衡器之后,其工作是根据请求路径将请求路由到正确的服务。这些服务可以是整体的或单独的微服务。我们不需要自己构建许多这样的无状态服务,因为市场上有可以轻松集成的服务。我们将深入讨论的一项服务是服务发现。它的主要工作是给客户端一个聊天服务器的 DNS 主机名列表,客户端可以连接到这些服务器。

有状态服务

唯一有状态的服务是聊天服务。该服务是有状态的,因为每个客户端都保持与聊天服务器的持久网络连接。在这种服务中,只要服务器仍然可用,客户通常不会切换到另一个聊天服务器。服务发现与聊天服务密切配合,以避免服务器过载。我们将深入探讨细节。

第三方整合

对于一个聊天 app 来说,推送通知是最重要的第三方整合。 恰当的整合推送通知至关重要。更多信息请参考第 10 章设计通知系统。

可扩展性

在小范围内,上面列出的所有服务都可以放在一台服务器上。即使在我们设计的规模下,理论上也有可能在一个现代云服务器中容纳所有用户连接。服务器可以处理的并发连接数很可能是限制因素。在我们的场景中,在 100 万并发用户的情况下,假设每个用户连接都需要服务器上的 10K 内存(这是一个非常粗略的数字,并且非常依赖于语言选择),它只需要大约 10GB 的内存来容纳一个机器上的所有连接。

如果我们提出一个所有东西都放在一台服务器上的设计,这可能会在面试官的脑海中升起一面大红旗。没有哪个技术专家会在一台服务器上设计出如此大的规模。单服务器设计是一个交易破坏者,原因有很多。其中单点故障是最大的。

然而,从单一服务器设计开始也是非常好的。确保面试官知道这是一个起点。把我们提到的所有东西放在一起,图 12-8 显示了调整后的高层设计。

A close up of a map  Description automatically generated

在图 12-8 中,客户端维护一个持久的 WebSocket 连接到一个聊天服务器,用于实时消息传递。

聊天服务器方便消息发送/接收。

呈现服务器管理在线/离线状态。

API 服务器处理一切事务,包括用户登录、注册、更改个人资料等。

通知服务器发送推送通知。

最后,键值存储用于存储聊天历史。当离线用户在线时,她将看到她以前的所有聊天记录。

存储

现在,我们已经准备好了服务器,启动了服务,完成了第三方集成。技术堆栈的最底层是数据层。数据层通常需要一些努力来使它正确。我们必须做出的一个重要决定是决定使用哪种类型的数据库:关系数据库还是 NoSQL 数据库?为了做出明智的决定,我们将检查数据类型和读/写模式。

典型的聊天系统中存在两种类型的数据。第一种是通用数据,如用户资料、设置、用户好友列表。这些数据存储在健壮可靠的关系数据库中。复制和分片是满足可用性和可伸缩性需求的常用技术。

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

聊天系统的数据量巨大。之前的一项研究[2]显示,脸书信使和 Whatsapp 每天处理 600 亿条信息。

只经常访问最近的聊天记录。用户通常不会查找旧聊天。

虽然在大多数情况下会查看最近的聊天记录,但用户可能会使用需要随机访问数据的功能,如搜索、查看您的提及、跳转到特定消息等。这些情况应该得到数据访问层的支持。

一对一聊天应用的读写比例约为 1:1。

选择支持我们所有使用情形的正确存储系统至关重要。我们推荐键值存储的原因如下:

键值存储允许简单的横向扩展。

键值存储提供非常低的数据访问延迟。

关系数据库不能很好地处理数据的长尾效应。当索引变大时,随机访问的代价很高。

键值存储被其他经过验证的可靠聊天应用所采用。例如,脸书信使和不和谐都使用键值存储。脸书信使用 HBase [4],不和用卡珊德拉[5]。

数据模型

刚才,我们谈到了使用键值存储作为我们的存储层。最重要的数据是消息数据。让我们仔细看看。

一对一聊天的消息表

图 12-9 显示了一对一聊天的消息表。主键是 message_id ,帮助决定消息顺序。我们不能依靠created _ at来决定消息顺序,因为可以同时创建两条消息。

A screenshot of a cell phone  Description automatically generated

群聊消息表

图 12-10 显示了群聊的消息表。复合主键是 (channel_id,message_id)。 渠道和集团在这里代表同一个意思。channel _ Id是分区键,因为群聊中的所有查询都在一个频道中操作。

A screenshot of a cell phone  Description automatically generated

消息 ID

如何生成 消息 _id 是一个值得探讨的有趣话题。 Message_id 承载着保证消息顺序的责任。为了确定消息的顺序, message_id 必须满足以下两个要求:

id 必须唯一。

id 应该可以按时间排序,这意味着新行比旧行具有更高的 id。

我们如何实现这两个保证?首先想到的是 MySql 中的“ auto_increment ”关键字。然而,NoSQL 数据库通常不提供这样的功能。

第二种方法是使用一个全局 64 位序列号生成器,如雪花[6]。这将在“第 7 章:在分布式系统中设计唯一的 ID 生成器”中讨论。

最后一种方法是使用本地序列号生成器。本地意味着 id 在一个组中是唯一的。本地 IDs 工作的原因是在一对一通道或组通道内维护消息序列就足够了。与全局 ID 实现相比,这种方法更容易实现。

步骤 3 -设计深度潜水

在系统设计面试中,通常你会深入了解概要设计中的一些组件。对于聊天系统,服务发现、消息传递流和在线/离线指标值得深入研究。

服务发现

服务发现的主要作用是根据地理位置、服务器容量等标准为客户推荐最佳的聊天服务器。Apache Zookeeper [7]是一个流行的服务发现开源解决方案。它注册所有可用的聊天服务器,并根据预定义的标准为客户选择最佳的聊天服务器。

图 12-11 显示了服务发现(Zookeeper)是如何工作的。

A close up of a map  Description automatically generated

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

2。负载平衡器将登录请求发送到 API 服务器。

3。在后端对用户进行身份验证后,服务发现会为用户 a 找到最佳的聊天服务器。在本例中,选择了服务器 2,并将服务器信息返回给用户 a。

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

消息流

理解一个聊天系统的端到端流程是很有趣的。在本节中,我们将探讨一对一聊天流程、跨多个设备的消息同步以及群聊流程。

一对一聊天流程

图 12-12 解释了当用户 A 给用户 b 发送消息时会发生什么。

A close up of text on a white background  Description automatically generated

1。用户 A 向聊天服务器 1 发送聊天消息。

2。聊天服务器 1 从 ID 生成器获得消息 ID。

3。聊天服务器 1 将消息发送到消息同步队列。

4。消息存储在键值存储中。

5.a .如果用户 B 在线,则消息被转发到用户 B 所连接的聊天服务器 2。

5.b .如果用户 B 离线,则从推送通知(PN)服务器发送推送通知。

6。聊天服务器 2 将消息转发给用户 B。在用户 B 和聊天服务器 2 之间存在持久的 WebSocket 连接。

跨多个设备的消息同步

许多用户拥有多台设备。我们将解释如何在多个设备之间同步消息。图 12-13 显示了一个消息同步的例子。

A screenshot of a cell phone  Description automatically generated

在图 12-13 中,用户 A 有两台设备:一台电话和一台笔记本电脑。当用户 A 用她的电话登录聊天应用时,它与聊天服务器 1 建立 WebSocket 连接。类似地,在膝上型电脑和聊天服务器 1 之间存在连接。

每个设备维护一个名为cur _ max _ message _ id的变量,该变量跟踪设备上的最新消息 ID。满足以下两个条件的消息被认为是新闻消息:

收件人 ID 等于当前登录的用户 ID。

键值存储中的消息 ID 大于cur _ max _ Message _ Id。

由于每个设备上都有不同的cur _ max _ message _ id,因此消息同步很容易,因为每个设备都可以从 KV store 获得新消息。

小组聊天流程

与一对一聊天相比,群聊的逻辑更加复杂。图 12-14 和 12-15 解释了这个流程。

A screenshot of a cell phone  Description automatically generated

图 12-14 解释了当用户 A 在群聊中发送消息时会发生什么。假设组中有 3 个成员(用户 A、用户 B 和用户 C)。首先,来自用户 A 的邮件被复制到每个组成员的邮件同步队列中:一个是用户 B 的,另一个是用户 c 的。您可以将邮件同步队列视为收件人的收件箱。这种设计选择对小组聊天很有好处,因为:

它简化了消息同步流程,因为每个客户端只需查看自己的收件箱即可获得新消息。

当群组数量较少时,在每个收件人的收件箱中存储一份副本并不太昂贵。

微信使用了类似的方法,它将一个群的成员限制在 500 人[8]。但是,对于有很多用户的组,为每个成员存储一个消息副本是不可接受的。

在接收方,一个接收方可以接收来自多个用户的消息。每个收件人都有一个收件箱(邮件同步队列),其中包含来自不同发件人的邮件。图 12-15 说明了这种设计。

A screenshot of a cell phone  Description automatically generated

在线存在感

在线状态指示器是许多聊天应用程序的基本功能。通常,您可以在用户的个人资料图片或用户名旁边看到一个绿点。本节解释了幕后发生的事情。

在高级设计中,在线状态服务器负责管理在线状态,并通过 WebSocket 与客户端通信。有几个流会触发在线状态更改。让我们逐一检查一下。

用户登录

用户登录流程在“服务发现”一节中解释。客户端与实时服务建立 WebSocket 连接后,用户 A 的在线状态和last _ active _ At时间戳保存在 KV store 中。在线状态指示器显示用户登录后在线。

A screenshot of a cell phone  Description automatically generated

用户注销

当用户注销时,会经历如图 12-17 所示的用户注销流程。KV 商店中的在线状态更改为离线。在线状态指示器显示用户离线。

A picture containing screenshot, drawing  Description automatically generated

用户断开

我们都希望我们的互联网连接稳定可靠。然而,情况并非总是如此;因此,我们必须在设计中解决这个问题。当用户断开与互联网的连接时,客户端和服务器之间的持久连接就会丢失。处理用户断开连接的一个简单方法是将用户标记为脱机,并在连接重新建立时将状态更改为联机。然而,这种方法有一个重大缺陷。用户在短时间内频繁断开和重新连接互联网是很常见的。例如,当用户通过隧道时,网络连接可以打开和关闭。在每次断开/重新连接时更新在线状态会使在线指示器改变得太频繁,从而导致较差的用户体验。

我们引入了心跳机制来解决这个问题。在线客户端定期向在线服务器发送心跳事件。如果存在服务器在特定时间内(比如说 x 秒)从客户端接收到心跳事件,则认为用户在线。否则,它是脱机的。

在图 12-18 中,客户端每 5 秒向服务器发送一次心跳事件。发送 3 个心跳事件后,客户端断开连接,不重新连接,i n x = 30 秒(这个数字是任意选择的,用来演示逻辑)。联机状态更改为脱机。

A screenshot of a cell phone  Description automatically generated

在线状态扇出

用户 A 的好友是如何知道状态变化的?图 12-19 解释了它是如何工作的。存在服务器使用发布-订阅模型,其中每个朋友对维护一个通道。当用户 A 的在线状态改变时,它将事件发布到三个通道,通道 A-B、A-C 和 A-D。这三个通道分别由用户 B、C 和 D 订阅。因此,朋友很容易获得在线状态更新。客户端和服务器之间的通信是通过实时 WebSocket 进行的。

A close up of a device  Description automatically generated

以上设计对小用户群有效。例如,微信使用类似的方法,因为其用户群上限为 500 人。对于较大的群组,通知所有成员在线状态是昂贵且耗时的。假设一个团体有 100,000 名成员。每个状态变化将产生 100,000 个事件。为了解决性能瓶颈,一个可能的解决方案是仅当用户进入一个组或手动刷新好友列表时获取在线状态。

步骤 4 -总结

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

如果你在采访结束时有额外的时间,以下是额外的谈话要点:

扩展聊天 app,支持照片、视频等媒体文件。媒体文件的大小远远大于文本。压缩、云存储和缩略图是有趣的话题。

端到端加密。Whatsapp 支持消息的端到端加密。只有发件人和收件人可以阅读邮件。感兴趣的读者可以参考参考资料[9]中的文章。

在客户端缓存消息可以有效减少客户端和服务器之间的数据传输。

提高加载时间。Slack 建立了一个地理上分散的网络来缓存用户的数据、频道等。为了更好的加载时间[10]。

错误处理。

聊天服务器出错。可能有几十万个,甚至更多的持久连接到聊天服务器。如果聊天服务器离线,service discovery (Zookeeper)将为客户端提供一个新的聊天服务器来建立新的连接。

消息重发机制。重试和排队是重新发送消息的常用技术。

祝贺你走到这一步!现在给自己一个鼓励。干得好!

参考资料

【1】二郎在脸书:T3】https://www . Erlang-factory . com/upload/presentations/31/EugeneLetuchy-erlangatfacebook . pdfT5】

【2】Messenger 和 WhatsApp 每天处理 600 亿条消息:https://www . the erge . com/2016/4/12/11415198/Facebook-Messenger-WhatsApp-number-messages-vs-SMS-F8-2016

【3】长尾:

【4】消息的底层技术:https://www . Facebook . com/notes/Facebook-engineering/The-Underlying-Technology-of-Messages/454991608919/

【5】Discord 如何存储数十亿条消息:https://blog . discordapp . com/How-Discord-Stores-billion-of-Messages-7 fa 6 EC 7 ee 4c 7

【6】宣布雪花:https://blog . Twitter . com/engineering/en _ us/a/2010/Announcing-snow flake . html

【7】阿帕奇动物园管理员:T3】https://zookeeper.apache.org/T5】

【8】从无到有:微信后台系统的演变(中文文章):https://www . infoq . cn/Article/the-road-of-the-growth-weixin-background

【9】端到端加密:【https://faq.whatsapp.com/en/android/28030015/】

【10】法兰绒:制作 Slack Scale 的应用级边缘缓存:https://Slack . engineering/法兰绒-An-Application-Level-Edge-Cache-to-Make-Slack-Scale-b8a 6400 e2f 6b


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组