etcd系列-----raft协议:重要数据结构介绍(raftlog、raft)

raftlog结构体

Raft 协议中日志复制部分的核心就是在集群中各个节点之间完成日志的复制,因此在raft模块的实现中使用raftLog结构来管理节点上的日志, 它依赖于前面介绍的Storage接口和unstable结构体。 raftLog中主要字段的含义与功能

storage ( Storage类型):实际上就是前面介绍的MemoryStorage实例,其中存储了快照数据及该快照之后的Entry记录。
    :unstable ( unstable类型):用于存储未写入Storage的快照数据及Entry记录,在前面己经对unstable做了详细介绍。
    :committed ( uint64类型): 己提交的位置,即己提交的Entry记录中最大的索引值。
    :applied ( uint64类型):己应用的位置,即己应用的Entry记录中最大的索引值。其中committed和applied之间始终满足committed<=applied这个不等式关系。

当Follower节点或Candidate节点需要向raftLog 中追加Entry记录时,会通过raft.handleAppendEntriesO方法调用raftLog.maybeAppend()方法完成追加Entry记录的功能:

func (r *raft) handleAppendEntries(m pb.Message) {
	if m.Index < r.raftLog.committed {
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
		return
	}

	if mlastIndex, ok := r.raftLog.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...); ok {
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: mlastIndex})
	} else {
		r.logger.Debugf("%x [logterm: %d, index: %d] rejected msgApp [logterm: %d, index: %d] from %x",
			r.id, r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(m.Index)), m.Index, m.LogTerm, m.Index, m.From)
		r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: m.Index, Reject: true, RejectHint: r.raftLog.lastIndex()})
	}
}

func (l *raftLog) maybeAppend(index, logTerm, committed uint64, ents ...pb.Entry) (lastnewi uint64, ok bool) {
    //调用matchTerm()方法检测MsgApp消息的Index 字段及LogTerm字段是否合法
	if l.matchTerm(index, logTerm) {
		lastnewi = index + uint64(len(ents))
		//遍历待追加的Entry集合,查找是否与ra负Log中己有的Entry发生冲突(Index相同但Term不同)
		ci := l.findConflict(ents)
		switch {
		case ci == 0://findConflict ()方法返回0时,表示raftLog中已经包含了所有待追加的Entry记录,不必进行任何追加操作
		case ci <= l.committed://如采出现冲突的位置是己提交的记录,则输出异常日志并终止整个程序
			l.logger.Panicf("entry %d conflict with committed entry [committed(%d)]", ci, l.committed)
		default://如果冲突位置是未提交的部分
			offset := index + 1
			//则将ents中未发生冲突的部分追加到raftLog中
			l.append(ents[ci-offset:]...)
		}
		l.commitTo(min(committed, lastnewi))
		return lastnewi, true
	}
	return 0, false
}

raftLog.append()方法主要通过调用unstable.truncateAndAppend()方法完成记录的追加功能,其实现如下:

func (l *raftLog) append(ents ...pb.Entry) uint64 {
	if len(ents) == 0 {
		return l.lastIndex()
	}
	if after := ents[0].Index - 1; after < l.committed {
		l.logger.Panicf("after(%d) is out of range [committed(%d)]", after, l.committed)
	}
	//调用unstable.truncateAndAppend()方法将Entry记录追加到unstable中
	l.unstable.truncateAndAppend(ents)
	return l.lastIndex()
}

通过commitTo()方法更新ra负Log.committed宇段:

func (l *raftLog) commitTo(tocommit uint64) {
	// never decrease commit
	if l.committed < tocommit {//raftLog.committed字段只能后移,不能前移
		if l.lastIndex() < tocommit {
			l.logger.Panicf("tocommit(%d) is out of range [lastIndex(%d)]. Was the raft log corrupted, truncated, or lost?", tocommit, l.lastIndex())
		}
		//更新committed字段
		l.committed = tocommit
	}
}

raftLog.firstindex()和lastlndex()方法与前面介绍的MemoryStorage的firstlndex()和lastlndex()方法类似,它们返回的是raftLog中第一条和最后一条Entry记录的索引值。这两个方法分别先尝试通过unstable.maybeFirstindex()和maybeLastlndex()方法进行查找,如果查找失败,则通过Storage.Firstlndex()和Lastlndex()方法进行查找.

当上层模块需要从raftLog获取Entry记录进行处理时,会先调用hasNextEnts()方法检测是否有待应用的记录,然后调用nextEnts()方法将己提交且未应用的Entry记录返回给上层模块处理。

在前面介绍Raft协议时提到过,Follower 节点在接收到Candidate节点的选举请求之后,会通过比较Candidate节点的本地日志与自身本地日志的新旧程度,从而决定是否投票。raftLog提供了isUpToDat巳()方法用于比较日志的新旧程度.

//首先注意一下参数,lasti和term分别是Candidate节点的最大记录索引位和最大任期号(即 MsgVote 请求( Candidate发送的选举请求)携带的Index和LogTerm)
func (l *raftLog) isUpToDate(lasti, term uint64) bool {
       //比较日志新旧的方式也如前面介绍的一样,先比较任期号,任其月号相同时再比较索引值
	return term > l.lastTerm() || (term == l.lastTerm() && lasti >= l.lastIndex())
}

raft

前面介绍了raft中使用到的组件,即将介绍ra位中的方法, 也是raft模块的核心实现。

1、初始化

func newRaft(c *Config) *raft {
	if err := c.validate(); err != nil {
		panic(err.Error())
	}
	raftlog := newLog(c.Storage, c.Logger)
	hs, cs, err := c.Storage.InitialState()
	if err != nil {
		panic(err) // TODO(bdarnell)
	}
	peers := c.peers
	learners := c.learners
	if len(cs.Nodes) > 0 || len(cs.Learners) > 0 {
		if len(peers) > 0 || len(learners) > 0 {
			// TODO(bdarnell): the peers argument is always nil except in
			// tests; the argument should be removed and these tests should be
			// updated to specify their nodes through a snapshot.
			panic("cannot specify both newRaft(peers, learners) and ConfState.(Nodes, Learners)")
		}
		peers = cs.Nodes
		learners = cs.Learners
	}
	r := &raft{
		id:                        c.ID,
		lead:                      None,
		isLearner:                 false,
		//负责管理Entry记录的raftLog实例每条消息的最大字节数,如采是math.MaxUint64则没有上限如采是0则表示每条消息最多携带一条Entry
		raftLog:                   raftlog,
		maxMsgSize:                c.MaxSizePerMsg,
		maxInflight:               c.MaxInflightMsgs,
		prs:                       make(map[uint64]*Progress),
		learnerPrs:                make(map[uint64]*Progress),
		electionTimeout:           c.ElectionTick,
		heartbeatTimeout:          c.HeartbeatTick,
		logger:                    c.Logger,
		checkQuorum:               c.CheckQuorum,
		preVote:                   c.PreVote,
		readOnly:                  newReadOnly(c.ReadOnlyOption),
		disableProposalForwarding: c.DisableProposalForwarding,
	}
	//初始化raft.prs字段,这里会根据集群中节点的ID,为每个节点初始化Progress实例,在Progress中维护了对应节点的NextIndex值和Matchindex值, 以及一些其他的Follower节点信息注意:只有Leader节点的raft.prs字段是有效的
	for _, p := range peers {
		r.prs[p] = &Progress{Next: 1, ins: newInflights(r.maxInflight)}
	}
	for _, p := range learners {
		if _, ok := r.prs[p]; ok {
			panic(fmt.Sprintf("node %x is in both learner and peer list", p))
		}
		r.learnerPrs[p] = &Progress{Next: 1, ins: newInflights(r.maxInflight), IsLearner: true}
		if r.id == p {
			r.isLearner = true
		}
	}
    //根据从Storage中获取的HardState,初始化raftLog.committed字段,以及raft.Term和Vote字段
	if !isHardStateEqual(hs, emptyState) {
		r.loadState(hs)
	}
	//如采Config中国己置了Applied,则将raftLog.applied字段重直为指定的Applied值上层模块自己的控制正确的己应用位置时使用该配置
	if c.Applied > 0 {
		raftlog.appliedTo(c.Applied)
	}
	//当前节点切换成Follower状态
	r.becomeFollower(r.Term, None)

	var nodesStrs []string
	for _, n := range r.nodes() {
		nodesStrs = append(nodesStrs, fmt.Sprintf("%x", n))
	}

	r.logger.Infof("newRaft %x [peers: [%s], term: %d, commit: %d, applied: %d, lastindex: %d, lastterm: %d]",
		r.id, strings.Join(nodesStrs, ","), r.Term, r.raftLog.committed, r.raftLog.applied, r.raftLog.lastIndex(), r.raftLog.lastTerm())
	return r
}

