Lecture3:GFS (MIT6.824)

笔者的个人理解以注释的方式书写。

这门课程的主要内容是“大型存储”,GFS是这门课里有关如何构建大型存储系统的众多案例学习的第一篇。存储是一种关键的抽象,很多系统要么是设计的简单易用的存储接口,要么是基于底层存储进而构建。在分布式系统中,可能有各种各样重要的抽象可以应用在分布式系统中,但是实际上,简单的存储接口往往非常有用且极其通用。构建分布式系统大多都是关于如何设计存储系统,或是设计其它基于大型分布式存储的系统。因此我们会更加关注如何为大型分布式存储系统设计一个优秀的接口,以及如何设计优秀的存储系统的内部结构。

感觉就是分布式存储是很多分布式的底层,因此一个优秀、简单、抽象的接口和优秀的存储内部结构就很重要了。这里一方面是说明存储的重要性,另一方面值得关注的是接口和存储内部结构,这两点都很重要。

3.1 Why HARD

为什么分布式存储系统会如此之难,以至于你需要做大量的工作才能让它正确工作?

  • Performance --> Sharding
  • Fault --> Tolerance
  • Tolerance --> Replication
  • Repl --> In Consistency
  • Consistency --> Low Performance

人们设计大型分布式系统或大型存储系统出发点通常是想获取巨大的性能加成,通过利用数百台计算机的资源来同时完成大量工作。因此,性能performance问题就成为了最初的诉求。 之后,很自然的想法就是将数据分割放到大量的服务器上,这样就可以并行的从多台服务器读取数据。我们将这种方式称之为分片(Sharding)。
如果你在成百上千台服务器进行分片,你将会看见常态的故障。数千台服务器里每天会有一台服务器宕机,甚至每个小时都可能会发生错误。因此需要自动化的方法而不是人工介入来修复错误。每天都有大量错误发生,需要自动化的容错即fault tolerance
实现容错最有用的一种方法是使用复制,只需要维护2-3个数据的副本,当其中一个故障了,你就可以使用另一个。如果想要容错能力就得有复制(replication)。
如果有复制,那就有了两份以上的数据副本,而且很容易不一致。本来想有了多个副本,就能通过任一实现容错,现在发现副本间数据很难完全一致(严格来说,它们就不再互为副本了)。这时候向不同副本请求,获取的数据也就不一样了。因此复制又带来了不一致的问题inconsistency。
通过合理的设计可以避免不一致的问题,并且让数据看起来也表现的符合预期。但这样需要额外的工作,需要服务器间额外的网络交互,这样又导致性能降低。因此为了获得一致性,又需要导致性能降低。
实际设计系统时,这些问题都难以避免,因此就要在性能和一致性之间权衡,并为一致性的保障做出性能上的让步,否则系统就会出现异常问题。

3.2 Strong Consistency

**对于具备强一致 strong consistency或者好的一致性 good consistency的系统,从应用程序或者客户端看起来就像是和一台服务器在通信。**尽管我们会通过数百台计算机构建一个系统,但是对于一个理想的强一致模型,你看到的就像是只有一台服务器,一份数据,并且系统一次只做一件事情。可以认为完全就是单个服务器上的单线程,同一时间只处理来自客户端的一个请求。
举例来说:(一个强一致性系统的示例)
一个只支持put, get的 key-value 系统。为了让服务有可预期的行为,需要定义一条规则:**一个时间只执行一条请求。这样每个请求都可以看到之前所有请求按照顺序执行生成的数据。**如果我们有一些客户端,客户端C1发起写请求将X设置成1;在同一时刻,客户端C2发起写请求将X设置成2。在C1和C2的写请求都执行完毕之后,客户端C3会发送读取X的请求,并得到了一个结果。客户端C4也会发送读取X的请求,也得到了一个结果。现在的问题是,这两个客户端看到的结果是什么?
学生提问:为什么一定要一次只处理一个请求?
Robert教授:假设C1和C2在同一时间发起请求,之后在某个时刻,服务器会响应它们。但是无法判断两个请求处理的先后顺序,可能先处理C1后处理C2,这样最终结果是2;反之则是1。这个例子也说明,即使非常简单的系统,仍然会出现一些模糊的场景使得你不知道系统的执行过程以及输出结果。**能做的只是从产生的结果来判断系统的输出是一致性还是非一致性。**如果C3读X得到2,那么C4最好也是读X得到2,否则就不是强一致性了。但是这里的单系统又会带来容错能力的问题,又需要构建多副本的分布式系统,但这却又是所有问题的开始。

3.3 Bad Replication Design

假设有一个类似上面的支持put, get的 key-value 系统,但是这里启动了两个服务以便于容错。假设客户端C1和C2都想执行写请求,其中一个要写X为1,另一个写X为2;每一个请求同时发给两个服务。这时,两个服务处理C1和C2的先后顺序有可能不一致,就会导致X最终结果不一致。如果C3从S1读数据,C4从S2读数据,这两个客户端读取的数据不一样。如果挂了一个服务,那么后面读取的可能和之前读到的值不同。
这个问题是可以解决的,但是需要服务器之间额外的通信,同时也会提高系统的复杂度。获取强一致性会带来复杂度的提升,当然也有很多方法能够在还不错的一致性和存在小瑕疵之间平衡。

3.4 GFS的设计目标

Google有大量的数据(网页、视频、索引中间文件),需要大量的磁盘来存储这些数据,同时也需要能借助MapReduce这样的工具来快速处理这些数据,因此Google需要能够快速的并行访问这些海量数据。谷歌构建了一个大型快速的文件系统,这个文件系统的数据是全局有效的,不同应用程序都可以从中读取数据。为了提高容量,文件会被GFS分割之后存放在多个服务器上,这样一方面从多个服务器上同时读同一个文件,获得了更高的吞吐;另一方面文件分割存储还可以保存比单个磁盘还要大的文件。因为在数百台服务器上构建了存储系统,系统需要具有一定的故障修复能力。
GFS被设计成只在一个数据中心运行,单个GFS只存在于单个数据中心的单个机房里。理论上应该副本间跨数据中心比较好,但是这样实现起来比较困难。GFS也只是谷歌工程师内部使用。

单数据中心可能是距离带来的性能影响,需要大量额外的设计逻辑。

GFS在各个方面对大型的顺序文件读写做了定制,是为TB级别的文件而生,只支持顺序访问,不支持随机访问。某种程度上来说,它有点像批处理的风格。GFS并没有花费过多的精力来降低延迟,它的关注点在于巨大的吞吐量上,因此单次操作都涉及到MB级别的数据。存储系统有单独针对小份数据优化的系统,但GFS并不是。
GFS论文发表在2003年的SOSP会议上,它描述了一个真正运行在成百上千台计算机上的系统,规模远大于学术界。反映了工业界对于保障系统正常工作和节省成本的经验。论文也提出了一个当时非常异类的观点:存储系统具有弱一致性也是可以的GFS并不保证返回正确的数据,其目标是提供更好的性能。学术论文做多是
多个Master节点共同分担工作
,但GFS却使用单个Master节点(Active-Standby模式,只有一个Master节点在工作)。

学生提问:如果GFS返回错误的数据,会不会影响应用程序?
Robert教授:如果你通过搜索引擎做搜索,20000个搜索结果中丢失了一条或者搜索结果排序是错误的,没有人会注意到这些。尽管GFS可能会返回错误的数据,但是可以在应用程序中做一些补偿。例如论文中提到,应用程序应当对数据做校验,并明确标记数据的边界,这样应用程序在GFS返回不正确数据时可以恢复。

关于这个问题,后面关于spanner等基于GFS的系统是一个很好的答案。

3.5 Master Data

了解系统如何容错以及一致性,就需要知道master存放的数据。

  • **一个master节点,Active-Standby模式,只有一个Master节点在工作,**用来管理文件和Chunk的信息
  • 上百个客户端
  • 大量Chunk服务器,每个上面有1-2块磁盘,用来存储实际的数据。

