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

轉一個我在知乎上回答的有關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

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