理解 The Google File System

背景

分布式文件系统是构建整个分布式系统的基石,为分布式计算提供底层数据存储。谷歌早在 2013 年就发表了论文 The Google File System,它在谷歌内部是配合其分布式计算框架MapReduce使用,共同为谷歌搜索等业务提供技术栈支撑。虽然数据量激增以及技术革新使得GFS不断演进,但理解其最初的设计理念、运行原理以及关键实现技术同样让人受益匪浅,并指导着我们实际的学习和工程实践。这篇博文阐述个人对原论文的一些理解与心得,并不是对原论文的完整翻译,因此你需要提前阅读论文。

设计动机与目标

设计一个通用的分布式文件系统是不现实的,它不仅在实现上异常困难(因为不得不考虑所有应用场景),而且实际使用也难以满足要求(往往存在显而易见的性能或容错瓶颈)。GFS 设计初衷是利用数以千计的廉价机器为MapReduce提供底层可靠且高性能的分布式数据存储,以应对海量离线数据存储与处理的应用场景,比如存储应用程序持续产生的日志流以提供离线日志分析。由此,其设计目标为容错可靠(fault tolerance)、高性能读写(high-performance read&write)以及节约网络带宽(save bandwidth)。

一致性(consistency)是分布式系统不可回避的问题。对于分布式文件系统而言,为了提供容错,必须维持数据副本(replica),那如何保证各副本间一致显得至关重要,特别是在应用并发访问场合。一致性是个极其宽泛的术语,你可以实现数据的强一致性(strong consistency)以保证用户始终读到的是最新的数据,这对于用户(客户端)而言是个极佳选择,但它提高了系统的实现难度,因为你必须设计复杂的一致性协议(如Paxos或Raft)来实现强一致性,它也会损害系统性能,典型的它需要机器之间通信以对副本状态达成一致。而弱一致性(weak consistency)则几乎相反。因此,必须根据特定的应用场景,在保证系统逻辑正确的提前下,放宽一致性要求,设计具备良好性能且能提供足够的一致性(sufficient consistency)的系统。对于GFS而言,它针对MapReduce应用程序进行了特定优化,比如,对大文件高性能读取、允许出现文件空洞(hole)、数据记录重复(record duplicate)以及偶尔读取不一致(inconsistent reads)。具体在数据读写方面,其侧重于大规模一次性写入和追加写入,而并非覆盖写和随机写;读取同样倾向于顺序读取,并不关心随机读取。

Trade-off 理念哲学

GFS在设计上存在大量的trade-off。正如前文所述,你不能企图设计出一个完美的系统,而只能针对具体应用场景作出各方面的权衡考量,以达到工程最佳实践目的。

chunk大小设计。GFS针对大文件存储(数百上千兆)设计,因此若存储大量小文件则不能体现其性能。其默认块大小为 64MB。选择大文件作为存储目标原因如下:首先它减少了client与master的交互次数(即使client并不需要整个数据块,但实际上往往存在“就近原则”);另外,这也直接减少了网络带宽;最后,它减少了存储在master内存中的元数据(metadata)的大小。但凡事总利弊相随。较大的块大小设定使得小文件也不得不占用整个块,浪费空间。

集群元数据‘存储。在master的内存中存放着三种类型的元数据:文件和chunk的名称空间(namespace)、文件到chunk的映射信息以及chunk副本的位置信息。且前两种元数据会定期通过operation log持久化到磁盘以及副本冗余。为什么将这些元信息存储到内存?一方面,缓存在内存无疑会提高性能,另外它也不会造成内存吃紧,因为每个64MB 的chunk只会占用到 64B 的内存空间(粗略估算普通 2G 内存的机器可以容纳 2PB 数据),而且为机器增加内存的代价也很小。那为什么chunk位置信息没有持久化?首先master在启动的时候可以通过heartbeat从各chunk server获取。另一方面,chunk的位置信息有时会变动频繁,比如进行chunk garbage collection、chunk re-replication以及chunk migration,因此,若master也定期持久化chunk位置信息,则master可能会成为集群性能bottleneck。从另一个角度来看,chunck是由chunk server保存,而且随时可能发生disk failure而导致chunk暂时不可被访问,因此其位置信息也应该由chunk server负责提供。

chunk副本(默认3个)存放策略。chunk副本选择目标机器的原则包括两个方面:一是最大化数据可靠性(reliability)及可用性(availability),这就要求不能把所有的副本存放在一台机器上,如果此机器的发生disk failure,则数据的所有副本全部不可用。放在同一个机架也类似,因为机架之间的交换机或其它网络设计也可能出现故障。另外一个原则是,最大化网络带宽,如果两个副本的位置相隔太远,跨机架甚至跨数据中心,那么副本的写复制代价是巨大的。因此一般的存放位置包括本机器、同一机架不同机器以及不同机架机器。

垃圾回收。当一个文件被删除,GFS不会真正回收对应的chunk,而只是在log operation记录删除日志后,将对应的文件名设置为隐藏。在一定期限内(默认3天),用户可以执行撤销删除操作。否则,master会通过其后台进程定期扫描其文件系统,回收那些隐藏的文件,并且对应的元数据信息也会从内存中擦除。另外,master的后台进程同时还会扫描孤儿块(orphaned chunk),即那些不能链接到任何文件的chunk,并将这些chunk的元信息删除,这样在后续的heartbeat中让chunk server将对应的chunk删除。这种垃圾回收机制的优点如下:其一,很明显允许用户作出撤销删除操作。其二,统一管理的垃圾回收机制对于故障频繁的分布式系统而言是便捷且可靠的(系统中很容易出现孤儿块);最后,也有利于提升系统性能。垃圾回收发生在后台进程定期扫描活动中,此时masetr相对空闲,它不会一次性将大量文件从系统移除,从而导致 IO 瓶颈,换言之,其chunk回收成本被均摊(amortized)。但其同样有缺点:如果系统中一段时间内频繁出现文件删除与创建操作时,可能使得系统的存储空间紧张(原论文中也提供了解决方案)。

一致性模型 和 原子 Record Append

前文提到GFS并没有采用复杂的一致性协议来保证副本数据的一致性,而是通过定义了三种不同的文件状态,并保证在这三种文件状态下,能够使得客户端看到一致的副本。三种状态描述如下:GFS将文件处于consistent状态定义为:当chunk被并发执行了操作后,不同的客户端看到的并发执行后的副本内容是一致的。而defined状态被定义为:在文件处于consistent状态的基础上,还要保证所有客户端能够看到在此期间对文件执行的所有并发操作,换言之,当文件操作并发执行时,如果它们是全局有序执行的(执行过程中没有被打断),则由此产生的文件状态为defined(当然也是consistent)。换言之,如果某一操作在执行过程中被打断,但所有的并发操作仍然成功执行,只是对文件并发操作的结果不能反映出任一并发操作,因为此时文件的内容包含的是各个并发操作的结果的混合交叉,但无论如何,所有客户端看到的副本的内容还是一致的,在这种情况下就被称为consistent。自然而然,如果并发操作文件失败,此时各客户端看到的文件内容不一致,则称文件处于undefined状态,当然也处于inconsistent状态。

我们先区分几种不同的文件写类型:write指的是由应用程序在写入文件时指定写入的offset;而append同样也是由应用程序来指定写入文件时的offeset,只是此时的offset默认为文件末尾;而record append则指的是应用程序在写入文件时,只提供文件内容,而写入的offset则由GFS来指定,并在写成功后,返回给应用程序,而record append操作正是GFS提供一致性模型的关键,因为它能够保证所有的record append都是原子的(atomic),并且是at least once atomically。这一点并非我们想像的简单,其所谓的at least once atomic,并不表示采用了atomic record append后,即使在客户端并发操作的情况,也能保证所有的副本完全相同(bytewise idetical),它只保证数据是以原子的形式写入的,即一次完整的从start chunk offset到end chunk offset的写入,中间不会被其它操作打断。且所有副本被数据写入的chunk offset是相同的。但存在这种情况,GFS对某一副本的执行结果可能会出现record duplicate或者inset padding,这两种情况的写入所占居的文件区域被称为是inconsistent。而最后为了保证应用程序能够从所有副本看到一致的状态,需要由应用程序协同处理。

如果文件的并发操作成功,那么根据其定义的一致性模型,文件结果状态为defined。这通过两点来保证:其一,对文件的副本应用相同的客户端操作顺序。其二,使用chunk version number来检测过期(stale)副本。

