Merge branch 'master' into patch-1

This commit is contained in:
Feng Ruohang
2020-05-27 21:59:11 +08:00
committed by GitHub
9 changed files with 126 additions and 125 deletions

42
ch9.md
View File

@@ -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**,例如,它可以是键值存储中的一个**键**,关系数据库中的一**行**,或文档数据库中的一个**文档**。
![](img/fig9-2.png)
**图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 ClustersRAC【18】以更细的粒度使用。 RAC对每个磁盘页面使用一个锁多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
分布式锁也在一些分布式数据库如Oracle Real Application ClustersRAC【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】以及额外的网络往返。