原始碼解讀etcd heartbeat,election timeout之間的拉鋸

部落格猿馬甲哥發表於2022-05-24

轉一個我在知乎上回答的有關raft election timeout/ heartbeat interval 的回答吧。


答:準確來講: election是timeout,而heartbeat 是interval, 這樣就很容易理解了。

heartbeat interval 是leader 安撫folower的時間,這個時間間隔是體現在leader上,是leader傳送心跳的週期 (我xxxx ms 來一次)。

election timeout 是follower能容忍多久沒收到心跳開始騷動的時間 (我等你xxxx ms,沒來我就起義)。

為壓制follower隨時起義的騷動,heartbeat timeout 一般小於 election timeout。

樓主說兩個配置超時,都會成為候選者,實際上,heartbeat interval/election timeout 是一個此消彼長的拉鋸。

  1. 想象一個剛初始化的叢集,大家都是follower,沒有heartbeat壓制, 各follower節點的election timeout之後開始騷動。

  2. 在一次選舉週期沒有選出leader,很可能是選票瓜分了, 需要發起新的選舉; 為緩解選票瓜分的情況, 每個節點的election timeout騷動時間是隨機的。

  3. 發生網路分割槽的時候, 少數派分割槽的follower收不到leader 的安撫,是不是又要起義,這個時候election timeout也起作用了。

我們結合etcd的預設配置和原始碼理解:

目前etcd預設heartbeat = 100ms, election = 1000ms

https://github.com/etcd-io/etcd/blob/5fd69102ce785136aeb3168c56adce7957b99e2d/raft/raft.go#L1718

raft 為節點定義了以下狀態:

const (
    StateFollower StateType = iota
    StateCandidate
    StateLeader
    StatePreCandidate
    numStates
)

becomeLeader 註冊了定期傳送心跳的動作 r.tick = r.tickHeartbeat ;

becomeFollower becomeCandidate becomePreCandidate 都註冊了(沒收到安撫而)起義的動作 r.tick = r.tickElection;

我們以follower節點為例:

func (r *raft) becomeFollower(term uint64, lead uint64) {
	r.step = stepFollower
	r.reset(term)
	r.tick = r.tickElection
	r.lead = lead
	r.state = StateFollower
	r.logger.Infof("%x became follower at term %d", r.id, r.Term)
}
  • r.reset(term)==> r.resetRandomizedElectionTimeout() 會接受傳播過來的term,並計算隨機選舉超時時間。
func (r *raft) resetRandomizedElectionTimeout() {
	r.randomizedElectionTimeout = r.electionTimeout + globalRand.Intn(r.electionTimeout)
}

從上面原始碼看出,etcd預設配置產生的節點隨機超時時間是 [1000,2000]ms。

  • r.tickElection 會判斷:如果當前經歷的時間electionElapsed大於隨機超時時間,就開始起義,並重置electionElapsed時間。
func (r *raft) tickElection() {
	r.electionElapsed++

	if r.promotable() && r.pastElectionTimeout() {
		r.electionElapsed = 0
		if err := r.Step(pb.Message{From: r.id, Type: pb.MsgHup}); err != nil {
			r.logger.Debugf("error occurred during election: %v", err)
		}
	}
}

func (r *raft) pastElectionTimeout() bool {
	return r.electionElapsed >= r.randomizedElectionTimeout
}

becomePreCandidate 沒有r.reset(term)動作,這是一個預投票狀態,也稱prevote,這也是etcd的常見面試題。

prevote 是論文作者為解決“分割槽少數派重新加入叢集,因為高term導致叢集瞬間不穩定”的提出的方案,etcd 預設加入prevote機制, 在成為真正意義的候選者之前不自增term,先預投票,因為其他節點一直收到心跳,並不會起義,故該節點預投票拿不到多數投票,等到該節點收到leader心跳,自行降為follower,term和Leader一致,   現在這一機制已經插入到每次follower-->Candidate之間。

	switch m.Type {
	case pb.MsgHup:
		if r.preVote {
			r.hup(campaignPreElection)
		} else {
			r.hup(campaignElection)
		}

Prevote是一個典型的2PC協議,第一階段先徵求其他節點是否同意選舉,如果同意選舉則發起真正的選舉操作,否則降為Follower角色。這樣就避免了網路分割槽節點重新加入叢集,觸發不必要的選舉操作。

相關文章