Paper翻译----GFS谷歌文件系统

目录

摘要:

 1. 介绍

2 设计概览

2.1 假设

2.2 接口

2.3 架构

2.4 单节点Master

2.5 区块尺寸(Chunk Size)

2.6 元数据

2.6.1 内存数据结构

2.6.2 区块位置

2.6.3 操作日志

2.7 一致性模型

2.7.1 GFS的保证

 2.7.2 对应用的启示(Implications for Applications)

3 系统交互

3.1 租约和修改顺序

3.2 数据流

3.3 原子性的记录追加

3.4 快照

4. master操作

4.1 命名空间管理和锁

4.2 副本的安置

4.3 创建、重新复制、重新平衡

4.4 垃圾收集

4.4.1 机制

4.4.2 讨论

4.5 变质副本的检测

5. 错误容忍和诊断

5.1 高可用

5.1.1 快速恢复

5.1.2 区块复制

5.1.3 master复制

5.2 数据完整性

5.3 诊断工具

6. 度量

6.1 微基准测试

6.1.1 读取

6.1.2 写入

6.1.3 记录追加

6.2 现实世界中的集群

6.2.1 存储

6.2.2 元数据

6.2.3 读和写速率

6.2.4 master的负载

6.2.5 恢复时间

6.3 工作负载分解

6.3.1 方法和注意事项

6.3.2 chunkserver的工作负载

6.3.3 追加Vs写入

6.3.4 master的工作负载

7. 经验

8. 相关工作

9. 结论

致谢

参考文献


==============================================================================

本技术论文的翻译工作由不动明王1984独自完成,特此声明。

翻译辛苦,珍惜劳动,引用时请注明出处!

==============================================================================

摘要:

我们设计并实现了Google文件系统,一个可扩展的分布式文件系统,可用于巨大的数据密集的应用中。其提供了在不昂贵的商用硬件上的错误容忍,并且能为大量的客户端提供很高的总体性能。

虽然与先前的分布式文件系统有很多相同的目标,但我们的设计还是被我们自己应用中的工作负载和技术环境的一些观察所驱动,其中既包括当前的也包括对未来预期的,这些观察反映出与早期文件系统的假设之间的一些标志性背离。这导致我们重新检视了传统的选择,并探索出一些根本上的不同设计点。

该文件系统成功的满足了我们的存储需求。其作为存储平台在Google内部被广泛的部署,用于产生并处理被我们的服务(services)以及需要在巨大的数据集上进行的研究和发展所使用的数据。目前为止最大的集群在上千台机器上的数千块磁盘上提供了数百TB的存储,并且其被数百个客户端并发的访问。

在本论文中,我们展示了被设计用于支持分布式应用的文件系统接口扩展、讨论了我们设计的很多方面、并从微基准测试(micro-benchmarks)及实际使用情况两方面报告了性能测试的一些度量值。

 1. 介绍

我们设计并实现了Google文件系统(GFS)以满足Google数据处理需求的快速增长。GFS与先前的分布式文件系统有很多相同的目标,例如性能、扩展性、可靠性以及可用性。然而,其设计还是被我们自己应用中的工作负载和技术环境的一些核心观察所驱动,这些观察既包括当前的也包括对未来预期的,其反映出与早期文件系统的假设之间的一些标志性的背离。我们重新检视了一下传统的选择,并在设计空间上探索出了一些根本上的不同设计点。

首先,组件失效是正常的情况而非异常。文件系统由数百甚至数千台不昂贵的商用部件构成的存储机器所组成,并由相当数量的客户端机器所访问。这些组件的数量和质量其实就保证了在任意给定时刻都会有一些是不能工作的,并且有一些是无法从错误中恢复的。我们看见过由应用程序bugs、操作系统bugs、人为错误、以及磁盘、内存、链接器、网络、还有供电设备所导致的问题。因此,持续的监控、错误检测、错误容忍、以及自动恢复等功能在系统中必须完备。

其次,在传统标准中文件是巨大的。数GB大小的文件很常见。每个文件一般包含了很多个应用层面的对象,例如web文档。当我们日常工作在快速增长的包含了数TB及数十亿对象的数据集时,管理数十亿在KB级别大小的文件是很笨重的,即使文件系统可以支撑这个量级。结果是,文件系统在设计时的假设和诸如I/O操作及块大小这样的参数需要再次拿出来讨论。

第三,大部分文件的修改都是追加新数据而非覆盖已存在数据。一个文件中的随机写入在实践中并不存在。一旦写入,文件就只会被读取,并且大部分时间是顺序的读取。很多数据都有这种特征。一些数据可能构成巨大的数据仓储供数据分析程序扫描。一些可能是由运行中的应用持续不断的产生的数据流。还有一些可能是归档数据。另外一些可能是在一台机器上产生的中间结果,并在另一台机上继续处理,无论是同时处理还是稍后处理。由于这种在巨大的文件上的访问模式,追加操作就成了性能优化和原子性保证的核心关注点,而将数据块缓存在客户端则失去了其吸引力。

第四,将应用和文件系统API放在一起考虑设计,这为我们带来更大的灵活性。例如,我们放松了GFS的一致性模型以最大的简化文件系统,同时并没有引起应用的巨大负担。我们也引入了一个原子的追加操作,如此一来多个客户端可以并发的追加数据到一个文件中,它们之间不用做额外的同步处理。这将在本文的后面被详细讨论。

当前,有多个GFS集群被部署于不同的目的。最大的集群有超过1000个存储节点,超过300TB的磁盘存储空间,并且被数百个位于不同机器上的客户端持续繁重的访问。

2 设计概览

2.1 假设

在为满足我们的需求而设计一个文件系统时,我们被一些假设所驱动,这些假设既提供了挑战也提供了机会。我们前面隐约的提到一些核心观察,现在在这里详述一下我们的假设。

  • 系统构建于很多不昂贵的商用组件,它们经常会失效。它必须持续的监控自己、检测、容错、并从组件失效中快速恢复,这些都应该是常规操作。
  • 系统存储了不太大数量的大文件。我们预计会有数百万的文件,每个文件往往有100MB或更大。数GB的文件是常见情况,其需要被高效的管理。小文件必须被支持,但是我们无需对其太过优化(这种情况不是重点)。
  • 工作负载主要由两种类型的读构成:大的流式读和小的随机读。在大的流式读中,单个操作往往会读取数百KB的数据,更常见的是1MB或更多。来自相同客户端的连续操作常常会读取文件的相连区域。一个小的随机读往往读取任意偏移上的几KB数据。关注性能的应用常常会批量处理并排序它们的小读取,以在文件中稳步前进,而不是前后的不断移动。
  • 工作负载同样包括很多巨大的、顺序写,其会将数据追加到文件中。典型的操作数据的尺寸会比读取的数据尺寸小。一旦写入,文件就很少再被修改。在文件任意位置上的小的写入要被支持,但是不需要高效。
  • 系统必须高效的实现一种被定义良好的语义,该语义支持多个客户端并发的往相同的文件中追加数据。我们的文件经常被作为一个生产者-消费者队列,或用于多路归并。数百个生产者,每个跑在一台机器上,会并发的往一个文件中追加数据。以最小同步代价实现的原子性至关重要。文件可能会在后面被读取,或者可能会有一个消费者在同时读取文件。
  • 较高的持续带宽比低延迟更加重要。我们绝大部分的目标应用都会触发大块数据的高速处理,而很少对于单个的读或写有苛刻的响应时间要求。

2.2 接口

GFS提供了一种常见的文件系统接口,虽然它没有实现标准的诸如POSIX这样的API。文件在目录中被组织为层次结构,并通过文件路径来标识。我们支持常见的操作来创建、删除、打开、关闭、读和写文件。

而且,GFS支持快照和记录追加操作。快照操作能够以很小的代价创建一个文件或目录树的拷贝。记录追加允许多个客户端并发的往相同文件中追加数据,同时保证每个客户端的每个追加操作的原子性。其在实现多路合并结果和生产者消费者队列时非常有用,这些场景下多个客户端可以往一个文件中同时执行追加而无需额外的同步锁。我们发现这些类型的文件对于构建大型分布式应用来说极其有用。快照和记录追加分别会在章节3.4和3.3中深入讨论。

2.3 架构

一个GFS集群由一个单点master和多个chunkservers构成,并可以由多个客户端访问,如图1所示。每个节点都是一个商用的Linux机器,运行着一个用户级别的server进程。很容易在相同的机器上同时运行一个chunkserver和一个客户端,如果机器资源允许并且由于运行一个行为古怪的应用代码而导致的低可靠性是可以接收的话。

文件被划分为固定尺寸的数据区块(chunks)。每一个数据区块都通过一个不可变的并且全局唯一的64位的区块句柄所标识,该区块句柄是由master在区块被创建时分配的。Chunkservers在本地磁盘上以Linux文件的方式保存区块,并通过一个区块句柄及区块内的字节范围来读或写区块数据。为了可靠性,每一个区块都被复制到多个chunkservers上。默认的,我们存储三个副本,而用户可以对文件命名空间的不同区域指定不同的复制级别。

master维护了所有的文件系统元数据。包括了命名空间、访问控制信息、从文件到区块的映射、以及所有区块的当前存储位置。master同样控制了整个系统范围的活动,例如区块租约的管理、孤儿区块的垃圾回收、以及在chunkservers之间的区块迁移。master周期性的与每一个chunkserver进行沟通,通过心跳消息收集chunkserver的状态并给其发送指令。

GFS客户端代码被嵌入到每一个应用中,其实现了文件系统的API并与master和chunkservers通信来为应用读或写数据。客户端与master交互以执行元数据操作,而所有涉及到数据的通信都直接发往chunkservers。我们不提供POSIX接口,因此无需挂钩到Linux的虚拟节点层。

