Lec 3 谷歌文件系统(GFS)
GFS论文是一个经典的论文,它是第一个用于数据中心应用的分布式文件系统,比如MapReduce,并且涉及到这门课的很多内容,比如并行性能,容错,复制,一致性等等。如今GFS已经被Colossus取代,总体目标一致,但是后者提高了协调者性能和容错。 并且,在谷歌内部很多应用已经迁移到类数据库的存储系统了,比如BigTable, 和Spanner。然而,GFS的设计仍然在HDFS里面,它是Hadoop开源的MapReduce的存储系统。总之,这是一篇很好的系统论文,提供了一个系统性的设计,从应用程序层面的细节到底层网络层的成功实现。
阅读资料
GFS总览
GFS的背景 很多Google服务需要一个高性能且统一的存储系统,比如MR、爬虫、索引器、日志存储/分析,为多个应用提供共享的数据,比如爬虫等。同时需要,海量容量,容错能力。但是这些只是在内部使用,只在提高批量处理大数据的性能,而非交互式。
组成: 数百到数千的客户端(例如,MR工作机器);数百个块服务器,每个都有自己的磁盘;一个协调者
容量 大文件被分割成64MB的chunk,每个文件的块被条带化/分片存储到不同的块服务器上,因此一个文件可以远大于任何一块磁盘容量。每个chunk在块服务器上存储为一个Linux文件。
吞吐量 客户端直接与块服务器通信来读写数据。如果大量客户端访问(读写)不同的块,就能实现巨大的并行吞吐量。
容错 每64MB的块被复制存储到3台块服务器上,客户端执行写入时,数据会被发送到一个块的所有副本上,而读取时只需查询一个副本
客户端访问流程
读取操作
客户端C将 文件名 和 偏移量 发送给CO (如果没有缓存)
CO 维护着:
filename -> 块句柄数组的映射表块句柄 -> 块服务器列表的映射表
CO根据偏移量找到对应的块句柄(chunk handler)
CO回复客户端,包含块句柄和存储该块的块服务器列表
C 缓存 这个句柄和块服务器列表
C向最近的一个块服务器发送请求。
- 请求包含了块句柄和偏移量
块服务器从磁盘上的块文件中读取数据,并返回给客户端
写入操作
客户端知道哪些块服务器持有必须被更新的副本。 我们应如何管理一个块的所有副本的更新?
论文阅读:GFS
思考题
GFS依赖于什么假设?
它如何利用这些假设?为什么需要这些假设?
你认为谷歌在GFS中存储了什么样的数据(即文件中包含什么)?谁使用这些数据?
控制器(master)的角色是什么?
读取操作是如何工作的?
写入操作是如何工作的?
为什么GFS使用大块大小?
如果GFS中的副本故障会发生什么?
如果控制器故障会发生什么?
在讲座中,你会听到一个叫RAID的系统。我们能否在广域网中使用类似RAID的奇偶校验方案?挑战是什么?
摘要
我们设计并实现了Google文件系统(Google File System,GFS),这是一种面向大型分布式数据密集型应用的可扩展分布式文件系统。它能够在廉价的商用硬件上提供故障容错功能,同时为大量客户端提供高聚合性能。
尽管与之前的分布式文件系统共享许多相同的目标,但我们的设计是基于对当前及未来应用负载和技术环境的观察。这些观察显示出与早期文件系统假设的显著差异,因此我们重新审视了传统的设计选择,并探索了截然不同的设计点。
该文件系统已成功满足了我们的存储需求,广泛部署在Google内部,作为数据生成和处理的平台,支持我们服务的运行以及需要大规模数据集的研究和开发工作。迄今为止,最大规模的集群提供了跨越数千块磁盘和超过一千台机器的数百TB的存储容量,支持数百个客户端的并发访问。
在本文中,我们介绍了设计的文件系统接口扩展,以支持分布式应用程序,讨论了设计的多个方面,并报告了微基准测试和实际使用中的测量结果。
1. 引言
我们审视了传统的文件系统设计的选择,并探索了截然不同的设计点。
首先,组件故障是常态而非例外。文件系统由廉价商用存储机器组成,并由数量相当的客户端机器访问。我们遇到的问题包括应用程序漏洞、操作系统漏洞、人工错误以及磁盘、内存、连接器、网络和电源供应故障。因此,系统必须具有持续监控、错误检测、容错和自动恢复的能力。
其次,与传统标准相比,文件规模巨大。多GB的文件很常见,每个文件通常包含许多应用对象(如网页文档),管理数十亿个KB级大小的文件即使在文件系统支持的情况下也非常困难。因此,I/O操作和块大小等设计假设和参数必须重新评估。
第三, 大多数文件是通过追加新数据而改变的,而非覆盖现有数据。文件内部的随机写入几乎不存在。文件一旦写入,通常只会被读取,而且通常是顺序读取。鉴于这种大文件的访问模式,追加操作成为性能优化和原子性保证的重点,而客户端缓存数据块的价值则下降了。
第四,共同设计应用程序和文件系统API有利于提升系统的灵活性。例如,我们放宽了GFS的一致性模型,大幅简化了文件系统设计,而不会对应用程序带来过重负担。我们还引入了一个原子追加操作,使多个客户端可以同时向文件追加数据而无需额外的同步。
目前,多个GFS集群已部署于不同用途。最大规模的集群拥有超过1000个存储节点,超过300TB的磁盘存储,且被数百台不同机器上的客户端持续高频访问。
2. 设计概览
2.1 假设
我们之前提到了一些关键观察,现在将详细说明我们的假设。
- 系统由许多廉价的商用组件构成,而这些组件经常会发生故障。系统必须持续监控自身,能够例行地检测、容错并迅速从组件故障中恢复。
- 系统存储少量的大文件。我们预计文件数量为几百万个,每个文件通常大小为100MB或更大。多GB的文件是常见情况,应该高效管理。小文件需要支持,但不必优化。
- 工作负载主要包含两种读取操作:大型流式读取和小型随机读取。在大型流式读取中,单个操作通常读取数百KB,更多时候是1MB或以上。同一客户端的连续操作通常会读取文件的连续区域。小型随机读取通常读取几KB,且在任意偏移处进行。对性能敏感的应用通常会批处理并排序小型读取操作,以平稳地读取文件,而不是频繁来回跳转。
- 工作负载还包括许多大型的顺序写操作,将数据追加到文件中。操作规模通常与读取操作类似。一旦写入,文件很少再次被修改。虽然系统支持在文件任意位置的小型写入操作,但不必高效支持。
- 系统必须有效地为多个客户端实现明确的并发追加语义。我们的文件经常被用作生产者-消费者队列或用于多向合并。每台机器上运行的数百个生产者将并发地向文件追加数据。需要最小同步开销的原子性是关键。文件可能在稍后被读取,或在消费者同时读取文件时被追加。
- 高持续带宽比低延迟更重要。我们的目标应用大多注重以高速度批量处理数据,而很少对单个读写操作的响应时间有严格要求。
2.2 接口
GFS提供了熟悉的文件系统接口,但它并未实现诸如POSIX的标准API。文件在目录中分层组织,通过路径名识别。我们支持创建、删除、打开、关闭、读取和写入文件的常规操作。
此外,GFS具备快照和记录追加操作。快照可以低成本地创建文件或目录树的副本。记录追加允许多个客户端同时向同一文件追加数据,同时确保每个客户端追加的原子性。这在实现多路合并结果和生产者-消费者队列时非常有用,众多客户端可以在不需要额外锁的情况下同时向其追加数据。我们发现这些类型的文件在构建大型分布式应用时极为宝贵。快照和记录追加将在第3.4节和第3.3节进一步讨论。
2.3 架构
一个GFS集群由一个控制器和多个块服务器(chunkservers)组成,并由多个客户端访问,如图1所示。每台服务器通常是运行用户级别服务进程的商用Linux机器。只要机器资源允许并且运行可能不稳定的应用代码带来的可靠性降低是可接受的,则可以在同一台机器上同时运行块服务器和客户端。

