raft 系列解读(4) 之 etcd-raft学习

作者: 小聪明李良才 | 来源:发表于2016-10-25 15:02 被阅读4250次

好的实现,看看别人怎么写的,github

大多数Raft的实现都是整体设计,包括存储处理,消息序列化和网络传输,但是本raft库在实现的时候只实现了最核心的算法,换来了灵活性和性能,网络和disk IO部分都由使用者实现,使用者需要实现自己的消息传送层,同时,需要自己实现持久化部分来存储Raft log和state。
为了实现Raft库的可测性,库在实现的时候将Raft建模为一个状态机,输入是消息,可能是本地时间的更新或者网络消息,状态机的输出是一个3元组:{[]Messages, []LogEntries, NextState}。

第一步是使用,怎么使用raft来搭建自己的key-value系统

etcd-raft代码走读

node-run
上面是raft中一个node做的事,Node代表raft集群中的一个节点,刚开始node是follower,然后随着tickc的进行,开始进入选举,raft在变为follower的时候做了下面几件事:
becomeFollower
初始化了tick函数tickElection,用来开始选举,做判断后,调用Step
选举函数
判断消息类型为MsgHup,于是进入campaign
进行选举
选举函数中做的事情
选举
转换成candidate时,开始一个选举:
  1. 递增currentTerm;投票给自己;
  2. 重置election timer;
  3. 向所有的服务器发送 RequestVote RPC请求
成为candidate
接着看下send函数
raft.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

stepCandidate
自后调用了node的send函数,此时是拒绝的,因为已经是candidate状态了,而如果是follower,其处理函数是stepFollower,
stepFollower
其规则就是之前说到的:

如果本地的voteFor为空或者为candidateId,并且候选者的日志至少与接受者的日志一样新,则投给其选票

进行到这,我们看到了follower在收到vote rpc后的处理,下面是candidate的处理了。

回到之前调用Ready(),接着应该调用Advance,

Advance
里面会设置advancec,好了,运行到这,我们又要回到node.run中了

此时的状态是:candidate,advancec中有数据,接着来看candidate在发送出vote rpc,接收到响应的处理,网络层的Send函数是:

Transport.Send
Send会调用Peer.send,函数注释说:此函数是非阻塞的,不保证请求一定能被peer收到
Peer.send
一般常理我们发送后,等待响应后再处理,但是找了很久也没找到常理函数,这个时候,我们再去看下follower对于投票的处理
send.MsgVoteResp
发现在响应上也是通过发送一个消息来响应的,因此我们此时可以看到peer之间的交互不是传统意义上的request-response模型了。

我们去看对于MsgVoteResp的处理,其入口都是通过调用node.Step函数,此时如果得到大多数票选,则成为leader

MsgVoteResp处理
看becomeLeader函数
becomeLeader
在leader函数中,最重要的就是发送命令了,我们看看这个过程

这是通过node.Propose函数实现的

node.Propose
到最后又是通过step函数
stepLeader
里面挨个调用send函数
func (r *raft) bcastAppend() {
    for id := range r.prs {
        if id == r.id {
            continue
        }
        r.sendAppend(id)
    }
}
sendAppend
看完发送端,接着看follower的接收端处理
handleAppendEntries
细看handleAppendEntries函数,就是去做raft协议中规定的操作了
图片
maybeAppend中,会去尝试更新committed index,然后接着看AppendResp的处理
AppendResp
去检查各个peer的matchIndex,然后尝试更新commitIndex

下一个问题,接着去看commitIndex > lastAppied后,在哪儿去应用log到状态机的
这是通过node.runreadycadvancec来实现的

node.run

上面就是etcd中raft的大致流程,有一个机遇raft实现的简单key-value系统,github地址:https://github.com/zhuanxuhit/distributed-system/tree/master/etcd-raft

读完代码后,最大的一个感受是整个node在实现的时候都是无锁的,其技巧是通过go的channel将所有请求串行化,然后另一个特点是根据不同的状态,设置不同的处理函数,整个实现非常的清晰,因为每个状态针对每个请求的处理都是非常明确的。

相关文章

网友评论

    本文标题:raft 系列解读(4) 之 etcd-raft学习

    本文链接:https://www.haomeiwen.com/subject/htvruttx.html