mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
unify the translation of cross references
This commit is contained in:
parent
17b2f013e0
commit
7d7ac64719
@ -141,7 +141,7 @@
|
||||
0. 全文校订 by [@yingang](https://github.com/yingang)
|
||||
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
|
||||
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 与[第10章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 与[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
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)
|
||||
@ -209,7 +209,7 @@
|
||||
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
|
||||
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
|
||||
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 删除一个多余的右括号 |
|
||||
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第7章底部链接错误 |
|
||||
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部链接错误 |
|
||||
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假简单"->"更加简单" |
|
||||
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修复 ch1 中的无序列表格式 |
|
||||
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 纠正多处的翻译小错误 |
|
||||
|
8
ch1.md
8
ch1.md
@ -60,11 +60,11 @@
|
||||
|
||||
***可伸缩性(Scalability)***
|
||||
|
||||
有合理的办法应对系统的增长(数据量、流量、复杂性)(参阅“[可伸缩性](#可伸缩性)”)
|
||||
有合理的办法应对系统的增长(数据量、流量、复杂性)(请参阅“[可伸缩性](#可伸缩性)”)
|
||||
|
||||
***可维护性(Maintainability)***
|
||||
|
||||
许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。(参阅”[可维护性](#可维护性)“)
|
||||
许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。(请参阅”[可维护性](#可维护性)“)
|
||||
|
||||
|
||||
|
||||
@ -199,7 +199,7 @@
|
||||
|
||||
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可伸缩性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。
|
||||
|
||||
推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。在[第12章](ch12.md)中我们将重新讨论这个例子,这在覆盖更多技术层面之后。
|
||||
推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。在[第十二章](ch12.md)中我们将重新讨论这个例子,这在覆盖更多技术层面之后。
|
||||
|
||||
### 描述性能
|
||||
|
||||
@ -264,7 +264,7 @@
|
||||
|
||||
人们经常讨论**纵向伸缩(scaling up)**(**垂直伸缩(vertical scaling)**,转向更强大的机器)和**横向伸缩(scaling out)** (**水平伸缩(horizontal scaling)**,将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“**无共享(shared-nothing)**”架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向伸缩。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
|
||||
|
||||
有些系统是 **弹性(elastic)** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动伸缩(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测(highly unpredictable)**,则弹性系统可能很有用,但手动伸缩系统更简单,并且意外操作可能会更少(参阅“[分区再平衡](ch6.md#分区再平衡)”)。
|
||||
有些系统是 **弹性(elastic)** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动伸缩(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测(highly unpredictable)**,则弹性系统可能很有用,但手动伸缩系统更简单,并且意外操作可能会更少(请参阅“[分区再平衡](ch6.md#分区再平衡)”)。
|
||||
|
||||
跨多台机器部署 **无状态服务(stateless services)** 非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向伸缩),直到伸缩成本或可用性需求迫使其改为分布式。
|
||||
|
||||
|
72
ch10.md
72
ch10.md
@ -12,7 +12,7 @@
|
||||
|
||||
在本书的前两部分中,我们讨论了很多关于**请求**和**查询**以及相应的**响应**或**结果**。许多现有数据系统中都采用这种数据处理方式:你发送请求指令,一段时间后(我们期望)系统会给出一个结果。数据库、缓存、搜索索引、Web服务器以及其他一些系统都以这种方式工作。
|
||||
|
||||
像这样的**在线(online)**系统,无论是浏览器请求页面还是调用远程API的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(参阅“[描述性能](ch1.md#描述性能)”)。
|
||||
像这样的**在线(online)**系统,无论是浏览器请求页面还是调用远程API的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅“[描述性能](ch1.md#描述性能)”)。
|
||||
|
||||
Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统:
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
***流处理系统(准实时系统)***
|
||||
|
||||
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时(near-real-time)**或**准在线(nearline)**处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第11章](ch11.md)讨论它。
|
||||
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时(near-real-time)**或**准在线(nearline)**处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。
|
||||
|
||||
正如我们将在本章中看到的那样,批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能被过分热情地)被称为“造就Google大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用,包括Hadoop,CouchDB和MongoDB。
|
||||
|
||||
@ -124,7 +124,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
哪种方法更好?这取决于你有多少个不同的URL。对于大多数中小型网站,你可能可以为所有不同网址提供一个计数器(假设我们使用1GB内存)。在此例中,作业的**工作集(working set)**(作业需要随机访问的内存大小)仅取决于不同URL的数量:如果日志中只有单个URL,重复出现一百万次,则散列表所需的空间表就只有一个URL加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。
|
||||
|
||||
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在“[SSTables和LSM树](ch3.md#SSTables和LSM树)”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,针对顺序I/O进行优化是[第3章](ch3.md)中反复出现的主题,相同的模式在此重现)
|
||||
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在“[SSTables和LSM树](ch3.md#SSTables和LSM树)”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,针对顺序I/O进行优化是[第三章](ch3.md)中反复出现的主题,相同的模式在此重现)
|
||||
|
||||
GNU Coreutils(Linux)中的`sort `程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个CPU核进行并行排序【9】。这意味着我们之前看到的简单的Unix命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。
|
||||
|
||||
@ -209,11 +209,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
[^iv]: 一个不同之处在于,对于HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用纠删码(Erasure Coding),则会丢失局部性,因为来自多台机器的数据必须进行合并以重建原始文件【20】。
|
||||
|
||||
与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于**无共享**原则(参见[第二部分](part-ii.md)的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。
|
||||
与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于**无共享**原则(请参阅[第二部分](part-ii.md)的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。
|
||||
|
||||
HDFS在每台机器上运行了一个守护进程,它对外暴露网络服务,允许其他节点访问存储在该机器上的文件(假设数据中心中的每台通用计算机都挂载着一些磁盘)。名为**NameNode**的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。
|
||||
|
||||
为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如[第5章](ch5.md)中所述,或者诸如Reed-Solomon码这样的纠删码方案,它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与RAID相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。
|
||||
为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如[第五章](ch5.md)中所述,或者诸如Reed-Solomon码这样的纠删码方案,它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与RAID相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。
|
||||
|
||||
HDFS的可伸缩性已经很不错了:在撰写本书时,最大的HDFS部署运行在上万台机器上,总存储容量达数百PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于在专用存储设备上支持同等容量的成本【24】。
|
||||
|
||||
@ -228,7 +228,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
这四个步骤可以作为一个MapReduce作业执行。步骤2(Map)和4(Reduce)是你编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 —— 你不必编写它,因为Mapper的输出始终在送往Reducer之前进行排序。
|
||||
|
||||
要创建MapReduce作业,你需要实现两个回调函数,Mapper和Reducer,其行为如下(参阅“[MapReduce查询](ch2.md#MapReduce查询)”):
|
||||
要创建MapReduce作业,你需要实现两个回调函数,Mapper和Reducer,其行为如下(请参阅“[MapReduce查询](ch2.md#MapReduce查询)”):
|
||||
|
||||
***Mapper***
|
||||
|
||||
@ -243,9 +243,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
MapReduce与Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来显式处理并行问题。Mapper和Reducer一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。
|
||||
|
||||
在分布式计算中可以使用标准的Unix工具作为Mapper和Reducer【25】,但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,Mapper和Reducer都是实现特定接口的Java类。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函数(参阅“[MapReduce查询](ch2.md#MapReduce查询)”)。
|
||||
在分布式计算中可以使用标准的Unix工具作为Mapper和Reducer【25】,但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,Mapper和Reducer都是实现特定接口的Java类。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函数(请参阅“[MapReduce查询](ch2.md#MapReduce查询)”)。
|
||||
|
||||
[图10-1](img/fig10-1.png)显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(参见[第6章](ch6.md)):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务([图10-1](img/fig10-1.png)中的m1,m2和m3标记)。
|
||||
[图10-1](img/fig10-1.png)显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(请参阅[第六章](ch6.md)):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务([图10-1](img/fig10-1.png)中的m1,m2和m3标记)。
|
||||
|
||||
每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个Mapper,只要该机器有足够的备用RAM和CPU资源来运行Mapper任务【26】。这个原则被称为**将计算放在数据附近**【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。
|
||||
|
||||
@ -255,7 +255,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在大多数情况下,应该在Mapper任务中运行的应用代码在将要运行它的机器上还不存在,所以MapReduce框架首先将代码(例如Java程序中的JAR文件)复制到适当的机器。然后启动Map任务并开始读取输入文件,一次将一条记录传入Mapper回调函数。Mapper的输出由键值对组成。
|
||||
|
||||
计算的Reduce端也被分区。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对最终落在相同的Reducer处,框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对(参见“[根据键的散列分区](ch6.md#根据键的散列分区)”))。
|
||||
计算的Reduce端也被分区。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对最终落在相同的Reducer处,框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对(请参阅“[根据键的散列分区](ch6.md#根据键的散列分区)”))。
|
||||
|
||||
键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个Map任务都按照Reducer对输出进行分区。每个分区都被写入Mapper程序的本地磁盘,使用的技术与我们在“[SSTables与LSM树](ch3.md#SSTables与LSM树)”中讨论的类似。
|
||||
|
||||
@ -281,21 +281,21 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
### Reduce侧连接与分组
|
||||
|
||||
我们在[第2章](ch2.md)中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。
|
||||
我们在[第二章](ch2.md)中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。
|
||||
|
||||
在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的**外键**,文档模型中的**文档引用**或图模型中的**边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如[第2章](ch2.md)所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除[^v]。
|
||||
在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的**外键**,文档模型中的**文档引用**或图模型中的**边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如[第二章](ch2.md)所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除[^v]。
|
||||
|
||||
[^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录通过与其他记录在特定字段(例如ID)中具有**相同值**相关联。有些数据库支持更通用的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有地方来讲这些东西。
|
||||
|
||||
在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用**索引**来快速定位感兴趣的记录(参阅[第3章](ch3.md))。如果查询涉及到连接,则可能涉及到查找多个索引。然而MapReduce没有索引的概念 —— 至少在通常意义上没有。
|
||||
在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用**索引**来快速定位感兴趣的记录(请参阅[第三章](ch3.md))。如果查询涉及到连接,则可能涉及到查找多个索引。然而MapReduce没有索引的概念 —— 至少在通常意义上没有。
|
||||
|
||||
当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为**全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。
|
||||
当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为**全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。
|
||||
|
||||
当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。 例如我们假设一个作业是同时处理所有用户的数据,而非仅仅是为某个特定用户查找数据(而这能通过索引更高效地完成)。
|
||||
|
||||
#### 示例:用户活动事件分析
|
||||
|
||||
[图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件(activity events)**或**点击流数据(clickstream data)**),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
[图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件(activity events)**或**点击流数据(clickstream data)**),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
|
||||
![](img/fig10-2.png)
|
||||
|
||||
@ -307,7 +307,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。而且,查询远程数据库意味着批处理作业变为**非确定的(nondeterministic)**,因为远程数据库中的数据可能会改变。
|
||||
|
||||
因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,参阅“[数据仓库](ch3.md#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在HDFS中的一组文件中,而用户活动记录存储在另一组文件中,并能用MapReduce将所有相关记录集中到同一个地方进行高效处理。
|
||||
因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,请参阅“[数据仓库](ch3.md#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在HDFS中的一组文件中,而用户活动记录存储在另一组文件中,并能用MapReduce将所有相关记录集中到同一个地方进行高效处理。
|
||||
|
||||
#### 排序合并连接
|
||||
|
||||
@ -349,13 +349,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
如果存在与单个键关联的大量数据,则“将具有相同键的所有记录放到相同的位置”这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为**关键对象(linchpin object)**【38】或**热键(hot key)**。
|
||||
|
||||
在单个Reducer中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的**偏斜**(也称为**热点(hot spot)**)—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(参见“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。
|
||||
在单个Reducer中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的**偏斜**(也称为**热点(hot spot)**)—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。
|
||||
|
||||
如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig中的**偏斜连接(skewed join)**方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper会将热键的关联记录**随机**(相对于传统MapReduce基于键散列的确定性方法)发送到几个Reducer之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到**所有**处理该键的Reducer上【40】。
|
||||
|
||||
这种技术将处理热键的工作分散到多个Reducer上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接(sharded join)**方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。
|
||||
|
||||
Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用Map端连接(参阅下一节)。
|
||||
Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用Map端连接(请参阅下一节)。
|
||||
|
||||
当按照热键进行分组并聚合时,可以将分组分两个阶段进行。第一个MapReduce阶段将记录发送到随机Reducer,以便每个Reducer只对热键的子集执行分组,为每个键输出一个更紧凑的中间聚合结果。然后第二个MapReduce作业将所有来自第一阶段Reducer的中间聚合结果合并为每个键一个值。
|
||||
|
||||
@ -413,9 +413,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
我们已经说了很多用于实现MapReduce工作流的算法,但却忽略了一个重要的问题:这些处理完成之后的最终结果是什么?我们最开始为什么要跑这些作业?
|
||||
|
||||
在数据库查询的场景中,我们将事务处理(OLTP)与分析两种目的区分开来(参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。我们看到,OLTP查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前10项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。
|
||||
在数据库查询的场景中,我们将事务处理(OLTP)与分析两种目的区分开来(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。我们看到,OLTP查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前10项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。
|
||||
|
||||
批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而MapReduce作业工作流与用于分析目的的SQL查询是不同的(参阅“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。
|
||||
批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而MapReduce作业工作流与用于分析目的的SQL查询是不同的(请参阅“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。
|
||||
|
||||
#### 建立搜索索引
|
||||
|
||||
@ -423,13 +423,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
我们在“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”中简要地了解了Lucene这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档ID列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等 —— 但这个原则是成立的。
|
||||
|
||||
如果需要对一组固定文档执行全文搜索,则批处理是一种构建索引的高效方法:Mapper根据需要对文档集合进行分区,每个Reducer构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(参阅“[分区与次级索引](ch6.md#分区与次级索引)”)并行处理效果拔群。
|
||||
如果需要对一组固定文档执行全文搜索,则批处理是一种构建索引的高效方法:Mapper根据需要对文档集合进行分区,每个Reducer构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)并行处理效果拔群。
|
||||
|
||||
由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。
|
||||
|
||||
如果索引的文档集合发生更改,一种选择是定期重跑整个索引工作流,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法的计算成本可能会很高。但它的优点是索引过程很容易理解:文档进,索引出。
|
||||
|
||||
另一个选择是,可以增量建立索引。如[第3章](ch3.md)中讨论的,如果要在索引中添加,删除或更新文档,Lucene会写新的段文件,并在后台异步合并压缩段文件。我们将在[第11章](ch11.md)中看到更多这种增量处理。
|
||||
另一个选择是,可以增量建立索引。如[第三章](ch3.md)中讨论的,如果要在索引中添加,删除或更新文档,Lucene会写新的段文件,并在后台异步合并压缩段文件。我们将在[第十一章](ch11.md)中看到更多这种增量处理。
|
||||
|
||||
#### 键值存储作为批处理输出
|
||||
|
||||
@ -447,7 +447,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
更好的解决方案是在批处理作业**内**创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上节中的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。不少键值存储都支持在MapReduce作业中构建数据库文件,包括Voldemort 【46】,Terrapin 【47】,ElephantDB 【48】和HBase批量加载【49】。
|
||||
|
||||
构建这些数据库文件是MapReduce的一种好用法:使用Mapper提取出键并按该键排序,已经完成了构建索引所必需的大量工作。由于这些键值存储大多都是只读的(文件只能由批处理作业一次性写入,然后就不可变),所以数据结构非常简单。比如它们就不需要预写式日志(WAL,参阅“[让B树更可靠](ch3.md#让B树更可靠)”)。
|
||||
构建这些数据库文件是MapReduce的一种好用法:使用Mapper提取出键并按该键排序,已经完成了构建索引所必需的大量工作。由于这些键值存储大多都是只读的(文件只能由批处理作业一次性写入,然后就不可变),所以数据结构非常简单。比如它们就不需要预写式日志(WAL,请参阅“[让B树更可靠](ch3.md#让B树更可靠)”)。
|
||||
|
||||
将数据加载到Voldemort时,服务器将继续用旧数据文件服务请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动将查询切换到新文件。如果在这个过程中出现任何问题,它可以轻易回滚至旧文件,因为它们仍然存在而且不可变【46】。
|
||||
|
||||
@ -463,7 +463,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。
|
||||
- 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。
|
||||
|
||||
在这些领域,在Unix上表现良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如Avro(参阅“[Avro](ch4.md#Avro)”)和Parquet(参阅“[列存储](ch3.md#列存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见[第4章](ch4.md))。
|
||||
在这些领域,在Unix上表现良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如Avro(请参阅“[Avro](ch4.md#Avro)”)和Parquet(请参阅“[列存储](ch3.md#列存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见[第四章](ch4.md))。
|
||||
|
||||
### Hadoop与分布式数据库的对比
|
||||
|
||||
@ -481,11 +481,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在纯粹主义者看来,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更高质量的数据来处理。然而实践经验表明,简单地使数据快速可用 —— 即使它很古怪,难以使用,使用原始格式 —— 也通常要比事先决定理想数据模型要更有价值【54】。
|
||||
|
||||
这个想法与数据仓库类似(参阅“[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨越以前相互分离的数据集进行连接。 MPP数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为“**数据湖(data lake)**”或“**企业数据中心(enterprise data hub)**”【55】)。
|
||||
这个想法与数据仓库类似(请参阅“[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨越以前相互分离的数据集进行连接。 MPP数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为“**数据湖(data lake)**”或“**企业数据中心(enterprise data hub)**”【55】)。
|
||||
|
||||
不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式**方法【56】;参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为**寿司原则(sushi principle)**:“原始数据更好”【57】。
|
||||
不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式**方法【56】;请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为**寿司原则(sushi principle)**:“原始数据更好”【57】。
|
||||
|
||||
因此,Hadoop经常被用于实现ETL过程(参阅“[数据仓库](ch3.md#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系形式,并将其导入MPP数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。
|
||||
因此,Hadoop经常被用于实现ETL过程(请参阅“[数据仓库](ch3.md#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系形式,并将其导入MPP数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。
|
||||
|
||||
#### 处理模型的多样性
|
||||
|
||||
@ -499,7 +499,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在Hadoop方式中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个集群内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。
|
||||
|
||||
Hadoop生态系统包括随机访问的OLTP数据库,如HBase(参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)和MPP风格的分析型数据库,如Impala 【41】。 HBase与Impala都不使用MapReduce,但都使用HDFS进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
|
||||
Hadoop生态系统包括随机访问的OLTP数据库,如HBase(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)和MPP风格的分析型数据库,如Impala 【41】。 HBase与Impala都不使用MapReduce,但都使用HDFS进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
|
||||
|
||||
#### 针对频繁故障设计
|
||||
|
||||
@ -536,13 +536,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次而解决,而对于某些类型的处理,它表现得非常差劲。一方面,MapReduce非常稳健:你可以使用它在任务会频繁终止的多租户系统上处理几乎任意大量级的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理而言,其他工具有时会快上几个数量级。
|
||||
|
||||
在本章的其余部分中,我们将介绍一些批处理方法。在[第11章](ch11.md)我们将转向流处理,它可以看作是加速批处理的另一种方法。
|
||||
在本章的其余部分中,我们将介绍一些批处理方法。在[第十一章](ch11.md)我们将转向流处理,它可以看作是加速批处理的另一种方法。
|
||||
|
||||
### 物化中间状态
|
||||
|
||||
如前所述,每个MapReduce作业都独立于其他任何作业。作业与世界其他地方的主要连接点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为第一个作业输出目录,且外部工作流调度程序必须在第一个作业完成后再启动第二个。
|
||||
|
||||
如果第一个作业的输出是要在组织内广泛发布的数据集,则这种配置是合理的。在这种情况下,你需要通过名称引用它,并将其重用为多个不同作业的输入(包括由其他团队开发的作业)。将数据发布到分布式文件系统中众所周知的位置能够带来**松耦合**,这样作业就不需要知道是谁在提供输入或谁在消费输出(参阅“[逻辑与布线相分离](#逻辑与布线相分离)”)。
|
||||
如果第一个作业的输出是要在组织内广泛发布的数据集,则这种配置是合理的。在这种情况下,你需要通过名称引用它,并将其重用为多个不同作业的输入(包括由其他团队开发的作业)。将数据发布到分布式文件系统中众所周知的位置能够带来**松耦合**,这样作业就不需要知道是谁在提供输入或谁在消费输出(请参阅“[逻辑与布线相分离](#逻辑与布线相分离)”)。
|
||||
|
||||
但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的**中间状态(intermediate state)**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由50或100个MapReduce作业组成的复杂工作流中,存在着很多这样的中间状态【29】。
|
||||
|
||||
@ -564,7 +564,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
与MapReduce不同,这些功能不需要严格扮演交织的Map与Reduce的角色,而是可以以更灵活的方式进行组合。我们称这些函数为**算子(operators)**,数据流引擎提供了几种不同的选项来将一个算子的输出连接到另一个算子的输入:
|
||||
|
||||
- 一种选项是对记录按键重新分区并排序,就像在MapReduce的混洗阶段一样(参阅“[分布式执行MapReduce](#分布式执行MapReduce)”)。这种功能可以用于实现排序合并连接和分组,就像在MapReduce中一样。
|
||||
- 一种选项是对记录按键重新分区并排序,就像在MapReduce的混洗阶段一样(请参阅“[分布式执行MapReduce](#分布式执行MapReduce)”)。这种功能可以用于实现排序合并连接和分组,就像在MapReduce中一样。
|
||||
- 另一种可能是接受多个输入,并以相同的方式进行分区,但跳过排序。当记录的分区重要但顺序无关紧要时,这省去了分区散列连接的工作,因为构建散列表还是会把顺序随机打乱。
|
||||
- 对于广播散列连接,可以将一个算子的输出,发送到连接算子的所有分区。
|
||||
|
||||
@ -605,11 +605,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
### 图与迭代处理
|
||||
|
||||
在“[图数据模型](ch2.md#图数据模型)”中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第2章](ch2.md)的讨论集中在OLTP风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。
|
||||
在“[图数据模型](ch2.md#图数据模型)”中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](ch2.md)的讨论集中在OLTP风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。
|
||||
|
||||
批处理上下文中的图也很有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank 【69】,它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分,用于确定网络搜索引擎呈现结果的顺序。
|
||||
|
||||
> 像Spark,Flink和Tez这样的数据流引擎(参见“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图(DAG)**的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
|
||||
> 像Spark,Flink和Tez这样的数据流引擎(请参阅“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图(DAG)**的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
|
||||
|
||||
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为**传递闭包(transitive closure)**)。
|
||||
|
||||
@ -629,13 +629,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用Reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。
|
||||
|
||||
这与Actor模型有些相似(参阅“[分布式的Actor框架](ch4.md#分布式的Actor框架)”),除了顶点状态和顶点之间的消息具有容错性和持久性,且通信以固定的回合进行:在每次迭代中,框架递送上次迭代中发送的所有消息。Actor通常没有这样的时序保证。
|
||||
这与Actor模型有些相似(请参阅“[分布式的Actor框架](ch4.md#分布式的Actor框架)”),除了顶点状态和顶点之间的消息具有容错性和持久性,且通信以固定的回合进行:在每次迭代中,框架递送上次迭代中发送的所有消息。Actor通常没有这样的时序保证。
|
||||
|
||||
#### 容错
|
||||
|
||||
顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高Pregel作业的性能,因为消息可以成批处理,且等待通信的次数也减少了。唯一的等待是在迭代之间:由于Pregel模型保证所有在一轮迭代中发送的消息都在下轮迭代中送达,所以在下一轮迭代开始前,先前的迭代必须完全完成,而所有的消息必须在网络上完成复制。
|
||||
|
||||
即使底层网络可能丢失、重复或任意延迟消息(参阅“[不可靠的网络](ch8.md#不可靠的网络)”),Pregel的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像MapReduce一样,框架能从故障中透明地恢复,以简化在Pregel上实现算法的编程模型。
|
||||
即使底层网络可能丢失、重复或任意延迟消息(请参阅“[不可靠的网络](ch8.md#不可靠的网络)”),Pregel的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像MapReduce一样,框架能从故障中透明地恢复,以简化在Pregel上实现算法的编程模型。
|
||||
|
||||
这种容错是通过在迭代结束时,定期存档所有顶点的状态来实现的,即将其全部状态写入持久化存储。如果某个节点发生故障并且其内存中的状态丢失,则最简单的解决方法是将整个图计算回滚到上一个存档点,然后重启计算。如果算法是确定性的,且消息记录在日志中,那么也可以选择性地只恢复丢失的分区(就像之前讨论过的数据流引擎)【72】。
|
||||
|
||||
@ -671,9 +671,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
但MapReduce及其数据流后继者在其他方面,与SQL的完全声明式查询模型有很大区别。 MapReduce是围绕着回调函数的概念建立的:对于每条记录或者一组记录,调用一个用户定义的函数(Mapper或Reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作:解析、自然语言分析、图像分析以及运行数值或统计算法等。
|
||||
|
||||
自由运行任意代码,长期以来都是传统MapReduce批处理系统与MPP数据库的区别所在(参见“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
自由运行任意代码,长期以来都是传统MapReduce批处理系统与MPP数据库的区别所在(请参阅“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
|
||||
然而数据流引擎已经发现,支持除连接之外的更多**声明式特性**还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外CPU开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(参阅“[列存储](ch3.md#列存储)”),只从磁盘读取所需的列。 Hive、Spark DataFrames和Impala还使用了向量化执行(参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存友好的内部循环中迭代数据,避免函数调用。Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。
|
||||
然而数据流引擎已经发现,支持除连接之外的更多**声明式特性**还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外CPU开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(请参阅“[列存储](ch3.md#列存储)”),只从磁盘读取所需的列。 Hive、Spark DataFrames和Impala还使用了向量化执行(请参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存友好的内部循环中迭代数据,避免函数调用。Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。
|
||||
|
||||
通过在高级API中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像MPP数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。
|
||||
|
||||
|
92
ch11.md
92
ch11.md
@ -10,9 +10,9 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第10章](ch10.md)中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是**衍生数据(derived data)**的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。
|
||||
在[第十章](ch10.md)中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是**衍生数据(derived data)**的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。
|
||||
|
||||
然而,在[第10章](ch10.md)中仍然有一个很大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成输入的读取。例如,MapReduce核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的。
|
||||
然而,在[第十章](ch10.md)中仍然有一个很大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成输入的读取。例如,MapReduce核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的。
|
||||
|
||||
实际上,很多数据是**无界限**的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产生了数据,明天他们将继续产生更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。
|
||||
|
||||
@ -27,11 +27,11 @@
|
||||
|
||||
在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。流处理领域中的等价物看上去是什么样子的?
|
||||
|
||||
当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件(event)** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(参见“[单调钟与日历时钟](ch8.md#单调钟与日历时钟)”)。
|
||||
当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件(event)** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(请参阅“[单调钟与日历时钟](ch8.md#单调钟与日历时钟)”)。
|
||||
|
||||
例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或CPU利用率的周期性测量。在“[使用Unix工具的批处理](ch10.md#使用Unix工具的批处理)”的示例中,Web服务器日志的每一行都是一个事件。
|
||||
|
||||
事件可能被编码为文本字符串或JSON,或者某种二进制编码,如[第4章](ch4.md)所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。
|
||||
事件可能被编码为文本字符串或JSON,或者某种二进制编码,如[第四章](ch4.md)所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。
|
||||
|
||||
在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由 **生产者(producer)** (也称为 **发布者(publisher)** 或 **发送者(sender)** )生成一次,然后可能由多个 **消费者(consumer)** ( **订阅者(subscribers)** 或 **接收者(recipients)** )进行处理【3】。在文件系统中,文件名标识一组相关记录;在流式系统中,相关的事件通常被聚合为一个 **主题(topic)** 或 **流(stream)** 。
|
||||
|
||||
@ -50,15 +50,15 @@
|
||||
|
||||
在这个**发布/订阅**模式中,不同的系统采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助:
|
||||
|
||||
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?**一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用**背压(backpressure)**(也称为**流量控制(flow control)**;即阻塞生产者,以免其发送更多的消息)。例如Unix管道和TCP就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(参见“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。
|
||||
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?**一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用**背压(backpressure)**(也称为**流量控制(flow control)**;即阻塞生产者,以免其发送更多的消息)。例如Unix管道和TCP就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(请参阅“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。
|
||||
|
||||
如果消息被缓存在队列中,那么理解队列增长会发生什么是很重要的。当队列装不进内存时系统会崩溃吗?还是将消息写入磁盘?如果是这样,磁盘访问又会如何影响消息传递系统的性能【6】?
|
||||
|
||||
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?**与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(参阅“[复制和持久性](ch7.md#复制和持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
|
||||
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?**与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅“[复制和持久性](ch7.md#复制和持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
|
||||
|
||||
是否可以接受消息丢失取决于应用。例如,对于周期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新的值会在短时间内发出。但要注意,如果大量的消息被丢弃,可能无法立刻意识到指标已经不正确了【7】。如果你正在对事件计数,那么它们能够可靠送达是更重要的,因为每个丢失的消息都意味着使计数器的错误扩大。
|
||||
|
||||
我们在[第10章](ch10.md)中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。
|
||||
我们在[第十章](ch10.md)中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。
|
||||
|
||||
#### 直接从生产者传递给消费者
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
* UDP组播广泛应用于金融行业,例如股票市场,其中低时延非常重要【8】。虽然UDP本身是不可靠的,但应用层的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便能按需重新发送数据包)。
|
||||
* 无代理的消息库,如ZeroMQ 【9】和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。
|
||||
* StatsD 【10】和Brubeck 【7】使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,只有接收到所有消息,才认为计数器指标是正确的;使用UDP将使得指标处在一种最佳近似状态【11】。另请参阅“[TCP与UDP](ch8.md#TCP与UDP)”
|
||||
* 如果消费者在网络上公开了服务,生产者可以直接发送HTTP或RPC请求(参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”)将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。
|
||||
* 如果消费者在网络上公开了服务,生产者可以直接发送HTTP或RPC请求(请参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”)将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。
|
||||
|
||||
尽管这些直接消息传递系统在设计它们的环境中运行良好,但是它们通常要求应用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
#### 消息代理与数据库的对比
|
||||
|
||||
有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异:
|
||||
有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异:
|
||||
|
||||
* 数据库通常保留数据直至显式删除,而大多数消息代理在消息成功递送给消费者时会自动删除消息。这样的消息代理不适合长期的数据存储。
|
||||
* 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小—— 即队列很短。如果代理需要缓冲很多消息,比如因为消费者速度较慢(如果内存装不下消息,可能会溢出到磁盘),每个消息需要更长的处理时间,整体吞吐量可能会恶化【6】。
|
||||
@ -130,7 +130,7 @@
|
||||
|
||||
数据库和文件系统采用截然相反的方法论:至少在某人显式删除前,通常写入数据库或文件的所有内容都要被永久记录下来。
|
||||
|
||||
这种思维方式上的差异对创建衍生数据的方式有巨大影响。如[第10章](ch10.md)所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。
|
||||
这种思维方式上的差异对创建衍生数据的方式有巨大影响。如[第十章](ch10.md)所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。
|
||||
|
||||
如果你将新的消费者添加到消息传递系统,通常只能接收到消费者注册之后开始发送的消息。先前的任何消息都随风而逝,一去不复返。作为对比,你可以随时为文件和数据库添加新的客户端,且能读取任意久远的数据(只要应用没有显式覆盖或删除这些数据)。
|
||||
|
||||
@ -138,11 +138,11 @@
|
||||
|
||||
#### 使用日志进行消息存储
|
||||
|
||||
日志只是磁盘上简单的仅追加记录序列。我们先前在[第3章](ch3.md)中日志结构存储引擎和预写式日志的上下文中讨论了日志,在[第5章](ch5.md)复制的上下文里也讨论了它。
|
||||
日志只是磁盘上简单的仅追加记录序列。我们先前在[第三章](ch3.md)中日志结构存储引擎和预写式日志的上下文中讨论了日志,在[第五章](ch5.md)复制的上下文里也讨论了它。
|
||||
|
||||
同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。 Unix工具`tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。
|
||||
|
||||
为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行**分区**(按[第6章](ch6.md)的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如[图11-3](img/fig11-3.png)所示。
|
||||
为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行**分区**(按[第六章](ch6.md)的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如[图11-3](img/fig11-3.png)所示。
|
||||
|
||||
在每个分区内,代理为每个消息分配一个单调递增的序列号或**偏移量(offset)**(在[图11-3](img/fig11-3.png)中,框中的数字是消息偏移量)。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。
|
||||
|
||||
@ -152,7 +152,7 @@
|
||||
|
||||
Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的DistributedLog 【20,21】都是基于日志的消息代理。 Google Cloud Pub/Sub在架构上类似,但对外暴露的是JMS风格的API,而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。
|
||||
|
||||
#### 日志与传统消息相比
|
||||
#### 日志与传统的消息传递相比
|
||||
|
||||
基于日志的方法天然支持扇出式消息传递,因为多个消费者可以独立读取日志,而不会相互影响 —— 读取消息不会将其从日志中删除。为了在一组消费者之间实现负载平衡,代理可以将整个分区分配给消费者组中的节点,而不是将单条消息分配给消费者客户端。
|
||||
|
||||
@ -209,7 +209,7 @@
|
||||
|
||||
我们之前曾经说过,事件是某个时刻发生的事情的记录。发生的事情可能是用户操作(例如键入搜索查询)或读取传感器,但也可能是**写入数据库**。某些东西被写入数据库的事实是可以被捕获、存储和处理的事件。这一观察结果表明,数据库和数据流之间的联系不仅仅是磁盘日志的物理存储 —— 而是更深层的联系。
|
||||
|
||||
事实上,复制日志(参阅“[复制日志的实现](ch5.md#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。
|
||||
事实上,复制日志(请参阅“[复制日志的实现](ch5.md#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。
|
||||
|
||||
我们还在“[全序广播](ch9.md#全序广播)”中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态 (假设事件处理是一个确定性的操作)。这是事件流的又一种场景!
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
正如我们在本书中所看到的,没有一个系统能够满足所有的数据存储、查询和处理需求。在实践中,大多数重要应用都需要组合使用几种不同的技术来满足所有的需求:例如,使用OLTP数据库来为用户请求提供服务,使用缓存来加速常见请求,使用全文索引来处理搜索查询,使用数据仓库用于分析。每一种技术都有自己的数据副本,并根据自己的目的进行存储方式的优化。
|
||||
|
||||
由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由ETL进程执行(参见“[数据仓库](ch3.md#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在“[批处理工作流的输出](ch10.md#批处理工作流的输出)”中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。
|
||||
由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由ETL进程执行(请参阅“[数据仓库](ch3.md#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在“[批处理工作流的输出](ch10.md#批处理工作流的输出)”中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。
|
||||
|
||||
如果周期性的完整数据库转储过于缓慢,有时会使用的替代方法是**双写(dual write)**,其中应用代码在数据变更时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存项失效(甚至同时执行这些写入)。
|
||||
|
||||
@ -231,9 +231,9 @@
|
||||
|
||||
除非有一些额外的并发检测机制,例如我们在“[检测并发写入](ch5.md#检测并发写入)”中讨论的版本向量,否则你甚至不会意识到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。
|
||||
|
||||
双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(参阅“[原子提交与两阶段提交(2PC)](ch7.md#原子提交与两阶段提交(2PC))”)。
|
||||
双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(请参阅“[原子提交与两阶段提交(2PC)](ch7.md#原子提交与两阶段提交(2PC))”)。
|
||||
|
||||
如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在[图11-4](img/fig11-4.png)中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(参见“[多主复制](ch5.md#多主复制)“)。
|
||||
如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在[图11-4](img/fig11-4.png)中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(请参阅“[多主复制](ch5.md#多主复制)“)。
|
||||
|
||||
如果实际上只有一个领导者 —— 例如,数据库 —— 而且我们能让搜索索引成为数据库的追随者,情况要好得多。但这在实践中可能吗?
|
||||
|
||||
@ -257,11 +257,11 @@
|
||||
|
||||
从本质上说,变更数据捕获使得一个数据库成为领导者(被捕获变化的数据库),并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更事件,因为它保留了消息的顺序(避免了[图11-2](img/fig11-2.png)的重新排序问题)。
|
||||
|
||||
数据库触发器可用来实现变更数据捕获(参阅“[基于触发器的复制](ch5.md#基于触发器的复制)”),通过注册观察所有变更的触发器,并将相应的变更项写入变更日志表中。但是它们往往是脆弱的,而且有显著的性能开销。解析复制日志可能是一种更稳健的方法,但它也很有挑战,例如如何应对模式变更。
|
||||
数据库触发器可用来实现变更数据捕获(请参阅“[基于触发器的复制](ch5.md#基于触发器的复制)”),通过注册观察所有变更的触发器,并将相应的变更项写入变更日志表中。但是它们往往是脆弱的,而且有显著的性能开销。解析复制日志可能是一种更稳健的方法,但它也很有挑战,例如如何应对模式变更。
|
||||
|
||||
LinkedIn的Databus【25】,Facebook的Wormhole【26】和Yahoo!的Sherpa【27】大规模地应用这个思路。 Bottled Water使用解码WAL的API实现了PostgreSQL的CDC【28】,Maxwell和Debezium通过解析binlog对MySQL做了类似的事情【29,30,31】,Mongoriver读取MongoDB oplog【32,33】,而GoldenGate为Oracle提供类似的功能【34,35】。
|
||||
|
||||
像消息代理一样,变更数据捕获通常是异步的:记录数据库系统不会等待消费者应用变更再进行提交。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。不过,所有复制延迟可能有的问题在这里都可能出现(参见“[复制延迟问题](ch5.md#复制延迟问题)”)。
|
||||
像消息代理一样,变更数据捕获通常是异步的:记录数据库系统不会等待消费者应用变更再进行提交。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。不过,所有复制延迟可能有的问题在这里都可能出现(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。
|
||||
|
||||
#### 初始快照
|
||||
|
||||
@ -275,7 +275,7 @@
|
||||
|
||||
如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但**日志压缩(log compaction)** 提供了一个很好的备选方案。
|
||||
|
||||
我们之前在“[哈希索引](ch3.md#哈希索引)”中关于日志结构存储引擎的上下文中讨论了日志压缩(参见[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。
|
||||
我们之前在“[哈希索引](ch3.md#哈希索引)”中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。
|
||||
|
||||
在日志结构存储引擎中,具有特殊值NULL(**墓碑(tombstone)**)的更新表示该键被删除,并会在日志压缩过程中被移除。但只要键不被覆盖或删除,它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入,则先前的值将最终将被垃圾回收,只有最新的值会保留下来。
|
||||
|
||||
@ -306,7 +306,7 @@
|
||||
|
||||
例如,存储“学生取消选课”事件以中性的方式清楚地表达了单个行为的意图,而其副作用“从登记表中删除了一个条目,而一条取消原因的记录被添加到学生反馈表“则嵌入了很多有关稍后对数据的使用方式的假设。如果引入一个新的应用功能,例如“将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地从现有事件中脱开。
|
||||
|
||||
事件溯源类似于**编年史(chronicle)**数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
事件溯源类似于**编年史(chronicle)**数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
|
||||
诸如Event Store【46】这样的专业数据库已经被开发出来,供使用事件溯源的应用使用,但总的来说,这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。
|
||||
|
||||
@ -337,7 +337,7 @@
|
||||
|
||||
### 状态、流和不变性
|
||||
|
||||
我们在[第10章](ch10.md)中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
|
||||
我们在[第十章](ch10.md)中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
|
||||
|
||||
我们通常将数据库视为应用程序当前状态的存储 —— 这种表示针对读取进行了优化,而且通常对于服务查询而言是最为方便的表示。状态的本质是,它会变化,所以数据库才会支持数据的增删改。这又该如何匹配不变性呢?
|
||||
|
||||
@ -372,13 +372,13 @@ $$
|
||||
|
||||
#### 从同一事件日志中派生多个视图
|
||||
|
||||
此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样([图11-5](img/fig11-5.png)):例如,分析型数据库Druid使用这种方式直接从Kafka摄取数据【55】,Pistachio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect能将来自Kafka的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统(如搜索服务器)来说是很有意义的,当系统要从分布式日志中获取输入时亦然(参阅“[保持系统同步](#保持系统同步)”)。
|
||||
此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样([图11-5](img/fig11-5.png)):例如,分析型数据库Druid使用这种方式直接从Kafka摄取数据【55】,Pistachio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect能将来自Kafka的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统(如搜索服务器)来说是很有意义的,当系统要从分布式日志中获取输入时亦然(请参阅“[保持系统同步](#保持系统同步)”)。
|
||||
|
||||
添加从事件日志到数据库的显式转换,能够使应用更容易地随时间演进:如果你想要引入一个新功能,以新的方式表示现有数据,则可以使用事件日志来构建一个单独的、针对新功能的读取优化视图,无需修改现有系统而与之共存。并行运行新旧系统通常比在现有系统中执行复杂的模式迁移更容易。一旦不再需要旧的系统,你可以简单地关闭它并回收其资源【47,57】。
|
||||
|
||||
如果你不需要担心如何查询与访问数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性,都是希望支持某些特定查询和访问模式的结果(参见[第3章](ch3.md))。出于这个原因,通过将数据写入的形式与读取形式相分离,并允许几个不同的读取视图,你能获得很大的灵活性。这个想法有时被称为**命令查询责任分离(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
如果你不需要担心如何查询与访问数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性,都是希望支持某些特定查询和访问模式的结果(请参阅[第三章](ch3.md))。出于这个原因,通过将数据写入的形式与读取形式相分离,并允许几个不同的读取视图,你能获得很大的灵活性。这个想法有时被称为**命令查询责任分离(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
|
||||
数据库和模式设计的传统方法是基于这样一种谬论,数据必须以与查询相同的形式写入。如果可以将数据从针对写入优化的事件日志转换为针对读取优化的应用状态,那么有关规范化和非规范化的争论就变得无关紧要了(参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”):在针对读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程提供了使其与事件日志保持一致的机制。
|
||||
数据库和模式设计的传统方法是基于这样一种谬论,数据必须以与查询相同的形式写入。如果可以将数据从针对写入优化的事件日志转换为针对读取优化的应用状态,那么有关规范化和非规范化的争论就变得无关紧要了(请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”):在针对读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程提供了使其与事件日志保持一致的机制。
|
||||
|
||||
在“[描述负载](ch1.md#描述负载)”中,我们讨论了推特主页时间线,它是特定用户关注的人群所发推特的缓存(类似邮箱)。这是**针对读取优化的状态**的又一个例子:主页时间线是高度非规范化的,因为你的推文与你所有粉丝的时间线都构成了重复。然而,扇出服务保持了这种重复状态与新推特以及新关注关系的同步,从而保证了重复的可管理性。
|
||||
|
||||
@ -388,13 +388,13 @@ $$
|
||||
|
||||
一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要**事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中讨论的方法。
|
||||
|
||||
另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。
|
||||
另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。
|
||||
|
||||
如果事件日志与应用状态以相同的方式分区(例如,处理分区3中的客户事件只需要更新分区3中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(参阅“[真的串行执行](ch7.md#真的串行执行)”)。日志通过在分区中定义事件的序列顺序,消除了并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在[第12章](ch12.md)讨论。
|
||||
如果事件日志与应用状态以相同的方式分区(例如,处理分区3中的客户事件只需要更新分区3中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(请参阅“[真的串行执行](ch7.md#真的串行执行)”)。日志通过在分区中定义事件的序列顺序,消除了并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在[第十二章](ch12.md)讨论。
|
||||
|
||||
#### 不变性的限制
|
||||
|
||||
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(参见“[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
|
||||
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅“[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
|
||||
|
||||
永远保持所有变更的不变历史,在多大程度上是可行的?答案取决于数据集的流失率。一些工作负载主要是添加数据,很少更新或删除;它们很容易保持不变。其他工作负载在相对较小的数据集上有较高的更新/删除率;在这些情况下,不可变的历史可能增至难以接受的巨大,碎片化可能成为一个问题,压缩与垃圾收集的表现对于运维的稳健性变得至关重要【60,61】。
|
||||
|
||||
@ -416,9 +416,9 @@ $$
|
||||
2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。
|
||||
3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项1或2)。
|
||||
|
||||
在本章的剩余部分中,我们将讨论选项3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为**算子(operator)**或**作业(job)**。它与我们在[第10章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。
|
||||
在本章的剩余部分中,我们将讨论选项3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为**算子(operator)**或**作业(job)**。它与我们在[第十章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。
|
||||
|
||||
流处理中的分区和并行化模式也非常类似于[第10章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不再重复这些主题。基本的Map操作(如转换和过滤记录)也是一样的。
|
||||
流处理中的分区和并行化模式也非常类似于[第十章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不再重复这些主题。基本的Map操作(如转换和过滤记录)也是一样的。
|
||||
|
||||
与批量作业相比的一个关键区别是,流不会结束。这种差异会带来很多隐含的结果。正如本章开始部分所讨论的,排序对无界数据集没有意义,因此无法使用**排序合并连接**(请参阅“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重启失败任务,但是对于已经运行数年的流作业,重启后从头开始跑可能并不是一个可行的选项。
|
||||
|
||||
@ -459,7 +459,7 @@ $$
|
||||
|
||||
#### 维护物化视图
|
||||
|
||||
我们在“[数据库与流](#数据库与流)”中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护**物化视图(materialized view)** 的一种具体场景(参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。
|
||||
我们在“[数据库与流](#数据库与流)”中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护**物化视图(materialized view)** 的一种具体场景(请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。
|
||||
|
||||
同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的**所有**事件,除了那些可能由日志压缩丢弃的过时事件(请参阅“[日志压缩](#日志压缩)“)。实际上,你需要一个可以一直延伸到时间开端的窗口。
|
||||
|
||||
@ -481,7 +481,7 @@ $$
|
||||
* Actor之间的交流往往是短暂的、一对一的;而事件日志则是持久的、多订阅者的。
|
||||
* Actor可以以任意方式进行通信(包括循环的请求/响应模式),但流处理通常配置在无环流水线中,其中每个流都是一个特定作业的输出,由良好定义的输入流中派生而来。
|
||||
|
||||
也就是说,RPC类系统与流处理之间有一些交叉领域。例如,Apache Storm有一个称为**分布式RPC**的功能,它允许将用户查询分散到一系列也处理事件流的节点上;然后这些查询与来自输入流的事件交织,而结果可以被汇总并发回给用户【78】(另参阅“[多分区数据处理](ch12.md#多分区数据处理)”)。
|
||||
也就是说,RPC类系统与流处理之间有一些交叉领域。例如,Apache Storm有一个称为**分布式RPC**的功能,它允许将用户查询分散到一系列也处理事件流的节点上;然后这些查询与来自输入流的事件交织,而结果可以被汇总并发回给用户【78】(另请参阅“[多分区数据处理](ch12.md#多分区数据处理)”)。
|
||||
|
||||
也可以使用Actor框架来处理流。但是,很多这样的框架在崩溃时不能保证消息的传递,除非你实现了额外的重试逻辑,否则这种处理不是容错的。
|
||||
|
||||
@ -491,13 +491,13 @@ $$
|
||||
|
||||
在批处理中过程中,大量的历史事件被快速地处理。如果需要按时间来分析,批处理器需要检查每个事件中嵌入的时间戳。读取运行批处理机器的系统时钟没有任何意义,因为处理运行的时间与事件实际发生的时间无关。
|
||||
|
||||
批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是**确定性**的:在相同的输入上再次运行相同的处理过程会得到相同的结果(参阅“[容错](ch10.md#容错)”)。
|
||||
批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是**确定性**的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“[容错](ch10.md#容错)”)。
|
||||
|
||||
另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间(processing time)**)来确定**窗口(windowing)**【79】。这种方法的优点是简单,如果事件创建与事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。
|
||||
|
||||
#### 事件时间与处理时间
|
||||
|
||||
很多原因都可能导致处理延迟:排队,网络故障(参阅“[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理/消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(参阅“[重播旧消息](#重播旧消息)”),或者在修复代码BUG之后。
|
||||
很多原因都可能导致处理延迟:排队,网络故障(请参阅“[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理/消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅“[重播旧消息](#重播旧消息)”),或者在修复代码BUG之后。
|
||||
|
||||
而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个Web请求(由Web服务器A处理),然后发出第二个请求(由服务器B处理)。 A和B发出描述它们所处理请求的事件,但是B的事件在A的事件发生之前到达消息代理。现在,流处理器将首先看到B事件,然后看到A事件,即使它们实际上是以相反的顺序发生的。
|
||||
|
||||
@ -528,7 +528,7 @@ $$
|
||||
|
||||
当事件可能在系统内多个地方进行缓冲时,为事件分配时间戳更加困难了。例如,考虑一个移动应用向服务器上报关于用量的事件。该应用可能会在设备处于脱机状态时被使用,在这种情况下,它将在设备本地缓冲事件,并在下一次互联网连接可用时向服务器上报这些事件(可能是几小时甚至几天)。对于这个流的任意消费者而言,它们就如延迟极大的滞留事件一样。
|
||||
|
||||
在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(参见“[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。
|
||||
在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(请参阅“[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。
|
||||
|
||||
要校正不正确的设备时钟,一种方法是记录三个时间戳【82】:
|
||||
|
||||
@ -558,11 +558,11 @@ $$
|
||||
|
||||
***会话窗口(Session window)***
|
||||
|
||||
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(参阅“[分组](ch10.md#分组)”)。
|
||||
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅“[分组](ch10.md#分组)”)。
|
||||
|
||||
### 流式连接
|
||||
### 流连接
|
||||
|
||||
在[第10章](ch10.md)中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。
|
||||
在[第十章](ch10.md)中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。
|
||||
|
||||
然而,新事件随时可能出现在一个流中,这使得流连接要比批处理连接更具挑战性。为了更好地理解情况,让我们先来区分三种不同类型的连接:**流-流**连接,**流-表**连接,与**表-表**连接【84】。我们将在下面的章节中通过例子来说明。
|
||||
|
||||
@ -631,7 +631,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
### 容错
|
||||
|
||||
在本章的最后一节中,让我们看一看流处理是如何容错的。我们在[第10章](ch10.md)中看到,批处理框架可以很容易地容错:如果MapReduce作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的独立文件中,而输出仅当任务成功完成后可见。
|
||||
在本章的最后一节中,让我们看一看流处理是如何容错的。我们在[第十章](ch10.md)中看到,批处理框架可以很容易地容错:如果MapReduce作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的独立文件中,而输出仅当任务成功完成后可见。
|
||||
|
||||
特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为**恰好一次语义(exactly-once semantics)**,尽管**有效一次(effectively-once)** 可能会是一个更写实的术语【90】。
|
||||
|
||||
@ -651,9 +651,9 @@ GROUP BY follows.follower_id
|
||||
|
||||
为了在出现故障时表现出恰好处理一次的样子,我们需要确保事件处理的所有输出和副作用**当且仅当**处理成功时才会生效。这些影响包括发送给下游算子或外部消息传递系统(包括电子邮件或推送通知)的任何消息,任何数据库写入,对算子状态的任何变更,以及对输入消息的任何确认(包括在基于日志的消息代理中将消费者偏移量前移)。
|
||||
|
||||
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(参阅“[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。
|
||||
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅“[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。
|
||||
|
||||
在[第9章](ch9.md)中,我们讨论了分布式事务传统实现中的问题(如XA)。然而在限制更为严苛的环境中,也是有可能高效实现这种原子提交机制的。 Google Cloud Dataflow【81,92】和VoltDB 【94】中使用了这种方法,Apache Kafka有计划加入类似的功能【95,96】。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
|
||||
在[第九章](ch9.md)中,我们讨论了分布式事务传统实现中的问题(如XA)。然而在限制更为严苛的环境中,也是有可能高效实现这种原子提交机制的。 Google Cloud Dataflow【81,92】和VoltDB 【94】中使用了这种方法,Apache Kafka有计划加入类似的功能【95,96】。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
|
||||
|
||||
#### 幂等性
|
||||
|
||||
@ -665,7 +665,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意味着隐含了一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(基于日志的消息代理能做这些事),处理必须是确定性的,没有其他节点能同时更新相同的值【98,99】。
|
||||
|
||||
当从一个处理节点故障切换到另一个节点时,可能需要进行**防护(fencing)**(参阅“[领导者和锁](ch8.md#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现**恰好一次语义**的有效方式,仅需很小的额外开销。
|
||||
当从一个处理节点故障切换到另一个节点时,可能需要进行**防护(fencing)**(请参阅“[领导者和锁](ch8.md#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现**恰好一次语义**的有效方式,仅需很小的额外开销。
|
||||
|
||||
#### 失败后重建状态
|
||||
|
||||
@ -673,9 +673,9 @@ GROUP BY follows.follower_id
|
||||
|
||||
一种选择是将状态保存在远程数据存储中,并进行复制,然而正如在“[流表连接(流扩充)](#流表连接(流扩充))”中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。
|
||||
|
||||
例如,Flink定期捕获算子状态的快照,并将它们写入HDFS等持久存储中【92,93】。 Samza和Kafka Streams通过将状态变更发送到具有日志压缩功能的专用Kafka主题来复制状态变更,这与变更数据捕获类似【84,100】。 VoltDB通过在多个节点上对每个输入消息进行冗余处理来复制状态(参阅“[真的串行执行](ch7.md#真的串行执行)”)。
|
||||
例如,Flink定期捕获算子状态的快照,并将它们写入HDFS等持久存储中【92,93】。 Samza和Kafka Streams通过将状态变更发送到具有日志压缩功能的专用Kafka主题来复制状态变更,这与变更数据捕获类似【84,100】。 VoltDB通过在多个节点上对每个输入消息进行冗余处理来复制状态(请参阅“[真的串行执行](ch7.md#真的串行执行)”)。
|
||||
|
||||
在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(参阅“[日志压缩](#日志压缩)”)。
|
||||
在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(请参阅“[日志压缩](#日志压缩)”)。
|
||||
|
||||
然而,所有这些权衡取决于底层基础架构的性能特征:在某些系统中,网络延迟可能低于磁盘访问延迟,网络带宽也可能与磁盘带宽相当。没有针对所有情况的普适理想权衡,随着存储和网络技术的发展,本地状态与远程状态的优点也可能会互换。
|
||||
|
||||
@ -683,7 +683,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
## 本章小结
|
||||
|
||||
在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在[第10章](ch10.md) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。
|
||||
在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在[第十章](ch10.md) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。
|
||||
|
||||
我们花了一些时间比较两种消息代理:
|
||||
|
||||
@ -695,7 +695,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
|
||||
|
||||
基于日志的方法与数据库中的复制日志(参见[第5章](ch5.md))和日志结构存储引擎(请参阅[第3章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。
|
||||
基于日志的方法与数据库中的复制日志(请参阅[第五章](ch5.md))和日志结构存储引擎(请参阅[第三章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。
|
||||
|
||||
就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和Feed数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。
|
||||
|
||||
|
26
ch2.md
26
ch2.md
@ -26,7 +26,7 @@
|
||||
|
||||
掌握一个数据模型需要花费很多精力(想想关系数据建模有多少本书)。即便只使用一个数据模型,不用操心其内部工作机制,构建软件也是非常困难的。然而,因为数据模型对上层软件的功能(能做什么,不能做什么)有着至深的影响,所以选择一个适合的数据模型是非常重要的。
|
||||
|
||||
在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在第3章中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第3点)。
|
||||
在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在[第三章](ch3.md)中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第3点)。
|
||||
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
* 后续的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,并支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。JSON数据类型也得到多个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。
|
||||
* 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解析其结构和内容。这种配置下,通常不能使用数据库来查询该编码列中的值。
|
||||
|
||||
对于一个像简历这样自包含文档的数据结构而言,JSON表示是非常合适的:参见[例2-1]()。JSON比XML更简单。面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。
|
||||
对于一个像简历这样自包含文档的数据结构而言,JSON表示是非常合适的:请参阅[例2-1]()。JSON比XML更简单。面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。
|
||||
|
||||
**例2-1. 用JSON文档表示一个LinkedIn简介**
|
||||
|
||||
@ -117,7 +117,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
有一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。
|
||||
有一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在[第四章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。
|
||||
|
||||
JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部性(locality)**。如果在前面的关系型示例中获取简介,那需要执行多个查询(通过`user_id`查询每个表),或者在User表与其下属表之间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。
|
||||
|
||||
@ -157,7 +157,7 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
|
||||
|
||||
***组织和学校作为实体***
|
||||
|
||||
在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(参见[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。
|
||||
在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(请参阅[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。
|
||||
|
||||
***推荐***
|
||||
|
||||
@ -220,7 +220,7 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
|
||||
|
||||
### 关系型数据库与文档数据库在今日的对比
|
||||
|
||||
将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。本章将只关注数据模型中的差异。
|
||||
将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(请参阅[第五章](ch5.md))和处理并发性(请参阅[第七章](ch7.md))。本章将只关注数据模型中的差异。
|
||||
|
||||
支持文档数据模型的主要论据是架构灵活性,因局部性而拥有更好的性能,以及对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。
|
||||
|
||||
@ -234,7 +234,7 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
|
||||
|
||||
但如果你的应用程序确实会用到多对多关系,那么文档模型就没有那么诱人了。尽管可以通过反规范化来消除对连接的需求,但这需要应用程序代码来做额外的工作以确保数据一致性。尽管应用程序代码可以通过向数据库发出多个请求的方式来模拟连接,但这也将复杂性转移到应用程序中,而且通常也会比由数据库内的专用代码更慢。在这种情况下,使用文档模型可能会导致更复杂的应用代码与更差的性能【15】。
|
||||
|
||||
我们没有办法说哪种数据模型更有助于简化应用代码,因为它取决于数据项之间的关系种类。对高度关联的数据而言,文档模型是极其糟糕的,关系模型是可以接受的,而选用图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。
|
||||
我们没有办法说哪种数据模型更有助于简化应用代码,因为它取决于数据项之间的关系种类。对高度关联的数据而言,文档模型是极其糟糕的,关系模型是可以接受的,而选用图形模型(请参阅“[图数据模型](#图数据模型)”)是最自然的。
|
||||
|
||||
#### 文档模型中的模式灵活性
|
||||
|
||||
@ -280,7 +280,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
|
||||
|
||||
值得指出的是,为了局部性而分组集合相关数据的想法并不局限于文档模型。例如,Google的Spanner数据库在关系数据模型中提供了同样的局部性属性,允许模式声明一个表的行应该交错(嵌套)在父表内【27】。Oracle类似地允许使用一个称为 **多表索引集群表(multi-table index cluster tables)** 的类似特性【28】。Bigtable数据模型(用于Cassandra和HBase)中的 **列族(column-family)** 概念与管理局部性的目的类似【29】。
|
||||
|
||||
在[第3章](ch3.md)将还会看到更多关于局部性的内容。
|
||||
在[第三章](ch3.md)将还会看到更多关于局部性的内容。
|
||||
|
||||
#### 文档和关系数据库的融合
|
||||
|
||||
@ -419,7 +419,7 @@ for (var i = 0; i < liElements.length; i++) {
|
||||
|
||||
MapReduce是一个由Google推广的编程模型,用于在多台机器上批量处理大规模的数据【33】。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
|
||||
|
||||
MapReduce将[第10章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB使用的模型。
|
||||
MapReduce将[第十章](ch10.md)中有更详细的描述。现在我们将简要讨论一下MongoDB使用的模型。
|
||||
|
||||
MapReduce既不是一个声明式的查询语言,也不是一个完全命令式的查询API,而是处于两者之间:查询的逻辑用代码片段来表示,这些代码片段会被处理框架重复性调用。它基于`map`(也称为`collect`)和`reduce`(也称为`fold`或`inject`)函数,两个函数存在于许多函数式编程语言中。
|
||||
|
||||
@ -487,7 +487,7 @@ db.observations.mapReduce(function map() {
|
||||
|
||||
map和reduce函数在功能上有所限制:它们必须是**纯**函数,这意味着它们只使用传递给它们的数据作为输入,它们不能执行额外的数据库查询,也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能,并在失败时重新运行它们。然而,map和reduce函数仍然是强大的:它们可以解析字符串,调用库函数,执行计算等等。
|
||||
|
||||
MapReduce是一个相当底层的编程模型,用于计算机集群上的分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现(见[第10章](ch10.md)),但是也有很多不使用MapReduce的分布式SQL实现。请注意,SQL中没有任何内容限制它在单个机器上运行,而MapReduce在分布式查询执行上没有垄断权。
|
||||
MapReduce是一个相当底层的编程模型,用于计算机集群上的分布式执行。像SQL这样的更高级的查询语言可以用一系列的MapReduce操作来实现(见[第十章](ch10.md)),但是也有很多不使用MapReduce的分布式SQL实现。请注意,SQL中没有任何内容限制它在单个机器上运行,而MapReduce在分布式查询执行上没有垄断权。
|
||||
|
||||
能够在查询中使用JavaScript代码是高级查询的一个重要特性,但这不限于MapReduce,一些SQL数据库也可以用JavaScript函数进行扩展【34】。
|
||||
|
||||
@ -539,7 +539,7 @@ db.observations.aggregate([
|
||||
|
||||
**图2-5 图数据结构示例(框代表顶点,箭头代表边)**
|
||||
|
||||
有几种不同但相关的方法用来构建和查询图表中的数据。在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三元组存储(triple-store)模型(由Datomic,AllegroGraph等实现)。我们将查看图的三种声明式查询语言:Cypher,SPARQL和Datalog。除此之外,还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架(见[第10章](ch10.md))。
|
||||
有几种不同但相关的方法用来构建和查询图表中的数据。在本节中,我们将讨论属性图模型(由Neo4j,Titan和InfiniteGraph实现)和三元组存储(triple-store)模型(由Datomic,AllegroGraph等实现)。我们将查看图的三种声明式查询语言:Cypher,SPARQL和Datalog。除此之外,还有像Gremlin 【36】这样的图形查询语言和像Pregel这样的图形处理框架(见[第十章](ch10.md))。
|
||||
|
||||
### 属性图
|
||||
|
||||
@ -730,7 +730,7 @@ _:namerica :type :"continent"
|
||||
|
||||
在这个例子中,图的顶点被写为:`_:someName`。这个名字并不意味着这个文件以外的任何东西。它的存在只是帮助我们明确哪些三元组引用了同一顶点。当谓语表示边时,该宾语是一个顶点,如`_:idaho :within _:usa.`。当谓语是一个属性时,该宾语是一个字符串,如`_:usa :name "United States"`
|
||||
|
||||
一遍又一遍地重复相同的主语看起来相当重复,但幸运的是,可以使用分号来说明关于同一主语的多个事情。这使得Turtle格式相当不错,可读性强:参见[例2-7]()。
|
||||
一遍又一遍地重复相同的主语看起来相当重复,但幸运的是,可以使用分号来说明关于同一主语的多个事情。这使得Turtle格式相当不错,可读性强:请参阅[例2-7]()。
|
||||
|
||||
**例2-7 一种相对例2-6写入数据的更为简洁的方法。**
|
||||
|
||||
@ -756,7 +756,7 @@ _:namerica a :Location; :name "North America"; :type "continent".
|
||||
|
||||
#### RDF数据模型
|
||||
|
||||
[例2-7]()中使用的Turtle语言是一种用于RDF数据的人可读格式。有时候,RDF也可以以XML格式编写,不过完成同样的事情会相对啰嗦,参见[例2-8]()。Turtle/N3是更可取的,因为它更容易阅读,像Apache Jena 【42】这样的工具可以根据需要在不同的RDF格式之间进行自动转换。
|
||||
[例2-7]()中使用的Turtle语言是一种用于RDF数据的人可读格式。有时候,RDF也可以以XML格式编写,不过完成同样的事情会相对啰嗦,请参阅[例2-8]()。Turtle/N3是更可取的,因为它更容易阅读,像Apache Jena 【42】这样的工具可以根据需要在不同的RDF格式之间进行自动转换。
|
||||
|
||||
**例2-8 用RDF/XML语法表示例2-7的数据**
|
||||
|
||||
@ -794,7 +794,7 @@ RDF有一些奇怪之处,因为它是为了在互联网上交换数据而设
|
||||
|
||||
**SPARQL**是一种用于三元组存储的面向RDF数据模型的查询语言,【43】。(它是SPARQL协议和RDF查询语言的缩写,发音为“sparkle”。)SPARQL早于Cypher,并且由于Cypher的模式匹配借鉴于SPARQL,这使得它们看起来非常相似【37】。
|
||||
|
||||
与之前相同的查询 - 查找从美国转移到欧洲的人 - 使用SPARQL比使用Cypher甚至更为简洁(参见[例2-9]())。
|
||||
与之前相同的查询 - 查找从美国转移到欧洲的人 - 使用SPARQL比使用Cypher甚至更为简洁(请参阅[例2-9]())。
|
||||
|
||||
**例2-9 与示例2-4相同的查询,用SPARQL表示**
|
||||
|
||||
|
18
ch3.md
18
ch3.md
@ -13,7 +13,7 @@
|
||||
|
||||
一个数据库在最基础的层次上需要完成两件事情:当你把数据交给数据库时,它应当把数据存储起来;而后当你向数据库要数据时,它应当把数据返回给你。
|
||||
|
||||
在[第2章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员将数据录入数据库的格式,以及再次要回数据的机制。在本章中我们会从数据库的视角来讨论同样的问题:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
|
||||
在[第二章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员将数据录入数据库的格式,以及再次要回数据的机制。在本章中我们会从数据库的视角来讨论同样的问题:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
|
||||
|
||||
作为程序员,为什么要关心数据库内部存储与检索的机理?你可能不会去从头开始实现自己的存储引擎,但是你**确实**需要从许多可用的存储引擎中选择一个合适的。而且为了协调存储引擎以适配应用工作负载,你也需要大致了解存储引擎在底层究竟做什么。
|
||||
|
||||
@ -130,7 +130,7 @@ $ cat database
|
||||
|
||||
乍一看,只有追加日志看起来很浪费:为什么不更新文件,用新值覆盖旧值?但是只能追加设计的原因有几个:
|
||||
|
||||
* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的 **固态硬盘(SSD)** 上也是优选的【4】。我们将在第83页的“[比较B树和LSM树](#比较B树和LSM树)”中进一步讨论这个问题。
|
||||
* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的 **固态硬盘(SSD)** 上也是优选的【4】。我们将在“[比较B树和LSM树](#比较B树和LSM树)”中进一步讨论这个问题。
|
||||
* 如果段文件是附加的或不可变的,并发和崩溃恢复就简单多了。例如,您不必担心在覆盖值时发生崩溃的情况,而将包含旧值和新值的一部分的文件保留在一起。
|
||||
* 合并旧段可以避免数据文件随着时间的推移而分散的问题。
|
||||
|
||||
@ -180,7 +180,7 @@ $ cat database
|
||||
|
||||
到目前为止,但是如何让你的数据首先被按键排序呢?我们的传入写入可以以任何顺序发生。
|
||||
|
||||
在磁盘上维护有序结构是可能的(参阅“[B树](#B树)”),但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红黑树或AVL树【2】。使用这些数据结构,您可以按任何顺序插入键,并按排序顺序读取它们。
|
||||
在磁盘上维护有序结构是可能的(请参阅“[B树](#B树)”),但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红黑树或AVL树【2】。使用这些数据结构,您可以按任何顺序插入键,并按排序顺序读取它们。
|
||||
|
||||
现在我们可以使我们的存储引擎工作如下:
|
||||
|
||||
@ -285,13 +285,13 @@ LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的
|
||||
|
||||
#### LSM树的缺点
|
||||
|
||||
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性【28】。
|
||||
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(请参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性【28】。
|
||||
|
||||
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
|
||||
|
||||
如果写入吞吐量很高,并且压缩没有仔细配置,压缩跟不上写入速率。在这种情况下,磁盘上未合并段的数量不断增加,直到磁盘空间用完,读取速度也会减慢,因为它们需要检查更多段文件。通常情况下,即使压缩无法跟上,基于SSTable的存储引擎也不会限制传入写入的速率,所以您需要进行明确的监控来检测这种情况【29,30】。
|
||||
|
||||
B树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得B树在想要提供强大的事务语义的数据库中很有吸引力:在许多关系数据库中,事务隔离是通过在键范围上使用锁来实现的,在B树索引中,这些锁可以直接连接到树【5】。在[第7章](ch7.md)中,我们将更详细地讨论这一点。
|
||||
B树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得B树在想要提供强大的事务语义的数据库中很有吸引力:在许多关系数据库中,事务隔离是通过在键范围上使用锁来实现的,在B树索引中,这些锁可以直接连接到树【5】。在[第七章](ch7.md)中,我们将更详细地讨论这一点。
|
||||
|
||||
B树在数据库体系结构中是非常根深蒂固的,为许多工作负载提供始终如一的良好性能,所以它们不可能很快就会消失。在新的数据存储中,日志结构化索引变得越来越流行。没有快速和容易的规则来确定哪种类型的存储引擎对你的场景更好,所以值得进行一些经验上的测试。
|
||||
|
||||
@ -299,7 +299,7 @@ B树在数据库体系结构中是非常根深蒂固的,为许多工作负载
|
||||
|
||||
到目前为止,我们只讨论了关键值索引,它们就像关系模型中的**主键(primary key)** 索引。主键唯一标识关系表中的一行,或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键(或ID)引用该行/文档/顶点,并且索引用于解析这样的引用。
|
||||
|
||||
有二级索引也很常见。在关系数据库中,您可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。
|
||||
有二级索引也很常见。在关系数据库中,您可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第二章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。
|
||||
|
||||
一个二级索引可以很容易地从一个键值索引构建。主要的不同是键不是唯一的。即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者通过使索引中的每个值,成为匹配行标识符的列表(如全文索引中的发布列表),或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式,B树和日志结构索引都可以用作辅助索引。
|
||||
|
||||
@ -369,7 +369,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
在早期业务数据处理过程中,一次典型的数据库写入通常与一笔 *商业交易(commercial transaction)* 相对应:卖个货,向供应商下订单,支付员工工资等等。但随着数据库应用至那些不涉及到钱的领域,术语 **交易/事务(transaction)** 仍留了下来,用于指代一组读写操作构成的逻辑单元。
|
||||
|
||||
事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 —— 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性,在[第10章](ch10.md)中讨论批处理。
|
||||
事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 —— 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第七章](ch7.md)中讨论ACID属性,在[第十章](ch10.md)中讨论批处理。
|
||||
|
||||
即使数据库开始被用于许多不同类型的数据,比如博客文章的评论,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理商业交易。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,这种访问模式被称为 **在线事务处理(OLTP, OnLine Transaction Processing)** 。
|
||||
|
||||
@ -421,7 +421,7 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
正如[第2章](ch2.md)所探讨的,根据应用程序的需要,在事务处理领域中使用了大量不同的数据模型。另一方面,在分析中,数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用,被称为星型模式(也称为维度建模【55】)。
|
||||
正如[第二章](ch2.md)所探讨的,根据应用程序的需要,在事务处理领域中使用了大量不同的数据模型。另一方面,在分析中,数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用,被称为星型模式(也称为维度建模【55】)。
|
||||
|
||||
图3-9中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览量或点击量。
|
||||
|
||||
@ -514,7 +514,7 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
加载 `product_sk = 31` 和 `store_sk = 3` 的位图,并逐位计算AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第 k 位对应于与另一列的位图中的第 k 位相同的行。
|
||||
|
||||
对于不同种类的数据,也有各种不同的压缩方案,但我们不会详细讨论它们,参见【58】的概述。
|
||||
对于不同种类的数据,也有各种不同的压缩方案,但我们不会详细讨论它们,请参阅【58】的概述。
|
||||
|
||||
> #### 面向列的存储和列族
|
||||
>
|
||||
|
32
ch4.md
32
ch4.md
@ -11,11 +11,11 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着**功能(feature)** 的增增改改。[第一章](ch1.md)介绍了**可演化性(evolvability)**的概念:应该尽力构建能灵活适应变化的系统(参阅“[可演化性:拥抱变化](ch1.md#可演化性:拥抱变化)”)。
|
||||
应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着**功能(feature)** 的增增改改。[第一章](ch1.md)介绍了**可演化性(evolvability)**的概念:应该尽力构建能灵活适应变化的系统(请参阅“[可演化性:拥抱变化](ch1.md#可演化性:拥抱变化)”)。
|
||||
|
||||
在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。
|
||||
|
||||
我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式(schema-on-read)**(或 **无模式(schemaless)**)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
|
||||
我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式(schema-on-read)**(或 **无模式(schemaless)**)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
|
||||
|
||||
当数据**格式(format)** 或**模式(schema)** 发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
|
||||
|
||||
@ -266,7 +266,7 @@ Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只
|
||||
|
||||
* 有很多记录的大文件
|
||||
|
||||
Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在[第10章](ch10.md)讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次Writer模式。 Avro指定了一个文件格式(对象容器文件)来做到这一点。
|
||||
Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在[第十章](ch10.md)讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次Writer模式。 Avro指定了一个文件格式(对象容器文件)来做到这一点。
|
||||
|
||||
* 支持独立写入的记录的数据库
|
||||
|
||||
@ -274,7 +274,7 @@ Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只
|
||||
|
||||
* 通过网络连接发送记录
|
||||
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(请参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)就是这样工作的。
|
||||
|
||||
具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。
|
||||
|
||||
@ -325,9 +325,9 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式:
|
||||
|
||||
* 通过数据库(参阅“[数据库中的数据流](#数据库中的数据流)”)
|
||||
* 通过服务调用(参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)
|
||||
* 通过异步消息传递(参阅“[消息传递中的数据流](#消息传递中的数据流)”)
|
||||
* 通过数据库(请参阅“[数据库中的数据流](#数据库中的数据流)”)
|
||||
* 通过服务调用(请参阅“[服务中的数据流:REST与RPC](#服务中的数据流:REST与RPC)”)
|
||||
* 通过异步消息传递(请参阅“[消息传递中的数据流](#消息传递中的数据流)”)
|
||||
|
||||
|
||||
|
||||
@ -365,11 +365,11 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
#### 归档存储
|
||||
|
||||
也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
|
||||
也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(请参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
|
||||
|
||||
由于数据转储是一次写入的,而且以后是不可变的,所以Avro对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如Parquet(请参阅“[列压缩](ch3.md#列压缩)”)。
|
||||
|
||||
在[第10章](ch10.md)中,我们将详细讨论使用档案存储中的数据。
|
||||
在[第十章](ch10.md)中,我们将详细讨论使用档案存储中的数据。
|
||||
|
||||
|
||||
|
||||
@ -383,7 +383,7 @@ Web浏览器不是唯一的客户端类型。例如,在移动设备或桌面
|
||||
|
||||
此外,服务器本身可以是另一个服务的客户端(例如,典型的Web应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构(service-oriented architecture,SOA)**,最近被改进和更名为**微服务架构**【31,32】。
|
||||
|
||||
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在[第2章](ch2.md)中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出【33】。这种限制提供了一定程度的封装:服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
|
||||
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在[第二章](ch2.md)中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的API,它只允许由服务的业务逻辑(应用程序代码)预定的输入和输出【33】。这种限制提供了一定程度的封装:服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
|
||||
|
||||
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——这正是我们在本章所一直在谈论的。
|
||||
|
||||
@ -405,7 +405,7 @@ REST不是一个协议,而是一个基于HTTP原则的设计哲学【34,35】
|
||||
|
||||
[^vii]: 尽管首字母缩写词相似,SOAP并不是SOA的要求。 SOAP是一种特殊的技术,而SOA是构建系统的一般方法。
|
||||
|
||||
SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(参阅“[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
|
||||
SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少(请参阅“[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
|
||||
|
||||
由于WSDL的设计不是人类可读的,而且由于SOAP消息通常因为过于复杂而无法手动构建,所以SOAP的用户在很大程度上依赖于工具支持,代码生成和IDE【38】。对于SOAP供应商不支持的编程语言的用户来说,与SOAP服务的集成是困难的。
|
||||
|
||||
@ -420,12 +420,12 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
|
||||
所有这些都是基于 **远程过程调用(RPC)** 的思想,该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同:
|
||||
|
||||
* 本地函数调用是可预测的,并且成功或失败仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第8章](ch8.md)更详细地讨论这个问题。)
|
||||
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( **幂等(idempotence)**)机制。本地函数调用没有这个问题。 (在[第11章](ch11.md)更详细地讨论幂等性)
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第八章](ch8.md)更详细地讨论这个问题。)
|
||||
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( **幂等(idempotence)**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
|
||||
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
|
||||
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
|
||||
|
||||
客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
|
||||
客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(请参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
|
||||
|
||||
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。
|
||||
|
||||
@ -473,11 +473,11 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承:
|
||||
|
||||
#### 消息代理
|
||||
|
||||
过去,**消息代理(Message Broker)**主要是TIBCO,IBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第11章](ch11.md)中对它们进行更详细的比较。
|
||||
过去,**消息代理(Message Broker)**主要是TIBCO,IBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第十一章](ch11.md)中对它们进行更详细的比较。
|
||||
|
||||
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给那个队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。
|
||||
|
||||
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第11章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。
|
||||
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第十一章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)。
|
||||
|
||||
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后和向前兼容的,您可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。
|
||||
|
||||
|
44
ch5.md
44
ch5.md
@ -16,7 +16,7 @@
|
||||
* 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
|
||||
* 伸缩可以接受读请求的机器数量(从而提高读取吞吐量)
|
||||
|
||||
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在[第6章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
||||
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在[第六章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
|
||||
|
||||
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)**和**无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
>
|
||||
> 对于异步复制系统而言,主库故障时有可能丢失数据。这可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。 例如,**链式复制**【8,9】]是同步复制的一种变体,已经在一些系统(如Microsoft Azure存储【10,11】)中成功实现。
|
||||
>
|
||||
> 复制的一致性与**共识(consensus)**(使几个节点就某个值达成一致)之间有着密切的联系,[第9章](ch9.md)将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
|
||||
> 复制的一致性与**共识(consensus)**(使几个节点就某个值达成一致)之间有着密切的联系,[第九章](ch9.md)将详细地探讨这一领域的理论。本章主要讨论实践中数据库常用的简单复制形式。
|
||||
>
|
||||
|
||||
### 设置新从库
|
||||
@ -103,7 +103,7 @@
|
||||
故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障切换过程通常由以下步骤组成:
|
||||
|
||||
1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
|
||||
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第9章](ch9.md)详细讨论。
|
||||
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第九章](ch9.md)详细讨论。
|
||||
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在“[请求路由](ch6.md#请求路由)”中讨论这个问题)。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库。
|
||||
|
||||
故障切换会出现很多大麻烦:
|
||||
@ -112,7 +112,7 @@
|
||||
|
||||
* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在GitHub 【13】的一场事故中,一个过时的MySQL从库被提升为主库。数据库使用自增ID作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用,主键重用使得MySQL和Redis中数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
|
||||
|
||||
* 发生某些故障时(见[第8章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(参见“[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
|
||||
* 发生某些故障时(见[第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂(split brain)**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅“[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点[^ii],但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
|
||||
|
||||
[^ii]: 这种机制称为 **屏蔽(fencing)**,充满感情的术语是:**爆彼之头(Shoot The Other Node In The Head, STONITH)**。
|
||||
|
||||
@ -120,7 +120,7 @@
|
||||
|
||||
这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换。
|
||||
|
||||
节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第8章](ch8.md)和[第9章](ch9.md)将更深入地讨论它们。
|
||||
节点故障、不可靠的网络、对副本一致性,持久性,可用性和延迟的权衡 ,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md)和[第九章](ch9.md)将更深入地讨论它们。
|
||||
|
||||
### 复制日志的实现
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
|
||||
#### 传输预写式日志(WAL)
|
||||
|
||||
在[第3章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
|
||||
在[第三章](ch3.md)中,我们讨论了存储引擎如何在磁盘上表示数据,并且我们发现,通常写操作都是追加到日志中:
|
||||
|
||||
* 对于日志结构存储引擎(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
|
||||
* 对于覆写单个磁盘块的[B树](ch3.md#B树),每次修改都会先写入 **预写式日志(Write Ahead Log, WAL)**,以便崩溃后索引可以恢复到一个一致的状态。
|
||||
@ -169,11 +169,11 @@
|
||||
|
||||
由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。
|
||||
|
||||
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获(change data capture)**,第11章将重新讲到它。
|
||||
对于外部应用程序来说,逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统,这一点很有用,例如复制到数据仓库进行离线分析,或建立自定义索引和缓存【18】。 这种技术被称为 **数据变更捕获(change data capture)**,[第十一章](ch11.md)将重新讲到它。
|
||||
|
||||
#### 基于触发器的复制
|
||||
|
||||
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
|
||||
到目前为止描述的复制方法是由数据库系统实现的,不涉及任何应用程序代码。在很多情况下,这就是你想要的。但在某些情况下需要更多的灵活性。例如,如果您只想复制数据的一个子集,或者想从一种数据库复制到另一种数据库,或者如果您需要冲突解决逻辑(请参阅“[处理写入冲突](#处理写入冲突)”),则可能需要将复制移动到应用程序层。
|
||||
|
||||
一些工具,如Oracle Golden Gate 【19】,可以通过读取数据库日志,使得其他应用程序可以使用数据。另一种方法是使用许多关系数据库自带的功能:触发器和存储过程。
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
* 客户端可以记住最近一次写入的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。
|
||||
|
||||
时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”)。
|
||||
时间戳可以是逻辑时间戳(指示写入顺序的东西,例如日志序列号)或实际系统时钟(在这种情况下,时钟同步变得至关重要;请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”)。
|
||||
|
||||
* 如果您的副本分布在多个数据中心(出于可用性目的与用户尽量在地理上接近),则会增加复杂性。任何需要由领导者提供服务的请求都必须路由到包含主库的数据中心。
|
||||
|
||||
@ -277,7 +277,7 @@
|
||||
|
||||
防止这种异常,需要另一种类型的保证:**一致前缀读(consistent prefix reads)**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
|
||||
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在[第6章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在[第六章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
|
||||
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“[“此前发生”的关系和并发](#“此前发生”的关系和并发)”一节中返回这个主题。
|
||||
|
||||
@ -299,7 +299,7 @@
|
||||
|
||||
基于领导者的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它[^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库, 就无法向数据库写入。
|
||||
|
||||
[^iv]: 如果数据库被分区(见第6章),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
|
||||
[^iv]: 如果数据库被分区(见[第六章](ch6.md)),每个分区都有一个领导。 不同的分区可能在不同的节点上有其领导者,但是每个分区必须有一个领导者节点。
|
||||
|
||||
基于领导者的复制模型的自然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为**多领导者配置**(也称多主、多活复制)。 在这种情况下,每个领导者同时扮演其他领导者的追随者。
|
||||
|
||||
@ -410,7 +410,7 @@
|
||||
|
||||
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突,并将结果写回数据库。例如,CouchDB以这种方式工作。
|
||||
|
||||
请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此,如果您有一个事务会原子性地进行几次不同的写入(请参阅[第7章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
|
||||
请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此,如果您有一个事务会原子性地进行几次不同的写入(请参阅[第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
|
||||
|
||||
|
||||
|
||||
@ -435,7 +435,7 @@
|
||||
|
||||
其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。
|
||||
|
||||
现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在[第7章](ch7.md)中看到更多的冲突示例,在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
|
||||
现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在[第七章](ch7.md)中看到更多的冲突示例,在[第十二章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
|
||||
|
||||
|
||||
|
||||
@ -463,9 +463,9 @@
|
||||
|
||||
在[图5-9](img/fig5-9.png)中,客户端A向主库1的表中插入一行,客户端B在主库3上更新该行。然而,主库2可以以不同的顺序接收写入:它可以首先接收更新(从它的角度来看,是对数据库中不存在的行的更新),并且仅在稍后接收到相应的插入(其应该在更新之前)。
|
||||
|
||||
这是一个因果关系的问题,类似于我们在“[一致前缀读](#一致前缀读)”中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,以便在主库2处正确地排序这些事件(见[第8章](ch8.md))。
|
||||
这是一个因果关系的问题,类似于我们在“[一致前缀读](#一致前缀读)”中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,以便在主库2处正确地排序这些事件(见[第八章](ch8.md))。
|
||||
|
||||
要正确排序这些事件,可以使用一种称为 **版本向量(version vectors)** 的技术,本章稍后将讨论这种技术(参阅“[检测并发写入](#检测并发写入)”)。然而,冲突检测技术在许多多领导者复制系统中执行得不好。例如,在撰写本文时,PostgreSQL BDR不提供写入的因果排序【27】,而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】。
|
||||
要正确排序这些事件,可以使用一种称为 **版本向量(version vectors)** 的技术,本章稍后将讨论这种技术(请参阅“[检测并发写入](#检测并发写入)”)。然而,冲突检测技术在许多多领导者复制系统中执行得不好。例如,在撰写本文时,PostgreSQL BDR不提供写入的因果排序【27】,而Tungsten Replicator for MySQL甚至不尝试检测冲突【34】。
|
||||
|
||||
如果您正在使用具有多领导者复制功能的系统,那么应该了解这些问题,仔细阅读文档,并彻底测试您的数据库,以确保它确实提供了您认为具有的保证。
|
||||
|
||||
@ -483,7 +483,7 @@
|
||||
|
||||
### 当节点故障时写入数据库
|
||||
|
||||
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(参阅「[处理节点宕机](#处理节点宕机)」)。
|
||||
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于主机的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅「[处理节点宕机](#处理节点宕机)」)。
|
||||
|
||||
另一方面,在无领导配置中,故障切换不存在。[图5-10](img/fig5-10.png)显示了发生了什么事情:客户端(用户1234)并行发送写入到所有三个副本,并且两个可用副本接受写入,但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的:在用户1234已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
|
||||
|
||||
@ -493,7 +493,7 @@
|
||||
|
||||
现在想象一下,不可用的节点重新联机,客户端开始读取它。节点关闭时发生的任何写入都从该节点丢失。因此,如果您从该节点读取数据,则可能会将陈旧(过时)值视为响应。
|
||||
|
||||
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(参阅“[检测并发写入](#检测并发写入)”)。
|
||||
为了解决这个问题,当一个客户端从数据库中读取数据时,它不仅仅发送它的请求到一个副本:读请求也被并行地发送到多个节点。客户可能会从不同的节点获得不同的响应。即来自一个节点的最新值和来自另一个节点的陈旧值。版本号用于确定哪个值更新(请参阅“[检测并发写入](#检测并发写入)”)。
|
||||
|
||||
#### 读修复和反熵
|
||||
|
||||
@ -523,7 +523,7 @@
|
||||
|
||||
在Dynamo风格的数据库中,参数n,w和r通常是可配置的。一个常见的选择是使n为奇数(通常为3或5)并设置 $w = r =(n + 1)/ 2$(向上取整)。但是可以根据需要更改数字。例如,设置$w = n$和$r = 1$的写入很少且读取次数较多的工作负载可能会受益。这使得读取速度更快,但具有只有一个失败节点导致所有数据库写入失败的缺点。
|
||||
|
||||
> 集群中可能有多于n的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在n个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在[第6章](ch6.md)继续讨论分区。
|
||||
> 集群中可能有多于n的节点。(集群的机器数可能多于副本数目),但是任何给定的值只能存储在n个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在[第六章](ch6.md)继续讨论分区。
|
||||
>
|
||||
|
||||
法定人数条件$w + r> n$允许系统容忍不可用的节点,如下所示:
|
||||
@ -598,7 +598,7 @@
|
||||
|
||||
#### 运维多个数据中心
|
||||
|
||||
我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制也适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
|
||||
我们先前讨论了跨数据中心复制作为多主复制的用例(请参阅“[多主复制](#多主复制)”)。无主复制也适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。
|
||||
|
||||
Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持:副本的数量n包括所有数据中心的节点,在配置中,您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何,每个来自客户端的写入都会发送到所有副本,但客户端通常只等待来自其本地数据中心内的法定节点的确认,从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生,尽管配置有一定的灵活性【50,51】。
|
||||
|
||||
@ -606,7 +606,7 @@
|
||||
|
||||
### 检测并发写入
|
||||
|
||||
Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(参阅“[处理写入冲突](#处理写入冲突)”),但在Dynamo样式的数据库中,在**读修复**或**提示移交**期间也可能会产生冲突。
|
||||
Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(请参阅“[处理写入冲突](#处理写入冲突)”),但在Dynamo样式的数据库中,在**读修复**或**提示移交**期间也可能会产生冲突。
|
||||
|
||||
问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X:
|
||||
|
||||
@ -653,7 +653,7 @@
|
||||
|
||||
> #### 并发性,时间和相对性
|
||||
>
|
||||
> 如果两个操作 **“同时”** 发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第8章](ch8.md)中详细讨论。
|
||||
> 如果两个操作 **“同时”** 发生,似乎应该称为并发——但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否**同时**发生的,这个问题我们将在[第八章](ch8.md)中详细讨论。
|
||||
>
|
||||
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作**并发**,而不管它们发生的物理时间。人们有时把这个原理和狭义相对论的物理学联系起来【54】,它引入了信息不能比光速更快的思想。因此,如果两个事件发生的时间差小于光通过它们之间的距离所需要的时间,那么这两个事件不可能相互影响。
|
||||
>
|
||||
@ -696,7 +696,7 @@
|
||||
|
||||
这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并发写入的值来擦屁股。 Riak称这些并发值**兄弟(siblings)**。
|
||||
|
||||
合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
|
||||
合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(请参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。
|
||||
|
||||
以购物车为例,一种合理的合并兄弟方法就是集合求并集。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋同时出现在两个兄弟里,即使他们每个只被写过一次。合并的值可以是[牛奶,面粉,鸡蛋,培根,火腿],没有重复。
|
||||
|
||||
|
32
ch6.md
32
ch6.md
@ -11,9 +11,9 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第5章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区(partitions)**,也称为**分片(sharding)**[^i]。
|
||||
在[第五章](ch5.md)中,我们讨论了复制——即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行**分区(partitions)**,也称为**分片(sharding)**[^i]。
|
||||
|
||||
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits)** 无关,这是节点之间网络故障的一种。我们将在[第8章](ch8.md)讨论这些错误。
|
||||
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits)** 无关,这是节点之间网络故障的一种。我们将在[第八章](ch8.md)讨论这些错误。
|
||||
|
||||
> #### 术语澄清
|
||||
>
|
||||
@ -22,11 +22,11 @@
|
||||
|
||||
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
|
||||
|
||||
分区主要是为了**可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
|
||||
分区主要是为了**可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(请参阅[第二部分](part-ii.md)关于[无共享架构](part-ii.md#无共享架构)的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
|
||||
|
||||
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
|
||||
|
||||
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出,最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(参阅“[事务处理还是分析](ch3.md#事务处理还是分析)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
|
||||
分区数据库在20世纪80年代由Teradata和NonStop SQL【1】等产品率先推出,最近因为NoSQL数据库和基于Hadoop的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(请参阅“[事务处理还是分析](ch3.md#事务处理还是分析)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
|
||||
|
||||
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论[分区再平衡(rebalancing)](#分区再平衡),如果想要添加或删除集群中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
|
||||
|
||||
一个节点可能存储多个分区。 如果使用主从复制模型,则分区和复制的组合如[图6-1](img/fig6-1.png)所示。 每个分区领导者(主)被分配给一个节点,追随者(从)被分配给其他节点。 每个节点可能是某些分区的领导者,同时是其他分区的追随者。
|
||||
我们在[第5章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
|
||||
我们在[第五章](ch5.md)讨论的关于数据库复制的所有内容同样适用于分区的复制。 大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
|
||||
|
||||
![](img/fig6-1.png)
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在“[分区再平衡](#分区再平衡)”中更详细地讨论分区边界的选择)。 Bigtable使用了这种分区策略,以及其开源等价物HBase 【2, 3】,RethinkDB和2.4版本之前的MongoDB 【4】。
|
||||
|
||||
在每个分区中,我们可以按照一定的顺序保存键(参见“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(参阅“[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
|
||||
在每个分区中,我们可以按照一定的顺序保存键(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,您可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅“[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
|
||||
|
||||
然而,Key Range分区的缺点是某些特定的访问模式会导致热点。 如果主键是时间戳,则分区对应于时间范围,例如,给每天分配一个分区。 不幸的是,由于我们在测量发生时将数据从传感器写入数据库,因此所有写入操作都会转到同一个分区(即今天的分区),这样分区可能会因写入而过载,而其他分区则处于空闲状态【5】。
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
|
||||
> #### 一致性哈希
|
||||
>
|
||||
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统,例如CDN中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅[第5章](ch5.md))或ACID一致性(参阅[第7章](ch7.md))无关,而只是描述了一种重新平衡(reblancing)的特定方法。
|
||||
> 一致性哈希由Karger等人定义。【7】 用于跨互联网级别的缓存系统,例如CDN中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界(partition boundaries)** 来避免中央控制或分布式共识的需要。 请注意,这里的一致性与复制一致性(请参阅[第五章](ch5.md))或ACID一致性(请参阅[第七章](ch7.md))无关,而只是描述了一种重新平衡(reblancing)的特定方法。
|
||||
>
|
||||
> 正如我们将在“[分区再平衡](#分区再平衡)”中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。 因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为**散列分区(hash partitioning)**。
|
||||
|
||||
@ -129,7 +129,7 @@
|
||||
|
||||
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是**字段(field)**,关系数据库中这些是**列(column)** )。 如果您声明了索引,则数据库可以自动执行索引[^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目`color:red`的文档ID列表中。
|
||||
|
||||
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 参见“[多对象事务的需求](ch7.md#多对象事务的需求)”。
|
||||
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档ID的映射来实现辅助索引。 如果沿着这条路线走下去,请万分小心,确保您的索引与底层数据保持一致。 竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅“[多对象事务的需求](ch7.md#多对象事务的需求)”。
|
||||
|
||||
![](img/fig6-4.png)
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
但是,从文档分区索引中读取需要注意:除非您对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
|
||||
|
||||
|
||||
这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。
|
||||
这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(请参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。
|
||||
|
||||
|
||||
### 基于关键词(Term)的二级索引进行分区
|
||||
@ -159,11 +159,11 @@
|
||||
|
||||
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要**分散/收集**所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
|
||||
|
||||
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第7章](ch7.md)和[第9章](ch9.md))。
|
||||
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅[第七章](ch7.md)和[第九章](ch9.md))。
|
||||
|
||||
在实践中,对全局二级索引的更新通常是**异步**的(也就是说,如果在写入之后不久读取索引,刚才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB声称在正常情况下,其全局次级索引会在不到一秒的时间内更新,但在基础架构出现故障的情况下可能会有延迟【20】。
|
||||
|
||||
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库,它允许您在本地和全局索引之间进行选择【22】。我们将在[第12章](ch12.md)中继续关键词分区二级索引实现的话题。
|
||||
全局关键词分区索引的其他用途包括Riak的搜索功能【21】和Oracle数据仓库,它允许您在本地和全局索引之间进行选择【22】。我们将在[第十二章](ch12.md)中继续关键词分区二级索引实现的话题。
|
||||
|
||||
## 分区再平衡
|
||||
|
||||
@ -216,9 +216,9 @@
|
||||
|
||||
#### 动态分区
|
||||
|
||||
对于使用键范围分区的数据库(参阅“[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
|
||||
对于使用键范围分区的数据库(请参阅“[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果出现边界错误,则可能会导致一个分区中的所有数据或者其他分区中的所有数据为空。手动重新配置分区边界将非常繁琐。
|
||||
|
||||
出于这个原因,按键的范围进行分区的数据库(如HBase和RethinkDB)会动态创建分区。当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据【26】。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似(参阅“[B树](ch3.md#B树)”)。
|
||||
出于这个原因,按键的范围进行分区的数据库(如HBase和RethinkDB)会动态创建分区。当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据【26】。与之相反,如果大量数据被删除并且分区缩小到某个阈值以下,则可以将其与相邻分区合并。此过程与B树顶层发生的过程类似(请参阅“[B树](ch3.md#B树)”)。
|
||||
|
||||
每个分区分配给一个节点,每个节点可以处理多个分区,就像固定数量的分区一样。大型分区拆分后,可以将其中的一半转移到另一个节点,以平衡负载。在HBase中,分区文件的传输通过HDFS(底层使用的分布式文件系统)来实现【3】。
|
||||
|
||||
@ -236,7 +236,7 @@
|
||||
|
||||
当一个新节点加入集群时,它随机选择固定数量的现有分区进行拆分,然后占有这些拆分分区中每个分区的一半,同时将每个分区的另一半留在原地。随机化可能会产生不公平的分割,但是平均在更大数量的分区上时(在Cassandra中,默认情况下,每个节点有256个分区),新节点最终从现有节点获得公平的负载份额。 Cassandra 3.0引入了另一种再平衡的算法来避免不公平的分割【29】。
|
||||
|
||||
随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】(参阅“[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
|
||||
随机选择分区边界要求使用基于散列的分区(可以从散列函数产生的数字范围中挑选边界)。实际上,这种方法最符合一致性哈希的原始定义【7】(请参阅“[一致性哈希](#一致性哈希)”)。最新的哈希函数可以在较低元数据开销的情况下达到类似的效果【8】。
|
||||
|
||||
### 运维:手动还是自动再平衡
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
|
||||
**图6-7 将请求路由到正确节点的三种不同方式。**
|
||||
|
||||
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第9章](ch9.md))。
|
||||
这是一个具有挑战性的问题,因为重要的是所有参与者都同意 - 否则请求将被发送到错误的节点,得不到正确的处理。 在分布式系统中有达成共识的协议,但很难正确地实现(见[第九章](ch9.md))。
|
||||
|
||||
许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据,如[图6-8](img/fig6-8.png)所示。 每个节点在ZooKeeper中注册自己,ZooKeeper维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在ZooKeeper中订阅此信息。 只要分区分配发生了改变,或者集群中添加或删除了一个节点,ZooKeeper就会通知路由层使路由信息保持最新状态。
|
||||
|
||||
@ -290,7 +290,7 @@
|
||||
|
||||
然而,通常用于分析的**大规模并行处理(MPP, Massively parallel processing)** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接,过滤,分组和聚合操作。 MPP查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
|
||||
|
||||
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在[第10章](ch10.md)讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述,请参阅参考文献【1,33】。
|
||||
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在[第十章](ch10.md)讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述,请参阅参考文献【1,33】。
|
||||
|
||||
## 本章小结
|
||||
|
||||
|
68
ch7.md
68
ch7.md
@ -31,7 +31,7 @@
|
||||
|
||||
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入**并发控制**的领域,讨论各种可能发生的竞争条件,以及数据库如何实现**读已提交(read committed)**,**快照隔离(snapshot isolation)**和**可串行化(serializability)**等隔离级别。
|
||||
|
||||
本章同时适用于单机数据库与分布式数据库;在[第8章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。
|
||||
本章同时适用于单机数据库与分布式数据库;在[第八章](ch8.md)中将重点讨论仅出现在分布式系统中的特殊挑战。
|
||||
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
现今,几乎所有的关系型数据库和一些非关系数据库都支持**事务**。其中大多数遵循IBM System R(第一个SQL数据库)在1975年引入的风格【1,2,3】。40年里,尽管一些实现细节发生了变化,但总体思路大同小异:MySQL,PostgreSQL,Oracle,SQL Server等数据库中的事务支持与System R异乎寻常地相似。
|
||||
|
||||
2000年以后,非关系(NoSQL)数据库开始普及。它们的目标是在关系数据库的现状基础上,通过提供新的数据模型选择(参见[第2章](ch2.md))并默认包含复制(第5章)和分区(第6章)来进一步提升。事务是这次运动的主要牺牲品:这些新一代数据库中的许多数据库完全放弃了事务,或者重新定义了这个词,描述比以前所理解的更弱得多的一套保证【4】。
|
||||
2000年以后,非关系(NoSQL)数据库开始普及。它们的目标是在关系数据库的现状基础上,通过提供新的数据模型选择(请参阅[第二章](ch2.md))并默认包含复制(第五章)和分区(第六章)来进一步提升。事务是这次运动的主要牺牲品:这些新一代数据库中的许多数据库完全放弃了事务,或者重新定义了这个词,描述比以前所理解的更弱得多的一套保证【4】。
|
||||
|
||||
随着这种新型分布式数据库的炒作,人们普遍认为事务是可伸缩性的对立面,任何大型系统都必须放弃事务以保持良好的性能和高可用性【5,6】。另一方面,数据库厂商有时将事务保证作为“重要应用”和“有价值数据”的基本要求。这两种观点都是**纯粹的夸张**。
|
||||
|
||||
@ -71,7 +71,7 @@ ACID原子性的定义特征是:**能够在错误时中止事务,丢弃该
|
||||
|
||||
一致性这个词被赋予太多含义:
|
||||
|
||||
* 在[第5章](ch5.md)中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。
|
||||
* 在[第五章](ch5.md)中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。
|
||||
* [一致性哈希(Consistency Hashing)](ch6.md#一致性哈希))是某些系统用于重新分区的一种分区方法。
|
||||
* 在[CAP定理](ch9.md#CAP定理)中,一致性一词用于表示[线性一致性](ch9.md#线性一致性)。
|
||||
* 在ACID的上下文中,**一致性**是指数据库在应用程序的特定概念中处于“良好状态”。
|
||||
@ -104,7 +104,7 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**
|
||||
|
||||
数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。**持久性** 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
|
||||
|
||||
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或SSD。它通常还包括预写日志或类似的文件(参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
|
||||
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或SSD。它通常还包括预写日志或类似的文件(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
|
||||
|
||||
如“[可靠性](ch1.md#可靠性)”一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
|
||||
|
||||
@ -115,8 +115,8 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**
|
||||
> 真相是,没有什么是完美的:
|
||||
>
|
||||
> * 如果你写入磁盘然后机器宕机,即使数据没有丢失,在修复机器或将磁盘转移到其他机器之前,也是无法访问的。这种情况下,复制系统可以保持可用性。
|
||||
> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的Bug)可能会一次性摧毁所有副本(参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
|
||||
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(参阅「[处理节点宕机](ch5.md#处理节点宕机)」)。
|
||||
> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的Bug)可能会一次性摧毁所有副本(请参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
|
||||
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(请参阅「[处理节点宕机](ch5.md#处理节点宕机)」)。
|
||||
> * 当电源突然断电时,特别是固态硬盘,有证据显示有时会违反应有的保证:甚至fsync也不能保证正常工作【12】。硬盘固件可能有错误,就像任何其他类型的软件一样【13,14】。
|
||||
> * 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误,并可能导致磁盘上的文件在崩溃后被损坏【15,16】。
|
||||
> * 磁盘上的数据可能会在没有检测到的情况下逐渐损坏【17】。如果数据已损坏一段时间,副本和最近的备份也可能损坏。这种情况下,需要尝试从历史备份中恢复数据。
|
||||
@ -173,27 +173,27 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
- 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
|
||||
- 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值?
|
||||
|
||||
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(参阅“[让B树更可靠](ch3.md#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
|
||||
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
|
||||
|
||||
一些数据库也提供更复杂的原子操作[^iv],例如自增操作,这样就不再需要像 [图7-1](img/fig7-1.png) 那样的读取-修改-写入序列了。同样流行的是 **[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
|
||||
|
||||
[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下,它实际上应该被称为 **隔离的(isolated)** 的或**可串行的(serializable)** 的增量。 但这就太吹毛求疵了。
|
||||
|
||||
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。
|
||||
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(请参阅“[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。
|
||||
|
||||
#### 多对象事务的需求
|
||||
|
||||
许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第9章](ch9.md) 将讨论分布式事务的实现。
|
||||
许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第九章](ch9.md) 将讨论分布式事务的实现。
|
||||
|
||||
但是我们是否需要多对象事务?**是否有可能只用键值数据模型和单对象操作来实现任何应用程序?**
|
||||
|
||||
有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象:
|
||||
|
||||
* 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。(类似的是,在一个图数据模型中,一个顶点有着到其他顶点的边)。多对象事务使你确保这些引用始终有效:当插入几个相互引用的记录时,外键必须是正确的和最新的,不然数据就没有意义。
|
||||
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
|
||||
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(请参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
|
||||
* 在具有二级索引的数据库中(除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离性,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。
|
||||
|
||||
这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第12章](ch12.md)中探讨其他方法。
|
||||
这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第十二章](ch12.md)中探讨其他方法。
|
||||
|
||||
#### 处理错误和中止
|
||||
|
||||
@ -215,7 +215,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
如果两个事务不触及相同的数据,它们可以安全地**并行(parallel)** 运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
|
||||
|
||||
并发BUG很难通过测试找到,因为这样的错误只有在特殊时机下才会触发。这样的时机可能很少,通常很难重现[^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
|
||||
并发BUG很难通过测试找到,因为这样的错误只有在特殊时序下才会触发。这样的时序问题可能非常少发生,通常很难重现[^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
|
||||
|
||||
[^译注i]: 轶事:偶然出现的瞬时错误有时称为***Heisenbug***,而确定性的问题对应地称为***Bohrbugs***
|
||||
|
||||
@ -298,7 +298,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
这种异常被称为**不可重复读(nonrepeatable read)**或**读取偏差(read skew)**:如果Alice在事务结束时再次读取账户1的余额,她将看到与她之前的查询中看到的不同的值(600美元)。在读已提交的隔离条件下,**不可重复读**被认为是可接受的:Alice看到的帐户余额时确实在阅读时已经提交了。
|
||||
|
||||
> 不幸的是,术语**偏差(skew)** 这个词是过载的:以前使用它是因为热点的不平衡工作量(参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时机。
|
||||
> 不幸的是,术语**偏差(skew)** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。
|
||||
|
||||
对于Alice的情况,这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面,她很可能会看到一致的帐户余额。但是有些情况下,不能容忍这种暂时的不一致:
|
||||
|
||||
@ -308,7 +308,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
***分析查询和完整性检查***
|
||||
|
||||
有时,您可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
|
||||
有时,您可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
|
||||
|
||||
**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照(consistent snapshot)** 中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
|
||||
|
||||
@ -400,7 +400,7 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
|
||||
|
||||
类似地,像MongoDB这样的文档数据库提供了对JSON文档的一部分进行本地修改的原子操作,Redis提供了修改数据结构(如优先级队列)的原子操作。并不是所有的写操作都可以用原子操作的方式来表达,例如维基页面的更新涉及到任意文本编辑[^viii],但是在可以使用原子操作的情况下,它们通常是最好的选择。
|
||||
|
||||
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。参阅“[自动冲突解决](ch5.md#自动冲突解决)”。
|
||||
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。请参阅“[自动冲突解决](ch5.md#自动冲突解决)”。
|
||||
|
||||
原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为**游标稳定性(cursor stability)**【36,37】。另一个选择是简单地强制所有的原子操作在单一线程上执行。
|
||||
|
||||
@ -453,7 +453,7 @@ UPDATE wiki_pages SET content = '新内容'
|
||||
|
||||
#### 冲突解决和复制
|
||||
|
||||
在复制数据库中(参见[第5章](ch5.md)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
|
||||
在复制数据库中(请参阅[第五章](ch5.md)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
|
||||
|
||||
锁和CAS操作假定只有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行,并异步复制到副本上,因此无法保证只有一个最新数据的副本。所以基于锁或CAS操作的技术不适用于这种情况。 (我们将在“[线性一致性](ch9.md#线性一致性)”中更详细地讨论这个问题。)
|
||||
|
||||
@ -483,12 +483,12 @@ UPDATE wiki_pages SET content = '新内容'
|
||||
|
||||
这种异常称为**写偏差**【28】。它既不是**脏写**,也不是**丢失更新**,因为这两个事务正在更新两个不同的对象(Alice和Bob各自的待命记录)。在这里发生的冲突并不是那么明显,但是这显然是一个竞争条件:如果两个事务一个接一个地运行,那么第二个医生就不能歇班了。异常行为只有在事务并发进行时才有可能。
|
||||
|
||||
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时机)。
|
||||
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生脏写或丢失更新(取决于时序)。
|
||||
|
||||
我们已经看到,有各种不同的方法来防止丢失的更新。但对于写偏差,我们的选择更受限制:
|
||||
|
||||
* 由于涉及多个对象,单对象的原子操作不起作用。
|
||||
* 不幸的是,在一些快照隔离的实现中,自动检测丢失更新对此并没有帮助。在PostgreSQL的可重复读,MySQL/InnoDB的可重复读,Oracle可串行化或SQL Server的快照隔离级别中,都不会自动检测写入偏差【23】。自动防止写入偏差需要真正的可串行化隔离(请参见“[可串行化](#可串行化)”)。
|
||||
* 不幸的是,在一些快照隔离的实现中,自动检测丢失更新对此并没有帮助。在PostgreSQL的可重复读,MySQL/InnoDB的可重复读,Oracle可串行化或SQL Server的快照隔离级别中,都不会自动检测写入偏差【23】。自动防止写入偏差需要真正的可串行化隔离(请参阅“[可串行化](#可串行化)”)。
|
||||
* 某些数据库允许配置约束,然后由数据库强制执行(例如,唯一性,外键约束或特定值限制)。但是为了指定至少有一名医生必须在线,需要一个涉及多个对象的约束。大多数数据库没有内置对这种约束的支持,但是你可以使用触发器,或者物化视图来实现它们,这取决于不同的数据库【42】。
|
||||
* 如果无法使用可串行化的隔离级别,则此情况下的次优选项可能是显式锁定事务所依赖的行。在例子中,你可以写下如下的代码:
|
||||
|
||||
@ -514,7 +514,7 @@ COMMIT;
|
||||
|
||||
***会议室预订系统***
|
||||
|
||||
比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参见示例7-2)[^ix]。
|
||||
比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参阅示例7-2)[^ix]。
|
||||
|
||||
[^ix]: 在PostgreSQL中,您可以使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持。
|
||||
|
||||
@ -584,7 +584,7 @@ COMMIT;
|
||||
|
||||
- 隔离级别难以理解,并且在不同的数据库中实现的不一致(例如,“可重复读”的含义天差地别)。
|
||||
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,您可能并不知道并发发生的所有事情。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时机下才会出现问题。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时序下才会出现问题。
|
||||
|
||||
这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可串行化(serializable)** 的隔离级别!
|
||||
|
||||
@ -592,11 +592,11 @@ COMMIT;
|
||||
|
||||
但如果可串行化隔离级别比弱隔离级别的烂摊子要好得多,那为什么没有人见人爱?为了回答这个问题,我们需要看看实现可串行化的选项,以及它们如何执行。目前大多数提供可串行化的数据库都使用了三种技术之一,本章的剩余部分将会介绍这些技术。
|
||||
|
||||
- 字面意义上地串行顺序执行事务(参见“[真的串行执行](#真的串行执行)”)
|
||||
- **两阶段锁定(2PL, two-phase locking)**,几十年来唯一可行的选择。(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”)
|
||||
- 乐观并发控制技术,例如**可串行化快照隔离(serializable snapshot isolation)**(参阅“[可串行化快照隔离(SSI)](#可串行化快照隔离(SSI))”
|
||||
- 字面意义上地串行顺序执行事务(请参阅“[真的串行执行](#真的串行执行)”)
|
||||
- **两阶段锁定(2PL, two-phase locking)**,几十年来唯一可行的选择。(请参阅“[两阶段锁定(2PL)](#两阶段锁定(2PL))”)
|
||||
- 乐观并发控制技术,例如**可串行化快照隔离(serializable snapshot isolation)**(请参阅“[可串行化快照隔离(SSI)](#可串行化快照隔离(SSI))”
|
||||
|
||||
现在将主要在单节点数据库的背景下讨论这些技术;在[第9章](ch9.md)中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
|
||||
现在将主要在单节点数据库的背景下讨论这些技术;在[第九章](ch9.md)中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
|
||||
|
||||
### 真的串行执行
|
||||
|
||||
@ -606,8 +606,8 @@ COMMIT;
|
||||
|
||||
两个进展引发了这个反思:
|
||||
|
||||
- RAM足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中。(参阅“[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
|
||||
- 数据库设计人员意识到OLTP事务通常很短,而且只进行少量的读写操作(参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
|
||||
- RAM足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中。(请参阅“[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
|
||||
- 数据库设计人员意识到OLTP事务通常很短,而且只进行少量的读写操作(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
|
||||
|
||||
串行执行事务的方法在VoltDB/H-Store,Redis和Datomic中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统更好,因为它可以避免锁的协调开销。但是其吞吐量仅限于单个CPU核的吞吐量。为了充分利用单一线程,需要与传统形式的事务不同的结构。
|
||||
|
||||
@ -645,13 +645,13 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
顺序执行所有事务使并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。
|
||||
|
||||
为了伸缩至多个CPU核心和多个节点,可以对数据进行分区(参见[第6章](ch6.md)),在VoltDB中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的CPU核,事务吞吐量就可以与CPU核数保持线性伸缩【47】。
|
||||
为了伸缩至多个CPU核心和多个节点,可以对数据进行分区(请参阅[第六章](ch6.md)),在VoltDB中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的CPU核,事务吞吐量就可以与CPU核数保持线性伸缩【47】。
|
||||
|
||||
但是,对于需要访问多个分区的任何事务,数据库必须在触及的所有分区之间协调事务。存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。
|
||||
|
||||
由于跨分区事务具有额外的协调开销,所以它们比单分区事务慢得多。 VoltDB报告的吞吐量大约是每秒1000个跨分区写入,比单分区吞吐量低几个数量级,并且不能通过增加更多的机器来增加【49】。
|
||||
|
||||
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个二级索引的数据可能需要大量的跨分区协调(参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。
|
||||
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个二级索引的数据可能需要大量的跨分区协调(请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。
|
||||
|
||||
#### 串行执行小结
|
||||
|
||||
@ -672,16 +672,16 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
> #### 2PL不是2PC
|
||||
>
|
||||
> 请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在[第9章](ch9.md)讨论2PC。
|
||||
> 请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在[第九章](ch9.md)讨论2PC。
|
||||
|
||||
之前我们看到锁通常用于防止脏写(参阅“[没有脏写](#没有脏写)”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
|
||||
之前我们看到锁通常用于防止脏写(请参阅“[没有脏写](#没有脏写)”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
|
||||
|
||||
两阶段锁定类似,但是锁的要求更强得多。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)** 权限:
|
||||
|
||||
- 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续。 (这确保B不能在A底下意外地改变对象。)
|
||||
- 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续。 (像[图7-1](img/fig7-1.png)那样读取旧版本的对象在2PL下是不可接受的。)
|
||||
|
||||
在2PL中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得**读不阻塞写,写也不阻塞读**(参阅“[实现快照隔离](#实现快照隔离)”),这是2PL和快照隔离之间的关键区别。另一方面,因为2PL提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。
|
||||
在2PL中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得**读不阻塞写,写也不阻塞读**(请参阅“[实现快照隔离](#实现快照隔离)”),这是2PL和快照隔离之间的关键区别。另一方面,因为2PL提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。
|
||||
|
||||
#### 实现两阶段锁
|
||||
|
||||
@ -704,7 +704,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
传统的关系数据库不限制事务的持续时间,因为它们是为等待人类输入的交互式应用而设计的。因此,当一个事务需要等待另一个事务时,等待的时长并没有限制。即使你保证所有的事务都很短,如果有多个事务想要访问同一个对象,那么可能会形成一个队列,所以事务可能需要等待几个其他事务才能完成。
|
||||
|
||||
因此,运行2PL的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(参阅“[描述性能](ch1.md#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
|
||||
因此,运行2PL的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(请参阅“[描述性能](ch1.md#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
|
||||
|
||||
基于锁实现的读已提交隔离级别可能发生死锁,但在基于2PL实现的可串行化隔离级别中,它们会出现的频繁的多(取决于事务的访问模式)。这可能是一个额外的性能问题:当事务由于死锁而被中止并被重试时,它需要从头重做它的工作。如果死锁很频繁,这可能意味着巨大的浪费。
|
||||
|
||||
@ -768,11 +768,11 @@ WHERE room_id = 123 AND
|
||||
|
||||
但是,如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用且无需冲突。
|
||||
|
||||
顾名思义,SSI基于快照隔离——也就是说,事务中的所有读取都是来自数据库的一致性快照(参见“[快照隔离和可重复读取](#快照隔离和可重复读)”)。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的串行化冲突,并确定要中止哪些事务。
|
||||
顾名思义,SSI基于快照隔离——也就是说,事务中的所有读取都是来自数据库的一致性快照(请参阅“[快照隔离和可重复读取](#快照隔离和可重复读)”)。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的串行化冲突,并确定要中止哪些事务。
|
||||
|
||||
#### 基于过时前提的决策
|
||||
|
||||
先前讨论了快照隔离中的写入偏差(参阅“[写入偏斜与幻读](#写入偏斜与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
先前讨论了快照隔离中的写入偏差(请参阅“[写入偏斜与幻读](#写入偏斜与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
|
||||
换句话说,事务基于一个**前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
|
||||
|
48
ch8.md
48
ch8.md
@ -20,11 +20,11 @@
|
||||
|
||||
但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事)
|
||||
|
||||
[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(参见“[拜占庭故障](#拜占庭故障)”)。
|
||||
[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(请参阅“[拜占庭故障](#拜占庭故障)”)。
|
||||
|
||||
使用分布式系统与在一台计算机上编写软件有着根本的区别,主要的区别在于,有许多新颖和刺激的方法可以使事情出错【1,2】。在这一章中,我们将了解实践中出现的问题,理解我们能够依赖,和不可以依赖的东西。
|
||||
|
||||
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在[第9章](ch9.md)中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。
|
||||
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在[第九章](ch9.md)中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。
|
||||
|
||||
本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
* 系统越大,其组件之一就越有可能坏掉。随着时间的推移,坏掉的东西得到修复,新的东西又坏掉,但是在一个有成千上万个节点的系统中,有理由认为总是有一些东西是坏掉的【7】。当错误处理的策略只由简单放弃组成时,一个大的系统最终会花费大量时间从错误中恢复,而不是做有用的工作【8】。
|
||||
|
||||
* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(参阅[第4章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。
|
||||
* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(请参阅[第四章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。
|
||||
|
||||
* 在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们的所有节点都靠近在一起。
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
1. 请求可能已经丢失(可能有人拔掉了网线)。
|
||||
2. 请求可能正在排队,稍后将交付(也许网络或接收方过载)。
|
||||
3. 远程节点可能已经失效(可能是崩溃或关机)。
|
||||
4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;参阅“[进程暂停](#进程暂停)”),但稍后会再次响应。
|
||||
4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;请参阅“[进程暂停](#进程暂停)”),但稍后会再次响应。
|
||||
5. 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。
|
||||
6. 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。
|
||||
|
||||
@ -123,20 +123,20 @@
|
||||
|
||||
> #### 网络分区
|
||||
>
|
||||
> 当网络的一部分由于网络故障而被切断时,有时称为**网络分区(network partition)**或**网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语**网络故障(network fault)**,以避免与[第6章](ch6.md)讨论的存储系统的分区(分片)相混淆。
|
||||
> 当网络的一部分由于网络故障而被切断时,有时称为**网络分区(network partition)**或**网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语**网络故障(network fault)**,以避免与[第六章](ch6.md)讨论的存储系统的分区(分片)相混淆。
|
||||
|
||||
即使网络故障在你的环境中非常罕见,故障可能发生的事实,意味着你的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法避免的。
|
||||
|
||||
如果网络故障的错误处理没有定义与测试,武断地讲,各种错误可能都会发生:例如,即使网络恢复【20】,集群可能会发生**死锁**,永久无法为请求提供服务,甚至可能会删除所有的数据【21】。如果软件被置于意料之外的情况下,它可能会做出出乎意料的事情。
|
||||
|
||||
处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,您确实需要知道您的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是Chaos Monkey背后的想法;参阅“[可靠性](ch1.md#可靠性)”)。
|
||||
处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,您确实需要知道您的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是Chaos Monkey背后的想法;请参阅“[可靠性](ch1.md#可靠性)”)。
|
||||
|
||||
### 检测故障
|
||||
|
||||
许多系统需要自动检测故障节点。例如:
|
||||
|
||||
* 负载平衡器需要停止向已死亡的节点转发请求(即从**移出轮询列表(out of rotation)**)。
|
||||
* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。
|
||||
* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。
|
||||
|
||||
不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,您可能会收到一些反馈信息,明确告诉您某些事情没有成功:
|
||||
|
||||
@ -155,7 +155,7 @@
|
||||
|
||||
长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短的超时可以更快地检测到故障,但有更高地风险误将一个节点宣布为失效,而该节点实际上只是暂时地变慢了(例如由于节点或网络上的负载峰值)。
|
||||
|
||||
过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在“[知识、真相与谎言](#知识、真相与谎言)”以及[第9章](ch9.md)和[第11章](ch11.md)中更详细地讨论这个问题。
|
||||
过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在“[知识、真相与谎言](#知识、真相与谎言)”以及[第九章](ch9.md)和[第十一章](ch11.md)中更详细地讨论这个问题。
|
||||
|
||||
当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。特别是如果节点实际上没有死亡,只是由于过载导致其响应缓慢;这时将其负载转移到其他节点可能会导致**级联失效(cascading failure)**(在极端情况下,所有节点都宣告对方死亡,所有节点都将停止工作)。
|
||||
|
||||
@ -187,7 +187,7 @@
|
||||
|
||||
所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的变化范围特别大:拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。
|
||||
|
||||
在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce)(参阅[第10章](ch10.md))能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈变化【28,29】。
|
||||
在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce)(请参阅[第十章](ch10.md))能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈变化【28,29】。
|
||||
|
||||
在这种环境下,您只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定**故障检测延迟**与**过早超时风险**之间的适当折衷。
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
>
|
||||
> 相比之下,互联网动态分享网络带宽。发送者互相推挤和争夺,以让他们的数据包尽可能快地通过网络,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。这种方法有排队的缺点,但其优点是它最大限度地利用了电线。电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节都会更便宜。
|
||||
>
|
||||
> CPU也会出现类似的情况:如果您在多个线程间动态共享每个CPU内核,则一个线程有时必须在操作系统的运行队列里等待,而另一个线程正在运行,这样每个线程都有可能被暂停一个不定的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(参阅“[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。
|
||||
> CPU也会出现类似的情况:如果您在多个线程间动态共享每个CPU内核,则一个线程有时必须在操作系统的运行队列里等待,而另一个线程正在运行,这样每个线程都有可能被暂停一个不定的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(请参阅“[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。
|
||||
>
|
||||
> 如果资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可以实现**延迟保证**。但是,这是以降低利用率为代价的——换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它更便宜,但它具有可变延迟的缺点。
|
||||
>
|
||||
@ -324,7 +324,7 @@
|
||||
|
||||
尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时,会错误地推断出`x = 1`是最近的值,而丢弃写入`x = 2`。效果上表现为,客户端B的增量操作会丢失。
|
||||
|
||||
这种冲突解决策略被称为**最后写入胜利(LWW)**,它在多领导者复制和无领导者数据库(如Cassandra 【53】和Riak 【54】)中被广泛使用(参见“[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))”一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题:
|
||||
这种冲突解决策略被称为**最后写入胜利(LWW)**,它在多领导者复制和无领导者数据库(如Cassandra 【53】和Riak 【54】)中被广泛使用(请参阅“[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))”一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变LWW的基本问题:
|
||||
|
||||
* 数据库写入可能会神秘地消失:具有滞后时钟的节点无法覆盖之前具有快速时钟的节点写入的值,直到节点之间的时钟偏差消逝【54,55】。此方案可能导致一定数量的数据被悄悄丢弃,而未向应用报告任何错误。
|
||||
* LWW无法区分**高频顺序写入**(在[图8-3](img/fig8-3.png)中,客户端B的增量操作**一定**发生在客户端A的写入之后)和**真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止违背因果关系(请参阅“[检测并发写入](ch5.md#检测并发写入)”)。
|
||||
@ -334,7 +334,7 @@
|
||||
|
||||
NTP同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为NTP的同步精度本身,除了石英钟漂移这类误差源之外,还受到网络往返时间的限制。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。
|
||||
|
||||
所谓的**逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的**日历时钟**和**单调钟**也被称为**物理时钟(physical clock)**。我们将在“[顺序保证](ch9.md#顺序保证)”中来看顺序问题。
|
||||
所谓的**逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参阅“[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的**日历时钟**和**单调钟**也被称为**物理时钟(physical clock)**。我们将在“[顺序保证](ch9.md#顺序保证)”中来看顺序问题。
|
||||
|
||||
#### 时钟读数存在置信区间
|
||||
|
||||
@ -406,7 +406,7 @@ while (true) {
|
||||
* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致**页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将另一个页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为**抖动(thrashing)**)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
|
||||
* 可以通过发送SIGSTOP信号来暂停Unix进程,例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期,直到SIGCONT恢复为止,此时它将继续运行。 即使你的环境通常不使用SIGSTOP,也可能由运维工程师意外发送。
|
||||
|
||||
所有这些事件都可以随时**抢占(preempt)**正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时机做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
|
||||
所有这些事件都可以随时**抢占(preempt)**正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时序做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
|
||||
|
||||
当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不能直接转化为分布式系统操作,因为分布式系统没有共享内存,只有通过不可靠网络发送的消息。
|
||||
|
||||
@ -420,13 +420,13 @@ while (true) {
|
||||
|
||||
> #### 实时是真的吗?
|
||||
>
|
||||
> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见[第11章](ch11.md))。
|
||||
> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见[第十一章](ch11.md))。
|
||||
|
||||
例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为GC暂停而延迟弹出。
|
||||
|
||||
在系统中提供**实时保证**需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。库函数必须申明最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的负担);必须进行大量的测试和测量,以确保达到保证。
|
||||
|
||||
所有这些都需要大量额外的工作,严重限制了可以使用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**”与“**高性能**”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须让及时响应的优先级高于一切(另请参见“[延迟和资源利用](#延迟和资源利用)“)。
|
||||
所有这些都需要大量额外的工作,严重限制了可以使用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**”与“**高性能**”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须让及时响应的优先级高于一切(另请参阅“[延迟和资源利用](#延迟和资源利用)“)。
|
||||
|
||||
对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。
|
||||
|
||||
@ -436,7 +436,7 @@ while (true) {
|
||||
|
||||
一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧向客户端隐藏了GC暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。
|
||||
|
||||
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从该节点移开,就像[第4章](ch4.md)里描述的滚动升级一样。
|
||||
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从该节点移开,就像[第四章](ch4.md)里描述的滚动升级一样。
|
||||
|
||||
这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。
|
||||
|
||||
@ -452,7 +452,7 @@ while (true) {
|
||||
|
||||
幸运的是,我们不需要去搞清楚生命的意义。在分布式系统中,我们可以陈述关于行为(系统模型)的假设,并以满足这些假设的方式设计实际系统。算法可以被证明在某个系统模型中正确运行。这意味着即使底层系统模型提供了很少的保证,也可以实现可靠的行为。
|
||||
|
||||
但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在[第9章](ch9.md)中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。
|
||||
但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在[第九章](ch9.md)中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。
|
||||
|
||||
### 真相由多数所定义
|
||||
|
||||
@ -462,17 +462,17 @@ while (true) {
|
||||
|
||||
第三种情况,想象一个经历了一个长时间**停止所有处理垃圾收集暂停(stop-the-world GC Pause)**的节点。节点的所有线程被GC抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。
|
||||
|
||||
这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(参阅“[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。
|
||||
这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。
|
||||
|
||||
这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。
|
||||
|
||||
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第9章](ch9.md)中讨论**共识算法(consensus algorithms)**时,我们将更详细地讨论法定人数的应用。
|
||||
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第九章](ch9.md)中讨论**共识算法(consensus algorithms)**时,我们将更详细地讨论法定人数的应用。
|
||||
|
||||
#### 领导者和锁
|
||||
|
||||
通常情况下,一些东西在一个系统中只能有一个。例如:
|
||||
|
||||
* 数据库分区的领导者只能有一个节点,以避免**脑裂(split brain)**(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。
|
||||
* 数据库分区的领导者只能有一个节点,以避免**脑裂(split brain)**(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。
|
||||
* 特定资源的锁或对象只允许一个事务/客户端持有,以防同时写入和损坏。
|
||||
* 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。
|
||||
|
||||
@ -516,7 +516,7 @@ while (true) {
|
||||
|
||||
> ### 拜占庭将军问题
|
||||
>
|
||||
> 拜占庭将军问题是对所谓“两将军问题”的泛化【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在[第9章](ch9.md)讨论这个共识问题。
|
||||
> 拜占庭将军问题是对所谓“两将军问题”的泛化【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在[第九章](ch9.md)讨论这个共识问题。
|
||||
>
|
||||
> 在这个问题的拜占庭版本里,有n位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。
|
||||
>
|
||||
@ -545,7 +545,7 @@ while (true) {
|
||||
|
||||
### 系统模型与现实
|
||||
|
||||
已经有很多算法被设计以解决分布式系统问题——例如,我们将在[第9章](ch9.md)讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。
|
||||
已经有很多算法被设计以解决分布式系统问题——例如,我们将在[第九章](ch9.md)讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。
|
||||
|
||||
算法的编写方式不应该过分依赖于运行的硬件和软件配置的细节。这就要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可以假设的事情。
|
||||
|
||||
@ -584,7 +584,7 @@ while (true) {
|
||||
|
||||
为了定义算法是正确的,我们可以描述它的属性。例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。这只是定义对列表进行排序含义的一种形式方式。
|
||||
|
||||
同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性:
|
||||
同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(请参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性:
|
||||
|
||||
***唯一性(uniqueness)***
|
||||
|
||||
@ -621,7 +621,7 @@ while (true) {
|
||||
|
||||
例如,在崩溃-恢复(crash-recovery)模型中的算法通常假设稳定存储器中的数据在崩溃后可以幸存。但是,如果磁盘上的数据被破坏,或者由于硬件错误或错误配置导致数据被清除,会发生什么情况【91】?如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器,即使驱动器已正确连接到服务器,那又会发生什么情况【92】?
|
||||
|
||||
法定人数算法(参见“[读写法定人数](ch5.md#读写法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但有时也可能会丢失。但是那个模型就变得更难以推理了。
|
||||
法定人数算法(请参阅“[读写法定人数](ch5.md#读写法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但有时也可能会丢失。但是那个模型就变得更难以推理了。
|
||||
|
||||
算法的理论描述可以简单宣称一些事是不会发生的——在非拜占庭式系统中,我们确实需要对可能发生和不可能发生的故障做出假设。然而,真实世界的实现,仍然会包括处理“假设上不可能”情况的代码,即使代码可能就是`printf("Sucks to be you")`和`exit(666)`,实际上也就是留给运维来擦屁股【93】。(这可以说是计算机科学和软件工程间的一个差异)。
|
||||
|
||||
|
124
ch9.md
124
ch9.md
@ -9,11 +9,11 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
正如[第8章](ch8.md)所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法—— 即使某些内部组件出现故障,服务也能正常运行。
|
||||
正如[第八章](ch8.md)所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法—— 即使某些内部组件出现故障,服务也能正常运行。
|
||||
|
||||
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设[第8章](ch8.md)的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。
|
||||
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设[第八章](ch8.md)的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。
|
||||
|
||||
构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与[第7章](ch7.md)中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。
|
||||
构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与[第七章](ch7.md)中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。
|
||||
|
||||
现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是**共识(consensus)**:**就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,要可靠地达成共识,且不被网络故障和进程故障所影响,是一个令人惊讶的棘手问题。
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。
|
||||
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚才写入的值,因为读请求可能会被路由到另外的副本上。(参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚才写入的值,因为读请求可能会被路由到另外的副本上。(请参阅“[读己之写](ch5.md#读己之写)” )。
|
||||
|
||||
对于应用开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。对于后者,如果将一个值赋给一个变量,然后很快地再次读取,不可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
本章将探索数据系统可能选择提供的更强一致性模型。它不是免费的:具有较强保证的系统可能会比保证较差的系统具有更差的性能或更少的容错性。尽管如此,更强的保证能够吸引人,因为它们更容易用对。只有见过不同的一致性模型后,才能更好地决定哪一个最适合自己的需求。
|
||||
|
||||
**分布式一致性模型**和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(参见“[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了**避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于**在面对延迟和故障时如何协调副本间的状态**。
|
||||
**分布式一致性模型**和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(请参阅“[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了**避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于**在面对延迟和故障时如何协调副本间的状态**。
|
||||
|
||||
本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的:
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
|
||||
为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i]
|
||||
|
||||
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
|
||||
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。
|
||||
|
||||
在这个例子中,寄存器有两种类型的操作:
|
||||
|
||||
@ -139,15 +139,15 @@
|
||||
>
|
||||
> ***可串行化***
|
||||
>
|
||||
> **可串行化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。
|
||||
> **可串行化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。
|
||||
>
|
||||
> ***线性一致性***
|
||||
>
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。
|
||||
>
|
||||
> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或**强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”一节)或**真的串行执行**(参见第“[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。
|
||||
> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或**强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”一节)或**真的串行执行**(请参阅第“[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。
|
||||
>
|
||||
> 但是,可串行化的快照隔离(参见“[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
|
||||
> 但是,可串行化的快照隔离(请参阅“[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
|
||||
|
||||
|
||||
### 依赖线性一致性
|
||||
@ -158,9 +158,9 @@
|
||||
|
||||
一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。
|
||||
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,参阅“[领导者和锁](ch8.md#领导者和锁)”中的防护问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,请参阅“[领导者和锁](ch8.md#领导者和锁)”中的防护问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
|
||||
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本提供服务。你可以选择请求线性一致性读取:etcd称之为**法定人数读取(quorum read)**【16】,而在ZooKeeper中,你需要在读取之前调用`sync()`【15】。参阅“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”。
|
||||
[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本提供服务。你可以选择请求线性一致性读取:etcd称之为**法定人数读取(quorum read)**【16】,而在ZooKeeper中,你需要在读取之前调用`sync()`【15】。请参阅“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”。
|
||||
|
||||
分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中更多的粒度级别上使用。RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
|
||||
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。
|
||||
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
|
||||
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(请参阅[第十一章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
|
||||
![](img/fig9-5.png)
|
||||
**图9-5 Web服务器和图像缩放器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
|
||||
|
||||
@ -198,15 +198,15 @@
|
||||
|
||||
由于线性一致性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。
|
||||
|
||||
使系统容错最常用的方法是使用复制。我们再来回顾[第5章](ch5.md)中的复制方法,并比较它们是否可以满足线性一致性:
|
||||
使系统容错最常用的方法是使用复制。我们再来回顾[第五章](ch5.md)中的复制方法,并比较它们是否可以满足线性一致性:
|
||||
|
||||
***单主复制(可能线性一致)***
|
||||
|
||||
在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(potential)**是线性一致性的[^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
在具有单主复制功能的系统中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(potential)**是线性一致性的[^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务与共识](#分布式事务与共识)”)。
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(请参阅“[分布式事务与共识](#分布式事务与共识)”)。
|
||||
|
||||
从主库读取依赖一个假设,你确定地知道领导者是谁。正如在“[真相由多数所定义](ch8.md#真相由多数所定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
从主库读取依赖一个假设,你确定地知道领导者是谁。正如在“[真相由多数所定义](ch8.md#真相由多数所定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
|
||||
***共识算法(线性一致)***
|
||||
|
||||
@ -214,13 +214,13 @@
|
||||
|
||||
***多主复制(非线性一致)***
|
||||
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
|
||||
|
||||
***无主复制(也许不是线性一致的)***
|
||||
|
||||
对于无领导者复制的系统(Dynamo风格;参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
对于无领导者复制的系统(Dynamo风格;请参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
|
||||
基于日历时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(参见“[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。
|
||||
基于日历时钟(例如,在Cassandra中;请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(请参阅“[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。
|
||||
|
||||
#### 线性一致性和法定人数
|
||||
|
||||
@ -234,7 +234,7 @@
|
||||
|
||||
法定人数条件满足( $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】;但是由于使用了最后写入胜利的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。
|
||||
|
||||
而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置(CAS)操作,因为它需要一个共识算法【28】。
|
||||
|
||||
@ -246,7 +246,7 @@
|
||||
|
||||
一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。
|
||||
|
||||
我们已经在[第五章](ch5.md)中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
|
||||
我们已经在[第五章](ch5.md)中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(请参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。
|
||||
|
||||
![](img/fig9-7.png)
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
|
||||
在分布式系统中有更多有趣的“不可能”的结果【41】,且CAP定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。
|
||||
|
||||
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)**指代将大数据集细分为小数据集的操作(分片;参见[第6章](ch6.md))。与之对应的是,**网络分区(network partition)**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下我们无法避免混乱。
|
||||
[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)**指代将大数据集细分为小数据集的操作(分片;请参阅[第六章](ch6.md))。与之对应的是,**网络分区(network partition)**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下我们无法避免混乱。
|
||||
|
||||
#### 线性一致性和网络延迟
|
||||
|
||||
@ -299,7 +299,7 @@
|
||||
|
||||
许多分布式数据库也是如此:它们是**为了提高性能**而选择了牺牲线性一致性,而不是为了容错【46】。线性一致的速度很慢——这始终是事实,而不仅仅是网络故障期间。
|
||||
|
||||
能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(参见“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在[第12章](ch12.md)中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。
|
||||
能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在[第十二章](ch12.md)中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。
|
||||
|
||||
|
||||
|
||||
@ -309,9 +309,9 @@
|
||||
|
||||
**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文:
|
||||
|
||||
* 在[第5章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
|
||||
* 在[第7章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序(some sequential order)**执行的保证。它可以字面意义上地以**串行顺序(serial order)**执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
|
||||
* 在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
|
||||
* 在[第五章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。
|
||||
* 在[第七章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序(some sequential order)**执行的保证。它可以字面意义上地以**串行顺序(serial order)**执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
|
||||
* 在[第八章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
|
||||
|
||||
事实证明,顺序、线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
|
||||
|
||||
@ -323,7 +323,7 @@
|
||||
* [图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)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))通过跟踪事务之间的因果依赖来检测写偏差。
|
||||
* 事务之间**写偏差(write skew)**的例子(请参阅“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI))通过跟踪事务之间的因果依赖来检测写偏差。
|
||||
* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。
|
||||
|
||||
因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
|
||||
@ -348,7 +348,7 @@
|
||||
|
||||
***因果性***
|
||||
|
||||
我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
|
||||
我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(请参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
|
||||
|
||||
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。
|
||||
|
||||
@ -394,14 +394,14 @@
|
||||
|
||||
[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的UUID,并按照字典序比较UUID,以定义操作的全序。这是一个有效的全序,但是随机的UUID并不能告诉你哪个操作先发生,或者操作是否为并发的。
|
||||
|
||||
在单主复制的数据库中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。
|
||||
在单主复制的数据库中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。
|
||||
|
||||
#### 非因果序列号生成器
|
||||
|
||||
如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法:
|
||||
|
||||
* 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。
|
||||
* 可以将日历时钟(物理时钟)的时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以将日历时钟(物理时钟)的时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(请参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
|
||||
* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,而节点 B 可能要求序列号1,001到2,000区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。
|
||||
|
||||
这三个选项都比单一主库的自增计数器表现要好,并且更具可伸缩性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。
|
||||
@ -485,7 +485,7 @@
|
||||
|
||||
像ZooKeeper和etcd这样的共识服务实际上实现了全序广播。这一事实暗示了全序广播与共识之间有着紧密联系,我们将在本章稍后进行探讨。
|
||||
|
||||
全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为**状态机复制(state machine replication)**【60】,我们将在[第11章](ch11.md)中重新回到这个概念。
|
||||
全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为**状态机复制(state machine replication)**【60】,我们将在[第十一章](ch11.md)中重新回到这个概念。
|
||||
|
||||
与之类似,可以使用全序广播来实现可串行化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。
|
||||
|
||||
@ -493,7 +493,7 @@
|
||||
|
||||
考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志、事务日志或预写式日志中):传递消息就像追加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。
|
||||
|
||||
全序广播对于实现提供防护令牌的锁服务也很有用(参见“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。
|
||||
全序广播对于实现提供防护令牌的锁服务也很有用(请参阅“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。
|
||||
|
||||
#### 使用全序广播实现线性一致的存储
|
||||
|
||||
@ -521,7 +521,7 @@
|
||||
|
||||
* 你可以通过在日志中追加一条消息,然后读取日志,直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点。 (etcd的法定人数读取有些类似这种情况【16】。)
|
||||
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。 (这是Zookeeper `sync()` 操作背后的思想【15】)。
|
||||
* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。 (这种技术用于链式复制(chain replication)【63】;参阅“[关于复制的研究](ch5.md#关于复制的研究)”。)
|
||||
* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。 (这种技术用于链式复制(chain replication)【63】;请参阅“[关于复制的研究](ch5.md#关于复制的研究)”。)
|
||||
|
||||
#### 使用线性一致性存储实现全序广播
|
||||
|
||||
@ -545,26 +545,26 @@
|
||||
|
||||
**共识**是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是**让几个节点达成一致(get serveral nodes to agree on something)**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。
|
||||
|
||||
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。
|
||||
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第五章](ch5.md)),事务([第七章](ch7.md)),系统模型([第八章](ch8.md)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。
|
||||
|
||||
节点能达成一致,在很多场景下都非常重要,例如:
|
||||
|
||||
***领导选举***
|
||||
|
||||
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
|
||||
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
|
||||
|
||||
***原子提交***
|
||||
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)**问题[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 参阅“[三阶段提交](#三阶段提交)”。
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 请参阅“[三阶段提交](#三阶段提交)”。
|
||||
|
||||
> ### 共识的不可能性
|
||||
>
|
||||
> 你可能已经听说过以作者Fischer,Lynch和Paterson命名的FLP结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在**总是**能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事?
|
||||
>
|
||||
> 答案是FLP结果是在**异步系统模型**中被证明的(参阅“[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。
|
||||
> 答案是FLP结果是在**异步系统模型**中被证明的(请参阅“[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。
|
||||
>
|
||||
> 因此,虽然FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。
|
||||
|
||||
@ -576,17 +576,17 @@
|
||||
|
||||
### 原子提交与两阶段提交(2PC)
|
||||
|
||||
在[第7章](ch7.md)中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。
|
||||
在[第七章](ch7.md)中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。
|
||||
|
||||
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
|
||||
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
|
||||
|
||||
#### 从单节点到分布式原子提交
|
||||
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,参阅“[让B树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
|
||||
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
|
||||
|
||||
因此,在单个节点上,事务的提交主要取决于数据持久化落盘的**顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘的控制器,且挂载在单台机器上)使得提交具有原子性。
|
||||
|
||||
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(参见“[实践中的分布式事务](#实践中的分布式事务)”)。
|
||||
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(请参阅“[实践中的分布式事务](#实践中的分布式事务)”)。
|
||||
|
||||
在这些情况下,仅向所有节点发送提交请求并独立提交每个节点的事务是不够的。这样很容易发生违反原子性的情况:提交在某些节点上成功,而在其他节点上失败:
|
||||
|
||||
@ -612,7 +612,7 @@
|
||||
|
||||
> #### 不要把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。
|
||||
|
||||
@ -659,7 +659,7 @@
|
||||
|
||||
两阶段提交被称为**阻塞(blocking)**原子提交协议,因为存在2PC可能卡住并等待协调者恢复的情况。理论上,可以使一个原子提交协议变为**非阻塞(nonblocking)**的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。
|
||||
|
||||
作为2PC的替代方案,已经提出了一种称为**三阶段提交(3PC)**的算法【13,80】。然而,3PC假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它并不能保证原子性。
|
||||
作为2PC的替代方案,已经提出了一种称为**三阶段提交(3PC)**的算法【13,80】。然而,3PC假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第八章](ch8.md)),它并不能保证原子性。
|
||||
|
||||
通常,非阻塞原子提交需要一个**完美的故障检测器(perfect failure detector)**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中,超时并不是一种可靠的故障检测机制,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。
|
||||
|
||||
@ -691,7 +691,7 @@
|
||||
|
||||
然而,只有当所有受事务影响的系统都使用同样的**原子提交协议(atomic commit protocl)**时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。
|
||||
|
||||
在[第11章](ch11.md)中将再次回到“恰好一次”消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
|
||||
在[第十一章](ch11.md)中将再次回到“恰好一次”消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
|
||||
|
||||
#### XA事务
|
||||
|
||||
@ -709,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分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。
|
||||
|
||||
@ -731,10 +731,10 @@
|
||||
|
||||
* 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。
|
||||
* 许多服务器端应用都是使用无状态模式开发的(受HTTP的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。
|
||||
* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与SSI(参阅[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI) ))协同工作,因为这需要一个跨系统定位冲突的协议。
|
||||
* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与SSI(请参阅[可串行化快照隔离(SSI)](ch7.md#可串行化快照隔离(SSI) ))协同工作,因为这需要一个跨系统定位冲突的协议。
|
||||
* 对于数据库内部的分布式事务(不是XA),限制没有这么大 —— 例如,分布式版本的SSI是可能的。然而仍然存在问题:2PC成功提交一个事务需要所有参与者的响应。因此,如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效(amplifying failures)**的趋势,这又与我们构建容错系统的目标背道而驰。
|
||||
|
||||
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于**共识**的话题。
|
||||
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于**共识**的话题。
|
||||
|
||||
|
||||
|
||||
@ -767,11 +767,11 @@
|
||||
|
||||
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。
|
||||
|
||||
**终止**属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 (**终止**是一种**活性属性**,而另外三种是**安全属性** —— 参见“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
**终止**属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 (**终止**是一种**活性属性**,而另外三种是**安全属性** —— 请参阅“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
|
||||
共识的系统模型假设,当一个节点“崩溃”时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足**终止**属性。特别是,2PC不符合终止属性的要求。
|
||||
|
||||
当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数(majority)**的节点正确工作,以确保终止属性【67】。多数可以安全地组成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
|
||||
当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数(majority)**的节点正确工作,以确保终止属性【67】。多数可以安全地组成法定人数(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。
|
||||
|
||||
因此**终止**属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足—— 一致同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。
|
||||
|
||||
@ -781,7 +781,7 @@
|
||||
|
||||
最著名的容错共识算法是**视图戳复制(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】)
|
||||
|
||||
大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,并满足一致同意、完整性、有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。
|
||||
大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,并满足一致同意、完整性、有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(请参阅“[全序广播](#全序广播)”)。
|
||||
|
||||
请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息【67】。
|
||||
|
||||
@ -796,11 +796,11 @@
|
||||
|
||||
#### 单领导者复制和共识
|
||||
|
||||
在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢?
|
||||
在[第五章](ch5.md)中,我们讨论了单领导者复制(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢?
|
||||
|
||||
答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种**独裁类型**的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的**终止**属性,因为它需要人为干预才能取得**进展**。
|
||||
|
||||
一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。
|
||||
一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。
|
||||
|
||||
但是还有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,而单主复制需要一个领导者,那么...
|
||||
|
||||
@ -814,7 +814,7 @@
|
||||
|
||||
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真相由多数所定义](ch8.md#真相由多数所定义)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
|
||||
|
||||
相反,它必须从**法定人数(quorum)**的节点中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。
|
||||
相反,它必须从**法定人数(quorum)**的节点中获取选票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。
|
||||
|
||||
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。
|
||||
|
||||
@ -822,13 +822,13 @@
|
||||
|
||||
#### 共识的局限性
|
||||
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”)。
|
||||
共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(请参阅“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”)。
|
||||
|
||||
尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。
|
||||
|
||||
节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步复制与异步复制](ch5.md#同步复制与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
|
||||
|
||||
共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(参阅“[线性一致性的代价](#线性一致性的代价)”)。
|
||||
共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(请参阅“[线性一致性的代价](#线性一致性的代价)”)。
|
||||
|
||||
大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展(dynamic membership extension)**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。
|
||||
|
||||
@ -848,7 +848,7 @@
|
||||
|
||||
***线性一致性的原子操作***
|
||||
|
||||
使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)**的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)**的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
|
||||
***操作的全序排序***
|
||||
|
||||
@ -868,7 +868,7 @@
|
||||
|
||||
ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。
|
||||
|
||||
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
|
||||
另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅“[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
|
||||
|
||||
这类任务可以通过在ZooKeeper中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在ZooKeeper客户端API基础之上提供更高层工具的库,例如Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多,这样的尝试鲜有成功记录【107】。
|
||||
|
||||
@ -888,7 +888,7 @@
|
||||
|
||||
ZooKeeper和它的小伙伴们可以看作是成员资格服务(membership services)研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。
|
||||
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在[第八章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
|
||||
|
||||
即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,知道哪些节点构成了当前的成员关系是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有的成员都有谁有不同意见,则这种方法将不起作用。
|
||||
|
||||
@ -938,13 +938,13 @@
|
||||
|
||||
尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。
|
||||
|
||||
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。
|
||||
像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受[第八章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。
|
||||
|
||||
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(参见“[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
|
||||
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
|
||||
|
||||
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索。
|
||||
|
||||
这里已经到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统的故障模型([第8章](ch8.md))以及最后的一致性与共识([第9章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在[第三部分](part-iii.md)再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。
|
||||
这里已经到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第五章](ch5.md)),分区([第六章](ch6.md)),事务([第七章](ch7.md)),分布式系统的故障模型([第八章](ch8.md))以及最后的一致性与共识([第九章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在[第三部分](part-iii.md)再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。
|
||||
|
||||
|
||||
|
||||
|
28
glossary.md
28
glossary.md
@ -18,7 +18,7 @@
|
||||
|
||||
1.在并发操作的上下文中:描述一个在单个时间点看起来生效的操作,所以另一个并发进程永远不会遇到处于“半完成”状态的操作。另见隔离。
|
||||
|
||||
2.在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。参见“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”。
|
||||
2.在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。请参阅“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
### 边界(bounded)
|
||||
|
||||
有一些已知的上限或大小。例如,网络延迟情况(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”)和数据集(请参阅[第11章](ch11.md)的介绍)。
|
||||
有一些已知的上限或大小。例如,网络延迟情况(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”)和数据集(请参阅[第十一章](ch11.md)的介绍)。
|
||||
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
### CAP定理(CAP theorem)
|
||||
|
||||
一个被广泛误解的理论结果,在实践中是没有用的。参见“[CAP定理](ch9.md#CAP定理)”。
|
||||
一个被广泛误解的理论结果,在实践中是没有用的。请参阅“[CAP定理](ch9.md#CAP定理)”。
|
||||
|
||||
|
||||
|
||||
@ -84,13 +84,13 @@
|
||||
|
||||
### 非规范化(denormalize)
|
||||
|
||||
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参见“[单对象和多对象操作](ch7.md#单对象和多对象操作)”和“[从同一事件日志中派生多个视图](ch11.md#从同一事件日志中派生多个视图)”。
|
||||
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”和“[从同一事件日志中派生多个视图](ch11.md#从同一事件日志中派生多个视图)”。
|
||||
|
||||
|
||||
|
||||
### 衍生数据(derived data)
|
||||
|
||||
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。参见[第三部分](part-iii.md)的介绍。
|
||||
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。请参阅[第三部分](part-iii.md)的介绍。
|
||||
|
||||
|
||||
|
||||
@ -246,7 +246,7 @@
|
||||
|
||||
### 分区(partitioning)
|
||||
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。 也称为分片。见[第6章](ch6.md)。
|
||||
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。 也称为分片。见[第六章](ch6.md)。
|
||||
|
||||
|
||||
|
||||
@ -258,7 +258,7 @@
|
||||
|
||||
### 主键(primary key)
|
||||
|
||||
唯一标识记录的值(通常是数字或字符串)。 在许多应用程序中,主键由系统在创建记录时生成(例如,按顺序或随机); 它们通常不由用户设置。 另请参阅二级索引。
|
||||
唯一标识记录的值(通常是数字或字符串)。 在许多应用程序中,主键由系统在创建记录时生成(例如,按顺序或随机); 它们通常不由用户设置。 另请参阅次级索引。
|
||||
|
||||
|
||||
|
||||
@ -276,13 +276,13 @@
|
||||
|
||||
### 复制(replication)
|
||||
|
||||
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅[第5章](ch5.md)。
|
||||
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅[第五章](ch5.md)。
|
||||
|
||||
|
||||
|
||||
### 模式(schema)
|
||||
|
||||
一些数据结构的描述,包括其字段和数据类型。 可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第4章](ch4.md))。
|
||||
一些数据结构的描述,包括其字段和数据类型。 可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第四章](ch4.md))。
|
||||
|
||||
|
||||
|
||||
@ -294,7 +294,7 @@
|
||||
|
||||
### 可串行化(serializable)
|
||||
|
||||
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。 请参阅第7章的“[可串行化](ch7.md#可串行化)”。
|
||||
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。 请参阅第七章的“[可串行化](ch7.md#可串行化)”。
|
||||
|
||||
|
||||
|
||||
@ -326,7 +326,7 @@
|
||||
|
||||
### 流处理(stream process)
|
||||
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。 见[第11章](ch11.md)。
|
||||
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。 见[第十一章](ch11.md)。
|
||||
|
||||
|
||||
|
||||
@ -338,7 +338,7 @@
|
||||
|
||||
### 记录系统(system of record)
|
||||
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。 参见[第三部分](part-iii.md)的介绍。
|
||||
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。 请参阅[第三部分](part-iii.md)的介绍。
|
||||
|
||||
|
||||
|
||||
@ -350,13 +350,13 @@
|
||||
|
||||
### 全序(total order)
|
||||
|
||||
一种比较事物的方法(例如时间戳),可以让您总是说出两件事中哪一件更大,哪件更小。 总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。 请参见“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
|
||||
一种比较事物的方法(例如时间戳),可以让您总是说出两件事中哪一件更大,哪件更小。 总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。 请参阅“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
|
||||
|
||||
|
||||
|
||||
### 事务(transaction)
|
||||
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。 见[第7章](ch7.md)。
|
||||
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。 见[第七章](ch7.md)。
|
||||
|
||||
|
||||
|
||||
|
@ -62,11 +62,11 @@
|
||||
|
||||
本书分为三部分:
|
||||
|
||||
1. 在[第一部分](part-i.md)中,我们会讨论设计数据密集型应用所赖的基本思想。我们从[第1章](ch1.md)开始,讨论我们实际要达到的目标:可靠性,可伸缩性和可维护性;我们该如何思考这些概念;以及如何实现它们。在[第2章](ch2.md)中,我们比较了几种不同的数据模型和查询语言,看看它们如何适用于不同的场景。在[第3章](ch3.md)中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第4章](ch4.md)转向数据编码(序列化),以及随时间演化的模式。
|
||||
1. 在[第一部分](part-i.md)中,我们会讨论设计数据密集型应用所赖的基本思想。我们从[第一章](ch1.md)开始,讨论我们实际要达到的目标:可靠性,可伸缩性和可维护性;我们该如何思考这些概念;以及如何实现它们。在[第二章](ch2.md)中,我们比较了几种不同的数据模型和查询语言,看看它们如何适用于不同的场景。在[第三章](ch3.md)中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第四章](ch4.md)转向数据编码(序列化),以及随时间演化的模式。
|
||||
|
||||
2. 在[第二部分](part-ii.md)中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第5章](ch5.md)),分区/分片([第6章](ch6.md))和事务([第7章](ch7.md))。然后我们将探索关于分布式系统问题的更多细节([第8章](ch8.md)),以及在分布式系统中实现一致性与共识意味着什么([第9章](ch9.md))。
|
||||
2. 在[第二部分](part-ii.md)中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第五章](ch5.md)),分区/分片([第六章](ch6.md))和事务([第七章](ch7.md))。然后我们将探索关于分布式系统问题的更多细节([第八章](ch8.md)),以及在分布式系统中实现一致性与共识意味着什么([第九章](ch9.md))。
|
||||
|
||||
3. 在[第三部分](part-iii.md)中,我们讨论那些从其他数据集衍生出一些数据集的系统。衍生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库,缓存,索引等。在[第10章](ch10.md)中我们将从一种衍生数据的批处理方法开始,然后在此基础上建立在[第11章](ch11.md)中讨论的流处理。最后,在[第12章](ch12.md)中,我们将所有内容汇总,讨论在将来构建可靠,可伸缩和可维护的应用程序的方法。
|
||||
3. 在[第三部分](part-iii.md)中,我们讨论那些从其他数据集衍生出一些数据集的系统。衍生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库,缓存,索引等。在[第十章](ch10.md)中我们将从一种衍生数据的批处理方法开始,然后在此基础上建立在[第十一章](ch11.md)中讨论的流处理。最后,在[第十二章](ch12.md)中,我们将所有内容汇总,讨论在将来构建可靠,可伸缩和可维护的应用程序的方法。
|
||||
|
||||
|
||||
|
||||
|
@ -141,7 +141,7 @@
|
||||
0. 全文校訂 by [@yingang](https://github.com/yingang)
|
||||
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
|
||||
2. [第一章語法標點校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 與[第10章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 與[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
|
||||
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)
|
||||
@ -209,7 +209,7 @@
|
||||
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
|
||||
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
|
||||
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 刪除一個多餘的右括號 |
|
||||
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第7章底部連結錯誤 |
|
||||
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部連結錯誤 |
|
||||
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假簡單"->"更加簡單" |
|
||||
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修復 ch1 中的無序列表格式 |
|
||||
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 糾正多處的翻譯小錯誤 |
|
||||
|
@ -60,11 +60,11 @@
|
||||
|
||||
***可伸縮性(Scalability)***
|
||||
|
||||
有合理的辦法應對系統的增長(資料量、流量、複雜性)(參閱“[可伸縮性](#可伸縮性)”)
|
||||
有合理的辦法應對系統的增長(資料量、流量、複雜性)(請參閱“[可伸縮性](#可伸縮性)”)
|
||||
|
||||
***可維護性(Maintainability)***
|
||||
|
||||
許多不同的人(工程師、運維)在不同的生命週期,都能高效地在系統上工作(使系統保持現有行為,並適應新的應用場景)。(參閱”[可維護性](#可維護性)“)
|
||||
許多不同的人(工程師、運維)在不同的生命週期,都能高效地在系統上工作(使系統保持現有行為,並適應新的應用場景)。(請參閱”[可維護性](#可維護性)“)
|
||||
|
||||
|
||||
|
||||
@ -199,7 +199,7 @@
|
||||
|
||||
在推特的例子中,每個使用者粉絲數的分佈(可能按這些使用者的發推頻率來加權)是探討可伸縮性的一個關鍵負載引數,因為它決定了扇出負載。你的應用程式可能具有非常不同的特徵,但可以採用相似的原則來考慮它的負載。
|
||||
|
||||
推特軼事的最終轉折:現在已經穩健地實現了方法2,推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者(即名流)會被排除在外。當用戶讀取主頁時間線時,分別地獲取出該使用者所關注的每位名流的推文,再與使用者的主頁時間線快取合併,如方法1所示。這種混合方法能始終如一地提供良好效能。在[第12章](ch12.md)中我們將重新討論這個例子,這在覆蓋更多技術層面之後。
|
||||
推特軼事的最終轉折:現在已經穩健地實現了方法2,推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者(即名流)會被排除在外。當用戶讀取主頁時間線時,分別地獲取出該使用者所關注的每位名流的推文,再與使用者的主頁時間線快取合併,如方法1所示。這種混合方法能始終如一地提供良好效能。在[第十二章](ch12.md)中我們將重新討論這個例子,這在覆蓋更多技術層面之後。
|
||||
|
||||
### 描述效能
|
||||
|
||||
@ -264,7 +264,7 @@
|
||||
|
||||
人們經常討論**縱向伸縮(scaling up)**(**垂直伸縮(vertical scaling)**,轉向更強大的機器)和**橫向伸縮(scaling out)** (**水平伸縮(horizontal scaling)**,將負載分佈到多臺小機器上)之間的對立。跨多臺機器分配負載也稱為“**無共享(shared-nothing)**”架構。可以在單臺機器上執行的系統通常更簡單,但高階機器可能非常貴,所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合,因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。
|
||||
|
||||
有些系統是 **彈性(elastic)** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載**極難預測(highly unpredictable)**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”)。
|
||||
有些系統是 **彈性(elastic)** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載**極難預測(highly unpredictable)**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”)。
|
||||
|
||||
跨多臺機器部署 **無狀態服務(stateless services)** 非常簡單,但將帶狀態的資料系統從單節點變為分散式配置則可能引入許多額外複雜度。出於這個原因,常識告訴我們應該將資料庫放在單個節點上(縱向伸縮),直到伸縮成本或可用性需求迫使其改為分散式。
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
在本書的前兩部分中,我們討論了很多關於**請求**和**查詢**以及相應的**響應**或**結果**。許多現有資料系統中都採用這種資料處理方式:你傳送請求指令,一段時間後(我們期望)系統會給出一個結果。資料庫、快取、搜尋索引、Web伺服器以及其他一些系統都以這種方式工作。
|
||||
|
||||
像這樣的**線上(online)**系統,無論是瀏覽器請求頁面還是呼叫遠端API的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(參閱“[描述效能](ch1.md#描述效能)”)。
|
||||
像這樣的**線上(online)**系統,無論是瀏覽器請求頁面還是呼叫遠端API的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱“[描述效能](ch1.md#描述效能)”)。
|
||||
|
||||
Web和越來越多的基於HTTP/REST的API使互動的請求/響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統:
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
***流處理系統(準實時系統)***
|
||||
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)**或**準線上(nearline)**處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第11章](ch11.md)討論它。
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)**或**準線上(nearline)**處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。
|
||||
|
||||
正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004年釋出的批處理演算法Map-Reduce(可能被過分熱情地)被稱為“造就Google大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括Hadoop,CouchDB和MongoDB。
|
||||
|
||||
@ -124,7 +124,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
哪種方法更好?這取決於你有多少個不同的URL。對於大多數中小型網站,你可能可以為所有不同網址提供一個計數器(假設我們使用1GB記憶體)。在此例中,作業的**工作集(working set)**(作業需要隨機訪問的記憶體大小)僅取決於不同URL的數量:如果日誌中只有單個URL,重複出現一百萬次,則散列表所需的空間表就只有一個URL加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的膝上型電腦上也可以正常工作。
|
||||
|
||||
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。 歸併排序具有在磁碟上執行良好的順序訪問模式。 (請記住,針對順序I/O進行最佳化是[第3章](ch3.md)中反覆出現的主題,相同的模式在此重現)
|
||||
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。 歸併排序具有在磁碟上執行良好的順序訪問模式。 (請記住,針對順序I/O進行最佳化是[第三章](ch3.md)中反覆出現的主題,相同的模式在此重現)
|
||||
|
||||
GNU Coreutils(Linux)中的`sort `程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個CPU核進行並行排序【9】。這意味著我們之前看到的簡單的Unix命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。
|
||||
|
||||
@ -209,11 +209,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
[^iv]: 一個不同之處在於,對於HDFS,可以將計算任務安排在儲存特定檔案副本的計算機上執行,而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸,從本地磁碟讀取有效能優勢。但是請注意,如果使用糾刪碼(Erasure Coding),則會丟失區域性性,因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。
|
||||
|
||||
與網路連線儲存(NAS)和儲存區域網路(SAN)架構的共享磁碟方法相比,HDFS基於**無共享**原則(參見[第二部分](part-ii.md)的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
|
||||
與網路連線儲存(NAS)和儲存區域網路(SAN)架構的共享磁碟方法相比,HDFS基於**無共享**原則(請參閱[第二部分](part-ii.md)的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
|
||||
|
||||
HDFS在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為**NameNode**的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此,HDFS在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。
|
||||
|
||||
為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如[第5章](ch5.md)中所述,或者諸如Reed-Solomon碼這樣的糾刪碼方案,它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與RAID相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
|
||||
為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如[第五章](ch5.md)中所述,或者諸如Reed-Solomon碼這樣的糾刪碼方案,它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與RAID相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
|
||||
|
||||
HDFS的可伸縮性已經很不錯了:在撰寫本書時,最大的HDFS部署執行在上萬臺機器上,總儲存容量達數百PB【23】。如此大的規模已經變得可行,因為使用商品硬體和開源軟體的HDFS上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。
|
||||
|
||||
@ -228,7 +228,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
這四個步驟可以作為一個MapReduce作業執行。步驟2(Map)和4(Reduce)是你編寫自定義資料處理程式碼的地方。步驟1(將檔案分解成記錄)由輸入格式解析器處理。步驟3中的排序步驟隱含在MapReduce中 —— 你不必編寫它,因為Mapper的輸出始終在送往Reducer之前進行排序。
|
||||
|
||||
要建立MapReduce作業,你需要實現兩個回撥函式,Mapper和Reducer,其行為如下(參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”):
|
||||
要建立MapReduce作業,你需要實現兩個回撥函式,Mapper和Reducer,其行為如下(請參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”):
|
||||
|
||||
***Mapper***
|
||||
|
||||
@ -243,9 +243,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
MapReduce與Unix命令管道的主要區別在於,MapReduce可以在多臺機器上並行執行計算,而無需編寫程式碼來顯式處理並行問題。Mapper和Reducer一次只能處理一條記錄;它們不需要知道它們的輸入來自哪裡,或者輸出去往什麼地方,所以框架可以處理在機器之間移動資料的複雜性。
|
||||
|
||||
在分散式計算中可以使用標準的Unix工具作為Mapper和Reducer【25】,但更常見的是,它們被實現為傳統程式語言的函式。在Hadoop MapReduce中,Mapper和Reducer都是實現特定介面的Java類。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函式(參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”)。
|
||||
在分散式計算中可以使用標準的Unix工具作為Mapper和Reducer【25】,但更常見的是,它們被實現為傳統程式語言的函式。在Hadoop MapReduce中,Mapper和Reducer都是實現特定介面的Java類。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函式(請參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”)。
|
||||
|
||||
[圖10-1](../img/fig10-1.png)顯示了Hadoop MapReduce作業中的資料流。其並行化基於分割槽(參見[第6章](ch6.md)):作業的輸入通常是HDFS中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理map任務([圖10-1](../img/fig10-1.png)中的m1,m2和m3標記)。
|
||||
[圖10-1](../img/fig10-1.png)顯示了Hadoop MapReduce作業中的資料流。其並行化基於分割槽(請參閱[第六章](ch6.md)):作業的輸入通常是HDFS中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理map任務([圖10-1](../img/fig10-1.png)中的m1,m2和m3標記)。
|
||||
|
||||
每個輸入檔案的大小通常是數百兆位元組。 MapReduce排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個Mapper,只要該機器有足夠的備用RAM和CPU資源來執行Mapper任務【26】。這個原則被稱為**將計算放在資料附近**【27】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性性。
|
||||
|
||||
@ -255,7 +255,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在大多數情況下,應該在Mapper任務中執行的應用程式碼在將要執行它的機器上還不存在,所以MapReduce框架首先將程式碼(例如Java程式中的JAR檔案)複製到適當的機器。然後啟動Map任務並開始讀取輸入檔案,一次將一條記錄傳入Mapper回撥函式。Mapper的輸出由鍵值對組成。
|
||||
|
||||
計算的Reduce端也被分割槽。雖然Map任務的數量由輸入檔案塊的數量決定,但Reducer的任務的數量是由作業作者配置的(它可以不同於Map任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的Reducer處,框架使用鍵的雜湊值來確定哪個Reduce任務應該接收到特定的鍵值對(參見“[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”))。
|
||||
計算的Reduce端也被分割槽。雖然Map任務的數量由輸入檔案塊的數量決定,但Reducer的任務的數量是由作業作者配置的(它可以不同於Map任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的Reducer處,框架使用鍵的雜湊值來確定哪個Reduce任務應該接收到特定的鍵值對(請參閱“[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”))。
|
||||
|
||||
鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個Map任務都按照Reducer對輸出進行分割槽。每個分割槽都被寫入Mapper程式的本地磁碟,使用的技術與我們在“[SSTables與LSM樹](ch3.md#SSTables與LSM樹)”中討論的類似。
|
||||
|
||||
@ -281,21 +281,21 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
### Reduce側連線與分組
|
||||
|
||||
我們在[第2章](ch2.md)中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
|
||||
我們在[第二章](ch2.md)中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
|
||||
|
||||
在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的**外來鍵**,文件模型中的**文件引用**或圖模型中的**邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如[第2章](ch2.md)所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除[^v]。
|
||||
在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的**外來鍵**,文件模型中的**文件引用**或圖模型中的**邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如[第二章](ch2.md)所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除[^v]。
|
||||
|
||||
[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如ID)中具有**相同值**相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。
|
||||
|
||||
在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用**索引**來快速定位感興趣的記錄(參閱[第3章](ch3.md))。如果查詢涉及到連線,則可能涉及到查詢多個索引。然而MapReduce沒有索引的概念 —— 至少在通常意義上沒有。
|
||||
在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用**索引**來快速定位感興趣的記錄(請參閱[第三章](ch3.md))。如果查詢涉及到連線,則可能涉及到查詢多個索引。然而MapReduce沒有索引的概念 —— 至少在通常意義上沒有。
|
||||
|
||||
當MapReduce作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為**全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
|
||||
當MapReduce作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為**全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
|
||||
|
||||
當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。 例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
|
||||
|
||||
#### 示例:使用者活動事件分析
|
||||
|
||||
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件(activity events)**或**點選流資料(clickstream data)**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件(activity events)**或**點選流資料(clickstream data)**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
|
||||
![](../img/fig10-2.png)
|
||||
|
||||
@ -307,7 +307,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為**非確定的(nondeterministic)**,因為遠端資料庫中的資料可能會改變。
|
||||
|
||||
因此,更好的方法是獲取使用者資料庫的副本(例如,使用ETL程序從資料庫備份中提取資料,參閱“[資料倉庫](ch3.md#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在HDFS中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用MapReduce將所有相關記錄集中到同一個地方進行高效處理。
|
||||
因此,更好的方法是獲取使用者資料庫的副本(例如,使用ETL程序從資料庫備份中提取資料,請參閱“[資料倉庫](ch3.md#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在HDFS中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用MapReduce將所有相關記錄集中到同一個地方進行高效處理。
|
||||
|
||||
#### 排序合併連線
|
||||
|
||||
@ -349,13 +349,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
如果存在與單個鍵關聯的大量資料,則“將具有相同鍵的所有記錄放到相同的位置”這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為**關鍵物件(linchpin object)**【38】或**熱鍵(hot key)**。
|
||||
|
||||
在單個Reducer中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的**偏斜**(也稱為**熱點(hot spot)**)—— 也就是說,一個Reducer必須比其他Reducer處理更多的記錄(參見“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)“)。由於MapReduce作業只有在所有Mapper和Reducer都完成時才完成,所有後續作業必須等待最慢的Reducer才能啟動。
|
||||
在單個Reducer中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的**偏斜**(也稱為**熱點(hot spot)**)—— 也就是說,一個Reducer必須比其他Reducer處理更多的記錄(請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)“)。由於MapReduce作業只有在所有Mapper和Reducer都完成時才完成,所有後續作業必須等待最慢的Reducer才能啟動。
|
||||
|
||||
如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig中的**偏斜連線(skewed join)**方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper會將熱鍵的關聯記錄**隨機**(相對於傳統MapReduce基於鍵雜湊的確定性方法)傳送到幾個Reducer之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到**所有**處理該鍵的Reducer上【40】。
|
||||
|
||||
這種技術將處理熱鍵的工作分散到多個Reducer上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個Reducer上。 Crunch中的**分片連線(sharded join)**方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
|
||||
|
||||
Hive的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用Map端連線(參閱下一節)。
|
||||
Hive的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用Map端連線(請參閱下一節)。
|
||||
|
||||
當按照熱鍵進行分組並聚合時,可以將分組分兩個階段進行。第一個MapReduce階段將記錄傳送到隨機Reducer,以便每個Reducer只對熱鍵的子集執行分組,為每個鍵輸出一個更緊湊的中間聚合結果。然後第二個MapReduce作業將所有來自第一階段Reducer的中間聚合結果合併為每個鍵一個值。
|
||||
|
||||
@ -413,9 +413,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
我們已經說了很多用於實現MapReduce工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業?
|
||||
|
||||
在資料庫查詢的場景中,我們將事務處理(OLTP)與分析兩種目的區分開來(參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。我們看到,OLTP查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前10項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
|
||||
在資料庫查詢的場景中,我們將事務處理(OLTP)與分析兩種目的區分開來(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。我們看到,OLTP查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前10項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
|
||||
|
||||
批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而MapReduce作業工作流與用於分析目的的SQL查詢是不同的(參閱“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。
|
||||
批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而MapReduce作業工作流與用於分析目的的SQL查詢是不同的(請參閱“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。
|
||||
|
||||
#### 建立搜尋索引
|
||||
|
||||
@ -423,13 +423,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
我們在“[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”中簡要地瞭解了Lucene這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件ID列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名,糾正拼寫錯誤,解析同義詞等等 —— 但這個原則是成立的。
|
||||
|
||||
如果需要對一組固定文件執行全文搜尋,則批處理是一種構建索引的高效方法:Mapper根據需要對文件集合進行分割槽,每個Reducer構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)並行處理效果拔群。
|
||||
如果需要對一組固定文件執行全文搜尋,則批處理是一種構建索引的高效方法:Mapper根據需要對文件集合進行分割槽,每個Reducer構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)並行處理效果拔群。
|
||||
|
||||
由於按關鍵字查詢搜尋索引是隻讀操作,因而這些索引檔案一旦建立就是不可變的。
|
||||
|
||||
如果索引的文件集合發生更改,一種選擇是定期重跑整個索引工作流,並在完成後用新的索引檔案批次替換以前的索引檔案。如果只有少量的文件發生了變化,這種方法的計算成本可能會很高。但它的優點是索引過程很容易理解:文件進,索引出。
|
||||
|
||||
另一個選擇是,可以增量建立索引。如[第3章](ch3.md)中討論的,如果要在索引中新增,刪除或更新文件,Lucene會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在[第11章](ch11.md)中看到更多這種增量處理。
|
||||
另一個選擇是,可以增量建立索引。如[第三章](ch3.md)中討論的,如果要在索引中新增,刪除或更新文件,Lucene會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在[第十一章](ch11.md)中看到更多這種增量處理。
|
||||
|
||||
#### 鍵值儲存作為批處理輸出
|
||||
|
||||
@ -447,7 +447,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
更好的解決方案是在批處理作業**內**建立一個全新的資料庫,並將其作為檔案寫入分散式檔案系統中作業的輸出目錄,就像上節中的搜尋索引一樣。這些資料檔案一旦寫入就是不可變的,可以批次載入到處理只讀查詢的伺服器中。不少鍵值儲存都支援在MapReduce作業中構建資料庫檔案,包括Voldemort 【46】,Terrapin 【47】,ElephantDB 【48】和HBase批次載入【49】。
|
||||
|
||||
構建這些資料庫檔案是MapReduce的一種好用法:使用Mapper提取出鍵並按該鍵排序,已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的(檔案只能由批處理作業一次性寫入,然後就不可變),所以資料結構非常簡單。比如它們就不需要預寫式日誌(WAL,參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”)。
|
||||
構建這些資料庫檔案是MapReduce的一種好用法:使用Mapper提取出鍵並按該鍵排序,已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的(檔案只能由批處理作業一次性寫入,然後就不可變),所以資料結構非常簡單。比如它們就不需要預寫式日誌(WAL,請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”)。
|
||||
|
||||
將資料載入到Voldemort時,伺服器將繼續用舊資料檔案服務請求,同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成,伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題,它可以輕易回滾至舊檔案,因為它們仍然存在而且不可變【46】。
|
||||
|
||||
@ -463,7 +463,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
|
||||
- 與Unix工具類似,MapReduce作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。
|
||||
|
||||
在這些領域,在Unix上表現良好的設計原則似乎也適用於Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因為大多數Unix工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用`{print $7}`來提取URL)。在Hadoop上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如Avro(參閱“[Avro](ch4.md#Avro)”)和Parquet(參閱“[列儲存](ch3.md#列儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見[第4章](ch4.md))。
|
||||
在這些領域,在Unix上表現良好的設計原則似乎也適用於Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因為大多數Unix工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用`{print $7}`來提取URL)。在Hadoop上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如Avro(請參閱“[Avro](ch4.md#Avro)”)和Parquet(請參閱“[列儲存](ch3.md#列儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見[第四章](ch4.md))。
|
||||
|
||||
### Hadoop與分散式資料庫的對比
|
||||
|
||||
@ -481,11 +481,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【54】。
|
||||
|
||||
這個想法與資料倉庫類似(參閱“[資料倉庫](ch3.md#資料倉庫)”):將大型組織的各個部分的資料集中在一起是很有價值的,因為它可以跨越以前相互分離的資料集進行連線。 MPP資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為“**資料湖(data lake)**”或“**企業資料中心(enterprise data hub)**”【55】)。
|
||||
這個想法與資料倉庫類似(請參閱“[資料倉庫](ch3.md#資料倉庫)”):將大型組織的各個部分的資料集中在一起是很有價值的,因為它可以跨越以前相互分離的資料集進行連線。 MPP資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為“**資料湖(data lake)**”或“**企業資料中心(enterprise data hub)**”【55】)。
|
||||
|
||||
不加區分的資料轉儲轉移瞭解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式**方法【56】;參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為**壽司原則(sushi principle)**:“原始資料更好”【57】。
|
||||
不加區分的資料轉儲轉移瞭解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式**方法【56】;請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為**壽司原則(sushi principle)**:“原始資料更好”【57】。
|
||||
|
||||
因此,Hadoop經常被用於實現ETL過程(參閱“[資料倉庫](ch3.md#資料倉庫)”):事務處理系統中的資料以某種原始形式轉儲到分散式檔案系統中,然後編寫MapReduce作業來清理資料,將其轉換為關係形式,並將其匯入MPP資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。
|
||||
因此,Hadoop經常被用於實現ETL過程(請參閱“[資料倉庫](ch3.md#資料倉庫)”):事務處理系統中的資料以某種原始形式轉儲到分散式檔案系統中,然後編寫MapReduce作業來清理資料,將其轉換為關係形式,並將其匯入MPP資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。
|
||||
|
||||
#### 處理模型的多樣性
|
||||
|
||||
@ -499,7 +499,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在Hadoop方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。
|
||||
|
||||
Hadoop生態系統包括隨機訪問的OLTP資料庫,如HBase(參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”)和MPP風格的分析型資料庫,如Impala 【41】。 HBase與Impala都不使用MapReduce,但都使用HDFS進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
|
||||
Hadoop生態系統包括隨機訪問的OLTP資料庫,如HBase(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”)和MPP風格的分析型資料庫,如Impala 【41】。 HBase與Impala都不使用MapReduce,但都使用HDFS進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
|
||||
|
||||
#### 針對頻繁故障設計
|
||||
|
||||
@ -536,13 +536,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
但是,MapReduce執行模型本身也存在一些問題,這些問題並沒有透過增加另一個抽象層次而解決,而對於某些型別的處理,它表現得非常差勁。一方面,MapReduce非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。
|
||||
|
||||
在本章的其餘部分中,我們將介紹一些批處理方法。在[第11章](ch11.md)我們將轉向流處理,它可以看作是加速批處理的另一種方法。
|
||||
在本章的其餘部分中,我們將介紹一些批處理方法。在[第十一章](ch11.md)我們將轉向流處理,它可以看作是加速批處理的另一種方法。
|
||||
|
||||
### 物化中間狀態
|
||||
|
||||
如前所述,每個MapReduce作業都獨立於其他任何作業。作業與世界其他地方的主要連線點是分散式檔案系統上的輸入和輸出目錄。如果希望一個作業的輸出成為第二個作業的輸入,則需要將第二個作業的輸入目錄配置為第一個作業輸出目錄,且外部工作流排程程式必須在第一個作業完成後再啟動第二個。
|
||||
|
||||
如果第一個作業的輸出是要在組織內廣泛釋出的資料集,則這種配置是合理的。在這種情況下,你需要透過名稱引用它,並將其重用為多個不同作業的輸入(包括由其他團隊開發的作業)。將資料釋出到分散式檔案系統中眾所周知的位置能夠帶來**松耦合**,這樣作業就不需要知道是誰在提供輸入或誰在消費輸出(參閱“[邏輯與佈線相分離](#邏輯與佈線相分離)”)。
|
||||
如果第一個作業的輸出是要在組織內廣泛釋出的資料集,則這種配置是合理的。在這種情況下,你需要透過名稱引用它,並將其重用為多個不同作業的輸入(包括由其他團隊開發的作業)。將資料釋出到分散式檔案系統中眾所周知的位置能夠帶來**松耦合**,這樣作業就不需要知道是誰在提供輸入或誰在消費輸出(請參閱“[邏輯與佈線相分離](#邏輯與佈線相分離)”)。
|
||||
|
||||
但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的**中間狀態(intermediate state)**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由50或100個MapReduce作業組成的複雜工作流中,存在著很多這樣的中間狀態【29】。
|
||||
|
||||
@ -564,7 +564,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
與MapReduce不同,這些功能不需要嚴格扮演交織的Map與Reduce的角色,而是可以以更靈活的方式進行組合。我們稱這些函式為**運算元(operators)**,資料流引擎提供了幾種不同的選項來將一個運算元的輸出連線到另一個運算元的輸入:
|
||||
|
||||
- 一種選項是對記錄按鍵重新分割槽並排序,就像在MapReduce的混洗階段一樣(參閱“[分散式執行MapReduce](#分散式執行MapReduce)”)。這種功能可以用於實現排序合併連線和分組,就像在MapReduce中一樣。
|
||||
- 一種選項是對記錄按鍵重新分割槽並排序,就像在MapReduce的混洗階段一樣(請參閱“[分散式執行MapReduce](#分散式執行MapReduce)”)。這種功能可以用於實現排序合併連線和分組,就像在MapReduce中一樣。
|
||||
- 另一種可能是接受多個輸入,並以相同的方式進行分割槽,但跳過排序。當記錄的分割槽重要但順序無關緊要時,這省去了分割槽雜湊連線的工作,因為構建散列表還是會把順序隨機打亂。
|
||||
- 對於廣播雜湊連線,可以將一個運算元的輸出,傳送到連線運算元的所有分割槽。
|
||||
|
||||
@ -605,11 +605,11 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
### 圖與迭代處理
|
||||
|
||||
在“[圖資料模型](ch2.md#圖資料模型)”中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第2章](ch2.md)的討論集中在OLTP風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
|
||||
在“[圖資料模型](ch2.md#圖資料模型)”中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](ch2.md)的討論集中在OLTP風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
|
||||
|
||||
批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是PageRank 【69】,它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分,用於確定網路搜尋引擎呈現結果的順序。
|
||||
|
||||
> 像Spark,Flink和Tez這樣的資料流引擎(參見“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)**的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
|
||||
> 像Spark,Flink和Tez這樣的資料流引擎(請參閱“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)**的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
|
||||
|
||||
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**傳遞閉包(transitive closure)**)。
|
||||
|
||||
@ -629,13 +629,13 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫Reducer一樣。與MapReduce的不同之處在於,在Pregel模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被髮送訊息,那裡就不需要做任何工作。
|
||||
|
||||
這與Actor模型有些相似(參閱“[分散式的Actor框架](ch4.md#分散式的Actor框架)”),除了頂點狀態和頂點之間的訊息具有容錯性和永續性,且通訊以固定的回合進行:在每次迭代中,框架遞送上次迭代中傳送的所有訊息。Actor通常沒有這樣的時序保證。
|
||||
這與Actor模型有些相似(請參閱“[分散式的Actor框架](ch4.md#分散式的Actor框架)”),除了頂點狀態和頂點之間的訊息具有容錯性和永續性,且通訊以固定的回合進行:在每次迭代中,框架遞送上次迭代中傳送的所有訊息。Actor通常沒有這樣的時序保證。
|
||||
|
||||
#### 容錯
|
||||
|
||||
頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高Pregel作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於Pregel模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。
|
||||
|
||||
即使底層網路可能丟失、重複或任意延遲訊息(參閱“[不可靠的網路](ch8.md#不可靠的網路)”),Pregel的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像MapReduce一樣,框架能從故障中透明地恢復,以簡化在Pregel上實現演算法的程式設計模型。
|
||||
即使底層網路可能丟失、重複或任意延遲訊息(請參閱“[不可靠的網路](ch8.md#不可靠的網路)”),Pregel的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像MapReduce一樣,框架能從故障中透明地恢復,以簡化在Pregel上實現演算法的程式設計模型。
|
||||
|
||||
這種容錯是透過在迭代結束時,定期存檔所有頂點的狀態來實現的,即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失,則最簡單的解決方法是將整個圖計算回滾到上一個存檔點,然後重啟計算。如果演算法是確定性的,且訊息記錄在日誌中,那麼也可以選擇性地只恢復丟失的分割槽(就像之前討論過的資料流引擎)【72】。
|
||||
|
||||
@ -671,9 +671,9 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
但MapReduce及其資料流後繼者在其他方面,與SQL的完全宣告式查詢模型有很大區別。 MapReduce是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper或Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影象分析以及執行數值或統計演算法等。
|
||||
|
||||
自由執行任意程式碼,長期以來都是傳統MapReduce批處理系統與MPP資料庫的區別所在(參見“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
自由執行任意程式碼,長期以來都是傳統MapReduce批處理系統與MPP資料庫的區別所在(請參閱“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如Java的Maven、Javascript的npm以及Ruby的gems)。
|
||||
|
||||
然而資料流引擎已經發現,支援除連線之外的更多**宣告式特性**還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外CPU開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用面向列的儲存佈局(參閱“[列儲存](ch3.md#列儲存)”),只從磁碟讀取所需的列。 Hive、Spark DataFrames和Impala還使用了向量化執行(參閱“[記憶體頻寬和向量處理](ch3.md#記憶體頻寬和向量處理)”):在對CPU快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark生成JVM位元組碼【79】,Impala使用LLVM為這些內部迴圈生成本機程式碼【41】。
|
||||
然而資料流引擎已經發現,支援除連線之外的更多**宣告式特性**還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外CPU開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用面向列的儲存佈局(請參閱“[列儲存](ch3.md#列儲存)”),只從磁碟讀取所需的列。 Hive、Spark DataFrames和Impala還使用了向量化執行(請參閱“[記憶體頻寬和向量處理](ch3.md#記憶體頻寬和向量處理)”):在對CPU快取友好的內部迴圈中迭代資料,避免函式呼叫。Spark生成JVM位元組碼【79】,Impala使用LLVM為這些內部迴圈生成本機程式碼【41】。
|
||||
|
||||
透過在高階API中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像MPP資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。
|
||||
|
||||
|
@ -10,9 +10,9 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第10章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)**的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
|
||||
在[第十章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)**的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
|
||||
|
||||
然而,在[第10章](ch10.md)中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
|
||||
然而,在[第十章](ch10.md)中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
|
||||
|
||||
實際上,很多資料是**無界限**的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式“完成”【1】。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。
|
||||
|
||||
@ -27,11 +27,11 @@
|
||||
|
||||
在批處理領域,作業的輸入和輸出是檔案(也許在分散式檔案系統上)。流處理領域中的等價物看上去是什麼樣子的?
|
||||
|
||||
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(參見“[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。
|
||||
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱“[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。
|
||||
|
||||
例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或CPU利用率的週期性測量。在“[使用Unix工具的批處理](ch10.md#使用Unix工具的批處理)”的示例中,Web伺服器日誌的每一行都是一個事件。
|
||||
|
||||
事件可能被編碼為文字字串或JSON,或者某種二進位制編碼,如[第4章](ch4.md)所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
|
||||
事件可能被編碼為文字字串或JSON,或者某種二進位制編碼,如[第四章](ch4.md)所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
|
||||
|
||||
在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者(producer)** (也稱為 **釋出者(publisher)** 或 **傳送者(sender)** )生成一次,然後可能由多個 **消費者(consumer)** ( **訂閱者(subscribers)** 或 **接收者(recipients)** )進行處理【3】。在檔案系統中,檔名標識一組相關記錄;在流式系統中,相關的事件通常被聚合為一個 **主題(topic)** 或 **流(stream)** 。
|
||||
|
||||
@ -50,15 +50,15 @@
|
||||
|
||||
在這個**釋出/訂閱**模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
|
||||
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?**一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control)**;即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(參見“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?**一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control)**;即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
|
||||
如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能【6】?
|
||||
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?**與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(參閱“[複製和永續性](ch7.md#複製和永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?**與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(請參閱“[複製和永續性](ch7.md#複製和永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
|
||||
是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
|
||||
|
||||
我們在[第10章](ch10.md)中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
|
||||
我們在[第十章](ch10.md)中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
|
||||
|
||||
#### 直接從生產者傳遞給消費者
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
* UDP組播廣泛應用於金融行業,例如股票市場,其中低時延非常重要【8】。雖然UDP本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
|
||||
* 無代理的訊息庫,如ZeroMQ 【9】和nanomsg採取類似的方法,透過TCP或IP多播實現釋出/訂閱訊息傳遞。
|
||||
* StatsD 【10】和Brubeck 【7】使用不可靠的UDP訊息傳遞來收集網路中所有機器的指標並對其進行監控。 (在StatsD協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用UDP將使得指標處在一種最佳近似狀態【11】。另請參閱“[TCP與UDP](ch8.md#TCP與UDP)”
|
||||
* 如果消費者在網路上公開了服務,生產者可以直接傳送HTTP或RPC請求(參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”)將訊息推送給使用者。這就是webhooks背後的想法【12】,一種服務的回撥URL被註冊到另一個服務中,並且每當事件發生時都會向該URL發出請求。
|
||||
* 如果消費者在網路上公開了服務,生產者可以直接傳送HTTP或RPC請求(請參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”)將訊息推送給使用者。這就是webhooks背後的想法【12】,一種服務的回撥URL被註冊到另一個服務中,並且每當事件發生時都會向該URL發出請求。
|
||||
|
||||
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
#### 訊息代理與資料庫的對比
|
||||
|
||||
有些訊息代理甚至可以使用XA或JTA參與兩階段提交協議(參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
|
||||
有些訊息代理甚至可以使用XA或JTA參與兩階段提交協議(請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
|
||||
|
||||
* 資料庫通常保留資料直至顯式刪除,而大多數訊息代理在訊息成功遞送給消費者時會自動刪除訊息。這樣的訊息代理不適合長期的資料儲存。
|
||||
* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小—— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化【6】。
|
||||
@ -130,7 +130,7 @@
|
||||
|
||||
資料庫和檔案系統採用截然相反的方法論:至少在某人顯式刪除前,通常寫入資料庫或檔案的所有內容都要被永久記錄下來。
|
||||
|
||||
這種思維方式上的差異對建立衍生資料的方式有巨大影響。如[第10章](ch10.md)所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。
|
||||
這種思維方式上的差異對建立衍生資料的方式有巨大影響。如[第十章](ch10.md)所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。
|
||||
|
||||
如果你將新的消費者新增到訊息傳遞系統,通常只能接收到消費者註冊之後開始傳送的訊息。先前的任何訊息都隨風而逝,一去不復返。作為對比,你可以隨時為檔案和資料庫新增新的客戶端,且能讀取任意久遠的資料(只要應用沒有顯式覆蓋或刪除這些資料)。
|
||||
|
||||
@ -138,11 +138,11 @@
|
||||
|
||||
#### 使用日誌進行訊息儲存
|
||||
|
||||
日誌只是磁碟上簡單的僅追加記錄序列。我們先前在[第3章](ch3.md)中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在[第5章](ch5.md)複製的上下文裡也討論了它。
|
||||
日誌只是磁碟上簡單的僅追加記錄序列。我們先前在[第三章](ch3.md)中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在[第五章](ch5.md)複製的上下文裡也討論了它。
|
||||
|
||||
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix工具`tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
|
||||
|
||||
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(按[第6章](ch6.md)的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。
|
||||
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(按[第六章](ch6.md)的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。
|
||||
|
||||
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或**偏移量(offset)**(在[圖11-3](../img/fig11-3.png)中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
|
||||
|
||||
@ -152,7 +152,7 @@
|
||||
|
||||
Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的DistributedLog 【20,21】都是基於日誌的訊息代理。 Google Cloud Pub/Sub在架構上類似,但對外暴露的是JMS風格的API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。
|
||||
|
||||
#### 日誌與傳統訊息相比
|
||||
#### 日誌與傳統的訊息傳遞相比
|
||||
|
||||
基於日誌的方法天然支援扇出式訊息傳遞,因為多個消費者可以獨立讀取日誌,而不會相互影響 —— 讀取訊息不會將其從日誌中刪除。為了在一組消費者之間實現負載平衡,代理可以將整個分割槽分配給消費者組中的節點,而不是將單條訊息分配給消費者客戶端。
|
||||
|
||||
@ -209,7 +209,7 @@
|
||||
|
||||
我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是**寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。
|
||||
|
||||
事實上,複製日誌(參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
|
||||
事實上,複製日誌(請參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
|
||||
|
||||
我們還在“[全序廣播](ch9.md#全序廣播)”中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用OLTP資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。
|
||||
|
||||
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由ETL程序執行(參見“[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
|
||||
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由ETL程序執行(請參閱“[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
|
||||
|
||||
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是**雙寫(dual write)**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。
|
||||
|
||||
@ -231,9 +231,9 @@
|
||||
|
||||
除非有一些額外的併發檢測機制,例如我們在“[檢測併發寫入](ch5.md#檢測併發寫入)”中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
|
||||
|
||||
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(參閱“[原子提交與兩階段提交(2PC)](ch7.md#原子提交與兩階段提交(2PC))”)。
|
||||
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱“[原子提交與兩階段提交(2PC)](ch7.md#原子提交與兩階段提交(2PC))”)。
|
||||
|
||||
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(參見“[多主複製](ch5.md#多主複製)“)。
|
||||
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱“[多主複製](ch5.md#多主複製)“)。
|
||||
|
||||
如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎?
|
||||
|
||||
@ -257,11 +257,11 @@
|
||||
|
||||
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了[圖11-2](../img/fig11-2.png)的重新排序問題)。
|
||||
|
||||
資料庫觸發器可用來實現變更資料捕獲(參閱“[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
|
||||
資料庫觸發器可用來實現變更資料捕獲(請參閱“[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
|
||||
|
||||
LinkedIn的Databus【25】,Facebook的Wormhole【26】和Yahoo!的Sherpa【27】大規模地應用這個思路。 Bottled Water使用解碼WAL的API實現了PostgreSQL的CDC【28】,Maxwell和Debezium透過解析binlog對MySQL做了類似的事情【29,30,31】,Mongoriver讀取MongoDB oplog【32,33】,而GoldenGate為Oracle提供類似的功能【34,35】。
|
||||
|
||||
像訊息代理一樣,變更資料捕獲通常是非同步的:記錄資料庫系統不會等待消費者應用變更再進行提交。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(參見“[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
像訊息代理一樣,變更資料捕獲通常是非同步的:記錄資料庫系統不會等待消費者應用變更再進行提交。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
|
||||
#### 初始快照
|
||||
|
||||
@ -275,7 +275,7 @@
|
||||
|
||||
如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但**日誌壓縮(log compaction)** 提供了一個很好的備選方案。
|
||||
|
||||
我們之前在“[雜湊索引](ch3.md#雜湊索引)”中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(參見[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
|
||||
我們之前在“[雜湊索引](ch3.md#雜湊索引)”中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
|
||||
|
||||
在日誌結構儲存引擎中,具有特殊值NULL(**墓碑(tombstone)**)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
|
||||
|
||||
@ -306,7 +306,7 @@
|
||||
|
||||
例如,儲存“學生取消選課”事件以中性的方式清楚地表達了單個行為的意圖,而其副作用“從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表“則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如“將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
|
||||
|
||||
事件溯源類似於**編年史(chronicle)**資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
事件溯源類似於**編年史(chronicle)**資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
|
||||
諸如Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
|
||||
|
||||
@ -337,7 +337,7 @@
|
||||
|
||||
### 狀態、流和不變性
|
||||
|
||||
我們在[第10章](ch10.md)中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
|
||||
我們在[第十章](ch10.md)中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
|
||||
|
||||
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢?
|
||||
|
||||
@ -372,13 +372,13 @@ $$
|
||||
|
||||
#### 從同一事件日誌中派生多個檢視
|
||||
|
||||
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖11-5](../img/fig11-5.png)):例如,分析型資料庫Druid使用這種方式直接從Kafka攝取資料【55】,Pistachio是一個分散式的鍵值儲存,使用Kafka作為提交日誌【56】,Kafka Connect能將來自Kafka的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(參閱“[保持系統同步](#保持系統同步)”)。
|
||||
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖11-5](../img/fig11-5.png)):例如,分析型資料庫Druid使用這種方式直接從Kafka攝取資料【55】,Pistachio是一個分散式的鍵值儲存,使用Kafka作為提交日誌【56】,Kafka Connect能將來自Kafka的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(請參閱“[保持系統同步](#保持系統同步)”)。
|
||||
|
||||
新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行復雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源【47,57】。
|
||||
|
||||
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(參見[第3章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為**命令查詢責任分離(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱[第三章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為**命令查詢責任分離(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
|
||||
資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關規範化和非規範化的爭論就變得無關緊要了(參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行非規範化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
|
||||
資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關規範化和非規範化的爭論就變得無關緊要了(請參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行非規範化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
|
||||
|
||||
在“[描述負載](ch1.md#描述負載)”中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是**針對讀取最佳化的狀態**的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
|
||||
|
||||
@ -388,13 +388,13 @@ $$
|
||||
|
||||
一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要**事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中討論的方法。
|
||||
|
||||
另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
|
||||
另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
|
||||
|
||||
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽3中的客戶事件只需要更新分割槽3中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(參閱“[真的序列執行](ch7.md#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在[第12章](ch12.md)討論。
|
||||
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽3中的客戶事件只需要更新分割槽3中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱“[真的序列執行](ch7.md#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在[第十二章](ch12.md)討論。
|
||||
|
||||
#### 不變性的限制
|
||||
|
||||
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(參見“[索引和快照隔離](ch7.md#索引和快照隔離)” )。 Git,Mercurial和Fossil等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
|
||||
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱“[索引和快照隔離](ch7.md#索引和快照隔離)” )。 Git,Mercurial和Fossil等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
|
||||
|
||||
永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新/刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。
|
||||
|
||||
@ -416,9 +416,9 @@ $$
|
||||
2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可實時顯示的儀表板上。在這種情況下,人是流的最終消費者。
|
||||
3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項1或2)。
|
||||
|
||||
在本章的剩餘部分中,我們將討論選項3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為**運算元(operator)**或**作業(job)**。它與我們在[第10章](ch10.md)中討論過的Unix程序和MapReduce作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
|
||||
在本章的剩餘部分中,我們將討論選項3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為**運算元(operator)**或**作業(job)**。它與我們在[第十章](ch10.md)中討論過的Unix程序和MapReduce作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
|
||||
|
||||
流處理中的分割槽和並行化模式也非常類似於[第10章](ch10.md)中介紹的MapReduce和資料流引擎,因此我們不再重複這些主題。基本的Map操作(如轉換和過濾記錄)也是一樣的。
|
||||
流處理中的分割槽和並行化模式也非常類似於[第十章](ch10.md)中介紹的MapReduce和資料流引擎,因此我們不再重複這些主題。基本的Map操作(如轉換和過濾記錄)也是一樣的。
|
||||
|
||||
與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用**排序合併連線**(請參閱“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
|
||||
|
||||
@ -459,7 +459,7 @@ $$
|
||||
|
||||
#### 維護物化檢視
|
||||
|
||||
我們在“[資料庫與流](#資料庫與流)”中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護**物化檢視(materialized view)** 的一種具體場景(參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。
|
||||
我們在“[資料庫與流](#資料庫與流)”中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護**物化檢視(materialized view)** 的一種具體場景(請參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。
|
||||
|
||||
同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的**所有**事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱“[日誌壓縮](#日誌壓縮)“)。實際上,你需要一個可以一直延伸到時間開端的視窗。
|
||||
|
||||
@ -481,7 +481,7 @@ $$
|
||||
* Actor之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。
|
||||
* Actor可以以任意方式進行通訊(包括迴圈的請求/響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
|
||||
|
||||
也就是說,RPC類系統與流處理之間有一些交叉領域。例如,Apache Storm有一個稱為**分散式RPC**的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另參閱“[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。
|
||||
也就是說,RPC類系統與流處理之間有一些交叉領域。例如,Apache Storm有一個稱為**分散式RPC**的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另請參閱“[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。
|
||||
|
||||
也可以使用Actor框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。
|
||||
|
||||
@ -491,13 +491,13 @@ $$
|
||||
|
||||
在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
|
||||
|
||||
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(參閱“[容錯](ch10.md#容錯)”)。
|
||||
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱“[容錯](ch10.md#容錯)”)。
|
||||
|
||||
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間(processing time)**)來確定**視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
|
||||
|
||||
#### 事件時間與處理時間
|
||||
|
||||
很多原因都可能導致處理延遲:排隊,網路故障(參閱“[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理/訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(參閱“[重播舊訊息](#重播舊訊息)”),或者在修復程式碼BUG之後。
|
||||
很多原因都可能導致處理延遲:排隊,網路故障(請參閱“[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理/訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱“[重播舊訊息](#重播舊訊息)”),或者在修復程式碼BUG之後。
|
||||
|
||||
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個Web請求(由Web伺服器A處理),然後發出第二個請求(由伺服器B處理)。 A和B發出描述它們所處理請求的事件,但是B的事件在A的事件發生之前到達訊息代理。現在,流處理器將首先看到B事件,然後看到A事件,即使它們實際上是以相反的順序發生的。
|
||||
|
||||
@ -528,7 +528,7 @@ $$
|
||||
|
||||
當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。
|
||||
|
||||
在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(參見“[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
|
||||
在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱“[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
|
||||
|
||||
要校正不正確的裝置時鐘,一種方法是記錄三個時間戳【82】:
|
||||
|
||||
@ -558,11 +558,11 @@ $$
|
||||
|
||||
***會話視窗(Session window)***
|
||||
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(參閱“[分組](ch10.md#分組)”)。
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱“[分組](ch10.md#分組)”)。
|
||||
|
||||
### 流式連線
|
||||
### 流連線
|
||||
|
||||
在[第10章](ch10.md)中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
|
||||
在[第十章](ch10.md)中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
|
||||
|
||||
然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流-流**連線,**流-表**連線,與**表-表**連線【84】。我們將在下面的章節中透過例子來說明。
|
||||
|
||||
@ -631,7 +631,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
### 容錯
|
||||
|
||||
在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在[第10章](ch10.md)中看到,批處理框架可以很容易地容錯:如果MapReduce作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到HDFS上的獨立檔案中,而輸出僅當任務成功完成後可見。
|
||||
在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在[第十章](ch10.md)中看到,批處理框架可以很容易地容錯:如果MapReduce作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到HDFS上的獨立檔案中,而輸出僅當任務成功完成後可見。
|
||||
|
||||
特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為**恰好一次語義(exactly-once semantics)**,儘管**有效一次(effectively-once)** 可能會是一個更寫實的術語【90】。
|
||||
|
||||
@ -651,9 +651,9 @@ GROUP BY follows.follower_id
|
||||
|
||||
為了在出現故障時表現出恰好處理一次的樣子,我們需要確保事件處理的所有輸出和副作用**當且僅當**處理成功時才會生效。這些影響包括髮送給下游運算元或外部訊息傳遞系統(包括電子郵件或推送通知)的任何訊息,任何資料庫寫入,對運算元狀態的任何變更,以及對輸入訊息的任何確認(包括在基於日誌的訊息代理中將消費者偏移量前移)。
|
||||
|
||||
這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(參閱“[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。
|
||||
這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱“[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。
|
||||
|
||||
在[第9章](ch9.md)中,我們討論了分散式事務傳統實現中的問題(如XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。 Google Cloud Dataflow【81,92】和VoltDB 【94】中使用了這種方法,Apache Kafka有計劃加入類似的功能【95,96】。與XA不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
|
||||
在[第九章](ch9.md)中,我們討論了分散式事務傳統實現中的問題(如XA)。然而在限制更為嚴苛的環境中,也是有可能高效實現這種原子提交機制的。 Google Cloud Dataflow【81,92】和VoltDB 【94】中使用了這種方法,Apache Kafka有計劃加入類似的功能【95,96】。與XA不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
|
||||
|
||||
#### 冪等性
|
||||
|
||||
@ -665,7 +665,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。
|
||||
|
||||
當從一個處理節點故障切換到另一個節點時,可能需要進行**防護(fencing)**(參閱“[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現**恰好一次語義**的有效方式,僅需很小的額外開銷。
|
||||
當從一個處理節點故障切換到另一個節點時,可能需要進行**防護(fencing)**(請參閱“[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現**恰好一次語義**的有效方式,僅需很小的額外開銷。
|
||||
|
||||
#### 失敗後重建狀態
|
||||
|
||||
@ -673,9 +673,9 @@ GROUP BY follows.follower_id
|
||||
|
||||
一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在“[流表連線(流擴充)](#流表連線(流擴充))”中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
|
||||
|
||||
例如,Flink定期捕獲運算元狀態的快照,並將它們寫入HDFS等持久儲存中【92,93】。 Samza和Kafka Streams透過將狀態變更傳送到具有日誌壓縮功能的專用Kafka主題來複制狀態變更,這與變更資料捕獲類似【84,100】。 VoltDB透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(參閱“[真的序列執行](ch7.md#真的序列執行)”)。
|
||||
例如,Flink定期捕獲運算元狀態的快照,並將它們寫入HDFS等持久儲存中【92,93】。 Samza和Kafka Streams透過將狀態變更傳送到具有日誌壓縮功能的專用Kafka主題來複制狀態變更,這與變更資料捕獲類似【84,100】。 VoltDB透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱“[真的序列執行](ch7.md#真的序列執行)”)。
|
||||
|
||||
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(參閱“[日誌壓縮](#日誌壓縮)”)。
|
||||
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱“[日誌壓縮](#日誌壓縮)”)。
|
||||
|
||||
然而,所有這些權衡取決於底層基礎架構的效能特徵:在某些系統中,網路延遲可能低於磁碟訪問延遲,網路頻寬也可能與磁碟頻寬相當。沒有針對所有情況的普適理想權衡,隨著儲存和網路技術的發展,本地狀態與遠端狀態的優點也可能會互換。
|
||||
|
||||
@ -683,7 +683,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
## 本章小結
|
||||
|
||||
在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在[第10章](ch10.md) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
|
||||
在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在[第十章](ch10.md) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
|
||||
|
||||
我們花了一些時間比較兩種訊息代理:
|
||||
|
||||
@ -695,7 +695,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
|
||||
|
||||
基於日誌的方法與資料庫中的複製日誌(參見[第5章](ch5.md))和日誌結構儲存引擎(請參閱[第3章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
|
||||
基於日誌的方法與資料庫中的複製日誌(請參閱[第五章](ch5.md))和日誌結構儲存引擎(請參閱[第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
|
||||
|
||||
就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和Feed資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。
|
||||
|
||||
|
26
zh-tw/ch2.md
26
zh-tw/ch2.md
@ -26,7 +26,7 @@
|
||||
|
||||
掌握一個數據模型需要花費很多精力(想想關係資料建模有多少本書)。即便只使用一個數據模型,不用操心其內部工作機制,構建軟體也是非常困難的。然而,因為資料模型對上層軟體的功能(能做什麼,不能做什麼)有著至深的影響,所以選擇一個適合的資料模型是非常重要的。
|
||||
|
||||
在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第2點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在第3章中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第3點)。
|
||||
在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第2點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在[第三章](ch3.md)中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第3點)。
|
||||
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
* 後續的SQL標準增加了對結構化資料型別和XML資料的支援;這允許將多值資料儲存在單行內,並支援在這些文件內查詢和索引。這些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支援【6,7】。JSON資料型別也得到多個數據庫的支援,包括IBM DB2,MySQL和PostgreSQL 【8】。
|
||||
* 第三種選擇是將職業,教育和聯絡資訊編碼為JSON或XML文件,將其儲存在資料庫的文字列中,並讓應用程式解析其結構和內容。這種配置下,通常不能使用資料庫來查詢該編碼列中的值。
|
||||
|
||||
對於一個像簡歷這樣自包含文件的資料結構而言,JSON表示是非常合適的:參見[例2-1]()。JSON比XML更簡單。面向文件的資料庫(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支援這種資料模型。
|
||||
對於一個像簡歷這樣自包含文件的資料結構而言,JSON表示是非常合適的:請參閱[例2-1]()。JSON比XML更簡單。面向文件的資料庫(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支援這種資料模型。
|
||||
|
||||
**例2-1. 用JSON文件表示一個LinkedIn簡介**
|
||||
|
||||
@ -117,7 +117,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
有一些開發人員認為JSON模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。不過,正如我們將在[第4章](ch4.md)中看到的那樣,JSON作為資料編碼格式也存在問題。缺乏一個模式往往被認為是一個優勢;我們將在“[文件模型中的模式靈活性](#文件模型中的模式靈活性)”中討論這個問題。
|
||||
有一些開發人員認為JSON模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。不過,正如我們將在[第四章](ch4.md)中看到的那樣,JSON作為資料編碼格式也存在問題。缺乏一個模式往往被認為是一個優勢;我們將在“[文件模型中的模式靈活性](#文件模型中的模式靈活性)”中討論這個問題。
|
||||
|
||||
JSON表示比[圖2-1](../img/fig2-1.png)中的多表模式具有更好的**區域性性(locality)**。如果在前面的關係型示例中獲取簡介,那需要執行多個查詢(透過`user_id`查詢每個表),或者在User表與其下屬表之間混亂地執行多路連線。而在JSON表示中,所有相關資訊都在同一個地方,一個查詢就足夠了。
|
||||
|
||||
@ -157,7 +157,7 @@ JSON表示比[圖2-1](../img/fig2-1.png)中的多表模式具有更好的**區
|
||||
|
||||
***組織和學校作為實體***
|
||||
|
||||
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(參見[圖2-3](../img/fig2-3.png),來自LinkedIn的一個例子)。
|
||||
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(請參閱[圖2-3](../img/fig2-3.png),來自LinkedIn的一個例子)。
|
||||
|
||||
***推薦***
|
||||
|
||||
@ -220,7 +220,7 @@ CODASYL中的查詢是透過利用遍歷記錄列和跟隨訪問路徑表在資
|
||||
|
||||
### 關係型資料庫與文件資料庫在今日的對比
|
||||
|
||||
將關係資料庫與文件資料庫進行比較時,可以考慮許多方面的差異,包括它們的容錯屬性(參閱[第5章](ch5.md))和處理併發性(參閱[第7章](ch7.md))。本章將只關注資料模型中的差異。
|
||||
將關係資料庫與文件資料庫進行比較時,可以考慮許多方面的差異,包括它們的容錯屬性(請參閱[第五章](ch5.md))和處理併發性(請參閱[第七章](ch7.md))。本章將只關注資料模型中的差異。
|
||||
|
||||
支援文件資料模型的主要論據是架構靈活性,因區域性性而擁有更好的效能,以及對於某些應用程式而言更接近於應用程式使用的資料結構。關係模型透過為連線提供更好的支援以及支援多對一和多對多的關係來反擊。
|
||||
|
||||
@ -234,7 +234,7 @@ CODASYL中的查詢是透過利用遍歷記錄列和跟隨訪問路徑表在資
|
||||
|
||||
但如果你的應用程式確實會用到多對多關係,那麼文件模型就沒有那麼誘人了。儘管可以透過反規範化來消除對連線的需求,但這需要應用程式程式碼來做額外的工作以確保資料一致性。儘管應用程式程式碼可以透過向資料庫發出多個請求的方式來模擬連線,但這也將複雜性轉移到應用程式中,而且通常也會比由資料庫內的專用程式碼更慢。在這種情況下,使用文件模型可能會導致更復雜的應用程式碼與更差的效能【15】。
|
||||
|
||||
我們沒有辦法說哪種資料模型更有助於簡化應用程式碼,因為它取決於資料項之間的關係種類。對高度關聯的資料而言,文件模型是極其糟糕的,關係模型是可以接受的,而選用圖形模型(參見“[圖資料模型](#圖資料模型)”)是最自然的。
|
||||
我們沒有辦法說哪種資料模型更有助於簡化應用程式碼,因為它取決於資料項之間的關係種類。對高度關聯的資料而言,文件模型是極其糟糕的,關係模型是可以接受的,而選用圖形模型(請參閱“[圖資料模型](#圖資料模型)”)是最自然的。
|
||||
|
||||
#### 文件模型中的模式靈活性
|
||||
|
||||
@ -280,7 +280,7 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
|
||||
|
||||
值得指出的是,為了區域性性而分組集合相關資料的想法並不侷限於文件模型。例如,Google的Spanner資料庫在關係資料模型中提供了同樣的區域性性屬性,允許模式宣告一個表的行應該交錯(巢狀)在父表內【27】。Oracle類似地允許使用一個稱為 **多表索引叢集表(multi-table index cluster tables)** 的類似特性【28】。Bigtable資料模型(用於Cassandra和HBase)中的 **列族(column-family)** 概念與管理區域性性的目的類似【29】。
|
||||
|
||||
在[第3章](ch3.md)將還會看到更多關於區域性性的內容。
|
||||
在[第三章](ch3.md)將還會看到更多關於區域性性的內容。
|
||||
|
||||
#### 文件和關係資料庫的融合
|
||||
|
||||
@ -419,7 +419,7 @@ for (var i = 0; i < liElements.length; i++) {
|
||||
|
||||
MapReduce是一個由Google推廣的程式設計模型,用於在多臺機器上批次處理大規模的資料【33】。一些NoSQL資料儲存(包括MongoDB和CouchDB)支援有限形式的MapReduce,作為在多個文件中執行只讀查詢的機制。
|
||||
|
||||
MapReduce將[第10章](ch10.md)中有更詳細的描述。現在我們將簡要討論一下MongoDB使用的模型。
|
||||
MapReduce將[第十章](ch10.md)中有更詳細的描述。現在我們將簡要討論一下MongoDB使用的模型。
|
||||
|
||||
MapReduce既不是一個宣告式的查詢語言,也不是一個完全命令式的查詢API,而是處於兩者之間:查詢的邏輯用程式碼片段來表示,這些程式碼片段會被處理框架重複性呼叫。它基於`map`(也稱為`collect`)和`reduce`(也稱為`fold`或`inject`)函式,兩個函式存在於許多函數語言程式設計語言中。
|
||||
|
||||
@ -487,7 +487,7 @@ db.observations.mapReduce(function map() {
|
||||
|
||||
map和reduce函式在功能上有所限制:它們必須是**純**函式,這意味著它們只使用傳遞給它們的資料作為輸入,它們不能執行額外的資料庫查詢,也不能有任何副作用。這些限制允許資料庫以任何順序執行任何功能,並在失敗時重新執行它們。然而,map和reduce函式仍然是強大的:它們可以解析字串,呼叫庫函式,執行計算等等。
|
||||
|
||||
MapReduce是一個相當底層的程式設計模型,用於計算機叢集上的分散式執行。像SQL這樣的更高階的查詢語言可以用一系列的MapReduce操作來實現(見[第10章](ch10.md)),但是也有很多不使用MapReduce的分散式SQL實現。請注意,SQL中沒有任何內容限制它在單個機器上執行,而MapReduce在分散式查詢執行上沒有壟斷權。
|
||||
MapReduce是一個相當底層的程式設計模型,用於計算機叢集上的分散式執行。像SQL這樣的更高階的查詢語言可以用一系列的MapReduce操作來實現(見[第十章](ch10.md)),但是也有很多不使用MapReduce的分散式SQL實現。請注意,SQL中沒有任何內容限制它在單個機器上執行,而MapReduce在分散式查詢執行上沒有壟斷權。
|
||||
|
||||
能夠在查詢中使用JavaScript程式碼是高階查詢的一個重要特性,但這不限於MapReduce,一些SQL資料庫也可以用JavaScript函式進行擴充套件【34】。
|
||||
|
||||
@ -539,7 +539,7 @@ db.observations.aggregate([
|
||||
|
||||
**圖2-5 圖資料結構示例(框代表頂點,箭頭代表邊)**
|
||||
|
||||
有幾種不同但相關的方法用來構建和查詢圖表中的資料。在本節中,我們將討論屬性圖模型(由Neo4j,Titan和InfiniteGraph實現)和三元組儲存(triple-store)模型(由Datomic,AllegroGraph等實現)。我們將檢視圖的三種宣告式查詢語言:Cypher,SPARQL和Datalog。除此之外,還有像Gremlin 【36】這樣的圖形查詢語言和像Pregel這樣的圖形處理框架(見[第10章](ch10.md))。
|
||||
有幾種不同但相關的方法用來構建和查詢圖表中的資料。在本節中,我們將討論屬性圖模型(由Neo4j,Titan和InfiniteGraph實現)和三元組儲存(triple-store)模型(由Datomic,AllegroGraph等實現)。我們將檢視圖的三種宣告式查詢語言:Cypher,SPARQL和Datalog。除此之外,還有像Gremlin 【36】這樣的圖形查詢語言和像Pregel這樣的圖形處理框架(見[第十章](ch10.md))。
|
||||
|
||||
### 屬性圖
|
||||
|
||||
@ -730,7 +730,7 @@ _:namerica :type :"continent"
|
||||
|
||||
在這個例子中,圖的頂點被寫為:`_:someName`。這個名字並不意味著這個檔案以外的任何東西。它的存在只是幫助我們明確哪些三元組引用了同一頂點。當謂語表示邊時,該賓語是一個頂點,如`_:idaho :within _:usa.`。當謂語是一個屬性時,該賓語是一個字串,如`_:usa :name "United States"`
|
||||
|
||||
一遍又一遍地重複相同的主語看起來相當重複,但幸運的是,可以使用分號來說明關於同一主語的多個事情。這使得Turtle格式相當不錯,可讀性強:參見[例2-7]()。
|
||||
一遍又一遍地重複相同的主語看起來相當重複,但幸運的是,可以使用分號來說明關於同一主語的多個事情。這使得Turtle格式相當不錯,可讀性強:請參閱[例2-7]()。
|
||||
|
||||
**例2-7 一種相對例2-6寫入資料的更為簡潔的方法。**
|
||||
|
||||
@ -756,7 +756,7 @@ _:namerica a :Location; :name "North America"; :type "continent".
|
||||
|
||||
#### RDF資料模型
|
||||
|
||||
[例2-7]()中使用的Turtle語言是一種用於RDF資料的人可讀格式。有時候,RDF也可以以XML格式編寫,不過完成同樣的事情會相對囉嗦,參見[例2-8]()。Turtle/N3是更可取的,因為它更容易閱讀,像Apache Jena 【42】這樣的工具可以根據需要在不同的RDF格式之間進行自動轉換。
|
||||
[例2-7]()中使用的Turtle語言是一種用於RDF資料的人可讀格式。有時候,RDF也可以以XML格式編寫,不過完成同樣的事情會相對囉嗦,請參閱[例2-8]()。Turtle/N3是更可取的,因為它更容易閱讀,像Apache Jena 【42】這樣的工具可以根據需要在不同的RDF格式之間進行自動轉換。
|
||||
|
||||
**例2-8 用RDF/XML語法表示例2-7的資料**
|
||||
|
||||
@ -794,7 +794,7 @@ RDF有一些奇怪之處,因為它是為了在網際網路上交換資料而
|
||||
|
||||
**SPARQL**是一種用於三元組儲存的面向RDF資料模型的查詢語言,【43】。(它是SPARQL協議和RDF查詢語言的縮寫,發音為“sparkle”。)SPARQL早於Cypher,並且由於Cypher的模式匹配借鑑於SPARQL,這使得它們看起來非常相似【37】。
|
||||
|
||||
與之前相同的查詢 - 查詢從美國轉移到歐洲的人 - 使用SPARQL比使用Cypher甚至更為簡潔(參見[例2-9]())。
|
||||
與之前相同的查詢 - 查詢從美國轉移到歐洲的人 - 使用SPARQL比使用Cypher甚至更為簡潔(請參閱[例2-9]())。
|
||||
|
||||
**例2-9 與示例2-4相同的查詢,用SPARQL表示**
|
||||
|
||||
|
18
zh-tw/ch3.md
18
zh-tw/ch3.md
@ -13,7 +13,7 @@
|
||||
|
||||
一個數據庫在最基礎的層次上需要完成兩件事情:當你把資料交給資料庫時,它應當把資料儲存起來;而後當你向資料庫要資料時,它應當把資料返回給你。
|
||||
|
||||
在[第2章](ch2.md)中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
|
||||
在[第二章](ch2.md)中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
|
||||
|
||||
作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你**確實**需要從許多可用的儲存引擎中選擇一個合適的。而且為了協調儲存引擎以適配應用工作負載,你也需要大致瞭解儲存引擎在底層究竟做什麼。
|
||||
|
||||
@ -130,7 +130,7 @@ $ cat database
|
||||
|
||||
乍一看,只有追加日誌看起來很浪費:為什麼不更新檔案,用新值覆蓋舊值?但是隻能追加設計的原因有幾個:
|
||||
|
||||
* 追加和分段合併是順序寫入操作,通常比隨機寫入快得多,尤其是在磁碟旋轉硬碟上。在某種程度上,順序寫入在基於快閃記憶體的 **固態硬碟(SSD)** 上也是優選的【4】。我們將在第83頁的“[比較B樹和LSM樹](#比較B樹和LSM樹)”中進一步討論這個問題。
|
||||
* 追加和分段合併是順序寫入操作,通常比隨機寫入快得多,尤其是在磁碟旋轉硬碟上。在某種程度上,順序寫入在基於快閃記憶體的 **固態硬碟(SSD)** 上也是優選的【4】。我們將在“[比較B樹和LSM樹](#比較B樹和LSM樹)”中進一步討論這個問題。
|
||||
* 如果段檔案是附加的或不可變的,併發和崩潰恢復就簡單多了。例如,您不必擔心在覆蓋值時發生崩潰的情況,而將包含舊值和新值的一部分的檔案保留在一起。
|
||||
* 合併舊段可以避免資料檔案隨著時間的推移而分散的問題。
|
||||
|
||||
@ -180,7 +180,7 @@ $ cat database
|
||||
|
||||
到目前為止,但是如何讓你的資料首先被按鍵排序呢?我們的傳入寫入可以以任何順序發生。
|
||||
|
||||
在磁碟上維護有序結構是可能的(參閱“[B樹](#B樹)”),但在記憶體儲存則要容易得多。有許多可以使用的眾所周知的樹形資料結構,例如紅黑樹或AVL樹【2】。使用這些資料結構,您可以按任何順序插入鍵,並按排序順序讀取它們。
|
||||
在磁碟上維護有序結構是可能的(請參閱“[B樹](#B樹)”),但在記憶體儲存則要容易得多。有許多可以使用的眾所周知的樹形資料結構,例如紅黑樹或AVL樹【2】。使用這些資料結構,您可以按任何順序插入鍵,並按排序順序讀取它們。
|
||||
|
||||
現在我們可以使我們的儲存引擎工作如下:
|
||||
|
||||
@ -285,13 +285,13 @@ LSM樹可以被壓縮得更好,因此經常比B樹在磁碟上產生更小的
|
||||
|
||||
#### LSM樹的缺點
|
||||
|
||||
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試逐步執行壓縮而不影響併發訪問,但是磁碟資源有限,所以很容易發生請求需要等待磁碟完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是在更高百分比的情況下(參閱“[描述效能](ch1.md#描述效能)”),對日誌結構化儲存引擎的查詢響應時間有時會相當長,而B樹的行為則相對更具可預測性【28】。
|
||||
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試逐步執行壓縮而不影響併發訪問,但是磁碟資源有限,所以很容易發生請求需要等待磁碟完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是在更高百分比的情況下(請參閱“[描述效能](ch1.md#描述效能)”),對日誌結構化儲存引擎的查詢響應時間有時會相當長,而B樹的行為則相對更具可預測性【28】。
|
||||
|
||||
壓縮的另一個問題出現在高寫入吞吐量:磁碟的有限寫入頻寬需要在初始寫入(記錄和重新整理記憶體表到磁碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全磁碟頻寬進行初始寫入,但資料庫越大,壓縮所需的磁碟頻寬就越多。
|
||||
|
||||
如果寫入吞吐量很高,並且壓縮沒有仔細配置,壓縮跟不上寫入速率。在這種情況下,磁碟上未合併段的數量不斷增加,直到磁碟空間用完,讀取速度也會減慢,因為它們需要檢查更多段檔案。通常情況下,即使壓縮無法跟上,基於SSTable的儲存引擎也不會限制傳入寫入的速率,所以您需要進行明確的監控來檢測這種情況【29,30】。
|
||||
|
||||
B樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得B樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在B樹索引中,這些鎖可以直接連線到樹【5】。在[第7章](ch7.md)中,我們將更詳細地討論這一點。
|
||||
B樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得B樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在B樹索引中,這些鎖可以直接連線到樹【5】。在[第七章](ch7.md)中,我們將更詳細地討論這一點。
|
||||
|
||||
B樹在資料庫體系結構中是非常根深蒂固的,為許多工作負載提供始終如一的良好效能,所以它們不可能很快就會消失。在新的資料儲存中,日誌結構化索引變得越來越流行。沒有快速和容易的規則來確定哪種型別的儲存引擎對你的場景更好,所以值得進行一些經驗上的測試。
|
||||
|
||||
@ -299,7 +299,7 @@ B樹在資料庫體系結構中是非常根深蒂固的,為許多工作負載
|
||||
|
||||
到目前為止,我們只討論了關鍵值索引,它們就像關係模型中的**主鍵(primary key)** 索引。主鍵唯一標識關係表中的一行,或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵(或ID)引用該行/文件/頂點,並且索引用於解析這樣的引用。
|
||||
|
||||
有二級索引也很常見。在關係資料庫中,您可以使用 `CREATE INDEX` 命令在同一個表上建立多個二級索引,而且這些索引通常對於有效地執行聯接而言至關重要。例如,在[第2章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個二級索引,以便您可以在每個表中找到屬於同一使用者的所有行。
|
||||
有二級索引也很常見。在關係資料庫中,您可以使用 `CREATE INDEX` 命令在同一個表上建立多個二級索引,而且這些索引通常對於有效地執行聯接而言至關重要。例如,在[第二章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個二級索引,以便您可以在每個表中找到屬於同一使用者的所有行。
|
||||
|
||||
一個二級索引可以很容易地從一個鍵值索引構建。主要的不同是鍵不是唯一的。即可能有許多行(文件,頂點)具有相同的鍵。這可以透過兩種方式來解決:或者透過使索引中的每個值,成為匹配行識別符號的列表(如全文索引中的釋出列表),或者透過向每個索引新增行識別符號來使每個關鍵字唯一。無論哪種方式,B樹和日誌結構索引都可以用作輔助索引。
|
||||
|
||||
@ -369,7 +369,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
|
||||
在早期業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易(commercial transaction)* 相對應:賣個貨,向供應商下訂單,支付員工工資等等。但隨著資料庫應用至那些不涉及到錢的領域,術語 **交易/事務(transaction)** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
|
||||
|
||||
事務不一定具有ACID(原子性,一致性,隔離性和永續性)屬性。事務處理只是意味著允許客戶端進行低延遲讀取和寫入 —— 而不是批次處理作業,而這些作業只能定期執行(例如每天一次)。我們在[第7章](ch7.md)中討論ACID屬性,在[第10章](ch10.md)中討論批處理。
|
||||
事務不一定具有ACID(原子性,一致性,隔離性和永續性)屬性。事務處理只是意味著允許客戶端進行低延遲讀取和寫入 —— 而不是批次處理作業,而這些作業只能定期執行(例如每天一次)。我們在[第七章](ch7.md)中討論ACID屬性,在[第十章](ch10.md)中討論批處理。
|
||||
|
||||
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論,遊戲中的動作,地址簿中的聯絡人等等,基本訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵查詢少量記錄。根據使用者的輸入插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為 **線上事務處理(OLTP, OnLine Transaction Processing)** 。
|
||||
|
||||
@ -421,7 +421,7 @@ Teradata,Vertica,SAP HANA和ParAccel等資料倉庫供應商通常使用昂
|
||||
|
||||
### 星型和雪花型:分析的模式
|
||||
|
||||
正如[第2章](ch2.md)所探討的,根據應用程式的需要,在事務處理領域中使用了大量不同的資料模型。另一方面,在分析中,資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用,被稱為星型模式(也稱為維度建模【55】)。
|
||||
正如[第二章](ch2.md)所探討的,根據應用程式的需要,在事務處理領域中使用了大量不同的資料模型。另一方面,在分析中,資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用,被稱為星型模式(也稱為維度建模【55】)。
|
||||
|
||||
圖3-9中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽量或點選量。
|
||||
|
||||
@ -514,7 +514,7 @@ WHERE product_sk = 31 AND store_sk = 3
|
||||
|
||||
載入 `product_sk = 31` 和 `store_sk = 3` 的點陣圖,並逐位計算AND。 這是因為列按照相同的順序包含行,因此一列的點陣圖中的第 k 位對應於與另一列的點陣圖中的第 k 位相同的行。
|
||||
|
||||
對於不同種類的資料,也有各種不同的壓縮方案,但我們不會詳細討論它們,參見【58】的概述。
|
||||
對於不同種類的資料,也有各種不同的壓縮方案,但我們不會詳細討論它們,請參閱【58】的概述。
|
||||
|
||||
> #### 面向列的儲存和列族
|
||||
>
|
||||
|
32
zh-tw/ch4.md
32
zh-tw/ch4.md
@ -11,11 +11,11 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著**功能(feature)** 的增增改改。[第一章](ch1.md)介紹了**可演化性(evolvability)**的概念:應該盡力構建能靈活適應變化的系統(參閱“[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
|
||||
應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著**功能(feature)** 的增增改改。[第一章](ch1.md)介紹了**可演化性(evolvability)**的概念:應該盡力構建能靈活適應變化的系統(請參閱“[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
|
||||
|
||||
在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。
|
||||
|
||||
我們在[第二章](ch2.md)討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即`ALTER`語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式(schema-on-read)**(或 **無模式(schemaless)**)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
|
||||
我們在[第二章](ch2.md)討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即`ALTER`語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式(schema-on-read)**(或 **無模式(schemaless)**)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
|
||||
|
||||
當資料**格式(format)** 或**模式(schema)** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
|
||||
|
||||
@ -266,7 +266,7 @@ Avro的關鍵思想是Writer模式和Reader模式不必是相同的 - 他們只
|
||||
|
||||
* 有很多記錄的大檔案
|
||||
|
||||
Avro的一個常見用途 - 尤其是在Hadoop環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼。 (我們將在[第10章](ch10.md)討論這種情況。)在這種情況下,該檔案的作者可以在檔案的開頭只包含一次Writer模式。 Avro指定了一個檔案格式(物件容器檔案)來做到這一點。
|
||||
Avro的一個常見用途 - 尤其是在Hadoop環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼。 (我們將在[第十章](ch10.md)討論這種情況。)在這種情況下,該檔案的作者可以在檔案的開頭只包含一次Writer模式。 Avro指定了一個檔案格式(物件容器檔案)來做到這一點。
|
||||
|
||||
* 支援獨立寫入的記錄的資料庫
|
||||
|
||||
@ -274,7 +274,7 @@ Avro的關鍵思想是Writer模式和Reader模式不必是相同的 - 他們只
|
||||
|
||||
* 透過網路連線傳送記錄
|
||||
|
||||
當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。 Avro RPC協議(參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
|
||||
當兩個程序透過雙向網路連線進行通訊時,他們可以在連線設定上協商模式版本,然後在連線的生命週期中使用該模式。 Avro RPC協議(請參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)就是這樣工作的。
|
||||
|
||||
具有模式版本的資料庫在任何情況下都是非常有用的,因為它充當文件併為您提供了檢查模式相容性的機會【24】。作為版本號,你可以使用一個簡單的遞增整數,或者你可以使用模式的雜湊。
|
||||
|
||||
@ -325,9 +325,9 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是
|
||||
|
||||
這是一個相當抽象的概念 - 資料可以透過多種方式從一個流程流向另一個流程。誰編碼資料,誰解碼?在本章的其餘部分中,我們將探討資料如何在流程之間流動的一些最常見的方式:
|
||||
|
||||
* 透過資料庫(參閱“[資料庫中的資料流](#資料庫中的資料流)”)
|
||||
* 透過服務呼叫(參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)
|
||||
* 透過非同步訊息傳遞(參閱“[訊息傳遞中的資料流](#訊息傳遞中的資料流)”)
|
||||
* 透過資料庫(請參閱“[資料庫中的資料流](#資料庫中的資料流)”)
|
||||
* 透過服務呼叫(請參閱“[服務中的資料流:REST與RPC](#服務中的資料流:REST與RPC)”)
|
||||
* 透過非同步訊息傳遞(請參閱“[訊息傳遞中的資料流](#訊息傳遞中的資料流)”)
|
||||
|
||||
|
||||
|
||||
@ -365,11 +365,11 @@ Avro為靜態型別程式語言提供了可選的程式碼生成功能,但是
|
||||
|
||||
#### 歸檔儲存
|
||||
|
||||
也許您不時為資料庫建立一個快照,例如備份或載入到資料倉庫(參閱“[資料倉庫](ch3.md#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
|
||||
也許您不時為資料庫建立一個快照,例如備份或載入到資料倉庫(請參閱“[資料倉庫](ch3.md#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
|
||||
|
||||
由於資料轉儲是一次寫入的,而且以後是不可變的,所以Avro物件容器檔案等格式非常適合。這也是一個很好的機會,可以將資料編碼為面向分析的列式格式,例如Parquet(請參閱“[列壓縮](ch3.md#列壓縮)”)。
|
||||
|
||||
在[第10章](ch10.md)中,我們將詳細討論使用檔案儲存中的資料。
|
||||
在[第十章](ch10.md)中,我們將詳細討論使用檔案儲存中的資料。
|
||||
|
||||
|
||||
|
||||
@ -383,7 +383,7 @@ Web瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面
|
||||
|
||||
此外,伺服器本身可以是另一個服務的客戶端(例如,典型的Web應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為**面向服務的體系結構(service-oriented architecture,SOA)**,最近被改進和更名為**微服務架構**【31,32】。
|
||||
|
||||
在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在[第2章](ch2.md)中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的API,它只允許由服務的業務邏輯(應用程式程式碼)預定的輸入和輸出【33】。這種限制提供了一定程度的封裝:服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
|
||||
在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在[第二章](ch2.md)中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的API,它只允許由服務的業務邏輯(應用程式程式碼)預定的輸入和輸出【33】。這種限制提供了一定程度的封裝:服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
|
||||
|
||||
面向服務/微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務API之間相容——這正是我們在本章所一直在談論的。
|
||||
|
||||
@ -405,7 +405,7 @@ REST不是一個協議,而是一個基於HTTP原則的設計哲學【34,35】
|
||||
|
||||
[^vii]: 儘管首字母縮寫詞相似,SOAP並不是SOA的要求。 SOAP是一種特殊的技術,而SOA是構建系統的一般方法。
|
||||
|
||||
SOAP Web服務的API使用稱為Web服務描述語言(WSDL)的基於XML的語言來描述。 WSDL支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為XML訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(參閱“[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
|
||||
SOAP Web服務的API使用稱為Web服務描述語言(WSDL)的基於XML的語言來描述。 WSDL支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為XML訊息並由框架再次解碼)訪問遠端服務。這在靜態型別程式語言中非常有用,但在動態型別程式語言中很少(請參閱“[程式碼生成和動態型別的語言](#程式碼生成和動態型別的語言)”)。
|
||||
|
||||
由於WSDL的設計不是人類可讀的,而且由於SOAP訊息通常因為過於複雜而無法手動構建,所以SOAP的使用者在很大程度上依賴於工具支援,程式碼生成和IDE【38】。對於SOAP供應商不支援的程式語言的使用者來說,與SOAP服務的整合是困難的。
|
||||
|
||||
@ -420,12 +420,12 @@ Web服務僅僅是透過網路進行API請求的一系列技術的最新版本
|
||||
所有這些都是基於 **遠端過程呼叫(RPC)** 的思想,該過程呼叫自20世紀70年代以來一直存在【42】。 RPC模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管RPC起初看起來很方便,但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同:
|
||||
|
||||
* 本地函式呼叫是可預測的,並且成功或失敗僅取決於受您控制的引數。網路請求是不可預知的:由於網路問題,請求或響應可能會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在您的控制範圍之內。網路問題是常見的,所以你必須預測他們,例如透過重試失敗的請求。
|
||||
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過。 (我們將在[第8章](ch8.md)更詳細地討論這個問題。)
|
||||
* 如果您重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非您在協議中引入除重( **冪等(idempotence)**)機制。本地函式呼叫沒有這個問題。 (在[第11章](ch11.md)更詳細地討論冪等性)
|
||||
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過。 (我們將在[第八章](ch8.md)更詳細地討論這個問題。)
|
||||
* 如果您重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非您在協議中引入除重( **冪等(idempotence)**)機制。本地函式呼叫沒有這個問題。 (在[第十一章](ch11.md)更詳細地討論冪等性)
|
||||
* 每次呼叫本地功能時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間完成一樣的東西。
|
||||
* 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會變成問題。
|
||||
|
||||
客戶端和服務可以用不同的程式語言實現,所以RPC框架必須將資料型別從一種語言翻譯成另一種語言。這可能會捅出大簍子,因為不是所有的語言都具有相同的型別 —— 例如回想一下JavaScript的數字大於$2^{53}$的問題(參閱“[JSON,XML和二進位制變體](#JSON,XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
|
||||
客戶端和服務可以用不同的程式語言實現,所以RPC框架必須將資料型別從一種語言翻譯成另一種語言。這可能會捅出大簍子,因為不是所有的語言都具有相同的型別 —— 例如回想一下JavaScript的數字大於$2^{53}$的問題(請參閱“[JSON,XML和二進位制變體](#JSON,XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
|
||||
|
||||
所有這些因素意味著嘗試使遠端服務看起來像程式語言中的本地物件一樣毫無意義,因為這是一個根本不同的事情。 REST的部分吸引力在於,它並不試圖隱藏它是一個網路協議的事實(儘管這似乎並沒有阻止人們在REST之上構建RPC庫)。
|
||||
|
||||
@ -473,11 +473,11 @@ RPC方案的前後向相容性屬性從它使用的編碼方式中繼承:
|
||||
|
||||
#### 訊息代理
|
||||
|
||||
過去,**訊息代理(Message Broker)**主要是TIBCO,IBM WebSphere和webMethods等公司的商業軟體的秀場。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka這樣的開源實現已經流行起來。我們將在[第11章](ch11.md)中對它們進行更詳細的比較。
|
||||
過去,**訊息代理(Message Broker)**主要是TIBCO,IBM WebSphere和webMethods等公司的商業軟體的秀場。最近像RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka這樣的開源實現已經流行起來。我們將在[第十一章](ch11.md)中對它們進行更詳細的比較。
|
||||
|
||||
詳細的交付語義因實現和配置而異,但通常情況下,訊息代理的使用方式如下:一個程序將訊息傳送到指定的佇列或主題,代理確保將訊息傳遞給那個佇列或主題的一個或多個消費者或訂閱者。在同一主題上可以有許多生產者和許多消費者。
|
||||
|
||||
一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在[第11章](ch11.md)中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求/響應資料流,類似於RPC)。
|
||||
一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在[第十一章](ch11.md)中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求/響應資料流,類似於RPC)。
|
||||
|
||||
訊息代理通常不會執行任何特定的資料模型 - 訊息只是包含一些元資料的位元組序列,因此您可以使用任何編碼格式。如果編碼是向後和向前相容的,您可以靈活地對釋出者和消費者的編碼進行獨立的修改,並以任意順序進行部署。
|
||||
|
||||
|
44
zh-tw/ch5.md
44
zh-tw/ch5.md
@ -16,7 +16,7 @@
|
||||
* 即使系統的一部分出現故障,系統也能繼續工作(從而提高可用性)
|
||||
* 伸縮可以接受讀請求的機器數量(從而提高讀取吞吐量)
|
||||
|
||||
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在[第6章](ch6.md)中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
|
||||
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在[第六章](ch6.md)中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
|
||||
|
||||
如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的**變更(change)**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者(single leader)**,**多領導者(multi leader)**和**無領導者(leaderless)**。幾乎所有分散式資料庫都使用這三種方法之一。
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
>
|
||||
> 對於非同步複製系統而言,主庫故障時有可能丟失資料。這可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。 例如,**鏈式複製**【8,9】]是同步複製的一種變體,已經在一些系統(如Microsoft Azure儲存【10,11】)中成功實現。
|
||||
>
|
||||
> 複製的一致性與**共識(consensus)**(使幾個節點就某個值達成一致)之間有著密切的聯絡,[第9章](ch9.md)將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
|
||||
> 複製的一致性與**共識(consensus)**(使幾個節點就某個值達成一致)之間有著密切的聯絡,[第九章](ch9.md)將詳細地探討這一領域的理論。本章主要討論實踐中資料庫常用的簡單複製形式。
|
||||
>
|
||||
|
||||
### 設定新從庫
|
||||
@ -103,7 +103,7 @@
|
||||
故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動故障切換過程通常由以下步驟組成:
|
||||
|
||||
1. 確認主庫失效。有很多事情可能會出錯:崩潰,停電,網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時(Timeout)** :節點頻繁地相互來回傳遞訊息,並且如果一個節點在一段時間內(例如30秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
|
||||
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的**控制器節點(controller node)**來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個**共識**問題,將在[第9章](ch9.md)詳細討論。
|
||||
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的**控制器節點(controller node)**來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(最小化資料損失)。讓所有的節點同意一個新的領導者,是一個**共識**問題,將在[第九章](ch9.md)詳細討論。
|
||||
3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在“[請求路由](ch6.md#請求路由)”中討論這個問題)。如果老領導回來,可能仍然認為自己是主庫,沒有意識到其他副本已經讓它下臺了。系統需要確保老領導認可新領導,成為一個從庫。
|
||||
|
||||
故障切換會出現很多大麻煩:
|
||||
@ -112,7 +112,7 @@
|
||||
|
||||
* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在GitHub 【13】的一場事故中,一個過時的MySQL從庫被提升為主庫。資料庫使用自增ID作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的ID作為主鍵。這些主鍵也在Redis中使用,主鍵重用使得MySQL和Redis中資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
|
||||
|
||||
* 發生某些故障時(見[第8章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂(split brain)**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(參見“[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點[^ii],但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
|
||||
* 發生某些故障時(見[第八章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂(split brain)**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱“[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點[^ii],但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
|
||||
|
||||
[^ii]: 這種機制稱為 **遮蔽(fencing)**,充滿感情的術語是:**爆彼之頭(Shoot The Other Node In The Head, STONITH)**。
|
||||
|
||||
@ -120,7 +120,7 @@
|
||||
|
||||
這些問題沒有簡單的解決方案。因此,即使軟體支援自動故障切換,不少運維團隊還是更願意手動執行故障切換。
|
||||
|
||||
節點故障、不可靠的網路、對副本一致性,永續性,可用性和延遲的權衡 ,這些問題實際上是分散式系統中的基本問題。[第8章](ch8.md)和[第9章](ch9.md)將更深入地討論它們。
|
||||
節點故障、不可靠的網路、對副本一致性,永續性,可用性和延遲的權衡 ,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md)和[第九章](ch9.md)將更深入地討論它們。
|
||||
|
||||
### 複製日誌的實現
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
|
||||
#### 傳輸預寫式日誌(WAL)
|
||||
|
||||
在[第3章](ch3.md)中,我們討論了儲存引擎如何在磁碟上表示資料,並且我們發現,通常寫操作都是追加到日誌中:
|
||||
在[第三章](ch3.md)中,我們討論了儲存引擎如何在磁碟上表示資料,並且我們發現,通常寫操作都是追加到日誌中:
|
||||
|
||||
* 對於日誌結構儲存引擎(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
|
||||
* 對於覆寫單個磁碟塊的[B樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌(Write Ahead Log, WAL)**,以便崩潰後索引可以恢復到一個一致的狀態。
|
||||
@ -169,11 +169,11 @@
|
||||
|
||||
由於邏輯日誌與儲存引擎內部分離,因此可以更容易地保持向後相容,從而使領導者和跟隨者能夠執行不同版本的資料庫軟體甚至不同的儲存引擎。
|
||||
|
||||
對於外部應用程式來說,邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統,這一點很有用,例如複製到資料倉庫進行離線分析,或建立自定義索引和快取【18】。 這種技術被稱為 **資料變更捕獲(change data capture)**,第11章將重新講到它。
|
||||
對於外部應用程式來說,邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統,這一點很有用,例如複製到資料倉庫進行離線分析,或建立自定義索引和快取【18】。 這種技術被稱為 **資料變更捕獲(change data capture)**,[第十一章](ch11.md)將重新講到它。
|
||||
|
||||
#### 基於觸發器的複製
|
||||
|
||||
到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果您只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果您需要衝突解決邏輯(參閱“[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製移動到應用程式層。
|
||||
到目前為止描述的複製方法是由資料庫系統實現的,不涉及任何應用程式程式碼。在很多情況下,這就是你想要的。但在某些情況下需要更多的靈活性。例如,如果您只想複製資料的一個子集,或者想從一種資料庫複製到另一種資料庫,或者如果您需要衝突解決邏輯(請參閱“[處理寫入衝突](#處理寫入衝突)”),則可能需要將複製移動到應用程式層。
|
||||
|
||||
一些工具,如Oracle Golden Gate 【19】,可以透過讀取資料庫日誌,使得其他應用程式可以使用資料。另一種方法是使用許多關係資料庫自帶的功能:觸發器和儲存過程。
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
* 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫為該使用者提供任何查詢時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀,或者等待從庫追趕上來。
|
||||
|
||||
時間戳可以是邏輯時間戳(指示寫入順序的東西,例如日誌序列號)或實際系統時鐘(在這種情況下,時鐘同步變得至關重要;參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。
|
||||
時間戳可以是邏輯時間戳(指示寫入順序的東西,例如日誌序列號)或實際系統時鐘(在這種情況下,時鐘同步變得至關重要;請參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。
|
||||
|
||||
* 如果您的副本分佈在多個數據中心(出於可用性目的與使用者儘量在地理上接近),則會增加複雜性。任何需要由領導者提供服務的請求都必須路由到包含主庫的資料中心。
|
||||
|
||||
@ -277,7 +277,7 @@
|
||||
|
||||
防止這種異常,需要另一種型別的保證:**一致字首讀(consistent prefix reads)**【23】。 這個保證說:如果一系列寫入按某個順序發生,那麼任何人讀取這些寫入時,也會看見它們以同樣的順序出現。
|
||||
|
||||
這是**分割槽(partitioned)**(**分片(sharded)**)資料庫中的一個特殊問題,將在[第6章](ch6.md)中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在**全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
|
||||
這是**分割槽(partitioned)**(**分片(sharded)**)資料庫中的一個特殊問題,將在[第六章](ch6.md)中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在**全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
|
||||
|
||||
一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽。對於某些無法高效完成這種操作的應用,還有一些顯式跟蹤因果依賴關係的演算法,本書將在“[“此前發生”的關係和併發](#“此前發生”的關係和併發)”一節中返回這個主題。
|
||||
|
||||
@ -299,7 +299,7 @@
|
||||
|
||||
基於領導者的複製有一個主要的缺點:只有一個主庫,而所有的寫入都必須透過它[^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫, 就無法向資料庫寫入。
|
||||
|
||||
[^iv]: 如果資料庫被分割槽(見第6章),每個分割槽都有一個領導。 不同的分割槽可能在不同的節點上有其領導者,但是每個分割槽必須有一個領導者節點。
|
||||
[^iv]: 如果資料庫被分割槽(見[第六章](ch6.md)),每個分割槽都有一個領導。 不同的分割槽可能在不同的節點上有其領導者,但是每個分割槽必須有一個領導者節點。
|
||||
|
||||
基於領導者的複製模型的自然延伸是允許多個節點接受寫入。 複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料更改轉發給所有其他節點。 稱之為**多領導者配置**(也稱多主、多活複製)。 在這種情況下,每個領導者同時扮演其他領導者的追隨者。
|
||||
|
||||
@ -410,7 +410,7 @@
|
||||
|
||||
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB以這種方式工作。
|
||||
|
||||
請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果您有一個事務會原子性地進行幾次不同的寫入(請參閱[第7章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
|
||||
請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果您有一個事務會原子性地進行幾次不同的寫入(請參閱[第七章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
|
||||
|
||||
|
||||
|
||||
@ -435,7 +435,7 @@
|
||||
|
||||
其他型別的衝突可能更為微妙,難以發現。例如,考慮一個會議室預訂系統:它記錄誰訂了哪個時間段的哪個房間。應用需要確保每個房間只有一組人同時預定(即不得有相同房間的重疊預訂)。在這種情況下,如果同時為同一個房間建立兩個不同的預訂,則可能會發生衝突。即使應用程式在允許使用者進行預訂之前檢查可用性,如果兩次預訂是由兩個不同的領導者進行的,則可能會有衝突。
|
||||
|
||||
現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在[第7章](ch7.md)中看到更多的衝突示例,在[第12章](ch12.md)中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
|
||||
現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在[第七章](ch7.md)中看到更多的衝突示例,在[第十二章](ch12.md)中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
|
||||
|
||||
|
||||
|
||||
@ -463,9 +463,9 @@
|
||||
|
||||
在[圖5-9](../img/fig5-9.png)中,客戶端A向主庫1的表中插入一行,客戶端B在主庫3上更新該行。然而,主庫2可以以不同的順序接收寫入:它可以首先接收更新(從它的角度來看,是對資料庫中不存在的行的更新),並且僅在稍後接收到相應的插入(其應該在更新之前)。
|
||||
|
||||
這是一個因果關係的問題,類似於我們在“[一致字首讀](#一致字首讀)”中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,以便在主庫2處正確地排序這些事件(見[第8章](ch8.md))。
|
||||
這是一個因果關係的問題,類似於我們在“[一致字首讀](#一致字首讀)”中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,以便在主庫2處正確地排序這些事件(見[第八章](ch8.md))。
|
||||
|
||||
要正確排序這些事件,可以使用一種稱為 **版本向量(version vectors)** 的技術,本章稍後將討論這種技術(參閱“[檢測併發寫入](#檢測併發寫入)”)。然而,衝突檢測技術在許多多領導者複製系統中執行得不好。例如,在撰寫本文時,PostgreSQL BDR不提供寫入的因果排序【27】,而Tungsten Replicator for MySQL甚至不嘗試檢測衝突【34】。
|
||||
要正確排序這些事件,可以使用一種稱為 **版本向量(version vectors)** 的技術,本章稍後將討論這種技術(請參閱“[檢測併發寫入](#檢測併發寫入)”)。然而,衝突檢測技術在許多多領導者複製系統中執行得不好。例如,在撰寫本文時,PostgreSQL BDR不提供寫入的因果排序【27】,而Tungsten Replicator for MySQL甚至不嘗試檢測衝突【34】。
|
||||
|
||||
如果您正在使用具有多領導者複製功能的系統,那麼應該瞭解這些問題,仔細閱讀文件,並徹底測試您的資料庫,以確保它確實提供了您認為具有的保證。
|
||||
|
||||
@ -483,7 +483,7 @@
|
||||
|
||||
### 當節點故障時寫入資料庫
|
||||
|
||||
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(參閱「[處理節點宕機](#處理節點宕機)」)。
|
||||
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於主機的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
|
||||
|
||||
另一方面,在無領導配置中,故障切換不存在。[圖5-10](../img/fig5-10.png)顯示了發生了什麼事情:客戶端(使用者1234)並行傳送寫入到所有三個副本,並且兩個可用副本接受寫入,但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的:在使用者1234已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
|
||||
|
||||
@ -493,7 +493,7 @@
|
||||
|
||||
現在想象一下,不可用的節點重新聯機,客戶端開始讀取它。節點關閉時發生的任何寫入都從該節點丟失。因此,如果您從該節點讀取資料,則可能會將陳舊(過時)值視為響應。
|
||||
|
||||
為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅傳送它的請求到一個副本:讀請求也被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應。即來自一個節點的最新值和來自另一個節點的陳舊值。版本號用於確定哪個值更新(參閱“[檢測併發寫入](#檢測併發寫入)”)。
|
||||
為了解決這個問題,當一個客戶端從資料庫中讀取資料時,它不僅僅傳送它的請求到一個副本:讀請求也被並行地傳送到多個節點。客戶可能會從不同的節點獲得不同的響應。即來自一個節點的最新值和來自另一個節點的陳舊值。版本號用於確定哪個值更新(請參閱“[檢測併發寫入](#檢測併發寫入)”)。
|
||||
|
||||
#### 讀修復和反熵
|
||||
|
||||
@ -523,7 +523,7 @@
|
||||
|
||||
在Dynamo風格的資料庫中,引數n,w和r通常是可配置的。一個常見的選擇是使n為奇數(通常為3或5)並設定 $w = r =(n + 1)/ 2$(向上取整)。但是可以根據需要更改數字。例如,設定$w = n$和$r = 1$的寫入很少且讀取次數較多的工作負載可能會受益。這使得讀取速度更快,但具有隻有一個失敗節點導致所有資料庫寫入失敗的缺點。
|
||||
|
||||
> 叢集中可能有多於n的節點。(叢集的機器數可能多於副本數目),但是任何給定的值只能儲存在n個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在[第6章](ch6.md)繼續討論分割槽。
|
||||
> 叢集中可能有多於n的節點。(叢集的機器數可能多於副本數目),但是任何給定的值只能儲存在n個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在[第六章](ch6.md)繼續討論分割槽。
|
||||
>
|
||||
|
||||
法定人數條件$w + r> n$允許系統容忍不可用的節點,如下所示:
|
||||
@ -598,7 +598,7 @@
|
||||
|
||||
#### 運維多個數據中心
|
||||
|
||||
我們先前討論了跨資料中心複製作為多主複製的用例(參閱“[多主複製](#多主複製)”)。無主複製也適用於多資料中心操作,因為它旨在容忍衝突的併發寫入,網路中斷和延遲尖峰。
|
||||
我們先前討論了跨資料中心複製作為多主複製的用例(請參閱“[多主複製](#多主複製)”)。無主複製也適用於多資料中心操作,因為它旨在容忍衝突的併發寫入,網路中斷和延遲尖峰。
|
||||
|
||||
Cassandra和Voldemort在正常的無主模型中實現了他們的多資料中心支援:副本的數量n包括所有資料中心的節點,在配置中,您可以指定每個資料中心中您想擁有的副本的數量。無論資料中心如何,每個來自客戶端的寫入都會發送到所有副本,但客戶端通常只等待來自其本地資料中心內的法定節點的確認,從而不會受到跨資料中心鏈路延遲和中斷的影響。對其他資料中心的高延遲寫入通常被配置為非同步發生,儘管配置有一定的靈活性【50,51】。
|
||||
|
||||
@ -606,7 +606,7 @@
|
||||
|
||||
### 檢測併發寫入
|
||||
|
||||
Dynamo風格的資料庫允許多個客戶端同時寫入相同的Key,這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多領導者複製相似(參閱“[處理寫入衝突](#處理寫入衝突)”),但在Dynamo樣式的資料庫中,在**讀修復**或**提示移交**期間也可能會產生衝突。
|
||||
Dynamo風格的資料庫允許多個客戶端同時寫入相同的Key,這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多領導者複製相似(請參閱“[處理寫入衝突](#處理寫入衝突)”),但在Dynamo樣式的資料庫中,在**讀修復**或**提示移交**期間也可能會產生衝突。
|
||||
|
||||
問題在於,由於可變的網路延遲和部分故障,事件可能在不同的節點以不同的順序到達。例如,[圖5-12](../img/fig5-12.png)顯示了兩個客戶機A和B同時寫入三節點資料儲存區中的鍵X:
|
||||
|
||||
@ -653,7 +653,7 @@
|
||||
|
||||
> #### 併發性,時間和相對性
|
||||
>
|
||||
> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發——但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否**同時**發生的,這個問題我們將在[第8章](ch8.md)中詳細討論。
|
||||
> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發——但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否**同時**發生的,這個問題我們將在[第八章](ch8.md)中詳細討論。
|
||||
>
|
||||
> 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作**併發**,而不管它們發生的物理時間。人們有時把這個原理和狹義相對論的物理學聯絡起來【54】,它引入了資訊不能比光速更快的思想。因此,如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間,那麼這兩個事件不可能相互影響。
|
||||
>
|
||||
@ -696,7 +696,7 @@
|
||||
|
||||
這種演算法可以確保沒有資料被無聲地丟棄,但不幸的是,客戶端需要做一些額外的工作:如果多個操作併發發生,則客戶端必須透過合併併發寫入的值來擦屁股。 Riak稱這些併發值**兄弟(siblings)**。
|
||||
|
||||
合併兄弟值,本質上是與多領導者複製中的衝突解決相同的問題,我們先前討論過(參閱“[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中做更聰明的事情。
|
||||
合併兄弟值,本質上是與多領導者複製中的衝突解決相同的問題,我們先前討論過(請參閱“[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中做更聰明的事情。
|
||||
|
||||
以購物車為例,一種合理的合併兄弟方法就是集合求並集。在[圖5-14](../img/fig5-14.png)中,最後的兩個兄弟是[牛奶,麵粉,雞蛋,燻肉]和[雞蛋,牛奶,火腿]。注意牛奶和雞蛋同時出現在兩個兄弟裡,即使他們每個只被寫過一次。合併的值可以是[牛奶,麵粉,雞蛋,培根,火腿],沒有重複。
|
||||
|
||||
|
32
zh-tw/ch6.md
32
zh-tw/ch6.md
@ -11,9 +11,9 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第5章](ch5.md)中,我們討論了複製——即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行**分割槽(partitions)**,也稱為**分片(sharding)**[^i]。
|
||||
在[第五章](ch5.md)中,我們討論了複製——即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行**分割槽(partitions)**,也稱為**分片(sharding)**[^i]。
|
||||
|
||||
[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽(network partitions, netsplits)** 無關,這是節點之間網路故障的一種。我們將在[第8章](ch8.md)討論這些錯誤。
|
||||
[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽(network partitions, netsplits)** 無關,這是節點之間網路故障的一種。我們將在[第八章](ch8.md)討論這些錯誤。
|
||||
|
||||
> #### 術語澄清
|
||||
>
|
||||
@ -22,11 +22,11 @@
|
||||
|
||||
通常情況下,每條資料(每條記錄,每行或每個文件)屬於且僅屬於一個分割槽。有很多方法可以實現這一點,本章將進行深入討論。實際上,每個分割槽都是自己的小型資料庫,儘管資料庫可能支援同時進行多個分割槽的操作。
|
||||
|
||||
分割槽主要是為了**可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(參閱[第二部分](part-ii.md)關於[無共享架構](part-ii.md#無共享架構)的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
|
||||
分割槽主要是為了**可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(請參閱[第二部分](part-ii.md)關於[無共享架構](part-ii.md#無共享架構)的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
|
||||
|
||||
對於在單個分割槽上執行的查詢,每個節點可以獨立執行對自己的查詢,因此可以透過新增更多的節點來擴大查詢吞吐量。大型,複雜的查詢可能會跨越多個節點並行處理,儘管這也帶來了新的困難。
|
||||
|
||||
分割槽資料庫在20世紀80年代由Teradata和NonStop SQL【1】等產品率先推出,最近因為NoSQL資料庫和基於Hadoop的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(參閱“[事務處理還是分析](ch3.md#事務處理還是分析)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
|
||||
分割槽資料庫在20世紀80年代由Teradata和NonStop SQL【1】等產品率先推出,最近因為NoSQL資料庫和基於Hadoop的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(請參閱“[事務處理還是分析](ch3.md#事務處理還是分析)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
|
||||
|
||||
在本章中,我們將首先介紹分割大型資料集的不同方法,並觀察索引如何與分割槽配合。然後我們將討論[分割槽再平衡(rebalancing)](#分割槽再平衡),如果想要新增或刪除叢集中的節點,則必須進行再平衡。最後,我們將概述資料庫如何將請求路由到正確的分割槽並執行查詢。
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。 這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
|
||||
|
||||
一個節點可能儲存多個分割槽。 如果使用主從複製模型,則分割槽和複製的組合如[圖6-1](../img/fig6-1.png)所示。 每個分割槽領導者(主)被分配給一個節點,追隨者(從)被分配給其他節點。 每個節點可能是某些分割槽的領導者,同時是其他分割槽的追隨者。
|
||||
我們在[第5章](ch5.md)討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。 大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
|
||||
我們在[第五章](ch5.md)討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。 大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
|
||||
|
||||
![](../img/fig6-1.png)
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在“[分割槽再平衡](#分割槽再平衡)”中更詳細地討論分割槽邊界的選擇)。 Bigtable使用了這種分割槽策略,以及其開源等價物HBase 【2, 3】,RethinkDB和2.4版本之前的MongoDB 【4】。
|
||||
|
||||
在每個分割槽中,我們可以按照一定的順序儲存鍵(參見“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,您可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(參閱“[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
|
||||
在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,您可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱“[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
|
||||
|
||||
然而,Key Range分割槽的缺點是某些特定的訪問模式會導致熱點。 如果主鍵是時間戳,則分割槽對應於時間範圍,例如,給每天分配一個分割槽。 不幸的是,由於我們在測量發生時將資料從感測器寫入資料庫,因此所有寫入操作都會轉到同一個分割槽(即今天的分割槽),這樣分割槽可能會因寫入而過載,而其他分割槽則處於空閒狀態【5】。
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
|
||||
> #### 一致性雜湊
|
||||
>
|
||||
> 一致性雜湊由Karger等人定義。【7】 用於跨網際網路級別的快取系統,例如CDN中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。 請注意,這裡的一致性與複製一致性(請參閱[第5章](ch5.md))或ACID一致性(參閱[第7章](ch7.md))無關,而只是描述了一種重新平衡(reblancing)的特定方法。
|
||||
> 一致性雜湊由Karger等人定義。【7】 用於跨網際網路級別的快取系統,例如CDN中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界(partition boundaries)** 來避免中央控制或分散式共識的需要。 請注意,這裡的一致性與複製一致性(請參閱[第五章](ch5.md))或ACID一致性(請參閱[第七章](ch7.md))無關,而只是描述了一種重新平衡(reblancing)的特定方法。
|
||||
>
|
||||
> 正如我們將在“[分割槽再平衡](#分割槽再平衡)”中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。 因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為**雜湊分割槽(hash partitioning)**。
|
||||
|
||||
@ -129,7 +129,7 @@
|
||||
|
||||
你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是**欄位(field)**,關係資料庫中這些是**列(column)** )。 如果您聲明瞭索引,則資料庫可以自動執行索引[^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目`color:red`的文件ID列表中。
|
||||
|
||||
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件ID的對映來實現輔助索引。 如果沿著這條路線走下去,請萬分小心,確保您的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 參見“[多物件事務的需求](ch7.md#多物件事務的需求)”。
|
||||
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件ID的對映來實現輔助索引。 如果沿著這條路線走下去,請萬分小心,確保您的索引與底層資料保持一致。 競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱“[多物件事務的需求](ch7.md#多物件事務的需求)”。
|
||||
|
||||
![](../img/fig6-4.png)
|
||||
|
||||
@ -140,7 +140,7 @@
|
||||
但是,從文件分割槽索引中讀取需要注意:除非您對文件ID做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在[圖6-4](../img/fig6-4.png)中,紅色汽車出現在分割槽0和分割槽1中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
|
||||
|
||||
|
||||
這種查詢分割槽資料庫的方法有時被稱為**分散/聚集(scatter/gather)**,並且可能會使二級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散/聚集也容易導致尾部延遲放大(參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)”)。然而,它被廣泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文件分割槽二級索引。大多數資料庫供應商建議您構建一個能從單個分割槽提供二級索引查詢的分割槽方案,但這並不總是可行,尤其是當在單個查詢中使用多個二級索引時(例如同時需要按顏色和製造商查詢)。
|
||||
這種查詢分割槽資料庫的方法有時被稱為**分散/聚集(scatter/gather)**,並且可能會使二級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散/聚集也容易導致尾部延遲放大(請參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)”)。然而,它被廣泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文件分割槽二級索引。大多數資料庫供應商建議您構建一個能從單個分割槽提供二級索引查詢的分割槽方案,但這並不總是可行,尤其是當在單個查詢中使用多個二級索引時(例如同時需要按顏色和製造商查詢)。
|
||||
|
||||
|
||||
### 基於關鍵詞(Term)的二級索引進行分割槽
|
||||
@ -159,11 +159,11 @@
|
||||
|
||||
關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要**分散/收集**所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。
|
||||
|
||||
理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱[第7章](ch7.md)和[第9章](ch9.md))。
|
||||
理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱[第七章](ch7.md)和[第九章](ch9.md))。
|
||||
|
||||
在實踐中,對全域性二級索引的更新通常是**非同步**的(也就是說,如果在寫入之後不久讀取索引,剛才所做的更改可能尚未反映在索引中)。例如,Amazon DynamoDB聲稱在正常情況下,其全域性次級索引會在不到一秒的時間內更新,但在基礎架構出現故障的情況下可能會有延遲【20】。
|
||||
|
||||
全域性關鍵詞分割槽索引的其他用途包括Riak的搜尋功能【21】和Oracle資料倉庫,它允許您在本地和全域性索引之間進行選擇【22】。我們將在[第12章](ch12.md)中繼續關鍵詞分割槽二級索引實現的話題。
|
||||
全域性關鍵詞分割槽索引的其他用途包括Riak的搜尋功能【21】和Oracle資料倉庫,它允許您在本地和全域性索引之間進行選擇【22】。我們將在[第十二章](ch12.md)中繼續關鍵詞分割槽二級索引實現的話題。
|
||||
|
||||
## 分割槽再平衡
|
||||
|
||||
@ -216,9 +216,9 @@
|
||||
|
||||
#### 動態分割槽
|
||||
|
||||
對於使用鍵範圍分割槽的資料庫(參閱“[根據鍵的範圍分割槽](#根據鍵的範圍分割槽)”),具有固定邊界的固定數量的分割槽將非常不便:如果出現邊界錯誤,則可能會導致一個分割槽中的所有資料或者其他分割槽中的所有資料為空。手動重新配置分割槽邊界將非常繁瑣。
|
||||
對於使用鍵範圍分割槽的資料庫(請參閱“[根據鍵的範圍分割槽](#根據鍵的範圍分割槽)”),具有固定邊界的固定數量的分割槽將非常不便:如果出現邊界錯誤,則可能會導致一個分割槽中的所有資料或者其他分割槽中的所有資料為空。手動重新配置分割槽邊界將非常繁瑣。
|
||||
|
||||
出於這個原因,按鍵的範圍進行分割槽的資料庫(如HBase和RethinkDB)會動態建立分割槽。當分割槽增長到超過配置的大小時(在HBase上,預設值是10GB),會被分成兩個分割槽,每個分割槽約佔一半的資料【26】。與之相反,如果大量資料被刪除並且分割槽縮小到某個閾值以下,則可以將其與相鄰分割槽合併。此過程與B樹頂層發生的過程類似(參閱“[B樹](ch3.md#B樹)”)。
|
||||
出於這個原因,按鍵的範圍進行分割槽的資料庫(如HBase和RethinkDB)會動態建立分割槽。當分割槽增長到超過配置的大小時(在HBase上,預設值是10GB),會被分成兩個分割槽,每個分割槽約佔一半的資料【26】。與之相反,如果大量資料被刪除並且分割槽縮小到某個閾值以下,則可以將其與相鄰分割槽合併。此過程與B樹頂層發生的過程類似(請參閱“[B樹](ch3.md#B樹)”)。
|
||||
|
||||
每個分割槽分配給一個節點,每個節點可以處理多個分割槽,就像固定數量的分割槽一樣。大型分割槽拆分後,可以將其中的一半轉移到另一個節點,以平衡負載。在HBase中,分割槽檔案的傳輸透過HDFS(底層使用的分散式檔案系統)來實現【3】。
|
||||
|
||||
@ -236,7 +236,7 @@
|
||||
|
||||
當一個新節點加入叢集時,它隨機選擇固定數量的現有分割槽進行拆分,然後佔有這些拆分分割槽中每個分割槽的一半,同時將每個分割槽的另一半留在原地。隨機化可能會產生不公平的分割,但是平均在更大數量的分割槽上時(在Cassandra中,預設情況下,每個節點有256個分割槽),新節點最終從現有節點獲得公平的負載份額。 Cassandra 3.0引入了另一種再平衡的演算法來避免不公平的分割【29】。
|
||||
|
||||
隨機選擇分割槽邊界要求使用基於雜湊的分割槽(可以從雜湊函式產生的數字範圍中挑選邊界)。實際上,這種方法最符合一致性雜湊的原始定義【7】(參閱“[一致性雜湊](#一致性雜湊)”)。最新的雜湊函式可以在較低元資料開銷的情況下達到類似的效果【8】。
|
||||
隨機選擇分割槽邊界要求使用基於雜湊的分割槽(可以從雜湊函式產生的數字範圍中挑選邊界)。實際上,這種方法最符合一致性雜湊的原始定義【7】(請參閱“[一致性雜湊](#一致性雜湊)”)。最新的雜湊函式可以在較低元資料開銷的情況下達到類似的效果【8】。
|
||||
|
||||
### 運維:手動還是自動再平衡
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
|
||||
**圖6-7 將請求路由到正確節點的三種不同方式。**
|
||||
|
||||
這是一個具有挑戰性的問題,因為重要的是所有參與者都同意 - 否則請求將被髮送到錯誤的節點,得不到正確的處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見[第9章](ch9.md))。
|
||||
這是一個具有挑戰性的問題,因為重要的是所有參與者都同意 - 否則請求將被髮送到錯誤的節點,得不到正確的處理。 在分散式系統中有達成共識的協議,但很難正確地實現(見[第九章](ch9.md))。
|
||||
|
||||
許多分散式資料系統都依賴於一個獨立的協調服務,比如ZooKeeper來跟蹤叢集元資料,如[圖6-8](../img/fig6-8.png)所示。 每個節點在ZooKeeper中註冊自己,ZooKeeper維護分割槽到節點的可靠對映。 其他參與者(如路由層或分割槽感知客戶端)可以在ZooKeeper中訂閱此資訊。 只要分割槽分配發生了改變,或者叢集中新增或刪除了一個節點,ZooKeeper就會通知路由層使路由資訊保持最新狀態。
|
||||
|
||||
@ -290,7 +290,7 @@
|
||||
|
||||
然而,通常用於分析的**大規模並行處理(MPP, Massively parallel processing)** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線,過濾,分組和聚合操作。 MPP查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
|
||||
|
||||
資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在[第10章](ch10.md)討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述,請參閱參考文獻【1,33】。
|
||||
資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在[第十章](ch10.md)討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述,請參閱參考文獻【1,33】。
|
||||
|
||||
## 本章小結
|
||||
|
||||
|
68
zh-tw/ch7.md
68
zh-tw/ch7.md
@ -31,7 +31,7 @@
|
||||
|
||||
本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入**併發控制**的領域,討論各種可能發生的競爭條件,以及資料庫如何實現**讀已提交(read committed)**,**快照隔離(snapshot isolation)**和**可序列化(serializability)**等隔離級別。
|
||||
|
||||
本章同時適用於單機資料庫與分散式資料庫;在[第8章](ch8.md)中將重點討論僅出現在分散式系統中的特殊挑戰。
|
||||
本章同時適用於單機資料庫與分散式資料庫;在[第八章](ch8.md)中將重點討論僅出現在分散式系統中的特殊挑戰。
|
||||
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援**事務**。其中大多數遵循IBM System R(第一個SQL資料庫)在1975年引入的風格【1,2,3】。40年裡,儘管一些實現細節發生了變化,但總體思路大同小異:MySQL,PostgreSQL,Oracle,SQL Server等資料庫中的事務支援與System R異乎尋常地相似。
|
||||
|
||||
2000年以後,非關係(NoSQL)資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上,透過提供新的資料模型選擇(參見[第2章](ch2.md))並預設包含複製(第5章)和分割槽(第6章)來進一步提升。事務是這次運動的主要犧牲品:這些新一代資料庫中的許多資料庫完全放棄了事務,或者重新定義了這個詞,描述比以前所理解的更弱得多的一套保證【4】。
|
||||
2000年以後,非關係(NoSQL)資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上,透過提供新的資料模型選擇(請參閱[第二章](ch2.md))並預設包含複製(第五章)和分割槽(第六章)來進一步提升。事務是這次運動的主要犧牲品:這些新一代資料庫中的許多資料庫完全放棄了事務,或者重新定義了這個詞,描述比以前所理解的更弱得多的一套保證【4】。
|
||||
|
||||
隨著這種新型分散式資料庫的炒作,人們普遍認為事務是可伸縮性的對立面,任何大型系統都必須放棄事務以保持良好的效能和高可用性【5,6】。另一方面,資料庫廠商有時將事務保證作為“重要應用”和“有價值資料”的基本要求。這兩種觀點都是**純粹的誇張**。
|
||||
|
||||
@ -71,7 +71,7 @@ ACID原子性的定義特徵是:**能夠在錯誤時中止事務,丟棄該
|
||||
|
||||
一致性這個詞被賦予太多含義:
|
||||
|
||||
* 在[第5章](ch5.md)中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
* 在[第五章](ch5.md)中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
* [一致性雜湊(Consistency Hashing)](ch6.md#一致性雜湊))是某些系統用於重新分割槽的一種分割槽方法。
|
||||
* 在[CAP定理](ch9.md#CAP定理)中,一致性一詞用於表示[線性一致性](ch9.md#線性一致性)。
|
||||
* 在ACID的上下文中,**一致性**是指資料庫在應用程式的特定概念中處於“良好狀態”。
|
||||
@ -104,7 +104,7 @@ ACID意義上的隔離性意味著,**同時執行的事務是相互隔離的**
|
||||
|
||||
資料庫系統的目的是,提供一個安全的地方儲存資料,而不用擔心丟失。**永續性** 是一個承諾,即一旦事務成功完成,即使發生硬體故障或資料庫崩潰,寫入的任何資料也不會丟失。
|
||||
|
||||
在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或SSD。它通常還包括預寫日誌或類似的檔案(參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
|
||||
在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或SSD。它通常還包括預寫日誌或類似的檔案(請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
|
||||
|
||||
如“[可靠性](ch1.md#可靠性)”一節所述,**完美的永續性是不存在的** :如果所有硬碟和所有備份同時被銷燬,那顯然沒有任何資料庫能救得了你。
|
||||
|
||||
@ -115,8 +115,8 @@ ACID意義上的隔離性意味著,**同時執行的事務是相互隔離的**
|
||||
> 真相是,沒有什麼是完美的:
|
||||
>
|
||||
> * 如果你寫入磁碟然後機器宕機,即使資料沒有丟失,在修復機器或將磁碟轉移到其他機器之前,也是無法訪問的。這種情況下,複製系統可以保持可用性。
|
||||
> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的Bug)可能會一次性摧毀所有副本(參閱「[可靠性](ch1.md#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
|
||||
> * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(參閱「[處理節點宕機](ch5.md#處理節點宕機)」)。
|
||||
> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的Bug)可能會一次性摧毀所有副本(請參閱「[可靠性](ch1.md#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
|
||||
> * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](ch5.md#處理節點宕機)」)。
|
||||
> * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至fsync也不能保證正常工作【12】。硬碟韌體可能有錯誤,就像任何其他型別的軟體一樣【13,14】。
|
||||
> * 儲存引擎和檔案系統之間的微妙互動可能會導致難以追蹤的錯誤,並可能導致磁碟上的檔案在崩潰後被損壞【15,16】。
|
||||
> * 磁碟上的資料可能會在沒有檢測到的情況下逐漸損壞【17】。如果資料已損壞一段時間,副本和最近的備份也可能損壞。這種情況下,需要嘗試從歷史備份中恢復資料。
|
||||
@ -173,27 +173,27 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
- 如果在資料庫正在覆蓋磁碟上的前一個值的過程中電源發生故障,是否最終將新舊值拼接在一起?
|
||||
- 如果另一個客戶端在寫入過程中讀取該文件,是否會看到部分更新的值?
|
||||
|
||||
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
|
||||
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
|
||||
|
||||
一些資料庫也提供更復雜的原子操作[^iv],例如自增操作,這樣就不再需要像 [圖7-1](../img/fig7-1.png) 那樣的讀取-修改-寫入序列了。同樣流行的是 **[比較和設定(CAS, compare-and-set)](#比較並設定(CAS))** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
|
||||
|
||||
[^iv]: 嚴格地說,**原子自增(atomic increment)** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。 在ACID的情況下,它實際上應該被稱為 **隔離的(isolated)** 的或**可序列的(serializable)** 的增量。 但這就太吹毛求疵了。
|
||||
|
||||
這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(參閱“[防止丟失更新](#防止丟失更新)”)。但它們不是通常意義上的事務。CAS以及其他單一物件操作被稱為“輕量級事務”,甚至出於營銷目的被稱為“ACID”【20,21,22】,但是這個術語是誤導性的。事務通常被理解為,**將多個物件上的多個操作合併為一個執行單元的機制**。
|
||||
這些單物件操作很有用,因為它們可以防止在多個客戶端嘗試同時寫入同一個物件時丟失更新(請參閱“[防止丟失更新](#防止丟失更新)”)。但它們不是通常意義上的事務。CAS以及其他單一物件操作被稱為“輕量級事務”,甚至出於營銷目的被稱為“ACID”【20,21,22】,但是這個術語是誤導性的。事務通常被理解為,**將多個物件上的多個操作合併為一個執行單元的機制**。
|
||||
|
||||
#### 多物件事務的需求
|
||||
|
||||
許多分散式資料儲存已經放棄了多物件事務,因為多物件事務很難跨分割槽實現,而且在需要高可用性或高效能的情況下,它們可能會礙事。但說到底,在分散式資料庫中實現事務,並沒有什麼根本性的障礙。[第9章](ch9.md) 將討論分散式事務的實現。
|
||||
許多分散式資料儲存已經放棄了多物件事務,因為多物件事務很難跨分割槽實現,而且在需要高可用性或高效能的情況下,它們可能會礙事。但說到底,在分散式資料庫中實現事務,並沒有什麼根本性的障礙。[第九章](ch9.md) 將討論分散式事務的實現。
|
||||
|
||||
但是我們是否需要多物件事務?**是否有可能只用鍵值資料模型和單物件操作來實現任何應用程式?**
|
||||
|
||||
有一些場景中,單物件插入,更新和刪除是足夠的。但是許多其他場景需要協調寫入幾個不同的物件:
|
||||
|
||||
* 在關係資料模型中,一個表中的行通常具有對另一個表中的行的外來鍵引用。(類似的是,在一個圖資料模型中,一個頂點有著到其他頂點的邊)。多物件事務使你確保這些引用始終有效:當插入幾個相互引用的記錄時,外來鍵必須是正確的和最新的,不然資料就沒有意義。
|
||||
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件——更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(參閱“[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
|
||||
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件——更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(請參閱“[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
|
||||
* 在具有二級索引的資料庫中(除了純粹的鍵值儲存以外幾乎都有),每次更改值時都需要更新索引。從事務角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離性,記錄可能出現在一個索引中,但沒有出現在另一個索引中,因為第二個索引的更新還沒有發生。
|
||||
|
||||
這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在“[弱隔離級別](#弱隔離級別)”中討論這些問題,並在[第12章](ch12.md)中探討其他方法。
|
||||
這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在“[弱隔離級別](#弱隔離級別)”中討論這些問題,並在[第十二章](ch12.md)中探討其他方法。
|
||||
|
||||
#### 處理錯誤和中止
|
||||
|
||||
@ -215,7 +215,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
如果兩個事務不觸及相同的資料,它們可以安全地**並行(parallel)** 執行,因為兩者都不依賴於另一個。當一個事務讀取由另一個事務同時修改的資料時,或者當兩個事務試圖同時修改相同的資料時,併發問題(競爭條件)才會出現。
|
||||
|
||||
併發BUG很難透過測試找到,因為這樣的錯誤只有在特殊時機下才會觸發。這樣的時機可能很少,通常很難重現[^譯註i]。併發性也很難推理,特別是在大型應用中,你不一定知道哪些其他程式碼正在訪問資料庫。在一次只有一個使用者時,應用開發已經很麻煩了,有許多併發使用者使得它更加困難,因為任何一個數據都可能隨時改變。
|
||||
併發BUG很難透過測試找到,因為這樣的錯誤只有在特殊時序下才會觸發。這樣的時序問題可能非常少發生,通常很難重現[^譯註i]。併發性也很難推理,特別是在大型應用中,你不一定知道哪些其他程式碼正在訪問資料庫。在一次只有一個使用者時,應用開發已經很麻煩了,有許多併發使用者使得它更加困難,因為任何一個數據都可能隨時改變。
|
||||
|
||||
[^譯註i]: 軼事:偶然出現的瞬時錯誤有時稱為***Heisenbug***,而確定性的問題對應地稱為***Bohrbugs***
|
||||
|
||||
@ -298,7 +298,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
這種異常被稱為**不可重複讀(nonrepeatable read)**或**讀取偏差(read skew)**:如果Alice在事務結束時再次讀取賬戶1的餘額,她將看到與她之前的查詢中看到的不同的值(600美元)。在讀已提交的隔離條件下,**不可重複讀**被認為是可接受的:Alice看到的帳戶餘額時確實在閱讀時已經提交了。
|
||||
|
||||
> 不幸的是,術語**偏差(skew)** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時機。
|
||||
> 不幸的是,術語**偏差(skew)** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
|
||||
|
||||
對於Alice的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
|
||||
|
||||
@ -308,7 +308,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
***分析查詢和完整性檢查***
|
||||
|
||||
有時,您可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
|
||||
有時,您可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
|
||||
|
||||
**快照隔離(snapshot isolation)**【28】是這個問題最常見的解決方案。想法是,每個事務都從資料庫的**一致快照(consistent snapshot)** 中讀取——也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
|
||||
|
||||
@ -400,7 +400,7 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
|
||||
|
||||
類似地,像MongoDB這樣的文件資料庫提供了對JSON文件的一部分進行本地修改的原子操作,Redis提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如維基頁面的更新涉及到任意文字編輯[^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
|
||||
|
||||
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。參閱“[自動衝突解決](ch5.md#自動衝突解決)”。
|
||||
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱“[自動衝突解決](ch5.md#自動衝突解決)”。
|
||||
|
||||
原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為**遊標穩定性(cursor stability)**【36,37】。另一個選擇是簡單地強制所有的原子操作在單一執行緒上執行。
|
||||
|
||||
@ -453,7 +453,7 @@ UPDATE wiki_pages SET content = '新內容'
|
||||
|
||||
#### 衝突解決和複製
|
||||
|
||||
在複製資料庫中(參見[第5章](ch5.md)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
|
||||
在複製資料庫中(請參閱[第五章](ch5.md)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
|
||||
|
||||
鎖和CAS操作假定只有一個最新的資料副本。但是多主或無主複製的資料庫通常允許多個寫入併發執行,並非同步複製到副本上,因此無法保證只有一個最新資料的副本。所以基於鎖或CAS操作的技術不適用於這種情況。 (我們將在“[線性一致性](ch9.md#線性一致性)”中更詳細地討論這個問題。)
|
||||
|
||||
@ -483,12 +483,12 @@ UPDATE wiki_pages SET content = '新內容'
|
||||
|
||||
這種異常稱為**寫偏差**【28】。它既不是**髒寫**,也不是**丟失更新**,因為這兩個事務正在更新兩個不同的物件(Alice和Bob各自的待命記錄)。在這裡發生的衝突並不是那麼明顯,但是這顯然是一個競爭條件:如果兩個事務一個接一個地執行,那麼第二個醫生就不能歇班了。異常行為只有在事務併發進行時才有可能。
|
||||
|
||||
可以將寫入偏差視為丟失更新問題的一般化。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),則可能發生寫入偏差。在多個事務更新同一個物件的特殊情況下,就會發生髒寫或丟失更新(取決於時機)。
|
||||
可以將寫入偏差視為丟失更新問題的一般化。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),則可能發生寫入偏差。在多個事務更新同一個物件的特殊情況下,就會發生髒寫或丟失更新(取決於時序)。
|
||||
|
||||
我們已經看到,有各種不同的方法來防止丟失的更新。但對於寫偏差,我們的選擇更受限制:
|
||||
|
||||
* 由於涉及多個物件,單物件的原子操作不起作用。
|
||||
* 不幸的是,在一些快照隔離的實現中,自動檢測丟失更新對此並沒有幫助。在PostgreSQL的可重複讀,MySQL/InnoDB的可重複讀,Oracle可序列化或SQL Server的快照隔離級別中,都不會自動檢測寫入偏差【23】。自動防止寫入偏差需要真正的可序列化隔離(請參見“[可序列化](#可序列化)”)。
|
||||
* 不幸的是,在一些快照隔離的實現中,自動檢測丟失更新對此並沒有幫助。在PostgreSQL的可重複讀,MySQL/InnoDB的可重複讀,Oracle可序列化或SQL Server的快照隔離級別中,都不會自動檢測寫入偏差【23】。自動防止寫入偏差需要真正的可序列化隔離(請參閱“[可序列化](#可序列化)”)。
|
||||
* 某些資料庫允許配置約束,然後由資料庫強制執行(例如,唯一性,外來鍵約束或特定值限制)。但是為了指定至少有一名醫生必須線上,需要一個涉及多個物件的約束。大多數資料庫沒有內建對這種約束的支援,但是你可以使用觸發器,或者物化檢視來實現它們,這取決於不同的資料庫【42】。
|
||||
* 如果無法使用可序列化的隔離級別,則此情況下的次優選項可能是顯式鎖定事務所依賴的行。在例子中,你可以寫下如下的程式碼:
|
||||
|
||||
@ -514,7 +514,7 @@ COMMIT;
|
||||
|
||||
***會議室預訂系統***
|
||||
|
||||
比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參見示例7-2)[^ix]。
|
||||
比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參閱示例7-2)[^ix]。
|
||||
|
||||
[^ix]: 在PostgreSQL中,您可以使用範圍型別優雅地執行此操作,但在其他資料庫中並未得到廣泛支援。
|
||||
|
||||
@ -584,7 +584,7 @@ COMMIT;
|
||||
|
||||
- 隔離級別難以理解,並且在不同的資料庫中實現的不一致(例如,“可重複讀”的含義天差地別)。
|
||||
- 光檢查應用程式碼很難判斷在特定的隔離級別執行是否安全。 特別是在大型應用程式中,您可能並不知道併發發生的所有事情。
|
||||
- 沒有檢測競爭條件的好工具。原則上來說,靜態分析可能會有幫助【26】,但研究中的技術還沒法實際應用。併發問題的測試是很難的,因為它們通常是非確定性的 —— 只有在倒黴的時機下才會出現問題。
|
||||
- 沒有檢測競爭條件的好工具。原則上來說,靜態分析可能會有幫助【26】,但研究中的技術還沒法實際應用。併發問題的測試是很難的,因為它們通常是非確定性的 —— 只有在倒黴的時序下才會出現問題。
|
||||
|
||||
這不是一個新問題,從20世紀70年代以來就一直是這樣了,當時首先引入了較弱的隔離級別【2】。一直以來,研究人員的答案都很簡單:使用**可序列化(serializable)** 的隔離級別!
|
||||
|
||||
@ -592,11 +592,11 @@ COMMIT;
|
||||
|
||||
但如果可序列化隔離級別比弱隔離級別的爛攤子要好得多,那為什麼沒有人見人愛?為了回答這個問題,我們需要看看實現可序列化的選項,以及它們如何執行。目前大多數提供可序列化的資料庫都使用了三種技術之一,本章的剩餘部分將會介紹這些技術。
|
||||
|
||||
- 字面意義上地序列順序執行事務(參見“[真的序列執行](#真的序列執行)”)
|
||||
- **兩階段鎖定(2PL, two-phase locking)**,幾十年來唯一可行的選擇。(參見“[兩階段鎖定(2PL)](#兩階段鎖定(2PL))”)
|
||||
- 樂觀併發控制技術,例如**可序列化快照隔離(serializable snapshot isolation)**(參閱“[可序列化快照隔離(SSI)](#可序列化快照隔離(SSI))”
|
||||
- 字面意義上地序列順序執行事務(請參閱“[真的序列執行](#真的序列執行)”)
|
||||
- **兩階段鎖定(2PL, two-phase locking)**,幾十年來唯一可行的選擇。(請參閱“[兩階段鎖定(2PL)](#兩階段鎖定(2PL))”)
|
||||
- 樂觀併發控制技術,例如**可序列化快照隔離(serializable snapshot isolation)**(請參閱“[可序列化快照隔離(SSI)](#可序列化快照隔離(SSI))”
|
||||
|
||||
現在將主要在單節點資料庫的背景下討論這些技術;在[第9章](ch9.md)中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
|
||||
現在將主要在單節點資料庫的背景下討論這些技術;在[第九章](ch9.md)中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
|
||||
|
||||
### 真的序列執行
|
||||
|
||||
@ -606,8 +606,8 @@ COMMIT;
|
||||
|
||||
兩個進展引發了這個反思:
|
||||
|
||||
- RAM足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中。(參閱“[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
|
||||
- 資料庫設計人員意識到OLTP事務通常很短,而且只進行少量的讀寫操作(參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
|
||||
- RAM足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中。(請參閱“[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
|
||||
- 資料庫設計人員意識到OLTP事務通常很短,而且只進行少量的讀寫操作(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
|
||||
|
||||
序列執行事務的方法在VoltDB/H-Store,Redis和Datomic中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統更好,因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個CPU核的吞吐量。為了充分利用單一執行緒,需要與傳統形式的事務不同的結構。
|
||||
|
||||
@ -645,13 +645,13 @@ VoltDB還使用儲存過程進行復制:但不是將事務的寫入結果從
|
||||
|
||||
順序執行所有事務使併發控制簡單多了,但資料庫的事務吞吐量被限制為單機單核的速度。只讀事務可以使用快照隔離在其它地方執行,但對於寫入吞吐量較高的應用,單執行緒事務處理器可能成為一個嚴重的瓶頸。
|
||||
|
||||
為了伸縮至多個CPU核心和多個節點,可以對資料進行分割槽(參見[第6章](ch6.md)),在VoltDB中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的CPU核,事務吞吐量就可以與CPU核數保持線性伸縮【47】。
|
||||
為了伸縮至多個CPU核心和多個節點,可以對資料進行分割槽(請參閱[第六章](ch6.md)),在VoltDB中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的CPU核,事務吞吐量就可以與CPU核數保持線性伸縮【47】。
|
||||
|
||||
但是,對於需要訪問多個分割槽的任何事務,資料庫必須在觸及的所有分割槽之間協調事務。儲存過程需要跨越所有分割槽鎖定執行,以確保整個系統的可序列性。
|
||||
|
||||
由於跨分割槽事務具有額外的協調開銷,所以它們比單分割槽事務慢得多。 VoltDB報告的吞吐量大約是每秒1000個跨分割槽寫入,比單分割槽吞吐量低幾個數量級,並且不能透過增加更多的機器來增加【49】。
|
||||
|
||||
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個二級索引的資料可能需要大量的跨分割槽協調(參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
|
||||
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個二級索引的資料可能需要大量的跨分割槽協調(請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
|
||||
|
||||
#### 序列執行小結
|
||||
|
||||
@ -672,16 +672,16 @@ VoltDB還使用儲存過程進行復制:但不是將事務的寫入結果從
|
||||
|
||||
> #### 2PL不是2PC
|
||||
>
|
||||
> 請注意,雖然兩階段鎖定(2PL)聽起來非常類似於兩階段提交(2PC),但它們是完全不同的東西。我們將在[第9章](ch9.md)討論2PC。
|
||||
> 請注意,雖然兩階段鎖定(2PL)聽起來非常類似於兩階段提交(2PC),但它們是完全不同的東西。我們將在[第九章](ch9.md)討論2PC。
|
||||
|
||||
之前我們看到鎖通常用於防止髒寫(參閱“[沒有髒寫](#沒有髒寫)”一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
|
||||
之前我們看到鎖通常用於防止髒寫(請參閱“[沒有髒寫](#沒有髒寫)”一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
|
||||
|
||||
兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要**獨佔訪問(exclusive access)** 許可權:
|
||||
|
||||
- 如果事務A讀取了一個物件,並且事務B想要寫入該物件,那麼B必須等到A提交或中止才能繼續。 (這確保B不能在A底下意外地改變物件。)
|
||||
- 如果事務A寫入了一個物件,並且事務B想要讀取該物件,則B必須等到A提交或中止才能繼續。 (像[圖7-1](../img/fig7-1.png)那樣讀取舊版本的物件在2PL下是不可接受的。)
|
||||
|
||||
在2PL中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得**讀不阻塞寫,寫也不阻塞讀**(參閱“[實現快照隔離](#實現快照隔離)”),這是2PL和快照隔離之間的關鍵區別。另一方面,因為2PL提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
|
||||
在2PL中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得**讀不阻塞寫,寫也不阻塞讀**(請參閱“[實現快照隔離](#實現快照隔離)”),這是2PL和快照隔離之間的關鍵區別。另一方面,因為2PL提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
|
||||
|
||||
#### 實現兩階段鎖
|
||||
|
||||
@ -704,7 +704,7 @@ VoltDB還使用儲存過程進行復制:但不是將事務的寫入結果從
|
||||
|
||||
傳統的關係資料庫不限制事務的持續時間,因為它們是為等待人類輸入的互動式應用而設計的。因此,當一個事務需要等待另一個事務時,等待的時長並沒有限制。即使你保證所有的事務都很短,如果有多個事務想要訪問同一個物件,那麼可能會形成一個佇列,所以事務可能需要等待幾個其他事務才能完成。
|
||||
|
||||
因此,執行2PL的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(參閱“[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
|
||||
因此,執行2PL的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱“[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
|
||||
|
||||
基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於2PL實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。
|
||||
|
||||
@ -768,11 +768,11 @@ WHERE room_id = 123 AND
|
||||
|
||||
但是,如果有足夠的備用容量,並且事務之間的爭用不是太高,樂觀的併發控制技術往往比悲觀的要好。可交換的原子操作可以減少爭用:例如,如果多個事務同時要增加一個計數器,那麼應用增量的順序(只要計數器不在同一個事務中讀取)就無關緊要了,所以併發增量可以全部應用且無需衝突。
|
||||
|
||||
顧名思義,SSI基於快照隔離——也就是說,事務中的所有讀取都是來自資料庫的一致性快照(參見“[快照隔離和可重複讀取](#快照隔離和可重複讀)”)。與早期的樂觀併發控制技術相比這是主要的區別。在快照隔離的基礎上,SSI添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。
|
||||
顧名思義,SSI基於快照隔離——也就是說,事務中的所有讀取都是來自資料庫的一致性快照(請參閱“[快照隔離和可重複讀取](#快照隔離和可重複讀)”)。與早期的樂觀併發控制技術相比這是主要的區別。在快照隔離的基礎上,SSI添加了一種演算法來檢測寫入之間的序列化衝突,並確定要中止哪些事務。
|
||||
|
||||
#### 基於過時前提的決策
|
||||
|
||||
先前討論了快照隔離中的寫入偏差(參閱“[寫入偏斜與幻讀](#寫入偏斜與幻讀)”)時,我們觀察到一個迴圈模式:事務從資料庫讀取一些資料,檢查查詢的結果,並根據它看到的結果決定採取一些操作(寫入資料庫)。但是,在快照隔離的情況下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在同一時間被修改。
|
||||
先前討論了快照隔離中的寫入偏差(請參閱“[寫入偏斜與幻讀](#寫入偏斜與幻讀)”)時,我們觀察到一個迴圈模式:事務從資料庫讀取一些資料,檢查查詢的結果,並根據它看到的結果決定採取一些操作(寫入資料庫)。但是,在快照隔離的情況下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在同一時間被修改。
|
||||
|
||||
換句話說,事務基於一個**前提(premise)** 採取行動(事務開始時候的事實,例如:“目前有兩名醫生正在值班”)。之後當事務要提交時,原始資料可能已經改變——前提可能不再成立。
|
||||
|
||||
|
48
zh-tw/ch8.md
48
zh-tw/ch8.md
@ -20,11 +20,11 @@
|
||||
|
||||
但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西**都會**出錯[^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事)
|
||||
|
||||
[^i]: 除了一個例外:我們將假定故障是非拜占庭式的(參見“[拜占庭故障](#拜占庭故障)”)。
|
||||
[^i]: 除了一個例外:我們將假定故障是非拜占庭式的(請參閱“[拜占庭故障](#拜占庭故障)”)。
|
||||
|
||||
使用分散式系統與在一臺計算機上編寫軟體有著根本的區別,主要的區別在於,有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中,我們將瞭解實踐中出現的問題,理解我們能夠依賴,和不可以依賴的東西。
|
||||
|
||||
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。 在[第9章](ch9.md)中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。 但首先,在本章中,我們必須瞭解我們面臨的挑戰。
|
||||
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。 在[第九章](ch9.md)中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。 但首先,在本章中,我們必須瞭解我們面臨的挑戰。
|
||||
|
||||
本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。 我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。 所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理髮生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
* 系統越大,其元件之一就越有可能壞掉。隨著時間的推移,壞掉的東西得到修復,新的東西又壞掉,但是在一個有成千上萬個節點的系統中,有理由認為總是有一些東西是壞掉的【7】。當錯誤處理的策略只由簡單放棄組成時,一個大的系統最終會花費大量時間從錯誤中恢復,而不是做有用的工作【8】。
|
||||
|
||||
* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(參閱[第4章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。
|
||||
* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱[第四章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。
|
||||
|
||||
* 在地理位置分散的部署中(保持資料在地理位置上接近使用者以減少訪問延遲),通訊很可能透過網際網路進行,與本地網路相比,通訊速度緩慢且不可靠。超級計算機通常假設它們的所有節點都靠近在一起。
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
1. 請求可能已經丟失(可能有人拔掉了網線)。
|
||||
2. 請求可能正在排隊,稍後將交付(也許網路或接收方過載)。
|
||||
3. 遠端節點可能已經失效(可能是崩潰或關機)。
|
||||
4. 遠端節點可能暫時停止了響應(可能會遇到長時間的垃圾回收暫停;參閱“[程序暫停](#程序暫停)”),但稍後會再次響應。
|
||||
4. 遠端節點可能暫時停止了響應(可能會遇到長時間的垃圾回收暫停;請參閱“[程序暫停](#程序暫停)”),但稍後會再次響應。
|
||||
5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。
|
||||
6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。
|
||||
|
||||
@ -123,20 +123,20 @@
|
||||
|
||||
> #### 網路分割槽
|
||||
>
|
||||
> 當網路的一部分由於網路故障而被切斷時,有時稱為**網路分割槽(network partition)**或**網路斷裂(netsplit)**。在本書中,我們通常會堅持使用更一般的術語**網路故障(network fault)**,以避免與[第6章](ch6.md)討論的儲存系統的分割槽(分片)相混淆。
|
||||
> 當網路的一部分由於網路故障而被切斷時,有時稱為**網路分割槽(network partition)**或**網路斷裂(netsplit)**。在本書中,我們通常會堅持使用更一般的術語**網路故障(network fault)**,以避免與[第六章](ch6.md)討論的儲存系統的分割槽(分片)相混淆。
|
||||
|
||||
即使網路故障在你的環境中非常罕見,故障可能發生的事實,意味著你的軟體需要能夠處理它們。無論何時透過網路進行通訊,都可能會失敗,這是無法避免的。
|
||||
|
||||
如果網路故障的錯誤處理沒有定義與測試,武斷地講,各種錯誤可能都會發生:例如,即使網路恢復【20】,叢集可能會發生**死鎖**,永久無法為請求提供服務,甚至可能會刪除所有的資料【21】。如果軟體被置於意料之外的情況下,它可能會做出出乎意料的事情。
|
||||
|
||||
處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,您確實需要知道您的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是Chaos Monkey背後的想法;參閱“[可靠性](ch1.md#可靠性)”)。
|
||||
處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,您確實需要知道您的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是Chaos Monkey背後的想法;請參閱“[可靠性](ch1.md#可靠性)”)。
|
||||
|
||||
### 檢測故障
|
||||
|
||||
許多系統需要自動檢測故障節點。例如:
|
||||
|
||||
* 負載平衡器需要停止向已死亡的節點轉發請求(即從**移出輪詢列表(out of rotation)**)。
|
||||
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。
|
||||
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。
|
||||
|
||||
不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,您可能會收到一些反饋資訊,明確告訴您某些事情沒有成功:
|
||||
|
||||
@ -155,7 +155,7 @@
|
||||
|
||||
長時間的超時意味著長時間等待,直到一個節點被宣告死亡(在這段時間內,使用者可能不得不等待,或者看到錯誤資訊)。短的超時可以更快地檢測到故障,但有更高地風險誤將一個節點宣佈為失效,而該節點實際上只是暫時地變慢了(例如由於節點或網路上的負載峰值)。
|
||||
|
||||
過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在“[知識、真相與謊言](#知識、真相與謊言)”以及[第9章](ch9.md)和[第11章](ch11.md)中更詳細地討論這個問題。
|
||||
過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在“[知識、真相與謊言](#知識、真相與謊言)”以及[第九章](ch9.md)和[第十一章](ch11.md)中更詳細地討論這個問題。
|
||||
|
||||
當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致**級聯失效(cascading failure)**(在極端情況下,所有節點都宣告對方死亡,所有節點都將停止工作)。
|
||||
|
||||
@ -187,7 +187,7 @@
|
||||
|
||||
所有這些因素都會造成網路延遲的變化。當系統接近其最大容量時,排隊延遲的變化範圍特別大:擁有足夠備用容量的系統可以輕鬆排空佇列,而在高利用率的系統中,很快就能積累很長的佇列。
|
||||
|
||||
在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和CPU(在虛擬機器上執行時)。批處理工作負載(如MapReduce)(參閱[第10章](ch10.md))能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況,如果附近的某個人(嘈雜的鄰居)正在使用大量資源,則網路延遲可能會發生劇烈變化【28,29】。
|
||||
在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和CPU(在虛擬機器上執行時)。批處理工作負載(如MapReduce)(請參閱[第十章](ch10.md))能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況,如果附近的某個人(嘈雜的鄰居)正在使用大量資源,則網路延遲可能會發生劇烈變化【28,29】。
|
||||
|
||||
在這種環境下,您只能透過實驗方式選擇超時:在一段較長的時期內、在多臺機器上測量網路往返時間的分佈,以確定延遲的預期變化。然後,考慮到應用程式的特性,可以確定**故障檢測延遲**與**過早超時風險**之間的適當折衷。
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
>
|
||||
> 相比之下,網際網路動態分享網路頻寬。傳送者互相推擠和爭奪,以讓他們的資料包儘可能快地透過網路,並且網路交換機決定從一個時刻到另一個時刻傳送哪個分組(即,頻寬分配)。這種方法有排隊的缺點,但其優點是它最大限度地利用了電線。電線固定成本,所以如果你更好地利用它,你透過電線傳送的每個位元組都會更便宜。
|
||||
>
|
||||
> CPU也會出現類似的情況:如果您在多個執行緒間動態共享每個CPU核心,則一個執行緒有時必須在作業系統的執行佇列裡等待,而另一個執行緒正在執行,這樣每個執行緒都有可能被暫停一個不定的時間長度。但是,與為每個執行緒分配靜態數量的CPU週期相比,這會更好地利用硬體(參閱“[響應時間保證](#響應時間保證)”)。更好的硬體利用率也是使用虛擬機器的重要動機。
|
||||
> CPU也會出現類似的情況:如果您在多個執行緒間動態共享每個CPU核心,則一個執行緒有時必須在作業系統的執行佇列裡等待,而另一個執行緒正在執行,這樣每個執行緒都有可能被暫停一個不定的時間長度。但是,與為每個執行緒分配靜態數量的CPU週期相比,這會更好地利用硬體(請參閱“[響應時間保證](#響應時間保證)”)。更好的硬體利用率也是使用虛擬機器的重要動機。
|
||||
>
|
||||
> 如果資源是靜態分割槽的(例如,專用硬體和專用頻寬分配),則在某些環境中可以實現**延遲保證**。但是,這是以降低利用率為代價的——換句話說,它是更昂貴的。另一方面,動態資源分配的多租戶提供了更好的利用率,所以它更便宜,但它具有可變延遲的缺點。
|
||||
>
|
||||
@ -324,7 +324,7 @@
|
||||
|
||||
儘管如此,[圖8-3](../img/fig8-3.png)中的時間戳卻無法正確排列事件:寫入`x = 1`的時間戳為42.004秒,但寫入`x = 2`的時間戳為42.003秒,即使`x = 2`在稍後出現。當節點2接收到這兩個事件時,會錯誤地推斷出`x = 1`是最近的值,而丟棄寫入`x = 2`。效果上表現為,客戶端B的增量操作會丟失。
|
||||
|
||||
這種衝突解決策略被稱為**最後寫入勝利(LWW)**,它在多領導者複製和無領導者資料庫(如Cassandra 【53】和Riak 【54】)中被廣泛使用(參見“[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))”一節)。有些實現會在客戶端而不是伺服器上生成時間戳,但這並不能改變LWW的基本問題:
|
||||
這種衝突解決策略被稱為**最後寫入勝利(LWW)**,它在多領導者複製和無領導者資料庫(如Cassandra 【53】和Riak 【54】)中被廣泛使用(請參閱“[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))”一節)。有些實現會在客戶端而不是伺服器上生成時間戳,但這並不能改變LWW的基本問題:
|
||||
|
||||
* 資料庫寫入可能會神祕地消失:具有滯後時鐘的節點無法覆蓋之前具有快速時鐘的節點寫入的值,直到節點之間的時鐘偏差消逝【54,55】。此方案可能導致一定數量的資料被悄悄丟棄,而未嚮應用報告任何錯誤。
|
||||
* LWW無法區分**高頻順序寫入**(在[圖8-3](../img/fig8-3.png)中,客戶端B的增量操作**一定**發生在客戶端A的寫入之後)和**真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止違背因果關係(請參閱“[檢測併發寫入](ch5.md#檢測併發寫入)”)。
|
||||
@ -334,7 +334,7 @@
|
||||
|
||||
NTP同步是否能足夠準確,以至於這種不正確的排序不會發生?也許不能,因為NTP的同步精度本身,除了石英鐘漂移這類誤差源之外,還受到網路往返時間的限制。為了進行正確的排序,你需要一個比測量物件(即網路延遲)要精確得多的時鐘。
|
||||
|
||||
所謂的**邏輯時鐘(logic clock)**【56,57】是基於遞增計數器而不是振盪石英晶體,對於排序事件來說是更安全的選擇(請參見“[檢測併發寫入](ch5.md#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的**日曆時鐘**和**單調鍾**也被稱為**物理時鐘(physical clock)**。我們將在“[順序保證](ch9.md#順序保證)”中來看順序問題。
|
||||
所謂的**邏輯時鐘(logic clock)**【56,57】是基於遞增計數器而不是振盪石英晶體,對於排序事件來說是更安全的選擇(請參閱“[檢測併發寫入](ch5.md#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的**日曆時鐘**和**單調鍾**也被稱為**物理時鐘(physical clock)**。我們將在“[順序保證](ch9.md#順序保證)”中來看順序問題。
|
||||
|
||||
#### 時鐘讀數存在置信區間
|
||||
|
||||
@ -406,7 +406,7 @@ while (true) {
|
||||
* 如果作業系統配置為允許交換到磁碟(頁面交換),則簡單的記憶體訪問可能導致**頁面錯誤(page fault)**,要求將磁碟中的頁面裝入記憶體。當這個緩慢的I/O操作發生時,執行緒暫停。如果記憶體壓力很高,則可能需要將另一個頁面換出到磁碟。在極端情況下,作業系統可能花費大部分時間將頁面交換到記憶體中,而實際上完成的工作很少(這被稱為**抖動(thrashing)**)。為了避免這個問題,通常在伺服器機器上禁用頁面排程(如果你寧願幹掉一個程序來釋放記憶體,也不願意冒抖動風險)。
|
||||
* 可以透過傳送SIGSTOP訊號來暫停Unix程序,例如透過在shell中按下Ctrl-Z。 這個訊號立即阻止程序繼續執行更多的CPU週期,直到SIGCONT恢復為止,此時它將繼續執行。 即使你的環境通常不使用SIGSTOP,也可能由運維工程師意外發送。
|
||||
|
||||
所有這些事件都可以隨時**搶佔(preempt)**正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時機做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
所有這些事件都可以隨時**搶佔(preempt)**正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時序做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
|
||||
當在一臺機器上編寫多執行緒程式碼時,我們有相當好的工具來實現執行緒安全:互斥量,訊號量,原子計數器,無鎖資料結構,阻塞佇列等等。不幸的是,這些工具並不能直接轉化為分散式系統操作,因為分散式系統沒有共享記憶體,只有透過不可靠網路傳送的訊息。
|
||||
|
||||
@ -420,13 +420,13 @@ while (true) {
|
||||
|
||||
> #### 實時是真的嗎?
|
||||
>
|
||||
> 在嵌入式系統中,實時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與Web上對實時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見[第11章](ch11.md))。
|
||||
> 在嵌入式系統中,實時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與Web上對實時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見[第十一章](ch11.md))。
|
||||
|
||||
例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為GC暫停而延遲彈出。
|
||||
|
||||
在系統中提供**實時保證**需要各級軟體棧的支援:一個實時作業系統(RTOS),允許在指定的時間間隔內保證CPU時間的分配。庫函式必須申明最壞情況下的執行時間;動態記憶體分配可能受到限制或完全不允許(實時垃圾收集器存在,但是應用程式仍然必須確保它不會給GC太多的負擔);必須進行大量的測試和測量,以確保達到保證。
|
||||
|
||||
所有這些都需要大量額外的工作,嚴重限制了可以使用的程式語言、庫和工具的範圍(因為大多數語言和工具不提供實時保證)。由於這些原因,開發實時系統非常昂貴,並且它們通常用於安全關鍵的嵌入式裝置。而且,“**實時**”與“**高效能**”不一樣——事實上,實時系統可能具有較低的吞吐量,因為他們必須讓及時響應的優先順序高於一切(另請參見“[延遲和資源利用](#延遲和資源利用)“)。
|
||||
所有這些都需要大量額外的工作,嚴重限制了可以使用的程式語言、庫和工具的範圍(因為大多數語言和工具不提供實時保證)。由於這些原因,開發實時系統非常昂貴,並且它們通常用於安全關鍵的嵌入式裝置。而且,“**實時**”與“**高效能**”不一樣——事實上,實時系統可能具有較低的吞吐量,因為他們必須讓及時響應的優先順序高於一切(另請參閱“[延遲和資源利用](#延遲和資源利用)“)。
|
||||
|
||||
對於大多數伺服器端資料處理系統來說,實時保證是不經濟或不合適的。因此,這些系統必須承受在非實時環境中執行的暫停和時鐘不穩定性。
|
||||
|
||||
@ -436,7 +436,7 @@ while (true) {
|
||||
|
||||
一個新興的想法是將GC暫停視為一個節點的短暫計劃中斷,並在這個節點收集其垃圾的同時,讓其他節點處理來自客戶端的請求。如果執行時可以警告應用程式一個節點很快需要GC暫停,那麼應用程式可以停止向該節點發送新的請求,等待它完成處理未完成的請求,然後在沒有請求正在進行時執行GC。這個技巧向客戶端隱藏了GC暫停,並降低了響應時間的高百分比【70,71】。一些對延遲敏感的金融交易系統【72】使用這種方法。
|
||||
|
||||
這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整GC)之前重新啟動程序【65,73】。一次可以重新啟動一個節點,在計劃重新啟動之前,流量可以從該節點移開,就像[第4章](ch4.md)裡描述的滾動升級一樣。
|
||||
這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整GC)之前重新啟動程序【65,73】。一次可以重新啟動一個節點,在計劃重新啟動之前,流量可以從該節點移開,就像[第四章](ch4.md)裡描述的滾動升級一樣。
|
||||
|
||||
這些措施不能完全阻止垃圾回收暫停,但可以有效地減少它們對應用的影響。
|
||||
|
||||
@ -452,7 +452,7 @@ while (true) {
|
||||
|
||||
幸運的是,我們不需要去搞清楚生命的意義。在分散式系統中,我們可以陳述關於行為(系統模型)的假設,並以滿足這些假設的方式設計實際系統。演算法可以被證明在某個系統模型中正確執行。這意味著即使底層系統模型提供了很少的保證,也可以實現可靠的行為。
|
||||
|
||||
但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在[第9章](ch9.md)中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。
|
||||
但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在[第九章](ch9.md)中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。
|
||||
|
||||
### 真相由多數所定義
|
||||
|
||||
@ -462,17 +462,17 @@ while (true) {
|
||||
|
||||
第三種情況,想象一個經歷了一個長時間**停止所有處理垃圾收集暫停(stop-the-world GC Pause)**的節點。節點的所有執行緒被GC搶佔並暫停一分鐘,因此沒有請求被處理,也沒有響應被髮送。其他節點等待,重試,不耐煩,並最終宣佈節點死亡,並將其丟到靈車上。最後,GC完成,節點的執行緒繼續,好像什麼也沒有發生。其他節點感到驚訝,因為所謂的死亡節點突然從棺材中抬起頭來,身體健康,開始和旁觀者高興地聊天。GC後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。
|
||||
|
||||
這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)“):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。
|
||||
這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)“):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。
|
||||
|
||||
這也包括關於宣告節點死亡的決定。如果法定數量的節點宣告另一個節點已經死亡,那麼即使該節點仍感覺自己活著,它也必須被認為是死的。個體節點必須遵守法定決定並下臺。
|
||||
|
||||
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數——不能同時存在兩個相互衝突的多數決定。當我們在[第9章](ch9.md)中討論**共識演算法(consensus algorithms)**時,我們將更詳細地討論法定人數的應用。
|
||||
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數——不能同時存在兩個相互衝突的多數決定。當我們在[第九章](ch9.md)中討論**共識演算法(consensus algorithms)**時,我們將更詳細地討論法定人數的應用。
|
||||
|
||||
#### 領導者和鎖
|
||||
|
||||
通常情況下,一些東西在一個系統中只能有一個。例如:
|
||||
|
||||
* 資料庫分割槽的領導者只能有一個節點,以避免**腦裂(split brain)**(參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。
|
||||
* 資料庫分割槽的領導者只能有一個節點,以避免**腦裂(split brain)**(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。
|
||||
* 特定資源的鎖或物件只允許一個事務/客戶端持有,以防同時寫入和損壞。
|
||||
* 一個特定的使用者名稱只能被一個使用者所註冊,因為使用者名稱必須唯一標識一個使用者。
|
||||
|
||||
@ -516,7 +516,7 @@ while (true) {
|
||||
|
||||
> ### 拜占庭將軍問題
|
||||
>
|
||||
> 拜占庭將軍問題是對所謂“兩將軍問題”的泛化【78】,它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地,他們只能透過信使進行溝通,信使有時會被延遲或丟失(就像網路中的資訊包一樣)。我們將在[第9章](ch9.md)討論這個共識問題。
|
||||
> 拜占庭將軍問題是對所謂“兩將軍問題”的泛化【78】,它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地,他們只能透過信使進行溝通,信使有時會被延遲或丟失(就像網路中的資訊包一樣)。我們將在[第九章](ch9.md)討論這個共識問題。
|
||||
>
|
||||
> 在這個問題的拜占庭版本里,有n位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。
|
||||
>
|
||||
@ -545,7 +545,7 @@ while (true) {
|
||||
|
||||
### 系統模型與現實
|
||||
|
||||
已經有很多演算法被設計以解決分散式系統問題——例如,我們將在[第9章](ch9.md)討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。
|
||||
已經有很多演算法被設計以解決分散式系統問題——例如,我們將在[第九章](ch9.md)討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。
|
||||
|
||||
演算法的編寫方式不應該過分依賴於執行的硬體和軟體配置的細節。這就要求我們以某種方式將我們期望在系統中發生的錯誤形式化。我們透過定義一個系統模型來做到這一點,這個模型是一個抽象,描述一個演算法可以假設的事情。
|
||||
|
||||
@ -584,7 +584,7 @@ while (true) {
|
||||
|
||||
為了定義演算法是正確的,我們可以描述它的屬性。例如,排序演算法的輸出具有如下特性:對於輸出列表中的任何兩個不同的元素,左邊的元素比右邊的元素小。這只是定義對列表進行排序含義的一種形式方式。
|
||||
|
||||
同樣,我們可以寫下我們想要的分散式演算法的屬性來定義它的正確含義。例如,如果我們正在為一個鎖生成防護令牌(參閱“[防護令牌](#防護令牌)”),我們可能要求演算法具有以下屬性:
|
||||
同樣,我們可以寫下我們想要的分散式演算法的屬性來定義它的正確含義。例如,如果我們正在為一個鎖生成防護令牌(請參閱“[防護令牌](#防護令牌)”),我們可能要求演算法具有以下屬性:
|
||||
|
||||
***唯一性(uniqueness)***
|
||||
|
||||
@ -621,7 +621,7 @@ while (true) {
|
||||
|
||||
例如,在崩潰-恢復(crash-recovery)模型中的演算法通常假設穩定儲存器中的資料在崩潰後可以倖存。但是,如果磁碟上的資料被破壞,或者由於硬體錯誤或錯誤配置導致資料被清除,會發生什麼情況【91】?如果伺服器存在韌體錯誤並且在重新啟動時無法識別其硬碟驅動器,即使驅動器已正確連線到伺服器,那又會發生什麼情況【92】?
|
||||
|
||||
法定人數演算法(參見“[讀寫法定人數](ch5.md#讀寫法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。
|
||||
法定人數演算法(請參閱“[讀寫法定人數](ch5.md#讀寫法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。
|
||||
|
||||
演算法的理論描述可以簡單宣稱一些事是不會發生的——在非拜占庭式系統中,我們確實需要對可能發生和不可能發生的故障做出假設。然而,真實世界的實現,仍然會包括處理“假設上不可能”情況的程式碼,即使程式碼可能就是`printf("Sucks to be you")`和`exit(666)`,實際上也就是留給運維來擦屁股【93】。(這可以說是電腦科學和軟體工程間的一個差異)。
|
||||
|
||||
|
124
zh-tw/ch9.md
124
zh-tw/ch9.md
@ -9,11 +9,11 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
正如[第8章](ch8.md)所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法—— 即使某些內部元件出現故障,服務也能正常執行。
|
||||
正如[第八章](ch8.md)所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法—— 即使某些內部元件出現故障,服務也能正常執行。
|
||||
|
||||
在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設[第8章](ch8.md)的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複遞送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰。
|
||||
在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設[第八章](ch8.md)的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複遞送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰。
|
||||
|
||||
構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與[第7章](ch7.md)中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。
|
||||
構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與[第七章](ch7.md)中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。
|
||||
|
||||
現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是**共識(consensus)**:**就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
大多數複製的資料庫至少提供了**最終一致性**,這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間,那麼最終所有的讀取請求都會返回相同的值【1】。換句話說,不一致性是暫時的,最終會自行解決(假設網路中的任何故障最終都會被修復)。最終一致性的一個更好的名字可能是**收斂(convergence)**,因為我們預計所有的副本最終會收斂到相同的值【2】。
|
||||
|
||||
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前,讀操作可能會返回任何東西或什麼都沒有【1】。例如,如果你寫入了一個值,然後立即再次讀取,這並不能保證你能看到剛才寫入的值,因為讀請求可能會被路由到另外的副本上。(參閱“[讀己之寫](ch5.md#讀己之寫)” )。
|
||||
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前,讀操作可能會返回任何東西或什麼都沒有【1】。例如,如果你寫入了一個值,然後立即再次讀取,這並不能保證你能看到剛才寫入的值,因為讀請求可能會被路由到另外的副本上。(請參閱“[讀己之寫](ch5.md#讀己之寫)” )。
|
||||
|
||||
對於應用開發人員而言,最終一致性是很困難的,因為它與普通單執行緒程式中變數的行為有很大區別。對於後者,如果將一個值賦給一個變數,然後很快地再次讀取,不可能讀到舊的值,或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數,但實際上它有更復雜的語義【3】。
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
本章將探索資料系統可能選擇提供的更強一致性模型。它不是免費的:具有較強保證的系統可能會比保證較差的系統具有更差的效能或更少的容錯性。儘管如此,更強的保證能夠吸引人,因為它們更容易用對。只有見過不同的一致性模型後,才能更好地決定哪一個最適合自己的需求。
|
||||
|
||||
**分散式一致性模型**和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】(參見“[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了**避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於**在面對延遲和故障時如何協調副本間的狀態**。
|
||||
**分散式一致性模型**和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】(請參閱“[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了**避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於**在面對延遲和故障時如何協調副本間的狀態**。
|
||||
|
||||
本章涵蓋了廣泛的話題,但我們將會看到這些領域實際上是緊密聯絡在一起的:
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
|
||||
為了簡單起見,[圖9-2](../img/fig9-2.png)採用了使用者請求的視角,而不是資料庫內部的視角。每個柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間——只知道它發生在傳送請求和接收響應的之間的某個時刻。[^i]
|
||||
|
||||
[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。即使真實的系統通常沒有準確的時鐘(參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”),但這種假設是允許的:為了分析分散式演算法,我們可以假設一個精確的全域性時鐘存在,不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和NTP產生的實時逼近。
|
||||
[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。即使真實的系統通常沒有準確的時鐘(請參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”),但這種假設是允許的:為了分析分散式演算法,我們可以假設一個精確的全域性時鐘存在,不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和NTP產生的實時逼近。
|
||||
|
||||
在這個例子中,暫存器有兩種型別的操作:
|
||||
|
||||
@ -139,15 +139,15 @@
|
||||
>
|
||||
> ***可序列化***
|
||||
>
|
||||
> **可序列化(Serializability)**是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)——參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”。它確保事務的行為,與它們按照**某種**順序依次執行的結果相同(每個事務在下一個事務開始之前執行完成)。這種執行順序可以與事務實際執行的順序不同。【12】。
|
||||
> **可序列化(Serializability)**是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)——請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”。它確保事務的行為,與它們按照**某種**順序依次執行的結果相同(每個事務在下一個事務開始之前執行完成)。這種執行順序可以與事務實際執行的順序不同。【12】。
|
||||
>
|
||||
> ***線性一致性***
|
||||
>
|
||||
> **線性一致性(Linearizability)**是讀取和寫入暫存器(單個物件)的**新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(參閱“[寫入偏差和幻讀](ch7.md#寫入偏斜與幻讀)”),除非採取其他措施(例如[物化衝突](ch7.md#物化衝突))。
|
||||
> **線性一致性(Linearizability)**是讀取和寫入暫存器(單個物件)的**新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(請參閱“[寫入偏差和幻讀](ch7.md#寫入偏斜與幻讀)”),除非採取其他措施(例如[物化衝突](ch7.md#物化衝突))。
|
||||
>
|
||||
> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或**強的單副本可序列化(strong-1SR)**【4,13】。基於兩階段鎖定的可序列化實現(參見“[兩階段鎖定(2PL)](ch7.md#兩階段鎖定(2PL))”一節)或**真的序列執行**(參見第“[真的序列執行](ch7.md#真的序列執行)”)通常是線性一致性的。
|
||||
> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或**強的單副本可序列化(strong-1SR)**【4,13】。基於兩階段鎖定的可序列化實現(請參閱“[兩階段鎖定(2PL)](ch7.md#兩階段鎖定(2PL))”一節)或**真的序列執行**(請參閱第“[真的序列執行](ch7.md#真的序列執行)”)通常是線性一致性的。
|
||||
>
|
||||
> 但是,可序列化的快照隔離(參見“[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI))”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於**它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。
|
||||
> 但是,可序列化的快照隔離(請參閱“[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI))”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於**它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。
|
||||
|
||||
|
||||
### 依賴線性一致性
|
||||
@ -158,9 +158,9 @@
|
||||
|
||||
一個使用單主複製的系統,需要確保領導真的只有一個,而不是幾個(腦裂)。一種選擇領導者的方法是使用鎖:每個節點在啟動時嘗試獲取鎖,成功者成為領導者【14】。不管這個鎖是如何實現的,它必須是線性一致的:所有節點必須就哪個節點擁有鎖達成一致,否則就沒用了。
|
||||
|
||||
諸如Apache ZooKeeper 【15】和etcd 【16】之類的協調服務通常用於實現分散式鎖和領導者選舉。它們使用一致性演算法,以容錯的方式實現線性一致的操作(在本章後面的“[容錯共識](#容錯共識)”中討論此類演算法)[^iii]。還有許多微妙的細節來正確地實現鎖和領導者選舉(例如,參閱“[領導者和鎖](ch8.md#領導者和鎖)”中的防護問題),而像Apache Curator 【17】這樣的庫則透過在ZooKeeper之上提供更高級別的配方來提供幫助。但是,線性一致性儲存服務是這些協調任務的基礎。
|
||||
諸如Apache ZooKeeper 【15】和etcd 【16】之類的協調服務通常用於實現分散式鎖和領導者選舉。它們使用一致性演算法,以容錯的方式實現線性一致的操作(在本章後面的“[容錯共識](#容錯共識)”中討論此類演算法)[^iii]。還有許多微妙的細節來正確地實現鎖和領導者選舉(例如,請參閱“[領導者和鎖](ch8.md#領導者和鎖)”中的防護問題),而像Apache Curator 【17】這樣的庫則透過在ZooKeeper之上提供更高級別的配方來提供幫助。但是,線性一致性儲存服務是這些協調任務的基礎。
|
||||
|
||||
[^iii]: 嚴格地說,ZooKeeper和etcd提供線性一致性的寫操作,但讀取可能是陳舊的,因為預設情況下,它們可以由任何一個副本提供服務。你可以選擇請求線性一致性讀取:etcd稱之為**法定人數讀取(quorum read)**【16】,而在ZooKeeper中,你需要在讀取之前呼叫`sync()`【15】。參閱“[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”。
|
||||
[^iii]: 嚴格地說,ZooKeeper和etcd提供線性一致性的寫操作,但讀取可能是陳舊的,因為預設情況下,它們可以由任何一個副本提供服務。你可以選擇請求線性一致性讀取:etcd稱之為**法定人數讀取(quorum read)**【16】,而在ZooKeeper中,你需要在讀取之前呼叫`sync()`【15】。請參閱“[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”。
|
||||
|
||||
分散式鎖也在一些分散式資料庫(如Oracle Real Application Clusters(RAC)【18】)中更多的粒度級別上使用。RAC對每個磁碟頁面使用一個鎖,多個節點共享對同一個磁碟儲存系統的訪問許可權。由於這些線性一致的鎖處於事務執行的關鍵路徑上,RAC部署通常具有用於資料庫節點之間通訊的專用叢集互連網路。
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
|
||||
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如[圖9-5](../img/fig9-5.png)所示。
|
||||
|
||||
影象縮放器需要明確的指令來執行尺寸縮放作業,指令是Web伺服器透過訊息佇列傳送的(參閱[第11章](ch11.md))。 Web伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
|
||||
影象縮放器需要明確的指令來執行尺寸縮放作業,指令是Web伺服器透過訊息佇列傳送的(請參閱[第十一章](ch11.md))。 Web伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
|
||||
![](../img/fig9-5.png)
|
||||
**圖9-5 Web伺服器和影象縮放器透過檔案儲存和訊息佇列進行通訊,開啟競爭條件的可能性。**
|
||||
|
||||
@ -198,15 +198,15 @@
|
||||
|
||||
由於線性一致性本質上意味著“表現得好像只有一個數據副本,而且所有的操作都是原子的”,所以最簡單的答案就是,真的只用一個數據副本。但是這種方法無法容錯:如果持有該副本的節點失效,資料將會丟失,或者至少無法訪問,直到節點重新啟動。
|
||||
|
||||
使系統容錯最常用的方法是使用複製。我們再來回顧[第5章](ch5.md)中的複製方法,並比較它們是否可以滿足線性一致性:
|
||||
使系統容錯最常用的方法是使用複製。我們再來回顧[第五章](ch5.md)中的複製方法,並比較它們是否可以滿足線性一致性:
|
||||
|
||||
***單主複製(可能線性一致)***
|
||||
|
||||
在具有單主複製功能的系統中(參見“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)**是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
在具有單主複製功能的系統中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)**是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(參閱“[分散式事務與共識](#分散式事務與共識)”)。
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(請參閱“[分散式事務與共識](#分散式事務與共識)”)。
|
||||
|
||||
從主庫讀取依賴一個假設,你確定地知道領導者是誰。正如在“[真相由多數所定義](ch8.md#真相由多數所定義)”中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此——如果具有錯覺的領導者繼續為請求提供服務,可能違反線性一致性【20】。使用非同步複製,故障切換時甚至可能會丟失已提交的寫入(參閱“[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
|
||||
從主庫讀取依賴一個假設,你確定地知道領導者是誰。正如在“[真相由多數所定義](ch8.md#真相由多數所定義)”中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此——如果具有錯覺的領導者繼續為請求提供服務,可能違反線性一致性【20】。使用非同步複製,故障切換時甚至可能會丟失已提交的寫入(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
|
||||
|
||||
***共識演算法(線性一致)***
|
||||
|
||||
@ -214,13 +214,13 @@
|
||||
|
||||
***多主複製(非線性一致)***
|
||||
|
||||
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
|
||||
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
|
||||
|
||||
***無主複製(也許不是線性一致的)***
|
||||
|
||||
對於無領導者複製的系統(Dynamo風格;參閱“[無主複製](ch5.md#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r> n$ )可以獲得“強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
|
||||
對於無領導者複製的系統(Dynamo風格;請參閱“[無主複製](ch5.md#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r> n$ )可以獲得“強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
|
||||
|
||||
基於日曆時鐘(例如,在Cassandra中;參見“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)的“最後寫入勝利”衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(參見“[寬鬆的法定人數與提示移交](ch5.md#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也只是可能的,如下節所示。
|
||||
基於日曆時鐘(例如,在Cassandra中;請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)的“最後寫入勝利”衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(請參閱“[寬鬆的法定人數與提示移交](ch5.md#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也只是可能的,如下節所示。
|
||||
|
||||
#### 線性一致性和法定人數
|
||||
|
||||
@ -234,7 +234,7 @@
|
||||
|
||||
法定人數條件滿足( $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】;但是由於使用了最後寫入勝利的衝突解決方案,當同一個鍵有多個併發寫入時,將不能保證線性一致性。
|
||||
|
||||
而且,這種方式只能實現線性一致的讀寫;不能實現線性一致的比較和設定(CAS)操作,因為它需要一個共識演算法【28】。
|
||||
|
||||
@ -246,7 +246,7 @@
|
||||
|
||||
一些複製方法可以提供線性一致性,另一些複製方法則不能,因此深入地探討線性一致性的優缺點是很有趣的。
|
||||
|
||||
我們已經在[第五章](ch5.md)中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(參閱“[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖9-7](../img/fig9-7.png)說明了這種部署的一個例子。
|
||||
我們已經在[第五章](ch5.md)中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(請參閱“[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖9-7](../img/fig9-7.png)說明了這種部署的一個例子。
|
||||
|
||||
![](../img/fig9-7.png)
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
|
||||
在分散式系統中有更多有趣的“不可能”的結果【41】,且CAP定理現在已經被更精確的結果取代【2,42】,所以它現在基本上成了歷史古蹟了。
|
||||
|
||||
[^vi]: 正如“[真實世界的網路故障](ch8.md#真實世界的網路故障)”中所討論的,本書使用**分割槽(partition)**指代將大資料集細分為小資料集的操作(分片;參見[第6章](ch6.md))。與之對應的是,**網路分割槽(network partition)**是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是CAP的P,所以這種情況下我們無法避免混亂。
|
||||
[^vi]: 正如“[真實世界的網路故障](ch8.md#真實世界的網路故障)”中所討論的,本書使用**分割槽(partition)**指代將大資料集細分為小資料集的操作(分片;請參閱[第六章](ch6.md))。與之對應的是,**網路分割槽(network partition)**是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是CAP的P,所以這種情況下我們無法避免混亂。
|
||||
|
||||
#### 線性一致性和網路延遲
|
||||
|
||||
@ -299,7 +299,7 @@
|
||||
|
||||
許多分散式資料庫也是如此:它們是**為了提高效能**而選擇了犧牲線性一致性,而不是為了容錯【46】。線性一致的速度很慢——這始終是事實,而不僅僅是網路故障期間。
|
||||
|
||||
能找到一個更高效的線性一致儲存實現嗎?看起來答案是否定的:Attiya和Welch 【47】證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中(參見“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在[第12章](ch12.md)中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。
|
||||
能找到一個更高效的線性一致儲存實現嗎?看起來答案是否定的:Attiya和Welch 【47】證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中(請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在[第十二章](ch12.md)中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。
|
||||
|
||||
|
||||
|
||||
@ -309,9 +309,9 @@
|
||||
|
||||
**順序(ordering)**這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過**順序**的上下文:
|
||||
|
||||
* 在[第5章](ch5.md)中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定**寫入順序(order of write)**——也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。
|
||||
* 在[第7章](ch7.md)中討論的**可序列化**,是關於事務表現的像按**某種先後順序(some sequential order)**執行的保證。它可以字面意義上地以**序列順序(serial order)**執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
|
||||
* 在[第8章](ch8.md)討論過的在分散式系統中使用時間戳和時鐘(參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
|
||||
* 在[第五章](ch5.md)中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定**寫入順序(order of write)**——也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。
|
||||
* 在[第七章](ch7.md)中討論的**可序列化**,是關於事務表現的像按**某種先後順序(some sequential order)**執行的保證。它可以字面意義上地以**序列順序(serial order)**執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
|
||||
* 在[第八章](ch8.md)討論過的在分散式系統中使用時間戳和時鐘(請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
|
||||
|
||||
事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。
|
||||
|
||||
@ -323,7 +323,7 @@
|
||||
* [圖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)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI))透過跟蹤事務之間的因果依賴來檢測寫偏差。
|
||||
* 事務之間**寫偏差(write skew)**的例子(請參閱“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”)也說明了因果依賴:在[圖7-8](../img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI))透過跟蹤事務之間的因果依賴來檢測寫偏差。
|
||||
* 在愛麗絲和鮑勃看球的例子中([圖9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在“[跨通道的時序依賴](#跨通道的時序依賴)”一節中,以“影象大小調整服務”的偽裝再次出現。
|
||||
|
||||
因果關係對事件施加了一種**順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。
|
||||
@ -348,7 +348,7 @@
|
||||
|
||||
***因果性***
|
||||
|
||||
我們說過,如果兩個操作都沒有在彼此**之前發生**,那麼這兩個操作是併發的(參閱[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
|
||||
我們說過,如果兩個操作都沒有在彼此**之前發生**,那麼這兩個操作是併發的(請參閱[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
|
||||
|
||||
因此,根據這個定義,線上性一致的資料儲存中是不存在併發操作的:必須有且僅有一條時間線,所有的操作都在這條時間線上,構成一個全序關係。可能有幾個請求在等待處理,但是資料儲存確保了每個請求都是在唯一時間線上的某個時間點自動處理的,不存在任何併發。
|
||||
|
||||
@ -394,14 +394,14 @@
|
||||
|
||||
[^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的UUID,並按照字典序比較UUID,以定義操作的全序。這是一個有效的全序,但是隨機的UUID並不能告訴你哪個操作先發生,或者操作是否為併發的。
|
||||
|
||||
在單主複製的資料庫中(參見“[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。
|
||||
在單主複製的資料庫中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。
|
||||
|
||||
#### 非因果序列號生成器
|
||||
|
||||
如果主庫不存在(可能因為使用了多主資料庫或無主資料庫,或者因為使用了分割槽的資料庫),如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法:
|
||||
|
||||
* 每個節點都可以生成自己獨立的一組序列號。例如有兩個節點,一個節點只能生成奇數,而另一個節點只能生成偶數。通常,可以在序列號的二進位制表示中預留一些位,用於唯一的節點識別符號,這樣可以確保兩個不同的節點永遠不會生成相同的序列號。
|
||||
* 可以將日曆時鐘(物理時鐘)的時間戳附加到每個操作上【55】。這種時間戳並不連續,但是如果它具有足夠高的解析度,那也許足以提供一個操作的全序關係。這一事實應用於 *最後寫入勝利* 的衝突解決方法中(參閱“[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。
|
||||
* 可以將日曆時鐘(物理時鐘)的時間戳附加到每個操作上【55】。這種時間戳並不連續,但是如果它具有足夠高的解析度,那也許足以提供一個操作的全序關係。這一事實應用於 *最後寫入勝利* 的衝突解決方法中(請參閱“[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。
|
||||
* 可以預先分配序列號區塊。例如,節點 A 可能要求從序列號1到1,000區塊的所有權,而節點 B 可能要求序列號1,001到2,000區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號,並在序列號告急時請求分配一個新的區塊。
|
||||
|
||||
這三個選項都比單一主庫的自增計數器表現要好,並且更具可伸縮性。它們為每個操作生成一個唯一的,近似自增的序列號。然而它們都有同一個問題:生成的序列號與因果不一致。
|
||||
@ -485,7 +485,7 @@
|
||||
|
||||
像ZooKeeper和etcd這樣的共識服務實際上實現了全序廣播。這一事實暗示了全序廣播與共識之間有著緊密聯絡,我們將在本章稍後進行探討。
|
||||
|
||||
全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為**狀態機複製(state machine replication)**【60】,我們將在[第11章](ch11.md)中重新回到這個概念。
|
||||
全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為**狀態機複製(state machine replication)**【60】,我們將在[第十一章](ch11.md)中重新回到這個概念。
|
||||
|
||||
與之類似,可以使用全序廣播來實現可序列化的事務:如“[真的序列執行](ch7.md#真的序列執行)”中所述,如果每個訊息都表示一個確定性事務,以儲存過程的形式來執行,且每個節點都以相同的順序處理這些訊息,那麼資料庫的分割槽和副本就可以相互保持一致【61】。
|
||||
|
||||
@ -493,7 +493,7 @@
|
||||
|
||||
考量全序廣播的另一種方式是,這是一種建立日誌的方式(如在複製日誌、事務日誌或預寫式日誌中):傳遞訊息就像追加寫入日誌。由於所有節點必須以相同的順序傳遞相同的訊息,因此所有節點都可以讀取日誌,並看到相同的訊息序列。
|
||||
|
||||
全序廣播對於實現提供防護令牌的鎖服務也很有用(參見“[防護令牌](ch8.md#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在ZooKeeper中,這個序列號被稱為`zxid` 【15】。
|
||||
全序廣播對於實現提供防護令牌的鎖服務也很有用(請參閱“[防護令牌](ch8.md#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在ZooKeeper中,這個序列號被稱為`zxid` 【15】。
|
||||
|
||||
#### 使用全序廣播實現線性一致的儲存
|
||||
|
||||
@ -521,7 +521,7 @@
|
||||
|
||||
* 你可以透過在日誌中追加一條訊息,然後讀取日誌,直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點。 (etcd的法定人數讀取有些類似這種情況【16】。)
|
||||
* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。 (這是Zookeeper `sync()` 操作背後的思想【15】)。
|
||||
* 你可以從同步更新的副本中進行讀取,因此可以確保結果是最新的。 (這種技術用於鏈式複製(chain replication)【63】;參閱“[關於複製的研究](ch5.md#關於複製的研究)”。)
|
||||
* 你可以從同步更新的副本中進行讀取,因此可以確保結果是最新的。 (這種技術用於鏈式複製(chain replication)【63】;請參閱“[關於複製的研究](ch5.md#關於複製的研究)”。)
|
||||
|
||||
#### 使用線性一致性儲存實現全序廣播
|
||||
|
||||
@ -545,26 +545,26 @@
|
||||
|
||||
**共識**是分散式計算中最重要也是最基本的問題之一。從表面上看似乎很簡單:非正式地講,目標只是**讓幾個節點達成一致(get serveral nodes to agree on something)**。你也許會認為這不會太難。不幸的是,許多出故障的系統都是因為錯誤地輕信這個問題很容易解決。
|
||||
|
||||
儘管共識非常重要,但關於它的內容出現在本書的後半部分,因為這個主題非常微妙,欣賞細微之處需要一些必要的知識。即使在學術界,對共識的理解也是在幾十年的過程中逐漸沉澱而來,一路上也有著許多誤解。現在我們已經討論了複製([第5章](ch5.md)),事務([第7章](ch7.md)),系統模型([第8章](ch8.md)),線性一致以及全序廣播(本章),我們終於準備好解決共識問題了。
|
||||
儘管共識非常重要,但關於它的內容出現在本書的後半部分,因為這個主題非常微妙,欣賞細微之處需要一些必要的知識。即使在學術界,對共識的理解也是在幾十年的過程中逐漸沉澱而來,一路上也有著許多誤解。現在我們已經討論了複製([第五章](ch5.md)),事務([第七章](ch7.md)),系統模型([第八章](ch8.md)),線性一致以及全序廣播(本章),我們終於準備好解決共識問題了。
|
||||
|
||||
節點能達成一致,在很多場景下都非常重要,例如:
|
||||
|
||||
***領導選舉***
|
||||
|
||||
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
|
||||
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
|
||||
|
||||
***原子提交***
|
||||
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)**問題[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 參閱“[三階段提交](#三階段提交)”。
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 請參閱“[三階段提交](#三階段提交)”。
|
||||
|
||||
> ### 共識的不可能性
|
||||
>
|
||||
> 你可能已經聽說過以作者Fischer,Lynch和Paterson命名的FLP結果【68】,它證明,如果存在節點可能崩潰的風險,則不存在**總是**能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事?
|
||||
>
|
||||
> 答案是FLP結果是在**非同步系統模型**中被證明的(參閱“[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用**超時**或其他方法來識別可疑的崩潰節點(即使懷疑有時是錯誤的),則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數,也足以繞過這個不可能的結果【69】。
|
||||
> 答案是FLP結果是在**非同步系統模型**中被證明的(請參閱“[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用**超時**或其他方法來識別可疑的崩潰節點(即使懷疑有時是錯誤的),則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數,也足以繞過這個不可能的結果【69】。
|
||||
>
|
||||
> 因此,雖然FLP是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。
|
||||
|
||||
@ -576,17 +576,17 @@
|
||||
|
||||
### 原子提交與兩階段提交(2PC)
|
||||
|
||||
在[第7章](ch7.md)中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
|
||||
在[第七章](ch7.md)中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
|
||||
|
||||
原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構—— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。
|
||||
原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構—— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。
|
||||
|
||||
#### 從單節點到分散式原子提交
|
||||
|
||||
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。
|
||||
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。
|
||||
|
||||
因此,在單個節點上,事務的提交主要取決於資料持久化落盤的**順序**:首先是資料,然後是提交記錄【72】。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻:在此之前,仍有可能中止(由於崩潰),但在此之後,事務已經提交(即使資料庫崩潰)。因此,是單一的裝置(連線到單個磁碟的控制器,且掛載在單臺機器上)使得提交具有原子性。
|
||||
|
||||
但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的二級索引(其中索引條目可能位於與主資料不同的節點上;參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。大多數“NoSQL”分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(參見“[實踐中的分散式事務](#實踐中的分散式事務)”)。
|
||||
但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的二級索引(其中索引條目可能位於與主資料不同的節點上;請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。大多數“NoSQL”分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(請參閱“[實踐中的分散式事務](#實踐中的分散式事務)”)。
|
||||
|
||||
在這些情況下,僅向所有節點發送提交請求並獨立提交每個節點的事務是不夠的。這樣很容易發生違反原子性的情況:提交在某些節點上成功,而在其他節點上失敗:
|
||||
|
||||
@ -612,7 +612,7 @@
|
||||
|
||||
> #### 不要把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。
|
||||
|
||||
@ -659,7 +659,7 @@
|
||||
|
||||
兩階段提交被稱為**阻塞(blocking)**原子提交協議,因為存在2PC可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為**非阻塞(nonblocking)**的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。
|
||||
|
||||
作為2PC的替代方案,已經提出了一種稱為**三階段提交(3PC)**的演算法【13,80】。然而,3PC假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見[第8章](ch8.md)),它並不能保證原子性。
|
||||
作為2PC的替代方案,已經提出了一種稱為**三階段提交(3PC)**的演算法【13,80】。然而,3PC假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見[第八章](ch8.md)),它並不能保證原子性。
|
||||
|
||||
通常,非阻塞原子提交需要一個**完美的故障檢測器(perfect failure detector)**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中,超時並不是一種可靠的故障檢測機制,因為即使沒有節點崩潰,請求也可能由於網路問題而超時。出於這個原因,2PC仍然被使用,儘管大家都清楚可能存在協調者故障的問題。
|
||||
|
||||
@ -691,7 +691,7 @@
|
||||
|
||||
然而,只有當所有受事務影響的系統都使用同樣的**原子提交協議(atomic commit protocl)**時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。
|
||||
|
||||
在[第11章](ch11.md)中將再次回到“恰好一次”訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
|
||||
在[第十一章](ch11.md)中將再次回到“恰好一次”訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
|
||||
|
||||
#### XA事務
|
||||
|
||||
@ -709,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分鐘。如果協調者的日誌由於某種原因徹底丟失,這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。
|
||||
|
||||
@ -731,10 +731,10 @@
|
||||
|
||||
* 如果協調者沒有複製,而是隻在單臺機器上執行,那麼它是整個系統的失效單點(因為它的失效會導致其他應用伺服器阻塞在存疑事務持有的鎖上)。令人驚訝的是,許多協調者實現預設情況下並不是高可用的,或者只有基本的複製支援。
|
||||
* 許多伺服器端應用都是使用無狀態模式開發的(受HTTP的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分—— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。
|
||||
* 由於XA需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與SSI(參閱[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI) ))協同工作,因為這需要一個跨系統定位衝突的協議。
|
||||
* 由於XA需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與SSI(請參閱[可序列化快照隔離(SSI)](ch7.md#可序列化快照隔離(SSI) ))協同工作,因為這需要一個跨系統定位衝突的協議。
|
||||
* 對於資料庫內部的分散式事務(不是XA),限制沒有這麼大 —— 例如,分散式版本的SSI是可能的。然而仍然存在問題:2PC成功提交一個事務需要所有參與者的響應。因此,如果系統的**任何**部分損壞,事務也會失敗。因此,分散式事務又有**擴大失效(amplifying failures)**的趨勢,這又與我們構建容錯系統的目標背道而馳。
|
||||
|
||||
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在[第11章](ch11.md) 和[第12章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於**共識**的話題。
|
||||
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於**共識**的話題。
|
||||
|
||||
|
||||
|
||||
@ -767,11 +767,11 @@
|
||||
|
||||
如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為“獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。
|
||||
|
||||
**終止**屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定。 (**終止**是一種**活性屬性**,而另外三種是**安全屬性** —— 參見“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
**終止**屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定。 (**終止**是一種**活性屬性**,而另外三種是**安全屬性** —— 請參閱“[安全性和活性](ch8.md#安全性和活性)”。)
|
||||
|
||||
共識的系統模型假設,當一個節點“崩潰”時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在30英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足**終止**屬性。特別是,2PC不符合終止屬性的要求。
|
||||
|
||||
當然如果**所有**的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體**多數(majority)**的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。
|
||||
當然如果**所有**的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體**多數(majority)**的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。
|
||||
|
||||
因此**終止**屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足—— 一致同意,完整性和有效性【92】。因此,大規模的中斷可能會阻止系統處理請求,但是它不能透過使系統做出無效的決定來破壞共識系統。
|
||||
|
||||
@ -781,7 +781,7 @@
|
||||
|
||||
最著名的容錯共識演算法是**檢視戳複製(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。這些演算法之間有不少相似之處,但它們並不相同【103】。在本書中我們不會介紹各種演算法的詳細細節:瞭解一些它們共通的高階思想通常已經足夠了,除非你準備自己實現一個共識系統。(可能並不明智,相當難【98,104】)
|
||||
|
||||
大多數這些演算法實際上並不直接使用這裡描述的形式化模型(提議與決定單個值,並滿足一致同意、完整性、有效性和終止屬性)。取而代之的是,它們決定了值的**順序(sequence)**,這使它們成為全序廣播演算法,正如本章前面所討論的那樣(參閱“[全序廣播](#全序廣播)”)。
|
||||
大多數這些演算法實際上並不直接使用這裡描述的形式化模型(提議與決定單個值,並滿足一致同意、完整性、有效性和終止屬性)。取而代之的是,它們決定了值的**順序(sequence)**,這使它們成為全序廣播演算法,正如本章前面所討論的那樣(請參閱“[全序廣播](#全序廣播)”)。
|
||||
|
||||
請記住,全序廣播要求將訊息按照相同的順序,恰好傳遞一次,準確傳送到所有節點。如果仔細思考,這相當於進行了幾輪共識:在每一輪中,節點提議下一條要傳送的訊息,然後決定在全序中下一條要傳送的訊息【67】。
|
||||
|
||||
@ -796,11 +796,11 @@
|
||||
|
||||
#### 單領導者複製和共識
|
||||
|
||||
在[第5章](ch5.md)中,我們討論了單領導者複製(參見“[領導者與追隨者](ch5.md#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在[第五章](ch5.md)裡一點都沒擔心過共識問題呢?
|
||||
在[第五章](ch5.md)中,我們討論了單領導者複製(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在[第五章](ch5.md)裡一點都沒擔心過共識問題呢?
|
||||
|
||||
答案取決於如何選擇領導者。如果主庫是由運維人員手動選擇和配置的,那麼你實際上擁有一種**獨裁型別**的“共識演算法”:只有一個節點被允許接受寫入(即決定寫入複製日誌的順序),如果該節點發生故障,則系統將無法寫入,直到運維手動配置其他節點作為主庫。這樣的系統在實踐中可以表現良好,但它無法滿足共識的**終止**屬性,因為它需要人為干預才能取得**進展**。
|
||||
|
||||
一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(參見“[處理節點宕機](ch5.md#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。
|
||||
一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。
|
||||
|
||||
但是還有一個問題。我們之前曾經討論過腦裂的問題,並且說過所有的節點都需要同意是誰領導,否則兩個不同的節點都會認為自己是領導者,從而導致資料庫進入不一致的狀態。因此,選出一位領導者需要共識。但如果這裡描述的共識演算法實際上是全序廣播演算法,並且全序廣播就像單主複製,而單主複製需要一個領導者,那麼...
|
||||
|
||||
@ -814,7 +814,7 @@
|
||||
|
||||
在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在“[真相由多數所定義](ch8.md#真相由多數所定義)”中提到的:一個節點不一定能相信自己的判斷—— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。
|
||||
|
||||
相反,它必須從**法定人數(quorum)**的節點中獲取選票(參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。
|
||||
相反,它必須從**法定人數(quorum)**的節點中獲取選票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。
|
||||
|
||||
因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的**法定人群**必須相互**重疊(overlap)**:如果一個提案的表決透過,則至少得有一個參與投票的節點也必須參加過最近的領導者選舉【105】。因此,如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論:沒有發生過更高時代的領導選舉,因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。
|
||||
|
||||
@ -822,13 +822,13 @@
|
||||
|
||||
#### 共識的侷限性
|
||||
|
||||
共識演算法對於分散式系統來說是一個巨大的突破:它為其他充滿不確定性的系統帶來了基礎的安全屬性(一致同意,完整性和有效性),然而它們還能保持容錯(只要多數節點正常工作且可達,就能取得進展)。它們提供了全序廣播,因此它們也可以以一種容錯的方式實現線性一致的原子操作(參見“[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”)。
|
||||
共識演算法對於分散式系統來說是一個巨大的突破:它為其他充滿不確定性的系統帶來了基礎的安全屬性(一致同意,完整性和有效性),然而它們還能保持容錯(只要多數節點正常工作且可達,就能取得進展)。它們提供了全序廣播,因此它們也可以以一種容錯的方式實現線性一致的原子操作(請參閱“[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”)。
|
||||
|
||||
儘管如此,它們並不是在所有地方都用上了,因為好處總是有代價的。
|
||||
|
||||
節點在做出決定之前對提議進行投票的過程是一種同步複製。如“[同步複製與非同步複製](ch5.md#同步複製與非同步複製)”中所述,通常資料庫會配置為非同步複製模式。在這種配置中發生故障切換時,一些已經提交的資料可能會丟失 —— 但是為了獲得更好的效能,許多人選擇接受這種風險。
|
||||
|
||||
共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(參閱“[線性一致性的代價](#線性一致性的代價)”)。
|
||||
共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(請參閱“[線性一致性的代價](#線性一致性的代價)”)。
|
||||
|
||||
大多數共識演算法假定參與投票的節點是固定的集合,這意味著你不能簡單的在叢集中新增或刪除節點。共識演算法的**動態成員擴充套件(dynamic membership extension)**允許叢集中的節點集隨時間推移而變化,但是它們比靜態成員演算法要難理解得多。
|
||||
|
||||
@ -848,7 +848,7 @@
|
||||
|
||||
***線性一致性的原子操作***
|
||||
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)**的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)**的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
|
||||
***操作的全序排序***
|
||||
|
||||
@ -868,7 +868,7 @@
|
||||
|
||||
ZooKeeper/Chubby模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。
|
||||
|
||||
另一個例子是,當你有一些分割槽資源(資料庫,訊息流,檔案儲存,分散式Actor系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。
|
||||
另一個例子是,當你有一些分割槽資源(資料庫,訊息流,檔案儲存,分散式Actor系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。
|
||||
|
||||
這類任務可以透過在ZooKeeper中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在ZooKeeper客戶端API基礎之上提供更高層工具的庫,例如Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多,這樣的嘗試鮮有成功記錄【107】。
|
||||
|
||||
@ -888,7 +888,7 @@
|
||||
|
||||
ZooKeeper和它的小夥伴們可以看作是成員資格服務(membership services)研究的悠久歷史的一部分,這個歷史可以追溯到20世紀80年代,並且對建立高度可靠的系統(例如空中交通管制)非常重要【110】。
|
||||
|
||||
成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在[第8章](ch8.md)中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。
|
||||
成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在[第八章](ch8.md)中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。
|
||||
|
||||
即使它確實存在,仍然可能發生一個節點被共識錯誤地宣告死亡。但是對於一個系統來說,知道哪些節點構成了當前的成員關係是非常有用的。例如,選擇領導者可能意味著簡單地選擇當前成員中編號最小的成員,但如果不同的節點對現有的成員都有誰有不同意見,則這種方法將不起作用。
|
||||
|
||||
@ -938,13 +938,13 @@
|
||||
|
||||
儘管單領導者資料庫可以提供線性一致性,且無需對每個寫操作都執行共識演算法,但共識對於保持及變更領導權仍然是必須的。因此從某種意義上說,使用單個領導者不過是“緩兵之計”:共識仍然是需要的,只是在另一個地方,而且沒那麼頻繁。好訊息是,容錯的共識演算法與容錯的共識系統是存在的,我們在本章中簡要地討論了它們。
|
||||
|
||||
像ZooKeeper這樣的工具為應用提供了“外包”的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受[第8章](ch8.md)中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似ZooKeeper的東西是明智之舉。
|
||||
像ZooKeeper這樣的工具為應用提供了“外包”的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受[第八章](ch8.md)中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似ZooKeeper的東西是明智之舉。
|
||||
|
||||
儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(參見“[處理寫入衝突](ch5.md#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。
|
||||
儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。
|
||||
|
||||
本章引用了大量關於分散式系統理論的研究。雖然理論論文和證明並不總是容易理解,有時也會做出不切實際的假設,但它們對於指導這一領域的實踐有著極其重要的價值:它們幫助我們推理什麼可以做,什麼不可以做,幫助我們找到反直覺的分散式系統缺陷。如果你有時間,這些參考資料值得探索。
|
||||
|
||||
這裡已經到了本書[第二部分](part-ii.md)的末尾,第二部介紹了複製([第5章](ch5.md)),分割槽([第6章](ch6.md)),事務([第7章](ch7.md)),分散式系統的故障模型([第8章](ch8.md))以及最後的一致性與共識([第9章](ch9.md))。現在我們已經奠定了紮實的理論基礎,我們將在[第三部分](part-iii.md)再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。
|
||||
這裡已經到了本書[第二部分](part-ii.md)的末尾,第二部介紹了複製([第五章](ch5.md)),分割槽([第六章](ch6.md)),事務([第七章](ch7.md)),分散式系統的故障模型([第八章](ch8.md))以及最後的一致性與共識([第九章](ch9.md))。現在我們已經奠定了紮實的理論基礎,我們將在[第三部分](part-iii.md)再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。
|
||||
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
1.在併發操作的上下文中:描述一個在單個時間點看起來生效的操作,所以另一個併發程序永遠不會遇到處於“半完成”狀態的操作。另見隔離。
|
||||
|
||||
2.在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。參見“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”。
|
||||
2.在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。請參閱“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
### 邊界(bounded)
|
||||
|
||||
有一些已知的上限或大小。例如,網路延遲情況(請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”)和資料集(請參閱[第11章](ch11.md)的介紹)。
|
||||
有一些已知的上限或大小。例如,網路延遲情況(請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”)和資料集(請參閱[第十一章](ch11.md)的介紹)。
|
||||
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
### CAP定理(CAP theorem)
|
||||
|
||||
一個被廣泛誤解的理論結果,在實踐中是沒有用的。參見“[CAP定理](ch9.md#CAP定理)”。
|
||||
一個被廣泛誤解的理論結果,在實踐中是沒有用的。請參閱“[CAP定理](ch9.md#CAP定理)”。
|
||||
|
||||
|
||||
|
||||
@ -84,13 +84,13 @@
|
||||
|
||||
### 非規範化(denormalize)
|
||||
|
||||
為了加速讀取,在標準資料集中引入一些冗餘或重複資料,通常採用快取或索引的形式。非規範化的值是一種預先計算的查詢結果,像物化檢視。請參見“[單物件和多物件操作](ch7.md#單物件和多物件操作)”和“[從同一事件日誌中派生多個檢視](ch11.md#從同一事件日誌中派生多個檢視)”。
|
||||
為了加速讀取,在標準資料集中引入一些冗餘或重複資料,通常採用快取或索引的形式。非規範化的值是一種預先計算的查詢結果,像物化檢視。請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”和“[從同一事件日誌中派生多個檢視](ch11.md#從同一事件日誌中派生多個檢視)”。
|
||||
|
||||
|
||||
|
||||
### 衍生資料(derived data)
|
||||
|
||||
一種資料集,根據其他資料透過可重複執行的流程建立。必要時,你可以執行該流程再次建立衍生資料。衍生資料通常用於提高特定資料的讀取速度。常見的衍生資料有索引、快取和物化檢視。參見[第三部分](part-iii.md)的介紹。
|
||||
一種資料集,根據其他資料透過可重複執行的流程建立。必要時,你可以執行該流程再次建立衍生資料。衍生資料通常用於提高特定資料的讀取速度。常見的衍生資料有索引、快取和物化檢視。請參閱[第三部分](part-iii.md)的介紹。
|
||||
|
||||
|
||||
|
||||
@ -246,7 +246,7 @@
|
||||
|
||||
### 分割槽(partitioning)
|
||||
|
||||
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。 也稱為分片。見[第6章](ch6.md)。
|
||||
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。 也稱為分片。見[第六章](ch6.md)。
|
||||
|
||||
|
||||
|
||||
@ -258,7 +258,7 @@
|
||||
|
||||
### 主鍵(primary key)
|
||||
|
||||
唯一標識記錄的值(通常是數字或字串)。 在許多應用程式中,主鍵由系統在建立記錄時生成(例如,按順序或隨機); 它們通常不由使用者設定。 另請參閱二級索引。
|
||||
唯一標識記錄的值(通常是數字或字串)。 在許多應用程式中,主鍵由系統在建立記錄時生成(例如,按順序或隨機); 它們通常不由使用者設定。 另請參閱次級索引。
|
||||
|
||||
|
||||
|
||||
@ -276,13 +276,13 @@
|
||||
|
||||
### 複製(replication)
|
||||
|
||||
在幾個節點(副本)上保留相同資料的副本,以便在某些節點無法訪問時,資料仍可訪問。請參閱[第5章](ch5.md)。
|
||||
在幾個節點(副本)上保留相同資料的副本,以便在某些節點無法訪問時,資料仍可訪問。請參閱[第五章](ch5.md)。
|
||||
|
||||
|
||||
|
||||
### 模式(schema)
|
||||
|
||||
一些資料結構的描述,包括其欄位和資料型別。 可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第4章](ch4.md))。
|
||||
一些資料結構的描述,包括其欄位和資料型別。 可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第四章](ch4.md))。
|
||||
|
||||
|
||||
|
||||
@ -294,7 +294,7 @@
|
||||
|
||||
### 可序列化(serializable)
|
||||
|
||||
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。 請參閱第7章的“[可序列化](ch7.md#可序列化)”。
|
||||
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。 請參閱第七章的“[可序列化](ch7.md#可序列化)”。
|
||||
|
||||
|
||||
|
||||
@ -326,7 +326,7 @@
|
||||
|
||||
### 流處理(stream process)
|
||||
|
||||
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。 見[第11章](ch11.md)。
|
||||
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。 見[第十一章](ch11.md)。
|
||||
|
||||
|
||||
|
||||
@ -338,7 +338,7 @@
|
||||
|
||||
### 記錄系統(system of record)
|
||||
|
||||
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。 參見[第三部分](part-iii.md)的介紹。
|
||||
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。 請參閱[第三部分](part-iii.md)的介紹。
|
||||
|
||||
|
||||
|
||||
@ -350,13 +350,13 @@
|
||||
|
||||
### 全序(total order)
|
||||
|
||||
一種比較事物的方法(例如時間戳),可以讓您總是說出兩件事中哪一件更大,哪件更小。 總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。 請參見“[因果順序不是全序的](ch9.md#因果順序不是全序的)”。
|
||||
一種比較事物的方法(例如時間戳),可以讓您總是說出兩件事中哪一件更大,哪件更小。 總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。 請參閱“[因果順序不是全序的](ch9.md#因果順序不是全序的)”。
|
||||
|
||||
|
||||
|
||||
### 事務(transaction)
|
||||
|
||||
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。 見[第7章](ch7.md)。
|
||||
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。 見[第七章](ch7.md)。
|
||||
|
||||
|
||||
|
||||
|
@ -62,11 +62,11 @@
|
||||
|
||||
本書分為三部分:
|
||||
|
||||
1. 在[第一部分](part-i.md)中,我們會討論設計資料密集型應用所賴的基本思想。我們從[第1章](ch1.md)開始,討論我們實際要達到的目標:可靠性,可伸縮性和可維護性;我們該如何思考這些概念;以及如何實現它們。在[第2章](ch2.md)中,我們比較了幾種不同的資料模型和查詢語言,看看它們如何適用於不同的場景。在[第3章](ch3.md)中將討論儲存引擎:資料庫如何在磁碟上擺放資料,以便能高效地再次找到它。[第4章](ch4.md)轉向資料編碼(序列化),以及隨時間演化的模式。
|
||||
1. 在[第一部分](part-i.md)中,我們會討論設計資料密集型應用所賴的基本思想。我們從[第一章](ch1.md)開始,討論我們實際要達到的目標:可靠性,可伸縮性和可維護性;我們該如何思考這些概念;以及如何實現它們。在[第二章](ch2.md)中,我們比較了幾種不同的資料模型和查詢語言,看看它們如何適用於不同的場景。在[第三章](ch3.md)中將討論儲存引擎:資料庫如何在磁碟上擺放資料,以便能高效地再次找到它。[第四章](ch4.md)轉向資料編碼(序列化),以及隨時間演化的模式。
|
||||
|
||||
2. 在[第二部分](part-ii.md)中,我們從討論儲存在一臺機器上的資料轉向討論分佈在多臺機器上的資料。這對於可伸縮性通常是必需的,但帶來了各種獨特的挑戰。我們首先討論複製([第5章](ch5.md)),分割槽/分片([第6章](ch6.md))和事務([第7章](ch7.md))。然後我們將探索關於分散式系統問題的更多細節([第8章](ch8.md)),以及在分散式系統中實現一致性與共識意味著什麼([第9章](ch9.md))。
|
||||
2. 在[第二部分](part-ii.md)中,我們從討論儲存在一臺機器上的資料轉向討論分佈在多臺機器上的資料。這對於可伸縮性通常是必需的,但帶來了各種獨特的挑戰。我們首先討論複製([第五章](ch5.md)),分割槽/分片([第六章](ch6.md))和事務([第七章](ch7.md))。然後我們將探索關於分散式系統問題的更多細節([第八章](ch8.md)),以及在分散式系統中實現一致性與共識意味著什麼([第九章](ch9.md))。
|
||||
|
||||
3. 在[第三部分](part-iii.md)中,我們討論那些從其他資料集衍生出一些資料集的系統。衍生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫,快取,索引等。在[第10章](ch10.md)中我們將從一種衍生資料的批處理方法開始,然後在此基礎上建立在[第11章](ch11.md)中討論的流處理。最後,在[第12章](ch12.md)中,我們將所有內容彙總,討論在將來構建可靠,可伸縮和可維護的應用程式的方法。
|
||||
3. 在[第三部分](part-iii.md)中,我們討論那些從其他資料集衍生出一些資料集的系統。衍生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫,快取,索引等。在[第十章](ch10.md)中我們將從一種衍生資料的批處理方法開始,然後在此基礎上建立在[第十一章](ch11.md)中討論的流處理。最後,在[第十二章](ch12.md)中,我們將所有內容彙總,討論在將來構建可靠,可伸縮和可維護的應用程式的方法。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user