Master节点知道每一个文件对应的所有的Chunk的ID,每个Chunk 64MB,多个Chunk共同构成了一个文件。比如一个1G的文件,先从master节点查询对应的Chunk所在服务器,之后直接从Chunk Server读取对应的数据。
Master节点内保存的数据内容主要是两个表单:
在这里插入图片描述

  • 文件名到Chunk ID或者Chunk Handle数组的对应,即一个文件对应了哪些ChunkID。
  • Chunk ID到Chunk数据的对应关系。这里的数据又包括了:
    • 每个Chunk存储在哪些服务器上,即Chunk服务器的列表
    • 每个Chunk当前的版本号,通过这个递增字段,用来标识最新的版本信息,cs对应的chunk值小则认为数据是不准确的
    • 主Chunk对应的服务器,因为对Chunk的写操作都必须在Primary Chunk 上顺序处理
    • 主Chunk的租约过期时间,主Chunk只能在特定的租约时间内担任主Chunk

以上数据都存放在内存中,同时会有log以及checkPoint存放在磁盘上,内存中用于快速访问,磁盘中用于持久化。
有些数据需要存在磁盘上,而有些不用。它们分别是:

  • Chunk Handle的数组(第一个表单)要保存在磁盘上,标记成NV(non-volatile, 非易失)表示对应的数据会写入到磁盘上。
  • Chunk服务器列表不用保存到磁盘上。因为Master节点重启之后可以与所有的Chunk服务器通信,并查询每个Chunk服务器存储了哪些Chunk,这里标记成V(volatile),
  • 版本号要不要写入磁盘取决于GFS是如何工作的,我认为它需要写入磁盘
  • 主Chunk的ID,Master节点重启之后会忘记谁是主Chunk,它只需要等待60秒租约到期,那么它知道对于这个Chunk来说没有主Chunk,这个时候,Master节点可以安全指定一个新的主Chunk,这里标记成V。
  • 类似的,租约过期时间也不用写入磁盘,这里标记成V。

如果文件写满了一个64MB Chunk,之后需要新增一个Chunk或者由于指定了新的主Chunk而导致版本号更新,Master节点需要向磁盘中的Log追加一条记录,刚刚向这个文件添加了一个新的Chunk或者刚刚修改了Chunk的版本号。这种更新都需要落盘,因为磁盘更新速度有限,进而影响master节点更新速度,所以要尽可能的少写入数据到磁盘中。
在磁盘中维护log而不是数据库。log数据都是向同一个地址追加,可以将最近的多个log记录一次性的写入,这样只需要等待磁盘的磁碟旋转一次。数据库本质上来说是某种B树(b-tree)或者hash table。对于B树来说,每一份数据都需要在磁盘中随机找个位置写入。
当Master节点故障重启,并重建它的状态,log的最开始可能是几年之前,通过在磁盘中创建一些checkpoint点,从log中的最近一个checkpoint开始恢复,再逐条执行从Checkpoint开始的log,最后恢复自己的状态。

3.6 GFS读文件(Read File)

  1. 客户端(或者应用程序)将file name,offset,size发送给Master
  2. Master根据file name以及偏移量/64MB就可以找到Chunk Handle,之后将这个chunk对应的服务器列表发送给客户端,客户端会缓存chunk和服务器列表的对应关系
  3. 客户端会选择一个网络上最近的服务器(Google的数据中心中,可以从IP地址的差异判断网络位置的远近),并将Chunk Handle和偏移量发送给那个Chunk server,之后返回给客户端数据

1.后面基于GFS的应用可以知道,为了提高效率,而且GFS是大文件的,所以一个文件可能应用只需要读取其中的一部分数据,因为这个文件本身就被应用用来对应多条不同的数据了。2.可能有点绕结合3.5的图即可

