Raft成員變更的工程實踐

<meta name="source" content="lake">

簡介: 成員變更是一致性系統實現繞不開的難題,對於提升運維能力以及服務可用性都有很大的幫助。 本文從Raft成員變更理論出發,介紹了Raft成員變更和單步成員變更的問題,其中包括Raft著名的Bug。 對於Raft成員變更的工程實現上需要考慮的問題,本文給出了一些工程實踐經驗。

一 引言

成員變更是一致性系統實現繞不開的難題,對於提升運維能力以及服務可用性都有很大的幫助。

本文從Raft成員變更理論出發,介紹了Raft成員變更和單步成員變更的問題,其中包括Raft著名的Bug。

對於Raft成員變更的工程實現上需要考慮的問題,本文給出了一些工程實踐經驗。

二 Raft成員變更簡介

分佈式系統運行過程中節點經常會出現故障,需要支持節點的動態增加和刪除。

成員變更是在集羣運行過程中改變運行一致性協議的節點,如增加、減少節點、節點替換等。成員變更過程不能影響系統的可用性。

成員變更也是一個一致性問題,即所有節點對新成員達成一致。但是成員變更又有其特殊性,因爲在成員變更的過程中,參與投票的成員會發生變化。

如果將成員變更當成一般的一致性問題,直接向Leader節點發送成員變更請求,Leader同步成員變更日誌,達成多數派之後提交,各節點提交成員變更日誌後從舊成員配置(Cold)切換到新成員配置(Cnew)。

因爲各個節點提交成員變更日誌的時刻可能不同,造成各個節點從舊成員配置(Cold)切換到新成員配置(Cnew)的時刻不同。可能在某一時刻出現Cold和Cnew中同時存在兩個不相交的多數派,進而可能選出兩個Leader,形成不同的決議,破壞安全性。

圖1 成員變更的某一時刻Cold和Cnew中同時存在兩個不相交的多數派

如圖1是3個節點的集羣擴展到5個節點的集羣,直接擴展可能會造成Server1和Server2構成老成員配置的多數派,Server3、Server4和Server5構成新成員配置的多數派,兩者不相交從而可能導致決議衝突。
由於成員變更的這一特殊性,成員變更不能當成一般的一致性問題去解決。爲了解決這個問題,Raft提出了兩階段的成員變更方法Joint Consensus。

1 Joint Consensus成員變更

Joint Consensus成員變更讓集羣先從舊成員配置Cold切換到一個過渡成員配置,稱爲聯合一致成員配置(Joint Consensus),聯合一致成員配置是舊成員配置Cold和新成員配置Cnew 的組合Cold,new,一旦聯合一致成員配置Cold,new提交,再切換到新成員配置Cnew

圖2 Joint Consensus成員變更

Leader收到成員變更請求後,先向Cold和Cnew同步一條Cold,new日誌,此後所有日誌都需要Cold和Cnew兩個多數派的確認。Cold,new日誌在Cold和Cnew都達成多數派之後才能提交,此後Leader再向Cold和Cnew同步一條只包含Cnew的日誌,此後日誌只需要Cnew的多數派確認。Cnew日誌只需要在Cnew達成多數派即可提交,此時成員變更完成,不在Cnew中的成員自動下線。

成員變更過程中如果發生Failover,老Leader宕機,Cold,new中任意一個節點都可能成爲新Leader,如果新Leader上沒有Cold,new日誌,則繼續使用Cold,Follower上如果有Cold,new日誌會被新Leader截斷,回退到Cold,成員變更失敗;如果新Leader上有Cold,new日誌,則繼續將未完成的成員變更流程走完。

Joint Consensus成員變更比較通用且容易理解,但是實現比較複雜,之所以分爲兩個階段,是因爲對 與 的關係沒有做任何假設,爲了避免 和 各自形成不相交的多數派而選出兩個Leader,才引入了兩階段方案。

如果增強成員變更的限制,假設Cold與Cnew任意的多數派交集不爲空,Cold與Cnew就無法各自形成多數派,則成員變更就可以簡化爲一階段。

2 單步成員變更 實現單步的成員變更,關鍵在於限制Cold與Cnew,使之任意的多數派交集不爲空。方法就是每次成員變更只允許增加或刪除一個成員。

圖3 增加或刪除一個成員

增加或刪除一個成員時的情形,如圖3所示,可以從數學上嚴格證明,只要每次只允許增加或刪除一個成員,Cold與Cnew不可能形成兩個不相交的多數派。因此只要每次只增加或刪除一個成員,從Cold可直接切換到Cnew,無需過渡成員配置,實現單步成員變更。

單步成員變更一次只能變更一個成員,如果需要變更多個成員,可以通過執行多次單步成員變更來實現。

單步成員變更理論雖然簡單,但卻埋了很多坑,實際用起來並不是那麼簡單。

三 Raft單步成員變更的問題

Raft單步成員變更的問題,最著名的莫過於Raft著名的正確性問題,另外單步成員變更還有潛在的可用性問題。

1 Raft單步成員變更的正確性問題

Raft單步變更過程中如果發生Leader切換會出現正確性問題,可能導致已經提交的日誌又被覆蓋。Raft作者(Diego Ongaro)早在2015年就發現了這個問題,並且在Raft-dev詳細的說明了這個問題[1]。

下面是一個Raft單步變更出問題的例子, 初始成員配置是abcd這4節點,節點u和V要加入集羣, 如果中間出現Leader切換, 就會丟失已提交的日誌:

圖4 Raft單步成員變更的正確性問題

  • t0:節點abcd的成員配置爲C0

  • t1 :節點abcd在Term 0選出a爲Leader,b和c爲Follower;

  • t2:節點a同步成員變更日誌Cu,只同步到a和u,未成功提交;

  • t3:節點a宕機;

  • t4:節點d在Term 1被選爲Leader,b和c爲Follower;

  • t5:節點d同步成員變更日誌Cv,同步到c、d、V,成功提交;

  • t6:節點d同步普通日誌E,同步到c、d、V,成功提交;

  • t7:節點d宕機;

  • t8:節點a在Term 2重新選爲Leader,u和b爲Follower;

  • t9:節點a同步本地的日誌Cu給所有人,造成已提交的Cv和E丟失。

爲什麼會出現這樣的問題呢?根本原因是上一任Leader的成員變更日誌還沒有同步到多數派就宕機了,新Leader一上任就進行成員變更,使用新的成員配置提交日誌,之前上一任Leader重新上任之後可能形成另外一個多數派集合,產生腦裂,將已提交的日誌覆蓋,造成數據丟失。

Raft作者在發現這個問題之後,也給出了修復方法。修復方法很簡單, 跟Raft的日誌Commit條件類似:新任Leader必須在當前Term提交一條日誌之後,才允許同步成員變更日誌。也即Leader在當前Term還未提交日誌之前,不允許同步成員變更日誌。

按照這個修復方法,最簡單的實現就是Leader上任後先提交一條no-op日誌,然後再同步成員變更日誌。這條no-op日誌可以保證跟上一任Leader未提交的成員變更日誌至少有一個節點交集,這樣可以發現上一任Leader的日誌是舊的,從而阻止上一任Leader重新選爲Leader,進而阻止了腦裂的產生。

對應上面這個例子,就是L1當選Leader後必須先提交一條no-op日誌,然後才能開始同步Cv和E,以便能發現L2的日誌是舊的,從而阻止L2當選Leader。

另一種方法是使用Joint Consensus成員變更,沒有這樣的正確性問題。

2 Raft單步成員變更的可用性問題

單步成員變更每次只能增加或者減少一個成員,在做成員替換的時候需要分兩次變更,第一次變更先將新成員加入進來,第二次變更再將老成員刪除,中間如果如果網絡分區,有可能會導致服務不可用。

考慮a、b、c三個成員部署在三個機房,現在因爲a發生故障要將a替換爲同機房的d。按照單步成員變更,abc要先變爲abcd,再變爲bcd。

中間經歷的4節點abcd的狀態, 有可能在出現二分的網絡分區(ad|bc)時導致整個集羣不可用。因爲a與d位於同一機房,這種二分網絡分區的情況在實際情況中還是不容忽視的。

怎麼解決這個問題呢?一種方法是做成員替換的時候,先刪除老成員,再加入新成員,即abc先變爲bc,再變爲bcd,這樣可以避免abcd的狀態。

另一種方法是使用Joint Consensus成員變更,abc先變爲abc U bcd ,再變爲bcd,也不會經歷abcd的狀態。

四 Raft成員變更的工程實踐

Raft成員變更的理論雖簡單,但實際工程實現上還是有很多地方要考慮。因爲Raft單步成員變更有正確性問題及可用性問題,工程上建議儘量使用Joint Consensus成員變更,這裏主要討論一些Joint Consensus成員變更工程實現上必須考慮的問題。

1 新成員先加入再同步數據還是先同步數據再加入

因爲Raft需要嚴格保證順序,而新成員上還沒有任何數據,因此新成員加入集羣后需要先同步數據才能正常工作。工程實現時就有兩種選擇,一種是讓新成員先加入再同步數據,另一種是先給新成員同步數據,同步完成後再加入。這兩種方式各有利弊。

表1 新成員先加入再同步數據和先同步數據再加入的優缺點

新成員先加入再同步數據,成員變更可以立即完成,並且因爲只要大多數成員同意即可加入,甚至可以加入還不存在的成員,加入後再慢慢同步數據。但在數據同步完成之前新成員無法服務,但新成員的加入可能讓多數派集合增大,而新成員暫時又無法服務,此時如果有成員發生Failover,很可能導致無法滿足多數成員存活的條件,讓服務不可用。因此新成員先加入再同步數據,簡化了成員變更,但可能降低服務的可用性。

新成員先同步數據再加入,成員變更需要後臺異步進行,先將新成員作爲Learner角色加入,只能同步數據,不具有投票權,不會增加多數派集合,等數據同步完成後再讓新成員正式加入,正式加入後可立即開始工作,不影響服務可用性。因此新成員先同步數據再加入,不影響服務的可用性,但成員變更流程複雜,並且因爲要先給新成員同步數據,不能加入還不存在的成員。

2 成員變更日誌使用什麼配置

成員變更日誌本身是爲了改變成員配置,處在成員配置變更的臨界點上,因此成員變更日誌使用什麼配置就很關鍵。

表2 Joint Consensus成員變更日誌使用的成員配置

對於Joint Consensus成員變更,成員變更日誌使用什麼配置是確定的。Cold,new日誌使用聯合一致成員配置Cold,new,需要老成員配置Cold和新成員配置Cnew兩個多數派確認才能提交,Cnew日誌使用新成員配置Cnew,只需要新成員配置Cnew的多數派確認即可提交,但Cnew日誌也會同步給老成員配置Cold,主要是爲了讓Cold中不在Cnew中的成員自動退出。

3 成員變更日誌什麼時候生效

成員變更通過成員變更日誌來完成,讓各成員對成員配置達成一致,但成員變更日誌與普通日誌不同,並不一定要等到提交後Apply生效。

表3 成員變更日誌的生效時機

對於Joint Consensus成員變更,成員變更日誌什麼時候生效是確定的。在Leader上開始同步成員變更日誌之前就需要生效,在Follower上成員變更日誌持久化完成後就需要生效。成員變更日誌還未提交就先生效了,因此在Leader切換後可能會回滾。

4 成員變更期間日誌是否需要嚴格按序提交

考慮這樣一種情況,成員變更減少了成員數量,進而減小了多數派集合,而更小的多數派更容易達成,造成成員變更之後的日誌比之前的日誌先達成多數派。

按照Raft論文中的commitIndex的推進算法:

If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm:

set commitIndex = N

一條日誌達成多數派就往前推進commitIndex至該日誌,如果該日誌之前有日誌按照老成員配置還未達成多數派,也一併提交了。

這種情況是否會出問題呢?實際上並不會,因爲成員變更之後,已經有日誌使用新成員配置提交了,不在新成員配置中的節點不可能再當選Leader了,進而不會覆蓋之前的日誌,因此就算之前的日誌按照老成員配置未達成多數派也可以安全的提交。

hashicorp raft的實現還是嚴格按序提交的,即只有前面的日誌都達成多數派之後才能提交。

5 只有少數成員存活時怎麼恢復服務

Raft只能在大多數成員存活的情況下才能正常工作,實際可能會遇到只有少數成員存活的情況,這個時候要怎麼恢復服務

呢。

因爲只有少數成員存活,已經不能達成多數派,不能寫入數據,也不能做正常的成員變更。需要提供一個強制更改成員配置的接口,通過它設置每個成員的成員配置列表,便於從大多數成員故障中恢復。

比如只剩一個成員S1存活的時候,強制更改成員配置設置成員列表爲{S1},這樣形成一個只有S1的成員列表,讓S1繼續提供讀寫服務,後續再調度其他節點通過成員變更加入。通過強制修改成員列表,可以實現最大可用模式。

五 單步成員變更的工程實踐

單步成員變更雖然不推薦在工程中使用,這裏還是總結一下單步成員變更的一些工程實踐,供研究討論。

1 單步成員變更日誌使用什麼配置

對於單步成員變更,成員變更日誌是使用新成員配置 還是老成員配置Cnew呢?實際上單步成員變更日誌無論使用新成員配置Cold還是老成員配置Cnew都不會破壞Cold與Cnew的多數派至少有一個節點相交,因此單步成員變更日誌既可以使用新成員配置Cold也可以使用老成員配置Cnew,兩種方式各有利弊。

表4 單步成員變更日誌使用老成員配置和使用新成員配置的優缺點

單步成員變更日誌使用老成員配置Cold,可以避免單步成員變更的正確性問題,因此可以省略掉Leader上任後的no-op日誌,同時在增加成員時可能只需要更小的多數派集合,但在減少成員時可能需要更大的多數派集合。

單步成員變更日誌使用新成員配置Cnew,需要Leader上任後先提交一條no-op日誌,以避免單步成員變更的正確性問題,同時在減少成員時可能只需要更小的多數派集合,但在增加成員時可能需要更大的多數派集合。

單步成員變更日誌不管使用新成員配置還是老成員配置,最好都同步給新老成員配置中的所有成員,這樣在增加成員時可以讓新成員遲早收到通知,在減少成員時也可以讓被刪除的成員收到通知而自動退出。

Raft論文中單步成員變更日誌使用新成員配置Cnew,etcd中單步成員變更日誌使用老成員配置Cold

2 單步成員變更日誌什麼時候生效

表5 單步成員變更日誌的生效時機

對於單步成員變更,如果成員變更日誌使用新成員配置,則與Joint Consensus成員變更一樣,Leader上開始同步成員變更日誌之前就需要生效,在Follower上成員變更日誌持久化完成後就需要生效。如果成員變更日誌使用老成員配置,理論上只需要在下一次成員變更開始之前生效即可,但實際爲了讓新加入的節點儘快開始服務,一般在成員變更日誌提交後就生效。

Raft論文中單步成員變更日誌使用新成員配置Cnew,本地持久化完成就生效;etcd中單步成員變更日誌使用老成員配置Cold,提交後再生效。

六 總結

Raft提供了Joint Consensus成員變更和單步成員變更,極大的推動了成員變更在工程中的應用。本文總結了一些Raft單步成員變更的問題,以及成員變更的工程實踐。Joint Consensus通用並且不容易踩坑,一階段成員變更坑比較多。工程上建議儘量使用Joint Consensus成員變更。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章