fix many wrongly formatted emphasis

This commit is contained in:
Gang Yin 2021-09-14 12:05:33 +08:00
parent 2b4ff8e827
commit fb150bf5e3
7 changed files with 86 additions and 86 deletions

22
ch10.md
View File

@ -12,7 +12,7 @@
在本书的前两部分中,我们讨论了很多关于**请求**和**查询**以及相应的**响应**或**结果**。许多现有数据系统中都采用这种数据处理方式:你发送请求指令,一段时间后(我们期望)系统会给出一个结果。数据库、缓存、搜索索引、Web服务器以及其他一些系统都以这种方式工作。
像这样的**在线online**系统无论是浏览器请求页面还是调用远程API的服务我们通常认为请求是由人类用户触发的并且正在等待响应。他们不应该等太久所以我们非常关注系统的响应时间请参阅“[描述性能](ch1.md#描述性能)”)。
像这样的**在线online** 系统无论是浏览器请求页面还是调用远程API的服务我们通常认为请求是由人类用户触发的并且正在等待响应。他们不应该等太久所以我们非常关注系统的响应时间请参阅“[描述性能](ch1.md#描述性能)”)。
Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统:
@ -22,11 +22,11 @@
***批处理系统(离线系统)***
一个批处理系统有大量的输入数据,跑一个**作业job**来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。
一个批处理系统有大量的输入数据,跑一个**作业job** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。
***流处理系统(准实时系统)***
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时near-real-time**或**准在线nearline**处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时near-real-time** 或**准在线nearline** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。
正如我们将在本章中看到的那样批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如2004年发布的批处理算法Map-Reduce可能被过分热情地被称为“造就Google大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用包括HadoopCouchDB和MongoDB。
@ -269,7 +269,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
单个MapReduce作业可以解决的问题范围很有限。以日志分析为例单个MapReduce作业可以确定每个URL的页面浏览次数但无法确定最常见的URL因为这需要第二轮排序。
因此将MapReduce作业链接成为**工作流workflow**中是极为常见的,例如,一个作业的输出成为下一个作业的输入。 Hadoop MapReduce框架对工作流没有特殊支持所以这个链是通过目录名隐式实现的第一个作业必须将其输出配置为HDFS中的指定目录第二个作业必须将其输入配置为从同一个目录。从MapReduce框架的角度来看这是两个独立的作业。
因此将MapReduce作业链接成为**工作流workflow** 中是极为常见的,例如,一个作业的输出成为下一个作业的输入。 Hadoop MapReduce框架对工作流没有特殊支持所以这个链是通过目录名隐式实现的第一个作业必须将其输出配置为HDFS中的指定目录第二个作业必须将其输入配置为从同一个目录。从MapReduce框架的角度来看这是两个独立的作业。
因此被链接的MapReduce作业并没有那么像Unix命令管道它直接将一个进程的输出作为另一个进程的输入仅用一个很小的内存缓冲区。它更像是一系列命令其中每个命令的输出写入临时文件下一个命令从临时文件中读取。这种设计有利也有弊我们将在“[物化中间状态](#物化中间状态)”中讨论。
@ -353,7 +353,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
如果连接的输入存在热键可以使用一些算法进行补偿。例如Pig中的**偏斜连接skewed join**方法首先运行一个抽样作业Sampling Job来确定哪些键是热键【39】。连接实际执行时Mapper会将热键的关联记录**随机**相对于传统MapReduce基于键散列的确定性方法发送到几个Reducer之一。对于另外一侧的连接输入与热键相关的记录需要被复制到**所有**处理该键的Reducer上【40】。
这种技术将处理热键的工作分散到多个Reducer上这样可以使其更好地并行化代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接sharded join**方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。
这种技术将处理热键的工作分散到多个Reducer上这样可以使其更好地并行化代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接sharded join** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。
Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键并将与这些键相关的记录单独存放与其它文件分开。当在该表上执行连接时对于热键它会使用Map端连接请参阅下一节
@ -458,7 +458,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
MapReduce作业的输出处理遵循同样的原理。通过将输入视为不可变且避免副作用如写入外部数据库批处理作业不仅实现了良好的性能而且更容易维护
- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的概念被称为**人类容错human fault tolerance**【50】
- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种**最小化不可逆性minimizing irreversibility**的原则有利于敏捷软件开发【51】。
- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种**最小化不可逆性minimizing irreversibility** 的原则有利于敏捷软件开发【51】。
- 如果Map或Reduce任务失败MapReduce框架将自动重新调度并在同样的输入上再次运行它。如果失败是由代码中的错误造成的那么它会不断崩溃并最终导致作业在几次尝试之后失败。但是如果故障是由于临时问题导致的那么故障就会被容忍。因为输入不可变这种自动重试是安全的而失败任务的输出会被MapReduce框架丢弃。
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。
- 与Unix工具类似MapReduce作业将逻辑与布线配置输入和输出目录分离这使得关注点分离可以重用代码一个团队可以专注实现一个做好一件事的作业而其他团队可以决定何时何地运行这项作业。
@ -469,7 +469,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
正如我们所看到的Hadoop有点像Unix的分布式版本其中HDFS是文件系统而MapReduce是Unix进程的怪异实现总是在Map阶段和Reduce阶段运行`sort`工具)。我们了解了如何在这些原语的基础上实现各种连接和分组操作。
当MapReduce论文发表时【1】它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的**大规模并行处理MPP massively parallel processing**数据库中实现了【3,40】。比如Gamma database machineTeradata和Tandem NonStop SQL就是这方面的先驱【52】。
当MapReduce论文发表时【1】它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的**大规模并行处理MPP massively parallel processing** 数据库中实现了【3,40】。比如Gamma database machineTeradata和Tandem NonStop SQL就是这方面的先驱【52】。
最大的区别是MPP数据库专注于在一组机器上并行执行分析SQL查询而MapReduce和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。
@ -548,7 +548,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
将这个中间状态写入文件的过程称为**物化materialization**。 (在“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算)
作为对照本章开头的日志分析示例使用Unix管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态而是只使用一个小的内存缓冲区将输出增量地**流stream**向输入。
作为对照本章开头的日志分析示例使用Unix管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态而是只使用一个小的内存缓冲区将输出增量地**流stream** 向输入。
与Unix管道相比MapReduce完全物化中间状态的方法存在不足之处
@ -587,7 +587,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
SparkFlink和Tez避免将中间状态写入HDFS因此它们采取了不同的方法来容错如果一台机器发生故障并且该机器上的中间状态丢失则它会从其他仍然可用的数据重新计算在可行的情况下是先前的中间状态要么就只能是原始输入数据通常在HDFS上
为了实现这种重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用了哪些输入分区?应用了哪些算子? Spark使用**弹性分布式数据集RDDResilient Distributed Dataset**的抽象来跟踪数据的谱系【61】而Flink对算子状态存档允许恢复运行在执行过程中遇到错误的算子【66】。
为了实现这种重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用了哪些输入分区?应用了哪些算子? Spark使用**弹性分布式数据集RDDResilient Distributed Dataset** 的抽象来跟踪数据的谱系【61】而Flink对算子状态存档允许恢复运行在执行过程中遇到错误的算子【66】。
在重新计算数据时,重要的是要知道计算是否是**确定性的**:也就是说,给定相同的输入数据,算子是否始终产生相同的输出?如果一些丢失的数据已经发送给下游算子,这个问题就很重要。如果算子重新启动,重新计算的数据与原有的丢失数据不一致,下游算子很难解决新旧数据之间的矛盾。对于不确定性算子来说,解决方案通常是杀死下游算子,然后再重跑新数据。
@ -609,7 +609,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
批处理上下文中的图也很有趣其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用如推荐引擎或排序系统中。例如最着名的图形分析算法之一是PageRank 【69】它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分用于确定网络搜索引擎呈现结果的顺序。
> 像SparkFlink和Tez这样的数据流引擎请参阅“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图DAG**的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
> 像SparkFlink和Tez这样的数据流引擎请参阅“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图DAG** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为**传递闭包transitive closure**)。
@ -667,7 +667,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。 HiveSpark和Flink都有基于代价的查询优化器可以做到这一点甚至可以改变连接顺序最小化中间状态的数量【66,77,78,79】。
连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以**声明式declarative**的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在“[数据查询语言](ch2.md#数据查询语言)”中见过这个想法。
连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以**声明式declarative** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在“[数据查询语言](ch2.md#数据查询语言)”中见过这个想法。
但MapReduce及其数据流后继者在其他方面与SQL的完全声明式查询模型有很大区别。 MapReduce是围绕着回调函数的概念建立的对于每条记录或者一组记录调用一个用户定义的函数Mapper或Reducer并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作解析、自然语言分析、图像分析以及运行数值或统计算法等。

18
ch11.md
View File

@ -10,17 +10,17 @@
[TOC]
在[第十章](ch10.md)中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是**衍生数据derived data**的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。
在[第十章](ch10.md)中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是**衍生数据derived data** 的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。
然而,在[第十章](ch10.md)中仍然有一个很大的假设即输入是有界的即已知和有限的大小所以批处理知道它何时完成输入的读取。例如MapReduce核心的排序操作必须读取其全部输入然后才能开始生成输出可能发生这种情况最后一条输入记录具有最小的键因此需要第一个被输出所以提早开始输出是不可行的。
实际上,很多数据是**无界限**的因为它随着时间的推移而逐渐到达你的用户在昨天和今天产生了数据明天他们将继续产生更多的数据。除非你停业否则这个过程永远都不会结束所以数据集从来就不会以任何有意义的方式“完成”【1】。因此批处理程序必须将数据人为地分成固定时间段的数据块例如在每天结束时处理一天的数据或者在每小时结束时处理一小时的数据。
日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 —— 比如说,在每秒钟的末尾 —— 或者甚至更连续一些,完全抛开固定的时间切片,当事件发生时就立即进行处理,这就是**流处理stream processing**背后的想法。
日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 —— 比如说,在每秒钟的末尾 —— 或者甚至更连续一些,完全抛开固定的时间切片,当事件发生时就立即进行处理,这就是**流处理stream processing** 背后的想法。
一般来说“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方Unix的stdin和stdout编程语言惰性列表【2】文件系统API如Java的`FileInputStream`TCP连接通过互联网传送音频和视频等等。
在本章中,我们将把**事件流event stream**视为一种数据管理机制:无界限,增量处理,与上一章中的批量数据相对应。我们将首先讨论怎样表示、存储、通过网络传输流。在“[数据库与流](#数据库与流)”中,我们将研究流和数据库之间的关系。最后在“[流处理](#流处理)”中,我们将研究连续处理这些流的方法和工具,以及它们用于应用构建的方式。
在本章中,我们将把**事件流event stream** 视为一种数据管理机制:无界限,增量处理,与上一章中的批量数据相对应。我们将首先讨论怎样表示、存储、通过网络传输流。在“[数据库与流](#数据库与流)”中,我们将研究流和数据库之间的关系。最后在“[流处理](#流处理)”中,我们将研究连续处理这些流的方法和工具,以及它们用于应用构建的方式。
## 传递事件流
@ -50,11 +50,11 @@
在这个**发布/订阅**模式中,不同的系统采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助:
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?**一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用**背压backpressure**(也称为**流量控制flow control**即阻塞生产者以免其发送更多的消息。例如Unix管道和TCP就使用了背压它们有一个固定大小的小缓冲区如果填满发送者会被阻塞直到接收者从缓冲区中取出数据请参阅“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?** 一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用**背压backpressure**(也称为**流量控制flow control**即阻塞生产者以免其发送更多的消息。例如Unix管道和TCP就使用了背压它们有一个固定大小的小缓冲区如果填满发送者会被阻塞直到接收者从缓冲区中取出数据请参阅“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。
如果消息被缓存在队列中那么理解队列增长会发生什么是很重要的。当队列装不进内存时系统会崩溃吗还是将消息写入磁盘如果是这样磁盘访问又会如何影响消息传递系统的性能【6】
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?**与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅“[复制与持久性](ch7.md#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?** 与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅“[复制与持久性](ch7.md#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
是否可以接受消息丢失取决于应用。例如对于周期传输的传感器读数和指标偶尔丢失的数据点可能并不重要因为更新的值会在短时间内发出。但要注意如果大量的消息被丢弃可能无法立刻意识到指标已经不正确了【7】。如果你正在对事件计数那么它们能够可靠送达是更重要的因为每个丢失的消息都意味着使计数器的错误扩大。
@ -79,7 +79,7 @@
通过将数据集中在代理上,这些系统可以更容易地容忍来来去去的客户端(连接,断开连接和崩溃),而持久性问题则转移到代理的身上。一些消息代理只将消息保存在内存中,而另一些消息代理(取决于配置)将其写入磁盘,以便在代理崩溃的情况下不会丢失。针对缓慢的消费者,它们通常会允许无上限的排队(而不是丢弃消息或背压),尽管这种选择也可能取决于配置。
排队的结果是,消费者通常是**异步asynchronous**的:当生产者发送消息时,通常只会等待代理确认消息已经被缓存,而不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点 —— 通常在几分之一秒之内,但有时当消息堆积时会显著延迟。
排队的结果是,消费者通常是**异步asynchronous** 的:当生产者发送消息时,通常只会等待代理确认消息已经被缓存,而不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点 —— 通常在几分之一秒之内,但有时当消息堆积时会显著延迟。
#### 消息代理与数据库的对比
@ -299,14 +299,14 @@
与变更数据捕获类似,事件溯源涉及到**将所有对应用状态的变更**存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了一个不同的抽象层次上:
* 在变更数据捕获中,应用以**可变方式mutable way**使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞态条件。写入数据库的应用不需要知道CDC的存在。
* 在变更数据捕获中,应用以**可变方式mutable way** 使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞态条件。写入数据库的应用不需要知道CDC的存在。
* 在事件溯源中,应用逻辑显式构建在写入事件日志的不可变事件之上。在这种情况下,事件存储是仅追加写入的,更新与删除是不鼓励的或禁止的。事件被设计为旨在反映应用层面发生的事情,而不是底层的状态变更。
事件溯源是一种强大的数据建模技术从应用的角度来看将用户的行为记录为不可变的事件更有意义而不是在可变数据库中记录这些行为的影响。事件溯源使得应用随时间演化更为容易通过更容易理解事情发生的原因来帮助调试的进行并有利于防止应用Bug请参阅“[不可变事件的优点](#不可变事件的优点)”)。
例如,存储“学生取消选课”事件以中性的方式清楚地表达了单个行为的意图,而其副作用“从登记表中删除了一个条目,而一条取消原因的记录被添加到学生反馈表“则嵌入了很多有关稍后对数据的使用方式的假设。如果引入一个新的应用功能,例如“将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地从现有事件中脱开。
事件溯源类似于**编年史chronicle**数据模型【45】事件日志与星型模式中的事实表之间也存在相似之处请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
事件溯源类似于**编年史chronicle** 数据模型【45】事件日志与星型模式中的事实表之间也存在相似之处请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
诸如Event Store【46】这样的专业数据库已经被开发出来供使用事件溯源的应用使用但总的来说这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。
@ -643,7 +643,7 @@ GROUP BY follows.follower_id
微批次也隐式提供了一个与批次大小相等的滚动窗口(按处理时间而不是事件时间戳分窗)。任何需要更大窗口的作业都需要显式地将状态从一个微批次转移到下一个微批次。
Apache Flink则使用不同的方法它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃它可以从最近的存档点重启并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的**壁障barrier**触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。
Apache Flink则使用不同的方法它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃它可以从最近的存档点重启并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的**壁障barrier** 触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。
在流处理框架的范围内,微批次与存档点方法提供了与批处理一样的**恰好一次语义**。但是,只要输出离开流处理器(例如,写入数据库,向外部消息代理发送消息,或发送电子邮件),框架就无法抛弃失败批次的输出了。在这种情况下,重启失败任务会导致外部副作用发生两次,只有微批次或存档点不足以阻止这一问题。

18
ch12.md
View File

@ -197,7 +197,7 @@
**联合数据库:统一读取**
可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库federated database**或**多态存储polystore**的方法【18,19】。例如PostgreSQL的**外部数据包装器foreign data wrapper**功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库federated database** 或**多态存储polystore** 的方法【18,19】。例如PostgreSQL的**外部数据包装器foreign data wrapper** 功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
联合查询接口遵循着单一集成系统的关系型传统,带有高级查询语言和优雅的语义,但实现起来非常复杂。
@ -373,7 +373,7 @@
最近用于开发有状态的客户端与用户界面的工具例如如Elm语言【30】和Facebook的ReactFlux和Redux工具链已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态其结构与事件溯源相似请参阅“[事件溯源](ch11.md#事件溯源)”)。
将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端end-to-end**的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。
将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端end-to-end** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。
一些应用(如即时消息传递与在线游戏)已经具有这种“实时”架构(在低延迟交互的意义上,不是在“[响应时间保证](ch8.md#响应时间保证)”中的意义上)。但我们为什么不用这种方式构建所有的应用?
@ -465,7 +465,7 @@ COMMIT;
#### 操作标识符
要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端end-to-end**的请求流。
要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端end-to-end** 的请求流。
例如你可以为操作生成一个唯一的标识符例如UUID并将其作为隐藏表单字段包含在客户端应用中或通过计算所有表单相关字段的散列来生成操作ID 【3】。如果Web浏览器提交了两次POST请求这两个请求将具有相同的操作ID。然后你可以将该操作ID一路传递到数据库并检查你是否曾经使用给定的ID执行过一个操作如[例12-2]()中所示。
**例12-2 使用唯一ID来抑制重复请求**
@ -635,7 +635,7 @@ COMMIT;
1. 数据流系统可以维持衍生数据的完整性保证,而无需原子提交、线性一致性或者同步的跨分区协调。
2. 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被临时违反并在稍后被修复。
总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调coordination-avoiding**的数据系统有着很大的吸引力比起需要执行同步协调的系统它们能达到更好的性能与更强的容错能力【56】。
总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调coordination-avoiding** 的数据系统有着很大的吸引力比起需要执行同步协调的系统它们能达到更好的性能与更强的容错能力【56】。
例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统的及时性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。
@ -711,7 +711,7 @@ COMMIT;
我对这些技术的拜占庭容错方面有些怀疑(请参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明proof of work** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管更多是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。
密码学审计与完整性检查通常依赖**默克尔树Merkle tree**【74】这是一颗散列值的树能够用于高效地证明一条记录出现在一个数据集中以及其他一些特性。除了炒作的沸沸扬扬的加密货币之外**证书透明性certificate transparency**也是一种依赖Merkle树的安全技术用来检查TLS/SSL证书的有效性【75,76】。
密码学审计与完整性检查通常依赖**默克尔树Merkle tree**【74】这是一颗散列值的树能够用于高效地证明一条记录出现在一个数据集中以及其他一些特性。除了炒作的沸沸扬扬的加密货币之外**证书透明性certificate transparency** 也是一种依赖Merkle树的安全技术用来检查TLS/SSL证书的有效性【75,76】。
我可以想象,那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越广泛的应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要一些工作。 但我认为这是一个值得关注的有趣领域。
@ -766,7 +766,7 @@ COMMIT;
当预测性分析影响人们的生活时自我强化的反馈循环会导致非常有害的问题。例如考虑雇主使用信用分来评估候选人的例子。你可能是一个信用分不错的好员工但因不可抗力的意外而陷入财务困境。由于不能按期付账单你的信用分会受到影响进而导致找到工作更为困难。失业使你陷入贫困这进一步恶化了你的分数使你更难找到工作【87】。在数据与数学严谨性的伪装背后隐藏的是由恶毒假设导致的恶性循环。
我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为**系统思维systems thinking**的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为结构或特性。该系统是否加强和增大了人们之间现有的差异例如损不足以奉有余富者愈富贫者愈贫还是试图与不公作斗争而且即使有着最好的动机我们也必须当心意想不到的后果。
我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为**系统思维systems thinking** 的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为结构或特性。该系统是否加强和增大了人们之间现有的差异例如损不足以奉有余富者愈富贫者愈贫还是试图与不公作斗争而且即使有着最好的动机我们也必须当心意想不到的后果。
### 隐私和追踪
@ -806,7 +806,7 @@ COMMIT;
#### 隐私与数据使用
有时候,人们声称“隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对**隐私privacy**一词的误解。
有时候,人们声称“隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对**隐私privacy** 一词的误解。
拥有隐私并不意味着保密一切东西;它意味着拥有选择向谁展示哪些东西的自由,要公开什么,以及要保密什么。**隐私权是一项决定权**在从保密到透明的光谱上隐私使得每个人都能决定自己想要在什么地方位于光谱上的哪个位置【99】。这是一个人自由与自主的重要方面。
@ -868,13 +868,13 @@ COMMIT;
## 本章小结
在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一**数据集成data integration**问题,以便让数据变更在不同系统之间流动。
在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一**数据集成data integration** 问题,以便让数据变更在不同系统之间流动。
在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换衍生自记录系统。通过这种方式,我们可以维护索引,物化视图,机器学习模型,统计摘要等等。通过使这些衍生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。
将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果你想变更其中一个处理步骤,例如变更索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码,以便重新衍生输出。同样,出现问题时,你也可以修复代码并重新处理数据以便恢复。
这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆unbundling**数据库组件,并通过组合这些松散耦合的组件来构建应用程序。
这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆unbundling** 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。
衍生状态可以通过观察底层数据的变更来更新。此外,衍生状态本身可以进一步被下游消费者观察。我们甚至可以将这种数据流一路传送至显示数据的终端用户设备,从而构建可动态更新以反映数据变更,并在离线时能继续工作的用户界面。

2
ch5.md
View File

@ -559,7 +559,7 @@
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取是返回旧值还是新值。
* 如果写操作在某些副本上成功而在其他节点上失败例如因为某些节点上的磁盘已满在小于w个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着如果一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值【47】。
* 如果携带新值的节点失败需要读取其他带有旧值的副本。并且其数据从带有旧值的副本中恢复则存储新值的副本数可能会低于w从而打破法定人数条件。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing** 的边缘情况,我们将在**“[线性一致性和法定人数](ch9.md#线性一致性和法定人数)”中看到这点。
* 即使一切工作正常,有时也会不幸地出现关于**时序timing** 的边缘情况,我们将在“[线性一致性和法定人数](ch9.md#线性一致性和法定人数)”中看到这点。
因此,尽管法定人数似乎保证读取返回最新的写入值,但在实践中并不那么简单。 Dynamo风格的数据库通常针对可以忍受最终一致性的用例进行优化。允许通过参数w和r来调整读取陈旧值的概率但把它们当成绝对的保证是不明智的。

2
ch7.md
View File

@ -29,7 +29,7 @@
怎样知道你是否需要事务?为了回答这个问题,首先需要确切理解事务可以提供的安全保障,以及它们的代价。尽管乍看事务似乎很简单,但实际上有许多微妙但重要的细节在起作用。
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交read committed****快照隔离snapshot isolation**和**可串行化serializability**等隔离级别。
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交read committed****快照隔离snapshot isolation** 和**可串行化serializability** 等隔离级别。
本章同时适用于单机数据库与分布式数据库;在[第八章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。

38
ch8.md
View File

@ -39,7 +39,7 @@
当你编写运行在多台计算机上的软件时情况有本质上的区别。在分布式系统中我们不再处于理想化的系统模型中我们别无选择只能面对现实世界的混乱现实。而在现实世界中各种各样的事情都可能会出现问题【4】如下面的轶事所述
> 在我有限的经验中,我已经和很多东西打过交道:单个**数据中心DC**中长期存在的网络分区配电单元PDU故障交换机故障整个机架的意外重启整个数据中心主干网络故障整个数据中心的电源故障以及一个低血糖的司机把他的福特皮卡撞在数据中心的HVAC加热通风和空调系统上。而且我甚至不是一个运维。
> 在我有限的经验中,我已经和很多东西打过交道:单个**数据中心DC** 中长期存在的网络分区配电单元PDU故障交换机故障整个机架的意外重启整个数据中心主干网络故障整个数据中心的电源故障以及一个低血糖的司机把他的福特皮卡撞在数据中心的HVAC加热通风和空调系统上。而且我甚至不是一个运维。
>
> ——柯达黑尔
@ -59,9 +59,9 @@
在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同:
* 许多与互联网有关的应用程序都是**在线online**的,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止集群以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。
* 许多与互联网有关的应用程序都是**在线online** 的,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止集群以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。
* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和**远程直接内存访问RDMA**进行通信。另一方面,云服务中的节点是由商用机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。
* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和**远程直接内存访问RDMA** 进行通信。另一方面,云服务中的节点是由商用机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。
* 大型数据中心网络通常基于IP和以太网以CLOS拓扑排列以提供更高的对分bisection带宽【9】。超级计算机通常使用专门的网络拓扑结构例如多维网格和Torus网络 【10】这为具有已知通信模式的HPC工作负载提供了更好的性能。
@ -82,7 +82,7 @@
> 您可能想知道这是否有意义——直观地看来系统只能像其最不可靠的组件最薄弱的环节一样可靠。事实并非如此事实上从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想【11】。例如
>
> * 纠错码允许数字数据在通信信道上准确传输偶尔会出现一些错误例如由于无线网络上的无线电干扰【12】。
> * **互联网协议Internet Protocol, IP**不可靠:可能丢弃,延迟,重复或重排数据包。 传输控制协议Transmission Control Protocol, TCP在互联网协议IP之上提供了更可靠的传输层它确保丢失的数据包被重新传输消除重复并且数据包被重新组装成它们被发送的顺序。
> * **互联网协议Internet Protocol, IP** 不可靠:可能丢弃,延迟,重复或重排数据包。 传输控制协议Transmission Control Protocol, TCP在互联网协议IP之上提供了更可靠的传输层它确保丢失的数据包被重新传输消除重复并且数据包被重新组装成它们被发送的顺序。
>
> 虽然这个系统可以比它的底层部分更可靠但它的可靠性总是有限的。例如纠错码可以处理少量的单比特错误但是如果你的信号被干扰所淹没那么通过信道可以得到多少数据是有根本性的限制的【13】。 TCP可以隐藏数据包的丢失重复和重新排序但是它不能神奇地消除网络中的延迟。
>
@ -211,13 +211,13 @@
如果数据中心网络和互联网是电路交换网络那么在建立电路时就可以建立一个受保证的最大往返时间。但是它们并不是以太网和IP是**分组交换协议**,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。
为什么数据中心网络和互联网使用分组交换?答案是,它们针对**突发流量bursty traffic**进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求——我们只是希望它尽快完成。
为什么数据中心网络和互联网使用分组交换?答案是,它们针对**突发流量bursty traffic** 进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求——我们只是希望它尽快完成。
如果想通过电路传输文件你得预测一个带宽分配。如果你猜的太低传输速度会不必要的太慢导致网络容量闲置。如果你猜的太高电路就无法建立因为如果无法保证其带宽分配网络不能建立电路。因此将电路用于突发数据传输会浪费网络容量并且使传输不必要地缓慢。相比之下TCP动态调整数据传输速率以适应可用的网络容量。
已经有一些尝试去建立同时支持电路交换和分组交换的混合网络比如ATM[^iii]。InfiniBand有一些相似之处【35】它在链路层实现了端到端的流量控制从而减少了在网络中排队的需要尽管它仍然可能因链路拥塞而受到延迟【36】。通过仔细使用**服务质量quality of service,**QoS数据包的优先级和调度和**准入控制admission control**(限速发送器),可以在分组网络上模拟电路交换,或提供统计上的**有限延迟**【25,32】。
[^iii]: **异步传输模式Asynchronous Transfer Mode, ATM**在20世纪80年代是以太网的竞争对手【32】但在电话网核心交换机之外并没有得到太多的采用。它与自动柜员机也称为自动取款机无关尽管共用一个缩写词。或许在一些平行的世界里互联网是基于像ATM这样的东西因此它们的互联网视频通话可能比我们的更可靠因为它们不会遭受包的丢失和延迟。
[^iii]: **异步传输模式Asynchronous Transfer Mode, ATM** 在20世纪80年代是以太网的竞争对手【32】但在电话网核心交换机之外并没有得到太多的采用。它与自动柜员机也称为自动取款机无关尽管共用一个缩写词。或许在一些平行的世界里互联网是基于像ATM这样的东西因此它们的互联网视频通话可能比我们的更可靠因为它们不会遭受包的丢失和延迟。
但是,目前在多租户数据中心和公共云或通过互联网[^iv]进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设网络拥塞,排队和无限的延迟总是会发生。因此,超时时间没有“正确”的值——它需要通过实验来确定。
@ -279,7 +279,7 @@
在具有多个CPU插槽的服务器上每个CPU可能有一个单独的计时器但不一定与其他CPU同步。操作系统会补偿所有的差异并尝试向应用线程表现出单调钟的样子即使这些线程被调度到不同的CPU上。当然明智的做法是不要太把这种单调性保证当回事【40】。
如果NTP协议检测到计算机的本地石英钟比NTP服务器要更快或更慢则可以调整单调钟向前走的频率这称为**偏移skewing**时钟。默认情况下NTP允许时钟速率增加或减慢最高至0.05但NTP不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好在大多数系统中它们能在几微秒或更短的时间内测量时间间隔。
如果NTP协议检测到计算机的本地石英钟比NTP服务器要更快或更慢则可以调整单调钟向前走的频率这称为**偏移skewing** 时钟。默认情况下NTP允许时钟速率增加或减慢最高至0.05但NTP不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好在大多数系统中它们能在几微秒或更短的时间内测量时间间隔。
在分布式系统中,使用单调钟测量**经过时间elapsed time**(比如超时)通常很好,因为它不假定不同节点的时钟之间存在任何同步,并且对测量的轻微不准确性不敏感。
@ -399,14 +399,14 @@ while (true) {
假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种:
* 许多编程语言运行时如Java虚拟机都有一个垃圾收集器GC偶尔需要停止所有正在运行的线程。这些“**停止所有处理stop-the-world**”GC暂停有时会持续几分钟【64】甚至像HotSpot JVM的CMS这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行它需要不时地停止所有处理【65】。尽管通常可以通过改变分配模式或调整GC设置来减少暂停【66】但是如果我们想要提供健壮的保证就必须假设最坏的情况发生。
* 在虚拟化环境中,可以**挂起suspend**虚拟机暂停执行所有进程并将内存内容保存到磁盘并恢复恢复内存内容并继续执行。这个暂停可以在进程执行的任何时候发生并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移而不需要重新启动在这种情况下暂停的长度取决于进程写入内存的速率【67】。
* 在虚拟化环境中,可以**挂起suspend** 虚拟机暂停执行所有进程并将内存内容保存到磁盘并恢复恢复内存内容并继续执行。这个暂停可以在进程执行的任何时候发生并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移而不需要重新启动在这种情况下暂停的长度取决于进程写入内存的速率【67】。
* 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
* 当操作系统上下文切换到另一个线程时或者当管理程序切换到另一个虚拟机时在虚拟机中运行时当前正在运行的线程可能在代码中的任意点处暂停。在虚拟机的情况下在其他虚拟机中花费的CPU时间被称为**窃取时间steal time**。如果机器处于沉重的负载下(即,如果等待运行的线程队列很长),暂停的线程再次运行可能需要一些时间。
* 如果应用程序执行同步磁盘访问则线程可能暂停等待缓慢的磁盘I/O操作完成【68】。在许多语言中即使代码没有包含文件访问磁盘访问也可能出乎意料地发生——例如Java类加载器在第一次使用时惰性加载类文件这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备如亚马逊的EBSI/O延迟进一步受到网络延迟变化的影响【29】。
* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致**页面错误page fault**要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时线程暂停。如果内存压力很高则可能需要将另一个页面换出到磁盘。在极端情况下操作系统可能花费大部分时间将页面交换到内存中而实际上完成的工作很少这被称为**抖动thrashing**)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
* 可以通过发送SIGSTOP信号来暂停Unix进程例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期直到SIGCONT恢复为止此时它将继续运行。 即使你的环境通常不使用SIGSTOP也可能由运维工程师意外发送。
所有这些事件都可以随时**抢占preempt**正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时序做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
所有这些事件都可以随时**抢占preempt** 正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时序做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不能直接转化为分布式系统操作,因为分布式系统没有共享内存,只有通过不可靠网络发送的消息。
@ -416,7 +416,7 @@ while (true) {
在许多编程语言和操作系统中,线程和进程可能暂停一段无限制的时间,正如讨论的那样。如果你足够努力,导致暂停的原因是**可以**消除的。
某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:控制飞机、火箭、机器人、汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的**截止时间deadline**,如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的**硬实时hard real-time**系统。
某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:控制飞机、火箭、机器人、汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的**截止时间deadline**,如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的**硬实时hard real-time** 系统。
> #### 实时是真的吗?
>
@ -456,17 +456,17 @@ while (true) {
### 真相由多数所定义
设想一个具有不对称故障的网络一个节点能够接收发送给它的所有消息但是来自该节点的任何传出消息被丢弃或延迟【19】。即使该节点运行良好并且正在接收来自其他节点的请求其他节点也无法听到其响应。经过一段时间后其他节点宣布它已经死亡因为他们没有听到节点的消息。这种情况就像梦魇一样**半断开semi-disconnected**的节点被拖向墓地,敲打尖叫道“我没死!” ——但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。
设想一个具有不对称故障的网络一个节点能够接收发送给它的所有消息但是来自该节点的任何传出消息被丢弃或延迟【19】。即使该节点运行良好并且正在接收来自其他节点的请求其他节点也无法听到其响应。经过一段时间后其他节点宣布它已经死亡因为他们没有听到节点的消息。这种情况就像梦魇一样**半断开semi-disconnected** 的节点被拖向墓地,敲打尖叫道“我没死!” ——但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。
在一个稍微不那么梦魇的场景中,半断开的节点可能会注意到它发送的消息没有被其他节点确认,因此意识到网络中必定存在故障。尽管如此,节点被其他节点错误地宣告为死亡,而半连接的节点对此无能为力。
第三种情况,想象一个经历了一个长时间**停止所有处理垃圾收集暂停stop-the-world GC Pause**的节点。节点的所有线程被GC抢占并暂停一分钟因此没有请求被处理也没有响应被发送。其他节点等待重试不耐烦并最终宣布节点死亡并将其丢到灵车上。最后GC完成节点的线程继续好像什么也没有发生。其他节点感到惊讶因为所谓的死亡节点突然从棺材中抬起头来身体健康开始和旁观者高兴地聊天。GC后的节点最初甚至没有意识到已经经过了整整一分钟而且自己已被宣告死亡。从它自己的角度来看从最后一次与其他节点交谈以来几乎没有经过任何时间。
第三种情况,想象一个经历了一个长时间**停止所有处理垃圾收集暂停stop-the-world GC Pause** 的节点。节点的所有线程被GC抢占并暂停一分钟因此没有请求被处理也没有响应被发送。其他节点等待重试不耐烦并最终宣布节点死亡并将其丢到灵车上。最后GC完成节点的线程继续好像什么也没有发生。其他节点感到惊讶因为所谓的死亡节点突然从棺材中抬起头来身体健康开始和旁观者高兴地聊天。GC后的节点最初甚至没有意识到已经经过了整整一分钟而且自己已被宣告死亡。从它自己的角度来看从最后一次与其他节点交谈以来几乎没有经过任何时间。
这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。
这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第九章](ch9.md)中讨论**共识算法consensus algorithms**时,我们将更详细地讨论法定人数的应用。
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第九章](ch9.md)中讨论**共识算法consensus algorithms** 时,我们将更详细地讨论法定人数的应用。
#### 领导者和锁
@ -522,7 +522,7 @@ while (true) {
>
> 拜占庭是后来成为君士坦丁堡的古希腊城市现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反这个名字来源于拜占庭式的过度复杂官僚迂回等意义早在计算机之前就已经在政治中被使用了【79】。Lamport想要选一个不会冒犯任何读者的国家他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意【80】。
当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为**拜占庭容错Byzantine fault-tolerant**的,在特定场景下,这种担忧在是有意义的:
当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为**拜占庭容错Byzantine fault-tolerant** 的,在特定场景下,这种担忧在是有意义的:
* 在航空航天环境中计算机内存或CPU寄存器中的数据可能被辐射破坏导致其以任意不可预知的方式响应其他节点。由于系统故障非常昂贵例如飞机撞毁和炸死船上所有人员或火箭与国际空间站相撞飞行控制系统必须容忍拜占庭故障【81,82】。
* 在多个参与组织的系统中一些参与者可能会试图欺骗或欺骗他人。在这种情况下节点仅仅信任另一个节点的消息是不安全的因为它们可能是出于恶意的目的而被发送的。例如像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式而不依赖于中心机构central authority【83】。
@ -553,11 +553,11 @@ while (true) {
***同步模型***
**同步模型synchronous model**假设网络延迟、进程暂停和和时钟误差都是受限的。这并不意味着完全同步的时钟或零网络延迟这只意味着你知道网络延迟、暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型因为如本章所讨论的无限延迟和暂停确实会发生。
**同步模型synchronous model** 假设网络延迟、进程暂停和和时钟误差都是受限的。这并不意味着完全同步的时钟或零网络延迟这只意味着你知道网络延迟、暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型因为如本章所讨论的无限延迟和暂停确实会发生。
***部分同步模型***
**部分同步partial synchronous**意味着一个系统在大多数情况下像一个同步系统一样运行但有时候会超出网络延迟进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型大多数情况下网络和进程表现良好否则我们永远无法完成任何事情但是我们必须承认在任何时刻都存在时序假设偶然被破坏的事实。发生这种情况时网络延迟、暂停和时钟错误可能会变得相当大。
**部分同步partial synchronous** 意味着一个系统在大多数情况下像一个同步系统一样运行但有时候会超出网络延迟进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型大多数情况下网络和进程表现良好否则我们永远无法完成任何事情但是我们必须承认在任何时刻都存在时序假设偶然被破坏的事实。发生这种情况时网络延迟、暂停和时钟错误可能会变得相当大。
***异步模型***
@ -568,17 +568,17 @@ while (true) {
***崩溃-停止故障***
在**崩溃停止crash-stop**模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。
在**崩溃停止crash-stop** 模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。
***崩溃-恢复故障***
我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在**崩溃-恢复crash-recovery**模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。
我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在**崩溃-恢复crash-recovery** 模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。
***拜占庭(任意)故障***
节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点,如上一节所述。
对于真实系统的建模,具有**崩溃-恢复故障crash-recovery**的**部分同步模型partial synchronous**通常是最有用的模型。分布式算法如何应对这种模型?
对于真实系统的建模,具有**崩溃-恢复故障crash-recovery** 的**部分同步模型partial synchronous** 通常是最有用的模型。分布式算法如何应对这种模型?
#### 算法的正确性

72
ch9.md
View File

@ -90,7 +90,7 @@
* 客户端A的第一个读操作完成于写操作开始之前因此必须返回旧值 `0`
* 客户端A的最后一个读操作开始于写操作完成之后。如果数据库是线性一致性的它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。
* 与写操作在时间上重叠的任何读操作,可能会返回 `0``1` ,因为我们不知道读取时,写操作是否已经生效。这些操作是**并发concurrent**的。
* 与写操作在时间上重叠的任何读操作,可能会返回 `0``1` ,因为我们不知道读取时,写操作是否已经生效。这些操作是**并发concurrent** 的。
但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii]
@ -202,7 +202,7 @@
***单主复制(可能线性一致)***
在具有单主复制功能的系统中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能potential**是线性一致性的[^iv]。然而实际上并不是每个单主数据库都是线性一致性的无论是因为设计的原因例如因为使用了快照隔离还是因为在并发处理上存在错误【10】。
在具有单主复制功能的系统中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能potential** 是线性一致性的[^iv]。然而实际上并不是每个单主数据库都是线性一致性的无论是因为设计的原因例如因为使用了快照隔离还是因为在并发处理上存在错误【10】。
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(请参阅“[分布式事务与共识](#分布式事务与共识)”)。
@ -287,7 +287,7 @@
在分布式系统中有更多有趣的“不可能”的结果【41】且CAP定理现在已经被更精确的结果取代【2,42】所以它现在基本上成了历史古迹了。
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区partition**指代将大数据集细分为小数据集的操作(分片;请参阅[第六章](ch6.md))。与之对应的是,**网络分区network partition**是一种特定类型的网络故障我们通常不会将其与其他类型的故障分开考虑。但是由于它是CAP的P所以这种情况下我们无法避免混乱。
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区partition** 指代将大数据集细分为小数据集的操作(分片;请参阅[第六章](ch6.md))。与之对应的是,**网络分区network partition** 是一种特定类型的网络故障我们通常不会将其与其他类型的故障分开考虑。但是由于它是CAP的P所以这种情况下我们无法避免混乱。
#### 线性一致性和网络延迟
@ -310,7 +310,7 @@
**顺序ordering**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文:
* 在[第五章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序order of write**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
* 在[第七章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序some sequential order**执行的保证。它可以字面意义上地以**串行顺序serial order**执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
* 在[第七章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序some sequential order** 执行的保证。它可以字面意义上地以**串行顺序serial order** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
* 在[第八章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
事实证明,顺序、线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
@ -322,21 +322,21 @@
* 在“[一致前缀读](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.png)所示)。
* 事务之间**写偏差write skew**的例子(请参阅“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离)通过跟踪事务之间的因果依赖来检测写偏差。
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致consistent with causality**如果快照包含答案它也必须包含被回答的问题【48】。在某个时间点观察整个数据库与因果关系保持一致意味着因果上在该时间点之前发生的所有操作其影响都是可见的但因果上在该时间点之后发生的操作其影响对观察者不可见。**读偏差read skew** 意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6.png)所示)。
* 事务之间**写偏差write skew** 的例子(请参阅“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离)通过跟踪事务之间的因果依赖来检测写偏差。
* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。
因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致causally consistent**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。
如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致causally consistent** 的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。
#### 因果顺序不是全序的
**全序total order**允许任意两个元素进行比较所以如果有两个元素你总是可以说出哪个更大哪个更小。例如自然数集是全序的给定两个自然数比如说5和13那么你可以告诉我13大于5。
**全序total order** 允许任意两个元素进行比较所以如果有两个元素你总是可以说出哪个更大哪个更小。例如自然数集是全序的给定两个自然数比如说5和13那么你可以告诉我13大于5。
然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较incomparable**的,因此数学集合是**偏序partially order**的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]。
然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较incomparable** 的,因此数学集合是**偏序partially order** 的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]。
[^译注i]: 设R为非空集合A上的关系如果R是自反的、反对称的和可传递的则称R为A上的偏序关系。简称偏序通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集记作$(A,R)$或$(A, ≦)$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。
@ -354,11 +354,11 @@
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
如果你熟悉像Git这样的分布式版本控制系统那么其版本历史与因果关系图极其相似。通常一个**提交Commit**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并Merge**会在这些并发创建的提交相融合时创建。
如果你熟悉像Git这样的分布式版本控制系统那么其版本历史与因果关系图极其相似。通常一个**提交Commit** 发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并Merge** 会在这些并发创建的提交相融合时创建。
#### 线性一致性强于因果一致性
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着implies**因果关系任何线性一致的系统都能正确保持因果性【7】。特别是如果系统中有多个通信通道如[图9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着implies** 因果关系任何线性一致的系统都能正确保持因果性【7】。特别是如果系统中有多个通信通道如[图9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。
线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。
@ -390,7 +390,7 @@
这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每个操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。
特别是,我们可以使用**与因果一致consistent with causality**的全序来生成序列号[^vii]:我们保证,如果操作 A 因果地发生在操作 B 前,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。
特别是,我们可以使用**与因果一致consistent with causality** 的全序来生成序列号[^vii]:我们保证,如果操作 A 因果地发生在操作 B 前,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。
[^vii]: 与因果关系不一致的全序很容易创建但没啥用。例如你可以为每个操作生成随机的UUID并按照字典序比较UUID以定义操作的全序。这是一个有效的全序但是随机的UUID并不能告诉你哪个操作先发生或者操作是否为并发的。
@ -447,7 +447,7 @@
乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。
这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上right now**决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存在其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。
这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上right now** 决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存在其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。
为了确保没有其他节点正在使用相同的用户名和较小的时间戳并发创建同名账户你必须检查其它每个节点看看它在做什么【56】。如果其中一个节点由于网络问题出现故障或不可达则整个系统可能被拖至停机。这不是我们需要的那种容错系统。
@ -499,7 +499,7 @@
如 [图9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系[^x]。
[^x]: 从形式上讲,线性一致读写寄存器是一个“更容易”的问题。 全序广播等价于共识【67】而共识问题在异步的崩溃-停止模型【68】中没有确定性的解决方案而线性一致的读写寄存器**可以**在这种模型中实现【23,24,25】。 然而,支持诸如**比较并设置CAS, compare-and-set**,或**自增并返回increment-and-get**的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。
[^x]: 从形式上讲,线性一致读写寄存器是一个“更容易”的问题。 全序广播等价于共识【67】而共识问题在异步的崩溃-停止模型【68】中没有确定性的解决方案而线性一致的读写寄存器**可以**在这种模型中实现【23,24,25】。 然而,支持诸如**比较并设置CAS, compare-and-set**,或**自增并返回increment-and-get** 的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。
全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息**何时**被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。
@ -555,7 +555,7 @@
***原子提交***
在支持跨多节点或跨多分区事务的数据库中一个事务可能在某些节点上失败但在其他节点上成功。如果我们想要维护事务的原子性就ACID而言请参阅“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交atomic commit**问题[^xii]。
在支持跨多节点或跨多分区事务的数据库中一个事务可能在某些节点上失败但在其他节点上成功。如果我们想要维护事务的原子性就ACID而言请参阅“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交atomic commit** 问题[^xii]。
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 请参阅“[三阶段提交](#三阶段提交)”。
@ -568,7 +568,7 @@
>
> 因此虽然FLP是关于共识不可能性的重要理论结果但现实中的分布式系统通常是可以达成共识的。
在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交2PC, two-phase commit**算法这是解决原子提交问题最常见的办法并在各种数据库、消息队列和应用服务器中被实现。事实证明2PC是一种共识算法但不是一个非常好的共识算法【70,71】。
在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交2PC, two-phase commit** 算法这是解决原子提交问题最常见的办法并在各种数据库、消息队列和应用服务器中被实现。事实证明2PC是一种共识算法但不是一个非常好的共识算法【70,71】。
通过对2PC的学习我们将继续努力实现更好的一致性算法比如ZooKeeperZab和etcdRaft中使用的算法。
@ -616,10 +616,10 @@
2PC使用一个通常不会出现在单节点事务中的新组件**协调者coordinator**(也称为**事务管理器transaction manager**。协调者通常在请求事务的相同应用进程中以库的形式实现例如嵌入在Java EE容器中但也可以是单独的进程或服务。这种协调者的例子包括Narayana、JOTM、BTM或MSDTC。
正常情况下2PC事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者participants**。当应用准备提交时,协调者开始阶段 1 :它发送一个**准备prepare**请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:
正常情况下2PC事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者participants**。当应用准备提交时,协调者开始阶段 1 :它发送一个**准备prepare** 请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:
* 如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出**提交commit**请求,然后提交真正发生。
* 如果任意一个参与者回复了“否”则协调者在阶段2 中向所有节点发送**中止abort**请求。
* 如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出**提交commit** 请求,然后提交真正发生。
* 如果任意一个参与者回复了“否”则协调者在阶段2 中向所有节点发送**中止abort** 请求。
这个过程有点像西方传统婚姻仪式司仪分别询问新娘和新郎是否要结婚通常是从两方都收到“我愿意”的答复。收到两者的回复后司仪宣布这对情侣成为夫妻事务就提交了这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复”我愿意“婚礼就会中止【73】。
@ -644,7 +644,7 @@
我们已经讨论了在2PC期间如果参与者之一或网络发生故障时会发生什么情况如果任何一个**准备**请求失败或者超时,协调者就会中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是如果协调者崩溃,会发生什么情况就不太清楚了。
如果协调者在发送**准备**请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为**存疑in doubt**的或**不确定uncertain**的。
如果协调者在发送**准备**请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为**存疑in doubt** 的或**不确定uncertain** 的。
情况如[图9-10](img/fig9-10.png) 所示。在这个特定的例子中协调者实际上决定提交数据库2 收到提交请求。但是协调者在将提交请求发送到数据库1 之前发生崩溃因此数据库1 不知道是否提交或中止。即使**超时**在这里也没有帮助如果数据库1 在超时后单方面中止它将最终与执行提交的数据库2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。
@ -657,9 +657,9 @@
#### 三阶段提交
两阶段提交被称为**阻塞blocking**原子提交协议因为存在2PC可能卡住并等待协调者恢复的情况。理论上可以使一个原子提交协议变为**非阻塞nonblocking**的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。
两阶段提交被称为**阻塞blocking**- 原子提交协议因为存在2PC可能卡住并等待协调者恢复的情况。理论上可以使一个原子提交协议变为**非阻塞nonblocking** 的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。
作为2PC的替代方案已经提出了一种称为**三阶段提交3PC**的算法【13,80】。然而3PC假定网络延迟有界节点响应时间有限在大多数具有无限网络延迟和进程暂停的实际系统中见[第八章](ch8.md)),它并不能保证原子性。
作为2PC的替代方案已经提出了一种称为**三阶段提交3PC** 的算法【13,80】。然而3PC假定网络延迟有界节点响应时间有限在大多数具有无限网络延迟和进程暂停的实际系统中见[第八章](ch8.md)),它并不能保证原子性。
通常,非阻塞原子提交需要一个**完美的故障检测器perfect failure detector**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中超时并不是一种可靠的故障检测机制因为即使没有节点崩溃请求也可能由于网络问题而超时。出于这个原因2PC仍然被使用尽管大家都清楚可能存在协调者故障的问题。
@ -679,7 +679,7 @@
***异构分布式事务***
在**异构heterogeneous**事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
在**异构heterogeneous** 事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
数据库内部事务不必与任何其他系统兼容,因此它们可以使用任何协议,并能针对特定技术进行特定的优化。因此数据库内部的分布式事务通常工作地很好。另一方面,跨异构技术的事务则更有挑战性。
@ -687,15 +687,15 @@
异构的分布式事务处理能够以强大的方式集成不同的系统。例如:消息队列中的一条消息可以被确认为已处理,当且仅当用于处理消息的数据库事务成功提交。这是通过在同一个事务中原子提交**消息确认**和**数据库写入**两个操作来实现的。藉由分布式事务的支持,即使消息代理和数据库是在不同机器上运行的两种不相关的技术,这种操作也是可能的。
如果消息传递或数据库事务任意一者失败,两者都会中止,因此消息代理可能会在稍后安全地重传消息。因此,通过原子提交**消息处理及其副作用**,即使在成功之前需要几次重试,也可以确保消息被**有效地effectively**恰好处理一次。中止会抛弃部分完成事务所导致的任何副作用。
如果消息传递或数据库事务任意一者失败,两者都会中止,因此消息代理可能会在稍后安全地重传消息。因此,通过原子提交**消息处理及其副作用**,即使在成功之前需要几次重试,也可以确保消息被**有效地effectively** 恰好处理一次。中止会抛弃部分完成事务所导致的任何副作用。
然而,只有当所有受事务影响的系统都使用同样的**原子提交协议atomic commit protocl**时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。
然而,只有当所有受事务影响的系统都使用同样的**原子提交协议atomic commit protocol** 时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。
在[第十一章](ch11.md)中将再次回到“恰好一次”消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
#### XA事务
*X/Open XA***扩展架构eXtended Architecture**的缩写是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实现许多传统关系数据库包括PostgreSQLMySQLDB2SQL Server和Oracle和消息代理包括ActiveMQHornetQMSMQ和IBM MQ 都支持XA。
*X/Open XA***扩展架构eXtended Architecture** 的缩写是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实现许多传统关系数据库包括PostgreSQLMySQLDB2SQL Server和Oracle和消息代理包括ActiveMQHornetQMSMQ和IBM MQ 都支持XA。
XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定例如在Java EE应用的世界中XA事务是使用**Java事务APIJTA, Java Transaction API**实现的,而许多使用**Java数据库连接JDBC, Java Database Connectivity**的数据库驱动,以及许多使用**Java消息服务JMS**API的消息代理都支持**Java事务APIJTA**。
@ -717,13 +717,13 @@
#### 从协调者故障中恢复
理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何存疑事务。然而在实践中,**孤立orphaned**的存疑事务确实会出现【89,90】即无论出于何种理由协调者无法确定事务的结果例如事务日志已经由于软件错误丢失或损坏。这些事务无法自动解决所以它们永远待在数据库中持有锁并阻塞其他事务。
理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何存疑事务。然而在实践中,**孤立orphaned** 的存疑事务确实会出现【89,90】即无论出于何种理由协调者无法确定事务的结果例如事务日志已经由于软件错误丢失或损坏。这些事务无法自动解决所以它们永远待在数据库中持有锁并阻塞其他事务。
即使重启数据库服务器也无法解决这个问题因为在2PC的正确实现中即使重启也必须保留存疑事务的锁否则就会冒违反原子性保证的风险。这是一种棘手的情况。
唯一的出路是让管理员手动决定提交还是回滚事务。管理员必须检查每个存疑事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(不然为什么协调者处于这种糟糕的状态),并很可能要在巨大精神压力和时间压力下完成。
许多XA的实现都有一个叫做**启发式决策heuristic decisions**的紧急逃生舱口允许参与者单方面决定放弃或提交一个存疑事务而无需协调者做出最终决定【76,77,91】。要清楚的是这里**启发式**是**可能破坏原子性probably breaking atomicity**的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。
许多XA的实现都有一个叫做**启发式决策heuristic decisions** 的紧急逃生舱口允许参与者单方面决定放弃或提交一个存疑事务而无需协调者做出最终决定【76,77,91】。要清楚的是这里**启发式**是**可能破坏原子性probably breaking atomicity** 的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。
#### 分布式事务的限制
@ -732,7 +732,7 @@
* 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。
* 许多服务器端应用都是使用无状态模式开发的受HTTP的青睐所有持久状态都存储在数据库中因此具有应用服务器可随意按需添加删除的优点。但是当协调者成为应用服务器的一部分时它会改变部署的性质。突然间协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。
* 由于XA需要兼容各种数据系统因此它必须是所有系统的最小公分母。例如它不能检测不同系统间的死锁因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息而且它无法与SSI请参阅[可串行化快照隔离](ch7.md#可串行化快照隔离 ))协同工作,因为这需要一个跨系统定位冲突的协议。
* 对于数据库内部的分布式事务不是XA限制没有这么大 —— 例如分布式版本的SSI是可能的。然而仍然存在问题2PC成功提交一个事务需要所有参与者的响应。因此如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效amplifying failures**的趋势,这又与我们构建容错系统的目标背道而驰。
* 对于数据库内部的分布式事务不是XA限制没有这么大 —— 例如分布式版本的SSI是可能的。然而仍然存在问题2PC成功提交一个事务需要所有参与者的响应。因此如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效amplifying failures** 的趋势,这又与我们构建容错系统的目标背道而驰。
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于**共识**的话题。
@ -740,13 +740,13 @@
### 容错共识
非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时concurrently**尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容mutually incompatible**的操作中,哪一个才是赢家。
非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时concurrently** 尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容mutually incompatible** 的操作中,哪一个才是赢家。
共识问题通常形式化如下:一个或多个节点可以**提议propose**某些值,而共识算法**决定decides**采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**将要服务的顾客的ID而**决定**指明了哪个顾客获得了座位。
在这种形式下共识算法必须满足以下性质【25】[^xiii]
[^xiii]: 这种共识的特殊形式被称为**统一共识uniform consensus**,相当于在具有不可靠故障检测器的异步系统中的**常规共识regular consensus**【71】。学术文献通常指的是**进程process**而不是节点,但我们在这里使用**节点node**来与本书的其余部分保持一致。
[^xiii]: 这种共识的特殊形式被称为**统一共识uniform consensus**,相当于在具有不可靠故障检测器的异步系统中的**常规共识regular consensus**【71】。学术文献通常指的是**进程process** 而不是节点,但我们在这里使用**节点node** 来与本书的其余部分保持一致。
***一致同意Uniform agreement***
@ -771,7 +771,7 @@
共识的系统模型假设当一个节点“崩溃”时它会突然消失而且永远不会回来。不像软件崩溃想象一下地震包含你的节点的数据中心被山体滑坡所摧毁你必须假设节点被埋在30英尺以下的泥土中并且永远不会重新上线在这个系统模型中任何需要等待节点恢复的算法都不能满足**终止**属性。特别是2PC不符合终止属性的要求。
当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数majority**的节点正确工作以确保终止属性【67】。多数可以安全地组成法定人数请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数majority** 的节点正确工作以确保终止属性【67】。多数可以安全地组成法定人数请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
因此**终止**属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足—— 一致同意完整性和有效性【92】。因此大规模的中断可能会阻止系统处理请求但是它不能通过使系统做出无效的决定来破坏共识系统。
@ -792,7 +792,7 @@
* 由于**有效性**属性,消息不会被损坏,也不能凭空编造。
* 由于**终止**属性,消息不会丢失。
视图戳复制Raft和Zab直接实现了全序广播因为这样做比重复**一次一值one value a time**的共识更高效。在Paxos的情况下这种优化被称为Multi-Paxos。
视图戳复制Raft和Zab直接实现了全序广播因为这样做比重复**一次一值one value a time** 的共识更高效。在Paxos的情况下这种优化被称为Multi-Paxos。
#### 单领导者复制与共识
@ -814,7 +814,7 @@
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真相由多数所定义](ch8.md#真相由多数所定义)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
相反,它必须从**法定人数quorum**的节点中获取选票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。对领导者想要做出的每一个决定都必须将提议值发送给其他节点并等待法定人数的节点响应并赞成提案。法定人数通常但不总是由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下一个节点才会投票赞成提议。
相反,它必须从**法定人数quorum** 的节点中获取选票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。对领导者想要做出的每一个决定都必须将提议值发送给其他节点并等待法定人数的节点响应并赞成提案。法定人数通常但不总是由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下一个节点才会投票赞成提议。
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠overlap**如果一个提案的表决通过则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论没有发生过更高时代的领导选举因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。
@ -830,7 +830,7 @@
共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(请参阅“[线性一致性的代价](#线性一致性的代价)”)。
大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展dynamic membership extension**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。
大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展dynamic membership extension** 允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。
共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。
@ -848,7 +848,7 @@
***线性一致性的原子操作***
使用原子CAS操作可以实现锁如果多个节点同时尝试执行相同的操作只有一个节点会成功。共识协议保证了操作的原子性和线性一致性即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约lease**的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
使用原子CAS操作可以实现锁如果多个节点同时尝试执行相同的操作只有一个节点会成功。共识协议保证了操作的原子性和线性一致性即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约lease** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
***操作的全序排序***