谷歌文件系统(The Google File System译)(1~3章)

栏目: 后端 · 发布时间: 5年前

内容简介:我们设计并实现了一个面向大规模数据密集型应用的可扩展的分布式文件系统,即谷歌文件系统。当运行在廉价硬件上时,它能提供一种容错机制,在大量客户端连接时提供了高内聚的性能尽管与之前的分布式文件系统有着相同的目标,但我们的设计是考量于我们应用的当前和未来的工作负载以及技术环境,这反映了它与一些早期的文件系统预设有着明显的不同,也让我们重新审视传统的设计选择,并探索有根本性差别的设计观点这个文件系统已经成功满足我们的存储需要,已经作为存储平台在谷歌内部广泛部署,用于生成并处理我们用于需要大量数据集的搜索和研发服务的

我们设计并实现了一个面向大规模数据密集型应用的可扩展的分布式文件系统,即谷歌文件系统。当运行在廉价硬件上时,它能提供一种容错机制,在大量客户端连接时提供了高内聚的性能

尽管与之前的分布式文件系统有着相同的目标,但我们的设计是考量于我们应用的当前和未来的工作负载以及技术环境,这反映了它与一些早期的文件系统预设有着明显的不同,也让我们重新审视传统的设计选择,并探索有根本性差别的设计观点

这个文件系统已经成功满足我们的存储需要,已经作为存储平台在谷歌内部广泛部署,用于生成并处理我们用于需要大量数据集的搜索和研发服务的数据。迄今为止最大的集群,在超过一千台机器上的的数千个磁盘上提供了数百TB的存储服务,同时能够响应数百台客户端的并发访问

在本篇论文中,我们将展示用于支持分布式应用的文件系统的接口扩展,讨论我们的许多设计切面,以及微观标准下和现实世界中的一些测量报告

分类和主题描述

D [4]: 3— 分布式文件系统

通用词汇

设计,可靠性,性能,测量

关键词

容错,可扩展性,数据存储,集群存储

1. 介绍

我们设计并实现了谷歌文件系统(GFS)来满足快速增长的谷歌数据处理需求的需要。GFS和之前的分布式文件系统有着许多相同的目标,比如性能,可扩展性,可靠性,以及可用性。然而,谷歌文件系统的设计是受到当前乃至未来我们应用的工作负载和技术环境的观察而推动的,反映了与早期的一些文件系统的设计理念有着根本性的不同。我们重新审视了传统的设计选择,并在设计空间中探索出了一些根本不同的观点

第一,组件失效是一种常态而不是异常。文件系统由数百乃至数千个通过廉价商品零件组装起来的存储服务器组成,能够被相当数量的客户端访问。这些组件的数量和质量在本质上决定了一部分机器在任何给定的时间均无法使用,一部分机器将不会从故障中恢复。我们观察到了很多故障,包括由于应用程序的bug,操作系统的bug,人为的错误,磁盘、内存、连接器、网络,乃至电力系统的故障。因此,常态化的监控,故障发现,容错机制,以及自动恢复机制必须集成到系统中

第二,以传统的标准来看,文件是庞大的,数GB的文件是很常见的。每一个文件一般都由许多类似网页文档等应用程序对象组成,当我们经常处理快速增长的包含数十亿个对象的数TB大小的数据集时,即使文件系统可以支持,管理数十亿个KB左右大小的文件也是不可取的。所以,不得不重新考量设计中的假设和参数,比如I/O操作和块大小的设计

第三,大多数文件是通过追加新数据发生内容改变的,而不是覆写已存在的数据,文件内容的随机写几乎是不存在的。一旦写入,文件就是只读的,而且都是顺序读取的。大量的数据都有这种特点,有些可能是构成数据分析程序扫描的大型存储库,有些可能是运行中的应用持续生成的数据流,有些可能是档案数据,有些可能是在一台机器上生成的用于另一台机器处理的中间结果,不管是同时还是稍后。给定大文件的访问模式, 当缓存数据块在客户端丢失引用时,追加操作就成了性能优化和保证原子性的关键

第四,共同设计应用程序和文件系统的服务接口可以提高灵活性,这对于整个系统是有利的。比如,我们放宽了GFS的一致性模型从而大大简化了文件系统,而没有给应用程序带来繁重的负担。我们还引入了一个原子性的追加操作,使得多个客户端可以同时对文件执行追加操作而不需要在它们之间执行额外的同步操作。这些都将会在接下来进行更详细的讨论。

目前我们部署了多个出于不同目的的GFS集群,其中最大的存储节点拥有超过1000个存储节点,以及超过300TB的磁盘存储,并且被不同机器上的数百个客户端频繁大量地访问

2. 设计概述

2.1 假设

在设计一个满足我们自己需要的文件系统时,我们以充满着机遇与挑战的假设为导向。之前曾提到了一些关键的观察结果,现在我们把我们的假设在这里更详细地罗列出来

  • 系统是由许多经常会故障的廉价商品组件组成,所以必须要经常对它进行监控,以及在例行基础上进行检测、容错,以及故障组件的恢复
  • 系统存储了一定量的大文件。我们预计有数百万个文件,每个一般在100MB或更大。数GB的文件是很常见的情况,应该被有效的管理。小文件必须得到支持,但我们并不需要为它们进行优化
  • 工作负载主要包括两种读取情况:大型流式读取和小型随机读取。在大型流式读取中,单个操作一般会读取数百KB的内容,更常见的是1MB或更多。同一个客户端连续的操作通常会读取文件中的相邻区域。小型随机读取则一般会在随机偏移位置读取几KB的内容。注重性能的应用程序通常会对小量读取进行批处理和排序,以便稳步地向前读取文件而不是来回移动
  • 工作负载还有很多追加数据到文件的大型的顺序写入。一般操作的大小和读取的大小是相似的。一旦写入结束,文件就几乎不再进行改变。文件随机位置的少量写入是被支持的,但是效率不一定高
  • 系统必须为同时对同一文件执行追加操作的客户端有效地实现定义良好的语义。我们的文件通常用作生产者消费者队列或是多方合并。每台机器运行数百个生产者同时为文件执行追加操作,具有最小同步开销的原子操作是必不可少的。文件可以被稍后阅读,而消费者可以同时阅读文件
  • 持续的高带宽比低延迟更重要。我们大多数的应用程序都非常重视以高速率批量处理数据,而几乎不会对单次读写有着严格的响应时间要求

2.2 接口

GFS提供了我们熟悉的文件系统的接口,虽然没有实现例如POSIX的标准API。文件在目录中按层次结构组织,并通过路径名标识。我们也支持诸如创建、删除、打开、关闭、读取,以及写入文件等常规操作

除此之外,GFS还提供了快照和记录追加操作。快照可以以较低的成本创建文件或目录树的一个副本,记录追加操作允许多个客户端同时追加数据到同一个文件中,同时保证每个客户端追加操作的原子性。许多客户端在不需要额外加锁的条件下同时进行追加操作,这一点对于实现多路合并和生产者消费者队列是很有用的,我们还发现这些文件的类型在构建大规模分布式应用中是很有价值的。快照和记录追加将会分别在3.4和3.3节中进一步讨论

2.3 架构

一个GFS集群由单个 master 和数个 chunkserver 组成,可以被多个客户端访问,如图1所示。每个客户端通常都是由一台商用 Linux 服务器来运行用户级的服务器进程。只要机器的资源允许,并且能够接受由于运行可能的碎片应用代码导致的较低的可靠性,那么在同一台机器上运行chunkserver和客户端是很容易的

文件分为固定大小的块,每个块都是由块创建时被master分配的一个全局且不可变的唯一64位块句柄标识,chunkserver将块Linux文件存储在本地磁盘上,块数据的读写由块句柄和字节范围来指定。出于可靠性的考虑,每个块都在多个chunkserver上进行复制。默认情况下,我们存储三个副本,但是用户可以为文件命名空间的不同区域指定不同的拷贝级别

master维护文件系统中包括命名空间,访问控制信息,文件到块的映射关系,以及块的当前位置在内的所有的元数据,同样也控制着系统范围内的活动,比如块的租约管理,孤立块的垃圾收集,还有chunkserver之前的块迁移。master通过心跳消息和每个chunserver进行周期性的通信,以发送指令并收集它们的状态

连接到每个应用程序的GFS客户端代码实现了文件系统API,并代替应用程序与master和chunkserver进行通信,从而完成数据的读取和写入。客户端通过与master交互可以进行元数据的操作,但是所有承载数据的通信都必须直接进入chunkserver,我们不提供POSIX API,因此也不需要挂载到Linux的vnode层

客户端和chunkserver都不缓存文件数据。客户端的缓存没有意义,因为大多数的应用程序使用大型文件流工作,或是工作集过大导致难以缓存。没有这些地方的缓存就消除了缓存一致性,从而简化了客户端和整个系统(但是客户端会缓存元数据)。chunkserver不需要缓存是因为块作为本地文件存储,Linux的缓存已经将频繁访问的数据保存在内存中了

谷歌文件系统(The Google File System译)(1~3章)

2.4 单个Master

只拥有一个master可以极大简化我们的设计,同时保证master能够应用全局信息来针对块的放置和备份作出复杂的决策。然而,我们必须将其在读写操作中的参与度降到最小,以避免成为瓶颈。客户端永远不会通过master进行文件的读写,相反的,客户端只是询问master它应该与哪个chunkserver建立通信。master在有限的时间内会缓存此信息,并与chunkserver直接进行交互来执行许多后续操作

我们参考图1来解释一个简单的读取教交互过程。首先,使用固定的块大小,客户端将应用程序中指定的文件名和字节偏移量转化为块索引。然后,客户端向master发送包含有文件名和块索引的请求,master会回复对应的块句柄和副本的位置。客户端会使用文件名和块索引作为键值来缓存这些信息

接下来客户端会向其中一个副本发送请求,通常是最近的那一个。请求中指定了块句柄和块中的字节范围。在缓存过期或是文件重新打开之前,对同一个块的后续读取操作不需要与master再进行交互。实际上,客户端通常会在一次请求中请求多个块,master也可以将这些请求的块信息包裹在一起返回。这些额外的信息几乎不需要什么开销就避免接下来的一些客户端和master的交互

2.5 块大小

块大小是一个关键的设计参数。我们选择了64MB作为块大小,这比一般的文件系统的块大小要大得多。每个块副本都作为一个普通的Linux文件在chunkserver上存储,并在需要时进行扩展。惰性空间分配避免了由于内部碎片导致的空间浪费,可能出现的最大碎片要比我们设定的这么大的块大小还要大

大的块有一些重要的优点。首先,减少了客户端与master的交互需要,因为同一个块上的读写只需要给master发送一个初始化请求来获得块定位信息。这些工作量的减少对我们的工作负载尤其重要,因为应用程序大多数情况下都是按序读写大文件,即使对于少量随机读写,客户端也可以方便地缓存一个数TB工作集的所有的块定位信息。其次,因为块很大,客户端更可能会在给定的块上执行大量操作,因此可以通过在较长时间内维持与chunkserver的TCP长连接来减少网络开销。最后,减少了master需要存储的元数据的大小,所以允许我们将元数据保存在内存中,这又给我们带来了将在2.6.1节中讨论的其他优点

另一方面,即使采用了惰性空间分配,大的块也有其缺点。一个小文件由一些或一个小块组成,如果许多客户端都来访问相同的文件,存储这些块的chunkserver可能会成为热点。在实际情况中,热点并不是主要的问题,因为我们的应用程序主要是按序读取多个大块

然而,当批处理队列系统首次使用GFS时,确实产生了热点:一个可执行文件作为单独的块文件写入GFS,紧接着在数百台机器上同时开始执行。存储此文件的少部分chunkserver因为数百个并发请求而导致过载。我们的解决办法是使用更高的备份级别来存储这样的可执行文件,以及使用批处理队列来错开应用程序的启动时间。一个潜在的长远的解决方案是允许客户端在这样的情况下从其他的客户端读取数据

2.6 元数据

master存储了3种主要类型的元数据:文件和块的命名空间,文件到块的映射,以及每个块副本的位置。所有元数据都保存在master的内存中。前两种类型(命名空间和文件到块的映射)通过将更新操作记录到存储在master本地磁盘的操作日志上来保证持久化,这份日志也会在远程服务器上进行备份。使用日志可以让我们简便可靠地更新master的状态,同时也避免了由于master故障导致不一致的风险。master并不会永久存储块的位置信息,相反地,会在master启动或是一个chunkserver加入集群时,来询问每一台chunkserver的块信息

2.6.1 内存数据结构

由于元数据存储在内存中,所有master的操作很快。此外,master可以简单高效地在后台对其整个状态进行定期扫描。这个定期扫描被用来实现块的垃圾收集,当出现故障时进行重备份,以及在chunkserver之间进行块迁移时平衡负载和磁盘空间。我们将在4.3和4.4节进一步讨论这些行为

这种局限于内存的方式有一个潜在的限制,就是块的数量,因此整个系统的容量受到master的内存大小限制,不过在实际场景中并不是很严重的限制。master为每个64MB的块维护了至少64字节的元数据。大多数块都是满的,因为大部分文件都包含了许多块。类似地,对于每个文件而言,因为使用了前缀压缩算法来压缩存储,文件命名空间数据一般需要的字节数要少于64

如果需要支持更大的文件系统,与我们将元数据存储在内存中获得的简便性、可靠性、高性能,以及灵活性相比,为master增加额外的内存的开销几乎算不了什么

2.6.2 块的位置

master不会保留哪些chunkserver拥有给定块的副本这样的持久化信息,而只会在启动时对chunkserver进行轮询来获取这些信息。master可以让自己保持最新状态,因为它控制着所有的块的放置,并通过定期的心跳消息来监控chunkserver的状态

我们最初尝试将块的位置信息持久化保存在master上,但我们认为在启动时以及之后定期的从chunkserver请求这些数据要简单得多。这种方式避免了chunkserver加入和退出集群,更改名称,失效和重启等等情况下需要保持master和chunkserver的同步的问题。当集群中有数百台服务器时,这些情况会频繁地发生

要想理解这样的设计决策,我们要知道,只有chunkserver它本身才能够确定一个块是否存于它的磁盘中,在master中维护这个一致性视图信息是没有意义的,因为chunkserver上的错误可能导致文件块自行消失(例如,磁盘损坏导致不可用),或是管理员可能对chunkserver进行重命名

2.6.3 操作日志

操作日志包含了关键元数据更改的历史记录,它是GFS的核心。操作日志不仅是元数据的唯一持久记录,而且还充当了定义并发操作顺序的逻辑时间线。文件和块,以及它们的版本(参见4.5节),都由它们创建的逻辑时间进行唯一永久的标识

由于操作日志至关重要,所以我们必须要进行可靠的存储,并且在元数据的变更被持久化之前应当对客户端不可见。否则,即使块处于存活状态,我们也会在根本上丢失整个文件系统或是最近的客户端操作信息。因此,我们对操作日志在多台机器上进行备份,并且只有当本地和远程均刷新了磁盘上对应的日志记录后,再响应客户端的操作。master在刷新之前对多个日志记录执行批处理,从而减少了刷新和备份对系统整体的影响

master通过重新执行操作日志来恢复文件系统的状态。为了最大限度缩短启动时间,我们必须让日志尽量小。只要日志增长超过了一定大小,master就会给当前状态设置检查点,以便可以在这之后通过从本地磁盘加载最近的检查点,并重放有限数量的日志来实现系统的恢复。检查点采用压缩的类似B树的结构,不需要额外的解析就可以直接映射到内存中,并使用命名空间来进行查找,这进一步的提高了恢复的速度和系统的可用性

