分布式一致性协议在MongoDB选举中的应用

前言

之前在学习MongoDB复制集时发现网上的很多相关的分享都是针对r3.2.0以前的版本,新版本对选举机制做了较大的更改,但是在网上大多都是一笔带过。于是写过一篇《MongoDB选举机制》。当时主要结合了官网最新的文档和部分源码。由于官网介绍比较泛泛,源码阅读的时候又受限于个人能力与时间,最终只整理了选举相关的核心逻辑,对应复杂的条件检查和数据结构没有深入的了解。最近了解了一些分布式一致性算法之后,打算换一个角度从协议层面重新整理一下MongoDB的选举。本文简要介绍分布式系统一致性相关的概念,重点介绍Bully算法和Raft算法在MongoDB选举中的应用。

分布式系统简介

分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。分布式系统具有分布性、对等性、并发性、缺乏全局时钟、故障总会发生的特点,由于这些特点导致了分布式系统中的一致性难题。

CAP理论

分布式系统的CAP理论:

CAP理论告诉我们,一个分布式系统不可能同时满足一致性(C),可用性(A),分区容错性(P)这三个基本需求,最多只能同时满足其中两项。

  • 分布式环境中的一致性是指数据在多个副本之间是否能够保持一致的特性。
  • 可用性是指系统必须一致处于可用的状态,对用用户的请求能够在有限时间内返回结果。其中,“有限时间”是相对的,是用户可接受的合理响应时间;“返回结果”是期望的结果,可以是成功或失败,不能是用户不能接受的结果。
  • 分布式系统在遇到网络分区故障时,仍能够对外提供满足一致性和可用性的服务,除非整个网络发生故障。

由于分区容错性是分布式系统首先要保证的,所以只能在一致性和可用性之间找平衡。

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致)三个短语的简写,基于CAP发展而来,主要解决一致性和可用性之间的平衡。

  • 基本可用是指允许故障发生时损失部分可用性,但不是说系统不可用。比如MongoDB选举过程中系统是只读的。
  • 软状态是指允许数据存在中间状态,既允许数据不同副本之间的同步过程存在延时。
  • 最终一致是指数据副本在经过一段时间的软状态后最终达到一致。最终一致是一种特殊的弱一致性。

Paxos,Raft,ZAB等常见的分布式一致性算法都是符合BASE理论。

MongoDB的选举机制

MongoDB复制集的目的是提供高可用性,如果一个写操作被复制到大多数节点,只要任意大多数节点可用,那么数据就是可靠的。复制集一般包含一个Primary和若干个Secondary节点。选举机制主要负责在集群初始化及节点发生变化时重新选举主节点,保证系统可用性。

MongoDB节点之间维护心跳检查,主节点选举由心跳触发。MongoDB复制集成员会向自己之外的所有成员发送心跳并处理响应信息,因此每个节点都维护着从该节点POV看到的其他所有节点的状态信息。节点根据自己的集群状态信息判断是否需要发起选举。

MongoDB r3.2.0以前选举协议是基于Bully算法,从r3.2.0开始默认使用基于Raft算法的选举策略。MongoDB选举过程比较复杂,下面的在介绍算法应用时重点关注与算法应用相关的逻辑。

Bully算法

算法描述

Bully算法是一种相对简单的协调者竞选算法。算法的主要思想是集群的每个成员都可以声明它是协调者并通知其他节点。别的节点可以选择接受这个声称或是拒绝并进入协调者竞争。被其他所有节点接受的节点才能成为协调者。节点按照一些属性来判断谁应该胜出。

Bully算法中有三类消息:
- Election Message:宣布发起选举
- Answer Message:响应选举消息
- Victory Message:由选举胜利者发送,宣布胜利

当一个节点从故障中恢复或者监测到主节点故障时,执行过程如下:
- 如果该节点具有最高ID,则向其他节点发送Victory Message,成为主节点并结束选举,否则向所有编号比它大的节点发送Election Message;
- 如果该节点规定时间内没有收到答复,则向其他节点发送Victory Message,成为主节点并结束选举;
- 如果有ID比该节点大的节点响应,则等待Victory Message(如果一定时间后仍未收到Victory Message,则重新发起选举)
- 如果一个节点收到较低ID发来的Election Message,它将发送一个Answer Message,并发送Election Message到更高ID的节点开始选举。
- 如果一个节点收到Victory Message,则将发送者视为主节点。

举例

下面是Bully算法选举过程的一个经典例子:

分布式一致性协议在MongoDB选举中的应用_第1张图片

  • 1,最初集群中有5个节点,节点5是一个公认的协调者
  • 2,如果节点5挂了,并且节点2和节点3同时发现了这一情况。两个节点开始竞选并发送竞选消息给ID更大的节点。
  • 3,节点4淘汰了节点2和3,节点3淘汰了节点2
  • 4,这时候节点1察觉了节点5失效并向所有ID更大的节点发送了竞选信息
  • 5,节点2、3和4都淘汰了节点1
  • 6,节点4发送竞选信息给节点5
  • 7,节点5没有响应,所以节点4宣布自己当选并向其他节点通告了这一消息

算法应用

MongoDB在实现时对Bully算法做了一些调整:
- Bully ID对应MongoDB节点的priority,且两个节点priority可能相等
- Secondary发现集群中没有Primary后不止向更高priority节点,而是向所有节点发送选举
- 收到选举请求的节点发现候选节点中有更高priority节点时投veto票,每个veto会将得票数-10000
- MongoDB没有vote权限的节点才能投票
- 发起选举过程中的节点不会投票
- 选举发起和投票过程中包含复杂的条件判断,但不影响协议理解
- 通过审核的节点会投vote票,每个vote会将得票数+1
- 最终得票数超过集群可投票节点数一半时通过选举,发起节点成为Primary。
- 当得票不足一半时,发起者随机退让,随机sleep 0 到1 秒后重新发起选举。
- 引入30s的“选举锁”,在持有锁的时间内不得给其他发起者投票,避免一个节点同时给两个发起者投票。
- 如果新选举出的主节点立马挂掉,至少需要30s时间重新选主。

Raft算法

新版本的MongoDB用Raft取代了Bully。

算法描述

Raft将问题拆成数个子问题分开解决:
- 领袖选举(Leader Election)
- 记录复写(Log Replication)
- 安全性(Safety)

这里我们主要关注领袖选举。
Raft cluster中为服务器定义了三种角色:
- Leader:领导者
- Flower:追随者
- Candidate:候选者

在正常情况下,集群中只有一个 leader 并且其他的节点全部都是 follower 。Follower 都是被动的:他们不会发送任何请求,只是简单的响应来自 leader 和 candidate 的请求。三种角色的转换关系:

分布式一致性协议在MongoDB选举中的应用_第2张图片

Raft用Term区分每一次任期(包含一次选举),Term在Raft中起到逻辑时钟(时序)的作用,Term是单调递增的。每个节点都保存当前的Term,并在服务器间通信中包含Term字段。
Raft为实现一致性定义了两种基本RPC:
- 请求投票(RequestVote) RPC 由 candidate 在选举期间发起
- 追加条目(AppendEntries)RPC 由 leader 发起,用来复制日志和提供一种心跳机制

Raft 用心跳来触发 leader 选举。当服务器程序启动时,他们都是 follower 。一个服务器节点只要能从 leader 或 candidate 处接收到有效的 RPC 就一直保持 follower 状态。Leader 周期性地向所有 follower 发送心跳(不包含日志条目的 AppendEntries RPC)来维持自己的地位。如果一个 follower 在一段选举超时时间内没有接收到任何消息,它就假设系统中没有可用的 leader ,然后开始进行选举以选出新的 leader 。

选举规则中有一些限制条件:
- 对于同一个任期,每个服务器节点只会投给一个 Candidate ,按照先来先服务的原则。
- 只有当候选人的操作日志至少跟投票人一样新时,才投赞同票。
- 如果一个 Follower 在一段选举超时时间内没有接收到任何消息,则发起选举。
- 每个节点的选举超时有一定随机性,新任期重置选举超时。

下面详细的介绍一下选举的过程:

  • follower 先增加自己的当前任期号并且转换到 Candidate 状态。
  • 投票给自己并且并行地向集群中的其他服务器节点发送 RequestVote RPC。
  • Candidate 会一直保持当前状态直到以下三件事情之一发生:
    • (a) 收到过半的投票,赢得了这次的选举
    • (b) 其他的服务器节点成为 leader
    • (c) 一段时间之后没有任何获胜者。

Candidate 赢得选举
当一个 Candidate 获得集群中过半服务器节点针对同一个任期的投票,它就赢得了这次选举并成为 Leader 。对于同一个任期,每个服务器节点只会投给一个 Candidate ,按照先来先服务的原则。一旦 Candidate 赢得选举,就立即成为 Leader 。然后它会向其他的服务器节点发送心跳消息来确定自己的地位并阻止新的选举。

其他节点成为Leader
在等待投票期间,Candidate 可能会收到另一个声称自己是 leader 的服务器节点发来的 AppendEntries RPC 。如果这个 Leader 的任期号(包含在RPC中)不小于 candidate 当前的任期号,那么 Candidate 会承认该 Leader 的合法地位并回到 Follower 状态。 如果 RPC 中的任期号比自己的小,那么 Candidate 就会拒绝这次的 RPC 并且继续保持 Candidate 状态。

没有获胜者
第三种可能的结果是 Candidate 既没有赢得选举也没有输:如果有多个 follower 同时成为 candidate ,那么选票可能会被瓜分以至于没有 candidate 赢得过半的投票。当这种情况发生时,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,如果没有其他机制的话,该情况可能会无限重复。

举例

这里还是用Bully算法中的案例来说明:
- 1,最初集群中有5个节点,节点5是 Leader,此时集群Term为1
- 2,如果节点5挂了,并且节点2和节点3同时发现这一情况,两个节点同时发起选举
- 3,节点2,3 Term变为2,转换为Candidate,给自己投一票并分别向其中中其他节点发送RequestVote RPC。
- 假如节点1,4都先收到节点2,Term2 RequestVote:
- 将Term 记为2
- 都会为节点2投票,拒绝后收到的投票,此时节点2胜出,切换为Leader 并发送心跳
- 节点3 受到心跳,且Term不小于自己Term,则转换为Flower,集群达到最终一致
- 假如节点1,4分别先收到节点2,3Term2 的RequestVote:
- 将Term 记为2
- 节点2,3分别收到1票,拒绝后收到的投票,Candidate节点各自总共两票,都不到多半(5/2 + 1),没有获胜者
- 此时会由第一个选举超时的节点,重新发起新一轮Term为3的选举

算法应用

MongoDB在实现时对Raft算法做了一些调整:
- Secondary节点不只是被动接受,也会发起心跳监测
- 当Secondary节点发现集群中没有Primary或者自己priority比Primary高时发起选举
- 保留了优先级但只考虑自己和当前主结点的优先级,其他结点的优先级不决定选举与否,也不影响投票

对比

对比新旧版本的选举:
- Raft为MongoDB选举引入Term,取消Bully的选举锁,效率更高,更优雅的避免重复投票,减少投票等待时间。
- Raft弱化了priority功能,可能出现非最高priority候选节点当选的情况,后续的心跳中会发现,并重新选举。
- 取消了veto,选举不一定需要等待心跳超时。
- 主节点的降级有自己发起,效率更高。

参考

深入理解NoSQL数据库分布式算法及策略
维基百科-Bully algorithm
《从Paxos到ZooKeeper 分布式一致性原理与实践》
《Time, Clocks, and the Ordering of Events in a Distributed System》
《MongoDB 中的Raft一致性协议》

你可能感兴趣的:(分布式系统)