此文已由做者孙建良受权网易云社区发布。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 一致性协议相对来讲易于实现主要归结为如下几个缘由:json
○ 模块化的拆分:把一致性协议划分为 Leader 选举、MemberShip 变动、日志复制、SnapShot 等相对比较解耦的模块;安全
○ 设计的简化:好比不容许相似 Paxos 算法的乱序提交、使用 Randomization 算法设计 Leader Election 算法以简化系统的状态,只有 Leader、Follower、Candidate 等等。网络
本文不打算对 Basic Raft 一致性协议的具体内容进行说明,而是介绍记录一些关键点,由于绝大部份内容原文已经介绍的很详实,有意者还可把 Raft 做者 Diego Ongaro 200 多页的博士论文刷一遍(连接在文末,可自取)。并发
Pointsapp
Old Term LogEntry 处理dom
旧 Term 未提交日志的提交依赖于新一轮的日志的提交
这个在原文 “5.4.2 Committing entries from previews terms” 有说明,可是在看的时候可能会以为有点绕。
Raft 协议约定,Candidate 在使用新的 Term 进行选举的时候,Candidate 可以被选举为 Leader 的条件为:
○ 获得一半以上(包括本身)节点的投票
○ 获得投票的前提是:Candidate 节点的最后一个LogEntry 的 Term 比投票节点大,或者在 Term 同样状况下,LogEnry 的 SN (serial number) 必须大于等于投票者。
而且有一个安全截断机制:
○ Follower 在接收到 logEntry 的时候,若是发现发送者节点当前的 Term 大于等于 Follower 当前的 Term;而且发现相同序号的(相同 SN)LogEntry 在 Follower 上存在,未 Commit,而且 LogEntry Term 不一致,那么 Follower 直接截断从 (SN~文件末尾)的全部内容,而后将接收到的 LogEntryAppend 到截断后的文件末尾。
在以上条件下,Raft 论文列举了一个 Corner Case ,以下图所示:
○ (a):S1 成为 Leader,Append Term2 的LogEntry(黄色)到 S一、S2 成功;
○ (b):S1 Crash,S5 使用 Term(3) 成功竞选为 Term(3) 的 Leader(经过得到 S三、S四、S5 的投票),而且将 Term 为 3 的 LogEntry(蓝色) Append 到本地;
○ (c):S5 Crash, S1 使用 Term(4) 成功竞选为Leader(经过得到 S一、S二、S3 的投票),将黄色的 LogEntry 复制到 S3,获得多数派响应(S一、S二、S3) 的响应,提交黄色 LogEntry 为 Commit,并将 Term 为 4 的 LogEntry (红色) Append 到本地;
○ (d) :S5 使用新的 Term(5) 竞选为 Leader (获得 S二、S三、S4 的投票),按照协议将全部全部节点上的黄色和红色的 LogEntry 截断覆盖为本身的 Term 为 3 的 LogEntry。
进行到这步的时候咱们已经发现,黄色的 LogEnry(2) 在被设置为 Commit 以后从新又被否认了。
因此协议又强化了一个限制;
○ 只有当前 Term 的 LogEntry 提交条件为:知足多数派响应以后(一半以上节点 Append LogEntry 到日志)设置为 commit;
○ 前一轮 Term 未 Commit 的 LogEntry 的 Commit 依赖于高轮 Term LogEntry 的 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 设置为 S2 而且持久化,而后返回 S1。即 S1 成功成为 Term 为 2 的 Leader 的前提是一个多数派已经记录 Current Term 为 2 ,而且 VotedFor 为 S2。那么 (b) 状态 S5 如使用 Term 为 2 进行 Leader 选举,必然得不到多数派赞成,由于 Term 2 已经投给 S1,S5 只能 将 Term++ 使用Term 为3 进行从新发起请求。
Current Term、VotedFor 如何持久化?
type CurrentTermAndVotedFor {
Term int64 json:"Term"
VotedFor int64 json:"Votedfor"
Crc int32
}
//current state
var currentState CurrentTermAndVotedFor
.. set value and calculate crc ...
content, err := json.Marshal(currentState)
//flush to disk
f, err := os.Create("/dist/currentState.txt")
f.Write(content)
f.Sync()
简单的方法,只须要保存在一个单独的文件,如上为简单的 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
论文中提如下几个关键点:
○ 因为 Single 方式不管如何 Cold 和 CNew 都会相交,因此 raft 采用了直接提交一个特殊的 replicated LogEntry 的方式来进行 single 集群关系变动。
○ 跟普通的 LogEntry 提交的不一样点,configuration LogEntry 不须要 commit 就生效,只须要 append 到 Log 中便可。( PS: 原文 “The New configuration takes effect on each server as soon as it is added to the server’s log”)。
○ 后一轮 MemberShip Change 的开始必须在前一轮 MemberShip Change Commit 以后进行,以免出现多个 Leader 的问题。
关注点 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 实例可对外提供服务。
其余须要关注的事项
○ servers process incoming RPC requests without consulting their current configurations. server 处理在 AppendEntries & Voting Request 的时候不用考虑本地的 configuration 信息。
○ CatchUp:为了保障系统的可靠性和可用性,加入 no-voting membership 状态,进行 CatchUp,须要加入的节点将历史 LogEntry 基本所有 Get 到以后再发送 Configuration。
○ Disruptive serves:为了防止移除的节点因为没有接收到新的 Leader 的心跳,而发起 Leader 选举而扰绕当前正在进行的集群状态。集群中节点在 Leader 心跳租约期间内收到 Leader 选举请求能够直接 Deny。(PS:固然对于一些肯定性的事情,好比发现 Leader listen port reset,那么能够发起强制 Leader 选举的请求)。
参考文献:
Raft Paper:
https://raft.github.io/raft.pdf
Raft 博士论文:
https://web.stanford.edu/~ous...
事务性(Transactional)存储须要硬件参与吗?
https://www.zhihu.com/questio...
文章来源: 网易云社区