黄东旭解析 TiDB 的核心优势
600
2024-02-24
Raft 是分布式领域中应用非常广泛的一种共识算法,相比于此类算法的鼻祖 Paxos,具有更简单、更容易理解和实现的特点。TiKV 依赖的周边库 raft-rs 是参照 ETCD 的 RAFT 库编写的 RUST 版本。
本文不会详细介绍 RAFT 协议的原理或者实现,而是来讲解 raft-rs 如何使用。
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 /// This includes appending and applying entries or a snapshot, updating the HardState, /// and sending messages. The returned `Ready` *MUST* be handled and subsequently /// passed back via `advance` or its families. Before that, *DO NOT* call any function like /// `step`, `propose`, `campaign` to change internal state. pub fn advance(&mut self, rd: Ready) -> LightReady pub fn tick(&mut self) -> bool }值得注意的是,根据注释 ready 函数和 advance 需要联合使用,而且在两个函数调用期间不允许调用 step、propose、campaign 等等函数改变 RAFT 内部的状态。其实 advance 函数类似于迭代器的 next,在使用迭代器过程中不允许更改主体的状态。
我们知道,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 的大概接口和 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 或者 Append 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。
TICK 接口Tick 接口的作用是驱动 Raft 内部的逻辑时钟前进,并对超时进行处理。
比如对于 Follower 而言,如果它在 tick 的时候发现 Leader 已经失联很久了,便会发起一次选举;而 Leader 为了避免自己被取代,也会在一个更短的超时之后给 Follower 发送心跳。
值得注意的是,tick 也是会产生 Raft 消息的,为了使这部分 Raft 消息能够及时发送出去,在应用程序的每一轮循环中一般应该先处理 tick,然后处理 Ready。
<!-- 下面这行代码会帮助你自动生成神奇的目录,需要的话请不要删除哦~ --><div data-theme-toc="true"> </div>版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。