etcd-2-raft总体设计
0. 引言
etcd/raft将Raft算法是实现分成了三个模块:
- Raft状态机;
- 存储模块;
- 传输模块。
Raft状态机完全由etcd/raft负责,raft
结构体即为其实现。使用etc/raft的开发者不能直接操作raft
结构体,只能通过etcd/raft提供的Node
接口对其进行操作。
存储模块可以划分为两个部分:对存储的读取与写入。etcd/raft只需要读取存储,etcd/raft依赖的Storage
接口中只有读取存储的方法。而对存储的写入由用户负责,etcd/raft并不关心开发者如何写入存储,对存储的写入方法可以由开发者自己定义。etcd使用的存储模块是与Storage
接口同一文件下的MemoryStorage
结构体。MemoryStorage
既实现了Storage
接口需要的读取存储的方法,也为用户提供了写入存储的方法。
说明
Storage
接口定义的是稳定存储的读取方法。之所以etcd使用了基于内存的MemoryStorage
,是因为etcd在写入MemoryStorage
前,需要先写入预写日志(Write Ahead Log,WAL)或快照。而预写日志和快照是保存在稳定存储中的,这样,在每次重启时,etcd可以基于保存在稳定存储中的快照和预写日志恢复MemoryStorage
的状态。也就是说,etcd的稳定存储是通过快照、预写日志、MemoryStorage
三者共同实现的。
通信模块是完全由使用etcd/raft的开发者负责的。etcd/raft不关心开发者如何实现通信模块。
下图是一张关于etcd/raft的实现中,开发者与etcd/raft对这三个模块的职责的示意图。

因为Node
接口是开发者仅有的操作etcd/raft的方式,所以我们先来看看Node
接口与其相关实现。
1. Node、node、rawnode
Node
接口为开发者提供了操作etcd/raft的方法。其接口定义如下:
1 | // Node represents a node in a raft cluster. |
Node
结构中的方法按调用时机可以分为三类:
方法 | 描述 |
---|---|
Tick |
由时钟(循环定时器)驱动,每隔一定时间调用一次,驱动raft 结构体的内部时钟运行。 |
Ready 、Advance |
这两个方法往往成对出现。准确的说,是Ready 方法返回的Ready 结构体信道的信号与Advance 方法成对出现。每当从Ready 结构体信道中收到来自raft 的消息时,用户需要按照一定顺序对Ready 结构体中的字段机械能处理。在完成对Ready 的处理后,需要调用Advance 方法,通知raft 这批数据已经处理完成,可以继续传入下一批。 |
其它方法 | 需要时随时调用。 |
对于Ready
结构体,有几个重要的字段需要按照入顺序处理:
- 将
HardState
、Entries
、Snapshot
写入稳定存储(其中,Snapshot
的写入不需要严格按照此顺序,etcd/raft为快照的传入提供了另一套机制以优化执行效率)。 - 本条中的操作可以并行执行:
- 将
Messages
中的消息发送给相应的节点; - 将
Snapshot
和CommittedEntries
应用到本地状态机中。
- 将
- 调用
Advance
方法。
在了解了Node
接口的基本使用方式后,我们再关注一下其实现。
在etcd/raft中,Node
接口的实现一共有两个,分别是Node
结构体和raw
结构体。二者都是对etcd/raft中Raft状态机raft
结构体进行操作。不同的是,node
结构体是线程安全的,其内部封装了rawnode
,并通过各种信道操作实现线程安全的操作;而rawnode
是非线程安全的,其直接将Node
接口中的方法转为对raft
结构体对方法对调用。rawnode
是为需要实现Multi-Raft的开发者提供的更底层的接口。
学习etcd/raft中Raft算法的实现与优化不需要深入node
或rawnode
的实现,因此这里不对其进行详细的分析。
2. Raft状态机——raft
etcd/raft的实现的优雅之处之一,在于其很好地剥离了各模块的职责。在etcd/raft的实现中,raft
结构体是一个Raft状态机,其通过Step
方法进行状态转移。只要涉及到Raft状态机的状态转移,最终都会通过Step
方法完成。Step
方法的参数是Raft消息(在etcd/raft/raftpb中,是直接通过.proto
文件生成的Protocol Buffers的go语言实现)。
这里我们以Node
接口的Tick
方法为例,其调用了raft
结构体的tick
“方法”。
1 | // Tick advances the internal logical clock by a single tick. |
这里之所以给“方法”打上引号,是因为tick
其实并非一个真正的方法,而是raft
的一个字眼,其类型为一个无参无返回值的函数。
这样设计的原因,是leader和follower在tick
被调用时行为不同。tick
字段可能的值有两个,分别为tickElection
和tickHeartbeat
,二者分别对应follower(或candidate,pre candidate)和leader的tick
行为。leader每隔一段时间需要广播心跳来防止follower谋权篡位,folower每隔一段时间没有收到leader的心跳就要重新选主。我们可以在如下4个方法中找到相应的依据:
1 | func (r *raft) becomeFollower(term uint64, lead uint64) { |
这里我们先以tickElection
为例,分析其如何将这一方法转为对Step
方法对调用的。
1 | // tickElection is run by followers and candidates after r.electionTimeout. |
我们可以看到,tickElection
方法会增大electionElapsed
的值。当其超过了选举超时且当前节点可提拔为leader时(具体实现会在后续的文章中分析),重制其值,并创建一条MsgHup
消息,传给Step
方法。Step
方法会对该消息进行处理,并适当地转移Raft状态机的状态。
raft
结构体中的字段和相应的方法有很多。在后续的文章中,我们会再介绍etcd/raft中Raft算法的各部分实现时,介绍相应的字段与方法。这里仅给出创建node
或rawnode
时所需的Config
结构体的结构,其大部分字段都与raft
结构体中的有关字段相对应。
1 | // Config contains the parameters to start a raft. |
3. 总结
本文主要从顶层的视角,简单地分析了etcd/raft的总体设计。本文的主要目的是给读者etcd/raft的结构的整体认识,便于读者接下来学习etcd/raft中Raft算法的实现与优化。