主節點選舉
每個節點有三種角色:
follower: 從節點,被動回覆leader和candidate的request。
leader: 主節點,處理client的寫request
candidate: leader的候選人
follower 會接受 leader 的心跳包,如果沒有收到 leader 的心跳包,將會在隨機時間之後變成 candidate 開始競選 leader。同時選舉出來的新主節點也是通過發送心跳來讓其餘的節點從 candidate 狀態轉換爲 follower 的。
投票前的準備
-
每個節點有一個固定的ID以在多個節點之間進行識別。
-
每個節點有一個任期號 term,從零開始,以後逐漸單向往上遞增。服務器重啓後需要知道當前的term 纔可以正確的跟其它節點交流,所以 term 是必須持久化的.
-
每個節點都可以向其他節點發起投票,要求其他的節點選擇自己作爲主節點。
-
爲了避免所有節點同時發起投票,每個節點會分配一個隨機的選舉超時時間 electionTimeout,也就是一個等待時間,等待時間過了之後纔可以發起投票。所以每個節點發起投票的時間都是不一樣的。這樣降低了提示投票導致選票被瓜分的情況。等待時間超時之後繼續隨機一個等待時間並開始下一輪選舉。
開始投票
那麼當一個節點的等待時間到了之後,該節點會轉換爲 Candidate,
- 先給自己投一票,並自增terms, 自增 term 表示當前任期內我已經投過票了(投給自己),所以任期結束。重置選舉超時計時器 electionTimeout。
- 通知其他節點,要求他們選擇自己,通知的時候需要帶上自己的 term,ID,最新的日誌 Index 。
投票節點收到投票請求的回覆
有2種情況
-
成功了。拿到一個節點的選票。
-
失敗了。此時要將自己的 term 更新爲對方回覆的 term。
而每收到一個節點的回覆,要判斷是不是以下情況:
-
當前是否已經獲得了半數以上節點的票,是的話贏得選舉,成爲leader;向其他節點發送心跳,宣誓主權。
-
沒有獲得半數以上節點的票。electionTimeout 時間到了,在超時之後自增當前任期 terms,重新隨機一個 electionTimeout 並開始下一個任期的選舉;
當然在這個過程中有可能發生:收到其他節點的心跳請求,說明leader已經選出了,那結束選舉,自己變成 follower,選舉結束;
節點收到其他節點的投票請求
-
如果對方的 term 小於自己的,則拒絕投票,並將自己的 term 返回給對方,讓投票的人跟上時間(更新自己的term);如果大於,則立刻轉爲 follower,並返回成功,告訴對方我已經成爲你的 follower 了。
-
如果1的情況都不符合,說明2者的任期相同。判斷當前任期內是否已經投過票了。是的話也要拒絕投票,因爲同一任期內不能多次投票。
-
如果步驟2中還沒有結果,此時再比較對方的數據和自己的哪一個更新(根據日誌的最新時間),如果對方沒有自己新,也要拒絕投票。
-
上述條件都滿足,將票投給對方,返回成功。並等待 electionTimeout。
最終一定會有一個節點勝出。
當選出了主節點之後,主節點會跟從節點都保持心跳,從節點每次收到心跳都要重置自己的等待定時器,這樣只要主節點跟從節點之間不失聯,從節點就永遠不會發生選舉。
而一旦失聯,其餘的從節點立刻就根據自己的等待時間 electionTimeout 再次開始選舉了。
上述過程中,主節點就是 leader, 從節點就是 follower,從節點失去主節點的心跳開始競爭選舉的階段就是 candidate。
整個過程中有三種RPC:
- RequestVote RPC:投票用的RPC
- AppendEntries RPC:主從保活的心跳RPC(同時同步也用這個RPC)
- InstallSnapshot RPC:主節點給落後太多的從節點發送快照。
整個過程有三個定時器:
- BroadcastTime : 主節點定時發送給從節點的心跳定時時間
- Election Timeout :從節點等待進行選舉的超時時間
- MTBT : 指的是單個服務器發生故障的間隔時間的平均數
三個定時器的超時時間應該是:
BroadcastTime << ElectionTimeout << MTBF
心跳時間一定要小於從節點等待的超時時間,從節點每次收到心跳都會重置這個等待時間。
一般BroadcastTime大約爲0.5毫秒到20毫秒,ElectionTimeout一般在10ms到500ms之間。大多數服務器的MTBF都在幾個月甚至更長。
主從同步,日誌複製
leader 選出來之後,如何保證主從之間的同步?
複製狀態機通常都是基於複製日誌實現的,每個節點都有各自的日誌文件,比如在Redis 中是 AOF 文件。
在不同服務器上的不同文件中,每條日誌記錄都有一個任期號和遞增的索引。
如果不同文件的索引和任期號都相同,說明這2個日誌一樣。也就說明2臺服務器上的數據是一致的。
那麼會有2種情況:
-
正常運行的情況,在主從數據都一致的情況下:
客戶端每發送一個寫命令給主節點,主節點會將寫命令 append 到主節點的日誌文件最後,然後將命令廣播給所有的從節點。當所有的從節點都收到這條命令之後,將執行結果返回給客戶端。如果這個過程有的從節點因爲網絡原因沒有收到,也會回給客戶端,但是對於失敗的命令會不斷重試。
因爲主機節點擁有所有從節點的最新的日誌 index 。當從節點回復了之後就更新其日誌 index。 -
從節點崩潰很久之後重啓,導致的主從數據差異很大:
-
主節點崩潰,導致重新選舉。
同時主從之間的心跳會不斷的發送主節點的log
有一個問題,主節點和從節點之間的日誌不同步,主節點領先從節點N條數據,然後主節點掛掉,新的主節點被選中,此時新主的數據其實不是全局最新的。
安全性和成員變化
如果重新選舉之後的主節點落後於從節點,從節點多餘的數據將會全部抹掉。
如果從節點的日誌比主節點少,主節點會減少日誌索引,直到找到最終的一致的地方
如果一個跟隨者的日誌和領導人不一致,那麼在下一次的附加日誌 RPC 時的一致性檢查就會失敗。在被跟隨者拒絕之後,領導人就會減小 nextIndex 值並進行重試。最終 nextIndex 會在某個位置使得領導人和跟隨者的日誌達成一致。當這種情況發生,附加日誌 RPC 就會成功,這時就會把跟隨者衝突的日誌條目全部刪除並且加上領導人的日誌。一旦附加日誌 RPC 成功,那麼跟隨者的日誌就會和領導人保持一致,並且在接下來的任期裏一直繼續保持
https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md