url: https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-03-gfs
3.1分布式存储系统的难点(Why Hard)
今天我们讨论一下GFS,也就是The Google File System这篇论文。这门课程的主要内容是“大型存储”,而GFS这是这门课里有关如何构建大型存储系统的众多案例学习的第一篇。
之所以要说存储,原因是,存储是一种关键的抽象。你可以想象,在分布式系统中,可能有各种各样重要的抽象可以应用在分布式系统中,但是实际上,简单的存储接口往往非常有用且极其通用。所以,构建分布式系统大多都是关于如何设计存储系统,或是设计其它基于大型分布式存储的系统。所以我们会更加关注如何为大型分布式存储系统设计一个优秀的接口,以及如何设计存储系统的内部结构,这样系统才能良好运行。通过阅读GFS论文,我们可以开始了解到这是怎么做到的。
同时,GFS论文也涉及到很多本课程常出现的话题,例如并行性能、容错、复制和一致性。论文的内容也较为直观,容易理解。论文本身也是一篇优秀的系统论文,它从硬件到使用了GFS的软件都有讨论,并且它也是一个成功的现实世界的设计。尽管这是在学术会议上发表的学术论文,但是文章里介绍的东西(GFS)也相当成功,并且在现实世界中使用了相当长的时间。所以,我们知道,我们今天讨论的是一个非常好的且有用的设计。
在讨论GFS之前,我想聊一聊关于分布式存储系统,做一些背景介绍。
首先 为什么分布式存储系统会如此之难,以至于你需要做大量的工作才能让它正确工作?我们从一个特殊的角度来理解,在这门课程的后面,这种理解角度也会出现。
人们设计大型分布式系统或大型存储系统出发点通常是,他们想获取巨大的性能加成,进而利用数百台计算机的资源来同时完成大量工作。因此,性能问题就成为了最初的诉求。 之后,很自然的想法就是将数据分割放到大量的服务器上,这样就可以并行的从多台服务器读取数据。我们将这种方式称之为分片(Sharding)。
如果你在成百上千台服务器进行分片,你将会看见常态的故障。如果你有数千台服务器,那么总是会有一台服务器宕机,每天甚至每个小时都可能会发生错误。所以,我们需要自动化的方法而不是人工介入来修复错误。我们需要一个自动的容错系统,这就引出了容错这个话题(fault tolerance)。
实现容错最有用的一种方法是使用复制,只需要维护2-3个数据的副本,当其中一个故障了,你就可以使用另一个。所以,如果想要容错能力,就得有复制(replication)。
如果有复制,那就有了两份数据的副本。可以确定的是,如果你不小心,它们就会不一致。所以,你本来设想的是,有了两个数据副本,你可以任意使用其中一个副本来容错。但是如果你不够小心,两个数据的副本就不是完全一致,严格来说,它们就不再互为副本了。而你获取到的数据内容也将取决于你向哪个副本请求数据。这对于应用程序来说就有些麻烦了。所以,如果我们有了复制,我们就有不一致的问题(inconsistency)。
通过聪明的设计,你可以避免不一致的问题,并且让数据看起来也表现的符合预期。但是为了达到这样的效果,你总是需要额外的工作,需要不同服务器之间通过网络额外的交互,而这样的交互会降低性能。所以如果你想要一致性,你的代价就是低性能。但这明显不是我们最开始所希望的。(trade-off)
当然,这里并不是绝对的。你可以构建性能很高的系统,但是不可避免的,都会陷入到这里的循环来。现实中,如果你想要好的一致性,你就要付出相应的代价。如果你不想付出代价,那就要忍受一些不确定的行为。我们之后会在很多系统中看到这里介绍的循环。通常,人们很少会乐意为好的一致性付出相应的性能代价。(trade-off)
3.2 错误的设计(Bad Design)
这里说到了一致性,我在后面的课程会对“好”的一致性做更多的介绍(lec6-7)。对于具备强一致或者好的一致性的系统,从应用程序或者客户端看起来就像是和一台服务器在通信。尽管我们会通过数百台计算机构建一个系统,但是对于一个理想的强一致模型,你看到的就像是只有一台服务器,一份数据,并且系统一次只做一件事情。这是一种直观的理解强一致的方式。你可以认为只有一台服务器,甚至这个服务器只运行单线程,它同一时间只处理来自客户端的一个请求。这很重要,因为可能会有大量的客户端并发的发送请求到服务器上。这里要求服务器从请求中挑选一个出来先执行,执行完成之后再执行下一个。
尽管我们会通过数百台计算机构建一个系统,但是对于一个理想的强一致模型,你看到的就像是只有一台服务器,一份数据,并且系统一次只做一件事情。这是一种直观的理解强一致的方式。你可以认为只有一台服务器,甚至这个服务器只运行单线程,它同一时间只处理来自客户端的一个请求。
对于存储服务器来说,它上面会有一块磁盘。执行一个写请求或许意味着向磁盘写入一个数据或者对数据做一次自增。如果这是一次修改操作,并且我们有一个以key-value为索引的数据表单,那么我们会修改这个表单。如果是一次读取操作,我们只需要将之前写入的数据,从表单中取出即可。
为了让这里的简单服务有可预期的行为,需要定义一条规则:一个时间只执行一条请求。这样每个请求都可以看到之前所有请求按照顺序执行生成的数据。所以,如果有一些写请求,并且服务器以某种顺序一次一个的处理了它们,当你从服务器读数据时,你可以看到期望的数据。这里的解释不是很直观,你可以通过下面的例子去理解。
为了让“修改数据”服务有可预期的行为,需要定义一条规则:一个时间只执行一条请求。如果有一些写请求,并且服务器以某种顺序一次一个的处理了它们,当你从服务器读数据时,你可以看到期望的数据。这里的解释不是很直观,你可以通过下面的例子去理解。
一个场景为了展示:即使在一个非常简单的系统中,仍然会出现一些模糊的场景使得你不知道系统的执行过程以及输出结果。你能做的只是从产生的结果来判断系统的输出是一致性还是非一致性 --------------------------------- start
例如,我们有一些客户端,客户端C1发起写请求将X设置成1。在同一时刻,客户端C2发起写请求将X设置成2。
过了一会,在C1和C2的写请求都执行完毕之后,客户端C3会发送读取X的请求,并得到了一个结果。客户端C4也会发送读取X的请求,也得到了一个结果。现在的问题是,这两个客户端看到的结果是什么?
===
学生提问:为什么一定要一次只处理一个请求?
Robert教授:这是个好问题。在这里,我假设C1和C2在同一时间发起请求。所以,如果我们在监控网络的话,我们可以看到两个请求同时发往服务器。之后在某个时刻,服务器会响应它们。但是这里没有足够的信息来判断,服务器会以哪种顺序执行这两个写请求。如果服务器先处理了写X为1的请求,那么就意味着它接下来会处理写X为2的请求,所以接下来的读X请求可以看到2。然而,如果服务器先处理了写X为2的请求,再处理写X为1的请求,那么接下来的读X请求看到的就是1。
===
我这里提出这个场景的目的是为了展示,即使在一个非常简单的系统中,仍然会出现一些模糊的场景使得你不知道系统的执行过程以及输出结果。你能做的只是从产生的结果来判断系统的输出是一致性还是非一致性。
如果C3读X得到2,那么C4最好也是读X得到2,因为在我们的例子中,C3读X得到2意味着,写X为2的请求必然是第二个执行的写请求。当C4读X时,写X为2应该仍然是第二个写请求。希望这里完全直观的介绍清楚了有关强一致的一个模型。
当然,这里的问题是,因为只有单个服务器,所以容错能力很差。如果服务器故障了,磁盘坏了,系统整个就不可用了。所以,在现实世界中,我们会构建多副本的分布式系统,但这却又是所有问题的开始。
一个场景为了展示:即使在一个非常简单的系统中,仍然会出现一些模糊的场景使得你不知道系统的执行过程以及输出结果。你能做的只是从产生的结果来判断系统的输出是一致性还是非一致性 --------------------------------- end
糟糕的多副本设计例子 ------------------------------------- start
这里有一个几乎是最糟糕的多副本设计,我提出它是为了让你们知道问题所在,并且同样的问题在GFS中也存在。这个设计是这样,我们有两台服务器,每个服务器都有数据的一份完整拷贝。它们在磁盘上都存储了一个key-value表单。当然,直观上我们希望这两个表单是完全一致的,这样,一台服务器故障了,我们可以切换到另一台服务器去做读写。
两个表单完全一致意味着,每一个写请求都必须在两台服务器上执行,而读请求只需要在一台服务器上执行,否则就没有容错性了。因为如果读请求也需要从两台服务器读数据,那么一台服务器故障我们就没法提供服务了。现在问题来了,假设客户端C1和C2都想执行写请求,其中一个要写X为1,另一个写X为2。C1会将写X为1的请求发送个两个服务器,因为我们想要更新两台服务器上的数据。C2也会将写X为2的请求发送给两个服务器。
这里会出现什么错误呢?是的,我们没有做任何事情来保障两台服务器以相同的顺序处理这2个请求。这个设计真不咋样。如果服务器1(S1)先处理C1的请求,那么在它的表单里面,X先是1,之后S1看到了来自C2的请求,会将自己表单中的X覆盖成2。但是,如果S2恰好以不同的顺序收到客户端请求,那么S2会先执行C2的请求,将X设置为2,之后收到C1的请求,将X设置为1。
之后,如果另外一些客户端,假设C3从S1读数据,C4从S2读数据,我们就会面临一个可怕的场景:这两个客户端读取的数据不一样。但是从前一个例子中的简单模型可以看出,相连的读请求应该读出相同的数据。
这里的问题可以以另一种方式暴露出来。假设我们尝试修复上面的问题,我们让客户端在S1还在线的时候,只从S1读取数据,S1不在线了再从S2读取数据。这样最开始所有的客户端读X都能得到2。但是突然,如果S1故障了,尽管没有写请求将X改成1,客户端读X得到的数据将会从2变成1。因为S1故障之后,所有的客户端都会切换到S2去读数据。这种数据的神奇变化与任何写操作都没有关联,并且也不可能在前一个例子的简单模型中发生。
糟糕的多副本设计例子 ------------------------------------- end
当然,这里的问题是可以修复的,修复需要服务器之间更多的通信,并且复杂度也会提升。由于获取强一致会带来不可避免的复杂性的提升,有大量的方法可以在好的一致性和一些小瑕疵行为之间追求一个平衡。
有大量的方法可以在好的一致性和一些小瑕疵行为之间追求一个平衡。
3.3 GFS的设计目标
让我们来讨论GFS吧。GFS做了很多工作来解决前面提到的问题,虽然不够完美,但是GFS已经做的很好了。
GFS在2003年提出,距今已经有很长一段时间了。实际上,在当时,互联网的规模已经很大了,人们也在构建大型网站,在那之前,人们也对分布式系统做了数十年的研究。所以,在当时,至少在学术领域,人们知道如何构建高度并行且具备容错的分布式系统。不过在当时,很少有在工业界能有应用这些学术界思想的例子。大概从GFS论文发表的时间起,像Google这样的大型网站开始构建严格意义上的分布式系统。这让像我(Robert教授)这样的学术界分子感到非常兴奋,因为终于看到像Google这样的拥有大量数据的公司开始实际使用分布式系统。Google拥有远超过单个磁盘容量的数据,例如从整个互联网爬出来的网页,YouTube视频,用来构建搜索索引的中间文件,Web服务器中的大量日志文件等。所以,Google有大量的数据,需要大量的磁盘来存储这些数据,并且需要能借助MapReduce这样的工具来快速处理这些数据。所以,Google需要能够快速的并行访问这些海量数据。
Google的目标是构建一个大型的,快速的文件系统。并且这个文件系统是全局有效的,这样各种不同的应用程序都可以从中读取数据。一种构建大型存储系统的方法是针对某个特定的应用程序构建特定的裁剪的存储系统。但是如果另一个应用程序也想要一个大型存储系统,那么又需要重新构建一个存储系统。如果有一个全局通用的存储系统,那就意味着如果我存储了大量从互联网抓取的数据,你也可以通过申请权限来查看这些数据,因为我们都使用了同一个存储系统。这样,任何在Google内部的人员都可以根据名字读取这个文件系统(GFS)中可被共享的内容。目标1:共享文件系统
为了获得大容量和高速的特性,每个包含了数据的文件会被GFS自动的分割并存放在多个服务器之上,这样读写操作自然就会变得很快。因为可以从多个服务器上同时读取同一个文件,进而获得更高的聚合吞吐量。将文件分割存储还可以在存储系统中保存比单个磁盘还要大的文件。目标2:大容量、高速(客户端访问离自己近的数据块服务器)
因为我们现在在数百台服务器之上构建存储系统,我们希望有自动的故障修复。我们不希望每次服务器出了故障,派人到机房去修复服务器或者迁移数据。我们希望系统能自动修复自己。目标3:容错
还有一些特征并非是设计目标。比如GFS被设计成只在一个数据中心运行,所以这里并没有将副本保存在世界各地,单个GFS只存在于单个数据中心的单个机房里。理论上来说,数据的多个副本应该彼此之间隔的远一些,但是实现起来挺难的,所以GFS局限在一个数据中心内。
其次,GFS并不面向普通的用户,这是一个Google内部使用的系统,供Google的工程师写应用程序使用。所以Google并没有售卖GFS,它或许售卖了基于GFS的服务,但是GFS并不直接面向普通用户。
第三,GFS在各个方面对大型的顺序文件读写做了定制。在存储系统中有一个完全不同的领域,这个领域只对小份数据进行优化。例如一个银行账户系统就需要一个能够读写100字节的数据库,因为100字节就可以表示人们的银行账户。但是GFS不是这样的系统,GFS是为TB级别的文件而生。并且GFS只会顺序处理,不支持随机访问。某种程度上来说,它有点像批处理的风格。GFS并没有花费过多的精力来降低延迟,它的关注点在于巨大的吞吐量上,所以单次操作都涉及到MB级别的数据。
GFS在各个方面对大型的顺序文件读写做了定制
GFS论文发表在2003年的SOSP会议上,这是一个有关系统的顶级学术会议。通常来说,这种会议的论文标准是需要有大量的创新研究,但是GFS的论文不属于这一类标准。论文中的一些思想在当时都不是特别新颖,比如分布式,分片,容错这些在当时已经知道如何实现了。这篇论文的特点是,它描述了一个真正运行在成百上千台计算机上的系统,这个规模远远超过了学术界建立的系统。并且由于GFS被用于工业界,它反映了现实世界的经验,例如对于一个系统来说,怎样才能正常工作,怎样才能节省成本,这些内容也极其有价值。同时,论文也提出了一个当时非常异类的观点:存储系统具有弱一致性也是可以的。当时,学术界的观念认为,存储系统就应该有良好的行为,如果构建了一个会返回错误数据的系统,就像前面(详见3.2)介绍的糟糕的多副本系统一样,那还有什么意义?为什么不直接构建一个能返回正确数据的系统?GFS并不保证返回正确的数据,借助于这一点,GFS的目标是提供更好的性能。
GFS并不保证返回正确的数据,借助于这一点,GFS的目标是提供更好的性能。
最后,这篇论文还有一个有意思的事情。在一些学术论文中,你或许可以看到一些容错的,多副本,自动修复的多个Master节点共同分担工作,但是GFS却宣称使用单个Master节点并能够很好的工作。
GFS却宣称使用单个Master节点并能够很好的工作。
===
学生提问:如果GFS返回错误的数据,会不会影响应用程序?
Robert教授:讽刺的是。有谁关心网上的投票数量是否正确呢,如果你通过搜索引擎做搜索,20000个搜索结果中丢失了一条或者搜索结果排序是错误的,没有人会注意到这些。这类系统对于错误的接受能力好过类似于银行这样的系统。当然并不意味着所有的网站数据都可以是错误的。如果你通过广告向别人收费,你最好还是保证相应的数字是对的。
部分应用本身不需要太高的正确性
另外,尽管GFS可能会返回错误的数据,但是可以在应用程序中做一些补偿。例如论文中提到,应用程序应当对数据做校验,并明确标记数据的边界,这样应用程序在GFS返回不正确数据时可以恢复。
另外,尽管GFS可能会返回错误的数据,但是可以在应用程序中做一些补偿。例如论文中提到,应用程序应当对数据做校验,并明确标记数据的边界,这样应用程序在GFS返回不正确数据时可以恢复。
===
3.4 GFS Master 节点
接下来看看GFS的大致架构,这在论文的图1中也有介绍。
假设我们有上百个客户端和一个Master节点。尽管实际中可以拿多台机器作为Master节点,但是GFS中Master是Active-Standby模式,所以只有一个Master节点在工作。Master节点保存了文件名和存储位置的对应关系。除此之外,还有大量的Chunk服务器,可能会有数百个,每一个Chunk服务器上都有1-2块磁盘。
Master节点保存了文件名和存储位置的对应关系。除此之外,还有大量的Chunk服务器,可能会有数百个,每一个Chunk服务器上都有1-2块磁盘。
在这里,Master节点用来管理文件和Chunk的信息,而Chunk服务器用来存储实际的数据。这是GFS设计中比较好的一面,它将这两类数据的管理问题几乎完全隔离开了,这样这两个问题可以使用独立设计来解决。Master节点知道每一个文件对应的所有的Chunk的ID,这些Chunk每个是64MB大小,它们共同构成了一个文件。如果我有一个1GB的文件,那么Master节点就知道文件的第一个Chunk存储在哪,第二个Chunk存储在哪,等等。当我想读取这个文件中的任意一个部分时,我需要向Master节点查询对应的Chunk在哪个服务器上,之后我可以直接从Chunk服务器读取对应的Chunk数据。
Master节点用来管理文件和Chunk的信息,而Chunk服务器用来存储实际的数据。Master节点知道每一个文件对应的所有的Chunk的ID,这些Chunk每个是64MB大小,它们共同构成了一个文件。
更进一步,我们看一下GFS的一致性以及GFS是如何处理故障。为了了解这些,我们需要知道Master节点内保存的数据内容,这里我们关心的主要是两个表单:
Master 节点保存了两个表单,1: 文件名到 Chunk ID 的对应 2: Chunk ID 到 Chunk 数据的对应
以上数据都存储在内存中,如果Master故障了,这些数据就都丢失了。为了能让Master重启而不丢失数据,Master节点会同时将数据存储在磁盘上。所以Master节点读数据只会从内存读,但是写数据的时候,至少有一部分数据会接入到磁盘中。更具体来说,Master会在磁盘上存储log,每次有数据变更时,Master会在磁盘的log中追加一条记录,并生成CheckPoint(类似于备份点)。
这里介绍了单个 Master 的容错机制:Checkpoint
有些数据需要存在磁盘上,而有些不用。它们分别是:
介绍了不用保存在磁盘上的数据
任何时候,如果文件扩展到达了一个新的64MB,需要新增一个Chunk或者由于指定了新的主Chunk而导致版本号更新了,Master节点需要向磁盘中的Log追加一条记录说,我刚刚向这个文件添加了一个新的Chunk或者我刚刚修改了Chunk的版本号。所以每次有这样的更新,都需要写磁盘。GFS论文并没有讨论这么多细节,但是因为写磁盘的速度是有限的,写磁盘会导致Master节点的更新速度也是有限的,所以要尽可能少的写入数据到磁盘。
这里在磁盘中维护log而不是数据库的原因是,数据库本质上来说是某种B树(b-tree)或者hash table,相比之下,追加log会非常的高效,因为你可以将最近的多个log记录一次性的写入磁盘。因为这些数据都是向同一个地址追加,这样只需要等待磁盘的磁碟旋转一次。而对于B树来说,每一份数据都需要在磁盘中随机找个位置写入。所以使用Log可以使得磁盘写入更快一些。
这里在磁盘中维护log而不是数据库的原因是,数据库本质上来说是某种B树(b-tree)或者hash table,相比之下,追加log会非常的高效,因为你可以将最近的多个log记录一次性的写入磁盘。因为这些数据都是向同一个地址追加,这样只需要等待磁盘的磁碟旋转一次。而对于B树来说,每一份数据都需要在磁盘中随机找个位置写入。所以使用Log可以使得磁盘写入更快一些。
当Master节点故障重启,并重建它的状态,你不会想要从log的最开始重建状态,因为log的最开始可能是几年之前,所以Master节点会在磁盘中创建一些checkpoint点,这可能要花费几秒甚至一分钟。这样Master节点重启时,会从log中的最近一个checkpoint开始恢复,再逐条执行从Checkpoint开始的log,最后恢复自己的状态。
当Master节点故障重启,并重建它的状态,你不会想要从log的最开始重建状态,因为log的最开始可能是几年之前,所以Master节点会在磁盘中创建一些checkpoint点,这可能要花费几秒甚至一分钟。这样Master节点重启时,会从log中的最近一个checkpoint开始恢复,再逐条执行从Checkpoint开始的log,最后恢复自己的状态。
3.5 GFS读文件(Read File)
有了之前的基础,我接下来会列出GFS读和写的步骤,最后,我会介绍出现故障之后,系统是如何保持正确的行为。
对于读请求来说,意味着应用程序或者GFS客户端有一个文件名和它想从文件的某个位置读取的偏移量(offset),应用程序会将这些信息发送给Master节点。Master节点会从自己的file表单中查询文件名,得到Chunk ID的数组。因为每个Chunk是64MB,所以偏移量除以64MB就可以从数组中得到对应的Chunk ID。之后Master再从Chunk表单中找到存有Chunk的服务器列表,并将列表返回给客户端。所以,第一步是客户端(或者应用程序)将文件名和偏移量发送给Master。第二步,Master节点将Chunk Handle(也就是ID,记为H)和服务器列表发送给客户端。
这里介绍GFS读文件的工作流程
现在客户端可以从这些Chunk服务器中挑选一个来读取数据。GFS论文说,客户端会选择一个网络上最近的服务器(Google的数据中心中,IP地址是连续的,所以可以从IP地址的差异判断网络位置的远近),并将读请求发送到那个服务器。因为客户端每次可能只读取1MB或者64KB数据,所以,客户端可能会连续多次读取同一个Chunk的不同位置。所以,客户端会缓存Chunk和服务器的对应关系,这样,当再次读取相同Chunk数据时,就不用一次次的去向Master请求相同的信息。
客户端会缓存Chunk和服务器的对应关系
接下来,客户端会与选出的Chunk服务器通信,将Chunk Handle和偏移量发送给那个Chunk服务器。Chunk服务器会在本地的硬盘上,将每个Chunk存储成独立的Linux文件,并通过普通的Linux文件系统管理。并且可以推测,Chunk文件会按照Handle(也就是ID)命名。所以,Chunk服务器需要做的就是根据文件名找到对应的Chunk文件,之后从文件中读取对应的数据段,并将数据返回给客户端。
Chunk 服务器查找数据段并返回的工作流程
===
学生提问:可以再讲一下第一步吗?
Robert教授:第一步是,应用程序想读取某个特定文件的某个特定的偏移位置上的某段特定长度的数据,比如说第1000到第2000个字节的数据。所以,应用程序将文件名,长度和起始位置发送给Master节点。Master节点会从其file表单中查询文件名并找到包含这个数据段的Chunk,这样可以吗?
学生提问:如果读取的数据超过了一个Chunk怎么办?
Robert教授:我不知道详细的细节。我的印象是,如果应用程序想要读取超过64MB的数据,或者就是2个字节,但是却跨越了Chunk的边界,应用程序会通过一个库来向GFS发送RPC,而这个库会注意到这次读请求会跨越Chunk边界,因此会将一个读请求拆分成两个读请求再发送到Master节点。所以,这里可能是向Master节点发送两次读请求,得到了两个结果,之后再向两个不同的Chunk服务器读取数据。
学生提问:如果客户端有偏移量信息,那可以直接算出来是第几个Chunk吧?
Robert教授:客户端可以算出来是哪个Chunk,但是客户端不知道Chunk在哪个服务器上。为了获取服务器信息,客户端需要与Master交互。我不能保证究竟是在哪里决定的要读取的是第几个Chunk。但是可以确定的是,Master节点找到了Chunk对应的ID,并确定了Chunk存储在哪个服务器上。
学生提问:能再介绍一下读数据跨越了Chunk边界的情况吗?
Robert教授:客户端本身依赖了一个GFS的库,这个库会注意到读请求跨越了Chunk的边界 ,并会将读请求拆分,之后再将它们合并起来。所以这个库会与Master节点交互,Master节点会告诉这个库说Chunk7在这个服务器,Chunk8在那个服务器。之后这个库会说,我需要Chunk7的最后两个字节,Chunk8的头两个字节。GFS库获取到这些数据之后,会将它们放在一个buffer中,再返回给调用库的应用程序。Master节点会告诉库有关Chunk的信息,而GFS库可以根据这个信息找到应用程序想要的数据。应用程序只需要确定文件名和数据在整个文件中的偏移量,GFS库和Master节点共同协商将这些信息转换成Chunk。
学生提问:从哪个Chunk服务器读取数据重要吗?
Robert教授:是也不是。概念上讲,它们都是副本。实际上,你可能已经注意到,或者我们之前也说过,不同Chunk服务器上的数据并不一定完全相同。应用程序应该要能够容忍这种情况。所以,实际上,如果从不同的Chunk服务器读取数据,可能会略微不同。GFS论文提到,客户端会尝试从同一个机架或者同一个交换机上的服务器读取数据。
GFS 从不同 Chunk 读取的数据(副本)不一定完全相同
===
3.6 GFS写文件(Write File)(1)
GFS写文件的过程会更加复杂且有趣。从应用程序的角度来看,写文件和读文件的接口是非常类似的,它们都是调用GFS的库。写文件是,应用程序会告诉库函数说,我想对这个文件名的文件在这个数据段写入当前存在buffer中的数据。让我(Robert教授)稍微收敛一下,我只想讨论数据的追加。所以我会限制这里的客户端接口,客户端(也就是应用程序)只能说,我想把buffer中的数据,追加到这个文件名对应的文件中。这就是GFS论文中讨论的记录追加(Record Append)。所以,再次描述一下,对于写文件,客户端会向Master节点发送请求说:我想向这个文件名对应的文件追加数据,请告诉我文件中最后一个Chunk的位置。
GFS 写文件流程(仅讨论追加)
当有多个客户端同时写同一个文件时,一个客户端并不能知道文件究竟有多长。因为如果只有一个客户端在写文件,客户端自己可以记录文件长度,而多个客户端时,一个客户端没法知道其他客户端写了多少。例如,不同客户端写同一份日志文件,没有一个客户端会知道文件究竟有多长,因此也就不知道该往什么样的偏移量,或者说向哪个Chunk去追加数据。这个时候,客户端可以向Master节点查询哪个Chunk服务器保存了文件的最后一个Chunk。
当有多个客户端写同一个文件时,客户端并不能知道文件究竟有多长,因为一个客户端没法知道其他客户端写了多少。
对于读文件来说,可以从任何最新的Chunk副本读取数据,但是对于写文件来说,必须要通过Chunk的主副本(Primary Chunk)来写入。对于某个特定的Chunk来说,在某一个时间点,Master不一定指定了Chunk的主副本。所以,写文件的时候,需要考虑Chunk的主副本不存在的情况。
对于Master节点来说,如果发现Chunk的主副本不存在,Master会找出所有存有Chunk最新副本的Chunk服务器。如果你的一个系统已经运行了很长时间,那么有可能某一个Chunk服务器保存的Chunk副本是旧的,比如说还是昨天或者上周的。导致这个现象的原因可能是服务器因为宕机而没有收到任何的更新。所以,Master节点需要能够在Chunk的多个副本中识别出,哪些副本是新的,哪些是旧的。所以第一步是,找出新的Chunk副本。这一切都是在Master节点发生,因为,现在是客户端告诉Master节点说要追加某个文件,Master节点需要告诉客户端向哪个Chunk服务器(也就是Primary Chunk所在的服务器)去做追加操作。所以,Master节点的部分工作就是弄清楚在追加文件时,客户端应该与哪个Chunk服务器通信。
写文件的时候,需要考虑Chunk的主副本不存在的情况。Master节点需要能够在Chunk的多个副本中识别出,哪些副本是新的,哪些是旧的。所以第一步是,找出新的Chunk副本。
每个Chunk可能同时有多个副本,最新的副本是指,副本中保存的版本号与Master中记录的Chunk的版本号一致。Chunk副本中的版本号是由Master节点下发的,所以Master节点知道,对于一个特定的Chunk,哪个版本号是最新的。这就是为什么Chunk的版本号在Master节点上需要保存在磁盘这种非易失的存储中的原因(见3.4),因为如果版本号在故障重启中丢失,且部分Chunk服务器持有旧的Chunk副本,这时,Master是没有办法区分哪个Chunk服务器的数据是旧的,哪个Chunk服务器的数据是最新的。
每个Chunk可能同时有多个副本,最新的副本是指,副本中保存的版本号与Master中记录的Chunk的版本号一致。Chunk副本中的版本号是由Master节点下发的,所以Master节点知道,对于一个特定的Chunk,哪个版本号是最新的。
===
学生提问:为什么不将所有Chunk服务器上保存的最大版本号作为Chunk的最新版本号?
Robert教授:当Master重启时,无论如何都需要与所有的Chunk服务器进行通信,因为Master需要确定哪个Chunk服务器存了哪个Chunk。你可能会想到,Master可以将所有Chunk服务器上的Chunk版本号汇总,找出里面的最大值作为最新的版本号。如果所有持有Chunk的服务器都响应了,那么这种方法是没有问题的。但是存在一种风险,当Master节点重启时,可能部分Chunk服务器离线或者失联或者自己也在重启,从而不能响应Master节点的请求。所以,Master节点可能只能获取到持有旧副本的Chunk服务器的响应,而持有最新副本的Chunk服务器还没有完成重启,或者还是离线状态(这个时候Master能找到的Chunk最大版本明显不对)。
当Master找不到持有最新Chunk的服务器时该怎么办?Master节点会定期与Chunk服务器交互来查询它们持有什么样版本的Chunk。假设Master保存的Chunk版本是17,但是又没有找到存储了版本号是17的Chunk服务器,那么有两种可能:要么Master会等待,并不响应客户端的请求;要么会返回给客户端说,我现在还不知道Chunk在哪,过会再重试吧。比如说机房电源故障了,所有的服务器都崩溃了,我们正在缓慢的重启。Master节点和一些Chunk服务器可能可以先启动起来,一些Chunk服务器可能要5分钟以后才能重启,这种场景下,我们需要等待,甚至可能是永远等待,因为你不会想使用Chunk的旧数据。
当Master找不到持有最新Chunk的服务器时该怎么办?Master节点会定期与Chunk服务器交互来查询它们持有什么样版本的Chunk。若找不到,那么有两种可能:要么Master会等待,并不响应客户端的请求;要么会返回给客户端说,我现在还不知道Chunk在哪,过会再重试吧。
所以,总的来说,在重启时,因为Master从磁盘存储的数据知道Chunk对应的最新版本,Master节点会整合具有最新版本Chunk的服务器。每个Chunk服务器会记住本地存储Chunk对应的版本号,当Chunk服务器向Master汇报时,就可以说,我有这个Chunk的这个版本。而Master节点就可以忽略哪些版本号与已知版本不匹配的Chunk服务器。
重启时,Master节点会整合具有最新版本Chunk的服务器。
===
回到之前的话题,当客户端想要对文件进行追加,但是又不知道文件尾的Chunk对应的Primary在哪时,Master会等所有存储了最新Chunk版本的服务器集合完成,然后挑选一个作为Primary,其他的作为Secondary。
回到之前的话题,当客户端想要对文件进行追加,但是又不知道文件尾的Chunk对应的Primary在哪时,Master会等所有存储了最新Chunk版本的服务器集合完成,然后挑选一个作为Primary,其他的作为Secondary。
之后,Master会增加版本号,并将版本号写入磁盘,这样就算故障了也不会丢失这个数据。
接下来,Master节点会向Primary和Secondary副本对应的服务器发送消息并告诉它们,谁是Primary,谁是Secondary,Chunk的新版本是什么。Primary和Secondary服务器都会将版本号存储在本地的磁盘中。这样,当它们因为电源故障或者其他原因重启时,它们可以向Master报告本地保存的Chunk的实际版本号。
Master 重启时挑选主、次副本的逻辑
===
学生提问:如果Chunk服务器上报的版本号高于Master存储的版本号会怎么样?
Robert教授:这个问题很好。我不知道答案,不过论文中有一些线索。其实刚刚我的介绍有一些错误,我认为你的问题让我明白了一些事情。GFS论文说,如果Master节点重启,并且与Chunk服务器交互,同时一个Chunk服务器重启,并上报了一个比Master记住的版本更高的版本。Master会认为它在分配新的Primary服务器时出现了错误,并且会使用这个更高的版本号来作为Chunk的最新版本号。
GFS论文说,如果Master节点重启,并且与Chunk服务器交互,同时一个Chunk服务器重启,并上报了一个比Master记住的版本更高的版本。Master会认为它在分配新的Primary服务器时出现了错误,并且会使用这个更高的版本号来作为Chunk的最新版本号。
当Master向Primary和Secondary发送完消息之后就崩溃了,可能会出现上面这种情况。为了让Master能够处理这种情况,Master在发送完消息之后,需要将Chunk的最新版本写入到磁盘中。这里的写入或许需要等到Primary和Secondary返回确认消息之后。
当Master向Primary和Secondary发送完消息之后就崩溃了。。。。
学生提问:听不清(但是应该与这一节的第一个问题一样)。
Robert教授:我不认为这行得通。因为存在这种可能性,当Master节点重启时,存储了Chunk最新版本号的Chunk服务器是离线状态。这种情况下,我们不希望Master在重启时不知道当前的版本号,因为那样的话,Master就会认为当前发现的最高版本号是当前版本号,但是(由于有最新版本号的Chunk服务器还是离线状态)发现的最高版本号可能是个旧版本号。
我(Robert教授)之前没太关注这块,所以我也不太确定Master究竟是先写本地磁盘中的版本号,然后再通知Primary和Secondary,还是反过来。但是不管怎么样,Master会更新自己的版本号,并通知Primary和Secondary说,你们现在是Primary和Secondary,并且版本号更新了。
===
TODO: 后面的内容逐渐听不懂了,先做 LAB 吧
所以,现在我们有了一个Primary,它可以接收来自客户端的写请求,并将写请求应用在多个Chunk服务器中。之所以要管理Chunk的版本号,是因为这样Master可以将实际更新Chunk的能力转移给Primary服务器。并且在将版本号更新到Primary和Secondary服务器之后,如果Master节点故障重启,还是可以在相同的Primary和Secondary服务器上继续更新Chunk。
现在,Master节点通知Primary和Secondary服务器,你们可以修改这个Chunk。它还给Primary一个租约,这个租约告诉Primary说,在接下来的60秒中,你将是Primary,60秒之后你必须停止成为Primary。这种机制可以确保我们不会同时有两个Primary,我们之后会再做讨论(3.7的问答中有一个专门的问题讨论)。
我们现在来看GFS论文的图2。假设现在Master节点告诉客户端谁是Primary,谁是Secondary,GFS提出了一种聪明的方法来实现写请求的执行序列。客户端会将要追加的数据发送给Primary和Secondary服务器,这些服务器会将数据写入到一个临时位置。所以最开始,这些数据不会追加到文件中。当所有的服务器都返回确认消息说,已经有了要追加的数据,客户端会向Primary服务器发送一条消息说,你和所有的Secondary服务器都有了要追加的数据,现在我想将这个数据追加到这个文件中。Primary服务器或许会从大量客户端收到大量的并发请求,Primary服务器会以某种顺序,一次只执行一个请求。对于每个客户端的追加数据请求(也就是写请求),Primary会查看当前文件结尾的Chunk,并确保Chunk中有足够的剩余空间,然后将客户端要追加的数据写入Chunk的末尾。并且,Primary会通知所有的Secondary服务器也将客户端要追加的数据写入在它们自己存储的Chunk末尾。这样,包括Primary在内的所有副本,都会收到通知将数据追加在Chunk的末尾。
但是对于Secondary服务器来说,它们可能可以执行成功,也可能会执行失败,比如说磁盘空间不足,比如说故障了,比如说Primary发出的消息网络丢包了。如果Secondary实际真的将数据写入到了本地磁盘存储的Chunk中,它会回复“yes”给Primary。如果所有的Secondary服务器都成功将数据写入,并将“yes”回复给了Primary,并且Primary也收到了这些回复。Primary会向客户端返回写入成功。如果至少一个Secondary服务器没有回复Primary,或者回复了,但是内容却是:抱歉,一些不好的事情发生了,比如说磁盘空间不够,或者磁盘故障了,Primary会向客户端返回写入失败。
GFS论文说,如果客户端从Primary得到写入失败,那么客户端应该重新发起整个追加过程。客户端首先会重新与Master交互,找到文件末尾的Chunk;之后,客户端需要重新发起对于Primary和Secondary的数据追加操作。
3.7 GFS写文件(Write File)(2)
这一部分主要是对写文件操作的问答。
===
学生提问:写文件失败之后Primary和Secondary服务器上的状态如何恢复?
Robert教授:你的问题是,Primary告诉所有的副本去执行数据追加操作,某些成功了,某些没成功。如果某些副本没有成功执行,Primary会回复客户端说执行失败。之后客户端会认为数据没有追加成功。但是实际上,部分副本还是成功将数据追加了。所以现在,一个Chunk的部分副本成功完成了数据追加,而另一部分没有成功,这种状态是可接受的,没有什么需要恢复,这就是GFS的工作方式。
学生提问:写文件失败之后,读Chunk数据会有什么不同?
Robert教授:如果写文件失败之后,一个客户端读取相同的Chunk,客户端可能可以读到追加的数据,也可能读不到,取决于客户端读的是Chunk的哪个副本。
如果一个客户端发送写文件的请求,并且得到了表示成功的回复,那意味着所有的副本都在相同的位置追加了数据。如果客户端收到了表示失败的回复,那么意味着0到多个副本实际追加了数据,其他的副本没有追加上数据。所以这时,有些副本会有追加的数据,有些副本没有。这时,取决于你从哪个副本读数据,有可能读到追加的新数据,也有可能读不到。
学生提问:可不可以通过版本号来判断副本是否有之前追加的数据?
Robert教授:所有的Secondary都有相同的版本号。版本号只会在Master指定一个新Primary时才会改变。通常只有在原Primary发生故障了,才会指定一个新的Primary。所以,副本(参与写操作的Primary和Secondary)都有相同的版本号,你没法通过版本号来判断它们是否一样,或许它们就是不一样的(取决于数据追加成功与否)。
这么做的理由是,当Primary回复“no”给客户端时,客户端知道写入失败了,之后客户端的GFS库会重新发起追加数据的请求,直到最后成功追加数据。成功了之后,追加的数据会在所有的副本中相同位置存在。在那之前,追加的数据只会在部分副本中存在。
学生提问:客户端将数据拷贝给多个副本会不会造成瓶颈?
Robert教授:这是一个好问题。考虑到底层网络,写入文件数据的具体传输路径可能会非常重要。当论文第一次提到这一点时,它说客户端会将数据发送给每个副本。实际上,之后,论文又改变了说法,说客户端只会将数据发送给离它最近的副本,之后那个副本会将数据转发到另一个副本,以此类推形成一条链,直到所有的副本都有了数据。这样一条数据传输链可以在数据中心内减少跨交换机传输(否则,所有的数据吞吐都在客户端所在的交换机上)。
学生提问:什么时候版本号会增加?
Robert教授:版本号只在Master节点认为Chunk没有Primary时才会增加。在一个正常的流程中,如果对于一个Chunk来说,已经存在了Primary,那么Master节点会记住已经有一个Primary和一些Secondary,Master不会重新选择Primary,也不会增加版本号。它只会告诉客户端说这是Primary,并不会变更版本号。
学生提问:如果写入数据失败了,不是应该先找到问题在哪再重试吗?
Robert教授:我认为这是个有趣的问题。当Primary向客户端返回写入失败时,你或许会认为一定是哪里出错了,在修复之前不应该重试。实际上,就我所知,论文里面在重试追加数据之前没有任何中间操作。因为,错误可能就是网络数据的丢失,这时就没什么好修复的,网络数据丢失了,我们应该重传这条网络数据。客户端重新尝试追加数据可以看做是一种复杂的重传数据的方法。或许对于大多数的错误来说,我们不需要修改任何东西,同样的Primary,同样的Secondary,客户端重试一次或许就能正常工作,因为这次网络没有丢包。
但是如果是某一个Secondary服务器出现严重的故障,那问题变得有意思了。我们希望的是,Master节点能够重新生成Chunk对应的服务器列表,将不工作的Secondary服务器剔除,再选择一个新的Primary,并增加版本号。如果这样的话,我们就有了一组新的Primary,Secondary和版本号,同时,我们还有一个不太健康的Secondary,它包含的是旧的副本和旧的版本号,正是因为版本号是旧的,Master永远也不会认为它拥有新的数据。但是,论文中没有证据证明这些会立即发生。论文里只是说,客户端重试,并且期望之后能正常工作。最终,Master节点会ping所有的Chunk服务器,如果Secondary服务器挂了,Master节点可以发现并更新Primary和Secondary的集合,之后再增加版本号。但是这些都是之后才会发生(而不是立即发生)。
学生提问:如果Master节点发现Primary挂了会怎么办?
Robert教授:可以这么回答这个问题。在某个时间点,Master指定了一个Primary,之后Master会一直通过定期的ping来检查它是否还存活。因为如果它挂了,Master需要选择一个新的Primary。Master发送了一些ping给Primary,并且Primary没有回应,你可能会认为Master会在那个时间立刻指定一个新的Primary。但事实是,这是一个错误的想法。为什么是一个错误的想法呢?因为可能是网络的原因导致ping没有成功,所以有可能Primary还活着,但是网络的原因导致ping失败了。但同时,Primary还可以与客户端交互,如果Master为Chunk指定了一个新的Primary,那么就会同时有两个Primary处理写请求,这两个Primary不知道彼此的存在,会分别处理不同的写请求,最终会导致有两个不同的数据拷贝。这被称为脑裂(split-brain)。
脑裂是一种非常重要的概念,我们会在之后的课程中再次介绍它(详见6.1),它通常是由网络分区引起的。比如说,Master无法与Primary通信,但是Primary又可以与客户端通信,这就是一种网络分区问题。网络故障是这类分布式存储系统中最难处理的问题之一。
所以,我们想要避免错误的为同一个Chunk指定两个Primary的可能性。Master采取的方式是,当指定一个Primary时,为它分配一个租约,Primary只在租约内有效。Master和Primary都会知道并记住租约有多长,当租约过期了,Primary会停止响应客户端请求,它会忽略或者拒绝客户端请求。因此,如果Master不能与Primary通信,并且想要指定一个新的Primary时,Master会等到前一个Primary的租约到期。这意味着,Master什么也不会做,只是等待租约到期。租约到期之后,可以确保旧的Primary停止了它的角色,这时Master可以安全的指定一个新的Primary而不用担心出现这种可怕的脑裂的情况。
学生提问:为什么立即指定一个新的Primary是坏的设计?既然客户端总是先询问Master节点,Master指定完Primary之后,将新的Primary返回给客户端不行吗?
Robert教授:因为客户端会通过缓存提高效率,客户端会在短时间缓存Primary的身份信息(这样,客户端就不用每次都会向Master请求Primary信息)。即使没有缓存,也可能出现这种情况,客户端向Master节点查询Primary信息,Master会将Primary信息返回,这条消息在网络中传播。之后Master如果发现Primary出现故障,并且立刻指定一个新的Primary,同时向新的Primary发消息说,你是Primary。Master节点之后会向其他查询Primary的客户端返回这个新的Primary。而前一个Primary的查询还在传递过程中,前一个客户端收到的还是旧的Primary的信息。如果没有其他的更聪明的一些机制,前一个客户端是没办法知道收到的Primary已经过时了。如果前一个客户端执行写文件,那么就会与后来的客户端产生两个冲突的副本。
学生提问:如果是对一个新的文件进行追加,那这个新的文件没有副本,会怎样?
Robert教授:你会按照黑板上的路径(见3.6)再执行一遍。Master会从客户端收到一个请求说,我想向这个文件追加数据。我猜,Master节点会发现,该文件没有关联的Chunk。Master节点或许会通过随机数生成器创造一个新的Chunk ID。之后,Master节点通过查看自己的Chunk表单发现,自己其实也没有Chunk ID对应的任何信息。之后,Master节点会创建一条新的Chunk记录说,我要创建一个新的版本号为1,再随机选择一个Primary和一组Secondary并告诉它们,你们将对这个空的Chunk负责,请开始工作。论文里说,每个Chunk默认会有三个副本,所以,通常来说是一个Primary和两个Secondary。
===
3.8 GFS的一致性
或许这里最重要的部分就是重复我们刚刚(3.7的问答中)讨论过的内容。
当我们追加数据时,面对Chunk的三个副本,当客户端发送了一个追加数据的请求,要将数据A追加到文件末尾,所有的三个副本,包括一个Primary和两个Secondary,都成功的将数据追加到了Chunk,所以Chunk中的第一个记录是A。
假设第二个客户端加入进来,想要追加数据B,但是由于网络问题发送给某个副本的消息丢失了。所以,追加数据B的消息只被两个副本收到,一个是Primary,一个是Secondary。这两个副本都在文件中追加了数据B,所以,现在我们有两个副本有数据B,另一个没有。
之后,第三个客户端想要追加数据C,并且第三个客户端记得下图中左边第一个副本是Primary。Primary选择了偏移量,并将偏移量告诉Secondary,将数据C写在Chunk的这个位置。三个副本都将数据C写在这个位置。
对于数据B来说,客户端会收到写入失败的回复,客户端会重发写入数据B的请求。所以,第二个客户端会再次请求追加数据B,或许这次数据没有在网络中丢包,并且所有的三个副本都成功追加了数据B。现在三个副本都在线,并且都有最新的版本号。
之后,如果一个客户端读文件,读到的内容取决于读取的是Chunk的哪个副本。客户端总共可以看到三条数据,但是取决于不同的副本,读取数据的顺序是不一样的。如果读取的是第一个副本,那么客户端可以读到A、B、C,然后是一个重复的B。如果读取的是第三个副本,那么客户端可以读到A,一个空白数据,然后是C、B。所以,如果读取前两个副本,B和C的顺序是先B后C,如果读的是第三个副本,B和C的顺序是先C后B。所以,不同的读请求可能得到不同的结果。
或许最坏的情况是,一些客户端写文件时,因为其中一个Secondary未能成功执行数据追加操作,客户端从Primary收到写入失败的回复。在客户端重新发送写文件请求之前,客户端就故障了。所以,你有可能进入这种情形:数据D出现在某些副本中,而其他副本则完全没有。
在GFS的这种工作方式下,如果Primary返回写入成功,那么一切都还好,如果Primary返回写入失败,就不是那么好了。Primary返回写入失败会导致不同的副本有完全不同的数据。
===
学生提问:客户端重新发起写入的请求时从哪一步开始重新执行的?
Robert教授:根据我从论文中读到的内容,(当写入失败,客户端重新发起写入数据请求时)客户端会从整个流程的最开始重发。客户端会再次向Master询问文件最后一个Chunk是什么,因为文件可能因为其他客户端的数据追加而发生了改变。
学生提问:为什么GFS要设计成多个副本不一致?
Robert教授:我不明白GFS设计者为什么要这么做。GFS可以设计成多个副本是完全精确同步的,你们在lab2和lab3会设计一个系统,其中的副本是同步的。并且你们也会知道,为了保持同步,你们要使用各种各样的技术。如果你们想要让副本保持同步,其中一条规则就是你们不能允许这种只更新部分服务器的不完整操作。这意味着,你必须要有某种机制,即使客户端挂了,系统仍然会完成请求。如果这样的话,GFS中的Primary就必须确保每一个副本都得到每一条消息。
学生提问:如果第一次写B失败了,C应该在B的位置吧?
Robert教授:实际上并没有。实际上,Primary将C添加到了Chunk的末尾,在B第一次写入的位置之后。我认为这么做的原因是,当写C的请求发送过来时,Primary实际上可能不知道B的命运是什么。因为我们面对的是多个客户端并发提交追加数据的请求,为了获得高性能,你会希望Primary先执行追加数据B的请求,一旦获取了下一个偏移量,再通知所有的副本执行追加数据C的请求,这样所有的事情就可以并行的发生。
也可以减慢速度,Primary也可以判断B已经写入失败了,然后再发一轮消息让所有副本撤销数据B的写操作,但是这样更复杂也更慢。
===
GFS这样设计的理由是足够的简单,但是同时也给应用程序暴露了一些奇怪的数据。这里希望为应用程序提供一个相对简单的写入接口,但应用程序需要容忍读取数据的乱序。如果应用程序不能容忍乱序,应用程序要么可以通过在文件中写入序列号,这样读取的时候能自己识别顺序,要么如果应用程序对顺序真的非常敏感那么对于特定的文件不要并发写入。例如,对于电影文件,你不会想要将数据弄乱,当你将电影写入文件时,你可以只用一个客户端连续顺序而不是并发的将数据追加到文件中。
有人会问,如何将这里的设计转变成强一致的系统,从而与我们前面介绍的单服务器模型更接近,也不会产生一些给人“惊喜”的结果。实际上我不知道怎么做,因为这需要完全全新的设计。目前还不清楚如何将GFS转变成强一致的设计。但是,如果你想要将GFS升级成强一致系统,我可以为你列举一些你需要考虑的事情:
为了实现强一致,以上就是我认为的需要在系统中修复的东西,它们增加了系统的复杂度,增加了系统内部组件的交互。我也是通过思考课程的实验,得到上面的列表的,你们会在lab2,3中建立一个强一致系统,并完成所有我刚刚说所有的东西。
最后,让我花一分钟来介绍GFS在它生涯的前5-10年在Google的出色表现,总的来说,它取得了巨大的成功,许多许多Google的应用都使用了它,许多Google的基础架构,例如BigTable和MapReduce是构建在GFS之上,所以GFS在Google内部广泛被应用。它最严重的局限可能在于,它只有一个Master节点,会带来以下问题:
(所以我们才需要多副本,多活,高可用,故障自修复的分布式系统啊)