MIT6.824/lecture-07-raft2/7.5-ri-zhi-kuai-zhao-log-snapshot.md
2022-01-25 02:41:31 +00:00

9.3 KiB
Raw Blame History

7.5 日志快照Log Snapshot

Log压缩和快照Log compaction and snapshots在Lab3b中出现的较多。在Raft中Log压缩和快照解决的问题是对于一个长期运行的系统例如运行了几周几个月甚至几年如果我们按照Raft论文图2的规则那么Log会持续增长。最后可能会有数百万条Log从而需要大量的内存来存储。如果持久化存储在磁盘上最终会消耗磁盘的大量空间。如果一个服务器重启了它需要通过重新从头开始执行这数百万条Log来重建自己的状态。当故障重启之后遍历并执行整个Log的内容可能要花费几个小时来完成。这在某种程度上来说是浪费因为在重启之前服务器已经有了一定的应用程序状态。

为了应对这种场景Raft有了快照Snapshots的概念。快照背后的思想是要求应用程序将其状态的拷贝作为一种特殊的Log条目存储下来。我们之前几乎都忽略了应用程序但是事实是假设我们基于Raft构建一个key-value数据库Log将会包含一系列的Put/Get或者Read/Write请求。假设一条Log包含了一个Put请求客户端想要将X设置成1另一条Log想要将X设置成2下一条将Y设置成7。

如果Raft一直执行没有故障Raft之上的将会是应用程序在这里应用程序将会是key-value数据库。它将会维护一个表单当Raft一个接一个的上传命令时应用程序会更新它的表单。

所以第一个命令之后应用程序会将表单中的X设置为1。

第二个命令之后表单中的X会被设置为2。

第三个命令之后表单中的Y会被设置为7。

这里有个有趣的事实那就是对于大多数的应用程序来说应用程序的状态远小于Log的大小。某种程度上我们知道在某些时间点Log和应用程序的状态是可以互换的它们是用来表示应用程序状态的不同事物。但是Log可能包含大量的重复的记录例如对于X的重复赋值这些记录使用了Log中的大量的空间但是同时却压缩到了key-value表单中的一条记录。这在多副本系统中很常见。在这里如果存储Log可能尺寸会非常大相应的如果存储key-value表单这可能比Log尺寸小得多。这就是快照的背后原理。

所以当Raft认为它的Log将会过于庞大例如大于1MB10MB或者任意的限制Raft会要求应用程序在Log的特定位置对其状态做一个快照。所以如果Raft要求应用程序做一个快照Raft会从Log中选取一个与快照对应的点然后要求应用程序在那个点的位置做一个快照。这里极其重要因为我们接下来将会丢弃所有那个点之前的Log记录。如果我们有一个点的快照那么我们可以安全的将那个点之前的Log丢弃。在key-value数据库的例子中快照本质上就是key-value表单。

我们还需要为快照标注Log的槽位号。在这个图里面这个快照对应的正好是槽位3。

有了快照并且Raft将它存放在磁盘中之后Raft将不会再需要这部分Log。只要Raft持久化存储了快照快照对应的Log槽位号以及Log槽位号之后的所有Log那么快照对应槽位号之前的这部分Log可以被丢弃我们将不再需要这部分Log。

所以这就是Raft快照的工作原理Raft要求应用程序做快照得到快照之后将其存储在磁盘中同时持久化存储快照之后的Log并丢弃快照之前的Log。所以Raft的持久化存储实际上是持久化应用程序快照和快照之后的Log。大家都明白了吗

学生提问:听不清。

Robert教授或许可以这样看这些Log快照之后的Log是实际存在的而快照之前的Log可以认为是幽灵条目我们可以认为它们还在那只是说我们永远不会再去查看它们了 因为我们现在有快照了。事实上我们不再存储幽灵条目但是效果上是等效于有完整的Log。

刚刚的回答可能有些草率。因为如果按照Raft论文的图2你有时还是需要这些早期的Log槽位123。所以在知道了有时候某些Log可能不存在的事实之后你可能需要稍微重新理解一下图2。

所以重启的时候会发生什么呢现在重启的场景比之前只有Log会更加复杂一点。重启的时候必须让Raft有方法知道磁盘中最近的快照和Log的组合并将快照传递给应用程序。因为现在我们不能重演所有的Log部分被删掉了所以必须要有一种方式来初始化应用程序。所以应用程序不仅需要有能力能生成一个快照它还需要能够吸纳一个之前创建的快照并通过它稳定的重建自己的内存。所以尽管Raft在管理快照快照的内容实际上是应用程序的属性。Raft并不理解快照中有什么只有应用程序知道因为快照里面都是应用程序相关的信息。所以重启之后应用程序需要能够吸纳Raft能找到的最近的一次快照。到目前为止还算简单。

不幸的是这里丢弃了快照之前的Log引入了大量的复杂性。如果有的Follower的Log较短在Leader的快照之前就结束那么除非有一种新的机制否则那个Follower永远也不可能恢复完整的Log。因为如果一个Follower只有前两个槽位的LogLeader不再有槽位3的Log可以通过AppendEntries RPC发给FollowerFollower的Log也就不可能补齐至Leader的Log。

我们可以通过这种方式来避免这个问题如果Leader发现有任何一个Follower的Log落后于Leader要做快照的点那么Leader就不丢弃快照之前的Log。Leader原则上是可以知道Follower的Log位置然后Leader可以不丢弃所有Follower中最短Log之后的本地Log。

这或许是一个短暂的好方法之所以这个方法不完美的原因在于如果一个Follower关机了一周它也就不能确认Log条目同时也意味着Leader不能通过快照来减少自己的内存消耗因为那个Follower的Log长度一直没有更新

所以Raft选择的方法是Leader可以丢弃Follower需要的Log。所以我们需要某种机制让AppendEntries能处理某些Follower Log的结尾到Leader Log开始之间丢失的这一段Log。解决方法是一个新的消息类型InstallSnapshot RPC。

当Follower刚刚恢复如果它的Log短于Leader通过 AppendEntries RPC发给它的内容那么它首先会强制Leader回退自己的Log。在某个点Leader将不能再回退因为它已经到了自己Log的起点。这时Leader会将自己的快照发给Follower之后立即通过AppendEntries将后面的Log发给Follower。

不幸的是这里明显的增加了的复杂度。因为这里需要Raft组件之间的协同这里还有点违反模块性因为这里需要组件之间有一些特殊的协商。例如当Follower收到了InstallSnapshot这个消息是被Raft收到的但是Raft实际需要应用程序能吸纳这个快照。所以它们现在需要更多的交互了。

学生提问:快照的创建是否依赖应用程序?

Robert教授肯定依赖。快照生成函数是应用程序的一部分如果是一个key-value数据库那么快照生成就是这个数据库的一部分。Raft会通过某种方式调用到应用程序通知应用程序生成快照因为只有应用程序自己才知道自己的状态进而能生成快照。而通过快照反向生成应用程序状态的函数同样也是依赖应用程序的。但是这里又有点纠缠不清因为每个快照又必须与某个Log槽位号对应。

学生提问如果RPC消息乱序该怎么处理

Robert教授是在说Raft论文图13的规则6吗这里的问题是你们会在Lab3遇到这个问题因为RPC系统不是完全的可靠和有序RPC可以乱序的到达甚至不到达。你或许发了一个RPC但是收不到回复并认为这个消息丢失了但是消息实际上送达了实际上是回复丢失了。所有这些都可能发生包括发生在InstallSnapshot RPC中。Leader几乎肯定会并发发出大量RPC其中包含了AppendEntries和InstallSnapshot因此Follower有可能受到一条很久以前的InstallSnapshot消息。因此Follower必须要小心应对InstallSnapshot消息。我认为你想知道的是如果Follower收到了一条InstallSnapshot消息但是这条消息看起来完全是冗余的这条InstallSnapshot消息包含的信息比当前Follower的信息还要老这时Follower该如何做

Raft论文图13的规则6有相应的说明。我认为正常的响应是Follower可以忽略明显旧的快照。其实我Robert教授看不懂那条规则6。