谷歌文件系统
概要
我们的设计实现了谷歌文件系统,一个英语大型分布式数据增强应用可扩展的分布式文件系统。他提供了在廉价的通用一件在运行时的容错,它为大量的客户端提供了高聚合性能。
虽然和先前的文件系统有许多相同的目标,我们的设计通过观察应用负载和技术环境当前与预期驱动的,它反应了与早期文件系统明显的背离。这导致了我们重新审视了传统的选择,探索截然不同的设计点
文件系统已经成功满足了我们的存储需要。它被广泛的部署在谷歌内部,作为我们服务使用的数据生成和存储平台,以及需要大数据集的研发工作。迄今为止的大规模集群提供了数百TB的存储在几千个机器的上千个磁盘中,并且可以被上百个连接端同时访问。
在这个文章中,我们介绍文件系统接口的扩展设计已提供分布式应用。讨论一些我们的设计方面,并且上报在小型基准测试和真实使用中的测量值。
介绍
我们设计并实现了谷歌文件系统(GFS)以应对快速增长的谷歌数据处理需求。GFS与先前的分布式文件系统有许多相同的目标,比如性能,可扩展性,可靠性。然而它的设计是由对应用程序负载观察和技术环境驱动的,包括当前的和预期的,它反应了与早期的文件系统概念的一些背离。我们新审查传统的选择并从根本上探索在设计空间中根本的不同点。
第一,受限组件的故障是常态而不是意外。文件系统包含了成百的甚至上千的廉价的通用部分存储机器,由相当数量的客户端机器。组件的数量和品质几乎保证了,有些在任何时候都不能正常工作,有些不同从当前故障中恢复过来。我们看到了应用bug,操作系统bug,人为错误和磁盘,内存,连接器,网络电源错误导致的问题。因此,持续不断的监视器,异常单测,故障容忍,和自动康复在系统中是必要的。
第二,文件在传统标准中通常是巨大的。几个GB的文件是常见的。每个文件通常包含了几个应用对象比如web文档。当我们经常处理快速增长的几个包含几个TB数十亿个对象的数据集时,它通常难以管理数十亿个KB级别大小的文件,即使文件系统可以支持它。这导致了,必须重新审视类似I/O操作和块大小的参数
第三,大部分的文件都是通过添加新的数据来修改的,而不是重写现有的数据。文件的随机通常是不存在的。一旦写入文件就智能杯读取,而且通常是之内顺序读。不同的数据都有这个特征。其中可能包括一些构成数据分析程序的扫描大型存储。一些数据被运行程序流连续不断的生成。一些可能是档案资料。一些可能是来自一个机器同时或者晚些在另一台机器处理。考虑到这种大文件的访问模式,追加操作成为性能优化和原子保证的重点。在客户端缓存数据失去了吸引力。
第四,共同设计应用程序和文件系统API通过增加我们系统的灵活性使整个系统收益。
例如,我们放宽了GFS一致性模型极大的简化了文件系统而不会在应用上带来繁重的负担。我们也引入了原子化追加操作,使多个客户端可以同时追加文件而不需要在彼此间进行同步。这将在本文后面进行详细的讨论。
当前GFS集群被部署用于不同的目的,最大的一个超过了1000个存储节点,超过300TB的磁盘存储,并且被不同机器上的数百个客户端连续地大量访问。
2设计预览
2.1假设
在设计我们需要的文件系统时,我们根据假设和指导它提供了挑战和机遇。我们提到了关键性的观察结果,现在更详细的列出我们的假设。
- 系统是由许多经常会失败的廉价的通用组件构成的。它必须监控自身并在常规基础上检测、容忍组件故障,并迅速从组件故障中恢复。
- 这个系统里存储了少量的大文件。我们预计会有几百万个文件,每个文件通常会是100MB会是更大。几个GB的文件是常见的情况,应该被有效的管理。小文件必须被支持,但是我们不需要优化他们。
- 工作负载主要包含了俩个类型的读取:大型的连续读和小型的随机读。在大的顺序读取中,单个操作通常需要读取数百KB更常见的是1mb甚至更多。同一客户机的连续操作通常读取文件的连续区域。一个小的随机读取通常以一些容易偏移读取几个KB。注意性能的应用程序通常对他们的小读取进行批处理和排序,以便稳定的前进而不是来回移动。
- 工作负载也有很大的,顺序些操作,将数据附加到文件中。操作大小与读取操作大小类似。一旦读取,文件很少被再次改动。支持在任意位置进行小的写入,但是不一定要高效。
- 文件系统必须为了多个客户端同时追加文件有效的实现定义良好的语义。我们的文件通常用于生产者消费队列或者多路合并。上百个生产者,每台机器运行一个,将会同时追加文件。具有最小的同步开销的原子性是必要的。该文件可以稍后读取,或者消费者可以同时读取该文件。
- 高带宽比低延迟更重要。大部分目标程序都非常重视高速率批量处理数据,虽然很少对于单独的读写请求有响应时间要求。
2.2接口
GFS提供了常见的文件接口,虽然他不识闲任何标准的API例如POSIX。文件在目录中中有组织的分层并通过路径名进行识别。我们支持常见的操作,比如新增、删除、打开、关闭、读取和写入。
此外,GFS快照和记录追加操作。快照创建了文件的副本或者目录树在低代价的情况下。记录的追加通过多个客户端追加数据到相同的文件,同时保证每个客户端的原子追加。它对于实现多路归并结果和生成消费队列非常有用,其中一些客户端可以同时追加而不需要额外的锁定。我们发现这些文件在构建大的分布式系统时是无价的。快照和记录的追加会再之后的3.4章,3.3章分别进一步讨论。
2.3架构
GFS集群包括了单个主(master)和多个块集群(chunkservers)被多个客户端访问。如图1。他们中的每个都是通用Linux机器运行在用户级的服务进程。它很容易在同一个机器上同时运行块服务和客户端,只要机器资源允许并且由于运行可能不可靠的应用代码是可接受的。
文件被分成大小固定的块。每个数据块在创建时由主服务器分配一个不可变且全局唯一的64位数据块句柄来标识。块存储块在linux文件系统上的本地磁盘中,并根据块句柄和字节范围的指定读或者写入块数据。为了可用性,每个块都会备份到多个块服务上。默认情况下,我们存储三个副本,不过用户可以设计不同的复制等级在不同文件命名空间的分区中。
master维护所有文件系统的元数据。包括命名空间,访问控制信息,文件到块的映射,当前块的位置。它也控制了整个系统的活动,比如块租赁管理,孤立块的垃圾收集,和块在不同的块服务中的迁移。主定期与每个块服务使用心跳信息连接,以给他们指令和收集状态。
GFS客户端代码连接到每个实现了文件系统API的应用,并与master连接,代表应用程序读取或写入数据。客户端与主通过元数据交流,但是所有承载数据的通信都直接发送到块服务器,我们不提供POSIX API因此不需要挂勾到Linux的vnode层。
客户端和块服务都不缓存文件数据。文件提供了更小的收益,因为大部分应用流式处理大文件或者有大的工作集以至于无法缓存。没有它们简化了客户端和整个文件系统,消除了缓存一致性问题(然而客户端会缓存元数据)。块服务不需要缓存文件数据因为块被存储在本地磁盘上诸如Linux缓冲缓存已经保证了频繁访问的数据会在内存中。
2.4单主
存在一个主极大的简化了我们的设计并且能够让主进行复杂块的放置和使用全局知识进行副本决策。然而我们必须最小化读写的参与,所以它不会成为瓶颈。客户端从来不通过主读或者写数据。相反,客户端询问主它应该联系哪个块。它在有限的时间内缓存这些信息并与其他的块服务在之后的几个操作中直接互动。
让我来解释一下图1中所涉及的简单读取的交互。受限使用固定的块大小,客户端将应用程序的文件名和字节偏移量转化为文件中的块索引。然后,它发送给主包含文件名和块索引的请求。主回答对应的块句柄和复制品的位置。客户端使用文件名也就是键缓存这个信息。
客户端发送请求到一个副本,最优可能是最近的一个。请求指定了块中的块句柄和字节范围。对同一个块的进一步读取不需要更多的客户端和主交互,直到缓存文件到期或者文件被重新打开。事实上,客户端通常在相同的请求中询问多个块,而主服务器也可以在请求后立即包含数据块的信息。这些额外的信息在没有额外开销的情况下,避免了将来客户端和主的几次交流。
2.5块大小
块大小是设计参数的其中一个关键。我们选择了64MB,它比典型的文件系统块大小大得多。每个块副本以普通的Linux文件的形式存储在块上并且仅在需要的时候进行扩展。懒空间分配避免了由于内部碎片导致的空间浪费,这可能是反对如此大的块大小最重要的原因。
大的块大小提供了几个重要的优势:第一,它减少了客户端和主需要的交流因为读取和写入在同一个节点上,只需要一个初始请求让主来获取到块为止信息。在我们的工作负载中这个减少的非常显著的,因为因为应用大部分的读写大文件都是顺序的。甚至对于小的随机读,客户端可以舒服的缓存几个TB工作集的所有的块地址信息。第二,在大块上,客户端更偏向于在给定的块上执行多个操作,它可以通过保持较长与块服务的持久TCP连接来减少网络过载。第三,它减少了元数据在主节点上的存储大小。这允许我们将元数据存放在内存中,这反过来有带来了其他优势我们将在2.6.1章讨论。
另一方面,一个大的块大小,即便使用了懒空间分配,也有其缺点。小文件由少量块组成,也可能只有一个。块服务排序了这些块可能会导致热点,如果许多客户端访问同一个文件的话。在实践中,热点不是主要的问题因为我们的应用程序大多是按顺序读取的多块文件。
然而,热点在在批处理队列首次使用GFS时,确实出现了热点:可执行文件作为单个文件块写入GFS,然后同时在数百台机器上启动。少数存储这些可执行文件的服务器因为同时处理数百个请求而超载。我们通过更高的复制等级并通过批量队列系统错开应用程序启动时间来解决这个问题。一个潜在的重启方案是允许客户机在这种情况下从其他客户机读取数据。
2.6元数据
主存储了3个主要的元数据类型:文件与块命名空间,从文件到块的映射,每个块副本的位置。所有的元数据被保存在主的内存中。前俩个类型(命名空间和文件到块的映射)同样也通过将更改记录到主的本地磁盘中的操作日志来保持持久化。使用日志允许我们简单,可靠的更新主的状态,并且在主崩溃的情况下没有不一致的风险。主不持久化存储块位置信息。相反,它询问每一个块服务关于它的块在主启动和任何块服务加入集群时。
2.6.1在内存中的数据结构
因为元数据存在于内存之中,主操作是非常快的。此外,主可以简单有效的在后台扫描整个状态。周期性的扫描被用来执行块垃圾收集,重新复制存在异常的块,并且在块服务之间大量迁移负载均衡和磁盘空间使用。章节4.3和4.4将会进一步讨论这个行为。
这种使用内存的方法有一个潜在的问题就是,块的数量因此受到主节点中拥有的内存容量的限制。在实践中它不是重要的限制。主机维为每个64MB的块护了少于64个字节的元数据。大部分的块是满的因为大部分的文件包含许多块,只有最后一个可以部分填充,类似的,文件命名空间的数据通常被包含在64个字节一个文件,因为它使用前缀压缩紧凑地存储文件名。
如果必须支持更大的文件系统,添加额外的内存到主节点,只需要很小的代价就可以得到简单,可靠,高效,弹性,问你们将元数据存储在内存中而获益。
2.6.2块位置
主不保留块给定的块在块服务中的记录的持久化记录。它只是在启动时轮询块以获取该信息。在那之后master可以保持自最新的状态,因为它控制所有的块并安置和使用定时心跳信息监听块状态。
我们最初企图保持块在master上的信息,但是我们明白在启动时候从块中请求数据是更简单的,自那以后。自那以后消除了在master上的问题并且在可以在块服务加入离开集群,修改名字,失败,重启等情况下块服务仍保持同步,在集群有上百个服务时,这件事情会经常发生。
另一个理解这个设计决策是为了实现块服务有最终决策权决定它的磁盘上有块还是没有块。尝试在主机上维护信息视图是没有意义的。因为在块服务上的异常可能导致块自发消失(即磁盘可能损坏或者被禁用)或者操作员可以重命名区块。
2.6.3
操作日志包含了对于关键数据的概念。这是GFS的核心。它不仅是对元数据的记录,也是服务作为一个逻辑时间线,它的定义是为了并发操作的顺序。文件和块,以及他们的版本(查看章节4.5),都是唯一的并且由创造他们的逻辑时间所标识。
因为操作日志是关键的,我们必须可靠的存储它,不让更改对客户端可见直到元数据被持久化。否则我们将失去整个文件系统或者最近客户端的连接操作,即使区块自己还活着。因此,我们在多个远程几区备份它,并且仅在我们同时刷盘了对应的日志记录在远程和本地时才对客户端进行响应。在刷盘之前主批处理多条记录到一起,以此减少刷盘的对于整个系统吞吐量的影响。
主恢通过重演操作日志复文件系统的状态。为了最小化启动时间,我们必须保持了日志较小。每当日志的增长超过特定的大小,master检查它的状态,可以隐通过加载最近磁盘中的的检查点并在之后只重放有限的数据记录。检查点呈现紧凑的b树形状,它可以直接映射到内存用于命名空间的查找无需额外的解析。这进一步提升了恢复速度并增强了能力。
因为建立检查点可以需要一段时间,主机内部的状态是这样构造的,它可以在不延迟新来的变化的情况下创建检查点。主切换到新的日志文件并创建新的检查点在独立的线程。新的检查点包含了在切换之前的所有改变。对于一个数百万的文件集群,它可以在一分钟内创建。当计算完毕,它同时写入本地和远程磁盘。
恢复只需要最新完成的一个检查点,和随后的日志文件。旧的检查点和日志文件可以被自由删除,虽然我们会保持几个周期以防范灾难。检查点过程中的失败不会影响准确性,因为恢复代码会跳过检查点。
2.7一致性模型
GFS有一个宽松的一致性模型,它很好的支持我们高度分布式的程序,但是保留了相对的简单和高效的实现。我们现在讨论GFS和对应用的实际意义。我们也强调如何让GFS保持这些保证,但是保留细节给论文的其他部分。
2.7.1 GFS的保证
文件命名空间的变化(即文件创建)是原子的。他们仅仅被master进行处理:命名空间的锁保证了原子性和正确性(4.1章)主操作日志定义了这些操作的全局总顺序(章节2.6.3)。
写 | 记录追加 | |
---|---|---|
顺序成功 | 确定的 | 确定中穿插着不一致 |
正确成功 | 一致性但是不确定 | 确定中穿插着不一致 |
失败 | 不一致 | 不一致 |
表1:文件块在修改之后的状态
数据修改之后的文件块状态取决于修改的类型,无论它是成功还是失败,无论是不是正确的操作。表1描述了结果。如果所有的客户端将总是看到同一组数据,那么文件块是一致的,不管他们读取的是哪个副本。区块在文件数据变化之后被确定,如果它是保持一致性的,客户端将会看到整个变更写入的内容。当一个修改在没有并发写的情况下成功。受影响的区域是确定的(隐含了保证一致性):所有的客户端都会看到什么变化被写入了。并发操作会让区块处于未定义状态,但是仍然保持一致性。所有的客户端看到了相同的数据,但是他不能反应是哪个改变写入了。通常,由来自多个改变的混合片段构成。错误的变化让区块不一致(因此也不确定):不同的客户端可能在不同的时间看到不同数据。我们在下面描述我们的应用如何能清晰的区分确定的区块和未定义的区块。这个应用不许对不同类型的未定义区块进行进一步的区分。
数据修改可能是写入或者是追加记录。写操作导致在应用的特定文件偏移被写入。以此记录追加操作导致了数据原子性的添加只少一次即使同时的修改是存在的,但是以GFS选择的偏移量进行追加(章节3.3)。(相反,“常规”追加只是在客户端认为是文件当前结束的偏移量处进行写操作。)这个偏移被客户端返回并且标记了包含记录的确定区块的开始。此外,GFS可能插入填充或者记录副本在俩者之间。被占有的区域被认为是不一致的, 通常与用户数据量显得微不足道。
在一系列的成功改变之后,修改的文件区域保证是确定的,包含了最后一次修改写入的数据。GFS通过应用在所有副本上相同顺序对块应用改变来实现这一点(3.1章)。并且使用区块版本号以检查任何已经过时了的副本,因为当块服务下线时,它错过了改变(4.5章)。陈旧的副本将永远不会参与改变或者给客户端请求主数据块位置。他们会被尽快垃圾收集。
因为客户端缓存了块位置,它们可能在信息刷新之前从老副本读取。这个窗口受到缓存项超时和下次打开文件的限制,从缓存中清除该文件块的所有块信息。进一步,大部分我们的数据是只追加的,过时的副本通常返回过早的结束,而不是过时的数据。当读取者重试或者联系主服务器时,它将立即获得当前区块的位置。
在一个成功的改变发生很久之后,当然组件失败仍然会破坏或者销毁数据。GFS通过定期在主和所有区块之件握手鉴别失败的块服务并通过校验和探测数据是否被破坏。(5.2章)一旦发生问题,数据从有效的副本中尽快的恢复。(章4.3)。只有在GFS做出反应之前所有区块副本都丢失了,一个块才会不可逆的丢失,通常是在几分钟内。即便在这个例子中,它变得不可用,而不是崩溃:应用接收到清晰的错误,而不是损坏的数据。
2.7.2 对应用的影响
GFS应用通过一些简单的技术能提供宽松的一致性模型,这些技术也是其他目的所必须的:依赖于追加而不是重写,检查点,写自我验证,自我识别记录。
事实上,我们所有的语言改变文件都是通过追加而不是重写的。在一个典型的用法中,写入器从到到尾生成一个文件。他在写入了所有数据之后,将文件原子的重命名为一个恒定的名字,或者定期检查已经写入了多少数据。检查点还包括了应用级别和校验和。读取器只校验和处理到最后一个检查点的文件区域,该检查点已知处于确定状态。不管一致性和并发问题,这种方法对我们很有帮助。追加更有效率并更有弹性应对与应用的失败,而不是写入。检查点允许写入器逐步重启并防止读取器不会处理那些已成功写入的文件数据,着从程序的视角来看仍然是不完整的。
在另一个典型的用法中,一些写入器并发追加到文件中以合并结果或者作为生产消费队列。记录追加的‘至少追加一次’语义保留了每个写入器的输出。读取器处理偶尔的填充或者副本的方法如下。写入器准备的每条记录都包含校验和等额外信息以便验证其有效性。读取器可以使用校验和识别和丢弃多余的填充和记录段。如果它不能容忍偶尔的重复,(即如果它会触发非幂等性的计算)它可以不适用特定记录中的标识符过滤他们,通常需要命名相应的应用程序实体(如web文档)。这些记录I/O的功能(去除重复)是我们的应用程序共享代码库中,并适用于谷歌的其他文件接口实现。
3系统交互
我们设计的系统最小化主在操作中的相关性。在这个背景下,我们现在描述客户端,主,块服务和实现数据改变之间的交互,原子记录追加,和快照。
3.1租约和变化的顺序
变化是一种改变数据块内容或者元数据块的操作,比如些或者追加操作。每个改变都在所有的副本块上执行。我们使用租约维护在副本之间一致性的改变顺序。主将块租约授予其中一个副本,我们交它primary。primary选择为数据块的所有突变选择一个序列顺序。当应用了改变时所有的副本跟随这个顺序。因此,全局的改变顺序被主选择的租约授予顺序定义,并在一个租约内由primary分配序列号。
租约机制的设计目的是最小化主服务器的管理开销。租约的初始超时为60秒。然而,在只要那个块还在被修改,primary可以无限期地请求和接收主的扩展。这个请求和授权会被附加在心跳信息上,定期的在主和块服务之间进行交换。主有的时候会尝试在到期之后前回租约(即,当主希望禁止修改对一个正在重命名的文件进行修改),甚至如果主丢失了与primary的连接,它可以安全的将租约授予其他的副本在老的副本到期后。
如图2,我们通过这些编号的步骤,遵循写操作的控制流,来描述这个过程
- 客户端告诉主哪个区块持有当前的块租约以及其他副本的位置。如果没有一个有租约,服务器将会选择一个副本授予租约(未展示)。
- master回复primary身份的机器,并定位其他副本(secondary)的位置。客户端 缓存加来修改的数据。客户端缓存这些数据已备将来的变化。仅当primary不可达或者回复说它不再持有租约时,需要再次连接主。
- 客户端对推送数据到所有副本。客户端可以以任何顺序执行此操作。每个块服务奖数据存储在内部LRU缓冲区缓存中,直到数据被使用或者超时。通过奖数据流和控制流解耦,不管哪个块服务是primary,我们都可以通过基于网络拓扑来调度昂贵的数据流来提升性能。3.2章回进一步讨论。
- 一旦所有副本承认收到数据,客户端发送写请求到primary。这个请求标识除了之前推送到所有副本的数据。primary为它接收到的所有接收到的修改分配连续的序列号,可能来自多个客户端,这提供了必要的串行。它按序列号顺序将改变应用到自己持有的本地状态。
- primary 将写请求转发给所有的secondary 副本,每个secondary 副本应用相同被primary 指派的的序列号顺序进行变更。
- 所有的secondaries 都会回复primary 服务器表明他们已经完成了操作
- primary回复客户端。任何副本上发生的错误将会上报到客户端。如果发生错误,写可能在primary上成功,任意secondary副本的子集失败(如果在primary上发生错误,它将不能指派序列号和转发)。客户端请求被认为失败,修改后的区块处于不一致的状态。我们客户端通过重试发生异常的操作来处理类似的错误。它将在回退到从写操作的开始,在步骤3到7之间进行几次重试
如果写入过大或者跨越了区块的边界,GFS客户端代码将他分解为多个写操作。他们都遵循着上述控制流的描述,但是可能与其他客户端的并发操作交叉或者覆盖。因此,共享的文件区块最终可能包含不同客户端的片段,虽然副本是一样的,在所有副本上以相同的顺序成功完成单个操作。这使得文件区域保持一致,但未定义状态如2.7章所属。
3.2 数据流
我们将数据流与控制流解耦,以有效利用网络。当控制流从客户端流向primary然后流向secondaries,数据以管道的方式沿着小心挑选的块服务链线性推送。我们的目标是充分利用每台机器的网络带宽,避免网络瓶颈和高延迟连接并最小化延迟以推送所有数据。
为了完全利用每个机器的网络带宽,数据沿着块服务链线性的推送,而不是分布在其他的拓扑结构中(比如树)。因此每一个机器出栈带宽被用来尽可能的块的传输数据,而不是在多个接受者之间分配。
为了尽可能避免网络瓶颈和高延迟的连接(例如,交换机之间的连接通常是瓶颈和高延迟的)。每个机器转发数据到在网络拓扑中最接近的尚未接收到的数据的机器。假设客户端正向块服务S1到S4推送数据。它发送数据到最近的块服务,比如S1。然后S1将数据转发到最近的块服务S2与S4,假设是S2,S2转发数据到S3或者S4中最接近S2的一个,以此类推。我们的网络拓扑是足够简单的。距离可以被精确的估计通过IP地址。
最后,我们通过TCP进行管道的连接传输最小化延迟。一旦块服务收到一些数据,他立即开始转发。管道对我们特别有帮助,因为我们使用全双工的交换网络。立即发送数据不会减少数据的接受率。在没有网络拥塞的情况下,将B字节传输到R副本的理想运行时间是B/T + RL,其中T是网络吞吐量,L是在两台机器之间传输字节的延迟。我们的网络连接是常规的100Mbps(T),L会超过1毫秒。因此1MB理想情况应该是80ms左右。
3.3原子记录追加
GFS提供了叫做记录追加原子追加操作。在传统的写入中,客户端指定写入数据的偏移量。并发写相同的区块不是串行的:该区块最终可能包含来自多个客户端的数据片段。然而在记录追加中,客户端只是指定数据。GFS在GFS选择的偏移量处只少追加到文件一次(即 作为一个连续的字节序列)并返回偏移给客户端。这类似于在Unix中O_APPEND模式打开的文件写入数据,当多个写并发时,没有竞争条件。
记录追加被我们的分布式应用大量使用,其中很多的客户端在不同的机器上同时追加相同的文件。如果他们用传统写的方式,例如通过分布式锁管理器,客户端可能需要复杂和昂贵的同步。在我们的工作中,类似的文件通常用作多生产者-单消费者队列,或者包含了来自不同客户端的合并结果。
记录追加是一种变更,并按照3.1章中的控制流,只有很小的额外的逻辑在primary上。客户端推送数据到文件最后一个块的所有副本,它会发送它的请求到primary。primary会检查追加记录当当前的区块是否会导致块超过在最大值(64MB)。如果发生了,它填充区块到最大的大小,告诉secondaries 做同样的事情,并告诉客户端它的操作需要到下一个块进行重试(记录添加受最大限制为最大块大小的四分之一,以将最坏情况下的碎片保持在可接受的水平)。如果记录符合最大大小,这种情况比较常见,primary 追加记录到它的副本,告诉secondaries将数据写在它们自身精确的偏移位置,最后回复成功给客户端。
如果记录在任意副本上追加失败,客户端重试这个操作。 因此,同一数据块的各个副本可能包含不同的数据,甚至可能包含相同记录的全部或部分重复。GFS不保证所有副本是字节相同的。它只保证数据作为原子单元最少写入一次。这个性质很容易在观察中得到,为了报告成功,数据必须在某个块的所有副本上以相同的偏移量写入。此外在这之后,所有的副本只少与记录结束时一样长,并且其他进一步的记录会被指派到更高的偏移量或者不同的块,即使另一个副本稍后回成为primary。根据我们的一致性保证,成功的追加操作已写入数据区域是确定的(因此保持一致性),然而中间区块是不一致的(因此未确定)。我们的应用可以处理不一致的区块就像我们在2.7.2中讨论的一样。
3.4快照
快照操作几乎是在瞬间复制一个文件或者目录树(源),同时尽量减少任何的对正在进行突变的干扰。我们用户使用他对于大的数据集快速创建分支副本(通常递归的复制这些副本)。或者在尝试改变之前,创建当前状态的快照,可以很容易在之后提交或者回滚。
就如同AFS一样,我们使用标准的写时备份技术实现快照。它首先撤销它要快照文件中未到期的租约。这确保了对这些区块的任何后续写入都需要与主交互以找到租约持有者。这将会给master机会受限创建块的副本。
在所有租约被撤销或到期之后,主记录操作到磁盘。然后,通过复制源文件或目录树的元数据,将日志记录应用与其内存中的状态。新创建的快照文件指向与源文件相同的块。
在快照操作完成后,客户端第一时间想要写入块C,它发送请求到主以寻找当前的持有者。主注意到块C的引用计数大于1。它推迟客户端的请求,而选择了一个新的块处理C'。然后告诉每个块服务有一个当前C的副本被创建了,这个新的块名字叫做C'。通过创建新的块在同一个块服务源,我们确保数据可以被本地复制,而不是通过网络(我们的磁盘大概是我们100MB以太网的三倍速度)。从这一点来看,请求处理与任何块的处理没有什么不同:主授在一个新的区块C'上权副本一个租约,并回复给客户端,客户端可以正常的写区块,不知道他是从现有块中创建的。
4 主的操作(master operation)
主执行所有命名空间操作,此外,它管理整个服务的块副本:它做出拜访决策,创建新的块和副本,协调系统中各种活动以保持块被完全复制,在块服务之间的负载均衡和回收未使用的存储。我们现在讨论每一个主题。
4.1 命名空间管理和锁
一些主操作要花费大量时间,例如快照操作必须撤销快照覆盖的所有块的的块服务租约。我们不想要延迟其他主操作在运行时被延迟。所以,我们允许多个操作被创建并在命名空间的区块上使用锁以确保真正的串行化。
不像传统的文件系统,GFS不对每个目录维护数据结构,那要列出目录中的所有文件。也不会对同名的文件或者目录支持别名(即硬链接或Unix属于中的符号链接)。GFS逻辑上将其名称空间表示为一个查找表,该表完整的路径映射到元数据。使用前缀压缩,这个表可以高效的在内存中表示。每个节点在命名空间树(绝对文件名或绝对目录名)有一个相关的读写锁。
每个主的操作在运行之后获取一组锁。通常如果它包含了/d1/d2/.../dn/leaf,它将会在文件名/d1/,/d1/d2,...,/d1d/d2/.../dn获取读锁。以及完整路径名/d1/d2/.../dn/leaf上的读写锁,注意这个leaf可以是文件或者目录取决于操作。
我们现在阐述这个锁机制如何防止在将/home/user快照到/save/user时创建/home/user/foo文件。快照操作获得在/home和/save的读锁,和/home/user与/save/user的写锁。文件创建获取/home和/home/user的读锁和/on//home/user/foo的写锁。俩个操作将会被正确的串行,因为他们尝试获取/home/user上冲突的锁。文件创建不需要父目录的写锁,因为不是目录或者索引节点,要防止数据结构被修改。名称上的读锁足以保护目录不被删除。
加锁机制的好处在于它允许并发在相同目录的变化。例如,多个文件创建可以被在同一个目录上并发执行:每个获取在目录名上的读锁和在文件名上的写锁。目录名上的读锁足以防止文件被删除,改名或者快照。文件名上的写锁序列化两次创建同名文件的尝试。
因为名称空间可以有多个节点,读写锁对象是懒分配的,在它不适用的时候删除一次。同样,锁以一次性的总顺序获得以防止死锁:它们首先在名称空间树中按级别排序,按字典顺序排列在同一级别内。
4.2副本放置
GFS集群在多个层面都是高度分布式的。通常有上百的块服务分布在多个机架上。这些块服务反过来可能会被上百个机架上的客户端存取。在一个机架上的俩个机器可能要穿过一个活多个网络交换机。此外,进入活离开机架的带宽可能比机架内所有机器的总带宽更少。多级分布提出了对于分发数据和可扩展性,恢复性,可用性的一个特殊的挑战。
块副本放置策略是由俩个目的:最大化数据可靠性和可用性,并最大化网络带宽录用率。对于这俩种情况,将副本分散到不同的机器是不够的,它只是防止磁盘或者机器失败并充分利用每个机器的网络带宽。我们也必须跨机架分布块分布。这确保了一些块副本将会幸存和依然可用,即便整个机架被损害或者下线(例如,由于共享的资源失败,比如网络交换机或者电源),尤其是读取,对于每个块可以利用多个机架的聚合带宽。另一方面,写流量必须流经多个机架,这是我们愿意做的权衡。
4.3创建,再复制,再平衡
区块副本的创建主要有三个原因,块创建,再复制,再平衡。
当主创建了块,它选择在哪里防止最初的副本。它会考虑几个因素(1)我们想要放置新的副本在块服务上兵保持低于平均的磁盘空间占用。久而久之它将均衡在不同块服务之间磁盘的占用率(2)我们想要限制每个块服务上最近创建的数量。虽然创建本身是廉价的,它可靠的预测即将到来的大写入流量,因为块服务是要在准备写入是创建的,并且在我们的追加一次读取多次工作负载中,一旦它们完全写入,它就变成了只读。(3)如上所述,我们需要分配副本到不同的机架上。
一旦可用的副本低于用户指定的数量,主再次复制块。这可能有多重原因:块服务变得不可用,它报告它的副本可能以损坏,其中一个磁盘因为错误被禁用,或者增加了复制目标。需要重新复制的每个块基于几个因素确定优先级。一个是它距离复制目标有多远。例如,我们给丢失了俩个副本块比只丢失一个副本块更高的优先级。此外,我们倾向于首先重新复制活动文件的块,而不是最近删除文件的块(见章节4.4)。最后,最小化故障对运行时应用的影响,我们提高了阻塞客户端进程块的优先级。
主选择最高优先级的区块,并通过指示一些块服务直接从有限的有效副本复制块数据来克隆它。新的副本的目标与创建时的目标相类似:相同的磁盘录用率,在单个块服务上限制活动的克隆操作以及在机架上散布副本。以防止克隆流量压倒客户端流量,主限制了集群和每个块服务活动克隆操作的数量。此外,每个块服务限制了带宽量,它通过限制源块服务的读请求来花费在每个克隆操作上。
最后,主定期重平衡副本:他审查当前副本的分布并移动副本为了更好的磁盘空间和负载均衡。也是通过这个过程,主逐渐填满了一个新的块服务,而不是立刻淹没一个新的块同时大量的写流量也会随之到来。新副本的防止标准与上面的讨论类似。此外主页必须选择哪个现存的副本应该被移出。一般的,主倾向于删除那个剩余空间低于平均水平的块服务,以保证相同的磁盘空间占用。
4.4垃圾回收
在文件被删除之后,GFS不会立即要求归还可用的物理存储。它仅在文件和块级别的常规垃圾收集起见惰性执行此操作。我们发现这个方式让系统更简单并更可靠。
4.4.1机制
当文件被应用删除,主立即记录删除的部分就像其他改变一样。然而不是立即回收资源,文件只是被重命名成为包括了删除时间戳隐藏文件。当主在系统的命名空间中常规扫描,如果它发现现存的隐藏文件超过3天(间隔可以配置),移出如何类似的隐藏文件。直到那时,文件仍然可以存在新的,特殊的名称,可以通过将其重命名来从删除中恢复。当隐藏文件被在命名空间中删除,它在内存的元数据中被擦除。这有效切断了和其他块的链接。
在类似对块命名空间的常规扫描中,主辨别出孤立的区块(即那些无法从任何文件访问的)并且擦除这些块的元数据。在定期的心跳信息中与主做交换,每个块服务上报他们所持有的块子集,主会回复所有不存在主服务器元数据中块的信息。块服务可以自由删除这些块副本。
4.4.2 讨论
尽管分布式的垃圾收集是一个困难的问题,它依赖于编程语言上下文中复杂的环境,在我们的例子中很简单。我们可以简单识别所有对块的引用:他们位于有主服务器专门维护的映射中。我们也可以简单的识别所有的块副本:它们是每个块服务上指定目录下的linux文件。任何不为主所知道的副本都是垃圾。
与急切的删除相比,垃圾收集存储回收方法有几个有点。它在组件故障常见的大规模分布式系统中简单可靠。块的创建可能在一些系统上成功但是在另一些系统上失败,主不知道幸存的块还存活着。副本的删除信息可能会丢失,并且主必须记得在失败的时候重新发送它们,无论是自身的失败还是块服务们的失败。垃圾收集提供了一致和可靠的方法清理任何不知道有用的副本。第二,它将存储回收合并到主服务器的常规后台活动中,如定期扫描命名空间和与块服务握手等操作。因此,它是分批完成的,成本是平摊的。此外,它只有在主相对自由的情况下才会这样做。主可以恢复那些需要即使关注的请求更加的迅速。第三,回收的延迟提供了一个安全网,防止意外的,不可逆转的删除。
在我们的经历中,主要的劣势是一些时候的延迟妨碍了用户在存储紧张的时候调整录用率。应用频繁的创建删除临时文件可能不能立即再次使用存储。我们通过推进存储回收解决了这些问题:如果删除的文件直接再次删除。我们允许用户在命名空间的不同部分使用不同的复制和回收策略。例如,用户可以指定某个文件树中文件中的所有块都不进行复制,并且删除的文件是立即不可撤回的从文件系统的状态中删除。
失效的副本探测
如果块服务器故障,并且在关闭时错过了对块的改变,块副本可能变得陈旧。对于每个块主保存了块版本号以区分最新的和过时的版本。
每当主授予新的租约给块,它增加块的版本号并通知信的副本。主服务器和这些副本都在它们的持久状态中记录了最新的副本。这发生在任何客户端收到通知之前,因此客户端开始向该数据块写入数据之前。如果其他的副本当前不可用,那么它的版本号将不是最新的。当块服务重启并上报它的块集合与它相关的版本号时 ,主将探测块服务有过时的副本。如果主看到了版本号记录中的那个版本号,主会假设它在授予租约时失败,因此将较高的版本最为最新的版本。
主删在它的垃圾收集中除失效的副本。在那之后,当它回复客户端的块信息请求时,它实际上视作失效副本不存在。另一个措施,当主服务器通知客户端哪个块服务持有一个块租约时或者在克隆操作中指示块服务读取该块时,它会包含块的版本号。客户端或者块服务校验版本号,当他执行操作时,它总是访问最新的数据。
5 容错与诊断
我们设计中最优挑战的之一是处理频繁的组件异常。组件的质量和数量让这些问题比一般的异常更加常见:我们不能完全的信赖机器,也不能完全信任磁盘。组件的异常可能导致系统不能被访问或者更糟糕的数据崩溃。我们讨论我们如何处理这些遇到的挑战和我们在系统中内置的工具,当问题不可避免的发生时诊断问题。
5.1高可用
上百个服务器在GFS的及群中,一些是机器必然在任何时间必然是不可用的。我们用俩个简单和有效的策略保持我们整个系统的高可用:快速恢复和复制。
5.1.1快速恢复
主和块服务被设计为恢复他们的状态并启动,无关他们什么时候终止,都要在几秒内开始。事实上我们不区分常规终止和异常终止;服务器通常通过终止进程来关闭。客户机和其他服务器会遇到一个小问题,因为它们会超时处理未完成的请求重新连接到重启的服务器,然后重试。章节6.2.2报告了观察到的启动时间。
5.1.2块复制
在早期的讨论中,每个块在不同的机架上的多个块服务上进行复制。用户可以指定文件命名空间不同部分的级别。默认是三。主根据需要克隆的现有副本,以保证当块服务下线或者通过校验和检测到损坏的副本时,每个数据块都能完全的复制(见5.2章)。虽然,复制对我们很有好处,我们探索其他跨服务的冗余形式,比如奇偶校验或者擦除码以满足我们只读存储请求。我们认为在我们松耦合的系统中实施这些更复杂的荣誉方案是有挑战性但仍可以管理的,因为我们的流量由追加和读取进行控制的,而不是随机写。
5.1.3主复制
主的复制是为了提高可靠性。它的操作日志和检查点会被复制到多个机器上。只有日志记录被刷到盘底磁盘和全部的主副本中,才会认为装填的突变已提交。简单起见,一个主进程仍然控制着所有改变以及后台活动,比如来自系统内部的垃圾收集。当它失败了,它可以几乎顷刻重启。如果机器或者磁盘故障,监视GFS外部的基础设施在其他地方复制操作日志启动一个新的主进程。客户端只是用主服务器的规范名称(即 gfs-test),它是DNS别名,如果主被重新定位到另一台机器上。
此外影子主机提供了只读访问文件系统即使主下线了。这是一个影子而不是镜子,因为它们可能稍微落后于主光源,通常是几分之一秒。它增强了那些不积极被修改或者那些不介意获取到稍微过时状态的应用程序的可用性。事实上,因为文件内容是从块服务中读取的,应用不会观察到过期的内容。在短期窗口内过期的可能是文件的元数据,比如目录内容或者访问控制信息。
为了保持自身的沟通,影子主读取增量的操作日志副本并应用相同的修改序列到数据结构中就像主一样。在启动时和主一样轮询块服务(之后很少)以确定块副本和交换频繁的握手信息以监视他们的状态。它仅依赖于在服务来进行由于主决定创建和删除导致的副本位置更新。
5.2 数据完整性
每个块服务使用校验和以检测存储数据的损坏。给定的GFS集群通常有几千块磁盘在上百台机器上,它定期经历磁盘故障这会导致数据错误或者丢失读写路径(见第七章的例子)。我们可以使用其他副本从损坏的数据中恢复,但是通过跨块比较副本以检测损坏是不明智的。此外不同的副本可能是合法的:GFS变化的语义,特别是前面讨论过的原子记录追加,不能保证副本完全相同。因此每个块服务必须通过维护校验和独立校验自身副本的准确性。
一个区块被分成64KB的blocks。每个有它对应的32bit的校验和。就像其他元数据一样,校验和在内存中并且被持久化到日志中,与用户数据分离。
为了读取,块服务会在返回任何数据给请求者之前,验证读取范围重叠的数据blocks的校验和,无论那个请求来自客户端还是另一个块服务。因此块服务不会讲损坏传播到其他机器。如果块服务不匹配记录的校验和,块服务像请求者报告一个错误并且将不匹配报告给主服务器。作为回答,请求者将会读取其他副本的数据,而主服务器将从另一个副本克隆数据块。在新的有效副本就位后,主服务名令那个上报错误的块服务删掉自身的副本。
校验和对于读取的性能有小幅度的影响。因为大多数的读会跨越几个块,我们需要读取对相对较少的额外数据进行校验和。GFS客户端代码进一步通过尝试在校验和block边界对齐读取减少了负载。在块服务上完成查找和比较而不是任何I/O,校验和计算通常可以与I/O操作重叠。
校验和计算在很大程度上优化了附加到块末尾的写操作(而不是覆盖现有数据的写操作),因为它们在我们的工作负载中占主导地位。我们只需要增量地更新最后一个部分校验和block的校验和,并为任何由追加部分填充的全新校验和块计算新的校验和。即便由于最后一个部分的校验和block已过时并且我们现在没有发现他,新的校验和值将不匹配存储的数据,并且响应的当下一次块被读取时,损坏会被和平时一样检测到。
相反,如果写操作覆盖了块的现有范围,我们必须读取和校验被覆盖范围的第一个和最后一个block,然后执行写操作,最终计算和记录新的校验和。如果我们在部分覆盖第一个和最后一个块之前不验证它们,新的校验和可能会隐藏存在于违背覆盖的区域中损坏。
在空闲期间,块服务可以扫描和校验非活动的块内容。这允许我们探测可读块中的损坏。一旦损坏被探测到,主可以创建未损坏的副本和删除对应的副本。这阻止了不活动但是损坏的块副本欺骗主,让主认为它有足够的有效块副本。
5.3诊断工具
广泛的和详细的诊断日志可以极大的帮助问题的隔离,debug,和性能分析,只花费很小的代价。除了日志很难去理解瞬时的非重复的在机器之间的问题。GFS服务生成诊断日志,日志记录了一些特殊的事件(块服务启动和结束)还有全部的RPC请求和回复。这个诊断日志可以自由的删除而不影响正确性。然而我们尝试保持这些日志在空间允许的范围内。
RPC日志包含了在网上准确的请求和响应。除了正在写入或读取的文件数据。根据匹配请求和回复,并整理不同机器上的RPC记录,我们可以重建整个互动历史以整段问题。日志还可以作为负载测试与性能分析的跟踪。
日志对于性能的是很小的(远远被好处超过)因为这个日志是被循序的写入而且是异步的。大部分内存的事件也保持在内存中可以进行连续的在线监测。
6测量
在这一张我们介绍一些小的基准测试以说明GFS架构和实现中的瓶颈,并且一些数字来自谷歌中真实的集群。
6.1 小基准测试
我们测量由一个主组成的GFS集群的性能,俩个主副本,16个块服务,16个客户端。记住这个配置是为了便于测试。通常集群有上百个块服务和上百个连接端。
所有的机器陪配置为一对Pentium III处理器,2GB的内存,2个80GB 5400RPM的磁盘,和100mbps的全双工以太网连接到HP2524交换机。所有19GFS服务是连接到一个交换机上的,所有的16个机器连接到另一个。俩个交换机通过1GBPS进行连接。
6.1.1 读
N客户端同时读取文件系统。每个客户端读在320GB的文件集中取随机选择的4MB个块。它重复256次,所以每个客户端最终读取1GB的数据。块服务加到一起一共只有32GB的内存,所以我们期望最大10%的linux缓冲缓存。我们的结果应该很接近冷缓存的返回。
图3:总吞吐量。最上面的曲线展示了网络拖布强加的理论上的限制。下面的曲线展示了测量到的吞吐量。它使用误差线来表示95%的置信区间, 在某些情况下这些误差线难以辨认,因为测量值差异非常小。
如图3所示n个客户端的总读取率。当1个Gbps的连接在俩个交换机之间饱和,极限峰值是1255MB/s。在100Mbps网络接口饱和时,12.5MB/s每个客户端。当只有一个客户端读取的时候,观察到的读取速率是10MB/s,或者一个客户端限制的80%。对于16个读取器,总读取率达到94MB/s,大概是125MB/s的70%,或者每个客户端6MB/s。因为读取器的增加数量,效能从80%下降到了75%。多个读取器同时从同一个chunkserver读取数据的概率也是如此。
6.1.2写入
N个客户端同时写入N个不同的文件。每个客户端在一系列1mb的写入中写入1GB的数据到新的文件。总写入率和理论限制如图3(b)所示。极限在67MB/s,因为我们需要写入每个字节写入16个块中的3个,每个有12.5MB/s的输入连接。
一个客户端的写入取率是6.3MB/s,大概是这个限制的一半。主要的原因是我们的网络栈。它不能很好地与我们用于将数据推送到块副本的管道方案交互。从一个副本传播数据到另一个副本的延迟降,降低了总写入率。
总写入率达到了35MB/s对于16个客户端(或2.2MB/s每个客户端),大概一半的理论限制。在读案例中,它变得更显著,多个客户端同时写一个块服务,也就是客户端数量增加。此外,16个协相对于16个读来说碰撞更加显著,因为每个读设计了三个副本。
写入操作比我们想要的要慢。事实上,这并不是一个大问题,因为即使它增加了单个客户机看到的延迟,它没有被系统的大规模的客户端数量显著的影响总写入带宽。
6.1.3记录追加
图3(c)展示了记录追加的性能。N个客户端同时追加到一个文件。性能被存储了最后的文件的块服务的网络带宽限制,而无关与客户端的数量。开始在6MB/s每个客户端,然后降低到了16个客户端4.8MB/s,主要是由于阻塞和不同客户端看到网络传输的差异。
我们的应用倾向于同时产生多个类似的文件。另一方面,N个客户端同时追加M个共享文件,其中N和M都是几十或者成百个。因此,快哦服务网络在我们的实验中拥堵在现实中不是一个显著的问题,因为客户端在写一个文件的同时,处理另一个文件的块服务正在忙着。
6.2真实集群
我们现在检查俩台机器在谷歌中的使用,这是其他几个像它一样的代表。集群A被100名工程师用于研究和开发。一个典型的任务是由人类用户发起的兵运行几个小时。它读取几MB到几TB的数据,变换和分析数据,将结果写回到集群。集群B主要用于生产环境数据处理。任务持续时间更长,持续的生成和处理几个TB的集合,只有偶尔的人为干预。在俩个例子中,数个进程由几个运行在数个机器上的数个进程组成,同时读写一些文件。
Cluster | A | B |
---|---|---|
块服务 | 342 | 227 |
可用磁盘空间 | 72TB | 180TB |
使用的磁盘空间 | 55TB | 155TB |
文件数量 | 735k | 737k |
死亡的文件数量 | 22k | 232k |
块数量 | 992k | 1550k |
块服务的元数据 | 13GB | 21GB |
主的元数据 | 48MB | 60MB |
6.2.1 存储
如表中前五个条目所示,俩个集群有上百个块服务,支持几TB的磁盘空间,并且相当满并不是完全满。可用空间包括了所有块副本。事实上所有的文件都被复制为三倍。因此集群分别存储了18TB和52TB的文件数据。
这俩个集群有相似的文件数量,虽然B中的死文件比例更大,即被删除或者被新版本取代的文件。但是它的存储仍未被回收。它仍占有很多块,因为它的文件往往会更大。
6.2.2 元数据
块服务在整个存储中往往存储了GB级别的元数据,大部分是64KB的用户数据block的校验和。在块服务上保存的唯一其他的元数据是4.5章中讨论的版本号。
元数据在主机上保持要小的多,通常只有几MB,平均每个文件100字节。这允许我们假设,实践中主服务器的内存通常不是限制。大多数每个文件的的元数据是以压缩前缀树形式存储的文件名,其他的元数据包括了文件持有者和权限,从文件到块的映射,每个文件的当前版本。此外对于每个块,我们存储当前副本位置以及一个引用计数,用于实现写时复制机制(copy-on-write)。
每个单独的服务的包括块服务与主,只有50-100的元数据。因此恢复是非常快的:在服务开始响应数据之前,它只需要几秒来从磁盘中读取元数据。然而主在一段时间内会有些受限,大概是30-60秒,直到它拿到了所有块服务中的块位置信息。
6.2.3存储和读取率
Cluster | A | B |
---|---|---|
Read rate (last minute) | ${583}\mathrm{{MB}}/\mathrm{s}$ | ${380}\mathrm{{MB}}/\mathrm{s}$ |
Read rate (last hour) | ${562}\mathrm{{MB}}/\mathrm{s}$ | ${384}\mathrm{{MB}}/\mathrm{s}$ |
Read rate (since restart) | ${589}\mathrm{{MB}}/\mathrm{s}$ | ${49}\mathrm{{MB}}/\mathrm{s}$ |
Write rate (last minute) | $1\mathrm{{MB}}/\mathrm{s}$ | ${101}\mathrm{{MB}}/\mathrm{s}$ |
Write rate (last hour) | $2\mathrm{{MB}}/\mathrm{s}$ | 117 MB/s |
Write rate (since restart) | ${25}\mathrm{{MB}}/\mathrm{s}$ | ${13}\mathrm{{MB}}/\mathrm{s}$ |
Master ops (last minute) | 325 Ops/s | 533 Ops/s |
Master ops (last hour) | 381 Ops/s | 518 Ops/s |
Master ops (since restart) | 202 Ops/s | 347 Ops/s |
表3:俩个GFS集群的性能度量
表3展示了读和写在不同时期的速率,当记录这些测量值时,俩个集群都已经启动一周了。(集群在最近重新启动,以升级到新版本的GFS)。
重启之后,平均的读取速率低于30MB/s,当我们进行这些测量,B正在进行大量的写操作,生成了大约100MB/s的数据,它产生了300MB/s的网络负载。因为写会被传播到三个副本。
读取速率远远高于写入速率。总的工作负载是由更多的读而不是写构成的,正如我们假设的那样。俩个集群都处于大量的读取活动之间。事实上,在上一周A一直维持580MB/s的读取率。它的网络配置可以支持7550MB/s,所以它对资源的利用率非常有效。集群B最高可以支持1300MB/s的读取率,但是应用只使用了380MB/s
6.2.4主负载
表3也展示了操作频率,大约每分钟发送给主200-500个操作。主可以轻松保持这个速率,因此不会在这个工作负载出现瓶颈。
在早期版本的GFS中,主偶尔会出现对于一些任务负载的瓶颈。它将大部分时间化在顺序扫描大型目录(其中包含数十万个文件)以查找特定的文件上。我们在之后修改了master的数据结构以允许通过明明空间进行更有效的二进制搜索。它现在可以秦松的支持接近每秒几千个文件的访问。这是必要的,我们可以通过将名称查找缓存在命名空间的数据结构之前来进一步提升速度。
6.2.5 恢复时间
在块服务故障之后,一些块服务奖会变的复制不足,必须克隆恢复其复制级别。恢复所有的块的时间依赖于资源的数量。在一个实验中,我们杀死了集群B中的单个块服务。集群有大约15000个块包含了600GB的数据。为了限制对运行中应用的影响以及为了调度决策提供退路,我们默认参数限制集群为91个并发的克隆(40%的块服务数量)其中每个克隆操作允许消耗最多6.25MB/s(50MBps)。所有的块在23.2分钟后恢复,有效复制速率为440MB/s。
在另一个实验中,我们杀死了俩个块服务,每个块服务大概有16000个块和660GB的数据。这个双重故障将226个块减少到了一个副本。这266个块将被高优先级的克隆,并且在俩分钟内恢复到至少2个副本,从而使集群处于允许其他块服务异常而不丢失的状态。
6.3工作负载崩溃
在这一章中,我们详细分析了的GFS上俩个集群工作负载崩溃,可以与6.2章节不完全的进行比较。集群X是用来研究和开发的集群,其中集群Y是用于生产数据处理。
关于IO的操作统计基于启发式地从GFS服务器记录实际RPC请求中重的信息。例如,GFS客户端代码可能会将一个读操作从我们指向的源头分成多个RPC来增加并行度。由于我们的访问模式非常有规律,我们预计任何误错误都会在误差范围内,可以视为噪声。应用程序的日志记录可能会提供更准确的数据,但是逻辑上不可能重新编译或重启上千个运行的客户端,这么多台机器上收集结果很麻烦。
我们应该注意,不要从我们的工作量中过度的泛化。由于Google完全控制着GFS和应用程序,应用通常倾向于为GFS进行调优,相反GFS是为了这些程序而设计的。这种相互的影响也会存在于一般应用程序和文件新系统之间,但在我们的案例中这个影响可能更为显著。
6.3.2块服务工作负载
Operation | Read | Write | Record Append | |||
---|---|---|---|---|---|---|
Cluster | X | Y | X | Y | X | Y |
0K | 0.4 | 2.6 | 0 | 0 | 0 | 0 |
1B..1K | 0.1 | 4.1 | 6.6 | 4.9 | 0.2 | 9.2 |
$1\mathrm{\;K}.{.8}\mathrm{\;K}$ | 65.2 | 38.5 | 0.4 | 1.0 | 18.9 | 15.2 |
$8\mathrm{K}.{.64}\mathrm{K}$ | 29.9 | 45.1 | 17.8 | 43.0 | 78.0 | 2.8 |
${64}\mathrm{K}$ .. ${128}\mathrm{K}$ | 0.1 | 0.7 | 2.3 | 1.9 | $< {.1}$ | 4.3 |
${128}\mathrm{K}$ .. ${256}\mathrm{K}$ | 0.2 | 0.3 | 31.6 | 0.4 | $< {.1}$ | 10.6 |
${256}\mathrm{K}$ .. ${512}\mathrm{K}$ | 0.1 | 0.1 | 4.2 | 7.7 | $< {.1}$ | 31.2 |
${512}\mathrm{K}.{.1}\mathrm{M}$ | 3.9 | 6.9 | 35.5 | 28.7 | 2.2 | 25.5 |
1M..inf | 0.1 | 1.8 | 1.5 | 12.3 | 0.7 | 2.2 |
表4:按大小划分的操作占比,为了阅读,大小是实际读取和传输的数量,而不是请求的量。
表4展示了按大小的操作分布。读取大小呈双峰分布。小的读取(小于64KB)来自搜索密集型客户端,它们查找大文件中的小块。大的读取(超过512KB)来自长顺序读取整个文件。
在集群Y中,大量的读取操作根本不返回任何数据。我们的应用,尤其是这些生产系统,通常使用文件作为生产消费序列。生产者同时追加文件,同时消费者同时读取文件的结尾。然而,当消费者超过生产者没有数据返回。集群X很少出现这种情况,通常因为它被用于短生命周期的数据分析任务而不是长生命周期的应用分布。
写的规模也展现出双峰分布。大的写(超过256KB)通常是由于写入器内部大量的缓冲造成的。写入器缓存较少数据,检查点或者同步更频繁,只需为较小的写入生成较少的数据帐户(低于64KB)。
如同记录追加,集群Y占用了更高的百分比要比集群X高得多,因为我们生产系统使用集群Y,它更为积极的调整GFS。
表5展示了在不同大小的操作中数据传输总数。对于所有类型的操作,大操作(超过 256KB)通常占传输的大部分字节。小的读(低于64KB)进行的传输很小,但是由于随机寻道的工作负载,确实传输了一小部分但很重要的读数据。
Operation | Read | Write | Record Append | |||
---|---|---|---|---|---|---|
Cluster | X | Y | X | Y | X | Y |
1B..1K | $< {.1}$ | $< {.1}$ | $< {.1}$ | $< {.1}$ | $< {.1}$ | $< {.1}$ |
$1\mathrm{\;K}.{.8}\mathrm{\;K}$ | 13.8 | 3.9 | $< {.1}$ | $< {.1}$ | $< {.1}$ | 0.1 |
$8\mathrm{K}.{.64}\mathrm{K}$ | 11.4 | 9.3 | 2.4 | 5.9 | 2.3 | 0.3 |
${64}\mathrm{K}.{.128}\mathrm{K}$ | 0.3 | 0.7 | 0.3 | 0.3 | 22.7 | 1.2 |
${128}\mathrm{K}$ .. ${256}\mathrm{K}$ | 0.8 | 0.6 | 16.5 | 0.2 | $< {.1}$ | 5.8 |
${256}\mathrm{K}$ .. ${512}\mathrm{K}$ | 1.4 | 0.3 | 3.4 | 7.7 | $< {.1}$ | 38.4 |
${512}\mathrm{K}..1\mathrm{M}$ | 65.9 | 55.1 | 74.1 | 58.0 | .1 | 46.8 |
1M..inf | 6.4 | 30.1 | 3.3 | 28.0 | 53.9 | 7.4 |
表5:传输的字节按照操作大小分类(%)。对于读取,大小是真是读取和传输的数据量,而不是请求量。如果读取超过了文件结尾,则俩者可能不一样,这在我们的工作中并不常见。
6.3.3 追加与写入
记录追加是被大量的使用,特别是我们生产系统。对于集群X,写入和追加的比例按传输字节数是108:1,按操作数量是8:1.对于集群Y,被用作生产系统,比例分别是3.7:1和2.5:1。此外,这个比例表明,俩个集群记录追加通常是大于写的。然本人对于集群X,在测量期间记录追加总体的使用情况是相当的低,因此,结果可能会受到一俩个应用程序和特地额的缓存大小选择的影响。
和预期一样,我们的数据追加变化的工作负载显著超过重写。我们测量了primary副本上的数据重写量。这近似于实际情况,客户端故意重写之间写入的数据而不是追加新数据。对于集群X,重写占改变的字节数不到0.0001,占改变的操作数不到0.0003%个。对于Y集群,俩个比例都是0.05。虽然这个数字很小,但是仍比我们的预期要搞。这表明,大部分的重写来自错误或者超时导致客户端的重试。本质上不是工作负载的一部分,而是重试机制的结果
6.3.4 主负载
Cluster | XY |
---|---|
打开 | 26.116.3 |
删除 | 0.71.5 |
文件定位 | 64.365.8 |
文件租约持有 | 7.813.4 |
查找匹配文件 | 0.62.2 |
所有其他组合 | 0.50.8 |
表6:主请求按照类型分类
表展示到主的请求按照类型分类。大部分的请求是询问区块的位置(查找定位)以读取和持有租约信息以进行数据变动。
集群X和Y的删除请求数量有显著的不同,因为集群Y存储了生产数据,它门会定期更新并被更新版本所取代。其中的一些差异进一步的隐藏在open请求中,因为老版本的文件可能回通过从头打开而被写入而被隐式删除(模式“w”在Unix开放术语)
查找匹配文件是一种匹配请求的模式,它支持‘is’或者类似文件系统的操作。不像其他的对于主的请求,它可能会处理命名空间中很大的一部分,所以可能会很高昂。集群Y看到它的频率更高,因为自动的数据处理任务通常检查文件系统的一部分以理解整个文件系统的状态。相反的,集群X的应用处于更明确的用户控制之下,通常提前知道所有需要的文件的名称。
7经历
在发布和构建GFS的过程中,我们经历了非常多的问题,一些是操作性的,一些是技术性的。
最开始,GFS被设想为我们生产环境的后端文件系统。久而久之,它的使用逐渐包括了研究和开发任务。它开始支持一些类似权限和配额的事情,但是现在也包括了这些基本的形式。同时山产系统也非常有序受控,用户有时候确没有。需要更底层的结构来保持用户之间不会彼此干扰。
一些大问题是磁盘和其中linux相关的。我们的许多磁盘都向Linux驱动程序声称它们支持一系列IDE协议版本,但实际上它们只对最新版本做出可靠的响应。由于协议版本非常相似,这些驱动大部分工作正常,但是偶尔的不匹配可能导致驱动和内核的状态不一致。由于这些问题是在内核中,它会慢慢的破坏数据。这个问题驱动了我们使用校验和来探测数据损坏,同时我们改进了内核去处理这些协议不匹配。
早起由于fsync()函数的开销我们在linux2.2内核中有一些问题。它的成本与文件大小成比例而不是与修改部分的大小。在我们执行检查点之前操作大型日志的时候,这个问题尤为显著。我们通过同步写和最终迁移到Linux2.4解决了这个问题。
另一个Linux问题是单个读-写锁,地址空间中的任何线程在从磁盘进行分页(读锁)或在mmap()调用中修改地址空间(写锁)时都必须持有该锁。我们看到了轻负载下我们系统出现了短时的超时,并努力的寻找资源瓶颈或者硬件故障。我们发现,当磁盘线程分页以前映射的数据时,这个锁会阻止主网络线程将新数据映射到内存中。所以我们主要根据网络接口进行限制而不是根据内存复制带宽进行限制,我们通过将mmap()替换为pread()来解决这个问题,但需要额外的副本。
尽管偶尔会出现问题,Linux代码的可用性一次又一次地帮助我们探索和理解系统行为。在适当的时候我们会改进内核并与开源社区共享更改。
8相关工作
就像其他的大型分布式系统比如AFS,GFS提供了独立于位置的命名空间,它允许透明移动数据以实现负载均衡与容错。不像AFS,GFS扩展了存储服务之间的文件数据,这种方式类似XFS和Swift,已提供聚合性能和强大的容错。
因为磁盘相对便宜,复制也相对简单于精密的RAID方式,GFS现在只使用几个副本进行荣誉,因此比xFS或Swift消耗更多的原始存储。
作为与 systems like AFS, xFS, Frangipani ,Intermezzo 的差异。GFS不提供任何在文件系统接口下的缓存。我们的目标工作负载在单个运行的服务中几乎没有重用,它要么顺序访问大的数据集,要么随机查找,每次读取少量数据。
一些分布式系统比如 Frangipani, xFS, Minnesota's GFS,GPFS 删除了中心式服务以来分布式算法进行一致性和管理。我选择中心化方式以简化设计,增加可用性和获得弹性。实践中,中心化的主让他更简单的执行精密的块防止和复制策略,因为主持有大部分相关的信息,并控制如何交换。我们保持住的状态最小和在另一台机器上的完全复制,来解决容错问题。可扩展性和高可用性(对于读)在当前通过影子主进行提供。更新主的状态是通过追加提前写入的日志来持久化的。
我们解决问题类似Lustre ,在向大量客户端交付聚合性能方面。然而,我们通过聚焦我们应用的需要显著简化了问题,而不是构建符合poxis标准的文件系统。此外,GFS假设大量不可靠的部分所以容错在我们的设计中是一个重点。
GFS最接近NASD 的架构。NASD 架构是基于网络连接磁盘驱动的。GFS使用了通用机器作为块服务,并且完善了NASD 的原型。不像NASD 的工作,我们的块服务使用惰性分配块大小而不是,变长的对象。此外GFS实现了再平衡,复制和恢复的特性,他们在生产环境的系统中是必须的。
与明尼苏达州的GFS和NASD不同,我们并不寻求改变存储设备的模型。我们关注与处理日常数据处理,需要复杂的分布式系统和现有的通用组件一起。
生产消费队列通过原子记录追加解决了River 中分布式队列相似的问题。而River使用分布在机器上的基于内存的队列和仔细的数据流控制,GFS使用了持久化文件它可以通过多个生产者进行并发追加。River 模式支持m对n个分布式队列,但是缺乏容错机制来持久化数据,其中GFS只支持m对1个高效队列。多个消费者可以读取相同的文件,但是只能协调传入的负载。
9总结
谷歌文件系统展示了在通用硬件上支持大规模的数据处理工作负载所必须的质量。
我们首先根据我们的环境和预期的应用工作负载和技术环境重新评估传统的文件系统假设。我们的观察导致了在设计空间中根本的不同点。我们将组件规章视为正常而不是意外,优化那些主要是追加(可能并发)然后读取(通常顺序)的大文件,延展和放宽文件系统的标准以提升整个系统。
我们系统通过持续监控,复制关键数据和快速自动恢复提供了容错,块副本允许我们容忍块服务异常。这种频繁的异常促使了一种新的机制,它定期和透明的修理损坏,以平滑的方式补偿丢失的副本。此外,我们使用校验和探测磁盘或者IDE 子系统级别的数据正确性,考虑到系统中的磁盘数量,这种情况变得非常常见。
我们设计提供高聚合吞吐量以便并发读取器和写入器执行各种任务。我们通过使用主服务器分离文件系统控制,直接通过块服务和客户端之间传输来。通过大块的大小和块的租期,将主操作的参与最小化,它授权primary 副本进行数据变化。这使得一个简单的、集中的主机成为可能,而不会成为瓶颈。我们相信他能增强我们的网络占,将接触当前在在单个客户端看到的写吞吐量上的限制。
GFS成功的应对我们的存储需求并被广泛的使用在谷歌的内部存储平台,提供研究开发以及生产的数据处理。这是一个重要的工具能够让我们继续创新和应对在整个网络上的规模问题。