Page tree
Skip to end of metadata
Go to start of metadata

摘要

我们已经设计并实现了Google文件系统,这是一种可扩展的分布式文件系统,适用于大型分布式数据密集型应用程序。它可以在廉价的商用硬件上运行,同时提供了容错功能,并且为大量客户端提供了高聚合性能。

虽然我们的设计与以前的分布式文件系统有许多相同的目标,但是我们的设计是由我们的应用程序工作负载和技术环境(包括当前的和预期的)的观察结果驱动的,这些观察结果反映出我们对早期文件系统的假设存在明显的偏离。这促使我们重新审视传统的选择,探索完全不同的设计要点。

这个文件系统已经成功地满足了我们的存储需求。在Google内,它被大范围地部署,作为存储平台,用于生成和处理我们服务使用的数据,以及需要大型数据集的研发工作。到目前为止,最大的集群使用1000多台机器上的数千个磁盘提供了数百TB的存储,并且数百个客户端可以并发访问。

在本文中,我们介绍了用于支持分布式应用程序的文件系统接口扩展,讨论了我们设计的许多方面,并且报告了微观基准测试和实际使用的测量结果。

类别和主题描述符

D [4]: 3 — 分布式文件系统

通用术语

设计、可靠性、性能、测量

关键词

容错、可伸缩性、数据存储、集群存储

1. 序言

我们设计并实现了Google文件系统(GFS),以便于满足Google快速增长的数据处理需求。GFS与以前的分布式文件系统有许多相同的目标,比如性能、可伸缩性、可靠性和可用性。但是,它的设计是由我们的应用程序工作负载和技术环境(包括当前的和预期的)的关键观察结果驱动的,这些观察结果反映出对一些早期文件系统设计的假设存在明显的偏离。我们重新审视了传统的选择,并且从根本上探索了设计空间中的不同点。

首先,组件故障是常见的,而不是异常事件。这个文件系统由数百甚至数千台存储机器组成,这些存储机器是由廉价的商用零件构建的,可以支撑数量相当的客户端机器访问。实际上,组件的数量和质量导致某些组件在任何时候都不能正常工作,而某些组件则无法从当前的故障中恢复。我们经常看到由应用程序缺陷、操作系统缺陷、人为错误,以及磁盘、内存、连接器、网络和电源故障引起的问题。因此,持续监控、容错和自动恢复必须是系统不可或缺的组成部分。

其次,按照传统标准,文件是巨大的。数个GB的文件是很常见的。通常,每个文件包含许多应用程序对象(例如:web文档)。当我们经常处理由数十亿个对象组成的许多TB的快速增长的数据集时,即使文件系统能够支持,但要管理数十亿个大约KB大小的文件也是非常麻烦的。因此,必须重新审视设计假设和参数(例如:I/O操作和数据块大小)。

再次,大多数文件是通过追加新的数据而不是覆盖现有的数据来进行修改的。文件中几乎不存在随机写操作。一旦写入,文件就只能读取,而且通常只能按顺序读取。许多数据都具有这些特征。有些可能构成数据分析程序扫描的大型存储库。有些可能是运行中的应用程序持续生成的数据流。有些可能是归档数据。有些可能是在一台机器上产生的中间结果,然后在另一台机器上处理,无论是同时处理还是稍后处理。鉴于这种对大文件的访问模式,追加成为性能优化和原子性保证的重点,而在客户端缓存数据块则失去了吸引力。

最后,通过增加我们的灵活性,共同设计应用程序和文件系统的API,能够使整个系统受益。

例如,我们放宽了GFS的一致性模型,大大简化了文件系统,而没有给应用程序带来繁重的负担。我们还引入了一个原子追加操作,以便多个客户端可以并发地追加一个文件,而不必在它们之间进行额外的同步。本文稍后将详细讨论这些问题。

目前部署了多个GFS集群,用于不同的目的。最大的集群有超过1000个存储节点,有超过300 TB的磁盘存储,并且由不同机器上的数百个客户端连续地大量访问。

