hashicorp raft 代码分析3 - Snapshot

简介

上一篇文章介绍了Raft的主要工作流程。 这篇文章主要介绍raft的snapshot机制。Raft中引入Snapshot的目的是为了避免存储过多的Log。比如集群维护一个状态Status,他的可取值是Running和Stopped。假设集群运行的过程中Status经历了Stopped->Running->Stopped->Running这几个状态。如果在最后的Running状态有个新的节点加入到集群,在没有做snapshot的情况下就要发送3次Log以使新节点的同步到Running的状态。如果在最后一个Running状态后,集群做了一次Snapshot,那新加节点只需要安装最新的snapshot,明显数据量会少很多。

生成Snapshot

默认情况下Raft库每120s检查一次是否需要snapshot,默认设置下如果当前log id比上次成功的snapshot的log id大于8192时raft开始执行snapshot操作。下图为Raft库生成snapshot的流程。

+--------------------+                                                                              
| Raft lib           |                                                                                       
|   +--------------+ |    +----------------------+
|   |              | |    |Lib user              |
|   |              | |    |    +---------------+ |
|   |runSnapshots  | |    |    |SnapshotStore  | |
|   |go routine    |-+-D--+--->|Create         | |
|   |              | |    |    +---------------+ |
|   |              | |    |    +---------------+ |
|   |              |-+-E--+--->|FSMSnapshot    | |
|   |              | |    |    |Persist        | |
|   |              | |    |    +---------------+ |
|   |              | |    |    +---------------+ |
|   |              |-+-E--+--->|SnapshotSink   | |
|   |              | |    |    |               | |
|   +--------------+ |    |    +---------------+ |
|          |  |      |    |                      |
|          A  C      |    |                      |
|          |  |      |    |                      |
|   +--------------+ |    |    +---------------+ |
|   |runFSM        |-+-B--+--->|FSM interface  | |
|   |go routine    | |    |    |Snapshot       | |
|   +--------------+ |    |    |               | |
|                    |    |    +---------------+ |
+--------------------+    +----------------------+
  • A. Snapshot操作由runSnapshot go routine发起(最新log id和上次生成snapshot时的log id已经超过threshhold),这个go routine将Raft.reqSnapshotFuture请求发送给runFSM go routine
  • B. runFSM go routine调用FSM.Snapshot获得raft.FSMSnapshot接口
  • C. raft.FSMSnapshot接口返回给runSnapshot go routine
  • D. runSnapshot go routine使用raft.SnapshotStore接口创建一个raft.SnapshotSink接口
  • E. raft.FSMSnapshot.Persist以raft.SnapshotSink为参数,将数据持久化到存储介质上

raft.FSM, raft.SnapshotStore, raft.FSMSnapshot, raft.SnapshotSink都由使用者实现,因此由Raft库的使用者实际负责保存数据。这里的数据就是raft集群维护的各节点一致的状态,比如简介例子中的Status状态。这里再简单介绍下几个接口函数的作用。SnapshotStore.Create返回一个FSMSnapshot接口,FSMSnapshot.Persist用于将raft集群维护的状态序列化,然后将序列化后的数据写入SnapshotSink提供的IO(这里使用io.Writer)接口中。

Snapshot生成后,snapshot之前的Log会被删除,这样Log的数量就不会无限增长。

从Snapshot恢复数据

当出现节点关闭一段时间重新打开或者新节点加入集群时都需要从Snapshot恢复数据。当然如果在最新snapshot之后有其他Log,这些Log也会被Apply到待恢复的节点上。数据恢复主要有以下两种方式。

从当前节点的Snapshot中恢复数据

要使用本地Snapshot恢复数据,首先使用SnapshotStore.List获得所有snapshot,然后从最新的snapshot开始遍历,调用FSM.Restore恢复当前snapshot的数据,如果恢复失败则尝试下一个snapshot。

Leader发送InstallSnapshot RPC恢复follower状态

如果有新的节点加入集群,可以通过InstallSnapshot安装leader上最新的snapshot进而同步数据。

Follower收到InstallSnapshot的RPC request后,首先将Snapshot存储到SnapshotStore,存储的方法是将request body的内容直接复制到raft.SnapshotSink。然后将raft.restoreFuture发送到Raft.fsmMutateCh channel。runFSM go routine负责将raft.restoreFuture从channel中取出,再根据restore的snapshot id,打开对应的snapshot(这里是刚存储的snapshot),然后调用FSM.Restore恢复snapshot。FSM.Restore由库的使用者实现,因此如何反序列化这个snapshot完全由实现者自己定义。

总结

其实生成和恢复snapshot的过程就是一个序列化和反序列化数据的过程,当然这个过程中要考虑的就是这些数据存储的位置。hashicorp的Raft库定义了比较清晰的接口用于完成序列化,反序列化和存储接口,这给使用这个库带的人来了很大的方便。