文件被划分为固定大小的块。每个块由一个在创建时由主服务器分配的不可变且全局唯一的64位块句柄标识。块服务器将块作为Linux文件存储在本地磁盘上,并根据块句柄和字节范围读写块数据。为保证可靠性,每个块在多个块服务器上复制。默认情况下,我们存储三个副本,但用户可以为文件命名空间的不同区域指定不同的复制级别。
主服务器维护所有文件系统的元数据。这包括命名空间、访问控制信息、从文件到块的映射以及块的当前位置。它还控制系统范围的活动,如块租约管理、孤立块的垃圾回收和块服务器之间的块迁移。主服务器通过心跳消息定期与每个块服务器通信,以发出指令并收集其状态。
GFS客户端代码链接到每个应用程序中,负责实现文件系统API,并代表应用程序与主服务器和块服务器通信以读写数据。客户端与主服务器进行元数据操作的交互,但所有数据传输直接与块服务器通信。我们不提供POSIX API,因此不需要连接到Linux的vnode层。
客户端和块服务器均不缓存文件数据。客户端缓存几乎没有优势,因为大多数应用程序通过大型文件进行流式处理,或者其工作集过大无法缓存。不使用缓存简化了客户端和整体系统,消除了缓存一致性问题。(不过,客户端会缓存元数据。)块服务器不需要缓存文件数据,因为块存储为本地文件,因此Linux的缓冲区缓存已经将频繁访问的数据保留在内存中。
2.4 单一控制器
使用单一主服务器大大简化了我们的设计,使主服务器能够利用全局信息做出复杂的块放置和复制决策。然而,我们必须尽量减少主服务器在读写过程中的参与,以免成为瓶颈。客户端在读取或写入文件数据时从不经过主服务器。相反,客户端向主服务器询问它应联系哪些块服务器。客户端会缓存这些信息一段时间,并在后续操作中直接与块服务器交互。
让我们参考图1来解释一个简单读取操作的交互过程。首先,客户端使用固定的块大小,将应用程序指定的文件名和字节偏移转换为文件中的块索引。然后,它向主服务器发送包含文件名和块索引的请求。主服务器返回对应的块句柄和副本的位置。客户端使用文件名和块索引作为键来缓存这些信息。
接着,客户端向一个副本发送请求,通常是距离最近的那个副本。请求包含块句柄和该块内的字节范围。对同一块的后续读取不再需要与主服务器交互,直到缓存信息过期或文件被重新打开。实际上,客户端通常会在同一请求中请求多个块,主服务器还可以包含紧随所请求块之后的块信息。这些额外信息几乎不增加成本,却避免了未来几次客户端-主服务器的交互。
2.5 块大小
块大小是设计中的关键参数之一。我们选择了64 MB的块大小,这远大于典型文件系统的块大小。每个块副本作为普通Linux文件存储在块服务器上,并在需要时进行扩展。延迟空间分配避免了由于内部碎片化而浪费空间,解决了对大块大小的主要质疑。
大块大小带来了几个重要优势。首先,它减少了客户端与主服务器交互的需求,因为同一块上的读写操作只需要一次初始请求来获取块位置信息。对于我们主要顺序读取和写入大文件的工作负载而言,这种减少尤为显著。即使是小的随机读取,客户端也可以轻松缓存多TB工作集的所有块位置信息。其次,由于客户端在一个大块上执行的操作较多,它可以通过在较长时间内保持与块服务器的持久TCP连接来减少网络开销。第三,它减少了主服务器上存储的元数据大小。这允许我们将元数据保存在内存中,带来其他优势,这将在第2.6.1节讨论。
另一方面,即使有延迟空间分配,大块大小也有其缺点。小文件仅包含少量块,可能只有一个块。存储这些块的块服务器可能会在许多客户端同时访问同一文件时成为热点。在实践中,热点并不构成主要问题,因为我们的应用程序大多顺序读取包含多个块的大文件。
然而,当GFS最初被批处理队列系统使用时,确实出现了热点:一个可执行文件以单块文件写入GFS,然后在数百台机器上同时启动。存储该可执行文件的少数块服务器因数百个同时请求而超负荷。我们通过为此类可执行文件设置更高的复制因子,并让批处理队列系统错开应用程序启动时间来解决此问题。一个潜在的长期解决方案是在此类情况下允许客户端从其他客户端读取数据。
2.6 元数据
主服务器(master)存储三种主要类型的元数据:文件和块的命名空间、文件到块的映射,以及每个块的副本位置。所有元数据都保存在主服务器的内存中。前两种类型(命名空间和文件到块的映射)通过记录操作日志保存在主服务器的本地磁盘上,并在远程机器上进行备份。这种日志方式可以简单、可靠地更新主服务器的状态,同时在主服务器崩溃时不会导致不一致。主服务器不持久化存储块位置信息,而是在主服务器启动或块服务器加入集群时询问每个块服务器以获取该信息。
2.6.1 内存数据结构
由于元数据保存在内存中,主服务器的操作速度很快。此外,主服务器可以定期在后台扫描整个状态,这种定期扫描用于实现块垃圾回收、在块服务器故障情况下重新复制块、以及平衡块服务器间的负载和磁盘空间(见4.3和4.4节)。这种只在内存中存储的方式的潜在问题是,块的数量和整个系统的容量受到主服务器内存大小的限制,但在实践中这并不是一个严重的限制。主服务器为每个64MB块维护的元数据少于64字节。大多数块是满的,因为大多数文件包含许多块,只有最后一个块可能是部分填充的。同样,文件命名空间数据每个文件通常占用不到64字节,因为它使用前缀压缩来紧凑地存储文件名。
如果需要支持更大的文件系统,增加主服务器内存的成本是很小的代价,而这种方式带来了元数据存储在内存中的简单性、可靠性、性能和灵活性。
2.6.2 块位置
主服务器不持久化记录哪个块服务器拥有某个块的副本,而是在启动时简单地向块服务器请求该信息。主服务器可以在之后保持更新,因为它控制所有块的放置,并通过定期的心跳消息监控块服务器的状态。
最初我们尝试在主服务器中持久化保存块位置信息,但最终决定在启动时从块服务器获取数据,并定期更新,这样可以避免块服务器在加入和离开集群、更改名称、故障、重启等情况下造成的不一致问题。在包含数百台服务器的集群中,这种事件频繁发生。
另一个理解这一设计决策的方式是认识到块服务器对其拥有的块具有最终决定权。例如,由于块服务器出现错误可能导致块意外消失(如磁盘故障导致禁用),或者操作人员可能重命名块服务器,因此在主服务器上维护该信息的一致视图并没有意义。
2.6.3 操作日志
操作日志包含关键元数据更改的历史记录,是GFS的核心。它不仅是元数据的唯一持久记录,还作为逻辑时间线定义了并发操作的顺序。文件和块以及它们的版本(见4.5节)都由创建时的逻辑时间唯一标识。
由于操作日志至关重要,我们必须可靠地存储它,且在元数据更改持久化之前不向客户端展示更改。否则,即使块本身幸存下来,我们也可能丢失整个文件系统或近期的客户端操作。因此,我们在多台远程机器上复制操作日志,并在对应的日志记录本地和远程写入磁盘后才响应客户端操作。主服务器在将多个日志记录组合后进行写入,减少写入和复制对系统总体吞吐量的影响。
主服务器通过重放操作日志恢复文件系统状态。为减少启动时间,需保持日志简短。主服务器在日志超过一定大小时检查其状态,通过加载本地磁盘上的最新检查点并仅重放少量日志记录来恢复。检查点采用紧凑的B树形式,可以直接映射到内存中并用于命名空间查找,无需额外解析。这进一步加速了恢复并提高了可用性。
由于创建检查点可能需要一些时间,主服务器的内部状态结构允许创建新检查点而不延迟新进来的变动。主服务器切换到新日志文件,并在单独的线程中创建新检查点。新检查点包括切换前的所有变动。对于拥有几百万个文件的集群,检查点可以在一分钟左右完成。检查点完成后会写入本地和远程磁盘。
恢复只需要最新的完整检查点和随后的日志文件。较旧的检查点和日志文件可以自由删除,不过我们会保留几个以防灾难性事件。检查点期间的失败不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。
2.7 一致性模型
GFS(Google File System)采用了一种宽松的一致性模型,既支持我们高度分布式的应用程序,又相对简单且高效实现。我们将讨论GFS的保证及其对应用程序的意义,并介绍GFS如何保持这些保证,但详细实现将在文中其他部分描述。
2.7.1 GFS的保证
文件命名空间的变动(如文件创建)是原子操作。此类操作由主服务器独占处理,命名空间的锁定保证了操作的原子性和正确性(见4.1节),主服务器的操作日志定义了这些操作的全局顺序(见2.6.3节)。
在数据变动后,文件区域的状态取决于变动类型、变动是否成功以及是否有并发变动。表1总结了结果。一个文件区域是“一致的”是指所有客户端从任意副本读取时都能看到相同的数据。“定义状态”指在数据变动后,区域是一致的,且所有客户端都能看到变动写入的完整数据。如果一次变动在没有其他写入干扰的情况下成功,受影响的区域即为定义状态(且隐含一致):所有客户端都能看到变动写入的内容。并发成功变动使区域处于未定义但一致的状态:所有客户端看到的内容一致,但可能不代表任何一次变动的具体写入内容,通常是多个变动内容的混合片段。失败的变动使区域处于不一致(因此也是未定义)的状态:不同客户端可能在不同时刻看到不同的数据。我们接下来描述如何让应用区分定义区域和未定义区域。应用程序无需进一步区分不同种类的未定义区域。
数据变动可以是写入或记录追加。写入是指在应用程序指定的文件偏移处写入数据。记录追加指的是在有并发变动的情况下,以GFS选择的偏移处原子性地追加记录,保证记录至少写入一次(见3.3节)。与之相比,普通的“追加”只是客户端认为的文件末尾偏移处的写入。GFS返回的偏移标志着记录的开始,是已定义区域。此外,GFS可能在记录之间插入填充或记录重复,通常与用户数据相比,这些填充或重复所占区域不多,且被视为不一致区域。
在一系列成功变动之后,变动的文件区域可保证为定义状态,并包含最后一次变动的数据。GFS通过以下方式实现此点:(a) 在所有副本上以相同顺序对一个数据块进行变动(见3.1节);(b) 使用块版本号检测因服务器故障错过变动而变为过期的副本(见4.5节)。过期的副本不会参与新的变动,且在客户端请求块位置时不会返回给客户端。一旦有机会,这些副本会被垃圾回收。
由于客户端会缓存块位置,它们可能在缓存信息刷新前从过期副本中读取数据。此窗口期受缓存条目的超时限制和文件重新打开的影响,重新打开文件时缓存的所有块信息会被清除。此外,由于大多数文件仅追加写入,过期副本通常返回块的早期结束信息,而非过期数据。当读取请求重新联系主服务器时,会立即获得当前的块位置。
在成功变动之后,如果组件发生故障,仍可能损坏或丢失数据。GFS通过主服务器与所有块服务器的定期握手确认失败的块服务器,并通过校验和检测数据损坏(见5.2节)。一旦问题出现,GFS会尽快从有效的副本恢复数据(见4.3节)。数据块只有在所有副本在GFS反应之前(通常几分钟内)丢失时才会不可恢复。这种情况下,数据块不会被破坏,只是变得不可用,应用会收到清晰的错误,而不是损坏的数据。
2.7.2 对应用程序的影响
GFS的应用程序可以通过一些简单的技术来适应这种宽松的一致性模型,这些技术本来就需要用于其他目的,比如依赖追加操作而非覆盖写入、设置检查点以及写入自验证和自标识的记录。
实际上,几乎所有应用程序都是通过追加数据而非覆盖数据来变动文件。在一种典型用法中,写入者从文件的起始写到结束。写入所有数据后,它会将文件原子性地重命名为永久名称,或者周期性地检查已成功写入的部分。检查点可能还包含应用级的校验和。读取者仅验证并处理到最后一个检查点之前的文件区域,因为这一部分在定义状态中。无论一致性和并发性问题如何,这种方法对我们很有帮助。追加操作比随机写入更高效且对应用故障更具容错性。检查点允许写入者按增量重启,并防止读取者处理到应用认为尚未完成的已成功写入文件数据。
在另一种典型用法中,许多写入者并发地向一个文件追加数据,用于合并结果或作为生产者-消费者队列。记录追加的“至少一次”追加语义保留了每个写入者的输出。读取者可以通过以下方式处理偶尔的填充和重复。每个写入者准备的记录包含额外信息(如校验和),以便验证其有效性。读取者可以通过校验和识别并丢弃额外的填充和记录片段。如果无法容忍偶尔的重复(如触发非幂等操作),可以通过记录中所含的唯一标识符过滤它们。这些标识符通常也是命名应用程序实体(如网页文档)所需的。记录I/O的这些功能(除重复移除外)在我们应用程序共享的库代码中实现,适用于Google的其他文件接口实现。因此,相同的记录序列加上少量重复内容始终会传递到记录读取者。
3. 系统交互
我们设计系统的目标是最小化主节点在所有操作中的参与。基于这一背景,下面我们描述客户端、主节点和块服务器如何交互,以实现数据变更、原子记录追加和快照功能

