在 ETCD 源码学习过程,不会讲解太多的源码知识,只讲解相关的实现机制,需要关注源码细节的朋友可以自行根据文章中的提示,找到相关源码进行学习
选举触发方式
1.Follower 节点选举计数器超时,触发 MsgHup 消息, Follower 节点接收此消息时,会发起一轮选举。
2.当 Leader 节点选举计数器超时,触发 MsgCheckQuorum 消息,且当前节点经过检查之后,发现当前节点不能维持 Leader 状态,Leader 会切换为 Follower 等待选举超时。
3. 当 Leader 节点正常停止时,会触发 MsgTransferLeader 消息,并且当前 Leader 会根据一定规则选择一个合适的 Follower 作为下一个 Leader。Leader 节点收到 MsgTransferLeader 后会立即发送 MsgTimeoutNow 消息给下一任 Leader 节点,节点收到消息之后会立即发起一轮新选举。
选举过程
1.选举触发
// raft/raft.go
func (r *raft) Step(m pb.Message) error {
...
switch m.Type {
case pb.MsgHup:
...
if r.state != StateLeader {
if r.preVote { //开启了preVote模式
r.campaign(campaignPreElection) //选举
} else {
r.campaign(campaignElection) //选举
}
} else {
r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
}
....
}
...
return nil
}
2.选举处理函数
// raft/raft.go 选举函数
func (r *raft) campaign(t CampaignType) {
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection { //切换为PreCandidate
r.becomePreCandidate()
voteMsg = pb.MsgPreVote
term = r.Term + 1
} else {
r.becomeCandidate() //切换为Candidate
voteMsg = pb.MsgVote
term = r.Term
}
//判断当前节点把表投给自己之后,是否得到半数以上的投票
if _, _, res := r.poll(r.id, voteRespMsgType(voteMsg), true); res == quorum.VoteWon {
if t == campaignPreElection {
r.campaign(campaignElection)
} else {
r.becomeLeader()
}
return
}
...
for _, id := range ids { //发送投票消息到各个节点
...
r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
}
}
3.Candidate消息处理
func stepCandidate(r *raft, m pb.Message) error {
var myVoteRespType pb.MessageType
if r.state == StatePreCandidate {
myVoteRespType = pb.MsgPreVoteResp
} else {
myVoteRespType = pb.MsgVoteResp
}
switch m.Type {
...
case myVoteRespType:
//统计票数
gr, rj, res := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x has received %d %s votes and %d vote rejections", r.id, gr, m.Type, rj)
switch res {
case quorum.VoteWon: //超过半数
if r.state == StatePreCandidate { //preCandidate 转变为 Candidate,进行正式选举
r.campaign(campaignElection)
} else {
r.becomeLeader() //切换Leader状态
r.bcastAppend() //向集群广播MsgApp 消息 主要是将msgs.中的消息发送出去,同时通知其他节点当前Leader是谁
}
case quorum.VoteLost:
r.becomeFollower(r.Term, None)
}
...
}
return nil
}
4.选举过程包括两个部分,预选举和正式选举。
(1) 预选举
如果开启了预选举模式(preVote),那么会首先进行一轮预选举,该过程的的主要目的是确定是否有足够多的节点能参与选举(n/2+1)。
首先当前节点会切换为 预选举状态 (preCandidate),然后当前节点选票投给自己并判断此时是否有足够多的节点能参与选举,如果可以,开始正式选举。
如果还没有足够多的节点能参与选举,发送 MsgPreVote 消息给所有节点。并且等待其他节点的响应 (stepCandidate 处理响应消息)。
当接收到其他节点的响应消息(MsgPreVoteResp)后, 统计节点票数,如果有足够多的节点能参与选举则发起正式选举,否则切换回 Follower 状态。
(2) 正式选举, 与预选举的过程相似区别在于:
发送的选举和响应消息类型为 MsgVote 和 MsgVoteResp。
当收到足够多节点投票之后,要么切换回 Follower, 要么切换回 Leader。
选举PK
// raft/raft.go
func (r *raft) Step(m pb.Message) error {
...
switch m.Type {
...
case pb.MsgVote, pb.MsgPreVote: //选举消息
/*
1.是否已经投过票
2.MysPreVote term是否比较大
3.当前票已经投给了对方
4.MsgPreVote 消息发送者的raftlog是否包含当前节点的所有entry, isUpToDate
*/
canVote := r.Vote == m.From ||
(r.Vote == None && r.lead == None) ||
(m.Type == pb.MsgPreVote && m.Term > r.Term)
if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) { //判断 消息的term 和 index,是否符合选举
//返回投票响应
r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
} else { //回复拒绝
r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
}
...
}
return nil
}
(1) 当前节点的 Term>候选节点的 Term,拒绝。
(2) 当前节点的 Term<候选节点的 Term,同意
(3) 当前节点的 Term=候选节点的 Term。则对比 index
如果当前节点 index > 候选节点 index, 拒绝,
否则,同意
涉及消息类型
MsgHup |
MsgCheckQuorum |
MsgTransferLeader |
MsgTimeoutNow |
MsgPreVote |
MsgPreVoteResp |
MsgVote |
MsgVoteResp |
PS:欢迎纠正