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。

最后更新于