轉一個我在知乎上回答的有關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 是一個此消彼長的拉鋸。
-
想象一個剛初始化的叢集,大家都是follower,沒有heartbeat壓制, 各follower節點的election timeout之後開始騷動。
-
在一次選舉週期沒有選出leader,很可能是選票瓜分了, 需要發起新的選舉; 為緩解選票瓜分的情況, 每個節點的election timeout騷動時間是隨機的。
-
發生網路分割槽的時候, 少數派分割槽的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角色。這樣就避免了網路分割槽節點重新加入叢集,觸發不必要的選舉操作。