《分布式协议与算法实战》——专栏笔记

拜占庭将军问题——初探

问题描述

下面以“苏秦困境”来了解拜占庭将军问题,便于牢记。

战国时期,齐、楚、燕、韩、赵、魏、秦七雄并立,后来秦国的势力不断强大起来,成了东方六国的共同威胁。于是,这六个国家决定联合,全力抗秦,免得被秦国各个击破。一天,苏秦作为合纵长(首领),挂六国相印,带着六国的军队叩关函谷,驻军在了秦国边境,为围攻秦国作准备。但是,因为各国军队分别驻扎在秦国边境的不同地方,所以军队之间只能通过信使互相联系,这时,苏秦面临了一个很严峻的问题:如何统一大家的作战计划?万一某些诸侯国在暗通秦国,发送误导性的作战信息,怎么办?如果信使被敌人截杀,甚至被敌人间谍替换,又该怎么办?这些都会导致自己的作战计划被扰乱,然后出现有的诸侯国在进攻,有的诸侯国在撤退的情况,而这时,秦国一定会趁机出兵,把他们逐一击破的。

提示,下面故事中的名次解释:

  • 故事里的各位将军,你可以理解为计算机节点
  • 忠诚的将军,你可以理解为正常运行的计算机节点
  • 叛变的将军,你可以理解为出现故障并会发送误导信息的计算机节点
  • 信使被杀,可以理解为通讯故障、信息丢失
  • 信使被间谍替换,可以理解为通讯被中间人攻击,攻击者在恶意伪造信息和劫持通讯

为了便于理解,我们现在只假设有三个国家要合作攻秦,分别是齐、楚、燕,若想打败秦国,至少需要两国合作,而这三国中有一个叛徒,我们要如何达成一致?即让忠诚的国家都保持一致。

解决方案一:口信消息型拜占庭问题之解

具体流程

这三个国家(齐楚燕)分别由他们各自的将军带领(可以理解为三个节点),他们会按照“少数服从多数”的原则执行收到的指令。但是将军数仍不够,我们需要引入第四个将军——苏秦,这样一来,原本的三将军协商作战,变成了要四个将军协商作战(相当于增加讨论中忠诚将军的数量)。

四个将军会进行两轮作战信息协商,第一轮协商时,会有一个将军率先发出消息给另外三个将军,这三个将军在接收到指令之后,他们三个会准备第二轮协商。第二轮协商时,这三个收到信息的将军分别给另外两个将军发送刚刚自己收到的信息,最后他们各自查看哪个命令最多,自己就执行哪个。并且这四个将军还有一个约定,“若是没有接到命令,则默认撤退”。

现在这四个将军中,有一个叛徒,他们可能的情况如下:

情况一:由忠诚将军率先发出消息

为了演示方便,假设苏秦先发起作战信息,作战指令是“进攻”。那么在第一轮作战信息协商中,苏秦向另外三个将军发送作战指令“进攻”。
《分布式协议与算法实战》——专栏笔记_第1张图片在第二轮作战信息协商中,齐楚燕分别向另外 2 位发送作战信息“进攻”,但是楚已经叛变了,所以,为了干扰作战计划,他就对着干,发送“撤退”作战指令。
最终,三位将军收到的指令如下

  • 齐:进、进、撤——最后进攻
  • 燕:进、进、撤——最后进攻
  • 楚:叛徒,不予理会

按照少数命令服从多数命令的原则,最后能实现“收到消息的三国中,忠诚国能执行正确命令”。

情况二:由叛徒将军率先发出消息

为了使忠诚国产生分歧,在第一轮作战信息协商中,叛徒楚向苏秦发送作战指令“进攻”,向齐、燕发送作战指令“撤退”。

《分布式协议与算法实战》——专栏笔记_第2张图片第二轮作战信息协商中,苏秦、齐、燕分别向另外两位发送刚刚接收到的作战信息。
最终,三位将军收到的指令如下

  • 苏:进、撤、撤——最后撤退
  • 齐:撤、进、撤——最后撤退
  • 燕:撤、进、撤——最后撤退

最后,忠诚国的作战方案仍达成一致。

口信消息型拜占庭问题之解所要注意的问题

如果叛徒人数为 m,则总将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了。这个算法有个前提,先需要知道能容忍的叛将数 m,然后计算最终部署的人数使 3m+1 。你也可以从另外一个角度理解:n 位将军,最多能容忍 (n - 1) / 3 位叛将。(具体推导请参考论文)

解决方案二:签名消息型拜占庭问题之解

具体流程

该方案要解决的是,在不额外添加将军的情况下,达成一致性(所以下面的例子就只有三个将军)。该方案需要实现如下特性:

  • 忠诚将军的签名无法伪造,而且对他签名消息的内容进行任何更改都会被发现。
  • 任何人都能验证将军签名的真伪。

下面仍是展示两种情况

情况一:由忠诚将军率先发出消息,叛徒尝试篡改

齐先发出消息,叛徒楚尝试篡改。
《分布式协议与算法实战》——专栏笔记_第3张图片燕在接收到叛徒楚的作战信息后,会发现齐的作战信息被修改,楚已叛变,这时他只执行齐发送的作战信息。

情况二:由叛徒将军率先发出消息,发出有分歧的消息

叛徒楚先发送有分歧的作战信息,那么齐和燕将发现叛徒楚发送的作战信息是不一致的,会知道楚叛变。这个时候,他们可以先处理叛将,然后再重新协商作战计划。
《分布式协议与算法实战》——专栏笔记_第4张图片
最后,忠诚的将军们仍能达成一致的作战计划。

拜占庭将军问题小总结

BFT和CFT

除了故事中提到两种算法,常用的拜占庭容错算法(BFT)还有:

  • PBFT算法
  • PoW算法

在分布式计算机系统中,最常用的是非拜占庭容错算法(CFT),CFT解决的是分布式系统中存在故障,但不存在恶意节点的场景下的共识问题,也就是说,这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。其常见的算法有:

  • Paxos 算法
  • Raft 算法
  • ZAB 协议

如何在实际场景选择合适的算法类型

如果能确定该环境中各节点是可信赖的,不存在篡改消息或者伪造消息等恶意行为,推荐使用非拜占庭容错算法;反之,推荐使用拜占庭容错算法,例如在区块链中使用 PoW 算法。

CAP理论:分布式系统的PH试纸,用它来测酸碱度

CAP 理论像 PH 试纸一样,可以用来度量分布式系统的酸碱值,帮助我们思考如何设计合适的酸碱度,在一致性和可用性之间进行妥协折中,设计出满足场景特点的分布式系统。

  • 酸(ACID理论)
  • 碱(BASE理论)

基础概念

所谓的 CAP 理论,就是对分布式系统的特性进行了高度抽象,形成了三个指标:

  • 一致性(Consistency):一致性说的是客户端的每次操作,不管访问哪个节点,要么读到的都是同一份最新的数据,要么读取失败。
  • 可用性(Availability):可用性说的是对于客户端不管访问哪个节点,都能得到响应数据,但不保证是同一份最新数据。
  • 分区容错性(Partition Tolerance):分区容错性说的是当节点间出现任意数量的消息丢失或高延迟的时候,系统仍然可以继续提供服务。

需要注意的是,在分布式系统中,P是一定要考虑的,因为舍弃 P,就意味着舍弃分布式系统,故也没有“CA模型”的说法。

CAP不可能三角

CAP 不可能三角说的是对于一个分布式系统而言,一致性、可用性、分区容错性,这 3 个指标不可兼得,最多选其中 2 个。然后P是一定要保重的,故存在AP模型和CP模型。

  • 当选择了一致性的时候(CP),如果因为消息丢失、延迟过高发生了网络分区,部分节点无法保证特定信息是最新的,那么这个时候,当集群节点接收到来自客户端的写请求时,因为无法保证所有节点都是最新信息,所以系统将返回写失败错误,也就是说集群拒绝新数据写入。
  • 当选择了可用性的时候(AP),系统将始终处理客户端的查询,返回特定信息,如果发生了网络分区,一些节点虽然无法返回最新的特定信息,但它们将返回自己当前的相对新的信息。

CAP理论的误区

大部分人对 CAP 理论有个误解,认为无论在什么情况下,分布式系统都只能在 C 和 A 中选择 1 个。

其实,在不存在网络分区的情况下,也就是分布式系统正常运行时,就是说在不需要 P 时,C 和 A 能够同时保证。只有当发生分区故障的时候,也就是说用到P时,才会在A和C之间二选其一。

而且如果各节点数据不一致会影响到系统运行或业务运行,推荐选择 C 一致性,否则选 A 可用性。

ACID理论:CAP的酸,追求一致性

需要理清的概念

关于一致性的程度:满足ACID的事务 > 强一致性 > 最终一致性

  • 满足ACID的事务:要求所有节点都确认
  • 强一致性:大多数节点确认即可

ACID特性,可以理解为CAP理论中关于“一致性的边界”,是最强的一致性。

分布式系统与ACID

ACID就是事务的四大特性,不必多述。在单机上实现 ACID 是比较容易的,比如可以通过锁、时间序列等机制保障操作的顺序执行。而在分布式系统中是很难实现的,是因为分布式系统涉及多个节点间的操作,我们使用加锁、时间序列等机制,只能保证单个节点上操作的 ACID 特性,无法保证节点间操作的 ACID 特性。

因此我们需要引入二阶段提交协议TCC。为了便于理解,下面仍使用“苏秦”的故事讲解。

背景:苏秦想协调赵魏韩三国,明天一起行动攻秦。那么对苏秦来说,他面临的问题是,如何高效协同赵、魏、韩一起行动,并且保证当有一方不方便行动时,取消整个计划。(要么全部成功,要么全部失败)

二阶段提交协议

二阶段提交协议,它不仅仅是协议,它还是一个重要的思想,有许多算法都是由它衍生出来的。

发起二阶段提交(此处还不属于二阶段协议)

苏秦发消息给赵,接收到消息后就扮演协调者的身份,赵去联系魏、韩,发起二阶段提交。(此处,苏秦可以理解为客户端,客户端发消息给“赵节点”,由“赵节点”去同步其他节点)

