Step 1 - 明确需求

以下是一些可供参考的讨论点:

  1. 主要特性是什么?=> 支持上传和下载文件,文件同步,以及通知。
  2. 移动端 or web应用 or both ? => both。
  3. 支持哪些文件类型?=> 所有类型。
  4. 文件需要加密存储吗?=> 需要。
  5. 文件大小有限制吗?=> 不超过10GB。
  6. 日活多少?=>1000万。

这里以下列需求作为设计目标:

  • 上传文件。
  • 下载文件。
  • 多端自动同步。
  • 文件历史记录。
  • 文件分享。
  • 当文件被修改,删除,或被共享给你时,发送推送消息。

以下特性暂不支持:

  • 文档在线协作。

除以上需求外,非功能性需求也要得到保证:

  • 可靠性,绝不能导致数据丢失。
  • 快速同步。
  • 带宽友好,不能太占用带宽。
  • 可扩展,能够支持大流量并发。
  • 高可用性,即使服务器掉线,用户也需要能够使用部分功能,比如编辑本地文件。

规模预估:

  • 假设应用有5000万注册用户以及1000万日活用户。
  • 每位用户有10GB免费使用空间。
  • 假设用户平均每天上传2个文件,文件平均大小为500KB。
  • 读写比例为1:1。
  • 总空间占用:5000万 * 10GB = 500PB。
  • 上传QPS:1000万 * 2 / 24 / 3600 ~= 240
  • 峰值上传QPS:QPS * 2 = 480

Step 2 - 概要设计

从单服务器开始,逐渐将系统拓展到支持百万级用户。单服务器设计如下:

  • 一台服务器用于上传和下载文件。
  • 一个数据库用于保存元数据,比如用户配置,登录信息,文件信息等。
  • 一个文件存储系统用于存储文件。

简单地使用一台Apache web服务器和MySql数据库,以及一个本地路径 drive/ 作为根目录即可实现上面的系统。在 drive/ 目录下,每个用户对应一个文件夹,每个文件夹存储对应用户的文件,文件名与用户上传的原始文件名相同。以下是这个系统的示例:

API设计

主要需要3个API:上传文件,下载文件,获取文件历史 。

1. 上传文件

包含两种上传:

  • 简单上传,适合小文件
  • 断点重传,适合大文件

以下是一个断点重传的API示例:

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

包含上传类型和本地文件数据两个参数。

一次断点重传的流程如下:

  • 发送初始请求以获取重传URL。
  • 上传文件,监测上传进度。
  • 如果上传被打断,则恢复传输。

2.下载文件

示例API: https://api.example.com/files/download

参数:

  • path: 文件路径

示例参数:

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

3.获取文件历史记录

示例API: https://api.example.com/files/list_revisions

参数:

  • path: 文件路径
  • limit: 返回的最大历史记录数


以上所有的API都需要支持HTTPS,以保证数据是加密传输。

多服务器设计

与其自建服务器分片与复制,不如将数据存储交给现有的云服务商来实现,这样可以借助云服务商成熟的服务和多数据中心来保障数据安全。以Amazon S3为例,Amazon S3支持同数据中心内的数据库复制和跨数据中心的数据库复制,通过将数据在多地进行冗余备份,可以有效保证数据安全及服务可用。

除了多地备份外,以下措施也必不可少:

  • 负载均衡:增加负载均衡以平均分布网络流量,以及容错。
  • Web服务器集群:根据流量动态添加或删除服务器。
  • 元数据库:将数据库独立出来,以避免单点故障,同时,使用分片和数据库复制技术,以实现可用性和扩展需求。
  • 文件存储:使用Amazon S3来存储文件,为了保证可用性和可靠性,可以将文件在两地进行冗余备份存储。

经过以上措施的优化后,系统设计调整如下:


同步冲突

当两个用户同时修改了同一个文件时,冲突发生。这里我们以第一次修改提交为准,第二次提交被视为冲突,如下:

当用户2收到冲突通知时,服务器会一并返回服务器上最新的版本,由用户2自己来解决冲突。

总体设计

以下是各组件介绍:

用户:包括客户端,浏览器,移动APP。

块服务器:用于将存储块上传到云存储。块级存储是指在云环境下将数据文件分块进行存储,每个块都包含一个唯一的哈希值,这个哈希值存储在元数据库中。对于云存储来说,每个块都是独立的,为了重建文件,需要将所有的块按顺序联合起来。块的大小可参考Dropbox,它的块大小是4MB。

云存储:存储数据块。

负载均衡:平摊请求到API服务器上。

API服务器:负责除了上传文件之外的全部请求,比如认证,用户配置,更新文件元数据等。

元数据库:存储用户、文件、存储块、版本信息等的元数据,不存储具体文件。

元数据缓存:缓存部分元数据,用于快速读取。

通知推送服务:用于推送事件通知,比如文件添加、删除、修改等事件。

离线备份队列:如果客户离线,无法拉取服务端文件的最新修改,那么离线备份队列会将这些修改存入队列,以便于在客户重新上线时推送文件修改。

Step 3 - 详细设计

深入探讨块服务器,元数据库,上传流程,下载流程,通知推送,节省存储空间,以及错误处理。

块服务器

对于大文件,每次更新时都上传整个文件需要占用大量带宽,对此,可以从下面的两方面进行优化:

  • 增量同步,每次修改时只上传修改过的部分。
  • 数据压缩。使用数据压缩可显著缩小数据的大小,因此,数据块需要以一定的算法进行压缩之后再进行存储,比如使用gzip或者bzip2对文本文件进行压缩。对于音视频文件,可采用其他合适的压缩算法。

在当前系统中,块服务器承载了文件上传的全部压力。块服务器会把用户上传的文件分成合适的块,然后压缩并加密,块服务器还负责只上传那些被修改过的块。通过增量同步和数据压缩,可显著降低服务器的带宽占用。

高一致性要求

本系统天然需要强一致性,因为同一时间用户看到的文件必须是相同的,系统要求在元数据缓存和数据库层保证强一致性。

缓存一般使用最终一致性模型,意味着不同副本可能不一样,为了实现强一致性,必须保证以下两点:

  • 数据在缓存中和在主服务器中必须保持一致
  • 写数据库采用无缓存操作,以保证数据库中的值和元数据缓存一致。

在关系型数据库中实现强一致性相对简单,因为关系型数据性支持ACID属性。但是非关系型数据库一般不支持ACID属性,需要由软件来保证在同步时能保持强一致性。在本系统设计中,我们使用关系型数据库,以支持ACID。

元数据库

以下是一个简化的元数据库的表设计:

包含以下组件:

用户表:包含用户的基本信息,比如用户名,邮件地址,头像等。

设备表:存储设备信息,通过device_id来发送和接收推送消息。用户可能有多个设备。

工作空间:用户的根目录。

文件:存储文件的最新信息。

文件历史:存储文件的历史记录,现有行必须是只读的,以保证文件历史的完整性。

块:存储文件块的信息。任何版本的文件都可以通过块来组装。

上传流程

这里有两个请求并行:上传元数据及上传文件到云存储,两个请求都从用户1中发出,用户2只用于接收推送消息。

  • 上传元数据。
    • 用户1发送上传新文件元数据请求。
    • API服务器将元数据存储到元数据库中,并设置文件的上传状态为pending。
    • 通知推送服务有一个新文件正在上传。
    • 推送服务将文件正在上传的消息推送给另一个用户2。
  • 上传文件到云存储
    • 用户1上传文件到块服务器。
    • 块服务器将文件分块,压缩,加密,然后存储到云存储。
    • 一旦文件完成上传,云存储触发任务完成回调,相关请求被发送到API服务器。
    • API服务器将元数据库中的文件状态改为 uploaded。
    • 通知推送服务文件已上传成功。
    • 推送服务将文件已上传成功的消息推送给另一个用户2。

