etcd系列-----raft协议:Node结构

Node接口

raft结构体及各种消息处理流程的分析可以看出,结构体raft实现了Raft协议中最核心的内容, 它也是整个rft模块的核心, 但是它并没有实现网络传输、 持久化存储(注意与存储Entry记录的raftLog进行区分〉等功能,也没有对外提供简单易用的APi.在raft模块中,结构体Node表示集群中的一个节点,它是在结构体raft之上的一层封装,对外提供相对简单的API接口。下面看-下Node接口提供的方法

//在前面介绍Raft算法及raft结构体时,捉到了选举计时器和心跳计时器两种计时器,也捉到过
//这两个计时器的时间刻度是逻辑时间,并不是真实世界的时间刻度。Tick()方法就是用来推进逻辑时钟的指针,从而才在进上述的两个计时器
Tick()
//当选举计时器起时之后,会调用Campaign()方法会将当前节切换成Candidate状态(或是PerCandidate状态),底层就是通过发送MsgHup消息实现的,具体的选举前面已介绍
Campaign(ctx context.Context) error
//接收到Client发来的写请求时,Node实例会调用Propose()方法进行处理,底层就是通过发送MsgProp 消息实现的,MsgProp消息的处理流程参考上一小节的介绍
Propose(ctx context.Context,  data []byte)  error
//Client除了会发送读写请求,还会发送修改集群配置的请求(例如, 新增集群中的节点),这种请求Node实例会调用ProposeConfChange()方法进行处理, 底层就是通过发送MsgProp 消息实现的,只不过其中记录的Entry记录是EntryConfChange类型
ProposeConfChange(ctx context.Context,  cc pb.ConfChange) error

//当前节点收到其他节点的消息时,会通过Step()方法将消息交给底层封装的raft实例进行处理
Step(ctx co口text. Context, msg pb.Message) error
//Ready ()方法返回的是一个Channel,通过该Channel返回的Ready实例中封装了底层raft实例的相关状态数据,例如,需妥发送到其他节点的消息、交给上层模块的Entry记录,等等。这是etcd-raft
//模块与上层模块交互的主要方式,Ready的结构在后面会详细分析
Ready() <-chan Ready
//当上层模块处理完从上述Channel中返回的Ready实例之后, 需要调用Advance()通知底层的etcd-raft模块返回新的Ready实例
Advance()
//在收到集群配置请求时,会通过调用ApplyConfChange()方法进行处理
ApplyConfChange(cc pb.ConfChange) *pb.ConfState
//TransferLeadership()方法用于Leader节点的转移,具体实现后面详细介绍
TransferLeadership(ctx context.Context,  lead,  transferee uint64)

//Readindex ()方法用于处理只读请求,具体实现后面详细介绍
Readlndex(ctx context.Context,  rctx  []byte) error
//Status ()返回当前节点的状态运行状态,Status结构体在后面详细介绍
Status() Status
//通过ReportUnreachable()方法通知底层的raft实例,当前节点无法与指定的节点进行通信
ReportUnreachable(id ui到t64)
//ReportSnapshot()方法用于通知底层的raft实例上次发送快照的结采
ReportSnapshot(id uint64,  status SnapshotStatus)
Stop() //关闭当前节点

在开始介绍Node接口中各方法的实现之前,有必要先介绍一下Node相关的几个结构体。首先来看一下Ready结构体,Ready内嵌了SoftState和HardState,在SofteState 中封装了当前集群的Leader节点ID (Lead宇段〉及当前节点的角色(RaftState),在HardState中封装了当前节点的任期号(Term字段〉、当前节点在该任期投票结果(Vote字段)及当前节点的raftLog的己提交位置。除此之外,Ready实例中每个字段都封装一部分的数据,它们的含义如下
    Entries ( []pb.Entry类型):该字段中的Entry记录是从unstable 中读取出来的,上层模块会将其保存到Storage中。
    CommittedEntries ( []pb.Entry类型): 己提交、待应用的Entry记录,这些Entry记录之前己经保存到了Storage中。
    Snapshot ( pb.Snapshot类型):待持久化的快照数据,raftpb.Snapshot中封装了快照数据及相关元数据,在前面己经详细介绍过。
    Messages ( []pb. Message类型):该字段中保存了当前节点中等待发送到集群其他节点的Message消息。
    ReadStates ( []ReadState类型):该字段记录了当前节点中等待处理的只读请求。

最后需要注意的是,Ready实例只是用来传递数据的,其全部字段都是只读的。

Status是另一个需要介绍的结构体,它也内嵌了HardState和So负State,除此之外,它还记录了当前节点的ID (ID字段)、己应用的Entry记录的最大索引值(Applied字段〉。如果当前节点是Leader节点在其Progress字段(map[ uint64 ]Progress类型〉中还记录了集群中每个节点对应的Progress实例,raft.Progress结构体在前面己经介绍过。

1、node结构体

node结构体是Node接口的实现之一, 本小节重点介绍node的具体实现。这里首先来分析node结构体中各个字段的含义,它提供的方法在后面的分析过程中详细介绍。
    propc ( chan pb.Message类型):该通道用于接收MsgProp类型的消息。
    reeve( chan pb.Message类型):除MsgProp外的其他类型的消息都是由该通道接收的。
    confc ( chan pb.ConfChange类型): 当节点收到EntryConfChange类型的Entry记录时,会转换成ConfChange,井写入该通道中等待处理。 在ConfChange中封装了其唯一ID、 待处理的节点ID ( NodeID 字段)及处理类型(Type字段,例如,ConfChangeAddNode类型表示添加节点)等信息。
    confstatec ( chan pb.ConfState类型):在ConfState中封装了当前集群中所有节点的ID,该通道用于向上层模块返回ConfState实例。
    readyc( chan Ready类型): Ready结构体的功能在上一小节己经介绍过了, 该通道用于向上层模块返回Ready实例,即node.Ready()方法的返回值。
    advancec( chan struct{}类型):当上层模块处理完通过上述readyc通道获取到的Ready实例之后,会通过node.Advance()方法向该通道写入信号,从而通知底层raft实例。
    status ( chan chan Status类型): Status结构体在上一小节己经介绍过了, 注意该通道的类型, 其中传递的元素也是Channel类型,即node.Status()方法的返回值。
    tickc ( chan struct{}类型): 用来接收逻辑时钟发出的信号,之后会根据当前节点的角色推进选举计时器和心跳计时器。
    stop ( chan struct{}类型): 当node.Stop()方法被调用时,会向该通道发送信号, 在后续介绍中会提到, 有另一个goroutine会尝试读取该通道中的内容, 当读取到信息之后,会关闭done通道。
    done ( chan struct{}类型): 当检测到done通道关闭后,在其上阻塞的goroutine会继续执行,井进行相应的关闭操作

2、初始化

    当集群中的节点初次启动时会通过StartNode()函数启动创建对应的node实例和底层的raft实例。 在StartNode()方法中,主要是根据传入的Config配置创建raft实例并初始化raft的相关组件

func StartNode(c *Config, peers []Peer) Node {
	r := newRaft(c)//根据Config配置信息创建raft实例
	// become the follower at term 1 and apply initial configuration
	// entries of term 1
	r.becomeFollower(1, None)//切换成Follower状态,因为节点初次启动,所以任期号为l
	for _, peer := range peers {
	//根据传递的节点71]表,创建对应的ConfChange实例,其Type是ConfChangeAddNode,表示添加指定节点
		cc := pb.ConfChange{Type: pb.ConfChangeAddNode, NodeID: peer.ID, Context: peer.Context}
		d, err := cc.Marshal()
		if err != nil {
			panic("unexpected marshal error")
		}
		//将ConfChange记录序列化后的数据封装成EntryConfChange类型的Entry记录
		e := pb.Entry{Type: pb.EntryConfChange, Term: 1, Index: r.raftLog.lastIndex() + 1, Data: d}
		r.raftLog.append(e)
	}
	// Mark these initial entries as committed.
	// TODO(bdarnell): These entries are still unstable; do we need to preserve
	// the invariant that committed < unstable?
	r.raftLog.committed = r.raftLog.lastIndex()//直接修改raftLog.committed,提交上述EntryConfChange记录
	// Now apply them, mainly so that the application can call Campaign
	// immediately after StartNode in tests. Note that these nodes will
	// be added to raft twice: here and when the application's Ready
	// loop calls ApplyConfChange. The calls to addNode must come after
	// all calls to raftLog.append so progress.next is set after these
	// bootstrapping entries (it is an error if we try to append these
	// entries since they have already been committed).
	// We do not set raftLog.applied so the application will be able
	// to observe all conf changes via Ready.CommittedEntries.
	for _, peer := range peers {//为节点列表中的每个节点都创建对应的Progress实例
		r.addNode(peer.ID)//添加节点
	}

	n := newNode()//初始化node实例
	n.logger = c.Logger
	//启动一个goroutine,其中会根据底层raft的状态及上层模块传递的数据,协调处理node中各种远远的数据
	go n.run(r)
	return &n
}

当集群中的节点重新启动时,就不是通过StartNode()函数创建node 实例,而是调用RestartNode()函数,RestartNode()函数与StartNode()函数的实现非常类似,其中最主要的区别就是不会根据Config 中的配置信息调用raft.addNode()方法添加节点,因为这些信息会从Storage中恢复。

func RestartNode(c *Config) Node {
	r := newRaft(c)//创建底层封装的raft实例,该过程与StartNode()函数相同

	n := newNode()//创建node实例
	n.logger = c.Logger
	//启动一个goroutine,其中会根据底层raft的状态及上层模块传递的数据,协调处理node中各种通道的数据
	go n.run(r)
	return &n
}

3、run()方法

node.run()方法会处理node结构体中封装的全部通道,它也是node的核心。正是因为在该方法中会对多个通道进行处理,所以会将其中几个比较重要的通道处理过程单独介绍

func (n *node) run(r *raft) {
	var propc chan pb.Message//指向node.propc通道
	var readyc chan Ready//指向node.readyc通道
	var advancec chan struct{}//指向node. advancec通道
	//用于记录unstable中最后一条Entry记录的索引值和Term值
	var prevLastUnstablei, prevLastUnstablet uint64
	var havePrevLastUnstablei bool
	var prevSnapi uint64//用于记录快照中最后一条记录的索引值
	var rd Ready

	lead := None
	prevSoftSt := r.softState()//用于记录SoftState(当前Leader节点ID和当前节点的角色)
	prevHardSt := emptyState//用于记录HardState

	for {
		if advancec != nil {
		//上层模块还在处理上次从readye通道返回的Ready实例,所以不能继续向readye中写入数据
			readyc = nil
		} else {
			rd = newReady(r, prevSoftSt, prevHardSt)
			if rd.containsUpdates() {//根据Ready决定,是否存在需要交给上层模块处理的数据
				readyc = n.readyc
			} else {
				readyc = nil
			}
		}

		if lead != r.lead {//检测当前的Leader节点是否发生变化
			if r.hasLeader() {
				if lead == None {
					r.logger.Infof("raft.node: %x elected leader %x at term %d", r.id, r.lead, r.Term)
				} else {
					r.logger.Infof("raft.node: %x changed leader from %x to %x at term %d", r.id, lead, r.lead, r.Term)
				}
				propc = n.propc
			} else {////如采当前节点无法确定集群中的Leader节点,则清空propc,此次循环不再处理MsgPropc消息
				r.logger.Infof("raft.node: %x lost leader %x at term %d", r.id, lead, r.Term)
				propc = nil
			}
			lead = r.lead
		}

		select {
		// TODO: maybe buffer the config propose if there exists one (the way
		// described in raft dissertation)
		// Currently it is dropped in Step silently.
		case m := <-propc://读取propc通道,获取MsgPropc消息,并交给raft.Step()方法处理
			m.From = r.id
			r.Step(m)//raft.Step ()方法在前面已经详细分析过
		case m := <-n.recvc://读取node.recvc通道,获取消息(非MsgPrope类型),并交给raft.Step ()方法进行处理
			// filter out response message from unknown From.
			if pr := r.getProgress(m.From); pr != nil || !IsResponseMsg(m.Type) {//如来自未知节点的响应消息(例如, MsgHeartbeatResp类型消息)则会被过滤
				r.Step(m) // raft never returns an error
			}
		case cc := <-n.confc:
			if cc.NodeID == None {
				r.resetPendingConf()
				select {
				case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
				case <-n.done:
				}
				break
			}
			switch cc.Type {
			case pb.ConfChangeAddNode:
				r.addNode(cc.NodeID)
			case pb.ConfChangeAddLearnerNode:
				r.addLearner(cc.NodeID)
			case pb.ConfChangeRemoveNode:
				// block incoming proposal when local node is
				// removed
				if cc.NodeID == r.id {
					propc = nil
				}
				r.removeNode(cc.NodeID)
			case pb.ConfChangeUpdateNode:
				r.resetPendingConf()
			default:
				panic("unexpected conf type")
			}
			select {
			case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
			case <-n.done:
			}
		case <-n.tickc://逻辑时钟每推进一次, 就会向tickc远远写入一个信号
		//raft.tick ()在前面已经详细介绍过, Leader节点推进选举计时器, Follower节点推进心跳计时器
			r.tick()
		case readyc <- rd://将前面创建的Ready实例写入node.readyc通过中, 等待上层读取
			if rd.SoftState != nil {
				prevSoftSt = rd.SoftState//prevSoftSt 记录此次返回Ready实例的SoftState状态
			}
			if len(rd.Entries) > 0 {
			//记录此次返回的Ready实例是否包含待持久化的Entry,并记录最后一条记录的索引值和Termf直
				prevLastUnstablei = rd.Entries[len(rd.Entries)-1].Index
				prevLastUnstablet = rd.Entries[len(rd.Entries)-1].Term
				havePrevLastUnstablei = true
			}
			if !IsEmptyHardState(rd.HardState) {
				prevHardSt = rd.HardState//prevHardSt记录此次返回的Ready实例的HardState状态
			}
			if !IsEmptySnap(rd.Snapshot) {
				prevSnapi = rd.Snapshot.Metadata.Index//prevSnapi记录了此次返回的Ready实例中快照的元数据
			}

			r.msgs = nil//清空raft.msgs和raft.readStates
			r.readStates = nil
			//将advancec指向node.advancec通道,这样在下次for循环时, 就无法继续向上层模块返回Ready实例了(因为readye会被设立为nil,无法向readye通道中写入Ready实例)
			advancec = n.advancec
		case <-advancec://上层模块处理完Ready实例之后, 会向advance通道写入信号
			if prevHardSt.Commit != 0 {
				r.raftLog.appliedTo(prevHardSt.Commit)
			}
			if havePrevLastUnstablei {
				r.raftLog.stableTo(prevLastUnstablei, prevLastUnstablet)
				havePrevLastUnstablei = false
			}
			r.raftLog.stableSnapTo(prevSnapi)
			advancec = nil
		case c := <-n.status:
			c <- getStatus(r)
		case <-n.stop:
			close(n.done)
			return
		}
	}
}

通过上面的分析可知, 在向readyc通道写入Ready实例之后, 会使用pre*变量记录Ready实例中的相关状态信息, 在下次创建Ready实例时,会根据这些pre变量判断当前底层的raft实例与上次相比是否已经发生了变化, 并由此决定是否设置Ready实例的相应字段。

当上层模块通过readye通道读取Ready实例之后, 会将其中封装数据进行一系列处理,例如,上层模块会应用Ready. CommittedEntries中的Entry记录、持久化Ready.Entries 中的记录和快照数据等。当上层模块处理完此次返回的Ready 实例之后,会通过node.Advance()方法向node.advancec通道写入信号, 通知etcd-raft模块其此次返回的Ready实例己经被处理完成,node.run()方法中advancec通道的相关处理,其中会根据pre*记录的状态更新raftLog的相关字段, 具体实现如下:

func (n *node) run(r *raft) {
    var propc chan pb.Message
    var readyc chan Ready
    var advancec chan struct{}
    var prevLastUnstablei, prevLastUnstablet uint64
    var havePrevLastUnstablei bool
    var prevSnapi uint64
    var rd Ready

    lead := None
    prevSoftSt := r.softState()
    prevHardSt := emptyState

    for {
        if advancec != nil {
            readyc = nil
        } else {
            rd = newReady(r, prevSoftSt, prevHardSt)
            if rd.containsUpdates() {
                readyc = n.readyc
            } else {
                readyc = nil
            }
        }

        if lead != r.lead {
            if r.hasLeader() {
                if lead == None {
                    r.logger.Infof("raft.node: %x elected leader %x at term %d", r.id, r.lead, r.Term)
                } else {
                    r.logger.Infof("raft.node: %x changed leader from %x to %x at term %d", r.id, lead, r.lead, r.Term)
                }
                propc = n.propc
            } else {
                r.logger.Infof("raft.node: %x lost leader %x at term %d", r.id, lead, r.Term)
                propc = nil
            }
            lead = r.lead
        }

        select {

        case <-n.tickc:
            r.tick()
        case readyc <- rd:
            
        case <-advancec://当读取到advancec中的信号时,则表示上层模块已经处理完Ready实例
            if prevHardSt.Commit != 0 {//更新raftLog.applied字段(即已应用的记录位置)
                r.raftLog.appliedTo(prevHardSt.Commit)
            }
            if havePrevLastUnstablei {
            //Ready.Entries中的记录已经被持久化了,这里会将unstable中的对应记录清理掉,并更新其offset,也可能缩小unstable底层保存Entry记录的数纽
                r.raftLog.stableTo(prevLastUnstablei, prevLastUnstablet)
                havePrevLastUnstablei = false
            }
            //Ready中封装的快照数据已经被持久化,这里会清空unstable中记录的快照数据
            r.raftLog.stableSnapTo(prevSnapi)
            advancec = nil
        case c := <-n.status:
            c <- getStatus(r)
        case <-n.stop:
            close(n.done)
            return
        }
    }
}

当上层模块在处理待应用的Entry记录(即Ready.CommittedEntries 中保存的记录)时,会对Entry记录进行分类处理,上层模块会将EntryNormal类型的记录(对应一个普通的数据写入操作)应用到自身状态机中,而对于EntryConf℃hange类型的记录(对应一个集群配置更改操作, 例如, 添加或删除一个节点〉, 上层模块会将其封装成ConfChange实例并写入node.confc通道中,等待底层的raft实例进行处理。

发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/cyq6239075/article/details/105354930