etcd-raft整体架构
引用:http://blog.mrcroxx.com/posts/code-reading/etcdraft-made-simple/2-overview/
RaftNode
就像在raft-example当中分析的那样,Raft Package只负责Raft 的具体逻辑的实现,而存储通信等模块就交给了上层的Raft Server,而二者之间的桥梁即为interface node
Raft Package对于raft进行进一步的封装,对外只暴露一个node借口,开发者可以使用Node接口当中的函数来使用Raft,而对于Node接口,Raft Package由定义了两个实现的结构体,分别是node 和rawnode,二者的主要区别为node通过channel保证了线程安全问题。
|
|
结构体当中的注视已经给的较为清晰,挑几个重点说一下:
首先是Tick,正如之前所说的那样,etcd当中的raft模块使用的是逻辑时钟,即需要手动调用某些函数来推进时钟,即为此处的Tick,该Tick留给上层在serverChannels当中进行调用。
在Node当中既有一个Tick()函数的实现,其向一个channel当中发送一个空结构体作为通知,而这个通知就被node.run()当中的一个select接收到,之后调用下层的raft的tick最终驱动时钟
而rn的Tick()为一个函数类型的字段,会根据当前raftnode的身份而选择不同的Tick(),如TickElection()、TickHeartBeat()等
1 2 3 4 5 6 7 8 9 10 11 12 13 14
func (n *node) Tick() { select { case n.tickc <- struct{}{}: case <-n.done: default: n.rn.raft.logger.Warningf("%x A tick missed to fire. Node blocks too long!", n.rn.raft.id) } } // in node.run() select { case <-n.tickc: n.rn.Tick() }
Campaign和Propose分别是用来进行选举和将上层kvstore传下来的请求封装为日志,这个留到之后的选举流程和日志再进行分析
Step()为整个Raft模块的核心,整个Raft状态机的改变或者更新都由Step函数来完成
Ready()之前已经分析过,在Raft Package当中已经完成了的工作,之后需要上层的raft server进行处理的都通过Ready进行传递,如需要持久化存储的HardState、Entries,已经提交了需要进行Apply的committedEntries,以及需要发送给其他节点的Messages[]
Advance(),通常和Ready()成对出现,当上层的Raft Server处理完一批通过Ready返回的内容之后,通过Advance()来通知下层的Raft Node已经完成,推进进度
ReadIndex()用于保证线性一致性,读取状态具有一个读取索引。一旦应用程序的进度超过了读取索引,任何在读取请求之前发出的线性可读请求就可以安全地处理。
Raft Package
还是先看一下相关的结构体
|
|
在其中除了一个raft本身的结构体,还有一个config结构体,在在上层的startRaft当中调用配置Raft的初始信息
|
|
Message
整个Raft的状态机的切换都是通过Step函数,而Step函数调用时会传入一个message,因此不妨先看一下message
grpc当中的消息传输为protobuf的形式,message通过grpc进行传输,首先在raft.proto当中定义,之后可以生成对应的go结构体。
所有的rpc的消息都通过Message来承载,如RequestVote,AppendEntries,通过一个MessageType进行区分。
Message也并不完全用于消息传输,对于本机的操作涉及到修改状态机的也可以通过封装成一个Message来交给Step处理
|
|
消息的类型可以在
当中查看
|
|
Step
etcd当中将所有和Raft状态机相关的操作全部集中于Step当中,而具体应当进行何种操作则根据Step当中传入的Message当中的MessageType进行判断,而对于Message的调用,一般在Node当中或直接或间接的对其进行调用。
对于Step,首先定义一个统一的Step函数,在其中进行一些预处理或者综合处理,即无论当前的节点的身份为何都可能需要进行的操作,之后在单独定义几个step函数,针对不同身份的节点。在Step当中,综合处理完之后仍需处理的就调用单独的step。而如果有一些操作就是针对某些特定身份,也可以直接调用对应的stepxxx,无需调用Step。
针对特定身份的step如下,作为一个函数类型的变量定义在结构体当中,在状态切换时进行设置
|
|
|
|
|
|
Campaign
:Campaign只会由candidate出发,对step
的直接调用,最终会调用到stepCandidate
|
|
Tick
:算是对Step的间接调用,主要顺序如下:
- 调用Tick时向channel当中发送一条通知
- 该通知被运行node.run()当中的协程获取到,调用下层raft的Tick,该Tick同样为一个函数类型的变量,如果当前为Leader,那么就会调用到
tickHeartBeat
- 在
tickHeartBeat
当中调用到最终的Step,对状态机进行操作
|
|
|
|
|
|
|
|