Etcd Raft架构设计和源码剖析3:重要结构体定义
序言
etcd raft定义了一些重要的结构体,来传递和表示raft使用到的数据。
在介绍各结构体之前,先澄清一下raft、log和state machine的关系,它们三个是独立的,没有隶属关系,尤其是state machine并不属于raft。
Consensus Module指raft算法,它输出一致的Log Entry序列,State machine指应用Entry后得到的状态,状态机是并不是raft的一部分,而是用来存储数据的模块。
Entry
每个Raft集群节点都是一个状态机,每个节点都使用相同的log entry序列修改状态机的数据,Entry就是每一个操作项,raft的核心能力就是为应用层提供序列相同的entry。
1 | type Entry struct { |
每一个Entry,都可以使用(Term, Index)进行唯一标记,相当于Entry的ID:
- Term:即raft论文中的Term,表明了当前Entry所属的Term。raft不使用绝对时间,而是使用相对时间,它把时间分割成了大小不等的term,每一轮选举都会开启一个新的term,term值会连续累加。如果当前的节点已经是Term 10缺收到了Term 8的Entry,Term 8的Entry已经过时,会被丢弃。
- Index:每一个Entry都有一个的Index,代表当前Entry在log entry序列中的位置,每个index上最终只有1个达成共识的Entry。
除了用于达成一致的Term和Index外,Entry还携带了数据:
- Type:表明当前Entry的类型,
EntryNormal
代表是Entry携带的是修改状态机的操作数据,EntryConfChange
和EntryConfChangeV2
代表的是Entry携带的是修改当前raft集群的配置。 - Data:是序列化的数据,不同的Type类型,对应不同的Data。
Snapshot
在Entry特别多的场景下,会存在一些问题,比如现在有1亿条已经达成一致Entry,后面还有源源不断的Entry产生,是否有以下问题:
- 这些Entry占用了大量的磁盘空间,但实际上过去的Entry已经对已经拥有这些Entry的节点没有意义了,只对那些没有Entry的节点有意义,leader把Entry发送给没有这些Entry节点,以让这些节点最终能和leader保持一致的状态。
- 有些follower非常慢,或者刚启动,或者重启过,与leader的当前状态已经严重脱节,让他们从Entry 0开始同步,然后应用到状态机,这种操作时间效率是不是非常慢?然后每一个Entry都会产生一个历史的状态,当产生新的状态之后,历史状态对当前节点也没有意义。
解决这种问题的办法就是快照,比如虚拟机的快照,或者docker镜像(镜像本质也是一种快照),有了快照就可以把状态机快速恢复到快照时的状态,空间和时间上效率都能提高很多。
Raft可以定期产生一些快照,然后在这些快照上按序应用快照之后的Entry就能得到一致的状态。1亿个Entry + 1亿01个Entry得到的状态,跟第1亿个Entry后所产生的快照+1亿零1个Entry得到的状态是一致的。
1 | type Snapshot struct { |
- Data:是状态机中状态的快照。
- Metadata:是快照自身相关的数据。
- ConfState:是快照时,当前raft的配置状态,这些状态数据并不在状态机中,所以需要进行保存。
- Index、Term:快照所依据的Entry所在的Index和Term。
Message
Raft集群节点之间的通信只使用了1个结构体Message
,Message中有一个Type
成员,表明了当前的Message是哪种消息,比如可以是Raft论文中提到的AppendEntries,RequestVotes等,目前实际可以容纳19种类型的消息,每种消息对Raft都有不同的作用,具体见这篇文章:
1 | // 不同的Message类型会用到不同的字段 |
Message中包含了很多字段,不同的消息类型使用的字段组合不相同,可以从不同消息的处理逻辑中看出来。
- To, From:是消息的接收节点和发送节点的的Raft ID。
- Term:创建Message时,发送节点所在的Term。
- LogTerm:创建Message时,发送节点本地所保存的log entry序列中最大的Term,在选举的时候会使用。
- Index:不同的消息类型,Index的含义不同。Term和Index与Entry中的Term和Index不一定会相同,因为某个follower可能比较慢,leader向follower发送已经committed的Entry。
- Entries:发送给follower,待follower处理的Entry。
- Commit:创建Message时,不同消息含义不同,Append时是发送节点本地已committed的Index,Heartbeat时是committed Index或者与follower匹配的Index。
- Snapshot:leader传递给follower的snapshot。
- Reject:投票和Append的响应消息使用,Reject表示拒绝leader发来的消息。
- RejectHint:拒绝Append消息的响应消息使用,用来给leader提示,发送follower已有的最后一个Index。
- Context:某些消息的附加信息,即用来存储通用的数据。比如竞选时,存放
campaignTransfer
。
Storage
etcd/raft不负责持久化数据存储和网络通信,网络数据都是通过Node接口的函数传入和传出raft。持久化数据存储由创建raft.Node的应用层负责,包含:
- 应用层使用Entry生成的状态机,即一致的应用数据。
- WAL:Write Ahead Log,历史的Entry(包含还未达成一致的Entry)和快照数据。
Snapshot是已在节点间达成一致Entry的快照,快照之前的Entry必然都是已经达成一致的,而快照之后,有达成一致的,也有写入磁盘还未达成一致的Entry。etcd/raft会使用到这些Entry和快照,而Storage
接口,就是用来读这些数据的。
1 | // Storage is an interface that may be implemented by the application |
使用这个接口,从应用层读取:
- InitialState:HardState和配置状态Confstate
- Entries:根据Index获取连续的Entry
- Term:获取某个Entry所在的Term
- LastIndex:获取本节点已存储的最新的Entry的Index
- FirstIndex:获取本节点已存储的第一个Entry的Index
- Snapshot:获取本节点最近生成的Snapshot,Snapshot是由应用层创建的,并暂时保存起来,raft调用此接口读取
每次都从磁盘文件读取这些数据,效率必然是不高的,所以etcd/raft内定义了MemoryStorage
,它实现了Storage
接口,并且提供了函数来维护最新快照后的Entry,关于MemoryStorage
见raftLog小节,其中的storage
即为MemoryStorage
。
unstable
因为Entry的存储是由应用层负责的,所以raft需要暂时存储还未存到Storage中的Entry或者Snapshot,在创建Ready时,Entry和Snapshot会被封装到Ready,由应用层写入到storage。
1 | // unstable.entries[i] has raft log position i+unstable.offset. |
- Snapshot:是follower从leader收到的最新的Snapshot。
- entries:对leader而已,是raft刚利用请求创建的Entry,对follower而言是从leader收到的Entry。
- offset:Entries[i].Index = i + offset。
raftLog
raft使用raftLog来管理当前Entry序列和Snapshot等信息,它由Storage、unstable、committed和applied组成。
1 | type raftLog struct { |
Storage和unstable前面已经介绍过了,所以介绍下committed和applied。
committed指最后一个在raft集群多数节点之间达成一致的Entry Index。
applied指当前节点被应用层应用到状态机的最后一个Entry Index。applied和committed之间的Entry就是等待被应用层应用到状态机的Entry。
前面提到Storage接口可以获取第一个索引firstIdx,最后一个索引lastIdx,在生成snapshot之后签名的Entry就可以删除了,所以firstidx是storage中snapshot后的第一个Entry的Index,lastIndex是storage中保存的最后一个Entry的Index,这个Entry可能还没有在raft集群多数节点之间达成一致,所以在committed之后,这些Entry是等待commit的Entry,leader发现某个Entry Index已经在多数节点之间达成一致,就会把committed移动到该Entry Index。
SoftState
SoftState指易变的状态数据,记录了当前的Leader的Node ID,以及当前节点的角色。
1 | // SoftState provides state that is useful for logging and debugging. |
HardState
HardState是写入到WAL(存储Entry的文件)的状态,可以在节点重启时恢复raft的状态,它了记录:
- Term:节点当前所在的Term。
- Vote:节点在竞选期间所投的候选节点ID。
- Commit:当前已经committed Entry Index。
1 | type HardState struct { |
Ready
终于到etcd raft最重要的一个结构体了。raft使用Ready结构体对外传递数据,是多种数据的打包。
1 | // Ready encapsulates the entries and messages that are ready to read, |
SoftState、HardState、Entry、Snapshot、Message都已经介绍过,不再单独介绍含义。
Entries和CommittedEntries的区别是,Entries保存的是从unstable读取的Entry,它们即将被应用层写入storage,CommittedEntries是已经被Committed,还没有applied,应用层会把他们应用到状态机。
ReadStates用来处理读请求,MustSync用来指明应用层是否采用异步的方式写数据。
应用层在接收到Ready后,应当处理Ready中的每一个有效字段,处理完毕后,调用Advance()
通知raft Ready已处理完毕。