4. 主服务器操作
主服务器执行所有的命名空间操作。此外,它管理系统中的块副本:做出放置决策、创建新块(以及相应的副本),并协调各项系统范围的活动,以保持块的充分复制、平衡所有块服务器的负载并回收未使用的存储。接下来,我们将讨论这些主题。
4.1 命名空间管理和锁定
许多主服务器操作可能需要较长时间。例如,快照操作需要撤销所有受快照覆盖的块的块服务器租约。我们不希望在这些操作进行时延迟其他主服务器操作。因此,我们允许多个操作同时进行,并使用命名空间区域上的锁来确保正确的序列化。
与许多传统文件系统不同,GFS 没有按目录存储的结构来列出该目录中的所有文件。它也不支持文件或目录的别名(例如,Unix 中的硬链接或符号链接)。GFS 将其命名空间逻辑上表示为一个查找表,将完整路径映射到元数据。通过前缀压缩,可以在内存中高效地表示该表。命名空间树中的每个节点(绝对文件名或绝对目录名)都有一个关联的读写锁。
每个主服务器操作在运行前会获取一组锁。通常情况下,如果操作涉及路径 /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 类数据结构。目录名上的读锁足以防止父目录被删除。
这个锁机制的一个优点是它允许在同一目录中进行并发的更改。例如,可以在同一目录中并发执行多个文件创建操作:每个操作在目录名上获取读锁,在文件名上获取写锁。目录名上的读锁足以防止目录被删除、重命名或快照。文件名上的写锁序列化了创建具有相同名称的文件的尝试。
由于命名空间可能包含许多节点,因此读写锁对象在需要时会被懒加载,并在不再使用时删除。为了防止死锁,锁按照一致的总顺序获取:首先按照命名空间树中的层级排序,在同一级别内按字典序排序。
4.2 副本放置
一个 GFS 集群通常在多个级别上高度分布。它通常拥有数百个分布在许多机架上的块服务器。这些块服务器可能会被同一机架或不同机架上的数百个客户端访问。两个不同机架上的机器之间的通信可能需要经过一个或多个网络交换机。此外,某个机架的带宽进出可能小于该机架内所有机器的总带宽。
多级分布为实现数据的可扩展性、可靠性和可用性带来了独特的挑战。块副本放置策略有两个目的:最大化数据的可靠性和可用性,以及最大化网络带宽的利用率。为此,仅在机器之间分布副本是不够的,这只能够防范磁盘或机器故障并充分利用每台机器的网络带宽。我们还必须在机架之间分布块副本。这确保了即使整个机架受损或离线(例如,由于共享资源如网络交换机或电路的故障),某些块副本仍然能够存活并保持可用性。这也意味着可以利用多个机架的总带宽来处理块的流量,尤其是读取流量。另一方面,写流量必须通过多个机架传输,这是我们愿意接受的权衡。
4.3 创建、重复制、重新平衡
块副本的创建有三个原因:块创建、重复制和重新平衡。
当主服务器创建一个块时,它会选择放置最初为空的副本的位置。它会考虑几个因素:(1)我们希望将新的副本放置在磁盘空间利用率低于平均水平的块服务器上。随着时间的推移,这将使块服务器之间的磁盘利用率趋于平衡。(2)我们希望限制每个块服务器上的“最近”创建数量。尽管创建操作本身是廉价的,但它可靠地预测了即将到来的重写流量,因为块是在写入时按需创建的,在我们“追加一次,读取多次”的工作负载中,它们通常在完全写入后几乎变成只读状态。(3)如前所述,我们希望将块的副本分布在不同的机架上。
当块副本数量低于用户指定的目标时,主服务器会立即重新复制该块。这种情况可能由多种原因引起:块服务器变得不可用,报告其副本可能已损坏,某个磁盘因错误被禁用,或者复制目标增加。每个需要重新复制的块根据几个因素进行优先级排序。一个因素是副本距离复制目标的远近。例如,我们会给失去两个副本的块更高的优先级,而不是仅失去一个副本的块。此外,我们倾向于首先重新复制活跃文件的块,而不是最近已删除文件的块(见 4.4 节)。最后,为了减少故障对正在运行的应用程序的影响,我们提高了任何阻止客户端进度的块的优先级。
主服务器选择优先级最高的块,并通过指示某个块服务器直接从现有有效副本复制数据来“克隆”它。新副本的放置目标类似于创建时的目标:平衡磁盘空间利用率,限制单一块服务器上的活跃克隆操作,并将副本分布在不同的机架上。为了避免克隆流量压倒客户端流量,主服务器限制了整个集群和每个块服务器上活跃克隆操作的数量。此外,每个块服务器通过限制其读取请求的带宽,来限制每个克隆操作的带宽消耗。
最后,主服务器会定期进行副本重新平衡:它检查当前的副本分布,并移动副本以便更好地平衡磁盘空间和负载。同时,在此过程中,主服务器会逐渐填充新的块服务器,而不是立即将新的块和随之而来的重写流量压垮它。新副本的放置标准类似于前面讨论的标准。此外,主服务器还必须选择要移除的现有副本。一般来说,它倾向于移除那些位于磁盘空间低于平均水平的块服务器上的副本,以便平衡磁盘空间的使用。
4.4 垃圾回收
文件删除后,GFS 并不会立即回收可用的物理存储空间。它仅在常规垃圾回收时,以懒惰的方式回收文件和块层次的存储。我们发现这种方式使得系统更加简单和可靠。
4.4.1 机制
当应用程序删除文件时,主服务器会像其他更改一样立即记录删除操作。然而,主服务器不会立即回收资源,而是将文件重命名为一个隐藏名称,其中包含删除时间戳。在主服务器定期扫描文件系统命名空间时,它会删除所有存在超过三天(此时间间隔是可配置的)的隐藏文件。在此之前,文件仍然可以通过新的特殊名称读取,并可以通过将文件重命名回正常名称来恢复。当隐藏文件从命名空间中删除时,其内存中的元数据会被清除。这有效地断开了文件与所有块的链接。
在对块命名空间的类似定期扫描中,主服务器会识别孤立块(即无法从任何文件访问的块)并删除这些块的元数据。在与主服务器定期交换的心跳消息中,每个块服务器报告它拥有的部分块,主服务器则回复所有不再存在于主服务器元数据中的块的标识。块服务器可以自由删除这些块的副本。
4.4.2 讨论
尽管分布式垃圾回收在编程语言的上下文中是一个难题,通常需要复杂的解决方案,但在我们的案例中却相对简单。我们可以轻松地识别所有对块的引用:它们存在于由主服务器独占维护的文件到块的映射中。我们还可以轻松识别所有的块副本:它们是每个块服务器上指定目录下的 Linux 文件。任何不被主服务器知晓的副本都可以视为“垃圾”。
垃圾回收的存储回收方法相比于急切删除具有几个优势。首先,在一个分布式系统中,组件故障是常见的,而这种方法简单且可靠。块创建可能在一些块服务器上成功,但在其他块服务器上失败,导致主服务器无法知道副本的存在。副本删除消息可能会丢失,主服务器需要跨越故障重新发送这些消息,包括它自己的故障和块服务器的故障。垃圾回收为清理任何不被认为有用的副本提供了一个统一且可靠的方式。其次,它将存储回收合并到主服务器的常规后台活动中,例如对命名空间的定期扫描和与块服务器的握手。因此,它是批量执行的,成本得到了摊销。此外,它仅在主服务器相对空闲时执行,主服务器可以更及时地响应那些需要立即处理的客户端请求。第三,存储回收的延迟为防止意外不可逆的删除提供了安全网。
在我们的经验中,主要的缺点是,延迟有时会妨碍用户在存储紧张时进行精细调整。那些重复创建和删除临时文件的应用程序可能无法立即重新使用这些存储。我们通过加速存储回收来解决这些问题,尤其是当已删除的文件再次显式删除时。我们还允许用户对命名空间的不同部分应用不同的复制和回收策略。例如,用户可以指定某个目录树中的所有文件块不需要复制,任何删除的文件都会立即且不可逆地从文件系统状态中移除。
4.5 陈旧副本检测
当块服务器故障且在恢复期间错过块的变更时,块副本可能变得陈旧。对于每个块,主服务器维护一个块版本号,用以区分最新的副本和陈旧的副本。
每当主服务器授予一个块新的租约时,它会增加块版本号并通知所有最新的副本。主服务器和这些副本都在其持久化状态中记录新的版本号。这一过程发生在任何客户端收到通知之前,因此客户端在开始写入块之前会确保数据是最新的。如果另一个副本当前不可用,其块版本号将不会被更新。主服务器在块服务器重新启动并报告其块及其关联版本号时会检测到陈旧副本。如果主服务器看到版本号大于其记录中的版本号,主服务器会假设授予租约时发生了故障,因此它会认为较高版本的副本是最新的。
主服务器会在其常规垃圾回收中删除陈旧副本。在此之前,主服务器实际上会将陈旧副本视为不存在,在回复客户端请求块信息时不会考虑这些副本。作为另一种保护措施,主服务器在告知客户端哪个块服务器持有块租约,或者在指示块服务器从另一个块服务器读取块进行克隆操作时,会包括块版本号。客户端或块服务器在执行操作时会验证版本号,以确保始终访问最新的数据。
5. 容错和诊断(Diagnosis)
设计这个系统的最大的挑战之一是如何处理频繁的组件级失效。面对这样的情况,我们不能完全相信机器,也不能完全相信磁盘。基于以上,我们探讨如何满足这些挑战,以及如何构建工具去诊断不可避免发生的故障
5.1 高可用性
在一个包含数百台服务器的GFS集群中,任何时刻都可能会有部分服务器不可用。为了保证系统的高可用性,我们采用了两种简单而有效的策略:快速恢复和数据复制。
5.1.1 快速恢复
无论是主服务器还是块服务器,都被设计成在结束运行后可以在数秒内恢复状态并重新启动。实际上,我们并不区分正常和异常终止;服务器通常通过杀死进程来关闭。客户端和其他服务器在遇到当前请求超时时会出现小小的“顿卡”,它们会重新连接到已重启的服务器并重试请求。第6.2.2节报告了观察到的启动时间。
5.1.2 块复制
如前所述,每个数据块都在不同的机架上的多个块服务器上复制。用户可以为文件命名空间的不同部分指定不同的复制级别,默认情况下为三份。当块服务器下线或通过校验和验证检测到已损坏的副本时,主服务器会根据需要克隆现有的副本以确保每个块都保持完全复制状态。
5.1.3 主服务器的复制
主服务器的状态被复制以确保可靠性。其操作日志和检查点在多台机器上被复制。只有当日志记录被写入本地磁盘和所有主服务器副本后,状态的变更才被视为已提交。为了简化操作,系统中的所有变更(如垃圾回收等内部操作)均由一个主服务器进程负责。当它发生故障时,可以几乎瞬间重启。如果它的机器或磁盘发生故障,GFS系统外的监控基础设施会在其他地方启动新的主服务器进程,利用已复制的操作日志。客户端只使用主服务器的规范名称(例如gfs-test),这是一个DNS别名,如果主服务器迁移到其他机器上,可以更改该别名。
此外,“影子”主服务器提供只读访问,即使主服务器宕机时也能读取文件系统。影子服务器并不是主服务器的完全镜像,它们可能比主服务器略有延迟,通常只有几毫秒。它们可以为未被积极修改的文件或不介意获取略旧结果的应用程序提高读取可用性。实际上,由于文件内容是从块服务器读取的,应用程序不会观察到过时的文件内容。在短时间窗口内可能会变旧的是文件的元数据,如目录内容或访问控制信息。
为了保持信息更新,影子主服务器会读取操作日志的副本,并按照主服务器相同的变更序列更新其数据结构。像主服务器一样,它会在启动时(以及之后不定期)轮询块服务器,以定位块副本,并通过频繁的握手消息来监控块服务器的状态。影子主服务器只依赖主服务器提供的副本位置信息更新,这些更新来自主服务器创建或删除副本的决定。
5.2 数据完整性
每个块服务器都使用校验和来检测存储数据的损坏。由于GFS集群通常包含数千个磁盘,分布在数百台机器上,磁盘故障经常会导致读写路径上的数据损坏或丢失(第7节中将提到其中一个原因)。尽管我们可以通过其他块副本恢复数据,但通过在各块服务器间比较副本来检测损坏是不切实际的。此外,副本不一致在某些情况下是正常的:GFS的变更操作语义(特别是之前提到的原子记录追加)并不保证副本完全一致。因此,每个块服务器必须通过维护校验和独立验证其副本的完整性。
每个数据块被划分为64KB的子块,每个子块对应一个32位的校验和。与其他元数据一样,校验和存储在内存中,并通过日志记录持久保存,且与用户数据分开存储。
在读取数据时,块服务器在返回数据给请求者(无论是客户端还是其他块服务器)之前会先验证与读取范围重叠的数据块的校验和。因此,块服务器不会将损坏的数据传播到其他机器上。如果块的数据与记录的校验和不匹配,块服务器会向请求者返回错误并将不匹配情况报告给主服务器。请求者会从其他副本中读取数据,同时主服务器会从另一副本中克隆块。新的有效副本到位后,主服务器会指示报告不匹配的块服务器删除其副本。
校验和对读取性能的影响较小,原因有几个。首先,我们的读取操作通常跨越多个数据块,只需读取和验证少量额外数据即可。GFS客户端代码通过尝试将读取操作对齐到校验和块边界,进一步减少了这类开销。此外,块服务器上的校验和查找与比较无需任何I/O操作,且校验和计算往往可以与I/O操作重叠执行。
校验和计算对追加写(即向块末尾添加数据)进行了大量优化,因为这类操作在我们的工作负载中占主导地位。我们只需对最后一个部分校验和块进行增量更新,并对追加填充的新校验和块计算校验和。如果最后的部分校验和块已损坏且未被检测到,新计算的校验和值将无法与存储数据匹配,损坏将在下次读取该块时被检测到。
相比之下,如果写操作覆盖块的现有范围,则我们必须先读取并验证被覆盖范围的首尾块,然后执行写操作,并最终计算并记录新的校验和。如果在部分覆盖前未验证首尾块,新校验和可能会掩盖未覆盖区域中的损坏。
在空闲期,块服务器可以扫描并验证非活跃块的内容。这使我们能够检测到很少被读取的数据块中的损坏。一旦检测到损坏,主服务器可以创建一个新的未损坏的副本,并删除损坏的副本。这避免了非活跃但已损坏的块副本欺骗主服务器,使其误以为该块有足够的有效副本。
5.3 诊断工具
详细的诊断日志在问题隔离、调试和性能分析中起到了巨大的帮助作用,且成本极低。没有日志记录,很难理解机器之间瞬时、不可重现的交互。GFS服务器会生成诊断日志,记录许多重要事件(例如块服务器的上下线)以及所有RPC请求和回复。这些诊断日志可以自由删除,不会影响系统的正确性。不过,我们会尽量在空间允许的情况下保留这些日志。
RPC日志包含传输中发送的确切请求和响应(不包括被读或写的文件数据)。通过匹配请求与回复并整理不同机器上的RPC记录,我们可以重建完整的交互历史,以诊断问题。这些日志还可作为负载测试和性能分析的痕迹。
日志记录对性能的影响最小(且其带来的好处远大于影响),因为这些日志是按顺序异步写入的。最新事件也会保存在内存中,便于持续在线监控。
6. 测量
在本节中,我们将展示一些微基准测试,以说明 GFS 架构和实现中的瓶颈,并提供 Google 内部使用的真实集群的相关数据。
6.1 微基准测试
我们在一个包含 1 个主节点、2 个主节点副本、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) 显示了总写入速率及其理论上限。由于每个字节需要写入 16 个块服务器中的 3 个,因此总写入速率的上限为 67 MB/s,每个块服务器的输入连接速率为 12.5 MB/s。
一个客户端的写入速率为 6.3 MB/s,大约是上限的一半。主要原因是我们的网络栈与我们用于向块副本推送数据的流水线方案之间的交互不佳。从一个副本传播数据到另一个副本的延迟降低了总体写入速率。当有 16 个客户端时,总写入速率达到 35 MB/s(即每客户端 2.2 MB/s),约为理论上限的一半。与读取一样,随着客户端数量的增加,同时写入到同一个块服务器的概率也增加。此外,由于每次写入涉及三个不同的副本,与读取相比,发生冲突的可能性更高。
尽管写入速度较慢,但在实际中这并不是主要问题。虽然这会增加个别客户端的延迟,但并未显著影响系统为大量客户端提供的总写入带宽。
6.1.3 记录追加
图 3(c) 显示了记录追加的性能。N 个客户端同时追加到一个文件。性能受限于存储该文件最后一个块的块服务器的网络带宽,与客户端数量无关。当有一个客户端时,速率为 6.0 MB/s,而当有 16 个客户端时,速率降至 4.8 MB/s,这主要是由于网络拥塞和不同客户端的传输速率波动。
我们的应用通常同时生成多个这样的文件。换句话说,N 个客户端同时追加到 M 个共享文件,这里的 N 和 M 都在几十或几百之间。因此,在实验中观察到的块服务器网络拥塞在实践中不是一个显著问题,因为一个客户端可以在一个文件的块服务器忙碌时在另一个文件上继续写入。
6.2 真实集群
我们现在来分析 Google 内部使用的两个具有代表性的 GFS 集群的情况。集群 A 主要用于研究和开发,由数百名工程师定期使用。典型的任务由用户发起,持续数小时不等,读取几 MB 到几 TB 的数据,分析或转换数据后将结果写回集群。集群 B 主要用于生产数据处理,任务持续更长时间,持续生成和处理多 TB 数据集,偶尔会有人为干预。
| 项目 | 集群 A | 集群 B |
|---|---|---|
| 块服务器数量 | 342 | 227 |
| 可用磁盘空间 | 72 TB | 180 TB |
| 已用磁盘空间 | 55 TB | 155 TB |
| 文件数量 | 735 k | 737 k |
| 无效文件数量 | 22 k | 232 k |
| 数据块数量 | 992 k | 1550 k |
| 块服务器元数据大小 | 13 GB | 21 GB |
| 主服务器元数据大小 | 48 MB | 60 MB |
6.2.1 存储
如表格中前五项所示,两个集群都包含数百个块服务器,支持数 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 显示了在不同时间段的读写速率。两个集群在测量时已运行一周左右,期间经历了版本升级。自重启以来,平均写入速率小于 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 主服务器负载
表 3 显示了主服务器每秒的操作率大约在 200 到 500 次之间。主服务器可以轻松处理这个负载,因此在这些工作负载下不会成为瓶颈。
在早期版本的 GFS 中,主服务器偶尔会成为某些工作负载的瓶颈。主服务器大部分时间会按顺序扫描包含数十万个文件的大目录,寻找特定文件。我们已更改了主服务器的数据结构,以便通过命名空间进行高效的二分查找。现在,它可以轻松支持每秒数千次的文件访问。如果需要,我们可以在命名空间数据结构前添加名称查找缓存,以进一步提升速度。
6.2.5 恢复时间
在块服务器故障后,一些数据块会变为复制不足状态,必须进行克隆以恢复其复制级别。恢复这些数据块的时间取决于可用资源。在一项实验中,我们终止了 B 集群中的一个块服务器,该块服务器包含约 15,000 个数据块,总计 600 GB 的数据。为了减少对正在运行的应用程序的影响,并为调度决策提供余地,我们的默认参数限制该集群只能同时进行 91 个克隆(块服务器数量的 40%),每个克隆操作的带宽上限为 6.25 MB/s(50 Mbps)。所有数据块在 23.2 分钟内恢复,实际复制速率为 440 MB/s。
在另一项实验中,我们终止了两个块服务器,每个服务器拥有约 16,000 个数据块和 660 GB 的数据。这一双重故障导致 266 个数据块仅有一个副本。这些数据块被优先克隆,并在 2 分钟内恢复到至少双倍复制,从而使集群处于可承受另一个块服务器故障而不会丢失数据的状态。
6.3 工作负载分析
在本节中,我们详细分析了两个 GFS 集群的工作负载,这些集群与 6.2 节中的集群相似,但不完全相同。集群 X 用于研究和开发,而集群 Y 用于生产数据处理。
6.3.1 方法和注意事项
这些结果仅包含客户端发起的请求,因此反映了我们的应用程序对文件系统整体产生的工作负载。它们不包括服务器间的请求(用于处理客户端请求)或内部后台活动,例如转发写入或重新平衡操作。
I/O 操作的统计数据基于从 GFS 服务器记录的实际 RPC 请求信息中启发式重建。例如,GFS 客户端代码可能会将一次读取操作分解为多个 RPC,以增加并行性,我们据此推断原始读取请求。由于访问模式高度规范化,我们预期误差微乎其微。显式记录应用程序可能会提供稍微更准确的数据,但要重新编译和重新启动成千上万个正在运行的客户端以实现这一点,且收集数据也较为麻烦。
在对我们的工作负载进行分析时,不应过度泛化。由于 Google 完全控制 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 的删除请求数量显著不同,因为集群 Y 存储的生产数据集会定期重新生成并替换为新版本。部分差异在打开请求(Open requests)中被掩盖,因为旧版本的文件可能通过从头开始写入(类似于 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 [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])移除了集中式服务器,依靠分布式算法来实现一致性和管理。我们选择了集中式方法,以简化设计、提高可靠性并增加灵活性。尤其是集中式主服务器使得实现复杂的数据块放置和复制策略变得更为容易,因为主服务器已经掌握了大部分相关信息并控制其变化。通过将主服务器状态保持较小并在其他机器上完全复制,我们解决了容错问题。我们的影子主服务器机制目前提供了可扩展性和高可用性(针对读取操作)。对主服务器状态的更新通过追加到预写日志来持久化。因此,我们可以采用类似 Harp [7] 中的主备副本方案,以提供比当前方案更强的一致性保证和高可用性。
在向大量客户端提供聚合性能方面,我们正处理与 Lustre [8] 类似的问题。然而,我们通过专注于应用程序需求而不是构建一个符合 POSIX 的文件系统,显著简化了问题。此外,GFS 假设大量组件不可靠,因此容错性是设计的核心。
GFS 最类似于 NASD 架构 [4]。尽管 NASD 架构基于网络附加磁盘驱动器,GFS 使用商用机器作为块服务器,这一点与 NASD 原型相同。不同于 NASD,GFS 的块服务器使用延迟分配的固定大小块,而不是可变长度对象。此外,GFS 实现了生产环境所需的重新平衡、复制和恢复等功能。
与 Minnesota 的 GFS 和 NASD 不同的是,我们不寻求改变存储设备的模型。我们专注于利用现有商用组件解决复杂分布式系统的日常数据处理需求。GFS 的原子记录追加实现的生产者-消费者队列解决的问题类似于 River [2] 中的分布式队列。虽然 River 使用分布在多台机器上的基于内存的队列和精确的数据流控制,GFS 使用一个可以由多个生产者并发追加的持久化文件。River 模型支持 m 对 n 的分布式队列,但缺乏持久存储带来的容错性,而 GFS 只能高效地支持 m 对 1 的队列。多个消费者可以读取同一文件,但必须协调以分担传入的负载。
9. 结论
Google 文件系统(GFS)展示了在商用硬件上支持大规模数据处理工作负载的关键特性。虽然有些设计决策是针对我们独特的环境,但其中许多可能适用于具有类似规模和成本考量的数据处理任务。
我们从重新审视传统文件系统的假设出发,以适应当前和未来的应用工作负载和技术环境。我们的观察结果引导我们在设计空间中做出根本不同的选择。我们将组件故障视为常态而非例外,优化系统以处理大文件,这些文件大多会被追加写入(可能是并发的),然后再被读取(通常是顺序的);此外,我们扩展并放宽了标准文件系统接口,以改善整体系统性能。
我们的系统通过持续监控、关键数据的复制以及快速自动的恢复机制来实现容错能力。块的复制使我们能够应对块服务器故障。由于这些故障频繁发生,我们设计了一种新的在线修复机制,以便定期透明地修复损坏并尽快补偿丢失的副本。此外,我们还通过校验和检测磁盘或 IDE 子系统层面的数据损坏,这在系统中大量使用磁盘的情况下变得尤为重要。
我们的设计为众多并发读写用户执行各种任务提供了高聚合吞吐量。我们通过将文件系统的控制(由主服务器处理)与数据传输(直接在块服务器和客户端之间进行)分离来实现这一点。大块大小和块租约将数据变更的权限委托给主要副本,从而减少了主服务器在常规操作中的参与。这使得主服务器能够简单集中,不会成为瓶颈。我们相信,网络栈的改进将提升单个客户端所看到的写入吞吐量限制。
GFS 成功满足了我们的存储需求,被广泛应用于 Google 内部的研发和生产数据处理中。它是一个重要的工具,使我们能够继续创新并解决全网级别的规模化问题。
论文阅读总结
概述
GFS的目标:分布在数百或数千台物理机器上的共享文件系统
增加的复杂性:复制、许多用户同时写入相同的文件、超大文件
依赖一些假设
:主要是只追加的工作负载,写入后的只读
- 意味着不需要复杂的一致性模型和原子性保证
组件
- 控制器(论文中的“master”):具有组织/控制角色
- 块服务器:数据实际存储的地方
- 块:大块的数据
工作原理
GFS论文中的图1和图2是你在6.033任何论文中见到的最好的图之一。为你的设计项目系统图获取灵感!
- 读取:
- 客户端将文件名和文件内的偏移量发送给控制器
- 控制器回复包含有该块的服务器集合
- 客户端请求最近的块服务器
- 写入:
- 客户端向控制器询问存储文件的位置;控制器回应
- 客户端将数据推送到最近的块服务器,块服务器将数据转发给其他服务器
- 主块服务器应用序列号
- 注意,控制器处理控制流量/元数据类型的内容。它不移动真实的文件数据。
讨论
- GFS中大量顺序读取和写入、追加操作效果很好
- 三重复制使其具有很高的容错性(阅读MapReduce时多思考这一点)
- 控制器是单点故障,但它也可以被复制,而且负载较轻
- 还要记住:特定机器故障的概率相对较低。GFS解决的问题是某些机器故障(从而丢失数据)的概率较高
FAQ(GFS 答疑整理)
来自
gfs-faq.txt。
- 单 master 是个好主意吗? 一开始简化了部署,但长期有问题:规模上去后元数据塞不进 master 内存、CPU 成为瓶颈、手动故障切换慢。Google 用 Colossus 把 master 分散到多台并自动恢复。
- 为什么原子 record append 是 at-least-once 而非 exactly-once? secondary 失败时客户端重试 → 在正常节点上产生重复。要做 exactly-once 需在任意失败下检测重复请求,代价(性能 + 复杂度)很大。
- 应用如何知道哪段是 padding/重复记录? 应用在记录里嵌magic number / 校验和识别有效记录、嵌唯一 ID去重;GFS 提供处理这些的库——把复杂度从文件系统转移给应用。
- append 写在不可预知的偏移,客户端怎么找数据? append 面向"顺序读整个文件"的应用,客户端扫描有效记录而非靠预知偏移(如多个爬虫并发收集 URL 的场景)。
- 什么是 checksum? 把一段字节算成一个数代表该数据。GFS 对每 64KB 块存校验和,chunkserver 读盘后重算校验和验完整性;应用也可在文件里存自己的校验和以区分有效记录与 padding。
- reference count 是什么? 用于写时复制快照:建快照只是给 chunk 引用计数 +1 而非复制数据;当对引用数 >1 的 chunk 写入时,master 先复制该 chunk,把昂贵操作推迟到必要时。
- 用标准 POSIX 文件 API 的应用需要改吗? 需要。GFS 面向新写的应用(如 MapReduce),不为兼容已有 POSIX 软件而设计。
- GFS 怎么判断最近的副本? 用 IP 地址:2003 年 Google 的 IP 分配使数值相近的 IP 在网络拓扑上也相近(同交换机/直连交换机)。
- lease 是什么? 给 chunkserver 有时限的 primary 身份:master 保证租约期内不另指 primary,primary 保证租约到期前停止当 primary。免去反复问 master 谁是 primary。
- S1 是 primary,master↔S1 网络断了,master 改指 S2,会不会出现两个 primary? 不会。lease 机制保证 S1 的 60 秒租约到期前 S2 拿不到 primary,S1 到期前停止当 primary,无重叠。
- 64MB chunk 是不是太大了? 大 chunk 减小 master 元数据表、利于大块传输、开销小;客户端仍可发小读小写。但 <64MB 的小文件从分布式并行中获益少。
- Google 还用 GFS 吗? 已被 Colossus 取代(master 性能与容错更好),很多应用转向 BigTable/Spanner;但 GFS 设计活在 HDFS(Hadoop 的存储)里。
- 为性能/简单牺牲正确性可接受吗? 这是分布式系统反复出现的折中。强一致需复杂、通信密集的协议。GFS 为容忍宽松一致性(空洞、重复、不一致读)的 MapReduce 类应用优化,但不适合银行这类要严格正确的系统。
- master 挂了怎么办? replica master 持有完整状态副本;当前设计需人工介入切换,后来的系统可自动故障切换。
- 为什么 3 副本? 2 副本在两者都坏前可能来不及重新复制;3 副本让"同时全坏"在成千上万块盘里也很不可能。决策要权衡盘可靠性、重新复制耗时、各种故障模式与成本。
- 什么是内部碎片?lazy allocation 为何有帮助? 分配单位大于实际需求就浪费(1 字节文件占 64MB 单位会浪费近 64MB)。GFS 靠 lazy allocation 避免:每个 chunk 是一个用更小块大小的 Linux 文件,1 字节的 GFS 文件只占一个 Linux 盘块,而非 64MB。
- GFS 从弱一致性里得到什么好处? 避免大量开销:强一致要对所有 secondary 做两轮通信征得同意才写、primary 失败后同步分歧的 secondary、过滤重复客户端操作、防止读到陈旧的缓存 chunk 位置。
参考资料
- 课件:GFS (l-gfs.txt);
- 论文:GFS (2003);
- FAQ