Raft 成員變更的工程實踐

一  引言

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

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

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

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

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

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

如圖 1 是 3 個節點的集羣擴展到 5 個節點的集羣,直接擴展可能會造成 Server1 和 Server2 構成老成員配置的多數派,Server3、Server4 和 Server5 構成新成員配置的多數派,兩者不相交從而可能導致決議衝突。

由於成員變更的這一特殊性,成員變更不能當成一般的一致性問題去解決。爲了解決這個問題,Raft 提出了兩階段的成員變更方法 Joint Consensus。

1  Joint Consensus 成員變更

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

圖 2 Joint Consensus 成員變更

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

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

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

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

2  單步成員變更

圖 3 增加或刪除一個成員

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

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

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

三  Raft 單步成員變更的問題

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

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

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

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

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

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

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

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

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

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

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

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

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

 無法變爲 

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

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

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

四  Raft 成員變更的工程實踐

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

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

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

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

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

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

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

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

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

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

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  單步成員變更日誌使用什麼配置

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

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

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

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

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

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

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

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

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

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

六  總結

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VKu7fBLliztx3tzvzYDkqw