This commit is contained in:
Vonng 2018-03-26 00:18:02 +08:00
parent 7580e80d16
commit af1b8a24ea
4 changed files with 220 additions and 210 deletions

View File

@ -128,7 +128,8 @@
All contribution will give proper credit. 贡献者需要同意[法律声明](#法律声明)所叙内容。
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点修正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章第一部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)

4
ch5.md
View File

@ -596,9 +596,9 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
在所有常见的Dynamo实现中松散法定人数是可选的。在Riak中它们默认是启用的而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。
#### 多数据中心操作
#### 运维数据中心
我们先前讨论了跨数据中心复制作为多主复制的用例(请参阅第162页的“[多重复制]()”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持副本的数量n包括所有数据中心的节点在配置中您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何每个来自客户端的写入都会发送到所有副本但客户端通常只等待来自其本地数据中心内的法定节点的确认从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生尽管配置有一定的灵活性【50,51】。

6
ch8.md
View File

@ -484,15 +484,15 @@ while(true){
这个问题就是我们先前在“[进程暂停](#进程暂停)”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。
#### 击剑令牌
#### 防护令牌
当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是**屏蔽fencing**,如[图8-5]()所示
当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是**防护fencing**,如[图8-5]()所示
![](img/fig8-5.png)
**图8-5 只允许以增加屏蔽令牌的顺序进行写操作,从而保证存储安全**
我们假设每次锁定服务器授予锁或租约时,它还会返回一个**屏蔽令牌fencing token**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。
我们假设每次锁定服务器授予锁或租约时,它还会返回一个**防护令牌fencing token**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。
在[图8-5](img/fig8-5.png)中客户端1以33的令牌获得租约但随后进入一个长时间的停顿并且租约到期。客户端2以34的令牌该数字总是增加获取租约然后将其写入请求发送到存储服务包括34的令牌。稍后客户端1恢复生机并将其写入存储服务包括其令牌值33.但是存储服务器会记住它已经处理了一个具有更高令牌编号34的写入因此它会拒绝带有令牌33的请求。

417
ch9.md
View File

@ -31,7 +31,7 @@
## 一致性保证
在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单引导程序,多引导程序或无引导程序复制),都会出现这些不一致情况。
在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。
大多数复制的数据库至少提供了**最终一致性**这意味着如果你停止向数据库写入数据并等待一段不确定的时间那么最终所有的读取请求都会返回相同的值【1】。换句话说不一致性是暂时的最终会自行解决假设网络中的任何故障最终都会被修复。最终一致性的一个更好的名字可能是**收敛convergence**因为我们预计所有的复本最终会收敛到相同的值【2】。
@ -150,9 +150,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)”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。
@ -182,7 +182,7 @@
然而一个硬性的唯一性约束关系型数据库中常见的那种需要线性一致性。其他类型的约束如外键或属性约束可以在不需要线性一致性的情况下实现【19】。
#### 跨信道的时间依赖性
#### 跨信道的时序依赖
注意[图9-1](img/fig9-1.png)中的一个细节如果Alice没有惊呼得分Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面并最终看到最后的分数。由于系统中存在额外的信道Alice的声音传到了Bob的耳朵中线性一致性的违背才被注意到。
@ -208,7 +208,7 @@
***单主复制(可能线性一致)***
在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能protential**是线性一致性的[^iv]。然而并不是每个单主数据库都是实际线性一致性的无论是通过设计例如因为使用快照隔离还是并发错误【10】。
在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能protential**是线性一致性的[^iv]。然而并不是每个单主数据库都是实际线性一致性的无论是通过设计例如因为使用快照隔离还是并发错误【10】。
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”)。
@ -250,135 +250,139 @@
### 线性一致性的代价
由于一些复制方法可以提供线性一致性,其他复制方法则不能,因此更深入地探讨线性一致性的优缺点是很有趣的。
一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。
我们已经在第五章讨论了不同复制方法的一些用例。例如,我们看到多领导者复制通常是多数据中心复制的理想选择(参阅“[多数据中心操作](ch5.md#多数据中心操作)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
我们已经在[第五章讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
![](img/fig9-7.png)
**图9-7 网络中断迫使在线性一致性和可用性之间做出选择。**
考虑如果两个数据中心之间发生网络中断,会发生什么情况。我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心不能互相连接。
考虑这样一种情况:如果两个数据中心之间发生网络中断会发生什么?我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心之间彼此无法互相连接。
使用多领导者数据库,每个数据中心都可以继续正常运行:由于从一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,只需排队并交换数据
使用多主数据库,每个数据中心都可以继续正常运行:由于在一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,写入操作只是简单地排队并交换
另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性读取都必须发送给leader因此对于连接到跟随者数据中心的任何客户端这些读取和写入请求必须通过网络同步发送到leader数据中心。
另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性一致的读取请求都必须发送给该主库,因此对于连接到从库所在数据中心的客户端,这些读取和写入请求必须通过网络同步发送到主库所在的数据中心。
如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性一致性读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。
如果客户端可以直接连接到领导者数据中心,这不是问题,因为应用程序在那里继续正常工作。但是只能访问下一个数据中心的客户端将会经历停机,直到网络链接被修复。
在单主配置的条件下,如果数据中心之间的网络被中断,则连接到从库数据中心的客户端无法联系到主库,因此它们无法对数据库执行任何写入,也不能执行任何线性一致的读取。它们仍能从从库读取,但结果可能是陈旧的(非线性一致)。如果应用需要线性一致的读写,却又位于与主库网络中断的数据中心,则网络中断将导致这些应用不可用。
如果客户端可以直接连接到主库所在的数据中心,这就不是问题了,哪些应用可以继续正常工作。但直到网络链接修复之前,只能访问从库数据中心的客户端会中断运行。
#### CAP定理
这个问题不仅仅是单领导者和多领导者复制的结果:任何线性一致性的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v]
这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。问题面临的权衡如下:[^v]
* 如果您的应用程序需要线性一致性,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。
* 如果您的应用程序不需要线性一致性,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不线性一致性
* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用unavailable**)。
* 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的
[^v]: 这两种选择有时分别称为CP在网络分区下一致但不可用和AP在网络分区下可用但不一致。 但是这种分类方案存在一些缺陷【9】所以最好避免
[^v]: 这两种选择有时分别称为CP在网络分区下一致但不可用和AP在网络分区下可用但不一致。 但是这种分类方案存在一些缺陷【9】所以最好不要这样用
因此不需要线性一致性的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】由Eric Brewer于2000年命名尽管自20世纪70年代以来分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。
因此不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】由Eric Brewer于2000年命名尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。
CAP最初是作为一个经验法则提出的没有准确的定义目的是开始讨论数据库的权衡。当时许多分布式数据库侧重于在具有共享存储的机器集群上提供线性一致性的语义【18】并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间这更适合于实施大规模的网络服务【37】。 CAP值得赞扬因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长被称为NoSQL
CAP最初是作为一个经验法则提出的没有准确的定义目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变CAP值得赞扬——它见证了自00年代中期以来新数据库的技术爆炸NoSQL
> ### CAP定理没有帮助
>
> CAP有时以这种面目出现一致性可用性和分区容忍三者只能择其二。
> CAP有时以这种面目出现一致性可用性和分区容忍三者只能择其二。不幸的是这种说法很有误导性【32】因为网络分区是一种错误所以它并不是一个选项不管你喜不喜欢它都会发生【38】。
>
> 只能选择从3中挑选出2个。不幸的是这样做是误导的【32】因为网络分区是一种错误所以它们不是你所拥有的一个选择他们会发生不管你喜欢还是不喜欢【38】。
>
> 在网络正常工作的时候系统可以提供一致性线性一致性和总体可用性。发生网络故障时您必须选择线性或总可用性。因此一个更好的表达CAP的方法可以是一致的或者在分区时可用【39】。一个更可靠的网络需要减少这个选择但是在某些时候选择是不可避免的。
> 在网络正常工作的时候系统可以提供一致性线性一致性和整体可用性。发生网络故障时你必须在线性一致性和整体可用性之间做出选择。因此一个更好的表达CAP的方法可以是一致的或者在分区时可用【39】。一个更可靠的网络需要减少这个选择但是在某些时候选择是不可避免的。
>
> 在CAP的讨论中术语可用性有几个相互矛盾的定义形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”容错系统实际上不符合CAP对可用性的特殊定义。总而言之围绕着CAP有很多误解和困惑并不能帮助我们更好地理解系统所以最好避免使用CAP。
正式定义的CAP定理【30】的范围很窄,它只考虑一个一致性模型(即线性一致性)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此尽管CAP在历史上具有影响力但对于设计系统来说没有实际价值【9,40】。
CAP定理的正式定义仅限于很狭隘的范围【30】它只考虑一个一致性模型(即线性一致性)和一种故障(网络分区[^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。 因此尽管CAP在历史上有一些影响力但对于设计系统而言并没有实际价值【9,40】。
在分布式系统中有更多有趣的不可能的结果【41】并且CAP已经被更精确的结果所取代【2,42】所以它现在基本上是历史感兴趣的
在分布式系统中有更多有趣的“不可能”的结果【41】且CAP定理现在已经被更精确的结果取代【2,42】所以它现在基本上成了历史古迹了
[^vi]: 正如第279页的“实践中的网络故障”中所讨论的本书使用分区来指将大数据集精细分解成小数据集分片;参见第6章。相比之下网络分区是特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于是CAP的P所以在这种情况下我们不能避免混淆
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区partition**指代将大数据集细分为小数据集的操作(分片;参见[第6章](ch6.md))。与之对应的是,**网络分区network partition**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P所以这种情况下不能将其混为一谈
#### 线性一致性和网络延迟
虽然线性一致性是一个有用的保证但实际上很少有系统实际上是线性一致性的。例如现代多核CPU上的RAM甚至不线性一致性【43】如果一个CPU内核上运行的线程写入内存地址而另一个CPU内核上的线程不久后读取相同的地址保证读取第一个线程写入的值除非使用了内存屏障或栅栏【44】
虽然线性一致是一个很有用的保证但实际上线性一致的系统惊人的少。例如现代多核CPU上的内存甚至都不是线性一致的【43】如果一个CPU核上运行的线程写入某个内存地址而另一个CPU核上运行的线程不久之后读取相同的地址并没有保证一定能一定读到第一个线程写入的值除非使用了**内存屏障memory barrier**或**围栏fence**【44】
这种行为的原因是每个CPU核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】所以这个特性对于现代CPU的良好性能是至关重要的。但是现在有几个数据副本一个在主内存中另外几个在不同的高速缓存中而且这些副本是异步更新的因此线性一致性会丢失
这种行为的原因是每个CPU核都有自己的内存缓存和存储缓冲区。默认情况下内存访问首先走缓存任何变更会异步写入主存。因为缓存访问比主存要快得多【45】所以这个特性对于现代CPU的良好性能表现至关重要。但是现在就有几个数据副本一个在主存中也许还有几个在不同缓存中的其他副本而且这些副本是异步更新的所以就失去了线性一致性
为什么要做这个交换使用CAP定理来证明多核内存一致性模型是没有意义的在一台计算机中我们通常假定可靠的通信并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性一致性的原因是性能,而不是容错。
为什么要做这个权衡对多核内存一致性模型而言CAP定理是没有意义的在同一台计算机中我们通常假定通信都是可靠的。并且我们并不指望一个CPU核能在脱离计算机其他部分的条件下继续正常工作。牺牲线性一致性的原因是**性能performance**,而不是容错。
许多分布式数据库也是如此它们选择不提供线性保证它们主要是为了提高性能而不是为了容错【46】。线性一致性速度很慢 - 这一直是事实,不仅在网络故障期间。
许多分布式数据库也是如此:它们是**为了提高性能**而选择了牺牲线性一致性而不是为了容错【46】。线性一致的速度很慢——这始终是事实而不仅仅是网络故障期间。
我们不能找到一个更有效的线性一致性存储实现吗?看来答案是否定的Attiya和Welch 【47】证明如果你想要线性一致性读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”第267页线性读写的响应时间不可避免地会很高。线性一致性算法不存在但是一致性较弱的模型可以更快所以这种权衡对于延迟敏感的系统是很重要的。在第12章中我们将讨论一些避免线性一致性而不牺牲正确性的方法。
能找到一个更高效的线性一致存储实现吗?看起来答案是否定的Attiya和Welch 【47】证明如果你想要线性一致性读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中参见“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在[第12章](ch12.md)中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。
## 顺序保证
## 顺序保证
我们之前曾经说过,线性一致性寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。
之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作在某个时间点似乎都是原子性生效的。这个定义意味着,操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作,以此说明[图9-4](img/fig9-4.png)中的顺序。
订购在本书中一直是反复出现的主题,这表明这可能是一个重要的基本概念。让我们简要回顾一下我们在其中讨论过的其他一些情况
**顺序ordering**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其他**顺序**曾经出现过的上下文
* 在[第5章](ch5.md)中,我们看到领导者在单引导者复制中的主要目的是确定复制日志中的写入顺序——也就是追随者应用这些写入的顺序。如果不存在单个领导,则可能由于并发操作而发生冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
* 我们在[第7章](ch7.md)中讨论的可序列化是关于确保事务按照某种顺序执行的行为。它可以通过以该序列字面执行事务来实现,或者通过允许并行执行,同时防止序列化冲突(通过锁或中止)来实现
* 我们在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如确定两个写入中的哪一个稍后发生。
* 在[第5章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序order of write**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
* 在[第7章](ch7.md)中讨论的**可序列化**,是关于事务表现的像按**某种序列顺序some sequential order**执行的保证。它可以通过字面意义上地**序列顺序serial order**执行事务来实现,或者通过允许并行执行,同时防止序列化冲突来实现(通过锁或中止事务)。
* 在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
事实证明,排序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
事实证明,顺序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
### 顺序与因果
顺序不断涌现有几个原因,其中一个原因是它有助于保持因果关系。在这本书的过程中,我们已经看到了几个例子,其中因果关系是重要的:
**顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系causality**。在本书中我们已经看到了几个例子,其中因果关系是重要的:
* 在“[一致前缀读]()”([图5-5](img/fig5-5.png))中,我们看到一个例子一个对话的观察者首先看到问题的答案,然后回答问题。这是令人困惑的,因为它违背了我们对因果的直觉:如果一个问题得到了回答,那么显然这个问题必须首先在那里,因为给出答案的人必须看到这个问题(假设他们不是精神的,未来)。我们说在问题和答案之间存在因果关系
* [图5-9](img/fig5-9.png)中出现了类似的模式,我们在这里看到三位领导者之间的复制,并注意到由于网络延迟,一些文字可能会“超过”其他文字。从其中一个副本的角度看,好像有一个不存在的行的更新。这里的因果意味着一行必须先被创建,然后才能被更新。
* 在“[检测并发写入](ch5.md#检测并发写入)”中我们观察到如果您有两个操作A和B则有三种可能性A发生在B之前或B发生在A之前或者A和B并发。这是在关系是因果关系的另一种表达之前发生的如果A发生在B之前那么意味着B可能已经知道了A或者建立在A上或者依赖于A.如果A和B是并发的那么他们;换句话说,我们确信没有人知道另一个
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务从一致的快照中读取。但在这方面“一致”是什么意思这意味着与因果关系一致如果快照包含答案它也必须包含被回答的问题【48】。在一个时间点观察整个数据库使其与因果性保持一致在那个时间点之前发生的所有操作的效果都是可见的但是没有发生严重的后续操作。读取偏斜非重复读取如[图7-6](img/fig7-6.png)所示)意味着读取的数据处于违反因果关系的状态
* 我们在事务之间写偏差的例(参见“[写偏差和幻象](ch7.md#写偏差和幻象)”)也说明了因果关系:在[图7-8](img/fig7-8.png)中Alice被允许关闭电话因为交易认为Bob仍在通话反之亦然。在这种情况下去电的动作因果关系取决于观察当前谁在呼叫。[可序列化的快照隔离](ch7.md#可序列化的快照隔离SSI)通过跟踪事务之间的因果依赖关系来检测写入歪斜
* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼结果之后鲍勃从服务器得到一个陈旧结果的事实是一个因果关系的违反爱丽丝的感叹是因果关系依赖于比分所以鲍勃应该也可以在听完艾丽斯后看到比分。在图像大小调整服务的幌子下第331页的“[跨渠道时序依赖关系](#跨渠道时序依赖关系)”中再次出现了相同的模式
* 在“[一致前缀读](ch5.md#一致前缀读)”([图5-5](img/fig5-5.png))中,我们看到一个例子一个对话的观察者首先看到问题的答案,然后才看到被回答问题。这是令人困惑的,因为它违背了我们对**因cause**与**果effect**的直觉:如果一个问题得到了回答,显然这个问题得先在那里,因为给出答案的人必须看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在**因果依赖causal dependency**
* [图5-9](img/fig5-9.png)中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会“压倒”其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。
* 在“[检测并发写入](ch5.md#检测并发写入)”中我们观察到如果有两个操作A和B则存在三种可能性A发生在B之前或B发生在A之前或者A和B**并发**。这种**此前发生happened before**关系是因果关系的另一种表述如果A在B前发生那么意味着B可能已经知道了A或者建立在A的基础上或者依赖于A。如果A和B是**并发**的那么它们之间并没有因果联系换句话说我们确信A和B不知道彼此
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致consistent with causality**如果快照包含答案它也必须包含被回答的问题【48】。在某个时间点观察整个数据库与因果关系保持一致意味着因果上在该时间点之前发生的所有操作其影响都是可见的但因果上在该时间点之后发生的操作其影响对观察者不可见。**读偏差read skew**意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6)所示)
* 事务之间**写偏差write skew**的例(参见“[写偏差和幻象](ch7.md#写偏差和幻象)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可序列化的快照隔离](ch7.md#可序列化的快照隔离SSI)通过跟踪事务之间的因果依赖来检测写偏差
* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现
因果关系对事件施加了一种排序:在收到该消息之前发送消息;问题出现在答案之前。而且,就像现实生活中一样,有一件事情会导致另一件事情:一个节点读取一些数据,然后写出一些结果,另一个节点读取写入的内容并依次写入其他内容,等等。这些依赖因果关系的操作链定义了系统中的因果顺序,即在什么之前发生了什么
因果关系对事件施加了一种顺序:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
如果一个系统服从因果关系所规定的顺序,我们说它是因果关系一致的。例如,快照隔离提供了因果一致性:当您从数据库中读取数据,并且看到一些数据时,您还必须能够看到任何因果关系的数据(假设在此期间还没有被删除)。
如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致causally**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。
#### 因果顺序不是全序关系
#### 因果顺序不是全序
**全序关系total order**允许任何两个元素进行比较,所以如果你有两个元素,你总是可以说哪个更大,哪个更小。例如,自然数是完全有序的:如果我给你两个数字比如说5和13那么你可以告诉我13大于5。
**全序total order**允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数比如说5和13那么你可以告诉我13大于5。
但是,数学集并不完全排序:是`{a, b}`大于`{b, c}`?那么,你不能真正地比较它们,因为它们都不是其中的一个子集。我们说它们是无法比拟的,因此数学集是部分排序的:在某些情况下,一个集大于另一个(如果一个集包含另一个集的所有元素),但在其他情况下它们是无法比拟的
然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较incomparable**的,因此数学集合是**偏序partially order**的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]
全局顺序和局部顺序之间的差异反映在不同的数据库一致性模型中:
[^译注i]: 设R为非空集合A上的关系如果R是自反的、反对称的和可传递的则称R为A上的偏序关系。简称偏序通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集记作$<A,R>$或$<A, >$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。
全序和偏序之间的差异反映在不同的数据库一致性模型中:
***线性一致性***
一个线性一致性的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。
线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序[图9-4](img/fig9-4.png)中以时间线表示。
***因果关系***
***因果***
我们说过,如果两个操作都没有发生在另一个之前那么这两个操作是并发的请参阅第186页上的“[发生之前的关系和并发]()”)。换句话说,如果两个事件是因果关系的(一个发生在另一个事件之前),则它们被排序,但是如果它们是并发的,则它们是无法比拟的。这意味着因果关系定义了一个部分的秩序,而不是一个整体的秩序:一些行动是相互排序的,但有些是无法比拟的。
我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
因此,根据这个定义,在可数据化数据存储中不存在并发操作:必须有一个时间线,所有的操作都是按顺序排列的。可能有几个请求等待处理,但是数据存储确保了每个请求都是在单个时间点自动处理的,并且在单个时间轴上作用于单个数据副本,而没有任何并发性
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发
并发性意味着时间线会再次分支和合并 - 在这种情况下不同分支上的操作是无法比拟的即并发。在第五章中我们看到了这种现象例如图5-14不是一条直线的总体顺序而是一堆不同的操作同时进行。图中的箭头表示因果关系 - 操作的部分顺序。
并发意味着时间线会分岔然后合并——在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md)并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖——操作的偏序。
如果您熟悉像Git这样的分布式版本控制系统那么它们的版本历史非常类似于因果关系图。通常一个提交会以一条直线进行但是有时您会得到分支当多个人同时在一个项目上工作时并且在这些创建的提交合并时创建合并
如果你熟悉像Git这样的分布式版本控制系统那么其版本历史与因果关系图极其相似。通常一个**提交Commit**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并Merge**会在这些并发创建的提交相融合时创建
#### 线性一致性强于因果一致性
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性意味着因果关系:任何线性一致性的系统都能正确保存注意力【7】。特别是如果系统中有多个通信通道如图9-5中的消息队列和文件存储服务线性可确保因果关系被自动保留,而系统不必做任何特殊的事情(如通过不同部件之间的时间戳)。
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着implies**因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是如果系统中有多个通信通道[图9-5](img/fig9-5.png)中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。
线性一致性确保因果关系的事实使线性一致性系统变得简单易懂更具吸引力。然而正如第335页的“线性可用性的成本”中所讨论的使系统线性一致性可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,这使得它们可以获得更好的性能,但却使它们难以工作
线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来却更困难
好消息是中间地带是可能的。线性一致性不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的不会造成使其线性一致性的性能命中特别是CAP定理不适用。事实上因果一致性是最强可能的一致性模型不会由于网络延迟而减慢并且在网络故障时仍然可用【2,42】。
好消息是折衷是可能的。线性一致性并不是保持因果性的唯一途径——还有其他方法。一个系统可以是因果一致的而无需承担线性一致带来的性能折损尤其是CAP定理不适用的情况。实际上在所有的不会被网络延迟拖慢的一致性模型中因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。
在许多情况下似乎需要线性一致性的系统实际上只需要确定因果一致性这可以更有效地实施。基于这种观察研究人员正在探索新的数据库来保存因果关系其性能和可用性特征与最终一致的系统类似【49,50,51】。
由于这项研究是相当新的其中没有很多已经进入生产系统仍然有挑战需要克服【52,53】。但是这是未来系统的一个有利的方向。
在许多情况下似乎需要线性一致性的系统实际上只需要因果一致性因果一致性可以更高效地实现。基于这种观察研究人员正在探索新型的数据库既能保证因果一致性且性能与可用性与最终一致系统类似【49,50,51】。
这方面的研究相当新鲜其中很多尚未应用到生产系统仍然有不少挑战需要克服【52,53】。但对于未来的系统而言这是一个有前景的方向。
#### 捕获因果关系
我们不会详细讨论非线性系统如何在这里维持因果一致性,而只是简要地探讨一些关键的思想。为了保持因果关系,您需要知道哪个操作发生在哪个其他操作之前。这是一个部分命令:并发操作可以按任意顺序进行,但是如果一个操作发生在另一个操作之前,那么它们必须按照每个副本的顺序处理。因此,当一个副本处理一个操作时,它必须确保所有因果先前的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。
我们不会在这里讨论非线性系统如何保证因果一致性的细节,而只是简要地探讨一些关键的思想。
为了保持因果性,您需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。
为了确定因果依赖关系我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y时已经看到X值则X和Y可能是因果关系的。这个分析使用了你在欺诈指控的刑事调查中所期望的那些问题CEO在做出决定时是否知道X
在其他操作之前确定哪些操作发生的技术与我们在第181页中的“检测并发写入”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步它需要跟踪整个数据库的因果关系而不仅仅是一个关键。版本向量可以被推广到做这个【54】。
在其他操作之前确定哪些操作发生的技术与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步它需要跟踪整个数据库的因果关系而不仅仅是一个关键。版本向量可以被推广到做这个【54】。
为了确定因果顺序数据库需要知道应用程序读取哪个版本的数据。这就是为什么在图5-13中来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法如“[可序列化的快照隔离SSI]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。
@ -396,14 +400,14 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
[^vii]: 与因果关系不一致的整个顺序很容易创建但不是很有用。例如您可以为每个操作生成随机UUID并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序但是随机的UUID并不告诉你哪个操作首先实际发生或者操作是否是并发的。
具有单引导程序复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。
单主复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。
#### 非因果序列号发生器
如果没有一个领导者(可能是因为您使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法:
* 每个节点都可以生成自己独立的一组序列号。例如,如果有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中保留一些位以包含唯一的节点标识符,这将确保两个不同的节点永远不会生成相同的序列号。
* 您可以将时间戳从时钟物理时钟附加到每个操作【55】。这样的时间戳是不连续的但是如果它们具有足够高的分辨率那么它们可能足以完成命令操作。这个事实用于最后的写赢取冲突解决方法请参阅第291页的“订购事件的时间戳”)。
* 您可以将时间戳从时钟物理时钟附加到每个操作【55】。这样的时间戳是不连续的但是如果它们具有足够高的分辨率那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
* 您可以预先分配序列号的块。例如节点A可能要求从1到1,000的序列号的块并且节点B可能要求该区块从1,001到2,000。然后每个节点可以独立分配其块的序列号并在序列号的提供开始变低时分配一个新的块。
这三个选项都比单独的领导者增加一个计数器的表现更好,并且更具可扩展性。它们为每个操作生成一个唯一的,大约增加的序列号。然而,他们都有一个问题:他们产生的序列号与因果关系不一致。
@ -412,41 +416,42 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
* 每个节点可以每秒处理不同数量的操作。因此,如果一个节点产生偶数而另一个产生奇数,则偶数的计数器可能落后于奇数的计数器,反之亦然。如果你有一个奇数的操作和一个偶数的操作,你不能准确地说出哪一个因果关系发生了。
* 来自物理时钟的时间戳会受到时钟偏移的影响这可能会使其与因果性不一致。例如见图8-3其中显示了一个情况其中后来发生因果关系的操作实际上被分配了较低的时间戳。[^vii]
* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果性不一致。例如,见[图8-3](img/fig8-3.png),其中显示了一个情况,其中后来发生因果关系的操作实际上被分配了较低的时间戳。[^vii]
[^viii]: 可以使物理时钟时间戳与因果关系一致在第294页的“用于全局快照的同步时钟”中我们讨论了Google的Spanner它可以估计预期的时钟偏差并在提交写入之前等待不确定性间隔。 这个方法确保了一个事实上的后续交易得到了更大的时间戳。 但是,大多数时钟不能提供所需的不确定性度量。
[^viii]: 可以使物理时钟时间戳与因果关系一致在第294页的“用于全局快照的同步时钟”中我们讨论了Google的Spanner它可以估计预期的时钟偏差并在提交写入之前等待不确定性间隔。 这个方法确保了一个事实上的后续事务得到了更大的时间戳。 但是,大多数时钟不能提供所需的不确定性度量。
* 在块分配器的情况下一个操作可能会被赋予一个范围从1,001到2,000的序列号而一个因果较晚的操作可能被赋予一个范围从1到1,000的数字。在这里序列号与因果关系也是不一致的。
#### Lamport时间戳
#### 兰伯特时间戳
尽管刚才描述的三个序列号发生器与因果关系不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为Lamport时间戳莱斯利·兰波Leslie Lamport于1978年提出【56】现在是分布式系统领域中被引用最多的论文之一。
尽管刚才描述的三个序列号发生器与因果关系不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯Leslie Lamport于1978年提出【56】现在是分布式系统领域中被引用最多的论文之一。
[图9-8](img/fig9-8.png)说明了Lamport时间戳的使用。每个节点都有一个唯一的标识符,每个节点都保存一个处理操作数量的计数器。 Lamport时间戳然后是一对计数器节点ID。二节点有时可能具有相同的计数器值但通过在时间戳中包含节点ID每个时间戳都是唯一的。
[图9-8](img/fig9-8.png)说明了兰伯特时间戳的使用。每个节点都有一个唯一的标识符,每个节点都保存一个处理操作数量的计数器。 兰伯特时间戳然后是一对计数器节点ID。二节点有时可能具有相同的计数器值但通过在时间戳中包含节点ID每个时间戳都是唯一的。
![](img/fig9-8.png)
**图9-8 Lamport时间戳提供了与因果关系一致的总排序。**
Lamport时间戳与物理时间时钟没有任何关系但是它提供了总计次数如果您有两个时间戳则计数器值较大的时间戳是较大的时间戳。如果计数器值相同则节点ID越大的时间戳越大。
兰伯特时间戳与物理时间时钟没有任何关系但是它提供了总计次数如果您有两个时间戳则计数器值较大的时间戳是较大的时间戳。如果计数器值相同则节点ID越大的时间戳越大。
到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于Lamport时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。
到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于兰伯特时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。
这如图9-8所示其中客户端A从节点2接收计数器值5然后将最大值5发送到节点1.此时节点1的计数器仅为1但是它立即向前移动到5所以下一个操作的计数器值增加了6。
只要最大计数器值与每一个操作一起进行,这个方案确保Lamport时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。
只要最大计数器值与每一个操作一起进行,这个方案确保兰伯特时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。
Lamport时间戳有时会与版本向量混淆我们在第184页上的“检测并发写入”中看到了这些向量戳。虽然存在一些相似之处但它们具有不同的目的版本向量可以区分两个操作是并发还是因果依赖另一个Lamport时间戳总是执行一个总的顺序。从Lamport的全部订购时间戳你不能分辨两个操作是并行还是因果关系。 Lamport时间戳优于版本向量的优点是它们更紧凑。
兰伯特时间戳有时会与版本向量混淆我们在第184页上的“检测并发写入”中看到了这些向量戳。虽然存在一些相似之处但它们具有不同的目的版本向量可以区分两个操作是并发还是因果依赖另一个兰伯特时间戳总是执行一个总的顺序。从兰伯特的全部顺序时间戳,你不能分辨两个操作是并行还是因果关系。 兰伯特时间戳优于版本向量的优点是它们更紧凑。
#### 光有时间戳排序还不够
虽然Lamport时间戳定义了与因果关系一致的操作总顺序但它们还不足以解决分布式系统中的许多常见问题。
例如,考虑一个需要确保用户名唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 我们之前在第301页的“[领导和锁定](#领导和锁定)”中提到过这个问题。)
虽然兰伯特时间戳定义了与因果关系一致的操作总顺序,但它们还不足以解决分布式系统中的许多常见问题。
乍看之下似乎总的操作顺序例如使用Lamport时间戳应该足以解决此问题如果创建了两个具有相同用户名的帐户请选择时间戳较低的那个作为获胜者一个谁先抓住用户名并让更大的时间戳失败。由于时间戳是完全有序的所以这个比较总是有效的。
例如,考虑一个需要确保用户名唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导和锁定](#领导和锁定)”中提到过这个问题。)
乍看之下,似乎总的操作顺序(例如,使用兰伯特时间戳)应该足以解决此问题:如果创建了两个具有相同用户名的帐户,请选择时间戳较低的那个作为获胜者(一个谁先抓住用户名),并让更大的时间戳失败。由于时间戳是完全有序的,所以这个比较总是有效的。
这种方法适用于事后确定胜利者:一旦收集了系统中的所有用户名创建操作,就可以比较他们的时间戳。然而,当一个节点刚刚收到用户的一个请求来创建一个用户名,并且现在需要决定这个请求是成功还是失败,这是不够的。此时,节点不知道另一个节点是否正在同时创建具有相同用户名的帐户,以及其他节点可以分配给该操作的时间戳。
@ -456,13 +461,13 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,您还需要知道该命令何时完成。如果您有创建用户名的操作,并且您确定没有其他节点可以在您的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。
这个知道什么时候你的总顺序被完成的概念被记录在总顺序广播的话题中。
这个知道什么时候你的总顺序被完成的概念被记录在序广播的话题中。
### 全广播
### 全序广播
如果你的程序只运行在一个CPU内核上那么定义一个操作总的顺序是很容易的它只是CPU执行的顺序。但是在分布式系统中让所有节点在相同的操作顺序上达成一致是非常棘手的。在最后一节中我们讨论了按时间戳或序列号进行排序但发现它不如单主复制如果使用时间戳排序来实现唯一性约束则不能容忍任何错误
如前所述单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统以及如果领导者失败参见第156页的“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。
如前所述单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统以及如果领导者失败“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。
[^ix]: “原子广播”这个术语是传统的但是它是非常混乱的因为它与原子的其他用法不一致它与ACID事务中的原子性没有任何关系只是与原子操作在多线程编程的意义上 )或原子寄存器(线性一致性存储)。 总的顺序组播是另一个同义词。
@ -470,7 +475,7 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
>
> 每个分区有一个单独的引导程序的分区数据库通常只对每个分区进行排序,这意味着它们不能提供跨分区的一致性保证(例如,一致的快照,外键引用)。 所有分区的总排序是可能的但需要额外的协调【59】。
总顺序广播通常被描述为在节点之间交换消息的协议。 非正式地,它要求总是满足两个安全属性:
序广播通常被描述为在节点之间交换消息的协议。 非正式地,它要求总是满足两个安全属性:
***可靠交付reliable delivery***
@ -480,34 +485,34 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
消息以相同的顺序传递给每个节点。
总顺序广播的正确算法必须确保始终满足可靠性和订购属性,即使节点或网络出现故障。当然,在网络中断的时候,消息不会被传送,但是一个算法可以继续重试,以便在网络被最终修复的时候消息能够通过(然后它们仍然必须按照正确的顺序传送)。
序广播的正确算法必须确保始终满足可靠性和订购属性,即使节点或网络出现故障。当然,在网络中断的时候,消息不会被传送,但是一个算法可以继续重试,以便在网络被最终修复的时候消息能够通过(然后它们仍然必须按照正确的顺序传送)。
#### 使用全序广播
像ZooKeeper和etcd这样的共识服务实际上是实现全面的顺序播放。这个事实暗示了整个命令广播和共识之间有着密切的联系我们将在本章后面进行探讨。
总顺序广播正是您所需的数据库复制如果每封邮件都表示写入数据库并且每个副本按相同的顺序处理相同的写入则副本将保持一致除了临时复制滞后。这个原则被称为状态机复制【60】我们将在第11章中回到它。
类似地,可以使用总顺序广播来实现可序列化的事务如第242页上的“实际的串行执行”中所述如果每个消息表示一个确定性事务作为存储过程来执行并且每个节点都处理这些消息相同的顺序那么数据库的分区和副本保持一致【61】。
序广播正是您所需的数据库复制如果每封邮件都表示写入数据库并且每个副本按相同的顺序处理相同的写入则副本将保持一致除了临时复制滞后。这个原则被称为状态机复制【60】我们将在[第11章](ch11.md)中回到它。
类似地,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述如果每个消息表示一个确定性事务作为存储过程来执行并且每个节点都处理这些消息相同的顺序那么数据库的分区和副本保持一致【61】。
总顺序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全部命令广播比时间戳命令更强。
序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全广播比时间戳命令更强。
查看总顺序广播的另一种方式是创建日志(如在复制日志,事务日志或预写日志中):传递消息就像附加到日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志并看到相同的消息序列。
查看序广播的另一种方式是创建日志(如在复制日志,事务日志或预写日志中):传递消息就像附加到日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志并看到相同的消息序列。
全面订购广播对于实施提供防护令牌的锁定服务也很有用(参见第294页的“防护令牌”。每个获取锁的请求都作为消息添加到日志中并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记因为它是单调递增的。在ZooKeeper中这个序列号被称为`zxid` 【15】。
全面订购广播对于实施提供防护令牌的锁定服务也很有用(参见“[防护令牌](ch8.md#防护令牌)。每个获取锁的请求都作为消息添加到日志中并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记因为它是单调递增的。在ZooKeeper中这个序列号被称为`zxid` 【15】。
#### 使用全序广播实现线性一致性的存储
如图9-4所示在线性一致性的系统中有一个操作的总顺序。这是否意味着线性一致性与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。
[图9-4](img/fig9-4.png)所示,在线性一致性的系统中,有一个操作的全序。这是否意味着线性一致性与全序播放相同?不完全,但两者之间有密切的联系[^x]。
[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】在异步崩溃停止模型【68】中没有确定性的解决方案而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而支持原子操作如比较和设置或者在寄存器中增加和获取使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。
[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 序广播等同于共识【67】在异步崩溃停止模型【68】中没有确定性的解决方案而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而支持原子操作如比较和设置或者在寄存器中增加和获取使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。
但是,如果您有全面的顺序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。
但是,如果您有全序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。
您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作
您可以通过使用全广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作
1. 在日志中添加一条消息,暂时指明您要声明的用户名。
2. 阅读日志,并等待你附加的信息被传回给你。[^xi]
@ -517,27 +522,27 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
由于日志条目以相同顺序传递到所有节点因此如果有多个并发写入则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者并中止后面的写入确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 具体来说这里描述的过程提供了顺序一致性【47,64】有时也称为时间线一致性【65,66】这比线性一致性要弱一些。为了使读取线性一致,有几个选项:
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 具体来说这里描述的过程提供了顺序一致性【47,64】有时也称为时间线一致性【65,66】这比线性一致性要弱一些。为了使读取线性一致有几个选项
* 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 法定读取etcd的工作有点像这样【16】。
* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 这是Zookeeper的`sync()`操作背后的思想【15】
* 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。
#### 使用线性一致性存储实现总顺序广播
#### 使用线性一致性存储实现序广播
最后一节介绍了如何从全部命令广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。
最后一节介绍了如何从全广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。
最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数并且有一个原子增量和获取操作【28】。或者原子比较和设置操作也可以完成这项工作。
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
请注意,与Lamport时间戳不同您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此如果一个节点已经发送了消息4并且接收到序列号为6的传入消息则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
请注意,与兰伯特时间戳不同您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此如果一个节点已经发送了消息4并且接收到序列号为6的传入消息则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
Lamport时间戳 - 事实上,这是总顺序广播和时间戳订购之间的关键区别。
兰伯特时间戳——事实上,这是全序广播和时间戳顺序之间的关键区别。
使用原子增量和获取操作来创建线性一致性整数有多困难像往常一样如果事情从来没有失败过那很容易你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况并在该节点失败时恢复该值【59】。一般来说如果你对线性一致性序列号的产生者认真思考你不可避免地会得出一个一致的算法。
这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全部命令广播都相当于【2867】。也就是说如果你能解决其中的一个问题你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力
这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全序广播都相当于【28,67】。也就是说如果你能解决其中的一个问题你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力
现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。
@ -546,64 +551,69 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
## 分布式事务与共识
共识是分布式计算中最重要也是最基本的问题之一。从表面上看,似乎很简单:非正式地说,目标只是让几个节点达成一致。你可能会认为这不应该太难。不幸的是,许多破损的系统已经被误认为这个问题很容易解决。
虽然共识是非常重要的但关于它的部分在本书的后半部分已经出现了因为这个主题非常微妙欣赏细微之处需要一些必要的知识。即使在学术研究界对共识的理解也只是在几十年的时间内逐渐显现出来一路上有许多误解。现在我们已经讨论了复制第5章事务第7章系统模型第8章线性一致性以及总播放本章我们终于准备好解决共识问题了。
虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致性以及全序([本章](ch9.md)),我们终于准备好解决共识问题了。
在节点达成一致的情况下,有许多情况是很重要的。例如:
***领导选举***
在具有单引导程序复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会引起争议。在这种情况下,一致性对于避免错误的故障切换非常重要,从而导致两个节点都认为自己是领导者的分裂大脑情况请参阅第156页的“处理节点中断”。如果有两个领导者们都会接受写入,他们的数据会发生分歧,导致不一致和数据丢失。
在具有单引导程序复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会引起争议。在这种情况下,一致性对于避免错误的故障切换非常重要,从而导致两个节点都认为自己是领导者的脑裂情况(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,他们的数据会发生分歧,导致不一致和数据丢失。
***原子提交***
在支持跨越多个节点或分区的事务的数据库中有一个事务可能在某些节点上失败但在其他节点上成功。如果我们想要维护事务的原子性就ACID而言请参阅第223页的“原子性”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误)或者他们都承诺(如果没有出错)。这个共识的例子被称为原子提交问题[^xii]。
在支持跨越多个节点或分区的事务的数据库中有一个事务可能在某些节点上失败但在其他节点上成功。如果我们想要维护事务的原子性就ACID而言请参“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误)或者他们都承诺(如果没有出错)。这个共识的例子被称为原子提交问题[^xii]。
[^]: 原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 允许共识决定其中一位参与者提出的任何价值。 然而原子的承诺和共识是可以相互压缩的【70,71】。 非阻塞原子提交比共识更难 - 请参阅第359页上的“三阶段提交”。
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 允许共识决定其中一位参与者提出的任何值。 然而原子的承诺和共识是可以相互压缩的【70,71】。 非阻塞原子提交比共识更难——参阅“[三阶段提交](#三阶段提交)”。
> ### 共识的不可能性
>
> 您可能已经听说过作者FischerLynch和Paterson之后的FLP结果【68】这证明如果存在节点可能崩溃的风险则不存在总是能够达成一致的算法。在分布式系统中我们必须假设节点可能会崩溃所以可靠的共识是不可能的。然而在这里我们正在讨论达成共识的算法。这里发生了什么
>
> 答案是FLP结果在异步系统模型中得到了证明请参阅“系统模型与现实”在本部分这是一个非常有限的模型它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点即使怀疑有时是错误的那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。
>
> 因此FLP虽然不可能达成共识但理论上具有重要意义但实际上分布式系统通常可以达成共识。
在本节中我们将首先更详细地检查原子提交问题。具体来说我们将讨论两阶段提交2PC算法这是解决原子提交最常见的方法并在各种数据库消息传递系统和应用服务器中实现。事实证明2PC是一种一致的算法但不是一个很好的【70,71】。
在本节中我们将首先更详细地检查原子提交问题。具体来说我们将讨论两阶段提交2PC算法这是解决原子提交最常见的方法并在各种数据库消息传递系统和应用服务器中实现。事实证明2PC是一种一致的算法不是一个很好的算法【70,71】。
通过2PC学习我们将继续努力实现更好的一致性算法比如ZooKeeperZab和etcdRaft中使用的算法。
通过2PC学习我们将继续努力实现更好的一致性算法比如ZooKeeperZab和etcdRaft中使用的算法。
### 原子提交与二阶段提交2PC
在第7章中我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交在这种情况下所有事务的写入都是持久的或者中止在这种情况下所有事务的写入都被回滚即撤消或丢弃
[第7章](ch7.md)中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。
原子性可以防止失败的事务乱数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“单对象和多对象操作”一节第228页)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构 - 因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会非常有用)。
原子性可以防止失败的事务乱数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会有用)。
#### 从单节点到分布式原子提交
对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预先写好的日志中;请参阅第82页的“使B树可靠”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。
对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。
因此在单个节点上事务承诺主要取决于数据持久写入磁盘的顺序首先是数据然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻在此之前仍有可能中止由于崩溃但在此之后事务提交 - 特德(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使提交原子。
因此在单个节点上事务承诺主要取决于数据持久写入磁盘的顺序首先是数据然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻在此之前仍有可能中止由于崩溃但在此之后事务已经提交(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使提交具有原子
但是,如果一个事务中涉及多个节点呢?例如,也许在分区数据库中有一个多对象事务,或者是一个由术语分区的二级索引(其中索引条目可能位于与主数据不同的节点上;请参阅“分区和二级索引”第206页。大多数“NoSQL”分布式数据存储不支持这种分布式事务而是各种集群关系系统请参见“实践中的分布式事务”
但是,如果一个事务中涉及多个节点呢?例如,也许在分区数据库中有一个多对象事务,或者是一个由关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区和二级索引](ch6.md#分区和二级索引)”。大多数“NoSQL”分布式数据存储不支持这种分布式事务而是各种集群关系系统请参见“[实践中的分布式事务](#实践中的分布式事务)”)。
在这些情况下,仅向所有节点发送提交请求并且独立提交每个节点的事务是不够的。这样做很容易发生,提交在一些节点上成功,在其他节点上失败,这违反了原子性保证:
在这些情况下,仅向所有节点发送提交请求并且独立提交每个节点的事务是不够的。这样做很容易发生:提交在某些节点上成功,在其他节点上失败,这违反了原子性保证:
* 某些节点可能会检测到约束冲突或冲突,因此需要中止,而其他节点则可以成功进行提交。
* 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。
* 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。
如果某些节点提交了事务但其他节点却放弃了这些事务那么这些节点就会彼此不一致如图7-3所示。而且一旦在一个节点上提交了一个事务如果事后证明它在另一个节点上被中止它将不能被收回。出于这个原因一旦确定事务中的所有其他节点也将提交节点就必须进行提交。
如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被收回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。
事务提交必须是不可撤销的 - 您不得改变主意,并在交易提交后追溯中止交易。这个规则的原因是,一旦数据被提交,其他交易就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“读取提交”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。
事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。
承诺交易的效果有可能在后来被另一个补偿交易取消【73,74】但从数据库的角度来看这是一个单独的交易因此任何交叉交易的正确性要求是应用程序的问题。)
提交事务的效果有可能在后来被另一个补偿事务取消【73,74】但从数据库的角度来看这是一个单独的事务因此任何交叉事务的正确性要求是应用程序的问题。)
#### 介绍两阶段提交
两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保这一点
两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用并且还以XA事务【76,77】例如Java Transaction API支持或通过`WS-AtomicTransaction for SOAP Web`服务的形式提供给应用程序【78,79】。
![](img/fig9-8.png)
[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样2PC中的提交/终止进程分为两个阶段(因此名称),而不是单个提交请求。
![](img/fig9-9.png)
**图9-9 两阶段提交2PC的成功执行**
@ -611,130 +621,126 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
>
> 两阶段提交2PC和两阶段锁定请参阅第257页上的“两阶段锁定2PL是两个完全不同的事情。 2PC在分布式数据库中提供原子提交而2PL提供可序列化的隔离。为了避免混淆最好把它们看作完全独立的概念并忽略名称中的不幸的相似性。
2PC使用一个通常不会出现在单节点事务中的新组件协调器也称为事务管理器。协调器通常在请求事务的相同应用程序进程例如嵌入在Java EE容器中中实现为库但也可以是单独的进程或服务。这种协调的例子包括NarayanaJOTMBTM或MSDTC。
2PC使用一个通常不会出现在单节点事务中的新组件**协调器coordinator**也称为事务管理器。协调器通常在请求事务的相同应用程序进程例如嵌入在Java EE容器中中实现为库但也可以是单独的进程或服务。这种协调的例子包括NarayanaJOTMBTM或MSDTC。
正常情况下2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为交易参与者。当应用程序准备提交时协调器开始阶段1它发送一个准备请求到每个节点询问他们是否能够提交。协调然后跟踪参与者的回应:
正常情况下2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时协调器开始阶段1它发送一个准备请求到每个节点询问他们是否能够提交。协调然后跟踪参与者的回应:
* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调员在阶段2发出提交请求,实际发生提交。
* 如果任何参与者回复“否”,则协调员在阶段2中向所有节点发送放弃请求。
* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调者在阶段2发出**提交commit**请求,实际发生提交。
* 如果任何参与者回复“否”,则协调者在阶段2中向所有节点发送**中止abort**请求。
这个过程有点像西方传统婚姻仪式:部长要求新娘和新郎分别是否要结婚,而且通常都是从两个方面接受“我做”的答案。收到两者都恢复后,因为参与者投票“是”,它不能拒绝提交时恢复
这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚通常是从两方都收到“我愿意”的答复。收到两者的回复后司仪宣布这对情侣成为夫妻事务就提交了这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复”我愿意“婚礼就会中止【73】
因此该协议包含两个关键的“不归路”点当参与者投票“是”时它承诺它肯定能够稍后提交尽管协调员可能仍然选择放弃。一旦协调员决定这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。)
回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“放弃”这个交易的自由说“不行或者这个。然而在说“我这样做”之后你不能收回那个声明。如果你说“我这样做”后你晕了而你没有听到部长说“你现在是夫妻”那不会改变交易的事实。当你稍后恢复意识时你可以通过查询部长的全球交易ID状态来查明你是否已婚或者你可以等待部长下一次提交请求的重试因为重试将一直持续下去你的无意识的时期
#### 承诺体系
#### 承诺系统
从这个简短的描述可能不清楚为什么两阶段提交确保了原子性,而跨几个节点的一阶段提交没有。准备和提交请求当然可以在两阶段的情况下轻易地丢失。 2PC有什么不同
为了理解它的工作原理,我们必须更详细地分解这个过程:
1. 当应用程序想要开始一个分布式事务时它向协调器请求一个事务ID。此交易ID是全球唯一的。
2. 应用程序在每个参与者上开始单节点事务并将全局唯一事务ID附到单节点事务。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。
3. 当应用程序准备提交时协调器向所有参与者发送一个准备请求标记为全局事务ID。如果这些请求中的任何一个失败或超时则协调器向所有参与者发送针对该交易ID的放弃请求。
4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行交易。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交交易。换句话说,参与者放弃了放弃交易的权利,但没有实际承诺
5. 当协调员收到所有准备请求的答复时,就是否提交或中止交易作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调员必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为提交点。
6. 一旦协调的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间坠毁,交易将在恢复时进行 - 由于参与者投票“是”,因此恢复时不能拒绝提交。
1. 当应用想要开启一个分布式事务时它向协调者请求一个事务ID。此事务ID是全局唯一的。
2. 应用在每个参与者上开始单节点事务并将全局唯一事务ID附到单节点事务。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。
3. 当应用程序准备提交时协调器向所有参与者发送一个准备请求标记为全局事务ID。如果这些请求中的任何一个失败或超时则协调器向所有参与者发送针对该事务ID的放弃请求。
4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行事务。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交事务。换句话说,参与者放弃了中止事务的权利,但没有实际提交
5. 当协调者收到所有准备请求的答复时,就是否提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调者必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为**提交点commit point**
6. 一旦协调的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间崩溃,事务将在恢复时进行——由于参与者投票“是”,因此恢复时不能拒绝提交。
因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调员可能仍然选择放弃)。一旦协调员决定这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。)
因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调者可能仍然选择放弃)。一旦协调者决定这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。)
回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“放弃”这个交易的自由说“不行或者这个。然而在说“我这样做”之后你不能收回那个声明。如果你说“我这样做”后你晕了而你没有听到部长说“你现在是夫妻”那不会改变交易的事实。当你稍后恢复意识时你可以通过查询部长的全球交易ID状态来查明你是否已婚或者你可以等待部长下一次提交请求的重试因为重试将一直持续下去你的无意识的时期)。
回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“中止”这个事务的自由通过回复“不行或者差不多效果的话。然而在说“我愿意”之后你就不能收回那个声明了。如果你说“我愿意”后晕倒了而你没有听到司仪说“你们现在是夫妻了”那并不会改变事务已经提交的事实。当你稍后恢复意识时你可以通过查询司仪的全局事务ID状态来查明你是否已婚或者你可以等待司仪重试下一次提交请求因为重试将在你无意识期间一直持续)。
#### 协调者失效
我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况如果任何一个准备请求失败或者超时协调员就中止交易。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调员崩溃,会发生什么情况不太清楚。
我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况如果任何一个准备请求失败或者超时协调者就中止事务。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。
如果协调员在发送准备请求之前失败,参与者可以安全地中止交易。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 - 必须等待协调者回答交易是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的交易被怀疑或不确定。
如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务被怀疑或不确定。
情况如[图9-10](img/fig9-10)所示。在这个特定的例子中协调器实际上决定提交数据库2收到提交请求。但是协调器在将提交请求发送到数据库1之前发生崩溃因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助如果数据库1在超时后单方面中止它将最终与提交的数据库2不一致。同样单方面犯也是不安全的因为另一个参与者可能已经中止了。
![](img/fig9-10.png)
 **图9-10 参与者投赞成票后,协调崩溃。数据库1不知道是否提交或中止**
 **图9-10 参与者投赞成票后,协调崩溃。数据库1不知道是否提交或中止**
没有协调的消息参与者无法知道是否承诺或放弃。原则上参与者可以相互沟通找出每个参与者如何投票并达成一致但这不是2PC协议的一部分。
没有协调的消息参与者无法知道是否承诺或放弃。原则上参与者可以相互沟通找出每个参与者如何投票并达成一致但这不是2PC协议的一部分。
2PC可以完成的唯一方法是等待协调员恢复。这就是为什么协调员必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志协调器恢复后通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此2PC的提交点归结为协调器上的常规单节点原子提交。
2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志协调器恢复后通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此2PC的提交点归结为协调器上的常规单节点原子提交。
#### 三阶段提交
两阶段提交被称为阻塞原子提交协议因为2PC可能卡住等待协调器恢复。理论上可以使一个原子提交协议非阻塞以便在节点失败时不会卡住。但是在实践中做这个工作并不那么简单。
作为2PC的替代方案已经提出了一种称为三阶段提交3PC的算法【13,80】。然而3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中见第8章它不能保证原子性。
作为2PC的替代方案已经提出了一种称为三阶段提交3PC的算法【13,80】。然而3PC假定一个有界延迟的网络和有限响应时间的节点在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它不能保证原子性。
通常非阻塞原子提交需要一个完美的故障检测器【67,71】 - 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中超时不是可靠的故障检测器因为即使没有节点崩溃请求也可能由于网络问题而超时。出于这个原因2PC继续被使用,尽管协调器故障的已知问题。
通常非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中超时不是可靠的故障检测器因为即使没有节点崩溃请求也可能由于网络问题而超时。出于这个原因2PC仍然被使用,尽管大家都清楚可能有协调器故障的问题。
### 实践中的分布式事务
分布式事务,尤其是那些通过两阶段提交实现的事务,声誉混杂。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运营问题造成业绩下滑承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其产生的操作问题而选择不执行分布式事务【85,86】。
分布式事务,尤其是那些通过两阶段提交实现的事务,毁誉参半。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运维问题造成性能下降承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其导致的运维问题而选择不实现分布式事务【85,86】。
分布式事务的某些实现会带来严重的性能损失 - 例如MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外磁盘强制fsync【88】以及额外的网络往返。
分布式事务的某些实现会带来严重的性能损失——例如MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘`fsync`【88】以及额外的网络往返。
但是,我们不应该直接抛弃分布式交易,而应该更加详细地审视这些交易,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“分布式交易”的含义。两种截然不同的分布式交易类型经常被混淆:
但是,我们不应该直接抛弃分布式事务,而应该更加详细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆:
***数据库内部的分布式事务***
一些分布式数据库即在其标准配置中使用复制和分区的数据库支持该数据库节点之间的内部事务。例如VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下所有参与交易的节点都运行相同的数据库软件。
一些分布式数据库即在其标准配置中使用复制和分区的数据库支持该数据库节点之间的内部事务。例如VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下所有参与事务的节点都运行相同的数据库软件。
***异构分布式事务***
在异构交易中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
在异构事务中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的交易则更具挑战性。
数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的事务则更具挑战性。
#### 恰好一次的消息处理
异构的分布式事务处理能够以强大的方式集成不同的系统。例如,当且仅当用于处理消息的数据库事务处理时,来自消息队列的消息才能被确认为已处理成功承诺。这是通过自动提交消息确认和数据库写入单个事务来实现的。使用分布式事务支持,即使消息代理和数据库是在不同机器上运行的两个不相关技术,也是可能的。
如果消息传递或数据库事务失败,两者都会中止,因此消息代理可能会稍后安全地重新传递消息。因此,通过自动提交消息及其处理的副作用,即使在成功之前需要几次重试,也可以确保消息被有效处理一次。中止放弃部分完成的交易的任何副作用。
如果消息传递或数据库事务失败,两者都会中止,因此消息代理可能会稍后安全地重新传递消息。因此,通过自动提交消息及其处理的副作用,即使在成功之前需要几次重试,也可以确保消息被有效处理一次。中止放弃部分完成的事务的任何副作用。
这样的分布式事务只有在所有受事务影响的系统都能够使用相同的原子提交协议的情况下才是可能的。例如,处理消息的副作用是发送邮件,而邮件服务器不支持两阶段提交:如果邮件处理失败并重试,可能会发送两次或更多次的邮件。但是,如果处理消息的所有副作用在事务中止时回滚,那么可以安全地重新尝试处理步骤,就好像什么都没发生过一样。
我们将回到第11章中的一次消息处理的主题。让我们首先看看允许这种异构分布式事务的原子提交协议。
我们将在[第11章](ch11.md)中再次回到”恰好一次“消息处理的主题。让我们首先看看允许这种异构分布式事务的原子提交协议。
#### XA事务
X/Open XA**扩展架构eXtended Architecture**的缩写是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施许多传统关系数据库包括PostgreSQLMySQLDB2SQL Server和Oracle和消息代理包括ActiveMQHornetQMSMQ和IBM MQ
*X/Open XA***扩展架构eXtended Architecture**的缩写是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施许多传统关系数据库包括PostgreSQLMySQLDB2SQL Server和Oracle和消息代理包括ActiveMQHornetQMSMQ和IBM MQ
XA不是一个网络协议 - 它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在;例如在Java EE应用程序的世界中XA事务是使用Java事务APIJTA实现的而Java事务APIJTA则由许多用于使用Java数据库连接JDBC的数据库驱动程序以及使用Java消息服务JMSAPI。
XA不是一个网络协议——它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在例如在Java EE应用程序的世界中XA事务是使用Java事务APIJTA实现的而Java事务APIJTA则由许多用于使用Java数据库连接JDBC的数据库驱动程序以及使用Java消息服务JMSAPI。
XA假定您的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA则表示它调用XA API以查明操作是否应该是分布式事务的一部分 - 如果是,则将必要的信息发送到数据库服务器。司机还会提供回调,协调员可以通过回调来要求参与者准备,提交或中止。
XA假定您的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA则表示它调用XA API以查明操作是否应该是分布式事务的一部分——如果是,则将必要的信息发送到数据库服务器。司机还会提供回调,协调者可以通过回调来要求参与者准备,提交或中止。
事务协调器实现XA API。标准没有指定应该如何实现但实际上协调器通常只是一个库与发出事务的应用程序不是单独的服务一起被加载到相同的进程中。它跟踪交易的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次交易的提交/中止决定。
事务协调器实现XA API。标准没有指定应该如何实现但实际上协调器通常只是一个库与发出事务的应用程序不是单独的服务一起被加载到相同的进程中。它跟踪事务的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次事务的提交/中止决定。
如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的交易的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调器因为所有通信都必须通过其客户端库。
如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的事务的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调器因为所有通信都必须通过其客户端库。
#### 怀疑时持有锁
为什么我们非常关心交易被怀疑?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的交易吗?
为什么我们非常关心事务被怀疑?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的事务吗?
问题在于锁定。正如在第225页上的“读取已提交”中所讨论的那样数据库事务通常对其修改的行进行行级别的排他锁定以防止脏写入。此外如果要使用可序列化的隔离则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定参见“[两阶段锁定2PL](ch7.md#两阶段锁定2PL)”)。
在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,交易必须在整个时间内保持锁定状态。如果协调员已经坠毁需要20分钟才能重新启动这些锁将会保持20分钟。如果协调员的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。
在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经坠毁需要20分钟才能重新启动这些锁将会保持20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。
当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他交易不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。
当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他事务不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。
#### 从协调器故障中恢复
理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定交易确实发生【89,90】也就是说协调者不能以任何理由决定结果的交易例如因为交易日志已经由于软件错误。这些交易不能自动解决所以他们永远坐在数据库中持有锁和阻止其他交易
理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】也就是说协调者不能以任何理由决定结果的事务例如因为事务日志已经由于软件错误。这些事务不能自动解决所以他们永远坐在数据库中持有锁和阻止其他事务
即使重新启动数据库服务器也不能解决这个问题因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁否则就会冒违反原子性保证的风险。这是一个棘手的情况。
唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的交易的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。
唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。
许多XA的实现都有一个叫做启发式决策的紧急逃生舱口允许参与者单方面决定放弃或进行一个有疑问的交易而不需要协调员做出明确的决定【76,77,91】。要清楚的是这里的平庸是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了摆脱灾难性的情况,而不是经常使用。
许多XA的实现都有一个叫做**启发式决策heuristic decistions**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务而不需要协调者做出明确的决定【76,77,91】。要清楚的是这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常使用。
#### 分布式事务的限制
XA事务解决了保持多个参与者数据系统一致的真实而重要的问题但正如我们所看到的那样它们也引入了主要的操作问题。特别是关键的实现是事务协调器本身就是一种数据库在其中存储事务结果因此需要像其他重要数据库一样小心
* 如果协调器没有被复制,而是只在一台机器上运行,那么整个系统是一个失败的单点(因为它的失败导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调器实现默认情况下不是高度可用,或者只有基本的复制支持。
* 许多服务器端应用程序都是在无状态模式下开发的受到HTTP的青睐所有持久状态都存储在数据库中具有应用程序服务器可随意添加和删除的优点。但是当协调器是应用程序服务器的一部分时它会改变部署的性质。突然间协调员的日志成为持久系统状态的关键部分 - 与数据库本身一样重要,因为协调员日志是为了在崩溃后恢复疑问交易所必需的。这样的应用程序服务器不再是无状态的。
* 许多服务器端应用程序都是在无状态模式下开发的受到HTTP的青睐所有持久状态都存储在数据库中具有应用程序服务器可随意添加和删除的优点。但是当协调器是应用程序服务器的一部分时它会改变部署的性质。突然间协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。
* 由于XA需要与各种数据系统兼容因此这是必须的最低公分母。例如它不能检测到不同系统间的死锁因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息而且它不适用于[SSI](ch7.md#可串行快照隔离SSI ),因为这需要一个协议来识别不同系统之间的冲突。
* 对于数据库内部的分布式事务而不是XA限制不是很大例如SSI的分布式版本是可能的。然而仍然存在2PC成功进行交易的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,交易也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。
* 对于数据库内部的分布式事务而不是XA限制不是很大例如SSI的分布式版本是可能的。然而仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。
这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全 - 有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在第十一章和第十二章回到这些章节。但首先,我们应该总结一致的话题。
这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md)[第十二章](ch12.md)回到这些章节。但首先,我们应该总结共识的话题。
@ -742,7 +748,9 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
非正式地,共识意味着让几个节点达成一致。例如,如果有几个人同时尝试预订飞机上的最后一个座位或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户,则可以使用一个一致的算法来确定哪个其中一个互不相容的行动应该是赢家。
共识问题通常形式化如下一个或多个节点可以提出值并且共识算法决定其中的一个值。在座位预订的例子中当几个顾客同时试图购买最后一个座位时处理顾客请求的每个节点可以提出正在服务的顾客的ID并且决定指示哪个顾客获得座位。在这种形式主义中共识算法必须满足以下性质【25】[^xiii]
共识问题通常形式化如下:一个或多个节点可以**提出propose**某个值,而共识算法**决定decides**采用其中的一个值。在座位预订的例子中当几个顾客同时试图购买最后一个座位时处理顾客请求的每个节点可以提出正在服务的顾客的ID且决定指明了哪个顾客获得座位。
在这种形式中共识算法必须满足以下性质【25】[^xiii]
[^xiii]: 这种共识的特殊形式被称为统一共识相当于在具有不可靠故障检测器的异步系统中的常规共识【71】。学术文献通常指的是过程而不是节点但我们在这里使用节点来与本书的其余部分保持一致。
@ -756,34 +764,34 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
***有效性***
如果一个节点决定值`v`则v由某个节点提出。
如果一个节点决定值 `v`,则`v`由某个节点提出。
***终止***
由每个未崩溃节点来最终决定值。
由所有未崩溃的节点来最终决定值。
统一协议和完整性属性定义了共识的核心思想:每个人都决定相同的结果,一旦你决定了,你就不能改变主意。有效性属性主要是为了排除微不足道的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调失败了,那么不确定的参与者就不能决定是否提交或中止。
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调失败了,那么不确定的参与者就不能决定是否提交或中止。
终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事 - 换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种活泼的财产,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。)
终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。)
共识的系统模型假设,当一个节点“崩溃”时,它突然消失,永远不会回来。 而不是软件崩溃想象一下地震包含你的节点的数据中心被山体滑坡所摧毁你必须假设你的节点被埋在30英尺以下的泥土中并且永远不会回到在线状态。这个系统模型任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。
共识的系统模型假设当一个节点“崩溃”时它突然消失永远不会回来。而不是软件崩溃想象一下地震包含你的节点的数据中心被山体滑坡所摧毁你必须假设你的节点被埋在30英尺以下的泥土中并且永远不会回到在线状态。这个系统模型任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。
当然如果所有的节点都崩溃而且没有一个正在运行那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制事实上可以证明任何一致性算法都需要至少大部分节点正确运行以确保终止【67】。大多数人可以安全地形成法定人数请参阅第179页上的“读和写的法定人数”)。
当然如果所有的节点都崩溃而且没有一个正在运行那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制事实上可以证明任何一致性算法都需要至少大部分节点正确运行以确保终止【67】。大多数人可以安全地形成法定人数参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性 - 协议完整性和有效性【92】。因此大规模的中断可能会阻止系统处理请求但是它不能通过使系统做出无效的决定来破坏共识系统。
因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意完整性和有效性【92】。因此大规模的中断可能会阻止系统处理请求但是它不能通过使系统做出无效的决定来破坏共识系统。
大多数一致性算法假定没有拜占庭式的错误,正如在“拜占庭式故障”一节中所讨论的那样。也就是说如果一个节点没有正确地遵循协议例如如果它发送矛盾的消息到不同的节点它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】就可以对拜占庭故障形成共识但我们没有空间在本书中详细讨论这些算法。
大多数一致性算法假定没有拜占庭式的错误,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说如果一个节点没有正确地遵循协议例如如果它发送矛盾的消息到不同的节点它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】就可以对拜占庭故障形成共识但我们没有地方在本书中详细讨论这些算法。
#### 共识算法和总顺序广播
#### 共识算法和序广播
最着名的容错一致性算法是**视图戳复制viewstamped replication**VSR【94,95】Paxos 【96,97,98,99】Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处但它们并不相同【103】。在本书中我们不会详细介绍不同的算法除非你自己实现一个共识系统这可能不是一个明智的做法只要了解一些共同的高级思想就足够了这很难【98,104】
最着名的容错一致性算法是**视图戳复制viewstamped replication**VSR【94,95】Paxos 【96,97,98,99】Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处但它们并不相同【103】。在本书中我们不会详细介绍不同的算法只要了解一些共同的高级思想就足够了除非你准备自己实现一个共识系统。这可能不是一个明智的做法相当困难【98,104】
这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了一系列的值这使得他们成为了顺序广播算法正如本章前面所讨论的那样请参阅第348页上的“全部顺序广播”)。
这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
请记住,总顺序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想这相当于进行了几轮的共识在每一轮中节点提出下一个要发送的消息然后决定下一个要发送的消息总数【67】。
请记住,序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想这相当于进行了几轮的共识在每一轮中节点提出下一个要发送的消息然后决定下一个要发送的消息总数【67】。
所以,总的顺序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递):
所以,序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递):
* 由于协商一致意见,所有节点决定以相同的顺序传递相同的消息。
@ -792,28 +800,29 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
* 由于有效性属性,消息不会被破坏,也不是凭空制造的。
* 由于终止属性,消息不会丢失。
已加密的复制Raft和Zab直接执行全部命令广播因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下这种优化被称为Multi-Paxos。
已加密的复制Raft和Zab直接执行全广播因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下这种优化被称为Multi-Paxos。
#### 单领导者复制和共识
第5章中我们讨论了单领导者复制参见第152页的“领导者和追随者”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使复制品保持最新状态。这不是基本上全部命令播放?我们怎么不用担心第五章的共识?
[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识?
答案取决于如何选择领导者。如果领导人是由您的运营团队中的人员手动选择和配置的,那么您基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到操作员手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止性,因为它需要人为干预才能取得进展。
一些数据库执行自动领导者选举和故障转移如果旧领导者失败则促使追随者成为新的领导者参见第156页的“处理节点中断”)。这使我们更接近容错的全面命令播出,从而达成共识。
一些数据库执行自动领导者选举和故障转移如果旧领导者失败则促使追随者成为新的领导者参见第156页的“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。
但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全部命令广播就像单引导复制,单引导复制需要领导,那么...
看来要选一个领导,我们首先需要一个领导。要解决共识,首先要解决共识。我们如何摆脱这个难题?
但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么...
看来要选出一个领导,我们首先需要一个领导。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题?
#### 时代编号和法定人数
迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者但是并不能保证领导者是独一无二的。相反他们可以做出较弱的保证协议定义了一个纪元号码称为Paxos中的选票号码Viewstamped复制中的视图号码以及Raft中的术语号码并确保在每个纪元中领导者是唯一的。
迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者但是并不能保证领导者是独一无二的。相反他们可以做出较弱的保证协议定义了一个纪元号码称为Paxos中的选票号码视图戳复制中的视图号码以及Raft中的术语号码并确保在每个纪元中领导者是唯一的。
每当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了。
在任何领导人被允许决定任何事情之前必须首先检查是否没有其他具有较高时代的领导者这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下回想一下第300页的“真理是由多数人定义的”:一个节点不一定相信自己的判断 - 只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。
在任何领导人被允许决定任何事情之前必须首先检查是否没有其他具有较高时代的领导者这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下回想一下第300页的“[真理是由多数所定义](ch8.md#真理是由多数所定义)”:一个节点不一定相信自己的判断——只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。
相反,它必须从节点法定人数中收集选票(请参阅第179页上的“读和写的法定人数。对于领导者想要做出的每一个决定都必须将建议值发送给其他节点并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。
相反,它必须从节点法定人数中收集选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)。对于领导者想要做出的每一个决定都必须将建议值发送给其他节点并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。
因此我们有两轮投票一次是选一位领导人二是投票领导人的提议。关键的看法是这两票的法定人数必须重叠如果一个提案的投票成功至少有一个投票的节点也必须参加最近的领导人选举【105】。因此如果一个提案的投票没有显示任何更高的时代那么现在的领导者就可以得出这样的结论没有一个更高时代的领袖选举发生了因此可以确定它仍然是领导。然后它可以安全地决定提出的价值。
@ -821,13 +830,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
#### 共识的局限性
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播因此它们也可以容错的方式实现线性一致性的原子操作参见第350页的“使用全部命令广播实现线性一致性存储”)。
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全序广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。
尽管如此,它们并没有到处使用,因为它的好处是有代价的。
节点在决定之前对节点进行投票的过程是一种同步复制。如第153页的“同步与异步复制”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失 - 但是为了获得更好的性能,许多人选择接受这种风险。
节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“线性一致性的成本第295页
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“[线性一致性的代价](#线性一致性的代价)第295页
大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
@ -841,23 +850,23 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
为了理解这一点简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员您很少需要直接使用ZooKeeper因为它实际上不适合作为通用数据库。更有可能的是通过其他项目间接依赖它例如HBaseHadoop YARNOpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么
ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据虽然它们仍然写入磁盘以保持持久性所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样部命令广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。
ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据虽然它们仍然写入磁盘以保持持久性所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。
ZooKeeper模仿Google的Chubby锁定服务【14,98】不仅实现了全面的命令广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用:
ZooKeeper模仿Google的Chubby锁定服务【14,98】不仅实现了全广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用:
***线性一致性的原子操作***
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅第295页上的“进程暂停”)。
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
***操作的总排序***
***操作的序***
如“页首301和锁定”中所述当某个资源受到锁定或租约的保护时您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作并为每个操作提供一个单调递增的事务IDzxid和版本号cversion来提供这个功能【15】。
如“页首301和锁定”中所述当某个资源受到锁定或租约的保护时您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作并为每个操作提供一个单调递增的事务ID`zxid`)和版本号(`cversion`来提供这个功能【15】。
***故障检测***
***失效检测***
客户端在ZooKeeper服务器上维护一个长期的会话客户端和服务器周期性地交换心跳来检查另一个节点是否还活着。即使连接暂时中断或者ZooKeeper节点失败会话仍保持活动状态。但是如果心跳停止持续时间超过会话超时ZooKeeper会声明该会话已经死亡。当会话超时ZooKeeper调用这些临时节点会话持有的任何锁都可以配置为自动释放。
***更通知***
***更通知***
一个客户端不仅可以读取其他客户端创建的锁和值还可以监视其中的更改。因此客户端可以找出另一个客户端何时加入集群基于它写入ZooKeeper的值还是另一个客户端发生故障因为其会话超时并且其临时节点消失。通过订阅通知客户避免了不得不经常轮询以找出变化。
@ -867,15 +876,15 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】不仅实现了全面的
#### 将工作分配给节点
ZooKeeper/Chubby模型运行良好的一个例子是如果您有几个流程或服务的实例并且需要选择其中一个实例作为leader或primary。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。
ZooKeeper/Chubby模型运行良好的一个例子是如果您有几个流程或服务的实例并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅第195页的“重新平衡分区”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。
这些类型的任务可以通过在ZooKeeper中明智地使用原子操作各种节点和通知来实现。如果正确完成这种方法允许应用程序自动从故障中恢复无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高级别的工具但这样做并不容易但它仍然比尝试从头开始实现必要的一致性算法要好得多成绩不佳【107】。
应用程序最初只能在单个节点上运行但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反ZooKeeper在固定数量的节点通常是三到五个上运行并在这些节点之间执行其多数票同时支持潜在的大量客户端。因此ZooKeeper提供了一种将协调节点共识操作排序和故障检测的一些工作“外包”到外部服务的方式。
通常由ZooKeeper管理的数据的类型变化十分缓慢代表“分区7中的节点运行在10.1.1.23上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点则可以使用其他工具如Apache BookKeeper 【108】
通常由ZooKeeper管理的数据的类型变化十分缓慢代表“分区7中的节点运行在`10.1.1.23`上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点则可以使用其他工具如Apache BookKeeper 【108】
#### 服务发现
@ -903,11 +912,11 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。
但是,即使我们捕捉到因果顺序(例如使用Lamport时间戳我们也看到有些事情不能以这种方式实现在“时间戳排序不够充分”的第347页中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。
但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),我们也看到有些事情不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括:
***线性一致性的比较和设置寄存器***
***线性一致性的CAS寄存器***
寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。
@ -915,7 +924,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
数据库必须决定是否提交或中止分布式事务。
***全部顺序广播***
***全序广播***
消息传递系统必须决定传递消息的顺序。
@ -923,27 +932,27 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
当几个客户争抢锁或租约时,锁决定哪个客户成功获得。
***员/协调服务***
***员/协调服务***
给定故障检测器(例如,超时),系统必须决定哪些节点处于活动状态,哪些应该被认为是死的,因为它们的会话超时。
***唯一性约束***
当多个事务同时尝试使用相同的密钥创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
当多个事务同时尝试使用相同的创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法:
1. 等待领导者恢复同时接受系统将被阻止。许多XA/JTA事务协调选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。
2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识 - 计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
1. 等待领导者恢复同时接受系统将被阻止。许多XA/JTA事务协调选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。
2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法建议使用经过验证的算法来正确处理不利的网络条件【107】。
尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议故障检测和会员服务方面起着重要的作用。使用起来并不容易但比开发自己的算法要好得多可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西而且你想要它要容错那么建议使用类似ZooKeeper的东西。
尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。