【raft】学习七:etcd/raft节点启动流程

13 篇文章 0 订阅
8 篇文章 2 订阅

前言

在前面的raft学习中,探讨了基于etcd/raft的一些数据结构和raft的日志存储,以及Leader选举算法。随着对raft的使用和了解,本次将带着前面的学习,看看raft 节点的启动流程和一些准备工作,从而在使用raft时能够更加简单地将raft运用到我们的实际工作中。

准备工作

raft本身是一种共识算法,因而需要实体节点来运行协议并提供输入和输出。在etcd/raft中,RawNode是原生的 raft 节点的实体,里面包含了raft协议层和一些状态信息。

RawNode

type RawNode struct {
	raft       *raft
	prevSoftSt *SoftState
	prevHardSt pb.HardState
}

因此我们启动一个raft节点的时候会初始化一个RawNode:

func NewRawNode(config *Config) (*RawNode, error) {
// 初始化一个raft,这个raft主要是实现raft的协议
	r := newRaft(config)
	rn := &RawNode{
		raft: r,
	}
	rn.prevSoftSt = r.softState()
	rn.prevHardSt = r.hardState()
	return rn, nil
}

在使用etcd/raft模块时,可直接调用raft包下提供的两种方法:StartNode和RestartNode

// 方案一:启动时会检查同辈节点,如果同辈节点为空就会建议采用第二种方案
func StartNode(c *Config, peers []Peer) Node {
	if len(peers) == 0 {
		panic("no peers given; use RestartNode instead")
	}
	// 初始化一个实际节点,里面包含一个raft,实现具体的taft的协议
	rn, err := NewRawNode(c)
	if err != nil {
		panic(err)
	}
	// 在启动前会先加载配置,如果Storage接口方法返回非空这个方法将会返回错误,即初始化启动时Storage接口返回应该为空
	err = rn.Bootstrap(peers)
	if err != nil {
		c.Logger.Warningf("error occurred during starting a new node: %v", err)
	}
	// 包含一个rawNode,同时包含一些channel用于接受传入的消息,通过这个node将消息传入raft
	n := newNode(rn)

	go n.run()
	return &n
}
// 方案二: 是一种重启方案,不会去检查同辈节点,集群成员关系将会从Storage加载,Storage是raft提供给用户自定义持久化数据的接口,换言之我们需要在Storage中保存集群的成员关系
func RestartNode(c *Config) Node {
	rn, err := NewRawNode(c)
	if err != nil {
		panic(err)
	}
	n := newNode(rn)
	go n.run()
	return &n
}

raft协议到这里起始就已经初始化完成了,但是不可用。因为缺少输入和输出,还记得在前面的学习中,我们学习过Node接口,raft对于这个是接口提供了一个实现版本,这个node节点主要是提供一些输入输出的channel以及一些其他的控制变量。其实,启动raft节点可以简单理解为监听node中的输入输出的channel。

func newNode(rn *RawNode) node {
	return node{
		propc:      make(chan msgWithResult),
		recvc:      make(chan pb.Message),
		confc:      make(chan pb.ConfChangeV2),
		confstatec: make(chan pb.ConfState),
		readyc:     make(chan Ready),
		advancec:   make(chan struct{}),
		// make tickc a buffered chan, so raft node can buffer some ticks when the node
		// is busy processing raft messages. Raft node will resume process buffered
		// ticks when it becomes idle.
		tickc:  make(chan struct{}, 128),
		done:   make(chan struct{}),
		stop:   make(chan struct{}),
		status: make(chan chan Status),
		rn:     rn,
	}
}

初始化好这node实例,就可以开始启动raft节点了通过raft.run方法,因为需要循环监听应用层的输入以及raft向应用层传递的数据,这里开启一个协程去做这个工作。这种方式充分地应用golang的chan的特性,到这里raft节点算是启动完成了。

func (n *node) run() {
	var propc chan msgWithResult
	var readyc chan Ready
	var advancec chan struct{}
	var rd Ready

	r := n.rn.raft

	lead := None

	for {
		if advancec != nil {
			readyc = nil
		} else if n.rn.HasReady() {
			// Populate a Ready. Note that this Ready is not guaranteed to
			// actually be handled. We will arm readyc, but there's no guarantee
			// that we will actually send on it. It's possible that we will
			// service another channel instead, loop around, and then populate
			// the Ready again. We could instead force the previous Ready to be
			// handled first, but it's generally good to emit larger Readys plus
			// it simplifies testing (by emitting less frequently and more
			// predictably).
			// 拿到raft层的消息
			rd = n.rn.readyWithoutAccept()
			readyc = n.readyc
		}
		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 {
		// TODO: maybe buffer the config propose if there exists one (the way
		// described in raft dissertation)
		// Currently it is dropped in Step silently.
		case pm := <-propc:
			m := pm.m
			m.From = r.id
			// 通过node将消息传入raft协议层
			err := r.Step(m)
			if pm.result != nil {
				// 将结果返回给node层->具体节点
				pm.result <- err
				close(pm.result)
			}
		case m := <-n.recvc:
			// filter out response message from unknown From.
			if pr := r.prs.Progress[m.From]; pr != nil || !IsResponseMsg(m.Type) {
				r.Step(m)
			}
		case cc := <-n.confc:
			_, okBefore := r.prs.Progress[r.id]
			cs := r.applyConfChange(cc)
			// If the node was removed, block incoming proposals. Note that we
			// only do this if the node was in the config before. Nodes may be
			// a member of the group without knowing this (when they're catching
			// up on the log and don't have the latest config) and we don't want
			// to block the proposal channel in that case.
			//
			// NB: propc is reset when the leader changes, which, if we learn
			// about it, sort of implies that we got readded, maybe? This isn't
			// very sound and likely has bugs.
			if _, okAfter := r.prs.Progress[r.id]; okBefore && !okAfter {
				var found bool
			outer:
				for _, sl := range [][]uint64{cs.Voters, cs.VotersOutgoing} {
					for _, id := range sl {
						if id == r.id {
							found = true
							break outer
						}
					}
				}
				if !found {
					propc = nil
				}
			}
			select {
			case n.confstatec <- cs:
			case <-n.done:
			}
		case <-n.tickc:
			n.rn.Tick()
		case readyc <- rd:
			// 这里只是负责将消息传给应用层,并不处理消息
			n.rn.acceptReady(rd)

			advancec = n.advancec
		case <-advancec:
			// 交给RawNode处理消息
			n.rn.Advance(rd)
			// 表表示应用已经处理好Ready中的数据,告知raft可以开始接收新的消息
			rd = Ready{}
			advancec = nil
		case c := <-n.status:
			c <- getStatus(r)
		case <-n.stop:
			close(n.done)
			return
		}
	}
}

到这里整个raft算是启动成功,启动的第一件事情就是开始选主。你会发现单单调用一个启动方法,会面临许多问题导致启动实例不成功。下面我们主要来看看第二种方案启动的流程,一般用的比较多的也是这种方式。

newRaft

RestartNode和Start的主要区别在配置加载的方式,以及启动的要求具有差异。先从newRaft方法作为切入点,看看启动raft需要哪些?

