谷歌三大核心技术(一)Google File System - 设计

2.1设计预期

在设计满足我们需求的文件系统时候,我们的设计目标既有机会、又有挑战。之前我们已经提到了一些需要关注的关键点,这里我们将设计的预期目标的细节展开讨论。

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

2.2 接口

GFS提供了一套类似传统文件系统的API接口函数,虽然并不是严格按照POSIX等标准API的形式实现的。文件以分层目录的形式组织,用路径名来标识。我们支持常用的操作,如创建新文件、删除文件、打开文件、关闭文件、读和写文件。
另外,GFS提供了快照和记录追加操作。快照以很低的成本创建一个文件或者目录树的拷贝。记录追加操作允许多个客户端同时对一个文件进行数据追 加操作,同时保证每个客户端的追加操作都是原子性的。这对于实现多路结果合并,以及”生产者-消费者”队列非常有用,多个客户端可以在不需要额外的同步锁 定的情况下,同时对一个文件追加数据。我们发现这些类型的文件对于构建大型分布应用是非常重要的。快照和记录追加操作将在3.4和3.3节分别讨论。

2.3 架构

一个GFS集群包含一个单独的Master节点(alex注:这里的一个单 独的Master节点的含义是GFS系统中只存在一个逻辑上的Master组件。后面我们还会提到Master节点复制,因此,为了理解方便,我们把 Master节点视为一个逻辑上的概念,一个逻辑的Master节点包括两台物理主机,即两台Master服务器)、多台 Chunk服务器,并且同时被多个客户端访问,如图1所示。所有的这些机器通常都是普通的Linux机器,运行着用户级别(user-level)的服务 进程。我们可以很容易的把Chunk服务器和客户端都放在同一台机器上,前提是机器资源允许,并且我们能够接受不可靠的应用程序代码带来的稳定性降低的风 险。
谷歌三大核心技术(二)Google MapReduce中文版
GFS存储的文件都被分割成固定大小的Chunk。在Chunk创建的时候,Master服务器会给每个Chunk分配一个不变的、全球唯一的 64位的Chunk标识。Chunk服务器把Chunk以linux文件的形式保存在本地硬盘上,并且根据指定的Chunk标识和字节范围来读写块数据。 出于可靠性的考虑,每个块都会复制到多个块服务器上。缺省情况下,我们使用3个存储复制节点,不过用户可以为不同的文件命名空间设定不同的复制级别。
Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射信息、以及当前Chunk的位置信息。Master节点还管理着系统范围内的活动,比如,Chunk租用管理(alex注:BDB也有关于lease的描述,不知道是否相同)、孤儿Chunk(alex注:orphaned chunks)的回收、以及Chunk在Chunk服务器之间的迁移。Master节点使用心跳信息周期地和每个Chunk服务器通讯,发送指令到各个Chunk服务器并接收Chunk服务器的状态信息。
GFS客户端代码以库的形式被链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程序与Master节点和Chunk服 务器通讯、以及对数据进行读写操作。客户端和Master节点的通信只获取元数据,所有的数据操作都是由客户端直接和Chunk服务器进行交互的。我们不 提供POSIX标准的API的功能,因此,GFS API调用不需要深入到Linux vnode级别。
无论是客户端还是Chunk服务器都不需要缓存文件数据。客户端缓存数据几乎没有什么用处,因为大部分程序要么以流的方式读取一个巨大文件,要 么工作集太大根本无法被缓存。无需考虑缓存相关的问题也简化了客户端和整个系统的设计和实现。(不过,客户端会缓存元数据。)Chunk服务器不需要缓存 文件数据的原因是,Chunk以本地文件的方式保存,Linux操作系统的文件系统缓存会把经常访问的数据缓存在内存中。

2.4 单一Master节点

单一的Master节点的策略大大简化了我们的设计。单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。另 外,我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。反之,客户端向 Master节点询问它应该联系的Chunk服务器。客户端将这些元数据信息缓存一段时间,后续的操作将直接和Chunk服务器进行数据读写操作。
我们利用图1解释一下一次简单读取的流程。首先,客户端把文件名和程序指定的字节偏移,根据固定的Chunk大小,转换成文件的Chunk索 引。然后,它把文件名和Chunk索引发送给Master节点。Master节点将相应的Chunk标识和副本的位置信息发还给客户端。客户端用文件名和 Chunk索引作为key缓存这些信息。
之后客户端发送请求到其中的一个副本处,一般会选择最近的。请求信息包含了Chunk的标识和字节范围。在对这个Chunk的后续读取操作中, 客户端不必再和Master节点通讯了,除非缓存的元数据信息过期或者文件被重新打开。实际上,客户端通常会在一次请求中查询多个Chunk信 息,Master节点的回应也可能包含了紧跟着这些被请求的Chunk后面的Chunk的信息。在实际应用中,这些额外的信息在没有任何代价的情况下,避 免了客户端和Master节点未来可能会发生的几次通讯。

2.5 Chunk尺寸

Chunk的大小是关键的设计参数之一。我们选择了64MB,这个尺寸远远大于一般文件系统的Block size。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成 的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议一点。
选择较大的Chunk尺寸有几个重要的优点。首先,它减少了客户端和Master节点通讯的需求,因为只需要一次和Mater节点的通信就可以 获取Chunk的位置信息,之后就可以对同一个Chunk进行多次的读写操作。这种方式对降低我们的工作负载来说效果显著,因为我们的应用程序通常是连续 读写大文件。即使是小规模的随机读取,采用较大的Chunk尺寸也带来明显的好处,客户端可以轻松的缓存一个数TB的工作数据集所有的Chunk位置信 息。其次,采用较大的Chunk尺寸,客户端能够对一个块进行多次操作,这样就可以通过与Chunk服务器保持较长时间的TCP连接来减少网络负载。第 三,选用较大的Chunk尺寸减少了Master节点需要保存的元数据的数量。这就允许我们把元数据全部放在内存中,在2.6.1节我们会讨论元数据全部 放在内存中带来的额外的好处。
另一方面,即使配合惰性空间分配,采用较大的Chunk尺寸也有其缺陷。小文件包含较少的Chunk,甚至只有一个Chunk。当有许多的客户 端对同一个小文件进行多次的访问时,存储这些Chunk的Chunk服务器就会变成热点。在实际应用中,由于我们的程序通常是连续的读取包含多个 Chunk的大文件,热点还不是主要的问题。
然而,当我们第一次把GFS用于批处理队列系统的时候,热点的问题还是产生了:一个可执行文件在GFS上保存为single-chunk文件, 之后这个可执行文件在数百台机器上同时启动。存放这个可执行文件的几个Chunk服务器被数百个客户端的并发请求访问导致系统局部过载。我们通过使用更大 的复制参数来保存可执行文件,以及错开批处理队列系统程序的启动时间的方法解决了这个问题。一个可能的长效解决方案是,在这种的情况下,允许客户端从其它 客户端读取数据。

2.6 元数据

Master服务器 (alex注:注意逻辑的Master节点和物理的Master服务器的区别。后续我们谈的是每个Master服务器的行为,如存储、内存等等,因此我们将全部使用物理名称)存 储3种主要类型的元数据,包括:文件和Chunk的命名空间、文件和Chunk的对应关系、每个Chunk副本的存放地点。所有的元数据都保存在 Master服务器的内存中。前两种类型的元数据(命名空间、文件和Chunk的对应关系)同时也会以记录变更日志的方式记录在操作系统的系统日志文件 中,日志文件存储在本地磁盘上,同时日志会被复制到其它的远程Master服务器上。采用保存变更日志的方式,我们能够简单可靠的更新Master服务器 的状态,并且不用担心Master服务器崩溃导致数据不一致的风险。Master服务器不会持久保存Chunk位置信息。Master服务器在启动时,或 者有新的Chunk服务器加入时,向各个Chunk服务器轮询它们所存储的Chunk的信息。
2.6.1 内存中的数据结构
因为元数据保存在内存中,所以Master服务器的操作速度非常快。并且,Master服务器可以在后台简单而高效的周期性扫描自己保存的全部 状态信息。这种周期性的状态扫描也用于实现Chunk垃圾收集、在Chunk服务器失效的时重新复制数据、通过Chunk的迁移实现跨Chunk服务器的 负载均衡以及磁盘使用状况统计等功能。4.3和4.4章节将深入讨论这些行为。
将元数据全部保存在内存中的方法有潜在问题:Chunk的数量以及整个系统的承载能力都受限于Master服务器所拥有的内存大小。但是在实际 应用中,这并不是一个严重的问题。Master服务器只需要不到64个字节的元数据就能够管理一个64MB的Chunk。由于大多数文件都包含多个 Chunk,因此绝大多数Chunk都是满的,除了文件的最后一个Chunk是部分填充的。同样的,每个文件的在命名空间中的数据大小通常在64字节以 下,因为保存的文件名是用前缀压缩算法压缩过的。
即便是需要支持更大的文件系统,为Master服务器增加额外内存的费用是很少的,而通过增加有限的费用,我们就能够把元数据全部保存在内存里,增强了系统的简洁性、可靠性、高性能和灵活性。
2.6.2 Chunk位置信息

Master服务器并不保存持久化保存哪个Chunk服务器存有指定Chunk的副本的信息。Master服务器只是在启动的时候轮询Chunk服 务器以获取这些信息。Master服务器能够保证它持有的信息始终是最新的,因为它控制了所有的Chunk位置的分配,而且通过周期性的心跳信息监控 Chunk服务器的状态。

最初设计时,我们试图把Chunk的位置信息持久的保存在Master服务器上,但是后来我们发现在启动的时候轮询Chunk服务器,之后定期轮询 更新的方式更简单。这种设计简化了在有Chunk服务器加入集群、离开集群、更名、失效、以及重启的时候,Master服务器和Chunk服务器数据同步 的问题。在一个拥有数百台服务器的集群中,这类事件会频繁的发生。

可以从另外一个角度去理解这个设计决策:只有Chunk服务器才能最终确定一个Chunk是否在它的硬盘上。我们从没有考虑过在Master服务器 上维护一个这些信息的全局视图,因为Chunk服务器的错误可能会导致Chunk自动消失(比如,硬盘损坏了或者无法访问了),亦或者操作人员可能会重命 名一个Chunk服务器。

2.6.3 操作日志

操作日志包含了关键的元数据变更历史记录。这对GFS非常重要。这不仅仅是因为操作日志是元数据唯一的持久化存储记录,它也作为判断同步操作顺序的逻辑时间基线(alex注:也就是通过逻辑日志的序号作为操作发生的逻辑时间,类似于事务系统中的LSN)。文件和Chunk,连同它们的版本(参考4.5节),都由它们创建的逻辑时间唯一的、永久的标识。

操作日志非常重要,我们必须确保日志文件的完整,确保只有在元数据的变化被持久化后,日志才对客户端是可见的。否则,即使Chunk本身没有出现任 何问题,我们仍有可能丢失整个文件系统,或者丢失客户端最近的操作。所以,我们会把日志复制到多台远程机器,并且只有把相应的日志记录写入到本地以及远程 机器的硬盘后,才会响应客户端的操作请求。Master服务器会收集多个日志记录后批量处理,以减少写入磁盘和复制对系统整体性能的影响。

Master服务器在灾难恢复时,通过重演操作日志把文件系统恢复到最近的状态。为了缩短Master启动的时间,我们必须使日志足够小(alex注:即重演系统操作的日志量尽量的少)。Master服务器在日志增长到一定量时对系统状态做一次Checkpoint(alex注:Checkpoint是一种行为,一种对数据库状态作一次快照的行为),将所有的状态数据写入一个Checkpoint文件(alex注:并删除之前的日志文件)。 在灾难恢复的时候,Master服务器就通过从磁盘上读取这个Checkpoint文件,以及重演Checkpoint之后的有限个日志文件就能够恢复系统。Checkpoint文件以压缩B-树形势的数据结构存储,可以直接映射到内存,在用于命名空间查询时无需额外的解析。这大大提高了恢复速度,增强了可用性。

 

由于创建一个Checkpoint文件需要一定的时间,所以Master服务器的内部状态被组织为一种格式,这种格式要确保在Checkpoint 过程中不会阻塞正在进行的修改操作。Master服务器使用独立的线程切换到新的日志文件和创建新的Checkpoint文件。新的Checkpoint 文件包括切换前所有的修改。对于一个包含数百万个文件的集群,创建一个Checkpoint文件需要1分钟左右的时间。创建完成后,Checkpoint 文件会被写入在本地和远程的硬盘里。

Master服务器恢复只需要最新的Checkpoint文件和后续的日志文件。旧的Checkpoint文件和日志文件可以被删除,但是为了应对灾难性的故障(alex注:catastrophes,数据备份相关文档中经常会遇到这个词,表示一种超出预期范围的灾难性事件),我们通常会多保存一些历史文件。Checkpoint失败不会对正确性产生任何影响,因为恢复功能的代码可以检测并跳过没有完成的Checkpoint文件。

2.7 一致性模型

GFS支持一个宽松的一致性模型,这个模型能够很好的支撑我们的高度分布的应用,同时还保持了相对简单且容易实现的优点。本节我们讨论GFS的一致 性的保障机制,以及对应用程序的意义。我们也着重描述了GFS如何管理这些一致性保障机制,但是实现的细节将在本论文的其它部分讨论。

2.7.1 GFS一致性保障机制

文件命名空间的修改(例如,文件创建)是原子性的。它们仅由Master节点的控制:命名空间锁提供了原子性和正确性(4.1章)的保障;Master节点的操作日志定义了这些操作在全局的顺序(2.6.3章)。

谷歌三大核心技术(二)Google MapReduce中文版

 

数据修改后文件region(alex注:region这个词用中文非常难以表达,我认为应该是修改操作所涉及的文件中的某个范围)的 状态取决于操作的类型、成功与否、以及是否同步修改。表1总结了各种操作的结果。如果所有客户端,无论从哪个副本读取,读到的数据都一样,那么我们认为文 件region是“一致的”;如果对文件的数据修改之后,region是一致的,并且客户端能够看到写入操作全部的内容,那么这个region是“已定义 的”。当一个数据修改操作成功执行,并且没有受到同时执行的其它写入操作的干扰,那么影响的region就是已定义的(隐含了一致性):所有的客户端都可 以看到写入的内容。并行修改操作成功完成之后,region处于一致的、未定义的状态:所有的客户端看到同样的数据,但是无法读到任何一次写入操作写入的 数据。通常情况下,文件region内包含了来自多个修改操作的、混杂的数据片段。失败的修改操作导致一个region处于不一致状态(同时也是未定义 的):不同的客户在不同的时间会看到不同的数据。后面我们将描述应用如何区分已定义和未定义的region。应用程序没有必要再去细分未定义region 的不同类型。

数据修改操作分为写入或者记录追加两种。写入操作把数据写在应用程序指定的文件偏移位置上。即使有多个修改操作并行执行时,记录追加操作至少可以把数据原子性的追加到文件中一次,但是偏移位置是由GFS选择的(3.3章)(alex注:这句话有点费解,其含义是所有的追加写入都会成功,但是有可能被执行了多次,而且每次追加的文件偏移量由GFS自己计算)。 (相比而言,通常说的追加操作写的偏移位置是文件的尾部。)GFS返回给客户端一个偏移量,表示了包含了写入记录的、已定义的region的起点。另 外,GFS可能会在文件中间插入填充数据或者重复记录。这些数据占据的文件region被认定是不一致的,这些数据通常比用户数据小的多。

 

经过了一系列的成功的修改操作之后,GFS确保被修改的文件region是已定义的,并且包含最后一次修改操作写入的数据。GFS通过以下措施确保 上述行为:(a) 对Chunk的所有副本的修改操作顺序一致(3.1章),(b)使用Chunk的版本号来检测副本是否因为它所在的Chunk服务器宕机(4.5章)而错 过了修改操作而导致其失效。失效的副本不会再进行任何修改操作,Master服务器也不再返回这个Chunk副本的位置信息给客户端。它们会被垃圾收集系 统尽快回收。

由于Chunk位置信息会被客户端缓存,所以在信息刷新前,客户端有可能从一个失效的副本读取了数据。在缓存的超时时间和文件下一次被打开的时 间之间存在一个时间窗,文件再次被打开后会清除缓存中与该文件有关的所有Chunk位置信息。而且,由于我们的文件大多数都是只进行追加操作的,所以,一 个失效的副本通常返回一个提前结束的Chunk而不是过期的数据。当一个Reader(alex注:本文中将用到两个专有名词,Reader和Writer,分别表示执行GFS读取和写入操作的程序)重新尝试并联络Master服务器时,它就会立刻得到最新的Chunk位置信息。

 

即使在修改操作成功执行很长时间之后,组件的失效也可能损坏或者删除数据。GFS通过Master服务器和所有Chunk服务器的定期“握手” 来找到失效的Chunk服务器,并且使用Checksum来校验数据是否损坏(5.2章)。一旦发现问题,数据要尽快利用有效的副本进行恢复(4.3 章)。只有当一个Chunk的所有副本在GFS检测到错误并采取应对措施之前全部丢失,这个Chunk才会不可逆转的丢失。在一般情况下GFS的反应时间(alex注:指Master节点检测到错误并采取应对措施)是几分钟。即使在这种情况下,Chunk也只是不可用了,而不是损坏了:应用程序会收到明确的错误信息而不是损坏的数据。
2.7.2 程序的实现
使用GFS的应用程序可以利用一些简单技术实现这个宽松的一致性模型,这些技术也用来实现一些其它的目标功能,包括:尽量采用追加写入而不是覆盖,Checkpoint,自验证的写入操作,自标识的记录。
在实际应用中,我们所有的应用程序对文件的写入操作都是尽量采用数据追加方式,而不是覆盖方式。一种典型的应用,应用程序从头到尾写入数据,生 成了一个文件。写入所有数据之后,应用程序自动将文件改名为一个永久保存的文件名,或者周期性的作Checkpoint,记录成功写入了多少数据。 Checkpoint文件可以包含程序级别的校验和。Readers仅校验并处理上个Checkpoint之后产生的文件region,这些文件 region的状态一定是已定义的。这个方法满足了我们一致性和并发处理的要求。追加写入比随机位置写入更加有效率,对应用程序的失败处理更具有弹性。 Checkpoint可以让Writer以渐进的方式重新开始,并且可以防止Reader处理已经被成功写入,但是从应用程序的角度来看还并未完成的数 据。
我们再来分析另一种典型的应用。许多应用程序并行的追加数据到同一个文件,比如进行结果的合并或者是一个生产者-消费者队列。记录追加方式的 “至少一次追加”的特性保证了Writer的输出。Readers使用下面的方法来处理偶然性的填充数据和重复内容。Writers在每条写入的记录中都 包含了额外的信息,例如Checksum,用来验证它的有效性。Reader可以利用Checksum识别和抛弃额外的填充数据和记录片段。如果应用不能 容忍偶尔的重复内容(比如,如果这些重复数据触发了非幂等操作),可以用记录的唯一标识符来过滤它们,这些唯一标识符通常用于命名程序中处理的实体对象, 例如web文档。这些记录I/O功能(alex注:These functionalities for record I/O)(除了剔除重复数据)都包含在我们的程序共享的库中,并且适用于Google内部的其它的文件接口实现。所以,相同序列的记录,加上一些偶尔出现的重复数据,都被分发到Reader了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值