Leader
很快,泥坑銀行就在回音山谷和海盜島建立了網點。這時候,兔小姐就對豬爸爸說到:『豬爸爸,現在我們已經有三個銀行網點了,那么我們是不是可以允許客戶在三個地方都可以進行交易呢?』
豬爸爸想了想,說到:『恐怕不行,兔小姐。』
兔小姐奇怪的回復到:『為什么?,客戶在任何地方交易,我們不是都可以先記錄下來,然后通知其他兩個地方,如果多數銀行都確認了這筆交易,這不就行了嗎?』
『因為我們的交易記錄都是有唯一 ID,而且這個 ID 都是單調加 1 遞增的。假設現在我們的交易記錄 ID 是 10 了,如果我們允許在多個地方同時交易,譬如在泥坑小鎮和回音山谷,那么這次交易的記錄 ID 都是 11,這時候,海盜島就會收到兩個 ID 都是 11 的交易記錄,海盜島這邊沒法區分到底哪一個是正確的記錄了。』
『嗯,這么說起來倒是的,但我們現在可是在三個地方都建立了銀行網點,如果不使用,真的很可惜。』
『兔小姐,我們最開始在三個地方建立銀行網點的目的就是為了完全保證數據的安全性,也就是如果一個地方出現了問題,我們的系統仍然能夠正常的工作。』
『我明白了,豬爸爸。也就是說,我們的系統雖然部署在了三個地方,但同時只有一個能對外提供服務是吧?』
『是的,兔小姐。』
『那么,豬爸爸,我有個問題。我們是如何知道哪一個能對外提供服務呢?因為現在我們有三個網點,很有可能每個網點都認為自己能對外提供服務了。』
『這是個好問題,我們需要有一套機制,能讓這三個網點自己選出一個 Leader 網點,對外提供服務。同時,如果這個 Leader 網點出現了故障,其他兩個網點能夠知道并再次選出一個 Leader 網點對外提供服務。』
豬爸爸接著道:『為了容易說明,我這里以我們實際的選舉為例吧。假設現在有三個成員,A,B 和 C,他們三人會相互投票選出一個領導。這里我們先定義三種狀態,Leader,Candiate 和 Follower。最開始三個人都是 Follower 狀態,然后他們如果決定要選舉了,就變成 Candiate 狀態,如果一個 Candiate 收到了大多數的選票,那么這個 Candiate 就變成了 Leader。而這時候其他的成員都重新變成 Follower,只有 Leader 能跟外部進行交互。我這么解釋你大概能明白吧,兔小姐?』
兔小姐:『是的,豬爸爸,我大概能明白這三種狀態,也就是只要一個網點成為了 Leader,這個網點才能對外提供服務吧。』
好吧,又輪到作者吐槽自己吐槽了,現實中銀行這么多個網點如果每次只能有一個主的銀行網點能對外提供服務,那么這個銀行應該會被客戶給投訴了。但這里我們就假設這樣吧。在 Raft 里面,每次只會有一個節點對外提供服務,這個節點就是 Leader,而其他的節點就叫做 Follower。當節點開始競選的時候,它們就會從 Follower 變成 Candidate。
Election
豬爸爸喝了一口水,繼續說到:『是的,兔小姐。我們繼續以成員 A,B,C 為例。那我們現在要面臨的第一個問題,就是這三個成員如何選出一個 Leader?』
豬爸爸:『我們先假設三個成員地上都有很多小石子,但他們手上都只有 0 顆。只有某個成員碰到了一些事件,他才會從地上撿起一顆或者多顆石子。到底是什么事件,我們稍后再詳細解釋。』
豬爸爸接著道:『最開始,我們知道三個成員都是 Follower。然后,我們約定一個時間,譬如 10 分鐘之后吧,各個成員各自開始選舉,變成了 Candidate,同時,各自從地上撿起來一顆石子。』
兔小姐:『看來,這就是你上面說的事件,也就是當一個 Follower 變成 Candidate 的時候,也就是自己開始新一輪選舉的時候,就給自己加一顆石子吧。』
『非常正確,兔小姐。』然后豬爸爸接著說道:『 Candidate 會先給自己投一票,然后會給其他的幾個成員發送投票信息,讓它們選舉自己成為 Leader。如果一個 Candidate 知道自己已經得到了大多數的選票,那么就能成為 Leader 了。』
『獲得大部分投票就成為 Leader 這個就跟我們自己的選舉一樣的。但是,豬爸爸,自己給自己投一票我能理解,但其他人為什么要給我投票呢?』
『這是個好問題,兔小姐,你還記得我前面說的石子吧?』
『當然記得,豬爸爸,當開始選舉的時候,就給自己加一顆石子。』
『是的,兔小姐。首先我們來考慮初始情況,這時候我們還沒進行任何交易,交易記錄還是 0。當一個 Candidate 給其他成員發送投票消息的時候,會帶上自己的石子數量。當其他成員收到投票消息,如果發現自己的石子數量比收到的投票信息消息的石子數量要少,就給這個 Candidate 投票,同時自己變成跟隨者,從地上撿起來足夠多的石子直到自己的石子數量跟投票消息里面的一樣。』
『如果自己的石子數量比投票消息的石子數量要多呢,我們如何處理?』
『兔小姐,如果是這樣,那么就直接丟棄這條消息。我們通常不管石子數量比自己當前手上石子數量少的消息。』
『這主意不錯,如果自己手上的石子數量跟投票消息里面的一樣呢,我們又如何處理?』
『這個就要看自己是不是已經給其他人或者自己投票了,如果我已經給其他人或者自己投票了,我就給你回復一條拒絕消息。』
『我討厭被拒絕。』兔小姐很傷心的說道,不過我想兔先生可不敢拒絕她。
『沒辦法,為了保證選舉的安全,我們必須這樣。所以對于一輪選舉來說,它可能會碰到三種情況,自己變成了 Leader,其它節點變成了 Leader,以及沒有選出 Leader。』
『為什么會沒選出 Leader 呢?』
『考慮到這種情況,三個成員 A,B,C ,最開始石子數量都是 0,然后他們同時開始選舉,先都給自己投了一票,這樣所有人的石子數量現在都是 1,所以無論誰收到其他人的消息,都會回復拒絕,這樣我們必須開始下一輪選舉』
『但下一輪選舉也可能沒有選出來吧。』
『是的,兔小姐,你還記得我之前提到的 10 分鐘吧。』
『記得,10 分鐘之后,都開始選舉。』
『如果每次大家重新開始選舉的時間都是一樣,很有可能都選不出來。所以我們可以約定一個時間范圍,譬如 10 到 20 分鐘,各個成員會隨機在這個時間段里面選一個時間來等待,這樣就能錯開選舉了。』
『這主意不錯,這樣選出來 Leader 的概率就大了很多。』
好了,說了這么多,該說說實際的 Raft 了。上面的石子數量就是 Raft 里面的 Term。每次開始一輪新的選舉,Term 就加 1。如果選出了 Leader,那么就會 一直維持這個 Term,直到下一次選舉。上面的 10 分鐘就是 election timeout。
在實際 Raft 中,還有一些復雜的 corner case,譬如如果選出了 Leader,但另一個 Candidate 有更高的 Term,這樣很可能會讓 Leader 變成 Follower,或者讓 Follower 給這個更高 Term 的 Candidate 重新投票。為了解決這樣的問題,我們可以考慮 Pre-Vote,也就是一個 Follower 如果要開始投票,它并不會立刻變成 Candidate,給自己 Term + 1,而是會先變成 Pre-Candidate 的狀態,用當前的 Term 去問其他的節點能否給自己投票,如果收到了大多數的同意消息,那么才會變成 Candidate 繼續后面的選舉流程。
另外,我們也可以考慮 Check Quorum,當一個 Follower 收到了更高的 Term 選舉消息,如果它確信當前集群還在正在工作,也就是在 election timeout 的時間里面仍然收到了 Leader 的消息,那么這個 Follower 會直接丟掉這個消息。
這里我們只討論了 Log 都沒有的情況,對于已經有 Log 的情況,選舉情況還要做一些其它判斷,我們后續再說明。
Keepalive
豬爸爸休息了下,繼續說道:『現在我們解決了第一個問題,就是如何選出一個 Leader。下面,我們就要面臨兩個問題。一個是如何讓 Leader 一直能正常工作。另一個就是萬一 Leader 出現了問題,其它的 Follower 如何知道,并開始重新選舉?』
『這看起來有點復雜。』
『其實一點也不,兔小姐。當我們選出來 Leader 之后,Leader 就會就會開始處理外面的請求。你還記得我前面說的我們的安全交易模型吧?必須大多數節點都記錄了這筆交易,我們才能實際交易。』
『當然記得,豬爸爸。』
『因為現在只有 Leader 能處理交易,所以每筆交易,Leader 都要發給 Follower,這樣 Leader 和 Follower 之間就有了通信。所以只要我們一直有交易處理,這條通信鏈條就不會斷掉,Leader 就一直能維持自己是 Leader 的狀態了。』
『嗯,是這樣的。但如果到了晚上,沒有交易,怎么辦呢?』
『這是個好問題,兔小姐。所以 Leader 會定期給 Follower 發送一條消息,說我現在還是 Leader,這個消息其實就是跟上面說的發送交易記錄的作用一樣的,只是讓 Follower 知道 Leader 還在就可以了。』
『這主意不錯,那如果很長一段時間 Follower 沒收到 Leader 發過來的消息,那我們怎么辦呢?』
『還記得我前面說的 10 分鐘吧,我們可以繼續約定,如果 10 分鐘內,Follower 沒有收到 Leader 的任何消息,那么 Follower 就認為 Leader 有問題了,這樣 Follower 就開始重新選舉。』
在 Raft 里面,如果選出來一個 Leader,Leader 會定期給 Follower 發送心跳,這個定期的時間我們通常叫做 heartbeat timeout,如果 Follower 在 election timeout 的時間里都沒收到 Leader 的消息,就開始新一輪的選舉。Heartbeat timeout 的時間要比 election timeout 小很多,譬如 election timeout 如果是 10s,那么 heartbeat timeout 可能就是 2s 或者 3s。
More about Election
兔小姐聽完了豬爸爸的回答,仔細想了想,說道『豬爸爸,你前面說的重新選舉,貌似假設的是初始情況,沒有任何交易記錄的情況。但我們選出了 Leader,這時候可能進行了很多交易了,那么這時候 Follower 再選舉 Leader 還有啥需要注意的呢?』
『這個問題非常的好,兔小姐!』豬爸爸由衷的贊嘆道。『之前,我們僅僅是通過石子數量來決定是否成為 Leader,但這樣是遠遠不夠的,我們還必須通過各自交易記錄的數量來最終確定是否能成為 Leader。』
『要通過交易記錄數量來確定?』兔小姐不解的問道。
『是的,兔小姐。你還記得最開始我說的交易記錄的特性吧。每次交易,我們都會使用一個遞增的唯一 ID 來標識記錄。』
『我當然記得,豬爸爸』
『現在我們回到銀行網點這邊,假設它們之間選出泥坑小鎮作為了 Leader,那么客戶就能在泥坑小鎮進行交易了。一段時間之后,泥坑小鎮 這邊進行了 10 次交易,也就是最后一次交易記錄 ID 是 10。因為一次交易必須大部分銀行網點都確定收到了這次交易記錄,所以一定有一個網點交易數量是跟泥坑小鎮 一樣的,假設這里是回音山谷。而海盜島可能稍微落后了,只有 9 條,最后一次交易記錄是 9。如果只按照我們先前的石子數量來判斷,如果海盜島的石子數量最多,那海盜島在泥坑小鎮出現了問題之后,就會被選出 Leader,但實際是不允許的。因為只有回音山谷有最新的交易數據,但海盜島沒有。』
『雖然很繞,但我大概有點明白了,豬爸爸。也就是說,我們選舉的時候,還需要判斷,被選舉者是否有最新的交易記錄吧。』
『是的,兔小姐,所以 Candidate 在發送投票消息的時候,不光要帶上石子數量,還需要帶上自己最后一條交易記錄的 ID。如果我收到了一條投票消息,發現這條消息里面的交易記錄 ID 比我當前的還要大或者相等,那我就能認為發送這條投票消息的人有比我更多的交易記錄,我就可以給他投票。』
在實際 Raft 里面,Candidate 給其他節點發送投票消息的時候,會帶上自己當前最后一條 Log 的 Term 和 Index。如果投票消息的 Term 比自己最后一條 Log 的 Term 大,或者兩個 Term 相等,但投票消息的 Index 大于或者等于自己最后一條 Log 的 Index,那么就可以給這個 Candidate 投票。至于為什么要同時判斷 Term 和 Index,我們可以考慮一個 corner case,假設有 A,B,C 三個節點。
- A 在 Term 10 被選成了 Leader,開始對外服務,假設這時候都把 Log 寫到了 Index 99。
- 出現了腦裂,A 被隔離,但這時候,A 仍然收到了一個外部請求,先寫入自己的 Log,A 的 last Log Index 是 100,但這個 Log 沒法發送給 B 和 C。
- B 和 C 重新開始選舉,Term 增加,在 Term 11 的時候 B 成為了 Leader,對外服務。
- B 在 Log 100 的位置寫入了另一個新的 Log。
- A 的網絡恢復,開始給 B 和 C 投票,這時候 A 的 Term 可能已經是 12 了。
在 5 的時候,A 也有一個 Log 100,但這個 Log 其實是不正確的,所以 B 和 C 必須拒絕給 A 投票,所以這里我們要借助 Term。因為 A 寫 Log 100 的時候,Term 還是 10,但 B 和 C 這時候寫的 Log 100 里面的 Term 已經是 11 了,所以它們就會知道 A 并沒有最新的 Log。
小結
好了,扯了這么多,都無非是說的 Raft 里面的 Leader Election,雖然 Raft 的 Leader Election 原理很好理解,主要還是一些 corner case 的問題,以及出現了一個壞的節點,如果讓它別亂發投票消息,影響到整個集群。