使用multi-paxos實現日志同步應用


paxos

說multi-paxos之前先簡要說一下paxos

paxos是在多個成員之間對某個值(提議)達成一致的一致性協議。這個值可以是任何東西。比如多個成員之間進行選主,那么這個值就是主的身份。在把multi-paxos協議應用在日志同步中時,這個值就是一條日志。網上講paxos的文章已經很多了,這里簡要說明一下。

paxos分為prepare和accept兩個階段。協議中有兩個主要的角色,proposer和acceptor。

value被majority accept之前,每個acceptor可以accept多個不同的值。但是,一旦一個value被majority accept(即value達成一致),那么這個value就不會變了。因為prepare階段會將該value給找出來,隨后accept階段會使用這個value,后續的所有的提案都會選擇這個value。

需要注意的是,每個階段都是收到majority的響應后即開始處理。並且由於機器會宕機,acceptor需要對acceptedProposalID, acceptedValue和minProposal進行持久化。

從流程中可以看出prepare有兩個作用:

  1. 大的proposal id會block未完成的小的proposal id達成一致的過程,所以為了減少無效的prepare請求,每次都選擇比自己以往見過的proposal id更大的id。
  2. 一旦某個value達成一致,那么后續的prepare都會找到這個value作為accept階段的值

可以看出,一次paxos達成一致至少需要兩次網絡交互。

multi-paxos

paxos是對一個值達成一致,multi-paxos是運行多個paxos instance來對多個值達成一致,每個paxos instance對一個值達成一致。在一個支持多寫並且強一致性的系統中,每個節點都可以接收客戶端的寫請求,生成redo日志然后開始一個paxos instance的prepare和accept過程。這里的問題是每條redo日志到底對應哪個paxos instance。

在日志同步應用中,用log id來區分不同的paxos instance。每條日志都由一個id唯一標示,這個log id標識一個paxos instance,這個paxos instance達成一致,即對應的日志內容達成一致,即majority的成員accept了這個日志內容。在一個由N個機器(每個機器既承擔proposer也承擔acceptor角色)組成的集群(通常叫做paxos group)中,每個proposer都可以產生redo日志並且進行paxos instance,那么每條redo日志到底使用哪個log id? 顯然,每個proposer都會選擇自己知道的還沒有達成一致的最小的log id來作為這次日志的log id,然后運行paxos協議,顯然,多個proposer可能會選擇同一個log id(最典型的場景就是空集群啟動的情況下),最終,只有一個proposer能夠成功,那么其他的proposer就需要選擇更大的未達成一致的log id來運行paxos。顯然,這種沖突是非常嚴重的,會有很多的proposer成功不了進而選擇更大的log id來運行paxos。

在真實的系統中,比如chubby, spanner,都會在paxos group中選擇一個成員作為leader,只有leader能夠propose日志,這樣,prepare階段就不會存在沖突,相當於對整個log文件做了一次prepare,后面這些日志都可以選用同一個proposal id。這樣的話,每條日志只需要一次網絡交互就能達成一致。回顧一下文章開頭提到paxos中需要每個成員需要記錄3個值,minProposal,acceptedProposal,acceptedValue,其中后面兩個值可以直接記錄在log中,而第一個值minProposal可以單獨存在一個文件中。由於這里后面的日志都可以選用同一個proposal id,顯然,在大部分時間內,minProposal都不需要改變。這正是multi-paxos的精髓

選主

對於paxos來說,主的身份無所謂,主不需要像raft那樣擁有最全的已經commit的日志。所以選主算法無所謂,比如大家都給機器ip最大的機器投票,或者給日志最多的投票,或者干脆直接運行一次paxos,值的內容就是主的身份。顯然,由於對新主的身份無限制,那么,新主很有可能沒有某些已經達成一致的日志,這個時候,就需要將這些已達成一致的日志拉過來,另外,新主也有可能沒有某些還未達成一致的日志。如下圖所示:

圖中,恢復之前,log id等於3的日志C已經在多數派上達成了一致,但是在新主上沒有。比如log id等於4的日志D在多數派上沒有達成一致,在新主上也沒有。

恢復

新主向所有成員發送查詢最大log id的請求,收到majority的響應后,選擇最大的log id作為日志恢復的結束點。圖中,如果收到的majority不包括2號成員,那么log id=6為恢復結束點。如果收到的majority包括2號成員,那么log id=7為恢復結束點。這里取majority的意義在於恢復結束點包含任何的majority達成一致的日志。拿到log id后,從頭開始掃描日志,對於每條日志都運行paxos協議確認一次:如果日志之前已經達成一致了,比如日志A,B,C,E,F,那么再次運行paxos的prepare階段會把日志內容找出來作為accept階段的值,不影響結果。如果日志之前並沒有達成一致,比如日志D,那么當返回的majority中包含3號成員時,D會被選出來當作accept階段的值,當返回的majority中不包含3號成員時,那么D實際上不會被選出,這時主可以選擇一個dummy日志作為accept階段的值。

恢復優化

可以看出,如果日志非常多,每次重啟后都要對每條日志做一次paxos,那么恢復時間可想而知。在上面的例子中,A,B,E已經達成一致,做了無用功。paxos協議中,只有主即proposer知道哪些日志達成了一致,acceptor不知道,那么很容易想到的一個優化就是proposer將已經達成一致的日志id告訴其他acceptor,acceptor寫一條確認日志到日志文件中。后續重啟的時候,掃描本地日志只要發現對應的確認日志就知道這條日志已經達成多數派,不需要重新使用paxos進行確認了。這種做法有一個問題,考慮如下場景:

舊主成功的給自己和2號成員發送了確認日志,但是沒有給3號成員發送成功舊掛了,然后2號成員被選為新主,那么新主不會對log id=3的日志重新運行paxos,因為本機已經存在確認日志。這樣的話,3號成員就回放log id=3的日志到上層了。解決這個問題的做法就是followers需要主動的向主詢問日志到底有沒有達成一致,如果有,則自己補充確認日志。

回放

宕機重啟后,對未達成一致的日志重新運行paxos時,如log id=4的日志,如果返回的majority中不包含3號成員,那么日志D不會被找出來,這樣就需要將3號成員的log id=4日志置未一條無操作的日志記作NOP日志,D最終也就不會形成多數派。由於multi-paxos允許日志亂序接收,並且日志的長度幾乎都不一樣,所以在磁盤上log id是亂序的,所以從物理上說,每個成員的日志不是一模一樣的。那么要把log id=4的日志覆蓋寫成NOP日志也就比較麻煩,需要為每條日志維護索引。實現上可以不覆蓋寫,直接append一條log id=4的NOP日志到日志文件,這樣沒有問題,因為回放的時候只會回放能找到確認日志的日志到上層應用中。

成員變更

multi-paxos處理成員變更比較簡單,規定第i條日志參與paxos同步的時候,其成員組是第i-k條日志包含的成員組(每條日志里面都包含成員組)。

multi-paxos協議之外

multi-paxos只是保證大家對日志達成一致。但是具體multi-paxos運用到真實的系統中時,從應用層面上看,可能會出現一些詭異的問題。考慮如下場景:

如圖,1號主寫了A,B,C,D,其中B,C,D沒有形成多數派,然后A宕機了,2號被選為了主,客戶端過來讀不到B,C,D,然后B沒寫任何東西,就掛了,這個時候,A起來后重新被選為主,對B,C,D重新運行paxos,把B,C,D達成了一致,這個時候客戶端再次過來讀,又能讀到B,C,D了。對於multi-paxos本身來說,並沒有什么不對的地方,但是上層應用的語義出現了問題:曾經讀不到的東西,什么都沒做,又能讀到了。

解決這個問題的方法是通過主提供服務之前必須成功寫入一條start working日志來解決。如下圖:

如圖,每個成員成為主提供服務之前都要首先寫一條start working日志,只有達成多數派才能提供服務。1號在重新成為主之后,通過對log id=2的日志運行paxos,將2號start日志恢復了出來,然后對C和D運行paxos恢復出來后,后續回放的時候,如果發現后面日志中帶有的timestamp(其實時leader上任時間)比start working帶有的timestamp更小,那么就不回放到上層。隨后客戶端來讀仍然讀不到B,C,D,前后保持一致。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM