11.4 缓存一致性(Cache Coherence)
最后更新于
最后更新于
工作站和锁服务器之间的缓存一致协议协议包含了4种不同的消息。本质上你可以认为它们就是一些单向的网络消息。
首先是Request消息,从工作站发给锁服务器。Request消息会说:hey锁服务器,我想获取这个锁。
如果从锁服务器的lock表单中发现锁已经被其他人持有了,那锁服务器不能立即交出锁。但是一旦锁被释放了,锁服务器会回复一个Grant消息给工作站。这里的Request和Grant是异步的。
如果你向锁服务器请求锁,而另一个工作站现在正持有锁,锁服务器需要持有锁的工作站先释放锁,因为一个锁不能同时被两个人持有。那我们怎么能让这个工作站获取到锁呢?
前面说过,如果一个工作站在使用锁,并在执行读写操作,那么它会将锁标记为Busy。但是通常来说,当工作站使用完锁之后,不会向锁服务器释放锁。所以,如果我创建了一个新文件,create函数返回时,这些新文件的锁仍然被我的工作站持有。只是说现在锁的状态会变成Idle而不是Busy。但是从锁服务器看来,我的工作站仍然持有锁。这里延迟将锁还给锁服务器的原因是,如果我在我的工作站上创建了文件Y。我接下来几乎肯定要将Y用于其他目的,或许我向它写一些数据,或许会从它读数据。所以,如果工作站能持有所有最近用过的文件的锁并不主动归还的话,会有非常大的优势。在一个常见的例子中,我使用了home目录下的一些文件,并且其他工作站没有人查看过这些文件。我的工作站最后会为我的文件持有数百个在Idle状态的锁。但是如果某人查看了我的文件,他需要先获取锁,而这时我就需要释放锁了。
所以这里的工作方式是,如果锁服务器收到了一个加锁的请求,它查看自己的lock表单可以发现,这个锁现在正被工作站WS1所持有,锁服务器会发送一个Revoke消息给当前持有锁的工作站WS1。并说,现在别人要使用这个文件,请释放锁吧。
当一个工作站收到了一个Revoke请求,如果锁时在Idle状态,并且缓存的数据脏了,工作站会首先将修改过的缓存写回到Petal存储服务器中,因为前面的规则要求在释放锁之前,要先将数据写入Petal。所以如果锁的状态是Idle,首先需要将修改了的缓存数据发回给Petal,只有在那个时候,工作站才会再向锁服务器发送一条消息说,好吧,我现在放弃这个锁。所以,对于一个Revoke请求的响应是,工作站会向锁服务器发送一条Release消息。
如果工作站收到Revoke消息时,它还在使用锁,比如说正在删除或者重命名文件的过程中,直到工作站使用完了锁为止,或者说直到它完成了相应的文件系统操作,它都不会放弃锁。完成了操作之后,工作站中的锁的状态才会从Busy变成Idle,之后工作站才能注意到Revoke请求,在向Petal写完数据之后最终释放锁。
所以,这就是Frangipani使用的一致性协议的一个简单版本的描述。如我之前所描述的,这里面没有考虑一个事实,那就是锁可以是为写入提供的排他锁(Exclusive Lock),也可以是为只读提供的共享锁(Shared Lock)。
就像Petal只是一个块存储服务,并不理解文件系统。锁服务器也不理解文件,目录,还有文件系统,它只是维护lock表单,表单中记录的是锁的名字和锁的持有者。Frangipani可以理解锁与某个文件相关联。实际上Frangipani在这里使用的是Unix风格的inode号来作为lock表单的key,而不是文件的名字。
接下来,我们看一下如何应用这里的缓存一致协议,并演示Petal操作和和锁服务器操作之间的关联。我会过一遍工作站修改文件系统数据,之后另一个工作站查看对应数据的流程。
所以,首先我们有了2个工作站(WS1,WS2),一个锁服务器(LS)。
按照协议,如果WS1想要读取并修改文件Z。在它从Petal读取文件之前,它需要先获取对于Z的锁,所以它向锁服务器发送Request消息(下图中ACQ Z)。
如果当前没有人持有对文件Z的锁,或者锁服务器没听过对于文件Z的锁(初始化状态),锁服务器会在lock表单中增加一条记录,并返回Grant消息给工作站说,你现在持有了对于Z文件的锁。
从这个时间点开始,工作站WS1持有了对文件Z的锁,并且被授权可以从Petal读取Z的数据。所以这个时间点,WS1会从Petal读取并缓存Z的内容。之后,WS1也可以在本地缓存中修改Z的内容。
过了一会,坐在工作站WS2前面的用户也想读取文件Z。但是一开始WS2并没有对于文件Z的锁,所以它要做的第一件事情就是向锁服务器发送Request消息,请求对于文件Z的锁。
但是,锁服务器知道不能给WS2回复Grant消息,因为WS1现在还持有锁。接下来锁服务器会向WS1发送Revoke消息。
而工作站WS1在向Petal写入修改数据之前,不允许释放锁。所以它现在会将任何修改的内容写回给Petal。
写入结束之后,WS1才可以向锁服务器发送Release消息。
锁服务器必然会有一个表单记录谁在等待文件Z的锁,一旦锁的当前持有者释放了锁,锁服务器需要通知等待者。所以当锁服务器收到了这条Release消息时,锁服务器会更新自己的表单,并最终将Grant消息发送给工作站WS2。
这个时候,WS2终于可以从Petal读取文件Z。
这就是缓存一致性协议的工作流程,它确保了,直到所有有可能私底下在缓存中修改了数据的工作站先将数据写回到Petal,其他工作站才能读取相应的文件。所以,这里的锁机制确保了读文件总是能看到最新写入文件的数据。
在这个缓存一致性的协议中,有许多可以优化的地方。实际上,我之前已经描述过一个优化点了,
每个工作站用完了锁之后,不是立即向锁服务器释放锁,而是将锁的状态标记为Idle就是一种优化。
另一个主要的优化是,Frangipani有共享的读锁(Shared Read Lock)和排他的写锁(Exclusive Write Lock)。如果有大量的工作站需要读取文件,但是没有人会修改这个文件,它们都可以同时持有对这个文件的读锁。如果某个工作站需要修改这个已经被大量工作站缓存的文件时,那么它首先需要Revoke所有工作站的读锁,这样所有的工作站都会放弃自己对于该文件的缓存,只有在那时,这个工作站才可以修改文件。因为没有人持有了这个文件的缓存,所以就算文件被修改了,也没有人会读到旧的数据。
这就是以锁为核心的缓存一致性。
学生提问:如果没有其他工作站读取文件,那缓存中的数据就永远不写入后端存储了吗?
Robert教授:这是一个好问题。实际上,在我刚刚描述的机制中是有风险的,如果我在我的工作站修改了一个文件,但是没有人读取它,这时,这个文件修改后的版本的唯一拷贝只存在于我的工作站的缓存或者RAM上。这些文件里面可能有一些非常珍贵的信息,如果我的工作站崩溃了,并且我们不做任何特殊的操作,数据的唯一拷贝会丢失。所以为了阻止这种情况,不管怎么样,工作站每隔30秒会将所有修改了的缓存写回到Petal中。所以,如果我的工作站突然崩溃了,我或许会丢失过去30秒的数据,但是不会丢更多,这实际上是模仿Linux或者Unix文件系统的普通工作模式。在一个分布式文件系统中,很多操作都是在模仿Unix风格的文件系统,这样使用者才不会觉得Frangipani的行为异常,因为它基本上与用户在使用的文件系统一样。