Merge branch 'master' into master

This commit is contained in:
Feng Ruohang 2018-03-24 16:45:28 +08:00 committed by GitHub
commit b1bf5d111b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 173 deletions

View File

@ -3,15 +3,22 @@
- 作者: [Martin Kleppmann](https://martin.kleppmann.com)
- 原书名称:[《Designing Data-Intensive Application》](http://shop.oreilly.com/product/0636920032175.do)
- 译者:[冯若航]( http://vonng.com/about) fengruohang@outlook.com
- Gitbook地址[ddia-cn](https://www.gitbook.com/book/vonng/ddia-cn)(需要科学上网)
- 建议使用[Typora](https://www.typora.io)或Gitbook以获取最佳阅读体验。
-------------
## 法律声明
译者纯粹出于学习目的与个人兴趣翻译,本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
从原作者处得知已经有简体中文的翻译计划将于2018年末完成。
译者纯粹出于**学习目的**与**个人兴趣**翻译本书,不追求任何经济利益。
译者保留对此版本译文的署名权,其他权利以原作者和出版社的主张为准。
本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
译者保留对译文的署名权,其他权利以原作者和出版社的主张为准,侵删。
## 译序
@ -51,24 +58,24 @@
### [数据系统的基石](part-i.md)
1. [可靠性、可扩展性、可维护性](ch1.md)
2. [数据模型与查询语言](ch2.md)
3. [存储与检索](ch3.md)
4. [编码与演化](ch4.md)
* [第一章:可靠性、可扩展性、可维护性](ch1.md)
* [第二章:数据模型与查询语言](ch2.md)
* [第三章:存储与检索](ch3.md)
* [第四章:编码与演化](ch4.md)
### [分布式数据](part-ii.md)
1. [复制](ch5.md)
2. [分片](ch6.md)
3. [事务](ch7.md)
4. [分布式系统的麻烦](ch8.md)
5. [一致性与共识](ch9.md)
* [第五章:复制](ch5.md)
* [第六章:分区](ch6.md)
* [第七章:事务](ch7.md)
* [第八章:分布式系统的麻烦](ch8.md)
* [第九章:一致性与共识](ch9.md)
### [派生数据](part-iii.md)
1. [批处理](ch10.md)
2. [流处理](ch11.md)
3. [数据系统的未来](ch12.md)
* [第十章:批处理](ch10.md)
* [第十一章:流处理](ch11.md)
* [第十二章:数据系统的未来](ch12.md)
### [术语表](glossary.md)
@ -82,9 +89,7 @@
* 机翻:只在乎结构:梳理文章结构、图片、引用、备注。
* 初翻:保证经完全理解本章内容,人工修复显著的错误,重新组织语言。
* 精翻:阅读相关领域文献书籍,确定术语的最终译法,修复格式瑕疵,着力信达雅。
通常机翻一章1个小时左右初翻一章6小时精翻一章三到五天。
* 精校:阅读相关领域文献书籍,确定术语的最终译法,修复格式瑕疵,着力信达雅。
精翻可以看,机翻基本没法看,初翻对于业内人士能凑合看。
@ -97,9 +102,9 @@
| 第三章:存储与检索 | 初翻 | |
| 第四章:编码与演化 | 初翻 | |
| 第二部分:分布式数据——概览 | 精翻 | |
| 第五章:复制 | 精翻 30% | |
| 第六章:分 | 初翻 | |
| 第七章:事务 | 精翻 60% | |
| 第五章:复制 | 精翻 30% | Vonng |
| 第六章:分 | 初翻 | |
| 第七章:事务 | 精翻 60% | Vonng |
| 第八章:分布式系统中的问题 | 初翻 | |
| 第九章:一致性与共识 | 初翻 30% | Vonng |
| 第三部分:前言 | 精翻 | |
@ -110,17 +115,24 @@
| 后记 | 机翻 | |
计划在3月25日前完成所有章节的初翻。
计划在3月完成所有章节的初翻。
## CONTRIBUTION
欢迎贡献初翻后的章节接受ISSUE指正。
欢迎贡献
贡献者需要同意[法律声明](#法律声明)所叙内容,翻译请提前联系以免冲突
整章的翻译、精校请使用PR将列入译者署名少量词法、语法、译法问题请使用ISSUE将列入致谢
有人建议拉个群,也许发布更新通知或者同步翻译进度吧
All contribution will give proper credit. 贡献者需要同意[法律声明](#法律声明)所叙内容。
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点修正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
## 译读者交流微信群
![](img/ddia-wexin.JPG)

View File

@ -9,7 +9,7 @@
* [第四章:编码与演化](ch4.md)
* [第二部分:分布式数据](part-ii.md)
* [第五章:复制](ch5.md)
* [第六章:分](ch6.md)
* [第六章:分](ch6.md)
* [第七章:事务](ch7.md)
* [第八章:分布式系统的麻烦](ch8.md)
* [第九章:一致性与共识](ch9.md)

2
ch5.md
View File

@ -919,4 +919,4 @@ LWW实现了最终收敛的目标但以**持久性**为代价:如果同一
| 上一章 | 目录 | 下一章 |
| :--------------------------------: | :-----------------------------: | :--------------------: |
| [第二部分:分布式数据](part-ii.md) | [设计数据密集型应用](README.md) | [第六章:分](ch6.md) |
| [第二部分:分布式数据](part-ii.md) | [设计数据密集型应用](README.md) | [第六章:分](ch6.md) |

31
ch6.md
View File

@ -2,7 +2,7 @@
![](img/ch6.png)
> 我们必须跳出电脑指令序列的窠臼。 叙述定义、提供数据的描述和优先级、梳理关系,而不是编写过程。
> 我们必须跳出电脑指令序列的窠臼。 叙述定义、描述元数据、梳理关系,而不是编写过程。
>
> —— Grace Murray Hopper未来的计算机及其管理1962
>
@ -11,9 +11,9 @@
[TOC]
在[第5章](ch5.md)中,我们讨论了复制 - 即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区partitions**,也称为**分片sharding**[^i]
[^i]: 正如本章所讨论的分区是一种有意将大型数据库分解成小型数据库的方式。它与网络分区net splits无关这是节点之间网络中的一种故障类型。我们将在第8章讨论这些错误。
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与**网络分区net splits**无关,这是节点之间网络中的一种故障类型。我们将在[第8章](ch8.md)讨论这些错误。
> ##### 术语澄清
>
@ -22,13 +22,13 @@
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
分区主要为了**可扩展性**。不同的分区可以放在不共享集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
分区主要为了**可扩展性**。不同的分区可以放在不共享集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
对于在单个分区上运行的查询,每个节点可以独立执行对自己的分区的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的有些系统则用于分析参阅“[事务处理或分析]”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[再平衡](),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[重新平衡分区](#重新平衡分区),如果想要添加或删除群集中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
## 分区与复制
@ -111,7 +111,9 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
也许在将来,数据系统将能够自动检测和补偿偏斜的工作负载;但现在,您需要自己来权衡。
## 分区与次级索引
## 分片与次级索引
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
@ -123,7 +125,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
### 按文档的二级索引
假设您正在经营一个销售二手车的网站如图6-4所示。 每个列表都有一个唯一的ID - 称之为文档ID - 并且用文档ID对数据库进行分区例如分区0中的ID为 0到499分区1中的ID为500到999等
假设你正在经营一个销售二手车的网站(如[图6-4](img/fig6-4.png)所示)。 每个列表都有一个唯一的ID——称之为文档ID——并且用文档ID对数据库进行分区例如分区0中的ID 0到499分区1中的ID 500到999等
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段field**,关系数据库中这些是**列column** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`colorred`的文档ID列表中。
@ -135,15 +137,17 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
在这种索引方法中每个分区是完全独立的每个分区维护自己的二级索引仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时您需要写入数据库添加删除或更新文档只需处理包含您正在编写的文档ID的分区即可。出于这个原因**文档分区索引**也被称为**本地索引local index**(而不是将在下一节中描述的**全局索引global index**)。
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在图6-4中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
但是从文档分区索引中读取需要注意除非您对文档ID做了特别的处理否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中红色汽车出现在分区0和分区1中。因此如果要搜索红色汽车则需要将查询发送到所有分区并合并所有返回的结果。
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”。然而它被广泛使用MonDBDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个二级索引时例如同时需要按颜色和制造商查询
这种查询分区数据库的方法有时被称为**分散/聚集scatter/gather**,并且可能会使二级索引上的读取查询相当昂贵。即使您并行查询分区,分散/聚集也容易导致尾部延迟放大请参阅第16页的“实践中的百分比”。然而它被广泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和VoltDB 【19】,都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。
### 根据关键词(Term)的二级索引
我们可以构建一个覆盖所有分区数据的**全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
[图6-5](img/fig6-5.png)阐述了这可能是什么样子来自所有分区的红色汽车在红色索引中并且索引是分区的首字母从a到r的颜色在分区0中s到z的在分区1。汽车制造商的索引也与之类似分区边界在f和h之间
[图6-5](img/fig6-5.png)阐述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从`a``r`的颜色在分区0中`s``z`的在分区1。汽车制造商的索引也与之类似分区边界在`f``h`之间)。
![](img/fig6-5.png)
@ -177,6 +181,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
* 再平衡发生时,数据库应该继续接受读取和写入。
* 节点之间只移动必须的数据以便快速再平衡并减少网络和磁盘I/O负载。
### 平衡策略
有几种不同的分区分配方法【23】,让我们依次简要讨论一下。
@ -187,7 +192,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
也许你想知道为什么我们不使用***mod***(许多编程语言中的%运算符)。例如,`hash(key) mod 10`会返回一个介于0和9之间的数字如果我们将散列写为十进制数散列模10将是最后一个数字。如果我们有10个节点编号为0到9这似乎是将每个键分配给一个节点的简单方法。
模N方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的移动使再平衡的过程过于昂贵。
$N$方法的问题是如果节点数量N发生变化大多数密钥将需要从一个节点移动到另一个节点。例如假设$hash(key)=123456$。如果最初有10个节点那么这个键一开始放在节点6上因为$123456\ mod\ 10 = 6$。当您增长到11个节点时密钥需要移动到节点3$123456\ mod\ 11 = 3$当您增长到12个节点时需要移动到节点0$123456\ mod\ 12 = 0$)。这种频繁的举动使得重新平衡过于昂贵。
我们需要一种只移动必需数据的方法。
@ -227,7 +232,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
通过动态分区,分区的数量与数据集的大小成正比,因为拆分和合并过程将每个分区的大小保持在固定的最小值和最大值之间。另一方面,对于固定数量的分区,每个分区的大小与数据集的大小成正比。在这两种情况下,分区的数量都与节点的数量无关。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比 - 换句话说每个节点具有固定数量的分区【23, 27, 28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比——换句话说每个节点具有固定数量的分区【23,27,28】。在这种情况下每个分区的大小与数据集大小成比例地增长而节点数量保持不变但是当增加节点数时分区将再次变小。由于较大的数据量通常需要较大数量的节点进行存储因此这种方法也使每个分区的大小较为稳定。
当一个新节点加入集群时它随机选择固定数量的现有分区进行拆分然后占有这些拆分分区中每个分区的一半同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割但是平均在更大数量的分区上时在Cassandra中默认情况下每个节点有256个分区新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再分配的算法来避免不公平的分割【29】。

2
ch7.md
View File

@ -958,4 +958,4 @@ WHERE room_id = 123 AND
| 上一章 | 目录 | 下一章 |
| ---------------------- | ------------------------------- | ---------------------------------- |
| [第六章:分](ch6.md) | [设计数据密集型应用](README.md) | [第八章:分布式系统的麻烦](ch7.md) |
| [第六章:分](ch6.md) | [设计数据密集型应用](README.md) | [第八章:分布式系统的麻烦](ch7.md) |

265
ch9.md
View File

@ -47,40 +47,40 @@
本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的:
* 首先看一下常用的**最强一致性模型**之一,**线性linearizability**,并考察其优缺点。
* 首先看一下常用的**最强一致性模型**之一,**线性一致性linearizability**,并考察其优缺点。
* 然后我们将检查分布式系统中[**事件顺序**](#顺序保证)的问题,特别是因果关系和全局顺序的问题。
* 在第三部分(“[分布式事务和共识](#分布式事务和共识)”)中将探讨如何自动提交分布式事务,这将最终引导我们解决共识问题。
## 线性
## 线性一致性
在**最终一致**的数据库,如果你在同一时刻问两个不同副本相同的问题,可能会得到两个不同的答案。这很让人困惑。如果数据库可以提供只有一个副本的假象(即,只有一个数据副本),那么事情就简单太多了。那么每个客户端都会有相同的数据视图,且不必担心复制滞后了。
这就是**线性linearizability**背后的想法【6】也称为**原子一致性atomic consistency**【7】**强一致性strong consistency****立即一致性immediate consistency**或**外部一致性external consistency **【8】。线性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。
这就是**线性一致性linearizability**背后的想法【6】也称为**原子一致性atomic consistency**【7】**强一致性strong consistency****立即一致性immediate consistency**或**外部一致性external consistency **【8】。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。
在一个线性系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。维护数据的单个副本的错觉是指,系统能保障读到的值是最近的,最新的,不是来自一个陈旧的缓存或副本。换句话说,线性是一个**新鲜度保证recency guarantee**。为了阐明这个想法,我们来看看一个不可线性化系统的例子。
在一个线性一致性系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。维护数据的单个副本的错觉是指,系统能保障读到的值是最近的,最新的,不是来自一个陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证recency guarantee**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。
![](img/fig9-1.png)
**图9-1 这个系统是非线性化的,导致球迷迷惑**
**图9-1 这个系统是非线性一致的,导致了球迷的困惑**
[图9-1](img/fig9-1.png)展示了一个非线性关于体育网站的例子【9】。Alice和Bob正坐在同一个房间里都盯着各自的手机关注着2014年FIFA世界杯决赛的结果。在最后得分公布后Alice刷新页面看到宣布了获胜者并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机但他的请求路由到了一个落后的数据库副本上手机显示比赛仍在进行。
[图9-1](img/fig9-1.png)展示了一个非线性一致性关于体育网站的例子【9】。Alice和Bob正坐在同一个房间里都盯着各自的手机关注着2014年FIFA世界杯决赛的结果。在最后得分公布后Alice刷新页面看到宣布了获胜者并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机但他的请求路由到了一个落后的数据库副本上手机显示比赛仍在进行。
如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分**之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性的要求。
如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分**之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。
### 什么使得系统线性一致?
线性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性化,让我们看看更多的例子。
线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。
[图9-2](img/fig9-2.png)显示了三个客户端在线性数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器register**,例如它可以是键值存储中的一个**键**,关系数据库中的一**行**或文档数据库中的一个**文档**。
[图9-2](img/fig9-2.png)显示了三个客户端在线性一致数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器register**,例如它可以是键值存储中的一个**键**,关系数据库中的一**行**或文档数据库中的一个**文档**。
![](img/fig9-2.png)
**图9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值**
为了简单起见,[图9-2](img/fig9-2.png)仅显示了来自客户端的请求,而不是数据库的内部。每个小节都是由客户端发出的请求,其中小节的开始是发送请求的时间,小节的结尾是客户端收到响应的时间。由于可变的网络延迟,客户端不知道恰恰在数据库处理它的请求时——它只知道它在发送请求和接收响应的客户端之间的某个时间已经发生了。[^i]
为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i]
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”但这种假设是允许的为了分析分布式算法我们可以假设一个精确的全局时钟存在不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
@ -89,195 +89,196 @@
* $ read(x)⇒v$表示客户端请求读取寄存器`x`的值,数据库返回值`v`。
* $write(x,v)⇒r$表示客户端请求将寄存器`x`设置为值`v`,数据库返回响应`r`(可能正确,可能错误)。
* $write(x,v)⇒r$表示客户端请求将寄存器`x`设置为值`v`,数据库返回响应`r`(可能正确,可能错误)。
在[图9-2](img/fig9-2.png)中x的值最初为0客户端C执行写请求将其设置为1。发生这种情况时客户端A和B反复轮询数据库以读取最新值。 A和B可能会为他们的阅读请求得到什么回应?
在[图9-2](img/fig9-2.png)中x的值最初为`0`客户端C执行写请求将其设置为`1`。发生这种情况时客户端A和B反复轮询数据库以读取最新值。 A和B的请求可能会收到怎样的响应?
* 客户端A的第一个读取操作在写入开始之前完成因此必须返回旧值0
* 客户端A的最后一次读操作是在写操作完成后开始的所以如果数据库是线性化的它必须定期返回新的值1我们知道写操作必须在写操作开始和结束之间的某个时间被处理并且读取操作必须在读取操作的开始和结束之间的某个时间进行处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。
* 与写操作时间重叠的任何读操作可能会返回0或1因为我们不知道写操作在处理读操作时是否生效。这些操作与写入同时发生
* 客户端A的第一个读操作,完成于写操作开始之前,因此必须返回旧值`0`
* 客户端A的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值`1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。
* 与写操作在时间上重叠的任何读操作可能会返回0或1因为我们不知道读取时写操作是否已经生效。这些操作是**并发concurrent**的
但是,这还不足以完全描述线性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii]
但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii]
[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为**常规寄存器regular register**【7,25】
为了使系统可线性化,我们需要添加另一个约束,如**图9-3**所示
为了使系统线性一致,我们需要添加另一个约束,如**图9-3**所示
![](img/fig9-3.png)
**图9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。**
在一个可线性化的系统中我们可以想象在x的值自动翻转从0到1的时候在写操作的开始和结束之间必定有一个时间点。因此如果一个客户端的读取返回新的值1即使写操作尚未完成所有后续读取也必须返回新值。
在一个线性一致的系统中我们可以想象在x的值自动翻转从0到1的时候在写操作的开始和结束之间必定有一个时间点。因此如果一个客户端的读取返回新的值1即使写操作尚未完成所有后续读取也必须返回新值。
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后B开始新的读取。由于B的读取严格在A的读取之后发生因此即使C的写入仍在进行中也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同在Alice读取新值之后Bob也希望读取新的值。
[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后B开始新的读取。由于B的读取严格在发生于A的读取之后因此即使C的写入仍在进行中也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同在Alice读取新值之后Bob也希望读取新的值。
我们可以进一步细化这个时序图,以可视化每个操作在某个时间点以原子方式生效。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。
在[图9-4]()中,除了读写之外,还增加了第三种类型的操作:
* $cas(x, v_{old}, v_{new})$⇒r表示客户端请求进行原子[**比较与设置**](ch7.md#比较并设置CAS)操作。如果寄存器$x$的当前值等于$v_{old}$,则应该原子地设置为$v_{new}$。如果$x≠vold$,则操作应该保持寄存器不变并返回一个错误。 $r$是数据库的响应(正确或错误)。
* $cas(x, v_{old}, v_{new})$⇒r表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置CAS)操作。如果寄存器$x$的当前值等于$v_{old}$,则应该原子地设置为$v_{new}$。如果$x≠vold$,则操作应该保持寄存器不变并返回一个错误。 $r$是数据库的响应(正确或错误)。
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的小节内部)。这些标记按顺序连接,结果必须是一个有效的寄存器读写序列(每次读取都必须返回最近一次写入设置的值)。
[图9-4]()中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
线性化的要求是,连接操作标记的线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新性保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
![](img/fig9-4.png)
**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是可线性化的**
**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是线性一致性的**
[图9-4]()中有一些有趣的细节需要指出:
* 第一个客户端B发送一个读取x的请求然后客户端D发送一个请求将x设置为0然后客户端A发送请求将x设置为1.不过返回到B的读取值为1写入的值由A。这是可以的这意味着数据库首先处理D的写入然后是A的写入最后是B的读取。虽然这不是发送请求的顺序但这是一个可以接受的顺序因为这三个请求是并发的。也许B的读取请求在网络上略有延迟,所以在两次写入之后它只能到达数据库。
* 第一个客户端B发送一个读取`x`的请求然后客户端D发送一个请求将`x`设置为`0`然后客户端A发送请求将`x`设置为`1`。尽管如此返回到B的读取值为`1`由A写入的值。这是可以的这意味着数据库首先处理D的写入然后是A的写入最后是B的读取。虽然这不是请求发送的顺序但这是一个可以接受的顺序因为这三个请求是并发的。也许B的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。
* 在客户端A从数据库收到响应之前客户端B的读取返回1表示写入值1已成功。这也是可以的这并不意味着在写入之前读取了值这只是意味着从数据库到客户端A的正确响应在网络中稍微延迟。
* 在客户端A从数据库收到响应之前客户端B的读取返回`1`,表示写入值`1`已成功。这也是可以的:这并不意味着在写之前读到了值这只是意味着从数据库到客户端A的正确响应在网络中略有延迟。
* 此模型不承担任何事务隔离另一个客户端可能随时更改值。例如C首先读取1然后读取2因为两次读取之间的值由B更改。可以使用原子**比较并设置cas**操作来检查该值是否未被另一客户端同时更改B和C的cas请求成功但是D的cas请求失败在数据库处理它时 x的值不再是0)。
* 此模型不假设有任何事务隔离另一个客户端可能随时更改值。例如C首先读取`1`,然后读取`2`因为两次读取之间的值由B更改。可以使用原子**比较并设置cas**操作来检查该值是否未被另一客户端同时更改B和C的**cas**请求成功但是D的**cas**请求失败(在数据库处理它时,`x`的值不再是`0`)。
* 客户B的最后读取(阴影栏中)不是线性化的。 该操作与C的cas写操作同时发生它将x从2更新为4.在没有其他请求的情况下B的读取返回2是可以的。然而在B的读取开始之前客户端A已经读取了新的值4 因此不允许B读取比A更旧的值。再次与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。
* 客户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](#两阶段锁定2PL)”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。
>
> 但是,可序列化的快照隔离(请参见“[可序列化的快照隔离SSI](ch7.md#可序列化的快照隔离SSI)”)不是线性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点在于,它不包括比快照更新的写入,因此从快照读取不是线性化的。
> 但是,可序列化的快照隔离(请参见“[可序列化的快照隔离SSI](ch7.md#可序列化的快照隔离SSI)”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。
### 依赖线性一致性
线性在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:过了几秒钟的结果不可能在这种情况下造成任何真正的伤害。然而,线性化对于使系统正确工作的一个重要条件是有一些领域
线性一致性在什么情况下有用?观看体育比赛的最后得分可能是一个轻率的例子:过了几秒钟的结果不可能在这种情况下造成任何真正的伤害。然而对于少数领域,线性一致性是系统正确工作的一个重要条件
#### 锁定和领导选举
一个使用单主复制的系统需要确保确实只有一个领导而不是几个脑裂。选择领导者的一种方法是使用一个锁每个启动的节点试图获得锁而成功的则成为领导者【14】。不管这个锁是如何实现的它必须是可线性化的所有节点必须同意哪个节点拥有锁否则就没用了。
一个使用单主复制的系统需要确保领导真的只有一个而不是几个脑裂。一种选择领导者的方法是使用锁每个节点在启动时尝试获取锁成功者成为领导者【14】。不管这个锁是如何实现的它必须是线性一致的所有节点必须就哪个节点拥有锁达成一致否则就没用了。
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。他们使用一致性算法以容错的方式实现可线性化的操作(我们在本章后面讨论这样的算法,在“[容错共识](#容错共识)”中[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择例如请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是线性存储服务是这些协调任务的基础。
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择例如请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是线性一致性存储服务是这些协调任务的基础。
[^iii]: 严格地说ZooKeeper和etcd提供可线性化的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性读取etcd调用这个法定读取【16】而在ZooKeeper中您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#)使用全局顺序广播实现线性存储”。
[^iii]: 严格地说ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性一致性读取etcd调用这个法定读取【16】而在ZooKeeper中您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。
分布式锁也在一些分布式数据库如Oracle Real Application ClustersRAC【18】中以更细化的级别使用。 RAC对每个磁盘页面使用一个锁多个节点共享对同一个磁盘存储系统的访问权限。由于这些可线性化的锁处于事务执行的关键路径上RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
分布式锁也在一些分布式数据库如Oracle Real Application ClustersRAC【18】中以更细的粒度使用。 RAC对每个磁盘页面使用一个锁多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
#### 限制和唯一性保证
#### 约束和唯一性保证
唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性
唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性一致性
这种情况实际上类似于一个锁:当一个用户注册你的服务时,你可以想到他们获得了他们所选用户名的“锁定”。该操作也非常类似于原子比较和设置将用户名设置为声明它的用户的ID,前提是用户名尚未被使用。
这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。
如果想要确保银行账户余额永远不会为负数,或者你没有出售比仓库里的库存更多的物品,或者两个人不同时在同一个位置一个航班或一个剧院。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。
然而,一个硬性的唯一性约束,比如你通常在关系数据库中发现的约束,需要线性化。其他类型的约束,如外键或属性约束,可以在不需要线性化的情况下实现【19】。
然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。
#### 跨道的时间依赖性
#### 跨道的时间依赖性
注意[图9-1](img/fig9-1.png)中的一个细节如果Alice没有惊呼得分Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面并最终看到最后的分数。由于系统中存在额外信道Alice对Bob的耳朵的声音线性化破坏才被注意到。
注意[图9-1](img/fig9-1.png)中的一个细节如果Alice没有惊呼得分Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面并最终看到最后的分数。由于系统中存在额外的信道Alice的声音传到了Bob的耳朵中线性一致性的违背才被注意到。
计算机系统也会出现类似的情况。例如,假设您有一个用户可以上传照片的网站,而且后台进程会调整照片大小来降低分辨率,从而加快下载速度(缩略图)。该系统的体系结构和数据流如[图9-5](img/fig9-5.png)所示。
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图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](img/fig9-5.png)中的步骤3和4可能比存储服务内部的复制更快。在这种情况下调整器读取图像步骤5可能会看到图像的旧版本或者什么都没有。如果它处理旧版本的图像则文件存储中的全尺寸和调整大小的图像永久不一致。
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4可能比存储服务内部的复制更快。在这种情况下缩放器读取图像步骤5可能会看到图像的旧版本或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和略缩图就产生了永久性的不一致。
出现这个问题是因为Web服务器和调整器之间存在两个不同的通信通道:文件存储器和消息队列。没有线性化的新近性保证,这两个通道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png)两个通信渠道之间也存在竞争状态数据库复制和Alice口中与Bob耳朵之间的真实音频通道
出现这个问题是因为Web服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png)数据库复制与Alice的嘴到Bob耳朵之间的真人音频信道之间也存在竞争条件
线性化并不是避免这种竞争条件的唯一方法但它是最容易理解的。如果您控制附加的通信通道例如在消息队列的情况下而不是在Alice和Bob的情况下则可以使用与我们在“[读己之写](ch5.md#读己之写)”额外的复杂性的成本
线性一致性并不是避免这种竞争条件的唯一方法但它是最容易理解的。如果你可以控制额外信道例如消息队列的例子而不是在Alice和Bob的例子则可以使用在“[读己之写](ch5.md#读己之写)”讨论过的备选方法,不过会有额外的复杂度代价
### 实现线性一致的系统
现在我们已经看了几个线性化有用的例子,让我们考虑一下如何实现一个提供可线性化语义的系统。
我们已经见到了几个线性一致性有用的例子,让我们思考一下,如何实现一个提供线性一致语义的系统。
由于线性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是只使用一个数据副本。但是,这种方法将无法处理错误:如果持有该副本的节点失败,数据将会丢失,或者至少无法访问,直到节点重新启动。
由于线性一致性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。
使系统容错最常用的方法是使用复制。我们再来回顾第5章中的复制方法并比较它们是否可以线性化
使系统容错最常用的方法是使用复制。我们再来回顾[第5章](ch5.md)中的复制方法,并比较它们是否可以满足线性一致性
***单主复制(可能可线性化***
***单主复制(可能线性一致***
在具有单引导程序复制功能的系统中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),领导者具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果你从领导读取数据,或者从同步更新的追随者读取数据,他们有可能是线性化的[^iv]。然而,并不是每个单独的领导者数据库都是可以线性化的,无论是通过设计(例如,因为它使用快照隔离)或者由于并发错误【10】。
在具有单复制功能的系统中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能protential**是线性一致性的[^iv]。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误【10】。
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性化,因为它只是单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”第357页)。
[^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”)。
使用领导进行阅读依赖于你确定领导是谁的假设。正如在“[真理被多数人定义](ch8.md#真理被多数人定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此 - 如果妄想的领导者继续为请求提供服务可能违反线性化【20】。使用异步复制故障转移甚至可能会丢失提交的写入参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这违反了持久性和线性化
从主库读取依赖一个假设,你确定领导是谁。正如在“[真理在多数人手中](ch8.md#真理被多数人定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务可能违反线性一致性【20】。使用异步复制故障转移时甚至可能会丢失已提交的写入参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性
***共识算法(可线性化***
***共识算法(线性一致***
一些共识算法,我们将在本章后面讨论,与单引导者复制相似。然而,共识协议包含措施,以防止裂脑和陈旧的复制品。由于这些细节,协调算法可以安全地实现线性化存储。例如Zookeeper 【21】和etcd 【22】就是这样工作的。
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。由于这些细节,协调算法可以安全地实现线性一致性存储。例如Zookeeper 【21】和etcd 【22】就是这样工作的。
***多主复制(不可线性化***
***多主复制(非线性一致***
具有多引导程序复制的系统通常不是线性化的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。由于这个原因,它们可能会产生冲突的写入,需要解析(参阅第171页的“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是缺乏单一数据副本的人为因素
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因,它们可能会产生冲突的写入,需要解析(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本人为产生的
***无主复制(可能不是线性化的)***
***无主复制(也许不是线性一致的)***
对于无领导者复制的系统Dynamo风格参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定读写($w + r> n$)可以获得“强一致性”。根据法定人数的确切配置,取决于您如何界定强一致性,这是不正确的
对于无领导者复制的系统Dynamo风格参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写($w + r> n$)可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)
基于时钟例如在Cassandra中参见第291页上的“[依赖同步时钟](#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎是非线性的因为时钟时间戳不能保证与时间戳一致由于时钟歪斜而导致的实际事件排序。不规范的法定人数第183页的“[马虎法定人数和暗示交接](#马虎法定人数和暗示交接)”)也破坏了线性化的可能性。即使是严格的法定人数,非线性行为也是可能的,如下一节所示。
基于时钟例如在Cassandra中参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的由于时钟偏差不能保证时钟的时间戳与实际事件顺序一致。松散的法定人数第183页的“[松散法定人数与暗示接力](#马虎法定人数和暗示交接)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下一节所示。
#### 线性和法定人数
#### 线性一致性和法定人数
直觉上严格的仲裁读写应该是在Dynamo风格的模型中可线性化的。但是当我们有可变的网络延迟时就有可能存在竞争条件,如[图9-6](img/fig9-6.png)所示。
直觉上在Dynamo风格的模型中严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时可能存在竞争条件,如[图9-6](img/fig9-6.png)所示。
![](img/fig9-6.png)
**图9-6 尽管使用严格的法定人数,仍然是非线性的执行。**
在[图9-6](img/fig9-6.png)中x的初始值为0写入客户端通过向所有三个副本$n = 3, w = 3$发送写入将x更新为1。同时客户端A从两个节点$r = 2$的仲裁中读取数据并在其中一个节点上看到新值1。同时与写操作相关客户端B从两个节点的不同仲裁中读取并从两个节点中取回旧值0。
**图9-6 非线性一致的执行,尽管使用了严格的法定人数**
仲裁条件满足($w + r> n$但是这个执行是不可排队的B的请求在A的请求完成后开始但是B返回旧值而A返回新值。 (这又是爱丽丝和鲍勃的情况了[图9-1]()
在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0写入客户端通过向所有三个副本$n = 3, w = 3$)发送写入将 $x$ 更新为1。客户端A并发地从两个节点组成的法定人群$r = 2$中读取数据并在其中一个节点上看到新值1。客户端B也并发地从两个不同的节点组成的法定人数中读取并从两个节点中取回了旧值0。
有趣的是可以使Dynamo风格的定额以可降低的性能为代价线性化读取器必须在将结果返回给应用程序之前同步执行读取修复请参阅“修复和反熵”一节第178页 并且作者必须在发送写入之前读取法定数量的最新状态【24,25】。然而由于性能损失Riak不执行同步读取修复【26】。 Cassandra确实等待读取修复以完成读取问题【27】但是如果由于使用了最后写入赢得冲突解决方案而导致同一个关键字有多个并发写入则它将失去线性化。
仲裁条件满足($w + r> n$但是这个执行是非线性一致的B的请求在A的请求完成后开始但是B返回旧值而A返回新值。 又一次如同Alice和Bob的例子 [图9-1]()
而且这种方式只能实现线性化的读写操作。线性化的比较和设置操作不能因为它需要一个一致的算法【28】
有趣的是通过牺牲性能可以使Dynamo风格的法定人数线性化读取者必须在将结果返回给应用程序之前同步执行读取修复请参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) 并且写入者必须在发送写入之前读取法定数量节点的最新状态【24,25】。然而由于性能损失Riak不执行同步读取修复【26】。 Cassandra在进行法定人数读取时**确实**在等待读取修复完成【27】但是由于使用了最后写入为准的冲突解决方案当同一个键有多个并发写入时将不能保证线性一致性
总而言之假设无Dynamo风格复制的无领导者系统不能提供线性化性是最安全的。
而且这种方式只能实现线性一致的读写不能实现线性一致的比较和设置操作因为它需要一个共识算法【28】。
总而言之最安全的做法是假设采用Dynamo风格无主复制的系统不能提供线性一致性。
### 线性一致性的代价
由于一些复制方法可以提供线性化,其他复制方法则不能,因此更深入地探讨线性化的优缺点是很有趣的。
由于一些复制方法可以提供线性一致性,其他复制方法则不能,因此更深入地探讨线性一致性的优缺点是很有趣的。
我们已经在第五章讨论了不同复制方法的一些用例。例如,我们看到多领导者复制通常是多数据中心复制的理想选择(参阅“[多数据中心操作](ch5.md#多数据中心操作)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
![](img/fig9-7.png)
**图9-7 网络中断迫使在线性和可用性之间做出选择。**
**图9-7 网络中断迫使在线性一致性和可用性之间做出选择。**
考虑如果两个数据中心之间发生网络中断,会发生什么情况。我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心不能互相连接。
使用多领导者数据库,每个数据中心都可以继续正常运行:由于从一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,只需排队并交换数据。
另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性读取都必须发送给leader因此对于连接到跟随者数据中心的任何客户端这些读取和写入请求必须通过网络同步发送到leader数据中心。
另一方面如果使用单主复制则主库必须位于其中一个数据中心。任何写入和任何线性读取都必须发送给leader因此对于连接到跟随者数据中心的任何客户端这些读取和写入请求必须通过网络同步发送到leader数据中心。
如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。
如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性一致性读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。
如果客户端可以直接连接到领导者数据中心,这不是问题,因为应用程序在那里继续正常工作。但是只能访问下一个数据中心的客户端将会经历停机,直到网络链接被修复。
#### CAP定理
这个问题不仅仅是单领导者和多领导者复制的结果:任何可线性化的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v]
这个问题不仅仅是单领导者和多领导者复制的结果:任何线性一致性的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v]
* 如果您的应用程序需要线性,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。
* 如果您的应用程序不需要线性,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不可线性化
* 如果您的应用程序需要线性一致性,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。
* 如果您的应用程序不需要线性一致性,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不线性一致性
[^v]: 这两种选择有时分别称为CP在网络分区下一致但不可用和AP在网络分区下可用但不一致。 但是这种分类方案存在一些缺陷【9】所以最好避免。
因此,不需要线性的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】由Eric Brewer于2000年命名尽管自20世纪70年代以来分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。
因此,不需要线性一致性的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】由Eric Brewer于2000年命名尽管自20世纪70年代以来分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。
CAP最初是作为一个经验法则提出的没有准确的定义目的是开始讨论数据库的权衡。当时许多分布式数据库侧重于在具有共享存储的机器集群上提供可线性化的语义【18】并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间这更适合于实施大规模的网络服务【37】。 CAP值得赞扬因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长被称为NoSQL
CAP最初是作为一个经验法则提出的没有准确的定义目的是开始讨论数据库的权衡。当时许多分布式数据库侧重于在具有共享存储的机器集群上提供线性一致性的语义【18】并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间这更适合于实施大规模的网络服务【37】。 CAP值得赞扬因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长被称为NoSQL
> ### CAP定理没有帮助
>
@ -285,33 +286,33 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
>
> 只能选择从3中挑选出2个。不幸的是这样做是误导的【32】因为网络分区是一种错误所以它们不是你所拥有的一个选择他们会发生不管你喜欢还是不喜欢【38】。
>
> 在网络正常工作的时候,系统可以提供一致性(线性和总体可用性。发生网络故障时您必须选择线性或总可用性。因此一个更好的表达CAP的方法可以是一致的或者在分区时可用【39】。一个更可靠的网络需要减少这个选择但是在某些时候选择是不可避免的。
> 在网络正常工作的时候,系统可以提供一致性(线性一致性和总体可用性。发生网络故障时您必须选择线性或总可用性。因此一个更好的表达CAP的方法可以是一致的或者在分区时可用【39】。一个更可靠的网络需要减少这个选择但是在某些时候选择是不可避免的。
>
> 在CAP的讨论中术语可用性有几个相互矛盾的定义形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”容错系统实际上不符合CAP对可用性的特殊定义。总而言之围绕着CAP有很多误解和困惑并不能帮助我们更好地理解系统所以最好避免使用CAP。
正式定义的CAP定理【30】的范围很窄它只考虑一个一致性模型即线性)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此尽管CAP在历史上具有影响力但对于设计系统来说它没有实际价值【9,40】。
正式定义的CAP定理【30】的范围很窄它只考虑一个一致性模型即线性一致性)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此尽管CAP在历史上具有影响力但对于设计系统来说它没有实际价值【9,40】。
在分布式系统中有更多有趣的不可能的结果【41】并且CAP已经被更精确的结果所取代【2,42】所以它现在基本上是历史感兴趣的。
[^vi]: 正如第279页的“实践中的网络故障”中所讨论的本书使用分区来指将大数据集精细分解成小数据集分片;参见第6章。相比之下网络分区是特定类型的网络故障我们通常不会将其与其他类型的故障分开考虑。但是由于是CAP的P所以在这种情况下我们不能避免混淆。
#### 线性和网络延迟
#### 线性一致性和网络延迟
虽然线性化是一个有用的保证但实际上很少有系统实际上是线性化的。例如现代多核CPU上的RAM甚至不可线性化【43】如果一个CPU内核上运行的线程写入内存地址而另一个CPU内核上的线程不久后读取相同的地址保证读取第一个线程写入的值除非使用了内存屏障或栅栏【44】
虽然线性一致性是一个有用的保证但实际上很少有系统实际上是线性一致性的。例如现代多核CPU上的RAM甚至不线性一致性【43】如果一个CPU内核上运行的线程写入内存地址而另一个CPU内核上的线程不久后读取相同的地址保证读取第一个线程写入的值除非使用了内存屏障或栅栏【44】
这种行为的原因是每个CPU内核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】所以这个特性对于现代CPU的良好性能是至关重要的。但是现在有几个数据副本一个在主内存中另外几个在不同的高速缓存中而且这些副本是异步更新的因此线性会丢失。
这种行为的原因是每个CPU内核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】所以这个特性对于现代CPU的良好性能是至关重要的。但是现在有几个数据副本一个在主内存中另外几个在不同的高速缓存中而且这些副本是异步更新的因此线性一致性会丢失。
为什么要做这个交换使用CAP定理来证明多核内存一致性模型是没有意义的在一台计算机中我们通常假定可靠的通信并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性的原因是性能,而不是容错。
为什么要做这个交换使用CAP定理来证明多核内存一致性模型是没有意义的在一台计算机中我们通常假定可靠的通信并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性一致性的原因是性能,而不是容错。
许多分布式数据库也是如此它们选择不提供线性保证它们主要是为了提高性能而不是为了容错【46】。线性速度很慢 - 这一直是事实,不仅在网络故障期间。
许多分布式数据库也是如此它们选择不提供线性保证它们主要是为了提高性能而不是为了容错【46】。线性一致性速度很慢 - 这一直是事实,不仅在网络故障期间。
我们不能找到一个更有效的线性化存储实现吗看来答案是否定的Attiya和Welch 【47】证明如果你想要线性化,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”第267页线性读写的响应时间不可避免地会很高。线性算法不存在但是一致性较弱的模型可以更快所以这种权衡对于延迟敏感的系统是很重要的。在第12章中我们将讨论一些避免线性而不牺牲正确性的方法。
我们不能找到一个更有效的线性一致性存储实现吗看来答案是否定的Attiya和Welch 【47】证明如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”第267页线性读写的响应时间不可避免地会很高。线性一致性算法不存在但是一致性较弱的模型可以更快所以这种权衡对于延迟敏感的系统是很重要的。在第12章中我们将讨论一些避免线性一致性而不牺牲正确性的方法。
## 顺序保证
我们之前曾经说过,线性寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。
我们之前曾经说过,线性一致性寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。
订购在本书中一直是反复出现的主题,这表明这可能是一个重要的基本概念。让我们简要回顾一下我们在其中讨论过的其他一些情况:
@ -319,7 +320,7 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
* 我们在[第7章](ch7.md)中讨论的可序列化是关于确保事务按照某种顺序执行的行为。它可以通过以该序列字面执行事务来实现,或者通过允许并行执行,同时防止序列化冲突(通过锁定或中止)来实现。
* 我们在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如确定两个写入中的哪一个稍后发生。
事实证明,排序,线性和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
事实证明,排序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
### 顺序与因果
@ -346,9 +347,9 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
全局顺序和局部顺序之间的差异反映在不同的数据库一致性模型中:
***线性***
***线性一致性***
在一个可线性化的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。
在一个线性一致性的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。
***因果关系***
@ -360,15 +361,15 @@ CAP最初是作为一个经验法则提出的没有准确的定义目的
如果您熟悉像Git这样的分布式版本控制系统那么它们的版本历史非常类似于因果关系图。通常一个提交会以一条直线进行但是有时您会得到分支当多个人同时在一个项目上工作时并且在这些创建的提交合并时创建合并。
#### 线性强于因果一致性
#### 线性一致性强于因果一致性
那么因果顺序和线性化之间的关系是什么?答案是线性化意味着因果关系:任何可线性化的系统都能正确保存注意力【7】。特别是如果系统中有多个通信通道如图9-5中的消息队列和文件存储服务线性可确保因果关系被自动保留而系统不必做任何特殊的事情如通过不同部件之间的时间戳
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性意味着因果关系:任何线性一致性的系统都能正确保存注意力【7】。特别是如果系统中有多个通信通道如图9-5中的消息队列和文件存储服务线性可确保因果关系被自动保留而系统不必做任何特殊的事情如通过不同部件之间的时间戳
线性化确保因果关系的事实使线性化系统变得简单易懂更具吸引力。然而正如第335页的“线性可用性的成本”中所讨论的使系统可线性化可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性,这使得它们可以获得更好的性能,但却使它们难以工作。
线性一致性确保因果关系的事实使线性一致性系统变得简单易懂更具吸引力。然而正如第335页的“线性可用性的成本”中所讨论的使系统线性一致性可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,这使得它们可以获得更好的性能,但却使它们难以工作。
好消息是中间地带是可能的。线性不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的,不会造成使其线性的性能命中特别是CAP定理不适用。事实上因果一致性是最强可能的一致性模型不会由于网络延迟而减慢并且在网络故障时仍然可用【2,42】。
好消息是中间地带是可能的。线性一致性不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的,不会造成使其线性一致性的性能命中特别是CAP定理不适用。事实上因果一致性是最强可能的一致性模型不会由于网络延迟而减慢并且在网络故障时仍然可用【2,42】。
在许多情况下,似乎需要线性的系统实际上只需要确定因果一致性这可以更有效地实施。基于这种观察研究人员正在探索新的数据库来保存因果关系其性能和可用性特征与最终一致的系统类似【49,50,51】。
在许多情况下,似乎需要线性一致性的系统实际上只需要确定因果一致性这可以更有效地实施。基于这种观察研究人员正在探索新的数据库来保存因果关系其性能和可用性特征与最终一致的系统类似【49,50,51】。
由于这项研究是相当新的其中没有很多已经进入生产系统仍然有挑战需要克服【52,53】。但是这是未来系统的一个有利的方向。
#### 捕获因果关系
@ -463,7 +464,7 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
如前所述单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统以及如果领导者失败参见第156页的“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。
[^ix]: “原子广播”这个术语是传统的但是它是非常混乱的因为它与原子的其他用法不一致它与ACID事务中的原子性没有任何关系只是与原子操作在多线程编程的意义上 )或原子寄存器(线性存储)。 总的顺序组播是另一个同义词。
[^ix]: “原子广播”这个术语是传统的但是它是非常混乱的因为它与原子的其他用法不一致它与ACID事务中的原子性没有任何关系只是与原子操作在多线程编程的意义上 )或原子寄存器(线性一致性存储)。 总的顺序组播是另一个同义词。
> #### 顺序保证的范围
>
@ -494,19 +495,19 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
全面订购广播对于实施提供防护令牌的锁定服务也很有用请参见第294页的“防护令牌”。每个获取锁的请求都作为消息添加到日志中并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记因为它是单调递增的。在ZooKeeper中这个序列号被称为`zxid` 【15】。
#### 使用全序广播实现线性的存储
#### 使用全序广播实现线性一致性的存储
如图9-4所示可线性化的系统中,有一个操作的总顺序。这是否意味着线性化与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。
如图9-4所示线性一致性的系统中,有一个操作的总顺序。这是否意味着线性一致性与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。
[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】在异步崩溃停止模型【68】中没有确定性的解决方案可线性化的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而支持原子操作如比较和设置或者在寄存器中增加和获取使得它相当于共识【28】。 因此,共识问题和可线性化的注册问题密切相关。
[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】在异步崩溃停止模型【68】中没有确定性的解决方案线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而支持原子操作如比较和设置或者在寄存器中增加和获取使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性是最近的保证:读取保证看到写入的最新值。
全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。
但是,如果您有全面的顺序广播,则可以在其上构建线性存储。例如,您可以确保用户名唯一标识用户帐户。
但是,如果您有全面的顺序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性)。
想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。
您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下可线性化的比较和设置操作:
您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作:
1. 在日志中添加一条消息,暂时指明您要声明的用户名。
2. 阅读日志,并等待你附加的信息被传回给你。[^xi]
@ -516,27 +517,27 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
由于日志条目以相同顺序传递到所有节点因此如果有多个并发写入则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者并中止后面的写入确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。
虽然此过程确保可线性写入,但不能保证线性化读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 具体来说这里描述的过程提供了顺序一致性【47,64】有时也称为时间线一致性【65,66】这比线性化要弱一些)。为了使读取线性化,有几个选项:
虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 具体来说这里描述的过程提供了顺序一致性【47,64】有时也称为时间线一致性【65,66】这比线性一致性要弱一些)。为了使读取线性一致性,有几个选项:
* 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 法定读取etcd的工作有点像这样【16】。
* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 这是Zookeeper的`sync()`操作背后的思想【15】
* 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。
#### 使用线性存储实现总顺序广播
#### 使用线性一致性存储实现总顺序广播
最后一节介绍了如何从全部命令广播中构建一个线性化的比较和设置操作。我们也可以把它转过来,假设我们有可线性化的存储,并展示如何从它构建全部命令播放。
最后一节介绍了如何从全部命令广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。
最简单的方法是假设你有一个线性的寄存器来存储一个整数并且有一个原子增量和获取操作【28】。或者原子比较和设置操作也可以完成这项工作。
最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数并且有一个原子增量和获取操作【28】。或者原子比较和设置操作也可以完成这项工作。
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取可线性化的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。
请注意与Lamport时间戳不同您通过递增可线性化寄存器获得的数字形成一个没有间隙的序列。因此如果一个节点已经发送了消息4并且接收到序列号为6的传入消息则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
请注意与Lamport时间戳不同您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此如果一个节点已经发送了消息4并且接收到序列号为6的传入消息则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此
与Lamport时间戳 - 事实上,这是总顺序广播和时间戳订购之间的关键区别。
使用原子增量和获取操作来创建线性整数有多困难像往常一样如果事情从来没有失败过那很容易你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况并在该节点失败时恢复该值【59】。一般来说如果你对可线性化序列号的产生者认真思考,你不可避免地会得出一个一致的算法。
使用原子增量和获取操作来创建线性一致性整数有多困难像往常一样如果事情从来没有失败过那很容易你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况并在该节点失败时恢复该值【59】。一般来说如果你对线性一致性序列号的产生者认真思考,你不可避免地会得出一个一致的算法。
这并非巧合:可以证明,线性的比较和设置或增量和取得寄存器和全部命令广播都相当于【2867】。也就是说如果你能解决其中的一个问题你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力
这并非巧合:可以证明,线性一致性的比较和设置或增量和取得寄存器和全部命令广播都相当于【2867】。也就是说如果你能解决其中的一个问题你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力
现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。
@ -545,7 +546,7 @@ Lamport时间戳有时会与版本向量混淆我们在第184页上的“检
## 分布式事务与共识
共识是分布式计算中最重要也是最基本的问题之一。从表面上看,似乎很简单:非正式地说,目标只是让几个节点达成一致。你可能会认为这不应该太难。不幸的是,许多破损的系统已经被误认为这个问题很容易解决。
虽然共识是非常重要的但关于它的部分在本书的后半部分已经出现了因为这个主题非常微妙欣赏细微之处需要一些必要的知识。即使在学术研究界对共识的理解也只是在几十年的时间内逐渐显现出来一路上有许多误解。现在我们已经讨论了复制第5章事务第7章系统模型第8章线性以及总播放(本章),我们终于准备好解决共识问题了。
虽然共识是非常重要的但关于它的部分在本书的后半部分已经出现了因为这个主题非常微妙欣赏细微之处需要一些必要的知识。即使在学术研究界对共识的理解也只是在几十年的时间内逐渐显现出来一路上有许多误解。现在我们已经讨论了复制第5章事务第7章系统模型第8章线性一致性以及总播放(本章),我们终于准备好解决共识问题了。
在节点达成一致的情况下,有许多情况是很重要的。例如:
@ -820,13 +821,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问
#### 共识的局限性
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播,因此它们也可以容错的方式实现线性化的原子操作参见第350页的“使用全部命令广播实现线性化存储”)。
共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播,因此它们也可以容错的方式实现线性一致性的原子操作参见第350页的“使用全部命令广播实现线性一致性存储”)。
尽管如此,它们并没有到处使用,因为它的好处是有代价的。
节点在决定之前对节点进行投票的过程是一种同步复制。如第153页的“同步与异步复制”中所述通常将数据库配置为使用异步复制。在这种配置中一些承诺的数据在故障转移时可能会丢失 - 但是为了获得更好的性能,许多人选择接受这种风险。
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“可线性化的成本”第295页
共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“线性一致性的成本”第295页
大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。
@ -844,9 +845,9 @@ ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据
ZooKeeper模仿Google的Chubby锁定服务【14,98】不仅实现了全面的命令广播因此也实现了共识而且还构建了一组有趣的其他特性这些特性在构建分布式系统时变得特别有用
***线性的原子操作***
***线性一致性的原子操作***
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现这个租约有一个到期时间以便在客户端失败的情况下最终被释放请参阅第295页上的“进程暂停”
使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现这个租约有一个到期时间以便在客户端失败的情况下最终被释放请参阅第295页上的“进程暂停”
***操作的总排序***
@ -860,7 +861,7 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】不仅实现了全面的
一个客户端不仅可以读取其他客户端创建的锁和值还可以监视其中的更改。因此客户端可以找出另一个客户端何时加入集群基于它写入ZooKeeper的值还是另一个客户端发生故障因为其会话超时并且其临时节点消失。通过订阅通知客户避免了不得不经常轮询以找出变化。
在这些特征中,只有线性的原子操作才需要达成共识。但是这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。
在这些特征中,只有线性一致性的原子操作才需要达成共识。但是这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。
@ -880,9 +881,9 @@ ZooKeeper/Chubby模型运行良好的一个例子是如果您有几个流程
ZooKeeperetcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中虚拟机连续来去常见您通常不会提前知道您的服务的IP地址。相反您可以配置您的服务使其在启动时注册服务注册表中的网络端点然后可以由其他服务找到它们。
但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不可线性化如果DNS查询的结果有点陈旧通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。
但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性如果DNS查询的结果有点陈旧通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性的读取请求。
尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。
#### 成员服务
@ -898,15 +899,15 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
## 本章小结
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性化不同,线性化将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性的协调开销,并且对网络问题的敏感性要低得多。
我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。
但是即使我们捕捉到因果顺序例如使用Lamport时间戳我们也看到有些事情不能以这种方式实现在“时间戳排序不够充分”的第347页中我们考虑了确保用户名是唯一的并拒绝同一用户名的并发注册。如果一个节点要接受注册则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。
我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括:
***线性的比较和设置寄存器***
***线性一致性的比较和设置寄存器***
寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。
@ -930,7 +931,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
当多个事务同时尝试使用相同的密钥创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性操作,唯一性约束,完全有序的复制日志等等。
如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。
但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法:
@ -938,11 +939,11 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部
2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识 - 计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。
3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法建议使用经过验证的算法来正确处理不利的网络条件【107】。
尽管一个单独的领导者数据库可以提供线性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。
像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议故障检测和会员服务方面起着重要的作用。使用起来并不容易但比开发自己的算法要好得多可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西而且你想要它要容错那么建议使用类似ZooKeeper的东西。
尽管如此并不是每个系统都需要达成共识例如无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
尽管如此并不是每个系统都需要达成共识例如无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 107 KiB