1.为什么引入快照机制?
快照机制本质上也是一种对日志数据复制的优化手段。
两个问题:
- 1)因为日志数据需要落盘存储,当日志数据量大到磁盘空间无法容纳时,除了扩容是否还有其它的优化手段?
- 2)当一个新的节点加入 Raft 集群时需要重放集群之前接收到的所有指令以追赶上集群的数据状态,这一过程往往比较耗时和消费带宽,如何进行优化?
解决方法就是快照机制:
- 快照机制通过定期为本地的数据状态生成对应的快照文件,并删除对应的日志文件,从而降低对于磁盘空间的容量消耗。
- 当一个新的节点加入集群时,不同于从 Leader 节点复制集群在此之前的所有日志文件,基于快照机制该节点只需要从 Leader 节点下载安装最新的快照文件即可。
2.生成快照
如果在启动 JRaft 节点时指定了快照路径 snapshotUri,则表明业务希望启用快照机制。JRaft 节点会在初始化期间(即执行 Node#init 方法)启动快照计时器 snapshotTimer,用于周期性生成快照(默认周期为 1 小时)。该计时器的具体执行逻辑由 NodeImpl#handleSnapshotTimeout 方法实现,该方法会判断当前节点是否处于活跃状态,如果是则会异步调用 NodeImpl#doSnapshot 方法执行生成快照的操作。
NodeImpl#doSnapshot
-> SnapshotExecutorImpl#doSnapshot
- 1)SnapshotExecutor 已被停止,返回
- 2)正在安装快照,返回
- 3)正在生成快照,不允许重复执行,返回
- 4)状态机调度器最后应用的 LogEntry 已经被快照,说明没有新的数据可以被快照
- 5)可以被快照的数据量小于阈值,暂不生成快照
- 6)创建并初始化快照写入器,默认使用 LocalSnapshotWriter 实现类
- 7)向状态机调度器发布一个 SNAPSHOT_SAVE 事件用于异步生成快照文件,同时会绑定一个 SaveSnapshotDone 回调以感知异步快照生成的状态。
FSMCallerImpl#doSnapshotSave处理事件
- 1)构造快照元数据信息,封装当前被状态机应用的 LogEntry 的 logIndex 和 term 值,以及对应的集群节点配置信息
- 2)记录快照元数据
- 3)调用状态机 StateMachine#onSnapshotSave 方法生成快照
如果生成快照成功,需要调用 SnapshotWriter#addFile 方法将快照文件名和对应的元数据信息记录到快照元数据信息表中。这么做的目的除了能够让 JRaft 识别该快照文件,业务也可以在后续安装快照文件时读取到快照的元数据信息。
生成快照文件之后,回调 SaveSnapshotDone#run 方法,该方法以异步的方式将请求委托给 SaveSnapshotDone#continueRun 方法执行。
SaveSnapshotDone#continueRun
-> SnapshotExecutorImpl#onSnapshotSaveDone记录快照元数据信息,更新已经被快照的 logIndex 和 term 状态值,更新 LogManager 状态,并将本地已快照的日志剔除。
快照数据除了可以由本地生成,也可以是从 Leader 节点复制而来,如果从远端复制过来的快照数据相对于本地更新,则应该忽略本地生成快照文件的结果。SnapshotExecutor 定义了 SnapshotExecutorImpl#lastSnapshotIndex 和 SnapshotExecutorImpl#lastSnapshotTerm 两个字段用于记录最近一次快照对应的 logIndex 和 term 值,所以当生成快照成功之后需要更新这两个状态值。此外,既然相应的数据已经被快照,则表示对应的原生日志文件可以从本地存储系统中删除,从而节省存储空间。这一过程由 LogManager#setSnapshot 方法实现,该方法会对本地的日志数据执行截断处理。
3.安装快照
Replicator 期望给目标 Follower 节点复制日志数据时发现对应 logIndex 的数据已经变为快照文件,所以需要先给目标 Follower 节点安装快照。
Replicator#installSnapshot
- 1)如果当前正在给目标 Follower 或 Learner 节点安装快照文件,则直接返回;
- 2)否则,构造安装快照 InstallSnapshot RPC 请求对象,除了填充基本的状态数据外,其中还包含快照的远程访问地址,以及快照的元数据信息;
- 3)向目标节点发送安装快照的请求。
Follower 节点对于 InstallSnapshot 请求的处理过程,由 NodeImpl#handleInstallSnapshot 方法实现。
NodeImpl#handleInstallSnapshot
- 1)会完成一些基本的状态校验(具体实现与处理 AppendEntries 请求基本相同)
- 2)SnapshotExecutorImpl#installSnapshot
2-1)registerDownloadingSnapshot()尝试注册一个下载快照数据的 DownloadingSnapshot 任务;
A)如果当前 SnapshotExecutor 已被停止,则放弃注册新的任务;
B)否则,如果当前正在生成快照文件,则放弃注册新的任务;
C)否则,校验安装快照请求中指定的 term 值是否与当前节点的 term 值相匹配,如果不匹配则说明请求来源节点已经不再是 LEADER 角色,放弃为本次安装快照请求注册新的任务;
D)否则,校验本次需要安装的快照数据是否已在本地被快照,如果是则放弃注册新的任务;
E)否则,尝试为本次安装快照请求注册新的下载快照文件任务,并开始下载快照文件。
2-2)SnapshotStorage#startToCopyFrom从 Leader 节点下载快照文件到本地,并阻塞等待文件下载完成(下载快照文件的操作由拷贝器 LocalSnapshotCopier 完成,本质上是一个 RPC 交互的过程);
2-3)SnapshotExecutorImpl#loadDownloadingSnapshot从本地加载下载回来的快照文件。
快照文件的生成过程时已知具体生成快照数据的过程由业务负责完成,而此处加载快照数据的过程同样也需要业务实现,因为这些快照数据都是执行业务指令所生成的特定时刻的数据状态备份。JRaft 同样会向状态机调度器 FSMCaller 发布一个 SNAPSHOT_LOAD 类型的事件,并由 FSMCallerImpl#doSnapshotLoad 方法负责处理此类事件。
FSMCallerImpl#doSnapshotLoad
- 1)获取快照数据读取器SnapshotReader
- 2)获取快照元数据信息
- 3)本地已经应用的日志 logIndex 和 term 值相对于当前正在安装的快照更新,说明待加载的快照数据已经过期
- 4)调用状态机 StateMachine#onSnapshotLoad 方法加载快照。FSMCaller 通过调用状态机 StateMachine#onSnapshotLoad 方法将快照数据透传给业务,并由业务决定如何在本地恢复快照所蕴含的状态数据。
- 5)更新状态数据lastAppliedIndex和lastAppliedTerm 为快照数据的logIndex 和 term 值
SnapshotExecutor 在向 FSMCaller 发布 SNAPSHOT_LOAD 事件时会设置一个 InstallSnapshotDone 回调,用于感知加载快照数据的状态,如果操作正常则该回调会更新 SnapshotExecutor 本地的状态数据,包括最新被快照的 LogEntry 对应的 logIndex 和 term 值等。此外,还会调用 LogManager#setSnapshot 方法对本地已被快照的日志文件执行截断处理,以节省存储空间。
最后来看一下复制器 Replicator 对于安装快照请求 InstallSnapshot 的响应处理过程,由 Replicator#onInstallSnapshotReturned 方法实现。
Replicator#onInstallSnapshotReturned
- 1)关闭快照数据读取器
- 2)目标 Follower 节点运行异常
- 3)目标 Follower 节点拒绝本次安装快照的请求
- 4)目标 Follower 节点成功处理本次安装快照的请求,更新 nextIndex
- 5)给目标节点安装快照失败,清空 inflight 请求,重新发送探针请求
- 6)回调 CatchUpClosure
如果给目标 Follower 或 Learner 节点安装快照成功,则对应的复制器 Replicator 会更新下一个待发送的 LogEntry 索引值 Replicator#nextIndex 字段,并切换运行状态为 Replicate,接下去转为向目标 Follower 或 Learner 节点复制日志数据。