3.6 GFS写文件(Write File)(1)
最后更新于
最后更新于
GFS写文件的过程会更加复杂且有趣。从应用程序的角度来看,写文件和读文件的接口是非常类似的,它们都是调用GFS的库。写文件是,应用程序会告诉库函数说,我想对这个文件名的文件在这个数据段写入当前存在buffer中的数据。让我(Robert教授)稍微收敛一下,我只想讨论数据的追加。所以我会限制这里的客户端接口,客户端(也就是应用程序)只能说,我想把buffer中的数据,追加到这个文件名对应的文件中。这就是GFS论文中讨论的记录追加(Record Append)。所以,再次描述一下,对于写文件,客户端会向Master节点发送请求说:我想向这个文件名对应的文件追加数据,请告诉我文件中最后一个Chunk的位置。
当有多个客户端同时写同一个文件时,一个客户端并不能知道文件究竟有多长。因为如果只有一个客户端在写文件,客户端自己可以记录文件长度,而多个客户端时,一个客户端没法知道其他客户端写了多少。例如,不同客户端写同一份日志文件,没有一个客户端会知道文件究竟有多长,因此也就不知道该往什么样的偏移量,或者说向哪个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副本中的版本号是由Master节点下发的,所以Master节点知道,对于一个特定的Chunk,哪个版本号是最新的。这就是为什么Chunk的版本号在Master节点上需要保存在磁盘这种非易失的存储中的原因(见3.4),因为如果版本号在故障重启中丢失,且部分Chunk服务器持有旧的Chunk副本,这时,Master是没有办法区分哪个Chunk服务器的数据是旧的,哪个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服务器会记住本地存储Chunk对应的版本号,当Chunk服务器向Master汇报时,就可以说,我有这个Chunk的这个版本。而Master节点就可以忽略哪些版本号与已知版本不匹配的Chunk服务器。
回到之前的话题,当客户端想要对文件进行追加,但是又不知道文件尾的Chunk对应的Primary在哪时,Master会等所有存储了最新Chunk版本的服务器集合完成,然后挑选一个作为Primary,其他的作为Secondary。
之后,Master会增加版本号,并将版本号写入磁盘,这样就算故障了也不会丢失这个数据。
接下来,Master节点会向Primary和Secondary副本对应的服务器发送消息并告诉它们,谁是Primary,谁是Secondary,Chunk的新版本是什么。Primary和Secondary服务器都会将版本号存储在本地的磁盘中。这样,当它们因为电源故障或者其他原因重启时,它们可以向Master报告本地保存的Chunk的实际版本号。
学生提问:如果Chunk服务器上报的版本号高于Master存储的版本号会怎么样?
Robert教授:这个问题很好。我不知道答案,不过论文中有一些线索。其实刚刚我的介绍有一些错误,我认为你的问题让我明白了一些事情。GFS论文说,如果Master节点重启,并且与Chunk服务器交互,同时一个Chunk服务器重启,并上报了一个比Master记住的版本更高的版本。Master会认为它在分配新的Primary服务器时出现了错误,并且会使用这个更高的版本号来作为Chunk的最新版本号。
当Master向Primary和Secondary发送完消息之后就崩溃了,可能会出现上面这种情况。为了让Master能够处理这种情况,Master在发送完消息之后,需要将Chunk的最新版本写入到磁盘中。这里的写入或许需要等到Primary和Secondary返回确认消息之后。
学生提问:听不清(但是应该与这一节的第一个问题一样)。
Robert教授:我不认为这行得通。因为存在这种可能性,当Master节点重启时,存储了Chunk最新版本号的Chunk服务器是离线状态。这种情况下,我们不希望Master在重启时不知道当前的版本号,因为那样的话,Master就会认为当前发现的最高版本号是当前版本号,但是(由于有最新版本号的Chunk服务器还是离线状态)发现的最高版本号可能是个旧版本号。
我(Robert教授)之前没太关注这块,所以我也不太确定Master究竟是先写本地磁盘中的版本号,然后再通知Primary和Secondary,还是反过来。但是不管怎么样,Master会更新自己的版本号,并通知Primary和Secondary说,你们现在是Primary和Secondary,并且版本号更新了。
所以,现在我们有了一个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的数据追加操作。