http://static.googleusercontent.com/media/research.google.com/zh-CN/us/archive/gfs-sosp2003.pdf
翻译版:
https://blog.csdn.net/xiaotom5/article/details/8142728
摘要
1. 简介
1.首先,组件失效被认为是常态事件,而不是意外事件。GFS包括几百甚至几千台普通的廉价设备组装的存储机器,同时被相当数量的客户机访问。 GFS组件的数量和质量导致在事实上,任何给定时间内都有可能发生某些组件无法工作,某些组件无法从它们目前的失效状态中恢复。我们遇到过各种各样的问 题,比如应用程序bug、操作系统的bug、人为失误,甚至还有硬盘、内存、连接器、网络以及电源失效等造成的问题。所以,持续的监控、错误侦测、灾难冗 余以及自动恢复的机制必须集成在GFS中。
2.其次,以通常的标准衡量,我们的文件非常巨大。数GB的文件非常普遍。每个文件通常都包含许多应用程序对象,比如web文档。当我们经常需要处 理快速增长的、并且由数亿个对象构成的、数以TB的数据集时,采用管理数亿个KB大小的小文件的方式是非常不明智的,尽管有些文件系统支持这样的管理方 式。因此,设计的假设条件和参数,比如I/O操作和Block的尺寸都需要重新考虑。
3.第三,绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式。对文件的随机写入操作在实际中几乎不存在。一旦写完之后,对文 件的操作就只有读,而且通常是按顺序读。大量的数据符合这些特性。
4.第四,应用程序和文件系统API的协同设计提高了整个系统的灵活性。比如:
a.我们放松了对GFS一致性模型的要求,这样就减轻了文件系统对应用程 序的苛刻要求,大大简化了GFS的设计。
b.我们引入了原子性的记录追加操作,从而保证多个客户端能够同时进行追加操作,不需要额外的同步操作来保证数据的一 致性。
2.设计概述
2.1设计预期
在设计满足我们需求的文件系统时候,我们的设计目标既有机会、又有挑战。之前我们已经提到了一些需要关注的关键点,这里我们将设计的预期目标的细节展开讨论。
· 1.系统由许多廉价的普通组件组成,组件失效是一种常态。系统必须持续监控自身的状态,它必须将组件失效作为一种常态,能够迅速地侦测、冗余并恢复失效的组件。
· 2.系统存储一定数量的大文件。我们预期会有几百万文件,文件的大小通常在100MB或者以上。数个GB大小的文件也是普遍存在,并且要能够被有效的管理。系统也必须支持小文件,但是不需要针对小文件做专门的优化。
· 3.两种读操作:系统的工作负载主要由两种读操作组成:大规模的流式读取和小规模的随机读取。a.大规模的流式读取通常一次读取数百KB的数据,更常见的是一次读取 1MB甚至更多的数据。来自同一个客户机的连续操作通常是读取同一个文件中连续的一个区域。
b.小规模的随机读取通常是在文件某个随机的位置读取几个KB数 据。如果应用程序对性能非常关注,通常的做法是把小规模的随机读取操作合并并排序,之后按顺序批量读取,这样就避免了在文件中前后来回的移动读取位置。
· 4.写操作:系统的工作负载还包括许多大规模的、顺序的、数据追加方式的写操作。一般情况下,每次写入的数据的大小和大规模读类似。数据一旦被写入后,文件就很少会被修改了。系统支持小规模的随机位置写入操作,但是效率不好。
· 5.并发写入时的原子性:系统必须实现多客户端并行追加数据到同一个文件里的原子性。
我们的文件通常被用于”生产者-消费者“队列,或者其它多路文件合并操作。通常会有数百个生产者,每个生产者进 程运行在一台机器上,同时对一个文件进行追加操作。使用最小的同步开销来实现的原子的多路追加数据操作是必不可少的。文件可以在稍后读取,也可以在追加的同时读取文件。
· 6.高性能的稳定网络带宽>低延迟。我们的目标程序绝大部分要求能够高速率的、大批量的处理数据,而对每个请求的响应时间延迟不作要求。
2.2 接口
一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式实现的。文件以分层目录的形式组织,用路径名来标识。我们支持常用的操作,如创建新文件、删除文件、打开文件、关闭文件、读和写文件。
快照和记录追加操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追加操作,同时保证每个客户端的追加操作都是原子性的。
2.3 架构
一个GFS集群包含一个单独的Master节点 、多台 Chunk服务器,并且同时被多个客户端访问,如图1所示。所有的这些机器通常都是普通的Linux机器。可以把Chunk服务器和客户端都放在同一台机器上,只要资源够,并且客户端代码没bug。
G存储的文件被分割成固定大小的Chunk。在Chunk创建的时候,Master服务器会给每个Chunk分配一个不变的、全球唯一的 64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。 为了保证可靠性,每个块都会复制到多个Chunk服务器上。缺省情况下,我们使用3副本,不过用户可以为不同的文件命名空间设定不同的副本数。
Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的mapping、以及当前Chunk的Location。Master节点还管理着系统范围内的活动,比如,Chunk租用管理、orphaned chunks的回收、以及Chunk在ChunkServer之间的迁移。Master节点使用心跳信息周期地和每个ChunkServer通讯,发送指令到各个ChunkServer并接收Chunk服务器的状态信息。
client以库的形式被链接到客户程序里。client实现了GFS文件系统的API接口函数,它代表应用程序与Master节点和ChunkServer通讯、以及对数据进行读写操作。client和Master节点的通信只获取元数据,所有的数据操作都是由client直接和ChunkServer进行交互的。
无论是客户端还是ChunkServer都不需要在内存中cache文件数据。客户端cache数据没有什么用,因为大部分程序要么以流的方式读取一个巨大文件,要么工作集太大根本无法被缓存。无需考虑缓存相关的问题也简化了客户端和整个系统的设计和实现。
不过,客户端会缓存metadata。
Chunk以本地文件的方式保存,Linux操作系统的文件系统缓存会把经常访问的数据缓存在内存中。因此ChunkServer也就不需要自己cache文件数据。
2.4 单一Master节点
单一的Master节点的策略大大简化了我们的设计。单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。
同时,我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。客户端只向 Master节点询问它应该访问的是哪个ChunkServer。客户端将这些元数据信息缓存一段时间,后续的操作将直接和ChunkServer进行数据读写操作。
我们利用图1解释一下一次简单读取的流程:
1.首先,客户端根据app传入的file name和byte offset,根据每个Chunk设置的大小,转换成file name和Chunk index(就把byte offset%chunk size)。然后,它把文件名和Chunk index发送给Master节点。Master节点将相应的Chunk handle(标识)和replicas的位置信息发还给客户端。客户端用file name+Chunk index作为key缓存这些信息:
<file name+Chunk index, Chunk handle + replica locations>
之后客户端发送请求到其中的一个副本处,一般会选择最近的。请求信息包含了Chunk handle和byte range。在对这个Chunk的后续读取操作中, 客户端不必再和Master通讯了,除非缓存的metadata过期或者文件被重新打开。实际上,客户端通常会在一次请求中查询多个Chunk信息,Master节点的回应包含了被请求的这个Chunk后面的其他的Chunk的信息。节省了客户端和Master的通信次数。
2.5 Chunk尺寸
Chunk的大小是关键的设计参数之一。我们选择了64MB,这个尺寸远远大于一般文件系统的Block size。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成 的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议一点??????。
选择较大的Chunk尺寸有几个重要的优点(因为chunk大,所以chunk少,所以请求基本上都在那一两个chunk):
1.首先,它减少了客户端和Master节点通讯的次数,因为只需要一次和Mater的通信就可以获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。这样大大降低了工作负载,因为我们的应用程序通常是连续读写大文件。而对于小规模的随机读取来说,采用较大的Chunk尺寸也有明显的好处:客户端可以轻松的缓存一个TB数据量的所有的Chunk位置信息。
2.其次,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。
3.第三,选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。这就可以把元数据全部放在内存中。
另一方面,采用较大的Chunk尺寸也有其缺点:
小文件包含较少的Chunk,甚至只有一个Chunk。当有许多的客户端对同一个小文件进行多次的访问时,存储这些Chunk的Chunk服务器就会变成热点。在实际应用中,由于我们的程序通常是连续的读取包含多个Chunk的大文件,因此还没又出现过这种热点问题。
然而,当我们第一次把GFS用于批处理队列系统的时候,还是产生了热点问题:一个可执行文件在GFS上保存为single-chunk文件, 之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个Chunk服务器被数百个客户端的并发请求访问导致系统局部过载。
这个问题可以通过如下2个方法来解决:
1.使用更多的复制副本来保存可执行文件
2.错开批处理队列系统程序的启动时间的方法
可能的长效解决方案是:允许客户端从其它客户端读取数据。
2.6 元数据
Master服务器存储3种主要的metadata,所有的metadata都保存在Master的内存中:
1.文件和Chunk的命名空间
2.文件到Chunk的映射关系
3.每个Chunk replica的Location
前两种metadata会以记录变更的方式存放在operation log文件中持久化。operation log存储在本地磁盘上,同时该日志会被复制到其它的远程Master服务器上。用operation log能够简单可靠的更新Master服务器的状态,不用担心Master服务器崩溃导致数据不一致的风险。
第三种metadata不会持久化保存。而是Master服务器在启动时,或者有新的Chunk服务器加入时,向各个ChunkServer轮询它们所存储的Chunk的信息。
2.6.1 元数据缓存在内存中
优点:
因为元数据保存在Master内存中,所以Master服务器的操作速度非常快。并且,Master服务器可以在后台简单而高效的周期性扫描自己保存的全部状态信息。
这种周期性的状态扫描便于Master实现如下功能:Chunk垃圾收集、当Chunk服务器宕机时在其他Server上重新复制数据、Chunk迁移以实现跨Chunk服务器的负载均衡以及磁盘使用状况统计等功能。
缺点:
Chunk的数量以及整个系统的承载能力都受限于Master服务器所拥有的内存大小。但是在实际应用中,这并不是一个严重的问题。Master服务器只需要不到64个字节的元数据就能够管理一个64MB的Chunk。由于大多数文件都包含多个 Chunk,因此绝大多数Chunk都是满的,除了文件的最后一个Chunk是部分填充的。同样的,每个文件的在命名空间中的数据大小通常在64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。
也就是说,只需要多一点内存,一点点钱,就能支持更大的文件系统。
2.6.2 Chunk的Location
Master服务器并不持久化保存哪个Chunk服务器存有指定Chunk的副本的信息。Master服务器只是在启动的时候轮询Chunk服务器以获取这些信息。Master服务器能够保证它持有的信息始终是最新的,因为它控制了所有的Chunk位置的分配,并且通过周期性的心跳信息监控 Chunk服务器的状态。
这种设计简化了在有Chunk服务器加入集群、离开集群、更名、失效、以及重启的时候,Master服务器和Chunk服务器数据同步的问题。在一个拥有数百台服务器的集群中,这类事件会频繁的发生。
可以从另外一个角度去理解这个设计决策:只有Chunk服务器才能最终确定一个Chunk是否在它的硬盘上。我们从没有考虑过在Master服务器 上维护一个这些信息的全局视图,因为Chunk服务器的错误可能会导致Chunk自动消失(比如,硬盘损坏了或者无法访问了),亦或者操作人员可能会重命名一个Chunk服务器。??????
2.6.3 operation log
operation log记录了元数据的变更历史。它很重要,不仅仅是因为operation log是元数据唯一的持久化存储记录,它也作为判断同步操作顺序的逻辑时间基线(也就是通过日志的序号作为操作发生的逻辑时间,类似于事务系统中的LSN)。文件和Chunk以及它们的版本,都由它们被创建时的逻辑时间唯一地、永久的标识。
operation log非常重要,因此我们必须确保它的可靠性,确保只有在元数据的变化被持久化后,它才对client可见。否则,即使Chunk本身没有出现任何问题,我们仍有可能丢失整个文件系统,或者丢失客户端最近的操作???。所以,我们会把日志复制到多台远程机器,并且只有把相应的operation log写入到本地以及远程机器的硬盘后,才会响应客户端的操作请求。Master服务器会攒多个log records后批量处理,以减少写入磁盘和复制对系统整体性能的影响。
Master服务器在启动时,通过replay operation log把文件系统恢复到最近的状态。为了缩短Master启动的时间,我们必须使该日志文件足够小。Master服务器在日志增长到一定量时对Master内存中的元数据做一次Checkpoint,将所有的状态数据写入一个Checkpoint文件。 在启动时,Master服务器就从磁盘上读取这个Checkpoint文件,以及replay 打完Checkpoint之后的少量operation log records就能够恢复系统。Checkpoint文件以压缩B-树的形式存储,可以直接映射到内存,在用于命名空间查询时无需额外的解析。
由于创建一个Checkpoint文件需要一定的时间,所以Master服务器的内部状态被组织为一种格式,这种格式要确保在Checkpoint 过程中不会阻塞正在进行的修改操作。Master服务器使用独立的2个线程分别做如下2件事情:
1.切换到新的operation log文件
2.创建新的Checkpoint文件。
新的Checkpoint 文件包括切换log前所有的修改。对于一个包含数百万个文件的集群,创建一个Checkpoint文件需要1分钟左右的时间。创建完成后,Checkpoint 文件会被写入在本地和远程的硬盘里。
Master服务器恢复只需要最新的Checkpoint文件和后续的operation log文件。旧的Checkpoint文件和operation log文件可以被删除,但是为了应对灾难性的故障,我们通常会多保存一些历史文件。Checkpoint失败不会对正确性产生任何影响,因为启动时在内存中恢复元数据的代码可以检测并跳过没有完成的Checkpoint文件。
2.7 一致性模型
GFS支持一个宽松的一致性模型,这个模型能够很好的支撑我们的高度分布的应用,同时还保持了相对简单且容易实现的优点。
2.7.1 GFS的保障
文件命名空间的修改(例如,文件创建)是原子性的。它们仅由Master节点的控制:命名空间锁提供了原子性和正确性(4.1章)的保障;Master节点的操作日志定义了这些操作在全局的顺序(2.6.3章)。
The state of a file region after a data mutation depends on :
1.the type of mutation(write/record append)
2.whether it succeeds or fails
3.whether there are concurrent mutations
表1总结了各种操作的结果。
RegionState有2中consistant和defined:
1.consistant:如果所有客户端,无论从哪个副本读取,读到的数据都一样,那么我们认为文件region是“consistant”;
2.defined:如果对文件的数据修改之后,region是consistant的,并且客户端能够看到写入操作全部的内容,那么这个region是“defined”。
操作有3种:
serial success:当一个数据修改操作成功执行,并且没有受到同时执行的其它写入操作的干扰,那么这个region就是defined的(隐含了consistant):所有的客户端都可以看到写入的内容。
Concurrent successes:并行修改操作成功完成后,region处于consistant、undefined状态:所有的客户端看到同样的数据,但是无法读到任何一次写入操作写入的数据。通常情况下,文件region内包含了来自多个修改操作的、混杂的数据片段。
Failure:失败的修改操作导致一个region处于inconsistant状态:不同的客户在不同的时间会看到不同的数据。
后面会讲我们的apps如何区分defined和undefined的region。应用程序没有必要再去细分未定义region的不同类型。
数据修改操作分为write或者Record Append两种。写入操作把数据写在应用程序指定的文件偏移位置上。即使有多个修改操作并行执行时,Record Append操作至少可以把数据原子性的追加到文件中一次,但是偏移位置是由GFS选择的(这句话有点费解,其含义是所有的追加写入都会成功,但是有可能被执行了多次,而且每次追加的文件偏移量由GFS自己计算)。 (相比而言,通常说的追加操作写的偏移位置是文件的尾部。)GFS返回给客户端一个偏移量,表示写入record的defined region的起点。另外,GFS可能会在文件中间插入填充数据或者重复record。这些数据占据的文件region被认定是inconsistant的,这些数据通常比用户数据小的多。
经过了一系列的成功修改操作之后,GFS确保被修改的文件region是defined的,并且包含最后一次修改操作写入的数据。GFS通过以下措施确保:
(a)对Chunk的所有replicas的修改操作要顺序一致(3.1章),
(b)使用Chunk version number来检测副本是否Stale(比如当它所在的Chunk服务器宕机时,错过了修改操作)。Stale的副本不会再进行任何修改操作,Master服务器也不再返回这个Chunk副本的Location给客户端。它们会很快被垃圾收集模块回收。
由于Chunk的Locations会被客户端缓存,因此在缓存的超时时间还没有到之前, 或者文件下一次(文件再次被打开后会清除缓存中与该文件有关的所有Chunk Locations)被打开前,就可能出现客户端从一个Stale的副本读取了数据。
而且,由于我们的文件大多数都是只进行追加操作的,所以一 个失效的副本通常返回一个提前结束(没有正确结束)的Chunk而不是过期的数据。因此当Reader重新尝试和Master服务器通信时,它就可以立刻得到最新的Chunk位置信息了,所以没关系。
即使在修改操作成功执行很长时间之后,机器故障也可能损坏或者删除数据。GFS通过Master服务器和所有Chunk服务器的定期“握手” 来找到故障的Chunk服务器,并且使用Checksum来校验数据是否损坏。一旦发现问题,数据要尽快利用有效的副本进行恢复。
若在GFS系统检测到错误并采取措施前,某个Chunk的所有副本已经全损毁或者丢失了,那么Chunk才会不可逆转的丢失。在一般情况下GFS的反应时间 (指Master节点检测到错误并采取应对措施)是几分钟。即使在这种情况下,Chunk也只是不可用了,而不是损坏了:应用程序会收到明确的错误信息而不会收到损坏了的数据。
2.7.2 applications的实现
使用GFS的应用程序可以利用一些简单技术实现这个宽松的一致性模型,这些技术也用来实现一些其它的目标功能,包括:尽量采用追加写入而不是覆盖,Checkpoint,自验证的写入操作,自标识的记录。
在实际应用中,我们所有的应用程序对文件的写入操作都是尽量采用数据追加方式,而不是覆盖方式。
比如一种典型的应用场景:Writer从头到尾写入数据,生成了一个文件。写入所有数据之后,应用程序自动将文件改名为一个永久保存的文件名,或者周期性的作Checkpoint,记录成功写入了多少数据。 Checkpoint文件可以包含程序级别的校验和;Readers仅校验并处理上个Checkpoint之后产生的文件region,这些文件region的状态一定是defined的。如果不考虑consistency and concurrency,这个方法可以满足我们的要求。
追加写入比随机位置写入更加有效率,对应用程序的失败处理更具有弹性。 Checkpoint可以让Writer以渐进的方式重新开始,并且可以防止Reader处理已经被成功写入,但是从应用程序的角度来看还并未完成的数据。
我们再来分析另一种典型的应用场景。许多应用程序并行的追加数据到同一个文件,比如对结果进行合并或者是一个生产者-消费者队列。record append的 “至少一次追加”的特性保证了Writer的输出。Readers使用下面的方法来处理偶然性的填充数据和重复内容:
Writers在每条写入的记录中都包含了额外的信息,例如Checksum,用来验证它的有效性。Reader可以利用Checksum识别和抛弃额外的填充数据和记录片段。如果应用不能容忍偶尔的重复内容(比如,如果这些重复数据触发了非幂等操作),可以用记录的唯一标识符来过滤它们,这些唯一标识符通常用于命名程序中处理的实体对象, 例如web文档。这些记录I/O功能 (alex注:These functionalities for record I/O)(除了剔除重复数据)都包含在我们的程序共享的库中,并且适用于Google内部的其它的文件接口实现。所以,相同序列的记录,加上一些偶尔出现的重复数据,都被分发到Reader了。
3. 系统交互
我们在设计这个系统时,一个重要的原则就是尽量减少所有操作和Master节点的交互。带着这样的设计理念,我们现在描述一下client、Master服务器和Chunk服务器如何进行交互,以实现数据修改操作、原子的record append操作以及snapshot功能。
3.1 lease和变更顺序
mutation(变更)是一个会改变Chunk内容或者元数据的操作。比如write操作或者append操作。mutation操作会在Chunk的所有副本上执行。我们使用租约 (lease)机制来保持多个副本间变更顺序的一致性。Master节点为Chunk的一个副本建立一个租约,我们把这个副本叫做主Chunk。主 Chunk对Chunk的所有mutation操作进行序列化。所有的副本都遵从这个序列进行mutation操作。因此,mutation操作全局的顺序首先由Master节点选择的租约的顺序决定,然后由租约中主Chunk分配的序列号决定。????
设计租约机制的目的是为了最小化Master节点的管理负担。租约的初始超时设置为60秒。不过,只要Chunk被修改了,主Chunk就可以申请更长的租期,通常会得到Master节点的确认并收到租约延长的时间。这些租约延长请求和批准的信息通常都是附加在Master节点和Chunk服务器之间的心跳消息中来传递。有时Master节点会试图提前取消租约(例如,Master节点想取消在一个已经被改名的文件上的修改操作)。即使 Master节点和主Chunk失去联系,它仍然可以安全地在旧的租约到期后和另外一个Chunk副本签订新的租约。
在图2中,我们依据步骤编号,展现写入操作的控制流程。
1. Client向Master节点询问哪一个Chunk服务器持有当前的租约,以及其它副本的位置。如果没有一个Chunk持有租约,Master节点就选择其中一个副本建立一个租约(这个步骤在图上没有显示)。
2. Master节点将主Chunk的标识符以及其它副本(secondary副本)的位置返回给Client。Client缓存这些数据以便后续的操作。只有在主Chunk不可用,或者主Chunk回复信息表明它已不再持有租约的时候,客户机才需要重新跟Master节点联系。
3. Client把数据推送到所有的副本上。客户机可以以任意的顺序推送数据。Chunk服务器接收到数据并保存在它的内部LRU缓存中,一直到数据被使用或者过期。由于数据流的网络传输负载非常高,通过分离数据流和控制流,我们可以基于网络拓扑情况对数据流进行规划,提高系统性能,而不用去理会 哪个Chunk服务器保存了主Chunk。3.2章节会进一步讨论这点。
4. 当所有的副本都确认接收到了数据,客户机发送写请求到主Chunk服务器。这个请求标识了早前推送到所有副本的数据。主Chunk为接收到的所 有操作分配连续的序列号,这些操作可能来自不同的客户机,序列号保证了操作顺序执行。它以序列号的顺序把操作应用到它自己的本地状态中 (alex注:也就是在本地执行这些操作,这句话按字面翻译有点费解,也许应该翻译为“它顺序执行这些操作,并更新自己的状态”)。
5. 主Chunk把写请求传递到所有的二级副本。每个二级副本依照主Chunk分配的序列号以相同的顺序执行这些操作。
6. 所有的二级副本回复主Chunk,它们已经完成了操作。
7. 主Chunk服务器回复Client。任何副本产生的任何错误都会返回给Client。在出现错误的情况下,写入操作可能在主Chunk和一些Secondary副本执行成功了(如果操作在主Chunk 上失败了,操作就不会被分配序列号,也不会被转发)。客户端的这次请求被确认为失败,被修改的region处于inconsistent的状态。如果出现这样的错误,我们Client的代码通过retry来重复执行。Client会先从步骤(3)到步骤(7)retry几次,再从头开始重复执行。
如果应用程序一次写入的数据量很大,或者数据跨越了多个Chunk,GFS客户机代码会把它们分成多个写操作。这些操作都遵循前面描述的控制流程,但是可能会被其它客户机上同时进行的操作打断或者覆盖。因此,共享的文件region的尾部可能包含来自不同客户机的数据片段,尽管如此,由于这些分 解后的写入操作在所有的副本上都以相同的顺序执行完成,Chunk的所有副本都是一致的。这使文件region处于consistent的、但是undefined的状态。(即2.7.1 GFS的保障的Concurrent successes)
3.2 数据流
为了提高网络效率,我们采取了把数据流和控制流分开的措施。在控制流从Client到主Chunk、然后再到所有二级副本的同时,数据以管道的方式, 顺序的沿着一个精心选择的Chunk服务器链推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延时的连接,使推送所有数据的延时最小化。
为了充分利用每台机器的带宽,数据沿着一个Chunk服务器链顺序的推送,而不是以其它拓扑形式分散推送(如tree)。线性推送模式下,每台机器所有的出口带宽都用于以最快的速度传输数据,而不用是在多个接受者之间分配带宽了。
为了尽可能的避免出现网络瓶颈和高延迟的链接(eg,inter-switch最有可能出现类似问题),每台机器都尽量的在网络拓扑中选择一台还没有接收到数据的、离自己最近的机器作为目标推送数据。假设客户机需要把数据推送到ChunkServer S1至S4一共4台Server:
1.首先它把数据推送到最近的Chunk服务器 S1。
2.然后S1把数据推送到离它最近的Server,假设S2~S4三台Server中最接近S1的机器是S2。那么S1就把数据推送给S2。
3.同样的,S2把数据传递给S3和S4中离自己更近的机器,依次类推推送下去。
我们的网络拓扑非常简单,通过IP地址就可以计算出节点的“距离”。
最后,我们利用基于TCP连接的管道式数据推送方式来使延迟最小化。Chunk服务器接收到数据后,马上开始向前推送。管道方式的数据推送很有用,因为我们采用全双工的交换网络:接收到数据后立刻转发出去不会降低接收的速度。
在没有网络拥塞的情况下,传送B字节的数据到R个副本的理想时间是 B/T+RL ,T是网络的吞吐量,L是在两台机器数据传输的延迟。通常情况下,我们的网络连接速度是100Mbps(T),L将远小于1ms。因此,在理想情况下,1MB的数据80ms左右就能分发出去。
3.3 Atomic Record Appends
GFS提供了一种原子的数据追加操作–record append。传统方式的写入操作,客户程序会指定数据写入的偏移量。对同一个region的并行写入操作不是串行的:region尾部可能会包含多个不同客户机写入的数据片段。使用record append,客户机只需要指定要写入的数据。GFS保证至少有一次原子的写入操作成功执行(即写入一个顺序的byte流),写入的数据追加到GFS指定的偏移位置上,之后GFS返回这个偏移量给客户机。这类似于在Unix操作系统编程环境中,以O_APPEND模式打开文件,在没有竞态条件时,对该文件并发写操作的行为。
record append在我们的分布应用中频繁被使用,在这些分布式应用中,通常有很多的Client并行地对同一个文件追加写入数据。如果我们采用传统方式的文件写入操作,Client需要额外的复杂、昂贵的同步机制,比如需要使用一个分布式的锁管理器之类的。在我们的工作中,这样的文件通常用于多个生产者/单一消费者的队列系统,或者是合并了来自多个Client的数据的结果文件。
record append是一种修改操作,它也遵循3.1节描述的控制流程,除了在主Chunk有些额外的控制逻辑。Client把数据推送给文件最后一个Chunk的所有副本,之后发送请求给主Chunk。主Chunk会检查这次记录追加操作是否会使Chunk超过最大尺寸(64MB)。
如果超过了最大尺寸,主 Chunk首先将当前Chunk填充到最大尺寸,之后通知所有Secondary副本做同样的操作,然后回复Client要求其对下一个Chunk重新进行record append操作。(record append的数据大小严格控制在Chunk最大尺寸的1/4,这样即使在最坏情况下,数据碎片的数量仍然在可控的范围。)
通常情况下追加的记录不超过 Chunk的最大尺寸,主Chunk把数据追加到自己的副本内,然后通知Secondary副本把数据写在跟主Chunk一样的位置上,最后回复Client操作成功。
如果记录追加操作在任何一个副本上失败了,客户端就需要重新进行操作。重新进行record append的结果是,同一个Chunk的不同副本可能包含不同的数据:重复包含一个record的全部数据或者部分的数据。GFS并不保证Chunk的所有副本在字节级别是完全一致的。它只保证数据作为一个整体原子h的被至少写入一次。 这个特性可以通过简单观察推导出来:如果操作成功执行,数据一定已经写入到Chunk的所有副本的相同偏移位置上。这之后,所有的副本至少都到了记录尾部的长度,任何后续的记录都会追加到更大的偏移地址,或者是不同的Chunk上,即使其它的Chunk副本被Master节点选为了主Chunk。就我们的一致性保障模型而言,record append操作成功写入数据的region是defined的(因此也是consistent的),反之则是inconsistent的(因此也就是undefined的)。正如我们在 2.7.2节讨论的,我们的程序可以处理inconsistent的区域。
3.4 快照
快照操作几乎可以瞬间完成对一个文件或者目录树(“源”)做一个拷贝,并且几乎不会对正在进行的其它操作造成任何干扰。我们的用户可以使用快照迅速的创建一个巨大的数据集的分支拷贝(而且经常是递归的拷贝),或者是在做实验性的数据操作之前,使用快照操作备份当前状态,这样之后就可以轻松的提交或者回滚到备份时的状态。
就像AFS (alex注:AFS,即Andrew File System,一种分布式文件系统),我 们用标准的copy-on-write技术实现快照。当Master节点收到一个快照请求,首先取消需要打快照的文件的所有Chunk的租约。这个措施保证了后续对这些Chunk的写操作都必须与Master交互以找到租约持有者。这就给Master节点一个创建Chunk拷贝的机会。
租约取消或者过期之后,Master节点把这个操作以日志的方式记录到硬盘上。然后,Master节点通过复制源文件/目录的元数据的方式,把这条日志记录的变化反映在内存的状态中。新创建的快照文件和源文件一样,指向完全相同的Chunks(原来文件对应的c1,c2,c3 三个chunks,那么新的快照文件也指向这三个chunks)。
在快照操作之后,当Client第一次想写入数据到Chunk C,它首先会发送一个请求到Master节点查询当前的租约持有者。Master节点注意到Chunk C的引用计数超过了1 (因为snapshot和源文件都指向这个Chunk)。 Master节点不会马上回复Client的请求,而是选择一个新的Chunk句柄C`。Master节点要求每个拥有Chunk C当前副本的Chunk服务器创建一个叫做C`的新Chunk。在源Chunk所在Chunk服务器上创建新的Chunk,使数据在本地复制而不是通过网络复制(硬盘比100Mb以太网大约快3倍)。并且,请求的处理方式和其它普通Chunk没什么不同:Master节点确保新 Chunk C`中的一个副本拥有租约,之后回复Client,Client得到回复后就可以正常的写这个Chunk,而不用知道它是从一个已存在的Chunk克隆出来的。
4. Master节点的操作
Master执行所有的namespace操作。此外,它还管理着整个系统里所有Chunk的副本:它决定Chunk的存储位置,创建新Chunk和它的副本,协调各种各样的系统活动以保证Chunk被完全复制,在所有的Chunk服务器之间的进行负载均衡,回收不再使用的存储空间。本节讨论这些主题。
4.1 namespace管理和锁
Master节点的很多操作会花费很长的时间:比如,快照操作必须取消Chunk服务器上快照所涉及的所有的Chunk的租约。我们不希望在这些操作运行的同时,延缓了其它的Master节点的操作。因此,我们允许多个操作同时进行,使用namespace的region上的锁来保证执行的正确顺序。
于许多传统的文件系统不同,GFS没有针对每个目录实现能够列出目录下所有文件的数据结构。GFS也不支持文件或者目录的链接(即Unix术语中的硬链接或者符号链接)。在逻辑上,GFS的namespace就是一个全路径和元数据映射关系的查找表。利用前缀压缩,这个表可以高效的存储在内存中。在存储namespace的树型结构上,每个节点(绝对路径的文件名或绝对路径的目录名)都有一个关联的读写锁。
每个Master节点的操作在开始之前都要获得一系列的锁。通常情况下,如果一个操作涉及/d1/d2/…/dn/leaf,那么操作首先要获 得目录/d1,/d1/d2,…,/d1/d2/…/dn的读锁,以及/d1/d2/…/dn/leaf的读写锁。注意,根据操作的不同,leaf可以是 一个文件,也可以是一个目录。
解释一下在/home/user被快照到/save/user的时候,锁机制如何防止创建文件/home/user/foo。快照操作获取/home和/save的读锁,以及/home/user和/save/user的写锁。文件创建操作获得/home和/home/user的读锁,以及/home/user/foo的写锁。这两个操作只能顺序执行,因为它们试图获取的/home/user的锁是相互冲突的。文件创建操作不需要获取父目录的写锁,因为这里没有”目录”,或者类似inode等用来禁止修改的数据结构。文件名的读锁足以防止父目录被删除。
采用这种锁方案的优点是支持对同一目录的并行操作。比如,可以在同一个目录下同时创建多个文件:每一个操作都获取一个目录名的上的读锁和文件名上的写锁。目录名的读锁足以的防止目录被删除、改名以及被快照。文件名的写锁使文件创建操作串行化,确保不会多次创建同名的文件。
因为名称空间可能有很多节点,读写锁采用惰性分配策略,在不再使用的时候立刻被删除。同样,锁的获取也要依据一个全局一致的顺序来避免死锁:首先按名称空间的层次排序,在同一个层次内按字典顺序排序。
4.2 副本的位置
GFS集群是高度分布的多层布局结构,而不是平面结构。典型的拓扑结构是有数百个Chunk服务器安装在许多机架上。Chunk服务器被来自同一或者不同机架上的数百个Client轮流访问。不同机架上的两台机器间的通讯可能跨越一个或多个网络交换机。另外,机架的出入带宽可能比机架内所有机器加和在一起的带宽要小。多层分布架构对数据的灵活性、可靠性以及可用性方面提出特有的挑战。
Chunk副本位置的选择有两大目标:最大化数据可靠性和可用性,最大化网络带宽利用率。为了实现这两个目的,仅仅是在多台机器上分别存储这些副本是不够的,这只能预防硬盘损坏或者机器宕机带来的影响,也只能最大化每台机器的网络带宽利用率。
我们必须在多个机架间分布储存Chunk的副本。 这保证Chunk的一些副本在整个机架被破坏或掉线(如电源或者网络交换机造成的问题)的情况下依然存在且保持可用状态。同时,在网络流量方面,对Chunk的读操作,能够有效利用多个机架的整合带宽;但写操作必须和多个机架上的设备进行网络通信,但是我们愿意付出这个代价。
4.3 Creation,Re-replication,Rebalancing
Chunk的副本的创建有三种触发方式:Chunk Creation,Re-replication,Rebalancing。
Chunk Creation:当Master节点创建一个Chunk时,它会选择在哪里放置初始的空副本。Master节点会考虑几个因素。
(1)我们希望在低于平均硬盘使用率的Chunk服务器上存储新的副本。这样的做法最终能够平衡Chunk服务器之间的硬盘使用率。
(2)我们希望限制在每个Chunk服务器上最近创建Chunk的次数。虽然创建操作本身是廉价的,但是创建操作也意味着随之会有大量的写入数据的操作,因为Chunk在Writer真正写入数据的时候才被创建,而在我们的”append-once-read-many”的工作模式下,Chunk一旦写入成功之后就会变为只读的了。
(3)如4.2所述,我们希望把Chunk的副本分布在多个机架上。
Re-replication:当Chunk的有效副本数量少于用户指定的复制因数的时候,Master节点会重新复制它。这可能是由几个原因引起的:一个Chunk服务器不可用了;Chunk服务器报告它所存储的一个副本损坏了;Chunk服务器的一个磁盘因为错误不可用了;或者Chunk副本的复制因数提高了。每个需要Re-replication的Chunk都会根据几个因素排优先级:1.Chunk现有副本数量和复制因数相差多少。例如,丢失两个副本的Chunk比丢失一个副本的 Chunk有更高的优先级。2.优先恢复活跃(live)文件的Chunk而不是最近刚被删除的文件的Chunk。3.为了最小化失效的Chunk对正在运行的应用程序的影响,我们提高会阻塞Client的Chunk的优先级(比如Client正在往一个只有一个replica的chunk追加数据,如果限制至少需要追加成功2个replica,才能返回client成功,那么这个chunk的复制任务会阻塞Client的写操作,需要提高优先级)。
Master节点选择优先级最高的Chunk,然后命令某个Chunk服务器直接从可用的副本”克隆”一个副本出来。选择新副本的位置的策略和创建时类似:平衡硬盘使用率、限制同一台Chunk服务器上的正在进行的克隆操作的数量、在机架间分布副本。为了防止克隆产生的网络流量大大超过Client的流量,Master节点对整个集群和每个Chunk服务器上的同时进行的克隆操作的数量都进行了限制。另外,Chunk服务器通过调节它对源Chunk服务器读请求的频率来限制它用于克隆操作的带宽。
Rebalancing:最后,Master服务器周期性地对副本进行Rebalancing:它检查当前的副本分布情况,然后移动副本以便更好的利用硬盘空间、更有效的进行负载均衡。而且在这个过程中,Master服务器逐渐的填满一个新的Chunk服务器,而不是在短时间内用新的Chunk填满它,以至于过载。新副本的存储位置选择策略和上面讨论的相同。另外,Master节点必须选择哪个副本要被移走。通常情况,Master节点移走那些剩余空间低于平均值的Chunk服务器上的副本,从而平衡系统整体的硬盘使用率。
4.4 垃圾回收
GFS在文件删除后不会立刻回收可用的物理空间。GFS空间回收采用惰性的策略,只在文件级别或Chunk级别的常规垃圾收集时进行。这个方法使系统更简单、更可靠。
4.4.1 机制
当一个file被应用程序删除时,Master节点像对待其它修改操作一样,立刻把删除操作以日志的方式记录下来。但是,Master节点并不马上回收资源,而是(在内存的元数据中)把文件名改为一个包含删除时间戳的、隐藏的名字。当Master节点对文件系统namespace做常规扫描的时候,它会删除所有三天前(这个时间间隔是可以设置的)的隐藏文件 。在文件被真正删除前,仍旧可以通过隐藏文件名读取它,也可以通过把隐藏文件改名为正常显示的文件名的方式“undeleted”。 当隐藏文件从名称空间中被真正删除后,Master内存中保存的这个文件的相关元数据才会被删除。这也有效的切断了file和它包含的所有Chunk的关联关系(就是说原来这个文件对应的chunk就变成孤儿chunk了)。
在对Chunk namespace做类似的常规扫描时,Master节点找到孤儿Chunk(不被任何文件包含的Chunk)并删除它们的元数据(在内存中删除这些chunk)。 Chunk服务器在和Master节点交互的心跳信息中,报告它拥有的Chunk子集的信息,Master节点回复Chunk服务器哪些Chunk在 Master节点保存的元数据中已经不存在了(刚刚被删除的孤儿chunk)。Chunk服务器可以任意删除这些Chunk的副本。
4.4.2 讨论
虽然分布式垃圾回收在编程语言领域是一个需要复杂的方案才能解决的难题,但是在GFS系统中是非常简单的。我们可以轻易的得到所有的Chunk:它们都只存储在Master服务器上的file-to-chunk mappings映射表中。我们也可以很轻易的得到所有Chunk的副本:它们都以Linux文件的形式存储在Chunk服务器的指定目录下。所有Master节点不能识别的副本都是”垃圾”。
在空间回收方面,垃圾回收相比直接删除有几个优势:
1.首先,对于组件失效是常态的大规模分布式系统,垃圾回收方式简单可靠。比如Chunk可能在某些Chunk服务器创建成功,某些Chunk服务器上创建失败,失败的副本处于无法被Master节点识别的状态。如果采用直接删除:Master发给Server的副本删除消息可能丢失,Master节点必须重新发送删除消息,包括自身的和Chunk服务器的 (alex注:自身的指删除metadata的消息),这样不一定能成功。 而垃圾回收提供了一致的、可靠的清除无用副本的方法。
2.第二,垃圾回收把存储空间的回收操作合并到Master节点规律性的后台进程中,比如,例行扫描和与Chunk服务器握手等。因此,批量的执行操作,使开销被分散。另外,垃圾回收在Master节点相对空闲的时候完成。这样Master节点就可以给那些需要快速反应的Client请求提供更快捷的响应。
3.第三,推迟存储空间的回收 为意外的、不可逆转的删除操作提供了安全保障。
延迟回收空间的主要问题是:延迟回收会阻碍用户调优存储空间的使用,特别是当存储空间比较紧缺的时候。当应用程序重复创建和删除临时文件时,释放的存储空间不能马上重用。通过显式的再次删除一个已经被删除的文件的方式,来加速空间回收的速度。我们允许用户为namespace的不同部分设定不同的复制和回收策略。例如,用户可以指定某些目录树下面的文件不做复制,删除的文件被即时的、不可恢复的从文件系统移除。
4.5 Stale Replica Detection
当Chunk服务器失效时,Chunk的副本有可能因错失了一些修改操作而Stale。Master节点保存了每个Chunk version number,用来区分当前的副本和过期副本。
无论何时,只要Master节点和Chunk签订一个新的租约,它就增加Chunk的版本号,然后通知最新的副本们。Master节点和这些副本都把新的版本号记录在它们持久化存储的状态信息中。这个动作发生在任何Client得到通知以前,因此也是对这个Chunk开始写之前。如果某个副本所在的Chunk服务器正好处于失效状态,那么副本的版本号就不会被增加。Master节点在这个Chunk服务器重新启动,并且向Master节点报告它拥有的Chunk的集合以及相应的版本号的时候,就会检测出它包含过期的Chunk;如果Master节点看到一个比它记录的版本号更高的版本号,Master节点会认为它和Chunk服务器签订租约的操作失败了,因此Master会选择这个更高的版本号作为当前的版本号。
Master节点在例行的垃圾回收过程中移除所有的Stale副本。在回收前,如果有Client向Master节点请求Chunk信息,Master不会把那些Stale的replica返回给Client,Client根本就不知道它们的不存在。另外一重保障措施是,Master节点在通知Client哪个Chunk服务器持有租约、或者指示Chunk服务器从哪个Chunk服务器进行克隆时,消息中都附带了Chunk的版本号。Client或者Chunk服务器在执行操作时都会验证版本号以确保总是访问up-to-date数据。
5. 容错和诊断
我们在设计GFS时遇到的最大挑战之一是如何处理频繁发生的组件失效。组件的数量和质量让这些问题出现的频率远远超过一般系统意外发生的频率: 我们不能完全依赖机器的稳定性,也不能完全相信硬盘的可靠性。组件的失效可能造成系统不可用,更糟糕的是,还可能产生不完整的数据。这一章讨论如何面对这些挑战,以及当组件失效不可避免的发生时,用GFS自带工具来诊断系统故障。
5.1 高可用性
在GFS集群的数百个服务器之中,在任何给定的时间必定会有些服务器是不可用的。我们使用两条简单但是有效的策略保证整个系统的高可用性:fast recovery(快速恢复)和replication(复制)。
5.1.1 Fast Recovery
不管Master服务器和Chunk服务器是如何关闭的,它们都被设计为可以在数秒钟内恢复它们的状态并重新启动。事实上,我们并不区分正常关闭和异常关闭;通常,我们通过直接kill掉进程来关闭服务器。Clients和其它的服务器会感觉到系统有点颠簸,正在发出的请求会超时,需要重新连接到重启后的服务器,然后重试这个请求。
5.1.2 Chunk Replication
正如之前讨论的,每个Chunk都被复制到不同机架上的不同的Chunk服务器上。用户可以为文件命名空间的不同部分设定不同的复制级别。默认是3副本。当有Chunk服务器宕机,或者通过Checksum校验发现数据corrupt,Master节点通过克隆已有的副本保证每个Chunk都是3副本。虽然Chunk复制策略对我们非常有效,但是我们也在寻找其它形式的跨服务器的冗余解决方案,比如使用奇偶校验(parity)、或者Erasure codes来解决我们日益增长的只读存储需求。我们的系统主要的工作负载是追加方式的写入和读取操作,很少有随机的写入操作,因此,在这个高度解耦合的系统架构下,实现这些复杂的冗余方案虽然具有挑战性,但依然是可以实现的。
5.1.3 Master Replication
Secondary Master:
为了保证Master服务器的可靠性,Master服务器的状态也要复制。Master服务器所有的operation log和checkpoint文件都被复制到多台机器上。当operation log写入到Master服务器的本机的磁盘并同步到其他所有备Master节点后,此次修改操作才能够称为committed。一个 Master服务进程不仅负责所有的修改操作,还包括后台的服务,比如垃圾回收等改变系统内部状态的模块。当Master进程挂了,可以立刻重新启动。如果Master进程所在的机器或者磁盘挂了了,处于GFS系统外部的监控进程会在另一个存有完整operation log的机器上启动一个新的Master进程。客户端使用规范的名字访问Master(比如gfs-test)节点,类似DNS。在Master进程转到别的机器上执行时,通过更改别名的实际指向机器,Client可以访问新的Master节点。
Shadow Master:
此外,GFS中还有些“影子”Master服务器,这些“影子”服务器在“主”Master服务器宕机的时候提供文件系统的只读访问。它们是影子, 而不是镜像,所以它们的数据可能比“主”Master服务器更新要慢,通常是不到1秒。对于那些不经常改变的文件、或者那些允许获取的数据有少量过期的应用程序来说,“影子”Master服务器能够提高读取的效率。但实际上,因为文件内容是从Chunk服务器上读取的,因此,应用程序不会发现过期的文件内容。在这个短暂的时间内,可能过期的只是文件的元数据,比如目录的内容或者访问控制信息等。
“影子”Master服务器为了保持自身状态是最新的,它会读当前正在增长的operation log的副本,并且依照和主Master服务器完全相同的顺序,来apply到内存,更改内部的数据结构。
和主Master服务器一样,“影子”Master服务器在启动的时候也会从Chunk服务器轮询数据(之后定期拉数据),数据中包括了Chunk副本的位置信息;
“影子”Master服务器也会定期和Chunk服务器“握手”来确定它们的状态。
在主Master服务器因创建和删除副本导致副本位置信息更新时,“影子”Master服务器才需要依赖主Master服务器(更新的operation log)。
5.2 数据完整性
每个Chunk服务器都使用Checksum来检查保存的数据是否损坏。考虑到一个GFS集群通常都有好几百台机器、几千块硬盘,磁盘损坏导致数据在读写过程中损坏或者丢失是很常见的。我们可以通过别的Chunk副本来解决数据损坏问题,但是跨越Chunk服务器比较副本来检查数据是否损坏很不实际。另外,GFS允许有歧义的副本存在:GFS修改操作的语义,特别是原子record append操作,并不保证副本完全相同(副本不是byte-wise完全一致的)。因此,每个Chunk服务器必须独立维护Checksum来校验自己的副本的完整性。
我们把每个Chunk(64MB)都分成64KB大小的block。每个block都对应一个32位的Checksum。和其它元数据一样,Checksum保存在内存中并通过记录日志的方式持久化,与用户数据是分开的。
对于读操作来说,在把数据返回给Client或者其它的Chunk服务器之前,Chunk服务器会校验此次读取操作涉及的Chunk的Checksum。因此 Chunk服务器不会把错误数据传递到其它的机器上。如果发生某个Chunk的Checksum不正确,Chunk服务器返回给请求者一个错误信息,并且通知 Master服务器这个mismatch。发送request的机器收到response后,会从其它副本所在的Server读取数据,Master服务器也会从其它副本克隆一份新的数据。当新的副本准备就绪后,Master服务器通知之前的Chunk服务器删掉mismatch的副本。
Checksum对读操作的性能影响很小,可以基于几个原因来分析一下。因为大部分的读操作都至少要读取几个blocks,而我们只需要读取一小部分额外的相关数据进行校验。GFS客户端代码通过每次把读取操作都对齐在Checksum block的边界上?????,进一步减少了这些额外的读取操作的负面影响。另外,在Chunk服务器上,Chunksum的查找和比较不需要I/O操作,Checksum的计算可以和I/O操作同时进行。????
因为尾部append操作(与之对应的是覆盖现有数据的写入操作)在我们的系统占了很大比例,因此,Checksum的计算方法,针对append操作作了高度优化。我们只增量更新最后一个不完整的块的Checksum,并且用所有的追加来的新Checksum块来计算新的Checksum???。即使是最后一个不完整的Checksum块已经损坏了,而且我们不能够马上检查出来,由于新的Checksum和已有数据不吻合,在下次对这个块进行读取操作的时候,会检查出数据已经损坏了。
相比之下,如果写操作覆盖已存在的一个的Chunk的范围,我们必须读取和校验被覆盖的第一个和最后一个block,然后再执行写操作;操作完成之后再重新计算和写入新的Checksum。如果我们不校验第一个和最后一个被写的block,那么新的Checksum可能会隐藏没有被覆盖区域内的数据错误。
在Chunk服务器空闲的时候,它会扫描和校验每个没有读写的Chunk的内容。这使得我们能够发现很少被读取的Chunk是否完整。一旦发现有Chunk的数据损坏,Master可以创建一个新的、正确的副本,然后把损坏的副本删除掉。这个机制能够及时发现损坏的冷副本。(因为如果某个Chunk没有人读,那么我不会去算checksum)
5.3 诊断工具
详尽的、深入细节的诊断日志,在问题隔离、调试、以及性能分析等方面给我们带来无法估量的帮助,同时也只需要很小的开销。没有日志的帮助,我们很难理解短暂的、不重复的机器之间的消息交互。GFS的服务器会产生大量的日志,记录了大量关键的事件(比如,Chunk服务器启动和关闭)以及所有的RPC的请求和回复。这些诊断日志可以随意删除,对系统的正确运行不造成任何影响。然而,我们在存储空间允许的情况下会尽量的保存这些日志。
RPC日志包含了网络上发生的所有请求和响应的详细记录,但是不包括读写的文件数据。通过匹配请求与回应,以及收集不同机器上的RPC日志记录,我们可以重演所有的消息交互来诊断问题。日志还用来跟踪负载测试和性能分析。
日志对性能的影响很小(远小于它带来的好处),因为这些日志的写入方式是顺序的、异步的。最近发生的事件日志保存在内存中,可用于持续不断的在线监控。
7. 经验
在建造和部署GFS的过程中,我们经历了各种各样的问题,有些是操作上的,有些是技术上的。
起初,GFS被设想为我们的生产系统的后端文件系统。随着时间推移,在GFS的使用中,不断投入了研发。我们开始增加一些小的功能,比如权限和配额,到了现在,GFS已经初步支持了这些功能。虽然我们生产系统是严格受控的,但是用户使用时却不总是这样的。因此需要更多的基础架构来防止用户间的相互干扰。
我们最大的问题是磁盘以及和Linux相关的问题。很多磁盘都声称它们支持某个范围内的Linux IDE硬盘驱动程序,但是实际应用中反映出来的情况却不是这样,它们只支持最新的驱动。因为协议版本很接近,所以大部分磁盘都可以用,但是偶尔也会有由于协议不匹配,导致驱动和内核对于驱动器的状态判断失误。这会导致数据因为内核中的问题意外的corrupt。这个问题促使我们使用Checksum来校验数据, 同时修改内核来处理这些因为协议不匹配带来的问题。
较早的时候,我们在使用Linux 2.2内核时遇到了些问题,主要是fsync()的效率问题。它的效率与文件的大小而不是文件修改部分的大小有关。因此在我们尚未实现Checkpoint的时候,如果operation log文件过大时,会带来性能上问题。我们费了很大的力气用同步写来解决这个问题,但是最后还是移植到了Linux2.4内核上。
另一个和Linux相关的问题是单个读写锁的问题,也就是说,在某一个地址空间的任意一个线程都必须在从磁盘page in(读锁)的时候先hold住,或者在mmap()调用(写锁)的时候改写地址空间。我们发现即使我们的系统负载很轻的情况下也会有偶尔的超时,我们花费了很多的精力去查找资源的瓶颈或者硬件的问题。最后我们终于发现这个单个锁在磁盘线程交换以前映射的数据到磁盘的时候,锁住了当前的网络线程,阻止它把新数据映射到内存。由于我们的性能主要受限于网络接口,而不是内存copy的带宽,因此,我们用pread()替代mmap(),用了一个额外的copy动作来解决这个问题。
尽管偶尔还是有其它的问题,Linux的开放源代码还是使我们能够快速探究和理解系统的行为。在适当的时候,我们会改进内核并且和公开源码组织共享这些改动。
8. 相关工作
和其它的大型分布式文件系统,比如AFS[5]类似,GFS提供了一个与位置无关的名字空间,这使得数据可以为了负载均衡或者灾难冗余等目的在不同位置透明的迁移。不同于AFS的是,GFS把文件分布存储到不同的服务器上,这种方式更类似Xfs[1]和Swift[3],这是为了提高整体性能以及灾难冗余的能力。
由于磁盘相对来说比较便宜,并且复制的方式比RAID[9]方法简单的多,GFS目前只使用复制的方式来进行冗余,因此要比xFS或者Swift占用更多的裸存储空间。
与AFS、xFS、Frangipani[12]以及Intermezzo[6]等文件系统不同的是,GFS并没有在文件系统层面提供任何Cache机制。我们主要的工作在单个应用程序执行的时候几乎不会重复读取数据,因为它们的工作方式要么是流式的读取一个大型的数据集,要么是在大型的数据集中随机Seek到某个位置,之后每次读取少量的数据。
某些分布式文件系统,比如Frangipani、xFS、Minnesota’s GFS[11]、GPFS[10],去掉了中心服务器,只依赖于分布式算法来保证一致性和可管理性。我们选择了中心服务器的方法,目的是为了简化设计,增加可靠性,能够灵活扩展。特别值得一提的是,由于处于中心位置的Master服务器保存有几乎所有的Chunk相关信息,并且控制着Chunk的所有变更,因此,它极大地简化了原本非常复杂的Chunk分配和复制策略的实现方法。
我们通过减少Master服务器保存的状态信息的数量,以及将Master服务器的状态复制到其它节点来保证系统的灾难冗余能力。
我们通过影子Master服务器机制来保证扩展能力和高可用性(对于读取)。对 Master服务器状态更改是通过预写日志的方式实现持久化。为此,我们可以调整为使用类似Harp[7]中的primary-copy方案,从而提供比我们现在的方案更严格的一致性保证。
我们解决了一个难题,这个难题类似Lustre[8]在如何在有大量客户端时保障系统整体性能遇到的问题。不过,我们通过只关注我们的应用程序的需求,而不是提供一个兼容POSIX的文件系统,从而达到了简化问题的目的。此外,GFS设计预期是使用大量的不可靠节点组建集群,因此,灾难冗余方案是我们设计的核心。
GFS很类似NASD架构[4]。NASD架构是基于网络磁盘的,而GFS使用的是普通计算机作为Chunk服务器,就像NASD原形中方案一 样。所不同的是,我们的Chunk服务器采用惰性分配固定大小的Chunk的方式,而不是分配变长的对象存储空间。此外,GFS实现了诸如Rebalancing,replication,recovery等等在生产环境中需要的模块。
不同于与Minnesota’s GFS和NASD,我们并不改变存储设备的Model (alex注:对这两个文件系统不了解,因为不太明白改变存储设备的Model用来做什么,这不明白这个model是模型、还是型号)。我们只关注用普通的设备来解决非常复杂的分布式系统日常的数据处理。
我们通过原子的记录追加操作实现了生产者-消费者队列,这个问题类似River[2]中的分布式队列。River使用的是跨主机的、基于内存 分布式队列,为了实现这个队列,必须仔细控制数据流;而GFS采用可以被生产者并发追加记录的持久化的文件的方式实现。River模式支持m-to-n的分布式队列,但是缺少由持久化存储提供的容错机制,GFS只支持m-to-1的队列。多个消费者可以同时读取一个文件,但是它们输入流的区间必须是对齐的。???
9. 结束语
Google文件系统展示了一个使用普通硬件支持大规模数据处理的系统的特质。虽然一些设计要点都是针对我们的特殊的需要定制的,但是还是有很多特性适用于类似规模的和成本的数据处理任务。
首先,我们根据我们当前的和可预期的将来的应用规模和技术环境来评估传统的文件系统的特性。我们的评估结果将我们引导到一个使用完全不同于传统的设计思路上。根据我们的设计思路,我们认为组件失效是常态而不是异常,针对采用追加方式(有可能是并发追加)写入、然后再读取(通常序列化读取)的大文件进行优化,以及扩展标准文件系统接口、放松接口限制来改进整个系统。
我们系统通过持续监控,复制关键数据,快速和自动恢复提供灾难冗余。 Chunk复制使得我们可以对Chunk服务器的失效进行容错。高频率的组件失效要求系统具备在线修复机制,能够周期性的、透明的修复损坏的数据,也能够第一时间重新建立丢失的副本。此外,我们使用Checksum在磁盘或者IDE子系统级别检测数据损坏,在这样磁盘数量惊人的大系统中,损坏率是相当高的。
我们的设计保证了在有大量的并发读写操作时能够提供很高的合计吞吐量。我们通过分离控制流和数据流来实现这个目标,控制流在Master服务器处理,而数据流在Chunk服务器和客户端处理。当一般的操作涉及到Master服务器时,由于GFS选择的Chunk尺寸较大(alex注:从而减小了元数据的大小),以及通过Chunk Lease将控制权限移交给主副本,这些措施将Master服务器的负担降到最低。这使得一个简单、中心的Master不会成为成为瓶颈。我们相信我们对网络协议栈的优化可以提升当前对于每客户端的写入吞吐量限制。
GFS成功的实现了我们对存储的需求,在Google内部,无论是作为研究和开发的存储平台,还是作为生产系统的数据处理平台,都得到了广泛的应用。它是我们持续创新和处理整个WEB范围内的难题的一个重要工具。
网友评论