客户端和chunkserver都不会缓存文件数据。客户端缓存只会带来非常有限的好处,因为大部分应用都会流式的访问巨大的文件,或者拥有太大而不适合缓存的工作集(working sets)。去掉这些缓存简化了客户端以及整个系统的实现,这消除了缓存一致性问题。(然而,客户端是会缓存元数据的。)Chunkservers不需要缓存文件数据是因为数据区块(chunks)作为本地文件存储,因此Linux的数据缓冲区(buffer cache)已经在内存中维护了经常访问的数据。

2.4 单节点Master

使用一个单节点的Master极大的简化了我们的设计,并使得master能够基于全局的知识来作出老练的数据区块位置安置及复制决定。然而,我们必须最小化其在读和写操作中的牵涉,这样它才不至于成为瓶颈。客户端永远不会通过master来读和写数据。相反,客户端会询问master它需要和哪些chunkservers沟通。客户端会在一段时间内缓存这些信息,因此在随后的操作中会直接和chunkservers交互,而不用再去询问master了。

让我们解释一下对于一个简单的读所需要的交互,参照图1。首先,使用固定的区块大小,客户端将应用指定的文件名和字节偏移(offset)转换为文件中的一个区块的索引。然后,其向master发送一个包含了文件名和区块索引(序号)的请求。master会将对应的区块句柄及其副本的具体存储位置返回。客户端会用文件名和区块索引作为key值来缓存该信息。

客户端随后向副本中的一个发送请求,最可能的是发送给“最近”的那个。请求中指定了区块句柄以及该区块内的一个字节范围。对相同区块的后续读取就不再需要客户端与master之间的交互了,直到缓存信息失效,或者文件被重新打开。实际上,客户端一般会在一个请求中询问多个区块的元数据,并且master也会在应答中包含紧跟着请求区块的若干个区块的信息。在实践中,这个额外的信息可以不花什么代价的避免了后续客户端与master之间可能的交互。

2.5 区块尺寸(Chunk Size)

区块尺寸是一个核心的设计参数。我们选择了64MB,这比典型的文件系统块要大得多。每一个区块副本都在一个chunkserver上以普通Linux文件的方式存储,并且只在需要时才会扩展(懒惰分配)。懒惰空间分配避免了由于内部碎片而造成的空间浪费,其可能是在如此一个区块尺寸下最大对象的尺寸。

一个大的区块尺寸提供了很多重要的优势。首先,其减少了客户端与master交互的次数,因为读写相同的区块只需要向master发送一次请求以获得区块的位置信息。这种降低在我们的工作负载中尤为明显,因为我们的应用绝大部分时间会顺序的读写大文件。即使对于小的随机读取,客户端也可以舒服的缓存一个数TB工作集的所有区块位置。其次,由于区块比较大,一个客户端更有可能在同一个区块上执行多个操作,这可以通过与chunkserver在一段时间内保持一个TCP长连接来降低网络负载。第三,其减少了master中需要维护的元数据尺寸。这允许我们将元数据整个保存在内存中,而这又反过来带来了其他的优势,这些优势我们将在章节2.6.1中讨论。

另一方面,一个较大的区块尺寸,即使使用了懒惰空间分配,也有其不利的方面。一个小文件会包含很少的区块,或许只有1个。如果很多客户端都访问相同的文件时,保存这些文件区块的chunkservers可能会变成热点。在实践中,访问热点并没有成为一个主要问题,因为我们的应用绝大部分时间会顺序的读取包含了多个区块的大文件。

然而,当GFS第一次被用于一个批队列系统时确实发生了热点:在该系统中一个可执行程序被写入一个单区块的文件,然后同时在数百台机器上被开始执行。存储该可执行程序的几个chunkservers就被几百个并发的请求给过载了。我们通过将这种可执行文件存储为更多的副本并错开应用启动的时间来解决了这个问题。一个可能的永久解决方案是在这种情况下允许客户端从其他客户端读取数据(p2p下载)。

2.6 元数据

master存储了三种主要类型的元数据:文件和区块的命名空间、从文件到区块的映射信息、以及每个区块副本的位置信息。所有的元数据都维护在master的内存中。前两种类型(命名空间和文件到区块的映射)同样会通过在master的本地磁盘上记录操作日志来持久化保存,并复制到远程机器上(主备复制)。使用一个操作日志允许我们简单的、可靠的、并且没有不一致风险的更新master的状态,在master发生崩溃时。master并不将区块位置信息持久化。相反,在master自身启动时或任何一个chunkserver加入集群时,master会询问对应chunkserver关于其存储的区块信息。

2.6.1 内存数据结构

由于元数据被维护在内存中,master的操作会非常快。而且,master可以很容易并且很高效的在后台周期性扫描其整个状态。这种周期性的扫描用于实现区块垃圾收集、当chunkserver失败时的重新复制、以及在chunkservers之间为了平衡负载及磁盘空间使用而做的区块迁移。章节4.3和4.4会进一步讨论这些活动。

一个对于这种只在内存中维护元数据方法可能的担心是区块的数量,以及由此而决定的整个系统能力会被master的内存所限制。在实践中这不是一个很严重的限制。master会为每个64MB的区块维护少于64字节的元数据。绝大部分的块都是满的,因为大部分文件包含有多个块,只有最后一个块可能被部分填充。同样,对于每个文件往往需要少于64字节的文件命名空间数据,因为会使用前缀压缩来保存文件命名。(GFS这种设计会非常不擅长于大量小文件,以及频繁的文件命名或list等元数据操作)

如果有必要支持更大的文件系统,给master添加额外内存的代价相较于我们将元数据全部维护在内存中带来的简单性、可靠性、性能、及弹性来说是非常小的。

2.6.2 区块位置

master不会持久化保存哪些chunkservers上保存了给定区块的信息。其在启动时会简单的从chunkservers上调查该信息。master随后就可以保持自己是最新的,因为它控制了所有区块的安置工作并通过常规的心跳信息监控所有chunkservers的状态。

我们一开始尝试将区块位置信息也持久化在master上,但是后来我们确定在启动时从chunkservers中询问这些信息并在随后周期性的询问会简单的多。这减少了在chunkservers加入和离开集群、改变名字、失败、重启等行为时需要保持master和chunkservers同步而导致的一系列问题。在一个有数百个服务器的集群中,这些事件会发生的非常频繁。(由于整个集群中chunkserver的状态变动比较频繁,如果chunk的位置信息需要持久化就会导致这部分元数据在持久化保存时称为瓶颈,因而直接在内存中非持久化维护是更好的选择)

另一种理解该设计决定的方式是,意识到一个chunkserver具有它本地磁盘上维护了或没维护哪些区块的最终话语权。不需要在master上尝试维护一个该信息的一致性视图,因为一个chunkserver上的错误可能会导致区块自然的失效(例如,一个磁盘可能坏了),或者一个操作可能重命名了一个chunkserver。

2.6.3 操作日志

操作日志包含了一个重要元数据变化记录的历史。其是GFS的核心。不仅仅是因为它是元数据唯一的持久化记录,而且它也作为一个逻辑的时间线为并发操作定义一个顺序。文件和区块,以及它们的版本(参见章节4.5),都在其创建时通过逻辑时间戳被唯一的且永久的标识。

由于操作日志是至关重要的,我们必须可靠的存储它们,并且确保直到元数据的改变被持久化之后才对客户端可见。否则,我们会实际上丢失整个文件系统或最近的客户端操作,即使区块本身存活下来了。因此,我们将其复制到多个远程机器上,并只有同时在本地及远程将对应的日志记录刷到磁盘上之后才会响应一个客户端的操作。master在flush之前会将若干个日志记录攒为一批,如此一来可以降低刷新和复制对于整个系统吞吐量的影响。

master可以通过重放操作日志来恢复其文件系统状态。为了最小化启动时间,我们必须保持操作日志比较小。master会在日志增长超过某个特定的尺寸时对其状态执行checkpoint,如此一来其可以通过从本地磁盘上加载最新的checkpoint并只回放从检查点开始的有限的日志记录就可以恢复了。checkpoint被保存在一个类似于B-树结构的文件中,从该存储结构可以直接映射到内存数据结构中,并且不需要额外的解析就可以查找命名空间。这进一步的加速了恢复时间并改善了可用性。

由于构建一个checkpoint会需要一点时间,master的内部状态会以如下的方式来构建,创建一个新的检查点的行为不会延误新来的修改(COW方式)。master会在一个单独的线程中切换到一个新的日志文件,并创建一个新的检查点。新的检查点包含了切换动作之前的所有修改。对于一个含有数百万文件的集群,其可以在一分钟左右被创建完毕。当创建完成,其会同时在本地和远程被写入磁盘。(实际生成新的检查点的方式应该是读取上一次检查点并在其上回放直到新checkpoint截止点的操作日志形成内存结构,然后再生成新的检查点)

恢复时只需要最近完成的检查点及随后的日志文件。老的检查点和日志文件可以被随时删掉,然而,我们会保存稍前的几个以容灾。在生成检查点时的错误不会影响到正确性,因为恢复代码会检测并跳过未完成的检查点。

2.7 一致性模型

GFS有一个放松的一致性模型,该模型可以很好的支持我们高度分布式的应用,但仍可以保持实现时相对的简单和高效。我们现在讨论GFS提供的保证,以及它们对应用来说意味着什么。我们同样会着重叙述GFS是如何维持这些保证的,但会将细节留到本论文的其他部分描述。

2.7.1 GFS的保证

文件命名空间修改(例如,文件创建)是原子性的。这些操作会由master排他的处理:命名空间锁确保了原子性和正确性(章节4.1);master的操作日志定义了一个这些操作的全局总顺序(章节2.6.3)。

