大数据之谷歌文件系统论文 GFS The Google File System

原文地址

谷歌文件系统论文

摘要

我们设计并实现了Google文件系统,这是一个面向大规模分布式数据密集型应用的可扩展分布式文件系统。

它在廉价的通用硬件上运行,提供了容错性,并向大量客户端提供高聚合性能。

尽管与先前的分布式文件系统有许多相同的目标,但我们的设计是通过观察我们的应用工作负载和技术环境(包括当前和预期的环境)驱动的,这反映了对一些先前文件系统假设的明显偏离。这使我们重新审视了传统选择并探索了根本不同的设计点。

该文件系统成功地满足了我们的存储需求。它在Google内广泛部署,作为我们服务使用的数据生成和处理的存储平台,以及需要大型数据集的研发工作。迄今为止,最大的集群在数千台磁盘上提供了数百TB的存储,并且同时由数百个客户端访问。

在本文中,我们提出了旨在支持分布式应用的文件系统接口扩展,讨论了我们设计的许多方面,并报告了来自微基准测试和实际使用的测量结果。

分类和主题描述

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

一般术语

设计、可靠性、性能、测量 Design, reliability, performance, measurement

关键词

容错性、可扩展性、数据存储、集群存储 Fault tolerance, scalability, data storage, clustered storage

∗作者可通过以下地址联系:

{sanjay,hgobioff,shuntak}@google.com.

1. 引言

我们设计并实现了Google文件系统(GFS),以满足Google数据处理需求的迅速增长。

GFS与先前的分布式文件系统共享许多相同的目标,如性能、可扩展性、可靠性和可用性。

然而,它的设计是通过对我们的应用工作负载和技术环境的关键观察驱动的,无论是当前的还是预期的,这反映了对一些早期文件系统设计假设的明显偏离。我们重新审视了传统选择,并在设计空间中探索了根本不同的点。

首先,组件故障是常态而非例外。

文件系统由数百甚至数千台由廉价的通用零件构建的存储机器组成,并由相似数量的客户端机器访问。组件的数量和质量几乎可以保证在任何给定时间都有一些组件不可用,而有些组件将无法从当前的故障中恢复。我们遇到的问题包括应用程序错误、操作系统错误、人为错误以及磁盘、内存、连接器、网络和电源供应的故障。因此,系统必须具备持续监控、错误检测、容错和自动恢复的功能。

其次,按照传统标准,文件非常庞大。

多GB的文件是常见的。每个文件通常包含许多应用对象,如Web文档。当我们经常处理快速增长的TB级别的数据集,其中包含数十亿个对象时,即使文件系统支持,管理数十亿个大约KB大小的文件也变得不方便。因此,设计假设和参数,如I/O操作和块大小,必须重新审视。

第三,大多数文件通过追加新数据而不是覆盖现有数据进行变异。

在文件内进行的随机写入实际上是不存在的。一旦写入,文件只会被读取,通常只是顺序读取。许多数据共享这些特征。有些可能构成数据分析程序扫描的大型存储库。有些可能是由运行应用程序持续生成的数据流。有些可能是归档数据。有些可能是在一台机器上生成的中间结果,并在另一台机器上进行处理,无论是同时还是以后。鉴于对大文件的这种访问模式,追加成为性能优化和原子性保证的重点,而在客户端缓存数据块失去吸引力。

第四,通过共同设计应用程序和文件系统API,可以增加系统的灵活性,从而使整个系统受益。

例如,我们已经放宽了GFS的一致性模型,极大地简化了文件系统,而不对应用程序施加繁重的负担。我们还引入了一个原子追加操作,使多个客户端能够在不需要额外同步的情况下并发地向文件追加数据。这些将在本文后面更详细地讨论。

目前,已经为不同目的部署了多个GFS集群。

最大的集群拥有1000多个存储节点,超过300TB的磁盘存储,经常由数百台不同机器上的客户端密集访问。

2. 设计概述

2.1 假设

在设计满足我们需求的文件系统时,我们受到了既具有挑战性又提供机会的假设的指导。

我们之前提到了一些关键观察,现在更详细地列出我们的假设。

• 系统由许多廉价的通用组件构建,这些组件经常发生故障。它必须不断监视自身,并定期检测、容忍并迅速从组件故障中恢复。

• 系统存储了一定数量的大文件。我们预计有几百万个文件,每个文件通常大小为100 MB或更大。多GB的文件是常见的情况,应该以高效的方式进行管理。小文件必须得到支持,但我们不必为它们进行优化。

• 工作负载主要包括两种读操作:大规模流式读取和小规模随机读取。在大规模流式读取中,单个操作通常读取数百KB,更常见的是1MB或更多。来自同一客户端的连续操作通常会通过文件的一片连续区域进行读取。小规模随机读取通常在文件的某个任意偏移处读取几KB。注重性能的应用程序通常会批量处理和排序它们的小读取操作,以便通过文件稳定前进而不是前后跳跃。

• 工作负载还包括许多大型的顺序写入,将数据追加到文件中。典型的操作大小与读取相似。一旦写入,文件很少再次修改。支持在文件中任意位置进行的小写入,但不必高效。

• 系统必须有效地为同时追加到同一文件的多个客户端实现明确定义的语义。我们的文件经常用作生产者-消费者队列或多路合并。数百个生产者,每台机器运行一个,将同时追加到一个文件中。具有最小同步开销的原子性是必不可少的。文件可能以后被读取,或者消费者可能同时读取文件。

• 高持续带宽比低延迟更重要。我们的大多数目标应用程序更注重以高速率批量处理数据,而很少对单个读取或写入有严格的响应时间要求。

2.2 接口

GFS提供了一个熟悉的文件系统接口,尽管它没有实现诸如POSIX之类的标准API。

文件以层次结构的方式组织在目录中,并通过路径名进行标识。

我们支持创建、删除、打开、关闭、读取和写入文件的常规操作。

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

有关快照和记录追加的详细讨论分别见第3.4和第3.3节。

2.3 架构

一个GFS集群由一个单独的主节点和多个块服务器组成,并由多个客户端访问,如图1所示。

  • 图1

其中每个通常是运行用户级服务器进程的通用Linux机器。在资源允许的情况下,在同一台机器上运行块服务器和客户端是很容易的,只要可以接受由运行可能不稳定的应用程序代码引起的较低可靠性。

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

主节点维护所有文件系统元数据,包括命名空间、访问控制信息、文件到块的映射以及块的当前位置。它还控制系统范围的活动,如块租赁管理、孤立块的垃圾回收和块服务器之间的块迁移。主节点周期性地通过HeartBeat消息与每个块服务器通信,以给予指令并收集其状态。

嵌入到每个应用程序的GFS客户端代码实现了文件系统API,并与主节点和块服务器通信,代表应用程序读取或写入数据。客户端与主节点进行元数据操作的交互,但所有携带数据的通信都直接发送到块服务器。我们不提供POSIX API,因此无需连接到Linux vnode层。客户端和块服务器都不缓存文件数据。客户端缓存提供的好处很少,因为大多数应用程序会流式处理大型文件,或者其工作集太大以至于无法缓存。不提供缓存简化了客户端和整个系统,消除了缓存一致性问题的影响(但客户端确实缓存元数据)。块服务器不需要缓存文件数据,因为块被存储为本地文件,因此Linux的缓冲区缓存已经将经常访问的数据保留在内存中。

2.4 单一主节点

拥有单一主节点极大地简化了我们的设计,并使主节点能够利用全局知识进行复杂的块放置和复制决策。然而,我们必须使其在读取和写入中的参与程度最小化,以防止其成为瓶颈。客户端从不通过主节点读取和写入文件数据。相反,客户端会询问主节点应该联系哪些块服务器。它会缓存这些信息一段有限的时间,并与块服务器直接进行许多后续操作的交互。

让我们通过参考图1来解释一个简单的读取操作的交互。

首先,使用固定的块大小,客户端将应用程序指定的文件名和字节偏移转换为文件中的块索引。然后,它向主节点发送一个包含文件名和块索引的请求。主节点回复相应的块句柄和副本的位置。客户端使用文件名和块索引作为键缓存这些信息。然后,客户端向其中一个副本发送请求,很可能是最近的副本。该请求指定了块句柄和该块内的字节范围。对于同一块的进一步读取,不需要更多的客户端-主节点交互,直到缓存的信息过期或文件被重新打开。

事实上,客户端通常会在同一个请求中请求多个块,主节点还可以包含请求块后面的块的信息。这些额外的信息几乎没有额外成本,避免了几次将来的客户端-主节点交互。

2.5 块大小 Chunk Size

块大小是关键设计参数之一。我们选择了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 块位置

主节点不会保持有关哪些块服务器具有给定块副本的持久记录。它只是在启动时向块服务器轮询该信息。主节点随后可以使自己保持最新状态,因为它控制所有块的放置并使用定期的HeartBeat消息监视块服务器的状态。

我们最初尝试在主节点上持久保持块位置信息,但我们决定从块服务器在启动时请求数据,以及此后定期请求数据更为简单。这消除了在块服务器加入和离开集群、更改名称、失败、重新启动等情况下保持主节点和块服务器同步的问题。在具有数百台服务器的集群中,这些事件经常发生。

理解这个设计决策的另一种方式是意识到块服务器对其自己磁盘上有哪些块副本拥有最终决定权。在主节点上维护关于这些信息的一致视图是没有意义的,因为块服务器上的错误可能导致块突然消失(例如,磁盘可能损坏并被禁用)或者操作员可能重命名块服务器。

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. 系统交互 SYSTEM INTERACTIONS

我们设计系统时的一个目标是最小化主节点在所有操作中的参与。在这个背景下,我们现在描述客户端、主节点和块服务器如何交互以实现数据变异、原子记录追加和快照。

3.1 租约和变异顺序 Leases and Mutation Order

变异是一种改变块内容或元数据的操作,比如写入或追加操作。每个变异都在块的所有副本上执行。我们使用租约来维护在所有副本之间的一致变异顺序。主节点向其中一个副本(称为主副本)授予块租约。主副本为块的所有变异选择一个序列顺序。所有副本在应用变异时都遵循此顺序。因此,全局变异顺序首先由主节点选择的租约授予顺序定义,在租约内部由主副本分配的序列号定义。

租约机制的设计旨在最小化主节点的管理开销。租约的初始超时为60秒。但只要块正在变异,主副本就可以向主节点请求并通常无限期地接收扩展。这些扩展请求和授予是通过主节点和所有块服务器之间定期交换的心跳消息进行传递的。主节点有时可能会在租约到期之前尝试撤销租约(例如,当主节点想要禁用正在被重命名的文件上的变异时)。即使主节点与主副本失去通信,也可以在旧租约到期后安全地向其他副本授予新租约。

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

  1. 客户端询问主节点哪个块服务器持有块的当前租约以及其他副本的位置。如果没有人持有租约,主节点将租约授予其选择的副本(未显示)。
  2. 主节点回复主副本的标识和其他(次要)副本的位置。客户端将此数据缓存以供将来的变异使用。只有在主副本不可达或回复不再持有租约时,客户端才需要再次与主节点联系。
  3. 客户端将数据推送到所有副本。客户端可以以任何顺序执行此操作。每个块服务器将数据存储在内部LRU缓冲区缓存中,直到数据被使用或被淘汰。通过将数据流与控制流解耦,我们可以通过根据网络拓扑调度昂贵的数据流来提高性能,而不考虑主要是哪个块服务器。第3.2节进一步讨论了这一点。
  4. 一旦所有副本都确认接收到数据,客户端向主副本发送写入请求。请求标识之前推送到所有副本的数据。主副本为接收到的所有变异分配连续的序列号,可能来自多个客户端,这提供了必要的串行化。它按照序列号的顺序将变异应用于其本地状态。
  5. 主副本将写入请求转发给所有次要副本。每个次要副本按照主副本分配的相同序列号顺序应用变异。
  6. 次要副本都回复主副本,指示它们已完成操作。
  7. 主副本回复客户端。在任何副本遇到错误的情况下,错误将报告给客户端。在发生错误的情况下,写入可能已在主副本和次要副本的任意子集成功。 (如果在主副本失败,它将不会被分配序列号并转发。)客户端请求被视为失败,并且修改后的区域处于不一致状态。我们的客户端代码通过在重新尝试失败的变异之前执行步骤(3)到(7)之间的几次尝试来处理此类错误。

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

3.2 数据流 Data Flow

我们将数据流与控制流分离,以有效利用网络。虽然控制从客户端流向主服务器,然后流向所有辅助服务器,但数据以流水线方式沿着一条精心选择的块服务器链线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟链路,并最小化推送所有数据的延迟。

为了充分利用每台机器的网络带宽,数据沿着块服务器链的线性路径推送,而不是在其他拓扑结构(例如树)中分布。因此,每台机器的全出站带宽用于尽可能快地传输数据,而不是分配给多个接收方。

为了尽量避免网络瓶颈和高延迟链路(例如,交换机之间的链路通常都是),每台机器将数据转发到在网络拓扑中尚未收到数据的"最近"机器。假设客户端正在将数据推送到块服务器S1到S4。它将数据发送到最近的块服务器,例如S1。S1将其转发到最近的块服务器S2到S4,最接近S1的是S2。同样,S2将其转发到S3或S4,取决于它离S2更近的是哪一个,依此类推。我们的网络拓扑结构足够简单,可以从IP地址准确估算"距离"。

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

3.3 原子记录附加 Atomic Record Appends

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

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

记录附加是一种变异操作,并且遵循第3.1节中的控制流,主要在主服务器上增加了一点额外的逻辑。客户端将数据推送到文件的最后一个块的所有副本。然后,它将请求发送到主服务器。主服务器检查附加记录到当前块是否会导致块超过最大大小(64 MB)。如果是这样,它会将块填充到最大大小,告诉辅助服务器也执行相同的操作,并回复客户端表示应在下一个块上重试该操作(为了保持最坏情况下的碎片化在可接受的水平上,记录附加被限制为最大块大小的四分之一)。如果记录适合于最大大小,这是常见情况,主服务器将数据附加到其副本,并告诉辅助服务器在确切的偏移量处写入数据,最后回复客户端表示成功。

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

3.4 快照

快照操作在几乎瞬间创建文件或目录树("源")的副本,同时最小化对正在进行的变异的任何中断。我们的用户使用它来快速创建庞大数据集的分支副本(通常是那些副本的副本,递归地),或在尝试可以轻松提交或回滚的更改之前对当前状态进行检查点。

与AFS [5]类似,我们使用标准的写时复制技术来实现快照。当主服务器收到快照请求时,首先撤销源文件中块的任何未完成的租约。这确保对这些块的任何后续写操作都将需要与主服务器交互以查找租约持有者。这将为主服务器提供创建块的新副本的机会。

在租约已被撤销或已过期后,主服务器将操作记录到磁盘。然后,通过复制源文件或目录树的元数据,将此日志记录应用于其内存状态。新创建的快照文件指向与源文件相同的块。客户端在进行快照操作后首次想要写入块C时,会向主服务器发送请求以查找当前租约持有者。主服务器注意到块C的引用计数大于一。它推迟回复客户端请求,而是选择一个新的块句柄C'。然后,它要求拥有C当前副本的每个块服务器创建一个名为C'的新块。通过在与原始块相同的块服务器上创建新块,我们确保数据可以在本地复制,而不是通过网络传输(我们的磁盘速度大约是我们的100 Mb以太网链路的三倍)。从这一点开始,请求处理与任何块的处理没有区别:主服务器向新块C'的副本中的一个授予租约,并回复客户端,客户端可以正常写入该块,而不知道它刚刚是从现有块创建的。

4. 主服务器操作

主服务器执行所有命名空间操作。此外,它在整个系统中管理块的副本:它做出放置决策,创建新的块和因此新的副本,并协调各种系统范围的活动,以保持块完全复制,平衡负载在所有块服务器上,并回收未使用的存储。我们现在讨论每个主题。

4.1 命名空间管理和锁定 Namespace Management and Locking

许多主服务器操作可能需要很长时间:例如,快照操作必须撤销快照覆盖的所有块的块服务器租约。我们不希望在它们运行时延迟其他主服务器操作。因此,我们允许多个操作处于活动状态,并使用命名空间的区域上的锁来确保正确的序列化。

与许多传统文件系统不同,GFS没有每个目录的数据结构,列出该目录中的所有文件。它也不支持相同文件或目录的别名(即,在Unix术语中的硬链接或符号链接)。GFS在逻辑上将其命名空间表示为查找表,将完整的路径名映射到元数据。使用前缀压缩,这个表可以在内存中高效地表示。命名空间树中的每个节点(绝对文件名或绝对目录名)都有一个关联的读写锁。每个主服务器操作在运行之前都会获取一组锁。通常,如果它涉及到 /d1/d2/.../dn/leaf,它将在目录名 /d1, /d1/d2, ..., /d1/d2/.../dn 上获取读锁,并在完整路径名 /d1/d2/.../dn/leaf 上获取读锁或写锁中的一个。请注意,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 副本放置 Replica Placement

GFS 集群在多个层面上都高度分布。它通常有数百个分布在许多机架上的块服务器。这些块服务器反过来可能被来自同一机架或不同机架的数百个客户端访问。两个位于不同机架上的机器之间的通信可能会穿越一个或多个网络交换机。此外,进出机架的带宽可能小于机架内所有机器的总带宽。 多层次的分布对于可扩展性、可靠性和可用性的数据分发提出了独特的挑战。

块副本放置策略有两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽的利用。对于这两者,仅仅将副本分布在不同的机器上是不够的,这仅仅能防范磁盘或机器故障并充分利用每个机器的网络带宽。我们还必须将块副本分布在不同的机架上。这确保了即使整个机架被损坏或离线(例如,由于共享资源故障,如网络交换机或电源电路故障),某个块的某些副本仍将存活并保持可用。这也意味着某个块的流量,尤其是读取流量,可以利用多个机架的总带宽。另一方面,写入流量必须通过多个机架流动,这是我们自愿做出的权衡。

4.3 创建、重新复制、再平衡 Creation, Re-replication, Rebalancing

块副本的创建有三个原因:块创建、重新复制和再平衡。

当主节点创建一个块时,它会选择在哪里放置最初的空白副本。

它考虑了几个因素。

(1) 我们希望将新副本放置在磁盘利用率低于平均水平的块服务器上。随着时间的推移,这将使磁盘利用率在块服务器之间平均化。

(2) 我们希望限制每个块服务器上的"最近"创建的数量。尽管创建本身很便宜,但它可靠地预测了即将发生的大量写入流量,因为块是按需由写入要求创建的,并且在我们的一次追加多次读取的工作负载中,它们通常一旦完全写入就变得几乎只读。

(3) 正如上面所讨论的,我们希望在机架之间分布块的副本。

当主节点的可用副本数量低于用户指定的目标时,主节点会立即重新复制一个块。

这可能由于各种原因引起:一个块服务器变得不可用,它报告说其副本可能损坏,其磁盘由于错误而被禁用,或者复制目标增加。需要重新复制的每个块都根据几个因素进行了优先排序。一个因素是它距离其复制目标有多远。例如,我们更愿意优先复制失去了两个副本的块,而不是失去了一个副本的块。此外,我们更愿意首先重新复制属于活动文件的块,而不是属于最近删除的文件的块(参见第4.4节)。最后,为了最小化对正在运行的应用程序的影响,我们提高了阻塞客户端进度的任何块的优先级。主节点选择优先级最高的块,并通过指示某个块服务器直接从现有有效副本复制块数据来"克隆"它。新副本的放置目标与创建的目标相似:平衡磁盘利用率,限制在任一块服务器上的活动克隆操作,并在机架之间分布副本。

为了防止克隆流量压倒客户端流量,主节点限制了群集和每个块服务器的活动克隆操作的数量。此外,每个块服务器通过调节对源块服务器的读请求来限制其在每个克隆操作上花费的带宽。

最后,主节点会定期重新平衡副本:它会检查当前的副本分布并移动副本以实现更好的磁盘空间和负载平衡。通过这个过程,主节点逐渐填满一个新的块服务器,而不是立即用新块和随之而来的大量写入流量淹没它。新副本的放置标准与上面讨论的类似。此外,主节点还必须选择要删除的现有副本。通常,它更喜欢删除那些磁盘空闲空间低于平均水平的块服务器上的副本,以便平衡磁盘空间的使用。

4.4 垃圾回收

在应用程序删除文件后,GFS 不会立即回收可用的物理存储空间。它只会在文件和块级别的定期垃圾回收期间懒惰地执行。我们发现这种方法使系统更加简单和可靠。

4.4.1 机制

当应用程序删除文件时,主节点会立即记录删除操作,就像其他更改一样。但是,文件并不会立即回收资源,而是被重新命名为包含删除时间戳的隐藏名称。在主节点对文件系统命名空间的定期扫描期间,如果这些隐藏文件存在时间超过三天(该间隔可配置),则删除任何此类隐藏文件。在此期间,可以在新的特殊名称下读取文件,并且可以通过将其重新命名回正常状态来取消删除。

当隐藏文件从命名空间中删除时,其内存中的元数据将被擦除。这有效地切断了与所有块的链接。

在块命名空间的类似定期扫描中,主节点识别出孤立的块(即那些无法从任何文件访问到的块),并擦除这些块的元数据。在与主节点定期交换的 HeartBeat 消息中,每个块服务器报告其拥有的一部分块,主节点回复了所有不再存在于主节点元数据中的块的标识。块服务器可以自由地删除这些块的副本。

4.4.2 讨论

尽管在编程语言的上下文中,分布式垃圾回收是一个复杂的问题,需要复杂的解决方案,但在我们的情况下,它非常简单。

我们可以轻松地识别对块的所有引用:它们在主节点专门维护的文件到块的映射中。我们还可以轻松地识别所有块副本:它们是位于每个块服务器指定目录下的 Linux 文件。主节点不知道的任何这样的副本都是"垃圾"。垃圾回收方法对存储回收提供了几个优势,而不是急切删除。首先,在组件故障常见的大规模分布式系统中,它是简单且可靠的。块的创建可能在一些块服务器上成功,而在其他块服务器上失败,留下主节点不知道存在的副本。副本删除消息可能会丢失,主节点必须记住在故障期间重新发送它们,包括自身和块服务器的故障。垃圾回收提供了一种统一而可靠的方式来清理所有未知是否有用的副本。其次,它将存储回收合并到主节点的常规后台活动中,例如对命名空间的定期扫描和与块服务器的握手。因此,它是分批处理的,成本是分期的。此外,它仅在主节点相对空闲时执行。主节点可以更迅速地响应需要及时处理的客户端请求。第三,推迟回收存储提供了对意外、不可逆删除的安全保障。

根据我们的经验,主要的缺点是该延迟有时会阻碍用户在存储空间紧张时进行精细调整的努力。重复创建和删除临时文件的应用程序可能无法立即重用存储空间。我们通过在删除的文件被显式再次删除时加速存储回收来解决这些问题。我们还允许用户对命名空间的不同部分应用不同的复制和回收策略。例如,用户可以指定某个目录树中所有文件中的所有块都以无复制的方式存储,并且任何删除的文件将立即从文件系统状态中删除,且无法撤销。

4.5 过期副本检测

如果块服务器在故障并且错过了对块的变更操作,块副本可能会变得过期。为了区分最新的和过期的副本,主节点为每个块维护一个块版本号。

每当主节点为块授予新租约时,它都会增加块版本号并通知最新的副本。主节点和这些副本都在其持久状态中记录新的版本号。这发生在通知任何客户端之前,因此在客户端开始写入块之前。如果另一个副本当前不可用,则其块版本号将不会增加。当块服务器重新启动并报告其块集及其相关版本号时,主节点将检测到该块服务器具有过期副本。如果主节点看到版本号大于其记录中的版本号,则主节点假定在授予租约时发生了故障,因此将较高版本视为最新的。

主节点在其定期的垃圾回收中删除过期副本。在此之前,在回复客户端对块信息的请求时,主节点会将过期副本视为根本不存在。作为另一个保障,主节点在通知客户端块服务器持有块的租约或在指示块服务器在克隆操作中从另一个块服务器读取块时,会包含块版本号。客户端或块服务器在执行操作时验证版本号,以便始终访问最新的数据。

5. 容错与诊断 FAULT TOLERANCE AND DIAGNOSIS

在设计系统时,我们面临的最大挑战之一是处理频繁的组件故障。

组件的质量和数量使得这些问题更常见而不是例外:我们不能完全信任机器,也不能完全信任硬盘。组件故障可能导致系统不可用,甚至数据损坏。我们将讨论我们如何应对这些挑战以及我们在系统中构建的工具,用于在问题不可避免地发生时进行诊断。

5.1 高可用性 High Availability

在GFS集群的数百台服务器中,任何时候都可能有一些服务器不可用。我们通过两种简单而有效的策略来保持整个系统高度可用:快速恢复和复制。

5.1.1 快速恢复 Fast Recovery

无论主节点还是块服务器,都被设计成能够在几秒内恢复其状态并启动,无论它们是如何终止的。实际上,我们不区分正常终止和异常终止;服务器通常通过终止进程来关闭。客户端和其他服务器在其未完成的请求上超时时会经历一个小的中断,然后重新连接到重新启动的服务器并重试。第6.2.2节报告了观察到的启动时间。

5.1.2 块复制 Chunk Replication

正如前面讨论的,每个块都在不同机架上的多个块服务器上复制。用户可以为文件命名空间的不同部分指定不同的复制级别,默认为三个。主节点根据需要克隆现有副本,以保持每个块在块服务器下线或通过校验和验证检测到损坏的情况下完全复制(参见第5.2节)。

尽管复制对我们非常有效,但我们正在探索其他形式的跨服务器冗余,例如奇偶校验或纠删码,以应对日益增加的只读存储需求。我们预计在我们的系统中实现这些更复杂的冗余方案是具有挑战性但可管理的,因为我们的流量主要由追加和读取而不是小范围的随机写入组成。

5.1.3 主节点复制 Master Replication

主节点状态进行了复制以提高可靠性。它的操作日志和检查点被复制到多台机器上。对状态的变更被认为只有在其日志记录已经本地和在所有主节点副本上都刷新到磁盘之后才算是已提交。为简单起见,一个主节点进程负责所有的变更,以及像垃圾回收这样在系统内部改变系统状态的后台活动。当主节点进程失败时,它可以几乎立即重新启动。如果其所在的机器或磁盘失败,GFS外部的监控基础设施将在其他地方使用复制的操作日志启动一个新的主节点进程。客户端只使用主节点的规范名称(例如gfs-test),这是一个DNS别名,如果主节点被迁移到另一台机器,该别名可以更改。

此外,"影子"主节点在主节点宕机时提供对文件系统的只读访问。它们是影子而不是镜像,因为它们可能稍微滞后于主节点,通常是几分之一秒。它们增强了对未被积极变更的文件或不介意获得稍微过时结果的应用程序的读取可用性。实际上,由于文件内容是从块服务器读取的,应用程序不会观察到过时的文件内容。在短时间窗口内可能过时的是文件的元数据,比如目录内容或访问控制信息。

为了保持自身的信息同步,影子主节点读取操作日志的副本,并且与主节点完全相同地将相同的变更序列应用于其数据结构,就像主节点一样。与主节点一样,它在启动时(以及之后不频繁地)轮询块服务器,以定位块的副本,并与它们交换频繁的握手消息以监视它们的状态。它仅依赖于主节点,仅用于处理由于主节点决定创建和删除副本而导致的副本位置更新。

5.2 数据完整性 Data Integrity

每个块服务器使用校验和来检测存储数据的损坏。考虑到GFS集群通常有数千个磁盘分布在数百台机器上,它经常经历磁盘故障,这会导致读写路径上的数据损坏或丢失(有关其中一个原因,请参见第7节)。我们可以通过其他块副本恢复损坏的数据,但通过比较不同块服务器上的副本来检测损坏是不切实际的。而且,不同的副本可能是合法的:GFS变更的语义,特别是前面讨论的原子记录追加,不能保证相同的副本。因此,每个块服务器都必须通过维护校验和来独立验证其自己的副本的完整性。

一个块被分成64 KB的块。每个块都有相应的32位校验和。与其他元数据一样,校验和被保存在内存中,并与日志一起持久存储,与用户数据分开。

对于读取,块服务器在返回任何数据给请求者(无论是客户端还是另一个块服务器)之前,会验证与读取范围重叠的数据块的校验和。因此,块服务器不会将损坏传播到其他机器。如果块与记录的校验和不匹配,块服务器会向请求者返回错误,并将不匹配的情况报告给主节点。作为响应,请求者将从其他副本中读取,而主节点将从另一个副本克隆块。在建立了一个有效的新副本之后,主节点会指示报告了不匹配的块服务器删除其副本。

校验和对读性能几乎没有影响,原因有几个。由于我们的大多数读取跨越至少几个块,因此我们只需读取和校验相对较小数量的额外数据进行验证。GFS客户端代码通过尝试将读取对齐到校验和块边界,进一步减小了这种开销。此外,块服务器上的校验和查找和比较是在没有任何I/O的情况下完成的,校验和计算通常可以与I/O重叠。

校验和计算在对追加到块末尾的写入(而不是覆盖现有数据的写入)进行了重点优化,因为在我们的工作负载中,这些写入占主导地位。我们只是增量更新最后一个部分校验和块的校验和,并为追加填充的任何全新的校验和块计算新的校验和。即使最后一个部分校验和块已经损坏且我们现在无法检测到,新的校验和值也将与存储的数据不匹配,当块下次被读取时,将像往常一样检测到损坏。

相反,如果写入覆盖了块的现有范围,我们必须在进行写入之前读取和验证范围的第一个和最后一个块,然后执行写入,最后计算和记录新的校验和。如果在部分覆盖之前不验证第一个和最后一个块,新的校验和可能会隐藏在未被部分覆盖的区域中存在的损坏。

在空闲时段,块服务器可以扫描并验证不活跃块的内容。这使我们能够检测到很少被读取的块中的损坏。一旦检测到损坏,主节点就可以创建一个新的未损坏的副本并删除损坏的副本。这可以防止不活跃但损坏的块副本愚弄主节点,使其认为它有足够的有效副本。

5.3 诊断工具 Diagnostic Tools

广泛而详细的诊断日志在问题隔离、调试和性能分析方面起到了极大的帮助,而且只产生了很小的成本。没有日志,很难理解机器之间的瞬时、不可重复的交互。GFS服务器生成记录许多重要事件(如块服务器的上下线)以及所有RPC请求和响应的诊断日志。这些诊断日志可以自由删除而不会影响系统的正确性。然而,我们尽量保留这些日志,尽可能多地保留空间。

RPC日志包括在网络上传送的确切请求和响应,除了正在读取或写入的文件数据。通过将请求与响应进行匹配并整理不同机器上的RPC记录,我们可以重建整个交互历史以诊断问题。这些日志还可以作为负载测试和性能分析的跟踪。

由于这些日志是顺序和异步写入的,因此记录对性能的影响很小(并且远远超过了好处)。最近的事件也被保存在内存中,并可用于持续的在线监控。

6. 测量 MEASUREMENTS

在本节中,我们提供一些微基准测试,以说明GFS架构和实现中固有的瓶颈,并展示在Google使用的实际集群中的一些数据。

6.1 微基准测试 Micro-benchmarks

我们在一个包含一个主服务器、两个主服务器副本、16个块服务器和16个客户端的GFS集群上进行了性能测量。请注意,此配置是为了方便测试而设置的。典型的集群具有数百个块服务器和数百个客户端。

所有计算机都配置有双1.4 GHz PIII处理器,2 GB内存,两个80 GB 5400 rpm硬盘,并通过100 Mbps全双工以太网连接到HP 2524交换机。所有19台GFS服务器机器都连接到一个交换机,而所有16台客户端机器连接到另一个。这两个交换机通过1 Gbps的链路连接。

6.1.1 读取

N个客户端同时从文件系统中读取。

每个客户端从320 GB的文件集中随机选择一个4 MB的区域。这个过程重复256次,以便每个客户端最终读取1 GB的数据。块服务器总共只有32 GB的内存,所以我们期望Linux缓冲缓存中最多有10%的命中率。我们的结果应该接近冷缓存结果。

图3(a)显示了N个客户端的总读取速率及其理论极限。当两个交换机之间的1 Gbps链路饱和,或者当其100 Mbps网络接口饱和时,理论极限峰值为125 MB/s,即每个客户端为12.5 MB/s。当只有一个客户端进行读取时,观察到的读取速率为10 MB/s,即每个客户端80%的限制。当有16个读取者时,总读取速率达到94 MB/s,约为125 MB/s链路限制的75%,即每个客户端6 MB/s。效率从80%下降到75%,因为随着读取者数量的增加,多个读取者同时从同一块服务器读取的概率也增加。

6.1.2 写入

N个客户端同时写入N个不同的文件。每个客户端向一个新文件写入1 GB的数据,采用一系列1 MB的写入。图3(b)显示了总写入速率及其理论极限。由于我们需要将每个字节写入16块块服务器中的3个,每个输入连接速度为12.5 MB/s,极限在67 MB/s时达到平稳状态。

一个客户端的写入速率为6.3 MB/s,约为极限的一半。这主要是由于我们的网络堆栈。它与我们用于将数据推送到块副本的流水线方案的交互效果不是很好。从一个副本传播数据到另一个副本的延迟降低了总体写入速率。16个客户端的总写入速率达到35 MB/s(每个客户端2.2 MB/s),约为理论极限的一半。与读取的情况一样,随着客户端数量的增加,多个客户端并发写入同一块服务器的可能性更大。此外,与16个读取者相比,对于16个写入者而言,发生冲突的可能性更大,因为每个写入涉及三个不同的副本。

写入速度比我们期望的要慢。在实践中,这并不是一个主要问题,因为尽管它会增加个别客户端看到的延迟,但并不会显著影响系统提供给大量客户端的总体写入带宽。

6.1.3 记录追加

图3(c)显示了记录追加的性能。N个客户端同时追加到单个文件。性能受存储文件最后一块的块服务器的网络带宽限制,与客户端数量无关。对于一个客户端,它从6.0 MB/s开始,并在16个客户端时降至4.8 MB/s,主要是由于不同客户端看到的网络传输速率的拥塞和差异。我们的应用程序往往会同时产生多个这样的文件。换句话说,N个客户端同时追加到M个共享文件,其中N和M都是十几个或几百个。因此,在我们的实验中,块服务器网络拥塞实际上并不是一个重要的问题,因为客户端可以在为一个文件写入数据时,另一个文件的块服务器正忙。

  • 图 3

6.2 实际集群

我们现在来看一下谷歌内部使用的两个代表性集群,它们代表了其他几个类似的集群。集群 A 经常被一百多名工程师用于研究和开发。典型的任务由人类用户发起,持续数小时。它会读取几兆字节到几 TB 的数据,对数据进行转换或分析,然后将结果写回集群。集群 B 主要用于生产数据处理。任务持续时间更长,持续生成和处理几 TB 的数据集,只有偶尔需要人类干预。在这两种情况下,单个"任务"包括许多进程在许多机器上同时读写许多文件。

6.2.1 存储

正如表中的前五个条目所示,这两个集群都有数百个块服务器,支持许多 TB 的磁盘空间,并且相当但并非完全充满。 "已用空间"包括所有块副本。几乎所有文件都被复制了三次。因此,这两个集群分别存储了 18 TB 和 52 TB 的文件数据。

这两个集群的文件数目相似,尽管 B 有更大比例的"死"文件,即已删除或被新版本替换但其存储尚未被回收的文件。它还有更多的块,因为其文件往往较大。

6.2.2 元数据

块服务器总共存储了数十 GB 的元数据,主要是用户数据 64 KB 块的校验和。块服务器上保存的唯一其他元数据是第 4.5 节中讨论的块版本号。

主服务器上的元数据要小得多,仅为几十 MB,平均每个文件约为 100 字节。这符合我们的假设,即主内存的大小在实践中不会限制系统的容量。大部分每个文件的元数据是以前缀压缩形式存储的文件名。其他元数据包括文件所有权和权限、从文件到块的映射,以及每个块的当前版本。此外,对于每个块,我们存储当前副本位置和用于实现写时复制的引用计数。

每个单独的服务器,包括块服务器和主服务器,仅具有 50 到 100 MB 的元数据。因此,恢复速度很快:只需几秒钟就可以从磁盘中读取这些元数据,然后服务器就能够回答查询。但是,主服务器在一段时间内(通常为 30 到 60 秒)受到一些限制,直到它从所有块服务器那里获取了块位置信息。

6.2.3 读取和写入速率

表 3 显示了不同时间段的读取和写入速率。这些测量是在这两个集群启动约一周后进行的(这两个集群最近重新启动以升级到 GFS 的新版本)。(重新启动后)平均写入速率小于 30 MB/s。当我们进行这些测量时,B 正处于产生约 100 MB/s 数据的高写入活动中,这产生了 300 MB/s 的网络负载,因为写入被传播到三个副本。

读取速率远高于写入速率。总体工作负载主要由读取操作组成,与我们的假设相符。两个集群都处于大量读取活动中。特别是,A在过去的一周内一直保持着每秒580 MB的读取速率。其网络配置可以支持750 MB/s,因此它有效地利用了资源。Cluster B可以支持每秒1300 MB的峰值读取速率,但其应用程序仅使用了380 MB/s。

6.2.4 Master负载

表3还显示,发送到主节点的操作速率约为每秒200到500次。主节点可以轻松跟上这个速率,因此对于这些工作负载来说,它不是一个瓶颈。

  • 表3

