本文是翻译文章,翻译者是一位大四英语系学生,天生对软件系统架构敏感,也非常感兴趣。翻译文章是最好的入门方法之一。
本篇中,我们将设计一个类似Instagram的照片共享服务,用户可以在其中上传照片,以和其他用户共享。
类似服务:Flickr,Picasa
难度等级:中
1.什么是Instagram
Instagram是一项社交网络服务,用户可以上传照片和视频,并与其他用户共享。Instagram用户可以选择公开或向私人共享信息。公开共享的内容可以被所有用户查看,而私人共享的内容只能由一组指定的人访问。Instagram还允许其用户通过其他社交网络平台进行共享,例如Facebook,Twitter,Flickr和Tumblr。
出于实际考虑,我们计划设计一个更简单的Instagram版本,在此版本上,用户可以共享照片,也可以关注其他用户。每个用户的“NewsFeed”将包含该用户关注的所有人的热门照片。
2.系统的要求和目标
设计Instagram时,我们将重点关注以下几组要求:
功能性需求:
1) 用户可以上传/下载/查看照片。
2) 用户可以根据照片/视频标题进行搜索。
3) 用户可以关注其他用户。
4) 该系统可以生成并显示用户的“News Feed”,其中包含来自用户关注的所有人的热门照片。
非功能性需求:
1) 我们的服务需要具有高可用性。
2) 对于"News Feed"的生成,系统可接受的延迟时间为200毫秒。
3) 如果用户有一段时间不看照片,一致性可能会受到影响(为了可用性),但这是小问题。
4) 该系统应高度可靠;任何上传的照片或视频都不会丢失。
不在要求范围内的需求:在照片中添加标签,根据标签搜索照片,评论照片,在照片中为用户添加标签,关注对象等。
3. 一些设计时的考虑
该系统将需进行大量的读取工作,因此我们将聚焦于构建一个可以快速检索照片的系统。
1) 实际使用时,用户可以上传任意数量的照片。因此在设计该系统时,有效的存储管理是很关键的。
2) 观看照片时,我们期望低延迟。
3) 数据应该是100%可靠的。如果用户上传了照片,系统将保证照片100%不会丢失。
4.容量估算和约束
l 假设我们有5亿总用户,每天有100万活跃用户。
l 每天有200万张新照片,每秒有23张新照片。
l 照片平均大小=> 200KB
l 1天的照片所需的总空间:2M * 200KB => 400 GB
l 10年所需的总空间:400GB * 365(每年的天数)*10(年)〜= 1425TB
5.高阶系统设计
从高阶设计来看,我们需要支持两种情况:一种是上传照片,另一种是查看/搜索照片。我们的服务需要一些对象存储服务器来存储照片,还需要一些数据库服务器来存储有关照片的元数据信息。
6.数据库架构
?在interview的早期阶段定义数据库架构将有助于理解各个组件之间的数据流,随后可以指导数据分区。
我们需要存储用户上传照片的数据,包括用户的以及他们关注的人的数据。照片表将存储与照片相关的所有数据。因为我们要先获取最近的照片,所以我们需要在(PhotoID,CreationDate)上建立索引。
一种存储上述结构的简单方法是使用像MySQL这样的RDBMS,因为我们需要连接查询。但是关系数据库总是面临多种挑战,特别是当我们需要动态扩容时。更多详细信息,请查看“SQL vs.NoSQL”(https://www.educative.io/courses/grokking-the-system-design-interview/YQlK1mDPgpK)
我们可以将照片存储在类似HDFS或S3的分布式文件存储系统中。
我们可以将上述结构存储在分布式键值存储系统中,以获得NoSQL带来的好处。所有与照片相关的元数据都可以转到一个表,表中“键”是“ PhotoID”,“值”是一个包含PhotoLocation,UserLocation,CreationTimestamp等的对象。
我们需要存储用户和照片之间的关系,以了解谁拥有哪张照片。我们还需要存储用户关注的人员列表。对于这两个表,我们都可以使用像Cassandra这样的宽列数据存储系统。对于“UserPhoto”表,“键”为“ UserID”,“值”为用户拥有的“ PhotoID”列表,它们存储在不同的列中。对于“UserFollow”表,我们有一个类似的结构。
通常,Cassandra或键值存储系统始终维护一定数量的副本以提供可靠性。此外,在此类数据存储系统中,删除操作不会立即生效,数据会保留几天(以支持取消删除),然后才能从系统中永久删除。
7. 数据大小估算
下面我们来估算一下每个表中要存储多少数据,以及十年后我们需要多少存储空间。
用户:
假设每个“int”和“ dateTime”为四个字节,则用户表中的每一行均为68个字节:
用户ID(4个字节)+名称(20个字节)+电子邮件(32个字节)+ DateOfBirth(4个字节)+ CreationDate(4个字节)+ LastLogin(4个字节)=68个字节
如果我们有5亿用户,那么我们需要32GB的总存储空间。
5亿*68〜= 32GB
照片:
照片表中的每一行为284个字节:
PhotoID(4个字节)+ UserID(4个字节)+PhotoPath(256个字节)+PhotoLatitude(4个字节)+PhotLongitude(4个字节)+ UserLatitude(4个字节)+ UserLongitude(4个字节)+ CreationDate(4个字节)= 284个字节
如果每天上传200万张新照片,则一天需要0.5GB的存储空间:
2M * 284字节〜=每天0.5GB
10年后,我们需要1.88TB的存储空间。
UserFollow:
UserFollow表中的每一行将由8个字节组成。如果我们有5亿用户,并且平均每个用户关注500个用户。UserFollow表需要1.82TB的存储空间:
5亿用户*500位关注者* 8字节〜=1.82TB
所有表在10年内所需的总空间将为3.7TB:
32GB + 1.88TB + 1.82TB〜=3.7TB
8.组件设计
照片上传(或写入)的速度可能会很慢,因为它们必须进入磁盘。但是读取照片会更快,尤其是从缓存中读取照片时。
用户在上传过程中的会消耗大量的网络,因为上传是一个缓慢的过程。这意味着,如果系统忙于所有“写”的请求,则无法提供“读”功能。我们应该记住,在设计系统之前,Web服务器有连接限制。如果我们假设一个Web服务器在任何时刻最多可以有500个连接,那么它不能有超过500个并发上传或读取。为了解决这个瓶颈,我们可以将读取和写入分为单独的服务。我们将有专用的服务器进行读取,并有不同的服务器用于写入,从而确保上传操作不会独占系统。
分离照片的读写请求也会使我们能够分别缩放和优化这些操作。
9.可靠性和冗余
我们的服务要确保不能丢失文件。因此,对于每个文件,我们将存储多个副本,如果一个存储服务器停止工作,我们可以从其他存储服务器上存在的副本中检索照片。
同样的原理也适用于系统的其他组件。如果要使系统具有较高的可用性,则需要在系统中有各项服务的多重副本,这样在少数服务中断时,系统仍然可用并仍在运行。冗余消除了系统中的单点故障。
如果在任何时候只需要运行一个服务的实例,我们就可以运行该服务的冗余辅助副本,该副本不服务于任何信息流量,但是当主节点出现问题时,它可以在故障切换之后进行控制。
在系统中创建冗余可以消除单点故障,并在危机时提供备份或备用功能。例如,如果生产中正在运行同一服务的两个实例,而一个实例发生故障或降级,则系统可以故障切换到正常副本。故障切换可以自动发生或需要手动干预。
10. 数据分片
让我们讨论元数据分片的不同方案:
A. 基于UserID进行分区
假设我们基于“ UserID”进行分片,以便将用户的所有照片保留在同一分片上。如果一个数据库分片为1TB,我们需要四个分片来存储3.7TB的数据。为了获得更好的性能和可伸缩性,我们保留10个分片。
因此,我们将通过用户UserID%10查找分片号,然后将数据存储在此。为了唯一的识别我们系统中的任何照片,我们可以在每个PhotoID后附上分片号。
我们如何生成PhotoID?
每个数据库分片都可以有自己的PhotoID自动递增序列,并且由于我们在每个PhotoID后附上ShardID,因此PhotoID在整个系统中是唯一的。
此分区方案有哪些问题?
1 我们将如何处理热门用户?这些热门用户有很多关注者,很多人会看到他们上传的任何照片。
2 与其他用户相比,某些用户将拥有很多照片,因此产生存储分配不均匀的问题。
3 如果我们无法将用户的所有图片存储在一个分片上怎么办?如果我们将用户的照片分配到多个分片上,会导致更高的延迟吗?
4 将用户的所有照片存储在一个分片上可能会引起问题,例如:如果该分片停止工作,则无法使用该用户的所有数据;如果服务处于高负载,则会导致更高的延迟等。
B. 基于PhotoID的分区
如果我们可以首先生成唯一的PhotoID,然后通过PhotoID%10找到一个分片号,则上述方案的问题将得到解决。这种情况下,我们不需要在PhotoID后面附加ShardID,因为PhotoID本身在整个系统中已经是唯一的了。
我们如何生成PhotoID?
在这里,我们无法在每个分片中都设一个自动递增序列来定义PhotoID,因为我们需要首先知道PhotoID,从而才能找到其存储在哪个分片。一种解决方案是,我们专用一个单独的数据库实例来生成自动递增的ID。如果我们的PhotoID可以容纳64位,则可以定义一个仅包含64位ID字段的表。因此,无论我们何时要在系统中添加照片,我们都可以在此表中插入新行,并将该ID用作新照片的PhotoID。
生成Key的数据库会成为单点故障吗?
是的,它会有单点故障的问题。一种解决方案是定义两个这样的数据库,其中一个生成偶数编号的ID,另一个生成奇数编号的ID。对于MySQL,以下脚本可以定义此类序列:
我们可以在这两个数据库的前面放置一个负载均衡器,从而在它们之间进行轮循并应对停机时间。这两个服务器可能都不同步,其中一个服务器比另一个服务器生成的key更多,但这不会在我们的系统中引起任何问题。我们可以通过为用户,照片评论或系统中存在的其他对象定义单独的ID表来扩展此设计。
我们如何计划系统的未来增长?
我们可以有大量的逻辑分区来适应未来的数据增长,这样从一开始,多个逻辑分区就驻留在单个物理数据库服务器上。由于每个数据库服务器上都可以有多个数据库实例,因此任何服务器上的每个逻辑分区都可以有单独的数据库。所以,只要我们感觉到特定的数据库服务器具有大量数据,就可以将一些逻辑分区从该服务器迁移到另一台服务器。我们可以维护一个配置文件(或一个单独的数据库),该文件可以将逻辑分区映射到数据库服务器,这将使我们能够轻松移动分区。每当我们要移动分区时,我们只需更新配置文件即可宣布更改。
11. 排序和News Feed的生成
要为任何给定的用户创建News Feed,我们需要获取该用户关注的人的最新的,最受欢饮迎的和任何相关的照片。
简单起见,假设我们需要为用户的News Feed获取前100张照片。我们的应用程序服务器将首先获取用户关注的人员列表,然后从每个用户获取最新的100张照片的元数据信息。在最后一步,服务器会将所有的这些照片提交给我们的排序算法,该算法将确定前100张照片(基于新近度,相似度等)并将其返回给用户。这种方法可能出现的问题是更高的延迟,因为我们必须查询多个表并对结果执行分类/合并/排序。为了提高效率,我们可以预先生成News Feed,并将其存储在单独的表格中。
预先生成NewsFeed:
我们可以拥有专用的服务器,这些服务器可以连续不断地生成用户的News Feed,并将其存储在“UserNewsFeed”表中。因此,无论用户何时需要其News Feed的最新照片,我们都能简单地查询该表并将结果返回给用户。
每当这些服务器需要生成用户的News Feed时,它们将首先查询”UserNewsFeed”表以寻找上一次为该用户生成News Feed是何时。然后,从该时间起将生成新的NewsFeed数据(遵循上述提到的步骤)。
将NewsFeed内容发送给用户的方法有哪些?
1)拉取:客户端可以定期或在用户需要时手动从服务器拉取News Feed的内容。这种方法可能的问题是:a)在客户端发出拉取请求之前,新的数据不会向用户显示。b)在大多数情况下,如果没有新数据,则拉取请求将导致一个空的响应。
2)推送:只要服务器是可用的,其就可以立即将新数据推送给用户。为了有效地进行管理,用户必须与服务器维持Long Poll请求以接收更新。这种方法在处理关注很多人的用户或拥有数百万关注者的名人用户时出现问题。在这种情况下,服务器必须非常频繁地推送更新的信息。
3)混合:
我们还可以采用混合方法。我们可以将所有关注度很高的用户转移到基于拉取的模型,而仅将数据推送给关注度数百(或千)的用户。另一种可行的方法是服务器将更新的信息推送给所有用户的频率不超过某个特定频率,从而使拥有大量关注者/更新信息的用户可以定期拉取数据。
有关生成News Feed的详细讨论,请参阅“设计Facebook的News Feed”(https://www.educative.io/collection/page/5668639101419520/5649050225344512/5641332169113600)
12.使用分片数据创建NewsFeed
为任何给定用户创建News Feed的重要要求之一就是从该用户关注的所有人那里获取最新照片。为此,我们需要一种机制以依据照片的创建时间进行排序。为了有效地做到这一点,我们可以将照片创建时间作为PhotoID的一部分。由于我们将拥有有关PhotoID的主要索引,我们可以很快地找到最新的PhotoID。
为此,我们可以使用纪元时间。假设我们的PhotoID有两部分;第一部分代表纪元时间,第二部分是个自动递增序列。因此,要制作一个新的PhotoID,我们可以采用当前的纪元时间,并从生成key的数据库中读入一个自动递增的ID,追加到PhotoID的尾部。我们可以从该PhotoID中找出分片号(PhotoID%10)并将照片存储在那里。
PhotoID的大小可能是多少?
假设我们的纪元时间从今天开始,那么接下来的50年我们需要多少二进制位来存储秒数?
86400秒/天* 365(天/年)*50(年)=> 16亿秒
我们需要31位来存储这个数字。因为就平均而言,我们期望每秒有23张新照片;我们可以分配9位来存储自动递增的序列。因此,我们每秒可以存储(2^ 9 => 512)张新照片。我们可以每秒重置一次自动递增序列。
有关此技术的更多细节,请看“Twitter设计中的数据分片”(https://www.educative.io/courses/grokking-the-system-design-interview/m2G48X18NDO)
13. 缓存和负载平衡
我们的服务需要一个大规模的照片传送系统以服务于全球的用户。我们的服务应使用大量按地理位置分布的图片缓存服务器和CDNs(有关详细信息,请参阅“Caching”https://www.educative.io/courses/grokking-the-system-design-interview/3j6NnJrpp5p)将其内容推近至用户。
我们可以为元数据服务器引进一个高速缓冲存储器以缓存热数据。在命中数据库之前,我们可以使用Memcache来缓存数据,而ApplicationServer可以快速检查缓存是否具有所需的行。对于我们的系统,Least Recently Used(LRU,最近使用)可能是个合理的缓存回收策略。
我们如何建立更智能的缓存?
如果我们遵循二八原则,即照片20%的每日阅读量产生了80%的流量,就是说某些照片非常受欢迎,以致于大多数人都会浏览他们。这表明我们可以尝试缓存每日照片阅读量的前20%。