# 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。
