MIT6.824分布式系统(paper2)-GFS

1,036 阅读12分钟

本文是对MIT6.824课程中GFS论文的一个总结,在面对大数据量的存储问题时,分布式往往是普遍的一种解决方案,通过将数据写入不同的“机器”,可以较好的克服单机缺陷,但也引入了部分复杂度和一致性问题。而在GFS架构提出的年代,Google内部已经通过应用GFS取得了比较好的效果,同时这种架构和思想也对后续各大公司和部分开源的分布式存储组件都有着较重要的意义。


构建分布式文件系统的难点

在8.624课程中提到过,当数据量增长比较迅速的时候,如果对海量数据进行存储,并且要保证一定的读写性能是比较困难的。具体体现在:

  1. 首先我们构建分布式存储系统的目标是提高性能,当单机数据量比较大的时候,我们需要对数据进行分片(sharding)

  2. 但是进行分片后,数据存放在不同的服务器节点上,那么如果某些服务器宕机,就需要系统拥有容错机制。

  3. 一般而言为了保证系统具有容错的机制,需要将数据进行复制replication,并且把副本放置在多台服务器上。

  4. 那么多个副本也会带来数据一致性的问题。

  5. 而为了解决数据一致性,往往会导致性能下降。

以上阐述的难点导致了这些问题构成了一个闭环结构,也就证明了实际中设计的分布式系统需要权衡好数据一致性、读写性能等方面之间的关系。而GFS就对上述场景作了比较好的综合,首先将介绍下GFS针对的场景。

GFS针对的场景

GFS主要针对如下场景的系统进行设计:

  • 系统由价格上并不昂贵并且经常出错的机器(有纠错机制)构成

  • 系统主要存储大文件(100MB以上)

  • 读写性能中读操作包含大数据量的流式读(1MB)和小数据量的随机读(1KB),写操作连续写(追加写)占绝大部分。

  • 高代宽(比低延时重要)

GFS架构

如论文中描述,GFS架构如下图所示:

image.png

从图中可以看出,整个架构包含如下几种角色:

  • 单Master:master节点主要维护整个文件系统的元数据(metadata),并且master知道一个文件被分成了哪几个chunk以及这些chunk存储的位置。除此之外,master还负责chunk的迁移,再平衡和GC。同时,master也需要保持和chunkserver的沟通(通过心跳机制)来判断其状态。

    元数据

    master节点保存的元数据主要包含三部分

    • namespace和文件名。

    • 文件名和其chunk handles数组的映射。

    • chunk handles对应的文件chunk的版本号,chunkservers的列表,primary和租约

      在这其中,namespace、文件名、文件名及其chunk handles数组映射、版本号都是需要进行持久化到磁盘的,而其他类似于primary等部分因为可以频繁改变因此是在master启动之后询问chunkservers来得到的。此外,持久化都是通过写入系统operating log实现的,这是因为log多为追加操作,并且由于GFS内置了checkpoint,可以在需要恢复某种状态的时候达到更快的速度。

  • Chunk

    每一个存储在GFS的文件都会被分成固定大小(默认64MB)的chunk,并且chunk创建时,都会有一个全局唯一且不可变的标识符(chunk handle),并且为了保证容错性,每一个chunk还会有3个副本,分别存储在不同的chunkserver上。

  • Chunkserver

    是实际存储chunk的物理机(master不存储实际数据)。

  • Client

    主要负责向master询问数据存放位置,根据master返回回来的信息想具体的chunkserver进行数据读写操作。

    client和chunkserver都不会缓存chunk数据,这是为了防止数据产生不一致,client只缓存从master返回的元数据信息,并且缓存是通过LRU形式实现的

GFS读写流程

读流程

读流程主要分为以下几步:

  • 首先client要先将(文件名, offset)转化为(文件名,chunk index)发送给master继续查询

    chunk index = offset % len(chunk hanles)

  • master在自己的内存中查询chunk对应的chunk handler和chunk所在位置,返回给client

  • client缓存元数据,并通过返回的chunk所在位置利用文件名+chunk index作为key去chunkserver进行查询(这里通常是到距离自己网络上最近的chunkserver去取数据)

    如果读取的数据超过了一个chunk,GFS可能会调用一个库函数,这个库函数会识别出这是一次多个chunk数据的访问,因此会把这个请求变成2次分别请求不同chunk index的读取请求

写流程

  • 首先,在写流程中首先要清楚一个概念叫做租约(Lease)。租约的出现是因为如果当大量的并发操作同时访问单master节点时,master可能是系统性能的瓶颈之一,因此为了避免这一情况,master将找到一次chunk请求中chunkserver,授予其一个租约(有时间限制),则该chunkserver被称为primary,其余拥有该chunk副本的chunkserver被称为secondary。那么以后关于同一个chunk的请求访问就可以直接打到对应的primary上,使得master释放了部分压力。

  • 同时,在授予了某个chunkserver租约后,master会增加版本号并写入磁盘,接着会向primary和secondary分别发送消息,即各自的身份和最新的版本号。在租约的有效期内(默认60s),所有的写操作都由primary来负责,读操作读任意一个副本即可。此外租约到期后master会重新进行分配,同时primary也可以在写过程中申请延长租约时间。

接下来我们来看具体的写流程,如下图所示,总共分为7步:

image.png

  1. client向master进行询问primary和secondary。如果此时并没有primary,master会选择一个chunkserver授予租约,使其变为primary。

  2. master返回了primary和secondary的信息,client进行缓存,如果primary信息失效了才会继续请求master

  3. client会把某个要追加的内容发送给每一个chunkserver,接着,这些chunkserver会把数据先缓存到LRU cache中

    ---为什么不直接写到磁盘中?我猜是因为直接刷磁盘比较消耗资源,而且因为每一个chunk其实是64MB,相对而言文件较大,那么对于连续写的话,频繁的执行fsync操作开销太过频繁,因此先保存在cache中,并且由于一份chunk数据会有多份副本,那么如果多个副本都放到了对应的chunkserver上,才有必要进行下一步的实际写入操作

    ---并且将数据写入到缓存的过程中是通过pipeline的形式实现,并非是分别进行写入,文章中给出的理由是如果相较于client分别写入,client只需要执行一次写入过程,进而就可以去处理其他工作,这样可以更好的利用每台机器的代宽

    ---使用pipeline的方式在文中给出了数值分析,在两台机器通过TCP连接进行传输数据时,假设数据分成了R个副本,整体的传输时延为L,且数据总体为B比特,那么总时延=B/T+RL,其中T代表了网络吞吐量。那么常规情况下如果L远小于1ms且网络连接速度为100Mpbs(T)时,理想情况下,1MB的数据被传递出去大约是80ms

  4. 一旦client知晓了所有的chunkserver都拿到了数据,那么client会发送请求告诉primary发送写请求。

    primary可能会收到很多client的写入请求,primary会根据顺序执行

  5. primary写完以后会把写请求和顺序转发给所有的secondary,让他们按照同样的顺序进行写入。

  6. secondary写完以后会返回状态给primary

  7. primary最后返回给client报告其写入是否成功,如果失败,将回到第3步重新进行写入。

一致性模型

GFS的一致性模型是弱一致性的,也就是说GFS并不能保证每一个chunk的副本都是相同的。

弱一致性的体现

首先在课堂上,Robert教授用写操作阐述这种弱一致性,如下图所示:

image.png

从图中可以看出,尽管在写入内容为B的副本经过重试得以成功,但是每个chunkserver的从低地址开始数第二块(offset=1)的内容是没有办法达到一致性的,并且chunkserver里包含了重复的内容:我们可以注意到,当client第一次写入B内容的chunk副本失败时,如果这个时候另外client来分别查询chunkserver2中offset = 1(value = B)和chunkserver3中offset = 1(value = X)的内容时,可以明显发现数据是不一致的,因此这就是不一致性的体现。

更一般化来说,为了更好的阐述GFS的一致性模型,我们需要将写入的种类分成两种,一种是写入,另一种是record append,通过Google,我们可以得知这两种写入不同的特点为:

A write causes data to be written at an application-specified file offset. A record append causes data (the “record”) to be appended atomically at least once even in the presence of concurrent mutations, but at an offset of GFS's choosing

翻译过来就是写入操作是对固定的offset的内容进行write,而追加则是在并发存在的条件下自动的在最后一条offset记录后面继续写入。

因此可以总结为:

  • 对于写操作:某些副本可能成功,有些副本可能失败,那么副本之间就会不一致。
  • 对于追加操作:尽管会重试,但是会留下特定offset位置的数据是永久性的不一致,并且使得副本可能包含重复内容(例如chunkserver1中,有两个B)。

GFS一致性模型

论文中所提到的一致性模型如下图所示:

image.png

图中有几个概念需要阐述下,分别是:

  • defined:一个文件区域在经过多次操作后,client能看到所有的数据变更。
  • consistent:所有的client不论读取文件的哪个chunk副本,所得到的结果都是一致的。

总结Table1中的内容如下:

  • 对于master维护的元数据而言,可以通过设置锁来保证其一致性。
  • 对于普通的文件读写而言
    • 没有并发情况下,写操作没有冲突,因此是defined
    • 存在并发的情况下,写操作冲突,最终是consistent
    • 顺序写和并发写record append时可以保证defined,但是各区域之间可能会有不一致的区域
    • 如果失败副本之间的值会不一致。

那么写副本失败后产生的这种异常情况就需要进行弥补,思路有:

  • 需要让Primary重复探测请求
  • Secondary必须执行来自Primary要求的操作,不能返回错误
  • 在Primary确认所有的Secondary完成操作之前,其他读Secondary的操作不能返回结果
  • ...

快照

GFS通过创建一个文件或者目录树的方式来进行备份,其中备份的方式是写时复制。

一般而言,快照的建立方式如下:

  • master接收snapshot请求
  • master撤销即将要做snapshot操作的chunk的租约(暂停了写操作)
  • master将操作记录通过operation log的方式写入到磁盘
  • master将源文件和目录树的metadata进行复制,新创建的快照文件指向和源文件相同的chunk

容错性

  • 快恢复性:master和chunkserver都可以在几秒内恢复状态和重启
  • chunk副本:通过将chunk数据复制到多台机器上
  • master副本:master往往也需要被复制一份来保证可用性(shadow-master)

数据完整性

每一个chunkserver都是用checksum来检查存储数据的完整性的。

每个chunk按照64kb进行划分,划分之后每一个块(64kb)都对应一个32位的checksum,这个值会写入到chunkserver的内存中,并且随着用户数据一起进行持久化操作,每次用户操作之前都需要通过校验,校验通过后结果才会返回给客户端。

GFS缺点

GFS最严重的缺点就是单master:

  • 随着文件数量的增多,master所要维护的metadata也逐渐增大,尽管可以通过增加内存的方式解决,但是内存增加总有上限
  • master节点还需要承受很多客户端的连接请求,但往往master的cpu并不支持一秒内如此多的连接请求,特别是这些请求里可能还包括一些写入请求。
  • 其次,由于GFS整体是弱一致性,就会造成类似于副本写入失败后产生不一致等的缺点
  • 如果master发生故障需要人为干预,而不是自动修复。

上述众多问题就引出了6.824这门课接下来的内容,就是分布式共识算法。


参考文献

  1. gfs论文
  2. gfs2.0论文
  3. zhuanlan.zhihu.com/p/354450124