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

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2