本文章参考https://systeminterview.com/d...,有删减。
在本章中,我们将探讨聊天系统的设计。几乎每个人都使用聊天应用程序。图12-1显示了市场上一些最流行的应用程序。
聊天应用为不同的人执行不同的功能。确定确切的要求是极其重要的。例如,当面试官考虑一对一聊天时,您不希望设计一个专注于集体聊天的系统。探索功能需求是很重要的。
步骤1-确定需求
当然,世界上没有最完美的架构,只有最合适的架构,也没有所谓的通用方案,不同的解决方案都有其优缺点,只有最满足业务的系统才是一个好的系统。而且,在有限的人力、物力,综合考虑时间成本,通常需要做出很多权衡。
就要设计的聊天应用程序的类型达成一致至关重要。在市场上,有Facebook Messenger、微信和WhatsApp等一对一聊天应用,Slack等专注于群聊的office聊天应用,Discord等专注于大群体互动和低语音聊天延迟的游戏聊天应用。
第一组澄清问题应该明确面试官在要求你设计聊天系统时的具体想法。至少,弄清楚你是应该专注于一对一聊天还是群聊应用。您可能会问以下问题:
候选人:我们应该设计什么样的聊天应用程序?1对1还是基于组?面试官:它应该支持1对1和群聊。
候选人:这是一个移动应用程序吗?还是网络应用?或者两者都有?面试官:都有。
候选人:这个应用程序的规模是多少?创业应用还是大规模?面试官:它应该支持5000万每日活跃用户(DAU)。
候选人:对于群组聊天,群组成员限制是多少?面试官:最多100人
候选人:聊天应用程序的重要功能是什么?它能支持附件吗?面试官:1对1聊天,群聊,在线指示器。系统仅支持文本消息。
候选人:邮件大小有限制吗?面试官:是的,文本长度应该少于100000个字符。
候选人:需要端到端加密吗?面试官:现在不需要,但如果时间允许,我们会讨论的。
候选人:我们应该将聊天记录存储多久?面试官:永远。
在本章中,我们将重点设计一款类似Facebook messenger的聊天应用程序,重点介绍以下功能:
•一对一聊天,传递延迟低
•小组聊天(最多100人)
•在线状态
•多设备支持。同一帐户可以同时登录到多个帐户。
•推送通知
就设计规模达成一致也很重要。我们将设计一个支持5000万DAU的系统。
第2步-设计
为了开发高质量的设计,我们应该具备客户机和服务器如何通信的基本知识。在聊天系统中,客户端可以是移动应用程序或web应用程序。客户机之间不直接通信。相反,每个客户端都连接到一个聊天服务,该服务支持上述所有功能。让我们关注基本业务。聊天室服务必须支持以下功能:
•接收来自其他客户端的消息。
•为每条消息找到合适的收件人,并将消息转发给收件人。
•如果收件人不在线,则在服务器上保留该收件人的消息,直到其在线。
图12-2显示了客户端(发送方和接收方)与聊天服务之间的关系。
当客户端打算启动聊天时,它会使用一个或多个网络协议连接聊天服务。对于聊天服务,网络协议的选择很重要。让我们和面试官讨论一下。
对于大多数客户机/服务器应用程序,请求由客户机发起。对于聊天应用程序的发送方来说也是如此。在图12-2中,当发送方通过聊天服务向接收方发送消息时,它使用经过时间测试的HTTP协议,这是最常见的web协议。在此场景中,客户端打开与聊天服务的HTTP连接并发送消息,通知服务将消息发送给接收方。keep-alive在这方面很有效,因为keep-alive头允许客户端与聊天服务保持持久连接。它还减少了TCP握手的次数。HTTP在发送方是一个很好的选择,许多流行的聊天应用程序(如Facebook[1])最初使用HTTP发送消息。
然而,接收器端要复杂一些。由于HTTP是由客户机发起的,因此从服务器发送消息并不简单。多年来,许多技术被用来模拟服务器启动的连接:轮询、长轮询和WebSocket。这些都是系统设计面试中广泛使用的重要技巧,所以让我们来研究一下它们。
轮询
如图12-3所示,轮询是一种客户端定期询问服务器是否有可用消息的技术。根据轮询频率,轮询的成本可能会很高。它可能会消耗宝贵的服务器资源来回答一个在大多数情况下都没有答案的问题。
长轮询
由于轮询可能效率低下,下一步是长轮询(图12-4)。
在长轮询中,客户端保持连接打开,直到有新消息可用或达到超时阈值。一旦客户端接收到新消息,它会立即向服务器发送另一个请求,从而重新启动进程。长轮询有几个缺点:
•发送方和接收方可能无法连接到同一聊天服务器。基于HTTP的服务器通常是无状态的。如果使用循环法进行负载平衡,则接收消息的服务器可能与接收消息的客户端没有长轮询连接。
•服务器无法很好地判断客户端是否已断开连接。
•效率低下。如果用户聊天不多,长轮询仍然会在超时后进行定期连接。
websocket长连接
WebSocket是从服务器向客户端发送异步更新的最常见解决方案。图12-5显示了其工作原理。
WebSocket连接由客户端启动。它是双向和持久的。它从HTTP连接开始,可以通过一些定义良好的握手“升级”到WebSocket连接。通过这种持久连接,服务器可以向客户端发送更新。即使有防火墙,WebSocket连接通常也能正常工作。这是因为它们使用的端口80或443也被HTTP/HTTPS连接使用。
前面我们说过,在发送方使用HTTP是一种很好的协议,但是由于WebSocket是双向的,因此没有强有力的技术理由不将其用于发送。图12-6显示了如何将WebSocket(ws)用于发送方和接收方。
通过将WebSocket用于发送和接收,它简化了设计,并使客户端和服务器上的实现更加简单。由于WebSocket连接是持久的,因此高效的连接管理在服务器端至关重要。
高级设计
刚才我们提到WebSocket被选为客户端和服务器之间双向通信的主要通信协议,需要注意的是,其他一切都不必是WebSocket。事实上,聊天应用程序的大多数功能(注册、登录、用户配置文件等)都可以通过HTTP使用传统的请求/响应方法。让我们深入了解一下系统的高级组件。
如图12-7所示,聊天系统分为三大类:无状态服务、有状态服务和第三方集成。
无状态服务
无状态服务是传统的面向公众的请求/响应服务,用于管理登录、注册、用户配置文件等。这些是许多网站和应用程序的常见功能。
无状态服务位于负载平衡器后面,负载平衡器的任务是根据请求路径将请求路由到正确的服务。这些服务可以是单一的或单个的微服务。我们不需要自己构建这些无状态服务中的许多,因为市场上存在可以轻松集成的服务。我们将深入讨论的一项服务是服务发现。它的主要工作是为客户端提供一个可以连接到的聊天服务器的DNS主机名列表。
有状态服务
唯一有状态的服务是聊天服务。该服务是有状态的,因为每个客户端都保持与聊天服务器的持久网络连接。在该服务中,只要服务器仍然可用,客户端通常不会切换到另一个聊天服务器。服务发现与聊天服务密切配合,以避免服务器过载。我们将深入探讨细节。
第三方集成
对于聊天应用程序,推送通知是最重要的第三方集成。这是一种在新消息到达时通知用户的方法,即使应用程序未运行。推送通知的正确集成至关重要。有关更多信息,请参阅第10章设计通知系统。
可伸缩性
在小范围内,上面列出的所有服务都可以安装在一台服务器中。即使按照我们设计的规模,理论上也可以在一台现代云服务器中容纳所有用户连接。服务器可以处理的并发连接数很可能是限制因素。在我们的场景中,在一百万并发用户的情况下,假设每个用户连接在服务器上需要10K的内存(这是一个非常粗略的数字,并且非常依赖于语言选择),它只需要大约10GB的内存就可以在一个框中容纳所有连接。
如果我们提出一个设计,所有的东西都放在一台服务器上,这可能会在面试官的脑海中升起一个巨大的危险信号。没有一个技术专家会在一台服务器上设计这样的规模。由于许多因素,单服务器设计是交易的破坏者。单点故障是其中最大的故障。
但是,从单一服务器设计开始是完全正确的。确保面试官知道这是一个起点。将我们提到的所有内容放在一起,图12-8显示了调整后的高级设计。
在图12-8中,客户端保持与聊天服务器的持久WebSocket连接,以进行实时消息传递。
•聊天服务器便于发送/接收消息。
•状态服务器管理在线/离线状态。
•API服务器处理一切,包括用户登录、注册、更改配置文件等。
•通知服务器发送推送通知。
•最后,键值存储用于存储聊天历史记录。当离线用户联机时,她将看到以前的所有聊天记录。
存储
现在,我们已经准备好了服务器,服务正在运行,第三方集成已经完成。在技术层的深处是数据层。数据层通常需要一些努力才能使其正确。我们必须做出的一个重要决定是选择正确的数据库类型:关系数据库还是NoSQL数据库?为了做出明智的决定,我们将检查数据类型和读/写模式。
典型的聊天系统中存在两种类型的数据。第一种是通用数据,例如用户配置文件、设置、用户好友列表。这些数据存储在健壮可靠的关系数据库中。复制和分片是满足可用性和可伸缩性要求的常用技术。
第二个是聊天系统特有的:聊天历史数据。理解读/写模式很重要。
•聊天系统的数据量巨大。此前的一项研究[2]显示,Facebook messenger和Whatsapp每天处理600亿条消息。
•只经常访问最近的聊天。用户通常不会查找旧聊天记录。
•尽管在大多数情况下都会查看最近的聊天记录,但用户可能会使用需要随机访问数据的功能,如搜索、查看您的提及、跳转到特定消息等。数据访问层应支持这些情况。
•1对1聊天应用的读写比约为1:1。
选择支持我们所有用例的正确存储系统至关重要。出于以下原因,我们推荐键值存储:
•键值存储允许轻松水平缩放。
•键值存储提供非常低的数据访问延迟。
•关系数据库不能很好地处理长尾[3]数据。当索引变大时,随机访问的代价很高。
•键值存储被其他经验证的可靠聊天应用程序采用。例如,Facebook messenger和Discord都使用键值存储。Facebook messenger使用HBase[4],Discord使用Cassandra[5]。
数据模型
刚才,我们讨论了使用键值存储作为存储层。最重要的数据是消息数据。让我们仔细看看。
1对1聊天的消息表
图12-9显示了1对1聊天的消息表。主键是message_id,它有助于确定消息顺序。我们不能依靠created_at来决定消息序列,因为可以同时创建两条消息。
群组聊天的消息表
图12-10显示了群组聊天的消息表。复合主键是(通道id、消息id)。频道和组在这里表示相同的含义。channel_id是分区键,因为群组聊天中的所有查询都在一个频道中运行。
消息ID
如何生成消息id是一个值得探讨的有趣话题。消息id负责确保消息的顺序。要确定消息的顺序,消息id必须满足以下两个要求:
•ID必须是唯一的。
•ID应按时间进行排序,这意味着新行的ID高于旧行。
我们如何才能实现这两个保证?首先想到的是MySql中的“auto_increment”关键字。然而,NoSQL数据库通常不提供这样的功能。
第二种方法是使用全局64位序列号生成器,如Snowflake[6]。这将在“第7章:在分布式系统中设计唯一的ID生成器”中讨论。
最后一种方法是使用本地序列号生成器。本地意味着ID仅在组中是唯一的。本地ID工作的原因是在一对一通道或组通道内维护消息序列就足够了。与全局ID实现相比,这种方法更容易实现。
第3步-深度设计
在系统设计面试中,通常期望您深入了解高级设计中的一些组件。对于聊天系统,服务发现、消息流和在线/离线指标值得深入探索。
服务发现
服务发现的主要作用是根据地理位置、服务器容量等标准为客户端推荐最佳的聊天服务器。Apache Zookeeper[7]是一种流行的服务发现开源解决方案。它注册所有可用的聊天服务器,并根据预定义的标准为客户端选择最佳的聊天服务器。
图12-11显示了服务发现(Zookeeper)的工作原理。
- 用户A尝试登录应用程序。
- 负载平衡器将登录请求发送到API服务器。
- 在后端对用户进行身份验证后,服务发现会为用户A找到最佳的聊天服务器。在本例中,选择了服务器2,并将服务器信息返回给用户A。
- 用户A通过WebSocket连接到聊天服务器2。
消息流
了解聊天系统的端到端流程很有趣。在本节中,我们将探讨1对1聊天流、跨多个设备的消息同步以及群组聊天流。
1对1聊天消息流
图12-12解释了当用户A向用户B发送消息时会发生什么。
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显示了消息同步的示例。
在图12-13中,用户A有两个设备:电话和笔记本电脑。当用户A用手机登录聊天应用程序时,它会与聊天服务器1建立WebSocket连接。类似地,笔记本电脑和聊天服务器1之间存在连接。
每个设备都维护一个名为cur_max_message_id的变量,该变量跟踪设备上的最新消息id。满足以下两个条件的消息被视为新闻消息:
•收件人ID等于当前登录的用户ID。
•键值存储中的消息ID大于cur_max_Message_ID。
由于每个设备上都有不同的cur_max_message_id,消息同步很容易,因为每个设备都可以从KV存储中获取新消息。
群聊消息流
与一对一聊天相比,群组聊天的逻辑更加复杂。图12-14和12-15解释了流程。
图12-14解释了用户A在群聊中发送消息时发生的情况。假设组中有3个成员(用户A、用户B和用户C)。首先,将来自用户A的邮件复制到每个组成员的邮件同步队列:一个用于用户B,另一个用于用户C。您可以将邮件同步队列视为收件人的收件箱。此设计选择适用于小团体聊天,因为:
•它简化了邮件同步流程,因为每个客户端只需检查自己的收件箱即可获得新邮件。
•当组号较小时,在每个收件人的收件箱中存储一份副本并不太昂贵。
微信采用了类似的方法,它将一个群组的成员限制在500人[8]。但是,对于具有大量用户的组,为每个成员存储消息副本是不可接受的。
在收件人端,收件人可以接收来自多个用户的消息。每个收件人都有一个收件箱(邮件同步队列),其中包含来自不同发件人的邮件。图12-15说明了设计。
在线状态
在线状态指示器是许多聊天应用程序的基本功能。通常,您可以在用户的个人资料图片或用户名旁边看到一个绿点。本节解释了幕后发生的事情。
在高级设计中,状态服务器负责管理在线状态并通过WebSocket与客户端通信。有一些流将触发联机状态更改。让我们逐一检查一下。
用户登录
“服务发现”部分解释了用户登录流程。在客户端和实时服务之间建立WebSocket连接后,用户a的在线状态和时间戳处的最后一次活动将保存在KV存储中。状态指示器显示用户登录后处于联机状态。
用户注销
当用户注销时,它将通过用户注销流程,如图12-17所示。KV商店中的联机状态更改为脱机。状态指示器显示用户处于脱机状态。
用户断开连接
我们都希望我们的互联网连接是一致和可靠的。然而,情况并非总是如此;因此,我们必须在设计中解决这个问题。当用户断开与internet的连接时,客户端和服务器之间的持久连接将丢失。处理用户断开连接的一种简单方法是将用户标记为脱机,并在重新建立连接时将状态更改为联机。然而,这种方法有一个主要缺陷。用户通常会在短时间内频繁断开和重新连接到internet。例如,当用户通过隧道时,可以打开和关闭网络连接。在每次断开/重新连接时更新联机状态会使状态指示器频繁更改,从而导致用户体验不佳。
我们引入心跳机制来解决这个问题。在线客户端定期向状态服务器发送心跳事件。如果状态服务器在特定时间内(例如x秒)从客户端接收到心跳事件,则认为用户处于联机状态。否则,它将处于脱机状态。
在图12-18中,客户端每5秒向服务器发送一次心跳事件。发送3个心跳事件后,客户端断开连接,并且在x=30秒内未重新连接(任意选择此数字以演示逻辑)。联机状态更改为脱机。
用户在线状态
用户A的朋友如何知道状态更改?图12-19解释了其工作原理。状态服务器使用发布-订阅模型,其中每个好友对维护一个通道。当用户A的联机状态更改时,它将事件发布到三个频道,即频道A-B、A-C和A-D。这三个频道分别由用户B、C和D订阅。因此,朋友很容易获得在线状态更新。客户端和服务器之间的通信是通过实时WebSocket进行的。
上述设计对于小用户群是有效的。例如,微信采用了类似的方法,因为其用户群上限为500。对于较大的群体,通知所有成员在线状态既昂贵又耗时。假设一个组有100000名成员。每次状态更改将生成100000个事件。要解决性能瓶颈,一个可能的解决方案是仅当用户进入组或手动刷新好友列表时获取联机状态。
第4步-技术总结
在本章中,我们介绍了一个聊天系统体系结构,它支持1对1聊天和群聊。WebSocket用于客户端和服务器之间的实时通信。聊天系统包含以下组件:用于实时消息传递的聊天服务器、用于管理在线状态的状态服务器、用于发送推送通知的推送通知服务器、用于聊天历史持久性的键值存储以及用于其他功能的API服务器。
如果你在面试结束时有多余的时间,下面是一些额外的谈话要点:
•扩展聊天应用程序以支持照片和视频等媒体文件。媒体文件的大小明显大于文本。压缩、云存储和缩略图是值得讨论的有趣话题。
•端到端加密。Whatsapp支持对消息进行端到端加密。只有发件人和收件人才能阅读邮件。感兴趣的读者应参考参考资料[9]中的文章。
•在客户端缓存消息可以有效减少客户端和服务器之间的数据传输。
•缩短加载时间。Slack构建了一个地理分布的网络,以缓存用户的数据、通道等,从而缩短加载时间[10]。
•错误处理。
–聊天服务器错误。与聊天服务器的连接可能有几十万个,甚至更多的持久连接。如果聊天服务器脱机,服务发现(Zookeeper)将为客户端提供一个新的聊天服务器,以建立新的连接。
–消息重新发送机制。重试和排队是重新发送消息的常用技术。
OpenIM了解我们
OpenIMgithub开源地址:
https://github.com/OpenIMSDK/...
OpenIM官网 : https://www.rentsoft.cn
OpenIM官方论坛:https://forum.rentsoft.cn/
更多技术文章:
开源OpenIM:高性能、可伸缩、易扩展的即时通讯架构
https://forum.rentsoft.cn/thr...
【OpenIM原创】简单轻松入门 一文讲解WebRTC实现1对1音视频通信原理
https://forum.rentsoft.cn/thr...
【OpenIM原创】开源OpenIM:轻量、高效、实时、可靠、低成本的消息模型
https://forum.rentsoft.cn/thr...