11.7 故障恢复(Crash Recovery)

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

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

  • 要么工作站正在向Petal写入Log,所以这个时候工作站必然还没有向Petal写入任何文件或者目录。

  • 要么工作站正在向Petal写入修改的文件,所以这个时候工作站必然已经写入了完整的Log。

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

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

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

  • 第一种场景是,工作站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)的操作。

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

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

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

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

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

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

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

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

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

最后更新于