4. 复制

复制问题是分布式系统中的众多问题之一。我选择专注于它而不是其他问题(如领导者选举、故障检测、互斥、共识和全局快照),因为它通常是人们最感兴趣的部分。例如,区分并行数据库的一种方式是它们的复制功能。此外,复制为许多子问题提供了上下文,如领导者选举、故障检测、共识和原子广播。

复制是一个组通信问题。什么安排和通信模式能给我们期望的性能和可用性特征?我们如何在网络分区和同时节点故障的情况下确保容错、持久性和不发散?

同样,有许多方法可以处理复制。我在这里采取的方法只是看看具有复制的系统可能的高级模式。从视觉上看看这有助于将讨论集中在整体模式上,而不是涉及的具体消息。我的目标是探索设计空间,而不是解释每个算法的具体细节。

让我们首先定义复制看起来像什么。我们假设我们有一些初始数据库,客户端发出请求更改数据库的状态。

复制

然后可以将安排和通信模式分为几个阶段:

  1. (请求) 客户端向服务器发送请求
  2. (同步) 发生复制的同步部分
  3. (响应) 向客户端返回响应
  4. (异步) 发生复制的异步部分

这个模型松散地基于 这篇文章。注意,在任务的每个部分中交换的消息模式取决于特定算法:我故意试图在不讨论特定算法的情况下完成。

给定这些阶段,我们可以创建什么样的通信模式?我们选择的模式的性能和可用性影响是什么?

同步复制

第一种模式是同步复制(也称为主动、或急切、或推送、或悲观复制)。让我们画出它的样子:

同步复制

在这里,我们可以看到三个不同的阶段:首先,客户端发送请求。接下来,发生我们称之为复制的同步部分。该术语指的是客户端被阻塞——等待来自系统的回复。

在同步阶段期间,第一台服务器联系其他两台服务器,并等待直到它收到来自所有其他服务器的回复。最后,它向客户端发送一个响应,告知其结果(例如,成功或失败)。

所有这一切似乎都很简单。在不讨论同步阶段期间算法细节的情况下,我们可以对这种特定的通信模式安排说什么?首先,观察到这是一种 N 写 N 的方法:在返回响应之前,系统中的每个服务器都必须看到并确认它。

从性能角度来看,这意味着系统将与其中最慢的服务器一样快。系统也将对网络延迟的变化非常敏感,因为它需要每个服务器在继续之前回复。

鉴于 N 写 N 方法,系统不能容忍任何服务器的丢失。当服务器丢失时,系统不能再写入所有节点,因此无法继续。它可能能够提供对数据的只读访问,但在这种设计中,节点故障后不允许修改。

这种安排可以提供非常强的持久性保证:客户端可以确定所有 N 个服务器在返回响应时已接收、存储并确认了请求。为了丢失已接受的更新,所有 N 个副本都需要丢失,这是你能做出的最好保证。

异步复制

让我们将其与第二种模式进行对比——异步复制(也称为被动复制、或拉取复制、或惰性复制)。正如你可能猜到的,这与同步复制相反:

异步复制

在这里,主节点(/领导者/协调器)立即向客户端发送回响应。它最多可能在本地存储更新,但不会同步执行任何重要工作,客户端不会被强制等待服务器之间发生更多轮通信。

在以后的某个阶段,发生复制任务的异步部分。在这里,主节点使用某种通信模式联系其他服务器,其他服务器更新它们的数据副本。具体细节取决于使用的算法。

在不深入算法细节的情况下,我们可以对这种特定的安排说什么?嗯,这是一种 1 写 N 的方法:立即返回响应,更新传播在以后某个时间发生。

从性能角度来看,这意味着系统很快:客户端不需要花费额外的时间等待系统内部完成工作。系统也更容忍网络延迟,因为内部延迟的波动不会导致客户端额外的等待。

这种安排只能提供弱的或概率的持久性保证。如果没有出错,数据最终会复制到所有 N 台机器。但是,如果包含数据的唯一服务器在这之前丢失,数据将永久丢失。

鉴于 1 写 N 方法,系统可以保持可用,只要至少有一个节点运行(至少在理论上,尽管实际上负载可能太高)。像这样纯粹的惰性方法不提供持久性或一致性保证;你可能被允许写入系统,但如果发生任何故障,没有保证你可以读回你写入的内容。

