我们设计并实现了Google File System,这是一个可扩展的分布式文件系统,适用于大型分布式数据密集型应用。它在廉价的商用硬件上运行且提供容错功能,并为大量客户端提供总体很高的性能。
虽然与以前的分布式文件系统有着许多相同的目标,但我们的设计是基于对当前和预期的应用工作负载和技术环境的观察来驱动的,这反映出与早期文件系统假设的明显不同。这使我们重新审视传统的选择,并探索完全不同的设计观点。
文件系统已成功满足我们的存储需求。它在Google中作为存储平台广泛部署,用于生成和处理我们的服务所使用的数据以及需要大数据集的研发工作。迄今为止最大的集群在超过一千台计算机的数千块磁盘上提供数百TB的存储,并且可以由数百个客户端同时访问。
在本文中,我们提供了旨在支持分布式应用文件系统接口扩展、讨论我们设计的许多方面、并报告来自微基准测试和现实使用的测量。
D [4]:3 - 分布式文件系统
设计,可靠性,性能,测量
容错,可扩展性,数据存储,集群存储
我们设计并实现了Google文件系统(GFS),以满足Google数据处理需求的快速增长需要。GFS与以前的分布式文件系统具有许多相同的目标,例如性能、可扩展性、可靠性和可用性。然而,其设计受到我们的应用工作负载和技术环境(包括当前和预期)的关键观察的驱动,这反映了与早期文件系统设计假设的明显不同。我们重新审视了传统选择,并从设计空间中探索出截然不同的观点。
首先,组件故障是常态而非例外。该文件系统由数百甚至数千个廉价商业部件构建的存储机器组成,并且可由相当数量的客户端机器访问。组件的数量和质量实际上导致了在任何给定时间都可能有一些组件不可用,并且有些组件无法从其当前故障中恢复。我们已经看到了由应用错误、操作系统错误、人为错误以及磁盘、内存、连接器、网络和电源故障引起的问题。因此,持续监控、错误检测、容错和自动恢复必须集成到系统中。
其次,按传统标准衡量,文件都很大。多GB大的文件很常见。每个文件通常包含许多应用对象,例如Web文档。当我们经常处理快速增长的包含数十亿个对象的许多TB的数据集时,即使文件系统可以支持它,也很难管理数十亿个大小仅为KB的文件。因此,必须重新考虑设计假设和参数,例如I/O操作和块大小。
第三,大多数文件通过追加新数据而不是覆盖现有数据而改变。文件中的随机写入几乎不存在。 一旦写入,文件只会读取,且通常只会按顺序读取。各种各样的数据拥有这些特性。有些可能构成数据分析程序扫描的大型数据仓库。有些可能是运行中的应用程序不断生成的数据流。有些可能是归档数据。有些可能是在一台机器上生成并在另一台机器上处理的中间结果,无论时间上是同时还是稍后。鉴于在大文件上的这种访问模式,追加成为性能优化和原子性保证的焦点,而在客户端中缓存数据块则失去吸引力。
第四,共同设计应用程序和文件系统API有益于整个系统,这提高了我们的灵活性。例如,我们放宽了GFS的一致性模型,以大大简化文件系统,而不会给应用带来繁重的负担。我们还引入了一个原子追加操作,以便多个客户端可以同时追加到文件,而无需在它们之间进行额外的同步。这些将在本文后面更详细地讨论。
目前为不同目的部署了多个GFS集群。最大的集群有超过1000个存储节点,超过300 TB的磁盘空间,并且不断地被不同的机器上的数百个客户端大量访问。
在根据我们的需求设计文件系统时,我们一直遵循既提供挑战又提供机遇的假设。我们之前提到了一些关键的观察结果,现在详细地列出我们的假设。
GFS提供了熟悉的文件系统接口,但它没有实现POSIX等标准API。文件在目录中按层次结构组织,并由路径名标识。我们支持create、delete、open、close、read和write文件的常规操作。
此外,GFS还有快照和记录追加操作。快照可以低成本地创建文件或目录树的副本。记录追加允许多个客户端同时将数据追加到同一文件,同时保证每个客户端追加的原子性。它对于实现多路合并结果和生产者 - 消费者队列非常有用,许多客户端可以在没有额外锁的条件下同时进行追加。我们发现这些文件类型在构建大型分布式应用程序时非常有用。快照和记录追加分别在第3.4节和第3.3节中进一步讨论。
GFS集群由单个master和多个chunkserver组成,并由多个客户端访问,如图1所示。每个客户端通常都是运行用户级服务器进程的一台商用Linux机器。只要机器资源允许并且运行可能不稳定的应用程序代码而导致较低可靠性是可接受的,那么在同一台机器上运行chunkserver和客户端很容易。
文件被分为固定大小的块。每个块由master在创建块时分配的不可变且全局唯一的64位块句柄标识。Chunkserver将块作为Linux文件存储在本地磁盘上,并读取或写入由块句柄和字节范围指定的块数据。为了可靠性,每个块都在多个chunkserver上复制。默认情况下,我们存储三个副本,但用户可以为文件命名空间的不同区域指定不同的副本级别。
master维护所有文件系统元数据。这包括命名空间、访问控制信息、从文件到块的映射以及块的当前位置。它还控制系统范围的活动,例如块租约管理、孤立块的垃圾收集以及chunkserver之间的块迁移。master定期与每个chunkserver使用HeartBeat消息通信,以便为其提供指令并收集其状态。
连接每个应用的GFS客户端代码实现了文件系统API,并与master和chunkserver通信,以代表应用读取或写入数据。客户端与master进行元数据操作交互,但所有承载数据的通信都直连chunkserver。我们不提供POSIX API,因此不需要在Linux vnode层挂载钩子。
客户端和chunkserver都不缓存文件数据。客户端缓存提供的好处很少,因为大多数应用流式读取大文件或者工作集太大而无法缓存。没有了客户端缓存(然而,客户端会缓存元数据。),通过消除缓存一致性问题简化了客户端和整个系统。chunkserver不需要缓存文件数据,因为块存储为本地文件,因此Linux的缓冲区缓存已经将频繁访问的数据保存在内存中。
拥有单一master可以极大地简化我们的设计,并使master能够使用全局信息进行复杂的块放置和复制决策。但是,我们必须尽量减少对读写的干预,这样才不会成为瓶颈。客户端永远不会通过master读取和写入文件数据。相反,客户端询问master获取其应该联系哪些chunkserver。它在有限的时间内缓存此信息,并直接与chunkserver交互以进行许多后续操作。
让我们参考图1解释简单读取的交互。首先,使用固定的块大小,客户端将应用指定的文件名和字节偏移转换为文件中的块索引。然后,它向master发送包含文件名和块索引的请求。master回复相应的块句柄和副本的位置。客户端使用文件名和块索引作为键来缓存此信息。
然后,客户端向其中一个副本发送请求,最可能是最接近的副本。请求指定块句柄和该块中的字节范围。在缓存的信息过期或重新打开文件之前,对同一块的再一次读取不再需要客户端和master交互。实际上,客户端通常在同一请求中请求多个块,并且master还可能包括紧跟在那些请求块之后的信息。这些额外的信息可以避免几个未来的客户端 - master交互,几乎不需要额外的成本。
块大小是关键设计参数之一。我们选择了64 MB,这比典型的文件系统块大小要大得多。每个块副本都作为普通Linux文件存储在chunkserver 上,并且仅在需要时进行扩展。延迟的空间分配避免了由于内部碎片(这可能是对如此大的块大小的最大反对意见)造成的空间浪费。
大的块大小提供了几个重要的优点。首先,它减少了客户端与master交互的需要,因为在同一块上的读取和写入只需要向master发送一个初始请求以获取块位置信息。交互减少对我们的工作负载尤其重要,因为应用主要是按顺序读取和写入大文件。即使对于小的随机读取,客户端也可以容易地缓存多TB工作集的所有块位置信息。其次,由于在大块上,客户端更有可能对给定的块执行更多操作,因此可以通过在较长时间内保持与chunkserver的持久TCP连接来减少网络开销。第三,它减少了存储在master上的元数据的大小。这允许我们将元数据保存在内存中,这反过来又带来了我们将在2.6.1节中讨论的其他优势。
另一方面,即使具有延迟空间分配,大的块大小也有其缺点。一个小文件由少量块组成,可能只有一个。如果许多客户端访问同一文件,则存储这些块的chunkserver可能成为热点。在实践中,热点并不是主要问题,因为我们的应用程序主要是按顺序读取大型多块文件。
但是,当批处理队列系统首次使用GFS时,确实产生了热点:可执行文件作为单块文件写入GFS,然后同时在数百台计算机上启动。存储此可执行文件的少数chunkserver因数百个并发请求而过载。我们通过存储具有更高复制因子的可执行文件并使批处理系统错开应用程序启动时间来解决此问题。潜在的长期解决方案是允许客户端在这种情况下从其他客户端读取数据。
master存储三种主要类型的元数据:文件和块命名空间、从文件到块的映射以及每个块的副本的位置。所有元数据都保存在master的内存中。通过将变化记录为存储在master本地磁盘上及在远程计算机上副本的操作日志,前两种类型(名称空间和文件到块的映射)也进行持久化。使用日志可以让我们简单、可靠地更新master状态,并且在master发生崩溃时不会出现不一致的风险。master不会持久化存储块位置信息。相反,它会在master启动时以及每当chunkserver加入集群时向每个chunkserver询问其存储的块。
由于元数据存储在内存中,因此master操作很快。此外,master在后台周期性地扫描其整个状态是简单而高效的。此定期扫描用于实现块垃圾收集,在存在chunkserver故障时重新复制,以及块迁移以平衡chunkserver之间的负载和磁盘空间使用。第4.3和4.4节将进一步讨论这些行为。
这种仅使用内存的方法的一个潜在问题是,块的数量以及整个系统的容量受到master具有多少内存的限制。这在实践中不是严重的限制。master为每个64 MB的块维护少于64个字节的元数据。大多数块都会填满,因为大多数文件包含许多块,只有最后一块可能部分填充。类似地,文件命名空间数据通常每个文件需要少于64个字节,因为它使用前缀压缩紧凑地存储文件名。
如果需要支持更大的文件系统,相比于我们通过将元数据存储在内存中来获得的简单性、可靠性、性能和灵活性,为master增加额外内存的成本是一个很小的代价。
master不会保留哪些chunkserver具有指定块副本的持久化记录。它只是在启动时轮询chunkserver获取该信息。之后,master可以使自己保持最新状态,因为它控制所有块放置并使用常规HeartBeat消息监视chunkserver状态。
我们最初尝试将块位置信息持久保存在master上,但我们认为在启动时(之后定期)从chunkserver请求数据要简单得多。这消除了在chunkserver加入和离开集群、更改名称、失败和重新启动等时保持master和chunkserver同步的问题。在具有数百台服务器的集群中,这些事件经常发生。
理解这个设计策略的另一种方法是认识到一个chunkserver在它自己的磁盘上有没有什么块有最终决定权。尝试在master上维护该信息的一致性视图是没有意义的,因为chunkserver上的错误可能导致块自发消失(例如,磁盘可能坏掉并被禁用)或者操作员可能重命名chunkserver。
操作日志包含关键元数据更改的历史记录。它是GFS的核心。它不仅是元数据的唯一持久化记录,而且还充当定义并发操作顺序的逻辑时间线。文件和块以及它们的版本(参见第4.5节)都是由它们创建时的逻辑时间唯一且永久地标识的。
由于操作日志至关重要,因此我们必须可靠地存储它,并且在元数据的更改持久化之前该改动对客户端不可见。否则,即使块本身存在,我们也会有效地丢失整个文件系统或最近的客户端操作。因此,我们在多台远程计算机上复制它,并且只有在本地和远程将相应的日志记录刷新到磁盘后才响应客户端操作。master在刷新之前将多个日志记录一起批量处理,从而减少刷新和复制对整个系统吞吐量的影响。
master通过重新执行操作日志来恢复其文件系统状态。为了最大限度地缩短启动时间,我们必须保持日志较小。只要日志增长到超过一定大小,master就会保存其状态的检查点,以便通过从本地磁盘加载最新检查点并仅重新执行在此之后有限数量的日志记录来恢复。检查点采用紧凑的B树格式,可以直接映射到内存中,用于命名空间查找而无需额外的解析。这进一步加快了恢复速度并提高了可用性。
因为构建检查点可能需要一段时间,所以master的内部状态以可以创建新的检查点而不会延迟输入的变化的方式构建。master切换到新的日志文件,并在单独的线程中创建新的检查点。新检查点包括切换前的所有变化。对于具有几百万个文件的集群,可以在一分钟左右创建完成它。完成后,它已经在本地和远程被写入磁盘。
恢复只需要最新的完整检查点和后续日志文件。较旧的检查点和日志文件可以自由删除,但我们会保留一些以防止灾难。创建检查点期间的故障不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。
GFS具有宽松的一致性模型,可以很好地支持我们的高度分布式应用程序,但实现起来相对简单和高效。我们现在讨论GFS的对一致性的保证及其对应用程序的意义。我们还重点介绍了GFS如何得到这些一致性保证,但将细节留给了论文的其他部分。
文件命名空间变化(例如,文件创建)是原子的。它们由master完全处理:命名空间锁保证原子性和正确性(第4.1节);master的操作日志定义了这些操作的全局顺序(第2.6.3节)。
数据变化后文件区域的状态取决于变化的类型、是否成功、以及是否存在并发改变。表1总结了结果。如果所有客户端始终看到相同的数据,则文件区域是一致的,无论他们从哪个副本读取。如果区域是一致的,并且客户端将看到改变完整写入的内容,则文件区域在文件数据变化之后确定。当变化成功而没有来自并发写入者的干扰时,受影响的区域是确定的(并且一致):所有客户端将始终看到改变写入后的内容。并发成功的改变使该区域不确定但一致:所有客户端都看到相同的数据,但它可能无法反映任何一个改变所写的内容。通常,它由来自多个变化的混合片段组成。失败的改变会使区域不一致(因此也不确定):不同的客户端可能在不同的时间看到不同的数据。我们在下面描述我们的应用如何区分确定区域和未确定区域。应用不需要进一步区分不同类型的未确定区域。
数据变化可能是写入或记录追加。写入会导致数据在应用指定的文件偏移量处写入。记录追加导致数据(“记录”)即使存在并发改变也至少原子地追加一次,但是在GFS选择的偏移处(第3.3节)。(相反,“常规”追加仅仅是客户端认为是文件当前结尾的偏移量的写入。)偏移量返回给客户端并标记包含该记录的已确定区域的开头。此外,GFS可以在其间插入填充或记录重复项。它们占据被认为不一致的区域,这通常与用户数据量相比相形见绌。
在一系列成功的变化之后,保证变化的文件区域是确定的并包含由最后一个变化写入的数据。GFS通过以下方式实现这一目标:(a)在所有副本上以相同的顺序将改变应用于块(第3.1节),以及(b)使用块版本号来检测任何已变得过时的副本,因为它已丢失变化且其chunkserver下线了(第4.5节)。过时的副本永远不会涉及到变化或在客户端询问master块位置时被返回。它们是最早被收集的垃圾。
由于客户端缓存块位置,因此在刷新信息之前,它们可能会从过时的副本中读取。此刷新窗口受缓存条目超时和文件的下一次打开(这会从缓存中清除该文件的所有块信息)限制。此外,由于我们的大多数文件都是仅追加的,因此过时的副本通常会返回块的提早结束的数据而不是过时的数据。当读取者重试并联系master时,它将立即获得当前的块位置。
成功改变很久之后,组件故障当然仍然会破坏或摧毁数据。GFS通过master和所有chunkserver之间的常规握手来识别失败的chunkserver,并通过校验和检测数据损坏(第5.2节)。一旦出现问题,数据将尽快从有效副本中恢复(第4.3节)。只有在GFS能够做出反应(通常在几分钟之内)之前所有副本都丢失时,块才会不可逆转地丢失。即使在这种情况下,它也是变得不可用,而不是损坏:应用程序收到明确的错误而不是损坏的数据。
GFS应用程序可以通过一些基于其他目的已经使用的简单技术来适应宽松一致性模型:依赖于追加而不是覆盖、检查点和写自我校验、记录自我识别。
实际上我们所有的应用都通过追加而不是覆盖来改变文件。在一个典型的用途中,写入者从头到尾生成文件。它在写入所有数据后以原子方式将文件重命名为永久名称,或定期建立已成功写入多少的检查点。检查点还能包括应用级校验和。读取者仅验证并处理更新到最后一个检查点的文件区域,该文件区域已经处于确定状态。无论一致性和并发性问题如何,这种方法对我们都有好处。与随机写入相比,追加更有效、更适应应用故障。检查点允许写入者以增量方式重新启动,并使读取者无法处理从应用的角度未完成的已经成功写入的文件数据。
在另一个典型用法中,许多写入者同时将合并结果或作为生产者 - 消费者队列追加到文件。记录追加的append-at-least-once
语义保留了每个写入者的输出。读取者处理偶然的填充和重复,如下所示。写入者准备的每条记录都包含校验和等额外信息,以便验证其有效性。读取者可以使用校验和来识别和丢弃额外的填充和记录片段。如果它不能容忍偶然的重复(例如,如果它们将触发非幂等操作),则它可以使用记录中的唯一标识符来过滤掉它们,这些标识符通常需要命名相应的应用实体(例如web文档)。记录I/O的这些功能(除了重复删除)在我们的应用共享的库代码中,同时适用于Google中其他文件接口实现。有了这个,总是给记录读取者传递相同的记录序列加上罕见的重复记录。
我们设计的系统可以最大限度地减少master对所有操作的参与。在此背景下,我们现在描述客户端、master和chunkserver如何交互以实现数据改变、原子记录追加和快照。
变化是改变块的内容或元数据的操作,例如写入或追加操作。每个变化都在块的所有副本上执行。我们使用租约来维护跨副本的一致变化顺序。master将一个块租约授予其中一个副本,我们将其称为主副本。主副本选择块的所有变化的序列顺序。所有副本都遵循此顺序应用变化。因此,全局变化顺序首先由master选择的租约授权顺序确定,并在租约内由主副本分配的序列号确定。
租约机制旨在最大限度地减少master的管理开销。租约的初始超时为60秒。但是,只要块正在变化,主副本就可以请求并且通常无限期地从master获得延期授权。这些延期请求和授权是在master和所有chunkserver之间定期交换的HeartBeat消息上搭载的。master有时可能会在租约到期之前尝试撤销租约(例如,当master想要禁用正在重命名的文件上的变化时)。即使master与主副本失去通信,它也可以在旧租约到期后安全地将新租约授予另一个副本。
在图2中,我们通过遵循这些编号步骤表示的写入控制流来说明该过程。
如果应用的写入很大或跨越块边界,GFS客户端代码会将其分解为多个写入操作。它们都遵循上述控制流程,但可以与来自其他客户端的并发操作交错并覆盖。因此,共享文件区域可能最终包含来自不同客户端的片段,尽管副本将是相同的,因为各个操作在所有副本上以相同顺序成功完成。这使文件区域保持一致但未确定的状态,如第2.7节所述。
我们将数据流与控制流分离,以有效地利用网络。当控制从客户端流向主副本然后流向所有辅助副本时,数据将以流水线方式沿着精心挑选的chunkserver链线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟链路,并最大限度地减少推送所有数据的延迟。
为了充分利用每台机器的网络带宽,数据沿着一组chunkserver线性推送,而不是以一些其他拓扑方式分布(例如,树)。因此,每台机器的全部出包带宽用于尽可能快地传输数据,而不是划分给多个接收者。
为了尽可能地避免网络瓶颈和高延迟链路(例如,交换机间的链路通常两者都是),每台机器将数据转发到尚未接收到它的网络拓扑中的“最近”机器。假设客户端正在将数据推送到chunkserver S1至S4。它将数据发送到最近的chunkserver,即S1。S1将其转发通过最靠近S1的chunkserver S2到达S4,即S2。类似地,S2将其转发到S3或S4,即哪个更靠近S2,依此类推。我们的网络拓扑非常简单,可以从IP地址准确估算“距离”。
最后,我们通过在TCP连接上流水线地传输数据来最小化延迟。一旦chunkserver收到一些数据,它就会立即开始转发。流水线技术对我们特别有用,因为我们使用具有全双工链路的交换网络。立即转发数据不会降低接收速率。在没有网络拥塞的情况下,将B字节传输到R副本的理想消耗时间是B/T + RL,其中T是网络吞吐量,L是在两台机器之间传输字节的延迟。我们的网络链路通常为100 Mbps(T),并且L远低于1 ms。因此,理想情况下,1 MB可以在大约80 ms内完成分发。
GFS提供了一个名为record append
的原子追加操作。在传统的写入中,客户端指定要写入数据的偏移量。对同一区域的并发写入不可串行化:该区域最终可能包含来自多个客户端的数据片段。但是,在record append
中,客户端仅指定数据。GFS在其选择的偏移量处以原子方式(即,作为一个连续的字节序列)将其追加到文件至少一次,并将该偏移量返回给客户端。这类似于在多个写入者同时执行此操作时写入在Unix中以O_APPEND
模式打开的文件,从而没有竞争条件。
记录追加被我们的分布式应用程序大量使用,其中不同机器上的许多客户端同时追加到同一文件。如果他们使用传统写入,客户端需要额外的复杂且昂贵的同步,例如通过分布式锁管理器。在我们的工作负载中,此类文件通常用作多生产者/单个消费者队列,或包含来自许多不同客户端的合并结果。
记录追加是一种变化,并遵循3.1节中的控制流程,只在主副本有一点额外的逻辑。客户端将数据文件最后一个块推送到所有副本。然后,它将其请求发送到主副本。主副本检验看该记录添加是否会超过块的最大大小(64 MB)。如果不会,它会将块填充到最大大小,告诉辅助副本执行相同操作,并回复客户端,指示应在下一个块上重试该操作。(记录追加被限制为最大块大小的四分之一,以将最坏情况的碎片保持在可接受的水平。)如果记录符合最大大小(这是常见情况),则主副本将数据附加到其自身副本 ,告诉辅助副本将数据写在它的确切偏移处,最后向客户端回复成功。
如果任何副本上的记录追加失败,则客户端将重试该操作。结果,相同块的副本可能包含不同的数据,可能包括整个或部分相同的重复记录。GFS不保证所有副本都是字节相同的。它只保证数据至少作为原子单位写入一次。这个属性很容易从简单的观察中得出,对于报告成功的操作,数据必须在某些块的所有副本上以相同的偏移量写入。此外,在此之后,所有副本至少与记录结束一样长,因此即使不同的副本稍后成为主副本,任何未来记录也将被分配更高的偏移量或不同的块。就我们的一致性保证而言,成功的记录追加操作已写入数据的区域是确定的(因此是一致的),而中间区域是不一致的(因此未确定的)。正如我们在第2.7.2节中讨论的那样,我们的应用程序可以处理不一致的区域。
快照操作几乎立即生成文件或目录树(“源”)的副本,同时最小化对正在进行的变化的任何中断。我们的用户使用它来快速创建大型数据集的分支副本(通常是递归地复制这些副本),或者在尝试稍后可以提交或回滚的更改之前,建立当前状态的检查点。
与AFS [5]一样,我们使用标准的写时复制技术来实现快照。当master收到快照请求时,它首先撤消它即将快照的文件中的块上的任何未完成租约。这可确保对这些块的任何后续写入都需要与master进行交互才能找到租约持有者。这将使master最先有机会创建块的新副本。
租约被撤销或过期后,master将操作记录到磁盘。然后,它通过复制源文件或目录树的元数据将此日志记录应用于其内存中状态。新创建的快照文件指向与源文件相同的块。
客户端第一次想要在快照操作之后写入块C时,它向master发送请求以查找当前的租约持有者。master注意到块C的引用计数大于1。它推迟回复客户端请求,而是选择一个新的块句柄C’。然后它要求每个具有C的当前副本的chunkserver创建一个名为C’的新块。通过在与原始块相同的chunkserver上创建新块,我们确保可以在本地复制数据,而不是通过网络复制(我们的磁盘速度是我们100 Mb以太网链路的三倍)。从这一点来看,请求处理与任何块的请求处理没有什么不同:master在新块C’上授予其中一个副本租约并回复客户端,客户端可以正常写入该块,而不知道它刚刚被从现有的块创建。
master执行所有命名空间操作。此外,它还管理整个系统中的块副本:它进行放置决策、创建新块并因此产生副本、协调各种系统范围的活动以保持块复制完全、平衡所有chunkserver的负载,并回收未使用的存储。我们现在讨论这些主题。
许多master操作可能需要很长时间:例如,快照操作必须撤消快照覆盖的所有块上的chunkserver租约。我们不希望在运行时延迟其他master操作。因此,我们允许多个操作处于活跃状态,并在命名空间的区域上使用锁以确保正确的序列化。
与许多传统文件系统不同,GFS没有每个目录的数据结构来列出该目录中所有文件。它也不支持同一文件或目录的别名(即Unix术语中的硬链接或符号链接)。GFS逻辑上将其名称空间表示为将完整路径名映射到元数据的查找表。通过前缀压缩,此表可以在内存中有效地表示。命名空间树中的每个节点(绝对文件名或绝对目录名)都具有关联的读写锁。
每个master操作在运行之前获取一组锁。通常,如果涉及/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的数据结构可以防止修改。名称上的读锁足以保护父目录不被删除。
这种锁定方案的一个很好的特性是它允许同一目录中的并发改变。例如,可以在同一目录中同时执行多个文件创建:每个文件创建获取目录名称的读锁和文件名称的写锁。目录名称上的读锁足以阻止目录被删除、重命名或创建快照。序列化的文件名称的写锁尝试两次创建具有相同名称的文件。
由于命名空间可以有许多节点,因此读写锁对象会被延迟分配,并且一旦不使用就会被删除。此外,以一致的全局顺序获取锁以防止死锁:它们首先按命名空间树中的级别排序,然后按字典顺序排列在同一级别中。
GFS集群高度分布于多个层级上。它通常有数百个chunkserver分布在许多机架上。这些chunkserver反过来可以从相同或不同机架的数百个客户端访问。不同机架上的两台机器之间的通信可能跨越一个或多个网络交换机。另外,入包或出包机架的带宽可能小于机架内所有机器的带宽总和。多级分发对于分发数据以实现可伸缩性、可靠性和可用性提出了独特的挑战。
块副本放置策略有两个目标:最大化数据可靠性和可用性,并最大化网络带宽利用率。对于两者而言,仅在机器上传播副本是不够的,这些副本只能防止磁盘或机器故障并充分利用每台机器的网络带宽。我们还必须在机架上传播块副本。这可确保即使整个机架损坏或下线(例如,由于网络交换机或电源电路等共享资源的故障),块的某些副本仍然存活并保持可用。这也意味着一个块的流量,特别是读取,可以利用多个机架的总体带宽。但另一方面,写入流量必须流经多个机架,这是我们自愿做出的权衡。
创建块副本有三个原因:块创建,重新复制和重新均衡。
当master创建一个块时,它会选择放置最初为空的副本的位置。它考虑了几个因素。(1) 我们希望在具有低于平均磁盘空间利用率的chunkserver上放置新的副本。随着时间的推移,这将均衡chunkserver之间的磁盘利用率。(2) 我们想限制每个chunkserver上“最近”创建的数量。尽管创建本身低成本,但它可以可靠地预测即将来临的大量写入流量,因为在写入需要时会创建块,而在我们的一次追加多次读取工作量中,一旦完全写入它们,它们通常几乎是只读的。(3) 如上所述,我们希望在机架上分散块的副本。
一旦可用副本的数量低于用户指定的目标数,master就会重新复制一个数据块。这可能由于各种原因而发生:chunkserver变得不可用、它报告其副本可能已损坏、其中一个磁盘因错误而被禁用、或副本目标数增加。需要重新复制的每个块根据几个因素进行优先级排序。一个是它与副本目标数的差距。例如,我们为丢失了两个副本的块提供了比仅丢失一个副本的块更高的优先级。此外,我们更喜欢首先为活着的文件重新复制块,而不是最近删除的文件的块(参见第4.4节)。最后,为了最大限度地减少故障对运行应用程序的影响,我们提高了阻碍客户端进度的任何块的优先级。
master选择最高优先级的块并通过指示某个chunkserver直接从现有的有效副本复制块数据来“克隆”它。新副本的放置目标类似于创建时:均衡磁盘利用率,限制任何单个chunkserver上的活跃克隆操作,并在机架上分散副本。为了保持克隆流量免受压倒性客户端流量的影响,master限制了集群和每个chunkserver的活跃克隆操作数。此外,每个chunkserver通过限制其对源chunkserver的读取请求来限制它在每个克隆操作上花费的带宽量。
最后,master定期重新均衡副本:它检查当前副本分布并移动副本以获得更好的磁盘空间和负载均衡。同样通过这个过程,master逐渐填满一个新的chunkserver,而不是立即用新的块和随之而来的大量写入流量来淹没它。新副本的放置标准与上面讨论的类似。此外,master必须选择要删除的现有副本。通常,它更喜欢删除低于平均可用空间的chunkserver上的那些,以便均衡磁盘空间使用。
删除文件后,GFS不会立即回收可用的物理存储。它在文件和块级别的常规垃圾收集期间只是懒惰地这样做。 我们发现这种方法使系统更简单,更可靠。
当应用程序删除文件时,master会立即记录删除,就像其他更改一样。但是,不是立即回收资源,而是将文件重命名为包含删除时间戳的隐藏名称。在master定期扫描文件系统命名空间期间,如果它们已存在超过三天(间隔可配置),它将删除任何此类隐藏文件。在此之前,仍然可以使用新的特殊名称读取该文件,并且可以通过将其重命名为正常名称来取消删除。从命名空间中删除隐藏文件时,其内存元数据将被删除。这有效地切断了其所有块的链接。
在块命名空间的类似常规扫描中,master识别孤立的块(即,从任何文件不可到达的块)并擦除那些块的元数据。在与master定期交换的HeartBeat消息中,每个chunkserver报告它具有的块的子集,并且master回复其元数据中不再存在的所有块的标识。chunkserver可以自由删除这些块的副本。
虽然分布式垃圾收集是一个难题,需要在编程语言环境中使用复杂的解决方案,但在我们的情况中它非常简单。我们可以轻松识别对块的所有引用:它们位于由master专门维护的文件到块的映射中。我们还可以轻松识别所有块副本:它们是每个chunkserver上指定目录下的Linux文件。master不知道的任何这样的副本是“垃圾”。
存储回收的垃圾收集方法与立即删除相比具有多种优势。首先,它在组件故障很常见的大规模分布式系统中简单可靠。块创建可能会在某些chunkserver上成功,但在其他chunkserver上没有成功,从而master不知道存在遗留的副本。副本删除消息可能会丢失,并且master必须记住在故障时重新发送它们,包括它自己和chunkserver。垃圾收集提供了一种统一且可靠的方法来清理任何无用的副本。其次,它将存储回收合并到master的常规后台活动中,例如使用与chunkserver的定期扫描命名空间和握手。因此,它是分批完成的,成本是摊销的。而且,只有当master相对自由时才会这样做。master可以更迅速地响应需要及时关注客户的请求。第三,延迟回收存储提供了防止意外、不可逆删除的安全性。
根据我们的经验,主要的缺点是延迟有时会妨碍用户在存储紧张时优化使用的努力。重复创建和删除临时文件的应用可能无法立即重用存储。如果再次显式删除已删除的文件,我们可以通过加快存储回收来解决这些问题。我们还允许用户将不同的复制和回收策略应用于命名空间的不同部分。例如,用户可以指定某些目录树中存储的文件中的所有块不进行复制,并且任何已删除的文件都会立即且不可撤销地从文件系统状态中删除。
如果chunkserver失败并且在块丢失时错过块的变化,则块副本可能变得过时。对于每个块,master维护一个块版本号以区分最新和过时的副本。
每当master给块授予新租约时,它会增加块版本号并通知最新的副本。master和这些副本都将新版本号记录在持久状态。这发生在任何客户端被通知之前,因此在它可以开始写入块之前。如果另一个副本当前不可用,则其块版本号将不会更新。当chunkserver重新启动并报告其块集合及其关联的版本号时,master将检测到此chunkserver具有过时的副本。如果master看到的版本号大于其记录中的版本号,则master会在授予租约时假定它失败,因此将该更高版本更新为最新版本。
master在其常规垃圾回收中删除过时的副本。在此之前,当它回复客户端对块信息的请求时,它简单地认为陈旧的副本根本不存在。作为另一种安全措施,当master通知客户端哪个chunkserver在块上保持租约时,或者当它指示chunkserver在克隆操作中从另一个chunkserver读取块时,master会包含块版本号。客户端或chunkserver在执行操作时验证版本号,以便始终访问最新数据。
我们在设计系统时面临的最大挑战之一是处理频繁的组件故障。组件的质量和数量共同使这些问题成为常态而不是例外:我们不能完全信任机器,也不能完全信任磁盘。组件故障可能导致系统不可用,或者更糟糕的是,数据损坏。我们将讨论如何应对这些挑战以及我们在系统中构建的工具,以便在问题不可避免地发生时进行诊断。
在GFS集群中的数百台服务器中,一定有些服务器在任何给定时间不可用。我们通过两种简单而有效的策略保持整个系统的高可用性:快速恢复和复制。
无论master和chunkserver如何终止,它们都设计为恢复状态,并可在几秒钟内启动。事实上,我们不区分正常和异常终止; 通过终止进程来定期关闭服务器。客户端和其他服务器因未完成的超时请求,重新连接到重新启动的服务器并重试,会遇到轻微的间断。 第6.2.2节展示了观察到的启动时间。
如前所述,每个块都复制在不同机架上的多个chunkserver上。用户可以为文件命名空间的不同部分指定不同的复制级别。默认值为3。master根据需要克隆现有副本,以便在chunkserver下线或通过校验和检测损坏的副本时保持每个块完全复制(参见第5.2节)。尽管复制对我们有利,但我们正在探索其他形式的跨服务器冗余,例如奇偶校验或纠错码,以满足我们不断增加的只读存储要求。我们期望在我们非常松散耦合的系统中实现这些更复杂的冗余方案是具有挑战性但可管理的,因为我们的流量主要是追加和读取而不是小的随机写入。
复制master状态以确保可靠性。其操作日志和检查点在多台计算机上复制。只有在其日志记录在本地和所有master副本上刷新到磁盘后,才会认为状态改变已提交。为简单起见,一个master进程仍然负责所有改变以及后台活动,例如更改系统内部的垃圾收集。当它失败时,几乎可以立即重启。如果其计算机或磁盘发生故障,GFS外部的监视设施将在其他位置使用复制的操作日志启动新的master进程。客户端仅使用master的规范名称(例如gfs-test),这是一个DNS别名,如果master重定位到另一台计算机,则可以自动切换。
此外,即使主master掉线,“影子”master也提供对文件系统的只读访问权限。它们是阴影,而不是镜像,因为它们可能略微滞后于主体,通常是几分之一秒。它们增强了未被主动改变的文件的读取可用性或者不介意获得稍微陈旧结果的应用程序。实际上,由于从chunkserver读取文件内容,因此应用程序不会获得过时的文件内容。短时间内可能过时的是文件元数据,如目录内容或访问控制信息。
为了使自己了解情况,影子master会读取不断增长的操作日志的副本,并如主体一样将完全相同的更改序列应用于其数据结构。与主master一样,它会在启动时轮询chunkserver(此后很少),以查找块副本并与它们交换频繁的握手消息以监控其状态。这取决于主master只用于主副本创建和删除副本的决策所导致的副本位置更新。
每个chunkserver使用校验和来检测存储数据的损坏。鉴于GFS集群通常在数百台计算机上拥有数千个磁盘,因此它经常会遇到导致数据损坏或读取和写入路径丢失的磁盘故障。(有关第一个原因,请参阅第7节。)我们可以使用其他块副本从损坏中恢复,但通过比较chunkserver上的副本来检测损坏是不切实际的。此外,不同的副本可能是合法的:GFS变化的语义,特别是如前所述的原子记录追加,并不保证相同的副本。因此,每个chunkserver必须通过维护校验和来独立地验证其自身副本的完整性。
数据块被划分为64 KB大小的块。每个都有一个相应的32位校验和。与其他元数据一样,校验和保存在内存中并持久存储其日志记录,与用户数据分开。
对于读取,在将任何数据返回给请求者(无论是客户端还是其他chunkserver)之前,chunkserver会验证与读取范围重叠的数据块的校验和。因此,chunkserver不会将损坏扩散到其他计算机。如果块与记录的校验和不匹配,则chunkserver向请求者返回错误并向master报告不匹配。作为响应,请求者将从其他副本读取,而master将从另一个副本克隆该块。在有效的新副本到位后,master会指示报告不匹配的chunkserver删除其副本。
由于多种原因,校验和对读取性能几乎没有影响。由于我们的大多数读取至少跨越几个块,因此我们只需要读取和校验相对少量的额外数据以进行验证。GFS客户端代码通过尝试在校验和块边界处对齐读取来进一步减少此开销。此外,在没有任何I/O的情况下,在chunkserver上进行校验和查找及比较,并且校验和计算通常可以与I/O重叠。
校验和计算针对附加到块末尾的写入进行了大量优化(与覆盖现有数据的写入相反),因为它们在我们的工作负载中占主导地位。我们只是增量更新最后一个部分校验和块的校验和,并为附加填充的任何全新校验和块计算新校验和。即使最后部分的校验和块已经损坏并且我们现在无法检测到它,新的校验和值也不会与存储的数据匹配,并且当块下次读取时将像往常一样检测到损坏。
相反,如果写入覆盖了块的现有范围,我们必须读取并验证被覆盖的范围的第一个和最后一个块,然后执行写入,最后计算并记录新的校验和。如果我们在部分覆盖它们之前不验证第一个和最后一个块,则新的校验和可能会隐藏未被覆盖的区域中存在的损坏。
在空闲期间,chunkserver可以扫描并验证非活跃块的内容。这允许我们检测很少读取的块中的损坏。检测到损坏后,master可以创建新的未损坏的副本并删除损坏的副本。这可以防止一个不活跃但损坏的块副本欺骗master认为它有足够的有效的块副本。
广泛而详细的诊断日志记录在问题隔离、调试和性能分析方面提供了无可估量的帮助,同时只产生了最低成本。没有日志,很难了解机器之间的瞬态和不可重现的交互。GFS服务器生成诊断日志,记录许多重要事件(例如chunkserver上下线)以及所有RPC请求和回复。可以自由删除这些诊断日志,而不会影响系统的正确性。但是,在空间允许的情况下,我们会尽量保留这些日志。
RPC日志包括在线上发送的确切请求和响应,但正在读取或写入的文件数据除外。通过将请求与回复相匹配并在不同的计算机上对照整理RPC记录,我们可以重建整个交互历史记录来诊断问题。日志还可用作跟踪负载测试和性能分析。
日志记录的性能影响很小(远远超过好处),因为这些日志是按顺序和异步写入的。最近的事件也保存在内存中,可用于连续在线监控。
在本节中,我们提供了一些微基准测试来说明GFS架构和实现中固有的瓶颈,以及Google中使用的真实集群中的一些数据。
我们在GFS集群上测量了性能,该集群由一个主master,两个master副本,16个chunkserver和16个客户端组成。请注意,此配置已设置为易于测试。典型的集群有数百个chunkserver和数百个客户端。
所有机器都配置了双1.4 GHz PIII处理器,2 GB内存,两个80 GB 5400 rpm磁盘,以及用HP 2524交换机的100 Mbps全双工以太网连接。所有19台GFS服务器机器都连接到一台交换机,所有16台客户机都连接到另一台交换机。两个交换机通过1 Gbps链路连接。
N个客户端同时从文件系统读取。每个客户端从320 GB文件集中读取随机选择的4 MB区域。这重复256次,以便每个客户端最终读取1 GB的数据。这些chunkserver只有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%,因为随着读取者数量的增加,多个读取者同时从同一个chunkserver读取的概率也会增加。
N个客户端同时写入N个不同的文件。每个客户端通过一系列1 MB写入将1 GB数据写入新文件。总写入速率及其理论极限如图3(b)所示。限制上限为67 MB/s,因为我们需要将每个字节写入16个chunkserver中的3个,每个服务器具有12.5 MB/s的写入速率。
一个客户端的写入速率为6.3 MB/s,约为限制的一半。对此的主要罪魁祸首是我们的网络ack。它与我们用于将数据推送到块副本的流水线方案不能很好地交互。将数据从一个副本传输到另一个副本的延迟会降低整体写入速率。
16个客户端的总体写入速率达到35 MB/s(或每个客户端2.2 MB/s),大约是理论上限的一半。与读取的情况一样,当客户端数量增加时,多个客户端更可能同时写入同一个chunkserver。此外,16个写入者比16个读取者更容易发生碰撞,因为每次写入涉及三个不同的副本。
写入比我们想的要慢。 实际上,这不是主要问题,因为即使它增加了各个客户端所看到的延迟,也不会显著影响系统向大量客户端提供的总体写入带宽。
图3©显示了记录追加性能。N个客户端同时追加到单个文件。性能受到存储文件最后一块的chunkserver的网络带宽的限制,与客户端的数量无关。一个客户端时的启动速率为6.0 MB/s,16个客户端时的速率降至4.8 MB/s,主要是由于不同客户端看到网络传输速率的拥塞和差异。
我们的应用倾向于同时生成多个这样的文件。换句话说,N个客户端同时附加到M个共享文件,其中N和M都是几十个或几百个。因此,我们实验中的chunkserver网络拥塞在实践中不是一个重要问题,因为客户端可以在chunkserver忙于另一个文件时同时推进这一个文件。
我们现在检验Google中使用的两个集群,这些集群对于其他几个类似集群具有代表性。集群A经常被100多名工程师用于研究和开发。典型的任务由人类用户发起并运行长达数小时。它读取几MB到几TB的数据,转换或分析数据,并将结果写回集群。集群B主要用于线上数据处理。这些任务持续时间更长,并且只需偶尔进行人为干预即可持续生成和处理多TB数据集。在这两种情况下,单个“任务”由许多机器上的许多进程组成,这些进程同时读取和写入许多文件。
如表中的前五个条目所示,两个集群都有数百个chunkserver,支持许多TB的磁盘空间,并且只占用了相当一部分但不完全满。“已用空间”包括所有块副本。几乎所有文件都被复制三次。因此,集群分别存储18TB和52TB的文件数据。
这两个集群具有相似数量的文件,但B具有较大比例的死文件,即被新版本删除或替换但其存储尚未被回收的文件。它还有更多的块,因为它的文件往往更大。
chunkserver总共存储数十GB的元数据,主要是64 KB用户数据块的校验和。保留在chunkserver上的唯一其他元数据是第4.5节中讨论的块版本号。
保存在master上的元数据要小得多,只有几十MB,或者平均每个文件大约100个字节。这与我们的假设一致,即在实践中master内存的大小不会限制系统的容量。大多数的每文件元数据是以前缀压缩形式存储的文件名。其他元数据包括文件所有权和权限,从文件到块的映射以及每个块的当前版本。此外,对于每个块,我们存储当前副本位置和用于实现写时复制的引用计数。
每个服务器(包括chunkserver和master)只有50到100 MB的元数据。因此恢复速度很快:在服务器能够应答查询之前,从磁盘读取此元数据只需几秒钟。但是,master在一段时间内(通常为30到60秒)有些缓慢,直到它从所有chunkserver获取了块位置信息。
表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。
表3还显示发送给master的操作速率约为每秒200到500次操作。master可以轻松跟上此速率,因此不会成为这些工作负载的瓶颈。
在早期版本的GFS中,master偶尔会成为某些工作负载的瓶颈。它把大部分时间花在按顺序扫描大型目录(包含数十万个文件)来查找特定文件。我们已经改变了master数据结构,以允许通过命名空间进行有效的二分查找。它现在可以轻松支持每秒数千次的文件访问。如有必要,我们可以通过在名称空间数据结构前面放置名称查找缓存来进一步加快速度。
在chunkserver失败后,一些块将变得副本不足,必须克隆以恢复其副本级别。恢复所有这些块所需的时间取决于资源量。在一个实验中,我们在集群B中杀死了一个chunkserver。该chunkserver有大约15000个包含600 GB数据的块。为了限制对运行应用的影响并为调度决策提供余地,我们将此集群的默认参数限制为91个并发复制(占chunkserver数量的40%),其中每个克隆操作最多允许消耗6.25 MB/s(50 Mbps)。所有块在23.2分钟内恢复,有效复制速率为440 MB/s。
在另一个实验中,我们杀死了两个chunkserver,每个服务器大约有16000个块和660 GB的数据。这种双重故障减少了266个块,使其只有一个副本。这266个块以更高的优先级克隆,并且在2分钟内全部恢复到至少2x副本,从而使集群处于可以容忍另一个chunkserver故障而没有数据丢失的状态。
在本节中,我们将详细介绍两个GFS集群的工作负载,与第6.2节中的工作负载相当但不完全相同。 集群X用于研究和开发,而集群Y用于线上数据处理。
这些结果仅包括客户端发起的请求,以便它们反映我们的应用程序为整个文件系统产生的工作负载。它们不包括执行客户端请求或内部后台活动的服务器间请求,例如转发写入或重新均衡。
I/O操作的统计信息基于从GFS服务器记录的实际RPC请求中启发式重建的信息。例如,GFS客户端代码可能会将单个读取拆分为多个RPC以增加并行性,我们从中推断出原始读取。由于我们的访问模式是高度程序化的,我们预计任何错误都会出现在噪音中。应用程序的显式记录可能提供了稍微更准确的数据,但从逻辑上讲,重新编译和重新启动数千个正在运行的客户端是不可能的,并且很难从多台计算机中收集结果。
应该注意不要过度概括我们的工作负载。由于Google完全控制GFS及其应用,因此应用倾向于针对GFS进行调整,同样地,GFS专为这些应用而设计。一般应用和文件系统之间也可能存在这种相互影响,但在我们的情况中,这种影响可能更为明显。
表4按大小显示了操作的分布。读取大小呈现双峰分布。小读取(低于64 KB)来自搜索密集型客户端,这些客户端在大型文件中查找小块数据。 大读取(超过512 KB)来自对整个文件的长连续读取。
大量的读取在集群Y中根本不返回任何数据。我们的应用,特别是生产系统中的应用,经常使用文件作为生产者 - 消费者队列。当消费者读取文件结尾时,生产者会同时附加到文件。有时,当消费者超过生产者时,不会返回任何数据。集群X显示这种情况的频率较低,因为它通常用于短期数据分析任务,而不是长期分布式应用程序。
写入大小也呈现双峰分布。大写(超过256 KB)通常是由写入者的大缓存引起的。写入者可以缓存较少的数据,检查点或同步更常见,或者只是为较小的写入生成较少的数据(64 KB以下)。
至于记录追加,集群Y看到的大记录追加比例比集群X高得多,因为我们生产系统使用集群Y,并对GFS进行了更积极的调整。
表5显示了在各种规模的操作中传输的数据总量。对于所有类型的操作,较大的操作(超过256 KB)通常占转移字节的大多数。由于随机搜索工作负载,小读取(低于64 KB)确实传输了一小部分但很重要的读取数据。
记录追加在我们的生产系统中被大量使用。对于集群X,写入与记录追加的比率按传输的字节数为108:1,按操作计数为8:1。对于生产系统使用的集群Y,比率分别为3.7:1和2.5:1。此外,这些比率表明,对于两个群集,记录追加量往往大于写入量。但是,对于集群X,在测量期间追加的记录的总体使用率相当低,因此结果可能会被具有特定缓冲区大小选择的一个或两个应用所倾斜。
正如预期的那样,我们的数据变化工作负载主要是追加而不是覆盖。我们测试了主副本上覆盖的数据量。这大部分是客户故意覆盖先前写入的数据而不是追加新数据的情况。对于集群X,覆盖占变化字节的0.0001%以下和变化操作的0.0003%以下。对于集群Y,比率均为0.05%。虽然这是微不足道的,但仍然高于我们的预期。事实证明,大多数这些覆盖都是由于错误或超时而导致客户端重试。它们不是工作量本身的一部分,而是重试机制的结果。
表6显示了按master的请求类型细分。表6显示了按master的请求类型细分。大多数请求是为读取询问块位置(FindLocation)和为数据变化获取租约持有者信息(FindLease-Locker)。
集群X和Y看到显著不同数量的删除请求,因为集群Y存储定期重新生成并替换为较新版本的生产数据集。这些差异中的一些进一步隐藏在Open请求的差异中,因为可以通过从头开始写入(Unix开放术语中的模式“w”)来隐式删除旧版本的文件。
FindMatchingFiles是一种模式匹配请求,支持“ls”和类似的文件系统操作。与master的其他请求不同,它可能要处理命名空间的大部分,因此成本可能很高。集群Y更经常地看到它,因为自动数据处理任务倾向于检查文件系统的各个部分以了解全局应用状态。相比之下,集群X的应用受到更明确的用户控制,并且通常事先知道所有所需文件的名称。
在构建和部署GFS的过程中,我们遇到了各种问题,一些是操作问题,一些是技术问题。
最初,GFS被认为是我们生产系统的后端文件系统。随着时间的推移,使用范围逐渐扩大到包括研究和开发任务。它开始时很少支持权限和配额之类的东西,但现在包括这些的初级形式。虽然生产系统受到良好的规范和控制,但用户有时却没有。需要更多的基础设施来防止用户互相干扰。
我们最大的一些问题是磁盘和Linux相关。我们的许多磁盘都向Linux驱动程序声称它们支持一系列IDE协议版本,但实际上只对较新的版本可靠地响应。由于协议版本非常相似,因此这些驱动器大多数情况都可以工作,但偶尔会出现的不匹配会导致驱动器和内核不匹配驱动器的状态。由于内核中的问题,这会无声地破坏数据。这个问题促使我们使用校验和来检测数据损坏,同时我们修改内核来处理这些协议不匹配。
早些时候,由于fsync()的成本,我们遇到了Linux 2.2内核的一些问题。其成本与文件大小成比例,而不是修改部分的大小。这对我们的大型操作日志来说是一个问题,特别是在我们实现检查点之前。我们通过使用同步写入并最终迁移到Linux 2.4来解决这个问题。
另一个Linux问题是单个读写锁,地址空间中的任何线程在从磁盘读取(读锁)时都必须保持,或者修改mmap()调用中的地址空间(写入锁)。我们在低负载下看到了系统中的瞬态超时,并且很难发现资源瓶颈或零星的硬件故障。最终,我们发现这个单一锁定阻止主网络线程将新数据映射到内存,而磁盘线程在先前映射的数据中进行分页。由于我们主要受网络接口而不是内存复制带宽的限制,我们通过以额外拷贝为代价用pread()替换mmap()来解决这个问题。
尽管偶尔出现问题,Linux代码的可用性已经帮助我们一次又一次地探索和理解系统行为。在适当的时候,我们改进内核并与开源社区共享更改。
与其他大型分布式文件系统(如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],移除了集中式服务器,并依靠分布式算法实现一致性和管理。我们选择集中式方法,以简化设计,提高可靠性并获得灵活性。特别是,集中式master可以更轻松地实现复杂的块放置和复制策略,因为master已经拥有大部分相关信息并控制其更改方式。我们通过保持master状态较小并在其他计算机上完全复制来解决容错问题。目前,我们的影子master机制提供了可伸缩性和高可用性(用于读取)。通过附加预写日志使master状态的更新持久化。因此,我们可以调整像Harp[7]中的主副本方案来提供高可用性,并提供比我们当前方案更强的一致性保证。
我们正在解决类似于Lustre[8]的问题,即为大量客户端提供聚合性能。但是,我们通过关注应用的需求而不是构建符合POSIX的文件系统来大大简化了问题。此外,GFS假设大量不可靠的组件,因此容错是我们设计的核心。
GFS最接近NASD架构[4]。虽然NASD架构基于网络连接的磁盘驱动器,但GFS使用商用机器作为chunkserver,就像在NASD原型中所做的那样。与NASD的工作不同,我们的chunkserver使用延迟分配固定大小的块而不是可变长度的对象。此外,GFS还实现了生产环境中所需的重新均衡、复制和恢复等功能。
与明尼苏达州的GFS和NASD不同,我们不寻求改变存储设备的型号。我们专注于解决使用现有商业组件的复杂分布式系统的日常数据处理需求。
由原子记录追加启用的生产者 - 消费者队列解决了与River[2]中的分布式队列类似的问题。虽然River使用跨机器分布的基于内存的队列和谨慎的数据流控制,但GFS使用可以由许多生产者同时追加的持久化文件。River模型支持m到n的分布式队列,但缺乏持久存储带来的容错能力,而GFS只支持有效的m对1队列。多个使用者可以读取相同的文件,但是他们必须协调划分输入的负载。
Google文件系统展示了支持在商用硬件上的大规模数据处理工作负载所必需的特质。虽然一些设计决策是针对我们独特设置的,但许多可能适用于具有相似大小和成本意识的数据处理任务。
我们首先根据当前和预期的应用工作负载和技术环境重新审视传统的文件系统假设。我们的观察结果在设计领域产生了截然不同的观点。我们将组件故障视为常态而非异常,对大文件的优化大多数是附加(可能同时)然后读取(通常是顺序),并且扩展和放宽标准文件系统接口以改进整个系统。
我们的系统通过持续监控,复制关键数据以及快速自动恢复来提供容错功能。块复制允许我们容忍chunkserver故障。这些故障的频率激发了一种新颖的在线修复机制,该机制定期和透明地修复损坏并尽快补偿丢失的副本。此外,我们使用校验和来检测磁盘或IDE子系统级别的数据损坏,考虑到系统中的磁盘数量,这种情况变得非常普遍。
我们的设计为执行各种任务的许多并发读和写提供了总体高很吞吐量。 我们通过将master的文件系统控制与数据传输分开来实现这一点,数据传输直接在chunkserver和客户端之间传递。通过大的块大小和块租约将master参与的常见操作最小化,块租约将权限委托给数据变化中的主副本。这使得一个简单的集中式master成为可能,而不会成为瓶颈。我们相信,我们对网络改进将解除当前对单个客户端所看到的写入吞吐量的限制。
GFS已成功满足我们的存储需求,并在Google中广泛用作研发和生产数据处理的存储平台。它是一个重要的工具,使我们能够继续创新和解决整个网络的问题。
我们要感谢以下人员对系统或论文的贡献。Brain Bershad(我们的上级)和匿名审稿人给了我们宝贵的意见和建议。Anurag Acharya,Jeff Dean和David des-Jardins为早期设计做出了贡献。Fay Chang致力于比较跨chunkserver的副本。Guy Edjlali致力于存储配额。Markus Gutschke致力于测试框架和安全强化功能。David Kramer致力于提升性能。Fay Chang,Urs Hoelzle,Max Ibel,Sharon Perl,Rob Pike和Debby Wallach品评了该论文的早期草稿。Google的许多同事都勇敢地将他们的数据信任地迁移一个新的文件系统,并给了我们有用的反馈。Yoshka帮助进行了早期测试。