zoukankan      html  css  js  c++  java
  • Google论文之二Google文件系统(GFS)翻译学习

    摘要

    我们设计并实现了Google文件系统,它是一个可扩展的分布式分局系统,用于大型分布式数据密集型应用。它运行在便宜的普通硬件上,提供了容错处理;并为大量的客户端提供了高性能。

    在与之前的分布式文件系统提供相同功能的同时,我们的设计是由对我们应用的工作和技术环境的观察驱动的,无论当前还是未来,都与之前的文件系统有明显的区别。这使我们重新测试了传统的文件系统,并探索出完全不同的设计。

    这个文件系统完全符合我们的存储需求。作为一个存储平台在Google中得到大范围部署,用于存储服务产生和处理的数据,如搜索和需要大数据集的研发工作。目前为止最大的集群拥有几百TB的数据,存储在超过一千台机器的数千个磁盘上,能同时被几百个客户端访问。

    在这篇论文中,我们介绍了用于支持分布式应用的文件系统接口的扩展设计,讨论了设计的许多方面,以及报告了来自两个微基准测试和实际应用中的测试结果。

    一、    介绍

    我们设计和实现Google文件系统是为了满足快速发展的数据处理需要。GFS与之前的分布式文件系统有很多相同之处,如性能、扩展性、可靠性和可用性。然而,它的设计是由对我们应用负载和技术环节的探索所驱动的,无论是现在还是未来,都与之前的文件系统设计上有明显的区别。我们重新验证了传统的文件系统,并探索出完全不同的设计

    首先,组件出错是一个普通情况,而非异常事件。文件系统是由数百甚至数千台便宜、普通的存储机器组成的,能够同时被相当数量的客户端访问。组件的质量和数量基本可以让我们确定,一些机器在任何时候都不能工作,一些机器无法从目前的错误状态恢复。我们已经看到,造成错误的原因有:应用的bug,操作系统的bug,人为错误,磁盘、内存、连接和网络的错误,以及电源供给等。因此,实时监控、错误检测、容错和自动恢复是这个系统不可或缺的一部分。

    其次,以传统的标准衡量来看,文件都是巨大的,几个GB的文件很常见。每个文件通常包含很多的应用对象,如网页文档。当我们定期的处理快速增长的由十亿级个对象组成的数TB数据集时,它不能管理数十亿个几KB大小的文件,尽管文件系统支持。因此,设计的假设和参数不得不重新考虑,如I/O操作和块大小。

    第三,大多数文件通过追加数据而不是覆盖文件来进行操作。文件的随机写在事实上并不存在。一旦写入,文件将只能读,并且经常只是序列读。各种数据共享这些属性,一些组成了数据仓库,用于数据分析程序浏览;一些是正在运行的程序连续产生的数据流;一些是档案资料;一些是存储在一台机器上并在另一台机器上处理的中间数据,无论是同时还是以后某个时间进行。对于这种海量文件的处理方式,在客户端缓存数据块已经没有意义,而追加成为了性能优化和原子性保证的焦点。

    第四,协同设计的应用和文件系统的API通过提高系统的灵活性来优化整个系统。例如,我们放松了GFS的一致性模型的要求,对应用不引入繁重的负担,以此大大简化了文件系统。我们还采用了原子性的追加操作,以此使多个用户可以同时对一个文件进行追加操作,而不用在他们之间进行额外的同步操作。这些将在后面的章节中更详细的介绍。

    多个GFS集群以不同的目的进行部署。最大的一个集群包含了超过1000个存储节点,300TB以上的磁盘存储,能让数百个不同机器上的客户端进行连续的频繁访问。

    二、    设计概要

    2.1 设想

    在根据我们的需求设计文件系统过程中,我们也会依照一些挑战和机会并存的假设进行设计。我们顺便提到一些之前的观察结果,并在细节上展示我们的一些假设。

    • 系统是由许多廉价的、普通的、易出错的组件组成。它必须有不间断的自我监控,探测,容错,以及组件错误状态的快速恢复。
    • 系统存储了适量的大文件。我们期望有几百万的文件,每个文件通常是100MB或者更大。GB级别的文件是很常见的,并且能够有效的进行管理。小文件必须被支持,但是我们无需对它们进行优化。
    • 工作负载主要由两种读操作组成:大规模的流读取和小规模的随机读取。在大规模的流读取中,单个的操作通常会读几百KB的数据,更常见的是1MB或者更多的数据。来自同一个客户端的连续操作通常读取一个文件的一个连续范围。小规模的随机读取通常在任意的位置读取几KB的数据。追求性能的应用通过批处理和排序小规模的读操作,用以顺序的读取文件,而不是在读取过程中前后移动。
    • 工作负载同样还有很多大量的、序列写操作,它们将数据追加到文件末尾。一般操作的大小与相应的读操作相近。一旦写入,文件将很少再次改变。在文件任意位置进行的小规模的写操作虽然是支持的,但效率很低。
    • 系统必须是高效的,这里的高效是指有很多客户端能够同时对一个文件进行数据追加。我们的文件通常用于生产者-消费者队列或者多路合并。运行在不同机器上的数百个生产者,将并发的对一个文件进行数据追加,使用最小同步开销的原子化操作是很有必要的。这个文件可能会在以后被读取,或者正在同时被一个消费者读取。
    • 持续的高带宽比低时延更重要。大多数目标应用更看重大量的、高效的处理数据,而很少有应用对单个的读或写操作有严格的响应时间要求。

    2.2 接口

    GFS提供了一个常见的文件系统接口,尽管它没有实现类似POSIX的标准API。文件被分级存放在目录中,并由路径名进行标识。我们支持常见的操作,如create,delete,open,close,read,以及write文件。

    此外,GFS还有快照(snapshot)和记录追加(record append)操作。快照(Snapshot)低开销的创建了一个文件或目录树的拷贝。记录追加(record append)允许多个客户端同时向一个文件追加数据,并保证每个单独的客户端追加操作的原子性。可以用于实现多路结果合并和生产者-消费者队列,它们使很多客户端在不加锁的情况下能够同时进行追加操作。我们发现这些类型的文件对建立大型分布式应用是很有意义的。快照和记录追加操作将分别在3.4和3.3节中进行的深入讨论。

    2.3 架构

    一个GFS集群由一个Master和多个块服务器(chunkservers)组成,可以被多个客户端访问,如图1。其中的每个节点都是运行在一个普通linux服务器上的用户级服务进程。只要机器资源允许,以及能够接受运行其它应用造成的可靠性降低,就可以在同一个机器上运行一个块服务器和一个客户端。

     

    图1:GFS架构

    文件被分为固定大小的块。每个块由一个不变的、全局唯一的64bit块句柄(chunk handle)标识,它是由主节点在创建块时分配的。块服务器以linux文件的形式存储,并且对由一个块句柄和字节范围指定的块数据进行读或写操作。为了提高可靠性,每个块都会在多个块服务器上进行复制。默认情况下,我们存储三个副本。用户可以对不同的文件命名空间设置不同的复制级别。

    Master含有整个文件系统的元数据(metadata),包括了命名空间,访问控制信息,文件到块的映射,以及块的当前位置。它也控制了一些系统层的行为,如块的租约管理,孤儿块的垃圾回收,以及块服务器间的块迁移。Master周期性的与每个块服务器进行通信,通过心跳信息发送指令并收集块服务器状态。

    GFS客户端代码被嵌入到每个应用中,实现了文件系统的API,代表客户端进行读或写数据,与主节点和块服务器进行通信。客户端与主节点只进行元数据的交互操作,而所有数据相关的通信都直接与块服务器进行。我们不提供POSIX API,因此不需要对Linux vnode层使用钩子。

    无论是客户端还是块服务器都不缓存文件数据。客户端缓存几乎带来不了任何好处,因为大多数应用以流的形式读入海量文件,或者工作集过大而不能被缓存。没有缓存机制可以简化客户端和整个系统之间的一致性问题(然而,客户端缓存元数据)。块服务器不需要缓存文件数据是因为块以文件的形式存储,Linux的缓存机制已经将经常被访问的数据加载到内存中了。

    2.4 单一主节点

    拥有单一主节点能够极大的简化我们的设计,并且能使主节点根据整体的信息精确定位块(chunk)的位置以及进行复制决策。然而,我们必须最小化对主节点的读写操作,以此保证它不会成为系统的瓶颈。客户端不会通过主节点进行读写数据,相反,客户端询问主节点哪些块服务器需要进行联系,将这个数据缓存一段有限的时间,并直接与块服务器进行后面的操作交互。

    让我们解释一下图1中的一个简单的读操作的交互。首先,使用固定的块大小,客户端将文件名和应用指定的字节偏移转换为文件块的索引。然后,它向主节点发送一个包含文件名和块索引的请求,主节点回复相应的块句柄和副本的位置。客户端使用文件名和块索引作为key缓存这条信息。

    然后,客户端向其中的一个副本发送一个请求,大多数时会选择最近的那个。请求指定了块句柄和那个块的一个字节范围。在对相同块的后续读操作中,客户端无需额外的客户端-主节点交互,直到缓存信息过期或文件被重新打开。事实上,客户端通常在一个请求中询问多个块,主节点的回复也会包含紧跟在请求块后面的块的信息。在现实中,获取这些额外的信息在没有付出更多代价的情况下,可以减少一些将来的客户端-主节点的交互。

    2.5 块大小

    块大小是关键的设计参数之一,我们选择了64MB,它比一般的文件系统的块大小大很多。每个块副本在块服务器上存储为一个普通的linux文件,并且只有在需要时才会扩大。惰性空间分配防止内存碎片造成空间的浪费,这可能是选择这么大的块大小的最具争议的一点。

    采用大的块大小有几个重要的优点。第一,他能减少客户端与主节点的交互,因为对相同块的读和写操作只需要一次初始的请求,用于获取块的位置信息。对于我们的工作负载来说,这种减少更加明显,因为应用的大多数读和写文件操作都是顺序进行的。即使是小规模的随机读操作,客户端也能足够的缓存一个几TB工作集的所有位置信息。第二,由于大的块大小,客户端能在一个块上进行更多的操作,这样可以通过与块服务器在一段时间内保持一个TCP长连接来减少网络开销。第三,它减少了存储在主节点上的元数据,这样可以让我们将元数据存储在内存中,所带来的好处将在2.6.1中进行讨论。

    另一方面,即使使用了惰性空间分配机制,采用大的块大小也有它自己的缺点。小文件包含较少的块,甚至只包含一个块。当有大量的客户端同时访问相同的文件时,存储这个块的块服务器将成为热点。在实际中,热点不是一个主要问题,因为我们的应用大多会顺序读取大量的包含多个块的文件。

    然而,当GFS第一次用于一个批处理序列系统时,热点问题还是发生了:一个可执行文件以单个块的形式写入GFS,然后在数百台机器上同时启动。少量的存储这个文件的块服务器会由于数百台机器的同时访问而过载。我们通过提高可执行文件的复制因数和批队列处理系统应用交错启动的方法解决了这个问题。一个可能的长期解决方案是在这种情况下让客户端能够从其它的客户端读取数据。

    2.6 元数据

    主节点存储了三种主要类型的元数据:文件和块的命名空间,文件到块的映射,以及每个块副本的位置。所有的元数据都保留在主节点的内存中。前两个类型(命名空间和文件到块的映射)通过将操作记录存储在本地磁盘上的日志文件中得以永久保存,并在远程的机器上进行日志备份。使用日志使我们能够简单可靠的更新主节点状态,并且不用担心由于主节点崩溃而造成的不一致性。主节点不会永久的保存块位置信息,相反,主节点会在启动时,以及有新的块服务器加入集群时,询问每个块服务器的块信息。

    2.6.1 内存中的数据结果

    由于元数据存放在内存中,所以主节点的操作非常快。此外,它也使主节点能够周期性的在后台简单有效的浏览整个系统的状态。这个周期性的浏览操作用于实现块的垃圾回收,块服务器出错后的重复制,以及均衡负载和磁盘空间使用的块迁移。4.3和4.4节会深入的讨论这些行为。

    对于这种内存存储的方法有一个潜在的问题,块的数量和将来整个系统的容量受到主节点的内存大小限制。在实际中,这不是一个严重的问题,主节点为每个64MB大小的块保留不到64字节的元数据。大多数块都是满的,因为大多数文件都包含了多个块,只有最后一个块才可能被部分使用。相似的,每个文件命名空间数据通常也不到64字节,因为它使用前缀压缩来简洁的存储文件名。

    即使是要支持更大的文件系统,为主节点增加额外的内存的花费,比起将元数据存放在内存中所带来的简单性、可靠性、有效性和扩展性来说,也是相当值得的。

    2.6.2 块位置

    主节点不会永久保留哪些块服务器含有一个给定的块的记录,它只是简单的在启动时从块服务器获取这些信息,并能在此后保持最新状态,因为它控制所有的块分布,以及通过心跳消息监控块服务器的状态。

    起初我们试图在主节点永久的保存块位置信息,但是我们决定在启动时向块服务器请求数据并在此后进行周期性更新的方法要简单很多。这消除了当块服务器加入或离开集群、更改名字、出错、重启等发生时,保持主节点和块服务器同步的问题。在一个包含数百个服务器的集群中,这些事件发生的很频繁。

    理解这种设计决定的另一个方法是认识到一个块服务器对自身磁盘中含有或不含有哪些块有最终的话语权。这并不是表明要试图保持主节点上对这些信息的一致性视图,因为块服务器上的错误会造成其含有的块自动的消失(如,一个磁盘坏掉或不可用)或者是一个操作会改变块服务器的名字。

    2.6.3 操作日志

    操作日志包含了关键的元数据变化的历史记录,是GFS的核心。它不仅永久的记录了元数据,还能提供确定并发操作顺序的逻辑时间线服务。文件和块,连同它们的版本,都是由它们创建的逻辑时间唯一的、永久的进行标识的。

    因为操作日志是临界资源,我们必须可靠的存储它,在元数据的变化进行持久化之前,客户端是无法看到这些操作日志的。否则,即使块本身保存下来,仍然有可能丢失整个文件系统或者客户端最近的操作。因此,我们将它复制到几个远程的机器上,并在将相应的操作刷新(flush)到本地和远程磁盘后回复客户端。主节点会在刷新之前批处理一些日志记录,因此减少刷新和系统内复制对整个系统吞吐量的影响。

    主节点通过重新执行操作日志来恢复状态。为了使启动时间尽量短,我们必须保持日志较小。当日志超过一个特定的大小时,主节点会检查它的状态,以使它能够通过载入本地磁盘的最后一个检查点,并重新执行检查点(checkpoint)后的日志记录进行恢复。检查点是一个压缩B树类似的形式存储,能够直接映射到内存中,并且在用于命名空间查询时无需额外的解析。这大大提高了恢复速度,增加了可用性。

    因为创建检查点需要一定的时间,所以主节点的内部状态会结构化为一种格式,在这种格式下,新检查点的创建不会阻塞正在进行的修改操作。主节点切换到新的日志文件,并通过另一个线程进行新检查点的创建。新检查点包括切换前所有的修改操作。对于一个有几百万文件的集群来说,创建一个新检查点大概需要1分钟。当创建完成后,它将写入本地和远程磁盘。

    恢复只需要最近完成的检查点和在此之后的日志文件。老的检查点和日志文件能够被删除,但为了应对灾难性故障,我们会保留其中的一部分。检查点的失败不会影响恢复的正确性,因为恢复代码会探测并跳过未完成的检查点。

    2.7 一致性模型

    GFS采用松弛一致性模型,能很好的支持高分布式应用,但同时保持相对简单,易于实现。我们现在讨论GFS的保障机制以及对于应用的意义。我们也着重描述了GFS如何维持这些保障机制,但将一些细节留在了其它章节。

    2.7.1 GFS保障机制

    文件命名空间修改(如,创建文件)是原子性的。它们仅由主节点进行处理:命名空间锁保障了操作的原子性和正确性;主节点操作日志定义了这些操作的全局排序。

     

    表1:变化后的文件区域状态

    一个文件区域(region)在一个数据修改后的状态依赖于该修改的类型、成功或失败、以及是否为同步的修改。表1总结了修改的结果。如果所有的客户端无论从哪些副本读取数据,得到的数据都是相同的,则这个文件区域为一致的。在一个文件数据修改后,如果它是一致的,则这个区域已定义,客户端将看到被写入的全部内容。当一个修改没有受到并发写入操作的影响而成功时,那么影响到的区域已定义(隐含一致性):所有的客户端将总会看到修改已经写入的内容。并发成功的修改使区域处于未定义,但一致的状态:所有的客户端将看到相同的数据,但是它可能不能反映出任意一个修改已写入的内容,通常,它是由一些来自多个修改操作的、混合的数据碎片组成的。一个失败的修改会造成区域的不一致性(因此也没有被定义):不同的客户端在不同的时间可能会看到不同的数据。我们将在后面描述我们的应用如何区分已定义和未定义的区域。这些应用不需要更深入的区分不同未定义区域间的区别。

    数据修改操作可能是写入或记录追加操作。一个写操作会将数据写入到应用指定的文件位置;即使有多个并发修改操作,一个记录追加操作会将数据(记录)至少一次的原子性的追加到文件,而具体的偏移量是由GFS选定的。(与其相比,一个通常的追加操作仅仅是将数据追加到客户端认为是当前文件结尾的位置。)偏移位置返回给客户端,并标明包含这个记录的已定义区间的开始位置。此外,GFS能够在文件中插入数据或复制记录,这些数据占据的文件区域被认为为非一致性的,数量通常比用户数据总量要小很多。

    在一系列成功的修改操作后,被修改的文件区域被保证为是已定义的,并且包含了最后一次修改操作写入的数据。GFS通过以下措施完成上面行为:(a)在所有副本上按相同的顺序执行一个块上的修改操作(3.1),(b)使用版本号来检测并复制过期文件(4.5),这种过期可能是由于块服务器宕机而造成了部分修改丢失引起的。过期的副本不会再涉及修改操作,主节点也不会将该副本返回给客户端。它们会尽快的进行垃圾回收操作。

    因为客户端缓存了块位置信息,它们可能会在位置信息更新前直接访问过期的副本。这个窗口是受缓存条目的超时时间和下一次文件的打开时间限制的,下一次文件打开才会对这个文件的所有块信息的缓存进行清理。此外,由于大多数文件都是只能追加的,一个过期的副本经常会返回一个过早结束的块而不是过期的数据。当一个读客户端再次与主节点查询时,它将会立即获得当前块的位置信息。

    在一个修改操作成功执行完一段时间后,组件的失效依然能损坏或删除数据。GFS通过在主节点和所有块服务器间定期的握手来标识失效的块服务器,并通过校验和探测数据损坏(5.2)。一旦问题被发现,数据将会尽快的利用有效副本进行恢复(4.3)。只有在GFS反应之前,通常在几分钟之内,一个块的所有副本都丢失,这个块才会彻底的丢失。即使在这种情况下,它会变为不可用,而不是被损坏的数据:应用会接收到明确的错误码而不是损坏的数据。

    2.7.2 应用的实现

    GFS利用一些简单的用于其它目的的技术实现了一个宽松的一致性模型:依赖追加而不是覆盖、检查点、以及写入自验证、自标识的记录。

    实际中,我们所有的应用都是通过追加操作来修改文件,而不是覆盖。在一个典型的例子中,一个写操作生成一个文件,并从头到尾的写入数据。它将写入完数据的文件原子性的重命名为一个不变的名字,或者周期性的创建检查点,记录有多少执行成功的写操作。检查点也含有应用层的校验和。读操作仅验证并处理上一个检查点之后的文件区域,这个文件区域应该是已定义的。即使一致性和并发性问题,这个方法都很好的满足了我们的需求。追加写操作比随机写操作更有效率,对应用的错误更有弹性。检查点允许写操作逐步的进行重启,并防止读操作处理已成功写入但在应用角度没有完成的文件数据。

    在另一个典型应用中,许多写操作并行的追加一个文件,如进行结果的合并或者作为一个生产者-消费者队列。记录追加方式的“至少一次追加”的特性保证了写操作的输出。读操作使用下面的方法处理偶尔的填充数据和重复数据。每个写操作写入的记录都包含了一些额外的信息,如校验和,以使这个记录能够被验证。一个读操作能够通过校验和识别并丢弃额外的填充数据和记录碎片。如果它不能处理偶而的重复数据(如,如果他们将引发非幂等的操作),它能通过记录中的唯一标识来进行过滤,这些标识通常用于命名相关的应用实体,如网页文档。这些记录I/O的函数(除了删除重复数据)都在应用的共享库中,并且适用于Google其它的文件接口实现。于是,相同序列的记录,加上偶尔出现的重复数据,总是被分发到记录的读操作上。

    三、    系统交互

    我们设计这个系统时,需要最小化所有操作与Master的交互。依照这个前提,我们描述了客户端、Master和块服务器如何进行交互,来实现数据的修改、原子记录追加,以及快照。

    3.1 租约(lease)和修改操作顺序

    一个修改操作是一个改变了内容或块的元数据的操作,如一个写操作或者一个追加操作。一个修改操作将在一个块的所有副本上进行。我们使用租约来保证一个一致性操作在副本间的顺序。Master将一个块租约授予所有副本中的一个,这个副本成为primary。Primary对一个块的所有修改操作选择一个串行顺序,所有的副本在执行修改时都按照这个顺序。因此,全局的修改操作的顺序首先由Master选择的授予租约的顺序决定,然后由租约中primary分配的序列号决定。

    租约机制的设计是为了最小化Master的管理开销,一个租约初始超时时间为60秒,然而,只要块正在被修改,primary就能请求续约,并通常会收到主节点租约延长的响应。这些续约请求和授予都包含在主节点和所有块服务器间定期交换的心跳消息中。主节点有时会在租约到期之前尝试取消它(如,当Master想要禁止对一个被重命名的文件的修改操作时)。即使Master与primary失去通信,它也能在前一个租约到期后安全的将一个新租约授予另一个副本。

     

    图2:写操作的控制流和数据流

    在图2中,我们通过跟踪一个写操作的控制流的这些步骤,描述了这个过程:

    1. 客户端询问Master哪个块服务器持有这个块的当前租约,以及这个块的其它副本位置。如果没有一个租约,则Master选择一个副本并授予一个租约(没有在图上显示)。
    2. Master回复客户端primary的标识,以及其它(secondary)副本的位置。客户端缓存这些数据用于以后的修改操作。只有当primary不可达或者接收到primary不再持有租约时才需要再一次请求主节点。
    3. 客户端将数据推送到所有的副本。一个客户端能够以任意顺序进行推送。每个块服务器将数据保存在内部的LRU缓存中,直到数据被使用或者过期被替换掉。通过对数据流和控制流的分流,我们能够通过基于网络拓扑来调度数据流,不管哪个块服务器为primary,以此提高性能。3.2节将进一步讨论。
    4. 一旦所有的副本都确认接收到了数据,客户端将向primary发送一个写请求。这个请求确定了之前的数据被推送到了所有副本。Primary为接收到的所有修改操作分配连续的序列号,这些操作可能来自多个客户端,序列号提供了严格的序列化,应用按序列号顺序执行修改操作,进而改变自己的状态。
    5. Primary将写请求发送到所有的secondary副本上。每个secondary副本按照primary分配的相同的序列号顺序执行这些修改操作。
    6. Secondary副本回复primary,表示它们已经完成了所有的操作。
    7. Primary回复客户端。任意副本上的任意错误都将报告给客户端。在一些错误情况下,写操作可能在primary和一些secondary副本上执行成功。(如果失败发生在primary,它将不会分片一个序列号,并且不会被传递。)客户端的请求被视为已经失败,这个修改的区域停留在不一致的状态上。我们的客户端代码通过重试失败的修改操作来处理这种错误。在从头开始重复执行之前,它将在3-7步骤上做几次尝试。

    如果一个应用的写操作数据很大或者跨越了一个块的边界,GFS客户端会将这个操作分割成几个写操作。这些操作都遵循上面描述的控制流,但是可能会被其它客户端上的请求打断或覆盖。因此,共享的文件区域可能最终包含不同客户端的数据碎片,尽管如此,所有的副本都是完全相同的,因为这些独立的写操作在所有副本上都按着相同的顺序成功执行。如2.7节所提到的那样,这使文件区域处于一个一致的但未定义的状态。

    3.2 数据流

    为了有效的利用网络,我们将数据流和控制流分开。控制流从客户端到primary上,再到所有的secondary上,而数据则以管道形式、线性的沿一个精心设计的块服务器链进行推送。我们的目标是最大限度的使用每台服务器的网络带宽,避免网络瓶颈和高延迟,最小化推送所有数据的时间延迟。

    为了有效利用每台机器的网络带宽,数据被线性的沿一个块服务器组成的链进行推送,而不是按着其它拓扑进行分发(如,树)。因此每台机器的出口带宽都用于以最快的速度传输数据,而不是分配给多个接收者。

    为了尽可能的避免网络瓶颈和高延迟链路,每台机器只将数据推送给网络拓扑中最近的没有接收到数据的机器。假设客户端将数据推送给块服务器S1到S4。它将数据发送给最近的块服务器,成为S1,S1将数据推送给S2到S4中最近的块服务器,成为S2,类似的,S2将数据推送到S3或S4中距离S2较近的块服务器等等。我们的网络拓扑足够简单,可以通过IP地址来精确的估计距离。

    最后,我们通过在TCP连接上使用管道传输数据来最小化时间延迟。一旦一个块服务器接收到数据,则马上开始进行数据推送。管道对我们的帮助很大,因为我们采用了全双工的交换网络。立刻推送数据不会降低数据的接收速率。在没有网络拥塞的情况下,将B个字节传输到R个副本的理想时间消耗为B/T+RL,这里T是网络的吞吐量,L是两台机器间的传输延迟。我们的网络链路通常为100Mbps(T),并且L远小于1ms,因此,在理想情况下,1MB的数据能够在80ms内分发完成。

    3.3 原子性的记录追加

    GFS提供了一种原子性的追加操作称为记录追加。在传统的写操作中,客户端指定数据写入的偏移量,对同一个文件区域的并行写操作不能进行序列化:该区域最终可能包含来自多个客户端的数据片段。然而,在一个记录追加操作中,客户端只需要指定数据,GFS将数据至少一次的原子性的追加到文件(如,一个连续的字节序列)的GFS选定的一个偏移位置,并将这个偏移位置返回给客户端。这与在没有竞争的情况下,Uinx下多个并发写操作向一个使用O_APPEND选项打开的文件中写数据相似。

    记录追加操作在我们的分布式应用中使用频繁,许多运行在不同机器上的客户端并行的向同一文件上追加数据。如果使用传统的写操作进行并发写,客户端将需要额外复杂的、昂贵的同步机制,比如,通一个分布式锁的管理器。在我们的工作中,这样的文件通常用于多个生产者/一个消费者队列,或者合并来自许多不同客户端的数据结果。

    记录追加操作是修改操作中的一种,遵循3.1中介绍的控制流程,只在primary上有一些额外的逻辑。客户端把数据推送到文件最后一个块的所有的副本上,然后将向primary发送它的请求。Primary会检查这次追加操作是否使块的大小超过了最大尺寸(64MB)。如果超过,它将把这个块填充满,通知所有的secondary副本进行相同的操作,并回复客户端表明这个操作将在下一个块上重新执行。(记录追加操作的数据大小严格控制在最大尺寸的1/4以内,以确保最坏情况下碎片的数量在一个可接受范围。)通常情况下,如果记录不超过最大尺寸,primary将数据追加到它的副本上,然后通知secondary把数据写到与primary相同的位置上,最后回复客户端操作成功。

    如果在任意一个副本上的记录追加失败,客户端将重试这个操作。因此,在同一个块的副本上可能包含不同的数据,包括同一个记录的全部或部分的重复数据。GFS不保证写入的数据在字节上完全相同,它只保证作为一个原子单元至少被写入一次。这个特性能够通过简单的观察得到:如果操作执行成功,数据肯定被写入到了某些块副本的相同位置。此外,在这之后,所有副本至少都达到了记录尾部的长度,因此,即使一个不同的副本成为了primary,以后的任何记录也都将被放置在更大的偏移位置或者是一个不同的块上。在我们的一致性保障方面,记录追加操作成功的写入数据的区域是被定义的(因此是一致的),反之,介于中间状态的区域是不一致的(因此是未定义的)。我们的应用使用在2.7.2讨论的方法处理这种不一致的区域。

    3.4 快照

    快照操作几乎瞬间的为一个文件或一个目录树(源)创建一个拷贝,并且不会对正在进行的其它操作造成任何影响。我们的用户使用它为一个巨大的数据集创建一个拷贝分支(而且经常递归的对拷贝进行拷贝),或者是在尝试变化之前对当前的状态创建检查点,之后可以轻松的进行提交或回滚。

    像AFS一样,我们使用标准的写时拷贝(Copy-On-Write)技术来实现快照。当Master接收到一个快照请求时,它先取消快照相关的文件块的所有租约。这确保了任何后面对这些块的写操作将需要与Master进行交互,以获取租约的持有者,这将为Master提供一个为块创建一个新拷贝的机会。

    在租约被取消或者过期后,Master将这些操作记录到磁盘,然后通过复制源文件或目录树的元数据在它内存中的状态上执行这些日志记录。新创建的快照文件与源文件指向相同的块。

    在快照操作后,客户端第一次想要向块C中写入数据前,它将向Master发送一个请求来查找当前的租约持有者。Master注意到块C的引用计数大于1,它将推迟回复客户端的请求,并选择一个新的块句柄C’,然后通知每个拥有块C副本的块服务器,创建一个新的块C’。通过在同一个块服务器上创建一个新的块,我们能确保这个拷贝是本地的,不需要通过网络进行的(我们的磁盘速度是100MB以太网链路的3倍)。从这点上看,请求的处理不会与其它的块处理有差别:主节点将新块C’的租约授予其中一个副本,并回复客户端,客户端能够进行一般的写操作,并不知道这个块是从一个已存在的块上创建出来的。

    四、    Master操作

    Master执行所有的命名空间操作,此外,它管理整个系统内的所有块的副本:它决定块的存储位置,以及协调系统范围内的各种行为以保障块能够有足够的副本,均衡所有块服务器的负载,以及回收不再使用的存储空间。我们现在讨论这里提到的每一个话题。

    4.1 命名空间关键与锁

    许多Master操作会占用较长时间:例如,一个快照操作必须撤销所有进行快照的块所在块服务器的租约,我们不想推迟正在进行的其它Master操作,因此,我们允许多个操作同时进行,并使用命名空间区域的锁来确保这些操作按适当的顺序执行。

    不像许多传统的文件系统那样,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)如上面讨论的那样,我们想要将一个块的副本分布到不同的机架上。

    当副本的可用数量低于用户指定的值时,Master会尽快进行块的重新复制。以下原因可能引起这个操作:一个块服务器不可用,它会报告存储的副本可能被损坏,其中的一个磁盘由于错误不可用,或者副本的目标数变大了。每个需要进行重新复制的块基于几个因素优先进行:一个是块现有的副本数量与目标数差多少。比如,一个丢失两个副本的块比丢失一个副本的优先级高;此外,相对于最近被删除的文件的块来说,我们优先对活跃的文件的块进行重新复制(4.4);最后,为了最小化失效对正在运行的应用的影响,我们会提高阻塞客户进程的块的优先级。

    Master选取优先级最高的块,通过通知一些块服务器从一个存在的可用副本上拷贝块数据来“克隆”它。选择副本位置的方法与创建时类似:平均磁盘空间是使用率,限制单一块服务器上运行的克隆操作熟练,以及跨机架分布。为了防止克隆操作的流量影响到客户端的流量,Master限制了集群和每个块服务器上正在运行的克隆操作数量。此外,每个块服务器通过减少发往源块服务器的读请求,限制了每个克隆操作所占用的带宽。

    最后,Master周期性的重新均衡副本的负载:它检查当前的副本分布情况,并将副本移动到更好的磁盘空间上,达到负载均衡。通过这个过程,Master逐步的填充新的块服务器,而不是马上使用新的块和大量的写入流量填充它。这个新副本的分布标准与上面提到的类似。此外,Master必须选择移动哪个已存在的副本,通常情况下,它更倾向于从剩余空间小于平均值的那个块服务器上移动副本,以使磁盘使用率相等。

    4.4 垃圾回收

    在一个文件被删除后,GFS不会立即回收可用的物理存储空间。它只有在文件和块级别上定期的垃圾回收时才会进行。我们发现,这个方法使系统更加简单,更加可靠。

    4.4.1 机制

    当一个文件被应用删除时,Master会像其他操作一样立即记录下这个删除操作。然而,文件仅仅会被重命名为一个包含删除时间戳的、隐藏的名字,而不是立即回收资源。在Master定期的扫描文件系统的命名空间期间,它将真正删除已经被隐藏超过三天的文件(这个间隔可以配置),在此之前,文件仍然可以通过新的特殊的名字进行读取,也可以通过重命名为普通的文件名而撤销删除操作。当隐藏文件从命名空间中真正被删除时,内存中对应的元数据也会被清除,这样能有效的切断文件和它的所有块的连接。

    在对块的命名空间做类似的扫描时,Master标识出孤儿块(任何文件都无法访问到的块),并将那些块的元数据清除。在与Master进行定期的心跳信息交换时,每个块服务器报告其所含有块的集合,Master回复块服务器哪些块没有出现在Master的元数据中,块服务器将释放并删除这些块的副本。

    4.4.2 讨论

    虽然分布式回收机制在编程语言领域是一个需要复杂解决方案的难题,但在我们的系统中十分简单。我们能简单的确定一个块的所有引用:它们唯一的保存在Master的文件-块映射中。我们也能轻松的确定一个块的所有副本:它们都以Linux文件的形式存储在每台块服务器的指定目录下。任何Master不认识的副本都是“垃圾”

    用于存储回收的垃圾回收方法通过惰性删除拥有几个优势。首先,对于组件失效是常态的大规模分布式系统来说,这种方式简单可靠。块创建操作可能在一些块服务器上执行成功,但在另一些服务器上执行失败,残余的副本在主节点上无法识别。副本删除消息可能丢失,Master必须重新发送执行失败的消息,包括自身的和块服务器的。垃圾回收机制提供了一个一致的、可靠的方法来清除没用的副本。其次,它将存储回收操作合并到了Master定期的后台操作中,比如,定期的浏览命名空间和与块服务器握手。因此,它被批处理完成,开销被分摊了。此外,只有在Master相对空闲时才会完成垃圾回收,Master对客户端需要及时回复的请求有更高的优先级进行响应。第三,延迟回收存储空间可以为防止意外的、不可撤销的删除操作提供一个安全保障。

    在我们的经验中,主要的缺点是,当存储空间紧张时,会阻碍用户对使用的调优工作。频繁创建和删除临时文件的应用不能马上重用删除数据后的可用空间。我们通过在已被删除的文件上显式的再次进行删除来解决垃圾回收的这些问题。我们也可以允许用户对命名空间的不同部分应用不同的复制和回收策略。比如,用户可以指定一些目录树下的所有文件进行无副本存储,任何被删除的文件会从系统上迅速的、不可恢复的删除。

    4.5 过期副本的检测

    如果一个块服务器失效并在宕机期间丢失了块的修改操作,块副本可能会过期。对于每个块,Master维护一个块版本号来区分最新的副本和过期的副本。

    每当Master授予某个块一个新租约,它都会增加块的版本号,并通知最新的副本。Master和其它(Master的)副本都在它们的持久状态中记录了新的版本号,这会在任何客户端接收到通知并开始进行写操作之前进行。如果其它副本当前不可用,它的块版本号将不会被更新。当块服务器重启并报告它所有的块集合和它们相应的版本号时,Master还会探测到这个块服务器有过期的副本。如果Master发现有一个版本号高于自己的记录,Master会假设在它授予租约时失败,因此会选择更高的版本号作为最新的版本号。

    Master在定期的垃圾回收操作中清除过期的副本。在这之前,当它回复客户端的块信息请求时,它将认为一个过期的副本实际上根本并不存在。作为另一个安全措施,当Master通知客户端哪个块服务器持有这个块的租约时,或者在进行克隆操作期间它通知一个块服务器从另一个块服务器上读取数据时,包含块版本号。客户端或块服务器在执行操作时会验证这个版本号,以保证总是可访问的最新的数据。

    五、    容错和诊断

    在设计系统时,最大的挑战之一就是如何处理频繁的组件故障。组件的数量和质量让这些问题变得更为普通,而不是作为异常情况:我们不能完全的信任机器,也不能完全的信任磁盘。组件故障可能会造成系统不可用,甚至损坏数据。我们讨论如何面对这些挑战,以及当组件不可用时,系统内部用于诊断问题的工具。

    5.1 高可用性

    在GFS集群的数百台服务器中,必定有一些机器在给定的一段时间内是不可用的。我们通过两个简单但有效的策略来保证整个系统的高可用性:快速恢复和复制。

    5.1.1 快速恢复

    Master和块服务器都被设计为无论它们如何终止都能够在几秒内恢复自己的状态并重新启动。实际上,我们并不区分正常的和不正常的终止;可以通过kill掉进程来关闭服务。由于它们的请求超时而没有完成,客户端或其他服务器可能经历小的颠簸,它们会重新连接服务器并重试。6.2.2节报告了经过观察的启动时间。

    5.1.2 块复制

    如之前讨论过的,每个块被复制到不同机架上的多个块服务器上。用户能够为文件命名空间的不同部分指定不同的复制级别,默认为三个。当有块服务器掉线或通过校验和(5.2节)检测出损坏的副本时,Master克隆已存在的副本来保证每个块由足够的副本。尽管复制策略非常有效,但我们也在探索其他的跨服务器的冗余解决方案,如奇偶校验,或者纠删码(erasure code),来应对我们日益增长的只读存储需求。我们希望在我们的高度松耦合的系统中,这些复杂的冗余方案是具有挑战的,但并不是不可实现的,因为我们的操作主要是追加和读取,而不是小规模的随机写。

    5.1.3 Master复制

    为了可靠性,Master的状态也被复制。它的操作日志和检查点被复制到多个机器上。一个修改操作只有在它的日志被刷新到本地磁盘和所有的Master副本后才认为被提交完成。简单的说,一个Master负责所有的操作,包括后台的行为,如垃圾回收等改变系统内部状态的行为。当它失效时,能够瞬间重启。如果它的机器或磁盘发送故障,GFS的外部监控会其它有副本操作日志的机器上启动一个新的Master。客户端只使用主节点的名字(如,gfs-test),它是一个DNS的别名,可以更改这个名字来访问被重新放置到其它机器上的Master。

    此外,当primary Master宕机时,“影子”Master可以为文件系统提供一个只读的访问接口。它们是影子,而不是镜像,它们内部的数据会稍微的落后于primary Master,通常不到1秒。因此,它们对于那些不经常修改的文件的读取,或是不太在意得到的是稍微过期的数据的应用来说是有效的。实际中,因为文件内容是从块服务器读取的,所以应用察觉不到过期的文件内容。在短暂的时间窗口内,过期的可能是文件的元数据,如目录内容,或访问控制信息。

    为了保持最新的状态,影子Master读取一个正在增长的操作日志副本,并执行与primary Master相同的操作序列来改变自己的数据结构。像primary那样,它在启动后轮询块服务器(之后很少)来定位块副本,通过频繁的交换握手信息来监控它们的状态。当primary Master决定创建或删除副本造成位置信息更新时,影子Master才会通过primary Master更新状态。

    5.2 数据完整性

    每个块服务器使用校验和来探测存储的数据是否被损坏。考虑到一个GFS集群经常由数百个机器上的数千块磁盘组成,由于磁盘的损坏而造成的读和写路径上的数据损坏或丢失是很常见的。(其中的一个原因见7.1节。)我们能够使用块的其它副本来进行恢复,但是通过不同块服务器上的副本的比较无法探测出数据块是否损坏。此外,不同的副本可能是合理的:GFS修改操作的语法,特别是之前讨论过的原子追加操作,不能保证所有副本都相同。因此,每个块服务器必须独立的通过校验和验证它所拥有的拷贝是否合法。

    一个块被分割成64KB大小的block,每个block都有一个对应的32bit校验和。像其它元数据一样,校验和保存在内存中,并通过日志永久存储,与用户数据分开。

    对于读操作,在把数据返回给请求者之前,块服务器要验证读范围内所有数据block的校验和,因此,块服务器不会将损坏的数据传播到其它机器上。如果一个block与记录的校验和不匹配,块服务器会返回给请求者一个错误,并向Master报告这个错误。在响应中,请求者将从其它的副本读取数据,同时,Master将从其它副本上克隆这个块。在一个新的可用的副本复制完毕后,Master会通知报告错误的块服务器删除出错的副本。

    校验和对读操作几乎没有影响,原因有几个。因为大多数的读操作至少需要读取几个块,我们只需要读取一小部分额外的数据用于验证验证和。GFS客户端代码通过试图将读操作对齐到校验和的block边界上,大大减小了验证的开销。此外,块服务器上的校验和查询和对比不需要任何I/O就能完成,校验和的计算可以和I/O操作同时进行。

    校验和计算针对在块尾部进行追加写操作做了很大的优化(相对于覆盖已有数据的写操作),因为写操作在我们的工作中是占主导的。我们仅增量更新最后一个不完整的块的校验和,并用追加的新的校验和来计算block新的校验和。即使最后一个不完整的校验和已经被损坏,并且我们现在没有探测出来,新的校验和将不匹配存储的数据,当这个块下一次被读取时,就是检测出数据已经损坏。

    相反的,如果写操作覆盖块中已经存在的一个范围内,我们必须读取并验证要被覆盖的这个范围内的第一个和最后一个block,然后执行写操作,最后计算并记录新的校验和。如果我们在覆盖它们之前不进行对第一个和最后一个block的验证,新的校验和可能会隐藏没有被覆盖区域的错误。

    在空闲的时候,块服务器能浏览和验证不活动跃的块的内容,这允许我们探测很少被读取的块的数据损坏。一旦损坏被探测到,Master就能创建一个新的没有损坏的副本,并删除已损坏的副本。这能够防止不活跃的、已损坏的块欺骗Master,使Master认为它是块的一个可用的副本。

    5.3 诊断工具

    在问题隔离、调试和性能分析方面,详尽的、细节的诊断日志给我们很大的帮助。如果没有日志,我们很难理解短暂的、不重复的机器间的消息交互。GFS服务器生成诊断日志用于记录许多关键的事件(如块服务器启动和关闭),以及所有的远程调用(RPC)的请求和响应。这些诊断日志能够被自由的删除,不会给系统带来任何影响。然而,在空间允许的情况下,我们会尽量保存这些日志。

    远程调用(RPC)日志包括网络上所有的请求和响应,除了文件数据被读取或写入。通过匹配请求和响应,以及核对其它机器上的RPC记录,我们能重建这个交互历史用于诊断错误。这些日志也能用于跟踪负载测试和性能分析。

    记录日志所造成的性能影响很小(远小于它带来的好处),因为这些日志异步的、顺序的被写入。最近的事件也会保存在内存中,可用于持续的在线监控。

    六、    度量

    在这章中,我们介绍几个微基准测试来描述GFS架构和实现中固有的瓶颈,还有一些来自于Google正在使用的集群中。

    6.1 微基准测试

    我们在一个由1个主节点,2个主节点副本,16个块服务器和16个客户端组成的GFS系统上进行性能测试。注意,这个配置是为了方便测试,一般的集群包含数百个块服务器和数百个客户端。

    所有的机器都配置为两个1.4GHz的PIII处理器,2GB的内存,两个5400rpm的80GB的磁盘,以及一个100Mpbs全双工以太网连接到一个HP2524的交换机上。两个交换机之间只用1Gpbs的链路连接。

    6.1.1 读操作

    N个客户端同时从文件系统读取数据。每个客户端从320GB的数据中,随机的选取4MB区间进行读取。这个读操作会重复256次,使客户端最终读取到1GB的数据。块服务器总共只有32GB的内存,,所以我们期望最多有10%的缓存命中率。我们的结果应该与没有缓存的情况下相近。

     

    图3:总吞吐量。顶上的曲线显示了理论情况下的由我们的网络拓扑引入的限制。下面的曲线显示了测试的吞吐量。它们显示了置信区间为95%的误差,在一些情况下,由于测量的方差小而不太明显。

    图3(a)中显示了N个客户端的总读取速率和它的理论极限。当两个交换机之间的1Gpbs链路饱和时,理论极限为125MB/s,或者当客户端的网络接口达到饱和时,每个客户端的理论极限为12.5MB/s,无论哪个都适用。当只有一个客户端进行读取时,观测到的读取速率为10MB/s,或者是每个客户端极限的80%。对于16个读操作,总速率可以达到94MB/s,大约是125MB/s网络极限的75%,或者是每个客户端到达6MB/s的速率。效率从80%下降到75%的原因是,当读取操作的数量增加时,多个读操可能同时的从同一个块服务器上读取数据。

    6.1.2 写操作

    N个客户端同时往N个不同文件进行写操作。每个客户端以每次1MB的速率向一个新文件写入1GB的数据。总的写入速率和理论极限显示在图3(b)中。理论极限是67MB/s,因为我们需要把每个字节都写入到16个块服务器中的3个,而每个块服务器的输入连接极限为12.5MB/s。

    每个客户端的写入速率为6.3MB/s,大约是极限的一半,主要的原因是我们的网络协议栈,它与我们用于推送块副本的管道(pipelining)方式不太适应。从一个副本到另一个副本传播数据的延迟降低了整个系统的写入速率。

    16个客户端的总写入速率达到35MB/s(每个客户端的速率大概是2.2MB/s),大约是理论极限的一半。与多个客户端进行读操作的情况类似,由于进行写操作的客户端数量的增加,客户端同时向同一个块服务器写入的几率也会增加。此外,写操作冲突的几率比读操作的要更大,因为每次写操作需要涉及三个不同的副本。

    写操作比我们想要的要慢,在实际中,这不会成为一个主要的问题,因为即使对于单个客户端它也增加了时延,但是对于含有大量客户端的系统来说,它不会对系统的写入带宽有太大影响。

    6.1.3 记录追加

    图3(c)显示了记录追加的性能,N个客户端同时向一个单独的文件追加数据。性能受存储文件最后一个块的块服务器的网络带宽的限制,而不是客户端的数量。它的速率由一个客户端的6MB/s下降到16个客户端的4.8MB/s,主要是由冲突和不同客户端的网络传输速率不同造成的。

    我们的应用倾向于同时处理多个这样的文件。换句话说,N个客户端同时向M个共享文件追加数据,N和M在0到几百之间。因此,在我们的经验中,块服务器网络冲突在实际中不是一个严重的问题,因为当块服务器正在忙于处理一个文件时,客户端可以进行另一个文件的写入操作。

    6.2 实际应用中的集群

    我们现在测试了两个Google正在使用的集群,它们有一定的代表性。集群A通常用于上百个工程师进行研究和开发。一个普通的任务被人为初始化,并运行几个小时,它读取几MB到几TB的数据,进行转换或分析数据,并将结果写回到集群中。集群B主要用于生产数据的处理。在很少人为干预下,这些任务运行更长的时间,持续的产生和处理几TB的数据集。在这两个实例中,一个单独的任务由运行在很多机器上的很多进程同时读和写很多文件组成。

    6.2.1 存储

     

    表2:两个GFS集群的属性

    如表2的前五个条目所示,两个集群都有上百个块服务器,支持数TB的磁盘空间,存储了大小合适的数据,但没有占满。“已使用空间”包括所有的块副本。实际上,所有的文件都有三个副本,因此,集群实际上存储了18TB和52TB的文件数据。

    两个集群的文件数量相近,不过B含有更大比例的死文件,死文件是指被删除或者被新版本替换的文件,但它们的存储空间还没有被回收。它也存储了更多的块,因为它的文件较大。

    6.2.2 元数据

    块服务器总共存储了数10GB的元数据,大多数是用户数据64KB的block的校验和。块服务器上其它的元数据则是4.5节讨论的块版本号。

    Master上的元数据要小很多,只有几十MB,或者说,每个文件平均有100字节的元数据。这符合我们对Master内存在实际中不会限制系统能力的设想。大多数的文件元数据是以前缀压缩方式存放的文件名,其它的元数据包括文件的所有者和权限,文件到块的映射表,以及每个块的当前版本号。此外,还为每个块存储了当前副本的位置信息和引用计数用来实现写时拷贝(COW)。

    每个单独的服务器,不论是块服务器还是Master,只有50-100MB的元数据。因此恢复速度很快:在服务器能够应答请求之前,它会只花费几秒时间先从磁盘中读取元数据。然而,主节点会持续颠簸一段时间,通常是30秒-60秒,直到它从所有的块服务器上获取到块位置信息。

    6.2.3 读写速率

     

    表3:两个GFS集群的性能度量

    表3中显示了持续时间不同的读写速率,两个集群在做这些测试前都已经运行了大概一个星期的时间。(这两个集群是因为GFS更新到新版本而进行重启。)

    自从重启后,平均写速率一直小于30MB/s。当我们做这些测试时,B正在进行速率为100MB/s的大量写操作,因为写操作被传送到三个副本上,所以将产生300MB/s的网络负载。

    读速率要达到高于写速率。如我们设想的那样,整个工作由更多的读操作组成。两个集群都在进行繁重的读操作,特别是,集群A在前些周维持了580MB/s的读速率。它的网络配置能够支持750MB/s的速率,所以它有效的利用了它的资源。集群B支持的极限读速率为1300MB/s,但是,它的应用只用到了380MB/s。

    6.2.4 Master复制

    表3也显示了发往Master的操作速率,每秒有200-500个操作。Master可以轻松的应对这个速率,因此Master的处理性能不是瓶颈。

    在早期的GFS版本中,Master有时会成为瓶颈。它花费大多数时间连续的浏览大的目录(可能含有几百几千个文件)来查找指定的文件。我们已经改变了Master的数据结构,使用二分查找在命名空间中进行查找,以此提高效率。它也能轻松的支持每秒数千次的文件访问。如果需要,我们能通过在命名空间数据结构之前放置名字查询缓存的方法来进一步提高速度。

    6.2.5 恢复时间

    在一个块服务器发生故障后,一些块的副本数量会小于指定值,必须进行克隆操作使副本数恢复到指定值。恢复所有块所需的时间由资源的数量决定。在一个试验中,我们杀掉了B集群中的一个单独的块服务器,这个块服务器大概有15,000个块,包含了600GB的数据。为了减小对正在运行的应用的影响,以及为调度决策提供修正空间,我们的默认参数限制了集群的并发克隆数为91(块服务器数量的40%),在这里,每个克隆操作被允许最多使用6.2MB/s的带宽。所有的块在23.2分钟后恢复,复制速率为440MB/s。

    在另一个试验中,我们杀掉了两个块服务器,每个包含了大约16000个块和660GB的数据。这个双故障造成了有266个块的成为单副本。这266个块作为更高的优先级被克隆,并在两分钟之内恢复到至少有两个副本的状态,这样可以让集群处于一个能够容忍另一个块服务器故障而不丢失数据的状态。

    6.3 工作量明细

    在这章中,我们对两个GFS集群的工作进行详细的分解介绍,这两个集群与6.2节中的类似,但不完全相同。集群X用于研究和开发,而集群Y用于生产数据的处理。

    6.3.1 方法论和注意事项

    这些结果中包含了只有用户发起的请求,以至于它们能够反映出由应用文件系统整体产生的工作量。它们不包含用于执行客户端请求或者内部的后台行为的服务器间的请求,如正向写或重新均衡负载。

    I/O操作的统计是基于从GFS服务器的实际RPC请求日志上重新建立起来的信息的。例如,GFS客户端为了增加并行性,会将一个读操作分解成多个PRC,通过这些调用,我们可以推导出原始的读操作。因为我们的访问模式是高度模程式的,我们认为任何错误都属于误差。应用记录的详细日志能够提供更准确的数据,但是为了这个目的重新编译和重启上千个客户端在逻辑上是不可能的,而且从这么多机器上收集结果也是非常麻烦的。

    应该注意不要对我们的工作量做过度的归纳,因为Google完全控制着GFS和其它自己的应用,应用针对于GFS做了优化,同样,GFS也是为了这些应用而设计的。这类的相互作用同样也存在于普通应用和文件系统之间,但是在我们的实例中,这种作用更加显著。

    6.3.2 块服务器工作量

     

    表4:操作工作量大小明细表(%)

    表4显示了操作数量的分布。读操作展现出一个双峰的分布,小规模读(64KB以内的)来自于查询为主的客户端,它从海量文件中查询小片的数据。大规模读取(大于512KB)来自于从头到尾读取整个文件的连续读操作。

    在集群Y中有一些读操作没有返回任何数据。我们的应用,特别是哪些在生产系统中的,经常将文件用于生产者-消费者队列。生产者同时向一个文件追加数据,同时消费者从文件尾部读取数据。有时,当消费者超过生产者时,就没有数据返回了。集群X显示这种情况不常发生,因为它经常用于进行短暂的数据分析,而不是分布式应用的长久数据分析。

    写操作也展现出一个双峰的分布。大规模写操作(超过256KB)通常是由于Writer使用了缓存机制造成的;缓存了较少的数据,频繁的进行检查点操作或同步操作,或者仅仅生成了少量数据的Writer解释了为什么存在小规模的写操作(小于64KB)。

     

    表5:字节传输明细表(%)。对于读操作,这个大小是实际的读取和传输数据的总量,而不是请求的数据量。这两个可能的不同点在于如果试图读取超过文件结束的大小,这样的操作在我们的工作中并不常见。

    对于记录追加操作,集群Y比集群X中有更高的大规模追加操作比例,这是因为我们的生产系统使用了集群Y,针对GFS做了更多的优化。

    表5显示了按操作的数据大小得到的总的数据传输量。对于所有的操作,较大规模的操作(大于256KB)占据了主要的传输量;小规模的读操作(小于64KB)传输量较小,但是在读取的数据量中仍占有相当的比例,因为随机读操作需要进行查找工作。

    6.3.3 追加操作 vs 写操作

    追加操作被频繁的使用,尤其是在我们的生产系统中。对于集群X,写操作与记录追加操作在字节传输量上的比例为108:1,在操作数量上的比例为8:1。对于集群Y,,这个比例为3.7:1和2.5:1。此外,这些比例说明了对于两个集群,记录追加操作所占的比例都比写操作的大。然而,对于集群X,在测试期间,使用的追加操作相当少,以至于结果几乎被一两个使用特定缓存大小的应用歪曲了。

    不出所料的,记录追加操作占据了我们的数据修改的主要工作量,而不是覆盖写操作。我们测试了在primary副本上被覆盖的数据总量。这近似于一种情况,其中客户端故意的覆盖之前写入的数据,而不是追加新的数据。对于集群X,覆盖操作占字节修改的不到0.0001%,占操作数量的不到0.0003%。对于集群Y,这两个比例都为0.05%。尽管这是微小的,但是仍然比我们预期的要高。它证明了这些覆盖操作来自于客户端对于错误和超时的重试操作。它们不是工作量的一部分,而是重试机制的结果。

    6.3.4 Master工作量

     

    表6:主节点请求明细表(%)

    表6显示了根据发往Master的请求类型的明细。大多数请求都是读取操作查询块的位置信息(FindLocation)和数据修改操作的租约持有者的信息(FindLeaseHolder)。

    集群X和Y在删除请求的数量上有显现的差别,因为集群Y存储生产数据集,这些数据集会定期的重新生成数据以及用新版本的数据替换旧的数据。其中的一些不同被隐藏在了Open请求中,因为旧版本的文件可能以写模式打开并从头开始的方式,被隐式的删除。

    FindMatchingFiles是一个模式匹配请求,提供了“ls”和类似的文件操作。不像其他的Master操作,它可能处理一大部分命名空间,所以可能很昂贵。集群Y中看到了更多的这类请求,因为自动化的数据处理任务倾向检测部分文件系统,来获知整个应用的状态。相反的,集群X的应用更多的处于用户的显式控制之下,经常预先知道所有所需文件的名字。

    七、    经验

    在建造和部署GFS的过程中,我们经历了各种问题,一些是操作上的,一些是技术上的。

    起初,GFS被想象为是我们生产系统的一个后台的文件系统。随着时间的推移,GFS的使用逐渐包含了研究和开发的任务。它开始完全不支持类似权限和配额的功能,但是现在已经初步的包含了这些功能。虽然生产系统是被严格控制的,但是用户则不会总是这样。更多的基础构件被需要用来防止用户间的干扰。

    一些最大的问题是磁盘和Linux相关的问题。我们的许多磁盘要求Linux驱动,它们支持一定范围的IDE协议版本,但在实际中,只对最新版本的响应可靠。因为协议版本非常相似,这些驱动多数可以工作,但是偶尔也会有错误,造成驱动和内核对驱动器状态的认识不一致。由于内核的问题,这会导致数据静静的被损坏。这个问题迫使我们使用校验和来检测数据是否损坏,同时我们修改内核来处理这些协议错误。

    早期在使用Linux2.2内核时,由于fsync()效率问题,我们遇到了一些问题。它花费与文件大小成正比的时间,而不是修改部分的大小。对于我们的海量操作日志,特别是在检查点之前来说,确实是一个问题。我们在这个问题上花费了一些时间,通过同步写解决了这个问题,最后还是移植到了Linux2.4上。

    另一个Linux的问题是一个单独的读写锁的问题,在这个地址空间内的任何线程都必须在将磁盘page in时(读锁)或在调用mmap()进行修改地址空间时持有这个锁。我们发现我们的系统在低负载时,也会短暂出现超时现象,我们艰难的寻找资源瓶颈,或不定时发生的硬件故障。最后,我们发现在磁盘线程正在page in之前的映射数据时,这个单独的锁阻塞了主网络线程向内存中映射新数据。因为我们主要受网络接口的限制,而不是内存拷贝的带宽,所以,我们用pread()来代替mmap(),使用一个额外的拷贝开销来解决了这个问题。

    尽管偶尔会有问题,Linux代码常常帮助我们发现和理解系统行为。在适当的时候,我们会推进这个内核,并与开源社区共享我们的这些改动。

    八、    相关工作

    像其他的大型分布式文件系统一样,如AFS,GFS提供了一个与位置无关的命名空间,它能使数据为了负载均衡或容错透明的迁移。与AFS不同的是,GFS把文件数据分别到不同的存储服务器上,这种方式更像xFS和Swift,这是为了提高整体性能和增加容错能力。

    由于磁盘相对便宜,并且复制要比RAID的方法简单许多,GFS当前只使用了复制来提供冗余,所以比xFS和Swift存储了更多的原始数据。

    与AFS、xFS、Frangipani以及Intermezzo等文件系统不同,GFS并没有在文件系统接口下提供任何缓存机制。我们的目标工作是一个运行的应用很少会读取重复的数据,因为他们要么是从一个大的数据集中流式的读取数据,要么是随机的进行读取。

    一些分布式文件系统,如Frangipani、xFs、Minnesota’s GFS和GPFS,去掉了中间的服务器,依靠分布式算法来保证一致性和进行管理。为了简单的设计、增加它的可靠性和获得弹性,我们选择了中心化的方法。特别的是,因为Master已经保存有大多数有关的信息,并可以控制它们的改变,因此,它极大地简化了原本非常复杂的块分配和复制策略的方法。我们通过保持较小的Master状态信息,以及在其它机器上做完整的备份来提高系统容错性。弹性和高可用性(对于读操作)通过影子Master机制来提供的。更新Master状态是通过追加预写日志来实现持久化的。因此,我们可以调整为使用类似Harp的主拷贝方式来提供高可用性的,它比我们目前的方式有更高的一致性保证。

    我们解决了一个类似Lustre在有大量客户端时总的传输性能的问题。然而,我们通过只关注我们自己的应用,而不是创建一个适用于POSIX的文件系统来简化这个问题。此外,GFS采取大量的不可靠组件,所以容错将是我们设计的中心问题。

    GFS十分接近NASD架构,NASD架构是基于网络磁盘的,GFS使用了一般的机器作为块服务器,与NASD协议类型相同。但是与NASD不同的是,我们的块服务器使用了惰性分配固定大小块的方式,而不是可变大小的块。此外,GFS实现了在生产环境中所需的一些功能,如重新负载均衡、复制和恢复等。

    不像Minnesota’s GFS和NASD,我们不会去追求改变存储设备的模式。我们只关注由一般组件组成的复杂分布式系统所需要的日常数据的处理。

    通过原子记录追加操作实现的生产者-消费者队列解决了一个与River中的分布式队列类似的简单问题。River使用了基于内存的分布式队列,必须小心的控制数据流,GFS则能够使用被很多生产者同时追加持久化的文件。River模型支持m-n的分布式队列,但是缺少由持久化存储提供的容错功能,而GFS只能够有效的支持m-1的队列。多个消费者能够读取同一文件,但他们必须协调分区输入的负载。

    九、    结论

    Google文件系统展示了一个在一般硬件上支持大规模数据处理工作的核心特性。一些设计决定都是根据我们的特殊环境定制的,但许多还是能够用于类似规模和成本的数据处理任务。

    首先,根据我们当前的和预期的应用工作量和技术环境来重新考察传统的文件系统。观察结果引导出一个完全不同的设计思路。我们将组件失败看成是普通现象而非异常情况,对于向大文件进行追加(可能是并行的)和读操作(通常是序列化的)来进行优化,以及通过扩展接口和放松限制来改进整个系统。

    我们的系统通过持续健康、复制关键数据以及快速自动的恢复来提供容错。块复制允许我们容忍块服务器的错误。这些错误的频率促进了一个新奇的在线修复机制,它定期的、透明的修复损坏的数据,并尽快对丢失的副本进行补偿。此外,在磁盘或IDE子系统层上,我们使用了校验和来探测数据是否损坏,在这种规模的磁盘数量上,这种问题十分普遍。

    对于进行许多并行读写操作的各种任务,我们的设计提供了很高的总吞吐量。我们通过将控制流和数据流分开来实现这个目标,控制流直接经过Master,数据流块服务器和客户端之间传输。通过大的块大小和在数据修改操作中授予主拷贝一个块租约,能够最小化Master涉及的操作,这使简单的、中心的Master不会太可能成为瓶颈。我们相信我们的网络协议栈的改进可以提高从客户端看到的写入吞吐量的限制。

    GFS很好的满足了我们的存储需求,并作为存储平台在Google中得到广泛使用,无论在研究和开发,还是生产数据处理上。它是我们持续创新和攻克整个web范围的难题的一个重要工具。

    十、   个人的一些总结

    1. 总体架构

     

    图4:GFS的架构概要

    2. 存储结构

     

    图5:存储结构及关系。其中块的名字是由32bit的唯一值进行标识的。

    3. 元数据

     

    图6:元数据及文件定位

  • 相关阅读:
    QOMO Linux 4.0 正式版发布
    LinkChecker 8.1 发布,网页链接检查
    pgBadger 2.1 发布,PG 日志分析
    Aletheia 0.1.1 发布,HTTP 调试工具
    Teiid 8.2 Beta1 发布,数据虚拟化系统
    zLogFabric 2.2 发布,集中式日志存储系统
    开源电子工作套件 Arduino Start Kit 登场
    Piwik 1.9 发布,网站访问统计系统
    Ruby 1.9.3p286 发布,安全修复版本
    toBraille 1.1.2 发布,Java 盲文库
  • 原文地址:https://www.cnblogs.com/geekma/p/3128372.html
Copyright © 2011-2022 走看看