最后,值得注意的是,被动复制不能确保系统中的所有节点始终包含相同的状态。如果你在多个位置接受写入并且不要求这些节点同步达成一致,那么你将面临发散的风险:读取可能从不同位置返回不同的结果(特别是在节点故障和恢复后),并且无法执行全局约束(需要与每个人通信)。

我没有真正提到读取(而不是写入)期间的通信模式,因为读取的模式确实遵循写入的模式:在读取期间,你希望联系尽可能少的节点。我们将在仲裁的背景下更多地讨论这一点。

我们只讨论了两种基本安排,没有讨论任何特定算法。然而,我们已经能够找出很多关于可能的通信模式以及它们的性能、持久性保证和可用性特征。

主要复制方法概述

在讨论了两种基本复制方法(同步和异步复制)之后,让我们看看主要的复制算法。

有许多不同的方法可以对复制技术进行分类。我想介绍的第二个区别(在同步与异步之后)是:

  • 防止发散的复制方法(单副本系统)和
  • 有发散风险的复制方法(多主系统)

第一组方法的特性是它们”像单系统一样行为”。特别是,当部分故障发生时,系统确保只有一个系统副本是活动的。此外,系统确保副本始终达成一致。这称为共识问题。

如果几个进程(或计算机)都同意某个值,则它们达成共识。更正式地说:

  1. 一致性:每个正确的进程必须同意相同的值。
  2. 完整性:每个正确的进程最多决定一个值,如果它决定某个值,那么它必须由某个进程提出。
  3. 终止:所有进程最终都达成决定。
  4. 有效性:如果所有正确的进程提出相同的值 V,那么所有正确的进程决定 V。

互斥、领导者选举、多播和原子广播都是共识这个更一般问题的实例。维护单副本一致性的复制系统需要以某种方式解决共识问题。

维护单副本一致性的复制算法包括:

  • 1n 消息(异步主/备)
  • 2n 消息(同步主/备)
  • 4n 消息(两阶段提交,Multi-Paxos)
  • 6n 消息(三阶段提交,具有重复领导者选举的 Paxos)

这些算法在它们的容错性方面有所不同(例如,它们可以容忍的故障类型)。我只是根据算法执行期间交换的消息数量对这些进行了分类,因为我认为尝试回答”我们通过增加的消息交换买到了什么”这个问题很有趣。

下图(改编自 Google 的 Ryan Barret)描述了不同选项的一些方面:

复制方法比较,来自 http://www.google.com/events/io/2009/sessions/TransactionsAcrossDatacenters.html

上图中的一致性、延迟、吞吐量、数据丢失和故障转移特征可以追溯到两种不同的复制方法:同步复制(例如,在响应之前等待)和异步复制。当你等待时,你会得到更差的性能但更强的保证。当我们讨论分区(和延迟)容错时,2PC 和仲裁系统之间的吞吐量差异将变得明显。

在该图中,强制执行弱(/最终)一致性的算法被归为一类(“gossip”)。然而,我将更详细地讨论弱一致性的复制方法——gossip 和(部分)仲裁系统。“事务”行实际上更多地指全局谓词评估,这在具有弱一致性的系统中不受支持(尽管可以支持局部谓词评估)。

值得注意的是,强制执行弱一致性要求的系统具有较少的通用算法,以及更多可以选择性应用的技术。由于不强制执行单副本一致性的系统可以自由地表现为由多个节点组成的分布式系统,因此需要修复的明显目标较少,重点更多地是为人们提供一种推理他们所拥有的系统特征的方法。

例如:

  • 以客户为中心的一致性模型试图在允许发散的同时提供更易理解的一致性保证。
  • CRDT(收敛和交换复制数据类型)利用某些状态和基于操作的数据类型的半格属性(结合性、交换性、幂等性)。
  • 汇合分析(如在 Bloom 语言中)使用关于计算单调性的信息来最大限度地利用无序性。
  • PBS(概率有界陈旧性)使用从现实世界系统收集的模拟和信息来表征部分仲裁系统的预期行为。

我稍后会进一步讨论所有这些,首先;让我们看看维护单副本一致性的复制算法。

主/备复制

主/备复制(也称为主副本复制、主从复制或日志传输)可能是最常用的复制方法,也是最基本的算法。所有更新都在主节点上执行,操作日志(或替代地,更改)通过网络传输到备副本。有两种变体:

  • 异步主/备复制和
  • 同步主/备复制

同步版本需要两条消息(“更新” + “确认接收”),而异步版本可以只用一条(“更新”)运行。

P/B 非常常见。例如,默认情况下 MySQL 复制使用异步变体。MongoDB 也使用 P/B(带有一些额外的故障转移过程)。所有操作都在一个主服务器上执行,主服务器将它们序列化到本地日志,然后异步复制到备份服务器。

正如我们之前在异步复制的背景下讨论的那样,任何异步复制算法只能提供弱的持久性保证。在 MySQL 复制中,这表现为复制延迟:异步备份总是至少落后主节点一个操作。如果主节点故障,那么尚未发送到备份的更新将丢失。

主/备复制的同步版本确保在返回客户端之前写入已存储在其他节点上——以等待来自其他副本的响应为代价。然而,值得注意的是,即使这个变体也只能提供弱的保证。考虑以下简单的故障场景:

  • 主节点接收写入并将其发送到备份
  • 备份持久化并确认写入
  • 然后主节点在向客户端发送确认之前故障

客户端现在假设提交失败,但备份提交了它;如果备份被提升为主节点,那将是不正确的。可能需要手动清理来调和故障的主节点或发散的备份。

我当然在这里简化了。虽然所有主/备复制算法都遵循相同的通用消息模式,但它们在处理故障转移、副本长时间离线等方面有所不同。然而,在这种方案中,不可能对主节点的不适时故障具有弹性。

在日志传输/主/备方案中的关键是它们只能提供尽力而为的保证(例如,如果节点在不适当的时间故障,它们容易丢失更新或不正确的更新)。此外,P/B 方案容易出现脑裂,其中由于临时网络问题触发对备份的故障转移,导致主节点和备份同时处于活动状态。

为了防止不适时故障导致违反一致性保证;我们需要增加另一轮消息传递,这使我们获得两阶段提交协议(2PC)。

两阶段提交(2PC)

两阶段提交(2PC)是许多经典关系数据库中使用的协议。例如,MySQL Cluster(不要与常规 MySQL 混淆)使用 2PC 提供同步复制。下图说明了消息流:

[ Coordinator ] -> OK to commit? [ Peers ]
<- Yes / No
[ Coordinator ] -> Commit / Rollback [ Peers ]
<- ACK

在第一阶段(投票),协调器将更新发送给所有参与者。每个参与者处理更新并投票决定是否提交或中止。当投票提交时,参与者将更新存储到临时区域(预写日志)。在第二阶段完成之前,更新被视为临时的。

在第二阶段(决策),协调器决定结果并通知每个参与者。如果所有参与者都投票提交,则从临时区域获取更新并使其永久。

在提交被视为永久之前有第二阶段是有用的,因为它允许系统在节点故障时回滚更新。相比之下,在主/备(“1PC”)中,没有用于回滚在某些节点上失败而在其他节点上成功的操作的步骤,因此副本可能发散。

2PC 容易阻塞,因为单个节点故障(参与者或协调器)会阻塞进度,直到节点恢复。由于第二阶段,恢复通常是可能的,在此期间其他节点被告知系统状态。注意,2PC 假设每个节点稳定存储中的数据永远不会丢失,并且没有节点永远崩溃。如果稳定存储中的数据在崩溃中损坏,数据丢失仍然是可能的。

节点故障期间恢复过程的细节非常复杂,所以我不会深入具体细节。主要任务是确保对磁盘的写入是持久的(例如,刷新到磁盘而不是缓存),并确保做出正确的恢复决策(例如,了解轮次的结果,然后在本地重做或撤消更新)。

正如我们在关于 CAP 的章节中学到的那样,2PC 是 CA——它不是分区容错的。2PC 解决的故障模型不包括网络分区;从节点故障中恢复的规定方法是等待网络分区愈合。如果一个协调器故障,没有安全的方法来提升新的协调器;而是需要手动干预。2PC 也对延迟相当敏感,因为它是一种 N 写 N 方法,在最慢的节点确认之前写入无法进行。

