tinykv 项目地址:https://github.com/talent-plan/tinykv
本博客提供一个参考思路,未必是正确答案(但能够通过测试集),请注意甄别。欢迎在评论区讨论。
Project2a 的整体目标是实现 Raft 算法。作为其子任务,Project2aa 实现其中的领导人选举(Leader Election)部分。
需要修改的文件:
raft/raft.go
首先我们需要完成函数newRaft
,用于创建一个全新的 Raft 结点。下面是一个简单的实现,提供了 Raft 的基本要素,主要包括State
,heartbeatTimeout
,electionTimeout
,votes
等。原文档没有解释清楚,但是可以看出,Raft.Vote
表示这个 Raft 给谁投了票,而Raft.vote
记录了谁给这个 Raft 投了票。这里值得一提的是electionElapsed
成员的初始化,原论文指出一个每次成为 follower(包括 Raft 新建立时)或者 candidate,都需要随机选取一个选举超时间隔,从而最大限度地避免多个 Raft 同时成为 candidate 的情况;但是原论文中没有说明具体要怎么去实现这个“随机”。raft_test.go
中的测试则隐约指出了这个随机的含义,基于 Raft 的成员electionTimeOut
,每次随机确定的选举超时间隔被规定在区间[electionTimeOut, 2 * electionTimeOut)
。在代码中,采用将electionElapsed
随机初始化到区间(-electionTimeOut, 0]
的方式实现相同的功能。
// newRaft return a raft peer with the given config
func newRaft(c *Config) *Raft {
if err := c.validate(); err != nil {
panic(err.Error())
}
// Your Code Here (2A).
raft := &Raft{id: c.ID,
State: StateFollower,
heartbeatTimeout: c.HeartbeatTick,
electionTimeout: c.ElectionTick,
electionElapsed: -rand.Intn(c.ElectionTick),
Prs: make(map[uint64]*Progress),
votes: make(map[uint64]bool),
}
for _, pr := range c.peers {
raft.votes[pr] = false
}
return raft
}
当然,上面只是对newRaft
函数的初步实现,在 Project2ab 和后续的任务中肯定需要对其进行修改。
接下来,我们还要实现函数sendHeartbeat
函数。实现方式是在对应 Raft 的msgs
(消息箱)中添加一个心跳类型的消息,代码如下所示。
// sendHeartbeat sends a heartbeat RPC to the given peer.
func (r *Raft) sendHeartbeat(to uint64) {
// Your Code Here (2A).
if r.State != StateLeader {
return
}
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgHeartbeat,
From: r.id,
To: to,
Term: r.Term,
})
}
类似地,实现一个sendRequestVote
函数。这是我自己增加的函数,后面会用到。
// newly added: sendRequestVote sends a RequestVote RPC to the given peer.
func (r *Raft) sendRequestVote(to uint64) {
if r.State != StateCandidate {
return
}
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgRequestVote,
From: r.id,
To: to,
Term: r.Term,
})
}
随后,实现tick
函数,这个函数的功能是使逻辑时钟计时一个滴答。对于所有的 State,electionElapsed
都要加一;而 leader 还额外维护了一个heartbeatElapsed
,这时它也要加一。可能出现下面两种行动:
- leader:在心跳超时间隔完成时,向其他所有 Raft 发出心跳。具体做法是将发送给其他 Raft 的心跳类型信息放入自己的信箱。
- candidate/follower:在选举超时间隔完成时,成为新一轮(Term 自增)的 candidate,并且向其他结点发送 RequestVote RPC 消息。通过处理(Step)本地的举手消息(MsgHup),启动新一轮的选举并发送 RequestVote RPC。
// tick advances the internal logical clock by a single tick.
func (r *Raft) tick() {
// Your Code Here (2A).
r.electionElapsed++
if r.State == StateLeader {
r.heartbeatElapsed++
if r.heartbeatElapsed >= r.heartbeatTimeout {
r.heartbeatElapsed = 0
r.Step(pb.Message{MsgType: pb.MessageType_MsgBeat})
}
} else if r.electionElapsed >= r.electionTimeout {
r.electionElapsed = -rand.Intn(r.electionTimeout)
r.Step(pb.Message{MsgType: pb.MessageType_MsgHup})
}
}
接下来完成函数becomeFollower
,描述一个 Raft 结点如何成为 follower 成员。需要注意的是,当一个结点称为新 Term的 follower 时,需要清除当前 Term 的选举记录 Vote。
// becomeFollower transform this peer's state to Follower
func (r *Raft) becomeFollower(term uint64, lead uint64) {
// Your Code Here (2A).
if term != r.Term {
// new term, currently votes for nobody
r.Vote = 0
}
r.Term = term
r.State = StateFollower
r.Lead = lead
r.electionElapsed = -rand.Intn(r.electionTimeout)
}
然后完成函数becomeCandidate
,描述一个 Raft 结点如何成为 candidate 成员。一个 Raft 在成为 candidate 时,会清楚当前 Term 其它结点给自己的投票结构(votes),并且进入新的 Term。在新 Term 中,Raft 立刻给自己投票。需要注意的是,如果当前 cluster 中只有自己一个 Raft 结点,它会立刻成为 leader。
// becomeCandidate transform this peer's state to candidate
func (r *Raft) becomeCandidate() {
// Your Code Here (2A).
r.Term++
r.Vote = r.id
r.State = StateCandidate
for pr, _ := range r.votes {
if pr == r.id {
r.votes[pr] = true
} else {
r.votes[pr] = false
}
}
r.electionElapsed = -rand.Intn(r.electionTimeout)
// extreme case: when the cluster has only 1 raft, it become leader immediately.
if len(r.votes) == 1 {
r.becomeLeader()
}
}
之后完成函数becomeLeader
,描述一个 Raft 结点如何成为一个 leader 成员。这个函数让我们注意,leadre 需要在它的任期发送一个空 Entry,但是这涉及到很多 Project2ab 的内容,我们在 Project2aa 中可以暂且不实现这个功能。
// becomeLeader transform this peer's state to leader
func (r *Raft) becomeLeader() {
// Your Code Here (2A).
// NOTE: Leader should propose a noop entry on its term
r.State = StateLeader
r.Lead = r.id
r.heartbeatElapsed = 0
// ignore entries temperarily
}
然后就是核心函数Step
了。一个 Raft 调用这个函数,用于处理本地的或者来自其他 Raft 的消息,以作出相应改变。这里我们没有对MsgPropose
,MsgAppend
等类型的消息进行处理,将放在后面的任务中完成。注意:
- 当一个 Raft 收到的消息中的 Term 大于自己当前的 Term 时,需要更新自己的 Term。
- candidate/follower 收到 MsgHup 消息时,会开启新的选举;而 leader 保持现状。
// Step the entrance of handle message, see `MessageType`
// on `eraftpb.proto` for what msgs should be handled
func (r *Raft) Step(m pb.Message) error {
// Your Code Here (2A).
// undone:
// MsgPropose 2
// MsgAppend 3: half completed
// MsgAppendResponce 4
// MsgSnapshot 7
// MessageType_MsgTransferLeader 11
// MessageType_MsgTimeoutNow 12
if m.Term > r.Term && m.MsgType != pb.MessageType_MsgAppend && m.MsgType != pb.MessageType_MsgRequestVote {
r.becomeFollower(m.Term, r.Lead)
return nil
}
switch r.State {
case StateFollower:
switch m.MsgType {
case pb.MessageType_MsgHup:
r.startNewElection()
case pb.MessageType_MsgRequestVote:
r.handleRequestVote(m)
case pb.MessageType_MsgHeartbeat:
r.handleHeartbeat(m)
case pb.MessageType_MsgAppend:
r.handleAppendEntries(m)
}
case StateCandidate:
switch m.MsgType {
case pb.MessageType_MsgHup:
r.startNewElection()
case pb.MessageType_MsgRequestVote:
r.handleRequestVote(m)
case pb.MessageType_MsgRequestVoteResponse:
if m.Term == r.Term {
r.votes[m.From] = !m.Reject
if r.canBecomeLeader() {
r.becomeLeader()
}
}
case pb.MessageType_MsgHeartbeat:
r.handleHeartbeat(m)
case pb.MessageType_MsgAppend:
r.becomeFollower(m.Term, r.id)
r.handleAppendEntries(m)
}
case StateLeader:
switch m.MsgType {
// note that MessageType_MsgHup wont't cause a leader to start a new election.
// the leader remains its term.
case pb.MessageType_MsgBeat:
for pr, _ := range r.votes {
if pr != r.id {
r.sendHeartbeat(pr)
}
}
case pb.MessageType_MsgRequestVote:
r.handleRequestVote(m)
case pb.MessageType_MsgHeartbeat:
r.handleHeartbeat(m)
case pb.MessageType_MsgAppend:
r.handleAppendEntries(m)
}
}
return nil
}
上面的Step
函数中调用了很多其他的函数。比如自定义的startNewElection
,其定义如下。
// newly added: startNewElection starts a new election, including turning the raft state to StateCandidate and send RequestVote RPC to other nodes
func (r *Raft) startNewElection() {
r.electionElapsed = -rand.Intn(r.electionTimeout)
r.becomeCandidate()
for pr, _ := range r.votes {
if pr != r.id {
r.sendRequestVote(pr)
}
}
}
以及自定义的handleRequestVote
:
// newly added: handleRequestVote handles RequestVote RPC request
func (r *Raft) handleRequestVote(m pb.Message) {
if m.MsgType != pb.MessageType_MsgRequestVote {
return
}
if m.Term < r.Term || (m.Term == r.Term && r.Vote != 0 && r.Vote != m.From) {
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgRequestVoteResponse,
From: r.id,
To: m.From,
Term: r.Term,
Reject: true,
})
} else {
r.becomeFollower(m.Term, m.From)
r.Vote = m.From
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgRequestVoteResponse,
From: r.id,
To: m.From,
Term: m.Term,
})
}
}
还有handleHeartbeat
函数:
// handleHeartbeat handle Heartbeat RPC request
func (r *Raft) handleHeartbeat(m pb.Message) {
// Your Code Here (2A).
if m.Term < r.Term {
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgHeartbeatResponse,
From: r.id,
To: m.From,
Term: r.Term,
Reject: true,
})
} else {
r.becomeFollower(m.Term, m.From)
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgHeartbeatResponse,
From: r.id,
To: m.From,
Term: m.Term,
})
}
}
然后是canBecomeLeader
。我记得论文还是哪个地方要求一个 candidate 在收到一半的拒绝时应该退回到 follower,但是暂且没实现这个功能。
// newly added: canBecomeLeader counts the supportive votes received and judge if a raft can become the leader
func (r *Raft) canBecomeLeader() bool {
if r.State != StateCandidate {
return false
}
count := 0
for _, support := range r.votes {
if support {
count++
if 2*count > len(r.votes) {
return true
}
}
}
return false
}
随后是两个自带的函数,实现如下。其中handleAppendEntries
的实现显然是不完整的,它涉及到 RaftLog 的更新,这个将在后面的项目中继续实现。
// handleAppendEntries handle AppendEntries RPC request
func (r *Raft) handleAppendEntries(m pb.Message) {
// Your Code Here (2A).
if m.MsgType != pb.MessageType_MsgAppend {
return
}
if m.Term < r.Term {
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgAppendResponse,
From: r.id,
To: m.From,
Term: r.Term,
Reject: true,
})
} else {
r.becomeFollower(m.Term, m.From)
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgAppendResponse,
From: r.id,
To: m.From,
Term: m.Term,
})
}
// need more complements to append entries to RaftLog.
}
// handleHeartbeat handle Heartbeat RPC request
func (r *Raft) handleHeartbeat(m pb.Message) {
// Your Code Here (2A).
if m.Term < r.Term {
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgHeartbeatResponse,
From: r.id,
To: m.From,
Term: r.Term,
Reject: true,
})
} else {
r.becomeFollower(m.Term, m.From)
r.msgs = append(r.msgs, pb.Message{
MsgType: pb.MessageType_MsgHeartbeatResponse,
From: r.id,
To: m.From,
Term: m.Term,
})
}
}
至此,Project2aa 也就完成了。