Files
MIT6.824/lecture-11-cache-consistency-frangipani/11.7-gu-zhang-hui-fu-crash-recovery.md
2022-01-25 02:41:31 +00:00

11 KiB
Raw Permalink Blame History

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。尽管部分修改已经写入了PetalWS2会重新执行修改。对于部分已经写入的数据相当于在相同的位置写入相同的数据。对于部分未写入的数据相当于更新了Petal中的这部分数据并完成了操作。

上面的描述并没有涵盖所有的场景下面的这个场景会更加复杂一些。如果一个工作站完成了上面流程的步骤12在释放锁的过程中崩溃了进而导致崩溃的工作站不是最后修改特定数据的工作站。具体可以看下面这个例子假设我们有一个工作站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的LogWS1的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。所以这里不需要关心锁的问题。