前言
之前的兩篇文章更多的是在描述Raft算法的正常流程,沒有過多的去討論異常場景。
而實際在分布式系統中,我們更多的都是在應對網絡不可用、機器故障等異常場景,所以本篇來討論一下Raft協議的安全性,即在異常場景下是否會導致數據丟失、數據不一致等情況。
選舉限制
在Raft協議中,所有的日志條目都只會從Leader節點往Follower節點寫入,且Leader節點上的日志只會增加,絕對不會刪除或者覆蓋。
這意味着Leader節點必須包含所有已經提交的日志,即能被選舉為Leader的節點一定需要包含所有的已經提交的日志。因為日志只會從Leader向Follower傳輸,所以如果被選舉出的Leader缺少已經Commit的日志,那么這些已經提交的日志就會丟失,顯然這是不符合要求的。
這就是Leader選舉的限制:能被選舉成為Leader的節點,一定包含了所有已經提交的日志條目。
回看算法基礎中的RequestVote RPC:
參數 | 解釋 |
---|---|
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最后一條日志的索引 |
lastLogTerm | Candidate最后一條日志的任期 |
參數 | 解釋 |
---|---|
term | 當前任期,用於Candidate更新自己的任期 |
voteGranted | true表示給Candidate投票 |
請求中的lastLogIndex和lastLogTerm即用於保證Follower投票選出的Leader一定包含了已經被提交的所有日志條目。
- Candidate需要收到超過版本的節點的選票來成為Leader
- 已經提交的日志條目至少存在於超過半數的節點上
- 那么這兩個集合一定存在交集(至少一個節點),且Follower只會投票給日志條目比自己的“新”的Candidate,那么被選出的節點的日志一定包含了交集中的節點已經Commit的日志
日志比較規則(即上面“新”的含義):Raft 通過比較兩份日志中最后一條日志條目的索引值和任期號定義誰的日志比較新。如果兩份日志最后的條目的任期號不同,那么任期號大的日志更加新。如果兩份日志最后的條目任期號相同,那么日志比較長的那個就更加新。
日志提交限制
上圖按時間序列展示了Leader在提交日志時可能會遇到的問題。
- 在 (a) 中,S1 是領導者,部分的復制了索引位置 2 的日志條目。
- 在 (b) 中,S1 崩潰了,然后 S5 在任期 3 里通過 S3、S4 和自己的選票贏得選舉,然后從客戶端接收了一條不一樣的日志條目放在了索引 2 處。
- 然后到 (c),S5 又崩潰了;S1 重新啟動,選舉成功,開始復制日志。在這時,來自任期 2 的那條日志已經被復制到了集群中的大多數機器上,但是還沒有被提交。
- 如果 S1 在 (d) 中又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然后覆蓋了他們在索引 2 處的日志。反之,如果在崩潰之前,S1 把自己主導的新任期里產生的日志條目復制到了大多數機器上,就如 (e) 中那樣,那么在后面任期里面這些新的日志條目就會被提交(因為S5 就不可能選舉成功)。 這樣在同一時刻就同時保證了,之前的所有老的日志條目就會被提交。
任期2內產生的日志可能在(d)的情況下被覆蓋,所以在出現(c)的狀態下,Leader節點是不能commit任期2的日志條目的,即不能更新commitIndex。
在上圖最終狀態是(e)的情況下,commitIndex的變化應該是1->3,即在(c)的情況下,任期4在索引3的位置commit了一條消息,commitIndex直接被修改成3。
而任期2的那條日志會通過Log Matching Property最終被復制到大多數節點企且被應用。
Raft算法保證了以下特性:
- 如果兩個日志條目有相同的index和term,那么他們存儲了相同的指令(即index和term相同,那么可定是同一條指令,就是同一個日志條目)
- 如果不同的日志中有兩個日志條目,他們的index和term相同,那么這個條目之前的所有日志都相同
兩條規則合並起來的含義:兩個日志LogA、LogB,如果LogA[i].index=Log[i]B.index且LogA[i].term=Log[i].term,那么LogA[i]=Log[i]B,且對於任何n < i的日志條目,LogA[n]=LogB[n]都成立。(這個結論顯而易見的可以從日志復制規則中推導出來)