談談raft fig8 —— 迷惑的提交條件和選舉條件

前言

這篇文章的思路其實在兩個月前就已經成型了,但由於實習太累了,一直沒來得及寫出來。大概一個月前在群裡和群友爭論fig8的一些問題時,發現很多群友對fig 8是充滿了迷惑的。我個人在做lab的時候也對fig 8的問題感到非常頭疼,真正大概理解這個問題的來龍去脈(大概?)的時間大概是兩個月前複習raft的時候,而此時距離我做完lab已經足足一年有餘了......這個問題難於理解,其實raft的作者要至少背一部分鍋的——2014年的論文裡,對於這個問題的討論不是很充足,很多該回扣的點並沒有回扣到。我這裡嘗試按照我自己的理解,梳理一下fig 8問題的原因。如果你想要看懂這篇文章,請至少把raft的論文完整的讀幾遍,如果懂basic paxos再好不過了,因為理解了basic paxos對於理解fig 8的問題非常有幫助。

迷惑的提交條件

如果一個操作得到了提交,那麼相當於系統給出了一個保證:只要是這個系統是能推進的,那麼這個操作日誌遲早會被apply。如果日誌能夠提交,這條日誌必須要滿足某些條件,在滿足了所有的條件的瞬間,這條日誌的狀態就是已提交的,即使它的index高於commitIndex。

那麼細化來說,這些條件是什麼呢?對於資料庫中的事務,提交條件是該事務所有的WAL Log均已落盤;對於Percolator式的分散式事務,提交條件是Primary成功提交;但是對於Raft中的日誌呢?如果回顧論文你可能會比較驚訝,直到討論fig 8的問題之前,作者似乎都回避了這個問題,只是一味的說,日誌只要成功備份到了Majority上,就提交它。初次閱讀論文時,我們潛意識中便隱隱約約的埋藏了這樣一個先入為主的錯誤認知——只要日誌備份到了Majority上,就可以提交它,連帶著提交這條日誌之前的所有日誌。然後論文來到了5.4,作者開始打我們的臉,告訴了我們日誌的真正提交條件:要麼被連帶提交,要麼只能提交一條term == leader.term的日誌。

讀到這一部分,你只需要知道,在raft演算法中,日誌真正的提交條件為,要麼被連帶提交,要麼只能提交一條term == leader.term的日誌,就可以了。如果日誌不滿足這個提交條件,那麼它被截斷是合理的。

迷惑的選舉條件

現在讓我們先忘掉raft日誌的提交條件,先梳理一下raft演算法的部分基本規則:

  1. 採用邏輯時鐘term,一個term內至多有一個leader,candidate之間通過選舉演算法競選leader;
  2. 日誌流是單向的,只允許日誌從leader流向follower;
  3. 對於日誌衝突的場景,直接截斷follower不匹配的日誌,替代以leader的日誌;

綜合2和3,我們不難發現,leader必須要擁有所有commitable的日誌;根據2,leader是無法從其他機器上學習到它缺失的commitable的日誌的,根據3,leader可能截斷那些commitable的日誌。而根據1我們可以知道,leader當選的充分必要條件為它在那個term內得到了Majority的選票。

現在讓我們來審視選舉演算法。論文中定義了candidate與follower間比較日誌新舊的方法:

Raft determines which of two logs is more up-to-date by comparing the index and term of the last entries in the logs. If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date.

比較最後一條日誌,到底誰最up-to-date。如果candidate更加up-to-date,且其他更優先的條件均已滿足(term不超過leader,尚未投出票等等),那麼follower會投票給candidate。

演算法看似很自然直觀,但你會發現,這和我們之前所強調的,當選的leader必須擁有所有commitable的日誌這一條件,似乎並不存在一個包含關係,假設存在一個Majority認定candidate的日誌更加up-to-date,那個這個candidate真的擁有所有的commitable的日誌嗎?

既然我們提到了commitable的日誌,那麼肯定要討論commitable的條件是什麼。現在讓我們成為那個當開始讀論文的初學者,根據論文前文,理所應當的認定那個先入為主的條件吧(也就是說,令日誌的commitable的條件為日誌“已經備份到Majority上”),看一看fig 8的場景(終於到fig 8了,淚目):

在fig 8的c階段,s1是term = 4的leader,它先將(index = 2, term = 2)的日誌備份到了{s1, s2, s3}上。根據我們所認定的那個commitable條件,既然(index = 2, term = 2)的日誌成功備份到了Majority上,那麼這條日誌就是commitable的,當前日誌的commitable集合為{1, 2}。到了d階段,s5的日誌相比於{s2, s3, s4}來說都是more up-to-date的,因此它能得到多數派的投票,成為term 5的leader,但當它成為了term = 5的leader的那一刻它開始了背刺行為:它截斷了index = 2的日誌,而這條日誌是commitable的。究其原因,是因為在d階段中,commitable的日誌集合為{1, 2},而s5並不持有2,因此s5並不持有全部的commitable的日誌

總的來說,在commitable條件設定為“備份到了Majority上”的場景下,我們的選舉演算法無法滿足最核心的那個約束:當選的leader必須持有全部commitable的日誌,這正是5.4 safty所討論的安全性問題。

兩個月前當終於想到了這些東西時,我的第一反應就是去譴責那個該死的選舉演算法,選leader這麼重要的演算法居然無法滿足這麼重要的約束條件,那麼這個演算法就未免太寒顫了。但我很快發現這還真的不是個我行我上的問題:在一輪拉票的過程中僅涉及到了candidate和follower,這兩個server間的互動,一次互動你還能指望candidate得到多少的資訊呢?而在完全非同步的網路環境下,即使是能和一個Majority進行交流,又能做到多少的事情呢?

補日誌or加強提交約束?

補日誌

我們用精確的語言描述一下fig 8的錯誤場景:

在commitable的條件為“備份到了Majority上”的場景下,使用現存的選舉演算法,無法保證當選的leader持有所有commitable的日誌,導致commitable的日誌存在被截斷的風險。

我們不妨先把思維發散一下:既然當選的leader不保證持有全部commitable的日誌,那缺啥補啥唄!先把所有commitable的日誌補全,再去ReplicateLogs,不就可以了嗎?既然現在commitable的日誌已經備份到了Majority上,那我只要和任意一個Majority取得聯絡就肯定能獲得commitable的日誌!誒等等這樣的話選舉演算法的more-up-to-date貌似意義不大了,反正日誌會補全,那保證單主就可以了吧!誒等等如果我們把這個聯絡Majority的過程普及化,任意一個index處的日誌,都要先聯絡到Majority協商好這個index處寫什麼日誌,再去寫下這個日誌,這樣的話單主的約束也可以去掉,因為每個主都要先和Majority取得聯絡,那麼它們必定會有交集,獲悉到其他主想寫的日誌!

恭喜你,你已經進入了basic paxos的大門。basic paxos解決的是多個機器對單個Value的共識問題。basic paxos中,也存在一個全域性單增的邏輯時鐘proposeId。proposer們不能隨便的提議Value,在提議Value之前,它們必須要和Majority取得聯絡,要求它們拒絕一切proposeId < myProposeId的提案,同時,如果Majority中有部分機器已經accept了一個Value(這個Value可能就是已提交的結果),這個proposer就要考慮自己是否需要將自己的提案改成那個Value。這個過程被稱為basic paxos的Prepare階段(basic paxos的第二個階段為Accept階段,這裡不討論它)。現在將Value改成單條日誌,對單條日誌進行Prepare階段,如果這條日誌已經被提交了,那麼proposer就可以學到這條日誌!對所有的日誌均走一遍basic paxos流程,就可以保證所有的日誌是一致的,這就是最樸素的multi paxos思想,實際的multi paxos方法會複雜很多,存在著大量的優化。

雖然提到了paxos,但你完全不必去擔憂上述那段文字你無法理解。我這裡僅僅是想表達一個觀點,解決fig 8裡的問題,方法不止一種,而raft採用了非常精彩的辦法。在raft的一些變種中,也存在著融合paxos思想的方案,例如說PolarFS的Parallel Raft,不需要leader持有全部commitable的日誌,只需要leader有最新的checkpoint就可以了,leader上任後,通過merge階段補全所有日誌,這個merge階段其實就非常類似於paxos的perpare階段。

不管怎樣,先補全commitable的日誌,再去ReplicateLogs是一個解決fig 8中所描述問題的方法。值得注意的是,這種方案下,日誌commitable的條件仍然是“備份到Majority”上就可以了。如果有人問你,為什麼paxos將日誌備份到Majority上就可以提交,而raft這樣做就要在fig 8上吃癟,你就可以告訴他,因為paxos的proposer可以通過prepare階段補全日誌。關於paxos演算法此處不再討論,如果想要更詳細的瞭解paxos演算法務必閱讀《paxos made simple》這篇論文。

加強提交約束

再次回顧一下fig 8中的錯誤場景:

在commitable的條件為“備份到了Majority上”的場景下,使用現存的選舉演算法,無法保證當選的leader持有所有commitable的日誌。

raft的辦法是,將commitbale的條件進行加強:日誌要麼被連帶提交,要麼只能提交term = leader.term的日誌。如果leader能夠提交一條自己term內的日誌,毫無疑問,這條日誌之前的所有日誌均是一致的。

現在再看一下fig 8:

將commitable的條件強化為“日誌要麼被連帶提交,要麼只能提交term = leader.term的日誌”之後,無論是d還是e,均是允許的,因為index = 2的日誌並不滿足commitable的條件,所以這條日誌不會被c中的s1提交,客戶也收不到ok的結果,會一直等待,所以在圖d中截斷它是允許的。但這個個例並不是重點,真正的重點是,在這樣的commitable條件下,“leader必須持有全部的commitable的日誌”這一約束條件是滿足的!我個人嘗試著做了一下證明,放在了文章的末尾。

