做者:屈鹏node
本文为 TiKV 源码解析系列的第二篇,按照计划首先将为你们介绍 TiKV 依赖的周边库 raft-rs 。raft-rs 是 Raft 算法的 Rust 语言实现。Raft 是分布式领域中应用很是普遍的一种共识算法,相比于此类算法的鼻祖 Paxos,具备更简单、更容易理解和实现的特色。git
分布式系统的共识算法会将数据的写入复制到多个副本,从而在网络隔离或节点失败的时候仍然提供可用性。具体到 Raft 算法中,发起一个读写请求称为一次 proposal。本文将以 raft-rs 的公共 API 做为切入点,介绍通常 proposal 过程的实现原理,让用户能够深入理解并掌握 raft-rs API 的使用, 以便用户开发本身的分布式应用,或者优化、定制 TiKV。github
文中引用的代码片断的完整实现能够参见 raft-rs 仓库中的 source-code 分支。算法
仓库中的 examples/five_mem_node/main.rs
文件是一个包含了主要 API 用法的简单示例。它建立了一个 5 节点的 Raft 系统,并进行了 100 个 proposal 的请求和提交。通过进一步精简以后,主要的类型封装和运行逻辑以下:数组
struct Node { // 持有一个 RawNode 实例 raft_group: Option<RawNode<MemStorage>>, // 接收其余节点发来的 Raft 消息 my_mailbox: Receiver<Message>, // 发送 Raft 消息给其余节点 mailboxes: HashMap<u64, Sender<Message>>, } let mut t = Instant::now(); // 在 Node 实例上运行一个循环,周期性地处理 Raft 消息、tick 和 Ready。 loop { thread::sleep(Duration::from_millis(10)); while let Ok(msg) = node.my_mailbox.try_recv() { // 处理收到的 Raft 消息 node.step(msg); } let raft_group = match node.raft_group.as_mut().unwrap(); if t.elapsed() >= Duration::from_millis(100) { raft_group.tick(); t = Instant::now(); } // 处理 Raft 产生的 Ready,并将处理进度更新回 Raft 中 let mut ready = raft_group.ready(); persist(ready.entries()); // 处理刚刚收到的 Raft Log send_all(ready.messages); // 将 Raft 产生的消息发送给其余节点 handle_committed_entries(ready.committed_entries.take()); raft_group.advance(ready); }
这段代码中值得注意的地方是:网络
RawNode 是 raft-rs 库与应用交互的主要界面。要在本身的应用中使用 raft-rs,首先就须要持有一个 RawNode 实例,正如 Node 结构体所作的那样。并发
RawNode 的范型参数是一个知足 Storage 约束的类型,能够认为是一个存储了 Raft Log 的存储引擎,示例中使用的是 MemStorage。app
在收到 Raft 消息以后,调用 RawNode::step
方法来处理这条消息。分布式
每隔一段时间(称为一个 tick),调用 RawNode::tick
方法使 Raft 的逻辑时钟前进一步。函数
使用 RawNode::ready
接口从 Raft 中获取收到的最新日志(Ready::entries
),已经提交的日志(Ready::committed_entries
),以及须要发送给其余节点的消息等内容。
在确保一个 Ready 中的全部进度被正确处理完成以后,调用 RawNode::advance
接口。
接下来的几节将展开详细描述。
Raft 算法中的日志复制部分抽象了一个能够不断追加写入新日志的持久化数组,这一数组在 raft-rs 中即对应 Storage。使用一个表格能够直观地展现这个 trait 的各个方法分别能够从这个持久化数组中获取哪些信息:
方法 | 描述 |
---|---|
initial_state | 获取这个 Raft 节点的初始化信息,好比 Raft group 中都有哪些成员等。这个方法在应用程序启动时会用到。 |
entries | 给定一个范围,获取这个范围内持久化以后的 Raft Log。 |
term | 给定一个日志的下标,查看这个位置的日志的 term。 |
first_index | 因为数组中陈旧的日志会被清理掉,这个方法会返回数组中未被清理掉的最小的位置。 |
last_index | 返回数组中最后一条日志的位置。 |
snapshot | 返回一个 Snapshot,以便发送给日志落后过多的 Follower。 |
值得注意的是,这个 Storage 中并不包括持久化 Raft Log,也不会将 Raft Log 应用到应用程序本身的状态机的接口。这些内容须要应用程序自行处理。
RawNode::step
接口这个接口处理从该 Raft group 中其余节点收到的消息。好比,当 Follower 收到 Leader 发来的日志时,须要把日志存储起来并回复相应的 ACK;或者当节点收到 term 更高的选举消息时,应该进入选举状态并回复本身的投票。这个接口和它调用的子函数的详细逻辑几乎涵盖了 Raft 协议的所有内容,代码较多,所以这里仅阐述在 Leader 上发生的日志复制过程。
当应用程序但愿向 Raft 系统提交一个写入时,须要在 Leader 上调用 RawNode::propose
方法,后者就会调用 RawNode::step
,而参数是一个类型为 MessageType::MsgPropose
的消息;应用程序要写入的内容被封装到了这个消息中。对于这一消息类型,后续会调用 Raft::step_leader
函数,将这个消息做为一个 Raft Log 暂存起来,同时广播到 Follower 的信箱中。到这一步,propose 的过程就能够返回了,注意,此时这个 Raft Log 并无持久化,同时广播给 Follower 的 MsgAppend 消息也并未真正发出去。应用程序须要设法将这个写入挂起,等到从 Raft 中获知这个写入已经被集群中的过半成员确认以后,再向这个写入的发起者返回写入成功的响应。那么, 如何可以让 Raft 把消息真正发出去,并接收 Follower 的确认呢?
RawNode::ready
和 RawNode::advance
接口这个接口返回一个 Ready 结构体:
pub struct Ready { pub committed_entries: Option<Vec<Entry>>, pub messages: Vec<Message>, // some other fields... } impl Ready { pub fn entries(&self) -> &[Entry] { &self.entries } // some other methods... }
一些暂时无关的字段和方法已经略去,在 propose 过程当中主要用到的方法和字段分别是:
方法/字段 | 做用 |
---|---|
entries(方法) | 取出上一步发到 Raft 中,但还没有持久化的 Raft Log。 |
committed_entries | 取出已经持久化,并通过集群确认的 Raft Log。 |
messages | 取出 Raft 产生的消息,以便真正发给其余节点。 |
对照 examples/five_mem_node/main.rs
中的示例,能够知道应用程序在 propose 一个消息以后,应该调用 RawNode::ready
并在返回的 Ready 上继续进行处理:包括持久化 Raft Log,将 Raft 消息发送到网络上等。
而在 Follower 上,也不断运行着示例代码中与 Leader 相同的循环:接收 Raft 消息,从 Ready 中收集回复并发回给 Leader……对于 propose 过程而言,当 Leader 收到了足够的确认这一 Raft Log 的回复,便可以认为这一 Raft Log 已经被确认了,这一逻辑体如今 Raft::handle_append_response
以后的 Raft::maybe_commit
方法中。在下一次这个 Raft 节点调用 RawNode::ready
时,即可以取出这部分被确认的消息,并应用到状态机中了。
在将一个 Ready 结构体中的内容处理完成以后,应用程序便可调用这个方法更新 Raft 中的一些进度,包括 last index、commit index 和 apply index 等。
RawNode::tick
接口这是本文最后要介绍的一个接口,它的做用是驱动 Raft 内部的逻辑时钟前进,并对超时进行处理。好比对于 Follower 而言,若是它在 tick 的时候发现 Leader 已经失联好久了,便会发起一次选举;而 Leader 为了不本身被取代,也会在一个更短的超时以后给 Follower 发送心跳。值得注意的是,tick 也是会产生 Raft 消息的,为了使这部分 Raft 消息可以及时发送出去,在应用程序的每一轮循环中通常应该先处理 tick,而后处理 Ready,正如示例程序中所作的那样。
最后用一张图展现在 Leader 上是经过哪些 API 进行 propose 的:
本期关于 raft-rs 的源码解析就到此结束了,咱们很是鼓励你们在本身的分布式应用中尝试 raft-rs 这个库,同时提出宝贵的意见和建议。后续关于 raft-rs 咱们还会深刻介绍 Configuration Change 和 Snapshot 的实现与优化等内容,展现更深刻的设计原理、更详细的优化细节,方便你们分析定位 raft-rs 和 TiKV 使用中的潜在问题。