本文主要记录了 raft 算法在 etcd 中的具体实现,主要分为 Leader 选举、日志复制、安全性、配置变更四大部分
1. 引言
etcd 在实现 raft 时,将内容依据职责边界拆分为应用层和算法层两个模块。
- 算法层:负责raft 算法的核心实现,包括日志二阶段决策,竞选计时,心跳计时,选举投票决策,节点角色管理,配置变更等
- 应用层:负责如何写入持久存储以及raft节点之间如何通信,同时负责处理外部客户端的请求,是raft算法层的使用者
算法层和应用层的交互是通过channel实现的
2. 核心数据结构
2.1. Entry
一个Entry就是一笔预写日志,它包含了普通类型(写请求)和配置变更两种类型。每笔日志都有对应的任期,索引以及类型等,对应日志的数据会序列化后存储在data中。
type EntryType int32
const (
EntryNormal EntryType = 0
// 配置变更类的日志
EntryConfChange EntryType = 1
)
type Entry struct {
Term uint64 `protobuf:"varint,2,opt,name=Term" json:"Term"`
Index uint64 `protobuf:"varint,3,opt,name=Index" json:"Index"`
Type EntryType `protobuf:"varint,1,opt,name=Type,enum=raftpb.EntryType" json:"Type"`
Data []byte `protobuf:"bytes,4,opt,name=Data" json:"Data,omitempty"`
}
2.2. Message
消息类型如下
const (
// 本节点要选举
MsgHup MessageType = 0
// MsgBeat不用于节点之间通信,仅用于leader内部HB时间到了让leader发送HB消息
MsgBeat MessageType = 1
// 用户向raft提交数据
MsgProp MessageType = 2
// leader向集群中其他节点同步数据
MsgApp MessageType = 3
// append消息的应答
MsgAppResp MessageType = 4
// 投票
MsgVote MessageType = 5
MsgVoteResp MessageType = 6
// 快照
MsgSnap MessageType = 7
// 心跳
MsgHeartbeat MessageType = 8
MsgHeartbeatResp MessageType = 9
MsgUnreachable MessageType = 10
// 快照状态
MsgSnapStatus MessageType = 11
// check quorum 机制用的
MsgCheckQuorum MessageType = 12
MsgTransferLeader MessageType = 13
MsgTimeoutNow MessageType = 14
// 只读请求
MsgReadIndex MessageType = 15
MsgReadIndexResp MessageType = 16
// 预投票
MsgPreVote MessageType = 17
MsgPreVoteResp MessageType = 18
// 持久化
MsgStorageAppend MessageType = 19
MsgStorageAppendResp MessageType = 20
// 应用到状态机
MsgStorageApply MessageType = 21
MsgStorageApplyResp MessageType = 22
MsgForgetLeader MessageType = 23
)
消息结构图如下所示
type Message struct {
// 消息类型
Type MessageType `protobuf:"varint,1,opt,name=type,enum=raftpb.MessageType" json:"type"`
// 消息来自哪个节点和发送到哪一个节点
To uint64 `protobuf:"varint,2,opt,name=to" json:"to"`
From uint64 `protobuf:"varint,3,opt,name=from" json:"from"`
// 消息发送方的当前任期
Term uint64 `protobuf:"varint,4,opt,name=term" json:"term"`
// 待同步日志的上一笔日志的任期和索引,用于leader节点日志同步
LogTerm uint64 `protobuf:"varint,5,opt,name=logTerm" json:"logTerm"`
Index uint64 `protobuf:"varint,6,opt,name=index" json:"index"`
// 预写日志
Entries []Entry `protobuf:"bytes,7,rep,name=entries" json:"entries"`
// leader 已提交的日志索引
Commit uint64 `protobuf:"varint,8,opt,name=commit" json:"commit"`
// 标识响应结果为拒绝或赞同
Reject bool `protobuf:"varint,10,opt,name=reject" json:"reject"`
// 日志同步快速回退用的
RejectHint uint64 `protobuf:"varint,11,opt,name=rejectHint" json:"rejectHint"`
// ...
}
在算法层中,raft 节点本质上是一个状态机,根据输入的消息体进行状态变更. 这里提到的所谓”消息体“ 指的就是上述 Message 类.
2.3. step
Step 函数是Raft算法中的核心函数之一,它的主要作用是根据接收到的消息类型和当前节点的角色(Leader、Follower或Candidate),执行相应的状态转移和算法逻辑。becomeXXX 函数是一组用于改变Raft节点角色的函数,它们让状态机在不同角色之间切换。
// 下面是raft节点在某个角色下对消息体如何处理
func (r *raft) Step(m pb.Message) error
func stepLeader(r *raft, m pb.Message) error
func stepCandidate(r *raft, m pb.Message) error
func stepFollower(r *raft, m pb.Message) error
// Step raft的状态机转移
func (r *raft) Step(m pb.Message) error {
switch {
case m.Term == 0:
// local message
case m.Term > r.Term:
if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
...
}
switch {
case m.Type == pb.MsgPreVote:
case m.Type == pb.MsgPreVoteResp && !m.Reject:
default:
...
}
case m.Term < r.Term:
if (r.checkQuorum || r.preVote) && (m.Type == pb.MsgHeartbeat || m.Type == pb.MsgApp) {
...
} else if m.Type == pb.MsgPreVote {
...
} else if m.Type == pb.MsgStorageAppendResp {
...
} else {
...
}
}
switch m.Type {
case pb.MsgHup:
...
case pb.MsgStorageAppendResp:
...
case pb.MsgStorageApplyResp:
...
case pb.MsgVote, pb.MsgPreVote:
...
default:
// r.step 会根据角色不同,继续调用一下三个函数其中的一个
// func stepLeader(r *raft, m pb.Message) error
// func stepCandidate(r *raft, m pb.Message) error
// func stepFollower(r *raft, m pb.Message) error
err := r.step(r, m)
...
}
...
}
而step、stepLeader等四个状态转移函数可以总结为以下逻辑:
- 如果消息是MsgVote
-
- 如果消息的任期更大
-
-
- 如果是leader lease 且选举未超时,忽略消息,不做任何处理,返回并结束流程
- 否则,会转化为follower,则根据是否满足投票条件进行投票或者拒绝投票
-
-
- 如果消息的任期更小,忽略消息,不做任何处理并返回
- 如果任期相等,发送拒绝投票的信息回复
- 如果消息是MsgPreVote
-
- 如果消息的任期更大
-
-
- 如果是leader lease 且选举未超时,忽略消息,不做任何处理,返回并结束流程
- 否则,如果是follower,则根据是否满足投票条件进行投票或者拒绝投票
-
-
- 如果消息的任期更小,并拒绝对应的预投票并返回结束流程
- 如果是消息是MsgApp
-
- 如果消息的任期更大,说明是来自leader的信息,降级为follower并设置leaderid,同时继续执行后续的角色逻辑
- 如果消息的任期更小
-
-
- 如果开启了checkQuorum或者预投票,则主动发送一条消息让消息的节点进行降级,同时继续执行后续的角色逻辑
- 其他情况忽略,不做任何处理,返回并结束流程
-
-
- 如果是follower,处理日志添加逻辑
- 如果是candidate,转化为为follower,设置leaderid并继续处理日志添加逻辑
- 如果是消息是MsgHeartbeat
-
- 如果消息的任期更大,说明是来自leader的信息,降级为follower并设置leaderid,同时继续执行后续的角色逻辑
- 如果消息的任期更小
-
-
- 如果开启了checkQuorum或者预投票,则主动发送一条消息让消息的节点进行降级
- 其他情况忽略,不做任何处理,返回并结束流程
-
-
- 如果是follower,处理心跳的相关逻辑
- 如果是candidate,转化为为follower,设置leaderid并继续心跳的相关逻辑
- 如果是消息是MsgSnap
-
- 如果消息的任期更大,说明是来自leader的信息,降级为follower并设置leaderid,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是follower,处理快照添加的相关逻辑
- 如果是candidate,转化为为follower,设置leaderid并继续快照添加的相关逻辑
- 如果消息是MsgHup(任期为0,本地消息)
-
- 根据节点是否已经发起过预投票去决定投票类型是否是预投票
- 发起投票的节点应该满足:不是leader或者learner,当前节点没有被集群移除,当前节点没有有未被保存到稳定存储中的快照
- 若满足上述条件,调用campaign函数发起投票
- 如果消息是MsgStorageAppendResp
-
- 如果消息的任期更大,降级为follower,不设置leader id,处理unstable 和stable 里面的快照日志的相关信息(不一定会改变),也就是日志持久化的逻辑
- 如果消息的任期更小,在处理下面的逻辑后,返回并结束流程
-
-
- 日志索引不为0,不做任何处理,因为可能会被新的任期覆盖
- 快照不为空,可以处理持久化快照的存储响应,因为持久化快照的存储响应可能是最新的,如果在后续处理逻辑中,判断出是比较旧的,则会被忽略
-
-
- 如果任期相等,会处理unstable 和stable 里面的快照日志的相关信息,也就是日志持久化的逻辑
- 如果消息是MsgStorageApplyResp
-
- 如果消息任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果消息任期更大,降级为follower,不设置leader id,处理日志被应用到状态机的后续逻辑
- 如果消息任期相等,处理日志被应用到状态机的后续逻辑
- 如果消息是MsgBeat(任期为0,本地消息)
-
- 如果是leader节点,取出只读队列的最后一个唯一请求id,作为心跳的数据,向所有节点广播心跳
- 其他角色类型不处理
- 如果消息是MsgCheckQuorum(任期为0,本地消息):
-
- 如果是leader 节点,检查存活的follower是否超过半数,没有超过半数,降级为follower
- 其他角色类型不处理
- 如果消息是MsgProp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader 节点,会添加日志并向其他follower节点同步,如果日志内容包括配置变更,还会继续处理配置变更逻辑
- 如果是candidate,返回ErrProposalDropped错误
- 如果是follower,会判断是否有leader,如果有转发给leader
- 如果消息是MsgReadIndex
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader节点
-
-
- 如果集群只有一个节点,直接返回commit index
- 根据raft节点的只读选项进行后续处理
-
-
- 如果是follower,会判断是否有leader,如果有转发给leader
- 其他角色类型不处理
- 如果消息是MsgForgetLeader:
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader,不做任何处理
- 如果是follower,设置leader id 为 None
- 其他角色类型不处理
- 如果消息是MsgAppResp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader,根据日志添加响应处理后续日志复制逻辑(限流、批处理、快照等状况)
- 其他角色不处理
- 如果消息是MsgHeartbeatResp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader,处理只读请求和CheckQuorum的相关逻辑
- 其他角色不处理
- 如果消息是MsgSnapStatus(本地消息,任期为0)
-
- 如果是leader,处理日志复制流量控制的相关逻辑
- 其他角色不处理
- 如果消息是MsgUnreachable(本地消息,任期为0):
-
- 如果是leader,处理日志复制流量控制的相关逻辑
- 其他角色不处理
- 如果是消息是MsgTransferLeader
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理
- 如果是leader,处理领导者转移的相关逻辑
- 如果是follower,设置新leader
- 如果消息是MsgReadIndexResp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理
- 如果follower,处理只读请求的响应
- 如果消息是MsgVoteResp
-
- 如果消息的任期更大,则降级为follower
- 如果是candidate,处理后续投票逻辑
- 其他角色不处理
- 如果消息是MsgPreVoteResp
-
- 如果消息的任期更大,且不同意投票,则降级为follower,执行后续逻辑
- 如果是candidate,处理后续投票逻辑
- 其他角色不处理
2.4. RaftLog
raftLog中的数据,按照是否已持久化到稳定存储,可分为两部分:已持久化到稳定存储的部分(stable)和还未持久化到稳定存储的部分(unstable)。前者在算法层内由 raftLog.unstable 完成,后者在应用层内完成
无论是stable的部分还是unstable的部分中,都可能包含快照或日志,且每部分的快照中包含的已压缩的日志比该部分相应的未压缩的日志更旧。
在etcd/raft的实现中,在同一时刻,raftLog中的4个段可能并不是同时存在的。
type raftLog struct {
// 持久化存储
storage Storage
// 用于保存还没有持久化的数据和快照,这些数据最终都会保存到storage中
unstable unstable
// committed数据索引
committed uint64
// committed保存是写入持久化存储中的最高index,而applied保存的是传入状态机中的最高index
// 即一条日志首先要提交成功(即committed),才能被applied到状态机中
// 因此以下不等式一直成立:applied <= committed
applied uint64
// ...
}
在etcd/raft的日志操作中,有4个经常使用的索引:
注意:这些索引都是相对于当前节点而不是整个集群的
| 索引名 | 描述 |
|---|---|
| committed | 在该节点所知数量达到大多数的节点保存到了稳定存储中的日志里,index最高的日志的index |
| applied | 在该节点的应用程序已应用到其状态机的日志里,index最高的日志的index。 其中, applied≤committed applied \le committed applied≤committed 总是成立的 |
| firstIndex | 在该节点的日志中,最新的快照之后的第一条日志的index |
| lastIndex | 在该节点的日志中,最后一条日志的index |
2.4.1. Storage
在Raft共识算法中,Storage接口定义了从稳定存储读取数据的六种方法。这些方法用于访问和管理持久化状态,其中包括:
- HardState:指Raft状态机需要在本地稳定存储中持久保存的状态,例如当前任期(term)、投票给哪个候选者的信息等。
- SoftState:指不需要持久化保存的状态,如当前领导者信息等。这类状态可以在重启后重新计算或获取。
由于旧的日志条目会被压缩成快照以节省空间,某些方法可能无法总是直接获取到所需的数据,而是需要通过快照来恢复。
type Storage interface {
// 返回保存的初始状态
InitialState() (pb.HardState, pb.ConfState, error)
// 返回索引范围在[lo,hi)之内并且不大于maxSize的entries数组
Entries(lo, hi, maxSize uint64) ([]pb.Entry, error)
// 传入一个索引值,返回这个索引值对应的任期号,如果不存在则error不为空,其中:
// ErrCompacted:表示传入的索引数据已经找不到,说明已经被压缩成快照数据了。
// ErrUnavailable:表示传入的索引值大于当前的最大索引
Term(i uint64) (uint64, error)
// 获得最后一条数据的索引值
LastIndex() (uint64, error)
// 返回第一条数据的索引值
FirstIndex() (uint64, error)
// 返回快照
Snapshot() (pb.Snapshot, error)
}
Q1: etcd的 MemoryStorage结构体 实现了Storage接口(你想的没错,就是存储在内存,不同步到磁盘),这样做不怕数据丢吗?
etcd使用了基于内存的MemoryStorage,是因为etcd在写入MemoryStorage前,需要先写入wal。这一步能保证程序后崩溃后能及时恢复数据,所以没有必要在将这个日志再写一次进磁盘里面。
2.4.2. unstable
unstable:可读可写;entries 是还未持久化的预写日志列表;offset 是首笔未持久化预写日志在全局预写日志中的索引偏移量
type unstable struct {
// the incoming unstable snapshot, if any.
snapshot *pb.Snapshot
// all entries that have not yet been written to storage.
entries []pb.Entry
offset uint64
logger Logger
}
Q1:firstIndex,lastIndex 为什么是maybe?
firstIndex,lastIndex 这个是相对于整个rafelog的,包括stable 和 unstable。
只有unstable中包含快照时,unstable才可能得知整个raftLog的first index的位置(快照前的日志不会影响快照后的状态);而只有当unstable中既没有日志也没有快照时,unstable才无法得知last index的位置。
| 方法 | 描述 |
|---|---|
maybeFirstIndex() (uint64, bool) | 获取相对于整个raftLog的首个索引(first index)。如果unstable无法确定该值,则第二个返回值为false。 |
maybeLastIndex() (uint64, bool) | 获取相对于整个raftLog的最后一个索引(last index)。如果unstable无法确定该值,则第二个返回值为false。 |
maybeTerm(i uint64) (uint64, bool) | 获取给定索引i的日志条目的任期(term)。如果unstable无法确定该值,则第二个返回值为false。 |
stableTo(i, t uint64) | 通知unstable当前索引i及其之前的日志已保存到稳定存储中,并且这些日志的任期为t。可以裁剪掉unstable中的这段日志以释放空间,并根据空间利用率进行优化。 |
stableSnapTo(i uint64) | 通知unstable当前索引i及其之前的快照已保存到稳定存储中。如果unstable中保存了该快照,则可以释放该快照以节省内存。 |
restore(s pb.Snapshot) | 根据提供的快照恢复unstable的状态。这包括设置unstable中的偏移量(offset)、快照(snapshot),并将现有的日志条目(entries)清空。 |
truncateAndAppend(ents []pb.Entry) | 对给定的日志切片进行裁剪,并将其追加到unstable保存的日志中。此操作通常用于处理新的日志条目或快照。 |
slice(lo uint64, hi uint64) | 返回指定范围内的日志切片。首先通过mustCheckOutOfBounds(lo, hi uint64)方法检查是否越界;如果越界则会触发panic。 |
2.5. Ready
Ready 是算法层与应用层交互的数据格式. 每当算法层执行完一轮处理逻辑后,会往一个 channel 中(readyc) 传入一个 Ready 结构体,其中封装了算法层处理好的结果.
上面图画少了一个,懒得改了
type Ready struct {
// 软状态是异变的,包括:当前集群leader、当前节点状态
*SoftState
// 硬状态需要被保存,包括:节点当前Term、Vote、Commit
// 如果当前这部分没有更新,则等于空状态
pb.HardState
ReadStates []ReadState // 线性一致性读的请求处理
// 需要在消息发送之前被写入到持久化存储中的entries数据数组和快照文件
Entries []pb.Entry
Snapshot pb.Snapshot
// 需要输入到状态机中的数据数组,这些数据之前已经被保存到持久化存储中了(发给应用层的)
CommittedEntries []pb.Entry
// 在entries被写入持久化存储中以后,需要发送出去的数据
Messages []pb.Message
}
2.6. node、rawnode、node
Node 是算法层中 raft 节点的抽象,也是应用层与算法层交互的唯一入口,应用层持有Node 作为算法层 raft 节点的引用,通过调用 Node 接口的几个 api,完成与算法层的 channel 通信.
注意:提议指的是 leader 向所有节点发起日志同步请求的过程.提交指的是 leader 认可一笔写请求已经被系统采纳的动作
type Node interface {
// 由时钟(循环定时器)驱动,每隔一定时间调用一次,驱动raft结构体的内部时钟运行
Tick()
// 发起选举
Campaign(ctx context.Context) error
// 写提议请求
Propose(ctx context.Context, data []byte) error
// 提议变更请求
ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
// 应用变更请求
ApplyConfChange(cc pb.ConfChange) *pb.ConfState
// 读请求
ReadIndex(ctx context.Context, rctx []byte) error
// 根据消息进行状态机转移
Step(ctx context.Context, msg pb.Message) error
// 准确的说,是Ready方法返回的Ready结构体信道的信号与Advance方法成对出现。
// 每当从Ready结构体信道中收到来自raft的消息时,用户需要按照一定顺序对Ready结构体中的字段进行处理。
// 在完成对Ready的处理后,需要调用Advance方法,通知raft这批数据已经处理完成,可以继续传入下一批
Ready() <-chan Ready
Advance()
// 返节点回状态
Status() Status
ReportUnreachable(id uint64)
ReportSnapshot(id uint64, status SnapshotStatus)
// 停止
Stop()
}
在etcd/raft中,Node接口的实现一共有两个,分别是node结构体和rawnode结构体。二者都是对etcd/raft中Raft状态机raft结构体进行操作。不同的是,node结构体是线程安全的,其内部封装了rawnode,并通过各种信道实现线程安全的操作;而rawnode是非线程安全的,其直接将Node接口中的方法转为对raft结构体的方法的调用。rawnode是为需要实现Multi-Raft的开发者提供的更底层的接口。
3. raft算法层和etcd server 应用层如何交互
raft 算法层调用这个创建一个node
并开启一个协程,调用run方法
func StartNode(c *Config, peers []Peer) Node {
n := setupNode(c, peers)
go n.run()
return n
}
在run方法里面实现了算法层和应用层的交互
下面算法层 run方法的处理逻辑,也是经典的for+select
func (n *node) run() {
var propc chan msgWithResult
var readyc chan Ready
var advancec chan struct{}
var rd Ready
r := n.rn.raft
lead := None
for {
if advancec == nil && n.rn.HasReady() {
// advancec 为空,且有新数据
rd = n.rn.readyWithoutAccept()
readyc = n.readyc
}
if lead != r.lead {
if r.hasLeader() {
if lead == None {
r.logger.Infof("raft.node: %x elected leader %x at term %d", r.id, r.lead, r.Term)
} else {
r.logger.Infof("raft.node: %x changed leader from %x to %x at term %d", r.id, lead, r.lead, r.Term)
}
propc = n.propc
} else {
r.logger.Infof("raft.node: %x lost leader %x at term %d", r.id, lead, r.Term)
propc = nil
}
lead = r.lead
}
select {
case pm := <-propc:
// 处理本地收到的提交值
m := pm.m
m.From = r.id
err := r.Step(m)
if pm.result != nil {
pm.result <- err
close(pm.result)
}
case m := <-n.recvc:
// 处理其他节点的
if IsResponseMsg(m.Type) && !IsLocalMsgTarget(m.From) && r.trk.Progress[m.From] == nil {
break
}
r.Step(m)
case cc := <-n.confc:
// 配置变更的
_, okBefore := r.trk.Progress[r.id]
cs := r.applyConfChange(cc)
if _, okAfter := r.trk.Progress[r.id]; okBefore && !okAfter {
var found bool
for _, sl := range [][]uint64{cs.Voters, cs.VotersOutgoing} {
for _, id := range sl {
if id == r.id {
found = true
break
}
}
if found {
break
}
}
if !found {
propc = nil
}
}
select {
case n.confstatec <- cs:
case <-n.done:
}
case <-n.tickc:
n.rn.Tick()
case readyc <- rd:
// 通过channel写入ready数据
// 以下先把ready的值保存下来,等待下一次循环使用,或者当advance调用完毕之后用于修改raftLog
n.rn.acceptReady(rd)
if !n.rn.asyncStorageWrites {
advancec = n.advancec
} else {
rd = Ready{}
}
readyc = nil
case <-advancec:
// 收到advance channel的消息
n.rn.Advance(rd)
rd = Ready{}
advancec = nil
case c := <-n.status:
c <- getStatus(r)
case <-n.stop:
close(n.done)
return
}
}
}
下面是应用层(etcd server)主循环的逻辑,同样是经典的for+select
func (r *raftNode) start(rh *raftReadyHandler) {
internalTimeout := time.Second
go func() {
defer r.onStop()
islead := false
for {
select {
case <-r.ticker.C:
// 定时
r.tick()
// 调用Ready()方法,其实就是调用rawnode 的Ready方法,告诉算法层我需要新的数据了
case rd := <-r.Ready():
// 有算法层来的消息
if rd.SoftState != nil {
newLeader := rd.SoftState.Lead != raft.None && rh.getLead() != rd.SoftState.Lead
if newLeader {
leaderChanges.Inc()
}
if rd.SoftState.Lead == raft.None {
hasLeader.Set(0)
} else {
hasLeader.Set(1)
}
rh.updateLead(rd.SoftState.Lead)
islead = rd.RaftState == raft.StateLeader
if islead {
isLeader.Set(1)
} else {
isLeader.Set(0)
}
rh.updateLeadership(newLeader)
r.td.Reset()
}
// 线性一致性读请求处理
if len(rd.ReadStates) != 0 {
select {
case r.readStateC <- rd.ReadStates[len(rd.ReadStates)-1]:
case <-time.After(internalTimeout):
r.lg.Warn("timed out sending read state", zap.Duration("timeout", internalTimeout))
case <-r.stopped:
return
}
}
notifyc := make(chan struct{}, 1)
raftAdvancedC := make(chan struct{}, 1)
ap := toApply{
entries: rd.CommittedEntries,
snapshot: rd.Snapshot,
notifyc: notifyc,
raftAdvancedC: raftAdvancedC,
}
// 更新committed index
updateCommittedIndex(&ap, rh)
select {
// 将日志应用到状态机(也就是etcd server)
case r.applyc <- ap:
case <-r.stopped:
return
}
if islead {
// Messages 是发送给其他节点的
r.transport.Send(r.processMessages(rd.Messages))
}
// 这部分是wal 日志应用 和 raft 日志持久化
if !raft.IsEmptySnap(rd.Snapshot) {
if err := r.storage.SaveSnap(rd.Snapshot); err != nil {
r.lg.Fatal("failed to save Raft snapshot", zap.Error(err))
}
}
if err := r.storage.Save(rd.HardState, rd.Entries); err != nil {
r.lg.Fatal("failed to save Raft hard state and entries", zap.Error(err))
}
if !raft.IsEmptyHardState(rd.HardState) {
proposalsCommitted.Set(float64(rd.HardState.Commit))
}
if !raft.IsEmptySnap(rd.Snapshot) {
if err := r.storage.Sync(); err != nil {
r.lg.Fatal("failed to sync Raft snapshot", zap.Error(err))
}
notifyc <- struct{}{}
r.raftStorage.ApplySnapshot(rd.Snapshot)
r.lg.Info("applied incoming Raft snapshot", zap.Uint64("snapshot-index", rd.Snapshot.Metadata.Index))
if err := r.storage.Release(rd.Snapshot); err != nil {
r.lg.Fatal("failed to release Raft wal", zap.Error(err))
}
}
// raft storage 日志持久化
r.raftStorage.Append(rd.Entries)
confChanged := false
for _, ent := range rd.CommittedEntries {
if ent.Type == raftpb.EntryConfChange {
confChanged = true
break
}
}
if !islead {
msgs := r.processMessages(rd.Messages)
notifyc <- struct{}{}
if confChanged {
// blocks until 'applyAll' calls 'applyWait.Trigger'
// to be in sync with scheduled config-change job
// (assume notifyc has cap of 1)
select {
case notifyc <- struct{}{}:
case <-r.stopped:
return
}
}
r.transport.Send(msgs)
} else {
notifyc <- struct{}{}
}
// 处理完,调用advance 告诉算法层,应用层处理完了
r.Advance()
if confChanged {
raftAdvancedC <- struct{}{}
}
case <-r.stopped:
return
}
}
}()
}
readyc 和 advancec 是成对出现的,readyc 发送算法层的值给应用层,应用层接受值后处理然后调用advance方法向advancec 方法发送值,表示已经处理
4. 选举
下面会讲解选举是如何实现的
4.1. 发起投票
q1: 如何触发选举?触发时会做什么?
选举可以主动触发,也可以被动超时触发。
触发时发送一条MsgHup消息
// 主动触发
func (n *node) Campaign(ctx context.Context) error { return n.step(ctx, pb.Message{Type: pb.MsgHup}) }
func (rn *RawNode) Campaign() error {
return rn.raft.Step(pb.Message{
Type: pb.MsgHup,
})
}
// 被动超时触发
func (r *raft) tickElection() {
r.electionElapsed++
if r.promotable() && r.pastElectionTimeout() {
r.electionElapsed = 0
r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
}
在step函数,对MsgHup消息类型的处理如下:
case pb.MsgHup:
// 是否预投票了
if r.preVote {
r.hup(campaignPreElection)
} else {
r.hup(campaignElection)
}
继续调用hup函数
func (r *raft) hup(t CampaignType) {
// 已经是leader
if r.state == StateLeader {
r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
return
}
// 判断当前节点能否提升为leader
// 条件如下:当前节点是否已被集群移除,当前节点是否为learner节点,当前节点是否还有未被保存到稳定存储中的快照
if !r.promotable() {
r.logger.Warningf("%x is unpromotable and can not campaign", r.id)
return
}
// 当前节点有未应用的配置变更
if r.hasUnappliedConfChanges() {
r.logger.Warningf("%x cannot campaign at term %d since there are still pending configuration changes to apply", r.id, r.Term)
return
}
r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
r.campaign(t)
}
campaign是用来发起投票或预投票的重要方法
func (r *raft) campaign(t CampaignType) {
// 判断当前节点能否提升为leader
if !r.promotable() {
r.logger.Warningf("%x is unpromotable; campaign() should have been called", r.id)
}
// 投票请求前的一些变量赋值
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection {
r.becomePreCandidate()
voteMsg = pb.MsgPreVote
term = r.Term + 1
} else {
r.becomeCandidate()
voteMsg = pb.MsgVote
term = r.Term
}
// 发送投票
var ids []uint64
{
idMap := r.trk.Voters.IDs()
ids = make([]uint64, 0, len(idMap))
for id := range idMap {
ids = append(ids, id)
}
slices.Sort(ids)
}
for _, id := range ids {
if id == r.id {
r.send(pb.Message{To: id, Term: term, Type: voteRespMsgType(voteMsg)})
continue
}
// TODO(pav-kv): it should be ok to simply print %+v for the lastEntryID.
last := r.raftLog.lastEntryID()
r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
r.id, last.term, last.index, voteMsg, id, r.Term)
var ctx []byte
if t == campaignTransfer {
ctx = []byte(t)
}
r.send(pb.Message{To: id, Term: term, Type: voteMsg, Index: last.index, LogTerm: last.term, Context: ctx})
}
}
4.2. 投票处理
投票信息的类型是MsgVote或者MsgPreVote
投票条件: (已经投过 or (没有投过且lead不存在) or (消息是预投票且消息任期更大) ) 且日志更新
逻辑总结为如下
- 如果消息是MsgVote
-
- 如果消息的任期更大
-
-
- 如果是leader lease 且选举未超时,忽略消息,不做任何处理,返回并结束流程
- 否则,会转化为follower,并根据是否满足投票条件进行投票或者拒绝投票
-
-
- 如果消息的任期更小,忽略消息,不做任何处理并返回
- 如果任期相等,发送拒绝投票的信息回复
- 如果消息是MsgPreVote
-
- 如果消息的任期更大
-
-
- 如果是leader lease 且选举未超时,忽略消息,不做任何处理,返回并结束流程
- 否则,如果是follower,则根据是否满足投票条件进行投票或者拒绝投票
-
-
- 如果消息的任期更小,并拒绝对应的预投票并返回结束流程
- 如果消息是MsgPreVoteResp
-
- 如果消息的任期更大,且不同意投票,则降级为follower,执行后续逻辑
- 如果是candidate,处理后续投票逻辑
- 其他角色不处理
- 如果消息是MsgVoteResp
-
- 如果消息的任期更大,则降级为follower
- 如果是candidate,处理后续投票逻辑
- 其他角色不处理
// step function
func (r *raft) Step(m pb.Message) error {
switch {
case m.Term == 0:
case m.Term > r.Term:
// 消息的Term大于节点当前的Term
if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
force := bytes.Equal(m.Context, []byte(campaignTransfer))
// 如果是leader lease 且选举未超时,返回
inLease := r.checkQuorum && r.lead != None && r.electionElapsed < r.electionTimeout
if !force && inLease {
// If a server receives a RequestVote request within the minimum election timeout
// of hearing from a current leader, it does not update its term or grant its vote
last := r.raftLog.lastEntryID()
// TODO(pav-kv): it should be ok to simply print the %+v of the lastEntryID.
r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] ignored %s from %x [logterm: %d, index: %d] at term %d: lease is not expired (remaining ticks: %d)",
r.id, last.term, last.index, r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term, r.electionTimeout-r.electionElapsed)
return nil
}
}
switch {
case m.Type == pb.MsgPreVote:
// 收到一笔任期大于自己的预竞选请求,暂时不变 follower
case m.Type == pb.MsgPreVoteResp && !m.Reject:
// 处理预竞选的响应时,对方未拒绝自己,暂时不变 follower
default:
// 其他情形下,收到任期更大的消息,都变成follower状态
r.logger.Infof("%x [term: %d] received a %s message with higher term from %x [term: %d]",
r.id, r.Term, m.Type, m.From, m.Term)
if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
r.becomeFollower(m.Term, m.From)
} else {
r.becomeFollower(m.Term, None)
}
}
case m.Term < r.Term:
if (r.checkQuorum || r.preVote) && (m.Type == pb.MsgHeartbeat || m.Type == pb.MsgApp){
...
}else if m.Type == pb.MsgPreVote {
...
r.send(pb.Message{To: m.From, Term: r.Term, Type: pb.MsgPreVoteResp, Reject: true})
}
...
return nil
}
// 能走到这里意味着消息的任期是大于等于本节点的
...
switch m.Type {
...
case pb.MsgVote, pb.MsgPreVote:
// 投票条件: 已经投过 or (没有投过且lead不存在) or (消息是预投票且消息任期更大)
canVote := r.Vote == m.From ||
(r.Vote == None && r.lead == None) ||
(m.Type == pb.MsgPreVote && m.Term > r.Term).
lastID := r.raftLog.lastEntryID()
candLastID := entryID{term: m.LogTerm, index: m.Index}
// 能投票且日志更新
if canVote && r.raftLog.isUpToDate(candLastID) {
r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] cast %s for %x [logterm: %d, index: %d] at term %d",
r.id, lastID.term, lastID.index, r.Vote, m.Type, m.From, candLastID.term, candLastID.index, r.Term)
r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
if m.Type == pb.MsgVote {
r.electionElapsed = 0
r.Vote = m.From
}
} else {
r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] rejected %s from %x [logterm: %d, index: %d] at term %d",
r.id, lastID.term, lastID.index, r.Vote, m.Type, m.From, candLastID.term, candLastID.index, r.Term)
r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
}
}
}
// stepCandiate function
func stepCandidate(r *raft, m pb.Message) error {
var myVoteRespType pb.MessageType
if r.state == StatePreCandidate {
myVoteRespType = pb.MsgPreVoteResp
} else {
myVoteRespType = pb.MsgVoteResp
}
switch m.Type {
// ...
case myVoteRespType:
// 计算选票,赞成票数,拒绝票数,结果
gr, rj, res := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x has received %d %s votes and %d vote rejections", r.id, gr, m.Type, rj)
switch res {
case quorum.VoteWon:
if r.state == StatePreCandidate {
r.campaign(campaignElection)
} else {
// 成为leader并广播日志
r.becomeLeader()
r.bcastAppend()
}
case quorum.VoteLost:
// pb.MsgPreVoteResp contains future term of pre-candidate
// m.Term > r.Term; reuse r.Term
r.becomeFollower(r.Term, None)
}
}
5. 日志
5.1. 复制进度
leader通过 nextInext[] 和 matchIndex[] 来跟踪follower的日志进度。
matchIndex[]记录了每个跟随者已经成功复制的日志条目索引。nextIndex[]记录了领导者为每个跟随者准备复制的下一个日志索引。
而etcd/raft为了解耦不同情况下的日志复制逻辑并实现一些日志复制相关的优化(限流和批处理),还需要记录一些其它信息。
Progess结构体是leader用来跟踪follower日志复制进度的结构,即“表示从leader视角看到的follower的进度”。leader会为每个follower(和learner)维护各自的Progress结构
type Progress struct {
Match uint64
Next uint64
// 正在传输中的最高提交索引
sentCommit uint64
State StateType
// 用于跟踪领导者在意识到需要快照时的最后索引
PendingSnapshot uint64
// 如果进度最近活跃,则为 true
RecentActive bool
// 用于指示对某个节点的 MsgApp 流量是否被限流。当处于 StateProbe 状态或在 StateReplicate 状态下,Inflights 饱和时,会发生限
MsgAppFlowPaused bool
// 用于表示消息的滑动窗口
Inflights *Inflights
IsLearner bool
}
const (
StateProbe StateType = iota
StateReplicate
StateSnapshot
)
etcd/raft将leader向follower复制日志的行为分成三种,记录在Progress的State字段中:
- StateProbe:当leader刚刚当选时,或当follower拒绝了leader复制的日志时,该follower的进度状态会变为StateProbe类型。在该状态下,leader每次心跳期间仅为follower发送一条MsgApp消息,且leader会根据follower发送的相应的MsgAppResp消息调整该follower的进度。
- StateReplicate:该状态下的follower处于稳定状态,leader会优化为其复制日志的速度,每次可能发送多条MsgApp消息(受Progress的流控限制,后文会详细介绍)。
- StateSnapshot:当follower所需的日志已被压缩无法访问时,leader会将该follower的进度置为StateSnapshot状态,并向该follower发送快照。leader不会为处于StateSnapshot状态的follower发送任何的MsgApp消息,直到其成功收到快照。
可以理解为StateType 限制Inflights这个fifo队列的大小
StateProbe => Inflight.size = 1
StateReplicate => Inflight.size = MaxInflightMsgs
StateSnapshot => Inflight.size = 0
通过这种方式,实现了限流和批处理
三种状态转化图如下
5.2. leader 当选的日志处理
leader 发送日志的过程如下:
- 节点在当选为leader后,会初始化所有节点的next index为leader日志的last index + 1(因为leader刚当选时不知道除了自己之外的节点的复制进度,将除自己外的所有节点的match index置为0,而将自己的match index置为自己的last index)
- 之后会提交一条空日志条目,以提交之前term的日志
func (r *raft) becomeLeader() {
if r.state == StateFollower {
panic("invalid transition [follower -> leader]")
}
r.step = stepLeader
//
r.reset(r.Term)
r.tick = r.tickHeartbeat
r.lead = r.id
r.state = StateLeader
pr := r.trk.Progress[r.id]
pr.BecomeReplicate()
// The leader always has RecentActive == true; MsgCheckQuorum makes sure to
// preserve this.
pr.RecentActive = true
r.pendingConfIndex = r.raftLog.lastIndex()
traceBecomeLeader(r)
// 提交空日志
emptyEnt := pb.Entry{Data: nil}
if !r.appendEntry(emptyEnt) {
// This won't happen because we just called reset() above.
r.logger.Panic("empty entry was dropped")
}
...
}
5.3. leader提议日志
“提议”是新的日志条目的起点,日志的提议是通过MsgProp消息实现的,处理逻辑如下
- 如果消息是MsgProp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader 节点,会添加日志并向其他follower节点同步,如果日志内容包括配置变更,还会继续处理配置变更逻辑
- 如果是candidate,返回ErrProposalDropped错误
- 如果是follower,会判断是否有leader,如果有转发给leader
从下面代码可以看出,只有leader才有资格处理日志提议
// stepCandidate
// ... ...
case pb.MsgProp:
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return ErrProposalDropped
// stepFollower :
// ... ...
case pb.MsgProp:
if r.lead == None {
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return ErrProposalDropped
} else if r.disableProposalForwarding {
r.logger.Infof("%x not forwarding to leader %x at term %d; dropping proposal", r.id, r.lead, r.Term)
return ErrProposalDropped
}
m.To = r.lead
r.send(m)
// stepLeader :
// ... ...
case pb.MsgProp:
if len(m.Entries) == 0 {
r.logger.Panicf("%x stepped empty MsgProp", r.id)
}
if r.prs.Progress[r.id] == nil {
// If we are not currently a member of the range (i.e. this node
// was removed from the configuration while serving as leader),
// drop any new proposals.
return ErrProposalDropped
}
if r.leadTransferee != None {
r.logger.Debugf("%x [term %d] transfer leadership to %x is in progress; dropping proposal", r.id, r.Term, r.leadTransferee)
return ErrProposalDropped
}
// Process ConfChange Msg
//... ...
if !r.appendEntry(m.Entries...) {
return ErrProposalDropped
}
r.bcastAppend()
return nil
5.4. leader 为follower复制日志
leader 主要通过调用bcastAppend 广播日志到其他节点
这个方法的调用时机:
- 成为leader添加空日志后会进行调用
- 收到AppProp 消息
// bcastAppend sends RPC, with entries to all peers that are not up-to-date
// according to the progress recorded in r.prs.
func (r *raft) bcastAppend() {
r.prs.Visit(func(id uint64, _ *tracker.Progress) {
if id == r.id {
return
}
r.sendAppend(id)
})
}
// sendAppend sends an append RPC with new entries (if any) and the
// current commit index to the given peer.
func (r *raft) sendAppend(to uint64) {
r.maybeSendAppend(to, true)
}
// maybeSendAppend sends an append RPC with new entries to the given peer,
// if necessary. Returns true if a message was sent. The sendIfEmpty
// argument controls whether messages with no entries will be sent
// ("empty" messages are useful to convey updated Commit indexes, but
// are undesirable when we're sending multiple messages in a batch).
func (r *raft) maybeSendAppend(to uint64, sendIfEmpty bool) bool {
pr := r.prs.Progress[to]
if pr.IsPaused() {
return false
}
m := pb.Message{}
m.To = to
term, errt := r.raftLog.term(pr.Next - 1)
ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)
if len(ents) == 0 && !sendIfEmpty {
return false
}
if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
// ... ... #1
} else {
// ... ... #2
}
r.send(m)
return true
}
5.5. follower 处理来自leader的日志
follower处理来自leader的日志复制消息时,同样分为对MsgApp和对MsgSnap的处理,在处理这两种消息时,都会使用MsgAppResp方法对其进行相应,消息处理逻辑如下:
- 如果是消息是MsgApp
-
- 如果消息的任期更大,说明是来自leader的信息,降级为follower并设置leaderid,同时继续执行后续的角色逻辑
- 如果消息的任期更小
-
-
- 如果开启了checkQuorum或者预投票,则主动发送一条消息让消息的节点进行降级,同时继续执行后续的角色逻辑
- 其他情况忽略,不做任何处理,返回并结束流程
-
-
- 如果是follower,处理日志添加逻辑
- 如果是candidate,转化为为follower,设置leaderid并继续处理日志添加逻辑
- 如果是消息是MsgSnap
-
- 如果消息的任期更大,说明是来自leader的信息,降级为follower并设置leaderid,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是follower,处理快照添加的相关逻辑
- 如果是candidate,转化为为follower,设置leaderid并继续快照添加的相关逻辑
handleAppendEntries方法用来处理MsgApp消息
handleSnapshot用来处理MsgSnap消息
func (r *raft) handleAppendEntries(m pb.Message) {
if m.Index < r.raftLog.committed {
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
return
}
// 添加日志
if mlastIndex, ok := r.raftLog.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...); ok {
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: mlastIndex})
} else {
r.logger.Debugf("%x [logterm: %d, index: %d] rejected MsgApp [logterm: %d, index: %d] from %x",
r.id, r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(m.Index)), m.Index, m.LogTerm, m.Index, m.From)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: m.Index, Reject: true, RejectHint: r.raftLog.lastIndex()})
}
}
func (r *raft) handleSnapshot(m pb.Message) {
sindex, sterm := m.Snapshot.Metadata.Index, m.Snapshot.Metadata.Term
// 恢复快照
if r.restore(m.Snapshot) {
r.logger.Infof("%x [commit: %d] restored snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, sindex, sterm)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.lastIndex()})
} else {
r.logger.Infof("%x [commit: %d] ignored snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, sindex, sterm)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
}
}
5.6. leader处理来自follower的日志复制响应
follower 在处理完日志后,会使用MsgAppResp方法对其进行相应
然后leader会继续处理follow发来的MsgAppResp消息
- 如果消息是MsgAppResp
-
- 如果消息的任期更大,降级为follower,不设置leader id,同时继续执行后续的角色逻辑
- 如果消息的任期更小,忽略消息,不做任何处理,返回并结束流程
- 如果是leader,根据日志添加响应处理后续日志复制逻辑(限流、批处理、快照等)
- 其他角色不处理
// stepLeader
// ... ...
case pb.MsgAppResp:
pr.RecentActive = true
if m.Reject {
// Follower处理MsgApp(Append Entries请求)消息时发现日志条目不匹配
if pr.MaybeDecrTo(m.Index, m.RejectHint) {
// 回退Follower的progress Next索引
r.logger.Debugf("%x decreased progress of %x to [%s]", r.id, m.From, pr)
if pr.State == tracker.StateReplicate {
// progress 状态转换
pr.BecomeProbe()
}
// 重新发送
r.sendAppend(m.From)
}
} else {
// 跟上了
oldPaused := pr.IsPaused()
// MaybeUpdate方法来判断该消息的Index字段是否跟上了该follower的match index,
// 并在需要时更新其next index
if pr.MaybeUpdate(m.Index) {
// 更新成功
switch {
case pr.State == tracker.StateProbe:
// 如果该follower处于StateProbe状态且现在跟上了进度,则将其转为StateReplica状态
pr.BecomeReplicate()
case pr.State == tracker.StateSnapshot && pr.Match >= pr.PendingSnapshot:
// 如果该follower处于StateSnapshot状态且现在跟上了进度,
// 且从该follower发送该消息后到leader处理这条消息时,
// leader没有为其发送新快照(通过比较Match与PendingSnapshot判断),
// 则将其转为StateReplica状态。
pr.BecomeProbe()
pr.BecomeReplicate()
case pr.State == tracker.StateReplicate:
// 如果该follower处于StateReplicate状态,那么释放Inflights中该消息的Index字段值之前的所有消息。
// 因为收到的MsgAppResp可能是乱序的,因此需要释放之前的所有消息(过期消息不会被处理)
pr.Inflights.FreeLE(m.Index)
}
if r.maybeCommit() {
r.bcastAppend()
} else if oldPaused {
// If we were paused before, this node may be missing the
// latest commit index, so send it.
r.sendAppend(m.From)
}
// We've updated flow control information above, which may
// allow us to send multiple (size-limited) in-flight messages
// at once (such as when transitioning from probe to
// replicate, or when freeTo() covers multiple messages). If
// we have more entries to send, send as many messages as we
// can (without sending empty messages for the commit index)
for r.maybeSendAppend(m.From, false) {
}
// ... ...
}
}
6. 配置
etcd/raft实现的配置是按照联合一致算法组织的
6.1. MajorityConfig
MajorityConfig是voter节点id的集合
// MajorityConfig is a set of IDs that uses majority quorums to make decisions.
type MajorityConfig map[uint64]struct{}
| 方法 | 描述 |
|---|---|
CommittedIndex(l AckedIndexer) Index | 计算被大多数节点接受的commit index |
VoteResult(votes map[uint64]bool) VoteResult | 计算投票结果 |
6.2. JointConfig
JointConfig表示联合一直算法下的配置,包括新老配置
它是一个数组,两个元素,分别代表新配置和老配置
type JointConfig [2]MajorityConfig
投票结果计算
// VoteResult takes a mapping of voters to yes/no (true/false) votes and returns
// a result indicating whether the vote is pending, lost, or won. A joint quorum
// requires both majority quorums to vote in favor.
func (c JointConfig) VoteResult(votes map[uint64]bool) VoteResult {
r1 := c[0].VoteResult(votes)
r2 := c[1].VoteResult(votes)
if r1 == r2 {
// 两个一样,有三种可能:赢,输,未知
return r1
}
if r1 == VoteLost || r2 == VoteLost {
// 只有有一个失败就是失败
return VoteLost
}
// 返回未知
return VotePending
}
返回联合一致下的提交索引
func (c JointConfig) CommittedIndex(l AckedIndexer) Index {
idx0 := c[0].CommittedIndex(l)
idx1 := c[1].CommittedIndex(l)
if idx0 < idx1 {
return idx0
}
return idx1
}
6.3. confchange
confchange 有以下几种类型:
- ConfChangeAddNode:添加新节点。
- ConfChangeRemoveNode:移除节点。
- ConfChangeUpdateNode:用于状态机更新节点url等操作,etcd/raft模块本身不会对节点进行任何操作。
- ConfChangeAddLearnerNode:添加learner节点。
6.4. 提议配置
提议配置要满足以下条件:
- Raft同一时间只能有一个未被提交的ConfChange
- 当前配置不能处于joint configuration
- 正在从联合一致转换成新配置
如果要拒绝提议,只需要将该日志条目替换为没有任何意义的普通空日志条目pb.Entry{Type: pb.EntryNormal}
// stepLeader
// case pb.MsgProp:
// ... ...
for i := range m.Entries {
e := &m.Entries[i]
var cc pb.ConfChangeI
if e.Type == pb.EntryConfChange {
var ccc pb.ConfChange
if err := ccc.Unmarshal(e.Data); err != nil {
panic(err)
}
cc = ccc
} else if e.Type == pb.EntryConfChangeV2 {
var ccc pb.ConfChangeV2
if err := ccc.Unmarshal(e.Data); err != nil {
panic(err)
}
cc = ccc
}
if cc != nil {
// 三个条件
alreadyPending := r.pendingConfIndex > r.raftLog.applied
alreadyJoint := len(r.prs.Config.Voters[1]) > 0
wantsLeaveJoint := len(cc.AsV2().Changes) == 0
var refused string
if alreadyPending {
refused = fmt.Sprintf("possible unapplied conf change at index %d (applied to %d)", r.pendingConfIndex, r.raftLog.applied)
} else if alreadyJoint && !wantsLeaveJoint {
refused = "must transition out of joint config first"
} else if !alreadyJoint && wantsLeaveJoint {
refused = "not in joint state; refusing empty conf change"
}
if refused != "" {
// 拒绝提议
r.logger.Infof("%x ignoring conf change %v at config %s: %s", r.id, cc, r.prs.Config, refused)
m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
} else {
r.pendingConfIndex = r.raftLog.lastIndex() + uint64(i) + 1
}
}
}
// ... ...
6.5. 应用配置
etcd/raft需要使用者自行调用Node的ApplyConfChange方法来应用新配置,也就是由应用层主动调用。该方法最终会调用raft结构体的applyConfChange方法,其源码如下:
func (r *raft) applyConfChange(cc pb.ConfChangeV2) pb.ConfState {
cfg, prs, err := func() (tracker.Config, tracker.ProgressMap, error) {
changer := confchange.Changer{
Tracker: r.prs,
LastIndex: r.raftLog.lastIndex(),
}
if cc.LeaveJoint() {
// 离开联合一致
return changer.LeaveJoint()
} else if autoLeave, ok := cc.EnterJoint(); ok {
// 进入联合一致
return changer.EnterJoint(autoLeave, cc.Changes...)
}
// 一个节点,简单节点变换算法
return changer.Simple(cc.Changes...)
}()
if err != nil {
// TODO(tbg): return the error to the caller.
panic(err)
}
// 进入新配置
return r.switchToConfig(cfg, prs)
}
7. 线性一致性读
前面讲过,线性一致性读有三种:
- Log Read
- ReadIndex
- Lease Read
etcd 实现了后两种,但无论是ReadIndex方法还是Lease Read方法,都需要获取read index。Node的ReadIndex方法就是用来获取read index的方法。
对于read index 消息处理区别是,一个是如果leader在有效期内,直接处理,一个是要通过心跳去核验leader 身份
case pb.MsgReadIndex:
// 单节点
if r.prs.IsSingleton() {
if resp := r.responseToReadIndexReq(m, r.raftLog.committed); resp.To != None {
r.send(resp)
}
return nil
}
if !r.committedEntryInCurrentTerm() {
return nil
}
switch r.readOnly.option {
case ReadOnlySafe:
// 通过心跳处理
r.readOnly.addRequest(r.raftLog.committed, m)
// The local node automatically acks the request.
r.readOnly.recvAck(r.id, m.Entries[0].Data)
r.bcastHeartbeatWithCtx(m.Entries[0].Data)
case ReadOnlyLeaseBased:
// 通过leader lease
if resp := r.responseToReadIndexReq(m, r.raftLog.committed); resp.To != None {
r.send(resp)
}
}
return nil
}