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

让我们从源码层面逐步分析这段代码。这段代码是 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 操作都会被同步到存储,并根据节点角色(领导者或跟随者)执行不同的行为。

相关推荐
这个DBA有点耶1 分钟前
DELETE没删多少数据,表却越来越大——InnoDB删除空洞与页合并失效的根因
数据库·mysql·代码规范
对象存储与RustFS1 小时前
网盘限速逼你开会员?用 RustFS 自己搭一个不限速的“私人网盘
服务器·后端·docker
..Dauntless..3 小时前
【Linux】一切皆文件和缓冲区
linux·运维·服务器
裕晟资质规划3 小时前
三类资质变更报告时限差在哪?军工 15 日、集成印制 10 日
大数据·服务器·网络
ly76893 小时前
PostgreSQL 备份恢复策略:pg_basebackup、WAL 归档与 PITR 在企业 Java 系统中的落地
java·数据库·postgresql·pitr·pg_basebackup·wal 归档
QMCY_jason3 小时前
x86 ubuntu2204 无gui 安装 openeuler 2203 aarch64版本
linux·运维·服务器
重生之小比特3 小时前
【Linux系统】12 进程程序替换 自定义Shell 命令行解释器
linux·运维·服务器
轻舟技术A3 小时前
Provider 不属于 Agent 核心:rpi 的分层理由与收益
服务器·网络
连山齐名4 小时前
ai应用开发的学习笔记
服务器·笔记·学习
Shadow(⊙o⊙)4 小时前
仿muduo实现高并发服务器——事件就绪管理Channel.hpp
服务器·c++