mirror of
https://github.com/Vonng/ddia.git
synced 2026-07-29 17:49:57 +08:00
Merge branch 'master' into patch-1
This commit is contained in:
42
ch9.md
42
ch9.md
@@ -31,11 +31,11 @@
|
||||
|
||||
在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。
|
||||
|
||||
大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的复本最终会收敛到相同的值【2】。
|
||||
大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。
|
||||
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
|
||||
对于应用开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。如果将一个值赋给一个变量,然后很快地再次读取,你不会认为可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。
|
||||
对于应用开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。对于后者,如果将一个值赋给一个变量,然后很快地再次读取,不可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。
|
||||
|
||||
在与只提供弱保证的数据库打交道时,你需要始终意识到它的局限性,而不是意外地作出太多假设。错误往往是微妙的,很难找到,也很难测试,因为应用可能在大多数情况下运行良好。当系统出现故障(例如网络中断)或高并发时,最终一致性的边缘情况才会显现出来。
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。
|
||||
|
||||
[图9-2](img/fig9-2.png) 显示了三个客户端在线性一致数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器(register)**,例如,它可以是键值存储中的一个**键**,关系数据库中的一**行**,或文档数据库中的一个**文档**。
|
||||
|
||||
|
||||

|
||||
|
||||
**图9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值**
|
||||
@@ -148,9 +148,9 @@
|
||||
>
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
>
|
||||
> 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
|
||||
> 一个数据库可以提供可序列化和线性一致性,这种组合被称为严格的可序列化或强的**强单副本可序列化(strong-1SR)**【4,13】。基于两阶段锁定的可序列化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
|
||||
>
|
||||
> 但是,可序列化的快照隔离(参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。
|
||||
> 但是,可序列化的快照隔离(参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
|
||||
|
||||
|
||||
|
||||
@@ -162,11 +162,11 @@
|
||||
|
||||
一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。
|
||||
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,参阅“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,参阅“[领导者和锁](#领导者和锁)”中的防护问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。你可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,你需要在读取【15】之前调用`sync()`。参阅“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
|
||||
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中以更细的粒度使用。 RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中更多的粒度级别上使用。RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
|
||||
#### 约束和唯一性保证
|
||||
|
||||
@@ -176,9 +176,9 @@
|
||||
|
||||
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
|
||||
|
||||
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在“[及时性与完整性](ch12.md#及时性与完整性)”中讨论这种松散解释的约束。
|
||||
在实际应用中,宽松地处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在“[及时性与完整性](ch12.md#及时性与完整性)”中讨论这种宽松的约束。
|
||||
|
||||
然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。
|
||||
然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以不需要线性一致性【19】。
|
||||
|
||||
#### 跨信道的时序依赖
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
|
||||
***共识算法(线性一致)***
|
||||
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。正是由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
|
||||
***多主复制(非线性一致)***
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
|
||||
|
||||
|
||||
基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。[松散的法定人数](ch5.md#马虎法定人数和暗示交接)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。
|
||||
基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。[宽松的法定人数](ch5.md#宽松的法定人数与提示移交)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。
|
||||
|
||||
#### 线性一致性和法定人数
|
||||
|
||||
@@ -238,9 +238,9 @@
|
||||
|
||||
在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0,写入客户端通过向所有三个副本( $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端A并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端B也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0` 。
|
||||
|
||||
仲裁条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]())
|
||||
法定人数条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]())
|
||||
|
||||
有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读修复完成【27】;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读修复完成【27】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置操作,因为它需要一个共识算法【28】。
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
|
||||
在单主配置的条件下,如果数据中心之间的网络被中断,则连接到从库数据中心的客户端无法联系到主库,因此它们无法对数据库执行任何写入,也不能执行任何线性一致的读取。它们仍能从从库读取,但结果可能是陈旧的(非线性一致)。如果应用需要线性一致的读写,却又位于与主库网络中断的数据中心,则网络中断将导致这些应用不可用。
|
||||
|
||||
如果客户端可以直接连接到主库所在的数据中心,这就不是问题了,哪些应用可以继续正常工作。但直到网络链接修复之前,只能访问从库数据中心的客户端会中断运行。
|
||||
如果客户端可以直接连接到主库所在的数据中心,这就不是问题了,那些应用可以继续正常工作。但只能访问从库数据中心的客户端会中断运行,直到网络链接得到修复。
|
||||
|
||||
#### CAP定理
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
>
|
||||
> CAP有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种错误,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。
|
||||
>
|
||||
> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,一个更好的表达CAP的方法可以是一致的,或者在分区时可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。
|
||||
> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,CAP更好的表述成:在分区时要么选择一致,要么选择可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。
|
||||
>
|
||||
> 在CAP的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”(容错)系统实际上不符合CAP对可用性的特殊定义。总而言之,围绕着CAP有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用CAP。
|
||||
|
||||
@@ -334,7 +334,7 @@
|
||||
|
||||
因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
|
||||
|
||||
如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致(causally)**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。
|
||||
如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致(causally consistent)**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。
|
||||
|
||||
|
||||
|
||||
@@ -408,7 +408,7 @@
|
||||
如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法:
|
||||
|
||||
* 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。
|
||||
* 可以将时钟(物理时钟)时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入为准* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以将时钟(物理时钟)时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,而节点 B 可能要求序列号1,001到2,000区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。
|
||||
|
||||
这三个选项都比单一主库的自增计数器表现要好,并且更具可扩展性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。
|
||||
@@ -462,7 +462,7 @@
|
||||
|
||||
总之:为了实诸如如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中,没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。
|
||||
|
||||
如何知道你的全序关系已经尘埃落定,这个想法将在[全序广播](#全序广播)一节中详细说明。
|
||||
如何确定全序关系已经尘埃落定,这将在[全序广播](#全序广播)一节中详细说明。
|
||||
|
||||
### 全序广播
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
|
||||
没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。
|
||||
|
||||
**全序交付(totally ordered delivery)***
|
||||
***全序交付(totally ordered delivery)***
|
||||
|
||||
消息以相同的顺序传递给每个节点。
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
|
||||
### 实践中的分布式事务
|
||||
|
||||
分 布式事务的名声毁誉参半,尤其是那些通过两阶段提交实现的。一方面,它被视作提供了一个难以实现的重要的安全性保证;另一方面,它们因为导致运维问题,造成性能下降,做出超过能力范围的承诺而饱受批评【81,82,83,84】。许多云服务由于其导致的运维问题,而选择不实现分布式事务【85,86】。
|
||||
分布式事务的名声毁誉参半,尤其是那些通过两阶段提交实现的。一方面,它被视作提供了一个难以实现的重要的安全性保证;另一方面,它们因为导致运维问题,造成性能下降,做出超过能力范围的承诺而饱受批评【81,82,83,84】。许多云服务由于其导致的运维问题,而选择不实现分布式事务【85,86】。
|
||||
|
||||
分布式事务的某些实现会带来严重的性能损失 —— 例如据报告称,MySQL中的分布式事务比单节点事务慢10倍以上【87】,所以当人们建议不要使用它们时就不足为奇了。两阶段提交所固有的性能成本,大部分是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user