0
1
1
0
专栏/.../

Raft-rs 最佳实践与使用

 LeoYang90  发表于  2022-03-11

Raft 是分布式领域中应用非常广泛的一种共识算法,相比于此类算法的鼻祖 Paxos,具有更简单、更容易理解和实现的特点。TiKV 依赖的周边库 raft-rs 是参照 ETCD 的 RAFT 库编写的 RUST 版本。

本文不会详细介绍 RAFT 协议的原理或者实现,而是来讲解 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
    
    /// 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,在使用迭代器过程中不允许更改主体的状态。

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 或者 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。

0
1
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论