分布式存储的元数据设计

阅读数:3785 2015 年 6 月 9 日

话题:云计算DevOps语言 & 开发架构

虽然分布式存储的存储层和上传下载这一层很重要,但在元数据方面有哪些选择,这些选择有什么优缺点则更为重要。在 QCon 北京 2015 大会上,七牛云存储首席架构师李道兵结合自己多年的实践和思考,分享了关于存储设计的几大方法(幻灯片),并详细地分析了各自的利弊。本文由杨爽和李巧伶根据演讲内容整理而成。

常规的存储设计方法主要有以下几类。

  • 无中心的存储设计,如 GlusterFS。
  • 有中心的存储设计,如 Hadoop。
  • 基于数据库的存储设计,如 GridFS 和 HBase。
  • 绕过元数据的存储设计,如 FastDFS。

下面我们逐一讲述。

目前,GlusterFS 在互联网行业中用的较少,在大型计算机中用的较多。它在设计上具有以下几个特点。

  • 兼容 POSIX 文件系统:单机上的应用程序无需修改就可以放到 GlusterFS 运行。
  • 没有中心节点:不存在单点故障和性能瓶颈。
  • 扩展能力:因为没有中心节点,所以 GlusterFS 的扩展能力无限,扩展多少台服务器都没有问题。

我们不讨论 POSIX 兼容的优劣,集中通过 GlusterFS 来讨论无中心节点设计中普遍遇到的一些难点。

1. 坏盘修复。无中心暗示着可通过 key 推算这个 key 的存储位置。但是,磁盘坏了(单盘故障)怎么办呢?直接的处理方式是去掉坏盘,换上新盘,将这些数据拷贝过来。这样的处理方式在小型系统上非常适用。但是一个 4T 的 Sata 盘,即使在千兆网下写入速度也只能达到 100MB/s,修复时间会是 11 小时。考虑到一般的存储满指的是全部存储量的 80%,那么修复时间将达到八到九个小时,所以不适合用于大型系统。大型系统磁盘多,每天会坏很多块磁盘。如果磁盘是同一批购买的,由于对称设计,读写的特性很相似,那么在坏盘修复的八九个小时里,其他磁盘同时坏的概率非常高。这时,整个系统的可靠性会比较低,只能达到 6 个 9 或 7 个 9。如果想达到更高的可靠性,只能通过疯狂增加副本数这种会让成本大量提高的方法来解决。

GlusterFS 本身不用这种修复设计,它的修复方式是在读磁盘时发现有坏块就触发修复,但这种设计的修复时间会更长。该如何回避掉这个问题呢?最简单的方法是将磁盘分成多个区,确保每个区尽量小。例如,将 4T 盘分成 50 个区,每个区只有 80GB,并且记录各个区的一些元信息。在我们从 key 推算存储位置时,先计算出这个 key 所在区的编号,再通过刚才的元信息获得这个区对应的机器、磁盘和目录。这样带来的好处是,在磁盘坏了的时候,不用等换盘就可以开始修复,并且不一定要修复到同一块磁盘上,可以修复到有空间的多块磁盘上,从而得到一种并发的修复能力。如果将 4T 盘分成 50 个区(每个区 80G),那么修复时间就可以从 8 到 9 个小时缩减到 13 分钟左右。

2. 扩容。如前所述,在无中心节点的设计中,从 key 可以推算它存储的位置,并且我们要求 key 是均匀 Hash 的。这时,如果集群规模从一百台扩容到二百台,会出现什么问题呢?Hash 是均匀的,意味着新加的一百台机器的存储负载要和以前的一致,有一半的数据要进行迁移。但集群很大时,数据迁移过程中所花的时间通常会很长,这时会出现网络拥塞。同时数据迁移过程中的读写逻辑会变得非常复杂。解决方法是多加一些测试,改善代码质量,不要出 BUG。还有一种方法是,保持容量固定,尽量不要扩容,但这与互联网的风格(从很小的模型做起,慢慢将它扩大化)不相符。

3. 不支持异构存储。提供云存储服务的公司必然会面临一个问题,有很多客户存储的文件非常小,而另外很多客户存储的文件非常大。小文件通常伴随着很高的 IOPS 需求,比较简单的做法是将 Sata 盘(100 IOPS)换成 SAS 盘 (300 IOPS) 或者 SSD 盘 (10000 IOPS 以上) 来得到更高的 IOPS。但是对于无中心存储,由于 key 是 Hash 的,所以根本不知道小文件会存在哪里,这时能提高 IOPS 的唯一办法是加强所有机器的能力。例如,每台机器中将 10 块 Sata 盘、2 块 SAS 盘和 1 块 SSD 盘作为一个整体加入缓存系统,提升系统的 IOPS,但整体的成本和复杂性都会增加很多。另外一种方式是拼命扩大集群规模,这样整体的 IOPS 能力自然会比较高。

类似的异构需求还包括某些客户数据只想存两份,而其他客户数据则想多存几份,这在无中心存储中都很难解决。

4. 数据不一致的问题。比如要覆盖一个 key,基于 hash 意味着要删除一个文件,再重新上传一个文件,新上传的文件和之前的一个文件会在同一块磁盘的同一个目录下使用同样的文件名。如果覆盖时出现意外,只覆盖了三个副本中的两个或者一个,那么就很容易读到错误的数据,让用户感觉到覆盖没有发生,但原始数据已经丢失了。这是最难容忍的一个问题。解决方案是在写入文件时,先写一个临时文件,最后再重命名修改。但由于是在分布式架构中,且跨机器操作,会导致逻辑的复杂性增加很多。

简单总结一下,以 GlusterFS 为代表的无中心设计带来的一些问题如下图所示。

Hadoop 的设计目标是存大文件、做 offline 分析、可伸缩性好。Hadoop 的元数据存储节点(NameNode 节点)属于主从式存储模式,尽量将数据加载到内存,能提高对数据的访问性能。另外,放弃高可用,可进一步提高元数据的响应性能(NameNode 的变更不是同步更新到从机,而是通过定期合并的方式来更新)。

Hadoop 具有以下几方面优点。

1. 为大文件服务。例如在块大小为 64MB 时,10PB 文件只需要存储 1.6 亿条数据,如果每条 200Byte,那么需要 32GB 左右的内存。而元信息的 QPS 也不用太高,如果每次 QPS 能提供一个文件块的读写,那么 1000QPS 就能达到 512Gb/s 的读写速度,满足绝大部分数据中心的需求。

2. 为 offline 业务服务,高可用服务可以部分牺牲。

3. 为可伸缩性服务,可以伸缩存储节点,元信息节点无需伸缩。

然而有如此设计优点的 Hadoop 为什么不适合在公有云领域提供服务呢?

1. 元信息容量太小。在公有云平台上,1.6 亿是个非常小的数字,单个热门互联网应用的数据都不止这些。1.6 亿条数据占用 32GB 内存,100 亿条数据需要 2000GB 内存,这时 Hadoop 就完全扛不住了。

2. 元信息节点无法伸缩。元信息限制在单台服务器,1000QPS 甚至是优化后的 15000QPS 的单机容量远不能达到公有云的需求。

3. 高可用不完美。就是前面所提到的 NameNode 问题,在业务量太大时,Hadoop 支撑不住。

因此,做有中心的云存储时,通常的做法是完全抛弃掉原先的单中心节点设计,引入一些其他的设计。

常见的是 WRN 算法,为数据准备多个样本,写入 W 份才成功,成功读取 R 份才算成功,W+R 大于 N(其中 N 为总副本数)。这样就解决了之前提到的高可用问题,任何一个副本宕机,整个系统的读写都完全不受影响。

在这个系统里,利用这种技术有以下几个问题需要注意:

  1. W,R,N 选多少合适

第一种选择 2,2,3,一共三副本,写入两份成功,读取时成功读取两份算成功。但是其中一台机器下线的话,会出现数据读不出来的情况,导致数据不可用。

第二种选择 4,4,6 或者 6,6,9,能够容忍单机故障。但机器越多,平均响应时间越长。

  1. 失败的写入会污染数据

比如 4,4,6 的场景,如果写入只成功了 3 份,那么这次写入是失败的。但如果写入是覆盖原来的元数据,就意味着现在有三份正确的数据,有三份错误的数据,无法判断哪份数据正确,哪份数据错误。回避的方法是写入数据带版本(不覆盖,只是追加),即往数据库里面插入新数据。有些云存储的使用方法是对一个文件进行反复的修改,比如用 M3U8 文件直播。随着直播进程的开始不停地更改文件,大概 5 秒钟一次,持续一个小时文件会被修改 720 次。这时候从数据库读取文件信息时,就不再是读 1 条记录,而是每台读取 720 条记录,很容易导致数据库出现性能瓶颈。

下面,我们简单总结一下以 Hadoop 为代表的有中心的存储设计存在的问题。

GridFS

GridFS 基于 MongoDB,相当于一个文件存储系统,是一种分块存储形式,每块大小为 255KB。数据存放在两个表里,一个表是 chunks,加上元信息后单条记录在 256KB 以内;另一个表是 files,存储文件元信息。

GridFS 的优点如下所述。