提交请求阶段(第一阶段)

  1. 由协调者赵同步消息给其他两国
  2. 这三国各自都判断一下明天是否能攻打(看看资源是否充足)
  3. 协调者接收回复的消息,包括他自己的消息
    《分布式协议与算法实战》——专栏笔记_第5张图片

提交执行阶段(第二阶段)

  1. 协调者统计投票结果
  2. 协调者按照投票结果发出指令
  3. 各个节点执行分布式事务
  4. 协调者接收执行结果

二阶段提交协议注意事项

二阶段提交协议最早是用来实现数据库的分布式事务的不过现在最常用的协议是 XA 协议。比如 MySQL 就是通过 MySQL XA 实现了分布式事务。
但是不管是原始的二阶段提交协议,还是 XA 协议,都存在一些问题,在提交请求阶段,需要预留资源,并且在资源预留期间,其他人不能操作。所以我们无法根据业务特点弹性地调整锁的粒度,为此,我们可以选择TCC协议。

TCC协议

TCC协议的全称是Try-Confirm-Cancel,它是三个操作的缩写,但我们在使用时只会用到其中两个。

  1. Try(预留)
  2. Confirm(确认)或者 Cancel(撤销)

预留阶段

  1. 苏秦通知赵魏韩,让他们预留明天的时间和相关资源,同时苏秦注册确认操作和撤回操作。
  2. 苏秦接收三国的答复。
    《分布式协议与算法实战》——专栏笔记_第6张图片

确认或撤回阶段

  1. 苏秦根据三国的答复,发出对应的指令。
  2. 苏秦接收执行结果。
    《分布式协议与算法实战》——专栏笔记_第7张图片或者
    《分布式协议与算法实战》——专栏笔记_第8张图片

TCC协议注意事项

TCC 本质上是补偿事务,它的核心思想是针对每个操作都要注册一个与其对应的确认操作补偿操作(也就是撤销操作)。

TCC是一个业务层面的协议,也就是说,我们要通过代码实现这三个操作,同时还要考虑这两个CC操作(确认和取消)是幂等的,因为这两个操作可能有失败重试的情况。

何时使用TCC

TCC 不依赖于数据库的事务,而是在业务中实现了分布式事务,这样能减轻数据库的压力,但对业务代码的入侵性也更强,实现的复杂度也更高。所以作者推荐在需要用到分布式事务时,优先考虑现成的事务型数据库(如MySQL XA),当现有的事务型数据库不能满足业务的需求时,再考虑用 TCC 实现分布式事务。

补充:二阶段与三阶段

三阶段提交协议,虽然针对二阶段提交协议的“协调者故障,参与者长期锁定资源”的痛点,通过引入了询问阶段超时机制,来减少资源被长时间锁定的情况,不过这会导致集群各节点在正常运行的情况下,使用更多的消息进行协商,增加系统负载和响应延迟。也正是因为这些问题,三阶段提交协议很少被使用。

BASE理论:CAP的碱,追求可用性

前情概要

集群的可用性是每个节点的可用性的乘积,比如,存在某 3 个节点的集群,每个节点的可用性为 99.9%,那么整个集群的可用性为 99.7%,也就是说,每个月可能宕机 129.6 分钟,这是非常严重的问题。

BASE 理论可以说是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。几乎所有的互联网后台分布式系统都有 BASE 的支持,这个理论很重要,地位也很高。

BASE理论的核心就是基本可用最终一致性

基本可用

基本可用的概念

基本可用是指当分布式系统在出现故障时,允许损失部分功能,保障核心功能的可用性。就像弹簧一样,遇到外界的压迫,它不是折断,而是变形伸缩,不断适应外力。

实现基本可用的 4 板斧

  • 流量削峰:如订票网站在不同的时间,出售不同区域的票,将访问请求错开,削弱请求峰值。
  • 延迟响应:流量大时,自己的请求可能在几分钟或十几分钟后,系统才开始处理,最后响应处理结果。
  • 体验降级:如用小图片来替代原始图片,通过降低图片的清晰度和大小,提升系统的处理能力。
  • 过载保护:把接收到的请求放在队列中排队处理,如果请求等待时间超时了,这时可以直接拒绝超时请求。再比如队列满后,就清除队列中一定数量的排队请求,以保护系统不过载。

基本可用在本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。

最终一致性

最终一致性的概念

最终一致性是说,系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。即在数据的同步上允许存在一个短暂的延迟。

几乎所有的互联网系统采用的都是最终一致性。

最终一致性,以什么为准?

通常为两种方案:

  • 最新写入的数据为准。
  • 首次写入的数据为准。

实现最终一致性的方案

通常为以下三种:

  • 读时修复:在读取数据时,检测数据的不一致,进行修复。
  • 写时修复:写失败时,将数据缓存到本地磁盘上,然后周期性的重传,本质上,就是失败重传。
  • 异步修复:这个是最常用的方式,通过定时对账、检测副本数据的一致性并修复。

ACID理论和BASE理论总结

  • ACID 理论是传统数据库常用的设计理念,追求强一致性模型。
  • BASE 理论在NoSQL中应用广泛,是 NoSQL 系统设计的事实上的理论支撑。
  • BASE 理论是对 CAP 中一致性和可用性权衡的结果,它来源于对大规模互联网分布式系统实践的总结,是基于 CAP 定理逐步演化而来的。它的核心思想是,如果不是必须的话,不推荐实现事务或强一致性,鼓励可用性和性能优先。

Basic Paxos 算法

Basic Paxos 算法,描述的是多节点之间如何就某个值达成共识

Basic Paxos 算法基础概念

在该算法中,有这么几个重要的概念

  • 提案:每个提议,包含“提议编号”和“提议值”。提议编号的大小代表着优先级,提议值可以理解为要存储的数据。
  • 提议者:提议一个值,用于投票表决(投票的过程是一个“二阶段提交协议”)。一个集群中,收到客户端请求的节点会充当提议者。
  • 接受者:对每个提议的值进行投票,并存储接受的值。一般来说,集群中的所有节点都会扮演接受者的角色,参与公式投票。
  • 学习者:不参与投票,它被告知投票的结果,接受达成共识的值,存储保存,通常作为数据备份。

一个节点可以身兼多个角色。
《分布式协议与算法实战》——专栏笔记_第9张图片

场景介绍

假设我们要实现一个分布式集群,这个集群是由节点 A、B、C 组成,提供只读 KV 存储服务。因此可知,创建只读变量的时候,必须要对它进行赋值,首次赋值成功后,后面再次赋值以尝试修改它时会失败

现在假设我们有多个客户端,他们都尝试去赋值,当然了,最终只能有一个客户端能成功。客户端1试图创建值为 3 的数据,客户端2试图创建值为 7 的数据。

Basic Paxos 算法的“二阶段提交”具体流程

我们假设客户端 1 的提案编号为 1,客户端 2 的提案编号为 5,并假设节点 A、B 先收到来自客户端 1 的准备请求,而节点 C 却先收到来自客户端 2 的准备请求。

1、准备阶段

客户端 1、2 作为提议者,分别向所有接受者发送包含提案编号的准备请求在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了。
《分布式协议与算法实战》——专栏笔记_第10张图片节点 A、B 收到提案编号为 1 的准备请求,节点 C 收到提案编号为 5 的准备请求后,由于之前没有通过任何提案,所以节点 A、B 将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 1 的准备请求,也不会通过编号小于 1 的提案

节点 C 也是如此,它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案
《分布式协议与算法实战》——专栏笔记_第11张图片《分布式协议与算法实战》——专栏笔记_第12张图片

2、接受阶段

客户端 1、2 在收到大多数节点的准备响应之后,会分别发送接受请求,接受请求中要带上提议值

当客户端 1 收到大多数的接受者(节点 A、B)的准备响应后,根据响应中提案编号最大的提案的提议值,设置接受请求中的提议值。因为该值在来自节点 A、B 的准备响应中都为空(尚无提案),所以就把自己的提议值 3 作为提案的值,发送接受请求[1, 3]。

当客户端 2 收到大多数的接受者的准备响应后(节点 A、B、C),根据响应中提案编号最大的提案的提议值,来设置接受请求中的提议值。因为该值在来自节点 A、B、C 的准备响应中都为空(尚无提案),所以就把自己的提议值 7 作为提案的值,发送接受请求[5, 7]。

这样一来,就相当于两个客户端各自想设置不同的值了。

当三个接受者收到这两个客户端的请求后,会进行如下处理。
会发现,提案[1,3]会被拒绝,而选择提案[5,7],具体原因如下:

  • 当节点 A、B、C 收到接受请求[1, 3]的时候,由于提案的提案编号 1 小于三个节点承诺能通过的提案的最小提案编号 5,所以提案[1, 3]将被拒绝。
  • 当节点 A、B、C 收到接受请求[5, 7]的时候,由于提案的提案编号 5 不小于三个节点承诺能通过的提案的最小提案编号 5,所以就通过提案[5, 7],也就是接受了值 7,三个节点就 X 值为 7 达成了共识。

至此,该算法的流程就结束了。此后,这个值——7,就不能再改了!不过集群的提议编号可能会变化。

补充:尝试篡改集群中的数据

假设,上述示例中,节点 A、B 已经通过了提案[5, 7],节点 C 未通过任何提案,那么当客户端 3 提案编号为 9 时,通过 Basic Paxos 算法尝试执行“SET X = 6”,最终三个节点上 X 值(提议值)是多少?

解答:

  1. 准备阶段,节点C收到客户端3的准备请求[9,6], 因为节点C未收到任何提案,所以返回“尚无提案”的响应。如果此时节点C收到了以前客户端的准备请求[5, 7], 根据提案编号5小于它之前响应的准备请求的提案编号9,会丢弃该准备请求,若没收到以前的请求,也毫无影响,因为当前的编号已经到9,大于以前的请求了。
  2. 客户端3发送准备请求[9,6]给节点A,B,这时因为节点A,B以前通过了提案[5,7], 根据“如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含已经通过的最大编号的提案信息”,所以节点A,B会返回[5,7]给客户端3,然后客户端3用这里面的7作为提议值,构建出这个提议,即[9,7]。
  3. 接受阶段,客户端3发送会接受请求[9,7]给节点A,B,C,因为此时请求编号9不小于之前的请求编号,所以所有节点接受该请求[9,7]。
  4. 最后,学习者会接受达成共识的值,保存该数据。

