本文檔主要面向 TiKV 社區開發者,主要介紹 TiKV 的系統架構,源碼結構,流程解析。目的是使得開發者閱讀文檔以後,能對 TiKV 項目有一個初步瞭解,更好的參與進入 TiKV 的開發中。node
須要注意,TiKV 使用 Rust 語言編寫,用戶須要對 Rust 語言有一個大概的瞭解。另外,本文檔並不會涉及到 TiKV 中心控制服務 Placement Driver(PD) 的詳細介紹,可是會說明一些重要流程 TiKV 是如何與 PD 交互的。git
TiKV 是一個分佈式的 KV 系統,它採用 Raft 協議保證數據的強一致性,同時使用 MVCC + 2PC 的方式實現了分佈式事務的支持。github
TiKV 的總體架構比較簡單,以下:算法
Placement Driver : Placement Driver (PD) 負責整個集羣的管理調度。架構
Node : Node 能夠認爲是一個實際的物理機器,每一個 Node 負責一個或者多個 Store。app
Store : Store 使用 RocksDB 進行實際的數據存儲,一般一個 Store 對應一塊硬盤。異步
Region : Region 是數據移動的最小單元,對應的是 Store 裏面一塊實際的數據區間。每一個 Region 會有多個副本(replica),每一個副本位於不一樣的 Store ,而這些副本組成了一個 Raft group。分佈式
TiKV 使用 Raft 算法實現了分佈式環境下面數據的強一致性,關於 Raft,能夠參考論文 「In Search of an Understandable Consensus Algorithm」 以及官網,這裏不作詳細的解釋。簡單理解,Raft 是一個 replication log + State Machine 的模型,咱們只能經過 leader 進行寫入,leader 會將 command 經過 log 的形式複製到 followers,當集羣的大多數節點都收到了這個 log,咱們就認爲這個 log 是 committed,能夠 apply 到 State Machine 裏面。函數
TiKV 的 Raft 主要移植 etcd Raft,支持 Raft 全部功能,包括:測試
Leader election
Log replicationLog compaction
Membership changesLeader transfer
Linearizable / Lease read
這裏須要注意,TiKV 以及 etcd 對於 membership change 的處理,跟 Raft 論文是稍微有一點不同的,主要在於 TiKV 的 membership change 只有在 log applied 的時候生效,這樣主要的目的是爲了實現簡單,但有一個風險在於若是咱們只有兩個節點,要從裏面移掉一個節點,若是一個 follower 還沒收到 ConfChange 的 log entry,leader 就當掉而且不可恢復了,整個集羣就無法工做了。因此一般咱們都建議用戶部署 3 個或者更多個奇數個節點。
Raft 庫是一個獨立的庫,用戶也能夠很是方便的將其直接嵌入到本身的應用程序,而僅僅只須要自行處理存儲以及消息的發送。這裏簡單介紹一下如何使用 Raft,代碼在 TiKV 源碼目錄的 /src/raft 下面。
首先,咱們須要定義本身的 Storage,Storage 主要用來存儲 Raft 相關數據,trait 定義以下:
pub trait Storage { fn initial_state(&self) -> Result<RaftState>; fn entries(&self, low: u64, high: u64, max_size: u64) -> Result<Vec<Entry>>; fn term(&self, idx: u64) -> Result<u64>; fn first_index(&self) -> Result<u64>; fn last_index(&self) -> Result<u64>; fn snapshot(&self) -> Result<Snapshot>; }
咱們須要實現本身的 Storage trait,這裏詳細解釋一下各個接口的含義:
initial_state:初始化 Raft Storage 的時候調用,它會返回一個 RaftState,RaftState 的定義以下:
pub struct RaftState { pub hard_state: HardState, pub conf_state: ConfState, }
HardState 和 ConfState 是 protobuf,定義:
message HardState { optional uint64 term = 1; optional uint64 vote = 2; optional uint64 commit = 3; } message ConfState { repeated uint64 nodes = 1; }
在 HardState 裏面,保存着該 Raft 節點最後一次保存的 term 信息,以前 vote 的哪個節點,以及已經 commit 的 log index。而 ConfState 則是保存着 Raft 集羣全部的節點 ID 信息。
在外面調用 Raft 相關邏輯的時候,用戶須要本身處理 RaftState 的持久化。
entries: 獲得 [low, high) 區間的 Raft log entry,經過 max_size 來控制最多返回多少個 entires。
term,first_index 和 last_index 分別是獲得當前的 term,以及最小和最後的 log index。
snapshot:獲得當前的 Storage 的一個 snapshot,有時候,當前的 Storage 數據量已經比較大,生成 snapshot 會比較耗時,因此咱們可能得在另外一個線程異步去生成,而不用阻塞當前 Raft 線程,這時候,能夠返回 SnapshotTemporarilyUnavailable 錯誤,這時候,Raft 就知道正在準備 snapshot,會一段時間以後再次嘗試。
須要注意,上面的 Storage 接口只是 Raft 庫須要的,實際咱們還會用這個 Storage 存儲 raft log 等數據,因此還須要單獨提供其餘的接口。在 Raft storage.rs 裏面,咱們提供了一個 MemStorage,用於測試,你們也能夠參考 MemStorage 來實現本身的 Storage。
在使用 Raft 以前,咱們須要知道 Raft 一些相關的配置,在 Config 裏面定義,這裏只列出須要注意的:
pub struct Config { pub id: u64, pub election_tick: usize, pub heartbeat_tick: usize, pub applied: u64, pub max_size_per_msg: u64, pub max_inflight_msgs: usize, }
id: Raft 節點的惟一標識,在一個 Raft 集羣裏面,id 是不可能重複的。在 TiKV 裏面,id 的經過 PD 來保證全局惟一。
election_tick:當 follower 在 election_tick 的時間以後尚未收到 leader 發過來的消息,那麼就會從新開始選舉,TiKV 默認使用 50。
heartbeat_tick: leader 每隔 hearbeat_tick 的時間,都會給 follower 發送心跳消息。默認 10。
applied: applied 是上一次已經被 applied 的 log index。
max_size_per_msg: 限制每次發送的最大 message size。默認 1MB。
max_inflight_msgs: 限制複製時候最大的 in-flight 的 message 的數量。默認 256。
這裏詳細解釋一下 tick 的含義,TiKV 的 Raft 是定時驅動的,假設咱們每隔 100ms 調用一次 Raft tick,那麼當調用到 headtbeat_tick 的 tick 次數以後,leader 就會給 follower 發送心跳。
咱們經過 RawNode 來使用 Raft,RawNode 的構造函數以下:
pub fn new(config: &Config, store: T, peers: &[Peer]) -> Result<RawNode<T>>
咱們須要定義 Raft 的 Config,而後傳入一個實現好的 Storage,peers 這個參數只是用於測試,實際要傳空。生成好 RawNode 對象以後,咱們就可使用 Raft 了。咱們關注以下幾個函數:
tick: 咱們使用 tick 函數按期驅動 Raft,在 TiKV,咱們每隔 100ms 調用一次 tick。
propose: leader 經過 propose 命令將 client 發過來的 command 寫入到 raft log,並複製給其餘節點。
propose_conf_change: 跟 propose 相似,只是單獨用來處理 ConfChange 命令。
step: 當節點收到其餘節點發過來的 message,主動調用驅動 Raft。
has_ready: 用來判斷一個節點是否是 ready 了。
ready: 獲得當前節點的 ready 狀態,咱們會在以前用 has_ready 來判斷一個 RawNode 是否 ready。
apply_conf_change: 當一個 ConfChange 的 log 被成功 applied,須要主動調用這個驅動 Raft。
advance: 告訴 Raft 已經處理完 ready,開始後續的迭代。
對於 RawNode,咱們這裏重點關注下 ready 的概念,ready 的定義以下:
pub struct Ready { pub ss: Option<SoftState>, pub hs: Option<HardState>, pub entries: Vec<Entry>, pub snapshot: Snapshot, pub committed_entries: Vec<Entry>, pub messages: Vec<Message>, }
ss: 若是 SoftState 變動,譬如添加,刪除節點,ss 就不會爲空。
hs: 若是 HardState 有變動,譬如從新 vote,term 增長,hs 就不會爲空。
entries: 須要在 messages 發送以前存儲到 Storage。
snapshot: 若是 snapshot 不是 empty,則須要存儲到 Storage。
committed_entries: 已經被 committed 的 raft log,能夠 apply 到 State Machine 了。
messages: 給其餘節點發送的消息,一般須要在 entries 保存成功以後才能發送,但對於 leader 來講,能夠先發送 messages,在進行 entries 的保存,這個是 Raft 論文裏面提到的一個優化方式,TiKV 也採用了。
當外部發現一個 RawNode 已經 ready 以後,獲得 Ready,處理以下:
持久化非空的 ss 以及 hs。
若是是 leader,首先發送 messages。
若是 snapshot 不爲空,保存 snapshot 到 Storage,同時將 snapshot 裏面的數據異步應用到 State Machine(這裏雖然也能夠同步 apply,但 snapshot 一般比較大,同步會 block 線程)。
將 entries 保存到 Storage 裏面。
若是是 follower,發送 messages。
將 committed_entries apply 到 State Machine。
調用 advance 告知 Raft 已經處理完 ready。
(未完待續…) 做者:唐劉