SOFAStack( Scalable Open Financial Architecture Stack)是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。git
本文为《剖析 | SOFAJRaft 实现原理》最后一篇,本篇做者胡宗棠,来自中国移动。《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,文末包含往期系列文章。github
SOFAJRaft:https://github.com/sofastack/sofa-jraft算法
本文主要介绍 SOFAJRaft 在日志复制和管理中所采用的快照机制。考虑到单独介绍 SOFAJRaft 中的快照机制原理和实现或许有一些唐突,我会先经过一个读者都可以看得明白的例子做为切入点,让你们对快照这个概念、它能够解决的主要问题,先有一个比较深入的理解。浏览器
SOFAJRaft 是对 Raft 共识算法的 Java 实现。既然是共识算法,就不可避免的要对须要达成共识的内容,在多个服务器节点之间进行传输,通常将这些共识的内容称之为日志块(LogEntry)。若是读过《剖析 | SOFAJRaft 实现原理》系列前面几篇文章的同窗,应该了解到在 SOFAJRaft 中,能够经过“节点之间并发复制日志”、“批量化复制日志”和“复制日志pipeline机制”等优化手段来保证服务器节点之间日志复制效率达到最大化。服务器
但若是遇到下面的两个场景,仅依靠上面的优化方法并不能有效地根本解决问题:网络
带着上面两个疑问,咱们能够先来看一个你们平常生活中都会遇到的场景—从新安装操做系统,而后再通俗易懂地为你们介绍快照的概念与特色。架构
有一天,你的笔记本电脑的 Windows 操做系统由于某一些缘由出现启动后屡次崩溃问题,无论经过任何方式都没办法解决。这时候,咱们想到解决问题的第一个方案就是为这台电脑从新安装操做系统。若是,咱们平时偶尔为本身电脑的操做系统作过镜像,直接用以前的镜像文件便可快速还原系统至以前的某一时间点的状态,而无需从零开始安装 Windows 操做系统后,再花大量时间来从新安装一些本身所须要的系统软件(好比 Chrome 浏览器、印象笔记和 FoxMail 邮件客户端等)。并发
在上面的例子中,电脑操做系统的镜像就是系统某一时刻的“快照”,由于它包含了这一时刻,系统当前状态机的值(对于用户来讲,就是安装了哪些的应用软件)。在须要从新安装操做系统时候,经过镜像这一“快照”,能够很高效地完成还原电脑操做系统这个任务,而无需从零开始安装系统和相应的应用软件。因此,咱们这里能够为“快照”下一个简单的定义:一种经过某种数据格式文件来保存系统当前的状态值的一个副本。框架
“快照”的特色,就如同它字面意思同样,能够分为“快”和“照”:异步
读到这里,再去回顾第一节内容开头提出的两个问题,你们应该能够想到解决问题的方法就是经过引入快照机制。
在 SOFAJRaft 中,Snapshot 为当前 Raft 节点状态机的最新状态打了一个“镜像”单独保存,保存成功后在这个时刻以前的日志便可删除,减小了日志文件在磁盘中的占用空间。而在 Raft 节点启动时,能够直接加载最新的 Snapshot 镜像,直接重放在此以后的日志文件便可。若是设置保存 Snapshot 的时间间隔比较合理,那么节点加载镜像后重放的日志文件较少,启动速度也会比较快。对于新 Raft 节点加入某个 SOFAJRaft Group 集群的场景,新节点可先从 Leader 节点上拷贝最新的 Snapshot 安装到本地状态机,而后拷贝后续的日志数据便可,这样能够在快速跟上整个 SOFAJRaft Group 集群进度的同时,又不会占用 Leader 节点较大的网络带宽资源。
在一个正常运行的 SOFAJRaft Group 集群中,当其中某一个 Raft 节点出现故障了(假设该故障的缘由不是由磁盘损坏等不可逆因素致使的),该 Raft 节点修复故障从新启动时,若是节点禁用 Snapshot 快照机制,那么会重放全部本地的日志到状态机以跟上最新的日志,这样节点启动和达到日志备份完整的耗时均会比较长。可是,若是此时节点开启了 Snapshot 快照机制,那么一切就会变得很是高效,节点只须要加载最新的 Snapshot 至状态机,而后以 Snapshot 数据的日志为起点开始继续回放日志至状态机,直到使得状态机达到最新状态。
图1 在 Snapshot 禁用状况下集群节点扩容
图2 在 Snapshot 启用状况下集群节点扩容
从上面两张 SOFAJRaft 集群的结构图上,能够很明显地看出在开启和禁用 Snapshot 时,扩容的新 Raft 节点须要从 Leader 节点传输过来不一样的日志数量。在禁用 Snapshot 状况下,新 Raft 节点须要把 Leade 节点内从起始的 T1 时刻至当前 T3 时刻这一时间范围内的全部日志都从新传至本地后提交给状态机。而在开启 Snapshot 状况下,新 Raft 节点则无需像 图1 中那么逐条复制 T1~T3 时刻内的全部日志,而只需先从 Leader 节点加载最新的镜像文件 Snapshot_Index_File 至本地,而后仅复制 T3 时刻之后的日志至本地并提交状态机便可。
在这里可能有同窗会有疑问:“在 图 1 中,从 Leader 节点传给新扩容的 Raft 节点的数据是 T1~T3 的日志,而 图2 中取而代之的是 Snapshot_Index_File 快照镜像文件,彷佛仍是不可避免额外的数据传输么?”仔细看下图 2,会发现其中 Snapshot_Index_File 快照镜像文件是对 T1~T3 时刻内日志数据指令的合并(包括数集合[Add 1,Add 6,Add 4,Sub 3,Sub 4,Add 3]),也即为最终的数据状态值。
若是用户需开启 SOFAJRaft 的 Snapshot 机制,则须要在其客户端中设置配置参数类 NodeOptions 的“snapshotUri”属性(即为:Snapshot 文件的存储路径),配置该属性后,默认会启动一个定时器任务(“JRaft-SnapshotTimer”)自动去完成 Snapshot 操做,间隔时间经过配置类 NodeOptions 的“snapshotIntervalSecs”属性指定,默认 3600 秒。定时任务启动代码以下:
从上面源码中能够看出,除了依靠定时任务触发之外,SOFAJRaft 也支持用户实现自定义的 Closure 类的回调方法,经过 Node 接口主动触发 Snapshot,并将结果经过 Closure 回调。示例代码以下:
同时,用户在继承并实现业务状态机类“StateMachineAdapter”(该类为抽象类)时候须要,一并实现其中的 onSnapshotSave()/onSnapshotLoad()
方法:
这里须要注意的是,上面的 onSnapshotSave()
和 onSnapshotLoad()
方法均会阻塞 Raft 节点自己的状态机,应该尽可能经过异步或其余方式进行优化,避免出现阻塞的状况。对于 onSnapshotSave()
方法,须要在保存快照文件后调用传入的参数 closure.run(status)
通知调用者保存成功或者失败;具体的应用实践示例,能够参考 github 上的 Counter 计数器示例。
Counter 计数器示例:https://www.sofastack.tech/projects/sofa-jraft/counter-example/
上一节 handleSnapshotTimeout
方法的关键代码为最后一行 doSnapshot(null)
方法,深刻代码后发现,最终调用的是 Snapshot 执行器(SnapshotExecutor)的 doSnapshot(final Closure done)
方法。顺着这条源码线路,接下来看最为核心的 SnapshotExecutor 快照执行器实现类:SnapshotExecutorImpl,并推出 Raft 节点生成快照、安装快照和加载快照的总体的框架结构图。
SOFAJRaft 中 Snapshot 机制的核心类是 SnapshotExecutorImpl。这个 SnapshotExecutor 快照执行器的核心方法是 doSnapshot(...)
和 installSnapshot(...)
:
doSnapshot(...)
方法:该方法用于生成 Raft 节点的快照文件。在该方法中,要先完成如下几个前置状态的校验和检查:
在完成上面的状态校验和检查后,SOFAJRaft 调用了业务状态机实现的 onSnapshotSave()
方法,这里调用者能够经过参数传入的参数 closure.run(status)
通知本身保存 Snapshot 文件成功或者失败。该方法具体的源代码以下:
installSnapshot(...)
方法:该方法主要适用于 SOFAJRaft 集群中的 Follower 角色节点,在收到从 Leader 节点发送过来的安装 Snapshot 的 RPC 请求后,先会对当前节点的状态作一些前置状态的校验(这一点跟上面的 doSnapshot(...)
方法同样):
在完成上面的状态校验和检查后,SOFAJRaft 在 loadDownloadingSnapshot()
中,调用了业务状态机实现的 onSnapshotLoad()
方法。该方法具体的源代码以下:
结合上文对 SnapshotExecutor 快照执行器两个核心方法的解读,能够推出 Raft 节点生成快照、安装快照和加载快照的总体的框架结构图:
图3 生成快照/安装快照/加载快照框架图
从上面的总体流程框架图中能够看到,在新扩容的 Raft 节点启动后(它为 Follower 角色),它获取到 Leader 节点发送的安装 Snapshot 的 RPC 请求(InstallSnapshotRequest)后,会在 T1 时刻先调用 SnapshotExecutor 执行器的 installSnapshot()
方法,本地生成如上图所示的“snapshot_1”数据文件。
而后,该 Follower 节点从 T2 时刻开始继续执行 SOFAJRaft 的日志复制流程,从 Leader 节点接收到后续的 LogEntry 日志文件(如上图所示的 [Add 5,Sub 2,Add 1] 日志数据集合)。
最后,在 T3 时刻,该 Follower 节点,调用 SnapshotExecutor 执行器的 doSnapshot()
方法,合并日志数据集合并生成如上图所示的“snapshot_2”文件,同时会对以前的日志进行一个裁剪。具体的作法是,本地清理删除上图中从“snapshot_1”文件最后的 index+1 位置前的日志。
有读者朋友可能会问裁剪日志时,为何不删除从“snapshot_2”文件最后的 index+1 位置前的日志?这里考虑到的主要缘由是,在Raft集群中, Leader 和 Follower 节点间作日志复制时,极可能会存在有部分 Follower 节点没有彻底跟上 Leader 节点的状况,若是此时 Leader 节点裁剪了从“snapshot_2”文件最后的 index+1 位置前的日志,那剩余未完成日志复制的 Follower 节点就没法从 Leader 节点同步日志,而只能经过 Leader 发送过来的 installSnapshotRequest 来完成同步最新的状态了(感兴趣的同窗能够参考着研究下 SOFAJRaft 源码 LogManagerImpl 类的 setSnapshot()
方法实现)。
本文围绕 Snapshot 机制的概念、特色和原理,结合 SOFAJRaft 的 Snapshot 机制的实现细节详细阐述了 SOFAJRaft-Snapshot 基本流程,介绍了 Snapshot 的实践应用,并剖析用户的业务系统如何使用 SOFAJRaft-Snapshot 机制解决 Raft 日志体积增长占用磁盘空间和节点重启时重放全部日志过多占用网络带宽资源的问题。
本篇是《剖析 | SOFAJRaft 实现原理》系列的最后一篇,感谢 SOFAStack 社区的核心贡献者们的编写,也欢迎更多感兴趣的技术同窗加入,项目地址:SOFAJRaft:https://github.com/sofastack/sofa-jraft
欢迎阅读原理解析系列,系统学习 SOFAJRaft 并让它帮助到你的项目: