前言:
谷歌的“大数据三驾马车”——Google文件系统(GFS)、MapReduce、和BigTable。这三篇论文不仅是谷歌处理海量信息的基石,也为全球的大数据技术发展设定了标杆。
从GFS的创新存储解决方案到MapReduce的高效数据处理模型,再到BigTable的强大查询性能,每一篇论文都展现出了其独特的技术优势和业务价值。本系列将尽可能的用翻译出论文的“原汁原味”降低阅读成本,供大家学习探讨。
读者将通过这些论文获得对大数据基础设施关键组件的全面理解,洞察这些技术如何共同协作,支持复杂的数据分析任务,并推动了数据驱动决策的新时代。无论您是数据科学家、软件工程师还是技术策略家,论文内容都将为您提供宝贵的知识和启发,帮助您在大数据的浪潮中乘风破浪。
The Google File System
作者:Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung
翻译:曦松
摘要
我们设计并实现了 Google 文件系统,这是一种可扩展的分布式文件系统,适用于大型分布式数据密集型应用程序。它在运行于廉价的普通硬件上时提供容错能力,并为大量客户端提供高聚合性能。
虽然与以前的分布式文件系统研究有很多相似之处,但我们的设计是通过观察现在的和过去的应用工作负载和技术环境,反映出与一些早期文件系统假设的明显不同。这促使我们重新审视传统选择,并探索截然不同的设计思路。
该文件系统已成功满足我们的存储需求。它作为生成和处理我们服务所用数据的存储平台,在 Google 内部被广泛部署,同时也支持需要大数据集的研究和开发工作。迄今为止,最大的集群提供了跨越数千个磁盘和超过一千台机器的数百TB存储,并且被数百个客户端同时访问。
在本文中,我们介绍了旨在支持分布式应用程序的文件系统接口扩展,讨论了设计的许多方面,并报告了来自微基准测试和现实世界使用的测量结果。
**关键词:**容错、可扩展性、数据存储、集群存储
1.引言
我们设计并实现了 Google 文件系统 (GFS),以满足 Google 日益增长的数据处理需求。GFS 保持着许多与以往分布式文件系统相同的目标,例如性能、可扩展性、可靠性和可用性。然而,该设计受到我们对当前和预期应用工作负载及技术环境的关键审视的影响,表现出与早期文件系统设计假设的显著不同。我们重新审视了传统选择,并在设计空间中探索了截然不同的方向。
**首先,组件故障是常态而非例外。**文件系统由数百甚至数千台由廉价普通部件构成的存储机器组成,并且被相当数量的客户端机器访问。由于组件的数量和质量原因,几乎在任何给定时间都有部分组件无法正常工作,有些甚至无法从当前故障中恢复。我们遇到了由应用程序错误、操作系统错误、人为错误以及磁盘、内存、连接器、网络和电源故障引起的问题。因此,持续监控、错误检测、故障容错和自动恢复必须是系统的核心组成部分。
**其次,我们的文件大小按传统标准来看是巨大的。**多GB文件很常见。每个文件通常包含许多应用对象,例如网页文档。当我们定期处理包含数十亿个对象的快速增长的数据集时,管理数十亿个大约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 集群由一个Master节点和多个块服务器组成,并由多个客户端访问,如图1所示。每个组件通常是运行用户级服务器进程的普通 Linux 机器。只要机器资源允许,并且能够接受由于运行可能不稳定的应用程序代码而导致的较低可靠性,在同一台机器上同时运行块服务器和客户端是很容易的。
图1 GFS架构
文件被划分为固定大小的块。每个块由Master节点在创建块时分配的不可变且全局唯一的64位块标识。块服务器将块存储在本地磁盘上作为 Linux 文件,并根据块标识和字节范围读写块数据。为了保证可靠性,每个块在多个块服务器上复制。默认情况下,我们存储三个副本,但用户可以为文件命名空间的不同区域指定不同的复制级别。
Master节点维护所有文件系统元数据。这包括命名空间、访问控制信息、文件到块的映射以及块的当前位置。它还控制系统范围的活动,如块租赁管理、孤立块的垃圾收集和块服务器之间的块迁移。Master节点定期通过心跳消息与每个块服务器通信,向其发出指令并收集其状态。
嵌入每个应用程序的 GFS 客户端代码实现文件系统 API,并与Master节点和块服务器通信,代表应用程序读写数据。客户端与Master节点交互进行元数据操作,但所有承载数据的通信直接与块服务器进行。我们不提供 POSIX API,因此不需要挂接到 Linux vnode 层。
客户端和块服务器都不缓存文件数据。客户端缓存几乎没有好处,因为大多数应用程序通过大文件流式传输或工作集太大而无法缓存。不使用缓存通过消除缓存一致性问题简化了客户端和整个系统。(不过,客户端确实缓存元数据。)块服务器无需缓存文件数据,因为块作为本地文件存储,因此 Linux 的缓冲区缓存已经将频繁访问的数据保存在内存中。
2.4 单一Master节点
单一的Master节点极大地简化了我们的设计,并使Master节点能够利用全局信息做出复杂的块位置分配和复制决策。然而,我们必须尽量减少其在读写操作中的参与,以避免其成为瓶颈。客户端从不通过Master节点读写文件数据。相反,客户端会询问Master节点应联系哪些块服务器,并在有限时间内缓存此信息,以便在随后的许多操作中直接与块服务器交互。
让我们参照图1解释一个简单读取操作的交互过程。首先,客户端使用固定的块大小将应用程序指定的文件名和字节偏移量转换为文件内的块索引。然后,它向Master节点发送包含文件名和块索引的请求。Master节点回复相应的块标识和副本位置。客户端使用文件名和块索引作为键缓存此信息。接着,客户端向其中一个副本发送请求,通常是最接近的副本。请求指定块标识和该块内的字节范围。相同块的后续读取不需要再与Master节点交互,直到缓存的信息过期或文件重新打开。实际上,客户端通常在同一请求中询问多个块,Master节点还可以包含紧随所请求块之后的块信息。这些额外信息几乎不增加成本,避免了未来的多次客户端-Master节点交互。
2.5 块大小
块大小是关键设计参数之一。我们将文件数据块的大小分为64 MB,这比典型文件系统的块要大得多。每个块副本作为普通 Linux 文件存储在块服务器上,并仅在需要时扩展。懒惰式空间分配方法避免了由于内部碎片化导致的空间浪费,而内部碎片则可能会与数据块不兼容,成为运行的最大障碍。
选择较大的块带来了几个重要优势。首先,它减少了客户端与Master节点交互的需求,因为同一块上的读写只需要向Master节点发送一次初始请求以获取块位置信息。对于我们的工作负载,这种交互的减少尤其显著,因为应用程序主要顺序读取和写入大文件。即使是小范围随机读取,客户端也可以轻松缓存多TB工作集的所有块位置信息。其次,由于客户端更可能在给定块上执行多次操作,它可以通过在长时间内保持与块服务器的持久 TCP 连接来减少网络开销。第三,它减少了存储在Master节点上的元数据大小。这使我们能够将元数据保存在内存中,从而带来其他优势,我们将在第2.6.1节中讨论。
另一方面,即使采用懒惰空间分配,较大的块也有其缺点。小文件由少量块组成,可能只有一个块。如果许多客户端同时访问同一文件,存储这些块的块服务器可能成为热点。实际上,热点并不是主要问题,因为我们的应用程序大多顺序读取多块文件。
然而,当 GFS 首次被批处理队列系统使用时确实出现了热点问题:一个可执行文件作为单块文件写入 GFS,然后在数百台机器上同时启动。存储这个可执行文件的少数块服务器被数百个同时请求过载。我们通过以更高的复制因子存储此类可执行文件并使批处理队列系统错开应用程序启动时间来解决这个问题。一个潜在的长期解决方案是在这种情况下允许客户端从其他客户端读取数据。
2.6 元数据
Master节点存储三种主要类型的元数据:文件和块命名空间、文件到块的映射,以及每个块副本的位置。所有元数据都保存在Master节点的内存中。前两种类型(命名空间和文件到块的映射)通过将变更记录到存储在Master节点本地磁盘并复制到远程机器的操作日志中来保持持久性。使用日志可以让我们简单、可靠地更新Master节点状态,并在Master节点崩溃时避免不一致性。Master节点不会持久化存储块位置信息,而是在Master节点启动时和每当块服务器加入集群时,询问每个块服务器其所拥有的块信息。
2.6.1 内存数据结构
由于元数据存储在内存中,Master节点操作速度很快。此外,Master节点可以轻松高效地在后台定期扫描其整个状态。这种定期扫描用于实现块垃圾回收、块服务器故障时的再复制以及块迁移以平衡块服务器之间的负载和磁盘空间使用情况。第4.3节和第4.4节将进一步讨论这些活动。
这种仅限内存的方法的一个潜在问题是,块的数量以及整个系统的容量受到Master节点内存大小的限制。这在实践中并不是一个严重的限制。Master节点为每个64 MB的块维护少于64字节的元数据。大多数块都是满的,因为大多数文件包含许多块,只有最后一个块可能部分填充。同样,文件命名空间数据通常每个文件需要少于64字节,因为它使用前缀压缩来紧凑地存储文件名。
如果有必要支持更大的文件系统,增加额外内存的成本是很小的代价,因为我们通过在内存中存储元数据获得了简洁性、可靠性、性能和灵活性。
2.6.2 块位置
Master节点不保持给定块副本的块服务器的持久记录。它只在启动时向块服务器请求这些信息。此后,Master节点可以保持最新状态,因为它控制所有块的放置并通过定期的心跳消息监控块服务器状态。
我们最初尝试在Master节点上持久存储块位置信息,但最终决定在启动时向块服务器请求数据更为简单,并定期重复请求。这消除了在块服务器加入和离开集群、改变名称、失败、重启等情况下保持Master节点和块服务器同步的问题。在拥有数百台服务器的集群中,这些事件频繁发生。
理解这一设计决策的另一种方式是认识到块服务器对其自身磁盘上拥有的块具有最终决定权。尝试在Master节点上维护这些信息的一致视图毫无意义,因为块服务器上的错误可能导致块自行消失(例如,磁盘可能损坏并被禁用),或者操作员可能会重命名块服务器。
2.6.3 操作日志
操作日志包含关键元数据更改的历史记录。它是 GFS 的核心。不仅是元数据的唯一持久记录,还作为定义并发操作顺序的逻辑时间线。文件和块以及它们的版本(参见第4.5节)都由它们创建时的逻辑时间唯一且永久地标识。
由于操作日志至关重要,我们必须可靠地存储它,并且在元数据更改变为持久之前不向客户端显示更改。否则,虽然块还存在,我们却可能丢失整个文件系统或者最近的客户端操作。因此,我们在多个远程机器上复制它,并且只有在将相应的日志记录刷新到本地和远程磁盘后才响应客户端操作。Master节点在刷新之前将多个日志记录一起批处理,从而减少刷新和复制对整个系统吞吐量的影响。
Master节点通过重放操作日志来恢复其文件系统状态。为了最小化启动时间,我们必须保持日志的小规模。当日志增长到一定大小时,Master节点会检查其状态,以便通过从本地磁盘加载最新检查点并仅重放之后的有限数量的日志记录来恢复。检查点以紧凑的 B-tree 形式存储,可以直接映射到内存中并用于命名空间查找,无需额外解析。这进一步加快了恢复速度并提高了可用性。
由于构建检查点可能需要一段时间,Master节点的内部状态结构化为可以在不延迟传入变更的情况下创建新检查点。Master节点切换到一个新的日志文件,并在一个单独的线程中创建新的检查点。新的检查点包括切换前的所有变更。对于一个拥有几百万个文件的集群来说,创建检查点大约需要一分钟左右。完成后,它会写入本地和远程磁盘。恢复只需要最新的完整检查点和随后的日志文件。尽管我们保留一些旧的检查点和日志文件以防灾难发生,但可以自由删除旧的检查点和日志文件。检查点期间的失败不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。
2.7 一致性模型
GFS 具有一个宽松的一致性模型,它很好地支持我们高度分布式的应用程序,同时实现起来相对简单和高效。我们现在讨论GFS如何保障一致性,以及这对于应用程序的意义。我们强调GFS如何管理这些保障,但是实现细节将在论文的后续部分进行讨论。
2.7.1 GFS 的保证
文件命名空间的变更(如文件创建)是原子的。它们完全由Master节点处理:命名空间锁定保证原子性和正确性(参见第4.1节);Master节点的操作日志定义了这些操作的全局总顺序(参见第2.6.3节)。
表1 变更后的文件区域状态
数据变更后,文件区域的状态取决于变更的类型、是否成功以及是否存在并发变更。表1总结了结果。如果所有客户端始终看到相同的数据,无论它们从哪个副本读取,则文件区域是一致的。如果文件数据变更后区域是一致的,并且客户端会看到变更写入的完整内容,则该区域被定义。当变更在没有并发写入者干扰的情况下成功时,受影响的区域是已定义的(并且隐含一致的):所有客户端始终会看到变更写入的内容。并发成功的变更会使区域未定义但一致:所有客户端看到相同的数据,但这些数据可能不反映任何一个变更写入的内容。通常,它由多个变更的混合片段组成。变更失败会使区域不一致(因此也未定义):不同客户端在不同时间可能会看到不同的数据。我们将在下面描述我们的应用程序如何区分已定义区域和未定义区域。应用程序无需进一步区分不同类型的未定义区域。
数据变更可以是写入或记录追加。写入导致数据写入到应用程序指定的文件偏移量。记录追加导致数据(“记录”)至少一次原子性地追加,即使在存在并发变更的情况下也是如此,但在 GFS 选择的偏移量处(参见第3.3节)。相比之下,“常规”追加只是写入客户端认为是文件当前末尾的偏移量。偏移量返回给客户端,并标记包含记录的已定义区域的开始。此外,GFS 可能会在其间插入填充或记录副本。它们占据被认为不一致的区域,通常与用户数据量相比微不足道。
在一系列成功的变更后,变更的文件区域保证是已定义的,并包含最后一次变更写入的数据。GFS 通过以下方式实现这一点:(a)对所有副本按相同顺序应用变更(参见第3.1节),以及(b)使用块版本号检测在块服务器宕机期间错过变更的副本(参见第4.5节)。过时的副本不会参与变更,也不会提供给向Master节点请求块位置的客户端。它们会在第一时间被垃圾回收。
由于客户端缓存块位置,它们可能在刷新信息之前从过时的副本读取。这一窗口由缓存条目的超时和下一次文件打开时清除该文件的所有块信息决定。此外,由于我们的多数文件仅追加,过时的副本通常返回块的过早结束而不是过时数据。当读取器重试并联系Master节点时,它会立即获得当前的块位置。
在成功变更后很长时间内,组件故障仍可能损坏或破坏数据。GFS 通过Master节点与所有块服务器之间的定期握手识别故障块服务器,并通过校验和检测数据损坏(参见第5.2节)。一旦出现问题,数据会尽快从有效副本中恢复(参见第4.3节)。只有在 GFS 能够反应之前所有副本都丢失的情况下,块才会不可逆转地丢失,通常在几分钟内发生。即使在这种情况下,它也变得不可用,而不是损坏:应用程序收到明确的错误而不是损坏的数据。
2.7.2 对应用程序的影响
GFS 应用程序可以通过一些已用于其他目的的简单技术来适应宽松的一致性模型:依赖追加而非覆盖、检查点以及写入自验证、自标识的记录。
几乎所有我们的应用程序通过追加而不是覆盖来变更文件。在一种典型用例中,写入者从头到尾生成一个文件。它在写入所有数据后原子性地将文件重命名为永久名称,或定期检查点已成功写入的数据量。检查点还可能包括应用级别的校验和。读取者只验证并处理到最后一个检查点的文件区域,该区域被认为是已定义状态。无论一致性和并发问题如何,这种方法都很好地服务于我们。追加比随机写入更高效且对应用程序故障更有弹性。检查点使写入者能够逐步重启,并防止读取者处理从应用程序角度看仍不完整的已成功写入文件数据。
在另一种典型用例中,许多写入者同时向一个文件追加数据以获取合并结果或作为生产者-消费者队列。记录追加的“至少一次追加”语义保留了每个写入者的输出。读取者处理偶尔的填充和重复记录如下。写入者准备的每个记录包含额外信息如校验和,以便验证其有效性。读取者可以使用校验和识别并丢弃额外的填充和记录片段。如果不能容忍偶尔的重复记录(例如,它们会触发非幂等操作),则可以使用记录中的唯一标识符过滤它们,这些标识符通常也用于命名相应的应用程序实体如网页文档。这些记录I/O功能(除了重复删除)在我们的应用程序共享的库代码中实现,并适用于 Google 的其他文件接口实现。通过这些功能,记录读取器总是接收到相同的记录序列,外加罕见的重复记录。
未完待续