下面以“苏秦困境”来了解拜占庭将军问题,便于牢记。
战国时期,齐、楚、燕、韩、赵、魏、秦七雄并立,后来秦国的势力不断强大起来,成了东方六国的共同威胁。于是,这六个国家决定联合,全力抗秦,免得被秦国各个击破。一天,苏秦作为合纵长(首领),挂六国相印,带着六国的军队叩关函谷,驻军在了秦国边境,为围攻秦国作准备。但是,因为各国军队分别驻扎在秦国边境的不同地方,所以军队之间只能通过信使互相联系,这时,苏秦面临了一个很严峻的问题:如何统一大家的作战计划?万一某些诸侯国在暗通秦国,发送误导性的作战信息,怎么办?如果信使被敌人截杀,甚至被敌人间谍替换,又该怎么办?这些都会导致自己的作战计划被扰乱,然后出现有的诸侯国在进攻,有的诸侯国在撤退的情况,而这时,秦国一定会趁机出兵,把他们逐一击破的。
提示,下面故事中的名次解释:
- 故事里的各位将军,你可以理解为计算机节点
- 忠诚的将军,你可以理解为正常运行的计算机节点
- 叛变的将军,你可以理解为出现故障并会发送误导信息的计算机节点
- 信使被杀,可以理解为通讯故障、信息丢失
- 信使被间谍替换,可以理解为通讯被中间人攻击,攻击者在恶意伪造信息和劫持通讯
为了便于理解,我们现在只假设有三个国家要合作攻秦,分别是齐、楚、燕,若想打败秦国,至少需要两国合作,而这三国中有一个叛徒,我们要如何达成一致?即让忠诚的国家都保持一致。
这三个国家(齐楚燕)分别由他们各自的将军带领(可以理解为三个节点),他们会按照“少数服从多数”的原则执行收到的指令。但是将军数仍不够,我们需要引入第四个将军——苏秦,这样一来,原本的三将军协商作战,变成了要四个将军协商作战(相当于增加讨论中忠诚将军的数量)。
四个将军会进行两轮作战信息协商,第一轮协商时,会有一个将军率先发出消息给另外三个将军,这三个将军在接收到指令之后,他们三个会准备第二轮协商。第二轮协商时,这三个收到信息的将军分别给另外两个将军发送刚刚自己收到的信息,最后他们各自查看哪个命令最多,自己就执行哪个。并且这四个将军还有一个约定,“若是没有接到命令,则默认撤退”。
现在这四个将军中,有一个叛徒,他们可能的情况如下:
为了演示方便,假设苏秦先发起作战信息,作战指令是“进攻”。那么在第一轮作战信息协商中,苏秦向另外三个将军发送作战指令“进攻”。
在第二轮作战信息协商中,齐楚燕分别向另外 2 位发送作战信息“进攻”,但是楚已经叛变了,所以,为了干扰作战计划,他就对着干,发送“撤退”作战指令。
最终,三位将军收到的指令如下
按照少数命令服从多数命令的原则,最后能实现“收到消息的三国中,忠诚国能执行正确命令”。
为了使忠诚国产生分歧,在第一轮作战信息协商中,叛徒楚向苏秦发送作战指令“进攻”,向齐、燕发送作战指令“撤退”。
第二轮作战信息协商中,苏秦、齐、燕分别向另外两位发送刚刚接收到的作战信息。
最终,三位将军收到的指令如下
最后,忠诚国的作战方案仍达成一致。
如果叛徒人数为 m,则总将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了。这个算法有个前提,先需要知道能容忍的叛将数 m,然后计算最终部署的人数使 3m+1 。你也可以从另外一个角度理解:n 位将军,最多能容忍 (n - 1) / 3 位叛将。(具体推导请参考论文)
该方案要解决的是,在不额外添加将军的情况下,达成一致性(所以下面的例子就只有三个将军)。该方案需要实现如下特性:
下面仍是展示两种情况
齐先发出消息,叛徒楚尝试篡改。
燕在接收到叛徒楚的作战信息后,会发现齐的作战信息被修改,楚已叛变,这时他只执行齐发送的作战信息。
叛徒楚先发送有分歧的作战信息,那么齐和燕将发现叛徒楚发送的作战信息是不一致的,会知道楚叛变。这个时候,他们可以先处理叛将,然后再重新协商作战计划。
最后,忠诚的将军们仍能达成一致的作战计划。
除了故事中提到两种算法,常用的拜占庭容错算法(BFT)还有:
在分布式计算机系统中,最常用的是非拜占庭容错算法(CFT),CFT解决的是分布式系统中存在故障,但不存在恶意节点的场景下的共识问题,也就是说,这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。其常见的算法有:
如果能确定该环境中各节点是可信赖的,不存在篡改消息或者伪造消息等恶意行为,推荐使用非拜占庭容错算法;反之,推荐使用拜占庭容错算法,例如在区块链中使用 PoW 算法。
CAP 理论像 PH 试纸一样,可以用来度量分布式系统的酸碱值,帮助我们思考如何设计合适的酸碱度,在一致性和可用性之间进行妥协折中,设计出满足场景特点的分布式系统。
- 酸(ACID理论)
- 碱(BASE理论)
所谓的 CAP 理论,就是对分布式系统的特性进行了高度抽象,形成了三个指标:
需要注意的是,在分布式系统中,P是一定要考虑的,因为舍弃 P,就意味着舍弃分布式系统,故也没有“CA模型”的说法。
CAP 不可能三角说的是对于一个分布式系统而言,一致性、可用性、分区容错性,这 3 个指标不可兼得,最多选其中 2 个。然后P是一定要保重的,故存在AP模型和CP模型。
大部分人对 CAP 理论有个误解,认为无论在什么情况下,分布式系统都只能在 C 和 A 中选择 1 个。
其实,在不存在网络分区的情况下,也就是分布式系统正常运行时,就是说在不需要 P 时,C 和 A 能够同时保证。只有当发生分区故障的时候,也就是说用到P时,才会在A和C之间二选其一。
而且如果各节点数据不一致会影响到系统运行或业务运行,推荐选择 C 一致性,否则选 A 可用性。
关于一致性的程度:满足ACID的事务 > 强一致性 > 最终一致性
- 满足ACID的事务:要求所有节点都确认
- 强一致性:大多数节点确认即可
ACID特性,可以理解为CAP理论中关于“一致性的边界”,是最强的一致性。
ACID就是事务的四大特性,不必多述。在单机上实现 ACID 是比较容易的,比如可以通过锁、时间序列等机制保障操作的顺序执行。而在分布式系统中是很难实现的,是因为分布式系统涉及多个节点间的操作,我们使用加锁、时间序列等机制,只能保证单个节点上操作的 ACID 特性,无法保证节点间操作的 ACID 特性。
因此我们需要引入二阶段提交协议和TCC。为了便于理解,下面仍使用“苏秦”的故事讲解。
背景:苏秦想协调赵魏韩三国,明天一起行动攻秦。那么对苏秦来说,他面临的问题是,如何高效协同赵、魏、韩一起行动,并且保证当有一方不方便行动时,取消整个计划。(要么全部成功,要么全部失败)
二阶段提交协议,它不仅仅是协议,它还是一个重要的思想,有许多算法都是由它衍生出来的。
苏秦发消息给赵,赵接收到消息后就扮演协调者的身份,赵去联系魏、韩,发起二阶段提交。(此处,苏秦可以理解为客户端,客户端发消息给“赵节点”,由“赵节点”去同步其他节点)
二阶段提交协议最早是用来实现数据库的分布式事务的,不过现在最常用的协议是 XA 协议。比如 MySQL 就是通过 MySQL XA 实现了分布式事务。
但是不管是原始的二阶段提交协议,还是 XA 协议,都存在一些问题,在提交请求阶段,需要预留资源,并且在资源预留期间,其他人不能操作。所以我们无法根据业务特点弹性地调整锁的粒度,为此,我们可以选择TCC协议。
TCC协议的全称是Try-Confirm-Cancel,它是三个操作的缩写,但我们在使用时只会用到其中两个。
TCC 本质上是补偿事务,它的核心思想是针对每个操作都要注册一个与其对应的确认操作和补偿操作(也就是撤销操作)。
TCC是一个业务层面的协议,也就是说,我们要通过代码实现这三个操作,同时还要考虑这两个CC操作(确认和取消)是幂等的,因为这两个操作可能有失败重试的情况。
TCC 不依赖于数据库的事务,而是在业务中实现了分布式事务,这样能减轻数据库的压力,但对业务代码的入侵性也更强,实现的复杂度也更高。所以作者推荐在需要用到分布式事务时,优先考虑现成的事务型数据库(如MySQL XA),当现有的事务型数据库不能满足业务的需求时,再考虑用 TCC 实现分布式事务。
三阶段提交协议,虽然针对二阶段提交协议的“协调者故障,参与者长期锁定资源”的痛点,通过引入了询问阶段和超时机制,来减少资源被长时间锁定的情况,不过这会导致集群各节点在正常运行的情况下,使用更多的消息进行协商,增加系统负载和响应延迟。也正是因为这些问题,三阶段提交协议很少被使用。
集群的可用性是每个节点的可用性的乘积,比如,存在某 3 个节点的集群,每个节点的可用性为 99.9%,那么整个集群的可用性为 99.7%,也就是说,每个月可能宕机 129.6 分钟,这是非常严重的问题。
BASE 理论可以说是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。几乎所有的互联网后台分布式系统都有 BASE 的支持,这个理论很重要,地位也很高。
BASE理论的核心就是基本可用和最终一致性。
基本可用是指当分布式系统在出现故障时,允许损失部分功能,保障核心功能的可用性。就像弹簧一样,遇到外界的压迫,它不是折断,而是变形伸缩,不断适应外力。
基本可用在本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。
最终一致性是说,系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。即在数据的同步上允许存在一个短暂的延迟。
几乎所有的互联网系统采用的都是最终一致性。
通常为两种方案:
通常为以下三种:
Basic Paxos 算法,描述的是多节点之间如何就某个值达成共识。
在该算法中,有这么几个重要的概念
假设我们要实现一个分布式集群,这个集群是由节点 A、B、C 组成,提供只读 KV 存储服务。因此可知,创建只读变量的时候,必须要对它进行赋值,首次赋值成功后,后面再次赋值以尝试修改它时会失败。
现在假设我们有多个客户端,他们都尝试去赋值,当然了,最终只能有一个客户端能成功。客户端1试图创建值为 3 的数据,客户端2试图创建值为 7 的数据。
我们假设客户端 1 的提案编号为 1,客户端 2 的提案编号为 5,并假设节点 A、B 先收到来自客户端 1 的准备请求,而节点 C 却先收到来自客户端 2 的准备请求。
客户端 1、2 作为提议者,分别向所有接受者发送包含提案编号的准备请求。在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了。
节点 A、B 收到提案编号为 1 的准备请求,节点 C 收到提案编号为 5 的准备请求后,由于之前没有通过任何提案,所以节点 A、B 将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 1 的准备请求,也不会通过编号小于 1 的提案。
节点 C 也是如此,它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案。
客户端 1、2 在收到大多数节点的准备响应之后,会分别发送接受请求,接受请求中要带上提议值。
当客户端 1 收到大多数的接受者(节点 A、B)的准备响应后,根据响应中提案编号最大的提案的提议值,设置接受请求中的提议值。因为该值在来自节点 A、B 的准备响应中都为空(尚无提案),所以就把自己的提议值 3 作为提案的值,发送接受请求[1, 3]。
当客户端 2 收到大多数的接受者的准备响应后(节点 A、B、C),根据响应中提案编号最大的提案的提议值,来设置接受请求中的提议值。因为该值在来自节点 A、B、C 的准备响应中都为空(尚无提案),所以就把自己的提议值 7 作为提案的值,发送接受请求[5, 7]。
这样一来,就相当于两个客户端各自想设置不同的值了。
当三个接受者收到这两个客户端的请求后,会进行如下处理。
会发现,提案[1,3]会被拒绝,而选择提案[5,7],具体原因如下:
至此,该算法的流程就结束了。此后,这个值——7,就不能再改了!不过集群的提议编号可能会变化。
假设,上述示例中,节点 A、B 已经通过了提案[5, 7],节点 C 未通过任何提案,那么当客户端 3 提案编号为 9 时,通过 Basic Paxos 算法尝试执行“SET X = 6”,最终三个节点上 X 值(提议值)是多少?
解答:
- 准备阶段,节点C收到客户端3的准备请求[9,6], 因为节点C未收到任何提案,所以返回“尚无提案”的响应。如果此时节点C收到了以前客户端的准备请求[5, 7], 根据提案编号5小于它之前响应的准备请求的提案编号9,会丢弃该准备请求,若没收到以前的请求,也毫无影响,因为当前的编号已经到9,大于以前的请求了。
- 客户端3发送准备请求[9,6]给节点A,B,这时因为节点A,B以前通过了提案[5,7], 根据“如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含已经通过的最大编号的提案信息”,所以节点A,B会返回[5,7]给客户端3,然后客户端3用这里面的7作为提议值,构建出这个提议,即[9,7]。
- 接受阶段,客户端3发送会接受请求[9,7]给节点A,B,C,因为此时请求编号9不小于之前的请求编号,所以所有节点接受该请求[9,7]。
- 最后,学习者会接受达成共识的值,保存该数据。
此处的标题,我特意写了是“思想”,而不是“算法”。
Multi-Paxos 是一种思想,不是算法,而且还缺少算法过程的细节和编程所必须的细节,比如如何选举领导者等,这也就导致了每个人实现的 Multi-Paxos 都不一样。可以理解为,Multi-Paxos 是一个统称,它是指基于 Multi-Paxos 思想,通过多个Basic Paxos 实例实现一系列数据的共识的算法。
Basic Paxos 只能就单个值达成共识,一旦遇到为一系列的值实现共识的时候,它就不管用了,这个时候我们就需要使用Multi Paxos。下面将讲解基于 Chubby 的 Multi-Paxos实现细节。
Basic Paxos是采用二阶段提交来实现的。
如果多个提议者同时提交提案,可能出现因为提案冲突,在准备阶段没有提议者接收到大多数准备响应,就会协商失败,需要重新协商。
你想象一下,一个 5 节点的集群,如果 3 个节点作为提议者同时提案,就可能发生因为没有提议者接收大多数响应(比如 1 个提议者接收到 1 个准备响应,另外 2 个提议者分别接收到 2 个准备响应)而准备失败,需要重新协商。
如何避免协商失败?
这两阶段,采用RPC通讯,往返消息多、耗性能、延迟大,如何减少往返消息,提高性能?
通过引入领导者和优化 Basic Paxos 执行来解决。
上面两个方案,就是Multi Paxos的核心。下面看一下 Chubby 是如何补充细节,实现 Multi-Paxos 算法的。
首先,Basic Paxos 是经过证明的,可以放心使用。而 Multi-Paxos 算法是未经过证明的。比如 Chubby 的作者做了大量的测试,和运行一致性检测脚本,验证和观察系统的健壮性。在实际使用时,不推荐你设计和实现新的 Multi-Paxos 算法,而是建议优先考虑 Raft 算法,因为Raft 的正确性是经过证明的。当 Raft 算法不能满足需求时,再考虑实现Multi-Paxos算法。
Raft 算法属于 Multi-Paxos 算法,属于共识算法,它做了简化和限制,在理解和算法实现上都相对容易许多。除此之外,Raft 算法是现在分布式系统开发首选的共识算法。
在Raft算法中,集群中的节点有三种状态,在任何时候,每一个服务器节点都处于这 3 个状态中的 1 个。
- 跟随者:普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
- 候选人:想当领导的人,候选人将向其他节点发送请求投票,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
- 领导者:霸道总裁,一切以我为准,平常的主要工作内容就是,处理写请求、管理日志复制、不断地发送心跳信息。
若要用一句话概括Raft算法,那就是,“通过一切以领导者为准的方式实现值的共识和各节点日志的一致”。
需要注意的是,Raft 算法是强领导者模型,集群中只能有一个“霸道总裁”。
接下来对这三个核心概念逐一介绍。
假设我们有一个由节点 A、B、C 组成的 Raft 集群,Raft 算法如何保证在同一个时间,集群中只有一个领导者呢?
在初始状态下,集群中的所有节点都是“跟随者”。每个跟随者都有各自的“随机超时时间”,即不同的跟随者的超时时间是不同的(这样一来,大部分情况下,都会有一个跟随者率先超时并变成候选人开始“自荐”,避免多个候选人同时竞争领导者,分散了选票而导致选举失败)。
根据上图可知,节点A会率先超时并变成候选人,然后将自己的任期编号+1,再向其他节点发出请求投票,希望其他节点投它一票。
其他节点收到投票请求后,若他们此前没投过票(要保证一人一票),并且他们的日志记录的完整性不高于候选人A,那就将选票给A,然后他们发现自己的任期编号比候选人的任期编号小,故其他节点将自己的任期编号与A同步,即变成1。
如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。节点 A 当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举。
到此,新的领导人就选举成功了。
节点之间采用RPC通信,包含两种RPC:
每个任期由单调递增的数字标识,任期编号是随着选举的举行而变化的。它的变化规律如下:
副本数据是以日志的形式存在的,日志是由日志项组成。
日志项是一种数据格式,它包含如下数据:
- 指令:一条由客户端请求指定的、状态机需要执行的指令。可以理解为用户指定的数据。
- 索引值:日志项对应的整数索引值,即用来标识日志项的。
- 任期编号:创建这条日志项的领导者的任期编号。
如图所示,一届领导者任期,往往有多条日志项,且日志项的索引值是连续的。
上图存在的问题:为什么图中 4 个跟随者的日志都不一样呢?日志是怎么复制的呢?又该如何实现日志的一致呢?下面逐一解释。
可以把 Raft 的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段),减少了一半的往返消息,即降低了一半的消息延迟。
具体过程如下:
在实际环境中,复制日志的时候,可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致。那么在这种情况下,Raft算法是如何处理不一致日志的呢?
在 Raft 算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志问题。即 Raft 是通过以领导者的日志为准,来实现各节点日志的一致的,具体有 2 个步骤:
总的来说,在日常工作中,集群中的服务器数量是会发生变化的(故障、扩容、缩容等)。那么当成员变更时,集群成员发生了变化,就可能同时存在新旧配置的 2 个“大多数”,出现 2 个领导者,破坏了 Raft 集群的领导者唯一性,影响了集群的运行(这就是成员变更问题)。在Raft中,是通过“单节点变更”来解决这个问题的。
单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。
比如将 3 节点集群扩容为 5 节点集群,这时你需要执行 2 次单节点变更。
- 先将 3 节点集群变更为 4 节点集群。
- 然后再将 4 节点集群变更为 5 节点集群,就像下图的样子。
背景:
假设我们有一个由节点 A、B、C 组成的 Raft 集群,现在我们需要增加数据副本数,增加 2 个副本(也就是增加 2 台服务器),扩展为由节点 A、B、C、D、E,即 5 个节点组成的新集群。
通过“单节点变更”解决的流程:
目前的集群配置为[A, B, C],我们先向集群中加入节点 D(先变更一个节点)。
- 第一步,领导者(节点 A)向新节点(节点 D)同步数据。
- 第二步,领导者(节点 A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点 A、B、C、D)上,然后将新配置的日志项提交到本地状态机,完成单节点变更。
在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点 E(即再变更一个节点,是一个一个的变更)。- 第一步,领导者(节点 A)向新节点(节点 E)同步数据。
- 第二步,领导者(节点 A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项提交到本地状态机,完成单节点变更。
结束
除了“单节点变更”方案以外,还有“联合共识”方案可以解决成员变更问题,但是它难以实现,很少被Raft实现采用。
假设我们有一个KV存储服务器,现在数据量访问增大,便对服务器做集群处理,然后引入Proxy层,由 Proxy 层处理来自客户端的读写请求,接收到读写请求后,通过对 Key 做哈希找到对应的集群。但是缺点也很明显,当需要变更集群数时,这时大部分的数据都需要迁移,并重新映射,数据的迁移成本是非常高的。
因此可以使用“一致性哈希算法”来解决该问题。
一致性哈希算法本质上是一种路由寻址算法,适合简单的路由寻址场景。比如在 KV 存储系统内部。
一致性哈希算法是对 2^32 (全球IPV4总数)进行取模运算。你可以想象下,一致哈希算法,将整个哈希值空间组织成一个虚拟的圆环,也就是哈希环。哈希环的空间是按顺时针方向组织的,圆环的正上方的点代表 0,0点右侧的第一个点代表 1,以此类推,2、3、4、5、6……直到 2^32-1。
当需要对指定 key 的值进行读写的时候,通过下面 2 步进行寻址:
假设,现在有一个节点故障了,比如节点 C。可以看到,key-01 和 key-02 不会受到影响,只有 key-03 的寻址被重定位到 A。即,受影响的数据仅仅是,会寻址到此节点和前一节点之间的数据。
增加一个节点,比如节点D。受影响的数据仅仅是,会寻址到新节点和前一节点之间的数据。
总的来说,使用了一致哈希算法后,扩容或缩容的时候,都只需要重定位或迁移环空间中的一小部分数据。而对于普通哈希算法而言,他就要重定位或迁移更多数据。
所谓“冷热不均”,实际上就是“节点分布不均匀造成数据访问的冷热不均,即说大多数请求都会落在少数节点上”,如下图。
解决方案是:引入虚拟节点。
是对每一个服务器节点计算多个哈希值,在每个计算结果位置上,都放置一个虚拟节点,并将虚拟节点映射到实际节点。如下图有6个虚拟节点,其中的“Node-A-01”和“Node-A-02”,代表的是同一个A节点。
增加了节点后,节点在哈希环上的分布就相对均匀了。如果有访问请求寻址到“Node-A-01”这个虚拟节点,将被重定位到节点 A。
使用一致性哈希算法,可以通过增加节点数量达到以下效果:
Gossip 协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。(即实现最终一致性)
下面逐一介绍
直接邮寄:就是直接发送更新数据,当数据发送失败时,将数据缓存下来,然后做重传。如下图:
直接邮寄是一种简单的实现方案,数据同步也很及时,它的缺点是“存在缓存队列满时出现数据丢失”的情况。可以说,只使用直接邮寄的话,是无法实现最终一致性的。
反熵指的是集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。
反熵又细分为三种操作:推、拉、推拉。
将自己的所有副本数据,推给对方,修复对方副本。
拉取对方的所有副本数据,修复自己副本。
同时修复自己副本和对方副本。
反熵需要节点两两交换和比对自己所有的数据,执行反熵时通讯成本会很高,所以我不建议你在实际场景中频繁执行反熵,并且可以通过引入校验和等机制,降低需要对比的数据量和通讯消息。
是执行反熵时,相关的节点都是已知的,而且节点数量不能太多,如果是一个动态变化或节点数比较多的分布式环境(比如在 DevOps 环境中检测节点故障,并动态维护集群节点状态),这时反熵就不适用了。该采用“谣言传播”。
关于“反熵”这个名词,可以这么理解,反熵中的熵是指混乱程度,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,降低熵值。
谣言传播,指的是当一个节点获得新数据后,这个节点变成活跃状态,并周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。
“谣言传播”适合动态变化的分布式系统。
上面提到过,在分布式存储系统中,实现数据副本最终一致性,最常用的方法就是“反熵”。下面来看一个具体的“反熵”实现方案,是自研的InfluxDB的反熵实现例子。
在自研 InfluxDB 中,一份数据副本是由多个分片组成的,三节点三副本的集群,如下图所示:
我们要确保,不同节点上,同一分片组中的分片都没有差异。例如,节点 A 要拥有分片 Shard1 和 Shard2,而且,节点 A 的 Shard1 和 Shard2,与节点 B、C 中的 Shard1 和 Shard2,是一样的。
我们可能出现数据丢失的问题,我们将数据缺失,分为这样 2 种情况:
第一种情况修复起来不复杂,我们只需要将分片数据,通过 RPC 通讯,从其他节点上拷贝过来就可以了。
直接把Shard2的整个分片拷贝过来。
第二种情况修复起来要复杂一些。我们需要设计一个闭环的流程,按照一个顺序地修复,执行完一轮流程后,就能实现了最终一致性了。
具体流程:先随机选择一个节点,然后循环修复,每个节点生成“自己节点有但下一个节点没有的差异数据”,发送给下一个节点,进行修复。
案例,为了方便演示,假设 Shard1、Shard2 在各节点上是不一致的。
上图中,数据修复的起始节点为节点 A,数据修复是按照顺时针顺序,循环修复的。需要注意的是,最后节点 A 又对节点 B 的数据执行了一次数据修复操作,因为只有这样,“节点 C 有但节点 B 缺失的差异数据”,才会同步到节点 B 上。
此实现方案,和我们一开始说的反熵有些区别,一开始说的反熵是“随机选择某个其他节点去进行数据同步”,而此处的实现方案并不是随机地选择节点,仅仅是在一开始随机选择一个起始节点,然后接下来的修复是有序的。选择有序修复的原因是——我们希望能在一个确定的时间范围内实现数据副本的最终一致性,而不是基于随机性的概率,在一个不确定的时间范围内实现数据副本的最终一致性。
实现数据副本的最终一致性时,一般来说:
假设我们刚刚开发好一套AP型分布式系统,此时若又来了新需求,要求写入数据后能立刻读到新数据,即实现“强一致性”。而我们这个系统系统是AP型的,难道要重新开发出另一套系统吗?显然是不可能的,我们需要的是一个能够灵活变换一致性级别的系统,即能够自定义一致性级别。
Quorum NWR,我们可以自定义一致性级别。即我们可以根据业务场景的不同,通过调整写入或者查询的方式,完成对“最终一致性”和“强一致性”的切换。在 Quorum NWR 中有三个要素,分别是N、W、R,下面逐一介绍。
N 表示副本数,又称复制因子,即N表示集群中的同一份数据有多少个副本。
如上图,集群有三个节点,DATA-1有2个副本,即它的N为2。而DATA-2的N为3,DATA-3的N为1。不同的数据可以设置不同副本数N。
W,又称写一致性级别,表示成功完成 W 个副本更新,才完成写操作。
如上图,DATA-2的写一致性级别为2,我们对 DATA-2 执行写操作时,完成 2 个副本的更新(比如节点 A、C),才完成写操作。
此处你可能会有疑问,我们这里对节点A、C更新后,就算完成写操作了。那我们读数据的时候读的是节点B呢?那读到的此不是旧数据?这其实是可以避免的,请看接下来的R。
R,又称读一致性级别,表示读取一个数据对象时需要读 R个副本。即读取指定数据时,要读 R 个副本,然后返回 R 个副本中最新那份数据。
如上图,我们将DATA-2的读一致性级别设置为2,我们就可以回答上面的问题。我们从节点B、C中读取数据,B中的数据是旧的,但是C中数据是新的,所以我们返回C中的那份最新数据,那就保证了强一致性。(若此处将R设置为1,那就不能保证强一致性了)。
N、W、R 值的不同组合,会产生不同的一致性效果,具体来说有两种: