raft实现原理可看raft算法原理个人解读-CSDN博客
1 etcd简介
etcd是一个具有强一致性的分布式键值对存储系统,底层基于raft协议保证分布式系统数据的一致性和可用性。
etcd基于golang编写,项目地址:GitHub - etcd-io/etcd: Distributed reliable key-value store for the most critical data of a distributed system
我看的版本是tag:v3.1.10
2 基本术语
中文名 | 英文名 | 说明 |
---|---|---|
算法层 | algorithm module | 内聚了raft共识机制的核心模块,以sdk的形式呗应用层引入使用,启动以独立goroutine的形式存在,与应用层通过channel通信 |
应用层 | application module | 聚合了etcd存储、通信能力的模块,启动时是raft节点的主goroutine,既要负责与客户端通信,又要负责和算法层交互 |
算法层节点 | node | 是raft节点在算法层的抽象,也是应用层与算法层交互的唯一入口 |
应用层节点 | raftNode | 是raft节点在应用层的抽象,内部持有算法层节点node的引用,同时提供处理客户端请求以及与集群内其他节点通信的能力 |
通信模块 | transport | etcd中的网络通信模块,为raft节点间通信提供服务 |
提议 | proposal | 两阶段提交中的第一阶段:提议 |
提交 | commit | 两阶段提交中的第二阶段:提交 |
应用 | apply | 将已提交的日志应用到数据状态机,使得写请求真正生效 |
数据状态机 | data state machine | raft节点用于存储数据的介质,为避免与raft节点状态机产生歧义,统一命名为数据状态机 |
节点状态机 | node state machine | etcd实现中,raft节点本质是个大的状态机,任何操作,比如选举、提交数据等,最后都会封装成一则消息输入节点状态机中,驱动节点状态发生变化 |
3 宏观架构梳理
简单的说:算法层node其实就是大脑,应用层raftNode就是身体, 应用层raftNode在接收到外界请求的时候,首先会把请求通过channel告知算法层node,然后由算法层做出决策,封装好应用层raftNode应该对此请求做出何等操作到channel告知应用层raftNode,应用层raftNode通过channel接收到算法层发送的决策,然后执行该决策
4 核心数据结构
4.1 Entry
代码位于./raft/raftpb/raft.pb.go
// Entry 预写日志结构体
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"`
XXX_unrecognized []byte `json:"-"`
}
type EntryType int32
const (
EntryNormal EntryType = 0
// 配置变更类日志
EntryConfChange EntryType = 1
)
一条Entry就是一笔预写日志,包含了普通类型(写清求)和配置变更两种类型。
Entry中包含任期Term、索引Index、内容Data三个字段,一条Entry的全局唯一标识是Term和Index共同组成。
4.2 Message
代码位于 ./raft/raftpb/raft.pb.go
// Message 消息体
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"`
// 日志任期
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"`
Snapshot Snapshot `protobuf:"bytes,9,opt,name=snapshot" json:"snapshot"`
// 响应投票结果为赞同或者拒绝,在节点响应精选投票或者响应日志同步时使用
Reject bool `protobuf:"varint,10,opt,name=reject" json:"reject"`
// 在日志同步过程中,如果节点发现自己的日志数据落后于(缺少)Leader的部分日志数据,会拒绝日志同步请求,并返回自己最后一条的日志索引给Leader,方便Leader发送自己缺失的部分日志数据
// 拒绝同步日志请求时返回的当前节点日志Index,用于被拒绝方快速定位到下一次合适的同步日志位置
RejectHint uint64 `protobuf:"varint,11,opt,name=rejectHint" json:"rejectHint"`
Context []byte `protobuf:"bytes,12,opt,name=context" json:"context,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
- Type:消息类型
type MessageType int32
const (
// 选举
MsgHup MessageType = 0
// 心跳检测超时Leader发送心跳给follower
MsgBeat MessageType = 1
// 写请求(只能Leader处理)
MsgProp MessageType = 2
// Leader给follower发送日志同步请求
MsgApp MessageType = 3
// follower给leader响应日志同步
MsgAppResp MessageType = 4
// 投票消息
MsgVote MessageType = 5
// 投票响应
MsgVoteResp MessageType = 6
MsgSnap MessageType = 7
MsgHeartbeat MessageType = 8
MsgHeartbeatResp MessageType = 9
MsgUnreachable MessageType = 10
MsgSnapStatus MessageType = 11
MsgCheckQuorum MessageType = 12
MsgTransferLeader MessageType = 13
MsgTimeoutNow MessageType = 14
// 读请求
MsgReadIndex MessageType = 15
// 读请求响应
MsgReadIndexResp MessageType = 16
// 预投票(防止candidate因为网络分区无限自增term)
MsgPreVote MessageType = 17
// 预投票响应
MsgPreVoteResp MessageType = 18
)
2.To/From:消息接受和发送方,以节点的id为标识
3.Term:消息发送方的任期
4.LogTerm、Index:带同步日志的上一笔日志的任期和索引(后续介绍)
5.Entries:待完成同步的预写日志
6.Commit:Leader已提交的日志索引
7.Reject:标识响应结果为拒绝或者赞同。竞选和响应日志同步使用
8.RejectHint:拒绝同步日志请求时返回的当前节点日志Index,用于被拒绝方快速定位到下一次合适的同步日志位置
4.3 raftLog
raftLog代码位于 ./raft/log.go
unstable代码位于 ./raft/log_unstable.go
storage代码位于 ./raft/storage.go
// raftLog 算法层管理预写日志的模块
type raftLog struct {
// storage contains all stable entries since the last snapshot.
//预写日志的产生需要经历一个未持久化(内存)-》 已持久化(磁盘)的过程 前者在算法层的raftLog.unstable完成,后者在应用层完成
// 用于保存来自最后一次snapshot之后提交的日志数据
// 主要用于把预写日志写入磁盘
storage Storage
// unstable contains all unstable entries and snapshot.
// they will be saved into storage.
// 包含所有未同步的预写日志和snapshot,他们最终都会被保存到storage
// 主要用于把预写日志写入内存
unstable unstable
// committed is the highest log position that is known to be in
// stable storage on a quorum of nodes.
// 已提交的日志索引(被多数派认可)
committed uint64
// applied is the highest log position that the application has
// been instructed to apply to its state machine.
// Invariant: applied <= committed
// 已经应用到状态机的日志索引(真正存储到状态机),applied <= committed,
applied uint64
logger Logger
}
预写日志产生时,需要经历一个未持久化(内存)-> 已持久化(磁盘)的过程,前者在raftLog.unstable完成,后者在应用层完成,并通过raftLog.storage为算法层提供已持久化预写日志的查询能力。
1.storage:持久化日志存储接口,提供日志的查询能力,storage是一个抽象接口,可由用户自定义实现
// Storage 持久化日志储存接口,可由用户实现,提供了日志查询能力
type Storage interface {
// InitialState returns the saved HardState and ConfState information.
InitialState() (pb.HardState, pb.ConfState, error)
// Entries returns a slice of log entries in the range [lo,hi).
// MaxSize limits the total size of the log entries returned, but
// Entries returns at least one entry if any.
// 返回预写日志索引范围在 lo <= index < hi 的maxSize(最小为一个)个预写日志
Entries(lo, hi, maxSize uint64) ([]pb.Entry, error)
// Term returns the term of entry i, which must be in the range
// [FirstIndex()-1, LastIndex()]. The term of the entry before
// FirstIndex is retained for matching purposes even though the
// rest of that entry may not be available.
// 传入预写日志的索引返回相对应的任期
Term(i uint64) (uint64, error)
// LastIndex returns the index of the last entry in the log.
// 返回最后一条预写日志的索引
LastIndex() (uint64, error)
// FirstIndex returns the index of the first log entry that is
// possibly available via Entries (older entries have been incorporated
// into the latest Snapshot; if storage only contains the dummy entry the
// first log entry is not available).
// 返回第一条预写日志的索引
FirstIndex() (uint64, error)
// Snapshot returns the most recent snapshot.
// If snapshot is temporarily unavailable, it should return ErrSnapshotTemporarilyUnavailable,
// so raft state machine could know that Storage needs some time to prepare
// snapshot and call Snapshot later.
Snapshot() (pb.Snapshot, error)
}
2.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
// 用于保存预写日志数组中的还未持久化的预写日志的起始索引
// *保存快照和日志数组的分界线 感觉是stable和unstable的分界线
offset uint64
logger Logger
}
3.committed:已提交的日志索引
4.applied:已应用的日志索引
4.4 Ready
代码位于 ./raft/node.go
// 是算法层和应用层交互的数据格式。每当算法层执行完一轮处理逻辑之后,会往一个channel(readyc)传入一个Ready结构体,其中封装了算法层处理好的结果
type Ready struct {
// The current volatile state of a Node.
// SoftState will be nil if there is no update.
// It is not required to consume or store SoftState.
// 软状态,可变,即为集群的Leader和当前节点的状态
*SoftState
// The current state of a Node to be saved to stable storage BEFORE
// Messages are sent.
// HardState will be equal to empty state if there is no update.
// 硬状态需要被保存,包括:节点当前的Term、Vote、Commit
pb.HardState
// ReadStates can be used for node to serve linearizable read requests locally
// when its applied index is greater than the index in ReadState.
// Note that the readState will be returned when raft receives msgReadIndex.
// The returned is only valid for the request that requested to read.
ReadStates []ReadState
// Entries specifies entries to be saved to stable storage BEFORE
// Messages are sent.
// 需要在消息发送之前被写入到持久化存储中的entries数组数据 待持久化的的预写日志
Entries []pb.Entry
// Snapshot specifies the snapshot to be saved to stable storage.
Snapshot pb.Snapshot
// CommittedEntries specifies entries to be committed to a
// store/state-machine. These have previously been committed to stable
// store.
// 本轮算法层已经提交的预写日志,需要传输到应用层,由应用层应用到状态机 已提交待应用的预写日志
CommittedEntries []pb.Entry
// Messages specifies outbound messages to be sent AFTER Entries are
// committed to stable storage.
// If it contains a MsgSnap message, the application MUST report back to raft
// when the snapshot has been received or has failed by calling ReportSnapshot.
// 本轮算法层需要发送的消息,由应用层调用网络通信模块发送
Messages []pb.Message
}
Ready主要是算法层跟应用层交互的数据格式,每当算法层处理完一轮逻辑处理,会往readyc channel返回一个Ready结构体,其中封装好算法层处理好的结果,应用层执行就行。
1.SoftState:raft节点的软状态,包含节点的Leader和当前节点的角色状态,因为都容易发生改变且通过通信可重新恢复的信息,因此无需进行持久化存储
// SoftState provides state that is useful for logging and debugging.
// The state is volatile and does not need to be persisted to the WAL.
type SoftState struct {
Lead uint64 // must use atomic operations to access; keep 64-bit aligned.
RaftState StateType
}
// StateType represents the role of a node in a cluster.
type StateType uint64
// Possible values for StateType.
// 节点身份
const (
StateFollower StateType = iota
StateCandidate
StateLeader
StatePreCandidate
numStates
)
2.HardState:raft节点的当前任期、投票归属和已提交日志索引,这些信息都需要持久化存储,节点宕机后重启亦可恢复如初
// HardState 硬状态
type HardState struct {
// 当前节点的任期
Term uint64 `protobuf:"varint,1,opt,name=term" json:"term"`
// 投票归属
Vote uint64 `protobuf:"varint,2,opt,name=vote" json:"vote"`
// 当前节点的已提交日志索引
Commit uint64 `protobuf:"varint,3,opt,name=commit" json:"commit"`
XXX_unrecognized []byte `json:"-"`
}
3.Entries:待应用层持久化的预写日志
4.CommittedEntries:算法层已提交的预写日志,需要传输给应用层,应用层将其应用到数据状态机
5.Message:算法层需要发送的消息,由应用层调用通信模块发送
4.5 Node Interface
代码位于 ./raft/node.go
Node是算法层中raft节点的抽象,也是应用层与算法层交互的唯一入口,应用层持有的Node作为算法层raft节点的引用,通过调用Node接口的几个api,完成于算法层的channel通信(先留个印象,后续走流程一个个介绍)
// Node represents a node in a raft cluster. 算法层抽象接口
type Node interface {
// Tick increments the internal logical clock for the Node by a single tick. Election
// timeouts and heartbeat timeouts are in units of ticks.
// 定时驱动刻度, 此刻度为心跳检测和选举超时的基本单位 心跳检测是一个tick 100ms election timeout默认是10tick + 随机扰动数值(范围在election timeout <= time < 2 * election timeout -1)
Tick()
// Campaign causes the Node to transition to candidate state and start campaigning to become leader.
// 竞选驱动 想从candidate->leader
Campaign(ctx context.Context) error
// Propose proposes that data be appended to the log.
// 发送写请求给算法层 添加预写日志
Propose(ctx context.Context, data []byte) error
// ProposeConfChange proposes config change.
// At most one ConfChange can be in the process of going through consensus.
// Application needs to call ApplyConfChange when applying EntryConfChange type entry.
// 配置变更请求
ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
// Step advances the state machine using the given message. ctx.Err() will be returned, if any.
// Step使用给定的消息推动状态机(此消息不能为MsgHub或者MsgBeat,这两个消息为Tick驱动) 可拔插,不同的节点状态有不同的函数,leaderStep candidateStep followStep
Step(ctx context.Context, msg pb.Message) error
// Ready returns a channel that returns the current point-in-time state.
// Users of the Node must call Advance after retrieving the state returned by Ready.
//
// NOTE: No committed entries from the next Ready may be applied until all committed entries
// and snapshots from the previous one have finished.
// 应用层接收算法层的处理请求
// 接收到Ready后需要调用Advance()通知算法层,应用层已经处理完Ready返回的逻辑
Ready() <-chan Ready
// Advance notifies the Node that the application has saved progress up to the last Ready.
// It prepares the node to return the next available Ready.
//
// The application should generally call Advance after it applies the entries in last Ready.
//
// However, as an optimization, the application may call Advance while it is applying the
// commands. For example. when the last Ready contains a snapshot, the application might take
// a long time to apply the snapshot data. To continue receiving Ready without blocking raft
// progress, it can call Advance before finishing applying the last ready.
// 应用层响应算法层已经处理完成,可以进入下轮循环调度
Advance()
// ApplyConfChange applies config change to the local node.
// Returns an opaque ConfState protobuf which must be recorded
// in snapshots. Will never return nil; it returns a pointer only
// to match MemoryStorage.Compact.
// 应用变更的配置到本地节点
ApplyConfChange(cc pb.ConfChange) *pb.ConfState
// TransferLeadership attempts to transfer leadership to the given transferee.
TransferLeadership(ctx context.Context, lead, transferee uint64)
// ReadIndex request a read state. The read state will be set in the ready.
// Read state has a read index. Once the application advances further than the read
// index, any linearizable read requests issued before the read request can be
// processed safely. The read state will have the same rctx attached.
// 应用层向算法层发起读请求
ReadIndex(ctx context.Context, rctx []byte) error
// Status returns the current status of the raft state machine.
Status() Status
// ReportUnreachable reports the given node is not reachable for the last send.
ReportUnreachable(id uint64)
// ReportSnapshot reports the status of the sent snapshot.
ReportSnapshot(id uint64, status SnapshotStatus)
// Stop performs any necessary termination of the Node.
Stop()
}
4.6 readOnly
代码位于 ./raft/read_only.go
// 挂起的读请求队列,由raft持有
type readOnly struct {
option ReadOnlyOption
// 一系列还未处理的读请求 存放待定的读请求 key为entry的数据
pendingReadIndex map[string]*readIndexStatus
// 有顺序的存放读请求的id的队列
readIndexQueue []string
}
// 一笔读请求的状态信息
type readIndexStatus struct {
// 保存原始的ReadIndex()请求消息,消息类型为MsgReadIndex
req pb.Message
// 保存收到ReadIndex()请求时leader已提交的预写日志索引
index uint64
// 保留了有哪一些节点进行应答,从此处判断是否有半数节点应答
acks map[uint64]struct{}
}
4.7 Progress
代码位于 ./raft/progress.go
// Progress represents a follower’s progress in the view of the leader. Leader maintains
// progresses of all followers, and sends entries to the follower based on its progress.
type Progress struct {
// Match 已经跟leader同步的最新一条预写日志索引 Next是leader将要发送给该follower的下一条日志条目的索引
Match, Next uint64
State ProgressStateType
// Paused is used in ProgressStateProbe.
// When Paused is true, raft should pause sending replication message to this peer.
Paused bool
PendingSnapshot uint64
// RecentActive is true if the progress is recently active. Receiving any messages
// from the corresponding follower indicates the progress is active.
// RecentActive can be reset to false after an election timeout.
RecentActive bool
ins *inflights
}
4.8 raft
代码位于 ./raft/raft.go
// raft时raft共识算法的抽象,几乎囊括了一个raft节点正常运行时所必备的全部信息
type raft struct {
// 节点id
id uint64
// 节点的当前任期
Term uint64
// 节点当前任期投票给了那个节点
Vote uint64
readStates []ReadState
// the log
// 管理预写日志的模块
raftLog *raftLog
maxInflight int
maxMsgSize uint64
// 记录各节点的预写日志同步记录
prs map[uint64]*Progress
// 当前节点的状态 leader candidate follow PreCandidate
state StateType
// 存放哪一些节点给本节点投票 candidate使用
votes map[uint64]bool
// 当前节点需要发送的消息 应用层接收算法层传递的Ready结构体然后发送
msgs []pb.Message
// the leader id
lead uint64
// leadTransferee is id of the leader transfer target when its value is not zero.
// Follow the procedure defined in raft thesis 3.10.
leadTransferee uint64
// New configuration is ignored if there exists unapplied configuration.
// 标识当前节点是否有还没有被应用的配置变更信息
pendingConf bool
// 挂起的读请求队列 --等待leader认证身份后才响应给客户端ack
readOnly *readOnly
// number of ticks since it reached last electionTimeout when it is leader
// or candidate.
// number of ticks since it reached last electionTimeout or received a
// valid message from current leader when it is a follower.
// 计算选举计时 单位为tick
electionElapsed int
// number of ticks since it reached last heartbeatTimeout.
// only leader keeps heartbeatElapsed.
// 计算心跳计时 单位为tick
heartbeatElapsed int
// 是否得到多数派认可
checkQuorum bool
// 是否发起预投票
preVote bool
// 心跳超时时间 单位为tick
heartbeatTimeout int
// 选举超时时间 单位为tick
electionTimeout int
// randomizedElectionTimeout is a random number between
// [electiontimeout, 2 * electiontimeout - 1]. It gets reset
// when raft changes its state to follower or candidate.
// 新增随机扰动值,目的是为了让每一个节点的选举时间不一样,防止同一时间多节点竞选,范围在[electiontimeout, 2 * electiontimeout - 1],每次切换身份的时候重置此值
randomizedElectionTimeout int
// 节点的定时处理函数,不同身份的处理函数不同,leader时广播心跳的tickHeartbeat函数,follower和candidate时发起竞选的tickElection函数
tick func()
// 状态机处理函数,不同身份的状态机函数不同,分为stepLeader stepCandidate stepFollower
step stepFunc
logger Logger
}
4.9 raftNode
代码位于 ./contrib/raftexample/raft.go
// 应用层实现类
type raftNode struct {
// 用于接收客户端发送的写请求
proposeC <-chan string // proposed messages (k,v)
// 用于接收客户端的配置变更请求
confChangeC <-chan raftpb.ConfChange // proposed cluster config changes
// 接收已提交的预写日志 然后将已提交的预写日志应用到数据状态机
commitC chan<- *string // entries committed to log (k,v)
errorC chan<- error // errors from raft session
// raft节点id
id int // client ID for raft session
// 同一集群内其他raft节点的标识信息
peers []string // raft peer URLs
join bool // node is joining an existing cluster
waldir string // path to WAL directory
snapdir string // path to snapshot directory
getSnapshot func() ([]byte, error)
lastIndex uint64 // index of log at start
confState raftpb.ConfState
snapshotIndex uint64
// 本节点已经应用到状态机的日志索引
appliedIndex uint64
// raft backing for the commit/error channel
// 算法层入口
node raft.Node
// 持久化预写日志存储模块
raftStorage *raft.MemoryStorage
wal *wal.WAL
snapshotter *snap.Snapshotter
snapshotterReady chan *snap.Snapshotter // signals when snapshotter is ready
snapCount uint64
// 集群通信模块
transport *rafthttp.Transport
stopc chan struct{} // signals proposal channel closed
httpstopc chan struct{} // signals http server to shutdown
httpdonec chan struct{} // signals http server shutdown complete
}
4.10 node
代码位于 ./raft/node.go
type node struct {
// 用于接收来自应用层的客户端写请求消息
propc chan pb.Message
// 用于接收来自应用层的别的节点的消息
recvc chan pb.Message
// 用于接收来自应用层的客户端配置变更消息
confc chan pb.ConfChange
confstatec chan pb.ConfState
// 用于传递给应用层消息Ready
readyc chan Ready
// 用于接收来自应用层的处理响应(代表应用层已经处理完ready)
advancec chan struct{}
// 用于接收来自应用层的定时tick驱动
tickc chan struct{}
done chan struct{}
stop chan struct{}
status chan chan Status
logger Logger
}
4.11 kvStore
代码位于 ./contrib/raftexample/kvstore.go
// 键值对存储模块
type kvstore struct {
// 负责向raftNode发送来自客户端的写数据请求
proposeC chan<- string // channel for proposing updates
// 读写锁,保证数据并发安全
mu sync.RWMutex
// 键值对存储介质 可以理解为这个就是数据状态机
kvStore map[string]string // current committed key-value pairs
snapshotter *snap.Snapshotter
}
kvstore是一个半成品的键值对存储模块,模拟etcd存储系统于raft节点间的交互模式。
1.proposeC:于raftNode.proposeC是同一个channel,用于接收到客户端写数据请求时发送给raftNode
2.kvstore:键值对存储介质,简单的理解为这个就是所谓的数据状态机
// 这里的commitC和raftNode的commitC是同一个 proposeC也是同一个
func newKVStore(snapshotter *snap.Snapshotter, proposeC chan<- string, commitC <-chan *string, errorC <-chan error) *kvstore {
s := &kvstore{proposeC: proposeC, kvStore: make(map[string]string), snapshotter: snapshotter}
// replay log into key-value map
s.readCommits(commitC, errorC)
// read commits from raft into kvStore map until error
// 开启协程监听raftNode的commitC,将数据应用到数据状态机
go s.readCommits(commitC, errorC)
return s
}
kvstore模块启动时,会注入一个commitC channel,和raftNode.commitC是同一个channel
kvstore会持续监听这个commitC channel,等待raftNode提交的日志数据,然后将其应用到数据状态机
// readCommits 这里的commitC和raftNode的commitC是同一个,也就是说,这个方法会一直监听该channel(commitC),获取到raftNode提交的预写日志数据,然后将数据应用到数据状态机
func (s *kvstore) readCommits(commitC <-chan *string, errorC <-chan error) {
for data := range commitC {
if data == nil {
// done replaying log; new data incoming
// OR signaled to load snapshot
snapshot, err := s.snapshotter.Load()
if err == snap.ErrNoSnapshot {
return
}
if err != nil && err != snap.ErrNoSnapshot {
log.Panic(err)
}
log.Printf("loading snapshot at term %d and index %d", snapshot.Metadata.Term, snapshot.Metadata.Index)
if err := s.recoverFromSnapshot(snapshot.Data); err != nil {
log.Panic(err)
}
continue
}
var dataKv kv
dec := gob.NewDecoder(bytes.NewBufferString(*data))
if err := dec.Decode(&dataKv); err != nil {
log.Fatalf("raftexample: could not decode message (%v)", err)
}
// 重点在这
s.mu.Lock()
s.kvStore[dataKv.Key] = dataKv.Val
s.mu.Unlock()
}
if err, ok := <-errorC; ok {
log.Fatal(err)
}
}
此外,还提供了Propose方法给客户端发送写请求,kvstore通过proposeC将请求发送给raftNode处理
func (s *kvstore) Propose(k string, v string) {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(kv{k, v}); err != nil {
log.Fatal(err)
}
s.proposeC <- string(buf.Bytes())
}
5 客户端与应用层交互流程
代码位于 ./contrib/raftexample/main.go
func main() {
cluster := flag.String("cluster", "http://127.0.0.1:9021", "comma separated cluster peers")
id := flag.Int("id", 1, "node ID")
kvport := flag.Int("port", 9121, "key-value server port")
join := flag.Bool("join", false, "join an existing cluster")
flag.Parse()
proposeC := make(chan string)
defer close(proposeC)
confChangeC := make(chan raftpb.ConfChange)
defer close(confChangeC)
// raft provides a commit stream for the proposals from the http api
var kvs *kvstore
getSnapshot := func() ([]byte, error) { return kvs.getSnapshot() }
commitC, errorC, snapshotterReady := newRaftNode(*id, strings.Split(*cluster, ","), *join, getSnapshot, proposeC, confChangeC)
kvs = newKVStore(<-snapshotterReady, proposeC, commitC, errorC)
// the key-value http handler will propose updates to raft
serveHttpKVAPI(kvs, *kvport, confChangeC, errorC)
}
这里主要是验证上面说的,kvstore和raftNode的proposeC和commitC是同一个,进入serveHttpKVAPI()方法如下
type httpKVAPI struct {
store *kvstore
confChangeC chan<- raftpb.ConfChange
}
func serveHttpKVAPI(kv *kvstore, port int, confChangeC chan<- raftpb.ConfChange, errorC <-chan error) {
srv := http.Server{
Addr: ":" + strconv.Itoa(port),
Handler: &httpKVAPI{
store: kv,
confChangeC: confChangeC,
},
}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
// exit when raft goes down
if err, ok := <-errorC; ok {
log.Fatal(err)
}
}
启动一个http服务,监听客户端请求。重点在下面
// 接收用户发送的请求
// 如果收到PUT请求,则视为一个写数据请求,调用kvstore.Propose api进行转发处理(实际上就是把要写的数据放到kvstore的proposeC channel,然后再转发给应用层的proposeC)
// 如果收到GET请求,则视为一个读数据请求,调用kvstore.Lookup api直接读取数据状态机的数据(已经应用到数据状态的数据)
// 如果收到POST请求,则视为一个添加配置请求
// 如果收到DELETE请求,则视为一个删除配置请求
func (h *httpKVAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := r.RequestURI
switch {
case r.Method == "PUT":
v, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read on PUT (%v)\n", err)
http.Error(w, "Failed on PUT", http.StatusBadRequest)
return
}
h.store.Propose(key, string(v))
// Optimistic-- no waiting for ack from raft. Value is not yet
// committed so a subsequent GET on the key may return old value
w.WriteHeader(http.StatusNoContent)
case r.Method == "GET":
if v, ok := h.store.Lookup(key); ok {
w.Write([]byte(v))
} else {
http.Error(w, "Failed to GET", http.StatusNotFound)
}
case r.Method == "POST":
url, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read on POST (%v)\n", err)
http.Error(w, "Failed on POST", http.StatusBadRequest)
return
}
nodeId, err := strconv.ParseUint(key[1:], 0, 64)
if err != nil {
log.Printf("Failed to convert ID for conf change (%v)\n", err)
http.Error(w, "Failed on POST", http.StatusBadRequest)
return
}
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: nodeId,
Context: url,
}
h.confChangeC <- cc
// As above, optimistic that raft will apply the conf change
w.WriteHeader(http.StatusNoContent)
case r.Method == "DELETE":
nodeId, err := strconv.ParseUint(key[1:], 0, 64)
if err != nil {
log.Printf("Failed to convert ID for conf change (%v)\n", err)
http.Error(w, "Failed on DELETE", http.StatusBadRequest)
return
}
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: nodeId,
}
h.confChangeC <- cc
// As above, optimistic that raft will apply the conf change
w.WriteHeader(http.StatusNoContent)
default:
w.Header().Set("Allow", "PUT")
w.Header().Add("Allow", "GET")
w.Header().Add("Allow", "POST")
w.Header().Add("Allow", "DELETE")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
如果请求时PUT,那就调用propose(),给raftNode发送一个写请求
func (s *kvstore) Propose(k string, v string) {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(kv{k, v}); err != nil {
log.Fatal(err)
}
s.proposeC <- string(buf.Bytes())
}
如果请求是POST,会视为添加节点的配置变更请求,如果是DELETE请求,会视为删除节点的集群配置变更请求.
6 应用层和算法层的交互流程
从5的main方法进入,首先是newRaftNode(),主要就是创建raftNode对象 (主要先看到底使用啥交流,具体细节先不看,后续慢慢展开,先有个印象这两层怎么串联起来)
func newRaftNode(id int, peers []string, join bool, getSnapshot func() ([]byte, error), proposeC <-chan string,
confChangeC <-chan raftpb.ConfChange) (<-chan *string, <-chan error, <-chan *snap.Snapshotter) {
commitC := make(chan *string)
errorC := make(chan error)
rc := &raftNode{
// 接收客户端的写请求
proposeC: proposeC,
// 接收客户端的配置变更请求
confChangeC: confChangeC,
// 已经提交了准备给kvstove应用的日志提交channel
commitC: commitC,
errorC: errorC,
// 当前节点的唯一标识
id: id,
peers: peers,
join: join,
waldir: fmt.Sprintf("raftexample-%d", id),
snapdir: fmt.Sprintf("raftexample-%d-snap", id),
getSnapshot: getSnapshot,
snapCount: defaultSnapCount,
stopc: make(chan struct{}),
httpstopc: make(chan struct{}),
httpdonec: make(chan struct{}),
snapshotterReady: make(chan *snap.Snapshotter, 1),
// rest of structure populated after WAL replay
}
// 启动raft 启动算法层node和应用层raftNode
go rc.startRaft()
return commitC, errorC, rc.snapshotterReady
}
然后就是开启了一个goroutine执行了rc.startRaft(),z主要就是初始化raft的一些配置信息还有transport。
// startRaft 启动应用和算法层
func (rc *raftNode) startRaft() {
if !fileutil.Exist(rc.snapdir) {
if err := os.Mkdir(rc.snapdir, 0750); err != nil {
log.Fatalf("raftexample: cannot create dir for snapshot (%v)", err)
}
}
rc.snapshotter = snap.New(rc.snapdir)
rc.snapshotterReady <- rc.snapshotter
oldwal := wal.Exist(rc.waldir)
rc.wal = rc.replayWAL()
// raft的peers存放的是每个节点的地址
// 获取集群内其他节点的信息包括自己,rpeers编号从0开始
rpeers := make([]raft.Peer, len(rc.peers))
for i := range rpeers {
rpeers[i] = raft.Peer{ID: uint64(i + 1)}
}
// 创建raft配置
c := &raft.Config{
ID: uint64(rc.id),
ElectionTick: 10,
HeartbeatTick: 1,
Storage: rc.raftStorage,
MaxSizePerMsg: 1024 * 1024,
MaxInflightMsgs: 256,
}
if oldwal {
rc.node = raft.RestartNode(c)
} else {
startPeers := rpeers
if rc.join {
startPeers = nil
}
// 启动算法层
rc.node = raft.StartNode(c, startPeers)
}
ss := &stats.ServerStats{}
ss.Initialize()
rc.transport = &rafthttp.Transport{
ID: types.ID(rc.id),
ClusterID: 0x1000,
Raft: rc,
ServerStats: ss,
LeaderStats: stats.NewLeaderStats(strconv.Itoa(rc.id)),
ErrorC: make(chan error),
}
// 启动通信模块 开始
rc.transport.Start()
for i := range rc.peers {
if i+1 != rc.id {
rc.transport.AddPeer(types.ID(i+1), []string{rc.peers[i]})
}
}
go rc.serveRaft()
// 启动通信模块 结束
// 异步开启raftNode的主循环,用于跟算法层的goroutine建立持续通信的关系 启动应用层
go rc.serveChannels()
}
第一个重点 是这个raft.StartNode(c, startPeers)里面的run()
func StartNode(c *Config, peers []Peer) Node {
// 创建一个raft
r := newRaft(c)
// become the follower at term 1 and apply initial configuration
// entries of term 1
// 初次以follower启动,term为1,没有leader
r.becomeFollower(1, None)
// 将集群中其他节点都封装成添加节点的配置变更信息,添加到非持久化预写日志当中(也就是内存,算法层负责)
for _, peer := range peers {
cc := pb.ConfChange{Type: pb.ConfChangeAddNode, NodeID: peer.ID, Context: peer.Context}
d, err := cc.Marshal()
if err != nil {
panic("unexpected marshal error")
}
e := pb.Entry{Type: pb.EntryConfChange, Term: 1, Index: r.raftLog.lastIndex() + 1, Data: d}
r.raftLog.append(e)
}
// 启动之初的配置变更日志视为已提交
r.raftLog.committed = r.raftLog.lastIndex()
// 把每个节点的日志同步进度添加到raft的map prs中
for _, peer := range peers {
r.addNode(peer.ID)
}
// 初始化node
n := newNode()
n.logger = c.Logger
// 异步调用node.run方法,启动算法层raft节点goroutine,正是这一个goroutine持续与应用层进行通信交互
go n.run(r)
return &n
}
异步开启一个goroutine执行run(),主要用处如下
(1)propc channel监听来自应用层发送过来的写请求
(2) 接收其他节点的消息 ---> m := <-n.recvc
(3)接收应用层传递的配置变更信息 ---> cc := <-n.confc
(4)接受应用层的定时驱动 <-n.tickc
(5)处理完应用层的请求,封装好处理结果ready发送给应用层 readyc <- rd
(6)接收应用层处理完readyc的响应
// 启动算法层,持续与应用层进行通信交互
func (n *node) run(r *raft) {
var propc chan pb.Message
var readyc chan Ready
var advancec chan struct{}
var prevLastUnstablei, prevLastUnstablet uint64
var havePrevLastUnstablei bool
var prevSnapi uint64
var rd Ready
lead := None
prevSoftSt := r.softState()
prevHardSt := emptyState
// 一个请求可能需要执行多次循环才能完成
// 比如写请求,第一次循环先到 <-propc,执行Step函数,然后根据不同的身份执行对应的step函数(持久化预写日志到内存raft.unstable,封装发给集群内除了leader的其他节点要添加预写日志的消息到r.msgs)
// 然后到下一轮循环NewReady,才执行发送ready到应用层readyc <- rd,设置advance的值为node.addvance,并且清空已经发送的r.msgs,保留raft的软状态(preSftSt)和硬状态(PreHardSt)给下一次循环的advance使用,最后等待应用层处理响应
// 然后到下一轮循环接收到应用层的处理响应<- advance,
for {
if advancec != nil {
// advance channel不为空,说明还在等应用层调用advance接口通知已经处理完毕ready消息
readyc = nil
} else {
rd = newReady(r, prevSoftSt, prevHardSt)
if rd.containsUpdates() {
// 如果此次ready消息包含更新,那么ready channel就不为空
readyc = n.readyc
} else {
// 否则为空
readyc = nil
}
}
if lead != r.lead {
// 初始化lead和proc
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 {
// TODO: maybe buffer the config propose if there exists one (the way
// described in raft dissertation)
// Currently it is dropped in Step silently.
case m := <-propc:
// 处理本地(本节点)收到的客户端的写请求
m.From = r.id
r.Step(m)
case m := <-n.recvc:
// 处理其他节点发送过来的请求
// filter out response message from unknown From.
if _, ok := r.prs[m.From]; ok || !IsResponseMsg(m.Type) {
// 需要保证节点在集群中或者不是应答类消息的情况下才进行处理
r.Step(m) // raft never returns an error
}
case cc := <-n.confc:
// 接收到配置发生变化的消息
if cc.NodeID == None {
// NodeID为空的情况,只需要返回当前集群内的全部节点就好
r.resetPendingConf()
select {
case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
case <-n.done:
}
break
}
switch cc.Type {
case pb.ConfChangeAddNode:
// 添加节点
r.addNode(cc.NodeID)
case pb.ConfChangeRemoveNode:
// 如果移除的是本节点,停止本节点接收应用层请求
// block incoming proposal when local node is
// removed
if cc.NodeID == r.id {
propc = nil
}
r.removeNode(cc.NodeID)
case pb.ConfChangeUpdateNode:
// 更新节点信息
r.resetPendingConf()
default:
panic("unexpected conf type")
}
select {
case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
case <-n.done:
}
case <-n.tickc:
// 接收到应用层定时驱动,会根据raft节点不同的身份,使用对应的tic函数处理
r.tick()
case readyc <- rd:
// 通过ready channel写入需要应用层处理的ready数据
// 以下先把ready的值保存下来,等待下一次循环使用(也就是给下面的advance那里使用),或者当成advance调用完毕之后用于修改raftLog
if rd.SoftState != nil {
prevSoftSt = rd.SoftState
}
if len(rd.Entries) > 0 {
prevLastUnstablei = rd.Entries[len(rd.Entries)-1].Index
prevLastUnstablet = rd.Entries[len(rd.Entries)-1].Term
havePrevLastUnstablei = true
}
if !IsEmptyHardState(rd.HardState) {
prevHardSt = rd.HardState
}
if !IsEmptySnap(rd.Snapshot) {
prevSnapi = rd.Snapshot.Metadata.Index
}
r.msgs = nil
r.readStates = nil
// 打开advance通道,等待接收应用层处理完ready
advancec = n.advancec
case <-advancec:
if prevHardSt.Commit != 0 {
// 将上一笔(也就是上次循环保留的ready)committed的消息applied
r.raftLog.appliedTo(prevHardSt.Commit)
}
if havePrevLastUnstablei {
// 把prevLastUnstablei,prevLastUnstablet对应在raft.raftLog.unstable前面的数据删除(视为已经稳定存储) 清理unstable中已经被raftNode持久化的entries
r.raftLog.stableTo(prevLastUnstablei, prevLastUnstablet)
havePrevLastUnstablei = false
}
r.raftLog.stableSnapTo(prevSnapi)
advancec = nil
case c := <-n.status:
c <- getStatus(r)
case <-n.stop:
close(n.done)
return
}
}
}
第二个重点是这个rc.serveChannels(),具体细节先不展开(后续流程在深入)
1.首先就是启动了一个goroutine用于监听客户端的写请求和配置变更请求,然后调用node的sdk通知到node
2.其次,使用for死循环
(1)定期执行node.tick()驱动算法层node执行定时函数
(2)等待算法层处理完请求返回响应(case rd := <-rc.node.Ready():)
(3)然后调用node.Advance()通知算法层本次Ready()请求已经处理完成,可以开始处理下一轮请求Ready()
// 异步开启 raftNode 的主循环,用于与算法层的 goroutine 建立持续通信的关系.
func (rc *raftNode) serveChannels() {
snap, err := rc.raftStorage.Snapshot()
if err != nil {
panic(err)
}
rc.confState = snap.Metadata.ConfState
rc.snapshotIndex = snap.Metadata.Index
rc.appliedIndex = snap.Metadata.Index
defer rc.wal.Close()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
// send proposals over raft 协程跟客户端发起写请求和配置变更有关 对外(应用层和客户端打交道)
// 开启一个异步协程持续监听raft的两个channel proposeC和confChangeC,从而接收来自客户端的写数据和配置变更请求,然后调用Node接口的api发送给算法层
go func() {
var confChangeCount uint64 = 0
for rc.proposeC != nil && rc.confChangeC != nil {
select {
case prop, ok := <-rc.proposeC:
// 接收客户端的写请求
if !ok {
rc.proposeC = nil
} else {
// blocks until accepted by raft state machine
// 调用node的Propose函数把写请求发送给算法层node处理
rc.node.Propose(context.TODO(), []byte(prop))
}
case cc, ok := <-rc.confChangeC:
if !ok {
rc.confChangeC = nil
} else {
confChangeCount += 1
cc.ID = confChangeCount
rc.node.ProposeConfChange(context.TODO(), cc)
}
}
}
// client closed channel; shutdown raft if not already
close(rc.stopc)
}()
// event loop on raft state machine updates
// 对内(应用层和算法层打交道)
for {
select {
case <-ticker.C:
// tick默认为100ms,定时调用node.Tick()方法驱动算法层执行定时函数
rc.node.Tick()
// store raft entries to wal, then publish over commit channel
// raftNode通过node.Ready()方法接收到算法层的处理结果后
// 1.需要将待持久化的预写日志(Ready.Entries)进行持久化 storage,
// 2.然后调用通信模块执行消息(Ready.Messages)发送,
// 3.需要和数据状态机交互,应用算法层已确认提交的预写日志
// 4.处理完上述步骤后,raftNode会调用Node.Advance()方法对算法层进行响应(告诉算法层,我已经处理完你要我处理的Ready)
case rd := <-rc.node.Ready():
rc.wal.Save(rd.HardState, rd.Entries)
if !raft.IsEmptySnap(rd.Snapshot) {
rc.saveSnap(rd.Snapshot)
rc.raftStorage.ApplySnapshot(rd.Snapshot)
rc.publishSnapshot(rd.Snapshot)
}
// 1
rc.raftStorage.Append(rd.Entries)
// 2
rc.transport.Send(rd.Messages)
// 3
if ok := rc.publishEntries(rc.entriesToApply(rd.CommittedEntries)); !ok {
rc.stop()
return
}
rc.maybeTriggerSnapshot()
// 4
rc.node.Advance()
case err := <-rc.transport.ErrorC:
rc.writeError(err)
return
case <-rc.stopc:
rc.stop()
return
}
}
}
6.1 写操作的交互流程图
7 写流程(重点)
7.1 整体流程
raft节点处理写请求需要经历一个两阶段提交的流程,分为以下三部分:
1.当前节点为Leader,应用层调用Propose发送写请求给算法层,算法层添加日志到unstable,然后将同步日志请求封装到ready,通过Node.Ready方法让应用层消费,应用层记录日志到wal,存储日志到raftStorage,然后清除刚添加的日志unstable,然后调用通信模块转发消息msg给集群内的其他节点,最后调用Node.Advance开启下一轮交互;
2.当前节点为follower, 应用层收到来自Leader的同步日志请求,会调用Node.Step()将请求转发给算法层,算法层同步日志到unstable后,将同步结果封装到ready,通过Node.Ready()让应用层消费,然后应用层存储日志到raftStorage,然后清除刚添加的日志unstable,然后通过通信模块转发同步结果给Leader,最后调用Node.Advance开启下一轮交互;
3.当前节点为Leader,应用层收到follower的同步日志响应,调用Node.Step转发请求给算法层,算法层检测同步请求得到多数派同意,更新集群内的其他节点的已提交日志同步进度,然后更新自己的已提交日志同步进度,然后封装新的committed index到raedy,应用层通过Node.Ready()消费,然后应用层会将已提交的日志应用到数据状态机,广播已提交日志请求,最后调用Node.Advance开启下一轮交互;
7.2 流程细化
7.2.1 应用层发送写请求
首先,客户端向应用层提交写请求,raftNode调用Node.Propose函数将请求转发到算法层的propc channel,消息类型为MsgProp
// 异步开启 raftNode 的主循环,用于与算法层的 goroutine 建立持续通信的关系.
func (rc *raftNode) serveChannels() {
...
// send proposals over raft 协程跟客户端发起写请求和配置变更有关 对外(应用层和客户端打交道)
// 开启一个异步协程持续监听raft的两个channel proposeC和confChangeC,从而接收来自客户端的写数据和配置变更请求,然后调用Node接口的api发送给算法层
go func() {
var confChangeCount uint64 = 0
for rc.proposeC != nil && rc.confChangeC != nil {
select {
case prop, ok := <-rc.proposeC:
// 接收客户端的写请求
if !ok {
rc.proposeC = nil
} else {
// blocks until accepted by raft state machine
// 调用node的Propose函数把写请求发送给算法层node处理
rc.node.Propose(context.TODO(), []byte(prop))
}
case cc, ok := <-rc.confChangeC:
if !ok {
rc.confChangeC = nil
} else {
confChangeCount += 1
cc.ID = confChangeCount
rc.node.ProposeConfChange(context.TODO(), cc)
}
}
}
// client closed channel; shutdown raft if not already
close(rc.stopc)
}()
...
}
}
func (n *node) Propose(ctx context.Context, data []byte) error {
return n.step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Data: data}}})
}
func (n *node) step(ctx context.Context, m pb.Message) error {
ch := n.recvc
if m.Type == pb.MsgProp {
ch = n.propc
}
select {
case ch <- m:
return nil
case <-ctx.Done():
return ctx.Err()
case <-n.done:
return ErrStopped
}
}
7.2.2 不同身份接收到写请求处理
算法层接收到消息,进入状态机处理函数,然后根据节点不同的身份做不同的处理,也就是每个身份都有一个专属的状态处理函数
// 启动算法层,持续与应用层进行通信交互
func (n *node) run(r *raft) {
...
for {
select {
case m := <-propc:
// 处理本地(本节点)收到的客户端的写请求
m.From = r.id
r.Step(m)
}
...
}
}
// Step 通用状态机函数 算法层处理来自应用层的请求时,会先进入Strp,后续才会进入不同身份定制的状态机函数
func (r *raft) Step(m pb.Message) error {
...
switch m.Type {
...
default:
// 其他情况进入不同身份的状态机函数
r.step(r, m)
}
return nil
}
r.step()接下来就是进入专属的定制状态机函数
首先是follower的定制状态机函数stepFollower()处理写请求,如果收到写请求,会直接转发给Leader
// follower状态机处理函数
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
case pb.MsgProp:
// follower收到写请求会转发到leader
if r.lead == None {
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return
}
m.To = r.lead
r.send(m)
...
}
}
然后是candidate定制状态机函数stepCandidate()处理写请求.candidate的出现代表集群内一定没有leader(对此有疑问接着往下看就知道为什么我用的是“一定”了),又因为只有leader才能处理写请求,所以这里直接打印没有leader错误
func stepCandidate(r *raft, m pb.Message) {
...
switch m.Type {
case pb.MsgProp:
// leader才处理写请求,candidate忽略
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return
...
}
}
最后就是Leader定制状态机函数stepLeader()处理写请求,
func stepLeader(r *raft, m pb.Message) {
// These message types do not require any progress for m.From.
// 请求类消息
switch m.Type {
...
case pb.MsgProp:
...
// 添加预写日志到内存 raftLog.unstable 更新raft.prs中自己(leader)的Match和Next
r.appendEntry(m.Entries...)
// 广播添加append消息
r.bcastAppend()
return
...
}
}
appendEntry()的详细如下,主要就是添加日志到unstable
// 算法层添加预写日志到内存
func (r *raft) appendEntry(es ...pb.Entry) {
// 获取raftLog的最新一条日志的索引
li := r.raftLog.lastIndex()
// 给添加的预写日志设置term和index
for i := range es {
es[i].Term = r.Term
es[i].Index = li + 1 + uint64(i)
}
// 添加日志到raftLog.unstable
r.raftLog.append(es...)
// 更新自己的同步进度
r.prs[r.id].maybeUpdate(r.raftLog.lastIndex())
// Regardless of maybeCommit's return, our caller will call bcastAppend.
// 这里对于leader写请求的时候是多余的调用,因为此时只是leader自己写入了内存unstable,其他节点并未写入,所以leader不可能更新committed index
r.maybeCommit()
}
// 算法层添加日志到内存
func (l *raftLog) append(ents ...pb.Entry) uint64 {
...
l.unstable.truncateAndAppend(ents)
return l.lastIndex()
}
// 判断要添加预写日志是否需要回滚
func (u *unstable) truncateAndAppend(ents []pb.Entry) {
// 先获取要添加的预写日志的第一条数据的索引
after := ents[0].Index
switch {
case after == u.offset+uint64(len(u.entries)):
// 如果索引是下一条要添加数据,直接添加 简单地说就是要添加的日志就是下一条添加的位置就直接添加
// u.offset+uint64(len(u.entries)) 获取日志的最后一条数据索引+1也就是新增数据的索引
// 情况1
// after is the next index in the u.entries
// directly append
u.entries = append(u.entries, ents...)
case after <= u.offset:
// 如果要添加的第一条数据索引小于等于偏移量,需要回滚全部unstable日志,替换新的偏移量offset和entries 简单地说就是要添加的日志在offset之前,直接丢弃offset后面的enties脏数据
// 情况2
u.logger.Infof("replace the unstable entries from index %d", after)
// The log is being truncated to before our current offset
// portion, so set the offset and replace the entries
u.offset = after
u.entries = ents
default:
// 情况3
// truncate to after and copy to u.entries
// then append
// 到此处 u.offset < after < u.offset+uint(len(u.entries)) 也就是要回滚部分日志,重新拼接u.entries
// 简单来说就是要添加的日志在unstable的中间,也就是有一部分数据是脏数据需要剔除
u.logger.Infof("truncate the unstable entries before index %d", after)
// 前面相同部分
u.entries = append([]pb.Entry{}, u.slice(u.offset, after)...)
// 替换需要回滚的后面那部分日志
u.entries = append(u.entries, ents...)
}
}
truncateAndAppend()解析如下图(个人理解)
bcastAppend()主要遍历每个节点,sendAppend()主要就是把要同步的日志以及要同步的日志的上一条日志的索引和任期还有当前leader committed index这些信息封装到message(消息类型为MsgApp),然后调用send()把消息封装到ready.msg中,等后续算法层newReady的时候发送给应用层,让应用层转发。
func (r *raft) bcastAppend() {
for id := range r.prs {
if id == r.id {
continue
}
r.sendAppend(id)
}
}
func (r *raft) sendAppend(to uint64) {
pr := r.prs[to]
if pr.IsPaused() {
return
}
m := pb.Message{}
m.To = to
// to 接收方
// 从该节点的Next获取的上一条数据获取term pr是收消息的节点 获取要发送数据的上一条数据的term 也就是Match index
term, errt := r.raftLog.term(pr.Next - 1)
// 获取pr的Next(已提交)之后的entries(预写日志),数量总和不超过maxMsgSize follower带持久化的日志
ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)
if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
if !pr.RecentActive {
r.logger.Debugf("ignore sending snapshot to %x since it is not recently active", to)
return
}
m.Type = pb.MsgSnap
snapshot, err := r.raftLog.snapshot()
if err != nil {
if err == ErrSnapshotTemporarilyUnavailable {
r.logger.Debugf("%x failed to send snapshot to %x because snapshot is temporarily unavailable", r.id, to)
return
}
panic(err) // TODO(bdarnell)
}
if IsEmptySnap(snapshot) {
panic("need non-empty snapshot")
}
m.Snapshot = snapshot
sindex, sterm := snapshot.Metadata.Index, snapshot.Metadata.Term
r.logger.Debugf("%x [firstindex: %d, commit: %d] sent snapshot[index: %d, term: %d] to %x [%s]",
r.id, r.raftLog.firstIndex(), r.raftLog.committed, sindex, sterm, to, pr)
pr.becomeSnapshot(sindex)
r.logger.Debugf("%x paused sending replication messages to %x [%s]", r.id, to, pr)
} else {
// 消息类型为append
m.Type = pb.MsgApp
// 接收方节点的committed index 要添加的日志的上一条日志的索引index
m.Index = pr.Next - 1
// 接收方节点的committed term 要添加的日志的上一条日志的任期term
m.LogTerm = term
// 接收方节点待应用层持久化的预写日志 待持久化的日志
m.Entries = ents
// 当前leader的committed index
m.Commit = r.raftLog.committed
if n := len(m.Entries); n != 0 {
switch pr.State {
// optimistically increase the next when in ProgressStateReplicate
case ProgressStateReplicate:
last := m.Entries[n-1].Index
pr.optimisticUpdate(last)
pr.ins.add(last)
case ProgressStateProbe:
pr.pause()
default:
r.logger.Panicf("%x is sending append in unhandled state %s", r.id, pr.State)
}
}
}
r.send(m)
}
// 封装message到ready
func (r *raft) send(m pb.Message) {
// 对拉票、读、写请求之外的消息填充任期信息,并记录消息体到raft.msgs(待发送的消息)中
m.From = r.id
if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
if m.Term == 0 {
// PreVote RPCs are sent at a term other than our actual term, so the code
// that sends these messages is responsible for setting the term.
panic(fmt.Sprintf("term should be set when sending %s", m.Type))
}
} else {
if m.Term != 0 {
panic(fmt.Sprintf("term should not be set when sending %s (was %d)", m.Type, m.Term))
}
// do not attach term to MsgProp, MsgReadIndex
// proposals are a way to forward to the leader and
// should be treated as local message.
// MsgReadIndex is also forwarded to leader.
if m.Type != pb.MsgProp && m.Type != pb.MsgReadIndex {
m.Term = r.Term
}
}
r.msgs = append(r.msgs, m)
}
接着回到算法层的run(),上面是第一轮for循环的处理,主要就是为封装ready结构体准备的。下面是第二轮for循环。
第二层循环主要就是newReady(),封装好第一轮for循环处理结果到ready,然后发送给应用层处理
// 启动算法层,持续与应用层进行通信交互
func (n *node) run(r *raft) {
...
for {
if advancec != nil {
// advance channel不为空,说明还在等应用层调用advance接口通知已经处理完毕ready消息
readyc = nil
} else {
rd = newReady(r, prevSoftSt, prevHardSt)
if rd.containsUpdates() {
// 如果此次ready消息包含更新,那么ready channel就不为空
readyc = n.readyc
} else {
// 否则为空
readyc = nil
}
}
select {
...
case readyc <- rd:
// 通过ready channel写入需要应用层处理的ready数据
// 以下先把ready的值保存下来,等待下一次循环使用(也就是给下面的advance那里使用),或者当成advance调用完毕之后用于修改raftLog
if rd.SoftState != nil {
prevSoftSt = rd.SoftState
}
if len(rd.Entries) > 0 {
prevLastUnstablei = rd.Entries[len(rd.Entries)-1].Index
prevLastUnstablet = rd.Entries[len(rd.Entries)-1].Term
havePrevLastUnstablei = true
}
if !IsEmptyHardState(rd.HardState) {
prevHardSt = rd.HardState
}
if !IsEmptySnap(rd.Snapshot) {
prevSnapi = rd.Snapshot.Metadata.Index
}
r.msgs = nil
r.readStates = nil
// 打开advance通道,等待接收应用层处理完ready
advancec = n.advancec
...
}
}
7.2.3 应用层持久化日志和广播同步日志消息
应用层通过node.Ready()接收到算法层发送的消息,然后把写操作记录到日志wal(数据存入磁盘,方便宕机数据恢复),然后存储刚才算法层存放到unstable的那一条待应用层持久化的entry到raftStorage(注意一下这个raftStorage和unstable存储的日志此时都是没提交的,也就是可回滚,可能有脏数据,raftNode.Append和unstable.truncateAndAppend这两个方法分别是处理应用层和算法层日志回滚的问题),广播日志同步请求给集群内的其他节点
func (rc *raftNode) serveChannels() {
...
for {
select {
...
// raftNode通过node.Ready()方法接收到算法层的处理结果后
// 1.需要将待持久化的预写日志(Ready.Entries)进行持久化 storage,
// 2.然后调用通信模块执行消息(Ready.Messages)发送,
// 3.需要和数据状态机交互,应用算法层已确认提交的预写日志
// 4.处理完上述步骤后,raftNode会调用Node.Advance()方法对算法层进行响应(告诉算法层,我已经处理完你要我处理的Ready)
case rd := <-rc.node.Ready():
rc.wal.Save(rd.HardState, rd.Entries)
if !raft.IsEmptySnap(rd.Snapshot) {
rc.saveSnap(rd.Snapshot)
rc.raftStorage.ApplySnapshot(rd.Snapshot)
rc.publishSnapshot(rd.Snapshot)
}
// 1
rc.raftStorage.Append(rd.Entries)
// 2
rc.transport.Send(rd.Messages)
// 3
if ok := rc.publishEntries(rc.entriesToApply(rd.CommittedEntries)); !ok {
rc.stop()
return
}
rc.maybeTriggerSnapshot()
// 4
rc.node.Advance()
...
}
}
raftStorage.Append(),详细看图解(跟unstable.truncateAndAppend很像)
// 添加日志到raftStorage,主要确保添加的日志是可提交的,如果有脏数据,会回滚
func (ms *MemoryStorage) Append(entries []pb.Entry) error {
if len(entries) == 0 {
return nil
}
ms.Lock()
defer ms.Unlock()
first := ms.firstIndex()
last := entries[0].Index + uint64(len(entries)) - 1
// shortcut if there is no new entry.
if last < first {
return nil
}
// truncate compacted entries
if first > entries[0].Index {
// 情况1
entries = entries[first-entries[0].Index:]
}
offset := entries[0].Index - ms.ents[0].Index
switch {
case uint64(len(ms.ents)) > offset:
// 情况3
ms.ents = append([]pb.Entry{}, ms.ents[:offset]...)
ms.ents = append(ms.ents, entries...)
case uint64(len(ms.ents)) == offset:
// 情况2
ms.ents = append(ms.ents, entries...)
default:
raftLogger.Panicf("missing log entry [last: %d, append at: %d]",
ms.lastIndex(), entries[0].Index)
}
return nil
}
7.2.4 follower/candidate处理同步日志提议
follower和candidate主要是在定制的状态机函数处理同步日志提议
candidate收到任期大于等于他自己竞选任期的同步日志请求,会回退follower,然后尝试添加日志到unstable中;
func stepCandidate(r *raft, m pb.Message) {
...
switch m.Type {
...
case pb.MsgApp:
// 收到append消息,说明集群中已经有了leader,转换为follower
r.becomeFollower(r.Term, m.From)
// follower同步持久化日志
r.handleAppendEntries(m)
...
}
}
follower收到同步日志提议后,会将选举时间重置,然后尝试添加日志到unstable中;
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
...
case pb.MsgApp:
// 收到添加日志消息,重置选举时间
r.electionElapsed = 0
r.lead = m.From
r.handleAppendEntries(m)
...
}
}
两者公用handleAppendEntries(),这个函数主要作用就是做数据匹配,如果leader发送的待同步消息的前一条日志能和本节点的已同步的日志的最后一条日志匹配上,则接受新的日志同步,否则拒绝,拒绝的原因是因为本节点缺失了部分leader已经持久化的日志记录,所以拒绝的时候返回本节点最新已提交的索引给leader,方便leader传输给本节点缺失的日志数据。
最后就是调用send()封装同步日志的响应,后续在node.run()的for循环封装成新的ready,返还给应用层,然后再转发给Leader。
func (r *raft) handleAppendEntries(m pb.Message) {
// 如果发送方的committed index小于本节点,告知对方当前节点committed index 和 term
if m.Index < r.raftLog.committed {
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
return
}
// 尝试添加到日志模块中
// mlastIndex是当前follower最新数据的index
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()})
}
}
// 添加日志,返回最新一条日志的index,true
func (l *raftLog) maybeAppend(index, logTerm, committed uint64, ents ...pb.Entry) (lastnewi uint64, ok bool) {
if l.matchTerm(index, logTerm) {
// 传入的index和logTerm匹配到才进入此处
// 最新一条日志的index
lastnewi = index + uint64(len(ents))
// 查找传入的数据从哪里开始就找不到对应的Term 正常就是ents[0].Index
ci := l.findConflict(ents)
switch {
case ci == 0:
// 没有冲突 证明数据已经添加过了 忽略
case ci <= l.committed:
// 冲突的索引小于已提交的索引,说明传入数据有误
l.logger.Panicf("entry %d conflict with committed entry [committed(%d)]", ci, l.committed)
default:
// ci > 0 的情况来到此处
offset := index + 1
// 从查找到的数据索引开始,将这之后的数据放入到unstable储存中(内存)
l.append(ents[ci-offset:]...)
}
// 选择committed和lastnewi中最小的进行commit
l.commitTo(min(committed, lastnewi))
return lastnewi, true
}
return 0, false
}
7.2.5 Leader收到follower响应同步日志提议后提交日志
Leader每次收到follower同步日志响应,会在定制的状态机函数中做出响应。
如果follower拒绝同步日志提议,说明缺失备份日志数据,此时leader更新follower的同步日志进度,根据follower提供的它自己的已提交的索引,leader把从这个索引的后的全部日志重新发送给follower
如果follower同意日志同步提议,leader更新此follower再集群内的同步进度,然后等Leader获得集群内的多数派同意提议后,leader更新已提交日志的索引,然后广播给follower此提交日志的索引。
func stepLeader(r *raft, m pb.Message) {
...
// 响应类消息
switch m.Type {
case pb.MsgAppResp:
// follower对leader同步日志的请求响应
pr.RecentActive = true
if m.Reject {
// 对append请求拒绝,说明term,index不匹配也就是follower缺失数据,得先补齐follower缺失的数据
r.logger.Debugf("%x received msgApp rejection(lastindex: %d) from %x for index %d",
r.id, m.RejectHint, m.From, m.Index)
// 更新pr的同步进度,重新发送pr缺失的日志
if pr.maybeDecrTo(m.Index, m.RejectHint) {
r.logger.Debugf("%x decreased progress of %x to [%s]", r.id, m.From, pr)
if pr.State == ProgressStateReplicate {
pr.becomeProbe()
}
// 再次发送
r.sendAppend(m.From)
}
} else {
oldPaused := pr.IsPaused()
// 判断follower的MsgAppResp响应最新添加预写日志索引是否需要更新 pr.maybeUpdate更新leader下follower的同步进度
if pr.maybeUpdate(m.Index) {
switch {
case pr.State == ProgressStateProbe:
pr.becomeReplicate()
case pr.State == ProgressStateSnapshot && pr.needSnapshotAbort():
r.logger.Debugf("%x snapshot aborted, resumed sending replication messages to %x [%s]", r.id, m.From, pr)
pr.becomeProbe()
case pr.State == ProgressStateReplicate:
pr.ins.freeTo(m.Index)
}
if r.maybeCommit() {
// maybeCommit() 主要是判断同步的预写日志是否在集群内的多数派follower都已经持久化,如果都已经持久化,说明上一笔预写日志已经被多数派的follower持久化到unstable,已经可以被leader提交
// 那leader就更新自己的committed index为上一笔预写日志的index,然后广播通知其他节点持久化committed index
// 如果可以commit日志,那就广播append消息
r.bcastAppend()
} else if oldPaused {
// update() reset the wait state on this node. If we had delayed sending
// an update before, send it now.
r.sendAppend(m.From)
}
// Transfer leadership is in progress.
if m.From == r.leadTransferee && pr.Match == r.raftLog.lastIndex() {
r.logger.Infof("%x sent MsgTimeoutNow to %x after received MsgAppResp", r.id, m.From)
r.sendTimeoutNow(m.From)
}
}
}
...
}
}
maybeDecrTo()主要就是更新follower的日志同步进度,follower拒绝leader日志同步会告知自己当前最新的已提交日志索引 committed index,leader就同步此follower的Next(下一条同步日志索引)为committed index + 1,然后把从索引为Next开始的日志,一并重新发送给follower同步
// 更新拒绝日志同步的节点的同步进度
func (pr *Progress) maybeDecrTo(rejected, last uint64) bool {
if pr.State == ProgressStateReplicate {
// the rejection must be stale if the progress has matched and "rejected"
// is smaller than "match".
if rejected <= pr.Match {
return false
}
// directly decrease next to match + 1
pr.Next = pr.Match + 1
return true
}
// the rejection must be stale if "rejected" does not match next - 1
if pr.Next-1 != rejected {
return false
}
if pr.Next = min(rejected, last+1); pr.Next < 1 {
pr.Next = 1
}
pr.resume()
return true
}
raft.maybeCommit()主要就是拿到所有节点的Match,从小到大的排序后,取中位数,看此中位数如果大于当前leader的committed index 并且term也匹配的上就更新leader的committed index
// 判断是否得到多数派的认可
func (r *raft) maybeCommit() bool {
// TODO(bmizerany): optimize.. Currently naive
mis := make(uint64Slice, 0, len(r.prs))
// 拿到所有节点的Match到数组中
for id := range r.prs {
mis = append(mis, r.prs[id].Match)
}
// 逆序排序
sort.Sort(sort.Reverse(mis))
// 排序之后拿到中位数的Match,因为如果这个位置的Match对应的Term也等于当前的Term,说明有过半的节点至少commit了mci这个索引,这样leader就可以以这个索引进行commit
mci := mis[r.quorum()-1]
return r.raftLog.maybeCommit(mci, r.Term)
}
// 更新当前节点的committed index
func (l *raftLog) maybeCommit(maxIndex, term uint64) bool {
if maxIndex > l.committed && l.zeroTermOnErrCompacted(l.term(maxIndex)) == term {
l.commitTo(maxIndex)
return true
}
return false
}
7.2.6 Leader再算法层更新committed index之后通知应用层应用此已提交的日志
上面Leader收到多数派同意的follower的同步日志响应之后,算法层node.run()那个for循环检测到committed index发生了变化,就会重新newReady()发送给应用层。
raftNode.entriesToApply()主要就是去掉已经应用过的Entry,防止重复应用。
raftNode.publishEntries()主要就是把要应用的Entry直接丢进rc.commitC channel中,这个就是kvstore监听的那一个commitC(不清楚回去看4.11),然后kvstore把entry存放到数据状态机
func (rc *raftNode) serveChannels() {
...
// event loop on raft state machine updates
// 对内(应用层和算法层打交道)
for {
select {
...
// store raft entries to wal, then publish over commit channel
// raftNode通过node.Ready()方法接收到算法层的处理结果后
// 1.需要将待持久化的预写日志(Ready.Entries)进行持久化 storage,
// 2.然后调用通信模块执行消息(Ready.Messages)发送,
// 3.需要和数据状态机交互,应用算法层已确认提交的预写日志
// 4.处理完上述步骤后,raftNode会调用Node.Advance()方法对算法层进行响应(告诉算法层,我已经处理完你要我处理的Ready)
case rd := <-rc.node.Ready():
rc.wal.Save(rd.HardState, rd.Entries)
if !raft.IsEmptySnap(rd.Snapshot) {
rc.saveSnap(rd.Snapshot)
rc.raftStorage.ApplySnapshot(rd.Snapshot)
rc.publishSnapshot(rd.Snapshot)
}
// 1
rc.raftStorage.Append(rd.Entries)
// 2
rc.transport.Send(rd.Messages)
// 3
if ok := rc.publishEntries(rc.entriesToApply(rd.CommittedEntries)); !ok {
rc.stop()
return
}
rc.maybeTriggerSnapshot()
// 4
rc.node.Advance()
...
}
}
// 去掉已经应用过的Entry
func (rc *raftNode) entriesToApply(ents []raftpb.Entry) (nents []raftpb.Entry) {
if len(ents) == 0 {
return
}
firstIdx := ents[0].Index
if firstIdx > rc.appliedIndex+1 {
log.Fatalf("first index of committed entry[%d] should <= progress.appliedIndex[%d] 1", firstIdx, rc.appliedIndex)
}
// 去掉已经应用过的那部分entries 比如ents有6条数据(2 <= index <= 7),rc.appliedIndex = 4, firstIdx = 2,为了去掉重复的2,3,4,只取5,6,7,那数组就得从5开始,也就是ents[ 4 - 2 + 1 :]
if rc.appliedIndex-firstIdx+1 < uint64(len(ents)) {
nents = ents[rc.appliedIndex-firstIdx+1:]
}
return
}
func (rc *raftNode) publishEntries(ents []raftpb.Entry) bool {
for i := range ents {
switch ents[i].Type {
// 已提交日志是正常的写数据请求,通过commitC将数据传送给kvstore,然后由kvstore的消费goroutine将其真正写入数据状态机
case raftpb.EntryNormal:
if len(ents[i].Data) == 0 {
// ignore empty messages
break
}
s := string(ents[i].Data)
select {
// 把待应用的预写日志传入raft.commitC channel,由kvstore.readCommits()存入状态机
case rc.commitC <- &s:
...
// after commit, update appliedIndex
rc.appliedIndex = ents[i].Index
...
}
return true
}
8 读流程(重点)
8.1 整体路程图
首先就是应用层接收到客户端的读请求,然后调用node.ReadIndex函数把读请求发送给算法层,进入算法层,如果是Leader,接着看是什么读模式,如果是ReadOnlyLeaseBased就直接封装响应到ready给应用层,应用层直接响应客户端的读请求就行,如果是ReadOnlySafe会吧读请求挂起到读请求队列,然后leader向follower发送心跳,得到多数派之后才释放读请求,后续流程跟上一样。 如果是follower会把读请求转发给Leader,然后等Leader执行完上述操作,返回给自己响应后,follower再响应给客户端
(这里etcd的example其实并没有给出应用层读取到算法层的相应之后的操作,纯个人脑补)
8.2 应用层发送读请求
应用层raftNode调用Node.ReadIndex方法,通过recvc channel 向算法层goroutine发送一条类型为ReadIndex的消息
func (n *node) ReadIndex(ctx context.Context, rctx []byte) error {
return n.step(ctx, pb.Message{Type: pb.MsgReadIndex, Entries: []pb.Entry{{Data: rctx}}})
}
func (n *node) step(ctx context.Context, m pb.Message) error {
ch := n.recvc
if m.Type == pb.MsgProp {
ch = n.propc
}
select {
case ch <- m:
return nil
case <-ctx.Done():
return ctx.Err()
case <-n.done:
return ErrStopped
}
}
8.3 不同身份不同处理
如果follower接收到读请求,则会直接转发给Leader,让leader处理好后给自己响应处理结果,然后自己再响应给客户端
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
...
case pb.MsgReadIndex:
if r.lead == None {
r.logger.Infof("%x no leader at term %d; dropping index reading msg", r.id, r.Term)
return
}
m.To = r.lead
r.send(m)
...
}
如果是Leader接收到读请求,会先判断是什么读模式,如果是ReadOnlyLeaseBased,直接通知应用层响应客户端的读请求即可,如果是ReadOnlySafe,就会把读请求挂起到读请求队列(因为Leader可能宕机恢复,此时集群内已经有了新的leader,所以相应之前得证明自己leader的身份),然后发送广播心跳MsgHeartbeat,等待follower的响应
func stepLeader(r *raft, m pb.Message) {
// These message types do not require any progress for m.From.
// 请求类消息
switch m.Type {
...
case pb.MsgReadIndex:
// 消息为读请求类型
if r.quorum() > 1 {
if r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(r.raftLog.committed)) != r.Term {
// Reject read only request when this leader has not committed any log entry at its term.
return
}
switch r.readOnly.option {
// 如果是 ReadOnlySafe 模式,调用 readOnly.addRequest() 方法将只读请求加入到只读请求队列中。
// 将本地节点的响应信息加入到只读请求的 ACK 队列中。
case ReadOnlySafe:
// 把读请求到来时的committed index 保存下来
r.readOnly.addRequest(r.raftLog.committed, m)
// 广播消息出去,其中消息的ctx是该读请求的唯一标识
// 在应答时context要原样返回,将使用这个ctx操作readonly相关数据
r.bcastHeartbeatWithCtx(m.Entries[0].Data)
//如果是 ReadOnlyLeaseBased 模式,直接调用 responseToReadIndexReq() 方法生成响应信息并发送给客户端。在这种模式下,不需要等待多数节点响应
case ReadOnlyLeaseBased:
var ri uint64
if r.checkQuorum {
ri = r.raftLog.committed
}
if m.From == None || m.From == r.id { // from local member
r.readStates = append(r.readStates, ReadState{Index: r.raftLog.committed, RequestCtx: m.Entries[0].Data})
} else {
r.send(pb.Message{To: m.From, Type: pb.MsgReadIndexResp, Index: ri, Entries: m.Entries})
}
}
} else {
r.readStates = append(r.readStates, ReadState{Index: r.raftLog.committed, RequestCtx: m.Entries[0].Data})
}
return
}
...
}
}
8.4 follower接收到Leader的广播心跳
follower接收到Leader的广播心跳后,会重置自己的选举时间,然后更新自己的committed index,然后给leader响应
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
...
case pb.MsgHeartbeat:
r.electionElapsed = 0
r.lead = m.From
r.handleHeartbeat(m)
...
}
}
func (r *raft) handleHeartbeat(m pb.Message) {
r.raftLog.commitTo(m.Commit)
r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context})
}
8.5 leader收到follower响应
如果leader收到了多数派的follower响应自己的心跳,则释放读请求,然后发送给应用层,最后响应给客户端
func stepLeader(r *raft, m pb.Message) {
...
switch m.Type {
...
case pb.MsgHeartbeatResp:
// 接收到follower心跳响应,证明follower很健康活跃
pr.RecentActive = true
pr.resume()
// free one slot for the full inflights window to allow progress.
if pr.State == ProgressStateReplicate && pr.ins.full() {
pr.ins.freeFirstOne()
}
if pr.Match < r.raftLog.lastIndex() {
r.sendAppend(m.From)
}
if r.readOnly.option != ReadOnlySafe || len(m.Context) == 0 {
return
}
// 收到应答调用recvAck函数返回当前针对该消息已经应答的节点数量 本节点默认答应
ackCount := r.readOnly.recvAck(m)
if ackCount < r.quorum() {
// 小于集群半数以上就返回不往下走了
return
}
// 调用advance()函数返回该读请求前的一系列读请求,然后把这些读请求从readOnly.pendingReadIndex和readIndexQueue删除
rss := r.readOnly.advance(m)
for _, rs := range rss {
req := rs.req
if req.From == None || req.From == r.id { // from local member
r.readStates = append(r.readStates, ReadState{Index: rs.index, RequestCtx: req.Entries[0].Data})
} else {
r.send(pb.Message{To: req.From, Type: pb.MsgReadIndexResp, Index: rs.index, Entries: req.Entries})
}
}
...
}
}
func (ro *readOnly) recvAck(m pb.Message) int {
rs, ok := ro.pendingReadIndex[string(m.Context)]
if !ok {
return 0
}
rs.acks[m.From] = struct{}{}
// add one to include an ack from local node
return len(rs.acks) + 1
}
9 身份转换(重点)
9.1 Leader -> follower
一共有两种情况,下面是第一种
Leader再处理应用层发送过来的请求时,会做任期term校验,如果对方任期term比自己大,并且不是预选举的话(预选举时还没有candidate,所以忽略),说明自己该退位了
func (r *raft) Step(m pb.Message) error {
// Handle the message term, which may result in our stepping down to a follower.
// 比较term
switch {
...
case m.Term > r.Term:
// 对方任期term比自己大
lead := m.From
...
switch {
case m.Type == pb.MsgPreVote:
// 如果消息类型时预投票 节点身份不用转换
case m.Type == pb.MsgPreVoteResp && !m.Reject:
// 如果消息类型时预投票响应并且对方节点没有拒绝自己的预投票请求 节点身份不用转换
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)
r.becomeFollower(m.Term, lead)
}
...
return nil
}
第二种情况,就是在Leader的专属处理状态机函数中,Leader如果监测到集群没有半数节点是活跃的就会变成follower,因为此时的多数派已经失效
func stepLeader(r *raft, m pb.Message) {
// These message types do not require any progress for m.From.
// 请求类消息
switch m.Type {
...
case pb.MsgCheckQuorum:
// 检测集群可用性
if !r.checkQuorumActive() {
// 如果超过半数的服务器没有活跃
// 变成follower
r.logger.Warningf("%x stepped down to follower since quorum is not active", r.id)
r.becomeFollower(r.Term, None)
}
return
...
}
9.1.1 becomeFollower
主要就是切换节点的身份,状态机处理函数还有一些基础信息
// 切换成follower函数
func (r *raft) becomeFollower(term uint64, lead uint64) {
r.step = stepFollower
r.reset(term)
r.tick = r.tickElection
r.lead = lead
r.state = StateFollower
r.logger.Infof("%x became follower at term %d", r.id, r.Term)
}
func (r *raft) reset(term uint64) {
if r.Term != term {
r.Term = term
r.Vote = None
}
r.lead = None
r.electionElapsed = 0
r.heartbeatElapsed = 0
r.resetRandomizedElectionTimeout()
r.abortLeaderTransfer()
r.votes = make(map[uint64]bool)
for id := range r.prs {
r.prs[id] = &Progress{Next: r.raftLog.lastIndex() + 1, ins: newInflights(r.maxInflight)}
if id == r.id {
r.prs[id].Match = r.raftLog.lastIndex()
}
}
r.pendingConf = false
r.readOnly = newReadOnly(r.readOnly.option)
}
9.2 follower -> candidate
出现这种情况的可能就是因为leader没有定时发送心跳给follower去重置选举时间,导致选举时间超时,follower就会给自己发送一条MsgHub消息,让自己先开始预选举(预选举是因为防止网络分区导致follower跟多数派失去联系,然后无限发起选举自增term)
func (r *raft) tickElection() {
r.electionElapsed++
if r.promotable() && r.pastElectionTimeout() {
// 如果可以被提升为leader,同时选举时间到了
r.electionElapsed = 0
// 发送HUB消息开始选举
r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
}
}
follower接收到自己发送给自己的MsgHub消息后,进入raft.Step()状态机处理函数,然后调用rafr.compaign发起一轮预选举操作
func (r *raft) Step(m pb.Message) error {
...
switch m.Type {
case pb.MsgHup:
...
if r.preVote {
r.campaign(campaignPreElection)
} else {
r.campaign(campaignElection)
}
}
...
return nil
}
在raft.campaign方法中follower首先会调用becomePreCandidate()切换身份成preCandidate,然后给自己拉票
func (r *raft) campaign(t CampaignType) {
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection {
r.becomePreCandidate()
voteMsg = pb.MsgPreVote
// PreVote RPCs are sent for the next term before we've incremented r.Term.
term = r.Term + 1
}
...
// 给除了本节点之外集群内的其他节点发送竞选消息
for id := range r.prs {
if id == r.id {
continue
}
r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), voteMsg, id, r.Term)
var ctx []byte
if t == campaignTransfer {
ctx = []byte(t)
}
r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
}
}
becomePreCandidate(),follower切换状态机处理函数还有身份
// 切换成预竞选状态
func (r *raft) becomePreCandidate() {
...
// 预竞选不会递增term,也不会先进行投票,而是等prevote结果出来在进行决定
r.step = stepCandidate
r.tick = r.tickElection
r.state = StatePreCandidate
r.logger.Infof("%x became pre-candidate at term %d", r.id, r.Term)
}
9.2.1 其他节点收到拉票消息
其他节点收到拉票消息后,会在raft.Step()通用状态机处理函数处理,判断自己有没有投票,对方的日志数据是否新过自己,如果都满足那就投同意票,否则拒绝
func (r *raft) Step(m pb.Message) error {
...
switch m.Type {
...
case pb.MsgVote, pb.MsgPreVote:
// 收到投票类消息
// The m.Term > r.Term clause is for MsgPreVote. For MsgVote m.Term should
// always equal r.Term.
if (r.Vote == None || m.Term > r.Term || r.Vote == m.From) && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
// 如果当前本节点没有给任何节点投过票或者对方任期大于自己或者本节点已经投过票给发投票消息的节点
// 同时满足发起投票的节点数据是最新的,跟自己相等也可以
// 满足以上条件,就偷同意,否则拒绝
r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] cast %s for %x [logterm: %d, index: %d] at term %d",
r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
r.send(pb.Message{To: m.From, Type: voteRespMsgType(m.Type)})
if m.Type == pb.MsgVote {
// Only record real votes.
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, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
r.send(pb.Message{To: m.From, Type: voteRespMsgType(m.Type), Reject: true})
}
default:
// 其他情况进入不同身份的状态机函数
r.step(r, m)
}
return nil
}
// 判断发起投票的节点的数据是否新于或者等于自己 先比较term,term相等再比较lastIndex
func (l *raftLog) isUpToDate(lasti, term uint64) bool {
return term > l.lastTerm() || (term == l.lastTerm() && lasti >= l.lastIndex())
}
9.2.2 preCandidate收到follower响应
因为拉票的时候follower把身份切换成preCandidate,状态机函数也切换成stepCandidate,所以,收到follower的投票响应后会进入stepCandidate函数,然后看自己是否得到集群中多数派的支持,如果得到多数派的支持就在以campaignElection去调用一次campaign()函数,切换身份成candidate
func stepCandidate(r *raft, m pb.Message) {
var myVoteRespType pb.MessageType
if r.state == StatePreCandidate {
myVoteRespType = pb.MsgPreVoteResp
} else {
myVoteRespType = pb.MsgVoteResp
}
switch m.Type {
...
case myVoteRespType:
gr := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
switch r.quorum() {
case gr:
// 如果正好有半数节点进行投票
if r.state == StatePreCandidate {
r.campaign(campaignElection)
} else {
r.becomeLeader()
r.bcastAppend()
}
case len(r.votes) - gr:
// 如果半数以上节点拒绝了投票就变成follower
r.becomeFollower(r.Term, None)
}
...
}
}
func (r *raft) campaign(t CampaignType) {
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection {
...
} else {
r.becomeCandidate()
voteMsg = pb.MsgVote
term = r.Term
}
}
func (r *raft) becomeCandidate() {
// TODO(xiangli) remove the panic when the raft implementation is stable
if r.state == StateLeader {
// leader不可能退化成candidate
panic("invalid transition [leader -> candidate]")
}
r.step = stepCandidate
// 进入candidate状态,就意味着需要重新选举leader,所以传入的是term+1
r.reset(r.Term + 1)
r.tick = r.tickElection
// 给自己投票
r.Vote = r.id
r.state = StateCandidate
r.logger.Infof("%x became candidate at term %d", r.id, r.Term)
}
9.3 candidate -> Leader
接着上面9.2,变成candidate的时候会发起一轮投票,如果得到多数派认可,直接变成leader,否则变回follower
func (r *raft) campaign(t CampaignType) {
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection {
r.becomePreCandidate()
voteMsg = pb.MsgPreVote
// PreVote RPCs are sent for the next term before we've incremented r.Term.
term = r.Term + 1
} else {
r.becomeCandidate()
voteMsg = pb.MsgVote
term = r.Term
}
// 统计票数
if r.quorum() == r.poll(r.id, voteRespMsgType(voteMsg), true) {
// 判断当前节点是否获得集群的半数节点支持
// We won the election after voting for ourselves (which must mean that
// this is a single-node cluster). Advance to the next state.
if t == campaignPreElection {
// 如果预选举已经获得多数派认可,直接发起选举
r.campaign(campaignElection)
} else {
// 如果上一轮拉票是选举,当前又获得了多数派的投票,标识已经可以直接切换成leader
r.becomeLeader()
}
return
}
// 给除了本节点之外集群内的其他节点发送竞选消息
for id := range r.prs {
if id == r.id {
continue
}
r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), voteMsg, id, r.Term)
var ctx []byte
if t == campaignTransfer {
ctx = []byte(t)
}
r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
}
}
9.3.1 Candidate处理拉票结果
跟preCandidate处理一样,得到多数派认可,就直接变成leader,否则变成follower,流程跟follower -> preCandidate一样。
func stepCandidate(r *raft, m pb.Message) {
// Only handle vote responses corresponding to our candidacy (while in
// StateCandidate, we may get stale MsgPreVoteResp messages in this term from
// our pre-candidate state).
var myVoteRespType pb.MessageType
if r.state == StatePreCandidate {
myVoteRespType = pb.MsgPreVoteResp
} else {
myVoteRespType = pb.MsgVoteResp
}
switch m.Type {
...
case myVoteRespType:
gr := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
switch r.quorum() {
case gr:
// 如果正好有半数节点进行投票
if r.state == StatePreCandidate {
r.campaign(campaignElection)
} else {
r.becomeLeader()
r.bcastAppend()
}
case len(r.votes) - gr:
// 如果半数以上节点拒绝了投票就变成follower
r.becomeFollower(r.Term, None)
}
case pb.MsgTimeoutNow:
r.logger.Debugf("%x [term %d state %v] ignored MsgTimeoutNow from %x", r.id, r.Term, r.state, m.From)
}
}
9.4 Candidate ->follower
candidate变成follower情况比较多,首先就是进入raft.Step()通用状态机函数,如果收到消息任期比自己大并且不是预投票就会回退成follower
func (r *raft) Step(m pb.Message) error {
// Handle the message term, which may result in our stepping down to a follower.
// 比较term
switch {
...
case m.Term > r.Term:
// 对方任期term比自己大
lead := m.From
...
switch {
case m.Type == pb.MsgPreVote:
// 如果消息类型时预投票 节点身份不用转换
case m.Type == pb.MsgPreVoteResp && !m.Reject:
// 如果消息类型时预投票响应并且对方节点没有拒绝自己的预投票请求 节点身份不用转换
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)
r.becomeFollower(m.Term, lead)
}
...
return nil
}
另外的情况就是在进入专属的状态机处理函数的时候
func stepCandidate(r *raft, m pb.Message) {
// Only handle vote responses corresponding to our candidacy (while in
// StateCandidate, we may get stale MsgPreVoteResp messages in this term from
// our pre-candidate state).
var myVoteRespType pb.MessageType
if r.state == StatePreCandidate {
myVoteRespType = pb.MsgPreVoteResp
} else {
myVoteRespType = pb.MsgVoteResp
}
switch m.Type {
case pb.MsgProp:
// leader才处理写请求,candidate忽略
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return
case pb.MsgApp:
// 收到append消息,说明集群中已经有了leader,转换为follower
r.becomeFollower(r.Term, m.From)
// follower同步持久化日志
r.handleAppendEntries(m)
case pb.MsgHeartbeat:
r.becomeFollower(r.Term, m.From)
r.handleHeartbeat(m)
case pb.MsgSnap:
r.becomeFollower(m.Term, m.From)
r.handleSnapshot(m)
case myVoteRespType:
gr := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x [quorum:%d] has received %d %s votes and %d vote rejections", r.id, r.quorum(), gr, m.Type, len(r.votes)-gr)
switch r.quorum() {
case gr:
// 如果正好有半数节点进行投票
if r.state == StatePreCandidate {
r.campaign(campaignElection)
} else {
r.becomeLeader()
r.bcastAppend()
}
case len(r.votes) - gr:
// 如果半数以上节点拒绝了投票就变成follower
r.becomeFollower(r.Term, None)
}
case pb.MsgTimeoutNow:
r.logger.Debugf("%x [term %d state %v] ignored MsgTimeoutNow from %x", r.id, r.Term, r.state, m.From)
}
}
10 个人注解的版本
binarysear/etcd-3.1.10-raft-: etcd-3.1.10-raft实现部分注解 (github.com)