record append操作流程如下:客户端首先去请求master以获取chunk位置信息,之后当客户端完成将数据 push 到所有replica的最后一个chunk后,它会发送请求给primiary chuck server准备执行record append。primary首先为每一个客户端操作分配sequence number,然后立即检查此次的record append操作是否会使得chunk大小超过chunk预设定的值(64MB),若超过了则必须先执行insert padding,并将此操作命令同步给所有副本chunk server,然后回复客户端重新请求一个chunk并重试record append。如果未超过chunk阈值,primary会选择一个offset,然后先在本地执行record append操作,然后同样将命令发送给所有副本chunk server,最后回复写入成功给客户端。如果副本chunk server在执行record append的过程中宕机了,则primary会回复客户端此次操作失败,要求进行重试。客户端会请求master,然后重复上述流程。此时,毫无疑问会造成副本节点在相同的chunk offset存储不同的数据,因为有些副本chunk server可能上一次已经执行成功写入了所有数据(duplicate record),或者写了部分数据(record segment),因此,必须先进行inset padding,使得各副本能够有一个相同且可用的offset,然后才执行record append。GFS将这种包含paddings & record segments的操作结果交由应用程序来处理。

应用程序的writer会为每个合法的record在其起始位置附加此record的checksum或者一个predictable magic number以检验其合法性,因此能检测出paddings & record segments。如果应用程序不支持record duplicate(比如非幂等idempotent操作),则它会为每一个record赋予一个unique ID,一旦发现两个record具有相同的ID它便认为出现了duplicate record。由GFS为应用程序提供处理这些异常情况的库。

除此之外,GFS对namespace的操作也是原子的(具体通过文件与目录锁实现)。

我们再来理解为什么GFS的record append提供的是at least once atomically语义。这种一致性语义模型较为简单(简单意味着正确性易保证,且有利于工程实践落地,还能在一定程度上提升系统性能),因为如果客户端写入record失败,它只需要重试此过程直至收到操作成功的回复,而server也只需要正常对等待每一个请求,不用额外记录请求执行状态(但不表示不用执行额外的检查)。除此之外,若采用Exactly-once语义模型,那将使整个实现变得复杂:primary需要对请求执行的状态进行保存以实现duplicate detection,关键是这些状态信息必须进行冗余备份,以防primary宕机。事实上,Exactly-once的语义模型几乎不可能得到保证。另外,如果采用at most once语义模型,则因为primary可能收到相同的请求,因此它必须执行请求duplicate detection,而且还需缓存请求执行结果(而且需要处理缓存失效问题),一旦检测到重复的请求,对客户端直接回复上一次的请求执行结果。最后,数据库会采用Zero or once的事务语义(transactional semantics)模型,但严格的事务语义模型在分布式场景会严重影响系统性能。

延迟 Copy On Write

快照(snapshot)是存储系统常见的功能。对于分布式系统而言,一个关键挑战是如何尽可能地降低snapshot对成百上千的客户端并发访的性能影响。GFS同样采用的是copy on write技术。事实上,它延迟了snapshot的真正执行时间点,因为在分布式系统中,副本是必须的,大多数情况下,快照涉及的副本可能不会被修改,这样可以不用对那些副本进行 copy,以最大程度提升系统性能。换言之,只有收到客户端对快照的副本执行mutations才对副本进行 copy,然后,将客户端的mutations应用到新的副本。具体的操作流程如下:当master收到客户端的snapshot指令时,首先会从primary节点revoke相应chunk的lease(或者等待lease expire),以确保客户端后续对涉及snapshot的chunk的mutations必须先与master进行交互,并对这些操作执行log operation,然后会对涉及到的chunk的metadata执行duplicate操作,并且会对chunk的reference count进行累加(换言之,那些chunk reference count大于1的chunk即表示执行了snapshot)。如此一来,当客户端发现对已经快照的chunk的操作请求时,master发现请求的chunk的reference count大于1。因此,它会先defer客户端的操作请求,然后选择对应chunk的handler并将其发送给对应的chunk server,让chunk server真正执行copy操作,最后将chunk handler等信息返回给客户端。这种delay snapshot措施能够改善系统的性能。

最后,值得注意的是,虽然客户端并不缓存实际的数据文件(为什么?),但它缓存了chunk位置信息,因此若对应的chunk server因宕机而miss了部分chunk mutations,那客户端是有可能从这些stale的replica中读取到premature数据,这种读取数据不一致的时间取决于chunk locations的过期时间以及对应的文件下一次被open的时间(因为一旦触发这两个操作之一,客户端的cache信息会被purge)。

参考文献:

[1] Ghemawat S, Gobioff H, Leung S T. The Google file system[M]. ACM, 2003.
[2].MIT 6.824 Lecture

作者:Zeng Qiaoqiao
原文链接: https://qqzeng.top/2018/11/11/%E7%90%86%E8%A7%A3-The-Google-File-System/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值