TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析
作者:屈鵬
本文為 TiKV 源碼解析系列的第二篇,按照計劃首先將為大家介紹 TiKV 依賴的周邊庫 raft-rs 。raft-rs 是 Raft 算法的 Rust 語言實現。Raft 是分布式領域中應用非常廣泛的一種共識算法,相比于此類算法的鼻祖 Paxos,具有更簡單、更容易理解和實現的特點。
分布式系統的共識算法會將數據的寫入復制到多個副本,從而在網絡隔離或節點失敗的時候仍然提供可用性。具體到 Raft 算法中,發起一個讀寫請求稱為一次 proposal。本文將以 raft-rs 的公共 API 作為切入點,介紹一般 proposal 過程的實現原理,讓用戶可以深刻理解并掌握 raft-rs API 的使用, 以便用戶開發自己的分布式應用,或者優化、定制 TiKV。
文中引用的代碼片段的完整實現可以參見 raft-rs 倉庫中的 source-code 分支。
Public API 簡述
倉庫中的 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 Logsend_all(ready.messages); // 將 Raft 產生的消息發送給其他節點handle_committed_entries(ready.committed_entries.take());raft_group.advance(ready); }這段代碼中值得注意的地方是:
接下來的幾節將展開詳細描述。
Storage trait
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 使用中的潛在問題。
總結
以上是生活随笔為你收集整理的TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HTTP全解析
- 下一篇: c# 三种异步编程模型EAP(*)、