跳至主要內容

15. 设计 Google Drive


15. 设计 Google Drive

Google Drive 是一种云文件存储产品,帮助您将文档、视频等存储在云端。

您可以从任何设备访问它,并与朋友和家人共享。

第一步:理解问题并确定设计范围

  • 候选人:最重要的功能是什么?
  • 面试官:上传/下载文件、文件同步和通知功能。
  • 候选人:是移动端还是网页端?
  • 面试官:两者都需要支持。
  • 候选人:支持哪些文件格式?
  • 面试官:支持任何文件类型。
  • 候选人:文件需要加密吗?
  • 面试官:是的,存储中的文件需要加密。
  • 候选人:是否有文件大小限制?
  • 面试官:是的,文件大小限制为 10 GB。
  • 候选人:应用的用户量有多少?
  • 面试官:每天活跃用户(DAU)为 1000 万。

我们关注的功能包括:

  • 上传文件
  • 下载文件
  • 跨设备同步文件
  • 查看文件修订历史
  • 与朋友共享文件
  • 文件被编辑/删除/共享时发送通知

不讨论的功能:

  • 协作编辑

非功能性需求:

  • 可靠性:数据丢失是不可接受的。
  • 同步速度:快速的同步速度。
  • 带宽使用:如果应用消耗过多网络流量或电量,用户会不满。
  • 可扩展性:需要处理大量流量。
  • 高可用性:即使部分服务宕机,用户仍然应能使用系统。

粗略估算

  • 假设有 5000 万注册用户,日活跃用户(DAU)为 1000 万。
  • 用户可获得 10 GB 免费存储空间。
  • 用户每天上传 2 个文件,平均文件大小为 500 KB。
  • 读写比为 1:1。
  • 总分配空间:50M 用户 * 10GB = 500 PB
  • 上传 API 的每秒请求数(QPS):1000 万用户 * 2 文件 / 24 小时 / 3600 秒 ≈ 240
  • 峰值 QPS:480

第二步:提出高层设计并获得认可

这次我们将采用不同的方法——从单一服务器开始设计,并逐步扩展。

我们将从以下内容开始:

  • 一个用于上传和下载文件的 Web 服务器。
  • 一个用于跟踪元数据的数据库,包括用户数据、登录信息、文件信息等。
  • 一个存储系统,用于存储文件。

示例存储系统如下:

storage-example
storage-example

接口设计 (APIs)

上传文件:

https://api.example.com/files/upload?uploadType=resumable

该端点用于上传文件,支持简单上传和断点续传(用于大文件)。
断点续传通过获取上传 URL 并监控上传状态实现,如果中断,可以恢复上传。

下载文件:

https://api.example.com/files/download

参数指定要下载的文件:

{
    "path": "/recipes/soup/best_soup.txt"
}

获取文件修订历史:

https://api.example.com/files/list_revisions

参数:

  • 文件路径,用于检索修订历史。
  • 返回的最大修订数量。

所有接口需要身份验证,并使用 HTTPS。


从单一服务器扩展

随着文件上传量的增加,存储服务器的容量会被耗尽。

一种扩展存储服务器的方法是实现分片——每个用户的数据存储在单独的服务器上:

sharding-example
sharding-example

这解决了存储容量问题,但数据丢失仍然是一个潜在风险。

一个不错的解决方案是使用现成的解决方案,比如 Amazon S3,它支持同一区域和跨区域的复制:

amazon-s3
amazon-s3

其他改进点:

  • 负载均衡:确保将网络流量均匀分配到 Web 服务器副本上。
  • 增加 Web 服务器数量:有了负载均衡器,可以通过添加更多服务器轻松扩展 Web 服务器层。
  • 元数据数据库:将数据库从服务器中移出,避免单点故障。可以设置复制和分片以满足可扩展性需求。
  • 文件存储:使用 Amazon S3 作为存储系统。为确保可用性和耐用性,文件会复制到两个独立的地理区域。

更新后的设计如下:

updated-simple-design
updated-simple-design

同步冲突

当用户量足够大时,同步冲突不可避免。

我们可以应用“先修改者优先”的策略:

sync-conflict
sync-conflict

发生冲突时,会生成文件的第二版本,用户可以自行合并:

sync-conflict-example
sync-conflict-example

高层设计

high-level-design
high-level-design

设计细节:

  • 用户通过浏览器或移动应用使用系统。
  • 块服务器:将文件上传到云存储。块存储技术允许将大文件拆分为块并存储在后端存储中。例如,Dropbox 将块大小设置为 4MB。
  • 云存储:将拆分为多个块的文件存储在云存储中。
  • 冷存储:存储不活跃文件(不常访问的文件)。
  • 负载均衡器:将请求均匀分配给 API 服务器。
  • API 服务器:负责上传文件以外的所有操作,包括身份验证、用户资料管理和更新文件元数据等。
  • 元数据数据库:存储上传到云存储的文件的元数据。
  • 元数据缓存:缓存部分元数据以快速检索。
  • 通知服务:发布/订阅系统,在文件更新/编辑/删除时通知用户,以便他们拉取最新更改。
  • 离线备份队列:用于为离线用户排队文件更改,当他们重新上线时可以同步。

第三步:深入设计

我们将探讨以下内容:

  • 块服务器
  • 元数据数据库
  • 上传/下载流程
  • 通知服务
  • 节省存储空间
  • 故障处理

块服务器

对于大型文件,每次更新时发送整个文件是不现实的,因为会消耗大量带宽。

我们将探讨两种优化方式:

  • 增量同步:一旦文件被修改,仅将修改的块发送到块服务器,而不是整个文件。
  • 压缩:对块应用压缩可以显著减少数据大小。不同的文件类型适合不同的算法,例如,对于文本文件,可以使用 gzip/bzip2。

除了将文件分块,块服务器还在存储文件之前对其进行加密:

块服务器深度解析
块服务器深度解析

增量同步示例:

增量同步
增量同步

高一致性需求

我们的系统需要强一致性,因为向不同用户展示文件的不同版本是不可接受的。

这主要在使用缓存时成为问题,尤其是元数据缓存。
为了保持强一致性,我们需要:

  • 确保缓存主节点与副本保持一致。
  • 在数据库写入时使缓存失效。

对于数据库,只要使用支持 ACID 的关系型数据库,通常可以保证强一致性。


元数据数据库

以下是元数据数据库的简化表结构(仅显示关键字段):

元数据数据库深度解析
元数据数据库深度解析
  • 用户表:包含用户的基本信息,例如用户名、电子邮件、个人资料照片等。
  • 设备表:存储设备信息。Push_id 用于发送推送通知。每个用户可以有多个设备。
  • 命名空间表:用户的根目录。
  • 文件表:存储与文件相关的所有信息。
  • 文件版本表:存储文件的版本历史。已有字段是只读的,以保持文件完整性。
  • 块表:存储与文件块相关的所有信息。可以通过连接正确版本的所有块来重建文件版本。

上传流程

上传流程
上传流程

在上述流程中,有两个请求并行发送:更新文件元数据和将文件上传到云存储。

添加文件元数据:

  • 客户端 1 发送请求更新文件元数据。
  • 新的文件元数据被存储,并将上传状态设置为“pending”(待定)。
  • 通知服务被告知新文件正在上传。
  • 通知服务通知相关客户端文件上传状态。

将文件上传到云存储:

  • 客户端 1 将文件内容上传到块服务器。
  • 块服务器将文件分块、压缩、加密并上传到云存储。
  • 文件上传完成后,触发上传完成回调。请求发送到 API 服务器。
  • 在元数据数据库中将文件状态更改为“uploaded”(已上传)。
  • 通知服务收到文件上传事件,并通知客户端 2 新文件已上传。

下载流程

当文件在其他地方被添加或编辑时,会触发下载流程。客户端通过以下方式获取通知:

  • 在线时接收通知。
  • 离线时,变更被缓存,直到用户上线。

一旦客户端收到变更通知,它会请求文件元数据并下载块以重建文件:

下载流程
下载流程
  • 通知服务告知客户端 2 文件变更。
  • 客户端 2 从 API 服务器获取元数据。
  • API 服务器从元数据数据库获取元数据。
  • 客户端 2 收到元数据。
  • 收到元数据后,客户端向块服务器发送请求下载块。
  • 块服务器从云存储下载块并转发给客户端。

通知服务

通知服务使文件变更能够实时传达给客户端。

客户端可以通过以下方式与通知服务通信:

  • 长轮询(例如 Dropbox 使用这种方式)。
  • WebSocket:通信是持久的、双向的。

这两种方式都很好,但我们选择长轮询,因为:

  • 通知服务的通信不是双向的。服务器向客户端发送信息,而不是相反。
  • WebSocket 适用于实时双向通信。而对于 Google Drive,通知发送的频率并不高。

在长轮询中,客户端向服务器发送请求,请求保持打开状态,直到接收到变更或超时。
之后,客户端发送后续请求以获取下一批变更。


节省存储空间

为了支持文件版本历史并确保可靠性,多个文件版本会存储在多个数据中心。

存储空间可能很快被填满。可以采用以下三种技术节省存储空间:

  • 数据块去重:如果两个块具有相同的哈希值,只需存储一次。
  • 采用智能备份策略:对最大版本历史设置限制,并将频繁的编辑合并为单个版本。
  • 将不常访问的数据移动到冷存储:例如,Amazon S3 Glacier 是一个很好的选择,比 Amazon S3 便宜得多。

故障处理

一些典型的故障及其解决方法:

  • 负载均衡器故障:如果负载均衡器失败,备用负载均衡器会激活并接管流量。
  • 块服务器故障:如果块服务器失败,其他副本接管流量并完成任务。
  • 云存储故障:S3 存储桶会在不同区域间复制。如果一个区域失败,流量会重定向到另一个区域。
  • API 服务器故障:负载均衡器将流量重定向到其他服务实例。
  • 元数据缓存故障:元数据缓存服务器有多个副本。如果一个节点宕机,其他节点仍然可用。
  • 元数据数据库故障:如果主节点宕机,将其中一个从节点提升为主节点。如果从节点宕机,使用其他节点进行读取操作。
  • 通知服务故障:如果长轮询连接丢失,客户端重新连接到其他服务副本,但数百万客户端的重新连接可能需要一些时间。
  • 离线备份队列故障:队列有多个副本。如果一个队列失败,消费者需要重新订阅备用队列。

第四步:总结

我们的 Google Drive 系统设计的特性一览:

  • 强一致性
  • 低网络带宽占用
  • 快速同步

我们的设计包含两种流程——文件上传和文件同步。

如果有时间,还可以讨论替代设计方案,因为没有完美的设计。例如,可以直接将块上传到云存储,而不是通过块服务器。

这种方法比我们的方案更快,但也有一些缺点:

  • 分块、压缩、加密需要在不同平台(Android、iOS、Web)上实现。
  • 客户端可能会被黑客攻击,因此在客户端实现加密并不理想。

另一个有趣的讨论是将在线/离线逻辑移到单独的服务中,这样其他服务可以重用它来实现有趣的功能。