Raft基本理論

本文以問答的方式,總結Raft相關知識。
所有信息來源於Raft論文

第一章

1.爲什麼要尋找一個新的一致性算法?

Unfortunately, Paxos has two significant drawbacks.
The first drawback is that Paxos is exceptionally difficult to understand.
The second problem with Paxos is that it does not provide a good foundation for building practical implementations.
(1)paxos算法比較難理解,即便有很多人嘗試着用更容易理解的方式去解釋它。education
(2)paxos沒有爲實際實現打好基礎,有許多細節問題沒有公佈,或者業內沒有形成一致意見,以致於在實際構建和部署時,需要做複雜的變更。practice
Because of these problems, we concluded that Paxos does not provide a good foundation either
for system building or for education.
Given the importance of consensus in large-scale software systems, we decided to see if we could design an alternative consensus algorithm with better properties than Paxos. Raft is the result of that experiment.

第二章

1.通常使用什麼方法來解決分佈式系統的容錯問題?舉例?

複製狀態機
Replicated state machines are used to solve a variety of fault tolerance problems in distributed systems, as described in Section 2.2.
Many large-scale storage systems that have a single cluster leader, such as GFS [30], HDFS [105], and RAMCloud [90], use this approach.

2.複製狀態機如何實現?架構?

使用replicated log實現
Replicated state machines are typically implemented using a replicated log, as shown in Figure 2.1.
這裏寫圖片描述
集羣中的每臺服務器上都有複製狀態機,每個狀態機按同樣的順序執行一串相同的命令,從而每個狀態機的狀態也是一致的。
一致性模塊接收客戶端命令並將其寫入到日誌中,每個服務器中的一致性模塊互相通信,保證每臺服務器上寫入日誌的命令內容和順序相同

第三章 Basic Raft algorithm

1.如何實現易懂性?

(1)將單個複雜的問題分解爲若干可以獨立解決,解釋,理解的問題。–問題分解
we divided problems into separate pieces that could be solved, explained, and understood relatively independently.
(2)第二種方法是通過減少需要考慮的情況的數量來簡化狀態空間,使系統更加連貫並儘可能消除不確定性。–狀態簡化
Our second approach was to simplify the state space by reducing the number of states to consider, making the system more coherent and eliminating nondeterminism where possible.

2.簡單說一下Raft。概述。

Raft實現一致性的方法是:首先選出一個server作爲leader,讓leader來全權負責replicate log的管理(利用leader來簡化replicated log的管理)。leader從客戶端接收日誌條目,複製到其他服務器,並且告訴其他服務器什麼時候將日誌應用到狀態機上纔是安全的。數據流動的方向只會是從leader流向其他服務器。
leader在決定將日誌條目放在日誌的哪個位置的時候,不需要和其他服務器商議。

有了在系統中使用leader的這個前提,Raft將一致性問題分解爲三個相對獨立的子問題:
(1)Leader election
(2)Log replication
(3)Safety。(leader變更時的安全性)

(1)leader election ————————- term

At any given time each server is in one of three states: leader, follower, or candidate.
這裏寫圖片描述
通常只有一個leader,其餘的都是follower。
leader處理所有客戶端請求,follower將來自客戶端的請求轉發到leader。
follwoer只響應來自leader和candidate的請求。(passive)
candidate用來選舉新的leader。

Raft將時間分成任意長度的任期(term,連續的整數編號),每個term開始的時候都要進行選舉(elect),一個或多個候選者參與選舉,試圖成爲leader,贏得選舉的server在這個任期的剩餘時間內作爲leader。某些情況下可能無法成功選出一個leader,所以一個term內最多隻有一個leader。
每個server都保存了當前term的數值,server之間通信時,會帶上term的信息。

我們選擇在Raft中將通信結構化爲RPC,以簡化其通信模式。
server使用RPC進行通信,基本的一致性算法只需要兩種RPC(RequestVote RPC和AppendEntries RPC),後續將要介紹的leader的資格轉換會使用其他的RPC。
RequestVote RPC:候選者在elect期間發出
AppendEntries RPC:leader發出的,用於複製日誌條目,和提供心跳機制。

Raft uses a heartbeat mechanism to trigger leader election.
Raft使用心跳機制觸發leader選舉:
server剛啓動時,先以follower作爲開始。leader會週期性的發送心跳信息到各個follower,維護自己作爲leader的地位。如果follower從leader或者candidate收到RPC,則保持follower狀態。如果follower在election timeout時間內沒有收到心跳信息,則認爲沒有leader了,follower發起新的選舉。

follower發起選舉的過程:
增加當前term –> 轉換爲candidate狀態 –> 投票給自己 –> 發送RequestVote RPC給集羣中其他服務器,會有三種結果:
(a)贏得選舉,成爲leader。(相同的term內獲得集羣中多數選票)(在同一個term中,每個服務器最多隻會投票給一個candidate,先收到誰的投票請求,就投票給誰。這保證了在同一個term中最多隻有一個leader。當candidate成爲leader之後會向集羣中其他server發送信息,確保自己的leader地位,避免其他server發起新的選舉。)safety
(b)收到其他server以leader身份發來的信息,轉變爲follower。如果收到其他server作爲leader發來的AppendEntries RPC信息,該信息中包含的term不小於自己的term,則承認其他server的leader地位,並轉換爲follower狀態。如果收到的RPC信息中的term小於自己的term,則拒絕該RPC信息,保持candidate狀態。(收到其他candidate發來的term大於自己的RequestVote RPC信息應該也會轉變爲follower吧???否則新一輪競選的時候,還是無法選出leader)
(c)在election timeout時間內,集羣中沒有選出新的leader。多個server同時轉變爲candidate發起競選,可能沒有一個candidate能贏得多數選票,則沒有新的leader選出來。這種情況下,每個candidate在elect timeout之後增加自己的term,發起新一輪的競選。(如果沒有額外的機制,這種分裂選票導致的無法選出leader的情況可能會不斷重複,解決辦法是,每個candidate的elect timeout不相同,取150-300ms中的隨機數,這樣每個candidate超時的時間不相同,發起競選的時間就會不同,最先發起競選的很可能成爲leader,在其他candidate超時之前,他就已經贏得選舉並且發送心跳信息了。網絡通信耗時一般在1ms以內,隨機數之間的時間差距,提供了充足的選舉時間。分裂選票的情況很少出現,並且能快速解決。)(曾經嘗試使用給每個server指定一個rank的方式來減少split vote,但是遇到了很多問題,沒成功。)We used randomization to simplify the Raft leader election algorithm.(看到的Raft資料中有考慮使用rank的方式)

(2)Log replication ————— term & index

leader選出來之後,就開始處理客戶端請求,客戶端請求中包含需要執行的命令,leader將命令作爲日誌條目追加到日誌中,然後並行發送給集羣中的其他服務器(AppendEntries RPC),日誌被安全的複製到其他服務器之後(判斷條件?超過半數響應),leader將日誌應用到狀態機上,然後返回結果給客戶端。如果有follower crash,或者運行慢,或者網絡丟包的情況,leader會不斷髮送日誌條目直到所有follower都存儲了所有的日誌條目。
每個日誌條目都包含客戶端命令command,term,index。
index用來確定日誌條目在日誌中的位置。
log中的term用來檢測不同server上的日誌是否一致,以及確保一些算法的特性。

committed:A log entry is committed once the leader that created the entry has replicated it on a majority of the servers (e.g., entry 7 in Figure 3.5).多數服務器接收到日誌之後,日誌被提交,之前的所有日誌也都提交。(safety)
leader會記錄已經提交了的日誌條目的highest index,這個index也會包含在後續將要發送給follower的AppendEntries RPC(包括心跳)中,follower得知該日誌條目被提交之後,將該日誌條目應用到狀態機。(何時應用日誌是安全的)

Raft的日誌機制:
(1)不同server上的日誌條目,如果term和index相同,則該條目中保存的命令也肯定相同。(leader在給定的term和index的位置,最多創建一個條目,並且日誌條目的位置(term&index)是不會改變的)—-當前的
(2)不同server上的日誌條目,如果term和index相同,則該日誌條目之前的所有的日誌條目都相同。(AppendEntries RPC一致性檢測,leader在發送AppendEntries RPC時,會將前一個entry的term和index也包含在其中。如果follower在自己的日誌中沒有找到對應的前一個entry,則拒絕新的entry。通過歸納法得知,如果leader發送的AppendEntries RPC返回成功的結果,則leader可以知道,截止到最新的entry,follower的日誌和自己的日誌是相同的。)—之前的

leader的crash可能導致leader和follower之間日誌的不一致,舊的leader可能有一些日誌條目沒有複製出去。follower的日誌可能比leader少,也可能比leader多。
(1)follower的日誌比leader少:
(2)follower的日誌比leader多:
(3)以上兩種都有
這裏寫圖片描述

Raft如何讓follower的日誌和leader的相同?引入match index的概念。
Raft強制讓follower的日誌與leader相同,leader以此來解決日誌不一致的問題。這就意味着follower中與leader不一致的日誌會被覆蓋。通過在leader的競選過程中添加一些限制,可以保證這是安全的。(什麼限制?後面safety部分詳細描述。)
leader首先在follower的日誌中找到一個位置點match index,在這個點之前,leader和follower的日誌相同,然後在follower中刪除該點之後的所有的日誌條目,然後將leader中該點之後的所有日誌條目發送給follower,在此過程中AppendEntries RPC的一致性檢查也會起作用,以此來保證leader和follower的日誌相同。
leader中會維護每個follower的nextindex,nextindex是leader將要發送給follower的下個日誌條目的index。leader剛被選舉出來的時候,會將它自己的日誌中最後一個index的下一個index初始化爲所有follower的nextindex(nextindex=latest index+1),然後對於每一個follower,leader向其發送AppendEntries時,根據日誌一致性檢測機制,如果follower的日誌和leader的日誌不同(檢測term和index,follower.latest index=leader.follower.nextindex-1),則consistency check會失敗,此時leader將該follower的nextindex減小,並且重新發送AppendEntries,一直重複這個過程,直到consistency check成功。當follower的日誌和leader中記錄的nextindex能夠相匹配(nextindex=matchindex+1),然後follower中該index(matchindex)之後的日誌被刪除,leader發送follower缺少的日誌,follower和leader中的日誌最終達到一致。
一些優化手段,可能沒有必要,因爲failure不會經常發生,也不會有那麼多不一致的日誌:
(1)節省帶寬:在leader找到與follower的matchindex之前,可以不發送包含真正log entry的RPC,只發送包含心跳信息的RPC(應該包含term和index),節省帶寬。找到了matchindex之後,再發送log entry。
(2)減少尋找matchindex時AppendEntries RPC被拒絕的次數:
a.leader使用二分查找的方式,尋找follower日誌中第一個與之不同的index
b.follower在拒絕AppendEntries RPC時,將其日誌中當前term的第一個index返回給leader。當有多個term的日誌不一致時,每個term只需要一次比較,而不是將term中的每個log entry都進行比較。
leader不需要執行特別的動作去保持日誌的一致性,它永遠不會刪除或者覆蓋自己日誌中的條目。

Raft的日誌機制保持了不同server之間日誌的連貫性,這個機制不僅簡化了系統的行爲,使系統的行爲可預測,同時它也是保證安全性的一個重要組成部分。
在多數服務器正常運行的條件下,Raft就可以接受,複製,應用新的log entry。每個entry只需要一輪RPC即可複製到多數server上,單個慢的follower不影響整體性能。
日誌複製算法比較容易實現,因爲leader不會爲了趕進度而在單個AppendEntries request中發送超過一個entry。有的算法需要通過網絡發送整個日誌,真正實現需要很多優化,負擔比較大。

(3)Safety ——-如何選出包含所有已提交日誌的server作爲leader
可能會出現什麼問題?
之前的部分介紹了Raft如何選擇leader和複製日誌,但是沒有充分的保證每個狀態機按照同樣的順序執行同樣的命令。比如,一個follower落後leader很多,leader很多提交了的日誌並沒有複製到該follower上,但是該follower可能會被選爲leader,然後覆蓋掉原來的leader上面那些已經被提交的日誌,這可能導致不同的狀態機執行了不同的命令。新的leader缺少部分已提交的日誌。

如何解決這個問題?思路。
Raft算法對於leader的選舉添加了一些限制,以保證新的leader的日誌中包含所有之前已經提交的日誌。每次選新的leader,term的值都會增加,也就是說,新的leader的日誌中包含所有以前的term中已經提交了的日誌。

如何實現?
其他算法的實現:不包含所有已提交的日誌的candidate也能被選爲leader,在選舉的過程中或者稍後,通過額外的機制確定缺失的日誌,並傳輸到新選出的leader上面。增加了需要考慮的機制及複雜性。
Raft的實現:使用一個簡單的方法,保證被選出來的leader,包含之前term中已經提交的所有entry。由此,這意味着日誌流動的方向只有一個,即從leader流向follower。leader不需要刪除或覆蓋自己的日誌。
Raft在選舉的過程中避免了不包含全部已提交日誌的candidate成爲leader。Candidate只有獲得集羣中的多數voter的選票才能成爲leader,在這些voter至少會有一個包含所有已提交的entry。如果Candidate的日誌,至少和集羣中多數voter的日誌一樣新(與集羣中多數server中的日誌相比,相同或者更新),則其必然包含所有已提交了的日誌信息。

具體實現方式:
Candidate發送的RequestVote RPC中包含其log的信息(已提交日誌的term & index ,不是現有日誌的term & index),如果voter中的日誌比candidate中的更新,則voter不會投票給該candidate。——-一定要注意,是比較已提交日誌的term和index,比較未提交的日誌沒什麼意義。
如何比較兩個日誌誰更新?最後一個日誌條目的term和index。先比較term,再比較index。
Raft determines which of two logs is more up-to-date by comparing the index and term of the
last entries in the logs.

複製到多數server上的entry就一定提交了?
一個entry,即便被複制到了集羣中的多數機器上,但是如果leader在提交之前宕機,該entry仍有可能被刪除或覆蓋。
換句話說,新term中選出來的leader,無法判斷出上一個term中的entry是否已經提交,即便該entry已經複製到了集羣中的多數機器上。
圖3.7.舉例說明。(此情況比較複雜,但是解釋的很清楚,是必須要考慮到的情況。)
這裏寫圖片描述
如何解決?
Raft不會使用計算副本數量的方式來提交前面term中的日誌。只有當前term中的日誌,才採用計算副本數量的方式來提交。—-以前的term中的日誌不會直接提交,會間接提交。
一旦當前term中的entry提交了,由於日誌匹配屬性(Log Matching Property)之前term中的entry也就被間接提交了。
有時候,leader可以安全的判斷出entry已經被提交了,但是爲了簡單起見,Raft採取更保守的方法。

