mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
update ch5.md for leaderless replication
This commit is contained in:
parent
1542e29db5
commit
05c425f386
87
ch5.md
87
ch5.md
@ -462,25 +462,25 @@
|
||||
|
||||
我们在本章到目前为止所讨论的复制方法 —— 单主复制、多主复制 —— 都是这样的想法:客户端向一个主库发送写请求,而数据库系统负责将写入复制到其他副本。主库决定写入的顺序,而从库按相同顺序应用主库的写入。
|
||||
|
||||
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是 **无领导的(leaderless)**【1,44】,但是在关系数据库主导的时代,这个想法几乎已被忘却。在亚马逊将其用于其内部的 Dynamo 系统 [^vi] 之后,它再一次成为数据库的一种时尚架构【37】。 Riak,Cassandra 和 Voldemort 是由 Dynamo 启发的无领导复制模型的开源数据存储,所以这类数据库也被称为 *Dynamo 风格*。
|
||||
一些数据存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。最早的一些的复制数据系统是 **无主的(leaderless)**【1,44】,但是在关系数据库主导的时代,这个想法几乎已被忘却。在亚马逊将其用于其内部的 Dynamo 系统 [^vi] 之后,它再一次成为数据库的一种时尚架构【37】。Riak,Cassandra 和 Voldemort 是受 Dynamo 启发的无主复制模型的开源数据存储,所以这类数据库也被称为 *Dynamo 风格*。
|
||||
|
||||
[^vi]: Dynamo 不适用于 Amazon 以外的用户。 令人困惑的是,AWS 提供了一个名为 DynamoDB 的托管数据库产品,它使用了完全不同的体系结构:它基于单领导者复制。
|
||||
[^vi]: Dynamo 不适用于 Amazon 以外的用户。令人困惑的是,AWS 提供了一个名为 DynamoDB 的托管数据库产品,它使用了完全不同的体系结构:它基于单主复制。
|
||||
|
||||
在一些无领导者的实现中,客户端直接将写入发送到几个副本中,而另一些情况下,一个 **协调者(coordinator)** 节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
|
||||
在一些无主复制的实现中,客户端直接将写入发送到几个副本中,而另一些情况下,由一个 **协调者(coordinator)** 节点代表客户端进行写入。但与主库数据库不同,协调者不执行特定的写入顺序。我们将会看到,这种设计上的差异对数据库的使用方式有着深远的影响。
|
||||
|
||||
### 当节点故障时写入数据库
|
||||
|
||||
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅「[处理节点宕机](#处理节点宕机)」)。
|
||||
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于领导者的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅「[处理节点宕机](#处理节点宕机)」)。
|
||||
|
||||
另一方面,在无领导配置中,不存在故障转移。[图 5-10](img/fig5-10.png) 显示了发生了什么事情:客户端(用户 1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
|
||||
另一方面,在无主配置中,不存在故障转移。[图 5-10](img/fig5-10.png) 演示了会发生了什么事情:客户端(用户 1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
|
||||
|
||||
![](img/fig5-10.png)
|
||||
|
||||
**图 5-10 法定写入,法定读取,并在节点中断后读修复。**
|
||||
|
||||
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果你从该节点读取数据,则可能会将陈旧(过时)值视为响应。
|
||||
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭期间发生的任何写入都不在该节点上。因此,如果你从该节点读取数据,则可能会从响应中拿到陈旧的(过时的)值。
|
||||
|
||||
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(请参阅 “[检测并发写入](#检测并发写入)”)。
|
||||
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅把它的请求发送到一个副本:读请求将被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应,即来自一个节点的最新值和来自另一个节点的陈旧值。版本号将被用于确定哪个值是更新的(请参阅 “[检测并发写入](#检测并发写入)”)。
|
||||
|
||||
#### 读修复和反熵
|
||||
|
||||
@ -496,89 +496,88 @@
|
||||
|
||||
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。
|
||||
|
||||
并不是所有的系统都实现了这两个,例如,Voldemort 目前没有反熵过程。请注意,如果没有反熵过程,某些副本中很少读取的值可能会丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。
|
||||
并不是所有的系统都实现了这两种机制,例如,Voldemort 目前没有反熵过程。请注意,如果没有反熵过程,很少被读取的值可能会从某些副本中丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。
|
||||
|
||||
#### 读写的法定人数
|
||||
|
||||
在 [图 5-10](img/fig5-10.png) 的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写成功?
|
||||
在 [图 5-10](img/fig5-10.png) 的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写入成功?
|
||||
|
||||
如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。
|
||||
|
||||
更一般地说,如果有 n 个副本,每个写入必须由 w 节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r> n$,我们期望在读取时获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值,w 值的读写称为 **法定人数(quorum)**[^vii] 的读和写【44】。你可以认为,r 和 w 是有效读写所需的最低票数。
|
||||
更一般地说,如果有 n 个副本,每个写入必须由 w 个节点确认才能被认为是成功的,并且我们必须至少为每个读取查询 r 个节点。 (在我们的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我们可以预期在读取时能获得最新的值,因为 r 个读取中至少有一个节点是最新的。遵循这些 r 值和 w 值的读写称为 **法定人数(quorum)**[^vii] 的读和写【44】。你可以认为,r 和 w 是有效读写所需的最低票数。
|
||||
|
||||
[^vii]: 有时候这种法定人数被称为严格的法定人数,相对 “宽松的法定人数” 而言(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
|
||||
[^vii]: 有时候这种法定人数被称为严格的法定人数,其相对 “宽松的法定人数” 而言(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”)
|
||||
|
||||
在 Dynamo 风格的数据库中,参数 n,w 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,写入次数较少且读取次数较多的工作负载可以从设置 $w = n$ 和 $r = 1$中受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
|
||||
在 Dynamo 风格的数据库中,参数 n、w 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5)并设置 $w = r = (n + 1) / 2$(向上取整)。但是你可以根据需要更改数字。例如,写入次数较少且读取次数较多的工作负载可以从设置 $w = n$ 和 $r = 1$中受益。这会使得读取速度更快,但缺点是只要有一个不可用的节点就会导致所有的数据库写入都失败。
|
||||
|
||||
> 集群中可能有多于 n 的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](ch6.md) 继续讨论分区。
|
||||
>
|
||||
> 集群中可能有多于 n 个的节点(集群的机器数可能多于副本数目)。但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](ch6.md) 继续讨论分区。
|
||||
|
||||
法定人数条件 $w + r> n$ 允许系统容忍不可用的节点,如下所示:
|
||||
法定人数条件 $w + r > n$ 允许系统容忍不可用的节点,如下所示:
|
||||
|
||||
* 如果 $w <n$,如果节点不可用,我们仍然可以处理写入。
|
||||
* 如果 $r <n$,如果节点不可用,我们仍然可以处理读取。
|
||||
* 如果 $w < n$,当节点不可用时,我们仍然可以处理写入。
|
||||
* 如果 $r < n$,当节点不可用时,我们仍然可以处理读取。
|
||||
* 对于 $n = 3,w = 2,r = 2$,我们可以容忍一个不可用的节点。
|
||||
* 对于 $n = 5,w = 3,r = 3$,我们可以容忍两个不可用的节点。 这个案例如 [图 5-11](img/fig5-11.png) 所示。
|
||||
* 通常,读取和写入操作始终并行发送到所有 n 个副本。 参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
|
||||
* 通常,读取和写入操作始终并行发送到所有 n 个副本。参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
|
||||
|
||||
![](img/fig5-11.png)
|
||||
|
||||
**图 5-11 如果 $w + r > n$,读取 r 个副本,至少有一个 r 副本必然包含了最近的成功写入**
|
||||
**图 5-11 如果 $w + r > n$,读取 r 个副本,至少有一个副本必然包含了最近的成功写入。**
|
||||
|
||||
如果少于所需的 w 或 r 节点可用,则写入或读取将返回错误。 由于许多原因,节点可能不可用:因为执行操作的错误(由于磁盘已满而无法写入),因为节点关闭(崩溃,关闭电源),由于客户端和服务器节点之间的网络中断,或任何其他原因。 我们只关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
|
||||
如果可用的节点少于所需的 w 或 r,则写入或读取将返回错误。节点可能由于多种原因而不可用,比如:节点关闭(异常崩溃,电源关闭)、操作执行过程中的错误(由于磁盘已满而无法写入)、客户端和服务器节点之间的网络中断或任何其他原因。我们只需要关心节点是否返回了成功的响应,而不需要区分不同类型的错误。
|
||||
|
||||
|
||||
### 法定人数一致性的局限性
|
||||
|
||||
如果你有 n 个副本,并且你选择 w 和 r,使得 $w + r> n$,你通常可以期望每个键的读取都能返回最近写入的值。情况就是这样,因为你写入的节点集合和你读取的节点集合必须重叠。也就是说,你读取的节点中必须至少有一个具有最新值的节点(如 [图 5-11](img/fig5-11.png) 所示)。
|
||||
如果你有 n 个副本,并且你选择了满足 $w + r > n$ 的 w 和 r,你通常可以期望每次读取都能返回最近写入的值。情况就是这样,因为你写入的节点集合和你读取的节点集合必然有重叠。也就是说,你读取的节点中必然至少有一个节点具有最新值(如 [图 5-11](img/fig5-11.png) 所示)。
|
||||
|
||||
通常,r 和 w 被选为多数(超过 $n/2$ )节点,因为这确保了 $w + r> n$,同时仍然容忍多达 $n/2$ 个节点故障。但是,法定人数不一定必须是大多数,只是读写使用的节点交集至少需要包括一个节点。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性【45】。
|
||||
通常,r 和 w 被选为多数(超过 $n/2$ )节点,因为这确保了 $w + r > n$,同时仍然容忍多达 $n/2$ 个节点故障。但是,法定人数不一定必须是大多数,重要的是读写使用的节点至少有一个节点的交集。其他法定人数的配置是可能的,这使得分布式算法的设计有一定的灵活性【45】。
|
||||
|
||||
你也可以将 w 和 r 设置为较小的数字,以使 $w + r≤n$(即法定条件不满足)。在这种情况下,读取和写入操作仍将被发送到 n 个节点,但操作成功只需要少量的成功响应。
|
||||
你也可以将 w 和 r 设置为较小的数字,以使 $w + r ≤ n$(即法定条件不满足)。在这种情况下,读取和写入操作仍将被发送到 n 个节点,但操作成功只需要少量的成功响应。
|
||||
|
||||
较小的 w 和 r 更有可能会读取过时的数据,因为你的读取更有可能不包含具有最新值的节点。另一方面,这种配置允许更低的延迟和更高的可用性:如果存在网络中断,并且许多副本变得无法访问,则可以继续处理读取和写入的机会更大。只有当可达副本的数量低于 w 或 r 时,数据库才分别变得不可用于写入或读取。
|
||||
较小的 w 和 r 更有可能会读取到陈旧的数据,因为你的读取更有可能未包含具有最新值的节点。另一方面,这种配置允许更低的延迟和更高的可用性:如果存在网络中断,并且许多副本变得无法访问,则有更大的机会可以继续处理读取和写入。只有当可达副本的数量低于 w 或 r 时,数据库才变得不可写入或读取。
|
||||
|
||||
但是,即使在 $w + r> n$ 的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
|
||||
但是,即使在 $w + r > n$ 的情况下,也可能存在返回陈旧值的边缘情况。这取决于实现,但可能的情况包括:
|
||||
|
||||
* 如果使用宽松的法定人数(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”),w 个写入和 r 个读取落在完全不同的节点上,因此 r 节点和 w 之间不再保证有重叠节点【46】。
|
||||
* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅 “[处理写入冲突](#处理写入冲突)”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则由于时钟偏差【35】,写入可能会丢失。我们将在 “[检测并发写入](#检测并发写入)” 继续讨论此话题。
|
||||
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
|
||||
* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于 w 个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
|
||||
* 如果携带新值的节点失败,需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复,则存储新值的副本数可能会低于 w,从而打破法定人数条件。
|
||||
* 如果使用宽松的法定人数(见 “[宽松的法定人数与提示移交](#宽松的法定人数与提示移交)”),w 个写入和 r 个读取有可能落在完全不同的节点上,因此 r 节点和 w 之间不再保证有重叠节点【46】。
|
||||
* 如果两个写入同时发生,不清楚哪一个先发生。在这种情况下,唯一安全的解决方案是合并并发写入(请参阅 “[处理写入冲突](#处理写入冲突)”)。如果根据时间戳(最后写入胜利)挑选出一个胜者,则写入可能由于时钟偏差【35】而丢失。我们将在 “[检测并发写入](#检测并发写入)” 继续讨论此话题。
|
||||
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取返回的是旧值还是新值。
|
||||
* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于 w 个副本上写入成功。所以整体判定写入失败,但整体写入失败并没有在写入成功的副本上回滚。这意味着一个写入虽然报告失败,后续的读取仍然可能会读取这次失败写入的值【47】。
|
||||
* 如果携带新值的节点发生故障,需要从其他带有旧值的副本进行恢复,则存储新值的副本数可能会低于 w,从而打破法定人数条件。
|
||||
* 即使一切工作正常,有时也会不幸地出现关于 **时序(timing)** 的边缘情况,我们将在 “[线性一致性和法定人数](ch9.md#线性一致性和法定人数)” 中看到这点。
|
||||
|
||||
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数 w 和 r 来调整读取陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
||||
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。你可以通过参数 w 和 r 来调整读取到陈旧值的概率,但把它们当成绝对的保证是不明智的。
|
||||
|
||||
尤其是,因为通常没有得到 “[复制延迟问题](#复制延迟问题)” 中讨论的保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务** 或 **共识**。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到这些话题。
|
||||
尤其是,因为通常得不到 “[复制延迟问题](#复制延迟问题)” 中讨论的那些保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务** 或 **共识**。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到这些话题。
|
||||
|
||||
#### 监控陈旧度
|
||||
|
||||
从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,你也需要了解复制的健康状况。如果显著落后,应该提醒你,以便你可以调查原因(例如,网络中的问题或超载节点)。
|
||||
从运维的角度来看,监视你的数据库是否返回最新的结果是很重要的。即使应用可以容忍陈旧的读取,你也需要了解复制的健康状况。如果显著落后,它应该提醒你以便你可以调查原因(例如网络中的问题或过载的节点)。
|
||||
|
||||
对于基于领导者的复制,数据库通常会公开复制滞后的度量标准,你可以将其提供给监视系统。这是可能的,因为写入按照相同的顺序应用于领导者和追随者,并且每个节点在复制日志中具有一个位置(在本地应用的写入数量)。通过从领导者的当前位置中减去追随者的当前位置,你可以测量复制滞后量。
|
||||
对于基于领导者的复制,数据库通常会提供复制延迟的测量值,你可以将其提供给监视系统。这之所以能做到,是因为写入是按照相同的顺序应用于主库和从库,并且每个节点对应了复制日志中的一个位置(已经在本地应用的写入数量)。通过从主库的当前位置中减去从库的当前位置,你可以测量复制延迟的程度。
|
||||
|
||||
然而,在无领导者复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多大的限制是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
|
||||
然而,在无主复制的系统中,没有固定的写入顺序,这使得监控变得更加困难。而且,如果数据库只使用读修复(没有反熵过程),那么对于一个值可能会有多陈旧其实是没有限制的 - 如果一个值很少被读取,那么由一个陈旧副本返回的值可能是古老的。
|
||||
|
||||
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 n,w 和 r 来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将陈旧测量值包含在数据库的标准度量集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化 “最终” 也是很重要的。
|
||||
已经有一些关于衡量无主复制数据库中的复制陈旧度的研究,并根据参数 n、w 和 r 来预测陈旧读取的预期百分比【48】。不幸的是,这还不是很常见的做法,但是将陈旧测量值包含在数据库的标准度量集中是一件好事。虽然最终一致性是一种有意模糊的保证,但是从可操作性角度来说,能够量化 “最终” 也是很重要的。
|
||||
|
||||
### 宽松的法定人数与提示移交
|
||||
|
||||
合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。也可以容忍个别节点变慢,因为请求不必等待所有 n 个节点响应 —— 当 w 或 r 节点响应时它们可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说,这些特性使无主复制的数据库很有吸引力。
|
||||
合理配置的法定人数可以使数据库无需故障切换即可容忍个别节点的故障。它也可以容忍个别节点变慢,因为请求不必等待所有 n 个节点响应 —— 当 w 或 r 个节点响应时它们就可以返回。对于需要高可用、低延时、且能够容忍偶尔读到陈旧值的应用场景来说,这些特性使无主复制的数据库很有吸引力。
|
||||
|
||||
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能能够连接到它们,但是从数据库节点切断的客户端来看,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于 w 或 r,因此客户端不再能达到法定人数。
|
||||
然而,法定人数(如迄今为止所描述的)并不像它们可能的那样具有容错性。网络中断可以很容易地将客户端从大量的数据库节点上切断。虽然这些节点是活着的,而其他客户端可能也能够连接到它们,但是从数据库节点切断的客户端来看,它们也可能已经死亡。在这种情况下,剩余的可用节点可能会少于 w 或 r,因此客户端不再能达到法定人数。
|
||||
|
||||
在一个大型的集群中(节点数量明显多于 n 个),网络中断期间客户端可能仍能连接到一些数据库节点,但又不足以组成一个特定值的法定人数。在这种情况下,数据库设计人员需要权衡一下:
|
||||
在一个大型的集群中(节点数量明显多于 n 个),网络中断期间客户端可能仍能连接到一些数据库节点,但又不足以组成一个特定的法定人数。在这种情况下,数据库设计人员需要权衡一下:
|
||||
|
||||
* 对于所有无法达到 w 或 r 节点法定人数的请求,是否返回错误是更好的?
|
||||
* 对于所有无法达到 w 或 r 个节点法定人数的请求,是否返回错误是更好的?
|
||||
* 或者我们是否应该接受写入,然后将它们写入一些可达的节点,但不在这些值通常所存在的 n 个节点上?
|
||||
|
||||
后者被认为是一个 **宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要 w 和 r 成功的响应,但这些响应可能来自不在指定的 n 个 “主” 节点中的其它节点。比方说,如果你把自己锁在房子外面,你可能会敲开邻居的门,问你是否可以暂时呆在沙发上。
|
||||
后者被认为是一个 **宽松的法定人数(sloppy quorum)**【37】:写和读仍然需要 w 和 r 个成功的响应,但这些响应可能来自不在指定的 n 个 “主” 节点中的其它节点。就好比说,如果你把自己锁在房子外面了,你可能会去敲开邻居的门,问是否可以暂时呆在他们的沙发上。
|
||||
|
||||
一旦网络中断得到解决,代表另一个节点临时接受的一个节点的任何写入都被发送到适当的 “主” 节点。这就是所谓的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的钥匙,你的邻居礼貌地要求你离开沙发回家)。
|
||||
一旦网络中断得到解决,一个节点代表另一个节点临时接受的任何写入都将被发送到适当的 “主” 节点。这就是所谓的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的钥匙,你的邻居可以礼貌地要求你离开沙发回家)。
|
||||
|
||||
宽松的法定人数对写入可用性的提高特别有用:只要有任何 w 节点可用,数据库就可以接受写入。然而,这意味着即使当 $w + r> n$ 时,也不能确定读取某个键的最新值,因为最新的值可能已经临时写入了 n 之外的某些节点【47】。
|
||||
宽松的法定人数对写入可用性的提高特别有用:只要有任何 w 个节点可用,数据库就可以接受写入。然而,这意味着即使当 $w + r > n$ 时,也不能确保读取到某个键的最新值,因为最新的值可能已经临时写入了 n 之外的某些节点【47】。
|
||||
|
||||
因此,在传统意义上,一个宽松的法定人数实际上不是一个法定人数。这只是一个保证,即数据存储在 w 节点的地方。但不能保证 r 节点的读取,直到提示移交已经完成。
|
||||
因此,在传统意义上,宽松的法定人数实际上并不是法定人数。它只是一个持久性的保证,即数据已存储在某处的 w 个节点。但不能保证 r 个节点的读取能看到它,除非提示移交已经完成。
|
||||
|
||||
在所有常见的 Dynamo 实现中,宽松的法定人数是可选的。在 Riak 中,它们默认是启用的,而在 Cassandra 和 Voldemort 中它们默认是禁用的【46,49,50】。
|
||||
|
||||
|
176
zh-tw/ch5.md
176
zh-tw/ch5.md
@ -288,29 +288,29 @@
|
||||
|
||||
## 多主複製
|
||||
|
||||
本章到目前為止,我們只考慮使用單個領導者的複製架構。 雖然這是一種常見的方法,但也有一些有趣的選擇。
|
||||
本章到目前為止,我們只考慮了使用單個主庫的複製架構。雖然這是一種常見的方法,但還有其它一些有趣的選擇。
|
||||
|
||||
基於領導者的複製有一個主要的缺點:只有一個主庫,而所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
|
||||
基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
|
||||
|
||||
[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個領導。 不同的分割槽可能在不同的節點上有其領導者,但是每個分割槽必須有一個領導者節點。
|
||||
[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。
|
||||
|
||||
基於領導者的複製模型的自然延伸是允許多個節點接受寫入。 複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料更改轉發給所有其他節點。 稱之為 **多領導者配置**(也稱多主、多活複製)。 在這種情況下,每個領導者同時扮演其他領導者的追隨者。
|
||||
基於領導者的複製模型的自然延伸是允許多個節點接受寫入。複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料變更轉發給所有其他節點。我們將其稱之為 **多領導者配置**(multi-leader configuration,也稱多主、多活複製,即 master-master replication 或 active/active replication)。在這種情況下,每個主庫同時是其他主庫的從庫。
|
||||
|
||||
### 多主複製的應用場景
|
||||
|
||||
在單個數據中心內部使用多個主庫沒有太大意義,因為複雜性已經超過了能帶來的好處。 但在一些情況下,多活配置是也合理的。
|
||||
在單個數據中心內部使用多個主庫的配置沒有太大意義,因為其導致的複雜性已經超過了能帶來的好處。但在一些情況下,這種配置也是合理的。
|
||||
|
||||
#### 運維多個數據中心
|
||||
|
||||
假如你有一個數據庫,副本分散在好幾個不同的資料中心(也許這樣可以容忍單個數據中心的故障,或地理上更接近使用者)。 使用常規的基於領導者的複製設定,主庫必須位於其中一個數據中心,且所有寫入都必須經過該資料中心。
|
||||
假如你有一個數據庫,副本分散在好幾個不同的資料中心(可能會用來容忍單個數據中心的故障,或者為了在地理上更接近使用者)。如果使用常規的基於領導者的複製設定,主庫必須位於其中一個數據中心,且所有寫入都必須經過該資料中心。
|
||||
|
||||
多領導者配置中可以在每個資料中心都有主庫。 [圖 5-6](../img/fig5-6.png) 展示了這個架構的樣子。 在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
|
||||
多領導者配置中可以在每個資料中心都有主庫。[圖 5-6](../img/fig5-6.png) 展示了這個架構。在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
|
||||
|
||||
![](../img/fig5-6.png)
|
||||
|
||||
**圖 5-6 跨多個數據中心的多主複製**
|
||||
|
||||
我們來比較一下在運維多個數據中心時,單主和多主的適應情況。
|
||||
我們來比較一下在運維多個數據中心時,單主和多主的適應情況:
|
||||
|
||||
* 效能
|
||||
|
||||
@ -318,17 +318,17 @@
|
||||
|
||||
* 容忍資料中心停機
|
||||
|
||||
在單主配置中,如果主庫所在的資料中心發生故障,故障切換必須使另一個數據中心裡的追隨者成為領導者。在多主配置中,每個資料中心可以獨立於其他資料中心繼續執行,並且當發生故障的資料中心歸隊時,複製會自動趕上。
|
||||
在單主配置中,如果主庫所在的資料中心發生故障,故障切換必須使另一個數據中心裡的從庫成為主庫。在多主配置中,每個資料中心可以獨立於其他資料中心繼續執行,並且當發生故障的資料中心歸隊時,複製會自動趕上。
|
||||
|
||||
* 容忍網路問題
|
||||
|
||||
資料中心之間的通訊通常穿過公共網際網路,這可能不如資料中心內的本地網路可靠。單主配置對這資料中心間的連線問題非常敏感,因為透過這個連線進行的寫操作是同步的。採用非同步複製功能的多主配置通常能更好地承受網路問題:臨時的網路中斷並不會妨礙正在處理的寫入。
|
||||
資料中心之間的通訊通常穿過公共網際網路,這可能不如資料中心內的本地網路可靠。單主配置對資料中心之間的連線問題非常敏感,因為透過這個連線進行的寫操作是同步的。採用非同步複製功能的多主配置通常能更好地承受網路問題:臨時的網路中斷並不會妨礙正在處理的寫入。
|
||||
|
||||
有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於 MySQL 的 Tungsten Replicator 【26】,用於 PostgreSQL 的 BDR【27】以及用於 Oracle 的 GoldenGate 【19】。
|
||||
|
||||
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如 [圖 5-6](../img/fig5-6.png) 中 “衝突解決(conflict resolution)”)。本書將在 “[處理寫入衝突](#處理寫入衝突)” 中詳細討論這個問題。
|
||||
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如 [圖 5-6](../img/fig5-6.png) 中的 “衝突解決(conflict resolution)”)。本書將在 “[處理寫入衝突](#處理寫入衝突)” 中詳細討論這個問題。
|
||||
|
||||
由於多主複製在許多資料庫中都屬於改裝的功能,所以常常存在微妙的配置缺陷,且經常與其他資料庫功能之間出現意外的反應。例如自增主鍵、觸發器、完整性約束等,都可能會有麻煩。因此,多主複製往往被認為是危險的領域,應儘可能避免【28】。
|
||||
由於多主複製在許多資料庫中都屬於改裝的功能,所以常常存在微妙的配置缺陷,且經常與其他資料庫功能之間出現意外的反應。比如自增主鍵、觸發器、完整性約束等都可能會有麻煩。因此,多主複製往往被認為是危險的領域,應儘可能避免【28】。
|
||||
|
||||
#### 需要離線操作的客戶端
|
||||
|
||||
@ -336,19 +336,19 @@
|
||||
|
||||
例如,考慮手機,膝上型電腦和其他裝置上的日曆應用。無論裝置目前是否有網際網路連線,你需要能隨時檢視你的會議(發出讀取請求),輸入新的會議(發出寫入請求)。如果在離線狀態下進行任何更改,則裝置下次上線時,需要與伺服器和其他裝置同步。
|
||||
|
||||
在這種情況下,每個裝置都有一個充當領導者的本地資料庫(它接受寫請求),並且在所有裝置上的日曆副本之間同步時,存在非同步的多主複製過程。複製延遲可能是幾小時甚至幾天,具體取決於何時可以訪問網際網路。
|
||||
在這種情況下,每個裝置都有一個充當主庫的本地資料庫(它接受寫請求),並且在所有裝置上的日曆副本之間同步時,存在非同步的多主複製過程。複製延遲可能是幾小時甚至幾天,具體取決於何時可以訪問網際網路。
|
||||
|
||||
從架構的角度來看,這種設定實際上與資料中心之間的多領導者複製類似,每個裝置都是一個 “資料中心”,而它們之間的網路連線是極度不可靠的。從歷史上各類日曆同步功能的破爛實現可以看出,想把多活配好是多麼困難的一件事。
|
||||
從架構的角度來看,這種設定實際上與資料中心之間的多領導者複製類似,每個裝置都是一個 “資料中心”,而它們之間的網路連線是極度不可靠的。從歷史上各類日曆同步功能的破爛實現可以看出,想把多主複製用好是多麼困難的一件事。
|
||||
|
||||
有一些工具旨在使這種多領導者配置更容易。例如,CouchDB 就是為這種操作模式而設計的【29】。
|
||||
|
||||
#### 協同編輯
|
||||
|
||||
實時協作編輯應用程式允許多個人同時編輯文件。例如,Etherpad 【30】和 Google Docs 【31】允許多人同時編輯文字文件或電子表格(該演算法在 “[自動衝突解決](#自動衝突解決)” 中簡要討論)。我們通常不會將協作式編輯視為資料庫複製問題,但與前面提到的離線編輯用例有許多相似之處。當一個使用者編輯文件時,所做的更改將立即應用到其本地副本(Web 瀏覽器或客戶端應用程式中的文件狀態),並非同步複製到伺服器和編輯同一文件的任何其他使用者。
|
||||
實時協作編輯應用程式允許多個人同時編輯文件。例如,Etherpad 【30】和 Google Docs 【31】允許多人同時編輯文字文件或電子表格(該演算法在 “[自動衝突解決](#自動衝突解決)” 中簡要討論)。我們通常不會將協作式編輯視為資料庫複製問題,但它與前面提到的離線編輯用例有許多相似之處。當一個使用者編輯文件時,所做的更改將立即應用到其本地副本(Web 瀏覽器或客戶端應用程式中的文件狀態),並非同步複製到伺服器和編輯同一文件的任何其他使用者。
|
||||
|
||||
如果要保證不會發生編輯衝突,則應用程式必須先取得文件的鎖定,然後使用者才能對其進行編輯。如果另一個使用者想要編輯同一個文件,他們首先必須等到第一個使用者提交修改並釋放鎖定。這種協作模式相當於主從複製模型下在主節點上執行事務操作。
|
||||
|
||||
但是,為了加速協作,你可能希望將更改的單位設定得非常小(例如,一個按鍵),並避免鎖定。這種方法允許多個使用者同時進行編輯,但同時也帶來了多領導者複製的所有挑戰,包括需要解決衝突【32】。
|
||||
但是,為了加速協作,你可能希望將更改的單位設定得非常小(例如單次按鍵),並避免鎖定。這種方法允許多個使用者同時進行編輯,但同時也帶來了多主複製的所有挑戰,包括需要解決衝突【32】。
|
||||
|
||||
### 處理寫入衝突
|
||||
|
||||
@ -362,17 +362,17 @@
|
||||
|
||||
#### 同步與非同步衝突檢測
|
||||
|
||||
在單主資料庫中,第二個寫入將被阻塞,並等待第一個寫入完成,或中止第二個寫入事務,強制使用者重試。另一方面,在多主配置中,兩個寫入都是成功的,並且在稍後的時間點僅僅非同步地檢測到衝突。那時要求使用者解決衝突可能為時已晚。
|
||||
在單主資料庫中,第二個寫入將被阻塞並等待第一個寫入完成,或者中止第二個寫入事務並強制使用者重試。另一方面,在多主配置中,兩個寫入都是成功的,在稍後的某個時間點才能非同步地檢測到衝突。那時再來要求使用者解決衝突可能為時已晚。
|
||||
|
||||
原則上,可以使衝突檢測同步 - 即等待寫入被複制到所有副本,然後再告訴使用者寫入成功。但是,透過這樣做,你將失去多主複製的主要優點:允許每個副本獨立接受寫入。如果你想要同步衝突檢測,那麼你可以使用單主程式複製。
|
||||
原則上,可以使衝突檢測同步 - 即等待寫入被複制到所有副本,然後再告訴使用者寫入成功。但是,透過這樣做,你將失去多主複製的主要優點:允許每個副本獨立地接受寫入。如果你想要同步衝突檢測,那麼你可能不如直接使用單主複製。
|
||||
|
||||
#### 避免衝突
|
||||
|
||||
處理衝突的最簡單的策略就是避免它們:如果應用程式可以確保特定記錄的所有寫入都透過同一個領導者,那麼衝突就不會發生。由於許多的多領導者複製實現在處理衝突時處理得相當不好,避免衝突是一個經常推薦的方法【34】。
|
||||
處理衝突的最簡單的策略就是避免它們:如果應用程式可以確保特定記錄的所有寫入都透過同一個主庫,那麼衝突就不會發生。由於許多的多主複製實現在處理衝突時處理得相當不好,避免衝突是一個經常被推薦的方法【34】。
|
||||
|
||||
例如,在使用者可以編輯自己的資料的應用程式中,可以確保來自特定使用者的請求始終路由到同一資料中心,並使用該資料中心的領導者進行讀寫。不同的使用者可能有不同的 “家庭” 資料中心(可能根據使用者的地理位置選擇),但從任何使用者的角度來看,配置基本上都是單一的領導者。
|
||||
例如,在一個使用者可以編輯自己資料的應用程式中,可以確保來自特定使用者的請求始終路由到同一資料中心,並使用該資料中心的主庫進行讀寫。不同的使用者可能有不同的 “主” 資料中心(可能根據使用者的地理位置選擇),但從任何一位使用者的角度來看,本質上就是單主配置了。
|
||||
|
||||
但是,有時你可能需要更改指定的記錄的主庫 —— 可能是因為一個數據中心出現故障,你需要將流量重新路由到另一個數據中心,或者可能是因為使用者已經遷移到另一個位置,現在更接近不同的資料中心。在這種情況下,衝突避免會中斷,你必須處理不同主庫同時寫入的可能性。
|
||||
但是,有時你可能需要更改被指定的主庫 —— 可能是因為某個資料中心出現故障,你需要將流量重新路由到另一個數據中心,或者可能是因為使用者已經遷移到另一個位置,現在更接近其它的資料中心。在這種情況下,衝突避免將失效,你必須處理不同主庫同時寫入的可能性。
|
||||
|
||||
#### 收斂至一致的狀態
|
||||
|
||||
@ -380,11 +380,11 @@
|
||||
|
||||
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在 [圖 5-7](../img/fig5-7.png) 中,在主庫 1 中標題首先更新為 B 而後更新為 C;在主庫 2 中,首先更新為 C,然後更新為 B。兩種順序都不比另一種“更正確”。
|
||||
|
||||
如果每個副本只是按照它看到寫入的順序寫入,那麼資料庫最終將處於不一致的狀態:最終值將是在主庫 1 的 C 和主庫 2 的 B。這是不可接受的,每個複製方案都必須確保資料在所有副本中最終都是相同的。因此,資料庫必須以一種 **收斂(convergent)** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
|
||||
如果每個副本只是按照它看到寫入的順序寫入,那麼資料庫最終將處於不一致的狀態:最終值將是在主庫 1 的 C 和主庫 2 的 B。這是不可接受的,每個複製方案都必須確保資料最終在所有副本中都是相同的。因此,資料庫必須以一種 **收斂(convergent)** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
|
||||
|
||||
實現衝突合併解決有多種途徑:
|
||||
|
||||
* 給每個寫入一個唯一的 ID(例如,一個時間戳,一個長的隨機數,一個 UUID 或者一個鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利(LWW, last write wins)**。雖然這種方法很流行,但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 更詳細地討論 LWW。
|
||||
* 給每個寫入一個唯一的 ID(例如時間戳、長隨機數、UUID 或者鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利(LWW, last write wins)**。雖然這種方法很流行,但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 一節更詳細地討論 LWW。
|
||||
* 為每個副本分配一個唯一的 ID,ID 編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
|
||||
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](../img/fig5-7.png) 中,合併的標題可能類似於 “B/C”)。
|
||||
* 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。
|
||||
@ -392,7 +392,7 @@
|
||||
|
||||
#### 自定義衝突解決邏輯
|
||||
|
||||
作為解決衝突最合適的方法可能取決於應用程式,大多數多主複製工具允許使用應用程式程式碼編寫衝突解決邏輯。該程式碼可以在寫入或讀取時執行:
|
||||
解決衝突的最合適的方法可能取決於應用程式,大多數多主複製工具允許使用應用程式程式碼編寫衝突解決邏輯。該程式碼可以在寫入或讀取時執行:
|
||||
|
||||
* 寫時執行
|
||||
|
||||
@ -400,88 +400,87 @@
|
||||
|
||||
* 讀時執行
|
||||
|
||||
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB 以這種方式工作。
|
||||
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可以提示使用者或自動解決衝突,並將結果寫回資料庫。例如 CouchDB 就以這種方式工作。
|
||||
|
||||
請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果你有一個事務會原子性地進行幾次不同的寫入(請參閱 [第七章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
|
||||
請注意,衝突解決通常適用於單行記錄或單個文件的層面,而不是整個事務【36】。因此,如果你有一個事務會原子性地進行幾次不同的寫入(請參閱 [第七章](ch7.md)),對於衝突解決而言,每個寫入仍需分開單獨考慮。
|
||||
|
||||
|
||||
> #### 自動衝突解決
|
||||
>
|
||||
> 衝突解決規則可能很快變得複雜,並且自定義程式碼可能容易出錯。亞馬遜是一個經常被引用的例子,由於衝突解決處理程式而產生令人意外的效果:一段時間以來,購物車上的衝突解決邏輯將保留新增到購物車的物品,但不包括從購物車中移除的物品。因此,顧客有時會看到物品重新出現在他們的購物車中,即使他們之前已經被移走【37】。
|
||||
> 衝突解決規則可能很容易變得變得越來越複雜,自定義程式碼可能也很容易出錯。亞馬遜是一個經常被引用的例子,由於衝突解決處理程式而產生了令人意外的效果:一段時間以來,購物車上的衝突解決邏輯將保留新增到購物車的物品,但不包括從購物車中移除的物品。因此,顧客有時會看到物品重新出現在他們的購物車中,即使他們之前已經被移走【37】。
|
||||
>
|
||||
> 已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾項研究值得一提:
|
||||
>
|
||||
> * **無衝突複製資料型別(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多個使用者同時編輯的集合,對映,有序列表,計數器等的一系列資料結構,它們以合理的方式自動解決衝突。一些 CRDT 已經在 Riak 2.0 中實現【39,40】。
|
||||
> * **無衝突複製資料型別(Conflict-free replicated datatypes,CRDT)**【32,38】是可以由多個使用者同時編輯的集合、對映、有序列表、計數器等一系列資料結構,它們以合理的方式自動解決衝突。一些 CRDT 已經在 Riak 2.0 中實現【39,40】。
|
||||
> * **可合併的持久資料結構(Mergeable persistent data structures)**【41】顯式跟蹤歷史記錄,類似於 Git 版本控制系統,並使用三向合併功能(而 CRDT 使用雙向合併)。
|
||||
> * **可執行的轉換(operational transformation)**[42] 是 Etherpad 【30】和 Google Docs 【31】等合作編輯應用背後的衝突解決演算法。它是專為同時編輯專案的有序列表而設計的,例如構成文字文件的字元列表。
|
||||
>
|
||||
> 這些演算法在資料庫中的實現還很年輕,但很可能將來它們將被整合到更多的複製資料系統中。自動衝突解決方案可以使應用程式處理多領導者資料同步更為簡單。
|
||||
> * **操作轉換(operational transformation)**[42] 是 Etherpad 【30】和 Google Docs 【31】等協同編輯應用背後的衝突解決演算法。它是專為有序列表的併發編輯而設計的,例如構成文字文件的字元列表。
|
||||
>
|
||||
> 這些演算法在資料庫中的實現還很年輕,但很可能將來它們會被整合到更多的複製資料系統中。自動衝突解決方案可以使應用程式處理多主資料同步更為簡單。
|
||||
|
||||
|
||||
#### 什麼是衝突?
|
||||
|
||||
有些衝突是顯而易見的。在 [圖 5-7](../img/fig5-7.png) 的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
|
||||
|
||||
其他型別的衝突可能更為微妙,難以發現。例如,考慮一個會議室預訂系統:它記錄誰訂了哪個時間段的哪個房間。應用需要確保每個房間只有一組人同時預定(即不得有相同房間的重疊預訂)。在這種情況下,如果同時為同一個房間建立兩個不同的預訂,則可能會發生衝突。即使應用程式在允許使用者進行預訂之前檢查可用性,如果兩次預訂是由兩個不同的領導者進行的,則可能會有衝突。
|
||||
其他型別的衝突可能更為微妙而難以發現。例如,考慮一個會議室預訂系統:它記錄誰訂了哪個時間段的哪個房間。應用程式需要確保每個房間在任意時刻都只能被一組人進行預定(即不得有相同房間的重疊預訂)。在這種情況下,如果為同一個房間同時建立兩個不同的預訂,則可能會發生衝突。即使應用程式在允許使用者進行預訂之前先檢查會議室的可用性,如果兩次預訂是由兩個不同的主庫進行的,則仍然可能會有衝突。
|
||||
|
||||
現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在 [第七章](ch7.md) 中看到更多的衝突示例,在 [第十二章](ch12.md) 中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
|
||||
雖然現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在 [第七章](ch7.md) 中看到更多的衝突示例,在 [第十二章](ch12.md) 中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
|
||||
|
||||
|
||||
### 多主複製拓撲
|
||||
|
||||
**複製拓撲**(replication topology)描述寫入從一個節點傳播到另一個節點的通訊路徑。如果你有兩個領導者,如 [圖 5-7](../img/fig5-7.png) 所示,只有一個合理的拓撲結構:領導者 1 必須把他所有的寫到領導者 2,反之亦然。當有兩個以上的領導,各種不同的拓撲是可能的。[圖 5-8](../img/fig5-8.png) 舉例說明了一些例子。
|
||||
**複製拓撲**(replication topology)用來描述寫入操作從一個節點傳播到另一個節點的通訊路徑。如果你有兩個主庫,如 [圖 5-7](../img/fig5-7.png) 所示,只有一個合理的拓撲結構:主庫 1 必須把它所有的寫入都發送到主庫 2,反之亦然。當有兩個以上的主庫,多種不同的拓撲都是可能的。[圖 5-8](../img/fig5-8.png) 舉例說明了一些例子。
|
||||
|
||||
![](../img/fig5-8.png)
|
||||
|
||||
**圖 5-8 三個可以設定多領導者複製的示例拓撲。**
|
||||
**圖 5-8 三種可以在多主複製中使用的拓撲示例。**
|
||||
|
||||
最普遍的拓撲是全部到全部([圖 5-8 (c)](../img/fig5-8.png)),其中每個領導者將其寫入每個其他領導。但是,也會使用更多受限制的拓撲:例如,預設情況下,MySQL 僅支援 **環形拓撲(circular topology)**【34】,其中每個節點接收來自一個節點的寫入,並將這些寫入(加上自己的任何寫入)轉發給另一個節點。另一種流行的拓撲結構具有星形的形狀 [^v]。一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
|
||||
最常見的拓撲是全部到全部(all-to-all,如 [圖 5-8 (c)](../img/fig5-8.png)),其中每個主庫都將其寫入傳送給其他所有的主庫。然而,一些更受限的拓撲也會被使用到:例如,預設情況下 MySQL 僅支援 **環形拓撲(circular topology)**【34】,其中每個節點都從一個節點接收寫入,並將這些寫入(加上自己的寫入)轉發給另一個節點。另一種流行的拓撲結構具有星形的形狀 [^v]:一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
|
||||
|
||||
[^v]: 不要與星型模式混淆(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了資料模型的結構,而不是節點之間的通訊拓撲。
|
||||
|
||||
在環形和星形拓撲中,寫入可能需要在到達所有副本之前透過多個節點。因此,節點需要轉發從其他節點收到的資料更改。為了防止無限複製迴圈,每個節點被賦予一個唯一的識別符號,並且在複製日誌中,每次寫入都會使用其經過的所有節點的識別符號進行標記【43】。當一個節點收到用自己的識別符號標記的資料更改時,該資料更改將被忽略,因為節點知道它已經被處理過。
|
||||
|
||||
環形和星形拓撲的問題是,如果只有一個節點發生故障,則可能會中斷其他節點之間的複製訊息流,導致它們無法通訊,直到節點修復。拓撲結構可以重新配置為在發生故障的節點上工作,但在大多數部署中,這種重新配置必須手動完成。更密集連線的拓撲結構(例如全部到全部)的容錯性更好,因為它允許訊息沿著不同的路徑傳播,避免單點故障。
|
||||
環形和星形拓撲的問題是,如果只有一個節點發生故障,則可能會中斷其他節點之間的複製訊息流,導致它們無法通訊,除非節點被修復。拓撲結構可以重新配置為跳過發生故障的節點,但在大多數部署中,這種重新配置必須手動完成。更密集連線的拓撲結構(例如全部到全部)的容錯性更好,因為它允許訊息沿著不同的路徑傳播,可以避免單點故障。
|
||||
|
||||
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如,由於網路擁塞),結果是一些複製訊息可能 “超過” 其他複製訊息,如 [圖 5-9](../img/fig5-9.png) 所示。
|
||||
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如由於網路擁塞),結果是一些複製訊息可能 “超越” 其他複製訊息,如 [圖 5-9](../img/fig5-9.png) 所示。
|
||||
|
||||
![](../img/fig5-9.png)
|
||||
|
||||
**圖 5-9 使用多主程式複製時,可能會在某些副本中寫入錯誤的順序。**
|
||||
**圖 5-9 使用多主複製時,寫入可能會以錯誤的順序到達某些副本。**
|
||||
|
||||
在 [圖 5-9](../img/fig5-9.png) 中,客戶端 A 向主庫 1 的表中插入一行,客戶端 B 在主庫 3 上更新該行。然而,主庫 2 可以以不同的順序接收寫入:它可以首先接收更新(從它的角度來看,是對資料庫中不存在的行的更新),並且僅在稍後接收到相應的插入(其應該在更新之前)。
|
||||
在 [圖 5-9](../img/fig5-9.png) 中,客戶端 A 向主庫 1 的表中插入一行,客戶端 B 在主庫 3 上更新該行。然而,主庫 2 可以以不同的順序接收寫入:它可能先接收到更新(從它的角度來看,是對資料庫中不存在的行的更新),稍後才接收到相應的插入(其應該在更新之前)。
|
||||
|
||||
這是一個因果關係的問題,類似於我們在 “[一致字首讀](#一致字首讀)” 中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,以便在主庫 2 處正確地排序這些事件(見 [第八章](ch8.md))。
|
||||
這是一個因果關係的問題,類似於我們在 “[一致字首讀](#一致字首讀)” 中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,所以主庫 2 就無法正確地對這些事件進行排序(見 [第八章](ch8.md))。
|
||||
|
||||
要正確排序這些事件,可以使用一種稱為 **版本向量(version vectors)** 的技術,本章稍後將討論這種技術(請參閱 “[檢測併發寫入](#檢測併發寫入)”)。然而,衝突檢測技術在許多多領導者複製系統中執行得不好。例如,在撰寫本文時,PostgreSQL BDR 不提供寫入的因果排序【27】,而 Tungsten Replicator for MySQL 甚至不嘗試檢測衝突【34】。
|
||||
要正確排序這些事件,可以使用一種稱為 **版本向量(version vectors)** 的技術,本章稍後將討論這種技術(請參閱 “[檢測併發寫入](#檢測併發寫入)”)。然而,許多多主複製系統中的衝突檢測技術實現得並不好。例如,在撰寫本文時,PostgreSQL BDR 不提供寫入的因果排序【27】,而 Tungsten Replicator for MySQL 甚至都不做檢測衝突【34】。
|
||||
|
||||
如果你正在使用具有多領導者複製功能的系統,那麼應該瞭解這些問題,仔細閱讀文件,並徹底測試你的資料庫,以確保它確實提供了你認為具有的保證。
|
||||
如果你正在使用基於多主複製的系統,那麼你應該多瞭解這些問題,仔細閱讀文件,並徹底測試你的資料庫,以確保它確實提供了你想要的保證。
|
||||
|
||||
|
||||
## 無主複製
|
||||
|
||||
我們在本章到目前為止所討論的複製方法 —— 單主複製、多主複製 —— 都是這樣的想法:客戶端向一個主庫傳送寫請求,而資料庫系統負責將寫入複製到其他副本。主庫決定寫入的順序,而從庫按相同順序應用主庫的寫入。
|
||||
|
||||
一些資料儲存系統採用不同的方法,放棄主庫的概念,並允許任何副本直接接受來自客戶端的寫入。最早的一些的複製資料系統是 **無領導的(leaderless)**【1,44】,但是在關係資料庫主導的時代,這個想法幾乎已被忘卻。在亞馬遜將其用於其內部的 Dynamo 系統 [^vi] 之後,它再一次成為資料庫的一種時尚架構【37】。 Riak,Cassandra 和 Voldemort 是由 Dynamo 啟發的無領導複製模型的開源資料儲存,所以這類資料庫也被稱為 *Dynamo 風格*。
|
||||
一些資料儲存系統採用不同的方法,放棄主庫的概念,並允許任何副本直接接受來自客戶端的寫入。最早的一些的複製資料系統是 **無主的(leaderless)**【1,44】,但是在關係資料庫主導的時代,這個想法幾乎已被忘卻。在亞馬遜將其用於其內部的 Dynamo 系統 [^vi] 之後,它再一次成為資料庫的一種時尚架構【37】。Riak,Cassandra 和 Voldemort 是受 Dynamo 啟發的無主複製模型的開源資料儲存,所以這類資料庫也被稱為 *Dynamo 風格*。
|
||||
|
||||
[^vi]: Dynamo 不適用於 Amazon 以外的使用者。 令人困惑的是,AWS 提供了一個名為 DynamoDB 的託管資料庫產品,它使用了完全不同的體系結構:它基於單領導者複製。
|
||||
[^vi]: Dynamo 不適用於 Amazon 以外的使用者。令人困惑的是,AWS 提供了一個名為 DynamoDB 的託管資料庫產品,它使用了完全不同的體系結構:它基於單主複製。
|
||||
|
||||
在一些無領導者的實現中,客戶端直接將寫入傳送到幾個副本中,而另一些情況下,一個 **協調者(coordinator)** 節點代表客戶端進行寫入。但與主庫資料庫不同,協調者不執行特定的寫入順序。我們將會看到,這種設計上的差異對資料庫的使用方式有著深遠的影響。
|
||||
在一些無主複製的實現中,客戶端直接將寫入傳送到幾個副本中,而另一些情況下,由一個 **協調者(coordinator)** 節點代表客戶端進行寫入。但與主庫資料庫不同,協調者不執行特定的寫入順序。我們將會看到,這種設計上的差異對資料庫的使用方式有著深遠的影響。
|
||||
|
||||
### 當節點故障時寫入資料庫
|
||||
|
||||
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
|
||||
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於領導者的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
|
||||
|
||||
另一方面,在無領導配置中,不存在故障轉移。[圖 5-10](../img/fig5-10.png) 顯示了發生了什麼事情:客戶端(使用者 1234)並行傳送寫入到所有三個副本,並且兩個可用副本接受寫入,但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的:在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
|
||||
另一方面,在無主配置中,不存在故障轉移。[圖 5-10](../img/fig5-10.png) 演示了會發生了什麼事情:客戶端(使用者 1234)並行傳送寫入到所有三個副本,並且兩個可用副本接受寫入,但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的:在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
|
||||
|
||||
![](../img/fig5-10.png)
|
||||
|
||||
**圖 5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
|
||||
|
||||
現在想象一下,不可用的節點重新聯機,客戶端開始讀取它。節點關閉時發生的任何寫入都從該節點丟失。因此,如果你從該節點讀取資料,則可能會將陳舊(過時)值視為響應。
|
||||
現在想象一下,不可用的節點重新聯機,客戶端開始讀取它。節點關閉期間發生的任何寫入都不在該節點上。因此,如果你從該節點讀取資料,則可能會從響應中拿到陳舊的(過時的)值。
|
||||
|
||||
為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅傳送它的請求到一個副本:讀請求也被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應。即來自一個節點的最新值和來自另一個節點的陳舊值。版本號用於確定哪個值更新(請參閱 “[檢測併發寫入](#檢測併發寫入)”)。
|
||||
為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅把它的請求傳送到一個副本:讀請求將被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應,即來自一個節點的最新值和來自另一個節點的陳舊值。版本號將被用於確定哪個值是更新的(請參閱 “[檢測併發寫入](#檢測併發寫入)”)。
|
||||
|
||||
#### 讀修復和反熵
|
||||
|
||||
@ -497,89 +496,88 @@
|
||||
|
||||
此外,一些資料儲存具有後臺程序,該程序不斷查詢副本之間的資料差異,並將任何缺少的資料從一個副本複製到另一個副本。與基於領導者的複製中的複製日誌不同,此反熵過程不會以任何特定的順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
|
||||
|
||||
並不是所有的系統都實現了這兩個,例如,Voldemort 目前沒有反熵過程。請注意,如果沒有反熵過程,某些副本中很少讀取的值可能會丟失,從而降低了永續性,因為只有在應用程式讀取值時才執行讀修復。
|
||||
並不是所有的系統都實現了這兩種機制,例如,Voldemort 目前沒有反熵過程。請注意,如果沒有反熵過程,很少被讀取的值可能會從某些副本中丟失,從而降低了永續性,因為只有在應用程式讀取值時才執行讀修復。
|
||||
|
||||
#### 讀寫的法定人數
|
||||
|
||||
在 [圖 5-10](../img/fig5-10.png) 的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫成功?
|
||||
在 [圖 5-10](../img/fig5-10.png) 的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫入成功?
|
||||
|
||||
如果我們知道,每個成功的寫操作意味著在三個副本中至少有兩個出現,這意味著至多有一個副本可能是陳舊的。因此,如果我們從至少兩個副本讀取,我們可以確定至少有一個是最新的。如果第三個副本停機或響應速度緩慢,則讀取仍可以繼續返回最新值。
|
||||
|
||||
更一般地說,如果有 n 個副本,每個寫入必須由 w 節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢 r 個節點。 (在我們的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r> n$,我們期望在讀取時獲得最新的值,因為 r 個讀取中至少有一個節點是最新的。遵循這些 r 值,w 值的讀寫稱為 **法定人數(quorum)**[^vii] 的讀和寫【44】。你可以認為,r 和 w 是有效讀寫所需的最低票數。
|
||||
更一般地說,如果有 n 個副本,每個寫入必須由 w 個節點確認才能被認為是成功的,並且我們必須至少為每個讀取查詢 r 個節點。 (在我們的例子中,$n = 3,w = 2,r = 2$)。只要 $w + r > n$,我們可以預期在讀取時能獲得最新的值,因為 r 個讀取中至少有一個節點是最新的。遵循這些 r 值和 w 值的讀寫稱為 **法定人數(quorum)**[^vii] 的讀和寫【44】。你可以認為,r 和 w 是有效讀寫所需的最低票數。
|
||||
|
||||
[^vii]: 有時候這種法定人數被稱為嚴格的法定人數,相對 “寬鬆的法定人數” 而言(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”)
|
||||
[^vii]: 有時候這種法定人數被稱為嚴格的法定人數,其相對 “寬鬆的法定人數” 而言(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”)
|
||||
|
||||
在 Dynamo 風格的資料庫中,引數 n,w 和 r 通常是可配置的。一個常見的選擇是使 n 為奇數(通常為 3 或 5)並設定 $w = r =(n + 1)/ 2$(向上取整)。但是可以根據需要更改數字。例如,寫入次數較少且讀取次數較多的工作負載可以從設定 $w = n$ 和 $r = 1$中受益。這使得讀取速度更快,但具有隻有一個失敗節點導致所有資料庫寫入失敗的缺點。
|
||||
在 Dynamo 風格的資料庫中,引數 n、w 和 r 通常是可配置的。一個常見的選擇是使 n 為奇數(通常為 3 或 5)並設定 $w = r = (n + 1) / 2$(向上取整)。但是你可以根據需要更改數字。例如,寫入次數較少且讀取次數較多的工作負載可以從設定 $w = n$ 和 $r = 1$中受益。這會使得讀取速度更快,但缺點是隻要有一個不可用的節點就會導致所有的資料庫寫入都失敗。
|
||||
|
||||
> 叢集中可能有多於 n 的節點。(叢集的機器數可能多於副本數目),但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在 [第六章](ch6.md) 繼續討論分割槽。
|
||||
>
|
||||
> 叢集中可能有多於 n 個的節點(叢集的機器數可能多於副本數目)。但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在 [第六章](ch6.md) 繼續討論分割槽。
|
||||
|
||||
法定人數條件 $w + r> n$ 允許系統容忍不可用的節點,如下所示:
|
||||
法定人數條件 $w + r > n$ 允許系統容忍不可用的節點,如下所示:
|
||||
|
||||
* 如果 $w <n$,如果節點不可用,我們仍然可以處理寫入。
|
||||
* 如果 $r <n$,如果節點不可用,我們仍然可以處理讀取。
|
||||
* 如果 $w < n$,當節點不可用時,我們仍然可以處理寫入。
|
||||
* 如果 $r < n$,當節點不可用時,我們仍然可以處理讀取。
|
||||
* 對於 $n = 3,w = 2,r = 2$,我們可以容忍一個不可用的節點。
|
||||
* 對於 $n = 5,w = 3,r = 3$,我們可以容忍兩個不可用的節點。 這個案例如 [圖 5-11](../img/fig5-11.png) 所示。
|
||||
* 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。 引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
|
||||
* 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
|
||||
|
||||
![](../img/fig5-11.png)
|
||||
|
||||
**圖 5-11 如果 $w + r > n$,讀取 r 個副本,至少有一個 r 副本必然包含了最近的成功寫入**
|
||||
**圖 5-11 如果 $w + r > n$,讀取 r 個副本,至少有一個副本必然包含了最近的成功寫入。**
|
||||
|
||||
如果少於所需的 w 或 r 節點可用,則寫入或讀取將返回錯誤。 由於許多原因,節點可能不可用:因為執行操作的錯誤(由於磁碟已滿而無法寫入),因為節點關閉(崩潰,關閉電源),由於客戶端和伺服器節點之間的網路中斷,或任何其他原因。 我們只關心節點是否返回了成功的響應,而不需要區分不同型別的錯誤。
|
||||
如果可用的節點少於所需的 w 或 r,則寫入或讀取將返回錯誤。節點可能由於多種原因而不可用,比如:節點關閉(異常崩潰,電源關閉)、操作執行過程中的錯誤(由於磁碟已滿而無法寫入)、客戶端和伺服器節點之間的網路中斷或任何其他原因。我們只需要關心節點是否返回了成功的響應,而不需要區分不同型別的錯誤。
|
||||
|
||||
|
||||
### 法定人數一致性的侷限性
|
||||
|
||||
如果你有 n 個副本,並且你選擇 w 和 r,使得 $w + r> n$,你通常可以期望每個鍵的讀取都能返回最近寫入的值。情況就是這樣,因為你寫入的節點集合和你讀取的節點集合必須重疊。也就是說,你讀取的節點中必須至少有一個具有最新值的節點(如 [圖 5-11](../img/fig5-11.png) 所示)。
|
||||
如果你有 n 個副本,並且你選擇了滿足 $w + r > n$ 的 w 和 r,你通常可以期望每次讀取都能返回最近寫入的值。情況就是這樣,因為你寫入的節點集合和你讀取的節點集合必然有重疊。也就是說,你讀取的節點中必然至少有一個節點具有最新值(如 [圖 5-11](../img/fig5-11.png) 所示)。
|
||||
|
||||
通常,r 和 w 被選為多數(超過 $n/2$ )節點,因為這確保了 $w + r> n$,同時仍然容忍多達 $n/2$ 個節點故障。但是,法定人數不一定必須是大多數,只是讀寫使用的節點交集至少需要包括一個節點。其他法定人數的配置是可能的,這使得分散式演算法的設計有一定的靈活性【45】。
|
||||
通常,r 和 w 被選為多數(超過 $n/2$ )節點,因為這確保了 $w + r > n$,同時仍然容忍多達 $n/2$ 個節點故障。但是,法定人數不一定必須是大多數,重要的是讀寫使用的節點至少有一個節點的交集。其他法定人數的配置是可能的,這使得分散式演算法的設計有一定的靈活性【45】。
|
||||
|
||||
你也可以將 w 和 r 設定為較小的數字,以使 $w + r≤n$(即法定條件不滿足)。在這種情況下,讀取和寫入操作仍將被傳送到 n 個節點,但操作成功只需要少量的成功響應。
|
||||
你也可以將 w 和 r 設定為較小的數字,以使 $w + r ≤ n$(即法定條件不滿足)。在這種情況下,讀取和寫入操作仍將被傳送到 n 個節點,但操作成功只需要少量的成功響應。
|
||||
|
||||
較小的 w 和 r 更有可能會讀取過時的資料,因為你的讀取更有可能不包含具有最新值的節點。另一方面,這種配置允許更低的延遲和更高的可用性:如果存在網路中斷,並且許多副本變得無法訪問,則可以繼續處理讀取和寫入的機會更大。只有當可達副本的數量低於 w 或 r 時,資料庫才分別變得不可用於寫入或讀取。
|
||||
較小的 w 和 r 更有可能會讀取到陳舊的資料,因為你的讀取更有可能未包含具有最新值的節點。另一方面,這種配置允許更低的延遲和更高的可用性:如果存在網路中斷,並且許多副本變得無法訪問,則有更大的機會可以繼續處理讀取和寫入。只有當可達副本的數量低於 w 或 r 時,資料庫才變得不可寫入或讀取。
|
||||
|
||||
但是,即使在 $w + r> n$ 的情況下,也可能存在返回陳舊值的邊緣情況。這取決於實現,但可能的情況包括:
|
||||
但是,即使在 $w + r > n$ 的情況下,也可能存在返回陳舊值的邊緣情況。這取決於實現,但可能的情況包括:
|
||||
|
||||
* 如果使用寬鬆的法定人數(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”),w 個寫入和 r 個讀取落在完全不同的節點上,因此 r 節點和 w 之間不再保證有重疊節點【46】。
|
||||
* 如果兩個寫入同時發生,不清楚哪一個先發生。在這種情況下,唯一安全的解決方案是合併併發寫入(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。如果根據時間戳(最後寫入勝利)挑選出一個勝者,則由於時鐘偏差【35】,寫入可能會丟失。我們將在 “[檢測併發寫入](#檢測併發寫入)” 繼續討論此話題。
|
||||
* 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取是返回舊值還是新值。
|
||||
* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗,但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著如果一個寫入雖然報告失敗,後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
|
||||
* 如果攜帶新值的節點失敗,需要讀取其他帶有舊值的副本。並且其資料從帶有舊值的副本中恢復,則儲存新值的副本數可能會低於 w,從而打破法定人數條件。
|
||||
* 如果使用寬鬆的法定人數(見 “[寬鬆的法定人數與提示移交](#寬鬆的法定人數與提示移交)”),w 個寫入和 r 個讀取有可能落在完全不同的節點上,因此 r 節點和 w 之間不再保證有重疊節點【46】。
|
||||
* 如果兩個寫入同時發生,不清楚哪一個先發生。在這種情況下,唯一安全的解決方案是合併併發寫入(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。如果根據時間戳(最後寫入勝利)挑選出一個勝者,則寫入可能由於時鐘偏差【35】而丟失。我們將在 “[檢測併發寫入](#檢測併發寫入)” 繼續討論此話題。
|
||||
* 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取返回的是舊值還是新值。
|
||||
* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗,但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著一個寫入雖然報告失敗,後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
|
||||
* 如果攜帶新值的節點發生故障,需要從其他帶有舊值的副本進行恢復,則儲存新值的副本數可能會低於 w,從而打破法定人數條件。
|
||||
* 即使一切工作正常,有時也會不幸地出現關於 **時序(timing)** 的邊緣情況,我們將在 “[線性一致性和法定人數](ch9.md#線性一致性和法定人數)” 中看到這點。
|
||||
|
||||
因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。允許透過引數 w 和 r 來調整讀取陳舊值的概率,但把它們當成絕對的保證是不明智的。
|
||||
因此,儘管法定人數似乎保證讀取返回最新的寫入值,但在實踐中並不那麼簡單。 Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的概率,但把它們當成絕對的保證是不明智的。
|
||||
|
||||
尤其是,因為通常沒有得到 “[複製延遲問題](#複製延遲問題)” 中討論的保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務** 或 **共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。
|
||||
尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務** 或 **共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。
|
||||
|
||||
#### 監控陳舊度
|
||||
|
||||
從運維的角度來看,監視你的資料庫是否返回最新的結果是很重要的。即使應用可以容忍陳舊的讀取,你也需要了解複製的健康狀況。如果顯著落後,應該提醒你,以便你可以調查原因(例如,網路中的問題或超載節點)。
|
||||
從運維的角度來看,監視你的資料庫是否返回最新的結果是很重要的。即使應用可以容忍陳舊的讀取,你也需要了解複製的健康狀況。如果顯著落後,它應該提醒你以便你可以調查原因(例如網路中的問題或過載的節點)。
|
||||
|
||||
對於基於領導者的複製,資料庫通常會公開復制滯後的度量標準,你可以將其提供給監視系統。這是可能的,因為寫入按照相同的順序應用於領導者和追隨者,並且每個節點在複製日誌中具有一個位置(在本地應用的寫入數量)。透過從領導者的當前位置中減去追隨者的當前位置,你可以測量複製滯後量。
|
||||
對於基於領導者的複製,資料庫通常會提供複製延遲的測量值,你可以將其提供給監視系統。這之所以能做到,是因為寫入是按照相同的順序應用於主庫和從庫,並且每個節點對應了複製日誌中的一個位置(已經在本地應用的寫入數量)。透過從主庫的當前位置中減去從庫的當前位置,你可以測量複製延遲的程度。
|
||||
|
||||
然而,在無領導者複製的系統中,沒有固定的寫入順序,這使得監控變得更加困難。而且,如果資料庫只使用讀修復(沒有反熵過程),那麼對於一個值可能會有多大的限制是沒有限制的 - 如果一個值很少被讀取,那麼由一個陳舊副本返回的值可能是古老的。
|
||||
然而,在無主複製的系統中,沒有固定的寫入順序,這使得監控變得更加困難。而且,如果資料庫只使用讀修復(沒有反熵過程),那麼對於一個值可能會有多陳舊其實是沒有限制的 - 如果一個值很少被讀取,那麼由一個陳舊副本返回的值可能是古老的。
|
||||
|
||||
已經有一些關於衡量無主複製資料庫中的複製陳舊度的研究,並根據引數 n,w 和 r 來預測陳舊讀取的預期百分比【48】。不幸的是,這還不是很常見的做法,但是將陳舊測量值包含在資料庫的標準度量集中是一件好事。雖然最終一致性是一種有意模糊的保證,但是從可操作性角度來說,能夠量化 “最終” 也是很重要的。
|
||||
已經有一些關於衡量無主複製資料庫中的複製陳舊度的研究,並根據引數 n、w 和 r 來預測陳舊讀取的預期百分比【48】。不幸的是,這還不是很常見的做法,但是將陳舊測量值包含在資料庫的標準度量集中是一件好事。雖然最終一致性是一種有意模糊的保證,但是從可操作性角度來說,能夠量化 “最終” 也是很重要的。
|
||||
|
||||
### 寬鬆的法定人數與提示移交
|
||||
|
||||
合理配置的法定人數可以使資料庫無需故障切換即可容忍個別節點的故障。也可以容忍個別節點變慢,因為請求不必等待所有 n 個節點響應 —— 當 w 或 r 節點響應時它們可以返回。對於需要高可用、低延時、且能夠容忍偶爾讀到陳舊值的應用場景來說,這些特性使無主複製的資料庫很有吸引力。
|
||||
合理配置的法定人數可以使資料庫無需故障切換即可容忍個別節點的故障。它也可以容忍個別節點變慢,因為請求不必等待所有 n 個節點響應 —— 當 w 或 r 個節點響應時它們就可以返回。對於需要高可用、低延時、且能夠容忍偶爾讀到陳舊值的應用場景來說,這些特性使無主複製的資料庫很有吸引力。
|
||||
|
||||
然而,法定人數(如迄今為止所描述的)並不像它們可能的那樣具有容錯性。網路中斷可以很容易地將客戶端從大量的資料庫節點上切斷。雖然這些節點是活著的,而其他客戶端可能能夠連線到它們,但是從資料庫節點切斷的客戶端來看,它們也可能已經死亡。在這種情況下,剩餘的可用節點可能會少於 w 或 r,因此客戶端不再能達到法定人數。
|
||||
然而,法定人數(如迄今為止所描述的)並不像它們可能的那樣具有容錯性。網路中斷可以很容易地將客戶端從大量的資料庫節點上切斷。雖然這些節點是活著的,而其他客戶端可能也能夠連線到它們,但是從資料庫節點切斷的客戶端來看,它們也可能已經死亡。在這種情況下,剩餘的可用節點可能會少於 w 或 r,因此客戶端不再能達到法定人數。
|
||||
|
||||
在一個大型的叢集中(節點數量明顯多於 n 個),網路中斷期間客戶端可能仍能連線到一些資料庫節點,但又不足以組成一個特定值的法定人數。在這種情況下,資料庫設計人員需要權衡一下:
|
||||
在一個大型的叢集中(節點數量明顯多於 n 個),網路中斷期間客戶端可能仍能連線到一些資料庫節點,但又不足以組成一個特定的法定人數。在這種情況下,資料庫設計人員需要權衡一下:
|
||||
|
||||
* 對於所有無法達到 w 或 r 節點法定人數的請求,是否返回錯誤是更好的?
|
||||
* 對於所有無法達到 w 或 r 個節點法定人數的請求,是否返回錯誤是更好的?
|
||||
* 或者我們是否應該接受寫入,然後將它們寫入一些可達的節點,但不在這些值通常所存在的 n 個節點上?
|
||||
|
||||
後者被認為是一個 **寬鬆的法定人數(sloppy quorum)**【37】:寫和讀仍然需要 w 和 r 成功的響應,但這些響應可能來自不在指定的 n 個 “主” 節點中的其它節點。比方說,如果你把自己鎖在房子外面,你可能會敲開鄰居的門,問你是否可以暫時呆在沙發上。
|
||||
後者被認為是一個 **寬鬆的法定人數(sloppy quorum)**【37】:寫和讀仍然需要 w 和 r 個成功的響應,但這些響應可能來自不在指定的 n 個 “主” 節點中的其它節點。就好比說,如果你把自己鎖在房子外面了,你可能會去敲開鄰居的門,問是否可以暫時呆在他們的沙發上。
|
||||
|
||||
一旦網路中斷得到解決,代表另一個節點臨時接受的一個節點的任何寫入都被傳送到適當的 “主” 節點。這就是所謂的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的鑰匙,你的鄰居禮貌地要求你離開沙發回家)。
|
||||
一旦網路中斷得到解決,一個節點代表另一個節點臨時接受的任何寫入都將被傳送到適當的 “主” 節點。這就是所謂的 **提示移交(hinted handoff)**(一旦你再次找到你的房子的鑰匙,你的鄰居可以禮貌地要求你離開沙發回家)。
|
||||
|
||||
寬鬆的法定人數對寫入可用性的提高特別有用:只要有任何 w 節點可用,資料庫就可以接受寫入。然而,這意味著即使當 $w + r> n$ 時,也不能確定讀取某個鍵的最新值,因為最新的值可能已經臨時寫入了 n 之外的某些節點【47】。
|
||||
寬鬆的法定人數對寫入可用性的提高特別有用:只要有任何 w 個節點可用,資料庫就可以接受寫入。然而,這意味著即使當 $w + r > n$ 時,也不能確保讀取到某個鍵的最新值,因為最新的值可能已經臨時寫入了 n 之外的某些節點【47】。
|
||||
|
||||
因此,在傳統意義上,一個寬鬆的法定人數實際上不是一個法定人數。這只是一個保證,即資料儲存在 w 節點的地方。但不能保證 r 節點的讀取,直到提示移交已經完成。
|
||||
因此,在傳統意義上,寬鬆的法定人數實際上並不是法定人數。它只是一個永續性的保證,即資料已儲存在某處的 w 個節點。但不能保證 r 個節點的讀取能看到它,除非提示移交已經完成。
|
||||
|
||||
在所有常見的 Dynamo 實現中,寬鬆的法定人數是可選的。在 Riak 中,它們預設是啟用的,而在 Cassandra 和 Voldemort 中它們預設是禁用的【46,49,50】。
|
||||
|
||||
@ -615,9 +613,9 @@ Dynamo 風格的資料庫允許多個客戶端同時寫入相同的 Key,這意
|
||||
|
||||
實現最終融合的一種方法是宣告每個副本只需要儲存 **“最近”** 的值,並允許 **“更舊”** 的值被覆蓋和拋棄。然後,只要我們有一種明確的方式來確定哪個寫是 “最近的”,並且每個寫入最終都被複制到每個副本,那麼複製最終會收斂到相同的值。
|
||||
|
||||
正如 **“最近”** 的引號所表明的,這個想法其實頗具誤導性。在 [圖 5-12](../img/fig5-12.png) 的例子中,當客戶端向資料庫節點發送寫入請求時,客戶端都不知道另一個客戶端,因此不清楚哪一個先發生了。事實上,說 “發生” 是沒有意義的:我們說寫入是 **併發(concurrent)** 的,所以它們的順序是不確定的。
|
||||
正如 **“最近”** 的引號所表明的,這個想法其實頗具誤導性。在 [圖 5-12](../img/fig5-12.png) 的例子中,當客戶端向資料庫節點發送寫入請求時,兩個客戶端都不知道另一個客戶端,因此不清楚哪一個先發送請求。事實上,說這兩種情況誰先發送請求是沒有意義的:我們說寫入是 **併發(concurrent)** 的,所以它們的順序是不確定的。
|
||||
|
||||
即使寫入沒有自然的排序,我們也可以強制任意排序。例如,可以為每個寫入附加一個時間戳,挑選最 **“最近”** 的最大時間戳,並丟棄具有較早時間戳的任何寫入。這種衝突解決演算法被稱為 **最後寫入勝利(LWW, last write wins)**,是 Cassandra 【53】唯一支援的衝突解決方法,也是 Riak 【35】中的一個可選特徵。
|
||||
即使寫入沒有自然的排序,我們也可以強制任意排序。例如,可以為每個寫入附加一個時間戳,挑選的最大時間戳作為**“最近的”**,並丟棄具有較早時間戳的任何寫入。這種衝突解決演算法被稱為 **最後寫入勝利(LWW, last write wins)**,是 Cassandra 【53】唯一支援的衝突解決方法,也是 Riak 【35】中的一個可選特徵。
|
||||
|
||||
LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同一個 Key 有多個併發寫入,即使它們報告給客戶端的都是成功(因為它們被寫入 w 個副本),也只有一個寫入將存活,而其他寫入將被靜默丟棄。此外,LWW 甚至可能會刪除不是併發的寫入,我們將在的 “[有序事件的時間戳](ch8.md#有序事件的時間戳)” 中討論。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user