Multi Paxos 思想

前情提要

此处的标题,我特意写了是“思想”,而不是“算法”。
Multi-Paxos 是一种思想,不是算法,而且还缺少算法过程的细节和编程所必须的细节,比如如何选举领导者等,这也就导致了每个人实现的 Multi-Paxos 都不一样。可以理解为,Multi-Paxos 是一个统称,它是指基于 Multi-Paxos 思想,通过多个Basic Paxos 实例实现一系列数据的共识的算法。

Basic Paxos和Multi Paxos

Basic Paxos 只能就单个值达成共识,一旦遇到为一系列的值实现共识的时候,它就不管用了,这个时候我们就需要使用Multi Paxos。下面将讲解基于 Chubby 的 Multi-Paxos实现细节

Basic Paxos存在的问题

问题一:协商失败

Basic Paxos是采用二阶段提交来实现的。

如果多个提议者同时提交提案,可能出现因为提案冲突,在准备阶段没有提议者接收到大多数准备响应,就会协商失败,需要重新协商。

你想象一下,一个 5 节点的集群,如果 3 个节点作为提议者同时提案,就可能发生因为没有提议者接收大多数响应(比如 1 个提议者接收到 1 个准备响应,另外 2 个提议者分别接收到 2 个准备响应)而准备失败,需要重新协商。

如何避免协商失败?

问题二:延迟

这两阶段,采用RPC通讯,往返消息多、耗性能、延迟大,如何减少往返消息,提高性能?

解决方案

通过引入领导者优化 Basic Paxos 执行来解决。

  • 领导者:领导者节点就是唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了。
  • 优化 Basic Paxos 执行:当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段。因为在领导者节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案。
    《分布式协议与算法实战》——专栏笔记_第13张图片

上面两个方案,就是Multi Paxos的核心。下面看一下 Chubby 是如何补充细节,实现 Multi-Paxos 算法的。

Chubby 的 Multi-Paxos 实现

  1. 引入主节点其实就是领导者。也就是说,主节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了。主节点是通过执行 Basic Paxos 算法,投票选举产生的,并且,产生之后,若主节点不发生故障,会长期存在,若出故障了,会重新选举。
  2. 实现了“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制。
  3. 实现了“成员变更”,以此保证节点变更的时候集群的平稳运行。

是否使用Multi Paxos?

首先,Basic Paxos 是经过证明的,可以放心使用。而 Multi-Paxos 算法是未经过证明的。比如 Chubby 的作者做了大量的测试,和运行一致性检测脚本,验证和观察系统的健壮性。在实际使用时,不推荐你设计和实现新的 Multi-Paxos 算法,而是建议优先考虑 Raft 算法,因为Raft 的正确性是经过证明的。当 Raft 算法不能满足需求时,再考虑实现Multi-Paxos算法。

Raft算法⭐️

基础概念

Raft 算法属于 Multi-Paxos 算法,属于共识算法,它做了简化和限制,在理解和算法实现上都相对容易许多。除此之外,Raft 算法是现在分布式系统开发首选的共识算法。

在Raft算法中,集群中的节点有三种状态,在任何时候,每一个服务器节点都处于这 3 个状态中的 1 个。

  • 跟随者:普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人
  • 候选人:想当领导的人,候选人将向其他节点发送请求投票,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
  • 领导者:霸道总裁,一切以我为准,平常的主要工作内容就是,处理写请求、管理日志复制、不断地发送心跳信息。

若要用一句话概括Raft算法,那就是,“通过一切以领导者为准的方式实现值的共识和各节点日志的一致”。
需要注意的是,Raft 算法是强领导者模型,集群中只能有一个“霸道总裁”。

三个核心

  • 领导者选举
  • 日志复制
  • 成员变更

接下来对这三个核心概念逐一介绍。

一、领导者选举

背景介绍

假设我们有一个由节点 A、B、C 组成的 Raft 集群,Raft 算法如何保证在同一个时间,集群中只有一个领导者呢?
《分布式协议与算法实战》——专栏笔记_第14张图片

选领导者的具体过程

在初始状态下,集群中的所有节点都是“跟随者”。每个跟随者都有各自的“随机超时时间”,即不同的跟随者的超时时间是不同的(这样一来,大部分情况下,都会有一个跟随者率先超时并变成候选人开始“自荐”,避免多个候选人同时竞争领导者,分散了选票而导致选举失败)。
根据上图可知,节点A会率先超时并变成候选人,然后将自己的任期编号+1,再向其他节点发出请求投票,希望其他节点投它一票。
其他节点收到投票请求后,若他们此前没投过票(要保证一人一票),并且他们的日志记录的完整性不高于候选人A,那就将选票给A,然后他们发现自己的任期编号比候选人的任期编号小,故其他节点将自己的任期编号与A同步,即变成1。
如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。节点 A 当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举。
《分布式协议与算法实战》——专栏笔记_第15张图片到此,新的领导人就选举成功了。

关于领导者选举的重要细节

通信方式

节点之间采用RPC通信,包含两种RPC:

  1. 请求投票RPC:由候选人在选举期间发出,请求投票。
  2. 日志复制RPC:由领导者发出,用来复制日志和提供心跳消息。
任期

每个任期由单调递增的数字标识,任期编号是随着选举的举行而变化的。它的变化规律如下:

  1. 当跟随者发现超时后,会推举自己为候选人,然后使自己的任期编号+1。
  2. 一个节点,若发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。(如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1)
  3. 当一个候选人领导者发现自己的任期编号比其他节点小时,会立刻变成跟随者。
  4. 当一个节点收到一个比自己任期编号值小的请求时,它会直接拒绝这个请求。(如节点 C 任期编号为 4,若收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息)
选举规则
  1. 领导者一直都会是领导者,直到它出现故障。
  2. 投票时会采取先来先服务的原则投票,且一人一票。
  3. 日志完整性高的跟随者会拒绝投票给日志完整性低的候选人。

二、日志复制

日志用于存储副本数据

副本数据是以日志的形式存在的,日志是由日志项组成。

日志项是一种数据格式,它包含如下数据:

  • 指令:一条由客户端请求指定的、状态机需要执行的指令。可以理解为用户指定的数据
  • 索引值:日志项对应的整数索引值,即用来标识日志项的。
  • 任期编号:创建这条日志项的领导者的任期编号。

《分布式协议与算法实战》——专栏笔记_第16张图片
如图所示,一届领导者任期,往往有多条日志项,且日志项的索引值是连续的。

上图存在的问题:为什么图中 4 个跟随者的日志都不一样呢?日志是怎么复制的呢?又该如何实现日志的一致呢?下面逐一解释。

如何复制日志

可以把 Raft 的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段),减少了一半的往返消息,即降低了一半的消息延迟。

具体过程如下:

  1. 接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
  2. 领导者通过日志复制 RPC消息,将新的日志项复制到其他的服务器。
  3. 当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项提交到它的状态机中。
  4. 领导者将执行的结果返回给客户端。
  5. 当跟随者接收到心跳信息,或者新的日志复制 RPC 消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没提交,那么跟随者就将这条日志项提交到本地的状态机中。

《分布式协议与算法实战》——专栏笔记_第17张图片

如何实现日志的一致

实际环境中,复制日志的时候,可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致。那么在这种情况下,Raft算法是如何处理不一致日志的呢?

在 Raft 算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志问题。即 Raft 是通过以领导者的日志为准,来实现各节点日志的一致的,具体有 2 个步骤:

  1. 首先,领导者通过日志复制 RPC 消息进行一致性检查,直到找到跟随者节点上与自己相同的日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了
  2. 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。

三、成员变更

成员变更问题

总的来说,在日常工作中,集群中的服务器数量是会发生变化的(故障、扩容、缩容等)。那么当成员变更时,集群成员发生了变化,就可能同时存在新旧配置的 2 个“大多数”,出现 2 个领导者,破坏了 Raft 集群的领导者唯一性,影响了集群的运行(这就是成员变更问题)。在Raft中,是通过“单节点变更”来解决这个问题的。

单节点变更

单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。

比如将 3 节点集群扩容为 5 节点集群,这时你需要执行 2 次单节点变更。

  1. 先将 3 节点集群变更为 4 节点集群。
  2. 然后再将 4 节点集群变更为 5 节点集群,就像下图的样子。

案例

背景:

假设我们有一个由节点 A、B、C 组成的 Raft 集群,现在我们需要增加数据副本数,增加 2 个副本(也就是增加 2 台服务器),扩展为由节点 A、B、C、D、E,即 5 个节点组成的新集群。
《分布式协议与算法实战》——专栏笔记_第18张图片

通过“单节点变更”解决的流程:

目前的集群配置为[A, B, C],我们先向集群中加入节点 D(先变更一个节点)。

  1. 第一步,领导者(节点 A)向新节点(节点 D)同步数据。
  2. 第二步,领导者(节点 A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点 A、B、C、D)上,然后将新配置的日志项提交到本地状态机,完成单节点变更。
    《分布式协议与算法实战》——专栏笔记_第19张图片
    在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点 E(即再变更一个节点,是一个一个的变更)。
  3. 第一步,领导者(节点 A)向新节点(节点 E)同步数据。
  4. 第二步,领导者(节点 A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项提交到本地状态机,完成单节点变更。
    《分布式协议与算法实战》——专栏笔记_第20张图片
    结束

补充

除了“单节点变更”方案以外,还有“联合共识”方案可以解决成员变更问题,但是它难以实现,很少被Raft实现采用。

一致性哈希算法

背景

假设我们有一个KV存储服务器,现在数据量访问增大,便对服务器做集群处理,然后引入Proxy层,由 Proxy 层处理来自客户端的读写请求,接收到读写请求后,通过对 Key 做哈希找到对应的集群。但是缺点也很明显,当需要变更集群数时,这时大部分的数据都需要迁移,并重新映射,数据的迁移成本是非常高的。

因此可以使用“一致性哈希算法”来解决该问题。

基本概念

一致性哈希算法本质上是一种路由寻址算法,适合简单的路由寻址场景。比如在 KV 存储系统内部。

一致性哈希算法是对 2^32 (全球IPV4总数)进行取模运算。你可以想象下,一致哈希算法,将整个哈希值空间组织成一个虚拟的圆环,也就是哈希环。哈希环的空间是按顺时针方向组织的,圆环的正上方的点代表 0,0点右侧的第一个点代表 1,以此类推,2、3、4、5、6……直到 2^32-1。
《分布式协议与算法实战》——专栏笔记_第21张图片

具体寻址方式

当需要对指定 key 的值进行读写的时候,通过下面 2 步进行寻址:

  1. 计算key的哈希值,确定此 key 在环上的位置。
  2. 从这个位置沿着哈希环顺时针“行走”,遇到的第一个节点就是 key 所对应的节点。

《分布式协议与算法实战》——专栏笔记_第22张图片

1、节点故障场景

假设,现在有一个节点故障了,比如节点 C。可以看到,key-01 和 key-02 不会受到影响,只有 key-03 的寻址被重定位到 A。即,受影响的数据仅仅是,会寻址到此节点和前一节点之间的数据。
《分布式协议与算法实战》——专栏笔记_第23张图片

2、扩容场景

增加一个节点,比如节点D。受影响的数据仅仅是,会寻址到新节点和前一节点之间的数据。
《分布式协议与算法实战》——专栏笔记_第24张图片

总的来说,使用了一致哈希算法后,扩容或缩容的时候,都只需要重定位或迁移环空间中的一小部分数据。而对于普通哈希算法而言,他就要重定位或迁移更多数据。

3、冷热不均场景

所谓“冷热不均”,实际上就是“节点分布不均匀造成数据访问的冷热不均,即说大多数请求都会落在少数节点上”,如下图。
《分布式协议与算法实战》——专栏笔记_第25张图片
解决方案是:引入虚拟节点

是对每一个服务器节点计算多个哈希值,在每个计算结果位置上,都放置一个虚拟节点,并将虚拟节点映射到实际节点。如下图有6个虚拟节点,其中的“Node-A-01”和“Node-A-02”,代表的是同一个A节点。
《分布式协议与算法实战》——专栏笔记_第26张图片增加了节点后,节点在哈希环上的分布就相对均匀了。如果有访问请求寻址到“Node-A-01”这个虚拟节点,将被重定位到节点 A。

内容小结

使用一致性哈希算法,可以通过增加节点数量达到以下效果:

  1. 降低个别节点宕机对整个集群的影响。
  2. 减少故障恢复时需要迁移的数据量。
  3. 提升系统的容灾能力。

Gossip协议

基本介绍

Gossip 协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。(即实现最终一致性

Gossip的三种实现方案

  1. 直接邮寄
  2. 反熵(最常用)
  3. 谣言传播

下面逐一介绍

直接邮寄

直接邮寄:就是直接发送更新数据,当数据发送失败时,将数据缓存下来,然后做重传。如下图:

《分布式协议与算法实战》——专栏笔记_第27张图片直接邮寄是一种简单的实现方案,数据同步也很及时,它的缺点是“存在缓存队列满时出现数据丢失”的情况。可以说,使用直接邮寄的话,是无法实现最终一致性的。

反熵

反熵指的是集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。

反熵又细分为三种操作:推、拉、推拉。

1、推

将自己的所有副本数据,推给对方,修复对方副本。

《分布式协议与算法实战》——专栏笔记_第28张图片

2、拉

拉取对方的所有副本数据,修复自己副本。

《分布式协议与算法实战》——专栏笔记_第29张图片

3、推拉

同时修复自己副本和对方副本。

《分布式协议与算法实战》——专栏笔记_第30张图片

反熵的应用场景

反熵需要节点两两交换比对自己所有的数据,执行反熵时通讯成本会很高,所以我不建议你在实际场景中频繁执行反熵,并且可以通过引入校验和等机制,降低需要对比的数据量和通讯消息。

是执行反熵时,相关的节点都是已知的,而且节点数量不能太多,如果是一个动态变化或节点数比较多的分布式环境(比如在 DevOps 环境中检测节点故障,并动态维护集群节点状态),这时反熵就不适用了。该采用“谣言传播”。

补充

关于“反熵”这个名词,可以这么理解,反熵中的熵是指混乱程度,反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,降低熵值。

谣言传播

谣言传播,指的是当一个节点获得新数据后,这个节点变成活跃状态,并周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。

“谣言传播”适合动态变化的分布式系统。

使用 Anti-entropy 实现最终一致

上面提到过,在分布式存储系统中,实现数据副本最终一致性,最常用的方法就是“反熵”。下面来看一个具体的“反熵”实现方案,是自研的InfluxDB的反熵实现例子。

背景介绍

在自研 InfluxDB 中,一份数据副本是由多个分片组成的,三节点三副本的集群,如下图所示:
《分布式协议与算法实战》——专栏笔记_第31张图片
我们要确保,不同节点上,同一分片组中的分片都没有差异。例如,节点 A 要拥有分片 Shard1 和 Shard2,而且,节点 A 的 Shard1 和 Shard2,与节点 B、C 中的 Shard1 和 Shard2,是一样的。

可能出现的问题

我们可能出现数据丢失的问题,我们将数据缺失,分为这样 2 种情况:

  1. 缺失分片:也就是说,在某个节点上整个分片都丢失了。
  2. 节点之间的分片不一致:也就是说,节点上分片都存在,但里面的数据不一样

1、缺失分片的解决方案

第一种情况修复起来不复杂,我们只需要将分片数据,通过 RPC 通讯,从其他节点上拷贝过来就可以了。
《分布式协议与算法实战》——专栏笔记_第32张图片
直接把Shard2的整个分片拷贝过来。

2、节点之间不一致的解决方案

第二种情况修复起来要复杂一些。我们需要设计一个闭环的流程,按照一个顺序地修复,执行完一轮流程后,就能实现了最终一致性了。

具体流程:先随机选择一个节点,然后循环修复,每个节点生成“自己节点有但下一个节点没有的差异数据”,发送给下一个节点,进行修复。

案例,为了方便演示,假设 Shard1、Shard2 在各节点上是不一致的。
《分布式协议与算法实战》——专栏笔记_第33张图片
上图中,数据修复的起始节点为节点 A,数据修复是按照顺时针顺序,循环修复的。需要注意的是,最后节点 A 对节点 B 的数据执行了一次数据修复操作,因为只有这样,“节点 C 有但节点 B 缺失的差异数据”,才会同步到节点 B 上。

注意事项

此实现方案,和我们一开始说的反熵有些区别,一开始说的反熵是“随机选择某个其他节点去进行数据同步”,而此处的实现方案并不是随机地选择节点,仅仅是在一开始随机选择一个起始节点,然后接下来的修复是有序的。选择有序修复的原因是——我们希望能在一个确定的时间范围内实现数据副本的最终一致性,而不是基于随机性的概率,在一个不确定的时间范围内实现数据副本的最终一致性

Gossip协议小结

实现数据副本的最终一致性时,一般来说:

  1. 直接邮寄方式是一定要实现的。
  2. 在存储组件中,节点都是已知的,一般会采用反熵。
  3. 若是节点会动态变化或节点较多的分布式系统,就选择谣言传播。

Quorum NWR算法

背景介绍

假设我们刚刚开发好一套AP型分布式系统,此时若又来了新需求,要求写入数据后能立刻读到新数据,即实现“强一致性”。而我们这个系统系统是AP型的,难道要重新开发出另一套系统吗?显然是不可能的,我们需要的是一个能够灵活变换一致性级别的系统,即能够自定义一致性级别

介绍Quorum NWR

Quorum NWR,我们可以自定义一致性级别。即我们可以根据业务场景的不同,通过调整写入或者查询的方式,完成对“最终一致性”和“强一致性”的切换。在 Quorum NWR 中有三个要素,分别是N、W、R,下面逐一介绍。

N

N 表示副本数,又称复制因子,即N表示集群中的同一份数据有多少个副本。
《分布式协议与算法实战》——专栏笔记_第34张图片
如上图,集群有三个节点,DATA-1有2个副本,即它的N为2。而DATA-2的N为3,DATA-3的N为1。不同的数据可以设置不同副本数N。

W

W,又称写一致性级别,表示成功完成 W 个副本更新,才完成写操作。
《分布式协议与算法实战》——专栏笔记_第35张图片如上图,DATA-2的写一致性级别为2,我们对 DATA-2 执行写操作时,完成 2 个副本的更新(比如节点 A、C),才完成写操作。

此处你可能会有疑问,我们这里对节点A、C更新后,就算完成写操作了。那我们读数据的时候读的是节点B呢?那读到的此不是旧数据?这其实是可以避免的,请看接下来的R。

R

R,又称读一致性级别,表示读取一个数据对象时需要读 R个副本。即读取指定数据时,要读 R 个副本,然后返回 R 个副本中最新那份数据。
《分布式协议与算法实战》——专栏笔记_第36张图片
如上图,我们将DATA-2的读一致性级别设置为2,我们就可以回答上面的问题。我们从节点B、C中读取数据,B中的数据是旧的,但是C中数据是新的,所以我们返回C中的那份最新数据,那就保证了强一致性。(若此处将R设置为1,那就不能保证强一致性了)。

如何设置NWR的具体值

N、W、R 值的不同组合,会产生不同的一致性效果,具体来说有两种:

  1. W + R > N:对于客户端来讲,整个系统能保证强一致性一定能返回更新后的那份数据。
  2. W + R < N:对于客户端来讲,整个系统只能保证最终一致性可能会返回旧数据。

Quorum NWR小结

  1. 一般来说,不推荐 N > 集群节点数,因为副本数的意义是“冗余备份”,而当N大于集群节点数时,就说明某些节点中包含了重复的副本,此处若该节点发生故障,那这里面的副本都会受到影响,此时的冗余备份没什么意义。
  2. N 决定了副本的冗余备份能力。
  3. 如果设置 W = (N + 1) / 2R = (N + 1) / 2容错能力比较好,能容忍少数节点(也就是 (N - 1) / 2)的故障。

你可能感兴趣的:(笔记,分布式,分布式,算法)