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升级成强一致系统,我可以为你列举一些你需要考虑的事情:
- 你可能需要让Primary来探测重复的请求,这样第二个写入数据B的请求到达时,Primary就知道,我们之前看到过这个请求,可能执行了也可能没执行成功。Primay要尝试确保B不会在文件中出现两次。所以首先需要的是探测重复的能力。
- 对于Secondary来说,如果Primay要求Secondary执行一个操作,Secondary必须要执行而不是只返回一个错误给Primary。对于一个严格一致的系统来说,是不允许Secondary忽略Primary的请求而没有任何补偿措施的。所以我认为,Secondary需要接受请求并执行它们。如果Secondary有一些永久性故障,例如磁盘被错误的拔出了,你需要有一种机制将Secondary从系统中移除,这样Primary可以与剩下的Secondary继续工作。但是GFS没有做到这一点,或者说至少没有做对。