Joint Consensus兩階段成員變更的單步實現
一 引言
分布式系統運行過程中節點經常會出現故障,需要支持節點的動態增加、刪除和替換。
成員變更是分布式系統繞不開的話題,特別是在一致性系統中,對于提升運維能力和服務可用性都有很大的幫助。
Raft提出的兩階段成員變更Joint Consensus是業界主流的成員變更方法,極大的推動了成員變更的工程應用。但Joint Consensus成員變更采用兩階段,一次變更需要提議兩條日志, 在一些系統中直接使用時有些不便。雖然Raft也提出了單步成員變更方法,但單步成員變更方法一次只能增加或減少一個成員,限制較大,并且容易踩坑,一般不推薦使用。
那么很自然的想到,Joint Consensus成員變更能否只使用單步實現呢?本文對這個問題進行了深入探討。
二 成員變更
我們先來回顧下一致性協議中的成員變更問題。成員變更是在集群運行過程中改變運行一致性協議的節點,如增加、減少節點、節點替換等。成員變更過程不能影響系統的可用性。
成員變更也是一個一致性問題,即所有節點對成員配置達成一致。但是成員變更又有其特殊性,因為在成員變更的過程中,參與投票的成員會發生變化。
圖1 成員變更的某一時刻Cold和Cnew中同時存在兩個不相交的多數派
如果將成員變更當成一般的一致性問題,成員變更過程中,各節點從舊成員配置Cold切換到新成員配置Cnew的時刻可能有差異,可能在某一時刻Cold和Cnew中同時存在兩個不相交的多數派,形成雙Quorum,破壞一致性。
為了解決這個問題,Raft提出了兩階段的成員變更方法Joint Consensus。
1 Joint Consensus成員變更
Joint Consensus成員變更為了避免雙Quorum問題,引入一個聯合成員配置Cold,new作為過渡配置, Cold,new是Cold和Cnew的組合。Cold與Cold,new的Quorum有交集,Cold,new與Cnew的Quorum也有交集。成員變更先從Cold切換到Cold,new,待Cold,new提交后,再切換到Cnew,保證Cold與Cnew不同時使用,因而不會形成雙Quorum,保障安全性。
圖2 Cold與Cold, new與Cnew三者的Quorum集合之間的關系
Joint Consensus使用兩條日志完成成員變更過程。Leader收到成員變更請求后,先向Cold和Cnew同步一條Cold,new日志,此后所有日志都需要Cold和Cnew兩個多數派的確認。Cold,new日志在Cold和Cnew都達成多數派之后才能提交,此后Leader再向Cold和Cnew同步一條只包含Cnew的日志,此后日志只需要Cnew的多數派確認。Cnew日志只需要在Cnew達成多數派即可提交,此時成員變更完成,不在Cnew中的成員自動下線。
圖3 Joint Consensus成員變更過程
成員變更過程中如果發生Failover,老Leader宕機,Cold,new中任意節點都可能成為新Leader,如果新Leader上沒有Cold,new日志,則繼續使用Cold,Follower上如果有Cold,new日志會被新Leader截斷,回退到Cold,成員變更失敗;如果新Leader上有Cold,new日志,則繼續將未完成的成員變更流程走完。
2 單步成員變更
Joint Consensus成員變更之所以需要兩個階段,是因為對Cold與Cnew的關系沒有做任何假設,為了避免Cold和Cnew各自形成不相交的多數派而形成雙Quorum,才引入了兩階段方案。
如果增強成員變更的限制,假設Cold與Cnew的Quorum交集不為空,Cold與Cnew就無法形成雙Quorum,則成員變更就可以簡化為一階段。
實現單步的成員變更,關鍵在于限制Cold與Cnew,使Cold與Cnew的Quorum交集不為空。那么怎么樣限制Cold與Cnew,才能使Cold與Cnew的Quorum交集不為空呢?方法就是每次成員變更只允許增加或刪除一個成員。
圖4 增加或刪除一個成員時Cold與Cnew的Quorum
增加或刪除一個成員時的情形,如圖4所示,可以從數學上嚴格證明,只要每次只允許增加或刪除一個成員,Cold與Cnew不可能形成兩個不相交的Quorum。因此只要每次只增加或刪除一個成員,從Cold可直接切換到Cnew,無需過渡成員配置,實現單步成員變更。
單步成員變更一次只能變更一個成員,如果需要變更多個成員,如實現替換成員等,可以通過執行多次單步成員變更來實現。
單步成員變更理論雖然簡單,但卻埋了很多坑,實際用起來并不是那么簡單。先前的文章Raft成員變更的工程實踐中有詳細介紹。
三 兩階段成員變更的單步實現
Joint Consensus成員變更雖然通用但是采用兩階段,一次變更需要提交兩條日志,單步成員變更雖然只需要提交一條日志,但是限制較大,一次只能變更一個成員。兩者的優勢能否結合呢?Joint Consensus成員變更能否只用單步實現呢?
Joint Consensus成員變更過程中,Cold,new日志的提交已經讓各節點對Cnew配置達成了一致,那么Cnew日志有什么作用呢?能否在Cold,new日志提交后就從Cold,new配置切換到Cnew配置呢?這樣是不是就可以不需要Cnew日志,變成單步實現了呢?
考慮Joint Consensus成員變更中Cnew日志的作用,Cnew日志在Cold,new日志提交之后發起提議,節點收到并持久化Cnew日志后從Cold,new配置切換到Cnew配置,不在Cnew配置中的成員在Cnew日志提交后下線。根據這個過程,可以總結出Cnew日志的作用:
通知節點在收到并持久化Cnew日志后從Cold,new配置切換到Cnew配置。
通知不在Cnew配置中的節點在Cnew日志提交后下線。
成員變更過程中發生Failover后,本地有Cnew日志的節點具有優先選舉權。
如果能不使用Cnew日志同時又完成Cnew日志的工作,不就可以用單步實現兩階段的Joint Consensus成員變更嗎?事實上已經有系統探索過這條路。
1 ZooKeeper成員變更
ZooKeeper從3.5.0版本開始在Zab的基礎上支持了成員變更。ZooKeeper具有Primary Order特性,而使用兩條日志的Joint Consensus成員變更無法保證Primary Order特性,為了既滿足成員變更的通用性,又不喪失Primary Order特性,ZooKeeper在論文《Dynamic Reconfiguration of Primary/Backup Clusters》中提出了自己的成員變更方法,并在ZooKeeper中應用了此方法,比Raft的提出還早。
如圖5是ZooKeeper成員變更協議,圖中舊成員配置用S表示,新成員配置用S‘表示,P為Leader節點,圖5展示了將B1和B2節點替換成B3和B4節點的過程:
圖5 ZooKeeper成員變更協議
- 為了讓新節點追上最新數據,新成員配置S’中的新節點B3、B4先連接到當前的主節點P,P會向它們傳輸自己當前的狀態作為他們的初始狀態。在Zab協議中當備節點連接上主節點時這樣的狀態傳輸就會自動發生,并且會繼續從主節點P接收所有后續的操作日志(例如圖中的Op1和Op2),這個過程中節點B3、B4不參與投票。
- 主節點P向連接到它的所有備節點(S U S‘)發送成員變更日志COP,COP日志中攜帶舊成員配置S和新成員配置S‘,并等待舊成員配置S中的節點確認。一旦S中的多數派確認了COP日志,就對S’達成了共識。
- 在COP日志之前的日志只需要舊成員配置S中的多數派確認,可以在舊成員配置和新成員配置(S U S‘)中提交;在COP命令之后且在S’的激活消息ACTIVATE之前的日志需要新舊成員配置(S U S‘)兩個多數派確認,并且只能在S’中提交;在S’的激活消息ACTIVATE后的日志,只需要在S‘中確認和提交。
- 主節點P等待COP日志以及S'中COP之前的日志的確認。
- 一旦新舊成員配置(S U S1)兩個多數派都確認了COP日志,主節點P就提交COP日志,并廣播一條激活消息ACTIVATE來激活新成員配置S’從而完成成員變更。與日志同步消息類似,ACTIVATE消息包含主節點P的Epoch,攜帶過時的Epoch的ACTIVATE消息將被忽略。
成員變更過程中如果發生Failover,可能出現下面幾種情況:
如果在COP日志發送之前Failover,那么成員變更失敗,在舊成員配置中重新選主后繼續工作;
如果在COP日志發送之后并且在ACTIVATE之前Failover,新舊成員配置中任意節點都可能成為新Leader,如果新Leader上沒有COP日志,則成員變更失敗;如果新Leader上有COP日志,則繼續將未完成的成員變更流程走完。
如果在ACTIVATE后Failover,成員變更已經完成,但還無法保證新Leader一定在新成員配置中,此時不在新成員配置中的節點還不能下線。因此在發送ACTIVATE消息后還需要在新成員配置中提交一條no-op日志,no-op日志提交后可保證新Leader一定在新成員配置中,不在新成員配置中的節點可以安全下線。
ZooKeeper利用異步的Commit消息,也即ACTIVATE消息來通知節點從新舊成員配置切換到新成員配置。使用異步的no-op日志讓不在新成員配置中的節點安全下線。ZooKeeper的ACTIVATE消息和異步的no-op日志起到了Joint Consensus成員變更中Cnew日志的作用。
2 改進的單步實現
ZooKeeper成員變更協議不如Joint Consensus成員變更那么簡潔,Joint Consensus成員變更通過兩階段可以利用協議本身而不需要做過多的限制來保證成員變更的安全性。那么ZooKeeper成員變更協議是否可以改進呢?
ZooKeeper成員變更協議中異步的ACTIVATE消息和no-op日志其實就是為了完成Joint Consensus成員變更中Cnew日志的作用,明白了這一點后那么也可以將Joint Consensus成員變更的Cnew日志改為異步的,在Cold,new日志提交后就認為成員變更完成,然后異步的提交Cnew日志。之所以可以將Cnew日志改為異步的,在Cold,new日志提交后就認為成員變更完成,是因為Cold,new日志一旦提交,各節點已經對新成員配置達成了一致,再也不會回退到舊成員配置了,剩下的過程最終一定會執行完成,Cnew日志最終一定會提交。
還有一種改進方法是繼續保留ACTIVATE消息,但不使用no-op日志,那么怎么樣保證切換到新成員配置的節點具有優先選舉權呢?根據選舉的安全性,具有最新日志的節點具有優先選舉權,那么可以在選舉的時候攜帶節點當前的成員配置,在日志一樣新的情況下,優先給已經切換到新成員配置的節點投票,即可保證切換到新成員配置的節點具有優先選舉權。新成員配置中的大多數節點切換到新成員配置后,不在新成員配置中的節點可以安全下線。
四 總結
Joint Consensus成員變更的提出極大的推動了成員變更的工程應用,其簡潔優美并且通用,但是采用兩階段,一次變更需要提交兩條日志。本文探討了兩階段的Joint Consensus成員變更的單步實現方法,并做了一些改進,為成員變更的工程應用提供了更多的選擇。
五 思考
為什么Cold,new日志提交后,Cnew日志最終一定會提交?
使用ACTIVATE消息讓節點切換到新成員配置后,如果節點重啟,如何保證繼續使用新成員配置?
兩階段成員變更的單步實現還有沒有其它實現方法?