咱们对Raft协议已经进行了原理的解析,接下去咱们从通过SOFAJRaft 框架的核心流程剖析加深对Raft协议的理解。SOFAJRaft 是一个纯 Java 的 Raft 算法实现库, 基于百度 braft 实现而来, 使用 Java 重写了所有功能, 支持:
- 领导人选举和基于优先级的半确定性领导人选举。
- 日志复制和恢复。
- 快照和日志压缩。
- 只读成员(learner)。
- 集群成员管理,添加节点,删除节点,替换节点等。
- 完全并发复制。
- 容错能力。
- 非对称网络分区容忍性。
- 当法定人数同伴都死亡的解决方法。
- 管道复制
- 线性一致读,ReadIndex/LeaseRead。
额外扩展了一些功能:
- 对称网络分区容忍性
- 重启后的转移领袖、负载均衡场景实现
- 更丰富的指标统计展示
- 通过Jepsen一致性验证测试
- 包含嵌入式分布式KV存储实现
整体项目如下:
SOFAJRaft 的选举主要通过判单两个属性:LogIndex 和 Term;Term 即任期,LogIndex即提交到 raft group 中的任务都将序列化为一条日志存储下来,每条日志一个编号,在整个 raft group 内单调递增并复制到每个 raft 节点。可以理解为事务id。投票处理的逻辑主要在 com.alipay.sofa.jraft.core.NodeImpl中,主要有四个函数:
- 处理处理预投票请求 Message handlePreVoteRequest(request)
- 预投票 void preVote()
- 处理投票请求 Message handleRequestVoteRequest(request)
- 投票 electSelf()
整体流程如下:
- Candidate(候选人) 被 Election timeout触发
- Candidate 开始尝试发起 pre-vote 预投票
- Follower(追随者) 判断是否认可该 pre-vote request
- Candidate 根据 pre-vote response 来决定是否发起 RequestVoteRequest
- Follower 判断是否认可该 RequestVoteRequest
- Candidate 根据 response 来判断自己是否当选
使用预投票可以防止网络抖动等特殊原因引起的瞬时失联节点无故捣乱:候选者在发起投票之前,先发起预投票,如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选,也就不会抬升全局的 Term。
投票源码:
预投票源码:
SOFAJRaft 存储模块分为:
- Log 存储记录 Raft 配置变更和用户提交任务日志,把日志从 Leader 复制到其他节点上面;
- LogStorage 是日志存储实现,默认实现基于 RocksDB 存储,通过 LogStorage 接口扩展自定义日志存储实现;核心接口包括:
- 返回日志里的首/末个日志索引;
- 按照日志索引获取 Log Entry 及其任期;
- 把单个/批量 Log Entry 添加到日志存储;
- 从 Log 存储头部/末尾删除日志;
- 删除所有现有日志,重置下任日志索引。
- LogManager 负责调用底层日志存储 LogStorage,针对日志存储调用进行缓存、批量提交、必要的检查和优化。
- checkAndResolveConflict(entries, done)
- 检查Node节点,解决日志冲突。
- 配置管理器:缓存配置变更
- LogsInMemory缓存日志Entries
- offerEvent(done, type) Disruptor队列发布other类型事件
- appendToStorage(toAppend) 回调事件处理器StableClosureEventHandler存储日志
- checkAndResolveConflict(entries, done)
- LogStorage 是日志存储实现,默认实现基于 RocksDB 存储,通过 LogStorage 接口扩展自定义日志存储实现;核心接口包括:
- Meta 存储即元信息存储记录 Raft 实现的内部状态,比如当前 term,、投票给哪个节点等信息
- RaftMetaStorage 元信息存储实现,定义 Raft 元数据的 Metadata 存储模块核心 API 接口包括:
- 设置/获取 Raft 元数据的当前任期 Term;
- 分配/查询 Raft 元信息的 PeerId 节点投票。
- RaftMetaStorage 元信息存储实现,定义 Raft 元数据的 Metadata 存储模块核心 API 接口包括:
- Snapshot 存储用于存放用户的状态机 Snapshot 及元信息,用于Node重启重建整个状态机实例。
- SnapshotStorage 用于 snapshot 存储实现,定义 Raft 状态机的 Snapshot 存储模块核心接口包括:
- 设置 filterBeforeCopyRemote ,为 true 表示复制到远程之前过滤数据;
- 创建快照编写器;
- 打开快照阅读器;
- 从远程 Uri 复制数据;
- 启动从远程 Uri 复制数据的复制任务;
- 配置 SnapshotThrottle,SnapshotThrottle 用于重盘读/写场景限流的,比如磁盘读写、网络带宽。
- SnapshotExecutor 用于 snapshot 实际存储、远程安装、复制的管理。
- 状态机快照 doSnapshot(done)
- 安装快照 installSnapshot(request, response, done)。
- SnapshotStorage 用于 snapshot 存储实现,定义 Raft 状态机的 Snapshot 存储模块核心接口包括:
LogManager 调用日志存储 LogStorage 实现逻辑:
SnapshotExecutor 状态机快照和远程安装镜像实现逻辑:
通过存储的设计,在引入状态机机制,就可以完成一致性状态机。SOFAJRaft状态机组成有:
- StateMachine:业务逻辑实现的主要接口,状态机运行在每个 raft 节点上,提交的 task 如果成功,最终都会复制应用到每个节点的状态机上。,核心是 onApply(Iterator) 方法,应用通过 Node#apply(task) 提交的日志到业务状态机。
- FSMCaller:封装对业务 StateMachine 的状态转换的调用以及日志的写入等,一个有限状态机的实现,做必要的检查、请求合并提交和并发处理等。
SOFAJRaft Node节点利用日志复制完成数据同步,主要组成有:
- Replicator:用于 leader 向 follower 复制日志,也就是 raft 中的 appendEntries 调用,包括心跳存活检查等。
- ReplicatorGroup: 用于单个 RAFT Group 管理所有的 replicator,必要的权限检查和派发。
本文通过简单介绍了下SOFAJRaft的选举实现、存储机制、状态机和日志复制四个方面。基本上完成了Raft实现的核心实现。但SOFAJRaft还有更多核心及优化,因为篇幅原因没有进入细细剖析。如果咱们自实现Raft协议,基本上也是实现这几个主流程即可完成简版Raft了。关于Raft协议暂时先告一段路,接下去准备开写ZAB协议。
林淮川
毕业于西安交通大学;奈学教育《百万架构师训练营》讲师、企业级源码内源负责人,前大树金融高级架构师、技术委员会开创者、技术总监;前天阳宏业交易事业部技术主管;多年互联网金融行业(ToB)经验。