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

相关文章

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依