面试官:聊聊 etcd 中的 Raft 吧( 二 )


  • To, From 分别代表了这个消息的接受者和发送者 。
  • Term:这个消息发出时整个集群所处的任期 。
  • LogTerm:消息发出者所保存的日志中最后一条的任期号 , 一般MsgVote会用到这个字段 。
  • Index:日志索引号 。 如果当前消息是MsgVote的话 , 代表这个 candidate 最后一条日志的索引号 , 它跟上面的LogTerm一起代表这个 candidate 所拥有的最新日志信息 , 这样别人就可以比较自己的日志是不是比 candidata 的日志要新 , 从而决定是否投票 。
  • Entries:需要存储的日志 。
  • Commit:已经提交的日志的索引值 , 用来向别人同步日志的提交信息 。
  • Snapshot:一般跟MsgSnap合用 , 用来放置具体的 Snapshot 值 。
  • Reject , RejectHint:代表对方节点拒绝了当前节点的请求(MsgVote/MsgApp/MsgSnap…)
log_unstable.go顾名思义 , unstable 数据结构用于还没有被用户层持久化的数据 , 它维护了两部分内容snapshot和entries:
// #L23type unstable struct {// the incoming unstable snapshot, if any.snapshot *pb.Snapshot// all entries that have not yet been written to storage.entries []pb.Entryoffsetuint64logger Logger}entries代表的是要进行操作的日志 , 但日志不可能无限增长 , 在特定的情况下 , 某些过期的日志会被清空 。 那这就引入一个新问题了 , 如果此后一个新的follower加入 , 而leader只有一部分操作日志 , 那这个新follower不是没法跟别人同步了吗?所以这个时候snapshot就登场了 - 我无法给你之前的日志 , 但我给你所有之前日志应用后的结果 , 之后的日志你再以这个snapshot为基础进行应用 , 那我们的状态就可以同步了 。 因此它们的结构关系可以用下图表示2[11]:
面试官:聊聊 etcd 中的 Raft 吧文章插图
img
这里的前半部分是快照数据 , 而后半部分是日志条目组成的数组 entries , 另外 unstable.offset 成员保存的是 entries 数组中的第一条数据在 raft 日志中的索引 , 即第 i 条 entries 在 raft 日志中的索引为i + unstable.offset 。
storage.go这个文件定义了一个Storage[12]接口 , 因为 etcd 中的 raft 实现并不负责数据的持久化 , 所以它希望上面的应用层能实现这个接口 , 以便提供给它查询 log 的能力 。
另外 , 这个文件也提供了Storage接口的一个内存版本的实现MemoryStorage[13] , 这个实现同样也维护了snapshot和entries这两部分 , 他们的排列跟unstable中的类似 , 也是snapshot在前 , entries在后 。 从代码中看来etcdserver和raftexample都是直接用的这个实现来提供 log 的查询功能的 。
log.go有了以上的介绍 unstable、Storage 的准备之后 , 下面可以来介绍 raftLog 的实现 , 这个结构体承担了 raft 日志相关的操作 。
raftLog 由以下成员组成:
  • storage Storage:前面提到的存放已经持久化数据的 Storage 接口 。
  • unstable unstable:前面分析过的 unstable 结构体 , 用于保存应用层还没有持久化的数据 。
  • committed uint64:保存当前提交的日志数据索引 。
  • applied uint64:保存当前传入状态机的数据最高索引 。
需要说明的是 , 一条日志数据 , 首先需要被提交(committed)成功 , 然后才能被应用(applied)到状态机中 。 因此 , 以下不等式一直成立:applied <= committed 。
raftLog 结构体中 , 几部分数据的排列如下图所示2[14]:
面试官:聊聊 etcd 中的 Raft 吧文章插图
RaftLog Layout
这个数据排布的情况 , 可以从 raftLog 的初始化函数中看出来:
// #L45// newLog returns log using the given storage. It recovers the log to the state// that it just commits and applies the latest snapshot.func newLog(storage Storage, logger Logger) *raftLog {if storage == nil {log.Panic("storage must not be nil")}log :=--tt-darkmode-color: #EF7060;">Storage管理的已经持久化的数据 , 而在此之后都是unstable管理的还没有持久化的数据 。
以上分析中还有一个疑问 , 为什么并没有初始化 unstable.snapshot 成员 , 也就是 unstable 结构体的快照数据?原因在于 , 上面这个是初始化函数 , 也就是节点刚启动的时候调用来初始化存储状态的函数 , 而 unstable.snapshot 数据 , 是在启动之后同步数据的过程中 , 如果需要同步快照数据时才会去进行赋值修改的数据 , 因此在这里并没有对它进行操作的地方 。
progress.goLeader 通过Progress这个数据结构来追踪一个 follower 的状态 , 并根据Progress里的信息来决定每次同步的日志项 。 这里介绍三个比较重要的属性: