# 11.7 故障恢复（Crash Recovery）

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

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

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

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

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

![](/files/-MFzVLBa1q5kPxb8m9-k)

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

* 第一种场景是，工作站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）的操作。

![](/files/-MG0yFRwzWI_bkAPM6lI)

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

![](/files/-MG0ye1Xa1ZKRKEf14Hd)

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

![](/files/-MG0yyJBpM8_lSUhIAoE)

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

![](/files/-MG0zHHMq3RKoXDAHWVN)

这里的时序表明，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。

![](/files/-MG12p-P00KWy_MVtq82)

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

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

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

![](/files/-MG15MVI0iMG1Y-4yACe)

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

![](/files/-MG16qe6dMtFMJ6UfCXi)

之后，当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。所以这里不需要关心锁的问题。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-11-cache-consistency-frangipani/11.7-gu-zhang-hui-fu-crash-recovery.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