安全性證明過程:
根據上面給出的完整的Raft算法,可以更詳細的證明Leader Completeness Property成立。(好麻煩)
先假設Leader Completeness Property不成立,然後證明這是矛盾的。
假設在term T中,leader提交了一個entry,但是這個entry沒有被複制到後來的Term U選出的leader中。
1.由於leader不會刪除或者覆蓋自己的日誌,故在leaderU競選leader時,其日誌中沒有該entry。
2.leaderT將該entry複製到了集羣中的多數server上。leaderU獲得了集羣中多數server的投票。因此,至少有一個server,既保存了leaderT提交的entry,又投票給了leaderU。—-這個voter是造成矛盾的關鍵點。
3.這個voter在投票給leaderU之前,必然已經接收了leaderT發來的提交了的entry。否則就是拒絕了leaderT發來的AppendEntries request(此時它的term大於T)。
4.voter依舊存儲了這個entry,因爲介於U和T之間的每一個leader都存儲了該entry。leader不會刪除或覆蓋自己的日誌。follower只刪除與leader相沖突的日誌。
5.voter投票給了leaderU ,所以leaderU的日誌和voter一樣內保持最新。這導致兩個矛盾。
6.首先,如果voter和leaderU有相同的term,那麼leaderU的日誌至少和voter的一樣新,所以leaderU的日誌包含了voter的log中的每個entry。這是矛盾的,因爲我們假設voter包含了已經提交的entry,但是leaderU不包含。
7.Otherwise, leaderU’s last log term must have been larger than the voter’s. Moreover, it was
larger than T, since the voter’s last log term was at least T (it contains the committed entry
from term T). The earlier leader that created leaderU’s last log entry must have contained the
committed entry in its log (by assumption). Then, by the Log Matching Property, leaderU’s
log must also contain the committed entry, which is a contradiction.
否則,leaderU的最後一個日誌term必須大於voter的日誌term。 此外,leaderU的term大於T,因爲voter的最後一個日誌term至少是T(它包含了來自termT的承諾條目)。 創建leaderU最後一個日誌條目的之前的leader必須在其日誌中包含這個已提交的條目(通過假設)。 然後,通過日誌匹配屬性,leaderU的日誌也必須包含這個已提交的條目,這是一個矛盾。
8.這就形成了矛盾。 因此,term大於T的leader,必然包含了termT期間所有提交的entry。
9.日誌匹配屬性保證了後來的leader必然包含之前間接提交的日誌條目。

這就證明了leader完整屬性,這也證明了狀態機安全屬性(如果一個server按照給的順序應用了log正的entry,則其他server會以同樣的順序執行日誌。每個server上的日誌是相同的。)
========================回頭再仔細看證明過程===============================

3.7 Follower and candidate crashes

follower和candidate的crash比leader的crash處理起來要簡單很多,它倆採用相同的處理方式。
follower和candidate crash之後,發送給他們的RequestVote和AppendEntries RPC信息會返回失敗的結果,Raft會不定期的重試發送這些信息,直到follower/candidate重啓,RPC返回成功的結果。
如果server在接收到RPC之後,還沒來得及響應就crash了,則在server重啓之後,會再次收到相同的RPC,但是這不會產生什麼危害。Raft RPCs have the same effect if repeated, so this causes no harm.如果follower收到與日誌中已經存在的entry相同的AppendEntries request,會忽略這些request中的entry。

3.8 Persisted state and server restarts

Raft服務器必須將足夠的信息持久化到穩定的存儲器中,以便安全地重新啓動服務器。(舉例:term,vote, entry)
特別是term和vote,這是爲了防止在一個term內投票兩次(vote),或者新的leader產生的日誌entry將以前的leader產生的log entry替換掉。
每個server中的log entry在提交之前必須持久化,避免server重啓後,提交了的entry丟失或者變爲未提交的狀態。

其他的信息可以重新創建,重啓不會引起問題。
舉例:commit index,即便所有server同時重啓也不會有問題。服務器重啓先將commit index初始化爲0,在leader選出之後提交新的log entry時,commit index會變成真實的值,並且將該值傳播到所有follower。(應該可以讀取自己的日誌,獲得真正的commit index)

狀態機可以是易失性的或持久性的。
易失性狀態機:最新快照+重新應用log entry
持久性狀態機:記錄last applied index,重啓之後應用該index之後的entry

如果服務器所有的持久化狀態都丟失了,則無法以其先前的身份安全地重新加入羣集。 這種服務器通常可以通過調用集羣成員資格更改的方式,以新的身份重新添加到集羣中。 但是,集羣中的多數服務器丟失了持久化狀態,則已經提交的日誌條目可能會丟失,集羣成員資格更改的也無法進行。如果要系統繼續運行,則系統管理員需要承認數據丟失的可能性。

3.9 Timing and availability

不管系統中的事件發生的是快還是慢,系統都不能產生不正確的結果。(安全性與時間無關)
但是可用性與時間有關。舉例:server間消息傳遞需要的時間,如果大於server crash的時間間隔,則無法選出可用的leader,沒有leader,Raft無法正常工作。

在Raft的leader選舉中,時間很重要。Raft能夠選出並且保持一個穩定的leader的時間要求:
broadcastTime << electionTimeout << MTBF
broadcastTime:Server發送RPC並接受響應的時間,要比election timeout至少小一個數量級,以便leader能夠可靠的發送心跳信息,防止follower開始新的選舉。結合election timeout的隨機方法,可以儘量避免split vote。
electionTimeout :超過該時間,如果follower沒有收到心跳信息的話,則發起新的elect。當leader crash時,系統大概有electionTimeout的時間是不可用的。
MTBF:單個server平均故障間隔時間

broadcastTime和MTBF都是與系統屬性,只有election timeout是我們必須設定的。
接收者需要將RPC中的信息持久化到存儲上,所以broadcastTime大概在0.5-2ms,根據系統存儲的性能而定。因此election timeout可以選擇在10–500ms之間。MTBF一般爲幾個月或更長時間。

3.10 Leadership transfer extension (可選的擴展)

兩種情況下需要用到leader轉換:
1.leader必須下臺(重啓維護或從集羣中移除)。leader下臺,會有election timeout的時間系統是不可用的,直到其他的server超時,發起新的選舉並贏得選舉。
在下臺之前,leader將其角色轉移到其他server,可以儘量避免這段系統不可用的時間。
2.其他server更適合作爲leader。舉例:負載高的服務器不適合作爲leader。廣域網中,主數據中心的服務區更適合作爲leader以儘可能減少客戶端和leader之間的網絡延遲。(其他算法可能在選舉階段就選出了更合適的leader,但是Raft由於要選舉包含最完整日誌的server作爲leader,被選舉出來的可能不是最優的server。Raft的leader會不時的去檢測follower中有沒有更適合作爲leader的server,並且將leader的角色轉給該server,人工操作。)

詳細的leadership transfer步驟:
1.原leader停止接受客戶端的新請求。
2.原leader將所有已提交的log entry傳輸到目標server,保證原leader和目標server日誌一致(正常的日誌複製機制,沒有其他特殊操作)。
3.原leader向目標server發送一個TimeoutNow的請求,這個請求可以使目標server立即發起一輪新的選舉,而不需要等待election timeout的時間。(目標server增加term的值,轉換爲candidate狀態)
這樣基本能保證目標server在其他server election timeout之前就發起新的election並且成爲leader。目標server發送給原leader的信息中也會包含新的term,使原leader下臺。此時,leadership transfer完成。

如何保證transfer異常的情況下儘快恢復響應客戶端請求?–只提供了思路,還沒有真正去實現這個功能。
如果leadership transfer這個操作在一個election timeout的時間內沒有完成,則原leader終止此操作,並重新接受客戶端請求,防止客戶端請求被長期阻塞。如果目標server實際上已經成爲leader,但是原leader依舊錯誤的終止了transfer的操作,則最壞的情況也就是多進行一次選舉,然後就又可以響應客戶端請求了。

3.11 Conclusion

本章解決了一致性相關的所有問題。Raft超越了single-decree Paxos中對於單一值實現一致性的機制,實現了不斷增長的日誌的一致性,這是建立複製狀態機所必須的。它將所有節點達成一致的消息發送到其他服務器,讓其他服務器知道log entry已經提交。Raft通過選擇一個集羣leader,由該leader單獨做決定,並由leader向集羣其他成員發送日誌信息的方式,高效的,可行的實現了一致性。
Raft僅使用少量機制來解決完全一致的問題。 例如,它只使用兩個RPC(RequestVote和AppendEntries)。 創建一個緊湊的算法/實現並不是Raft的最直接的目標,相反,這是設計易懂性的結果,每一個機制都必須充分激活和解釋。 我們發現冗餘或曲折機制很難激發,所以它在設計過程中自然會被清除。
對於不會影響大部分Raft部署的問題,我們沒有在Raft中解決它,所以Raft中有的部分可能不夠成熟(幼稚,比如leader選舉還可以改進,但是在增加複雜性的同時並沒有帶來多少實際的好處),有的又比較保守(比如leader只能提交當前term下生成的日誌)。爲了讓讀者更容易理解,放棄了一些不成熟的優化手段。
本章不可避免的會遺漏一些功能或優化,這些功能或優化在實踐中非常有用。 隨着實施者獲得更多的Raft經驗,他們將瞭解何時以及爲什麼某些附加功能可能會有用,並且他們可能需要在實際部署中實施這些功能。 在整個章節中,我們勾勒了一些我們認爲不必要的可選擴展,但如果需要的話,這可能有助於指導實施者。 通過關注可理解性,我們希望爲實施者根據他們的經驗調整Raft提供堅實的基礎。 由於Raft在我們的測試環境中工作,我們期望這些是簡單直接的擴展,而不是根本性的變化。

第四章 Cluster membership changes

在此之前,我們一直假定集羣成員配置是不變的,但是在實際環境中,集羣配置偶爾需要改變,比如踢除出問題的成員,或者添加新的成員。
有兩種解決辦法:
1.將集羣中所有成員關閉,更改集羣配置,然後重啓集羣。在集羣關閉這段時間,整個集羣不可用。
2.新的server獲取到某個成員的網絡地址,以取代該集羣成員。必須保證被替代的server不會再回來,否則系統的安全屬性無法保證。
這兩種集羣成員變更的方法都有很大缺陷。

如何解決這些問題?
配置變更自動化,將其合併到Raft一致性算法中。只需要對基本的一致性算法進行一些擴展,同時成員變更的過程中,集羣正常運行。

具體實現?
AddServer RPC:將新的server添加到現有集羣。
RemoveServer RPC:將server從現有集羣中移除。

4.1 Safety

爲了防止配置變更時,針對相同的term選出兩個leader的情況,不能使用一次添加或刪除多個server的方式。同時添加或刪除多個server造成集羣選出多個leader的情況見下圖:
這裏寫圖片描述
Raft最初的也在設計如何解決一次添加多個server造成兩個不相交的多數成員的辦法。後來發現一個更簡單的辦法:一次只能添加或刪除一個server,複雜的變更可以由一系列單個server的變更來實現。同時單server變更的方法也更容易理解。

每次添加或刪除一個server,則舊的集羣中的多數成員,和新的集羣中的多數成員肯定有交集,以此來避免新舊集羣被分裂成兩個互相獨立的多數派。

Cluster configurations are stored and communicated using special entries in the replicated log.(也就是上面提到的兩種RPC)

leader收到添加或刪除server的請求後,就將新的配置Cnew添加到自己的log中並使用普通的Raft日誌複製機制複製該日誌條目。Cnew中的server在接受到該log entry之後,配置立即生效(無需等待該entry提交,也就是說server始終使用接收到的最新的集羣成員配置),Cnew中的多數派決定該log entry是否提交。
Cnew的entry被提交後,集羣成員變更就完成了。此時Cnew中的多數成員已經採用了新的Cnew配置,並且不包含在Cnew中的server無法組成多數派去選出另外一個leader了。(可以結合圖4.2講解)

Cnew被提交之後
1.leader可以確認集羣變更成功
2.被移除出集羣的server可以被關閉
3.可以開始新的集羣變更。爲什麼?解釋如下:
因爲每個server都會使用接收到的最新的配置Cnew,不管它是否已經提交。所以爲了防止出現上面截圖中出現的集羣被分爲兩個多數派的情況,在Cnew被提交之前,不能開始新的集羣變更。

server在響應收到的RPC request時不需要理會自己當前的集羣配置,而是要使用調用者的配置(也就是發送request的server的配置)。
爲什麼這麼設計?有兩點原因
1.添加新server。server可以接受自己存儲的集羣配置之外的leader發來的AppendEntries requests,否則新的server永遠也無法添加到集羣中(也就是說如果server的log中沒有集羣配置,那麼她就無法接受任何log entry,但是集羣配置也是通過log entry發來的,也就是說它也無法接收集羣配置,這樣,新的server的log是空的,沒有記錄任何集羣配置信息,也就不可能接受任何log entry,也就無法加入到集羣了。綜上,server必須能接受自己記錄的集羣配置之外的leader發來的log entry)。
2.投票。server可以投票給自己保存的集羣配置列表之外的server(前提條件時被選舉的server包含足夠新的已提交日誌)。這個機制有時候用來保證集羣的可用性。比如往三個server組成的集羣中添加第四個server,此時原有三個server中的一個宕機了,剩餘的兩臺server不能在四server的集羣中形成多數派,也就無法選出新的leader,所以需要新的server的投票。—-是否可以用後面提到的日誌

4.2 Availability

保證集羣可用性的措施:
1.在將server添加到集羣中之前,先讓新的server追趕上集羣中已有的server,避免新的server導致新加入到log中的entry無法提交(無法接收最新的日誌,導致日誌副本無法形成多數派,無法提交)。
添加沒有任何日誌的新server可能威脅到集羣的可用性。圖4.4的兩種情況解釋:
一個新的server在加入到集羣之前,一般都沒有任何日誌。如果以這種狀態加入到集羣中,有兩種可能會導致集羣不可用:
(1)三server的集羣,加入第四個沒有任何日誌,或者日誌遠遠落後於原有集羣的server。此時三server中的一個failure了,則新添加到log中的entry不能提交(不滿足多數原則),此時系統不可用,直到新添加的server日誌追上原有集羣的日誌。
(2)三server的集羣,連續加入三個新的server。新的entry提交時,由於不滿足多數派原則(不夠四個),entry不能提交,系統不可用。
以上兩種情況,在新的server日誌追趕上原有集羣成員日誌之前,集羣是不可用的。

如何解決這個問題,避免可用性出現gap?
引入新的機制。在執行配置變更之前的階段,新加入的server不參與投票。在這個階段中,leader向新加入的server發送日誌,使其能夠追上集羣中現有server,但是新的server不參與選leader時的投票以及日誌提交時計算副本數量。一旦新的server追上了原有的server,纔開始上面提到的配置變更過程。(也就是在這個準備階段,配置變更的信息不會寫入到leader的日誌的中並複製到集羣其他服務器上)
—————–這項技術也可以應用在對一致性要求不太高的只讀查詢上,減少集羣負載。

如何判斷新的server已經追上了原來的server並且可以開始進行集羣變更?
因爲過早添加進集羣容易造成可用性問題。
所以我們的目標是讓集羣不可用的時間小於election timeout的時間,因爲客戶端必須能夠接受偶然的非常短暫的不可用(election timeout數量級不可用,比如leader 失敗)
儘可能的讓新server的日誌接近原有server的日誌
————–如果新添加的server出了問題,或者永遠也不可能追上現有的server,則leader終止添加server這個過程。

如何追日誌?如何判斷新server的日誌已經足夠追上現有server了?
可以將日誌複製分成10輪,第一輪複製截止到複製開始的時間點的已有的全部日誌,第二輪複製第一輪日誌複製期間追加的日誌,後面每輪重複這個過程,每一輪複製的時間會越來越短。如果最後一輪複製日誌的時間小於election timeout的時間,則將server添加到集羣中。這也不會造成長時間的不可用問題。
如果最後一輪複製時間仍大於election timeout的時間,則leader終止配置更新過程並報錯。
但是用戶可以再次執行配置變更操作,因爲之前的操作已經把大部分日誌複製過來了,再次執行變更的話,每輪的時間就更少了。重複這個過程,直到將新server添加到集羣中。
第一步:對於新添加的server,其日誌必須是空的, AppendEntries 的consistency check會一直失敗直到nextindex減小到1,。nextindex會有一個先減小到1,然後隨着日誌的傳輸不斷增加的過程,這個過程是影響server添加性能的主要因素。在第三章中,介紹過獲得server的nextindex的更高效的辦法,針對添加新server這個特殊場景,有一種最簡單的辦法,就是讓follower直接返回自己log的長度,leader可以由此直接確定follower的nextindex。

2.如何將集羣中現有leader逐步移除出集羣?
分兩步:
第一步,將當前leader的資格轉移到其他server上。
第二步,將該server按照普通的成員變更的辦法移除。

3.如何避免已被移除集羣的server干擾現有的集羣?
被移除出集羣的server無法收到entry,也就無法知道自己已經被踢出集羣了,也無法收到心跳信息及日誌信息,因此會election timeout,然後使用新的term發起選舉,然後集羣當前的leader會被下臺變成follower。最終會從Cnew裏面選出一個leader,然後被踢出的服務器再次timmeout開始選舉,如此循環,極大降低了可用性。
Pre-Vote也不能解決這個問題。(被踢出集羣的server可能包含最新的日誌,有資格競選leader)
解決辦法:使用心跳。
server收到RequestVote RPC的時間距離上次收到leader發來的heartbeat信息不超過minimum election timeout時間的話,投票請求被拒絕或推遲,這兩種處理方式,結果是一樣的。也就是說每個server至少要等待minimum election timeout時間才能開始投票。這不會影響正常的投票,因爲正常投票server發起選舉的時間肯定要大於minimum election timeout的。
意思也就是,如果一個leader能夠正常給集羣中的server發送心跳信息,該leader不會被廢除。

這與第三章提到的leadership transfer有衝突(沒有election timeout也可以投票),處理方式就是,在leadership transfer的RequestVote RPC信息中添加一個flag,表明是leader允許該server發起的投票。

4.集羣成員變更算法如何在變更的過程中充分保證集羣的可用性?
證明集羣成員變更期間,依舊可以正常響應客戶端請求並完成集羣成員變更操作。
(假設舊配置和新配置中的多數服務器都可用)
1.配置變更的任何一個步驟中都可以選擇新的leader
Cnew中包含最新日誌的server可以被選爲leader
Cnew未提交的話,既在Cnew中,又在Cold中的包含最新日誌的server,既能得到大部分Cnew的選票,又能得到到部分Cold的選票,所以不管用什麼配置,都能選出新的leader

2.leader在能正常發送心跳的情況下是不會變的,除非它不在Cnew中並且Cnew已經提交(自己願意下臺)。
leader正常發送心跳,自己和follower不會接受新的term投票請求。
leader將自己剔除出集羣的情況下,先提交Cnew然後下臺。Cnew很可能選出一個leader完成配置變更。
但是這會有一小小危險,被踢出出去的leader會再次成爲leader,然後確認已經提交的Cnew,然後下臺。然後Cnew中的server選leader。????????????????與上面的第3個問題是一樣的,有點不好理解,需要再想想。

3.leader可以在配置變更的整個過程中響應客戶端請求
4.leader可以推動並完成配置變更

4.3 Arbitrary configuration changes using joint consensus

多個server一起添加或刪除的實現方式——不需要仔細研究

第五章 Log compaction

隨着日誌不斷增長,如何刪除不需要的日誌。避免存儲空間滿了導致系統不可用,或者日誌太多,啓動時間太長。

原則:過時的,不需要的日誌,可以刪除。
1.更看重實現的簡單性,還是性能?
2.狀態機的大小,存放在磁盤還是內存中?

主要責任在於狀態機,負責將狀態寫入到磁盤並簡化日誌

四種實現方式:基於內存,基於磁盤,基於增量,基於日誌。
不同實現方式的共同點:
1.每個server獨立的處理(簡化截斷)自己的日誌。(除了第四種)
2.Raft發送日誌給狀態機。在將來的某一時刻,狀態機將狀態持久化到磁盤,然後通知Raft丟棄相應的日誌。Raft會記錄被丟棄的日誌的term和index(與持久化了的狀態相關對應)。同時Raft也會記錄該時刻的集羣配置信息。
3.Raft刪除以前的日誌之後,狀態機的責任有兩個:server重啓後加載磁盤中的狀態並應用日誌;創建發送給其他follower的一致性映像。

1.基於內存的狀態機,將快照保存到存儲上之後,快照生成點之前的日誌都可以刪除。 1G-10G
(之前的快照也可以刪除了)
InstallSnapshot RPC,用來發送最新的快照到其他follower。
follower收到snapshot之後,刪除snapshot之前的日誌,應用snaphsot之後的日誌。
如何並行的生成快照,儘量減小對客戶端響應的影響?
寫磁盤時間太長。使用copy-on-write技術實現。fork。子進程寫快照,父進程響應客戶請求。
需要額外內存空間。
什麼時候生成快照?
太頻繁佔有磁盤帶寬,太少則重啓的時候需要時間長。
只有在需要發送一致性映像文件到其他server的時候才需要快照。
簡單的辦法,當日志增大到某個值的時候生成快照。—值設的太大,節省了磁盤帶寬,但日誌佔用空間可能偏大。
更好的辦法是當前日誌大小是當前快照大小的N倍時生成快照,但是計算當前快照大小太麻煩。
合理的辦法是使用上一個快照的大小來計算是否該生成快照,而不是當前快照的大小。
leader生成快照之前,可以先執行leadership transfer。
不要有大於或等於半數的server同時生成快照,防止影響客戶端響應。(只需要多數派就可以提交日誌)

