Google文件系统.docx
《Google文件系统.docx》由会员分享,可在线阅读,更多相关《Google文件系统.docx(30页珍藏版)》请在冰豆网上搜索。
Google文件系统
Google文件系统
摘要
我们设计并实现了Google文件系统,一个为数据中心的大规模分布应用设计的可伸缩的分布文件系统。
Google文件系统虽然运行在廉价的普遍硬件上,但是可以提供容错能力,为大量客户机提供高性能的服务。
我们的系统与许多以前的分布文件系统拥有许多相同的目标,但我们的设计还受到我们对我们的应用负载和技术环境观察的影响,不管现在还是将来,我们和早期文件系统的假设都有明显的不同。
所以我们重新审视了传统的选择,发展了完全不同的设计观点。
Google文件系统成功的满足了我们的存储需求。
它作为存储平台被广泛的部署在Google内部,用在我们的服务中产生和处理数据,还用于那些需要大规模数据集的研究和开发。
目前为止最大的集群利用数千台机器内的数千个硬盘,提供了数百T的存储空间,同时为数百个客户机服务。
在这篇论文中,我们展现如果用文件系统接口扩展设计去支撑分布应用,讨论我们设计的许多方面,最后报告在小规模性能测试以及真实世界中系统的性能测试结果。
1.简介
我们设计并实现了Google文件系统(GoogleFileSystem-GFS),用来满足Google迅速增长的数据处理需求。
GFS与过去的分布文件系统拥有许多相同的目标,例如性能,可伸缩性,可靠性以及可用性。
然而,它的设计还受到我们对我们的应用负载和技术环境观察的影响,不管现在还是将来,我们和早期文件系统的假设都有明显的不同。
所以我们重新审视了传统的选择,采取了完全不同的设计观点。
首先,组件失效不再被认为是意外,而是被看做正常的现象。
这个文件系统包括几百甚至几千台普通廉价部件构成的存储机器,又被相应数量的客户机访问。
组件的数量和质量几乎保证,在任何给定时间,某些组件无法工作,而某些组件无法从他们的目前的失效状态恢复。
我们发现过,应用程序bug造成的问题,操作系统bug造成的问题,人为原因造成的问题,甚至硬盘、内存、连接器、网络以及电源失效造成的问题。
所以,常量监视器,错误侦测,容错以及自动恢复系统必须集成在系统中。
其次,按照传统的标准来看,我们的文件非常巨大。
数G的文件非常寻常。
每个文件通常包含许多应用程序对象,比如web文档。
传统情况下快速增长的数据集在容量达到数T,对象数达到数亿的时候,即使文件系统支持,处理数据集的方式也就是笨拙地管理数亿KB尺寸的小文件。
所以,设计预期和参数,例如I/O操作和块尺寸都要重新考虑。
第三,在Google大部分文件的修改,不是覆盖原有数据,而是在文件尾追加新数据。
对文件的随机写是几乎不存在的。
一般写入后,文件就只会被读,而且通常是按顺序读。
很多种数据都有这些特性。
有些数据构成数据仓库供数据分析程序扫描。
有些数据是运行的程序连续生成的数据流。
有些是存档的数据。
有些数据是在一台机器生成,在另外一台机器处理的中间数据。
对于这类巨大文件的访问模式,客户端对数据块缓存失去了意义,追加操作成为性能优化和原子性保证的焦点。
第四,应用程序和文件系统API的协同设计提高了整个系统的灵活性。
例如,我们放松了对GFS一致性模型的要求,这样不用加重应用程序的负担,就大大的简化了文件系统的设计。
我们还引入了原子性的追加操作,这样多个客户端同时进行追加的时候,就不需要额外的同步操作了。
论文后面还会对这些问题的细节进行讨论。
为了不同的应用,Google已经部署了许多GFS集群。
最大的一个,拥有超过1000个存储节点,超过300T的硬盘空间,被不同机器上的数百个客户端连续不断的频繁访问着。
2.设计概述
2.1设计预期
在设计我们需要的文件系统时,我们用既有机会又有挑战的设计预期来指导我们的工作。
前面我们提到了一些关键的信息,这里我们讲更细节的描述我们的设计预期。
∙这个系统由许多廉价易损的普通组件组成。
它必须持续监视自己的状态,它必须在组件失效作为一种常态的情况下,迅速地侦测、承担并恢复那些组件失效。
∙这个系统保存一定数量的大文件。
我们预期有几百万文件,尺寸通常是100MB或者以上。
数GB的文件也很寻常,而且被有效的管理。
小文件必须支持,但是不需要去优化。
∙负载中主要包含两种读操作:
大规模的流式读取和小规模随机读取。
大规模的流式读取通常一次操作就读取数百K数据,更常见的是一次性读取1MB甚至等多。
同一个客户机的连续操作通常是对一个文件的某个区域进行连续读取。
小规模的随机读取通常是在随机的位置读取几个KB。
对性能有所要求的程序通常把小规模的读批量处理并且排序,这样就不需要对文件进行时前时后的读取,提高了对文件读取的顺序性。
∙负载中还包括许多大规模的顺序的写操作,追加数据到文件尾部。
一般来说这些写操作跟大规模读的尺寸类似。
数据一旦被写入,文件就几乎不会被修改了。
系统对文件的随机位置写入操作是支持的,但是不必有效率。
∙系统必须高效的实现良好定义的多客户端并行追加到一个文件的语意。
我们的文件经常用于"生产者-消费者"队列,或者多路文件合并。
数百个生产者,一个机器一个,同时的对一个文件进行追加。
用最小的同步开销实现追加的原子操作是非常重要的。
文件可能稍后被读取,也可能一个消费者同步的读取文件。
∙高度可用的带宽比低延迟更加重要。
大多数我们的目标程序,在高传输速率下,大块地操作数据,因为大部分单独的读写操作没有严格的响应时间要求。
2.2接口
GFS提供了一个类似传统文件系统的接口,虽然它并没有实现类似POSIX的标准API。
文件在目录中按照层次组织,用路径名来标识。
我们支持常用的操作,如创建,删除,打开,关闭,读和写文件。
而且,GFS有快照和记录追加操作。
快照操作可以用很低的成本创建文件或者目录树的拷贝。
记录追加操作可以在保证原子性的前提下,允许多个客户端同时在一个文件上追加数据。
这对于实现多路结果合并以及"生产者-消费者"模型非常有好处,多个客户端可以同时在一个文件上追加数据,而不需要任何额外的锁定。
我们发现这些文件类型对构建大型分布应用是非常有价值的。
快照和记录追加将分别在3.4和3.3章节讨论。
2.3架构
一个GFS集群包含一个主服务器和多个块服务器,被多个客户端访问,如图1。
这些机器通常都是普通的Linux机器,运行着一个基于用户层的服务进程。
如果机器的资源允许,而且运行多个程序带来的低稳定性是可以接受的话,我们可以很简单的把块服务器和客户端运行在同一台机器。
图1:
GFS架构
文件被分割成固定尺寸的块。
在每个块创建的时候,服务器分配给它一个不变的、全球唯一的64位的块句柄对它进行标识。
块服务器把块作为linux文件保存在本地硬盘上,并根据指定的块句柄和字节范围来读写块数据。
为了保证可靠性,每个块都会复制到多个块服务器上。
缺省情况下,我们保存三个备份,不过用户可以为不同的文件命名空间设定不同的复制级别。
主服务器管理文件系统所有的元数据。
这包括名称空间,访问控制信息,文件到块的映射信息,以及块当前所在的位置。
它还管理系统范围的活动,例如块租用管理,孤儿块的垃圾回收,以及块在块服务器间的移动。
主服务器用心跳信息周期地跟每个块服务器通讯,给他们以指示并收集他们的状态。
GFS客户端代码被嵌入到每个程序里,它实现了Google文件系统API,帮助应用程序与主服务器和块服务器通讯,对数据进行读写。
客户端跟主服务器交互进行元数据操作,但是所有的数据操作的通讯都是直接和块服务器进行的。
我们并不提供POSIXAPI,而且调用不需要深入到Linux的vnode级别。
不管是客户端还是块服务器都不缓存文件数据。
客户端缓存几乎没什么好处,因为大部分程序读取巨大文件的全部,或者工作集太大无法被缓存。
不进行缓存简化了客户端和整个系统,因为无需考虑缓存相关的问题。
(不过,客户端会缓存元数据。
)块服务器不需要缓存文件数据的原因是,块保存成本地文件形式,Linux的缓冲器会把经常被访问的数据缓存在内存中。
2.4单一主服务器
单一的主服务器大大简化了我们的设计,这样主服务器可以通过全局的信息精确确定块的位置以及进行复制决定。
然而,我们必须减少主服务器对数据读写的影响,避免使主服务器成为系统的瓶颈。
客户端不通过主服务器读写数据。
反之,客户端向主服务器询问它应该联系的块服务器。
客户端短期缓存这些信息,后续的操作直接跟块服务器进行。
让我们解释下图一表示的一次简单读取的流程。
首先,利用固定的块尺寸,客户端把文件名和程序指定的字节偏移转换成文件的块索引。
然后,它把文件名和块索引发送给主服务器。
主服务器回答相应的块句柄和副本们的位置。
客户端用文件名和块索引作为键值缓存这些信息。
然后客户端发送请求到其中的有一个副本处,一般会选择最近的。
这个请求指定了块的块句柄和字节范围。
对同一块的更多读取不需要客户端和主服务器通讯,除非缓存的信息过期或者文件被重新打开。
实际上,客户端通常在一次请求中查询多个块,而主服务器的回应也可以包含紧跟着这些请求块后面的块的信息。
这些额外的信息实际上,在没有代价的前提下,避免了客户端和服务器未来的几次通讯。
2.5块尺寸
块尺寸是关键设计参数之一。
我们选择了64MB,这远大于一般文件系统的块尺寸。
每个块副本保存在块服务器上的Linux文件内,尺寸只在需要的时候才扩展变大。
惰性空间分配避免了造成内部碎片带来的空间浪费,或许对巨大的块尺寸最没有争议的就是这点。
巨大的块尺寸有几个重要的好处。
首先,它减少了客户端和主服务器通讯的需求,因为对同一个块的读写,只需要一次用于获得块位置信息的与主服务器的通讯。
对我们的工作负载来说,这种减少尤其明显,因为我们的应用程序经常连续读写巨大的文件。
即使是对小规模随机读取也有好处,因为客户端也可以轻松的缓存一个数TB工具集的所有块位置。
其次,由于块尺寸很大,所以客户端会对一个给定的块进行许多操作,这样就可以通过跟块服务器保持较长时间的TCP连接来减少网络负载。
第三,它降低了主服务器需要保存的元数据的尺寸。
这就允许我们把元数据放在内存中,这样会带来一些其他的好处,我们将在2.6.1章节讨论。
另一方面,巨大的块尺寸,即使配合惰性空间分配,也有它的缺点。
小文件包含较少的块,甚至可能是一块。
如果有许多的客户端访问同一个小文件,那么存储这些块的块服务器就会变成热点。
在实践中,热点没有成为主要的问题,因为我们的程序通常是连续的读取多个块的大文件。
然而,在GPS第一次用于一个批处理队列系统的时候,热点还是产生了:
一个执行文件被写为GFS的一个单块文件,然后在数百个机器上同时启动。
保存这个执行文件的几个块服务器被数百个并发请求访问到过载的地步。
我们解决了这个问题,方法是用更高的复制参数来保存这类的执行文件,以及让批处理队列系统错开程序的启动时间。
一个可能的长期解决方案是,在这样的情况下,允许客户端从其他客户端读取数据。
(译者注:
这个热点事件,其实就是一个典型的文件分发问题,而后面提到的所谓可能的长期解决方案其实就是p2p。
)
2.6元数据
主服务器保存三种主要类型的元数据:
文件和块的命名空间,文件到块的映射,以及每个块副本的位置。
所有的元数据都保存在主服务器的内存里。
除此之外,前两种类型(命名空间和文件块映射)的元数据,还会用日志的方式保存在主服务器的硬盘上的操作日志内,并在远程的机器内复制一个副本。
使用日志,让我们可以简单可靠的更新主服务器的状态,而且不用担心服务器崩溃带来的数据不一致的风险。
主服务器不会持久化保存块的位置信息。
主服务器在自己启动以及块服务器加入集群的时候,询问块服务器它所包含的块的信息。
2.6.1内存内数据结构
因为元数据保存在内存中,所以主服务器操作的速度很快。
而且,主服务器可以在后台,简单而高效的周期扫猫自己的整个状态。
这种周期性的扫描用来在块服务器间实现块的垃圾收集的功能,用来实现块服务器失效的时复制新副本的功能,用来实现负载均衡的块移动的功能,以及用来实现统计硬盘使用情况的功能等。
4.3和4.4章节将深入讨论这些行为。
这种纯内存的机制一种可能的问题是,块的数量和整个系统的容量都受限于主服务器拥有的内存尺寸。
不过实际上这不是一个严重的限制。
对于每个64MB的块,服务器只需管理不到64字节的元数据。
大多数的块是满的,因为每个文件只有最后一块是部分填充的。
类似的,每个文件的命名空间通常在64字节以下,因为保存的文件名是用前缀压缩算法压缩过的。
如果需要支持更大的文件系统,为主服务器增加额外内存的花费是很少的,而把元数据保存在内容带来了系统的简洁性,可靠性,高性能和灵活性。
2.6.2块位置
主服务器并不保存哪个块服务器有给定块的信息的持久化记录。
它只是在启动的时候向块服务器要求这些信息。
然后主服务器就可以保持它的信息最新,因为它控制了所有的块位置分配,而且用规律的心跳信息监控块服务器的状态。
开始的时候,我们试图把块位置信息持久化的保存在主服务器,但是我们发现在启动的时候查询块服务器,然后定期询问的方式更简单。
这简化了在块服务器在加入和离开集群,改名,失效,重启等等情况下,主服务器和块服务器的同步问题。
在一个有数百台服务器的集群,这些事件发生的太频繁了。
理解这个设计思想的另外的途径是,理解块服务器才能最后确定一个块是否在它的硬盘上。
我们没有打算在主服务器上面管理一个这些信息的全局视图,因为块服务器的错误可能会导致块自动消失,亦或者管理员可能会改变一个块服务器的名字。
2.6.3操作日志
操作日志包含核心元数据变化的历史记录。
这对GFS很重要。
这不仅是因为它是元数据唯一的持久化存储记录,而且因为它起到了定义同步操作顺序的逻辑时间线的作用。
文件和块以及他们的版本,都是唯一和持久地由他们创建时的逻辑时间标识的。
因为操作日志很重要,我们必须稳定的保存它,并保证只有在元数据的变化被持久化保存后,这种变化才对客户端可见。
否则,即使块没有发生任何问题,我们仍有可能丢失整个文件系统,或者丢失近期的客户端操作。
所以,我们把操作日志复制到多台远程机器,而且仅在把最近的日志记录写入本地以及远程机器的硬盘后,才会对客户端操作进行响应。
写入之前主服务器批量处理数个日志记录,减少写入和复制的负载。
主服务器通过重放操作日志恢复它的文件系统。
为了降低启动时间,我们必须保持日志足够小。
当日志增长到一个特定尺寸的时候,主服务器就会汇总它的状态为一个检查点,这样它就可以通过从硬盘调入最新的一个检查点来恢复系统,只需要重放检查点后有限数目的日志记录即可。
检查点保存在一个压缩B-树形式的结构里,可以直接映射到内存,而且用于命名空间查询时无需额外的解析。
这大大提高了恢复速度,而且增强了可用性。
创建一个检查点可能会花点时间,所以主服务器的内部状态被结构为一种形式,这种形式保证了检查点的创建不会对正好这时进入的动作造成延迟。
主服务器用分别的线程切换到新的日志文件和创建新的检查点。
新的检查点包括切换前所有的动作。
集群有百万文件时,创建一个检查点需要一分钟左右。
创建完成后,检查点会被写入在本地和远程的硬盘里。
进行恢复仅需要最新的检查点和相应的日志文件。
老的检查点和日志文件可以自由的被删除,不过我们通常会保存一些用来应付数据灾难。
创建检查点过程中的失败不会对正确性产生任何影响,因为恢复代码可以检测并跳过没有完成的检查点。
2.7一致性模型
GFS有一个简化的一致性模型可以完美的支持我们高度分布的应用,但保持了简单以及容易实现的优点。
我们现在讨论GFS系统对一致性的保证,以及应用程序如何保证。
我们强调GFS如何管理这些保证,但是实现细节我们将在论文的其他部分介绍。
2.7.1GFS的保证
文件命名空间的修改(例如,文件创建)是原子性的。
他们仅受主服务器的控制:
命名空间锁定保证了原子性和正确性(4.1章);主服务器的操作日志定义了这些操作的全局总顺序(2.6.3章)。
写
记录追加
串行成功
已定义
已定义
部分不一致
并行成功
一致但是未定义
失败
不一致
表1操作后的文件状态
数据操作后文件范围的状态取决于操作的类型,是否成功,以及是否是同步操作。
表1概述了这些结果。
如果所有客户端,不管从哪个副本读取,看到的数据都一样,那么我们说文件区域是一致的。
如果数据操作后,文件范围是一致的,且客户端可以知道到操作写入了什么内容,那么文件区域是已定义的。
当数据操作完成,同步写入者间没有冲突,那么影响的文件范围就是已定义的(隐含了一致性):
所有的客户端一直都可以看到写入的内容。
成功的同步操作后,文件范围未定义,但是是一致的:
所有的客户端看到同样的数据,但是任何一个操作的写入内容都不会被表现出来。
通常情况下,文件范围内包含了混杂的来自多个操作的数据片段。
一个失败的操作,会让文件范围不一致(所以也是未定义的):
不同的时间不同的客户端会看到不同的数据。
后面我们将描述我们的应用如何区分已定义和未定义的范围。
应用程序没有必要进一步区分未定义范围的不同类型。
数据操作可以是写或者记录追加。
写操作会把数据写在应用程序指定的偏移位置。
即使是在同步进行多个操作时,记录追加至少可以把数据(记录)原子性的追加到文件中一次,但是偏移位置是GFS选择的(3.3章)。
(相比而言,通常说的追加写入的偏移位置在文件的尾部。
)偏移位置被返回给客户端,用来标明包含记录的已定义范围的起点。
此外,GFS可能会在中间插入填充数据或者记录的副本。
他们占用那些被认定是不一致的范围,通常他们比用户数据的量小的多。
在一系列成功的操作后,被操作的数据范围被保证为已定义的,并且包含最后一次操作写入的数据。
(a)GFS对块的多个副本采用一样的顺序进行操作(3.1章)。
(b)并使用块版本号来检测副本是否因为它所在的块服务器当机(4.5章)而错过了某些操作,而失效了。
失效的副本不会再被任何操作涉及,也不会被主服务器作为块位置告知客户端。
他们会优先被垃圾收集。
由于客户端缓存块位置,所以在信息刷新前,他们有可能从一个失效的副本读取数据。
时间窗口由缓存的超时以及文件的下一次打开时间决定,文件打开后会清除缓存中与文件有关的所有块信息。
而且由于我们的文件大多数都是只进行追加的,所以一个失效的副本通常返回一个提前结束的块而不是过期的数据。
读取者重新尝试并联络主服务器后,就会立刻得到当前的块位置。
成功操作很久以后,组件的失效当然也可以损坏或者毁掉数据。
GFS用主服务器和块服务器之间的定期握手来找到失效的块服务器,用校验和来检测数据的损坏(5.2章)。
一旦发现问题,数据会尽快从有效的副本中恢复出来(4.3章)。
只有一个块的所有副本在GFS做出反应之前,全部丢失,这个块才会不可逆转的丢失,而通常GFS的反应是在几分钟内的。
即使在这种情况下,块不可用,而不是损坏:
应用程序会收到清晰的错误信息而不是损坏的数据。
2.7.2程序的实现
基于GFS的应用程序利用一些其他目的也需要的简单技术就可以跟简化的一致性模型协作了:
倚靠追加而不是其他的写入,检查点,写操作的自验证,自说明的记录。
实际上,我们所有的程序对文件的操作追加比覆盖多。
一种典型的使用情况是,写入者从头到尾生成了一个文件。
写入所有数据后写入者把文件名改为一个持久的文件名,或者周期性的检查成功写入了多少数据。
检查点可以包含程序级别的校验和。
读取者仅验证并处理上个检查点内的文件区域,这些文件区域的状态一定是已定义的。
这个方法满足了我们所有的一致性和并行性的要求。
追加比随机位置写入更加有效率,而且对应用程序的失败更有弹性。
检查点可以让写入者渐进的重新开始,并且可以保证读取者处理已经被成功写入的文件数据,即使从应用程序的观点来看文件的写入并未完成。
另外一种典型的使用情况下,许多写入者并行的追加数据到同一个文件,进行结果的合并或者进行生产者-消费者模型队列。
记录追加的至少一次追加的语意保证了每个写入者的输出。
读取者用下面的方法来处理偶然填充数据和重复内容。
写入者准备的每条记录都包含了额外的信息,例如校验和,可以用来验证它的有效性。
读取者可以利用校验和抛弃额外的填充数据和记录片段。
如果应用不能容忍偶尔的重复内容(比如,如果他们想触发非幂等操作),可以用记录的唯一标识符来过滤它们,这些唯一标识符通常用于命名相应的程序节点,例如web文档。
这些记录I/O的功能都包含在库代码中在我们的程序中共享,并且适用于Google实现的其他的文件接口。
所以,相同系列的记录,加上一些罕见的重复,总是直接被分发给记录读取者。
3.系统交互
我们设计这个系统来最小化主服务器对所有操作的参与。
在这样的背景下,我们现在描述一下客户机,服务器和块服务器如何交互以实现数据操作,原子化的记录追加以及快照。
3.1租约和变更顺序
变更就是一个会改变块内容或者元数据的操作,例如写入或者记录追加。
每个变更执行在块的所有副本上。
我们使用租约来保持多个副本间变更顺序的一致性。
主服务器为其中一个副本签署一个块租约,我们把这个副本叫做主块。
主块选择对块所有操作的一系列顺序。
进行操作的时候所有的副本遵从这个顺序。
这样全局的操作顺序首先由主服务器选择的租约生成顺序决定,然后由租约中主块分配的序列号决定。
租约机制的设计是为了最小化主服务器的管理负载。
租约的初始超时是60秒。
然而一旦块被修改过,主块可以请求更长的租期,通常可以通过主服务器的确认收到额外的时间。
这些额外时间的请求和批准都通过服务器和块服务器之间的心跳信息来传递。
有时候,主服务器会在租约到期前取消它(例如,当服务器想取消施加在一个已经被改名的文件上的操作。
)。
即使主服务器和主块失去联系,它仍旧可以安全地在老租约到期后产生一个加诸于其他的副本的租约。
在图2中,我们将用如下这些数字步骤,展现写入操作的控制流程。
图2:
写的控制和数据流程
1.客户机向主服务器询问哪一个块服务器保存了当前的租约,以及其他副本的位置。
如果没有一个块服务器有租约,主服务器就选择一个副本给它一个租约(没有被显示出来)。
2.主服务器回复主块的标识符以及其他副本的位置。
客户机为了后续的操作缓存这个数据。
只有主块不可用,或者主块回复说它已经不在拥有租约的时候,客户机才需要重新跟主服务器联络。
3.客户机把数据推送到所有的副本上。
客户机可以用任意的顺序推送。
每个块服务器会把这些数据保存在它的内部LRU缓冲内,直到数据被使用或者过期。
通过把数据流和控制流分离,我们可以基于网络负载状况对昂贵的数据流进行规划,以提高性能,而不用去管哪个块服务器是主块。
3.2章节会更详细地讨论这点。
4.所有的副本都被确认已经得到数据后,客户机发送写请求到主块。
这个请求标识了早前推送到所有副本的数据。
主块为收到的所有操作分配连续的序列号,这些可能来自不同的客户机。
它依照序列号的顺序把这些操作应用到它自己的本地状态中。
5.主块把写请求传递到所有的二级副本。
每个二级副本依照主块分配的序列号的顺序应用这些操作。
6.所有二级副本回复主块说明他们已经完成操作。
7.主块回复客户机。
任何副本产生的错误都会报告给客户机。
错误的情况下,主块和一些二级副本可能成功的写入了数据。
(如果主块写入失败,操作就不会被分配序列号,也不会被传递。
)客户端请求被确认为失败,已经修改的区域保持不一致的状态。
我们的客户机代码通过重复失败的操作来处理这样的错误。
在完全从头开始写入之前,可能会先从步骤3到步骤7进行几次尝试。
如果应用程序的写入量很大或者包含多个块,GFS客户机代码会把他们分成多个写操作。
他们都遵循前面描述的控制流程,但是他们可能会被来自其他客户机的同步操作插入或者覆盖。
所以,共享的文件范围的尾部可能包含来自不同客户机的片段,虽然这些副本是一致的,因为这些分别的操作在所有的副本都按照相同的顺序成功完成了。
这使文件范围处于2.7章描述过的一致但是未定义的状态。
3.2数据流
为了提高网络的效率我们把数据流和控制流分开。
控制流从客户机到主块然后再到所有二级副本的同时,数据顺序推送到一个精心选择的管道形式的块服务器链。
我们的目标是完全利用每个机器的带