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

相关文章

AJAX请求上传下载进度监控实现方式

《AJAX请求上传下载进度监控实现方式》在日常Web开发中,AJAX(AsynchronousJavaScriptandXML)被广泛用于异步请求数据,而无需刷新整个页面,:本文主要介绍AJAX请... 目录1. 前言2. 基于XMLHttpRequest的进度监控2.1 基础版文件上传监控2.2 增强版多

Redis分片集群的实现

《Redis分片集群的实现》Redis分片集群是一种将Redis数据库分散到多个节点上的方式,以提供更高的性能和可伸缩性,本文主要介绍了Redis分片集群的实现,具有一定的参考价值,感兴趣的可以了解一... 目录1. Redis Cluster的核心概念哈希槽(Hash Slots)主从复制与故障转移2.

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

使用Python实现一键隐藏屏幕并锁定输入

《使用Python实现一键隐藏屏幕并锁定输入》本文主要介绍了使用Python编写一个一键隐藏屏幕并锁定输入的黑科技程序,能够在指定热键触发后立即遮挡屏幕,并禁止一切键盘鼠标输入,这样就再也不用担心自己... 目录1. 概述2. 功能亮点3.代码实现4.使用方法5. 展示效果6. 代码优化与拓展7. 总结1.

Mybatis 传参与排序模糊查询功能实现

《Mybatis传参与排序模糊查询功能实现》:本文主要介绍Mybatis传参与排序模糊查询功能实现,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、#{ }和${ }传参的区别二、排序三、like查询四、数据库连接池五、mysql 开发企业规范一、#{ }和${ }传参的

Docker镜像修改hosts及dockerfile修改hosts文件的实现方式

《Docker镜像修改hosts及dockerfile修改hosts文件的实现方式》:本文主要介绍Docker镜像修改hosts及dockerfile修改hosts文件的实现方式,具有很好的参考价... 目录docker镜像修改hosts及dockerfile修改hosts文件准备 dockerfile 文

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态