深入淺出 Raft - Membership Change

在豬爸爸的努力下,泥坑銀行終于能高效正常的運作了,但豬爸爸一直比較擔(dān)心海盜島那邊的網(wǎng)點,因為他總是擔(dān)心跨海的通訊會因為極端情況出現(xiàn)問題。果不其然,一個雷雨交加的晚上,海盜島的發(fā)電站被擊中,整個島處于停電狀態(tài),海盜島的網(wǎng)點沒法正常工作了。雖然狗爺爺盡了很多努力,讓海盜島重新供電也花了一天時間。

第二天,豬爸爸去見了兔小姐:『兔小姐,我覺得我們不能再將網(wǎng)點放在海盜島上面,因為這個島上面的情況太復(fù)雜,很容易就因為極端天氣導(dǎo)致網(wǎng)點不可用?!?br> 『看起來是的,豬爸爸,那么我們怎么辦呢?』
『我們需要再找?guī)讉€安全的地方設(shè)立新的網(wǎng)點,順帶將海盜島的網(wǎng)點移除?!?br> 『是的,豬爸爸,可是我們要怎么做呢?直接找一個地方就對外提供服務(wù)嗎?或者為了更加安全,我們直接多設(shè)置幾個網(wǎng)點?』
『當(dāng)然不是的,兔小姐,為了保證數(shù)據(jù)的安全,我們需要做很多事情,防止我們在添加網(wǎng)點的時候出現(xiàn)多個 Leader 的情況?!?br> 『哦,好復(fù)雜,豬爸爸,你能詳細解釋一下嗎?』
『好的,兔小姐。』

Problem

『首先我們來看現(xiàn)在面臨的問題,為了更好的舉例,我就仍然以之前選舉的例子,成員 A,B,C 來舉例吧,兔小姐?!?br> 『好的,豬爸爸?!?br> 『假設(shè)現(xiàn)在 A 已經(jīng)是 Leader 了,我們有三個成員,這時候要加入兩個新的成員,D 和 E,因為 D 和 E 在加入的時候,其實是會知道之前的 A,B,C,所以我們需要處理的問題就是如何讓 A,B,C 能知道新加入的 D 和 E?!?br> 『恩,這是一個問題?!煌眯〗愠了嫉?。
『最簡單的做法,就是我們有另外一個全局協(xié)調(diào)者,先依次告訴 A,B,C 休息一下,別開會了,第二天 D 和 E 要加入,這樣大家都知道了最新的成員信息,然后第二天,就是五個成員開始競選 Leader 了。這個方式最好,但有一個問題就是一段時間會議沒法進行,對我們銀行來說,也就意味著不能正常對外提供服務(wù)?!?/p>

這里, 其實就是一個集群成員變更常見的問題,當(dāng)我們添加或者刪除節(jié)點的時候,如何讓其他的節(jié)點知道成員變更了。最通常的做法,可能就是通過一個全局的協(xié)調(diào)器,譬如 zookeeper 或者 etcd 這種的,做一個 Two Phase(2 PC) 的變更,但這樣其實是有問題的,先不說 2 PC 一些 corner case 需要處理,整個過程還可能會導(dǎo)致暫時的服務(wù)不可用,雖然這個時間在多數(shù)情況下面可能比較短,所以 Raft 這邊采用了另外一種做法,我們繼續(xù)說明。

Add/Remove one Node

『那怎么辦呢?』兔小姐繼續(xù)問道。
『為了保證在進行成員變更的時候,仍然能正常工作,我們可以這樣做。不知道你還記不記得,當(dāng)一個 Leader 被選出來之后,所有的操作都是由 Leader 進行的。兔小姐?』
『當(dāng)然記得,豬爸爸?!?br> 『好的,所以這里,如果我們要添加新的成員,我們就可以直接告訴 Leader,然后讓 Leader 再去告訴其他的 Follower,當(dāng)這條消息被大多數(shù)成員確認(rèn)了,我們就知道新的成員已經(jīng)加入了?!?br> 『這方法不錯,我們也不需要額外在用一個外部的協(xié)調(diào)人員了?!?br> 『是的,兔小姐,但這個方法也有一個問題,就是一次只能進行一個成員的變更,不能進行多個?!?br> 『哦,這是怎么回事?』兔小姐困惑的問道。
『繼續(xù)上面的例子,A,B,C,A 現(xiàn)在是 Leader,然后我們告訴 A 要加入了 D 和 E, A 給 B 和 C 發(fā)了消息,當(dāng)我們確認(rèn)這條消息被 committed 之后,A,B,C 都可能會 apply 這條消息,并且知道了 D 和 E 已經(jīng)加入了?!?/p>

豬爸爸稍微休息了一會,繼續(xù)說道:『那么,這時候,就有可能出現(xiàn)一種情況,C 先 apply 了這條消息,然后 C 就知道了 D 和 E,但 A 和 B 因為還沒 apply,不知道 D,E。然后 C,D,E 三個就可能選出新的 Leader,譬如 E,因為根據(jù)我們之前的討論,五個成員只需要三個成員同意,但這個時候,A 還認(rèn)為自己是 Leader,于是我們這里出現(xiàn)了兩個 Leader?!?br> 『那我們?nèi)绾稳シ乐惯@樣的事情發(fā)生呢?』
『很簡單,兔小姐,我們一次只能進行一個成員的變更。譬如上面的例子,我們只能先加入 D,然后等 D 加入成功之后,才能加入 E?!?br> 『那這樣為什么就沒有問題呢?』
『我們繼續(xù)上面的例子,C 仍然先 apply 知道了 D,但 C 和 D 這邊知道現(xiàn)在是四個成員,也就是至少需要三個成員投票才能選出 Leader,而 A 和 B 這時候因為根本還不知道 D,而且 A 仍然還是 Leader, 所以 A 和 B 都不可能給 D 投票。也就是說,只要我們能保證每次成員添加的時候,新的成員集合仍然跟老的成員集合大部分的成員是重合的,那么就不會有任何問題。』
『恩,我想我大概明白了,那么對我們來說,現(xiàn)在要做的就是先移除掉海盜島的網(wǎng)點,當(dāng)海盜島的網(wǎng)點移除成功了,我們在加入一個新的網(wǎng)點,是吧,豬爸爸?』
『非常正確,兔小姐!』

在 Raft 的博士論文里面,當(dāng) Leader 收到 Configuration Change 的消息之后,它就將新的配置(后面叫 C-new,舊的叫 C-old) 作為一個特殊的 Raft Entry 發(fā)送到其他的 Follower 上面,任何節(jié)點只要收到了這個 Entry,就開始直接使用 C-new。當(dāng) C-new 這個 Log 被 committed,那么這次 Configuration Change 就結(jié)束了。當(dāng)在 TiKV 以及 etcd 里面,我們并沒有使用這種方式,只有當(dāng) C-new 這個 Log 被 committed 以及被 applied 之后,節(jié)點才知道最新的 Configuration 的情況。這樣做的方式是比較簡單,但需要注意幾點:

  1. 當(dāng) Log 里面有一個 Configuration Change 還沒有被 committed,不允許接受新的 Configuration Change 請求,主要是為了防止出現(xiàn)多 Leader 情況。
  2. 如果只有兩個節(jié)點,需要移除一個節(jié)點,如果 Leader 在發(fā)起命令之后,另一個節(jié)點掛了,這時候系統(tǒng)沒法恢復(fù)了。

Snapshot

好了,我們繼續(xù)回到小鎮(zhèn)銀行這邊,兔小姐跟豬爸爸選好了新的場地 - 森林小徑,然后就準(zhǔn)備開始海盜島的網(wǎng)點替換工作了。但這時候,兔小姐突然想到一個嚴(yán)重的問題:『豬爸爸,我們是可以建立新的網(wǎng)點,但這邊新的網(wǎng)點現(xiàn)在可是完全沒有數(shù)據(jù)的呀?!?br> 『這個問題非常的好,兔小姐,我完全考慮到了。一個最簡單的做法,因為我們每筆交易都有記錄,我們將記錄從最開始以此重新發(fā)給新的網(wǎng)點不就可以了。』
『這辦法很不錯,但我們銀行現(xiàn)在已經(jīng)開業(yè)這么久了,記錄都有很多了,而且我們還在不停的交易,如果重頭這么傳輸,不知道要多久呀!』兔小姐擔(dān)心的問道。
『是的,兔小姐,很佩服你能想到這個問題。所以這里我們需要的是一套 snapshot 機制?!回i爸爸回答道。
『Snapshot,這個是什么?』
『就是鏡像,兔小姐。你可以這樣理解,雖然一個客戶進行了很多筆交易,但在某一個時間點上面,客戶的金錢總數(shù)就是一個固定的值?!?br> 『這個我能理解,豬爸爸?!?br> 『所以,我們只需要在一個時間點上面,記錄下客戶當(dāng)前的金錢總數(shù),然后將這條數(shù)據(jù)發(fā)送到新的網(wǎng)點,然后新的網(wǎng)點收到之后,直接在金庫里面給對應(yīng)的客戶設(shè)置好相應(yīng)的金錢。然后對于這個時間點后面新的交易,我們還是按照之前交易記錄發(fā)送的方式來同步了?!?br> 『哦,豬爸爸,聽起來有點糊涂,你能在詳細解釋一下嗎。』
『好的,兔小姐,我這里簡單舉一個例子,假設(shè)我們就一個客戶,這個客戶進行了 100 次交易,也就是我們現(xiàn)在有 100 條交易記錄,100 次交易之后,客戶的金錢是 100 塊錢,我們只需要告訴新的網(wǎng)點這個客戶是 100 塊錢就行了。然后當(dāng)這個客戶繼續(xù)進行第 101 次交易的時候,我們僅僅需要將 101 這次的交易記錄發(fā)給新的網(wǎng)點就可以了?!?br> 『哦,我明白了,豬爸爸?!?/p>

Snapshot 雖然簡單,但需要注意,假設(shè) 3 個節(jié)點,然后新加入了一個節(jié)點,如果 Leader 在給新的 Follower 發(fā)送 Snapshot 的時候,另一個 Follower 當(dāng)?shù)袅?,這時候整個系統(tǒng)是沒法工作了,只有等 Follower 完全收完 Snapshot 之后才能恢復(fù)。為了解決這個問題,我們可以引入 Learner 的狀態(tài),也就是新加入的 Learner 節(jié)點是不能算 Quorum 的,它不能投票。只有 Leader 確認(rèn)這個 Learner 接受完了 Snapshot,能正常同步 Raft Log 了,才會考慮將其變成正常的可以 Vote 的節(jié)點。

Joint Consensus

雖然上面一次進行一個成員變更的方式已經(jīng)能在生產(chǎn)環(huán)境中滿足大部分情況,但還有一種 corner case 我們是沒有辦法解決的。假設(shè)現(xiàn)在我們有 3 個 IDC,用 A,B,C 來表示,每個 IDC 里面有兩臺機器,就是 A1,A2,B1,B2,C1,C2?,F(xiàn)在有一個 Raft 副本在 A1,B1,C1 上面,這時候,如果我們發(fā)現(xiàn) A1 壓力比較大,要將副本轉(zhuǎn)移到 A2 上面,那么有兩種辦法:

  1. 移除 A1,增加 A2
  2. 增加 A2,移除 A1

但無論是上面哪一種方法,都會有風(fēng)險,譬如第一種,當(dāng) A1 移除之后,如果 B1 或者 C1 當(dāng)?shù)?,那么整個集群是不可用的。而對于第二種,A2 增加之后,如果這時候整個 IDC A 當(dāng)?shù)?,那么整個 Raft 集群也是不可用的了。也就是說,我們雖然將數(shù)據(jù)放在了 3 個 IDC 上面,但在一些情況下面,如果一個 IDC 整個當(dāng)?shù)?,都可能引?Raft 集群不可用。

我們可以通過 Learner 的方式緩解這個問題,也就是先增加 A2,但 A2 是 Learner,只有 A2 完全追上了,我們才將 A2 給變成 Voter,然后在移掉 A1。但這個方式只是能減少不可用的概率,并不能完全防止,所以最好的做法就是支持 Joint Consensus 算法。

這個算法其實比較簡單,相對于上面的一次成員變更的算法,它只引入了一個過渡狀態(tài),叫做 joint consensus。當(dāng)一個 Leader 收到成員變更的請求的時候,他首先會將 C-old 和 C-new 都放在 joint consensus 里面(我們叫做 C-old-new),作為一個 Raft Log 發(fā)送給其他的 Followers。當(dāng)節(jié)點收到 Log,不需要等待 Log 被 committed,就可以使用最新的 C-new 配置了,但這時候,仍然只有 C-old 里面的集群能進行 Vote。如果這時候 Leader 當(dāng)?shù)袅?,新選出來的節(jié)點 要不在 C-old 里面,要不在 C-old-new 里面,因為我們前面沒約定 C-old-new 這個 Log 必須 committed。但無論是哪一種 Leader,C-new 這邊的集群都不可能單邊決策的。

當(dāng) C-old-new 被 committed 之后,就進行了 joint consensus 狀態(tài),在這個狀態(tài)里面:

  1. Log 會被復(fù)制到所有在兩個 configurations 里面的節(jié)點上面
  2. 在兩個 configuration 里面的節(jié)點都可能被選為 Leader
  3. 但只有 C-old 里面 majority 和 C-new 里面 majority 都同意,才能選出 Leader 和進行 Log 提交。

當(dāng)進入 joint consensus 之后,Leader 就可以再次提交一個新的 C-new Raft Log,仍然是只要其他節(jié)點收到了這個 Log,就可以使用新的 Configuration 了,當(dāng) C-new 這個 Log 被 committed 了,那么 C-old 就沒用了,不在 C-new 的節(jié)點就可以直接關(guān)閉。這套流程就能保證在任意時候,C-old 和 C-new 不會出現(xiàn)單邊投票的情況。

雖然 joint consensus 很強大,但現(xiàn)在用的最多的仍然是一次成員變更的方法,畢竟很簡單,而 joint consensus 我只在 LogCabin 中看到過,所以這里并沒有很詳細的介紹。一些 corner case 的處理大家可以直接去看論文了。

那沒有 joint consensus,一些極端的 corner case 怎么辦呢?可能就先忍忍唄,或者使用 5 副本,甚至用 7 副本。

小結(jié)

成員變更我認(rèn)為算是 Raft 里面最難的概念,尤其是在 Raft 的 Paper 里面,重點就提到的是 joint consensus 算法,其實比較讓人難以理解。這里其實就體現(xiàn)了一個工程上面的取舍,雖然我知道理論上面 100% 的事情怎么做,但為了更加簡單,我可以稍微放低一點要求。

TiKV 和 etcd 現(xiàn)在都是沒有用 joint consensus 的,但我們現(xiàn)在在開始添加 Learner,后面如果真的遇到了其他的 corner case,會不會考慮一下,沒準(zhǔn)也不是不可能的事情。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容