Loading

GFSPaper

周末躺不平,卷不动,摆不烂,随便翻译一篇Paper吧。配合MIT6.824 Lecture3来看可能效果会更好。
原文:The Google File System
作者:Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung @ Google

引言

我们设计并实现了Google File System(GFS)来满足谷歌日益增长的数据处理需求。与先前的分布式文件系统一样,GFS的目标中也包括性能、扩展性、可靠性以及可用性等,只不过它的设计是由我们的应用在当前以及未来的工作负载以及技术环境来驱动的,这与早期的那些系统的一些设计假设是明显偏离的。我们重新审视了传统的设计选择,并从根本上探寻设计上的不同点。

首先,组件错误(component failures)并不是意外,而是常态。文件系统是由成百上千的廉价设备构成的,并且会被数量相当可观的客户端机器访问。机器的数量和质量导致在某个时间点,一些机器可能使无法正常工作的,并且可能有一些并不能从当前的错误中恢复。从我们的经验来看,错误可能来自应用bug、操作系统bug、人工错误以及磁盘、内存、连接器、网络甚至电源故障。因此,一个持久的检测、错误检测、容错以及自动恢复机制必须整合到系统中。

其次,如果用传统的标准来看,我们的文件非常庞大,几个GB的文件是家常便饭,每一个文件通常包含许多应用对象,比如web文档。当我们要经常处理大量TB级的,由数十亿对象构成的快速增长的数据集时,没人会想要用几十亿个KB级的文件来保存它们,即使操作系统可以支持你这样做。所以,一些设计假设以及类似IO操作、块大小这种参数需要被重新考量。

第三,许多文件的修改都是追加新数据,而非覆盖已有数据,实践中几乎不会对一个文件进行随机写,那些被写入的数据通常都只会被读取,并且经常是顺序读取的,很多种数据都具有这种特性。比如,一些可能构建成了一个大的仓库,并且有一个数据分析程序会扫描这些数据;一些可能使由正在运行的程序持续生成的数据流;另一些则可能是归档数据;一些可能作为媒介存储一个机器生产的结果,另一个机器会同步的或在稍后去处理它。在这种大文件的访问模式下,追加操作变成了性能优化以及原子性保证的焦点,也正是因此,在客户端缓存数据块的做法也许就没那么有吸引力了。

译者:如果读写是随机的,那么在客户端缓存数据块更容易带来性能提升,考虑操作系统的page cache、数据的buffer pool、CPU上的三级缓存。但一旦这个读写是顺序的,缓存数据块的提升便没那么大,而且还要考虑分布式系统中缓存一致性的问题,导致整个系统复杂化。

最后,采用应用与文件系统API一起设计提升了我们的灵活性,从而使得整个系统受益。举个例子,我们已经放宽了GFS的一致性模型以在不用给程序带来额外负担的情况下大量简化我们的系统。我们也引入了原子追加操作,这样多个客户端可以向一个文件中并发的追加而不需要在它们之间有任何额外的同步。我们会在后面讨论这些。

目前,我们已经为不同的需求部署了多个GFS集群,最大的已经超过1000个存储节点,300TB的磁盘空间,数以百计的客户端在不同的机器上持续且频繁的访问这些集群。

概要设计

假设

在设计一个符合我们需求的文件系统时,我们一直以这些机会与挑战并存的假设作为指导。之前我们已经提到了一些,现在我们将更详细的说明我们的假设。

  • 系统使用很多便宜的、经常出错的组件构建。系统必须持续监控自己并探测、容错以及从错误组件中恢复
  • 系统存储中等数量的大文件,预期是几百万个文件,通常在100MB或更大的大小。几个GB的文件将更为常见,并且我们需要它被高效的管理。小文件也是受支持的,但并不需要为它们而优化。
  • 主要的工作负载包含两类读取:大的流式读取以及小的随机读取。在大的流式读取中,单个操作通常读取几百KB,更常见的是1MB或更多,同一客户端的后续操作通常会读取一个文件的连续区域。小的随机读取通常在任意offset读取几KB的数据。注重性能的程序通常会将小读取打包并排序以在程序中稳步前进,而非来回移动。
  • 我们也会有很多大的,顺序写的工作负载,即向文件中追加数据。通常,这些操作的大小和读取时的非常类似,一旦写入,文件几乎不会被修改。在任意位置的小型写入也支持,但是不会太高效。
  • 系统必须高效的实现多个客户端并发追加到同一文件的well-define的语义。我们的文件通常被用在生产者、消费者队列或多方合并上,几百个生产者在独立的机器上运行,会并发的追加到文件中,所以最小化同步开销的原子性保证是必要的。文件在稍后会被读取,或者同时被消费者读取。
  • 高持续的带宽比低延迟更加重要。我们的很多目标程序都非常重视以高速率批量的处理数据,而很少有程序对单个读或写的响应时间有严格要求。

接口

GFS提供了一个与文件系统接口类似的接口,但它没有实现如POSIX之类的标准API。文件被目录组织出层级结构,并且可以通过路径名唯一标识。我们支持常见操作:createdeleteopencloseread以及write文件。

此外,GFS具有snapshot(快照)以及record append(记录追加)操作。快照可以以低成本创建一个文件或目录树的拷贝;记录追加允许多个客户端并发地向同一个文件追加数据,同时保证每一个单独的客户端追加的原子性。这在实现多方合并结果以及生产者消费者队列时非常有用,在这些场景中,许多客户端会在不使用额外的锁的情况下同时追加,我们发现这种类型的文件在构建大型分布式应用时是很有用的。快照以及记录追加将在3.4、3.3节进行相应的讨论。

架构

一个GFS集群包含单一的master以及多个chunkserver,集群会被多个client访问,如图一所示。每一个都是一个典型的运行用户级服务进程的商用Linux计算机。在机器资源允许并且可以忍受由于较为不稳定的应用代码带来的低可靠性可以被忍受的情况下,将chunkserver和client运行在同一台机器上也没什么不妥。

img

文件被分割成固定大小的chunk,每一个chunk被一个全局的、不可变的64位chunk handle标识,chunk handle由master节点在chunk创建时分配。chunkserver将chunk以Linux文件的形式存储在本地磁盘上,并且对chunk文件的读写操作都是被一个chunk handle和字节范围指定的。出于可靠性,每一个chunk被复制到多个chunkserver上。默认情况下,我们保存三份副本,并且用户可以对文件命名空间的不同区域设置不同的复制级别。

master维护所有文件的元数据,包括命名空间(namespace)、访问控制信息(access control information)、文件到chunks的映射、chunks的当前位置(location)。它也会控制系统范围的活动,比如chunk租期管理、孤儿chunk的垃圾回收以及chunk在chunkserver之间的合并。master会周期性的通过HeartBeat消息与每一个chunkserver交流,以给它们发送指令以及收集它们的状态。

译者:考虑单个大文件被分成多个大小相等的chunk,并且每一个chunk会被复制到多台chunkserver上,chunkhandle是一个chunk的标识。那么client想要访问某一个文件的某一个chunk时,它要计算出一个chunk id(比如文件第一个chunk的id为0,第二个为1),所以master必须维护一个文件名、chunk id到对应chunkhandle的映射。此外,一个chunkhandle所标识的chunk会在多个副本上,master必须有一个chunkhandle到它代表的所有chunk列表的映射。

链接到每个应用程序的GFS客户端代码实现文件系统的API,并于master和chunkserver通信,代表该应用程序来读写数据。客户端与master交互来获得元数据,但所有数据交流都是直接和chunkserver进行的。我们不需要实现POSIX API,因此不需要hook到Linux的vnode层。

client和chunkserver都不需要缓存文件数据。client缓存的收益很小,因为大多数应用流式访问大文件,或者有着大到无法缓存的工作集。而不使用缓存会消除缓存一致性问题,从而简化client以及系统整体。而chunkserver不需要缓存文件数据,因为chunk就存在于它们的本地文件中,Linux的buffer cache已经将频繁访问的数据保持在内存中了。

单Master

使用单master极大程度的简化了我们的设计,并且允许master使用它的全局知识去做精确的chunk放置以及复制决策。然而,我们必须最小化它在读写操作中的参与程度,以让它不至于成为系统的瓶颈。client永远不会通过master来读写文件数据,它只是询问master它应该联系哪个chunkserver,它在有限的时间内缓存这些信息,并且,余下的操作都直接和chunkserver交互。

我们来解释下图1中那个简单读操作的交互吧。首先,使用固定大小的chunk,client可以将文件名和byte offset翻译成文件的chunkindex,然后,它给master发送一个包含文件名和chunkindex的请求,master回复对应的chunkhandle以及副本们的位置,client使用文件名和chunkindex作为key缓存这个信息。

然后,client便可以发送请求到这些副本其中之一了,通常是最近的那个。请求指定要读取的chunkhandle以及该chunk中的字节范围。在同一个chunk的后续读取不再需要client-master之间的交互了,直到缓存信息过期或者文件被重新打开。事实上,client通常会在一个请求中询问多个chunk,master也可以包含那些它们后续即将请求的那些chunk的信息,这些额外信息避免了未来的几次client-master交互,并且几乎是无成本的。

chunk大小

chunk大小是一个关键设计参数。我们选择了64MB,这要比常见的文件系统块大小大得多。每一个chunk的副本在一个chunkserver上以一个普通Linux文件存储,并且它们是按需扩展的。惰性空间分配避免了由于内部碎片带来的空间浪费,或许这也是对“使用如此之大的chunk大小”最大的一个反对原因。

大chunk提供了很多重要的优势。首先,它降低了客户端与master的交互需求,因为对一个chunk的读写只需要一个初始的请求。对于我们的工作负载,这种降低更加明显,因为应用大部分都是顺序的读写大文件。即使是对于小的随机读,对于一个几TB级别的working set,client也可以轻易的缓存所有的chunk位置信息。另外,由于大chunk,client将会更加倾向于在一个给定chunk上执行许多操作,通过延长一段时间持久化到chunkserver的TCP连接,可以减少网络开销。第三,这减少了保存在master上的元数据的大小,这允许我们在内存中保持metadata,这会带来2.6.1节中我们将讨论的另一个优势。

在另一方面,一个大的chunk大小,即使是使用惰性空间分配也有它自己的劣势。一个只有几个chunk的小文件,或者说也许只有一个chunk,如果很多客户端在访问相同的数据时,chunkserver上存储的chunk可能会成为热点。在实践中,热点不会成为一个主要问题,因为我们的应用通常顺序读取一个大的,多chunk的文件。

不管咋说,热点问题还是在GFS首次被用在一个批量队列系统时被发现了:一个可执行程序作为一个单chunk文件被写入到GFS中,然后在同一时间开启数百台机器,存储这个可执行程序的少量的chunkserver被数百个同时的请求打垮。我们通过使用更高的复制因子以及让批量队列系统的应用程序启动时间错峰来解决这一问题。一个潜在的长期解决办法是允许client在这种情况下从其它client读取数据。

元数据

master存储了三类主要数据:文件和chunk命名空间、文件到chunk的映射、每一个chunk副本的位置,所有metadata都保存在master的内存中。前两种类型同时会被持久化到磁盘上,通过将改动记录到一个保存在master本地磁盘并且复制到远程机器上的操作日志来持久化。使用日志允许我们简单、可靠的更新master的状态,并且在master崩溃这种事件发生时我没有不一致的风险。master不持久化存储chunk位置信息,它只是在自己启动时来询问每一个chunkserver它的chunk信息,在一个chunkserver加入集群中时也会询问。

内存数据结构

因为元数据被存储在内存中,master的操作是很快的。并且,master可以简单且高效的在后台周期性的扫描它的整体状态。这种周期性扫描被用于实现chunk的垃圾回收、在chunkserver错误时的重复制以及chunk整合(在chunkserver间提供负载以及磁盘空间的均衡)。4.3,4.4节会进一步讨论这些活动。

这种使用内存方式的一个潜在的问题是,chunk的数量以及整个系统的容量被master具有多少内存限制。在实践中,这个限制没那么严重,对于每一个64MB的chunk,master维护小于64字节的元数据。大量的chunk是满的,因为大部分文件包含多个chunk,只有最后一个是部分填充的。类似的,文件命名空间数据中,一个文件通常也只需要小于64字节,因为它使用前缀压缩来保存文件名。