在一个数据修改之后的文件区域的状态取决于修改的类型、其是执行成功还是失败、以及是否有并发的修改。表1总结了相应结果。如果所有的客户端总会看到相同的数据,不考虑它们读取的是哪一个副本,那么这个文件区域是一致的。如果一个区域在一次文件数据修改之后是一致的,并且客户端都能完整的看到对它的修改写入,那么这个文件区域是被定义的。当一个修改没有与其他并发writer相互影响的写入成功后,被影响的区域就被定义如下(暗含它是一致的):所有客户端总是可以看见修改的写入。并发的成功写入会将该区域留为未定义(leave the region undefined)但是一致的:所有客户端会看到相同的数据,但它可能没有反映出其中任何一个修改的写入。典型地,它包含了来自于多个修改的混合片段。一个失败的修改会使得该区域不一致(因而同样是未定义的):不同的客户端可能在不同时间看到不同的数据。在下面会描述我们的应用如何可以区别定义的区域和未定义的区域。应用不需要进一步区分不同类型的未定义区域。

数据修改的类型可能是写入或记录追加。一个写入会导致数据被写到一个应用所指定的文件的特定偏移中。一个记录追加导致数据(就是“记录”)被原子性的追加至少一次,即使存在着并发修改,但是是在GFS自己选择的偏移上(章节3.3)。(对比而言,一个“常规的”追加仅仅是在一个客户端认为是当前文件末尾的位置上写入数据。)该偏移会被返回给客户端并在开头标记这是一个包含了该记录的已定义区域。另外,GFS可能会在已定义区域之间插入填充或重复的记录。它们占据了被认为是不一致的区域,并且相对于用户数据来说,往往是很少的。

在一系列顺序的成功修改之后,被修改的文件区域确保为定义的并包含了最后一个修改写入的数据。GFS通过(a)对于一个区块,在所有副本上以相同的顺序应用修改(章节3.1),以及(b)使用区块版本号来检测由于丢失修改信息而变坏的副本(章节4.5)。变坏的副本不会再被涉及到一个修改或被master返回给客户端。它们会在最早的时机上被垃圾收集。

由于客户端会缓存区块位置信息,因此它们可能会在相关信息刷新之前从一个变坏的副本上读取数据。这个时间窗口会被缓存项的超时时间,以及文件的下一次打开时间所限制,再次打开文件会从缓存中清理掉该文件的所有区块信息。而且,由于我们绝大多数的文件是只追加的,一个变坏的副本经常会返回一个未成熟的处于文件结尾的区块而不是一个过时的旧数据。当一个reader重试并与master沟通,其会立即得到当前的区块位置信息。

在一次成功修改很久之后,组件的失效当然仍可以损坏或摧毁数据。GFS会通过常规的在master和所有chunkservers之间握手来定位失效的chunkservers,并通过校验和(章节4.3)来检测数据损坏。一旦一个问题出现,数据会尽可能快的从有效副本中恢复(章节4.3)。一个区块只有当GFS有所反应之前丢失其所有副本,才会不可逆的丢失数据,该时间往往在几分钟之内。即使在这种情况下,它的状态也会变成不可用,而不是损坏:应用会收到明确的错误,而不是损坏(不一致)的数据。

 2.7.2 对应用的启示(Implications for Applications)

GFS的应用可以使用一些简单的技术来采用宽松的一致性模型,这些技术早就由于其他目的而被需要:基于追加而非重写、检查点checkpoints、以及写入可以自我校验、自我识别的记录。

实践中,我们所有的应用都通过追加而非重写来修改文件。在一个典型的应用中,一个writer会从头到尾的创建一个文件。其在写入所有数据之后原子性的将该文件重命名为一个永久性的名字,或者周期性的执行检查点以确定有多少记录已经被成功写入。检查点也可能包含了应用级别的校验和。Readers只会校验并处理直到最近一次检查点的文件区域,这些区域被认为是已被定义的状态。若不考虑一致性和并发问题,该方法为我们服务的很好。追加比随机写更有效率并且对于错误容忍具有更高的弹性。追加写允许writers增量重启,并使readers避免处理那些在文件中已经被成功写入,但是从应用的视角来看尚未完成的数据。

在其他的经典使用场景中,很多writers会并发的往一个文件中追加,用于归并结果或作为一个生产者-消费者队列。记录追加操作的至少追加一次语义保留了每一个writer的输出。readers需要遵循如下方式以处理偶尔出现的填充和重复记录。为writer准备好的每一条记录都包含了额外的信息,例如校验和,因此可以校验其有效性。一个reader可以使用校验和来校验并丢弃额外的填充和记录片段。如果应用不能忍受偶尔的重复(例如,如果它们会触发非幂等性的操作),那么它可以通过记录中的唯一标识将其过滤掉,这种唯一标识常常被用于命名对应的应用实体记录,例如网络文档。这些对于记录读写I/O(除了删除重复记录)的功能被放在库代码中,这些库代码被我们的应用所共享,并被应用到Google的其他文件接口实现中。有了这个库,与写入时顺序相同的记录,伴随着很稀少的重复记录,总是会被传递给记录读取器。

3 系统交互

我们如此设计系统,以使得master在所有操作中都被最小化的牵涉到。在这种背景下,我们现在描述一下客户端、master、和chunkservers是如何交互以实现数据修改、原子性记录追加、以及获取快照的。

3.1 租约和修改顺序

一个修改是一个会改变区块内容或元数据的操作,例如写入或追加操作。每一个修改都在一个区块的所有副本上执行。我们使用租约以在多个副本之间维护一个一致性的修改顺序。master将一个区块的租约赋予其中的一个副本,我们称其为主副本(primary)。主副本会为该区块的所有修改(尤其是并发修改)确定一个先后顺序。所有副本都会在应用修改时遵循该顺序。如此一来,全局的修改顺序就会首先由master选择的租约授予顺序,接着在一个租约中由主副本分发的操作顺序号来共同决定。

租约机制被设计来最小化master的管理工作负担。一个租约初始有60秒的超时时间。然而,随着区块被不断修改,主副本可以请求并往往会得到master无限期的租约扩展。这些租约扩展的请求和授予是在master和所有chunkservers之间心跳消息的基础上实现的。master可能有时候会尝试在超时失效之前撤销一个租约(例如,当master想要禁止一个正在被执行改名的文件上的修改时)。即使master与一个主副本失去联系,它也可以在旧租约失效之后安全的将租约授予另一个副本。

在图2中,我们展示了该过程,通过如下的步骤完成一个写入时的控制流。

  • 客户端询问master哪些chunkserver持有区块的当前租约,以及其他副本的位置。如果没有副本持有租约,master会选择一个副本并为其授予租约(没有展示)。
  • master会将当前主副本的标识及其他副本的位置响应给客户端。客户端会将这些信息缓存,以供未来的修改使用。只有在主副本变得不可达或者主副本回答说它已经不再持有租约时才需要再次与master沟通。
  • 客户端将数据推向所有的副本。客户端可以以任意顺序执行这个操作。每个chunkserver会将数据保存在一个LRU缓存中,直到该数据被使用或超时。通过解耦数据流和控制流,我们可以不用考虑哪个chunkserver是主副本,而是基于网络拓扑来调度昂贵的数据流以改善性能。章节3.2会对此做进一步讨论。
  • 一旦所有的副本都承认自己已经接收到数据了,客户端就会给主副本发送一个写入请求。该请求会确认先前向所有副本推送的数据(可类比commit操作)。主副本为其收到的所有修改分配连续的顺序号,这些修改可能来自于多个客户端,该操作提供了必要的串行化。其按照序号的顺序在本地状态上应用这些修改。
  • 主副本将写入请求推送给所有的备副本。每个备用副本按照主副本分发的相同序号顺序应用修改。
  • 备副本回答主副本,表示他们已经完成了操作。
  • 主副本应答客户端。在任何一个副本中遇到的错误也会被报告给客户端。当出现了错误,写入操作可能在主副本和备用副本的任意子集上执行成功了。(如果在主副本上失败了,其不会被分发顺序号并推送到备副本。)客户端请求就被认为失败了,被修改的区域就被作为不一致的状态留在那里(脏数据)。我们的客户端代码会通过对失败的修改重试来处理这种错误。这会导致在完全重试之前的一些从步骤3到步骤7的无效尝试。

如果应用的一个写入非常大或者骑跨一个区块的边界时,GFS客户端代码会将其分裂为多个写操作。它们都遵循着上面描述的控制流,但是可能会与其他客户端的操作相互间隔或被其他客户端的操作给覆盖。因此,共享的文件区域可能最后包含了来自于不同客户端的片段,不过副本还是会完全一致的,因为独立的操作在所有副本上会以完全相同的顺序被完成。这会导致文件区域处于一种一致但是未定义的状态,正如在章节2.7中提到的。

3.2 数据流

我们将数据流和控制流解耦以更加高效的使用网络。控制流是从客户端发送往主副本,然后由主副本发送往所有备副本;而数据流是线性推送的,以一个流水线的方式流过被小心翼翼选择的chunkservers链。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈及高延迟的链接,并使推送所有数据的延迟最小化。

为了充分利用每台机器的网络带宽,数据会沿着一个chunkservers的链被线性推送,而非以某种拓扑(例如,树形)分布式推送。如此一来,每台机器的全部输出带宽都被用来传输数据给一个下游,而不是在多个接收方之间切分。

为了尽可能的避免网络瓶颈和高延迟链接(例如,跨交换机的链接往往两者都具备),每台机器都会将数据推送到尚未收到数据的网络拓扑中“最近的”机器上。假设客户端要推送数据到从S1到S4的chunkservers中。其会将数据发送到最近的chunkserver,假设是S1。S1会将数据推送到从S2到S4中离自己最近的chunkserver上,假设是S2。同样的,S2将数据推送到S3或S4,其中离S2比较近的一个,以此类推。我们的网络拓扑足够简单,如此一来“距离”可以通过IP地址被准备的估算出来。

最终,我们通过在TCP链接上流水线化传输数据来最小化数据传输延迟。一旦一个chunkserver收到某些数据,它就会立刻往前推送。流水线对于我们尤为有用,因为我们使用了一种全双工链接的交换网络(switched network with full-duplex links)。立即发送数据并不会影响到接收数据速率。在没有网络拥堵的情况下,将B个字节传输到R个副本上,在理想情况下的耗时为B/T + RL,其中T是网络吞吐率,L是两个机器之间传输字节时的延迟。我们的网络链接性能一般是100Mbps(T),并且延迟L远小于1ms。因此,1MB数据在理想情况下可以在80ms内被分发(单个区块的分发速率在10MB/s左右,多个区块并发写入可以充分的利用多个机器之间的网络带宽,IO的并行度级别为区块chunk)。

3.3 原子性的记录追加

GFS提供了一种原子性的追加操作,称为记录追加。在一个传统写入操作中,客户端会指定数据被写入位置的偏移。对相同区域的并发写操作不是可串行化的:该区域最后可能包含了来自多个客户端的数据片段。然而,在一个记录追加操作中,客户端只会指定数据。GFS会原子性的将该数据追加到文件中至少一次(也即是,作为连续的字节数组),在一个由GFS指定的偏移位置上,并将该位置返回给客户端。这与在Unix中以0 APPEND模式打开的文件中多个writers并发的以一种无竞争的方式写入数据时的表现相似。

在我们的分布式应用中会重度使用记录追加操作,其中很多来自不同机器上的客户端并发的往相同文件中追加数据。如果它们使用传统的writers做这样的操作,它们就需要有额外的复杂并昂贵的同步方案,例如通过一个分布式锁管理器。在我们的工作负载中,这样的文件往往作为一个多生产者/单消费者队列或用来包含很多不同客户端结果的归并。

记录追加也是一种修改,并且遵循章节3.1中描述的控制流,只在主副本中需要一点额外的逻辑。客户端将数据推送到文件最后一个区块的所有副本之后,其将写入请求发送到主副本。主副本会检查将记录追加到当前区块后是否会导致该区块超过最大尺寸(64MB)。如果是这样,则直接将当前区块填充到最大尺寸,并告诉备用副本也这样做,然后响应客户端,告诉它当前操作应该在下一个区块上被重试。(追加记录的大小被限制在最大区块尺寸的四分之一,以保持最坏情况下的碎片处于一个可接受的级别。)如果记录满足最大区块尺寸要求,这是最常见的情况,主副本会将数据追加到其副本中,并告诉备副本数据写入的确切偏移(主副本自己写入的偏移),最终向客户端返回追加成功。

如果一个记录在任何一个副本上追加失败了,那么客户端就会重试该操作。结果是,相同区块的副本可能包含不同数据,这些不同的数据包括了相同记录的完全或部分重复的数据。GFS并不确保所有副本在字节级别上的一致性。它仅仅保证数据作为一个原子性的单元被至少写入一次。这个特性轻而易举的遵循了如下简单的观察,对于报告成功的操作,数据肯定已经被写入到相同区块的所有副本的相同偏移位置上。而且,在这之后,所有副本都至少是以该记录为结尾的(也可能已经写入了更靠后的记录),因此任何未来的记录都会被分发一个更高的写入偏移或被写到不同的区块中,即使后续有另一个不同的副本变成了主副本。根据我们的一致性保证,成功的记录追加操作写入数据的区域是被定义的(因此也是一致的),而中间穿插的那些区域是不一致的(因此是未定义的)。我们的应用可以像在章节2.7.2中一样处理不一致的区域(判断并跳过)。

3.4 快照

快照操作几乎可以立刻为一个文件或一个目录树(“数据源”)制造一个拷贝,同时可以最小化与正在执行的修改之间的干涉。我们的用户使用该功能来快速的对大数据集创建分支拷贝(并且经常递归的拷贝这些拷贝),或者在对数据集做实验之前执行checkpoint,这些实验包含了后面可能会被提交或回滚的修改。

像AFS[5]一样,我们使用标准的写时拷贝(copy-on-write)的技术来实现快照。当master收到一个执行快照的请求时,其首先收回要执行快照的文件中所有区块的任何尚存的租约。这确保了所有随后往这些区块中的写都需要与master交互以找到其租约持有者。这就给了master一个机会来首先创建一个该区块的拷贝。

在租约被收回或过期之后,master会将快照操作日志记录到磁盘中。随后通过复制源文件或目录树的元数据在其内存状态上应用该日志记录。新创建的快照文件指向与源文件相同的区块chunks。

在快照操作之后一个客户端第一次想要写数据到一个区块C时,它会向master发送一个请求以找到当前的租约持有者。master注意到区块C的引用计数大于1,因此它延迟回答客户端的请求,然后选择一个新的区块句柄C'。其随后指示每一个当前维护了C副本的chunkserver来创建一个新的叫做C'的区块。通过在相同chunkserver上创建与原始副本相同的新副本,我们确保了数据会被本地拷贝,而不是通过网络(我们的磁盘速度大约是我们100Mb以太网链接的三倍)。从这个时间点往后,请求的处理与任何其他区块都一样了:master对于新的区块C'选取其中的一个副本授予租约,然后响应给客户端,之后客户端可以正常的往区块C'中写入数据,并不知道它是刚刚从一个已存在的区块中创建出来的。

4. master操作

master执行所有的命名空间操作。另外,其管理整个系统的区块副本:作出副本安置的决定、创建新的区块(chunks)以及副本,并协调各种系统级别的活动以保持区块被充分的复制、在所有chunkservers之间平衡负载、并回收可用的存储。我们现在逐项讨论上述每一个主题。

4.1 命名空间管理和锁

很多master的操作会耗费较长时间:例如,一个快照操作需要撤回快照包含的所有区块所在chunkservers上的租约。我们不想这些操作运行的时候耽误master上的其他操作。因此,我们会允许多个操作处于活跃状态,并使用基于命名空间上不同区域的锁来确保适当的串行化。

不像很多传统的文件系统,GFS没有一个每目录的数据结构用来列出该目录中的所有文件。它也不支持相同文件或目录上的别名(alias,也即是Unix语义中的硬链接或软链接)。GFS逻辑上以一个查找表(lookup table)的方式表示其命名空间,该查找表将文件全路径映射到对应的元数据。由于有前缀压缩,这个查找表可以在内存中被高效的表达。命名空间树中的每一个节点(无论是一个绝对路径的文件名还是一个绝对路径的目录名)都有一个与其相关的读写锁。

每一个master操作都在其运行之前获取一组锁。典型地,如果该操作涉及到/d1/d2/.../dn/leaf,其会在路径/d1,/d1/d2,...,/d1/d2/.../dn上获取读锁,并在全路径/d1/d2/.../dn/leaf上获得一个要么读锁要么写锁。注意叶子节点可能是一个文件或目录,这取决于具体操作。

我们现在展示一下这个锁机制如何在目录/home/user被快照到/save/user时阻止文件/home/user/foo被创建。快照操作需要取得在/home和/save上的读锁,以及在/home/user和/save/user上的写锁。创建文件的操作需要获取/home和/home/user的读锁,以及在/home/user/foo上的写锁。这两个操作会被合理的串行化,因为它们尝试获取在/home/user上的冲突的锁。文件创建操作不需要获取父目录上的写锁,因为并不存在需要被保护免于修改的“目录”,或像inode一样的数据结构。在名字上的读锁足够保护父目录不被删除。

这种锁方案的一个很好的特性是,它允许对于相同目录的并发修改。例如,在相同目录中的多个文件创建操作可以并发执行:每一个操作都获取目录名上的一个读锁以及要创建的文件名上的一个写锁。目录名上的读锁足够防止目录被删除、重命名、或被执行快照。文件名上的写锁将尝试创建相同文件名的两个操作做了串行化。

由于命名空间可以包括多个节点,读写锁对象会被懒惰的分配,并会在其不需要时被删除。而且为防止死锁,锁会按照一种一致的总顺序被获取:它们首先通过在命名空间树中路径名的层级排序,接着在相同层级中使用字典序排序。

4.2 副本的安置

GFS集群是在不止一个层级上高度分布式的。其往往包含了跨越多个机架的成百上千个chunkservers。这些chunkservers又可能被来自于相同或不同机架中的数百个客户端所访问。两个不同机架上的机器之间的通信可能会跨越一个或多个网络交换机。另外,从一个机架中进入或输出数据的带宽可能会小于该机架中所有机器的带宽之和。多层次的分布结构提出了一种独特的挑战,如何将数据分布以达到扩展性、可靠性、以及可用性。

区块副本安置策略服务于两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽利用率。两者都考虑到的话,只在机器之间分布副本是不够的,其仅仅确保了磁盘或机器的失效并充分利用了每台机器的网络带宽。我们必须也要在机架之间分布副本。这确保了即使在整个机架被破坏或下线时一个区块的某些副本仍会存活并处于可用状态(例如,由于一个共享的资源例如网络交换器或电源电路失效了)。其同样意味着一个区块上的流量,尤其是读流量,可以利用多个机架的总带宽。另一方面,如此一来,写入的数据就需要流过多个机架,这是一种我们自愿做出的权衡。

4.3 创建、重新复制、重新平衡

区块副本会由于三个原因被创建:区块创建、重新复制、以及重新平衡。

当master创建一个区块时,它会选择在哪里放置最初的空副本。其会考虑到如下几个因素:(1)我们想要将新的副本放置到低于平均磁盘利用率的chunkservers上。随着时间推移,这会将chunkservers之间的磁盘利用率平衡。(2)我们想要限制在每个chunkserver上“最近”创建的区块个数。虽然创建操作本身是便宜的,但是它可靠的预示了即将到来的很重的写流量,因为区块是在写入请求要求时才会被创建,并且在我们的单次追加多次读取的工作负载下,它们在实践中往往会在完全写入后变成只读的。(3)正如在上面讨论的,我们想要将一个区块的副本在机架之间分布。

master会在一个区块的可用副本数低于定义的目标副本数时尽快对其执行重新复制。这种情况可能由于各种原因出现:一个chunkserver变成不可用的、chunkserver报告其副本可能损坏了、一个磁盘由于错误而不可用了、或者目标副本数增加了。每一个需要被重新复制的区块都会基于几个因素来确定其优先级。一个是它离目标复制数的距离。例如,我们给予一个丢失了两个副本的区块比仅丢失一个副本的区块更高的优先级。另外,我们选择首先重新复制活跃文件的区块而不是最近刚被删除文件的区块(查看章节4.4)。最后,为了最小化运行中应用的错误导致的影响,我们会提升任何会阻塞客户端执行过程的区块的优先级。

master选择最高优先级的区块并通过指示一些chunkserver直接从已存在的有效副本中拷贝区块数据来“克隆”它。新的副本通过与创建时相同的目的被安置:平衡磁盘利用率、限制单个chunkserver上的活跃拷贝操作、并将副本在机架之间分布。为了保持拷贝时的流量不去淹没客户端的流量,master限制了活跃的拷贝操作个数,既对整个集群有限制,也对每一个chunkserver有限制。另外,每一个chunkserver限制了其花费在每个拷贝操作上的带宽,通过窒息其往源chunkserver上发送的读请求。

最终,master会周期性的平衡副本:其检查当前副本的分布,移动副本以达到更好的磁盘空间利用平衡和负载平衡。同样通过这个过程,master会慢慢的填充一个新加入的chunkserver,而不是使其陷入到新创建的区块中并接受沉重的写入流量。新副本的安置原则与上面讨论的相似。另外,master必须选择要删除哪个已存在副本。一般而言,其会选择删除那些具有低于平均水平可用空间的chunkservers上的副本,以平均化空间使用率。

4.4 垃圾收集

在一个文件被删除之后,GFS并不会立即收回可用的物理存储。其只会在日常的文件和区块级垃圾收集时懒惰的执行物理存储回收。我们发现这种方式使得系统更加简单和更加可靠。

4.4.1 机制

当一个文件被应用删除,master会立即记录该删除操作的日志,像其他的修改一样。然而,不是立即回收相关资源,该文件仅仅被重命名为一个包含了删除时间戳的隐藏名。在master对文件系统命名空间的日常扫描中,它会删除这样的隐藏文件,如果它们已经存在超过三天的时间(该时间间隔是可以配置的)。在那之前,该文件都可以以新命名的、特殊的名字被读取,也可以通过将其重命名为正常名字而将其找回。当隐藏文件被从命名空间中删除之后,其内存中的元数据就被擦除了。这有效的切断了其到所有区块(chunks)的链接。

在一个相似的对区块命名空间的日常扫描中,master会确定孤儿区块(也即是,那些无法从任何文件映射到的区块)并擦除对于这些区块的元数据。在一个日常的与master交换的心跳消息中,每一个chunkserver都会汇报其所持有的一组区块,然后master会将所有不再存在于自己元数据中的区块的标识响应给对应的chunkserver。此后chunkserver就可以自己选择合适的时机物理删除这些区块。

4.4.2 讨论

虽然分布式的垃圾收集是一个在编程语言上下文中需要复杂解决方案的困难问题,但其在我们的场景中是非常简单的。我们可以简单的确定所有对于区块(chunks)的引用:它们处于由master单独维护着的从文件到区块的映射结构中。我们也可以简单的确定区块的所有的副本:它们是存在于每个chunkserver被设计好的目录下的Linux文件。那些不被master所知的副本就是“垃圾”。

这种用于垃圾回收的垃圾收集方法相较于急切删除的方式提供了若干优势。首先,在一个组件失效很常见的大规模分布式系统中,其很简单且可靠。区块创建时可能在一些chunkservers上成功了但是在另一些上失败了,留下了一些master当前并不知晓的副本。副本删除信息也可能被丢失,这些信息既有master自己的又有chunkserver的,若不采用垃圾收集的方法,则master需要在失败时记下相关信息以重新发送。垃圾收集提供了一种统一的且可信赖的方式来清理任何无用的副本。其次,它会将存储空间回收合并到master的日常后台活动中,例如日常的对命名空间的扫描以及和chunkservers的握手。如此一来,其会以批量的方式完成,并且开销被分期偿还了。而且,其是在master相对空闲的时候被执行。master可以更急切的响应需要被及时关注的用户请求。第三,延迟回收存储空间提供了一种安全网,以防止偶然的、不可逆的删除。

在我们的经验中,主要的不利情况是这种延迟有时候会在存储空间紧张时阻碍用户调优的效果。那些重复创建和删除临时文件的应用可能无法马上重用已删除文件的存储。我们通过加快回收已经明确删除文件的速度来处理这些问题。我们同样允许用户对命名空间的不同部分应用不同的复制和回收策略。例如,用户可以指定在某些目录下的文件的所有区块不需要被复制,并且任何被删除文件都需要立即且不可逆的从文件系统状态中删除。

4.5 变质副本的检测

区块副本可能会变质,如果一个chunkserver失效并在其宕机时丢失了针对该区块的修改的话。对于每个区块,master都会维持一个区块版本号来区别最新的和变质的副本。

任何时候master为一个区块授予租约,其都会增加区块版本号并通知最新的副本。master和这些副本都会在它们的持久化状态中记录这个新的版本号。这发生在任何客户端被通知之前,因而在客户端可以开始往区块中写入数据之前。如果有另一个副本在当前是不可用的,其版本号不会被推进。master会检测到某chunkserver中有一个变质的副本,当该chunkserver重启并汇报其持有的区块集合及其各自对应的版本号时。如果一个master看到有一个版本号大于自己记录的相应版本号时,master会假设自己在授予租约时突然失效了,因此其会采纳更高的版本号作为最新版本号。

master会在其日常的垃圾收集中删除变质的副本。在这之前,当在回答客户端关于区块的信息时,它实际上会认为一个变质的副本完全不存在。作为另一个保护措施,master在通知客户端哪个chunkserver持有一个区块的租约时或在一个克隆操作中当其通知一个chunkserver从另一个chunkserver读取区块数据时,都会带上该区块的版本号。客户端或chunkserver在执行操作时会校验版本号,因此它们总是会访问最新的数据。

5. 错误容忍和诊断

我们在设计系统时的一个最大的挑战是如何处理频繁发生的组件失效。集群中组件的质量和数量一起使得这些问题更像一种正常现象而非异常:我们不能完全信任机器,也不能完全信任磁盘。组件失效会导致不可用的系统或者,更糟糕的,损坏的数据。接下来讨论我们如何面对这些挑战,以及我们为系统构建的用以诊断问题的工具,当问题不可避免的发生时。

5.1 高可用

在一个GFS集群的成百上千台服务器中,一些服务器铁定会在某些时刻不可用。我们以两个简单但是有效的策略来保持整个系统高度可用:快速恢复和复制。

5.1.1 快速恢复

无论是master还是chunkserver都被设计为可以保存它们自己的状态,并且可以在数秒内启动,无论它们是如何关闭的。实际上,我们不会区分正常终止和异常终止;servers可以仅仅通过杀掉进程就例行的关闭。客户端和其他服务器会经历一次小的“打嗝”,随着它们请求超时、重新连到重启的服务器、并重试。章节6.2.2报告了观察到的启动时间。

5.1.2 区块复制

正如先前所讨论的,每一个区块都被复制到位于不同机架的多个chunkservers上。用户对于文件命名空间的不同部分可以指定不同的复制级别。默认使用三副本。master会按需克隆已存在的副本以确保每个区块都被充分的复制,当chunkservers掉线或通过校验和检测到损坏的副本时(参见章节5.2)。虽然复制可以工作的很好,但我们还是探索了一些其他形式的跨服务器冗余,例如针对我们日益增长的只读存储需求的奇偶校验或纠删码。我们预料到这会是很有挑战性的,但却是可行的,来在我们松散耦合的系统中实现这些复杂的冗余方案,因为我们的流量是被追加和读取而非小的随机写所统治。

5.1.3 master复制

master的状态也会被复制以实现可靠性。其操作日志和检查点都被复制到多个机器上。一个对其状态的修改只有在相应操作日志被刷新到本地及所有master副本中时才会被认为是提交了。为了简单起见,我们的master进程仍然负责所有的修改,以及诸如垃圾收集等会在内部改变系统状态的后台活动。当其失效时,它可以几乎立即重启。如果它的机器或磁盘失效,在GFS外部的监控基础设施会使用复制的操作日志在任何其他地方启动一个新的master进程。客户端只会使用master的规范化名字(例如:gfs-test),其是一个NDS别名,当master被重新定位到另一台机器时该别名对应的地址可以改变。

而且,“影子”masters会为文件系统提供只读访问,即使在主master宕机的时候。它们是影子,不是镜像,因为它们可能会稍微的拖慢主master,往往是几分之一秒。对于那些不会被活跃修改的文件或对于那些不太介意读取一些轻微陈旧结果的应用,它们增强了对文件读的可用性。实际上,由于文件内容是从chunkservers中读取的,应用并不会观察到陈旧的文件内容。在小时间窗口中可能出现的陈旧其实是文件的元数据,诸如目录内容或者访问控制信息。

为了保持自己对于元数据变化的感知,一个影子master会读取副本中的不断增长的操作日志并按照与主master相同的顺序将修改应用到自己的数据结构上。像主master一样,其在启动时也会询问chunkservers(在那之后就不太频繁)以定位区块副本的位置并与它们频繁的交换握手消息以监控它们的状态。其只会依赖主master获得源自于主master做出的创建和删除副本决定所产生的副本位置更新消息。

5.2 数据完整性

每一个chunkserver都使用校验和来检测已存储数据的损坏。假如一个GFS集群拥有在数百台机上的数千块磁盘,其会经常的经历导致数据损坏或丢失的磁盘失效问题,既包括读的路径上的又包括写的路径上的(查看第7章以了解其中的一个原因)。我们可以使用其他的副本以从数据损坏中恢复,但是通过在chunkservers之间对比副本来检测数据损坏是不明智的。而且,分离的副本也可能是合法的:由于GFS关于修改的语义,特别是前面讨论的原子性的记录追加,并不会确保完全一致的副本。因此,每一个chunkserver都必须通过维护校验和来独立的校验其自己副本拷贝的完整性。

一个区块会被切分为64KB的小块(blocks)。每一个小块都有一个对应的32位校验和。像其他元数据一样,校验和被维持在内存中,并与日志一起被持久化存储,同用户数据分开。

对于读取,chunkserver在返回任何数据给请求者之前都会检查与读取范围交叠的数据小块(blocks)的校验和,无论这个请求者是一个客户端还是另一个chunkserver。因此chunkservers不会传播损坏的数据给其他机器。如果一个小块不匹配原来记录的校验和,chunkserver就会给请求者返回一个错误,并将这个不匹配信息报告给master。相应的,请求者会从另一个副本中读取数据,同时master会从另一个副本中执行区块克隆。当一个有效的新副本被准备好后,master会指示报告了不匹配副本的chunkserver删除其上的损坏副本。

校验和对于读取性能的影响非常小,这有几个原因。由于我们绝大部分的读取范围都跨越了至少几个小块,我们仅仅需要读取相对很少数量的额外数据并对其执行校验和。GFS客户端代码通过尝试在校验和小块的边界上执行对齐来进一步降低这个负担。另外,在chunkserver上的校验和的查找和对比不需要消耗任何I/O来执行,并且校验和的计算经常可以与I/Os操作交叠执行。

校验和的计算对于往一个区块尾部的追加写做了重度的优化(相较于对已存在数据的覆盖写)因为它们在我们的工作负载中占统治地位。我们只会增量的更新最后部分小块的被局部校验的校验和,并对于任何追加操作所填充的新的小块计算新的校验和。即使最后被局部校验和的小块已经损坏了,并且我们当前没有检测到它,那么新的校验和值就不会匹配已存储的数据,如此一来数据损坏就会在下一次读取时被检测到。

相反,如果一个写入覆盖了某区块已经存在的区域,我们必须读取并校验被覆盖区域的第一个和最后一个小块,然后执行写入操作,最终计算并记录涉及到的每一个小块的新校验和。如果我们不在部分的重写第一个和最后一个小块之前检查它们的校验和,那新的校验和可能会隐藏在未被覆盖的区域中已发生的数据损坏。

在空闲的时候,chunkservers可以扫描并检查不活跃区块的内容。这允许我们可以检测出那些很少被使用区块的损坏。一旦损坏被检测到,master可以创建一个新的未损坏的副本并删除损坏的副本。这防止了一个不活跃的但是已损坏的副本来愚弄master,使其相信该区块拥有足够的有效副本。

5.3 诊断工具

大量的和详尽的诊断日志在问题隔离、调试、以及性能分析中提供了极大的帮助,同时只导致了很小的开销。如果没有日志,很难去理解多个机器之间转瞬即逝的,不可重复的交互。GFS服务器会产生记录了很多重要事件的诊断日志(诸如chunkservers上线和下线)以及所有的PRC请求和响应。这些诊断日志可以被随时删除,而不会影响到系统的正确性。然而,在空间允许的情况下我们会尝试尽可能长的保留这些日志。

RPC日志包含了在通信线路上的确切的请求和响应,除了被读取或写入的文件数据。通过将请求与响应进行匹配,并核对不同机器上的RPC记录,我们可以重新构建出整个的交互历史以诊断一个问题。日志同样会被用于压力测试和性能分析时的踪迹。

记录日志对性能的影响是很小的(并且远远小于其带来的好处)因为这些日志是被异步且顺序写入的。最近的事件往往会被维持在内存中,且其对于连续的在线监控是可用的。

6. 度量

在本章节中我们呈现了一些微基准测试以展示GFS架构和实现中的一些内在瓶颈,也展示了一些在Google中实际集群的数字。

6.1 微基准测试

我们测试了一个由一个master、两个master副本、16个chunkservers,以及16个客户端构成的GFS集群的性能。注意使用这个配置的原因是便于测试。典型的集群由数百个chunkservers和数百个客户端构成。

所有的机器都配置为双核1.4GHz PIII处理器、2GB内存、两个80GB 5400rpm磁盘,通过一个100Mbps全双工以太网连接到一个HP 2524交换机上。所有19个GFS服务器都连接到同一个交换机上,所有16个客户端机器都连接到另一个交换机上。两个交换机之间通过一个1Gbps带宽的链接相连。

6.1.1 读取

N个客户端同时从文件系统中读取。每个客户端从一个320GB大小的文件集中读取随机选取的4MB区域。重复读取256次,这样的话每个客户端最终会读取1GB的数据。chunkservers总共只有32GB的内存,因此我们预计在Linux的缓冲区中最多只有10%的命中率。我们的结果应该更接近于冷缓存的结果。

图3(a)展示了N个客户端总共的读取速率,以及其理论上的限制。上限为总共125MB/s,当在两个交换机之间的1Gbps的链路被填满时,或者每个客户端12.5MB/s,当其100Mbps的网络接口被填满时,无论那个先到。当只有一个客户端读取时观察到的读取速度为10MB/s,或客户端理论上限的80%。对于16个客户端,总共的读取速率达到了94MB/s,大约是理论上限125MB/s的75%,或者每个客户端6MB/s。随着读取客户端个数的增加效率从80%降低到75%,这是因为随着读取客户端的增加,多个readers同时从相同chunkserver中读取数据的概率就增加了。

6.1.2 写入

N个客户端同时写到N个不同的文件中。每个客户端在一系列1MB大小的写操作中往一个新文件里写1GB的数据。总共的写速率及其理论上限展示在图3(b)中。理论上限为67MB/s,因为我们需要将每个字节写到16个chunkservers中的3个上,每个chunkserver都有一个12.5MB/s的输入连接。

一个客户端的写入速率为6.3MB/s,大约是理论上限的一半。导致这种情况的罪魁祸首就是我们的网络栈。它并不能与我们往副本推送数据时所采用的流水线方案很好的配合。在从一个副本到另一个副本传播数据时的延迟降低了整体的写速率(流水线并不顺畅)。

16个客户端的总体写速率达到了35MB/s(或者每个客户端2.3MB/s),大约是理论上限的一半。像读取时的情况一样,随着客户端个数的增加,多个客户端更有可能并发的往相同的chunkserver中写入数据。而且,对于16个writers来说,其相较于16个readers更加容易导致冲突,因为每一个写入都涉及到三个不同的副本。(但其改善了单个客户端写入时由于流水线不顺畅导致的对网络带宽利用不足的问题)

写入比我们想要的更慢一些。实践中这没有成为一个主要的问题,因为即使这个问题增加了单个客户端的延迟,但是当客户端数量较多时却没有很大的影响到系统的总体带宽。

6.1.3 记录追加

图3(c)展示了记录追加的性能。N个客户端同时往同一个文件中追加记录。性能受限于存储文件的最后一个区块副本的chunkservers的网络带宽,与客户端的个数无关。可以看到追加速率开始于一个客户端的6.0MB/s,终结于16个客户端的4.8MB/s,造成这种情况的绝大部分原因是交通堵塞及不同客户端所看到的网络传输速度变化。

我们的应用倾向于并发的制造多个这样的文件。换句话说,N个客户端同时往M个共享的文件中追加记录,其中N和M都是数十或数百。因此,在我们测试中chunkserver网络的拥堵在实践中并不是一个很大的问题,因为客户端可以在一个文件所在的chunkserver很忙时写另一个文件。

6.2 现实世界中的集群

我们现在检查两个在Google中实际使用的集群,其很具有代表性。集群A被一百多个工程师用于日常的研究和开发。一个典型的任务会被一个个人用户初始化,并执行长达数小时。其会读取从数MBs到数TBs的数据,转换或分析这些数据,然后将结果写回到集群中。集群B主要用于生产环境数据的处理。其任务持续的时间更长一些并且会持续的生成并处理数TB的数据集,期间只有很偶尔的才会需要人工干预。在两种环境中,一个单个的“任务”都会包含在多个机器上的对多个文件同时的读和写处理。

6.2.1 存储

正如在表中的前5条记录所示,每个集群都包含数百个chunkservers,支持数TBs的磁盘空间,并且都是比较平均但没有完全被填满。“已使用空间”包含了所有的区块副本。实际上所有的文件都被复制了三次。因此,集群分别保存了18TB和52TB的文件数据。

两个集群有相似数量的文件,虽然B有更大比例的已死亡文件,也就是被删除的或被一个更新的版本替换的文件,但其存储尚未被回收。其同样有更多的区块数因为其持有的文件更大。

6.2.2 元数据

chunkservers总共保存了数十GBs的元数据,绝大部分是用户数据分割成64KB的小块的校验和(checksums)。在chunkservers中保存的唯一其他的元数据是在章节4.5中讨论过的区块版本号。

在master中维护的元数据更小,只有数十MBs,或大约平均每个文件100字节。这与我们关于在实践中master的内存不会成为系统容量限制瓶颈的假设相符。绝大部分每文件的元数据是以前缀压缩格式保存的文件名。其他元数据包括了文件所有权和访问许可、从文件到区块的映射、以及各个区块的当前版本。另外,对于每一个区块我们都保存了当前副本的位置和为实现写时复制而记录的一个引用计数。

每一个独立的服务器,无论是chunkserver还是master,都只包含了大约50到100MB的元数据。因此其恢复会是非常快的:只需花费数秒的时间来从磁盘上读取其元数据,然后就可以服务于查询了。然而,master往往会拖延一小段时间 —— 一般是30秒到60秒 —— 直到它从所有的chunkservers上获取到所有区块的位置信息。

6.2.3 读和写速率

表3展示了在不同时间段内的读和写速率。在做出这些测量时两个集群都启动了大约有一周的时间。(集群最近都因为升级到一个GFS的新版本而被重启过。)

自从重启以来的平均写速率低于30MB/s。当我们做这些测量时,集群B正处于一个写活动爆发期,产生大约100MB/s的数据,这会造成一个300MB/s的网络负载,因为写数据会被复制到三个副本中。

读取的速率远远高于写入的速率。正如我们所假设的,总工作负载中包含的读比写要多。两个集群都处于重度的读活动中。特别的,集群A在测试之前一周内维持了580MB/s的读速率。其网络配置可以支撑750MB/s,因此其在高效的使用着自己的资源。集群B可以支持的峰值读取速率为1300MB/s,但是其仅仅用了380MB/s。

6.2.4 master的负载

表3同样展示了发送到master的操作的速率为大约每秒200到500次。master可以轻松的保持这种操作速率,因此对于这种工作负载而言不会成为瓶颈。

在一个GFS的早期版本中,master在某些工作负载中偶尔会成为瓶颈。其花费了自己的大部分时间来顺序的扫描巨大的目录(其中包含了成百上千个文件)以查找特定的文件。自那以后我们修改了master的数据结构以允许在命名空间中高效的执行二分查找。现在可以轻松的支持每秒访问数千个文件。如有必要,我们可以通过在命名空间数据结构之前放置命名查找缓存来进一步加速该查找操作。

6.2.5 恢复时间

在一个chunkserver失效之后,一些区块的副本数会变得低于系统的要求,因此必须执行克隆以恢复它们的复制级别。恢复全部这样区块的时间取决于资源数。在一个试验中,我们杀掉了集群B中的一个chunkserver。该chunkserver持有大约15000个区块副本,包含了600GB的数据。为了限制对运行中应用的影响并提供调度决策时的自由空间,默认参数限制了本集群可以有91个并发的克隆任务(chunkservers个数的40%),并且每一个克隆操作允许消耗最大6.25MB/s(50Mbps)的带宽。所有区块在23.2分钟内全部恢复完毕,实际上的复制速率为440MB/s。

在另一个实验中,我们杀死了两个chunkservers,每一个都有大约16000个区块以及660GB的数据。这个双重错误使得266个区块变得只剩一个副本了。这266个区块会以一个更高的优先级被执行克隆,并且全部在2分钟内恢复到至少2副本,如此一来就将整个集群置于一个可以承受另一个chunkserver失效而不会丢失数据的较稳定状态了。

6.3 工作负载分解

在本章节中,我们展现了在两个GFS集群中工作负载的详细分解,这两个GFS集群与章节6.2中的集群类似但是并不相同。集群X被用于研究和开发,而集群Y被用于生产环境数据处理.

6.3.1 方法和注意事项

这些结果只包含了从客户端产生的请求,因此它们反应了由我们的应用所产生的对于整个文件系统的工作负载。其中不包括为了执行客户端请求而导致的服务器之间的请求,或者内部的后台活动,诸如前推写入或平衡行为。

对I/O操作的统计是基于被GFS服务器记录的实际RPC请求日志而启发性的重构出来的信息。例如,GFS客户端代码可能将一个读操作分解为多个RPCs请求以增加并行度,从中我们会推导出原始的读操作。由于我们的访问模式是高度程式化的,因此我们预计任何错误都会处于大量噪音中。由应用来明确的记录日志可能会提供稍稍准确一点的数据,但是逻辑上就不太可能重新编译并重新启动数千个正在运行的客户端来这么做,而且收集这么多机器上的结果也是很繁重复杂的。

应该很小心的不过度从我们的工作负载中进行归纳。由于Google完全控制着GFS及其应用,因而应用倾向于被针对GFS进行调优,反过来GFS也是针对这些应用被设计出来的。如此相互的影响可能也会存在于一般的应用和文件系统,但我们的场景中这种效果很可能更加显著。

6.3.2 chunkserver的工作负载

表4展示了按照数据尺寸统计的操作分布。读的尺寸展现出一种双峰分布。小读取(低于64KB)来自于查询密集型的客户端,其会查找在大文件中的小数据块。大读取(超过512KB)则来自于对于整个文件的较长的顺序读取。

 在集群Y中有大量的读取没有返回任何数据。我们的应用,特别是那些生产环境中的系统,经常将文件用作生产者-消费者队列。多个生产者会并发的往一个文件中追加数据,同时一个消费者会读取文件的结尾。偶尔,当消费者速度快过生产者时就不会读取到数据了。集群X更少的显式出这种情况,因为它常常被用于短生命周期的数据分析任务而非长生命周期的分布式应用。

写的尺寸同样展现出一种双峰分布。大的写入(超过256KB)往往是由于在writers中的巨大的缓存导致的。而缓存较少数据的writers,或更加频繁的执行checkpoint或同步写的writers,或者就是单纯的产生更少数据的writers会导致更小的写入(小于64KB)。

对于记录追加,集群Y看到了比集群X更高比例的大记录追加,由于我们的生产环境系统,其使用了集群Y,针对GFS被调参的更加激进了。

表5展示了在各种尺寸的操作中被传输的总数据量。对于所有类型的操作,大操作(超过256KB)往往导致了绝大部分传输的数据。小的读取(小于64KB)传输了一个较小但是很明显的读取数据比例,这是由随机读的工作负载导致的。

6.3.3 追加Vs写入

记录追加会被重度的使用,尤其是在我们的生产环境系统中。对于集群X,其写入与追加的比例为:传输的字节数108:1,操作次数8:1。对于集群Y,生产环境系统所使用的,相应比例为3.7:1和2.5:1。而且,这些比率说明对于两个集群而言,记录追加都倾向于比写入更大。然而,对于集群X,在测量期间对记录追加的整体的使用率是很低的,因此,其结果更像是被一个或两个具有特定的缓存尺寸选择的应用所偏斜了。

就像我们预期的,我们的数据修改工作负载被追加写而非覆盖写所主宰。我们测量了在主副本上的覆盖写的数据量。这接近于一个客户端故意的重写先前已写过的数据而非追加新数据。对于集群X,覆盖写导致了低于0.0001%的修改字节量以及低于0.0003%的修改操作次数。对于集群Y,相应比率都是0.05%。虽然这是很小的,但是仍高于我们的预期。原因是,绝大部分这样的覆盖写都来自于客户端由于错误或超时而产生的重试。它们本身并不是工作负载的一部分,但是是重试机制的结果。

6.3.4 master的工作负载

表6展示了对于master的请求类型的分解。绝大部分请求是读取时关于区块位置(FindLocation)以及数据修改时关于租约持有者信息(FindLeaseLocker)的。

 

集群X和Y对于删除请求看到了很不同的个数,因为集群Y存储了生产环境的数据集,这些数据集被日常的产生并被新版本数据所替换。还有一些这样的不同进一步被打开请求所掩盖,因为一个文件的旧版本可能被open for write这种操作隐含的删除了(在Unix的打开术语中的模式"w")。

FindMatchingFiles是一个模式匹配请求,其支持"ls"并类似于文件系统操作。不像其他发送到master中的请求,其可能会处理命名空间中的一大部分,因此可能会很比较昂贵。集群Y中更加频繁的看到这个操作,因为自动化的数据处理任务倾向于检查文件系统的某些部分以理解全局的应用状态。相反,集群X的应用更加明确的被用户所控制因此通常都提前了解所有需要的文件。(GFS元数据结构中关于命名空间和锁的设计,使其并不擅长于ls这种操作,尤其是在大并发的情况下)

7. 经验

在构建和部署GFS的过程中,我们经验了各种各样的问题,有些是操作层面的有些是技术层面的。

最初,GFS被设想为我们产品系统的后端文件系统。随着时间的推移,对它的使用进化到包括了研究和开发任务。其开始时对于授权和资源限额等支持甚少,但是现在包括了这些功能的初级形式。虽然产品系统可以被良好的调优并控制,但用户有时候却不那么容易受控。因此需要更多的基础设施来避免用户们相互干扰。

我们的一些最大的问题都是和磁盘及Linux相关的。我们很多的磁盘都向Linux驱动声称它们支持一系列的IDE协议版本,但是实际上只对一些更新的版本才会有可靠的响应。由于协议版本之间都非常相似,因此这些驱动几乎在大部分情况下都能工作,但是这种不匹配偶尔会导致驱动和内核对于驱动的状态产生不一致的看法。这会不声不响的损坏内核中的数据。这种问题促使我们使用校验和(checksums)来检测数据损坏,同时我们也会修改内核来处理这些协议的不匹配。

