【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 操作都会被同步到存储,并根据节点角色(领导者或跟随者)执行不同的行为。

相关推荐
OK_boom9 分钟前
Dapper的数据库操作备忘
数据库
艺杯羹41 分钟前
JDBC之ORM思想及SQL注入
数据库·sql·jdbc·orm·sql注入
blackA_1 小时前
数据库MySQL学习——day4(更多查询操作与更新数据)
数据库·学习·mysql
极限实验室2 小时前
Easysearch 迁移数据之 Reindex From Remote
数据库
朴拙数科2 小时前
基于LangChain与Neo4j构建企业关系图谱的金融风控实施方案,结合工商数据、供应链记录及舆情数据,实现隐性关联识别与动态风险评估
数据库·langchain·neo4j
小李学不完3 小时前
Oracle--SQL事务操作与管理流程
数据库
qq_441996053 小时前
为何 RAG 向量存储应优先考虑 PostgreSQL + pgvector 而非 MySQL?
数据库·mysql·postgresql
Ivan陈哈哈3 小时前
Redis是单线程的,如何提高多核CPU的利用率?
数据库·redis·缓存
唯独失去了从容4 小时前
WebRTC服务器Coturn服务器中的通信协议
运维·服务器·webrtc
小光学长4 小时前
基于vue框架的电信用户业务管理系统的设计与实现8ly70(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库