做者:屈鵬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 使用中的潛在問題。