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 // ...
}