GFS 论文阅读笔记

388 阅读1小时+

The Google File System

目录

Abstract

GFS,一个可扩展的分布式文件系统,用于大型分布式数据密集型应用程序。它可以在廉价的通用硬件上运行时提供容错功能,并且可以为大量客户端提供较高的聚合性能。

我们已经设计并实现了Google File System,这是一个可扩展的分布式文件系统,用于大型分布式数据密集型应用程序。它可以在廉价的通用硬件上运行时提供容错功能,并且可以为大量客户端提供较高的聚合性能。
在实现与以前的分布式文件系统相同的许多目标的同时,我们的设计是由对我们当前和预期的应用程序工作负载和技术环境的观察推动的,这反映出与某些早期文件系统假设的明显偏离。这使我们重新审视了传统选择,并探索了根本不同的设计要点。
GFS 已成功满足我们的存储需求。它已在Google内广泛部署,作为存储平台,用于生成和处理我们服务所使用的数据以及需要大量数据集的研发工作。迄今为止,最大的群集可在一千台计算机上的数千个磁盘上提供数百TB的存储量,并且数百个客户端可以同时访问它。
在本文中,我们介绍了旨在支持分布式应用程序的文件系统接口扩展,讨论了我们设计的许多方面,并报告了来自微基准测试和实际使用情况的测量结果。

keywords:Fault tolerance,scalability,data storage,clustered storage

1 Introduction

GFS 的目标:性能,可伸缩性,可靠性和可用性

GFS 和传统的分布式文件系统在设计上不同的点:

  • 组件故障是正常现象,而不是例外情况
  • 需要处理的文件容量都很大
  • 大多数文件是通过附加新数据而不是覆盖现有数据来进行改变的 appending not overwriting
  • 共同设计应用程序和文件系统的API使得整个系统的灵活性更好

我们已经设计并实施了Google文件系统(GFS),以满足Google数据处理需求快速增长的需求。 GFS与以前的分布式文件系统具有许多相同的目标,例如性能,可伸缩性,可靠性和可用性。但是,其设计是由对我们当前和预期的应用程序工作负载和技术环境的主要观察驱动的,这反映出与某些早期文件系统设计假设存在明显差异。我们重新审视了传统选择,并探索了设计空间中的根本不同点。

首先,组件故障是正常现象,而不是例外情况。文件系统由数百个或什至数千个由廉价商品部件构建的存储机组成,并由相当数量的客户端计算机访问。组件的数量和质量实际上保证了某些组件在任何给定的时间都无法运行,并且某些组件无法从当前的故障中恢复。我们已经看到了由应用程序错误,操作系统错误,人为错误以及磁盘,内存,连接器,网络和电源故障引起的问题。因此,持续监视,错误检测,容错和自动恢复必须是系统不可或缺的。

其次,按照传统标准,文件很大,多GB文件是常见的。每个文件通常包含许多应用程序对象,例如Web文档。当我们定期处理包含数十亿个对象的许多TB的快速增长的数据集时,即使文件系统可以支持它,也无法管理数十亿个大约KB大小的文件。结果,必须重新考虑设计假设和参数,例如I / O操作和块大小。

第三,大多数文件是通过附加新数据而不是覆盖现有数据来进行改变的。文件内的随机写入实际上是不存在的。写入后,仅读取文件,并且通常只能顺序读取。各种数据共享这些特征。有些可能会构成大型存储库,数据分析程序会扫描这些存储库。有些可能是运行应用程序连续生成的数据流。有些可能是档案数据。一台机器上同时产生或稍后产生的中间结果可能是中间结果。鉴于对大型文件的这种访问方式,附加成为性能优化和原子性保证的重点,而在客户端中缓存数据块却失去了吸引力。

第四,共同设计应用程序和文件系统API通过增加灵活性来使整个系统受益。 例如,我们放松了GFS的一致性模型,以极大地简化文件系统,而不会给应用程序带来繁重的负担。 我们还引入了原子附加操作,以便多个客户端可以并发附加到文件,而无需它们之间的额外同步。 这些将在本文后面详细讨论。

​ 当前部署了多个GFS群集以用于不同的目的。 最大的服务器拥有1000多个存储节点,超过300 TB的磁盘存储,并且连续不断地被数百台不同计算机上的客户端大量访问。

2 Design Overview

2.1 Assumptions

在设计满足我们需求的文件系统时,我们以既带来挑战又带来机遇的假设为指导。我们前面提到了一些关键的观察,现在更详细地阐述了我们的假设。

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

2.2 Interface

GFS提供了一个熟悉的文件系统接口,具有创建,删除,打开,关闭,读取和写入文件的常规操作,此外,GFS具有快照和记录附加操作。

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

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

2.3 Architecture

image-20200627124937953

一个GFS集群由一个 master和多个 chunkservers 组成,并且可以由多个 clients 访问,如图1所示。它们中的每一个通常都是运行用户级服务器进程的商用Linux计算机。只要在机器资源允许的情况下,就可以在同一台机器上同时运行chunkserver和 client,并且可以接受由于运行不稳定的应用程序代码而导致的较低可靠性。

文件分为固定大小的块。 每个块都由块创建时主机分配的不可变且全局唯一的64 bit chunk handle 来标识。chunk server将块作为Linux文件存储在本地磁盘上,并读取或写入由块句柄和字节范围指定的块数据。为了提高可靠性,每个块都复制到多个chunk server上。默认情况下,我们存储三个副本,尽管用户可以为文件namespce的不同区域指定不同的复制级别。

Master维护所有文件系统元数据。这包括namespce,访问控制信息,从文件到块的映射以及块的当前位置。它还控制整个系统范围内的活动,例如块租约管理,孤立块的垃圾回收以及chunk server之间的块迁移。master 周期性地与HeartBeat消息中的每个chunk server通信,以向其发出指令并收集其状态

链接到每个应用程序的GFS客户端代码实现文件系统API,并与Master和chunk server通信以代表该应用程序读取或写入数据。客户端与Master交互以进行元数据操作,但是所有承载数据的通信都直接传递给chunk server。我们不提供POSIX API,因此不需要挂接到Linux vnode层。

客户端和chunk server均不缓存文件数据。 客户端缓存几乎没有好处,因为大多数应用程序会流过大文件或工作集太大而无法缓存。没有它们,就消除了缓存一致性问题,从而简化了客户端和整个系统。 (但是,客户端确实缓存元数据。)chunk server不需要缓存文件数据,因为大块存储为本地文件,因此Linux的缓冲区缓存已经将经常访问的数据保存在内存中。

2.4 Single Master

只有一个 Master 可以极大地简化我们的设计,并使 Master 可以使用全局数据来制定复杂的块放置和复制决策。但是,我们必须最小化它在读写中的参与,以免它成为瓶颈。客户端永远不会通过Master读取和写入文件数据。取而代之的是,客户端询问Master应该联系哪些chunkserver。它在有限的时间内缓存此信息,并直接与chunkserver交互以进行许多后续操作。

让我们参考图1来解释简单阅读的交互。首先,客户端使用固定的块大小,将应用程序指定的文件名和字节偏移量转换为文件内的块索引。然后,它向主机发送一个包含文件名和块索引的请求。Master 答复相应的块句柄和副本的位置。客户端使用文件名和块索引作为关键字来缓存此信息。

然后,客户端将请求发送到其中一个副本,很可能是最接近的副本。该请求指定了块句柄和该块内的字节范围。在缓存信息过期或重新打开文件之前,对同一块的进一步读取不再需要客户端与主机之间的交互。实际上,客户端通常会在同一请求中请求多个块,而Master也可以在请求的块之后立即包含块的信息。这些额外的信息可以绕开未来的几项客户与主控之间的交互,而几乎无需有额外的代价。

2.5 Chunk Size

块大小是关键设计参数之一。

我们选择了64 MB,它比典型的文件系统块大得多。每个块副本都作为纯Linux文件存储在块服务器上,并且仅在需要时进行扩展。惰性空间分配避免了由于内部碎片而浪费空间,这可能是对如此大的块大小最被人诟病的地方。

大块块具有几个重要的优点。

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

缺点:

  • 一个小文件由少量块组成,也许只有一个。如果许多客户端正在访问同一文件,则存储这些块的块服务器可能会成为热点。

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

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

2.6 Metadata

Master 存储三种类型的元数据:

  • 文件和 chunk 的命名空间
  • 文件到 chunks 的映射
  • 每个 chunk 的冗余所在的位置

所有的元数据都存放在 Master 的内存中。

前两种类型(names-paces和files-chunk的映射)也通过将改变 log 到存储在主机本地磁盘上并复制到远程计算机上的操作日志中而保持不变。 使用日志可以使我们简单,可靠地更新主状态,而不会在主崩溃时冒不一致的风险。

Master不会持久存储 chunkserver 中 chunk 的信息。 相反,它会在主启动时以及每当一个块服务器加入集群时就向每个块服务器询问其块。

2.6.1 In-Memory Data Structures

由于元数据存储在内存中,因此 Master 操作速度很快。

这种仅使用内存的方法的一个潜在问题是,块的数量以及整个系统的容量受到主机拥有多少内存的限制

如果需要支持甚至更大的文件系统,则向Master添加额外内存的成本也很小,以补偿我们通过将元数据存储在内存中而获得的简单性,可靠性,性能和灵活性。

由于元数据存储在内存中,因此 Master 操作速度很快。此外,对于 Master 而言,在后台定期扫描其整个状态既简单又有效。这种定期扫描用于实现大块垃圾收集,在大块服务器故障时进行重新复制以及大块迁移以平衡大块服务器之间的负载和磁盘空间使用情况。第4.3节和第4.4节将进一步讨论这些活动。

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

如果需要支持甚至更大的文件系统,则向Master添加额外内存的成本也很小,以补偿我们通过将元数据存储在内存中而获得的简单性,可靠性,性能和灵活性。

2.6.2 Chunk Locations

Master 不保留有关哪个块服务器具有给定块副本的持久记录。

它只是在启动时轮询chunkserver以获取该信息。之后,Master可以保持最新状态,因为它可以控制所有块的放置并通过常规HeartBeat消息监视块服务器的状态。

我们最初尝试将块位置信息永久保留在Master上,但是我们决定,在启动时以及随后定期从块服务器请求数据要简单得多。这消除了在块服务器加入和离开集群,更改名称,失败,重新启动等时,使Master和块服务器保持同步的问题。在具有数百台服务器的群集中,这些事件经常发生。

理解该设计决策的另一种方法是认识到,chunk server 对自己的磁盘上有或没有的块有最终的决定权。试图在 Master 上维护此信息的一致性视图是没有意义的,因为块服务器上的错误可能导致块自发消失(例如,磁盘可能变质并被禁用),或者操作员可能会重命名块服务器。

2.6.3 Operation Log

操作日志包含关键元数据更改的历史记录。它是GFS的核心。

它不仅是元数据的唯一持久记录,而且还用作定义并发操作顺序的逻辑时间表。文件和块及其版本(请参见第4.5节)均由创建它们的逻辑时间唯一地标识。

由于操作日志至关重要,因此我们必须可靠地存储日志。在元数据改变永久性修改完成之前,客户端对这些改变不可见。

Master通过覆盖操作日志来恢复其文件系统状态。

日志的内部结构的构造方式可以在不延迟新的日志改变的的情况下创建新的检查点(新线程)。

恢复仅需要最新的完整检查点和后续日志文件。

操作日志包含关键元数据更改的历史记录。它是GFS的核心。它不仅是元数据的唯一持久记录,而且还用作定义并发操作顺序的逻辑时间表。文件和块及其版本(请参见第4.5节)均由创建它们的逻辑时间唯一唯一地标识。

由于操作日志至关重要,因此我们必须可靠地存储日志,在元数据改变永久性修改完成之前,客户端对这些改变不可见。否则,即使这些数据块本身仍然存在,我们也会有效地丢失整个文件系统或最近的客户端操作。因此,我们将其复制到多台远程计算机上,并且仅在将相应的日志记录刷新到本地和远程磁盘后才响应客户端操作。Master在刷新之前将一批日志记录一起批处理,从而减少了刷新和复制对整个系统吞吐量的影响。

Master通过覆盖操作日志来恢复其文件系统状态。为了最大程度地缩短启动时间,我们必须使日志较小。每当日志增长到超过特定大小时,主控点都会检查其状态,以便可以通过从本地磁盘加载最新的检查点并在此之后仅覆盖有限数量的日志记录来进行恢复。该检查点采用类似于B树的紧凑形式,可以直接映射到内存中并用于namespce查找,而无需进行额外解析。这进一步加快了恢复速度并提高了可用性。

由于建立检查点可能要花费一些时间,因此,内部结构的构造方式可以在不延迟新的到来改变的的情况下创建新的检查点。Master切换到新的日志文件,并在单独的线程中创建新的检查点。新的检查点包括切换之前的所有变化。对于具有数百万个文件的群集,可以在一分钟左右的时间内创建它。完成后,将其本地和远程写入磁盘。

恢复仅需要最新的完整检查点和后续日志文件。可以自由删除较旧的检查点和日志文件,尽管我们保留了一些检查点以防灾难。检查点期间的故障不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。

2.7 Consistency Model

GFS拥有一个宽松的一致性模型,该模型可以很好地支持我们高度分散的应用程序,但实现起来相对简单有效。 现在,我们讨论GFS的guarantee及其对应用的意义。 我们还将重点介绍GFS如何维护这些保证,但将细节留给本文的其他部分。

2.7.1 Guarantees by GFS

GFS应用程序可以使用其他目的已经需要的一些简单技术来适应宽松的一致性模型:

  • 依靠append而不是overwrite
  • 检查点 checkpoint
  • 编写自我验证,自我识别的记录 self-validating and self-identifying records

文件namespce变化(例如文件创建)是原子的。它们仅由Master处理:namespce锁定确保原子性和正确性(第4.1节);主操作日志定义了这些操作的全局总顺序(第2.6.3节)。

数据变化后文件区域的状态取决于变化的类型, 无论这次数据变化它是成功还是失败以及是否存在并发变化。表1总结了这些情况结果。如果所有客户端始终读取相同的数据,则无论它们从哪个副本读取文件,文件区域都是一致的。

如果文件数据变化是一致的,则这个 region 称为被 defined,客户端将看到该变化的全部内容。当变化成功而不受并发编写者的干扰时,受影响的区域被称为 defined(并暗示一致):所有客户端将始终看到该变化所写的内容。

并发成功的变化使该区域不确定但保持一致:所有客户都看到相同的数据,但可能无法反映任何一个变化所写的内容。通常,它由来自多个变化的混合片段组成。失败的变化会使区域不一致(因此也称为 undefined):不同的客户端可能在不同的时间看到不同的数据。我们在下面描述应用程序如何区分已定义区域和未定义区域。应用程序不需要进一步区分不同种类的未定义区域。

image-20200627133539354

数据变化可以是写或记录追加。写操作导致以应用程序指定的文件偏移量写入数据。记录附加会导致数据(“record”)至少在原子上附加一次,即使存在并发变化,但也会偏离GFS的选择(第3.3节)。 (相反,“常规”追加只是在客户机认为是文件当前结尾的偏移量处进行写操作。)该偏移量将返回给客户机,并标记包含记录的已定义区域的开始。此外,GFS可能会在其间插入填充或记录重复项。它们占据被认为不一致的区域,通常被用户的数据减少了。

经过一系列成功的变化后,可以确保定义了变化的文件区域,并包含最后一次变化写入的数据十位。 GFS通过(a)在其所有副本上以相同顺序对块应用变化来实现此目的(第3.1节),以及(b)使用块版本号来检测任何已过时的副本,因为它丢失了变化而又过时了chunkserver关闭(第4.5节)。陈旧的副本永远不会参与任何变化,也不会提供给客户向 master 询问块位置。尽早把它们当作垃圾收集它们。

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

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

2.7.2 Implications for Applications

实际上,我们所有的应用程序都通过追加而不是覆盖来使文件发生变化。

在一种典型的用法中,编写者会从头到尾生成一个文件。 写入所有数据后,它会自动将文件原子重命名为永久性名称,或定期检查成功写入了多少数据。检查点还可以包括应用程序级别的校验和。读取器仅验证和处理直到最后一个检查点的文件区域,已知该检查点处于已定义状态。无论一致性和并发性问题如何,这种方法都为我们提供了很好的服务。与随机写操作相比,追加操作对应用程序故障的处理效率更高,并且更具弹性。检查点允许编写者以增量方式重新启动,并阻止读者处理从应用程序角度来看仍不完整的成功写入文件数据。

在另一种典型用法中,许多作者同时将文件追加到文件中以合并结果或作为生产者-消费者队列。 记录追加的至少一次追加语义会保留每个作者的输出。读者对偶尔的填充和重复内容的处理如下。作者编写的每条记录都包含诸如校验和之类的额外信息,以便可以验证其有效性。读者可以使用校验和识别并丢弃多余的填充并记录片段。如果它不能容忍偶尔的重复(例如,如果它们会触发非幂等的操作),则可以使用记录中的唯一标识符将其过滤掉,而无论如何要为相应的应用程序实体(例如Web文档)命名,通常都需要使用这些标识符。记录I / O的这些功能(重复删除除外)在我们的应用程序共享的库代码中,并且适用于Google的其他文件接口实现。这样一来,相同的记录顺序,加上稀有的重复项,始终会传送到记录读取器。

3 System Interactions

我们设计了该系统,以最大程度地减少 Master 在所有操作中的参与。在此背景下,我们现在描述 client,Master 和 chunk servers 如何交互以实现数据变化,原子记录追加和快照。

3.1 Leases and Mutation Order

变化是一种更改块的内容或元数据的操作,例如写操作或追加操作。 每个变化都在所有区块的副本中执行。 我们使用租约来维护副本之间一致的变化顺序。 主服务器将块租约授予其中一个副本,我们称之为主副本。 主数据库为该块的所有变化选择一个序列顺序。 应用变化时,所有副本均遵循此顺序。 因此,全局变化顺序首先由主机选择的租约授权顺序定义,而在租约范围内则由主服务器分配的序列号定义。

租约机制旨在最大程度地减少主服务器上的管理开销。 租约的初始超时为60秒。 但是,只要对块进行了变化,主节点就可以无限期地请求主节点并通常从主节点接收扩展。 这些扩展请求和授权会承载在主机与所有块服务器之间定期交换的HeartBeat消息上。 主机有时可能会尝试在租约到期之前撤销租约(例如,当主机希望禁用正在重命名的文件上的变化时)。 即使主服务器与主服务器失去通信,它也可以在旧租约到期后安全地将新租约授予另一个副本。

在图2中,我们通过遵循这些编号步骤的写入控制流程来说明此过程:

image-20200627174723219

  1. 客户端询问主服务器,哪个块服务器持有该块的当前租约以及其他副本的位置。 如果没有人租用,则主服务器将其授予其选择的副本(未向外表现)。
  2. Master 答复 primary 副本的身份以及其他(辅助)副本的位置。 客户端缓存此数据以备将来使用。 仅当主服务器无法访问或答复它不再拥有租约时,它才需要再次与主服务器联系。
  3. 客户端将数据推送到所有副本。一个客户可以以任何顺序进行。每个块服务器将数据存储在内部LRU缓冲区高速缓存中,直到使用或老化数据为止。通过将数据流与控制流分离,我们可以通过基于网络拓扑调度昂贵的数据流来提高性能,而不管哪个块服务器是主要的。 3.2节将对此进行进一步讨论。
  4. 一旦所有副本均确认已接收到数据,客户端将向主数据库发送写请求。该请求标识了更早推送到所有副本的数据。主数据库为可能从多个客户端接收到的所有变化分配连续的序列号,这提供了必要的序列化。它将变化按序列号顺序应用于其自身的本地状态。
  5. 主服务器将写请求转发到所有辅助副本。每个辅助副本均按照主副本分配的相同序列号顺序应用变化。
  6. 辅助节点均答复主要节点,表明它们已完成操作。
  7. primary 节点答复客户。在任何副本上遇到的任何错误都将报告给客户端。万一出现错误,写操作可能会在辅助副本的主副本和任意子集成功完成。 (如果它在主服务器上失败,则不会分配序列号并转发。)客户端请求被视为失败,并且修改后的区域处于不一致状态。我们的客户代码通过重试失败的变化来处理此类错误。在从写入开始返回到重试之前,它将在步骤(3)到(7)进行一些尝试。

如果应用程序的写操作较大或跨越块边界,则GFS客户端代码会将其分解为多个写操作

它们都遵循上述控制流程,但可能与其他客户端的并发操作交错并被其覆盖。 因此,共享文件区域可能最终包含来自不同客户端的片段,尽管这些副本将是相同的,因为各个操作在所有副本上均以相同的顺序成功完成。 如第2.7节所述,这会使文件区域处于一致但未定义的状态。

3.2 Data Flow

我们将数据流与控制流分离开来,以有效地使用网络。

当控制权从客户端流向主服务器,然后流至所有第二服务器时,数据将以流水线方式沿着精心挑选的块服务器链线性推送。我们的目标是充分利用每台计算机的网络带宽,避免网络瓶颈和高延迟链接,并最小化推送所有数据的延迟。

为了充分利用每台机器的网络带宽,数据会沿着区块服务器链线性推送,而不是以其他拓扑结构(例如,树)进行分发。

因此,每台机器的全部出站带宽用于尽可能快地传输数据,而不是在多个接收者之间分配。

为了尽可能避免网络瓶颈和高延迟链接(例如,交换机间链接经常同时出现),每台计算机都将数据转发到网络拓扑中尚未接收到数据的“最近”计算机。

假设客户端将数据推送到块服务器S1到S4。它将数据发送到最近的块服务器,例如S1。 S1通过最接近S1的S4(例如S2)将其转发到最接近的块服务器S2。同样,S2将其转发到S3或S4,以更接近S2的那个为准,依此类推。我们的网络拓扑非常简单,可以从IP地址准确估计“距离”。

最后,我们通过流水线化TCP连接上的数据传输来最大程度地减少延迟。

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

3.3 Atomic Record Appends

GFS提供了一个称为记录追加的原子追加操作。

在传统的写入中,客户端指定要写入数据的偏移量。并发写入同一区域不可序列化:该区域最终可能包含来自多个客户端的数据片段。但是,在记录追加中,客户端仅指定数据。 GFS会以GFS选择的偏移量将它至少一次原子(即,作为一个连续的字节序列)附加到文件中一次,并将该偏移量返回给客户端。这类似于在多个编写器同时执行的情况下,写入以O_APPEND模式在Unix中打开的文件而没有竞争条件。

记录追加在我们的分布式应用程序中被大量使用,其中不同计算机上的许多客户端同时向同一文件追加。

如果客户使用传统写入操作,则它们将需要其他复杂且昂贵的同步,例如通过分布式锁管理器。在我们的工作负载中,此类文件通常充当多生产者/单消费者队列,或包含来自许多不同客户端的合并结果。

记录追加是一种变化,它遵循第3.1节中的控制流,而在主记录中仅包含一些额外的逻辑。

客户端将数据推送到文件最后一个块的所有副本,然后,将其请求发送到主服务器。主数据库检查是否将记录追加到当前块上会导致该块超过最大大小(64 MB)。如果是这样,它会将数据块填充到最大大小,告诉辅助数据库执行相同的操作,然后回复客户端以指示应在下一个数据块上重试该操作。 (记录追加被限制为最大块大小的四分之一,以使最坏情况的碎片保持在可接受的水平。)如果记录适合最大大小(通常是这种情况),则主记录将数据追加到它的副本,告诉辅助服务器将数据写入数据所处的确切偏移位置,最后将成功回复给客户端。

如果记录追加在任何副本上均失败,则客户端将重试该操作。

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

3.4 Snapshot

快照操作几乎可以瞬间复制文件或目录树(“源”),同时最大程度地减少正在进行的变化的中断。我们的用户使用它来快速创建大型数据集的分支副本(通常是递归创建这些副本的副本),或者在尝试进行以后可以轻松提交或回滚的更改之前检查当前状态。

像AFS [5]一样,我们使用标准的写时复制技术来实现快照。

当主服务器接收到快照请求时,它首先撤销要快照的文件中的块上所有未完成的租约。这确保了对这些块的任何后续写入都将需要与主机进行交互以找到租约持有人。这将使Master 有机会首先创建块的新副本。

租约被撤销或过期后,Master 将操作记录到磁盘。然后,它通过复制源文件或目录树的元数据,将此日志记录应用于其内存中状态。新创建的快照文件指向与源文件相同的块。

快照操作之后,客户端第一次要写入块C时,它将向主机发送请求以查找当前的租约持有人。主机注意到块C的引用计数大于1。它推迟了对客户请求的答复,而是选择了一个新的块句柄C’。然后,它要求具有当前C副本的每个块服务器创建一个名为C’的新块。通过在与原始服务器相同的chunkserver上创建新的chunk,我们确保可以在本地复制数据,而不是通过网络复制(我们的磁盘速度大约是我们100 Mb,是以太网链路速度的三倍)。从这一点来看,请求处理与任何块都没有什么不同:主服务器授予副本之一对新块C'的租约,并回复客户端,客户端可以正常写入该块,而不知道它刚被从现有块创建。

4 Master Operation

主机的工作:

  • 命名空间管理和锁定
  • 副本放置决策
  • 创建副本,再复制副本,再平衡副本
  • 回收未使用的存储
  • 陈旧副本检测

4.1 Namespace Management and Locking

许多主操作可能需要很长时间:例如,快照操作必须撤销快照覆盖的所有块上的块服务器租约。我们不想在其他主操作运行时延迟它们。

因此,我们允许多个操作处于活动状态,并在名称空间的区域上使用锁以确保正确的序列化。

与许多传统文件系统不同,GFS没有按目录的数据结构来列出该目录中的所有文件。它也不支持相同文件或目录的别名(即,Unix术语中的硬链接或符号链接)。 GFS在逻辑上将其命名空间表示为将完整路径名映射到元数据的查找表。通过前缀压缩,可以在内存中有效地表示该表。命名空间树中的每个节点(绝对文件名或绝对目录名)都有一个关联的读写锁。

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

创建块副本的原因有以下三个:

  • 块创建

    当 Master 创建一个块时,它选择放置最初为空的副本的位置。它考虑了几个因素。

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

    (2)我们想限制每个块服务器上“最近”创建的数量。尽管创建本身很便宜,但它可以可靠地预测即将到来的大量写流量,因为大块是在需要写时创建的,并且在我们的“一次读取多次”工作负载中,它们通常在被写入后实际上变成只读的。完全书面。

    (3)如上所述,我们希望在机架上分布块的副本。

  • 重新复制

一旦可用副本的数量低于用户指定的目标,主服务器就会重新复制块。

可能由于多种原因而发生这种情况:

  1. chunkserver变得不可用,chunkserver报告其副本可能已损坏,由于错误而禁用了其中一个磁盘,或者提高了复制目标。需要重新复制的每个块都基于多个因素确定优先级。一是离复制目标还有多远。例如,我们给丢失了两个副本的块赋予了比仅丢失一个副本的块更高的优先级。
  2. 另外,相对于属于最近删除的文件的块,我们更喜欢先为活动文件重新复制块(请参见第4.4节)。
  3. 最后,为了最大程度地减少故障对正在运行的应用程序的影响,我们提高了阻止客户端进度的任何块的优先级。

主机选择最高优先级的块,并通过指示某些块服务器直接从现有有效副本中复制块数据来对其进行“克隆”。

放置新副本的目标类似于创建副本的目标:均衡磁盘空间利用率,限制任何单个块服务器上的活动克隆操作,以及将副本分布在机架上。为了防止克隆的通信量淹没客户端的通信量,主服务器限制群集和每个块服务器的活动克隆操作数。此外,每个块服务器通过限制其对源块服务器的读取请求来限制其在每个克隆操作上花费的带宽量。

  • 重新平衡

主服务器会定期重新平衡副本:它检查当前副本分发,并移动副本以获得更好的磁盘空间和负载平衡。同样通过此过程,主服务器逐渐填充新的chunkserver,而不是立即用新的chunk和随之而来的大量写入流量将其淹没。新副本的放置标准与上面讨论的相似。此外,主服务器还必须选择要删除的现有副本。通常,它更喜欢删除块服务器上具有低于平均可用空间的磁盘,以平衡磁盘空间使用。

4.4 Garbage Collection

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

4.4.1 Mechanism

当应用程序删除文件时,主机会像其他更改一样立即记录该删除。但是,不是立即回收资源,而是将文件重命名为包含删除时间戳的隐藏名称。

在主文件系统定期扫描文件系统名称空间的过程中,如果这些隐藏文件已存在超过三天(时间间隔是可配置的),它将删除这些文件。在此之前,仍可以使用新的特殊名称读取文件,并且可以通过将其重命名为正常名称来取消删除该文件。当从名称空间中删除隐藏文件时,其内存中的元数据将被删除。这有效地切断了其所有块的链接。

在对块名称空间的类似常规扫描中,主服务器识别孤立的块(即,无法从任何文件访问的块)并清除这些块的元数据。在定期与主服务器交换的HeartBeat消息中,每个块服务器报告其拥有的块的子集,而主服务器将答复不再存在于主服务器的元数据中的所有块的标识。块服务器可以自由删除其此类块的副本。

4.4.2 Discussion

尽管分布式垃圾回收是一个困难的问题,需要在编程语言的上下文中提供复杂的解决方案,但在我们的案例中,这非常简单。我们可以轻松地识别所有对块的引用:

它们在主机专有的文件到块的映射中。我们还可以轻松识别所有块副本:它们是每个块服务器上指定目录下的Linux文件。主机不知道的任何此类副本都是“垃圾”。

与立即删除相比,用于回收存储的垃圾收集方法具有多个优点。

  • 首先,它在组件故障常见的大型分布式系统中既简单又可靠。块创建可能会在某些块服务器上成功,但在其他一些块服务器上不会成功,从而留下主服务器不知道的副本。副本删除消息可能会丢失,主服务器必须记住要在失败时重新发送它们,无论是自身的还是块服务器的。垃圾收集提供了一种统一且可靠的方式来清理所有未知有用的副本。
  • 其次,它将存储回收活动合并到主数据库的常规后台活动中,例如,对名称-速度和与块服务器握手的常规扫描。因此,它是分批完成的,成本也要摊销。而且,仅在主服务器相对空闲时才执行此操作。管理员可以更迅速地响应需要及时关注的客户请求。
  • 第三,回收存储的延迟为防止意外的,不可逆的删除提供了一个安全网。

根据我们的经验,主要缺点是延迟删除有时会妨碍用户在存储空间时进行微调使用

重复创建和删除临时文件的应用程序可能无法立即重用存储。如果删除的文件再次被明确删除,我们将通过加快存储相关性来解决这些问题。我们还允许用户将不同的复制和回收策略应用于名称空间的不同部分。例如,用户可以指定某个目录树中文件中的所有块都应存储而不复制,并且所有已删除的文件都会立即且不可撤消地从文件系统状态中删除。

4.5 Stale Replica Detection

如果块服务器发生故障并且在其关闭时丢失对块的变化,则块副本可能会过时。对于每个块,Master 维护一个块版本号,以区分最新副本和陈旧副本。

每当主服务器授予块上的新租约时,它都会增加块版本号并通知最新的副本。

主服务器和这些副本都在其持久状态下记录新版本号。这发生在任何客户端被通知之前,因此在它可以开始写入块之前。如果另一个副本当前不可用,则其块版本号不会提高。当块服务器重新启动并报告其块组及其关联的版本号时,主服务器将检测到该块服务器具有陈旧的副本。如果主服务器看到的版本号大于其记录中的版本号,则主设备会认为在授予租约时它失败了,因此将更高的版本更新为最新版本。

主机在其常规垃圾回收中删除陈旧的副本。 在此之前,它在答复客户端对块信息的请求时实际上认为陈旧的副本根本不存在。作为另一种保护措施,当主机在克隆操作中通知客户端哪个chunkserver持有该chunk的租约时或当它指示chunkserver从另一个chunkserver读取chunk时,master会包含chunk版本号。客户端或块服务器在执行操作时会验证版本号,以便它始终在访问最新数据。

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

复制 Master 的状态是为了提高可靠性。它的操作日志和检查点被复制到多台计算机上。仅在将其状态记录的日志记录刷新到本地磁盘和所有主副本上的磁盘之后,才认为状态变化已提交。

为简单起见,一个主进程负责所有变化以及后台活动(例如在内部更改系统的垃圾回收)。当它失败时,它几乎可以立即重新启动。如果其计算机或磁盘发生故障,则GFS外部的监视基础结构将使用复制的操作日志在其他位置启动新的主进程。客户端仅使用主机的规范名称(例如 gfs-test ),这是一个DNS别名,如果将主机重定位到另一台计算机,则可以更改该DNS别名。

而且,“影子”master即使在主master宕机时也提供对文件系统的只读访问。

它们是影子主机,而不是镜像主机,因为它们可能会稍微滞后于初级,通常是几分之一秒。它们提高了未被主动变化的文件或不介意获得稍微陈旧结果的应用程序的读取可用性。实际上,由于从块服务器读取文件内容,因此应用程序不会观察到过时的文件内容。短窗口内可能过时的是文件元数据,例如目录内容或访问控制信息。

为了使自己随时了解情况,影子主机读取了不断增长的操作日志的副本,并对其数据结构进行了与主数据库完全相同的更改序列。像 primary 一样,它在启动时(且之后很少)轮询块服务器,以查找块副本,并与它们交换频繁的握手消息以监视其状态。它仅依赖于主服务器上的副本位置更新,该更新是由主服务器决定创建和删除副本而导致的。

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磁盘以及与HP 2524交换机的100 Mbps全双工以太网连接。 所有19台GFS服务器计算机都连接到一台交换机,所有16台客户机计算机都连接到另一台。 两个交换机通过1 Gbps链路连接。

6.1.1 Reads

N个客户端同时从文件系统读取。每个客户端从320 GB的文件集中读取随机选择的4 MB区域。重复256次,以便每个客户端最终读取1 GB数据。合在一起的块服务器仅具有32 GB的内存,因此我们期望Linux缓冲区高速缓存中的命中率最高为10%。我们的结果应该接近冷缓存结果。

图3(a)显示了N个客户端的总读取速率及其理论极限。当两个交换机之间的1 Gbps链路达到饱和时,限制的峰值总计为125 MB / s;如果其客户端的100 Mbps网络接口达到饱和,则每个客户端为12.5 MB / s(以适用者为准)。当仅一个客户端正在读取时,观察到的读取速率为10 MB / s,即每个客户端限制的80%。对于16个读取器,总读取速率达到94 MB / s,约为125 MB / s链接限制的75%,或每个客户端6 MB / s。效率从80%下降到75%,因为随着读取器数量的增加,多个读取器同时从同一块服务器读取数据的可能性也随之增加。

image-20200627193839727

6.1.2 Writes

N个客户端同时写入N个不同的文件。每个客户端以1 MB的写入顺序将1 GB的数据写入新文件。总写入速率及其理论极限如图3(b)所示。极限平稳期为67 MB / s,因为我们需要将每个字节写入16个块服务器中的3个,每个块服务器具有12.5 MB / s的输入连接。

一个客户端的写入速率为6.3 MB / s,约为限制的一半。造成这种情况的主要原因是我们的网络堆栈。它与我们用于将数据推送到大块副本的流水线方案互动性不是很好。将数据从一个副本传播到另一个副本的延迟会降低总体写入速率。

16个客户端的总写入速率达到35 MB / s(每个客户端2.2 MB / s),约为理论上限的一半。与读取的情况一样,随着客户端数量的增加,多个客户端并发写入同一块服务器的可能性更高。而且,与每个16个读写器相比,与16个读写器发生冲突的可能性更大,因为每个写入涉及三个不同的副本。

写的速度比我们想要的慢。实际上,这并不是一个主要问题,因为即使它增加了各个客户端看到的延迟,也不会显着影响系统交付给大量客户端的聚合写带宽。

6.1.3 Record Appends

图3(c)显示了记录追加性能。 N个客户端同时附加到单个文件。 性能由存储文件的最后一个块的块服务器的网络带宽限制,而与客户端数量无关。 对于一个客户端,它的开始速度为6.0 MB / s,对于16个客户端,它的速度降至4.8 MB / s,这主要是由于不同客户端看到的网络传输速率的拥塞和差异所致。

目前,我们的应用程序倾向于生成多个此类文件。 换句话说,N个客户端同时附加到M个共享文件,其中N和M都在数十个或数百个中。 因此,在我们的实验中,chunkserver网络拥塞实际上并不是一个重要问题,因为客户端可以在编写一个文件时取得进展,而另一文件的chunkserver却很忙。

6.2 Real World Clusters

现在,我们检查一下Google内部正在使用的两个集群,它们分别代表了其他几个集群。

数百名工程师定期将Cluster A用于研发。 一个典型的任务是由人类用户发起的,并且运行时间长达几个小时。 它读取从几MB到几TB的数据,转换或分析数据,然后将结果写回群集。

群集B主要用于生产数据处理。 这些任务的持续时间更长,并且仅在偶尔的人工干预下就可以连续生成和处理多结核病数据集。 在这两种情况下,单个“任务”都由许多计算机上的多个进程同时读取和写入许多文件组成。

6.2.1 Storage

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

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

image-20200627194301440

6.2.2 Metadata

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

保留在主服务器上的元数据要小得多,只有几十MB,或者平均每个文件约100个字节。 这与我们的假设一致,即主机的内存大小实际上不会限制系统的容量。每个文件的元数据大多数是以前缀压缩形式存储的文件名。其他元数据包括文件所有权和权限,从文件到块的映射以及每个块的当前版本。另外,对于每个块,我们存储当前副本位置和用于实现写时复制的参考计数。

每个单独的服务器(块服务器和主服务器)都只有50到100 MB的元数据。因此,恢复速度很快:服务器仅需几秒钟即可从磁盘读取此元数据,服务器便可以回答查询。但是,主机会在一段时间(通常为30到60秒)内徘徊,直到它从所有块服务器获取块位置信息为止。

6.2.3 Read and Write Rates

表3列出了不同时间段的读写速率。 进行这些测量时,两个群集都已经运行了大约一周。 (群集最近已重新启动,以升级到新版本的GFS。)

自重启以来,平均写入速率小于30 MB / s。 当我们进行这些测量时,B处于写入活动突发的中间,该活动产生大约100 MB / s的数据,由于写入传播到三个副本,因此产生了300 MB / s的网络负载。

读取速率远高于写入速率。 如我们所假设的,总的工作量包含的读取次数多于写入次数。 两个集群都处于大量读活动中。 特别是,A在前一周一直保持580 MB / s的读取速率。 它的网络配置可以支持750 MB / s,因此它可以有效地利用其资源。 群集B可以支持1300 MB / s的峰值读取速率,但是其应用程序仅使用380 MB / s。

image-20200627194540529

6.2.4 Master Load

表3还显示发送到主服务器的操作速率约为每秒200到500个操作。主机可以轻松地跟上该速率,因此对于这些工作负载而言,这不是瓶颈。

在早期版本的GFS中,主服务器有时是某些工作负载的瓶颈。它花费了大部分时间依次浏览大型目录(包含数十万个文件)以查找特定文件。此后,我们更改了主数据结构,以允许通过名称空间进行有效的二进制搜索。现在,它可以轻松地支持每秒数千个文件访问。如有必要,我们可以通过在名称空间数据结构之前放置名称查找缓存来进一步加快速度。

6.2.5 Recovery Time

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

在另一个实验中,我们杀死了两个带有大约16,000个块和660 GB数据的块服务器。此双重故障将266个块减少为只有一个副本。这266个块以较高的优先级进行克隆,并在2分钟内全部恢复到至少2倍的复制,从而使群集处于可以忍受另一个块服务器故障而不会丢失数据的状态。

6.3 Workload Breakdown

在本节中,我们将详细介绍两个GFS群集上的工作量细分,这些工作量与6.2节中的工作量相当但不相同。群集X用于研发,而群集Y用于生产数据处理。

6.3.1 Methodology and Caveats

这些结果仅包括客户端发起的请求,因此它们反映了我们的应用程序为整个文件系统生成的工作量。它们不包括执行客户端请求的服务器间请求或内部后台活动,例如转发的写入或重新平衡。

I / O操作的统计信息基于从GFS服务器记录的实际RPC请求中启发式重建的信息。例如,GFS客户端代码可能将读取分为多个RPC,以增加并行度,我们从中推断出原始读取。由于我们的访问模式是高度程式化的,因此我们希望任何错误都可能来自噪声。应用程序的显式日志记录可能会提供稍微更准确的数据,但是从逻辑上讲,重新编译并重新启动成千上万个正在运行的客户端是不可能的,而且从许多机器中收集结果很麻烦。

