etcd-raft 2.3.7 raft 日誌複製 log replication 以及心跳Heartbeat

raft的log replication是在了leader選舉之後進行的,leader的選舉可以參考這篇文章etcd-raft的leader選舉


raft的Heartbeat是leader在選舉成功後鞏固自己地位和同步信息的一種方式,日誌的複製大多數情況下適合Heartbeat是同步進行的。當一個raft節點選舉成爲leader後,該節點週期性執行的tick函數指向了raft.tickHeartbeat,對於leader而言主要是通知follower自己還活着,與此同時leader在收到follower對Heartbeat的響應之後,又會向日志比較落後的follower發送追加日誌請求,進行log replication。

由於發起heartbeat的raft節點處於leader角色,因此,被週期調用的tick將指向tickHeartbeat:

unc (r *raft) tickHeartbeat() {
    r.heartbeatElapsed++
    // ... 

    // 如果心跳計時超過了心跳包的發送間隔,就進入發送心跳包流程,並重置心跳計時
    if r.heartbeatElapsed >= r.heartbeatTimeout {
        r.heartbeatElapsed = 0 
        r.Step(pb.Message{From: r.id, Type: pb.MsgBeat})
    }   
}

func (r *raft) Step(m pb.Message) error {
    // ...

    // Step最終調用raft.step變量指向的函數
    // 現階段當前節點處於leader狀態,所以step指向stepLeader 
    r.step(r, m)
    return nil 
}

stepLeader中心跳發送代碼相關執行流程如下:

func stepLeader(r *raft, m pb.Message) {
    switch m.Type {
    case pb.MsgBeat:
        // 把心跳包廣播出去
        r.bcastHeartbeat()
    return
    // ...
}

// 在bcastHeartbeat中循環的把心跳包發送出去
func (r *raft) bcastHeartbeat() {
    for id := range r.prs {
        if id == r.id {
            continue
        }
        r.sendHeartbeat(id)
        r.prs[id].resume()
    }
}
func (r *raft) sendHeartbeat(to uint64) {
    // r.raftLog.committed 已經拷貝到大多數節點上的日誌index
    // r.prs[to].Match拷貝到to這個節點的日誌最大下標
    commit := min(r.prs[to].Match, r.raftLog.committed)
    m := pb.Message{
        To:     to,
        Type:   pb.MsgHeartbeat,
        Commit: commit,
    }
    //把心跳包發送出去
    r.send(m)
}

follower在收到心跳包之後,最終處理心跳的包的流程會通過Step->step->stepFollower:

func stepFollower(r *raft, m pb.Message) {
    switch m.Type {
    case pb.MsgHeartbeat:
        r.electionElapsed = 0
        r.lead = m.From
        r.handleHeartbeat(m)
    case //...
    }
}

// 更新本地可以提交的日誌的最大下標,然後返回響應
func (r *raft) handleHeartbeat(m pb.Message) {
    r.raftLog.commitTo(m.Commit)
    r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp})
}

leader的心跳響應收到之後處理流程依舊會通過Step->step->stepLeader:

func stepLeader(r *raft, m pb.Message) {
    switch m.Type {
        // ...
    case pb.MsgHeartbeatResp:
        pr.RecentActive = true

        // free one slot for the full inflights window to allow progress.
        if pr.State == ProgressStateReplicate && pr.ins.full() {
            pr.ins.freeFirstOne()
        }
        // 檢查是否還有日誌沒有拷貝到當前follower,如果有待發送的日誌,通過sendAppend發送過去。
        if pr.Match < r.raftLog.lastIndex() {
            r.sendAppend(m.From)
        }
        case //...
    }
}

sendAppend函數的實現如下:

func (r *raft) sendAppend(to uint64) {
    pr := r.prs[to]
    if pr.isPaused() {
        return
    }
    m := pb.Message{}
    m.To = to
    // 給該follower發送的最後一條日誌的term
    term, errt := r.raftLog.term(pr.Next - 1)
    // 即將要發送給該follower的日誌條目
    ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)

    // 如果待發送給follower已經寫到snap裏面,在raftlog裏面無法找到
    if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
        if !pr.RecentActive {
            r.logger.Debugf("ignore sending snapshot to %x since it is not recently active", to)
            return
        }

        m.Type = pb.MsgSnap
        snapshot, err := r.raftLog.snapshot()
        if err != nil {
            if err == ErrSnapshotTemporarilyUnavailable {
                r.logger.Debugf("%x failed to send snapshot to %x because snapshot is temporarily unavailable", r.id, to)
                return
            }
            panic(err) // TODO(bdarnell)
        }
        if IsEmptySnap(snapshot) {
            panic("need non-empty snapshot")
        }
        m.Snapshot = snapshot
        sindex, sterm := snapshot.Metadata.Index, snapshot.Metadata.Term
        r.logger.Debugf("%x [firstindex: %d, commit: %d] sent snapshot[index: %d, term: %d] to %x [%s]",
            r.id, r.raftLog.firstIndex(), r.raftLog.committed, sindex, sterm, to, pr)
        pr.becomeSnapshot(sindex)
        r.logger.Debugf("%x paused sending replication messages to %x [%s]", r.id, to, pr)
    } else {
        m.Type = pb.MsgApp
        m.Index = pr.Next - 1
        m.LogTerm = term
        m.Entries = ents
        // leader記錄的最大的已經copy到大多數節點上的日誌下標
        m.Commit = r.raftLog.committed
        if n := len(m.Entries); n != 0 {
            switch pr.State {
            // optimistically increase the next when in ProgressStateReplicate
            case ProgressStateReplicate:
                last := m.Entries[n-1].Index
                pr.optimisticUpdate(last)
                pr.ins.add(last)
            case ProgressStateProbe:
                pr.pause()
            default:
                r.logger.Panicf("%x is sending append in unhandled state %s", r.id, pr.State)
            }
        }
    }
    r.send(m)
}

follower在收到leader發送過來的追加日誌請求時,會通過handleAppendEntries去處理消息:

func (r *raft) handleAppendEntries(m pb.Message) {
    // r.raftLog.committed實在當前節點已經提交的日誌
    // m.Index爲leader發送給該follower的上一條日誌索引
    // m.Index < r.raftLog.committed情況多出現在剛剛選出的leader向follower拷貝日誌,leader本地還未記錄向該follower拷貝了多少日誌
    // 出現這種請求leader會把自己已經提交的日誌的最大下標發送給leader,下次leader就會從r.raftLog.committed + 1開始發送日誌
    if m.Index < r.raftLog.committed {
        r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
        return
    }
    // leader拷貝日誌有可能在follower已經存在,所以要先找到相對與follower日誌比較新的日誌下標
    if mlastIndex, ok := r.raftLog.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...); ok {
        // 當leader發送過來的上次發送給該follower的最後一條日誌信息與follower本地儲存的無衝突時,
        // 返回follower本地已經寫入的最大日誌下標,下次leader就會從該下標發送日誌,並會更新leader記錄的該follower的日誌發送信息
        r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: mlastIndex})
    } else {
        // 最後leader發送過來的最後一條日誌的index,term等信息與follower本地記錄的不一樣
        r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: m.Index, Reject: true, RejectHint: r.raftLog.lastIndex()})
    }
}

func (l *raftLog) maybeAppend(index, logTerm, committed uint64, ents ...pb.Entry) (lastnewi uint64, ok bool) {
    lastnewi = index + uint64(len(ents))
    if l.matchTerm(index, logTerm) {
        // 找到leader發送給follower的第一條新日誌的位置
        ci := l.findConflict(ents)
        switch {
        // 說明leader拷貝過來的日誌都在follower的raftlog裏面有記錄
        case ci == 0:
        // 日誌衝突的下標小於follower本地已經提交的日誌,出現bug了
        case ci <= l.committed:
            l.logger.Panicf("entry %d conflict with committed entry [committed(%d)]", ci, l.committed)
        default:
           // 把leader拷貝過來的新日誌寫入follower本地
            offset := index + 1 
            l.append(ents[ci-offset:]...)
        }
        l.commitTo(min(committed, lastnewi))
        // 返回follower本地最新的日誌記錄index
        return lastnewi, true
    }   
    return 0, false
}

leader收到的follower對日誌追加請求的響應主要有兩種:follower拒絕追加日誌,follower追加0-n條日誌,相關響應的處理代碼如下:

func stepLeader(r *raft, m pb.Message) {
    // ...
    switch m.Type {
    case pb.MsgAppResp:
        pr.RecentActive = true

        if m.Reject {
            // 拒絕追加日誌
            // follower拒絕追加日誌時都會返回一個RejectHint,RejectHint表示當前follower最後一條日誌的index
            // maybeDecrTo函數在follower爲ProgressStateReplicate狀態是會把pr[to].Next設置爲pr[to].Match + 1,否者會設置爲被設置爲m.Index、m.RejectHint中較小的一個,然後繼續嘗試發送。
            if pr.maybeDecrTo(m.Index, m.RejectHint) {
                if pr.State == ProgressStateReplicate {
                    pr.becomeProbe()
                }
                r.sendAppend(m.From)
            }
        } else {
            oldPaused := pr.isPaused()
            // 如果又在follower上追加日誌,更新以下兩個下標:
            // (1)follower已經和leader一致的最大日誌下標:pr[to].Match = m.Index
            // (2)下一條需要拷貝給該follower的日誌下標:pr[to].Next = m.Index + 1
            if pr.maybeUpdate(m.Index) {
                switch {
                case pr.State == ProgressStateProbe:
                    pr.becomeReplicate()
                case pr.State == ProgressStateSnapshot && pr.maybeSnapshotAbort():
                    pr.becomeProbe()
                case pr.State == ProgressStateReplicate:
                    pr.ins.freeTo(m.Index)
                }
                // 如果該follower有追加成功的日誌,可能會出現新的日誌條目拷貝到大多數raft節點上,
                // 因此leader更新本地raftlog.commitid,然後通過bcastAppend把新更新的commit(如果follower日誌落後於leader,可能攜帶日誌)廣播給follower,
                // follower更新本地已經提交到大多數機器上的日誌下標,本地小於commit未apply的日誌就可以apply了
                if r.maybeCommit() {
                    r.bcastAppend()
                } else if oldPaused {
                    // update() reset the wait state on this node. If we had delayed sending
                    // an update before, send it now.
                    r.sendAppend(m.From)
                }
            }
        }
    case // ...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章