2、切换状态

在newRaft()函数中完成初始化之后, 会调用becomeFollower()方法将节点切换成Follower状态,其中会设置raft实例的多个字段

func (r *raft) becomeFollower(term uint64, lead uint64) {
	r.step = stepFollower
	r.reset(term)            //重置raft实例Term, Vote等字段
	r.tick = r.tickElection
	r.lead = lead
	r.state = StateFollower
	r.logger.Infof("%x became follower at term %d", r.id, r.Term)
}
func (r *raft) reset(term uint64) {
	if r.Term != term {
		r.Term = term
		r.Vote = None
	}
	r.lead = None

	r.electionElapsed = 0
	r.heartbeatElapsed = 0
	r.resetRandomizedElectionTimeout()

	r.abortLeaderTransfer()

	r.votes = make(map[uint64]bool)
	//重直prs, 其中每个Progress中的Next设置为raftLog.lastindex
	r.forEachProgress(func(id uint64, pr *Progress) {
		*pr = Progress{Next: r.raftLog.lastIndex() + 1, ins: newInflights(r.maxInflight), IsLearner: pr.IsLearner}
		if id == r.id {
			pr.Match = r.raftLog.lastIndex()//将当前节点对应的prs.Match设置成lastIndex
		}
	})

	r.pendingConf = false
	r.readOnly = newReadOnly(r.readOnly.option)//只读请求的相关设立
}

当节点变成Follower状态之后,会周期性地调用raft.tickElection()方法推进electionElapsed并检测是否超时,具体实现如下:

func (r *raft) tickElection() {
	r.electionElapsed++

	if r.promotable() && r.pastElectionTimeout() {
		r.electionElapsed = 0
		r.Step(pb.Message{From: r.id, Type: pb.MsgHup})//发起选举
	}
}

当Candidate 节点得到集群中半数以上节点的选票时,会调用becomeLeader()方法切换成Leader状态,becomeLeader()方法的具体实现

func (r *raft) becomeLeader() {
	// TODO(xiangli) remove the panic when the raft implementation is stable
	if r.state == StateFollower {
		panic("invalid transition [follower -> leader]")
	}
	r.step = stepLeader
	r.reset(r.Term)
	r.tick = r.tickHeartbeat
	r.lead = r.id
	r.state = StateLeader
	//获取当前节点中所有未提交的Entry记录,异常处理(咯)
	ents, err := r.raftLog.entries(r.raftLog.committed+1, noLimit)
	if err != nil {
		r.logger.Panicf("unexpected error getting uncommitted entries (%v)", err)
	}

	nconf := numOfPendingConf(ents)
	if nconf > 1 {
		panic("unexpected multiple uncommitted config entry")
	}
	if nconf == 1 {
		r.pendingConf = true
	}

	r.appendEntry(pb.Entry{Data: nil})//向当前节点的raftLog中追加一条空的Entry记录
	r.logger.Infof("%x became leader at term %d", r.id, r.Term)
}

 raft.appendEntry()方法,它的主要操作步骤如下:
    (l) 设置待追加的Entry记录的Term值和Index值。
    (2) 向当前节点的raftLog中追加Entry记录。
    (3)更新当前节点对应的Progress 实例。
    (4)尝试提交Entry记录, 即修改raftLog.committed字段的值。

如果该Entry记录己经复制到了半数以上的节点中,则在raft.maybeCommit()方法中会尝试将其提交。除了appendEntry()方法,在Leader 节点每次收到MsgAppResp 消息时也会调用maybeCommit()方法, maybeCommit()方法的具体实现如下

func (r *raft) maybeCommit() bool {
	// TODO(bmizerany): optimize.. Currently naive
	//将集群中所有节点对应的Progress.Match字段复制到mis切片中
	mis := make(uint64Slice, 0, len(r.prs))
	for _, p := range r.prs {
		mis = append(mis, p.Match)
	}
	 //对这些Match位进行排序
     //raft.quorum ()方法返回的位是集群节点的半数+1,这里举个例子:
     //如采节点数量为5, r.quorum()-1=2,则可以找到mis切片中下标为2的节点对应的Match值
     //该值之前的Entry记录都是可以提交的,因为节点0、 1、 2三个节点(起半数)已经复制了该记录
	sort.Sort(sort.Reverse(mis))
	mci := mis[r.quorum()-1]
	return r.raftLog.maybeCommit(mci, r.Term)//史新raftLog.committed字段,完成提交
}
发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

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