让我们从源码层面逐步分析这段代码。这段代码是 ETCD Raft 实现中,raftNode
的 start
方法,负责启动一个 Raft 节点,并在一个新的 goroutine 中处理 Raft 相关的事件。以下是对代码的逐步分析:
代码总体结构
go
func (r *raftNode) start(rh *raftReadyHandler) {
internalTimeout := time.Second
go func() {
defer r.onStop() // 确保 goroutine 停止时调用 onStop 方法
islead := false // 用来记录节点是否是领导者
for {
select {
case <-r.ticker.C:
r.tick() // 定时器事件,触发 tick 操作
case rd := <-r.Ready(): // 等待 Raft 操作准备好
// 处理 Raft 操作
case <-r.stopped: // 如果 raftNode 被停止
return
}
}
}()
}
1. 启动 Goroutine
go
go func() {
defer r.onStop()
- 这部分代码使用
go
关键字在一个新的 goroutine 中异步执行 Raft 节点的操作。defer r.onStop()
确保在 goroutine 停止时执行onStop()
方法进行清理。
2. 初始化领导者标志
go
islead := false
- 这行代码用于初始化
islead
标志,记录当前节点是否是 Raft 集群中的领导者。
3. 事件循环
go
for {
select {
case <-r.ticker.C:
r.tick() // 每当 ticker 定时器触发时,执行 tick 操作
case rd := <-r.Ready(): // 获取 Raft 准备好的日志条目
// 处理 Raft 准备好的日志条目
case <-r.stopped: // 监视是否收到停止信号
return
}
}
- 这部分是事件循环,
select
用来监听多个事件:r.ticker.C
:定时器事件,用于执行tick
操作。r.Ready()
:从 Raft 引擎获取准备好的日志条目并处理。r.stopped
:接收到停止信号时退出 goroutine。
4. 处理 Raft Ready 数据
go
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()
}
rd := <-r.Ready()
:从 Raft 中读取准备好的日志条目和状态。- 通过
rd.SoftState
判断是否发生了领导者变更,并更新领导者相关的信息(例如,islead
是否为领导者)。 leaderChanges.Inc()
:增加领导者变更计数。hasLeader.Set()
和isLeader.Set()
:分别更新是否存在领导者以及当前节点是否是领导者的状态。rh.updateLead()
和rh.updateLeadership()
:更新领导者信息。r.td.Reset()
:重置一个定时器。
5. 处理 Raft ReadState
go
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
}
}
- 处理
ReadState
,如果有待处理的读取请求,尝试将其发送到r.readStateC
信道。如果在internalTimeout
时间内没有完成,则会记录警告日志。
6. 应用日志条目和快照
go
notifyc := make(chan struct{}, 1)
ap := apply{
entries: rd.CommittedEntries,
snapshot: rd.Snapshot,
notifyc: notifyc,
}
updateCommittedIndex(&ap, rh)
waitWALSync := shouldWaitWALSync(rd)
if waitWALSync {
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))
}
}
- 创建一个
apply
对象,表示已提交的日志条目和快照。 updateCommittedIndex(&ap, rh)
:更新已提交的日志索引。shouldWaitWALSync(rd)
:判断是否需要等待 WAL(Write Ahead Log)同步。- 如果需要同步,则调用
r.storage.Save()
将硬状态和日志条目保存到存储中。
7. 处理领导者和快照
go
if islead {
r.transport.Send(r.processMessages(rd.Messages)) // 领导者处理消息并发送
}
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 !waitWALSync {
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))
}
}
- 如果当前节点是领导者(
islead
),则处理 Raft 消息并发送。 - 如果有快照,保存快照数据 (
r.storage.SaveSnap(rd.Snapshot)
),确保恢复后能够正确加载快照。
8. 同步存储和释放资源
go
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))
}
- 调用
r.storage.Sync()
确保存储同步。 - 应用快照 (
r.raftStorage.ApplySnapshot(rd.Snapshot)
) 并记录日志。 - 释放存储中的快照资源 (
r.storage.Release(rd.Snapshot)
).
9. 将日志条目添加到存储中
go
r.raftStorage.Append(rd.Entries)
- 将 Raft 条目追加到存储中。
10. 处理非领导者节点的消息
go
if !islead {
msgs := r.processMessages(rd.Messages)
notifyc <- struct{}{}
select {
case notifyc <- struct{}{}:
case <-r.stopped:
return
}
r.transport.Send(msgs)
} else {
notifyc <- struct{}{}
}
- 如果当前节点不是领导者(
!islead
),则处理 Raft 消息,并发送到网络。 - 其他情况(领导者节点)发送通知。
11. 推进 Raft 状态
go
r.Advance()
- 调用
r.Advance()
,推动 Raft 状态,更新日志。
总结
此方法用于启动 Raft 节点并在 goroutine 中异步运行 Raft 状态机。它通过定时器定期触发 tick
操作,并通过 Ready()
方法获取 Raft 的操作数据进行处理。它处理日志条目、快照、领导者变更等 Raft 相关的核心操作。每个 Raft 操作都会被同步到存储,并根据节点角色(领导者或跟随者)执行不同的行为。