mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
Merge pull request #123 from yingang/master
translation updates (chapter 9, TOC in readme, glossary, etc.)
This commit is contained in:
commit
b9d3f4e854
58
README.md
58
README.md
@ -45,23 +45,77 @@
|
||||
### [第一部分:数据系统的基石](part-i.md)
|
||||
|
||||
* [第一章:可靠性、可伸缩性、可维护性](ch1.md)
|
||||
* [关于数据系统的思考](ch1.md#关于数据系统的思考)
|
||||
* [可靠性](ch1.md#可靠性)
|
||||
* [可伸缩性](ch1.md#可伸缩性)
|
||||
* [可维护性](ch1.md#可维护性)
|
||||
* [本章小结](ch1.md#本章小结)
|
||||
* [第二章:数据模型与查询语言](ch2.md)
|
||||
* [关系模型与文档模型](ch2.md#关系模型与文档模型)
|
||||
* [数据查询语言](ch2.md#数据查询语言)
|
||||
* [图数据模型](ch2.md#图数据模型)
|
||||
* [本章小结](ch2.md#本章小结)
|
||||
* [第三章:存储与检索](ch3.md)
|
||||
* [驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)
|
||||
* [事务处理还是分析?](ch3.md#事务处理还是分析?)
|
||||
* [列存储](ch3.md#列存储)
|
||||
* [本章小结](ch3.md#本章小结)
|
||||
* [第四章:编码与演化](ch4.md)
|
||||
* [编码数据的格式](ch4.md#编码数据的格式)
|
||||
* [数据流的类型](ch4.md#数据流的类型)
|
||||
* [本章小结](ch4.md#本章小结)
|
||||
|
||||
### [第二部分:分布式数据](part-ii.md)
|
||||
|
||||
* [第五章:复制](ch5.md)
|
||||
* [领导者与追随者](ch5.md#领导者与追随者)
|
||||
* [复制延迟问题](ch5.md#复制延迟问题)
|
||||
* [多主复制](ch5.md#多主复制)
|
||||
* [无主复制](ch5.md#无主复制)
|
||||
* [本章小结](ch5.md#本章小结)
|
||||
* [第六章:分区](ch6.md)
|
||||
* [分区与复制](ch6.md#分区与复制)
|
||||
* [键值数据的分区](ch6.md#键值数据的分区)
|
||||
* [分区与次级索引](ch6.md#分区与次级索引)
|
||||
* [分区再平衡](ch6.md#分区再平衡)
|
||||
* [请求路由](ch6.md#请求路由)
|
||||
* [本章小结](ch6.md#本章小结)
|
||||
* [第七章:事务](ch7.md)
|
||||
* [事务的棘手概念](ch7.md#事务的棘手概念)
|
||||
* [弱隔离级别](ch7.md#弱隔离级别)
|
||||
* [可串行化](ch7.md#可串行化)
|
||||
* [本章小结](ch7.md#本章小结)
|
||||
* [第八章:分布式系统的麻烦](ch8.md)
|
||||
* [故障与部分失效](ch8.md#故障与部分失效)
|
||||
* [不可靠的网络](ch8.md#不可靠的网络)
|
||||
* [不可靠的时钟](ch8.md#不可靠的时钟)
|
||||
* [知识、真相与谎言](ch8.md#知识、真相与谎言)
|
||||
* [本章小结](ch8.md#本章小结)
|
||||
* [第九章:一致性与共识](ch9.md)
|
||||
* [一致性保证](ch9.md#一致性保证)
|
||||
* [线性一致性](ch9.md#线性一致性)
|
||||
* [顺序保证](ch9.md#顺序保证)
|
||||
* [分布式事务与共识](ch9.md#分布式事务与共识)
|
||||
* [本章小结](ch9.md#本章小结)
|
||||
|
||||
### [第三部分:衍生数据](part-iii.md)
|
||||
|
||||
* [第十章:批处理](ch10.md)
|
||||
* [使用Unix工具的批处理](ch10.md#使用Unix工具的批处理)
|
||||
* [MapReduce和分布式文件系统](ch10.md#MapReduce和分布式文件系统)
|
||||
* [MapReduce之后](ch10.md#MapReduce之后)
|
||||
* [本章小结](ch10.md#本章小结)
|
||||
* [第十一章:流处理](ch11.md)
|
||||
* [传递事件流](ch11.md#传递事件流)
|
||||
* [流与数据库](ch11.md#流与数据库)
|
||||
* [流处理](ch11.md#流处理)
|
||||
* [本章小结](ch11.md#本章小结)
|
||||
* [第十二章:数据系统的未来](ch12.md)
|
||||
* [数据集成](ch12.md#数据集成)
|
||||
* [分拆数据库](ch12.md#分拆数据库)
|
||||
* [将事情做正确](ch12.md#将事情做正确)
|
||||
* [做正确的事情](ch12.md#做正确的事情)
|
||||
* [本章小结](ch12.md#本章小结)
|
||||
|
||||
### [术语表](glossary.md)
|
||||
|
||||
@ -88,7 +142,7 @@
|
||||
4. [第一部分](part-i.md)前言,[ch2](ch2.md)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
|
||||
5. [词汇表](glossary.md)、[后记]()关于野猪的部分 by @[Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
|
||||
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本与转换脚本 by [@afunTW](https://github.com/afunTW)
|
||||
7. [对第各章进行大量翻译更正润色](https://github.com/Vonng/ddia/pull/118) by [@yingang](https://github.com/yingang)
|
||||
7. [对各章进行大量翻译更正润色](https://github.com/Vonng/ddia/pull/118) by [@yingang](https://github.com/yingang)
|
||||
8. 感谢所有作出贡献,提出意见的朋友们:
|
||||
|
||||
<details>
|
||||
@ -99,7 +153,7 @@
|
||||
| [121](https://github.com/Vonng/ddia/pull/121) | [@yingang](https://github.com/yingang) | translation updates (chapter 5 to chapter 8) |
|
||||
| [120](https://github.com/Vonng/ddia/pull/120) | [@jiong-han](https://github.com/jiong-han) | Typo fix: 呲之以鼻 -> 嗤之以鼻 |
|
||||
| [119](https://github.com/Vonng/ddia/pull/119) | [@cclauss](https://github.com/cclauss) | Streamline file operations in convert() |
|
||||
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 and 3) |
|
||||
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 and 4) |
|
||||
| [117](https://github.com/Vonng/ddia/pull/117) | [@feeeei](https://github.com/feeeei) | 统一每章的标题格式 |
|
||||
| [115](https://github.com/Vonng/ddia/pull/115) | [@NageNalock](https://github.com/NageNalock) | 第七章病句修改: 重复词语 |
|
||||
| [114](https://github.com/Vonng/ddia/pull/114) | [@Sunt-ing](https://github.com/Sunt-ing) | Update README.md: correct the book name |
|
||||
|
2
ch10.md
2
ch10.md
@ -347,7 +347,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
如果你有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话cookie,用户ID或类似的标识符作为分组键,以将特定用户的所有活动事件放在一起来实现会话化,与此同时,不同用户的事件仍然散步在不同的分区中。
|
||||
|
||||
#### 处理倾斜
|
||||
#### 处理偏斜
|
||||
|
||||
如果存在与单个键关联的大量数据,则“将具有相同键的所有记录放到相同的位置”这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为**关键对象(linchpin object)**【38】或**热键(hot key)**。
|
||||
|
||||
|
6
ch11.md
6
ch11.md
@ -333,9 +333,9 @@
|
||||
|
||||
事件流的消费者不允许拒绝事件:当消费者看到事件时,它已经成为日志中不可变的一部分,并且可能已经被其他消费者看到了。因此任何对命令的验证,都需要在它成为事件之前同步完成。例如,通过使用一个可自动验证命令的可序列化事务来发布事件。
|
||||
|
||||
或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如“[使用全序广播实现线性一致存储](ch9.md#使用全序广播实现线性一致存储)”中所述) 。这种分割方式允许验证发生在一个异步的过程中。
|
||||
或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中所述) 。这种分割方式允许验证发生在一个异步的过程中。
|
||||
|
||||
#### 状态,流和不变性
|
||||
### 状态,流和不变性
|
||||
|
||||
我们在[第10章](ch10.md)中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
|
||||
|
||||
@ -386,7 +386,7 @@ $$
|
||||
|
||||
事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在在“[读己之写](ch5.md#读己之写)”中讨论了这个问题以及可能的解决方案。
|
||||
|
||||
一种解决方案是将事件附加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要**事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在“[使用全序广播实现线性化存储](ch9.md#使用全序广播实现线性化存储)”中讨论的方法。
|
||||
一种解决方案是将事件附加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要**事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中讨论的方法。
|
||||
|
||||
另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。
|
||||
|
||||
|
4
ch12.md
4
ch12.md
@ -491,7 +491,7 @@ BEGIN TRANSACTION;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
[例12-2]()依赖于`request_id`列上的唯一约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中讨论过,应用级别的**检查-然后-插入**可能会在不可序列化的隔离下失败)。
|
||||
[例12-2]()依赖于`request_id`列上的唯一约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中讨论过,应用级别的**检查-然后-插入**可能会在不可序列化的隔离下失败)。
|
||||
|
||||
除了抑制重复的请求之外,[例12-2]()中的请求表表现得就像一种事件日志,提示向着事件溯源的方向(参阅“[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求ID来强制执行。
|
||||
|
||||
@ -553,7 +553,7 @@ COMMIT;
|
||||
|
||||
该算法基本上与“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区可以被独立处理。
|
||||
|
||||
该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如“[什么是冲突?](ch5.md#什么是冲突?)”与“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在90年代开创的方法类似【58】。
|
||||
该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如“[什么是冲突?](ch5.md#什么是冲突?)”与“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在90年代开创的方法类似【58】。
|
||||
|
||||
#### 多分区请求处理
|
||||
|
||||
|
4
ch3.md
4
ch3.md
@ -158,7 +158,7 @@ $ cat database
|
||||
|
||||
![](img/fig3-4.png)
|
||||
|
||||
##### 图3-4 合并几个SSTable段,只保留每个键的最新值
|
||||
**图3-4 合并几个SSTable段,只保留每个键的最新值**
|
||||
|
||||
如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值必须比另一个段中的所有值更新(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
|
||||
|
||||
@ -399,7 +399,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
这些OLTP系统往往对业务运作至关重要,因而通常会要求 **高可用** 与 **低延迟**。所以DBA会密切关注他们的OLTP数据库,他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询,因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时执行的事务的性能。
|
||||
|
||||
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响OLTP操作【48】。数据仓库包含公司各种OLTP系统中所有的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载(ETL)**”,如[图3-8](img/fig3-8)所示。
|
||||
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响OLTP操作【48】。数据仓库包含公司各种OLTP系统中所有的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载(ETL)**”,如[图3-8](img/fig3-8.png)所示。
|
||||
|
||||
![](img/fig3-8.png)
|
||||
|
||||
|
2
ch4.md
2
ch4.md
@ -258,7 +258,7 @@ Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只
|
||||
|
||||
只要Avro可以支持相应的类型转换,就可以改变字段的数据类型。更改字段的名称也是可能的,但有点棘手:Reader模式可以包含字段名称的别名,所以它可以匹配旧Writer的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
|
||||
|
||||
##### 但Writer模式到底是什么?
|
||||
#### 但Writer模式到底是什么?
|
||||
|
||||
到目前为止,我们一直跳过了一个重要的问题:对于一段特定的编码数据,Reader如何知道其Writer模式?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。
|
||||
|
||||
|
4
ch5.md
4
ch5.md
@ -52,7 +52,7 @@
|
||||
![](img/fig5-2.png)
|
||||
**图5-2 基于领导者的复制:一个同步从库和一个异步从库**
|
||||
|
||||
在[图5-2]()的示例中,从库1的复制是同步的:在向用户报告写入成功,并使结果对其他用户可见之前,主库需要等待从库1的确认,确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的:主库发送消息,但不等待从库的响应。
|
||||
在[图5-2](img/fig5-2.png)的示例中,从库1的复制是同步的:在向用户报告写入成功,并使结果对其他用户可见之前,主库需要等待从库1的确认,确保从库1已经收到写入操作。以及在使写入对其他客户端可见之前接收到写入。跟随者2的复制是异步的:主库发送消息,但不等待从库的响应。
|
||||
|
||||
在这幅图中,从库2处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在一秒向从库应用变更,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久;例如:从库正在从故障中恢复,系统在最大容量附近运行,或者如果节点间存在网络问题。
|
||||
|
||||
@ -277,7 +277,7 @@
|
||||
|
||||
防止这种异常,需要另一种类型的保证:**一致前缀读(consistent prefix reads)**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
|
||||
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在第6章中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在[第6章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
|
||||
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“[“此前发生”的关系和并发](#“此前发生”的关系和并发)”一节中返回这个主题。
|
||||
|
||||
|
4
ch6.md
4
ch6.md
@ -15,7 +15,7 @@
|
||||
|
||||
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits)** 无关,这是节点之间网络故障的一种。我们将在[第8章](ch8.md)讨论这些错误。
|
||||
|
||||
> ##### 术语澄清
|
||||
> #### 术语澄清
|
||||
>
|
||||
> 上文中的**分区(partition)**,在MongoDB,Elasticsearch和Solr Cloud中被称为**分片(shard)**,在HBase中称之为**区域(Region)**,Bigtable中则是 **表块(tablet)**,Cassandra和Riak中是**虚节点(vnode)**,Couchbase中叫做**虚桶(vBucket)**。但是**分区(partitioning)** 是最约定俗成的叫法。
|
||||
>
|
||||
@ -55,7 +55,7 @@
|
||||
|
||||
### 根据键的范围分区
|
||||
|
||||
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图6-2]())。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
|
||||
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图6-2](img/fig6-2.png))。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果您还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
|
||||
|
||||
![](img/fig6-2.png)
|
||||
|
||||
|
6
ch7.md
6
ch7.md
@ -463,7 +463,7 @@ UPDATE wiki_pages SET content = '新内容'
|
||||
|
||||
另一方面,最后写入胜利(LWW)的冲突解决方法很容易丢失更新,如“[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))”中所述。不幸的是,LWW是许多复制数据库中的默认方案。
|
||||
|
||||
### 写入偏差与幻读
|
||||
### 写入偏斜与幻读
|
||||
|
||||
前面的章节中,我们看到了**脏写**和**丢失更新**,当不同的事务并发地尝试写入相同的对象时,会出现这两种竞争条件。为了避免数据损坏,这些竞争条件需要被阻止——既可以由数据库自动执行,也可以通过锁和原子写操作这类手动安全措施来防止。
|
||||
|
||||
@ -772,7 +772,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
#### 基于过时前提的决策
|
||||
|
||||
先前讨论了快照隔离中的写入偏差(参阅“[写入偏差与幻读](#写入偏差与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
先前讨论了快照隔离中的写入偏差(参阅“[写入偏斜与幻读](#写入偏斜与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
|
||||
换句话说,事务基于一个**前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
|
||||
@ -805,7 +805,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
在两阶段锁定的上下文中,我们讨论了索引范围锁(请参阅“[索引范围锁](#索引范围锁)”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如 `WHERE shift_id = 1234`。可以在这里使用类似的技术,除了SSI锁不会阻塞其他事务。
|
||||
|
||||
在[图7-11]()中,事务42 和43 都在班次1234 查找值班医生。如果在`shift_id`上有索引,则数据库可以使用索引项1234 来记录事务42 和43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
|
||||
在[图7-11](img/fig7-11.png)中,事务42 和43 都在班次1234 查找值班医生。如果在`shift_id`上有索引,则数据库可以使用索引项1234 来记录事务42 和43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
|
||||
|
||||
当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务指导其他读事务完成,而是像警戒线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。
|
||||
|
||||
|
16
ch8.md
16
ch8.md
@ -193,7 +193,7 @@
|
||||
|
||||
更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器【30】来完成,该检测器在例如Akka和Cassandra 【31】中使用。 TCP的超时重传机制也是以类似的方式工作【27】。
|
||||
|
||||
### 同步网络 vs 异步网络
|
||||
### 同步网络与异步网络
|
||||
|
||||
如果我们可以依靠网络来传递一些**最大延迟固定**的数据包,而不是丢弃数据包,那么分布式系统就会简单得多。为什么我们不能在硬件层面上解决这个问题,使网络可靠,使软件不必担心呢?
|
||||
|
||||
@ -586,15 +586,15 @@ while (true) {
|
||||
|
||||
同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性:
|
||||
|
||||
***唯一性***
|
||||
***唯一性(uniqueness)***
|
||||
|
||||
没有两个防护令牌请求返回相同的值。
|
||||
|
||||
***单调序列***
|
||||
***单调序列(monotonic sequence)***
|
||||
|
||||
如果请求 $x$ 返回了令牌 $t_x$,并且请求$y$返回了令牌$t_y$,并且 $x$ 在 $y$ 开始之前已经完成,那么$t_x <t_y$。
|
||||
|
||||
***可用性***
|
||||
***可用性(availability)***
|
||||
|
||||
请求防护令牌并且不会崩溃的节点,最终会收到响应。
|
||||
|
||||
@ -602,22 +602,22 @@ while (true) {
|
||||
|
||||
#### 安全性和活性
|
||||
|
||||
为了澄清这种情况,有必要区分两种不同的性质:**安全性(safety)**和**活性(liveness)**。在刚刚给出的例子中,**唯一性(uniqueness)**和**单调序列(monotonic sequence)**是安全属性,但**可用性**是**活性(liveness)**属性。
|
||||
为了澄清这种情况,有必要区分两种不同的属性:**安全(safety)属性**和**活性(liveness)属性**。在刚刚给出的例子中,**唯一性**和**单调序列**是安全属性,而**可用性**是活性属性。
|
||||
|
||||
这两种性质有什么区别?一个试金石就是,活性属性通常在定义中通常包括“**最终**”一词。 (是的,你猜对了——最终一致性是一个活性属性【89】。)
|
||||
|
||||
安全性通常被非正式地定义为,**没有坏事发生**,而活性通常就类似:**最终好事发生**。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全性和活性的实际定义是精确的和数学的【90】:
|
||||
安全通常被非正式地定义为:**没有坏事发生**,而活性通常就类似:**最终好事发生**。但是,最好不要过多地阅读那些非正式的定义,因为好与坏的含义是主观的。安全和活性的实际定义是精确的和数学的【90】:
|
||||
|
||||
* 如果安全属性被违反,我们可以指向一个特定的安全属性被破坏的时间点(例如,如果违反了唯一性属性,我们可以确定重复的防护令牌被返回的特定操作)。违反安全属性后,违规行为不能被撤销——损失已经发生。
|
||||
* 活性属性反过来:在某个时间点(例如,一个节点可能发送了一个请求,但还没有收到响应),它可能不成立,但总是希望在未来能成立(即通过接受答复)。
|
||||
|
||||
区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求**始终**保持安全属性是常见的【88】。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。
|
||||
区分安全属性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求**始终**保持安全属性是常见的【88】。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全属性得到满足)。
|
||||
|
||||
但是,对于活性属性,我们可以提出一些注意事项:例如,只有在大多数节点没有崩溃的情况下,只有当网络最终从中断中恢复时,我们才可以说请求需要接收响应。部分同步模型的定义要求系统最终返回到同步状态——即任何网络中断的时间段只会持续一段有限的时间,然后进行修复。
|
||||
|
||||
#### 将系统模型映射到现实世界
|
||||
|
||||
安全性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。
|
||||
安全属性和活性属性以及系统模型对于推理分布式算法的正确性非常有用。然而,在实践中实施算法时,现实的混乱事实再一次地让你咬牙切齿,很明显系统模型是对现实的简化抽象。
|
||||
|
||||
例如,在崩溃-恢复(crash-recovery)模型中的算法通常假设稳定存储器中的数据在崩溃后可以幸存。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况【91】?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,那又会发生什么情况【92】?
|
||||
|
||||
|
265
ch9.md
265
ch9.md
@ -11,15 +11,15 @@
|
||||
|
||||
正如[第8章](ch8.md)所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法—— 即使某些内部组件出现故障,服务也能正常运行。
|
||||
|
||||
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设[第8章](ch8.md)的所有问题都可能发生:网络中的数据包可能会丢失,重新排序,重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。
|
||||
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设[第8章](ch8.md)的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。
|
||||
|
||||
构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与[第7章](ch7.md)中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。
|
||||
|
||||
现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是**共识(consensus)**:**就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,尽管存在网络故障和流程故障,可靠地达成共识是一个令人惊讶的棘手问题。
|
||||
现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是**共识(consensus)**:**就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,要可靠地达成共识,且不被网络故障和进程故障所影响,是一个令人惊讶的棘手问题。
|
||||
|
||||
一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在“[处理节点宕机](ch5.md#处理节点宕机)”中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为**脑裂(split brain)**,且经常导致数据丢失。正确实现共识有助于避免这种问题。
|
||||
一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在“[处理节点宕机](ch5.md#处理节点宕机)”中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为**脑裂(split brain)**,它经常会导致数据丢失。正确实现共识有助于避免这种问题。
|
||||
|
||||
在本章后面的“[分布式事务和共识](#分布式事务和共识)”中,我们将研究解决共识和相关问题的算法。但首先,我们首先需要探索可以在分布式系统中提供的保证和抽象的范围。
|
||||
在本章后面的“[分布式事务与共识](#分布式事务与共识)”中,我们将研究解决共识和相关问题的算法。但首先,我们首先需要探索可以在分布式系统中提供的保证和抽象的范围。
|
||||
|
||||
我们需要了解可以做什么和不可以做什么的范围:在某些情况下,系统可以容忍故障并继续工作;在其他情况下,这是不可能的。我们将深入研究什么可能而什么不可能的限制,既通过理论证明,也通过实际实现。我们将在本章中概述这些基本限制。
|
||||
|
||||
@ -33,22 +33,21 @@
|
||||
|
||||
大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。
|
||||
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚才写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
|
||||
对于应用开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。对于后者,如果将一个值赋给一个变量,然后很快地再次读取,不可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。
|
||||
|
||||
在与只提供弱保证的数据库打交道时,你需要始终意识到它的局限性,而不是意外地作出太多假设。错误往往是微妙的,很难找到,也很难测试,因为应用可能在大多数情况下运行良好。当系统出现故障(例如网络中断)或高并发时,最终一致性的边缘情况才会显现出来。
|
||||
|
||||
本章将探索数据系统可能选择提供的更强一致性模型。它不是免费的:具有较强保证的系统可能会比保证较差的系统具有更差的性能或更少的容错性。尽管如此,更强的保证可以吸引人,因为它们更容易用对。只有见过不同的一致性模型后,才能更好地决定哪一个最适合自己的需求。
|
||||
本章将探索数据系统可能选择提供的更强一致性模型。它不是免费的:具有较强保证的系统可能会比保证较差的系统具有更差的性能或更少的容错性。尽管如此,更强的保证能够吸引人,因为它们更容易用对。只有见过不同的一致性模型后,才能更好地决定哪一个最适合自己的需求。
|
||||
|
||||
**分布式一致性模型**和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(参见“[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了,**避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于,**面对延迟和故障时,如何协调副本间的状态。**
|
||||
**分布式一致性模型**和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(参见“[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了**避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于**在面对延迟和故障时如何协调副本间的状态**。
|
||||
|
||||
本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的:
|
||||
|
||||
* 首先看一下常用的**最强一致性模型**之一,**线性一致性(linearizability)**,并考察其优缺点。
|
||||
* 然后我们将检查分布式系统中[**事件顺序**](#顺序保证)的问题,特别是因果关系和全局顺序的问题。
|
||||
* 在第三部分(“[分布式事务和共识](#分布式事务和共识)”)中将探讨如何原子地提交分布式事务,这将最终引领我们走向共识问题的解决方案。
|
||||
|
||||
* 在第三节的(“[分布式事务与共识](#分布式事务与共识)”)中将探讨如何原子地提交分布式事务,这将最终引领我们走向共识问题的解决方案。
|
||||
|
||||
|
||||
|
||||
@ -58,7 +57,7 @@
|
||||
|
||||
这就是**线性一致性(linearizability)**背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)**或**外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。
|
||||
|
||||
在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。维护数据的单个副本的错觉是指,系统能保障读到的值是最近的,最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。
|
||||
在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。要维护数据的单个副本的假象,系统应保障读到的值是最近的、最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。
|
||||
|
||||
![](img/fig9-1.png)
|
||||
|
||||
@ -85,8 +84,6 @@
|
||||
在这个例子中,寄存器有两种类型的操作:
|
||||
|
||||
* $ read(x)⇒v$表示客户端请求读取寄存器 `x` 的值,数据库返回值 `v`。
|
||||
|
||||
|
||||
* $write(x,v)⇒r$ 表示客户端请求将寄存器 `x` 设置为值 `v` ,数据库返回响应 `r` (可能正确,可能错误)。
|
||||
|
||||
在[图9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端C 执行写请求将其设置为 `1`。发生这种情况时,客户端A和B反复轮询数据库以读取最新值。 A和B的请求可能会收到怎样的响应?
|
||||
@ -110,13 +107,13 @@
|
||||
|
||||
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
|
||||
|
||||
在[图9-4]()中,除了读写之外,还增加了第三种类型的操作:
|
||||
在[图9-4](img/fig9-4.png)中,除了读写之外,还增加了第三种类型的操作:
|
||||
|
||||
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。
|
||||
|
||||
[图9-4](img/fig9-4.png)中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
|
||||
|
||||
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
|
||||
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜度保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
|
||||
|
||||
![](img/fig9-4.png)
|
||||
|
||||
@ -132,39 +129,38 @@
|
||||
|
||||
* 客户B的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与C的**cas**写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B的读取返回 `2` 是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值 `4` ,因此不允许B读取比A更旧的值。再次,与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。
|
||||
|
||||
这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
||||
这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,以测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
|
||||
|
||||
|
||||
|
||||
> ### 线性一致性与可序列化
|
||||
> ### 线性一致性与可串行化
|
||||
>
|
||||
> **线性一致性**容易和[**可序列化**](ch7.md#可序列化)相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要:
|
||||
> **线性一致性**容易和[**可串行化**](ch7.md#可串行化)相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要:
|
||||
>
|
||||
> ***可序列化***
|
||||
> ***可串行化***
|
||||
>
|
||||
> **可序列化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。
|
||||
> **可串行化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。
|
||||
>
|
||||
> ***线性一致性***
|
||||
>
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
>
|
||||
> 一个数据库可以提供可序列化和线性一致性,这种组合被称为严格的可序列化或强的**强单副本可序列化(strong-1SR)**【4,13】。基于两阶段锁定的可序列化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
|
||||
> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或**强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”一节)或**真的串行执行**(参见第“[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。
|
||||
>
|
||||
> 但是,可序列化的快照隔离(参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
|
||||
|
||||
> 但是,可串行化的快照隔离(参见“[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
|
||||
|
||||
|
||||
### 依赖线性一致性
|
||||
|
||||
线性一致性在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:过了几秒钟的结果不可能在这种情况下造成任何真正的伤害。然而对于少数领域,线性一致性是系统正确工作的一个重要条件。
|
||||
线性一致性在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:滞后了几秒钟的结果不太可能在这种情况下造成任何真正的伤害。然而对于少数领域,线性一致性是系统正确工作的一个重要条件。
|
||||
|
||||
#### 锁定和领导选举
|
||||
|
||||
一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。
|
||||
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,参阅“[领导者和锁](#领导者和锁)”中的防护问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,参阅“[领导者和锁](ch8.md#领导者和锁)”中的防护问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。你可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,你需要在读取【15】之前调用`sync()`。参阅“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本提供服务。你可以选择请求线性一致性读取:etcd称之为**法定人数读取(quorum read)**【16】,而在ZooKeeper中,你需要在读取之前调用`sync()`【15】。参阅“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”。
|
||||
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中更多的粒度级别上使用。RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
|
||||
@ -172,7 +168,7 @@
|
||||
|
||||
唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性一致性。
|
||||
|
||||
这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。
|
||||
这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置(CAS)非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。
|
||||
|
||||
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
|
||||
|
||||
@ -186,15 +182,15 @@
|
||||
|
||||
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。
|
||||
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
|
||||
![](img/fig9-5.png)
|
||||
**图9-5 Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
|
||||
**图9-5 Web服务器和图像缩放器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
|
||||
|
||||
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4)可能比存储服务内部的复制更快。在这种情况下,当缩放器读取图像(步骤5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和略缩图就产生了永久性的不一致。
|
||||
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4)可能比存储服务内部的复制(replication)更快。在这种情况下,当缩放器读取图像(步骤5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和略缩图就产生了永久性的不一致。
|
||||
|
||||
出现这个问题是因为Web服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png),数据库复制与Alice的嘴到Bob耳朵之间的真人音频信道之间也存在竞争条件。
|
||||
|
||||
线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在Alice和Bob的例子),则可以使用在“[读己之写](ch5.md#读己之写)”讨论过的备选方法,不过会有额外的复杂度代价。
|
||||
线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在Alice和Bob的例子),则可以使用在“[读己之写](ch5.md#读己之写)”讨论过的类似方法,不过会有额外的复杂度代价。
|
||||
|
||||
### 实现线性一致的系统
|
||||
|
||||
@ -206,11 +202,11 @@
|
||||
|
||||
***单主复制(可能线性一致)***
|
||||
|
||||
在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(protential)**是线性一致性的[^iv]。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误【10】。
|
||||
在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(potential)**是线性一致性的[^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
|
||||
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”)。
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务与共识](#分布式事务与共识)”)。
|
||||
|
||||
从主库读取依赖一个假设,你确定领导是谁。正如在“[真理在多数人手中](ch8.md#真理被多数人定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
从主库读取依赖一个假设,你确定地知道领导者是谁。正如在“[真相由多数所定义](ch8.md#真相由多数所定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
|
||||
***共识算法(线性一致)***
|
||||
|
||||
@ -218,15 +214,13 @@
|
||||
|
||||
***多主复制(非线性一致)***
|
||||
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生冲突的写入,需要解析(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本人为产生的。
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
|
||||
|
||||
***无主复制(也许不是线性一致的)***
|
||||
|
||||
对于无领导者复制的系统(Dynamo风格;参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
|
||||
|
||||
|
||||
基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。[宽松的法定人数](ch5.md#宽松的法定人数与提示移交)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。
|
||||
基于日历时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(参见“[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。
|
||||
|
||||
#### 线性一致性和法定人数
|
||||
|
||||
@ -240,9 +234,9 @@
|
||||
|
||||
法定人数条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1](img/fig9-1.png))
|
||||
|
||||
有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读修复完成【27】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(参阅“[读修复和反熵](ch5.md#读修复和反熵)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读修复完成【27】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置操作,因为它需要一个共识算法【28】。
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置(CAS)操作,因为它需要一个共识算法【28】。
|
||||
|
||||
总而言之,最安全的做法是:假设采用Dynamo风格无主复制的系统不能提供线性一致性。
|
||||
|
||||
@ -277,13 +271,13 @@
|
||||
|
||||
[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。
|
||||
|
||||
因此不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。
|
||||
因此,不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。
|
||||
|
||||
CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬 —— 它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。
|
||||
|
||||
> ### CAP定理没有帮助
|
||||
>
|
||||
> CAP有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种错误,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。
|
||||
> CAP有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种故障类型,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。
|
||||
>
|
||||
> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,CAP更好的表述成:在分区时要么选择一致,要么选择可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。
|
||||
>
|
||||
@ -293,7 +287,7 @@
|
||||
|
||||
在分布式系统中有更多有趣的“不可能”的结果【41】,且CAP定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。
|
||||
|
||||
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)**指代将大数据集细分为小数据集的操作(分片;参见[第6章](ch6.md))。与之对应的是,**网络分区(network partition)**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下不能将其混为一谈。
|
||||
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)**指代将大数据集细分为小数据集的操作(分片;参见[第6章](ch6.md))。与之对应的是,**网络分区(network partition)**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下我们无法避免混乱。
|
||||
|
||||
#### 线性一致性和网络延迟
|
||||
|
||||
@ -311,25 +305,25 @@
|
||||
|
||||
## 顺序保证
|
||||
|
||||
之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作,以此说明[图9-4](img/fig9-4.png)中的顺序。
|
||||
之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了[图9-4](img/fig9-4.png)中的顺序。
|
||||
|
||||
**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它**顺序**曾经出现过的上下文:
|
||||
**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文:
|
||||
|
||||
* 在[第5章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
|
||||
* 在[第7章](ch7.md)中讨论的**可序列化**,是关于事务表现的像按**某种序列顺序(some sequential order)**执行的保证。它可以通过字面意义上地**序列顺序(serial order)**执行事务来实现,或者通过允许并行执行,同时防止序列化冲突来实现(通过锁或中止事务)。
|
||||
* 在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
|
||||
* 在[第7章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序(some sequential order)**执行的保证。它可以字面意义上地以**串行顺序(serial order)**执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
|
||||
* 在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
|
||||
|
||||
事实证明,顺序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
|
||||
事实证明,顺序、线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
|
||||
|
||||
### 顺序与因果
|
||||
### 顺序与因果关系
|
||||
|
||||
**顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系(causality)**。在本书中我们已经看到了几个例子,其中因果关系是很重要的:
|
||||
|
||||
* 在“[一致前缀读](ch5.md#一致前缀读)”([图5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对**因(cause)**与**果(effect)**的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在**因果依赖(causal dependency)**。
|
||||
* 在“[一致前缀读](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))通过跟踪事务之间的因果依赖来检测写偏差。
|
||||
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)**意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6.png)所示)。
|
||||
* 事务之间**写偏差(write skew)**的例子(参见“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))通过跟踪事务之间的因果依赖来检测写偏差。
|
||||
* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。
|
||||
|
||||
因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
|
||||
@ -350,7 +344,7 @@
|
||||
|
||||
***线性一致性***
|
||||
|
||||
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
|
||||
***因果性***
|
||||
|
||||
@ -358,7 +352,7 @@
|
||||
|
||||
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。
|
||||
|
||||
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
|
||||
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
|
||||
|
||||
如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)**会在这些并发创建的提交相融合时创建。
|
||||
|
||||
@ -384,7 +378,7 @@
|
||||
|
||||
用于确定*哪些操作发生在其他操作之前* 的技术,与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性:为了防止丢失更新,我们需要检测到对同一个键的并发写入。因果一致性则更进一步:它需要跟踪整个数据库中的因果依赖,而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。
|
||||
|
||||
为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图5-13 ](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI 的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。
|
||||
为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图5-13 ](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI 的冲突检测中会出现类似的想法,如“[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))”中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。
|
||||
|
||||
|
||||
|
||||
@ -392,12 +386,11 @@
|
||||
|
||||
虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。
|
||||
|
||||
但还有一个更好的方法:我们可以使用**序列号(sequence nunber)**或**时间戳(timestamp)**来排序事件。时间戳不一定来自时钟(或物理时钟,存在许多问题,如 “[不可靠时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。
|
||||
但还有一个更好的方法:我们可以使用**序列号(sequence nunber)**或**时间戳(timestamp)**来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。
|
||||
|
||||
这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每个操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。
|
||||
|
||||
这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。
|
||||
|
||||
特别是,我们可以使用**与因果一致(consistent with causality)**的全序来生成序列号[^vii]:我们保证,如果操作 A 因果后继于操作 B,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。
|
||||
特别是,我们可以使用**与因果一致(consistent with causality)**的全序来生成序列号[^vii]:我们保证,如果操作 A 因果地发生在操作 B 前,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。
|
||||
|
||||
[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的UUID,并按照字典序比较UUID,以定义操作的全序。这是一个有效的全序,但是随机的UUID并不能告诉你哪个操作先发生,或者操作是否为并发的。
|
||||
|
||||
@ -408,7 +401,7 @@
|
||||
如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法:
|
||||
|
||||
* 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。
|
||||
* 可以将时钟(物理时钟)时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以将日历时钟(物理时钟)的时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,而节点 B 可能要求序列号1,001到2,000区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。
|
||||
|
||||
这三个选项都比单一主库的自增计数器表现要好,并且更具可伸缩性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。
|
||||
@ -419,7 +412,7 @@
|
||||
|
||||
* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如[图8-3](img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^vii]
|
||||
|
||||
[^viii]: 可以使物理时钟时间戳与因果关系保持一致:在“[用于全局快照的同步时钟](#用于全局快照的同步时钟)”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。 这中方法确保了实际上靠后的事务会有更大的时间戳。 但是大多数时钟不能提供这种所需的不确定性度量。
|
||||
[^viii]: 可以使物理时钟时间戳与因果关系保持一致:在“[全局快照的同步时钟](#全局快照的同步时钟)”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。这种方法确保了实际上靠后的事务会有更大的时间戳。但是大多数时钟不能提供这种所需的不确定性度量。
|
||||
|
||||
* 在分配区块的情况下,某个操作可能会被赋予一个范围在1,001到2,000内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在1到1,000之间的数字。这里序列号与因果关系也是不一致的。
|
||||
|
||||
@ -433,10 +426,10 @@
|
||||
|
||||
![](img/fig9-8.png)
|
||||
|
||||
**图9-8 Lamport时间戳提供了与因果关系一致的总排序。**
|
||||
**图9-8 Lamport时间戳提供了与因果关系一致的全序。**
|
||||
|
||||
|
||||
兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则**计数器**值大者是更大的时间戳。如果计数器值相同,则节点ID越大的,时间戳越大。
|
||||
兰伯特时间戳与物理的日历时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则**计数器**值大者是更大的时间戳。如果计数器值相同,则节点ID越大的,时间戳越大。
|
||||
|
||||
迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大**计数器**值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。
|
||||
|
||||
@ -450,17 +443,17 @@
|
||||
|
||||
虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。
|
||||
|
||||
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导者与锁定](ch8.md#领导者与锁定)”中提到过这个问题。)
|
||||
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导者和锁](ch8.md#领导者和锁)”中提到过这个问题。)
|
||||
|
||||
乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。
|
||||
|
||||
这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上(right now)**决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。
|
||||
这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上(right now)**决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存在其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。
|
||||
|
||||
为了确保没有其他节点正在使用相同的用户名和较小的时间戳并发创建同名账户,你必须检查其它每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题出现故障或不可达,则整个系统可能被拖至停机。这不是我们需要的那种容错系统。
|
||||
|
||||
这里的问题是,只有在所有的操作都被收集之后,操作的全序才会出现。如果另一个节点已经产生了一些操作,但你还不知道那些操作是什么,那就无法构造所有操作最终的全序关系:来自另一个节点的未知操作可能需要被插入到全序中的不同位置。
|
||||
|
||||
总之:为了实诸如如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中,没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。
|
||||
总之:为了实现诸如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。
|
||||
|
||||
如何确定全序关系已经尘埃落定,这将在[全序广播](#全序广播)一节中详细说明。
|
||||
|
||||
@ -468,9 +461,9 @@
|
||||
|
||||
如果你的程序只运行在单个CPU核上,那么定义一个操作全序是很容易的:可以简单地就是CPU执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,就不能容忍任何错误,因为你必须要从每个节点都获取到最新的序列号)。
|
||||
|
||||
如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)**或**原子广播(atomic broadcast)**[^ix]【25,57,58】。
|
||||
如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)**或**原子广播(atomic broadcast)**[^ix]【25,57,58】。
|
||||
|
||||
[^ix]: “原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序广播是另一个同义词。
|
||||
[^ix]: “原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序组播(total order multicast)是另一个同义词。
|
||||
|
||||
> #### 顺序保证的范围
|
||||
>
|
||||
@ -494,17 +487,17 @@
|
||||
|
||||
全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为**状态机复制(state machine replication)**【60】,我们将在[第11章](ch11.md)中重新回到这个概念。
|
||||
|
||||
与之类似,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。
|
||||
与之类似,可以使用全序广播来实现可串行化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。
|
||||
|
||||
全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。
|
||||
全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳排序更强。
|
||||
|
||||
考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志,事务日志或预写式日志中):传递消息就像附加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。
|
||||
考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志、事务日志或预写式日志中):传递消息就像追加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。
|
||||
|
||||
全序广播对于实现提供防护令牌的锁服务也很有用(参见“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。
|
||||
|
||||
#### 使用全序广播实现线性一致的存储
|
||||
|
||||
如 [图9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有者密切的联系[^x]。
|
||||
如 [图9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系[^x]。
|
||||
|
||||
[^x]: 从形式上讲,线性一致读写寄存器是一个“更容易”的问题。 全序广播等价于共识【67】,而共识问题在异步的崩溃-停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器**可以**在这种模型中实现【23,24,25】。 然而,支持诸如**比较并设置(CAS, compare-and-set)**,或**自增并返回(increment-and-get)**的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。
|
||||
|
||||
@ -512,23 +505,23 @@
|
||||
|
||||
但如果有了全序广播,你就可以在此基础上构建线性一致的存储。例如,你可以确保用户名能唯一标识用户帐户。
|
||||
|
||||
设想对于每一个可能的用户名,你都可以有一个带有CAS原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行CAS操作,在先前寄存器值为空的条件,将其值设置为用户的账号ID。如果多个用户试图同时获取相同的用户名,则只有一个CAS操作会成功,因为其他用户会看到非空的值(由于线性一致性)。
|
||||
设想对于每一个可能的用户名,你都可以有一个带有CAS原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示未使用该用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行CAS操作,在先前寄存器值为空的条件,将其值设置为用户的账号ID。如果多个用户试图同时获取相同的用户名,则只有一个CAS操作会成功,因为其他用户会看到非空的值(由于线性一致性)。
|
||||
|
||||
你可以通过将全序广播当成仅追加日志【62,63】的方式来实现这种线性一致的CAS操作:
|
||||
|
||||
1. 在日志中追加一条消息,试探性地指明你要声明的用户名。
|
||||
2. 读日志,并等待你所附加的信息被回送。[^xi]
|
||||
3. 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。
|
||||
2. 读日志,并等待你刚才追加的消息被读回。[^xi]
|
||||
4. 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就是你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。
|
||||
|
||||
[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核x86处理器内存的一致性模型【43】。 该模型既不是线性一致的也不是顺序一致的。
|
||||
|
||||
由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可序列化的多对象事务【62】。
|
||||
由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可串行化的多对象事务【62】。
|
||||
|
||||
尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。 (精确地说,这里描述的过程提供了**顺序一致性(sequential consistency)**【47,64】,有时也称为**时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项:
|
||||
|
||||
* 你可以通过追加一条消息,当消息回送时读取日志,执行实际的读取。消息在日志中的位置因此定义了读取发生的时间点。 (etcd的法定人数读取有些类似这种情况【16】。)
|
||||
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置前的所有消息都传达到你,然后执行读取。 (这是Zookeeper `sync()` 操作背后的思想【15】)。
|
||||
* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。 (这种技术用于链式复制【63】;参阅“[复制研究](ch5.md#设置新从库)”。)
|
||||
* 你可以通过在日志中追加一条消息,然后读取日志,直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点。 (etcd的法定人数读取有些类似这种情况【16】。)
|
||||
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。 (这是Zookeeper `sync()` 操作背后的思想【15】)。
|
||||
* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。 (这种技术用于链式复制(chain replication)【63】;参阅“[关于复制的研究](ch5.md#关于复制的研究)”。)
|
||||
|
||||
#### 使用线性一致性存储实现全序广播
|
||||
|
||||
@ -536,11 +529,11 @@
|
||||
|
||||
最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子**自增并返回**操作【28】。或者原子CAS操作也可以完成这项工作。
|
||||
|
||||
该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行**自增并返回**操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号连续发送消息。
|
||||
该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行**自增并返回**操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号依序传递(deliver)消息。
|
||||
|
||||
请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同 —— 事实上,这是全序广播和时间戳排序间的关键区别。
|
||||
|
||||
实现一个带有原子性**自增并返回**操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行深入过足够深入的思考,你不可避免地会得出一个共识算法。
|
||||
实现一个带有原子性**自增并返回**操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行过足够深入的思考,你不可避免地会得出一个共识算法。
|
||||
|
||||
这并非巧合:可以证明,线性一致的CAS(或自增并返回)寄存器与全序广播都都等价于**共识**问题【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察!
|
||||
|
||||
@ -552,7 +545,7 @@
|
||||
|
||||
**共识**是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是**让几个节点达成一致(get serveral nodes to agree on something)**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。
|
||||
|
||||
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致以及全序广播([本章](ch9.md)),我们终于准备好解决共识问题了。
|
||||
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。
|
||||
|
||||
节点能达成一致,在很多场景下都非常重要,例如:
|
||||
|
||||
@ -562,20 +555,20 @@
|
||||
|
||||
***原子提交***
|
||||
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)**问题[^xii]。
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)**问题[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 参阅“[三阶段提交](#三阶段提交)”。
|
||||
|
||||
> ### 共识的不可能性
|
||||
>
|
||||
> 你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在**总是**能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事?
|
||||
> 你可能已经听说过以作者Fischer,Lynch和Paterson命名的FLP结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在**总是**能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事?
|
||||
>
|
||||
> 答案是FLP结果在**异步系统模型**中得到了证明(参阅“[系统模型与现实](ch8.md#系统模型与现实)”),这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。
|
||||
> 答案是FLP结果是在**异步系统模型**中被证明的(参阅“[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。
|
||||
>
|
||||
> 因此,FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。
|
||||
> 因此,虽然FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。
|
||||
|
||||
在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交(2PC, two-phase commit)**算法,这是解决原子提交问题最常见的办法,并在各种数据库、消息队列和应用服务器中实现。事实证明2PC是一种共识算法,但不是一个非常好的算法【70,71】。
|
||||
在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交(2PC, two-phase commit)**算法,这是解决原子提交问题最常见的办法,并在各种数据库、消息队列和应用服务器中被实现。事实证明2PC是一种共识算法,但不是一个非常好的共识算法【70,71】。
|
||||
|
||||
通过对2PC的学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。
|
||||
|
||||
@ -585,19 +578,19 @@
|
||||
|
||||
在[第7章](ch7.md)中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。
|
||||
|
||||
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个辅助索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
|
||||
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
|
||||
|
||||
#### 从单节点到分布式原子提交
|
||||
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,参阅“[让B树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
|
||||
|
||||
因此,在单个节点上,事务的提交主要取决于数据持久化落盘的**顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘驱动的控制器,且挂载在单台机器上)使得提交具有原子性。
|
||||
因此,在单个节点上,事务的提交主要取决于数据持久化落盘的**顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘的控制器,且挂载在单台机器上)使得提交具有原子性。
|
||||
|
||||
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区和二级索引](ch6.md#分区和二级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(参见“[实践中的分布式事务](#实践中的分布式事务)”)。
|
||||
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(参见“[实践中的分布式事务](#实践中的分布式事务)”)。
|
||||
|
||||
在这些情况下,仅向所有节点发送提交请求并独立提交每个节点的事务是不够的。这样很容易发生违反原子性的情况:提交在某些节点上成功,而在其他节点上失败:
|
||||
|
||||
* 某些节点可能会检测到约束冲突或冲突,因此需要中止,而其他节点则可以成功进行提交。
|
||||
* 某些节点可能会检测到违反约束或冲突,因此需要中止,而其他节点则可以成功进行提交。
|
||||
* 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。
|
||||
* 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。
|
||||
|
||||
@ -605,13 +598,13 @@
|
||||
|
||||
事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了**读已提交**隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了**已提交却又被追溯声明不存在数据**的事务也必须回滚。
|
||||
|
||||
(提交事务的结果有可能通过事后执行另一个补偿事务来取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于跨事务正确性的保证都是应用自己的问题。)
|
||||
(提交事务的结果有可能通过事后执行另一个补偿事务(compensating transaction)来取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于跨事务正确性的保证都是应用自己的问题。)
|
||||
|
||||
#### 两阶段提交简介
|
||||
|
||||
**两阶段提交(two-phase commit)**是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,也以**XA事务**的形式对应用可用【76,77】(例如Java Transaction API支持)或以SOAP Web服务的`WS-AtomicTransaction` 形式提供给应用【78,79】。
|
||||
|
||||
[ 图9-9](img/fig9-9)说明了2PC的基本流程。2PC中的提交/中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。
|
||||
[图9-9](img/fig9-9.png)说明了2PC的基本流程。2PC中的提交/中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。
|
||||
|
||||
![](img/fig9-9.png)
|
||||
|
||||
@ -619,9 +612,9 @@
|
||||
|
||||
> #### 不要把2PC和2PL搞混了
|
||||
>
|
||||
> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的东西。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
|
||||
> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的东西。 2PC在分布式数据库中提供原子提交,而2PL提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
|
||||
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为**事务管理器(transaction manager)**)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为**事务管理器(transaction manager)**)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
|
||||
正常情况下,2PC事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者(participants)**。当应用准备提交时,协调者开始阶段 1 :它发送一个**准备(prepare)**请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:
|
||||
|
||||
@ -643,7 +636,7 @@
|
||||
5. 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务日志中,如果它随后就崩溃,恢复后也能知道自己所做的决定。这被称为**提交点(commit point)**。
|
||||
6. 一旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执行。如果参与者在此期间崩溃,事务将在其恢复后提交——由于参与者投了赞成,因此恢复后它不能拒绝提交。
|
||||
|
||||
因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃)。一旦协调者做出决定,这一决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件混为一谈:将提交记录写入事务日志。)
|
||||
因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃);以及一旦协调者做出决定,这一决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合为了一体:将提交记录写入事务日志。)
|
||||
|
||||
回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有中止这个事务的自由,通过回复 “没门!”(或者有类似效果的话)。然而在说了“我愿意”之后,你就不能撤回那个声明了。如果你说“我愿意”后晕倒了,没有听到司仪说“你们现在是夫妻了”,那也并不会改变事务已经提交的现实。当你稍后恢复意识时,可以通过查询司仪的全局事务ID状态来确定你是否已经成婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。
|
||||
|
||||
@ -653,7 +646,7 @@
|
||||
|
||||
如果协调者在发送**准备**请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为**存疑(in doubt)**的或**不确定(uncertain)**的。
|
||||
|
||||
情况如[图9-10](img/fig9-10) 所示。在这个特定的例子中,协调者实际上决定提交,数据库2 收到提交请求。但是,协调者在将提交请求发送到数据库1 之前发生崩溃,因此数据库1 不知道是否提交或中止。即使**超时**在这里也没有帮助:如果数据库1 在超时后单方面中止,它将最终与执行提交的数据库2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。
|
||||
情况如[图9-10](img/fig9-10.png) 所示。在这个特定的例子中,协调者实际上决定提交,数据库2 收到提交请求。但是,协调者在将提交请求发送到数据库1 之前发生崩溃,因此数据库1 不知道是否提交或中止。即使**超时**在这里也没有帮助:如果数据库1 在超时后单方面中止,它将最终与执行提交的数据库2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。
|
||||
|
||||
![](img/fig9-10.png)
|
||||
**图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止**
|
||||
@ -686,7 +679,7 @@
|
||||
|
||||
***异构分布式事务***
|
||||
|
||||
在**异构(heterogeneous)**事务中,参与者是两种或以上不同技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
|
||||
在**异构(heterogeneous)**事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
|
||||
|
||||
数据库内部事务不必与任何其他系统兼容,因此它们可以使用任何协议,并能针对特定技术进行特定的优化。因此数据库内部的分布式事务通常工作地很好。另一方面,跨异构技术的事务则更有挑战性。
|
||||
|
||||
@ -698,7 +691,7 @@
|
||||
|
||||
然而,只有当所有受事务影响的系统都使用同样的**原子提交协议(atomic commit protocl)**时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。
|
||||
|
||||
在[第11章](ch11.md)中将再次回到”恰好一次“消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
|
||||
在[第11章](ch11.md)中将再次回到“恰好一次”消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
|
||||
|
||||
#### XA事务
|
||||
|
||||
@ -706,9 +699,9 @@
|
||||
|
||||
XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定;例如在Java EE应用的世界中,XA事务是使用**Java事务API(JTA, Java Transaction API)**实现的,而许多使用**Java数据库连接(JDBC, Java Database Connectivity)**的数据库驱动,以及许多使用**Java消息服务(JMS)**API的消息代理都支持**Java事务API(JTA)**。
|
||||
|
||||
XA假定你的应用使用网络驱动或客户端库来与**参与者**进行通信(数据库或消息服务)。如果驱动支持XA,则意味着它会调用XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备,提交或中止。
|
||||
XA假定你的应用使用网络驱动或客户端库来与**参与者**(数据库或消息服务)进行通信。如果驱动支持XA,则意味着它会调用XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备、提交或中止。
|
||||
|
||||
事务协调者需要实现XA API。标准没有指明应该如何实现,但实际上协调者通常只是一个库,被加载到发起事务的应用的同一个进程中(而不是单独的服务)。它在事务中个跟踪所有的参与者,并在要求它们**准备**之后收集参与者的响应(通过驱动回调),并使用本地磁盘上的日志记录每次事务的决定(提交/中止)。
|
||||
事务协调者需要实现XA API。标准没有指明应该如何实现,但实际上协调者通常只是一个库,被加载到发起事务的应用的同一个进程中(而不是单独的服务)。它在事务中跟踪所有的参与者,并在要求它们**准备**之后收集参与者的响应(通过驱动回调),并使用本地磁盘上的日志记录每次事务的决定(提交/中止)。
|
||||
|
||||
如果应用进程崩溃,或者运行应用的机器报销了,协调者也随之往生极乐。然后任何带有**准备了**但未提交事务的参与者都会在疑虑中卡死。由于协调程序的日志位于应用服务器的本地磁盘上,因此必须重启该服务器,且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过客户端库。
|
||||
|
||||
@ -716,7 +709,7 @@
|
||||
|
||||
为什么我们这么关心存疑事务?系统的其他部分就不能继续正常工作,无视那些终将被清理的存疑事务吗?
|
||||
|
||||
问题在于**锁(locking)**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常获取待修改的行上的**行级排他锁**,以防止脏写。此外,如果要使用可序列化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。
|
||||
问题在于**锁(locking)**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常获取待修改的行上的**行级排他锁**,以防止脏写。此外,如果要使用可串行化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。
|
||||
|
||||
在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要20分钟才能重启,那么这些锁将会被持有20分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。
|
||||
|
||||
@ -726,22 +719,22 @@
|
||||
|
||||
理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何存疑事务。然而在实践中,**孤立(orphaned)**的存疑事务确实会出现【89,90】,即无论出于何种理由,协调者无法确定事务的结果(例如事务日志已经由于软件错误丢失或损坏)。这些事务无法自动解决,所以它们永远待在数据库中,持有锁并阻塞其他事务。
|
||||
|
||||
即使重启数据库服务器也无法解决这个问题,因为在2PC的正确实现中,即使重启也必须保留存疑事务的锁(否则就会冒有违反原子性保证的风险)。这是一种棘手的情况。
|
||||
即使重启数据库服务器也无法解决这个问题,因为在2PC的正确实现中,即使重启也必须保留存疑事务的锁(否则就会冒违反原子性保证的风险)。这是一种棘手的情况。
|
||||
|
||||
唯一的出路是让管理员手动决定提交还是回滚事务。管理员必须检查每个存疑事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(不然为什么协调者处于这种糟糕的状态),并很可能要在巨大精神压力和时间压力下完成。
|
||||
|
||||
许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个存疑事务,而无需协调者做出最终决定【76,77,91】。要清楚的是,这里**启发式**是**可能破坏原子性(probably breaking atomicity)**的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。
|
||||
许多XA的实现都有一个叫做**启发式决策(heuristic decisions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个存疑事务,而无需协调者做出最终决定【76,77,91】。要清楚的是,这里**启发式**是**可能破坏原子性(probably breaking atomicity)**的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。
|
||||
|
||||
#### 分布式事务的限制
|
||||
|
||||
XA事务解决了保持多个参与者(数据系统)相互一致的现实的重要问题,但正如我们所看到的那样,它也引入了严重的运维问题。特别来讲,这里的核心认识是:事务协调者本身就是一种数据库(存储了事务的结果),因此需要像其他重要数据库一样小心地打交道:
|
||||
XA事务解决了保持多个参与者(数据系统)相互一致的现实的和重要的问题,但正如我们所看到的那样,它也引入了严重的运维问题。特别来讲,这里的核心认识是:事务协调者本身就是一种数据库(存储了事务的结果),因此需要像其他重要数据库一样小心地打交道:
|
||||
|
||||
* 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。
|
||||
* 许多服务器端应用都是使用无状态模式开发的(受HTTP的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。
|
||||
* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与[SSI ](ch7.md#可串行快照隔离(SSI) )协同工作,因为这需要一个跨系统定位冲突的协议。
|
||||
* 对于数据库内部的分布式事务(不是XA),限制没有这么大,例如,分布式版本的SSI 是可能的。然而仍然存在问题:2PC成功提交一个事务需要所有参与者的响应。因此,如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效(amplifying failures)**的趋势,这又与我们构建容错系统的目标背道而驰。
|
||||
* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与SSI(参阅[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI) ))协同工作,因为这需要一个跨系统定位冲突的协议。
|
||||
* 对于数据库内部的分布式事务(不是XA),限制没有这么大 —— 例如,分布式版本的SSI是可能的。然而仍然存在问题:2PC成功提交一个事务需要所有参与者的响应。因此,如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效(amplifying failures)**的趋势,这又与我们构建容错系统的目标背道而驰。
|
||||
|
||||
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些章节。但首先,我们应该概括一下关于**共识**的话题。
|
||||
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于**共识**的话题。
|
||||
|
||||
|
||||
|
||||
@ -749,7 +742,7 @@
|
||||
|
||||
非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时(concurrently)**尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容(mutually incompatible)**的操作中,哪一个才是赢家。
|
||||
|
||||
共识问题通常形式化如下:一个或多个节点可以**提议(propose)**某些值,而共识算法**决定(decides)**采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**正在服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。
|
||||
共识问题通常形式化如下:一个或多个节点可以**提议(propose)**某些值,而共识算法**决定(decides)**采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**将要服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。
|
||||
|
||||
在这种形式下,共识算法必须满足以下性质【25】:[^xiii]
|
||||
|
||||
@ -774,7 +767,7 @@
|
||||
|
||||
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。
|
||||
|
||||
**终止**属性正式形成了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 (**终止**是一种**活性属性**,而另外三种是安全属性 —— 参见“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
**终止**属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 (**终止**是一种**活性属性**,而另外三种是**安全属性** —— 参见“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
|
||||
共识的系统模型假设,当一个节点“崩溃”时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足**终止**属性。特别是,2PC不符合终止属性的要求。
|
||||
|
||||
@ -782,30 +775,28 @@
|
||||
|
||||
因此**终止**属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足—— 一致同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。
|
||||
|
||||
大多数共识算法假设不存在**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。
|
||||
大多数共识算法假设不存在**拜占庭式错误**,正如在“[拜占庭故障](ch8.md#拜占庭故障)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。
|
||||
|
||||
#### 共识算法和全序广播
|
||||
|
||||
最著名的容错共识算法是**视图戳复制(VSR, viewstamped replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】)
|
||||
最著名的容错共识算法是**视图戳复制(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】)
|
||||
|
||||
大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,一致同意,完整性,有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
|
||||
大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,并满足一致同意、完整性、有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
|
||||
|
||||
请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息【67】。
|
||||
|
||||
所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应):
|
||||
所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应):
|
||||
|
||||
* 由于**一致同意**属性,所有节点决定以相同的顺序传递相同的消息。
|
||||
|
||||
|
||||
* 由于**完整性**属性,消息不会重复。
|
||||
* 由于**有效性**属性,消息不会被损坏,也不能凭空编造。
|
||||
* 由于**终止**属性,消息不会丢失。
|
||||
|
||||
视图戳复制,Raft和Zab直接实现了全序广播,因为这样做比重复**一次一值(one value a time)**的共识更高效。在Paxos的情况下,这种优化被称为Multi-Paxos。
|
||||
视图戳复制,Raft和Zab直接实现了全序广播,因为这样做比重复**一次一值(one value a time)**的共识更高效。在Paxos的情况下,这种优化被称为Multi-Paxos。
|
||||
|
||||
#### 单领导者复制和共识
|
||||
|
||||
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢?
|
||||
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢?
|
||||
|
||||
答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种**独裁类型**的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的**终止**属性,因为它需要人为干预才能取得**进展**。
|
||||
|
||||
@ -815,27 +806,27 @@
|
||||
|
||||
这样看来,要选出一个领导者,我们首先需要一个领导者。要解决共识问题,我们首先需要解决共识问题。我们如何跳出这个先有鸡还是先有蛋的问题?
|
||||
|
||||
#### 时代编号和法定人数
|
||||
#### 纪元编号和法定人数
|
||||
|
||||
迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**时代编号(epoch number)**(在Paxos中称为**投票编号(ballot number)**,视图戳复制中的**视图编号(view number)**,以及Raft中的**任期号码(term number)**),并确保在每个时代中,领导者都是唯一的。
|
||||
迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**纪元编号(epoch number)**(在Paxos中称为**投票编号(ballot number)**,视图戳复制中的**视图编号(view number)**,以及Raft中的**任期号码(term number)**),并确保在每个时代中,领导者都是唯一的。
|
||||
|
||||
每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算。
|
||||
每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的纪元编号,因此纪元编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高纪元编号的领导说了算。
|
||||
|
||||
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高时代编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真理在多数人手中](ch8.md#真理在多数人手中)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
|
||||
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真相由多数所定义](ch8.md#真相由多数所定义)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
|
||||
|
||||
相反,它必须从**法定人数(quorum)**的节点中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高时代编号的领导者的情况下,一个节点才会投票赞成提议。
|
||||
相反,它必须从**法定人数(quorum)**的节点中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。
|
||||
|
||||
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的时代编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。
|
||||
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。
|
||||
|
||||
这一投票过程表面上看起来很像两阶段提交。最大的区别在于,2PC中协调者不是由选举产生的,而且2PC则要求**所有**参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。
|
||||
|
||||
#### 共识的局限性
|
||||
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”)。
|
||||
|
||||
尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。
|
||||
|
||||
节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
|
||||
节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步复制与异步复制](ch5.md#同步复制与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
|
||||
|
||||
共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(参阅“[线性一致性的代价](#线性一致性的代价)”)。
|
||||
|
||||
@ -861,11 +852,11 @@
|
||||
|
||||
***操作的全序排序***
|
||||
|
||||
如“[领导者与锁定](ch8.md#领导者与锁定)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全局排序操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。
|
||||
如“[领导者和锁](ch8.md#领导者和锁)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。
|
||||
|
||||
***失效检测***
|
||||
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时(ZooKeeper调用这些临时节点)时,会话持有的任何锁都可以配置为自动释放(ZooKeeper称之为**临时节点(ephemeral nodes)**)。
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点(ephemeral nodes)**),会话持有的任何锁都可以配置为自动释放。
|
||||
|
||||
***变更通知***
|
||||
|
||||
@ -877,29 +868,29 @@
|
||||
|
||||
ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。
|
||||
|
||||
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
|
||||
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
|
||||
|
||||
这类任务可以通过在ZooKeeper中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在ZooKeeper客户端API基础之上提供更高层工具的库,例如Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多,这样的尝试鲜有成功记录【107】。
|
||||
|
||||
应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。
|
||||
|
||||
通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区 7 中的节点运行在 `10.1.1.23` 上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。
|
||||
通常,由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和它的小伙伴们可以看作是成员资格服务(membership services)研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。
|
||||
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
|
||||
即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,在哪些节点构成当前的成员关系方面是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有成员的成员有不同意见,则这种方法将不起作用。
|
||||
即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,知道哪些节点构成了当前的成员关系是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有的成员都有谁有不同意见,则这种方法将不起作用。
|
||||
|
||||
|
||||
|
||||
@ -949,7 +940,7 @@
|
||||
|
||||
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。
|
||||
|
||||
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)正是不同领导者之间没有达成共识的结果,但也这并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
|
||||
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(参见“[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
|
||||
|
||||
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索。
|
||||
|
||||
|
108
glossary.md
108
glossary.md
@ -10,7 +10,7 @@
|
||||
|
||||
### 异步(asynchronous)
|
||||
|
||||
不等待某些事情完成(例如,将数据发送到网络中的另一个节点),并且不会假设要花多长时间。请参阅第153页上的“同步与异步复制”,第284页上的“同步与异步网络”,以及第306页上的“系统模型与现实”。
|
||||
不等待某些事情完成(例如,将数据发送到网络中的另一个节点),并且不会假设要花多长时间。请参阅[同步复制与异步复制](ch5.md#同步复制与异步复制)”,“[同步网络与异步网络](ch8.md#同步网络与异步网络)”,以及“[系统模型与现实](ch8.md#系统模型与现实)”。
|
||||
|
||||
|
||||
|
||||
@ -18,31 +18,31 @@
|
||||
|
||||
1.在并发操作的上下文中:描述一个在单个时间点看起来生效的操作,所以另一个并发进程永远不会遇到处于“半完成”状态的操作。另见隔离。
|
||||
|
||||
2.在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。参见第223页的“原子性”和第354页的“原子提交和两阶段提交(2PC)”。
|
||||
2.在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。参见“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交与二阶段提交(2PC)](ch9.md#原子提交与二阶段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
### 背压(backpressure)
|
||||
|
||||
接收方接收数据速度较慢时,强制降低发送方的数据发送速度。也称为流量控制。请参阅第441页上的“消息系统”。
|
||||
接收方接收数据速度较慢时,强制降低发送方的数据发送速度。也称为流量控制。请参阅“[消息系统](ch11.md#消息系统)”。
|
||||
|
||||
|
||||
|
||||
### 批处理(batch process)
|
||||
|
||||
一种计算,它将一些固定的(通常是大的)数据集作为输入,并将其他一些数据作为输出,而不修改输入。见第十章。
|
||||
一种计算,它将一些固定的(通常是大的)数据集作为输入,并将其他一些数据作为输出,而不修改输入。见[第十章](ch10.md)。
|
||||
|
||||
|
||||
|
||||
### 边界(bounded)
|
||||
|
||||
有一些已知的上限或大小。例如,网络延迟情况(请参阅“超时和未定义的延迟”在本页281)和数据集(请参阅第11章的介绍)。
|
||||
有一些已知的上限或大小。例如,网络延迟情况(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”)和数据集(请参阅[第11章](ch11.md)的介绍)。
|
||||
|
||||
|
||||
|
||||
### 拜占庭故障(Byzantine fault)
|
||||
|
||||
表现异常的节点,这种异常可能以任意方式出现,例如向其他节点发送矛盾或恶意消息。请参阅第304页上的“拜占庭故障”。
|
||||
表现异常的节点,这种异常可能以任意方式出现,例如向其他节点发送矛盾或恶意消息。请参阅“[拜占庭故障](ch8.md#拜占庭故障)”。
|
||||
|
||||
|
||||
|
||||
@ -54,43 +54,43 @@
|
||||
|
||||
### CAP定理(CAP theorem)
|
||||
|
||||
一个被广泛误解的理论结果,在实践中是没有用的。参见第336页的“CAP定理”。
|
||||
一个被广泛误解的理论结果,在实践中是没有用的。参见“[CAP定理](ch9.md#CAP定理)”。
|
||||
|
||||
|
||||
|
||||
### 因果关系(causality)
|
||||
|
||||
事件之间的依赖关系,当一件事发生在另一件事情之前。例如,后面的事件是对早期事件的回应,或者依赖于更早的事件,或者应该根据先前的事件来理解。请参阅第186页上的“发生之前的关系和并发性”和第339页上的“排序和因果关系”。
|
||||
事件之间的依赖关系,当一件事发生在另一件事情之前。例如,后面的事件是对早期事件的回应,或者依赖于更早的事件,或者应该根据先前的事件来理解。请参阅“[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发)”和“[顺序与因果关系](ch5.md#顺序与因果关系)”。
|
||||
|
||||
|
||||
|
||||
### 共识(consensus)
|
||||
|
||||
分布式计算的一个基本问题,就是让几个节点同意某些事情(例如,哪个节点应该是数据库集群的领导者)。问题比乍看起来要困难得多。请参阅第364页上的“容错共识”。
|
||||
分布式计算的一个基本问题,就是让几个节点同意某些事情(例如,哪个节点应该是数据库集群的领导者)。问题比乍看起来要困难得多。请参阅“[容错共识](ch9.md#容错共识)”。
|
||||
|
||||
|
||||
|
||||
### 数据仓库(data warehouse)
|
||||
|
||||
一个数据库,其中来自几个不同的OLTP系统的数据已经被合并和准备用于分析目的。请参阅第91页上的“数据仓库”。
|
||||
一个数据库,其中来自几个不同的OLTP系统的数据已经被合并和准备用于分析目的。请参阅“[数据仓库](ch3.md#数据仓库)”。
|
||||
|
||||
|
||||
|
||||
### 声明式(declarative)
|
||||
|
||||
描述某些东西应有的属性,但不知道如何实现它的确切步骤。在查询的上下文中,查询优化器采用声明性查询并决定如何最好地执行它。请参阅第42页上的“数据的查询语言”。
|
||||
描述某些东西应有的属性,但不知道如何实现它的确切步骤。在查询的上下文中,查询优化器采用声明性查询并决定如何最好地执行它。请参阅“[数据查询语言](ch2.md#数据查询语言)”。
|
||||
|
||||
|
||||
|
||||
### 非规范化(denormalize)
|
||||
|
||||
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参见“单对象和多对象操作”(第228页)和“从同一事件日志中派生多个视图”(第461页)。
|
||||
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参见“[单对象和多对象操作](ch7.md#单对象和多对象操作)”和“[从同一事件日志中派生多个视图](ch11.md#从同一事件日志中派生多个视图)”。
|
||||
|
||||
|
||||
|
||||
### 衍生数据(derived data)
|
||||
|
||||
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。参见第三部分的介绍。
|
||||
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。参见[第三部分](part-iii.md)的介绍。
|
||||
|
||||
|
||||
|
||||
@ -102,31 +102,31 @@
|
||||
|
||||
### 分布式(distributed)
|
||||
|
||||
在由网络连接的多个节点上运行。对于部分节点故障,具有容错性:系统的一部分发生故障时,其他部分仍可以正常工作,通常情况下,软件无需了解故障相关的确切情况。请参阅第274页上的“故障和部分故障”。
|
||||
在由网络连接的多个节点上运行。对于部分节点故障,具有容错性:系统的一部分发生故障时,其他部分仍可以正常工作,通常情况下,软件无需了解故障相关的确切情况。请参阅“[故障与部分失效](ch8.md#故障与部分失效)”。
|
||||
|
||||
|
||||
|
||||
### 持久(durable)
|
||||
|
||||
以某种方式存储数据,即使发生各种故障,也不会丢失数据。请参阅第226页上的“持久性”。
|
||||
以某种方式存储数据,即使发生各种故障,也不会丢失数据。请参阅“[持久性(Durability)](ch7.md#持久性(Durability))”。
|
||||
|
||||
|
||||
|
||||
### ETL(Extract-Transform-Load)
|
||||
|
||||
提取-转换-加载(Extract-Transform-Load)。从源数据库中提取数据,将其转换为更适合分析查询的形式,并将其加载到数据仓库或批处理系统中的过程。请参阅第91页上的“数据仓库”。
|
||||
提取-转换-加载(Extract-Transform-Load)。从源数据库中提取数据,将其转换为更适合分析查询的形式,并将其加载到数据仓库或批处理系统中的过程。请参阅“[数据仓库](ch3.md#数据仓库)”。
|
||||
|
||||
|
||||
|
||||
### 故障切换(failover)
|
||||
|
||||
在具有单一领导者的系统中,故障切换是将领导角色从一个节点转移到另一个节点的过程。请参阅第156页的“处理节点故障”。
|
||||
在具有单一领导者的系统中,故障切换是将领导角色从一个节点转移到另一个节点的过程。请参阅“[处理节点宕机](ch5.md#处理节点宕机)”。
|
||||
|
||||
|
||||
|
||||
### 容错(fault-tolerant)
|
||||
|
||||
如果出现问题(例如,机器崩溃或网络连接失败),可以自动恢复。请参阅第6页上的“可靠性”。
|
||||
如果出现问题(例如,机器崩溃或网络连接失败),可以自动恢复。请参阅“[可靠性](ch1.md#可靠性)”。
|
||||
|
||||
|
||||
|
||||
@ -138,85 +138,85 @@
|
||||
|
||||
### 追随者(follower)
|
||||
|
||||
一种数据副本,仅处理领导者发出的数据变更,不直接接受来自客户端的任何写入。也称为辅助、仆从、只读副本或热备份。请参阅第152页上的“领导和追随者”。
|
||||
一种数据副本,仅处理领导者发出的数据变更,不直接接受来自客户端的任何写入。也称为辅助、仆从、只读副本或热备份。请参阅“[领导者与追随者](ch5.md#领导者与追随者)”。
|
||||
|
||||
|
||||
|
||||
### 全文检索(full-text search)
|
||||
|
||||
通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅第88页上的“全文搜索和模糊索引”。
|
||||
通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”。
|
||||
|
||||
|
||||
|
||||
### 图(graph)
|
||||
|
||||
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅第49页上的“和图相似的数据模型”。
|
||||
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅“[图数据模型](ch2.md#图数据模型)”。
|
||||
|
||||
|
||||
|
||||
### 散列(hash)
|
||||
|
||||
将输入转换为看起来像随机数值的函数。相同的输入会转换为相同的数值,不同的输入一般会转换为不同的数值,也可能转换为相同数值(也被称为冲突)。请参阅第203页的“根据键的散列值分隔”。
|
||||
将输入转换为看起来像随机数值的函数。相同的输入会转换为相同的数值,不同的输入一般会转换为不同的数值,也可能转换为相同数值(也被称为冲突)。请参阅“[根据键的散列分区](ch6.md#根据键的散列分区)”。
|
||||
|
||||
|
||||
|
||||
### 幂等(idempotent)
|
||||
|
||||
用于描述一种操作可以安全地重试执行,即执行多次的效果和执行一次的效果相同。请参阅第478页的“幂等”。
|
||||
用于描述一种操作可以安全地重试执行,即执行多次的效果和执行一次的效果相同。请参阅“[幂等性](ch11.md#幂等性)”。
|
||||
|
||||
|
||||
|
||||
### 索引(index)
|
||||
|
||||
一种数据结构。通过索引,你可以根据特定字段的值,在所有数据记录中进行高效检索。请参阅第70页的“让数据库更强大的数据结构”。
|
||||
一种数据结构。通过索引,你可以根据特定字段的值,在所有数据记录中进行高效检索。请参阅“[驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)”。
|
||||
|
||||
|
||||
|
||||
### 隔离性(isolation)
|
||||
|
||||
在事务上下文中,用于描述并发执行事务的互相干扰程度。串行运行具有最强的隔离性,不过其它程度的隔离也通常被使用。请参阅第225页的“隔离”。
|
||||
在事务上下文中,用于描述并发执行事务的互相干扰程度。串行运行具有最强的隔离性,不过其它程度的隔离也通常被使用。请参阅“[隔离性(Isolation)](ch7.md#隔离性(Isolation))”。
|
||||
|
||||
|
||||
|
||||
### 连接(join)
|
||||
|
||||
汇集有共同点的记录。在一个记录与另一个记录有关(外键,文档参考,图中的边)的情况下最常用,查询需要获取参考所指向的记录。请参阅第33页上的“多对一和多对多关系”和第393页上的“减少端连接和分组”。
|
||||
汇集有共同点的记录。在一个记录与另一个记录有关(外键,文档参考,图中的边)的情况下最常用,查询需要获取参考所指向的记录。请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”和“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)”。
|
||||
|
||||
|
||||
|
||||
### 领导者(leader)
|
||||
|
||||
当数据或服务被复制到多个节点时,由领导者分发已授权变更的数据副本。领导者可以通过某些协议选举产生,也可以由管理者手动选择。也被称为主人。请参阅第152页的“领导者和追随者”。
|
||||
当数据或服务被复制到多个节点时,由领导者分发已授权变更的数据副本。领导者可以通过某些协议选举产生,也可以由管理者手动选择。也被称为主人。请参阅“[领导者与追随者](ch5.md#领导者与追随者)”。
|
||||
|
||||
|
||||
|
||||
### 线性化(linearizable)
|
||||
|
||||
表现为系统中只有一份通过原子操作更新的数据副本。请参阅第324页的“线性化”。
|
||||
表现为系统中只有一份通过原子操作更新的数据副本。请参阅“[线性一致性](ch9.md#线性一致性)”。
|
||||
|
||||
|
||||
|
||||
### 局部性(locality)
|
||||
|
||||
一种性能优化方式,如果经常在相同的时间请求一些离散数据,把这些数据放到一个位置。请参阅第41页的“请求数据的局部性”。
|
||||
一种性能优化方式,如果经常在相同的时间请求一些离散数据,把这些数据放到一个位置。请参阅“[查询的数据局部性](ch2.md#查询的数据局部性)”。
|
||||
|
||||
|
||||
|
||||
### 锁(lock)
|
||||
|
||||
一种保证只有一个线程、节点或事务可以访问的机制,如果其它线程、节点或事务想访问相同元素,则必须等待锁被释放。请参阅第257页的“两段锁(2PL)”和301页的“领导者和锁”。
|
||||
一种保证只有一个线程、节点或事务可以访问的机制,如果其它线程、节点或事务想访问相同元素,则必须等待锁被释放。请参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”和“[领导者和锁](ch8.md#领导者和锁)”。
|
||||
|
||||
|
||||
|
||||
### 日志(log)
|
||||
|
||||
日志是一个只能以追加方式写入的文件,用于存放数据。预写式日志用于在存储引擎崩溃时恢复数据(请参阅第82页的“使二叉树更稳定”);结构化日志存储引擎使用日志作为它的主要存储格式(请参阅第76页的“有序字符串表和日志结构的合并树”);复制型日志用于把写入从领导者复制到追随者(请参阅第152页的“领导者和追随者”);事件性日志可以表现为数据流(请参阅第446页的“分段日志”)。
|
||||
日志是一个只能以追加方式写入的文件,用于存放数据。预写式日志用于在存储引擎崩溃时恢复数据(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”);结构化日志存储引擎使用日志作为它的主要存储格式(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”);复制型日志用于把写入从领导者复制到追随者(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”);事件性日志可以表现为数据流(请参阅“[分区日志](ch11.md#分区日志)”)。
|
||||
|
||||
|
||||
|
||||
### 物化(materialize)
|
||||
|
||||
急切地计算并写出结果,而不是在请求时计算。请参阅第101页的“聚合:数据立方和物化视图”和419页的“中间状态的物化”。
|
||||
急切地计算并写出结果,而不是在请求时计算。请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”和“[物化中间状态](ch10.md#物化中间状态)”。
|
||||
|
||||
|
||||
|
||||
@ -228,31 +228,31 @@
|
||||
|
||||
### 规范化(normalized)
|
||||
|
||||
以没有冗余或重复的方式进行结构化。 在规范化数据库中,当某些数据发生变化时,您只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。 请参阅第33页上的“多对一和多对多关系”。
|
||||
以没有冗余或重复的方式进行结构化。 在规范化数据库中,当某些数据发生变化时,您只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。 请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”。
|
||||
|
||||
|
||||
|
||||
### OLAP(Online Analytic Processing)
|
||||
|
||||
在线分析处理。 通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。 请参阅第90页上的“交易处理或分析?”。
|
||||
在线分析处理。 通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。 请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
|
||||
|
||||
|
||||
### OLTP(Online Transaction Processing)
|
||||
|
||||
在线事务处理。 访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。 请参阅第90页上的“交易处理或分析?”。
|
||||
在线事务处理。 访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。 请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
|
||||
|
||||
|
||||
|
||||
### 分区(partitioning)
|
||||
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。 也称为分片。 见第6章。
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。 也称为分片。见[第6章](ch6.md)。
|
||||
|
||||
|
||||
|
||||
### 百分位点(percentile)
|
||||
|
||||
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。 例如,某个时间段的第95个百分位响应时间是时间t,则该时间段中,95%的请求完成时间小于t,5%的请求完成时间要比t长。 请参阅第13页上的“描述性能”。
|
||||
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。 例如,某个时间段的第95个百分位响应时间是时间t,则该时间段中,95%的请求完成时间小于t,5%的请求完成时间要比t长。 请参阅“[描述性能](ch1.md#描述性能)”。
|
||||
|
||||
|
||||
|
||||
@ -264,31 +264,31 @@
|
||||
|
||||
### 法定人数(quorum)
|
||||
|
||||
在操作完成之前,需要对操作进行投票的最少节点数量。 请参阅第179页上的“读写的法定人数”。
|
||||
在操作完成之前,需要对操作进行投票的最少节点数量。 请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。
|
||||
|
||||
|
||||
|
||||
### 再平衡(rebalance)
|
||||
|
||||
将数据或服务从一个节点移动到另一个节点以实现负载均衡。 请参阅第209页上的“再平衡分区”。
|
||||
将数据或服务从一个节点移动到另一个节点以实现负载均衡。 请参阅“[分区再平衡](ch6.md#分区再平衡)”。
|
||||
|
||||
|
||||
|
||||
### 复制(replication)
|
||||
|
||||
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅第5章。
|
||||
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅[第5章](ch5.md)。
|
||||
|
||||
|
||||
|
||||
### 模式(schema)
|
||||
|
||||
一些数据结构的描述,包括其字段和数据类型。 可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅第39页上的“文档模型中的模式灵活性”),模式可以随时间变化(请参阅第4章)。
|
||||
一些数据结构的描述,包括其字段和数据类型。 可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第4章](ch4.md))。
|
||||
|
||||
|
||||
|
||||
### 次级索引(secondary index)
|
||||
|
||||
与主要数据存储器一起维护的附加数据结构,使您可以高效地搜索与某种条件相匹配的记录。 请参阅第85页上的“其他索引结构”和第206页上的“分区和二级索引”。
|
||||
与主要数据存储器一起维护的附加数据结构,使您可以高效地搜索与某种条件相匹配的记录。 请参阅“[其他索引结构](ch3.md#其他索引结构)”和“[分区与次级索引](ch6.md#分区与次级索引)”。
|
||||
|
||||
|
||||
|
||||
@ -300,33 +300,33 @@
|
||||
|
||||
### 无共享(shared-nothing)
|
||||
|
||||
与共享内存或共享磁盘架构相比,独立节点(每个节点都有自己的CPU,内存和磁盘)通过传统网络连接。 见第二部分的介绍。
|
||||
与共享内存或共享磁盘架构相比,独立节点(每个节点都有自己的CPU,内存和磁盘)通过传统网络连接。 见[第二部分](part-ii.md)的介绍。
|
||||
|
||||
|
||||
|
||||
### 偏斜(skew)
|
||||
|
||||
1.各分区负载不平衡,例如某些分区有大量请求或数据,而其他分区则少得多。也被称为热点。请参阅第205页上的“工作负载偏斜和减轻热点”和第407页上的“处理偏斜”。
|
||||
1.各分区负载不平衡,例如某些分区有大量请求或数据,而其他分区则少得多。也被称为热点。请参阅“[负载偏斜和热点消除](ch6.md#负载偏斜和热点消除)”和“[处理偏斜](ch10.md#处理偏斜)”。
|
||||
|
||||
2.时间线异常导致事件以不期望的顺序出现。 请参阅第237页上的“快照隔离和可重复读取”中的关于读取偏斜的讨论,第246页上的“写入偏斜和模糊”中的写入偏斜以及第291页上的“订购事件的时间戳”中的时钟偏斜。
|
||||
2.时间线异常导致事件以不期望的顺序出现。 请参阅“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中的关于读取偏斜的讨论,“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中的写入偏斜以及“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中的时钟偏斜。
|
||||
|
||||
|
||||
|
||||
### 脑裂(split brain)
|
||||
|
||||
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。 请参阅第156页的“处理节点中断”和第300页的“真相由多数定义”。
|
||||
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。 请参阅“[处理节点宕机](ch5.md#处理节点宕机)”和“[真相由多数所定义](ch8.md#真相由多数所定义)”。
|
||||
|
||||
|
||||
|
||||
### 存储过程(stored procedure)
|
||||
|
||||
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。 请参阅第252页的“实际串行执行”。
|
||||
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。 请参阅“[真的串行执行](ch7.md#真的串行执行)”。
|
||||
|
||||
|
||||
|
||||
### 流处理(stream process)
|
||||
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。 见第11章。
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。 见[第11章](ch11.md)。
|
||||
|
||||
|
||||
|
||||
@ -338,37 +338,37 @@
|
||||
|
||||
### 记录系统(system of record)
|
||||
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。 参见第三部分的介绍。
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。 参见[第三部分](part-iii.md)的介绍。
|
||||
|
||||
|
||||
|
||||
### 超时(timeout)
|
||||
|
||||
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。 但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。 请参阅第281页上的“超时和无限延迟”。
|
||||
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。 但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。 请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”。
|
||||
|
||||
|
||||
|
||||
### 全序(total order)
|
||||
|
||||
一种比较事物的方法(例如时间戳),可以让您总是说出两件事中哪一件更大,哪件更小。 总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。 请参见第341页的“因果顺序不是全序”。
|
||||
一种比较事物的方法(例如时间戳),可以让您总是说出两件事中哪一件更大,哪件更小。 总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。 请参见“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
|
||||
|
||||
|
||||
|
||||
### 事务(transaction)
|
||||
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。 见第7章。
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。 见[第7章](ch7.md)。
|
||||
|
||||
|
||||
|
||||
### 两阶段提交(2PC, two-phase commit)
|
||||
|
||||
一种确保多个数据库节点全部提交或全部中止事务的算法。 请参阅第354页上的“原子提交和两阶段提交(2PC)”。
|
||||
一种确保多个数据库节点全部提交或全部中止事务的算法。 请参阅[原子提交与二阶段提交(2PC)](ch9.md#原子提交与二阶段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
### 两阶段锁定(2PL, two-phase locking)
|
||||
|
||||
一种用于实现可序列化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。 请参阅第257页上的“两阶段锁定(2PL)”。
|
||||
一种用于实现可序列化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。 请参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”。
|
||||
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
如果在世界各地都有用户,你也许会考虑在全球范围部署多个服务器,从而每个用户可以从地理上最近的数据中心获取服务,避免了等待网络数据包穿越半个世界。
|
||||
|
||||
### 伸缩至更高的载荷
|
||||
## 伸缩至更高的载荷
|
||||
|
||||
如果你需要的只是伸缩至更高的**载荷(load)**,最简单的方法就是购买更强大的机器(有时称为**垂直伸缩(vertical scaling)**或**向上伸缩(scale up)**)。许多处理器,内存和磁盘可以在同一个操作系统下相互连接,快速的相互连接允许任意处理器访问内存或磁盘的任意部分。在这种 **共享内存架构(shared-memory architecture)** 中,所有的组件都可以看作一台单独的机器[^i]。
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
[^ii]: 网络附属存储(Network Attached Storage, NAS),或**存储区网络(Storage Area Network, SAN)**
|
||||
|
||||
#### 无共享架构
|
||||
### 无共享架构
|
||||
|
||||
相比之下,**无共享架构(shared-nothing architecture)**(有时称为**水平伸缩(horizontal scale)** 或**向外伸缩(scale out)**)已经相当普及。在这种架构中,运行数据库软件的每台机器/虚拟机都称为**节点(node)**。每个节点只使用各自的处理器,内存和磁盘。节点之间的任何协调,都是在软件层面使用传统网络实现的。
|
||||
|
||||
|
@ -6,15 +6,15 @@
|
||||
|
||||
本书的最后一部分,会研究将多个不同数据系统(可能有着不同数据模型,并针对不同的访问模式进行优化)集成为一个协调一致的应用架构时,会遇到的问题。软件供应商经常会忽略这一方面的生态建设,并声称他们的产品能够满足你的所有需求。在现实世界中,集成不同的系统是实际应用中最重要的事情之一。
|
||||
|
||||
## 记录和衍生数据系统
|
||||
## 记录系统和衍生数据系统
|
||||
|
||||
从高层次上看,存储和处理数据的系统可以分为两大类:
|
||||
|
||||
#### 记录系统(System of record)
|
||||
***记录系统(System of record)***
|
||||
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**标准化的(normalized)**)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**正规化的(normalized)**)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
|
||||
#### 衍生数据系统(Derived data systems)
|
||||
***衍生数据系统(Derived data systems)***
|
||||
|
||||
**衍生系统**中的数据,通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。如果丢失衍生数据,可以从原始来源重新创建。典型的例子是**缓存(cache)**:如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常衍生自用户日志。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user