早期我们在Linux2.2内核上由于fsync()的开销产生了一些问题。fsync的代价是与整个文件的尺寸成比例,而不是与修改部分的尺寸成比例。这对于我们的巨大的操作日志来说是一个问题,尤其是在我们实现checkpoint之前。我们在一段时间内都是通过使用同步写绕过这个问题,直到最终迁移到了Linux2.4内核上。

另一个Linux的问题是在一个地址空间中的任何线程都必须要持有的一个单个的读写锁(大内核锁),当其从磁盘中把页加载进内存时(pages in)时(必须持有读锁)或当其在一个mmap()调用中修改地址空间时(写锁)。我们看到了在我们系统负载很轻时出现的转瞬即逝的超时,并很努力的寻找资源瓶颈或偶尔发生的硬件失效。最终,我们发现这个单个的锁在磁盘线程正在将刚刚映射的数据加载进内存时阻塞了主网络线程将新数据映射到内存中。由于我们主要受限于网络接口而非内存拷贝带宽,我们通过将mmap()修改为pread()绕过了该问题,代价是一次额外的内存拷贝。

尽管会有偶尔发生的问题,Linux代码的可用性还是帮助了我们一次又一次的探索并理解系统的行为。当时机成熟时,我们会改善Linux内核并将修改共享给开源社区。

8. 相关工作

像其他大型分布式系统例如AFS[5]一样,GFS提供了一个与位置无关的命名空间,这允许数据被透明的移动以支持负载均衡或错误容忍。不像AFS,GFS将一个文件的数据在很多存储服务器上展开,以一种更类似于xFS[1]和Swift[3]的方式,为了提高总体性能并增加错误容忍。

由于磁盘相对比较便宜,并且复制也比更加精密复杂的RAID[9]技术要简单,GFS当前只使用复制来做冗余,因此相较于xFS或Swift会消耗更多的原始存储。

与诸如AFS,xFS,Frangipani[12],以及intermezzo[6]这样的系统不同,GFS并不在文件系统接口之下提供任何缓存。我们的目标工作负载在一个应用中很少重用数据,因为它们要么流式的访问一个大数据集,要么随机的查找大数据集并每次读取少量数据。

一些分布式文件系统,例如Frangipani,xFS,Minnesota’s GFS[11]以及GPFS[10]都移除了中心化的服务器并依赖于分布式算法以实现一致性和管理。我们选择了中心化的方法,其目的是简化设计、增加可靠性、并获得弹性。特别是,一个中心化的master使我们更容易去实现复杂的区块安置及复制策略,由于master已经有了绝大部分相关信息并控制着这些信息是如何变化的。我们通过将master的状态维持在很小尺寸并完全复制到其他机器上来实现错误容忍。可扩展性和高可用性(针对读)当前由我们的影子master机制来提供。通过将操作日志追加到一个write-ahead log来持久化对于master状态的改变。因此我们可能会采用一个primary-copy拷贝方案,例如Harp[7]所采用以提供高可用以及比我们当前方案更强的一致性保证的方案。

我们正在处理一个与Lustre[8]中相似的问题,以对大量的客户端提供很好的总性能。然而,我们极大的将该问题简化了,通过专注于我们应用的需求而非构建一个遵循POSIX规范的文件系统。另外,GFS假设拥有大量不可靠的组件,因而错误容忍是我们设计的核心。

GFS看起来最像NASD架构[4]。NASD架构基于网络链接的磁盘驱动,而GFS像NASD原型所做的一样使用普通的商用机器作为chunkservers。不像NASD的工作,我们的chunkservers使用懒惰分配的固定尺寸区块而非变长的对象。另外,GFS实现了诸如重新平衡、复制、以及恢复等在生产环境中所要求的特性。

不像Minnesota's GFS和NASD,我们不会寻求改变存储设备的模型。我们专注于使用已经存在的商用组件处理复杂的分布式系统的日常数据处理需求。

由原子性的记录追加所支持的生产者消费者队列解决了一个与River[2]中的分布式队列相似的问题。River使用基于分布在多个机器上的内存中的队列,并关心数据流控制,而GFS使用一个可被多个生产者并发追加的持久化文件。River的模型支持m-to-n的分布式队列,但是缺少持久化存储提供的错误容忍性,而GFS只能有效的提供m-to-1队列。多个消费者可以读取相同的文件,但是它们需要协作来切分即将到来的工作负载。

9. 结论

Google文件系统展示了为了在商用的硬件上支持大规模数据处理工作负载所需要的一些重要特质。虽然一些设计决定是特定于我们独特的设定,但很多设计还是可以被应用到具有相同数据度量和开销考虑的数据处理任务中的。

本文开始于在我们当前的和预计的应用程序工作负载和技术环境中重新检查了传统文件系统的假设。我们的观察导致了在设计空间中的一些根本不同点。我们将组件失效视为正常情况而非异常,专门针对巨大的文件以及绝大部分是追加(可能是并发追加)和读取(通常是顺序读取)的操作进行了优化,然后对标准的文件系统结构既做了继承又做了一定程度的放松,以改善整个系统。

我们的系统通过持续的监控、对重要数据的复制、以及快速和自动的恢复来提供错误容忍。区块复制使得我们可以容忍chunkserver的失效。这些频发的失效催生了一个新颖的在线修复机制,该机制会常规且透明的修复损坏并尽可能快的补偿丢失的副本。另外,我们使用校验和来检测在磁盘或IDE子系统级别的数据损坏,考虑到系统中磁盘的数量,这些数据损坏会变得很常见。

我们的设计对于很多并发的readers和writers执行的各种各样的任务实现了很高的总吞吐量。我们通过将文件系统的控制和数据传输分离来达到这一目的,其中文件系统控制是通过master完成,而数据传输是直接在chunkserver和客户端之间进行。master在常用操作中被最小化的牵涉到,这会通过较大的区块尺寸及区块租约来实现,区块租约会在数据修改时将相关权力授予主副本。这使得一个简单的、中心化的且不会成为瓶颈的master成为可能。我们相信在我们网络栈上的改进会提升当前在单个客户端上看到的写入吞吐量的上限(Google在修改Linux内核的网络栈,因为其导致了流水线流的不顺畅)。

GFS已经成功的满足了我们的存储需求,并且在Google内部被广泛的应用于研究和开发存储平台,以及生产环境数据处理。它是一个使我们可以在整个网络的尺度上持续创新并处理问题的重要工具。

==============================================================================

本技术论文的翻译工作由不动明王1984独自完成,特此声明。

翻译辛苦,珍惜劳动,引用时请注明出处!

==============================================================================

致谢

略....

参考文献

  •  [1] Thomas Anderson, Michael Dahlin, Jeanna Neefe, David Patterson, Drew Roselli, and Randolph Wang. Serverless network file systems. In Proceedings of the 15th ACM Symposium on Operating System Principles, pages 109–126, Copper Mountain Resort, Colorado, December 1995.
  •  [2] Remzi H. Arpaci-Dusseau, Eric Anderson, Noah Treuhaft, David E. Culler, Joseph M. Hellerstein, David Patterson, and Kathy Yelick. Cluster I/O with River: Making the fast case common. In Proceedings of the Sixth Workshop on Input/Output in Parallel and Distributed Systems (IOPADS ’99), pages 10–22, Atlanta, Georgia, May 1999.
  •  [3] Luis-Felipe Cabrera and Darrell D. E. Long. Swift: Using distributed disk striping to provide high I/O data rates. Computer Systems, 4(4):405–436, 1991.
  •  [4] Garth A. Gibson, David F. Nagle, Khalil Amiri, Jeff Butler, Fay W. Chang, Howard Gobioff, Charles Hardin, Erik Riedel, David Rochberg, and Jim Zelenka. A cost-effective, high-bandwidth storage architecture. In Proceedings of the 8th Architectural Support for Programming Languages and Operating Systems, pages 92–103, San Jose, California, October 1998.
  •  [5] John Howard, Michael Kazar, Sherri Menees, David Nichols, Mahadev Satyanarayanan, Robert Sidebotham, and Michael West. Scale and performance in a distributed file system. ACM Transactions on Computer Systems, 6(1):51–81, February 1988.
  •  [6] InterMezzo. http://www.inter-mezzo.org, 2003.
  •  [7] Barbara Liskov, Sanjay Ghemawat, Robert Gruber, Paul Johnson, Liuba Shrira, and Michael Williams. Replication in the Harp file system. In 13th Symposium on Operating System Principles, pages 226–238, Pacific Grove, CA, October 1991.
  •  [8] Lustre. http://www.lustreorg, 2003.
  •  [9] David A. Patterson, Garth A. Gibson, and Randy H. Katz. A case for redundant arrays of inexpensive disks (RAID). In Proceedings of the 1988 ACM SIGMOD International Conference on Management of Data, pages 109–116, Chicago, Illinois, September 1988.
  •  [10] Frank Schmuck and Roger Haskin. GPFS: A shared-disk file system for large computing clusters. In Proceedings of the First USENIX Conference on File and Storage Technologies, pages 231–244, Monterey, California, January 2002.
  •  [11] Steven R. Soltis, Thomas M. Ruwart, and Matthew T. O’Keefe. The Gobal File System. In Proceedings of the Fifth NASA Goddard Space Flight Center Conference on Mass Storage Systems and Technologies, College Park, Maryland, September 1996.
  •  [12] Chandramohan A. Thekkath, Timothy Mann, and Edward K. Lee. Frangipani: A scalable distributed file system. In Proceedings of the 16th ACM Symposium on Operating System Principles, pages 224–237, Saint-Malo, France, October 1997.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值