THE Google File System2009
摘要
我们设计并实现了Google GFS文件系统,一个面向大规模数据密集型应用的、可伸缩的分布式文件系统。GFS虽然运行在廉价的普遍硬件设备上,但是它依然了提供灾难冗余的能力,为大量客户机提供了高性能的服务。
虽然GFS的设计目标与许多传统的分布式文件系统有很多相同之处,但是,我们的设计还是以我们对自己的应用的负载情况和技术环境的分析为基础的,不管现在还是将来,GFS和早期的分布式文件系统的设想都有明显的不同。所以我们重新审视了传统文件系统在设计上的折衷选择,衍生出了完全不同的设计思路。
GFS完全满足了我们对存储的需求。GFS作为存储平台已经被广泛的部署在Google内部,存储我们的服务产生和处理的数据,同时还用于那些需要大规模数据集的研究和开发工作。目前为止,最大的一个集群利用数千台机器的数千个硬盘,提供了数百TB的存储空间,同时为数百个客户机服务。
在本论文中,我们展示了能够支持分布式应用的文件系统接口的扩展,讨论我们设计的许多方面,最后列出了小规模性能测试以及真实生产系统中性能相关数据。
分类和主题描述
D [4]: 3—D分布文件系统
常用术语
设计,可靠性,性能,测量
关键词
容错,可伸缩性,数据存储,集群存储
1简介
为了满足Google迅速增长的数据处理需求,我们设计并实现了Google文件系统(Google File System –GFS)。GFS与传统的分布式文件系统有着很多相同的设计目标,比如,性能、可伸缩性、可靠性以及可用性。但是,我们的设计还基于我们对我们自己的应用的负载情况和技术环境的观察的影响,不管现在还是将来,GFS和早期文件系统的假设都有明显的不同。所以我们重新审视了传统文件系统在设计上的折衷选择,衍生出了完全不同的设计思路。
**首先,组件失效被认为是常态事件,而不是意外事件。**GFS包括几百甚至几千台普通的廉价设备组装的存储机器,同时被相当数量的客户机访问。GFS组件的数量和质量导致在事实上,任何给定时间内都有可能发生某些组件无法工作,某些组件无法从它们目前的失效状态中恢复。我们遇到过各种各样的问题,比如应用程序bug、操作系统的bug、人为失误,甚至还有硬盘、内存、连接器、网络以及电源失效等造成的问题。所以,持续的监控、错误侦测、灾难冗余以及自动恢复的机制必须集成在GFS中。
其次,以通常的标准衡量,我们的文件非常巨大。数GB的文件非常普遍。每个文件通常都包含许多应用程序对象,比如web文档。当我们经常需要处理快速增长的、并且由数亿个对象构成的、数以TB的数据集时,采用管理数亿个KB大小的小文件的方式是非常不明智的,尽管有些文件系统支持这样的管理方式。因此,设计的假设条件和参数,比如I/O操作和Block的尺寸都需要重新考虑。
第三,**绝大部分文件的修改是采用在文件尾部追加数据(append),而不是覆盖原有数据的方式。**对文件的随机写入操作在实际中几乎不存在。**被写入时,对文件的操作就只有读,而且通常是按顺序读。**很多数据场景都符合这些特征。比如文件组成大型的库,使用数据分析程序对其扫描。比如由运行中的程序持续生成的数据流。比如归档数据。还可能是分布式计算的中间结果,在一台机器上产生、然后在另一台处理。这些数据场景都是由制造者持续增量的产生新数据,再由消费者读取处理。在这种模式下append是性能优化和保证原子性的焦点。而在客户端缓存数据块没有太大意义。
第四,向应用提供类似文件系统API。我们放松了对GFS一致性模型的要求,这样就减轻了文件系统对应用程序的苛刻要求,大大简化了GFS的设计。我们引入了原子性的记录追加(append)操作,从而保证多个客户端能够同时进行追加操作,不需要额外的同步操作来保证数据的一致性。本文后面还有对这些问题的细节的详细讨论。
Google已经针对不同的应用部署了多套GFS集群。最大的一个集群拥有超过1000个存储节点,超过300TB的硬盘空间,被不同机器上的数百个客户端连续不断的频繁访问。
2.设计概述
2.1设计预期
在设计满足我们需求的文件系统时候,我们的设计目标既有机会、又有挑战。之前我们已经提到了一些需要关注的关键点,这里我们将设计的预期目标的细节展开讨论。
- 系统由许多廉价的普通组件组成,组件失效是一种常态。系统必须持续监控自身的状态,它必须将组件失效作为一种常态,能够迅速地侦测、冗余并恢复失效的组件。
- 系统存储一定数量的大文件。我们预期会有几百万文件,文件的大小通常在100MB或者以上。数个GB大小的文件也是普遍存在,并且要能够被有效的管理。系统也必须支持小文件,但是不需要针对小文件做专门的优化。
- 系统的工作负载主要由两种读操作组成:**大规模的流式读取和小规模的随机读取。**大规模的流式读取通常一次读取数百KB的数据,更常见的是一次读取1MB甚至更多的数据。来自同一个客户机的连续操作通常是读取同一个文件中连续的一个区域。小规模的随机读取通常是在文件某个随机的位置读取几个KB数据。如果应用程序对性能非常关注,通常的做法是把小规模的随机读取操作合并并排序,之后按顺序批量读取,这样就避免了在文件中前后来回的移动读取位置。
- 系统的工作负载还包括许多大规模的、顺序的、数据追加方式的写操作。一般情况下,每次写入的数据的大小和大规模读类似。数据一旦被写入后,文件就很少会被修改了。系统支持小规模的随机位置写入操作,但是可能效率不彰。
- 系统必须高效的、行为定义明确的实现多客户端并行追加数据到同一个文件里的语意。我们的**文件通常被用于”生产者-消费者“队列,或者其它多路文件合并操作。通常会有数百个生产者,每个生产者进程运行在一台机器上,同时对一个文件进行追加操作。使用最小的同步开销来实现的原子的多路追加数据操作是必不可少的。**文件可以在稍后读取,或者是消费者在追加的操作的同时读取文件。
- 高性能的稳定网络带宽远比低延迟重要。我们的目标程序绝大部分要求能够高速率的、大批量的处理数据,极少有程序对单一的读写操作有严格的响应时间要求。
2.2 接口
GFS提供了一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式实现的。文件以分层目录的形式组织,用路径名来标识。我们支持常用的操作,如创建新文件、删除文件、打开文件、关闭文件、读和写文件。
另外,GFS提供了快照和记录追加(record append)操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子性的。这对于实现多路结果合并,以及”生产者-消费者”队列非常有用,多个客户端可以在不需要额外的同步锁定的情况下,同时对一个文件追加数据。我们发现这些类型的文件对于构建大型分布应用是非常重要的。快照和记录追加操作将在3.4和3.3节分别讨论。
2.3 架构
一个GFS集群包含一个单独的Master节点(译注:这里的一个单独的Master节点的含义是GFS系统中只存在一个逻辑上的Master组件。后面我们还会提到Master节点复制,因此,为了理解方便,我们把Master节点视为一个逻辑上的概念,一个逻辑的Master节点包括两台物理主机,即两台Master服务器)、多台Chunk服务器,并且同时被多个客户端访问,如图1所示。所有的这些机器通常都是普通的Linux机器,运行着用户级别(user-level)的服务进程。我们可以很容易的把Chunk服务器和客户端都放在同一台机器上,前提是机器资源允许,并且我们能够接受不可靠的应用程序代码带来的稳定性降低的风险。
GFS存储的文件都被分割成固定大小的Chunk。在Chunk创建的时候,**Master服务器会给每个Chunk分配一个不变的、全球唯一的64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。**出于可靠性的考虑,每个块都会复制到多个块服务器上。缺省情况下,我们使用3个存储复制节点,不过用户可以为不同的文件命名空间设定不同的复制级别。
Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射信息、以及当前Chunk的位置信息。
Master节点还管理着系统范围内的活动,比如,Chunk租用管理、孤儿Chunk(注:orphaned chunks)的回收、以及Chunk在Chunk服务器之间的迁移。
Master节点使用心跳信息周期地和每个Chunk服务器通讯,发送指令到各个Chunk服务器并接收Chunk服务器的状态信息。
GFS客户端代码以库的形式被链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程序与Master节点和Chunk服务器通讯、以及对数据进行读写操作。客户端和Master节点的通信只获取元数据,所有的数据操作都是由客户端直接和Chunk服务器进行交互的。我们不提供POSIX标准的API的功能,因此,GFS API调用不需要深入到Linux vnode级别。
无论是客户端还是Chunk服务器都不需要缓存文件数据。客户端缓存数据几乎没有什么用处,因为大部分程序要么以流的方式读取一个巨大文件,要么工作集太大根本无法被缓存。无需考虑缓存相关的问题也简化了客户端和整个系统的设计和实现。(不过,客户端会缓存元数据。)Chunk服务器不需要缓存文件数据的原因是,Chunk以本地文件的方式保存,Linux操作系统的文件系统缓存会把经常访问的数据缓存在内存中。
2.4 单一Master节点
单一的Master节点的策略大大简化了我们的设计。单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。另外,我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。
客户端并不通过Master节点读写文件数据。反之,客户端向Master节点询问它应该联系的Chunk服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和Chunk服务器进行数据读写操作。
我们利用图1解释一下一次简单读取的流程。
-
首先,客户端把文件名和程序指定的字节偏移,根据固定的Chunk大小,转换成文件的Chunk索引。
-
然后,它把文件名和Chunk索引发送给Master节点。
-
Master节点将相应的Chunk标识和副本的位置信息发还给客户端。
-
客户端用文件名和Chunk索引作为key缓存这些信息。
-
之后客户端发送请求到其中的一个副本处,一般会选择最近的。
-
请求信息包含了Chunk的标识和字节范围。
-
在对这个Chunk的后续读取操作中,客户端不必再和Master节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。
实际上,客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应也可能包含了紧跟着这些被请求的Chunk后面的Chunk的信息。
在实际应用中,这些额外的信息在没有任何代价的情况下,避免了客户端和Master节点未来可能会发生的几次通讯。
2.5 Chunk尺寸
Chunk的大小是关键的设计参数之一。
我们选择了64MB,这个尺寸远远大于一般文件系统的Blocksize。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。
惰性空间分配策略避免了因内部碎片造成的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议一点。选择较大的Chunk尺寸有几个重要的优点。
- 首先,它减少了客户端和Master节点通讯的需求,因为只需要一次和Mater节点的通信就可以获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。这种方式对降低我们的工作负载来说效果显著,因为我们的应用程序通常是连续读写大文件。即使是小规模的随机读取,采用较大的Chunk尺寸也带来明显的好处,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信息。
- 其次,采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。
- 第三,选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。这就允许我们把元数据全部放在内存中,在2.6.1节我们会讨论元数据全部放在内存中带来的额外的好处。
另一方面,即使配合惰性空间分配,采用较大的Chunk尺寸也有其缺陷。
小文件包含较少的Chunk,甚至只有一个Chunk。当有许多的客户端对同一个小文件进行多次的访问时,存储这些Chunk的Chunk服务器就会变成热点。
在实际应用中,由于我们的程序通常是连续的读取包含多个Chunk的大文件,热点还不是主要的问题。
然而,当我们第一次把GFS用于批处理队列系统的时候,热点的问题还是产生了:一个可执行文件在GFS上保存为single-chunk文件,之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个Chunk服务器被数百个客户端的并发请求访问导致系统局部过载。
我们通过使用更大的复制参数来保存可执行文件,以及错开批处理队列系统程序的启动时间的方法解决了这个问题。一个可能的长效解决方案是,在这种的情况下,允许客户端从其它客户端读取数据。
2.6 元数据
Master服务器(注:注意逻辑的Master节点和物理的Master服务器的区别。后续我们谈的是每个Master服务器的行为,如存储、内存等等,因此我们将全部使用物理名称)存储3种主要类型的元数据,包括:
- 文件和Chunk的命名空间
- 文件和Chunk的对应关系
- 每个Chunk副本的存放地点
所有的元数据都保存在Master服务器的内存中。
前两种类型的元数据(命名空间、文件和Chunk的对应关系)同时也会以记录变更日志的方式记录在操作系统的系统日志文件中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master服务器上。
采用保存变更日志的方式,我们能够简单可靠的更新Master服务器的状态,并且不用担心Master服务器崩溃导致数据不一致的风险。Master服务器不会持久保存Chunk位置信息。Master服务器在启动时,或者有新的Chunk服务器加入时,向各个Chunk服务器轮询它们所存储的Chunk的信息。
2.6.1 内存中的数据结构
因为元数据保存在内存中,所以Master服务器的操作速度非常快。
并且,Master服务器可以在后台简单而高效的周期性扫描自己保存的全部状态信息。
这种周期性的状态扫描也用于实现
- Chunk垃圾收集
- 在Chunk服务器失效的时重新复制数据通过Chunk的迁移实现
- 跨Chunk服务器的负载均衡
- 磁盘使用状况统计等功能
4.3和4.4章节将深入讨论这些行为。将元数据全部保存在内存中的方法有潜在问题:
Chunk的数量以及整个系统的承载能力都受限于Master服务器所拥有的内存大小。
但是在实际应用中,这并不是一个严重的问题。Master服务器只需要不到64个字节的元数据就能够管理一个64MB的Chunk。由于大多数文件都包含多个Chunk,因此绝大多数Chunk都是满的,除了文件的最后一个Chunk是部分填充的。同样的,每个文件的在命名空间中的数据大小通常在64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。即便是需要支持更大的文件系统,为Master服务器增加额外内存的费用是很少的,而通过增加有限的费用,我们就能够把元数据全部保存在内存里,增强了系统的简洁性、可靠性、高性能和灵活性。
2.6.2 Chunk位置信息
Master服务器并不保存持久化保存哪个