raft算法和etcd代码解析-5.应用模块和算法模块的启动
Node接口
Node是raft应用模块在节点上的抽象,也是应用模块和算法模块交互的入口
应用模块持有Node作为算法模块的引用,通过调用Node接口的API与算法模块通信,通信方式是通过若干个Channel异步完成的。
// Node represents a node in a raft cluster.
type Node interface {
// 告知算法模块时间
Tick()
// 告知需要参与竞选
Campaign(ctx context.Context) error
// 发送写请求
Propose(ctx context.Context, data []byte) error
// 发送配置变更请求
ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
// ...
Ready() <-chan Ready
// Ready任务之后,通知算法模块
Advance()
// 应用模块感知到应用变更被集群认可
ApplyConfChange(cc pb.ConfChange) *pb.ConfState
// ...
// 读请求
ReadIndex(ctx context.Context, rctx []byte) error
// ...
}
ReadyOnly
读请求全部由leader处理
type readIndexStatus struct {
//封装读请求的消息体
req pb.Message
//leader已提交日志索引
index uint64
//Leader收到的节点响应,实际上是个Set类型
//为了防止网络同步时失误而存在多个leader的结果
//leader需要向其他follower告知自己身份,以期得到多数派响应,证明leader身份是合法的
//acks就是用来收集其他follower响应
acks map[uint64]struct{}
}
type readOnly struct {
option ReadOnlyOption
//使用entry数据当作key,保存读请求队列中的读请求的状态
pendingReadIndex map[string]*readIndexStatus
//将多个读请求放入队列,依次处理
readIndexQueue []string
}
startRaft方法
func (rc *raftNode) startNode() {
//...
// 获取集群其他raft信息
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, // 当前节点ID
ElectionTick: 10, // 选举时间 10个tick加上随机扰动值后发起选举
HeartbeatTick: 1, // 广播心跳时间
Storage: rc.raftStore // 应用层给算法层的存储接口 算法层可通过此查询存储
}
// 集群信息
startPeers := rpeers
//启动算法层的node
rc.node = raft.StartNode(c, startPeers)
// ...
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),
}
// raft节点之间的通信
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()
// 异步开启raftNoide的主循环 用于与算法层的goroutine建立持续通信关系
go rc.serveChannel
}
serveChannel方法
serveChannel方法有两处阻塞监听
func (rc *raftNode) serveChannels() {
// ...
// 这里定义一个ticker 为 100ms
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
// 接受客户端请求 传送到算法层
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 {
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)
}()
// 接受算法层关于日志持久化的信息
for {
select {
// 监听ticker 并发送给算法模块 让算法模块感知时间
case <-ticker.C:
rc.node.Tick()
// 接受算法层要求
case rd := <-rc.node.Ready():
// 预写日志持久化
rc.raftStorage.Append(rd.Entries)
// f调用模块 例如发送邮件信息
rc.transport.Send(rd.Messages)
// 将预写日志应用到状态机
if okI := rc.publishEntries(rc.entriesToApply(rd.CommittedEnttries)); ok {
rc.stop()
return
}
// 告知算法模块 已完成预写日志持久化
rc.node.Advance()
//...
}
}
}
StartNode方法
StartNode启动Node,Node是应用模块中的对算法模块的引用
func StartNode(c *Config, peers []Peer) Node {
// 初始化raft共识机制的抽象结构
r := newRaft (c)
//初次启动以term为1来启动
r.becomeFollower(1, None)
for _, peer := range peers {
// 遍历列表 将节点信息提交
// 提交方式和配置变更日志方式一样
}
r.raftLog.committed = r.raftLog.lastIndex()
for _, peer := range peers {
r.addNode(peer.ID)
}
n := newNode()
// Node开始运行 Node的核心方法
// 在其中有for select阻塞监听多个通道(chan)
// 通道详见下一个函数
go n.run(r)
return &n
}
node中有算法和应用模块通信的通道
func newNode() node {
return node{
propc: make(chan pb.Message),
recvc: make(chan pb.Message),
confc: make(chan pb.ConfChange),
readyc: make(chan Ready),
advancec: make(chan struct{}),
tickc: make(chan struct{}, 128),
}
}
run方法中的阻塞监听大概如下
for{
//首先处理advancec advancec是应用层向算法层告知请求处理完成的通道
if advancec != nil {
// advance channel不为空,说明还在等应用调用Advance接口通知已经处理完
readyc = nil
} else {
rd = newReady(r, prevSoftSt, prevHardSt)
if rd.containsUpdates() {
// 设置为非空
// 如果这次ready消息有包含更新,那么ready channel就不为空
readyc = n.readyc
} else {
// 设置为空 算法层不再可以通过这个通道传递消息
// 将通道置空 是go中一个使用通道技巧 可以锁住该通道
readyc = nil
}
}
// 处理其他通道
select {
case m := <- propc:
//处理本地收到的提交值
// ...
case m := <- n.recvc:
//处理其他节点发送过来的提交值
//...
case cc := <- n.confc:
//接收到配置发生变化的消息
//...
case <-n.tickc:
//...
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现