浅谈Etcd中Raft库的实现

一、Etcd Raft介绍
		· Raft一种用于集群维护一个复制状态机的协议,状态机通过复制日志的形式来保持同步;

		· Etcd的Raft库中提供了一个核心的Raft算法,没有存储处理、消息序列化、网络传输等工程应用;
			网络和磁盘的IO都由用户另外实现,也就是需要自己实现传输层;并且,持久化Raft日志和状态也需要用户自己实现;

		· Raft的响应必须是能确定的,因此库中会将Raft建模为一个状态机;
		  状态机接收Message作为输入;而输出是{[]Massage,[]LogEntries,NextState}的三元组,由Message数组、log entries、和Raft state changes组成;
			状态机对于相同的输入,也会输出相同的输出;

		· 特性:
				· Raft库是基于Raft协议实现的,所以也会包含Raft协议的核心功能:
					1. Leader election(领导人选举)
					2. Log replication(日志复制)
				  3. Log compaction(日志压缩,快照功能)
					4. Membership Changes(集群成员变更)
					5. Leadership transfer extension(Leader变更扩展)
					6. Leader和Follower都提供高效的线性只读查询;
							a. Leader在处理只读操作时不会通过log的形式记录,会通过quorum(法定人数)检查来保证自己的commitIndex没有过期
							b. Follower在处理只读请求时会知道哪些entry是committed(安全的)
				
				· 同时还支持一些可选的优化方案
					1. pipeline:用于减少日志复制的延迟(流式的向Follower发送entries,不需要等Follower响应);
					2. 在支持pipeline的情况下会通过记录安全的Index来保证必要的重传(Flow control)
					3. Batch:批处理Raft消息,减少网络IO调用;
					4. Batch:批处理Raft写入,减少磁盘IO调用;
					5. 并行写入:允许Leader一边写入log[]一边发送Entries
					6. 内置的Follower到Leader重定向;
					7. 失去quorum的Leader会主动下位;
					8. 使用快照防止日志无限增长;

一、Entry
### https://github.com/etcd-io/etcd/blob/main/raft/raftpb/raft.pb.go#L267		
		· 从整体来说,一个集群中的每个节点都是一个状态机,而Raft管理的就是对这个状态机进行更改的一些操作,这些操作会被封装为一个个Entry;
      也就是之前Raft中提到的logEntry;
      在Etcd中被定义为:
        	type Entry struct {
          	Term  uint64    `protobuf:"varint,2,opt,name=Term" json:"Term"`
        		Index uint64    `protobuf:"varint,3,opt,name=Index" json:"Index"`
        		Type  EntryType `protobuf:"varint,1,opt,name=Type,enum=raftpb.EntryType" json:"Type"`
        		Data  []byte    `protobuf:"bytes,4,opt,name=Data" json:"Data,omitempty"`
    			}
			Term是该entry是什么Term的Leader发布的entry;
			Index是该entry在log[]中的应该位于的Index;
			Type指的是EntryType,EntryType是int32的别名,EntryType包含Normal(0)、ConfChange(1)、ConfChangeV2(2);
			ConfChangeV2支持joint consensus;
			Data是实际的数据,里面记录着一些K-V的数据(因为对于状态机来说状态就是K-V形式的);
					
		· ConfChangeV2支持单节点变更,也支持任意个节点变更的joint consensus;
			ConfChangeV2的结构为:
					type ConfChangeV2 struct {
						Transition ConfChangeTransition `protobuf:"varint,1,opt,name=transition,enum=raftpb.ConfChangeTransition" json:"transition"`
						Changes    []ConfChangeSingle   `protobuf:"bytes,2,rep,name=changes" json:"changes"`
						Context    []byte               `protobuf:"bytes,3,opt,name=context" json:"context,omitempty"`
					}
			Transition ConfChangeTransition用于指定如何/是否使用joint consensus;
			ConfChangeSingle是一个单独的配置更改操作;

		· ConfChange是配置变更的信息(单节点变更不需要联合配置信息)
			ConfChange的结构为:
					type ConfChange struct {
						Type    ConfChangeType `protobuf:"varint,2,opt,name=type,enum=raftpb.ConfChangeType" json:"type"`
						NodeID  uint64         `protobuf:"varint,3,opt,name=node_id,json=nodeId" json:"node_id"`
						Context []byte         `protobuf:"bytes,4,opt,name=context" json:"context,omitempty"`
						ID uint64 `protobuf:"varint,1,opt,name=id" json:"id"`
					}
			ConfChangeType中包括AddNode、RemoveNode、UpdateNode、AddLearnerNode;


			
二、Message
### https://github.com/etcd-io/etcd/blob/main/raft/raftpb/raft.pb.go#L384
		· Message是作为Raft节点之间通讯的载体存在,Message涵盖了各种消息所需的字段;
		  接收端只需要根据Type字段来判断Message的类型,用对于的结构体接收即可;
			Message在库中被定义为:
					type Message struct {
						Type MessageType `protobuf:"varint,1,opt,name=type,enum=raftpb.MessageType" json:"type"`
						To   uint64      `protobuf:"varint,2,opt,name=to" json:"to"`
						From uint64      `protobuf:"varint,3,opt,name=from" json:"from"`
						Term uint64      `protobuf:"varint,4,opt,name=term" json:"term"`
						LogTerm    uint64   `protobuf:"varint,5,opt,name=logTerm" json:"logTerm"`
						Index      uint64   `protobuf:"varint,6,opt,name=index" json:"index"`
						Entries    []Entry  `protobuf:"bytes,7,rep,name=entries" json:"entries"`
						Commit     uint64   `protobuf:"varint,8,opt,name=commit" json:"commit"`
						Snapshot   Snapshot `protobuf:"bytes,9,opt,name=snapshot" json:"snapshot"`
						Reject     bool     `protobuf:"varint,10,opt,name=reject" json:"reject"`
						RejectHint uint64   `protobuf:"varint,11,opt,name=rejectHint" json:"rejectHint"`
						Context    []byte   `protobuf:"bytes,12,opt,name=context" json:"context,omitempty"`
					}

		· 参数:
				Type:		Message的类型,包括Vote、Heartbeat、App(end)等等;
				To:			接收者;
				From:		发送者;
				Term: 		任期;
				LogTerm:	prevLogTerm,如果是Type=MsgVote,发送者LastLogTerm;
				Index:		prevLogIndex,如果是Type=MsgVote,则表示为发送者LastLogIndex;
				Entries:	发送的entries;
				Commit:	已提交的日志Index;
				Snapshot:如果是MsgSnap,就在这个字段存放快照;
				Reject、RejectHint:返回message字段;
				Context:	运维相关的上下文信息,用于跟踪调试;
		
		· 其中Snapshot是分块发送,在结构体中也能看出这点:
					type Snapshot struct {
						Data     []byte           `protobuf:"bytes,1,opt,name=data" json:"data,omitempty"`
						Metadata SnapshotMetadata `protobuf:"bytes,2,opt,name=metadata" json:"metadata"`
					}
					type SnapshotMetadata struct {
						ConfState ConfState `protobuf:"bytes,1,opt,name=conf_state,json=confState" json:"conf_state"`
						Index     uint64    `protobuf:"varint,2,opt,name=index" json:"index"`
						Term      uint64    `protobuf:"varint,3,opt,name=term" json:"term"`
					}
					type ConfState struct {
						Voters []uint64 `protobuf:"varint,1,rep,name=voters" json:"voters,omitempty"`
						Learners []uint64 `protobuf:"varint,2,rep,name=learners" json:"learners,omitempty"`
						VotersOutgoing []uint64 `protobuf:"varint,3,rep,name=voters_outgoing,json=votersOutgoing" json:"voters_outgoing,omitempty"`
						LearnersNext []uint64 `protobuf:"varint,4,rep,name=learners_next,json=learnersNext" json:"learners_next,omitempty"`
						AutoLeave bool `protobuf:"varint,5,opt,name=auto_leave,json=autoLeave" json:"auto_leave"`
					}
			Snapshot.Data中是快照的内容;
			Metadata中是记录的快照块的偏移量和Term;
			ConfState:记录的配置状态


四、log_unstable.go	
### https://github.com/etcd-io/etcd/blob/main/raft/log_unstable.go#L23
		· 经过Raft的学习,可以得知,状态机的状态被通过Snapshot + Log来保存;
		  log[]的表示形式就是unstable结构,也是用于还没有被用户持久化的数据:
					type unstable struct {
						snapshot *pb.Snapshot
						entries []pb.Entry
						offset  uint64
						logger Logger
					}
			unstable中由两部分组成,一个是snapshot快照,一个是logEntires;
			entries表示要进行操作的日志条目,但是因为不可能让log无限增长,会在特定的情况下,将log的部分清空并转化为Snapshot;
			offset用于记载哪些部分的entries被转化为了Snapshot,因为当一个新的节点加入集群,Leader需要将所有数据都同步到这个节点;
			只需要将Snapshot发送给new Follower,然后Follower在快照的基础上应用Entries;


五、stroage.go
### https://github.com/etcd-io/etcd/blob/main/raft/storage.go#L46
		· storage.go中定义了一个Storage接口,因为Etcd中raft库不支持内置持久化,
		  所以只向外界应用暴露了一个持久化接口,当外界应用实现了这个接口,就可以从中外界获取日志;
					type Storage interface {
						// 获取保存了的HardState和ConfState信息
						InitialState() (pb.HardState, pb.ConfState, error)
						// 获取一个entry切片,范围是[lo,hi]
						Entries(lo, hi, maxSize uint64) ([]pb.Entry, error)
						// 返回entry i的Term
						Term(i uint64) (uint64, error)
						// 返回lastLogIndex
						LastIndex() (uint64, error)
						// 返回可以通过Entries可用的Index(因为之前的可能被放入Snapshot中了,详见unstable)
						FirstIndex() (uint64, error)
						// 返回最近的Snapshot
						Snapshot() (pb.Snapshot, error)
					}
			除此之外,还提供了内存版本的Storage实例,即	MemoryStorage实现了Storage接口;
					type MemoryStorage struct {
						sync.Mutex
						hardState pb.HardState
						snapshot  pb.Snapshot
						ents []pb.Entry
					}


六、log.go
### https://github.com/etcd-io/etcd/blob/main/raft/log.go#L24
		· 上面介绍了unstable和stroage,而raftlog承当了raft日志的所有操作;
			raftlog由上面的两个元素组成,除此之还会有一些数据来记录当前的信息;
					type raftLog struct {
						storage Storage
						unstable unstable
						committed uint64
						applied uint64
						logger Logger
						maxNextEntsSize uint64
					}
			storage:可用获取已经持久化数据的Stroage;
			unstable:用于保存还没有持久化的数据;
			committed:用于保存当前已提交的最高Index;
			applied:保存当前应用于状态机的最高Index;
			因为一条entry要被提交后才会被应用,所以 applied <= committed;

		· 所以Raft算法中Log[]的实际表现形式应该为:
			MemoryStorage.snapshot + MemoryStorage.ents + unstable.entries
			从newLog()方法就能看出来;
					func newLog(storage Storage, logger Logger) *raftLog {
						return newLogWithSize(storage, logger, noLimit)
					}
					func newLogWithSize(storage Storage, logger Logger, maxNextEntsSize uint64) *raftLog {
						if storage == nil {
							log.Panic("storage must not be nil")
						}
						log := &raftLog{
							storage:         storage,
							logger:          logger,
							maxNextEntsSize: maxNextEntsSize,
						}
						firstIndex, err := storage.FirstIndex()
						if err != nil {
							panic(err) // TODO(bdarnell)
						}
						lastIndex, err := storage.LastIndex()
						if err != nil {
							panic(err) // TODO(bdarnell)
						}
						log.unstable.offset = lastIndex + 1
						log.unstable.logger = logger
						log.committed = firstIndex - 1
						log.applied = firstIndex - 1
						return log
					}

		· 一个raftlog,需要一个Stroage来构建;
			首先会初始化一个raftlog,将给出的数据添加进对应字段;
			然后根据Storage的FirstIndex和LastIndex来构建unstable;
			而unstable.snapshot不会在newLog()中构建,
			会在启动之后同步快照数据时才去进行复赋值修改的数据


七、progress.go
### https://github.com/etcd-io/etcd/blob/main/raft/tracker/progress.go#L30
		· Leader通过Progress这个数据结构来跟踪一个Follower的状态,
			并根据Progress里的信息来决定每次同步的logEntry;
					type Progress struct {
						Match, Next uint64
						State StateType
						PendingSnapshot uint64
						RecentActive bool
						ProbeSent bool
						Inflights *Inflights
						IsLearner bool
					}
			Match、Next:MaxtchIndex和NextIndex
			State:表示Leader应该如何与Followerr交互,
						 StateProbe 表示需要确定next和match(节点下线一段时间重新获取进度),
						 StateReplicate 表示允许pipeline的方式发送,
						 StateSnapshot 表示需要发送快照;
			PendingSnapshot:因为Snapshot是分块传输的,所以会用一个offset来记录传输Snapshot的进度;
			RecentActive:表示是否活跃,没有time out;
			Inflights表示一个流量控制的buf窗口:因为三个state一个发送周期内的可发送的mag不一样,所以需要控制;
			Progress在机器中是这样使用的:
					type Status struct {
						BasicStatus
						Config   tracker.Config
						Progress map[uint64]tracker.Progress
					}
			只有Leader会拥有Progress,以一个map的形式存储

		· 状态机的变化:
			1. 收到receives msgAppResp(rej=false && index > match);
				 说明此次同步失败,并且想要的index 大于 Match,就把Match变为index,然后把Next = Match+1;
				 然后State从 StateProbe 置为 StateReplicate,开始发送Next的entry
			
			2. 收到receives msgAppResp(rej=true);
				 说明一个下线一段时间的节点需要发送entry,但是Leader不知道需要发送哪一个Index的entry;
				 所以State会从 StateReplicate 变为 StateProbe,开始商议应该发送哪一个;

			3. 发送了snapshot会从 StateProbe 变为 StateSnapshot;
			   表示正在发送快照;

			4. 收到snapshot success,说明快照同步成功,Next会变为Snapshot的Index+1,
				 也就是要发送Snapshot的下一个entry,State从 StateSnapshot 变为 StateProbe;
			
			5. 收到snapshot failure,说明快照同步失败,
				 StateSnapshot 变为 StateProbe,重新准备发送;
				
			6. 收到receives msgAppResp(rej=false && index > lastsnap.index);
			   说明同步失败,Follower想要收到的Index大于snapshot所包含的最后一条entry的Index;
				 将Match置为Index,Next置为Match+1,StateSnapshot 变为 StateProbe,准备发送其他entries;


八、raft.go
### https://github.com/etcd-io/etcd/blob/main/raft/raft.go
		· raft协议的逻辑部分在raft.go中被定义,其中驱动逻辑位于Step();
			raft节点收到的消息可能是来自与其他节点,也可能来自于上层的应用和下层的存储;
			所有的消息都会被转化位massage类型;
			使用一系列的swich/case、if/else来判断收到的mag的Type,并对不同type做出不同的响应(send response);
			### https://github.com/etcd-io/etcd/blob/main/raft/raft.go#L847
			最后需要处理raft节点状态的msg用default来将回收;
					default:
							err := r.step(r, m)
							if err != nil {
								return err
					}
			可以发现最后是使用了step字段对应的 stepLeader/stepCandidate/stepFollower 函数来处理具体的msg;

		· raft的定时器 tick和step字段相似,也是一个函数指针,根据角色的不同,对应 tickHeartbeat/tickElection;
		


