raft算法_Raft算法与实现
強一致性、高可用的存儲組件是構建現代分布式系統的必要條件,廣泛應用于注冊中心、配置中心等平臺設施中,分布式鎖、協調器等等各類場景需求也有相關需求,在該領域有眾多知名的開源組件,如etcd、zookeeper、Tikv等等。
共識算法是實現這類組件的關鍵算法。簡單的說共識算法是協調多個節點達成共識的算法,這是構建高可用系統的基石。大部分典型的共識算法都是基于狀態復制機來實現的,每個節點都有一個狀態機以及一個日志,狀態機用于保持一個共識的結果,這樣客戶端只要連接到任意節點上就可以獲取到狀態機的內容。而日志是用于保證輸入的順序,只要保證所有日志的提交順序是一致的則可以保證各個節點的狀態機是一直的。
共識算法有很多種,就我知道的有paxos、raft、zab、VR(Viewstamped Replication)等等。paxos以復雜和難以理解著稱,通常來說raft是更容易理解的算法,也更容易在工程實踐中采用,而且在性能上并沒有損失,所以下面我們討論一下raft的的基本原理和一些要點。 下面的文章主要基于raft的paper整理而成,相關資料可以從https://raft.github.io/獲取。
raft算法原理與實現
raft最主要的貢獻在于2點,一個是將復雜的分布式算法分解成幾個獨立的解釋和解決的子問題,并且。將狀態進行簡化,最小程度的考慮必要的問題,這些工作使得raft共識算法成為一種有效的新的方法。廣泛的應用于教學和工程實踐中: raft算法的基本流程是首先選舉出leader,由leader完成日志復刻的功能。如果follower僅僅接受來自leader的日志復制請求,而沒有反向的日志流。raft的leader是一個強大的leader。當follower的日志和leader的日志不同時會強制覆蓋自己的日志給follower。當leader宕機時,會選出新的leader,來繼續任務。
raft講上述的基本流程拆分成3個部分:選主過程(leader election)、日志復制(log replication)、安全性(safety)加上成員變更(membership changing)。不過在具體討論這4個部分的內容的過程中我們還是簡單的討論下raft的一些關鍵模型和概念:
角色和周期
節點的角色
raft的典型節點可以分為3種:follower、candidate、leader。
- follower:所有節點在初始化之后都是follower。follower的職責僅僅是接受來自leader的復制日志的請求,或是在leader宕機時(未在超時時間內接受到心跳信息)成candidate嘗試成為leader,亦或是接受到candidate的投票請求時響應改請求
- candidate:當leader宕機時,超過超時時間的follower會轉變成candidate,candidater會向集群內的所有機器發送請求投票給自己的請求。
- leader:leader負責日志復制的過程。并且將自己的日志通過心跳發送給其他機器同步日志,所有來自客戶端的寫請求都會由leader處理,在過半數的機器提交之后就可以返回成功標志給客戶端。
任期周期
raft工作流程由一個個任期組成,通常來說任期由2部分組成一部分是選舉階段,在該階段集群會嘗試進行選主,選主如果成功則進入了常規操作階段。在選舉階段是沒辦法響應客戶端請求的,只有進入常規操作階段才能正常的提供服務。 當然也會存在沒有選出主節點的情況,這時,會開啟一個新的任期。此外還有一點需要說明,一個集群內的節點可能在同一時刻處于不同的任期中的,因此任期本身是有編號的,如果節點接受到更高任期節點發送過來的請求則會更新自己的任期并轉變成follower。 在raft中,實施上只需要2中rpc,一種是由候選人發出的請求投票信息,用于在選主階段將請求其他服務器投票給自己,另一種是附加條目請求,由領導人發起,用來復制日志并提供一種心跳機制。
Leader election
raft使用一種從leader到follower的心跳機制來探活機器,如果在一個心跳周期內follower如果沒有收到來自leader的心跳,則follower會認為集群內沒有leader節點,自動轉成candidate并發起投票請求。 在選舉過程中follwer會將任期加1,并且轉換成candidate持續發送請求,直到下面3個情況發生: (1)贏得半數以上的票數贏得選舉。每個服務器會對一個任期投出一張選票,會投票給最先到來的請求。 (2)其他服務器成為領導者,候選人也會接收到其他機器的投票請求rpc,如果接收到投票請求所屬的任期小于當前任期,候選人則會承認該請求發送者的領導者地位,并轉換成follower,如果rpc所屬的任期比自己小就拒絕掉該請求。 (3)一段時間后沒有,這種情況下是可能有同時存在多個候選人,因此沒有一個候選人獲取了大多數人的支持,這種情況下則需要結束當前任期開始一個新的任期。為了避免重復這種現象,rafr使用隨機選舉超時時間的方法來確保很少發生選票瓜分的情況。 其實這里還涉及到一個選舉超時時間的設定。基本上來說raft的算法要求系統滿足下面的時間要求: 廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障間隔時間(MTBF) 廣播時間是節點發送rpc的品能根據rt時間,故障時間通常至少有幾個月。因此選項時間大概在幾十或者幾百毫秒比較合適。
Log replication
領導者接收到的來自客戶端的請求后會給在本地日志附加一套新的記錄,并且并發的發送該記錄給其他機器中。當這條日志條目被安全的復制,領導人會應用這條日志條目到他的狀態機中,并且把結果返回給客戶端。如果有follower沒有返回請求,則leader會不斷重試。
每一條日志條目存儲一個條狀態機指令和從領導人收到這條指令時的任期號。日志中的任期號用來檢測是否出現不一致的情況。每條日志也有一個整數索引值表明他的位置。 當一個日志對應的rpc接收到大多數機器的響應則說明該日志已經被提交了,raft會保證已提交的日志會被可用的狀態機執行。一旦當前的日志被提交,該條日志之前的所有日志都會被提交。包括有其他領導人創建的、以及此前任期創建的日志。領導人會保持當前被提交的日志的最大索引號,該索引會跟隨追加條目的rpc,因此該索引號會被其他服務器提交。 raft的日志具有日志匹配的特性:
- 如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么他們存儲了相同的指令。領導人最多在一個任期里在指定的一個日志索引位置創建一條日志條目,同時日志條目在日志中的位置也從來不會改變。
- 如果在不同的日志中的兩個條目擁有相同的索引和任期號,那么他們之前的所有日志條目也全部相同。因為在發送附加日志 RPC 的時候,領導人會把新的日志條目緊接著之前的條目的索引位置和任期號包含在里面。如果跟隨者在它的日志中找不到包含相同索引位置和任期號的條目,那么他就會拒絕接收新的日志條目。
然而在leader宕機的情況下,附加日志的rpc會不一致。如上圖所示,follower的日志可能比leader多或者少,這種情況下leader會強制復制自己的日志,從而簡化操作。 為了使得follower和自己的日志一致,領導人必須找到最后2者一致的地方。然后刪除從這個點那之后的所有日志。為了找到這個不一致的其實日志,leader在發送追加日志的rpc的時候回針對每個follwer維護一個nextIndex。每當leader進入常規階段時,會將nextIndex初始化為leader的最后一條日志,會將這個nextIndex給發送給其他follwer。如果follwer和當前機器不一致則會返回失敗,而這時leader會把該follwer對應的nextIndex-1,跟隨下一次追加日志的rpc發送,直到找到不一致的日志起始點。
Safety
前面的章節里描述了 Raft 算法是如何選舉和復制日志的。然而,到目前為止描述的機制并不能充分的保證每一個狀態機會按照相同的順序執行相同的指令。這里需要討論一些特殊的限制:
選舉限制
在基于leader 的一致性算法中,領導人必須存儲已經提交的所有日志。為了保證這一點raft的策略是在投票階段杜絕這種情況。RPC 中包含了候選人的日志信息,然后投票人會拒絕掉那些日志沒有自己新的投票請求。 Raft 通過比較兩份日志中最后一條日志條目的索引值和任期號定義誰的日志比較新。如果兩份日志最后的條目的任期號不同,那么任期號大的日志更加新。如果兩份日志最后的條目任期號相同,那么日志比較長的那個就更加新。
提交之前任期內的日志條目
下圖描述了一種之前未討論過的情況
如圖的時間序列展示了為什么領導人無法決定對老任期號的日志條目進行提交。在 (a) 中,S1 是leader,部分的復制了索引位置 2 的日志條目。在 (b) 中,S1 崩潰了,然后 S5 在任期 3 里通過 S3、S4 和自己的選票贏得選舉,然后從客戶端接收了一條不一樣的日志條目放在了索引 2 處。然后到 (c),S5 又崩潰了;S1 重新啟動,選舉成功,開始復制日志。在這時,來自任期 2 的那條日志已經被復制到了集群中的大多數機器上,但是還沒有被提交。如果 S1 在 (d) 中又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然后覆蓋了他們在索引 2 處的日志。反之,如果在崩潰之前,S1 把自己主導的新任期里產生的日志條目復制到了大多數機器上,就如 (e) 中那樣,那么在后面任期里面這些新的日志條目就會被提交(因為S5 就不可能選舉成功)。 這樣在同一時刻就同時保證了,之前的所有老的日志條目就會被提交。 為了消除這種場景,Raft 永遠不會通過計算副本數目的方式去提交一個之前任期內的日志條目。只有當前任期里的日志條目通過計算副本數目可以被提交;一旦當前任期的日志條目以這種方式被提交,那么由于日志匹配特性,之前的日志條目也都會被間接的提交。 Cluster membership change
多節點的成員變更
前3個小節的內容基本描述了整個raft的主要內容,但是在工程實踐中還有一個問題需要討論,如果完成節點的配置替換,或者希望新增或者刪除節點改如何處理呢?在如上圖的情況中,在新舊2個配置的集群中可能會同時存在2個組,分別產生2個leader的情況,違反了safety。 raft的原文中提出了一種2階段提交的方式:
基本思路是不允許舊配置和新配置同時進行決策,而是在2者之間加入一段Joint Consensus的過渡時期。具體的做法來說:向leader發送一個集群配置變更的請求Cold,new,leader將改日志復制給其他節點,嘗試commit,如果失敗了就重試,如果commit成功說明大部分節點都有該日志了,這時即使leader宕機了,重新啟動的節點也是擁有該節點的。這時leader發送一個Cnew狀態變更指令,帶到這個指令commit之后就可以只使用新配置來選主了。
Single Cluster MemberShip Change
這里描述的方式雖然可行,但是實踐中大部分不會采用這么麻煩的方式,一種策略也是Diego在其博士論文中提出的Single Cluster MemberShip Change,其基本思路就是上問中出現兩個leader的根本原因是2個集群的節點不存在重疊,也就是說無法存在一個仲裁者來覺得采用哪個配置。
如果所示,實際上如果每次只變更一個節點,要獲得大多數集群的投票新舊集群就必定有相交,Single Cluster MemberShip Change定了一種configuration LogEntry添加到日志,configuration LogEntry代表了集群配置的更新。Diego還論證了configuration LogEntry只要追加到日志就行了,因為如果configuration LogEntry沒有被提交其影響也僅僅是系統配置回滾到原始配置而已。 當然這種方式也有問題在于如果前一次的configuration LogEntry還沒有完全提交,新一次的configuration LogEntry就被寫入了也可能導致老配置被回滾而配置錯誤,不過一般情況下,配置變更是人為可控的,完全可以等到新配置生效后再進行下一個節點的配置。 不過即使是這樣實踐中還是有更簡單的方式比如etcd就是等到configuration LogEntry完全應用之后再進行下一次配置變更。
raft 語義與概念
最后我們總結一下raft的實現要點
狀態
狀態所有服務器上持久化currentTerm服務器最新任期號(初始化為 0,持續遞增)votedFor在當前獲得選票的candidateIdlog[]日志條目集;每一個條目包含一個用戶狀態機執行的指令,和收到時的任期號
狀態所有服務器上非持久化commitIndex已知的最大的已經被提交的日志條目的索引值lastApplied最后被應用到狀態機的日志條目索引值(初始化為 0,持續遞增)
狀態所有服務器上非持久化nextIndex[]對于每一個服務器,需要發送給他的下一個日志條目的索引值(初始化為leader最后索引值加一)matchIndex[]對于每一個服務器,已經復制給他的日志的最高索引值
rpc協議
追加日志
請求參數解釋termleader的任期號leaderIdleader的 Id,以便于跟隨者重定向請求prevLogIndex新的日志條目緊隨之前的索引值prevLogTermprevLogIndex 條目的任期號entries[]準備存儲的日志條目(表示心跳時為空;一次性發送多個是為了提高效率)leaderCommit領導人已經提交的日志的索引值
返回參數解釋term當前的任期號,用于領導人去更新自己success跟隨者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志時為真
接收者實現:
請求投票
請求參數解釋termleader的任期號candidateId請求選票的候選人的 IdlastLogIndex候選人的最后日志條目的索引值lastLogTerm候選人最后日志條目的任期號
返回參數解釋term當前的任期號,用于領導人去更新自己voteGranted候選人贏得了此張選票時為真
接收者實現:
規則
所有服務器:
- 如果commitIndex > lastApplied,那么就 lastApplied 加一,并把log[lastApplied]應用到狀態機中
- 如果接收到的 RPC 請求或響應中,任期號T > currentTerm,那么就令 currentTerm 等于 T,并切換狀態為跟隨者 follower:
- 響應來自候選人和領導者的請求
- 如果在超過選舉超時時間的情況之前都沒有收到領導人的心跳,或者是候選人請求投票的,就自己變成候選人 candidate:
- 在轉變成候選人后就立即開始選舉過程自增當前的任期號(currentTerm)給自己投票重置選舉超時計時器發送請求投票的 RPC 給其他所有服務器
- 如果接收到大多數服務器的選票,那么就變成領導人
- 如果接收到來自新的領導人的附加日志 RPC,轉變成跟隨者
- 如果選舉過程超時,再次發起一輪選舉 leader:
- 一旦成為領導人:發送空的附加日志 RPC(心跳)給其他所有的服務器;在一定的空余時間之后不停的重復發送,以阻止跟隨者超時
- 如果接收到來自客戶端的請求:附加條目到本地日志中,在條目被應用到狀態機后響應客戶端
- 如果對于一個跟隨者,最后日志條目的索引值大于等于 nextIndex,那么:發送從 nextIndex 開始的所有日志條目:如果成功:更新相應跟隨者的 nextIndex 和 matchIndex如果因為日志不一致而失敗,減少 nextIndex 重試
- 如果存在一個滿足N > commitIndex的 N,并且大多數的matchIndex[i] ≥ N成立,并且log[N].term == currentTerm成立,那么令 commitIndex 等于這個 N
總結
以上是生活随笔為你收集整理的raft算法_Raft算法与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tracepro杂散光分析例子_光刻机的
- 下一篇: java包名和类名可以一样吗_Java入