Google File System论文翻译

Google File System论文翻译

这篇论文拜读过很多遍了,对于做分布式存储的人来说实在过于经典,读多少遍都不为过,感觉每次读的时候都能够获取到不一样的知识。虽然读过很多次,但是一直没有做读书笔记,这次会针对Google的三大经典论文分别做中文翻译以及阅读笔记。如有纰漏,请各位多多批评。

只翻译论文核心部分,后面Benchmark的部分就不做翻译了,请各位看官自己去认真阅读原版论文吧。

本篇为论文翻译,当然也是同时参考着别的大佬的中文翻译和google的英文原版才自己完成的翻译。下一篇会对这篇论文出一篇论文笔记,重点写一些论文中重点部分的内容理解。

摘要

​ 我们设计实现了GFS文件系统,这是一种面向大规模分布式数据密集型应用的,可伸缩的分布式文件系统。GFS文件系统虽然运行在廉价的硬件机器上面,但是其仍然能够提供容错能力并且为大量客户端提供高性能的服务。

​ 虽然GFS文件系统的设计目标与以前的许多文件系统有很多相同之处,不过我们的设计还是从自身的应用负载情况和技术环境的角度出发的。无论过去还是将来,GFS文件系统和早期的文件系统是有着明显不同之处的。因此我们重新审视了传统文件系统的设计方案并且探索出了完全不同的设计方案。

​ GFS文件系统成功地满足了我们对于存储的需求。GFS文件系统作为分布式存储平台已经广泛地应用部署于Google内部,存储着Google服务所产生和处理的数据,同时也应用于需要大数据集的研究和开发工作。截止到目前为止,规模最大的一个集群通过数千台机器具有的数千块硬盘提供了数百TB的存储能力,同时为数百个客户端服务。

​ 在本文中,我们提供了能够支持分布式应用的文件系统接口的扩展,讨论了我们设计的许多方面,最后列举了小规模Benchmark和真实生产系统中的数据。

1.简介

​ 为了满足Google日益增长的数据处理需求,我们设计并实现了Google File System。GFS和传统的分布式文件系统有很多相同的设计目标,比如性能、伸缩性、可靠性以及可用性。不过我们的设计还是从自身的应用负载情况和技术环境的角度出发的。无论过去还是将来,GFS文件系统和早期的文件系统是有着明显不同之处的。因此我们重新审视了传统文件系统的设计方案并且探索出了完全不同的设计方案。

​ 第一,组件失效被认为是常态,而不是意外事件。文件系统由成百上千台廉价机器组成,为其提供存储能力,同时被一定数量的客户端访问。这些廉价机器的数量和质量事实上导致了其中有一些机器在任何给定时间内都有可能失效无法工作,有些机器无法从它们的失效状态中恢复。我们遇到过很多问题,应用程序bug,操作系统bug,人为错误,硬盘、内存、连接器、网络、电源失效。因此,持续的监控、错误监测、灾难冗余以及自动恢复机制必须集成在GFS中。

​ 第二,以传统的标准来说,文件通常是巨大的。大小为数GB的文件非常普遍。每个文件通常都包含许多应用程序对象,比如web文档。当我们需要经常处理包含着数亿个对象的TB级别的快速增长的数据集时,采用管理数亿个KB级别大小的文件的方式是非常不合理的,尽管有些分布式文件系统能够支持这种方式。因此,我们的设计思路中的假设条件和参数,比如I/O操作和Block尺寸都需要重新考虑。

​ 第三,大多数文件的修改都是通过在文件尾部追加新数据的方式,而不是通过覆盖已有数据的方式。对文件的随机写在实际情况中几乎是不存在的。一旦写入完成后,文件只能够被读取,而且通常是顺序读取。大量的数据负责一下这些特性:数据分析程序扫描超大数据集;正在运行的应用程序生成的连续数据流;有些可能是存档的数据;由一台机器生成、另外一台机器处理的中间数据,这些中间数据的处理可能是同时进行的、也可能是后面才处理的。考虑到针对这种海量大文件的处理方式,在客户端缓存数据块是没有意义的,反而数据的追加操作是性能优化和原子性保证的主要关注点。

​ 第四,应用程序和文件系统API的协同设计对于提高整个系统灵活性是有很大好处的。例如,我们放松了对GFS一致性模型的要求,这样就减轻了文件系统对应用程序的严苛要求。我们设计了原子性的记录追加操作,从而保证多个客户端可以同时对同一个文件进行追加操作,而不需要额外的同步操作来保证数据的一致性。后面还会对这一点进行详细说明。

​ 目前Google已经针对不同的应用部署了多套GFS集群。最大的一个集群有着超过1000节点,超过300TB的存储空间,被不同机器上的数百个客户端频繁访问。

2.设计概述

2.1 设想

​ 在设计满足我们需求的文件系统过程中,我们的设计理念既有机会又有挑战。我们先前已经提及了一些需要关注的关键点,现在我们会对我们的设计目标展开详细描述。

  • 系统由很多廉价的组件构成,并且这些廉价组件失效是一种常态。系统必须能够持续监控自身的状态,检测、容忍并且能够恢复已经失效的组件。
  • 系统存储一定数量的大文件。预期会有几百万文件,每个文件通常在100MB或者更大。数GB大小的文件很常见,并且能够被有效地管理起来。系统同时也必须支持存储小文件,但是我们并不需要专门针对小文件进行优化。
  • 系统的工作负载主要由两部分读操作构成:大规模流式读取和小规模随机读取。大规模的流式读取通常一次读取数百KBs的数据,更多的情况是一次性读取1MB或者更多的数据。来自一个客户端的连续操作通常是读取一个文件中的连续区域。而小规模的随机读取通常是在文件某个随机的位置读取几KB的数据。如果应用程序对性能要求很高,通常是把小规模的随机读取进行合并后排序,之后按顺序批量读取,这样就避免了在文件中前后移动位置来读取文件。
  • 系统的工作负载还包含许多大规模、顺序的、数据追加方式的写操作。一般情况下写操作的数据大小和大规模读操作类似。数据一旦被写入之后便很少会被修改了。系统支持小规模随机写,但是效率很低。
  • 系统必须高效地、行为定义明确的实现多个客户端同时并发地追加数据到同一个文件尾部。系统中的文件多用于生产者-消费者队列或者多路文件合并。数百个生产者,每个生产者进程运行在一台机器上,同时对一个文件进行追加操作。使用最小的同步开销来完成对同一个文件的原子性数据追加操作是十分必要的。文件可以后续再进行读取,或者消费者在追加操作的同时进行读取操作。
  • 高速稳定的网络带宽远比低网络延迟重要。我们的目标应用程序绝大部分能够高速、大批量的处理数据,很少有程序会对单独的读写操作有严格的响应时间要求。

2.接口

​ GFS提供了一整套传统文件系统风格的API接口,尽管不是严格按照POSIX等标准API接口的形式实现的。文件通过分层目录的形式组织起来,由路径名称标识。我们的设计支持常用的操作,比如创建、删除、打开、关闭、读取、写入文件。

​ 另外,GFS也支持快照和记录追加操作。快照以很低的开销创建一个文件或目录树的拷贝。记录追加操作能够实现多个客户端对同一个文件同时进行数据追加操作,并且能够保证每个客户端的追加操作都是原子性的。这对于实现多路合并操作,生产者-消费者队列是非常有用的。因为多个客户端可以不需要在额外的增加同步锁,就可以实现并发地对同一个文件追加数据。我们发现这些类型的文件对于构建大型分布式应用是非常有价值的。快照和记录追加操作将在3.4和3.3节分别进行讨论。

2.3 架构

一个GFS集群包含一个单独的Master节点、多台Chunk服务器,并且同时被多个客户端访问,如图1所示。集群中的每一台机器通常都是普通的Linux机器,上面运行着用户级别的服务进程。我们可以很容易地将客户端和Chunk服务器部署在同一台机器上,不过前提是机器的资源是可以保证的。但是我们必须能够接受不可靠的应用程序所带来的稳定性的风险。

在这里插入图片描述

​ GFS集群中存储的文件都被分割成固定size的Chunk。在每一个Chunk建立的时候,Master服务器会给每一个Chunk分配一个不变的、全球唯一的64位Chunk标识。Chunk服务器把Chunk以Linux文件的形式保存在本地磁盘上,并且根据指定的Chunk标识和字节范围来读写chunk块数据。为了保证可靠性,每个块都会复制到多个Chunk服务器上。默认情况下,我们会对每个块存储到3个Chunk服务器,不过用户可以对不同的文件命名空间设定不同的复制级别。

​ Master服务器节点管理所有文件系统的元数据。包括命名空间,访问控制信息,文件到Chunk的映射信息,当前Chunk的位置信息。Master节点同样还管理着系统范围内的行为,比如Chunk租用管理,孤儿Chunk的垃圾回收,Chunk在Chunk服务器之间的迁移。Master节点使用心跳周期性地和每个Chunk服务器通信,发送指令以及接收各个Chunk服务器的状态信息。

​ GFS客户端代码被链接到每个应用程序中(其实就是作为第三方库被引入)。客户端代码实现了GFS文件系统API,应用程序与Master节点和Chunk服务器通讯以对数据进行读写操作。客户端和Master节点的通信只获取元数据信息,所有数据操作都是客户端直接和Chunk服务器直接进行通讯的。我们不提供POSIX标准的API,因此GFS的API调用无需深入到Linunx vnode层级。

​ 无论是客户端还是Chunk服务器都不对文件数据进行缓存。客户端缓存文件数据几乎是起不到什么作用的,因为大部分应用程序要么是通过流的方式读取一个巨大文件,要么就是需要处理的工作集太大,没有办法缓存。缓存问题没有纳入我们的考虑范围也简化了客户端和整个系统的设计实现(客户端会缓存从Master节点获取到的元数据)。Chunk服务器不需要缓存文件数据是因为Chunk会将数据存储到服务器的本地磁盘上形成文件,Linux系统本身就会把经常访问的数据缓存在内存中。

2.4 单一Master节点

​ 单一Master节点的设计大大简化了GFS的设计,并且使得Master节点有能力通过全局信息定位复杂的Chunk位置信息以及进行复制决策。当然,我们还必须注意,要尽可能地减少对Master节点的读写,避免Master节点成为整个GFS系统的瓶颈。客户端不会通过Master节点读写文件数据,相反,客户端会先向Master节点询问它应该联系哪台Chunk服务器。获取到Master节点反馈给客户端的元数据之后,客户端会将这些元数据信息缓存一段时间,随后的数据读写操作将直接和ChunkServer直接交互。

​ 我们通过图1说明简单的读取数据流程。首先,客户端将文件名和程序指定的字节偏移量,利用确定的Chunk大小,转换成文件的Chunk索引。随后,客户端把文件名和Chunk索引发给Master节点。Master节点会将相应的Chunk Handle和副本的位置信息反馈给客户端。客户端用文件名和Chunk索引作为key缓存这些信息。

​ 客户端随后会发送请求到其中一个副本服务器,一般情况下会选择物理距离最近的。请求中指明了Chunk Handle和字节范围。接下来在对这个Chunk块做读取操作时,客户端不需要再和Master节点进行通讯交互,直到客户端缓存的Chunk位置信息过期失效或者文件被重新打开了。实际上,客户端通常会在一次请求中查询多个Chunk块的信息,Master节点的反馈信息也包含了跟在被请求的Chunk块后面的Chunk的信息。这些额外信息在没有付出额外的代价下,避免了客户端和Master节点将会发生的数次通讯交互。

2.5 Chunk尺寸

​ Chunk size是系统设计中关键的参数之一。我们选择了64MB,这个尺寸远远大于通常情况下文件系统的Block size。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,并且只有在有需求时才扩大。惰性空间分配策略避免了因内部碎片导致的空间浪费,对于选择64MB的Chunk尺寸争议最大的就是内部碎片问题。

​ 较大的Chunk size有几个重要优点。第一,它减少了客户端和Master节点的通讯交互次数,因为只需要一次和Master节点的通信就可以获取Chunk位置信息,随后便可以对同一个Chunk进行多次读写操作,无需与Master节点进行多次通信。这种方式可以显著降低我们的工作负载,因为应用程序通常是连续读写大文件。即使对于小规模读写操作来说,客户端也可以轻松缓存数个TB的超大数据集的所有Chunk位置信息。第二,在一个大尺寸的Chunk块中,客户端通常更可能对一个块进行多次操作,这样可以通过与Chunk Server维持长时间的TCP连接来减少网络负载。第三,较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。这样可以使得我们把所有元数据全部保存到Master节点的内存中,在2.6.1小节中我们将会讨论元数据放在内存中给整个系统带来的好处。

​ 另一方面,即使采用了惰性空间分配策略,较大的Chunk尺寸也有一些劣势。小文件包含很少的Chunk,甚至只有一个Chunk。就会发生当多个客户端对同一个小文件进行多次访问,存储对应Chunk的ChunkServers就会变成热点服务器。实际上,热点还不是我们设计的系统的主要问题,因为我们的应用程序通常是连续读取包含多个Chunk的大文件。

​ 但是,GFS在第一次部署在批处理队列系统上时,还是出现了热点问题:一个可执行文件在GFS上存储为single-chunk文件,随后在数百台机器上同时启动。存储这个可执行文件的几台ChunkServer被数百台客户端并发访问导致系统发生过载。我们通过使用更大的复制参数因子来保存可执行文件、错开批处理队列系统程序的启动时间解决热点问题。一个潜在的长效解决方案是,允许客户端从其他客户端读取数据。

2.6 元数据

​ Master节点主要存储三种类型的元数据:文件和Chunk的命名空间、文件和Chunk的映射关系、每个Chunk副本的位置信息。所有元数据都保存在Master节点的内存中。前两种类型的元数据同时也会通过记录变更日志的方式保存在系统的日志文件中,日志文件存储在系统本地磁盘上同时日志文件会被复制到其他的远程Master节点上。采用变更日志的方式,使得我们可以简单、可靠地更新Master节点的状态,同时也不用担心Master节点服务器崩溃所导致数据不一致的风险。Master节点不会持久存储Chunk位置信息。Master节点在启动时,或者有新的ChunkServer加入时,向各个ChunkServer轮询所存储的Chunk的位置信息。

2.6.1 内存中的数据结构

​ 因为元数据保存在内存中,所以Master节点的操作速度非常快。并且Master节点可以简单高效的周期性地扫描自己保存的全部状态信息。这种周期性地扫描也用于实现Chunk垃圾回收,ChunkServer失效时重新复制数据,通过Chunk迁移在跨ChunkServers之间实现负载均衡和磁盘空间统计。4.3和4.4小节将会进一步讨论这些问题。

​ 这种将元数据全部保存在内存中的做法存在着一个潜在的问题,Chunk的数量以及整个系统的容量将受限于Master节点的内存大小。不过在实际应用中,这个问题并不严重。Master节点只需要小于64B大小的元数据就可以管理一个64MB的Chunk。由于大部分文件都包含多个Chunk,所以大多数Chunk块都是满大小的,除了文件的最后一个Chunk是部分填充的。同样的,每个文件的命名空间的数据大小通常小于64B,因为保存的文件名是通过前缀压缩算法进行压缩的。

​ 如果需要支持更大的文件系统,给Master节点额外增加内存配置所需要的费用也不多,通过元数据保存在内存中的方式能够使系统更加简单、可靠、高效。

2.6.2 Chunk位置信息

​ Master节点并不持久化存储哪个Chunk Server保存哪个指定的Chunk的副本的信息。Master节点在启动的时候轮询ChunkServer获取这些信息。Master节点可以保证它的信息是最新的,因为Master节点控制了所有Chunk块的位置分配,并且通过定期地心跳信息监控ChunkServer的状态。

​ 在最一开始,我们把Chunk位置信息持久化地存储在Master节点上,但是后来我们发现在启动的时候轮询ChunkServers获取信息,之后再周期性的轮询各个ChunkServer更新Chunk位置信息。这种设计简化了在ChunkServer加入集群、离开集群,更改名称、失效、重启等等,发生的Master节点和ChunkServer的数据同步问题。在一个数百台服务器的集群中,这类事件常常发生。

​ 从另外一个角度理解这个设计思路,就是只有ChunkServer才能最终确定一个Chunk是否在它的磁盘上。我们也没有意义要在Master节点上面持久地维护这些信息的全局视图,因为ChunkServer的错误可能会导致Chunk自动消失(比如磁盘损坏或者无法访问),又或者操作人员可能会重命名一个ChunkServer。

2.6.3 操作日志

​ 操作日志包含了关键的元数据变更历史记录,对于GFS系统来说是核心问题。这不仅仅是因为操作日志是元数据的唯一持久化记录,同样操作日志还作为定义并发操作顺序的逻辑时间线。文件和Chunks,以及它们的版本,都由它们创建的逻辑时间唯一的、永久的标识。

​ 操作日志非常关键,我们必须可靠地存储日志文件,保证在元数据的变化被持久化后,日志文件才对客户端是可见的。否则,即使Chunk本身没有出现问题,我们也很有可能丢失整个文件系统或者客户端最近的操作。因此,我们会把日志文件复制到多个远程机器,并把相应的日志记录写入到本地和远程机器的磁盘上,才会响应客户端的操作请求。Master节点会批量处理多个日志记录,以减少写入磁盘和复制日志文件对系统整体吞吐量的影响。

​ Master节点在做灾难恢复时,通过日志回放把文件系统恢复到最近的状态。为了最小化Master节点的启动时间,我们必须保证日志文件足够小。当日志文件增长到一定数据量时,Master节点会对系统状态做一次CheckPoint,将所有的状态数据写入CheckPoint文件。在做灾难恢复时,Master节点从本次磁盘上读取CheckPoint文件,并读取其中的系统状态数据之后,接下来只需要重演之后的有限个日志文件就能够完成系统灾难恢复。CheckPoint文件以压缩B树形式的数据结构存储,可以直接映射到内存中,不需要额外的解析就可以用于命名空间解析。这大大提高了恢复速度和可用性。

​ 因为创建CheckPoint文件需要一些时间,所以Master节点的内部状态被组织为一种格式,这种格式在创建新CheckPoint文件过程中不会阻塞正在进行的修改操作。Master节点会使用一个独立的线程切换到新的日志文件和创建新的CheckPoint文件。新的CheckPoint文件包含切换前所有的修改。一个包含数百万文件的集群,需要花费1分钟的时间创建一个CheckPoint文件。创建完成后,CheckPoint文件会被写入本地和远程机器的磁盘。

​ 灾难恢复只需要最新的CheckPoint文件和后续的日志文件。旧的CheckPoint文件和日志文件可以被删除,不过我们可能会继续保留这些文件一段时间,为了应对一些灾难性的故障。CheckPoint失败不会对系统正确性产生影响,因为灾难恢复代码检测并跳过了不完整的CheckPoint文件。

2.7 一致性模型

​ GFS有着宽松的一致性模型,这种宽松的一致性模型可以很好地支撑系统高度分布的应用程序,同时也保持了相对简单和容易实现的优点。这一章节我们讨论GFS一致性模型的保障机制和对应用程序的意义。我们重点讲解了GFS系统如何管理这些一致性保障机制,不过其中的细节将在本文的其他部分讨论。

2.7.1 GFS一致性保障机制

​ 文件命名空间的修改(比如文件创建)是原子性的。仅由Master节点控制:命名空间锁保障了原子性和正确性;Master节点的操作日志定义了这些操作的全局顺序。

​ 数据修改后文件region的状态取决于操作的类型、成功与否、是否为并行操作。表1总结了各种操作的结果。
在这里插入图片描述

consistent:如果所有客户端无论从哪个副本读取,读到的数据都是一样的,那么此时我们认为文件区域就是一致的

defined:如果对文件数据修改之后,文件区域是一致的,客户端能够看到写入操作的全部内容,那么此时我们认为文件区域就是已定义的。

​ 当一个数据修改操作成功执行后,并且没有受到其他并行写入操作的影响,那么已修改的那部分文件区域就是已定义的(隐含了一致性):所有的客户端都可以看到写入的全部内容。并行修改操作执行成功之后,文件区域处于一致的、未定义的状态:所有客户端看到的都是同样的数据,但是无法保证完整读到任何一次修改操作写入的数据。通常情况下,修改的文件区域包含了来自多个修改操作的、混杂的数据片段。一次失败的修改操作会使得文件区域处于不一致状态(同时也是未定义的):不同的客户端在不同的时间会看到不同的数据。我们将描述我们的应用程序是如何区分已定义和未定义的文件区域的。应用程序不需要进一步区分未定义区域的不同类型。

​ 数据修改操作可以分为两种类型:写入或记录追加操作。写入操作把数据写在应用程序指定的文件偏移位置上。即使发生多个修改操作并行执行时,记录追加操作可以把数据原子性的追加到文件中至少一次,但是偏移位置是由GFS系统自己选择的(其实就是在文件尾部追加)。偏移位置返还给客户端,并且表示包含了写入记录的、已定义的文件区域的起点。另外,GFS还会在文件区域中间插入填充数据或者重复记录。这些数据占用的文件区域被认为是不一致的,并且通常比用户数据小得多。

​ 经过一系列成功的修改操作后,GFS确保被修改的文件区域是已定义的,并且包含最后一次修改操作写入的数据。GFS通过以下方法来保障上述行为:(a)对Chunk的所有副本的修改操作顺序保持一致 (b) 使用Chunk版本号来检测是否因为Chunk所在的ChunkServer宕机而错过了修改操作,从而导致副本失效。失效的副本不会发生任何修改操作,Master节点也不会返回这个Chunk副本的位置信息给客户端。失效的Chunk副本会被垃圾回收系统尽快回收。

​ 因为客户端会缓存Chunk位置信息,在信息刷新前,客户端有可能从一个失效的副本读取数据。在缓存超时和文件再次被打开的时间之间存在一个时间窗,文件再次被打开后会清除缓存中与该文件有关的所有Chunk信息。此外,由于我们的文件大多数都是只进行记录追加操作,所以一个失效的副本通常返回一个提前结束的Chunk而不是过期的数据。当客户端重新尝试并与Master节点通信,客户端就会立刻得到Master节点返还回来的最新Chunk位置信息。

​ 在成功执行修改操作很长一段时间后,组件失效也可能损坏或者毁灭数据。GFS通过Master节点与ChunkServers定期地握手来找到失效的ChunkServer,并使用CheckSum算法检测数据是否损坏。一旦发现问题,要尽快利用有效的Chunk副本进行数据恢复。只有当一个Chunk的所有副本在GFS系统反应过来检测错误,这个时间通常是几分钟,采取有效措施之前全部丢失,这个Chunk才算作不可逆转的丢失。即使在这种极端情况下,Chunk也只是不可用了,而不是损坏:应用程序会收到明确的错误信息而不是损坏的数据。

2.7.2 应用程序的实现

​ 使用GFS系统的应用程序可以利用一些简单的技术实现上节提到的宽松一致性模型,并且这些技术也可以用于实现一些其他的目标功能。这些技术分别为:更依赖于追加数据操作而不是覆盖写入;自验证的写入操作;自标识的记录;

​ 实际上,我们的应用程序在修改文件时采用的方式更倾向于采用数据追加的方式,而不是覆盖的写入方式。一种非常典型的应用,程序将数据按顺序从头至尾的写入一个文件。在将所有数据写入文件之后,应用程序自动地把文件重命名为一个永久保存的文件名,或者周期性地执行CheckPoint,记录成功写入了多少数据。CheckPoints包含应用程序级别的checksum,Readers仅需要校验和处理自上个CheckPoint之后产生的文件region,这些文件region的状态是已定义的。这种技术方式满足了我们系统一致性和并发性的需求。追加写入操作比随机写入效率更高,对于处理应用程序的失败方面更加具有弹性。CheckPoint可以让Writer以增量的方式重新开始写入,同时可以防止Reader处理已经被成功写入但是从应用程序的角度来说还并没有完成的数据。

​ 在另一种典型应用中,许多应用程序并发地追加数据到同一个文件,进行多路结果合并或者生产者-消费者队列。记录追加操作的“至少一次追加”的特性保证了所有writer的输出。Readers通过下面的方法解决偶然的填充数据和重复内容的问题。Writers在每条写入的记录中都包含了额外的信息,比如checksum校验和,用于验证写入数据的有效性。Readers可以使用Checksum识别和丢弃额外的填充和记录片段数据。如果应用不能够容忍偶然出现的重复数据(如果这些重复数据触发了非幂等操作),可以通过记录的唯一标识符来过滤重复数据,这些唯一标识符经常用于命名应用程序中的实体对象,比如web文档。这些记录IO的功能都包含在应用程序共享代码库中,并且适用于Google的其他文件接口实现。因此,相同序列的记录加上一些重复数据,都被分发到Reader。

3.系统交互

​ 我们在设计GFS系统时的一个重要理念是最小化所有操作与Master节点的交互。基于这个设计思路,我们现在描述客户端,Master节点和Chunk Server是如何交互进而实现数据修改操作,原子记录追加操作以及快照功能。

3.1 租约和修改顺序

​ 变更是一个会改变Chunk内容或者元数据的操作,比如写操作或者追加操作。所有变更操作会在Chunk的所有副本上执行。我们通过租约来保持多个Chunk副本之间变更顺序的一致性。Master节点会为其中的一个Chunk副本建立租约,我们把这个Chunk副本叫做Primary。Primary Chunk会对Chunk的所有变更操作进行序列化,所有Chunk副本都按照这个顺序进行变更操作。变更操作的全局顺序是由Master节点选择的具有租约的Chunk决定的,并且由租约中Primary Chunk分配的变更序号决定的。

​ 设计租约机制的目的是为了最小化Master节点的管理负担。租约的初始超时时间为60s。然而, 当Chunk被修改了,Primary就可以再次申请租约,并且通常情况下可以从Master节点获得延长的租约时间。这些延长租约的请求和批准是附加在Master节点和Chunk Server之间互相传递的心跳消息中的。Master节点有时会尝试在租约到期之前撤销Primary的租约(比如Master节点想取消一个在已经被重命名的文件上的修改操作)。即使Master节点和Primary之间失去通信,Master节点仍然可以在旧的租约到期之后和另外一个Chunk副本建立新的租约。

​ 在Fig2中,我们通过下面的流程步骤,展现写入操作的控制流程。
在这里插入图片描述

  1. 客户端向Master节点询问哪一个Chunk Server持有当前的租约和其他Chunk副本的位置信息。如果没有Chunk副本持有租约,Master节点会选择其中一个Chunk副本建立一个租约。
  2. Master节点将Primary的标识符和其他Chunk副本的位置信息返回给客户机。客户端缓存这些信息用于后续的变更操作。客户端只有在Primary不可用或者Primary回复信息表示它已经不再持有当前租约的情况下,客户端才会需要再次与Master节点取得联系。
  3. 客户端会把要写入的数据推送到所有的Chunk副本上。客户端可以以任意的顺序推送这些数据。每个Chunk Server接收到数据后会把数据存储到其内部的LRU cache中,直到数据被使用或者过期。通过将数据流和控制流解耦,我们可以基于网络拓扑结构规划昂贵的数据流,以此来提高系统性能,而不用去关心哪一台Chunk Server是Primary节点。
  4. 当所有副本都确认收到了写入数据,客户端会向Primary发送写入请求。这个写入请求标识了之前推送到所有副本的数据。Primary将接收到的所有变更操作分配连续的序列号,这些变更操作可能来自多个不同的客户端,这些分配号的序列号将会保证执行顺序。Primary将会按照分配号的序列号顺序,在本地上执行这些变更操作。
  5. Primary把写入请求发送给所有的二级副本。每个二级副本会按照Primary分配号的序列号,顺序执行变更操作。
  6. 所有二级副本回复Primary,它们已经完成了变更操作。
  7. Primary回复客户端。任何副本发生的错误都将会报告给客户端。在发生错误的情况下,写入操作是可能会在Primary和其他部分二级副本上面执行成功的(如果在Primary上就执行失败了,那么序列号就不会被分配,也不会被传递给其他二级副本了)。客户端的请求被确认为失败,被修改的region处于不一致的状态。我们客户端的代码将会通过重复执行失败的变更操作来处理这种错误。从写入开始执行之前,客户端重复执行3到7的步骤流程。

​ 如果应用程序一次性写入的数据量非常大或者跨越了多个Chunk,GFS客户端代码会把它们划分为多个写入操作。这些操作也都是统一遵循上面描述的控制流,但是可能会被其他客户端并发进行的操作打断或者覆盖。因此,共享的文件区域范围的尾部将会包含来自不同客户端的数据片段,由于这些分解后的写入操作是在所有的副本上按照统一的顺序执行的,所以所有的副本上的数据是一致的。但是根据2.7节提到的,这种写入方式使得写入的文件region处于一致的,但是未定义的状态。

3.2 数据流

​ 为了提供网络利用率,我们将数据流和控制流解耦。控制流先从客户端传递到Primary,随后再传递到所有的二级副本。于此同时数据流以一种线性管道的传递方式通过精心选择的Chunk Server传递链进行数据传递。这种设计思路的目的是为了充分利用每台机器的网络带宽,避免遭遇网络瓶颈和高延时的连接,最小化推送数据至所有Chunk副本的时延。

​ 为了充分利用每台机器的网络带宽,数据流是以一种线性管道的方式沿着Chunk Server链顺序推送,而不是通过其他拓扑结构进行分散地推送数据。因此,每台机器的全部出口带宽都用于以最快的速度传输数据,而不是在多个接收者之间分配带宽。

​ 为了尽可能避免网络瓶颈和高延时链接,每台机器都在现有的网络拓扑中选择一台距离自己最近的但是还没有收到数据的机器,然后将数据推送过去。假设客户端把数据从Chunk Server1推送到Chunk Server4。客户端首先把数据推送到离自己最近的:Chunk Server1,随后CS1(Chunk Server)把数据推送到CS2,CS2是距离CS4最近的机器。同样的,CS2把数据推送给距离CS2更近的机器CS3或CS4。我们的网络拓扑十分简单,通过IP地址就可以精确地估算出机器节点之间的“距离”。

​ 最终,我们通过TCP连接、管道式数据推送的方式来最小化延迟。只要Chunk Server接收到数据之后,就立刻继续向后推送数据。管道式的数据推送方式对我们系统设计的帮助非常大,这也是因为我们采用全双工交换网络。在接收到数据之后立刻继续向后推送数据并不会降低现有的数据接收速度。在没有网络拥塞的情况下,传输B字节至R个副本的理想时间是B/T+RL,T代表网络吞吐量,L代表两台机器数据传输的时延。我们的网络连接速度是100Mbps(T),L则远小于1ms。因此1MB的数据在理想情况下经过80ms就可以分发出去。

3.3 原子的记录追加

​ GFS提供了一种原子的数据追加操作-记录追加。传统方式的数据写入方式,客户端会指定数据写入的偏移量。对同一个文件region的并发写入不是串行执行的:文件region区域可能会包含多个客户端并发写入的数据片段。而使用记录追加写入方式,客户端只需要指定将要写入的数据。GFS保证至少完成一次原子性的记录追加操作,写入的数据将会追加到GFS指定的偏移位置上,随后GFS将给客户端返回写入偏移位置。这种方式类似于在Unix系统中以O_APPEND模式打开文件,多个并发写入操作在没有竞态条件时的行为。

​ 在我们的分布式应用中记录追加操作的使用频率很高,在这些分布式应用中,来自不同机器的客户端并发地向同一个文件追加写入数据。如果通过传统的写入方式对文件执行写入操作,客户端需要额外的复杂、昂贵的同步机制,比如通过一个分布式的锁管理器实现同步机制。在我们的工作中,这样的文件经常应用于多个生产者/单个消费者的队列系统或者合并来自多个客户端的结果数据。

​ 记录追加操作是一种修改操作,并且遵循3.1节中提到的控制流程。不过在设计到Primary时,有一些额外的控制逻辑。客户端把数据推送给文件最后一个Chunk的所有副本,随后向Primary发送写入请求。Primary会首先检查本次记录追加数据操作是否会使Chunk超过最大尺寸(64MB)。如果超出了最大尺寸,Primary会使用填充数据将当前Chunk填充到最大尺寸,然后告知客户端并指示客户端需要对下一个Chunk重复执行记录追加操作。(记录追加操作的数据尺寸严格控制在不超过Chunk最大尺寸的1/4,这样即使在最坏的情况下,数据碎片的数量仍然在可控范围内)。通常情况下,记录追加的数据尺寸不超过Chunk的最大尺寸,Primary把数据追加到本地的副本上,然后通知二级副本把数据追加到和Primary一样的偏移位置上,最后待所有副本都执行完毕后回复客户端执行成功。

​ 如果记录追加操作在任何一个副本上执行失败了,客户端就需要重新执行记录追加操作。导致的结果就是,同一个Chunk的不同副本很可能包含不同的数据,这其中就可能会有重复包含一个追加记录中全部或者部分的数据。GFS不对Chunk的所有副本在字节级别的一致性做保证,而只是保证追加的数据作为一个原子整体被至少写入一次。这个性质可以通过简单的观察得出:如果追加操作执行成功,那么数据一定被写入Chunk的所有副本的相同偏移位置上。而且,在这之后所有的副本都达到了追加记录的尾部,任何后续的记录数据都会被追加到更大的偏移位置或者是不同的Chunk上面,即使其他的Chunk副本和Master节点建立了租约,成为Primary。根据GFS系统的一致性模型,记录追加操作成功写入数据的文件region区域是已定义的(因此也是一致的),反之则是不一致的(因此也就是未定义的)。我们的应用可以处理不一致的区域,像我们在2.7.2节讨论的一样。

3.4 快照

​ 快照操作几乎可以瞬间完成一个对文件或者目录树的拷贝,最小限度地降低对正在进行的操作的影响。用户可以使用快照技术快速地完成对巨大数据集的分支拷贝,或者在做实验性的修改操作之前,对当前的状态通过checkpoint做备份,之后可以简单的提交或者回滚到备份时间点的状态。

​ 像AFS,我们使用标准的COW技术实现快照。当Master节点收到快照请求,它首先取消需要做快照操作的文件的Chunk的租约。这样就保证了随后的写操作都必须与Master节点交互以找到租约的持有Chunk Server。这样就可以使得Master节点可以率先创建Chunk副本的新拷贝的机会。

​ 租约被撤回或者过期之后,Master节点把这个操作通过日志的方式记录到磁盘上。随后Master节点通过复制源文件或者目录的元数据的方式,把这条日志记录应用到内存状态中。新创建的快照文件和源文件一样,指向同一个Chunk。

​ 在快照操作之后,当客户端首次想向Chunk C写入数据,它会向Master节点发起请求询问当前租约的持有者。此时Master节点此时注意到Chunk C的引用计数大于1。此时Master节点推迟回复客户端的时间,而是选择一个新的Chunk句柄C’。之后Master节点会要求所有存储Chunk C当前副本的Chunk Server创建一个新的Chunk,叫做C’。通过在原Chunk Server上创建一个新的Chunk,我们可以保证数据是通过本地拷贝的,而不是通过网络(磁盘比100Mb以太网大约快3倍)。从这点来说,请求的处理方式和其他任何Chunk没有任何不同:Master会和拥有Chunk C’的一个副本建立租约,之后会给客户端回复。这样客户端得到回复后就可以正常写这个新Chunk,无需关心这是从一个已存在Chunk拷贝而来的。

4. Master节点的操作

​ Master节点执行所有有关命名空间的操作。此外,Master节点还管理着整个系统的Chunk副本:它决定Chunk的存储位置,创建新Chunk和它的副本,协调各种各样的系统活动保证Chunk被完全复制,在所有的Chunk Server之间进行负载均衡,回收不再使用的存储空间。在这一节中,我们将要讨论上面的问题。

4.1 命名空间管理和锁

​ Master节点上面的许多操作都会花费很长的时间:例如,快照操作必须撤销涉及到的Chunk所在的Chunk Server的租约。我们不希望这些操作在运行时,影响其他操作的时间。因此,我们允许多个操作同时执行,并使用命名空间的region上的锁来保证正确的执行顺序。

​ 不同于许多传统的文件系统,GFS没有实现对每个目录能够列出目录下的所有文件的数据结构。GFS系统也不支持文件或者目录的链接。逻辑上来说,GFS中的命名空间就是一个全路径名和元数据映射关系的查找表。并且经过前缀压缩,这个查找表可以高效地存储在内存当中。在命名空间树中,每个节点都关联了一把读写锁。

​ 每个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/user/foo文件创建操作需要拿到/home和/home/user的读取锁,/home/user/foo的写锁。快照和创建文件这两个操作需要按照顺序执行,因此它们在获取/home/user的锁时是冲突的。文件创建操作不需要拿到父目录的写锁,因为这里没有“目录”,或者类似inode的数据结构来防止被修改。读取锁已经足够可以防止父目录被删除。

​ 采用这种锁方案的好处就是它允许对同一目录执行并发操作。例如,在同一个目录下,可以并发地创建多个文件:每个文件创建操作只需要先拿到父目录的读取锁,要创建的文件名的写锁。父目录的读取锁足够可以防止父目录被删除、重命名和被快照。创建文件所需要的写锁可以防止多次创建同名的文件。

​ 命名空间有许多节点,读写锁对象的分配策略是惰性的,一旦锁不再使用则立刻删除。另外,锁的获取也是按照一致的序列化顺序,这样可以避免发生deadlock:首先按照命名空间树中的层级排序,在同一个层级中按照字典序排序。

4.2 副本的位置分配

​ GFS集群是一个高度分布的多层级结构,而不是扁平化结构。通常的情况是数百台Chunk Server分布在多个机架上面。这些Chunk Server被来自同一个或者不同机架上的数百个客户端轮流访问。不同机架上的两台机器互相通信时可能需要跨越一个或者多个网络交换机。此外,机架的出入带宽可能会比机架上所有机器的带宽之和要小。多层级的分布式架构对分布式数据的可扩展性、可靠性以及可用性方面带来了特有的挑战。

​ Chunk副本的位置选择策略只要服务于两个目标:最大化数据可靠性和可用性,最大化网络带宽利用率。为了达到上述两个目标,仅在多台机器上防止数据副本是不够的,因为这样只能预防磁盘或者机器损坏带来的故障影响,以及充分利用每台机器的网络带宽。我们必须将Chunk副本存储在多个机架上。这样可以保证在整个机架被毁坏或者掉线的情况下,Chunk的一些副本仍然存活并且是可用的(例如,电源或者网络交换机造成的问题)。这还意味着在网络流量方面,尤其是对Chunk的读取操作,可以利用多个机架的整个带宽。另一方面,写操作需要和多个机架的机器进行交互,不过这个折中是我们愿意付出的。

4.3 创建,再复制,再负载均衡

​ 创建Chunk副本有三个原因:Chunk创建,再复制 ,再负载均衡。

​ 当Master节点创建一个Chunk,Master节点会选择在哪里存放这个初始化的空副本。放置策略需要考虑多个因素。

  1. 我们希望在低于平均磁盘利用率的Chunk服务器上放置新的副本。随着时间的推移,这种做法可以平衡Chunk Server之间的磁盘利用率。
  2. 我们希望限制在每个Chunk Server上“recent”创建Chunk副本的次数。尽管创建操作本身是廉价的,但是可以预料到的是在创建操作之后随之而来的就是大量的写入操作。因为只有在有写入需求的时候,Chunk才会被创建。并且在追加一次,读取多次的工作模式下,Chunk在被写入成功后实际上就成为只读的了。
  3. 像上面所讨论的,我们希望把Chunk副本分布在多个机架上。

​ 当Chunk副本的数量低于用户指定的复制因子时,Master节点会重新会再次复制Chunk副本。Chunk副本数量不足的情况可能是由几种不同的原因造成的:Chunk Server不可用,Chunk Server报告其上面存储的一个副本损坏了,因为某些错误导致Chunk Server上的磁盘不可用,或者Chunk副本的复制因子增加了。每个需要被再次复制的Chunk都会根据几个因素按照优先级排序。其中一个因素就是Chunk现有的副本数量和设定的复制因子差多少。比如,丢失两个副本的Chunk比丢失一个副本的Chunk有更高的优先级。此外,我们更倾向于优先重新复制活跃文件的Chunk,而不是最近刚被删除的文件Chunk。最终,为了最小化失效的Chunk对正在运行的应用程序的影响,我们会提高那些正在阻塞客户端程序处理的Chunk的复制优先级。

​ Master节点会选择优先级最高的Chunk,然后指示某些Chunk Server从现有可用的副本clone一个副本出来。选择新副本的放置策略和创建Chunk副本时的策略相似:平衡磁盘使用率,限制同一台Chunk Server上的正在进行clone操作的数量,多个机架之间分配副本。为了防止clone操作产生的流量大量超过客户端的流量,Master节点限制了对整个集群和每台Chunk Server上同时进行的clone操作的数量。此外,每台Chunk Server通过调节它对源Chunk Server读请求的频率,限制clone操作所占用的网络带宽。

​ 最终,Master节点周期性地会对副本进行重新负载均衡:它会检查当前副本的分布,然后为了得到更好的磁盘利用率和负载均衡进行副本迁移。在这个过程中,Master节点会渐渐地填满一个新Chunk Server,而不是在很短的时间内用新建的Chunk填满这个Chunk Server,随之而来是大量的写入操作,这样会使得新Chunk Server应接不暇。新副本的位置选择策略和上面讲的相同。此外,Master节点必须选择具体哪个副本将会被迁移。一般来说,更倾向于将那些剩余空间低于平均值的Chunk Server上的副本进行迁移,这样可以使得磁盘使用率更加平衡。

4.4 垃圾回收

​ 在GFS系统中,当一个文件被删除,GFS不会立刻回收这个文件占用的物理空间。在对文件和Chunk层级进行常规垃圾回收时,采取的都是惰性策略。我们发现这个方法使得系统更加简洁和可靠。

4.4.1 机制

​ 当文件被应用程序删除,Master节点会像对待其他修改操作一样,将删除操作以日志的方式记录下来。然而,Master节点不会马上回收文件占用的资源,被删除的文件将会被重命名为一个隐藏的名字,文件名包含删除的时间戳。当Master节点对文件系统命名空间做常规扫描过程中,会删除所有超过三天的隐藏文件(三天的时间间隔是可以配置的)。在文件的资源被真正回收之前,我们仍然可以通过它的特殊的文件名读取文件,还能通过把重命名过后的文件名再改回正常显示的文件名来完成反删除操作。当隐藏的文件从命名空间中被删除,Master节点内存中的相关元数据才会被清楚。这也有效地断开了文件和文件相关的Chunk的链接。

​ 在对Chunk命名空间的常规扫描过程中,Master节点会找出孤儿Chunk(和任何文件都没有关联的Chunk),并在内存中删除它们的元数据。在Chunk Server和Master节点的常规心跳信息交互过程中,Chunk Server会报告它所拥有的Chunk子集,Master节点回复给Chunk Server那些已经不存在于元数据中的Chunk标识。Chunk Server可以随意删除这些Chunk的副本。

4.4.2 讨论

​ 虽然分布式垃圾回收在编程语言领域中是一个需要复杂的解决方案才能够解决的难题,但是就GFS系统而言,是个非常简单的问题。我们可以轻松地识别出所有Chunk的引用:这些引用专门存储在Master节点中的文件-Chunk的映射表中。我们还可以轻松地识别出所有Chunk的副本:这些副本都是以Linux文件的形式存储在每台Chunk Server的指定目录了下。所有Master节点不能够识别的副本都归属于“垃圾”一类。

​ 在存储空间回收方面,垃圾回收方法相比于直接删除有几个优势。首先,对于组件失效为常态的大规模分布式系统来说,垃圾回收方法简单可靠。Chunk可能会在某些Chunk Server上创建成功,而在有些Chunk Server上会失败,创建失败的那些副本则无法被Master节点识别。副本删除消息可能会丢失,Master节点必须在这种情况下重新发送副本删除消息,包括自己和Chunk Server。垃圾回收方法提供了统一的、可靠的清楚无用副本的方法。第二,垃圾回收方法把回收存储空间过程合并到Master节点常规性的后台进程中,比如例行扫描命名空间和与Chunk Server握手。因此,操作被批量的执行而具体操作的开销则被分摊开来。此外,垃圾回收操作只会在Master节点相对空闲的时候完成。只有这样Master节点才可以给那些需要快速响应的客户端请求提供更迅速地响应。第三,推迟存储空间回收给意外的、不可逆转的删除操作提供了安全保障。

​ 在我们平时的使用过程中,总结出的经验就是,这种延迟回收存储空间的方法最大的缺点就是,当存储空间比较紧缺时,会阻碍用户调优存储空间的使用。当应用程序重复创建和删除临时文件时,我们无法立刻使用释放出来的存储空间。我们通过再次显式地删除一个已经被删除的文件的方式加速存储空间的回收,以解决延迟回收存储空间带来的问题。我们允许用户为命名空间的不同部分设定不同的复制和回收策略。比如,用户可以指定某些目录树下的所有文件只做存储而不进行复制,任何删除的文件即刻、不可逆转地从文件系统中删除。

4.5 过期副本检测

​ 当Chunk服务器失效,导致Chunk的副本有可能因为错过了一些修改操作从而导致Chunk副本失效。对于每个Chunk而言,Master节点保存了每个Chunk的版本号,用来区分当前的副本和过期失效的副本。

​ 无论什么时候,Master节点和Chunk建立一个新的租约,它就增加Chunk的版本号,并且通知最新的副本。Master节点和这些副本都把新的版本号记录在它们持久化的状态信息中。这发生在任何客户端得到通知之前,因此也就是对这个Chunk开始写入之前。如果某个副本处于失效状态,那么Chunk版本号不会被增加。当这个Chunk Server重启并且向Master节点报告它拥有的Chunk集合以及相关的版本号,Master节点就会检测出这个Chunk Server中过期的Chunk。如果Master节点看到一个比这个Chunk Server记录的版本号更高的版本号,Master节点就会认为它和Chunk Server建立的租约失效了,因此接下来会选择更高的版本号作为当前版本号。

​ Master节点会在例行的垃圾回收程序中移除所有的失效副本。当然在这之前,Master节点在回复客户端的Chunk信息请求时,实际上会直接认为过期的副本是完全不存在的。另外一个保护措施是,Master节点在通知客户端当前Chunk是哪个Chunk Server持有租约或者指示Chunk Server从哪个Chunk Server读取Chunk进行clone操作时,消息中都会附带着Chunk的版本号。客户端或者Chunk Server在执行操作时都会验证Chunk版本号,以确保总是访问当前最新版本的数据。

5.容错和诊断

​ 我们在设计GFS系统中遇到的最优挑战性的问题就是如何处理频繁的组件失效问题。组件的质量和数量共同让这些问题变得很常见:我们不能够完全依赖机器,也不能完全信任磁盘。组件失效可能会导致整个系统不可用,更坏的情况是,可能导致不完整的数据。我们讨论我们如何应对这些挑战,以及当这些问题不可避免的发生时,使用GFS系统中集成的诊断工具解决故障。

5.1 高可用性

​ 在GFS集群的数百台服务器中,在任何特定的时间某些服务器一定会是不可用的。我们使用两条简单有效的策略来保证整个系统高可用:快速回复和复制。

5.1.1 快速恢复

​ 无论Master和Chunk Server是如何关闭的,它们都被设计为可以在几秒钟之内恢复它们关闭之前的状态并重新启动。事实上,我们对正常关闭和非正常关闭不进行区分;通常我们都是通过kill进程来关闭服务器。客户端和其他服务器会经历系统抖动,正在发出的请求会超时,需要重新连接到重新启动的服务器,然后重试刚才超时的请求。6.2.2小节中的报告记录了重新启动的时间。

5.1.2 Chunk复制

​ 像之前讨论的部分,每个Chunk都被复制到不同机架的多台Chunk Server上。用户可以为文件命名空间的不同部分设定不同的复制级别,默认情况下的副本数量是3个。当有Chunk Server离线或者通过checksum校验发现了损坏的副本数据,Master节点通过clone已经存在的完整副本保证每个Chunk都被完整复制。尽管Chunk的复制策略对我们十分有效,不过我们也在探索其他形式的跨服务器的冗余解决方案,比如奇偶校验或者使用erasure code来满足日益增长的读取需求。我们认为在这个非常松耦合的系统中实现这些负载的冗余方案是很有挑战的,但是是可以实现的,因为GFS系统中的主要流量来自于追加操作和读取操作,很少有随机写入操作。

5.1.3 Master节点复制

​ 为了保证Master节点可靠性,其状态也需要被复制。Master节点的所有操作日志和checkpoint文件都被复制到多台机器上。只有当操作日志写入Master节点本次磁盘和所有Master节点的备份节点上,对Master节点状态的修改操作才能够成功提交。简单来说,一个Master进程负责所有的修改操作,比如垃圾回收等改变系统内部状态的活动。如果Master节点所在的机器或者磁盘失效了,处于GFS系统外部的监控会在另外一台存有完整操作日志的机器上启动一个新的Master进程。客户端使用标准的名字访问Master节点,就像DNS别名一样,如果Master进程迁移到别的机器上时,可以通过修改别名的实际指向访问新的Master节点。

​ 此外,GFS中还存在影子Master服务器,即使在Master节点宕机时,这些影子服务器也仅仅是提供文件系统的访问服务。它们是影子,而不是镜像服务器,因此它们可能比主Master节点更新的要慢,通常是不到1s。对于那些不经常修改的文件或者一些可以接受少量失效数据的应用程序来说,影子服务器可以提高读取的效率。事实上,文件内容是从Chunk服务器上读取的,应用程序不会发现失效的文件内容。在这个短暂的时间窗内,文件元数据是很有可能失效的,比如目录的内容或者访问控制信息。

​ 为了保持自身状态是最新的,影子服务器会读取一份正在进行的操作的日志副本,并且按照和主Master节点完全相同的顺序修改其内部的数据结构。和主Master节点一样,影子服务器在启动的时候从Chunk Server处轮询数据,数据中包含Chunk副本位置;也会和Chunk Server握手来监视他们的状态。只有在主Master节点创建和删除副本导致副本位置更新时,影子服务器才向Master节点获取最新数据,更新自身状态。

5.2 数据完整性

​ 每个Chunk Server使用checksum来检测存储数据是否损坏。考虑到一个GFS集群通常有数百台机器,数千块磁盘,磁盘损坏从而导致的数据在读写过程中损坏或者丢失是很常见的。我们可以通过别的Chunk副本来恢复数据损坏问题,但是跨越Chunk Server比较副本来检测数据是否损坏不切合实际。此外,GFS系统允许有歧义的副本存在:GFS修改操作的语义,尤其是之前讨论的原子记录追加操作,不能够保证副本完全一致。因此,每台Chunk Server必须独自维护checksum来验证自己副本数据的完整性。

​ 每个Chunk都划分为64KB大小的块。每个block都对应一个32bit的checksum。和其他元数据一样,checksum校验和保存在内存和通过日志方式进行持久化存储,checksum是和用户数据分开存储的。

​ 对于读操作,Chunk Server会在返回给客户端或者其他Chunk Server数据之前,会自行校验读取操作范围内涉及到的数据。因此Chunk Server不会把损坏的数据传递到其他的机器上。如果block的checksum发生错误,Chunk Server服务器返回给请求者一个错误信息,并通知Master节点这个错误。作为回应,请求者会从其他副本读取数据,Master节点也会从其他副本clone Chunk数据进行恢复。当一个有效的新副本就绪之后,Master节点通知发生checksum错误的Chunk Server删除发生数据损坏的副本。

​ Checksum对读操作的影响微乎其微,有以下几个原因。因为大部分的读操作至少需要读取几个block,我们只需要额外读取相对很小一部分数据来完成相关数据校验。GFS客户端代码通过把读操作对齐在Checksum block边界上进一步减少CheckSum对性能的影响。另外,在Chunk服务器上,CheckSum的查找和比较不需要I/O操作,CheckSum的计算操作可以和I/O同时进行。

​ CheckSum的计算针对在Chunk尾部的追加写入操作做了高度优化(与之相对的是覆盖现有数据的写入操作),这类操作在工作中占了主要部分。我们只通过增量更新最后一个不完整的块的CheckSum,并且用所有追加的新CheckSum块来计算新的CheckSum。即使最后一个不完整的CheckSum块已经损坏了,而且我们无法立刻检测出来,在下一次对这block进行读取操作时,由于新的CheckSum和已存数据不匹配,我们会检测出数据已损坏。

​ 作为对比,如果写操作覆盖Chunk中已存在的数据,那么我们必须读取和校验被覆盖的第一个和最后一个block,然后执行写操作,并且之后再计算和写入新的CheckSum。假设我们不对被覆盖的第一个和最后一个block进行校验,新的CheckSum可能会隐藏没有被覆盖区域内的错误。

,这类操作在工作中占了主要部分。我们只通过增量更新最后一个不完整的块的CheckSum,并且用所有追加的新CheckSum块来计算新的CheckSum。即使最后一个不完整的CheckSum块已经损坏了,而且我们无法立刻检测出来,在下一次对这block进行读取操作时,由于新的CheckSum和已存数据不匹配,我们会检测出数据已损坏。

​ 作为对比,如果写操作覆盖Chunk中已存在的数据,那么我们必须读取和校验被覆盖的第一个和最后一个block,然后执行写操作,并且之后再计算和写入新的CheckSum。假设我们不对被覆盖的第一个和最后一个block进行校验,新的CheckSum可能会隐藏没有被覆盖区域内的错误。

​ 在chunkserver空闲的时候,它会扫描和校验每个不活动Chunk的内容。从而我们可以发现那些被很少读取到的Chunk中的数据是否有错误。一旦在Chunk中发现有错误,Master可以创建一个新的、正确的副本,然后把损坏的副本删除。这种扫描检测机制避免了非活动的、损坏的Chunk副本欺骗Master节点,使得Master节点认为这些Chunk已经有了足够多的副本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值