应该注意不要过度概括我们的工作量。由于Google完全控制GFS及其应用程序,因此这些应用程序倾向于针对GFS进行调整,相反,GFS是为这些应用程序设计的。这样的相互影响也可能在通用应用程序和文件系统之间存在,但在我们的案例中,这种影响可能会更加明显。

6.3.2 Chunkserver Workload

表4显示了按大小划分的操作分布。读取大小显示出双峰分布。小读操作(不到64 KB)来自于搜索密集型客户端,这些客户端在大文件中查找小数据。大量读取(超过512 KB)来自对整个文件的长时间顺序读取。

image-20200627195048332

在集群Y中,大量读取根本不返回任何数据。我们的应用程序,尤其是生产系统中的应用程序,经常使用文件作为生产者-消费者队列。当使用者读取文件末尾时,生产者会同时将其附加到文件中。有时,当消费者超过生产者时,不会返回任何数据。群集X较少显示此信息,因为它通常用于短期数据分析任务,而不是长期分布式应用程序。

写入大小也表现出双峰分布。较大的写入(超过256 KB)通常是由于写入器内有大量缓冲造成的。写入器缓冲较少的数据,更频繁地检查点或进行同步,或者仅为较小的写入(小于64 KB)生成较少的数据。

至于记录追加,群集 Y 的大型记录追加百分比比群集 X 高得多,因为使用群集Y的生产系统针对GFS进行了更积极的调整。

表5列出了各种大小的操作中传输的数据总量。对于所有类型的操作,较大的操作(超过256 KB)通常占传输的大多数字节。由于随机查找工作负载,小读(64 KB以下)确实会传输一小部分但很大一部分的读数据。

image-20200627195313272

6.3.3 Appends versus Writes

记录追加被大量使用,尤其是在我们的生产系统中。对于群集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 Workload

表6显示了按主请求类型分类的细分。大多数请求要求块位置(FindLocation)进行读取,并要求租约所有者信息(FindLeaseLocker)进行数据变化。

image-20200627195346713

集群X和Y看到的删除请求数量明显不同,因为集群Y存储的生产数据集会定期重新生成并替换为较新的版本。这种差异中的某些差异进一步隐藏在Open请求中的差异中,因为文件的旧版本可能会通过从头开始进行写操作而被隐式删除(Unix开放术语中的模式“ w”)。

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

7 Experiences

在构建和部署GFS的过程中,我们遇到了许多问题,有些是操作方面的,有些是技术方面的。

最初,GFS被认为是我们生产系统的后端文件系统。随着时间的流逝,用法逐渐演变为包括研发任务。它最初几乎不支持权限和配额之类的东西,但现在包括了基本形式。尽管生产系统受到严格的纪律和控制,但用户有时却没有。需要更多的基础架构来防止用户相互干扰。
我们最大的问题是磁盘和Linux相关的。我们的许多磁盘都向Linux驱动程序声称它们支持多种IDE协议版本,但实际上仅对较新版本进行了可靠的响应。由于协议版本非常相似,因此这些驱动器大多数都可以工作,但是偶尔的不匹配会导致驱动器和内核对驱动器的状态持不同意见。由于内核问题,这将以静默方式破坏数据。这个问题促使我们使用校验和来检测数据损坏,同时我们修改了内核以处理这些协议不匹配。

早些时候,由于fsync()的成本,我们在Linux 2.2内核上遇到了一些问题。它的成本与文件的大小成正比,而不与修改部分的大小成正比。对于我们的大型操作日志而言,这是一个问题,尤其是在实施检查点之前。我们使用同步写入解决了一段时间,并最终迁移到Linux 2.4。

另一个Linux问题是单个读写器锁,当它从磁盘分页时(地址锁)或修改mmap()调用中的地址空间(写锁),地址空间中的任何线程都必须持有该线程。在轻负载下,我们看到了系统中的瞬时超时,并努力寻找资源瓶颈或偶发的硬件故障。最终,我们发现,当磁盘线程分页先前映射的数据时,该单个锁阻止了主网络线程将新数据映射到内存中。由于我们主要受网络接口的限制,而不是受内存复制带宽的限制,因此,我们通过使用pread()替换mmap()来解决此问题,但需要额外的副本。

尽管偶尔出现问题,Linux代码的可用性也一次又一次地帮助我们探索和理解系统行为。在适当的时候,我们改进内核并与开源社区共享所做的更改。

8 Related Work

9 Conclusions

GFS 展示了在商用硬件上支持大规模数据处理工作负载所必需的质量。尽管某些设计决策是特定于我们独特设置的,但许多决策可能适用于规模和成本意识相似的数据处理任务。

我们首先根据当前和预期的应用程序工作负载和技术环境,重新检查传统的文件系统假设。我们的发现导致了设计领域的根本不同。我们将组件故障视为正常现象,而不是例外情况,针对大型文件进行优化,这些文件通常会附加到(也许同时进行),然后再读取(通常是顺序执行),并且都扩展和放松了标准文件系统接口,以改善整个系统。

我们的系统通过不断监控,复制关键数据以及快速自动恢复功能来提供容错能力。块复制使我们能够容忍chunkserver故障。这些故障的发生率激发了一种新颖的在线修复机制,该机制定期透明地修复损坏并尽快补偿丢失的复制品。此外,我们使用校验和来检测磁盘或IDE子系统级别的数据损坏,鉴于系统中的磁盘数量,这种情况变得非常普遍。

我们的设计为许多同时执行各种任务的读取器和写入器提供了高总吞吐量。我们通过将通过主机的文件系统控制与直接在块服务器和客户端之间传递的数据传输分开来实现此目的。大型块的大小和块的租用将主操作对常见操作的影响降到最低,这将权限授予数据突变中的主副本。这样就可以成为一个简单的,集中的主服务器,而不会成为瓶颈。我们认为,网络堆栈的改进将解除当前对单个客户端看到的写入吞吐量的限制。

GFS已成功满足了我们的存储需求,并已在Google内部广泛用作研究,开发和生产数据处理的存储平台。它是使我们能够继续创新并解决整个Web规模问题的重要工具。