在GFS的早期版本中,主节点偶尔成为某些工作负载的瓶颈。它主要花费时间在大型目录(其中包含数十万个文件)中进行顺序扫描,寻找特定的文件。我们后来改变了主节点的数据结构,以通过命名空间进行高效的二进制搜索。它现在可以轻松支持每秒多次文件访问。如果有必要,我们可以通过在命名空间数据结构前放置名称查找缓存来进一步加速它。

6.2.5 恢复时间

在一个chunkserver发生故障后,一些chunks将变得不足复制,并且必须进行克隆以恢复其复制级别。恢复所有这些chunks所需的时间取决于资源的数量。在一个实验中,我们关闭了cluster B中的一个chunkserver。该chunkserver有大约15,000个包含600 GB数据的chunks。为了限制对运行中应用程序的影响并为调度决策提供余地,我们的默认参数将此集群限制为最多91个并发克隆操作(占chunkserver数量的40%),每个克隆操作允许消耗最多6.25 MB/s(50 Mbps)。所有chunks在23.2分钟内被恢复,有效的复制速率为440 MB/s。

在另一个实验中,我们关闭了两个分别包含大约16,000个chunks和660 GB数据的chunkserver。这次双重故障导致266个chunks只剩下一个副本。这266个chunks以更高的优先级进行克隆,并在2分钟内全部恢复至至少2倍的复制级别,从而使集群处于一种状态,可以容忍另一个chunkserver的故障而不会丢失数据。

6.3 工作负载分解

在本节中,我们详细介绍了两个与第6.2节中的集群相似但不相同的GFS集群的工作负载。Cluster X 用于研发,而 Cluster Y 用于生产数据处理。

6.3.1 方法和注意事项

这些结果仅包括客户端发起的请求,以反映我们的应用程序对整个文件系统生成的工作负载。它们不包括用于执行客户端请求或内部后台活动的服务器间请求,例如转发写入或重新平衡。 有关I/O操作的统计数据基于从GFS服务器记录的实际RPC请求中启发式重建的信息。例如,GFS客户端代码可能会将读取拆分为多个RPC以增加并行性,从中我们推断出原始读取。由于我们的访问模式具有高度的风格化,我们期望任何误差都在噪声范围内。应用程序的显式日志记录可能会提供略微更准确的数据,但重新编译和重新启动成千上万个正在运行的客户端以执行此操作在逻辑上是不可能的,并且从许多机器收集结果也很麻烦。

我们应当谨慎地不要过度概括我们的工作负载。由于Google完全控制GFS及其应用程序,这些应用程序往往被调整为适应GFS,反之亦然。一般应用程序和文件系统之间可能也存在相互影响,但在我们的情况下,这种影响可能更加显著。

6.3.2 Chunkserver 工作负载

表4显示了按大小分布的操作情况。读取大小呈双峰分布。小读取(小于64 KB)来自寻址密集型客户端,这些客户端在大文件中查找小数据块。大读取(超过512 KB)来自整个文件的长顺序读取。

  • 表4

在Cluster Y中,有相当数量的读取根本不返回任何数据。我们的应用程序,尤其是生产系统中的应用程序,通常使用文件作为生产者-消费者队列。生产者同时向文件追加数据,而消费者读取文件的末尾。偶尔在消费者超越生产者时不返回任何数据。Cluster X显示这种情况较少,因为它通常用于短暂的数据分析任务,而不是长时间运行的分布式应用程序。 写入大小也呈双峰分布。大写入(超过256 KB)通常是由于写入者内部的显着缓冲。缓冲数据较少的写入者,更频繁地进行检查点或同步,或者生成的数据较少,占据较小的写入(小于64 KB)。

至于记录附加,Cluster Y看到的大记录附加的百分比要高于Cluster X,因为我们使用Cluster Y的生产系统更积极地调整为GFS。

表5显示了在各种大小的操作中传输的数据总量。对于所有类型的操作,较大的操作(超过256 KB)通常占据大部分传输的字节数。小读取(小于64 KB)确实传输了读取数据的少量但显著的部分,这是由于随机寻址工作负载。

  • 表5

6.3.3 附加操作与写入操作

记录附加操作在我们的生产系统中得到了广泛使用。对于Cluster X,按传输字节数计算,写入操作与记录附加操作的比率为108:1,按操作计数为8:1。对于由生产系统使用的Cluster Y,这些比率分别为3.7:1和2.5:1。此外,这些比率表明对于两个集群来说,记录附加操作往往比写入操作更大。然而,对于Cluster X,在测量期间记录附加操作的整体使用相当低,因此结果可能受到一两个具有特定缓冲区大小选择的应用程序的影响。

正如预期的那样,我们的数据变更工作负载主要由附加而不是覆写来支配。我们测量了主要副本上被覆写的数据量。这近似于客户端故意覆写先前写入的数据而不是追加新数据的情况。对于Cluster X,覆写占变更字节的不到0.0001%,占变更操作的不到0.0003%。对于Cluster Y,这两个比率都是0.05%。尽管这很微小,但仍高于我们的预期。事实证明,这些覆写大多数来自于客户端由于错误或超时而进行的重试。它们并不是工作负载的一部分,而是重试机制的结果。

6.3.4 主节点工作负载

表6显示了对主节点的请求类型的详细分解。大多数请求是为了读取的块位置(FindLocation)和数据变更的租约持有者信息(FindLeaseLocker)。

  • 表6

Cluster X和Y在删除请求的数量上存在显着差异,因为Cluster Y存储定期生成并替换为新版本的生产数据集。其中一些差异在打开请求的差异中进一步隐藏,因为旧版本的文件可能通过从头开始写入(Unix打开术语中的"w"模式)而被隐式删除。

FindMatchingFiles是一种支持"ls"和类似文件系统操作的模式匹配请求。与主节点的其他请求不同,它可能处理命名空间的大部分,因此可能是昂贵的。Cluster Y更经常看到它,因为自动化数据处理任务倾向于检查文件系统的部分以了解全局应用程序状态。相反,Cluster X的应用程序更受明确用户控制,通常提前知道所有需要的文件的名称。

7. 经验总结

在构建和部署GFS的过程中,我们经历了各种问题,一些是操作性的,一些是技术性的。最初,GFS被构想为我们生产系统的后端文件系统。随着时间的推移,使用方式逐渐扩展到包括研发任务。它起初对权限和配额等功能的支持较少,但现在已包括这些功能的基本形式。尽管生产系统受到很好的纪律和控制,用户有时并非如此。需要更多基础设施来防止用户相互干扰。

