前言
Etcd是一個強一致性的分布式架構,即CP,所有請求必須經過leader節點,先由leader節點向follower節點發送日志同步消息,經過二階段提交最終將數據應用到狀態機。因此集群在初始化時必須有個選主的過程。
Etcd節點有以下三種角色:
Follower
集群初始化時,都是follower節點,follower節點負責以下幾個功能:
- 接收leader節點的日志同步請求。
- 接收競選節點的投票請求。
Candidate
每個follower節點都有一個超時時間,當過了這個超時時間一直沒有收到leader節點的心跳,則會成為競選節點,向其他節點發起投票請求。
Leader
所有的讀寫請求都需要經過leader節點,當競選節點競選成功后,會將角色置為leader,follower節點會從該節點同步最新的日志,以保證整個集群的一致性。
選舉流程
-
-
follower節點收到投票請求后會根據投票規則決定是否投票,並將投票結果(贊成/反對)返回給競選節點。
-
競選節點發起投票請求時,會向所有follower節點發送自己日志的任期和索引號, follower節點收到后會比較自己的日志是否比競選節點的新,先比較任期,任期大的的日志最新,如果任期一樣,則比較索引號,索引號大的日志最新。
預選舉
raft算法中,競選節點在選舉之前會先把自己的任期加1,然后發起投票請求,那如果此時出現了網絡分區,如下圖所示:
當Follower_2在達到electionTimeout后還沒收到leader的心跳,會觸發選舉,並轉為Candidate。每次發起選舉時,會把Term加1。由於網絡隔離,它既不會被選成Leader,也不會收到Leader的消息,而是會一直不斷地發起選舉。Term會不斷增大,這會產生什么問題呢?
在網絡恢復之后,因為Follower_2還是處於競選中,它這會把它的Term傳播到集群的其他節點,其他節點認為自己的日志比它舊,就肯定會選它為leader,,但事實上Follower_2節點的日志可能會落后其他節點很多了,顯然是不應該成為leader節點的。那如何避免這種情況發生呢?
raft算法對競選機制進行了改良,就是所謂的預選舉。Candidate首先要確認自己要能贏得集群中大多數節點的投票,這樣才會把自己的term增加,然后發起正式的投票,如果預選舉不通過,則該節點的term不會增加。關鍵邏輯如下:

// becomePreCandidate()方法 func (r *raft) becomePreCandidate() { if r.state == StateLeader { panic("invalid transition [leader -> pre-candidate]") } r.step = stepCandidate r.prs.ResetVotes() r.tick = r.tickElection r.lead = None r.state = StatePreCandidate r.logger.Infof("%x became pre-candidate at term %d", r.id, r.Term) } //becomeCandidate()方法 func (r *raft) becomeCandidate() { // TODO(xiangli) remove the panic when the raft implementation is stable if r.state == StateLeader { panic("invalid transition [leader -> candidate]") } r.step = stepCandidate r.reset(r.Term + 1) //一旦變為競選角色,term立馬加1 r.tick = r.tickElection r.Vote = r.id r.state = StateCandidate r.logger.Infof("%x became candidate at term %d", r.id, r.Term) }
可以看到預選舉時Term並不會增加,而是等進入正式選舉時Term才會增加。