Raft算法系列教程6:日志压缩
随着运行时间的增增长,日志信息也会变得越来越多,占有更多的空间。因此Raft采取了日志压缩的方法解决该问题,即将当前整个系统状态写入稳定存储的快照,然后该时间点之前的日志就可以丢弃掉,从而释放存储空间。
1 快照结构
从图中可见,快照包括以下几个部分内容:
lastIncludedIndex
:表明快照中最后一条日志的索引值。也就是说日志一直压缩到该索引值的位置。该值以前连续若干个索引值的日志被压缩为快照,而该值以后的日志则不在快照中。lastIncludedTerm
:表明快照中最后一条日志所在的任期值。state machine state
:复制状态机的当前状态。
集群中每一个服务器都可以独立地进行拍摄快照(只对已提交的日志进行快照的拍摄),其中lastIncludedIndex
与lastIncludedTerm
:值的存在时为了通过之前讲到的在日志复制中需要做的一致性检查。当服务器完成了该快照的写入之后,就可以将从快照中最后一条日志一直到先前所有的日志删除。
2 快照的发送
正常情况下,Leader的日志将会与Follower保持一致,但并不是所有情况都处于正常情况下,有时候可能因为Follower的反应缓慢或崩溃造成与Leader的日志不一致。所以有时候需要Leader将快照信息发送给Follower。快照信息是通过一个称为InstallSnapshot的RPC消息发送的,该消息的结构如下:
InstallSnapshot RPC | 由Leader调用并按顺序发送打包的快照到Follower。 |
---|---|
参数: | |
term | Leader的任期 |
leaderId | 用以Follower可以重定向客户端的请求到Leader |
lastIncludedIndex | 快照将替换直到该索引的所有日志条目 |
lastIncludedTerm | 快照中最后一条日志所在的任期值 |
offset | 快照文件的偏移量,简单来说就是该快照代表多少数量的连续个日志实体 |
data[] | 从offset开始,快照内的数据数组 |
done | 如果这是最后一个快照则为true,说明该快照之后的日志暂时不需要拍摄快照 |
快照的发送会出现以下几种情况:
(1)Follower的日志信息不包括快照中的日志信息,即缺少日志。
(2)Follower的日志信息与快照中的日志信息发生冲突。
(3)Follower的日志信息要多于快照中的日志信息。
至于前两种情况,Follower采取直接使用快照内容替代掉自己的日志。
Follower具有更多的日志信息的情况下,即Follower含有大于接收到的快照中的最后一条日志信息的索引的日志信息。那么直接使用快照代替快照中所包含的日志信息,至于快照之后的日志信息仍然保留。
2.1 疑问
为什么不像日志一样仅由Leader拍摄快照然后发送给Follower,而是允许每一个服务器独立生成快照信息呢?
答案很简单,为了减少带宽使用,以及资源的浪费。因为正常情况下Follower具有生成快照的所有信息,在自己本地直接生成快照所需要消耗的资源要远远小于通过网络发送所需要的资源。另外也是降低Leader设计的复杂。因为如果仅由Leader生成快照的话,Leader则需要在向Follower发送日志的同时,还要兼顾快照的发送。
2.2 存在的问题
存在两个问题会影响每个快照:
首先,服务器必须决定何时进行快照。如果服务器过于频繁地进行快照,则会浪费磁盘带宽和能量。如果快照太不频繁,则有耗尽其存储容量的风险,并且会增加重新启动期间加载日志所需的时间。一种简单的策略是在日志达到固定大小(以字节为单位)时拍摄快照。如果此大小设置为明显大于快照的预期大小,则用于快照的磁盘带宽开销将很小。
第二个问题是写快照可能要花费大量时间,拍摄快照会延迟正常操作。解决方案是使用写时复制技术,以便可以接受新的更新而不会影响快照的写入。例如,使用功能性数据结构构建的状态机自然支持这一点。或者,可以使用操作系统的写时复制支持(例如,Linux上的fork)来创建整个状态机的内存快照。