Paper | The Google File System(2003)

Posted on Aug 10, 2026

The Google File System(2003)

ABSTRACT 摘要

我们设计并实现了一个可扩展的分布式文件系统——Google File System(GFS),用于大型分布式数据密集型应用。它运行在廉价的通用硬件之上,具有容错能力,并能为大量客户端提供极高的聚合性能。

尽管 GFS 与以往的分布式文件系统在许多目标上大体一致,但我们的设计源于对应用负载以及技术环境(包括现状与预判)的观察,这些观察与早期文件系统的一些假设存在显著差异。这促使我们重新审视传统的设计选择,并探索截然不同的设计方向。

该文件系统已成功满足我们的存储需求。它在 Google 内部被广泛部署,作为我们服务的数据生成与处理平台,同时也服务于需要大规模数据集的研发工作。迄今为止,最大的集群通过分布在 1000 多台机器上的数千张磁盘,提供了数百 TB 的存储空间,并同时供数百个客户端并发访问。

在本论文中,我们将展示旨在支持分布式应用的文件系统接口扩展,探讨我们设计的诸多细节,并报告微基准测试以及实际应用中的性能测量数据。

1 INTRODUCTION 介绍

我们设计并实现了 Google 文件系统(Google File System,简称 GFS),以满足 Google 日益增长的数据处理需求。GFS 与以往的分布式文件系统有着许多共同的目标,例如高性能、可扩展性、可靠性和高可用性。然而,它的设计是由我们对当前及预期的应用负载和技术环境的若干关键观察所驱动的,这些观察体现出了与早期文件系统设计假设的显著不同。我们重新审视了传统的设计选择,并在设计空间中探索了截然不同的方向。

第一,组件故障是常态,而非例外。 该文件系统由数百甚至数千台由廉价通用部件构建的存储机器组成,并由数量相当Client(客户端)机器进行访问。组件的数量和质量实际上保证了在任何给定时刻都会有一些部件无法正常工作,且有些故障是无法恢复的。我们观察到了由应用程序 Bug、操作系统 Bug、人为失误,以及硬盘、内存、连接线、网络和电源故障所导致的问题。因此,持续监控、错误检测、容错机制和自动恢复必须成为系统不可分割的一部分。

第二,按照传统标准,文件是巨大无比的。 几个 GB 大小的文件非常普遍。每个文件通常包含许多应用程序对象(例如网页文档)。当我们日常处理包含数十亿对象、且快速增长的数 TB 级数据集时,即便文件系统能够支持,管理数十亿个 KB 级别大小的文件也会变得非常笨重。因此,诸如 I/O 操作和块大小(blocksize)等设计假设与参数必须被重新审视。

第三,大多数文件是通过追加新数据而非覆盖现有数据来进行修改的。 文件内部的随机写入实际上几乎不存在。文件一旦写入,就只会被读取,且通常是顺序读取。各种各样的数据都具备这些特征。有些可能构成数据分析程序扫描的大型存储库;有些可能是正在运行的应用持续产生的数据流;有些可能是归档数据;还有些可能是在一台机器上产生并在另一台机器上处理(无论是同时还是稍后)的中间结果。考虑到巨型文件上的这种访问模式,追加(append)操作成为了性能优化和原子性保证的焦点,而客户端的数据块缓存(caching)则失去了吸引力。

第四,联合设计(co-designing)应用程序和文件系统 API,能够通过增加灵活性来提升整个系统的效益。 例如,我们放宽了 GFS 的一致性模型,在不给应用程序增加繁重负担的前提下,极大地简化了文件系统。我们还引入了原子追加(atomic append)操作,以便多个客户端可以并发地向同一个文件追加数据,而无需它们之间进行额外同步。这些内容将在本论文后文中更详细地讨论。

目前我们部署了多个用于不同用途的 GFS 集群。其中最大的集群拥有 1000 多个存储节点、超过 300 TB 的磁盘存储空间,并持续受到来自不同机器上的数百个客户端的高强度访问。

2 DESIGN OVERVIEW 设计概览

2.1 Assumptions 假设

为了设计满足我们需求的系统,我们一直受一些既带来挑战又带来机遇的假设所指引。上文中我们已经提到了某些关键观察,现在将更详细地阐述我们的假设。

  • 系统由许多经常发生故障的廉价通用部件构建而成。它必须能够持续地自我监控,并在日常运行中对组件故障进行及时检测、容错和恢复。
  • 系统存储数量适中的大文件。我们预计会有几百万个文件,每个文件的大小通常在 100 MB 或以上。几个 GB 大小的文件是普遍情况,应当得到高效管理。系统必须支持小文件,但我们无需对其进行优化。
  • 工作负载主要由两种读取操作组成:大规模的流式读取和小规模的随机读取。在大规模流式读取中,单次操作通常读取数百 KB,更常见的是 1 MB 或更多。来自同一客户端的连续操作通常会顺序读取文件的连续区域。小规模随机读取通常在任意偏移量处读取几 KB。对性能敏感的应用程序通常会对小规模读取进行批处理和排序,从而在文件中稳步推进,而不是前后反复穿梭。
  • 工作负载还包含许多向文件追加数据的大规模顺序写入。典型的操作尺寸与读取操作类似。文件一旦写入,就极少再被修改。系统支持在文件任意位置进行小规模写入,但不必保证其高效性。
  • 系统必须为多个并发向同一文件追加数据的客户端高效地实现明确的语义。我们的文件经常被用作生产者-消费者队列或用于多路合并。数百个生产者(每台机器运行一个)会并发地向一个文件追加数据。以极小的同步开销保证原子性至关重要。这些文件可能会在稍后被读取,或者由消费者同时进行读取。
  • 持续的高带宽比低延迟更重要。我们的绝大多数目标应用都高度看重以高速率批量处理数据的能力,而极少有应用对单次读写操作有严格的响应时间要求。

2.2 Interface 接口

GFS 提供了一个令人熟悉的文件系统接口,尽管它并没有实现像 POSIX 这样的标准 API。文件在目录中按层次结构进行组织,并通过路径名进行标识。我们支持常见的操作:创建(create)、删除(delete)、打开(open)、关闭(close)、读取(read)和写入(write)文件。

此外,GFS 还提供了快照(snapshot)和记录追加(record append)操作。快照能够以极低的成本创建文件或目录树的副本。记录追加则允许多个客户端并发地向同一个文件追加数据,同时保证每个单独客户端追加操作的原子性。这对于实现多路合并结果以及生产者-消费者队列非常有用,许多客户端可以同时向其追加数据而无需额外的加锁。我们发现这类文件在构建大型分布式应用时具有无法估量的价值。快照和记录追加将分别在第 3.4 节和第 3.3 节中作进一步讨论。

2.3 Architecture 架构

image.png

一个 GFS 集群由单个 Master(主服务器) 和多个 Chunkserver(数据块服务器) 组成,并被多个 Client(客户端) 访问,如图 1 所示。其中每个角色通常都是运行在普通 Linux 机器上的用户级服务进程。只要机器资源允许,且能够接受因运行可能不够稳定的应用程序代码而导致的可靠性降低,在同一台机器上同时运行 Chunkserver 和 Client 是很容易实现的。

文件被划分为固定大小的 Chunk(数据块)。在创建 Chunk 时,Master 会为其分配一个不可变且全局唯一的 64 位 Chunk Handle(数据块句柄)。Chunkserver 将 Chunk 作为 Linux 文件存储在本地磁盘上,并根据指定的 Chunk Handle 和字节范围读取或写入 Chunk 数据。为了保证可靠性,每个 Chunk 都会在多个 Chunkserver 上进行副本复制。默认情况下,我们存储三个副本,但用户可以为文件命名空间的不同区域指定不同的副本级别。

Master 维护着所有文件系统的 Metadata(元数据)。这包括命名空间、访问控制信息、从文件到 Chunk 的映射关系,以及 Chunk 当前的位置信息。它还控制着全系统范围的活动,例如 Chunk 租约管理(lease management)、孤儿 Chunk 的垃圾回收(garbage collection),以及 Chunkserver 之间的 Chunk 迁移。Master 定期通过 HeartBeat(心跳) 消息与各个 Chunkserver 进行通信,向其发送指令并收集其状态。

链接到每个应用程序中的 GFS 客户端代码实现了文件系统 API,并代表应用程序与 Master 及 Chunkserver 通信以读取或写入数据。客户端与 Master 进行交互以执行元数据操作,但所有包含实际数据的通信则直接发送给 Chunkserver。我们不提供 POSIX API,因此不需要挂接到 Linux 的 vnode 层。

客户端和 Chunkserver 都不缓存文件数据。客户端缓存带来的收益微乎其微,因为大多数应用程序要么是流式读取巨型文件,要么是工作集太大而无法进行缓存。不使用缓存消除了一致性问题,从而简化了客户端和整个系统。(不过,客户端确实会缓存元数据。)Chunkserver 不需要缓存文件数据,因为 Chunk 是作为本地文件存储的,Linux 的页缓存(buffer cache)已经将频繁访问的数据保留在了内存中。

2.4 Single Master 单主服务器

采用单一 Master 极大地简化了我们的设计,并使 Master 能够利用全局信息做出精细的 Chunk 放置和副本复制决策。然而,我们必须尽量减少它对读写操作的参与,以免其成为系统的瓶颈。客户端绝不会通过 Master 来读取和写入文件数据。相反,客户端会向 Master 询问应当联系哪些 Chunkserver。客户端将此信息缓存一段有限的时间,并在后续的许多操作中直接与那些 Chunkserver 进行交互。

让我们结合图 1 来解释一次简单读取操作的交互过程。首先,利用固定的 Chunk 大小,客户端将应用程序指定的“文件名”和“字节偏移量”转换为文件内部的“Chunk 索引(chunk index)”。然后,它向 Master 发送一个包含文件名和 Chunk 索引的请求。Master 回复对应的 Chunk Handle 以及各个副本的位置信息。客户端以“文件名 + Chunk 索引”作为键(key)将这些信息缓存起来。

随后,客户端向其中一个副本(通常是距离最近的那个)发送请求。该请求指定了 Chunk Handle 以及该 Chunk 内部的字节范围。在缓存的信息过期或文件被重新打开之前,对同一 Chunk 的后续读取不再需要客户端与 Master 发生交互。事实上,客户端通常会在同一次请求中询问多个 Chunk,而 Master 也可以一并附带紧随请求之后那些 Chunk 的信息。这些额外的信息几乎没有增加额外的成本,却能规避未来数次客户端与 Master 之间的交互。

2.5 Chunk Size 数据块大小

数据块大小(Chunk size)是关键的设计参数之一。我们选择了 64 MB,这比典型文件系统的块大小要大得多。每个 Chunk 副本在 Chunkserver 上都作为普通的 Linux 文件存储,并仅在需要时才扩展。这种延迟空间分配(lazy space allocation)避免了因内部碎片而造成空间浪费,而内部碎片或许是对如此巨大 Chunk 大小的最大质疑。

巨大的 Chunk 大小带来了几个重要的好处。首先,它减少了客户端与 Master 交互的需求,因为对同一个 Chunk 的读取和写入只需要向 Master 发送一次初始请求以获取 Chunk 位置信息。这种减少对我们的工作负载尤为显著,因为应用程序大多顺序读取和写入大文件。即使对于小规模的随机读取,客户端也可以轻松缓存多 TB 工作集的所有 Chunk 位置信息。其次,由于在巨大的 Chunk 上,客户端更有可能对给定的 Chunk 执行多次操作,因此它可以通过在较长时间内与 Chunkserver 保持长 TCP 连接来减少网络开销。第三,它减少了存储在 Master 上的元数据的大小。这使我们能够将元数据保留在内存中,这反过来又带来了我们将在第 2.6.1 节中讨论的其他优势。

另一方面,巨大的 Chunk 大小即使采用了延迟空间分配,也有其缺点。一个小文件仅由少量的 Chunk 组成,可能只有一个。如果许多客户端正在访问同一个文件,保存这些 Chunk 的 Chunkserver 可能会成为热点(hot spots)。在实践中,热点并不是主要问题,因为我们的应用程序大多顺序读取多 Chunk 的大文件。

然而,当 GFS 最初被一个批处理队列系统使用时,确实出现了热点问题:一个可执行文件作为单个 Chunk 的文件写入 GFS,然后同时在数百台机器上启动。存储该可执行文件的少数几台 Chunkserver 因数百个同时发起的请求而超载。我们通过使用更高的副本系数存储此类可执行文件,以及让批处理队列系统错开应用程序的启动时间解决了这个问题。一个潜在的长期解决方案是允许客户端在此类情况下从其他客户端读取数据。

2.6 Metadata 元数据

Master 存储了三种主要类型的元数据:文件和 Chunk 的命名空间(namespaces)、从文件到 Chunk 的映射关系,以及每个 Chunk 副本的位置信息。所有元数据都保存在 Master 的内存中。前两种类型(命名空间以及文件到 Chunk 的映射)还会通过将变更记录写入存储在 Master 本地磁盘并复制到远程机器上的操作日志(operation log)中来实现持久化。使用日志使我们能够简单、可靠地更新 Master 的状态,并且不会在 Master 崩溃时引发不一致的风险。Master 不会持久化存储 Chunk 的位置信息。相反,它会在 Master 启动时以及每当有 Chunkserver 加入集群时,向各个 Chunkserver 询问其拥有的 Chunk 信息。

2.6.1 In-Memory Data Structures 内存数据结构

由于元数据存储在内存中,Master 的操作非常快速。此外,Master 可以轻松且高效地在后台定期扫描其整个状态。这种定期扫描被用于实现 Chunk 的垃圾回收(garbage collection)、在 Chunkserver 发生故障时进行重新复制(re-replication),以及为了平衡各个 Chunkserver 之间的负载和磁盘空间使用而进行的 Chunk 迁移(chunk migration)。第 4.3 节和第 4.4 节将对这些活动作进一步讨论。

对于这种纯内存(memory-only)的处理方式,一个潜在的担忧是 Chunk 的数量以及整个系统的容量会受到 Master 内存大小的限制。在实践中,这并不是一个严重的限制。对于每个 64 MB 的 Chunk,Master 仅维护少于 64 字节的元数据。大多数 Chunk 都是装满的,因为大多数文件包含许多 Chunk,仅最后一个 Chunk 可能只装了一部分。类似地,文件命名空间数据通常每个文件需要的空间也少于 64 字节,因为系统采用前缀压缩(prefix compression)紧凑地存储文件名。

如果为了支持更大的文件系统而有此必要,为 Master 增加额外内存的成本,与我们通过将元数据保存在内存中所获得的简单性、可靠性、性能和灵活性相比,是一个微不足道的代价。

2.6.2 Chunk Locations Chunk 位置信息

Master 不会持久化记录哪些 Chunkserver 保存了给定 Chunk 的副本。它只是在启动时向各个 Chunkserver 轮询这些信息。在此之后, Master 能够保持信息的最新状态,因为所有的 Chunk 放置均由它控制,并且它会通过定期的心跳(HeartBeat)消息监控 Chunkserver 的状态。

起初,我们曾尝试在 Master 上持久化保存 Chunk 的位置信息,但我们最终决定,在启动时以及此后定期向 Chunkserver 请求这些数据要简单得多。这消除了当 Chunkserver 加入和离开集群、更改名称、发生故障、重新启动等情况时,保持 Master 与 Chunkserver 同步的问题。在一个拥有数百台服务器的集群中,这些事件发生得太频繁了。

理解这一设计决策的另一种方式是意识到:Chunkserver 对于其自身磁盘上拥有或不拥有哪些 Chunk 具有最终决定权。 在 Master 上试图维持该信息的一致视图是没有意义的,因为 Chunkserver 上的错误可能会导致 Chunk 无故消失(例如,一块磁盘可能坏掉并被禁用),或者运维人员可能会重命名一台 Chunkserver。

2.6.3 Operation Log 操作日志

操作日志包含关键元数据变更的历史记录。它是 GFS 的核心。它不仅是元数据的唯一持久化记录,而且还充当了定义并发操作顺序的逻辑时间线。文件和 Chunk,以及它们的版本(见第 4.5 节),都是由它们被创建时的逻辑时间来唯一且永久地标识的。

由于操作日志至关重要,我们必须将其可靠地存储,并且在元数据变更完成持久化之前,不能让这些变更对客户端可见。否则,即使 Chunk 本身完好无损,我们也实际上失去了整个文件系统或最近的客户端操作。因此,我们在多台远程机器上对日志进行副本复制,并且只有在将对应的日志记录刷新(flush)到本地和远程磁盘之后,才向客户端响应操作请求。Master 会将多条日志记录批量组合在一起后再进行刷新,从而减少刷新和复制对系统整体吞吐量的影响。

Master 通过重放(replaying)操作日志来恢复其文件系统状态。为了尽量减少启动时间,我们必须保持日志文件较小。每当日志增长超过一定大小时,Master 就会对它的状态进行检查点(checkpoint)操作,这样它就可以通过从本地磁盘加载最新的检查点,并仅重放该检查点之后有限数量的日志记录来进行恢复。检查点采用紧凑的类似 B 树(B-tree)的形式,可以直接映射到内存中,并用于命名空间查找,而无需额外的解析。这进一步加快了恢复速度并提高了可用性。

由于创建检查点可能会耗费一些时间,Master 的内部状态经过精心结构设计,使得可以在不延迟新传入的变更操作的前提下创建新的检查点。Master 会切换到一个新的日志文件,并在一个独立的线程中创建新的检查点。新的检查点包含了切换前所有的变更。对于一个拥有数百万个文件的集群,创建检查点大约只需要一分钟左右。完成后,检查点会被同时写入本地和远程磁盘。

恢复只需要最新的完整检查点以及后续的日志文件。旧的检查点和日志文件可以被自由删除,尽管我们会保留一些以防万一出现灾难性故障。在创建检查点期间发生的故障不会影响正确性,因为恢复代码能够检测并跳过不完整的检查点。

2.7 Consistency Model 一致性模型

GFS 采用了宽松的一致性模型,该模型能够很好地支持高度分布式的应用,同时在实现上保持相对简单和高效。现在我们将讨论 GFS 提供的保证及其对应用程序意味着什么。我们还将重点阐述 GFS 是如何维持这些保证的,但会将详细细节留给本论文的其他部分。

2.7.1 Guarantees by GFS GFS 提供的保证

image.png

文件命名空间的修改(例如创建文件)是原子性的。它们完全由 Master 处理:命名空间锁保证了原子性和正确性(第 4.1 节);Master 的操作日志则定义了这些操作的全局总顺序(第 2.6.3 节)。

数据修改后文件区域的状态取决于修改的类型、修改成功与否以及是否存在并发修改。表 1 总结了这些结果。如果无论从哪个副本读取,所有客户端始终能看到相同的数据,则该文件区域是一致的(consistent)。如果在数据修改后,文件区域是一致的,且客户端能够完整看到本次修改所写入的内容,则该区域是已定义的(defined)。当一次修改在没有并发写入干扰的情况下成功完成时,受影响的区域就是已定义的(暗示也是一致的):所有客户端将始终看到该修改所写入的内容。并发的成功修改会使该区域处于未定义(undefined)但一致的状态:所有客户端都能看到相同的数据,但数据可能无法反映任何单次修改所写入的完整内容。通常,它由来自多次修改的交错片段混合而成。失败的修改会使该区域陷入不一致(inconsistent)的状态(因此也是未定义的):不同的客户端可能会在不同的时间看到不同的数据。我们在下文中将阐述应用程序如何区分已定义区域与未定义区域。应用程序无需进一步区分不同种类的未定义区域。

数据修改可以是写入(writes)或记录追加(record appends)。写入会导致数据被写入到应用程序指定的某文件偏移量处。记录追加则会使数据(即“记录”)即使在存在并发修改的情况下,也能以原子方式被至少追加一次,但追加的位置是由 GFS 选择的偏移量(第 3.3 节)。(相比之下,“普通”追加仅仅是在客户端认为是当前文件末尾的偏移量处进行写入。)该偏移量会返回给客户端,并标志着包含该记录的已定义区域的起始位置。此外,GFS 可能会在中间插入填充数据(padding)或重复记录。它们占用的区域被认为是不一致的,且其数量通常相比用户数据总量微乎其微。

在一连串成功的修改之后,被修改的文件区域保证是已定义的,并且包含最后一次修改所写入的数据。GFS 通过以下方式实现这一点:(a) 在所有副本上按相同的顺序将修改应用到 Chunk(第 3.1 节);(b) 使用 Chunk 版本号来检测因 Chunkserver 宕机而错过修改的过时副本(第 4.5 节)。过时的副本绝不会参与修改,也不会在客户端向 Master 询问 Chunk 位置时提供给客户端。它们会在最早的机会被垃圾回收。

由于客户端会缓存 Chunk 位置,因此在信息刷新之前,它们可能会从过时的副本中读取数据。这个时间窗口受限于缓存条目的超时时间,以及下一次打开该文件时对该文件所有 Chunk 缓存信息的清空。此外,由于我们的绝大多数文件都是仅追加(append-only)的,过时的副本通常只会返回一个尚未更新的 Chunk 结尾,而不是旧的错误数据。当读取者重试并联系 Master 时,它将立刻获得最新的 Chunk 位置。

在一次成功的修改过去很久之后,组件故障当然仍可能损坏或毁坏数据。GFS 通过 Master 与所有 Chunkserver 之间的定期握手来识别故障的 Chunkserver,并通过校验和(checksumming)来检测数据损坏(第 5.2 节)。一旦问题显现,数据会尽快从有效副本中恢复(第 4.3 节)。只有当一个 Chunk 的所有副本都在 GFS 做出反应前(通常在几分钟之内)全部丢失,该 Chunk 才会不可逆转地丢失。即便在这种情况下,它也只是变得不可用,而不是被损坏:应用程序收到的是明确的错误报告,而不是被损坏的数据。

2.7.2 Implications for Applications 对应用程序的影响

GFS 应用程序可以通过几项简单的技术来适应这种宽松的一致性模型,而这些技术本身在其他场合就已经需要用到:依赖追加而非覆盖、建立检查点(checkpointing),以及编写自我校验和自我标识的记录。

实际上,我们几乎所有的应用程序都是通过追加数据而非覆盖数据来修改文件的。在一种典型的用法中,写入者从头到尾生成一个文件。在写完所有数据后,它会以原子方式将文件重命名为一个永久名称,或者定期对已经成功写入的数据量建立检查点。检查点还可以包含应用层面的校验和(checksum)。读取者只校验并处理截至最新检查点之前的写入区域,这部分区域已知处于已定义(defined)状态。无论一致性和并发问题如何,这种方法都成效显著。相比于随机写入,追加操作效率高得多,且对应用故障具有更强的容错能力。建立检查点允许写入者以增量方式重启,并能防止读取者去处理那些从应用视角来看仍不完整的已成功写入数据。

在另一种典型的用法中,许多写入者并发地向同一个文件追加数据,用于合并结果或作为生产者-消费者队列。记录追加(record append)所提供的“至少追加一次(append-at-least-once)”语义保留了每个写入者的输出。读取者按如下方式处理偶发的填充数据(padding)和重复记录:由写入者准备的每条记录都包含诸如校验和等额外信息,以便验证其有效性。读取者可以使用校验和来识别并丢弃额外的填充数据和记录碎片。如果应用无法容忍偶发的重复记录(例如,如果它们会触发非幂等操作),则可以使用记录中的唯一标识符将其过滤掉,而这些标识符通常本就需要用于命名对应的应用实体(如网页文档)。这些针对记录 I/O 的功能(去重除外)都实现在我们的应用程序所共享的库代码中,并同样适用于 Google 内部的其他文件接口实现。由此,相同的记录序列(加上极少数重复项)总能被送达给记录读取者。

3 SYSTEM INTERACTIONS 系统交互

我们在设计系统时,尽量减少了 Master 在所有操作中的参与。基于这一背景,我们现在将阐述客户端(client)、Master 以及 Chunkserver 之间是如何相互协作,来实现数据修改(data mutations)、原子记录追加(atomic record append)以及快照(snapshot)功能的。

3.1 Leases and Mutation Order 租约与修改顺序

image.png

修改(mutation)是指改变 Chunk 的内容或元数据的操作,例如写入或追加操作。每次修改都会在该 Chunk 的所有副本上执行。我们使用租约(leases)来维持跨副本的一致修改顺序。Master 会将一个 Chunk 租约授予其中一个副本,我们称之为主副本(primary)。主副本为对该 Chunk 的所有修改挑选一个串行顺序。所有副本在应用修改时都遵循这一顺序。因此,全局的修改顺序首先由 Master 选择的租约授予顺序来定义,而在同一租约内则由主副本分配的序列号来定义。

租约机制旨在最大程度地减少 Master 上的管理开销。租约的初始超时时间为 60 秒。然而,只要 Chunk 正在被修改,主副本就可以向 Master 请求并通常可以无限期地获得租约延长。这些延长请求和授予都是搭便车(piggybacked)在 Master 与所有 Chunkserver 之间定期交换的心跳消息中的。Master 有时可能会尝试在租约到期之前将其撤销(例如,当 Master 想要禁用对某个正在被重命名的文件的修改时)。即使 Master 失去了与主副本的通信,它也可以在旧租约到期后,安全地将新租约授予另一个副本。

在图 2 中,我们通过沿着一次写入操作的控制流,按以下编号步骤来阐述这一过程:

  1. 客户端向 Master 询问哪台 Chunkserver 持有该 Chunk 的当前租约,以及其他副本的位置。如果没有节点持有租约,Master 会将其授予它所选择的一个副本(未在图中展示)。
  2. Master 回复主副本的标识以及其他(副)副本的位置。客户端会将这些数据缓存起来,以便后续修改时使用。只有当主副本变得不可达或者回复称其不再持有租约时,客户端才需要再次联系 Master。
  3. 客户端将数据推送(push)到所有的副本。客户端可以按任何顺序执行此操作。每台 Chunkserver 都会将数据保存在内部的 LRU 缓冲区缓存中,直到数据被使用或老化淘汰。通过将数据流与控制流解耦,我们可以根据网络拓扑结构来调度昂贵的数据传输,而不必考虑哪台 Chunkserver 是主副本,从而提升性能。第 3.2 节对此作了进一步讨论。
  4. 一旦所有副本都确认收到了数据,客户端就会向主副本发送一个写入请求。该请求标识了早前推送给所有副本的数据。主副本为其接收到的所有修改(可能来自多个客户端)分配连续的序列号,从而提供了必要的串行化。它按照序列号顺序将修改应用到自身的本地状态。
  5. 主副本将写入请求转发给所有副副本。每个副副本都按照主副本分配的相同序列号顺序应用修改。
  6. 副副本均向主副本回复,表明它们已完成操作。
  7. 主副本向客户端进行回复。在任何副本上遇到的任何错误都会向客户端报告。在发生错误的情况下,写入可能在主副本以及副副本的一个任意子集中取得了成功。(如果写入在主副本上就失败了,它就不会被分配序列号也不会被转发。)客户端请求被视为已失败,受影响的修改区域陷入不一致状态。我们的客户端代码通过重试失败的修改来处理此类错误。它会在退回到从头重新开始写入之前,先对步骤 (3) 到 (7) 进行几次重试尝试。

如果应用程序进行的一次写入量很大或者跨越了 Chunk 边界,GFS 客户端代码会将其拆分为多次写入操作。它们都遵循上述的控制流,但可能会与其他客户端的并发操作交错(interleaved)并被其覆盖。因此,共享的文件区域最终可能会包含来自不同客户端的片段,尽管由于单个操作在所有副本上都按照相同的顺序成功完成,副本之间仍将完全相同。正如第 2.7 节所述,这会使文件区域处于一致(consistent)但未定义(undefined)的状态。

3.2 Data Flow 数据流

我们将数据流与控制流解耦,以高效利用网络。控制流从客户端流向主副本,再流向所有副副本;而数据则是以流水线(pipelined)的方式,沿着精心挑选的 Chunkserver 链条进行线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟链路,并最小化推送所有数据所需的延迟。

为了充分利用每台机器的网络带宽,数据是沿着一条 Chunkserver 链条线性推送的,而不是采用其他拓扑结构(如树状)进行分发。因此,每台机器的全部出口带宽都用于尽可能快地传输数据,而不是被分摊给多个接收方。

为了尽可能避免网络瓶颈和高延迟链路(例如,交换机间的链路通常两者兼有),每台机器都会将数据转发给网络拓扑中离它“最近”且尚未收到数据的机器。假设客户端要将数据推送到 Chunkserver S1 到 S4。它会将数据发送给离它最近的 Chunkserver,假设是 S1。S1 会将数据转发给在 S2 到 S4 中离 S1 最近的 Chunkserver,假设是 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 内分发完毕。

3.3 Atomic Record Appends 原子记录追加

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

记录追加在我们分布式应用中被频繁使用,在这些应用中,不同机器上的许多客户端会并发地向同一个文件进行追加。如果采用传统的写入方式,客户端将需要额外复杂且昂贵的同步机制(例如通过分布式锁管理器)。在我们的工作负载中,此类文件通常用作多生产者/单消费者队列,或者包含来自许多不同客户端的合并结果。

记录追加是一种修改(mutation)操作,遵循第 3.1 节中的控制流,仅在主副本处增加了一点额外逻辑。客户端将数据推送到文件最后一个 Chunk 的所有副本上,然后向主副本发送请求。主副本会检查将该记录追加到当前 Chunk 是否会导致该 Chunk 超过最大尺寸(64 MB)。如果是,主副本会对该 Chunk 进行填充(pad)使其达到最大尺寸,告知副副本执行相同的填充操作,并回复客户端指出该操作应在下一个 Chunk 上重试。(为了将最坏情况下的碎片率控制在可接受的水平,记录追加的大小被限制为最多为最大 Chunk 尺寸的四分之一。)如果记录大小在最大尺寸范围内(这是常见情况),主副本会将数据追加到其自身的副本中,通知副副本在它追加的精确偏移量处写入数据,最后向客户端回复成功。

如果记录追加在任何一个副本上失败,客户端会重试该操作。结果是,同一个 Chunk 的不同副本可能会包含不同的数据,可能包括相同记录的完整或部分重复项。GFS 不保证所有副本在字节层面上完全相同。它仅保证数据作为原子单元至少被写入一次。这一特性可以从一个简单的观察中容易推出:要使该操作报告成功,数据必须已在某个 Chunk 的所有副本上的相同偏移量处写入完成。此外,在此之后,所有副本的长度至少与该记录的末尾一样长,因此即使稍后由不同的副本担任主副本,后续的任何记录也都会被分配一个更高的偏移量或不同的 Chunk。就我们的一致性保证而言,成功的记录追加操作写入数据的区域是已定义的(defined,因而也是一致的),而夹在中间的交错区域则是不一致的(inconsistent,因而也是未定义的)。正如我们在第 2.7.2 节中所讨论的,我们的应用程序能够妥善处理这些不一致区域。

3.4 Snapshot 快照

快照操作能几乎瞬间对文件或目录树(“源”)进行复制,同时将对正在进行的修改操作的干扰降至最低。我们的用户使用它来快速创建海量数据集的分支副本(通常是递归地创建这些副本的副本),或者在尝试进行修改之前对当前状态建立检查点,以便后续可以轻松提交或回滚这些修改。

与 AFS [5] 类似,我们使用标准的写时复制(copy-on-write)技术来实现快照。当 Master 收到快照请求时,它首先会撤销即将进行快照的文件中各个 Chunk 上所有未到期的租约(leases)。这确保了后续对这些 Chunk 的任何写入都必须与 Master 进行交互以寻找租约持有者,从而使 Master 能够获得先创建 Chunk 新副本的机会。

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

在快照操作之后,当客户端第一次想要写入某个 Chunk C 时,它会向 Master 发送请求以查找当前的租约持有者。Master 会注意到 Chunk C 的引用计数大于 1。它会延迟回复客户端的请求,转而挑选一个新的 Chunk 句柄 C’。然后,它指示保存有 Chunk C 当前副本的每台 Chunkserver 创建一个名为 C’ 的新 Chunk。通过在与原始 Chunk 相同的 Chunkserver 上创建新 Chunk,我们确保了数据可以在本地进行复制,而无需通过网络(我们的磁盘速度大约是 100 Mb 以太网链路的三倍)。从此刻开始,请求处理与处理任何普通 Chunk 没有任何不同:Master 将新 Chunk C’ 的租约授予其中一个副本并回复客户端,客户端随后可以正常写入该 Chunk,完全不知道它刚刚是由一个已有的 Chunk 创建出来的。

4 MASTER OPERATION Master 操作

Master 执行所有的命名空间操作。此外,它还管理整个系统中的 Chunk 副本:它做出放置决策、创建新的 Chunk 并由此创建副本,并协调各种系统范围内的活动,以保持 Chunk 被充分复制、平衡所有 Chunkserver 之间的负载,并回收未使用的存储空间。我们现在将对其中每个主题进行讨论。

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

许多 Master 操作可能会耗费很长时间:例如,快照操作必须撤销快照所涵盖的所有 Chunk 上的 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 的数据结构需要保护以防止被修改。路径名称上的读锁足以保护父目录不被删除。

这种加锁方案的一个良好特性是,它允许在同一个目录下进行并发修改。例如,可以在同一个目录下并发执行多个文件创建操作:每个操作获取目录名上的读锁和文件名上的写锁。目录名上的读锁足以防止目录被删除、重命名或进行快照。而文件名上的写锁则能将两次尝试创建同名文件的操作串行化。

由于命名空间可能包含许多节点,因此读写锁对象是延迟(lazily)分配的,一旦不再使用就会被删除。此外,锁是按照一致的全局总顺序获取的,以防止死锁:首先按照在命名空间树中的层级排序,同级树节点之间则按照字典序排序。

4.2 Replica Placement 副本放置

GFS 集群在多个层面上都是高度分布式的。它通常拥有数百台 Chunkserver,分散在许多机器机架(racks)中。而来自相同或不同机架的数百个客户端,又会依次访问这些 Chunkserver。不同机架上的两台机器之间的通信可能会跨越一个或多个网络交换机。此外,进出某个机架的带宽可能会小于该机架内所有机器的聚合带宽。这种多层级的分布为分发数据以实现可扩展性、可靠性和可用性带来了独特的挑战。

Chunk 副本放置策略有两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽利用率。为了同时实现这两个目标,仅仅将副本分散在不同的机器上是不够的,这只能防范磁盘或机器故障,并充分利用单台机器的网络带宽。我们还必须将 Chunk 副本跨机架分散放置。这确保了即使整个机架损坏或离线(例如,由于网络交换机或电源电路等共享资源发生故障),某个 Chunk 的部分副本仍能幸存并保持可用。这也意味着对某个 Chunk 的流量(尤其是读取流量)可以利用多个机架的聚合带宽。另一方面,写入流量必须跨越多个机架流动,这是我们心甘情愿做出的权衡(tradeoff)。

4.3 Creation, Re-replication, Rebalancing 创建、重新复制、重新负载均衡

Chunk 副本会因三种原因被创建:Chunk 创建、重新复制(re-replication)和重新负载均衡(rebalancing)。

当 Master 创建(creates)一个 Chunk 时,它会选择在何处放置初始为空的副本。它会考虑几个因素:(1) 我们希望把新副本放在磁盘空间利用率低于平均水平的 Chunkserver 上。随着时间的推移,这将使各个 Chunkserver 的磁盘利用率趋于平均。(2) 我们希望限制每台 Chunkserver 上“近期”创建的数量。虽然创建操作本身开销很低,但它能可靠地预示着即将来临的高写入流量,因为 Chunk 是在写入有需求时才创建的;并且在我们的“一次追加、多次读取”工作负载中,Chunk 一旦被完整写入,通常实际上就变成了只读状态。(3) 如前所述,我们希望将一个 Chunk 的各个副本跨机架分散放置。

一旦可用副本的数量低于用户指定的精细目标,Master 就会对该 Chunk 进行重新复制(re-replicates)。这可能因多种原因而发生:一台 Chunkserver 变得不可用;它报告其副本可能已损坏;它的一个磁盘因错误被禁用;或者副本数量目标被提高了。每个需要被重新复制的 Chunk 都会根据几个因素排定优先级。一是它距离副本目标的差距有多大。例如,相比于只丢失了一个副本的 Chunk,我们给丢失了两个副本的 Chunk 赋予更高的优先级。此外,我们倾向于优先重新复制活跃(live)文件的 Chunk,而不是属于近期已被删除文件的 Chunk(参见第 4.4 节)。最后,为了最小化故障对正在运行的应用程序的影响,我们会提高任何正在阻塞客户端进度的 Chunk 的优先级。

Master 会挑选优先级最高的 Chunk,并指示某些 Chunkserver 直接从现有的有效副本中复制 Chunk 数据,从而“克隆(clones)”该 Chunk。新副本的放置目标与创建时类似:均衡磁盘空间利用率、限制单台 Chunkserver 上活跃的克隆操作数量,以及跨机架分散副本。为了防止克隆产生的流量压垮客户端流量,Master 限制了整个集群以及每台 Chunkserver 上活跃克隆操作的数量。此外,每台 Chunkserver 还会通过限制对源 Chunkserver 的读取请求,来限制其在每个克隆操作上消耗的带宽。

最后,Master 会定期对副本进行重新负载均衡(rebalances):它检查当前的副本分布情况,并移动副本以获得更好的磁盘空间和负载均衡。此外,通过这个过程,Master 会逐渐填满一台新的 Chunkserver,而不是瞬间用新 Chunk 和随之而来巨大写入流量淹没它。新副本的放置标准与上面讨论的类似。此外,Master 还必须选择要移除哪一个已有的副本。通常情况下,它更倾向于移除那些位于剩余空闲空间低于平均水平的 Chunkserver 上的副本,以此来均衡磁盘空间的使用。

4.4 Garbage Collection 垃圾回收

在文件被删除后,GFS 不会立刻回收可用的物理存储空间。它仅在针对文件和 Chunk 层面的定期垃圾回收过程中延迟(lazily)执行这一操作。我们发现这种方法使系统变得简单得多且更加可靠。

4.4.1 Mechanism 机制

当应用程序删除一个文件时,Master 会像记录其他修改一样立刻将该删除操作记录到日志中。然而,文件并没有被立即回收资源,而只是被重命名为一个包含删除时间戳的隐藏名称。在 Master 对文件系统命名空间进行的定期扫描期间,如果这些隐藏文件已存在超过三天(该时间间隔是可配置的),Master 就会将其移除。在此之前,仍然可以通过这个新的特殊名称读取该文件,并且可以通过将其重命名回正常名称来取消删除(undelete)。当隐藏文件从命名空间中被移除时,它在内存中的元数据就会被擦除。这有效地切断了它与所有 Chunk 的关联。

在对 Chunk 命名空间进行的类似定期扫描中,Master 会识别出孤儿 Chunk(orphaned chunks,即无法从任何文件达到的 Chunk),并擦除这些 Chunk 的元数据。在与 Master 定期交换的心跳(HeartBeat)消息中,每台 Chunkserver 会汇报它所拥有的 Chunk 的一个子集,而 Master 则会回复所有不再存在于 Master 元数据中的 Chunk 的标识。Chunkserver 可以自由地删除这些 Chunk 的副本。

4.4.2 Discussion 讨论

虽然在编程语言语境下分布式垃圾回收是一个需要复杂解决方案的难题,但在我们的场景中却非常简单。我们可以轻松识别出对 Chunk 的所有引用:它们存在于由 Master 独占维护的文件到 Chunk 的映射关系中。我们也可以轻松识别出所有的 Chunk 副本:它们就是每台 Chunkserver 指定目录下的 Linux 文件。任何不被 Master 所知的此类副本都是“垃圾”。

这种通过垃圾回收来进行存储回收的方法,相比于立即(主动)删除具有多个优势。首先,在组件故障频发的大规模分布式系统中,它简单且可靠。Chunk 的创建可能会在某些 Chunkserver 上成功,而在另一些上失败,从而留下 Master 并不知道其存在的副本。副本删除消息可能会丢失,Master 必须记住在发生故障(无论是它自身还是 Chunkserver 的故障)后重新发送这些消息。垃圾回收提供了一种统一且可靠的方式来清理任何未知是否有用的副本。其次,它将存储回收合并到了 Master 的日常后台活动中,例如对命名空间的定期扫描以及与 Chunkserver 的握手。因此,它是分批完成的,成本得以平摊。此外,它仅在 Master 相对空闲时才执行。这样 Master 可以更迅速地响应需要及时处理的客户端请求。第三,延迟回收存储空间为防止意外的、不可逆的删除提供了一道安全网。

根据我们的经验,主要缺点在于这种延迟有时会在存储紧张时,妨碍用户微调存储使用量的努力。重复创建和删除临时文件的应用程序可能无法立即重用存储空间。我们通过以下方式解决这些问题:如果一个已被删除的文件再次被显式删除,我们就加速存储回收。我们还允许用户对命名空间的不同部分应用不同的副本和回收策略。例如,用户可以指定某个目录树下文件中所有的 Chunk 均不进行副本复制存储,并且任何被删除的文件都会立即且不可撤销地从文件系统状态中移除。

4.5 Stale Replica Detection 过期副本检测

如果 Chunkserver 发生故障,并在停机期间错过了对 Chunk 的修改,则其上的 Chunk 副本可能会变得过期(stale)。对于每个 Chunk,Master 都维护着一个 Chunk 版本号(chunk version number),以区分最新的副本和过期的副本。

每当 Master 为某个 Chunk 授予新的租约时,它就会增加该 Chunk 的版本号,并通知所有最新的副本。Master 和这些副本都会将新的版本号记录在其持久化状态中。这发生在通知任何客户端之前,因此也发生在客户端可以开始向该 Chunk 写入数据之前。如果另一个副本当前不可用,它的 Chunk 版本号将不会递增。当该 Chunkserver 重启并汇报其拥有的一组 Chunk 及其相关版本号时,Master 就会检测到该 Chunkserver 持有一个过期的副本。如果 Master 看到的版本号大于其记录中的版本号,Master 会认为自己在授予租约时发生了故障,因此会将较高的版本号视为最新的。

Master 会在其定期的垃圾回收过程中移除过期的副本。在此之前,当 Master 回复客户端对 Chunk 信息的请求时,它在实际上会认为过期的副本根本不存在。作为另一重安全保障,当 Master 通知客户端哪台 Chunkserver 持有该 Chunk 的租约,或者在克隆操作中指示某台 Chunkserver 从另一台 Chunkserver 读取 Chunk 时,Master 都会附带上该 Chunk 的版本号。客户端或 Chunkserver 在执行操作时会验证版本号,以确保它始终访问的是最新的数据。

5 FAULT TOLERANCE AND DIAGNOSIS 容错与诊断

我们在设计系统时面临的最大挑战之一,就是处理频繁发生的组件故障。组件的质量和数量加在一起,使得这些问题更像是一种常态,而非例外:我们不能完全信任机器,也不能完全信任磁盘。组件故障可能会导致系统不可用,或者更糟的是导致数据损坏。我们将讨论我们如何应对这些挑战,以及我们在系统中内置的用于在问题不可避免地发生时进行诊断的工具。

5.1 High Availability 高可用性

在包含数百台服务器的 GFS 集群中,在任何给定的时刻都必然有一些服务器是不可用的。我们通过两种简单而有效的策略来实现整个系统的高可用性:快速恢复和副本复制。

5.1.1 Fast Recovery 快速恢复

Master 和 Chunkserver 在设计上都被要求无论以何种方式终止,都能在几秒钟内恢复其状态并启动。事实上,我们并不区分正常终止和异常终止;服务器通常只需直接杀掉进程(kill the process)即可关机。客户端和其他服务器会经历轻微的短暂打嗝(hiccup)——即它们未完成的请求会超时,随后重新连接到重启后的服务器并进行重试。第 6.2.2 节报告了观察到的启动时间。

5.1.2 Chunk Replication Chunk 复制

正如前面所讨论的,每个 Chunk 都被复制到了不同机架的多台 Chunkserver 上。用户可以为文件命名空间的不同部分指定不同的复制级别。默认值为 3。当 Chunkserver 离线或者通过校验和验证检测到损坏的副本时(参见第 5.2 节),Master 会根据需要克隆现有的副本,以保持每个 Chunk 被充分复制。虽然副本复制机制对我们很有效,但为了满足我们日益增长的只读存储需求,我们正在探索其他形式的跨服务器冗余,例如奇偶校验(parity)或纠删码(erasure codes)。我们预计在我们这个高度松耦合的系统中实现这些更复杂的冗余方案虽然具有挑战性,但是可控的,因为我们的流量主要由追加和读取主导,而不是小的随机写入。

5.1.3 Master Replication Master 复制

Master 的状态经过复制以确保可靠性。它的操作日志和检查点(checkpoints)被复制到了多台机器上。对状态的修改只有在其日志记录刷入本地磁盘以及所有 Master 副本之后,才被认为已提交(committed)。为了简洁起见,仍由一个 Master 进程负责所有的修改,以及像垃圾回收这样会改变系统内部状态的后台活动。当它发生故障时,几乎可以瞬间重启。如果其所在的机器或磁盘发生故障,GFS 之外的监控基础设施会在其他地方利用已复制的操作日志启动一个新的 Master 进程。客户端仅使用 Master 的规范名称(例如 gfs-test),这是一个 DNS 别名,如果 Master 被迁移到了另一台机器,该别名可以被修改。

此外,“影子” Master(“shadow” masters)即使在主 Master(primary master)宕机时,也能提供对文件系统的只读访问。它们被称为“影子”而非“镜像”,是因为它们可能会稍微滞后于主 Master,通常滞后时间不到一秒。它们提高了对那些未被频繁修改的文件、或者不介意获取稍旧结果的应用程序的读取可用性。事实上,由于文件内容是从 Chunkserver 读取的,应用程序并不会观察到旧的文件内容。在极短的时间窗口内可能变旧的是文件元数据,例如目录内容或访问控制信息。

为了保持自身信息的更新,影子 Master 会读取不断增长的操作日志的一份副本,并像主 Master 一样,按照完全相同的修改序列更新其数据结构。与主 Master 类似,它在启动时(以及此后不频繁地)轮询 Chunkserver 以定位 Chunk 副本,并与它们频繁交换握手消息以监控其状态。它仅在主 Master 做出创建和删除副本决定所导致的副本位置更新方面,依赖于主 Master。

5.2 Data Integrity 数据完整性

每台 Chunkserver 都使用校验和(checksumming)来检测存储数据的损坏。鉴于一个 GFS 集群通常在数百台机器上拥有成千上万块磁盘,它经常会遇到引发读写路径上数据损坏或丢失的磁盘故障。(原因之一参见第 7 节。)我们可以利用其他 Chunk 副本从损坏中恢复,但通过跨 Chunkserver 比较副本的方式来检测损坏是非常不切实际的。此外,存在分歧的副本可能是合法的:GFS 修改的语义(尤其是前面讨论的原子记录追加)并不保证副本间在字节层面上完全相同。因此,每台 Chunkserver 必须通过维护校验和,独立地验证其自身副本的完整性。

一个 Chunk 被划分为 64 KB 大小的块(block)。每个块都有一个对应的 32 位校验和。与其他元数据一样,校验和保留在内存中,并附带日志进行持久化存储,与用户数据分开。

对于读取操作,Chunkserver 在向请求者(无论是客户端还是另一台 Chunkserver)返回任何数据之前,会先验证与读取范围重叠的数据块的校验和。因此,Chunkserver 不会将损坏传播到其他机器。如果某个块与记录的校验和不匹配,Chunkserver 会向请求者返回一个错误,并将该不匹配情况汇报给 Master。作为响应,请求者将从其他副本进行读取,而 Master 则会从另一个副本克隆该 Chunk。在新的有效副本到位后,Master 会指示汇报不匹配的 Chunkserver 删除其副本。

出于以下几个原因,校验和计算对读取性能影响微乎其微。由于我们的绝大多数读取会跨越至少几个块,我们只需要读取并校验相对较少量的额外数据来进行验证。GFS 客户端代码通过尝试将读取操作与校验和块的边界对齐,进一步降低了这一开销。此外,Chunkserver 上的校验和查找和比较无需任何 I/O 操作,且校验和计算通常可以与 I/O 操作重叠(并行)进行。

对于追加到 Chunk 末尾的写入(相较于覆盖现有数据的写入),校验和计算进行了高度优化,因为追加操作在我们的工作负载中占主导地位。我们只需要针对最后一个部分填满的校验和块进行增量更新,并为追加内容填满的任何全新校验和块计算新的校验和。即使最后一个部分填满的校验和块此前就已经损坏且我们现在未能检测到,新的校验和值也不会与存储的数据匹配,并且当下次读取该块时,损坏仍会像往常一样被检测出来。

相反,如果写入操作覆盖了 Chunk 的现有范围,我们必须先读取并验证被覆盖范围的首块和尾块,然后执行写入,最后计算并记录新的校验和。如果我们不在部分覆盖前验证首块和尾块,新的校验和可能会掩盖未被覆盖区域中此前已存在的损坏。

在空闲期间,Chunkserver 可以扫描并验证不活跃(inactive)Chunk 的内容。这使得我们能够检测到极少被读取的 Chunk 中的损坏。一旦检测出损坏,Master 就可以创建一个新的未损坏副本,并删除损坏的副本。这可以防止不活跃但已损坏的 Chunk 副本误导 Master,让其误以为该 Chunk 拥有足够的有效副本。

5.3 Diagnostic Tools 诊断工具

广泛且详尽的诊断日志在问题隔离、调试和性能分析方面给予了极大的帮助,同时仅产生了微小的开销。如果没有日志,将很难理解机器之间瞬时发生且不可复现的交互。GFS 服务器会生成诊断日志,用以记录许多重大事件(例如 Chunkserver 的上线和宕机)以及所有的 RPC 请求和回复。这些诊断日志可以随意删除,而不会影响系统的正确性。然而,只要空间允许,我们都会尽量保留这些日志。

RPC 日志记录了线路上传输的精准请求和响应,但被读取或写入的文件数据本身除外。通过将请求与回复进行匹配,并对照整理不同机器上的 RPC 记录,我们可以重构出整个交互历史以诊断问题。这些日志还可用作负载测试和性能分析的追踪数据(traces)。

日志记录对性能的影响极小(且带来的收益远大于开销),因为这些日志是按顺序且异步写入的。最新的事件还会保留在内存中,以便进行持续的在线监控。

6 MEASUREMENTS 测量

在本节中,我们将展示一些基准测试(micro-benchmarks),以说明 GFS 架构和实现中所固有的瓶颈,同时也展示一些来自 Google 实际使用集群的数据。

6.1 Micro-benchmarks 基准测试

我们在一个由 1 个 Master、2 个 Master 副本、16 台 Chunkserver 和 16 个客户端组成的 GFS 集群上测量了性能。请注意,设置此配置是为了便于测试。典型的集群拥有数百台 Chunkserver 和数百个客户端。

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

image.png

6.1.1 Reads 读取

$N$ 个客户端同时从文件系统中读取数据。每个客户端从包含 320 GB 的文件集中随机读取一个 4 MB 的区域。该过程重复 256 次,因此每个客户端最终读取 1 GB 的数据。所有 Chunkserver 的内存加起来只有 32 GB,因此我们预计 Linux 缓冲区缓存(buffer cache)的命中率最高仅为 10%。我们的测试结果应该接近冷缓存(cold cache)下的结果。

图 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 读取数据的概率也随之增加。

6.1.2 Writes 写入

$N$ 个客户端同时向 $N$ 个不同的文件写入数据。每个客户端通过一连串 1 MB 的写入操作,向一个新文件写入 1 GB 的数据。聚合写入速率及其理论极限如图 3(b) 所示。极限稳定在 67 MB/s,因为我们需要将每个字节写入 16 台 Chunkserver 中的 3 台,而每台 Chunkserver 的输入连接带宽为 12.5 MB/s。

单个客户端的写入速率为 6.3 MB/s,约为极限值的一半。主要的罪魁祸首是我们的网络栈。它与我们用于将数据推送至 Chunk 副本的流水线方案(pipelining scheme)配合得不够好。将数据从一个副本传播到另一个副本时的延迟降低了整体写入速率。

当有 16 个客户端时,聚合写入速率达到 35 MB/s(即每个客户端 2.2 MB/s),约为理论极限的一半。与读取的情况类似,随着客户端数量的增加,多个客户端同时向同一台 Chunkserver 写入数据的可能性也随之增加。此外,16 个写入者比 16 个读取者更容易发生冲突,因为每次写入都涉及三个不同的副本。

写入速度比我们预期的要慢。在实际应用中,这并没有成为主要问题,因为尽管它增加了单个客户端所观察到的延迟,但并不会显著影响系统向大量客户端提供的聚合写入带宽。

6.1.3 Record Appends 记录追加

图 3(c) 展示了记录追加的性能。$N$ 个客户端同时向单个文件追加数据。性能受限于存储该文件最后一个 Chunk 的 Chunkserver 的网络带宽,而与客户端的数量无关。单客户端时性能从 6.0 MB/s 开始,在 16 个客户端时降至 4.8 MB/s,这主要是由于网络拥塞以及不同客户端观察到的网络传输速率差异所致。

我们的应用程序倾向于并发生成多个此类文件。换句话说,$N$ 个客户端同时向 $M$ 个共享文件进行追加,其中 $N$ 和 $M$ 都在数十到数百的范围内。因此,我们实验中出现的 Chunkserver 网络拥塞在实际中并不是严重问题,因为当某个文件的 Chunkserver 繁忙时,客户端可以转向推进另一个文件的写入。

6.2 Real World Clusters 实际集群

我们现在考察 Google 内部投入使用的两个具有代表性的集群。集群 A 常年由一百多名工程师用于研发工作。一个典型的任务由人类用户发起,运行长达数小时。它读取几 MB 到几 TB 的数据,对数据进行转换或分析,并将结果写回集群。集群 B 主要用于生产数据处理。其任务持续时间要长得多,在极少需要人工干预的情况下,持续生成和处理数 TB 的数据集。在这两种情况下,单个“任务”都由多台机器上的许多进程组成,它们同时读取和写入多个文件。

image.png

image.png

6.2.1 Storage 存储

正如表中前五项所示,两个集群都拥有数百台 Chunkserver,支持数十 TB 的磁盘空间,并且处于相当满但尚未完全满的状态。“已用空间”包含了所有的 Chunk 副本。实际上,几乎所有文件都被复制了三次。因此,这两个集群分别存储了 18 TB 和 52 TB 的文件数据。

这两个集群拥有相似数量的文件,不过集群 B 含有更高比例的“已死文件(dead files)”,即那些已被删除或被新版本替换、但其存储空间尚未被回收的文件。集群 B 还拥有更多的 Chunk,因为它的文件往往更大。

6.2.2 Metadata 元数据

所有 Chunkserver 聚合存储了数十 GB 的元数据,主要是用户数据 64 KB 块的校验和。Chunkserver 上保留的唯一其他元数据是第 4.5 节中讨论的 Chunk 版本号。

Master 上保留的元数据要小得多,只有数十 MB,平均每个文件大约 100 字节。这符合我们的假设,即在实际应用中,Master 的内存大小并不会限制系统的容量。大部分单文件元数据是以前缀压缩形式存储的文件名。其他元数据包括文件所有权和权限、从文件到 Chunk 的映射,以及每个 Chunk 的当前版本。此外,对于每个 Chunk,我们还存储了当前副本的位置以及用于实现写时复制(copy-on-write)的引用计数。

每个独立的服务器(包括 Chunkserver 和 Master)都只有 50 到 100 MB 的元数据。因此恢复速度很快:服务器只需几秒钟从磁盘读取这些元数据,即可能够响应查询。然而,Master 在一段时间内(通常为 30 到 60 秒)会受到一定程度的受限,直到它从所有 Chunkserver 获取了 Chunk 位置信息。

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。

6.2.4 Master Load Master 负载

表 3 还显示,发送给 Master 的操作速率大约在每秒 200 到 500 次之间。Master 可以轻松跟上这一速率,因此对于这些工作负载来说,Master 并不是瓶颈。

在 GFS 的早期版本中,对于某些工作负载,Master 偶尔会成为瓶颈。它花费了大部分时间顺序扫描大型目录(其中包含数十万个文件)以查找特定的文件。此后,我们修改了 Master 的数据结构,以允许在命名空间中进行高效的二分查找(binary searches)。现在,它能够轻松支持每秒数千次的文件访问。如果有必要,我们还可以通过在命名空间数据结构前增加名称查找缓存(name lookup caches)来进一步加快速度。

6.2.5 Recovery Time 恢复时间

在一台 Chunkserver 发生故障后,某些 Chunk 的副本数量会不足,必须通过克隆来恢复其复制级别。恢复所有此类 Chunk 所需的时间取决于资源的数量。在一项实验中,我们在集群 B 中杀掉了单个 Chunkserver。该 Chunkserver 拥有大约 15,000 个 Chunk,包含 600 GB 的数据。为了限制对正在运行的应用程序的影响并为调度决策提供余地,我们的默认参数将该集群限制为 91 个并发克隆操作(占 Chunkserver 数量的 40%),其中每个克隆操作最多被允许消耗 6.25 MB/s(50 Mbps)的带宽。所有的 Chunk 在 23.2 分钟内全部恢复完成,有效复制速率达到了 440 MB/s。

在另一项实验中,我们杀掉了两台 Chunkserver,每台分别拥有大约 16,000 个 Chunk 和 660 GB 的数据。这次双重故障导致 266 个 Chunk 只剩下一个副本。这 266 个 Chunk 以更高的优先级进行了克隆,并在 2 分钟内全部恢复到了至少 2 倍的复制级别,从而使集群进入到了能够承受另一次 Chunkserver 故障而不会丢失数据的状态。

6.3 Workload Breakdown 工作负载分解

在本节中,我们将详细展示两个 GFS 集群上的工作负载分解情况,这两个集群与第 6.2 节中的集群具有可比性,但并不完全相同。集群 X 用于研发,而集群 Y 用于生产数据处理。

image.png

image.png

image.png

6.3.1 Methodology and Caveats 方法论与注意事项

这些结果仅包含由客户端发起的请求,从而反映了我们的应用程序为整个文件系统产生的工作负载。它们不包括用于执行客户端请求的服务器间请求,或内部后台活动(例如转发写入或重新负载均衡)。

I/O 操作的统计数据建立在根据 GFS 服务器记录的实际 RPC 请求启发式重构的信息之上。例如,GFS 客户端代码可能会将一次读取拆分为多个 RPC 以提高并行度,我们据此推断出原始的读取操作。由于我们的访问模式高度模式化(stylized),我们预计任何误差都处于噪声范围内。由应用程序进行显式日志记录可能会提供稍微更准确的数据,但在后勤保障上,重新编译并重启数千个正在运行的客户端是不可能的,而且从这么多机器上收集结果也非常繁琐。

人们应当注意,不要过度将我们的工作负载推广到通用场景。由于 Google 完全控制了 GFS 及其应用程序,应用程序倾向于针对 GFS 进行调优,反之 GFS 也是为这些应用程序而设计的。通用应用程序与文件系统之间可能也存在这种相互影响,但在我们这里,这种效应可能更加明显。

6.3.2 Chunkserver Workload Chunkserver 工作负载

表 4 展示了按大小划分的操作分布。读取大小呈现出双峰分布(bimodal distribution)。小额读取(小于 64 KB)来自于在巨型文件中查找小块数据的寻道密集型(seek-intensive)客户端。大额读取(超过 512 KB)则来自贯穿整个文件的长顺序读取。

在集群 Y 中,有相当数量的读取完全没有返回任何数据。我们的应用程序(特别是生产系统中的应用)经常将文件用作生产者-消费者队列(producer-consumer queues)。生产者并发地向文件进行追加,而消费者则读取文件的末尾。偶尔,当消费者的速度超过生产者时,就不会返回任何数据。集群 X 出现这种情况的频率较低,因为它通常用于短暂的数据分析任务,而不是长期运行的分布式应用程序。

写入大小同样呈现出双峰分布。大额写入(超过 256 KB)通常源于写入者内部的大量缓冲。而缓冲较少数据、更频繁地进行检查点(checkpoint)或同步、或者单纯产生较少数据的写入者,则构成了较小的写入(小于 64 KB)。

至于记录追加,集群 Y 中大额记录追加的比例远高于集群 X,因为使用集群 Y 的生产系统针对 GFS 进行了更积极的调优。

表 5 展示了不同大小的操作中传输的数据总量。对于所有类型的操作,较大的操作(超过 256 KB)普遍占据了传输字节数的绝大部分。小额读取(小于 64 KB)由于随机寻道工作负载的原因,虽然占比不大,但也传输了一部分相当客观的读取数据。

6.3.3 Appends versus Writes 追加与写入对比

记录追加被大量使用,尤其是在我们的生产系统中。对于集群 X,按传输字节数计算,写入与记录追加的比例为 108:1,按操作次数计算为 8:1。对于生产系统使用的集群 Y,这两个比例分别为 3.7:1 和 2.5:1。此外,这些比例表明,对于这两个集群而言,记录追加(单次操作的大小)往往比写入更大。不过,对于集群 X,在测量期间记录追加的总体使用率相当低,因此该结果很可能是由一两个采用了特定缓冲区大小选择的应用程序所偏置(skewed)的。

正如预期的那样,我们的数据修改工作负载由追加主导,而不是覆盖(overwriting)。我们测量了主副本(primary replicas)上被覆盖的数据量。这近似于客户端故意覆盖先前已写入的数据、而不是追加新数据的情况。对于集群 X,覆盖占修改字节数的 0.0001% 以下,占修改操作次数的 0.0003% 以下。对于集群 Y,这两个比例均为 0.05%。虽然这微乎其微,但仍高于我们的预期。事实证明,这些覆盖中的绝大部分来自于客户端因错误或超时而进行的重试。它们本身并不是工作负载的一部分,而是重试机制所导致的结果。

6.3.4 Master Workload Master 工作负载

表 6 展示了发送给 Master 的请求按类型划分的明细。绝大多数请求是用于读取的请求 Chunk 位置信息(FindLocation),以及用于数据修改的请求租约持有者信息(FindLeaseLocker)。

集群 X 和 Y 接收到的 Delete 请求数量差异显著,因为集群 Y 存储的是生产数据集,这些数据集会被定期重新生成并用更新的版本替换。这一差异的一部分还隐匿于 Open 请求的差异之中,因为旧版本的文件可能会通过从头开始以写入方式打开(Unix open 术语中的模式 "w")而被隐式删除。

FindMatchingFiles 是一种模式匹配请求,支持 "ls" 以及类似的文件系统操作。与发送给 Master 的其他请求不同,它可能会处理命名空间的很大一部分,因此开销可能非常昂贵。集群 Y 出现这种情况要频繁得多,因为自动化数据处理任务往往会检查文件系统的各个部分,以了解全局应用程序状态。相比之下,集群 X 的应用程序受到更明确的人类用户控制,通常事先就知道所有需要的文件名称。

7 EXPERIENCES 经验

在构建和部署 GFS 的过程中,我们遇到了各种各样的问题,其中有些是运维层面的,有些是技术层面的。

起初,GFS 被构想为我们生产系统的后端文件系统。随着时间的推移,其使用场景演变为包含研发任务。它在刚开始时对权限和配额(quotas)等功能的支持甚少,但现在已包含了这些功能的初级形式。虽然生产系统非常有纪律且受控,但人类用户有时却并非如此。因此需要更多的基础设施来防止用户之间互相干扰。

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

早期,由于 fsync() 的开销,我们在 Linux 2.2 内核上遇到了一些问题。它的开销与文件的大小成正比,而不是与被修改部分的大小成正比。这对于我们庞大的操作日志来说是个问题,特别是在我们实现检查点机制之前。我们曾通过使用同步写入(synchronous writes)来规避这一问题,并最终迁移到了 Linux 2.4。

另一个 Linux 问题是单读写锁(single reader-writer lock)问题:当地址空间中的任何线程从磁盘换入页面(读锁)、或者在 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 使得实现复杂的 Chunk 放置和复制策略变得容易得多,因为 Master 已经拥有了大部分相关信息并控制着这些信息如何变化。我们通过保持 Master 状态足够小且完全复制到其他机器上来解决容错问题。可扩展性和(针对读取的)高可用性目前由我们的影子 Master(shadow master)机制提供。对 Master 状态的更新通过追加到预写日志(write-ahead log)中来实现持久化。因此,我们可以采用类似于 Harp [7] 中的主副本(primary-copy)方案,以提供比我们当前方案更高可用性及更强一致性保证的服务。

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

GFS 与 NASD 架构 [4] 最为相似。虽然 NASD 架构基于网络附加磁盘驱动器(network-attached disk drives),但 GFS 使用通用廉价机器(commodity machines)作为 Chunkserver,正如 NASD 原型所做的那样。与 NASD 的研究不同,我们的 Chunkserver 使用延迟分配的固定大小 Chunk,而不是可变长度的对象。此外,GFS 还实现了生产环境所需的重新负载均衡、复制和恢复等功能。

与 Minnesota 的 GFS 和 NASD 不同,我们并不寻求改变存储设备的模型。我们专注于使用现有的通用廉价组件,解决复杂分布式系统的日常数据处理需求。

由原子记录追加所支持的生产者-消费者队列,解决了与 River [2] 中的分布式队列类似的问题。River 使用跨机器分布的基于内存的队列和精细的数据流控制,而 GFS 则使用一个可以被多个生产者并发追加的持久化文件。River 模型支持 m-对-n 的分布式队列,但缺乏持久化存储带来的容错能力;而 GFS 仅能高效支持 m-对-1 的队列。多个消费者可以读取同一个文件,但它们必须进行协调以划分传入的负载。

9 CONCLUSIONS 结论

Google 文件系统展现出了在通用廉价硬件上支持大规模数据处理工作负载所必需的品质。虽然某些设计决策是针对我们独特环境的,但其中许多可能也适用于类似规模和具备成本意识的数据处理任务。

我们首先根据当前和预期的应用程序工作负载以及技术环境,重新审视了传统文件系统的假设。我们的观察引出了设计空间中截然不同的取舍点。我们将组件故障视为常态而非例外;针对主要以追加方式(可能是并发追加)写入、随后进行读取(通常是顺序读取)的巨型文件进行优化;并对标准文件系统接口进行了扩展与放宽,以提升系统的整体性能。

我们的系统通过持续监控、复制关键数据以及快速自动恢复来提供容错能力。Chunk 副本复制使我们能够承受 Chunkserver 的故障。这些故障发生的频率促使我们设计了一种新颖的在线修复机制,能够定期且透明地修复损坏,并尽早弥补丢失的副本。此外,我们使用校验和来检测磁盘或 IDE 子系统层面的数据损坏,考虑到系统中磁盘的数量,这种损坏极为常见。

我们的设计为执行各种任务的许多并发读取者和写入者提供了很高的聚合吞吐量。我们通过将经过 Master 的文件系统控制流与直接在 Chunkserver 和客户端之间传输的数据流分离来实现这一点。通过使用较大的 Chunk 尺寸以及 Chunk 租约(将数据修改的权限下放给主副本),Master 在日常操作中的参与度被降到了最低。这使得构建一个简单、集中式且不会成为瓶颈的 Master 成为可能。我们相信,网络栈的改进将消除目前单个客户端所看到的写入吞吐量限制。

GFS 成功地满足了我们的存储需求,并在 Google 内部作为研发和生产数据处理的存储平台被广泛使用。它是一项重要的工具,使我们能够继续创新并解决整个互联网规模上的难题。