mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
ch9 65%
This commit is contained in:
parent
cede21522d
commit
0bcb50c2f8
13
README.md
13
README.md
@ -106,18 +106,15 @@
|
||||
| 第六章:分区 | 初翻 | |
|
||||
| 第七章:事务 | 精翻 60% | Vonng |
|
||||
| 第八章:分布式系统中的问题 | 初翻 | |
|
||||
| 第九章:一致性与共识 | 初翻 30% | Vonng |
|
||||
| 第九章:一致性与共识 | 初翻 65% | Vonng |
|
||||
| 第三部分:前言 | 精翻 | |
|
||||
| 第十章:批处理 | 机翻 | 于鑫 |
|
||||
| 第十一章:流处理 | 机翻 | 于鑫 |
|
||||
| 第十章:批处理 | 机翻 | |
|
||||
| 第十一章:流处理 | 机翻 | |
|
||||
| 第十二章:数据系统的未来 | 机翻 | |
|
||||
| 术语表 | - | |
|
||||
| 后记 | 机翻 | |
|
||||
|
||||
|
||||
计划在3月内完成所有章节的初翻。
|
||||
|
||||
|
||||
|
||||
## CONTRIBUTION
|
||||
|
||||
@ -129,7 +126,9 @@ 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)
|
||||
3. [第六章第一部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
223
ch9.md
223
ch9.md
@ -82,7 +82,7 @@
|
||||
|
||||
为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i]
|
||||
|
||||
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
|
||||
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
|
||||
|
||||
在这个例子中,寄存器有两种类型的操作:
|
||||
|
||||
@ -101,14 +101,14 @@
|
||||
|
||||
[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为**常规寄存器(regular register)**【7,25】
|
||||
|
||||
为了使系统线性一致,我们需要添加另一个约束,如**图9-3**所示
|
||||
为了使系统线性一致,我们需要添加另一个约束,如[图9-3](img/fig9-3.png)所示
|
||||
|
||||
![](img/fig9-3.png)
|
||||
**图9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。**
|
||||
|
||||
在一个线性一致的系统中,我们可以想象,在x的值自动翻转从0到1的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值1,即使写操作尚未完成,所有后续读取也必须返回新值。
|
||||
在一个线性一致的系统中,我们可以想象,在`x`的值自动翻转从`0`到`1`的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值`1`,即使写操作尚未完成,所有后续读取也必须返回新值。
|
||||
|
||||
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。)
|
||||
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值`1`的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回`1`。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。)
|
||||
|
||||
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
|
||||
|
||||
@ -148,7 +148,7 @@
|
||||
>
|
||||
> ***线性一致性***
|
||||
>
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(请参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
>
|
||||
> 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
|
||||
>
|
||||
@ -164,9 +164,9 @@
|
||||
|
||||
一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。
|
||||
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,参阅“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。你可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,你需要在读取【15】之前调用`sync()`。参阅“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
|
||||
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中以更细的粒度使用。 RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
|
||||
@ -178,7 +178,7 @@
|
||||
|
||||
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
|
||||
|
||||
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
|
||||
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
|
||||
|
||||
然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。
|
||||
|
||||
@ -188,7 +188,7 @@
|
||||
|
||||
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。
|
||||
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(请参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。
|
||||
![](img/fig9-5.png)
|
||||
**图9-5 Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
|
||||
|
||||
@ -240,7 +240,7 @@
|
||||
|
||||
仲裁条件满足($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】。
|
||||
|
||||
@ -378,13 +378,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
我们不会在这里讨论非线性系统如何保证因果一致性的细节,而只是简要地探讨一些关键的思想。
|
||||
|
||||
为了保持因果性,您需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。
|
||||
为了保持因果性,你需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。
|
||||
|
||||
为了确定因果依赖关系,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y时已经看到X值,则X和Y可能是因果关系的。这个分析使用了你在欺诈指控的刑事调查中所期望的那些问题:CEO在做出决定时是否知道X?
|
||||
|
||||
在其他操作之前确定哪些操作发生的技术与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系,我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步:它需要跟踪整个数据库的因果关系,而不仅仅是一个关键。版本向量可以被推广到做这个【54】。
|
||||
|
||||
为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在图5-13中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。
|
||||
为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在[图5-13](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。
|
||||
|
||||
|
||||
|
||||
@ -398,17 +398,17 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
特别是,我们可以按照与因果关系一致的顺序创建序列号[^vii]:我们保证,如果操作A因果关系发生在B之前,那么A在总顺序之前发生在B之前(A具有比B更小的序列号)。并行操作可以任意命令。这样一个总的秩序捕获所有的因果信息,但也强加了比由于因果关系所严格要求的更多的秩序。
|
||||
|
||||
[^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,您可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。
|
||||
[^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,你可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。
|
||||
|
||||
在单主复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。
|
||||
|
||||
#### 非因果序列号发生器
|
||||
|
||||
如果没有一个领导者(可能是因为您使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法:
|
||||
如果没有一个领导者(可能是因为你使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法:
|
||||
|
||||
* 每个节点都可以生成自己独立的一组序列号。例如,如果有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中保留一些位以包含唯一的节点标识符,这将确保两个不同的节点永远不会生成相同的序列号。
|
||||
* 您可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 您可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。
|
||||
* 你可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 你可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。
|
||||
|
||||
这三个选项都比单独的领导者增加一个计数器的表现更好,并且更具可扩展性。它们为每个操作生成一个唯一的,大约增加的序列号。然而,他们都有一个问题:他们产生的序列号与因果关系不一致。
|
||||
|
||||
@ -435,11 +435,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
**图9-8 Lamport时间戳提供了与因果关系一致的总排序。**
|
||||
|
||||
|
||||
兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果您有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。
|
||||
兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果你有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。
|
||||
|
||||
到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于兰伯特时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。
|
||||
|
||||
这如图9-8所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。
|
||||
这如[图9-8](img/fig9-8.png)所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。
|
||||
|
||||
只要最大计数器值与每一个操作一起进行,这个方案确保兰伯特时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。
|
||||
|
||||
@ -455,11 +455,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
这种方法适用于事后确定胜利者:一旦收集了系统中的所有用户名创建操作,就可以比较他们的时间戳。然而,当一个节点刚刚收到用户的一个请求来创建一个用户名,并且现在需要决定这个请求是成功还是失败,这是不够的。此时,节点不知道另一个节点是否正在同时创建具有相同用户名的帐户,以及其他节点可以分配给该操作的时间戳。
|
||||
|
||||
为了确保没有其他节点正在使用相同的用户名和较低的时间戳同时创建一个帐户,您必须检查每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题而出现故障或无法到达,则该系统将停止工作。这不是我们需要的那种容错系统。
|
||||
为了确保没有其他节点正在使用相同的用户名和较低的时间戳同时创建一个帐户,你必须检查每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题而出现故障或无法到达,则该系统将停止工作。这不是我们需要的那种容错系统。
|
||||
|
||||
这里的问题是,只有在收集了所有的操作之后,操作的总顺序才会出现。如果另一个节点已经产生了一些操作,但是你还不知道它们是什么,那么就不能构造最终的操作顺序:来自另一个节点的未知操作可能需要被插入到总数的不同位置订购。
|
||||
|
||||
总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,您还需要知道该命令何时完成。如果您有创建用户名的操作,并且您确定没有其他节点可以在您的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。
|
||||
总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,你还需要知道该命令何时完成。如果你有创建用户名的操作,并且你确定没有其他节点可以在你的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。
|
||||
|
||||
这个知道什么时候你的总顺序被完成的概念被记录在全序广播的话题中。
|
||||
|
||||
@ -491,7 +491,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
像ZooKeeper和etcd这样的共识服务实际上是实现全面的顺序播放。这个事实暗示了整个命令广播和共识之间有着密切的联系,我们将在本章后面进行探讨。
|
||||
|
||||
全序广播正是您所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。
|
||||
全序广播正是你所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。
|
||||
类似地,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息表示一个确定性事务作为存储过程来执行,并且每个节点都处理这些消息相同的顺序,那么数据库的分区和副本保持一致【61】。
|
||||
|
||||
全序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。
|
||||
@ -508,25 +508,25 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。
|
||||
|
||||
但是,如果您有全序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。
|
||||
但是,如果你有全序广播,则可以在其上构建线性一致性存储。例如,你可以确保用户名唯一标识用户帐户。
|
||||
|
||||
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。
|
||||
|
||||
您可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作:
|
||||
你可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作:
|
||||
|
||||
1. 在日志中添加一条消息,暂时指明您要声明的用户名。
|
||||
1. 在日志中添加一条消息,暂时指明你要声明的用户名。
|
||||
2. 阅读日志,并等待你附加的信息被传回给你。[^xi]
|
||||
3. 检查是否有任何消息声称你想要的用户名。如果所需用户名的第一条消息是你自己的消息,那么你是成功的:你可以提交用户名声明(也许通过附加另一条消息到日志)并确认给客户端。如果所需用户名的第一条消息来自其他用户,则中止操作。
|
||||
|
||||
[^xi]: 如果您不等待,但是在入队之后立即确认写入,则会得到类似于多核x86处理器的内存一致性模型【43】。 该模型既不是线性的也不是连续的。
|
||||
[^xi]: 如果你不等待,但是在入队之后立即确认写入,则会得到类似于多核x86处理器的内存一致性模型【43】。 该模型既不是线性的也不是连续的。
|
||||
|
||||
由于日志条目以相同顺序传递到所有节点,因此如果有多个并发写入,则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者,并中止后面的写入,确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。
|
||||
|
||||
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项:
|
||||
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果你从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项:
|
||||
|
||||
* 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。)
|
||||
* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。
|
||||
* 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。)
|
||||
* 你可以通过附加消息,读取日志以及在消息被传回给你时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。)
|
||||
* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给你,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。
|
||||
* 你可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;参阅“[复制研究](ch5.md#设置新从库)”。)
|
||||
|
||||
#### 使用线性一致性存储实现全序广播
|
||||
|
||||
@ -534,9 +534,9 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。
|
||||
|
||||
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
|
||||
该算法很简单:对于每个要通过全部顺序广播发送的消息,你将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,你可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
|
||||
|
||||
请注意,与兰伯特时间戳不同,您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
|
||||
请注意,与兰伯特时间戳不同,你通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
|
||||
|
||||
与兰伯特时间戳——事实上,这是全序广播和时间戳顺序之间的关键区别。
|
||||
|
||||
@ -569,13 +569,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
> ### 共识的不可能性
|
||||
>
|
||||
> 您可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么?
|
||||
> 你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么?
|
||||
>
|
||||
> 答案是FLP结果在异步系统模型中得到了证明(请参阅“系统模型与现实”在本部分),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。
|
||||
> 答案是FLP结果在异步系统模型中得到了证明(参阅“[系统模型与现实](#系统模型与现实)”),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。
|
||||
>
|
||||
> 因此,FLP虽然不可能达成共识,但理论上具有重要意义,但实际上分布式系统通常可以达成共识。
|
||||
|
||||
在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库,消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。
|
||||
在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库、消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。
|
||||
|
||||
通过对2PC学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。
|
||||
|
||||
@ -585,11 +585,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
在[第7章](ch7.md)中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。
|
||||
|
||||
原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。
|
||||
原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果你修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。
|
||||
|
||||
#### 从单节点到分布式原子提交
|
||||
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
|
||||
|
||||
因此,在单个节点上,事务承诺主要取决于数据持久写入磁盘的顺序:首先是数据,然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使得提交具有原子性。
|
||||
|
||||
@ -601,17 +601,17 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
* 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。
|
||||
* 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。
|
||||
|
||||
如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被收回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。
|
||||
如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被撤回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。
|
||||
|
||||
事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。
|
||||
事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读已提交隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据,所以它们也必须恢复。
|
||||
|
||||
(提交事务的效果有可能在后来被另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何交叉事务的正确性要求是应用程序的问题。)
|
||||
(提交事务的效果有可能后续通过另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于交叉事务正确性的保证是应用自己的问题。)
|
||||
|
||||
#### 介绍两阶段提交
|
||||
|
||||
两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,并且还以XA事务【76,77】(例如Java Transaction API支持)或通过`WS-AtomicTransaction for SOAP Web`服务的形式提供给应用程序【78,79】。
|
||||
|
||||
[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此名称),而不是单个提交请求。
|
||||
[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此而得名),而不是单个提交请求。
|
||||
|
||||
![](img/fig9-9.png)
|
||||
|
||||
@ -619,11 +619,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
> #### 不要混淆2PC和2PL
|
||||
>
|
||||
> 两阶段提交(2PC)和两阶段锁定(请参阅第257页上的“两阶段锁定(2PL)”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。
|
||||
> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。
|
||||
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调器(coordinator)**(也称为事务管理器)。协调器通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为事务管理器)。协调者通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。
|
||||
|
||||
正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调器开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应:
|
||||
正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调者开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应:
|
||||
|
||||
* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调者在阶段2发出**提交(commit)**请求,实际发生提交。
|
||||
* 如果任何参与者回复“否”,则协调者在阶段2中向所有节点发送**中止(abort)**请求。
|
||||
@ -638,7 +638,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
1. 当应用想要开启一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。
|
||||
2. 应用在每个参与者上开始单节点事务,并将全局唯一事务ID附到单节点事务上。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。
|
||||
3. 当应用程序准备提交时,协调器向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调器向所有参与者发送针对该事务ID的放弃请求。
|
||||
3. 当应用程序准备提交时,协调者向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调者向所有参与者发送针对该事务ID的放弃请求。
|
||||
4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行事务。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交事务。换句话说,参与者放弃了中止事务的权利,但没有实际提交。
|
||||
5. 当协调者收到所有准备请求的答复时,就是否提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调者必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为**提交点(commit point)**。
|
||||
6. 一旦协调者的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调者必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间崩溃,事务将在恢复时进行——由于参与者投票“是”,因此恢复时不能拒绝提交。
|
||||
@ -649,26 +649,26 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
#### 协调者失效
|
||||
|
||||
我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。
|
||||
我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。
|
||||
|
||||
如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务被怀疑或不确定。
|
||||
如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调者崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务是存疑的或不确定的。
|
||||
|
||||
情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调器实际上决定提交,数据库2收到提交请求。但是,协调器在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。
|
||||
情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调者实际上决定提交,数据库2收到提交请求。但是,协调者在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。
|
||||
|
||||
![](img/fig9-10.png)
|
||||
**图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止**
|
||||
|
||||
没有协调者的消息,参与者无法知道是否承诺或放弃。原则上,参与者可以相互沟通,找出每个参与者如何投票并达成一致,但这不是2PC协议的一部分。
|
||||
|
||||
2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调器恢复后,通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此,2PC的提交点归结为协调器上的常规单节点原子提交。
|
||||
2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC的**提交点**归结为协调者上的常规单节点原子提交。
|
||||
|
||||
#### 三阶段提交
|
||||
|
||||
两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住等待协调器恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但是,在实践中做这个工作并不那么简单。
|
||||
两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住,等待协调者恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但在实践中这个工作并不那么简单。
|
||||
|
||||
作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它不能保证原子性。
|
||||
作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它并不能保证原子性。
|
||||
|
||||
通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能有协调器故障的问题。
|
||||
通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。
|
||||
|
||||
|
||||
|
||||
@ -678,7 +678,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
分布式事务的某些实现会带来严重的性能损失——例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。
|
||||
|
||||
但是,我们不应该直接抛弃分布式事务,而应该更加详细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆:
|
||||
但是,我们不应该直接抛弃分布式事务,而应该更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆:
|
||||
|
||||
***数据库内部的分布式事务***
|
||||
|
||||
@ -688,7 +688,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
在异构事务中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
|
||||
|
||||
数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的事务则更具挑战性。
|
||||
数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常工作地很好。另一方面,跨越异构技术的事务则更有挑战性。
|
||||
|
||||
#### 恰好一次的消息处理
|
||||
|
||||
@ -704,40 +704,41 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
*X/Open XA*(**扩展架构(eXtended Architecture)**的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 。
|
||||
|
||||
XA不是一个网络协议——它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。
|
||||
XA不是一个网络协议——它只是一个用于与事务协调者连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。
|
||||
|
||||
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分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。
|
||||
问题在于**锁**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常对其修改的行进行行级别的排他锁定,以防止脏写。此外,如果要使用可序列化的隔离等级,则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。
|
||||
|
||||
在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经崩溃,需要20分钟才能重新启动,这些锁将会持有20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。
|
||||
|
||||
当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他事务不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。
|
||||
|
||||
#### 从协调器故障中恢复
|
||||
#### 从协调者故障中恢复
|
||||
|
||||
理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远坐在数据库中,持有锁和阻止其他事务。
|
||||
理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远待在数据库中,持有锁并阻止其他事务。
|
||||
|
||||
即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒违反原子性保证的风险)。这是一个棘手的情况。
|
||||
即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒有违反原子性保证的风险)。这是一个棘手的情况。
|
||||
|
||||
唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。
|
||||
唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。
|
||||
|
||||
许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常使用。
|
||||
许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常性的使用。
|
||||
|
||||
#### 分布式事务的限制
|
||||
|
||||
XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的操作问题。特别是,关键的实现是事务协调器本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心:
|
||||
XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的运维问题。特别是,关键的实现是事务协调者本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心:
|
||||
|
||||
* 如果协调器没有被复制,而是只在一台机器上运行,那么整个系统是一个失败的单点(因为它的失败导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调器实现默认情况下不是高度可用,或者只有基本的复制支持。
|
||||
* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调器是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。
|
||||
* 由于XA需要与各种数据系统兼容,因此这是必须的最低公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。
|
||||
* 如果协调者没有被复制,而是只在一台机器上运行,那么整个系统是一个失效的单点(因为它的失效会导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调者实现默认情况下不是高度可用,或者只有基本的复制支持。
|
||||
* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调者是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。
|
||||
* 由于XA需要与各种数据系统兼容,因此它是必须的最小公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。
|
||||
* 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。
|
||||
|
||||
这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md)和[第十二章](ch12.md)回到这些章节。但首先,我们应该总结共识的话题。
|
||||
@ -769,27 +770,27 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
***终止***
|
||||
由所有未崩溃的节点来最终决定值。
|
||||
|
||||
统一协议和完整性属性定义了共识的核心思想:每个人都决定相同的结果,一旦你决定了,你就不能改变主意。有效性属性主要是为了排除微不足道的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。
|
||||
一致同意和完整性属性定义了共识的核心思想:每个人都决定了相同的结果,一旦决定了,你就不能改变主意。有效性属性主要是为了排除平凡的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。
|
||||
|
||||
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。
|
||||
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果该节点失效,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段提交的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。
|
||||
|
||||
终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
终止属性正式形成了容错的思想。它基本上说的是,一个共识算法不能简单地坐下来,永远不做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
|
||||
共识的系统模型假设,当一个节点“崩溃”时,它突然消失,永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设你的节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。
|
||||
共识的系统模型假设,当一个节点“崩溃”时,它突然消失且永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。
|
||||
|
||||
当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
|
||||
当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止属性【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
|
||||
|
||||
因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意,完整性和有效性【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】)
|
||||
|
||||
这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
|
||||
这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,一致同意,完整性,有效性和终止性质)。相反,它们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
|
||||
|
||||
请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。
|
||||
请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果仔细思考,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。
|
||||
|
||||
所以,全序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递):
|
||||
|
||||
@ -806,13 +807,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
|
||||
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识?
|
||||
|
||||
答案取决于如何选择领导者。如果领导人是由您的运营团队中的人员手动选择和配置的,那么您基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到操作员手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止性,因为它需要人为干预才能取得进展。
|
||||
答案取决于如何选择领导者。如果领导人是由你的运营团队中的人员手动选择和配置的,那么你基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止属性,因为它需要人为干预才能取得进展。
|
||||
|
||||
一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见第156页的“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。
|
||||
一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。
|
||||
|
||||
但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么...
|
||||
但是,有一个问题。我们之前曾经讨论过脑裂的问题,并且说过,所有的节点都需要同意领导者是谁,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么...
|
||||
|
||||
看来要选出一个领导,我们首先需要一个领导。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题?
|
||||
看来要选出一个领导者,我们首先需要一个领导者。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题?
|
||||
|
||||
#### 时代编号和法定人数
|
||||
|
||||
@ -820,13 +821,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
|
||||
每当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了。
|
||||
|
||||
在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下第300页的“[真理是由多数所定义](ch8.md#真理是由多数所定义)”:一个节点不一定相信自己的判断——只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。
|
||||
在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下在“[真理是由多数所定义](ch8.md#真理是由多数所定义)”中:一个节点不一定能相信自己的判断——因为只有节点自己认为它是领导者,并不一定意味着其他节点接受它作为它们的领导者。
|
||||
|
||||
相反,它必须从节点法定人数中收集选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将建议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。
|
||||
相反,它必须从节点法定人数中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。
|
||||
|
||||
因此,我们有两轮投票:一次是选一位领导人,二是投票领导人的提议。关键的看法是,这两票的法定人数必须重叠:如果一个提案的投票成功,至少有一个投票的节点也必须参加最近的领导人选举【105】。因此,如果一个提案的投票没有显示任何更高的时代,那么现在的领导者就可以得出这样的结论:没有一个更高时代的领袖选举发生了,因此可以确定它仍然是领导。然后它可以安全地决定提出的价值。
|
||||
|
||||
这个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调器不是选出来的,而容错协议算法只需要大部分节点的投票,而2PC则要求每个参与者都做“是”的投票。而且,共识算法定义了一个恢复过程,通过这个过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。
|
||||
这个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调者不是选出来的,而容错协议算法只需要大部分节点的投票,而2PC则要求每个参与者都做“是”的投票。而且,共识算法定义了一个恢复过程,通过这个过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。
|
||||
|
||||
#### 共识的局限性
|
||||
|
||||
@ -836,9 +837,9 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
|
||||
节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。
|
||||
|
||||
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。
|
||||
共识体系总是需要严格的多数来操作。这意味着你至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。
|
||||
|
||||
大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
|
||||
大多数一致性算法假定一组参与投票的节点,这意味着你不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
|
||||
|
||||
共识系统通常依靠超时来检测失败的节点。在网络延迟高度变化的环境中,特别是在地理上分布的系统中,经常发生一个节点错误地认为由于暂时的网络问题,导致失败的原因。虽然这个错误不会损害安全属性,但频繁的领导者选举会导致糟糕的表现,因为系统最终会花费更多的时间来选择领导者而不是做任何有用的工作。
|
||||
|
||||
@ -848,7 +849,7 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
|
||||
|
||||
像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调和配置服务”。这种服务的API看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果他们基本上是数据库的话,他们为什么要全力实施一个共识算法呢?是什么使他们不同于任何其他类型的数据库?
|
||||
|
||||
为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,您很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么?
|
||||
为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,你很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么?
|
||||
|
||||
ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全序广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。
|
||||
|
||||
@ -856,11 +857,11 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广
|
||||
|
||||
***线性一致性的原子操作***
|
||||
|
||||
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
|
||||
***操作的全序***
|
||||
|
||||
如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。
|
||||
如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。
|
||||
|
||||
***失效检测***
|
||||
|
||||
@ -876,29 +877,29 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广
|
||||
|
||||
#### 将工作分配给节点
|
||||
|
||||
ZooKeeper/Chubby模型运行良好的一个例子是,如果您有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。
|
||||
ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单主数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。
|
||||
|
||||
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。
|
||||
|
||||
这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高级别的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。
|
||||
|
||||
应用程序最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。
|
||||
应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。
|
||||
|
||||
通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区7中的节点运行在`10.1.1.23`上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。
|
||||
|
||||
#### 服务发现
|
||||
|
||||
ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,您通常不会提前知道您的服务的IP地址。相反,您可以配置您的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。
|
||||
ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,你通常不会事先知道服务的IP地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。
|
||||
|
||||
但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。
|
||||
|
||||
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。
|
||||
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果你的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。
|
||||
|
||||
#### 成员服务
|
||||
|
||||
ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。
|
||||
ZooKeeper和它的小伙伴们可以看作是成员服务研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。
|
||||
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在第8章中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
|
||||
即使它确实存在,仍然可能发生一个节点被错误地宣布死于共识。但是对于一个系统来说,在哪些节点构成当前的成员关系方面是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有成员的成员有不同意见,则这种方法将不起作用。
|
||||
|
||||
@ -908,13 +909,13 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
|
||||
## 本章小结
|
||||
|
||||
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
|
||||
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人——它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
|
||||
|
||||
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。
|
||||
我们还探讨了因果性,因果性对系统中的事件进行了排序(根据因和过,以及什么发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些东西可以是并发的,所以版本历史就像是一个不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,并且对网络问题的敏感性要低得多。
|
||||
|
||||
但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),我们也看到有些事情不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。
|
||||
但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),有些事情也不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们需要**共识**。
|
||||
|
||||
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括:
|
||||
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,很广泛的一系列问题实际上都可以归结为共识问题,并且彼此是等价的(从这个意义上来讲,如果你有一个问题的解决方案,你可以容易地将它转换为其他问题的解决方案)。这种等价的问题包括:
|
||||
|
||||
***线性一致性的CAS寄存器***
|
||||
|
||||
@ -930,7 +931,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
|
||||
***锁和租约***
|
||||
|
||||
当几个客户争抢锁或租约时,锁决定哪个客户成功获得。
|
||||
当几个客户争抢锁或租约时,决定哪个客户端成功获得锁。
|
||||
|
||||
***成员/协调服务***
|
||||
|
||||
@ -940,23 +941,25 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
|
||||
|
||||
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
|
||||
|
||||
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
|
||||
|
||||
但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法:
|
||||
|
||||
1. 等待领导者恢复,同时接受系统将被阻止。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。
|
||||
2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
|
||||
3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。
|
||||
如果你只有一个节点,或者你愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
|
||||
|
||||
尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
|
||||
但是,如果单个领导失败,或者如果网络中断导致主库不可达,这样的系统变得无法取得进展。处理这种情况有三种方法:
|
||||
|
||||
像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议,故障检测和会员服务方面起着重要的作用。使用起来并不容易,但比开发自己的算法要好得多,可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西,而且你想要它要容错,那么建议使用类似ZooKeeper的东西。
|
||||
1. 等待领导者恢复,同时系统将阻止接受新请求。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止属性的要求:如果领导者没有恢复,系统可能会被永久封锁。
|
||||
2. 通过让人类选择一个新的领导者节点,并重新配置系统,执行手动故障切换来使用它。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的运维人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
|
||||
3. 使用算法自动选择一个新的领导。这种方法需要一个共识算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。
|
||||
|
||||
尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
|
||||
尽管单主数据库可以提供线性一致性,而不需要在每个写操作上执行共识算法,但对于保持领导权和领导权的变更,共识仍然是需要的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识还是需要的,只是在不同的地方,而且没那么频繁。好消息是,容错算法和共识系统是存在的,我们在本章中简要地讨论了它们。
|
||||
|
||||
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。
|
||||
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说用起来并不容易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要做的是一个可以归结为共识的东西,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。
|
||||
|
||||
我们在本书第二部分的末尾介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构构建块中构建强大的应用程序。
|
||||
尽管如此,并不是每个系统都需要共识:例如,无主复制和多主复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间没有达成共识的结果,但也许没关系:也许我们只需要简单地在没有线性一致性的环境下应对,并学会更好地与具有分支和合并版本历史的数据打交道。
|
||||
|
||||
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到违反直觉的方法,其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。
|
||||
|
||||
这里就到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构的组件积木块中构建强大的应用。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user