zoukankan      html  css  js  c++  java
  • The Google File System论文拜读

    The Google File System
    Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung
    Google∗

    摘要

    我们设计并实现了谷歌文件系统,这是一个为大型分布式数据密集型的应用而设计的可伸缩的分布式文件系统。

    它能够运行在廉价的商用机器上同时又提供了容错率,并且对大量客户端服务时提供了很高的聚合性能。

    虽然GFS和之前的分布式文件系统在设计上有很多共同的目标,但是我们的设计同时也受到对我们应用负载和技术环境的观察而驱动,包括当前的和预期的,反映出和某些之前的文件系统假设不同的标志分离。

    这种驱动迫使我们重新考量文件系统设计时的传统选择并且发掘出一些从根本上不同的设计点。

    新的文件系统可以很好的满足我们的存储需求。它在Google中广泛部署用作存储平台为生成和处理的数据我们服务中使用的以及搜索和开发任务需要存储大量的数据集。目前为止最大的集群可以提供数百TB的数据分布在超过一千台机器的数千个磁盘上,并且并发地被数百个客户端所访问。

    在本文中,我们提供了文件系统接口扩展被设计用于支持分布式的应用,讨论了我们设计中的很多方面,同时也报告了小的基准测试和实际使用的中的度量结果。

    分类和学科描述

    分布式文件系统

    一般术语

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

    关键词

    容错,伸展性,数据存储,集群存储

    1. 引言

    我们设计并实现了Google File System(GFS)来Google快速增长的数据处理的需要。GFS和之前的分布式文件系统有很多共同的特点,比如性能,伸缩性,可靠性和可用性。然而,它的设计还来源于我们对应用负载和技术环境的重要的观察,包括了当前的和预估的,表现出了和一些以前的文件系统在设计假设上的标志性不同。我们重新考量在文件系统设计时的传统选择,发掘了在设计上根本的不同点。

    首先,组件的失效是常态而不是仅仅会在异常的时候发生。新的文件系统中包含了数百个乃至数千个构建在廉价商用机器上的存储部件。组件的数量和质量实际上可以保证某些机器可以在任何给定的时间失效以及某些机器可以不从当前的故障中恢复过来。我们见到过由于应用bug,操作系统bug,人为因素和磁盘,内存,连接器,网络以及电力供应导致的失效而造成问题。因此,时刻的监视,错误检测,容错和自动回复必须集成到这个新的文件系统中。

    第二,第二个毛线,回家了,有些地方不好翻译,不过大部分还是易读的,同时读了也有收获呀。

    第二,以传统的标准来看,文件都是很大的。几个GB的文件都是常见的。每个文件通常都会包含多个应用对象比如web文件。当我们经常要处理很多增长迅速的包含了数百万个对象的TB级数据时,要管理数百万个在KB级大小的文件显得很笨重,即使文件系统能够对此提供支持。因此,设计上的假设和参数,例如I/O操作和块的大小必须重新审视。

    第三,大多数的文件都是通过在尾部新增数据而改变,而不是重写已经存在的数据的。在一个文件内部进行随机读写这种情形实际上是不存在的。一旦写入数据之后,这些文件都是用来读取内容,而且经常是顺序的读取。有很多的数据都有这种特性。有些数据会形成很大的库供数据分析程序扫描。有些则会通过正在运行的应用连续生成数据流。有些则是归档的数据。有些数据则是一些由一台机器产生的中间结果,然后被另一台机器处理,可能是同步的也可能是异步的。考虑到这些对大文件的访问模式,对文件进行追加成为了性能优化和原子性保证的关键,同时在客户端缓存数据块也就变得不是那么必要了。

    第四,应用和文件系统的协同设计可以通过增加灵活性从而有利于整个系统。例如,我们放宽了GFS的一致性模型来大量简化文件系统而又不会对应用施加繁重的负担。我们同时也引入了原子性的追加操作,从而多个客户端可以并发地对同一个文件追加内容而不用在它们之间施加额外的同步机制。这一点将会在论文的后面进行更加详细的讨论。

    多个GFS集群目前已经处于不同的目的而被部署。最大的一个GFS集群有超过1000个存储节点,超过了300TB的磁盘容量,并且被数百个在单独机器上的客户端连续访问。

    2. 设计概览

    2.1 假设

    在设计一个符合我们需要的文件系统时,采用了具有挑战与机会并存的假设来指导我们。在之前有提到过一些关键性的考察,现在来更加详细的陈述一下我们的假设。

    • 这个文件系统构建在许多廉价并且会经常出现故障的商用组件上。因此它必须时刻对自身进行监视和检测,能够容错并且要从常规的组件失效中恢复过来。
    • 这个文件系统存储数量适中的大文件。我们预计有数百万的文件,每个文件的大小一般为100MB或者更大。几个GB大小的文件都已经非常常见了,要有效的管理起来。小文件也要被支持,但是并不需要对它们进行专门的优化。
    • 文件系统的负载主要来自于两种读:大规模的流式读取和小规模的随机读取。在大规模的流式读取中,单个的操作一般要读取数百KB的数据,更常见的是1MB或者更多。
    • 一个客户端上的连续操作经常会读取一个文件中相邻的区域。小的随机读取一般会在任意的偏移位置读取若干KB。对性能有要求的应用通常会批量读取,并且不断的重排对文件小的读取,而不是来来回回。
    • 系统的负载也包含很多大量大规模的,顺序的写入来将数据追加到文件末尾。一般写入数据的操作规模和读取时类似的。一旦写入了数据之后,文件的内容很少会进行修改。小规模的随机写入一个文件的操作也被支持但是并不需要进行优化。
    • 系统必须高效地实现为不同客户端能并发对同一个文件进内容追加的定义良好的语义。我们的文件通常被消费者——生产者队列所使用,或者是进行多路合并。数百个运行在独立机器上的生产者将会并发的对文件追加。要实现花费最少同步的原子性操作。在文件写入之后可能会被读取,或者消费者会同时地读取文件。
    • 高持续性的带宽比低延迟更加重要。我们大部分的目标应用都会花费更多开销来高速的分散处理数据,很少会对单个的读或者写有严格的响应时间需求。

    2.2 接口

    GFS虽然没有像POSIX那样实现标准的API,但也提供了相似的文件系统的接口。文件在目录中以分层次的形式组织,通过目录名来识别。我们支持常见的文件操作,比如创建,删除,打开,关闭,读和写文件。

    除此之外,GFS还有快照和记录追加操作。快照以最小的代价创建文件或者目录树的拷贝。记录追加允许多个客户端并发的对同一个文件进行追加,同时有保证了每个客户端在追加的时候保证原子性。实现多路合并和生产者——消费者队列可以让客户端不用额外的锁就可以同时对文件进行追加。我们发现这种类型的文件在构建大型的分布式用的时候作用巨大。快照和记录追加将会在3.4和3.3节分别进行进一步的讨论。

    2.3 架构

    如图1所示,一个GFS集群包括了单一的主节点和多个块服务器,集群可以被多个客户端所访问。每个部分都是运行用户级服务进行的商用Linux机器。很容易在一台机器上同时运行快服务器和客户端,只要机器的资源允许,由于运行片状应用代码造成的低可靠性也是可以接受的。

    文件被分成了固定大小的块。每个块通过块创建时主节点赋予的不可变的和全局唯一的64位块句柄进行识别。块服务器就像Linux文件那样存储存储块通过指定块句柄和字节范围来读或者写块数据。为了可靠性,每个块会复制到多个块服务器上。虽然我们默认存储3个副本,但是用户可以对文件空间的不同区域设定不同的复制级别。

    主节点会保存所有文件系统的元数据。这包括了命名空间,访问控制信息,文件到块的映射,以及当前块的存放位置。同时主节点也会控制系统范围内的活动,比如块租赁管理,孤儿块的垃圾回收,以及块服务器之间的块合并。主节点会周期性地和块服务器通过心跳信息进行交流,通过这种方式来下达指令和收集状态。

    和每个应用相连的客户端代码实现了文件系统的API并且和主机以及块服务器进行通信来读写应用自身的数据。客户端和主节点交互进行元数据的操作,但是所有承载数据的通信都是直接和块服务器交互的。我们并没有提供POSIX API,因此也不用钩入Linux vnode layer。

    不论是客户端还是块服务器都不会缓存文件数据。客户端的缓存并没有什么好处,应为大多数的应用数据流都很大,而且工作集由于太大而难以缓存。不设置缓存可以避免缓存的一致性问题从而简化了客户端和整个系统的设计。(但客户端是会缓存元数据的。)块服务器不需要缓存文件数据是因为块都是以本地文件存储所以Linux的buffer缓存早就把经常访问的数据放到内存中了。

    image

    2.4 单个主节点

    采用单个主节点可以大大简化我们的设计并且可以使主节点根据全局信息进行复杂的块放置和复制决策。但是,我们必须最小化主节点的读写来防止它成为系统的瓶颈。客户端永远不会通过主节点来读写文件数据。而是询问主节点,它应该访问哪个块节点以获取文件数据。客户端会在一段有限的时间范围内缓存元数据信息并且在接下来的操作中直接和块服务器交互。

    让我们以图1中一次简单的读取为例说明系统之间是如何交互的。首先,客户端会使用固定的块大小将应用指定的文件名和字节偏移信息转换为文件中的块索引。然后,客户端向主节点发送包含文件名和块索引的请求。主节点会返回对应的块句柄和备份的位置。客户端使用文件名和块索引作为key来缓存该信息。

    客户端然后会向其中一个备份发送请求,最可能的是离得最近的那个。发送的请求中指定了块句柄和和块中的字节范围。后续如果读取相同的块的话直到缓存的信息过期或者文件被重新打开之前都不再需要进行客户端——主节点的交互了。实时上,客户端通常都会在一个请求中包含多个块,主节点也会立即在响应请求的时候包含这些信息。这些额外的信息能够减少以后若干次客户端——主节点的交互,实际上不会有任何额外的开销。

    2.5 块大小

    块大小是一个主要的设计参数。我们选择的是64MB,比一般的文件系统中的块要大得多。每个块备份作为一个普通的Linux文件存储在块服务器中,在需要的时候可以扩展。延迟空间分配可以避免由于内部碎片导致的空间浪费,也许这是对于如此巨大的块最大的异议。

    大的块规模有几个重要的优势。第一,它可以减少客户端和主节点必要的交互次数,因为在同个块上的读和写仅仅需要向主节点发送一次初始请求来获取块的位置信息。这种交互的减少对于我们的负载意义重大,因为应用大部分都是顺序的读写大文件。即使是对于小的随机读取,客户端可以很方便的缓存多个TB级工作集的块位置信息。第二,客户端更有可能在一个给定的大的块中执行多次操作,通过在一段时间内保持对块服务器持久的TPC连接可以减少网络负载。第三,大的块可以减少在主节点上存放的元数据。这可以让我们将元数据留在内存中,反过来又会带来其他的好处,在2.6.1节中会进行讨论。

    另一方面,就算是有延迟空间分配的大的数据块也有一些缺点。小的文件包含很少的块,也许仅仅只有一个。存放这些小文件块的块服务器如果被很多个客户端所访问的话就会成为热点。实际上,热点倒不是主要的问题,因为我们的应用大部分都是顺序的读取包含多个块的大文件。

    但是,热点问题会因为GFS首次被批量——队列系统使用而变严重:一个可执行文件作为单块文件写入了GFS,然后同时在数百台机器上同时启动。存放着这个可执行文件的少数几台块服务器会因为同时处理上百个请求而出现过载。我们通过提高这些可执行文件的存储时的复制因子以及让批量队列系统错开应用的启动时间修复了这个问题。一个可能的长期解决方案是在这种情况下允许客户端从其他的客户端读取数据。

    2.6 元数据

    主节点存放着三种类型的元数据:文件和块的命名空间,文件到块的映射以及每个块备份的位置。所有的元数据放在主节点的内存中。前两种类型的元数据(命名空间和文件到块的映射)也会通过记录变动到主节点本地磁盘中存放的操作日志中而永久保存,并且还会复制到远程机器中。使用日志可以让我们很简单可靠的更新主机点的状态信息,同时避免了因为主节点的宕机导致不一致的风险。主机节点不会永久存储块的位置信息。而是在主机点启动或者一点有新的块服务器加入到集群中的时候询问每个块服务器的块信息。

    2.6.1 驻留在内存中的数据结构

    由于元数据是存放在内存里面的,使得主机节点的操作很快。进一步而言,主节点可以很容易和高效地在后台周期性得扫描整个状态。这种周期性的扫描被来实现块的垃圾回收,在块服务器失败的时候重新复制,通过块的合并来平衡整个块服务器中的负载和磁盘空间。4.3和4.4节会进一步讨论这些行为。

    对于这种仅仅访问内存的方法有一个值的思考的问题就是块的数量乃至整个系统的容量都受限于主节点内存的大小。这在实际中并不是一个严重的问题。主节点对于每个64MB大小的块需要维护的数据少于64字节。由于大多数的文件都包含很多块所以大部分的块都是满的,仅仅有一小部分可能不是满的。同样,对于每个文件的文件命名空间的数据的需求也小于64字节,因为主节点会通过前缀压缩的方式来存储文件名。

    如果需要支持更大的文件系统的话,为主节点增加内存费用对于为我们把元数据存放在内存中而提供简洁性,可靠性,高性能和灵活性是相当划得来的。

    2.6.2 块位置

    主节点并不永久记录哪个块服务器拥有指定的数据块。它只是在启动的时候简单的询问一下块服务器这些信息。主节点通过控制所有块的放置和通过心跳信息监视块服务器的状态来保证自己的信息都是最新的。

    我们在最初试图将块的位置信息永久记录在主节点中,但是我们后来又决定了如果一开始向块服务器中请求这些信息,并且在之后周期性的话那么系统设计会更为简单。这样可以避免了由于块服务器加入或者离开集群,改变文件名,失效,重启等因素导致的主节点和块服务器之间的不一致的问题。

    另一种理解这种设计决策的方法是要意识到块服务器最终决定是否将某个块放到自己的磁盘上。由于块服务器的错误会导致块自然消失(即,磁盘会变坏或者不可用)或者有人重命名了块服务器,因此在主节点上维护块位置的一致性视图是没有意义的。

    2.6.3 操作日志

    操作日志包括了重要的元数据改变的历史记录。它是GFS的核心。它不仅仅是元数据唯一的永久记录,同时也用于定义并发操作顺序的逻辑时间线。文件和块,以及它们的版本(见4.5节)都是通过在它们创建时的时间线来唯一和永久确定的。

    由于操作日志很重要,我们必须对它们进行可靠的存储,并且在元数据持久化之前这些改变对于客户端都是不可见的。要不然,即使是块本身保存了下来,我们还是会丢失整个文件系统或者最近的客户端所做的操作。因此,我们把它备份到多个远程机器上,并且仅在响应的日志记录刷会本地和远程磁盘的时候才会对一个客户端进行响应。主节点会在刷回磁盘的时候将若干日志记录放到一起,这样可以减少由于刷回和备份整个系统的吞吐量。

    主节点可以通过回放操作日志来恢复文件系统的状态。为了最小化启动时间,我们必须使得日志尽可能小。只要日志增长到超过了某个大小,主节点都会对文件系统的状态设置一个检查点,因此主节点可以通过从本地磁盘导入最新的检查点并且回放在这个检查点之后有限的日志记录来恢复。检查点的形式是一个棵压缩了的B树,可以直接映射到内存中,不需要经过额外的解析就可以用于命名空间的查找。这由进一步的加速了恢复和提高了可用性。

    因为构建一个检查点会花一点时间,主节点内部的状态的结构设计为新的检查点的创建不会延迟即将到来的变更。主节点会切换到一个新的日志文件并且用另一个线程创建一个新的检查点。新的检查点包括了切换之前的所有变更。对于有数百万文件的集群来说检查点在一分钟左右可以创建完成。检查点创建完成之后就会被写入到本地和远程的磁盘中。

    恢复的时候只需要最新的完整检查点和对应的日志文件。再以前的检查点和日志文件可以删除了,虽然我们会保留一些来避免灾难。创建检查点失败并不会影响正确性,因为恢复代码会检测并跳过不完整的检查点。

    2.7 一致性模型

    GFS有一个宽松的一致性模型来很好地支持高分布性应用,但同时又保留了实现时的相对简单和高效。我们现在来讨论GFS的保障以及它们对于应用来说意味着什么。我们同时也会重点阐述GFS是如何维持这种保障的,但是会放在本文的其他部分讨论。

    2.7.1 GFS的保障

    看了两天才看了4页。。。

    文件命名空间的变更(比如,文件创建)都是原子性的。它们都被主节点排他性地处理:命名空间的锁用来保证一致性和正确性(4.1节);主节点的操作记录为这些操作定一个了全局总的顺序(2.6.3节)。

    数据变更之后的文件区域的状态取决于变更的类型,是否变更成功了,是否存在并发变更。表1总结了上述结果。

    image

    不管客户端从哪个备份读取数据,只要它们看见的数据总是相同的,那么该文件的区域就是一致的。在文件数据变更之后如果它是一致的话,这样就定义了一个区域,并且客户端会看到这次变更往整个文件中写的数据。当一个变更不通过并发写入器的干扰而成功完成的时候,就定义了影响的区域(通过默认的一致性):所有的客户端都会看到这次变更写了什么数据。成功的并发变更会使得区域成为未定义但是是一致性:所有的客户端都会看到相同的数据,但是并不一定反映了任何一次变更写了啥。一般来说,它包括了从多个变更而来的混杂的部分。一次失败的变更将会使得区域不一致(因此也是未定义的):不同的客户端也许在不同的时候看到不同的数据。我们会在下面描述我们的应用如何区分已定义的区域和未定义的区域。这些应用并不需要进一步区分是何种类型的未定义区域。

    数据变更可能是写或者记录追加。写操作会使得数据被写入应用指定的文件偏移处。记录追加使得数据("记录")即使在并发变更的时候也会至少原子性的追加到文件中一次,但是偏移位置是在GFS选择的地方(3.3节)。(对比而言,"普通的"文件追加仅仅会在客户端认为是当前文件结束的偏移处写入数据。)偏移量会返回给客户端,并且标记为包含该记录的已定义区域的开始位置。除此之外,GFS业务会插入填充或者在它们中间重复记录。它们占据着被认为是不连续的区域,并且因为用户数据的量而dwarfed。

    在一系列连续的变更之后,变更的区域保证是已定义的并且包含了最后一次变更时写入的数据。GFS通过将一个块上的变更已相同的顺序施加到它所对应的所有复制块中,然后使用块版本号来检测是否因为块服务器宕机(4.5节)导致了复制块丢失了变更而过时。过时的复制块将永远不会有变更也不会被客户端向主节点询问该块的位置。这些过时的块将会在最早一次的垃圾收集中被回收。

    由于客户端会缓存块的位置,因此它们会在元数据信息刷新之前访问到已经过时的数据块。这个时间窗口会通过缓存项的超时或者下一次文件的打开而被限制,因为这个会清除这个文件所有的块的缓存信息。还有,由于大多数的文件都是只进行追加,一个过时的复制块通常会返回块过早的结束而不是过期的数据。当一个读请求重试并到达主节点时,它将立即获得当前块的位置。

    成功变更很久以后,组件失败当然仍会崩溃或者摧毁数据。GFS通过主节点和所有块服务器之间的普通握手来识别失效的块服务器,通过校验和(5.2节)来检测数据异常。一旦有问题出现,那么数据就会以最快的速度从有效的备份进行重新存储(4.3节)。如果在GFS反应过来之前,一个数据块的所有备份都丢失了,那么这个数据块就不可逆的丢失了,这个过程一般在数分钟以内。即使是这种情况,它会变得不可用,而不是崩溃:应用会接收到清晰的错误而不是崩溃的数据。

    2.7.2 对应用的影响

    GFS应用能够通过一些已经用于其他目的的简单技术来调整放宽的一致性模型:依赖于追加而不是重写,检查点,写时自验证以及自我识别记录。

    实际上我们所有的应用都是通过追加而不是重写来对文件进行变更。在一个典型的使用当中,一个writer会从头到尾生成一个文件。它会在写完所有的数据之后原子性的将文件重名为另一个永久的名字,或者周期性地生成检查点有多少数据已经被成功写入了。检查点也可能包括了应用级别的校验和。readers会验证并仅仅处理直到最近一个检查点之间的文件区域,这个就是之前说的处于已定义的状态。不管一致性和并发行的问题,这种方法已经对我们来说很好用了。追加的效率更高,并且对于应用失效比随机写更有弹性。检查点允许writers增量性的重启并且使readers不用处理已经成功写入的文件数据但是对于应用而言仍不完整的。

    在其他的典型应用中,很多的writers为了合并结果,或者是生产者——消费者队列模型,并发地向一个文件追加数据。记录追加的在最少追加一次的语义保存了每个writer的输出。Readers会处理偶尔出现的空隙和重复数据。每个通过writer准备的数据都包含了额外的信息,比如校验和,使得它的有效性可以被验证。一个reader可以使用校验和识别并忽略额外的空隙和记录碎片。如果reader不能容忍偶尔的重复(比如,它们会触发非幂等的操作),那么它可以通过使用唯一的识别器来过滤掉它们,这通常需要命名对应的应用实体比如web文件。这个功能对于记录I/O(除了重复记录的去除)都是通过我们的应用以库代码的形式共享,对于在Google的其他文件系统的接口也是适用的。有了这个,相同序列的记录,加上少量的重复记录,都可以通过这种记录reader解决。

    (这里读的有点混乱了。)

    3 系统交互

    我们设计的这个系统避免了在所有的操作中涉及到主节点。有了这个背景之后,我们现在来描述一下客户端,主节点和块服务器如何交互来实现数据变更,原子记录追加以及快照的。

    3.1 租赁(Leases)和变更顺序

    一次变更是指改变了块的内容或者元数据的操作,比如写或者追加操作。每次变更都会在所有的块备份上执行。我们使用租赁来维持在所有备份上的一致性的变更顺序。主节点将一个块租赁给备份中的其中一个,我们称之为primary。primary会对所有的变更挑选一个序列顺序并施加到块中。因此,被主节点选中租赁的会首先定义一个全局的变更顺序,在租赁的时候会由primary分配一个序列号。

    租赁机制被设计用来最小化主节点的管理负担。一次租赁开始有60s的超时时间。然而,只要块块开始变更了,primary可以发出请求并可以从master中无限获取扩展时间。这种扩展请求和grants会由心跳信息捎带,这些心跳信息通常在主节点和块服务器之间交换。主节点有时候也会尝试在过期之前撤回租赁(例如,当主节点想要禁用一个正在改名的文件上的所有变更时)。即使主节点和primary之间失去了通信,它可以安全地将块在旧的租赁过期时租赁给另一个备份。

    在图2中,我们描绘了通过这些标号的步骤写操作的控制流的过程。

    1. 客户端询问主节点那个块服务器拥有当前块的租赁以及其他备份的位置。如果所有的备份都没有租赁,主节点会选中一个备份并把块租赁给它。

    2. 主节点会回复primary的识别信息和其他(secondary)备份的位置。客户端为了将来的变更会缓存这些数据。只有在primary不可达的时候客户端才会继续和主节点交互或者回复说它不在持有租赁了。

    3. 客户端会把数据推送到所有的备份中。一个客户端可以以任何顺序来推送。每个块服务器会在一个内部的LRU缓冲中存放这些数据直到这些数据已经被使用或者过期了。通过数据流和控制流的解耦,我们可以通过基于网络拓扑来调度开销比较大的数据流来提高性能。3.2节会进一步讨论这个。

    4. 一旦所有的备份都确认收到了数据,那么客户端会向primary发送一个写请求。该请求会标识之前推送到所有备份上的数据。primary会对它所接受到的所有的变更都赋予一个连续的序列号,这些变更可能来自于多个客户端,这些客户端会提供必要的序列化。它们会以序列号的顺序将变更应用到本地的状态。

    5. primary会将写请求发到所有的secondary备份。每个secondary备份会以primary分配的序列号的顺序将变更应用到自己的状态。

    6. secondary会向primary进行回复表明它们已经完成了操作。

    7. primary备份会回复客户端。在任何备份上遇到的错误都会报告给client。在发生错误的时候,写操作也许已经在primary和任意secondary的子集上执行成功了。(如果它在primary上失败了,将不会被赋予序列号和往前走。)客户端的请求被认为是失败的,并且修改的区域会处于不连续的状态。我们客户端的代码会通过重试失败的变更来处理这种错误。它会在回到写操作的起始之前尝试着执行几次步骤(3)~(7)。

    image

    如果应用一次写的数据很大或者跨越了块的边界,GFS客户端会将它分解成多次写操作。它们都会按照上面描述的控制流执行,也可能会被从其他客户端而来的并发重写操作而干扰。因此,共享的文件区域可能会以包含来组不同客户端的碎片而结束,虽然所有的备份会因为单独的操作都会在所有的备份上一相同的顺序执行。这让文件区域处于2.7节所示的一致但是未定义的状态。

    3.2 数据流

    我们将数据流和控制流进行解耦以更加高效地利用网络。当控制流从客户端从流向primary然后流向所有的secondary的时候,数据被线性地沿着进行挑选的块服务器链以管道的方式被推送。我们的目标是充分利用每台机器的带宽,避免网络瓶颈和高延迟的连接,并且最小化推送所有数据产生的延迟。

    为了充分利用每个机器的带宽,数据采用沿着块服务器链线性推送的方式而不是以其他的拓扑结构(比如,树)分布式的推送。因此,每台机器都是尽可能快地以满带宽的方式传输数据而不是在多个接受者之间分割传递。

    为了尽可能避免网络瓶颈以及高延迟的连接(比如,内部交换机连接通常两者都有),每个机器都是在网络拓扑中向离自己最近的还没有接受到数据的器推送数据。假设客户端正在向块服务器S1到S4推送数据。它首先会向离自己最近的块服务器,比如说S1,推送数据。S1然后向S2到S4中离自己最近的块服务器推送数据,比如说S2。同样的,S2会向S3和S4中离自己最近的一个推送数据等。我们的网络结构足够简单以至于"距离"可以利用IP地址精确地估计。

    最后,我们通过TCP连接以管道的方式传输数据来最小化延迟。一旦某个块服务器接收到了数据,它便立刻向前传递数据。管道传输对于我们来说特别有用,因为我们可以利用全双工连接的方式使用交换网络。可以立即发送数据而不会影响到数据接收的速率。没有网络拥塞后,传出B个字节到R备份的理想时间是B/T+RL,其中T是网络的吞吐量L是在两个机器之间传输数据的延迟。我们的网络连接一般都是100Mbps(T),L则远小于1ms。因此1MB在理想状态下会在80ms之内完成分发。

    3.3 原子记录追加

    GFS提供了被称之为记录追加的原子追加操作。在传统的写操作中,客户端会指定数据从哪儿开始写的偏移量。向一个相同的区域进行并发的写操作不是序列化的:区域会以来自多个客户端的数据而结尾。在记录追加操作中,客户端只用指定要写的数据就可以了。GFS会原子性地向文件在GFS选择的偏移量的地方至少追加一次(或者说,以字节相邻的序列)并且向客户端返回偏移量。这和Unix中向一个以O_APPEND模式打开的文件中写数据是类似的,当多个写操作并发的执行时不需要提供竞态条件。

    记录追加在分布式的应用使用频率很高,不同的客户端会以并发的方式向同一个文件中追加数据。如果以传统的写方式添加数据的话,客户端需要额外的复杂的和开销很大的同步机制,比如说通过分布式的锁管理。在我们的工作负载中,这种文件经常用于多个生产者/单个消费者队列或者从不同的客户端包含合并的结果。

    记录追加是一种变更操作,遵循3.1节的控制流,它只需要在primary的一点额外的逻辑。客户端会文件最后一个块的所有备份推送数据。然后,它会向primary发送请求。primary会检查如果将记录追加到当前的块会不会超过块的最大尺寸(64MB)。如果会的话,它会先填充块到最大的字节尺寸,告诉secondary进行相同的操作,然后向客户端回复表明该操作应该向下一个块进行尝试。(记录追加被严格限制在最大的块尺寸的四分之一以一个可以接受的水平保持最坏的碎片。)如果记录在最大的尺寸中可以放下,通常是这样的,primary会将数据追加到备份,并且告诉secondary向给定的偏移量写入数据,最后想客户端报告操作成功。

    如果记录追加在任何的备份上失败了,客户端将会重试此操作。结果,相同块的备份可能会包含不同的数据,这些数据可能部分或者全部包含重复的记录。GFS并不会保证所有的备份逐个字节都是相同的。它仅仅会保证这些数据会以原子性的方式至少写入一次。这种特性来自于报告成功操作之后的观察,就是数据必须在某个块的相同的偏移处被写入。而且,所有的备份至少都是和记录的末尾一样长,因此任何未来的记录都将被分配一个更高的偏移量或者在另一个块中即使在后面另一个不同的备份变成了primary。从一致性保证的观点来看,成功进行记录追加操作的区域写入的数据通常都是已定义的(因此也是一致的),然而干扰区域是不一致的(因此也就是未定义的)。我们的应用就像2.7.2中所描述的那样会处理这种不一致的区域。

    3.4 快照

    快照操作对文件或者目录树进行备份("源")几乎是同步的,同时会最小化正在进行的变更的干扰。我们的用户可以快速地对大型数据集创建分支拷贝,或者在对在实验在后来可能被提交或者回滚的操作之前对当前的状态建立检查点。

    就像AFS,我们使用标准的写时复制技术来实现快照。当主节点接受到创建快照的请求后,首先会撤回那些那将要制作快照的文件的块的对外租赁。这可以保证任何后来对这些块的写操作都需要和主节点交互来找到租赁的持有者。使主节点有机会首先为块创建新的拷贝。

    在租赁过期或者被撤回之后,主节点会把操作记录到磁盘。然后会通过复制源文件或者目录数的元数据将日志记录应用到内存中的状态。新创建的快照文件就像源文件一样会指向相同的块。

    在进行快照操作之后如果一个客户端第一次要写入块C的时候,它需要向主节点发送请求找到当前租赁的持有者。那么主节点会注意到对块C的引用数量大于1。它会推迟响应客户端,挑选一个新的块句柄C'。然后它会询问每个有块C备份的块服务器来创建一个新的块C'。通过向原来一样在相同的块服务器上创建新的块,我们确保数据可以在本地进行拷贝,而不是通过网络(我们磁盘的速度要比100Mb以太网连接快3倍)。从这点来看,对于任何的块来说,请求的处理没有什么不同:主节点会先在备份上给出一个块C'的租赁并且响应客户端,这样就可以正常的写入块了,而不会知道这个块是从已经存在的块上创建的。

    4. 主节点操作

    主节点执行所有的命名空间的操作。除此之外,它还会管理整个系统中块的备份:它会决定块放在那儿,创建新的块然后备份,然后协调各种不同的系统范围内的活动从而保证块完全备份,对所有的块服务器进行负载均衡以及回收不再使用的存储空间。现在我们来讨论以上的每个方面。

    4.1 命名空间的管理和加锁

    许多主节点的操作会花费很长的时间:比如,快照操作必须回收快照所覆盖的所有块的块服务器的租赁。在执行这些花费时间的主节点操作的时候我们也不想对其他的操作造成延迟。因此,我们允许激活多个主节点操作并且对命名空间使用锁来确报必要的序列化。

    不像很多其他的传统文件系统,GFS并不在每个目录的数据结构中列出在其中所有的文件。也不支持相同文件或者目录的别名(用Unix的术语就是软链接或者硬链接)。GFS从逻辑上将它的命名空间表示为一个查找表,在表中将全路径名映射到元数据。有了前缀压缩后,这个表可以很高效地在内存中进行表示。在命名空间树的每个节点(一个绝对文件名或者绝对的目录名)都有一个对应的读写锁。

    每个主节点操作在执行之前都需要获得一套锁。一般来说,如果是涉及到/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和/home/user的读锁,以及/home/user/foo的写锁。因为这个操作都尝试获取/home/user相互矛盾的锁,因此这两种操作会进行合适的序列化。文件创建并不会获得父目录的写锁因为并没有目录这一说,或者类似inode,数据结构来防止被修改。名字上的读锁足够能防止父目录被删除。

    这种锁方案的精妙之处在于它运行在相同目录的并发修改。例如,多个文件的创建可以在相同的目录中并发执行:每次只要获取目录上的读锁以及文件名的写锁就行。目录名上的读锁足够防止目录被删除,重命名或者快照。而文件名的写锁会序列化试图创建两次文件名相同的文件的操作。

    由于命名空间有很多的节点,读写锁对象都是延迟分配并在它们不再使用的时候就会删除。同样,锁的获取是采用一致性的顺序来避免死锁:它们首先会在命名空间树中以级别来排序,相同的级别则采用字典序列。

    4.2 备份的放置

    GFS集群在多于一个水平上都是高度分布的。一般来说都有上百个块服务器跨越许多的机架分布。这些块服务器反过来又会被相同或者不同机架上的数百个客户端所访问。不同机架上的两个机器通信可能要跨越一个或者多个网络路由器。除此之外,一个机架中进出的宽带可能会少于同一个机架内聚合的带宽。多级别的分布给数据分布的伸缩性,可靠性和可用性带来了独特的挑战。

    块备份的放置策略有两个目的:最大化数据的可靠性和可用性,同时最大化网络带宽的利用率。如果要达到这两个目标,仅仅把备份分散到各个机器上是不够的,这样之后放置磁盘或者机器失效以及充分利用每个机器的带宽。我们必须将块备份分布到不同的机架上。这样可以确保一个块的某些备份即使在某个机架整个都被毁坏了或者离线时仍幸存并且可用(例如,由于共性资源的失效比如网络路由器或者电路原因)。同时意味着拥堵,尤其是读,应为一个块可以利用多个机架的整合带宽。另一方面,写拥堵必须通过多个机架,我们可以根据自己的意愿作一些折中。

    4.3 创建,重新复制,重新均衡

    出于三个原因需要创建块的备份:块创建,重新复制和重新均衡。

    当主节点创建了一个块后,它会选择一个位置来初始化空的备份。它会考虑几个因素。(1)我们想把新的备份放到磁盘利用率较低的块服务器上。这将会使所有的块服务器的磁盘利用率趋近于相同。(2)我们想限制每个块服务器上最近的块创建数。虽然块创建本身是廉价的,但是因为块创建大都因为有写请求,可能造成拥塞。而且在我们的一次追加多次读取的负载中,它们在实际的数据写完之后一般都是只读的。(3)正如上面所讨论的,我们想要把块分布到所有的机架上。

    一旦可用的块的数量低于用户设定的目标,主节点就会继续备份块。这样做有很多原因:一个块服务器变得不可用的时候,会报告它的备份可能已经损坏,它的其中一个磁盘可能因为错误而不可用,或者复制的目标增加了。需要重新复制的块会因为几个因素而优先级会提高。其中一个是离它复制目标有多远。例如,我们会对损了2个备份的块比损失了1个备份的块赋予更高的优先级。除此之外,我们更倾向于首先重新复制有存活文件的块而不是属于最近删除的文件的块(4.4节)。最后,为了最小化对正在运行的应用的影响,我们增强了正在阻塞客户进程的块的优先级。

    主节点会挑选优先级最高的块然后通过指示某些块服务器直接从已经存在的可用数据中进行拷贝。新的备份放置的原则和新建块的原则相同:平衡磁盘空间利用率,限制单个块服务器的活跃克隆操作,尽可能在所有的机架上分布。为了防止克隆阻塞压倒客户端的阻塞,主节点会限制集群和每个块服务器的活跃的克隆操作数。除此之外,每个块服务器都会通过限流它对源块服务器的读请求来限制它花在每个克隆操作的带宽。

    最后,主节点会周期性的重新平衡备份:它会检查当前备份的分布并且会将备份移到更合适的磁盘上并进行负载均衡。同样通过这个步骤,主节点会逐渐地填满新的块服务器而不是一直在写操作很频繁的块服务器上创建新的块。新备份的放置规则和之前讨论的类似。除此之外,主节点必须选择移走哪个已经存在的备份。一般而言,它更倾向于移走那些低于平均可用空间的块服务器上的块以平衡磁盘空间利用率。

    4.4 垃圾回收

    当文件被删除之后,GFS并不会立即回收这些可用的物理空间。它会通过在常规的对文件和块级别进行垃圾回收的时候延迟做这件事情。我们发现这种方法使得系统更加简单和更加可靠。

    4.4.1 机制

    当一个文件被应用删除了之后,主节点会像其他的操作一样马上记录这个删除操作。但是文件占用的资源空间并不会被立即回收,而是将文件重命名为一个包含删除时间戳的隐藏的名字。在主节点对文件系统命名空间进行常规的扫描时,如果扫描到了已经被删除了3天(这个间隔时间在内部是可以配置的)的文件的话它会移走这些文件。到那个时候,文件在新的特别的名字下仍然是可读的并且可以通过将它重命名到正常的名字而被找回。当隐藏的文件从命名空间被移走之后,它在内存的中元数据也会被删除。这个对于它的连接到的块也很有效。

    在对块空间进行相似的常规的扫描时,主节点会识别出孤立的块(即,那些从任意的文件都不可达的块),同时也会擦除这些块的元数据。在和主节点交换的心跳信息中,每个块服务器都会报告它所含有的块的子集,主节点会识别出所有不在主节点元数据中的块并返回给块服务器。然后块服务器就会对这些块爱删不删。

    4.4.2 讨论

    虽然分布式的垃圾收集在某个变成语言中是一个很难的问题,需要很复杂的解决方案,然而在我们的案例里面很简单。我们可以很轻易的识别所有对块的引用:它们都在主节点额外维护的文件到块的映射中。我们同样可以轻易地识别所有的块备份:它们都是在每个块服务器中指定目录的Linux文件。任何对于主节点来说未知的块都是"垃圾"。

    对于存储空间回收的垃圾回收方法相对于立即删除有以下几点好处。首先,它在组件失效十分常见的大规模分布式系统中来说更加地可靠客户简单。块创建也许会在某些块服务器上创建成功但不是全部,是的主节点并不知道某些块的存在。备份删除的消息也有可能会丢失,主节点必须记得由于失败而需要重新发送消息,不论是它自己的还是块服务器的。垃圾回收提供了统一和可靠的方式来清理任何因为未知而导致的不可用的块。第二,它会合并空间回收请求到主节点的常规后台活动中,例如命名空间的常规扫描,或者和块服务器的握手。因此,它可以批量处理并且分摊开销。还有,他只会在主节点处于相对空闲的状态才会完成。主节点可以相应客户端那些需要及时响应的请求。第三,在空间回收上的延迟可以相对于偶然,不可逆的删除提供一定的安全性。

    在我们的经验中,这种做法的主要缺点在于当存储空间比较吃紧的时候可能会阻止用户找到可用的空间。需要重复创建和删除临时文件的应用可能会不能及时的复用存储空间。如果在已经显式要求删除文件的时候我们会通过加速删除过程来解决这个问题。我们也允许用户对命名空间不同的部分采用不同的复制和回收策略。例如,用户可以指定某个目录树中所有的块不经过复制的存储,以及立即删除和不可撤销地从文件系统空间移走。

    4.5 过期备份检测

    块备份如果在块服务器宕机而失效或者错过了对某些块的变更得到时候而过时。对于每个块来说,主节点维持了块版本号分别最新的和过时的备份。

    不论主节点什么时候对块租赁,它会增加块的版本号并且通知最新的备份。主节点和这些备份都会把新的版本号记录到持久状态。这个发生在任何客户端被通知之前,同时也会在客户端发起写操作之前。如果另外一个备份当前已经不可用了,它的快版本将不会更新。当块服务器重启然后报告它的块子集和对应的版本号的时候,主节点将会检测到这个块服务器有过期的备份。如果主节点发现一个版本号要比它的记录中的大,主节点就会假设他已经失效了当租赁以及更新到最新的版本号。

    主节点会在常规的垃圾收集的时候移走过时的版本。在那之前,它会认为过期的备份根本不存在当他回复客户端对于块的请求信息时。作为另一道保障,主节点会在通知客户端的时候包含拥有租赁的块的版本号或者当它指示一个块服务器向另一个块服务器进行可用操作的时候。客户端或者块服务器会咋执行操作的时候验证版本号以保证它们总是访问最新的数据。

    5. 容错和诊断

    在这几这个系统时遇到的最大的挑战是如何解决经常发生的组件失效。组件的质量和数量使得这些问题比出现异常更加正常:我们不能完全信任这些机器,也不能完全相信这些磁盘。组件失效会造成系统不可用,甚至是数据崩溃。现在我们来讨论一下我们是如何解决这些问题的以及我们往系统中构建了那些工具来诊断那些不可避免的问题。

    5.1 高可用

    在GFS集群的数百台服务器中,有些可能在任何给定的时间势必变得不可用。我们采用两种非常简单但是很有效的方法来保持整个系统是高可用的:快速恢复以及复制。

    5.1.1 快速恢复

    主节点和块服务器都被设计为可以恢复它们的状态并且不管它们是怎么退出的都可以在数秒内启动。实际上我们并不区分正常退出或者非正常退出;服务器通常用杀死进程的方法退出就行了。客户端和其他的服务器会在outstanding的请求超时之后经历一次小的hiccup,重新连接到重启的服务器,然后重试。6.2.2节报告了观察的启动次数。

    5.1.2 块复制

    正如之前所讨论的那样,每个块都会复制到不同机架的多台主机上。用户可以为命名空间中不同的部分指定不同的复制级别。默认的级别是3。为了是每个块都被充分的复制,主节点会在检测到块服务器离线或者通过校验和检测到备份的数据崩溃时候克隆已经存在的备份(5.2节中提到)。虽然复制对我们来说已经很好了,但是我们还是开发其他形式的跨服务器冗余比如为了日渐增长的只读存储需求的奇偶或者擦除代码。我们觉得在我们松耦合的系统中来实现这种更为复杂的冗余方案会具有挑战性但是会更加便于管理,因为我们拥堵主要在于追加和读而不是小规模的随机写。

    5.1.3 主节点复制

    为了可靠性主节点的状态也做了复制。它的操作日志和检查点会在多个机器上进行复制。对状态进行变更的操作只有在它的日志记录刷回到了本地磁盘以及所有的主节点备份上时才被认为是执行成功了。为了系统设计的简洁性,一个主节点进程会管理所有的变更以及诸如会内在地改变系统的垃圾回收等后台活动。如果它失败了,几乎可以一直重试。如果它所在的机器或者磁盘失效了,GFS外部的监视框架会在别的地方用复制的操作记录来启动一个新的主节点进程。客户端仅仅使用主节点的canonical名字(比如gfs-test),这是一个DNS的别名,这个别名在主节点被分配到另一台机器上的时候也会跟着改变。

    还有,"影子"主节点可以提供对文件系统的只读访问,即使在primary master宕机的时候。它们都是影子,而不是镜像,这种方式可能使得它们要比primary稍微之后一些。它们可以为那些并不是变更很活跃的或者应用并不介意稍微过时结果的文件增强读的访问性。事实上,由于文件内容是从块服务器那儿读过来的,应用并不会看到过时的文件内容。在短时间窗口内可能过时的是文件元数据,比如目录内容或者访问控制的信息。

    为了保持自己知悉变更,影子主节点会读取不断增多的操作日志的备份,然后就像主节点那样将相同的序列应用到它的数据结构的改变。

    就像primary主机一样,它会在启动的时候询问块服务器(在这之后则是偶尔)来定位块的备份以及和块服务器交换握手信息来监控它们的状态。它只会依赖主服务器请求那些由于主节点对于创建和删除备份的决策来请求备份位置更新的结果。

    5.2 数据完整性

    每个块服务器都会使用校验和来检测存储的是不是已经崩溃了。假设一个GFS集群在数百台服务器上有数千个磁盘,通常都会经历那些在读或者写的时候使得数据崩溃或者丢失的情况。(第7部分有一个例子。)我们可以通过使用其他的块备份来进行恢复,但是通过和所有的块服务器上的备份进行比价以确定数据是否崩溃是不现实的。还有,存在不同的备份也是合法的:GFS中变更的语义,尤其是在之前讨论的原子性的记录追加,并不能保证完全相同的备份。因此,每个块服务器必须通过它所持有的校验和来独自验证它存储的备份的完整性。

    一个块被分成64KB大小的块。每个都有对应的32位校验和。就像其他的元数据一样,校验和都是存放在内存中的并且持久化到中,和用户的数据是分开的。

    对于读操作而言,块服务器在返回任何数据给请求者,不论是客户端还是其他的块服务器,之前会先验证那些重叠了读区域数据块的校验和。因此,块服务器不会把已经损坏的数据传递到其他的机器上。如果一个块和记录的校验和对不上,块服务器会向请求者返回一个error,并且向主节点报告不匹配的情况。然后,数据的请求者会从其他的备份读取数据,同时主节点从其他的块服务器上克隆这个块。当有效的新备份就位之后,主节点会让报告校验和不匹配的块服务器删除那个备份。

    校验和出于一个原因会对读性能有一些影响。由于我们大多数的读操作都至少会读取多个块,我们需要读取仅仅是一小部分的数据并计算校验和用于验证。GFS客户端将会通过尝试着校验和块的边界进行对齐的读来减少这种开销。还有,在块服务器上进行校验和的查找和比较都是不用经过任何I/O的,而且校验和的计算通常都会和I/O重叠。

    对于像块的末尾进行追加的写操作(相对于复写之前已经存在的数据)时的校验和的计算已经是经过了优化的,因为它是我们工作负载中的主要部分。我们只是会根据对上一次的校验和来增量更新校验和,为填充的新的校验和的块计算新的校验和。及时上一次的部分校验和已经崩溃了并且已经检测不到它了,新的校验和的值不会匹配已经存储的数据,并且数据崩溃会向平常一样在下一次读取块的时候被检测到。

    相反,如果一次写操作覆写了某个块中已经存在的范围,我们必须读取并验证被覆写的范围的第一个块和最后一个块,然后来执行写的过程,最后会计算并记录新的校验和。如果我们在部分的覆写它们之前不去校验第一个和最后一个块,新的校验和也许会隐藏存在于没有覆写区域中的崩溃的数据。

    在空闲的时候,块服务器会浏览并校验不活跃的块中的内容。这允许我们检测块中很少会被读到的已崩溃的数据。一旦检测到了崩溃的数据,主节点会创建一个新的完好的备份然后删除掉已经崩溃的数据。这可以防止不活跃但是已经崩溃的块备份愚弄主节点,使其认为它已经有了这个块的足够多的备份了。

    5.3 诊断工具

    大量详细的诊断日志在问题隔离,调试以及性能分析上上有不可估量的作用,同时又只付出很少的开销。没有了日志的话,很难理解机器之间瞬态的,不可重复的交互。GFS服务器为了记录很多有意义的事件而生成诊断工具(比如块服务器的上线和下线)以及所有的RPC请求和回复。这些诊断日志可以随便删除而不会影响系统的正确性。然而,只要空间允许,我们都会保存这些日志。

    RPC日志包括了每次的请求和响应,除了文件数据的读和写。通过将请求和响应匹配以及对比不同机器上的RPC记录,我们可重构整个交互的历史来诊断问题。日志同样可以用来追踪负载测试以及性能分析。

    打日志的影响是很小的(好处远远大于开销)因为日志都是顺序和异步的写入的。最近的时间也会停留在内存中,用于连续的在线监控。

    6. 测试

    在这部分中我们会提供一些小的基准测试来描述GFS架构和实现的内在瓶颈,以及一些在Google中实际集群中的一些数据。

    6.1 小的基准测试

    我们在一个包含了一个master,2个master replicas,16个块服务器以及16个客户端的GFS集群上进行性能测试。注意到这种配置只是为了测试方便。典型的集群会有上百个块服务器以及上百个客户端。

    所有机器的配置都是双核1.4GHz的PIII处理器,2GB的内存,2个80GB 5400 rpm的硬盘,以及一个100Mbps的全双工连接到HP 2524交换机的以太网。所有的19GFS 服务器机器都连接到一个交换机上,而所有的16个客户端都连到另一个交换机上。这两个交换机都连接到1Gbps的连接上。

    6.1.1 读操作

    N个客户端同时对文件系统发起读操作。每个客户端会从320GB的文件集中随机读取4MB的区域。通过重复256次这种操作让每个客户端最终从文件系统中读取了1GB的数据。块服务器一共只有32GB的内存,所以我们希望在Linux buffer缓存中最多有10%的热点。我们的结果应该接近于冷缓存的结果。

    图3(a)描绘了对于N个客户端进行合并读的结果以及它的理论极限。极限的峰值在125MB/s的合并速度,此时在两个交换机中的1Gbps的连接已经饱和了,或者每个客户端是12.5MB/s,即已经是100Mbps网络接口的饱和了。观察到的读速率是10MB/s,或者每个客户端极限的80%,当只有一个客户端在读的时候。合并读的速度可以达到94MB/s,大概是125MB/s连接极限的75%,对于16个客户端,即每个客户端是6MB/s。效率从80%降低到了75%,是应为当读的数目增加了,因此多个读对象同时从相同的块服务器读取的概率变大了。

    image

    就剩下了这个测试没有翻译了。。。。。。

    6.1.2 写操作

    6.1.3 记录追加

    6.2 真实环境中的集群

    6.2.1 存储

    6.2.2 元数据

    6.2.3 读和写的速率

    6.2.4 主节点的负载

    6.2.5 恢复时间

    6.3 工作负载故障

    6.3.1 方法学和注意事项

    6.3.2 块服务器的工作负载

    6.3.3 追加和写对比

    6.3.4 主节点的负载

    7. 经验

    在构建和部署GFS的过程中,我们遇到了很多的问题,有些事操作上的,有些则是技术上的。

    在最开始的构想是将GFS作为我们生产系统的后端文件系统。到了后来GFS升级为研究和开发任务了。开始时对权限和配额的支持比较少,但是基本包括了上述功能。虽然生产系统很规范,控制得很好,都是用户有时候不是这样的。需要有更多设置来让用户之间不相互干扰。

    我们最大的问题是和硬盘以及Linux相关的。虽然我们很多的磁盘都声称支持一些列的IDE协议版本的Linux驱动,但是实际上只是对近来的一些有可靠的响应。由于协议版本都是类似的,这些驱动大部分都是工作的,但是偶尔会出现不匹配从而造成驱动和内核关于驱动的状态发生矛盾。这些发生在内核中的问题悄悄地使得数据崩溃。这种问题也激发我们使用校验和来检测数据是否崩溃,同时我们也会修改内核来处理这种协议不匹配的问题。

    早些时候由于fsync()的开销,使得我们在Linux 2.2版本的内核有些问题。它的开销是和文件的大小成线性关系的而不是修改部分的大小。这对于我们大型的操作日志来说是个问题,尤其是在我们实现检查点的时候。我们花了点时间通过同步写的方式解决了这个问题,并最终移植到了Linux 2.4中。

    另一个Linux的问题是在地址空间的任何线程在与磁盘进行页交换(读锁)或者在mmap()调用(写锁)修改了地址空间的时候都必须斥候reader-writer锁。我们在系统负载比较小的时候发现了瞬时的超时,当达到资源瓶颈或者零星的硬件故障的时候会变得严重。最终,我们发现当磁盘线程调入之前映射数据的时候这种单个锁会阻塞主网络线程将新数据映射到内存中。由于我们主要受限于网络接口而不是内存复制的带宽,因此我们使用了一点额外复制代价将mmap调用换成了pread。

    虽然偶尔会出现问题,但是Linux代码的可用性还是帮助了我们,并且又一次发现并理解了系统的特性。到合适的时候,我们将会改良内核在开源社区分享这些改变。

    8. 相关工作

    像其他的大型分布式文件系统例如AFS一样,GFS提供了与命名空间无关的位置,这使得数据可以为了负载均衡或者容错而透明地移动。不像AFS,GFS以更类似xFS和Swift的方式将文件分散到所有的服务器上,这样可以实现和并性能以及增加容错能力。

    由于磁盘都是相对便宜,而且备份起来也比更加复杂的RAID方法更加简单,GFS目前仅仅使用复制来实现冗余因此会比xFS和Swift消耗更多的原始存储。

    相比于AFS,xFS以及Frangipani和Intermezzo这些文件系统,GFS并不会在文件系统接口下提供任何的缓存。我们的目标负载在单个应用运行的时候并不会复用,因为它们要么是通过大的数据集进行流式访问,要么是在其中进行随机的seek并且每次只读取数据中的很少一部分。

    有些分布式的文件系统,例如Frangipani,xFS,Minnesota'GFS和GPFS移走中心化的服务器并且依赖于分布式的算法来保证一致性并且进行管理。我们倾向于使用中心化的方案以简化系统的设计,增加它的可靠性以及获得更大的灵活性。特别是中心化的主节点可以更简单的实现复杂的块放置以及复制策略,因为主节点已经掌握了最多的相关信息并且控制它如何变化。我们通过保持主节点的状态小并且复制到其他的机器中来解决容错问题。可伸缩性和高可用性(read)目前是通过我们的影子主节点机制来实现的。更新到主节点的状态会通过追加到写之前的日志进行持久化。因此我们可以向Harp中采用primary-copy的方案来提供比我们现有方案更强的一致性保证。

    我们从对大规模的客户端使用合并性能的角度解决了类似于Lustre中的问题。然而,我们通过关注与我们的应用的需求而不是构建一个POSIX兼容的文件系统极大地简化了问题。除此之外,GFS假设使用了很多不可靠的组件,因此容错成为了我们设计的一个中心问题。

    GFS大体上接近于NASD的架构。但NASD的架构是基于附着于网络的磁盘的,GFS则是使用廉价的商用机作为块服务器,就像NASD原型中的那样。和NASD工作方式不同的是,我们的块服务器使用懒分配的固定大小的块而不是使用变长的对象。除此之外,GFS实现了生产环境所需的诸如重新负载,复制以及恢复的功能。

    不像Minnesota GFS和NASD,我们并不寻求存储设备的替换模型。我们着力于解决使用已有的廉价商用机解决每天对于数据处理的需要。

    使用原子性的记录追加实现的生产者和消费者队列模型解决了在River中类似的分布式队列的问题。但是River使用的分布在所有机器上的基于内存的以及仔细地数据控制流的队列模型,而GFS使用可以由多个生产者并发追加的持久化文件。River模型支持m-to-n分布式队列,但是缺少持久存储会遇到容错问题,而GFS仅仅支持高效的m-to-1队列。多个消费者可以读取同一个文件,但是它们必须进行协调并对负载进行划分。

    9. 结论

    Google文件系统解决了用廉价的商用机来解决对于大规模数据处理负载所需的质量问题。由于有很多的设计决策都是针对于我们特定的设定,很多可能只适用于相似数量级和对开销敏感的数据处理任务。

    我们从当前和未来的应用的工作负载以及技术环境的角度重新考量了传统的文件系统设计的假设。我们观察使得我们在设计理念上有了根本的不同。我们认为组件失效是常态而不是异常情况,优化了那些通常是追加写(也许是并发的)以及读(通常是顺序的)的读的大文件,并且扩展和放宽了标准文件系统的接口来改善整个系统。

    我们的系统通过时时刻刻的监控,临界数据的备份和快速以及自动的恢复来提供容错。块复制允许我们容忍块的失败。这种失效频率激发了一种新的在线修复机制,可以常规以及透明地修复故障并且尽可能地不长丢失的备份。出自之外,我们使用校验和来检验磁盘或者IDE子系统级的数据是否崩溃,这对于给定的磁盘已经是十分常见的操作了。

    我们的设计对很多执行各种任务的并发读和写提供了很高的并发吞吐量。我们通过分析通过主节点完成的文件系统控制以及块服务器和客户端直接的数据传输而实现的数据传输来实现了上述效果。主节点通过大的块尺寸以及块租赁来最小化一般的操作,这样把数据变更的权限委托给了primary备份。这使得简单的中心化的主节点不至于成为整个系统的瓶颈。我们认为在我们网络栈上的改善可以提高单独的客户端在写吞吐量的限制。

    GFS成功的解决了我们的存储需求并且在Goole中作为存储平台进行研究和开发还有生产数据处理而广泛使用。这是一个使用我们能够继续创新并解决整个web规模问题的一个很重要的工具。

    鸣谢

    我们想要感谢一下人对本系统或者本文所做的贡献。Brain Bershad(我们的领导)和匿名的审稿人给我们的有价值的评论和意见。Anurag Acharya,Jeff Dean和David des-Jardins在最初的设计时有贡献。Fay Chang完成了所有块服务器上备份的比较。Guy Edjlali在存储配额方面作了贡献。Markus Gutschke工作于框架测试以及安全增强方面。David Karmer工作于性能增强。Fay Chang, Urs Hoelzle,Max Ibel, Sharon Perl,Rob Pike和Debby Wallach对本文早期的草稿进行评注。很多我们谷歌的同事勇敢地相信他们的数据可以可靠地放在新的文件系统上并给了我们很多有用的建议。Yoshka帮助进行了早期的测试。

    参考文献

    [1] Thomas Anderson, Michael Dahlin, Jeanna Neefe,
    David Patterson, Drew Roselli, and Randolph Wang.
    Serverless networkfile systems. In Proceedings of the
    15th ACM Symposium on Operating System
    Principles, pages 109–126, Copper Mountain Resort,
    Colorado, December 1995.
    [2] Remzi H. Arpaci-Dusseau, Eric Anderson, Noah
    Treuhaft, David E. Culler, Joseph M. Hellerstein,
    David Patterson, and Kathy Yelick. Cluster I/O with
    River: Making the fast case common. In Proceedings
    of the Sixth Workshop on Input/Output in Parallel
    and Distributed Systems (IOPADS ’99), pages 10–22,
    Atlanta, Georgia, May 1999.
    [3] Luis-Felipe Cabrera and Darrell D. E. Long. Swift:
    Using distributed diskstriping to provide high I/O
    data rates. Computer Systems, 4(4):405–436, 1991.
    [4] Garth A. Gibson, David F. Nagle, Khalil Amiri, Jeff
    Butler, Fay W. Chang, Howard Gobioff, Charles
    Hardin, ErikRiedel, David Rochberg, and Jim
    Zelenka. A cost-effective, high-bandwidth storage
    architecture. In Proceedings of the 8th Architectural
    Support for Programming Languages and Operating
    Systems, pages 92–103, San Jose, California, October
    1998.
    [5] John Howard, Michael Kazar, Sherri Menees, David
    Nichols, Mahadev Satyanarayanan, Robert
    Sidebotham, and Michael West. Scale and
    performance in a distributed file system. ACM
    Transactions on Computer Systems, 6(1):51–81,
    February 1988.
    [6] InterMezzo. http://www.inter-mezzo.org, 2003.
    [7] Barbara Liskov, Sanjay Ghemawat, Robert Gruber,
    Paul Johnson, Liuba Shrira, and Michael Williams.
    Replication in the Harp file system. In 13th
    Symposium on Operating System Principles, pages
    226–238, Pacific Grove, CA, October 1991.
    [8] Lustre. http://www.lustreorg, 2003.
    [9] David A. Patterson, Garth A. Gibson, and Randy H.
    Katz. A case for redundant arrays of inexpensive disks
    (RAID). In Proceedings of the 1988 ACM SIGMOD
    International Conference on Management of Data,
    pages 109–116, Chicago, Illinois, September 1988.
    [10] FrankSchmuckand Roger Haskin. GPFS: A
    shared-diskfile system for large computing clusters. In
    Proceedings of the First USENIX Conference on File
    and Storage Technologies, pages 231–244, Monterey,
    California, January 2002.
    [11] Steven R. Soltis, Thomas M. Ruwart, and Matthew T.
    O’Keefe. The Gobal File System. In Proceedings of the
    Fifth NASA Goddard Space Flight Center Conference
    on Mass Storage Systems and Technologies, College
    Park, Maryland, September 1996.
    [12] Chandramohan A. Thekkath, Timothy Mann, and
    Edward K. Lee. Frangipani: A scalable distributed file
    system. In Proceedings of the 16th ACM Symposium
    on Operating System Principles, pages 224–237,
    Saint-Malo, France, October 1997.

    有时间读一下这些参考文献也是好的呀,之前看操作系统,专门有一大块来讲文件系统。说实话做这些系统设计不知道要比写那些垃圾的业务代码高到哪里去了?

    目测也不能这样讲,再小的功能页需要设计,还是在于人,平常愿不愿意多思考,多总结。

    系统设计的能力也是很重要的呀。

    =========== 不在科研(职业)第一线,论文(操作)时时放心间

  • 相关阅读:
    「SAM」你的名字
    「疫期颓废」2
    「疫期颓废」1
    代码覆盖率简单介绍
    解决git报ssh variant 'simple' does not support setting port
    接口自动化基本流程和测试思路
    wait和sleep的区别
    vm垃圾回收算法的简单理解
    TCP-三次握手和四次挥手简单理解
    浏览器输入一个url 中间经历的过程
  • 原文地址:https://www.cnblogs.com/tuhooo/p/7755685.html
Copyright © 2011-2022 走看看