GFS 论文原文传送门:
The Google File System
1. 什么是 GFS
GFS,全称 Google File System,谷歌文件系统。
这篇论文是 2003 年发表的,在这之前,GFS 已经大规模应用在了 Google 内部。
GFS 是 Google 提出的一个文件系统,其是分布式的,主要用于处理越来越庞大的数据。因为当数据量大到一定程度时,传统的数据存储与处理方式就显得很笨重了,不适用了(比如你很难很快地读取数百 TB 的数据)。
2. 设计概述
2.1. 假想(目标)
GFS 在设计的时候有一些假想,即预期要实现的目标。
- 这个系统由很多廉价的、经常会故障的商用组件构建,所以在日常使用中,这个系统必须持续地监控自身,以检测、容忍组件故障,并迅速从组件故障中恢复。
- 这个系统存储数量适中的大文件。Google 期望是几百万个文件,每个一般是 100MB 或者更大。数 GB 大小的文件在这个系统中也是很常见的,需要高效管理。而小文件肯定也要支持,但是不需要为了这些小文件专门优化。
- 工作负载主要包括两类读:大文件流的读(流只能顺序读)和小文件的随机读。
- 大文件流的读:单个读操作一般读几百 KB,更常见的是读 1MB 或者更多。来自同一个客户端连续的读操作经常是从一个文件连续的位置读。
- 小文件的随机读:一般是在文件的任意位置读几 KB 大小。注重性能的应用程序通常对它们的小读取进行批处理和排序,以逐渐地浏览文件,而不是来回的读(文件指针来回移动)。
- 这个系统也会有很多大的、连续的写操作,将数据追加到文件末尾。一般这种操作的大小和读差不多。一旦写入操作完成,这个文件很少会再次修改。小的随机写也支持,但是不太高效。
- 这个系统必须高效地实现定义明确的语义,以支持多客户端并发写入(追加写入)同一个文件。GFS 中的文件通常用作生产者消费者队列或多路合并。系统中有数百个生产者,每个机器上运行一个,这些生产者并发地追加修改一个文件,因此以最小的同步开销来实现原子性是必不可少的。这些文件可能随后被读取,也可能有一个消费者在写的同时读。
- 高的持续的带宽比低的延迟更重要。GFS 的大多数目标应用程序都重视以高速率批量处理数据,而很少有应用程序对单个读或写有严格的响应时间要求。
2.2. 接口
GFS 提供了一个常见的文件系统接口,尽管 GFS 没有实现像 POSIX 这样的标准 API。
GFS 中文件在目录中以层次结构组织,通过路径名区分。
GFS 支持常用操作以创建(create)、删除(delete)、打开(open)、关闭(close)、读(read)和写(write)文件。
此外,GFS 中还有 snapshot 和 record append 操作。Snapshot 以一个很低的开销创建一个文件的或者一个目录树的拷贝。Record append 允许多个客户端并发地追加写入同一个文件,且确保每个客户端的写入操作都是原子的。Record append 对实现多路合并结果、生产者消费者队列很有用,因为很多客户端可以同时追加写入,而不需要额外的锁。Google 发现在构建大型分布式应用时,这些类型的文件是非常有用的。
Snapshot 和 record append 会在后面进一步讨论。
2.3. 架构
一个 GFS 集群包含单个 master 和多个 chunkservers,允许多个 client 访问。如图 1 所示。
每个 master 或 chunkserver 一般都是一个商品 Linux 机器中运行着的一个用户级服务进程。在同一个机器上同时运行一个 chunkserver 和一个 client 是很容易,但前提是机器资源允许,并且你可以接受运行不稳定的应用程序代码导致的更低的可靠性。
GFS 系统中的文件会被划分为固定大小的 chunks。每个 chunk 使用一个不可变的、全局唯一的 64 位 chunk 句柄来标识,这个 chunk 句柄是在 chunk 创建时由 master 指定的。Chunkservers 在本地磁盘中以 Linux 文件的形式存储 chunks,并读取或写入由 chunk 句柄和字节范围指定的块数据。为了可靠性,每个 chunk 都在多个 chunkservers 上有复制。默认是 3 个复制,但用户可以为文件命名空间的不同部分指定不同的复制级别。
master 维护所有文件系统元数据,包括命名空间、访问控制信息、从文件到 chunk 的映射以及 chunks 当前的位置。master 也会控制系统范围内的活动,比如 chunk 租用管理,孤儿 chunks 的垃圾回收,以及在 chunkservers 之间迁移 chunks。master 会定期在 HeartBeat 消息中与每个 chunkservers 通信,以给 chunkservers 指令并收集其状态信息。
链接到每个应用程序的 GFS 客户端代码中实现了文件系统 API,这个 GFS 客户端代表应用程序与 master 和 chunkservers 通信以读写数据。客户端与 master 交互以进行元数据操作,但所有数据承载通信直接进入 chunkservers。GFS 没有提供 POSIX API,因此不需要连接到 Linux 的 vnode 层。
客户端和 chunkserver 都不缓存文件数据。客户端缓存文件数据几乎没什么好处,因为大多数应用程序通过巨大的文件进行流式传输,或者工作集太大而无法缓存。不缓存文件数据使得客户端代码和总体系统的代码得以简化,因为无需编写代码解决缓存一致性的问题(不过客户端是缓存元数据的)。Chunkservers 不需要缓存文件数据是因为 chunks 是作为本地文件存储的,所以 Linux buffer 缓存已经把频繁访问的数据放在内存中了。
2.4. 单个 Master
GFS 中只有一个 master,这大大简化了其设计,并且使得 master 能够根据全局知识做出复杂的 chunk 放置和复制决策。不过必须最小化在读写中 master 的调用次数,防止 master 成为 GFS 系统的性能瓶颈。客户端永远都不会通过 master 读写文件数据,而是向 master 询问该联系哪些 chunkservers,当客户端会在有限的时间内缓存此信息,且直接和 chunkservers 互动,以进行一系列的操作。
现在我们通过一个简单的读操作来解释 GFS 的工作流程(就如图 1 中的那样)。
首先,要使用固定的 chunk 大小,客户端把应用程序指定的文件名和字节偏移翻译成这个文件中的一个 chunk 索引。然后客户端向 master 发送一个包含文件名和 chunk 索引的请求,master 给客户端回复相应的 chunk 句柄和 chunk 副本的位置。客户端以文件名和 chunk 索引作为 key 缓存这些信息。
客户端随后给副本之一发送一个请求(大部分情况是最近的一个副本),这个请求中指定了 chunk 句柄和一个 chunk 中的字节范围。同一个 chunk 的读就不再需要 client-master 互动了,直到客户端缓存的信息到期(前文说过在有限的时间内缓存这些信息,也就是说这些信息是有时效性的)或这个文件被重新打开。事实上,客户端往往在一个请求中询问多个 chunks,master 也可以在回复的信息中心包含这些请求的 chunks 信息,这些额外的信息几乎不需要什么额外的开销,就可以避免未来几次的 client-master 交互。
2.5. Chunk 大小
Chunk 的大小是关键的设计参数之一。GFS 中将 chunk 的大小设定为 64MB,远远大于一般文件系统的块大小。每个 chunk 副本都以一个普通的 Linux 文件存储在一个 chunkserver 上,只要需要的时候才会扩展 chunk 的数量。延迟空间分配避免了由于内部碎片造成的空间浪费,这可能是对如此大 chunk 大小的最大反对。
将 chunk 设置为 64MB 这么大,可以提供一个重要的优势。首先,减少了客户端与 master 的交互次数,因为在同一个 chunk 上的读和写只需要在最初的请求中向 master 询问一次 chunk 的位置信息。减少客户端与 master 交互次数对于我们的工作负载而言格外重要,因为应用程序往往是连续读写大文件的。即便是对于小的随机读,客户端也可以轻松缓存一个数 TB 工作集的所有 chunk 的位置信息。第二,由于一个 chunk 比较大,使得一个客户端更可能在一个给定的 chunk 上执行很多操作,这样就可以在很长的一段时间内,通过保持一个持续的客户端与 chunkserver 之间的 TCP 连接来减少网络开销。第三,减少了 master 上存储的元数据大小。这允许我们把元数据放在内存中,把元数据放在内存中又反过来带给我们一些其他的优势,这些优势我们在 2.6.1 中讨论。
另一方面,一个很大的 chunk 大小,即便有延迟空间分配策略,也还是有缺点的。一个小文件可能包含很少数量的 chunks,甚至可能只有一个。这样如果有很多客户端都要访问这同一个文件,那么存储这些 chunks 的 chunkservers 就会成为热点。不过在实践中,热点问题不是主要问题,因为我们的应用程序大多是顺序读多 chunk 的大文件。
然而,当 GFS 首次被批处理队列系统使用时,热点确实出现了:一个可执行文件作为单个 chunk 文件写入 GFS,然后同时在数百台机器上启动。存储此可执行文件的少数 chunkservers 被数百个同时请求过载。Google 通过以更高的复制因子存储此类可执行文件以及使批处理队列系统错开应用程序启动时间来解决此问题。一个潜在的长期解决方案是允许客户端在这种情况下从其他客户端读取数据。
2.6. 元数据
master 中主要存储三种类型的元数据:
- 文件和 chunk 的命名空间;
- 从文件到 chunks 的映射;
- 每个 chunk 的副本的位置。
所有的元数据都存储在 master 的内存里。前两种类型也会通过在操作日志(operation log)上记录修改来持久化,操作日志存储在 master 的本地磁盘上,并且会在远程机器上复制。使用日志使得我们可以容易地、可靠地更新 master 状态信息,而不用承受 master 崩溃导致的不一致性的风险。master 不会持久的存储 chunk 位置信息,而是会在 master 启动时或一个 chunkserver 加入集群时向 chunkserver 询问其 chunks 信息。
2.6.1. 内存中的数据结构
因为元数据存储在内存中,所以 master 的操作是非常快速的。进一步地说,master 定期在后台扫描其整个状态信息是非常简单且高效的。定期扫描是用来实现 chunk 垃圾回收、chunkserver 故障时的重新复制,以及为了负载均衡和跨 chunkserver 使用磁盘空间进行的 chunk 迁移的。4.3 和 4.4 小节会进一步讨论这些内容。
仅在内存访问这些,有一个潜在的问题是,chunks 的数量和整个 GFS 系统的容量受 master 拥有多少内存限制。在实践中这不是一个很严重的限制。对于每个 64MB 的 chunk,master 维护小于 64 字节的元数据。大部分 chunks 是满的,因为大部分文件包含很多个 chunks,只有最后一个可能是不满的。类似地,对于每个文件,master 存储的文件命名空间数据通常少于 64 个字节,因为它使用前缀压缩紧凑地存储文件名。
如果确有必要支持更大的文件系统,只需要给 master 增加额外的内存,这个开销相对于我们在内存中存储元数据获得的简单性、可靠性、性能与灵活性而言,是很小的。
2.6.2. Chunk 的位置
master 不会持有一个持久的关于哪些 chunkservers 有一个给定 chunk 的副本的记录,而是在 master 启动时简单地轮询 chunkservers 来获取这些信息。启动后 master 可以保持最新,因为 master 控制着所有 chunk 的放置,以及通过常规心跳(HearBeat)消息监控着 chunkserver 的状态。
Google 起初尝试在 master 中持久存储 chunk 的位置信息,但是后来决定在 master 启动时(以及启动后定期)从 chunkmasters 请求数据,这简单的多。并且这样做也排除了在有 chunkservsers 加入或离开集群、修改名字,故障、重启时等等保持 master 和 chunkservers 同步的问题。在一个有着数百个服务器的集群上,这些情况常常发生。
另一个理解这样设计决策的思路是这样想,一个 chunkserver 有决定存储哪些 chunks 在其本地磁盘上的最终话语权。在 master 上尝试维护一个这种信息的一致性视图是没有意义的,因为一个 chunkserver 上的错误可能导致 chunk 自发消失(比如磁盘损坏或不可用),或操作员可能修改 chunkserver 的名字。
2.6.3. 操作日志(Operation Log)
操作日志包含至关重要的元数据修改历史记录。
操作日志是 GFS 的核心。操作日志不仅仅是元数据唯一的持久化记录,也是一个逻辑时间线(充当定义并发操作的次序)。文件和 chunks 还有它们的版本(versions,详见 4.5. 小节),全部由他们被创建时的逻辑时间唯一且永久标识。
由于操作日志是非常重要的,我们必须将其可靠存储,并在在元数据修改持久化之前不让修改对客户端可见。否则,我们会在事实上丢失整个文件系统或最近的客户端操作(即便 chunks 本身还在)(这里原文翻译过来就是这样的。我的理解是客户端对 chunks 的操作依赖其缓存的元数据,如果元数据的改动在持久化前就对客户端可见的话,客户端就会依赖改动后的元数据对 chunks 操作,而此时这些元数据的改动还没有持久化,客户端的操作可能无法执行,导致操作丢失)。因此 GFS 将操作日志在多个远程机器上复制,并且仅在相应的日志记录已经被 flush 到本地和远程磁盘上后才会响应一个客户端的操作。Master 在 flush 前一起批处理几个 log 记录,从而减少 flush 和复制对整个系统吞吐量的影响。
Master 通过重放操作日志来恢复其文件系统状态信息。为了使 master 启动时间最短,就要保持日志小。每当日志超过一个特定的大小时,master 就会生成一个包含其此时状态的检查点(check point),这样 master 恢复的时候,只需要从本地磁盘加载最近的检查点,然后重放在这个检查点之后的有限数量的日志记录。检查点采用类似 B 树的紧凑形式,可以直接映射到内存中,用于命名空间查找,无需额外解析。 这进一步加快了恢复速度并提高了可用性
因为构建一个检查点需要一些时间,所以 master 的内部状态通过这样的一种方式构造:创建一个新的检查点时不推迟即将到来的修改,master 会切换到一个新的日志文件,并且在一个单独的线程中创建新的检查点。换句话说,新的检查点中包含了切换前的所有修改,切换后的修改会被记录到 master 切换过去的新的日志文件中。对于一个有几百万文件的集群来说,可以在一分钟左右创建完一个新的检查点。当检查点创建完成时,检查点会被写入本地和远程的磁盘。
恢复操作只需要最近的检查点和日志文件序列。更旧的检查点和日志文件就可以随便删了,尽管一般来说会保留一些以抵御灾难。在创建检查点时发生的故障不会影响正确性,因为恢复代码会检查并跳过不完整的检查点。
2.7. 一致性模式
GFS 有一个宽松的一致性模型,很好地支持我们的高度分布式应用程序,但是实现起来依然简单且高效。
我们现在讨论 GFS 如何保证一致性,以及这对应用程序来说有何意义。我们也会强调 GFS 如何维护这些保证,但是更详细的内容将在本文的其他部分来说。
2.7.1. GFS 如何保证一致性
文件命名空间的修改(例如,文件创建)是原子的,且只能由 master 来操作:命名空间锁确保原子性和正确性(详见 4.1);master 的操作日志定义了一个这些操作的全局的总的次序(详见 2.6.3)。
在数据修改后,文件区域的状态依赖于修改的类型,修改成功还是失败,以及这些是否是并发的修改。表 1 总结了在数据修改后的文件区域的状态。
对于一个文件区域,如果所有的客户端总是看到相同的数据(不论看的是哪个副本),那这个文件区域是一致的 consistent。
对于一个文件区域,在文件数据修改后,如果这个修改是一致的,并且客户端将看到这个修改写入的全部内容,那么这个文件区域就是 defined。
(个人理解:对于一个文件区域,只要所有客户端看到的数据都是一样的,那这个区域就是 consistent 的。在 consistent 的前提下,如果所有修改都已经被写入,就是 defined 的。consistent 是 defined 的子集。即 defined 的一定是 consistent 的,但 consistent 的不一定是 defined 的(上表中的 Recored Append 在后面单独说)。)
当一个修改成功,且没有受到并发写者的干预(即串行的修改),那么受影响的区域是 defined 的(且含义一致):即所有的客户端将总是能看到这个修改写入了什么。
并发的成功的修改使得受影响的区域是 undefined 但 consistent:即所有的客户端看到的数据是一样的,但这并不意味着每个修改都已经被写入。一般来说,写入的内容由多个修改的混合片段组成。
一个失败的修改会使得文件区域 inconsistent(因此也是 undefined):不同的客户端在不同的时间可能看到不同的数据。我们在下面描述我们的应用程序如何辨别 defined 的区域和 undefined 的区域。另外,应用程序不需要进一步区分不同种类的 undefined 的区域。
数据修改可能是 write 或 record appends。
- write 使数据被写入在一个由应用程序指定的文件偏移处。
- record append 使数据(即 record)被原子地的追加至少一次(即便是并发修改),但数据写入的文件偏移由 GFS 选择(详见 3.3)。
作为对比,一个普通的 append 仅仅是一个在客户端认为是当前文件末尾的偏移处的 write。
标志着包含写入 record 的 defined 的区域的开始的偏移会被返回给客户端。此外,GFS 可能会在写入的内容之间插入填充或 record 的复制。我们认为 GFS 插入内容占据的区域是 inconsistent 的(即表 1 中的 defined interspersed with inconsistent,即 defined 区域中穿插了 inconsistent 区域,但这些区域不会影响读取数据的结果,因为读者会过滤掉这些),且占用的空间比起用户数据的总量而言微不足道。
在连续的成功的修改后,GFS 会保证被修改的文件区域是 defined 的,并且包含最后一次修改写入的数据。GFS 实现这一点,通过 (a) 以相同的顺序应用修改到 chunk 以及其所有的拷贝上(详见 3.1),(b) 使用 chunk 版本号检测某个拷贝是否过期(即在其对应的 chunkserver 挂掉时,错过了修改。详见 4.5)。过期的 chunk 拷贝永远都不会被再应用修改,其位置也不会再由 master 提供给客户端,这些过期的 chunk 将尽快被垃圾回收。
由于客户端缓存了 chunk 的位置信息,所以在其缓存的位置信息更新之前,客户端可能会从一个旧的副本中读取数据。只有当缓存条目超时,或文件被重新打开时,这个问题才能解决,因为条目超时或重新打开文件会清除客户端缓存中的所有跟这个文件有关的 chunk 信息。此外,由于我们的文件大多数都是仅 append 的,一个旧的副本通常返回一个最新的 chunk 结束位置之前的位置,而不是过期的数据(也就是说,数据还是有效的数据,只是返回的偏移位置不对)。当一个读者重试并联系 master 时,读者会立即获得现在的 chunk 的位置。
即便在修改成功后的较长时间后,组件故障仍然可以导致数据被损坏、催毁。GFS 通过 master 与所有 chunkservers 定期握手的方式来找到故障的 chunkservers,通过校验和(详见 5.2)来检测数据是否损坏。一旦发现问题,GFS 会尽快通过相应数据的其他有效副本来恢复数据(详见 4.3)。仅当一个 chunk 的所有副本都丢失了,这个 chunk 的丢失才是不可逆地,即便在这种情况下,chunk 也是无法访问,而不是损坏:应用程序会收到一个明确的错误,而不是损坏的数据。
2.7.2. 对应用程序的影响
GFS 应用程序可以用一些其他目的已经需要的简单技术来适应宽松的一致性模型:依赖 append 而不是覆写、检查点,和写入自验证、自识别的记录。
实际上,我们应用程序所有的文件修改,都是通过 append 而不是覆写。
一个典型的用法是,一个写者从头到尾生产一个文件,在数据全部写入完成后,在原子地将文件重命名一个持久化的名字。或者是定期生成检查点,即每成功写入多少数据,就生成一个检查点。检查点也可能包含应用程序级检查和。读者只验证和处理直到最新的检查点的文件区域,即已知的 defined 状态的文件区域。无论一致性和并发问题怎样,这种方法都很好地为我们服务。append 比随机写要高效的多,而且在面对应用程序故障时更有弹性。检查点允许写者递增的重新开始(即可以从更新的检查点处接着写),阻止读者处理已经成功写入,但还未对应用程序可见(即在应用程序认为还不完整)的数据。
另一个典型的用法,很多写者并发 append 一个文件,以获取合并结果或作为一个生产者-消费者队列。Record append 的 append 至少一次语义保留了每个写者的输出。
读者通过下面的方法来处理偶然的填充或者 record 的复制。每个 record 由写者准备好,包含了诸如校验和这种额外信息,这样 record 的有效性就可以验证。读者使用校验和,可以区分填充和 recored 片段。如果应用程序不能容忍偶尔的重复(例如,如果重复的记录会触发非幂等操作),它可以使用记录中的唯一标识符将它们过滤掉,这通常是命名相应应用程序实体(例如 Web 文档)所必需的。这些 record I/O 的功能(除了重复记录的移除)在我们应用程序共享的库代码中,并且也适用其他 Google 的文件接口实现。这样,相同的 record 序列加上极少的重复,总是会被传送给 record 读者。
3. 系统交互
Google 设计 GFS 系统交互要最小化在所有的操作中对 master 的涉及(因为 master 只有一个,必须减轻 master 的压力)。在这个背景下,我们现在来说客户端、master 和 chunkserver 如何互动以实现数据修改、原子记录追加(append),以及快照(snapshot)。
3.1. 租约和修改顺序
像 write 或 append 的修改操作是会改变 chunk 的内容或者元数据的。每次修改都会应用在 chunk 的所有副本上。我们使用租约来维护一个在副本之间一致的修改顺序。master 授予一个 chunk 租约给副本之一,我们称这个副本为 primary。Primary 为所有修改挑选一个顺序给 chunk。当应用修改时,所有的副本都遵循这个顺序。因此,全局修改顺序先由 master 选择的租约授予顺序定义,在租约内由 priamry 指定的序列号定义。
租约机制是设计用来最小化 master 的管理开销的。一个租约有一个初始的 60s 的超时时间。然而,只要 chunk 被修改,primary 就可以向 master 请求延时,并且通常会收到延时的许可,并且这不限制次数。这些扩展请求与授权是附带在 master 和所有 chunkservers 之间交换的常规的心跳(HeartBeat)信息中的。master 有时可能会尝试在一个租约到期前将其撤销(例如,master 想要禁用一个正在重命名的文件上的修改)。即便 master 与一个 primary 失去了联系,master 也可以在旧的租约到期后安全地向另一个副本授予租约。
在图 2 中,我们通过列出 write 控制流描述了这个过程,并且用数字标记了步骤顺序。
- 客户端向 master 询问,哪个 chunkserver 持有要访问的 chunk 当前的租约,以及其他副本的位置。如果目前没有任何一个 chunkserver 持有要访问的 chunk 的租约,master 就会选择一个副本,授予一个租约(没有在图上显示出)。
- master 向客户端回复 primary 的标识和其他副本(图中 secondary 标记,所有除了 primary 的副本都是 secondary)的位置。客户端缓存这个数据,用于将来的修改操作。只有当 primary 变得不可达,或副本不再持有租约时,客户端才需要再次联系 master。
- 客户端把数据 push 给所有的副本,客户端可以以任意的顺序 push。每个 chunkserver 将会在一个内部的 LRU buffer 缓存这些数据,直到这些数据被使用或老化。通过将数据流与控制流解耦,我们可以通过基于网络拓扑调度昂贵的数据流来提高性能,而不管哪个 chunkserver 是 primary 的。我们会在 3.2 进一步讨论这一点。
- 一旦所有的副本都确认收到了数据,客户端就向 primary 发送一个 write 请求。这个请求标识了早前 push 给所有副本的数据。primary 给其收到的所有修改指定连续的序列号,由于这些修改可能来自多个客户端,所有进行编号是有必要的。primary 按着序号的顺序将修改应用到自己的本地状态。
- primary 把 write 请求传递给所有的 secondary 副本,每个 secondary 副本以由 primary 指定的同样的序列号顺序应用修改。
- 所有完成了操作的 secondary 向 primary 回复,表明他们已经完成了操作。
- primary 回复客户端,在任何副本上遇到的任何错误都会报告给客户端。在有错误的情况中,write 可能已经在 primary 和部分 secondary 中成功完成了,(如果操作是在 primary 这里失败了,那么其就不会被指定序列号并向 secondary 传递命令。个人理解:因为是 primary 先成功完成修改后,才会让 secondary 开始应用修改,如果 primary 失败了,secondary 就不会收到序列号以及应用更改的命令。)此时客户端会认为请求已经失败,已经修改完的区域就会处于 inconsistent 的状态。我们的客户端代码通过重试失败的修改来处理这种错误,即将在步骤 3 到 7 进行几次尝试,如果仍然没有成功,就会退回到 write 开始时(从步骤 1 开始)进行重试,直到操作完全成功(有一种最坏的情况是重试的时候客户端挂了,这种情况的数据可能最后就是不一致的了,GFS 毕竟是弱一致性的 )。(Q:这里有个疑问是,已经完成操作或部分完成操作的副本,接收到重试的数据后,如何处理?A:直接在文件末尾(最后一个 chunk 末尾)继续写入,之前成功的 secondary 会重复写入,去重任务由读取数据的客户端来完成。)
如果应用程序的 write 很大或者跨过了一个 chunk 的边界,GFS 客户端代码就会把其拆成多个 write 操作。这些新的 write 操作也都遵循上述控制流(图 2),但可能会与来自其他客户端的并发操作交错并被覆盖。因此,共享文件区域最终可能包含来自不同客户端的片段,尽管副本将是相同的,因为单个操作在所有的副本上以相同的顺序成功完成。这就会出现我们在 2.7 中提到过的 consistent 但 undefined 的状态。
3.2. 数据流
我们解耦了控制流和数据流以高效的使用网络。当控制流从客户端到 primary 再到所有的 secondary 时,数据是以流水线的方式沿着精心挑选的 chunkservers 链路线性 push 的。我们的目标是充分利用每个机器的网络带宽,避免网络瓶颈和高延迟链路,同时最小化 push 全部数据的延迟。
为了充分利用每台机器的带宽,数据是线性地沿着一个 chunkserver 链路 push 的,而不是分布式地 push (例如,树)。因此,每台机器全部的向外的带宽都被用来尽可能快的传输数据,而不是像分布式那样把数据在多个接受者之间分配。
为了尽可能的避免网络瓶颈和高延迟链路(例如,交换机见链路通常两者都有),每个机器向网络拓扑中“最近的”还没有收到数据的机器传递数据。假设客户端正在 push 数据,要将数据 push 至 chunkserver S1 到 S4。客户端先把数据发送到最近的 chunkserver,这里记为 S1。S1 将数据传递给 S2 到 S4 中离它最近的,这里记为 S2。类似的,S2 再把数据传递给 S3 或 S4,选择离它更近的,等等以此类推。我们的网络拓扑足够简单,因为“距离”可以通过 IP 地址来准确的估算(GFS 没有考虑异地备份这种,GFS 一般是部署在单个机架或者数据中心的。结合现实,同一个数据中心的 IP 地址分配一般是有规律的,所以通过 IP 就能知道大概位置,也就知道了距离)。
最终,我们通过 TCP 连接流水线式的传输数据最小化了延迟。一旦一个 chunkserver 收到了一些数据,它就会立即向其他 chunkserver 传递传递这些数据。对我们来说,流水线式传输数据是非常有用的,因为我们使用的是全双工链路的交换网络。立即发送数据不会减小接收速率。不考虑网络拥堵的话,把 $B$ 字节数据发送到 $R$ 个副本的理想的时间消耗是 $B/T+RL$,$T$ 是网络吞吐量,$L$ 是在两个机器上传输字节的延迟。我们的网络链路通常是 100 Mbps ($T$),$L$ 远远小于 1ms。因此,理想情况下,把 1 MB 散布出去需要大约 80ms。
3.3. 原子记录追加(append)
GFS 提供了一个原子的 append 操作,称为 record append。在传统的 write 操作中,客户端指定写入数据的偏移位置。对同一个区域的并发写不可以串行化,因为这个区域最终可能包含来自多个客户端的数据片段。而在 record apppend 中,客户端仅给出数据,由 GFS 选择偏移并把偏移返回给客户端,GFS 将在这个偏移处原子地 append 数据至少一次(即一个连续的字节序列)。这与在 Unix 中写入一个以 O_APPEND
模式打开的文件类似,多个写者并发写入的时候不会有冲突条件。
Record append 在我们的一些分布式应用程序(会有在不同的机器上的客户端并发 append 同一个文件的场景的分布式应用程序)中被重度使用。在这种情况下,如果客户端采用传统的 write,那么就额外需要复杂且昂贵的同步,例如通过一个分布式锁管理器。在我们的工作负载中,像这样的文件通常用作多生产者/单消费者队列,或包含来自很多不同的客户端的合并后的结果。
Record append 是修改操作的一种,也适合我们 3.1 中说到的控制流,只是在 primary 中有一点额外的逻辑。客户端将数据 push 给文件的最后一个 chunk 的所有副本,然后给 primary 发送请求。primary 检查如果将这个 record 记录 append 到当前的 chunk 是否会导致 chunk 的大小超过最大值(64 MB)。如果会超过,primary 就会将当前 chunk 填充至最大大小,并告诉所有 secondary 也这么做,然后回复客户端,说明这个操作将在下一个 chunk 上重试。(Record append 单次 append 的数据大小被限制为至多为 chunk 最大大小的 1/4,以将最坏情况下的碎片保持在可接受的水平。)如果在当前 chunk 上 append 这个 record 不会使得这个 chunk 的大小超过最大值(这是通常的情况),那么 primary 就会把这个记录 append 到其本地自己的副本,然后告诉 secondary 在精确的偏移处写入这些数据,最后将成功的通知回复给客户端。
如果一个 record append 在某个副本上失败了,客户端会重试操作。结果就是,同一个 chunk 的副本可能包含不同的数据,同一个 record,有的 chunk 中有完整的,有的只有一部分。GFS 不保证所有副本都完全一样(每个字节都一样),只会保证数据作为一个原子单位被写入至少一次。这个属性很容易从简单的观察中得出,即操作报告成功必须是数据在一些 chunk 的所有副本的同样的偏移处写入完成。进一步说,在操作报告成功后,所有的副本至少与 record 末尾一样长,因此未来的 record 将会被指定一个更高的偏移或一个不同的 chunk,即便后面会有一个不同的副本成为 primary。就我们的一致性保证而言,成功的 record append 操作写入数据的区域是 defined(因此也是 consistent),而没有完全成功写入的区域是 inconsistent 的(因此也是 undefined 的)。 我们在第 2.7.2 中讨论过,我们的应用程序可以处理 inconsistent 的区域。
3.4. 快照(snapshot)
快照操作几乎瞬间就可以制作一个文件或一个目录树(即,源)的拷贝,同时最大可能的减少中断正在进行中的修改。我们的用户使用快照来快速创建一个大数据集(经常是副本的副本(这段没读懂),递归)的分支副本,或者在试验修改前创建一个当前状态的检查点,稍后的提交或者回滚可以轻松些。
我们使用标准的写时复制(copy-on-write) 技术来实现快照。当 master 收到一个快照请求时,先撤销要做快照的文件中的 chunks 的所有未到期的租约。这保证了任何后来的对于这些 chunks 的 write 操作需要和 master 互动以寻找一个租约持有者(找不到就没法写),这会让 master 有机会先去创建这个 chunk 的一个新的副本。
在租约被撤回或者到期以后,master 将操作记录在磁盘上。master 随后通过复制源文件或目录树的元数据将此日志记录应用于其内存状态。最新创建的快照文件指向与源文件相同的 chunk。(这段没读懂)
在快照操作后,一个客户端第一次想要 write 一个 chunk C
,要给 master 发送一个请求,为了得到当前的租约持有者。master 注意到 chunk C
的引用数大于 1(写时复制方法创建快照时是给这个 chunk 加一个引用计数,没有立刻真的拷贝。一个 chunk 的引用计数大于 1 的话就代表这个 chunk 是某个快照的一部分,要保留原样数据的。当这个 chunk 上有新的写入的时候,这个 chunk 才会真的被复制,客户端在新复制的 chunk 上写入,而原来的旧 chunk 被快照继续引用),于是推迟回复客户端请求并选择一个新的 chunk 句柄 C'
。然后 master 让每个有 C
当前副本的 chunkserver 创建一个名为 C'
的新 chunk。通过在和原来相同的 chunkserver 上创建这个新的 chunk,我们可以确保数据可以在本地复制,而不经过网络(我们的磁盘速度大约是我们 100 Mb 以太网链路的三倍快)。从这一点看,对于任何 chunk 来说,请求操作一模一样:master 授予一个新的租约给新的 chunk C'
,回复客户端,客户端可以正常 write 这个 chunk,而不知道这个 chunk 其实是刚刚从已存在的 chunk 新创建的。
4. master 操作
所有的命名空间操作都由 master 执行。
此外,master 对 chunk 副本的管理贯穿整个 GFS 系统:
- master 决定在哪放置 chunks 副本;
- 创建新的 chunks 和之后的副本;
- 协调各种各样的系统范围内的活动以保持 chunks 完全拷贝;
- 在所有的 chunkservers 上做复杂均衡;
- 回收未使用的存储空间。
下面我们深入讨论下上述的几点。
4.1. 命名空间管理和锁
很多 master 的操作会花很长的时间。例如,一个快照操作必须撤回快照覆盖的所有 chunks 在 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
上的写锁(Q:这里为什么需要 /home/user
上的写锁?A:我的理解是为了和后面创建文件操作需要的读锁互斥。)。文件创建操作要获取 /home
和 /home/user
上的读锁,以及一个 /home/user/foo
上的写锁。这两个操作将会被按正确的顺序执行,因为他们尝试获取 /home/user
上冲突的锁(快照操作要的写锁与文件创建操作要的读锁)。文件创建操作不需要父目录(这里即 /home
和 /home/user
)上的写锁,因为没有“目录”或类似 inode 那样的数据结构需要在修改操作中受保护,而命名空间上的读锁可以有效地确保父目录不会被删除(这里即为 /home
和 /home/user
上的读锁可以保护他们不被删除)。
上述锁策略的一个好处是,允许在同一个目录内的并发修改。举个例子,在同一个目录中可以并发的创建多个文件:每个创建文件操作获取一个目录名字上的读锁和一个文件名字上的写锁。目录名字上的读锁足以阻止目录被删除、重命名或被创建快照(前面说过创建快照需要获取写锁)。文件名字上的写锁会连续两次尝试创建同名的文件(这句话黑人问号脸!)。
由于命名空间可能会包含很多结点,所以读写锁对象是延迟分配的,并且一旦不用了就会被删除。另外,多个锁要以一个一致的总体顺序被获取,以防死锁:这些锁会先被按照命名空间树的级别排序,同级别之间则按照字典序。
4.2. 副本放置
一个 GFS 集群高度分布在多个级别上。GFS 集群往往在很多个机器机架上含有数百个 chunkservers 。这些 chunkservers 可能轮流被来自相同或不同机架上的数百个客户端连接。在不同机架上的两个机器间通信可能经过一个或多个网络交换机。此外,出入一个机架的带宽可能小于这个机架中所有机器的总带宽。多级分布提出了一个特别的挑战,即在保证可伸缩性、可靠性和可用性的前提下,分发数据。
chunk 副本的放置策略服务于两个目的:(1) 最大化数据的可靠性和可用性,(2) 最大化网络带宽利用率。为了实现这两个目的,仅仅跨机器传播副本是不够的,因为这只是能抵御磁盘或机器故障,以及能充分利用每个机器的网络带宽。我们必须也跨机架传播 chunk 副本,这可以确保即便整个机架都损坏了或者离线了(例如,由于共享资源故障,如网络开关或电源电路)。这也意味着,关于一个 chunk 的流量,尤其是读,可以充分利用多个机架的总带宽。但另一方面,写流量必须流经多个机架,这是一个我们乐意看到的权衡。
4.3. 创建(Creation)、重新复制(Re-replication)、重新平衡(Rebalancing)
chunk 副本将在下面三种情况下创建:chunk 创建、重新赋值、重新平衡。
master 在创建(create)一个 chunk 时会选择一个位置来放置初始的空的副本。这考虑到了几个因素,(1) 我们想把新的副本放在磁盘空间利用率低于平均值的 chunkservers 上。随着时间推移,这个方法会使得各个 chunkservers 上的磁盘利用率相等。(2) 我们想限制在每个 chunkserver 上“最近”创建的数量。尽管创建操作本身开销很低,但创建操作会可靠的预测即将到来的大量的写流量,因为 chunk 是在写操作有要求时创建(这里就是说,在一个 chunkserver 上创建一个新的 chunk,创建操作本身开销不大,但是接下来往往会有写操作,如果“最近”创建的 chunk 太多,那么意味着后面会有太多的写流量,这会加重这个 chunkserer 的压力)。在我们的 append 一次读多次(append-once-read-many) 的工作负载中,当 chunk 被完全写入完成以后,通常会变成实际上的只读。(3) 像上面讨论的那样,我们想在跨机架传播一个 chunk 的副本。
当副本的有效数量低于用户指定的值时,master 重新复制(re-replicates)一个 chunk。可能导致重新复制操作发生的原因多种多样:(1) 一个 chunkserver 变得不可用,则会给 master 报告它上面的副本可能损坏;(2) chunkserver 上的磁盘之一由于错误变得不可用;(3) 或者用户指定的副本数量增加了。
基于下面几个因素需要被重新复制的 chunk 会被优先处理:(1) 当前副本数量与目标副本数量相差太多。例如,相比丢失了一个副本的 chunk,我们会更优先处理丢失了两个副本的 chunk 的重新复制操作。(2) 我们倾向先处理存在的文件的 chunk 的重新复制操作,而不是最近删除的文件(详见 4.4)。(3) 最后,为了最小化故障对正在运行的应用程序带来的影响,我们提高任何使得客户端进程阻塞的 chunk 的优先级。
master 选择优先级最高的 chunk,通过指示一些 chunkservers 直接从一个现存的有效副本拷贝 chunk 数据来“克隆”这个 chunk。新副本的放置策略的目标和新建 chunk 的放置类似:均衡磁盘空间利用率,限制任一单个 chunkserver 上的活跃的克隆操作数,以及跨机架传播副本。为了防止克隆流量大于客户端流量太多,master 同时在整个集群上和每个 chunkserver 上限制活跃克隆操作的数量。此外,每个 chunkserver 通过减少其向源 chunkserver 的读请求来限制其花在每个克隆操作上的带宽总量。
最后,master 定期重新平衡(rebalances) 副本:master 检查当前副本的分布,然后移动一些副本,为了更好的利用磁盘空间,以及更好的负载均衡。通过这个过程,master 也可以逐渐填满一个新的 chunkserver,而不是立即用新的 chunks 和随之而来的大量写流量将其淹没。新副本的放置标准也和上面讨论过的类似。此外,master 也必须选择要移除哪个现存的副本。一般来说,master 倾向于移除那些空闲空间低于平均值的 chunkservers 上的 chunk,以平衡各个 chunkserver 磁盘空间的使用。
4.4. 垃圾回收
在一个文件被删除后,GFS 不会立即回收其有效的物理存储空间。master 只会在文件级别和 chunk 级别的垃圾回收期间,延迟回收物理存储。我们发现这个方法使得 GFS 系统更简单,更可靠。
4.4.1. 机制
当一个文件被应用程序删除,master 和对其他的修改一样,立即记录删除日志。然而,这个文件只是被重命名为一个包含了删除时间戳的隐藏的名字,而不是立即回收了其资源。在 master 对文件系统命名空间的的定期扫描过程中,master 移除任何这样的,已经存在超过 3 天(内部可配置的)的隐藏文件。在这之前,被删除的文件仍然可以通过新的、特殊的名字(即被重命名后的带有删除时间戳的名字)被读取,也可以通过把名字改回正常名字取消删除。当隐藏文件被从命名空间中删除时,其内存中的元数据也会被删除。这有效地切断了它与所有 chunk 的链接。
类似的,在 master 对 chunk 命名空间的定期扫描中,master 识别孤儿 chunks(即不能从任何文件到达这个 chunk),并删除这些孤儿 chunks 的元数据。在与 master 的定期交换的心跳(HeartBeat)消息中,每个 chunkserver 报告其持有的 chunks 的一个子集,master 向 chunkserver 回复已经不在 master 存储的元数据中的所有 chunks 的身份信息,然后 chunkserver 就可以自由删除这些 chunks 的副本了。
4.4.2. 讨论
尽管分布式垃圾回收是一个难题,需要在编程语言的上下文中解决复杂的问题,但对于我们的 GFS 来说相当简单。我们可以轻松识别 chunks 的所有引用:master 维护着专门的文件到 chunk 的映射。我们也可以轻松识别所有的 chunk 副本:所有的副本都在每个 chunkserver 下一个指定的目录中。另外,任何 master 不知道的副本都被视为“垃圾(garbage)”。
这种存储回收利用的垃圾回收方法,相比即时删除有几个优势。首先,在组件故障很常见的大规模的分布式系统中更简单更可靠。chunk 的创建可能在一些 chunkservers 上成功了,而在另一些 chunkserves 上失败了,留下了一些 master 不知道存在的副本(这里我的理解是,若有部分 chunkserver 上的 chunk 创建失败了,就会重做所有的操作,那么成功创建了 chunk 的 chunkserver,其上的 chunk 就不被 master 认可,即 master 不知道的存在的副本)。副本删除信息可能丢失,master 必须记得在失败时重新发送这些信息,不论这个失败是由 master 自己还是由 chunkservers 导致的。垃圾回收提供了一个统一的、可靠的方法来清理任何未知有用的副本。第二,这种垃圾回收方法会将存储回收操作合并到 master 常规的后台活动中,就像定期扫描命名空间和与 chunkservers 定期握手一样。因此,存储回收操作是分批完成的,其开销分摊到了各个 master 常规的后台活动中。此外,存储回收操作只在 master 相对空闲的时候执行,这样 master 可以更迅速地响应需要及时关注的客户请求。第三,存储空间的延迟回收提供了一个防止意外的、不可逆的删除的安全网(这里我的理解是,意外删除的文件,在其被真正回收之前,是可以撤销删除的)。
根据我们的经验,这种垃圾回收方法最主要的缺点是,当存储空间紧张时,延迟垃圾回收有时会阻碍用户努力微调空间的使用。重复创建和删除临时文件的应用程序可能无法立即重用存储空间。如果已删除的文件再次被明确删除,我们会通过加快存储回收来解决这些问题。我们也允许用户在命名空间的不同部分应用不同的复制和回收策略。例如,用户可以指定一些目录树中的文件中的所有 chunks 存储时不复制,任何删除的文件都会立刻且不可撤销地被从文件系统状态中移除。
4.5. 过期副本检测
如果 chunkserver 对一个 chunk 的修改失败,或在其挂掉的时候错过了一些修改,就可能导致 chunk 副本过期。对于每个 chunk,master 维护一个 chunk 版本号( chunk version number),以区别最新的和过期的副本(这个版本号也会记录在日志中,是非易失的)。我们永远不会在过期的副本上应用更改,过期的副本只能等待回收。
每当 master 给一个 chunk 授予一个新的租约(注意,是每授予租约时增加版本号,不是每次修改时!),master 都会增加这个 chunk 的版本号并且通知这个 chunk 的最新的那些副本。master 和这些副本都会在他们的持久化的状态中记录这个新的版本号。这个过程发生在任何客户端被通知之前,因此也发生在开始向 chunk 写入之前。如果某个副本当前不可用,那它的版本号就不会增加。当 chunkserver 重启并向 master 报告其包含的 chunks 集合,以及这些 chunks 相关联的版本号时,master 就会检测到这个 chunkserver 有一个过期的副本。如果 master 看到一个版本号大于它自己的记录中的版本号,那么 master 就认为自己在授予租赁权时故障了,因此将更高的版本作为最新的版本。
master 在其定期的垃圾回收中移除过期的副本。在这之前,当 master 回复客户端对 chunk 信息的请求时,master 实际上会认为根本不存在一个过期的副本(也就是说,给客户端返回的 chunk 列表中可能包含过期的 chunk,客户端有可能去读过期的 chunk。GFS 是弱一致性的)。作为另一个保障措施,当 master 告知客户端哪个 chunkserver 持有一个 chunk 上的租约,或当 master 在一个克隆操作中指示一个 chunkserver 去从另一个 chunkserver 中读一个 chunk 时,master 包含这个 chunk 的最新版本号,客户端或 chunkserver 执行操作时会验证 chunk 的版本号,以便始终访问最新的数据。
5. 容错和诊断
在设计 GFS 系统时,我们最大的挑战之一是,如何解决频繁的组件故障。组件的质量和数量一起使得这些问题成为常态而不是意外。我们不能完全相信机器,也不能完全相信这些磁盘。组件故障可能会导致系统不可用,更糟糕的是导致数据损坏。这一节我们讨论我们如何面对这些挑战,还有我们在 GFS 系统中构建的一些工具,这些工具用来在故障不可避免的发生时诊断问题。
5.1. 高可用性
在 GFS 集群中的数百个服务器中,某些服务器在任何给定的时间都必然不可用。
我们通过两个简单的但有效的策略来保持系统的高可用性:快速恢复和复制。
5.1.1. 快速恢复
master 和 chunkserver 都被设计为,无论他们是如何终止的,都会在几秒恢复他们的状态并启动。事实上,我们不去区分正常或不正常的终止;服务器只会通过杀死其进程来例行关机。当客户端和其他服务器在他们未完成请求中超时,重新连接重启的服务器并重试操作时会遇到一个小问题。6.2.2 节中会讲到观察到的启动时间。
5.1.2. chunk 复制
像之前讨论过的那样,每个 chunk 在不同的机架上的多个 chunkservers 上复制。用户可以为文件命名空间的不同部分指定不同的复制级别,默认为 3 个复制。master 会根据需要克隆现存的副本,以在 chunkservers 离线或通过校验和验证(详见 5.2)检测到损坏的副本时,保持 chunk 有足够数量的副本。尽管对我们来说,复制机制工作的很好,我们依然正在探索其他形式的跨服务器冗余,例如奇偶校验码或纠删码,以满足我们不断增长的只读存储需求。我们预计在我们耦合很宽松的系统中实现这些更复杂的冗余策略是有挑战的但容易管理的,因为我们的流量主要是追加(append)和读取(read),而不是小的随机写入。
5.1.3. Master 复制
为了可靠性,master 状态将会被复制。master 的操作日志和检查点被复制到多个机器上。只有在其日志记录被 flush 到本地磁盘和所有 master 副本上之后,状态的修改才被视为已提交。为简单起见,一个 master 进程仍然负责所有修改以及后台活动,例如在内部更改系统的垃圾回收。master 进程在挂掉后可以几乎瞬间重启。如果是 master 进程所在的机器或者磁盘坏了,在 GFS 外部的监控设施会利用 master 操作日志的副本在别处启动一个新的 master 进程。客户端只通过 master 的规范名(如 gfs-test)与其通信,规范名是一个 DNS 别名,如果 master 被重新放到其他机器上,这个别名可以修改。
此外,“影子” masters (论文原文即为 masters,复数,所以这里应该是说“影子”可能有多个)提供了对文件系统的只读连接,即便主 master 挂掉了,“影子” masters 依然可以正常工作。注意这里说的是“影子”而不是“镜像”,也就是说“影子” masters 可能落后主 master 一点,通常是几分之一秒。“影子” master 增强了没有正在被活跃修改的文件和不在意获取有一点旧的结果的应用程序的读可用性。事实上,由于文件内容是从 chunkservers 读的,所以应用程序不会注意到文件内容是旧的。在短窗口内可能旧的是像目录内容或连接控制信息这样的文件元数据。
“影子” masters 为了保持其能知道情况,一个“影子” master 读取逐渐增长的操作日志的副本,并在其自己的数据结构中严格应用和主 master 一样的修改序列。“影子” master 和主 master 一样,在启动时(此后就很少了)轮询 chunkservers 来定位 chunk 副本,并且频繁和 chunkservers 交换握手信息以监控他们的状态。“影子” masters 依赖主 master 的只有副本位置更新结果,根据主 master 的决策来创建和删除副本。
(关于这节的“影子” master,有几个问题。(1) 影子 masters 和主 master 同时存在,都轮询 chunkservers,也就是说 chunkservers 同时要和多个 master 握手、保持联系吗?(2) 最后一段仅说了从主 master 获取创建或删除副本的信息,那修改的文件信息如何更新?)5.2. 数据完整性
每个 chunkserver 都用校验和来检测其存储的数据的损坏。考虑到一个 GFS 集群经常在数百机器上有数千磁盘,其经常会经历磁盘故障,导致读和写路径上的数据损坏或丢失。(原因之一见第 7 节)我们可以用其他的 chunk 副本恢复损坏的数据,但通过跨 chunkservers 比较副本来检测损坏是不切实际的。此外,不同的副本(同一个 chunk 的)可能是合法的:GFS 修改的语义,特别是之前讨论过的原子 record append,不能保证副本都一样。因此,每个 chunkserver 必须通过维护校验和来独立验证其自己的拷贝的完整性。(这里,副本之间可以不同是什么鬼,回头再看看。)
一个 chunk 被分成 64 KB 的块,每个块有一个相应的 32 比特的校验和。像其他元数据一样,校验和也会保存在内存中,并且通过日志持久化的存储,与用户数据分开。
对于读,在返回任何数据给请求者(无论是客户端还是另一个 chunkserver)之前,chunkserver 会验证与读取范围重叠的数据块的检验和。因此 chunkservers 不会把损坏的数据传播到其他机器上。如果一个块没有匹配记录的检验和,chunkserver 就会返回一个错误给请求者,并且将不匹配的情况报告给 master。作为回应,请求者将会从其他副本读取数据,而 master 将从另一个副本克隆 chunk。在一个有效的新的副本放置好后,master 指示报告了不匹配的 chunkserver 删除其副本。
由于以下几个原因,校验和对读性能几乎没有影响。由于我们大部分的读至少会跨越几个块,因此我们只需要读取和校验相对少量的额外数据以进行验证。GFS 客户端代码通过尝试在校验和块边界对齐读取来进一步减少这种开销。此外,chunkserver 上的校验和查找和比较不需要任何 I/O 就可以完成,并且检验和计算通常会与 I/Os 重叠。(这一段都没读懂。(1) 为什么读取的数据跨越几个快就可以只读、验证少量的额外数据?(2) 什么是对齐读?(3) 校验和查找和比较为什么不需要 I/O,是校验和记录存在内存中吗?校验和计算和 I/Os 重叠是什么意思?)
检验和计算为 append 到 chunk 末尾的写(而不是覆盖已存在数据的写)做了大量优化,因为在我们的工作负载中主要都是 append。我们只是逐渐更新最后部分的校验和块,并为由 append 填充的任何全新的校验和块计算新的校验和。即使最后部分的校验和块已经损坏,而且我们当前又无法检测出,也没什么关系,因为新的检验和将无法匹配已存储的数据,所以损坏通常会在这个块下次被读取时被检测出来。(检验和块存在哪?)
作为对比,如果一个 write 覆盖现有 chunk 的一个范围,我们必须读并验证被覆盖范围的第一个和最后一个块,然后执行这个 write,最终计算并记录新的校验和。如果我们在部分覆盖之前不验证第一个和最后一个块,新的校验和可能会隐藏存在于没有被覆盖区域中的损坏。(这里我的理解是,被覆盖的范围,中间部分的块校验和就没必要验证了,因为数据被全部覆盖了。而对于第一个和最后一个块,只有一半会被覆盖,所以先要验证一下,以保证没有呗覆盖的那部分数据是正确的。)
在空闲时期,chunkservers 可以扫描并验证不活跃 chunk 的内容,这使得我们可以检测出很少读的 chunk 中的损坏。一旦检测到损坏,master 就可以创建一个新的未损坏的副本并删除损坏的副本。这样可以防止一个不活跃但损坏的 chunk 副本骗过 master,使得 master 认为对应 chunk 还有用足够的有效副本。
5.3. 诊断工具
广泛而详细的诊断日志记录在问题隔离、调试和性能分析方面提供了不可估量的帮助,同时只产生了最小的成本。如果没有日志,我们很难理解机器之间短暂、不可复现的交互。GFS 服务生成诊断日志来记录很多重大事件(例如 chunkservers 的加入或 down 机)以及全部的 RPC 请求和回复。这些诊断日志可以随意删除而不会影响整个系统的正确性,不过在空间允许的情况下,我们会尽可能保留这些日志。
RPC 日志包括在线上发送的确切请求和响应,不包括被读取或写入的文件数据。 通过将请求与应答进行匹配,并整理不同机器上的RPC记录,我们可以重构整个交互历史来诊断问题。日志还可以作为负载测试和性能分析的跟踪。
日志记录对性能的影响很小(而且其优点远远超过这个影响),因为这些日志是顺序和异步写入的。 大部分最近的事件也会保持在内存中,并且可用于持续的在线监控。
6. 性能测试
在这一部分,我们展示了几个微型基准测试来说明 GFS 架构和实现中的固有瓶颈,以及在 Google 中应用的真实集群的一些数字。
6.1. Micro-benchmarks
我们此微型基准测试的组成如下:
- 1 个 master;
- 2 个 master 副本;
- 16 个 chunkservers;
- 16 个客户端。
注意这个配置主要是为了方便测试,真实的集群往往包含数百 chunkservers 和 数百客户端。
所有的机器都配备 dual 1.4 Ghz PIII 处理器,2 GB 内存,两个 80 GB 5400 rpm 的磁盘,以及 100 Mbps 的全双工以太网连接到一个 HP 2542 交换机。全部的 19 个服务器机器都连接到一个交换机上,全部的 16 个客户端机器都连接到另一个交换机上,两个交换机之间通过 1 Gbps 链路连接。
6.1.1. 读
N 个客户端同时从文件系统中读。每个客户端从一个 320 GB 的文件集中读取一个随机选择的 4 MB 区域,重复 256 次,所以每个客户端最终读取了 1 GB 的数据。这些 chunkservers 加起来只有 32 GB 的内存,所以我们预计 Linux 缓冲区缓存中的命中率最多为 10%。 我们的结果应该接近冷缓存结果。
图 3(a) 展示了 N 个客户端总的读速率以及该速率的理论上限。当两个交换机之间的 1Gbps 链路饱和时,总的读速率的极限峰值在 125 MB/s,或者说当客户端的 100 Mbps 网络接口饱和时,每个客户端的读速率极限峰值是 12.5 MB/s,以适用者为准。当只有一个客户端在读时,我们观察到的读速率是 10 MB/s,即每个客户端 12.5 MB/s 理论峰值的 80%。当 16 个客户端都在读时,总的读速率达到了 94 MB/s,大约是理论峰值 125 MB/s 的 75%。这个效率从 80% 降到 75% 是因为,随着读者的增加,多个读者同时从同一个 chunkserver 读取数据的可能性也增加了。
6.1.2. 写
N 个客户端同时写 N 个不同的文件。每个客户端通过一组 1 MB 的 write 往一个新文件中写入 1 GB 数据。图 3(b) 展示了总的写速率以及理论极限。总的写速度的峰值稳定在 67 MB/s,因为我们需要把每个字节都写到 16 个 chunkservers 中的 3 个上,每个 chunkserver 有一个 12.5 MB/s 的输入连接。
只有一个客户端写时,观察到的写速率是 6.3 MB/s,大约是理论极限值的一半。导致这一问题的罪魁祸首是我们的网络堆栈,网络堆栈和我们用来给 chunk 副本推送数据的流水线方案交互得不是很好。从一个副本向另一个副本传播数据的延迟会降低总的写速率。
当 N = 16 时,总的写速率到了 35 MB/s(即每个客户端 2.2 MB/s),大约是理论极值的一半。和读一样,导致这个结果最可能的原因是随着客户端数量的增加,会有多个客户端并发写入同一个 chunkserver。此外,16 个写者比 16 个读者更可能发生冲突,因为每个写者要涉及 3 个不同的副本(读者只读副本之一)。
写比我们想要的更慢,不过在实践中这不是主要问题,因为虽然这增加了单个客户端所看到的延迟,但对系统分给大量客户端的总的写带宽没什么大的影响。
6.1.3. 记录追加(Record Appends)
图 3(c) 展示了 record append 的性能。N 个客户端同时追加同一个文件。性能受限于存储该文件最后一个 chunk 的 chunkserver 的网络带宽,独立于客户端的数量。从一开始的一个客户端时 6.0 MB/s 到 16 个客户端时的 4.8 MB/s,这个下降主要是由于网络拥塞,以及不同客户端看到的网络传输速率差异。
我们的应用程序倾向于并发生成多个这样的文件。换句话说,N 个客户端同时 append 到 M 个共享文件,N 和 M 都是几十或者数百。因此,在实践中,我们实验中的 chunkserver 的网络拥塞不是大问题,因为客户端可以在写入一个文件时取得进展,而另一个文件的 chunkserver 则处于繁忙状态。
6.2. 现实世界集群
现在,我们研究了 Google 内部使用的两个集群,它们代表了其他几个类似的集群。
集群 A 经常被一百多名工程师用于研发。典型的任务由人类用户启动,运行长达数小时。其读取从几 MBs 到几 TBs 的数据,传输或分析数据,以及将结果写回集群。
集群 B 主要用于生产数据处理。集群 B 中任务的持续时间要长得多,其持续地生成并处理数 TB 的数据集,期间仅有偶尔的人类干预。
在集群 A 和 B 中,一个单个的任务包含了在很多机器上的很多进程同时读和写很多的文件。
6.2.1. 存储
从表 2 中的前 5 行中可以看到,两个集群都有上百个 chunkservers,都支持很多 TB 的磁盘空间,且这些磁盘空间中有相当多的,但没有全部写满磁盘的数据。”Used disk” 包含所有 chunk 副本。实际上所有文件都会被复制 3 次(3 个副本),因此,A 和 B 两个集群分别存储了 18 TB(55 / 3 ≈ 18) 和 52 TB (155 / 3 ≈ 52)的文件数据。
两个集群有相似的文件数量(A: 735 k, B: 737 k),尽管 B 中有很大比例的 dead files(即被删除、或被新版本替换的,但是其存储空间还没有被回收的文件)。集群 B 还具有更多 chunk,因为集群 B 的文件往往更大。这里没太理解是由于 dead files 多导致的 chunk 多,还是集群 B 的普通文件就更大。
6.2.2. 元数据
chunkservers 一共存储了数十 GB 的元数据,其中大部分是 64 KB 用户数据块的校验和。chunkservers 持有的其他元数据只有我们在 4.5 讨论过的 chunk 版本号。
保存在 master 上的元数据要小得多,只有几十 MB,或者每个文件平均大约 100 个字节。这与我们的假设一致,即 master 内存的大小在实践中不会限制我们 GFS 系统的容量,每个文件的大多数元数据是以前缀压缩形式存储的文件名。其他元数据包括文件所有权和权限、从文件到 chunks 的映射以及每个块的当前版本。此外,对于每个 chunk,我们存储其当前的副本位置和用于实现写时复制(copy-on-write)的引用计数。
每个单独的服务器,不论是 chunkserver 还是 master,都只有 50 ~ 100 MD 的元数据。因此恢复是很快的:服务器在能够回答询问时前只需要花几秒钟来读其存储的元数据。但是,master 在一段时间内(通常为 30 到 60 秒)会有些受阻,直到它从所有 chunkservers 获取完 chunk 定位信息(这里我的理解是 master 启动时要轮询 chunkserver)。
6.2.3. 读写速率
表 3 展示了不同时期的读写速率。当我们进行这些测量时,A 和 B 两个集群都已经启动了大约一周。(集群最近已经重新启动(restart)以升级到新的 GFS 版本)
从重新启动开始,平均写速率小于 30 MB/s。当我们进行这些测量时,集群 B 处于写入活动的突发过程中,生成了大约 100 MB/s的数据,这产生了 300 MB/s的网络负载,因为写入被传播到三个副本。
正如我们所假设的那样,读取速率远高于写入速率,总工作负载包含的读取次数多于写入次数。这两个集群都处于繁重的阅读活动中。特别是,A 在前一周一直保持 580 MB/s 的读取率。A 的网络配置可以支持750 MB / s,因此它有效地利用了资源。集群 B 可以支持 1300 MB/s 的峰值读取速率,但其应用程序仅使用 380 MB/s。
6.2.4. master 负载
表 3 还显示,发送到 master 的操作速率约为每秒 200 到 500 次操作。master 可以轻松跟上这个速度,因此这不是这些工作负载的瓶颈。
在早期版本的 GFS 中,master 偶尔会成为某些工作负载的瓶颈,它花费大部分时间按顺序扫描大型目录(包含数十万个文件)以查找特定文件。此后,我们更改了 master 数据结构,以允许通过命名空间进行高效的二进制搜索。master 现在可以轻松支持每秒数千次文件访问。如有必要,我们可以通过在命名空间数据结构前面放置名称查找缓存来进一步加快速度。
6.2.5. 恢复时间
当一个 chunkserver 故障后,一些 chunks 的副本数量会不足,必须再克隆这些副本以保持这些副本的复制级别。恢复所有这些受影响的 chunk 副本需要的时间依赖于资源的总量。
在一个实验中,我们杀死了集群 B 中的一个单个的 chunkserver,这个 chunkserver 有大约 15000 个 chunks,这些 chunks 共包含 600 GB 的数据。为了限制恢复操作对正在运行的应用程序的影响,并为调度决策提供余地(这里没搞清楚“余地”的主语和宾语),我们的默认参数限制这个集群最多同时进行 91 个克隆操作(chunkservers 数量的 40%,227 x 0.4 ≈ 91),并且每个克隆操作最多可以消耗 6.25 MB/s(即 50 Mbps)的带宽。以有效拷贝速率 440 MB/s 进行 23.2 分钟后,所有的 chunks 都恢复完成,
在另一个实验中,我们杀死了两个 chunkservers,每个有大概 16000 个 chunks 和 660 GB 的数据。这两个故障使得有 266 个 chunks 变得只剩一个副本。这 266 个 chunks 被以更高的优先级克隆,在 2 分钟内全部恢复到至少有 2 个副本,从而使集群处于可以容忍另一个 chunkserver 故障而不会丢失数据的状态。
6.3. 工作负载分解
在这部分,我们会给出两个 GFS 集群的详细的工作负载分解,这两个 GFS 集群的工作负载和 6.2 中的相当,但不完全相同。
集群 X 用于研发,而集群 Y 用于生产数据处理。
6.3.1. 方法和注意事项
这些结果仅包括来自客户端的请求,因此它们反映了我们的应用程序为整个文件系统生成的工作负载。它们不包括执行客户端请求或内部后台活动的服务器间请求,例如转发写入或重新平衡。
有关 I/O 操作的统计信息基于从 GFS 服务器记录的实际 RPC 请求中以启发式方式重建的信息。举例来说,GFS 客户端代码可能会把读拆分进多个 RPC 中以提高并行性,我们从中推断出原始读取。由于我们的访问模式是高度程式化的,因此我们希望任何错误都会出现在噪声中。应用程序的显式日志记录可能会提供稍微更准确的数据,但从逻辑上讲,重新编译和重新启动数千个正在运行的客户端来这样做是不可能的,而且从尽可能多的机器收集结果也很麻烦。
有一点应该小心,就是不要从我们的工作负载中过度概括。由于 GFS 和使用 GFS 的应用程序都由 Google 完全控制,所以应用程序往往会为了 GFS 做调整优化,反过来 GFS 也是为这些应用程序设计的。这些相互的影响可能也存在于广泛的应用程序和文件系统中,但是这种影响在我们的案例中可能更明显。
6.3.2. chunkserver 工作负载
表 4 显示了操作不同大小数据的操作的次数分布。
读(Read)大小是一个双峰分布。小的读(小于 64 KB)来自在巨大的文件中查找数据的小片段的搜索密集型客户端。大的读(大于 512 KB)来自贯穿整个文件的长顺序读。
集群 Y 中有大量的读根本没有返回数据。我们的应用程序,尤其是那些在生产系统中的,经常使用文件作为生产者消费者队列。生产者并发地 append 一个文件,同时一个消费者读这个文件的末尾。偶尔会有一种情况,即当消费者赶超了这些生产者时,就不会有数据返回了。集群 X 的这种情况要少一些,因为集群 X 一般用于短期数据分析任务,而不是长期的分布式应用程序。
写(Write)大小也是一个双峰分布。大的写(大于 256 KB)往往起因于写者的大缓存。缓存较少的数据、检查点或更频繁地进行同步,或者只是生成较少数据的写者导致较小的写(小于 64 KB)。
至于记录追加(Record Append),集群 Y 的大型记录追加百分比比集群 X 高得多,因为我们使用集群 Y 的生产系统针对 GFS 进行了更积极的调整。
表 5 展示了在各种大小的操作中数据传输的总量划分。对于所有类型的操作,较大的操作(超过 256 KB)通常占传输的大部分字节。由于随机搜索负载,小读取(小于 64 KB)也确实会传输一小部分但重要的读取数据。
6.3.3. Append vs. Writes
记录追加非常常用,尤其是在我们的产品系统中。对于集群 X,write 与 record append 的字节传输比例是 108:1,操作数是 8:1。对于集群 Y(我们的产品系统使用的),上述比例分别是 3.7:1 和 2.5:1。
此外,这些比例表明在这两个集群中,record append 往往比 write 用的多得多。不过对于集群 X,在测量期间记录追加的总体使用率相当低,因此结果可能会受到具有特定缓冲区大小选择的一两个应用程序的影响。
和预期一样,我们的数据修改工作负载中,相比覆写,append 操作占绝对大头。我们测量了 primary 副本上覆写的数据总量,这近似于客户端故意覆盖以前写入的数据而不是附加新数据的情况。对于集群 X,覆写的总字节数低于 0.0001%,覆写的操作次数低于 0.0003%。尽管这个数据很微小,但仍然比我们预期的要高。 事实证明,这些覆写中的大多数来自由于错误或超时而导致的客户端重试,它们本身不是工作负载的一部分,而是重试机制的结果。
这里没理解,因为看数据好像是 write 多一些。。。6.3.4. master 工作负载
表 6 显示了对 master 的请求类型的细分。
大多数请求都要询问 chunk 位置 (FindLocation)用来读,和数据的租约持有者信息(FindLeaseLocker)用于数据修改。
集群 X 和 Y 看到的删除请求数量明显不同,因为集群 Y 存储着定期重新生成并替换为较新版本的生产数据集。这种差异进一步隐藏在 Open 请求的差异中,因为旧版本的文件可能会通过从头开始写入而被隐式删除(Unix 开放术语中的 mode "w"
)。
FindMatchingFiles 是一个模式匹配请求,支持 ls
和类似的文件系统操作。FindMatchingFiles 与其他对 master 的请求不同,它可能会处理大部分的命名空间,因此可能代价很高。集群 Y 能更频繁地看到 FindMatchingFiles,因为自动数据处理任务倾向于检查文件系统的某些部分以了解全局应用程序状态。相比之下,集群 X 的应用程序处于更明确的用户控制之下,并且通常提前知道所有需要的文件的名称。
7. 经验
在 GFS 的构建和部署过程中,Google 经历了各种各样的问题,有些事操作方面的,有些事技术方面的。
最初,GFS 被设想为我们生产系统的后端文件系统。随着时间的推移,其用途演变为包括研究和开发任务。GFS 开始时对权限和配额等内容的支持很少,但现在包括这些的基本形式。虽然生产系统受到良好的纪律和控制,但用户有时却不是,需要更多的基础设施来防止用户相互干扰。
我们最大的一些问题与磁盘和 Linux 有关。我们的许多磁盘都向 Linux 驱动程序声称它们支持一系列 IDE 协议版本,但实际上只对较新的版本做出可靠响应。由于协议版本非常相似,这些驱动器大部分都可以工作,但偶尔不匹配会导致驱动器和内核对驱动器的状态存在分歧。由于内核中的问题会默默地破坏数据,这个问题促使我们使用校验和来检测数据损坏,同时我们修改内核来处理这些协议不匹配。
早些时候,由于 fsync() 的开销,我们在使用 Linux 2.2 内核时遇到了一些问题。 它的成本与文件的大小成正比,而不是与修改部分的大小成正比。 这对于我们的大型操作日志来说是一个问题,尤其是在我们实施检查点之前。 我们通过使用同步写入解决了一段时间,最终迁移到 Linux 2.4。
另一个 Linux 问题是单个读者-写者锁,地址空间中的任何线程在从磁盘分页(读者锁)或在 mmap()
调用(写者锁)中修改地址空间时都必须持有该锁。 我们在低负载下看到我们系统中的短暂的超时,并努力寻找资源瓶颈或零星的硬件故障。 最终,我们发现这个单一的锁阻止了主网络线程将新数据映射到内存,而磁盘线程正在分页先前映射的数据。 由于我们主要受网络接口而不是内存复制带宽的限制,因此我们通过将 mmap()
替换为 pread()
来解决此问题,但代价是额外的副本。
尽管偶尔会出现问题,但 Linux 代码的可用性已经一次又一次地帮助我们探索和理解系统行为。 在适当的时候,我们会改进内核并与开源社区分享更改。
8. 总结
Google 文件系统展示了在商品硬件上支持大规模数据处理工作负载所必需的品质。虽然一些设计决策是针对我们独特的环境的,但许多可能适用于具有相似规模和成本意识的数据处理任务。
我们首先根据我们当前和预期的应用程序工作负载和技术环境重新检查传统的文件系统假设。我们的观察导致了设计空间中的根本不同点。我们将组件故障视为常态而不是例外,针对大部分附加(可能同时)然后读取(通常顺序)的大文件进行优化,并扩展和放松标准文件系统接口以改进整个系统。
我们的系统通过持续监控、复制关键数据以及快速自动恢复来提供容错能力。Chunk 副本允许我们容忍 chunkserver 故障。 这些故障的频繁发生激发了一种新颖的在线修复机制,该机制定期透明地修复损坏并尽快补偿丢失的副本。此外,我们使用校验和来检测磁盘或 IDE 子系统级别的数据损坏,考虑到系统中的磁盘数量,这变得非常普遍。
我们的设计为执行各种任务的许多并发读者和写者提供了高聚合吞吐量。我们通过将通过 master 的文件系统控制与直接在 chunkserver 和客户端之间传递的数据传输分离来实现这一点。通过大 chunk 大小和 chunk 租约,将权限委托给数据修改中的 primary 副本,可以最大限度地减少常见操作对 master 的涉及。这使得一个不会成为瓶颈的简单、集中的 master 成为可能。我们相信,我们网络堆栈的改进将解除当前对单个客户端看到的写入吞吐量的限制。
GFS 成功满足了我们的存储需求,并在 Google 内部被广泛用作研发以及生产数据处理的存储平台。它是一个重要的工具,使我们能够在整个网络的规模上继续创新和解决问题。
9. FAQ
Q1. 为什么 record append 是原子的追加至少一次,而不是确定一次?
3.1 小节,步骤 7 中描述了,如果一个 write 在 secondaries 中之一失败了,客户端会重试这个写。这会导致在没有出错的副本中,数据被重复追加了超过一次。一个不同的设计可能会检测到任意故障(例如,原始请求和客户端重试之间的 primary 故障)导致的重复客户端请求,但可能会为复杂性和性能付出相当大的代价。
Q2. 应用程序如何知道一个 chunk 中哪些部分包含填充和重复记录?
为了检测填充,应用程序可以在有效数据的开头输出一个可预测的 magic number,或者包含一个检验和,该校验和可能仅在记录有效时才有效。
应用程序可以通过在记录中包含唯一的 ID 来检测重复。如果应用程序读到一个与之前读到过的记录的 ID 相同的记录,就知道这个记录与前面的重复了。
GFS 为应用程序提供了一个库以处理上述这些情况。
Q3. 鉴于原子记录追加将其写入文件中不可预测的偏移量,客户如何找到他们的数据?
Append(GFS 一般也是)主要用于顺序读整个文件的应用程序。这类应用程序扫描文件以寻找有效记录,所以客户端不需要提前知道记录的位置。例如,该文件可能包含一组并发网络爬虫遇到的一组链接 URL。任何给定 URL 的文件偏移量都无关紧要,读者只是希望能够阅读整个 URL 集。
Q4. 什么是校验和?
校验和算法将一个字节块作为输入,并返回一个数字,该数字是所有输入字节的函数。
例如,一个简单的校验和可能是输入中所有字节的总和(mod some big number)。
GFS 存储每个 chunk 以及他们的检验和。当一个 chunkserver 在它的磁盘上写入一个 chunk 时,它首先计算新 chunk 的校验和,并将校验和和 chunk 一起保存在磁盘上。当 chunkserver 从磁盘读取 chunk 时,它也会读取之前保存的校验和,从磁盘读取的 chunk 中重新计算校验和,并检查两个校验和是否匹配。 如果数据被磁盘损坏,校验和将不匹配,并且 chunkserver 将知道返回错误。另外,一些 GFS 应用程序通过应用程序定义的记录在 GFS 文件中存储自己的校验和,以区分正确的记录和填充。
CRC32 是校验和算法的一个示例。
Q5. 论文中提到了“引用计数”,这是什么?
引用计数是为了快照(snapshots)的写时复制(copy-on-write)实现的一部分。
当 GFS 创建一个快照时,GFS 不会拷贝这些 chunks,但是会增加每个 chunk 的引用计数,这使得创建一个快照变得成本很低。
如果一个客户端写入一个 chunk 并且 master 注意到这个 chunk 的引用计数大于 1,master 首先会拷贝这个 chunk,然后客户端更新这个拷贝(而不是作为快照一部分的那个 chunk)。
我们可以将此视为延迟复制,直到绝对必要为止。 这个策略使得在创建快照时,不是所有的 chunk 都会被修改,并且可以避免制作一些副本。
Q6. 如果一个应用程序使用标准 POSIX 文件 API,其是否需要修改以适应 GFS?
是的。
但是 GFS 不适用于现存的应用程序。GFS 是为新编写的应用程序设计的,例如 MapReduce 程序。
Q7. GFS 如何确定最近的副本的位置?
GFS 论文中暗示了其基于存储了有效副本的服务器的 IP 地址来确定最近副本的位置。
在 2003 年,Google 必须以下面的方式指定 IP 地址,即,如果两个 IP 地址在 IP 地址空间中彼此靠近,那么它们在机房中也很靠近。
Q8. 假设 S1 是一个 chunk 的 primary,在 master 和 S1 之间的网络故障了。master 将会注意到并制定一些其他的服务器作为 primary,叫做 S2。由于 S1 没有真的故障,那么此时同一个 chunk 是否有两个 primary?
如果同时存在两个 primaries 会是一个灾难,因为两个 primaries 可能会对同一个 chunk 应用不同的更新。
幸运的是 GFS 租约机制预防了这种情况。master 给 S1 授予一个 60 秒的租约使 S1 成为 primary。S1 知道当其租约到期后就不再是 primary 了。在先前给 S1 授予的租约之前,master 不会给 S2 授予租约。所以在 S1 的租约到期之前,S2 不会作为 primary 开始活动。
Q9. 64 MB 大小的 chunk 是否听起来很尴尬?
64 MB 的 chunk 大小是 master 中的簿记(book-keeping)单位,以及文件在 chunkserver 上分片的粒度。
客户端可以发出较小的读取和写入 —— 他们不必被迫处理整个 64 MB 的块。
使用如此大的 chunk 大小的目的是减少 master 中元数据表的大小,并避免限制想要进行大量传输以减少开销的客户端。 另一方面,小于 64 MB 的文件不会获得太多的并行性。
Q10. Google 是否仍然在使用 GFS?(注:此问题问于 2020 年,GFS 论文发表于 2003 年)
有传言称 GFS 已被 Colossus 所取代,总体目标相同,但 master 性能和容错能力有所提高。
Q11. GFS 以正确性换取性能和简单性的接受程度如何?
这是分布式系统中的一个反复出现的主题。
强一致性通常需要复杂的协议,并且需要机器之间的闲聊(chit-chat)。
通过利用特定应用程序类可以容忍宽松一致性的方法,可以设计具有良好性能和足够一致性的系统。例如,GFS 为 MapReduce 应用程序进行了优化,这些应用程序需要对大文件的高读取性能,并且可以接受文件中的漏洞、记录显示多次和读取不一致的情况。另一方面,GFS 不适合在银行存储账户余额。
Q12. 如果 master 故障了怎么办?
有 master 的副本,此副本包含 master 状态的完全拷贝。该论文的设计需要人工干预才能在 master 故障后切换到其中一个副本(第 5.1.3 节)。
我们可以使用 Raft 构建具有自动切换到备份的复制服务。
Q13. 只有一个 master 是好主意吗?
这个设计简化了一开始的部署,但对于长期运行的程序来说确实不太好。
https://queue.acm.org/detail.cfm?id=1594206 一文中说过,随着岁月的流逝和 GFS 使用的增长,出现了一些问题。
- 文件的数量增长到了足够大,以至于将所有文件的元数据存储在单个 master 的 RAM 中不再合理。
- 客户端的数量增长到了足够大,以至于单个 master 没有足够的 CPU 能力为这些客户端服务。
- 实际中,从一个故障的 master 切换到一个其备份,需要人类干预,这使得恢复过程很慢。
Google 的 GFS 替代,Colossus,将 master 划分为了多个服务器,并且有更多的自动 master 故障恢复。