SOFAJRaft Snapshot 原理剖析 | SOFAJRaft 实现原理

2024-01-17 05:08

本文主要是介绍SOFAJRaft Snapshot 原理剖析 | SOFAJRaft 实现原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

SOFAStack(Scalable Open Financial Architecture Stack)是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。
640?wx_fmt=png
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。
本文为《剖析 | SOFAJRaft 实现原理》最后一篇,本篇作者胡宗棠,来自中国移动。 《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaft Lab/> ,文末包含往期系列文章。
SOFAJRaft:
https://github.com/sofastack/sofa-jraft

导读 


本文主要介绍 SOFAJRaft 在日志复制和管理中所采用的快照机制。 考虑到单独介绍 SOFAJRaft 中的快照机制原理和实现或许有一些唐突,我会先通过一个读者都能够看得明白的例子作为切入点,让大家对快照这个概念、它可以解决的主要问题,先有一个比较深刻的理解。

一、快照的概念与特点 


SOFAJRaft 是对 Raft 共识算法的 Java 实现。 既然是共识算法,就不可避免的要对需要达成共识的内容,在多个服务器节点之间进行传输,一般将这些共识的内容称之为日志块(LogEntry)。 如果读过《剖析 | SOFAJRaft 实现原理》系列前面几篇文章的同学,应该了解到在 SOFAJRaft 中,可以通过“节点之间并发复制日志”、“批量化复制日志”和“复制日志pipeline机制”等优化手段来保证服务器节点之间日志复制效率达到最大化。
但如果遇到下面的两个场景,仅依靠上面的优化方法并不能有效地根本解决问题:
  • 当对某个 SOFAJRaft Group 集群以新增节点方式来扩容,新节点需要从当前的 Leader 中获取所有的日志并重放到本身的状态机中,这对 Leader 和网络带宽都会带来不小的开销,还有其他方法可以优化或解决这个问题么?
  • 因为服务器节点需要存储的日志不断增加,但是磁盘空间有限,除了对磁盘卷大小扩容外,还有其他方式来解决么?
带着上面两个疑问,我们可以先来看一个大家日常生活中都会遇到的场景—重新安装操作系统,然后再通俗易懂地为大家介绍快照的概念与特点。
有一天,你的笔记本电脑的 Windows 操作系统因为某一些原因出现启动后多次崩溃问题,不管通过任何方式都没办法解决。 这时候,我们想到解决问题的第一个方案就是为这台电脑重新安装操作系统。 如果,我们平时偶尔为自己电脑的操作系统做过镜像,直接用之前的镜像文件即可快速还原系统至之前的某一时间点的状态,而无需从零开始安装 Windows 操作系统后,再花大量时间来重新安装一些自己所需要的系统软件(比如 Chrome 浏览器、印象笔记和 FoxMail 邮件客户端等)。
在上面的例子中,电脑操作系统的镜像就是系统某一时刻的“快照”,因为它包含了这一时刻,系统当前状态机的值(对于用户来说,就是安装了哪些的应用软件)。 在需要重新安装操作系统时候,通过镜像这一“快照”,可以很高效地完成还原电脑操作系统这个任务,而无需从零开始安装系统和相应的应用软件。 所以,我们这里可以为“快照”下一个简单的定义: 一种通过某种数据格式文件来保存系统当前的状态值的一个副本。
“快照”的特点,就如同它字面意思一样,可以分为“快”和“照”:
  • “快”:高效快捷,通过快照可以很方便的将系统还原至某一时刻的状态;
  • “照”:通过快照机制是保存系统某一时刻的状态值;

二、SOFAJRaft 的 Snapshot 机制 


2.1 SOFAJRaft Snapshot 机制的原理
读到这里,再去回顾第一节内容开头提出的两个问题,大家应该可以想到解决问题的方法就是通过引入快照机制。

1. 解决日志复制与节点扩容的瓶颈问题
在 SOFAJRaft 中,Snapshot 为当前 Raft 节点状态机的最新状态打了一个“镜像”单独保存,保存成功后在这个时刻之前的日志即可删除,减少了日志文件在磁盘中的占用空间。 而在 Raft 节点启动时,可以直接加载最新的  Snapshot 镜像,直接重放在此之后的日志文件即可。 如果设置保存 Snapshot 的时间间隔比较合理,那么节点加载镜像后重放的日志文件较少,启动速度也会比较快。 对于新 Raft 节点加入某个 SOFAJRaft Group 集群的场景,新节点可先从 Leader 节点上拷贝最新的 Snapshot 安装到本地状态机,然后拷贝后续的日志数据即可,这样可以在快速跟上整个 SOFAJRaft Group 集群进度的同时,又不会占用 Leader 节点较大的网络带宽资源。

2. 解决 Raft 节点故障恢复中的时效问题
在一个正常运行的 SOFAJRaft Group 集群中,当其中某一个 Raft 节点出现故障了(假设该故障的原因不是由磁盘损坏等不可逆因素导致的),该 Raft 节点修复故障重新启动时,如果节点禁用 Snapshot 快照机制,那么会重放所有本地的日志到状态机以跟上最新的日志,这样节点启动和达到日志备份完整的耗时均会比较长。 但是,如果此时节点开启了 Snapshot 快照机制,那么一切就会变得非常高效,节点只需要加载最新的 Snapshot 至状态机,然后以 Snapshot 数据的日志为起点开始继续回放日志至状态机,直到使得状态机达到最新状态。
640?wx_fmt=png
图1 在 Snapshot 禁用情况下集群节点扩容
640?wx_fmt=png
图2 在 Snapshot 启用情况下集群节点扩容
从上面两张 SOFAJRaft 集群的结构图上,可以很明显地看出在开启和禁用 Snapshot 时,扩容的新 Raft 节点需要从 Leader 节点传输过来不同的日志数量。 在禁用 Snapshot 情况下,新 Raft 节点需要把 Leade 节点内从起始的 T1 时刻至当前 T3 时刻这一时间范围内的所有日志都重新传至本地后提交给状态机。 而在开启 Snapshot 情况下,新 Raft 节点则无需像 图1 中那么逐条复制 T1~T3 时刻内的所有日志,而只需先从 Leader 节点加载最新的镜像文件 SnapshotIndexFile 至本地,然后仅复制 T3 时刻以后的日志至本地并提交状态机即可。
在这里可能有同学会有疑问: “在 图 1 中,从 Leader 节点传给新扩容的 Raft 节点的数据是 T1~T3 的日志,而 图2 中取而代之的是 SnapshotIndexFile 快照镜像文件,似乎还是不可避免额外的数据传输么? ”仔细看下图 2,会发现其中 SnapshotIndexFile 快照镜像文件是对 T1~T3 时刻内日志数据指令的合并(包括数集合[Add 1,Add 6,Add 4,Sub 3,Sub 4,Add 3]),也即为最终的数据状态值。

2.2 SOFAJRaft Snapshot 机制的实践应用
如果用户需开启 SOFAJRaft 的 Snapshot 机制,则需要在其客户端中设置配置参数类 NodeOptions 的“snapshotUri”属性(即为: Snapshot 文件的存储路径),配置该属性后,默认会启动一个定时器任务(“JRaft-SnapshotTimer”)自动去完成 Snapshot 操作,间隔时间通过配置类 NodeOptions 的“snapshotIntervalSecs”属性指定,默认 3600 秒。 定时任务启动代码如下:
640?wx_fmt=jpeg
从上面源码中可以看出,除了依靠定时任务触发以外,SOFAJRaft 也支持用户实现自定义的 Closure 类的回调方法,通过 Node 接口主动触发 Snapshot,并将结果通过 Closure 回调。 示例代码如下:
640?wx_fmt=jpeg
同时,用户在继承并实现业务状态机类“StateMachineAdapter”(该类为抽象类)时候需要,一并实现其中的  onSnapshotSave()/onSnapshotLoad() 方法:
  • onSnapshotSave() 方法:定期保存 Snapshot;
  • onSnapshotLoad() 方法:启动或者安装 Snapshot 后加载 Snapshot;
这里需要注意的是,上面的  onSnapshotSave() 和  onSnapshotLoad() 方法均会阻塞 Raft 节点本身的状态机,应该尽量通过异步或其他方式进行优化,避免出现阻塞的情况。 对于  onSnapshotSave() 方法,需要在保存快照文件后调用传入的参数 closure.run(status) 通知调用者保存成功或者失败; 具体的应用实践示例,可以参考 github 上的 Counter 计数器示例。
Counter 计数器示例:
https://www.sofastack.tech/projects/sofa-jraft/counter-example/

2.3 SOFAJRaft Snapshot 源码简析
上一节 handleSnapshotTimeout 方法的关键代码为最后一行  doSnapshot(null) 方法,深入代码后发现,最终调用的是 Snapshot 执行器(SnapshotExecutor)的  doSnapshot(final Closure done) 方法。 顺着这条源码线路,接下来看最为核心的 SnapshotExecutor 快照执行器实现类: SnapshotExecutorImpl,并推出 Raft 节点生成快照、安装快照和加载快照的整体的框架结构图。
SOFAJRaft 中 Snapshot 机制的核心类是 SnapshotExecutorImpl。 这个 SnapshotExecutor 快照执行器的核心方法是  doSnapshot(...) 和  installSnapshot(...)
doSnapshot(...) 方法: 该方法用于生成 Raft 节点的快照文件。 在该方法中,要先完成以下几个前置状态的校验和检查:
  • 是否处于 Stopped 状态;
  • 是否正在加载另外一个 Snapshot 文件;
  • 是否正在生成另外一个 Snapshot 文件;
  • 当前业务状态机已经提交的 Index 索引是否等于 Snapshot 最后保存的日志 Index 索引(如果两个值相等则表示,业务数据没有新增,无需再生成一次没有意义的 Snapshot);
在完成上面的状态校验和检查后,SOFAJRaft 调用了业务状态机实现的  onSnapshotSave() 方法,这里调用者可以通过参数传入的参数  closure.run(status) 通知自己保存 Snapshot 文件成功或者失败。 该方法具体的源代码如下:
640?wx_fmt=jpeg
installSnapshot(...) 方法: 该方法主要适用于 SOFAJRaft 集群中的 Follower 角色节点,在收到从 Leader 节点发送过来的安装 Snapshot 的 RPC 请求后,先会对当前节点的状态做一些前置状态的校验(这一点跟上面的 doSnapshot(...) 方法一样):
  • 是否处于 Stopped 状态;
  • 是否正在生成 Snapshot 文件;
  • 节点的 term 值是否跟 RPC 请求的 term 值一致;
  • Leader 节点发送过来的待安装 Snapshot 文件中的数据是否为最新的;
  • 是否正在安装前面的 Snapshot 文件;
在完成上面的状态校验和检查后,SOFAJRaft 在  loadDownloadingSnapshot() 中,调用了业务状态机实现的  onSnapshotLoad() 方法。 该方法具体的源代码如下:
640?wx_fmt=jpeg
结合上文对 SnapshotExecutor 快照执行器两个核心方法的解读,可以推出 Raft 节点生成快照、安装快照和加载快照的整体的框架结构图:
640?wx_fmt=jpeg
图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() 方法,合并日志数据集合并生成如上图所示的“snapshot2”文件,同时会对之前的日志进行一个裁剪。具体的做法是,本地清理删除上图中从“snapshot1”文件最后的 index+1 位置前的日志。
有读者朋友可能会问裁剪日志时,为什么不删除从“snapshot2”文件最后的 index+1 位置前的日志?这里考虑到的主要原因是,在Raft集群中, Leader 和 Follower 节点间做日志复制时,很可能会存在有部分 Follower 节点没有完全跟上 Leader 节点的情况,如果此时 Leader 节点裁剪了从“snapshot2”文件最后的 index+1 位置前的日志,那剩余未完成日志复制的 Follower 节点就无法从 Leader 节点同步日志,而只能通过 Leader 发送过来的 installSnapshotRequest 来完成同步最新的状态了(感兴趣的同学可以参考着研究下 SOFAJRaft 源码 LogManagerImpl 类的  setSnapshot() 方法实现)。

三、总结 


本文围绕 Snapshot 机制的概念、特点和原理,结合 SOFAJRaft 的 Snapshot 机制的实现细节详细阐述了 SOFAJRaft-Snapshot 基本流程,介绍了 Snapshot 的实践应用,并剖析用户的业务系统如何使用 SOFAJRaft-Snapshot 机制解决 Raft 日志体积增加占用磁盘空间和节点重启时重放所有日志过多占用网络带宽资源的问题。

SOFAJRaft 源码解析系列阅读 


本篇是《剖析 | SOFAJRaft 实现原理》系列的最后一篇,感谢 SOFAStack 社区的核心贡献者们的编写,也欢迎更多感兴趣的技术同学加入。
项目地址: SOFAJRaft:
https://github.com/sofastack/sofa-jraft
欢迎阅读原理解析系列,系统学习 SOFAJRaft 并让它帮助到你的项目:


本文归档在 sofastack.tack 网站,点击“阅读原文”即可查看。

640?wx_fmt=png

这篇关于SOFAJRaft Snapshot 原理剖析 | SOFAJRaft 实现原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/614955

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja