系统设计(五)设计Facebook Messenger

让我们设计一个像Facebook Messenger这样的即时通讯服务,用户可以通过网络和移动界面相互发送文本信息。

1.什么是Facebook Messenger?

Facebook Messenger是一个软件应用程序,它为用户提供基于文本的即时消息服务。Messenger用户可以通过手机和facebook的网站与他们的facebook好友聊天。

2.系统的要求和目标

我们的Messenger应该满足以下要求:

功能需求:
1.Messenger应支持用户之间的一对一对话。
2.Messenger应跟踪其用户的在线/离线状态。
3.Messenger应该支持聊天历史的持久存储。

非功能性需求:
1.用户应具有实时聊天体验,且延迟最小.
2.我们的系统应该是高度一致的;用户应该能够在所有用户的设备上看到相同的聊天历史
3.Messenger的高可用性是可取舍的;为了一致性,我们可以容忍低可用性。

额外要求:
·小组聊天:Messenger应该支持多个人在一个小组中相互交谈。
·推送通知:当用户离线时,新消息出现Messenger也能够通知用户。

3.容量估计和制约因素

假设我们每天有5亿活跃用户,平均每个用户每天发送40条消息,这就给了我们每天200亿条消息。

存储估计:
假设一条消息平均是100个字节,所以要将所有消息存储一天,我们需要2TB的存储。
200亿条消息*100字节=>2 TB/天
为了存储五年的聊天历史,我们需要3.6兆字节的存储空间。
2 TB*365天*5年~=3.6PB

除了聊天消息之外,我们还需要存储用户的信息、消息的元数据(ID、时间戳等)。更不用说,上面的计算没有考虑到数据的压缩和复制。

带宽估计:
如果我们的服务每天得到2TB的数据,那么每秒钟就会有25 MB的输入数据。
2 TB/86400秒~=25 MB/s
由于每次传入的消息都需要发送给另一个用户,所以我们在上传和下载时都需要相同数量的带宽(25 MB/s)。

每天的消息总数为 200亿条
每天的存储量 2 TB
5年间的存储量 3.6 PB
输入数据 25 MB/s
输出数据 25 MB/s

4.高层设计

在高级别,我们将需要一个聊天服务器,这将是中心部分,协调用户之间的所有通信。当用户想向另一个用户发送消息时,他们将连接聊天服务器并将消息发送到服务器;然后服务器将该消息传递给另一个用户并将其存储在数据库中。

详细的工作流如下所示:
1.用户-A通过聊天服务器向用户B发送消息。
2.服务器接收消息并向用户-A发送确认。
3.服务器将消息存储在数据库中,并将消息发送给用户-B。
4.用户-B接收消息并将确认发送到服务器。
5.服务器通知用户-A消息已成功传递给用户-B。
在这里插入图片描述

5.详细组件设计

让我们首先构建一个简单的解决方案,其中所有的东西都运行在一台服务器上。

在高层,我们的系统需要处理以下用例:
1.接收收到的信息并发送传出消息。
2.从数据库中存储和检索消息。
3.记录哪些用户已在线或已离线,并将在线/离线状态通知所有相关用户。

让我们逐一讨论这些场景:
A.信息处理
我们如何有效地发送/接收信息?
要发送消息,用户需要连接到服务器并为其他用户发布消息。要从服务器获取消息,用户有两个选项:
1.拉模型:用户可以定期询问服务器是否有新消息。
2.推模型:用户可以保持与服务器的连接,并且可以依赖于服务器。每当有新消息时通知他们。

如果我们使用第一种方法,那么服务器需要跟踪仍在等待发送的消息,并且一旦接收用户连接到服务器以请求任何新消息,服务器就可以返回所有挂起的消息。为了尽量减少用户的延迟,用户客户端必须频繁地检查服务器,如果没有挂起的消息,大多数情况下他们将得到一个空的响应。这会浪费大量的资源,看起来不是一个有效率的解决方案。
如果我们采用第二种方法,即所有活跃用户都保持与服务器的连接,那么一旦服务器收到消息,就可以立即将消息传递给预期的用户。这样,服务器就不需要跟踪挂起的消息,并且我们将有最小的延迟,因为消息是在打开的连接上立即传递的。

客户端将如何保持与服务器的连接?
我们可以使用HTTP长轮询或WebSocket。在长轮询中,客户端在知道服务器不会立即响应的情况下从服务器请求信息。在服务器接收到客户端的轮询时,如果服务器没有新数据为客户端提供,服务器不会发送空响应,而会将请求保持打开并等待响应给客户端的信息就绪。一旦有了新的信息,服务器就会立即向客户端发送响应,完成打开的请求。
在收到服务器响应后,客户端可以立即发出另一个服务器请求,以便将来进行更新。这给延迟、吞吐量和性能带来了很多改进。长轮询请求可以收到超时或从服务器断开连接的结果,如果这种情况发生了,客户端必须打开一个新请求。

服务器如何跟踪所有打开的连接,以便有效地将消息重定向到用户?
服务器可以维护哈希表,其中“key”将是UserId,“value”将成为连接对象。因此,每当服务器收到用户的消息时,它就会在哈希表中查找该用户,以查找连接对象,并在打开的请求中发送消息。

当服务器收到一条消息给脱机的用户时,会发生什么情况
如果接收方断开连接,服务器可以将发送失败通知发送方。如果这是一个临时断开,例如,接收者的长轮询请求刚刚超时,那么我们应该期望用户重新连接。在这种情况下,我们可以要求发送者重试发送消息。这种重试可以嵌入到客户端的逻辑中,这样用户就不必重新键入消息。服务器还可以将消息存储一段时间,并在接收者重新连接后重试发送消息。

我们需要多少聊天服务器?
让我们假定随时会有5亿个连接。假设一台现代服务器可以在任何时候处理50K并发连接,那么我们需要10K个这样的服务器。

我们如何知道哪个服务器持有到哪个用户的连接?
我们可以在聊天服务器前面引入一个负载均衡器;它可以将每个用户ID映射到一个服务器上,以重定向请求。

服务器应该如何处理“传递消息”请求?
服务器在接收到新消息时需要执行以下操作:
1)将消息存储在数据库中;
2)将消息发送给接收方;
3)向发送者发送确认。
聊天服务器将首先找到为接收方保存连接的服务器,并将消息传递给该服务器,将其发送给接收方。然后,聊天服务器可以向发送方发送确认;我们不需要等待将消息存储在数据库中(这可能发生在后台)。存储消息将在下一节中讨论。

Message如何维护消息的顺序?
我们可以在每条消息中存储一个时间戳,即服务器接收消息的时间。这仍然不能确保客户端消息的正确排序。
服务器时间戳无法确定消息的确切顺序的场景如下所示:
1.用户-1为用户-2向服务器发送消息M1。
2.服务器在时间T1处接收消息M1。
3.同时,用户-2为用户-1向服务器发送消息M2。
4.服务器在时间T2处接收消息M2,因此T2>T1。
5.服务器向用户-2发送消息M1,向用户-1发送消息M2。因此,用户-1将首先看到M1,然后看到M2,而用户-2将首先看到M2,然后看到M1。

为了解决这个问题,我们需要为每个客户端的每条消息保留一个序列号。这个序列号将决定每个用户消息的确切顺序。使用此解决方案,两个客户端将看到消息序列的不同视图,但在所有设备上,此视图对它们都是一致的。

B.从数据库存储和检索消息
每当聊天服务器收到新消息时,它都需要将其存储在数据库中。为此,我们有两个选择:
1.启动一个单独的线程,该线程将与数据库协同存储消息。
2.向数据库发送异步请求以存储消息。

在设计数据库时,我们必须记住一些事情:
1.如何有效地使用数据库连接池。
2.如何重试失败的请求。
3.在哪里日志记录那些即使经过一些重试也失败的请求。
4.当所有问题都解决了,如何重试这些在日志的请求(在重试后也失败的请求)。

我们应该使用哪种存储系统?
我们需要一个数据库,它可以支持非常高的小更新率,并且可以快速获取一系列记录。这是必需的,因为我们有大量的小消息需要插入到数据库中,并且在查询时,用户主要是对顺序访问的消息感兴趣。
我们不能使用像MySQL这样的RDBMS,或者像MongoDB这样的NoSQL,因为我们负担不起每次用户接收/发送消息时从数据库读取/写入一行的费用。这不仅会使我们的服务的基本操作以较高的延迟运行,而且还会给数据库造成巨大的负载,我们的两个需求都可以很容易地通过像HBASE这样的宽列数据库解决方案来满足。
HBASE是一个面向列的键值NoSQL数据库,它可以将针对一个键的多个值存储到多个列中。HBASE以Google的BigTable为模型,运行在Hadoop分布式文件系统(HDFS)之上。HBASE将数据组合在一起,以便在内存缓冲区中存储新数据,一旦缓冲区已满,它就会将数据转储到磁盘。这种存储方式不仅有助于快速存储大量的小数据,而且还可以通过键或行的扫描范围来获取行。HBASE也是存储可变大小数据的有效数据库,这也是我们的服务所需要的。

客户端应该如何有效地从服务器获取数据?
客户端在从服务器获取数据时应该分页。不同客户端的页面大小可能不同,例如,手机屏幕较小,因此我们需要在视口中减少消息/对话的数量。

C.管理用户状态
我们需要跟踪用户的在线/离线状态,并在发生状态更改时通知所有相关用户。因为我们在服务器上为所有活动的用户维护一个连接对象,因此我们可以很容易地从这一点了解用户的当前状态。任何时候都有5亿活跃用户,如果我们必须
将每个状态变化广播给所有相关的活动用户,就会消耗大量的资源。我们可以围绕这一点进行以下优化:

1.每当客户端启动应用程序时,它都可以提取其好友列表中所有其他用户的当前状态。
2.每当用户向已下线的其他用户发送消息时,我们都可以将失败发送到发送方并更新当前用户客户端上的其他用户下线状态。
3.每当用户上线时,服务器在广播该用户状态时,总是可以延迟几秒再广播来确定用户没有立即下线。
4.客户端可以从服务器上拉取正在显示的当前用户窗口的其他用户的状态。这不应该是一个频繁的操作,因为服务器正在广播用户的在线状态,我们可以在一段时间内忍受用户旧的下线状态。
5.每当客户端开始与另一个用户进行新的聊天时,我们就可以在此时拉取状态。
在这里插入图片描述
详细组件设计:
客户端将打开到聊天服务器的连接以发送消息;然后服务器将将其传递给请求的用户。所有活动用户都将保持与服务器的连接,以接收消息。每当新消息到达时,聊天服务器将在长轮询请求中将其推送给接收用户。消息可以存储在支持快速小更新和基于范围搜索的HBASE中。服务器可以向其他相关用户广播当前用户的在线状态。客户端可以以更小的频率为在客户端视图中可见的其他用户提取状态用于更新。

6.数据分区

由于我们将存储大量数据(3.6PB,为期五年),我们需要将其分发到多个数据库服务器上。

我们的分区方案是什么?
基于UserId的分区:
假设我们基于UserId的散列进行分区,这样我们就可以将用户的所有消息保存在同一个数据库中。如果一个DB切片是4TB,五年内我们将有3.6PB/4TB~=900碎片。为了简单起见,让我们假设我们保留1K切片。因此,我们将通过hash(UserId)%1000找到切片编号,然后从其中存储/检索数据。这种分区方案也可以非常快地为任何用户获取聊天历史。

首先,我们可以从拥有驻留在一台物理服务器的多个切片的少量的数据库服务器出发。因为我们可以在一个服务器上有多个数据库实例,所以我们可以很容易地在一个服务器上存储多个分区。我们的哈希函数需要理解这个逻辑分区方案,以便它能够在一个物理服务器上映射多个逻辑分区。

由于我们将存储无限的消息历史,我们可以从大量的逻辑分区开始,这些逻辑分区将被映射到较少的物理服务器,并且随着存储需求的增加,我们可以添加更多的物理服务器来分发我们的逻辑分区。

基于MessageID的分区:
如果我们将用户的不同消息存储在不同的数据库切片上,获取聊天消息的范围将非常缓慢,所以我们不应该采用这种方案。

7.缓存

我们可以在最近的几次对话中缓存一些最近的消息(比如最后15条),这些信息在用户的视图(比如最后5条)中是可见的。由于我们决定将用户的所有消息存储在一个切片上,所以用户的缓存也应该完全驻留在一台机器上。

8.负载平衡

我们需要一个在聊天服务器前面放一个负载均衡器;它可以将每个UserId映射到一个为用户保存连接的服务器,然后将请求定向到该服务器。类似地,我们需要一个缓存服务器的负载均衡器。

9.容错和复制

当聊天服务器失败时会发生什么?
我们的聊天服务器保持与用户的连接。如果服务器发生故障,我们是否应该设计一种机制将这些连接传输到其他服务器?故障转移到其他服务器的TCP连接非常困难;如果连接丢失,可以让客户端自动重新连接。

我们应该存储多个用户消息副本吗?
我们不能只有用户数据的一个副本,因为如果保存数据的服务器崩溃或永久关闭,我们就没有任何机制恢复数据。为此,我们要么必须在不同的服务器上存储多个数据副本,要么使用Reed-Soloman编码等技术来分发和复制数据。

10.扩大需求

A.群聊
我们可以在我们的系统中有单独的群聊对象,这些对象可以存储在聊天服务器上。群聊对象由GroupChatID标识,并将维护参与该聊天的人的列表。我们的负载均衡器可以根据GroupChatID直接找到每个群聊,而处理群聊的服务器可以遍历聊天的所有用户,找到处理每个用户连接的服务器来传递消息。
在数据库中,我们可以将所有群聊对象存储在一个基于GroupChatID划分的单独表中。

B.推送通知
在我们当前的设计中,用户只能向其他在线的用户发送消息,如果接收者离线,则服务器告知发送者发送失败。推送通知将使我们的系统能够向离线用户发送消息。

对于推送通知,每个用户都可以从他们的设备(或网络浏览器)中选择,在出现新消息或事件时获得通知。每个制造商都维护着一组处理将这些通知推送给用户的服务器。

为了在我们的系统中有推送通知,我们需要设置一个通知服务器,它将接收到要发送给离线用户的消息,将消息发送到制造商的推送通知服务器,最后将消息发送到用户的设备上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值