本文由 网易云 发布。git
做者:孙建良github
Raft 协议的发布,对分布式行业是一大福音,虽然在核心协议上基本都是师继 Paxos 祖师爷(Lamport) 的精髓,基于多数派的协议。可是 Raft 一致性协议的贡献在于,定义了可易于实现的一致性协议的事实标准。把一致性协议从 “阳春白雪” 变成了让普通学生、IT 码农等均可以上手试一试玩一玩的东西,MIT 的分布式教学课程 6.824 都是直接使用 Raft 来介绍一致性协议。web
从论文 In Search of An Understandable Consensus Algorithm (Extend Version) 中,咱们能够看到,与其余一致性协议论文不一样的是,Diego 基本已经算是把一个易于工程实现的算法讲得很是明白了,just do it,没有太多争议和发挥的空间,即使如此,要实现一个工业级的靠谱的 Raft 仍是要花很多力气。算法
Raft 一致性协议相对来讲易于实现主要归结为如下几个缘由:安全
本文不打算对 Basic Raft 一致性协议的具体内容进行说明,而是介绍记录一些关键点,由于绝大部份内容原文已经介绍的很详实,有兴趣的读者还可把 Raft 做者 Diego Ongaro 200 多页的博士论文刷一遍(连接在文末,可自取)。bash
Old Term LogEntry 处理网络
旧 Term 未提交日志的提交依赖于新一轮的日志的提交
复制代码
这个在原文 “5.4.2 Committing entries from previews terms” 有说明,可是在看的时候可能会以为有点绕。并发
Raft 协议约定,Candidate 在使用新的 Term 进行选举的时候,Candidate 可以被选举为 Leader 的条件为:app
而且有一个安全截断机制:dom
在以上条件下,Raft 论文列举了一个 Corner Case ,以下图所示:
进行到这步的时候咱们已经发现,黄色的 LogEnry(2) 在被设置为 Commit 以后从新又被否认了。
因此协议又强化了一个限制:
如图所示 (c) 状态 Term2 的 LogEntry(黄色) 只有在 (e)状态 Term4 的 LogEntry(红色)被 commit 才可以提交。
提交 NO-OP LogEntry 提交系统可用性
复制代码
在 Leader 经过竞选刚刚成为 Leader 的时候,有一些等待提交的 LogEntry (即 SN > CommitPt 的 LogEntry),有多是 Commit 的,也有多是未 Commit 的(PS: 由于在 Raft 协议中 CommitPt 不用实时刷盘)。
因此为了防止出现非线性一致性(Non Linearizable Consistency);即以前已经响应客户端的已经 Commit 的请求回退,而且为了不出现上图中的 Corner Case,每每咱们须要经过下一个 Term 的 LogEntry 的 Commit 来实现以前的 Term 的 LogEntry 的 Commit (隐式commit),才能保障提供线性一致性。
可是有可能接下来的客户端的写请求不能及时到达,那么为了保障 Leader 快速提供读服务,系统可首先发送一个 NO-OP LogEntry 来保障快速进入正常可读状态。
Current Term、VotedFor 持久化
上图其实隐含了一些须要持久化的重要信息,即 Current Term、VotedFor! 为何(b) 状态 S5 使用的 Term Number 为 3,而不是 2?
由于竞选为 Leader 就必须是使用新的 Term 发起选举,而且获得多数派阶段的赞成,赞成的操做为将 Current Term、VotedFor 持久化。
好比(a) 状态 S1 为何能竞选为 Leader?首先 S1 知足成为 Leader 的条件,S2~S5 均可以接受 S1 成为发起 Term 为 2 的 Leader 选举。S2~S5 赞成 S1 成为 Leader 的操做为:将 Current Term 设置为 二、VotedFor 设置为 S1 而且持久化,而后返回 S1。即 S1 成功成为 Term 为 2 的 Leader 的前提是一个多数派已经记录 Current Term 为 2 ,而且 VotedFor 为 S1。那么 (b) 状态 S5 如使用 Term 为 2 进行 Leader 选举,必然得不到多数派赞成,由于 Term 2 已经投给 S1,S5 只能 将 Term++ 使用Term 为3 进行从新发起请求。
Current Term、VotedFor 如何持久化?
简单的方法,只须要保存在一个单独的文件,如上为简单的 go 语言示例;其余简单的方式好比在设计 Log File 的时候,Log File Header 中包含 Current Term 以及 VotedFor 的位置。
若是再深刻思考一层,其实这里头有一个疑问?如何保证写了一半(写入一半而后挂了)的问题?写了 Term、没写 VoteFor?或者只写了 Term 的高 32 位?
能够看到磁盘可以保证 512 Byte 的写入原子性,这个在知乎 事务性 (Transactional)存储须要硬件参与吗?这个问答上就能找到答案。因此最简单的方法是直接写入一个 tmpfile,写入完成以后,将 tmpfile mv 成CurrentTermAndVotedFor 文件,基本可保障更新的原子性。其余方式好比采用 Append Entry 的方式也能够实现。
Cluser Membership 变动
在 Raft 的 Paper 中,简要说明了一种一次变动多个节点的 Cluser Membership 变动方式。可是没有给出更多的在 Security 以及 Avaliable 上的更多的说明。
其实如今开源的 Raft 实现通常都不会使用这种方式,好比 Etcd raft 都是采用了更加简洁的一次只能变动一个节点的 “single Cluser MemberShip Change” 算法。
固然 single cluser MemberShip 并不是 Etcd 自创,其实 Raft 协议做者 Diego 在其博士论文中已经详细介绍了 Single Cluser MemberShip Change 机制,包括 Security、Avaliable 方面的详细说明,而且做者也说明了在实际工程实现过程当中更加推荐 Single 方式,首先由于简单,再则全部的集群变动方式均可以经过 Single 一次一个节点的方式达到任何想要的 Cluster 状态。
Raft restrict the types of change that allowed: only one server can be added or removed from the cluster at once. More complex changes in membership are implemented as a series of single-server-change.
Safty
回到问题的第一大核心要点:Safety,membership 变动必须保持 Raft 协议的约束:同一时间(同一个 Term)只能存在一个有效的 Leader。
为何不能直接变动多个节点,直接从 Old 变为 New 有问题? for example change from 3 Node to 5 Node?
如上图所示,在集群状态变动过程当中,在红色箭头处出现了两个不相交的多数派(Server三、Server四、Server 5 认知到新的 5 Node 集群;而 一、2 Server 的认知仍是处在老的 3 Node 状态)。在网络分区状况下(好比 S一、S2 做为一个分区;S三、S四、S5 做为一个分区),2个分区分别能够选举产生2个新的 Leader(属于configuration< Cold>的 Leader 以及 属于 new configuration < Cnew > 的 Leader) 。
固然这就致使了 Safty 无法保证;核心缘由是对于 Cold 和 CNew 不存在交集,不存在一个公共的交集节点充当仲裁者的角色。
可是若是每次只容许出现一个节点变动(增长 or 减少),那么 Cold 和 CNew 总会相交。 以下图所示:
如何实现 Single membership change
论文中提到如下几个关键点:
关注点 1
如图所示,如在前一轮 membership configure Change 未完成以前,又进行下一次 membership change 会致使问题,因此外部系统须要确保不会在第一次 Configuration 为成功状况下,发起另一个不一样的 Configuration 请求。( PS:因为增长副本、节点宕机丢失节点进行数据恢复的状况都是由外部触发进行的,只要外部节点可以确保在前一轮未完成以前发起新一轮请求,便可保障。)
关注点 2
跟其余客户端的请求不同的,Single MemberShip Change LogEntry 只须要 Append 持久化到 Log(而不须要 commit)就能够应用。
一方面是可用性的考虑,以下所示:Leader S1 接收到集群变动请求将集群状态从(S一、S二、S三、S4)变动为 (S二、S三、S4);提交到全部节点以后 commit 以后,返回客户端集群状态变动完成(以下状态 a),S1 退出(以下状态b);因为 Basic Raft 并不须要 commit 消息实施传递到其余 S一、S二、S3 节点,S1 退出以后,S一、S二、S3 因为没有接收到 Leader S1 的心跳,致使进行选举,可是不幸的是 S4 故障退出。假设这个时候 S二、S3 因为 Single MemberShip Change LogEntry 没有 Commit 仍是以(S一、S二、S三、S4)做为集群状态,那么集群无法继续工做。可是实质上在(b)状态 S1 返回客户端集群状态变动请求完成以后,实质上是认为可独立进入正常状态。
另外一方面,即便没有提交到一个多数派,也能够截断,没什么问题。(这里很少作展开)
另外一方面是可靠性&正确性。Raft 协议 Configuration 请求和普通的用户写请求是能够并行的,因此在并发进行的时候,用户写请求提交的备份数是没法确保是在 Configuration Change 以前的备份数仍是备份以后的备份数。可是这个没有办法,由于在并发状况下原本就无法保证,这是保证 Configuration 截断系统持续可用带来的代价。(只要确保在多数派存活状况下不丢失便可(PS:一次变动一个节点状况下,返回客户端成功,其中必然存在一个提交了客户端节点的 Server 被选举为Leader)。
关注点 3
Single membership change 其余方面的 safty 保障是跟原始的 Basic Raft 是同样的(在各个协议处理细节上对此类请求未有任何特殊待遇),即只要一个多数派(无论是新的仍是老的)将 single membership change 提交并返回给客户端成功以后,接下来不管节点怎么重启,都会确保新的 Leader 将会在已经知晓(应用)新的,前一轮变动成功的基础上处理接下来的请求:能够是读写请求、固然也能够是新的一轮 Configuration 请求。
初始状态如何进入最小备份状态
好比如何进入3副本的集群状态。可使用系统元素的 Single MemberShip 变动算法实现。
刚开始节点的副本状态最简单为一个节点 1(本身赞成本身很是简单),获得返回以后,再选择添加一个副本,达到 2个副本的状态。而后再添加一个副本,变成三副本状态,知足对系统可用性和可靠性的要求,此时该 Raft 实例可对外提供服务。
参考文献
了解网易云:
网易云官网:www.163yun.com/
新用户大礼包:www.163yun.com/gift
网易云社区:sq.163yun.com/