因为构建检查点需要一段时间,所以master的内部状态被构建为可以使得新的检查点在无需对到来的改变进行延时就能够创建的形式。master会使用另一个线程来切换新的日志文件,并创建新的检查点,新检查点包括切换前的所有变更。对于有着数百万文件的集群,可以在一分钟左右被创建。当创建完成后,会写入本地和远程的磁盘。

只需要最新的检查点和其后的日志文件就能够恢复系统状态。更早的检查点和日志文件可以自由删除,但我们也保存了一部分来防止意外情况发生。在检查点生成期间的错误不会影响系统的正确性,因为恢复代码会检测并跳过不完整的检查点。

2.7 一致性模型

GFS具有宽松的一致性模型,可以很好地支持我们的高度分布式应用程序,而且实现起来仍是相对简单和高效的。我们现在讨论GFS提供的保证和其对于应用程序的意义,我们还会重点介绍GFS是如何实现这些保证,具体的细节会在论文的其他部分呈现

2.7.1 GFS提供的保证

文件命名空间的更改(例如文件创建)是原子性的,仅由master负责处理:命名空间锁保证了原子性和正确性(4.1节);master的操作日志定义了所有这些操作的全局顺序

数据变更后,文件区域的状态取决于变更的类型,即变更是否成功,以及是否存在并发更新。表1是对结果的一个概述。如果所有的客户端无论从哪个副本读取,都能看到同样的数据,那我们就说文件区域是一致的。如果区域是一致的,那么我们称区域块在文件数据更新后是已定义的,所有的客户端都能从整体上看到变更的结果。如果一个变更成功执行,且没有被其他并发的写入干扰,那么被影响的区域就是已定义的(意味着一致性):所有的客户端都能够看到更新写入的结果。同时成功执行的变更操作会让该区域具有不确定性,但是依然是一致的:所有客户端都能够看到相同的数据,但是它可能无法反映其中任何一个变更写入的结果。通常,这部分数据由来自多个变更操作的混合片段组成。一个失败的更新可能会导致区域变得不一致(也因此导致不确定性):不同的客户端可能会在不同的时间看到不同的数据。我们会在下面来描述应用程序如何区分已定义和未定义的区域。应用程序不需要对未定义区域做进一步的区分。

写操作或是记录追加都会导致数据变更。写操作在应用程序指定的偏移位置写入数据,记录追加操作即使存在并发的数据变更,也会原子性的执行至少一次追加数据(即“记录”)操作,但是偏移量由GFS指定(3.3节)(相反地,一个“常规”的追加操作仅仅是一个偏移量是客户端认为的当前文件结尾的写操作)。偏移位置会返回给客户端,并标记了包含该记录的已定义区域的起始位置。此外,GFS还可能在其间插入填充或者记录的副本,它们会占用那些被认为是不一致的区域,通常这些数据和用户的数据比起来要小得多

在一系列成功的变更后,更新后的文件区域能够确保是已定义的,并且包含了最后一次更新写入的数据。GFS通过以下方式来实现:(a) 对所有块副本上以同样的顺序来对块执行更新(3.1节),(b) 使用块版本号来检测那些因为chunkserver宕机造成修改丢失的过期的副本(4.5节)。过期的副本将不会参与更新,也不会在客户端向master询问块位置时被返回,它们会被尽早的执行垃圾回收

因为客户端缓存了块的位置,因此在数据刷新之前,可能会读到过期的副本。窗口的大小收到缓存条目的超时时间和下一次打开文件的限制,打开文件的操作会从缓存中清除文件的所有块信息。此外,我们的大多数文件都是只能执行追加操作的,一个过期的副本通常会提前返回块的结束,而不是过时的数据。当读取者进行重试并与master联系时,会立即获得块的当前位置

变更成功执行很久之后,很明显组件故障依然能够污染或损坏数据。GFS通过master和所有chunkserver之间的定期握手来标识那些失效的chunkserver,并通过校验和来检测被污染的数据(5.2节)。一旦出现问题,数据会尽快地从有效的副本中恢复(4.3节)。只有在GFS作出响应前(通常在几分钟之内)所有块的副本均丢失,才会导致真正不可逆的块丢失。即使在这种情况下,也仅仅是变得不可用,而不是损坏:应用程序会接收到明确的报错信息,而不是损坏的数据

2.7.2 对应用程序的影响

GFS应用程序可以通过一些用于其他目的的简单的技术来适应这种弱一致性模型:依赖于追加而不是覆写操作,检查点技术,以及写入自检查和自认证的记录

实际上我们所有的应用程序都是通过追加操作来更新文件,而不是覆写操作。在一个典型的应用场景中,一个写入者从头到尾创建了一个文件,在写完数据后自动将文件重命名为一个永久的名称,或是使用检查点周期性地确认有多少数据被写入。检查点同样包括应用程序级的校验和。读取者仅仅会验证最后一个检查点之前的区域,这些区域可以确认处于已定义的状态。无论一致性和并发性如何要求,这种方法都对我们是很有帮助的。与随机写入相比,追加操作更为高效,对于应用程序的故障的处理也更为灵活。检查点技术允许写入者以增量的方式重启,也让读取者避免处理成功写入的数据,这在应用程序的角度来看仍然是不完整的

另一种典型的应用场景中,许多写入者为了合并结果或是作为生产者-消费者队列,同时对一个文件执行追加操作。记录追加的append-at-least-once(至少一次追加)的语义保证了每一个写入者的输出。读取者处理临时的填充和副本,如下所示。写入者的每条记录都包含了像校验和之类的额外信息,以便验证其有效性。读取者可以通过校验和来识别并丢弃额外的填充和记录段。如果不能容忍偶然的重复数据(例如,触发了非幂等的操作),可以在记录中使用唯一标识来对重复的部分进行过滤,通常不管怎样都需要这些标识来命名对应的应用程序实体,比如网页文档。这些用于记录的输出输出的功能函数(除了重复删除)都在我们应用程序共享的代码库中,同时适用于Google中其他文件接口的实现。使用这些工具,同一序列的记录,再加上一小部分的重复,总是会分发给记录的阅读者

3. 系统交互

我们是出于最大限度地减少master在所有操作中的参与度来设计系统的。在此背景之下,现在我们再来描述客户端、master,以及chunkserver是如何进行交互以实现数据变更,原子性记录追加和快照的操作

3.1 租约与更新顺序

数据更新是指改变元数据或是块的内容的操作,例如写入或追加操作。每一个变更都会在块的所有副本上执行。我们使用租约来维护副本间更新顺序的一致性。master将块的租约授予副本的其中一个,我们可以称其为主副本。主副本会为所有在该块上的更新操作选择一个顺序,其余所有副本都会在执行时遵循这一顺序。因此,全局变更顺序首先是被master选择的租约授予顺序所确定的,然后才是在租约中被主副本所分配的序列号来确定

租赁机制被设计用来最大限度地减轻master的管理负担。一个租约的初始超时时间为60秒,然而,只要块被更新,主副本就可以无限制地向master请求延长租约,通常都会得到回应。这些额外的请求和授权是在master和所有的chunkserver之间的定期交换的心跳消息中捎带的。master有时可能会尝试在到期前撤销租约(例如,master想要禁止一个对文件重命名的更新操作)。即使master和主副本之间断开通信,也可以在租约到期后将新的租约授予另一个副本

在图2中,我们通过编号的写入控制流来说明这一步骤

谷歌文件系统(The Google File System译)(1~3章)
  1. 客户端询问master哪个chunkserver保存着块当前的租约和其他的副本位置。如果都没有拥有租约,则master将从副本中选择一个来授予租约(未在图中显示)
  2. master给客户端回复主副本和其他(次要)副本的位置。客户端会缓存这些数据用于近期的更新操作。只有当主副本无法访问或是不再拥有租约时,客户端才需要重新联系master
  3. 客户端将数据推送给所有副本,顺序并不固定。每一个chunkserver都会把数据保存在内部的LRU缓存中,直到数据被使用或是过期。通过将数据流与控制流分离,我们可以基于网络拓扑来调度昂贵的数据流,从而提高性能,也不用关心哪一个是主副本。3.2节中对这一点进行了更深入的讨论
  4. 一旦所有副本都确认收到了数据,客户端就会向主副本发送写请求,这个请求标识了之前推送给所有副本的数据。主副本会为收到的所有数据变更操作(可能来自于多个客户端)分配连续的序列号,这提供了必要的串行化机制,它会按照序列号的顺序将更新应用于本地
  5. 主副本将写请求发送给其他所有的次级副本,每一个次级副本都会按照主副本分配的同样的序列号顺序来执行数据变更
  6. 次级副本在完成操作后需要向主副本回复消息
  7. 主副本向客户端返回应答。在任何副本上遇到的错误都会汇报给客户端。出现错误时,该写操作可能已经在主副本或其余一部分次级副本上执行成功了(如果在主副本上发生错误,将不会进行序列号的分发),客户端请求仍会被认为是失败的,并且被修改的区域将会处于不一致的状态。我们的客户端代码会通过重试失败的数据变更操作来处理这样的错误,将会在完全退回重写之前,先在第(3)步到第(7)步之间进行一些尝试

如果应用程序的写入操作过于庞大,或是超过了块的边界,GFS客户端代码将会将其分解为多个写入操作。它们都会遵循上述的写入流程,但可能会被来自其他客户端的并发操作交错覆盖。因此,共享文件区域最终可能包含来自于不同客户端的片段,虽然这些副本是一致的,因为各个操作都以相同的顺序在所有副本上成功执行,这会让文件区域保持一致但不确定的状态,如2.7节所述

3.2 数据流

为了更高效地使用网络资源,我们将数据流从控制流中分离出来。控制流从客户端到达主副本,再到其他所有次级副本的这一过程,数据是在认真挑选的chunkserver链上以流水线形式被线性推送的。我们的目的是充分利用每台机器的网络带宽,来避免网络瓶颈和高延迟的链路,同时最小化推送所有数据的延迟

为了充分利用每台机器的网络带宽,数据是沿着一条chunkserver链被线性推送的,而不是像其他拓扑结构的分布那样(例如树型结构)。因此,每一台机器全部的出站带宽都被用来尽快地传送数据,而不是用于为多个接收者进行切分

为了尽可能的避免网络瓶颈和高延迟链路(例如,通常情况下的内部交换链路),每一台机器都将数据转发到网络拓扑结构中尚未接收到数据的离他“最近”的机器。假设客户端将数据发送给chunkserver S1到S4,首先会将数据发送到离它最近的机器,比如S1。S1接下来就将数据转发到S2~S4中离它最近的机器,比如S2。类似地,S2再转发给S3或S4,看哪一个离S2更近,等等。我们的网络拓扑可以简单到能够从ip地址准确地估算“距离”

最后,我们通过在TCP连接上使用流水线来传送数据以最小化延迟。一旦chunkserver接收到一部分数据,就会立刻开始转发。流水线这里对我们来说尤其有用,因为我们使用了全双工链路的交换网络。立即发送数据并不会降低接收速率,在没有网络拥塞的情况下,将B个字节发送到R个副本上经过的时间理想情况下是B/R + RL,其中T是网络的吞吐量, L是两台机器间传送字节的延迟。我们的网络链路一般是100Mbps(T),L远低于1ms。因此,理想情况下,1MB的数据可以在80ms内完成分发

3.3 原子性的记录追加

GFS提供了一个名为record append的原子性追加操作。在传统的写操作中,客户端指定数据写入的位置。在同一区域的并发写操作是不可串行化的:该区域最终可能包含来自多个客户端的数据段。然而在一个记录追加操作中,客户端仅仅需要指定数据,GFS至少会将其原子性的(即,作为一个连续的字节序列)追加到文件中至少一次,追加的位置由GFS选择,并会向客户端返回这个偏移量。这很像Unix中 O_APPEND 的文件打开模式,当多个写入者并发操作时不会产生竞争条件

我们的分布式应用程序大量使用了记录追加操作,不同机器上的多个客户端并发地向同一个文件中进行数据的追加。如果使用传统的写入操作,客户端将会额外需要复杂且昂贵的同步开销,例如分布式锁管理器。在我们的工作负载中,这样的文件通常会作为 多生产者/单消费者 队列,或是不同客户端的归并结果

记录追加是一种数据变更,除了一些主副本上的额外逻辑操作之外,依然会遵循3.1节中所述的控制流。客户端将数据推送给文件最后一个块的所有副本后,会向主副本发送请求,主副本会检查当前块记录的追加操作是否导致块超过了最大大小(64MB),如果超过了,就将块填充到最大大小,并通知其余副本也这么做,然后告诉客户端应在下一个块上进行重试(追加的记录被限制为最大块大小的1/4,以将最坏情况下的碎片控制在一个可接受的水平上)。一般情况下,记录的大小都不会超过最大块大小,如果在这种情况下,主副本所在节点会将数据追加到它的副本上,并让其余的副本节点将数据写在它们控制的副本中确定的偏移量处,最后返回给客户端一个成功应答

如果任何副本上的记录追加操作失败,客户端会对操作进行重试。因此,同一个块的副本上的数据可能包含了不同的数据,这些数据可能包含了一份同样记录的全部或部分的重复值。GFS不保证所有副本是字节层面一致的,它仅能保证数据能被作为原子单位写入至少一次。这个特性很容易从对那些成功响应的报告中简单地观察出来:数据一定被写入到某些块所有副本的相同偏移位置处,此外,所有副本至少和记录结束的长度相同。因此,之后的任何记录都会被分配到更大的偏移位置处,或是不同的块上,即使有另一个不同的副本成为了主副本。就我们的一致性保证而言,成功执行写入数据的记录追加操作的区域是已定义的(所以是一致的),而介于其间是不一致的(所以是未定义的)。我们的应用程序可以如2.7.2节所述中来处理这种不一致的区域

3.4 快照

快照操作几乎可以在瞬间生成文件或目录树(“源文件”)的一份副本,同时会尽可能地避免中断任何正在进行的数据变更。我们的用户使用快照来迅速创建大型数据集的分支副本(通常是递归地拷贝这些副本),或是在调整实验前为当前状态建立检查点,以便可以在之后方便地进行提交或回滚

例如AFS[5],我们使用标准的 copy-on-write (写时复制)技术来实现快照。当master收到一份快照的请求时,会首先撤销那些在即将进行快照的文件块上的未完成的租约。这种行为确保了这些块上任何连续的写操作都需要与主机交互,以找到租约的持有者,这首先给了master有一个创建块副本的机会

在租约被撤销或过期之后,master会将操作以日志形式记录到磁盘中。然后,master会通过拷贝源文件或目录树的元数据来将此日志记录应用到它的内存状态中。新创建的快照文件会指向与源文件相同的块

客户端在快照生效后首次对块C进行写入时,会向master发送请求来寻找当前租约的持有者。master注意到块C的引用计数大于1,会推迟对客户端请求的响应,并选择一个新的块句柄C'。然后会让拥有块C副本的每一个chunkserver都创建一个新的块,叫做C'。通过在与原块相同的chunkserver上创建新块,我们可以确保数据在本地进行拷贝,而不是通过网络(我们磁盘的速度是100MB以太网链路的3倍左右)。从这一点来看,处理任何块的请求都没有什么不同:master在新块C'上为其副本之一授予租约,并给客户端回复,使其可以正常地对块进行写入,而不知道这个块是从已存在的块刚创建出来的


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

WEBMASTER技术手册

WEBMASTER技术手册

斯潘奥尔 / 斯潘奥尔 / 清华大学出版社 / 2004-4 / 63.0

本书的第三版升级到Apache PHP和Java Script 最新的版本上。同是它还包含了关于mod_perl更为详尽的信息以及提高Web 性能的方法。书中的内容涉及到HTML4.01、CSS、XML和XSLT、JavaScript1.5 、HTTP1.1、A pache2.0等等。一起来看看 《WEBMASTER技术手册》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器