ETCD背后的Raft一致性算法原理

項目中使用ETCD來實現服務發現和配置信息的存儲,最近我抽空研究了一下ETCD和背后的一致性算法 — Raft算法的邏輯。

ETCD是什么

  • ETCD是一個go語言實現的高可靠的KV存儲系統,支持HTTP協議的PUT/GET/DELETE操作;
  • 為了支持服務注冊與發現,支持WATCH接口(通過http long poll實現);
  • 支持KEY持有TTL屬性;
  • CAS(compare and swap)操作;
  • 支持多key的事務操作;
  • 支持目錄操作

簡單的來說,ETCD可以看做是一個no sql的存儲,存的是key-value的node,每個node又可以像樹形結構一樣產生子node。它是集群化的運行狀態來保證高可用,并且對外提供了一套簡單友好的交互接口。

其實ETCD暫時就想介紹這么多,本文的重點在于Raft算法,只是我機智的考慮到站內的SEO才加上ETCD的名號:smirk:,以后會陸續寫一些其他與ETCD相關的內容。

一致性的基礎:Raft算法

ETCD實現高可靠的基礎在于Raft算法,也是理解ETCD工作原理最重要的一部分。類似于zookeeper的zab協議(Paxos算法),Raft也是用于保證分布式環境下多節點數據的一致性,但更易于理解。

看了很多相關Raft算法的技術文章,要么是介紹的過于簡單,要么是過于晦澀難懂。最后看了原始的論文In search of an Understandable Consensus Algorithm和infoQ上對應的中文翻譯Raft 一致性算法論文譯文才對整個邏輯有細致的理解。

首先來看看Raft大致的原理,這是一個選主(leader selection)思想的算法,集群總每個節點都有三種可能的角色:

  • leader
    對客戶端通信的入口,對內數據同步的發起者,一個集群通常只有一個leader節點
  • follower:
    非leader的節點,被動的接受來自leader的數據請求
  • candidate:
    一種臨時的角色,只存在于leader的選舉階段,某個節點想要變成leader,那么就發起投票請求,同時自己變成candidate。如果選舉成功,則變為candidate,否則退回為follower

數據提交的過程

先看前兩種角色,leader扮演的是分布式事務中的協調者,每次有數據更新的時候產生二階段提交(two-phase commit)。在leader收到數據操作的請求,先不著急更新本地數據(數據是持久化在磁盤上的),而是生成對應的log,然后把生成log的請求廣播給所有的follower。

每個follower在收到請求之后有兩種選擇:一種是聽從leader的命令,也寫入log,然后返回success回去;另一種情況,在某些條件不滿足的情況下,follower認為不應該聽從leader的命令,返回false。例如下圖,leader收到客戶端的寫請求,我們暫時不考慮請求的具體值,虛線表示leader先寫log,

leader寫log

然后告訴所有的follower準備提交數據,先和我一樣寫log,

同步log

然后回到leader,此時如果超過半數的follower都成功寫了log,那么leader開始第二階段的提交:正式寫入數據,然后同樣廣播給follower,follower也根據自身情況選擇寫入或者不寫入并返回結果給leader。繼續上面的例子,leader先寫自己的數據,然后告訴follower也開始持久化數據,

leader持久化并同步數據

最終所有節點的數據達成一致,圖中用實線表示已提交的數據。

數據一致

這兩階段中如果任意一個都有超過半數的follower返回false或者根本沒有返回,那么這個分布式事務是不成功的。此時雖然不會有回滾的過程,但是由于數據不會真正在多數節點上提交,所以會在之后的過程中被覆蓋掉。

選舉的過程

上面只說了常規時候兩種角色是如何協調工作的,還剩下candidate沒說,對,就是一個follower是如何逆襲成為leader的。

初始狀態下,大家都是平等的follower,那么follow誰呢,總要選個老大吧。大家都蠢蠢欲動,每個follower內部都維護了一個隨機的timer。如下圖,

每個follower都有timer

在timer時間到了的時候還沒有人主動聯系它的話,那它就要變成candidate,同時發出投票請求(RequestVote)給其他人。特殊情況如下圖,S1和S3都變成了candidate,

轉變為candidate

當然選不選就是人家的事了,原則是

每個follower一輪只能投一次票給一個candidate,

對于相同條件的candidate,follower們采取先來先投票的策略。如果超過半數的follower都認為他是合適做領導的,那么恭喜,新的leader產生了,如下圖,S3變成了新一屆的大哥,又可以很開心的像上一節一樣的正常工作了。

所有follower接受candidate的大哥身份

但是如果很不幸,沒有人愿意選這個悲劇的candidate,那它只有老老實實的變回小弟的狀態。

選舉完成之后,leader靠什么來確保小弟是跟著我的呢?答案是定時發送心跳檢測(heart beat)。小弟們也是通過心跳來感知大哥的存在的。如下圖

leader定期發心跳檢測

同樣的,如果在timer期間內沒有收到大哥的聯絡,這時很可能大哥已經跪了,如下圖,所有小弟又開始蠢蠢欲動,新的一輪(term)選舉開始了。

新的一輪選舉

好了,Raft算法的大致原理就是這樣了,下面我們來說說一些沒說到的細節問題。

選舉時會產生的問題

之前說過,在選舉階段,每個follower如果在自身的timer到期之后都會變成candidate去參與選舉。所以就這個candidate身份而言,是沒有特別條件的,每個follower都有機會參選。但是,在分布式的環境里,每個follower節點存儲的數據是不一樣的,考慮一下下圖的情況,在這些節點經歷了一些損壞和恢復。此時S4想當leader,

不適合的candidate

但是如果S4成功當選的話,根據leader為上的原則,S4的log在index為4-7的數據,會覆蓋掉S2和S3的8。如何解決這樣的沖突的問題呢?有兩種方法:第一種是S4在變為大哥之前,先向所有的小弟拿數據來保證自己數據是最全的;第二種方法是其他小弟遇到這樣資歷不足的大哥想上位的時候,完全不予以理睬。Raft算法認為第一種策略過于復雜,所以選擇了第二種,保證數據只從leader流向follower。S4在vote請求中會帶上自身數據的描述信息,包括:

  1. term,自身處于的選舉周期
  2. lastLogIndex,log中最新的index值
  3. lastLogTerm,log中最近的index是在哪個term中產生的

S2和S3在收到vote請求時候會和自身的情況進行對比,每個節點保存的數據信息包括:

  1. currentTerm,節點處于的term號
  2. log[ ],自身的log集合
  3. commitIndex,log中最后一個被提交的index值

對比的原則有:

  1. 如果term < currentTerm,也就是說candidate的版本還沒我新,返回 false
  2. 如果已經投票給別的candidate了(votedFor),則返回false
  3. log匹配,如果和自身的log匹配上了,則返回true

這個log匹配原則(Log Matching Property)具體是:

如果在不同日志中的兩個條目有著相同的索引和任期號,則它們所存儲的命令是相同的。

如果在不同日志中的兩個條目有著相同的索引和任期號,則它們之間的所有條目都是完全一樣的。

這樣就可以一直等到含有最新數據的candidate被選上,從而保證領導人完全原則(Leader Completeness):

如果一個日志的index在一個給定term內被提交,那么這個index一定會出現在所有term號更大的領導人中。

好了,繼續看圖說話。S4的vote請求,

term lastLogIndex lastLogTerm
10 6 7

被無情的拒絕。接下來S3也變成了candidate,

S3變成candidate

一直等到S3變成了candidate,發出vote請求。

term lastLogIndex lastLogTerm
11 6 8

被S4和S10接受,變成新的leader,并初始化兩個數組:

  1. nextIndex[ ],表示需要發給每個follower的下一個日志條目的索引(初始化為leader最新log的index+1,因為leader總是先假定所有的follower和自己是一致的,后面說明當有不一致的時候是如何協商的)
  2. matchIndex[ ],表示已經復制到每個follower的log的最高index值(從0開始遞增)

在這個例子中,S3中的這兩個數組會初始化為,

S1 S2 S4 S5
nextIndex 7 7 7 7
matchIndex 0 0 0 0

數據更新的問題

現在新的一屆leader選舉出來了,雖然選舉的過程保證了leader的數據是最新的,但是follower中的數據還是可能存在不一致的情況。比如下圖的S4,這就需要一個補償機制來糾正這個問題。

在正常情況下,S3會給S4發心跳請求(一種名叫AppendEntries請求的特殊格式,entries為空),其中攜帶一些數據信息,包括,

term prevIndex prevTerm entries commitIndex
11 6 8 [ ] 6

commitIndex之前已經解釋過了,是log中最后一個被提交的index值。prevIndex與lastLogIndex類似,都是最新的日志的index值,只是屬于不同的請求類型。
prevTerm也與lastLogTerm類似,是prevLogIndex對應的term號。

S4在接收到該請求之后會做一致性的判斷,規則包括,

  1. 如果 term < currentTerm返回 false
  2. 如果在prevLogIndex處的log的term號與prevLogTerm不匹配時,返回 false
  3. 如果一條已經存在的log與新的沖突(index相同但是term號不同),則刪除已經存在的日志和它之后所有的日志,返回true
  4. 添加任何在已有的log中不存在的index,返回true
  5. 如果請求中leader的commitIndex > 自身的commitIndex,則比較leader的commitIndex和最新log index,將其中較小的賦給自身的commitIndex

結果與規則2不符合,返回false給S3。這時S3需要做一次退讓,修改保存的nextIndex數組,將S4的nextIndex退化為6

S4的nextIndex退化為6

再次發送AppendEntries詢問S4

term prevIndex prevTerm entries commitIndex
11 5 8 [ ] 5

如此循環的退讓,一直到nextIndex減小到4

nextIndex減小到4

S3此時發送的請求為,

term prevIndex prevTerm entries commitIndex
11 3 3 [ ] 3

S4和自己的log匹配成功,返回true,并告訴leader,當前的matchIndex等于3。S3收到之后更新matchIndex數組,

S1 S2 S4 S5
nextIndex 7 7 4 7
matchIndex 0 6 3 0

并發送從nextIndex之后的數據(entries),

term prevIndex prevTerm entries commitIndex
11 3 3 [8] 4

S4再根據覆蓋的原則,把自身的數據追平leader,并拋棄之后的數據。

S4的index4同步為leader的內容

這樣消息往復,數據最終一致。

一些其他的問題

還有一些值得注意的特殊情況,比如log的清理。log是以追加的方式遞增的,隨著系統的不斷運行,log會越來越大。Raft通過log的snapshot方式,可以定期壓縮log為一個snapshot,并且清除之前的log。壓縮的具體策略可以參考原論文。

還有集群節點的增減。當網絡發生波動的時候,節點可能需要增減甚至發生網絡分區。具體參考:ETCD系列之二:部署集群

總結

Raft是一種基于leader選舉的算法,用于保證分布式數據的一致性。所有節點在三個角色(leader, follower和candidate)之中切換。選舉階段candidate向其他節點發送vote請求,但是只有包括所有最新數據的節點可以變為leader。

在數據同步階段,leader通過一些標記(commitIndex,term,prevTerm,prevIndex等等)與follower不斷協商最終達成一致。當有新的數據產生時,采用二階段(twp-phase)提交,先更新log,等大多數節點都做完之后再正式提交數據。

以上的圖片來自github上raft算法的算法動畫的截圖。

(完)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容