The Google File System论文
Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung
Google∗
ABSTRACT
我们设计并实现了谷歌文件系统(GFS),这是一个可扩展的分布式文件系统,用于大型分布式数据密集型应用。它提供了容错功能,同时在廉价的商品硬件上运行,并为大量的client提供了高聚合性能。 虽然与以前的分布式文件系统有许多相同的目标,但我们的设计是由对我们的应用工作负载和技术环境的观察所驱动的,包括当前和预期的,反映了与早期文件系统的一些假设的明显不同。这促使我们重新审视传统的选择并探索完全不同的设计点。 该文件系统已经成功地满足了我们的存储需求。它在谷歌内部被广泛部署,作为我们服务所使用的数据的生成和处理的存储平台,以及需要大型数据集的研究和开发工作。迄今为止,最大的集群在一千多台机器上的数千个磁盘上提供了数百兆字节的存储,而且它被数百个client同时访问。 在本文中,我们介绍了为支持分布式应用而设计的文件系统接口扩展,讨论了我们设计的许多方面,并报告了来自微型测试和实际使用的测量结果。
1.简介
我们设计并实现了谷歌文件系统(GFS),以满足谷歌数据处理需求的快速增长。GFS与以前的分布式文件系统有许多相同的目标,如性能、可扩展性、可靠性和可用性。然而,它的设计是由我们对应用工作负载和技术环境的关键观察所驱动的,包括当前和预期的,反映了与早期的一些文件系统设计假设的明显不同。我们重新审视了传统的选择,并探索了设计空间中根本不同的点。 首先,组件故障是常态而非例外。文件系统由成百上千的存储机器组成,这些机器是由廉价的商品部件制造的,并由数量相当的client机访问。组件的数量和质量实际上保证了在任何时候都有一些组件不能正常工作,一些组件不能从当前的故障中恢复。我们已经看到了由应用程序错误、操作系统错误、人为错误以及磁盘、内存、连接器、网络和电源的故障造成的问题。因此,持续监控、错误检测、容错和自动恢复必须成为系统的组成部分。 第二,按照传统标准,文件是巨大的。多GB的文件很常见。每个文件通常包含许多应用对象,如网络文档。当我们经常处理由数十亿对象组成的许多TB的快速增长的数据集时,即使文件系统可以支持,管理数十亿大约KB大小的文件也是不容易的。因此,必须重新审视设计假设和参数,如I/O操作和块大小。 第三,大多数文件都是通过添加新的数据而不是覆盖现有的数据来改变的。文件内的随机写入实际上是不存在的。一旦写入,文件只被读取,而且往往只按顺序读取。各种各样的数据都有这些特点。有些可能构成了数据分析程序扫描的大型存储库。有些可能是由运行中的应用程序不断产生的数据流。有些可能是存档的数据。有些可能是在一台机器上产生的中间结果,并在另一台机器上处理,无论是同时还是稍后的时间。鉴于这种对巨大文件的访问模式,追加成为性能优化和原子性保证的重点,而在client端缓存数据块则失去了吸引力。 第四,共同设计应用程序和文件系统的API,通过增加我们的灵活性使整个系统受益。 例如,我们放宽了GFS的一致性模型,大大简化了文件系统,而没有给应用程序带来繁重的负担。我们还引入了一个原子追加操作,以便多个client端可以同时追加到一个文件,而不需要在它们之间进行额外的同步。这些将在本文后面详细讨论。 目前,多个GFS集群被部署用于不同的目的。最大的集群有超过1000个存储节点,超过300TB的磁盘存储,并且被不同机器上的数百个client端持续大量访问。
2.设计概述
2.1 假设
在为我们的需求设计一个文件系统时,我们一直遵循的假设是既是挑战也是机遇。我们在前面提到了一些关键的观察结果,现在更详细地阐述我们的假设。
该系统是由许多廉价的商品组件构建的,这些组件经常发生故障。它必须不断地监测自己,检测、容忍并及时恢复日常的组件故障。
该系统存储了数量不多的大文件。我们预计有几百万个文件,每个文件的大小通常为100MB或更大。多GB的文件是常见的情况,应该被有效管理。小文件必须得到支持,但我们不需要为它们进行优化。
工作负载主要包括两种类型的读取:大型流式读取和小型随机读取。在大型流式读取中,单个操作通常读取数百KB,更常见的是1MB或更多。来自同一个client端的连续操作通常会读取一个文件的连续区域。一个小的随机读取通常在某个任意的偏移处读取几KB。注重性能的应用程序经常对其小的读取进行批处理和排序,以便在文件中稳步推进,而不是来回走动。
工作负载也有许多大的、连续的写入,将数据附加到文件上。典型的操作大小与读取的操作类似。一旦写入,文件就很少被再次修改。支持在文件的任意位置进行小规模的写入,但不一定要有效率。
系统必须有效地实现定义良好的语义,以便多个client端同时追加到同一个文件。我们的文件经常被用作生产者与消费者的队列或用于多向合并。数以百计的生产者,每台机器运行一个,将并发地追加到一个文件。以最小的同步开销实现原子性是至关重要的。文件可能会在以后被读取,或者消费者可能会同时读取该文件。
高持续带宽比低延迟更重要。我们的大多数目标应用都重视以高速度处理大量的数据,而很少有人对单个读或写有严格的响应时间要求。
2.2接口
GFS提供了一个熟悉的文件系统接口,尽管它没有实现POSIX这样的标准API。文件在目录中被分层组织,并由路径名识别。我们支持创建、删除、打开、关闭、读取和写入文件的常规操作。 此外,GFS还有快照和记录追加操作。快照以较低的成本创建一个文件或目录树的副本。记录追加允许多个client端同时向同一个文件追加数据,同时保证每个client端的追加的原子性。它对于实现多路合并结果和生产者-消费者队列很有用,许多client可以同时追加数据而不需要额外的锁定。我们发现这些类型的文件在构建大型分布式应用中是非常宝贵的。快照和记录附加将分别在第3.4节和第3.3节中进一步讨论。
2.3架构
一个GFS集群由一个主服务器和多个分块服务器组成,并由多个client端访问,如图1所示。每个client通常是一台运行用户级服务器进程的商品Linux机器。只要机器资源允许,并且可以接受因运行可能不稳定的应用程序代码而导致的较低的可靠性,那么在同一台机器上同时运行chunkserver和client端是很容易的。 文件被划分为固定大小的块。每个分块都由master在创建分块时分配的一个不可改变的、全球唯一的64位分块句柄来识别。块服务器将块作为Linux文件存储在本地磁盘上,并读取或写入由块句柄和字节范围指定的块数据。为了保证可靠性,每个数据块都在多个chunkservers上进行复制。默认情况下,我们存储三个副本,尽管用户可以为文件命名空间的不同区域指定不同的复制级别。 master维护所有的文件系统元数据。这包括命名空间、访问控制信息、从文件到块的映射,以及块的当前位置。它还控制全系统的活动,如块租赁管理、无主块的垃圾收集,以及块服务器之间的块迁移。master在HeartBeat消息中定期与每个分块服务器进行通信,向其发出指令并收集其状态。 连接到每个应用程序的GFS client端代码实现了文件系统的API,并与master和分块服务器进行通信,代表应用程序读取或写入数据。client端与master进行元数据操作的交互,但所有的数据通信都是直接到分块服务器。我们不提供POSIX API,因此不需要与Linux vnode层挂钩。 client端和chunkserver都不对文件数据进行缓存。client端缓存的好处不大,因为大多数应用程序都是通过巨大的文件流来实现的,或者工作集太大,无法进行缓存。没有client端缓存可以消除缓存的一致性问题,从而简化client端和整个系统。(Chunkservers不需要缓存文件数据,因为chunks是作为本地文件存储的,所以Linux的缓冲区缓存已经将经常访问的数据保存在内存中。
2.4 单一master
单一master极大地简化了我们的设计,并使master能够利用全局知识做出复杂的分块放置和复制的决定。然而,我们必须尽量减少它在读写中的参与,这样它才不会成为一个瓶颈。client端从不通过master读写文件数据。相反,client端会询问master它应该联系哪些分块服务器。它在有限的时间内缓存这些信息,并在随后的许多操作中直接与chunkservers交互。 让我们参照图1来解释一下简单读取的交互方式。首先,使用固定的分块大小,client端将应用程序指定的文件名和字节偏移量翻译成文件中的分块索引。然后,它向master发送一个包含文件名和块索引的请求。master会回复相应的块柄和副本的位置。clientclient端使用文件名和分块索引作为密钥来缓存这些信息。然后,client端向其中一个副本发送请求,很可能是最近的一个。该请求指定了块处理和该块中的一个字节范围。对同一块的进一步读取不需要更多的client-master交互,直到缓存信息过期或文件被重新打开。事实上,client端通常在同一个请求中要求多个块,master也可以包括紧随请求的块的信息。这种额外的信息避免了未来client与master之间的几次互动而不用付出任何额外成本。
2.5分块大小
块的大小是关键的设计参数之一。我们选择了64MB,这比典型的文件系统块大小大得多。每个分块副本作为一个普通的Linux文件存储在一个分块服务器上,并且只在需要时才扩展。懒惰的空间分配避免了由于内部碎片造成的空间浪费,这也许是反对如此大的块大小的最大反对意见。 大块大小提供了几个重要的优势。首先,它减少了clients与master的交互需求,因为在同一个块上的读写只需要向master发出一个初始请求,以获取块的位置信息。这种减少对于我们的工作负载来说尤其重要,因为应用程序大多是按顺序读写大文件。即使是小的随机读取,client端也可以轻松地缓存一个多TB工作集的所有块的位置信息。第二,由于在一个大块上,client端更有可能对一个给定的块进行许多操作,它可以通过在较长的时间内保持与chunkserver的持久TCP连接来减少网络开销。第三,它减少了存储在主服务器上的元数据的大小。这允许我们将元数据保存在内存中,这又带来了我们将在第2.6.1节讨论的其他优势。 另一方面,大块的大小,即使有懒惰的空间分配,也有其缺点。一个小文件由 少量的块,也许只有一个。如果许多clients访问同一个文件,存储这些块的chunkservers可能成为热点。在实践中,热点并不是一个主要问题,因为我们的应用程序大多是按顺序读取大型多块文件。 然而,当GFS第一次被一个批处理队列系统使用时,确实出现了热点:一个可执行文件被作为一个单块文件写入GFS,然后在数百台机器上同时启动。储存这种可执行文件的少数chunkservers被数百个同时的请求弄得不堪重负。我们通过用更高的复制系数来存储这种可执行文件,并使批处理队列系统错开应用程序的启动时间来解决这个问题。在这种情况下,一个潜在的长期解决方案是允许clients从其他clients那里读取数据。
2.6元数据
master存储三种主要的元数据:文件和块的命名空间,文件到块的映射,以及每个块的复制位置。所有的元数据都保存在master的内存中。前两种类型(命名空间和文件到块的映射)也是通过将mutation(mutation)记录到存储在master本地磁盘上的操作日志中并复制到远程机器上来保持持久性。使用日志使我们能够简单、可靠地更新master状态,并且在master崩溃的情况下没有不一致的风险。master并不持久地存储分块的位置信息。相反,它在master启动时,以及当一个chunkserver加入集群时,会询问每个chunkserver关于它的chunks。
2.6.1 内存中的数据结构
由于元数据被存储在内存中,主控操作是快速的。此外,master在后台定期扫描其整个状态是很容易和有效的。这种定期扫描被用来实现分块垃圾收集,在分块服务器失败的情况下进行重新复制,以及分块迁移以平衡分块服务器的负载和磁盘空间的使用。第4.3和4.4节将进一步讨论这些活动。 这种仅有内存的方法的一个潜在问题是,分块的数量以及整个系统的容量受限于master的内存数量。这在实践中并不是一个严重的限制。master为每个64MB的块维护不到64字节的元数据。大多数块是满的,因为大多数文件包含许多块,只有最后一个可能是部分填充的。同样,文件命名空间数据通常需要少于64字节,因为它使用前缀压缩来紧凑地存储文件名。 如果有必要支持更大的文件系统,为master增加额外的内存的成本对于我们通过在内存中存储元数据所获得的简单性、可靠性、性能和灵活性来说是一个很小的代价。
2.6.2分块位置
master并不保留哪些分块服务器拥有某个分块的副本的持久性记录。它只是在启动时向chunkservers轮询该信息。此后,master可以保持自己的最新状态,因为它控制所有的分块放置,并通过定期的HeartBeat消息监控分块服务器状态。 我们最初试图在主控端持久地保存分块位置信息,但我们决定在启动时向分块服务器请求数据,并在此后定期地请求数据,这要简单得多。这就消除了在chunkservers加入和离开集群、改变名称、失败、重启等情况下保持主服务器和chunkservers同步的问题。在一个有数百台服务器的集群中,这些事件发生得太频繁了。 理解这一设计决定的另一个方法是,认识到一个chunkserver对它在自己的磁盘上拥有或不拥有哪些chunks有最终决定权。试图在主服务器上保持这种信息的一致性是没有意义的,因为chunkserver上的错误可能会导致chunks自发消失(例如,一个磁盘可能坏掉并被禁用),或者操作员可能会重新命名一个chunkserver。
2.6.3 操作日志
操作日志包含关键元数据变化的历史记录。它是GFS的核心。它不仅是元数据的唯一持久性记录,而且还作为一个逻辑时间线,定义了并发操作的顺序。文件和块,以及它们的版本(见第4.5节),都是由它们被创建的逻辑时间唯一地、永恒地识别的。 由于操作日志是关键的,我们必须可靠地存储它,并且在元数据变化被持久化之前,不使client端看到变化。否则,我们会有效地丢失整个文件系统或最近的client端操作,即使这些块本身还在。因此,我们把它复制到多个远程机器上,只有在把相应的日志记录冲到本地和远程的磁盘上之后,才能对client端的操作做出反应。master在刷新前将几个日志记录分批放在一起,从而减少刷新和复制对整个系统吞吐量的影响。 master通过重放操作日志来恢复其文件系统状态。为了尽量减少启动时间,我们必须保持日志的小规模。每当日志增长超过一定的大小时,master就会对其状态进行检查,这样它就可以通过从本地磁盘加载最新的检查点来恢复,并且只重放之后有限的日志记录。 之后只重放有限的日志记录。检查点采用类似于B树的紧凑形式,可以直接映射到内存中,用于命名空间查找,而不需要额外的解析。这进一步加快了恢复速度,提高了可用性。 由于建立一个检查点可能需要一段时间,master的内部状态的结构是这样的:可以在不延迟传入mutation(mutation)的情况下创建一个新的检查点。master切换到一个新的日志文件,在一个单独的线程中创建新的检查点。新的检查点包括切换前的所有mutation(mutation)。对于一个有几百万个文件的集群来说,它可以在一分钟左右创建。完成后,它被写入本地和远程的磁盘中。 恢复只需要最新的完整检查点和随后的日志文件。旧的检查点和日志文件可以自由删除,尽管我们保留一些以防止灾难的发生。检查点过程中的失败并不影响正确性,因为恢复代码会检测并跳过不完整的检查点。
2.7一致性模型
GFS有一个宽松的一致性模型,可以很好地支持我们的高度分布式应用,但在实现上仍然相对简单和高效。我们现在讨论GFS的保证(guarantees)以及它们对应用程序的意义。我们还强调了GFS是如何维护这些保证的,但将细节留给本文的其他部分。
2.7.1 GFS的保证(guarantees)
文件命名空间的mutation(mutation)(例如,文件创建)是原子性的。它们完全由master处理:命名空间锁保证了原子性和正确性(第4.1节);master的操作日志定义了这些操作的总体顺序(第2.6.3节)。 一个文件区域在数据mutation(mutation)后的状态取决于mutation(mutation)的类型,是成功还是失败,以及是否有并发的mutation(mutation)。表1总结了这一结果。如果所有client总是看到相同的数据,无论他们从哪个副本读取,文件区域是一致的。如果一个区域在文件数据mutation(mutation)后是一致的,并且client将看到mutation(mutation)后写下的全部内容,那么这个区域就是定义的。当一个mutation(mutation)成功而没有并发写入者的干扰时,受影响的区域被定义了(也意味着是一致的):所有client将始终看到mutation(mutation)所写入的内容。并发的成功mutation(mutation)使该区域未被定义,但却是一致的:所有client端看到的是相同的数据,但它可能不反映任何一个mutation(mutation)所写的内容。通常,它是由多个mutation(mutation)的混合片段组成的。一个失败的mutation(mutation)使该区域不一致(因此也是未定义的):不同的client可能在不同的时间看到不同的数据。我们在下面描述了我们的应用程序如何区分已定义区域和未定义区域。应用程序不需要进一步区分不同种类的未定义区域。
数据mutation(mutation)可能是写或记录附加。写入导致数据在应用程序指定的文件偏移处被写入。记录附加导致数据("记录")至少被附加一次,即使存在并发的mutation(mutation),但在GFS选择的偏移处(第3.3节)。(相比之下,一个 "常规 "的追加只是在client认为是文件当前末端的偏移处进行写入。) 该偏移量将返回给client,并标志着包含该记录的定义区域的开始。此外,GFS可能会在中间插入填充或记录重复。它们所占据的区域被认为是不一致的,通常与用户数据量相形见绌。 在一连串成功的mutation(mutation)之后,mutation(mutation)的文件区域被保证是定义的,并且包含最后一次mutation(mutation)所写入的数据。GFS通过以下方式实现这一目标:(a)以相同的顺序在所有副本上对一个块进行mutation(mutation)(第3.1节),以及(b)使用块的版本号来检测任何副本是否已经过时,因为它在chunkserver停机时错过了mutation(mutation)(第4.5节)。陈旧的副本将永远不会参与mutation(mutation),也不会被提供给向master询问大块位置的client。它们会在最早的时候被垃圾回收。 由于client机对分块位置进行了缓存,他们可能会在该信息被刷新之前从陈旧的副本中读取。这个窗口是由缓存条目的超时和文件的下一次打开所限制的,下一次打开会从缓存中清除该文件的所有分块信息。此外,由于我们的大多数文件都是只做附录的,陈旧的副本通常会返回一个过早结束的分块,而不是过时的数据。当读者重新尝试并联系master时,它将立即得到当前的分块位置。 在一次成功的mutation(mutation)之后很长时间,组件的故障当然仍然可以破坏或摧毁数据。GFS通过master和所有分块服务器之间的定期握手来识别失败的分块服务器,并通过校验来检测数据损坏(第5.2节)。一旦问题出现,数据会尽快从有效的复制中恢复(第4.3节)。只有在GFS能够做出反应之前,一个数据块的所有复制都丢失了,才是不可逆转的,通常在几分钟之内。即使在这种情况下,它也是不可用的,而不是损坏的:应用程序会收到清晰的错误,而不是损坏的数据。
2.7.2对应用的影响
GFS应用可以通过一些简单的技术来适应宽松的一致性模型,这些技术在其他方面已经需要了:依靠追加而不是覆盖,检查点,以及编写自我验证、自我识别的记录。 实际上,我们所有的应用程序都是通过追加而不是覆盖来改变文件的。在一个典型的应用中,一个写入器从头到尾生成一个文件。它在写完所有的数据后,原子式地将文件重命名为一个永久的名字,或者定期检查点,看看有多少数据已经成功写入。检查点也可能包括应用层面的校验。读取器只验证和处理到最后一个检查点的文件区域,已知该区域处于定义的状态。不管一致性和并发性问题如何,这种方法对我们来说是很好的。与随机写入相比,追加的效率要高得多,对应用程序的故障也更有弹性。检查点允许写入者逐步重启,并使读取者不会处理成功写入的文件数据,因为从应用程序的角度来看,这些数据仍然不完整。 在另一个典型的使用中,许多写者同时向一个文件追加,以获得合并的结果或作为生产者-消费者队列。Record append的append-at-least-once语义保留了每个写入者的输出。读取器处理偶尔出现的填充和重复的情况如下。编写者编写的每条记录都包含额外的信息,如校验和,以便可以验证其有效性。读取器可以使用校验和识别并丢弃额外的填充物和记录片段。如果它不能容忍偶尔的重复(例如,如果它们会触发非空闲操作),它可以使用记录中的唯一标识符将它们过滤掉,这些标识符通常需要用来命名相应的应用实体,如网络文档。这些记录I/O的功能(除了重复的删除)都在我们的应用程序共享的库代码中,适用于谷歌的其他文件接口实现。有了这些,同样的记录序列,加上罕见的重复,总是被传递给记录阅读器。
3.系统互动
我们设计了这个系统,以尽量减少master在所有操作中的参与。有了这个背景,我们现在描述一下client端、master和chunkservers是如何互动以实现数据mutation(mutation)、原子记录追加和快照的。
3.1 Leases(租约)和变异顺序
mutation(mutation)是一种改变块的内容或元数据的操作,如写或追加操作。每个mutation(mutation)都在该块的所有复制中进行。我们使用lease(租约)来维持各副本之间一致的变异顺序。master授予其中一个副本一个块状租赁,我们称之为primary。primary为该块的所有mutation(mutation)选择了一个序列顺序。所有副本在应用mutation(mutation)时都遵循这个顺序。因此,全局mutation(mutation)顺序首先由master选择的lease(租约)授予顺序来定义,而在lease(租约)内由primary分配的序列号来定义。
租约机制的设计是为了尽量减少master的管理开销。一个租约的初始超时时间是60秒。然而,只要该块正在mutated,primary可以无限期地请求并通常从master接收扩展。这些扩展请求和授予是在master和所有分块服务器之间定期交换的HeartBeat消息上附带进行的。master有时会试图在租约到期前撤销租约(例如,当master想要禁用正在重命名的文件的mutation时)。即使master失去了与primary的通信,它也可以在旧租约到期后安全地授予另一个副本一个新的租约。
在图2中,我们通过这些编号的步骤来说明这个过程,即按照写的控制流程来写。
1.client询问主控端哪个chunkserver持有该块的当前租约以及其他副本的位置。如果没有人有租约,master就把租约授予它选择的一个副本(未显示)。 2.master回复了primary 的身份和其他(次要)副本的位置。client缓存这些数据以备将来mutation。它只需要在primary 变得无法联系或回答说 "没有 "的情况下,再次联系master。 3.client将数据推送到所有的副本。client可以按任何顺序这样做。每个chunkserver将把数据存储在一个内部LRU缓冲区缓存中,直到数据被使用或老化。通过将数据流与控制流解耦,我们可以根据网络拓扑结构来调度昂贵的数据流,以提高性能,而不管哪个chunkserver是主服务器。第3.2节进一步讨论了这一点。 4.一旦所有的复制体都确认收到了数据,client就会向primary发送一个写请求。该请求确定了先前推送给所有复制体的数据。primary 将连续的序列号分配给它收到的所有mutation,可能来自多个client,这提供了必要的序列化。它按照序列号顺序将mutation应用到自己的本地状态。 5.primary 将写请求转发给所有次级副本。每个次级副本按照主副本分配的相同序列号顺序应用mutation。 6.二级副本都回复给主副本,表示他们已经完成了操作。 7.primary 对clients作出回复。在任何一个副本中遇到的错误都会报告给clients。在出现错误的情况下,写操作可能在主副本和次副本的任意子集上成功了。(如果它在主副本中失败了,它将不会被分配一个序列号并被转发)。client请求被认为是失败的,被修改的区域被留在一个不一致的状态中。我们的client代码通过重试失败的mutation来处理这种错误。它将在步骤(3)到(7)中进行几次尝试,然后再退回到从写的开始进行重试。
如果应用程序的写入量很大或者跨越了一个块的边界,GFSclient代码会将其分解成多个写入操作。它们都遵循上述的控制流程,但可能与其他client的并发操作交错并被覆盖。因此,共享文件区域最终可能包含来自不同client的片段,尽管副本将是相同的,因为各个操作在所有副本上都以相同的顺序成功完成。这使得文件区域处于一致但未定义的状态,如第2.7节所述。
3.2 数据流
我们将数据流与控制流解耦,以便有效地使用网络。当控制从client流向primary,然后流向所有辅助端时,数据则以流水线的方式沿着精心挑选的chunkservers链线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟链接,并尽量减少推送所有数据的延迟。 为了充分利用每台机器的网络带宽,数据沿chunkservers链线性推送,而不是以其他拓扑结构(如树形)分布。因此,每台机器的全部出站带宽被用来尽可能快地传输数据,而不是分给多个接收者。 为了尽可能避免网络瓶颈和高延迟链路(例如,交换机间链路往往都是),每台机器将数据转发到网络拓扑中未收到数据的 "最近的 "机器。假设client正在向chunkservers S1至S4推送数据。它将数据发送到最近的chunkserver,例如S1。S1通过S4将其转发给离S1最近的chunkserver S2,比如S2。同样,S2将数据转发给S3或S4,以离S2最近的为准,依次类推。我们的网络拓扑结构非常简单,"距离 "可以从IP地址中准确估计。 最后,我们通过对TCP连接的数据传输进行管道化处理,将延迟降到最低。一旦一个chunkserver收到一些数据,它就立即开始转发。管道化对我们特别有帮助,因为我们使用的是具有全双工链接的交换网络。立即发送数据并不会降低接收率。在没有网络拥堵的情况下,将B个字节传输到R个副本的理想耗时是B/T+RL,其中T是网络吞吐量,L是两台机器之间传输字节的延迟。我们的网络链接通常是100Mbps(T),而L远远低于1毫秒。 因此,在理想情况下,1MB可以在大约80毫秒内分发完毕。
3.3 原子记录追加
GFS提供了一个原子追加操作,叫做记录追加。在传统的写操作中,clients指定要写入数据的偏移量。对同一区域的并发写入是不可序列化的:该区域最终可能包含来自多个client的数据片段。然而,在记录附加中,client只指定数据。GFS至少在GFS选择的偏移量上将其追加到文件中一次(即作为一个连续的字节序列),并将该偏移量返回给client。这类似于在Unix中以O APPEND模式打开的文件的写入,当多个写入者同时这样做时,不会出现race情况。 记录追加被我们的分布式应用大量使用,其中不同机器上的许多client同时追加到同一个文件。client需要额外的复杂和昂贵的同步,例如通过分布式锁管理器,如果他们用传统的写法这样做。在我们的工作负载中,这样的文件经常作为多生产者/单消费者的队列,或者包含来自许多不同client的合并结果。 记录追加是一种突变,遵循第3.1节的控制流程,只在primary有一点额外的逻辑。client将数据推送到文件最后一块的所有副本中 然后,它将其请求发送给primary。primary检查将记录添加到当前块中是否会导致该块超过最大尺寸(64 MB)。如果是这样,它就将该块填充到最大尺寸,告诉第二块也要这样做,并回复client,指出该操作应该在下一个块上重试。(记录附加被限制为最大块大小的四分之一,以保持最坏的情况下碎片的可接受水平)。如果记录符合最大尺寸,也就是常见的情况,primary会将数据追加到它的副本中,告诉副副本在它所拥有的确切偏移处写入数据,最后将成功回复给client。 如果任何副本的记录追加失败,client会重试该操作。因此,同一个块的副本可能包含不同的数据,可能包括同一记录的全部或部分的重复。GFS并不保证所有的副本都在总体上是相同的。它只保证数据作为一个原子单元至少被写入一次。这个属性很容易从一个简单的观察中得出,为了使操作报告成功,数据必须在某个块的所有副本上以相同的偏移量被写入。此外,在这之后,所有的副本至少和记录的末端一样长,因此任何未来的记录都将被分配一个更高的偏移量或不同的块,即使后来不同的副本成为primary。就我们的一致性保证而言,成功的记录追加操作写入其数据的区域是确定的(因此是一致的),而中间的区域是不一致的(因此是未确定的)。我们的应用可以处理不一致的区域,正如我们在第2.7.2节所讨论的那样。
3.4快照
快照操作几乎是即时地复制一个文件或一个目录树("源"),同时尽量减少对正在进行的突变的干扰。我们的用户用它来快速创建巨大的数据集的分支副本(通常是这些副本的副本,递归),或者在试验改变之前对当前状态进行检查点,这些改变以后可以很容易地提交或回滚。 像AFS[5]一样,我们使用标准的写时拷贝技术来实现快照。当master收到一个快照请求时,它首先撤销了它要快照的文件中的任何未完成的租约。这就确保了随后对这些块的任何写入都需要与master互动,以找到租约持有人。这将给master一个机会,首先创建一个新的块的副本。 在租约被撤销或过期后,master将该操作记录到磁盘。然后,它通过复制源文件或目录树的元数据,将此日志记录应用于其内存状态。新创建的快照文件指向与源文件相同的区块。 client在快照操作后第一次想写到C块时,会向master发送一个请求,以找到当前的租赁持有人。master注意到,C块的引用计数大于1。它推迟了对clients请求的回复,而是选择了一个新的块处理C'。然后,它要求每个拥有C的当前副本的chunkserver创建一个名为C'的新块。通过在与原始数据相同的chunkserver上创建新的块,我们确保数据可以在本地复制,而不是通过网络复制(我们的磁盘速度是100Mb以太网链接的三倍)。从这一点上看,请求的处理与任何分块的处理没有什么不同:master授予其中一个副本一个新分块C'的租约,并回复给client,client可以正常写入分块,而不知道它是由一个现有分块创建的。
4.主操作
主操作执行所有命名空间的操作。此外,它还管理整个系统中的分块复制:它做出放置决定,创建新的分块,从而创建复制,并协调各种全系统的活动,以保持分块的完全复制,平衡所有分块服务器的负载,并回收未使用的存储。我们现在讨论这些主题中的每一个。
4.1命名空间管理和锁定
许多主操作可能需要很长的时间:例如,快照操作必须撤销快照所覆盖的所有块上的chunkserver租约。我们不希望在其他主操作运行的时候拖延它们。因此,我们允许多个操作处于活动状态,并对命名空间的区域使用锁以确保正确的序列化。 与许多传统的文件系统不同,GFS没有一个按目录的数据结构来列出该目录中的所有文件。它也不支持同一文件或目录的别名(即Unix术语中的硬链接或符号链接)。GFS在逻辑上将其命名空间表示为一个将全路径名映射到元数据的查询表。通过前缀压缩,这个表可以有效地在内存中表示。命名空间树中的每个节点(无论是绝对文件名还是绝对目录名)都有一个相关的读写锁。每个主操作在运行前都会获得一组锁。通常,如果它涉及/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又可以被来自相同或不同机架的数百个client访问。不同机架上的两台机器之间的通信可能跨越一个或多个网络交换机。此外,进入或离开一个机架的带宽可能低于该机架内所有机器的总带宽。多级分布对分布数据的可扩展性、可靠性和可用性提出了独特的挑战。 大块复制的放置策略有两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽的利用率。对于这两个目的来说,仅仅将副本分散到各台机器上是不够的,这只能防范磁盘或机器故障,并充分地利用每台机器的网络带宽。我们还必须将大块复制分散到各个机架上。这确保了即使整个机架被损坏或脱机(例如,由于网络交换机或电源电路等共享资源的故障),一个分块的一些副本将存活并保持可用。这也意味着,一个数据块的流量,特别是读取,可以利用多个机架的总带宽。另一方面,写流量必须流经多个机架,这是我们自愿做出的权衡。
4.3创建、再复制、再平衡
创建分块复制有三个原因:分块创建、再复制和再平衡。 当master创建一个大块时,它选择将最初的空复制放在哪里。它考虑了几个因素。(1) 我们希望将新的复制放在磁盘空间利用率低于平均水平的块服务器上。随着时间的推移,这将使各chunkservers的磁盘利用率达到平衡。(2) 我们希望限制每个chunkserver上 "最近 "创建的数量。虽然创建本身很便宜,但它可以可靠地预测即将到来的大量写入流量,因为分块是在写入需求时创建的,而在我们的append-once-read-many工作负载中,一旦它们被完全写入,它们通常实际上是只读的。(3) 如上所述,我们希望将一个块的复制分散到各个机架上。 一旦可用的复制数量低于用户指定的目标,master就会重新复制一个分块。这可能是由于各种原因发生的:一个chunkserver变得不可用,它报告说它的复制可能被破坏,它的一个磁盘由于错误而被禁用,或者复制目标被增加。每个需要重新复制的块都是根据几个因素来确定优先级的。一个是它离复制目标有多远。例如,我们对失去两个复制的数据块的优先级高于只失去一个复制的数据块。此外,我们倾向于首先重新复制活的文件的块,而不是属于最近删除的文件的块(见4.4节)。最后,为了最大限度地减少故障对运行中的应用程序的影响,我们提高了任何阻碍client进度的块的优先级。 master挑选优先级最高的分块,并通过指示一些分块服务器从现有的有效副本中直接复制分块数据来 "克隆 "它。新副本的放置目标与创建目标类似:均衡磁盘空间的利用,限制任何单一chunkserver上的活跃克隆操作,并将副本分散到各个机架上。为了防止克隆流量淹没client流量,master限制集群和每个chunkserver的活跃克隆操作的数量。此外,每个chunkserver通过节制其对源chunkserver的读取请求,限制其在每个克隆操作上花费的带宽。 最后,master定期对复制进行再平衡:它检查当前的复制分布,并移动复制以获得更好的磁盘空间和负载平衡。也是通过这个过程,master逐渐填满一个新的chunkserver,而不是立即用新的chunks和随之而来的大量写入流量来淹没它。新副本的放置标准与上面讨论的相似。此外,master还必须选择删除哪些现有的副本。一般来说,它更倾向于删除那些自由空间低于平均水平的chunkservers上的副本,以平衡磁盘空间的使用。
4.4 垃圾回收
在一个文件被删除后,GFS不会立即回收可用的物理存储。它只是在文件和块级的定期垃圾收集过程中懒洋洋地这样做。我们发现这种方法使系统更简单、更可靠。
4.4.1机制
当一个文件被应用程序删除时,主控端会像其他变化一样立即记录删除的情况。然而,该文件并没有立即回收资源,而是被重新命名为一个包括删除时间戳的隐藏名称。在master对文件系统命名空间的定期扫描中,如果任何这样的隐藏文件已经存在超过三天,它就会将其删除(时间间隔是可配置的)。在此之前,该文件仍然可以在新的、特殊的名称下被读取,并且可以通过将其重新命名为正常名称来撤销删除。当隐藏文件从命名空间中删除时,其内存中的元数据被删除。这有效地切断了它与所有块的联系。 在对块命名空间的类似定期扫描中,主控程序识别出无主的块(即那些无法从任何文件到达的块),并删除这些块的元数据。在与master定期交换的HeartBeat消息中,每个chunkserver报告它拥有的一个子集,master则回复所有不再出现在master元数据中的chunks的身份。chunkserver可以自由地删除其对这些块的复制。
4.4.2 讨论
尽管分布式垃圾收集是一个困难的问题,在编程语言的背景下需要复杂的解决方案,但在我们的案例中却相当简单。我们可以很容易地识别所有对块的引用:它们都在由master专门维护的文件到块的映射中。我们也可以很容易地识别所有的分块副本:它们是 每个chunkserver上指定目录下的Linux文件。 任何不为master所知的复制都是 "垃圾"。垃圾收集的存储回收方法比急切的删除有几个优点。首先,在一个大规模的分布式系统中,它是简单和可靠的,因为组件故障是很常见的。分块创建可能在一些分块服务器上成功,但在其他分块服务器上不成功,留下master不知道的副本。复本的删除信息可能会丢失,master必须记住在各种故障中重新发送这些信息,包括它自己和chunkserver的故障。垃圾收集提供了一种统一的、可靠的方式来清理任何不知道是否有用的复制。其次,它将存储回收并入master的常规后台活动中,如定期扫描命名空间和与chunkservers握手。因此,它是分批进行的,其成本是摊销的。此外,它只在master相对空闲时进行。master可以更迅速地响应需要及时关注的client请求。第三,回收存储的延迟提供了一个安全网,防止意外的、不可逆转的删除。 根据我们的经验,主要的缺点是,当存储空间紧张时,延迟有时会阻碍用户对使用的微调。反复创建和删除临时文件的应用程序可能无法立即重新使用存储。我们通过加快存储回收来解决这些问题,如果一个被删除的文件被明确地再次删除。我们还允许用户对名称空间的不同部分应用不同的复制和再生策略。例如,用户可以指定某个目录树内的所有文件块在不复制的情况下进行存储,而任何被删除的文件都会立即从文件系统状态中不可逆转地删除。
4.5 旧的复制检测
如果一个chunkserver发生故障,并且在它停机时错过了对chunk的突变,那么chunk的复制就可能变得陈旧。对于每个数据块,master维护一个数据块的版本号,以区分最新的和过时的复制。 每当master授予一个块的新租约,它就会增加块的版本号,并通知最新的副本。master和这些副本都在它们的持久化状态中记录新的版本号。这发生在任何client被通知之前,因此在它可以开始向该块写入之前。如果另一个副本目前不可用,其块的版本号将不会被提前。当chunkserver重新启动时,master将检测到这个chunkserver有一个陈旧的副本,并报告它的一组chunks和它们相关的版本号。如果master看到一个版本号大于其记录中的版本号,master就会认为它在授予租约时失败了,因此认为较高的版本是最新的。 master在其定期的垃圾收集中删除陈旧的复制。在此之前,当它回复client对块信息的请求时,它实际上认为陈旧的副本根本不存在。作为另一项保障措施,master在通知clients哪个chunkserver持有一个chunk的租约时,或者在克隆操作中指示一个chunkserver从另一个chunkserver读取该chunk时,会包括chunk的版本号。client或chunkserver在执行操作时,会验证版本号,这样它就能一直访问最新的数据。
5.容错和诊断
我们在设计系统时面临的最大挑战之一是处理频繁的组件故障。组件的质量和数量加在一起使这些问题成为常态而不是例外:我们不能完全信任机器,也不能完全信任磁盘。组件故障可能导致系统不可用,或者更糟糕的是,数据被破坏。我们讨论了我们如何应对这些挑战,以及我们在系统中建立的工具,以便在问题不可避免地发生时对其进行诊断。
5.1 高可用性
在一个GFS集群中的数百台服务器中,有些服务器在任何时候都是不可用的。我们通过两个简单而有效的策略保持整个系统的高可用性:快速恢复和复制。
5.1.1快速恢复
master和chunkserver都被设计成可以在几秒钟内恢复其状态并启动,无论它们是如何终止的。事实上,我们并不区分正常和非正常的终止;服务器通常只是通过杀死进程来关闭。客户端和其他服务器会经历一个小插曲,因为它们在未完成的请求上超时,重新连接到重新启动的服务器,并重新尝试。第6.2.2节报告了观察到的启动时间。
5.1.2分块复制
如前所述,每个分块都被复制到不同机架上的多个分块服务器上。用户可以为文件命名空间的不同部分指定不同的复制级别。默认的是三个。master根据需要克隆现有的复制,以保持每个块的完全复制,因为块服务器离线或通过检查和验证检测损坏的复制(见第5.2节)。尽管复制为我们提供了很好的服务,但我们正在探索其他形式的跨服务器冗余,如奇偶校验或擦除代码,以满足我们日益增长的只读存储需求。我们希望在我们非常松散的耦合系统中实现这些更复杂的冗余方案是具有挑战性的,但也是可以管理的,因为我们的流量主要是追加和读取,而不是小的随机写入。
5.1.3 master复制
master的状态被复制以保证可靠性。它的操作日志和检查点被复制到多个机器上。对状态的突变只有在其日志记录被刷新到本地和所有主副本的磁盘上之后,才被认为是提交。为了简单起见,一个主进程仍然负责所有的突变以及后台活动,如在内部改变系统的垃圾收集。当它发生故障时,它几乎可以立即重新启动。如果它的机器或磁盘出现故障,GFS之外的监控基础设施会在其他地方启动一个新的主进程,并附上复制的操作日志。客户端只使用master的规范名称(如gfs-test),这是一个DNS别名,如果master被重新安置到另一台机器上,它可以被改变。 此外,"影子 "master提供了对文件系统的只读访问,即使在primary master停机的时候。它们是影子,而不是镜像,因为它们可能略微滞后于primary ,通常是几分之一秒的时间。对于那些没有被主动突变的文件或不介意得到稍稍过时的结果的应用程序来说,它们增强了阅读的可用性。事实上,由于文件内容是从chunkservers读取的,应用程序不会观察到陈旧的文件内容。在短时间内可能变质的是文件元数据,如目录内容或访问控制信息。 为了保持自己的信息,影子master读取不断增长的操作日志的副本,并在其数据结构中应用与primary 完全相同的变化序列。像primary 一样,它在启动时(此后不经常)轮询chunkservers以定位chunk副本,并与它们频繁地交换握手信息以监控它们的状态。它只依赖于primary 的复制位置更新,这些更新是由primary 决定创建和删除复制而产生的。
5.2 数据的完整性
每个chunkserver都使用校验法来检测存储数据的损坏。鉴于GFS集群通常在数百台机器上有数千个磁盘,它经常遇到磁盘故障,导致数据损坏或在读和写路径上丢失。(原因之一见第7节。)我们可以使用其他块状复制来恢复损坏,但是通过比较各块状服务器的复制来检测损坏是不现实的。此外,不同的复制可能是合法的:GFS突变的语义,特别是前面讨论的原子记录追加,并不能保证相同的复制。因此,每个chunkserver必须通过维护校验和来独立验证自己副本的完整性。 一个块(chunk)被分解成64KB的块(blocks)。每个块都有一个相应的32位校验和。像其他元数据一样,校验和被保存在内存中,并与日志一起持久地存储,与用户数据分开。 对于读取,chunkserver在向请求者(无论是客户端还是其他chunkserver)返回任何数据之前,会验证与读取范围重叠的数据块的校验和。因此,chunkserver不会将损坏传播给其他机器。如果一个块与记录的校验和不匹配,chunkserver会向请求者返回一个错误,并向master报告不匹配。作为回应,请求者将从其他副本中读取,而主服务器将从另一个副本中克隆该块。在一个有效的新副本到位后,master会指示报告不匹配的chunkserver删除其副本。 由于一些原因,校验对读取性能的影响很小。由于我们大多数的读取至少跨越几个块,我们只需要读取和校验相对较少的额外数据来进行验证。GFS客户端代码通过尝试在校验块边界对齐读取,进一步减少了这种开销。此外,chunkserver上的校验和查找和比较是在没有任何I/O的情况下进行的,而且校验和计算往往可以与I/O重叠。 校验和计算对追加到块末尾的写入进行了大量优化(相对于覆盖现有数据的写入),因为它们在我们的工作负载中占主导地位。我们只需增量更新最后一个部分校验块的校验和,并为附加的任何全新的校验块计算新的校验和。即使最后一个部分校验块已经损坏,而我们现在未能检测到它,新的校验值将与存储的数据不匹配,并且在下次读取该块时,损坏将照常被检测到。 相反,如果一个写覆盖了块的现有范围,我们必须读取并验证被覆盖范围的第一个和最后一个块,然后执行写,最后计算并记录新的校验和。如果我们在部分覆盖前没有验证第一个和最后一个块,新的校验和可能会隐藏未被覆盖的区域中存在的腐败。 在空闲期间,chunkservers可以扫描和验证非活动块的内容。这使我们能够检测到很少被读取的块中的损坏。一旦检测到损坏,master可以创建一个新的未损坏的副本并删除损坏的副本。这可以防止一个不活动但已损坏的块副本欺骗master,使其认为它有足够的有效块副本。
5.3 诊断工具
广泛而详细的诊断日志对问题的隔离、调试和性能分析有不可估量的帮助,而产生的费用却很少。没有日志,就很难理解机器之间瞬时的、不可重复的互动。GFS服务器生成的诊断日志记录了许多重要的事件(如chunkservers的上升和下降)以及所有RPC请求和回复。这些诊断日志可以被自由删除,而不影响系统的正确性。然而,在空间允许的范围内,我们尽量保留这些日志。 RPC日志包括wire上发送的确切请求和响应,除了正在读或写的文件数据。通过匹配请求和响应,以及整理不同机器上的RPC记录,我们可以重建整个交互历史来诊断问题。日志还可以作为负载测试和性能分析的痕迹。 日志对性能的影响是最小的(而且远远超过其好处),因为这些日志是按顺序和异步写的。最近的事件也被保存在内存中,可用于持续的在线监测。
6.测评
测评我并不是很关心,所以只是粗略的看了一下,有兴趣可以查看原文
7. EXPERIENCES
在建立和部署GFS的过程中,我们遇到了各种各样的问题,有些是操作上的,有些是技术上的。 最初,GFS被设想为我们生产系统的后端文件系统。随着时间的推移,其用途逐渐演变为包括研究和开发任务。它开始时对诸如权限和配额的支持很少,但现在包括了这些的基本形式。虽然生产系统有很好的纪律和控制,但用户有时却不是这样。需要更多的基础设施来防止用户之间的相互干扰。 我们最大的一些问题是与磁盘和Linux有关。我们的许多磁盘向Linux驱动程序声称它们支持一系列的IDE协议版本,但实际上只对较新的版本作出可靠的反应。由于协议版本非常相似,这些硬盘大部分都能工作,但偶尔的不匹配会导致硬盘和内核对硬盘的状态产生分歧。这将会由于内核的问题而无声地破坏数据。这个问题促使我们使用校验和来检测数据损坏,同时我们也修改了内核来处理这些协议不匹配。 早些时候,由于fsync()的成本,我们在Linux 2.2内核中遇到了一些问题。它的成本与文件的大小成正比,而不是修改部分的大小。这对我们的大型操作日志来说是个问题,特别是在我们实施检查点之前。我们通过使用同步写来解决这个问题,并最终迁移到了Linux 2.4。 Linux的另一个问题是一个单一的读写锁,当一个地址空间中的任何线程从磁盘中翻页(读写锁)或在mmap()调用中修改地址空间(写入锁)时,必须持有这个锁。我们看到我们的系统在轻度负载下出现了瞬时超时,并努力寻找资源瓶颈或零星的硬件故障。最终,我们发现这个单一的锁阻止了主要的网络线程将新的数据映射到内存中,而磁盘线程正在将先前映射的数据分页。由于我们主要受到网络接口的限制,而不是内存拷贝带宽的限制,我们通过用pread()代替mmap()来解决这个问题,但要付出额外拷贝的代价。 尽管偶尔会出现问题,但是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、明尼苏达的GFS[11]和GPFS[10],取消了集中式服务器,并依靠分布式算法进行一致性和管理。我们选择集中式方法是为了简化设计,提高其可靠性,并获得灵活性。特别是,一个集中式的主服务器使得实现复杂的分块放置和复制策略变得更加容易,因为主服务器已经拥有大部分的相关信息,并控制其变化。我们通过保持主控状态的小规模和在其他机器上的完全复制来解决容错问题。目前,我们的影子主控机制提供了可扩展性和高可用性(针对读取)。对主控状态的更新是通过追加到一个写前日志来实现的。因此,我们可以采用像Harp[7]中的主拷贝方案来提供高可用性,并提供比我们目前方案更强的一致性保证。 我们正在解决一个类似于Lustre[8]的问题,即向大量的客户提供聚合性能。然而,我们通过关注我们应用程序的需求而不是建立一个符合POSIX标准的文件系统,大大简化了这个问题。此外,GFS假设有大量不可靠的组件,因此容错是我们设计的核心。 GFS与NASD架构[4]最为相似。虽然NASD架构是基于网络连接的磁盘驱动器,但GFS使用商品机作为分块服务器,就像在NASD原型中做的那样。与NASD的工作不同,我们的分块服务器使用懒散地分配固定大小的分块,而不是可变长度的对象。此外,GFS实现了诸如重新平衡、复制和恢复等生产环境中需要的功能。 与明尼苏达的GFS和NASD不同,我们不寻求改变存储设备的模型。我们专注于用现有的商品组件解决复杂的分布式系统的日常数据处理需求。 原子记录追加所启用的生产者-消费者队列解决了一个与River[2]中的分布式队列类似的问题。River使用分布在机器上的基于内存的队列和仔细的数据流控制,而GFS使用一个持久的文件,可以被许多生产者同时追加。River模型支持m-to-n分布式队列,但缺乏持久性存储带来的容错能力,而GFS只有效支持m-to-1队列。多个消费者可以读取同一个文件,但他们必须协调来划分传入的负载。
9.结论
谷歌文件系统展示了在商品硬件上支持大规模数据处理工作负载所必需的品质。虽然有些设计决定是针对我们独特的环境的,但许多决定可能适用于类似规模和成本意识的数据处理任务。我们首先根据我们当前和预期的应用工作负载和技术环境,重新审视了传统的文件系统假设。我们的观察导致了设计空间中根本不同的点。我们将组件故障视为常态而非例外,对主要是追加(可能是并发)然后读取(通常是顺序)的巨大文件进行优化,并同时扩展和放松标准文件系统接口以改善整个系统。 我们的系统通过持续监控、复制关键数据以及快速和自动恢复来提供容错。分块复制使我们能够容忍分块服务器的故障。这些故障的频率促使我们采用了一种新颖的在线修复机制,定期透明地修复损害,并尽快补偿损失的副本。此外,我们使用校验法来检测磁盘或IDE子系统层面的数据损坏,鉴于系统中的磁盘数量,这种情况变得非常普遍。 我们的设计为执行各种任务的许多并发的读者和写者提供了高聚合吞吐量。我们通过分离文件系统控制(通过主控)和数据传输(直接在chunkservers和客户之间传输)来实现这一点。通过一个大的分块大小和分块租赁,master在普通操作中的参与程度降到了最低,分块租赁将权力下放到数据突变中的主要副本。这使得一个简单的、集中的master成为可能,不会成为一个瓶颈。我们相信,我们的网络堆栈的改进将解除目前对单个客户端所看到的写入吞吐量的限制。 GFS已经成功地满足了我们的存储需求,并在谷歌内部广泛使用,作为研发和生产数据处理的存储平台。它是一个重要的工具,使我们能够在整个网络的规模上继续创新和攻击问题。
Case Study: GFS- Evolution on Fast-forward
在谷歌发展的早期阶段,最初的想法并不包括建立一个新的文件系统的计划。然而,当工作仍在进行时,该公司最早的一个抓取和索引系统的版本,对核心工程师来说,他们真的没有其他选择,GFS(谷歌文件系统)就这样诞生了。
首先,鉴于谷歌的目标是用廉价的商品硬件建立一个庞大的存储网络,必须假设组件故障将是常态,这意味着持续监控、错误检测、容错和自动恢复必须是文件系统的一个组成部分。此外,即使按照谷歌最早的估计,该系统的吞吐量要求以任何人的标准来看都是令人生畏的--拥有数千兆字节的文件和包含TB级信息和数百万对象的数据集。显然,这意味着必须重新审视关于I/O操作和块大小的传统假设。还有一个问题是可扩展性。这是一个文件系统,肯定需要像其他系统一样扩展。当然,在最初的那些日子里,没有人能够想象到需要多少可扩展性。他们很快就会了解到这一点。
然而,近十年过去了,谷歌的大部分令人难以置信的数据存储和不断增长的应用程序仍然依赖于GFS。一路走来,对文件系统进行了许多调整,再加上在使用GFS的应用程序中实施的相当数量的调整,它们使这一旅程成为可能。
为了探索一些更关键的初始设计决策背后的原因,以及此后所做的一些渐进式调整,ACM请Sean Quinlan揭开了不断变化的文件系统需求和谷歌不断发展的思路的盖子。由于Quinlan曾担任过几年的GFS技术负责人,并且现在继续担任谷歌的首席工程师,所以他在提供这一观点方面处于有利地位。作为Googleplex以外的落脚点,ACM邀请Kirk McKusick来领导讨论。他因在BSD(Berkeley Software Distribution)Unix上的工作而闻名,包括Berkeley FFS(快速文件系统)的最初设计。
讨论从一开始就适当地开始了--以单主站设计为基础的最初GFS实现的非正统决定。乍一看,单一集中式主站成为带宽瓶颈的风险,或者更糟糕的是,成为一个单点故障,似乎是相当明显的,但事实证明,谷歌的工程师做出这样的选择有他们的理由。
MCKUSICK 最初的GFS架构的一个更有趣也更重要的方面是决定将其建立在一个单一的主站之上。你能向我们介绍一下导致这一决定的原因吗?
QUINLAN 使用单一主站的决定实际上是最初的决定之一,主要是为了简化整个设计问题。也就是说,从一开始就建立一个分布式主站被认为是太困难了,而且会花费太多的时间。另外,通过采用单主站的方法,工程师们能够简化很多问题。有一个集中的地方来控制复制、垃圾收集和许多其他活动,肯定比在分布式基础上处理这些问题要简单。因此,决定将其集中在一台机器上。
MCKUSICK 这主要是为了能够在一个合理的短时间内推出一些东西吗?
QUINLAN 是的。事实上,参与早期工作的一些工程师后来又建立了BigTable,一个分布式存储系统,但这一工作花了很多年。围绕单一主站建立最初的GFS的决定,确实有助于比其他方式更迅速地把东西送到用户手中。
此外,在勾画他们预期的使用案例时,单主站设计似乎不会造成太大的问题。他们当时所考虑的规模是以数百兆字节和几百万个文件为框架的。事实上,这个系统一开始就运作良好。
MCKUSICK 但是后来呢?
QUINLAN 一旦基础存储的大小增加,问题就开始出现了。从几百兆字节到几千兆字节,再到几万兆字节,这确实需要按比例增加主系统所需维护的元数据量。此外,诸如扫描元数据以寻找恢复的操作也会随着数据量的增加而线性增加。因此,主站所需的工作量大幅增加。为保留所有这些信息所需的存储量也在增加。
此外,这被证明是clients的一个瓶颈,尽管客户本身很少进行元数据操作--例如,客户在进行打开操作时与主站对话。当你有数以千计的客户同时与主站对话时,考虑到主站每秒只能进行几千次操作,一般的客户并不能每秒指挥那么多操作。还要记住,有一些应用,如MapReduce,你可能突然有一千个任务,每个任务都想打开一些文件。很明显,处理所有这些请求需要很长的时间,主控器会受到相当大的胁迫。
MCKUSICK 现在,在GFS目前的模式下,你每个单元有一个主站,对吗?
QUINLAN 这是对的。
MCKUSICK 历史上,你们每个数据中心有一个单元,对吗?
QUINLAN 那是最初的目标,但在很大程度上并没有成功--部分原因是单主站设计的限制,部分原因是隔离被证明是困难的。因此,人们通常会在每个数据中心使用一个以上的单元。我们也最终采用了我们称之为 "多单元 "的方法,这基本上使得在一个分块服务器池上放置多个GFS主站成为可能。这样一来,分块服务器可以被配置为有八个GFS主站,这将给你提供至少一个底层存储池--如果你愿意的话,上面有多个主站头。然后,应用程序负责在这些不同的单元中进行数据划分。
MCKUSICK 据推测,每个应用程序基本上都有自己的主机,负责管理自己的小文件系统。基本上是这样的想法吗?
QUINLAN 嗯,是的,也不是。应用程序将倾向于使用一个主站或一组小的主站。我们也有一些我们称之为名称空间的东西,这只是一种非常静态的划分名称空间的方式,人们可以用它来隐藏所有这些实际的应用程序。日志处理系统提供了这种方法的一个例子:一旦日志用尽了他们只使用一个单元的能力,他们就会转移到多个GFS单元;一个命名空间文件描述了日志数据如何在这些不同的单元中进行分区,基本上起到了向应用程序隐藏确切分区的作用。但这都是相当静态的。
MCKUSICK 考虑到这些,性能如何?
QUINLAN 我们最终把相当多的精力放在了调整主程序的性能上,而谷歌把大量的工作放在调整任何一个特定的二进制程序上是不太典型的。一般来说,我们的方法是让事情运行得相当好,然后把我们的重点转向可扩展性--这通常很有效,因为你通常可以通过扩展事情来恢复你的性能。因为在这个例子中,我们有一个单一的瓶颈,开始对操作产生影响,然而,我们觉得投资一点额外的努力,使主控器的重量更轻,将是真正值得的。在从数以千计的操作扩展到数以万计甚至更多的过程中,单一的主站已经在某种程度上不再是一个瓶颈。在这种情况下,更多地关注一个二进制文件的效率,肯定有助于保持GFS比其他方式更长时间的运行。
可以说,在创纪录的时间内使GFS准备好投入生产,这本身就是一个胜利,通过加速谷歌进入市场,这最终为公司的成功做出了巨大的贡献。一个由三人组成的团队负责所有这些--GFS的核心--并在不到一年的时间内准备好部署该系统。
但是,任何成功的系统往往都要付出代价--也就是说,一旦规模和用例有时间扩展到远远超出任何人可能想象的程度。在谷歌的案例中,这些压力被证明是特别强烈的。
虽然企业没有交换文件系统统计数据的习惯,但可以假定GFS是正在运行的最大的文件系统(事实上,即使在谷歌收购YouTube之前,这可能是真的)。因此,尽管GFS的最初设计者认为他们已经为至少几个数量级的增长提供了充分的条件,但谷歌很快就放大了这一点。
此外,GFS被要求支持的应用程序的数量很快就膨胀了。在对最初的GFS架构师之一霍华德-戈比奥夫的采访中(就在他于2008年初意外去世之前),他回忆说:"我们所有最早的GFS版本的原始消费者基本上是这个巨大的抓取和索引系统。第二波是当我们的质量团队和研究小组开始相当积极地使用GFS时--基本上,他们都希望使用GFS来存储大型数据集。然后,没过多久,我们就有了50个用户,他们都需要不时地得到一点支持,以便他们都能很好地相互协作。"
对我们帮助很大的一件事是,谷歌不仅建立了文件系统,而且还建立了运行在它上面的所有应用程序。虽然GFS不断进行调整以使其更适应所有的新用例,但应用程序本身的开发也考虑到了GFS的各种优势和劣势。Gobioff总结说:"因为我们建立了一切,所以我们可以随时作弊,"。"我们可以在应用空间和文件系统空间之间来回推送问题,然后在两者之间进行调和。"
然而,规模庞大的问题需要一些更实质性的调整。一种应对策略是在整个网络中使用多个 "单元",基本上作为相关但不同的文件系统运作。除了帮助处理直接的规模问题外,这被证明是对广泛分散的数据中心的运作的一种更有效的安排。
快速增长也给最初的GFS设计的另一个关键参数带来了压力:选择将64MB作为标准块大小。当然,这比典型的文件系统块大小要大得多,但只是因为谷歌的抓取和索引系统产生的文件异常大。然而,随着时间的推移,应用程序的组合发生了变化,必须找到方法让系统有效地处理大量的文件,这些文件所需的容量远远小于64MB(例如,从Gmail的角度考虑)。问题不在于文件数量本身,而在于所有这些文件对集中式主机的内存需求,从而暴露了原始GFS设计中固有的瓶颈风险之一。
MCKUSICK 我从GFS的原始论文[Ghemawat, S., Gobioff, H., Leung, S-T. 2003. 谷歌文件系统。SOSP(ACM操作系统原理研讨会)],文件数量一直是你们的一个重要问题。你能稍微说说这个问题吗?
QUINLAN 文件数问题出现得相当早,因为人们最终围绕GFS设计他们的系统。让我举一个具体的例子。在我在谷歌工作的早期,我参与了日志处理系统的设计。我们最初的模式是,前端服务器写一个日志,然后我们基本上将其复制到GFS中进行处理和存档。开始的时候,这很好,但后来前端服务器的数量增加了,每个服务器每天都在滚动日志。同时,日志类型的数量也在增加,然后你会发现前端服务器会经历崩溃循环,产生更多的日志。因此,我们最终得到的文件比我们根据最初估计的要多得多。
这成为我们不得不关注的一个领域。最后,我们不得不承认,我们不可能在文件数量继续增长的情况下生存下去。
MCKUSICK 让我确定我的理解是正确的:你们的文件数量增长问题是由于你们需要在主文件上为每个文件提供一块元数据,而该元数据必须适合主文件的内存。
QUINLAN 这是对的。
MCKUSICK,在主站的内存耗尽之前,你能容纳的文件数量是有限的?
QUINLAN 是的。而且有两个元数据位。一个是识别文件,另一个是指出支持该文件的块。如果你有一个只包含1MB的块,它将只占用1MB的磁盘空间,但它仍然需要主站上的这两比特元数据。如果你的平均文件大小最终降到了64MB以下,你的主文件上的对象数量与你的存储空间的比例就开始下降了。这就是你遇到问题的地方。
回到那个日志的例子,我们很快就发现,我们所想到的自然映射--在我们做信封式估算时似乎是非常合理的--最后发现根本不能接受。我们需要找到一种方法来解决这个问题,那就是想办法把一些基础对象合并到更大的文件中。就日志而言,这并不完全是火箭科学,但它确实需要大量的努力。
MCKUSICK 这听起来像过去的日子,当时IBM只有最低限度的磁盘分配,所以它为你提供了一个工具,让你把一堆文件打包在一起,然后为它创建一个目录。
QUINLAN 正是这样。对我们来说,每个应用程序基本上都在不同程度上结束了这样做。事实证明,对一些应用程序来说,这比其他应用程序的负担要轻。就我们的日志而言,我们并没有真正计划删除单个日志文件。更有可能的是,我们最终会重写日志,使其匿名化,或沿着这些思路做一些其他事情。这样,你就不会有垃圾收集的问题,如果你只删除一个捆绑文件中的一些文件,就会出现这种问题。
然而,对于其他一些应用来说,文件数量的问题更加严重。很多时候,一些应用的最自然的设计并不适合GFS--即使乍一看你认为文件数是完全可以接受的,但它会变成一个问题。当我们开始使用更多的共享单元时,我们对文件数和存储空间都设置了配额。到目前为止,人们最终遇到最多的限制是文件数配额。相比之下,基础存储配额很少被证明是一个问题。
MCKUSICK 你想出了什么长期的策略来处理文件数量的问题?当然,分布式主站似乎并不能真正帮助解决这个问题--如果主站仍然需要在内存中保留所有的元数据,那就不是了。
QUINLAN 分布式主站当然允许你增加文件数量,与你愿意投入的机器数量相一致。这当然有帮助。
分布式多主机模型的吸引力之一是,如果你把所有的东西都扩大两个数量级,那么降低到1MB的平均文件大小将与64MB的平均文件大小有很大不同。如果你最终低于1MB,那么你也会遇到其他问题,你真的需要小心。例如,如果你最终不得不读取10,000个10-KB的文件,你将会比你只是读取100个1-MB的文件做更多的搜索。
我的直觉是,如果你为平均1MB的文件大小而设计,那么这应该比假设64MB的平均文件大小的设计提供更大的东西。理想的情况是,你希望想象一个系统能一直缩小到更小的文件大小,但在我们的环境中,1MB似乎是一个合理的妥协。
MCKUSICK 你一直在做什么来设计GFS来处理1MB的文件?
QUINLAN 我们没有对现有的GFS设计做什么。我们的分布式主系统将提供1MB的文件,基本上是一个全新的设计。这样,我们的目标是每个主站有1亿个文件的数量。你也可以有数百个主站。
MCKUSICK 所以,基本上没有一个主站会有所有这些数据?
QUINLAN 这就是我们的想法。
最近,谷歌内部出现了BigTable,这是一个用于管理结构化数据的分布式存储系统,现在有了一个解决文件数量问题的潜在补救办法--尽管可能不是最好的办法。
然而,BigTable的意义远远超过了文件数量。具体来说,它的设计是为了在数百或数千台机器上扩展到PB级的范围,同时也是为了方便向系统添加更多的机器,并自动开始利用这些资源而无需重新配置。对于一家以利用集体力量、潜在冗余和大规模部署商品硬件所固有的规模经济为理念的公司来说,这些确实是重大优势。
因此,BigTable现在与越来越多的谷歌应用程序一起使用。尽管它代表了与过去的不同,但也必须指出,BigTable是建立在GFS上的,在GFS上运行,并且在设计上有意识地与大多数GFS原则保持一致。因此,请将其视为沿途所做的主要调整之一,以帮助GFS在面对快速和广泛的变化时保持活力。
MCKUSICK 你现在有一个叫做BigTable的东西。你认为这本身就是一种应用吗?
QUINLAN 从GFS的角度来看,它是一个应用程序,但它显然更像是一个基础设施。
MCKUSICK 如果我理解正确的话,BigTable本质上是一个轻量级的关系型数据库。
QUINLAN 它不是一个真正的关系型数据库。我的意思是,我们不做SQL,而且它并不真正支持连接之类的。但BigTable是一个结构化的存储系统,让你拥有大量的键值对和一个模式。
MCKUSICK 谁是BigTable的真正客户?
QUINLAN BigTable越来越多地被用于谷歌的抓取和索引系统,我们在许多面向客户的应用中也大量使用它。事实是,BigTable有大量的客户。基本上,任何有大量小数据项的应用都倾向于使用BigTable。尤其是在有相当结构化数据的地方,情况更是如此。
MCKUSICK 我想我在这里真正想提出的问题是。BigTable是否只是作为处理小文件问题的一种尝试而被塞进了许多这些应用中,基本上是通过采取一大堆小东西,然后把它们聚集在一起?
QUINLAN 这当然是BigTable的一个用例,但它实际上是为了解决一个更普遍的问题。如果你以这种方式使用BigTable--也就是说,作为一种解决文件数量问题的方式,你可能会使用文件系统来处理这个问题--那么,无论如何,你最终都不会采用BigTable的所有功能。BigTable对于这个目的来说并不理想,因为它需要为自己的操作提供非微不足道的资源。另外,它的垃圾收集策略也不是超级积极的,所以这可能不是使用空间的最有效方式。我想说的是,那些纯粹使用BigTable来处理文件数问题的人可能并不太高兴,但毫无疑问,它是人们处理这个问题的一种方式。
MCKUSICK 我所读到的关于GFS的内容似乎表明,其想法是只有两种基本的数据结构:日志和SSTables(分类字符串表)。因为我猜测SSTables必须用来处理键值对之类的东西,这与BigTable有什么不同?
QUINLAN 主要的区别是,SSTables是不可变的,而BigTable提供了可变的键值存储,还有很多其他的。BigTable本身实际上是建立在日志和SSTables之上的。最初,它将传入的数据存储到交易日志文件中。然后,它被压缩--我们称之为压缩--到一系列的SST表中,这些表又随着时间的推移被压缩到一起。在某些方面,它让人想起了日志结构的文件系统。总之,正如你所观察到的,日志和SSTables似乎是我们大多数数据结构方式的两种数据结构。我们用日志文件来记录易变的东西,因为它正在被记录。然后,一旦你有了足够的数据,你就对其进行分类,并将其放入这个有索引的结构中。
尽管GFS不提供Posix接口,但它仍然有一个相当通用的文件系统接口,所以人们基本上可以自由地编写他们喜欢的任何类型的数据。只是,随着时间的推移,我们的大多数用户最终都使用这两种数据结构。我们还有一种叫做协议缓冲区的东西,这是我们的数据描述语言。大部分的数据最终都是这两个结构的协议缓冲区。
两者都提供了压缩和校验功能。尽管内部有一些人最终重新发明了这些东西,但大多数人只是满足于使用这两个基本的构建模块。
因为GFS最初是为了实现抓取和索引系统而设计的,吞吐量就是一切。事实上,关于该系统的原始论文就很明确地说明了这一点。"高持续带宽比低延迟更重要。我们的大多数目标应用都重视以较高的速度处理大量的数据,而很少有人对单个读写有严格的响应时间要求。"
但后来谷歌或者开发了或者接受了许多面向用户的互联网服务,对于这些服务来说,情况绝对不是这样的。
这立即暴露了GFS的一个缺点,与最初的单主站设计有关。单点故障对于面向批处理的应用来说可能不是一场灾难,但对于延迟敏感的应用,如视频服务,这肯定是不可接受的。后来增加的自动故障转移功能有所帮助,但即使如此,服务也可能中断一分钟。
当然,GFS的另一个主要挑战是如何在一个完全不同的优先级设计的文件系统上建立对延迟敏感的应用程序。
MCKUSICK 有充分的证据表明,最初设计GFS时强调的是批处理效率,而不是低延迟。现在,这又给你们带来了麻烦,特别是在处理诸如视频等事情方面。你是如何处理这个问题的?
QUINLAN GFS的设计模型从一开始就是为了实现吞吐量,而不是为了可能实现的延时。给你一个具体的例子,如果你正在写一个文件,它通常是以三倍的方式写的,这意味着你实际上是在向三个分块服务器写。如果这些分块服务器中的一个死亡或长时间停滞,GFS主站将注意到这个问题,并安排我们所说的pullchunk,这意味着它将基本上复制这些分块中的一个。这将使你恢复到三个副本,然后系统将把控制权交还给客户,客户将继续写入。
当我们做一个pullchunk时,我们把它限制在每秒5-10MB的范围内。所以,对于64MB来说,你说的是10秒的恢复时间。还有很多其他类似的事情,可能需要10秒到1分钟,这对批处理型操作来说是很好的。如果你正在做一个大型的MapReduce操作,只要其中一个项目不是一个真正的散兵游勇,你就没有问题,在这种情况下,你就有了一个不同的问题。不过,一般来说,在一个小时的批处理作业过程中,一分钟左右的小插曲并不会真正显现出来。然而,如果你在Gmail上工作,而你正试图写一个代表某些用户行为的突变,那么被卡住一分钟就真的会把你搞乱。
我们的主站故障转移也有类似的问题。最初,GFS没有自动主站故障转移的规定。它是一个手动过程。虽然这并不经常发生,但每当它发生时,cell可能会瘫痪一个小时。即使是我们最初的主故障切换实施也需要几分钟的时间。然而,在过去的一年里,我们已经把它减少到几十秒的程度。
MCKUSICK 但是,对于面向用户的应用程序来说,这是不可以接受的。
QUINLAN 对。虽然这些情况--你必须提供故障转移和错误恢复--在批处理情况下可能是可以接受的,但对于面向用户的应用程序来说,从延迟的角度来看,它们绝对是不行的。另一个问题是,在设计中有些地方,我们试图通过将成千上万的操作转入一个队列,然后直接处理它们来优化吞吐量。这导致了良好的吞吐量,但对延迟来说却不是很好。你很容易陷入这样的情况:你可能会在一个队列中停留几秒钟,只是等待进入队列的头部。
我们的用户群肯定已经从基于MapReduce的世界迁移到了更多依赖BigTable等东西的互动世界。Gmail就是一个明显的例子。在GFS方面,视频并不那么糟糕,因为你可以进行数据流,这意味着你可以缓冲。然而,试图在一个从一开始就被设计为支持更多的批处理操作的文件系统之上建立一个交互式数据库,这无疑被证明是一个痛点。
MCKUSICK 你到底是如何处理这个问题的?
QUINLAN 在GFS内部,我们已经设法在某种程度上改善了事情,主要是通过设计应用程序来处理出现的问题。以BigTable为一个很好的具体例子。BigTable交易日志实际上是获取交易记录的最大瓶颈。实际上,我们决定,"好吧,我们将在这些写入中看到hiccups ,所以我们要做的是在任何时候都有两个日志打开。然后我们将基本上合并这两个。我们会写到一个,如果卡住了,我们会写到另一个。一旦我们做了重放,我们就会合并这些日志--如果我们需要做重放的话。" 我们倾向于将我们的应用程序设计成这样的功能--也就是说,他们基本上试图隐藏延迟,因为他们知道下面的系统并不是真的那么好。
建立Gmail的人采用了多宿主模式,所以如果你的Gmail账户的一个实例被卡住了,你基本上就会被转移到另一个数据中心。实际上,无论如何都需要这种能力,以确保可用性。不过,部分动机是他们想隐藏GFS的问题。
MCKUSICK 我认为可以这样说,通过转移到分布式主文件系统,你肯定能够解决其中的一些延迟问题。
QUINLAN 这当然是我们的设计目标之一。另外,BigTable本身是一个非常具有故障意识的系统,它试图对故障作出比以前更迅速的反应。将其作为我们的元数据存储,也有助于解决其中的一些延迟问题。
从事GFS最早版本工作的工程师们,只要他们觉得有必要,就不会对文件系统设计中的传统选择感到羞涩。碰巧的是,对一致性采取的方法是该系统的一个方面,这一点特别明显。
当然,这其中有一部分是由必要性驱动的。由于谷歌的计划在很大程度上依赖于商品硬件的大规模部署,故障和硬件相关的故障是必然的。除此之外,根据最初的GFS论文,还有一些兼容性问题。"我们的许多磁盘向Linux驱动程序声称它们支持一系列的IDE协议版本,但实际上只对较新的版本作出可靠的反应。由于协议版本非常相似,这些硬盘大部分都能工作,但偶尔的不匹配会导致硬盘和内核对硬盘的状态产生分歧。这将会由于内核的问题而无声地破坏数据。这个问题促使我们使用校验和来检测数据损坏"。
然而,这并不意味着只是任何检查和,而是严格的端到端检查和,着眼于从磁盘损坏到TCP/IP损坏到机器背板损坏的一切。
有趣的是,对于所有这些校验的警惕性,GFS工程团队还选择了一种按文件系统标准相对宽松的一致性方法。基本上,GFS只是接受有的时候人们最终会读到稍微陈旧的数据。由于GFS主要是作为一个只附加的系统,而不是一个覆盖的系统,这通常意味着这些人可能最终会错过一些在他们已经打开文件后被附加到文件末尾的东西。对GFS的设计者来说,这似乎是一个可以接受的代价(尽管事实证明,在一些应用中,这证明是有问题的)。
另外,正如Gobioff所解释的,"在某些情况下,数据过期的风险只是一个高度分布式的架构所固有的,它并不要求主站维护那么多的信息。如果我们愿意把更多的数据倒入主控系统,然后让它维护更多的状态,我们肯定可以把事情做得更紧。但这对我们来说真的不是那么关键。"
也许这里更重要的问题是,做出这一决定的工程师不仅拥有文件系统,而且还拥有打算在文件系统上运行的应用程序。Gobioff说:"问题是,我们同时控制了横向和纵向--文件系统和应用程序。因此,我们可以确保我们的应用程序知道该从文件系统中期待什么。我们只是决定把一些复杂的东西推给应用程序,让它们来处理。"
尽管如此,在谷歌,还是有一些人怀疑这是否是正确的决定,如果只是因为人们在多次阅读一个特定文件的过程中有时会获得不同的数据,这往往与他们对数据存储应该如何工作的整个概念有很大的出入。
MCKUSICK 我们来谈谈一致性问题。问题似乎是需要一定的时间才能将所有的东西完全写入所有的副本中。我想你早些时候说过,GFS基本上要求在你继续之前,所有的东西都被完全写入。
QUINLAN 这是对的。
MCKUSICK 如果是这样,那么你怎么可能会出现不一致的事情呢?
QUINLAN 客户端故障有一种方法可以把事情弄得一团糟。基本上,GFS的模型是,客户端继续推动写入,直到它成功。如果客户端在操作过程中崩溃了,事情就会处于一种不确定的状态。
早期,这被认为是可以的,但随着时间的推移,我们收紧了可以容忍这种不一致的时间窗口,然后我们慢慢地继续减少。否则,只要数据处于这种不一致的状态,你就可能得到不同长度的文件。这可能会导致一些混乱。我们不得不有一些后门接口来检查这些情况下的文件数据的一致性。我们也有一个叫做RecordAppend的东西,这是一个为多个写作者同时追加到一个日志而设计的接口。那里的一致性被设计得非常松散。现在回想起来,这比任何人预期的都要痛苦得多。
MCKUSICK 到底是什么松动?如果primary为每次写入挑选偏移量,然后确保实际发生,我不知道哪里会出现不一致的情况。
QUINLAN 发生的情况是,primary 将尝试。它将选择一个偏移量,它将进行写入,但其中一个实际上不会被写入。然后主程序可能会改变,这时它可以选择一个不同的偏移。RecordAppend也没有提供任何重放保护。你可能最终会在文件中多次获得数据。
甚至有的情况下,你可以以不同的顺序获得数据。它可能在一个分块复制中出现多次,但不一定在所有的复制中出现。如果你在阅读文件,你可以在不同时间以不同方式发现数据。在记录层面,你可以以不同的顺序发现记录,这取决于你碰巧在读哪个块。
MCKUSICK 这是在设计上做的吗?
QUINLAN 在当时,这一定是个好主意,但现在回想起来,我认为大家的共识是,事实证明它的痛苦比它的价值要大。它只是没有满足人们对文件系统的期望,所以他们最终得到了惊喜。然后他们不得不想出变通的办法。
MCKUSICK 现在回想起来,你会如何以不同的方式处理这个问题?
QUINLAN 我认为每个文件有一个写入者更有意义。
MCKUSICK 好吧,但是当你有多个人想要追加到一个日志时,会发生什么?
QUINLAN 你通过一个单一的进程来序列化写入,这样可以确保副本的一致性。
MCKUSICK 还有这样的业务,你基本上是快照一个块。据推测,这是你在替换一个副本时使用的东西,或者当一些块服务器发生故障时,你需要替换它的一些文件。
QUINLAN 实际上,这里有两件事情要做。第一,正如你所建议的,是恢复机制,这肯定涉及到文件副本的复制。在GFS中的工作方式是,我们基本上撤销了锁,这样客户就不能再写了,这就是我们所说的延迟问题的一部分。
还有一个单独的问题,那就是支持GFS的快照功能。GFS拥有你能想象的最通用的快照功能。你可以在某个地方对任何目录进行快照,然后两个副本将完全等同。它们将共享未改变的数据。你可以改变任何一个,你可以进一步快照任何一个。所以它实际上更像是一个克隆,而不是大多数人认为的快照。这是一个有趣的事情,但它也带来了一些困难--特别是当你试图建立更多的分布式系统时,你可能想对文件树的更大块进行快照。
我还认为有趣的是,快照功能没有被更多地使用,因为它实际上是一个非常强大的功能。也就是说,从文件系统的角度来看,它确实提供了一个相当不错的功能。但是把快照放到文件系统中,我相信你也知道,是一件非常痛苦的事情。
MCKUSICK: 我知道。我已经做了。这是很痛苦的,尤其是在一个覆盖的文件系统中。
QUINLAN 正是如此。这是一个我们没有作弊的案例,但从实施的角度来看,很难创建真正的快照。不过,在这种情况下,进行全面的交易似乎是一个正确的决定。同样,它与早期在语义方面做出的其他一些决定形成了有趣的对比。
总而言之,近10年后关于GFS的成绩单似乎是积极的。可以肯定的是,有一些问题和缺点,但是谷歌的成功是无可争议的,而GFS无疑在其中发挥了重要作用。更重要的是,考虑到谷歌的运营规模已经超出了该系统设计的任何数量级,而谷歌目前支持的应用组合是任何人在90年代末都无法想象的,其持久力已经非常了不起了。
不过,毫无疑问,GFS现在面临着许多挑战。首先,在一个最初为批处理系统吞吐量而设计的系统之上,支持不断增长的面向用户的、对延迟敏感的应用程序的尴尬是显而易见的事情。
在这方面,BigTable的出现起到了一定的作用。然而,事实证明,BigTable实际上并不完全适合GFS。事实上,它只是使系统的单主站设计的瓶颈限制比其他情况下更加明显。
由于这些原因和其他原因,谷歌的工程师们在过去两年的大部分时间里一直在研究一个新的分布式主系统,旨在充分利用BigTable来解决一些被证明对GFS特别困难的问题。
因此,现在看来,除了为确保GFS继续生存而进行的所有调整之外,进化树上的最新分支在未来几年内将继续增长其重要性。
Last updated