九、node.go
### https://github.com/etcd-io/etcd/blob/main/raft/node.go
		· node的主要作用就是把应用层的数据和raft协议层的衔接起来;
		  将应用层的消息传递给raft协议层,通过协议层的处理,将处理结果在返回给应用层;
					type node struct {
						propc      chan msgWithResult
						recvc      chan pb.Message
						confc      chan pb.ConfChangeV2
						confstatec chan pb.ConfState
						readyc     chan Ready
						advancec   chan struct{}
						tickc      chan struct{}
						done       chan struct{}
						stop       chan struct{}
						status     chan chan Status
					
						rn *RawNode
					}
			propc:	来自应用层的输入
			recvc:	来自其他节点的输入
			confc:	来自其他节点的配置输入
			readyc:返回的节点的状态

		· 使用run()来使用这些channel;
			使用for-select-channel的模式循环的读取事件并处理它们
					for{
						select {
						case pm := <-propc:
								r.Step(m)
						case m := <-n.recvc:
								r.Step(m)
						case cc := <-n.confc:
								...
						case <-n.tickc:
								r.tick()
						case readyc <- rd:
								...
						case <-advancec:
								...
						case c := <-n.status:
								...
						case <-n.stop:
								close(n.done)
								return
						}
					}
		
		· 在Etcd中,node不负责实现数据的持久化、网络消息通信、以及将log应用到状态机上;
		  所有node使用readyc的channel对外通知又数据要处理,并将这些外部操作可能需要的数据打包成Ready struct;
			对外界来说,Ready中的数据都是只读的
					type Ready struct {
						*SoftState
						pb.HardState
						ReadStates []ReadState
						Entries []pb.Entry
						Snapshot pb.Snapshot
						CommittedEntries []pb.Entry
						Messages []pb.Message
						MustSync bool
					}
			包括,node的角色、当前的Vote/Term/Committed、读请求队列(读请求包括Index和读ID,保证线性可读)、entries、Snapshot、CommittedEntries、和Maeeages;

		· 当应用程序得到了一个Ready;
				1. 将HardStable、Entries、Snapshot 持久化到Strage;
				2. 将Massages广播给其他节点;
				3. 将CommittedEntries应用到状态机上;
				4. 如果发现CommittedEntries中有成员变更的entries,调用node.ApplyConfChange()让node知道;
				5. 最后在调用node.Advance()告诉raft,这批状态处理完毕,可以发送下一批Ready;
				(这里的node.*指的是node interface)


