Raft成员变更的工程实践
簡介: 成員變更是一致性系統實現繞不開的難題,對于提升運維能力以及服務可用性都有很大的幫助。 本文從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成員變更。
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的Raft成员变更的工程实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flink 源码 | 自定义 Forma
- 下一篇: 好物推荐|下载超过 23w 次的 IDE