在数字时代,内容分享平台成为人们生活中的重要一环,从分享生活点滴、表达情感,到提供信息和娱乐,这类平台已经深深影响了我们的生活。其中,国外Instagram和国内的LOFTER,作为优秀的内容分享平台,凭借其出色的用户体验和强大的功能,吸引了大量的用户。本文将针对LOFTER的系统架构进行深度解析,为我们提供一种理解和设计大规模内容分享系统的视角。
项目简介:LOFTER是由网易公司开发的一个内容创作和分享平台,用户可以创建自己的博客,分享文字、图片、音乐等内容。该平台受到创意人群,如摄影爱好者、插画家等的喜爱,提供自由的创作空间。用户可以关注其他博主,互相评论和点赞,通过标签系统找到自己感兴趣的内容。LOFTER也会定期举办各种创作活动,鼓励用户创作和分享。
现在让我们设计一个类似于LOFTER的内容分享服务,用户可以上传照片与其他用户共享。
类似的产品有:Instagram、Picasa
系统难度等级:中等
1、什么是LOFTER?
LOFTER是一个社交网络服务,使其用户能够上传并与其他用户分享他们的照片和视频。LOFTER用户可以选择公开或私密地分享信息。公开分享的任何内容都可以被任何其他用户看到,而私密分享的内容只能被指定的一组人访问。LOFTER还使其用户能够通过许多其他社交网络平台分享,例如微博、Twitter、Flickr和Tumblr。
我们计划设计一个LOFTER的简化版本来解决这个设计问题,用户可以分享照片并关注其他用户。每个用户的“新鲜事”将包括用户关注的所有人的热门照片。
2、系统的需求和目标
在设计LOFTER时,我们将关注以下一组需求:
功能需求:
- 用户应能够上传/下载/查看照片。
- 用户可以根据照片/视频标题进行搜索。
- 用户可以关注其他用户。
- 系统应生成并显示用户的“新鲜事”,包括用户关注的所有人的热门照片。
非功能性需求:
- 我们的服务需要高度可用。
- 系统生成“新鲜事”的可接受延迟为200毫秒。
- 如果用户在一段时间内看不到照片,那么为了保持可用性,一定程度的一致性降低是可以接受的。
- 系统应高度可靠;任何上传的照片或视频都不应丢失。
不在讨论范围内:给照片添加标签,根据标签搜索照片,评论照片,给照片标记用户,推荐关注等等。
3、设计考虑
该系统将是读取密集型的,因此我们将专注于构建一个可以快速检索照片的系统。
- 实际上,用户可以上传他们喜欢的任何数量的照片;因此,高效的存储管理在设计此系统时应该是一个关键因素。
- 预期查看照片时的延迟非常低。
- 数据应该是100%可靠的。如果用户上传了一张照片,系统将保证它绝不会丢失。
4、容量估算和约束
让我们假设我们总共有5亿个用户,每天有100万活跃用户。
每天有200万新照片,每秒23张新照片。
平均照片文件大小=> 200KB
1天的照片所需的总空间
2M * 200KB => 400 GB
10年所需的总空间:
400GB * 365(一年的天数)* 10(年)~= 1425TB
5、高层系统设计
在高层次上,我们需要支持两种情况,一是上传照片,另一是查看/搜索照片。我们的服务将需要一些对象存储服务器来存储照片,以及一些数据库服务器来存储关于照片的元数据信息。
6、数据库模式
在设计的早期阶段定义数据库模式将有助于理解各个组件之间的数据流,后期将有助于数据分区。
我们需要存储关于用户、他们上传的照片以及他们关注的人的数据。Photo表将存储与照片相关的所有数据;我们需要在(PhotoID,CreationDate)上建立索引,因为我们需要先获取最新的照片。
存储上述模式的直接方法是使用像MySQL这样的关系型数据库,因为我们需要联接。但是,关系型数据库带来了他们的挑战,尤其是当我们需要扩展它们的时候。有关详细信息,请参阅 'SQL vs. NoSQL' 章节。
我们可以在分布式文件存储,如HDFS或S3中存储照片。
我们可以在分布式键值存储中存储上述模式,以享受NoSQL提供的好处。所有与照片相关的元数据都可以放到一个表中,其中 'key' 将是 'PhotoID','value' 将是一个包含 PhotoLocation, UserLocation, CreationTimestamp 等的对象。
一般来说,NoSQL存储总是维护一定数量的副本以提供可靠性。此外,在这样的数据存储中,删除操作并不会立即执行;系统会保留数据若干天(支持撤销删除)后才会从系统中永久删除。
7、数据大小估计
让我们估计一下每个表中将要存入多少数据,以及我们在10年内需要多少总存储空间。
用户:假设每个 "int" 和 "dateTime" 是四字节,用户表中的每一行将是68字节:
UserID(4字节)+ Name(20字节)+ Email(32字节)+ DateOfBirth(4字节)+ CreationDate(4字节)+ LastLogin(4字节)= 68字节
如果我们有5亿用户,我们将需要32GB的总存储空间。
5亿 * 68 ~= 32GB
Photo:Photo 表中的每一行将是284字节:
PhotoID(4字节)+ UserID(4字节)+ PhotoPath(256字节)+ PhotoLatitude(4字节)+ PhotoLongitude(4字节)+ UserLatitude(4字节)+ UserLongitude(4字节)+ CreationDate(4字节)= 284字节
如果每天有200万新照片被上传,我们将需要0.5GB的存储空间:
2M * 284字节 ~= 0.5GB 每天
10年我们将需要1.88TB的存储空间。
UserFollow:UserFollow表中的每一行将占用8字节。如果我们有5亿用户,每个用户平均关注500个用户。我们将需要1.82TB的存储空间用于UserFollow表:
5亿用户 * 500关注者 * 8字节 ~= 1.82TB
10年内所有表所需的总空间将为3.7TB:
32GB+1.88TB+1.82TB =3.7TB
8、组件设计
照片上传(或写入)可能会比较慢,因为它们需要写入磁盘,而读取则会更快,尤其是当它们由缓存服务时。
上传的用户可能会占用所有可用的连接,因为上传是一个慢的过程。这意味着如果系统忙于处理所有的 '写' 请求,那么 '读' 请求就无法得到服务。在设计我们的系统时,我们应该记住网络服务器在连接数量上有一个限制。如果我们假设一个网络服务器在任何时候最多可以有500个连接,那么它不能同时有超过500个的上传或读取。为了处理这个瓶颈,我们可以将读取和写入分开成为独立的服务。我们将有专门的服务器用于读取,不同的服务器用于写入,以确保上传不会拖慢系统。
将照片的读取和写入请求分开也将允许我们独立地对这些操作进行扩展和优化。
9、可靠性和冗余性
丢失文件对我们的服务来说是不可接受的。因此,我们将存储每个文件的多个副本,这样即使一个存储服务器出现问题,我们也可以从另一个存储在不同存储服务器上的副本中取回照片。
这个原则同样适用于系统的其他组件。如果我们想要系统具有高可用性,我们需要在系统中运行多个服务的副本,这样即使有几个服务出现问题,系统仍然可以保持可用和运行。冗余消除了系统中的单点故障。
如果任何时刻只需要运行一个服务的实例,我们可以运行一个冗余的次要副本,它不服务任何流量,但当主服务有问题时,它可以在故障切换后接管控制。
在系统中创建冗余可以消除单点故障,并在危机时提供备用或备用功能。例如,如果有两个相同服务的实例在生产环境中运行,其中一个失败或降级,系统可以切换到健康的副本。故障切换可以自动发生,也可以需要手动干预。
10、数据分片
我们来讨论一下元数据分片的不同方案:
A. 基于UserID进行分区
假设我们根据'UserID '进行分片,以便我们可以将一个用户的所有照片保留在同一个分片上。如果一个数据库分片是1TB,我们需要4个分片来存储3.7TB的数据。假设为了更好的性能和可扩展性,我们保留10个分片。
因此,我们可以通过UserID % 10找到分片编号,然后在那里存储数据。为了在我们的系统中唯一标识任何照片,我们可以在每个PhotoID后附加分片编号。
我们如何生成PhotoID呢?每个数据库分片都可以有自己的PhotoID自增序列,而且由于我们将ShardID 附加到每个PhotoID上,因此它在我们的整个系统中都将是唯一的。
这种分区方案有哪些问题呢?
- 我们如何处理热门用户?很多人关注这样的热门用户,很多其他人会看到他们上传的任何照片。
- 有些用户相比其他用户会有很多照片,因此会使存储分布不均匀。
- 如果我们不能将一个用户的所有图片存储在一个分片上怎么办?如果我们将用户的照片分布到多个分片上,会不会导致延迟增加?
- 将用户的所有照片存储在一个分片上可能会导致一些问题,比如如果该分片停止工作,用户的所有数据都无法访问,或者如果它正在承受高负载,延迟会增加等等。
B. 基于照片ID进行分区
如果我们可以先生成唯一的PhotoID,然后通过“PhotoID % 10”找到分片编号,以上的问题就可以解决了。在这种情况下,我们不需要在PhotoID后附加ShardID,因为PhotoID本身在整个系统中都是唯一的。
我们如何生成PhotoID呢?在这里,我们不能在每个分片中定义一个自增序列来定义PhotoID,因为我们需要先知道PhotoID才能找到它将被存储的分片。一个解决方案可能是我们专门分配一个数据库实例来生成自增ID。如果我们的PhotoID可以适应64位,我们可以定义一个只包含一个64位ID字段的表。所以每当我们想在我们的系统中添加一张照片时,我们可以在这个表中插入一个新行,并取那个ID作为新照片的PhotoID。
这个生成键的数据库不会成为单点故障吗?是的,它会。一个解决方法可能是定义两个这样的数据库,一个生成偶数ID,另一个生成奇数ID。对于MySQL,以下脚本可以定义这样的序列:
KeyGeneratingServer1:
auto-increment-increment = 2
auto-increment-offset = 1
KeyGeneratingServer2:
auto-increment-increment = 2
auto-increment-offset = 2
我们可以在这两个数据库前面放一个负载均衡器,用来在它们之间轮询,并处理宕机问题。这两个服务器可能会失去同步,一个生成的键可能比另一个多,但这不会在我们的系统中造成任何问题。我们可以通过为用户、照片评论或系统中的其他对象定义独立的ID表来扩展这种设计。
另外,我们可以实施一种类似于我们在“设计像TinyURL这样的URL缩短服务”中讨论的'键'生成方案。
我们如何为我们系统的未来增长做计划?我们可以有大量的逻辑分区以适应未来的数据增长,这样一开始,多个逻辑分区就可以驻留在一个物理数据库服务器上。由于每个数据库服务器可以运行多个数据库实例,所以我们可以在任何服务器上为每个逻辑分区有单独的数据库。所以,每当我们觉得某个数据库服务器的数据量很大时,我们可以将一些逻辑分区从它迁移到另一个服务器。我们可以维护一个配置文件(或一个单独的数据库),它可以映射我们的逻辑分区到数据库服务器;这将使我们能够轻松地移动分区。每当我们想移动一个分区时,我们只需要更新配置文件来宣布改变。
11、排名和新闻动态生成
为任何给定的用户创建News-Feed,我们需要获取用户关注的人发布的最新的、最受欢迎的、和相关的照片。
为了简化,让我们假设我们需要为用户的News-Feed获取最热门的100张照片。我们的应用服务器首先会获取用户关注的人的列表,然后获取每个用户最新100张照片的元数据信息。在最后一步,服务器会将所有这些照片提交给我们的排名算法,该算法会基于最近性、喜欢程度等因素确定最热门的100张照片,并返回给用户。这种方法的一个可能问题是由于我们必须查询多个表并对结果进行排序/合并/排名,所以延迟可能会更高。为了提高效率,我们可以预先生成News-Feed并将其存储在一个单独的表中。
预先生成新闻动态:我们可以有专门的服务器持续生成用户的News-Feed,并将其存储在“UserNewsFeed”表中。所以每当任何用户需要他们的News-Feed的最新照片时,我们只需要查询这个表并将结果返回给用户。
每当这些服务器需要生成用户的News-Feed时,它们首先会查询UserNewsFeed表,找出上一次为该用户生成News-Feed的时间。然后,从那个时间点开始生成新的News-Feed数据(按照上述步骤)。
将News-Feed内容发送给用户有哪些不同的方法?
- 拉取:客户端可以定期或在需要时手动从服务器拉取News-Feed内容。这种方法可能的问题是
- a) 新数据可能不会在客户端发出拉取请求之前显示给用户
- b) 如果没有新数据,大多数时间拉取请求会返回空响应。
- 推送:服务器可以在新数据可用时立即将其推送给用户。为了有效管理这一点,用户必须和服务器维持一个长轮询请求以接收更新。这种方法可能的问题是一个关注了很多人的用户,或者一个有数百万粉丝的名人用户;在这种情况下,服务器必须频繁地推送更新。
- 混合:我们可以采取混合方法。我们可以将所有关注人数较多的用户移至拉取模型,并只将数据推送给关注人数为几百(或几千)的用户。另一种方法可能是服务器将更新推送给所有用户,但不超过一定频率,并让有大量更新的用户定期拉取数据。
关于News-Feed生成的详细讨论,请参阅“设计Facebook的News-Feed”。
12、分片数据的News-Feed创建
创建任何给定用户的News-Feed的一个最重要的需求是获取用户关注的所有人发布的最新照片。为此,我们需要有一种机制来根据照片的创建时间进行排序。为了有效地做到这一点,我们可以将照片的创建时间作为PhotoID的一部分。由于我们在PhotoID上有一个主索引,所以找到最新的PhotoID将会非常快。
我们可以使用纪元时间来实现这一点。假设我们的PhotoID有两部分;第一部分将表示纪元时间,第二部分将是一个自动递增的序列。因此,要生成新的PhotoID,我们可以取当前的纪元时间并追加我们的键生成数据库的自动递增ID。我们可以从这个PhotoID中找出碎片号(PhotoID % 10)并在那里存储照片。
我们的PhotoID的大小可能是多少呢?假设我们的纪元时间从今天开始;我们需要多少位来存储接下来50年的秒数?
86400 秒/天 * 365 (days a year) * 50 (years) => 16亿秒
我们需要 31 位来存储这个数字。由于平均而言,我们预计每秒 23 张新照片,因此我们可以分配 9 个额外位来存储自动递增序列。所以每一秒,我们都可以存储(29≤5122^9\leq51229≤512)新照片。我们为序列号分配了 9 位,这超出了我们的要求;我们这样做是为了获得完整的字节数(如40bits=5bytes)。我们可以每秒重置自动递增序列。
我们将在“设计 Twitter”中的“数据分片”下讨论这项技术。
13、缓存和负载均衡
我们的服务将需要一个大规模的照片传递系统来服务全球分布的用户。我们的服务应该使用大量地理分布的照片缓存服务器和CDN(详见我的另一篇关于“Caching”的详细介绍)将其内容推送到用户附近。
我们可以为元数据服务器引入一个缓存,以缓存热门数据库行。我们可以使用Memcache来缓存数据,应用服务器在访问数据库之前可以快速检查缓存是否有所需的行。最近最少使用(LRU)可以是我们系统的合理缓存淘汰策略。根据这种策略,我们首先丢弃最近最少查看的行。
我们如何构建一个更智能的缓存?如果我们遵循二八定则,即每日照片的20%阅读量产生了80%的流量,这意味着某些照片非常受欢迎,大多数人都会阅读。这就决定了我们可以尝试缓存20%的每日照片和元数据的阅读量。