mirror of
https://github.com/DistSysCorp/ddia.git
synced 2024-12-25 20:30:39 +08:00
update ch09 remove *
update ch09 remove *
This commit is contained in:
parent
9973a55f14
commit
1c18806c90
82
ch09.md
82
ch09.md
@ -80,8 +80,8 @@
|
||||
|
||||
在本例中,寄存器支持两种类型的操作:
|
||||
|
||||
1. *read*(*x*) ⇒ *v :*客户端请求读寄存器 x 的值,数据库会返回值 v
|
||||
2. *write*(*x*, *v*) ⇒ *r:*客户端请求将寄存器 x 的值设置为 v,数据返回结果 r(可能是成功或者失败)
|
||||
1. *read*(*x*) ⇒ *v:* 客户端请求读寄存器 x 的值,数据库会返回值 v
|
||||
2. *write*(*x*, *v*) ⇒ *r:* 客户端请求将寄存器 x 的值设置为 v,数据返回结果 r(可能是成功或者失败)
|
||||
|
||||
在上图中,x 初始值为 0,客户端 C 发出一个写请求将其置为 1。在此期间,客户端 A 和 B 不断地向数据库请求 x 的最新值。试问 A 和 B 的每个读请求都会读到什么值?
|
||||
|
||||
@ -213,7 +213,7 @@
|
||||
|
||||
对于使用无主模型的系统(Dynamo-style,参见前面[无主模型](https://ddia.qtmuniao.com/#/ch05?id=%e6%97%a0%e4%b8%bb%e6%a8%a1%e5%9e%8b)一节)来说,厂商有时候会声称你可以通过使用法定数目读写(Quorum reads and write,w+r > n)来获得强一致性。这个说法有点模糊,并不总是正确,这取决于你对法定节点的配置,也取决于你如何定义强一致性。
|
||||
|
||||
“后者胜”(Last write win)的冲突解决方法会依赖于多个机器的挂历时钟(time-of-day,参见[依赖时钟同步](https://ddia.qtmuniao.com/#/ch08?id=%e4%be%9d%e8%b5%96%e5%90%8c%e6%ad%a5%e6%97%b6%e9%92%9f)),由于多机时钟存在偏差,其物理时间戳不能保证和系统事件顺序一致,因此基本上不可能做到线性一致。放松的法定策略(Quorum,见****[放松的 Quorum 和提示转交](https://ddia.qtmuniao.com/#/ch05?id=%e6%94%be%e6%9d%be%e7%9a%84-quorum-%e5%92%8c%e6%8f%90%e7%a4%ba%e8%bd%ac%e4%ba%a4)****)也是破坏了线性一致性。即使对于严格的法定策略,非线性一致的现象也可能出现,下一节将会详细探讨。
|
||||
“后者胜”(Last write win)的冲突解决方法会依赖于多个机器的挂历时钟(time-of-day,参见[依赖时钟同步](https://ddia.qtmuniao.com/#/ch08?id=%e4%be%9d%e8%b5%96%e5%90%8c%e6%ad%a5%e6%97%b6%e9%92%9f)),由于多机时钟存在偏差,其物理时间戳不能保证和系统事件顺序一致,因此基本上不可能做到线性一致。放松的法定策略(Quorum,见[放松的 Quorum 和提示转交](https://ddia.qtmuniao.com/#/ch05?id=%e6%94%be%e6%9d%be%e7%9a%84-quorum-%e5%92%8c%e6%8f%90%e7%a4%ba%e8%bd%ac%e4%ba%a4))也是破坏了线性一致性。即使对于严格的法定策略,非线性一致的现象也可能出现,下一节将会详细探讨。
|
||||
|
||||
|
||||
### 线性一致和法定策略(Quorum)
|
||||
@ -289,7 +289,7 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
|
||||
很多分布式系统选择不提供线性一致性的原因也在于此:**是为了提升系统性能而非进行容错**。在任何时候,提供线性一致性都会严重拖慢系统。而非在网络故障发生时,才需要对线性一致性进行牺牲。
|
||||
|
||||
我们能找到一种**更高效的实现**来让存储服务提供线性一致吗?遗憾的是,暂时没有。Attiya 和 Welch 证明了,如果你想要保证线性一致,读写请求的响应时间是**正比于网络延迟**的。在一个具有高度不确定性的网络中(参见****[超时和无界延迟](https://ddia.qtmuniao.com/#/ch08?id=%e8%b6%85%e6%97%b6%e5%92%8c%e6%97%a0%e7%95%8c%e5%bb%b6%e8%bf%9f%ef%bc%88unbounded-delays%ef%bc%89)****),线性化的读写请求的响应时间不可避免的会很高。提供线性一致性保证可能没有更快的算法,但是我们稍微**放松一致性**,就可以设计出一个更快的系统。这种取舍在对延迟敏感的系统非常重要。在第十二章中,我们会探讨一些即使不提供线性一致性,但仍然可以保证正确性的一些方法。
|
||||
我们能找到一种**更高效的实现**来让存储服务提供线性一致吗?遗憾的是,暂时没有。Attiya 和 Welch 证明了,如果你想要保证线性一致,读写请求的响应时间是**正比于网络延迟**的。在一个具有高度不确定性的网络中(参见[超时和无界延迟](https://ddia.qtmuniao.com/#/ch08?id=%e8%b6%85%e6%97%b6%e5%92%8c%e6%97%a0%e7%95%8c%e5%bb%b6%e8%bf%9f%ef%bc%88unbounded-delays%ef%bc%89)),线性化的读写请求的响应时间不可避免的会很高。提供线性一致性保证可能没有更快的算法,但是我们稍微**放松一致性**,就可以设计出一个更快的系统。这种取舍在对延迟敏感的系统非常重要。在第十二章中,我们会探讨一些即使不提供线性一致性,但仍然可以保证正确性的一些方法。
|
||||
|
||||
# 顺序保证
|
||||
|
||||
@ -302,9 +302,9 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
|
||||
**顺序**(ordering)是本书中不断强调的一个主题,这也确实说明顺序是一个非常重要的基础概念。我们回忆一下本书所提到顺序的相关上下文:
|
||||
|
||||
- 在[第五章](https://ddia.qtmuniao.com/#/ch05),我们在单主模型中提到,主副本最重要的作用就是确定**复制日志**(replication log)中的**写入顺序**(order of writes),然后所有从副本都要遵从该顺序。如果不存在唯一的主节点作为权威来协调该顺序,则在并发的多个写入可能会产生冲突。(参见****[处理写入冲突](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%84%e7%90%86%e5%86%99%e5%85%a5%e5%86%b2%e7%aa%81)****)
|
||||
- 在[第五章](https://ddia.qtmuniao.com/#/ch05),我们在单主模型中提到,主副本最重要的作用就是确定**复制日志**(replication log)中的**写入顺序**(order of writes),然后所有从副本都要遵从该顺序。如果不存在唯一的主节点作为权威来协调该顺序,则在并发的多个写入可能会产生冲突。(参见[处理写入冲突](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%84%e7%90%86%e5%86%99%e5%85%a5%e5%86%b2%e7%aa%81))
|
||||
- 在[第七章](https://ddia.qtmuniao.com/#/ch07),我们讨论了**可串行化**(serializability),即保证所有并发的事务像以某种顺序一样串行执行(some sequential order)。可以通过物理上真的串行执行来实现,也可以通过并发执行但解决冲突(加锁互斥或者抛弃执行)来实现。
|
||||
- 在[第八章](https://ddia.qtmuniao.com/#/ch08),我们讨论了在分布式系统中使用时钟(参见****[依赖同步时钟](https://ddia.qtmuniao.com/#/ch08?id=%e4%be%9d%e8%b5%96%e5%90%8c%e6%ad%a5%e6%97%b6%e9%92%9f)****),这也是一个试图对无序的真实世界引入某种顺序,以解决诸如哪个写入更靠后之类的问题。
|
||||
- 在[第八章](https://ddia.qtmuniao.com/#/ch08),我们讨论了在分布式系统中使用时钟(参见[依赖同步时钟](https://ddia.qtmuniao.com/#/ch08?id=%e4%be%9d%e8%b5%96%e5%90%8c%e6%ad%a5%e6%97%b6%e9%92%9f*),这也是一个试图对无序的真实世界引入某种顺序,以解决诸如哪个写入更靠后之类的问题。
|
||||
|
||||
**顺序性**(ordering)、**线性一致性**(linearizability)和**共识协议**(consensus)三个概念间有很深的联系。相比本书其他部分,尽管这几个概念更偏理论和抽象,但理解他们却有助于来厘清系统的功能边界——哪些可以做,哪些做不了。在接下来的几小节中,我们会对此进行详细探讨。
|
||||
|
||||
@ -312,11 +312,11 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
|
||||
顺序在本书中反复被提及的原因有很多,其中一个是:**它可以维持因果性**。关于因果关系的重要性,本书也举过很多例子:
|
||||
|
||||
- 在****[一致前缀读](https://ddia.qtmuniao.com/#/ch05?id=%e4%b8%80%e8%87%b4%e5%89%8d%e7%bc%80%e8%af%bb)****中我们提到一个先看到答案、后看到问题的例子。这种现象看起来很奇怪,是因为它违反了我们关于因果顺序的直觉:*问题应该先于答案出现。*因为只有看到了问题,才可能针对其给出答案(假设这不是超自然现象,并且不能预言将来)。对于这种情况,我们说在问题和答案之间存在着**因果依赖**(causal dependency)。
|
||||
- 在[一致前缀读](https://ddia.qtmuniao.com/#/ch05?id=%e4%b8%80%e8%87%b4%e5%89%8d%e7%bc%80%e8%af%bb)中我们提到一个先看到答案、后看到问题的例子。这种现象看起来很奇怪,是因为它违反了我们关于因果顺序的直觉:问题应该先于答案出现。因为只有看到了问题,才可能针对其给出答案(假设这不是超自然现象,并且不能预言将来)。对于这种情况,我们说在问题和答案之间存在着**因果依赖**(causal dependency)。
|
||||
- 在第五章[图 5-9](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%9a%e4%b8%bb%e5%a4%8d%e5%88%b6%e6%8b%93%e6%89%91) 中有类似的情况,在有三个主的情况下,由于网络延迟,一些本应该先到的写入操作却居于后面。从某个副本的角度观察,就感觉像在更新一个不存在的数据。**因果**在此处意味着,某一行数据*只有先被创建才能够被更新*。
|
||||
- 在****[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)****一节我们知到,对于两个操作 A 和 B,有三种可能性:A 发生于 B 之前,B 发生于 A 之前,A 和 B 是并发的。这种**发生于之前**(happened before)是因果性的另一种表现:如果 A 发生在 B 之前,则 B 有可能知道 A ,进而基于 A 构建,或者说依赖于 A。如果 A 和 B 是并发的,则他们之间没有因果联系,也即,我们可以断定他们互不知道。
|
||||
- 在事务的快照隔离级别下(参见****[快照隔离和重复读](https://ddia.qtmuniao.com/#/ch07?id=%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb%e5%92%8c%e9%87%8d%e5%a4%8d%e8%af%bb)****),所有的读取都会发生在某个**一致性**的快照上。这里的一致性是什么意思呢?是**因果一致性**(consistent with causality)。如果一个快照包含某个问题答案,它一定包含该问题本身。假设我们以上帝视角,在某个**时间点**(意味着瞬时观察完)观察整个数据库可以让得到的快照满足因果一致性:所有在该时间点之前操作结果都可见,在该时间点之后的操作结果都不可见。**读偏序**(Read skew,即图 7-6 中提到的不可重复读),即意味读到了违反因果关系的状态。
|
||||
- 之前提到的事务间的写偏序的例子(参见****[写偏序和幻读](https://ddia.qtmuniao.com/#/ch07?id=%e5%86%99%e5%81%8f%e5%ba%8f%e5%92%8c%e5%b9%bb%e8%af%bb)****)本质上也是因果依赖:在图 7-8 中,系统允许 Alice 请假,是因为事务看到的 Bob 的状态是仍然再岗;当然,对于 Bob 也同样。在这个例子中,一个医生是否允许在值班时请假,依赖于当时是否仍有其他医生值班。在**可串行的快照隔离级别**(SSI,参见****[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb)****)下,我们通过追踪事务间的因果依赖(即读写数据集依赖)来检测写偏序。
|
||||
- 在[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)一节我们知到,对于两个操作 A 和 B,有三种可能性:A 发生于 B 之前,B 发生于 A 之前,A 和 B 是并发的。这种**发生于之前**(happened before)是因果性的另一种表现:如果 A 发生在 B 之前,则 B 有可能知道 A ,进而基于 A 构建,或者说依赖于 A。如果 A 和 B 是并发的,则他们之间没有因果联系,也即,我们可以断定他们互不知道。
|
||||
- 在事务的快照隔离级别下(参见[快照隔离和重复读](https://ddia.qtmuniao.com/#/ch07?id=%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb%e5%92%8c%e9%87%8d%e5%a4%8d%e8%af%bb)),所有的读取都会发生在某个**一致性**的快照上。这里的一致性是什么意思呢?是**因果一致性**(consistent with causality)。如果一个快照包含某个问题答案,它一定包含该问题本身。假设我们以上帝视角,在某个**时间点**(意味着瞬时观察完)观察整个数据库可以让得到的快照满足因果一致性:所有在该时间点之前操作结果都可见,在该时间点之后的操作结果都不可见。**读偏序**(Read skew,即图 7-6 中提到的不可重复读),即意味读到了违反因果关系的状态。
|
||||
- 之前提到的事务间的写偏序的例子(参见[写偏序和幻读](https://ddia.qtmuniao.com/#/ch07?id=%e5%86%99%e5%81%8f%e5%ba%8f%e5%92%8c%e5%b9%bb%e8%af%bb))本质上也是因果依赖:在图 7-8 中,系统允许 Alice 请假,是因为事务看到的 Bob 的状态是仍然再岗;当然,对于 Bob 也同样。在这个例子中,一个医生是否允许在值班时请假,依赖于当时是否仍有其他医生值班。在**可串行的快照隔离级别**(SSI,参见[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb))下,我们通过追踪事务间的因果依赖(即读写数据集依赖)来检测写偏序。
|
||||
- 在 Alice 和 Bob 看足球比赛的例子中,Bob 在 Alice 表示结果已经出来之后,仍然没有看到网页结果,便是违反了因果关系:Alice 的说法基于比赛结果已经出来的事实,因此 Bob 在听到 Alice 的陈述之后,应该当能看到比赛结果。图片尺寸调整的例子也是类似。
|
||||
|
||||
因果将顺序施加于**事件**(event):
|
||||
@ -336,11 +336,11 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
全序和偏序的区别还方应在不同强度**数据库一致性模型**(database consistency models)上:
|
||||
|
||||
- **线性一致性**(Linearizability):让我们回忆下对于可线性化的理解,可线性化对外表现的像**所有操作都发生于单副本上,并且会原子性的完成**。这就意味着,对于任意两个操作,我们总是可以确定其发生的先后关系,也即在可线性化系统中,所有的操作顺序满足全序关系。如之前图 9-4 中给的例子。
|
||||
- **因果一致性**(Causality)。如果我们无从判定两个操作的先后关系,则称之为**并发的**(concurrent,参见****[发生于之前和并发关系](https://ddia.qtmuniao.com/#/ch05?id=%e5%8f%91%e7%94%9f%e4%ba%8e%e4%b9%8b%e5%89%8d%ef%bc%88happens-before%ef%bc%89%e5%92%8c%e5%b9%b6%e5%8f%91%e5%85%b3%e7%b3%bb)****)。从另一个角度说,如果两个事件因果相关,则其一定有序。也即,因果性定义了一种**偏序**(partial order)关系,而非全序关系:有些操作存在因果,因此可比;而另外一些操作则是并发的,即不可比。
|
||||
- **因果一致性**(Causality)。如果我们无从判定两个操作的先后关系,则称之为**并发的**(concurrent,参见[发生于之前和并发关系](https://ddia.qtmuniao.com/#/ch05?id=%e5%8f%91%e7%94%9f%e4%ba%8e%e4%b9%8b%e5%89%8d%ef%bc%88happens-before%ef%bc%89%e5%92%8c%e5%b9%b6%e5%8f%91%e5%85%b3%e7%b3%bb))。从另一个角度说,如果两个事件因果相关,则其一定有序。也即,因果性定义了一种**偏序**(partial order)关系,而非全序关系:有些操作存在因果,因此可比;而另外一些操作则是并发的,即不可比。
|
||||
|
||||
根据上述解释,在线性一致性的数据存储服务中,是不存在并发操作的:**因为必然存在一个时间线能将所有操作进行排序**。同一时刻可能会有多个请求到来,但是线性化的存储服务可以保证:**所有请求都会在单个副本上、一个单向向前的时间线上的某个时间点被原子的处理,而没有任何并发**。
|
||||
|
||||
并发(concurrency)意味着时间线的分叉与合并。但在重新合并之时,来自两个时间分支的操作就有可能出现不可比的情况。在第五章的图 5-14 (参见****[确定 Happens-Before 关系](https://ddia.qtmuniao.com/#/ch05?id=%e7%a1%ae%e5%ae%9a-happens-before-%e5%85%b3%e7%b3%bb)****)中我们见过类似的现象,所有的事件不在一条时间线上,而是有相当复杂的图形依赖。图中的每个箭头,本质上定义了一种因果依赖,也即偏序关系。
|
||||
并发(concurrency)意味着时间线的分叉与合并。但在重新合并之时,来自两个时间分支的操作就有可能出现不可比的情况。在第五章的图 5-14 (参见[确定 Happens-Before 关系](https://ddia.qtmuniao.com/#/ch05?id=%e7%a1%ae%e5%ae%9a-happens-before-%e5%85%b3%e7%b3%bb))中我们见过类似的现象,所有的事件不在一条时间线上,而是有相当复杂的图形依赖。图中的每个箭头,本质上定义了一种因果依赖,也即偏序关系。
|
||||
|
||||
> 理解全序和偏序、线性一致性和因果一致性的一个关键模型是**有向图**。在该图中,点代表事件,有向边代表因果关系,并且从因事件指向果事件,很自然的,因果性满足**传递性**。**如果该图中有一条单一的路径能串起所有点,且不存在环**,则该系统是线性一致的。可以看出,因果关系是一种局部特性(也即偏序关系),定义在两个点之间(如果两个点之间存在着一条单向途径,则这两点有因果关系);而线性关系是一种全局特性(也即全序关系),定义在整个图上。
|
||||
>
|
||||
@ -376,15 +376,15 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
|
||||
为了确定因果依赖,我们需要某种手段来描述系统中节点的“**知识**”(knowledge)。如果某个节点在收到 Y 的写入请求时已经看到了值 X,则 X 和 Y 间可能会存在着因果关系。就如在调查公司的欺诈案时,CEO 常被问到,“你在做出 Y 决定时知道 X 吗”?
|
||||
|
||||
确定哪些操作先于哪些些操作发生的方法类似于我们在 “****[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)****”一节讨论的技术。那一节针对无主模型讨论了如何检测针对单个 Key 的并发写入,以防止更新丢失问题。因果一致性所需更多:**需要在整个数据库范围内追踪所有 Key 间操作的因果依赖,而非仅仅单个 Key 上**。**版本向量**(version vectors)常用于此道。
|
||||
确定哪些操作先于哪些些操作发生的方法类似于我们在 “[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)”一节讨论的技术。那一节针对无主模型讨论了如何检测针对单个 Key 的并发写入,以防止更新丢失问题。因果一致性所需更多:**需要在整个数据库范围内追踪所有 Key 间操作的因果依赖,而非仅仅单个 Key 上**。**版本向量**(version vectors)常用于此道。
|
||||
|
||||
为了解决确定因果顺序,数据库需要知道应用读取数据的**版本信息**。这也是为什么在图 5-13 中(参见 ****[确定 Happens-Before 关系](https://ddia.qtmuniao.com/#/ch05?id=%e7%a1%ae%e5%ae%9a-happens-before-%e5%85%b3%e7%b3%bb)****),我们在写入数据时需要知道先前读取操作中数据库返回的版本号。在 SSI 的冲突检测(参见****[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb)****)中也有类似的思想:当一个事务提交时,数据库需要检查其读取集合中的数据版本是否仍然是最新的。为此,数据库需要跟踪一个事务读取了哪些数据的哪些版本。
|
||||
为了解决确定因果顺序,数据库需要知道应用读取数据的**版本信息**。这也是为什么在图 5-13 中(参见 [确定 Happens-Before 关系](https://ddia.qtmuniao.com/#/ch05?id=%e7%a1%ae%e5%ae%9a-happens-before-%e5%85%b3%e7%b3%bb)),我们在写入数据时需要知道先前读取操作中数据库返回的版本号。在 SSI 的冲突检测(参见[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb))中也有类似的思想:当一个事务提交时,数据库需要检查其读取集合中的数据版本是否仍然是最新的。为此,数据库需要跟踪一个事务读取了哪些数据的哪些版本。
|
||||
|
||||
## 序列号定序
|
||||
|
||||
理论上来说,因果关系很重要;但在实践中,追踪所有的因果依赖非常不切实际。在很多应用场景,客户端会先读取大量数据,才会进行写入。并且我们也无从得知,之后的写入和先前有没有关系,和哪些有关系。**显式的追踪**所有**读集合**所带来的开销会非常大。
|
||||
|
||||
不过,我们有一种更简单的手段:**使用序列号(sequence numbers)或者时间戳(timestamps)来给事件定序**。我们不非得用物理时间戳(如日历时钟,time-of-day clock,参见****[日历时钟](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%a5%e5%8e%86%e6%97%b6%e9%92%9f)****),而可以使用逻辑时钟(logic clock),即使用某种算法来产生一系列的数值以关联操作。最简单的,可以用一个计数器来递增地为每个操作安排一个序列号。
|
||||
不过,我们有一种更简单的手段:**使用序列号(sequence numbers)或者时间戳(timestamps)来给事件定序**。我们不非得用物理时间戳(如日历时钟,time-of-day clock,参见[日历时钟](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%a5%e5%8e%86%e6%97%b6%e9%92%9f)),而可以使用逻辑时钟(logic clock),即使用某种算法来产生一系列的数值以关联操作。最简单的,可以用一个计数器来递增地为每个操作安排一个序列号。
|
||||
|
||||
此种序列号和时间戳通常都非常紧凑,只占几个字节,但却能提供一种全序关系。通过给每个操作关联一个序列号,就能比较任何两个操作的先后关系。
|
||||
|
||||
@ -408,7 +408,7 @@ CAP 最初被提出只是一个为了激发数据库取舍讨论的模糊的取
|
||||
由于这些序列号生成方法都不能够很好地捕捉跨节点的操作因果关系,因此都存在因果问题:
|
||||
|
||||
1. **不同节点上处理操作的速率很难完全同步**。因此,如果一个节点使用奇数序号,另一个节点时用偶数序号,则两个序号消耗的速率也会不一致。此时,当你有两个奇偶性不同的序号时,就难以通过比较大小来确定操作发生的先后顺序。
|
||||
2. **物理时间戳会由于多机时钟偏差,而不满足因果一致**。例如,在图 8-3 中(参见****[时间戳以定序](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%b6%e9%97%b4%e6%88%b3%e4%bb%a5%e5%ae%9a%e5%ba%8f)****),就出现了发生在之后的操作被分配了一个较小的时间戳。
|
||||
2. **物理时间戳会由于多机时钟偏差,而不满足因果一致**。例如,在图 8-3 中(参见[时间戳以定序](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%b6%e9%97%b4%e6%88%b3%e4%bb%a5%e5%ae%9a%e5%ba%8f)),就出现了发生在之后的操作被分配了一个较小的时间戳。
|
||||
3. 对于批量分配方式,有可能发生较早的操作被分配了 1001~2000 的序列号,而较晚的操作被分配了 1~1000 的序列号。如此一来,序列号的分配不满足因果一致。
|
||||
|
||||
### Lamport 时间戳
|
||||
@ -433,7 +433,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
只要最大的 counter 值通过每个操作被传播,就能保证 Lamport 时间戳满足因果一致。因为每次因果依赖的交互都会推高时间戳。
|
||||
|
||||
有时候我们会将 Lamport 时间戳和之前提到的**版本向量**(version vector,参见****[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)****)混淆。虽然看起来相似,但其根本目的却是不同:
|
||||
有时候我们会将 Lamport 时间戳和之前提到的**版本向量**(version vector,参见[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b))混淆。虽然看起来相似,但其根本目的却是不同:
|
||||
|
||||
1. 版本向量能够用于检测操作的并发和因果依赖
|
||||
2. Lamport 时间戳只是用于确定全序
|
||||
@ -444,7 +444,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
尽管 Lamport 时间戳能够给出一种能够追踪因果关系的全序时间戳生成算法,但并不足以解决分布式系统中所面临的的很多基本问题。
|
||||
|
||||
举个例子,考虑一个系统,在该系统中,以用户名唯一确定一个账户。如果两个用户并发的用同一个用户名创建账户,则一个成功,另一个失败(参见****[领导者和锁](https://ddia.qtmuniao.com/#/ch08?id=%e9%a2%86%e5%af%bc%e8%80%85%e5%92%8c%e9%94%81)****)。
|
||||
举个例子,考虑一个系统,在该系统中,以用户名唯一确定一个账户。如果两个用户并发的用同一个用户名创建账户,则一个成功,另一个失败(参见[领导者和锁](https://ddia.qtmuniao.com/#/ch08?id=%e9%a2%86%e5%af%bc%e8%80%85%e5%92%8c%e9%94%81))。
|
||||
|
||||
第一感觉,对所有事件进行全序定序(如使用 Lamport 时间戳)能够解决该问题:如果系统收到两个具有相同用户名的账户创建请求,让具有较小时间戳的那个请求成功,让另一个失败。由于所有时间戳满足全序关系,这两个请求的时间戳总是可以比的。
|
||||
|
||||
@ -469,7 +469,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
在分布式系统的语境下,该问题也被称为**全序广播**(total order broadcast)或者**原子广播**(atomic broadcast)。
|
||||
|
||||
> **顺序保证的范围。**多分区的数据库,对于每个分区使用单主模型,能够维持每个分区的操作全局有序,但并不能提供跨分区的一致性保证(比如一致性快照,外键约束)。当然,跨分区的全序保证也是可以提供的,只不过需要进行额外的协调。
|
||||
> **顺序保证的范围。** 多分区的数据库,对于每个分区使用单主模型,能够维持每个分区的操作全局有序,但并不能提供跨分区的一致性保证(比如一致性快照,外键约束)。当然,跨分区的全序保证也是可以提供的,只不过需要进行额外的协调。
|
||||
>
|
||||
|
||||
全序广播是一种多个节点间交换消息的协议。它要求系统满足两个安全性质:
|
||||
@ -485,7 +485,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
在做数据库备份时其实我们真正需要的是全序广播:如果我们让“**消息**”具象为数据库中的**写入**,且每个副本以相同的顺序处理相同的输入集,则每个副本必然会保持一致(除却暂时的临时同步延迟外)。该准则也被称为:**状态机复制**(state machine replication),在第 11 章的时候我们将继续该主题。
|
||||
|
||||
类似的,全序广播也可以用于实现可串行化的事务:如之前****[物理上串行](https://ddia.qtmuniao.com/#/ch07?id=%e7%89%a9%e7%90%86%e4%b8%8a%e4%b8%b2%e8%a1%8c)****提到的,消息在此具象为作为存储过程执行的一个**确定性**的事务,如果所有节点按同样的顺序处理这些消息,则数据中的所有分区和副本最终都会在数据上保持一致。
|
||||
类似的,全序广播也可以用于实现可串行化的事务:如之前[物理上串行](https://ddia.qtmuniao.com/#/ch07?id=%e7%89%a9%e7%90%86%e4%b8%8a%e4%b8%b2%e8%a1%8c)提到的,消息在此具象为作为存储过程执行的一个**确定性**的事务,如果所有节点按同样的顺序处理这些消息,则数据中的所有分区和副本最终都会在数据上保持一致。
|
||||
|
||||
需要注意到,全序广播的一个重要性质是:**当收到消息时,其顺序已经确定**。这是因为,节点不能将后收到的消息,插入之前的已经收到的消息序列。这让全序广播要强于时间戳排序(timestamp order)。
|
||||
|
||||
@ -555,13 +555,13 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
- **原子提交**
|
||||
|
||||
在一个横跨多节点或具有多分区的数据库中,可能会出现某个事务在一些节点执行成功,但在另外一些节点却运行失败。如果我们想保持事务的原子性(ACID 中的 A,参见****[原子性](https://ddia.qtmuniao.com/#/ch07?id=%e5%8e%9f%e5%ad%90%e6%80%a7%ef%bc%88atomicity%ef%bc%89)****),我们就必须让所有节点就事务的结果达成一致:要么全部回滚(只要有故障),要么提交(没有任何故障)。这个共识的特例也被称为**原子提交**(atomic commit)。
|
||||
在一个横跨多节点或具有多分区的数据库中,可能会出现某个事务在一些节点执行成功,但在另外一些节点却运行失败。如果我们想保持事务的原子性(ACID 中的 A,参见[原子性](https://ddia.qtmuniao.com/#/ch07?id=%e5%8e%9f%e5%ad%90%e6%80%a7%ef%bc%88atomicity%ef%bc%89)),我们就必须让所有节点就事务的结果达成一致:要么全部回滚(只要有故障),要么提交(没有任何故障)。这个共识的特例也被称为**原子提交**(atomic commit)。
|
||||
|
||||
|
||||
> **共识的不可能性**。你也许听过 FLP —— 以 Fischer,Lynch 和 Paterson 三位作者姓名首字母命名的一种不可能原理——在网络可靠,但允许节点宕机(即便只有一个)的**异步模型**系统中,不存在总是能够达成共识的算法。在分布式系统中,我们又必须得假设节点可能会宕机,因此稳定可靠的共识算法是不存在的。但是,我们现在却在探讨可以达成共识的算法。这又是为啥?这可能吗?
|
||||
>
|
||||
|
||||
> 答案是,FLP 不可能是基于异步系统模型(参见****[系统模型和现实](https://ddia.qtmuniao.com/#/ch08?id=%e7%b3%bb%e7%bb%9f%e6%a8%a1%e5%9e%8b%e5%92%8c%e7%8e%b0%e5%ae%9e)****)证明的,这是一种非常苛刻的模型,不能够使用任何时钟系统和超时检测。如果允许使用**超时宕机检测**、或者任何可以**识别节点宕机**的方法,就能够实现可靠的共识算法。甚而,只让算法用随机数来进行故障检测,也能够绕过这个不可能定理。
|
||||
> 答案是,FLP 不可能是基于异步系统模型(参见[系统模型和现实](https://ddia.qtmuniao.com/#/ch08?id=%e7%b3%bb%e7%bb%9f%e6%a8%a1%e5%9e%8b%e5%92%8c%e7%8e%b0%e5%ae%9e))证明的,这是一种非常苛刻的模型,不能够使用任何时钟系统和超时检测。如果允许使用**超时宕机检测**、或者任何可以**识别节点宕机**的方法,就能够实现可靠的共识算法。甚而,只让算法用随机数来进行故障检测,也能够绕过这个不可能定理。
|
||||
>
|
||||
|
||||
> 因此,尽管在理论上,FLP 定理非常重要,断言异步网络中共识不可能达到;但在实践中,分布式系统达成共识是可行的。
|
||||
@ -575,18 +575,18 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
在第七章我们探讨过,在多个写操作中途出现故障时,原子性能够对应用层提供一种简单的语义。事务结果是要么成功提交(事务的全部写入都成功持久化),要么全部丢弃(事务的所有写入都被回滚,即取消或者扔掉)。
|
||||
|
||||
原子性能够避免失败的事务通过**半完成**(half-finished)或者**半更新**(half-updated)的结果来破坏数据库系统。这一点对于**多对象事务**(参见****[单对象和多对象操作](https://ddia.qtmuniao.com/#/ch07?id=%e5%8d%95%e5%af%b9%e8%b1%a1%e5%92%8c%e5%a4%9a%e5%af%b9%e8%b1%a1%e6%93%8d%e4%bd%9c)****)和支持**二级索引**的数据库来说尤为重要。二级索引是独立于**原始数据**的一种数据结构,因此如果你更新了原始数据,对应的二级索引也需要进行同步更新。原子性能够保证二级索引和原始数据时刻保持一致。(如果索引不和原始数据保持同步更新,那该索引就失去了其作用)
|
||||
原子性能够避免失败的事务通过**半完成**(half-finished)或者**半更新**(half-updated)的结果来破坏数据库系统。这一点对于**多对象事务**(参见[单对象和多对象操作](https://ddia.qtmuniao.com/#/ch07?id=%e5%8d%95%e5%af%b9%e8%b1%a1%e5%92%8c%e5%a4%9a%e5%af%b9%e8%b1%a1%e6%93%8d%e4%bd%9c))和支持**二级索引**的数据库来说尤为重要。二级索引是独立于**原始数据**的一种数据结构,因此如果你更新了原始数据,对应的二级索引也需要进行同步更新。原子性能够保证二级索引和原始数据时刻保持一致。(如果索引不和原始数据保持同步更新,那该索引就失去了其作用)
|
||||
|
||||
### 从单机到分布式的原子提交
|
||||
|
||||
对于运行在单机数据上的事务,原子提交通常由存储引擎层来实现。当客户端请求数据库节点提交事务时,数据库会首先将事务所涉及到的写入进行持久化(通常通过写前日志 WAL 的方式,参见****[让 B 树更可靠](https://ddia.qtmuniao.com/#/ch03?id=%e8%ae%a9-b-%e6%a0%91%e6%9b%b4%e5%8f%af%e9%9d%a0)****),事务结束时在硬盘上追加一个特殊的**提交记录**(commit record)到日志上。如果数据库在处理事务的过程中宕机了,在重启时会从日志上对事务进行恢复:
|
||||
对于运行在单机数据上的事务,原子提交通常由存储引擎层来实现。当客户端请求数据库节点提交事务时,数据库会首先将事务所涉及到的写入进行持久化(通常通过写前日志 WAL 的方式,参见[让 B 树更可靠](https://ddia.qtmuniao.com/#/ch03?id=%e8%ae%a9-b-%e6%a0%91%e6%9b%b4%e5%8f%af%e9%9d%a0)),事务结束时在硬盘上追加一个特殊的**提交记录**(commit record)到日志上。如果数据库在处理事务的过程中宕机了,在重启时会从日志上对事务进行恢复:
|
||||
|
||||
1. 如果在宕机前,提交记录已经追加到磁盘上,则该事务被认为已经成功提交。
|
||||
2. 否则,该事务所有的写入将会被回滚。
|
||||
|
||||
因此,在单机数据库里,事务是否提交主要取决于数据持久化到磁盘的顺序:**首先是数据,接着是提交记录**。提交事务还是中止事务,决定性时刻在于**提交记录成功刷盘**的那一瞬间:在此之前,事务可能会被中止(由于宕机);在此之后,该事务一定会被提交(即使宕机)。也即,是唯一的硬件设备(某个特定节点上的某个具体的磁盘驱动)保证了提交的原子性。
|
||||
|
||||
然而,当事务涉及到多个节点时又当如何?例如,一个跨分区的多对象事务,或者基于关键词分区的二级索引(在该情况下,索引数据和基础数据可能不在一个分区里,参见****[分片和次级索引](https://ddia.qtmuniao.com/#/ch06?id=%e5%88%86%e7%89%87%e5%92%8c%e6%ac%a1%e7%ba%a7%e7%b4%a2%e5%bc%95)****)。大多数 “NoSQL” 分布式存储不支持这种跨节点的分布式事务,但很多分布式关系型数据库则支持。
|
||||
然而,当事务涉及到多个节点时又当如何?例如,一个跨分区的多对象事务,或者基于关键词分区的二级索引(在该情况下,索引数据和基础数据可能不在一个分区里,参见[分片和次级索引](https://ddia.qtmuniao.com/#/ch06?id=%e5%88%86%e7%89%87%e5%92%8c%e6%ac%a1%e7%ba%a7%e7%b4%a2%e5%bc%95))。大多数 “NoSQL” 分布式存储不支持这种跨节点的分布式事务,但很多分布式关系型数据库则支持。
|
||||
|
||||
在上述场景中,只是简单地在提交事务时给每个节点发送提交请求让其提交事务,是不能够满足事务基本要求的。这是因为,可能有的节点成功提交了,有的节点却提交失败了,从而违反了原子性保证:
|
||||
|
||||
@ -596,7 +596,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
如果有些节点提交了该事务,但另外的一些节点却中止该事务了,多个节点间就会处于不一致的状态。而且,一旦事务在一个节点上提交了(即便之后发现了该事务在其他节点上失败了)就难以进行撤销。由于这个原因,我们需要仅在确信所有相关节点都能成功提交时,本节点才能提交。
|
||||
|
||||
**事务提交后是不可撤销的**——在事务提交后,你不能再改变主意说,我要重新中止这个事务。这是因为,一旦事务提交了,就会对其他事务可见,从而可能让其他事务依赖于该事务的结果做出一些新的决策;这个原则构成了**读已提交**(read commited)隔离级别的基础(参见****[读已提交](https://ddia.qtmuniao.com/#/ch07?id=%e8%af%bb%e5%b7%b2%e6%8f%90%e4%ba%a4)****)。如果事务允许在提交后中止,其他已经读取了该事务结果的事务也会失效,从而引起事务的级联中止。
|
||||
**事务提交后是不可撤销的**——在事务提交后,你不能再改变主意说,我要重新中止这个事务。这是因为,一旦事务提交了,就会对其他事务可见,从而可能让其他事务依赖于该事务的结果做出一些新的决策;这个原则构成了**读已提交**(read commited)隔离级别的基础(参见[读已提交](https://ddia.qtmuniao.com/#/ch07?id=%e8%af%bb%e5%b7%b2%e6%8f%90%e4%ba%a4))。如果事务允许在提交后中止,其他已经读取了该事务结果的事务也会失效,从而引起事务的级联中止。
|
||||
|
||||
当然,事务所造成的**结果**在事实上是可以被撤销的,比如,通过**补偿事务**(*compensating transaction*)。但,从数据库的视角来看,这就是另外一个事务了;而跨事务的正确性,需要应用层自己来保证。
|
||||
|
||||
@ -608,7 +608,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
![Untitled](img/ch09-fig09.png)
|
||||
|
||||
> **不要混淆 2PC 和 2PL**。Two-phase *commit* (2PC) 和 two-phase locking **(2PL,参见****[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81)****) 是两个完全不同的概念。2PC 是为了在分布式系统中进行原子提交,而 2PL 是为了进行事务并发控制的一种加锁方式。为了避免歧义,可以忽略他们在名字简写上的相似性,而把它们当成完全不同的概念。
|
||||
> **不要混淆 2PC 和 2PL**。Two-phase *commit* (2PC) 和 two-phase locking (2PL,参见[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81)) 是两个完全不同的概念。2PC 是为了在分布式系统中进行原子提交,而 2PL 是为了进行事务并发控制的一种加锁方式。为了避免歧义,可以忽略他们在名字简写上的相似性,而把它们当成完全不同的概念。
|
||||
>
|
||||
|
||||
2PC 引入了一个单机事务中没有的角色:**协调者**(coordinator,有时也被称为事务管理器,transaction manager)。协调者通常以库的形式出现,并会嵌入到请求事务的应用进程中,但当然,它也可以以单独进程或者服务的形式出现。比如说,Narayana, JOTM, BTM, or MSDTC.
|
||||
@ -628,7 +628,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
1. 当应用想开启一个分布式事务时,它会首先向协调者要一个**事务 ID**。该事务 ID 是全局唯一的。
|
||||
2. 应用会使用前述事务 ID 向所有的参与者发起一个单机事务,所有节点会各自完成读写请求,在此过程中,如果有任何出错(比如节点宕机或者请求超时),协调者或者任意参与者都可以中止事务。
|
||||
3. 当应用层准备好提交事务时,协调者会向所有参与者发送**准备提交**( prepare) ****请求,并在请求中打上事务 ID 标记。如果有请求失败或者超时,则协调者会对所有参与者发送带有该事务 ID 的中止请求。
|
||||
3. 当应用层准备好提交事务时,协调者会向所有参与者发送**准备提交**( prepare) 请求,并在请求中打上事务 ID 标记。如果有请求失败或者超时,则协调者会对所有参与者发送带有该事务 ID 的中止请求。
|
||||
4. 当参与者收到**准备提交**请求时,它必须确认该事务能够在任何情况下都能被提交,才能回复“**可以**”。这包括,将所有写入刷到磁盘(一旦承诺了,就不能反悔,即使之后遇到宕机、断电或者磁盘空间不足)、检查是否有冲突或者违反约束的情况。换句话说,如果回复“可以”,意味着参与者**让渡了中止事务的权利(给协调者)**,但此时并没有真正地提交。
|
||||
5. 当协调者收到所有参与者准备提交的回复后,会决定提交还是中止该事务(只有在所有参与者都回复“可以”时,才会提交)。协调者需要将该决策写入事务日志,并下刷到磁盘,以保证即使宕机重启,该决策也不会丢失。这被称为**提交点**(commit point)。
|
||||
6. 协调者将决策刷入了磁盘后,就会将决策(提交或者中止)请求发给所有参与方。如果某个请求失败或者超时,则协调者会对其进行无限重试,直到成功。不允许走回头路:如果协调者决定了提交,则不管要进行多少次的重试,也必须要保证该决策的执行。如果参与者在此时宕机了,则当重启时也必须进行提交——因为它**承诺过要提交**,因此在重启后不能拒绝提交。
|
||||
@ -665,7 +665,7 @@ Lamport 时间戳不依赖于物理时钟,但可以提供全序保证,对于
|
||||
|
||||
由于 2PC 在等待协调者宕机恢复时系统可能会卡住,因此两阶段提交又称为**阻塞式原子提交协议**(blocking atomic commit protocol)。理论上,可以让使用某种办法让原子提交协议成为非阻塞的,从而在协调者宕机时,系统不会卡住。然而,在实践中该办法很不直观。
|
||||
|
||||
作为 2PC 的替代,人们又提出了三阶段提交(three-phase commit)。然而,3PC 对系统有一定假设:网络具有有界延迟,请求延迟也是有界的(bounded,参见****[超时和无界延迟)](https://ddia.qtmuniao.com/#/ch08?id=%e8%b6%85%e6%97%b6%e5%92%8c%e6%97%a0%e7%95%8c%e5%bb%b6%e8%bf%9f%ef%bc%88unbounded-delays%ef%bc%89)****。在具有无界网络延迟进程停顿的实际系统中,3PC 无法保证原子性。
|
||||
作为 2PC 的替代,人们又提出了三阶段提交(three-phase commit)。然而,3PC 对系统有一定假设:网络具有有界延迟,请求延迟也是有界的(bounded,参见[超时和无界延迟](https://ddia.qtmuniao.com/#/ch08?id=%e8%b6%85%e6%97%b6%e5%92%8c%e6%97%a0%e7%95%8c%e5%bb%b6%e8%bf%9f%ef%bc%88unbounded-delays%ef%bc%89))。在具有无界网络延迟进程停顿的实际系统中,3PC 无法保证原子性。
|
||||
|
||||
一般来说,非阻塞的原子提交依赖于一个**完美的故障检测器**(perfect failure detector)——即,一种可以判断某个节点是否宕机的可靠机制。在具有无界延迟的网络中,超时机制就不是一个可靠的故障检测方法,即使没有任何节点故障,一个请求仍会由于网络问题而超时。出于这个原因,即使 2PC 可能会因为协调者宕机卡住,但人们仍然在使用它,而没有转向 3PC。
|
||||
|
||||
@ -723,7 +723,7 @@ XA 不是一个网络协议——它定义了一组和事务协调者交互的 C
|
||||
|
||||
为什么我们这么关心事务的参与者在**未定状态**时卡住呢?系统的其他部分不能够无视该未定事务而继续干自己的事情吗?反正该未定事务最终会被处理。
|
||||
|
||||
问题的关键点在于存在**锁**(locking)。在[读已提交](https://ddia.qtmuniao.com/#/ch07?id=%e8%af%bb%e5%b7%b2%e6%8f%90%e4%ba%a4)一小节中,数据库中的事务通常会使用行级别的互斥锁来保护对某一行的修改,以防止脏写。更进一步,如果想获得可串行化隔离级别,数据库在使用两阶段锁进行实现时,会对事务所有读过的行加共享锁(参见****[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81)****)。
|
||||
问题的关键点在于存在**锁**(locking)。在[读已提交](https://ddia.qtmuniao.com/#/ch07?id=%e8%af%bb%e5%b7%b2%e6%8f%90%e4%ba%a4)一小节中,数据库中的事务通常会使用行级别的互斥锁来保护对某一行的修改,以防止脏写。更进一步,如果想获得可串行化隔离级别,数据库在使用两阶段锁进行实现时,会对事务所有读过的行加共享锁(参见[两阶段锁](https://ddia.qtmuniao.com/#/ch07?id=%e4%b8%a4%e9%98%b6%e6%ae%b5%e9%94%81))。
|
||||
|
||||
数据库在提交或者中止事务前**不能够释放获取的这些锁**。因此,在使用两阶段提交时,一个事务必须在其处于未定状态期间一直持有锁。如果协调者在宕机后花了 20 分钟才重新启动起来,则对应参与者的锁就要持有 20 分钟。如果参与者日志由于某种原因丢掉了,这些锁会被永远的持有——除非系统管理员会手动释放它们。
|
||||
|
||||
@ -745,7 +745,7 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
|
||||
|
||||
- 如果协调者没有使用多副本机制,仅运行在一台机器上,则它会成为系统的一个**单点**(因为它的宕机会造成存疑的参与者,进而阻塞其他应用服务的继续运行)。然而,令人惊讶的是,很多协调者的实现要么默认不是高可用的,要么只提供了很粗糙的冗余支持。
|
||||
- 很多服务端应用本身被设计为**无状态的**(stateless,HTTP 比较偏好无状态,如 Restful 设计风格)的,然后将状态都外存到数据库中。这样做的好处是,应用侧进程可以随意增删,按需扩展和收缩。但如果事务的协调者成为了应用层的一部分,就改变了这个本质设定。协调者的日志变成了应用层一个至关重要的、需要持久化的状态——需要像数据库一样按同等重要性对待。因为在宕机重启后,参与者会利用这些日志来推进卡住的参与者。由是,应用层不再是无状态的。
|
||||
- 由于 XA 需要和足够广泛的数据系统进行适配,因此其 API 只能维持一个**最小公共接口集**,由此带来了 XA 在能力上的**诸多限制**。如,XA 不能在跨系统检测死锁,因为这要求增加一种可以获取所有正在等待的锁信息接口(需要使用 Wait-For-Graph 死锁检测);XA 也不能提供跨系统的 SSI 隔离级别(参见****[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb)****),因为这要求支持一种可以跨系统监测冲突的协议(SSI 要在 SI 的基础上进行读写冲突检测)。
|
||||
- 由于 XA 需要和足够广泛的数据系统进行适配,因此其 API 只能维持一个**最小公共接口集**,由此带来了 XA 在能力上的**诸多限制**。如,XA 不能在跨系统检测死锁,因为这要求增加一种可以获取所有正在等待的锁信息接口(需要使用 Wait-For-Graph 死锁检测);XA 也不能提供跨系统的 SSI 隔离级别(参见[可串行的快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e5%8f%af%e4%b8%b2%e8%a1%8c%e7%9a%84%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb)),因为这要求支持一种可以跨系统监测冲突的协议(SSI 要在 SI 的基础上进行读写冲突检测)。
|
||||
- 对于数据库的**内部分布式事务**(非 XA),就没有这些限制——例如,可以提供分布式版本的 SSI 。然而,要成功地提交一个 2PC 事务仍有诸多问题:所有的参与者必须要回复(但可以异步回应)。因此,一旦系统内任何子模块损坏了,则事务也随之失败。从这个角度来说,分布式事务有**放大故障**的嫌疑,这与我们构建容错系统的目标背道而驰(这就是 tradeoff,为上层提供的更多的一致性保证,就会牺牲性能,降低可用性)。
|
||||
|
||||
上述事实是否意味着我们应该放弃让不同系统保持一致的努力?不尽然,有很多其他方法,既可以让我们达到同样的目标,而又不必引入异构分布式事务的痛点。我们在第 11 章和 12 章会回到对这个问题的讨论。现在让我们先把共识问题这个主题讲完。
|
||||
@ -779,15 +779,15 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
|
||||
|
||||
如果**不关心容错性,则仅满足前三个性质就足够**了:比如,可以通过硬编码指定某个节点为“独裁者”,并且让其做所有决策,其他节点只要服从即可。然而,一旦该节点故障,则整个系统不能继续决策和推进。事实上,这正是我们在两阶段提交算法中看到的:一旦协调者故障,所有处于未定状态的参与者都无法独自决策是提交还是中止。
|
||||
|
||||
**可终止性是对容错的一种形式化描述**(从结果来描述)。它本质上是在说,一个共识算法不能让系统陷入一种卡在那、啥也不干,直到永远的状态。换句话说,系统必须能够正常运作,即使有些节点宕机,其他节点也必须能够继续做出决策。(可结束性是存活性,liveness,而其他三个性质是安全性,safety,参见****[安全性和存活性](https://ddia.qtmuniao.com/#/ch08?id=%e5%ae%89%e5%85%a8%e6%80%a7%e5%92%8c%e5%ad%98%e6%b4%bb%e6%80%a7)**** )。
|
||||
**可终止性是对容错的一种形式化描述**(从结果来描述)。它本质上是在说,一个共识算法不能让系统陷入一种卡在那、啥也不干,直到永远的状态。换句话说,系统必须能够正常运作,即使有些节点宕机,其他节点也必须能够继续做出决策。(可结束性是存活性,liveness,而其他三个性质是安全性,safety,参见[安全性和存活性](https://ddia.qtmuniao.com/#/ch08?id=%e5%ae%89%e5%85%a8%e6%80%a7%e5%92%8c%e5%ad%98%e6%b4%bb%e6%80%a7) )。
|
||||
|
||||
该模型对节点宕机做了最坏的假设——**一旦节点宕机,就会凭空消失,再也不会回来**。适用于该模型的场景不是软件故障造成的宕机,而是由火山喷发、地震等造成的数据中心不可逆转的损坏。在该系统模型下,任何需要等待节点回复的算法都不可能满足可终止性。具体来说,在这种设定下,2PC 就不满足可结束性要求。
|
||||
|
||||
当然,如果所有节点都宕机,则任何算法都不可能做出任何决策。共识算法有其能够承受的宕机节点数上限:事实上,可以证明,任何共识算法都要求**多数节点存活**,以确保正常运行,满足可终止性。多数派节点可以安全的构成一个**法定多数**(quorum,参见****[Quorum 读写](https://ddia.qtmuniao.com/#/ch05?id=quorum-%e8%af%bb%e5%86%99)****)。
|
||||
当然,如果所有节点都宕机,则任何算法都不可能做出任何决策。共识算法有其能够承受的宕机节点数上限:事实上,可以证明,任何共识算法都要求**多数节点存活**,以确保正常运行,满足可终止性。多数派节点可以安全的构成一个**法定多数**(quorum,参见[Quorum 读写](https://ddia.qtmuniao.com/#/ch05?id=quorum-%e8%af%bb%e5%86%99))。
|
||||
|
||||
因此,可终止性受限于少于半数节点宕机或不可达的假设。然而,大多数共识算法的实现在大多数节点都宕机或者网络出现大范围故障时仍然能保持安全性——一致性,正直性和有效性。也即,大范围的节点下线可能会让系统**不能继续处理请求**,但**不会因此破坏共识协议**,让其做出不合法决策。
|
||||
|
||||
大多数共识算法会假设系统中**不存在拜占庭故障**(参见****[拜占庭错误](https://ddia.qtmuniao.com/#/ch08?id=%e6%8b%9c%e5%8d%a0%e5%ba%ad%e9%94%99%e8%af%af)****)。即如果某些节点故意不遵守协议(例如,对不同节点返回完全不同的信息),就有可能破坏协议的安全性。当然, 我们也有办法让系统足够鲁棒以容忍拜占庭错误,但就得要求集群中不能有超过三分之一的恶意节点(具有拜占庭错误的节点),但本书中没有足够精力来详细讨论这种算法的细节了。
|
||||
大多数共识算法会假设系统中**不存在拜占庭故障**(参见[拜占庭错误](https://ddia.qtmuniao.com/#/ch08?id=%e6%8b%9c%e5%8d%a0%e5%ba%ad%e9%94%99%e8%af%af))。即如果某些节点故意不遵守协议(例如,对不同节点返回完全不同的信息),就有可能破坏协议的安全性。当然, 我们也有办法让系统足够鲁棒以容忍拜占庭错误,但就得要求集群中不能有超过三分之一的恶意节点(具有拜占庭错误的节点),但本书中没有足够精力来详细讨论这种算法的细节了。
|
||||
|
||||
### 全序广播中的共识算法
|
||||
|
||||
@ -809,11 +809,11 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
|
||||
|
||||
### 单主复制和共识协议
|
||||
|
||||
在第五章,我们讨论了基于单主模型的复制协议(参见****[领导者与跟随者](https://ddia.qtmuniao.com/#/ch05?id=%e9%a2%86%e5%af%bc%e8%80%85%e4%b8%8e%e8%b7%9f%e9%9a%8f%e8%80%85)****),在该模型中,主节点会接管所有写入,并且以同样的顺序复制给从节点,以此保持所有副本的数据一致。这本质上不也是全序广播么?为什么我们在第五章不需要考虑共识问题呢?
|
||||
在第五章,我们讨论了基于单主模型的复制协议(参见[领导者与跟随者](https://ddia.qtmuniao.com/#/ch05?id=%e9%a2%86%e5%af%bc%e8%80%85%e4%b8%8e%e8%b7%9f%e9%9a%8f%e8%80%85)),在该模型中,主节点会接管所有写入,并且以同样的顺序复制给从节点,以此保持所有副本的数据一致。这本质上不也是全序广播么?为什么我们在第五章不需要考虑共识问题呢?
|
||||
|
||||
该问题的核心点在于**主节点(领导者)是怎样选出的**。如果主节点由运维团队的管理员手动配置,你本质上就获得了一个“共识算法”的独裁变种:只有一个节点允许接受写入(决定复制日志中所有日志的顺序),并且一旦该主节点宕机,系统便会陷入不可用的状态,直到运维人员手动的配置另外一个节点为主节点。这样的系统在实践中也可以正常运作,但是并不满足共识算法中的可终止性,因为它在停顿后要求运维人员的干预,才能继续运转。
|
||||
|
||||
有些数据库在遇到主节点故障时,会自动地重新进行主选举,将一个从节点提升为新的主节点(参见****[宕机处理](https://ddia.qtmuniao.com/#/ch05?id=%e5%ae%95%e6%9c%ba%e5%a4%84%e7%90%86)****)。这就让我们进一步逼近了可容错的全序广播,并且解决了共识问题。
|
||||
有些数据库在遇到主节点故障时,会自动地重新进行主选举,将一个从节点提升为新的主节点(参见[宕机处理](https://ddia.qtmuniao.com/#/ch05?id=%e5%ae%95%e6%9c%ba%e5%a4%84%e7%90%86))。这就让我们进一步逼近了可容错的全序广播,并且解决了共识问题。
|
||||
|
||||
但,**这中间有个循环依赖的问题**。我们之前讨论了脑裂(split brain)问题,并且断言所有的节点必须就谁是领导者达成一致——否则,如果有两个不同节点都认为自己是领导者,则会有多个写入点,进而让数据库陷入不一致的状态。因此,我们需要共识算法来进行选主。但我们说共识算法本质上可以描述为全序广播算法,然后全序广播算法又和单主复制一样,然后单主复制又依赖时刻保证单个主,然后…
|
||||
|
||||
@ -825,9 +825,9 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
|
||||
|
||||
每次当前的主节点被认为下线时(可能是宕机,也可能只是网络不通),所有认为该主下线的节点就会发起选举,以选出新的主节点。每次选举会使用一个更高的纪元编号,因此所有的纪元编号是全序且单调递增的。如果不同纪元中有两个节点都认为自己是主(比如之前的主节点并没有宕机),则具有较高纪元编号的主节点胜出。
|
||||
|
||||
在一个主节点被授权做任何事之前,它必须要确认不会有更权威的主节点(具有更高的纪元编号)会做出不同决策。那该一个主节点如何知道自己没有被其他节点“赶下台”呢?会议一下,我们在****[真相由多数派定义](https://ddia.qtmuniao.com/#/ch08?id=%e7%9c%9f%e7%9b%b8%e7%94%b1%e5%a4%9a%e6%95%b0%e6%b4%be%e5%ae%9a%e4%b9%89)****一节中讨论过的:分布式系统中,一个节点不能无脑相信自己的判断——因为**一个节点认为自己是主,不意味着其他节点也都认可这一点**。
|
||||
在一个主节点被授权做任何事之前,它必须要确认不会有更权威的主节点(具有更高的纪元编号)会做出不同决策。那该一个主节点如何知道自己没有被其他节点“赶下台”呢?会议一下,我们在**[真相由多数派定义](https://ddia.qtmuniao.com/#/ch08?id=%e7%9c%9f%e7%9b%b8%e7%94%b1%e5%a4%9a%e6%95%b0%e6%b4%be%e5%ae%9a%e4%b9%89)**一节中讨论过的:分布式系统中,一个节点不能无脑相信自己的判断——因为**一个节点认为自己是主,不意味着其他节点也都认可这一点**。
|
||||
|
||||
因此,主节点在决策前需要首先从所有节点获得法定票数(参见****[Quorum 读写](https://ddia.qtmuniao.com/#/ch05?id=quorum-%e8%af%bb%e5%86%99)****)。对于每个决策,主节点都必须将其作为提案发给其他所有节点,并且等待法定节点的同意。法定节点通常来说,会包含多数派节点,但也不绝对(****[Flexible Paxos](https://arxiv.org/abs/1608.06696)****介绍了一种不需要多数节点的放宽的 Paxos 算法)。如果法定节点的回复中没有任何更高纪元的,则当前主节点可以放心的认为没有发生新纪元的主选举,并可以据此认为他仍然“握有领导权”。从而,可以安全的对提案进行决策。
|
||||
因此,主节点在决策前需要首先从所有节点获得法定票数(参见[Quorum 读写](https://ddia.qtmuniao.com/#/ch05?id=quorum-%e8%af%bb%e5%86%99))。对于每个决策,主节点都必须将其作为提案发给其他所有节点,并且等待法定节点的同意。法定节点通常来说,会包含多数派节点,但也不绝对(**[Flexible Paxos](https://arxiv.org/abs/1608.06696)**介绍了一种不需要多数节点的放宽的 Paxos 算法)。如果法定节点的回复中没有任何更高纪元的,则当前主节点可以放心的认为没有发生新纪元的主选举,并可以据此认为他仍然“握有领导权”。从而,可以安全的对提案进行决策。
|
||||
|
||||
该投票过程非常像两阶段提交提交算法。最大的区别在于:
|
||||
|
||||
@ -842,7 +842,7 @@ XA 事务解决了一些很现实而重要的难题:让异构的数据系统
|
||||
|
||||
然而,共识算法并非银弹,因为这些收益都是有代价的。
|
||||
|
||||
**同步复制损失性能**。每次进行决策(更改数据)前都要让多数节点进行投票,意味着这是一个同步复制系统。在****[同步复制和异步复制](https://ddia.qtmuniao.com/#/ch05?id=%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6%e5%92%8c%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6)****一节中我们讲过,很多数据库都会配置为异步复制。在这种配置下,有些已经提交的数据在进行恢复时可能会丢失,但很多人仍然选择这种模式——承担这种风险,以换取更好的性能。
|
||||
**同步复制损失性能**。每次进行决策(更改数据)前都要让多数节点进行投票,意味着这是一个同步复制系统。在[同步复制和异步复制](https://ddia.qtmuniao.com/#/ch05?id=%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6%e5%92%8c%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6)一节中我们讲过,很多数据库都会配置为异步复制。在这种配置下,有些已经提交的数据在进行恢复时可能会丢失,但很多人仍然选择这种模式——承担这种风险,以换取更好的性能。
|
||||
|
||||
**多数派会增加系统冗余**。共识系统总是要求有**严格多数节点**存活才能正常运行。这意味着,如果你要容忍单节点故障就至少需要三个节点(三节点中的两个节点可以组成多数派),如果要容忍两个节点故障就至少需要五个节点(五个节点中的三个节点组成多数派)。如果网络故障切断了其中一些节点和其他节点的联系,则只有连通的多数派节点可以正常运行,其他节点都会被阻塞。
|
||||
|
||||
@ -873,7 +873,7 @@ Zookeeper 是模仿 Google 的 Chunk 锁服务实现的,不仅实现了全序
|
||||
|
||||
- **操作的全序保证(zxid)**
|
||||
|
||||
在****[领导者和锁](https://ddia.qtmuniao.com/#/ch08?id=%e9%a2%86%e5%af%bc%e8%80%85%e5%92%8c%e9%94%81)****一节中我们讨论过,当某个资源被锁或者租约保护时,你需要**防护令牌**机制来防止由于进程停顿而造成的加锁冲突。防护令牌一个在每次获取锁都会单调自增的数值。Zookeeper 通过给每个操作赋予一个全局自增的事务 id(zxid)和一个版本号(cversion)来提供该功能。
|
||||
在[领导者和锁](https://ddia.qtmuniao.com/#/ch08?id=%e9%a2%86%e5%af%bc%e8%80%85%e5%92%8c%e9%94%81)一节中我们讨论过,当某个资源被锁或者租约保护时,你需要**防护令牌机制**来防止由于进程停顿而造成的加锁冲突。防护令牌一个在每次获取锁都会单调自增的数值。Zookeeper 通过给每个操作赋予一个全局自增的事务 id(zxid)和一个版本号(cversion)来提供该功能。
|
||||
|
||||
- **故障检测(ephemeral node)**
|
||||
|
||||
@ -912,4 +912,4 @@ ZooKeeper 及类似服务可以视为**成员服务**(membership services)
|
||||
|
||||
成员服务可以确定当前**集群中哪些节点当前时存活的**。如第八章中所说,在具有无界延迟的网络中,不可能可靠的检测出一个节点是否故障。然而,如果你综合使用故障检测和共识算法,所有节点能够对哪些节点存活这件事达成共识。
|
||||
|
||||
使用共识协议也有可能错将一个节点认为下线了,尽管它事实上是存活的。但尽管如此,只要系统能够对当前系统包含哪些节点达成共识,就仍然很有用处。例如,选主算法可以是——在系统当前所有节点中选一个具有最小标号的节点。如果所有节点对系统当前包含哪些节点存在分歧,则这种方法就不能正常工作(不同节点眼中的的最小编号节点可能不一致,从而让大家选出的主不一致)。
|
||||
使用共识协议也有可能错将一个节点认为下线了,尽管它事实上是存活的。但尽管如此,只要系统能够对当前系统包含哪些节点达成共识,就仍然很有用处。例如,选主算法可以是——在系统当前所有节点中选一个具有最小标号的节点。如果所有节点对系统当前包含哪些节点存在分歧,则这种方法就不能正常工作(不同节点眼中的的最小编号节点可能不一致,从而让大家选出的主不一致)。
|
||||
|
Loading…
Reference in New Issue
Block a user