前言
數據一致性是分佈式服務的一大難題。假設我們能無成本的保證數據一致性,在分佈式環境中就可以使用無限擴容來分發流量。那麼數據一致性,何爲數據 ?往往日誌log是一應用的可靠性的保障,例如 mysql的binlog,redis 的aof日誌。通過分析日誌從機子可以達到和本機子相同的狀態。因此數據一致性的問題,化簡爲日誌一致性問題。Raft則是一個用於管理日誌一致性的協議。
Raft簡介
Raft又稱 木筏協議,一種共識算法,旨在替代PaxOS,相比容易理解許多。斯坦福大學的兩個博士Diego Ongaro和John Ousterhout兩個人以易懂爲目標設計的一致性算法,2013以論文方式發佈。由於易懂,不從論文發佈到不到兩年的時間,即出現大量的Raft開源實現。爲簡化PaxOS的晦澀,它採用分而治之的辦法,將算法分爲三個子問題:選舉(Leader Election)、日誌複製(Log Replication)、安全性(Safety)和集羣成員動態變更(Cluster Membership Changes)。
Raft選舉
Raft 中node 的 3 種角色/狀態
- Follower:完全被動,不能發送任何請求,只接受並響應來自 Leader 和 Candidate 的 Message,每個節點啓動後的初始狀態一定是 Follower;
- Leader:處理所有來自客戶端的請求,以及複製 Log 到所有 Followers;
- Candidate:用來競選一個新 Leader (Candidate 由 Follower 觸發超時而來)。
Follower --> Candidate
- Follower沒收到心跳的時間 > Heartbeat Timeout
Candidate --> Candidate (選舉超時)
- 處於 Candidate 狀態的時間 > Election Timeout
Candidate(選舉)
- 先建個term並投票給自己
- Candidate 向其它節點 node發投票請求
- Candidate.term小於 node. term,失敗並降級爲Follower
- Candidate.term大於 node. term,成功如果node也是Candidate則降級,並更新term
- Candidate.term等於node. term,如果node也是Candidate失敗,否則4
- 比較上一條日誌的term,如果node大則失敗,小則成功 ,等於到5
- 比較上一條日誌的index,如果node大則失敗,否則成功
Candidate --> Follower
- Candidate選舉時,發現其它節點term > 自己term.
Candidate --> Leader
- Candidate選舉結果 > 總節點數/2
獲取Leader結點特性
1.有最大的Term;
2. 如果Term相同,則有最大的Index;
3. 如果Term相同,Index也相同,就看誰最先發起;
Leader(維持)
- 向所有結點發送心跳和日誌
Leader --> Follower
- 發現其它節點term > 自己term.
Raft日誌複製
日誌複製是分佈式一致性算法的核心,所謂的一致性,就是集羣多節點的數據一致性。
日誌的數據結構
- 創建日誌時的任期號term(用來檢查節點日誌是否出現不一致的情況)
- 狀態機需要執行的指令(真正的內容)
- 索引index:整數索引表示日誌條目在日誌中位置
Raft 把每條日誌都附加了任期號和下標來保證日誌的唯一性。如果 2 個日誌的相同的索引位置的日誌條目的任期號相同,那麼 Raft 就認爲這個日誌從頭到這個索引之間全部相同。
日誌的複製過程
- 客戶端的向服務端發送請求(指令)。
- follower接收到請求轉發給leader
- leader 把這個指令作爲一條新的日誌條目添加到日誌中,然後並行發起 RPC 給其他的服務器,讓他們複製這條信息。
- 假如這條日誌被安全的複製 > 總結點/2,leader就提交這條日誌並返回給客戶端。
- 關於日誌複製異常的Follower,Raft異步會通過強制 follower 直接複製 leader 的日誌解決
日誌衝突及解決
下圖,所有的 follower 都和 leader 的日誌衝突了,leader 的最後一條日誌的任期是 6, 下標是 10 ,而其他 follower 的日誌都與 leader 不匹配。
一致性操作:
- a follower 不需要刪除任何條目,
- b 也不需要刪除
- c follower 需要刪除最後一個條目
- d follower 需要刪除最後 2 個任期爲 7 的條目,
- e 需要刪除最後 2 個任期爲 4 的條目,
- f 則比較厲害,需要刪除 下標爲 3 之後的所有條目。
具體思路:
- Leader 爲每一個 follower 維護一個下標,稱之爲 nextIndex,表示下一個需要發送給 follower 的日誌條目的索引。
- 當一個新 leader 剛獲得權力的時候,他將自己的最後一條日誌的 index + 1,也就是上面提的 nextIndex 值,如果一個 follower 的日誌和 leader 不一致,那麼在下一次 RPC 附加日誌請求中,一致性檢查就會失敗(不會插入數據)。
- 如果檢查失敗,leader 就會把 nextIndex 遞減進行重試,直到遇到匹配到正確的日誌。當匹配成功之後,follower 就會把衝突的日誌全部刪除,此時,follower 和 leader 的日誌就達成一致。
安全性
Raft在非拜占庭錯誤情況下,包括網絡延遲、分區、丟包、冗餘和亂序等錯誤都可以保證正確,不會返回錯誤結果,這就是安全性保證。
- Leader選舉之後,如果Follower與Leader日誌有衝突該如何處理?
答:通過強制 follower直接複製 leader 的日誌解決,詳情見上文。
- 如果在一個Follower宕機的時候Leader提交了若干的日誌條目,然後這個Follower上線後可能會被選舉爲Leader並且覆蓋這些日誌條目,如此就會產生不一致?
答:candidate發送出去的投票請求消息必須帶上其最後一條日誌條目的Index與Term;接收者需要判斷該Index與Term至少與本地日誌的最後一條日誌條目一樣新,否則不給投票。因爲前一個Leader提交日誌條目的條件是日誌複製給集羣中的過半成員,選舉爲Leader的條件也是需要過半成員的投票。
- a) S1是Leader,並且部分地複製了index-2;
- b) S1宕機,S5得到S3、S4、S5的投票當選爲新的Leader(S2不會選擇S5,因爲S2的日誌較S5新),並且在index-2寫入到一個新的條目,此時是term=3(注:之所以是term=3,是因爲在term-2的選舉中,S3、S4、S5至少有一個參與投票,也就是至少有一個知道term-2,雖然他們沒有term-2的日誌);
- c) S5宕機,S1恢復並被選舉爲Leader,並且開始繼續複製日誌(也就是將來自term-2的index-2複製給了S3),此時term-2,index-2已經複製給了多數的服務器,但是還沒有提交;
- d) S1再次宕機,並且S5恢復又被選舉爲Leader(通過S2、S3、S4投票,因爲S2、S3、S4的term=4<5,且日誌條目(爲term=2,index=2)並沒有S5的日誌條目新,所以可以選舉成功),然後覆蓋Follower中的index-2爲來自term-3的index-2;(注:此時出現了,term-2中的index-2已經複製到三臺服務器,還是被覆蓋掉);
- e) 然而,如果S1在宕機之前已經將其當前任期(term-4)的條目都複製出去,然後該條目被提交(那麼S5將不能贏得選舉,因爲S1、S2、S3的日誌term=4比S5都新)。此時所有在前的條目都會被很好地提交。
-
如果上述情況©中,term=2,index=2的日誌條目被複制到大多數後,如果此時當選的S1提交了該日誌條目,則後續產生的term=3,index=2會覆蓋它,此時就可能會在同一個index位置先後提交一個不同的日誌,這就違反了狀態機安全性,產生不一致。
答:爲了消除上述場景就規定Leader可以複製前面任期的日誌,但是不會主動提交前面任期的日誌。而是通過提交當前任期的日誌,而間接地提交前面任期的日誌。
-
網絡分區會不會產生不一致的狀況
答:機子是2n+1的個數。如產生分區,機子小於等於n的分區,將不會產生日誌數據以及term不再增加。如果分區合併,n分區的機子板本落後也不可能成爲Leader
線性一致性
所謂線性一致性,就是在 t1 的時間我們寫入了一個值,那麼在 t1 之後,我們的讀一定能讀到這個值,不可能讀到 t1 之前的值。
Mysql 主從異步複製,你t1時間向主庫插入數據x,t2時間去從庫讀數據x,是有可能讀不到的。而線性一致性系統則是必須讀到,即CAP理論中的C.
爲什麼Raft中沒有線性一致性
Raft中有一個狀態機,專門用來執行日誌,相當於Mysql的解析執行器。狀態機執行是要花時間的,Follower沒這麼快。
怎麼實現線性一致性
- Raft log
- 任何的讀請求走一次Leader
- Leader 將讀請求也記到log裏面(像寫請求一樣)
- 等這個 log 提交之後,在 apply 的時候從狀態機裏面讀取值
點評:非常方便的實現線性 read,但每次read 都需要走 Raft流程,全部依賴Leader, Follower在浪費。
- ReadIndex Read
- 讀請進來,將Leader commit index 記錄到一個 local 變量 ReadIndex
- 向其他節點發起一次 heartbeat,如果大多數節點返回了對應的 heartbeat response,那麼 leader 就能夠確定現在自己仍然是 leader。(防止網絡分區了,自己不知道)
- 將Leader commit index (Follower通過網絡獲取)記錄到一個 local 變量 ReadIndex
- 等待自己的狀態機執行,直到 apply index 超過了 ReadIndex
點評:Follower可通過網絡獲取ReadIndex。Follower幫助分擔了Learder的部份壓力,向其他節點發起一次 heartbeat仍是Learder的負擔
Lease Read
- 讀請進來,將Leader commit index 記錄到一個 local 變量 ReadIndex
- heartbeat + election timeout內默認自己是leader。超過時間 向其他節點發起一次 heartbeat並刷新heartbeat
- 將Leader commit index (Follower通過網絡獲取)記錄到一個 local 變量 ReadIndex
- 等待自己的狀態機執行,直到 apply index 超過了 ReadIndex
點評:優化了ReadIndex Read ,進一步節省了heartbeat對Leader的負擔
Raft存儲及日誌壓縮
在實際的系統中,不能讓日誌無限增長,否則系統重啓時需要花很長的時間進行回放,從而影響可用性。Raft採用對整個系統進行snapshot來解決,snapshot之前的日誌都可以丟棄。
集羣成員動態變更
集羣配額從 3 臺機器變成了 5 臺,可能存在這樣的一個時間點,兩個不同的領導者在同一個任期裏都可以被選舉成功(雙主問題),一個是通過舊的配置,一個通過新的配置。
解決方法
Raft解決方法是每次成員變更只允許增加或刪除一個成員(如果要變更多個成員,連續變更多次)。
Paxos算法
Client Proposer Acceptor Learner
| | | | | | |
X-------->| | | | | | Request
| X--------->|->|->| | | Prepare(1)
| |<---------X--X--X | | Promise(1,{Va,Vb,Vc})
| X--------->|->|->| | | Accept!(1,V)
| |<---------X--X--X------>|->| Accepted(1,V)
|<---------------------------------X--X Response
| | | | | | |
我的RaftDemo
/**
*
* 1.electionTime最終時間短的會選爲主
* 2.日誌存儲和讀取
*/
public class RaftTest {
public static void main(String[] args) {
List<String> list = Lists.newArrayList("localhost:8771", "localhost:8772", "localhost:8773", "localhost:8774", "localhost:8775");
List<RpcServer> ss = Lists.newArrayList();
for (int i = 0; i < list.size(); i++) {
String url = list.get(i);
RpcServer server = new RpcServer(url);
ss.add(server);
if (i == 0) {
server.setElectionTime(500l);
}
server.start();
}
ThreadUtil.sleep(3000);
//結果大概率是8771
for (int i = 0; i < ss.size(); i++) {
if(i == ss.size() -1) {
i = 0;
ThreadUtil.sleep(2000);
}
RpcServer server = ss.get(i);
if(server.isLeader()){
System.out.println("leader node is " + server.getUrl());
break;
}
}
//讓node結點接收一波心跳新
ThreadUtil.sleep(2000);
RpcClient rpcClient = new RpcClient();
ClientKVReq putReq = ClientKVReq.newBuilder()
.type(0)
.key("yoyo")
.value("eee")
.build();
ClientKVAck putRes = rpcClient.sendMassage(putReq);
System.out.println(putRes.getResult());
//讓node結點接收一波心跳新
//ThreadUtil.sleep(20000);
ClientKVReq getReq = ClientKVReq.newBuilder()
.type(1)
.key("yoyo")
.build();
ClientKVAck getRes = rpcClient.sendMassage(getReq);
System.out.println(getRes.getResult());
}
}
運行結果:
[localhost:8771] electionTime:500
[localhost:8774] electionTime:6785
[localhost:8772] electionTime:4948
[localhost:8775] electionTime:3759
[localhost:8773] electionTime:4643
[localhost:8772] stepDown become FOLLOWER
[localhost:8774] stepDown become FOLLOWER
[localhost:8774] agree [localhost:8771]
[localhost:8775] stepDown become FOLLOWER
[localhost:8775] agree [localhost:8771]
[localhost:8772] agree [localhost:8771]
[localhost:8773] stepDown become FOLLOWER
[localhost:8773] agree [localhost:8771]
[localhost:8771] become leader
leader node is localhost:8771
LEADER[localhost:8771] write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772] write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773] write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774] write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775] write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
LEADER[localhost:8771] apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
ok
FOLLOWER[localhost:8772] apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774] apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773] apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775] apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772] ReadIndex to [0] with value:eee
eee
資源地址:https://download.csdn.net/download/y3over/12577334
Raft進階-SOFAJRaft
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,是從百度的 braft 移植而來,做了一些優化和改進。
《SOFAStack 開源 SOFAJRaft:生產級 Java Raft 算法庫》
《SOFAJRaft 選舉機制剖析 | SOFAJRaft 實現原理》
《SOFAJRaft 線性一致讀實現剖析 | SOFAJRaft 實現原理》
《SOFAJRaft 實現原理 - 生產級 Raft 算法庫存儲模塊剖析》
《SOFAJRaft 實現原理 - SOFAJRaft-RheaKV 是如何使用 Raft 的》
《SOFAJRaft—初次使用》
主要參考
《Raft 日誌複製 Log replication》
《編寫你的第一個 Java 版 Raft 分佈式 KV 存儲》
《Raft協議安全性保證》
《TiKV 功能介紹 - Lease Read 》
《RAFT算法詳解》