分布式系统一致性问题:Raft 共识算法

776 阅读6分钟

前言

在上篇文章中 CAP 和 BASE 原理 已经提到过 CAP 理论。本文是分享强一致性模型协议Raft的设计原理。

正文

拜占庭将军问题

拜占庭帝国的军队正在围攻一座城市。这支军队被分成了多支小分队,驻扎在城市周围的不同方位,每支小分队由一个将军领导。这些将军们彼此之间只能依靠信使传递消息(无法聚在一起开个会)。每个将军在观察自己方位的敌情以后,会给出一个各自的行动建议(比如进攻、撤退或按兵不动),但最终的需要将军们达成一致的作战计划并共同执行,否则就会被敌人各个击破。但是,这些将军中可能有叛徒,他们会尝试阻止其他忠诚的将军达成一致的作战计划。

很显然,将这一场景套用到计算机系统中也是非常适用的:在一个分布式系统中,针对每个运算,每台独立的机器也需要最终达成一致的结果。但每台计算机之间也只能依靠网络通信(显然它们无法聚在一起开会),每台计算机都有出错的可能(被攻击,或故障),从而变成「叛徒」干扰正常的计算机达成一致。

我们尝试使用拜占庭将军问题来理解 Raft 协议。

假设将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成一致性决定?

Raft 的解决方案大概可以理解成先在所有将军中选出一个大将军,所有的决定由大将军来做。选举环节:比如说现在一共有 3 个将军 A, B, C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就会把自己当成大将军候选人,然后派信使去问其他几个将军,能不能选我为总将军?假设现在将军 A 倒计时结束了,他派信使传递选举投票的信息给将军 B 和 C,如果将军 B 和 C 还没把自己当成候选人(倒计时还没有结束),并且没有把选举票投给其他,他们把票投给将军 A,信使在回到将军 A 时,将军 A 知道自己收到了足够的票数,成为了大将军。在这之后,是否要进攻就由大将军决定,然后派信使去通知另外两个将军,如果在一段时间后还没有收到回复(可能信使被暗杀),那就再重派一个信使,直到收到回复。

节点的三种状态

该协议规定节点处于三种状态之一。

  • Follower(追随者)
  • Candidate(竞选者)
  • Leader(领导者)

从拜占庭将军的故事映射到分布式系统上,每个将军相当于一个分布式网络节点,每个节点有三种状态:Follower,Candidate,Leader,状态之间是互相转换的。

正常情况下选主

在一个节点倒计时结束 (Timeout) 后,这个节点的状态变成 Candidate 开始选举,它给其他几个节点发送选举请求 (RequestVote)

其他四个节点都返回成功,这个节点的状态由 Candidate 变成了 Leader,并在每个一小段时间后,就给所有的 Follower 发送一个 Heartbeat 以保持所有节点的状态,Follower 收到 Leader 的 Heartbeat 后重设 Timeout。

这是最简单的选主情况,只要有超过一半的节点投支持票了,Candidate 才会被选举为 Leader,5 个节点的情况下,3 个节点 (包括 Candidate 本身) 投了支持就行。

Leader 出故障情况下的选主

4 个节点的选主过程和 5 个节点的类似,在选出一个新的 Leader 后,原来的 Leader 恢复了又重新加入了,这个时候怎么处理?在 Raft 里,第几轮选举是有记录的,重新加入的 Leader 是第一轮选举 (Term 1) 选出来的,而现在的 Leader 则是 Term 2,所有原来的 Leader 会自觉降级为 Follower。

复制日志 Log Replication

Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。

客户端发送请求给 Leader,储存数据 “sally”,Leader 先将数据写在本地日志,这时候数据还是 Uncommitted (还没最终确认,红色表示)

Leader 给两个 Follower 发送 AppendEntries 请求,数据在 Follower 上没有冲突,则将数据暂时写在本地日志,Follower 的数据也还是 Uncommitted。

Follower 将数据写到本地后,返回 OK。Leader 收到后成功返回,只要收到的成功的返回数量超过半数 (包含 Leader),Leader 将数据 “sally” 的状态改成 Committed。( 这个时候 Leader 就可以返回给客户端了)

Leader 再次给 Follower 发送 AppendEntries 请求,收到请求后,Follower 将本地日志里 Uncommitted 数据改成 Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的。

Network Partition 情况下进行复制日志

在 Network Partition 的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。

Network Partition 将节点分成两边,一边有两个节点,一边三个节点。

两个节点这边已经有 Leader 了,来自客户端的数据 “bob” 通过 Leader 同步到 Follower。

因为只有两个节点,少于 3 个节点,所以 “bob” 的状态仍是 Uncommitted。所以在这里,服务器会返回错误给客户端。

另外一个 Partition 有三个节点,进行重新选主。客户端数据 “tom” 发到新的 Leader,通过和上节网络状态下相似的过程,同步到另外两个 Follower。

因为这个 Partition 有 3 个节点,超过半数,Leader 广播后,数据 “tom” 都 Commit 了。

网络状态恢复,5 个节点再次处于同一个网络状态下。但是这里出现了数据冲突 “bob" 和 “tom"。

两个节点 Partition 的 Leader 自动降级为 Follower,因为这个 Partition 的数据 “bob” 没有 Commit,返回给客户端的是错误,客户端知道请求没有成功,所以 Follower 在收到 AppendEntries 请求时,可以把 “bob“ 删除,然后同步 ”tom”,通过这么一个过程,就完成了在 Network Partition 情况下的复制日志,保证了数据的一致性。

资源集散地

后语

Raft 是能够实现分布式系统强一致性的算法,每个系统节点有三种状态 Follower,Candidate,Leader。实现 Raft 算法两个最重要的事是:选主和复制日志。一般使用奇数节点(votes>=N/2+1)。