【ETCD】【源码阅读】深入解析 raftNode.start方法实现

让我们从源码层面逐步分析这段代码。这段代码是 ETCD Raft 实现中,raftNodestart 方法,负责启动一个 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 操作都会被同步到存储,并根据节点角色(领导者或跟随者)执行不同的行为。

相关推荐
另一种开始1 小时前
记一个framebuffer显示混乱的低级错误
数据库
靠谱杨1 小时前
【Linux服务器nginx前端部署详解】ubantu22.04,前端Vue项目dist打包
linux·服务器·前端·vue.js·经验分享·阿里云·腾讯云
纪伊路上盛名在1 小时前
生成式AI、大模型、多模态技术开发与应用学习清单
服务器·人工智能·笔记·学习·知识图谱·学习方法
Dream25122 小时前
【Python与MySQL交互】
数据库·mysql
ssxueyi2 小时前
Java 常见Exception异常解决方法
java·开发语言·数据库·异常处理·exception
Dontas2 小时前
【Trouble Shooting】Oracle ADG hung,出现ORA-04021
数据库·oracle
Tttian6223 小时前
Pycharm访问MySQL数据库·上
数据库·mysql·pycharm
下雪了 ~3 小时前
HTTP和HTTPS的区别有哪些?
服务器·前端·笔记·网络协议·计算机网络
百川Cs4 小时前
【Linux】文件挂载系统-Filesystem mounting
linux·运维·服务器