Raft 是分布式领域中应用非常广泛的一种共识算法,相比于此类算法的鼻祖 Paxos,具有更简单、更容易理解和实现的特点。TiKV 依赖的周边库 raft-rs 是参照 ETCD 的 RAFT 库编写的 RUST 版本。
本文不会详细介绍 RAFT 协议的原理或者实现,而是利用 raft-rs 的示例程序来讲解 raft-rs 如何使用。
Public API 简述
RawNode 结构体
TIKV 的 RAFT 对外接口是 RawNode 结构体:
pub struct RawNode<T: Storage> {
/// The internal raft state.
pub raft: Raft<T>,
...
}
这个结构体重要的接口有:
impl<T: Storage> RawNode<T> {
pub fn propose(&mut self, context: Vec<u8>, data: Vec<u8>) -> Result<()>
pub fn propose_conf_change(&mut self, context: Vec<u8>, cc: impl ConfChangeI) -> Result<()>
pub fn step(&mut self, m: Message) -> Result<()>
pub fn ready(&mut self) -> Ready
pub fn advance(&mut self, rd: Ready) -> LightReady
pub fn tick(&mut self) -> bool
}
Ready 结构体
我们知道,RAFT 中流转的 Log Entries 分为两种类型,一种是已经被大多数节点确认的 Log,叫做 committed entries,一种是暂时还未被大多数节点确认的 Log,就简单的叫做 Entries。两种 Log Entries 都可以通过 ready 函数接口从 RAFT 状态机中获取,这个就是 Ready 结构体:
pub struct Ready {
...
// 发到 Raft 中,但尚未持久化的 Raft Log
entries: Vec<Entry>,
light: LightReady,
...
}
pub struct LightReady {
// 已经持久化,并经过集群确认的 Raft Log。
committed_entries: Vec<Entry>,
// Raft 产生的消息,以便真正发给其他节点。
messages: Vec<Message>,
}
RAFT 状态机流转
了解了 RAFT 的大概接口和 Ready 的大概作用,我们就可以了解使用 RAFT 的大概流程了
Leader 角度
-
一阶段
在第一个阶段里,一份 Data 数据会被 RAFT 状态机转换为两份数据,一份数据转换为 Entries,然后落盘存储到 Disk,另一份数据转换为 Message,发送给其他 Follower 节点。
-
应用接受到请求 Data 信息
-
应用通过调用 RAFT 的 propose 接口将 Data 数据传递到 RAFT 状态机中去
-
应用调用 Ready 函数等待 从 RAFT 中获取 Ready 结构体,从 Ready 结构体中拿出 Entries 和 Message,分别进行落盘和转化为 MsgAppend 信息传递给 Follower。
-
应用还需要调用 advance 接口,来更新 RAFT 的内部状态,例如 Log index 信息,代表 Log Entries 已落盘。
-
二阶段
-
Follower 收到 Message 进行处理后 (例如落盘) 会将 Entries 的确认信息 MsgAppend Response 发送回给 Leader,值得注意的是这个 Message 中含有 Follower 已接收的最新的 Log Entries Index。
-
当 Leader 收到 Follower 节点的 Message 确认信息后,将会调用 step 函数将 Message 传递到 RAFT,RAFT 就会更新 Follower 的状态信息,尤其重要的是各个 Follower 的 Log Index 信息。
-
应用调用 Ready 接口后,就会将大多数 Follower 确认的 Log Entries 放到 Ready 结构体,应用就会收到已确认的 Committed Entries,可以对其进行 Apply。
-
之后依然还要调用 advance 接口,更新 RAFT 模块的状态,例如更新 Apply Index 信息,代表已提交。
-
最后,Leader 在给 Follower 发送 HeartBeat Msg 的时候,会带着 Leader 的 Committed Index,以此来告知 Follower 对应的 Log Entries 已经被提交,Follower 可以进行对应的 Apply 流程了。
到此为止,Leader 和 Follower 已全部接受到最新的 Data 信息。
Follower 角度
-
第一阶段
-
Follower 收到 Leader 的 Message 信息后,应用会调用 step 函数将 MsgAppend 传递到 RAFT。这个 MsgAppend 中含有 Follower 需要落盘的 Log Entries 信息
-
当用户调用 Ready 后,RAFT 就会将加工好的 Ready 结构体传递给应用,应用拿到 Log Entries 后进行落盘,然后将确认信息传递回 Leader。值得注意的是,RAFT 的 pipeline 要求 Leader 的落盘和 Message 的传递两个步骤是并行的,但是 Follower 必须落盘后才能调用 Transport Send,防止发送成功后,Follower 落盘失败。
-
最后依然需要调用 advance 接口,更新 RAFT 状态。
-
第二阶段
-
Follower 接受到 MsgHeartbeat 或者 MsgAppend 信息后,会从信息中获取 Leader 的 commit index
-
应用调用 Ready 后,Follower 会根据 Leader 的 Commit Index,计算出 Committed Entries,从而对这些信息进行 Apply
至此,Leader 和大多数 Follower 都将 Log Entries 落盘,并对其数据进行 Apply