2. 设计概述

2.1 假设

在为我们的需求设计文件系统时,我们一直遵循既提供挑战又提供机遇的假设。我们先前提到了一些关键的观察结果,现在更详细地列出我们的假设。

  • 这个系统由许多经常发生故障的廉价商用组件构成。它必须不断地监控自己,并且在日常基础上检测、容忍和及时恢复组件故障。
  • 系统存储的大文件数量适中。我们预计有几百万个文件,每个文件的大小通常为100 MB或更大。多GB的文件是常见的情况,应该有效地进行管理。必须支持小文件,但我们不需要针对它们进行优化。
  • 工作负载主要包括两种类型的读取:大型流式读取和小型随机读取。在大型流式读取中,单个操作通常读取数百KB的数据,更常见的是1 MB或更多数据。来自同一客户端的连续操作通常读取文件的连续区域。小型随机读取通常在某个任意偏移处读取若干KB的数据。注重性能的应用程序通常会对它们的小型读取进行批处理和排序,从而稳定地遍历文件,而不是来回读取。
  • 工作负载还有许多向文件追加数据的大型的、连续的写入操作。典型的操作大小与读取操作类似。文件一旦写入,就很少再修改了。支持在文件的任意位置进行小规模的写入,但不一定要高效。
  • 系统必须高效地为并发追加到同一文件的多个客户端实现定义良好的语义。我们的文件经常用作生产者-消费者队列或用于多路合并。数百个生产者,每台机器运行一个,将并发追加到一个文件。以最小的同步开销实现原子性是至关重要的。消费者可能稍后再读取这个文件,也有可能同时读取这个文件。
  • 高持续带宽比低延迟更加重要。我们的大多数目标应用程序都注重高速率的数据处理,而很少对单次的读取或写入有严格的响应时间要求。

2.2 接口

GFS提供了一个熟悉的文件系统接口,尽管它没有实现类似于POSIX的标准API。文件在目录中按层次结构组织,并且通过路径名进行标识。我们支持创建删除打开关闭读取写入文件的常规操作。

此外,GFS还具有快照记录追加的操作。快照以较低的成本创建文件或目录树的副本。记录追加允许多个客户端同时向同一个文件追加数据,同时保证每个客户端追加的原子性。它对于实现多路合并结果和生产者-消费者队列非常有用,许多客户端可以同时追加到这些队列,而不需要额外的锁。我们发现,在构建大型分布式应用程序时,这些类型的文件非常宝贵。快照和记录追加分别在第3.4节和第3.3节中进一步讨论。

2.3 架构

GFS集群由一个主服务器和多个分块服务器构成,并且由多个客户端访问,如图-1所示。这些服务器通常都是运行用户级服务器进程的普通Linux机器。在同一台机器上同时运行分块服务器和客户端是很容易的,只要机器资源允许,而且运行可能不稳定的应用程序代码所造成的较低可靠性也是可以接受的。

图-1:GFS架构

文件被划分为固定大小的分块。每个分块由一个不可变的、全局唯一的64位分块句柄标识,这是在分块创建时,由主服务器分配的。分块服务器将分块作为Linux文件存储在本地磁盘上,并且通过分块句柄和字节范围来读写分块数据。为了提高可靠性,每个分块被复制到多个分块服务器上。默认情况下,我们存储三个副本,但是用户可以为文件命名空间的不同区域指定不同的复制级别。

主服务器维护所有文件系统元数据。这包括命名空间、访问控制信息、从文件到分块的映射,以及分块的当前位置。它还控制系统范围内的活动,如分块租赁管理、孤儿分块的垃圾收集,以及分块服务器之间的分块迁移。主服务器定期通过心跳消息与每个分块服务器通信,向其发送指令,并且收集其状态。

链接到每个应用程序中的GFS客户端代码实现了文件系统API,并且与主服务器和分块服务器通信,代表应用程序读取或写入数据。客户端与主服务器进行元数据操作的交互,但是所有承载数据的通信都直接连接到分块服务器。我们不提供POSIX API,因此不需要连接到Linux的vnode层。

客户端和分块服务器都不会缓存文件数据。客户端缓存几乎没有什么好处,因为大多数应用程序都是通过流来传输巨型文件的,或者工作集太大而无法缓存。没有它们,就可以通过消除缓存的一致性问题来简化客户端和整个系统。(然而,客户端确实会缓存元数据。)分块服务器不需要缓存文件数据,因为分块存储在本地文件中,所以Linux的缓冲区缓存已经将频繁访问的数据保存在内存中。

2.4 单个主服务器

拥有一个主服务器可以极大地简化我们的设计,并且使主服务器能够使用全局信息做出复杂的分块放置和复制决策。但是,我们必须尽量减少它在读写中的参与,这样它就不会成为瓶颈。客户端从不通过主服务器读写文件数据。相反,客户端会询问主服务器它应该联系哪些分块服务器。它在有限的时间内缓存这些信息,并且直接与分块服务器进行交互,以便于进行许多后续操作。

让我们参考图-1,简单地解释一下交互过程。首先,使用固定的分块大小,客户端将应用程序指定的文件名和字节偏移量转换为文件中的分块索引。然后,它向主服务器发送一个包含文件名和分块索引的请求。主服务器返回相应的分块句柄和副本位置。客户端使用文件名和分块索引作为键来缓存这些信息。

然后客户端向其中一个副本发送请求,很可能是距离最近的副本。这个请求指定分块句柄和分块内的字节范围。在缓存信息过期或文件重新打开之前,对同一分块的进一步读取不需要更多的客户端-主服务器交互。实际上,客户端通常会在同一个请求中请求多个分块,而主服务器也可以在这些请求之后立即包含分块信息。这些额外的信息避免了几次未来的客户端-主服务器的交互,实际上没有任何额外的成本。

2.5 分块大小

分块大小是关键的设计参数之一。我们选择了64 MB,这比典型的文件系统块大小要大得多。每个分块副本作为普通Linux文件存储在分块服务器上,只在需要时进行扩展。延迟空间分配避免了由于内部碎片造成的空间浪费,这可能是如此大的分块大小的最大缺点。

较大的分块大小提供了几个重要的优势。首先,它减少了客户端与主服务器之间的交互需求,因为对同一个分块的读写只需要向主服务器发出一个初始请求,以便于获取分块的位置信息。这种减少对于我们的工作负载来说尤其重要,因为应用程序大多是按顺序读写大文件。即使是很小的随机读取,客户端也可以轻松地缓存数TB工作集的所有分块的位置信息。其次,由于使用较大的分块,客户端更可能对一个给定的分块执行许多操作,因此它可以通过在一段延长的时间内保持连接到分块服务器的持久TCP连接来减少网络开销。再次,它减少了存储在主服务器上的元数据的大小。这使得我们可以将元数据保存在内存中,这又带来了其他的优势,我们将在第2.6.1节讨论。

另一方面,较大的分块大小,即便使用延迟空间分配,也有其缺点。一个小文件由少量的分块组成,也许只有一个。如果许多客户端正在访问同一个文件,存储这些分块的分块服务器可能会成为热点。实际上,热点并不是主要问题,因为我们的应用程序主要是顺序地读取大型的多分块文件。

然而,当批处理队列系统首次使用GFS时,热点确实出现了:可执行文件以单分块文件的形式写入GFS,然后在数百台机器上同时启动。数百个并发请求会导致存储这个可执行文件的几个分块服务器出现超载。我们通过使用更高的复制因数来存储此类可执行文件,并且使批处理队列系统错开应用程序的启动时间,从而修复这个问题。一个潜在的长期解决方案是允许客户端在这种情况下从其他客户端读取数据。

2.6 元数据

主服务器存储三种主要的元数据类型:文件和分块的命名空间、从文件到分块的映射关系、每个分块副本的位置。所有的元数据都保存在主服务器的内存中。前两种类型(命名空间和文件-分块映射关系)也可以通过将变化记录到操作日志中来实现持久化,这个日志文件存储在主服务器的本地磁盘上,并且复制到远程机器上。使用日志可以让我们简单、可靠地更新主服务器的状态,而且在主服务器崩溃的情况下,不会出现不一致的风险。主服务器不会持久化存储分块的位置信息。相反,在主服务器启动时,以及分块服务器加入集群时,主服务器会询问每个分块服务器的分块信息。

2.6.1 内存中的数据结构

由于元数据存储在内存中,所以主服务器的操作速度很快。此外,对于主服务器来说,在后台定期扫描其整个状态是简单而有效的。这种周期性扫描用于实现分块垃圾收集、分块服务器发生故障时的重新复制,以及分块迁移,以便于平衡分块服务器之间的负载和磁盘空间使用率。第4.3节和第4.4节将进一步讨论这些活动。

这种只使用内存的方法的一个潜在问题是,分块的数量以及整个系统的容量受到主服务器的内存数量的限制。这在实践中并不是一个严重的限制。主服务器为每个64 MB的分块维护少于64字节的元数据。大多数分块是填满的,因为大多数文件包含许多分块,只有最后一个分块可能是部分填充的。类似的,每个文件的文件命名空间数据通常只需要64字节,因为它使用前缀压缩来紧凑地存储文件名。

如果有必要支持更大的文件系统,那么相对于在内存中存储元数据所获得的简单性、可靠性、性能和灵活性来说,为主服务器添加额外内存的成本只是很小的代价。

2.6.2 分块位置

主服务器不会保存关于哪个分块服务器拥有给定分块副本的持久化记录。它只会在启动时轮询分块服务器,以便于获取这些信息。此后,主服务器可以使自己保持最新状态,因为它控制所有分块的放置,并且通过定期的心跳消息来监控分块服务器的状态。

我们最初尝试将分块位置信息持久化保存在主服务器上,但我们认为在启动时从分块服务器请求数据更加简单,之后还要定期请求数据。这消除了在分块服务器加入和离开集群、更改名称、发生故障、重新启动时,主服务器和分块服务器保持同步的问题。在拥有数百台服务器的集群中,这些事件发生的太频繁了。

理解这种设计决策的另一种方法是认识到,分块服务器对它自己的磁盘上有什么分块或没有什么分块有最终决定权。试图在主服务器上保持这些信息的一致性是没有意义的,因为分块服务器上的错误可能会导致分块自动消失(例如:磁盘可能损坏并被禁用),或者运维人员可能会重命名分块服务器。

2.6.3 操作日志

操作日志包含关键元数据变化的历史记录。它是GFS的核心。它不仅是元数据的唯一持久化记录,而且还是定义并发操作顺序的逻辑时间线。文件和分块,以及它们的版本(参考第4.5节),都是由它们创建的逻辑时间唯一且永久地标识的。

由于操作日志是至关重要的,我们必须将其可靠地存储,并且在持久化元数据的更改之前,不要让更改对客户端可见。否则,即使分块本身存活下来,我们实际上也会丢失整个文件系统或最近的客户端操作。因此,我们将其复制到多个远程机器上,只有在本地和远程都将相应的日志记录刷新到磁盘后,才能响应客户端操作。主服务器在刷新之前,将多个日志记录批处理在一起,从而减少刷新和复制对总体系统吞吐量的影响。

主服务器通过重放操作日志来恢复其文件系统状态。为了尽量减少启动时间,我们必须保持较小规模的日志。每当日志增长超过一定大小时,便会触发主服务器的检查点,主服务器会检查自己的状态,这样它就可以通过从本地磁盘加载最新的检查点,并且在此之后仅重放有限数量的日志记录来恢复。检查点采用类似于B-树的紧凑形式,可以直接映射到内存中,用于命名空间的查找,而不需要额外的解析。这进一步加快了恢复速度,提高了可用性。

因为建立检查点可能需要一段时间,所以主服务器的内部状态是以这样一种方式构建的,即可以在不延迟传入更改的情况下,创建一个新的检查点。主服务器会切换到一个新的日志文件,并且在一个独立线程中创建新的检查点。新的检查点包含切换前的所有更改。对于包含数百万个文件的集群,可以在大约一分钟内创建新的检查点。在完成后,它会被写入本地和远程的磁盘。

恢复只需要最新的完整检查点和后续的日志文件。旧的检查点和日志文件可以自由删除,但是我们保留了一些,以防止灾难发生。建立检查点失败不会影响正确性,因为恢复代码会检测和跳过不完整的检查点。

2.7 一致性模型

GFS有一个宽松的一致性模型,可以很好地支持我们的高度分布式的应用程序,但在实现上仍然相对简单和高效。现在,我们讨论GFS的保证,以及它们对应用程序的意义。我们还强调了GFS是如何维持这些保证的,但是把细节留给本文的其他部分。

2.7.1 GFS的保证

文件命名空间的更改(例如:文件创建)是原子性的。它们是由主服务器专门处理的:命名空间锁保证原子性和正确性(第4.1节);主服务器的操作日志定义了这些操作的全局总顺序(第2.6.3节)。

数据更改之后,文件区域的状态取决于更改的类型、更改成功或失败,以及是否存在并发的更改。表-1总结了结果。如果所有客户端总是看到相同的数据,而不管它们从哪个副本读取数据,那么文件区域就是一致的。在文件数据更改后,便会定义文件区域,如果它是一致的,客户端将看到更改后写入的全部内容。当一次更改在没有并发写入者干扰的情况下成功时,便会定义受影响的区域(而且隐含着一致性):所有的客户端都会看到这次更改写入的内容。成功并发的更改不会定义区域,但是会保持一致:所有的客户端都会看到相同的数据,但是它可能不反映任何一次更改写入的内容。通常,它由来自多次更改的混合片段组成。失败的更改会导致区域不一致(因此也是未定义的):不同的客户端可能在不同的时间看到不同的数据。下面我们将描述我们的应用程序如何区分已定义区域和未定义区域。应用程序不需要进一步区分不同类型的未定义区域。


写入记录追加
串行成功已定义的

已定义的

夹杂着不一致的

并发成功

一致的

未定义

失败不一致的

表-1:数据更改后的文件区域状态

数据更改可以是写入或记录追加。写入导致在应用程序指定的文件偏移处写入数据。记录追加会至少原子性地追加一次数据(“记录”),即使存在并发的更改,但是其偏移量由GFS选择(第3.3节)。(相反,“常规”追加只是在客户端认为是文件当前结束的偏移量处进行写入。)将偏移量返回给客户端,并且标记包含这个记录的已定义区域的开始位置。此外,GFS可能在中间插入填充数据或记录重复数据。它们占据的区域被认为是不一致的,通常与用户数据量相比相形见绌。

在一系列成功的更改之后,更改的文件区域保证被定义,并且包含最近一次更改写入的数据。GFS通过以下方式来实现这一点:(a)将数据更改以相同的顺序应用于分块的所有副本(第3.1节);(b)使用分块版本号来检测任何已经陈旧的副本,因为它们在分块服务器宕机时丢失数据更改(第4.5节)。陈旧的复制将永远不会参与到数据更改中,也不会返回给要求主服务器提供分块位置的客户端。它们是在最早的时候被收集的垃圾。

由于客户端会缓存分块位置,因此在缓存信息刷新之前,它们可能会从陈旧的副本中读取数据。这个窗口受到缓存条目超时和下一次打开文件的限制,下一次打开文件会从缓存中清除这个文件的所有分块信息。此外,由于我们的大多数文件只会追加数据,陈旧的副本通常会返回分块的提前结束位置,而不是过时的数据。当读取者重试,并且联系主服务器时,它将立即获得当前分块的位置。

当然,在数据更改成功之后很长一段时间,组件故障仍然会损坏或破坏数据。GFS通过主服务器和所有分块服务器之间的定期握手来识别发生故障的分块服务器,并且通过校验和来检测数据损坏(第5.2节)。一旦出现问题,将尽快从有效副本中恢复数据(第4.3节)。只有在GFS能够做出反应之前(通常在几分钟内),某个分块的所有副本都丢失了,这个分块才会不可逆转地丢失。即使在这种情况下,它也变得不可用,而不是损坏:应用程序会收到清晰的错误,而不是损坏的数据。

2.7.2 对应用程序的影响

GFS应用程序可以适应宽松的一致性模型,使用一些已经满足其他用途的简单技术:依赖于追加,而不是覆盖、检查点,以及编写自我验证、自我识别的记录。

实际上,我们所有的应用程序都是通过追加,而不是覆盖来修改文件的。在一个典型的用法中,写入者会从头到尾生成一个文件。写入所有数据后,它会自动将文件重命名为永久名称,或者定期检查已成功写入的数据量。检查点也可能包含应用程序级别的校验和。读取者只验证和处理文件区域,直到最后一个检查点,已知这个检查点处于已定义状态。不考虑一致性和并发问题,这种方法已经很好地为我们服务了。相比于随机写入,追加的效率要高得多,对应用程序的故障也更有弹性。检查点使得写入者能够以增量的方式重新启动,从应用程序的角度来看,有些成功写入的文件数据仍然是不完整的,检查点可以防止读取者处理这些数据。

在另一个典型用法中,许多写入者向文件追加数据,以获得合并的结果,或者作为生产者-消费者队列。记录追加的“至少追加一次”的语义,保留了每个写入者的输出。读取者会处理偶尔出现的填充数据和重复数据,处理方式如下所示。写入者准备的的每条记录都包含校验和等额外信息,以便于验证其有效性。读取者可以通过校验和来识别和丢弃额外的填充数据和记录片段。如果它不能容忍偶尔的重复数据(例如:如果它们会触发非幂等运算),它可以使用记录中的唯一标识符将其过滤掉,无论如何,这些标识符经常需要用来命名相应的应用程序实体,如web文档。这些用于记录I/O的功能(除了重复删除)在我们的应用程序共享的库代码中,并且适用于Google的其他文件接口的实现。于是,相同的记录序列,加上偶尔出现的重复数据,总是会传递给记录读取器。

3. 系统交互

我们设计这个系统是为了尽量减少主服务器在所有操作中的参与。在这种背景下,我们现在描述客户端、主服务器和分块服务器是如何交互来实现数据更改、原子记录追加和快照的。

3.1 租约和更改顺序

更改是一种修改分块内容或元数据的操作(例如:写入或追加操作)。每次更改都在分块的所有副本上执行。我们使用租约来维持各副本之间一致的更改顺序。主服务器向其中一个副本授权分块租约,我们称之为主副本。主副本会为分块的所有更改选取一个串行的顺序。所有副本在应用更改时都遵循这个顺序。因此,全局更改顺序首先由主服务器选取的租约赋予顺序来定义,然后由主副本在租约内分配的序列号来定义。

租约机制的设计是为了尽量减少主服务器的管理开销。租约的初始超时时间为60秒。然而,只要分块发生更改,主副本就可以无限期地发出请求并通常从主服务器接收扩展请求。这些扩展请求和授权承载在主服务器和所有分块服务器之间定期交换的心跳消息上。主服务器有时可能会尝试在租约到期前撤销租约(例如:当主服务器想要禁用正在被重命名的文件的更改时)。即使主服务器和主副本断开通信链路,它还可以在旧的租约到期后,安全地将新的租约授权给另一个副本。

在图-2中,我们按照写入操作的控制流程,通过这些编号的步骤来说明这个流程。

图-2:写入控制和数据流

  1. 客户端询问主服务器哪个分块服务器持有分块的当前租约,以及其他副本的位置。如果没有人有租约,那么主服务器会将租约授权给它选择的一个副本(不会显示)。
  2. 主服务器回复主副本的身份标识和其他(次级)副本的位置。客户端缓存这些数据,以备将来发生数据更改。只有当主副本无法访问或回复不再持有租约时,客户端才需要再次联系主服务器。
  3. 客户端将数据推送给所有的副本。客户端可以按任何顺序执行这项操作。每个分块服务器都将数据存储在一个内部的LRU缓冲区缓存中,直到数据被使用或过期。通过将数据流和控制流解耦,我们可以根据网络拓扑来调度昂贵的数据流,从而提高性能,而不管哪个分块服务器是主副本。第3.2节会对此展开进一步讨论。
  4. 一旦所有副本都确认接收到数据,客户端就会向主副本发送写入请求。这个请求会标识先前推送给所有副本的数据。主副本为其接收到的所有更改分配连续的序列号,可能来自于多个客户端,这提供了必要的序列化。它按照序列号顺序,将更改应用于自己的本地状态。
  5. 主副本会将写入请求转发给所有的次级副本。每个次级副本按照主副本分配的相同序列号顺序应用更改。
  6. 次级副本都向主副本发送回复,表示它们已经完成了操作。
  7. 主副本向客户端发送回复。在任何副本上遇到的任何错误都会报告给客户端。如果发生错误,主副本和次级副本的任意子集的写入操作可能已经成功。(如果主副本的写入操作失败,就不会为其分配序列号,也不会将其转发。)客户端请求被认为失败,并且被修改的区域处于不一致的状态。我们的客户端代码通过重试失败的更改来处理这样的错误。它将在步骤(3)到步骤(7)进行几次重试,然后再从写入操作的开始进行重试。

如果应用程序的写入操作很大或跨越分块边界,GFS客户端代码会将其分解成多次写入操作。它们都遵循上述的控制流,但是可能与其他客户端的并发操作交错并被覆盖。因此,共享文件区域最终可能包含来自于不同客户端的片段,尽管副本将是相同的,因为各个操作在所有的副本上都以相同的顺序成功完成。正如第2.7节所述,这将使文件区域处于一致但未定义的状态。

3.2 数据流

我们将数据流与控制流解耦,从而有效地利用网络。当控制命令从客户端流向主副本,然后再流向所有的次级副本时,数据则沿着精心挑选的分块服务器链条,以流水线的方式线性地推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟链接,并且尽量减少推送所有数据的延迟。

为了充分利用每台机器的网络带宽,数据是沿着分块服务器链条线性地推送的,而不是以其他拓扑结构(例如:树形结构)分发的。因此,每台机器的全部出站带宽用于尽可能快地传输数据,而不是分配给多个接收者。为了尽可能地避免网络瓶颈和高延迟的链路(例如:交换机之间的链路通常都是这样的),每台机器都会将数据转发给网络拓扑中尚未接收到数据的“最近的”机器。假设客户端正在向分块服务器S1至S4推送数据。它将数据发送到最近的分块服务器,比如S1。S1向最近的分块服务器S2至S4转发数据,其中离S1最近的是S2。类似地,S2向S3或S4转发数据,选取离S2最近的,以此类推。我们的网络拓扑结构足够简单,可以根据IP地址准确地估计“距离”。

最后,我们通过对TCP连接的数据传输进行流水线处理,将延迟降到最低。只要分块服务器接收到一些数据,它就立即开始转发。流水线对我们特别有帮助,因为我们使用的是具有全双工链路的交换网络。立即发送数据不会降低接收速率。在没有网络拥塞的情况下,向R个副本传输B个字节所需的理想时间是B/T + RL,其中T是网络吞吐量,L是在两台机器之间传输字节的延迟。我们的网络连接通常是100 Mbps(T),而L远低于1 ms。因此,理想情况下,1 MB数据可以在大约80 ms内分发完毕。

3.3 原子记录追加

GFS提供了一个原子式的追加操作,称为记录追加。在传统的写入操作中,客户端指定要写入数据的偏移量。对同一个区域的并发写入是不可序列化的。这个区域可能最终包含来自多个客户端的数据片段。然而,在记录追加中,客户端只指定数据。在GFS选择的偏移量处,GFS至少有一次将数据原子式地追加到文件中(即作为一个连续的字节序列),并且将这个偏移量返回给客户端。这类似于在Unix中对以O_APPEND模式打开的文件进行写入操作,当多个写入者并发进行写入操作时,不存在竞争条件。

我们的分布式应用程序大量使用记录追加,在这些应用程序中,不同机器上的许多客户端并发地追加同一个文件。如果使用传统的写入操作,客户端将需要额外的复杂和昂贵的同步(例如:通过分布式锁管理器)。在我们的工作负载中,这样的文件通常用作多生产者/单消费者队列,或者包含来自许多不同客户端的合并结果。

记录追加是一种更改,遵循第3.1节的控制流,只是在主副本有一些额外的逻辑。客户端将数据推送到文件最后一个分块的所有副本。然后,它将请求发送给主服务器。主副本检查将记录追加到当前分块中是否会导致这个分块超过最大尺寸(64 MB)。如果是这样,它就将分块填充至最大尺寸,告诉次级副本执行相同的操作,然后回复客户端,指出应该在下一个分块上重试这个操作。(记录追加被限制为最大分块大小的四分之一,确保最坏情况下的碎片在可接受的水平。)如果记录符合最大尺寸,也就是常见的情况,主副本就会将数据追加到它的副本中,告知次级副本在它的准确偏移处写入数据,最后向客户端回复成功。

如果在任何副本上追加记录失败,客户端将重试这个操作。因此,相同分块的副本可能包含不同的数据,可能包括相同记录的全部或部分副本。GFS不能保证所有副本在字节方面都是相同的。它只能保证数据作为一个原子单元至少写入一次。这个属性很容易从简单的观察中得出,为了使操作报告成功,必须在某个分块的所有副本上以相同的偏移量写入数据。此外,在这之后,所有副本的长度至少与记录的末端一样长,因此任何未来的记录都将被分配一个更高的偏移量或一个不同的分块,即使以后有一个不同的副本成为主副本。就我们的一致性保证而言,成功的记录追加操作写入其数据的区域是定义的(因此是一致的),而中间区域是不一致的(因此是未定义的)。我们的应用程序可以处理不一致的区域,正如我们在第2.7.2节中讨论的那样。

3.4 快照

快照操作几乎是在瞬间复制一个文件或一个目录树(“来源”),同时尽量减少对正在进行的更改的任何中断。我们的用户使用它来快速创建大型数据集的分支副本(通常是这些副本的递归副本),或者在实验修改之前触发当前状态的检查点,这些修改稍后可以轻松地提交或回滚。

Like AFS [5], we use standard copy-on-write techniques to implement snapshots.

类似于AFS,我们使用标准的写时复制技术来实现快照。

When the master receives a snapshot request, it first revokes any outstanding leases on the chunks in the files it is about to snapshot.

在主服务器接收到一个快照请求时,它首先撤销将要快照的文件分块上的所有未到期的租约。

This ensures that any subsequent writes to these chunks will require an interaction with the master to find the lease holder.


This will give the master an opportunity to create a new copy of the chunk first.


4. 主机操作

5. 容错与诊断

6. 测量

7. 经验

8. 相关工作

9. 结论

致谢

参考文献

2 Comments

  1. 在图-1中:

    GFS master相当于HDFS的NameNode,提供元数据服务;

    GFS chunkserver相当于HDFS的DataNode,提供数据存储服务;

    chunk相当于HDFS的block

    图-1类似于HDFS的架构图:

  2. GFS master的操作日志,等同于HDFS的NN节点的edits日志文件

    GFS master的检查点(checkpoint),等同于HDFS的NN节点的fsimage文件

Write a comment...