前言

GFS(Google File System)是google三驾马车中最为重要的一部分。在GFS之上google构建了伟大的事业,其含金量毋庸置疑,值得写一篇笔记记录一下个人的理解(后面的论文如果不是非常重要应该不会写了,写一篇总结太浪费时间了)

虽然GFS很成功,但是在从当时(2003年)的角度来看,GFS并没有提出很多创新点。它所采用的各种机制,比如多副本,单master,日志,分块等都是在过去的几十年内人们讨论分布式文件系统时经常讨论的话题。

GFS最大的优势就是它是完全出于google自身需求构建的一个文件系统,并且用成功运行在上千台机器上的实际成果说服了学术界,让学术界重新考虑新时代的分布式文件系统的形式。

接下来的内容说是对论文的解读,其实是把我个人在阅读GFS时所做的一些笔记摘录下来,其中不乏一些因个人理解能力不足所带来的一些错误观点。

设计总览

设计需求

正如前言所说,GFS是完全出于google自身需求构建的。GFS始终是一个仅使用于google内部的文件系统(经过有根据该论文的开源实现),所以一切的机制,接口等都首先考虑google的工作负载。

  1. 适用于廉价硬件

    GFS需要运行在由廉价的机器组成的序列上,这才能保证整个系统是容易升级和修复的,同时也让系统能够激进地淘汰可能出现故障的机器。所以GFS的设计必须假定所有的设备是不可靠的,随时会崩溃的

  2. 主要支持大文件存储

    这就是所谓的出于google自身需求构建。在新世纪以来,在摩尔定律的作用下硬件逐渐变得高效且廉价。google认为索引整个互联网是可能的(来自jyy的介绍),因此google需要一个能够存储大规模数据的文件系统,以此来支撑后续要构建在其上的应用。所以GFS面向的是大文件(一个chunk足足有64MB,就算现在看来也是很激进的分块)

  3. 主要提供大规模的流式读取

    google的工程师们观察到在构建大规模数据处理时,数据的访问通常是流式读取的。因此在GFS的设计中也假设用户读取数据时是按顺序访问的(master会在发送一个chunk的位置时顺便发送后续几个chunk的位置)

  4. 文件很少更改

    也是来自于google的需求,他们将要构建的应用是往往是用于处理静态文档的(主要是HTML文档)。这样的文档一经爬取就不再更改,因此GFS没有对随机写入做任何的优化,而是提供了record append这样更注重于追加的方案

  5. 在高并发访问下保持语义一致性

    分布式系统普遍会遇到的问题。比如对同一文件进行追加(append),因为传统的append是向用户认为的文件结尾进行写入,在并发下就会造成后执行的append覆盖了先执行的append。所以GFS提供了record append这样的方案

  6. 比起低延迟更注重高带宽

    对于google的应用程序而言,以高速率批量处理数据的能力比起快速响应更加重要。所以这让GFS的设计可以不太在意响应时间,而是更注重提高数据传输的带宽

提供的接口

GFS提供和Unix很像的操作接口,尽管并不是完全的符合POSIX标准。

提供以下几个标准的Unix接口,create, delete, open, close, read, write

除去这几个通用接口,GFS提供两个重要的自定义接口snapshotrecord append

snapshot:通过GFS内部的机制进行文件的复制,在复制期间通过锁来保护复制过程的安全

record append:对一个文件进行追加。保证完成至少一次的完整追加,追加位置由GFS来决定,但保证来自同一用户的两次追加在文件中有明显的先后关系。GFS最重要的机制之一,GFS通过提供这个弱一致性的操作展现了巨大的性能优势,给当时追求强一致性的学院派们好好地上了一课

结构设计

GFS采用单一master,多chunkserver的模式

每一个程序作为一个普通的linux应用程序运行在廉价机器上。因为linux允许多任务,所以每一个chunkserver也可以运行一些其他客户端程序。这就是MapReduce那篇论文中提到的一个方案:运行mapreduce的worker的机器上同时也运行着GFS的一部分,这样mapreduce的master可以将一些需要存储在GFS的数据的工作直接分配给存储着这些数据的机器上的worker,以此实现本地访问

每一块在被创建时都分配一个全局唯一的永不可变的handle(其实就是ID)。handle是一个64bit的数字(不知道后面他们是否反悔弄小了,不过扩展起来应该不麻烦)

由master来维护所有文件系统的元数据,主要包括名称空间(即目录树),访问权限文件名到chunk handle集合的映射chunk现在的所在位置

所有的应用的请求会通过GFS的客户端转发,这样的工作模式类似于数据库应用。因此GFS不需要挂载在Linux的文件树上。GFS的客户端通过与master通信获取元数据,通过与特定的chunkserver通信来获取数据

所有的文件数据不会被客户端或者chunkserver缓存。这顺应了GFS设计需求中的第3点,文件总是流式顺序读取,几乎不回读,因此没有必要使用缓存。这也消除了传统的分布式文件系统往往要解决的缓存一致性问题。不过为了减小通信开销,master提供的元数据还是会被客户端缓存一段时间的。

单一master模型

单一master模型能为实现带来很多好处,至少单一master来管理全局资源在实现上就可以免去那些繁琐的同步机制。

但是显而易见的后果就是单一master很容易成为性能瓶颈。更严重的是因为GFS的所有的数据都是in memory的,随着系统投入使用多年,越来越多的数据使得google不得不增加master的内存大小,然而一台机器能够加量的内存大小也是有上限的。这也是为什么GFS这么快被Colossus替代的原因之一

OK,抛开事后诸葛亮不谈,单一master至少在当时是一个正确的选择。

客户端所有的操作都要经过master的,包括访问。不过针对访问,在一次完整的访问之后,客户端在一定时间内可以通过缓存的元数据来直接访问chunkserver。

如下图所示,client先以文件名和chunk index请求master,master以chunk handle和chunk所在的位置回应。随后client根据这些信息向chunkserver发起请求,chunkserver以以数据回应。

一个让人迷糊的地方可能是client如何知道他要访问的chunk是该文件的哪一个。别忘了GFS构建的前提:几乎所有的文件都是顺序访问。因此应用程序总是从第一个chunk开始读取数据,按顺序接着就是第二个

chunk的大小配置

chunk的大小是一个能显著影响工作效率的参数。GFS将其设置为了一个现在看来也十分激进的数值,足足64MB

不过GFS使用了惰性分配的机制来保障磁盘使用率,所有的chunk在chunk server上都按照handle的数值来命名,作为一个普通的linux文件来保存。借用linux本身就有的一些机制,chunk虽然逻辑上是64MB,但是在实际填充满64MB之前,并不会占满64MB,只有在真正需要增加数据时才增长。

64MB的大块能够带来很多好处,首先对于流式读取的任务来说,在一段时间内总是访问相同的块,因此就能利用好缓存在客户端的元数据。其次大块能减小需要存储在master中的元数据的大小,让所有的元数据都能存储在内存中随时准备被读取,最后由于惰性分配空间,实际上大块并不会造成很大的内部碎片

不过接下来提到了一个大块的一个缺点,如果一个文件十分的小,那么在文件系统中只会占用一个块,这时如果多个客户端访问这个文件,那么这个块很容易成为热点(而其他的大文件因为有分块,一般不会导致一个块成为热点)

这个问题在google尝试通过GFS分发一个二进制可执行文件时出现,当几乎所有的机器都在尝试访问同一个文件,就算一个chunk有多个备份(通常只有三个)也难以承担这么高频的访问。对于这个问题,google的解决方案是为每一个文件设置复制因子,因子越高这个文件的chunk的备份次数就越高

元数据

元数据由master管理和分发。其中名称空间和file-to-chunk的映射需要持久化的存储,这样的持久化是通过日志系统实现的。

但是chunk的具体位置是不需要持久化的,这一信息通过master定期与chunkserver的通信来获取

所有的元数据在master启动之后都被置于内存之中,尽管这听起来是一个不怎么好的主意(这也是单master很快就被淘汰的原因之一),但是这确实让master所有有关元数据的操作都变得很快。

内存中的数据结构

将所有的数据都放置于内存中,能运行master在后台启动一些额外服务。比如定期的扫描这些数据可以实现垃圾回收,重新建立副本,处理chunkserver的故障,重新平衡chunk的放置情况

论文中也承认了将所有的数据放置于内存中的不足,但作者认为这样的不足可以直接通过增大内存来避免,然而内存也是有上限的

chunk的位置信息

正如前面提到的,GFS的设计前提是假定所有的硬件是十分不可靠的。 因为每次启动master时,都可能有chunkserver处于不可用状态。持久化地存储位置信息是没有用的,因为今天能够访问的chunkserver明天不一定能够使用

master通过定期与chunkserver通信来获取chunk的具体存储位置,这样的设计虽然有一定的维护开销,但是显然减少了为了同步持久化存储在master和chunkserver之间的信息的努力

操作日志

几乎所有的现代文件系统都使用日志来保障数据的持久化存储,GFS也不例外。

日志系统只有在保证元数据的变化情况已经在系统中持久化存储之后才会将其暴露给客户端

日志也有多重备份以供恢复使用。当master崩溃后,可以通过重放日志的方式重建master。

为了尽可能减小日志的空间占用,master会定期地创建自身状态的检查点(如同很多数据库应用会做的那样)。当一个检查点被创建后,之前的日志就可以丢弃了。接下来的运行过程中如果还出现了崩溃,就可以使用检查点和自从该检查点创建以来的日志进行恢复

定期创建检查点可能比较费时间,论文中介绍了一种能够不拖延当前操作的方案:创建一个新的日志文件,对旧检查点重放旧日志即可创建当前的检查点。这一过程可以在一个新的线程进行,同时因为新到的请求不会干涉旧日志文件,因此不会造成系统的拖累

一致性模型

纯学术定义,看的最痛苦的一章。。。该章最好在阅读完了Record Append的具体实现的情况下再来读,不然会非常痛苦

总之就是GFS采用了弱一致性模型,能够在满足应用程序需求的情况下提高性能

GFS的保证

所有对于命名空间(即文件树)的改动都是原子的,即所有的创建,重命名,删除等等会对文件树造成影响的改动都是原子的。这条保证是MapReduce这样的应用实现并发处理reduce工作的基础,所有reduce节点最后的输出会保存在GFS中,它们首先以一个临时文件名存在,然后在完成后进行重命名,在有多个相同的reduce节点时,这样的处理能保证两个reduce节点不会互相干扰

接下来的部分给出两个非常绕的定义

  • consistent(一致性):

    指所有的用户端在访问同一个区域的数据时,无论从任何块服务器上获取的,都能保证一样。即所有的块服务器在该文件的这个区域存储着相同的副本

  • defined(定义性):

    在保障一致性的基础上,客户端在写入完成后紧接着读取写入的数据,总能够在数据区域中找到它写入的那部分

    假如是串行的操作,write后马上读取,没有人修改,当然是defined的

    如果是并行的操作,write后读取可能会因为别的客户端修改了写入的数据,从而导致写入者再也无法读取到自己写入的那部分

  Write Record Append
Serial success defined defined
Concurrent successes consistent but undefined defined, interspersed within consistent
Failure inconsistent inconsistent

consistent还是比较好理解的,如果有一个副本进行写入时发生了失败,那么很显然会导致该副本的某个区域和其他副本不一致。反之如果所有的副本都在系统的协调下完成写入并返回了成功的消息,那么所有的副本就都是一致的。

一个典型的Undefined的场景就是如果多个客户端对同一文件进行传统的追加。传统的追加本质上是对客户端认为的该文件结尾进行写入。因此就很容易出现两个客户端都发起了类似于WriteAt(file, offset, content…)这样的请求,相当于在同一个地方写入两次。这时对于后写入的用户而言,如果它再请求看一下该文件的结尾,它能发现结尾处有它写入的内容,所以对于该用户而言,这个文件是defined;但是对于先写入的用户而言,因为其写入被后写入的用户的请求覆盖,当它再次尝试去看该文件的结尾,它根本找不到自己写入过的痕迹,因此这次写入对于它而言就是Undefined

而Record Append因为其至少写入一次的保障,因此对于所有它的用户而言,只要服务器返回了成功的响应,它总是defined。(但是因为Record Append的一些特殊机制,在部分副本处理写入时失败,它会导致部分区域处于不一致的状态)

总之GFS主推的文件修改方式Record Append在并发下提供了一种神奇的弱一致性模型,如下图,在有副本更新失败的情况下就会导致对应的区域不一致,但是它却最终能达成defined状态

| CS 1 | CS 2 | CS 3 | chunk server

| ...  | ...  | ...  |
| B    | B    | B    | 一次成功的Record Append,所有CS一致
| A    | A    | X    | CS 3更新失败,导致这次的Record Append失败,该区域成为不一致的区域
| C    | C    | C    | 一次成功的Record Append,所有CS一致
| A    | A    | A    | 第一次失败的Record Append的重做,所有CS一致

对应用的影响

因为弱一致性的问题,构建在GFS上的应用需要一些特殊的调整才能正常使用。

还是回到最开始的假设,构建在GFS上的应用应该总是使用record append而不是任何的随机写入。

同时因为弱一致性可能会导致一些文件区域是属于重复写入未完整写入空白等情况(比如上面的表中的第二次追加,CS 1,2都重复写入了一次A记录,而CS 3在第二次追加时可能写入了无效的数据或者根本没写入)

因此应用需要在写入数据和取出数据时要添加一些额外信息来避开这些不一致的区域。

首先是使用校验和,因为有可能造成不完全的写入,所以可以在写入时顺便记录一下校验和,在读出时只需比对一下就可以知道是否是一条有效的数据。比如还是上图,如果有一个客户端在顺序地从CS 3读取数据,在读到第二条时,因为这是一次失败的写入,所以校验和的计算将是错误的,这时应用程序就可以忽略这一条数据

接着是为每一条记录做好序号标记。在读取时就能通过判断该序号的数据是否被读取过来过滤掉重复写入的数据。还是上图,如果一个应用从CS 1读取数据,在第一次读取第二条数据时正确读取了记录A,接着遇到第四条数据,因为这是来自同一个Record Append的数据,所以序号一致,客户端知道这条已经被读取了,就可以跳过该条数据

google内部在使用GFS时会使用一个专用的库,这个库做好了以上的两个处理,应用程序只需要处理好读写即可

系统交互

该部分主要讨论了client,master,chunkserver之间是怎么样交互来实现数据修改原子化的record append以及snapshot

租约和修改顺序

因为有多个副本,为了维护它们的一致性,需要指定一个副本作为主服务器(Primary),让所有的副本跟随这个副本的改变

但这造成了一个问题:如果主服务器出现了故障,要怎么做

一个普通的想法就是重新指定一个主服务器。但这样做的问题在与如果上一个主服务器出现的问题是可恢复的(比如说只是临时的网络错误),当其修复完错误之后重新加入系统,仍认为自己是主服务器,这时系统中针对一个块就有了多个主服务器,造成了混乱

为了解决这样的问题,各种神奇的一致性协议被提出,而GFS解决这一问题的方案就是租约机制

master为一个chunk的副本授予租约,此时该副本被称作primary,在租约的期限内,所有的对此chunk的更改由primary管理

一个租约最初为60秒,但是primary也可以申请延长这一期限,同时master也可以在它不希望一个文件更改时主动销毁一个租约,在一个租约彻底到期之前,master绝对不会再赋予另一个副本primary(也意味着在到期前不允许任何更改)

下图展示了在有primary的情况下数据的更改是如何实现的

  1. 客户端询问master哪个chunkserver拥有它想访问的chunk的租约以及其他的副本所在的位置

  2. master以primary的标识符回应,同时也包括其他副本的位置

  3. client逻辑上将数据发送到所有的副本,这些数据会暂存在所有chunkserver的LRU缓冲区中,等待被使用

  4. 当确保所有的副本都收到了数据,client向primary发起写请求。primary将可能存在的并行写入线性化,为每个写入都标记好顺序,然后按顺序执行写入

  5. 接着primary向其他的副本转发写请求,同时告诉其他的副本它写入的顺序。所有的副本都按照与primary相同的顺序写入

  6. 其他的副本向primary报告完成情况

  7. primary向client报告完成情况。任何副本的写入失败都会被报告,client可以选择重做来修复这些失败的写入(当有任意的副本没有完成更新但是大部分的副本都完成了更新,这时这个文件区域就处于不一致的状态)

当一次客户端的写入超过了一个块的大小(大于64MB) 客户端会将其分作多次发送

在并发条件下会导致文件区域处于consistent但是undefined的状态,因为其他的客户端也可能在写入,而primary决定的顺序可能是将其他客户端的写入插入到多次发送的大规模写入之间

数据流

逻辑上数据将会被发送到所有的副本,实际操作中GFS选择通过线性地将数据在副本服务器间传输。如同Figure 2所示,client只将数据发送到距离其最近的一个副本服务器,然后这个副本服务器将数据发送给另一个副本服务器。这样的路径是通过一个优秀的算法来精心选择的

这样做的目的是减小网络通信开销,一条精心挑选的路径能最大化地降低两个节点之间的通信成本以此来降低整个系统的通信开销

因为chunkserver并不是只服务一个chunk

只使用线性传输保证每个块在每一台chunkserver上都占据相同的网络资源(即同一时间内每个块在一台chunkserver上最多只收发一次)。而且收发并不需要等待完整接收,可以通过管道的方式流式传输。这样可以很好地利用google的双全工网络

原子化的Record Append

个人认为是GFS最为重要的机制,完美诠释了什么叫为工作载荷量身定做的系统

Record Append只需要client提供数据,实际要追加的offset由GFS决定

GFS保证完成至少一次完整的追加(完整指原封不动地将client给予的数据追加)

要实现Record Append,需要由primary合理地安排并发的Record Append写入。

当一次Record Append请求转发到primary时,primary检查该chunk剩余的空间是否能够容纳要追加的数据,当一次append操作超过了当前chunk能够容纳的空间,primary会直接填充完当前的剩余空间(用完当前的chunk),然后通知client重新尝试在该文件的下一个chunk尝试append。

只要一个副本出现差错就直接重做整个Record Append,所以说是保证至少出现一次

因此Record Append是inconsisitent的。因为大部分的副本成功,但是有些失败。这时这次的尝试添加的区域就会变成不一致的状态(相同文件区域在不同副本上存储不同的数据),但是对于成功的区域而言,它总是defined

以上两个策略十分的保守,会显著减少磁盘的利用率,不过对于google来说,磁盘一点都不值钱

在遇到并发的多个Record Append时,primary会将其线性化处理然后转发到其他副本,保证所有的副本都以同样的策略来使用暂存在LRU缓存中的数据

Record Append是一个非常简单,非常保守的方案。但是在损失了强一致性的情况下,却也免去了为了维持强一致性的开销。这样的tradeoff就是系统领域令我沉迷的原因之一

快照

和AFS(Andrew File System)一样,GFS也使用COW(copy-on-write)来实现快照

当master收到一个创建快照请求时,它会取消该文件所有块的租约,这样就创建出了一个文件不会被修改的空档来执行COW,随后所有的块都会被标记为COW块。

当后续出现一个对这些块的写请求时,master会注意到这些块时COW的,于是它会暂缓响应客户端的请求,先完成COW块的复制工作

为了加速这一过程,新生成的块会在原有的块的本地机器上创建,这样就可以避免高昂的网络传输

总之流程和操作系统里面的COW几乎没有区别

Master的行为

master几乎处理了除了数据传输以外的任何事情,它决定了所有块的存放位置,管理新块的创建,并协调各种系统范围的活动,以此保障块的充分复制,平衡存储空间,回收不再使用的空间

命名空间管理和锁

通过取消租约来保障创建快照时的原子性是也是十分简单粗暴的方案。但是GFS也希望能够通过一些机制来优化并发条件下对文件树的更改(比如文件的创建和重命名),这时就不得不涉及到我们的老朋友->

GFS维护着完整路径->元数据的映射。传统的Unix文件系统会将目录也作为一种特殊的文件,以此来索引文件,但是在分布式系统中,一个长路径查找可能会造成多次访问目录文件,比较低效。所以GFS直接使用完整路径映射,比如数据结构会直接维护一个/abc/def/file.txt -> metadata-of-file的映射

命名空间需要用锁维护主要是为了避免对文件树的修改产生冲突,比如两个客户端同时尝试创建一个路径为/abc/def/file.txt的文件。如果没有锁的保护,显然是有风险的。

GFS通过使用读写锁一级一级地锁住上级目录来保证命名不会冲突,比如对def目录加读锁就可以保证客户端在尝试创建该文件时至少上级目录是有效的。只有需要修改的叶节点才需要加上写锁来读取到不合适的数据

说到底GFS完全可以不采用维护文件树的方式来组织文件,因为master维护的是完整的路径字符串到元数据的映射,映射的键完全是可以直接用数字之类的唯一编号取代的,这样做的目的只不过是为了给使用者提供一个熟悉的Unix目录接口罢了,锁是为了维护这样Unix like的目录所要付出的代价

副本的存放位置

副本的最重要的用处就是故障后恢复,为此要在设计副本的存放位置时,要首先保证多个副本很难出现同时崩溃的情况

google将组成GFS的机器分散在不同的机架(rack)上。只是把副本存储在不同机器只保证了系统不会因为磁盘或机器故障而损伤,但是不能面对更现实的灾难,比如机房断电,水灾,火灾,地震等等,这些灾难很容易导致处于同一机架上的机器同时物理崩溃,所以副本之间需要物理隔离

将副本分散在不同的机架上,虽然对于一些也处于集群中的客户端而言,能直接从最近的机器上读取(甚至是相同的机器)。但是对于写操作,写流量必须要跨越不同的机架

创建,再复制和再平衡

一个块的副本被创建只会来自于块的创建重复制重平衡

当一个副本被创建时,master会考虑将这个新的副本放置于哪个chunkserver上,具体参考以下几点:

  1. 优先选择磁盘使用率低于平均水平的chunksever

  2. 尽可能限制一台chunkserver最近创建的块的数量,因为块创建后很有可能会近期被写入,最近创建的越多,写入越频繁。

  3. 保证副本能处于不同机架上,以此应对非机器故障带来的灾难

同时master会根据以下几点考虑复制一个块的优先级

  1. 距离用户希望的副本数的差额

  2. 是否是一个已被标记为delete的文件所属的块

  3. 是否阻塞了用户程序的请求

同时master也会定期地尝试平衡chunkserver的磁盘使用情况,将那些磁盘使用率显著高于其他chunkserver的服务器的数据搬运到低于平均值的服务器上,这个优先级应该也和创建新的副本时的考虑是一样的

这些工作都会在master的后台完成,不会太多地占用用户的时间

垃圾回收

当一个文件被请求删除时,GFS不会立即删除所有的物理数据,而是通过一系列机制来实现惰性删除。这样做同时避免了为了同步删除可能造成的开销,而且GC可以在负载相对轻松的时候进行,不会影响高负载情况下系统的表现

机制

文件的删除分为三个阶段

首先是重命名为一个临时命名,逻辑上等于将其移动至回收站,此时仍然可以通过该名字访问文件。

接着在进入回收站多天后,彻底删除该文件的元数据

最后周期性的扫描会发现那些没有文件指向的chunk,向chunkserver发起删除请求

前两个步骤都只需要master自行处理,最后一个步骤需要chunkserver参与进来。在master与chunkserver的定期通信中,chunkserver会交换当前它所持有的chunk,master检查这些chunk是否属于合法的文件。如果不是则向该chunkserver发起删除请求,删去那些不应该存在的chunk

讨论

GFS所要实现的GC相比于编程语言那些涉及图论计算的GC而言简单了很多,因为判别一个块是否要回收的方法很简单,只要是master不认识的块都是可以回收的。这就是GFS的单master机制带来的好处之一

对比于直接进行删除,这种延迟回收有很多优点

更适合硬件故障是常态的分布式系统,这种被动式的删除减轻了为了同步各个副本所要花费的开销

同时这种自动的,定期的查询是否应该拥有这些chunk的机制能适合多种失败场景

最重要的是这种活动能够在master的后台定期执行,开销是可控的,完全可以选择一个服务器负载压力不大的时候来处理GC

当然也有一定的缺点。如果有用户频繁地创建和删除文件,磁盘空间的利用率会变得很低,因为删除是延后一段时间的(默认是三天)。不过解决方案也有,主要就是针对这些特例选择性地执行立即删除,或者让用户指定这些频繁创建后删除的文件不要进行复制等等

过期副本检测

GFS使用版本号机制来保障数据不会出现多重版本

每当master赋予一个块新的租约时,它会增加块版本号并通知最新的副本

之后所有的更改都会引起版本号的增加(master和chunkserver的都会增加),只要有副本没有跟随master或者primary的改动,导致版本号跟不上主流,该副本就会被标记为过期(stable)。之后所有的读写请求都不会被转发到这个副本(虽然对于一些缓存了元数据的客户端而言还是可以访问,但那种情况不长发生),在后续的GC中,master也会注意到这些过期的副本,定期删除它们

不过实际上版本号机制比想象中复杂,因为会有多种多样的故障情况,google内部的GFS肯定对此做了很多的优化

容错和诊断

这一节详细讨论了GFS为了高容错能力所作出的努力,其中很多的机制在前面已经介绍过了,大体上分为两种:快速恢复和多副本。

高可用性

快速恢复

master和chunkserver都是以能快速恢复为前提构建的,它们的启动时间通常都在几秒内

比如前面提过,master使用检查点加上日志系统,能够快速恢复到故障之前的状态。

快速的启动速度允许GFS以较小的代价重启那些失去响应的设备

块复制

这个机制在前面已经讨论的足够多了,GFS通过对块进行复制并分散到不同的机架上来避免数据的永久丢失

google在论文中也提到希望能通过类似奇偶效验和纠错码来保存一些一些长期存储的数据。这部分的工作几乎和RAID所想要做的一样了

master复制

虽然我们说GFS所采用的是单一master模式,但那指的是同一时刻只有一台master机器在工作。GFS有多个master的备份来防止元数据丢失,正如前面提过,只有当一次操作日志被完整地存储在master的所有副本上时才算有效。所以根据日志是能完整地复原master的

客户端与master之间的通信不是直接使用ip地址,而是一个域名(比如gfs-test)。这样在google内部的DNS服务器中,可以将这个域名指向正在工作的master的ip地址。当外部的监测进程发现master离线,可以立刻启动master的副本,同时修改DNS服务器中该域名的条目,使其指向新启动的master的ip地址(非常聪明的热启动方式)

master也会有被称作shadow的备份,shadow master几毫秒拉取一次master的数据,不保障与master任何时刻都一致。

shadow master虽然在状态上落后于master,但是如果一个应用程序只用读取长期存储的数据或者不介意获取过时数据,它能在master下线后临时为这类应用提供服务

数据完整性

这部分没怎么仔细看,大致就是GFS用校验和来保证数据在传输时是完整地。检查块是否合法的工作被分给了每一个chunkserver自行完成

诊断工具

和很多的系统一样,GFS几乎是不可调试的。因此大量的log记录是必须的,在系统设计中对几乎所有的操作都记录一次本地log能快速定位和重现到问题,使用log的好处无需多言

测量

好吧,虽然我知道不看measurements是一个非常不好的习惯,但是GFS太过于成功以至于它的伟大无需多言

这部分的内容通常都比较无聊,但是作为一篇科研论文却不可避免地要涉及。。。

经验

这部分google总结了在设计GFS的经验,主要是硬件和操作系统相关的话题

比如最开始他们使用硬盘因为对Linux的支持不好,很容易出现错误,这促使他们引入了校验和机制来检查磁盘写入是否正确(本来操作系统对于磁盘的写入应该是可信的,理应不需要用户程序来承担检验磁盘是否正确写入的责任)

在没有实现checkpoint机制之前,master维护一个超大规模的log文件。在Linux 2.2 内核下,fsync函数的效率受限于文件大小而不是更改区域大小,因此随着系统的使用,日志的追加变得十分缓慢,这催生了checkpoint机制。不过好在这个问题在迁移到Linux 2.4之后消失了

接着提到一个Linux的问题,似乎是mmap为了维护映射区域而不得不面对的问题。当负责磁盘管理的线程正在将磁盘中的数据page in到内存中时,会持有一个锁但该page in会比较费时,长期持有该锁导致网络线程无法通过mmap来映射要写入的内容。因此造成了客户端的timeout(大概是这意思吧)。工程师将mmap改为pread修正了这个问题(不知道区别在哪)

google写到,虽然Linux的一些缺陷为他们的工作带来了一些烦恼,但是Linux代码的可用性(即开源)帮助它们不断地探索和理解系统行为(即可以通过调试内核的方式来寻找系统瓶颈,阴阳了一下那些不开源的操作系统)。google也在持续改进Linux内核并和社区共享这些成果(respect!)

根据jyy所说,至少在这篇论文发表前的内核还是很简单的。直到像google这样的大公司下场做云计算,从2.6末期开始内核代码量狂飙。不过总归是好事(除了让初学者几乎难以理解linux以外

Lines of Code

相关工作和总结

这两个部分就是对全文做了个总结,重述了一下GFS突出的几个设计特点。比如假设硬件不可靠,为google的工作负载定制的接口,单一master等等

事实证明这种从自身的工作负载来设计系统的方式是十分有效的,像AFS,NFS那样专注于构建一个通用的分布式文件系统往往是天方夜谭(没有批评的意思),像这样认真地分析一个系统可能会面临的工作负载,定制适合这些负载的系统才是正道