下载流程

当文件被新增或修改时,会触发下载流程。首先是一个客户端如何知道文件被修改另一个客户端修改的问题,这里有两种方式:

  • 如果用户A在线且文件被另一个用户修改,那么推送服务可以通知用户A文件已被修改,需要发起下载流程。
  • 如果用户A不在线且文件被另一个用户修改,那么数据会被保存在缓存中,当离线用户上线时,会主动拉起最新修改。

一旦用户知道文件已被修改,首先会通过API服务器向元数据库发起请求,然后下载相应的块以在本地重组文件,以下是下载流程:

  1. 推送服务器通知用户2文件在别处被修改。
  2. 一旦用户2知道有可用更新,会首先向API服务器请求元数据。
  3. API服务器查询元数据库。
  4. 元数据库返回文件元数据修改。
  5. API服务器将元数据修改返回给用户。
  6. 一旦用户获取到元数据,会向块服务器发起下载请求。
  7. 块服务器从云存储中下载块。
  8. 云存储返回块。
  9. 用户下载全部的块并在本地重组文件。

通知推送

为了保证强一致性,任何的修改都必须通知其他用户,以降低冲突风险,通知推送服务可以用于实现该目的。从高层次来说,推送服务可以在事件发生时将数据发送给用户,以下是一些可选项:

  • 长轮徇,Dropbox使用这种方式
  • WebSocket。WebSocket可以在客户端与服务器之音建立持久的双向连接。

这里我们倾向于选择长轮徇,原因如下:

  • 通知推送并不是双向的,只从服务器发向客户端,但反过来不会。
  • SebSocket适用于实时双向应用,比如聊天应用。对于Google Drive来说,通知并不会频繁发送,也不存在数据突发问题。

通知长轮徇,每个用户都和推送服务器维持一个长轮徇链接。当文件修改被客户端检测到时,客户端会首先关闭长轮徇链接,并下载最新的文件,然后重新建立长连接。

节省存储空间

为支持文件历史版本以及保证可靠性,相同文件的多个历史版本会被保存在不同的数据中心,这会占用大量的存储空间。以下三点可以节约存储:

  • 数据块去重。在用户层去掉重复的数据块,使用哈希值作为判断标准。
  • 使用更加智能的备份策略,以下两项优化可用:
    • 限制历史版本的数量,超出限制时,旧的历史版本文件会被删除。
    • 只保存重要的历史版本,适用于频繁修改的文件。
  • 将不常用的数据移动到冷存储上,这可以降低云存储的费用。

错误处理

以下错误处理可供讨论:

  • 负载均匀服务器错误:将流量转移到备用的负载均匀服务器上。一般使用保活来维持在线,当主服务器保活超时时,启用备用服务器。
  • 块服务器错误:将剩余块的存储转移到其他块服务器上。
  • 云存储错误:S3云存储会将块存储在多个数据中心,如果其中一个不可用,可以从另一个获取。
  • API服务器错误:使用API层无状态设计,将请求转移到另一个可用的API服务器上。
  • 元数据缓存错误:元数据缓存服务器使用复制技术,将请求转移到另一个服务器 。
  • 元数据库错误:
    • 主数据库掉线:将一台从数据库临时提升为主数据库。
    • 从数据库掉线:将请求转移到另一台从数据库上。
  • 通知推送错误:使用长轮徇时单个服务器上可能有上百万个用户的连接,如果服务器掉线,这些长连接都会丢失,则服务器可能需要花很长时间重建这些连接。一种处理办法是客户端主动向另一个服务器建立连接。
  • 离线备份队列错误:使用复制技术,将用户订阅转移到另一台备份队列服务器上。

Step 4 - 总结

略略略。






















  • 无标签