十、Life of a Request
		· Life of Vote Request
				1. 在node的循环中,会有一个tick channel用于定时触发raft.tick字段函数;
				   当当前节点角色为Follower/Candideta/PreCandidate 时,会tick函数为 raft.tickElection();
					 然后 tickElection()会向自己发送一条MsgHup,Step函数发现 Type==MsgHub会调到用campaign() ,进入竞选状态;
							func (r *raft) tickElection() {
								r.electionElapsed++
								if r.promotable() && r.pastElectionTimeout() {
									r.electionElapsed = 0
									if err := r.Step(pb.Message{From: r.id, Type: pb.MsgHup}); err != nil {
										r.logger.Debugf("error occurred during election: %v", err)
									}
								}
							}
							func (r *raft) Step(m pb.Message) error {
								switch m.Type {
									case pb.MsgHup:
										if r.preVote {
											r.hup(campaignPreElection)
										} else {
											r.hup(campaignElection)
										}
									}
							}
							func (r *raft) hup(t CampaignType) {
								// ...
								r.campaign(t)
							}

				2. campaign()会调用r.becomeCandidate()把状态变为Candideta,并递增Term值;
					 然后再Send自己的Term以及相关信息给其他节点,请求投票;
							func (r *raft) campaign(t CampaignType) {
								// ...
								r.becomeCandidate()
								voteMsg = pb.MsgVote
								term = r.Term
								// 循环集群配置中所有的节点
								for _, id := range ids {
									// ...
									r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
								}
							}

				3. 其他节点收到这个pb.MsgVote,首先会判断Term是否比自己大,以及判断Candideta在MsgVota附带的日志信息是不是比自己的新,
					 从而决定是否投票,这个逻辑还是在Step函数中;
							func (r *raft) Step(m pb.Message) error {
								switch m.Type {
									case pb.MsgVote, pb.MsgPreVote:
										canVote := r.Vote == m.From ||
											(r.Vote == None && r.lead == None) ||
											(m.Type == pb.MsgPreVote && m.Term > r.Term)
									if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
										r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
										if m.Type == pb.MsgVote {
											r.electionElapsed = 0
											r.Vote = m.From
										}
									} else {
										r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
									}
								}
							}

				4. 最后当Candideta收到投票回复后,会通过Step的default进入到stepCandideta();
				   就会计算收到的选票数量是否大于半数以上,如果大于则成为Leader,然后发送一个信息,否则变为Folower;
							func stepCandidate(r *raft, m pb.Message) error {
								switch m.Type {
								case myVoteRespType:
									gr, rj, res := r.poll(m.From, m.Type, !m.Reject)
									r.logger.Infof("%x has received %d %s votes and %d vote rejections", r.id, gr, m.Type, rj)
									switch res {
									case quorum.VoteWon:
										if r.state == StatePreCandidate {
											r.campaign(campaignElection)
										} else {
											r.becomeLeader()
											r.bcastAppend()
										}
									case quorum.VoteLost:
										r.becomeFollower(r.Term, None)
									}
								}
							}


		· Life of Write Request
				1. 一个写请求一般会通过node接口提供的Propose()方法开始,
					 应用层会实现Propose()将这个写请求封装到一个MsgProp消息中,发送到propc channel中;
					 协议层通过run()抓取到这个消息;
							func (n *node) run() {
								select {
									case pm := <-propc:
										m := pm.m
										m.From = r.id
										err := r.Step(m)
										if pm.result != nil {
											pm.result <- err
											close(pm.result)
										}
								}
							}
				
				2. MsgProp会Step()通过default进入到角色Step函数,根据当前角色来处理;
					 假设当前为Follower,那么他会把这个消息转发给Leader;
							func stepFollower(r *raft, m pb.Message) error {
								switch m.Type {
									case pb.MsgProp:
										if r.lead == None {
											return ErrProposalDropped
										} else if r.disableProposalForwarding {
											return ErrProposalDropped
										}
										m.To = r.lead
										r.send(m)
									}
							}
				
				3. Leader收到这个消息后,通过stepLeader会将这个消息添加到自己的log中,再向其他follower广播MsgApp消息;
							func stepLeader(r *raft, m pb.Message) error {
								switch m.Type {
									case pb.MsgProp:
										// 省略了检查entries中是否有confchange的指令
										if !r.appendEntry(m.Entries...) {
											return ErrProposalDropped
										}
										r.bcastAppend()
										return nil
							}
							func (r *raft) bcastAppend() {
								r.prs.Visit(func(id uint64, _ *tracker.Progress) {
									if id == r.id {
										return
									}
									r.sendAppend(id)
								})
							}
				
			  4. 当Follower接收到这消息就会返回一个MsgAppResp;
							func stepFollower(r *raft, m pb.Message) error {
								switch m.Type {
									case pb.MsgApp:
										r.electionElapsed = 0
										r.lead = m.From
										r.handleAppendEntries(m)
								}
							}
				
				5. 当Leader确认MsgAppResp就会计算是否超过法定人数的Follower收到了这个消息,如果超过了就会Commit这个Log,
				   然后再广播一次,告诉Follower新的Committed状态

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值