文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:
- 瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 + 漲薪必備
- 瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 面試必備 + 大廠必備 + 漲薪必備
- 資源寶庫: Java程序員必備 網盤資源大集合 價值>1000元 隨便取 GO->【博客園總入口 】
- 獨孤九劍:Netty靈魂實驗 : 本地 100W連接 高並發實驗,瞬間提升Java內力
- 技術、面試交流:和大廠 小伙伴、技術高手、架構師 進行 純粹的的技術問題交流、探討求助、問題圍觀學習
推薦: 瘋狂創客圈 高質量 博文
高並發 必讀 的精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
Zookeeper 分布式鎖 (圖解+秒懂+史上最全) | Webflux(史上最全) |
SpringCloud gateway (史上最全) | TCP/IP(圖解+秒懂+史上最全) |
10分鍾看懂, Java NIO 底層原理 | Feign原理 (圖解) |
更多精彩博文 ..... | 請參見【 瘋狂創客圈 高並發 總目錄 】 |
史上最全 Java 面試題 28 專題 總目錄
raft算法之所以容易理解,其一是他將一致性問題划分成幾個子問題,這幾個子問題都是獨立、可理解和解釋的。從傳統的思維來講,對於一個復雜的系統或者工程,都是大化小,分解實現,然后去嘗試融合解決整體邏輯。
1、Raft 詳解
Raft 算法
是分布式系統開發首選的共識算法
。比如現在流行 Etcd、Consul、Nacos。
如果掌握
了這個算法,就可以較容易地處理絕大部分場景的容錯
和一致性
需求。比如分布式配置系統、分布式 NoSQL 存儲等等,輕松突破系統的單機限制。
Raft 算法是通過一切以領導者為准的方式,實現一系列值的共識和各節點日志的一致。
1.1 Raft 角色
跟隨者(Follower):普通群眾
,默默接收和來自領導者的消息,當領導者心跳信息超時的時候,就主動站出來,推薦自己當候選人。
候選人(Candidate):候選人
將向其他節點請求投票 RPC 消息,通知其他節點來投票,如果贏得了大多數投票選票,就晉升當領導者。
領導者(Leader):霸道總裁
,一切以我為准。處理寫請求、管理日志復制和不斷地發送心跳信息,通知其他節點“我是領導者,我還活着,你們不要”發起新的選舉,不用找新領導來替代我。
如下圖所示,分別用三種圖代表跟隨者、候選人和領導者。
1.1 數據庫服務器
現在我們想象一下,有一個單節點系統,這個節點作為數據庫服務器,且存儲了一個值為 X。
左邊綠色的實心圈就是客戶端,右邊的藍色實心圈就是節點 a(Node a)。Term 代表任期,后面會講到。
客戶端向單節點服務器發送了一條更新操作,設置數據庫中存的值為 8。單機環境下(單個服務器節點),客戶端從服務器拿到的值也是 8。一致性非常容易保證。
但如果有多個服務器節點,怎么保證一致性呢?比如有三個節點:a,b,c。如下圖所示。這三個節點組成一個數據庫集群。客戶端對這三個節點進行更新操作,如何保證三個節點中存的值一致?這個就是分布式一致性問題。Raft 算法就是來解決這個問題的。當然還有其他協議也可以保證,本篇只針對 Raft 算法。
在多節點集群中,在節點故障、分區錯誤等異常情況下,Raft 算法如何保證在同一個時間,集群中只有一個領導者呢?下面就開始講解 Raft 算法選舉領導者的過程。
1.2 初始狀態
初始狀態下,集群中所有節點都是跟隨者的狀態。
如下圖所示,有三個節點(Node) a、b、c,任期(Term)都為 0。
Raft 算法實現了隨機超時時間的特性,每個節點等待領導者節點心跳信息的超時時間間隔是隨機的。比如 A 節點等待超時的時間間隔 150 ms,B 節點 200 ms,C 節點 300 ms。那么 a 先超時,最先因為沒有等到領導者的心跳信息,發生超時。如下圖所示,三個節點的超時計時器開始運行。
1.3 發起投票
當 A 節點的超時時間到了后,A 節點成為候選者,並增加自己的任期編號,Term 值從 0 更新為 1,並給自己投了一票。
- Node A:Term = 1, Vote Count = 1。
- Node B:Term = 0。
- Node C:Term = 0。
1.4 成為領導者的簡化過程
我們來看下候選者如何成為領導者的。
- 第一步:節點 A 成為候選者后,向其他節點發送請求投票 RPC 信息,請它們選舉自己為領導者。
- 第二步:節點 B 和 節點 C 接收到節點 A 發送的請求投票信息后,在編號為 1 的這屆任期內,還沒有進行過投票,就把選票投給節點 A,並增加自己的任期編號。
- 第三步:節點 A 收到 3 次投票,得到了大多數節點的投票,從候選者成為本屆任期內的新的領導者。
- 第四步:節點 A 作為領導者,固定的時間間隔給 節點 B 和節點 C 發送心跳信息,告訴節點 B 和 C,我是領導者,組織其他跟隨者發起新的選舉。
- 第五步:節點 B 和節點 C 發送響應信息給節點 A,告訴節點 A 我是正常的。
1.5 領導者的任期
英文單詞是 term,領導者是有任期的。
-
自動增加:跟隨者在等待領導者心跳信息超時后,推薦自己為候選人,會增加自己的任期號,如上圖所示,節點 A 任期為 0,推舉自己為候選人時,任期編號增加為 1。
-
更新為較大值:當節點發現自己的任期編號比其他節點小時,會更新到較大的編號值。比如節點 A 的任期為 1,請求投票,投票消息中包含了節點 A 的任期編號,且編號為 1,節點 B 收到消息后,會將自己的任期編號更新為 1。
-
恢復為跟隨者:如果一個候選人或者領導者,發現自己的任期編號比其他節點小,那么它會立即恢復成跟隨者狀態。這種場景出現在分區錯誤恢復后,任期為 3 的領導者受到任期編號為 4 的心跳消息,那么前者將立即恢復成跟隨者狀態。
-
拒絕消息:如果一個節點接收到較小的任期編號值的請求,那么它會直接拒絕這個請求,比如任期編號為 6 的節點 A,收到任期編號為 5 的節點 B 的請求投票 RPC 消息,那么節點 A 會拒絕這個消息。
-
一個任期內,領導者一直都會領導者,直到自身出現問題(如宕機),或者網絡問題(延遲),其他節點發起一輪新的選舉。
-
在一次選舉中,每一個服務器節點最多會對一個任期編號投出一張選票,投完了就沒了。
假設一個集群由 N 個節點組成,那么大多數就是至少 N/2+1。例如: 3 個節點的集群,大多數就是 2。
1.6 防止多個節點同時發起投票
為了防止多個節點同時發起投票,會給每個節點分配一個隨機的選舉超時時間。這個時間內,節點不能成為候選者,只能等到超時。比如上述例子,節點 A 先超時,先成為了候選者。這種巧妙的設計,在大多數情況下只有一個服務器節點先發起選舉,而不是同時發起選舉,減少了因選票瓜分導致選舉失敗的情況。
1.7 觸發新的一輪選舉
如果領導者節點出現故障,則會觸發新的一輪選舉。如下圖所示,領導者節點 B 發生故障,節點 A 和 節點 B 就會重新選舉 Leader。
- 第一步 :節點 A 發生故障,節點 B 和節點 C 沒有收到領導者節點 A 的心跳信息,等待超時。
- 第二步:節點 C 先發生超時,節點 C 成為候選人。
- 第三步:節點 C 向節點 A 和 節點 B 發起請求投票信息。
- 第四步:節點 C 響應投票,將票投給了 C,而節點 A 因為發生故障了,無法響應 C 的投票請求。
- 第五步:節點 C 收到兩票(大多數票數),成為領導者。
- 第六步:節點 C 向節點 A 和 B 發送心跳信息,節點 B 響應心跳信息,節點 A 不響應心跳信息。
1.8 Raft 算法的幾個關鍵機制
Raft 算法通過以下幾個關鍵機制,保證了一個任期只有一位領導,極大減少了選舉失敗的情況。
- 任期
- 領導者心跳信息
- 隨機選舉超時時間
- 先來先服務的投票原則
- 大多數選票原則
2. Raft算法的典型應用
Raft算法的典型應用包括:
-
領導選舉
-
日志復制
對於一個集群只有一個leader(領導),那么我們就很容易理解。只要領導操作同步到對應的followers(跟隨者),數據必然一致。當leader宕機,需要進行領導選舉。
日志復制其實就是同步操作數據的過程。leader將操作日志同步到其他節點。
安全性:如何安全的同步,在不同的情況,我們都能保證一致性,這也就是安全性需要考慮的問題。
其實就是如此,raft首先假設了領導選舉。然后實現了日志復制,最后在安全問題上解決上面的漏洞問題。
1.領導選舉
目的:當集群初始化或者領導gg的時候選出一個新的領導。畢竟一個集群不能沒有領導,如果沒有,那么這個集群就不可用了。
觸發機制:通過心跳。
這幅圖展現了跟隨者、候選者和領導者之間的狀態轉換關系,下面主要介紹他們的轉換流程。
跟隨者
如果他能持續從領導者或者候選者接收到有效的RPCs,那么他的狀態就不會變。一直保持跟隨者身份。但如果沒有持續接受,也就是在一段時間沒收到有效的RPCs,那就選舉超時,他會變成候選者。
候選者
要變為候選者,也就意味着他要開啟一輪新的選舉。那么跟隨者會增加自己的任期號,轉為候選者。
成為候選者后,自己會並行發送一個為自己投票的RPCs請求 給其他服務器。
成為候選者的整個過程也會保持當前狀態,知道滿足下面三個條件
- 他贏得選舉,轉變為領導制
- 其他節點贏了,他轉為跟隨者
- 一段時間沒有任何人獲勝。說明選票被瓜分,重復執行。
這里需要注意的點:
1.對於同一任期號,每個節點一會投一張票。比如服務器A作為候選者生成了編號為5的任期號,那么如果接收到其他節點的編號為5的任期號的投票請求,他會忽略。這個過程遵循的是先來先投的原則。
2.候選者接收到領導者的聲明。會判聲明中RPC的任期號,如果比自己的還小,那么他還會保持候選者身份,否則轉為跟隨者。
3.對於上面第三點,重復執行,Raft采用隨機超時選舉時間進行了優化。降低選票瓜分的可能性。
2.Raft日志復制
直接去理解日志復制,是很容易的,客戶端的一條指令到達,領導者會為這條指令創建一條日志條目,並且並行發送到其他跟隨者。當日志被安全復制(所謂安全復制后面會有),領導會將日志應用到狀態機(比如如果是mysql的insert,那么就是執行insert操作),然后響應客戶端。
如上圖,每條日志都會有對應的任期號,和指令。
每個日志都會有對應的索引。
raft日志匹配特性
1.如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么他們存儲了相同的指令。
2.如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么他們之前的所有日志條目也全部相同。
第一點:一個任期只有一個領導人,並且領導人在一個任期中對於同一索引日志,只會創建一條日志,是不會改變的,是確定的。這就保證第一點成立。
第二點:要想全部相同,就要保證跟隨者得到的日志是領導者發送的順序附加上去的。領導者在發送新的日志時,會附加這條日志之前日志的索引和任期號。如果跟隨者發現數據匹配,才會附加上去,否則拒絕。就是一個個狀態保證了日志的匹配特性。
對於日志不一致的現象,raft是通過跟隨者強制復制領導者的日志來保證的。
如上圖,對於a-f,最終都會和leader同步,也就是說,d會丟棄日志。f的對應日志也會被丟棄和覆蓋。
其實就是通過日志覆蓋解決。但是對於日志覆蓋,我們就會想到一個問題,會不會覆蓋已經提交的日志(日志對應指令已經返回給客戶端)。那當然不會,如果真有這樣,就會有不一致,或者指令丟失現象。
那么如何去做覆蓋跟隨者日志
其實就是跟隨者在append日志的時候,會進行錯誤校驗。
在候選者成為領導者的時候,會為每個跟隨者初始化一個nextIndex數組,數組的值初始化為自己最后條日志+1,其實就是理想化狀態,默認認為日志都已經同步成功。但是理想總會因為各種原因導致不正確,就用上面那張圖。leader會初始化所有nextIndex為11,但是在同步日志的過程中。f節點會出現校驗錯誤的響應。因為f節點10索引對應的日志和leader10索引對應的日志不相同(主要是根據任期號判斷)
這里再強調一下為什么根據任期號就可以判斷日志是否一致,就是上面所說的日志匹配原則。
3.Raft算法的安全性
我們明白了如何選舉和日志復制,但是沒有考慮安全性問題。其實我上慢提到,比如一個宕機很久的跟隨着會被選為領導者,進行日志覆蓋操作會有丟失問題。
其實解決這個辦法很簡單,就是在領導選舉的時候,只能讓安全的節點當leader,所謂安全,就是對應節點擁有當前領導者已經提交的所有日志。Raft就是這么做的。
Raft中節點在投票的時候,會判斷被投票的候選者對應的日志是否至少和自己一樣新。如果不是,則不會給該候選者投票。
日志比較的方法:
1.最后一條日志的任期號。如果大說明新。如果小,說明不新。如果相等。跳到2
2.判斷索引長度。大的更新。
還有一個問題,就是領導人不能保證一個已經在大多數節點存在的日志是否已經提交。
a、b、c、d、e代表不同的任期階段
(a)S1是leader。同步任期2的數據給S2
(b)S1宕機,S5當選(S3、S4、S5投票),產生任期3的日志
(c)S5宕機,S1恢復當選(同步任期2的數據給S3)。
(d)S1宕機,S5當選(因為他的任期日志比其他的都新),復制了任期3的所有數據。
假如說在c階段,S1提交了任期2的數據,那么如果出現d,則會導致任期2數據被覆蓋,丟失。也就是說,S1在任期4時候,不能保證已經在大多數節點存在的日志(任期2的日志)是否提交。
所以raft永遠不會通過計算副本數目的方式(大多數存在)去提交一個之前任期內(任期2)的日志條目。只有領導人當前任期里的日志條目通過計算副本數目可以被提交(e階段)。這樣之前任期的數據也會被提交。
那這里我理解一點就是,加入S1當選為leader,如圖c狀態,那么,如果不再有新的日志出現,任期2對應的日志就不會提交。那么會導致客戶端對應的任期2請求失敗。
跟隨者和候選人宕機
這個就比較容易理解了,宕機的話RPCs就會失敗,Raft通過無限重試卻解決這個問題。
所以對於每個RPCs,做到冪等和無限重試,在節點恢復后,就還是會保證一致性狀態。
Raft集群成員變化
對於集群成員配置變化,如果直接更新每台機器配置,那么就會有安全性問題。以為對於同一時刻,不同節點使用的不同的配置去執行算法邏輯,這就是不安全的。
如圖,藍色代表新的配置。綠色代表老的配置。Old狀態有三台機器Server1、2、3。 New加入兩台server4、5。
那么隨着配置時間應用的不同,可能會導致選舉出兩個leader。
比如server1、2使用老配置,那么1和2都有可能被當選為leader。3、4、5使用心得配置,他們之中的一個也會被當選為leader。
其實這個問題原因就是節點使用了不同配置執行算法邏輯。為了解決這個問題,raft采用兩階段方法(其實只需要保證不會讓新或者舊配置單獨作出決定就行)
raft吧配置當作普通日志形式去提交。
為了實現兩階段,引入了C(old、new)配置。
還有一點就是,一點一個新的配置日志增加到對應的節點日志中,那么該節點就會立刻使用這條新的日志配置。
對於C(old、new)配置,其實就是只有同時滿足old和new配置的時候才會生效。
這樣理想狀態下,如果擁有C(old、new)配置的節點當選為leader。並且提交了該配置,那么說明C(old、new)配置已經在大多數節點應用。下次選舉的產生的leader日志中必然會有該配置。這個時候在創建一條新的C(new)配置提交,即可。