我们遇到的一些最大问题与磁盘和Linux有关。许多磁盘向Linux驱动器声称它们支持一系列IDE协议版本,但实际上只对更近期的版本做出可靠响应。由于协议版本非常相似,这些驱动器大多数情况下工作正常,但偶尔不匹配可能会导致驱动器和内核在驱动器状态上存在分歧。由于内核中的问题,这会导致数据默默损坏。这个问题促使我们使用校验和来检测数据损坏,同时我们修改了内核以处理这些协议不匹配问题。

早期我们在Linux 2.2内核中遇到了一些由于fsync()的成本引起的问题。它的成本与文件的大小成正比,而不是修改部分的大小。这对我们的大型操作日志尤其是在我们实现检查点之前是一个问题。我们通过使用同步写入来解决了这个问题,并最终迁移到了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、Minnesota的GFS[11]和GPFS [10])去除了中心化服务器,依靠分布式算法来实现一致性和管理。我们选择中心化方法是为了简化设计,提高可靠性,并增强灵活性。特别是,中心化的主节点使得实现复杂的块放置和复制策略变得更加容易,因为主节点已经具有大部分相关信息并控制它的变化。我们通过保持主状态小并在其他机器上完全复制来解决容错问题。目前,我们的影子主节点机制提供了可扩展性和高可用性(对于读取)。对主状态的更新通过附加到预写式日志来实现持久性。因此,我们可以改进类似于Harp [7]中的主-副本方案,以提供比我们当前方案更强一致性保证的高可用性。

我们正在解决与Lustre [8]类似的问题,即向大量客户端提供总体性能。然而,通过专注于满足我们应用程序需求而不是构建符合POSIX标准的文件系统,我们已经显著简化了这个问题。此外,GFS假定大量不可靠的组件,因此容错性是我们设计的核心。

GFS最接近NASD体系结构[4]。虽然NASD体系结构基于网络附加的磁盘驱动器,但GFS使用商品机器作为块服务器,与NASD原型相同。与NASD工作不同,我们的块服务器使用懒惰分配的固定大小块,而不是可变长度的对象。此外,GFS实现了在生产环境中所需的重新平衡、复制和恢复等功能。

与Minnesota的GFS和NASD不同,我们不寻求改变存储设备的模型。我们专注于解决使用现有商品组件的复杂分布式系统的日常数据处理需求。原子记录追加所支持的生产者-消费者队列解决了与River [2]中的分布式队列类似的问题。虽然River使用分布在多台机器上的基于内存的队列和精心设计的数据流控制,GFS使用可持续追加的文件,可以由许多生产者并发追加。River模型支持m对n的分布式队列,但缺乏与持久存储一起提供的容错性,而GFS只能有效地支持m对1的队列。

9. 结论

谷歌文件系统展示了在商品硬件上支持大规模数据处理工作负载所必需的特质。虽然一些设计决策是特定于我们独特的环境,但许多决策可能适用于类似规模和成本意识的数据处理任务。

我们从重新审视传统文件系统的假设开始,根据我们当前和预期的应用工作负载以及技术环境的情况。我们的观察在设计空间中导致了根本不同的观点。

我们将组件故障视为常规情况,而不是例外,针对主要是追加(可能是并发的)而后读取(通常是顺序的)的大型文件进行优化,并通过扩展和放宽标准文件系统接口来改善整个系统。

我们的系统通过持续监控、复制关键数据以及快速自动恢复来提供容错性。块复制使我们能够容忍块服务器故障。这些故障的频率促使我们采用了一种新颖的在线修复机制,该机制定期透明地修复损坏并尽快弥补丢失的副本。此外,我们使用校验和检测在磁盘或IDE子系统级别发生的数据损坏,鉴于系统中的磁盘数量,这变得非常普遍。

我们的设计通过为执行各种任务的许多并发读者和写入者提供高总体吞吐量来实现。我们通过将文件系统控制(通过主节点传递)与数据传输(直接在块服务器和客户端之间传递)分离来实现这一点。通过大块大小和块租约,最小化主节点在常见操作中的参与,块租约将数据突变的授权委派给主要副本。这使得可能采用简单的集中式主节点而不成为瓶颈。 我们相信我们网络堆栈的改进将解决目前由个体客户端看到的写入吞吐量的限制。

GFS已成功满足我们的存储需求,并在谷歌内部广泛用于研发和生产数据处理的存储平台。它是一种重要工具,使我们能够在整个网络规模上持续创新和解决问题。

10. 致谢

我们要感谢以下人员对系统或论文的贡献。Brian Bershad(我们的指导者)和匿名审稿人为我们提供了宝贵的评论和建议。Anurag Acharya、Jeff Dean和David desJardins为早期设计做出了贡献。Fay Chang负责比较块服务器之间的副本。Guy Edjlali负责存储配额。Markus Gutschke负责测试框架和安全增强。David Kramer负责性能增强。Fay Chang、Urs Hoelzle、Max Ibel、Sharon Perl、Rob Pike和Debby Wallach对论文的早期草稿提出了评论。谷歌的许多同事在新的文件系统上勇敢地信任他们的数据,并给予了有用的反馈。Yoshka帮助进行了早期测试。

相关推荐
初晴~12 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
DolphinScheduler社区15 分钟前
作业帮基于 Apache DolphinScheduler 3_0_0 的缺陷修复与优化
大数据
SeaTunnel17 分钟前
京东科技基于 Apache SeaTunnel 复杂场景适配 #数据集成
大数据
有一个好名字21 分钟前
zookeeper分布式锁模拟12306买票
分布式·zookeeper·云原生
喝醉酒的小白1 小时前
Elasticsearch 配置文件
大数据·elasticsearch·搜索引擎
一只敲代码的猪2 小时前
Llama 3 模型系列解析(一)
大数据·python·llama
智慧化智能化数字化方案2 小时前
深入解读数据资产化实践指南(2024年)
大数据·人工智能·数据资产管理·数据资产入表·数据资产化实践指南
哦哦~9212 小时前
深度学习驱动的油气开发技术与应用
大数据·人工智能·深度学习·学习
Anna_Tong3 小时前
云原生大数据计算服务 MaxCompute 是什么?
大数据·阿里云·云原生·maxcompute·odps
drebander3 小时前
SQL 实战-巧用 CASE WHEN 实现条件分组与统计
大数据·数据库·sql