如果必须要支持更大的文件系统,给master添加额外的内存的成本对于通过在内存中存储元数据而获得的简单性、可靠性、性能以及灵活性来说是一个很小的代价。

chunk位置

master不持久化保存一个chunkserver具有一个给定chunk的副本的数据,它只是简单的在启动时从chunkserver拉取这些信息,此后,master便可以保持最新状态(up-to-date),因为它控制着所有的chunk存放(placement)以及通过常规的HeartBeat消息来监视chunkserver状态。

我们一开始尝试将chunk位置数据也持久化在master上,但是我们决的在启动时请求chunkserver以及后续周期性的请求更加简单。这消除了在chunserver加入或者离开集群时、改名时、错误时以及重启时使master和chunkserver同步的问题。在一个具有几百台服务器的集群中,这些事件将经常发生。

理解这个设计决策的另一个方式是意识到chunkserver对于一个chunk是否在它的磁盘上有最终解释权。我们无需在master上维护这些信息的一致性视图,因为chunkserver上的异常可能导致chunk凭空消失(比如磁盘坏了)或者一个操作员可能重命名了chunkserver。

操作日志

操作日志包含了关键元数据改动的历史记录,它是GFS的核心。它不仅仅只是元数据的持久化记录,也可以作为一个逻辑时间线来定义并发操作的顺序。文件以及chunk,和它们的版本(version)一样(见4.5节),它们都是被它们创建时的逻辑时间唯一且永久的标识的。

因为操作日志很关键,我们必须可靠的存储它,并且直到元数据修改被持久化之前都不能对用户可见。否则,即使chunk本身存在,我们也可能丢失整个文件系统或最近的client操作。因此,我们在多个远程机器上复制它并且只有在将对应的日志刷到本地和远端的磁盘后才会响应client的操作。master会在刷盘前打包多个日志记录以减少刷盘和复制对整个系统吞吐量带来的冲击。

master通过重放操作日志来恢复它的文件系统状态。为了最小化启动时间,我们必须让保持log在很小的大小。master的checkpoint是log增长到一个特定大小时的状态,我们可以通过从磁盘中从最后的checkpoint加载并只重放在那之后有限数量的log记录。checkpoint是一个紧凑的B-tree形式,可以直接被映射到内存中用于名称空间解析,而不需要额外的parsing这进一步加快了恢复速度,提高可用性。

因为构建一个checkpoint可能会花一段时间,master的内部状态结构设计可以让checkpoint在创建的同时不会对新来的改动产生延迟。master切换到一个新的log文件,并在一个新的线程中创建新的检查点,新的检查点包括在此次切换之前的所有变化。在一个具有几百万文件的集群中,它可以在几分钟内创建检查点,当完成时,它会写入本地和远端磁盘。

恢复只需要最近完成的checkpoint以及后续的log文件,更老的checkpoint以及log文件可以被自由的删除,不过我们会保留一些检查点和日志以做灾备。检查点期间的错误不会影响正确性,因为恢复代码会检测并跳过不完整的checkpoint。

一致性模型

GFS拥有一个宽松的一致性模型去很好的支持我们高度分布式的应用,并且实现起来依然相对简单和高效。我们现在讨论GFS的保证并且这些保证对应用来说意味着什么。我们也会着重介绍GFS如何维护这些保证,但是会把细节留到本文的其它部分。

GFS的保证

文件名称空间修改(比如文件创建)是原子的。它们被master互斥的处理:名称空间锁会保护原子性和正确性(见4.1)。master操作日志定义了这些操作的全局的总体顺序。

在一次数据修改后,一个文件区域的状态取决于修改的类型、它是成功或是失败了,并且它们是否是并发修改的。表1总结了结果。

img

一个文件区域是一致的(consistent)如果所有的客户端都能看到相同的数据,不论它们是从哪个副本读取。在一次文件数据操作后,一个文件区域是确定的(defined),如果它是一致的并且客户端将会看到它完整的写入。当一次修改在没有并发写的情况下成功了,受影响的区域就是确定的(并且也一定是一致的):所有的client总会看到改动已经写入了。并发的成功修改会使得文件区域是不确定的(undefined),但却是一致的:所有的客户端都会看到相同的数据,但是这并不能反映任何一次修改的写入,通常,它包含来自多个修改的混合片段的组合。一个失败的修改使得region不一致(并且也是不确定的):不同的客户端可能在不同的时间看到不同的数据。我们会在下面描述我们的应用程序如何从非确定的区域中识别出确定的区域。程序不需要进一步识别不同类型的非确定区域。

