故障与仲裁

节点故障

假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在主从复制中,如果要继续处理写入,则可能需要执行故障切换。另一方面,在无领导配置中,故障切换不存在:客户端(用户 1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。

仲裁写入,法定读取,并在节点中断后读修复

现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新。

读修复和反熵

复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?在 Dynamo 风格的数据存储中经常使用两种机制:读修复与反熵过程。

读修复(Read repair)

当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如用户 2345 获得了来自 Replica 3 的版本 6 值和来自副本 1 和 2 的版本 7 值。客户端发现副本 3 具有陈旧值,并将新值写回复制品。这种方法适用于频繁阅读的值。

仲裁写入,法定读取,并在节点中断后读修复

反熵过程(Anti-entropy process)

此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。

并不是所有的系统都实现了这两个;例如,Voldemort 目前没有反熵过程。请注意,如果没有反熵过程,某些副本中很少读取的值可能会丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。

读写的法定人数

我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?我们能推多远呢?如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。

更一般地说,如果有 n 个副本,每个写入必须由 w 节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点(在我们的例子中,$n = 3,w = 2,r = 2$)。只要$w + r> n$,我们期望在读取时获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值,w 值的读写称为法定人数(quorum)^vii 的读和写,你可以认为,r 和 w 是有效读写所需的最低票数。

在 Dynamo 风格的数据库中,参数 n,w 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。

集群中可能有多于 n 的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在 n 个节点上这允许对数据集进行分区,从而支持可以放在一个节点上的数据集更大的数据集。仲裁条件$w + r> n$允许系统容忍不可用的节点,如下所示:

  • 如果$w <n$,如果节点不可用,我们仍然可以处理写入。
  • 如果$r <n$,如果节点不可用,我们仍然可以处理读取。
  • 对于$n = 3,w = 2,r = 2$,我们可以容忍一个不可用的节点。
  • 对于$n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。
  • 通常,读取和写入操作始终并行发送到所有 n 个副本参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。

如果$w + r &gt; n$,读取r个副本,至少有一个r副本必然包含了最近的成功写入

如果少于所需的 w 或 r 节点可用,则写入或读取将返回错误由于许多原因,节点可能不可用:因为由于执行操作的错误(由于磁盘已满而无法写入)导致节点关闭(崩溃,关闭电源),由于客户端和服务器之间的网络中断 节点,或任何其他原因我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。

仲裁一致性的局限性

如果你有 n 个副本,并且你选择 w 和 r,使得$w + r> n$,你通常可以期望每个读取返回为一个键写的最近的值。情况就是这样,因为你写的节点集合和你读过的节点集合必须重叠。也就是说,您读取的节点中必须至少有一个具有最新值的节点。通常,r 和 w 被选为多数(超过 $n/2$)节点,因为这确保了$w + r> n$,同时仍然容忍多达$n/2$个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性。

您也可以将 w 和 r 设置为较小的数字,以使$w + r≤n$(即法定条件不满足)。在这种情况下,读取和写入操作仍将被发送到 n 个节点,但操作成功只需要少量的成功响应。较小的 w 和 r 更有可能会读取过时的数据,因为您的读取更有可能不包含具有最新值的节点。另一方面,这种配置允许更低的延迟和更高的可用性:如果存在网络中断,并且许多副本变得无法访问,则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于 w 或 r 时,数据库才分别变得不可用于写入或读取。

但是,即使在 $w + r> n$ 的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:

  • 如果使用松散的法定人数,w 个写入和 r 个读取落在完全不同的节点上,因此 r 节点和 w 之间不再保证有重叠节点。
  • 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅第 171 页的“处理写入冲突”)。如果根据时间戳(最后写入胜利)挑选出胜者,则由于时钟偏差,写入可能会丢失。
  • 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
  • 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于 w 个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值。
  • 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于 w,从而打破法定人数条件。
  • 即使一切工作正常,有时也会不幸地出现关于时序(timing)的边缘情况,在这种情况下,您可能会感到不安。

因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单 Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数 w 和 r 来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。

监控陈旧度

从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,您也需要了解复制的健康状况。如果显著落后,应该提醒您,以便您可以调查原因(例如,网络中的问题或超载节点)。

对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,您可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入次数)。通过从领导者的当前位置中减去随从者的当前位置,您可以测量复制滞后量。

然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。

已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 n,w 和 r 来预测陈旧读取的预期百分比。不幸的是,这还不是很常见的做法,但是将过时测量值包含在数据库的标准度量标准中是一件好事。最终的一致性是故意模糊的保证,但是对于可操作性来说,能够量化“最终”是很重要的。

松散法定人数与带提示的接力

合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢,因为请求不必等待所有 n 个节点响应——当 w 或 r 节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说,这些特性使无主复制的数据库很有吸引力。

然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于可用节点,因此客户端可能无法达到法定人数。

在一个大型的群集中(节点数量明显多于 n 个),网络中断期间客户端可能连接到某些数据库节点,而不是为了为特定值组成法定人数的节点们。在这种情况下,数据库设计人员需要权衡一下:

  • 将错误返回给我们无法达到 w 或 r 节点的法定数量的所有请求是否更好?
  • 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在 n 值通常存在的 n 个节点之间?

后者被认为是一个松散的法定人数(sloppy quorum):写和读仍然需要 w 和 r 成功的响应,但是那些可能包括不在指定的 n 个“主”节点中的值。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时停留在沙发上。一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的“本地”节点。这就是所谓的带提示的接力(hinted handoff)(一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家。)

松散法定人数对写入可用性的提高特别有用:只要有任何 w 节点可用,数据库就可以接受写入。然而,这意味着即使当$w + r> n$时,也不能确定读取某个键的最新值,因为最新的值可能已经临时写入了 n 之外的某些节点。因此,在传统意义上,一个松散的法定人数实际上不是一个法定人数。这只是一个保证,即数据存储在 w 节点的地方。不能保证 r 节点的读取直到提示已经完成。

在所有常见的 Dynamo 实现中,松散法定人数是可选的。在 Riak 中,它们默认是启用的,而在 Cassandra 和 Voldemort 中它们默认是禁用的。

运维多个数据中心

无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。

Cassandra 和 Voldemort 在正常的无主模型中实现了他们的多数据中心支持:副本的数量 n 包括所有数据中心的节点,在配置中,您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何,每个来自客户端的写入都会发送到所有副本,但客户端通常只等待来自其本地数据中心内的法定节点的确认,从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生,尽管配置有一定的灵活性。

Riak 将客户端和数据库节点之间的所有通信保持在一个数据中心本地,因此 n 描述了一个数据中心内的副本数量。数据库集群之间的跨数据中心复制在后台异步发生,其风格类似于多领导者复制。

下一页