Google∗
摘要
我们设计并实现了谷歌文件系统,这是一个可扩展的分布式文件系统,适用于大型分布式数据密集型应用。该系统可在廉价的商品硬件上运行,同时提供容错功能,并能为大量客户端提供高聚合性能。
我们的设计目标与之前的分布式文件系统有许多相同之处,但我们对应用工作负载和技术环境(包括当前和预期环境)的观察结果表明,我们的设计明显偏离了之前的一些文件系统假设。这促使我们重新审视传统的选择,探索完全不同的设计要点。
文件系统成功地满足了我们的存储需求。它在谷歌内部被广泛部署,作为生成和处理我们的服务所使用的数据以及需要大型数据集的研发工作的存储平台。迄今为止,最大的集群在一千多台机器上的数千个磁盘上提供了数百 TB 的存储空间,并被数百个客户端并发访问。
在本文中,我们介绍了为支持分布式应用而设计的文件系统接口扩展,讨论了我们设计的许多方面,并报告了微基准测试和实际使用的测量结果。
1. 引言
我们设计并实施了谷歌文件系统(GFS),以满足谷歌快速增长的数据处理需求。GFS 与以前的分布式文件系统有许多相同的目标,如性能、可扩展性、可靠性和可用性。但是,在设计 GFS 时,我们对当前和预期的应用工作负载和技术环境进行了重要观察,这反映出我们明显偏离了之前的一些文件系统设计假设。我们重新审视了传统的选择,并探索了设计空间中完全不同的点。
首先,组件故障是常态而非例外。文件系统由数百甚至数千台存储机组成,这些存储机都是用廉价的商品部件制造的,并被数量相当的客户机访问。这些组件的数量和质量几乎可以保证,在任何特定时间都会有一些组件无法正常工作,而且有些组件无法从当前故障中恢复。我们见过应用程序错误、操作系统错误、人为错误以及磁盘、内存、连接器、网络和电源故障造成的问题。因此,持续监控、错误检测、容错和自动恢复必须成为系统的组成部分。
其次,按照传统的标准,文件是巨大的。多 GB 的文件很常见。每个文件通常包含许多应用对象,如网络文档。当我们经常处理由数十亿个对象组成的多 TB 快速增长的数据集时,即使文件系统可以支持,要管理数十亿个约 KB 大小的文件也很不方便。因此,必须重新审视 I/O 操作和块大小等设计假设和参数。
第三,大多数文件都是通过添加新数据而不是覆盖现有数据来改变的。文件内的随机写入几乎不存在。文件一旦写入,就只能读取,而且通常只能按顺序读取。各种数据都具有这些特征。有些可能是数据分析程序扫描过的大型存储库。有些可能是运行中的应用程序持续生成的数据流。有些可能是档案数据。有些可能是在一台机器上生成并在另一台机器上处理的中间结果,无论是同时处理还是稍后处理。鉴于巨型文件的这种访问模式,追加成为性能优化和原子性保证的重点,而在客户端缓存数据块则失去了吸引力。
第四,共同设计应用程序和文件系统 API 可以提高我们的灵活性,从而使整个系统受益。例如,我们放宽了 GFS 的一致性模型,大大简化了文件系统,而不会给应用程序带来沉重负担。我们还引入了原子追加操作,使多个客户端可以同时追加文件,而无需额外的同步。本文稍后将详细讨论这些内容。
目前,为不同目的部署了多个 GFS 集群。最大的集群有超过 1000 个存储节点,超过 300 TB 的磁盘存储空间,数百个客户端在不同的机器上持续大量访问。
2. 设计概述
2.1 假设
在设计满足我们需求的文件系统时,我们遵循的假设既是挑战也是机遇。我们在前面提到了一些重要的观察结果,现在详细介绍一下我们的假设。
- 该系统由许多经常发生故障的廉价商品组件构成。系统必须不断进行自我监控,日常检测、容忍和及时恢复组件故障。
- 系统存储的大文件数量不多。我们预计会有几百万个文件,每个文件的大小通常为 100 MB 或更大。多 GB 文件是常见情况,应得到有效管理。我们必须支持小文件,但无需对其进行优化。
- 工作负载主要包括两种读取:大型流式读取和小型随机读取。在大数据流读取中,单个操作通常读取数百 KB,更常见的是 1 MB 或更多。来自同一客户端的连续操作通常会读取文件的连续区域。小型随机读取通常在某个任意偏移位置读取几个 KB 的数据。注重性能的应用程序通常会对小规模读取进行批处理和排序,以稳定地读取文件,而不是来回读取。
- 这些工作负载中还有许多向文件追加数据的大型连续写入操作。典型的操作大小与读取类似。文件一旦写入,就很少再修改。支持在文件任意位置进行小规模写入,但不一定要高效。
- 系统必须有效地实现多个客户端同时追加到同一文件的定义明确的语义。我们的文件通常用作生产者-消费者队列或多路合并。数以百计的生产者(每台机器运行一个)将同时追加到一个文件。同步开销最小的原子性至关重要。文件可能会在稍后被读取,或者消费者可能会同时读取文件。
- 高持续带宽比低延迟更重要。我们的大多数目标应用都非常重视高速批量处理数据,而很少有应用对单个读取或写入的响应时间有严格要求。
2.2 接口
GFS 提供了一个熟悉的文件系统接口,尽管它没有实现标准的 API(如 POSIX)。文件在目录中按层次组织,并用路径名标识。我们支持创建、删除、打开、关闭、读取和写入文件的常规操作。
此外,GFS 还有快照和记录追加操作。快照以低成本创建文件或目录树的副本。记录追加允许多个客户端同时向同一个文件追加数据,同时保证每个客户端追加的原子性。它适用于实现多向合并结果和生产者-消费者队列,许多客户端可以同时追加数据而无需额外锁定。我们发现,这些类型的文件在构建大型分布式应用时非常有用。快照追加和记录追加将分别在第 3.4 节和第 3.3 节中进一步讨论。
2.3 架构
如图 1 所示,GFS 集群由一个主服务器和多个分块服务器组成,并由多个客户端访问。每个客户端通常是一台运行用户级服务器进程的商用 Linux 机器。只要机器资源允许,并且可以接受运行可能不稳定的应用程序代码所带来的较低可靠性,在同一台机器上同时运行分块服务器和客户端是很容易的。
文件被分成固定大小的块。每个分块都由主服务器程序在创建分块时分配的不可更改且全球唯一的 64 位分块句柄来标识。分块服务器将分块作为 Linux 文件存储在本地磁盘上,并读取或写入由分块句柄和字节范围指定的分块数据。为了保证可靠性,每个数据块都会在多个数据块服务器上复制。默认情况下,我们会存储三个副本,但用户可以为文件命名空间的不同区域指定不同的复制级别。
主文件系统维护所有文件系统元数据。其中包括命名空间、访问控制信息、文件到分块的映射以及分块的当前位置。主服务器还控制着全系统的活动,如块租约管理、孤儿块的垃圾回收以及块服务器之间的块迁移。主服务器会定期通过 HeartBeat 消息与每个块服务器进行通信,向其下达指令并收集其状态。
连接到每个应用程序的 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 分块位置
主服务器程序不会持续记录哪些分块服务器拥有某个分块的副本。它只需在启动时轮询各块服务器以获取该信息。由于主服务器控制着所有的分块放置,并通过定期的 HeartBeat 信息监控分块服务器的状态,因此主服务器可以随时更新自己的信息。
我们最初尝试在主服务器端持久保存块位置信息,但后来发现,在启动时和之后定期从块服务器请求数据要简单得多。这样就解决了主服务器和块服务器在块服务器加入、离开集群、更名、故障、重启等情况下保持同步的问题。在拥有数百台服务器的集群中,这些事件经常发生。
理解这一设计决定的另一种方法是认识到,对于自己的磁盘上是否有数据块,数据块服务器拥有最终决定权。试图在主服务器上保持对这些信息的一致看法是没有意义的,因为数据块服务器上的错误可能会导致数据块自发消失(例如,磁盘坏掉并被禁用),或者操作员可能会重命名数据块服务器。
2.6.3 操作日志
操作日志包含关键元数据更改的历史记录。它是 GFS 的核心。它不仅是元数据的唯一持久记录,还是定义并发操作顺序的逻辑时间线。文件和数据块以及它们的版本(见第 4.5 节)都以它们创建的逻辑时间为唯一且永恒的标识。
由于操作日志至关重要,因此我们必须可靠地存储操作日志,并且在元数据更改持久化之前,不能让客户端看到更改。否则,即使数据块本身存活下来,我们也会丢失整个文件系统或最近的客户端操作。因此,我们将日志复制到多台远程机器上,只有在本地和远程将相应的日志记录刷新到磁盘后,才能响应客户端操作。在刷新之前,主服务器程序会将多条日志记录集中在一起,从而减少刷新和复制对整个系统吞吐量的影响。
主服务器通过重放操作日志来恢复其文件系统状态。为了最大限度地缩短启动时间,我们必须保持日志较小。每当日志超过一定大小时,主服务器都会检查其状态,以便它可以通过从本地磁盘加载最新的检查点并在此之后仅重放有限数量的日志记录来恢复。检查点采用紧凑的 B 树形式,可以直接映射到内存中并用于命名空间查找,而无需额外解析。这进一步加快了恢复速度并提高了可用性。
由于创建一个检查点可能需要一段时间,因此主服务器的内部状态结构应能在不耽误接收传入的变更的情况下创建新的检查点。主服务器会切换到新的日志文件,并在单独的线程中创建新的检查点。新的检查点包括切换前的所有变更。对于拥有几百万个文件的集群来说,一分钟左右就能创建完毕。完成后,它将被写入本地和远程磁盘。
恢复只需要最新的完整检查点和后续日志文件。较早的检查点和日志文件可以随意删除,但我们会保留一些以防万一。检查点过程中的故障不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。
2.7 一致性模型
GFS 有一个宽松的一致性模型,可以很好地支持我们的高度分布式应用,而且实现起来相对简单高效。我们现在讨论 GFS 的保证及其对应用程序的意义。我们还将重点介绍 GFS 如何维护这些保证,但具体细节将留待本文其他部分讨论。
2.7.1 GFS的保证
文件命名空间的变更(如文件创建)是原子性的。它们完全由主服务器程序处理:命名空间锁定保证了原子性和正确性(第 4.1 节);主服务器程序的操作日志定义了这些操作的全局总顺序(第 2.6.3 节)。
文件区域在数据变更后的状态取决于变更的类型、变更是否成功,以及是否存在并发变更。表1总结了结果。如果所有客户端无论从哪个副本读取数据都始终看到相同的数据,则文件区域是一致的(consistent)。在文件数据变更后,如果区域是一致的,并且客户端能够完整地看到其变更所写入的内容,则该区域是确定的(defined)。当一个变更在没有并发写入者干扰的情况下成功时,受影响的区域则为确定的(包含了一致性):所有客户端将始终看到变更所写入的内容。并发成功的变更使区域不确定但仍然一致:所有客户端看到相同的数据,但这些数据可能无法反映任何单个变更的内容。通常,这些数据由多个变更的混合片段组成。失败的变更使区域变得不一致(因此也不确定):不同的客户端可能在不同的时间看到不同的数据。下面我们将描述我们的应用程序如何区分确定与不确定区域。应用程序不需要进一步区分不同类型的不确定区域。
数据变更可以是写入操作或记录追加操作。写入操作会在应用程序指定的文件偏移位置写入数据。记录追加操作会在存在并发变更的情况下,以原子方式至少追加一次数据(即“记录”),但写入的位置由 GFS 选择(见第3.3节)。(与此相反,“常规 “追加只是在客户端认为是当前文件末尾的偏移位置进行写入)。偏移量会返回给客户端,并标志着包含记录的确定区域的开始。此外,GFS 还可能在中间插入填充或重复记录。它们占据的区域被认为是不一致的,与用户数据量相比通常相形见绌。
在一连串成功的变更之后,变更后的文件区域保证是已确定的,并包含最后一次变更所写入的数据。GFS 通过以下方式实现这一目标:(a) 在所有副本上以相同顺序对一个主块进行变异(第 3.1 节);(b) 使用主块版本号来检测因其主块服务器宕机而错过变异的副本(第 4.5 节)。陈旧副本绝不会参与变更,也不会提供给向主服务器询问主块位置的客户端。它们会尽早被垃圾回收。
由于客户端会缓存块定位,因此可能会在信息刷新前从陈旧的副本中读取信息。这个窗口会受到缓存条目超时和下一次打开文件的限制,因为下一次打开文件会从缓存中清除该文件的所有分块信息。此外,由于我们的大多数文件都是仅附加的,因此陈旧的副本通常会返回一个过早结束的分块,而不是过时的数据。当阅读器重试并联系主文件时,它将立即获得当前的分块位置。
当然,在成功变更后的很长一段时间内,组件故障仍有可能损坏或毁坏数据。GFS 通过主服务器与所有主服务器之间的定期握手来识别故障的主服务器,并通过校验和来检测数据损坏(第 5.2 节)。一旦出现问题,就会尽快从有效副本中恢复数据(第 4.3 节)。只有当数据块的所有副本在 GFS 作出反应前丢失(通常在几分钟内),数据块才会不可逆转地丢失。即使在这种情况下,数据块也是不可用的,而不是损坏的:应用程序收到的是明确的错误,而不是损坏的数据。
2.7.2 对应用的影响
GFS 应用程序可以通过其他用途所需的一些简单技术来适应宽松的一致性模型:依靠追加而不是覆盖、检查点以及写入自验证、自识别记录。
实际上,我们所有的应用程序都是通过追加而不是覆盖来改变文件的。在一个典型的应用中,写入器从头到尾生成一个文件。在写入所有数据后,它会以原子方式将文件重命名为永久名称,或定期检查已成功写入多少数据。检查点还可能包括应用级校验和。读取器只验证和处理上一个检查点之前的文件区域,因为已知该区域处于确定的状态。无论一致性和并发性问题如何,这种方法都很有效。与随机写入相比,追加的效率要高得多,对应用程序故障的恢复能力也更强。检查点允许写入程序以增量方式重新启动,并防止读取程序处理从应用程序角度来看仍不完整的已成功写入文件数据。
在另一种典型用途中,许多写入器同时向文件追加合并结果或作为生产者-消费者队列。记录追加的 “至少追加一次 “语义保留了每个写入器的输出。读取器会按如下方式处理偶尔出现的填充和重复。写入器编写的每条记录都包含校验和等额外信息,以便验证其有效性。阅读器可以使用校验和来识别和丢弃额外的填充和记录片段。如果不能容忍偶尔出现的重复记录(例如,如果重复记录会触发非幂等操作),则可以使用记录中的唯一标识符将其过滤掉。记录 I/O 的这些功能(除重复删除外)都在我们应用程序共享的库代码中,也适用于谷歌的其他文件接口实现。有了这些功能,我们就能始终向记录阅读器提供相同序列的记录,以及极少数的重复记录。
3. 系统交互
我们在设计系统时尽量减少主服务器对所有操作的参与。有了上述背景,我们现在来介绍客户端、主服务器端和分块服务器是如何交互实现数据变更、原子记录追加和快照的。
3.1 租约和变更顺序
变更是指更改数据块内容或元数据的操作,例如写入或追加操作。每次变更都会在所有数据块的副本上执行。我们使用租约来维护副本之间一致的变更顺序。主服务器将一个数据块的租约授予其中一个副本,我们称之为主副本。主副本为所有对该数据块的变更选择一个串行顺序。所有副本在应用变更时都遵循这个顺序。因此,全局变更顺序首先由主服务器选择的租约授予顺序定义,而在一个租约内,则由主副本分配的序列号来定义。
租约机制旨在最小化主服务器的管理开销。租约的初始超时时间为60秒。然而,只要数据块正在被变更,主副本可以向主服务器请求并通常能够获得无限期的租约延长。这些延长请求和授予通过主服务器与所有数据块服务器定期交换的 HeartBeat 消息进行捆绑。当主服务器希望在文件重命名时禁用对该文件的变更时,有时会尝试在租约到期前撤销租约。即使主服务器与主副本失去通信,在旧租约到期后,它也可以安全地将新租约授予另一个副本。
在图 2 中,我们按照写入的控制流,通过这些编号步骤来说明这一过程。
- 客户端向主服务器询问当前哪个数据块服务器持有该数据块的租约,以及其他副本的位置。如果没有副本持有租约,主服务器将向它选择的一个副本授予租约(未显示)。
- 主服务器会回复主副本的身份和其他(次级)副本的位置。客户端将这些数据缓存以供将来的变更使用。只有在主副本变得不可达或回复称不再持有租约时,客户端才需要再次联系主服务器。
- 客户端将数据推送到所有副本。客户端可以按照任意顺序进行此操作。每个数据块服务器会在内部的LRU缓存中存储数据,直到数据被使用或过期。通过将数据流与控制流解耦,我们可以根据网络拓扑优化昂贵的数据流调度,而不依赖于哪个数据块服务器是主副本。第3.2节对此进行了进一步讨论。
- 一旦所有副本确认接收到数据,客户端会向主副本发送写请求。该请求标识了之前推送给所有副本的数据。主副本为接收到的所有变更(可能来自多个客户端)分配连续的序列号,以提供必要的序列化。它按照序列号的顺序将变更应用到其本地状态中。
- 主副本将写入请求转发给所有次级副本。每个次级副本按照主副本分配的序列号顺序应用变更。
- 所有次级副本都会回复主副本,表示它们已完成操作。
- 主副本会向客户端回复。任何在副本处遇到的错误都会报告给客户端。在发生错误的情况下,写操作可能在主副本成功,而在任意数量的次级副本中失败。(如果主副本的写操作失败,它将不会被分配序列号并进行转发。)客户端请求被视为失败,修改区域将处于不一致状态。我们的客户端代码通过重试失败的变更来处理这些错误。它会在步骤(3)到(7)之间进行几次尝试,然后才会回退到从写操作开始重新尝试。
如果应用程序的写操作较大或跨越了数据块边界,GFS客户端代码会将其分解为多个写操作。所有操作都会遵循上述控制流程,但可能与其他客户端的并发操作交错或被覆盖。因此,虽然副本之间的内容会保持一致(因为所有操作在所有副本上都以相同的顺序成功完成),共享文件区域可能最终包含来自不同客户端的片段。正如第2.7节提到的,这会使文件区域处于一致但未确定的状态。
3.2 数据流
我们将数据流与控制流解耦,以更高效地利用网络资源。控制流从客户端传递到主副本,再从主副本传递到所有次级副本;而数据则以流水线的方式沿着精心选择的链式数据块服务器线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟连接,并尽量减少推送全部数据的延迟。
为了充分利用每台机器的网络带宽,数据被沿着一条数据块服务器链线性推送,而不是通过其他拓扑结构(例如树形结构)分发。这样,每台机器的全部出站带宽都用于尽可能快速地传输数据,而不是被分配给多个接收者。
为了尽可能避免网络瓶颈和高延迟链路(例如,交换机间链路往往同时存在),每台机器都会将数据转发给网络拓扑结构中尚未接收数据的 “最近 的"机器。假设客户端正在向分块服务器 S1 至 S4 推送数据。客户端将数据发送到最近的分块服务器,如 S1。S1 将数据转发给离 S1 最近的分块服务器 S2 至 S4,即 S2。同样,S2 将数据转发给离 S2 最近的 S3 或 S4,以此类推。我们的网络结构非常简单,“距离 “可以通过 IP 地址准确估算。
为了最大限度地减少延迟,我们通过 TCP 连接进行数据传输的流水线化处理。一旦数据块服务器接收到一部分数据,它就会立即开始转发。流水线机制对我们特别有帮助,因为我们使用的是带有全双工链路的交换网络。立即发送数据不会降低接收速率。在没有网络拥塞的情况下,将 B 字节的数据传输到 R 个副本的理想时间为 B/T + RL,其中 T 是网络吞吐量,L 是两台机器之间传输数据的延迟。我们网络的典型链路速率为 100 Mbps (T),而L远低于 1 毫秒。因此,1 MB 的数据理想情况下可以在大约 80ms 内分发完成。
3.3 原子记录追加
GFS 提供了一种称为记录追加的原子追加操作。在传统的写操作中,客户端指定数据写入的偏移量。对于同一区域的并发写操作,无法实现可序列化:该区域可能最终包含来自多个客户端的数据片段。然而,在记录追加操作中,客户端只需指定数据。GFS 会以原子方式(即作为一个连续的字节序列)至少一次将数据追加到文件中,偏移量由 GFS 决定,并将该偏移量返回给客户端。这类似于在 Unix 系统中以 O_APPEND
模式打开文件进行写操作,但避免了多个写入者同时操作时的竞争条件。
我们的分布式应用程序大量使用记录追加功能,其中不同机器上的许多客户端会同时追加到同一个文件。如果客户端采用传统的写入方式,则需要额外复杂而昂贵的同步,例如通过分布式锁管理器。在我们的工作负载中,此类文件通常用作多生产者/单消费者队列,或包含来自许多不同客户端的合并结果。
记录追加是一种变更,它遵循第 3.1 节中的控制流,只在主副本有一些额外的逻辑。客户端将数据推送到文件最后一个分块的所有副本中,然后将请求发送给主副本。主副本会检查将记录追加到当前数据块是否会导致数据块超过最大容量(64 MB)。如果会,它就会将分块填充到最大大小,告诉辅助分块也这样做,并回复客户端,说明应在下一个分块上重试操作。(记录追加的大小最多只能是最大分块大小的四分之一,以便将最坏情况下的碎片保持在可接受的水平)。如果记录符合最大大小(这是常见的情况),主副本就会将数据追加到其副本中,并告诉次节点在其确切偏移量处写入数据,最后向客户端回复成功。
如果任何副本的记录追加失败,客户端会重试操作。因此,同一分块的副本可能包含不同的数据,其中可能包括同一记录的全部或部分副本。GFS 并不保证所有副本在字节上完全相同。它只保证数据作为一个原子单元至少被写入一次。这一特性很容易从简单的观察中得出:要使操作报告成功,数据必须在某个数据块的所有副本上以相同的偏移量写入。此外,在此之后,所有副本的长度至少与记录末尾的长度相同,因此,即使后来不同的副本成为主副本,未来的记录也会被分配到更高的偏移量或不同的分块。就一致性保证而言,成功进行记录追加操作并写入数据的区域是确定的(因此是一致的),而中间区域则是不一致的(因此是未确定的)。正如我们在第 2.7.2 节中所讨论的,我们的应用程序可以处理不一致的区域。
3.4 快照
快照操作几乎可以瞬间复制文件或目录树(“源”),同时最大限度地减少对正在进行的变更的干扰。我们的用户用它来快速创建庞大数据集的分支副本(通常是这些副本的递归副本),或者在尝试更改之前对当前状态进行检查,这些更改随后可以很容易地提交或回滚。
与 AFS [5] 一样,我们使用标准的写时复制技术来实现快照。当主服务器接收到快照请求时,它首先会撤销即将快照文件中的数据块上的所有未完成的租约。这确保了对这些数据块的任何后续写入都需要与主服务器进行交互,以查找租约持有者。这将主服务器就有机会首先创建数据块的新副本。
租约撤销或过期后,主服务器会将操作记录到磁盘上。然后,它通过复制源文件或目录树的元数据,将此日志记录应用到内存状态。新创建的快照文件指向与源文件相同的块。
当客户端第一次想要在快照操作之后写入数据块 C 时,它会向主服务器发送请求以查找当前的租约持有者。主服务器注意到数据块 C 的引用计数大于1。它推迟了对客户端请求的回复,而是选择了一个新的数据块句柄 C’。然后,主服务器要求每个拥有 C 当前副本的数据块服务器创建一个名为 C’ 的新数据块。通过在与原始数据块相同的数据块服务器上创建新数据块,我们确保数据可以在本地复制,而不是通过网络复制(我们的磁盘速度约是100 Mb 以太网链接的三倍)。从这一点开始,请求处理与任何数据块的处理没有区别:主服务器将新的数据块 C’ 的租约授予其中一个副本,并回复客户端,客户端可以正常写入该数据块,而不知道它刚刚是从现有数据块创建的。
4. 主服务器操作
主服务器执行所有命名空间操作。此外,它还负责管理整个系统中的分块副本:做出放置决定、创建新的分块和副本、协调各种全系统活动以保持分块完全复制、平衡所有分块服务器的负载以及回收未使用的存储空间。下面我们将逐一讨论这些主题。
4.1 命名空间管理和锁定
许多主服务器操作可能需要较长时间:例如,快照操作必须撤销覆盖快照的所有数据块服务器上的租约。我们不希望在这些操作运行时延迟其他主服务器操作。因此,我们允许多个操作同时进行,并在命名空间的区域上使用锁来确保适当的序列化。
与许多传统文件系统不同,GFS 没有每个目录的数据显示结构来列出该目录中的所有文件。它也不支持同一个文件或目录的别名(即 Unix 术语中的硬链接或符号链接)。GFS 在逻辑上将其命名空间表示为一个查找表,该表将完整路径名映射到元数据。通过前缀压缩,该表可以高效地在内存中表示。命名空间树中的每个节点(无论是绝对文件名还是绝对目录名)都有一个关联的读写锁。
每个主操作都会在运行前获取一组锁。通常情况下,如果涉及 /d1/d2/.../dn/leaf
,它将获得目录名 /d1
、/d1/d2
、…、/d1/d2/.../dn
的读锁,以及完整路径名 /d1/d2/.../dn/leaf
的读锁或写锁。请注意,根据操作的不同,leaf
可以是文件或目录。
现在,我们将说明这种锁定机制如何防止 /home/user/foo
文件在 /home/user
被快照到 /save/user
时被创建。快照操作会获取/home
和 /save
上的读锁,以及 /home/user
和 /save/user
上的写锁。文件创建会获取/home
和 /home/user
上的读锁,以及 /home/user/foo
上的写锁。这两个操作将被正确序列化,因为它们试图获取 /home/user
上的冲突锁。文件创建不需要对父级目录加写锁,因为没有 “目录 “或类似于 inode 的数据结构需要防止修改。对名称的读取锁足以保护父目录不被删除。
该锁机制的一个优点是它允许在同一目录中同时进行并发的修改。例如,可以在同一目录中同时执行多个文件创建操作:每个操作都会对目录名称获取读锁,并对文件名称获取写锁。对目录名称的读锁足以防止目录被删除、重命名或快照。而对文件名称的写锁则能序列化(排队)防止两次创建同名文件的尝试。
由于命名空间可能有很多节点,因此读写锁对象的分配比较懒惰,一旦不使用就会被删除。此外,为防止死锁,获取锁的总顺序是一致的:首先按命名空间树中的层级排序,然后在同一层级内按字典序排序。
4.2 副本放置
一个 GFS 集群在多个层面上高度分布化。它通常拥有数百个分布在多个机架上的数据块服务器。这些数据块服务器又可能被来自同一机架或不同机架的数百个客户端访问。位于不同机架的两台机器之间的通信可能需要通过一个或多个网络交换机。此外,进入或离开某个机架的带宽可能小于该机架内所有机器的总带宽。多层级的分布化为数据分布带来了独特的挑战,涉及到可扩展性、可靠性和可用性的需求。
分块副本放置策略有两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽的利用。为此,仅将副本分布在不同机器上是不够的,这样只能防止磁盘或机器故障并充分利用每台机器的网络带宽。我们还必须将块副本分布在不同的机架上。这可以确保即使整个机架发生故障或下线(例如,由于共享资源故障,如网络交换机或电路问题),块的一些副本仍能存活并保持可用性。这也意味着某个块的流量,尤其是读操作,可以利用多个机架的总带宽。然而,写操作的流量需要通过多个机架,这是我们愿意接受的权衡。
4.3 创建、再复制、再平衡
数据块副本的创建有三个原因:块创建、重新复制和再平衡。
当主服务器创建一个数据块时,它会选择放置初始空副本的位置,并考虑几个因素:(1) 我们希望将新副本放置在磁盘利用率低于平均水平的块服务器上。随着时间推移,这将使各块服务器的磁盘利用率趋于平衡。(2) 我们希望限制每个块服务器上“最近”创建的副本数量。虽然创建本身成本低,但它能可靠地预测即将到来的大量写入流量,因为数据块是在写入需求时创建的,并且在我们的“一次追加-多次读取”工作负载中,一旦完全写入,它们通常会变成几乎只读。(3) 如上所述,我们希望将数据块的副本分散到不同的机架上。
主服务器会在可用副本数量低于用户指定目标时立即重新复制数据块。这可能由于多种原因发生:数据块服务器变得不可用、报告其副本可能已损坏、某个磁盘因错误被禁用,或复制目标增加。每个需要重新复制的数据块会根据多个因素进行优先级排序。其中一个因素是它距离复制目标的远近。例如,我们会优先处理失去两个副本的数据块,而不是仅失去一个副本的数据块。此外,我们更倾向于优先重新复制活动文件的数据块,而不是属于最近删除文件的数据块(见第 4.4 节)。最后,为了最小化故障对正在运行的应用程序的影响,我们会提高任何阻塞客户端进展的数据块的优先级。
主服务器会选择优先级最高的数据块,并通过指示某个数据块服务器直接从现有有效副本复制数据块来“克隆”它。新副本的放置目标与创建时类似:平衡磁盘空间利用率、限制任何单个数据块服务器上的活动克隆操作,以及在机架间分散副本。为了防止克隆流量压倒客户端流量,主服务器限制了集群和每个数据块服务器上的活动克隆操作数量。此外,每个数据块服务器通过限制对源数据块服务器的读取请求,来控制每个克隆操作所使用的带宽。
最后,主服务器会定期重新平衡副本:它会检查当前的副本分布,并移动副本以实现更好的磁盘空间和负载平衡。在这个过程中,主服务器逐步填充新的数据块服务器,而不是立即用新数据块和随之而来的大量写入流量淹没它。新副本的放置标准与上述讨论的相似。此外,主服务器还必须选择移除哪个现有副本。一般来说,它更倾向于移除那些在磁盘空间低于平均水平的数据块服务器上的副本,以便平衡磁盘空间的使用。
4.4 垃圾回收
删除文件后,GFS 不会立即回收可用的物理存储空间。它只会在文件和块级的定期垃圾回收过程中懒惰地回收。我们发现,这种方法让系统变得更简单、更可靠。
4.4.1 机制
当应用程序删除一个文件时,主服务器会立即记录删除操作,就像其他更改一样。然而,主服务器并不会立即回收资源,而是将文件重命名为一个包含删除时间戳的隐藏名称。在主服务器定期扫描文件系统命名空间时,它会移除任何存在超过三天(该时间间隔是可配置的) 的隐藏文件。在此之前,该文件仍可以通过新的特殊名称进行读取,并且可以通过重命名恢复为正常文件。当隐藏文件从命名空间中移除时,其内存中的元数据会被擦除,这有效地切断了与所有数据块的链接。
在对数据块命名空间进行类似的定期扫描时,主服务器会识别孤立的数据块(即那些无法从任何文件访问的数据块),并擦除这些数据块的元数据。在与主服务器定期交换的心跳消息中,每个数据块服务器会报告它所拥有的一部分数据块,而主服务器则回复所有不再存在于其元数据中的数据块的身份。数据块服务器可以自由地删除这些孤立数据块的副本。
4.4.2 讨论
尽管分布式垃圾回收在编程语言中是一个难题,需要复杂的解决方案,但在我们的情况下却相对简单。我们可以轻松识别所有对数据块的引用:它们在由主服务器独占维护的文件到数据块的映射中。此外,我们也可以轻松识别所有的数据块副本:它们是每个数据块服务器上指定目录下的Linux文件。任何不被主服务器知晓的副本都可以视为“垃圾”。
这种垃圾回收的方法在存储回收方面相比于急切删除具有几个优势。首先,在组件故障常见的大规模分布式系统中,它简单且可靠。数据块的创建可能在某些数据块服务器上成功,但在其他服务器上失败,导致主服务器不知道存在的副本。副本删除消息可能会丢失,主服务器需要记住在故障(包括自身和数据块服务器的故障)后重新发送这些消息。垃圾回收提供了一种统一且可靠的方式来清理任何不被认为有用的副本。其次,它将存储回收与主服务器的常规后台活动(如命名空间的定期扫描和与数据块服务器的握手)合并。因此,回收操作是在批处理过程中进行的,成本被摊销。此外,它只在主服务器相对空闲时进行,从而使主服务器能够更迅速地响应需要及时关注的客户端请求。第三,回收存储的延迟提供了一个安全网,防止意外和不可逆的删除。
根据我们的经验,主要缺点是延迟有时会妨碍用户在存储空间紧张时对使用情况进行微调。反复创建和删除临时文件的应用程序可能无法立即重新使用存储空间。为了解决这些问题,如果删除的文件被再次明确删除,我们就会加快存储空间的回收。我们还允许用户对命名空间的不同部分应用不同的复制和回收策略。例如,用户可以指定某些目录树中的所有文件块都不进行复制存储,任何被删除的文件都会立即从文件系统状态中不可撤销地移除。
4.5 过时副本检测
如果一个分块服务器发生故障,并在宕机期间错过了对分块的变更,那么分块副本就可能成为过期副本。对于每个数据块,主服务器都会维护一个数据块版本号,以区分最新和过时的副本。
每当主服务器为某个数据块授予新的租约时,它会增加该数据块的版本号,并通知最新的副本。主服务器和这些副本都会在其持久状态中记录新的版本号。这一过程发生在任何客户端被通知之前,因此在客户端开始写入数据块之前。如果某个副本当前不可用,则该副本的版本号将不会被提升。当该数据块服务器重新启动并报告其数据块及其相关版本号时,主服务器将检测到该数据块服务器拥有一个过时的副本。如果主服务器看到某个版本号高于其记录中的版本号,它会假设在授予租约时发生了故障,因此会将更高的版本视为最新版本。
主服务器在其定期的垃圾回收过程中会移除过时的副本。在此之前,主服务器在回复客户端关于数据块信息的请求时,实际上会认为过时的副本根本不存在。作为另一种保护措施,主服务器在告知客户端哪个数据块服务器持有某个数据块的租约或在指示某个数据块服务器从另一数据块服务器读取数据块进行克隆操作时,会包含数据块的版本号。客户端或数据块服务器在执行操作时会验证版本号,以确保始终访问最新的数据。
5. 容错与诊断
我们在设计系统时面临的最大挑战之一就是处理频繁的组件故障。组件的质量和数量使得这些问题变得更加普遍,而不是偶尔出现:我们不能完全信任机器,也不能完全信任磁盘。组件故障可能导致系统不可用,或更糟糕的是,数据损坏。我们讨论如何应对这些挑战,以及我们在系统中内置的工具,用于在问题不可避免地发生时对其进行诊断。
5.1 高度可用性
5.1.1 快速恢复
无论主服务器和分块服务器是如何终止的,它们都能在数秒内恢复状态并启动。事实上,我们并不区分正常终止和非正常终止;服务器通常只需杀死进程即可关闭。客户端和其他服务器在未处理请求超时、重新连接到重新启动的服务器并重试时,都会经历一个小插曲。第 6.2.2 节报告了观察到的启动时间。
5.1.2 分块复制
如前所述,每个数据块会在不同机架的多个块服务器上进行复制。用户可以为文件命名空间的不同部分指定不同的复制级别,默认值为三。主服务器根据需要克隆现有副本,以保持每个数据块的完全复制,尤其是在块服务器离线或通过校验和验证检测到副本损坏时(参见第5.2节)。虽然复制技术一直表现良好,但我们正在探索其他形式的跨服务器冗余,如奇偶校验或纠删码,以满足日益增长的只读存储需求。我们预计在我们这种松耦合的系统中实施这些更复杂的冗余方案既具有挑战性又是可管理的,因为我们的流量主要是追加和读取,而不是小规模随机写入。
主服务器的状态被复制以确保可靠性。其操作日志和检查点会在多个机器上进行复制。只有当状态的日志记录已在本地及所有主服务器副本上刷新到磁盘后,状态的变更才被视为已提交。为了简化管理,只有一个主进程负责所有变更操作以及如垃圾回收等内部系统活动。当主服务器发生故障时,它几乎可以立即重新启动。如果其所在的机器或磁盘故障,GFS之外的监控基础设施会在其他地方启动一个新的主进程,并使用复制的操作日志。客户端只使用主服务器的规范名称(例如 gfs-test),这是一个可以在主服务器迁移到另一台机器时更改的DNS别名。
此外,“影子”主服务器在基本主服务器宕机时提供只读访问文件系统。它们被称为影子,而非镜像,因为它们可能会稍微滞后于主服务器,通常是几分之一秒。影子主服务器提高了对于未被积极修改的文件的读取可用性,或者对于不介意稍微陈旧结果的应用程序而言,增强了可用性。实际上,由于文件内容是从数据块服务器读取的,应用程序并不会观察到陈旧的文件内容。短时间内可能出现陈旧的,是文件元数据,比如目录内容或访问控制信息。
为了保持信息更新,影子主服务器读取正在增长的操作日志的副本,并以与主服务器完全相同的顺序将更改应用到其数据结构中。像主服务器一样,它在启动时(以及之后不频繁地)轮询块服务器以定位块副本,并与它们交换频繁的握手消息以监控其状态。影子主服务器仅依赖主服务器提供的副本位置更新,这些更新源于主服务器对副本的创建和删除决策。
5.2 数据完整性
每个块服务器使用校验和来检测存储数据的损坏。考虑到GFS集群通常有数千个磁盘分布在数百台机器上,它定期会经历磁盘故障,这会导致读写路径上的数据损坏或丢失(见第7节了解一个原因)。我们可以通过其他块副本来恢复损坏的数据,但通过比较块服务器之间的副本来检测损坏是不切实际的。此外,差异副本可能是合法的:GFS变更的语义,特别是之前讨论的原子记录追加,并不保证副本完全相同。因此,每个块服务器必须通过维护校验和独立验证其自身副本的完整性。
一个数据块被分成 64 KB 的块。每个块都有相应的 32 位校验和。与其他元数据一样,校验和也保存在内存中,并与日志一起持久存储,与用户数据分开。
对于读取,在向请求者(无论是客户端还是其他分块服务器)返回任何数据之前,分块服务器会验证与读取范围重叠的数据块的校验和。因此,数据块服务器不会将损坏传播给其他机器。如果数据块与记录的校验和不匹配,分块服务器会向请求者返回错误信息,并向主服务器报告不匹配情况。作为回应,请求者将从其他副本中读取,而主服务器将从另一个副本中克隆该块。在有效的新副本就位后,主服务器会指示报告不匹配的分块服务器删除其副本。
校验和对读取性能影响不大,原因有几个。由于我们的大多数读取至少跨越几个块,因此我们只需要读取和校验相对少量的额外数据以进行验证。GFS 客户端代码通过尝试在校验和块边界处对齐读取来进一步减少这种开销。此外,分块服务器上校验和的查找和比较无需任何 I/O,校验和计算通常可以与 I/O 重叠。
校验和计算针对附加到块末尾的写入(相对于覆盖现有数据的写入)进行了大量优化,因为它们在我们的工作负载中占主导地位。我们只需增量更新最后一个部分校验和块的校验和,并为附加所填充的全新校验和块计算新的校验和。即使最后一个部分校验和区块已经损坏,并且我们现在没有检测到,新的校验和值也不会与存储的数据匹配,而且在下一次读取该区块时,也会像往常一样检测到损坏。
相反,如果写入的内容覆盖了数据块的现有范围,我们就必须先读取并校验被覆盖范围的第一个和最后一个数据块,然后执行写入操作,最后计算并记录新的校验和。如果我们在部分覆盖首尾区块之前不对其进行校验,新的校验和可能会掩盖未被覆盖区域中存在的损坏。
在空闲期间,数据块服务器可以扫描并验证非活动数据块的内容。这样,我们就能检测到很少被读取的数据块的损坏。一旦检测到损坏,主控程序就可以创建一个新的未损坏副本,并删除损坏的副本。这样就能防止不活动但已损坏的数据块副本欺骗主服务器,让它以为自己有足够多的数据块有效副本。
5.4 诊断工具
广泛而细致的诊断日志在问题隔离、调试和性能分析方面发挥了不可估量的作用,而其成本却非常低。没有日志的话,很难理解机器之间瞬时且不可重复的交互。GFS 服务器生成诊断日志,记录许多重要事件(例如分块服务器的启动和关闭)以及所有的 RPC 请求和回复。这些诊断日志可以随意删除,而不会影响系统的正确性。然而,我们尽量在空间允许的情况下保留这些日志。
除了正在读取或写入的文件数据外,RPC 日志包括在线路上发送的确切请求和响应。通过匹配请求和回复,并整理不同机器上的 RPC 记录,我们可以重建整个交互历史,从而诊断问题。日志还可作为负载测试和性能分析的跟踪记录。
日志记录对性能的影响极小(并且其好处远远超过影响),因为这些日志是以顺序和异步方式写入的。最近的事件还保存在内存中,可用于持续的在线监控。
6. 测量
在本节中,我们将介绍一些微型基准测试,以说明 GFS 架构和实施中固有的瓶颈,以及谷歌实际使用的集群中的一些数据。
6.1 微基准测试
我们在由一个主服务器、两个主服务器副本、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 读
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%,是因为随着读取器数量的增加,多个读取器同时从同一个分块服务器读取数据的概率也在增加。
6.1.2 写
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 记录追加
图 3(c) 显示了记录附加性能。N 个客户端同时附加到单个文件。性能受存储文件最后一块的块服务器的网络带宽限制,与客户端数量无关。对于一个客户端,它从 6.0 MB/s 开始,对于 16 个客户端,它下降到 4.8 MB/s,这主要是由于拥塞和不同客户端看到的网络传输速率差异。
我们的应用通常会同时生成多个这样的文件。换句话说,N 个客户端会同时附加数据到 M 个共享文件中,N 和 M 的数量可以达到数十甚至上百。因此,在实际应用中,分块服务器的网络拥堵问题并不显著,因为当一个文件的分块服务器繁忙时,客户端仍然可以继续在其他文件上写入数据。
6.2 真实世界集群
我们现在研究 Google 内部使用的两个集群,它们代表了其他几个类似的集群。集群 A 经常被一百多名工程师用于研究和开发。一个典型的任务由人类用户发起,运行时间长达几个小时。它读取几 MB 到几 TB 的数据,转换或分析数据,并将结果写回集群。集群 B 主要用于生产数据处理。任务持续时间更长,并且持续生成和处理多 TB 数据集,仅偶尔需要人工干预。在这两种情况下,单个“任务”都由多台机器上的许多进程组成,这些进程同时读取和写入许多文件。
6.2.1 存储
如表2中的前五个条目所示,两个集群都有数百个分块服务器,支持许多 TB 的磁盘空间,而且都相当满,但不是完全满。“已用空间 “包括所有的分块复制。几乎所有文件都复制了三次。因此,集群分别存储了 18 TB 和 52 TB 的文件数据。
这两个集群的文件数量相似,但 B 集群的死文件比例更大,即已被删除或被新版本取代但其存储尚未被回收的文件。此外,由于 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 的数据,由于写入会传播到三个副本,因此产生了每秒 300 MB 的网络负载。
读取速率远高于写入速率。正如我们所假设的,总工作负载由更多的读取而不是写入组成。两个集群都处于大量读取活动之中。特别是,A 在前一周一直保持着 580 MB/s 的读取速率。其网络配置可以支持 750 MB/s,因此它正在高效地利用其资源。集群 B 可以支持 1300 MB/s 的峰值读取速率,但其应用程序仅使用了 380 MB/s。
6.2.4 主服务器负载
表 3 还显示,发送给主服务器的操作速率约为每秒 200 到 500 次操作。主服务器可以轻松跟上这个速率,因此不会成为这些工作负载的瓶颈。 在 GFS 的早期版本中,主服务器偶尔会成为某些工作负载的瓶颈。它大部分时间都在按顺序扫描大型目录(其中包含数十万个文件)以查找特定文件。此后,我们更改了主服务器数据结构,以允许通过命名空间进行高效的二分搜索。它现在可以轻松支持每秒数千次文件访问。如有必要,我们可以通过在命名空间数据结构前面放置名称查找缓存来进一步加快速度。
6.2.5 恢复时间
在块服务器发生故障后,某些块将变得复制不充分,必须进行克隆才能恢复其复制级别。恢复所有这些块所需的时间取决于资源量。在一次实验中,我们关闭了集群 B 中的单个块服务器。该块服务器有大约 15000 个块,包含 600 GB 的数据。为了限制对正在运行的应用程序的影响并为调度决策提供余地,我们的默认参数将此集群限制为 91 个并发克隆(块服务器数量的 40%),其中每个克隆操作最多允许消耗 6.25 MB/s(50 Mbps)。所有块在 23.2 分钟内恢复,有效复制速率为 440 MB/s。
在另一个实验中,我们关闭了两个分块服务器,每个服务器大约有 16000 个数据块和 660 GB 的数据。此双重故障导致 266 个数据块仅剩一个副本。这 266 个数据块被优先克隆,并在 2 分钟内全部恢复到至少 2 倍的副本数量,从而使集群恢复到可以容忍另一个分块服务器故障而不会导致数据丢失的状态。
6.3 工作负载细分
在本节中,我们将详细分析两个 GFS 集群的工作负载,它们与第 6.2 节中的集群类似但不完全相同。集群 X 用于研发,而集群 Y 用于生产数据处理。
6.3.1 方法与注意事项
这些结果仅包括客户端发起的请求,因此它们反映了我们的应用程序为整个文件系统生成的工作负载。它们不包括执行客户端请求的服务器间请求或内部后台活动,例如转发写入或重新平衡。
关于 I/O 操作的统计数据基于从 GFS 服务器记录的实际 RPC 请求中通过启发式方法重建的信息。例如,GFS 客户端代码可能会将一次读取分解成多个 RPC,以提高并行性,从中我们可以推断出原始读取。由于我们的访问模式具有高度的固定化特征,因此预期任何误差都微乎其微。应用程序进行显式日志记录可能会提供稍微更准确的数据,但在实际操作中,要重新编译并重启数千个正在运行的客户端并不现实,同时从这么多机器上收集结果也十分繁琐。
需要注意的是,不应过度泛化我们的工作负载。由于 Google 完全控制 GFS 及其应用程序,因此这些应用程序往往针对 GFS 进行了优化,反过来,GFS 也是为这些应用程序量身设计的。这种相互影响在通用应用程序和文件系统之间也可能存在,但在我们这种情况下,这种影响可能更加明显。
6.3.2 分块服务器工作负载
表 4 显示了按大小分列的操作分布情况。读取大小呈现双峰分布。小规模读取(64 KB 以下)来自查找密集型客户端,它们在庞大的文件中查找小块数据。大读取(超过 512 KB)来自对整个文件的长时间连续读取。
在集群 Y 中,大量读取操作没有返回任何数据。我们的应用程序(尤其是生产系统中的应用程序)经常使用文件作为生产者-消费者队列。生产者同时将数据附加到文件,而消费者则读取文件末尾。偶尔,当消费者超过生产者时,不会返回任何数据。集群 X 出现这种情况的频率较低,因为它通常用于短期数据分析任务,而不是长期分布式应用程序。
写入大小也呈现双峰分布。大写入量(超过 256 KB)通常是写入器内部大量缓冲造成的。写入器缓冲数据较少、检查点或同步频率较高,或仅生成较少数据,则导致写入量较小(64 KB 以下)。
对于记录追加操作,集群 Y 的大记录追加操作比例比集群 X 高得多,因为我们的生产系统使用集群 Y,并且对 GFS 的优化更加积极。
表 5 显示了各种大小的操作中传输的数据总量。对于所有类型的操作,较大的操作(超过 256 KB)通常占传输的字节数的大部分。由于随机寻道工作负载,较小的读取(低于 64 KB)确实传输了一小部分但相当可观的读取数据。
6.3.3 追加与写入
记录追加操作在我们的生产系统中被大量使用。在集群 X 中,按传输字节计算,写操作与记录追加的比率为 108:1,按操作次数计算为 8:1。而在生产系统使用的集群 Y 中,这些比率分别为 3.7:1 和 2.5:1。此外,这些比率表明在两个集群中记录追加操作往往比写操作更大。然而,对于集群 X,在测量期间记录追加的总体使用率相对较低,因此结果可能会因一两个特定选择了特定缓冲区大小的应用程序而有所偏差。
不出所料,我们的数据变更工作量主要是追加而不是覆盖。我们测量了主副本上被覆盖的数据量。这近似于客户端故意覆盖以前写入的数据而不是追加新数据的情况。对于集群 X,覆盖量占变异字节的 0.0001%,占变异操作的 0.0003%。对于集群 Y,这两个比例都是 0.05%。虽然这个比例很小,但仍然高于我们的预期。事实证明,这些覆盖操作大多来自客户端因错误或超时而进行的重试。它们本身并不是工作量的一部分,而是重试机制的结果。
6.3.4 主服务器工作负载
表 6 显示了向主服务器发出的请求类型的明细。大多数请求询问数据块位置(FindLocation)以进行读取,以及租约持有者信息(FindLeaseLocker)以进行数据变更。
集群 X 和 Y 的删除(Delete)请求数量存在显著差异,因为集群 Y 存储的生产数据集会定期重新生成并替换为新版本。部分差异进一步隐藏在打开请求的差异中,因为文件的旧版本可能会通过从头开始打开进行写入(Unix 打开术语中的模式“w”)而被隐式删除。
FindMatchingFiles 是一种支持 “ls” 和类似文件系统操作的模式匹配请求。与主服务器的其他请求不同,它可能需要处理命名空间的很大一部分,因此开销可能较大。集群 Y 上这种请求更为常见,因为自动化数据处理任务通常会检查文件系统的部分内容以了解全局应用状态。相比之下,集群 X 的应用更受用户控制,通常提前知道所需文件的名称。
7. 经历
在建立和部署GFS 的过程中,我们遇到了各种各样的问题,有些是操作问题,有些是技术问题。
最初,GFS 被设计为我们生产系统的后端文件系统。随着时间的推移,其用途逐渐扩展到研究和开发任务。最初它几乎不支持权限和配额之类的功能,但现在包含了这些功能的基本形式。虽然生产系统管理有序且受控,但用户有时并非如此。需要更多的基础设施来防止用户相互干扰。
我们遇到的最大问题与磁盘和 Linux 有关。我们的许多磁盘都向 Linux 驱动程序声称它们支持一系列 IDE 协议版本,但实际上它们只对较新的版本做出可靠响应。由于协议版本非常相似,这些驱动器大部分情况下都能正常工作,但偶尔不匹配会导致驱动器和内核对驱动器状态产生分歧。这会由于内核中的问题而悄无声息地损坏数据。这个问题促使我们使用校验和来检测数据损坏,同时我们修改了内核来处理这些协议不匹配。
之前,我们在使用 Linux 2.2 内核时遇到了一些问题,这是由于 fsync() 的成本所致。其成本与文件大小成正比,而不是与修改部分的大小成正比。这对于我们的大型操作日志来说是一个问题,尤其是在我们实施检查点之前。我们曾一度通过使用同步写入来解决这个问题,并最终迁移到 Linux 2.4。
另一个 Linux 问题是单一的读写锁。在地址空间内的任一线程从磁盘调入页面时(读锁)或在调用 mmap()
时修改地址空间(写锁),都必须持有此锁。在轻负载下,我们的系统出现了瞬时超时现象,并且我们花了很多精力寻找资源瓶颈或偶发的硬件故障。最终发现,该单一锁阻止了主网络线程将新数据映射到内存中,因为磁盘线程正在调入先前映射的数据。由于我们的瓶颈主要在网络接口,而非内存复制带宽,我们通过用 pread()
替换 mmap()
解决了这个问题,代价是增加了一次额外的数据复制。
尽管偶尔会出现问题,但 Linux 代码的可用性一次又一次地帮助我们探索和理解系统行为。在适当的时候,我们会对内核进行改进,并与开源社区分享这些变化。
8. 相关工作
与其他大型分布式文件系统(如 AFS 1)类似,GFS 提供了位置无关的命名空间,使得数据可以为负载平衡或容错而透明地移动。与 AFS 不同的是,GFS 将文件的数据分布在多个存储服务器上,这种方式更类似于 xFS 2 和 Swift 3,以实现聚合性能并提高容错能力。
由于磁盘相对便宜,且复制比更复杂的 RAID 4 方法更简单,GFS 目前仅使用复制来实现冗余,因此比 xFS 或 Swift 消耗更多的原始存储。
与 AFS、xFS、Frangipani 5 和 Intermezzo 6 等系统相比,GFS 不提供文件系统接口下的任何缓存。我们的目标工作负载在单个应用程序运行中几乎没有重用性,因为它们要么流经大型数据集,要么随机在其中查找并每次读取少量数据。
一些分布式文件系统,如 Frangipani、xFS、Minnesota 的 GFS7 和 GPFS 8 移除了集中式服务器,并依靠分布式算法来实现一致性和管理。我们选择集中式方法是为了简化设计、提高可靠性并获得灵活性。特别是,集中式主服务器可以更轻松地实现复杂的块放置和复制策略,因为主服务器已经拥有大部分相关信息并控制其更改方式。我们通过保持主服务器状态较小并在其他机器上完全复制来解决容错问题。我们的影子主服务器机制目前提供可扩展性和高可用性(对于读取)。通过附加到预写日志,可以持久保存对主服务器状态的更新。因此,我们可以采用 Harp 9 中的主副本方案,以提供比我们当前方案具有更强一致性保证的高可用性。
我们正在解决与 Lustre 10 类似的问题,即为大量客户提供总体性能。但是,我们通过专注于应用程序的需求而不是构建符合 POSIX 标准的文件系统,大大简化了这个问题。此外,GFS 假设存在大量不可靠的组件,因此容错是我们设计的核心。
GFS 最接近 NASD 架构 11。虽然 NASD 架构基于网络连接的磁盘驱动器,但 GFS 使用商用机器作为分块服务器,这与 NASD 原型类似。与 NASD 工作不同的是,GFS 的分块服务器使用懒惰分配的固定大小块,而不是可变长度对象。此外,GFS 实现了生产环境中所需的负载均衡、复制和恢复等功能。
与 Minnesota 的 GFS 和 NASD 不同,我们并不寻求改变存储设备的模型。我们专注于利用现有的商品组件满足复杂分布式系统的日常数据处理需求。
由原子记录附加功能启用的生产者-消费者队列解决了与 River 12 中的分布式队列类似的问题。虽然 River 使用分布在机器上的基于内存的队列和谨慎的数据流控制,但 GFS 使用可由许多生产者同时附加的持久文件。River 模型支持 m 对 n 分布式队列,但缺乏持久存储带来的容错能力,而 GFS 仅有效地支持 m 对 1 队列。多个消费者可以读取同一个文件,但他们必须协调以划分传入的负载。
9. 结论
Google 文件系统展示了在商用硬件上支持大规模数据处理工作负载所必需的品质。虽然一些设计决策特定于我们的独特环境,但许多设计决策可能适用于具有类似规模和成本意识的数据处理任务。
我们首先根据当前和预期的应用程序工作负载和技术环境重新审视了传统的文件系统假设。我们的观察结果在设计领域中得出了截然不同的观点。我们将组件故障视为常态而非例外,针对大多数情况下追加(可能并发)然后读取(通常顺序)的大型文件进行优化,并扩展和放宽标准文件系统接口以改进整个系统。
我们的系统通过持续监控、复制关键数据以及快速自动恢复来提供容错能力。块复制使我们能够容忍块服务器故障。这些故障的频率促使我们开发了一种新颖的在线修复机制,该机制定期透明地修复损坏并尽快补偿丢失的副本。此外,我们使用校验和来检测磁盘或 IDE 子系统级别的数据损坏,考虑到系统中的磁盘数量,这种情况变得非常常见。
我们的设计为执行各种任务的众多并发读取器和写入器提供了很高的整体吞吐量。我们通过将文件系统控制(通过主服务器)与数据传输(直接在块服务器和客户端之间传递)分开来实现这一点。通过较大的块大小和块租借(将数据变更的权限委托给主副本),主服务器对常见操作的参与被最小化。这使得简单、集中的主服务器成为可能,而不会成为瓶颈。我们相信,我们网络栈的改进将解除当前单个客户端看到的写入吞吐量的限制。
GFS 成功满足了我们的存储需求,在 Google 内部被广泛用作研发和生产数据处理的存储平台。它是我们能够继续创新和攻克整个网络规模问题的重要工具。
参考文献
John Howard, Michael Kazar, Sherri Menees, David Nichols, Mahadev Satyanarayanan, Robert Sidebotham, and Michael West. Scale and performance in a distributed file system. ACM Transactions on Computer Systems, 6(1):51–81, February 1988. ↩︎
Thomas Anderson, Michael Dahlin, Jeanna Neefe, David Patterson, Drew Roselli, and Randolph Wang. Serverless network file systems. In Proceedings of the 15th ACM Symposium on Operating System Principles, pages 109–126, Copper Mountain Resort, Colorado, December 1995. ↩︎
Luis-Felipe Cabrera and Darrell D. E. Long. Swift: Using distributed diskstriping to provide high I/O data rates. Computer Systems, 4(4):405–436, 1991. ↩︎
David A. Patterson, Garth A. Gibson, and Randy H. Katz. A case for redundant arrays of inexpensive disks (RAID). In Proceedings of the 1988 ACM SIGMOD International Conference on Management of Data, pages 109–116, Chicago, Illinois, September 1988. ↩︎
Chandramohan A. Thekkath, Timothy Mann, and Edward K. Lee. Frangipani: A scalable distributed file system. In Proceedings of the 16th ACM Symposium on Operating System Principles, pages 224–237, Saint-Malo, France, October 1997. ↩︎
InterMezzo. https://www.inter-mezzo.org, 2003. ↩︎
Steven R. Soltis, Thomas M. Ruwart, and Matthew T. O’Keefe. The Gobal File System. In Proceedings of the Fifth NASA Goddard Space Flight Center Conference on Mass Storage Systems and Technologies, College Park, Maryland, September 1996. ↩︎
FrankSchmuckand Roger Haskin. GPFS: A shared-diskfile system for large computing clusters. In Proceedings of the First USENIX Conference on File and Storage Technologies, pages 231–244, Monterey, California, January 2002. ↩︎
Barbara Liskov, Sanjay Ghemawat, Robert Gruber, Paul Johnson, Liuba Shrira, and Michael Williams. Replication in the Harp file system. In 13th Symposium on Operating System Principles, pages 226–238, Pacific Grove, CA, October 1991. ↩︎
Lustre. http://www.lustreorg, 2003. ↩︎
Garth A. Gibson, David F. Nagle, Khalil Amiri, Jeff Butler, Fay W. Chang, Howard Gobioff, Charles Hardin, ErikRiedel, David Rochberg, and Jim Zelenka. A cost-effective, high-bandwidth storage architecture. In Proceedings of the 8th Architectural Support for Programming Languages and Operating Systems, pages 92–103, San Jose, California, October 1998. ↩︎
Remzi H. Arpaci-Dusseau, Eric Anderson, Noah Treuhaft, David E. Culler, Joseph M. Hellerstein, David Patterson, and Kathy Yelick. Cluster I/O with River: Making the fast case common. In Proceedings of the Sixth Workshop on Input/Output in Parallel and Distributed Systems (IOPADS ‘99), pages 10–22, Atlanta, Georgia, May 1999. ↩︎