etcd系列-----raft协议:重要数据结构介绍(Entry、Message、storage、unstable)

一些基础结构体介绍:

Entry

Entry记录:在前面介绍Raft协议时提到,节点之间传递的是消息(Message), 每条消息中可以携带多条Entry记录,每条Entry记录对应一个独立的操作。
在Entry中其中封装了如下信息:
    :Term( uint64类型): 该Entry所在的任期号。
    : Index ( uint64类型): 该Entry对应的索引号。
    :Type( EntryType 类型):该Entry记录的类型。 该字段有两个可选项:一个是Entry Normal,表示普通的数据操作;另一个是EntryConfChange,表示集群的变更操作。
    : Data ( []byte类型): 具体操作使用的数据。

记录的本地Log的基本单位也是Entry记录。有的文章也会将Entry记录称为“日志记录”,在etcd 中还有一个WAL日志的概念,这两者并非完全等价,所以需要注意一下,避免两者混淆。

Message

在每个节点中在raft模块中另一个比较重要结构体就是raftpb.Message。在raft模块的实现中,Message是所有消息的抽象,包括了各种类型消息所需要的字段,其中核心宇段的含义如下。
    :Type ( MessageType类型): 该字段定义了消息的类型,raft实现中就是通过该字段区分不同的消息井进行分类处理的,MessageType中共定义了19 种消息类型,后面会介绍每种消息类型的含义及相应的处理方式。
    :From ( uint64 类型): 发送消息的节点ID。在集群中,每个节点都拥有一个唯一ID作为标识。
    :To ( uint64类型):消息的目标节点ID。
    :Term ( uint64类型):发送消息的节点的Term值。 如果Term值为0,则为本地消息,在etcd刊负模块的实现中,对本地消息进行特殊处理。
    :Entries ( []Entry类型): 如果是MsgApp类型的消息,则该字段中保存了Leader节点复制到Follower节点的Entry记录。在其他类型消息中,该字段的含义后面会详细介绍。
    :LogTerm ( uint64类型):该消息携带的第一条Entry记录的Term值。
    :Index ( uint64 类型):记录索引值,该索引值的具体含义与消息的类型相关。例如,MsgApp消息的Index宇段保存了其携带的Entry记录(即Entries字段)中前一条记录的Index值,而MsgAppResp消息的Index字段则是Follower节点提示Leader节点下次从哪个位置开始发送Entry记录。
    :Commit ( uint64类型): 消息发送节点的提交位置(commitlndex)。
    :Snapshot ( Snapshot类型):在传输快照时,该字段保存了快照数据。
    :Reject ( bool 类型): 主要用于响应类型的消息,表示是否拒绝收到的消息。 例如,Follower节点收到Leader节点发来的MsgApp消息,如果Follower节点发现MsgApp消息携带的Entry记录并不能直接追加到本地的raftLog中, 则会将响应消息的Reject宇段设置为true,并且会在RejectHint字段中记录合适的Entry索引值,供Leader节点参考。
    :RejectHint ( uint64类型):在Follower节点拒绝Leader节点的消息之后,会在该字段记录一个Entry索引值供Leader节点。

raft结构体

在etcd-ra负模块中,raft结构体是其核心数据结构,在结构体raft中封装了当前节点所有的
核心数据。先看其核心字段。
    :id  ( uint64类型): 当前节点在集群的ID
    :Term ( uint64类型): 当前任期号。如果Message的Term字段为0,则表示该消息是本地消息,例如,MsgHup、 MsgProp、 MsgReadlndex 等消息,都属于本地消息。
    :Vote ( uint64类型):当前任期中当前节点将选票投给了哪个节点,未投票时, 该字段为None。
    :raftlog( *raftlog类型): 在前面介绍过,在Ra企协议中的每个节点都会记录本地Log,在raft模块中,使用结构体raftLog表示本地Log, 在raftLog中还涉及日志的缓存等相关内容,后面会介绍。
    :maxlnflight ( int 类型): 对于当前节点来说,己经发送出去但未收到响应的消息个数上限。如果处于该状态的消息超过maxlnflight这个|现值,则暂停当前节点的消息发送,这是为了防止集群中的某个节点不断发送消息,引起网络阻塞或是压垮其他节点, 从而影响其他节点的正常运行。
    :maxMsgSize ( uint64类型): 单条消息的最大字节数。
    :prs ( map[uint64]*Progress类型): 在前面介绍Raft协议时提到过, Leader 节点会记录集群中其他节点的日志复制情况(Nextlndex和Matchlndex)。在raft模块中,每个Follower节点对应的Nextlndex值和Matchlndex值都封装在Progress实例中, 除此之外, 每个Progress实例中还封装了对应Follower节点的相关信息, 这里简单介绍主要字段。
         .Match ( uint64类型): 对应Follower节点当前己经成功复制的Entry记录的索引值。
         .Next ( uint64类型): 对应Follower节点下一个待复制的Entry记录的索引值。
         .State( ProgressState丁ype类型): 对应Follower节点的复制状态, 其可选项的含义后面详细介绍。
         .Paused ( boo I 类型):当前Leader节点是否可以向该Progress实例对应的Follower节点发送消息。
         .PendingSnapshot ( uint64类型): 当前正在发送的快照数据信息。
         .RecentActive ( bool类型): 从当前Leader节点的角度来看,该Progress实例对应的Follower节点是否存活。
         .ins ( *inflights类型): 记录了己经发送出去但未收到响应的消息信息。
    :state (StateType 类型): 当前节点在集群中的角色,可选值分为StateFollower、StateCandidate、 StateLeader和StatePreCandidat巳四种状态。
    :votes (map[uint64]bool类型):在选举过程,如果当前节点收到了来自某个节点的投票, 则会将votes 中对应的值设置为true,通过统计votes这个map, 就可以确定当前节点收到的投票是否超过半数。
    :msgs ([]pb. Message类型): 缓存了当前节点等待发送的消息。
    :lead ( uint64类型): 当前集群中Leader节点的ID。
    :electionElapsed ( int 类型):选举计时器的指针,其单位是逻辑时钟的刻度,逻辑时钟每推进一次,该字段值就会增加1。
    :electionTimeout ( int 类型): 选举超时时间,当electionE!apsed 宇段值到达该值时,就会触发新一轮的选举。
    :heartbeatElapsed ( int 类型): 心跳计时器的指针,其单位也是逻辑时钟的刻度,逻辑时钟每推进一次,该字段值就会增加1 。
    :heartbeatTimeout ( int类型):心跳超时时间,当heartbeatElapsed字段值到达该值时,就会触发Leader节点发送一条心跳消息。
    :tick ( func()类型): 当前节点推进逻辑时钟的函数。如果当前节点是Leader,则指向raft.tickHeartbeat()函数,如果当前节点是Follower 或是Candidate,则指向raft.tickElection()函数。
    :step ( stepFunc类型): 当前节点收到消息时的处理函数。如果是Leader节点, 则该字段指向stepLeader()函数,如果是Follower节点,则该字段指向stepFollower()函数,如果是处于preVote阶段的节点或是Candidate节点,则该字段指向stepCandidate()函数。

Config结构体

Config结构体主要用于配置参数的传递,在创建raft实例时需要的参数会通过Config实例传递进去。 Config的主要字段如下。
    :ID ( uint64类型):当前节点的ID。
    :peers ( []uint64类型):记录了集群中所有节点的ID。
    :ElectionTick ( int类型):用于初始化raft.electionTimeout,即逻辑时钟连续推进多少次后,就会触发Follower节点的状态切换及新一轮的Leader选举。
    :HeartbeatTick ( int 类型):用于初始化raftheartbeatTimeout,即逻辑时钟连续推进多少次后,就触发Leader节点发送心跳消息。
    :Storage ( Storage类型): 当前节点保存raft日志记录使用的存储,后面会j接收其接口及其实现。
    :Applied ( uint64类型):当前已经应用的记录位置(己应用的最后一条Entry记录的索引值),该值在节点重启时需要设置,否则会重新应用己经应用过ntry记录。
    :MaxSizePerMsg ( uint64类型):用于初始化raft.maxMsgSize字段,每条消息的最大字节数。
    :MaxlnflightMsgs ( int类型):用于初始化ra丘maxlnflight,即已经发送出去且未收到响应的最大消息个数。

Storage

MemoryStorage 是raft模块为Storage 接口提供的一个实现,从名字也能看出,MemoryStorage在内存中维护上述状态信息(hardState字段)、快照数据(snapshot宇段)及所有的Entry记录(ents 字段,[]raftpb.Entry类型〕,在MemoryStorage.ents字段中维护了快照数据之后的所有Entry记录。另外需要注意的是,MemoryStorage继承了sync.Mutex, MemoryStorage 中的大部分操作是需要加锁同步的。 通过这里的介绍,我们大概可以了解MemoryStorage的结构,如图示

 MemoryStorage 中追加Entry记录,该功能主要由MemoryStorage.Append()方法完成:

func (ms *MemoryStorage) Append(entries []pb.Entry) error {
	if len(entries) == 0 {
		return nil
	}

	ms.Lock()
	defer ms.Unlock()

	first := ms.firstIndex()
	last := entries[0].Index + uint64(len(entries)) - 1

	// shortcut if there is no new entry.
	if last < first {
		return nil//entries切片中所有的Entry都已经过时,无须添加任何Entry
	}
	//first之前的Entry已经记入Snapshot中,不应该再记录到ents中,所以将这部分Entry截掉
	if first > entries[0].Index {
		entries = entries[first-entries[0].Index:]
	}
    //计算entries切片中第一条可用的Entry与first之间的差距
	offset := entries[0].Index - ms.ents[0].Index
	switch {
	case uint64(len(ms.ents)) > offset:
	    //保留MemoryStorage.ents中first~offset的部分,offset之后的部分被抛弃
	    //然后将待追加的Entry追加到MemoryStorage.ents中
		ms.ents = append([]pb.Entry{}, ms.ents[:offset]...)
		ms.ents = append(ms.ents, entries...)
	case uint64(len(ms.ents)) == offset:
	    //直接将待追加的日志记录(entries)追加到MemoryStorage中
		ms.ents = append(ms.ents, entries...)
	default:
		raftLogger.Panicf("missing log entry [last: %d, append at: %d]",
			ms.lastIndex(), entries[0].Index)
	}
	return nil
}

随着系统的运行, MemoryStorage.ents 中保存的En位y记录会不断增加,为了减小内存的压力,定期创建快照来记录当前节点的状态并压缩MemoryStorage.ents数组的空间是非常有必要的, 这样就可以降低内存使用。这个过程中涉及三个方法, 首先是CreateSnapshot()方法, 它会接收当前集群状态,以及SnapShot相关数据来更新snapshot字段,具体实现如下:

//简单说明该方法的参数: i是新建Snapshot包含的最大的索引值,cs是当前集群的状态,data是新建Snapshot的具体数据
func (ms *MemoryStorage) CreateSnapshot(i uint64, cs *pb.ConfState, data []byte) (pb.Snapshot, error) {
	ms.Lock()
	defer ms.Unlock()
	//边界检查,l必须大于当前Snapshot包含的最大Index佳,并且小于MemoryStorage的LastIndex佳,否则抛出异常
	if i <= ms.snapshot.Metadata.Index {
		return pb.Snapshot{}, ErrSnapOutOfDate
	}

	offset := ms.ents[0].Index
	if i > ms.lastIndex() {
		raftLogger.Panicf("snapshot %d is out of bound lastindex(%d)", i, ms.lastIndex())
	}
    //更新MemoryStorage.snapshot的元数据
	ms.snapshot.Metadata.Index = i
	ms.snapshot.Metadata.Term = ms.ents[i-offset].Term
	if cs != nil {
		ms.snapshot.Metadata.ConfState = *cs
	}
	//更新具体的快照数据
	ms.snapshot.Data = data
	return ms.snapshot, nil
}

新建Snapshot之后,一般会调用MemoryStorage.Compact()方法将MemoryStorage.ents中指定索引之前的Entry记录全部抛弃,从而实现压缩MemoryStorage.ents 的目的,具体实现如下:

func (ms *MemoryStorage) Compact(compactIndex uint64) error {
	ms.Lock()
	defer ms.Unlock()
	offset := ms.ents[0].Index
	//边界检测
	if compactIndex <= offset {
		return ErrCompacted
	}
	if compactIndex > ms.lastIndex() {
		raftLogger.Panicf("compact %d is out of bound lastindex(%d)", compactIndex, ms.lastIndex())
	}
    //创建新的切片,用来存储compactIndex之后的Entry
	i := compactIndex - offset
	ents := make([]pb.Entry, 1, 1+uint64(len(ms.ents))-i)
	ents[0].Index = ms.ents[i].Index
	ents[0].Term = ms.ents[i].Term
	//将compactlndex之后的Entry拷贝到ents中,并更新MemoryStorage.ents 字段
	ents = append(ents, ms.ents[i+1:]...)
	ms.ents = ents
	return nil
}

 最后,上层模块可以通过MemoryStorage.Snapshot()方法获取SnapShot。

unstable结构体

unstable 使用内存数组维护其中所有的Entry记录,对于Leader节点而言,它维护了客户端请求对应的Entry记录;对于Follower节点而言,它维护的是从Leader节点复制来的Entry记录。无论是Leader节点还是Follower节点,对于刚刚接收到的Entry记录首先都会被存储在unstable中。然后按照Raft协议将unstable中缓存的这些Entry记录交给上层模块进行处理,上层模块会将这些Entry记录发送到集群其他节点或进行保存(写入Storage中)。之后,上层模块会调用Advance()方法通知底层的raft模块将unstable 中对应的Entry记录删除(因为己经保存到了Storage中)。正因为unstable中保存的Entry记录并未进行持久化,可能会因节点故障而意外丢失,所以被称为unstable。

unstable中的主要字段。
     :entries ( []pb.Entry类型):用于保存未写入Storage中的Entry记录。
     :offset ( uint64类型): entries 中的第一条Entry记录的索引值。
     :snapshot (pb.Snapshot类型):快照数据,该快照数据也是未写入Storage中的。

在unstable 中提供了很多与Storage类似的方法,在raftLog中,很多方法都是先尝试调用unstable的相应方法,在其失败后(unstable的方法返回(0, false)即表示失败),再尝试调用Storage的对应方法。
unstable.maybeFirstlndex()方法会尝试获取unstable 的第一条Entry 记录的索引值,unstable.maybeLastlndex()方法会尝试获取unstable 的最后一条Entry记录的索引值,如果获取失败则返回(0, false),unstable.maybeTerm()方法的主要功能是尝试获取指定Entry记录的Term值,根据条件查找指定的Entry记录的位置。

当unstable.entries 中的Entry记录己经被写入Storage之后,会调用unstable.stableTo()方法清除entries 中对应的Entry记录,stableTo()方法的具体实现如下:

func (u *unstable) stableTo(i, t uint64) {
    //查找指定Entry记录的Term佳,若查找失败则表示对应的Entry不在unstable中,直接返回
	gt, ok := u.maybeTerm(i)
	if !ok {
		return
	}
	// if i < offset, term is matched with the snapshot
	// only update the unstable entries if term is matched with
	// an unstable entry.
	if gt == t && i >= u.offset {
	//指定索引位之前的Entry记录都已经完成持久化,则将其之前的全部Entry记录删除
		u.entries = u.entries[i+1-u.offset:]
		u.offset = i + 1
		//随着多次追加日志和截断日志的操作unstable.entires底层的数组会越来越大,shrinkEntriesArray方法会在底层数组长度超过实际占用的两倍时,对底层数据进行缩减
		u.shrinkEntriesArray()
	}
}
func (u *unstable) shrinkEntriesArray() {
	// We replace the array if we're using less than half of the space in
	// it. This number is fairly arbitrary, chosen as an attempt to balance
	// memory usage vs number of allocations. It could probably be improved
	// with some focused tuning.
	const lenMultiple = 2
	if len(u.entries) == 0 {
		u.entries = nil
	} else if len(u.entries)*lenMultiple < cap(u.entries) {
        //重新创建切片,复制原有切片中的数据,重直entries字段
		newEntries := make([]pb.Entry, len(u.entries))
		copy(newEntries, u.entries)
		u.entries = newEntries
	}
}

 同理,当unstable.snapshot字段指向的快照被写入Storage之后, 会调用unstable.stableSnapTo()方法将snapshot字段清空.unstable.truncateAndAppend()方法的主要功能是向unstable.entries中追加Entry记录其实现与Storage.Append()方法类似也会涉及截断的场景:

func (u *unstable) truncateAndAppend(ents []pb.Entry) {
    //获取第一条待追加的Entry记录的索引值
	after := ents[0].Index
	switch {
	case after == u.offset+uint64(len(u.entries)):
		// after is the next index in the u.entries
		// directly append
		//若待追加的记录与e口tries中的记录正好连续,则可以直接向entries中追加
		u.entries = append(u.entries, ents...)
	case after <= u.offset:
	//直接用待追加的Entry记录替换当前的entries字段, 并支新offset
		u.logger.Infof("replace the unstable entries from index %d", after)
		// The log is being truncated to before our current offset
		// portion, so set the offset and replace the entries
		u.offset = after
		u.entries = ents
	default:
	    //after在offset~last之间,则after~last之间的Entry记录冲突。 这里会将offset~after 之间的记录保留,抛弃after之后的记录,然后完成追加操作
        //unstable.slice()方法会检测after是否合法,并返回offset~after的切片
		// truncate to after and copy to u.entries
		// then append
		u.logger.Infof("truncate the unstable entries before index %d", after)
		u.entries = append([]pb.Entry{}, u.slice(u.offset, after)...)
		u.entries = append(u.entries, ents...)
	}
}
发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

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