nop entry的引入

原論文中提到了nop entry。raft作者建議任何leader在上臺後,先不要急著響應客戶請求生成操作日誌,而是先提交一條自己term內的nop-entry,成功提交後,nop entry之前的所有日誌就保持了主從一致了,此時再響應客戶服務。

nop entry的引入其實還解決了一個更加棘手的問題:在提交條件被強化後,如果新的leader上臺後,遲遲不生成新的日誌,那麼leader就無法提交那些它已經成功備份到Majority,但term < leader.term的日誌。

儘管如此,我個人強烈不建議在6.824裡引入nop。因為nop也是要佔據logIndex的,後臺的測試程式碼會對每個index的日誌進行校驗。nop並不是測試程式碼新增的,因此測試程式碼在匹配index時會報錯。在6.824裡,無需考慮也不能考慮leader無法顯示提交日誌導致進度得不到推進的場景。

總結

關於fig8的討論就到這裡了。我們再次梳理一下原論文中的一些比較坑的點。首先是fig 8中場景d的正確性。在commitable的條件為"備份到Majority上即可"時,a → b → c → d是錯誤的,因為c中index = 2的日誌成功備份到了Majority上,commitable的集合為index = {1, 2},index = 2的日誌不允許被截斷。當commitable的條件為“日誌要麼被連帶提交,要麼只能提交term = leader.term的日誌”時,a → b → c → d是正確的,因為在c中,index = 2 的日誌term不等於s1的term,因此s1並不會提交它,所以到了d時,commitable的集合仍然是index = {1},所以d可以截斷index = 2的日誌。

為什麼我們要強化提交條件?因為我們在raft演算法的設定下,我們必須要保證leader必須持有全部的commitable的日誌"這一條件,而選舉演算法無法保證這樣的leader當選,為此,要麼讓leader在ReplicateLogs前補全所有commitable的日誌,要麼另闢蹊徑。raft的解決辦法是強化了提交條件,在這個提交條件下,選舉演算法所選出的leader必定持有全部的commitable的日誌。

前文中我提到“raft作者要為fig 8難以理解背一部分鍋”,也很大程度上是因為作者沒有對fig 8出現的根本原因進行討論,在提出加強提交條件後也沒有用文墨去回扣到選舉條件上。

關於提高約束條件後演算法正確性的證明

證明演算法正確,等價於證明將commitable的約束條件提升至"日誌要麼被連帶提交,要麼只能提交term = leader.term的日誌",後,當選的leader必定持有全部的commitable的日誌。可以用反證法,下文是我自己的證明,個人不保證證明是正確的,畢竟人菜癮大(

反證法

(1) 令TL是最後一次保持約束條件且成功commit了日誌的leader,TL的最後一條commitable的日誌index為c_index,term為c_term,假設在TL之後的第一個錯誤leader為FL,即第一個不持有全部的commitable的日誌,但仍然成功當選的leader。(TL.term, FL.term)間可能存在0個或者多個leader,這些leader進行過ReplicateLog,但沒能成功commit任何新的日誌,也正是因此,在這期間c_term和c_index是不變的。

(2) 根據假設可知FL ∉ Majority_1。根據TL的定義可知,叢集中必定有一個Majority_1,它們都持有這些commitable的日誌。令TL的最後一次提交時,TL的term為commit_term。根據當前的commitable條件,有commit_term == c_term。

(3) 根據投票演算法我們可知,存在一個Majority_2認為FL的日誌是more up-to-date的。這隻有兩種可能:要麼FL.last_log.term == c_term,但index更高,要麼雖然FL.last_log.index < c.index,但FL.last_log.term > c_term。第一種情景是不可能的,因為這樣的機器只能出現在Majority_1中,這與我們的假設矛盾,

(4) 考慮第二種場景,如果FL.last_log.term == FL.term,那麼這條日誌是FL自己新增的,因此它不可能靠這個日誌去贏得選舉,這條日誌只能是來自於之前term的leader。因此到了這裡我們得到了一個恆等式:TL.term == commit_term == c_term < FL.last_log.term < FL.term。

(5) 根據這個不等式可知,存在一個before_leader,它是term為 FL.last_log.term時刻的leader,它給自己添加了一條/幾條日誌,還沒來得及commit就step down了。根據(4) 中FL.last_log.term > c_term == commit_term == TL.term可知,before_leader 的當選時刻是在TL成功的commit之後;又根據我們對TL的定義可知,before_leader肯定是一個持有全部commitable日誌的leader(但它並沒有成功commit更多的日誌,否則要將它歸於TL),因此before_leader在生成日誌(FL.last_log.term, FL.last_log.idx)時,必定是持有所有的commitable的日誌的,且FL.last_log.index > c.index (新日誌是append進來的)。對應的,既然FL.last_log之前的日誌來自於before_leader,那麼它們的日誌在這之前應該一致,因此FL必須有這些commitable的日誌,這與我們的假設相矛盾。綜上所述,我們的假設均被否定,結論得證。