2PC 在性能和容错之间取得了不错的平衡,这就是它在关系数据库中流行的原因。然而,较新的系统通常使用分区容错共识算法,因为这种算法可以提供对临时网络分区的自动恢复以及更优雅地处理节点间延迟增加。

让我们接下来看看分区容错共识算法。

分区容错共识算法

分区容错共识算法是我们在维护单副本一致性的容错算法方面要走的最远。还有一类容错算法:容忍 任意(拜占庭)故障 的算法;这些包括通过恶意行为故障的节点。这种算法在商业系统中很少使用,因为它们运行成本更高且实现更复杂——因此我将它们排除在外。

当涉及到分区容错共识算法时,最著名的算法是 Paxos 算法。然而,它 notoriously 难以实现和解释,所以我将专注于 Raft,这是最近(约 2013 年初)设计的一种更容易教授和实现的算法。让我们首先看看网络分区和分区容错共识算法的一般特征。

什么是网络分区?

网络分区是到一个或多个节点的网络链路的故障。节点本身继续保持活动状态,它们甚至可能能够在网络分区的这一侧接收来自客户端的请求。正如我们之前学到的——在讨论 CAP 定理期间——网络分区确实发生,并不是所有系统都能优雅地处理它们。

网络分区很棘手,因为在网络分区期间,无法区分故障的远程节点和节点不可达。如果发生网络分区但没有节点故障,那么系统被分成两个同时活动的分区。下面的两个图表说明了网络分区看起来如何类似于节点故障。

一个 2 节点系统,故障与网络分区:

2 节点系统

一个 3 节点系统,故障与网络分区:

3 节点系统

强制执行单副本一致性的系统必须有某种方法来打破对称性:否则,它将分成两个独立的系统,它们可能彼此发散,不再能维持单副本的幻觉。

对于强制执行单副本一致性的系统,网络分区容错要求在网络分区期间,只有一个系统分区保持活动,因为在网络分区期间不可能防止发散(例如,CAP 定理)。

多数决策

这就是为什么分区容错共识算法依赖多数投票。要求多数节点——而不是所有节点(如 2PC 中)——就更新达成一致,允许少数节点宕机、缓慢或因网络分区而不可达。只要 (N/2 + 1) 个节点处于活动状态并可访问,系统就可以继续运行。

分区容错共识算法使用奇数个节点(例如,3、5 或 7)。只有两个节点,在故障后不可能有明确的多数。例如,如果节点数是三个,那么系统对一个节点故障具有弹性;有五个节点,系统对两个节点故障具有弹性。

当发生网络分区时,分区表现不对称。一个分区将包含大多数节点。少数分区将停止处理操作以防止在网络分区期间发散,但多数分区可以保持活动。这确保只有一个系统状态副本保持活动。

多数也很有用,因为它们可以容忍分歧:如果有扰动或故障,节点可能投票不同。然而,由于只能有一个多数决策,临时分歧最多只能阻止协议进行(放弃活性),但它不能违反单副本一致性标准(安全属性)。

角色

有两种方法可以构建系统:所有节点可能具有相同的职责,或者节点可能具有单独的、不同的角色。

用于复制的共识算法通常选择为每个节点具有不同的角色。具有单个固定领导者或主服务器是一种优化,使系统更高效,因为我们知道所有更新必须通过该服务器。不是领导者的节点只需要将它们的请求转发给领导者。

注意,具有不同的角色并不排除系统从领导者(或任何其他角色)的故障中恢复。仅仅因为角色在正常运行期间是固定的,并不意味着人们不能在故障后通过重新分配角色来从故障中恢复(例如,通过领导者选举阶段)。节点可以重用领导者选举的结果,直到节点故障和/或网络分区发生。

Paxos 和 Raft 都使用不同的节点角色。特别是,它们有一个领导者节点(Paxos 中的”proposer”),负责正常运行期间的协调。在正常运行期间,其余节点是跟随者(Paxos 中的”acceptors”或”voters”)。

时期

Paxos 和 Raft 中每个正常运行期间称为一个时期(Raft 中的”term”)。在每个时期,只有一个节点是指定的领导者(类似的系统 在日本使用,其中时代名称在皇位继承时更改)。

时期

在成功选举后,同一个领导者协调直到时期结束。如上图所示(来自 Raft 论文),一些选举可能失败,导致时期立即结束。

时期充当逻辑时钟,允许其他节点识别何时过时的节点开始通信——被分区或停止运行的节点将具有比当前时期小的时期编号,它们的命令被忽略。

通过决斗进行领导者更改

在正常运行期间,分区容错共识算法相当简单。正如我们之前看到的,如果我们不关心容错,我们可以只使用 2PC。大多数复杂性实际上来自于确保一旦做出共识决策,它就不会丢失,并且协议可以处理由于网络或节点故障导致的领导者更改。

所有节点都作为跟随者开始;一个节点在开始时被选为领导者。在正常运行期间,领导者维护一个心跳,允许跟随者检测领导者是否故障或变得分区。

当节点检测到领导者变得无响应(或在初始情况下,不存在领导者)时,它切换到中间状态(在 Raft 中称为”candidate”),它将时期/时期值递增一,发起领导者选举并竞争成为新的领导者。

为了被选为领导者,节点必须收到多数票。分配投票的一种方法是简单地按先到先得分配;这样,最终将选出一个领导者。在尝试当选之间添加随机等待时间将减少同时尝试当选的节点数量。

时期内的编号提议

在每个时期,领导者一次提出一个值进行投票。在每个时期内,每个提议都用一个唯一的严格递增的数字编号。跟随者(投票者/接受者)接受他们收到的特定提议编号的第一个提议。

正常运行

在正常运行期间,所有提议都通过领导者节点。当客户端提交提议(例如,更新操作)时,领导者联系仲裁中的所有节点。如果不存在竞争的提议(基于来自跟随者的响应),则领导者提出该值。如果多数跟随者接受该值,则该值被视为已接受。

由于另一个节点也可能试图充当领导者,我们需要确保一旦单个提议被接受,它的值就永远不会改变。否则,已经接受的提议可能会被竞争的领导者撤销。Lamport 将此陈述为:

P2:如果具有值 v 的提议被选中,那么每个被选中的更高编号的提议都具有值 v

确保此属性成立需要跟随者和提议者都受到算法的约束,永远不改变已被多数接受的值。注意,“值永远不会改变”指的是协议单次执行(或运行/实例/决策)的值。典型的复制算法将运行多次算法执行,但大多数关于算法的讨论都集中在单次运行上以保持简单。我们希望防止决策历史被更改或覆盖。

为了强制执行此属性,提议者必须首先询问跟随者他们的(最高编号的)已接受提议和值。如果提议者发现已经存在提议,那么它必须简单地完成协议的这次执行,而不是提出自己的提议。Lamport 将此陈述为:

P2b. 如果具有值 v 的提议被选中,那么任何提议者发出的每个更高编号的提议都具有值 v

更具体地说:

P2c. 对于任何 vn,如果具有值 v 和编号 n 的提议被发出 [由领导者],那么存在一个由多数接受者 [跟随者] 组成的集合 S,使得要么 (a) S 中没有接受者接受任何编号小于 n 的提议,要么 (b) vS 中跟随者接受的所有编号小于 n 的提议中最高编号提议的值。

这是 Paxos 算法以及从中派生的算法的核心。要提出的值在协议的第二阶段之前不被选择。提议者有时必须简单地重新传输先前做出的决策以确保安全(例如,P2c 中的子句 b),直到他们到达知道自己可以自由强加自己提议值的点(例如,子句 a)。

如果存在多个先前的提议,则提出最高编号的提议值。提议者只有在根本没有竞争提议的情况下才能尝试强加自己的值。

为了确保在提议者询问每个接受者其最近值的时间之间没有竞争提议出现,提议者要求跟随者不接受提议编号低于当前提议编号的提议。

把这些部分放在一起,使用 Paxos 达成决策需要两轮通信:

[ Proposer ] -> Prepare(n) [ Followers ]
<- Promise(n; previous proposal number
and previous value if accepted a
proposal in the past)
[ Proposer ] -> AcceptRequest(n, own value or the value [ Followers ]
associated with the highest proposal number
reported by the followers)
<- Accepted(n, value)

准备阶段允许提议者了解任何竞争或先前的提议。第二阶段是提出新值或先前接受的值的地方。在某些情况下——例如,如果两个提议者同时活动(决斗);如果消息丢失;或者如果多数节点故障——则没有提议被多数接受。但这是可以接受的,因为提出什么值的决策规则收敛到单个值(先前尝试中具有最高提议编号的值)。

事实上,根据 FLP 不可能结果,这是我们能做的最好的:解决共识问题的算法必须在消息传递边界的保证不成立时放弃安全性或活性。Paxos 放弃活性:它可能不得不无限期地延迟决策,直到没有竞争领导者且多数节点接受提议的时间点。这比违反安全保证更可取。

当然,实现这个算法比听起来要困难得多。有许多小问题,即使在专家手中也会累积成相当大量的代码。这些问题包括:

  • 实际优化:

    • 通过领导租约(而不是心跳)避免重复的领导者选举
    • 在领导者身份不变的稳定状态下避免重复的提议消息
  • 确保跟随者和提议者不会在稳定存储中丢失项目,并且稳定存储中存储的结果不会被微妙地损坏(例如,磁盘损坏)

  • 以安全的方式启用集群成员更改(例如,基础 Paxos 依赖于多数总是相交于一个节点的事实,如果成员可以任意更改,这就不成立)

  • 在崩溃、磁盘丢失或配置新节点后,以安全有效的方式使新副本最新的过程

  • 在合理时期后对保证安全所需的数据进行快照和垃圾回收的过程(例如,平衡存储要求和容错要求)

Google 的 Paxos Made Live 论文详细介绍了其中一些挑战。

分区容错共识算法:Paxos、Raft、ZAB

希望这让你对分区容错共识算法如何工作有了感觉。我鼓励你阅读进一步阅读部分中的一篇论文,以掌握不同算法的具体细节。

Paxos。Paxos 是编写强一致性分区容错复制系统时最重要的算法之一。它用于 Google 的许多系统中,包括 Chubby 锁管理器(被 BigTable/Megastore 使用)、Google 文件系统以及 Spanner

Paxos 以希腊岛屿 Paxos 命名,最初由 Leslie Lamport 在 1998 年一篇名为”The Part-Time Parliament”的论文中提出。它通常被认为难以实现,并且已经有一系列具有相当分布式系统专业知识的公司的论文解释了进一步的实际细节(参见进一步阅读)。你可能想阅读 Lamport 对这个问题的评论 这里这里

问题主要与 Paxos 被描述为单轮共识决策的事实有关,但实际工作的实现通常希望有效地运行多轮共识。这导致了许多 核心协议的扩展 的发展,任何有兴趣构建基于 Paxos 的系统的人仍然需要消化这些扩展。此外,还有其他实际挑战,如如何促进集群成员更改。

ZAB。ZAB——Zookeeper 原子广播协议用于 Apache Zookeeper。Zookeeper 是一个为分布式系统提供协调原语的系统,被许多以 Hadoop 为中心的分布式系统用于协调(例如,HBaseStormKafka)。Zookeeper 基本上是开源社区版本的 Chubby。从技术上讲,原子广播是一个与纯共识不同的问题,但它仍属于确保强一致性的分区容错算法类别。

Raft。Raft 是这个算法家族的最新(2013 年)成员。它的设计比 Paxos 更容易教授,同时提供相同的保证。特别是,算法的不同部分更清晰地分离,论文还描述了集群成员更改的机制。它最近被 etcd 采用,受到 ZooKeeper 的启发。

具有强一致性的复制方法

在本章中,我们看了看强制执行强一致性的复制方法。从同步工作和异步工作之间的对比开始,我们逐步研究了越来越复杂故障容错的算法。以下是每个算法的一些关键特征:

主/备

  • 单个、静态主节点
  • 复制日志,从节点不参与执行操作
  • 对复制延迟没有界限
  • 不是分区容错的
  • 手动/临时故障转移,不是容错的,“热备份”

2PC

  • 一致投票:提交或中止
  • 静态主节点
  • 2PC 不能在提交期间同时承受协调器和节点的故障
  • 不是分区容错的,尾部延迟敏感

Paxos

  • 多数投票
  • 动态主节点
  • 作为协议的一部分,对 n/2-1 同时故障具有弹性
  • 对尾部延迟不太敏感

进一步阅读

主 - 备和 2PC

Paxos

Raft 和 ZAB

本文为学习目的的个人翻译,译文仅供参考。

原文链接:Distributed systems for fun and profit - Chapter 4

版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。