需要考慮的問題:
內存與磁盤文件之間的流接口等等等等。。。

2.基於磁盤的狀態機,只要日誌應用完成了,就可以刪除了。10G-100G
日誌應用之後即可刪除。
寫緩存可以提升效率。
copy-on-write生成一致映像發送給follower。需要額外的存儲空間。

3.增量方法,如日誌清除,LSM tree。
比快照方式複雜。
使用日誌存儲state,使用索引結構來記錄數據的位置。

log cleaning
具體算法:
(1)選擇廢棄的條目最多的segment
(2)將上一步選擇的segment中,有效的條目,全部移動到日誌的最前面
(3)最後,釋放該segment的存儲空間,使其可以用來存儲新的segment。

狀態機負責決定entry是否live。

LSM tree
樹形結構的,存儲排序後的鍵值對。
一小段log存儲最近寫的key。當日志增大到某個值時,將log中的key排序然後寫入到一個文件,叫做run。run不能被修改。多個run被週期性的合併成新的run,並且將舊的run刪除。
讀取時,先查找最近修改的log中有沒有查詢的key,然後查找每個run(使用布隆過濾,快速確定run中有沒有需要的查找的值)

4.直接將快照存儲在日誌中。效率高,但只適用於小狀態機。
基於leader的辦法,與之前三個都不相同。
直接將快照放到log中複製到follower。分成多個chunk,與客戶端請求命令交替存放。
一旦snapshot被commit,之前的日誌都可以放棄。

第六章 Client interaction

1.客戶端如何尋找到集羣,集羣成員有可能變更

廣播 ,DNS

2.客戶端請求如何路由給leader處理

如何尋找leader?
a.先隨便連一個serve,不是leader的話,就再隨機連一個,平均次數(n+1)/2
b.先隨便連一個server,如果不是leader,server拒絕連接,在回覆clint的拒絕信息中,包含leader的地址。
或者將客戶端的請求代理轉發給leader,只讀請求自己處理,寫請求轉發給leader。

3.Raft如何提供語義線性一致性

過濾掉重複的請求?
服務端對每個session保存一個序列號併發送給客戶端,客戶端在提交請求時會帶上該序列號,和命令一起發送給server。
當重複提交命令時,也會發送相同的序列號給server,server判斷該序列號是否已經處理過了。
如果已經處理過了,則不需要再次執行,直接返回處理結果。

Session不可能永遠保存着,導致兩個問題:
(1)server如何對session過期達成一致?
       設置非活動客戶端過期時間,根據LRU算法清理。
       活動客戶端發送keep-alive requests保持客戶端活動。
(2)session已經過期的活躍客戶端如何處理?
        這屬於異常情況,肯定有風險。
        如果新建一個session,可能導致之前過期的session執行過的命令重複執行。
        如何改進:
        新啓動的客戶端連接時發送RegisterClient RPC,server創建新的session,返回客戶端的identifier。如果收到一個沒有記錄的session發來的命令,則不處理並返回報錯信息。客戶端也會被清理。

4.Raft如何處理只讀請求

只讀,不修改,是否可以繞過Raft log?
只讀請求如果繞過log,可能讀到的是被踢出集羣的server中的數據,不是最新提交的數據。
如果server在不諮詢其他server的情況下就返回結果,可能返回過時的結果。因爲server可能被踢出集羣了,但是自己不知道。

如何解決這個問題?(leader端)(不知道自己是否被踢出集羣的leader)(增加了返回查詢結果延遲)
1.新term選出的leader會提交一個空的不做任何操作的entry。只要確認了這個entry提交了,則說明這個leader已經包含了所有已提交的entry,日誌是最新的。並記錄這個commit index。
2.用一個本地變量read index保存commit index的值。作爲查詢的最小版本號。
3.leader發送心跳,確認自己沒有被新的leader取代。確認之後,此時,這個read index是所有server能看到的最大commit index。
4.leader等待他的狀態機至少執行到read index,這時可以滿足線性一致性。
5.leader查詢狀態機,返回結果給客戶端。

這種解決方式,避免了將查詢命令寫入日誌,避免了寫磁盤的操作。
可以一次心跳之後,執行多個累積的查詢。

如何替leader分擔壓力,提高吞吐率?(follower端)
follower去查詢leader的read index。
上面的1-3步驟leader執行,4-5步follower執行

因爲在返回查詢結果之前需要先執行心跳,所以增加了查詢延遲。

其他方法:使用時鐘的方式來避免發送心跳信息。心跳信息會形成一個租約時間(election timeout),在租約時間內,leader認爲不會有新的leader出現。在這段時間內,leader會直接回複查詢結果,不需要先與其他server通信。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章