MsgBeat和MsgHeartbeat消息
Leader推動心跳計時器(heartbeatElapsed),而Follower推動選舉計時器(electionElapsed),選舉計時器的流程前面已經提到,這裏主要介紹心跳計時器。當上層模塊調用Tick()時,Leader會推動心跳計時器,在tickHeartbeat函數中有詳細介紹。有兩種情況會是Leader向集羣中其他的節點發送心跳消息分別是心跳計時器超時和在ReadOnly模式下Leader接收到客戶端請求的只讀消息,這裏我們主要介紹心跳計時器超時的情況。
MsgBeat和MsgHeartbeat消息的主要區別是MsgBeat是本地消息,而發給集羣中其他節點的心跳消息是使用MsgHeartbeat消息
Leader調用tickHeartbeat推動心跳計時器
當心跳計時器超時時會向其他節點發送心跳,如果raft的配置checkQuorum爲true,則也會發送MsgCheckQuorum消息。tickHeartbeat的主要流程:
1.遞增心跳計時器和選舉計時器
2.如果選舉計時器大於等於選舉超時時間
重置選舉計時器,Leader節點不會主動發起選舉
檢測當前節點是否與集羣中的大多數節點連通,如果超過半數以上的節點連通則發送MsgCheckQuorum確認
如果有節點正在轉移,則禁止節點轉移
3.如果選舉計時器沒有超時
如果當前節點不是Leader,則直接返回
心跳計時器超時,則發送MsgBeat消息,繼續維持向其餘節點發送心跳消息,並重置心跳計時器
func (r *raft) tickHeartbeat() {
r.heartbeatElapsed++ //遞增心跳計時器
r.electionElapsed++ //遞增選舉計時器
if r.electionElapsed >= r.electionTimeout { //當選舉計時器大於等於選舉超時時間
r.electionElapsed = 0 //重置選舉計時器,Leader節點不會主動發起選舉
if r.checkQuorum { //檢測當前節點是否與集羣中的大多數節點連通
r.Step(pb.Message{From: r.id, Type: pb.MsgCheckQuorum})
}
// If current leader cannot transfer leadership in electionTimeout, it becomes leader again.
if r.state == StateLeader && r.leadTransferee != None { //禁止節點轉移
r.abortLeaderTransfer() //清空raft.leadTransferee字段,放棄轉移
}
}
if r.state != StateLeader { //檢測當前節點是否是Leader,不是Leader直接返回
return
}
if r.heartbeatElapsed >= r.heartbeatTimeout { //心跳計時器超時,則發生MsgBeat消息,繼續維持向其餘節點發送心跳
r.heartbeatElapsed = 0 //重置心跳計時器
r.Step(pb.Message{From: r.id, Type: pb.MsgBeat})
}
}
Leader發送MsgBeat類型的消息流程
Leader發送MsgBeat類型的消息主要是通過bcastHeartbeat方法向集羣中的其他節點廣播發送,如果readOnly中有隻讀消息,會在消息的上下文中攜帶readOnly中最後一條消息的消息ID,Follower節點收到後會在上下文中攜帶該消息ID響應。
//廣播發送心跳
func (r *raft) bcastHeartbeat() {
//獲取readOnly最新一次的上下文
lastCtx := r.readOnly.lastPendingRequestCtx()
if len(lastCtx) == 0 {
r.bcastHeartbeatWithCtx(nil)
} else {
r.bcastHeartbeatWithCtx([]byte(lastCtx))
}
}
//遍歷向集羣中的其他節點發送心跳消息
func (r *raft) bcastHeartbeatWithCtx(ctx []byte) {
r.forEachProgress(func(id uint64, _ *Progress) {
if id == r.id { //過濾當前節點
return
}
r.sendHeartbeat(id, ctx) //向指定的節點發送MsgBeat消息
})
}
//向指定節點發送消息
func (r *raft) sendHeartbeat(to uint64, ctx []byte) {
// Attach the commit as min(to.matched, r.committed).
// When the leader sends out heartbeat message,
// the receiver(follower) might not be matched with the leader
// or it might not have all the committed entries.
// The leader MUST NOT forward the follower's commit to
// an unmatched index.
//由於集羣中的節點不一定都收到全部已提交的Entry記錄,所以心跳消息攜帶commit提交位置
消息類型爲MsgHeartbeat
commit := min(r.getProgress(to).Match, r.raftLog.committed)
m := pb.Message{
To: to,
Type: pb.MsgHeartbeat,
Commit: commit,
Context: ctx,
}
r.send(m)
}
Follower節點接收到MsgHeartbeat消息的處理流程
Follower收到心跳計時器會重置選舉計時器,並且指定Leader,並且調用handleHeartbeat方法處理消息。
func stepFollower(r *raft,m pb.Message) error {
switch m.Type {
//其他類型消息的處理略
......
case pb.MsgHeartbeat: //如果是心跳消息
r.electionElapsed = 0 //重置選舉計時器
r.lead = m.From //指定leader
r.handleHeartbeat(m) //處理心跳消息
}
}
處理心跳消息比較簡單,節點嘗試更新raftLog中已提交的位置,並且發送心跳回復消息MsgHeartbeatResp
func (r *raft) handleHeartbeat(m pb.Message) {
r.raftLog.commitTo(m.Commit) //更新raftLog中已提交的位置
r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context}) //心跳回復
}
MsgHeartbeatResp消息
1.設置該節點的RecentActive字段爲true,表示該節點存活
2.將Paused設爲false,表示可以繼續向該Follower節點發送消息
3.如果消息隊列滿了,則釋放inflights中第一個消息,這樣就可以開始後續消息的發送
4.當Leader節點收到Follower節點的MsgHeartbeat消息之後,會比較對應的Match值與Leader節點的raftLog,從而判斷Follower節點是否已擁有了全部的Entry記錄
如果該Follower缺少Entry,則Leader會發送缺少的Entry記錄
5.如果該節點的只讀模式不是ReadOnlySafe,則返回
6.統計目前爲止響應上述攜帶消息ID的MsgHeartbeat消息的節點個數,如果沒有超過半數則退出
7.如果響應心跳的節點數超過半數,則會清空readOnly中指定消息ID及其之前的所有相關記錄
實現如下
根據MsgReadIndex消息的From字段,判斷該MsgReadIndex消息是否爲Follower節點轉發到Leader節點的消息
如果是客戶端直接發送到Leader節點的消息,
則將MsgReadIndex消息對應的已提交位置以及其消息ID封裝成ReadState實例,添加到raft.readStates中保存。
後續會有其他goroutine讀取該數組,並對相應的MsgReadIndex消息進行響應
如果其他Follower節點轉發到Leader節點的MsgReadIndex消息,
則Leader會向Follower節點返回相應的MsgReadIndex消息,並由Follower節點響應Client。
func stepLeader(r *raft, m pb.Message) error {
//其他類型的消息略
case pb.MsgHeartbeatResp: //處理心跳回復消息
pr.RecentActive = true //設置RecentActive字段
pr.resume() //設置Paused爲false,即可以重新發送消息
// free one slot for the full inflights window to allow progress.
if pr.State == ProgressStateReplicate && pr.ins.full() { //隊列已滿
pr.ins.freeFirstOne() //釋放第一個消息,這樣就可以開始後續消息的發送
}
//判斷Follower是否已擁有全部的Entry記錄
if pr.Match < r.raftLog.lastIndex() {
r.sendAppend(m.From) //通過向指定節點發送MsgApp消息完成Entry記錄的複製
}
if r.readOnly.option != ReadOnlySafe || len(m.Context) == 0 {
return nil
}
ackCount := r.readOnly.recvAck(m)
if ackCount < r.quorum() {
return nil
}
rss := r.readOnly.advance(m)
for _, rs := range rss {
req := rs.req
if req.From == None || req.From == r.id {
/*
根據MsgReadIndex消息的From字段,判斷該MsgReadIndex消息是否爲Follower節點轉發到Leader節點的消息
如果是客戶端直接發送到Leader節點的消息,
則將MsgReadIndex消息對應的已提交位置以及其消息ID封裝成ReadState實例,添加到raft.readStates中保存。
後續會有其他goroutine讀取該數組,並對相應的MsgReadIndex消息進行響應
*/
r.readStates = append(r.readStates, ReadState{Index: rs.index, RequestCtx: req.Entries[0].Data})
} else {
//如果其他Follower節點轉發到Leader節點的MsgReadIndex消息,
// 則Leader會向Follower節點返回相應的MsgReadIndex消息,並由Follower節點響應。
r.send(pb.Message{To: req.From, Type: pb.MsgReadIndexResp, Index: rs.index, Entries: req.Entries})
}
}
}
MsgCheckQuorum消息
MsgCheckQuorum消息和MsgBeat消息的Term字段都爲0,因爲它們都屬於本地消息。當Leader 的心跳計時器超時,並且開啓了checkQuorum模式(raft的checkQuorum字段爲true)。該Leader節點就會發送MsgCheckQuorum消息檢測與集羣中其他節點是否保持半數以上的連接,如果沒有則變成Follower節點。
MsgCheckQuorum類型的消息比較簡單,這裏就不在累述。
func stepLeader(r *raft, m pb.Message) error {
switch m.Type {
//其他類型的消息省略
case pb.MsgCheckQuorum:
if !r.checkQuorumActive() { //檢測當前節點是否與集羣中大多數節點連通,如果不連通則切換成Follower
r.logger.Warningf("%x stepped down to follower since quorum is not active", r.id)
r.becomeFollower(r.Term, None)
}
return nil
}