好的实现,看看别人怎么写的,github
大多数Raft的实现都是整体设计,包括存储处理,消息序列化和网络传输,但是本raft库在实现的时候只实现了最核心的算法,换来了灵活性和性能,网络和disk IO部分都由使用者实现,使用者需要实现自己的消息传送层,同时,需要自己实现持久化部分来存储Raft log和state。
为了实现Raft库的可测性,库在实现的时候将Raft建模为一个状态机,输入是消息,可能是本地时间的更新或者网络消息,状态机的输出是一个3元组:{[]Messages, []LogEntries, NextState}。
第一步是使用,怎么使用raft来搭建自己的key-value系统
etcd-raft代码走读
上面是raft中一个node做的事,Node代表raft集群中的一个节点,刚开始node是follower,然后随着
tickc
的进行,开始进入选举,raft在变为follower的时候做了下面几件事:初始化了tick函数
tickElection
,用来开始选举,做判断后,调用Step
判断消息类型为
MsgHup
,于是进入campaign
选举函数中做的事情
转换成candidate时,开始一个选举:
- 递增currentTerm;投票给自己;
- 重置election timer;
- 向所有的服务器发送 RequestVote RPC请求
接着看下send函数
send函数中将消息存储了
msgs
中,在哪儿消费呢?通过读取newReady来返回Ready此时又返回到
node.run
中,此时因为会进入case readyc <- rd
分支在里面做的事情
msgs
因为已经读取过了,设置为空,并且会赋值advancec
,进行到这readyc
中已经有一个数据, 而此channel会在函数Ready
中返回给外面,下面接着看谁会去读Ready
func (n *node) Ready() <-chan Ready { return n.readyc }
读取Ready的是应用程序,看下Ready()
函数的说明
//=> 读取到当前状态,当从Ready()取出状态后,需要调用 Advance
//=> 注意:只有当所有提交的entries都应用后,才会调用下一个 Ready 的状态
我们回到之前的选举上,读取到的Ready里面包含了Vote消息,我们会调用网络层发送消息出去,并且调用Advance()
,而此时其他Node接收到网络层消息后,会调用Step()
函数,在成为candidate的时候,我们设置了step函数为stepCandidate
,
自后调用了node的send函数,此时是拒绝的,因为已经是candidate状态了,而如果是follower,其处理函数是
stepFollower
,其规则就是之前说到的:
如果本地的voteFor为空或者为candidateId,并且候选者的日志至少与接受者的日志一样新,则投给其选票
进行到这,我们看到了follower在收到vote rpc后的处理,下面是candidate的处理了。
回到之前调用Ready()
,接着应该调用Advance
,
里面会设置
advancec
,好了,运行到这,我们又要回到node.run
中了
此时的状态是:candidate,advancec
中有数据,接着来看candidate在发送出vote rpc,接收到响应的处理,网络层的Send
函数是:
Send会调用
Peer.send
,函数注释说:此函数是非阻塞的,不保证请求一定能被peer收到一般常理我们发送后,等待响应后再处理,但是找了很久也没找到常理函数,这个时候,我们再去看下follower对于投票的处理
发现在响应上也是通过发送一个消息来响应的,因此我们此时可以看到peer之间的交互不是传统意义上的request-response模型了。
我们去看对于MsgVoteResp
的处理,其入口都是通过调用node.Step
函数,此时如果得到大多数票选,则成为leader
看becomeLeader函数
在leader函数中,最重要的就是发送命令了,我们看看这个过程
这是通过node.Propose
函数实现的
到最后又是通过step函数
里面挨个调用send函数
func (r *raft) bcastAppend() {
for id := range r.prs {
if id == r.id {
continue
}
r.sendAppend(id)
}
}
看完发送端,接着看follower的接收端处理
细看handleAppendEntries函数,就是去做raft协议中规定的操作了
在
maybeAppend
中,会去尝试更新committed index
,然后接着看AppendResp的处理去检查各个peer的matchIndex,然后尝试更新commitIndex
下一个问题,接着去看commitIndex > lastAppied后,在哪儿去应用log到状态机的
这是通过node.run
中readyc
和advancec
来实现的
上面就是etcd中raft的大致流程,有一个机遇raft实现的简单key-value系统,github地址:https://github.com/zhuanxuhit/distributed-system/tree/master/etcd-raft
读完代码后,最大的一个感受是整个node在实现的时候都是无锁的,其技巧是通过go的channel
将所有请求串行化,然后另一个特点是根据不同的状态,设置不同的处理函数,整个实现非常的清晰,因为每个状态针对每个请求的处理都是非常明确的。