Raft算法,從學習到忘記


Raft算法,從學習到忘記

--Raft算法閱讀筆記。

--Github

概述

說到分布式一致性算法,可能大多數人的第一反應是paxos算法。但是paxos算法一直以來都被認為是難以理解,難以實現。So...Stanford的Diego Ongaro和John Ousterhout提出了Raft算法,這是一個更容易理解的分布式一致性算法,在算法的論文中,不僅詳細描述了算法,甚至給出了RPC接口定義和偽代碼,這顯然更加容易應用到工程實踐中。這兩個算法在一定程度上是相通的,個人覺得Raft是加了更多約束的Poxas衍生算法。

log entry:每個entry包含狀態機的command term:用來標示是那一輪選舉,每次選舉都會遞增 index:log entry的標示,term和index唯一標示一個entry commit:將log entry應用到狀態機中

服務器狀態

圖中是Raft中的Repliated status machine architecture,每個server維護一份log,在這些log中記錄的是應用到state machine中的commands。各個服務器中的狀態機被apply一致的log的命令,從而保持集群中服務器的最終的一致性。

如下圖所示,在Raft算法中,服務器有三種狀態:Follower, Candidate, Leader。

  1. 在服務器剛啟動的時候,都屬於Follower狀態,它接收Leader的RPC請求。如果經過一定時間沒有發現Leader,那么它轉換到Candidate狀態,進而開始Leader的選舉。
  2. Candidate發現集群中已經有Leader的時候便會轉換到Follower狀態,又或者它在選舉中獲得大多數節點(majority of servers)的選票變成Leader。
  3. Leader只有在發現比自己高term的Leader的時候才會轉換成Follower。

RPC Interface

為了保證各個服務器的一致性,Raft算法有一些不變式是要保證的。

  • Election Safety: 在每一個term中最多只有一個leader會被選舉出來。
  • Leader Append-Only: 每個leader只能append自己的log,而不能覆蓋或者刪除它。
  • Log Matching: 如果兩條log具有相同的index和term,那么它們之前的logs應該是一致的。
  • Leader Completeness: 如果一個log在一個term中被committed,那么它會出現在所有更高term的leader的log中。
  • State Machine Safety: 如果一個server已經把一個log entry應用到她的狀態機,那么沒有其他的server可以應用一個具有同樣term和index的不同的log entry。

Raft算法給出了服務器狀態的變量,以及所所用到的RPC接口的定義和偽代碼:

Status

Persistent state on all servers
在RPC調用返回之前會被更新到穩定的存儲中
currentTerm:    server發現的最高的term
voteFor:        在當前term中投票給了哪一個candidate
log[]:          log entries,entry包含應用到狀態機的命令和從leader得來的term和command

Volatile state on all servers
commitIndex:    已盡被提交的log的最大log entry。
lastApplied:    已經被應用到status的最大log entry。

Volatile state on leaders
在選舉后重新初始化
nextIndex[]:    要發給各個follower的下一個log entry的index。
matchIndex[]:   各個服務器當前已被replicated的log entry的最大index。

AppendEntriesRPC

leader用於replicate log entries,也用作心跳。

Arguments
term:           leader's term
leaderId:       follower用於讓client重定向
prevLogIndex:   新log entry的前一個entry的index
preLogTerm:     新log entry前一個entry的term
entries[]:      需要同步的log entries,如果用作心跳,那么這個參數為空
leaderCommit:   leader's commitIndex

Results
term:           當前term
success:        如果follower有entry符合preLogIndex和preLogTerm,則返回true,否則返回false。

被調用方的實現:
1. replay false if term < currentTerm
2. replay false 如果沒有entry符合preLogIndex和preLogTerm
3. 如果存在與新entry沖突的entry,那么把現有的沖突entry和往后的entries刪除
4. Append any new entries not already in the log
5. if leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry)

RequestVote RPC

candidate用於收集選票
Arguments
term: candidate's term
candidateId: candidate requesting vote
lastLogIndex: index of candidate's last log entry
lastLogTerm: term of candidate's last log entry

Results
term: currentTerm, for candidate to update itself
voteGranted: true means candidate received vote

被調用方實現:
1. Reply false if term < currentTerm
2. If voitedFor is null or candidateId, and candidate's log is at least as up-to-date as receiver's log, grant vote.

Rules for Servers

All Servers

  • If commitIndex > lastApplied: increment lastApplied, apply log[lastApplied] to state machine
  • If RPC request or response contains term T > currentTerm: set currentTerm = T, convert to follower 

Followers

  • Respond to RPCs from candidates and leaders
  • If election timeout elapses without receiving AppendEntries RPC from current leader or granting vote to candidate: convert to candidate

Candidates

  • On conversion to candidate, start election:
    • Increment currentTerm
    • Vote for self
    • Reset election timer
    • Send RequestVote RPCs to all other servers
  • If votes received from majority of servers: become leader
  • If AppendEntries RPC received from new leader: convert to follower
  • If election timeout elapses: start new election

Leaders

  • Upon election: send initial empty AppendEntries RPCs (heartbeat) to each server; repeat during idle periods to prevent election timeouts
  • If command received from client: append entry to local log, respond after entry applied to state machine
  • If last log index ≥ nextIndex for a follower: send AppendEntries RPC with log entries starting at nextIndex
    • If successful: update nextIndex and matchIndex for follower
    • If AppendEntries fails because of log inconsistency:decrement nextIndex and retry
    • If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm: set commitIndex = N

Leader Election

在一個server轉換成candidate后就會++term,並且用RequestVoteRPC發起選舉。對於一個server來說,一輪選舉的結果有三種:

  1. 贏得選舉變成leader
  2. 另一個server贏得選舉,自己變成follower
  3. 沒有winner,開始新一輪選舉。

在一輪選舉中:

  1. 選票是先到先得,也就是說一個server收到一個RequestVoteRPC,如果請求投票的server滿足voitedFor is null or candidateId, and candidate's log is at least as up-to-date as receiver's log, 那么就將選票給它。
  2. 當一個server獲得大多數選票的時候,它成為leader,並向其他服務器發送心跳,告知新的leader已經產生。
  3. 如果一個server收到不低於自己term的心跳,說明已經產生了leader,這個server轉換成follwer。
  4. 如果一輪選舉沒有winner,那么心跳超時以后在隨機延時之后將發起新一輪的選舉。以避免形成活鎖

Log Replicate

一個log entry由leader接收client的請求而產生,通過appendEntryRPC同步集群中的其他服務器,在多數節點返回以后,leader向client返回處理結果。並且commit已經完成replicated的log entry。

在leader對一個entry做commite操作的時候,同時也會將這個entry之前的entrise一並做commit。在AppendEntries RPC中,會帶上leader已知被commite的entries的最大index,這樣follower就根據這個index將自己的log entrise中在index之前的entrise應用到狀態機中。

當AppendEntriesRPC中的preLog不存在於follower的log中,那么follower返回false,然后leader遞減該follower的nextIndex,並重新發送AppendEntriesRRC直到找到leader跟follower共同擁有的log entry。如果follower中存在leader中沒有的log entries,那么講使用leader的log進行覆蓋。

對於AppendEntriesRPC的實現,也有人提出一些優化的方案,比如說在返回false的時候附帶follower的last entry的信息,但是論文作者認為在實踐中失敗並不是經常發生,並且增加這些優化機制會增加算法實現的復雜度,index遞減的機制已經可以保證在實踐中算法的性能,所以沒有必要去使用更加復雜的機制。

對於上一個term的entries,如果它們沒有被commite,那么新的leader也不會去計算它們備份的數量來判斷它們是否應該commite,而是通過commite新leader自己提出的entry,從而把之前的entries也提交了,見Log Matching

Cluster membership

這個小節討論集群配置變化的情況。在實踐中,集群的配置不可能完全沒有變化,那么如何處理配置變化的情況就是一個需要解決的問題。最簡單的方式莫過於停止所有的服務器,更改配置,然后重啟。但是這樣在實際的工程實踐中一般是很難被接受的,因為需要完全停止服務。

為了配置變更的安全性,那么必須保證在任意的時間里在同一個term中不能有兩個server同時被選舉成為leader,否則就產生了分布式系統中所謂的腦裂。不幸的是任何直接將server從老的配置變更到新的配置的方式都是不安全的。因為在轉變的過程中,集群又可能分化為兩個(majorities)集群。

如上圖所示,由於每個服務器的轉變時間不一樣在某個時刻可能會有兩個leader選出。在這個例子中就是集群中的服務器從三台增加到五台的情況,在同一個term中就有可能會出現以老的配置Cold的一個集群和使用新的配置的一個集群。

為了保證配置更新的安全性,配置的變化必須使用兩段的策略。必入有一些系統使用第一階段停止老的配置,使之不能響應客戶端的請求,第二階段使新的配置生效。

在Raft算法中,集群首先會進入一個過渡的配置,我們稱之為joint consensus,在這個階段新老配置同時存在。一旦joint consensus被committed,那么系統將轉換到新的配置。

在joint consensus中,新老配置同時存在:

  1. Log entries are replicated to all servers in both con- figurations.
  2. Any server from either configuration may serve as leader.
  3. Agreement(forelectionsandentrycommitment)re- quires separate majorities from both the old and new configurations. 這個指的是一個agreement需要新老Configuration兩個集群中的各自的大多數都同意。

集群的configuration使用一個特殊的entries來保存和傳輸,當leader收到configuration從Cold到Cnew的變更的請求,它將configuration保存成Cold,new,並且將它replicate到其他server中。當server將new configuration entry保存到它的log中,它將使用新的configuration來做將來的決策(server都會使用最新的configuration,不管它是否被提交)。也就是說leader使用Coldnew的規則來判斷什么時候Cold,new被提交。如果這時候leader掛掉了,那么新的leader有可能具有Cold或者Cold,new,這要取決於它是否已經收到了Cold,new。在這種情況中,Cnew不能單方面的做出決定。

當Cold,new已經被提交,那么Cold和Cnew都不能在沒有對方同意的情況下單方面的作決定。在這之后leader就可以產生一個描述Cnew的entry,並且將他replicate到集群中。當Cnew被commite,就可以將Cnew中沒有包括的server安全的關閉了。

在任何時候Cold和Cnew都不能單方面的作出決定。這就保證了安全性。

不過還有3個延伸的問題:

  1. 新的server可能沒有任何日志,那么在與leader同步之前它可能並不能commite任何entry。Raft為這種情況添加了一個額外的phase,這個階段讓新加入的server與leader同步,但是它不被認為是majorities(non-voting members)。
  2. 如果新選出的leader不是new configuration的一員,那么在它commite完Cnew,他將主動卸任,轉換成follower。也就是說他要管理一個不包括它自己的集群,直到Cnew已經被commmite。
  3. 被下限的機器會因為收不到機器而超時,他會增加自己的term,並向集群中的機器發起選舉。因為它的term會比集群中的機器高,但是preLog卻不夠up-to-date,所以Cnew的機器總是會被選舉成新的leader。但是下線的機器又回超時而重復觸發選舉,從而降低可用性。為了解決這個問題,Raft使用了一個簡單而有效的策略,如果server確認leader還存活,也就是在心跳的超時時間之內,它將忽略RequestVoteRPC請求。這個策略既沒有修改選舉的核心策略,卻能完美的解決了下線機器的困擾。

Log compaction

Raft的log會隨着時間的推移而越來越大,但是在實際的實踐中並不能讓日志沒有限制的增長,這會使得占用空間和replay的時間增加。與Chubby和ZooKeeper等很多分布式系統一樣,Raft也使用Snapshot來壓縮自己的log。在Snapshot中,當前的系統狀態會被記錄並存儲下來,而之前的log就可以刪掉了。當然,使用log cleaning和LSM樹也是可以的,log clean需要對Raft算法做一定的修改,當然也會給算法帶來額外的復雜度;狀態機可以實現LSM樹並用相同的snapshot接口。

如圖,在snapshot中,會包括當前的狀態機狀態,並且會包括snapshot中的最后一個entry的index和term,這是為了讓AppendEntriesRPC調用的時候能夠檢查到preLog的信息。snapshot中還會包涵最新的configuration。 雖然各個server是獨立的進行snapshot的操作,但是當一個節點落后太多的時候leader還是需要向該節點發送snapshot來進行一致性的同步。這種情況發生在leader生成snapshot的時候把要發送給該節點的nextLog從log中刪除掉了。 下面是Leader給其他節點發送snapshot的RPC接口:

InstallSnapshot RPC

Arguments
term:       leader's term
leaderId:   讓follower能夠重定向請求
lastIncludedIndex:  the snapshot replaces all entries up through and including this index
lastIncludedTerm:   term of lastIncludedIndex
offset:     byte offset where chunk is positioned in the snapshot file
data[]:     raw bytes of the snapshot chunk, starting at offset
done:       true if this is the last chunk

Results
term:currentTerm, for leader to update itself

Receiver impleamentation
1. Reply immediately if term < currentTerm
2. Create new snapshot file if first chunk (offset is 0)
3. Write data into snapshot file at given offset
4. Reply and wait for more data chunks if done is false
5. Save snapshot file, discard any existing or partial snapshot with a smaller index
6. If existing log entry has same index and term as snapshot’s last included entry, retain log entries following it and reply
7. Discard the entire log
8. Reset state machine using snapshot contents (and load snapshot’s cluster configuration)

另外,如果server收到一個snapshot指向之前的entries,那么它將覆蓋snapshot指向的entries,並且保留之后的log entries,這種情況發生在網絡不穩定而重傳的時候。

生成snapshot也是需要消耗io和時鍾周期的,所以snapshot的時機也是需要考慮的。一個簡單的策略就是文件達到一定的大小就進行snapshot的生成。而對於磁盤io性能的消耗,一般使用,一般使用copy-on-write的技術,使得在snapshot的時候還是可以接收客戶端請求。例如把狀態機以支持寫時復制的數據結構實現。另外,一些操作系統的寫時復制機制也可以加以利用(Alternatively, the operat- ing system’s copy-on-write support (e.g., fork on Linux) can be used to create an in-memory snapshot of the entire state machine (our implementation uses this approach))

Client interaction

與其他主從結構的分布式系統一樣,Raft算法中寫請求的操作全都由leader來進行,如果follower接收到了寫請求,那么它將拒絕請求,並且提供leader的地址,供client訪問。在leader crash的時候有可能會使得client以為之前的請求沒有執行而發起重復的請求,從而使得請求被執行兩次,這個問題的解決辦法是客戶端在請求中附帶一個線性遞增的序列號,而狀態機則追蹤各個client的最新的序列號。

讀請求可以不經過leader,但是如果不加額外限制讀請求可能會讀到過期的數據,這種情況發生在讀請求由leader處理,並且這個leader是一個新的leader,它可能還不知道哪些entries已經被commite了,或者leader正好與集群斷開了連接。對於第一種情況,Raft算法要求leader在自己成為leader之后先commite一個空的entries(no-op entry)這樣它就擁有了哪些entries已經被commite的信息。對於第二種情況,Raft算法要求leader在處理只讀請求的時候,需要先獲得大多數節點的心跳成功。

小結

在讀完Raft算法的論文以后,最大的感受就是Raft算法就如它提出的理由一樣,簡單、易於理解(KISS原則有沒有),並且可以很好的指導工程實踐。但是還是需要看一下Paxos,可以借助Raft去理解它,畢竟是用講故事的方式描述的算法~

另外,突然覺得log就是分布式的核心啊。。(別問我為什么,我就是那么覺得的)

引用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM