序言
Etcd提供了一個樣例contrib/raftexample,用來展示如何使用etcd raft。這篇文章通過raftexample介紹如何使用etcd raft。
raft服務
raftexample是一個分布式KV資料庫,客戶端可以向集群的節點發送寫數據和讀數據,以及修改集群配置的請求,它使用etcd raft保持各集群之間數據的一致性。
etcd raft
etcd raft實現了raft論文的核心,所有的IO(磁碟存儲、網絡通信)它都沒有實現,它做了解耦。
它是一個狀態機,有數據作為輸入,經過當前狀態和輸入,得到確定性的輸出,即每個節點上都是一樣的。
raft應用架構
raft集群會由多個節點組成,客戶端的請求發送給raft leader,再由raft leader通過網絡通信在集群之中對請求達成共識。
集群中的每個節點從架構上都可以分為兩層:
- 應用層,負責處理用戶請求,數據存儲以及集群節點間的網絡通信,
- 共識層,負責相同和輸入數據和狀態,生成確定性的、一直的輸出,
共識層由etcd raft負責,應用層要負責業務邏輯,數據存儲和網絡通信不需要應用層實現,而是由不同的模塊負責,應用層負責起銜接存儲存儲和網絡通信即可。
應用層有3個重要組成部分:http API、kv store和raftNode。
http API
每個節點都會啟動一個http API用來接受客戶端請求,它只是接收請求,不對請求做處理。它會把客戶端的寫入請求PUT和查詢請求GET都交給kv store。
對於修改raft集群配置請求,它會生成ConfChange交給raftNode。
kv store
一個kv資料庫服務,它保存有一個kv db,用來存儲用戶數據。
- 對於查詢請求,它直接從db中讀取數據。
- 對於寫入請求,需要修改用戶數據,這就需要集群節點使用raft對請求達成共識,它把請求傳遞給raftNode。
raftNode
raftNode用來跟etcd raft交互,他需要:
- 把客戶端的寫請求,修改raft配置的請求交給etcd raft
- 銜接網絡通信跟etcd raft之間的橋樑,把etcd raft的消息發送出去,或接受到的raft消息交給raft
- 保存raft的WAL和snapshot。
對於寫請求,它會把請求數據編碼後發送給etcd raft,etcd raft會把寫請求封裝成raft的Propose消息MsgProp,編碼後的數據成為log Entry。因為raft並不關心具體的請求內容,它只需要保證每個集群節點在相同的log index擁有相同的log Entry,即請求即可。
raftNode還會啟動1個http server,用來集群節點之間的通信,傳遞raft消息,讓集群節點達成共識。它與http api是不同的,http api用來接收用戶請求。
raftNode與raft交互
raft模塊內部定義了一個Node接口,它代表了raft集群中的一個raft節點,它是應用層跟共識層交互的接口。
其中有幾個與數據傳遞相關函數的是:
- Propose:應用層通過此函數把客戶端寫請求傳遞raft。
- ProposeConfChange:應用層通過此函數把客戶端修改raft集群配置的請求傳遞raft。
- Step:應用層把收到的raft集群之間通信的消息傳遞給raft。
- Ready:raft對外的出口只有1個,就是Ready函數,Ready函數返回一個通道,應用層可以從這個通道中讀到raft要輸出的所有數據,這個數據被稱為Ready結構體,包括log entry,集群間的通信消息等。
- Advance:應用層處理完Ready結構體後,調用Advance通知raft,它已處理完剛讀到的Ready結構體,raft可以根據最新狀態生成下一個Ready結構體。
還有一個ApplyConfChange函數,當Ready結構體中包含修改raft集群配置的log entry時,應用層會調用此函數,把配置應用到raft。
raft架構
瞄完raft應用架構,可以從宏觀角度看一下raft是如何跟應用層對接的。
raft包內部有2個很重要的結構體:node和raft。
node結構體
node結構體(後續稱為raft.node)實現了Node接口,負責跟應用層對接,raft.node有個goroutine持續運行,應用層raftNode也有goroutine持續運行,raftNode調用raft.node的函數,每個函數都有對應的一個channel,用來把raftNode要傳遞給raft的數據,發送給raft.node。比如Propose函數的通道是proc,Step函數的通道是recvc。
raft結構體
raft結構體(後續稱為raft.raft)是raft算法的主要實現。
raft.node把輸入推給raft.raft,raft.raft根據輸入和當前的狀態數據生成輸出,輸出臨時保存在raft內,raft.node會檢查raft.raft是否有輸出,如果有輸出數據,就把輸出生成Ready結構體,並傳遞給應用層。
raft.raft應用層有一個storage,存放的是當前的狀態數據,包含了保存在內存中的log entry,但這個storage並不是raft.raft的,是應用層的,raft.raft只從中讀取數據,log entry的寫入由應用層負責。
幾個存儲相關的概念
WAL是Write Ahead Logs的縮寫,存儲的是log entry記錄,即所有寫請求的記錄。
storage也是存的log entry,只不過是保存在內存中的。
kv db是保存了所有數據的最新值,而log entry是修改數據值的操作記錄。
log entry在集群節點之間達成共識之後,log entry會寫入WAL文件,也會寫入storage,然後會被應用到kv store中,改變kv db中的數據。
Snapshot是kv db是某個log entry被應用後生成的快照,可以根據快照快速回復kv db,而無需從所有的歷史log entry依次應用,恢復kv db。
一個寫請求的處理過程
有了上面架構層面的了解,我們從宏觀的角度看一下一個寫請求被處理的過程。
- 客戶端把寫請求發給leader節點
- leader節點的http api接收請求,並把請求傳遞給kv store,kv sotre把寫請求發送給raftNode,raftNode把寫請求傳遞給raft.node
- leader節點的raft.node把寫請求轉化為log entry,並交給raft.raft,raft.raft生成發送給每一個follower的Append消息
- leader節點的raft.node取出raft.raft中的Append消息以及其他數據,封裝成Ready傳遞給raft.Node
- leader節點的raft.Node把Ready中的entry保存到storage,然後把Ready中的消息,發送給相應的節點
- follower節點的raft.Node收到消息,把消息傳遞給raft.node,raft.node退給raft.raft
- follower的raft.raft處理Append消息,進行匹配和校驗後,生成Append Response消息和保存log entry
- follower的raft.node從raft.raft獲取數據,然後生成Ready傳遞給raft.Node
- follower節點的raft.Node把Ready中的entry保存到storage,然後把Ready中的消息,發送給相應的節點
- leader節點的raft.Node收到消息,把消息傳遞給raft.node,raft.node退給raft.raft
- leader節點的raft.raft處理Append Response消息,然後檢查已經達成半數以上同意的log entry,更新已經被commit的log entry的index
- leader節點的raft.raft在創建Append等消息的時候,填寫了已被commited的log index,所以下次在生成消息,並發送給follower後,follower就根據committed log index提交本地的log entry
- 無論是leader,還是follower在生成Ready的時候,會包含已經被committed的log entry,這些entry是等待應用到kv store的,raftNode拿到Ready後,會把這些entry取出來,傳遞給kv store,kv store會修改key-value的最新值。
總結
本文從宏觀角度介紹了:
- 使用etcd raft應用的架構
- 使用etcd raft應用應當提供哪些功能供raft使用
- 應用是如何和etcd raft交互的
- etcd raft涉及到的存儲概念
- 一個寫請求從客戶端到在節點之間達成一致,應用到狀態機的過程
原文連結:http://lessisbetter.site/2019/08/19/etcd-raft-sources-arch/
本文作者:大彬,原創授權發布