無論是Paxos還是Raft,它們都是致力於維護一RSM(Replicated State Machine),如上圖所示。對於RSM來說,狀態存儲是非常關鍵的
(Replicated State Machine)狀態機:一致性group的節點的某個時刻的狀態(比如數據庫里x=1,y=1是一個狀態)轉移可以看成自動機里的一個狀態,所以叫狀態機。
Replicated Log: 包含了來自客戶端的用於執行在狀態機上的命令(操作)。
ETCD架構圖
Snapshot處理流程:https://www.jianshu.com/p/cf4f0d3ae253
從etcd的架構圖中可以看到,etcd主要分為四個部分:
HTTP Server: 用於處理用戶發送的API請求以及其它etcd節點的同步與心跳信息請求。
Store:這個模塊顧名思義,就像一個商店把etcd已經准備好的各項底層支持加工起來,為用戶提供五花八門的API支持,處理用戶的各項請求。
用於處理etcd支持的各類功能的事務,包括數據索引、節點狀態變更、監控與反饋、事件處理與執行等,是etcd對用戶提供的大多數API功能的具體實現。
Raft: raft 狀態機,raft強一致性算法的具體實現,是etcd的核心。
WAL:Write Ahead Log(預寫式日志),是etcd的數據存儲方式。除了在內存中存有所有數據的狀態以及節點的索引以外,etcd就通過WAL進行持久化存儲。WAL中,所有的數據提交前都會事先記錄日志。Entry表示存儲的具體日志內容。
隨着使用量的增加,當WAL文件中數據項內容過大達到設定值(默認為10000)時,會進行WAL的切分,同時進行snapshot操作,經過snapshot以后的WAL文件就可以刪除。而通過API可以查詢的歷史etcd操作默認為1000條。實際上數據目錄中有用的snapshot和WAL文件各只有一個,默認情況下etcd會各保留5個歷史文件。
故障快速恢復/回滾(undo)/重做(redo) :所有的修改操作都被記錄在WAL,可以通過執行所有WAL中記錄的修改操作,快速從最原始的數據恢復到之前的狀態。
通常,一個用戶的請求發送過來,會經由HTTP Server轉發給Store進行具體的事務處理,如果涉及到節點的修改,則交給Raft模塊進行狀態的變更、日志的記錄,然后再同步給別的etcd節點以確認數據提交,最后進行數據的提交,再次同步。
集群選主
Raft協議是用於維護一組服務節點數據一致性的協議。這一組服務節點構成一個集群,並且有一個主節點來對外提供服務。當集群初始化,或者主節點掛掉后,面臨一個選主問題。集群中每個節點,任意時刻處於Leader(主), Follower(從), Candidate(候選)這三個角色之一。
選舉特點如下:
•當集群初始化時候,每個節點都是Follower角色;
•集群中存在至多1個有效的主節點,通過心跳與其他節點同步數據;
•當Follower在一定時間內沒有收到來自主節點的心跳,會將自己角色改變為Candidate,並發起一次選主投票;當收到包括自己在內超過半數節點贊成后,選舉成功;當收到票數不足半數選舉失敗,或者選舉超時。若本輪未選出主節點,將進行下一輪選舉(出現這種情況,是由於多個節點同時選舉,所有節點均為獲得過半選票)。
•Candidate節點收到來自主節點的信息后,會立即終止選舉過程,進入Follower角色。
為了避免陷入選主失敗循環,每個節點未收到心跳發起選舉的時間是一定范圍內的隨機值,這樣能夠避免2個節點同時發起選主。
集群大小與容錯
集群的大小指集群節點的個數。根據 etcd 的分布式數據冗余策略,集群節點越多,容錯能力(Failure Tolerance)越強,同時寫性能也會越差。所以關於集群大小的優化,其實就是容錯和寫性能的一個平衡。
ETCD推薦使用【奇數】作為集群節點個數。原因有二:
一、奇數個節點與和其配對的偶數個節點相比(比如 3節點和4節點對比),容錯能力相同,卻可以少一個節點。
二、偶數個節點集群不可用風險更高,表現在選主過程中,有較大概率等額選票,從而出發下一輪選舉。
所以綜合考慮性能和容錯能力,etcd 官方文檔推薦的 etcd 集群大小是 3, 5, 7。至於到底選擇 3,5 還是 7,根據需要的容錯能力而定。
關於節點數和容錯能力對應關系,如下表所示:
日志復制
所謂日志復制,是指主節點將每次操作形成日志條目,並持久化到本地磁盤,然后通過網絡IO發送給其他節點。其他節點根據日志的邏輯時鍾(TERM)和日志編號(INDEX)來判斷是否將該日志記錄持久化到本地。當主節點收到包括自己在內超過半數節點成功返回,那么認為該日志是可提交的(committed),並將日志輸入到狀態機,將結果返回給客戶端。
這里需要注意的是,每次選主都會形成一個唯一的TERM編號,相當於邏輯時鍾。每一條日志都有全局唯一的編號。
主節點通過網絡IO向其他節點追加日志。若某節點收到日志追加的消息,首先判斷該日志的TERM是否過期,以及該日志條目的INDEX是否比當前以及提交的日志的INDEX跟早。若已過期,或者比提交的日志更早,那么就拒絕追加,並返回該節點當前的已提交的日志的編號。否則,將日志追加,並返回成功。
當主節點收到其他節點關於日志追加的回復后,若發現有拒絕,則根據該節點返回的已提交日志編號,發送其編號下一條日志。
主節點像其他節點同步日志,還作了擁塞控制。具體地說,主節點發現日志復制的目標節點拒絕了某次日志追加消息,將進入日志探測階段,一條一條發送日志,直到目標節點接受日志,然后進入快速復制階段,可進行批量日志追加。
按照日志復制的邏輯,我們可以看到,集群中慢節點不影響整個集群的性能。另外一個特點是,數據只從主節點復制到Follower節點。
安全性
截止此刻,選主以及日志復制並不能保證節點間數據一致。試想,當一個某個節點掛掉了,一段時間后再次重啟,並當選為主節點。而在其掛掉這段時間內,集群若有超過半數節點存活,集群會正常工作,那么會有日志提交。這些提交的日志無法傳遞給掛掉的節點。當掛掉的節點再次當選主節點,它將缺失部分已提交的日志。在這樣場景下,按Raft協議,它將自己日志復制給其他節點,會將集群已經提交的日志給覆蓋掉。
這顯然是不可接受的。
其他協議解決這個問題的辦法是,新當選的主節點會詢問其他節點,和自己數據對比,確定出集群已提交數據,然后將缺失的數據同步過來。這個方案有明顯缺陷,增加了集群恢復服務的時間(集群在選舉階段不可服務),並且增加了協議的復雜度。
Raft解決的辦法是,在選主邏輯中,對能夠成為主的節點加以限制,確保選出的節點已定包含了集群已經提交的所有日志。如果新選出的主節點已經包含了集群所有提交的日志,那就不需要從和其他節點比對數據了。簡化了流程,縮短了集群恢復服務的時間。
這里存在一個問題,加以這樣限制之后,還能否選出主呢?答案是:只要仍然有超過半數節點存活,這樣的主一定能夠選出。因為已經提交的日志必然被集群中超過半數節點持久化,顯然前一個主節點提交的最后一條日志也被集群中大部分節點持久化。當主節點掛掉后,集群中仍有大部分節點存活,那這存活的節點中一定存在一個節點包含了已經提交的日志了。