MIT6.824 Google File System论文学习

论文阅读

场景引入

为什么要设计这么一个系统?

  • 组件故障是常态不是异常.因为集群中包含成百上千廉价的机器,很容易导致各种问题导致数据丢失或机子不可用.因此持续监控,错误侦测,故障容忍和自动恢复必须全面覆盖整个系统.
  • 要处理的文件非常大,如果分成数十亿个小文件处理,会对文件索引带来非常大的挑战
  • 大部分文件都是在append新数据,不存在随机写数据,那么如何在append中保证原子性以及对其做性能优化是重点.
  • 向应用提供类似文件系统的API,可以提高灵活性

总结来说就是:
主要负载为大容量连续读、小容量随机读以及追加式的连续写
支持高效且原子的文件追加操作
系统应选择高数据吞吐量而不是低延时

设计模式

假设与接口部分都是废话,我就直接粘贴复制了

任何系统都是根据实际需求设计的,而在设计过程中,则需要抽象出实际场景的特点并进行相应的假设,从而根据这些假设推导出系统所必需的功能。GFS是基于以下几个假设设计的:

  • 系统是构建在很多廉价的、普通的组件上,组件会经常发生故障。它必须不间断监控自己、侦测错误,能够容错和快速恢复。

  • 系统存储了适当数量的大型文件,我们预期几百万个,每个通常是100MB或者更大,即使是GB级别的文件也需要高效管理。也支持小文件,但是不需要着重优化。

  • 系统主要面对两种读操作:大型流式读和小型随机读。在大型流式读中,单个操作会读取几百KB,也可以达到1MB或更多。相同客户端发起的连续操作通常是在一个文件读取一个连续的范围。小型随机读通常在特定的偏移位置上读取几KB。重视性能的应用程序通常会将它们的小型读批量打包、组织排序,能显著的提升性能。

  • 也会面对大型的、连续的写,将数据append到文件。append数据的大小与一次读操作差不多。一旦写入,几乎不会被修改。不过在文件特定位置的小型写也是支持的,但没有着重优化。

  • 系统必须保证多客户端对相同文件并发append的高效和原子性。我们的文件通常用于制造者消费者队列或者多路合并。几百个机器运行的制造者,将并发的append到一个文件。用最小的同步代价实现原子性是关键所在。文件被append时也可能出现并发的读。

  • 持久稳定的带宽比低延迟更重要。我们更注重能够持续的、大批量的、高速度的处理海量数据,对某一次读写操作的回复时间要求没那么严格。

接口:
GFS提供了一个非常亲切的文件系统接口,尽管它没有全量实现标准的POSIX API。像在本地磁盘中一样,GFS按层级目录来组织文件,一个文件路径(path)能作为一个文件的唯一ID。我们支持常规文件操作,比如create、delete、open、close、read和write。

除了常规操作,GFS还提供快照和record append操作。快照可以用很低的花费为一个文件或者整个目录树创建一个副本。record append允许多个客户端并发的append数据到同一个文件,而且保证它们的原子性。这对于实现多路合并、制造消费者队列非常有用,大量的客户端能同时的append,也不用要考虑锁等同步问题。这些特性对于构建大型分布式应用是无价之宝。快照和record append将在章节3.4、3.3讨论。

架构

我们重点来看看架构部分

在这里插入图片描述GFS的组件:

  • Master

  • Client

  • Chunkserver

  • 存入到GFS的文件被划分为很多chunk,每个chunk大小是64MB,并且在创建的时候会分配一个全局唯一的chunk句柄.
    chunk句柄:一个不变的,全局唯一的64位ID

  • chunkserver在本地磁盘上,把chunk存储为linux文件,然后就按照chunk handle和byte range(字节范围)来读取磁盘上的chunk,同时为了保持高可用,默认复制三分chunk到不同的chunkserver

  • master主要负责维护所有文件系统的元数据:包括namespace,文件到chunk的映射与chunk的位置,它页负责整个系统的一些协调:例如chunk的租赁管理,孤儿chunk的回收,以及chunkserver之间的chunk迁移. 与很多主从分布式架构一样,master会周期性向每台chunkserver发送心跳信息,以发号施令或者收集chunkserver状态

  • client与master和chunkserver的交互:

    • client只在获得元数据的时候与master交互
    • 真实的数据操作发生在client与chunkserver的交互
  • client与chunkserver不会存储文件数据,master会存储metadata.因为chunkserver已经把chunk缓存在本地,Linux在OS层面会提供buffer缓存来保存频繁访问的文件.

针对上图,我们看看文件读操作是怎样进行的?

  • 应用程序调用GFS Client提供的API,表面要读取的文件名,偏移以及长度
  • GFS Client将偏移按照规则翻译为chunk序号,发送给master
  • master把chunkid与chunk的副本位置告诉给GFS Client,客户端会缓存这个信息,使用GFS文件名+chunk序号作为key,那么相同的读就可以利用这些缓存了,不用再进行一次交互.而且,
  • GFS client向最近的持有副本的Chunkserver发出读请求,请求中包括chunk id 与 范围
  • ChunkServer读取相应的文件,然后将文件内容发给GFS Client

以下是实现的一些细节

单一的master

目的:简化架构设计,单一master能够放心使用全局策略执行复杂的chunk布置,指定复制决策等.然而我们必须在读写过程中尽量减少对其依赖,避免其称为bottleneck

可以这么说,client是不向master读写文件的,只会询问master自己应该访问哪个chunk server

此外,集群中还会有只读功能的Shadow Master,它们会同步Master的状态变更,主要用来分担Master的读操作压力,它通过读取Master的某个副本操作日志来使自己与Master保持同步.

chunk size

chunksize设置为64MB,比典型文件系统其实要大很多的,为什么要这么做?

  • chunk副本在chunkserver被存储为一个普通的Linux文件,只有必要的时候才去扩展,可以减少内部碎片导致的空间浪费
  • chunk变大了,那么chunk的数量就会减小,可以减少客户端与master的交互次数,具体体现在可以对同一个chunkserver保持长期的TCP链接来降低网络的负载,就是降低了客户端与Master通讯的频率啦
  • 同时也可以增大客户端进行操作的时候,这些操作落到同一个Chunk上的概率
  • chunk的数量减少了,相应减少了master上元数据的数量,因此可在内存中直接缓存元数据,减少了其内存存储的压力
metadata

上面提到了元数据,master主要存储三种类型的元数据:

  • 文件与chunk的命名空间
  • 从文件到chunk的映射
  • 每个chunk副本的位置

其中前两种数据会持久化存储,通过记录操作日志存储在master的本地磁盘并且还会复制到远程的机器上,这样可以允许我们更加可靠地更新master状态,不会因为master的登机导致数据不一致

master在启动的时候会询问每个chunkserver以获取他们各自的位置信息,新加入chunkserver加入集群的时候也会这样,

内存中的数据结构

元数据存储在内存中,所以master可以很快执行元数据操作,而且可以通过简单高效第在后台周期性扫描整个元数据状态.

周期性扫描的作用:

  • 用于实现chunk垃圾回收,用于chunkserver故障导致的重新复制
  • 为了均衡各个机器负载与磁盘使用率而进行chunk迁移
chunk的位置

master不会持久化存储那个chunkserver有哪些chunk副本,而是在master启动的时候拉取chunkserver上的信息,并且启动之后也会周期性拉取.master能够保证自己的信息时刻是最新的,因为其控制了所有的chunk布置操作,并且会常规地通过发送心跳信息来监控chunkserver状态.

操作日志
  • 操作日志是什么:
    对重要元数据变更的历史记录
    不仅因为它是元数据唯一的持久化记录,并且他要为并发的操作定义顺序,具体来说是当各文件,chunk以及他们的版本创建的时候,都会为他们创建时候的逻辑时间打一个唯一的标识.

  • 怎样可靠地存储操作日志:
    复制到多台远程机器,知道日志被刷写到本地磁盘以及远程机器之后才会对客户端可见,master会捆绑多个日志记录一起刷写,以减少刷写和复制对整个系统吞吐量的冲击.

  • master怎样通过操作日志来恢复其元数据状态?
    注意,日志数量多了,会影响master的启动时间,所以当日志增长超过一个特定的大小的时候,就会执行存档,其不需要从0开始回放日志,仅需要从本地磁盘装载最近的存档,并且回滚到上次执行的日志.存档利用类B树的结构进行存储恢复原数据的时候,只需要最后的存档以及其后产生的日志.

数据一致性的保持

文件命名空间变化(文件创建)是原子的,只有master能够处理这种操作:

  • master提供了命名空间的锁机制,保证了原子性与正确性
  • master的操作日志为这些操作定义了一个全局统一的顺序

但是客户端对数据的更改是uncertain的,因为chunk会有很多的副本,假如不同的client对同一个chunk的不同副本进行修改,怎么保持一致性呢?

答案是:GFS使用租赁机制来解决这个问题

首先,当文件的某一部分被修改后,可能进入以下三种状态的其中之一:

  • Inconsistent
    客户端读取不同的副本可能会读到不同的内容
  • Consistent
    客户端读取不同的副本可能会读到相同的内容
  • Defined
    所有客户端都能看到上一次修改的所有完整内容,而且这一部分文件是一致的

修改之后,一个文件的当前状态将取决于这次修改的类型以及修改是否成功

  • 失败的写入操作会导致不一致
  • 如果写入操作成功并且没有和其他的并发写入操作发生重叠,那么这部分的文件是确定的
  • 如果若干个写入操作并发执行成功,那么这部分文件会是一致的,但不确定,因为不是所有客户端都看到上一次修改的完整内容

关系整理如下:
在这里插入图片描述GFS支持的文件数据包括两种:

指定偏移值的数据写入&数据追加

  • 数据写入:

    指定的数据会被直接写入到客户端指定的偏移位置,覆盖原有的数据.GFS并未为该操作提供太多的一致性保证.所以如果不同的客户端并发地写入同一块文件区域,操作完成后这块区域的数据可能由各次写入的数据碎片组成,从而进入不确定的状态.

  • 数据追加:
    append操作是原子的并且是至少一次的,操作完成后,GFS会把实际写入的偏移值返回给客户端,这个偏移值是所写数据的确定的文件区域的起始位置

那怎么解决这个多个客户端同时修改可能会导致的碎片问题呢?
可行的方案是把修改操作串行化.在分布式环境下,不仅需要每个副本串行执行修改,而且还要保证每个副本串行的顺序要是一致的.好我们回过头讨论租赁机制,它就是GFS对此的对策.

租赁机制

首先这里引入了变异这个术语:这个操作会修改chunk的数据内容或者元数据,比如一个写操作或者一个append操作,对chunk的任何变异都需要实施到这个chunk的各个副本上,于是我们提出"租赁"机制,来维护一个跨副本的一致性变异顺序.

master会在chunk的各个副本中选择一个,授予其租赁权,这个副本称为首要副本, 其他的为次级副本

首要副本负责为chunk的所有变异排出一个严格的顺序,所有副本在实施变异的时候都要遵循这个顺序.

过程总结为:

  • 首先由master选出首要和次要的副本
  • 首要副本为这些变异制定实施序号
  • 首要和次级副本严格按照首要副本指定的序号来实施变异
GFS数据写入过程

在这里插入图片描述1.Client向master请求chunk的副本信息,以及询问哪个副本是primary replica
2.master回复client,并且client会把这些信息缓存在本地(时间局部性)
3.黑箭头:Client将data链式推送到所有副本
4.Client通知Primary提交
5.primary在自己成功提交后,通知所有secondary提交
6.secondary向primary回复提交的结果
7.primary向client回复提交结果

这种架构的设计能够分开数据流与控制流,即客户端先向Chunk Server提交数据,再将写请求发送给Primary 可以最大化利用每个机器的网络带宽,避免网络瓶颈和高延迟链接,以此来最小化推送延迟.

怎么理解这个线性数据管道?

