JRaft源码剖析1-节点启动过程

2023-10-01  本文已影响0人  王侦

1.RpcServer

添加系统自带的processors。
RaftRpcServerFactory#addRaftRequestProcessors()

AppendEntriesRequestProcessor
GetFileRequestProcessor
InstallSnapshotRequestProcessor
RequestVoteRequestProcessor
PingRequestProcessor
TimeoutNowRequestProcessor
ReadIndexRequestProcessor

AddPeerRequestProcessor
RemovePeerRequestProcessor
ResetPeerRequestProcessor
ChangePeersRequestProcessor
GetLeaderRequestProcessor
SnapshotRequestProcessor
TransferLeaderRequestProcessor
GetPeersRequestProcessor
AddLearnersRequestProcessor
RemoveLearnersRequestProcessor
ResetLearnersRequestProcessor

另外可以注册自定义的业务processor。

2.启动一个 JRaft 节点需要指定的核心参数

3.初始化和启动

            this.raftGroupService = new RaftGroupService(groupId, serverId, nodeOptions, rpcServer);
            // start raft node
            this.node = this.raftGroupService.start();

3.1 NodeImpl#init

创建NodeImpl,并调用NodeImpl#init()方法。

    public static Node createAndInitRaftNode(final String groupId, final PeerId serverId, final NodeOptions opts) {
        final Node ret = createRaftNode(groupId, serverId);
        if (!ret.init(opts)) {
            throw new IllegalStateException("Fail to init node, please see the logs to find the reason.");
        }
        return ret;
    }

NodeImpl#init()是个非常重要的方法,创建了很多核心组件:

3.1.1 周期任务调度器RepeatedTimer

对于Timer 接口,JRaft 提供了 DefaultTimer 和 HashedWheelTimer 两个实现类,其中前者基于 JDK 内置的 ScheduledExecutorService 实现,后者则基于单层时间轮算法实现。相对而言,HashedWheelTimer 较 DefaultTimer 在性能和精度层面表现更优,所以 JRaft 将其作为默认 Timer 应用于 RepeatedTimer 中。

RepeatedTimer#start
-> RepeatedTimer#schedule
-> 创建TimerTask,提交给timer(默认是HashedWheelTimer)执行,这里会调用 adjustTimeout 方法,用于调整计时周期(避免一直无法选出Leader)
-> RepeatedTimer#run
-> 会调用业务逻辑onTrigger(),如果本次任务调度完成,重新发起调度下一轮任务。计时器停止时回调onDestroy方法。

3.1.2 数据存储

JRaft 的数据存储层主要包含对三类数据的存储:日志数据、元数据,以及快照数据。

1)日志数据存储

NodeImpl#initLogStorage
-> 创建logStorage,基于 RocksDBLogStorage 实现类
-> 创建logManager,实现类:LogManagerImpl
-> LogManagerImpl#init

LogManagerImpl#init
-> this.logStorage.init()初始化日志存储服务
-> 基于日志初始化本地 logIndex 和 term 值
-> 创建对应的 Disruptor 队列,用于异步处理日志操作相关的事件,包括获取最新的 LogId、日志截断、重置日志数据存储服务,以及关闭日志管理器等。方法 LogManagerImpl#offerEvent 定义了往该 Disruptor 消息队列发送消息的逻辑,而具体处理消息的逻辑则有 StableClosureEventHandler 类实现。

RocksDBLogStorage#init
-> RocksDBLogStorage#initAndLoad 打开本地存储引擎 RocksDB,并从本地 conf 日志中恢复集群节点配置和 firstLogIndex 数据
-> RocksDBLogStorage#openDB,打开 RocksDB,并初始化对应的 ColumnFamily。两个ColumnFamily(名为 Configuration 的 ColumnFamily 用于存储集群节点配置相关的 LogEntry 实例;默认的 ColumnFamily 除了包含 Configuration 中的数据之外,还用于存储用户数据相关的 LogEntry 实例)
-> RocksDBLogStorage#load,从 conf 中加载集群节点配置,以及 firstLogIndex 值,并从本地剔除 firstLogIndex 之前的 conf 和 data 数据

另外,在NodeImpl#init会调用LogManagerImpl#checkConsistency方法对日志数据进行一致性校验。

LogManagerImpl#checkConsistency
-> 校验的逻辑主要是确保快照数据与当前数据的连续性,不允许存在数据断层。

2)元数据存储

元数据包括 currentTerm 值和当前节点的 votedId 信息。LocalRaftMetaStorage 实现类,基于本地文件系统采用 protobuf 协议对元数据执行序列化之后进行存储。LocalRaftMetaStorage 在初始化时(即执行 LocalRaftMetaStorage#init 方法期间)会从本地文件系统加载并反序列化元数据,以初始化 currentTerm 和 votedFor 属性值。运行期间对于这两个属性值的更改全部记录在内存中,并在关闭时(即执行 LocalRaftMetaStorage#shutdown 方法期间)将内存中的数据序列化后落盘。

NodeImpl#initMetaStorage
-> 实例化元数据存储服务,基于 LocalRaftMetaStorage 实现类
-> this.metaStorage.init()
-> 基于本地元数据恢复 currentTerm 和 votedId 属性值

3)快照数据存储

与日志数据存储模块的设计相类似,JRaft 针对快照数据存储模块同样采用了操作与存储相分离的策略,其中 SnapshotExecutor 主要负责生成和安装快照,而 SnapshotStorage 则主要负责针对快照文件的读写,以及从远端 Leader 节点拷贝快照数据。

NodeImpl#initSnapshotStorage
-> 创建和初始化SnapshotExecutorImpl
-> SnapshotExecutorImpl#init

SnapshotExecutor 的初始化过程主要是从本地加载最新的快照文件数据。

SnapshotExecutorImpl#init
-> 创建快照存储服务,基于 LocalSnapshotStorage 实现类
-> 初始化快照存储服务,主要工作是从本地删除除最后一次快照所生成的快照文件之外的其它快照数据文件
-> 打开快照文件读取器,加载快照元数据信息,加载最近一次的快照数据:FSMCaller#onSnapshotLoad 方法的实现

3.1.3 StateMachine和FSMCaller

JRaft 通过StateMachine接口抽象描述了 Raft 算法中引入的状态机。这也是 JRaft 向业务透传自己运行状态的核心接口,业务可以通过该接口捕获 JRaft 的运行事件。除了最核心的应用 LogEntry 中的指令外,还包括当前节点作为 LEADER 或 FOLLOWER 角色的启停事件、集群节点配置变更、快照加载与存储,以及集群运行错误与停机等。

JRaft 是如何将这些事件通知到业务的呢?具体点来说,通知到业务实现的状态机的呢?这就是状态机调度器 FSMCaller 所做的工作。

JRaft 通过调用 FSMCaller 中声明的方法实现将内部运行状态透传给业务,而 FSMCaller 在本地则基于 Disruptor 消息队列以事件的形式缓存这些内部状态,并通过异步的方式回调 StateMachine 接口声明的相应方法,这就是 FSMCaller 整体的运行逻辑。

NodeImpl#initFSMCaller
-> this.closureQueue = new ClosureQueueImpl(this.groupId),创建封装 Closure 的队列,基于 LinkedList 实现
-> FSMCallerImpl#init,创建和启动了一个 Disruptor 队列,用于异步处理各种状态机事件

FSMCallerImpl#enqueueTask 用于往该 Disruptor 队列写入具体的状态机事件,而 FSMCallerImpl 实现的FSMCaller 接口中声明的方法,基本上都是简单的调用了该方法。

ApplyTaskHandler处理消息。ApplyTaskHandler 通过调用 FSMCallerImpl#runApplyTask 方法对 Disruptor 消息队列中缓存的状态机事件进行处理。该方法本质上是一个事件分发器,基于具体的状态机事件类型调用对应的 do* 方法实现对事件的处理操作。

3.1.4 选票箱BallotBox

投票机制是 Raft 算法运行的基础,JRaft 在实现上为每个节点都设置了一个选票箱 BallotBox 实例,用于对 LogEntry 是否提交进行仲裁。

BallotBox 中持有的 ClosureQueue 实例是在前面介绍的 NodeImpl#initFSMCaller 中创建的,所以 FSMCaller 和 BallotBox 对象持有的 ClosureQueue 实例是同一个。BallotBox 负责往 ClosureQueue 中写数据,而 FSMCaller 则负责从 ClosureQueue 中读数据。

上一篇下一篇

猜你喜欢

热点阅读