func newRaft(c *Config) *raft {
	if err := c.validate(); err != nil {
		panic(err.Error())
	}
	// 实例化raftLog,这个结构我们在学习五中了解了,主要是保存raft的日志数据
	raftlog := newLogWithSize(c.Storage, c.Logger, c.MaxCommittedSizePerReady)
	// 从Storage中加载配置,则需要我们需要初始化好整个持久化的代码
	hs, cs, err := c.Storage.InitialState()
	if err != nil {
		panic(err) // TODO(bdarnell)
	}

	r := &raft{
		id:                        c.ID,
		lead:                      None,
		isLearner:                 false,
		raftLog:                   raftlog,
		maxMsgSize:                c.MaxSizePerMsg,
		maxUncommittedSize:        c.MaxUncommittedEntriesSize,
		prs:                       tracker.MakeProgressTracker(c.MaxInflightMsgs),
		electionTimeout:           c.ElectionTick,
		heartbeatTimeout:          c.HeartbeatTick,
		logger:                    c.Logger,
		checkQuorum:               c.CheckQuorum,
		preVote:                   c.PreVote,
		readOnly:                  newReadOnly(c.ReadOnlyOption),
		disableProposalForwarding: c.DisableProposalForwarding,
	}
	// 重新加载配置,
	cfg, prs, err := confchange.Restore(confchange.Changer{
		Tracker:   r.prs,
		LastIndex: raftlog.lastIndex(),
	}, cs)
	if err != nil {
		panic(err)
	}
	assertConfStatesEquivalent(r.logger, cs, r.switchToConfig(cfg, prs))

	if !IsEmptyHardState(hs) {
		// 保存了Commit Vote Term
		r.loadState(hs)
	}
	if c.Applied > 0 {
		raftlog.appliedTo(c.Applied)
	}
	// 初始化为follower节点
	r.becomeFollower(r.Term, None)

	var nodesStrs []string
	// 参与投票的节点,其中的节点是从Storage中加载上来的
	for _, n := range r.prs.VoterNodes() {
		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
}

从newRaft方法中得知,主要做如下几件事:
1. 实例化raftLog以保存日志;
2. 从持久化中加载的配置;
3. 应用加载的配置;
4. 应用到上次的应用的Index;
5. 初始化当前节点为follower节点;
6. 打印初始化后的raft集群信息。

// raft 状态配置
type ConfState struct {
	// The voters in the incoming config. (If the configuration is not joint,
	// then the outgoing config is empty).
	Voters []uint64 `protobuf:"varint,1,rep,name=voters" json:"voters,omitempty"`
	// The learners in the incoming config.
	Learners []uint64 `protobuf:"varint,2,rep,name=learners" json:"learners,omitempty"`
	// The voters in the outgoing config.
	VotersOutgoing []uint64 `protobuf:"varint,3,rep,name=voters_outgoing,json=votersOutgoing" json:"voters_outgoing,omitempty"`
	// The nodes that will become learners when the outgoing config is removed.
	// These nodes are necessarily currently in nodes_joint (or they would have
	// been added to the incoming config right away).
	LearnersNext []uint64 `protobuf:"varint,4,rep,name=learners_next,json=learnersNext" json:"learners_next,omitempty"`
	// If set, the config is joint and Raft will automatically transition into
	// the final config (i.e. remove the outgoing config) when this is safe.
	AutoLeave bool `protobuf:"varint,5,opt,name=auto_leave,json=autoLeave" json:"auto_leave"`
}
func (s *raftStorage) InitialState() (pb.HardState, pb.ConfState, error) {
	hs := s.wal.InitialState()
	return hs, s.cs, nil
}

通过这个接口可以知道,我们在启动节点之前需要先初始化这个ConfState。这样我们在加载的时候才能够正常的启动,如果peer为空由于无法完成选主。。就会导致启动阻塞或者失败,具体取决于应用层的处理方式。

小结

本次我们顺着etcd/raft的逻辑,学习了一个raft节点的启动流程,配置的加载以及一些初始化工作。主要流程如下:

  1. 初始化应用层自定义的Storage实体类;
  2. 调用RestartNode方法;
  3. 在RestartNode中,实例化一个RawNode,并包装为Node;
  4. 在NewRawNode中,实例化一个RawNode,并实例化raft,将raft包装为RawNode,返回一个RawNode;
  5. 这样raft的核心工作就已经准备完成,最终返回一个Node接口,用于获取raft层的数据以及向raft层数据传入数据。

还有很多细节值得探讨,建议对raft感兴趣的朋友可以在自己业余时间,读读源码,整个过程可能艰难因人而异。不过也有很多大佬针对源码做了思路上的解读,可以找来看看。总的来说,学习raft源码结合raft论文的学习,能够从理论到实践层面对一致性算法的有更好地理解与把握。除此之外,在学习源码的过程中对Golang的channel机制,会有更深刻的认知。整个raft的消息传递,将channel用得出神入化。好啦,今天的分享就这么多啦!

=-=-=-=-=-=-=-=-=-=-撒花!=-=-=-=-=-=-=-=-=-=-=

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值