概述
Raft是一种常见的分布式共识算法,分布式场景下多个参与方之间需要对某一件事情达成共识,最常见的应用就是数据达成一致性。
Raft也是etcd的共识算法,本文试图在仿制etcd的项目tinykv中讲解Raft算法。
三种角色/状态(state)
- follower(跟随者):所有节点都以 follower 的状态开始。如果没收到 leader 消息则会变成 candidate 状态;
- candidate(候选人):会向其他节点“拉选票”,如果得到大部分的票则成为 leader ,这个过程就叫做 Leader选举( Leader Election);
- leader(领导者):所有对系统的修改都会先经过 leader(这跟主从架构不一样,可能是主先收到数据,也可能是从先收到数据)。
两种timeout
分别是选举超时与心跳超时,leader需要在每一个heartbeat timeout之后发送一个心跳包,follower或者candidate在收到心跳包之后停止选举,(candidate)变为follower,并重置选举时间。如果follower在一个election timeout之内,没有接到心跳包,则转变为candidate,并开始选举,意图取代leader。
随机化election timeout
当两个candidate同时进行选举时,只有得票数超过所有节点总数一半的candidate能够成为leader(显然,只有candidate能够成为leader),如果在偶数个节点中,两个candidate获得了同样的票数,则两者需要重新进行选举,为了避免两个candidate多次获得同样的票数,致使选举不断发生下去,Raft算法需要随机化election timeout,这样两个candidate(几乎)不会在下次选举中获得与上次相同的票数。
MsgAppend与Heartbeat
两者都是来自leader的消息,都具有阻止follower选举与将candidate变为follower的作用。似乎在MIT6.824中,直接将空的MsgAppend请求视为Heartbeat,不在两者之间区分(没有研究过MIT6.824,不确定此处表述是否准确)。在etcd或者tinykv中,则在两者间做了区别,leader在heartbeat timeout之后发送heartbeat,而MsgAppend仅在leader新上任之后发送no-op entry或者收到MsgPropose之后发送,用于向follower追加日志。
递增的leader任期(Term)
在Raft算法中,leader的任期term是严格递增的,对于任意Raft节点,应当忽略任期低于自身term的请求。同时,在接到任期高于自身term的请求时,应当转变为follower,保证Raft算法的强一致性。
节点在提高自己任期的时间,应当是follower在become candidate时,而不是在become leader时;或是在接到term高于自身请求时,将自身的term提高到与Message.Term相同。
Step和tick
tick用于更新heartbeat和election的时间。
step则用于接受上层状态机的请求,值得注意的是,部分请求来自上层,其不关心底层raft节点的实现,亦对当前Raft节点的term一无所知,会默认发送term = 0的请求,需要特殊处理。例如tinykv中的MsgPropose,MsgHug,MsgBeat。
MsgRequestVote和MsgRequestVoteResponse
MsgRequestVote用于candidate向其它节点索要投票。
func (r *Raft) handleRequstVote(m pb.Message) {
if r.Term < m.Term {
r.becomeFollower(m.Term, m.GetFrom())
} else if r.State != StateFollower {
r.msgs = append(r.msgs, pb.Message{MsgType: pb.MessageType_MsgRequestVoteResponse, To: m.GetFrom(), From: r.id, Term: m.Term, Reject: true})
return
}
r.electionElapsed = 0
r.electionTimeout = int((1 + rand.Float64()) * float64(r.eletimeout))
if r.Vote == None || r.Vote == m.GetFrom() {
r.lg.Debugf("Term:%d,%d vote for %d", m.Term, m.GetTo(), m.GetFrom())
r.Vote = m.GetFrom()
r.msgs = append(r.msgs, pb.Message{MsgType: pb.MessageType_MsgRequestVoteResponse, To: m.GetFrom(), From: r.id, Term: m.Term, Reject: false})
} else {
r.msgs = append(r.msgs, pb.Message{MsgType: pb.MessageType_MsgRequestVoteResponse, To: m.GetFrom(), From: r.id, Term: m.Term, Reject: true})
}
}
上述示例代码是我本人实现,所以显得非常粗糙。规范而优雅的代码可以参考etcd源码。
首先是对于Message的term高于自身的请求,应当变为follower并向其投票。如果Message的term等于自身,且自身的身份不为follower,应当拒绝为其投票;否则则为其vote,并将自身election time重置(避免自身则投票后又向其竞选)。显然,如果该follower已经向其它同term内的candidate投票过,则应当拒绝为后来的candidate投票。
上述示例代码所没有演示的是,在follower保存的日志任期(LogTerm)大于Message的日志任期时,应拒绝为其投票。
func (r *Raft) handleRequestVoteResponse(m pb.Message) {
if m.Reject == false {
//r.lg.Debugf("Term:%d,%d receive vote from %d", m.Term, r.id, m.GetFrom())//debug log
if r.votes[m.GetFrom()] == false {
r.vts++
r.votes[m.GetFrom()] = true
}
}
//r.lg.Debugf("Term:%d,%d votes number: %d, peers number: %d", m.Term, r.id, r.vts, len(r.peerArray))//debug log
if r.vts > len(r.peerArray)/2 {
r.becomeLeader()
}
}
candidate在接到Response时,判断得票数是否已经满足成为leader的要求(超过节点总数的一半),并在满足要求的情况下变为leader。出于幂等性的考虑,应保留一个map记录各节点是否为这次选举已投过票。
只有唯一节点的特殊情况
Raft算法用于分布式场景,讨论只有唯一节点的选举情况在通常情况下没有意义,但tinykv似是出于严谨性的考虑,给出了只有一个节点的测试样例。在只有一个节点时,由于RequestVote请求无法发出,应该在becomeCandidate()时,先向自身投票,再发出RequestVoteMsg(实际上无法发出),然后直接在becomeCadidate()函数体内先行判断得票数是否已达到要求(只有唯一节点的情况下才有可能在此时达到要求)。
// becomeCandidate transform this peer's state to candidate
func (r *Raft) becomeCandidate() {
// Your Code Here (2A).
r.lg.Debugf("%d has become a candidate in Term %d", r.id, r.Term)
r.msgs = make([]pb.Message, 0)
r.votes = make(map[uint64]bool)
r.State = StateCandidate
r.Term++
r.votes[r.id] = true
r.Vote = r.id
r.electionElapsed = 0
r.electionTimeout = int((1 + rand.Float64()) * float64(r.eletimeout))
r.vts = 1
r.heartbeatElapsed = 0
if len(r.peerArray) == 1 {
r.becomeLeader()
return
}
}
参考文章
raft(一) 领导者选举 - 知乎 (zhihu.com)
talent-plan/tinykv: A course to build distributed key-value service based on TiKV model (github.com)