译者:是否这里的失败修改的意思是某一些副本上修改成功了,某一些失败了,此时就会不一致且不确定。

数据修改可能是写入(writes)或记录追加(record appends)。写入会导致数据被写到一个应用程序指定的文件offset处,而记录追加会导致即使在并发修改出现的情况下,数据(“记录”)也能被自动的至少一次追加,不过是在一个GFS选择的offset处(3.3节)。(相反的,一个“常规的”追加操作只是一次在client认为的当前文件的尾部的offset处的写入)。offset被返回给客户端,并标记包含该记录的确定区域的起始位置。此外,GFS可能在两条记录之间插入填充或冗余记录,它们占据的区域被认为是不一致的,并且通常与用户数据的数量相比非常少。

在一系列成功的修改后,改动的文件区域被确保是定义的并且包含最后一次改动写入的数据。GFS如何达到这一点呢?它通过向改动的chunk的所有副本上应用相同顺序的修改以及使用chunk版本号来检测任何由于宕机而过时的chunkserver。过时的副本将永远不会再次被卷入修改中,也不会在client向master询问chunk位置时被返回,它们将在最早一次垃圾回收中被清理。

由于client缓存了chunk位置,它们可能从一个过时的副本上读取,只要它的信息还没被刷新,这个时间窗口受限于缓存条目的超时时间以及该文件的下一次打开(会清理该文件相关的所有chunk信息的缓存)。此外,我们的大多数文件都是只追加的,所以过时的副本返回的通常是之前的chunk末尾,而非过时数据。当读取方重试并且联系master,它将立即获得当前的chunk位置。

在成功修改后,组件依然可能会损毁数据。GFS通过在所有chunkserver和master之间进行常规握手来识别错误的chunkserver,以及通过checksum来检测数据损坏(见5.2节)。一旦问题复现,数据便会从有效副本上尽快恢复(见4.3节)。一个chunk只有在所有它的副本都在GFS能够发现之前丢失了才是不可逆的丢失,这通常要在几分钟内发生。即使是在这种情况下,chunk也是变得不可用,而非损坏:程序只会收到清晰的异常而非损坏的数据。

译者:当数据中的某些位由于某些原因翻转了,或者丢失或增加了某些位称作数据损坏,程序若获得损坏的数据可能造成任何undefine的行为,这很危险。程序不知道数据是损坏的,它会当成正常数据来用。

对应用的影响

使用GFS的应用可以使用一些简单的技术来适应这种宽松的一致性模型:依赖追加而非覆盖、checkpoint、写时自校验、自标记记录......一般来说这些技术本来也需要被用于其他目的。

译者:它意思是GFS的宽松一致性模型可能出错,应用侧可以再自己去做这些保证

实践中我们所有的程序都是通过追加而非覆盖来修改文件的。在我们的典型使用场景中,写入方从头到尾生成一个文件,当写入所有数据后,它自动把文件重命名成一个永久的名字,或周期性的checkpoint已经成功写入了多少。checkpoint中可能也包含了应用层checksum,reader会校验并处理直到上次的checkpoint区域,这个区域当前已经是确定状态了。无论是一致性问题或并发问题,这种方式都可以很好的工作。追加写比随机写要高效的多,并且在错误发生时的弹性也更高。checkpoint允许写入程序以增量方式重启,并防止读取程序处理已经写入但在应用角度看仍不完整的文件数据。

在其它典型的使用场景下,很多写入并发追加到一个合并结果文件或者一个生产者消费者队列。记录追加的至少追加一次语义保存了每一个写入程序的输出。读取程序以如下方式处理偶发的填充或冗余记录。写者准备的每一条记录都包含的额外信息(如checksum),所以它的有效性可以得到验证。一个reader可以使用checksum识别并且抛弃额外的填充以及记录碎片。如果无法容忍偶发性的重复(比如它们会触发一个非幂等操作),它会通过记录中的一个唯一标识来过滤数据,这个唯一标识通常也是被需要用来命名对应的应用程序实体的,比如web文档。这些对于记录I/O的功能(除了去重)都已经在库代码中了,并且会被我们的程序共享,并且适用于Google的其它文件接口实现。有了它,相同的记录序列(以及罕见的重复记录)总会被递送给记录读取方。

系统交互

我们的设计是最小化了master在所有操作中的参与程度的。在这个背景下,我们现在开始介绍client、master以及chunkserver是如何交互的,以及chunkserver如何实现数据修改、原子记录追加以及快照。

租赁与修改顺序

一次修改(mutation)代表改动一个chunk的内容或元数据的操作,比如一个写或一个追加操作,每一次修改都在chunk的所有副本上执行。我们使用租赁(lease)来在副本间维护一致性修改顺序。master将一个chunk租赁给其副本中的一个,我们称该副本为primary。primary会为chunk的所有修改选择一个串行顺序,所有的副本在应用修改时都遵循该顺序。因此全局的修改顺序首先被master选择的租赁授予顺序定义,而在一次租赁中,被primary分配的序列号定义。

租赁机制的设计最小化了master的管理开销。一个租赁有一个60秒的超时时间,然而只要chunk正在变更,primary便可以无限期的请求从master延长。延长请求以及授权通常是与master和所有chunkserver之间的HeartBeat信息一起发送的。master有时可能会尝试在过期之前回收授权(比如master想要在文件要被重命名时关闭修改)。即使master丢掉了和primary之间的通信,它也可以在老的租约过期后安全的给另一个副本授予一个新的租约。

在图2中,我们通过如下的这些步骤描述了一个写入的控制流:

img

  1. 客户端询问master,哪一个chunkserver持有着这个chunk的当前租约?以及其它副本的位置。如果没有人持有租约,master会选择一个副本授予(图中没有展示)。
  2. master回复primary的身份以及其它(secondary)副本的位置。client会为后续的修改缓存这些数据。client只会在primary不可达时或者回复它已经不再持有租约时重新联系master。
  3. client向所有的副本推送数据,它可以以任意的顺序做这件事。每一个chunkserver将会将数据存储在一个内部的LRU缓冲中直到数据被使用或过期(aged out)。像这样将数据流和控制流解耦,我们可以基于网络拓扑来调度昂贵的数据流而不管chunkserver是否是primary,这样可以提升性能。3.2小节中我们会讨论。
  4. 一旦所有的副本都ack它们接收到了数据,client就会发送一个写入请求到primary,请求中标识了之前已经推送到所有副本的数据。primary会为所有它接收到的修改分配连续的序列号(有可能是从多个client接收的),从而提供必要的序列化。它以序列号的顺序向自己的本地状态应用这些变化。
  5. primary将写入请求转发到所有的secondary副本上,每一个secondary副本都以相同的,primary分配的序列号顺序应用变化。
  6. 当全部secondary都回复了primary时,意味着它们完成了操作
  7. primary向client回复。任何副本中遇到的errors都将报告给client。在错误发生时,写入也许已经在primary以及任意数量的secondary副本的子集中发生了(如果错误在primary中发生,是不可能分配一个序列号并且转发给其它副本的),此时写入应该被认为是失败的,被修改的区域处于不一致状态,我们的client代码会通过重试失败的修改来处理这种异常,它会在步骤3和7之间做几次尝试,然后再从头重试这个写操作。

如果一次应用程序的写入很大,或跨越了chunk边界,GFS的客户端代码会将它拆分成多个write操作,它们都遵循上面介绍的控制流,但是可能会被来自其它client的操作插入或覆盖。因此共享的文件区域可能会包含来自不同client的片段,虽然这些副本是一致的,因为独立的操作都以同样的顺序再所有副本上完成。这会使文件处于一致但非确定的状态,我们在2.7中提到过。

数据流

我们解耦了数据流和控制流以更加高效的使用网络。即使控制流是从客户端到primary,再到所有的secondary的,但数据却是沿着一个精心选择的由chunkserver组成的链以一种流水线的形式被推送的。我们的目标是让每一台机器的带宽得到完全利用,避免网络瓶颈以及高延迟链路,最小化传输全部数据的延迟。

为了完全利用每一台机器的网络带宽,数据被线性的沿着chunkserver的一条链推送,而非使用其它拓扑形状分发(比如tree)。因此,每一台机器的全部输出带宽都被用于传输尽可能快的传输数据,而不是被多个收件方瓜分。

为了尽可能避免网络瓶颈以及高延迟链路(比如交换机之间的链路通常都是),每一个机器都将数据转发到网络拓扑中离它最近的哪个还没收到的机器上。假设客户端正要将数据推到chunkserver S1到S4上,它将将数据发到离他最近的chunkserver上,比如S1,S1需要将数据转发到S2到S4中离它最近的哪个chunkserver上,假设是S2,类似地,S2将转发到S3到S4中离它最近的那个,以此类推。我们的网络拓扑简单到足以通过IP地址精确估算距离。

最后,我们在TCP连接之上通过流水线数据传输来最小化延迟。一旦一个chunkserver收到一些数据,它就立即开始转发。流水线对我们特别有用,因为我们使用全双工的交换机网络,立即发送数据并不会降低接受速率。如果没有网络拥塞发生,理想的传输\(B\)字节到\(R\)个副本上的时间消耗是\(B/T + RL\)\(T\)是网络的吞吐量,\(L\)是在两台机器之间传输的延时。我们的网络链路通常是100Mbps(\(T\))的,\(L\)通常会远远低于1ms,因此,1MB理想状态下可以在80ms内被分发完成。

原子记录追加

GFS提供一个原子追加操作,我们称作记录追加。在传统的写入中,client指定了数据要被写入的offset,到一块区域的并发写入并不是串行的:区域里最终可能会包含来自多个客户端的数据片段。而在记录追加中,客户端只指定要写入的数据,GFS会将记录原子地自动地至少追加一次(比如作为一个连续的字节序列),追加发生在GFS选择的一个offset处,并且这个offset会返回给client。这和在Unix系统中向使用O_APPEND模式打开的文件中写数据的行为有点类似,但(由于原子保证)我们不会有竞态条件发生。

我们的分布式应用程序大量使用记录追加,很多在不同机器上的client并发地向同一个文件追加。如果它们使用传统的写入,客户端必须额外的复杂且昂贵的同步,比如通过一个分布式锁管理器。在我们workload中,这样的文件通常作为多producer/单consumer队列,或者包含来自多个不同client的合并结果。

记录追加是修改的一类,也遵循3.1节中的数据流,只是在primary中有一些额外逻辑。client会将数据推送到所有的副本的该文件的最后一个chunk上,然后,它发送请求到primary。primary检查是否追加会导致chunk超过最大的64MB大小。如果是,它需要填充chunk到最大大小,告诉secondaries去做同一件事,并且告诉client操作将在下一个chunk上被重试。(记录追加被限制在最大1/4的最大块大小,以确保在最坏情况下的碎片仍然是可接受的)。如果记录可以装到最大大小中,这也是最常见的情况,primary将追加数据到它的副本中,告诉secondary在指定的offset上写入数据,最后给客户端返回成功。

如果一个记录在任何一个副本上追加失败,客户端重试这个操作。结果是,相同chunk的副本可能包含不同的数据,包括相同记录的完整或部分冗余数据。GFS不保证所有副本都是字节一致的,它只保证数据至少作为原子单元被写入一次。这个性质很容易从简单的观察中得出:对于成功的操作,数据一定已经在某个chunk的所有的副本的相同的offset处被写入了。在这之后,所有的副本的长度(该chunk)至少到达该记录的尾部,因此任何未来的记录都会被分配到一个更高的offset或一个不同的chunk中,即使另一个副本变成了primary。在我们的一致性保证中,成功执行记录追加操作的区域的数据是确定的(因此也是一致的),而那些介于其间的区域是不一致的(因此也是非确定的)。我们的程序可以处理不一致区域,我们在2.7.2节讨论过。

posted @ 2023-12-10 17:04  yudoge  阅读(27)  评论(0编辑  收藏  举报