Part 19:Raft论文翻译-《CONSENSUS BRIDGING THEORY AND PRACTICE》(日志压缩-对基于内存的状态机进行快照)
5.1 对基于内存的状态机进行快照
第一种快照方法适用于状态机的数据结构保存在内存中。对于数据集为几GB或几十GB的状态机,这是一个合理的选择。它使操作能够快速完成,因为它们永远不必从磁盘获取数据;它也很容易编程,因为可以使用丰富的数据结构,每个操作都可以运行到完成(不阻塞I/O)。
图5.2显示了在Raft中使用基于内存的快照技术。每个服务器独立的进行快照,将刚提交的log entry包含进去。大部分的工作就是对当前状态机状态的快照数据进行序列化,这是对于特定的状态机实现。例如,LogCabin的状态机使用了树作为主要的数据结构,它使用预定深度优先遍历对此树进行序列化(这样在应用快照时,父节点在其子节点之前)。状态机还必须序列化它们保留的信息,以便为客户端提供线性化信息(请参见第6章)。
一旦状态机对某些log entry进行了快照,相应的log entry就可以被丢弃了。Raft将先保存用于重启的状态信息:快照中最后一个log entry的index和term,还有对应index的最新的配置信息。然后,Raft就丢弃之前的log entry了。而且前面的快照也可以被丢弃。
如上所述,Leader可能偶尔需要将快照发送给其它的慢Follower以及新加入集群的服务器。在快照时,状态就是指的最新的快照里面的状态,Leader将通过所谓的InstallSnapshot RPC来将这个快照同步给其它服务器,如图5.3所示。当某个Follower接收到这个快照RPC的时候,它必须决定如何处理现在的log entry。一般情况下,快照里面应该包含了Follower log里面没有的新log entry。这种情况下,丢弃当前的整个log(这个log里面可能有与快照里面冲突的log entry)。如果,Follower接收到一个快照且有一个在此快照之前的log index(就是说,这个快照可能是部分快照,前面已经有一些快照发送过了),那么和这个快照里面位置重叠的log entry将被覆盖,随后的log entry将被保留。
本节的其余部分将讨论基于快照内存的状态机的第二个问题:
第5.1.1节讨论了如何与正常操作并行生成快照,以尽量减少其对客户端的影响;
第5.1.2节讨论何时拍摄快照,平衡空间使用和快照的开销;以及
第5.1.3节讨论了在实现快照过程中出现的问题。
5.1.1 Snapshotting concurrently(并行缓存)
创建快照可能需要很长时间,无论是序列化状态还是将快照写入磁盘。例如,在今天的服务器上复制10GB的内存大约需要一秒钟,而序列化它通常需要更长的时间:即使是固态磁盘也只能在一秒钟内写入大约500MB的内存。那么,序列化和快照写入需要与正常的操作并行来处理以避免系统的可用性受到影响。
幸运的是,写时复制技术允许在进行快照写入的时候进行新的快照更新。可通过以下两种方式实现:
状态机可以被存储于不可变的数据结构中。因为新的状态机不会修改已经保存的状态机,一个快照任务可以使用数据结构的引用而且一致的写入到快照;
另一个方法,可以使用操作系统的写时复制技术。在Linux操作系统中,内存中的状态机可以使用fork方法来复制服务器内存的相应地址空间中的数据(也就是状态机所在的内存地址,fork会利用写时复制技术保证更新不会破坏原始数据)。子进程可以写入快照并退出,父进程可以继续提供服务。LogCabin就是使用这种方式来实现的。
服务器需要额外的内存来同步进行快照,这是应该被计划和管理。状态机必须具有到快照文件的流媒体接口,以便在创建快照时不必完全存储在内存中。但是,写时复制机制需要额外的内存,这与在快照过程中更改的状态机状态的比例成正比。此外,由于false sharing,依赖操作系统通常会使用更多的内存(例如,如果两个不相关的数据项恰好在内存的同一页面上,即使只有第一个发生更改,第二个项也将被重复)。不幸的是,在快照过程中内存容量耗尽,服务器应该停止接受新的日志条目,直到完成快照;这将暂时牺牲服务器的可用性(集群可能仍然可用),但至少允许服务器恢复。最好不要中止快照并稍后再试,因为下一次尝试也可能面临同样的问题。
5.1.2 何时进行快照?
服务器必须决定何时进行快照。如果服务器快照过频繁,则会浪费磁盘带宽和其他资源;如果快照频率过于小,则会耗尽存储容量,并增加重启期间重播日志所需的时间。
一个简单的方法就是等日志到达某个固定的大小时候进行快照。如果这个大小被设置的很大,那么磁盘的带宽负载就会很小,但是,对于小的状态机来说,这个日志就太大了。
一个更好的方法是比较快照的大小与日志的大小。如果快照将比日志小许多倍,那么可能值得进行快照。然而,在进行快照之前计算快照的大小可能很困难,这会给状态机带来巨大负担,或者需要几乎与实际使用快照来动态计算大小一样多的工作。压缩快照文件也可以节省空间和带宽,但很难预测压缩后的输出会有多大。
幸运的是,我们可以使用前一个快照的大小作为参考而不是下一个快照的大小。服务器可以在当log的size大于前一个快照的size*扩展因子的时候进行快照。
快照将对CPU和磁盘的带宽产生冲击并影响对客户端的响应能力。在此方法中,集群中可以一次让不超过半数的服务器进行快照,这不会影响集群的可用性。因为Raft只要多数派可以正常运行就行。例如,当Leader准备进行快照的时候,它可以先退出集群进行快照,让其他服务器来管理集群。这将是Raft后面可能会进行的一个重点工作,因为这不仅能够提升系统性能也能简化算法复杂度。
5.1.3 具体实现的思考
本节回顾了快照实现所需的主要组件,并讨论了实现它们的困难:
保存和加载快照:保存快照涉及序列化状态机的状态并将数据写入文件,而加载是相反的过程。我们发现这相当简单。从状态机到磁盘上文件的流接口可以避免在内存中缓冲整个状态机状态;压缩流并对其应用校验和也可能有益。LogCabin首先将每个快照写入一个临时文件,然后在写入完成并已刷新到磁盘时重命名该文件;这将确保没有服务器在启动时加载部分写入的快照;
传输快照:传输快照涉及到实现InstallSnapshot RPC的Leader和Follower两部分的代码。这相当简单,Leader和Follower可以共用一些代码。这种传输的性能通常并不是很重要,因为这个Follower此时可能并没有正式成为集群的一员。另一方面,如果集群遭受其他故障,那可能就需要Follower尽快进行日志复制以回复集群的可用性;
消除不安全的日志访问和丢弃log entry:我们最初设计LogCabin并没有担心日志压缩,所以代码假设如果log entry i出现在日志中,log entry 1到i−1也会出现。采用日志压缩后,上述假设可能就不再正确;因为在进行AppenEntries RPC会进行log匹配,并会向前检查log entry,如果前面的log entry已经做了镜像并被丢弃,那就会出现log entry访问的越界情况,需要处理这个情况;
利用写时复制进行并发快照:可以基于操作系统的fork操作实现写时复制;
何时进行快照的决策:建议在开发期间可以每次apply一个log entry后就进行快照操作,这有利于快速发现问题。等开发完成后,可以加入相应的快照操作执行时间选择策略。
我们分块的进行开发和测试具有很大的挑战。因为在决定进行log丢弃时,很多组件都需要就绪,实现者应该仔细考虑实现和测试这些组件的顺序。