# 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的主副本不存在的情况。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDgqqcQCOWiEZmOGADp%2Fimage.png?alt=media\&token=12e8ba02-f826-4ff1-8be2-e020ca41bc80)

对于Master节点来说，如果发现Chunk的主副本不存在，Master会找出所有存有Chunk最新副本的Chunk服务器。如果你的一个系统已经运行了很长时间，那么有可能某一个Chunk服务器保存的Chunk副本是旧的，比如说还是昨天或者上周的。导致这个现象的原因可能是服务器因为宕机而没有收到任何的更新。所以，Master节点需要能够在Chunk的多个副本中识别出，哪些副本是新的，哪些是旧的。所以第一步是，找出新的Chunk副本。这一切都是在Master节点发生，因为，现在是客户端告诉Master节点说要追加某个文件，Master节点需要告诉客户端向哪个Chunk服务器（也就是Primary Chunk所在的服务器）去做追加操作。所以，Master节点的部分工作就是弄清楚在追加文件时，客户端应该与哪个Chunk服务器通信。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDgr64DiCGSgZCCkpMB%2Fimage.png?alt=media\&token=22e59e44-e584-4402-8362-483fc2947b3c)

每个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。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDh5QOejA1Se5aca1Vd%2Fimage.png?alt=media\&token=68313d63-7c73-4890-ad77-afd1378daec2)

之后，Master会增加版本号，并将版本号写入磁盘，这样就算故障了也不会丢失这个数据。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDh5o5y_qAzoJsahP85%2Fimage.png?alt=media\&token=7e81fc2a-7be0-4fa3-a97a-b866af295e35)

接下来，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返回确认消息之后。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDhERx8WbQJYx49mt26%2Fimage.png?alt=media\&token=9f74aff1-ab0a-43bb-b9be-50510cf00475)

> 学生提问：听不清（但是应该与这一节的第一个问题一样）。
>
> 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的问答中有一个专门的问题讨论）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDhWsP1_Rkt-NWarUhk%2Fimage.png?alt=media\&token=c9019529-d1f5-4985-9e09-f6ee783e8aee)

我们现在来看GFS论文的图2。假设现在Master节点告诉客户端谁是Primary，谁是Secondary，GFS提出了一种聪明的方法来实现写请求的执行序列。客户端会将要追加的数据发送给Primary和Secondary服务器，这些服务器会将数据写入到一个临时位置。所以最开始，这些数据不会追加到文件中。当所有的服务器都返回确认消息说，已经有了要追加的数据，客户端会向Primary服务器发送一条消息说，你和所有的Secondary服务器都有了要追加的数据，现在我想将这个数据追加到这个文件中。Primary服务器或许会从大量客户端收到大量的并发请求，Primary服务器会以某种顺序，一次只执行一个请求。对于每个客户端的追加数据请求（也就是写请求），Primary会查看当前文件结尾的Chunk，并确保Chunk中有足够的剩余空间，然后将客户端要追加的数据写入Chunk的末尾。并且，Primary会通知所有的Secondary服务器也将客户端要追加的数据写入在它们自己存储的Chunk末尾。这样，包括Primary在内的所有副本，都会收到通知将数据追加在Chunk的末尾。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MD_2fzyJxtqSWsz4PTS%2F-MDhXS1nVqF7PpmQYYMV%2Fimage.png?alt=media\&token=5cc6cb50-1e81-4833-9a0c-a7dbb4fee99d)

但是对于Secondary服务器来说，它们可能可以执行成功，也可能会执行失败，比如说磁盘空间不足，比如说故障了，比如说Primary发出的消息网络丢包了。如果Secondary实际真的将数据写入到了本地磁盘存储的Chunk中，它会回复“yes”给Primary。如果所有的Secondary服务器都成功将数据写入，并将“yes”回复给了Primary，并且Primary也收到了这些回复。Primary会向客户端返回写入成功。如果至少一个Secondary服务器没有回复Primary，或者回复了，但是内容却是：抱歉，一些不好的事情发生了，比如说磁盘空间不够，或者磁盘故障了，Primary会向客户端返回写入失败。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MDhYHrFEry6nd9Oq7xo%2F-MDhYJnTm1xs1P7Ea7gh%2Fimage.png?alt=media\&token=45c28166-308a-4966-8e46-790b1e5e9c35)

GFS论文说，如果客户端从Primary得到写入失败，那么客户端应该重新发起整个追加过程。客户端首先会重新与Master交互，找到文件末尾的Chunk；之后，客户端需要重新发起对于Primary和Secondary的数据追加操作。