客户端会把数据上传到离自己最近的副本,这个副本在接收到数据后会转发给离自己最近的另外一个副本,这样递归下去,不仅可以利用每一台机子的带宽,还能减少传输的时间

GFS的垃圾文件回收

当个文件被删除的时候,GFS不会立刻删除数据,而是在文件与chunk两个层面上lazy地对数据进行移除.

具体做法:

  • 用户删除文件的时候,GFS不会从Namespace直接移除这个文件的记录,而是把这个文件重命名为另外一个隐藏的名称,并且带上删除时候的时间戳.
  • 前面提到,master会周期性扫描namespace,当它发现那些已经被删除了很长时间的文件(如3天),这个时候master才会真正将其从Namespace中移除,真正执行死刑
  • 所以在死缓的时候,客户端仍然可以利用这个重命名后的隐藏名称读取这个文件,甚至再次把其重命名,进而来撤销删除操作.
  • Master在元数据中有维持文件与Chunk之间的映射关系,当Namespace中的文件被移除之后,对应Chunk的引用计数会自动-1,如果Master在周期性扫描元数据的过程中,Master会发现引用计数为0的Chunk,这个时候Master会从自己的内存中移除这些与Chunk有关的元数据.
    当Chunk Server和Master进行的周期心跳通信中,Chunk Server会汇报自己所持有的Chunk Replica,这个时候Master会告知Chunk Server哪些Chunk已经不在元数据中,Chunk Server可以自行移除相应的Replica.

采用这种删除机制的好处:

  • 删除chunk操作实际上是由chunk server主动进行的,因为有时候可能创建操作在某些chunk Server成功了,在某些ChunkServer失败了,导致chunk Server可能存在一些master不知道的副本,而且有时候master发送的删除操作可能失败对吧,需要重传
  • 这种删除机制与Master日常的周期扫描是合并在一起的,可以减少资源损耗.
  • 相比于直接判死刑,死缓可以避免用户误操作的问题
高可用机制
Master节点的高可用

前面我们提到,Master 会以先写日志(Operation Log)的形式对集群元数据进行持久化:日志在被确实写出前,Master 不会对客户端的请求进行响应,后续的变更便不会继续执行;除外,日志还会被备份到其他的多个机器上,日志只有在写入到本地以及远端备份的持久化存储中才被视为完成写出。

在重新启动时,Master 会通过重放已保存的操作记录来恢复自身的状态。为了保证 Master 能够快速地完成恢复,Master 会在日志达到一定大小后为自身的当前状态创建 Checkpoint(检查点),并删除 Checkpoing 创建以前的日志,重启时便从最近一次创建的 Checkpoint 开始恢复。Checkpoint 文件的内容会以 B 树的形式进行组织,且在被映射到内存后便能够在不做其他额外的解析操作的情况下检索其所存储的 Namespace,这便进一步减少了 Master 恢复所需的时间。

为了简化设计,同一时间只会有一个 Master 起作用。当 Master 失效时,外部的监控系统会侦测到这一事件,并在其他地方重新启动新的 Master 进程。

除外,集群中还会有其他提供只读功能的 Shadow Master:它们会同步 Master 的状态变更,但有可能延迟若干秒,其主要用于为 Master 分担读操作的压力。Shadow Master 会通过读取 Master 操作日志的某个备份来让自己的状态与 Master 同步;它也会像 Master 那样,在启动时轮询各个 Chunk Server,获知它们所持有的 Chunk Replica 信息,并持续监控它们的状态。实际上,在 Master 失效后,Shadow Master 仍能为整个 GFS 集群提供只读功能,而 Shadow Master 对 Master 的依赖只限于 Replica 位置的更新事件。

ChunkServer的高可用

作为集群中的 Slave 角色,Chunk Server 失效的几率比 Master 要大得多。在前面我们已经提到,Chunk Server 失效时,其所持有的 Replica 对应的 Chunk 的 Replica 数量便会降低,Master 也会发现 Replica 数量低于用户指定阈值的 Chunk 并安排重备份。

除外,当 Chunk Server 失效时,用户的写入操作还会不断地进行,那么当 Chunk Server 重启后,Chunk Server 上的 Replica 数据便有可能是已经过期的。为此,Master 会为每个 Chunk 维持一个版本号,以区分正常的和过期的 Replica。每当 Master 将 Chunk Lease 分配给一个 Chunk Server 时,Master 便会提高 Chunk 的版本号,并通知其他最新的 Replica 更新自己的版本号。如果此时有 Chunk Server 失效了,那么它上面的 Replica 的版本号就不会变化。

在 Chunk Server 重启时,Chunk Server 会向 Master 汇报自己所持有的 Chunk Replica 及对应的版本号。如果 Master 发现某个 Replica 版本号过低,便会认为这个 Replica 不存在,如此一来这个过期的 Replica 便会在下一次的 Replica 回收过程中被移除。除外,Master 向客户端返回 Replica 位置信息时也会返回 Chunk 当前的版本号,如此一来客户端便不会读取到旧的数据。

数据完整性

如前面所述,每个 Chunk 都会以 Replica 的形式被备份在不同的 Chunk Server 中,而且用户可以为 Namespace 的不同部分赋予不同的备份策略。

为了保证数据完整,每个 Chunk Server 都会以校验和的形式来检测自己保存的数据是否有损坏;在侦测到损坏数据后,Chunk Server 也可以利用其它 Replica 来恢复数据。

首先,Chunk Server 会把每个 Chunk Replica 切分为若干个 64KB 大小的块,并为每个块计算 32 位校验和。和 Master 的元数据一样,这些校验和会被保存在 Chunk Server 的内存中,每次修改前都会用先写日志的形式来保证可用。当 Chunk Server 接收到读请求时,Chunk Server 首先会利用校验和检查所需读取的数据是否有发生损坏,如此一来 Chunk Server 便不会把损坏的数据传递给其他请求发送者,无论它是客户端还是另一个 Chunk Server。发现损坏后,Chunk Server 会为请求发送者发送一个错误,并向 Master 告知数据损坏事件。接收到错误后,请求发送者会选择另一个 Chunk Server 重新发起请求,而 Master 则会利用另一个 Replica 为该 Chunk 进行重备份。当新的 Replica 创建完成后,Master 便会通知该 Chunk Server 删除这个损坏的 Replica。

当进行数据追加操作时,Chunk Server 可以为位于 Chunk 尾部的校验和块的校验和进行增量式的更新,或是在产生了新的校验和块时为其计算新的校验和。即使是被追加的校验和块在之前已经发生了数据损坏,增量更新后的校验和依然会无法与实际的数据相匹配,在下一次读取时依然能够检测到数据的损坏。在进行数据写入操作时,Chunk Server 必须读取并校验包含写入范围起始点和结束点的校验和块,然后进行写入,最后再重新计算校验和。

除外,在空闲的时候,Chunk Server 也会周期地扫描并校验不活跃的 Chunk Replica 的数据,以确保某些 Chunk Replica 即使在不怎么被读取的情况下,其数据的损坏依然能被检测到,同时也确保了这些已损坏的 Chunk Replica 不至于让 Master 认为该 Chunk 已有足够数量的 Replica。

FAQ

为什么原子记录追加操作是至少一次(At Least Once),而不是确定一次(Exactly Once)?

要让追加操作做到确定一次是不容易的,因为如此一来 Primary 会需要保存一些状态信息以检测重复的数据,而这些信息也需要复制到其他服务器上,以确保 Primary 失效时这些信息不会丢失。

应用怎么知道 Chunk 中哪些是填充数据或者重复数据?

要想检测填充数据,应用可以在每个有效记录之前加上一个魔数(Magic Number)进行标记,或者用校验和保证数据的有效性。应用可通过在记录中添加唯一 ID 来检测重复数据,这样应用在读入数据时就可以利用已经读入的 ID 来排除重复的数据了。GFS 本身提供了 library 来支撑这些典型的用例。

ref

GFS的总结博客
paper

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值