Chunk服务器会在本地的硬盘上,将每个Chunk存储成独立的Linux文件,并通过普通的Linux文件系统管理,Chunk文件会按照Handle(也就是ID)命名。Chunk服务器需要做的就是根据文件名找到对应的Chunk文件,之后从文件中读取对应的数据段,并将数据返回给客户端。
学生提问:如果客户端有偏移量信息,那可以直接算出来是第几个Chunk吧?
Robert教授:客户端可以算出来是哪个Chunk,但是客户端不知道Chunk在哪个服务器上。为了获取服务器信息,客户端需要与Master交互。Master节点找到了Chunk对应的ID,并确定了Chunk存储在哪个服务器上。
学生提问:如果读取的数据超过了一个Chunk怎么办?
Robert教授:会将一个读请求拆分成多个读请求(文件对应的多个chunk)再发送到Master节点,之后再向两个不同的Chunk服务器读取数据。
学生提问:能再介绍一下读数据跨越了Chunk边界的情况吗?
Robert教授:GFS的库会将读请求拆分,之后再将它们合并起来。比如需要Chunk7的最后两个字节,Chunk8的头两个字节,获取到这些数据之后,会将它们放在一个buffer中,再返回给调用库的应用程序。Master节点会告诉库有关Chunk的信息,而GFS库可以根据这个信息找到应用程序想要的数据。应用程序只需要确定文件名和数据在整个文件中的偏移量,GFS库和Master节点共同协商将这些信息转换成Chunk。
学生提问:从哪个Chunk服务器读取数据重要吗?
Robert教授:实际上不同Chunk服务器上的数据并不一定完全相同,应用程序应该要能够容忍这种情况。如果从不同的Chunk服务器读取数据,可能会略微不同。GFS论文提到,客户端会尝试从同一个机架或者同一个交换机上的服务器读取数据。

3.7 GFS写文件(Write File)

从应用程序的角度来看,写文件和读文件的接口都是调用GFS的库。对于写文件,客户端会向Master节点发送请求说:我想向这个文件名对应的文件追加数据,请告诉我文件中最后一个Chunk的位置,这里即为记录追加(Record Append)。

客户端根据文件名请求master,获取chunkID(这里肯定是最后一个,当然如果是新文件第一个chunkID也是最后一个),以及对应的服务器列表,进而能够往primary chunk写文件。

不同客户端写同一份日志文件,没有一个客户端会知道文件究竟有多长,因此也就不知道该往什么样的偏移量,或者说向哪个Chunk去追加数据。这个时候,客户端可以向Master节点查询哪个Chunk服务器保存了文件的最后一个Chunk。

这里有很多细节冲突问题,详见下文及3.8
并发量很高的情况下,返回了最后一个chunk,但是此时又被写满了,这个已经不是最后一个了?这个应该是首先会等client发送的所有数据到达之后开始写,这时候chunk发现没有足够空间了,会返回客户端错误
是不是新的chunk都是通过master指定的?应该是的,之后放在文件对应的chunk list里

写Primary Chunk

写文件必须写主副本,通过master获取chunk对应的服务器列表中的主副本信息。但有时Master不一定指定了Chunk的主副本,因此需要考虑Chunk的主副本不存在的情况。对于读文件来说,可以从任何最新的Chunk副本读取数据。
Master节点需要告诉客户端向哪个Chunk服务器(也就是Primary Chunk所在的服务器)去做追加操作,Master节点的部分工作就是弄清楚在追加文件时,客户端应该与哪个Chunk服务器通信。

No Primary 选主的逻辑
  1. 会找出所有存有Chunk最新副本的Chunk服务器,因为某个副本可能因为宕机尚未更新数据
  2. 找出新的Chunk副本。最新的副本是指副本中保存的版本号与Master中记录的Chunk的版本号一致。Chunk副本中的版本号是由Master节点下发的,Master节点知道,对于一个特定的Chunk,哪个版本号是最新的。
    1. Master找到最新的副本
    2. 选一个作为Primary,其他的作为Secondary
    3. Master会增加版本号
    4. Master节点会向Primary和Secondary副本对应的服务器发送消息告知主备关系、最新版本号–>Lease
    5. master持久化版本号

2.b应该是master向chunk的主副本发放lease,增加版本号

几点说明:

  • master通过版本号区别正常副本和数据落后的副本,同时master重启之后,依然可以通过版本号区分副本的情况。
  • master会给primary一个60s的租约(60s之后停止成为primary),持有租约才能够是primary,这种机制可以确保我们不会同时有两个Primary。

这里的60s,后面可以通过和master通信获得延期,实际上3个副本中的primary是master说了算

学生提问:为什么不将所有Chunk服务器上保存的最大版本号作为Chunk的最新版本号?
Robert教授:master重启之后,对于每一个chunk,汇总其各副本的版本号,但是无法确认当前最大的就是最新的,可能最新的并未启动。当Master找不到持有最新Chunk的服务器时,有两种可能:要么Master会等待,并不响应客户端的请求;要么会返回给客户端现在还不知道Chunk在哪,过会再重试吧。比如机房断电导致服务器大规模重启,这时只能等待,因为不会想使用Chunk的旧数据。

  1. 通过版本号匹配,确认最新的chunk版本,这里有两点思考,1)应该是并未保存当前的副本都有哪些节点,全靠上报,当然这里是可以简单优化的2)通过版本号确定最新的,第二个有点类似raft选主,最新的term可以成为leader
  2. 因为肯定不想使用旧数据,所以只能等待最新版本的chunk上线
  3. 一个思考:这种通过别人选主和raft副本之间自己选主的区别联系优劣?

学生提问:如果Chunk服务器上报的版本号高于Master存储的版本号会怎么样?
Robert教授:GFS论文说,chunk server上报了一个比Master记住的版本更高的chunk版本。Master会认为它在分配新的Primary服务器时出现了错误,并且会使用这个更高的版本号来作为Chunk的最新版本号。当Master向Primary和Secondary发送完消息之后就崩溃了,可能会出现上面这种情况。为了让Master能够处理这种情况,Master在发送完消息之后,需要将Chunk的最新版本写入到磁盘中。这里的写入或许需要等到Primary和Secondary返回确认消息之后。我(Robert教授)也不太确定Master究竟是先写本地磁盘中的版本号,然后再通知Primary和Secondary,还是反过来。但是不管怎么样,Master会更新自己的版本号,并通知Primary和Secondary说,你们现在是Primary和Secondary,并且版本号更新了。

感觉是要等chunk的副本确认版本号之后,master才能持久化更新chunkID,保障master的版本号永远不会大于chunk的,不然永远找不到最新的了。同样,这种情况才会出现chunk最新版本号更高的问题。磁盘故障什么的除外。

Primary写
  1. 客户端根据文件名从master获取到了最后一个chunkID以及对应的ChunkServer及其中的primary
  2. 客户端发送要追加的数据
  3. chunkServer会将数据写入临时位置,并不会直接追加到文件中,
  4. 当所有的服务器都返回确认消息说,已经有了要追加的数据,客户端会向Primary服务器发送一条消息说,你和所有的Secondary服务器都有了要追加的数据,现在我想将这个数据追加到这个文件中
  5. Primary会查看当前文件结尾的Chunk,并确保Chunk中有足够的剩余空间,然后将客户端要追加的数据写入Chunk的末尾,同时也会将offset发送给Secondary执行追加
  6. Secondary返回成功给primary
  7. primary收到所有成功回复,会向客户端返回写入成功,如果有一个服务器写失败或者超时,会向客户端返回写入失败

几点说明:

  • Primary服务器或许会从大量客户端收到大量的并发请求,Primary服务器会以某种顺序,一次只执行一个请求。
  • 对于Secondary服务器来说,可能会执行失败,比如说网络丢包,磁盘空间不足,发生故障
  • 如果客户端从Primary得到写入失败,那么客户端应该重新发起整个追加过程,客户端首先会重新与Master交互,找到文件末尾的Chunk
  1. primary控制写入的offset
  2. 这里要求三副本都写成功才能返回给客户端成功,之后客户端可以直接就近读取
  3. 只要写失败,客户端都要重新请求master,因为失败原因多多难以预料,保险起见直接重新走流程
关于master指定primary 以及版本号的问答
  • 什么时候版本号会增加?
    Robert教授:版本号只在Master节点认为Chunk没有Primary时才会增加。在一个正常的流程中,如果对于一个Chunk来说,已经存在了Primary,那么Master节点会记住已经有一个Primary和一些Secondary,Master不会重新选择Primary,也不会增加版本号。它只会告诉客户端说这是Primary,并不会变更版本号。
  • 如果Master节点发现Primary挂了会怎么办?
    Robert教授:Master指定了一个Primary,如果之后ping不通,并不会立即选主,因为这里可能是多种原因导致一两次不通,更主要是这样会导致同时存在两个primary(从客户端的视角,客户端1认为A是主,后面的客户端2认为B是主),会分别处理不同的写请求,最终会导致有两个不同的数据拷贝。这被称为**脑裂(split-brain),通常是由网络分区引起的,比如说Master无法与Primary通信,但是Primary又可以与客户端通信,这就是一种网络分区问题,网络故障是这类分布式存储系统中最难处理的问题之一。**要避免错误的为同一个Chunk指定两个Primary的可能性。Master采取的方式是,当指定一个Primary时,为它分配一个租约,Primary只在租约内有效。Master和Primary都会知道并记住租约有多长,当租约过期了,Primary会停止响应客户端请求,它会忽略或者拒绝客户端请求。如果Master不能与Primary通信,并且想要指定一个新的Primary时,Master会等到前一个Primary的租约到期。所以master必须等到租约到期之后,这样可以确保不会出现这种脑裂情况。

这里的租约,肯定不能用系统时间,如果用倒计时的话,是否需要考虑网络上花费的时间?比如master指定primary之后就开始计时,或者收到primary的响应之后开始,总之master和指定的primary必有先后。好像lease的话,一般会在块到期的前几秒时间就立即发送消息,进行续租。但是过期的判断是否也加上这个?加上这个是否带来延迟。

关于写入的问答
  • 如果是对一个新的文件进行追加,那这个新的文件没有副本,会怎样?
    Robert教授:Master节点或许会通过随机数生成器创造一个新的Chunk ID。之后Master节点会创建一条新的Chunk记录,再随机选择一个Primary和一组Secondary并告诉它们,你们将对这个空的Chunk负责,请开始工作。
  • 写文件失败之后Primary和Secondary服务器上的状态如何恢复?
    Robert教授:Primary会回复客户端说执行失败,部分副本还是成功将数据追加了。一个Chunk的部分副本成功完成了数据追加,而另一部分没有成功,这种状态是可接受的,没有什么需要恢复,这就是GFS的工作方式。
  • 写文件失败之后,读Chunk数据会有什么不同?
    Robert教授:如果写文件失败之后,一个客户端读取相同的Chunk,客户端可能可以读到追加的数据,也可能读不到,取决于客户端读的是Chunk的哪个副本。
  • 可不可以通过版本号来判断副本是否有之前追加的数据?
    Robert教授:所有的Secondary都有相同的版本号,版本号只会在Master指定一个新Primary时才会改变,通常只有在原Primary发生故障了,才会指定一个新的Primary。副本(参与写操作的Primary和Secondary)都有相同的版本号,你没法通过版本号来判断它们是否一样,或许它们就是不一样的(取决于数据追加成功与否)。
  • 客户端将数据拷贝给多个副本会不会造成瓶颈?
    Robert教授:考虑到底层网络,写入文件数据的具体传输路径可能会非常重要。当论文第一次说客户端会将数据发送给每个副本。之后论文又改变了说法,说客户端只会将数据发送给离它最近的副本,之后那个副本会将数据转发到另一个副本,以此类推形成一条链,直到所有的副本都有了数据。这样一条数据传输链可以在数据中心内减少跨交换机传输(否则,所有的数据吞吐都在客户端所在的交换机上)。
  • 为什么立即指定一个新的Primary是坏的设计?
    Robert教授:因为客户端会通过缓存提高效率,客户端会在短时间缓存Primary的身份信息(这样,客户端就不用每次都会向Master请求Primary信息)。即使没有缓存,也有可能刚向客户端返回之后,master就指定了新的primary。如果不采用其它机制,前一个客户端是没办法知道收到的Primary已经过时了,此时如果前一个客户端执行写文件,那么就会与后来的客户端产生两个冲突的副本。
  • 如果写入数据失败了,不是应该先找到问题在哪再重试吗?
    Robert教授:论文中在重试追加数据之前没有任何中间操作,因为可能是网络导致部分数据丢失。客户端重试,对于大多数错误来说可以直接解决。我们希望如果某个副本出错,master能够指定新的,剔除出错的副本,同时指定版本号和新的primary,旧的副本也永远因为版本号旧被抛弃。但是论文里只是说,客户端重试,并且期望之后能正常工作。Master节点会ping所有的Chunk服务器,如果Secondary服务器挂了,Master节点可以发现并更新Primary和Secondary的集合,之后再增加版本号。但是这些都是之后才会发生(而不是立即发生)。

3.8 GFS的一致性

对于一个新建的文件f1,会指定一个新的chunk以及对应的三个副本。客户端向文件追加数据A,此时三个副本(一个Primary和两个Secondary),都成功的将数据追加到了Chunk,Chunk中的第一个记录是A。
第二个客户端向文件f1追加数据B,由于网络问题导致一个secondary副本写失败,现在我们有两个副本有数据B,另一个没有。
第三个客户端向文件f1追加数据C,发送给主副本之后,Primary选择了偏移量,并将偏移量告诉Secondary,将数据C写在Chunk的这个位置。
第二个客户端会收到写失败的回复,之后会再次请求追加数据B,此时一切正常,写入成功。
又一个客户端向文件f1追加数据D,最终写成功两个副本,但是这个客户端挂了,因此并不会再次进行重试,最后数据D出现在某些副本中,而其他副本则完全没有。
在GFS的这种工作方式下,Primary返回写入失败会导致不同的副本有完全不同的数据。
在这里插入图片描述
如果一个客户端读文件f1,读到的内容取决于读取的是Chunk的哪个副本,不同的副本,读取数据的顺序是不一样的。如果读取的是第一个副本,那么客户端可以读到A、B、C,然后是一个重复的B。如果读取的是第三个副本,那么客户端可以读到A,一个空白数据,然后是C、B。

  • 客户端重新发起写入的请求时从哪一步开始重新执行的?
    Robert教授:根据我从论文中读到的内容,(当写入失败,客户端重新发起写入数据请求时)客户端会从整个流程的最开始重发。客户端会再次向Master询问文件最后一个Chunk是什么,因为文件可能因为其他客户端的数据追加而发生了改变。
  • 为什么GFS要设计成多个副本不一致?
    Robert教授:我不明白GFS设计者为什么要这么做。GFS可以设计成多个副本是完全精确同步的。但是为了保持同步,要使用各种各样的技术。其中一条规则就是不能允许这种只更新部分服务器的不完整操作。即使客户端挂了,系统只要收到了请求,就要保障完成,primary需要确保每一个副本都得到每一条消息。
  • 如果第一次写B失败了,C应该在B的位置吧?
    Robert教授:实际上并没有。Primary将C添加到了Chunk的末尾,在B第一次写入的位置之后。当写C的请求发送过来时,Primary实际上可能不知道B的命运是什么。因为我们面对的是多个客户端并发提交追加数据的请求,为了获得高性能,你会希望Primary先执行追加数据B的请求,一旦获取了下一个偏移量,再通知所有的副本执行追加数据C的请求,这样所有的事情就可以并行的发生。Primary也可以判断B已经写入失败了,然后再发一轮消息让所有副本撤销数据B的写操作,但是这样更复杂也更慢。

primary不去校验其它副本有没有写成功,会指定其他副本写入的offset,这样如果自己写成功了,其他副本有失败的,也不会有影响;如果自己写失败,其它副本成功了,也时按照自己的offset写入新的。
这样做的目的大概是为了提高性能,但是客户端就要自己处理这种乱序的数据问题。

GFS这样设计的理由是足够的简单,但是同时也给应用程序暴露了一些奇怪的数据。这里希望为应用程序提供一个相对简单的写入接口,但应用程序需要容忍读取数据的乱序。如果应用程序不能容忍乱序,应用程序要么可以通过在文件中写入序列号,这样读取的时候能自己识别顺序,要么如果应用程序对顺序真的非常敏感那么对于特定的文件不要并发写入。例如,对于电影文件,你不会想要将数据弄乱,当你将电影写入文件时,你可以只用一个客户端连续顺序而不是并发的将数据追加到文件中。

将GFS升级成强一致系统(增加了系统的复杂度,增加了系统内部组件的交互)
  • 你可能需要让Primary来探测重复的请求,这样第二个写入数据B的请求到达时,Primary就知道,我们之前看到过这个请求,可能执行了也可能没执行成功。Primay要尝试确保B不会在文件中出现两次。首先需要的是探测重复的能力

这里大概是考虑到客户端或者网络什么的,会出现重复的请求,通过序列号应该就能支持。

  • 对于Secondary来说,如果Primay要求Secondary执行一个操作,Secondary必须要执行而不是只返回一个错误给Primary。对于一个严格一致的系统来说,是不允许Secondary忽略Primary的请求而没有任何补偿措施的。如果Secondary有一些永久性故障,例如磁盘被错误的拔出了,你需要有一种机制将Secondary从系统中移除,这样Primary可以与剩下的Secondary继续工作。但是GFS没有做到这一点,或者说至少没有做对。

这里是否有点绝对,首先无法保障secondary一定能成功,这时primary认为大家都失败了即可。

  • 当Primary要求Secondary追加数据时,**直到Primary确信所有的Secondary都能执行数据追加之前,Secondary必须小心不要将数据暴露给读请求。**在第一个阶段,Primary向Secondary发请求,要求其执行某个操作,并等待Secondary回复说能否完成该操作,这时Secondary并不实际执行操作。在第二个阶段,如果所有Secondary都回复说可以执行该操作,这时Primary才会说,好的,所有Secondary执行刚刚你们回复可以执行的那个操作。这是现实世界中很多强一致系统的工作方式,这被称为两阶段提交(Two-phase commit)。

这里是不能让客户端读到脏数据

  • 当Primary崩溃时,可能有一组操作由Primary发送给Secondary,Primary在确认所有的Secondary收到了请求之前就崩溃了。当一个Primary崩溃了,一个Secondary会接任成为新的Primary,但是这时,新Primary和剩下的Secondary会在最后几个操作有分歧,因为部分副本并没有收到前一个Primary崩溃前发出的请求。新的Primary上任时,需要显式的与Secondary进行同步,以确保操作历史的结尾是相同的。

对于GFS,切换primary时,客户端会超时失败,之后请求master,最终得到了新的primary信息,这时不管或者primary写成功失败,它也无需考虑别人上次是成功失败,它只需要把自己最后的offset发过去,但是这样客户端读不同的副本得到的结果不一致,客户端需要自己处理这种不一致。
为了保障强一致性,新primary就需要有一个同步操作,让大家都一致。

  • 最后,时不时的,Secondary之间可能会有差异,或者客户端从Master节点获取的是稍微过时的Secondary。系统要么需要将所有的读请求都发送给Primary,因为只有Primary知道哪些操作实际发生了,要么对于Secondary需要一个租约系统,就像Primary一样,这样就知道Secondary在哪些时间可以合法的响应客户端。

这里应该是客户端读的时候,如果读secondary,可能这个secondary持有的数据并不是新的(新加入的,还在catch up阶段),这时它就不应该响应客户端。
读取也是一个很有趣的事,这里可以在raft中获得更多体会。

GFS单master的问题

最后,让我花一分钟来介绍GFS在它生涯的前5-10年在Google的出色表现,总的来说,它取得了巨大的成功,许多许多Google的应用都使用了它,许多Google的基础架构,例如BigTable和MapReduce是构建在GFS之上,GFS在Google内部广泛被应用。它最严重的局限可能在于,它只有一个Master节点,会带来以下问题:

  • Master节点必须为每个文件,每个Chunk维护表单,随着GFS的应用越来越多,这意味着涉及的文件也越来越多,最终Master会耗尽内存来存储文件表单。你可以增加内存,但是单台计算机的内存也是有上限的。

这个大概是单master内存有限,后面可以将元数据剥离出来。

  • 单个Master节点要承载数千个客户端的请求,而Master节点的CPU每秒只能处理数百个请求,尤其Master还需要将部分数据写入磁盘,很快客户端数量超过了单个Master的能力。

单节点复杂所有的选主,而且客户端要从master获取primary的信息,这里能想到的一个就是根据业务搭建一个新的集群。
思考:现实问题中在多集群和单一集群之间的利弊与权衡?即搭建多个集群固然简单,有什么弊端吗?

  • 应用程序发现很难处理GFS奇怪的语义(本节最开始介绍的GFS的副本数据的同步,或者可以说不同步)。
  • 从我们读到的GFS论文中,Master节点的故障切换不是自动的,需要人工干预来处理已经永久故障的Master节点,并更换新的服务器,这可能需要几十分钟甚至更长的而时间来处理,对于某些应用程序来说,这个时间太长了。

这个感觉时Google自己留了一手。

参考文献:
https://pdos.csail.mit.edu/6.824/schedule.html
https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值