Skip to content

Commit

Permalink
feat: chapter 12 & 13
Browse files Browse the repository at this point in the history
  • Loading branch information
honkinglin committed Oct 21, 2024
1 parent 85b7f12 commit 5f695d8
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

- [第十二章:设计一个聊天系统](/CHAPTER-12-DESIGN-A-CHAT-SYSTEM)

- 第十三章:设计一个搜索自动补全系统
- [第十三章:设计一个搜索自动补全系统](/CHAPTER-13-DESIGN-A-SEARCH-AUTOCOMPLETE-SYSTEM)

- 第十四章:设计 YouTube

Expand Down
146 changes: 139 additions & 7 deletions docs/CHAPTER-12-DESIGN-A-CHAT-SYSTEM.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 第十二章:设计一个聊天系统 (Design a Chat System)
# 第十二章:设计一个聊天系统 (Design A Chat System)

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

Expand Down Expand Up @@ -60,7 +60,7 @@

对于大多数客户端/服务器应用,请求是由客户端发起的。这对于聊天应用的发送方也是如此。在图12-2中,当发送者通过聊天服务向接收者发送消息时,它使用经过验证的 HTTP 协议,这是最常见的网络协议。在这种情况下,客户端与聊天服务建立 HTTP 连接并发送消息,通知服务将该消息发送给接收者。保持连接(keep-alive)在这里非常有效,因为保持连接头允许客户端与聊天服务维持持久连接。这还减少了 TCP 握手的次数。

HTTP 是发送方的一个不错的选择,许多流行的聊天应用程序(如 Facebook [1])最初也是使用 HTTP 来发送消息的。
HTTP 是发送方的一个不错的选择,许多流行的聊天应用程序(如 Facebook [[1]](https://www.erlangfactory.com/upload/presentations/31/EugeneLetuchy-ErlangatFacebook.pdf))最初也是使用 HTTP 来发送消息的。

然而,接收方的情况就复杂一些。由于 HTTP 是客户端发起的,因此从服务器发送消息并不是简单的事情。多年来,许多技术被用于模拟服务器发起的连接:**轮询****长轮询****WebSocket**。这些是系统设计面试中广泛使用的重要技术,我们来逐一审视它们。

Expand Down Expand Up @@ -94,7 +94,7 @@ WebSocket 连接由客户端发起。它是双向的并且是持久的。WebSock

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

## 高层设计
### 高层设计

刚才我们提到选择 WebSocket 作为客户端与服务器之间的主要通信协议,因其支持双向通信。但需要注意的是,并非所有内容都必须使用 WebSocket。实际上,聊天应用的大多数功能(如注册、登录、用户配置文件等)可以使用传统的 HTTP 请求/响应方法。让我们深入探讨一下系统的高层组件。

Expand Down Expand Up @@ -139,16 +139,16 @@ WebSocket 连接由客户端发起。它是双向的并且是持久的。WebSock
典型的聊天系统中存在两种数据类型。第一种是通用数据,例如用户资料、设置和用户好友列表。这些数据存储在强大而可靠的关系型数据库中。复制和分片是满足可用性和可扩展性要求的常见技术。

第二种数据是聊天系统特有的:聊天历史数据。理解读写模式非常重要。
- 聊天系统的数据量是巨大的。早期研究[2]显示,Facebook Messenger和WhatsApp每天处理600亿条消息。
- 聊天系统的数据量是巨大的。早期研究 [[2]](https://www.theverge.com/2016/4/12/11415198/facebook-messenger-whatsapp-numbermessages-vs-sms-f8-2016) 显示,Facebook Messenger和WhatsApp每天处理600亿条消息。
- 只有最近的聊天记录被频繁访问。用户通常不会查找旧聊天记录。
- 尽管在大多数情况下,用户会查看非常新的聊天历史,但他们可能会使用需要随机访问数据的功能,例如搜索、查看提及和跳转到特定消息等。这些情况应由数据访问层支持。
- 一对一聊天应用的读写比约为1:1。

选择正确的存储系统以支持我们所有的用例至关重要。我们推荐使用键值存储,原因如下:
- 键值存储允许轻松的横向扩展。
- 键值存储提供非常低延迟的数据访问。
- 关系型数据库无法很好地处理数据的长尾[3]。当索引变得庞大时,随机访问的成本很高。
- 键值存储被其他可靠的聊天应用广泛采用。例如,Facebook Messenger和Discord都使用键值存储。Facebook Messenger使用HBase[4],而Discord使用Cassandra[5]
- 关系型数据库无法很好地处理数据的长尾 [[3]](https://en.wikipedia.org/wiki/Long_tail)。当索引变得庞大时,随机访问的成本很高。
- 键值存储被其他可靠的聊天应用广泛采用。例如,Facebook Messenger和Discord都使用键值存储。Facebook Messenger使用HBase [[4]](https://www.facebook.com/notes/facebookengineering/the-underlying-technology-of-messages/454991608919/),而Discord使用Cassandra [[5]](https://blog.discordapp.com/how-discordstores-billions-of-messages-7fa6ec7ee4c7)

### 数据模型

Expand All @@ -174,7 +174,139 @@ WebSocket 连接由客户端发起。它是双向的并且是持久的。WebSock

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

第二种方法是使用像Snowflake [6]这样的全局64位序列号生成器。这将在“第七章:在分布式系统中设计唯一ID生成器”中讨论。
第二种方法是使用像Snowflake [[6]](https://blog.twitter.com/engineering/en_us/a/2010/announcingsnowflake.html) 这样的全局64位序列号生成器。这将在“第七章:在分布式系统中设计唯一ID生成器”中讨论。

最后一种方法是使用本地序列号生成器。本地意味着ID仅在一个组内是唯一的。本地ID之所以可行,是因为在一对一通道或组通道中维护消息顺序已足够。与全局ID实现相比,这种方法更容易实现。

## 第3步 - 设计深入探讨
在系统设计面试中,通常要求你深入探讨高层设计中的某些组件。对于聊天系统,服务发现、消息流以及在线/离线状态指示器是值得深入研究的部分。

### 服务发现 (Service discovery)

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

图12-11展示了服务发现(Zookeeper)如何工作:

![图12-11](/f12-11.png)

1. 用户A尝试登录应用。
2. 负载均衡器将登录请求发送到API服务器。
3. 后端认证用户后,服务发现为用户A找到最佳的聊天服务器。在此示例中,服务器2被选中,服务器信息返回给用户A。
4. 用户A通过WebSocket连接到聊天服务器2。

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

#### 一对一聊天流
图12-12解释了当用户A发送消息给用户B时发生的情况。

![图12-12](/f12-12.png)

1. 用户A将聊天消息发送到聊天服务器1。
2. 聊天服务器1从ID生成器获取消息ID。
3. 聊天服务器1将消息发送到消息同步队列。
4. 消息被存储在键值存储中。
5.a. 如果用户B在线,消息将转发到用户B连接的聊天服务器2。
5.b. 如果用户B离线,则推送通知服务器(PN服务器)发送推送通知。
6. 聊天服务器2通过WebSocket持久连接将消息转发给用户B。

#### 跨多设备的消息同步

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

![图12-13](/f12-13.png)

在图12-13中,用户A拥有两台设备:一部手机和一台笔记本电脑。当用户A使用手机登录聊天应用时,会与聊天服务器1建立WebSocket连接。同样,笔记本电脑也与聊天服务器1建立连接。

每个设备都会维护一个名为`cur_max_message_id`的变量,该变量用于跟踪该设备上最新的消息ID。满足以下两个条件的消息将被视为新消息:
- 收件人ID等于当前登录用户的ID。
- 键值存储中的消息ID大于设备的`cur_max_message_id`

由于每个设备都有不同的`cur_max_message_id`,消息同步变得简单,因为每个设备都可以从键值存储中获取新消息。

#### 小组群聊消息流

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

![图12-14](/f12-14.png)

图12-14解释了当用户A在群聊中发送消息时会发生什么。假设群组中有3个成员(用户A、用户B和用户C)。首先,用户A的消息会复制到每个群成员的消息同步队列中:一个给用户B,另一个给用户C。你可以将消息同步队列视为收件人的收件箱。这种设计对于小型群聊来说非常合适,因为:

- 它简化了消息同步流程,每个客户端只需检查自己的收件箱即可获取新消息。
- 当群组成员较少时,在每个收件人的收件箱中存储消息副本并不会占用太多资源。

微信采用了类似的方式,并将群组人数限制为500人 [[8]](https://www.infoq.cn/article/the-road-of-the-growth-weixin-background)。然而,对于用户众多的群组,为每个成员存储消息副本是不可接受的。

在接收方,每个接收者可能会从多个用户接收消息。每个接收者都有一个收件箱(消息同步队列),其中包含来自不同发送者的消息。图12-15展示了这种设计。

![图12-15](/f12-15.png)

### 在线状态

在线状态指示器是许多聊天应用中的核心功能。通常,你会在用户的头像或用户名旁边看到一个绿色的点。本节解释了背后的工作原理。

在高层设计中,**状态服务器**负责管理用户的在线状态,并通过WebSocket与客户端通信。有几个流程会触发在线状态的变化,我们来逐一分析这些流程。

#### 用户登录

用户登录流程已在“服务发现”部分解释。当客户端与实时服务之间建立了WebSocket连接后,用户A的在线状态以及`last_active_at`(最后活动时间戳)会被存储在键值存储(KV store)中。登录后,在线状态指示器显示该用户为在线状态。

![图12-16](/f12-16.png)

#### 用户登出

当用户登出时,会执行如图12-17所示的登出流程。在线状态在键值存储中被更新为离线状态,状态指示器显示该用户为离线状态。

![图12-17](/f12-17.png)

#### 用户断开连接

我们都希望互联网连接始终稳定可靠,但现实中并非如此,因此我们必须在设计中解决这一问题。当用户断开互联网连接时,客户端与服务器之间的持久连接会丢失。一个简单的处理方法是将用户标记为离线,当连接重新建立时再将状态更改为在线。然而,这种方法有一个主要缺陷:用户在短时间内频繁断开和重新连接是很常见的。例如,当用户通过隧道时,网络连接可能会断开又恢复。如果每次断开/重新连接都更新在线状态,状态指示器将频繁变化,导致用户体验差。

为解决这个问题,我们引入了**心跳机制**。在线客户端会定期向状态服务器发送心跳事件。如果状态服务器在设定时间(例如x秒)内收到客户端的心跳事件,则该用户被视为在线。否则,用户被视为离线。

在图12-18中,客户端每5秒向服务器发送一次心跳事件。在发送了3次心跳事件后,客户端断开连接且在x = 30秒(这个数值仅用于展示逻辑)内未重新连接,在线状态将更改为离线。

![图12-18](/f12-18.png)

上述设计对于小型用户群体是有效的。例如,微信采用了类似的方式,因为其群组人数限制为500人。然而,对于更大的群组,通知所有成员在线状态的开销很大且耗时。假设一个群组有100,000名成员,每次状态更改都会生成100,000个事件。

为了解决这个性能瓶颈,一个可能的解决方案是仅在用户进入群组或手动刷新好友列表时获取在线状态。

## 第 4 步 - 总结

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

如果在面试结束时还有额外的时间,可以讨论以下扩展点:

- **扩展聊天应用以支持媒体文件**:例如照片和视频。媒体文件的大小远大于文本,压缩、云存储和缩略图是可以深入探讨的有趣话题。
- **端到端加密**:WhatsApp支持消息的端到端加密,只有发送者和接收者可以读取消息。感兴趣的读者可以参考参考资料中的文章 [[9]](https://faq.whatsapp.com/en/android/28030015/)
- **客户端消息缓存**:缓存消息能够有效减少客户端与服务器之间的数据传输。
- **提升加载时间**:Slack建立了地理分布式网络,以缓存用户数据、频道等,提升加载时间 [[10]](https://slack.engineering/flannel-an-application-level-edge-cache-to-make-slack-scaleb8a6400e2f6b)
- **错误处理**
- **聊天服务器错误**:一个聊天服务器可能有成千上万甚至更多的持久连接。如果一个聊天服务器离线,服务发现(Zookeeper)会为客户端提供一个新的聊天服务器以重新建立连接。
- **消息重发机制**:重试和排队是常见的消息重发技术。

恭喜你走到这一步!给自己一个鼓励,做得很棒!

## 参考文献

[1] Erlang at Facebook: https://www.erlangfactory.com/upload/presentations/31/EugeneLetuchy-ErlangatFacebook.pdf

[2] Messenger and WhatsApp process 60 billion messages a day: https://www.theverge.com/2016/4/12/11415198/facebook-messenger-whatsapp-numbermessages-vs-sms-f8-2016

[3] Long tail: https://en.wikipedia.org/wiki/Long_tail

[4] The Underlying Technology of Messages: https://www.facebook.com/notes/facebookengineering/the-underlying-technology-of-messages/454991608919/

[5] How Discord Stores Billions of Messages: https://blog.discordapp.com/how-discordstores-billions-of-messages-7fa6ec7ee4c7

[6] Announcing Snowflake: https://blog.twitter.com/engineering/en_us/a/2010/announcingsnowflake.html

[7] Apache ZooKeeper: https://zookeeper.apache.org/

[8] From nothing: the evolution of WeChat background system (Article in Chinese): https://www.infoq.cn/article/the-road-of-the-growth-weixin-background

[9] End-to-end encryption: https://faq.whatsapp.com/en/android/28030015/

[10] Flannel: An Application-Level Edge Cache to Make Slack Scale: https://slack.engineering/flannel-an-application-level-edge-cache-to-make-slack-scaleb8a6400e2f6b
3 changes: 3 additions & 0 deletions docs/CHAPTER-13-DESIGN-A-SEARCH-AUTOCOMPLETE-SYSTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 第十三章:设计搜索自动完成系统 (Design A Search Autocomplete System)

在使用Google搜索或在亚马逊购物时,当你在搜索框中输入内容时,会出现一个或多个与搜索词匹配的结果。这项功能被称为自动完成、输入建议、实时搜索或增量搜索。图13-1展示了Google搜索的一个例子,当输入“dinner”时,搜索框显示了一系列自动完成的结果。搜索自动完成是许多产品中的重要功能,这引出了一个面试问题:设计一个搜索自动完成系统,也称为“设计前k个结果”或“设计前k个最常搜索的查询”。
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

- [第十二章:设计一个聊天系统](/CHAPTER-12-DESIGN-A-CHAT-SYSTEM)

- 第十三章:设计一个搜索自动补全系统
- [第十三章:设计一个搜索自动补全系统](/CHAPTER-13-DESIGN-A-SEARCH-AUTOCOMPLETE-SYSTEM)

- 第十四章:设计 YouTube

Expand Down

0 comments on commit 5f695d8

Please sign in to comment.