1. 概述
现代生活中已经离不开网盘,比如百度网盘。在使用网盘的过程中,有没有想过它是如何工作的?在本文中,我们将讨论如何设计像百度网盘这样的系统的基础架构。
2. 系统需求
2.1. 功能性需求
- 用户能够上传照片/文件。
- 用户能够创建/删除目录。
- 用户能够下载文件。
- 用户能够共享上传的文件。
- 能够在所有的用户设备之间同步数据。
- 即使网络不可用,用户也能上传文件/照片,只是存储在离线文件中,当网络可用时,离线文件将同步到在线存储。
2.2 非功能性需求
-
可用性: 指系统可用于处理用户请求的时间百分比。我们通常将可用性称为5个9、4个9。5个9意味着 99.999% 的可用性,4 个9意味着 99.99% 的可用性等。
-
持久性: 即使系统发生故障,用户上传的数据也应永久存储在数据库中。系统应确保用户上传的文件应永久存储在服务器上,而不会丢失任何数据。
-
可靠性: 指系统对于相同输入给出预期的输出。
-
可扩展性: 随着用户数量的不断增加,系统应该能处理不断增加的流量。
-
ACID: 原子性、一致性、隔离性和持久性。所有的文件操作都应该遵循这些属性。
- 原子性:对文件执行的任何操作都应该是完整的或不完整的,不应该是部分完整的。即如果用户上传文件,操作的最终状态应该是文件已 100% 上传或根本没有上传。
- 一致性: 保证操作完成之前和之后的数据是相同的。
- 隔离性:意味着同时运行的2个操作应该是独立的,并且不会影响彼此的数据。
- 持久性:参考第二点关于持久性的解释。
3. 容量估算
假设我们有 5 亿总用户,其中 1 亿是每日活跃用户。
那么,每分钟的活跃用户数:
1亿 / (24小时 * 60分钟)= 0.07万
再假设下高峰期每分钟有 100 万活跃用户,平均每个用户上传 5 个文件,则每分钟将有 500 万次上传。
如果1次上传平均100KB的文件,则1分钟上传的总文件大小为:
100KB * 5 = 500TB
4. API设计
4.1 上传文件
POST: /uploadFile
Request {
filename: string,
createdOnInUTC: long,
createdBy: string,
updatedOnInUTC: long,
updatedBy: string
}
Response: {
fileId: string,
downloadUrl: string
}
上传文件分为2步:
- 上传文件元数据
- 上传文件
4.2 下载文件
GET: /file/{fileId}
Response: {
fileId: string,
downloadUrl: string
}
通过返回的downloadURL进行文件下载。
4.3 删除文件
DELETE: /file/{fileId}
4.4 获取文件列表
GET: /folders/{folderId}?startIndex={startIndex}&limit={limit}
Response: {
folderId: string,
fileList: [
{
fileId: string,
filename: string,
thumbnail_img: string,
lastModifiedDateInUTC: string
creationDateInUTC: string
}
]
}
由于文件数量可能会很大,这里采用分页返回的方式。
5. 关键点的设计思考
- 文件存储: 我们希望系统具有高可用性和耐用性来存储用户上传的内容。为此,我们可以使用对象存储的系统作为文件存储,可选的有AWS的S3、阿里云的对象存储等。我们采用S3。
- 存储用户数据及其上传元数据: 为了存储用户数据及其文件元数据,我们可以使用关系型数据库和非关系型数据库结合的方式,关系型数据库采用MySQL, 非关系型数据库采用MongoDB。
- 离线存储: 当用户的设备离线时,用户完成的所有更新都将存储在其本地设备存储中,一旦用户上线,设备会将更新同步到云端。
- 上传文件: 用户上传的文件大小可能很大,为了将文件从任何设备上传到服务器而不出现任何失败,我们必须将其分段上传。目前常见的对象存储中都支持分段上传。
- 下载/共享文件: 通过分享文件的URL来实现共享和下载。如果文件存储是S3的话,也可以使用预签名的URL来实现此功能。
默认情况下,所有 S3 对象都是私有的,只有对象所有者有权访问它们。但是,对象所有者可以通过创建预签名 URL 与其他人共享对象。预签名 URL 使用安全凭证授予下载对象的限时权限。URL 可以在浏览器中输入或由程序使用来下载对象。
-
设备之间同步: 当用户在其中一台设备上进行更改时,当用户登录其他设备时,这些更改应同步在其他设备上。有两种方法可以做到这一点。
- 一旦用户从一台设备更新,其他设备也应该更新。
- 当用户登录时更新其他设备进行更新。
我们采用第二种方法,因为即使用户不使用其他设备,它也可以防止对其他设备进行不必要的更新。如果用户在两个不同的设备上在线怎么办?那么在这种情况下我们可以使用长轮询。用户当前在线的设备将长时间轮询后端服务器并等待任何更新。因此,当用户在一台设备上进行更新时,另一台设备也会收到更新。
6. 数据库设计
用户表
userId: string
username: string
emailId: string
creationDateInUtc: long
文件源数据表
fileId: string
userId: string
filename: string
fileLocation: string
creationDateInUtc: long
updationDateInUtc: long
7. 架构设计
-
File MetaData Service: 该服务负责添加/更新/删除用户上传文件的元数据。客户端设备将与此服务通信以获取文件/文件夹的元数据。
-
File Upload Service: 该服务负责将文件上传到 S3 存储桶。用户的设备将以块的形式将文件流式传输到此服务,一旦所有块都上传到 S3 存储桶,上传就会完成。
-
Synchronization Service: 同步服务,两种情况需要同步。
- 当用户在其设备上打开应用程序时,在这种情况下,我们将从同步服务同步用户的该设备与用户当前查看的目录的最新快照。
- 当用户从一个先后登录两个不同设备时,我们需要同步用户的第一个设备的数据,故而我们使用长轮询来轮询该目录/文件在服务器上的最新更改内容。
-
S3 存储桶: 我们使用 S3 存储桶来存储用户文件/文件夹。根据用户 ID 创建文件夹,每个用户的文件/文件夹可以存储在该用户的文件夹中。
-
Cache: 使用缓存来减少元数据检索的延迟,当客户端请求文件/文件夹的元数据时,它将首先查找缓存,如果在缓存中找不到,那么它将查找数据库。
-
负载均衡 : 我们希望我们的服务能够扩展到数百万用户,为此我们需要水平扩展我们的服务。我们将使用负载均衡器将流量分配到不同的主机。这里我们采用Nginx做负载均衡。
-
UserDevices: 用户可以使用移动设备、台式机、平板电脑等多种设备来访问驱动器。我们需要保证所有用户设备的数据都是相同的,并且不能存在数据差异。
8. 总结
本文讨论了如何设计一个网盘系统的架构,综合功能性需求和非功能性需求,设计了API、数据库和服务架构。但是没有讨论权限设计和数据安全的部分,也欢迎大家补充改进。