Google File System(GFS)是一个经典的面向大规模数据的分布式文件系统。它具有高性能、可拓展、可靠的特性。GFS在分布式领域的江湖地位相信不用多谈。BigTable,MapReduce和GFS向来有谷歌三驾马车之说。今天就来简单聊聊GFS的系统架构、以及谷歌针对Fault-tolerance、Consistency、Garbage collection、Balance、Recovery等一系列问题是如何提出合理解决方案的。

一、系统架构

GFS系统的参与者有包括了发起读写请求的客户端Client、唯一负责记录状态(naming & mapping)和调度的主服务器Master和多个存储数据和进行读写(data)的块服务器Chunk Server。如下图所示:

grafana 架构 gfs架构_grafana 架构

数据存储形式

GFS文件数据会被分割为多个块(Chunk),Chunk的大小为64MB,Chunk会被存储在Chunk Server中。Chunk的大小较大,这样设计有三个好处,一是减少Client向Master通信的次数,二是client 能够对一个块进行多次操作,这样可以通过与 chunkserver 保持较长时间的 TCP 连接来减少网络负载,三是减少metadata的总大小(metadata的概念后面会提到)。但是选取大chunk也有两个缺点:一是chunk位置分配是懒惰策略,碎片空间过大,二是并发客户端获得大量流量可能会导致读写性能跟不上。

一个Chunk通常会被备份到多个ChunkServer里默认3个副本)。Chunk的副本之间也有主/次之分(primary and secondary),后面的内容会讲到。

每个Chunk被有一个唯一且大小为64bit的Chunk Handle所标识。Chunk Handle在创建的时候被Master自动分配。通过Chunk Handle和读取字节的范围,Chunk Server能定位到具体读写chunk data的位置。

Master节点

Master节点负责了状态保存、所有对Chunk和ChunkServer的一系列调度。

元数据

元数据Metadata是Master用于记录各个Chunk元信息的数据结构。元数据包含了三种数据:文件+Chunk的namespace,从文件到Chunk的mapping以及Chunk的副本位置。元数据保存在Master的内存中。

内存中的数据面临断电丢失的风险,所以Master会将namespace和mapping变化写入operation log文件。log文件包含了操作顺序的时间线。GFS会复制多份log在不同机器上,且只有log写入到本地和远程机器的硬盘之后,才会应答客户端。

为了加快断电重启的恢复速度,Master定时将全量的mapping和namespace写入checkpoint文件中,这样reboot的时候只需要载入checkpoint加少量的增量log文件即可。

Chunk副本在ChunkServer中的位置不需要持久化,在Master重启后会询问各个ChunkServer得到相应Chunk的位置。除此之外,master会定期向各个ChunkServer请求HeartBeat信息来实时更新Chunk位置。

快照

快照操作几乎在瞬间构造一个文件和目录树的副本,同时将正在进行的其他修改操作对它的影响减至最小。GFS使用写时复制copy-on-write技术来实现snapshot。

当master受到一个snapshot请求时,它首先将要snapshot的文件上所有chunk的lease收回。取消lease的目的是让master触法lease分发功能,从中嵌入建立snapshot的逻辑。

副本被撤销或终止后,master在磁盘上登记执行的操作,然后复制源文件或目录树的metadata以对它的内存状态实施登记的操作。这个新创建的snapshot文件和源文件(其metadata)指向相同的chunk(即chunk的引用计数+1)。

Snapshot完成,client第一次向chunk写的时候,它发一个请求给master以找到拥有lease的primary副本。Master注意到该chunk的引用记数比1大,它延迟对用户的响应,选择一个chunk handle,然后要求每一个拥有该chunk的副本的chunkserver建立拷贝,对这三个拷贝中的一个设置lease,返回给client。

client在得到回复后就可以正常写这个chunk,原chunk将保存为快照。

namespace管理

为了保证master操作的顺序正确,master在进行读操作前需要获得所有父目录的读锁,以及当前读写目录的读写锁。

Chunk Server

数据的修改

数据的修改mutation一般有两种形式:

  • write:直接修改某个chunk中对应offset位置,某个range内的数据。
  • record append:某个Client直接在数据末尾添加。record append允许并发,且保证了append操作的原子性。(这里文章用的是at least once等级的原子性,个人理解在并发场景下,各个client都需要至少一次的原子操作,确定append的位置和范围,然后多个client可以同时append互不干涉)

一致性与确定性

GFS希望保证数据在mutation前后的确定性(defined)和一致性(consistency)。consistency指的是对于同一个trunk,所有client看到的所有replica都是完全相同的。defined首先满足一致性,其次client对写入内容的修改是可见的(个人理解,client能获悉所有replica中的mutation具体执行内容和顺序,执行内容和顺序也必须保持多副本一致)。

对于不同的mutation,相应文件的一致性和确定性状态如下

grafana 架构 gfs架构_Server_02


首先,文件write直接改变chunk内容,串行的write一般都是确定的。但是对于并发请求,可能client无法理解某一次mutation具体write了哪些数据;对于文件的append操作,无论是串行并发与否,append都是在客户端认为的文件结尾偏移量写入,此外,GFS可能会在期间插入padding或者其他重复的记录项。这导致了少部分数据显得不一致。

租约Lease与主块Primary

为了令系统在弱一致性的前提下拥有defined的特性,GFS提出了使用租约和主从块技术来串行化所有chunk的修改。

chunk生成之时,master会自动为其某一个副本生成租约lease,拥有lease的chunk副本被称为主块primary chunk,其余副本被称为secondary chunk。primary会对chunk的所有操作和变更序列化,所有的副本遵循primary修改的序列进行修改。lease有一定的租期(默认60s),在租期内有效,一般来说,只要chunk被改动,primary就可以向master申请延长lease租期,该请求由HeartBeat信息传递。

这里有个特殊场景,就是primary chunk所在的chunkserver宕机,master发现lease租期已知,而该server无响应后,便会发放新的lease给另一个server。当宕机的server恢复后,手头的lease已经超时,失去效力,此时应认为拥有新lease的那个server被设定为primary。

具体流程

  • 读数据
  1. client发送filename和chunk index(通过offset求得)给master
  2. master返回对应chunk的handle,以及chunk replica的location和version给client
  3. client将filename和chunk index以key缓存
  4. client通过location信息请求chunkserver,参数为chunk index和offset
  5. chunkserver读文件
  6. client接收chunkserver的数据
  7. client的reader验收数据,通过checksum丢掉padding和record fragment,并用独特标识符去过滤重复record
  • 写数据
  1. client请求master关于某个chunk的primary replica及其位置
  2. master返回primary的标识符和位置,以及其他secondary replica的位置
  3. client将所有数据推送到replica中,replica中数据以pipe的方式传递,顺序沿着一个chunkserver chain传递,优先选择最近的chunkserver。(数据流与控制流分离) chunkserver将接收到的数据保存在LRU缓存
  4. 所有replica接受到数据后,client向primary发出写请求(包含所有replica的标识符)。primary对所有操作分配连续的序列号,并以序列号的顺序本地执行
  5. primary将这个顺序传递到secondary中,secondary replica以相同顺序执行写请求,以保证写是defined的
  6. secondary向primary发送任务已完成的消息
  7. primary将执行结果返回client。如果结果为failure,则client需要重新发起step 3- step 7的步骤