1. 可以一次性满足数据库和文件都需要持久化这两个需求。绝大部分应用的数据库数据需要持久化,用户上传的图片也要做持久化,GridFS 用一套设施就能满足,可以降低整个运维成本和机器采购成本。

2. 拥有 MongoDB 的全部优点:在线存储、高可用、可伸缩、跨机房备份;

3. 支持 Range GET,删除时可以释放空间(需要用 MongoDB 的定期维护来释放空间)。

GridFS 的缺点如下所述。

1.oplog 耗尽。oplog 是 MongoDB 上一个固定大小的表,用于记录 MongoDB 上的每一步操作,MongoDB 的 ReplicaSet 的同步依赖于 oplog。一般情况下 oplog 在 5GB~50GB 附近,足够支撑 24 小时的数据库修改操作。但如果用于GridFS,几个大文件的写入就会导致 oplog 迅速耗尽,很容易引发 secondary 机器没有跟上,需要手工修复,大家都知道,MongoDB 的修复非常费时费力。简单来说,就是防冲击能力差,跟数据库的设计思路有关,毕竟不是为文件存储设计的。除了手工修复的问题,冲击还会造成主从数据库差异拉大,对于读写分离,或者双写后再返回等使用场景带来不小的挑战。

2. 滥用内存。MongoDB 使用 MMAP 将磁盘文件映射到内存。对于GridFS 来说,大部分场景都是文件只需读写一次,对这种场景没法做优化,内存浪费巨大,会挤出那些需要正常使用内存的数据(比如索引)。这也是设计阻抗失配带来的问题。

3. 伸缩性问题。需要伸缩性就必须引入 MongoDB Sharding,需要用 files_id 作 Sharding,如果不修改程序,files_id 递增,所有的写入都会压入最后一组节点,而不是均匀分散。在这种情况下,需要改写驱动,引入一个新的 files_id 生成方法。另外,MongoDB Sharding 在高容量压力下的运维很痛苦。MongoDB Sharding 需要分裂,分裂的时候响应很差,会导致整个服务陷入不可用的状态。

GridFS 的总结如下。

HBase

前面提到 Hadoop 因为 NameNode 容量问题,所以不适合用来做小文件存储,那么 HBase 是否合适呢?

HBase 有以下优点。

1. 伸缩性、高可用都在底层解决了。

2. 容量很大,几乎没有上限。

但缺点才是最关键的,HBase 有以下缺点。

1. 微妙的可用性问题。首先是 Hadoop NameNode 的高可用问题。其次,HBase 的数据放在 Region 上,Region 会有分裂的问题,分裂和合并的过程中 Region 不可用,不管用户这时是想读数据还是写数据,都会失败。虽然可以用预分裂回避这个问题,但这就要求预先知道整体规模,并且 key 的分布是近均匀的。在多用户场景下,key 均匀分布很难做到,除非舍弃掉 key 必须按顺序这个需求。

2. 大文件支持。HBase 对 10MB 以上的大文件支持不好。改良方案是将数据拼接成大文件,然后 HBase 只存储文件名、offset 和 size。这个改良方案比较实用,但在空间回收上需要补很多开发的工作。

应对方案是 HBase 存元数据,Hadoop 存数据。但是,Hadoop 是为 offline 设计的,对 NameNode 的高可用考虑不充分。HBase 的 Region 分拆和合并会造成短暂的不可用,如果可以,最好做预拆,但预拆也有问题。如果对可用性要求低,那么这种方案可用。

Hadoop 的问题是 NameNode 压力过高,那么 FastDFS 的思路是给 NameNode 减压。减压方法是将 NameNode 的信息编码到 key 中。对于范例URL:group1/M00/00/00/rBAXr1AJGF_3rCZAAAAEc45MdM850_big.txt来说,NameNode 只需要做一件事,将 group1 翻译成具体的机器名字,不用关心 key 的位置,只要关心组的信息所在的位置,而这个组的信息就放在 key 里面,就绕过了之前的问题。

FastDFS 的优点如下。

1. 结构简单,元数据节点压力低;

2. 扩容简单,扩容后无需重新平衡。

FastDFS 的缺点如下。

1. 不能自定义 key,这对多租户是致命的打击,自己使用也会降低灵活性。

2. 修复速度:磁盘镜像分布,修复速度取决于磁盘写入速度,比如 4TB 的盘,100MB/s 的写入速度,至少需要 11 个小时。

3. 大文件冲击问题没有解决。首先,文件大小有限制(不能超过磁盘大小);其次,大文件没有分片,导致大文件的读写都由单块盘承担,所以对磁盘的网络冲击很大。

几种存储设计可以总结如下。