面试官:聊聊 etcd 中的 Raft 吧

Raft:(computer_science "Raft") 是近年来比较流行的一个一致性算法 。 它的原理比较容易理解 , 网上也有很多相关的介绍 , 因此这里我就不再啰嗦原理了 , 而是打算以 raft 在 etcd 中的实现1[1]为例 , 从工程的角度来讲讲这个算法的一个具体实现 , 毕竟了解原理只算是“纸上谈兵” , 离真正能把它应用起来还有很长一段距离 。
如果你还不熟悉 raft , 这个经典的动画演示[2]、它的论文[3]以及这个 lecture[4]可能会对你有帮助 。 或者你也可以直接观看下面的视频 , 这是我作的一次技术分享 , 讲的是etcd 中 raft 模块的源码解析[5] 。 说句题外话 , 很多 Conference 和 Meetup 都会把视频录像上传到 YouTube 上 , YouTube 简直就是程序员的衣柜 , 每逛一次都有新收获 。 (方便播放 , 放一个 B 站链接)
OverviewEtcd 将 raft 协议实现为一个 library , 然后本身作为一个应用使用它 。 当然 , 可能是为了推广它所实现的这个 library , etcd 还额外提供了一个叫raftexample[6] 的示例程序 , 向用户展示怎样在它所提供的 raft library 的基础上构建出一个分布式的 KV 存储引擎 。
【面试官:聊聊 etcd 中的 Raft 吧】在 etcd 中 , raft 作为底层的共识模块 , 运行在一个goroutine里 , 通过channel接受上层(etcdserver)传来的消息 , 并将处理后的结果通过另一个channel返回给上层应用 , 他们的交互过程大概是这样的:
面试官:聊聊 etcd 中的 Raft 吧文章插图
Raft Stack
这种全异步的交互方式好处就是它提高了性能 , 但坏处就是难以调试 , 代码看起来会很绕 。 拿 etcd 举例 , 很多时候你只看到它把一个消息 push 到一个 slice/channel 里面 , 然后这部分函数调用链就结束了 , 你无法直观的追踪到 , 到底是谁最后处理了这个消息 。
Code Breakdown我们来看一下这个 raft library 里面都有哪些文件:
$ tree --dirsfirst -L 1 -I '*test*' -P '*.go'.├── raftpb├── doc.go├── log.go├── log_unstable.go├── logger.go├── node.go├── progress.go├── raft.go├── rawnode.go├── read_only.go├── status.go├── storage.go└── util.go下面按功能模块依次介绍:
raftpbRaft 中的序列化是借助于Protocol Buffer[7]来实现的 , 这个文件夹就定义了需要序列化的几个数据结构 , 我们先从Entry和Message开始看起:
Entry从整体上来说 , 一个集群中的每个节点都是一个状态机 , 而 raft 管理的就是对这个状态机进行更改的一些操作 , 这些操作在代码中被封装为一个个Entry 。
// #L203type Entry struct {Termuint64Indexuint64TypeEntryTypeData[]byte}

  • Term:选举任期 , 每次选举之后递增 1 。 它的主要作用是标记信息的时效性 , 比方说当一个节点发出来的消息中携带的 term 是 2 , 而另一个节点携带的 term 是 3 , 那我们就认为第一个节点的信息过时了 。
  • Index:当前这个 entry 在整个 raft 日志中的位置索引 。 有了Term和Index之后 , 一个 log entry 就能被唯一标识 。
  • Type:当前 entry 的类型 , 目前 etcd 支持两种类型:EntryNormal[8]和EntryConfChange[9] , EntryNormal 代表当前 Entry 是对状态机的操作 , EntryConfChange 则代表对当前集群配置进行更改的操作 , 比如增加或者减少节点 。
  • Data:一个被序列化后的 byte 数组 , 代表当前 entry 真正要执行的操作 , 比方说如果上面的Type是EntryNormal , 那这里的 Data 就可能是具体要更改的 key-value pair , 如果Type是EntryConfChange , 那 Data 就是具体的配置更改项ConfChange[10] 。 raft 算法本身并不关心这个数据是什么 , 它只是把这段数据当做 log 同步过程中的 payload 来处理 , 具体对这个数据的解析则有上层应用来完成 。
MessageRaft 集群中节点之间的通讯都是通过传递不同的Message来完成的 , 这个Message结构就是一个非常 general 的大容器 , 它涵盖了各种消息所需的字段 。
// #L239type Message struct {TypeMessageTypeTouint64Fromuint64Termuint64LogTermuint64Indexuint64Entries[]EntryCommituint64SnapshotSnapshotRejectboolRejectHintuint64Context[]byte}
  • Type:当前传递的消息类型 , 它的取值有很多个 , 但大致可以分成两类:
  • Raft 协议相关的 , 包括心跳 MsgHeartbeat、日志 MsgApp、投票消息 MsgVote 等 。
  • 上层应用触发的(没错 , 上层应用并不是通过 api 与 raft 库交互的 , 而是通过发消息) , 比如应用对数据更改的消息 MsgProp(osal) 。
不同类型的消息会用到下面不同的字段: