# 11.7 故障恢复（Crash Recovery）

接下来，我们讨论一下，当工作站持有锁，并且故障了会发生什么。

这里的场景是，当工作站需要重命名文件或者创建一个文件时，首先它会获得所有需要修改数据的锁，之后修改自身的缓存来体现改动。但是后来工作站在向Petal写入数据的过程中故障了。工作站可能在很多个位置发生故障，但是由于前面介绍过的工作流程，Frangipani总是会先将自身的Log先写入到Petal。这意味着如果发生了故障，那么发生故障时可能会有这几种场景：

* 要么工作站正在向Petal写入Log，所以这个时候工作站必然还没有向Petal写入任何文件或者目录。
* 要么工作站正在向Petal写入修改的文件，所以这个时候工作站必然已经写入了完整的Log。

因为有了前面的工作流程，我们需要担心的故障发生时间点是有限的。

当持有锁的工作站崩溃了之后，发生的第一件事情是锁服务器向工作站发送一个Revoke消息，但是锁服务器得不到任何响应，之后才会触发故障恢复。如果没有人需要用到崩溃工作站持有的锁，那么基本上没有人会注意到工作站崩溃了。假设一个其他的工作站需要崩溃了的工作站所持有的一个锁，锁服务器会发出Revoke消息，但是锁服务器永远也不会从崩溃了的工作站收到Release消息。Frangipani出于一些原因对锁使用了租约，当租约到期了，锁服务器会认定工作站已经崩溃了，之后它会初始化恢复过程。实际上，锁服务器会通知另一个还活着的工作站说：看，工作站1看起来崩溃了，请读取它的Log，重新执行它最近的操作并确保这些操作完成了，在你完成之后通知我。在收到这里的通知之后，锁服务器才会释放锁。这就是为什么日志存放在Petal是至关重要的，因为一个其他的工作站可能会要读取这个工作站在Petal中的日志。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFyQKFIU-OTYTzVGGY7%2F-MFzVLBa1q5kPxb8m9-k%2Fimage.png?alt=media\&token=d9f843fa-17cb-442c-b9f8-ae790f163bf0)

发生故障的场景究竟有哪些呢？

* 第一种场景是，工作站WS1在向Petal写入任何信息之前就故障了。这意味着，当其他工作站WS2执行恢复，查看崩溃了的工作站的Log时，发现里面没有任何信息，自然也就不会做任何操作。之后WS2会释放WS1所持有的锁。工作站WS1或许在自己的缓存中修改了各种各样的数据，但是如果它没有在自己的Log存储区写入任何信息，那么它也不可能在Petal中写入任何它修改的块数据。我们会丢失WS1的最后几个操作，但是文件系统会与WS1开始修改之前保持一致。因为很明显，工作站WS1没能走到向Petal写Log那一步，自然也不可能向Petal写入块数据。
* 第二种场景是，工作站WS1向Petal写了部分Log条目。这样的话，执行恢复的工作站WS2会从Log的最开始向后扫描，直到Log的序列号不再增加，因为这必然是Log结束的位置。工作站WS2会检查Log条目的更新内容，并向Petal执行Log条目中的更新内容。比如Petal中的特定块需要写入特定的数据，这里对应的其实就是工作站WS1在自己本地缓存中做的一些修改。所以执行恢复的工作站WS2会检查每个Log条目，并重新向Petal执行WS1的每一条Log。当WS2执行完WS1存放在Petal中的Log，它会通知锁服务器，之后锁服务器会释放WS1持有的锁。这样的过程会使得Petal更新至故障工作站WS1在故障前的执行的部分操作。或许不能全部恢复WS1的操作，因为故障工作站可能只向Petal写了部分Log就崩溃了。同时，除非在Petal中找到了完整的Log条目，否则执行恢复的工作站WS2是不会执行这条Log条目的，所以，这里的隐含意思是需要有类似校验和的机制，这样执行恢复的工作站就可以知道，这个Log条目是完整的，而不是只有操作的一部分数据。这一点很重要，因为在恢复时，必须要在Petal的Log存储区中找到完整的操作。所以，对于一个操作的所有步骤都需要打包在一个Log条目的数组里面，这样执行恢复的工作站就可以，要么全执行操作的所有步骤，要么不执行任何有关操作的步骤，但是永远不会只执行部分步骤。这就是当在向Petal写入Log时，发生了故障的修复过程。
* 另一个有趣的可能是，工作站WS1在写入Log之后，并且在写入块数据的过程中崩溃了。先不考虑一些极其重要的细节，执行恢复的工作站WS2并不知道WS1在哪个位置崩溃的，它只能看到一些Log条目，同样的，WS2会以相同的方式重新执行Log。尽管部分修改已经写入了Petal，WS2会重新执行修改。对于部分已经写入的数据，相当于在相同的位置写入相同的数据。对于部分未写入的数据，相当于更新了Petal中的这部分数据，并完成了操作。

上面的描述并没有涵盖所有的场景，下面的这个场景会更加复杂一些。如果一个工作站，完成了上面流程的步骤1，2，在释放锁的过程中崩溃了，进而导致崩溃的工作站不是最后修改特定数据的工作站。具体可以看下面这个例子，假设我们有一个工作站WS1，它执行了删除文件（d/f）的操作。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG0yFRwzWI_bkAPM6lI%2Fimage.png?alt=media\&token=a387c67d-8a25-413b-9dee-84e11a2d9b4c)

之后，有另一个工作站WS2，在删除文件之后，以相同的名字创建了文件，当然这是一个不同的文件。所以之后，工作站WS2创建了同名的文件（d/f）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG0ye1Xa1ZKRKEf14Hd%2Fimage.png?alt=media\&token=4fac5882-867a-4f02-9923-08167ee25ff7)

在创建完成之后，工作站WS1崩溃了，

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG0yyJBpM8_lSUhIAoE%2Fimage.png?alt=media\&token=b5fa5c02-4872-45dc-8ca4-641659dfdcda)

所以，我们需要基于WS1的Log执行恢复，这时，可能有第三个工作站WS3来执行恢复的过程。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG0zHHMq3RKoXDAHWVN%2Fimage.png?alt=media\&token=a2930a0a-96a7-4554-a5d5-9c3537846e55)

这里的时序表明，WS1删除了一个文件，WS2创建了一个文件，WS3做了恢复操作。有可能删除操作仍然在WS1的Log中，当WS1崩溃后，WS3需要读取WS1的Log，并重新执行WS1的Log中的更新。因为删除文件的Log条目仍然存在于WS1的Log中，如果不做任何额外的事情，WS3会删除这个文件（d/f）。但是实际上，WS3删除的会是WS2稍后创建的一个完全不同的文件。

这样的结果是完全错误的，因为需要被删除的是WS1指定的文件，而不是WS2创建的一个相同名字的文件。因为WS2的创建是在WS1的删除之后，所以我们不能只是不经思考的重新执行WS1的Log，WS1的Log在我们执行的时候可能已经过时了，其他的一些工作站可能已经以其他的方式修改了相同的数据，所以我们不能盲目的重新执行Log条目。

Frangipani是这样解决这个问题的，通过对每一份存储在Petal文件系统数据增加一个版本号，同时将版本号与Log中描述的更新关联起来。在Petal中，每一个元数据，每一个inode，每一个目录下的内容，都有一个版本号，当工作站需要修改Petal中的元数据时，它会向从Petal中读取元数据，并查看当前的版本号，之后在创建Log条目来描述更新时，它会在Log条目中对应的版本号填入元数据已有的版本号加1。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG12p-P00KWy_MVtq82%2Fimage.png?alt=media\&token=7eb60f76-9b0d-49c9-ad22-20f1e35d994b)

之后，如果工作站执行到了写数据到Petal的步骤，它也会将新的增加了的版本号写回到Petal。

所以，如果一个工作站没有故障，并且成功的将数据写回到了Petal。这样元数据的版本号会大于等于Log条目中的版本号。如果有其他的工作站之后修改了同一份元数据，版本号会更高。

所以，实际上WS3看到的WS1的删除操作对应的Log条目，会有一个特定的版本号，它表明，由这个Log条目影响的元数据对应版本号3（举例）。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG15MVI0iMG1Y-4yACe%2Fimage.png?alt=media\&token=2fc5516b-e190-4d57-8fa6-3fa0db698d45)

WS2的修改在WS1崩溃之前，所以WS1必然已经释放了相关数据的锁。WS2获得了锁，它会读取当前的元数据可以发现当前的版本号是3，当WS2写入数据的时候，它会将版本号设置为4。

![](https://2933519158-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MAkokVMtbC7djI1pgSw%2F-MFzXK0wOxMuYYWWxtF5%2F-MG16qe6dMtFMJ6UfCXi%2Fimage.png?alt=media\&token=e8ba6c06-f443-4b7c-b836-40a8ff895ed6)

之后，当WS3执行恢复流程时，WS3会重新执行WS1的Log，它会首先检查版本号，通过查看Log条目中的版本号，并查看Petal中存储的版本号，如果Petal中存储的版本号大于等于Log条目中的版本号，那么WS3会忽略Log条目中的修改，因为很明显Petal中的数据已经被故障了的工作站所更新，甚至可能被后续的其他工作站修改了。所以在恢复的过程中，WS3会选择性的根据版本号执行Log，只有Log中的版本号高于Petal中存储的数据的版本时，Log才会被执行。

这里有个比较烦人的问题就是，WS3在执行恢复，但是其他的工作站还在频繁的读取文件系统，持有了一些锁并且在向Petal写数据。WS3在执行恢复的过程中，WS2是完全不知道的。WS2可能还持有目录 d的锁，而WS3在扫描故障工作站WS1的Log时，需要读写目录d，但是目录d的锁还被WS2所持有。我们该如何解决这里的问题？

一种不可行的方法是，让执行恢复的WS3先获取所有关联数据的锁，再重新执行Log。这种方法不可行的一个原因是，有可能故障恢复是在一个大范围电力故障之后，这样的话谁持有了什么锁的信息都丢失了，因此我们也就没有办法使用之前的缓存一致性协议，因为哪些数据加锁了，哪些数据没有加锁在断电的过程中丢失了。

但是幸运的是，执行恢复的工作站可以直接从Petal读取数据而不用关心锁。这里的原因是，执行恢复的工作站想要重新执行Log条目，并且有可能修改与目录d关联的数据，它就是需要读取Petal中目前存放的目录数据。接下来只有两种可能，要么故障了的工作站WS1释放了锁，要么没有。如果没有的话，那么没有其他人不可以拥有目录的锁，执行恢复的工作站可以放心的读取目录数据，没有问题。如果释放了锁，那么在它释放锁之前，它必然将有关目录的数据写回到了Petal。这意味着，Petal中存储的版本号，至少会和故障工作站的Log条目中的版本号一样大，因此，之后恢复软件对比Log条目的版本号和Petal中存储的版本号，它就可以发现Log条目中的版本号并没有大于存储数据的版本号，那么这条Log条目就会被忽略。所以这种情况下，执行恢复的工作站可以不持有锁直接读取块数据，但是它最终不会更新数据。因为如果锁被释放了，那么Petal中存储的数据版本号会足够高，表明在工作站故障之前，Log条目已经应用到了Petal。所以这里不需要关心锁的问题。
