From 8547560e62d951546694341949e628c24e48a431 Mon Sep 17 00:00:00 2001 From: Gang Yin <1246410+yingang@users.noreply.github.com> Date: Thu, 20 Jan 2022 22:07:24 +0800 Subject: [PATCH] update text spacing with pangu and some manual adjustments --- ch10.md | 512 +++++++++++++++++++++---------------------- ch11.md | 402 +++++++++++++++++----------------- ch12.md | 496 +++++++++++++++++++++--------------------- ch8.md | 388 ++++++++++++++++----------------- ch9.md | 584 +++++++++++++++++++++++++------------------------- zh-tw/ch10.md | 504 +++++++++++++++++++++---------------------- zh-tw/ch11.md | 396 +++++++++++++++++----------------- zh-tw/ch12.md | 492 +++++++++++++++++++++--------------------- zh-tw/ch8.md | 386 ++++++++++++++++----------------- zh-tw/ch9.md | 580 ++++++++++++++++++++++++------------------------- 10 files changed, 2370 insertions(+), 2370 deletions(-) diff --git a/ch10.md b/ch10.md index e2d50e2..e3b1edf 100644 --- a/ch10.md +++ b/ch10.md @@ -1,4 +1,4 @@ -# 第十章:批处理 +# 第十章:批处理 ![](img/ch10.png) @@ -10,11 +10,11 @@ [TOC] -在本书的前两部分中,我们讨论了很多关于**请求**和**查询**以及相应的**响应**或**结果**。许多现有数据系统中都采用这种数据处理方式:你发送请求指令,一段时间后(我们期望)系统会给出一个结果。数据库、缓存、搜索索引、Web服务器以及其他一些系统都以这种方式工作。 +在本书的前两部分中,我们讨论了很多关于 **请求** 和 **查询** 以及相应的 **响应** 或 **结果**。许多现有数据系统中都采用这种数据处理方式:你发送请求指令,一段时间后(我们期望)系统会给出一个结果。数据库、缓存、搜索索引、Web 服务器以及其他一些系统都以这种方式工作。 -像这样的**在线(online)** 系统,无论是浏览器请求页面还是调用远程API的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅“[描述性能](ch1.md#描述性能)”)。 +像这样的 **在线(online)** 系统,无论是浏览器请求页面还是调用远程 API 的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅 “[描述性能](ch1.md#描述性能)”)。 -Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统: +Web 和越来越多的基于 HTTP/REST 的 API 使交互的请求 / 响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统: * 服务(在线系统) @@ -22,28 +22,28 @@ Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得 * 批处理系统(离线系统) - 一个批处理系统有大量的输入数据,跑一个**作业(job)** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。 + 一个批处理系统有大量的输入数据,跑一个 **作业(job)** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。 * 流处理系统(准实时系统) - 流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时(near-real-time)** 或**准在线(nearline)** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。 + 流处理介于在线和离线(批处理)之间,所以有时候被称为 **准实时(near-real-time)** 或 **准在线(nearline)** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在 [第十一章](ch11.md) 讨论它。 -正如我们将在本章中看到的那样,批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能被过分热情地)被称为“造就Google大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用,包括Hadoop,CouchDB和MongoDB。 +正如我们将在本章中看到的那样,批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004 年发布的批处理算法 Map-Reduce(可能被过分热情地)被称为 “造就 Google 大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用,包括 Hadoop,CouchDB 和 MongoDB。 -与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce是一个相当低级别的编程模型,但它使得在商用硬件上能进行的处理规模迈上一个新的台阶。虽然MapReduce的重要性正在下降【5】,但它仍然值得去理解,因为它描绘了一幅关于批处理为什么有用,以及如何做到有用的清晰图景。 +与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce 是一个相当低级别的编程模型,但它使得在商用硬件上能进行的处理规模迈上一个新的台阶。虽然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因为它描绘了一幅关于批处理为什么有用,以及如何做到有用的清晰图景。 -实际上,批处理是一种非常古老的计算方式。早在可编程数字计算机诞生之前,打孔卡制表机(例如1890年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,从大量输入中汇总计算。 Map-Reduce与1940年代和1950年代广泛用于商业数据处理的机电IBM卡片分类机器有着惊人的相似之处【7】。正如我们所说,历史总是在不断重复自己。 +实际上,批处理是一种非常古老的计算方式。早在可编程数字计算机诞生之前,打孔卡制表机(例如 1890 年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,从大量输入中汇总计算。 Map-Reduce 与 1940 年代和 1950 年代广泛用于商业数据处理的机电 IBM 卡片分类机器有着惊人的相似之处【7】。正如我们所说,历史总是在不断重复自己。 -在本章中,我们将了解MapReduce和其他一些批处理算法和框架,并探索它们在现代数据系统中的作用。但首先我们将看看使用标准Unix工具的数据处理。即使你已经熟悉了它们,Unix的哲学也值得一读,Unix的思想和经验教训可以迁移到大规模、异构的分布式数据系统中。 +在本章中,我们将了解 MapReduce 和其他一些批处理算法和框架,并探索它们在现代数据系统中的作用。但首先我们将看看使用标准 Unix 工具的数据处理。即使你已经熟悉了它们,Unix 的哲学也值得一读,Unix 的思想和经验教训可以迁移到大规模、异构的分布式数据系统中。 ## 使用Unix工具的批处理 -我们从一个简单的例子开始。假设你有一台Web服务器,每次处理请求时都会在日志文件中附加一行。例如,使用nginx默认的访问日志格式,日志的一行可能如下所示: +我们从一个简单的例子开始。假设你有一台 Web 服务器,每次处理请求时都会在日志文件中附加一行。例如,使用 nginx 默认的访问日志格式,日志的一行可能如下所示: ```bash -216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" -200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) +216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" +200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" ``` @@ -54,14 +54,14 @@ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" $status $body_bytes_sent "$http_referer" "$http_user_agent" ``` -日志的这一行表明在2015年2月27日17:55:11 UTC,服务器从客户端IP地址`216.58.210.78`接收到对文件`/css/typography.css`的请求。用户没有被认证,所以`$remote_user`被设置为连字符(`-` )。响应状态是200(即请求成功),响应的大小是3377字节。网页浏览器是Chrome 40,URL `http://martin.kleppmann.com/` 的页面中的引用导致该文件被加载。 +日志的这一行表明在 2015 年 2 月 27 日 17:55:11 UTC,服务器从客户端 IP 地址 `216.58.210.78` 接收到对文件 `/css/typography.css` 的请求。用户没有被认证,所以 `$remote_user` 被设置为连字符(`-` )。响应状态是 200(即请求成功),响应的大小是 3377 字节。网页浏览器是 Chrome 40,URL `http://martin.kleppmann.com/` 的页面中的引用导致该文件被加载。 ### 简单日志分析 -很多工具可以从这些日志文件生成关于网站流量的漂亮的报告,但为了练手,让我们使用基本的Unix功能创建自己的工具。 例如,假设你想在你的网站上找到五个最受欢迎的网页。 则可以在Unix shell中这样做:[^i] +很多工具可以从这些日志文件生成关于网站流量的漂亮的报告,但为了练手,让我们使用基本的 Unix 功能创建自己的工具。 例如,假设你想在你的网站上找到五个最受欢迎的网页。 则可以在 Unix shell 中这样做:[^i] -[^i]: 有些人认为`cat`这里并没有必要,因为输入文件可以直接作为awk的参数。 但这种写法让线性管道更为显眼。 +[^i]: 有些人认为 `cat` 这里并没有必要,因为输入文件可以直接作为 awk 的参数。 但这种写法让线性管道更为显眼。 ```bash cat /var/log/nginx/access.log | #1 @@ -73,10 +73,10 @@ cat /var/log/nginx/access.log | #1 ``` 1. 读取日志文件 -2. 将每一行按空格分割成不同的字段,每行只输出第七个字段,恰好是请求的URL。在我们的例子中是`/css/typography.css`。 -3. 按字母顺序排列请求的URL列表。如果某个URL被请求过n次,那么排序后,文件将包含连续重复出现n次的该URL。 -4. `uniq`命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。 `-c`则表示还要输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 -5. 第二种排序按每行起始处的数字(`-n`)排序,这是URL的请求次数。然后逆序(`-r`)返回结果,大的数字在前。 +2. 将每一行按空格分割成不同的字段,每行只输出第七个字段,恰好是请求的 URL。在我们的例子中是 `/css/typography.css`。 +3. 按字母顺序排列请求的 URL 列表。如果某个 URL 被请求过 n 次,那么排序后,文件将包含连续重复出现 n 次的该 URL。 +4. `uniq` 命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。 `-c` 则表示还要输出一个计数器:对于每个不同的 URL,它会报告输入中出现该 URL 的次数。 +5. 第二种排序按每行起始处的数字(`-n`)排序,这是 URL 的请求次数。然后逆序(`-r`)返回结果,大的数字在前。 6. 最后,只输出前五行(`-n 5`),并丢弃其余的。该系列命令的输出如下所示: ``` @@ -87,17 +87,17 @@ cat /var/log/nginx/access.log | #1 915 /css/typography.css ``` -如果你不熟悉Unix工具,上面的命令行可能看起来有点吃力,但是它非常强大。它能在几秒钟内处理几GB的日志文件,并且你可以根据需要轻松修改命令。例如,如果要从报告中省略CSS文件,可以将awk参数更改为`'$7 !~ /\.css$/ {print $7}'`,如果想统计最多的客户端IP地址,可以把awk参数改为`'{print $1}'`等等。 +如果你不熟悉 Unix 工具,上面的命令行可能看起来有点吃力,但是它非常强大。它能在几秒钟内处理几 GB 的日志文件,并且你可以根据需要轻松修改命令。例如,如果要从报告中省略 CSS 文件,可以将 awk 参数更改为 `'$7 !~ /\.css$/ {print $7}'`, 如果想统计最多的客户端 IP 地址,可以把 awk 参数改为 `'{print $1}'` 等等。 -我们不会在这里详细探索Unix工具,但是它非常值得学习。令人惊讶的是,使用awk,sed,grep,sort,uniq和xargs的组合,可以在几分钟内完成许多数据分析,并且它们的性能相当的好【8】。 +我们不会在这里详细探索 Unix 工具,但是它非常值得学习。令人惊讶的是,使用 awk、sed、grep、sort、uniq 和 xargs 的组合,可以在几分钟内完成许多数据分析,并且它们的性能相当的好【8】。 #### 命令链与自定义程序 -除了Unix命令链,你还可以写一个简单的程序来做同样的事情。例如在Ruby中,它可能看起来像这样: +除了 Unix 命令链,你还可以写一个简单的程序来做同样的事情。例如在 Ruby 中,它可能看起来像这样: ```ruby counts = Hash.new(0) # 1 -File.open('/var/log/nginx/access.log') do |file| +File.open('/var/log/nginx/access.log') do |file| file.each do |line| url = line.split[6] # 2 counts[url] += 1 # 3 @@ -108,480 +108,480 @@ top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 top5.each{|count, url| puts "#{count} #{url}" } # 5 ``` -1. `counts`是一个存储计数器的哈希表,保存了每个URL被浏览的次数,默认为0。 -2. 逐行读取日志,抽取每行第七个被空格分隔的字段为URL(这里的数组索引是6,因为Ruby的数组索引从0开始计数) -3. 将日志当前行中URL对应的计数器值加一。 +1. `counts` 是一个存储计数器的哈希表,保存了每个 URL 被浏览的次数,默认为 0。 +2. 逐行读取日志,抽取每行第七个被空格分隔的字段为 URL(这里的数组索引是 6,因为 Ruby 的数组索引从 0 开始计数) +3. 将日志当前行中 URL 对应的计数器值加一。 4. 按计数器值(降序)对哈希表内容进行排序,并取前五位。 5. 打印出前五个条目。 -这个程序并不像Unix管道那样简洁,但是它的可读性很强,喜欢哪一种属于口味的问题。但两者除了表面上的差异之外,执行流程也有很大差异,如果你在大文件上运行此分析,则会变得明显。 +这个程序并不像 Unix 管道那样简洁,但是它的可读性很强,喜欢哪一种属于口味的问题。但两者除了表面上的差异之外,执行流程也有很大差异,如果你在大文件上运行此分析,则会变得明显。 #### 排序 VS 内存中的聚合 -Ruby脚本在内存中保存了一个URL的哈希表,将每个URL映射到它出现的次数。 Unix管道没有这样的哈希表,而是依赖于对URL列表的排序,在这个URL列表中,同一个URL的只是简单地重复出现。 +Ruby 脚本在内存中保存了一个 URL 的哈希表,将每个 URL 映射到它出现的次数。 Unix 管道没有这样的哈希表,而是依赖于对 URL 列表的排序,在这个 URL 列表中,同一个 URL 的只是简单地重复出现。 -哪种方法更好?这取决于你有多少个不同的URL。对于大多数中小型网站,你可能可以为所有不同网址提供一个计数器(假设我们使用1GB内存)。在此例中,作业的**工作集**(working set,即作业需要随机访问的内存大小)仅取决于不同URL的数量:如果日志中只有单个URL,重复出现一百万次,则散列表所需的空间表就只有一个URL加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。 +哪种方法更好?这取决于你有多少个不同的 URL。对于大多数中小型网站,你可能可以为所有不同网址提供一个计数器(假设我们使用 1GB 内存)。在此例中,作业的 **工作集**(working set,即作业需要随机访问的内存大小)仅取决于不同 URL 的数量:如果日志中只有单个 URL,重复出现一百万次,则散列表所需的空间表就只有一个 URL 加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。 -另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在“[SSTables和LSM树](ch3.md#SSTables和LSM树)”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,针对顺序I/O进行优化是[第三章](ch3.md)中反复出现的主题,相同的模式在此重现) +另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,针对顺序 I/O 进行优化是 [第三章](ch3.md) 中反复出现的主题,相同的模式在此重现) -GNU Coreutils(Linux)中的`sort `程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个CPU核进行并行排序【9】。这意味着我们之前看到的简单的Unix命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 +GNU Coreutils(Linux)中的 `sort` 程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个 CPU 核进行并行排序【9】。这意味着我们之前看到的简单的 Unix 命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 ### Unix哲学 -我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是Unix的关键设计思想之一,而且它直至今天也仍然令人讶异地重要。让我们更深入地研究一下,以便从Unix中借鉴一些想法【10】。 +我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是 Unix 的关键设计思想之一,而且它直至今天也仍然令人讶异地重要。让我们更深入地研究一下,以便从 Unix 中借鉴一些想法【10】。 -Unix管道的发明者道格·麦克罗伊(Doug McIlroy)在1964年首先描述了这种情况【11】:“我们需要一种类似园艺胶管的方式来拼接程序 —— 当我们需要将消息从一个程序传递另一个程序时,直接接上去就行。I/O应该也按照这种方式进行“。水管的类比仍然在生效,通过管道连接程序的想法成为了现在被称为**Unix哲学**的一部分 —— 这一组设计原则在Unix用户与开发者之间流行起来,该哲学在1978年表述如下【12,13】: +Unix 管道的发明者道格・麦克罗伊(Doug McIlroy)在 1964 年首先描述了这种情况【11】:“我们需要一种类似园艺胶管的方式来拼接程序 —— 当我们需要将消息从一个程序传递另一个程序时,直接接上去就行。I/O 应该也按照这种方式进行 “。水管的类比仍然在生效,通过管道连接程序的想法成为了现在被称为 **Unix 哲学** 的一部分 —— 这一组设计原则在 Unix 用户与开发者之间流行起来,该哲学在 1978 年表述如下【12,13】: -1. 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加“功能”让老程序复杂化。 +1. 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加 “功能” 让老程序复杂化。 2. 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。 3. 设计和构建软件时,即使是操作系统,也让它们能够尽早地被试用,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。 4. 优先使用工具来减轻编程任务,即使必须曲线救国编写工具,且在用完后很可能要扔掉大部分。 -这种方法 —— 自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和DevOps运动。奇怪的是,四十年来变化不大。 +这种方法 —— 自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和 DevOps 运动。奇怪的是,四十年来变化不大。 -`sort`工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用`sort` 几乎没什么用。它只能与其他Unix工具(如`uniq`)结合使用。 +`sort` 工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用 `sort` 几乎没什么用。它只能与其他 Unix 工具(如 `uniq`)结合使用。 -像 `bash`这样的Unix shell可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix如何实现这种可组合性? +像 `bash` 这样的 Unix shell 可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix 如何实现这种可组合性? #### 统一的接口 -如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 —— 换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的I/O接口。 +如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 —— 换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的 I/O 接口。 -在Unix中,这种接口是一个**文件**(file,更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix套接字,stdin,stdout)的通信通道,设备驱动程序(比如`/dev/audio`或`/dev/lp0`),表示TCP连接的套接字等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起[^ii]。 +在 Unix 中,这种接口是一个 **文件**(file,更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix 套接字,stdin,stdout)的通信通道,设备驱动程序(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 连接的套接字等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起 [^ii]。 -[^ii]: 统一接口的另一个例子是URL和HTTP,这是Web的基石。 一个URL标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。 具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。 这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。 之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。 从一个BBS到另一个BBS的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他BBS,然后手动找到他们正在寻找的信息。 直接链接到另一个BBS内的一些内容当时是不可能的。 +[^ii]: 统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。 一个 URL 标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。 具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。 这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。 之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。 从一个 BBS 到另一个 BBS 的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他 BBS,然后手动找到他们正在寻找的信息。 直接链接到另一个 BBS 内的一些内容当时是不可能的。 -按照惯例,许多(但不是全部)Unix程序将这个字节序列视为ASCII文本。我们的日志分析示例使用了这个事实:`awk`,`sort`,`uniq`和`head`都将它们的输入文件视为由`\n`(换行符,ASCII `0x0A`)字符分隔的记录列表。 `\n`的选择是任意的 —— 可以说,ASCII记录分隔符`0x1E`本来就是一个更好的选择,因为它是为了这个目的而设计的【14】,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。 +按照惯例,许多(但不是全部)Unix 程序将这个字节序列视为 ASCII 文本。我们的日志分析示例使用了这个事实:`awk`、`sort`、`uniq` 和 `head` 都将它们的输入文件视为由 `\n`(换行符,ASCII `0x0A`)字符分隔的记录列表。`\n` 的选择是任意的 —— 可以说,ASCII 记录分隔符 `0x1E` 本来就是一个更好的选择,因为它是为了这个目的而设计的【14】,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。 -每条记录(即一行输入)的解析则更加模糊。 Unix工具通常通过空白或制表符将行分割成字段,但也使用CSV(逗号分隔),管道分隔和其他编码。即使像`xargs`这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。 +每条记录(即一行输入)的解析则更加模糊。 Unix 工具通常通过空白或制表符将行分割成字段,但也使用 CSV(逗号分隔),管道分隔和其他编码。即使像 `xargs` 这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。 -ASCII文本的统一接口大多数时候都能工作,但它不是很优雅:我们的日志分析示例使用`{print $7}`来提取网址,这样可读性不是很好。在理想的世界中可能是`{print $request_url}`或类似的东西。我们稍后会回顾这个想法。 +ASCII 文本的统一接口大多数时候都能工作,但它不是很优雅:我们的日志分析示例使用 `{print $7}` 来提取网址,这样可读性不是很好。在理想的世界中可能是 `{print $request_url}` 或类似的东西。我们稍后会回顾这个想法。 -尽管几十年后还不够完美,但统一的Unix接口仍然是非常出色的设计。没有多少软件能像Unix工具一样交互组合的这么好:你不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录以管道传送至电子表格中,并将结果发布到社交网络或维基。今天,像Unix工具一样流畅地运行程序是一种例外,而不是规范。 +尽管几十年后还不够完美,但统一的 Unix 接口仍然是非常出色的设计。没有多少软件能像 Unix 工具一样交互组合的这么好:你不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录以管道传送至电子表格中,并将结果发布到社交网络或维基。今天,像 Unix 工具一样流畅地运行程序是一种例外,而不是规范。 -即使是具有**相同数据模型**的数据库,将数据从一种数据库导出再导入到另一种数据库也并不容易。缺乏整合导致了数据的**巴尔干化**[^译注i]。 +即使是具有 **相同数据模型** 的数据库,将数据从一种数据库导出再导入到另一种数据库也并不容易。缺乏整合导致了数据的 **巴尔干化**[^译注i]。 [^译注i]: **巴尔干化(Balkanization)** 是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 #### 逻辑与布线相分离 -Unix工具的另一个特点是使用标准输入(`stdin`)和标准输出(`stdout`)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和/或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(有个小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 +Unix 工具的另一个特点是使用标准输入(`stdin`)和标准输出(`stdout`)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和 / 或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(有个小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 -如果需要,程序仍然可以直接读取和写入文件,但Unix方法在程序不关心特定的文件路径、只使用标准输入和标准输出时效果最好。这允许shell用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种**松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或**控制反转(inversion of control)**【16】)。将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。 +如果需要,程序仍然可以直接读取和写入文件,但 Unix 方法在程序不关心特定的文件路径、只使用标准输入和标准输出时效果最好。这允许 shell 用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种 **松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或 **控制反转(inversion of control)**【16】)。将输入 / 输出布线与程序逻辑分开,可以将小工具组合成更大的系统。 -你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,它就可以加入数据处理的管道中。在日志分析示例中,你可以编写一个将Usage-Agent字符串转换为更灵敏的浏览器标识符,或者将IP地址转换为国家代码的工具,并将其插入管道。`sort`程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 +你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,它就可以加入数据处理的管道中。在日志分析示例中,你可以编写一个将 Usage-Agent 字符串转换为更灵敏的浏览器标识符,或者将 IP 地址转换为国家代码的工具,并将其插入管道。`sort` 程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 -但是,使用`stdin`和`stdout`能做的事情是有限的。需要多个输入或输出的程序虽然可能,却非常棘手。你没法将程序的输出管道连接至网络连接中【17,18】[^iii] 。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,那么I/O的布线就取决于程序本身了。它仍然可以被配置(例如通过命令行选项),但在Shell中对输入和输出进行布线的灵活性就少了。 +但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多个输入或输出的程序虽然可能,却非常棘手。你没法将程序的输出管道连接至网络连接中【17,18】[^iii] 。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,那么 I/O 的布线就取决于程序本身了。它仍然可以被配置(例如通过命令行选项),但在 Shell 中对输入和输出进行布线的灵活性就少了。 -[^iii]: 除了使用一个单独的工具,如`netcat`或`curl`。 Unix起初试图将所有东西都表示为文件,但是BSD套接字API偏离了这个惯例【17】。研究用操作系统Plan 9和Inferno在使用文件方面更加一致:它们将TCP连接表示为`/net/tcp`中的文件【18】。 +[^iii]: 除了使用一个单独的工具,如 `netcat` 或 `curl`。 Unix 起初试图将所有东西都表示为文件,但是 BSD 套接字 API 偏离了这个惯例【17】。研究用操作系统 Plan 9 和 Inferno 在使用文件方面更加一致:它们将 TCP 连接表示为 `/net/tcp` 中的文件【18】。 #### 透明度和实验 -使Unix工具如此成功的部分原因是,它们使查看正在发生的事情变得非常容易: +使 Unix 工具如此成功的部分原因是,它们使查看正在发生的事情变得非常容易: -- Unix命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 -- 你可以在任何时候结束管道,将管道输出到`less`,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 +- Unix 命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 +- 你可以在任何时候结束管道,将管道输出到 `less`,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 - 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。 -因此,与关系数据库的查询优化器相比,即使Unix工具非常简单,但仍然非常有用,特别是对于实验而言。 +因此,与关系数据库的查询优化器相比,即使 Unix 工具非常简单,但仍然非常有用,特别是对于实验而言。 -然而,Unix工具的最大局限在于它们只能在一台机器上运行 —— 而Hadoop这样的工具即应运而生。 +然而,Unix 工具的最大局限在于它们只能在一台机器上运行 —— 而 Hadoop 这样的工具即应运而生。 ## MapReduce和分布式文件系统 -MapReduce有点像Unix工具,但分布在数千台机器上。像Unix工具一样,它相当简单粗暴,但令人惊异地管用。一个MapReduce作业可以和一个Unix进程相类比:它接受一个或多个输入,并产生一个或多个输出。 +MapReduce 有点像 Unix 工具,但分布在数千台机器上。像 Unix 工具一样,它相当简单粗暴,但令人惊异地管用。一个 MapReduce 作业可以和一个 Unix 进程相类比:它接受一个或多个输入,并产生一个或多个输出。 -和大多数Unix工具一样,运行MapReduce作业通常不会修改输入,除了生成输出外没有任何副作用。输出文件以连续的方式一次性写入(一旦写入文件,不会修改任何现有的文件部分)。 +和大多数 Unix 工具一样,运行 MapReduce 作业通常不会修改输入,除了生成输出外没有任何副作用。输出文件以连续的方式一次性写入(一旦写入文件,不会修改任何现有的文件部分)。 -虽然Unix工具使用`stdin`和`stdout`作为输入和输出,但MapReduce作业在分布式文件系统上读写文件。在Hadoop的MapReduce实现中,该文件系统被称为**HDFS(Hadoop分布式文件系统)**,一个Google文件系统(GFS)的开源实现【19】。 +虽然 Unix 工具使用 `stdin` 和 `stdout` 作为输入和输出,但 MapReduce 作业在分布式文件系统上读写文件。在 Hadoop 的 MapReduce 实现中,该文件系统被称为 **HDFS(Hadoop 分布式文件系统)**,一个 Google 文件系统(GFS)的开源实现【19】。 -除HDFS外,还有各种其他分布式文件系统,如GlusterFS和Quantcast File System(QFS)【20】。诸如Amazon S3,Azure Blob存储和OpenStack Swift【21】等对象存储服务在很多方面都是相似的[^iv]。在本章中,我们将主要使用HDFS作为示例,但是这些原则适用于任何分布式文件系统。 +除 HDFS 外,还有各种其他分布式文件系统,如 GlusterFS 和 Quantcast File System(QFS)【20】。诸如 Amazon S3,Azure Blob 存储和 OpenStack Swift【21】等对象存储服务在很多方面都是相似的 [^iv]。在本章中,我们将主要使用 HDFS 作为示例,但是这些原则适用于任何分布式文件系统。 -[^iv]: 一个不同之处在于,对于HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用纠删码(Erasure Coding),则会丢失局部性,因为来自多台机器的数据必须进行合并以重建原始文件【20】。 +[^iv]: 一个不同之处在于,对于 HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用纠删码(Erasure Coding),则会丢失局部性,因为来自多台机器的数据必须进行合并以重建原始文件【20】。 -与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于**无共享**原则(请参阅[第二部分](part-ii.md)的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 +与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS 基于 **无共享** 原则(请参阅 [第二部分](part-ii.md) 的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 -HDFS在每台机器上运行了一个守护进程,它对外暴露网络服务,允许其他节点访问存储在该机器上的文件(假设数据中心中的每台通用计算机都挂载着一些磁盘)。名为**NameNode**的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。 +HDFS 在每台机器上运行了一个守护进程,它对外暴露网络服务,允许其他节点访问存储在该机器上的文件(假设数据中心中的每台通用计算机都挂载着一些磁盘)。名为 **NameNode** 的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS 在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。 -为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如[第五章](ch5.md)中所述,或者诸如Reed-Solomon码这样的纠删码方案,它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与RAID相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 +为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如 [第五章](ch5.md) 中所述,或者诸如 Reed-Solomon 码这样的纠删码方案,它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与 RAID 相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 -HDFS的可伸缩性已经很不错了:在撰写本书时,最大的HDFS部署运行在上万台机器上,总存储容量达数百PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于在专用存储设备上支持同等容量的成本【24】。 +HDFS 的可伸缩性已经很不错了:在撰写本书时,最大的 HDFS 部署运行在上万台机器上,总存储容量达数百 PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的 HDFS 上的数据存储和访问成本远低于在专用存储设备上支持同等容量的成本【24】。 ### MapReduce作业执行 -MapReduce是一个编程框架,你可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。理解它的最简单方法是参考“[简单日志分析](#简单日志分析)”中的Web服务器日志分析示例。MapReduce中的数据处理模式与此示例非常相似: +MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS 等分布式文件系统中的大型数据集。理解它的最简单方法是参考 “[简单日志分析](#简单日志分析)” 中的 Web 服务器日志分析示例。MapReduce 中的数据处理模式与此示例非常相似: -1. 读取一组输入文件,并将其分解成**记录(records)**。在Web服务器日志示例中,每条记录都是日志中的一行(即`\n`是记录分隔符)。 -2. 调用Mapper函数,从每条输入记录中提取一对键值。在前面的例子中,Mapper函数是`awk '{print $7}'`:它提取URL(`$7`)作为键,并将值留空。 -3. 按键排序所有的键值对。在日志的例子中,这由第一个`sort`命令完成。 -4. 调用Reducer函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,Reducer是由`uniq -c`命令实现的,该命令使用相同的键来统计相邻记录的数量。 +1. 读取一组输入文件,并将其分解成 **记录(records)**。在 Web 服务器日志示例中,每条记录都是日志中的一行(即 `\n` 是记录分隔符)。 +2. 调用 Mapper 函数,从每条输入记录中提取一对键值。在前面的例子中,Mapper 函数是 `awk '{print $7}'`:它提取 URL(`$7`)作为键,并将值留空。 +3. 按键排序所有的键值对。在日志的例子中,这由第一个 `sort` 命令完成。 +4. 调用 Reducer 函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,Reducer 是由 `uniq -c` 命令实现的,该命令使用相同的键来统计相邻记录的数量。 -这四个步骤可以作为一个MapReduce作业执行。步骤2(Map)和4(Reduce)是你编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 —— 你不必编写它,因为Mapper的输出始终在送往Reducer之前进行排序。 +这四个步骤可以作为一个 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 - Mapper会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 + Mapper 会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括 None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 * Reducer - MapReduce框架拉取由Mapper生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用Reducer。 Reducer可以产生输出记录(例如相同URL的出现次数)。 + MapReduce 框架拉取由 Mapper 生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用 Reducer。 Reducer 可以产生输出记录(例如相同 URL 的出现次数)。 -在Web服务器日志的例子中,我们在第5步中有第二个`sort`命令,它按请求数对URL进行排序。在MapReduce中,如果你需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper的作用是将数据放入一个适合排序的表单中,并且Reducer的作用是处理已排序的数据。 +在 Web 服务器日志的例子中,我们在第 5 步中有第二个 `sort` 命令,它按请求数对 URL 进行排序。在 MapReduce 中,如果你需要第二个排序阶段,则可以通过编写第二个 MapReduce 作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper 的作用是将数据放入一个适合排序的表单中,并且 Reducer 的作用是处理已排序的数据。 #### 分布式执行MapReduce -MapReduce与Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来显式处理并行问题。Mapper和Reducer一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。 +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作业中的数据流。其并行化基于分区(请参阅[第六章](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】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。 +每个输入文件的大小通常是数百兆字节。 MapReduce 调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个 Mapper,只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务【26】。这个原则被称为 **将计算放在数据附近**【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。 ![](img/fig10-1.png) -**图10-1 具有三个Mapper和三个Reducer的MapReduce任务** +**图 10-1 具有三个 Mapper 和三个 Reducer 的 MapReduce 任务** -在大多数情况下,应该在Mapper任务中运行的应用代码在将要运行它的机器上还不存在,所以MapReduce框架首先将代码(例如Java程序中的JAR文件)复制到适当的机器。然后启动Map任务并开始读取输入文件,一次将一条记录传入Mapper回调函数。Mapper的输出由键值对组成。 +在大多数情况下,应该在 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树)”中讨论的类似。 +键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个 Map 任务都按照 Reducer 对输出进行分区。每个分区都被写入 Mapper 程序的本地磁盘,使用的技术与我们在 “[SSTables 与 LSM 树](ch3.md#SSTables与LSM树)” 中讨论的类似。 -只要当Mapper读取完输入文件,并写完排序后的输出文件,MapReduce调度器就会通知Reducer可以从该Mapper开始获取输出文件。Reducer连接到每个Mapper,并下载自己相应分区的有序键值对文件。按Reducer分区,排序,从Mapper向Reducer复制分区数据,这一整个过程被称为**混洗(shuffle)**【26】(一个容易混淆的术语 —— 不像洗牌,在MapReduce中的混洗没有随机性)。 +只要当 Mapper 读取完输入文件,并写完排序后的输出文件,MapReduce 调度器就会通知 Reducer 可以从该 Mapper 开始获取输出文件。Reducer 连接到每个 Mapper,并下载自己相应分区的有序键值对文件。按 Reducer 分区,排序,从 Mapper 向 Reducer 复制分区数据,这一整个过程被称为 **混洗(shuffle)**【26】(一个容易混淆的术语 —— 不像洗牌,在 MapReduce 中的混洗没有随机性)。 -Reduce任务从Mapper获取文件,并将它们合并在一起,并保留有序特性。因此,如果不同的Mapper生成了键相同的记录,则在Reducer的输入中,这些记录将会相邻。 +Reduce 任务从 Mapper 获取文件,并将它们合并在一起,并保留有序特性。因此,如果不同的 Mapper 生成了键相同的记录,则在 Reducer 的输入中,这些记录将会相邻。 -Reducer调用时会收到一个键,和一个迭代器作为参数,迭代器会顺序地扫过所有具有该键的记录(因为在某些情况可能无法完全放入内存中)。Reducer可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录会写入分布式文件系统上的文件中(通常是在跑Reducer的机器本地磁盘上留一份,并在其他机器上留几份副本)。 +Reducer 调用时会收到一个键,和一个迭代器作为参数,迭代器会顺序地扫过所有具有该键的记录(因为在某些情况可能无法完全放入内存中)。Reducer 可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录会写入分布式文件系统上的文件中(通常是在跑 Reducer 的机器本地磁盘上留一份,并在其他机器上留几份副本)。 #### MapReduce工作流 -单个MapReduce作业可以解决的问题范围很有限。以日志分析为例,单个MapReduce作业可以确定每个URL的页面浏览次数,但无法确定最常见的URL,因为这需要第二轮排序。 +单个 MapReduce 作业可以解决的问题范围很有限。以日志分析为例,单个 MapReduce 作业可以确定每个 URL 的页面浏览次数,但无法确定最常见的 URL,因为这需要第二轮排序。 -因此将MapReduce作业链接成为**工作流(workflow)** 中是极为常见的,例如,一个作业的输出成为下一个作业的输入。 Hadoop MapReduce框架对工作流没有特殊支持,所以这个链是通过目录名隐式实现的:第一个作业必须将其输出配置为HDFS中的指定目录,第二个作业必须将其输入配置为从同一个目录。从MapReduce框架的角度来看,这是两个独立的作业。 +因此将 MapReduce 作业链接成为 **工作流(workflow)** 中是极为常见的,例如,一个作业的输出成为下一个作业的输入。Hadoop MapReduce 框架对工作流没有特殊支持,所以这个链是通过目录名隐式实现的:第一个作业必须将其输出配置为 HDFS 中的指定目录,第二个作业必须将其输入配置为从同一个目录。从 MapReduce 框架的角度来看,这是两个独立的作业。 -因此,被链接的MapReduce作业并没有那么像Unix命令管道(它直接将一个进程的输出作为另一个进程的输入,仅用一个很小的内存缓冲区)。它更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利也有弊,我们将在“[物化中间状态](#物化中间状态)”中讨论。 +因此,被链接的 MapReduce 作业并没有那么像 Unix 命令管道(它直接将一个进程的输出作为另一个进程的输入,仅用一个很小的内存缓冲区)。它更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利也有弊,我们将在 “[物化中间状态](#物化中间状态)” 中讨论。 -只有当作业成功完成后,批处理作业的输出才会被视为有效的(MapReduce会丢弃失败作业的部分输出)。因此,工作流中的一项作业只有在先前的作业 —— 即生产其输入的作业 —— 成功完成后才能开始。为了处理这些作业之间的依赖,有很多针对Hadoop的工作流调度器被开发出来,包括Oozie,Azkaban,Luigi,Airflow和Pinball 【28】。 +只有当作业成功完成后,批处理作业的输出才会被视为有效的(MapReduce 会丢弃失败作业的部分输出)。因此,工作流中的一项作业只有在先前的作业 —— 即生产其输入的作业 —— 成功完成后才能开始。为了处理这些作业之间的依赖,有很多针对 Hadoop 的工作流调度器被开发出来,包括 Oozie、Azkaban、Luigi、Airflow 和 Pinball 【28】。 -这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统时,由50到100个MapReduce作业组成的工作流是常见的【29】。而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流而言非常重要。 +这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统时,由 50 到 100 个 MapReduce 作业组成的工作流是常见的【29】。而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流而言非常重要。 -Hadoop的各种高级工具(如Pig 【30】,Hive 【31】,Cascading 【32】,Crunch 【33】和FlumeJava 【34】)也能自动布线组装多个MapReduce阶段,生成合适的工作流。 +Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【32】、Crunch 【33】和 FlumeJava 【34】)也能自动布线组装多个 MapReduce 阶段,生成合适的工作流。 ### Reduce侧连接与分组 -我们在[第二章](ch2.md)中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。 +我们在 [第二章](ch2.md) 中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。 -在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的**外键**,文档模型中的**文档引用**或图模型中的**边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如[第二章](ch2.md)所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除[^v]。 +在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的 **外键**,文档模型中的 **文档引用** 或图模型中的 **边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如 [第二章](ch2.md) 所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除 [^v]。 -[^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录通过与其他记录在特定字段(例如ID)中具有**相同值**相关联。有些数据库支持更通用的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有地方来讲这些东西。 +[^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录通过与其他记录在特定字段(例如 ID)中具有 **相同值** 相关联。有些数据库支持更通用的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有地方来讲这些东西。 -在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用**索引**来快速定位感兴趣的记录(请参阅[第三章](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) -**图10-2 用户行为日志与用户档案的连接** +**图 10-2 用户行为日志与用户档案的连接** -分析任务可能需要将用户活动与用户档案信息相关联:例如,如果档案包含用户的年龄或出生日期,系统就可以确定哪些页面更受哪些年龄段的用户欢迎。然而活动事件仅包含用户ID,而没有包含完整的用户档案信息。在每个活动事件中嵌入这些档案信息很可能会非常浪费。因此,活动事件需要与用户档案数据库相连接。 +分析任务可能需要将用户活动与用户档案信息相关联:例如,如果档案包含用户的年龄或出生日期,系统就可以确定哪些页面更受哪些年龄段的用户欢迎。然而活动事件仅包含用户 ID,而没有包含完整的用户档案信息。在每个活动事件中嵌入这些档案信息很可能会非常浪费。因此,活动事件需要与用户档案数据库相连接。 -实现这一连接的最简单方法是,逐个遍历活动事件,并为每个遇到的用户ID查询用户数据库(在远程服务器上)。这是可能的,但是它的性能可能会非常差:处理吞吐量将受限于受数据库服务器的往返时间,本地缓存的有效性很大程度上取决于数据的分布,并行运行大量查询可能会轻易压垮数据库【35】。 +实现这一连接的最简单方法是,逐个遍历活动事件,并为每个遇到的用户 ID 查询用户数据库(在远程服务器上)。这是可能的,但是它的性能可能会非常差:处理吞吐量将受限于受数据库服务器的往返时间,本地缓存的有效性很大程度上取决于数据的分布,并行运行大量查询可能会轻易压垮数据库【35】。 -为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。而且,查询远程数据库意味着批处理作业变为**非确定的(nondeterministic)**,因为远程数据库中的数据可能会改变。 +为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。而且,查询远程数据库意味着批处理作业变为 **非确定的(nondeterministic)**,因为远程数据库中的数据可能会改变。 -因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,请参阅“[数据仓库](ch3.md#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在HDFS中的一组文件中,而用户活动记录存储在另一组文件中,并能用MapReduce将所有相关记录集中到同一个地方进行高效处理。 +因此,更好的方法是获取用户数据库的副本(例如,使用 ETL 进程从数据库备份中提取数据,请参阅 “[数据仓库](ch3.md#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在 HDFS 中的一组文件中,而用户活动记录存储在另一组文件中,并能用 MapReduce 将所有相关记录集中到同一个地方进行高效处理。 #### 排序合并连接 -回想一下,Mapper的目的是从每个输入记录中提取一对键值。在[图10-2](img/fig10-2.png)的情况下,这个键就是用户ID:一组Mapper会扫过活动事件(提取用户ID作为键,活动事件作为值),而另一组Mapper将会扫过用户数据库(提取用户ID作为键,用户的出生日期作为值)。这个过程如[图10-3](img/fig10-3.png)所示。 +回想一下,Mapper 的目的是从每个输入记录中提取一对键值。在 [图 10-2](img/fig10-2.png) 的情况下,这个键就是用户 ID:一组 Mapper 会扫过活动事件(提取用户 ID 作为键,活动事件作为值),而另一组 Mapper 将会扫过用户数据库(提取用户 ID 作为键,用户的出生日期作为值)。这个过程如 [图 10-3](img/fig10-3.png) 所示。 ![](img/fig10-3.png) -**图10-3 在用户ID上进行的Reduce端连接。如果输入数据集分区为多个文件,则每个分区都会被多个Mapper并行处理** +**图 10-3 在用户 ID 上进行的 Reduce 端连接。如果输入数据集分区为多个文件,则每个分区都会被多个 Mapper 并行处理** -当MapReduce框架通过键对Mapper输出进行分区,然后对键值对进行排序时,效果是具有相同ID的所有活动事件和用户记录在Reducer输入中彼此相邻。 Map-Reduce作业甚至可以也让这些记录排序,使Reducer总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为**二次排序(secondary sort)**【26】。 +当 MapReduce 框架通过键对 Mapper 输出进行分区,然后对键值对进行排序时,效果是具有相同 ID 的所有活动事件和用户记录在 Reducer 输入中彼此相邻。 Map-Reduce 作业甚至可以也让这些记录排序,使 Reducer 总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为 **二次排序(secondary sort)**【26】。 -然后Reducer可以容易地执行实际的连接逻辑:每个用户ID都会被调用一次Reducer函数,且因为二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出**已观看网址**和**观看者年龄**的结果对。随后的Map-Reduce作业可以计算每个URL的查看者年龄分布,并按年龄段进行聚集。 +然后 Reducer 可以容易地执行实际的连接逻辑:每个用户 ID 都会被调用一次 Reducer 函数,且因为二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer 将出生日期存储在局部变量中,然后使用相同的用户 ID 遍历活动事件,输出 **已观看网址** 和 **观看者年龄** 的结果对。随后的 Map-Reduce 作业可以计算每个 URL 的查看者年龄分布,并按年龄段进行聚集。 -由于Reducer一次处理一个特定用户ID的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为**排序合并连接(sort-merge join)**,因为Mapper的输出是按键排序的,然后Reducer将来自连接两侧的有序记录列表合并在一起。 +由于 Reducer 一次处理一个特定用户 ID 的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为 **排序合并连接(sort-merge join)**,因为 Mapper 的输出是按键排序的,然后 Reducer 将来自连接两侧的有序记录列表合并在一起。 #### 把相关数据放在一起 -在排序合并连接中,Mapper和排序过程确保了所有对特定用户ID执行连接操作的必须数据都被放在同一个地方:单次调用Reducer的地方。预先排好了所有需要的数据,Reducer可以是相当简单的单线程代码,能够以高吞吐量和与低内存开销扫过这些记录。 +在排序合并连接中,Mapper 和排序过程确保了所有对特定用户 ID 执行连接操作的必须数据都被放在同一个地方:单次调用 Reducer 的地方。预先排好了所有需要的数据,Reducer 可以是相当简单的单线程代码,能够以高吞吐量和与低内存开销扫过这些记录。 -这种架构可以看做,Mapper将“消息”发送给Reducer。当一个Mapper发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使键只是一个任意的字符串(不是像IP地址和端口号那样的实际的网络地址),它表现的就像一个地址:所有具有相同键的键值对将被传递到相同的目标(一次Reducer的调用)。 +这种架构可以看做,Mapper 将 “消息” 发送给 Reducer。当一个 Mapper 发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使键只是一个任意的字符串(不是像 IP 地址和端口号那样的实际的网络地址),它表现的就像一个地址:所有具有相同键的键值对将被传递到相同的目标(一次 Reducer 的调用)。 -使用MapReduce编程模型,能将计算的物理网络通信层面(从正确的机器获取数据)从应用逻辑中剥离出来(获取数据后执行处理)。这种分离与数据库的典型用法形成了鲜明对比,从数据库中获取数据的请求经常出现在应用代码内部【36】。由于MapReduce处理了所有的网络通信,因此它也避免了让应用代码去担心部分故障,例如另一个节点的崩溃:MapReduce在不影响应用逻辑的情况下能透明地重试失败的任务。 +使用 MapReduce 编程模型,能将计算的物理网络通信层面(从正确的机器获取数据)从应用逻辑中剥离出来(获取数据后执行处理)。这种分离与数据库的典型用法形成了鲜明对比,从数据库中获取数据的请求经常出现在应用代码内部【36】。由于 MapReduce 处理了所有的网络通信,因此它也避免了让应用代码去担心部分故障,例如另一个节点的崩溃:MapReduce 在不影响应用逻辑的情况下能透明地重试失败的任务。 #### 分组 -除了连接之外,“把相关数据放在一起”的另一种常见模式是,按某个键对记录分组(如SQL中的GROUP BY子句)。所有带有相同键的记录构成一个组,而下一步往往是在每个组内进行某种聚合操作,例如: +除了连接之外,“把相关数据放在一起” 的另一种常见模式是,按某个键对记录分组(如 SQL 中的 GROUP BY 子句)。所有带有相同键的记录构成一个组,而下一步往往是在每个组内进行某种聚合操作,例如: -- 统计每个组中记录的数量(例如在统计PV的例子中,在SQL中表示为`COUNT(*)`聚合) -- 对某个特定字段求和(SQL中的`SUM(fieldname)`) -- 按某种分级函数取出排名前k条记录。 +- 统计每个组中记录的数量(例如在统计 PV 的例子中,在 SQL 中表示为 `COUNT(*)` 聚合) +- 对某个特定字段求和(SQL 中的 `SUM(fieldname)`) +- 按某种分级函数取出排名前 k 条记录。 -使用MapReduce实现这种分组操作的最简单方法是设置Mapper,以便它们生成的键值对使用所需的分组键。然后分区和排序过程将所有具有相同分区键的记录导向同一个Reducer。因此在MapReduce之上实现分组和连接看上去非常相似。 +使用 MapReduce 实现这种分组操作的最简单方法是设置 Mapper,以便它们生成的键值对使用所需的分组键。然后分区和排序过程将所有具有相同分区键的记录导向同一个 Reducer。因此在 MapReduce 之上实现分组和连接看上去非常相似。 -分组的另一个常见用途是整理特定用户会话的所有活动事件,以找出用户进行的一系列操作(称为**会话化(sessionization)**【37】)。例如,可以使用这种分析来确定显示新版网站的用户是否比那些显示旧版本的用户更有购买欲(A/B测试),或者计算某个营销活动是否值得。 +分组的另一个常见用途是整理特定用户会话的所有活动事件,以找出用户进行的一系列操作(称为 **会话化(sessionization)**【37】)。例如,可以使用这种分析来确定显示新版网站的用户是否比那些显示旧版本的用户更有购买欲(A/B 测试),或者计算某个营销活动是否值得。 -如果你有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话cookie,用户ID或类似的标识符作为分组键,以将特定用户的所有活动事件放在一起来实现会话化,与此同时,不同用户的事件仍然散布在不同的分区中。 +如果你有多个 Web 服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话 cookie,用户 ID 或类似的标识符作为分组键,以将特定用户的所有活动事件放在一起来实现会话化,与此同时,不同用户的事件仍然散布在不同的分区中。 #### 处理偏斜 -如果存在与单个键关联的大量数据,则“将具有相同键的所有记录放到相同的位置”这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为**关键对象(linchpin object)**【38】或**热键(hot key)**。 +如果存在与单个键关联的大量数据,则 “将具有相同键的所有记录放到相同的位置” 这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为 **关键对象(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】。 +如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig 中的 **偏斜连接(skewed join)** 方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper 会将热键的关联记录 **随机**(相对于传统 MapReduce 基于键散列的确定性方法)发送到几个 Reducer 之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到 **所有** 处理该键的 Reducer 上【40】。 -这种技术将处理热键的工作分散到多个Reducer上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。 +这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。 Crunch 中的 **分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。 -Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用Map端连接(请参阅下一节)。 +Hive 的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用 Map 端连接(请参阅下一节)。 -当按照热键进行分组并聚合时,可以将分组分两个阶段进行。第一个MapReduce阶段将记录发送到随机Reducer,以便每个Reducer只对热键的子集执行分组,为每个键输出一个更紧凑的中间聚合结果。然后第二个MapReduce作业将所有来自第一阶段Reducer的中间聚合结果合并为每个键一个值。 +当按照热键进行分组并聚合时,可以将分组分两个阶段进行。第一个 MapReduce 阶段将记录发送到随机 Reducer,以便每个 Reducer 只对热键的子集执行分组,为每个键输出一个更紧凑的中间聚合结果。然后第二个 MapReduce 作业将所有来自第一阶段 Reducer 的中间聚合结果合并为每个键一个值。 ### Map侧连接 -上一节描述的连接算法在Reducer中执行实际的连接逻辑,因此被称为Reduce侧连接。Mapper扮演着预处理输入数据的角色:从每个输入记录中提取键值,将键值对分配给Reducer分区,并按键排序。 +上一节描述的连接算法在 Reducer 中执行实际的连接逻辑,因此被称为 Reduce 侧连接。Mapper 扮演着预处理输入数据的角色:从每个输入记录中提取键值,将键值对分配给 Reducer 分区,并按键排序。 -Reduce侧方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper都可以对其预处理以备连接。然而不利的一面是,排序,复制至Reducer,以及合并Reducer输入,所有这些操作可能开销巨大。当数据通过MapReduce 阶段时,数据可能需要落盘好几次,取决于可用的内存缓冲区【37】。 +Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper 都可以对其预处理以备连接。然而不利的一面是,排序,复制至 Reducer,以及合并 Reducer 输入,所有这些操作可能开销巨大。当数据通过 MapReduce 阶段时,数据可能需要落盘好几次,取决于可用的内存缓冲区【37】。 -另一方面,如果你**能**对输入数据作出某些假设,则通过使用所谓的Map侧连接来加快连接速度是可行的。这种方法使用了一个裁减掉Reducer与排序的MapReduce作业,每个Mapper只是简单地从分布式文件系统中读取一个输入文件块,然后将输出文件写入文件系统,仅此而已。 +另一方面,如果你 **能** 对输入数据作出某些假设,则通过使用所谓的 Map 侧连接来加快连接速度是可行的。这种方法使用了一个裁减掉 Reducer 与排序的 MapReduce 作业,每个 Mapper 只是简单地从分布式文件系统中读取一个输入文件块,然后将输出文件写入文件系统,仅此而已。 #### 广播散列连接 -适用于执行Map端连接的最简单场景是大数据集与小数据集连接的情况。要点在于小数据集需要足够小,以便可以将其全部加载到每个Mapper的内存中。 +适用于执行 Map 端连接的最简单场景是大数据集与小数据集连接的情况。要点在于小数据集需要足够小,以便可以将其全部加载到每个 Mapper 的内存中。 -例如,假设在[图10-2](img/fig10-2.png)的情况下,用户数据库小到足以放进内存中。在这种情况下,当Mapper启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的散列表中。完成此操作后,Mapper可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户ID[^vi]。 +例如,假设在 [图 10-2](img/fig10-2.png) 的情况下,用户数据库小到足以放进内存中。在这种情况下,当 Mapper 启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的散列表中。完成此操作后,Mapper 可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户 ID [^vi]。 -[^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户ID唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,而连接运算符将对每个键输出所有的匹配。 +[^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户 ID 唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,而连接运算符将对每个键输出所有的匹配。 -参与连接的较大输入的每个文件块各有一个Mapper(在[图10-2](img/fig10-2.png)的例子中活动事件是较大的输入)。每个Mapper都会将较小输入整个加载到内存中。 +参与连接的较大输入的每个文件块各有一个 Mapper(在 [图 10-2](img/fig10-2.png) 的例子中活动事件是较大的输入)。每个 Mapper 都会将较小输入整个加载到内存中。 -这种简单有效的算法被称为**广播散列连接(broadcast hash join)**:**广播**一词反映了这样一个事实,每个连接较大输入端分区的Mapper都会将较小输入端数据集整个读入内存中(所以较小输入实际上“广播”到较大数据的所有分区上),**散列**一词反映了它使用一个散列表。 Pig(名为“**复制链接(replicated join)**”),Hive(“**MapJoin**”),Cascading和Crunch支持这种连接。它也被诸如Impala的数据仓库查询引擎使用【41】。 +这种简单有效的算法被称为 **广播散列连接(broadcast hash join)**:**广播** 一词反映了这样一个事实,每个连接较大输入端分区的 Mapper 都会将较小输入端数据集整个读入内存中(所以较小输入实际上 “广播” 到较大数据的所有分区上),**散列** 一词反映了它使用一个散列表。 Pig(名为 “**复制链接(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支持这种连接。它也被诸如 Impala 的数据仓库查询引擎使用【41】。 除了将较小的连接输入加载到内存散列表中,另一种方法是将较小输入存储在本地磁盘上的只读索引中【42】。索引中经常使用的部分将保留在操作系统的页面缓存中,因而这种方法可以提供与内存散列表几乎一样快的随机查找性能,但实际上并不需要数据集能放入内存中。 #### 分区散列连接 -如果Map侧连接的输入以相同的方式进行分区,则散列连接方法可以独立应用于每个分区。在[图10-2](img/fig10-2.png)的情况中,你可以根据用户ID的最后一位十进制数字来对活动事件和用户数据库进行分区(因此连接两侧各有10个分区)。例如,Mapper3首先将所有具有以3结尾的ID的用户加载到散列表中,然后扫描ID为3的每个用户的所有活动事件。 +如果 Map 侧连接的输入以相同的方式进行分区,则散列连接方法可以独立应用于每个分区。在 [图 10-2](img/fig10-2.png) 的情况中,你可以根据用户 ID 的最后一位十进制数字来对活动事件和用户数据库进行分区(因此连接两侧各有 10 个分区)。例如,Mapper3 首先将所有具有以 3 结尾的 ID 的用户加载到散列表中,然后扫描 ID 为 3 的每个用户的所有活动事件。 -如果分区正确无误,可以确定的是,所有你可能需要连接的记录都落在同一个编号的分区中。因此每个Mapper只需要从输入两端各读取一个分区就足够了。好处是每个Mapper都可以在内存散列表中少放点数据。 +如果分区正确无误,可以确定的是,所有你可能需要连接的记录都落在同一个编号的分区中。因此每个 Mapper 只需要从输入两端各读取一个分区就足够了。好处是每个 Mapper 都可以在内存散列表中少放点数据。 -这种方法只有当连接两端输入有相同的分区数,且两侧的记录都是使用相同的键与相同的哈希函数做分区时才适用。如果输入是由之前执行过这种分组的MapReduce作业生成的,那么这可能是一个合理的假设。 +这种方法只有当连接两端输入有相同的分区数,且两侧的记录都是使用相同的键与相同的哈希函数做分区时才适用。如果输入是由之前执行过这种分组的 MapReduce 作业生成的,那么这可能是一个合理的假设。 -分区散列连接在Hive中称为**Map侧桶连接(bucketed map joins)【37】**。 +分区散列连接在 Hive 中称为 **Map 侧桶连接(bucketed map joins)【37】**。 #### Map侧合并连接 -如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行**排序**,则可适用另一种Map侧连接的变体。在这种情况下,输入是否小到能放入内存并不重要,因为这时候Mapper同样可以执行归并操作(通常由Reducer执行)的归并操作:按键递增的顺序依次读取两个输入文件,将具有相同键的记录配对。 +如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行 **排序**,则可适用另一种 Map 侧连接的变体。在这种情况下,输入是否小到能放入内存并不重要,因为这时候 Mapper 同样可以执行归并操作(通常由 Reducer 执行)的归并操作:按键递增的顺序依次读取两个输入文件,将具有相同键的记录配对。 -如果能进行Map侧合并连接,这通常意味着前一个MapReduce作业可能一开始就已经把输入数据做了分区并进行了排序。原则上这个连接就可以在前一个作业的Reduce阶段进行。但使用独立的仅Map作业有时也是合适的,例如,分好区且排好序的中间数据集可能还会用于其他目的。 +如果能进行 Map 侧合并连接,这通常意味着前一个 MapReduce 作业可能一开始就已经把输入数据做了分区并进行了排序。原则上这个连接就可以在前一个作业的 Reduce 阶段进行。但使用独立的仅 Map 作业有时也是合适的,例如,分好区且排好序的中间数据集可能还会用于其他目的。 #### MapReduce工作流与Map侧连接 -当下游作业使用MapReduce连接的输出时,选择Map侧连接或Reduce侧连接会影响输出的结构。Reduce侧连接的输出是按照**连接键**进行分区和排序的,而Map端连接的输出则按照与较大输入相同的方式进行分区和排序(因为无论是使用分区连接还是广播连接,连接较大输入端的每个文件块都会启动一个Map任务)。 +当下游作业使用 MapReduce 连接的输出时,选择 Map 侧连接或 Reduce 侧连接会影响输出的结构。Reduce 侧连接的输出是按照 **连接键** 进行分区和排序的,而 Map 端连接的输出则按照与较大输入相同的方式进行分区和排序(因为无论是使用分区连接还是广播连接,连接较大输入端的每个文件块都会启动一个 Map 任务)。 -如前所述,Map侧连接也对输入数据集的大小,有序性和分区方式做出了更多假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据是按哪些键做的分区和排序,以及分区的数量。 +如前所述,Map 侧连接也对输入数据集的大小,有序性和分区方式做出了更多假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据是按哪些键做的分区和排序,以及分区的数量。 -在Hadoop生态系统中,这种关于数据集分区的元数据通常在HCatalog和Hive Metastore中维护【37】。 +在 Hadoop 生态系统中,这种关于数据集分区的元数据通常在 HCatalog 和 Hive Metastore 中维护【37】。 ### 批处理工作流的输出 -我们已经说了很多用于实现MapReduce工作流的算法,但却忽略了一个重要的问题:这些处理完成之后的最终结果是什么?我们最开始为什么要跑这些作业? +我们已经说了很多用于实现 MapReduce 工作流的算法,但却忽略了一个重要的问题:这些处理完成之后的最终结果是什么?我们最开始为什么要跑这些作业? -在数据库查询的场景中,我们将事务处理(OLTP)与分析两种目的区分开来(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。我们看到,OLTP查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前10项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。 +在数据库查询的场景中,我们将事务处理(OLTP)与分析两种目的区分开来(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。我们看到,OLTP 查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前 10 项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。 -批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而MapReduce作业工作流与用于分析目的的SQL查询是不同的(请参阅“[Hadoop与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。 +批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而 MapReduce 作业工作流与用于分析目的的 SQL 查询是不同的(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。 #### 建立搜索索引 -Google最初使用MapReduce是为其搜索引擎建立索引,其实现为由5到10个MapReduce作业组成的工作流【1】。虽然Google后来也不仅仅是为这个目的而使用MapReduce 【43】,但如果从构建搜索索引的角度来看,更能帮助理解MapReduce。 (直至今日,Hadoop MapReduce仍然是为Lucene/Solr构建索引的好方法【44】) +Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】,但如果从构建搜索索引的角度来看,更能帮助理解 MapReduce。 (直至今日,Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】) -我们在“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”中简要地了解了Lucene这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档ID列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等 —— 但这个原则是成立的。 +我们在 “[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)” 中简要地了解了 Lucene 这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等 —— 但这个原则是成立的。 -如果需要对一组固定文档执行全文搜索,则批处理是一种构建索引的高效方法:Mapper根据需要对文档集合进行分区,每个Reducer构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)并行处理效果拔群。 +如果需要对一组固定文档执行全文搜索,则批处理是一种构建索引的高效方法:Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)并行处理效果拔群。 由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。 如果索引的文档集合发生更改,一种选择是定期重跑整个索引工作流,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法的计算成本可能会很高。但它的优点是索引过程很容易理解:文档进,索引出。 -另一个选择是,可以增量建立索引。如[第三章](ch3.md)中讨论的,如果要在索引中添加,删除或更新文档,Lucene会写新的段文件,并在后台异步合并压缩段文件。我们将在[第十一章](ch11.md)中看到更多这种增量处理。 +另一个选择是,可以增量建立索引。如 [第三章](ch3.md) 中讨论的,如果要在索引中添加,删除或更新文档,Lucene 会写新的段文件,并在后台异步合并压缩段文件。我们将在 [第十一章](ch11.md) 中看到更多这种增量处理。 #### 键值存储作为批处理输出 搜索索引只是批处理工作流可能输出的一个例子。批处理的另一个常见用途是构建机器学习系统,例如分类器(比如垃圾邮件过滤器,异常检测,图像识别)与推荐系统(例如,你可能认识的人,你可能感兴趣的产品或相关的搜索【29】)。 -这些批处理作业的输出通常是某种数据库:例如,可以通过给定用户ID查询该用户推荐好友的数据库,或者可以通过产品ID查询相关产品的数据库【45】。 +这些批处理作业的输出通常是某种数据库:例如,可以通过给定用户 ID 查询该用户推荐好友的数据库,或者可以通过产品 ID 查询相关产品的数据库【45】。 -这些数据库需要被处理用户请求的Web应用所查询,而它们通常是独立于Hadoop基础设施的。那么批处理过程的输出如何回到Web应用可以查询的数据库中呢? +这些数据库需要被处理用户请求的 Web 应用所查询,而它们通常是独立于 Hadoop 基础设施的。那么批处理过程的输出如何回到 Web 应用可以查询的数据库中呢? -最直接的选择可能是,直接在Mapper或Reducer中使用你最爱的数据库的客户端库,并从批处理作业直接写入数据库服务器,一次写入一条记录。它能工作(假设你的防火墙规则允许从你的Hadoop环境直接访问你的生产数据库),但这并不是一个好主意,出于以下几个原因: +最直接的选择可能是,直接在 Mapper 或 Reducer 中使用你最爱的数据库的客户端库,并从批处理作业直接写入数据库服务器,一次写入一条记录。它能工作(假设你的防火墙规则允许从你的 Hadoop 环境直接访问你的生产数据库),但这并不是一个好主意,出于以下几个原因: - 正如前面在连接的上下文中讨论的那样,为每条记录发起一个网络请求,要比批处理任务的正常吞吐量慢几个数量级。即使客户端库支持批处理,性能也可能很差。 -- MapReduce作业经常并行运行许多任务。如果所有Mapper或Reducer都同时写入相同的输出数据库,并以批处理的预期速率工作,那么该数据库很可能被轻易压垮,其查询性能可能变差。这可能会导致系统其他部分的运行问题【35】。 -- 通常情况下,MapReduce为作业输出提供了一个干净利落的“全有或全无”保证:如果作业成功,则结果就是每个任务恰好执行一次所产生的输出,即使某些任务失败且必须一路重试。如果整个作业失败,则不会生成输出。然而从作业内部写入外部系统,会产生外部可见的副作用,这种副作用是不能以这种方式被隐藏的。因此,你不得不去操心对其他系统可见的部分完成的作业结果,并需要理解Hadoop任务尝试与预测执行的复杂性。 +- MapReduce 作业经常并行运行许多任务。如果所有 Mapper 或 Reducer 都同时写入相同的输出数据库,并以批处理的预期速率工作,那么该数据库很可能被轻易压垮,其查询性能可能变差。这可能会导致系统其他部分的运行问题【35】。 +- 通常情况下,MapReduce 为作业输出提供了一个干净利落的 “全有或全无” 保证:如果作业成功,则结果就是每个任务恰好执行一次所产生的输出,即使某些任务失败且必须一路重试。如果整个作业失败,则不会生成输出。然而从作业内部写入外部系统,会产生外部可见的副作用,这种副作用是不能以这种方式被隐藏的。因此,你不得不去操心对其他系统可见的部分完成的作业结果,并需要理解 Hadoop 任务尝试与预测执行的复杂性。 -更好的解决方案是在批处理作业**内**创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上节中的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。不少键值存储都支持在MapReduce作业中构建数据库文件,包括Voldemort 【46】,Terrapin 【47】,ElephantDB 【48】和HBase批量加载【49】。 +更好的解决方案是在批处理作业 **内** 创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上节中的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。不少键值存储都支持在 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】。 +将数据加载到 Voldemort 时,服务器将继续用旧数据文件服务请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动将查询切换到新文件。如果在这个过程中出现任何问题,它可以轻易回滚至旧文件,因为它们仍然存在而且不可变【46】。 #### 批处理输出的哲学 -本章前面讨论过的Unix哲学(“[Unix哲学](#Unix哲学)”)鼓励以显式指明数据流的方式进行实验:程序读取输入并写入输出。在这一过程中,输入保持不变,任何先前的输出都被新输出完全替换,且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,略做改动或进行调试,而不会搅乱系统的状态。 +本章前面讨论过的 Unix 哲学(“[Unix 哲学](#Unix哲学)”)鼓励以显式指明数据流的方式进行实验:程序读取输入并写入输出。在这一过程中,输入保持不变,任何先前的输出都被新输出完全替换,且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,略做改动或进行调试,而不会搅乱系统的状态。 -MapReduce作业的输出处理遵循同样的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护: +MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护: -- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的概念被称为**人类容错(human fault tolerance)**【50】) -- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种**最小化不可逆性(minimizing irreversibility)** 的原则有利于敏捷软件开发【51】。 -- 如果Map或Reduce任务失败,MapReduce框架将自动重新调度,并在同样的输入上再次运行它。如果失败是由代码中的错误造成的,那么它会不断崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于临时问题导致的,那么故障就会被容忍。因为输入不可变,这种自动重试是安全的,而失败任务的输出会被MapReduce框架丢弃。 +- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的概念被称为 **人类容错(human fault tolerance)**【50】) +- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种 **最小化不可逆性(minimizing irreversibility)** 的原则有利于敏捷软件开发【51】。 +- 如果 Map 或 Reduce 任务失败,MapReduce 框架将自动重新调度,并在同样的输入上再次运行它。如果失败是由代码中的错误造成的,那么它会不断崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于临时问题导致的,那么故障就会被容忍。因为输入不可变,这种自动重试是安全的,而失败任务的输出会被 MapReduce 框架丢弃。 - 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。 -- 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。 +- 与 Unix 工具类似,MapReduce 作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。 -在这些领域,在Unix上表现良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如Avro(请参阅“[Avro](ch4.md#Avro)”)和Parquet(请参阅“[列式存储](ch3.md#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见[第四章](ch4.md))。 +在这些领域,在 Unix 上表现良好的设计原则似乎也适用于 Hadoop,但 Unix 和 Hadoop 在某些方面也有所不同。例如,因为大多数 Unix 工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用 `{print $7}` 来提取 URL)。在 Hadoop 上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如 Avro(请参阅 “[Avro](ch4.md#Avro)”)和 Parquet(请参阅 “[列式存储](ch3.md#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见 [第四章](ch4.md))。 ### Hadoop与分布式数据库的对比 -正如我们所看到的,Hadoop有点像Unix的分布式版本,其中HDFS是文件系统,而MapReduce是Unix进程的怪异实现(总是在Map阶段和Reduce阶段运行`sort`工具)。我们了解了如何在这些原语的基础上实现各种连接和分组操作。 +正如我们所看到的,Hadoop 有点像 Unix 的分布式版本,其中 HDFS 是文件系统,而 MapReduce 是 Unix 进程的怪异实现(总是在 Map 阶段和 Reduce 阶段运行 `sort` 工具)。我们了解了如何在这些原语的基础上实现各种连接和分组操作。 -当MapReduce论文发表时【1】,它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的**大规模并行处理(MPP, massively parallel processing)** 数据库中实现了【3,40】。比如Gamma database machine,Teradata和Tandem NonStop SQL就是这方面的先驱【52】。 +当 MapReduce 论文发表时【1】,它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的 **大规模并行处理(MPP, massively parallel processing)** 数据库中实现了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是这方面的先驱【52】。 -最大的区别是,MPP数据库专注于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。 +最大的区别是,MPP 数据库专注于在一组机器上并行执行分析 SQL 查询,而 MapReduce 和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。 #### 存储多样性 数据库要求你根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本、图像、视频、传感器读数、稀疏矩阵、特征向量、基因组序列或任何其他类型的数据。 -说白了,Hadoop开放了将数据不加区分地转储到HDFS的可能性,允许后续再研究如何进一步处理【53】。相比之下,在将数据导入数据库专有存储格式之前,MPP数据库通常需要对数据和查询模式进行仔细的前期建模。 +说白了,Hadoop 开放了将数据不加区分地转储到 HDFS 的可能性,允许后续再研究如何进一步处理【53】。相比之下,在将数据导入数据库专有存储格式之前,MPP 数据库通常需要对数据和查询模式进行仔细的前期建模。 在纯粹主义者看来,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更高质量的数据来处理。然而实践经验表明,简单地使数据快速可用 —— 即使它很古怪,难以使用,使用原始格式 —— 也通常要比事先决定理想数据模型要更有价值【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 数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。 #### 处理模型的多样性 -MPP数据库是单体的,紧密集成的软件,负责磁盘上的存储布局,查询计划,调度和执行。由于这些组件都可以针对数据库的特定需求进行调整和优化,因此整个系统可以在其设计针对的查询类型上取得非常好的性能。而且,SQL查询语言允许以优雅的语法表达查询,而无需编写代码,可以在业务分析师使用的可视化工具(例如Tableau)中访问到。 +MPP 数据库是单体的,紧密集成的软件,负责磁盘上的存储布局,查询计划,调度和执行。由于这些组件都可以针对数据库的特定需求进行调整和优化,因此整个系统可以在其设计针对的查询类型上取得非常好的性能。而且,SQL 查询语言允许以优雅的语法表达查询,而无需编写代码,可以在业务分析师使用的可视化工具(例如 Tableau)中访问到。 -另一方面,并非所有类型的处理都可以合理地表达为SQL查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文搜索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常是特别针对特定应用的(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 +另一方面,并非所有类型的处理都可以合理地表达为 SQL 查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文搜索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常是特别针对特定应用的(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 -MapReduce使工程师能够轻松地在大型数据集上运行自己的代码。如果你有HDFS和MapReduce,那么你**可以**在它之上建立一个SQL查询执行引擎,事实上这正是Hive项目所做的【31】。但是,你也可以编写许多其他形式的批处理,这些批处理不必非要用SQL查询表示。 +MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码。如果你有 HDFS 和 MapReduce,那么你 **可以** 在它之上建立一个 SQL 查询执行引擎,事实上这正是 Hive 项目所做的【31】。但是,你也可以编写许多其他形式的批处理,这些批处理不必非要用 SQL 查询表示。 -随后,人们发现MapReduce对于某些类型的处理而言局限性很大,表现很差,因此在Hadoop之上其他各种处理模型也被开发出来(我们将在“[MapReduce之后](#MapReduce之后)”中看到其中一些)。只有两种处理模型,SQL和MapReduce,还不够,需要更多不同的模型!而且由于Hadoop平台的开放性,实施一整套方法是可行的,而这在单体MPP数据库的范畴内是不可能的【58】。 +随后,人们发现 MapReduce 对于某些类型的处理而言局限性很大,表现很差,因此在 Hadoop 之上其他各种处理模型也被开发出来(我们将在 “[MapReduce 之后](#MapReduce之后)” 中看到其中一些)。只有两种处理模型,SQL 和 MapReduce,还不够,需要更多不同的模型!而且由于 Hadoop 平台的开放性,实施一整套方法是可行的,而这在单体 MPP 数据库的范畴内是不可能的【58】。 -至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在Hadoop方式中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个集群内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。 +至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在 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 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。 #### 针对频繁故障设计 -当比较MapReduce和MPP数据库时,两种不同的设计思路出现了:处理故障和使用内存与磁盘的方式。与在线系统相比,批处理对故障不太敏感,因为就算失败也不会立即影响到用户,而且它们总是能再次运行。 +当比较 MapReduce 和 MPP 数据库时,两种不同的设计思路出现了:处理故障和使用内存与磁盘的方式。与在线系统相比,批处理对故障不太敏感,因为就算失败也不会立即影响到用户,而且它们总是能再次运行。 -如果一个节点在执行查询时崩溃,大多数MPP数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种错误处理的方法是可以接受的,因为重试的代价不是太大。 MPP数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的开销。 +如果一个节点在执行查询时崩溃,大多数 MPP 数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种错误处理的方法是可以接受的,因为重试的代价不是太大。 MPP 数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的开销。 -另一方面,MapReduce可以容忍单个Map或Reduce任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也会非常急切地将数据写入磁盘,一方面是为了容错,另一部分是因为假设数据集太大而不能适应内存。 +另一方面,MapReduce 可以容忍单个 Map 或 Reduce 任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也会非常急切地将数据写入磁盘,一方面是为了容错,另一部分是因为假设数据集太大而不能适应内存。 -MapReduce方式更适用于较大的作业:要处理如此之多的数据并运行很长时间的作业,以至于在此过程中很可能至少遇到一个任务故障。在这种情况下,由于单个任务失败而重新运行整个作业将是非常浪费的。即使以单个任务的粒度进行恢复引入了使得无故障处理更慢的开销,但如果任务失败率足够高,这仍然是一种合理的权衡。 +MapReduce 方式更适用于较大的作业:要处理如此之多的数据并运行很长时间的作业,以至于在此过程中很可能至少遇到一个任务故障。在这种情况下,由于单个任务失败而重新运行整个作业将是非常浪费的。即使以单个任务的粒度进行恢复引入了使得无故障处理更慢的开销,但如果任务失败率足够高,这仍然是一种合理的权衡。 但是这些假设有多么现实呢?在大多数集群中,机器故障确实会发生,但是它们不是很频繁 —— 可能少到绝大多数作业都不会经历机器故障。为了容错,真的值得带来这么大的额外开销吗? -要了解MapReduce节约使用内存和在任务的层次进行恢复的原因,了解最初设计MapReduce的环境是很有帮助的。 Google有着混用的数据中心,在线生产服务和离线批处理作业在同样机器上运行。每个任务都有一个通过容器强制执行的资源配给(CPU核心,RAM,磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的进程花费更多【59】。 +要了解 MapReduce 节约使用内存和在任务的层次进行恢复的原因,了解最初设计 MapReduce 的环境是很有帮助的。Google 有着混用的数据中心,在线生产服务和离线批处理作业在同样机器上运行。每个任务都有一个通过容器强制执行的资源配给(CPU 核心、RAM、磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的进程花费更多【59】。 -这种架构允许非生产(低优先级)计算资源被**过量使用(overcommitted)**,因为系统知道必要时它可以回收资源。与分离生产和非生产任务的系统相比,过量使用资源可以更好地利用机器并提高效率。但由于MapReduce作业以低优先级运行,它们随时都有被抢占的风险,因为优先级较高的进程可能需要其资源。在高优先级进程拿走所需资源后,批量作业能有效地“捡面包屑”,利用剩下的任何计算资源。 +这种架构允许非生产(低优先级)计算资源被 **过量使用(overcommitted)**,因为系统知道必要时它可以回收资源。与分离生产和非生产任务的系统相比,过量使用资源可以更好地利用机器并提高效率。但由于 MapReduce 作业以低优先级运行,它们随时都有被抢占的风险,因为优先级较高的进程可能需要其资源。在高优先级进程拿走所需资源后,批量作业能有效地 “捡面包屑”,利用剩下的任何计算资源。 -在谷歌,运行一个小时的MapReduce任务有大约有5%的风险被终止,为了给更高优先级的进程挪地方。这一概率比硬件问题、机器重启或其他原因的概率高了一个数量级【59】。按照这种抢占率,如果一个作业有100个任务,每个任务运行10分钟,那么至少有一个任务在完成之前被终止的风险大于50%。 +在谷歌,运行一个小时的 MapReduce 任务有大约有 5% 的风险被终止,为了给更高优先级的进程挪地方。这一概率比硬件问题、机器重启或其他原因的概率高了一个数量级【59】。按照这种抢占率,如果一个作业有 100 个任务,每个任务运行 10 分钟,那么至少有一个任务在完成之前被终止的风险大于 50%。 -这就是MapReduce被设计为容忍频繁意外任务终止的原因:不是因为硬件很不可靠,而是因为任意终止进程的自由有利于提高计算集群中的资源利用率。 +这就是 MapReduce 被设计为容忍频繁意外任务终止的原因:不是因为硬件很不可靠,而是因为任意终止进程的自由有利于提高计算集群中的资源利用率。 -在开源的集群调度器中,抢占的使用较少。 YARN的CapacityScheduler支持抢占,以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos或Kubernetes不支持通用的优先级抢占【60】。在任务不经常被终止的环境中,MapReduce的这一设计决策就没有多少意义了。在下一节中,我们将研究一些与MapReduce设计决策相异的替代方案。 +在开源的集群调度器中,抢占的使用较少。 YARN 的 CapacityScheduler 支持抢占,以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos 或 Kubernetes 不支持通用的优先级抢占【60】。在任务不经常被终止的环境中,MapReduce 的这一设计决策就没有多少意义了。在下一节中,我们将研究一些与 MapReduce 设计决策相异的替代方案。 ## MapReduce之后 -虽然MapReduce在2000年代后期变得非常流行,并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。对于不同的数据量,数据结构和处理类型,其他工具可能更适合表示计算。 +虽然 MapReduce 在 2000 年代后期变得非常流行,并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。对于不同的数据量,数据结构和处理类型,其他工具可能更适合表示计算。 -不管如何,我们在这一章花了大把时间来讨论MapReduce,因为它是一种有用的学习工具,它是分布式文件系统的一种相当简单明晰的抽象。在这里,**简单**意味着我们能理解它在做什么,而不是意味着使用它很简单。恰恰相反:使用原始的MapReduce API来实现复杂的处理工作实际上是非常困难和费力的 —— 例如,任意一种连接算法都需要你从头开始实现【37】。 +不管如何,我们在这一章花了大把时间来讨论 MapReduce,因为它是一种有用的学习工具,它是分布式文件系统的一种相当简单明晰的抽象。在这里,**简单** 意味着我们能理解它在做什么,而不是意味着使用它很简单。恰恰相反:使用原始的 MapReduce API 来实现复杂的处理工作实际上是非常困难和费力的 —— 例如,任意一种连接算法都需要你从头开始实现【37】。 -针对直接使用MapReduce的困难,在MapReduce上有很多高级编程模型(Pig,Hive,Cascading,Crunch)被创造出来,作为建立在MapReduce之上的抽象。如果你了解MapReduce的原理,那么它们学起来相当简单。而且它们的高级结构能显著简化许多常见批处理任务的实现。 +针对直接使用 MapReduce 的困难,在 MapReduce 上有很多高级编程模型(Pig、Hive、Cascading、Crunch)被创造出来,作为建立在 MapReduce 之上的抽象。如果你了解 MapReduce 的原理,那么它们学起来相当简单。而且它们的高级结构能显著简化许多常见批处理任务的实现。 -但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次而解决,而对于某些类型的处理,它表现得非常差劲。一方面,MapReduce非常稳健:你可以使用它在任务会频繁终止的多租户系统上处理几乎任意大量级的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理而言,其他工具有时会快上几个数量级。 +但是,MapReduce 执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次而解决,而对于某些类型的处理,它表现得非常差劲。一方面,MapReduce 非常稳健:你可以使用它在任务会频繁终止的多租户系统上处理几乎任意大量级的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理而言,其他工具有时会快上几个数量级。 -在本章的其余部分中,我们将介绍一些批处理方法。在[第十一章](ch11.md)我们将转向流处理,它可以看作是加速批处理的另一种方法。 +在本章的其余部分中,我们将介绍一些批处理方法。在 [第十一章](ch11.md) 我们将转向流处理,它可以看作是加速批处理的另一种方法。 ### 物化中间状态 -如前所述,每个MapReduce作业都独立于其他任何作业。作业与世界其他地方的主要连接点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为第一个作业输出目录,且外部工作流调度程序必须在第一个作业完成后再启动第二个。 +如前所述,每个 MapReduce 作业都独立于其他任何作业。作业与世界其他地方的主要连接点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为第一个作业输出目录,且外部工作流调度程序必须在第一个作业完成后再启动第二个。 -如果第一个作业的输出是要在组织内广泛发布的数据集,则这种配置是合理的。在这种情况下,你需要通过名称引用它,并将其重用为多个不同作业的输入(包括由其他团队开发的作业)。将数据发布到分布式文件系统中众所周知的位置能够带来**松耦合**,这样作业就不需要知道是谁在提供输入或谁在消费输出(请参阅“[逻辑与布线相分离](#逻辑与布线相分离)”)。 +如果第一个作业的输出是要在组织内广泛发布的数据集,则这种配置是合理的。在这种情况下,你需要通过名称引用它,并将其重用为多个不同作业的输入(包括由其他团队开发的作业)。将数据发布到分布式文件系统中众所周知的位置能够带来 **松耦合**,这样作业就不需要知道是谁在提供输入或谁在消费输出(请参阅 “[逻辑与布线相分离](#逻辑与布线相分离)”)。 -但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的**中间状态(intermediate state)**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由50或100个MapReduce作业组成的复杂工作流中,存在着很多这样的中间状态【29】。 +但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的 **中间状态(intermediate state)**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由 50 或 100 个 MapReduce 作业组成的复杂工作流中,存在着很多这样的中间状态【29】。 -将这个中间状态写入文件的过程称为**物化(materialization)**。 (在“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算) +将这个中间状态写入文件的过程称为 **物化(materialization)**。 (在 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算) -作为对照,本章开头的日志分析示例使用Unix管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而是只使用一个小的内存缓冲区,将输出增量地**流(stream)** 向输入。 +作为对照,本章开头的日志分析示例使用 Unix 管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而是只使用一个小的内存缓冲区,将输出增量地 **流(stream)** 向输入。 -与Unix管道相比,MapReduce完全物化中间状态的方法存在不足之处: +与 Unix 管道相比,MapReduce 完全物化中间状态的方法存在不足之处: -- MapReduce作业只有在前驱作业(生成其输入)中的所有任务都完成时才能启动,而由Unix管道连接的进程会同时启动,输出一旦生成就会被消费。不同机器上的数据偏斜或负载不均意味着一个作业往往会有一些掉队的任务,比其他任务要慢得多才能完成。必须等待至前驱作业的所有任务完成,拖慢了整个工作流程的执行。 -- Mapper通常是多余的:它们仅仅是读取刚刚由Reducer写入的同样文件,为下一个阶段的分区和排序做准备。在许多情况下,Mapper代码可能是前驱Reducer的一部分:如果Reducer和Mapper的输出有着相同的分区与排序方式,那么Reducer就可以直接串在一起,而不用与Mapper相互交织。 +- MapReduce 作业只有在前驱作业(生成其输入)中的所有任务都完成时才能启动,而由 Unix 管道连接的进程会同时启动,输出一旦生成就会被消费。不同机器上的数据偏斜或负载不均意味着一个作业往往会有一些掉队的任务,比其他任务要慢得多才能完成。必须等待至前驱作业的所有任务完成,拖慢了整个工作流程的执行。 +- Mapper 通常是多余的:它们仅仅是读取刚刚由 Reducer 写入的同样文件,为下一个阶段的分区和排序做准备。在许多情况下,Mapper 代码可能是前驱 Reducer 的一部分:如果 Reducer 和 Mapper 的输出有着相同的分区与排序方式,那么 Reducer 就可以直接串在一起,而不用与 Mapper 相互交织。 - 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,对这些临时数据这么搞就比较过分了。 #### 数据流引擎 -为了解决MapReduce的这些问题,几种用于分布式批处理的新执行引擎被开发出来,其中最著名的是Spark 【61,62】,Tez 【63,64】和Flink 【65,66】。它们的设计方式有很多区别,但有一个共同点:把整个工作流作为单个作业来处理,而不是把它分解为独立的子作业。 +为了解决 MapReduce 的这些问题,几种用于分布式批处理的新执行引擎被开发出来,其中最著名的是 Spark 【61,62】,Tez 【63,64】和 Flink 【65,66】。它们的设计方式有很多区别,但有一个共同点:把整个工作流作为单个作业来处理,而不是把它分解为独立的子作业。 -由于它们将工作流显式建模为数据从几个处理阶段穿过,所以这些系统被称为**数据流引擎(dataflow engines)**。像MapReduce一样,它们在一条线上通过反复调用用户定义的函数来一次处理一条记录,它们通过输入分区来并行化载荷,它们通过网络将一个函数的输出复制到另一个函数的输入。 +由于它们将工作流显式建模为数据从几个处理阶段穿过,所以这些系统被称为 **数据流引擎(dataflow engines)**。像 MapReduce 一样,它们在一条线上通过反复调用用户定义的函数来一次处理一条记录,它们通过输入分区来并行化载荷,它们通过网络将一个函数的输出复制到另一个函数的输入。 -与MapReduce不同,这些函数不需要严格扮演交织的Map与Reduce的角色,而是可以以更灵活的方式进行组合。我们称这些函数为**算子(operators)**,数据流引擎提供了几种不同的选项来将一个算子的输出连接到另一个算子的输入: +与 MapReduce 不同,这些函数不需要严格扮演交织的 Map 与 Reduce 的角色,而是可以以更灵活的方式进行组合。我们称这些函数为 **算子(operators)**,数据流引擎提供了几种不同的选项来将一个算子的输出连接到另一个算子的输入: -- 一种选项是对记录按键重新分区并排序,就像在MapReduce的混洗阶段一样(请参阅“[分布式执行MapReduce](#分布式执行MapReduce)”)。这种功能可以用于实现排序合并连接和分组,就像在MapReduce中一样。 +- 一种选项是对记录按键重新分区并排序,就像在 MapReduce 的混洗阶段一样(请参阅 “[分布式执行 MapReduce](#分布式执行MapReduce)”)。这种功能可以用于实现排序合并连接和分组,就像在 MapReduce 中一样。 - 另一种可能是接受多个输入,并以相同的方式进行分区,但跳过排序。当记录的分区重要但顺序无关紧要时,这省去了分区散列连接的工作,因为构建散列表还是会把顺序随机打乱。 - 对于广播散列连接,可以将一个算子的输出,发送到连接算子的所有分区。 -这种类型的处理引擎是基于像Dryad【67】和Nephele【68】这样的研究系统,与MapReduce模型相比,它有几个优点: +这种类型的处理引擎是基于像 Dryad【67】和 Nephele【68】这样的研究系统,与 MapReduce 模型相比,它有几个优点: -- 排序等昂贵的工作只需要在实际需要的地方执行,而不是默认地在每个Map和Reduce阶段之间出现。 -- 没有不必要的Map任务,因为Mapper所做的工作通常可以合并到前面的Reduce算子中(因为Mapper不会更改数据集的分区)。 +- 排序等昂贵的工作只需要在实际需要的地方执行,而不是默认地在每个 Map 和 Reduce 阶段之间出现。 +- 没有不必要的 Map 任务,因为 Mapper 所做的工作通常可以合并到前面的 Reduce 算子中(因为 Mapper 不会更改数据集的分区)。 - 由于工作流中的所有连接和数据依赖都是显式声明的,因此调度程序能够总览全局,知道哪里需要哪些数据,因而能够利用局部性进行优化。例如,它可以尝试将消费某些数据的任务放在与生成这些数据的任务相同的机器上,从而数据可以通过共享内存缓冲区传输,而不必通过网络复制。 -- 通常,算子间的中间状态足以保存在内存中或写入本地磁盘,这比写入HDFS需要更少的I/O(必须将其复制到多台机器,并将每个副本写入磁盘)。 MapReduce已经对Mapper的输出做了这种优化,但数据流引擎将这种思想推广至所有的中间状态。 +- 通常,算子间的中间状态足以保存在内存中或写入本地磁盘,这比写入 HDFS 需要更少的 I/O(必须将其复制到多台机器,并将每个副本写入磁盘)。 MapReduce 已经对 Mapper 的输出做了这种优化,但数据流引擎将这种思想推广至所有的中间状态。 - 算子可以在输入就绪后立即开始执行;后续阶段无需等待前驱阶段整个完成后再开始。 -- 与MapReduce(为每个任务启动一个新的JVM)相比,现有Java虚拟机(JVM)进程可以重用来运行新算子,从而减少启动开销。 +- 与 MapReduce(为每个任务启动一个新的 JVM)相比,现有 Java 虚拟机(JVM)进程可以重用来运行新算子,从而减少启动开销。 -你可以使用数据流引擎执行与MapReduce工作流同样的计算,而且由于此处所述的优化,通常执行速度要明显快得多。既然算子是Map和Reduce的泛化,那么相同的处理代码就可以在任一执行引擎上运行:Pig,Hive或Cascading中实现的工作流可以无需修改代码,可以通过修改配置,简单地从MapReduce切换到Tez或Spark【64】。 +你可以使用数据流引擎执行与 MapReduce 工作流同样的计算,而且由于此处所述的优化,通常执行速度要明显快得多。既然算子是 Map 和 Reduce 的泛化,那么相同的处理代码就可以在任一执行引擎上运行:Pig,Hive 或 Cascading 中实现的工作流可以无需修改代码,可以通过修改配置,简单地从 MapReduce 切换到 Tez 或 Spark【64】。 -Tez是一个相当薄的库,它依赖于YARN shuffle服务来实现节点间数据的实际复制【58】,而Spark和Flink则是包含了独立网络通信层,调度器,及用户向API的大型框架。我们将简要讨论这些高级API。 +Tez 是一个相当薄的库,它依赖于 YARN shuffle 服务来实现节点间数据的实际复制【58】,而 Spark 和 Flink 则是包含了独立网络通信层,调度器,及用户向 API 的大型框架。我们将简要讨论这些高级 API。 #### 容错 -完全物化中间状态至分布式文件系统的一个优点是,它具有持久性,这使得MapReduce中的容错相当容易:如果一个任务失败,它可以在另一台机器上重新启动,并从文件系统重新读取相同的输入。 +完全物化中间状态至分布式文件系统的一个优点是,它具有持久性,这使得 MapReduce 中的容错相当容易:如果一个任务失败,它可以在另一台机器上重新启动,并从文件系统重新读取相同的输入。 -Spark,Flink和Tez避免将中间状态写入HDFS,因此它们采取了不同的方法来容错:如果一台机器发生故障,并且该机器上的中间状态丢失,则它会从其他仍然可用的数据重新计算(在可行的情况下是先前的中间状态,要么就只能是原始输入数据,通常在HDFS上)。 +Spark、Flink 和 Tez 避免将中间状态写入 HDFS,因此它们采取了不同的方法来容错:如果一台机器发生故障,并且该机器上的中间状态丢失,则它会从其他仍然可用的数据重新计算(在可行的情况下是先前的中间状态,要么就只能是原始输入数据,通常在 HDFS 上)。 -为了实现这种重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用了哪些输入分区?应用了哪些算子? Spark使用**弹性分布式数据集(RDD,Resilient Distributed Dataset)** 的抽象来跟踪数据的谱系【61】,而Flink对算子状态存档,允许恢复运行在执行过程中遇到错误的算子【66】。 +为了实现这种重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用了哪些输入分区?应用了哪些算子? Spark 使用 **弹性分布式数据集(RDD,Resilient Distributed Dataset)** 的抽象来跟踪数据的谱系【61】,而 Flink 对算子状态存档,允许恢复运行在执行过程中遇到错误的算子【66】。 -在重新计算数据时,重要的是要知道计算是否是**确定性的**:也就是说,给定相同的输入数据,算子是否始终产生相同的输出?如果一些丢失的数据已经发送给下游算子,这个问题就很重要。如果算子重新启动,重新计算的数据与原有的丢失数据不一致,下游算子很难解决新旧数据之间的矛盾。对于不确定性算子来说,解决方案通常是杀死下游算子,然后再重跑新数据。 +在重新计算数据时,重要的是要知道计算是否是 **确定性的**:也就是说,给定相同的输入数据,算子是否始终产生相同的输出?如果一些丢失的数据已经发送给下游算子,这个问题就很重要。如果算子重新启动,重新计算的数据与原有的丢失数据不一致,下游算子很难解决新旧数据之间的矛盾。对于不确定性算子来说,解决方案通常是杀死下游算子,然后再重跑新数据。 为了避免这种级联故障,最好让算子具有确定性。但需要注意的是,非确定性行为很容易悄悄溜进来:例如,许多编程语言在迭代哈希表的元素时不能对顺序作出保证,许多概率和统计算法显式依赖于使用随机数,以及用到系统时钟或外部数据源,这些都是都不确定性的行为。为了能可靠地从故障中恢复,需要消除这种不确定性因素,例如使用固定的种子生成伪随机数。 @@ -589,136 +589,136 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此它们采取了不同 #### 关于物化的讨论 -回到Unix的类比,我们看到,MapReduce就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是Unix管道。尤其是Flink是基于管道执行的思想而建立的:也就是说,将算子的输出增量地传递给其他算子,不待输入完成便开始处理。 +回到 Unix 的类比,我们看到,MapReduce 就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是 Unix 管道。尤其是 Flink 是基于管道执行的思想而建立的:也就是说,将算子的输出增量地传递给其他算子,不待输入完成便开始处理。 排序算子不可避免地需要消费全部的输入后才能生成任何输出,因为输入中最后一条输入记录可能具有最小的键,因此需要作为第一条记录输出。因此,任何需要排序的算子都需要至少暂时地累积状态。但是工作流的许多其他部分可以以流水线方式执行。 -当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它—— 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和最终输出。和MapReduce一样,输入是不可变的,输出被完全替换。比起MapReduce的改进是,你不用再自己去将中间状态写入文件系统了。 +当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它 —— 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS 上的物化数据集通常仍是作业的输入和最终输出。和 MapReduce 一样,输入是不可变的,输出被完全替换。比起 MapReduce 的改进是,你不用再自己去将中间状态写入文件系统了。 ### 图与迭代处理 -在“[图数据模型](ch2.md#图数据模型)”中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](ch2.md)的讨论集中在OLTP风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。 +在 “[图数据模型](ch2.md#图数据模型)” 中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](ch2.md) 的讨论集中在 OLTP 风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。 -批处理上下文中的图也很有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank 【69】,它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分,用于确定网络搜索引擎呈现结果的顺序。 +批处理上下文中的图也很有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是 PageRank 【69】,它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分,用于确定网络搜索引擎呈现结果的顺序。 -> 像Spark,Flink和Tez这样的数据流引擎(请参阅“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图(DAG)** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱! +> 像 Spark、Flink 和 Tez 这样的数据流引擎(请参阅 “[物化中间状态](#物化中间状态)”)通常将算子作为 **有向无环图(DAG)** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流** 被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱! -许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为**传递闭包**,即transitive closure)。 +许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在 [图 2-6](img/fig2-6.png) 中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为 **传递闭包**,即 transitive closure)。 -可以在分布式文件系统中存储图(包含顶点和边的列表的文件),但是这种“重复至完成”的想法不能用普通的MapReduce来表示,因为它只扫过一趟数据。这种算法因此经常以**迭代**的风格实现: +可以在分布式文件系统中存储图(包含顶点和边的列表的文件),但是这种 “重复至完成” 的想法不能用普通的 MapReduce 来表示,因为它只扫过一趟数据。这种算法因此经常以 **迭代** 的风格实现: 1. 外部调度程序运行批处理来计算算法的一个步骤。 2. 当批处理过程完成时,调度器检查它是否完成(基于完成条件 —— 例如,没有更多的边要跟进,或者与上次迭代相比的变化低于某个阈值)。 -3. 如果尚未完成,则调度程序返回到步骤1并运行另一轮批处理。 +3. 如果尚未完成,则调度程序返回到步骤 1 并运行另一轮批处理。 -这种方法是有效的,但是用MapReduce实现它往往非常低效,因为MapReduce没有考虑算法的迭代性质:它总是读取整个输入数据集并产生一个全新的输出数据集,即使与上次迭代相比,改变的仅仅是图中的一小部分。 +这种方法是有效的,但是用 MapReduce 实现它往往非常低效,因为 MapReduce 没有考虑算法的迭代性质:它总是读取整个输入数据集并产生一个全新的输出数据集,即使与上次迭代相比,改变的仅仅是图中的一小部分。 #### Pregel处理模型 -针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)** 计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】实现了它。它也被称为**Pregel**模型,因为Google的Pregel论文推广了这种处理图的方法【72】。 +针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)** 计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】实现了它。它也被称为 **Pregel** 模型,因为 Google 的 Pregel 论文推广了这种处理图的方法【72】。 -回想一下在MapReduce中,Mapper在概念上向Reducer的特定调用“发送消息”,因为框架将所有具有相同键的Mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以向另一个顶点“发送消息”,通常这些消息是沿着图的边发送的。 +回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定调用 “发送消息”,因为框架将所有具有相同键的 Mapper 输出集中在一起。 Pregel 背后有一个类似的想法:一个顶点可以向另一个顶点 “发送消息”,通常这些消息是沿着图的边发送的。 -在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用Reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。 +在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用 Reducer 一样。与 MapReduce 的不同之处在于,在 Pregel 模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。 -这与Actor模型有些相似(请参阅“[分布式的Actor框架](ch4.md#分布式的Actor框架)”),除了顶点状态和顶点之间的消息具有容错性和持久性,且通信以固定的回合进行:在每次迭代中,框架递送上次迭代中发送的所有消息。Actor通常没有这样的时序保证。 +这与 Actor 模型有些相似(请参阅 “[分布式的 Actor 框架](ch4.md#分布式的Actor框架)”),除了顶点状态和顶点之间的消息具有容错性和持久性,且通信以固定的回合进行:在每次迭代中,框架递送上次迭代中发送的所有消息。Actor 通常没有这样的时序保证。 #### 容错 -顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高Pregel作业的性能,因为消息可以成批处理,且等待通信的次数也减少了。唯一的等待是在迭代之间:由于Pregel模型保证所有在一轮迭代中发送的消息都在下轮迭代中送达,所以在下一轮迭代开始前,先前的迭代必须完全完成,而所有的消息必须在网络上完成复制。 +顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高 Pregel 作业的性能,因为消息可以成批处理,且等待通信的次数也减少了。唯一的等待是在迭代之间:由于 Pregel 模型保证所有在一轮迭代中发送的消息都在下轮迭代中送达,所以在下一轮迭代开始前,先前的迭代必须完全完成,而所有的消息必须在网络上完成复制。 -即使底层网络可能丢失、重复或任意延迟消息(请参阅“[不可靠的网络](ch8.md#不可靠的网络)”),Pregel的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像MapReduce一样,框架能从故障中透明地恢复,以简化在Pregel上实现算法的编程模型。 +即使底层网络可能丢失、重复或任意延迟消息(请参阅 “[不可靠的网络](ch8.md#不可靠的网络)”),Pregel 的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像 MapReduce 一样,框架能从故障中透明地恢复,以简化在 Pregel 上实现算法的编程模型。 这种容错是通过在迭代结束时,定期存档所有顶点的状态来实现的,即将其全部状态写入持久化存储。如果某个节点发生故障并且其内存中的状态丢失,则最简单的解决方法是将整个图计算回滚到上一个存档点,然后重启计算。如果算法是确定性的,且消息记录在日志中,那么也可以选择性地只恢复丢失的分区(就像之前讨论过的数据流引擎)【72】。 #### 并行执行 -顶点不需要知道它在哪台物理机器上执行;当它向其他顶点发送消息时,它只是简单地将消息发往某个顶点ID。图的分区取决于框架 —— 即,确定哪个顶点运行在哪台机器上,以及如何通过网络路由消息,以便它们到达正确的地方。 +顶点不需要知道它在哪台物理机器上执行;当它向其他顶点发送消息时,它只是简单地将消息发往某个顶点 ID。图的分区取决于框架 —— 即,确定哪个顶点运行在哪台机器上,以及如何通过网络路由消息,以便它们到达正确的地方。 -由于编程模型一次仅处理一个顶点(有时称为“像顶点一样思考”),所以框架可以以任意方式对图分区。理想情况下如果顶点需要进行大量的通信,那么它们最好能被分区到同一台机器上。然而找到这样一种优化的分区方法是很困难的 —— 在实践中,图经常按照任意分配的顶点ID分区,而不会尝试将相关的顶点分组在一起。 +由于编程模型一次仅处理一个顶点(有时称为 “像顶点一样思考”),所以框架可以以任意方式对图分区。理想情况下如果顶点需要进行大量的通信,那么它们最好能被分区到同一台机器上。然而找到这样一种优化的分区方法是很困难的 —— 在实践中,图经常按照任意分配的顶点 ID 分区,而不会尝试将相关的顶点分组在一起。 因此,图算法通常会有很多跨机器通信的额外开销,而中间状态(节点之间发送的消息)往往比原始图大。通过网络发送消息的开销会显著拖慢分布式图算法的速度。 -出于这个原因,如果你的图可以放入一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理【73,74】。图比内存大也没关系,只要能放入单台计算机的磁盘,使用GraphChi等框架进行单机处理是就一个可行的选择【75】。如果图太大,不适合单机处理,那么像Pregel这样的分布式方法是不可避免的。高效的并行图算法是一个进行中的研究领域【76】。 +出于这个原因,如果你的图可以放入一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理【73,74】。图比内存大也没关系,只要能放入单台计算机的磁盘,使用 GraphChi 等框架进行单机处理是就一个可行的选择【75】。如果图太大,不适合单机处理,那么像 Pregel 这样的分布式方法是不可避免的。高效的并行图算法是一个进行中的研究领域【76】。 ### 高级API和语言 -自MapReduce开始流行的这几年以来,分布式批处理的执行引擎已经很成熟了。到目前为止,基础设施已经足够强大,能够存储和处理超过10,000台机器集群上的数PB的数据。由于在这种规模下物理执行批处理的问题已经被认为或多或少解决了,所以关注点已经转向其他领域:改进编程模型,提高处理效率,扩大这些技术可以解决的问题集。 +自 MapReduce 开始流行的这几年以来,分布式批处理的执行引擎已经很成熟了。到目前为止,基础设施已经足够强大,能够存储和处理超过 10,000 台机器集群上的数 PB 的数据。由于在这种规模下物理执行批处理的问题已经被认为或多或少解决了,所以关注点已经转向其他领域:改进编程模型,提高处理效率,扩大这些技术可以解决的问题集。 -如前所述,Hive,Pig,Cascading和Crunch等高级语言和API变得越来越流行,因为手写MapReduce作业实在是个苦力活。随着Tez的出现,这些高级语言还有一个额外好处,可以迁移到新的数据流执行引擎,而无需重写作业代码。 Spark和Flink也有它们自己的高级数据流API,通常是从FlumeJava中获取的灵感【34】。 +如前所述,Hive,Pig,Cascading 和 Crunch 等高级语言和 API 变得越来越流行,因为手写 MapReduce 作业实在是个苦力活。随着 Tez 的出现,这些高级语言还有一个额外好处,可以迁移到新的数据流执行引擎,而无需重写作业代码。 Spark 和 Flink 也有它们自己的高级数据流 API,通常是从 FlumeJava 中获取的灵感【34】。 -这些数据流API通常使用关系型构建块来表达一个计算:按某个字段连接数据集;按键对元组做分组;按某些条件过滤;并通过计数求和或其他函数来聚合元组。在内部,这些操作是使用本章前面讨论过的各种连接和分组算法来实现的。 +这些数据流 API 通常使用关系型构建块来表达一个计算:按某个字段连接数据集;按键对元组做分组;按某些条件过滤;并通过计数求和或其他函数来聚合元组。在内部,这些操作是使用本章前面讨论过的各种连接和分组算法来实现的。 -除了少写代码的明显优势之外,这些高级接口还支持交互式用法,在这种交互式使用中,你可以在Shell中增量式编写分析代码,频繁运行来观察它做了什么。这种开发风格在探索数据集和试验处理方法时非常有用。这也让人联想到Unix哲学,我们在“[Unix哲学](#Unix哲学)”中讨论过这个问题。 +除了少写代码的明显优势之外,这些高级接口还支持交互式用法,在这种交互式使用中,你可以在 Shell 中增量式编写分析代码,频繁运行来观察它做了什么。这种开发风格在探索数据集和试验处理方法时非常有用。这也让人联想到 Unix 哲学,我们在 “[Unix 哲学](#Unix哲学)” 中讨论过这个问题。 此外,这些高级接口不仅提高了人类的工作效率,也提高了机器层面的作业执行效率。 #### 向声明式查询语言的转变 -与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。 Hive,Spark和Flink都有基于代价的查询优化器可以做到这一点,甚至可以改变连接顺序,最小化中间状态的数量【66,77,78,79】。 +与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。 Hive、Spark 和 Flink 都有基于代价的查询优化器可以做到这一点,甚至可以改变连接顺序,最小化中间状态的数量【66,77,78,79】。 -连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以**声明式(declarative)** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在“[数据查询语言](ch2.md#数据查询语言)”中见过这个想法。 +连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以 **声明式(declarative)** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在 “[数据查询语言](ch2.md#数据查询语言)” 中见过这个想法。 -但MapReduce及其数据流后继者在其他方面,与SQL的完全声明式查询模型有很大区别。 MapReduce是围绕着回调函数的概念建立的:对于每条记录或者一组记录,调用一个用户定义的函数(Mapper或Reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作:解析、自然语言分析、图像分析以及运行数值或统计算法等。 +但 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数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。 +通过在高级 API 中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像 MPP 数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。 #### 专业化的不同领域 -尽管能够运行任意代码的可扩展性是很有用的,但是也有很多常见的例子,不断重复着标准的处理模式。因而这些模式值得拥有自己的可重用通用构建模块实现。传统上,MPP数据库满足了商业智能分析和业务报表的需求,但这只是许多使用批处理的领域之一。 +尽管能够运行任意代码的可扩展性是很有用的,但是也有很多常见的例子,不断重复着标准的处理模式。因而这些模式值得拥有自己的可重用通用构建模块实现。传统上,MPP 数据库满足了商业智能分析和业务报表的需求,但这只是许多使用批处理的领域之一。 -另一个越来越重要的领域是统计和数值算法,它们是机器学习应用所需要的(例如分类器和推荐系统)。可重用的实现正在出现:例如,Mahout在MapReduce、Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能【54】。 +另一个越来越重要的领域是统计和数值算法,它们是机器学习应用所需要的(例如分类器和推荐系统)。可重用的实现正在出现:例如,Mahout 在 MapReduce、Spark 和 Flink 之上实现了用于机器学习的各种算法,而 MADlib 在关系型 MPP 数据库(Apache HAWQ)中实现了类似的功能【54】。 -空间算法也是有用的,例如**k近邻搜索(k-nearest neighbors, kNN)**【80】,它在一些多维空间中搜索与给定项最近的项目 —— 这是一种相似性搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串【81】。 +空间算法也是有用的,例如 **k 近邻搜索(k-nearest neighbors, kNN)**【80】,它在一些多维空间中搜索与给定项最近的项目 —— 这是一种相似性搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串【81】。 -批处理引擎正被用于分布式执行日益广泛的各领域算法。随着批处理系统获得各种内置功能以及高级声明式算子,且随着MPP数据库变得更加灵活和易于编程,两者开始看起来相似了:最终,它们都只是存储和处理数据的系统。 +批处理引擎正被用于分布式执行日益广泛的各领域算法。随着批处理系统获得各种内置功能以及高级声明式算子,且随着 MPP 数据库变得更加灵活和易于编程,两者开始看起来相似了:最终,它们都只是存储和处理数据的系统。 ## 本章小结 -在本章中,我们探索了批处理的主题。我们首先看到了诸如awk、grep和sort之类的Unix工具,然后我们看到了这些工具的设计理念是如何应用到MapReduce和更近的数据流引擎中的。一些设计原则包括:输入是不可变的,输出是为了作为另一个(仍未知的)程序的输入,而复杂的问题是通过编写“做好一件事”的小工具来解决的。 +在本章中,我们探索了批处理的主题。我们首先看到了诸如 awk、grep 和 sort 之类的 Unix 工具,然后我们看到了这些工具的设计理念是如何应用到 MapReduce 和更近的数据流引擎中的。一些设计原则包括:输入是不可变的,输出是为了作为另一个(仍未知的)程序的输入,而复杂的问题是通过编写 “做好一件事” 的小工具来解决的。 -在Unix世界中,允许程序与程序组合的统一接口是文件与管道;在MapReduce中,该接口是一个分布式文件系统。我们看到数据流引擎添加了自己的管道式数据传输机制,以避免将中间状态物化至分布式文件系统,但作业的初始输入和最终输出通常仍是HDFS。 +在 Unix 世界中,允许程序与程序组合的统一接口是文件与管道;在 MapReduce 中,该接口是一个分布式文件系统。我们看到数据流引擎添加了自己的管道式数据传输机制,以避免将中间状态物化至分布式文件系统,但作业的初始输入和最终输出通常仍是 HDFS。 分布式批处理框架需要解决的两个主要问题是: * 分区 - 在MapReduce中,Mapper根据输入文件块进行分区。Mapper的输出被重新分区、排序并合并到可配置数量的Reducer分区中。这一过程的目的是把所有的**相关**数据(例如带有相同键的所有记录)都放在同一个地方。 + 在 MapReduce 中,Mapper 根据输入文件块进行分区。Mapper 的输出被重新分区、排序并合并到可配置数量的 Reducer 分区中。这一过程的目的是把所有的 **相关** 数据(例如带有相同键的所有记录)都放在同一个地方。 - 后MapReduce时代的数据流引擎若非必要会尽量避免排序,但它们也采取了大致类似的分区方法。 + 后 MapReduce 时代的数据流引擎若非必要会尽量避免排序,但它们也采取了大致类似的分区方法。 * 容错 - MapReduce经常写入磁盘,这使得从单个失败的任务恢复很轻松,无需重新启动整个作业,但在无故障的情况下减慢了执行速度。数据流引擎更多地将中间状态保存在内存中,更少地物化中间状态,这意味着如果节点发生故障,则需要重算更多的数据。确定性算子减少了需要重算的数据量。 + MapReduce 经常写入磁盘,这使得从单个失败的任务恢复很轻松,无需重新启动整个作业,但在无故障的情况下减慢了执行速度。数据流引擎更多地将中间状态保存在内存中,更少地物化中间状态,这意味着如果节点发生故障,则需要重算更多的数据。确定性算子减少了需要重算的数据量。 -我们讨论了几种MapReduce的连接算法,其中大多数也在MPP数据库和数据流引擎内部使用。它们也很好地演示了分区算法是如何工作的: +我们讨论了几种 MapReduce 的连接算法,其中大多数也在 MPP 数据库和数据流引擎内部使用。它们也很好地演示了分区算法是如何工作的: * 排序合并连接 - 每个参与连接的输入都通过一个提取连接键的Mapper。通过分区、排序和合并,具有相同键的所有记录最终都会进入相同的Reducer调用。这个函数能输出连接好的记录。 + 每个参与连接的输入都通过一个提取连接键的 Mapper。通过分区、排序和合并,具有相同键的所有记录最终都会进入相同的 Reducer 调用。这个函数能输出连接好的记录。 * 广播散列连接 - 两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个Mapper,将输入小端的散列表加载到每个Mapper中,然后扫描大端,一次一条记录,并为每条记录查询散列表。 + 两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个 Mapper,将输入小端的散列表加载到每个 Mapper 中,然后扫描大端,一次一条记录,并为每条记录查询散列表。 * 分区散列连接 如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地对每个分区应用散列表方法。 -分布式批处理引擎有一个刻意限制的编程模型:回调函数(比如Mapper和Reducer)被假定是无状态的,而且除了指定的输出外,必须没有任何外部可见的副作用。这一限制允许框架在其抽象下隐藏一些困难的分布式系统问题:当遇到崩溃和网络问题时,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则其中只有一个能使其输出实际可见。 +分布式批处理引擎有一个刻意限制的编程模型:回调函数(比如 Mapper 和 Reducer)被假定是无状态的,而且除了指定的输出外,必须没有任何外部可见的副作用。这一限制允许框架在其抽象下隐藏一些困难的分布式系统问题:当遇到崩溃和网络问题时,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则其中只有一个能使其输出实际可见。 得益于这个框架,你在批处理作业中的代码无需操心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,虽然实际上也许不得不重试各种任务。比起在线服务一边处理用户请求一边将写入数据库作为处理请求的副作用,批处理提供的这种可靠性语义要强得多。 -批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入—— 换句话说,输出是从输入衍生出的。最关键的是,输入数据是**有界的(bounded)**:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。 +批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入 —— 换句话说,输出是从输入衍生出的。最关键的是,输入数据是 **有界的(bounded)**:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。 -在下一章中,我们将转向流处理,其中的输入是**无界的(unbounded)** —— 也就是说,你还有活儿要干,然而它的输入是永无止境的数据流。在这种情况下,作业永无完成之日。因为在任何时候都可能有更多的工作涌入。我们将看到,在某些方面上,流处理和批处理是相似的。但是关于无尽数据流的假设也对我们构建系统的方式产生了很多改变。 +在下一章中,我们将转向流处理,其中的输入是 **无界的(unbounded)** —— 也就是说,你还有活儿要干,然而它的输入是永无止境的数据流。在这种情况下,作业永无完成之日。因为在任何时候都可能有更多的工作涌入。我们将看到,在某些方面上,流处理和批处理是相似的。但是关于无尽数据流的假设也对我们构建系统的方式产生了很多改变。 ## 参考文献 diff --git a/ch11.md b/ch11.md index e302e08..249c8a4 100644 --- a/ch11.md +++ b/ch11.md @@ -1,37 +1,37 @@ -# 第十一章:流处理 +# 第十一章:流处理 ![](img/ch11.png) > 有效的复杂系统总是从简单的系统演化而来。 反之亦然:从零设计的复杂系统没一个能有效工作的。 > -> —— 约翰·加尔,Systemantics(1975) +> —— 约翰・加尔,Systemantics(1975) --------------- [TOC] -在[第十章](ch10.md)中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是**衍生数据(derived data)** 的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。 +在 [第十章](ch10.md) 中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是 **衍生数据(derived data)** 的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。 -然而,在[第十章](ch10.md)中仍然有一个很大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成输入的读取。例如,MapReduce核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的。 +然而,在 [第十章](ch10.md) 中仍然有一个很大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成输入的读取。例如,MapReduce 核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的。 -实际上,很多数据是**无界限**的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产生了数据,明天他们将继续产生更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 +实际上,很多数据是 **无界限** 的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产生了数据,明天他们将继续产生更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式 “完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 -日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 —— 比如说,在每秒钟的末尾 —— 或者甚至更连续一些,完全抛开固定的时间切片,当事件发生时就立即进行处理,这就是**流处理(stream processing)** 背后的想法。 +日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 —— 比如说,在每秒钟的末尾 —— 或者甚至更连续一些,完全抛开固定的时间切片,当事件发生时就立即进行处理,这就是 **流处理(stream processing)** 背后的想法。 -一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(惰性列表)【2】,文件系统API(如Java的`FileInputStream`),TCP连接,通过互联网传送音频和视频等等。 +一般来说,“流” 是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix 的 stdin 和 stdout,编程语言(惰性列表)【2】,文件系统 API(如 Java 的 `FileInputStream`),TCP 连接,通过互联网传送音频和视频等等。 -在本章中,我们将把**事件流(event stream)** 视为一种数据管理机制:无界限,增量处理,与上一章中的批量数据相对应。我们将首先讨论怎样表示、存储、通过网络传输流。在“[数据库与流](#数据库与流)”中,我们将研究流和数据库之间的关系。最后在“[流处理](#流处理)”中,我们将研究连续处理这些流的方法和工具,以及它们用于应用构建的方式。 +在本章中,我们将把 **事件流(event stream)** 视为一种数据管理机制:无界限,增量处理,与上一章中的批量数据相对应。我们将首先讨论怎样表示、存储、通过网络传输流。在 “[数据库与流](#数据库与流)” 中,我们将研究流和数据库之间的关系。最后在 “[流处理](#流处理)” 中,我们将研究连续处理这些流的方法和工具,以及它们用于应用构建的方式。 ## 传递事件流 在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。流处理领域中的等价物看上去是什么样子的? -当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件(event)** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(请参阅“[单调钟与日历时钟](ch8.md#单调钟与日历时钟)”)。 +当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件(event)** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(请参阅 “[单调钟与日历时钟](ch8.md#单调钟与日历时钟)”)。 -例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或CPU利用率的周期性测量。在“[使用Unix工具的批处理](ch10.md#使用Unix工具的批处理)”的示例中,Web服务器日志的每一行都是一个事件。 +例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或 CPU 利用率的周期性测量。在 “[使用 Unix 工具的批处理](ch10.md#使用Unix工具的批处理)” 的示例中,Web 服务器日志的每一行都是一个事件。 -事件可能被编码为文本字符串或JSON,或者某种二进制编码,如[第四章](ch4.md)所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。 +事件可能被编码为文本字符串或 JSON,或者某种二进制编码,如 [第四章](ch4.md) 所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由 **生产者(producer)** (也称为 **发布者(publisher)** 或 **发送者(sender)** )生成一次,然后可能由多个 **消费者(consumer)** ( **订阅者(subscribers)** 或 **接收者(recipients)** )进行处理【3】。在文件系统中,文件名标识一组相关记录;在流式系统中,相关的事件通常被聚合为一个 **主题(topic)** 或 **流(stream)** 。 @@ -44,30 +44,30 @@ ### 消息传递系统 -向消费者通知新事件的常用方式是使用**消息传递系统(messaging system)**:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在“[消息传递中的数据流](ch4.md#消息传递中的数据流)”中谈到了这些系统,但现在我们将详细介绍这些系统。 +向消费者通知新事件的常用方式是使用 **消息传递系统(messaging system)**:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中谈到了这些系统,但现在我们将详细介绍这些系统。 -像生产者和消费者之间的Unix管道或TCP连接这样的直接信道,是实现消息传递系统的简单方法。但是,大多数消息传递系统都在这一基本模型上进行了扩展。特别的是,Unix管道和TCP将恰好一个发送者与恰好一个接收者连接,而一个消息传递系统允许多个生产者节点将消息发送到同一个主题,并允许多个消费者节点接收主题中的消息。 +像生产者和消费者之间的 Unix 管道或 TCP 连接这样的直接信道,是实现消息传递系统的简单方法。但是,大多数消息传递系统都在这一基本模型上进行了扩展。特别的是,Unix 管道和 TCP 将恰好一个发送者与恰好一个接收者连接,而一个消息传递系统允许多个生产者节点将消息发送到同一个主题,并允许多个消费者节点接收主题中的消息。 -在这个**发布/订阅**模式中,不同的系统采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助: +在这个 **发布 / 订阅** 模式中,不同的系统采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助: -1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?** 一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用**背压**(backpressure,也称为**流量控制**,即flow control:阻塞生产者,以免其发送更多的消息)。例如Unix管道和TCP就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(请参阅“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。 +1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?** 一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用 **背压**(backpressure,也称为 **流量控制**,即 flow control:阻塞生产者,以免其发送更多的消息)。例如 Unix 管道和 TCP 就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(请参阅 “[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。 如果消息被缓存在队列中,那么理解队列增长会发生什么是很重要的。当队列装不进内存时系统会崩溃吗?还是将消息写入磁盘?如果是这样,磁盘访问又会如何影响消息传递系统的性能【6】? -2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?** 与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅“[复制与持久性](ch7.md#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 +2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?** 与数据库一样,持久性可能需要写入磁盘和 / 或复制的某种组合(请参阅 “[复制与持久性](ch7.md#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 是否可以接受消息丢失取决于应用。例如,对于周期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新的值会在短时间内发出。但要注意,如果大量的消息被丢弃,可能无法立刻意识到指标已经不正确了【7】。如果你正在对事件计数,那么它们能够可靠送达是更重要的,因为每个丢失的消息都意味着使计数器的错误扩大。 -我们在[第十章](ch10.md)中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。 +我们在 [第十章](ch10.md) 中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。 #### 直接从生产者传递给消费者 许多消息传递系统使用生产者和消费者之间的直接网络通信,而不通过中间节点: -* 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发出请求。 +* 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 发出请求。 尽管这些直接消息传递系统在设计它们的环境中运行良好,但是它们通常要求应用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。 @@ -75,54 +75,54 @@ #### 消息代理 -一种广泛使用的替代方法是通过**消息代理**(message broker,也称为**消息队列**,即message queue)发送消息,消息代理实质上是一种针对处理消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那里读取来接收消息。 +一种广泛使用的替代方法是通过 **消息代理**(message broker,也称为 **消息队列**,即 message queue)发送消息,消息代理实质上是一种针对处理消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那里读取来接收消息。 通过将数据集中在代理上,这些系统可以更容易地容忍来来去去的客户端(连接,断开连接和崩溃),而持久性问题则转移到代理的身上。一些消息代理只将消息保存在内存中,而另一些消息代理(取决于配置)将其写入磁盘,以便在代理崩溃的情况下不会丢失。针对缓慢的消费者,它们通常会允许无上限的排队(而不是丢弃消息或背压),尽管这种选择也可能取决于配置。 -排队的结果是,消费者通常是**异步(asynchronous)** 的:当生产者发送消息时,通常只会等待代理确认消息已经被缓存,而不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点 —— 通常在几分之一秒之内,但有时当消息堆积时会显著延迟。 +排队的结果是,消费者通常是 **异步(asynchronous)** 的:当生产者发送消息时,通常只会等待代理确认消息已经被缓存,而不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点 —— 通常在几分之一秒之内,但有时当消息堆积时会显著延迟。 #### 消息代理与数据库的对比 -有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异: +有些消息代理甚至可以使用 XA 或 JTA 参与两阶段提交协议(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异: * 数据库通常保留数据直至显式删除,而大多数消息代理在消息成功递送给消费者时会自动删除消息。这样的消息代理不适合长期的数据存储。 -* 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小—— 即队列很短。如果代理需要缓冲很多消息,比如因为消费者速度较慢(如果内存装不下消息,可能会溢出到磁盘),每个消息需要更长的处理时间,整体吞吐量可能会恶化【6】。 +* 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小 —— 即队列很短。如果代理需要缓冲很多消息,比如因为消费者速度较慢(如果内存装不下消息,可能会溢出到磁盘),每个消息需要更长的处理时间,整体吞吐量可能会恶化【6】。 * 数据库通常支持次级索引和各种搜索数据的方式,而消息代理通常支持按照某种模式匹配主题,订阅其子集。虽然机制并不一样,但对于客户端选择想要了解的数据的一部分,都是基本的方式。 * 查询数据库时,结果通常基于某个时间点的数据快照;如果另一个客户端随后向数据库写入一些改变了查询结果的内容,则第一个客户端不会发现其先前结果现已过期(除非它重复查询或轮询变更)。相比之下,消息代理不支持任意查询,但是当数据发生变化时(即新消息可用时),它们会通知客户端。 -这是关于消息代理的传统观点,它被封装在诸如JMS 【14】和AMQP 【15】的标准中,并且被诸如RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO企业消息服务、IBM MQ、Azure Service Bus和Google Cloud Pub/Sub所实现 【16】。 +这是关于消息代理的传统观点,它被封装在诸如 JMS 【14】和 AMQP 【15】的标准中,并且被诸如 RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO 企业消息服务、IBM MQ、Azure Service Bus 和 Google Cloud Pub/Sub 所实现 【16】。 #### 多个消费者 -当多个消费者从同一主题中读取消息时,有两种主要的消息传递模式,如[图11-1](img/fig11-1.png)所示: +当多个消费者从同一主题中读取消息时,有两种主要的消息传递模式,如 [图 11-1](img/fig11-1.png) 所示: * 负载均衡(load balancing) - 每条消息都被传递给消费者**之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在AMQP中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在JMS中则称之为**共享订阅**,即shared subscription)。 + 每条消息都被传递给消费者 **之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在 AMQP 中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在 JMS 中则称之为 **共享订阅**,即 shared subscription)。 * 扇出(fan-out) - 每条消息都被传递给**所有**消费者。扇出允许几个独立的消费者各自“收听”相同的消息广播,而不会相互影响 —— 这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件 (JMS中的主题订阅与AMQP中的交叉绑定提供了这一功能)。 + 每条消息都被传递给 **所有** 消费者。扇出允许几个独立的消费者各自 “收听” 相同的消息广播,而不会相互影响 —— 这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件 (JMS 中的主题订阅与 AMQP 中的交叉绑定提供了这一功能)。 ![](img/fig11-1.png) -**图11-1 (a)负载平衡:在消费者间共享消费主题;(b)扇出:将每条消息传递给多个消费者。** +**图 11-1 (a)负载平衡:在消费者间共享消费主题;(b)扇出:将每条消息传递给多个消费者。** 两种模式可以组合使用:例如,两个独立的消费者组可以每组各订阅同一个主题,每一组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。 #### 确认与重新传递 -消费者随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进行了部分处理。为了确保消息不会丢失,消息代理使用**确认(acknowledgments)**:客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。 +消费者随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进行了部分处理。为了确保消息不会丢失,消息代理使用 **确认(acknowledgments)**:客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。 -如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。 (请注意可能发生这样的情况,消息**实际上是**处理完毕的,但**确认**在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在“[实践中的分布式事务](ch9.md#实践中的分布式事务)”中所讨论的那样) +如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。 (请注意可能发生这样的情况,消息 **实际上是** 处理完毕的,但 **确认** 在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在 “[实践中的分布式事务](ch9.md#实践中的分布式事务)” 中所讨论的那样) -当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在[图11-2](img/fig11-2.png)中,消费者通常按照生产者发送的顺序处理消息。然而消费者2在处理消息m3时崩溃,与此同时消费者1正在处理消息m4。未确认的消息m3随后被重新发送给消费者1,结果消费者1按照m4,m3,m5的顺序处理消息。因此m3和m4的交付顺序与生产者1的发送顺序不同。 +当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在 [图 11-2](img/fig11-2.png) 中,消费者通常按照生产者发送的顺序处理消息。然而消费者 2 在处理消息 m3 时崩溃,与此同时消费者 1 正在处理消息 m4。未确认的消息 m3 随后被重新发送给消费者 1,结果消费者 1 按照 m4,m3,m5 的顺序处理消息。因此 m3 和 m4 的交付顺序与生产者 1 的发送顺序不同。 ![](img/fig11-2.png) -**图11-2 在处理m3时消费者2崩溃,因此稍后重传至消费者1** +**图 11-2 在处理 m3 时消费者 2 崩溃,因此稍后重传至消费者 1** -即使消息代理试图保留消息的顺序(如JMS和AMQP标准所要求的),负载均衡与重传的组合也不可避免地导致消息被重新排序。为避免此问题,你可以让每个消费者使用单独的队列(即不使用负载均衡功能)。如果消息是完全独立的,则消息顺序重排并不是一个问题。但正如我们将在本章后续部分所述,如果消息之间存在因果依赖关系,这就是一个很重要的问题。 +即使消息代理试图保留消息的顺序(如 JMS 和 AMQP 标准所要求的),负载均衡与重传的组合也不可避免地导致消息被重新排序。为避免此问题,你可以让每个消费者使用单独的队列(即不使用负载均衡功能)。如果消息是完全独立的,则消息顺序重排并不是一个问题。但正如我们将在本章后续部分所述,如果消息之间存在因果依赖关系,这就是一个很重要的问题。 ### 分区日志 @@ -130,38 +130,38 @@ 数据库和文件系统采用截然相反的方法论:至少在某人显式删除前,通常写入数据库或文件的所有内容都要被永久记录下来。 -这种思维方式上的差异对创建衍生数据的方式有巨大影响。如[第十章](ch10.md)所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。 +这种思维方式上的差异对创建衍生数据的方式有巨大影响。如 [第十章](ch10.md) 所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS 风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。 如果你将新的消费者添加到消息传递系统,通常只能接收到消费者注册之后开始发送的消息。先前的任何消息都随风而逝,一去不复返。作为对比,你可以随时为文件和数据库添加新的客户端,且能读取任意久远的数据(只要应用没有显式覆盖或删除这些数据)。 -为什么我们不能把它俩杂交一下,既有数据库的持久存储方式,又有消息传递的低延迟通知?这就是**基于日志的消息代理(log-based message brokers)** 背后的想法。 +为什么我们不能把它俩杂交一下,既有数据库的持久存储方式,又有消息传递的低延迟通知?这就是 **基于日志的消息代理(log-based message brokers)** 背后的想法。 #### 使用日志进行消息存储 -日志只是磁盘上简单的仅追加记录序列。我们先前在[第三章](ch3.md)中日志结构存储引擎和预写式日志的上下文中讨论了日志,在[第五章](ch5.md)复制的上下文里也讨论了它。 +日志只是磁盘上简单的仅追加记录序列。我们先前在 [第三章](ch3.md) 中日志结构存储引擎和预写式日志的上下文中讨论了日志,在 [第五章](ch5.md) 复制的上下文里也讨论了它。 -同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。 Unix工具`tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。 +同样的结构可以用于实现消息代理:生产者通过将消息追加到日志末尾来发送消息,而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾,则会等待新消息追加的通知。 Unix 工具 `tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。 -为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行**分区**(按[第六章](ch6.md)的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如[图11-3](img/fig11-3.png)所示。 +为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行 **分区**(按 [第六章](ch6.md) 的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如 [图 11-3](img/fig11-3.png) 所示。 -在每个分区内,代理为每个消息分配一个单调递增的序列号或**偏移量**(offset,在[图11-3](img/fig11-3.png)中,框中的数字是消息偏移量)。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。 +在每个分区内,代理为每个消息分配一个单调递增的序列号或 **偏移量**(offset,在 [图 11-3](img/fig11-3.png) 中,框中的数字是消息偏移量)。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。 ![](img/fig11-3.png) -**图11-3 生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件** +**图 11-3 生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件** -Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的DistributedLog 【20,21】都是基于日志的消息代理。 Google Cloud Pub/Sub在架构上类似,但对外暴露的是JMS风格的API,而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。 +Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基于日志的消息代理。 Google Cloud Pub/Sub 在架构上类似,但对外暴露的是 JMS 风格的 API,而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过跨多台机器分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。 #### 日志与传统的消息传递相比 基于日志的方法天然支持扇出式消息传递,因为多个消费者可以独立读取日志,而不会相互影响 —— 读取消息不会将其从日志中删除。为了在一组消费者之间实现负载平衡,代理可以将整个分区分配给消费者组中的节点,而不是将单条消息分配给消费者客户端。 -然后每个客户端将消费被指派分区中的**所有**消息。通常情况下,当一个用户被指派了一个日志分区时,它会以简单的单线程方式顺序地读取分区中的消息。这种粗粒度的负载均衡方法有一些缺点: +然后每个客户端将消费被指派分区中的 **所有** 消息。通常情况下,当一个用户被指派了一个日志分区时,它会以简单的单线程方式顺序地读取分区中的消息。这种粗粒度的负载均衡方法有一些缺点: -* 共享消费主题工作的节点数,最多为该主题中的日志分区数,因为同一个分区内的所有消息被递送到同一个节点[^i]。 -* 如果某条消息处理缓慢,则它会阻塞该分区中后续消息的处理(一种行首阻塞的形式;请参阅“[描述性能](ch1.md#描述性能)”)。 +* 共享消费主题工作的节点数,最多为该主题中的日志分区数,因为同一个分区内的所有消息被递送到同一个节点 [^i]。 +* 如果某条消息处理缓慢,则它会阻塞该分区中后续消息的处理(一种行首阻塞的形式;请参阅 “[描述性能](ch1.md#描述性能)”)。 -因此在消息处理代价高昂,希望逐条并行处理,以及消息的顺序并没有那么重要的情况下,JMS/AMQP风格的消息代理是可取的。另一方面,在消息吞吐量很高,处理迅速,顺序很重要的情况下,基于日志的方法表现得非常好。 +因此在消息处理代价高昂,希望逐条并行处理,以及消息的顺序并没有那么重要的情况下,JMS/AMQP 风格的消息代理是可取的。另一方面,在消息吞吐量很高,处理迅速,顺序很重要的情况下,基于日志的方法表现得非常好。 [^i]: 要设计一种负载均衡方案也是有可能的,在这种方案中,两个消费者通过读取全部消息来共享分区处理的工作,但是其中一个只考虑具有偶数偏移量的消息,而另一个消费者只处理奇数编号的偏移量。或者你可以将消息摊到一个线程池中来处理,但这种方法会使消费者偏移量管理变得复杂。一般来说,单线程处理单分区是合适的,可以通过增加更多分区来提高并行度。 @@ -169,7 +169,7 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 顺序消费一个分区使得判断消息是否已经被处理变得相当容易:所有偏移量小于消费者的当前偏移量的消息已经被处理,而具有更大偏移量的消息还没有被看到。因此,代理不需要跟踪确认每条消息,只需要定期记录消费者的偏移即可。这种方法减少了额外簿记开销,而且在批处理和流处理中采用这种方法有助于提高基于日志的系统的吞吐量。 -实际上,这种偏移量与单领导者数据库复制中常见的日志序列号非常相似,我们在“[设置新从库](ch5.md#设置新从库)”中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导者,并在不跳过任何写入的情况下恢复复制。这里原理完全相同:消息代理表现得像一个主库,而消费者就像一个从库。 +实际上,这种偏移量与单领导者数据库复制中常见的日志序列号非常相似,我们在 “[设置新从库](ch5.md#设置新从库)” 中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导者,并在不跳过任何写入的情况下恢复复制。这里原理完全相同:消息代理表现得像一个主库,而消费者就像一个从库。 如果消费者节点失效,则失效消费者的分区将指派给其他节点,并从最后记录的偏移量开始消费消息。如果消费者已经处理了后续的消息,但还没有记录它们的偏移量,那么重启后这些消息将被处理两次。我们将在本章后面讨论这个问题的处理方法。 @@ -177,25 +177,25 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 如果只追加写入日志,则磁盘空间终究会耗尽。为了回收磁盘空间,日志实际上被分割成段,并不时地将旧段删除或移动到归档存储。 (我们将在后面讨论一种更为复杂的磁盘空间释放方式) -这就意味着如果一个慢消费者跟不上消息产生的速率而落后得太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为**循环缓冲区(circular buffer)** 或**环形缓冲区(ring buffer)**。不过由于缓冲区在磁盘上,因此缓冲区可能相当的大。 +这就意味着如果一个慢消费者跟不上消息产生的速率而落后得太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为 **循环缓冲区(circular buffer)** 或 **环形缓冲区(ring buffer)**。不过由于缓冲区在磁盘上,因此缓冲区可能相当的大。 -让我们做个简单计算。在撰写本文时,典型的大型硬盘容量为6TB,顺序写入吞吐量为150MB/s。如果以最快的速度写消息,则需要大约11个小时才能填满磁盘。因而磁盘可以缓冲11个小时的消息,之后它将开始覆盖旧的消息。即使使用多个磁盘和机器,这个比率也是一样的。实践中的部署很少能用满磁盘的写入带宽,所以通常可以保存一个几天甚至几周的日志缓冲区。 +让我们做个简单计算。在撰写本文时,典型的大型硬盘容量为 6TB,顺序写入吞吐量为 150MB/s。如果以最快的速度写消息,则需要大约 11 个小时才能填满磁盘。因而磁盘可以缓冲 11 个小时的消息,之后它将开始覆盖旧的消息。即使使用多个磁盘和机器,这个比率也是一样的。实践中的部署很少能用满磁盘的写入带宽,所以通常可以保存一个几天甚至几周的日志缓冲区。 不管保留多长时间的消息,日志的吞吐量或多或少保持不变,因为无论如何,每个消息都会被写入磁盘【18】。这种行为与默认将消息保存在内存中,仅当队列太长时才写入磁盘的消息传递系统形成鲜明对比。当队列很短时,这些系统非常快;而当这些系统开始写入磁盘时,就要慢的多,所以吞吐量取决于保留的历史数量。 #### 当消费者跟不上生产者时 -在“[消息传递系统](#消息传递系统)”中,如果消费者无法跟上生产者发送信息的速度时,我们讨论了三种选择:丢弃信息,进行缓冲或施加背压。在这种分类法里,基于日志的方法是缓冲的一种形式,具有很大但大小固定的缓冲区(受可用磁盘空间的限制)。 +在 “[消息传递系统](#消息传递系统)” 中,如果消费者无法跟上生产者发送信息的速度时,我们讨论了三种选择:丢弃信息,进行缓冲或施加背压。在这种分类法里,基于日志的方法是缓冲的一种形式,具有很大但大小固定的缓冲区(受可用磁盘空间的限制)。 如果消费者远远落后,而所要求的信息比保留在磁盘上的信息还要旧,那么它将不能读取这些信息,所以代理实际上丢弃了比缓冲区容量更大的旧信息。你可以监控消费者落后日志头部的距离,如果落后太多就发出报警。由于缓冲区很大,因而有足够的时间让运维人员来修复慢消费者,并在消息开始丢失之前让其赶上。 即使消费者真的落后太多开始丢失消息,也只有那个消费者受到影响;它不会中断其他消费者的服务。这是一个巨大的运维优势:你可以实验性地消费生产日志,以进行开发,测试或调试,而不必担心会中断生产服务。当消费者关闭或崩溃时,会停止消耗资源,唯一剩下的只有消费者偏移量。 -这种行为也与传统的消息代理形成了鲜明对比,在那种情况下,你需要小心地删除那些消费者已经关闭的队列—— 否则那些队列就会累积不必要的消息,从其他仍活跃的消费者那里占走内存。 +这种行为也与传统的消息代理形成了鲜明对比,在那种情况下,你需要小心地删除那些消费者已经关闭的队列 —— 否则那些队列就会累积不必要的消息,从其他仍活跃的消费者那里占走内存。 #### 重播旧消息 -我们之前提到,使用AMQP和JMS风格的消息代理,处理和确认消息是一个破坏性的操作,因为它会导致消息在代理上被删除。另一方面,在基于日志的消息代理中,使用消息更像是从文件中读取数据:这是只读操作,不会更改日志。 +我们之前提到,使用 AMQP 和 JMS 风格的消息代理,处理和确认消息是一个破坏性的操作,因为它会导致消息在代理上被删除。另一方面,在基于日志的消息代理中,使用消息更像是从文件中读取数据:这是只读操作,不会更改日志。 除了消费者的任何输出之外,处理的唯一副作用是消费者偏移量的前进。但偏移量是在消费者的控制之下的,所以如果需要的话可以很容易地操纵:例如你可以用昨天的偏移量跑一个消费者副本,并将输出写到不同的位置,以便重新处理最近一天的消息。你可以使用各种不同的处理代码重复任意次。 @@ -206,158 +206,158 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 我们已经在消息代理和数据库之间进行了一些比较。尽管传统上它们被视为单独的工具类别,但是我们看到基于日志的消息代理已经成功地从数据库中获取灵感并将其应用于消息传递。我们也可以反过来:从消息传递和流中获取灵感,并将它们应用于数据库。 -我们之前曾经说过,事件是某个时刻发生的事情的记录。发生的事情可能是用户操作(例如键入搜索查询)或读取传感器,但也可能是**写入数据库**。某些东西被写入数据库的事实是可以被捕获、存储和处理的事件。这一观察结果表明,数据库和数据流之间的联系不仅仅是磁盘日志的物理存储 —— 而是更深层的联系。 +我们之前曾经说过,事件是某个时刻发生的事情的记录。发生的事情可能是用户操作(例如键入搜索查询)或读取传感器,但也可能是 **写入数据库**。某些东西被写入数据库的事实是可以被捕获、存储和处理的事件。这一观察结果表明,数据库和数据流之间的联系不仅仅是磁盘日志的物理存储 —— 而是更深层的联系。 -事实上,复制日志(请参阅“[复制日志的实现](ch5.md#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。 +事实上,复制日志(请参阅 “[复制日志的实现](ch5.md#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。 -我们还在“[全序广播](ch9.md#全序广播)”中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态 (假设事件处理是一个确定性的操作)。这是事件流的又一种场景! +我们还在 “[全序广播](ch9.md#全序广播)” 中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态 (假设事件处理是一个确定性的操作)。这是事件流的又一种场景! 在本节中,我们将首先看看异构数据系统中出现的一个问题,然后探讨如何通过将事件流的想法带入数据库来解决这个问题。 ### 保持系统同步 -正如我们在本书中所看到的,没有一个系统能够满足所有的数据存储、查询和处理需求。在实践中,大多数重要应用都需要组合使用几种不同的技术来满足所有的需求:例如,使用OLTP数据库来为用户请求提供服务,使用缓存来加速常见请求,使用全文索引来处理搜索查询,使用数据仓库用于分析。每一种技术都有自己的数据副本,并根据自己的目的进行存储方式的优化。 +正如我们在本书中所看到的,没有一个系统能够满足所有的数据存储、查询和处理需求。在实践中,大多数重要应用都需要组合使用几种不同的技术来满足所有的需求:例如,使用 OLTP 数据库来为用户请求提供服务,使用缓存来加速常见请求,使用全文索引来处理搜索查询,使用数据仓库用于分析。每一种技术都有自己的数据副本,并根据自己的目的进行存储方式的优化。 -由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由ETL进程执行(请参阅“[数据仓库](ch3.md#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在“[批处理工作流的输出](ch10.md#批处理工作流的输出)”中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。 +由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由 ETL 进程执行(请参阅 “[数据仓库](ch3.md#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。 -如果周期性的完整数据库转储过于缓慢,有时会使用的替代方法是**双写(dual write)**,其中应用代码在数据变更时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存项失效(甚至同时执行这些写入)。 +如果周期性的完整数据库转储过于缓慢,有时会使用的替代方法是 **双写(dual write)**,其中应用代码在数据变更时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存项失效(甚至同时执行这些写入)。 -但是,双写有一些严重的问题,其中一个是竞争条件,如[图11-4](img/fig11-4.png)所示。在这个例子中,两个客户端同时想要更新一个项目X:客户端1想要将值设置为A,客户端2想要将其设置为B。两个客户端首先将新值写入数据库,然后将其写入到搜索索引。因为运气不好,这些请求的时序是交错的:数据库首先看到来自客户端1的写入将值设置为A,然后来自客户端2的写入将值设置为B,因此数据库中的最终值为B。搜索索引首先看到来自客户端2的写入,然后是客户端1的写入,所以搜索索引中的最终值是A。即使没发生错误,这两个系统现在也永久地不一致了。 +但是,双写有一些严重的问题,其中一个是竞争条件,如 [图 11-4](img/fig11-4.png) 所示。在这个例子中,两个客户端同时想要更新一个项目 X:客户端 1 想要将值设置为 A,客户端 2 想要将其设置为 B。两个客户端首先将新值写入数据库,然后将其写入到搜索索引。因为运气不好,这些请求的时序是交错的:数据库首先看到来自客户端 1 的写入将值设置为 A,然后来自客户端 2 的写入将值设置为 B,因此数据库中的最终值为 B。搜索索引首先看到来自客户端 2 的写入,然后是客户端 1 的写入,所以搜索索引中的最终值是 A。即使没发生错误,这两个系统现在也永久地不一致了。 ![](img/fig11-4.png) -**图11-4 在数据库中X首先被设置为A,然后被设置为B,而在搜索索引处,写入以相反的顺序到达** +**图 11-4 在数据库中 X 首先被设置为 A,然后被设置为 B,而在搜索索引处,写入以相反的顺序到达** -除非有一些额外的并发检测机制,例如我们在“[检测并发写入](ch5.md#检测并发写入)”中讨论的版本向量,否则你甚至不会意识到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。 +除非有一些额外的并发检测机制,例如我们在 “[检测并发写入](ch5.md#检测并发写入)” 中讨论的版本向量,否则你甚至不会意识到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。 -双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(请参阅“[原子提交与两阶段提交](ch7.md#原子提交与两阶段提交)”)。 +双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(请参阅 “[原子提交与两阶段提交](ch7.md#原子提交与两阶段提交)”)。 -如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在[图11-4](img/fig11-4.png)中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(请参阅“[多主复制](ch5.md#多主复制)“)。 +如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在 [图 11-4](img/fig11-4.png) 中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(请参阅 “[多主复制](ch5.md#多主复制)“)。 如果实际上只有一个领导者 —— 例如,数据库 —— 而且我们能让搜索索引成为数据库的追随者,情况要好得多。但这在实践中可能吗? ### 变更数据捕获 -大多数数据库的复制日志的问题在于,它们一直被当做数据库的内部实现细节,而不是公开的API。客户端应该通过其数据模型和查询语言来查询数据库,而不是解析复制日志并尝试从中提取数据。 +大多数数据库的复制日志的问题在于,它们一直被当做数据库的内部实现细节,而不是公开的 API。客户端应该通过其数据模型和查询语言来查询数据库,而不是解析复制日志并尝试从中提取数据。 数十年来,许多数据库根本没有记录在档的获取变更日志的方式。由于这个原因,捕获数据库中所有的变更,然后将其复制到其他存储技术(搜索索引、缓存或数据仓库)中是相当困难的。 -最近,人们对**变更数据捕获(change data capture, CDC)** 越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。 CDC是非常有意思的,尤其是当变更能在被写入后立刻用于流时。 +最近,人们对 **变更数据捕获(change data capture, CDC)** 越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。 CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。 -例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如[图11-5](img/fig11-5.png)所示。 +例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如 [图 11-5](img/fig11-5.png) 所示。 ![](img/fig11-5.png) -**图11-5 将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统** +**图 11-5 将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统** #### 变更数据捕获的实现 -我们可以将日志消费者叫做**衍生数据系统**,正如在[第三部分](part-iii.md)的介绍中所讨论的:存储在搜索索引和数据仓库中的数据,只是**记录系统**数据的额外视图。变更数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在衍生数据系统中,以便衍生系统具有数据的准确副本。 +我们可以将日志消费者叫做 **衍生数据系统**,正如在 [第三部分](part-iii.md) 的介绍中所讨论的:存储在搜索索引和数据仓库中的数据,只是 **记录系统** 数据的额外视图。变更数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在衍生数据系统中,以便衍生系统具有数据的准确副本。 -从本质上说,变更数据捕获使得一个数据库成为领导者(被捕获变化的数据库),并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更事件,因为它保留了消息的顺序(避免了[图11-2](img/fig11-2.png)的重新排序问题)。 +从本质上说,变更数据捕获使得一个数据库成为领导者(被捕获变化的数据库),并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更事件,因为它保留了消息的顺序(避免了 [图 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】。 +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#复制延迟问题)”)。 #### 初始快照 -如果你拥有**所有**对数据库进行变更的日志,则可以通过重播该日志,来重建数据库的完整状态。但是在许多情况下,永远保留所有更改会耗费太多磁盘空间,且重播过于费时,因此日志需要被截断。 +如果你拥有 **所有** 对数据库进行变更的日志,则可以通过重播该日志,来重建数据库的完整状态。但是在许多情况下,永远保留所有更改会耗费太多磁盘空间,且重播过于费时,因此日志需要被截断。 -例如,构建新的全文索引需要整个数据库的完整副本 —— 仅仅应用最近变更的日志是不够的,因为这样会丢失最近未曾更新的项目。因此,如果你没有完整的历史日志,则需要从一个一致的快照开始,如先前的“[设置新从库](ch5.md#设置新从库)”中所述。 +例如,构建新的全文索引需要整个数据库的完整副本 —— 仅仅应用最近变更的日志是不够的,因为这样会丢失最近未曾更新的项目。因此,如果你没有完整的历史日志,则需要从一个一致的快照开始,如先前的 “[设置新从库](ch5.md#设置新从库)” 中所述。 -数据库的快照必须与变更日志中的已知位置或偏移量相对应,以便在处理完快照后知道从哪里开始应用变更。一些CDC工具集成了这种快照功能,而其他工具则把它留给你手动执行。 +数据库的快照必须与变更日志中的已知位置或偏移量相对应,以便在处理完快照后知道从哪里开始应用变更。一些 CDC 工具集成了这种快照功能,而其他工具则把它留给你手动执行。 #### 日志压缩 -如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但**日志压缩(log compaction)** 提供了一个很好的备选方案。 +如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但 **日志压缩(log compaction)** 提供了一个很好的备选方案。 -我们之前在“[散列索引](ch3.md#散列索引)”中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。 +我们之前在 “[散列索引](ch3.md#散列索引)” 中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅 [图 3-2](img/fig3-2.png) 的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。 -在日志结构存储引擎中,具有特殊值NULL(**墓碑**,即tombstone)的更新表示该键被删除,并会在日志压缩过程中被移除。但只要键不被覆盖或删除,它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入,则先前的值将最终将被垃圾回收,只有最新的值会保留下来。 +在日志结构存储引擎中,具有特殊值 NULL(**墓碑**,即 tombstone)的更新表示该键被删除,并会在日志压缩过程中被移除。但只要键不被覆盖或删除,它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入,则先前的值将最终将被垃圾回收,只有最新的值会保留下来。 -在基于日志的消息代理与变更数据捕获的上下文中也适用相同的想法。如果CDC系统被配置为,每个变更都包含一个主键,且每个键的更新都替换了该键以前的值,那么只需要保留对键的最新写入就足够了。 +在基于日志的消息代理与变更数据捕获的上下文中也适用相同的想法。如果 CDC 系统被配置为,每个变更都包含一个主键,且每个键的更新都替换了该键以前的值,那么只需要保留对键的最新写入就足够了。 -现在,无论何时需要重建衍生数据系统(如搜索索引),你可以从压缩日志主题的零偏移量处启动新的消费者,然后依次扫描日志中的所有消息。日志能保证包含数据库中每个键的最新值(也可能是一些较旧的值)—— 换句话说,你可以使用它来获取数据库内容的完整副本,而无需从CDC源数据库取一个快照。 +现在,无论何时需要重建衍生数据系统(如搜索索引),你可以从压缩日志主题的零偏移量处启动新的消费者,然后依次扫描日志中的所有消息。日志能保证包含数据库中每个键的最新值(也可能是一些较旧的值)—— 换句话说,你可以使用它来获取数据库内容的完整副本,而无需从 CDC 源数据库取一个快照。 -Apache Kafka支持这种日志压缩功能。正如我们将在本章后面看到的,它允许消息代理被当成持久性存储使用,而不仅仅是用于临时消息。 +Apache Kafka 支持这种日志压缩功能。正如我们将在本章后面看到的,它允许消息代理被当成持久性存储使用,而不仅仅是用于临时消息。 #### 变更流的API支持 -越来越多的数据库开始将变更流作为第一等的接口,而不像传统上要去做加装改造,或者费工夫逆向工程一个CDC。例如,RethinkDB允许查询订阅通知,当查询结果变更时获得通知【36】,Firebase 【37】和CouchDB 【38】基于变更流进行同步,该变更流同样可用于应用。而Meteor使用MongoDB oplog订阅数据变更,并改变了用户接口【39】。 +越来越多的数据库开始将变更流作为第一等的接口,而不像传统上要去做加装改造,或者费工夫逆向工程一个 CDC。例如,RethinkDB 允许查询订阅通知,当查询结果变更时获得通知【36】,Firebase 【37】和 CouchDB 【38】基于变更流进行同步,该变更流同样可用于应用。而 Meteor 使用 MongoDB oplog 订阅数据变更,并改变了用户接口【39】。 -VoltDB允许事务以流的形式连续地从数据库中导出数据【40】。数据库将关系数据模型中的输出流表示为一个表,事务可以向其中插入元组,但不能查询。已提交事务按照提交顺序写入这个特殊表,而流则由该表中的元组日志构成。外部消费者可以异步消费该日志,并使用它来更新衍生数据系统。 +VoltDB 允许事务以流的形式连续地从数据库中导出数据【40】。数据库将关系数据模型中的输出流表示为一个表,事务可以向其中插入元组,但不能查询。已提交事务按照提交顺序写入这个特殊表,而流则由该表中的元组日志构成。外部消费者可以异步消费该日志,并使用它来更新衍生数据系统。 -Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获工具与Kafka集成。一旦变更事件进入Kafka中,它就可以用于更新衍生数据系统,比如搜索索引,也可以用于本章稍后讨论的流处理系统。 +Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获工具与 Kafka 集成。一旦变更事件进入 Kafka 中,它就可以用于更新衍生数据系统,比如搜索索引,也可以用于本章稍后讨论的流处理系统。 ### 事件溯源 -我们在这里讨论的想法和**事件溯源(Event Sourcing)** 之间有一些相似之处,这是一个在 **领域驱动设计(domain-driven design, DDD)** 社区中折腾出来的技术。我们将简要讨论事件溯源,因为它包含了一些关于流处理系统的有用想法。 +我们在这里讨论的想法和 **事件溯源(Event Sourcing)** 之间有一些相似之处,这是一个在 **领域驱动设计(domain-driven design, DDD)** 社区中折腾出来的技术。我们将简要讨论事件溯源,因为它包含了一些关于流处理系统的有用想法。 -与变更数据捕获类似,事件溯源涉及到**将所有对应用状态的变更**存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了一个不同的抽象层次上: +与变更数据捕获类似,事件溯源涉及到 **将所有对应用状态的变更** 存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了一个不同的抽象层次上: -* 在变更数据捕获中,应用以**可变方式(mutable way)** 使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞态条件。写入数据库的应用不需要知道CDC的存在。 +* 在变更数据捕获中,应用以 **可变方式(mutable way)** 使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免 [图 11-4](img/fig11-4.png) 中的竞态条件。写入数据库的应用不需要知道 CDC 的存在。 * 在事件溯源中,应用逻辑显式构建在写入事件日志的不可变事件之上。在这种情况下,事件存储是仅追加写入的,更新与删除是不鼓励的或禁止的。事件被设计为旨在反映应用层面发生的事情,而不是底层的状态变更。 -事件溯源是一种强大的数据建模技术:从应用的角度来看,将用户的行为记录为不可变的事件更有意义,而不是在可变数据库中记录这些行为的影响。事件溯源使得应用随时间演化更为容易,通过更容易理解事情发生的原因来帮助调试的进行,并有利于防止应用Bug(请参阅“[不可变事件的优点](#不可变事件的优点)”)。 +事件溯源是一种强大的数据建模技术:从应用的角度来看,将用户的行为记录为不可变的事件更有意义,而不是在可变数据库中记录这些行为的影响。事件溯源使得应用随时间演化更为容易,通过更容易理解事情发生的原因来帮助调试的进行,并有利于防止应用 Bug(请参阅 “[不可变事件的优点](#不可变事件的优点)”)。 -例如,存储“学生取消选课”事件以中性的方式清楚地表达了单个行为的意图,而其副作用“从登记表中删除了一个条目,而一条取消原因的记录被添加到学生反馈表“则嵌入了很多有关稍后对数据的使用方式的假设。如果引入一个新的应用功能,例如“将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地从现有事件中脱开。 +例如,存储 “学生取消选课” 事件以中性的方式清楚地表达了单个行为的意图,而其副作用 “从登记表中删除了一个条目,而一条取消原因的记录被添加到学生反馈表 “则嵌入了很多有关稍后对数据的使用方式的假设。如果引入一个新的应用功能,例如 “将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地从现有事件中脱开。 -事件溯源类似于**编年史(chronicle)** 数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 +事件溯源类似于 **编年史(chronicle)** 数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 -诸如Event Store【46】这样的专业数据库已经被开发出来,供使用事件溯源的应用使用,但总的来说,这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。 +诸如 Event Store【46】这样的专业数据库已经被开发出来,供使用事件溯源的应用使用,但总的来说,这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。 #### 从事件日志中派生出当前状态 事件日志本身并不是很有用,因为用户通常期望看到的是系统的当前状态,而不是变更历史。例如,在购物网站上,用户期望能看到他们购物车里的当前内容,而不是他们购物车所有变更的一个仅追加列表。 -因此,使用事件溯源的应用需要拉取事件日志(表示**写入**系统的数据),并将其转换为适合向用户显示的应用状态(从系统**读取**数据的方式【47】)。这种转换可以使用任意逻辑,但它应当是确定性的,以便能再次运行,并从事件日志中衍生出相同的应用状态。 +因此,使用事件溯源的应用需要拉取事件日志(表示 **写入** 系统的数据),并将其转换为适合向用户显示的应用状态(从系统 **读取** 数据的方式【47】)。这种转换可以使用任意逻辑,但它应当是确定性的,以便能再次运行,并从事件日志中衍生出相同的应用状态。 与变更数据捕获一样,重播事件日志允许让你重新构建系统的当前状态。不过,日志压缩需要采用不同的方式处理: -* 用于记录更新的CDC事件通常包含记录的**完整新版本**,因此主键的当前值完全由该主键的最近事件确定,而日志压缩可以丢弃相同主键的先前事件。 +* 用于记录更新的 CDC 事件通常包含记录的 **完整新版本**,因此主键的当前值完全由该主键的最近事件确定,而日志压缩可以丢弃相同主键的先前事件。 * 另一方面,事件溯源在更高层次进行建模:事件通常表示用户操作的意图,而不是因为操作而发生的状态更新机制。在这种情况下,后面的事件通常不会覆盖先前的事件,所以你需要完整的历史事件来重新构建最终状态。这里进行同样的日志压缩是不可能的。 -使用事件溯源的应用通常有一些机制,用于存储从事件日志中导出的当前状态快照,因此它们不需要重复处理完整的日志。然而这只是一种性能优化,用来加速读取,提高从崩溃中恢复的速度;真正的目的是系统能够永久存储所有原始事件,并在需要时重新处理完整的事件日志。我们将在“[不变性的局限性](#不变性的局限性)”中讨论这个假设。 +使用事件溯源的应用通常有一些机制,用于存储从事件日志中导出的当前状态快照,因此它们不需要重复处理完整的日志。然而这只是一种性能优化,用来加速读取,提高从崩溃中恢复的速度;真正的目的是系统能够永久存储所有原始事件,并在需要时重新处理完整的事件日志。我们将在 “[不变性的局限性](#不变性的局限性)” 中讨论这个假设。 #### 命令和事件 -事件溯源的哲学是仔细区分**事件(event)** 和**命令(command)**【48】。当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变为一个持久化且不可变的事件。 +事件溯源的哲学是仔细区分 **事件(event)** 和 **命令(command)**【48】。当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变为一个持久化且不可变的事件。 -例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。(先前在“[容错共识](ch8.md#容错共识)”中讨论过这个例子)当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户ID注册的,或者座位已经预留给特定的顾客。 +例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。(先前在 “[容错共识](ch8.md#容错共识)” 中讨论过这个例子)当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户 ID 注册的,或者座位已经预留给特定的顾客。 -在事件生成的时刻,它就成为了**事实(fact)**。即使客户稍后决定更改或取消预订,他们之前曾预定了某个特定座位的事实仍然成立,而更改或取消是之后添加的单独的事件。 +在事件生成的时刻,它就成为了 **事实(fact)**。即使客户稍后决定更改或取消预订,他们之前曾预定了某个特定座位的事实仍然成立,而更改或取消是之后添加的单独的事件。 事件流的消费者不允许拒绝事件:当消费者看到事件时,它已经成为日志中不可变的一部分,并且可能已经被其他消费者看到了。因此任何对命令的验证,都需要在它成为事件之前同步完成。例如,通过使用一个可以原子性地自动验证命令并发布事件的可串行事务。 -或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中所述) 。这种分割方式允许验证发生在一个异步的过程中。 +或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中所述) 。这种分割方式允许验证发生在一个异步的过程中。 ### 状态、流和不变性 -我们在[第十章](ch10.md)中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。 +我们在 [第十章](ch10.md) 中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。 我们通常将数据库视为应用程序当前状态的存储 —— 这种表示针对读取进行了优化,而且通常对于服务查询而言是最为方便的表示。状态的本质是,它会变化,所以数据库才会支持数据的增删改。这又该如何匹配不变性呢? -只要你的状态发生了变化,那么这个状态就是这段时间中事件修改的结果。例如,当前可用的座位列表是你已处理的预订所产生的结果,当前帐户余额是帐户中的借与贷的结果,而Web服务器的响应时间图,是所有已发生Web请求的独立响应时间的聚合结果。 +只要你的状态发生了变化,那么这个状态就是这段时间中事件修改的结果。例如,当前可用的座位列表是你已处理的预订所产生的结果,当前帐户余额是帐户中的借与贷的结果,而 Web 服务器的响应时间图,是所有已发生 Web 请求的独立响应时间的聚合结果。 -无论状态如何变化,总是有一系列事件导致了这些变化。即使事情已经执行与回滚,这些事件出现是始终成立的。关键的想法是:可变的状态与不可变事件的仅追加日志相互之间并不矛盾:它们是一体两面,互为阴阳的。所有变化的日志—— **变化日志(changelog)**,表示了随时间演变的状态。 +无论状态如何变化,总是有一系列事件导致了这些变化。即使事情已经执行与回滚,这些事件出现是始终成立的。关键的想法是:可变的状态与不可变事件的仅追加日志相互之间并不矛盾:它们是一体两面,互为阴阳的。所有变化的日志 —— **变化日志(changelog)**,表示了随时间演变的状态。 -如果你倾向于数学表示,那么你可能会说,应用状态是事件流对时间求积分得到的结果,而变更流是状态对时间求微分的结果,如[图11-6](img/fig11-6.png)所示【49,50,51】。这个比喻有一些局限性(例如,状态的二阶导似乎没有意义),但这是考虑数据的一个实用出发点。 +如果你倾向于数学表示,那么你可能会说,应用状态是事件流对时间求积分得到的结果,而变更流是状态对时间求微分的结果,如 [图 11-6](img/fig11-6.png) 所示【49,50,51】。这个比喻有一些局限性(例如,状态的二阶导似乎没有意义),但这是考虑数据的一个实用出发点。 $$ state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\ stream(t) = \frac{d\ state(t)}{dt} $$ ![](img/fig11-6.png) -**图11-6 应用当前状态与事件流之间的关系** +**图 11-6 应用当前状态与事件流之间的关系** -如果你持久存储了变更日志,那么重现状态就非常简单。如果你认为事件日志是你的记录系统,而所有的衍生状态都从它派生而来,那么系统中的数据流动就容易理解的多。正如帕特·赫兰(Pat Helland)所说的【52】: +如果你持久存储了变更日志,那么重现状态就非常简单。如果你认为事件日志是你的记录系统,而所有的衍生状态都从它派生而来,那么系统中的数据流动就容易理解的多。正如帕特・赫兰(Pat Helland)所说的【52】: > 事务日志记录了数据库的所有变更。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。 -日志压缩(如“[日志压缩](#日志压缩)”中所述)是连接日志与数据库状态之间的桥梁:它只保留每条记录的最新版本,并丢弃被覆盖的版本。 +日志压缩(如 “[日志压缩](#日志压缩)” 中所述)是连接日志与数据库状态之间的桥梁:它只保留每条记录的最新版本,并丢弃被覆盖的版本。 #### 不可变事件的优点 @@ -365,43 +365,43 @@ $$ 如果发生错误,会计师不会删除或更改分类帐中的错误交易 —— 而是添加另一笔交易以补偿错误,例如退还一笔不正确的费用。不正确的交易将永远保留在分类帐中,对于审计而言可能非常重要。如果从不正确的分类账衍生出的错误数字已经公布,那么下一个会计周期的数字就会包括一个更正。这个过程在会计事务中是很常见的【54】。 -尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如“[批处理输出的哲学](ch10.md#批处理输出的哲学)”中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。 +尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如 “[批处理输出的哲学](ch10.md#批处理输出的哲学)” 中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。 不可变的事件也包含了比当前状态更多的信息。例如在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然从履行订单的角度,第二个事件取消了第一个事件,但对分析目的而言,知道客户考虑过某个特定项而之后又反悔,可能是很有用的。也许他们会选择在未来购买,或者他们已经找到了替代品。这个信息被记录在事件日志中,但对于移出购物车就删除记录的数据库而言,这个信息在移出购物车时可能就丢失了【42】。 #### 从同一事件日志中派生多个视图 -此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样([图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】。 -如果你不需要担心如何查询与访问数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性,都是希望支持某些特定查询和访问模式的结果(请参阅[第三章](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#描述负载)”中,我们讨论了推特主页时间线,它是特定用户关注的人群所发推特的缓存(类似邮箱)。这是**针对读取优化的状态**的又一个例子:主页时间线是高度非规范化的,因为你的推文与你所有粉丝的时间线都构成了重复。然而,扇出服务保持了这种重复状态与新推特以及新关注关系的同步,从而保证了重复的可管理性。 +在 “[描述负载](ch1.md#描述负载)” 中,我们讨论了推特主页时间线,它是特定用户关注的人群所发推特的缓存(类似邮箱)。这是 **针对读取优化的状态** 的又一个例子:主页时间线是高度非规范化的,因为你的推文与你所有粉丝的时间线都构成了重复。然而,扇出服务保持了这种重复状态与新推特以及新关注关系的同步,从而保证了重复的可管理性。 #### 并发控制 -事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在“[读己之写](ch5.md#读己之写)”中讨论了这个问题以及可能的解决方案。 +事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在 “[读己之写](ch5.md#读己之写)” 中讨论了这个问题以及可能的解决方案。 -一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要**事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中讨论的方法。 +一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要 **事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中讨论的方法。 -另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。 +另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。 -如果事件日志与应用状态以相同的方式分区(例如,处理分区3中的客户事件只需要更新分区3中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(请参阅“[真的串行执行](ch7.md#真的串行执行)”)。日志通过在分区中定义事件的序列顺序,消除了并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在[第十二章](ch12.md)讨论。 +如果事件日志与应用状态以相同的方式分区(例如,处理分区 3 中的客户事件只需要更新分区 3 中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(请参阅 “[真的串行执行](ch7.md#真的串行执行)”)。日志通过在分区中定义事件的序列顺序,消除了并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在 [第十二章](ch12.md) 讨论。 #### 不变性的局限性 -许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅“[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 +许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅 “[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git、Mercurial 和 Fossil 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 -永远保持所有变更的不变历史,在多大程度上是可行的?答案取决于数据集的流失率。一些工作负载主要是添加数据,很少更新或删除;它们很容易保持不变。其他工作负载在相对较小的数据集上有较高的更新/删除率;在这些情况下,不可变的历史可能增至难以接受的巨大,碎片化可能成为一个问题,压缩与垃圾收集的表现对于运维的稳健性变得至关重要【60,61】。 +永远保持所有变更的不变历史,在多大程度上是可行的?答案取决于数据集的流失率。一些工作负载主要是添加数据,很少更新或删除;它们很容易保持不变。其他工作负载在相对较小的数据集上有较高的更新 / 删除率;在这些情况下,不可变的历史可能增至难以接受的巨大,碎片化可能成为一个问题,压缩与垃圾收集的表现对于运维的稳健性变得至关重要【60,61】。 除了性能方面的原因外,也可能有出于管理方面的原因需要删除数据的情况,尽管这些数据都是不可变的。例如,隐私条例可能要求在用户关闭帐户后删除他们的个人信息,数据保护立法可能要求删除错误的信息,或者可能需要阻止敏感信息的意外泄露。 -在这种情况下,仅仅在日志中添加另一个事件来指明先前的数据应该被视为删除是不够的 —— 你实际上是想改写历史,并假装数据从一开始就没有写入。例如,Datomic管这个特性叫**切除(excision)** 【62】,而Fossil版本控制系统有一个类似的概念叫**避免(shunning)** 【63】。 +在这种情况下,仅仅在日志中添加另一个事件来指明先前的数据应该被视为删除是不够的 —— 你实际上是想改写历史,并假装数据从一开始就没有写入。例如,Datomic 管这个特性叫 **切除(excision)** 【62】,而 Fossil 版本控制系统有一个类似的概念叫 **避免(shunning)** 【63】。 -真正删除数据是非常非常困难的【64】,因为副本可能存在于很多地方:例如,存储引擎,文件系统和SSD通常会向一个新位置写入,而不是原地覆盖旧数据【52】,而备份通常是特意做成不可变的,防止意外删除或损坏。删除操作更多的是指“使取回数据更困难”,而不是指“使取回数据不可能”。无论如何,有时你必须得尝试,正如我们在“[立法与自律](ch12.md#立法与自律)”中所看到的。 +真正删除数据是非常非常困难的【64】,因为副本可能存在于很多地方:例如,存储引擎,文件系统和 SSD 通常会向一个新位置写入,而不是原地覆盖旧数据【52】,而备份通常是特意做成不可变的,防止意外删除或损坏。删除操作更多的是指 “使取回数据更困难”,而不是指 “使取回数据不可能”。无论如何,有时你必须得尝试,正如我们在 “[立法与自律](ch12.md#立法与自律)” 中所看到的。 ## 流处理 @@ -410,15 +410,15 @@ $$ 剩下的就是讨论一下你可以用流做什么 —— 也就是说,你可以处理它。一般来说,有三种选项: -1. 你可以将事件中的数据写入数据库、缓存、搜索索引或类似的存储系统,然后能被其他客户端查询。如[图11-5](img/fig11-5.png)所示,这是数据库与系统其他部分所发生的变更保持同步的好方法 —— 特别是当流消费者是写入数据库的唯一客户端时。如“[批处理工作流的输出](ch10.md#批处理工作流的输出)”中所讨论的,它是写入存储系统的流等价物。 +1. 你可以将事件中的数据写入数据库、缓存、搜索索引或类似的存储系统,然后能被其他客户端查询。如 [图 11-5](img/fig11-5.png) 所示,这是数据库与系统其他部分所发生的变更保持同步的好方法 —— 特别是当流消费者是写入数据库的唯一客户端时。如 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 中所讨论的,它是写入存储系统的流等价物。 2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。 -3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项1或2)。 +3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项 1 或 2)。 -在本章的剩余部分中,我们将讨论选项3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为**算子(operator)** 或**作业(job)**。它与我们在[第十章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。 +在本章的剩余部分中,我们将讨论选项 3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为 **算子(operator)** 或 **作业(job)**。它与我们在 [第十章](ch10.md) 中讨论过的 Unix 进程和 MapReduce 作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。 -流处理中的分区和并行化模式也非常类似于[第十章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不再重复这些主题。基本的Map操作(如转换和过滤记录)也是一样的。 +流处理中的分区和并行化模式也非常类似于 [第十章](ch10.md) 中介绍的 MapReduce 和数据流引擎,因此我们不再重复这些主题。基本的 Map 操作(如转换和过滤记录)也是一样的。 -与批量作业相比的一个关键区别是,流不会结束。这种差异会带来很多隐含的结果。正如本章开始部分所讨论的,排序对无界数据集没有意义,因此无法使用**排序合并连接**(请参阅“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重启失败任务,但是对于已经运行数年的流作业,重启后从头开始跑可能并不是一个可行的选项。 +与批量作业相比的一个关键区别是,流不会结束。这种差异会带来很多隐含的结果。正如本章开始部分所讨论的,排序对无界数据集没有意义,因此无法使用 **排序合并连接**(请参阅 “[Reduce 侧连接与分组](ch10.md#Reduce侧连接与分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重启失败任务,但是对于已经运行数年的流作业,重启后从头开始跑可能并不是一个可行的选项。 ### 流处理的应用 @@ -433,100 +433,100 @@ $$ #### 复合事件处理 -**复合事件处理(complex event processing, CEP)** 是20世纪90年代为分析事件流而开发出的一种方法,尤其适用于需要搜索某些事件模式的应用【65,66】。与正则表达式允许你在字符串中搜索特定字符模式的方式类似,CEP允许你指定规则以在流中搜索某些事件模式。 +**复合事件处理(complex event processing, CEP)** 是 20 世纪 90 年代为分析事件流而开发出的一种方法,尤其适用于需要搜索某些事件模式的应用【65,66】。与正则表达式允许你在字符串中搜索特定字符模式的方式类似,CEP 允许你指定规则以在流中搜索某些事件模式。 -CEP系统通常使用高层次的声明式查询语言,比如SQL,或者图形用户界面,来描述应该检测到的事件模式。这些查询被提交给处理引擎,该引擎消费输入流,并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个**复合事件**(即complex event,CEP因此得名),并附有检测到的事件模式详情【67】。 +CEP 系统通常使用高层次的声明式查询语言,比如 SQL,或者图形用户界面,来描述应该检测到的事件模式。这些查询被提交给处理引擎,该引擎消费输入流,并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个 **复合事件**(即 complex event,CEP 因此得名),并附有检测到的事件模式详情【67】。 -在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为临时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时丢掉查询。 CEP引擎反转了角色:查询是长期存储的,来自输入流的事件不断流过它们,搜索匹配事件模式的查询【68】。 +在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为临时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时丢掉查询。 CEP 引擎反转了角色:查询是长期存储的,来自输入流的事件不断流过它们,搜索匹配事件模式的查询【68】。 -CEP的实现包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO StreamBase和SQLstream。像Samza这样的分布式流处理组件,支持使用SQL在流上进行声明式查询【71】。 +CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIBCO StreamBase 和 SQLstream。像 Samza 这样的分布式流处理组件,支持使用 SQL 在流上进行声明式查询【71】。 #### 流分析 -使用流处理的另一个领域是对流进行分析。 CEP与流分析之间的边界是模糊的,但一般来说,分析往往对找出特定事件序列并不关心,而更关注大量事件上的聚合与统计指标 —— 例如: +使用流处理的另一个领域是对流进行分析。 CEP 与流分析之间的边界是模糊的,但一般来说,分析往往对找出特定事件序列并不关心,而更关注大量事件上的聚合与统计指标 —— 例如: * 测量某种类型事件的速率(每个时间间隔内发生的频率) * 滚动计算一段时间窗口内某个值的平均值 * 将当前的统计值与先前的时间区间的值对比(例如,检测趋势,当指标与上周同比异常偏高或偏低时报警) -这些统计值通常是在固定时间区间内进行计算的,例如,你可能想知道在过去5分钟内服务每秒查询次数的均值,以及此时间段内响应时间的第99百分位点。在几分钟内取平均,能抹平秒和秒之间的无关波动,且仍然能向你展示流量模式的时间图景。聚合的时间间隔称为**窗口(window)**,我们将在“[时间推理](#时间推理)”中更详细地讨论窗口。 +这些统计值通常是在固定时间区间内进行计算的,例如,你可能想知道在过去 5 分钟内服务每秒查询次数的均值,以及此时间段内响应时间的第 99 百分位点。在几分钟内取平均,能抹平秒和秒之间的无关波动,且仍然能向你展示流量模式的时间图景。聚合的时间间隔称为 **窗口(window)**,我们将在 “[时间推理](#时间推理)” 中更详细地讨论窗口。 -流分析系统有时会使用概率算法,例如Bloom filter(我们在“[性能优化](ch3.md#性能优化)”中遇到过)来管理成员资格,HyperLogLog【72】用于基数估计以及各种百分比估计算法(请参阅“[实践中的百分位点](ch1.md#实践中的百分位点)“)。概率算法产出近似的结果,但比起精确算法的优点是内存使用要少得多。使用近似算法有时让人们觉得流处理系统总是有损的和不精确的,但这是错误看法:流处理并没有任何内在的近似性,而概率算法只是一种优化【73】。 +流分析系统有时会使用概率算法,例如 Bloom filter(我们在 “[性能优化](ch3.md#性能优化)” 中遇到过)来管理成员资格,HyperLogLog【72】用于基数估计以及各种百分比估计算法(请参阅 “[实践中的百分位点](ch1.md#实践中的百分位点)“)。概率算法产出近似的结果,但比起精确算法的优点是内存使用要少得多。使用近似算法有时让人们觉得流处理系统总是有损的和不精确的,但这是错误看法:流处理并没有任何内在的近似性,而概率算法只是一种优化【73】。 -许多开源分布式流处理框架的设计都是针对分析设计的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 +许多开源分布式流处理框架的设计都是针对分析设计的:例如 Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams 【74】。托管服务包括 Google Cloud Dataflow 和 Azure Stream Analytics。 #### 维护物化视图 -我们在“[数据库与流](#数据库与流)”中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护**物化视图(materialized view)** 的一种具体场景(请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。 +我们在 “[数据库与流](#数据库与流)” 中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护 **物化视图(materialized view)** 的一种具体场景(请参阅 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。 -同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的**所有**事件,除了那些可能由日志压缩丢弃的过时事件(请参阅“[日志压缩](#日志压缩)“)。实际上,你需要一个可以一直延伸到时间开端的窗口。 +同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的 **所有** 事件,除了那些可能由日志压缩丢弃的过时事件(请参阅 “[日志压缩](#日志压缩)“)。实际上,你需要一个可以一直延伸到时间开端的窗口。 -原则上讲,任何流处理组件都可以用于维护物化视图,尽管“永远运行”与一些面向分析的框架假设的“主要在有限时间段窗口上运行”背道而驰, Samza和Kafka Streams支持这种用法,建立在Kafka对日志压缩的支持上【75】。 +原则上讲,任何流处理组件都可以用于维护物化视图,尽管 “永远运行” 与一些面向分析的框架假设的 “主要在有限时间段窗口上运行” 背道而驰, Samza 和 Kafka Streams 支持这种用法,建立在 Kafka 对日志压缩的支持上【75】。 #### 在流上搜索 -除了允许搜索由多个事件构成模式的CEP外,有时也存在基于复杂标准(例如全文搜索查询)来搜索单个事件的需求。 +除了允许搜索由多个事件构成模式的 CEP 外,有时也存在基于复杂标准(例如全文搜索查询)来搜索单个事件的需求。 -例如,媒体监测服务可以订阅新闻文章Feed与来自媒体的播客,搜索任何关于公司、产品或感兴趣的话题的新闻。这是通过预先构建一个搜索查询来完成的,然后不断地将新闻项的流与该查询进行匹配。在一些网站上也有类似的功能:例如,当市场上出现符合其搜索条件的新房产时,房地产网站的用户可以要求网站通知他们。Elasticsearch的这种过滤器功能,是实现这种流搜索的一种选择【76】。 +例如,媒体监测服务可以订阅新闻文章 Feed 与来自媒体的播客,搜索任何关于公司、产品或感兴趣的话题的新闻。这是通过预先构建一个搜索查询来完成的,然后不断地将新闻项的流与该查询进行匹配。在一些网站上也有类似的功能:例如,当市场上出现符合其搜索条件的新房产时,房地产网站的用户可以要求网站通知他们。Elasticsearch 的这种过滤器功能,是实现这种流搜索的一种选择【76】。 -传统的搜索引擎首先索引文件,然后在索引上跑查询。相比之下,搜索一个数据流则反了过来:查询被存储下来,文档从查询中流过,就像在CEP中一样。最简单的情况就是,你可以为每个文档测试每个查询。但是如果你有大量查询,这可能会变慢。为了优化这个过程,可以像对文档一样,为查询建立索引。因而收窄可能匹配的查询集合【77】。 +传统的搜索引擎首先索引文件,然后在索引上跑查询。相比之下,搜索一个数据流则反了过来:查询被存储下来,文档从查询中流过,就像在 CEP 中一样。最简单的情况就是,你可以为每个文档测试每个查询。但是如果你有大量查询,这可能会变慢。为了优化这个过程,可以像对文档一样,为查询建立索引。因而收窄可能匹配的查询集合【77】。 #### 消息传递和RPC -在“[消息传递中的数据流](ch4.md#消息传递中的数据流)”中我们讨论过,消息传递系统可以作为RPC的替代方案,即作为一种服务间通信的机制,比如在Actor模型中所使用的那样。尽管这些系统也是基于消息和事件,但我们通常不会将其视作流处理组件: +在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中我们讨论过,消息传递系统可以作为 RPC 的替代方案,即作为一种服务间通信的机制,比如在 Actor 模型中所使用的那样。尽管这些系统也是基于消息和事件,但我们通常不会将其视作流处理组件: -* Actor框架主要是管理模块通信的并发和分布式执行的一种机制,而流处理主要是一种数据管理技术。 -* Actor之间的交流往往是短暂的、一对一的;而事件日志则是持久的、多订阅者的。 -* Actor可以以任意方式进行通信(包括循环的请求/响应模式),但流处理通常配置在无环流水线中,其中每个流都是一个特定作业的输出,由良好定义的输入流中派生而来。 +* Actor 框架主要是管理模块通信的并发和分布式执行的一种机制,而流处理主要是一种数据管理技术。 +* Actor 之间的交流往往是短暂的、一对一的;而事件日志则是持久的、多订阅者的。 +* Actor 可以以任意方式进行通信(包括循环的请求 / 响应模式),但流处理通常配置在无环流水线中,其中每个流都是一个特定作业的输出,由良好定义的输入流中派生而来。 -也就是说,RPC类系统与流处理之间有一些交叉领域。例如,Apache Storm有一个称为**分布式RPC**的功能,它允许将用户查询分散到一系列也处理事件流的节点上;然后这些查询与来自输入流的事件交织,而结果可以被汇总并发回给用户【78】(另请参阅“[多分区数据处理](ch12.md#多分区数据处理)”)。 +也就是说,RPC 类系统与流处理之间有一些交叉领域。例如,Apache Storm 有一个称为 **分布式 RPC** 的功能,它允许将用户查询分散到一系列也处理事件流的节点上;然后这些查询与来自输入流的事件交织,而结果可以被汇总并发回给用户【78】(另请参阅 “[多分区数据处理](ch12.md#多分区数据处理)”)。 -也可以使用Actor框架来处理流。但是,很多这样的框架在崩溃时不能保证消息的传递,除非你实现了额外的重试逻辑,否则这种处理不是容错的。 +也可以使用 Actor 框架来处理流。但是,很多这样的框架在崩溃时不能保证消息的传递,除非你实现了额外的重试逻辑,否则这种处理不是容错的。 ### 时间推理 -流处理通常需要与时间打交道,尤其是用于分析目的时候,会频繁使用时间窗口,例如“过去五分钟的平均值”。“过去五分钟”的含义看上去似乎是清晰而无歧义的,但不幸的是,这个概念非常棘手。 +流处理通常需要与时间打交道,尤其是用于分析目的时候,会频繁使用时间窗口,例如 “过去五分钟的平均值”。“过去五分钟” 的含义看上去似乎是清晰而无歧义的,但不幸的是,这个概念非常棘手。 在批处理中过程中,大量的历史事件被快速地处理。如果需要按时间来分析,批处理器需要检查每个事件中嵌入的时间戳。读取运行批处理机器的系统时钟没有任何意义,因为处理运行的时间与事件实际发生的时间无关。 -批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是**确定性**的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“[容错](ch10.md#容错)”)。 +批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是 **确定性** 的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅 “[容错](ch10.md#容错)”)。 -另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间**,即processing time)来确定**窗口(windowing)**【79】。这种方法的优点是简单,如果事件创建与事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。 +另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间**,即 processing time)来确定 **窗口(windowing)**【79】。这种方法的优点是简单,如果事件创建与事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。 #### 事件时间与处理时间 -很多原因都可能导致处理延迟:排队,网络故障(请参阅“[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理/消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅“[重播旧消息](#重播旧消息)”),或者在修复代码BUG之后。 +很多原因都可能导致处理延迟:排队,网络故障(请参阅 “[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理 / 消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅 “[重播旧消息](#重播旧消息)”),或者在修复代码 BUG 之后。 -而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个Web请求(由Web服务器A处理),然后发出第二个请求(由服务器B处理)。 A和B发出描述它们所处理请求的事件,但是B的事件在A的事件发生之前到达消息代理。现在,流处理器将首先看到B事件,然后看到A事件,即使它们实际上是以相反的顺序发生的。 +而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个 Web 请求(由 Web 服务器 A 处理),然后发出第二个请求(由服务器 B 处理)。 A 和 B 发出描述它们所处理请求的事件,但是 B 的事件在 A 的事件发生之前到达消息代理。现在,流处理器将首先看到 B 事件,然后看到 A 事件,即使它们实际上是以相反的顺序发生的。 -有一个类比也许能帮助理解,“星球大战”电影:第四集于1977年发行,第五集于1980年,第六集于1983年,紧随其后的是1999年的第一集,2002年的第二集,和2005年的第三集,以及2015年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。 (集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门编写,以适应这种时序与顺序的问题。 +有一个类比也许能帮助理解,“星球大战” 电影:第四集于 1977 年发行,第五集于 1980 年,第六集于 1983 年,紧随其后的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。 (集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门编写,以适应这种时序与顺序的问题。 -[^ii]: 感谢Flink社区的Kostas Kloudas提出这个比喻。 +[^ii]: 感谢 Flink 社区的 Kostas Kloudas 提出这个比喻。 -将事件时间和处理时间搞混会导致错误的数据。例如,假设你有一个流处理器用于测量请求速率(计算每秒请求数)。如果你重新部署流处理器,它可能会停止一分钟,并在恢复之后处理积压的事件。如果你按处理时间来衡量速率,那么在处理积压日志时,请求速率看上去就像有一个异常的突发尖峰,而实际上请求速率是稳定的([图11-7](img/fig11-7.png))。 +将事件时间和处理时间搞混会导致错误的数据。例如,假设你有一个流处理器用于测量请求速率(计算每秒请求数)。如果你重新部署流处理器,它可能会停止一分钟,并在恢复之后处理积压的事件。如果你按处理时间来衡量速率,那么在处理积压日志时,请求速率看上去就像有一个异常的突发尖峰,而实际上请求速率是稳定的([图 11-7](img/fig11-7.png))。 ![](img/fig11-7.png) -**图11-7 按处理时间分窗,会因为处理速率的变动引入人为因素** +**图 11-7 按处理时间分窗,会因为处理速率的变动引入人为因素** #### 知道什么时候准备好了 用事件时间来定义窗口的一个棘手的问题是,你永远也无法确定是不是已经收到了特定窗口的所有事件,还是说还有一些事件正在来的路上。 -例如,假设你将事件分组为一分钟的窗口,以便统计每分钟的请求数。你已经计数了一些带有本小时内第37分钟时间戳的事件,时间流逝,现在进入的主要都是本小时内第38和第39分钟的事件。什么时候才能宣布你已经完成了第37分钟的窗口计数,并输出其计数器值? +例如,假设你将事件分组为一分钟的窗口,以便统计每分钟的请求数。你已经计数了一些带有本小时内第 37 分钟时间戳的事件,时间流逝,现在进入的主要都是本小时内第 38 和第 39 分钟的事件。什么时候才能宣布你已经完成了第 37 分钟的窗口计数,并输出其计数器值? 在一段时间没有看到任何新的事件之后,你可以超时并宣布一个窗口已经就绪,但仍然可能发生这种情况:某些事件被缓冲在另一台机器上,由于网络中断而延迟。你需要能够处理这种在窗口宣告完成之后到达的 **滞留(straggler)** 事件。大体上,你有两种选择【1】: 1. 忽略这些滞留事件,因为在正常情况下它们可能只是事件中的一小部分。你可以将丢弃事件的数量作为一个监控指标,并在出现大量丢消息的情况时报警。 -2. 发布一个**更正(correction)**,一个包括滞留事件的更新窗口值。你可能还需要收回以前的输出。 +2. 发布一个 **更正(correction)**,一个包括滞留事件的更新窗口值。你可能还需要收回以前的输出。 -在某些情况下,可以使用特殊的消息来指示“从现在开始,不会有比t更早时间戳的消息了”,消费者可以使用它来触发窗口【81】。但是,如果不同机器上的多个生产者都在生成事件,每个生产者都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下,添加和删除生产者都是比较棘手的。 +在某些情况下,可以使用特殊的消息来指示 “从现在开始,不会有比 t 更早时间戳的消息了”,消费者可以使用它来触发窗口【81】。但是,如果不同机器上的多个生产者都在生成事件,每个生产者都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下,添加和删除生产者都是比较棘手的。 #### 你用的是谁的时钟? 当事件可能在系统内多个地方进行缓冲时,为事件分配时间戳更加困难了。例如,考虑一个移动应用向服务器上报关于用量的事件。该应用可能会在设备处于脱机状态时被使用,在这种情况下,它将在设备本地缓冲事件,并在下一次互联网连接可用时向服务器上报这些事件(可能是几小时甚至几天)。对于这个流的任意消费者而言,它们就如延迟极大的滞留事件一样。 -在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(请参阅“[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。 +在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(请参阅 “[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。 要校正不正确的设备时钟,一种方法是记录三个时间戳【82】: @@ -544,74 +544,74 @@ CEP的实现包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO * 滚动窗口(Tumbling Window) - 滚动窗口有着固定的长度,每个事件都仅能属于一个窗口。例如,假设你有一个1分钟的滚动窗口,则所有时间戳在`10:03:00`和`10:03:59`之间的事件会被分组到一个窗口中,`10:04:00`和`10:04:59`之间的事件被分组到下一个窗口,依此类推。通过将每个事件时间戳四舍五入至最近的分钟来确定它所属的窗口,可以实现1分钟的滚动窗口。 + 滚动窗口有着固定的长度,每个事件都仅能属于一个窗口。例如,假设你有一个 1 分钟的滚动窗口,则所有时间戳在 `10:03:00` 和 `10:03:59` 之间的事件会被分组到一个窗口中,`10:04:00` 和 `10:04:59` 之间的事件被分组到下一个窗口,依此类推。通过将每个事件时间戳四舍五入至最近的分钟来确定它所属的窗口,可以实现 1 分钟的滚动窗口。 * 跳动窗口(Hopping Window) - 跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08:59`之间的事件,等等。通过首先计算1分钟的滚动窗口(tunmbling window),然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。 + 跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有 1 分钟跳跃步长的 5 分钟窗口将包含 `10:03:00` 至 `10:07:59` 之间的事件,而下一个窗口将覆盖 `10:04:00` 至 `10:08:59` 之间的事件,等等。通过首先计算 1 分钟的滚动窗口(tunmbling window),然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。 * 滑动窗口(Sliding Window) - 滑动窗口包含了彼此间距在特定时长内的所有事件。例如,一个5分钟的滑动窗口应当覆盖`10:03:39`和`10:08:12`的事件,因为它们相距不超过5分钟(注意滚动窗口与步长5分钟的跳动窗口可能不会把这两个事件分组到同一个窗口中,因为它们使用固定的边界)。通过维护一个按时间排序的事件缓冲区,并不断从窗口中移除过期的旧事件,可以实现滑动窗口。 + 滑动窗口包含了彼此间距在特定时长内的所有事件。例如,一个 5 分钟的滑动窗口应当覆盖 `10:03:39` 和 `10:08:12` 的事件,因为它们相距不超过 5 分钟(注意滚动窗口与步长 5 分钟的跳动窗口可能不会把这两个事件分组到同一个窗口中,因为它们使用固定的边界)。通过维护一个按时间排序的事件缓冲区,并不断从窗口中移除过期的旧事件,可以实现滑动窗口。 * 会话窗口(Session window) - 与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅“[分组](ch10.md#分组)”)。 + 与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果 30 分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅 “[分组](ch10.md#分组)”)。 ### 流连接 -在[第十章](ch10.md)中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。 +在 [第十章](ch10.md) 中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。 -然而,新事件随时可能出现在一个流中,这使得流连接要比批处理连接更具挑战性。为了更好地理解情况,让我们先来区分三种不同类型的连接:**流-流**连接,**流-表**连接,与**表-表**连接【84】。我们将在下面的章节中通过例子来说明。 +然而,新事件随时可能出现在一个流中,这使得流连接要比批处理连接更具挑战性。为了更好地理解情况,让我们先来区分三种不同类型的连接:**流 - 流** 连接,**流 - 表** 连接,与 **表 - 表** 连接【84】。我们将在下面的章节中通过例子来说明。 #### 流流连接(窗口连接) -假设你的网站上有搜索功能,而你想要找出搜索URL的近期趋势。每当有人键入搜索查询时,都会记录下一个包含查询与其返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击事件。为了计算搜索结果中每个URL的点击率,你需要将搜索动作与点击动作的事件连在一起,这些事件通过相同的会话ID进行连接。广告系统中需要类似的分析【85】。 +假设你的网站上有搜索功能,而你想要找出搜索 URL 的近期趋势。每当有人键入搜索查询时,都会记录下一个包含查询与其返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击事件。为了计算搜索结果中每个 URL 的点击率,你需要将搜索动作与点击动作的事件连在一起,这些事件通过相同的会话 ID 进行连接。广告系统中需要类似的分析【85】。 如果用户丢弃了搜索结果,点击可能永远不会发生,即使它出现了,搜索与点击之间的时间可能是高度可变的:在很多情况下,它可能是几秒钟,但也可能长达几天或几周(如果用户执行搜索,忘掉了这个浏览器页面,过了一段时间后重新回到这个浏览器页面上,并点击了一个结果)。由于可变的网络延迟,点击事件甚至可能先于搜索事件到达。你可以选择合适的连接窗口 —— 例如,如果点击与搜索之间的时间间隔在一小时内,你可能会选择连接两者。 请注意,在点击事件中嵌入搜索详情与事件连接并不一样:这样做的话,只有当用户点击了一个搜索结果时你才能知道,而那些没有点击的搜索就无能为力了。为了衡量搜索质量,你需要准确的点击率,为此搜索事件和点击事件两者都是必要的。 -为了实现这种类型的连接,流处理器需要维护**状态**:例如,按会话ID索引最近一小时内发生的所有事件。无论何时发生搜索事件或点击事件,都会被添加到合适的索引中,而流处理器也会检查另一个索引是否有具有相同会话ID的事件到达。如果有匹配事件就会发出一个表示搜索结果被点击的事件;如果搜索事件直到过期都没看见有匹配的点击事件,就会发出一个表示搜索结果未被点击的事件。 +为了实现这种类型的连接,流处理器需要维护 **状态**:例如,按会话 ID 索引最近一小时内发生的所有事件。无论何时发生搜索事件或点击事件,都会被添加到合适的索引中,而流处理器也会检查另一个索引是否有具有相同会话 ID 的事件到达。如果有匹配事件就会发出一个表示搜索结果被点击的事件;如果搜索事件直到过期都没看见有匹配的点击事件,就会发出一个表示搜索结果未被点击的事件。 #### 流表连接(流扩充) -在“[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图10-2](img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户ID的活动事件流,而输出还是活动事件流,但其中用户ID已经被扩展为用户的档案信息。这个过程有时被称为使用数据库的信息来**扩充(enriching)** 活动事件。 +在 “[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图 10-2](img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户 ID 的活动事件流,而输出还是活动事件流,但其中用户 ID 已经被扩展为用户的档案信息。这个过程有时被称为使用数据库的信息来 **扩充(enriching)** 活动事件。 -要执行此连接,流处理器需要一次处理一个活动事件,在数据库中查找事件的用户ID,并将档案信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现。但正如在“[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”一节中讨论的,此类远程查询可能会很慢,并且有可能导致数据库过载【75】。 +要执行此连接,流处理器需要一次处理一个活动事件,在数据库中查找事件的用户 ID,并将档案信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现。但正如在 “[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)” 一节中讨论的,此类远程查询可能会很慢,并且有可能导致数据库过载【75】。 -另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在“[Map侧连接](ch10.md#Map侧连接)”中讨论的散列连接非常相似:如果数据库的本地副本足够小,则可以是内存中的散列表,比较大的话也可以是本地磁盘上的索引。 +另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在 “[Map 侧连接](ch10.md#Map侧连接)” 中讨论的散列连接非常相似:如果数据库的本地副本足够小,则可以是内存中的散列表,比较大的话也可以是本地磁盘上的索引。 与批处理作业的区别在于,批处理作业使用数据库的时间点快照作为输入,而流处理器是长时间运行的,且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持更新。这个问题可以通过变更数据捕获来解决:流处理器可以订阅用户档案数据库的更新日志,如同活动事件流一样。当增添或修改档案时,流处理器会更新其本地副本。因此,我们有了两个流之间的连接:活动事件和档案更新。 -流表连接实际上非常类似于流流连接;最大的区别在于对于表的变更日志流,连接使用了一个可以回溯到“时间起点”的窗口(概念上是无限的窗口),新版本的记录会覆盖更早的版本。对于输入的流,连接可能压根儿就没有维护任何窗口。 +流表连接实际上非常类似于流流连接;最大的区别在于对于表的变更日志流,连接使用了一个可以回溯到 “时间起点” 的窗口(概念上是无限的窗口),新版本的记录会覆盖更早的版本。对于输入的流,连接可能压根儿就没有维护任何窗口。 #### 表表连接(维护物化视图) -我们在“[描述负载](ch1.md#描述负载)”中讨论的推特时间线例子时说过,当用户想要查看他们的主页时间线时,迭代用户所关注人群的推文并合并它们是一个开销巨大的操作。 +我们在 “[描述负载](ch1.md#描述负载)” 中讨论的推特时间线例子时说过,当用户想要查看他们的主页时间线时,迭代用户所关注人群的推文并合并它们是一个开销巨大的操作。 -相反,我们需要一个时间线缓存:一种每个用户的“收件箱”,在发送推文的时候写入这些信息,因而读取时间线时只需要简单地查询即可。物化与维护这个缓存需要处理以下事件: +相反,我们需要一个时间线缓存:一种每个用户的 “收件箱”,在发送推文的时候写入这些信息,因而读取时间线时只需要简单地查询即可。物化与维护这个缓存需要处理以下事件: -* 当用户u发送新的推文时,它将被添加到每个关注用户u的时间线上。 +* 当用户 u 发送新的推文时,它将被添加到每个关注用户 u 的时间线上。 * 用户删除推文时,推文将从所有用户的时间表中删除。 -* 当用户$u_1$开始关注用户$u_2$时,$u_2$最近的推文将被添加到$u_1$的时间线上。 -* 当用户$u_1$取消关注用户$u_2$时,$u_2$的推文将从$u_1$的时间线中移除。 +* 当用户 $u_1$ 开始关注用户 $u_2$ 时,$u_2$ 最近的推文将被添加到 $u_1$ 的时间线上。 +* 当用户 $u_1$ 取消关注用户 $u_2$ 时,$u_2$ 的推文将从 $u_1$ 的时间线中移除。 要在流处理器中实现这种缓存维护,你需要推文事件流(发送与删除)和关注关系事件流(关注与取消关注)。流处理需要维护一个数据库,包含每个用户的粉丝集合。以便知道当一条新推文到达时,需要更新哪些时间线【86】。 观察这个流处理过程的另一种视角是:它维护了一个连接了两个表(推文与关注)的物化视图,如下所示: ```sql -SELECT follows.follower_id AS timeline_id, +SELECT follows.follower_id AS timeline_id, array_agg(tweets.* ORDER BY tweets.timestamp DESC) FROM tweets -JOIN follows ON follows.followee_id = tweets.sender_id +JOIN follows ON follows.followee_id = tweets.sender_id GROUP BY follows.follower_id ``` -流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新[^iii]。 +流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新 [^iii]。 -[^iii]: 如果你将流视作表的衍生物,如[图11-6](img/fig11-6.png)所示,而把一个连接看作是两个表的乘法u·v,那么会发生一些有趣的事情:物化连接的变化流遵循乘积法则:(u·v)'= u'v + uv'。 换句话说,任何推文的变化量都与当前的关注联系在一起,任何关注的变化量都与当前的推文相连接【49,50】。 +[^iii]: 如果你将流视作表的衍生物,如 [图 11-6](img/fig11-6.png) 所示,而把一个连接看作是两个表的乘法u·v,那么会发生一些有趣的事情:物化连接的变化流遵循乘积法则:(u·v)'= u'v + uv'。 换句话说,任何推文的变化量都与当前的关注联系在一起,任何关注的变化量都与当前的推文相连接【49,50】。 #### 连接的时间依赖性 @@ -621,80 +621,80 @@ GROUP BY follows.follower_id 这就产生了一个问题:如果不同流中的事件发生在近似的时间范围内,则应该按照什么样的顺序进行处理?在流表连接的例子中,如果用户更新了它们的档案,哪些活动事件与旧档案连接(在档案更新前处理),哪些又与新档案连接(在档案更新之后处理)?换句话说:你需要对一些状态做连接,如果状态会随着时间推移而变化,那应当使用什么时间点来连接呢【45】? -这种时序依赖可能出现在很多地方。例如销售东西需要对发票应用适当的税率,这取决于所处的国家/州,产品类型,销售日期(因为税率时不时会变化)。当连接销售额与税率表时,你可能期望的是使用销售时的税率参与连接。如果你正在重新处理历史数据,销售时的税率可能和现在的税率有所不同。 +这种时序依赖可能出现在很多地方。例如销售东西需要对发票应用适当的税率,这取决于所处的国家 / 州,产品类型,销售日期(因为税率时不时会变化)。当连接销售额与税率表时,你可能期望的是使用销售时的税率参与连接。如果你正在重新处理历史数据,销售时的税率可能和现在的税率有所不同。 如果跨越流的事件顺序是未定的,则连接会变为不确定性的【87】,这意味着你在同样输入上重跑相同的作业未必会得到相同的结果:当你重跑任务时,输入流上的事件可能会以不同的方式交织。 -在数据仓库中,这个问题被称为**缓慢变化的维度(slowly changing dimension, SCD)**,通常通过对特定版本的记录使用唯一的标识符来解决:例如,每当税率改变时都会获得一个新的标识符,而发票在销售时会带有税率的标识符【88,89】。这种变化使连接变为确定性的,但也会导致日志压缩无法进行:表中所有的记录版本都需要保留。 +在数据仓库中,这个问题被称为 **缓慢变化的维度(slowly changing dimension, SCD)**,通常通过对特定版本的记录使用唯一的标识符来解决:例如,每当税率改变时都会获得一个新的标识符,而发票在销售时会带有税率的标识符【88,89】。这种变化使连接变为确定性的,但也会导致日志压缩无法进行:表中所有的记录版本都需要保留。 ### 容错 -在本章的最后一节中,让我们看一看流处理是如何容错的。我们在[第十章](ch10.md)中看到,批处理框架可以很容易地容错:如果MapReduce作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的独立文件中,而输出仅当任务成功完成后可见。 +在本章的最后一节中,让我们看一看流处理是如何容错的。我们在 [第十章](ch10.md) 中看到,批处理框架可以很容易地容错:如果 MapReduce 作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到 HDFS 上的独立文件中,而输出仅当任务成功完成后可见。 -特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为**恰好一次语义(exactly-once semantics)**,尽管**等效一次(effectively-once)** 可能会是一个更写实的术语【90】。 +特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为 **恰好一次语义(exactly-once semantics)**,尽管 **等效一次(effectively-once)** 可能会是一个更写实的术语【90】。 在流处理中也出现了同样的容错问题,但是处理起来没有那么直观:等待某个任务完成之后再使其输出可见并不是一个可行选项,因为你永远无法处理完一个无限的流。 #### 微批量与存档点 -一个解决方案是将流分解成小块,并像微型批处理一样处理每个块。这种方法被称为**微批次(microbatching)**,它被用于Spark Streaming 【91】。批次的大小通常约为1秒,这是对性能妥协的结果:较小的批次会导致更大的调度与协调开销,而较大的批次意味着流处理器结果可见之前的延迟要更长。 +一个解决方案是将流分解成小块,并像微型批处理一样处理每个块。这种方法被称为 **微批次(microbatching)**,它被用于 Spark Streaming 【91】。批次的大小通常约为 1 秒,这是对性能妥协的结果:较小的批次会导致更大的调度与协调开销,而较大的批次意味着流处理器结果可见之前的延迟要更长。 微批次也隐式提供了一个与批次大小相等的滚动窗口(按处理时间而不是事件时间戳分窗)。任何需要更大窗口的作业都需要显式地将状态从一个微批次转移到下一个微批次。 -Apache Flink则使用不同的方法,它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃,它可以从最近的存档点重启,并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的**壁障(barrier)** 触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。 +Apache Flink 则使用不同的方法,它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃,它可以从最近的存档点重启,并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的 **壁障(barrier)** 触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。 -在流处理框架的范围内,微批次与存档点方法提供了与批处理一样的**恰好一次语义**。但是,只要输出离开流处理器(例如,写入数据库,向外部消息代理发送消息,或发送电子邮件),框架就无法抛弃失败批次的输出了。在这种情况下,重启失败任务会导致外部副作用发生两次,只有微批次或存档点不足以阻止这一问题。 +在流处理框架的范围内,微批次与存档点方法提供了与批处理一样的 **恰好一次语义**。但是,只要输出离开流处理器(例如,写入数据库,向外部消息代理发送消息,或发送电子邮件),框架就无法抛弃失败批次的输出了。在这种情况下,重启失败任务会导致外部副作用发生两次,只有微批次或存档点不足以阻止这一问题。 #### 原子提交再现 -为了在出现故障时表现出恰好处理一次的样子,我们需要确保事件处理的所有输出和副作用**当且仅当**处理成功时才会生效。这些影响包括发送给下游算子或外部消息传递系统(包括电子邮件或推送通知)的任何消息,任何数据库写入,对算子状态的任何变更,以及对输入消息的任何确认(包括在基于日志的消息代理中将消费者偏移量前移)。 +为了在出现故障时表现出恰好处理一次的样子,我们需要确保事件处理的所有输出和副作用 **当且仅当** 处理成功时才会生效。这些影响包括发送给下游算子或外部消息传递系统(包括电子邮件或推送通知)的任何消息,任何数据库写入,对算子状态的任何变更,以及对输入消息的任何确认(包括在基于日志的消息代理中将消费者偏移量前移)。 -这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅“[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。 +这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅 “[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。 -在[第九章](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 不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。 #### 幂等性 -我们的目标是丢弃任何失败任务的部分输出,以便能安全地重试,而不会生效两次。分布式事务是实现这个目标的一种方式,而另一种方式是依赖**幂等性(idempotence)**【97】。 +我们的目标是丢弃任何失败任务的部分输出,以便能安全地重试,而不会生效两次。分布式事务是实现这个目标的一种方式,而另一种方式是依赖 **幂等性(idempotence)**【97】。 幂等操作是多次重复执行与单次执行效果相同的操作。例如,将键值存储中的某个键设置为某个特定值是幂等的(再次写入该值,只是用同样的值替代),而递增一个计数器不是幂等的(再次执行递增意味着该值递增两次)。 -即使一个操作不是天生幂等的,往往可以通过一些额外的元数据做成幂等的。例如,在使用来自Kafka的消息时,每条消息都有一个持久的、单调递增的偏移量。将值写入外部数据库时可以将这个偏移量带上,这样你就可以判断一条更新是不是已经执行过了,因而避免重复执行。 +即使一个操作不是天生幂等的,往往可以通过一些额外的元数据做成幂等的。例如,在使用来自 Kafka 的消息时,每条消息都有一个持久的、单调递增的偏移量。将值写入外部数据库时可以将这个偏移量带上,这样你就可以判断一条更新是不是已经执行过了,因而避免重复执行。 -Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意味着隐含了一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(基于日志的消息代理能做这些事),处理必须是确定性的,没有其他节点能同时更新相同的值【98,99】。 +Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性意味着隐含了一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(基于日志的消息代理能做这些事),处理必须是确定性的,没有其他节点能同时更新相同的值【98,99】。 -当从一个处理节点故障切换到另一个节点时,可能需要进行**防护**(fencing,请参阅“[领导者和锁](ch8.md#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现**恰好一次语义**的有效方式,仅需很小的额外开销。 +当从一个处理节点故障切换到另一个节点时,可能需要进行 **防护**(fencing,请参阅 “[领导者和锁](ch8.md#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现 **恰好一次语义** 的有效方式,仅需很小的额外开销。 #### 失败后重建状态 任何需要状态的流处理 —— 例如,任何窗口聚合(例如计数器,平均值和直方图)以及任何用于连接的表和索引,都必须确保在失败之后能恢复其状态。 -一种选择是将状态保存在远程数据存储中,并进行复制,然而正如在“[流表连接(流扩充)](#流表连接(流扩充))”中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。 +一种选择是将状态保存在远程数据存储中,并进行复制,然而正如在 “[流表连接(流扩充)](#流表连接(流扩充))” 中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。 -例如,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#真的串行执行)”)。 -在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(请参阅“[日志压缩](#日志压缩)”)。 +在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(请参阅 “[日志压缩](#日志压缩)”)。 然而,所有这些权衡取决于底层基础架构的性能特征:在某些系统中,网络延迟可能低于磁盘访问延迟,网络带宽也可能与磁盘带宽相当。没有针对所有情况的普适理想权衡,随着存储和网络技术的发展,本地状态与远程状态的优点也可能会互换。 ## 本章小结 -在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在[第十章](ch10.md) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。 +在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在 [第十章](ch10.md) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。 我们花了一些时间比较两种消息代理: -* AMQP/JMS风格的消息代理 +* AMQP/JMS 风格的消息代理 - 代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的RPC(另请参阅“[消息传递中的数据流](ch4.md#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。 + 代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的 RPC(另请参阅 “[消息传递中的数据流](ch4.md#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。 * 基于日志的消息代理 代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。 -基于日志的方法与数据库中的复制日志(请参阅[第五章](ch5.md))和日志结构存储引擎(请参阅[第三章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。 +基于日志的方法与数据库中的复制日志(请参阅 [第五章](ch5.md))和日志结构存储引擎(请参阅 [第三章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。 -就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和Feed数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。 +就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和 Feed 数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。 将数据库表示为流为系统集成带来了很多强大机遇。通过消费变更日志并将其应用至衍生系统,你能使诸如搜索索引、缓存以及分析系统这类衍生数据系统不断保持更新。你甚至能从头开始,通过读取从创世至今的所有变更日志,为现有数据创建全新的视图。 @@ -706,7 +706,7 @@ Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意 * 流流连接 - 两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接**,即self-join)。 + 两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户 30 分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接**,即 self-join)。 * 流表连接 diff --git a/ch12.md b/ch12.md index d9bd32f..2d31f05 100644 --- a/ch12.md +++ b/ch12.md @@ -1,168 +1,168 @@ -# 第十二章:数据系统的未来 +# 第十二章:数据系统的未来 ![](img/ch12.png) > 如果船长的终极目标是保护船只,他应该永远待在港口。 > -> —— 圣托马斯·阿奎那《神学大全》(1265-1274) +> —— 圣托马斯・阿奎那《神学大全》(1265-1274) --------------- [TOC] -到目前为止,本书主要描述的是**现状**。在这最后一章中,我们将放眼**未来**,讨论应该是怎么样的:我将提出一些想法与方法,我相信它们能从根本上改进我们设计与构建应用的方式。 +到目前为止,本书主要描述的是 **现状**。在这最后一章中,我们将放眼 **未来**,讨论应该是怎么样的:我将提出一些想法与方法,我相信它们能从根本上改进我们设计与构建应用的方式。 对未来的看法与推测当然具有很大的主观性。所以在撰写本章时,当提及我个人的观点时会使用第一人称。你完全可以不同意这些观点并提出自己的看法,但我希望本章中的概念,至少能成为富有成效的讨论出发点,并澄清一些经常被混淆的概念。 -[第一章](ch1.md)概述了本书的目标:探索如何创建**可靠**,**可伸缩**和**可维护**的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮,正确,可演化,且最终对人类有益。 +[第一章](ch1.md) 概述了本书的目标:探索如何创建 **可靠**、**可伸缩** 和 **可维护** 的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮、正确、可演化、且最终对人类有益。 ## 数据集成 -本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在[第三章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树,以及列式存储。在[第五章](ch5.md)讨论复制时,我们看到了单领导者,多领导者,和无领导者的方法。 +本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在 [第三章](ch3.md) 讨论存储引擎时,我们看到了日志结构存储、B 树以及列式存储。在 [第五章](ch5.md) 讨论复制时,我们看到了单领导者、多领导者和无领导者的方法。 -如果你有一个类似于“我想存储一些数据并稍后再查询”的问题,那么并没有一种正确的解决方案。但对于不同的具体环境,总会有不同的合适方法。软件实现通常必须选择一种特定的方法。使单条代码路径能做到稳定健壮且表现良好已经是一件非常困难的事情了 —— 尝试在单个软件中完成所有事情,几乎可以保证,实现效果会很差。 +如果你有一个类似于 “我想存储一些数据并稍后再查询” 的问题,那么并没有一种正确的解决方案。但对于不同的具体环境,总会有不同的合适方法。软件实现通常必须选择一种特定的方法。使单条代码路径能做到稳定健壮且表现良好已经是一件非常困难的事情了 —— 尝试在单个软件中完成所有事情,几乎可以保证,实现效果会很差。 -因此软件工具的最佳选择也取决于情况。每一种软件,甚至所谓的“通用”数据库,都是针对特定的使用模式设计的。 +因此软件工具的最佳选择也取决于情况。每一种软件,甚至所谓的 “通用” 数据库,都是针对特定的使用模式设计的。 面对让人眼花缭乱的诸多替代品,第一个挑战就是弄清软件与其适用环境的映射关系。供应商不愿告诉你他们软件不适用的工作负载,这是可以理解的。但是希望先前的章节能给你提供一些问题,让你读出字里行间的言外之意,并更好地理解这些权衡。 -但是,即使你已经完全理解各种工具与其适用环境间的关系,还有一个挑战:在复杂的应用中,数据的用法通常花样百出。不太可能存在适用于**所有**不同数据应用场景的软件,因此你不可避免地需要拼凑几个不同的软件来以提供应用所需的功能。 +但是,即使你已经完全理解各种工具与其适用环境间的关系,还有一个挑战:在复杂的应用中,数据的用法通常花样百出。不太可能存在适用于 **所有** 不同数据应用场景的软件,因此你不可避免地需要拼凑几个不同的软件来以提供应用所需的功能。 ### 组合使用衍生数据的工具 -例如,为了处理任意关键词的搜索查询,将OLTP数据库与全文搜索索引集成在一起是很常见的的需求。尽管一些数据库(例如PostgreSQL)包含了全文索引功能,对于简单的应用完全够了【1】,但更复杂的搜索能力就需要专业的信息检索工具了。相反的是,搜索索引通常不适合作为持久的记录系统,因此许多应用需要组合这两种不同的工具以满足所有需求。 +例如,为了处理任意关键词的搜索查询,将 OLTP 数据库与全文搜索索引集成在一起是很常见的的需求。尽管一些数据库(例如 PostgreSQL)包含了全文索引功能,对于简单的应用完全够了【1】,但更复杂的搜索能力就需要专业的信息检索工具了。相反的是,搜索索引通常不适合作为持久的记录系统,因此许多应用需要组合这两种不同的工具以满足所有需求。 -我们在“[保持系统同步](ch11.md#保持系统同步)”中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。 +我们在 “[保持系统同步](ch11.md#保持系统同步)” 中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。 -令人惊讶的是,我经常看到软件工程师做出这样的陈述:“根据我的经验,99%的人只需要X”或者 “......不需要X”(对于各种各样的X)。我认为这种陈述更像是发言人自己的经验,而不是技术实际上的实用性。可能对数据执行的操作,其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角,并考虑跨越整个组织范围的数据流时,数据集成的需求往往就会变得明显起来。 +令人惊讶的是,我经常看到软件工程师做出这样的陈述:“根据我的经验,99% 的人只需要 X” 或者 “...... 不需要 X”(对于各种各样的 X)。我认为这种陈述更像是发言人自己的经验,而不是技术实际上的实用性。可能对数据执行的操作,其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角,并考虑跨越整个组织范围的数据流时,数据集成的需求往往就会变得明显起来。 #### 理解数据流 当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:哪些数据先写入,哪些数据表示衍生自哪些来源?如何以正确的格式,将所有数据导入正确的地方? -例如,你可能会首先将数据写入**记录系统**数据库,捕获对该数据库所做的变更(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”),然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全派生自记录系统,因此与其保持一致(除软件错误外)。写入数据库是向该系统提供新输入的唯一方式。 +例如,你可能会首先将数据写入 **记录系统** 数据库,捕获对该数据库所做的变更(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”),然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全派生自记录系统,因此与其保持一致(除软件错误外)。写入数据库是向该系统提供新输入的唯一方式。 -允许应用程序直接写入搜索索引和数据库引入了如[图11-4](img/fig11-4.png)所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。 +允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](img/fig11-4.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。 -如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。 这是状态机复制方法的一个应用,我们在“[全序广播](ch9.md#全序广播)”中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。 +如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。 这是状态机复制方法的一个应用,我们在 “[全序广播](ch9.md#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。 -基于事件日志来更新衍生数据的系统,通常可以做到**确定性**与**幂等性**(请参阅[幂等性](ch11.md#幂等性)”),使得从故障中恢复相当容易。 +基于事件日志来更新衍生数据的系统,通常可以做到 **确定性** 与 **幂等性**(请参阅 “[幂等性](ch11.md#幂等性)”),使得从故障中恢复相当容易。 #### 衍生数据与分布式事务 -保持不同数据系统彼此一致的经典方法涉及分布式事务,如“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”中所述。与分布式事务相比,使用衍生数据系统的方法如何? +保持不同数据系统彼此一致的经典方法涉及分布式事务,如 “[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)” 中所述。与分布式事务相比,使用衍生数据系统的方法如何? -在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过**锁**进行互斥来决定写入的顺序(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”),而CDC和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于**确定性重试**和**幂等性**。 +在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过 **锁** 进行互斥来决定写入的顺序(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”),而 CDC 和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于 **确定性重试** 和 **幂等性**。 -最大的不同之处在于事务系统通常提供[线性一致性](ch9.md#线性一致性),这包含着有用的保证,例如[读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。 +最大的不同之处在于事务系统通常提供 [线性一致性](ch9.md#线性一致性),这包含着有用的保证,例如 [读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。 -在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为XA的容错能力和性能很差劲(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。 +在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为 XA 的容错能力和性能很差劲(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。 -在没有广泛支持的良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如读己之写的保证是有用的,我认为告诉所有人“最终一致性是不可避免的 —— 忍一忍并学会和它打交道”是没有什么建设性的(至少在缺乏**如何**应对的良好指导时)。 +在没有广泛支持的良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如读己之写的保证是有用的,我认为告诉所有人 “最终一致性是不可避免的 —— 忍一忍并学会和它打交道” 是没有什么建设性的(至少在缺乏 **如何** 应对的良好指导时)。 -在“[将事情做正确](#将事情做正确)”中,我们将讨论一些在异步衍生系统之上实现更强保障的方法,并迈向分布式事务和基于日志的异步系统之间的中间地带。 +在 “[将事情做正确](#将事情做正确)” 中,我们将讨论一些在异步衍生系统之上实现更强保障的方法,并迈向分布式事务和基于日志的异步系统之间的中间地带。 #### 全序的限制 对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单主复制数据库的流行所证明的那样,它正好建立了这样一种日志)。但是,随着系统向更大更复杂的工作负载伸缩,限制开始出现: -* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的**单个领导者节点**。如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅“[分区日志](ch11.md#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。 -* 如果服务器分布在多个**地理位置分散**的数据中心上,例如为了容忍整个数据中心掉线,你通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅“[多主复制](ch5.md#多主复制)“)。这意味着源自两个不同数据中心的事件顺序未定义。 -* 将应用程序部署为微服务时(请参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。 -* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅“[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。 +* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的 **单个领导者节点**。如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅 “[分区日志](ch11.md#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。 +* 如果服务器分布在多个 **地理位置分散** 的数据中心上,例如为了容忍整个数据中心掉线,你通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅 “[多主复制](ch5.md#多主复制)“)。这意味着源自两个不同数据中心的事件顺序未定义。 +* 将应用程序部署为微服务时(请参阅 “[服务中的数据流:REST 与 RPC](ch4.md#服务中的数据流:REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。 +* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅 “[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。 -在形式上,决定事件的全局顺序称为**全序广播**,相当于**共识**(请参阅“[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。 +在形式上,决定事件的全局顺序称为 **全序广播**,相当于 **共识**(请参阅 “[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。 #### 排序事件以捕获因果关系 -在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(请参阅“[顺序与因果关系](ch9.md#顺序与因果关系)”)。 +在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象 ID 的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(请参阅 “[顺序与因果关系](ch9.md#顺序与因果关系)”)。 例如,考虑一个社交网络服务,以及一对曾处于恋爱关系但刚分手的用户。其中一个用户将另一个用户从好友中移除,然后向剩余的好友发送消息,抱怨他们的前任。用户的心思是他们的前任不应该看到这些粗鲁的消息,因为消息是在好友状态解除后发送的。 -但是如果好友关系状态与消息存储在不同的地方,在这样一个系统中,可能会出现**解除好友**事件与**发送消息**事件之间的因果依赖丢失的情况。如果因果依赖关系没有被捕捉到,则发送有关新消息的通知的服务可能会在**解除好友**事件之前处理**发送消息**事件,从而错误地向前任发送通知。 +但是如果好友关系状态与消息存储在不同的地方,在这样一个系统中,可能会出现 **解除好友** 事件与 **发送消息** 事件之间的因果依赖丢失的情况。如果因果依赖关系没有被捕捉到,则发送有关新消息的通知的服务可能会在 **解除好友** 事件之前处理 **发送消息** 事件,从而错误地向前任发送通知。 -在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时序问题有关(请参阅“[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: +在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时序问题有关(请参阅 “[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: -* 逻辑时间戳可以提供无需协调的全局顺序(请参阅“[序列号顺序](ch9.md#序列号顺序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 -* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在“[读也是事件](#读也是事件)”中回到这个想法。 -* 冲突解决算法(请参阅“[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。 +* 逻辑时间戳可以提供无需协调的全局顺序(请参阅 “[序列号顺序](ch9.md#序列号顺序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 +* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在 “[读也是事件](#读也是事件)” 中回到这个想法。 +* 冲突解决算法(请参阅 “[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。 也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全序广播的瓶颈)。 ### 批处理与流处理 -我会说数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。这样做需要消费输入,转换,连接,过滤,聚合,训练模型,评估,以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。 +我会说数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。这样做需要消费输入、转换、连接、过滤、聚合、训练模型、评估、以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。 -批处理和流处理的输出是衍生数据集,例如搜索索引,物化视图,向用户显示的建议,聚合指标等(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和“[流处理的应用](ch11.md#流处理的应用)”)。 +批处理和流处理的输出是衍生数据集,例如搜索索引、物化视图、向用户显示的建议、聚合指标等(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 和 “[流处理的应用](ch11.md#流处理的应用)”)。 -正如我们在[第十章](ch10.md)和[第十一章](ch11.md)中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。 +正如我们在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。 -Spark在批处理引擎上执行流处理,将流分解为**微批次(microbatches)**,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以用另一种类型来模拟,但是性能特征会有所不同:例如,在跳跃或滑动窗口上,微批次可能表现不佳【6】。 +Spark 在批处理引擎上执行流处理,将流分解为 **微批次(microbatches)**,而 Apache Flink 则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以用另一种类型来模拟,但是性能特征会有所不同:例如,在跳跃或滑动窗口上,微批次可能表现不佳【6】。 #### 维护衍生状态 -批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅“[失败后重建状态”](ch11.md#失败后重建状态))。 +批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅 “[失败后重建状态”](ch11.md#失败后重建状态))。 -具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅“[幂等性](ch11.md#幂等性)”),也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引、统计模型还是缓存,采用这种观点思考都是很有帮助的:将其视为从一个东西衍生出另一个的数据管道,通过函数式应用代码推送一个系统的状态变更,并将其效果应用至衍生系统中。 +具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅 “[幂等性](ch11.md#幂等性)”),也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引、统计模型还是缓存,采用这种观点思考都是很有帮助的:将其视为从一个东西衍生出另一个的数据管道,通过函数式应用代码推送一个系统的状态变更,并将其效果应用至衍生系统中。 -原则上,衍生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。 +原则上,衍生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅 “[分布式事务的限制](ch9.md#分布式事务的限制)”)。 -我们在“[分区与次级索引](ch6.md#分区与次级索引)”中看到,次级索引经常跨越分区边界。具有次级索引的分区系统需要将写入发送到多个分区(如果索引按关键词分区的话)或将读取发送到所有分区(如果索引是按文档分区的话)。如果索引是异步维护的,这种跨分区通信也是最可靠和最可伸缩的【8】(另请参阅“[多分区数据处理](多分区数据处理)”)。 +我们在 “[分区与次级索引](ch6.md#分区与次级索引)” 中看到,次级索引经常跨越分区边界。具有次级索引的分区系统需要将写入发送到多个分区(如果索引按关键词分区的话)或将读取发送到所有分区(如果索引是按文档分区的话)。如果索引是异步维护的,这种跨分区通信也是最可靠和最可伸缩的【8】(另请参阅 “[多分区数据处理](#多分区数据处理)”)。 #### 应用演化后重新处理数据 在维护衍生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在衍生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。 -特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅[第四章](ch4.md))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。 +特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅 [第四章](ch4.md))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。 > ### 铁路上的模式迁移 > -> 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的竞争标准。为一种轨距而建的列车不能在另一种轨距的轨道上运行,这限制了火车网络中可能的相互连接【9】。 +> 大规模的 “模式迁移” 也发生在非计算机系统中。例如,在 19 世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的竞争标准。为一种轨距而建的列车不能在另一种轨距的轨道上运行,这限制了火车网络中可能的相互连接【9】。 > -> 在1846年最终确定了一个标准轨距之后,其他轨距的轨道必须转换 —— 但是如何在不停运火车线路的情况下进行数月甚至数年的迁移?解决的办法是首先通过添加第三条轨道将轨道转换为**双轨距(dual guage)** 或**混合轨距**。这种转换可以逐渐完成,当完成时,两种轨距的列车都可以在线路上跑,使用三条轨道中的两条。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 +> 在 1846 年最终确定了一个标准轨距之后,其他轨距的轨道必须转换 —— 但是如何在不停运火车线路的情况下进行数月甚至数年的迁移?解决的办法是首先通过添加第三条轨道将轨道转换为 **双轨距(dual guage)** 或 **混合轨距**。这种转换可以逐渐完成,当完成时,两种轨距的列车都可以在线路上跑,使用三条轨道中的两条。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 > -> 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的BART系统使用了与美国大部分地区不同的轨距。 +> 以这种方式 “再加工” 现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的 BART 系统使用了与美国大部分地区不同的轨距。 -衍生视图允许**渐进演化(gradual evolution)**。如果你想重新构建数据集,不需要执行突然切换式的迁移。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例,最终可以删除旧视图【10】。 +衍生视图允许 **渐进演化(gradual evolution)**。如果你想重新构建数据集,不需要执行突然切换式的迁移。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例,最终可以删除旧视图【10】。 这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:你始终有一个可以回滚的可用系统。通过降低不可逆损害的风险,你能对继续前进更有信心,从而更快地改善系统【11】。 #### Lambda架构 -如果批处理用于重新处理历史数据,而流处理用于处理最近的更新,那么如何将这两者结合起来?Lambda架构【12】是这方面的一个建议,引起了很多关注。 +如果批处理用于重新处理历史数据,而流处理用于处理最近的更新,那么如何将这两者结合起来?Lambda 架构【12】是这方面的一个建议,引起了很多关注。 -Lambda架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅“[事件溯源](ch11.md#事件溯源)”)。为了从这些事件中衍生出读取优化的视图, Lambda架构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 +Lambda 架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅 “[事件溯源](ch11.md#事件溯源)”)。为了从这些事件中衍生出读取优化的视图,Lambda 架构建议并行运行两个不同的系统:批处理系统(如 Hadoop MapReduce)和独立的流处理系统(如 Storm)。 -在Lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[容错](ch11.md#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 +在 Lambda 方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅 “[容错](ch11.md#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 -Lambda架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立衍生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题: +Lambda 架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立衍生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题: -* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。 +* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像 Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。 * 由于流管道和批处理管道产生独立的输出,因此需要合并它们以响应用户请求。如果计算是基于滚动窗口的简单聚合,则合并相当容易,但如果视图基于更复杂的操作(例如连接和会话化)而导出,或者输出不是时间序列,则会变得非常困难。 -* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了“[时间推理](ch11.md#时间推理)”中讨论的问题,例如处理滞留事件和处理跨批次边界的窗口。增量化批处理计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 +* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了 “[时间推理](ch11.md#时间推理)” 中讨论的问题,例如处理滞留事件和处理跨批次边界的窗口。增量化批处理计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 #### 统一批处理和流处理 -最近的工作使得Lambda架构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(在事件到达时即处理)在同一个系统中实现【15】。 +最近的工作使得 Lambda 架构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(在事件到达时即处理)在同一个系统中实现【15】。 在一个系统中统一批处理和流处理需要以下功能,这些功能也正在越来越广泛地被提供: -* 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息(请参阅“[重播旧消息](ch11.md#重播旧消息)”),某些流处理器可以从HDFS等分布式文件系统读取输入。 -* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅“[容错](ch11.md#容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。 -* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(请参阅“[时间推理](ch11.md#时间推理)”)。例如,Apache Beam提供了用于表达这种计算的API,可以在Apache Flink或Google Cloud Dataflow使用。 +* 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息(请参阅 “[重播旧消息](ch11.md#重播旧消息)”),某些流处理器可以从 HDFS 等分布式文件系统读取输入。 +* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅 “[容错](ch11.md#容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。 +* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(请参阅 “[时间推理](ch11.md#时间推理)”)。例如,Apache Beam 提供了用于表达这种计算的 API,可以在 Apache Flink 或 Google Cloud Dataflow 使用。 ## 分拆数据库 -在最抽象的层面上,数据库,Hadoop和操作系统都发挥相同的功能:它们存储一些数据,并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录(表中的行、文档、图中的顶点等),而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是“信息管理”系统【17】。正如我们在[第十章](ch10.md)中看到的,Hadoop生态系统有点像Unix的分布式版本。 +在最抽象的层面上,数据库,Hadoop 和操作系统都发挥相同的功能:它们存储一些数据,并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录(表中的行、文档、图中的顶点等),而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是 “信息管理” 系统【17】。正如我们在 [第十章](ch10.md) 中看到的,Hadoop 生态系统有点像 Unix 的分布式版本。 -当然,有很多实际的差异。例如,许多文件系统都不能很好地处理包含1000万个小文件的目录,而包含1000万个小记录的数据库完全是寻常而不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。 +当然,有很多实际的差异。例如,许多文件系统都不能很好地处理包含 1000 万个小文件的目录,而包含 1000 万个小记录的数据库完全是寻常而不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。 -Unix和关系数据库以非常不同的哲学来处理信息管理问题。Unix认为它的目的是为程序员提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望为应用程序员提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性,并发性,崩溃恢复等等。Unix发展出的管道和文件只是字节序列,而数据库则发展出了SQL和事务。 +Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Unix 认为它的目的是为程序员提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望为应用程序员提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性,并发性,崩溃恢复等等。Unix 发展出的管道和文件只是字节序列,而数据库则发展出了 SQL 和事务。 -哪种方法更好?当然这取决于你想要的是什么。 Unix是“简单的”,因为它是对硬件资源相当薄的包装;关系数据库是“更简单”的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化,索引,连接方法,并发控制,复制等),而不需要查询的作者理解其实现细节。 +哪种方法更好?当然这取决于你想要的是什么。 Unix 是 “简单的”,因为它是对硬件资源相当薄的包装;关系数据库是 “更简单” 的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化、索引、连接方法、并发控制、复制等),而不需要查询的作者理解其实现细节。 -这些哲学之间的矛盾已经持续了几十年(Unix和关系模型都出现在70年代初),仍然没有解决。例如,我将NoSQL运动解释为,希望将类Unix的低级别抽象方法应用于分布式OLTP数据存储的领域。 +这些哲学之间的矛盾已经持续了几十年(Unix 和关系模型都出现在 70 年代初),仍然没有解决。例如,我将 NoSQL 运动解释为,希望将类 Unix 的低级别抽象方法应用于分布式 OLTP 数据存储的领域。 在这一部分我将试图调和这两个哲学,希望我们能各取其美。 @@ -170,83 +170,83 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。Unix 在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括: -* 次级索引,使你可以根据字段的值有效地搜索记录(请参阅“[其他索引结构](ch3.md#其他索引结构)”) -* 物化视图,这是一种预计算的查询结果缓存(请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”) -* 复制日志,保持其他节点上数据的副本最新(请参阅“[复制日志的实现](ch5.md#复制日志的实现)”) -* 全文搜索索引,允许在文本中进行关键字搜索(请参阅“[全文搜索和模糊索引](ch3.md#全文搜4索和模糊索引)”),也内置于某些关系数据库【1】 +* 次级索引,使你可以根据字段的值有效地搜索记录(请参阅 “[其他索引结构](ch3.md#其他索引结构)”) +* 物化视图,这是一种预计算的查询结果缓存(请参阅 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”) +* 复制日志,保持其他节点上数据的副本最新(请参阅 “[复制日志的实现](ch5.md#复制日志的实现)”) +* 全文搜索索引,允许在文本中进行关键字搜索(请参阅 “[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”),也内置于某些关系数据库【1】 -在[第十章](ch10.md)和[第十一章](ch11.md)中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”),了解了如何维护物化视图(请参阅“[维护物化视图](ch11.md#维护物化视图)”)以及如何将变更从数据库复制到衍生数据系统(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”)。 +在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)”),了解了如何维护物化视图(请参阅 “[维护物化视图](ch11.md#维护物化视图)”)以及如何将变更从数据库复制到衍生数据系统(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”)。 数据库中内置的功能与人们用批处理和流处理器构建的衍生数据系统似乎有相似之处。 #### 创建索引 -想想当你运行`CREATE INDEX`在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。 +想想当你运行 `CREATE INDEX` 在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。 -此过程非常类似于设置新的从库副本(请参阅“[设置新从库](ch5.md#设置新从库)”),也非常类似于流处理系统中的**引导(bootstrap)** 变更数据捕获(请参阅“[初始快照](ch11.md#初始快照)”)。 +此过程非常类似于设置新的从库副本(请参阅 “[设置新从库](ch5.md#设置新从库)”),也非常类似于流处理系统中的 **引导(bootstrap)** 变更数据捕获(请参阅 “[初始快照](ch11.md#初始快照)”)。 -无论何时运行`CREATE INDEX`,数据库都会重新处理现有数据集(如“[应用演化后重新处理数据](#应用演化后重新处理数据)”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“[状态、流和不变性](ch11.md#状态、流和不变性)”)。 +无论何时运行 `CREATE INDEX`,数据库都会重新处理现有数据集(如 “[应用演化后重新处理数据](#应用演化后重新处理数据)” 中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅 “[状态、流和不变性](ch11.md#状态、流和不变性)”)。 #### 一切的元数据库 -有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理、流或ETL过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。 +有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理、流或 ETL 过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。 -从这种角度来看,批处理和流处理器就像精心实现的触发器、存储过程和物化视图维护例程。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引、散列索引、空间索引(请参阅“[多列索引](ch3.md#多列索引)”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 +从这种角度来看,批处理和流处理器就像精心实现的触发器、存储过程和物化视图维护例程。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持 B 树索引、散列索引、空间索引(请参阅 “[多列索引](ch3.md#多列索引)”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 这些发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统: **联合数据库:统一读取** -可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库(federated database)** 或**多态存储(polystore)** 的方法【18,19】。例如,PostgreSQL的**外部数据包装器(foreign data wrapper)** 功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 +可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为 **联合数据库(federated database)** 或 **多态存储(polystore)** 的方法【18,19】。例如,PostgreSQL 的 **外部数据包装器(foreign data wrapper)** 功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 联合查询接口遵循着单一集成系统的关系型传统,带有高级查询语言和优雅的语义,但实现起来非常复杂。 **分拆数据库:统一写入** -虽然联合能解决跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统**同步**写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据变更都会在所有正确的位置结束,即使在出现故障时也是如此。想要更容易地将存储系统可靠地插接在一起(例如,通过变更数据捕获和事件日志),就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。 +虽然联合能解决跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统 **同步** 写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据变更都会在所有正确的位置结束,即使在出现故障时也是如此。想要更容易地将存储系统可靠地插接在一起(例如,通过变更数据捕获和事件日志),就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。 -分拆方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低层级API(管道)进行通信,并且可以使用更高层级的语言进行组合(shell)【16】 。 +分拆方法遵循 Unix 传统的小型工具,它可以很好地完成一件事【22】,通过统一的低层级 API(管道)进行通信,并且可以使用更高层级的语言进行组合(shell)【16】 。 #### 开展分拆工作 联合和分拆是一个硬币的两面:用不同的组件构成可靠、 可伸缩和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可解决的问题。而我认为同步写入到几个存储系统是更困难的工程问题,所以我将重点关注它。 -传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“[衍生数据与分布式事务](#衍生数据与分布式事务)”)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 +传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅 “[衍生数据与分布式事务](#衍生数据与分布式事务)”)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 -例如,分布式事务在某些流处理组件内部使用,以匹配**恰好一次(exactly-once)** 语义(请参阅“[原子提交再现](ch11.md#原子提交再现)”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的有序事件日志(请参阅“[幂等性](ch11.md#幂等性)”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 +例如,分布式事务在某些流处理组件内部使用,以匹配 **恰好一次(exactly-once)** 语义(请参阅 “[原子提交再现](ch11.md#原子提交再现)”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的有序事件日志(请参阅 “[幂等性](ch11.md#幂等性)”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 -基于日志的集成的一大优势是各个组件之间的**松散耦合(loose coupling)**,这体现在两个方面: +基于日志的集成的一大优势是各个组件之间的 **松散耦合(loose coupling)**,这体现在两个方面: -1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。如果消费者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅“[磁盘空间使用](ch11.md#磁盘空间使用)”),以便生产者和任何其他消费者可以继续不受影响地运行。有问题的消费者可以在问题修复后赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。 +1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。如果消费者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅 “[磁盘空间使用](ch11.md#磁盘空间使用)”),以便生产者和任何其他消费者可以继续不受影响地运行。有问题的消费者可以在问题修复后赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参阅 “[分布式事务的限制](ch9.md#分布式事务的限制)”)。 2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。专业化使得每个团队都可以专注于做好一件事,并与其他团队的系统以明确的接口交互。事件日志提供了一个足够强大的接口,以捕获相当强的一致性属性(由于持久性和事件的顺序),但也足够普适于几乎任何类型的数据。 #### 分拆系统vs集成系统 -如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”与“[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)” 。 +如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 与 “[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP 数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)” 。 运行几种不同基础设施的复杂性可能是一个问题:每种软件都有一个学习曲线,配置问题和操作怪癖,因此部署尽可能少的移动部件是很有必要的。比起使用应用代码拼接多个工具而成的系统,单一集成软件产品也可以在其设计应对的工作负载类型上实现更好、更可预测的性能【23】。正如在前言中所说的那样,为了不需要的规模而构建系统是白费精力,而且可能会将你锁死在一个不灵活的设计中。实际上,这是一种过早优化的形式。 -分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许你结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”中讨论的存储和处理模型的多样性一样。 +分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许你结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)” 中讨论的存储和处理模型的多样性一样。 因此,如果有一项技术可以满足你的所有需求,那么最好使用该产品,而不是试图用更低层级的组件重新实现它。只有当没有单一软件满足你的所有需求时,才会出现拆分和联合的优势。 #### 少了什么? -用于组成数据系统的工具正在变得越来越好,但我认为还缺少一个主要的东西:我们还没有与Unix shell类似的分拆数据库等价物(即,一种声明式的、简单的、用于组装存储和处理系统的高级语言)。 +用于组成数据系统的工具正在变得越来越好,但我认为还缺少一个主要的东西:我们还没有与 Unix shell 类似的分拆数据库等价物(即,一种声明式的、简单的、用于组装存储和处理系统的高级语言)。 -例如,如果我们可以简单地声明`mysql | elasticsearch`,类似于Unix管道【22】,成为`CREATE INDEX`的分拆等价物:它将读取MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有变更,并自动将它们应用于搜索索引,而无需编写自定义应用代码。这种集成应当支持几乎任何类型的存储或索引系统。 +例如,如果我们可以简单地声明 `mysql | elasticsearch`,类似于 Unix 管道【22】,成为 `CREATE INDEX` 的分拆等价物:它将读取 MySQL 数据库中的所有文档并将其索引到 Elasticsearch 集群中。然后它会不断捕获对数据库所做的所有变更,并自动将它们应用于搜索索引,而无需编写自定义应用代码。这种集成应当支持几乎任何类型的存储或索引系统。 -同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以你可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅“[图数据模型](ch2.md#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如**差分数据流(differential dataflow)**【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 +同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以你可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅 “[图数据模型](ch2.md#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如 **差分数据流(differential dataflow)**【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 ### 围绕数据流设计应用 -使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为“**数据库由内而外(database inside-out)**”方法【26】,该名称来源于我在2014年的一次会议演讲标题【27】。然而称它为“新架构”过于夸大,我仅将其看作是一种设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们能更好地讨论它。 +使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为 “**数据库由内而外(database inside-out)**” 方法【26】,该名称来源于我在 2014 年的一次会议演讲标题【27】。然而称它为 “新架构” 过于夸大,我仅将其看作是一种设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们能更好地讨论它。 -这些想法不是我的;它们是很多人的思想的融合,这些思想非常值得我们学习。尤其是,以Oz【28】和Juttle【29】为代表的数据流语言,以Elm【30,31】为代表的**函数式响应式编程(functional reactive programming, FRP)**,以Bloom【32】为代表的逻辑编程语言。在这一语境中的术语**分拆(unbundling)** 是由Jay Kreps 提出的【7】。 +这些想法不是我的;它们是很多人的思想的融合,这些思想非常值得我们学习。尤其是,以 Oz【28】和 Juttle【29】为代表的数据流语言,以 Elm【30,31】为代表的 **函数式响应式编程(functional reactive programming, FRP)**,以 Bloom【32】为代表的逻辑编程语言。在这一语境中的术语 **分拆(unbundling)** 是由 Jay Kreps 提出的【7】。 -即使是**电子表格**也在数据流编程能力上甩开大多数主流编程语言几条街【33】。在电子表格中,可以将公式放入一个单元格中(例如,对另一列中的单元格求和),并且只要公式的任何输入发生变更,公式的结果都会自动重新计算。这正是我们在数据系统层次所需要的:当数据库中的记录发生变更时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。你不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 +即使是 **电子表格** 也在数据流编程能力上甩开大多数主流编程语言几条街【33】。在电子表格中,可以将公式放入一个单元格中(例如,对另一列中的单元格求和),并且只要公式的任何输入发生变更,公式的结果都会自动重新计算。这正是我们在数据系统层次所需要的:当数据库中的记录发生变更时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。你不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 -因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可伸缩性以及持久存储数据。它们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某一种特定的语言、框架或工具来开发所有软件是不切实际的。 +因此,我认为绝大多数数据系统仍然可以从 VisiCalc 在 1979 年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可伸缩性以及持久存储数据。它们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某一种特定的语言、框架或工具来开发所有软件是不切实际的。 在本节中,我将详细介绍这些想法,并探讨一些围绕分拆数据库和数据流的想法构建应用的方法。 @@ -254,171 +254,171 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。Unix 当一个数据集衍生自另一个数据集时,它会经历某种转换函数。例如: -* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用B树或SSTable索引,按键排序,如[第三章](ch3.md)所述)。 +* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用 B 树或 SSTable 索引,按键排序,如 [第三章](ch3.md) 所述)。 * 全文搜索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测、分词、词干或词汇化、拼写纠正和同义词识别,然后构建用于高效查找的数据结构(例如倒排索引)。 * 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数衍生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中衍生的。 -* 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段;UI中的变更可能需要更新缓存填充方式的定义,并重建缓存。 +* 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道 UI 中引用的字段;UI 中的变更可能需要更新缓存填充方式的定义,并重建缓存。 -用于次级索引的衍生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过`CREATE INDEX`来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要领域特定的调整。在机器学习中,特征工程是众所周知的特定于应用的特征,通常需要包含很多关于用户交互与应用部署的详细知识【35】。 +用于次级索引的衍生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过 `CREATE INDEX` 来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要领域特定的调整。在机器学习中,特征工程是众所周知的特定于应用的特征,通常需要包含很多关于用户交互与应用部署的详细知识【35】。 -当创建衍生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅“[传递事件流](ch11.md#传递事件流)”)。 +当创建衍生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅 “[传递事件流](ch11.md#传递事件流)”)。 #### 应用代码和状态的分离 理论上,数据库可以是任意应用代码的部署环境,就如同操作系统一样。然而实践中它们对这一目标适配的很差。它们不满足现代应用开发的要求,例如依赖和软件包管理、版本控制、滚动升级、可演化性、监控、指标、对网络服务的调用以及与外部系统的集成。 -另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。 +另一方面,Mesos、YARN、Docker、Kubernetes 等部署和集群管理工具专为运行应用代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。 我认为让系统的某些部分专门用于持久数据存储并让其他部分专门运行应用程序代码是有意义的。这两者可以在保持独立的同时互动。 -现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如函数式编程社区喜欢开玩笑说的那样,“我们相信**教会(Church)** 与**国家(state)** 的分离”【37】 [^i] +现在大多数 Web 应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如函数式编程社区喜欢开玩笑说的那样,“我们相信 **教会(Church)** 与 **国家(state)** 的分离”【37】 [^i] -[^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。 在这里,Church指代的是数学家的阿隆佐·邱奇,他创立了lambda演算,这是计算的早期形式,是大多数函数式编程语言的基础。 lambda演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与Church的工作是分离的。 +[^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。 在这里,Church 指代的是数学家的阿隆佐・邱奇,他创立了 lambda 演算,这是计算的早期形式,是大多数函数式编程语言的基础。 lambda 演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与 Church 的工作是分离的。 -在这个典型的Web应用模型中,数据库充当一种可以通过网络同步访问的可变共享变量。应用程序可以读取和更新变量,而数据库负责维持它的持久性,提供一些诸如并发控制和容错的功能。 +在这个典型的 Web 应用模型中,数据库充当一种可以通过网络同步访问的可变共享变量。应用程序可以读取和更新变量,而数据库负责维持它的持久性,提供一些诸如并发控制和容错的功能。 -但是,在大多数编程语言中,你无法订阅可变变量中的变更 —— 你只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知(你可以在自己的代码中实现这样的通知 —— 这被称为**观察者模式** —— 但大多数语言没有将这种模式作为内置功能)。 +但是,在大多数编程语言中,你无法订阅可变变量中的变更 —— 你只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知(你可以在自己的代码中实现这样的通知 —— 这被称为 **观察者模式** —— 但大多数语言没有将这种模式作为内置功能)。 -数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅变更只是刚刚开始出现的功能(请参阅“[变更流的API支持](ch11.md#变更流的API支持)”)。 +数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅变更只是刚刚开始出现的功能(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)”)。 #### 数据流:应用代码与状态变化的交互 从数据流的角度思考应用程序,意味着重新协调应用代码和状态管理之间的关系。我们不再将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。 -我们在“[数据库与流](ch11.md#数据库与流)”中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如Actor的消息传递系统(请参阅“[消息传递中的数据流 ](ch4.md#消息传递中的数据流)”)也具有响应事件的概念。早在20世纪80年代,**元组空间(tuple space)** 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应的过程【38,39】。 +我们在 “[数据库与流](ch11.md#数据库与流)” 中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如 Actor 的消息传递系统(请参阅 “[消息传递中的数据流](ch4.md#消息传递中的数据流)”)也具有响应事件的概念。早在 20 世纪 80 年代,**元组空间(tuple space)** 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应的过程【38,39】。 如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存、全文搜索索引、机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 -需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅“[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”): +需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅 “[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”): -* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如“[确认与重新传递](ch11.md#确认与重新传递)”中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅“[保持系统同步](ch11.md#保持系统同步)”)。 -* 容错是衍生数据的关键:仅仅丢失单个消息就会导致衍生数据集永远与其数据源失去同步。消息传递和衍生状态更新都必须可靠。例如,许多Actor系统默认在内存中维护Actor的状态和消息,所以如果运行Actor的机器崩溃,状态和消息就会丢失。 +* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如 “[确认与重新传递](ch11.md#确认与重新传递)” 中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅 “[保持系统同步](ch11.md#保持系统同步)”)。 +* 容错是衍生数据的关键:仅仅丢失单个消息就会导致衍生数据集永远与其数据源失去同步。消息传递和衍生状态更新都必须可靠。例如,许多 Actor 系统默认在内存中维护 Actor 的状态和消息,所以如果运行 Actor 的机器崩溃,状态和消息就会丢失。 稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。 -这些应用代码可以执行任意处理,包括数据库内置衍生函数通常不提供的功能。就像通过管道链接的Unix工具一样,流算子可以围绕着数据流构建大型系统。每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。 +这些应用代码可以执行任意处理,包括数据库内置衍生函数通常不提供的功能。就像通过管道链接的 Unix 工具一样,流算子可以围绕着数据流构建大型系统。每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。 #### 流处理器和服务 -当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的**服务**(service,请参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。 +当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如 REST API)进行通信的 **服务**(service,请参阅 “[服务中的数据流:REST 与 RPC](ch4.md#服务中的数据流:REST与RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。 -在数据流中组装流算子与微服务方法有很多相似之处【40】。但底层通信机制是有很大区别:数据流采用单向异步消息流,而不是同步的请求/响应式交互。 +在数据流中组装流算子与微服务方法有很多相似之处【40】。但底层通信机制是有很大区别:数据流采用单向异步消息流,而不是同步的请求 / 响应式交互。 -除了在“[消息传递中的数据流](ch4.md#消息传递中的数据流)”中列出的优点(如更好的容错性),数据流系统还能实现更好的性能。例如,假设客户正在购买以一种货币定价,但以另一种货币支付的商品。为了执行货币换算,你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: +除了在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中列出的优点(如更好的容错性),数据流系统还能实现更好的性能。例如,假设客户正在购买以一种货币定价,但以另一种货币支付的商品。为了执行货币换算,你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: 1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库,以获取特定货币的当前汇率。 2. 在数据流方法中,处理订单的代码会提前订阅汇率变更流,并在汇率发生变动时将当前汇率存储在本地数据库中。处理订单时只需查询本地数据库即可。 -第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅“[流表连接(流扩充)](ch11.md#流表连接(流扩充))”)。 +第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用 RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅 “[流表连接(流扩充)](ch11.md#流表连接(流扩充))”)。 [^ii]: 在微服务方法中,你也可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是为了保证缓存的新鲜度,你需要定期轮询汇率以获取其更新,或订阅变更流 —— 这恰好是数据流方法中发生的事情。 -连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅“[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。 +连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅 “[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。 订阅变更流,而不是在需要时查询当前状态,使我们更接近类似电子表格的计算模型:当某些数据发生变更时,依赖于此的所有衍生数据都可以快速更新。还有很多未解决的问题,例如关于时间相关连接等问题,但我认为围绕数据流构建应用的想法是一个非常有希望的方向。 ### 观察衍生数据状态 -在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为**写路径(write path)**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图12-1](img/fig12-1.png)显示了一个更新搜索索引的例子。 +在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为 **写路径(write path)**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图 12-1](img/fig12-1.png) 显示了一个更新搜索索引的例子。 ![](img/fig12-1.png) -**图12-1 在搜索索引中,写(文档更新)遇上读(查询)** +**图 12-1 在搜索索引中,写(文档更新)遇上读(查询)** -但你为什么一开始就要创建衍生数据集?很可能是因为你想在以后再次查询它。这就是**读路径(read path)**:当服务用户请求时,你需要从衍生数据集中读取,也许还要对结果进行一些额外处理,然后构建给用户的响应。 +但你为什么一开始就要创建衍生数据集?很可能是因为你想在以后再次查询它。这就是 **读路径(read path)**:当服务用户请求时,你需要从衍生数据集中读取,也许还要对结果进行一些额外处理,然后构建给用户的响应。 总而言之,写路径和读路径涵盖了数据的整个旅程,从收集数据开始,到使用数据结束(可能是由另一个人)。写路径是预计算过程的一部分 —— 即,一旦数据进入,即刻完成,无论是否有人需要看它。读路径是这个过程中只有当有人请求时才会发生的部分。如果你熟悉函数式编程语言,则可能会注意到写路径类似于立即求值,读路径类似于惰性求值。 -如[图12-1](img/fig12-1.png)所示,衍生数据集是写路径和读路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。 +如 [图 12-1](img/fig12-1.png) 所示,衍生数据集是写路径和读路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。 #### 物化视图和缓存 -全文搜索索引就是一个很好的例子:写路径更新索引,读路径在索引中搜索关键字。读写都需要做一些工作。写入需要更新文档中出现的所有关键词的索引条目。读取需要搜索查询中的每个单词,并应用布尔逻辑来查找包含查询中所有单词(AND运算符)的文档,或者每个单词(OR运算符)的任何同义词。 +全文搜索索引就是一个很好的例子:写路径更新索引,读路径在索引中搜索关键字。读写都需要做一些工作。写入需要更新文档中出现的所有关键词的索引条目。读取需要搜索查询中的每个单词,并应用布尔逻辑来查找包含查询中所有单词(AND 运算符)的文档,或者每个单词(OR 运算符)的任何同义词。 -如果没有索引,搜索查询将不得不扫描所有文档(如grep),如果有着大量文档,这样做的开销巨大。没有索引意味着写入路径上的工作量较少(没有要更新的索引),但是在读取路径上需要更多工作。 +如果没有索引,搜索查询将不得不扫描所有文档(如 grep),如果有着大量文档,这样做的开销巨大。没有索引意味着写入路径上的工作量较少(没有要更新的索引),但是在读取路径上需要更多工作。 -另一方面,可以想象为所有可能的查询预先计算搜索结果。在这种情况下,读路径上的工作量会减少:不需要布尔逻辑,只需查找查询结果并返回即可。但写路径会更加昂贵:可能的搜索查询集合是无限大的,因此预先计算所有可能的搜索结果将需要无限的时间和存储空间。那肯定没戏[^iii]。 +另一方面,可以想象为所有可能的查询预先计算搜索结果。在这种情况下,读路径上的工作量会减少:不需要布尔逻辑,只需查找查询结果并返回即可。但写路径会更加昂贵:可能的搜索查询集合是无限大的,因此预先计算所有可能的搜索结果将需要无限的时间和存储空间。那肯定没戏 [^iii]。 [^iii]: 假设一个有限的语料库,那么返回非空搜索结果的搜索查询集合是有限的。然而,它是与语料库中的术语数量呈指数关系,这仍是一个坏消息。 -另一种选择是预先计算一组固定的最常见查询的搜索结果,以便可以快速提供它们而无需转到索引。不常见的查询仍然可以通过索引来提供服务。这通常被称为常见查询的**缓存(cache)**,尽管我们也可以称之为**物化视图(materialized view)**,因为当新文档出现,且需要被包含在这些常见查询的搜索结果之中时,这些索引就需要更新。 +另一种选择是预先计算一组固定的最常见查询的搜索结果,以便可以快速提供它们而无需转到索引。不常见的查询仍然可以通过索引来提供服务。这通常被称为常见查询的 **缓存(cache)**,尽管我们也可以称之为 **物化视图(materialized view)**,因为当新文档出现,且需要被包含在这些常见查询的搜索结果之中时,这些索引就需要更新。 -从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界;缓存常见搜索结果也是可行的;而在少量文档上使用没有索引的类grep扫描也是可行的。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读路径上的工作量。 +从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界;缓存常见搜索结果也是可行的;而在少量文档上使用没有索引的类 grep 扫描也是可行的。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读路径上的工作量。 -在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在“[描述负载](ch1.md#描述负载)”中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名人的写路径和读路径可能有所不同。在500页之后,我们已经绕回了起点! +在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在 “[描述负载](ch1.md#描述负载)” 中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名人的写路径和读路径可能有所不同。在 500 页之后,我们已经绕回了起点! #### 有状态、可离线的客户端 我发现写路径和读路径之间的边界很有趣,因为我们可以试着改变这个边界,并探讨这种改变的实际意义。我们来看看不同上下文中的这一想法。 -过去二十年来,Web应用的火热让我们对应用开发作出了一些很容易视作理所当然的假设。具体来说就是,客户端/服务器模型 —— 客户端大多是无状态的,而服务器拥有数据的权威 —— 已经普遍到我们几乎忘掉了还有其他任何模型的存在。但是技术在不断地发展,我认为不时地质疑现状非常重要。 +过去二十年来,Web 应用的火热让我们对应用开发作出了一些很容易视作理所当然的假设。具体来说就是,客户端 / 服务器模型 —— 客户端大多是无状态的,而服务器拥有数据的权威 —— 已经普遍到我们几乎忘掉了还有其他任何模型的存在。但是技术在不断地发展,我认为不时地质疑现状非常重要。 -传统上,网络浏览器是无状态的客户端,只有当连接到互联网时才能做一些有用的事情(能离线执行的唯一事情基本上就是上下滚动之前在线时加载好的页面)。然而,最近的“单页面”JavaScript Web应用已经获得了很多有状态的功能,包括客户端用户界面交互,以及Web浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,而且大多数用户交互都不需要与服务器往返交互。 +传统上,网络浏览器是无状态的客户端,只有当连接到互联网时才能做一些有用的事情(能离线执行的唯一事情基本上就是上下滚动之前在线时加载好的页面)。然而,最近的 “单页面” JavaScript Web 应用已经获得了很多有状态的功能,包括客户端用户界面交互,以及 Web 浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,而且大多数用户交互都不需要与服务器往返交互。 -这些不断变化的功能重新引发了对**离线优先(offline-first)** 应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(请参阅“[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。 +这些不断变化的功能重新引发了对 **离线优先(offline-first)** 应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(请参阅 “[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。 -当我们摆脱无状态客户端与中央数据库交互的假设,并转向在终端用户设备上维护状态时,这就开启了新世界的大门。特别是,我们可以将设备上的状态视为**服务器状态的缓存**。屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。 +当我们摆脱无状态客户端与中央数据库交互的假设,并转向在终端用户设备上维护状态时,这就开启了新世界的大门。特别是,我们可以将设备上的状态视为 **服务器状态的缓存**。屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。 #### 将状态变更推送给客户端 -在典型的网页中,如果你在Web浏览器中加载页面,并且随后服务器上的数据发生变更,则浏览器在重新加载页面之前对此一无所知。浏览器只能在一个时间点读取数据,假设它是静态的 —— 它不会订阅来自服务器的更新。因此设备上的状态是陈旧的缓存,除非你显式轮询变更否则不会更新。(像RSS这样基于HTTP的Feed订阅协议实际上只是一种基本的轮询形式) +在典型的网页中,如果你在 Web 浏览器中加载页面,并且随后服务器上的数据发生变更,则浏览器在重新加载页面之前对此一无所知。浏览器只能在一个时间点读取数据,假设它是静态的 —— 它不会订阅来自服务器的更新。因此设备上的状态是陈旧的缓存,除非你显式轮询变更否则不会更新。(像 RSS 这样基于 HTTP 的 Feed 订阅协议实际上只是一种基本的轮询形式) -最近的协议已经超越了HTTP的基本请求/响应模式:服务端发送的事件(EventSource API)和WebSockets提供了通信信道,通过这些信道,Web浏览器可以与服务器保持打开的TCP连接,只要浏览器仍然连接着,服务器就能主动向浏览器推送信息。这为服务器提供了主动通知终端用户客户端的机会,服务器能告知客户端其本地存储状态的任何变化,从而减少客户端状态的陈旧程度。 +最近的协议已经超越了 HTTP 的基本请求 / 响应模式:服务端发送的事件(EventSource API)和 WebSockets 提供了通信信道,通过这些信道,Web 浏览器可以与服务器保持打开的 TCP 连接,只要浏览器仍然连接着,服务器就能主动向浏览器推送信息。这为服务器提供了主动通知终端用户客户端的机会,服务器能告知客户端其本地存储状态的任何变化,从而减少客户端状态的陈旧程度。 用我们的写路径与读路径模型来讲,主动将状态变更推至到客户端设备,意味着将写路径一直延伸到终端用户。当客户端首次初始化时,它仍然需要使用读路径来获取其初始状态,但此后它就能够依赖服务器发送的状态变更流了。我们在流处理和消息传递部分讨论的想法并不局限于数据中心中:我们可以进一步采纳这些想法,并将它们一直延伸到终端用户设备【43】。 -这些设备有时会离线,并在此期间无法收到服务器状态变更的任何通知。但是我们已经解决了这个问题:在“[消费者偏移量](ch11.md#消费者偏移量)”中,我们讨论了基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。同样的技术适用于单个用户,每个设备都是一个小事件流的小小订阅者。 +这些设备有时会离线,并在此期间无法收到服务器状态变更的任何通知。但是我们已经解决了这个问题:在 “[消费者偏移量](ch11.md#消费者偏移量)” 中,我们讨论了基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。同样的技术适用于单个用户,每个设备都是一个小事件流的小小订阅者。 #### 端到端的事件流 -最近用于开发有状态的客户端与用户界面的工具,例如如Elm语言【30】和Facebook的React,Flux和Redux工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似(请参阅“[事件溯源](ch11.md#事件溯源)”)。 +最近用于开发有状态的客户端与用户界面的工具,例如如 Elm 语言【30】和 Facebook 的 React、Flux 和 Redux 工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似(请参阅 “[事件溯源](ch11.md#事件溯源)”)。 -将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端(end-to-end)** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。 +将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过 **端到端(end-to-end)** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。 -一些应用(如即时消息传递与在线游戏)已经具有这种“实时”架构(在低延迟交互的意义上,不是在“[响应时间保证](ch8.md#响应时间保证)”中的意义上)。但我们为什么不用这种方式构建所有的应用? +一些应用(如即时消息传递与在线游戏)已经具有这种 “实时” 架构(在低延迟交互的意义上,不是在 “[响应时间保证](ch8.md#响应时间保证)” 中的意义上)。但我们为什么不用这种方式构建所有的应用? -挑战在于,关于无状态客户端和请求/响应交互的假设已经根深蒂固地植入在在我们的数据库、库、框架以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 请求返回一个随时间推移的响应流(请参阅“[变更流的API支持](ch11.md#变更流的API支持)” )。 +挑战在于,关于无状态客户端和请求 / 响应交互的假设已经根深蒂固地植入在在我们的数据库、库、框架以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 请求返回一个随时间推移的响应流(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)” )。 -为了将写路径延伸至终端用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流【27】。更具响应性的用户界面与更好的离线支持,我认为这些优势值得我们付出努力。如果你正在设计数据系统,我希望你对订阅变更的选项留有印象,而不只是查询当前状态。 +为了将写路径延伸至终端用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求 / 响应交互转向发布 / 订阅数据流【27】。更具响应性的用户界面与更好的离线支持,我认为这些优势值得我们付出努力。如果你正在设计数据系统,我希望你对订阅变更的选项留有印象,而不只是查询当前状态。 #### 读也是事件 我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写路径和读路径之间的边界。该存储应当允许对数据进行随机访问的读取查询,否则这些查询将需要扫描整个事件日志。 -在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(请参阅“[流连接](ch11.md#流连接)”)。这种状态通常隐藏在流处理器内部,但一些框架也允许这些状态被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 +在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(请参阅 “[流连接](ch11.md#流连接)”)。这种状态通常隐藏在流处理器内部,但一些框架也允许这些状态被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 我愿意进一步思考这个想法。正如到目前为止所讨论的那样,对存储的写入是通过事件日志进行的,而读取是临时的网络请求,直接流向存储着待查数据的节点。这是一个合理的设计,但不是唯一可行的设计。也可以将读取请求表示为事件流,并同时将读事件与写事件送往流处理器;流处理器通过将读取结果发送到输出流来响应读取事件【46】。 -当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(请参阅“[请求路由](ch6.md#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)“)。 +当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(请参阅 “[请求路由](ch6.md#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅 “[Reduce 侧连接与分组](ch10.md#Reduce侧连接与分组)“)。 服务请求与执行连接之间的这种相似之处是非常关键的【47】。一次性读取请求只是将请求传过连接算子,然后请求马上就被忘掉了;而一个订阅请求,则是与连接另一侧过去与未来事件的持久化连接。 记录读取事件的日志可能对于追踪整个系统中的因果关系与数据来源也有好处:它可以让你重现出当用户做出特定决策之前看见了什么。例如在网商中,向客户显示的预测送达日期与库存状态,可能会影响他们是否选择购买一件商品【4】。要分析这种联系,则需要记录用户查询运输与库存状态的结果。 -将读取事件写入持久存储可以更好地跟踪因果关系(请参阅“[排序事件以捕获因果关系](#排序事件以捕获因果关系)”),但会产生额外的存储与I/O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但如果你已经出于运维目的留下了读取请求日志,将其作为请求处理的副作用,那么将这份日志作为请求事件源并不是什么特别大的变更。 +将读取事件写入持久存储可以更好地跟踪因果关系(请参阅 “[排序事件以捕获因果关系](#排序事件以捕获因果关系)”),但会产生额外的存储与 I/O 成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但如果你已经出于运维目的留下了读取请求日志,将其作为请求处理的副作用,那么将这份日志作为请求事件源并不是什么特别大的变更。 #### 多分区数据处理 对于只涉及单个分区的查询,通过流来发送查询与收集响应可能是杀鸡用牛刀了。然而,这个想法开启了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用了流处理器已经提供的消息路由、分区和连接的基础设施。 -Storm的分布式RPC功能支持这种使用模式(请参阅“[消息传递和RPC](ch11.md#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特URL的人数 —— 即,发推包含该URL的所有人的粉丝集合的并集【48】。由于推特的用户是分区的,因此这种计算需要合并来自多个分区的结果。 +Storm 的分布式 RPC 功能支持这种使用模式(请参阅 “[消息传递和 RPC](ch11.md#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特 URL 的人数 —— 即,发推包含该 URL 的所有人的粉丝集合的并集【48】。由于推特的用户是分区的,因此这种计算需要合并来自多个分区的结果。 -这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,你可以检查该用户IP地址,电子邮件地址,帐单地址,送货地址的信用分。这些信用数据库中的每一个都是有分区的,因此为特定购买事件采集分数需要连接一系列不同的分区数据集【49】。 +这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,你可以检查该用户 IP 地址,电子邮件地址,帐单地址,送货地址的信用分。这些信用数据库中的每一个都是有分区的,因此为特定购买事件采集分数需要连接一系列不同的分区数据集【49】。 -MPP数据库的内部查询执行图有着类似的特征(请参阅“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。 +MPP 数据库的内部查询执行图有着类似的特征(请参阅 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。 ## 将事情做正确 对于只读取数据的无状态服务,出问题也没什么大不了的:你可以修复该错误并重启服务,而一切都恢复正常。像数据库这样的有状态系统就没那么简单了:它们被设计为永远记住事物(或多或少),所以如果出现问题,这种(错误的)效果也将潜在地永远持续下去,这意味着它们需要更仔细的思考【50】。 -我们希望构建可靠且**正确**的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性、隔离性和持久性([第七章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参阅“[弱隔离级别](ch7.md#弱隔离级别)”)。 +我们希望构建可靠且 **正确** 的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性、隔离性和持久性([第七章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参阅 “[弱隔离级别](ch7.md#弱隔离级别)”)。 -事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但后者有更复杂的语义(例如,请参阅“[无主复制](ch5.md#无主复制)”)。**一致性(Consistency)** 经常被谈起,但其定义并不明确(请参阅“[一致性](ch7.md#一致性)”和[第九章](ch9.md))。有些人断言我们应当为了高可用而“拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。 +事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但后者有更复杂的语义(例如,请参阅 “[无主复制](ch5.md#无主复制)”)。**一致性(Consistency)** 经常被谈起,但其定义并不明确(请参阅 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人断言我们应当为了高可用而 “拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。 对于如此重要的话题,我们的理解,以及我们的工程方法却是惊人地薄弱。例如,确定在特定事务隔离等级或复制配置下运行特定应用是否安全是非常困难的【51,52】。通常简单的解决方案似乎在低并发性的情况下工作正常,并且没有错误,但在要求更高的情况下却会出现许多微妙的错误。 -例如,Kyle Kingsbury的Jepsen实验【53】标出了一些产品声称的安全保证与其在网络问题与崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用代码仍然需要正确使用它们提供的功能才行,如果配置很难理解,这是很容易出错的(在这种情况下指的是弱隔离级别,法定人数配置等)。 +例如,Kyle Kingsbury 的 Jepsen 实验【53】标出了一些产品声称的安全保证与其在网络问题与崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用代码仍然需要正确使用它们提供的功能才行,如果配置很难理解,这是很容易出错的(在这种情况下指的是弱隔离级别,法定人数配置等)。 如果你的应用可以容忍偶尔的崩溃,以及以不可预料的方式损坏或丢失数据,那生活就要简单得多,而你可能只要双手合十念阿弥陀佛,期望佛祖能保佑最好的结果。另一方面,如果你需要更强的正确性保证,那么可串行化与原子提交就是久经考验的方法,但它们是有代价的:它们通常只在单个数据中心中工作(这就排除了地理位置分散的架构),并限制了系统能够实现的规模与容错特性。 @@ -426,27 +426,27 @@ MPP数据库的内部查询执行图有着类似的特征(请参阅“[Hadoop ### 数据库的端到端原则 -仅仅因为一个应用程序使用了具有相对较强安全属性的数据系统(例如可串行化的事务),并不意味着就可以保证没有数据丢失或损坏。例如,如果某个应用有个Bug,导致它写入不正确的数据,或者从数据库中删除数据,那么可串行化的事务也救不了你。 +仅仅因为一个应用程序使用了具有相对较强安全属性的数据系统(例如可串行化的事务),并不意味着就可以保证没有数据丢失或损坏。例如,如果某个应用有个 Bug,导致它写入不正确的数据,或者从数据库中删除数据,那么可串行化的事务也救不了你。 -这个例子可能看起来很无聊,但值得认真对待:应用会出Bug,而人也会犯错误。我在“[状态、流和不变性](ch11.md#状态、流和不变性)”中使用了这个例子来支持不可变和仅追加的数据,阉割掉错误代码摧毁良好数据的能力,能让从错误中恢复更为容易。 +这个例子可能看起来很无聊,但值得认真对待:应用会出 Bug,而人也会犯错误。我在 “[状态、流和不变性](ch11.md#状态、流和不变性)” 中使用了这个例子来支持不可变和仅追加的数据,阉割掉错误代码摧毁良好数据的能力,能让从错误中恢复更为容易。 虽然不变性很有用,但它本身并非万灵药。让我们来看一个可能发生的、非常微妙的数据损坏案例。 #### 正好执行一次操作 -在“[容错](ch11.md#容错)”中,我们见到了**恰好一次**(或**等效一次**)语义的概念。如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。 +在 “[容错](ch11.md#容错)” 中,我们见到了 **恰好一次**(或 **等效一次**)语义的概念。如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。 处理两次是数据损坏的一种形式:为同样的服务向客户收费两次(收费太多)或增长计数器两次(夸大指标)都不是我们想要的。在这种情况下,恰好一次意味着安排计算,使得最终效果与没有发生错误的情况一样,即使操作实际上因为某种错误而重试。我们先前讨论过实现这一目标的几种方法。 -最有效的方法之一是使操作**幂等**(idempotent,请参阅“[幂等性](ch11.md#幂等性)”);即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点故障切换至另一个节点时做好防护(请参阅“[领导者和锁](ch8.md#领导者和锁)”)。 +最有效的方法之一是使操作 **幂等**(idempotent,请参阅 “[幂等性](ch11.md#幂等性)”);即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作 ID 集合),并在从一个节点故障切换至另一个节点时做好防护(请参阅 “[领导者和锁](ch8.md#领导者和锁)”)。 #### 抑制重复 -除了流处理之外,其他许多地方也需要抑制重复的模式。例如,TCP使用了数据包上的序列号,以便接收方可以将它们正确排序,并确定网络上是否有数据包丢失或重复。在将数据交付应用前,TCP协议栈会重新传输任何丢失的数据包,也会移除任何重复的数据包。 +除了流处理之外,其他许多地方也需要抑制重复的模式。例如,TCP 使用了数据包上的序列号,以便接收方可以将它们正确排序,并确定网络上是否有数据包丢失或重复。在将数据交付应用前,TCP 协议栈会重新传输任何丢失的数据包,也会移除任何重复的数据包。 -但是,这种重复抑制仅适用于单条TCP连接的场景中。假设TCP连接是一个客户端与数据库的连接,并且它正在执行[例12-1]()中的事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个TCP连接上发送的)。如果客户端在发送`COMMIT`之后并在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端是不知道事务是否已经被提交的([图8-1](img/fig8-1.png))。 +但是,这种重复抑制仅适用于单条 TCP 连接的场景中。假设 TCP 连接是一个客户端与数据库的连接,并且它正在执行 [例 12-1]() 中的事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个 TCP 连接上发送的)。如果客户端在发送 `COMMIT` 之后并在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端是不知道事务是否已经被提交的([图 8-1](img/fig8-1.png))。 -**例12-1 资金从一个账户到另一个账户的非幂等转移** +**例 12-1 资金从一个账户到另一个账户的非幂等转移** ```sql BEGIN TRANSACTION; @@ -455,159 +455,159 @@ BEGIN TRANSACTION; COMMIT; ``` -客户端可以重连到数据库并重试事务,但现在已经处于TCP重复抑制的范围之外了。因为[例12-1]()中的事务不是幂等的,可能会发生转了\$22而不是期望的\$11。因此,尽管[例12-1]()是一个事务原子性的标准样例,但它实际上并不正确,而真正的银行并不会这样办事【3】。 +客户端可以重连到数据库并重试事务,但现在已经处于 TCP 重复抑制的范围之外了。因为 [例 12-1]() 中的事务不是幂等的,可能会发生转了 \$22 而不是期望的 \$11。因此,尽管 [例 12-1]() 是一个事务原子性的标准样例,但它实际上并不正确,而真正的银行并不会这样办事【3】。 -两阶段提交(请参阅“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”)协议会破坏TCP连接与事务之间的1:1映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。 +两阶段提交(请参阅 “[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”)协议会破坏 TCP 连接与事务之间的 1:1 映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。 -即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是Web浏览器,则它可能会使用HTTP POST请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了POST,但却在能够从服务器接收响应之前没了信号。 +即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是 Web 浏览器,则它可能会使用 HTTP POST 请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了 POST,但却在能够从服务器接收响应之前没了信号。 -在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户选“是”,因为他们希望操作发生(Post/Redirect/Get模式【54】可以避免在正常操作中出现此警告消息,但POST请求超时就没办法了)。从Web服务器的角度来看,重试是一个独立的请求;从数据库的角度来看,这是一个独立的事务。通常的除重机制无济于事。 +在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。 Web 浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户选 “是”,因为他们希望操作发生(Post/Redirect/Get 模式【54】可以避免在正常操作中出现此警告消息,但 POST 请求超时就没办法了)。从 Web 服务器的角度来看,重试是一个独立的请求;从数据库的角度来看,这是一个独立的事务。通常的除重机制无济于事。 #### 操作标识符 -要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端(end-to-end)** 的请求流。 -例如,你可以为操作生成一个唯一的标识符(例如UUID),并将其作为隐藏表单字段包含在客户端应用中,或通过计算所有表单相关字段的散列来生成操作ID 【3】。如果Web浏览器提交了两次POST请求,这两个请求将具有相同的操作ID。然后,你可以将该操作ID一路传递到数据库,并检查你是否曾经使用给定的ID执行过一个操作,如[例12-2]()中所示。 +要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑 **端到端(end-to-end)** 的请求流。 +例如,你可以为操作生成一个唯一的标识符(例如 UUID),并将其作为隐藏表单字段包含在客户端应用中,或通过计算所有表单相关字段的散列来生成操作 ID 【3】。如果 Web 浏览器提交了两次 POST 请求,这两个请求将具有相同的操作 ID。然后,你可以将该操作 ID 一路传递到数据库,并检查你是否曾经使用给定的 ID 执行过一个操作,如 [例 12-2]() 中所示。 -**例12-2 使用唯一ID来抑制重复请求** +**例 12-2 使用唯一 ID 来抑制重复请求** ```sql ALTER TABLE requests ADD UNIQUE (request_id); BEGIN TRANSACTION; INSERT INTO requests - (request_id, from_account, to_account, amount) + (request_id, from_account, to_account, amount) VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00); UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234; UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321; COMMIT; ``` -[例12-2]()依赖于`request_id`列上的唯一约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中讨论过,应用级别的**检查-然后-插入**可能会在不可串行化的隔离下失败)。 +[例 12-2]() 依赖于 `request_id` 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID,那么 `INSERT` 失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在 “[写入偏斜与幻读](ch7.md#写入偏斜与幻读)” 中讨论过,应用级别的 **检查 - 然后 - 插入** 可能会在不可串行化的隔离下失败)。 -除了抑制重复的请求之外,[例12-2]()中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅“[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求ID来强制执行。 +除了抑制重复的请求之外,[例 12-2]() 中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅 “[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求 ID 来强制执行。 #### 端到端原则 -抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为**端到端原则(end-to-end argument)**,它在1984年由Saltzer,Reed和Clark阐述【55】: +抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为 **端到端原则(end-to-end argument)**,它在 1984 年由 Saltzer、Reed 和 Clark 阐述【55】: > 只有在通信系统两端应用的知识与帮助下,所讨论的功能才能完全地正确地实现。因而将这种被质疑的功能作为通信系统本身的功能是不可能的(有时,通信系统可以提供这种功能的不完备版本,可能有助于提高性能)。 > -在我们的例子中**所讨论的功能**是重复抑制。我们看到TCP在TCP连接层次抑制了重复的数据包,一些流处理器在消息处理层次提供了所谓的恰好一次语义,但这些都无法阻止当一个请求超时时,用户亲自提交重复的请求。TCP,数据库事务,以及流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从终端用户的客户端一路传递到数据库的事务标识符。 +在我们的例子中 **所讨论的功能** 是重复抑制。我们看到 TCP 在 TCP 连接层次抑制了重复的数据包,一些流处理器在消息处理层次提供了所谓的恰好一次语义,但这些都无法阻止当一个请求超时时,用户亲自提交重复的请求。TCP,数据库事务,以及流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从终端用户的客户端一路传递到数据库的事务标识符。 -端到端原则也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到由连接两端发送/接收软件中Bug导致的损坏。或数据存储所在磁盘上的损坏。如果你想捕获数据所有可能的损坏来源,你也需要端到端的校验和。 +端到端原则也适用于检查数据的完整性:以太网,TCP 和 TLS 中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到由连接两端发送 / 接收软件中 Bug 导致的损坏。或数据存储所在磁盘上的损坏。如果你想捕获数据所有可能的损坏来源,你也需要端到端的校验和。 -类似的原则也适用于加密【55】:家庭WiFi网络上的密码可以防止人们窃听你的WiFi流量,但无法阻止互联网上其他地方攻击者的窥探;客户端与服务器之间的TLS/SSL可以阻挡网络攻击者,但无法阻止恶意服务器。只有端到端的加密和认证可以防止所有这些事情。 +类似的原则也适用于加密【55】:家庭 WiFi 网络上的密码可以防止人们窃听你的 WiFi 流量,但无法阻止互联网上其他地方攻击者的窥探;客户端与服务器之间的 TLS/SSL 可以阻挡网络攻击者,但无法阻止恶意服务器。只有端到端的加密和认证可以防止所有这些事情。 -尽管低层级的功能(TCP重复抑制,以太网校验和,WiFi加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们能降低较高层级出现问题的可能性。例如,如果我们没有TCP来将数据包排成正确的顺序,那么HTTP请求通常就会被搅烂。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 +尽管低层级的功能(TCP 重复抑制、以太网校验和、WiFi 加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们能降低较高层级出现问题的可能性。例如,如果我们没有 TCP 来将数据包排成正确的顺序,那么 HTTP 请求通常就会被搅烂。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 #### 在数据系统中应用端到端思考 这将我带回最初的论点:仅仅因为应用使用了提供相对较强安全属性的数据系统,例如可串行化的事务,并不意味着应用的数据就不会丢失或损坏了。应用本身也需要采取端到端的措施,例如除重。 -这实在是一个遗憾,因为容错机制很难弄好。低层级的可靠机制(比如TCP中的那些)运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级容错机制打包成抽象,而应用不需要再去操心,那该多好呀 —— 但恐怕我们还没有找到这一正确的抽象。 +这实在是一个遗憾,因为容错机制很难弄好。低层级的可靠机制(比如 TCP 中的那些)运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级容错机制打包成抽象,而应用不需要再去操心,那该多好呀 —— 但恐怕我们还没有找到这一正确的抽象。 -长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如[第七章](ch7.md)导言中所讨论的,它们将各种可能的问题(并发写入、违背约束、崩溃、网络中断、磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。 +长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如 [第七章](ch7.md) 导言中所讨论的,它们将各种可能的问题(并发写入、违背约束、崩溃、网络中断、磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。 -事务是代价高昂的,当涉及异构存储技术时尤为甚(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。 +事务是代价高昂的,当涉及异构存储技术时尤为甚(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。 出于这些原因,我认为探索对容错的抽象是很有价值的。它使提供应用特定的端到端的正确性属性变得更简单,而且还能在大规模分布式环境中提供良好的性能与运维特性。 ### 强制约束 -让我们思考一下在[分拆数据库](#分拆数据库)上下文中的**正确性(correctness)**。我们看到端到端的除重可以通过从客户端一路透传到数据库的请求ID实现。那么其他类型的约束呢? +让我们思考一下在 [分拆数据库](#分拆数据库) 上下文中的 **正确性(correctness)**。我们看到端到端的除重可以通过从客户端一路透传到数据库的请求 ID 实现。那么其他类型的约束呢? -我们先来特别关注一下**唯一性约束** —— 例如我们在[例12-2]()中所依赖的约束。在“[约束和唯一性保证](ch9.md#约束和唯一性保证)”中,我们看到了几个其他需要强制实施唯一性的应用功能例子:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能在航班或剧院预订同一个座位。 +我们先来特别关注一下 **唯一性约束** —— 例如我们在 [例 12-2]() 中所依赖的约束。在 “[约束和唯一性保证](ch9.md#约束和唯一性保证)” 中,我们看到了几个其他需要强制实施唯一性的应用功能例子:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能在航班或剧院预订同一个座位。 其他类型的约束也非常类似:例如,确保帐户余额永远不会变为负数,确保不会超卖库存,或者会议室没有重复的预订。执行唯一性约束的技术通常也可以用于这些约束。 #### 唯一性约束需要达成共识 -在[第九章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。 +在 [第九章](ch9.md) 中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。 -达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(请参阅“[单领导者复制与共识](ch9.md#单领导者复制与共识)”)。 +达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(请参阅 “[单领导者复制与共识](ch9.md#单领导者复制与共识)”)。 -唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求ID确保唯一性(如[例12-2]()所示),你可以确保所有具有相同请求ID的请求都被路由到同一分区(请参阅[第六章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。 +唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求 ID 确保唯一性(如 [例 12-2]() 所示),你可以确保所有具有相同请求 ID 的请求都被路由到同一分区(请参阅 [第六章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。 -但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(请参阅“[实现线性一致的系统](ch9.md#实现线性一致的系统)”)。如果你想立刻拒绝任何违背约束的写入,同步协调是无法避免的【56】。 +但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(请参阅 “[实现线性一致的系统](ch9.md#实现线性一致的系统)”)。如果你想立刻拒绝任何违背约束的写入,同步协调是无法避免的【56】。 #### 基于日志消息传递中的唯一性 -日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)** 并且等价于共识(请参阅“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 +日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为 **全序广播(total order boardcast)** 并且等价于共识(请参阅 “[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 -流处理器在单个线程上依次消费单个日志分区中的所有消息(请参阅“[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”)。因此,如果日志是按需要确保唯一的值做的分区,则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】: +流处理器在单个线程上依次消费单个日志分区中的所有消息(请参阅 “[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”)。因此,如果日志是按需要确保唯一的值做的分区,则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】: 1. 每个对用户名的请求都被编码为一条消息,并追加到按用户名散列值确定的分区。 2. 流处理器依序读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用了。对于所有申请可用用户名的请求,它都会记录该用户名,并向输出流发送一条成功消息。对于所有申请已占用用户名的请求,它都会向输出流发送一条拒绝消息。 3. 请求用户名的客户端监视输出流,等待与其请求相对应的成功或拒绝消息。 -该算法基本上与“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区都可以被独立处理。 +该算法基本上与 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区都可以被独立处理。 -该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如“[什么是冲突?](ch5.md#什么是冲突?)”与“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在90年代开创的方法类似【58】。 +该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如 “[什么是冲突?](ch5.md#什么是冲突?)” 与 “[写入偏斜与幻读](ch7.md#写入偏斜与幻读)” 中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与 Bayou 在 90 年代开创的方法类似【58】。 #### 多分区请求处理 -当涉及多个分区时,确保操作以原子方式执行且同时满足约束就变得很有趣了。在[例12-2]()中,可能有三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三种东西放入同一个分区,因为它们都是相互独立的。 +当涉及多个分区时,确保操作以原子方式执行且同时满足约束就变得很有趣了。在 [例 12-2]() 中,可能有三个分区:一个包含请求 ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三种东西放入同一个分区,因为它们都是相互独立的。 在数据库的传统方法中,执行此事务需要跨全部三个分区进行原子提交,就这些分区上的所有其他事务而言,这实质上是将该事务嵌入一个全序。而这样就要求跨分区协调,不同的分区无法再独立地进行处理,因此吞吐量很可能会受到影响。 但事实证明,使用分区日志可以达到等价的正确性而无需原子提交: -1. 从账户A向账户B转账的请求由客户端提供一个唯一的请求ID,并按请求ID追加写入相应日志分区。 -2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户A的借记指令(按A分区),收款人B的贷记指令(按B分区)。被发出的消息中会带有原始的请求ID。 -3. 后续处理器消费借记/贷记指令流,按照请求ID除重,并将变更应用至账户余额。 +1. 从账户 A 向账户 B 转账的请求由客户端提供一个唯一的请求 ID,并按请求 ID 追加写入相应日志分区。 +2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户 A 的借记指令(按 A 分区),收款人 B 的贷记指令(按 B 分区)。被发出的消息中会带有原始的请求 ID。 +3. 后续处理器消费借记 / 贷记指令流,按照请求 ID 除重,并将变更应用至账户余额。 -步骤1和步骤2是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅“[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。 +步骤 1 和步骤 2 是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅 “[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。 -如果流处理器在步骤2中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤3中的处理器可以使用端到端请求ID轻松地对其除重。 +如果流处理器在步骤 2 中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤 3 中的处理器可以使用端到端请求 ID 轻松地对其除重。 -如果你想确保付款人的帐户不会因此次转账而透支,则可以使用一个额外的流处理器来维护账户余额并校验事务(按付款人账户分区),只有有效的事务会被记录在步骤1中的请求日志中。 +如果你想确保付款人的帐户不会因此次转账而透支,则可以使用一个额外的流处理器来维护账户余额并校验事务(按付款人账户分区),只有有效的事务会被记录在步骤 1 中的请求日志中。 -通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求ID,我们实现了同样的正确性属性(每个请求对付款人与收款人都恰好生效一次),即使在出现故障,且没有使用原子提交协议的情况下依然如此。使用多个不同分区阶段的想法与我们在“[多分区数据处理](#多分区数据处理)”中讨论的想法类似(也请参阅“[并发控制](ch11.md#并发控制)”)。 +通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求 ID,我们实现了同样的正确性属性(每个请求对付款人与收款人都恰好生效一次),即使在出现故障,且没有使用原子提交协议的情况下依然如此。使用多个不同分区阶段的想法与我们在 “[多分区数据处理](#多分区数据处理)” 中讨论的想法类似(也请参阅 “[并发控制](ch11.md#并发控制)”)。 ### 及时性与完整性 -事务的一个便利属性是,它们通常是线性一致的(请参阅“[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。 +事务的一个便利属性是,它们通常是线性一致的(请参阅 “[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。 -当我们把一个操作拆分为跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。但是,客户端等待输出流中的特定消息是可能的。这正是我们在“[基于日志消息传递中的唯一性](#基于日志消息传递中的唯一性)”一节中检查唯一性约束时所做的事情。 +当我们把一个操作拆分为跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。但是,客户端等待输出流中的特定消息是可能的。这正是我们在 “[基于日志消息传递中的唯一性](#基于日志消息传递中的唯一性)” 一节中检查唯一性约束时所做的事情。 在这个例子中,唯一性检查的正确性不取决于消息发送者是否等待结果。等待的目的仅仅是同步通知发送者唯一性检查是否成功。但该通知可以与消息处理的结果相解耦。 -更一般地来讲,我认为术语**一致性(consistency)** 这个术语混淆了两个值得分别考虑的需求: +更一般地来讲,我认为术语 **一致性(consistency)** 这个术语混淆了两个值得分别考虑的需求: * 及时性(Timeliness) - 及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。 + 及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。 - CAP定理(请参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(请参阅“[读己之写](ch5.md#读己之写)”)。 + CAP 定理(请参阅 “[线性一致性的代价](ch9.md#线性一致性的代价)”)使用 **线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像 **写后读** 这样及时性更弱的一致性也很有用(请参阅 “[读己之写](ch5.md#读己之写)”)。 * 完整性(Integrity) - 完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅“[从事件日志中派生出当前状态](ch11.md#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。 + 完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅 “[从事件日志中派生出当前状态](ch11.md#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。 - 如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(请参阅“[ACID的含义](ch7.md#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。 + 如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在 ACID 事务的上下文中(请参阅 “[ACID 的含义](ch7.md#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。 口号形式:违反及时性,“最终一致性”;违反完整性,“永无一致性”。 我断言在大多数应用中,完整性比及时性重要得多。违反及时性可能令人困惑与讨厌,但违反完整性的结果可能是灾难性的。 -例如在你的信用卡对账单上,如果某一笔过去24小时内完成的交易尚未出现并不令人奇怪 —— 这些系统有一定的滞后是正常的。我们知道银行是异步核算与敲定交易的,这里的及时性并不是非常重要【3】。但果当期对账单余额与上期对账单余额加交易总额对不上(求和错误),或者出现一笔向你收费但未向商家付款的交易(消失的钱),那实在是太糟糕了。这样的问题就违背了系统的完整性。 +例如在你的信用卡对账单上,如果某一笔过去 24 小时内完成的交易尚未出现并不令人奇怪 —— 这些系统有一定的滞后是正常的。我们知道银行是异步核算与敲定交易的,这里的及时性并不是非常重要【3】。但果当期对账单余额与上期对账单余额加交易总额对不上(求和错误),或者出现一笔向你收费但未向商家付款的交易(消失的钱),那实在是太糟糕了。这样的问题就违背了系统的完整性。 #### 数据流系统的正确性 -ACID事务通常既提供及时性(例如线性一致性)也提供完整性保证(例如原子提交)。因此如果你从ACID事务的角度来看待应用的正确性,那么及时性与完整性的区别是无关紧要的。 +ACID 事务通常既提供及时性(例如线性一致性)也提供完整性保证(例如原子提交)。因此如果你从 ACID 事务的角度来看待应用的正确性,那么及时性与完整性的区别是无关紧要的。 另一方面,对于在本章中讨论的基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非你显式构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。 -**恰好一次**或**等效一次**语义(请参阅“[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在出现故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。 +**恰好一次** 或 **等效一次** 语义(请参阅 “[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在出现故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。 正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们有潜力达到与后者相当的正确性,同时还具备好得多的性能与运维稳健性。为了达成这种正确性,我们组合使用了多种机制: -* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅“[事件溯源](ch11.md#事件溯源)”)。 -* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅“[真的串行执行](ch7.md#真的串行执行)”和“[应用代码作为衍生函数](ch12.md#应用代码作为衍生函数)”) -* 将客户端生成的请求ID传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。 -* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅“[不可变事件的优点](ch11.md#不可变事件的优点)”) +* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅 “[事件溯源](ch11.md#事件溯源)”)。 +* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅 “[真的串行执行](ch7.md#真的串行执行)” 和 “[应用代码作为衍生函数](ch12.md#应用代码作为衍生函数)”) +* 将客户端生成的请求 ID 传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。 +* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅 “[不可变事件的优点](ch11.md#不可变事件的优点)”) 这种机制组合在我看来,是未来构建容错应用的一个非常有前景的方向。 @@ -617,16 +617,16 @@ ACID事务通常既提供及时性(例如线性一致性)也提供完整性 然而另一个需要了解的事实是,许多真实世界的应用实际上可以摆脱这种形式,接受弱得多的唯一性: -* 如果两个人同时注册了相同的用户名或预订了相同的座位,你可以给其中一个人发消息道歉,并要求他们换一个不同的用户名或座位。这种纠正错误的变化被称为**补偿性事务(compensating transaction)**【59,60】。 +* 如果两个人同时注册了相同的用户名或预订了相同的座位,你可以给其中一个人发消息道歉,并要求他们换一个不同的用户名或座位。这种纠正错误的变化被称为 **补偿性事务(compensating transaction)**【59,60】。 * 如果客户订购的物品多于仓库中的物品,你可以下单补仓,并为延误向客户道歉,向他们提供折扣。实际上,这么说吧,如果叉车在仓库中轧过了你的货物,剩下的货物比你想象的要少,那么你也是得这么做【61】。因此,既然道歉工作流无论如何已经成为你商业过程中的一部分了,那么对库存物品数目添加线性一致的约束可能就没必要了。 -* 与之类似,许多航空公司都会超卖机票,打着一些旅客可能会错过航班的算盘;许多旅馆也会超卖客房,抱着部分客人可能会取消预订的期望。在这些情况下,出于商业原因而故意违反了“一人一座”的约束;当需求超过供给的情况出现时,就会进入补偿流程(退款、升级舱位/房型、提供隔壁酒店的免费的房间)。即使没有超卖,为了应对由恶劣天气或员工罢工导致的航班取消,你还是需要道歉与补偿流程 —— 从这些问题中恢复仅仅是商业活动的正常组成部分。 +* 与之类似,许多航空公司都会超卖机票,打着一些旅客可能会错过航班的算盘;许多旅馆也会超卖客房,抱着部分客人可能会取消预订的期望。在这些情况下,出于商业原因而故意违反了 “一人一座” 的约束;当需求超过供给的情况出现时,就会进入补偿流程(退款、升级舱位 / 房型、提供隔壁酒店的免费的房间)。即使没有超卖,为了应对由恶劣天气或员工罢工导致的航班取消,你还是需要道歉与补偿流程 —— 从这些问题中恢复仅仅是商业活动的正常组成部分。 * 如果有人从账户超额取款,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 -在许多商业场景中,临时违背约束并稍后通过道歉来修复,实际上是可以接受的。道歉的成本各不相同,但通常很低(以金钱或名声来算):你无法撤回已发送的电子邮件,但可以发送一封后续电子邮件进行更正。如果你不小心向信用卡收取了两次费用,则可以将其中一项收费退款,而代价仅仅是手续费,也许还有客户的投诉。尽管一旦ATM吐了钱,你无法直接取回,但原则上如果账户透支而客户拒不支付,你可以派催收员收回欠款。 +在许多商业场景中,临时违背约束并稍后通过道歉来修复,实际上是可以接受的。道歉的成本各不相同,但通常很低(以金钱或名声来算):你无法撤回已发送的电子邮件,但可以发送一封后续电子邮件进行更正。如果你不小心向信用卡收取了两次费用,则可以将其中一项收费退款,而代价仅仅是手续费,也许还有客户的投诉。尽管一旦 ATM 吐了钱,你无法直接取回,但原则上如果账户透支而客户拒不支付,你可以派催收员收回欠款。 道歉的成本是否能接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型反而会带来不必要的限制,而线性一致性的约束也不是必须的。乐观写入,事后检查可能是一种合理的选择。你仍然可以在做一些挽回成本高昂的事情前确保有相关的验证,但这并不意味着写入数据之前必须先进行验证。 -这些应用**确实**需要完整性:你不会希望丢失预订信息,或者由于借方贷方不匹配导致资金消失。但是它们在执行约束时**并不需要**及时性:如果你销售的货物多于仓库中的库存,可以在事后道歉后并弥补问题。这种做法与我们在“[处理写入冲突](ch5.md#处理写入冲突)”中讨论的冲突解决方法类似。 +这些应用 **确实** 需要完整性:你不会希望丢失预订信息,或者由于借方贷方不匹配导致资金消失。但是它们在执行约束时 **并不需要** 及时性:如果你销售的货物多于仓库中的库存,可以在事后道歉后并弥补问题。这种做法与我们在 “[处理写入冲突](ch5.md#处理写入冲突)” 中讨论的冲突解决方法类似。 #### 无协调数据系统 @@ -635,69 +635,69 @@ ACID事务通常既提供及时性(例如线性一致性)也提供完整性 1. 数据流系统可以维持衍生数据的完整性保证,而无需原子提交、线性一致性或者同步的跨分区协调。 2. 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被临时违反并在稍后被修复。 -总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调(coordination-avoiding)** 的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力【56】。 +总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种 **无协调(coordination-avoiding)** 的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力【56】。 例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统的及时性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。 -在这种情况下,可串行化事务作为维护衍生状态的一部分仍然是有用的,但它们只能在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务,请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)不是必需的。同步协调仍然可以在需要的地方引入(例如在无法恢复的操作之前强制执行严格的约束),但是如果只是应用的一小部分地方需要它,没必要让所有操作都付出协调的代价。【43】。 +在这种情况下,可串行化事务作为维护衍生状态的一部分仍然是有用的,但它们只能在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如 XA 事务,请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”)不是必需的。同步协调仍然可以在需要的地方引入(例如在无法恢复的操作之前强制执行严格的约束),但是如果只是应用的一小部分地方需要它,没必要让所有操作都付出协调的代价。【43】。 另一种审视协调与约束的角度是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。你不可能将道歉数量减少到零,但可以根据自己的需求寻找最佳平衡点 —— 既不存在太多不一致性,又不存在太多可用性问题。 ### 信任但验证 -我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的**系统模型**(system model,请参阅“[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行`fsync`后不会丢失,内存中的数据没有损坏,而CPU的乘法指令总是能返回正确的结果。 +我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的 **系统模型**(system model,请参阅 “[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行 `fsync` 后不会丢失,内存中的数据没有损坏,而 CPU 的乘法指令总是能返回正确的结果。 -这些假设是相当合理的,因为大多数时候它们都是成立的,如果我们不得不经常担心计算机出错,那么基本上寸步难行。在传统上,系统模型采用二元方法处理故障:我们假设有些事情可能会发生,而其他事情**永远**不会发生。实际上,这更像是一个概率问题:有些事情更有可能,其他事情不太可能。问题在于违反我们假设的情况是否经常发生,以至于我们可能在实践中遇到它们。 +这些假设是相当合理的,因为大多数时候它们都是成立的,如果我们不得不经常担心计算机出错,那么基本上寸步难行。在传统上,系统模型采用二元方法处理故障:我们假设有些事情可能会发生,而其他事情 **永远** 不会发生。实际上,这更像是一个概率问题:有些事情更有可能,其他事情不太可能。问题在于违反我们假设的情况是否经常发生,以至于我们可能在实践中遇到它们。 -我们已经看到,数据可能会在尚未落盘时损坏(请参阅“[复制与持久性](ch7.md#复制与持久性)”),而网络上的数据损坏有时可能规避了TCP校验和(请参阅“[弱谎言形式](ch8.md#弱谎言形式)” )。也许我们应当更关注这些事情? +我们已经看到,数据可能会在尚未落盘时损坏(请参阅 “[复制与持久性](ch7.md#复制与持久性)”),而网络上的数据损坏有时可能规避了 TCP 校验和(请参阅 “[弱谎言形式](ch8.md#弱谎言形式)” )。也许我们应当更关注这些事情? -我过去所从事的一个应用收集了来自客户端的崩溃报告,我们收到的一些报告,只有在这些设备内存中出现了随机位翻转才解释的通。这看起来不太可能,但是如果有足够多的设备运行你的软件,那么即使再不可能发生的事也确实会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,一些病态的存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 —— 一种可用于破坏操作系统安全机制的效应【63】(这种技术被称为**Rowhammer**)。一旦你仔细观察,硬件并不是看上去那样完美的抽象。 +我过去所从事的一个应用收集了来自客户端的崩溃报告,我们收到的一些报告,只有在这些设备内存中出现了随机位翻转才解释的通。这看起来不太可能,但是如果有足够多的设备运行你的软件,那么即使再不可能发生的事也确实会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,一些病态的存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 —— 一种可用于破坏操作系统安全机制的效应【63】(这种技术被称为 **Rowhammer**)。一旦你仔细观察,硬件并不是看上去那样完美的抽象。 要澄清的是,随机位翻转在现代硬件上仍是非常罕见的【64】。我只想指出,它们并没有超越可能性的范畴,所以值得一些关注。 #### 维护完整性,尽管软件有Bug -除了这些硬件问题之外,总是存在软件Bug的风险,这些错误不会被较低层次的网络、内存或文件系统校验和所捕获。即使广泛使用的数据库软件也有Bug:即使像MySQL与PostgreSQL这样稳健、口碑良好、多年来被许多人充分测试过的软件,就我个人所见也有Bug,比如MySQL未能正确维护唯一约束【65】,以及PostgreSQL的可串行化隔离等级存在特定的写偏斜异常【66】。对于不那么成熟的软件来说,情况可能要糟糕得多。 +除了这些硬件问题之外,总是存在软件 Bug 的风险,这些错误不会被较低层次的网络、内存或文件系统校验和所捕获。即使广泛使用的数据库软件也有 Bug:即使像 MySQL 与 PostgreSQL 这样稳健、口碑良好、多年来被许多人充分测试过的软件,就我个人所见也有 Bug,比如 MySQL 未能正确维护唯一约束【65】,以及 PostgreSQL 的可串行化隔离等级存在特定的写偏斜异常【66】。对于不那么成熟的软件来说,情况可能要糟糕得多。 -尽管在仔细设计,测试,以及审查上做出很多努力,但Bug仍然会在不知不觉中产生。尽管它们很少,而且最终会被发现并被修复,但总会有那么一段时间,这些Bug可能会损坏数据。 +尽管在仔细设计,测试,以及审查上做出很多努力,但 Bug 仍然会在不知不觉中产生。尽管它们很少,而且最终会被发现并被修复,但总会有那么一段时间,这些 Bug 可能会损坏数据。 而对于应用代码,我们不得不假设会有更多的错误,因为绝大多数应用的代码经受的评审与测试远远无法与数据库的代码相比。许多应用甚至没有正确使用数据库提供的用于维持完整性的功能,例如外键或唯一性约束【36】。 -ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有Bug时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。 +ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有 Bug 时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。 #### 不要盲目信任承诺 -由于硬件和软件并不总是符合我们的理想,所以数据损坏似乎早晚不可避免。因此,我们至少应该有办法查明数据是否已经损坏,以便我们能够修复它,并尝试追查错误的来源。检查数据完整性称为**审计(auditing)**。 +由于硬件和软件并不总是符合我们的理想,所以数据损坏似乎早晚不可避免。因此,我们至少应该有办法查明数据是否已经损坏,以便我们能够修复它,并尝试追查错误的来源。检查数据完整性称为 **审计(auditing)**。 -如“[不可变事件的优点](ch11.md#不可变事件的优点)”一节中所述,审计不仅仅适用于财务应用程序。不过,可审计性在财务中是非常非常重要的,因为每个人都知道错误总会发生,我们也都认为能够检测和解决问题是合理的需求。 +如 “[不可变事件的优点](ch11.md#不可变事件的优点)” 一节中所述,审计不仅仅适用于财务应用程序。不过,可审计性在财务中是非常非常重要的,因为每个人都知道错误总会发生,我们也都认为能够检测和解决问题是合理的需求。 -成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS和Amazon S3等大规模存储系统并不完全信任磁盘:它们运行后台进程持续回读文件,并将其与其他副本进行比较,并将文件从一个磁盘移动到另一个,以便降低静默损坏的风险【67】。 +成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS 和 Amazon S3 等大规模存储系统并不完全信任磁盘:它们运行后台进程持续回读文件,并将其与其他副本进行比较,并将文件从一个磁盘移动到另一个,以便降低静默损坏的风险【67】。 如果你想确保你的数据仍然存在,你必须真正读取它并进行检查。大多数时候它们仍然会在那里,但如果不是这样,你一定想尽早知道答案,而不是更晚。按照同样的原则,不时地尝试从备份中恢复是非常重要的 —— 否则当你发现备份损坏时,你可能已经遇到了数据丢失,那时候就真的太晚了。不要盲目地相信它们全都管用。 #### 验证的文化 -像HDFS和S3这样的系统仍然需要假设磁盘大部分时间都能正常工作 —— 这是一个合理的假设,但与它们**始终**能正常工作的假设并不相同。然而目前还没有多少系统采用这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见的数据损坏的可能性做过准备。我希望未来能看到更多的**自我验证(self-validating)** 或**自我审计(self-auditing)** 系统,不断检查自己的完整性,而不是依赖盲目的信任【68】。 +像 HDFS 和 S3 这样的系统仍然需要假设磁盘大部分时间都能正常工作 —— 这是一个合理的假设,但与它们 **始终** 能正常工作的假设并不相同。然而目前还没有多少系统采用这种 “信任但是验证” 的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见的数据损坏的可能性做过准备。我希望未来能看到更多的 **自我验证(self-validating)** 或 **自我审计(self-auditing)** 系统,不断检查自己的完整性,而不是依赖盲目的信任【68】。 -我担心ACID数据库的文化导致我们在盲目信任技术(如事务机制)的基础上开发应用,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,通常会认为审计机制并不值得投资。 +我担心 ACID 数据库的文化导致我们在盲目信任技术(如事务机制)的基础上开发应用,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,通常会认为审计机制并不值得投资。 -但随之而来的是,数据库的格局发生了变化:在NoSQL的旗帜下,更弱的一致性保证成为常态,更不成熟的存储技术越来越被广泛使用。但是由于审计机制还没有被开发出来,尽管这种方式越来越危险,我们仍不断在盲目信任的基础上构建应用。让我们想一想如何针对可审计性而设计吧。 +但随之而来的是,数据库的格局发生了变化:在 NoSQL 的旗帜下,更弱的一致性保证成为常态,更不成熟的存储技术越来越被广泛使用。但是由于审计机制还没有被开发出来,尽管这种方式越来越危险,我们仍不断在盲目信任的基础上构建应用。让我们想一想如何针对可审计性而设计吧。 #### 为可审计性而设计 -如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明**为什么**要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。 +如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明 **为什么** 要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。 相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。 -显式处理数据流(请参阅“[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的**来龙去脉(provenance)** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。 +显式处理数据流(请参阅 “[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的 **来龙去脉(provenance)** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。 -具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它**为什么**做了某些事情【4,69】。如果出现意想之外的事情,那么重现导致意外事件的确切事故现场的诊断能力—— 一种时间旅行调试功能是非常有价值的。 +具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它 **为什么** 做了某些事情【4,69】。如果出现意想之外的事情,那么重现导致意外事件的确切事故现场的诊断能力 —— 一种时间旅行调试功能是非常有价值的。 #### 端到端原则重现 -如果我们不能完全相信系统的每个组件都不会损坏 —— 每一个硬件都没缺陷,每一个软件都没有Bug —— 那我们至少必须定期检查数据的完整性。如果我们不检查,我们就不能发现损坏,直到无可挽回地导致对下游的破坏时,那时候再去追踪问题就要难得多,且代价也要高的多。 +如果我们不能完全相信系统的每个组件都不会损坏 —— 每一个硬件都没缺陷,每一个软件都没有 Bug —— 那我们至少必须定期检查数据的完整性。如果我们不检查,我们就不能发现损坏,直到无可挽回地导致对下游的破坏时,那时候再去追踪问题就要难得多,且代价也要高的多。 -检查数据系统的完整性,最好是以端到端的方式进行(请参阅“[数据库的端到端原则](#数据库的端到端原则)”):我们能在完整性检查中涵盖的系统越多,某些处理阶中出现不被察觉损坏的几率就越小。如果我们能检查整个衍生数据管道端到端的正确性,那么沿着这一路径的任何磁盘、网络、服务以及算法的正确性检查都隐含在其中了。 +检查数据系统的完整性,最好是以端到端的方式进行(请参阅 “[数据库的端到端原则](#数据库的端到端原则)”):我们能在完整性检查中涵盖的系统越多,某些处理阶中出现不被察觉损坏的几率就越小。如果我们能检查整个衍生数据管道端到端的正确性,那么沿着这一路径的任何磁盘、网络、服务以及算法的正确性检查都隐含在其中了。 持续的端到端完整性检查可以不断提高你对系统正确性的信心,从而使你能更快地进步【70】。与自动化测试一样,审计提高了快速发现错误的可能性,从而降低了系统变更或新存储技术可能导致损失的风险。如果你不害怕进行变更,就可以更好地充分演化一个应用,使其满足不断变化的需求。 @@ -705,13 +705,13 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 目前,将可审计性作为顶层关注点的数据系统并不多。一些应用实现了自己的审计机制,例如将所有变更记录到单独的审计表中,但是确保审计日志与数据库状态的完整性仍然是很困难的。可以定期使用硬件安全模块对事务日志进行签名来防止篡改,但这无法保证正确的事务一开始就能进入到日志中。 -使用密码学工具来证明系统的完整性是十分有趣的,这种方式对于宽泛的硬件与软件问题,甚至是潜在的恶意行为都很稳健有效。加密货币,区块链,以及诸如比特币,以太坊,Ripple,Stellar的分布式账本技术已经迅速出现在这一领域【71,72,73】。 +使用密码学工具来证明系统的完整性是十分有趣的,这种方式对于宽泛的硬件与软件问题,甚至是潜在的恶意行为都很稳健有效。加密货币、区块链、以及诸如比特币、以太坊、Ripple、Stellar 的分布式账本技术已经迅速出现在这一领域【71,72,73】。 我没有资格评论这些技术用于货币,或者合同商定机制的价值。但从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型与事务机制,而不同副本可以由互不信任的组织托管。副本不断检查其他副本的完整性,并使用共识协议对应当执行的事务达成一致。 -我对这些技术的拜占庭容错方面有些怀疑(请参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明(proof of work)** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管更多是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。 +我对这些技术的拜占庭容错方面有些怀疑(请参阅 “[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现 **工作证明(proof of work)** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管更多是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。 -密码学审计与完整性检查通常依赖**默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)** 也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性【75,76】。 +密码学审计与完整性检查通常依赖 **默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)** 也是一种依赖 Merkle 树的安全技术,用来检查 TLS/SSL 证书的有效性【75,76】。 我可以想象,那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越广泛的应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要一些工作。 但我认为这是一个值得关注的有趣领域。 @@ -724,25 +724,25 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 我们将数据当成一种抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。对待这些数据,我们必须怀着人性与尊重。用户也是人类,人类的尊严是至关重要的。 -软件开发越来越多地涉及重要的道德抉择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范与专业实践【77】,但实践中很少会讨论这些,更不用说应用与强制执行了。因此,工程师和产品经理有时会对隐私与产品潜在的负面后果抱有非常傲慢的态度【78,79,80】。 +软件开发越来越多地涉及重要的道德抉择。有一些指导原则可以帮助软件工程师解决这些问题,例如 ACM 的软件工程道德规范与专业实践【77】,但实践中很少会讨论这些,更不用说应用与强制执行了。因此,工程师和产品经理有时会对隐私与产品潜在的负面后果抱有非常傲慢的态度【78,79,80】。 技术本身并无好坏之分 —— 关键在于它被如何使用,以及它如何影响人们。这对枪械这样的武器是成立的,而搜索引擎这样的软件系统与之类似。我认为,软件工程师仅仅专注于技术而忽视其后果是不够的:道德责任也是我们的责任。对道德推理很困难,但它太重要了,我们无法忽视。 ### 预测性分析 -举个例子,预测性分析是“大数据”炒作的主要内容之一。使用数据分析预测天气或疾病传播是一码事【81】;而预测一个罪犯是否可能再犯,一个贷款申请人是否有可能违约,或者一个保险客户是否可能进行昂贵的索赔,则是另外一码事。后者会直接影响到个人的生活。 +举个例子,预测性分析是 “大数据” 炒作的主要内容之一。使用数据分析预测天气或疾病传播是一码事【81】;而预测一个罪犯是否可能再犯,一个贷款申请人是否有可能违约,或者一个保险客户是否可能进行昂贵的索赔,则是另外一码事。后者会直接影响到个人的生活。 -当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫机,公司希望避免雇佣效率低下或不值得信任的人。从它们的角度来看,失去商机的成本很低,而不良贷款或问题员工的成本则要高得多,因而组织希望保持谨慎也是自然而然的事情。所以如果存疑,它们通常会Say No。 +当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫机,公司希望避免雇佣效率低下或不值得信任的人。从它们的角度来看,失去商机的成本很低,而不良贷款或问题员工的成本则要高得多,因而组织希望保持谨慎也是自然而然的事情。所以如果存疑,它们通常会 Say No。 -然而,随着算法决策变得越来越普遍,被某种算法(准确地或错误地)标记为有风险的某人可能会遭受大量这种“No”的决定。系统性地被排除在工作,航旅,保险,租赁,金融服务,以及其他社会关键领域之外。这是一种对个体自由的极大约束,因此被称为“算法监狱”【82】。在尊重人权的国家,刑事司法系统会做无罪推定(默认清白,直到被证明有罪)。另一方面,自动化系统可以系统地,任意地将一个人排除在社会参与之外,不需要任何有罪的证明,而且几乎没有申诉的机会。 +然而,随着算法决策变得越来越普遍,被某种算法(准确地或错误地)标记为有风险的某人可能会遭受大量这种 “No” 的决定。系统性地被排除在工作,航旅,保险,租赁,金融服务,以及其他社会关键领域之外。这是一种对个体自由的极大约束,因此被称为 “算法监狱”【82】。在尊重人权的国家,刑事司法系统会做无罪推定(默认清白,直到被证明有罪)。另一方面,自动化系统可以系统地,任意地将一个人排除在社会参与之外,不需要任何有罪的证明,而且几乎没有申诉的机会。 #### 偏见与歧视 算法做出的决定不一定比人类更好或更差。每个人都可能有偏见,即使他们主动抗拒这一点;而歧视性做法也可能已经在文化上被制度化了。人们希望根据数据做出决定,而不是通过人的主观评价与直觉,希望这样能更加公平,并给予传统体制中经常被忽视的人更好的机会【83】。 -当我们开发预测性分析系统时,不是仅仅用软件通过一系列IF ELSE规则将人类的决策过程自动化,那些规则本身甚至都是从数据中推断出来的。但这些系统学到的模式是个黑盒:即使数据中存在一些相关性,我们可能也压根不知道为什么。如果算法的输入中存在系统性的偏见,则系统很有可能会在输出中学习并放大这种偏见【84】。 +当我们开发预测性分析系统时,不是仅仅用软件通过一系列 IF ELSE 规则将人类的决策过程自动化,那些规则本身甚至都是从数据中推断出来的。但这些系统学到的模式是个黑盒:即使数据中存在一些相关性,我们可能也压根不知道为什么。如果算法的输入中存在系统性的偏见,则系统很有可能会在输出中学习并放大这种偏见【84】。 -在许多国家,反歧视法律禁止按种族、年龄、性别、性取向、残疾或信仰等受保护的特征区分对待不同的人。其他的个人特征可能是允许用于分析的,但是如果这些特征与受保护的特征存在关联,又会发生什么?例如在种族隔离地区中,一个人的邮政编码,甚至是他们的IP地址,都是很强的种族指示物。这样的话,相信一种算法可以以某种方式将有偏见的数据作为输入,并产生公平和公正的输出【85】似乎是很荒谬的。然而这种观点似乎常常潜伏在数据驱动型决策的支持者中,这种态度被讽刺为“在处理偏差上,机器学习与洗钱类似”(machine learning is like money laundering for bias)【86】。 +在许多国家,反歧视法律禁止按种族、年龄、性别、性取向、残疾或信仰等受保护的特征区分对待不同的人。其他的个人特征可能是允许用于分析的,但是如果这些特征与受保护的特征存在关联,又会发生什么?例如在种族隔离地区中,一个人的邮政编码,甚至是他们的 IP 地址,都是很强的种族指示物。这样的话,相信一种算法可以以某种方式将有偏见的数据作为输入,并产生公平和公正的输出【85】似乎是很荒谬的。然而这种观点似乎常常潜伏在数据驱动型决策的支持者中,这种态度被讽刺为 “在处理偏差上,机器学习与洗钱类似”(machine learning is like money laundering for bias)【86】。 预测性分析系统只是基于过去进行推断;如果过去是歧视性的,它们就会将这种歧视归纳为规律。如果我们希望未来比过去更好,那么就需要道德想象力,而这是只有人类才能提供的东西【87】。数据与模型应该是我们的工具,而不是我们的主人。 @@ -750,11 +750,11 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 自动决策引发了关于责任与问责的问题【87】。如果一个人犯了错误,他可以被追责,受决定影响的人可以申诉。算法也会犯错误,但是如果它们出错,谁来负责【88】?当一辆自动驾驶汽车引发事故时,谁来负责?如果自动信用评分算法系统性地歧视特定种族或宗教的人,这些人是否有任何追索权?如果机器学习系统的决定要受到司法审查,你能向法官解释算法是如何做出决定的吗? -收集关于人的数据并进行决策,信用评级机构是一个很经典的例子。不良的信用评分会使生活变得更艰难,但至少信用分通常是基于个人**实际的**借款历史记录,而记录中的任何错误都能被纠正(尽管机构通常会设置门槛)。然而,基于机器学习的评分算法通常会使用更宽泛的输入,并且更不透明;因而很难理解特定决策是怎样作出的,以及是否有人被不公正地,歧视性地对待【89】。 +收集关于人的数据并进行决策,信用评级机构是一个很经典的例子。不良的信用评分会使生活变得更艰难,但至少信用分通常是基于个人 **实际的** 借款历史记录,而记录中的任何错误都能被纠正(尽管机构通常会设置门槛)。然而,基于机器学习的评分算法通常会使用更宽泛的输入,并且更不透明;因而很难理解特定决策是怎样作出的,以及是否有人被不公正地,歧视性地对待【89】。 -信用分总结了“你过去的表现如何?”,而预测性分析通常是基于“谁与你类似,以及与你类似的人过去表现的如何?”。与他人的行为画上等号意味着刻板印象,例如,根据他们居住的地方(与种族和阶级关系密切的特征)。那么那些放错位置的人怎么办?而且,如果是因为错误数据导致的错误决定,追索几乎是不可能的【87】。 +信用分总结了 “你过去的表现如何?”,而预测性分析通常是基于 “谁与你类似,以及与你类似的人过去表现的如何?”。与他人的行为画上等号意味着刻板印象,例如,根据他们居住的地方(与种族和阶级关系密切的特征)。那么那些放错位置的人怎么办?而且,如果是因为错误数据导致的错误决定,追索几乎是不可能的【87】。 -很多数据本质上是统计性的,这意味着即使概率分布在总体上是正确的,对于个例也可能是错误的。例如,如果贵国的平均寿命是80岁,这并不意味着你在80岁生日时就会死掉。很难从平均值与概率分布中对某个特定个体的寿命作出什么判断,同样,预测系统的输出是概率性的,对于个例可能是错误的。 +很多数据本质上是统计性的,这意味着即使概率分布在总体上是正确的,对于个例也可能是错误的。例如,如果贵国的平均寿命是 80 岁,这并不意味着你在 80 岁生日时就会死掉。很难从平均值与概率分布中对某个特定个体的寿命作出什么判断,同样,预测系统的输出是概率性的,对于个例可能是错误的。 盲目相信数据决策至高无上,这不仅仅是一种妄想,而是有切实危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚,如何使算法更负责任且更加透明,如何避免加强现有的偏见,以及如何在它们不可避免地出错时加以修复。 @@ -762,11 +762,11 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 #### 反馈循环 -即使是那些对人直接影响比较小的预测性应用,比如推荐系统,也有一些必须正视的难题。当服务变得善于预测用户想要看到什么内容时,它最终可能只会向人们展示他们已经同意的观点,将人们带入滋生刻板印象,误导信息,与极端思想的**回音室**。我们已经看到过社交媒体回音室对竞选的影响了【91】。 +即使是那些对人直接影响比较小的预测性应用,比如推荐系统,也有一些必须正视的难题。当服务变得善于预测用户想要看到什么内容时,它最终可能只会向人们展示他们已经同意的观点,将人们带入滋生刻板印象,误导信息,与极端思想的 **回音室**。我们已经看到过社交媒体回音室对竞选的影响了【91】。 当预测性分析影响人们的生活时,自我强化的反馈循环会导致非常有害的问题。例如,考虑雇主使用信用分来评估候选人的例子。你可能是一个信用分不错的好员工,但因不可抗力的意外而陷入财务困境。由于不能按期付账单,你的信用分会受到影响,进而导致找到工作更为困难。失业使你陷入贫困,这进一步恶化了你的分数,使你更难找到工作【87】。在数据与数学严谨性的伪装背后,隐藏的是由恶毒假设导致的恶性循环。 -我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为**系统思维(systems thinking)** 的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和增大了人们之间现有的差异(例如,损不足以奉有余,富者愈富,贫者愈贫),还是试图与不公作斗争?而且即使有着最好的动机,我们也必须当心意想不到的后果。 +我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为 **系统思维(systems thinking)** 的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和增大了人们之间现有的差异(例如,损不足以奉有余,富者愈富,贫者愈贫),还是试图与不公作斗争?而且即使有着最好的动机,我们也必须当心意想不到的后果。 ### 隐私和追踪 @@ -774,7 +774,7 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 当系统只存储用户明确输入的数据时,是因为用户希望系统以特定方式存储和处理这些数据,**系统是在为用户提供服务**:用户就是客户。但是,当用户的活动被跟踪并记录,作为他们正在做的其他事情的副作用时,这种关系就没有那么清晰了。该服务不再仅仅完成用户想要它要做的事情,而是服务于它自己的利益,而这可能与用户的利益相冲突。 -追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要:追踪用户点击了哪些搜索结果有助于改善搜索结果的排名;推荐“喜欢X的人也喜欢Y”,可以帮助用户发现实用有趣的东西; A/B测试和用户流量分析有助于改善用户界面。这些功能需要一定量的用户行为跟踪,而用户也可以从中受益。 +追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要:追踪用户点击了哪些搜索结果有助于改善搜索结果的排名;推荐 “喜欢 X 的人也喜欢 Y”,可以帮助用户发现实用有趣的东西; A/B 测试和用户流量分析有助于改善用户界面。这些功能需要一定量的用户行为跟踪,而用户也可以从中受益。 但不同公司有着不同的商业模式,追踪并未止步于此。如果服务是通过广告盈利的,那么广告主才是真正的客户,而用户的利益则屈居其次。跟踪的数据会变得更详细,分析变得更深入,数据会保留很长时间,以便为每个人建立详细画像,用于营销。 @@ -782,9 +782,9 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 #### 监视 -让我们做一个思想实验,尝试用**监视(surveillance)** 一词替换**数据(data)**,再看看常见的短语是不是听起来还那么漂亮【93】。比如:“在我们的监视驱动的组织中,我们收集实时监视流并将它们存储在我们的监视仓库中。我们的监视科学家使用高级分析和监视处理来获得新的见解。” +让我们做一个思想实验,尝试用 **监视(surveillance)** 一词替换 **数据(data)**,再看看常见的短语是不是听起来还那么漂亮【93】。比如:“在我们的监视驱动的组织中,我们收集实时监视流并将它们存储在我们的监视仓库中。我们的监视科学家使用高级分析和监视处理来获得新的见解。” -对于本书《设计监控密集型应用》而言,这个思想实验是罕见的争议性内容,但我认为需要激烈的言辞来强调这一点。在我们尝试制造软件“吞噬世界”的过程中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大规模监视基础设施。我们正朝着万物互联迈进,我们正在迅速走近这样一个世界:每个有人居住的空间至少包含一个带互联网连接的麦克风,以智能手机、智能电视、语音控制助理设备、婴儿监视器甚至儿童玩具的形式存在,并使用基于云的语音识别。这些设备中的很多都有着可怕的安全记录【95】。 +对于本书《设计监控密集型应用》而言,这个思想实验是罕见的争议性内容,但我认为需要激烈的言辞来强调这一点。在我们尝试制造软件 “吞噬世界” 的过程中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大规模监视基础设施。我们正朝着万物互联迈进,我们正在迅速走近这样一个世界:每个有人居住的空间至少包含一个带互联网连接的麦克风,以智能手机、智能电视、语音控制助理设备、婴儿监视器甚至儿童玩具的形式存在,并使用基于云的语音识别。这些设备中的很多都有着可怕的安全记录【95】。 即使是最为极权与专制的政权,可能也只会想着在每个房间装一个麦克风,并强迫每个人始终携带能够追踪其位置与动向的设备。然而,我们显然是自愿地,甚至热情地投身于这个全域监视的世界。不同之处在于,数据是由公司,而不是由政府机构收集的【96】。 @@ -794,29 +794,29 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 #### 同意与选择的自由 -我们可能会断言用户是自愿选择使用了会跟踪其活动的服务,而且他们已经同意了服务条款与隐私政策,因此他们同意数据收集。我们甚至可以声称,用户在用所提供的数据来**换取**有价值的服务,并且为了提供服务,追踪是必要的。毫无疑问,社交网络、搜索引擎以及各种其他免费的在线服务对于用户来说都是有价值的,但是这个说法却存在问题。 +我们可能会断言用户是自愿选择使用了会跟踪其活动的服务,而且他们已经同意了服务条款与隐私政策,因此他们同意数据收集。我们甚至可以声称,用户在用所提供的数据来 **换取** 有价值的服务,并且为了提供服务,追踪是必要的。毫无疑问,社交网络、搜索引擎以及各种其他免费的在线服务对于用户来说都是有价值的,但是这个说法却存在问题。 用户几乎不知道他们提供给我们的是什么数据,哪些数据被放进了数据库,数据又是怎样被保留与处理的 —— 大多数隐私政策都是模棱两可的,忽悠用户而不敢打开天窗说亮话。如果用户不了解他们的数据会发生什么,就无法给出任何有意义的同意。有时来自一个用户的数据还会提到一些关于其他人的事,而其他那些人既不是该服务的用户,也没有同意任何条款。我们在本书这一部分中讨论的衍生数据集 —— 来自整个用户群的数据,加上行为追踪与外部数据源 —— 就恰好是用户无法(在真正意义上)理解的数据类型。 而且从用户身上挖掘数据是一个单向过程,而不是真正的互惠关系,也不是公平的价值交换。用户对能用多少数据换来什么样的服务,既没有没有发言权也没有选择权:服务与用户之间的关系是非常不对称与单边的。这些条款是由服务提出的,而不是由用户提出的【99】。 -对于不同意监视的用户,唯一真正管用的备选项,就是简单地不使用服务。但这个选择也不是真正自由的:如果一项服务如此受欢迎,以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 —— 使用它**事实上(de facto)** 是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交,以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择**不**使用会产生社会成本。 +对于不同意监视的用户,唯一真正管用的备选项,就是简单地不使用服务。但这个选择也不是真正自由的:如果一项服务如此受欢迎,以至于 “被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 —— 使用它 **事实上(de facto)** 是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用 Facebook 进行社交,以及使用 Google 查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择 **不** 使用会产生社会成本。 因为一个服务会跟踪用户而拒绝使用它,这只是少数人才拥有的权力,他们有足够的时间与知识来了解隐私政策,并承受得起代价:错过社会参与,以及使用服务可能带来的专业机会。对于那些处境不太好的人而言,并没有真正意义上的选择:监控是不可避免的。 #### 隐私与数据使用 -有时候,人们声称“隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对**隐私(privacy)** 一词的误解。 +有时候,人们声称 “隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对 **隐私(privacy)** 一词的误解。 拥有隐私并不意味着保密一切东西;它意味着拥有选择向谁展示哪些东西的自由,要公开什么,以及要保密什么。**隐私权是一项决定权**:在从保密到透明的光谱上,隐私使得每个人都能决定自己想要在什么地方位于光谱上的哪个位置【99】。这是一个人自由与自主的重要方面。 -当通过监控基础设施从人身上提取数据时,隐私权不一定受到损害,而是转移到了数据收集者手中。获取数据的公司实际上是说“相信我们会用你的数据做正确的事情”,这意味着,决定要透露什么和保密什么的权利从个体手中转移到了公司手中。 +当通过监控基础设施从人身上提取数据时,隐私权不一定受到损害,而是转移到了数据收集者手中。获取数据的公司实际上是说 “相信我们会用你的数据做正确的事情”,这意味着,决定要透露什么和保密什么的权利从个体手中转移到了公司手中。 这些公司反过来选择保密这些监视结果,因为揭露这些会令人毛骨悚然,并损害它们的商业模式(比其他公司更了解人)。用户的私密信息只会间接地披露,例如针对特定人群定向投放广告的工具(比如那些患有特定疾病的人群)。 -即使特定用户无法从特定广告定向的人群中以个体的形式区分出来,但他们已经失去了披露一些私密信息的能动性,例如他们是否患有某种疾病。决定向谁透露什么并不是由个体按照自己的喜好决定的,而是由**公司**,以利润最大化为目标来行使隐私权的。 +即使特定用户无法从特定广告定向的人群中以个体的形式区分出来,但他们已经失去了披露一些私密信息的能动性,例如他们是否患有某种疾病。决定向谁透露什么并不是由个体按照自己的喜好决定的,而是由 **公司**,以利润最大化为目标来行使隐私权的。 -许多公司都有一个目标,不要让人**感觉到**毛骨悚然 —— 先不说它们收集数据实际上是多么具有侵犯性,让我们先关注对用户感受的管理。这些用户感受经常被管理得很糟糕:例如,在事实上可能正确的一些东西,如果会触发痛苦的回忆,用户可能并不希望被提醒【100】。对于任何类型的数据,我们都应当考虑它出错、不可取、不合时宜的可能性,并且需要建立处理这些失效的机制。无论是“不可取”还是“不合时宜”,当然都是由人的判断决定的;除非我们明确地将算法编码设计为尊重人类的需求,否则算法会无视这些概念。作为这些系统的工程师,我们必须保持谦卑,充分规划,接受这些失效。 +许多公司都有一个目标,不要让人 **感觉到** 毛骨悚然 —— 先不说它们收集数据实际上是多么具有侵犯性,让我们先关注对用户感受的管理。这些用户感受经常被管理得很糟糕:例如,在事实上可能正确的一些东西,如果会触发痛苦的回忆,用户可能并不希望被提醒【100】。对于任何类型的数据,我们都应当考虑它出错、不可取、不合时宜的可能性,并且需要建立处理这些失效的机制。无论是 “不可取” 还是 “不合时宜”,当然都是由人的判断决定的;除非我们明确地将算法编码设计为尊重人类的需求,否则算法会无视这些概念。作为这些系统的工程师,我们必须保持谦卑,充分规划,接受这些失效。 允许在线服务的用户控制其隐私设置,例如控制其他用户可以看到哪些东西,是将一些控制交还给用户的第一步。但无论怎么设置,服务本身仍然可以不受限制地访问数据,并能以隐私策略允许的任何方式自由使用它。即使服务承诺不会将数据出售给第三方,它通常会授予自己不受限制的权利,以便在内部处理与分析数据,而且往往比用户公开可见的部分要深入的多。 @@ -824,7 +824,7 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 #### 数据资产与权力 -由于行为数据是用户与服务交互的副产品,因此有时被称为“数据废气” —— 暗示数据是毫无价值的废料。从这个角度来看,行为和预测性分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据就会被浪费。 +由于行为数据是用户与服务交互的副产品,因此有时被称为 “数据废气” —— 暗示数据是毫无价值的废料。从这个角度来看,行为和预测性分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据就会被浪费。 更准确的看法恰恰相反:从经济的角度来看,如果定向广告是服务的金主,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段【99】。在线服务中经常表现出的令人愉悦的人类创造力与社会关系,十分讽刺地被数据提取机器所滥用。 @@ -832,7 +832,7 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 因为数据很有价值,所以很多人都想要它。当然,公司也想要它 —— 这就是为什么它们一开始就收集数据的原因。但政府也想获得它:通过秘密交易、胁迫、法律强制或者只是窃取【101】。当公司破产时,收集到的个人数据就是被出售的资产之一。而且数据安全很难保护,因此经常发生令人难堪的泄漏事件【102】。 -这些观察已经导致批评者声称,数据不仅仅是一种资产,而且是一种“有毒资产”【101】,或者至少是“有害物质”【103】。即使我们认为自己有能力阻止数据滥用,但每当我们收集数据时,我们都需要平衡收益以及这些数据落入恶人手中的风险:计算机系统可能会被犯罪分子或敌国特务渗透,数据可能会被内鬼泄露,公司可能会落入不择手段的管理层手中,而这些管理者有着迥然不同的价值观,或者国家可能被能毫无愧色迫使我们交出数据的政权所接管。 +这些观察已经导致批评者声称,数据不仅仅是一种资产,而且是一种 “有毒资产”【101】,或者至少是 “有害物质”【103】。即使我们认为自己有能力阻止数据滥用,但每当我们收集数据时,我们都需要平衡收益以及这些数据落入恶人手中的风险:计算机系统可能会被犯罪分子或敌国特务渗透,数据可能会被内鬼泄露,公司可能会落入不择手段的管理层手中,而这些管理者有着迥然不同的价值观,或者国家可能被能毫无愧色迫使我们交出数据的政权所接管。 俗话说,“知识就是力量”。更进一步,“在避免自己被审视的同时审视他人,是权力最重要的形式之一”【105】。这就是极权政府想要监控的原因:这让它们有能力控制全体居民。尽管今天的科技公司并没有公开地寻求政治权力,但是它们积累的数据与知识却给它们带来了很多权力,其中大部分是在公共监督之外偷偷进行的【106】。 @@ -844,7 +844,7 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 制定保护措施花费了很长的时间,例如环境保护条例、工作场所安全条例、宣布使用童工非法以及食品卫生检查。毫无疑问,生产成本增加了,因为工厂再也不能把废物倒入河流、销售污染的食物或者剥削工人。但是整个社会都从中受益良多,我们中很少会有人想回到这些管制条例之前的日子【87】。 -就像工业革命有着黑暗面需要应对一样,我们转向信息时代的过程中,也有需要应对与解决的重大问题。我相信数据的收集与使用就是其中一个问题。用Bruce Schneier的话来说【96】: +就像工业革命有着黑暗面需要应对一样,我们转向信息时代的过程中,也有需要应对与解决的重大问题。我相信数据的收集与使用就是其中一个问题。用 Bruce Schneier 的话来说【96】: > 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生产信息。它堆积在周围,开始溃烂。我们如何处理它 —— 我们如何控制它,以及如何摆脱它 —— 是信息经济健康发展的核心议题。正如我们今天回顾工业时代的早期年代,并想知道我们的祖先在忙于建设工业世界的过程时怎么能忽略污染问题;我们的孙辈在回望信息时代的早期年代时,将会就我们如何应对数据收集和滥用的挑战来评断我们。 > @@ -852,9 +852,9 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 #### 立法与自律 -数据保护法可能有助于维护个人的权利。例如,1995年的“欧洲数据保护指示”规定,个人数据必须“为特定的、明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须“就收集的目的而言适当、相关、不过分。“【107】。 +数据保护法可能有助于维护个人的权利。例如,1995 年的 “欧洲数据保护指示” 规定,个人数据必须 “为特定的、明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须 “就收集的目的而言适当、相关、不过分。“【107】。 -但是,这个立法在今天的互联网环境下是否有效还是有疑问的【108】。这些规则直接否定了大数据的哲学,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的洞察。探索意味着将数据用于未曾预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地表示同意的话)【109】。更新的规章正在制定中【89】。 +但是,这个立法在今天的互联网环境下是否有效还是有疑问的【108】。这些规则直接否定了大数据的哲学,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的洞察。探索意味着将数据用于未曾预期的目的,这与用户同意的 “特定和明确” 目的相反(如果我们可以有意义地表示同意的话)【109】。更新的规章正在制定中【89】。 那些收集了大量有关人的数据的公司反对监管,认为这是创新的负担与阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,存在明显的隐私风险,但也有潜在的机遇:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,能够阻止多少人的死亡【110】?过度监管可能会阻止这种突破。在这种潜在机会与风险之间找出平衡是很困难的【105】。 @@ -862,12 +862,12 @@ ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基 我们应该允许每个人保留自己的隐私 —— 即,对自己数据的控制 —— 而不是通过监视来窃取这种控制权。我们控制自己数据的个体权利就像是国家公园的自然环境:如果我们不去明确地保护它、关心它,它就会被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视并非不可避免的 —— 我们现在仍然能阻止它。 -我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅“[不变性的局限性](ch11.md#不变性的局限性)”),但这是可以解决的问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化与态度的改变是必要的。 +我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅 “[不变性的局限性](ch11.md#不变性的局限性)”),但这是可以解决的问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化与态度的改变是必要的。 ## 本章小结 -在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一**数据集成(data integration)** 问题,以便让数据变更在不同系统之间流动。 +在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一 **数据集成(data integration)** 问题,以便让数据变更在不同系统之间流动。 在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换衍生自记录系统。通过这种方式,我们可以维护索引,物化视图,机器学习模型,统计摘要等等。通过使这些衍生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。 diff --git a/ch8.md b/ch8.md index bc5e866..e3db47c 100644 --- a/ch8.md +++ b/ch8.md @@ -1,4 +1,4 @@ -# 第八章:分布式系统的麻烦 +# 第八章:分布式系统的麻烦 ![](img/ch8.png) @@ -10,40 +10,40 @@ > > 无食我数 > -> —— Kyle Kingsbury, Carly Rae Jepsen 《网络分区的危害》(2013年)[^译著1] +> —— Kyle Kingsbury, Carly Rae Jepsen 《网络分区的危害》(2013 年)[^译著1] --------- [TOC] -最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了**副本故障切换**(“[处理节点中断](ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch5.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。 +最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了 **副本故障切换**(“[处理节点中断](ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch5.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。 -但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西**都会**出错[^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事) +但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西 **都会** 出错 [^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事) -[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(请参阅“[拜占庭故障](#拜占庭故障)”)。 +[^i]: 除了一个例外:我们将假定故障是非拜占庭式的(请参阅 “[拜占庭故障](#拜占庭故障)”)。 使用分布式系统与在一台计算机上编写软件有着根本的区别,主要的区别在于,有许多新颖和刺激的方法可以使事情出错【1,2】。在这一章中,我们将了解实践中出现的问题,理解我们能够依赖,和不可以依赖的东西。 -最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在[第九章](ch9.md)中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。 +最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。 在 [第九章](ch9.md) 中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。 但首先,在本章中,我们必须了解我们面临的挑战。 本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。 我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。 所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。 ## 故障与部分失效 -当你在一台计算机上编写一个程序时,它通常会以一种相当可预测的方式运行:无论是工作还是不工作。充满错误的软件可能会让人觉得电脑有时候也会有“糟糕的一天”(这种问题通常是重新启动就恢复了),但这主要是软件写得不好的结果。 +当你在一台计算机上编写一个程序时,它通常会以一种相当可预测的方式运行:无论是工作还是不工作。充满错误的软件可能会让人觉得电脑有时候也会有 “糟糕的一天”(这种问题通常是重新启动就恢复了),但这主要是软件写得不好的结果。 单个计算机上的软件没有根本性的不可靠原因:当硬件正常工作时,相同的操作总是产生相同的结果(这是确定性的)。如果存在硬件问题(例如,内存损坏或连接器松动),其后果通常是整个系统故障(例如,内核恐慌,“蓝屏死机”,启动失败)。装有良好软件的个人计算机通常要么功能完好,要么完全失效,而不是介于两者之间。 -这是计算机设计中的一个有意的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。 CPU指令总是做同样的事情;如果你将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算*这个设计目标贯穿始终【3】。 +这是计算机设计中的一个有意的选择:如果发生内部错误,我们宁愿电脑完全崩溃,而不是返回错误的结果,因为错误的结果很难处理。因为计算机隐藏了模糊不清的物理实现,并呈现出一个理想化的系统模型,并以数学一样的完美的方式运作。 CPU 指令总是做同样的事情;如果你将一些数据写入内存或磁盘,那么这些数据将保持不变,并且不会被随机破坏。从第一台数字计算机开始,*始终正确地计算* 这个设计目标贯穿始终【3】。 当你编写运行在多台计算机上的软件时,情况有本质上的区别。在分布式系统中,我们不再处于理想化的系统模型中,我们别无选择,只能面对现实世界的混乱现实。而在现实世界中,各种各样的事情都可能会出现问题【4】,如下面的轶事所述: -> 在我有限的经验中,我已经和很多东西打过交道:单个**数据中心(DC)** 中长期存在的网络分区,配电单元PDU故障,交换机故障,整个机架的意外重启,整个数据中心主干网络故障,整个数据中心的电源故障,以及一个低血糖的司机把他的福特皮卡撞在数据中心的HVAC(加热,通风和空调)系统上。而且我甚至不是一个运维。 +> 在我有限的经验中,我已经和很多东西打过交道:单个 **数据中心(DC)** 中长期存在的网络分区,配电单元 PDU 故障,交换机故障,整个机架的意外重启,整个数据中心主干网络故障,整个数据中心的电源故障,以及一个低血糖的司机把他的福特皮卡撞在数据中心的 HVAC(加热,通风和空调)系统上。而且我甚至不是一个运维。 > > —— 柯达黑尔 -在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为**部分失效(partial failure)**。难点在于部分失效是**不确定性的(nonderterministic)**:如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。正如我们将要看到的,你甚至不知道是否成功了,因为消息通过网络传播的时间也是不确定的! +在分布式系统中,尽管系统的其他部分工作正常,但系统的某些部分可能会以某种不可预知的方式被破坏。这被称为 **部分失效(partial failure)**。难点在于部分失效是 **不确定性的(nonderterministic)**:如果你试图做任何涉及多个节点和网络的事情,它有时可能会工作,有时会出现不可预知的失败。正如我们将要看到的,你甚至不知道是否成功了,因为消息通过网络传播的时间也是不确定的! 这种不确定性和部分失效的可能性,使得分布式系统难以工作【5】。 @@ -51,27 +51,27 @@ 关于如何构建大型计算系统有一系列的哲学: -* 一个极端是高性能计算(HPC)领域。具有数千个CPU的超级计算机通常用于计算密集型科学计算任务,如天气预报或分子动力学(模拟原子和分子的运动)。 -* 另一个极端是**云计算(cloud computing)**,云计算并不是一个良好定义的概念【6】,但通常与多租户数据中心,连接IP网络(通常是以太网)的商用计算机,弹性/按需资源分配以及计量计费等相关联。 +* 一个极端是高性能计算(HPC)领域。具有数千个 CPU 的超级计算机通常用于计算密集型科学计算任务,如天气预报或分子动力学(模拟原子和分子的运动)。 +* 另一个极端是 **云计算(cloud computing)**,云计算并不是一个良好定义的概念【6】,但通常与多租户数据中心,连接 IP 网络(通常是以太网)的商用计算机,弹性 / 按需资源分配以及计量计费等相关联。 * 传统企业数据中心位于这两个极端之间。 -不同的哲学会导致不同的故障处理方式。在超级计算机中,作业通常会不时地会将计算的状态存盘到持久存储中。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始【7,8】。因此,超级计算机更像是一个单节点计算机而不是分布式系统:通过让部分失败升级为完全失败来处理部分失败——如果系统的任何部分发生故障,只是让所有的东西都崩溃(就像单台机器上的内核恐慌一样)。 +不同的哲学会导致不同的故障处理方式。在超级计算机中,作业通常会不时地会将计算的状态存盘到持久存储中。如果一个节点出现故障,通常的解决方案是简单地停止整个集群的工作负载。故障节点修复后,计算从上一个检查点重新开始【7,8】。因此,超级计算机更像是一个单节点计算机而不是分布式系统:通过让部分失败升级为完全失败来处理部分失败 —— 如果系统的任何部分发生故障,只是让所有的东西都崩溃(就像单台机器上的内核恐慌一样)。 在本书中,我们将重点放在实现互联网服务的系统上,这些系统通常与超级计算机看起来有很大不同: -* 许多与互联网有关的应用程序都是**在线(online)** 的,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止集群以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。 +* 许多与互联网有关的应用程序都是 **在线(online)** 的,因为它们需要能够随时以低延迟服务用户。使服务不可用(例如,停止集群以进行修复)是不可接受的。相比之下,像天气模拟这样的离线(批处理)工作可以停止并重新启动,影响相当小。 -* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和**远程直接内存访问(RDMA)** 进行通信。另一方面,云服务中的节点是由商用机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。 +* 超级计算机通常由专用硬件构建而成,每个节点相当可靠,节点通过共享内存和 **远程直接内存访问(RDMA)** 进行通信。另一方面,云服务中的节点是由商用机器构建而成的,由于规模经济,可以以较低的成本提供相同的性能,而且具有较高的故障率。 -* 大型数据中心网络通常基于IP和以太网,以CLOS拓扑排列,以提供更高的对分(bisection)带宽【9】。超级计算机通常使用专门的网络拓扑结构,例如多维网格和Torus网络 【10】,这为具有已知通信模式的HPC工作负载提供了更好的性能。 +* 大型数据中心网络通常基于 IP 和以太网,以 CLOS 拓扑排列,以提供更高的对分(bisection)带宽【9】。超级计算机通常使用专门的网络拓扑结构,例如多维网格和 Torus 网络 【10】,这为具有已知通信模式的 HPC 工作负载提供了更好的性能。 * 系统越大,其组件之一就越有可能坏掉。随着时间的推移,坏掉的东西得到修复,新的东西又坏掉,但是在一个有成千上万个节点的系统中,有理由认为总是有一些东西是坏掉的【7】。当错误处理的策略只由简单放弃组成时,一个大的系统最终会花费大量时间从错误中恢复,而不是做有用的工作【8】。 -* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(请参阅[第四章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。 +* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(请参阅 [第四章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。 * 在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们的所有节点都靠近在一起。 -如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统(正如“[可靠性](ch1.md#可靠性)”中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的极限)。 +如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统(正如 “[可靠性](ch1.md#可靠性)” 中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的极限)。 即使在只有少数节点的小型系统中,考虑部分故障也是很重要的。在一个小系统中,很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的运维,你需要知道在发生故障的情况下,软件可能会表现出怎样的行为。 @@ -79,74 +79,74 @@ > #### 从不可靠的组件构建可靠的系统 > -> 你可能想知道这是否有意义——直观地看来,系统只能像其最不可靠的组件(最薄弱的环节)一样可靠。事实并非如此:事实上,从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想【11】。例如: +> 你可能想知道这是否有意义 —— 直观地看来,系统只能像其最不可靠的组件(最薄弱的环节)一样可靠。事实并非如此:事实上,从不太可靠的潜在基础构建更可靠的系统是计算机领域的一个古老思想【11】。例如: > > * 纠错码允许数字数据在通信信道上准确传输,偶尔会出现一些错误,例如由于无线网络上的无线电干扰【12】。 > * **互联网协议(Internet Protocol, IP)** 不可靠:可能丢弃、延迟、重复或重排数据包。 传输控制协议(Transmission Control Protocol, TCP)在互联网协议(IP)之上提供了更可靠的传输层:它确保丢失的数据包被重新传输,消除重复,并且数据包被重新组装成它们被发送的顺序。 > -> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过信道可以得到多少数据,是有根本性的限制的【13】。 TCP可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。 +> 虽然这个系统可以比它的底层部分更可靠,但它的可靠性总是有限的。例如,纠错码可以处理少量的单比特错误,但是如果你的信号被干扰所淹没,那么通过信道可以得到多少数据,是有根本性的限制的【13】。 TCP 可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。 > -> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在“[数据库的端到端原则](ch12.md#数据库的端到端原则)”中进一步探讨这个问题。 +> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在 “[数据库的端到端原则](ch12.md#数据库的端到端原则)” 中进一步探讨这个问题。 ## 不可靠的网络 -正如在[第二部分](part-ii.md)的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径——我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。 +正如在 [第二部分](part-ii.md) 的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径 —— 我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。 -**无共享**并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。 +**无共享** 并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。 -互联网和数据中心(通常是以太网)中的大多数内部网络都是**异步分组网络(asynchronous packet networks)**。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果你发送请求并期待响应,则很多事情可能会出错(其中一些如[图8-1](img/fig8-1.png)所示): +互联网和数据中心(通常是以太网)中的大多数内部网络都是 **异步分组网络(asynchronous packet networks)**。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果你发送请求并期待响应,则很多事情可能会出错(其中一些如 [图 8-1](img/fig8-1.png) 所示): 1. 请求可能已经丢失(可能有人拔掉了网线)。 2. 请求可能正在排队,稍后将交付(也许网络或接收方过载)。 3. 远程节点可能已经失效(可能是崩溃或关机)。 -4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;请参阅“[进程暂停](#进程暂停)”),但稍后会再次响应。 +4. 远程节点可能暂时停止了响应(可能会遇到长时间的垃圾回收暂停;请参阅 “[进程暂停](#进程暂停)”),但稍后会再次响应。 5. 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。 6. 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。 ![](img/fig8-1.png) -**图8-1 如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失。** +**图 8-1 如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失。** 发送者甚至不能分辨数据包是否被发送:唯一的选择是让接收者发送响应消息,这可能会丢失或延迟。这些问题在异步网络中难以区分:你所拥有的唯一信息是,你尚未收到响应。如果你向另一个节点发送请求并且没有收到响应,则不可能判断是什么原因。 -处理这个问题的通常方法是**超时(Timeout)**:在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求(如果请求仍然在某个地方排队,那么即使发送者已经放弃了该请求,仍然可能会将其发送给接收者)。 +处理这个问题的通常方法是 **超时(Timeout)**:在一段时间之后放弃等待,并且认为响应不会到达。但是,当发生超时时,你仍然不知道远程节点是否收到了请求(如果请求仍然在某个地方排队,那么即使发送者已经放弃了该请求,仍然可能会将其发送给接收者)。 ### 真实世界的网络故障 -我们几十年来一直在建设计算机网络——有人可能希望现在我们已经找出了使网络变得可靠的方法。但是现在似乎还没有成功。 +我们几十年来一直在建设计算机网络 —— 有人可能希望现在我们已经找出了使网络变得可靠的方法。但是现在似乎还没有成功。 -有一些系统的研究和大量的轶事证据表明,即使在像一家公司运营的数据中心那样的受控环境中,网络问题也可能出乎意料地普遍。在一家中型数据中心进行的一项研究发现,每个月大约有12个网络故障,其中一半断开一台机器,一半断开整个机架【15】。另一项研究测量了架顶式交换机,汇聚交换机和负载平衡器等组件的故障率【16】。它发现添加冗余网络设备不会像你所希望的那样减少故障,因为它不能防范人为错误(例如,错误配置的交换机),这是造成中断的主要原因。 +有一些系统的研究和大量的轶事证据表明,即使在像一家公司运营的数据中心那样的受控环境中,网络问题也可能出乎意料地普遍。在一家中型数据中心进行的一项研究发现,每个月大约有 12 个网络故障,其中一半断开一台机器,一半断开整个机架【15】。另一项研究测量了架顶式交换机,汇聚交换机和负载平衡器等组件的故障率【16】。它发现添加冗余网络设备不会像你所希望的那样减少故障,因为它不能防范人为错误(例如,错误配置的交换机),这是造成中断的主要原因。 -诸如EC2之类的公有云服务因频繁的暂态网络故障而臭名昭着【14】,管理良好的私有数据中心网络可能是更稳定的环境。尽管如此,没有人不受网络问题的困扰:例如,交换机软件升级过程中的一个问题可能会引发网络拓扑重构,在此期间网络数据包可能会延迟超过一分钟【17】。鲨鱼可能咬住海底电缆并损坏它们 【18】。其他令人惊讶的故障包括网络接口有时会丢弃所有入站数据包,但是成功发送出站数据包 【19】:仅仅因为网络链接在一个方向上工作,并不能保证它也在相反的方向工作。 +诸如 EC2 之类的公有云服务因频繁的暂态网络故障而臭名昭着【14】,管理良好的私有数据中心网络可能是更稳定的环境。尽管如此,没有人不受网络问题的困扰:例如,交换机软件升级过程中的一个问题可能会引发网络拓扑重构,在此期间网络数据包可能会延迟超过一分钟【17】。鲨鱼可能咬住海底电缆并损坏它们 【18】。其他令人惊讶的故障包括网络接口有时会丢弃所有入站数据包,但是成功发送出站数据包 【19】:仅仅因为网络链接在一个方向上工作,并不能保证它也在相反的方向工作。 > #### 网络分区 > -> 当网络的一部分由于网络故障而被切断时,有时称为**网络分区(network partition)** 或**网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语**网络故障(network fault)**,以避免与[第六章](ch6.md)讨论的存储系统的分区(分片)相混淆。 +> 当网络的一部分由于网络故障而被切断时,有时称为 **网络分区(network partition)** 或 **网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语 **网络故障(network fault)**,以避免与 [第六章](ch6.md) 讨论的存储系统的分区(分片)相混淆。 即使网络故障在你的环境中非常罕见,故障可能发生的事实,意味着你的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法避免的。 -如果网络故障的错误处理没有定义与测试,武断地讲,各种错误可能都会发生:例如,即使网络恢复【20】,集群可能会发生**死锁**,永久无法为请求提供服务,甚至可能会删除所有的数据【21】。如果软件被置于意料之外的情况下,它可能会做出出乎意料的事情。 +如果网络故障的错误处理没有定义与测试,武断地讲,各种错误可能都会发生:例如,即使网络恢复【20】,集群可能会发生 **死锁**,永久无法为请求提供服务,甚至可能会删除所有的数据【21】。如果软件被置于意料之外的情况下,它可能会做出出乎意料的事情。 -处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,你确实需要知道你的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是Chaos Monkey背后的想法;请参阅“[可靠性](ch1.md#可靠性)”)。 +处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,你确实需要知道你的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是 Chaos Monkey 背后的想法;请参阅 “[可靠性](ch1.md#可靠性)”)。 ### 检测故障 许多系统需要自动检测故障节点。例如: -* 负载平衡器需要停止向已死亡的节点转发请求(从轮询列表移出,即out of rotation)。 -* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。 +* 负载平衡器需要停止向已死亡的节点转发请求(从轮询列表移出,即 out of rotation)。 +* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。 不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,你可能会收到一些反馈信息,明确告诉你某些事情没有成功: -* 如果你可以连接到运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送FIN或RST来关闭并重用TCP连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据【22】。 -* 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase就是这么做的【23】。 +* 如果你可以连接到运行节点的机器,但没有进程正在侦听目标端口(例如,因为进程崩溃),操作系统将通过发送 FIN 或 RST 来关闭并重用 TCP 连接。但是,如果节点在处理请求时发生崩溃,则无法知道远程节点实际处理了多少数据【22】。 +* 如果节点进程崩溃(或被管理员杀死),但节点的操作系统仍在运行,则脚本可以通知其他节点有关该崩溃的信息,以便另一个节点可以快速接管,而无需等待超时到期。例如,HBase 就是这么做的【23】。 * 如果你有权访问数据中心网络交换机的管理界面,则可以通过它们检测硬件级别的链路故障(例如,远程机器是否关闭电源)。如果你通过互联网连接,或者如果你处于共享数据中心而无法访问交换机,或者由于网络问题而无法访问管理界面,则排除此选项。 -* 如果路由器确认你尝试连接的IP地址不可用,则可能会使用ICMP目标不可达数据包回复你。但是,路由器不具备神奇的故障检测能力——它受到与网络其他参与者相同的限制。 +* 如果路由器确认你尝试连接的 IP 地址不可用,则可能会使用 ICMP 目标不可达数据包回复你。但是,路由器不具备神奇的故障检测能力 —— 它受到与网络其他参与者相同的限制。 -关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使TCP确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的正确响应【24】。 +关于远程节点关闭的快速反馈很有用,但是你不能指望它。即使 TCP 确认已经传送了一个数据包,应用程序在处理之前可能已经崩溃。如果你想确保一个请求是成功的,你需要应用程序本身的正确响应【24】。 -相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你可能根本就得不到任何回应。你可以重试几次(TCP重试是透明的,但是你也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 +相反,如果出了什么问题,你可能会在堆栈的某个层次上得到一个错误响应,但总的来说,你必须假设你可能根本就得不到任何回应。你可以重试几次(TCP 重试是透明的,但是你也可以在应用程序级别重试),等待超时过期,并且如果在超时时间内没有收到响应,则最终声明节点已经死亡。 ### 超时与无穷的延迟 @@ -154,87 +154,87 @@ 长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短的超时可以更快地检测到故障,但有更高地风险误将一个节点宣布为失效,而该节点实际上只是暂时地变慢了(例如由于节点或网络上的负载峰值)。 -过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在“[知识、真相与谎言](#知识、真相与谎言)”以及[第九章](ch9.md)和[第十一章](ch11.md)中更详细地讨论这个问题。 +过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在 “[知识、真相与谎言](#知识、真相与谎言)” 以及 [第九章](ch9.md) 和 [第十一章](ch11.md) 中更详细地讨论这个问题。 -当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。特别是如果节点实际上没有死亡,只是由于过载导致其响应缓慢;这时将其负载转移到其他节点可能会导致**级联失效**(即cascading failure,表示在极端情况下,所有节点都宣告对方死亡,所有节点都将停止工作)。 +当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。特别是如果节点实际上没有死亡,只是由于过载导致其响应缓慢;这时将其负载转移到其他节点可能会导致 **级联失效**(即 cascading failure,表示在极端情况下,所有节点都宣告对方死亡,所有节点都将停止工作)。 -设想一个虚构的系统,其网络可以保证数据包的最大延迟——每个数据包要么在一段时间内传送,要么丢失,但是传递永远不会比$d$更长。此外,假设你可以保证一个非故障节点总是在一段时间内处理一个请求$r$。在这种情况下,你可以保证每个成功的请求在$2d + r$时间内都能收到响应,如果你在此时间内没有收到响应,则知道网络或远程节点不工作。如果这是成立的,$2d + r$ 会是一个合理的超时设置。 +设想一个虚构的系统,其网络可以保证数据包的最大延迟 —— 每个数据包要么在一段时间内传送,要么丢失,但是传递永远不会比 $d$ 更长。此外,假设你可以保证一个非故障节点总是在一段时间内处理一个请求 $r$。在这种情况下,你可以保证每个成功的请求在 $2d + r$ 时间内都能收到响应,如果你在此时间内没有收到响应,则知道网络或远程节点不工作。如果这是成立的,$2d + r$ 会是一个合理的超时设置。 -不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求(请参阅“[响应时间保证](#响应时间保证)”)。对于故障检测,即使系统大部分时间快速运行也是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡。 +不幸的是,我们所使用的大多数系统都没有这些保证:异步网络具有无限的延迟(即尽可能快地传送数据包,但数据包到达可能需要的时间没有上限),并且大多数服务器实现并不能保证它们可以在一定的最大时间内处理请求(请参阅 “[响应时间保证](#响应时间保证)”)。对于故障检测,即使系统大部分时间快速运行也是不够的:如果你的超时时间很短,往返时间只需要一个瞬时尖峰就可以使系统失衡。 #### 网络拥塞和排队 在驾驶汽车时,由于交通拥堵,道路交通网络的通行时间往往不尽相同。同样,计算机网络上数据包延迟的可变性通常是由于排队【25】: -* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如[图8-2](img/fig8-2.png)所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 -* 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 -* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。 -* TCP执行**流量控制**(flow control,也称为**拥塞避免**,即congestion avoidance,或**背压**,即backpressure),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。 +* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如 [图 8-2](img/fig8-2.png) 所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 +* 当数据包到达目标机器时,如果所有 CPU 内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 +* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用 CPU 内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。 +* TCP 执行 **流量控制**(flow control,也称为 **拥塞避免**,即 congestion avoidance,或 **背压**,即 backpressure),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。 ![](img/fig8-2.png) -**图8-2 如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口1,2和4都试图发送数据包到端口3** +**图 8-2 如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口 1,2 和 4 都试图发送数据包到端口 3** -而且,如果TCP在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。 +而且,如果 TCP 在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。 > #### TCP与UDP > -> 一些对延迟敏感的应用程序,比如视频会议和IP语音(VoIP),使用了UDP而不是TCP。这是在可靠性和和延迟变化之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了网络延迟变化的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 +> 一些对延迟敏感的应用程序,比如视频会议和 IP 语音(VoIP),使用了 UDP 而不是 TCP。这是在可靠性和和延迟变化之间的折衷:由于 UDP 不执行流量控制并且不重传丢失的分组,所以避免了网络延迟变化的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。 > -> 在延迟数据毫无价值的情况下,UDP是一个不错的选择。例如,在VoIP电话呼叫中,可能没有足够的时间重新发送丢失的数据包,并在扬声器上播放数据。在这种情况下,重发数据包没有意义——应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。 (“你能再说一遍吗?声音刚刚断了一会儿。“) +> 在延迟数据毫无价值的情况下,UDP 是一个不错的选择。例如,在 VoIP 电话呼叫中,可能没有足够的时间重新发送丢失的数据包,并在扬声器上播放数据。在这种情况下,重发数据包没有意义 —— 应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。 (“你能再说一遍吗?声音刚刚断了一会儿。“) 所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的变化范围特别大:拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。 -在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和CPU(在虚拟机上运行时)。批处理工作负载(如MapReduce,请参阅[第十章](ch10.md))能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈变化【28,29】。 +在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和 CPU(在虚拟机上运行时)。批处理工作负载(如 MapReduce,请参阅 [第十章](ch10.md))能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况,如果附近的某个人(嘈杂的邻居)正在使用大量资源,则网络延迟可能会发生剧烈变化【28,29】。 -在这种环境下,你只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定**故障检测延迟**与**过早超时风险**之间的适当折衷。 +在这种环境下,你只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定 **故障检测延迟** 与 **过早超时风险** 之间的适当折衷。 -更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器【30】来完成,该检测器在例如Akka和Cassandra 【31】中使用。 TCP的超时重传机制也是以类似的方式工作【27】。 +更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过 Phi Accrual 故障检测器【30】来完成,该检测器在例如 Akka 和 Cassandra 【31】中使用。 TCP 的超时重传机制也是以类似的方式工作【27】。 ### 同步网络与异步网络 -如果我们可以依靠网络来传递一些**最大延迟固定**的数据包,而不是丢弃数据包,那么分布式系统就会简单得多。为什么我们不能在硬件层面上解决这个问题,使网络可靠,使软件不必担心呢? +如果我们可以依靠网络来传递一些 **最大延迟固定** 的数据包,而不是丢弃数据包,那么分布式系统就会简单得多。为什么我们不能在硬件层面上解决这个问题,使网络可靠,使软件不必担心呢? -为了回答这个问题,将数据中心网络与非常可靠的传统固定电话网络(非蜂窝,非VoIP)进行比较是很有趣的:延迟音频帧和掉话是非常罕见的。一个电话需要一个很低的端到端延迟,以及足够的带宽来传输你声音的音频采样数据。在计算机网络中有类似的可靠性和可预测性不是很好吗? +为了回答这个问题,将数据中心网络与非常可靠的传统固定电话网络(非蜂窝,非 VoIP)进行比较是很有趣的:延迟音频帧和掉话是非常罕见的。一个电话需要一个很低的端到端延迟,以及足够的带宽来传输你声音的音频采样数据。在计算机网络中有类似的可靠性和可预测性不是很好吗? -当你通过电话网络拨打电话时,它会建立一个电路:在两个呼叫者之间的整个路线上为呼叫分配一个固定的,有保证的带宽量。这个电路会保持至通话结束【32】。例如,ISDN网络以每秒4000帧的固定速率运行。呼叫建立时,每个帧内(每个方向)分配16位空间。因此,在通话期间,每一方都保证能够每250微秒发送一个精确的16位音频数据【33,34】。 +当你通过电话网络拨打电话时,它会建立一个电路:在两个呼叫者之间的整个路线上为呼叫分配一个固定的,有保证的带宽量。这个电路会保持至通话结束【32】。例如,ISDN 网络以每秒 4000 帧的固定速率运行。呼叫建立时,每个帧内(每个方向)分配 16 位空间。因此,在通话期间,每一方都保证能够每 250 微秒发送一个精确的 16 位音频数据【33,34】。 -这种网络是同步的:即使数据经过多个路由器,也不会受到排队的影响,因为呼叫的16位空间已经在网络的下一跳中保留了下来。而且由于没有排队,网络的最大端到端延迟是固定的。我们称之为**有限延迟(bounded delay)**。 +这种网络是同步的:即使数据经过多个路由器,也不会受到排队的影响,因为呼叫的 16 位空间已经在网络的下一跳中保留了下来。而且由于没有排队,网络的最大端到端延迟是固定的。我们称之为 **有限延迟(bounded delay)**。 #### 我们不能简单地使网络延迟可预测吗? -请注意,电话网络中的电路与TCP连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而TCP连接的数据包**机会性地**使用任何可用的网络带宽。你可以给TCP一个可变大小的数据块(例如,一个电子邮件或一个网页),它会尽可能在最短的时间内传输它。 TCP连接空闲时,不使用任何带宽[^ii]。 +请注意,电话网络中的电路与 TCP 连接有很大不同:电路是固定数量的预留带宽,在电路建立时没有其他人可以使用,而 TCP 连接的数据包 **机会性地** 使用任何可用的网络带宽。你可以给 TCP 一个可变大小的数据块(例如,一个电子邮件或一个网页),它会尽可能在最短的时间内传输它。 TCP 连接空闲时,不使用任何带宽 [^ii]。 -[^ii]: 除了偶尔的keepalive数据包,如果TCP keepalive被启用。 +[^ii]: 除了偶尔的 keepalive 数据包,如果 TCP keepalive 被启用。 -如果数据中心网络和互联网是电路交换网络,那么在建立电路时就可以建立一个受保证的最大往返时间。但是,它们并不是:以太网和IP是**分组交换协议**,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。 +如果数据中心网络和互联网是电路交换网络,那么在建立电路时就可以建立一个受保证的最大往返时间。但是,它们并不是:以太网和 IP 是 **分组交换协议**,不得不忍受排队的折磨,及其导致的网络无限延迟。这些协议没有电路的概念。 -为什么数据中心网络和互联网使用分组交换?答案是,它们针对**突发流量(bursty traffic)** 进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求——我们只是希望它尽快完成。 +为什么数据中心网络和互联网使用分组交换?答案是,它们针对 **突发流量(bursty traffic)** 进行了优化。一个电路适用于音频或视频通话,在通话期间需要每秒传送相当数量的比特。另一方面,请求网页,发送电子邮件或传输文件没有任何特定的带宽要求 —— 我们只是希望它尽快完成。 -如果想通过电路传输文件,你得预测一个带宽分配。如果你猜的太低,传输速度会不必要的太慢,导致网络容量闲置。如果你猜的太高,电路就无法建立(因为如果无法保证其带宽分配,网络不能建立电路)。因此,将电路用于突发数据传输会浪费网络容量,并且使传输不必要地缓慢。相比之下,TCP动态调整数据传输速率以适应可用的网络容量。 +如果想通过电路传输文件,你得预测一个带宽分配。如果你猜的太低,传输速度会不必要的太慢,导致网络容量闲置。如果你猜的太高,电路就无法建立(因为如果无法保证其带宽分配,网络不能建立电路)。因此,将电路用于突发数据传输会浪费网络容量,并且使传输不必要地缓慢。相比之下,TCP 动态调整数据传输速率以适应可用的网络容量。 -已经有一些尝试去建立同时支持电路交换和分组交换的混合网络,比如ATM[^iii]。InfiniBand有一些相似之处【35】:它在链路层实现了端到端的流量控制,从而减少了在网络中排队的需要,尽管它仍然可能因链路拥塞而受到延迟【36】。通过仔细使用**服务质量**(quality of service,即QoS,数据包的优先级和调度)和**准入控制**(admission control,限速发送器),可以在分组网络上模拟电路交换,或提供统计上的**有限延迟**【25,32】。 +已经有一些尝试去建立同时支持电路交换和分组交换的混合网络,比如 ATM [^iii]。InfiniBand 有一些相似之处【35】:它在链路层实现了端到端的流量控制,从而减少了在网络中排队的需要,尽管它仍然可能因链路拥塞而受到延迟【36】。通过仔细使用 **服务质量**(quality of service,即 QoS,数据包的优先级和调度)和 **准入控制**(admission control,限速发送器),可以在分组网络上模拟电路交换,或提供统计上的 **有限延迟**【25,32】。 -[^iii]: **异步传输模式(Asynchronous Transfer Mode, ATM)** 在20世纪80年代是以太网的竞争对手【32】,但在电话网核心交换机之外并没有得到太多的采用。它与自动柜员机(也称为自动取款机)无关,尽管共用一个缩写词。或许,在一些平行的世界里,互联网是基于像ATM这样的东西,因此它们的互联网视频通话可能比我们的更可靠,因为它们不会遭受包的丢失和延迟。 +[^iii]: **异步传输模式(Asynchronous Transfer Mode, ATM)** 在 20 世纪 80 年代是以太网的竞争对手【32】,但在电话网核心交换机之外并没有得到太多的采用。它与自动柜员机(也称为自动取款机)无关,尽管共用一个缩写词。或许,在一些平行的世界里,互联网是基于像 ATM 这样的东西,因此它们的互联网视频通话可能比我们的更可靠,因为它们不会遭受包的丢失和延迟。 -但是,目前在多租户数据中心和公共云或通过互联网[^iv]进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设网络拥塞,排队和无限的延迟总是会发生。因此,超时时间没有“正确”的值——它需要通过实验来确定。 +但是,目前在多租户数据中心和公共云或通过互联网 [^iv] 进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设网络拥塞,排队和无限的延迟总是会发生。因此,超时时间没有 “正确” 的值 —— 它需要通过实验来确定。 -[^iv]: 互联网服务提供商之间的对等协议和通过**BGP网关协议(BGP)** 建立的路由,与IP协议相比,更接近于电路交换。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 +[^iv]: 互联网服务提供商之间的对等协议和通过 **BGP 网关协议(BGP)** 建立的路由,与 IP 协议相比,更接近于电路交换。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 > ### 延迟和资源利用 > -> 更一般地说,可以将**延迟变化**视为**动态资源分区**的结果。 +> 更一般地说,可以将 **延迟变化** 视为 **动态资源分区** 的结果。 > -> 假设两台电话交换机之间有一条线路,可以同时进行10,000个呼叫。通过此线路切换的每个电路都占用其中一个呼叫插槽。因此,你可以将线路视为可由多达10,000个并发用户共享的资源。资源以静态方式分配:即使你现在是电话上唯一的电话,并且所有其他9,999个插槽都未使用,你的电路仍将分配与导线充分利用时相同的固定数量的带宽。 +> 假设两台电话交换机之间有一条线路,可以同时进行 10,000 个呼叫。通过此线路切换的每个电路都占用其中一个呼叫插槽。因此,你可以将线路视为可由多达 10,000 个并发用户共享的资源。资源以静态方式分配:即使你现在是电话上唯一的电话,并且所有其他 9,999 个插槽都未使用,你的电路仍将分配与导线充分利用时相同的固定数量的带宽。 > > 相比之下,互联网动态分享网络带宽。发送者互相推挤和争夺,以让他们的数据包尽可能快地通过网络,并且网络交换机决定从一个时刻到另一个时刻发送哪个分组(即,带宽分配)。这种方法有排队的缺点,但其优点是它最大限度地利用了电线。电线固定成本,所以如果你更好地利用它,你通过电线发送的每个字节都会更便宜。 > -> CPU也会出现类似的情况:如果你在多个线程间动态共享每个CPU内核,则一个线程有时必须在操作系统的运行队列里等待,而另一个线程正在运行,这样每个线程都有可能被暂停一个不定的时间长度。但是,与为每个线程分配静态数量的CPU周期相比,这会更好地利用硬件(请参阅“[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。 +> CPU 也会出现类似的情况:如果你在多个线程间动态共享每个 CPU 内核,则一个线程有时必须在操作系统的运行队列里等待,而另一个线程正在运行,这样每个线程都有可能被暂停一个不定的时间长度。但是,与为每个线程分配静态数量的 CPU 周期相比,这会更好地利用硬件(请参阅 “[响应时间保证](#响应时间保证)”)。更好的硬件利用率也是使用虚拟机的重要动机。 > -> 如果资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可以实现**延迟保证**。但是,这是以降低利用率为代价的——换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它更便宜,但它具有可变延迟的缺点。 +> 如果资源是静态分区的(例如,专用硬件和专用带宽分配),则在某些环境中可以实现 **延迟保证**。但是,这是以降低利用率为代价的 —— 换句话说,它是更昂贵的。另一方面,动态资源分配的多租户提供了更好的利用率,所以它更便宜,但它具有可变延迟的缺点。 > -> 网络中的可变延迟不是一种自然规律,而只是成本/收益权衡的结果。 +> 网络中的可变延迟不是一种自然规律,而只是成本 / 收益权衡的结果。 ## 不可靠的时钟 @@ -242,7 +242,7 @@ 时钟和时间很重要。应用程序以各种方式依赖于时钟来回答以下问题: 1. 这个请求是否超时了? -2. 这项服务的第99百分位响应时间是多少? +2. 这项服务的第 99 百分位响应时间是多少? 3. 在过去五分钟内,该服务平均每秒处理多少个查询? 4. 用户在我们的网站上花了多长时间? 5. 这篇文章在何时发布? @@ -250,11 +250,11 @@ 7. 这个缓存条目何时到期? 8. 日志文件中此错误消息的时间戳是什么? -[例1-4](ch1.md)测量了**持续时间**(durations,例如,请求发送与响应接收之间的时间间隔),而[例5-8](ch5.md)描述了**时间点**(point in time,在特定日期和和特定时间发生的事件)。 +[例 1-4](ch1.md) 测量了 **持续时间**(durations,例如,请求发送与响应接收之间的时间间隔),而 [例 5-8](ch5.md) 描述了 **时间点**(point in time,在特定日期和和特定时间发生的事件)。 在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道晚了多少时间。这个事实导致有时很难确定在涉及多台机器时发生事情的顺序。 -而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是**网络时间协议(NTP)**,它允许根据一组服务器报告的时间来调整计算机时钟【37】。服务器则从更精确的时间源(如GPS接收机)获取时间。 +而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是 **网络时间协议(NTP)**,它允许根据一组服务器报告的时间来调整计算机时钟【37】。服务器则从更精确的时间源(如 GPS 接收机)获取时间。 ### 单调钟与日历时钟 @@ -262,50 +262,50 @@ #### 日历时钟 -日历时钟是你直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间**,即wall-clock time)返回当前日期和时间。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC时间1970年1月1日午夜)以来的秒数(或毫秒),根据公历(Gregorian)日历,不包括闰秒。有些系统使用其他日期作为参考点。 +日历时钟是你直观地了解时钟的依据:它根据某个日历(也称为 **挂钟时间**,即 wall-clock time)返回当前日期和时间。例如,Linux 上的 `clock_gettime(CLOCK_REALTIME)`[^v] 和 Java 中的 `System.currentTimeMillis()` 返回自 epoch(UTC 时间 1970 年 1 月 1 日午夜)以来的秒数(或毫秒),根据公历(Gregorian)日历,不包括闰秒。有些系统使用其他日期作为参考点。 -[^v]: 虽然该时钟被称为实时时钟,但它与实时操作系统无关,如“[响应时间保证](#响应时间保证)”中所述。 +[^v]: 虽然该时钟被称为实时时钟,但它与实时操作系统无关,如 “[响应时间保证](#响应时间保证)” 中所述。 -日历时钟通常与NTP同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳相同。但是如下节所述,日历时钟也具有各种各样的奇特之处。特别是,如果本地时钟在NTP服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使日历时钟不能用于测量经过时间(elapsed time)【38】。 +日历时钟通常与 NTP 同步,这意味着来自一台机器的时间戳(理想情况下)与另一台机器上的时间戳相同。但是如下节所述,日历时钟也具有各种各样的奇特之处。特别是,如果本地时钟在 NTP 服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使日历时钟不能用于测量经过时间(elapsed time)【38】。 -历史上的日历时钟还具有相当粗略的分辨率,例如,在较早的Windows系统上以10毫秒为单位前进【39】。在最近的系统中这已经不是一个问题了。 +历史上的日历时钟还具有相当粗略的分辨率,例如,在较早的 Windows 系统上以 10 毫秒为单位前进【39】。在最近的系统中这已经不是一个问题了。 #### 单调钟 -单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的`clock_gettime(CLOCK_MONOTONIC)`,和Java中的`System.nanoTime()`都是单调时钟。这个名字来源于他们保证总是往前走的事实(而日历时钟可以往回跳)。 +单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux 上的 `clock_gettime(CLOCK_MONOTONIC)`,和 Java 中的 `System.nanoTime()` 都是单调时钟。这个名字来源于他们保证总是往前走的事实(而日历时钟可以往回跳)。 你可以在某个时间点检查单调钟的值,做一些事情,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。但单调钟的绝对值是毫无意义的:它可能是计算机启动以来的纳秒数,或类似的任意值。特别是比较来自两台不同计算机的单调钟的值是没有意义的,因为它们并不是一回事。 -在具有多个CPU插槽的服务器上,每个CPU可能有一个单独的计时器,但不一定与其他CPU同步。操作系统会补偿所有的差异,并尝试向应用线程表现出单调钟的样子,即使这些线程被调度到不同的CPU上。当然,明智的做法是不要太把这种单调性保证当回事【40】。 +在具有多个 CPU 插槽的服务器上,每个 CPU 可能有一个单独的计时器,但不一定与其他 CPU 同步。操作系统会补偿所有的差异,并尝试向应用线程表现出单调钟的样子,即使这些线程被调度到不同的 CPU 上。当然,明智的做法是不要太把这种单调性保证当回事【40】。 -如果NTP协议检测到计算机的本地石英钟比NTP服务器要更快或更慢,则可以调整单调钟向前走的频率(这称为**偏移(skewing)** 时钟)。默认情况下,NTP允许时钟速率增加或减慢最高至0.05%,但NTP不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好:在大多数系统中,它们能在几微秒或更短的时间内测量时间间隔。 +如果 NTP 协议检测到计算机的本地石英钟比 NTP 服务器要更快或更慢,则可以调整单调钟向前走的频率(这称为 **偏移(skewing)** 时钟)。默认情况下,NTP 允许时钟速率增加或减慢最高至 0.05%,但 NTP 不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好:在大多数系统中,它们能在几微秒或更短的时间内测量时间间隔。 -在分布式系统中,使用单调钟测量**经过时间**(elapsed time,比如超时)通常很好,因为它不假定不同节点的时钟之间存在任何同步,并且对测量的轻微不准确性不敏感。 +在分布式系统中,使用单调钟测量 **经过时间**(elapsed time,比如超时)通常很好,因为它不假定不同节点的时钟之间存在任何同步,并且对测量的轻微不准确性不敏感。 ### 时钟同步与准确性 -单调钟不需要同步,但是日历时钟需要根据NTP服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确——硬件时钟和NTP可能会变幻莫测。举几个例子: +单调钟不需要同步,但是日历时钟需要根据 NTP 服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确 —— 硬件时钟和 NTP 可能会变幻莫测。举几个例子: -* 计算机中的石英钟不够精确:它会**漂移**(drifts,即运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google假设其服务器时钟漂移为200 ppm(百万分之一)【41】,相当于每30秒与服务器重新同步一次的时钟漂移为6毫秒,或者每天重新同步的时钟漂移为17秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。 -* 如果计算机的时钟与NTP服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置【37】。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。 -* 如果某个节点被NTP服务器的防火墙意外阻塞,有可能会持续一段时间都没有人会注意到。有证据表明,这在实践中确实发生过。 -* NTP同步只能和网络延迟一样好,所以当你在拥有可变数据包延迟的拥塞网络上时,NTP同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致NTP客户端完全放弃。 -* 一些NTP服务器是错误的或者配置错误的,报告的时间可能相差几个小时【43,44】。还好NTP客户端非常健壮,因为他们会查询多个服务器并忽略异常值。无论如何,依赖于互联网上的陌生人所告诉你的时间来保证你的系统的正确性,这还挺让人担忧的。 -* 闰秒导致一分钟可能有59秒或61秒,这会打破一些在设计之时未考虑闰秒的系统的时序假设【45】。闰秒已经使许多大型系统崩溃的事实【38,46】说明了,关于时钟的错误假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是让NTP服务器“撒谎”,并在一天中逐渐执行闰秒调整(这被称为**拖尾**,即smearing)【47,48】,虽然实际的NTP服务器表现各异【49】。 -* 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战【50】。当一个CPU核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,与此同时另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃【26】。 +* 计算机中的石英钟不够精确:它会 **漂移**(drifts,即运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google 假设其服务器时钟漂移为 200 ppm(百万分之一)【41】,相当于每 30 秒与服务器重新同步一次的时钟漂移为 6 毫秒,或者每天重新同步的时钟漂移为 17 秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。 +* 如果计算机的时钟与 NTP 服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置【37】。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。 +* 如果某个节点被 NTP 服务器的防火墙意外阻塞,有可能会持续一段时间都没有人会注意到。有证据表明,这在实践中确实发生过。 +* NTP 同步只能和网络延迟一样好,所以当你在拥有可变数据包延迟的拥塞网络上时,NTP 同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35 毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致 NTP 客户端完全放弃。 +* 一些 NTP 服务器是错误的或者配置错误的,报告的时间可能相差几个小时【43,44】。还好 NTP 客户端非常健壮,因为他们会查询多个服务器并忽略异常值。无论如何,依赖于互联网上的陌生人所告诉你的时间来保证你的系统的正确性,这还挺让人担忧的。 +* 闰秒导致一分钟可能有 59 秒或 61 秒,这会打破一些在设计之时未考虑闰秒的系统的时序假设【45】。闰秒已经使许多大型系统崩溃的事实【38,46】说明了,关于时钟的错误假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是让 NTP 服务器 “撒谎”,并在一天中逐渐执行闰秒调整(这被称为 **拖尾**,即 smearing)【47,48】,虽然实际的 NTP 服务器表现各异【49】。 +* 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战【50】。当一个 CPU 核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,与此同时另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃【26】。 * 如果你在没有完整控制权的设备(例如,移动设备或嵌入式设备)上运行软件,则可能完全不能信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。 -如果你足够在乎这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案MiFID II要求所有高频率交易基金在UTC时间100微秒内同步时钟,以便调试“闪崩”等市场异常现象,并帮助检测市场操纵【51】。 +如果你足够在乎这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案 MiFID II 要求所有高频率交易基金在 UTC 时间 100 微秒内同步时钟,以便调试 “闪崩” 等市场异常现象,并帮助检测市场操纵【51】。 -通过GPS接收机,精确时间协议(PTP)【52】以及仔细的部署和监测可以实现这种精确度。然而,这需要很多努力和专业知识,而且有很多东西都会导致时钟同步错误。如果你的NTP守护进程配置错误,或者防火墙阻止了NTP通信,由漂移引起的时钟误差可能很快就会变大。 +通过 GPS 接收机,精确时间协议(PTP)【52】以及仔细的部署和监测可以实现这种精确度。然而,这需要很多努力和专业知识,而且有很多东西都会导致时钟同步错误。如果你的 NTP 守护进程配置错误,或者防火墙阻止了 NTP 通信,由漂移引起的时钟误差可能很快就会变大。 ### 依赖同步时钟 -时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的86,400秒,**日历时钟**可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。 +时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的 86,400 秒,**日历时钟** 可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。 本章早些时候,我们讨论了网络丢包和任意延迟包的问题。尽管网络在大多数情况下表现良好,但软件的设计必须假定网络偶尔会出现故障,而软件必须正常处理这些故障。时钟也是如此:尽管大多数时间都工作得很好,但需要准备健壮的软件来处理不正确的时钟。 -有一部分问题是,不正确的时钟很容易被视而不见。如果一台机器的CPU出现故障或者网络配置错误,很可能根本无法工作,所以很快就会被注意和修复。另一方面,如果它的石英时钟有缺陷,或者它的NTP客户端配置错误,大部分事情似乎仍然可以正常工作,即使它的时钟逐渐偏离现实。如果某个软件依赖于精确同步的时钟,那么结果更可能是悄无声息的,仅有微量的数据丢失,而不是一次惊天动地的崩溃【53,54】。 +有一部分问题是,不正确的时钟很容易被视而不见。如果一台机器的 CPU 出现故障或者网络配置错误,很可能根本无法工作,所以很快就会被注意和修复。另一方面,如果它的石英时钟有缺陷,或者它的 NTP 客户端配置错误,大部分事情似乎仍然可以正常工作,即使它的时钟逐渐偏离现实。如果某个软件依赖于精确同步的时钟,那么结果更可能是悄无声息的,仅有微量的数据丢失,而不是一次惊天动地的崩溃【53,54】。 因此,如果你使用需要同步时钟的软件,必须仔细监控所有机器之间的时钟偏移。时钟偏离其他时钟太远的节点应当被宣告死亡,并从集群中移除。这样的监控可以确保你在损失发生之前注意到破损的时钟。 @@ -313,55 +313,55 @@ 让我们考虑一个特别的情况,一件很有诱惑但也很危险的事情:依赖时钟,在多个节点上对事件进行排序。 例如,如果两个客户端写入分布式数据库,谁先到达? 哪一个更近? -[图8-3](img/fig8-3.png)显示了在具有多领导者复制的数据库中对时钟的危险使用(该例子类似于[图5-9](img/fig5-9.png))。 客户端A在节点1上写入`x = 1`;写入被复制到节点3;客户端B在节点3上增加x(我们现在有`x = 2`);最后这两个写入都被复制到节点2。 +[图 8-3](img/fig8-3.png) 显示了在具有多领导者复制的数据库中对时钟的危险使用(该例子类似于 [图 5-9](img/fig5-9.png))。 客户端 A 在节点 1 上写入 `x = 1`;写入被复制到节点 3;客户端 B 在节点 3 上增加 x(我们现在有 `x = 2`);最后这两个写入都被复制到节点 2。 ![](img/fig8-3.png) -**图8-3 客户端B的写入比客户端A的写入要晚,但是B的写入具有较早的时间戳。** +**图 8-3 客户端 B 的写入比客户端 A 的写入要晚,但是 B 的写入具有较早的时间戳。** -在[图8-3](img/fig8-3.png)中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的日历时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点1和节点3之间的偏差小于3ms,这可能比你在实践中能预期的更好。 +在 [图 8-3](img/fig8-3.png) 中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的日历时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点 1 和节点 3 之间的偏差小于 3ms,这可能比你在实践中能预期的更好。 -尽管如此,[图8-3](img/fig8-3.png)中的时间戳却无法正确排列事件:写入`x = 1`的时间戳为42.004秒,但写入`x = 2`的时间戳为42.003秒,即使`x = 2`在稍后出现。当节点2接收到这两个事件时,会错误地推断出`x = 1`是最近的值,而丢弃写入`x = 2`。效果上表现为,客户端B的增量操作会丢失。 +尽管如此,[图 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#检测并发写入)”)。 -* 两个节点很可能独立地生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的**决胜值**(tiebreaker,可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系【53】。 +* LWW 无法区分 **高频顺序写入**(在 [图 8-3](img/fig8-3.png) 中,客户端 B 的增量操作 **一定** 发生在客户端 A 的写入之后)和 **真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止违背因果关系(请参阅 “[检测并发写入](ch5.md#检测并发写入)”)。 +* 两个节点很可能独立地生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的 **决胜值**(tiebreaker,可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系【53】。 -因此,尽管通过保留最“最近”的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近”的定义取决于本地的**日历时钟**,这很可能是不正确的。即使用严格同步的NTP时钟,一个数据包也可能在时间戳100毫秒(根据发送者的时钟)时发送,并在时间戳99毫秒(根据接收者的时钟)处到达——看起来好像数据包在发送之前已经到达,这是不可能的。 +因此,尽管通过保留最 “最近” 的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近” 的定义取决于本地的 **日历时钟**,这很可能是不正确的。即使用严格同步的 NTP 时钟,一个数据包也可能在时间戳 100 毫秒(根据发送者的时钟)时发送,并在时间戳 99 毫秒(根据接收者的时钟)处到达 —— 看起来好像数据包在发送之前已经到达,这是不可能的。 -NTP同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为NTP的同步精度本身,除了石英钟漂移这类误差源之外,还受到网络往返时间的限制。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。 +NTP 同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为 NTP 的同步精度本身,除了石英钟漂移这类误差源之外,还受到网络往返时间的限制。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。 -所谓的**逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参阅“[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的**日历时钟**和**单调钟**也被称为**物理时钟(physical clock)**。我们将在“[顺序保证](ch9.md#顺序保证)”中来看顺序问题。 +所谓的 **逻辑时钟(logic clock)**【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参阅 “[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的 **日历时钟** 和 **单调钟** 也被称为 **物理时钟(physical clock)**。我们将在 “[顺序保证](ch9.md#顺序保证)” 中来看顺序问题。 #### 时钟读数存在置信区间 -你可能能够以微秒或甚至纳秒的精度读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值对于这样的精度实际上是准确的。实际上,大概率是不准确的——如前所述,即使你每分钟与本地网络上的NTP服务器进行同步,几毫秒的时间漂移也很容易在不精确的石英时钟上发生。使用公共互联网上的NTP服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过100毫秒【57】。 +你可能能够以微秒或甚至纳秒的精度读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值对于这样的精度实际上是准确的。实际上,大概率是不准确的 —— 如前所述,即使你每分钟与本地网络上的 NTP 服务器进行同步,几毫秒的时间漂移也很容易在不精确的石英时钟上发生。使用公共互联网上的 NTP 服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过 100 毫秒【57】。 -因此,将时钟读数视为一个时间点是没有意义的——它更像是一段时间范围:例如,一个系统可能以95%的置信度认为当前时间处于本分钟内的第10.3秒和10.5秒之间,它可能没法比这更精确了【58】。如果我们只知道±100毫秒的时间,那么时间戳中的微秒数字部分基本上是没有意义的。 +因此,将时钟读数视为一个时间点是没有意义的 —— 它更像是一段时间范围:例如,一个系统可能以 95% 的置信度认为当前时间处于本分钟内的第 10.3 秒和 10.5 秒之间,它可能没法比这更精确了【58】。如果我们只知道 ±100 毫秒的时间,那么时间戳中的微秒数字部分基本上是没有意义的。 -不确定性界限可以根据你的时间源来计算。如果你的GPS接收器或原子(铯)时钟直接连接到你的计算机上,预期的错误范围由制造商告知。如果从服务器获得时间,则不确定性取决于自上次与服务器同步以来的石英钟漂移的期望值,加上NTP服务器的不确定性,再加上到服务器的网络往返时间(只是获取粗略近似值,并假设服务器是可信的)。 +不确定性界限可以根据你的时间源来计算。如果你的 GPS 接收器或原子(铯)时钟直接连接到你的计算机上,预期的错误范围由制造商告知。如果从服务器获得时间,则不确定性取决于自上次与服务器同步以来的石英钟漂移的期望值,加上 NTP 服务器的不确定性,再加上到服务器的网络往返时间(只是获取粗略近似值,并假设服务器是可信的)。 -不幸的是,大多数系统不公开这种不确定性:例如,当调用`clock_gettime()`时,返回值不会告诉你时间戳的预期错误,所以你不知道其置信区间是5毫秒还是5年。 +不幸的是,大多数系统不公开这种不确定性:例如,当调用 `clock_gettime()` 时,返回值不会告诉你时间戳的预期错误,所以你不知道其置信区间是 5 毫秒还是 5 年。 -一个有趣的例外是Spanner中的Google TrueTime API 【41】,它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。在不确定性估计的基础上,时钟知道当前的实际时间落在该区间内。区间的宽度取决于自从本地石英钟最后与更精确的时钟源同步以来已经过了多长时间。 +一个有趣的例外是 Spanner 中的 Google TrueTime API 【41】,它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。在不确定性估计的基础上,时钟知道当前的实际时间落在该区间内。区间的宽度取决于自从本地石英钟最后与更精确的时钟源同步以来已经过了多长时间。 #### 全局快照的同步时钟 -在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务(用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。 +在 “[快照隔离和可重复读](ch7.md#快照隔离和可重复读)” 中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务(用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。 -快照隔离最常见的实现需要单调递增的事务ID。如果写入比快照晚(即,写入具有比快照更大的事务ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务ID。 +快照隔离最常见的实现需要单调递增的事务 ID。如果写入比快照晚(即,写入具有比快照更大的事务 ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务 ID。 -但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务ID会很难生成。事务ID必须反映因果关系:如果事务B读取由事务A写入的值,则B必须具有比A更大的事务ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务ID成为一个难以处理的瓶颈[^vi]。 +但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务 ID 会很难生成。事务 ID 必须反映因果关系:如果事务 B 读取由事务 A 写入的值,则 B 必须具有比 A 更大的事务 ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务 ID 成为一个难以处理的瓶颈 [^vi]。 -[^vi]: 存在分布式序列号生成器,例如Twitter的雪花(Snowflake),其以可伸缩的方式(例如,通过将ID空间的块分配给不同节点)近似单调地增加唯一ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的ID块的时间范围比数据库读取和写入的时间范围要长。另请参阅“[顺序保证](ch9.md#顺序保证)”。 +[^vi]: 存在分布式序列号生成器,例如 Twitter 的雪花(Snowflake),其以可伸缩的方式(例如,通过将 ID 空间的块分配给不同节点)近似单调地增加唯一 ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的 ID 块的时间范围比数据库读取和写入的时间范围要长。另请参阅 “[顺序保证](ch9.md#顺序保证)”。 -我们可以使用同步时钟的时间戳作为事务ID吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。 +我们可以使用同步时钟的时间戳作为事务 ID 吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。 -Spanner以这种方式实现跨数据中心的快照隔离【59,60】。它使用TrueTime API报告的时钟置信区间,并基于以下观察结果:如果你有两个置信区间,每个置信区间包含最早和最晚可能的时间戳( $A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} < A_{latest} < B_{earliest} < B_{latest}$)的话,那么B肯定发生在A之后——这是毫无疑问的。只有当区间重叠时,我们才不确定A和B发生的顺序。 +Spanner 以这种方式实现跨数据中心的快照隔离【59,60】。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察结果:如果你有两个置信区间,每个置信区间包含最早和最晚可能的时间戳($A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),这两个区间不重叠(即:$A_{earliest} #### 实时是真的吗? > -> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与Web上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见[第十一章](ch11.md))。 +> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与 Web 上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见 [第十一章](ch11.md))。 -例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为GC暂停而延迟弹出。 +例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为 GC 暂停而延迟弹出。 -在系统中提供**实时保证**需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证CPU时间的分配。库函数必须申明最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给GC太多的负担);必须进行大量的测试和测量,以确保达到保证。 +在系统中提供 **实时保证** 需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证 CPU 时间的分配。库函数必须申明最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给 GC 太多的负担);必须进行大量的测试和测量,以确保达到保证。 -所有这些都需要大量额外的工作,严重限制了可以使用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**”与“**高性能**”不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须让及时响应的优先级高于一切(另请参阅“[延迟和资源利用](#延迟和资源利用)“)。 +所有这些都需要大量额外的工作,严重限制了可以使用的编程语言、库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且,“**实时**” 与 “**高性能**” 不一样 —— 事实上,实时系统可能具有较低的吞吐量,因为他们必须让及时响应的优先级高于一切(另请参阅 “[延迟和资源利用](#延迟和资源利用)“)。 对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。 @@ -433,9 +433,9 @@ while (true) { 进程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。 -一个新兴的想法是将GC暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以警告应用程序一个节点很快需要GC暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行GC。这个技巧向客户端隐藏了GC暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。 +一个新兴的想法是将 GC 暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以警告应用程序一个节点很快需要 GC 暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行 GC。这个技巧向客户端隐藏了 GC 暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。 -这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从该节点移开,就像[第四章](ch4.md)里描述的滚动升级一样。 +这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整 GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从该节点移开,就像 [第四章](ch4.md) 里描述的滚动升级一样。 这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。 @@ -444,63 +444,63 @@ while (true) { 本章到目前为止,我们已经探索了分布式系统与运行在单台计算机上的程序的不同之处:没有共享内存,只有通过可变延迟的不可靠网络传递的消息,系统可能遭受部分失效,不可靠的时钟和处理暂停。 -如果你不习惯于分布式系统,那么这些问题的后果就会让人迷惑不解。网络中的一个节点无法确切地知道任何事情——它只能根据它通过网络接收到(或没有接收到)的消息进行猜测。节点只能通过交换消息来找出另一个节点所处的状态(存储了哪些数据,是否正确运行等等)。如果远程节点没有响应,则无法知道它处于什么状态,因为网络中的问题不能可靠地与节点上的问题区分开来。 +如果你不习惯于分布式系统,那么这些问题的后果就会让人迷惑不解。网络中的一个节点无法确切地知道任何事情 —— 它只能根据它通过网络接收到(或没有接收到)的消息进行猜测。节点只能通过交换消息来找出另一个节点所处的状态(存储了哪些数据,是否正确运行等等)。如果远程节点没有响应,则无法知道它处于什么状态,因为网络中的问题不能可靠地与节点上的问题区分开来。 这些系统的讨论与哲学有关:在系统中什么是真什么是假?如果感知和测量的机制都是不可靠的,那么关于这些知识我们又能多么确定呢?软件系统应该遵循我们对物理世界所期望的法则,如因果关系吗? 幸运的是,我们不需要去搞清楚生命的意义。在分布式系统中,我们可以陈述关于行为(系统模型)的假设,并以满足这些假设的方式设计实际系统。算法可以被证明在某个系统模型中正确运行。这意味着即使底层系统模型提供了很少的保证,也可以实现可靠的行为。 -但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在[第九章](ch9.md)中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。 +但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在 [第九章](ch9.md) 中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。 ### 真相由多数所定义 -设想一个具有不对称故障的网络:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟【19】。即使该节点运行良好,并且正在接收来自其他节点的请求,其他节点也无法听到其响应。经过一段时间后,其他节点宣布它已经死亡,因为他们没有听到节点的消息。这种情况就像梦魇一样:**半断开(semi-disconnected)** 的节点被拖向墓地,敲打尖叫道“我没死!” ——但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。 +设想一个具有不对称故障的网络:一个节点能够接收发送给它的所有消息,但是来自该节点的任何传出消息被丢弃或延迟【19】。即使该节点运行良好,并且正在接收来自其他节点的请求,其他节点也无法听到其响应。经过一段时间后,其他节点宣布它已经死亡,因为他们没有听到节点的消息。这种情况就像梦魇一样:**半断开(semi-disconnected)** 的节点被拖向墓地,敲打尖叫道 “我没死!” —— 但是由于没有人能听到它的尖叫,葬礼队伍继续以坚忍的决心继续行进。 在一个稍微不那么梦魇的场景中,半断开的节点可能会注意到它发送的消息没有被其他节点确认,因此意识到网络中必定存在故障。尽管如此,节点被其他节点错误地宣告为死亡,而半连接的节点对此无能为力。 -第三种情况,想象一个经历了一个长时间**停止所有处理垃圾收集暂停(stop-the-world GC Pause)** 的节点。节点的所有线程被GC抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。 +第三种情况,想象一个经历了一个长时间 **停止所有处理垃圾收集暂停(stop-the-world GC Pause)** 的节点。节点的所有线程被 GC 抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC 完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC 后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。 -这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。 +这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。 这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。 -最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第九章](ch9.md)中讨论**共识算法(consensus algorithms)** 时,我们将更详细地讨论法定人数的应用。 +最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数 —— 不能同时存在两个相互冲突的多数决定。当我们在 [第九章](ch9.md) 中讨论 **共识算法(consensus algorithms)** 时,我们将更详细地讨论法定人数的应用。 #### 领导者和锁 通常情况下,一些东西在一个系统中只能有一个。例如: -* 数据库分区的领导者只能有一个节点,以避免**脑裂**(即split brain,请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。 -* 特定资源的锁或对象只允许一个事务/客户端持有,以防同时写入和损坏。 +* 数据库分区的领导者只能有一个节点,以避免 **脑裂**(即 split brain,请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。 +* 特定资源的锁或对象只允许一个事务 / 客户端持有,以防同时写入和损坏。 * 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。 -在分布式系统中实现这一点需要注意:即使一个节点认为它是“**天选者(the choosen one)**”(分区的负责人,锁的持有者,成功获取用户名的用户的请求处理程序),但这并不一定意味着有法定人数的节点同意!一个节点可能以前是领导者,但是如果其他节点在此期间宣布它死亡(例如,由于网络中断或GC暂停),则它可能已被降级,且另一个领导者可能已经当选。 +在分布式系统中实现这一点需要注意:即使一个节点认为它是 “**天选者(the choosen one)**”(分区的负责人,锁的持有者,成功获取用户名的用户的请求处理程序),但这并不一定意味着有法定人数的节点同意!一个节点可能以前是领导者,但是如果其他节点在此期间宣布它死亡(例如,由于网络中断或 GC 暂停),则它可能已被降级,且另一个领导者可能已经当选。 -如果一个节点继续表现为**天选者**,即使大多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。这样的节点能以自己赋予的权能向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。 +如果一个节点继续表现为 **天选者**,即使大多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。这样的节点能以自己赋予的权能向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。 -例如,[图8-4](img/fig8-4.png)显示了由于不正确的锁实现导致的数据损坏错误。 (这个错误不仅仅是理论上的:HBase曾经有这个问题【74,75】)假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图对此写入,该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。 +例如,[图 8-4](img/fig8-4.png) 显示了由于不正确的锁实现导致的数据损坏错误。 (这个错误不仅仅是理论上的:HBase 曾经有这个问题【74,75】)假设你要确保一个存储服务中的文件一次只能被一个客户访问,因为如果多个客户试图对此写入,该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。 ![](img/fig8-4.png) -**图8-4 分布式锁的实现不正确:客户端1认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件** +**图 8-4 分布式锁的实现不正确:客户端 1 认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件** -这个问题就是我们先前在“[进程暂停](#进程暂停)”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入将产生冲突并损坏文件。 +这个问题就是我们先前在 “[进程暂停](#进程暂停)” 中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入将产生冲突并损坏文件。 #### 防护令牌 -当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是**防护(fencing)**,如[图8-5](img/fig8-5.png)所示 +当使用锁或租约来保护对某些资源(如 [图 8-4](img/fig8-4.png) 中的文件存储)的访问时,需要确保一个被误认为自己是 “天选者” 的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是 **防护(fencing)**,如 [图 8-5](img/fig8-5.png) 所示 ![](img/fig8-5.png) -**图8-5 只允许以增加防护令牌的顺序进行写操作,从而保证存储安全** +**图 8-5 只允许以增加防护令牌的顺序进行写操作,从而保证存储安全** -我们假设每次锁定服务器授予锁或租约时,它还会返回一个**防护令牌(fencing token)**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的防护令牌。 +我们假设每次锁定服务器授予锁或租约时,它还会返回一个 **防护令牌(fencing token)**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的防护令牌。 -在[图8-5](img/fig8-5.png)中,客户端1以33的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端2以34的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括34的令牌。稍后,客户端1恢复生机并将其写入存储服务,包括其令牌值33。但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌33的请求。 +在 [图 8-5](img/fig8-5.png) 中,客户端 1 以 33 的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端 2 以 34 的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括 34 的令牌。稍后,客户端 1 恢复生机并将其写入存储服务,包括其令牌值 33。但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌 33 的请求。 -如果将ZooKeeper用作锁定服务,则可将事务标识`zxid`或节点版本`cversion`用作防护令牌。由于它们保证单调递增,因此它们具有所需的属性【74】。 +如果将 ZooKeeper 用作锁定服务,则可将事务标识 `zxid` 或节点版本 `cversion` 用作防护令牌。由于它们保证单调递增,因此它们具有所需的属性【74】。 -请注意,这种机制要求资源本身在检查令牌方面发挥积极作用,通过拒绝使用旧的令牌,而不是已经被处理的令牌来进行写操作——仅仅依靠客户端检查自己的锁状态是不够的。对于不明确支持防护令牌的资源,可能仍然可以解决此限制(例如,在文件存储服务的情况下,可以将防护令牌包含在文件名中)。但是,为了避免在锁的保护之外处理请求,需要进行某种检查。 +请注意,这种机制要求资源本身在检查令牌方面发挥积极作用,通过拒绝使用旧的令牌,而不是已经被处理的令牌来进行写操作 —— 仅仅依靠客户端检查自己的锁状态是不够的。对于不明确支持防护令牌的资源,可能仍然可以解决此限制(例如,在文件存储服务的情况下,可以将防护令牌包含在文件名中)。但是,为了避免在锁的保护之外处理请求,需要进行某种检查。 在服务器端检查一个令牌可能看起来像是一个缺点,但这可以说是一件好事:一个服务假定它的客户总是守规矩并不明智,因为使用客户端的人与运行服务的人优先级非常不一样【76】。因此,任何服务保护自己免受意外客户的滥用是一个好主意。 @@ -508,42 +508,42 @@ while (true) { 防护令牌可以检测和阻止无意中发生错误的节点(例如,因为它尚未发现其租约已过期)。但是,如果节点有意破坏系统的保证,则可以通过使用假防护令牌发送消息来轻松完成此操作。 -在本书中,我们假设节点是不可靠但诚实的:它们可能很慢或者从不响应(由于故障),并且它们的状态可能已经过时(由于GC暂停或网络延迟),但是我们假设如果节点它做出了回应,它正在说出“真相”:尽其所知,它正在按照协议的规则扮演其角色。 +在本书中,我们假设节点是不可靠但诚实的:它们可能很慢或者从不响应(由于故障),并且它们的状态可能已经过时(由于 GC 暂停或网络延迟),但是我们假设如果节点它做出了回应,它正在说出 “真相”:尽其所知,它正在按照协议的规则扮演其角色。 -如果存在节点可能“撒谎”(发送任意错误或损坏的响应)的风险,则分布式系统的问题变得更困难了——例如,如果节点可能声称其实际上没有收到特定的消息。这种行为被称为**拜占庭故障(Byzantine fault)**,**在不信任的环境中达成共识的问题被称为拜占庭将军问题**【77】。 +如果存在节点可能 “撒谎”(发送任意错误或损坏的响应)的风险,则分布式系统的问题变得更困难了 —— 例如,如果节点可能声称其实际上没有收到特定的消息。这种行为被称为 **拜占庭故障(Byzantine fault)**,**在不信任的环境中达成共识的问题被称为拜占庭将军问题**【77】。 > ### 拜占庭将军问题 > -> 拜占庭将军问题是对所谓“两将军问题”的泛化【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在[第九章](ch9.md)讨论这个共识问题。 +> 拜占庭将军问题是对所谓 “两将军问题” 的泛化【78】,它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地,他们只能通过信使进行沟通,信使有时会被延迟或丢失(就像网络中的信息包一样)。我们将在 [第九章](ch9.md) 讨论这个共识问题。 > -> 在这个问题的拜占庭版本里,有n位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 +> 在这个问题的拜占庭版本里,有 n 位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。 > -> 拜占庭是后来成为君士坦丁堡的古希腊城市,现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反,这个名字来源于拜占庭式的过度复杂,官僚,迂回等意义,早在计算机之前就已经在政治中被使用了【79】。Lamport想要选一个不会冒犯任何读者的国家,他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意【80】。 +> 拜占庭是后来成为君士坦丁堡的古希腊城市,现在在土耳其的伊斯坦布尔。没有任何历史证据表明拜占庭将军比其他地方更容易出现阴谋和阴谋。相反,这个名字来源于拜占庭式的过度复杂,官僚,迂回等意义,早在计算机之前就已经在政治中被使用了【79】。Lamport 想要选一个不会冒犯任何读者的国家,他被告知将其称为阿尔巴尼亚将军问题并不是一个好主意【80】。 -当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为**拜占庭容错(Byzantine fault-tolerant)** 的,在特定场景下,这种担忧在是有意义的: +当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作,称之为 **拜占庭容错(Byzantine fault-tolerant)** 的,在特定场景下,这种担忧在是有意义的: -* 在航空航天环境中,计算机内存或CPU寄存器中的数据可能被辐射破坏,导致其以任意不可预知的方式响应其他节点。由于系统故障非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障【81,82】。 +* 在航空航天环境中,计算机内存或 CPU 寄存器中的数据可能被辐射破坏,导致其以任意不可预知的方式响应其他节点。由于系统故障非常昂贵(例如,飞机撞毁和炸死船上所有人员,或火箭与国际空间站相撞),飞行控制系统必须容忍拜占庭故障【81,82】。 * 在多个参与组织的系统中,一些参与者可能会试图欺骗或欺骗他人。在这种情况下,节点仅仅信任另一个节点的消息是不安全的,因为它们可能是出于恶意的目的而被发送的。例如,像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式,而不依赖于中心机构(central authority)【83】。 然而,在本书讨论的那些系统中,我们通常可以安全地假设没有拜占庭式的错误。在你的数据中心里,所有的节点都是由你的组织控制的(所以他们可以信任),辐射水平足够低,内存损坏不是一个大问题。制作拜占庭容错系统的协议相当复杂【84】,而容错嵌入式系统依赖于硬件层面的支持【81】。在大多数服务器端数据系统中,部署拜占庭容错解决方案的成本使其变得不切实际。 -Web应用程序确实需要预期受终端用户控制的客户端(如Web浏览器)的任意和恶意行为。这就是为什么输入验证,数据清洗和输出转义如此重要:例如,防止SQL注入和跨站点脚本。然而,我们通常不在这里使用拜占庭容错协议,而只是让服务器有权决定是否允许客户端行为。但在没有这种中心机构的对等网络中,拜占庭容错更为重要。 +Web 应用程序确实需要预期受终端用户控制的客户端(如 Web 浏览器)的任意和恶意行为。这就是为什么输入验证,数据清洗和输出转义如此重要:例如,防止 SQL 注入和跨站点脚本。然而,我们通常不在这里使用拜占庭容错协议,而只是让服务器有权决定是否允许客户端行为。但在没有这种中心机构的对等网络中,拜占庭容错更为重要。 -软件中的一个错误(bug)可能被认为是拜占庭式的错误,但是如果你将相同的软件部署到所有节点上,那么拜占庭式的容错算法帮不到你。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付bug,你必须有四个独立的相同软件的实现,并希望一个bug只出现在四个实现之一中。 +软件中的一个错误(bug)可能被认为是拜占庭式的错误,但是如果你将相同的软件部署到所有节点上,那么拜占庭式的容错算法帮不到你。大多数拜占庭式容错算法要求超过三分之二的节点能够正常工作(即,如果有四个节点,最多只能有一个故障)。要使用这种方法对付 bug,你必须有四个独立的相同软件的实现,并希望一个 bug 只出现在四个实现之一中。 同样,如果一个协议可以保护我们免受漏洞,安全渗透和恶意攻击,那么这将是有吸引力的。不幸的是,这也是不现实的:在大多数系统中,如果攻击者可以渗透一个节点,那他们可能会渗透所有这些节点,因为它们可能都运行着相同的软件。因此,传统机制(认证,访问控制,加密,防火墙等)仍然是抵御攻击者的主要保护措施。 #### 弱谎言形式 -尽管我们假设节点通常是诚实的,但值得向软件中添加防止“撒谎”弱形式的机制——例如,由硬件问题导致的无效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,因为它们不能抵挡决心坚定的对手,但它们仍然是简单而实用的步骤,以提高可靠性。例如: +尽管我们假设节点通常是诚实的,但值得向软件中添加防止 “撒谎” 弱形式的机制 —— 例如,由硬件问题导致的无效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,因为它们不能抵挡决心坚定的对手,但它们仍然是简单而实用的步骤,以提高可靠性。例如: -* 由于硬件问题或操作系统、驱动程序、路由器等中的错误,网络数据包有时会受到损坏。通常,损坏的数据包会被内建于TCP和UDP中的校验和所俘获,但有时它们也会逃脱检测【85,86,87】 。要对付这种破坏通常使用简单的方法就可以做到,例如应用程序级协议中的校验和。 +* 由于硬件问题或操作系统、驱动程序、路由器等中的错误,网络数据包有时会受到损坏。通常,损坏的数据包会被内建于 TCP 和 UDP 中的校验和所俘获,但有时它们也会逃脱检测【85,86,87】 。要对付这种破坏通常使用简单的方法就可以做到,例如应用程序级协议中的校验和。 * 可公开访问的应用程序必须仔细清理来自用户的任何输入,例如检查值是否在合理的范围内,并限制字符串的大小以防止通过大内存分配的拒绝服务。防火墙后面的内部服务对于输入也许可以只采取一些不那么严格的检查,但是采取一些基本的合理性检查(例如,在协议解析中)仍然是一个好主意。 -* NTP客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否对某个时间范围达成一致。只要大多数的服务器没问题,一个配置错误的NTP服务器报告的时间会被当成特异值从同步中排除【37】。使用多个服务器使NTP更健壮(比起只用单个服务器来)。 +* NTP 客户端可以配置多个服务器地址。同步时,客户端联系所有的服务器,估计它们的误差,并检查大多数服务器是否对某个时间范围达成一致。只要大多数的服务器没问题,一个配置错误的 NTP 服务器报告的时间会被当成特异值从同步中排除【37】。使用多个服务器使 NTP 更健壮(比起只用单个服务器来)。 ### 系统模型与现实 -已经有很多算法被设计以解决分布式系统问题——例如,我们将在[第九章](ch9.md)讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。 +已经有很多算法被设计以解决分布式系统问题 —— 例如,我们将在 [第九章](ch9.md) 讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。 算法的编写方式不应该过分依赖于运行的硬件和软件配置的细节。这就要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可以假设的事情。 @@ -559,30 +559,30 @@ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏 * 异步模型 - 在这个模型中,一个算法不允许对时序做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。 + 在这个模型中,一个算法不允许对时序做任何假设 —— 事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。 -进一步来说,除了时序问题,我们还要考虑**节点失效**。三种最常见的节点系统模型是: +进一步来说,除了时序问题,我们还要考虑 **节点失效**。三种最常见的节点系统模型是: -* 崩溃-停止故障 +* 崩溃 - 停止故障 - 在**崩溃停止(crash-stop)** 模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。 + 在 **崩溃停止(crash-stop)** 模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失 —— 它永远不会回来。 -* 崩溃-恢复故障 +* 崩溃 - 恢复故障 - 我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在**崩溃-恢复(crash-recovery)** 模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。 + 我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在 **崩溃 - 恢复(crash-recovery)** 模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。 * 拜占庭(任意)故障 节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点,如上一节所述。 -对于真实系统的建模,具有**崩溃-恢复故障(crash-recovery)** 的**部分同步模型(partial synchronous)** 通常是最有用的模型。分布式算法如何应对这种模型? +对于真实系统的建模,具有 **崩溃 - 恢复故障(crash-recovery)** 的 **部分同步模型(partial synchronous)** 通常是最有用的模型。分布式算法如何应对这种模型? #### 算法的正确性 为了定义算法是正确的,我们可以描述它的属性。例如,排序算法的输出具有如下特性:对于输出列表中的任何两个不同的元素,左边的元素比右边的元素小。这只是定义对列表进行排序含义的一种形式方式。 -同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(请参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性: +同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(请参阅 “[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性: * 唯一性(uniqueness) @@ -590,7 +590,7 @@ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏 * 单调序列(monotonic sequence) - 如果请求 $x$ 返回了令牌 $t_x$,并且请求$y$返回了令牌$t_y$,并且 $x$ 在 $y$ 开始之前已经完成,那么$t_x 好死还是赖活着? -> —— Jay Kreps, 关于Kafka与 Jepsen的若干笔记 (2013) +> 好死还是赖活着? +> —— Jay Kreps, 关于 Kafka 与 Jepsen 的若干笔记 (2013) --------------- [TOC] -正如[第八章](ch8.md)所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法—— 即使某些内部组件出现故障,服务也能正常运行。 +正如 [第八章](ch8.md) 所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法 —— 即使某些内部组件出现故障,服务也能正常运行。 -在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设[第八章](ch8.md)的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。 +在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设 [第八章](ch8.md) 的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复递送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃。 -构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与[第七章](ch7.md)中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。 +构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与 [第七章](ch7.md) 中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。 -现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是**共识(consensus)**:**就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,要可靠地达成共识,且不被网络故障和进程故障所影响,是一个令人惊讶的棘手问题。 +现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是 **共识(consensus)**:**就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,要可靠地达成共识,且不被网络故障和进程故障所影响,是一个令人惊讶的棘手问题。 -一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在“[处理节点宕机](ch5.md#处理节点宕机)”中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为**脑裂(split brain)**,它经常会导致数据丢失。正确实现共识有助于避免这种问题。 +一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在 “[处理节点宕机](ch5.md#处理节点宕机)” 中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为 **脑裂(split brain)**,它经常会导致数据丢失。正确实现共识有助于避免这种问题。 -在本章后面的“[分布式事务与共识](#分布式事务与共识)”中,我们将研究解决共识和相关问题的算法。但首先,我们首先需要探索可以在分布式系统中提供的保证和抽象的范围。 +在本章后面的 “[分布式事务与共识](#分布式事务与共识)” 中,我们将研究解决共识和相关问题的算法。但首先,我们首先需要探索可以在分布式系统中提供的保证和抽象的范围。 我们需要了解可以做什么和不可以做什么的范围:在某些情况下,系统可以容忍故障并继续工作;在其他情况下,这是不可能的。我们将深入研究什么可能而什么不可能的限制,既通过理论证明,也通过实际实现。我们将在本章中概述这些基本限制。 -分布式系统领域的研究人员几十年来一直在研究这些主题,所以有很多资料—— 我们只能介绍一些皮毛。在本书中,我们没有空间去详细介绍形式模型和证明的细节,所以我们将坚持非正式的直觉。如果你有兴趣,参考文献可以提供更多的深度。 +分布式系统领域的研究人员几十年来一直在研究这些主题,所以有很多资料 —— 我们只能介绍一些皮毛。在本书中,我们没有空间去详细介绍形式模型和证明的细节,所以我们将坚持非正式的直觉。如果你有兴趣,参考文献可以提供更多的深度。 ## 一致性保证 -在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。 +在 “[复制延迟问题](ch5.md#复制延迟问题)” 中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。 -大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。 +大多数复制的数据库至少提供了 **最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是 **收敛(convergence)**,因为我们预计所有的副本最终会收敛到相同的值【2】。 -然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚才写入的值,因为读请求可能会被路由到另外的副本上。(请参阅“[读己之写](ch5.md#读己之写)” )。 +然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有【1】。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚才写入的值,因为读请求可能会被路由到另外的副本上。(请参阅 “[读己之写](ch5.md#读己之写)” )。 对于应用开发人员而言,最终一致性是很困难的,因为它与普通单线程程序中变量的行为有很大区别。对于后者,如果将一个值赋给一个变量,然后很快地再次读取,不可能读到旧的值,或者读取失败。数据库表面上看起来像一个你可以读写的变量,但实际上它有更复杂的语义【3】。 @@ -40,111 +40,111 @@ 本章将探索数据系统可能选择提供的更强一致性模型。它不是免费的:具有较强保证的系统可能会比保证较差的系统具有更差的性能或更少的容错性。尽管如此,更强的保证能够吸引人,因为它们更容易用对。只有见过不同的一致性模型后,才能更好地决定哪一个最适合自己的需求。 -**分布式一致性模型**和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(请参阅“[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了**避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于**在面对延迟和故障时如何协调副本间的状态**。 +**分布式一致性模型** 和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】(请参阅 “[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了 **避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于 **在面对延迟和故障时如何协调副本间的状态**。 本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的: -* 首先看一下常用的**最强一致性模型**之一,**线性一致性(linearizability)**,并考察其优缺点。 -* 然后我们将检查分布式系统中[**事件顺序**](#顺序保证)的问题,特别是因果关系和全局顺序的问题。 +* 首先看一下常用的 **最强一致性模型** 之一,**线性一致性(linearizability)**,并考察其优缺点。 +* 然后我们将检查分布式系统中 [**事件顺序**](#顺序保证) 的问题,特别是因果关系和全局顺序的问题。 * 在第三节的(“[分布式事务与共识](#分布式事务与共识)”)中将探讨如何原子地提交分布式事务,这将最终引领我们走向共识问题的解决方案。 ## 线性一致性 -在**最终一致**的数据库,如果你在同一时刻问两个不同副本相同的问题,可能会得到两个不同的答案。这很让人困惑。如果数据库可以提供只有一个副本的假象(即,只有一个数据副本),那么事情就简单太多了。那么每个客户端都会有相同的数据视图,且不必担心复制滞后了。 +在 **最终一致** 的数据库,如果你在同一时刻问两个不同副本相同的问题,可能会得到两个不同的答案。这很让人困惑。如果数据库可以提供只有一个副本的假象(即,只有一个数据副本),那么事情就简单太多了。那么每个客户端都会有相同的数据视图,且不必担心复制滞后了。 -这就是**线性一致性(linearizability)** 背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)** 或**外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。 +这就是 **线性一致性(linearizability)** 背后的想法【6】(也称为 **原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)** 或 **外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。 -在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。要维护数据的单个副本的假象,系统应保障读到的值是最近的、最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。 +在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。要维护数据的单个副本的假象,系统应保障读到的值是最近的、最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个 **新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。 ![](img/fig9-1.png) -**图9-1 这个系统是非线性一致的,导致了球迷的困惑** +**图 9-1 这个系统是非线性一致的,导致了球迷的困惑** -[图9-1 ](img/fig9-1.png)展示了一个关于体育网站的非线性一致例子【9】。Alice和Bob正坐在同一个房间里,都盯着各自的手机,关注着2014年FIFA世界杯决赛的结果。在最后得分公布后,Alice刷新页面,看到宣布了获胜者,并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。 +[图 9-1](img/fig9-1.png) 展示了一个关于体育网站的非线性一致例子【9】。Alice 和 Bob 正坐在同一个房间里,都盯着各自的手机,关注着 2014 年 FIFA 世界杯决赛的结果。在最后得分公布后,Alice 刷新页面,看到宣布了获胜者,并兴奋地告诉 Bob。Bob 难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。 -如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分**之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。 +如果 Alice 和 Bob 在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而 Bob 是在听到 Alice 惊呼最后得分 **之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。 ### 什么使得系统线性一致? 线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。 -[图9-2](img/fig9-2.png) 显示了三个客户端在线性一致数据库中同时读写相同的键`x`。在分布式系统文献中,`x`被称为**寄存器(register)**,例如,它可以是键值存储中的一个**键**,关系数据库中的一**行**,或文档数据库中的一个**文档**。 +[图 9-2](img/fig9-2.png) 显示了三个客户端在线性一致数据库中同时读写相同的键 `x`。在分布式系统文献中,`x` 被称为 **寄存器(register)**,例如,它可以是键值存储中的一个 **键**,关系数据库中的一 **行**,或文档数据库中的一个 **文档**。 ![](img/fig9-2.png) -**图9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值** +**图 9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值** -为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i] +为了简单起见,[图 9-2](img/fig9-2.png) 采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间 —— 只知道它发生在发送请求和接收响应的之间的某个时刻。[^i] -[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。 +[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和 NTP 产生的实时逼近。 在这个例子中,寄存器有两种类型的操作: * $read(x)⇒v$表示客户端请求读取寄存器 `x` 的值,数据库返回值 `v`。 * $write(x,v)⇒r$ 表示客户端请求将寄存器 `x` 设置为值 `v` ,数据库返回响应 `r` (可能正确,可能错误)。 -在[图9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端C 执行写请求将其设置为 `1`。发生这种情况时,客户端A和B反复轮询数据库以读取最新值。 A和B的请求可能会收到怎样的响应? +在 [图 9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端 C 执行写请求将其设置为 `1`。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。 A 和 B 的请求可能会收到怎样的响应? -* 客户端A的第一个读操作,完成于写操作开始之前,因此必须返回旧值 `0`。 -* 客户端A的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。 -* 与写操作在时间上重叠的任何读操作,可能会返回 `0` 或 `1` ,因为我们不知道读取时,写操作是否已经生效。这些操作是**并发(concurrent)** 的。 +* 客户端 A 的第一个读操作,完成于写操作开始之前,因此必须返回旧值 `0`。 +* 客户端 A 的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则必须在写入之后处理读取,因此它必须看到写入的新值。 +* 与写操作在时间上重叠的任何读操作,可能会返回 `0` 或 `1` ,因为我们不知道读取时,写操作是否已经生效。这些操作是 **并发(concurrent)** 的。 -但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真“单一数据副本”的系统。[^ii] +但是,这还不足以完全描述线性一致性:如果与写入同时发生的读取可以返回旧值或新值,那么读者可能会在写入期间看到数值在旧值和新值之间来回翻转。这不是我们所期望的仿真 “单一数据副本” 的系统。[^ii] -[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为**常规寄存器(regular register)**【7,25】 +[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为 **常规寄存器(regular register)**【7,25】 -为了使系统线性一致,我们需要添加另一个约束,如[图9-3](img/fig9-3.png)所示 +为了使系统线性一致,我们需要添加另一个约束,如 [图 9-3](img/fig9-3.png) 所示 ![](img/fig9-3.png) -**图9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。** +**图 9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。** -在一个线性一致的系统中,我们可以想象,在 `x` 的值从`0` 自动翻转到 `1` 的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值 `1`,即使写操作尚未完成,所有后续读取也必须返回新值。 +在一个线性一致的系统中,我们可以想象,在 `x` 的值从 `0` 自动翻转到 `1` 的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值 `1`,即使写操作尚未完成,所有后续读取也必须返回新值。 -[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A 是第一个读取新的值 `1` 的位置。在A 的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回 `1`(与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值)。 +[图 9-3](img/fig9-3.png) 中的箭头说明了这个时序依赖关系。客户端 A 是第一个读取新的值 `1` 的位置。在 A 的读取返回之后,B 开始新的读取。由于 B 的读取严格在发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 `1`(与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同:在 Alice 读取新值之后,Bob 也希望读取新的值)。 -我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。 +我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图 9-4](img/fig9-4.png) 显示了一个更复杂的例子【10】。 -在[图9-4](img/fig9-4.png)中,除了读写之外,还增加了第三种类型的操作: +在 [图 9-4](img/fig9-4.png) 中,除了读写之外,还增加了第三种类型的操作: -* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的[**比较与设置**](ch7.md#比较并设置(CAS))操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。 +* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](ch7.md#比较并设置(CAS)) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x≠v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。 $r$ 是数据库的响应(正确或错误)。 -[图9-4](img/fig9-4.png)中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。 +[图 9-4](img/fig9-4.png) 中的每个操作都在我们认为执行操作的时候用竖线标出(在每个操作的条柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。 线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜度保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。 ![](img/fig9-4.png) -**图9-4 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是线性一致性的** +**图 9-4 可视化读取和写入看起来已经生效的时间点。 B 的最后读取不是线性一致性的** -[图9-4](img/fig9-4.png)中有一些有趣的细节需要指出: +[图 9-4](img/fig9-4.png) 中有一些有趣的细节需要指出: -* 第一个客户端B发送一个读取 `x` 的请求,然后客户端D发送一个请求将 `x` 设置为 `0`,然后客户端A发送请求将 `x` 设置为 `1`。尽管如此,返回到B的读取值为 `1`(由A写入的值)。这是可以的:这意味着数据库首先处理D的写入,然后是A的写入,最后是B的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许B的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。 +* 第一个客户端 B 发送一个读取 `x` 的请求,然后客户端 D 发送一个请求将 `x` 设置为 `0`,然后客户端 A 发送请求将 `x` 设置为 `1`。尽管如此,返回到 B 的读取值为 `1`(由 A 写入的值)。这是可以的:这意味着数据库首先处理 D 的写入,然后是 A 的写入,最后是 B 的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许 B 的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。 -* 在客户端A从数据库收到响应之前,客户端B的读取返回 `1` ,表示写入值 `1` 已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端A的正确响应在网络中略有延迟。 +* 在客户端 A 从数据库收到响应之前,客户端 B 的读取返回 `1` ,表示写入值 `1` 已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端 A 的正确响应在网络中略有延迟。 -* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由B更改。可以使用原子**比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B和C的**cas**请求成功,但是D的**cas**请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 +* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C 首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由 B 更改。可以使用原子 **比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B 和 C 的 **cas** 请求成功,但是 D 的 **cas** 请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 -* 客户B的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与C的**cas**写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B的读取返回 `2` 是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值 `4` ,因此不允许B读取比A更旧的值。再次,与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。 +* 客户 B 的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与 C 的 **cas** 写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。 这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,以测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。 > ### 线性一致性与可串行化 > -> **线性一致性**容易和[**可串行化**](ch7.md#可串行化)相混淆,因为两个词似乎都是类似“可以按顺序排列”的东西。但它们是两种完全不同的保证,区分两者非常重要: +> **线性一致性** 容易和 [**可串行化**](ch7.md#可串行化) 相混淆,因为两个词似乎都是类似 “可以按顺序排列” 的东西。但它们是两种完全不同的保证,区分两者非常重要: > > ***可串行化*** > -> **可串行化(Serializability)** 是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。 +> **可串行化(Serializability)** 是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)—— 请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照 **某种** 顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。 > > ***线性一致性*** > -> **线性一致性(Linearizability)** 是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 +> **线性一致性(Linearizability)** 是读取和写入寄存器(单个对象)的 **新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅 “[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如 [物化冲突](ch7.md#物化冲突))。 > -> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或**强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”一节)或**真的串行执行**(请参阅第“[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。 +> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或 **强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)” 一节)或 **真的串行执行**(请参阅第 “[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。 > -> 但是,可串行化的快照隔离(请参阅“[可串行化快照隔离](ch7.md#可串行化快照隔离)”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于**它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。 +> 但是,可串行化的快照隔离(请参阅 “[可串行化快照隔离](ch7.md#可串行化快照隔离)”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于 **它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。 ### 依赖线性一致性 @@ -155,98 +155,98 @@ 一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【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部署通常具有用于数据库节点之间通信的专用集群互连网络。 +分布式锁也在一些分布式数据库(如 Oracle Real Application Clusters(RAC)【18】)中更多的粒度级别上使用。RAC 对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC 部署通常具有用于数据库节点之间通信的专用集群互连网络。 #### 约束和唯一性保证 唯一性约束在数据库中很常见:例如,用户名或电子邮件地址必须唯一标识一个用户,而在文件存储服务中,不能有两个具有相同路径和文件名的文件。如果要在写入数据时强制执行此约束(例如,如果两个人试图同时创建一个具有相同名称的用户或文件,其中一个将返回一个错误),则需要线性一致性。 -这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的“锁定”。该操作与原子性的比较与设置(CAS)非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。 +这种情况实际上类似于一个锁:当一个用户注册你的服务时,可以认为他们获得了所选用户名的 “锁定”。该操作与原子性的比较与设置(CAS)非常相似:将用户名赋予声明它的用户,前提是用户名尚未被使用。 如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。 -在实际应用中,宽松地处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在“[及时性与完整性](ch12.md#及时性与完整性)”中讨论这种宽松的约束。 +在实际应用中,宽松地处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在 “[及时性与完整性](ch12.md#及时性与完整性)” 中讨论这种宽松的约束。 然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以不需要线性一致性【19】。 #### 跨信道的时序依赖 -注意[图9-1](img/fig9-1.png) 中的一个细节:如果Alice没有惊呼得分,Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外的信道(Alice的声音传到了Bob的耳朵中),线性一致性的违背才被注意到。 +注意 [图 9-1](img/fig9-1.png) 中的一个细节:如果 Alice 没有惊呼得分,Bob 就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外的信道(Alice 的声音传到了 Bob 的耳朵中),线性一致性的违背才被注意到。 -计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。 +计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如 [图 9-5](img/fig9-5.png) 所示。 -图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(请参阅[第十一章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。 +图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的(请参阅 [第十一章](ch11.md))。 Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。 ![](img/fig9-5.png) -**图9-5 Web服务器和图像缩放器通过文件存储和消息队列进行通信,打开竞争条件的可能性。** +**图 9-5 Web 服务器和图像缩放器通过文件存储和消息队列进行通信,打开竞争条件的可能性。** -如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图9-5](img/fig9-5.png)中的步骤3和4)可能比存储服务内部的复制(replication)更快。在这种情况下,当缩放器读取图像(步骤5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和缩略图就产生了永久性的不一致。 +如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图 9-5](img/fig9-5.png) 中的步骤 3 和 4)可能比存储服务内部的复制(replication)更快。在这种情况下,当缩放器读取图像(步骤 5)时,可能会看到图像的旧版本,或者什么都没有。如果它处理的是旧版本的图像,则文件存储中的全尺寸图和缩略图就产生了永久性的不一致。 -出现这个问题是因为Web服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于[图9-1](img/fig9-1.png),数据库复制与Alice的嘴到Bob耳朵之间的真人音频信道之间也存在竞争条件。 +出现这个问题是因为 Web 服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于 [图 9-1](img/fig9-1.png),数据库复制与 Alice 的嘴到 Bob 耳朵之间的真人音频信道之间也存在竞争条件。 -线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在Alice和Bob的例子),则可以使用在“[读己之写](ch5.md#读己之写)”讨论过的类似方法,不过会有额外的复杂度代价。 +线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在 Alice 和 Bob 的例子),则可以使用在 “[读己之写](ch5.md#读己之写)” 讨论过的类似方法,不过会有额外的复杂度代价。 ### 实现线性一致的系统 我们已经见到了几个线性一致性有用的例子,让我们思考一下,如何实现一个提供线性一致语义的系统。 -由于线性一致性本质上意味着“表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。 +由于线性一致性本质上意味着 “表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。 -使系统容错最常用的方法是使用复制。我们再来回顾[第五章](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#处理节点宕机)”),这同时违反了持久性和线性一致性。 * 共识算法(线性一致) - 一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。正是由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。 + 一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。正是由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和 etcd 【22】就是这样工作的。 * 多主复制(非线性一致) - 具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅“[处理写入冲突](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#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。 #### 线性一致性和法定人数 -直觉上在Dynamo风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件,如[图9-6](img/fig9-6.png)所示。 +直觉上在 Dynamo 风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件,如 [图 9-6](img/fig9-6.png) 所示。 ![](img/fig9-6.png) -**图9-6 非线性一致的执行,尽管使用了严格的法定人数** +**图 9-6 非线性一致的执行,尽管使用了严格的法定人数** -在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0,写入客户端通过向所有三个副本( $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端A并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端B也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0` 。 +在 [图 9-6](img/fig9-6.png) 中,$x$ 的初始值为 0,写入客户端通过向所有三个副本( $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端 A 并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端 B 也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0` 。 -法定人数条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1](img/fig9-1.png)) +法定人数条件满足( $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】。 -总而言之,最安全的做法是:假设采用Dynamo风格无主复制的系统不能提供线性一致性。 +总而言之,最安全的做法是:假设采用 Dynamo 风格无主复制的系统不能提供线性一致性。 ### 线性一致性的代价 一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。 -我们已经在[第五章](ch5.md)中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(请参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。 +我们已经在 [第五章](ch5.md) 中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(请参阅 “[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图 9-7](img/fig9-7.png) 说明了这种部署的一个例子。 ![](img/fig9-7.png) -**图9-7 网络中断迫使在线性一致性和可用性之间做出选择。** +**图 9-7 网络中断迫使在线性一致性和可用性之间做出选择。** 考虑这样一种情况:如果两个数据中心之间发生网络中断会发生什么?我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心之间彼此无法互相连接。 @@ -262,101 +262,101 @@ 这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v] -* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用**)。 +* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都 **不可用**)。 * 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。 -[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。 +[^v]: 这两种选择有时分别称为 CP(在网络分区下一致但不可用)和 AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。 -因此,不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。 +因此,不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为 CAP 定理【29,30,31,32】,由 Eric Brewer 于 2000 年命名,尽管 70 年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。 -CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬 —— 它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。 +CAP 最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP 定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP 值得赞扬 —— 它见证了自 00 年代中期以来新数据库的技术爆炸(即 NoSQL)。 > #### CAP定理没有帮助 > -> CAP有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种故障类型,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。 +> CAP 有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种故障类型,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。 > -> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,CAP更好的表述成:在分区时要么选择一致,要么选择可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。 +> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,CAP 更好的表述成:在分区时要么选择一致,要么选择可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。 > -> 在CAP的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”(容错)系统实际上不符合CAP对可用性的特殊定义。总而言之,围绕着CAP有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用CAP。 +> 在 CAP 的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的 “高可用”(容错)系统实际上不符合 CAP 对可用性的特殊定义。总而言之,围绕着 CAP 有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用 CAP。 -CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区[^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。 因此,尽管CAP在历史上有一些影响力,但对于设计系统而言并没有实际价值【9,40】。 +CAP 定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区 [^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。 因此,尽管 CAP 在历史上有一些影响力,但对于设计系统而言并没有实际价值【9,40】。 -在分布式系统中有更多有趣的“不可能”的结果【41】,且CAP定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。 +在分布式系统中有更多有趣的 “不可能” 的结果【41】,且 CAP 定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。 -[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)** 指代将大数据集细分为小数据集的操作(分片;请参阅[第六章](ch6.md))。与之对应的是,**网络分区(network partition)** 是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下我们无法避免混乱。 +[^vi]: 正如 “[真实世界的网络故障](ch8.md#真实世界的网络故障)” 中所讨论的,本书使用 **分区(partition)** 指代将大数据集细分为小数据集的操作(分片;请参阅 [第六章](ch6.md))。与之对应的是,**网络分区(network partition)** 是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是 CAP 的 P,所以这种情况下我们无法避免混乱。 #### 线性一致性和网络延迟 -虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核CPU上的内存甚至都不是线性一致的【43】:如果一个CPU核上运行的线程写入某个内存地址,而另一个CPU核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了**内存屏障(memory barrier)** 或**围栏(fence)**【44】)。 +虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核 CPU 上的内存甚至都不是线性一致的【43】:如果一个 CPU 核上运行的线程写入某个内存地址,而另一个 CPU 核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了 **内存屏障(memory barrier)** 或 **围栏(fence)**【44】)。 -这种行为的原因是每个CPU核都有自己的内存缓存和存储缓冲区。默认情况下,内存访问首先走缓存,任何变更会异步写入主存。因为缓存访问比主存要快得多【45】,所以这个特性对于现代CPU的良好性能表现至关重要。但是现在就有几个数据副本(一个在主存中,也许还有几个在不同缓存中的其他副本),而且这些副本是异步更新的,所以就失去了线性一致性。 +这种行为的原因是每个 CPU 核都有自己的内存缓存和存储缓冲区。默认情况下,内存访问首先走缓存,任何变更会异步写入主存。因为缓存访问比主存要快得多【45】,所以这个特性对于现代 CPU 的良好性能表现至关重要。但是现在就有几个数据副本(一个在主存中,也许还有几个在不同缓存中的其他副本),而且这些副本是异步更新的,所以就失去了线性一致性。 -为什么要做这个权衡?对多核内存一致性模型而言,CAP定理是没有意义的:在同一台计算机中,我们通常假定通信都是可靠的。并且我们并不指望一个CPU核能在脱离计算机其他部分的条件下继续正常工作。牺牲线性一致性的原因是**性能(performance)**,而不是容错。 +为什么要做这个权衡?对多核内存一致性模型而言,CAP 定理是没有意义的:在同一台计算机中,我们通常假定通信都是可靠的。并且我们并不指望一个 CPU 核能在脱离计算机其他部分的条件下继续正常工作。牺牲线性一致性的原因是 **性能(performance)**,而不是容错。 -许多分布式数据库也是如此:它们是**为了提高性能**而选择了牺牲线性一致性,而不是为了容错【46】。线性一致的速度很慢——这始终是事实,而不仅仅是网络故障期间。 +许多分布式数据库也是如此:它们是 **为了提高性能** 而选择了牺牲线性一致性,而不是为了容错【46】。线性一致的速度很慢 —— 这始终是事实,而不仅仅是网络故障期间。 -能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在[第十二章](ch12.md)中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。 +能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya 和 Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参阅 “[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在 [第十二章](ch12.md) 中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。 ## 顺序保证 -之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了[图9-4](img/fig9-4.png)中的顺序。 +之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了 [图 9-4](img/fig9-4.png) 中的顺序。 -**顺序(ordering)** 这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文: +**顺序(ordering)** 这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过 **顺序** 的上下文: -* 在[第五章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。 -* 在[第七章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序(some sequential order)** 执行的保证。它可以字面意义上地以**串行顺序(serial order)** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。 -* 在[第八章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。 +* 在 [第五章](ch5.md) 中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定 **写入顺序(order of write)**—— 也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅 “[处理写入冲突](ch5.md#处理写入冲突)”)。 +* 在 [第七章](ch7.md) 中讨论的 **可串行化**,是关于事务表现的像按 **某种先后顺序(some sequential order)** 执行的保证。它可以字面意义上地以 **串行顺序(serial order)** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。 +* 在 [第八章](ch8.md) 讨论过的在分布式系统中使用时间戳和时钟(请参阅 “[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。 事实证明,顺序、线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。 ### 顺序与因果关系 -**顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系(causality)**。在本书中我们已经看到了几个例子,其中因果关系是很重要的: +**顺序** 反复出现有几个原因,其中一个原因是,它有助于保持 **因果关系(causality)**。在本书中我们已经看到了几个例子,其中因果关系是很重要的: -* 在“[一致前缀读](ch5.md#一致前缀读)”([图5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对**因(cause)** 与**果(effect)** 的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在**因果依赖(causal dependency)**。 -* [图5-9](img/fig5-9.png)中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会“压倒”其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。 -* 在“[检测并发写入](ch5.md#检测并发写入)”中我们观察到,如果有两个操作A和B,则存在三种可能性:A发生在B之前,或B发生在A之前,或者A和B**并发**。这种**此前发生(happened before)** 关系是因果关系的另一种表述:如果A在B前发生,那么意味着B可能已经知道了A,或者建立在A的基础上,或者依赖于A。如果A和B是**并发**的,那么它们之间并没有因果联系;换句话说,我们确信A和B不知道彼此。 -* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)** 意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6.png)所示)。 -* 事务之间**写偏差(write skew)** 的例子(请参阅“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离)通过跟踪事务之间的因果依赖来检测写偏差。 -* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。 +* 在 “[一致前缀读](ch5.md#一致前缀读)”([图 5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对 **因(cause)** 与 **果(effect)** 的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在 **因果依赖(causal dependency)**。 +* [图 5-9](img/fig5-9.png) 中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会 “压倒” 其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。 +* 在 “[检测并发写入](ch5.md#检测并发写入)” 中我们观察到,如果有两个操作 A 和 B,则存在三种可能性:A 发生在 B 之前,或 B 发生在 A 之前,或者 A 和 B**并发**。这种 **此前发生(happened before)** 关系是因果关系的另一种表述:如果 A 在 B 前发生,那么意味着 B 可能已经知道了 A,或者建立在 A 的基础上,或者依赖于 A。如果 A 和 B 是 **并发** 的,那么它们之间并没有因果联系;换句话说,我们确信 A 和 B 不知道彼此。 +* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中 “一致” 到底又是什么意思?这意味着 **与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)** 意味着读取的数据处于违反因果关系的状态(不可重复读,如 [图 7-6](img/fig7-6.png) 所示)。 +* 事务之间 **写偏差(write skew)** 的例子(请参阅 “[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在 [图 7-8](img/fig7-8.png) 中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离) 通过跟踪事务之间的因果依赖来检测写偏差。 +* 在爱丽丝和鲍勃看球的例子中([图 9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在 “[跨信道的时序依赖](#跨信道的时序依赖)” 一节中,以 “图像大小调整服务” 的伪装再次出现。 -因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。 +因果关系对事件施加了一种 **顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。 -如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致(causally consistent)** 的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。 +如果一个系统服从因果关系所规定的顺序,我们说它是 **因果一致(causally consistent)** 的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。 #### 因果顺序不是全序的 -**全序(total order)** 允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数,比如说5和13,那么你可以告诉我,13大于5。 +**全序(total order)** 允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数,比如说 5 和 13,那么你可以告诉我,13 大于 5。 -然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较(incomparable)** 的,因此数学集合是**偏序(partially order)** 的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]。 +然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是 **无法比较(incomparable)** 的,因此数学集合是 **偏序(partially order)** 的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的 [^译注i]。 -[^译注i]: 设R为非空集合A上的关系,如果R是自反的、反对称的和可传递的,则称R为A上的偏序关系。简称偏序,通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集,记作$(A,R)$或$(A, ≦)$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。 +[^译注i]: 设 R 为非空集合 A 上的关系,如果 R 是自反的、反对称的和可传递的,则称 R 为 A 上的偏序关系。简称偏序,通常记作≦。一个集合 A 与 A 上的偏序关系 R 一起叫作偏序集,记作 $(A,R)$ 或 $(A, ≦)$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。 全序和偏序之间的差异反映在不同的数据库一致性模型中: * 线性一致性 - 在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在[图9-4](img/fig9-4.png)中以时间线表示。 + 在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在 [图 9-4](img/fig9-4.png) 中以时间线表示。 * 因果性 - 我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(请参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。 + 我们说过,如果两个操作都没有在彼此 **之前发生**,那么这两个操作是并发的(请参阅 [“此前发生” 的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。 因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。 -并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。 +并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在 [第五章](ch5.md) 中我们看到了这种现象:例如,[图 5-14](img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。 -如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)** 发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)** 会在这些并发创建的提交相融合时创建。 +如果你熟悉像 Git 这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个 **提交(Commit)** 发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)** 会在这些并发创建的提交相融合时创建。 #### 线性一致性强于因果一致性 -那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着(implies)** 因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如[图9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 +那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性 **隐含着(implies)** 因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如 [图 9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 -线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。 +线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如 “[线性一致性的代价](#线性一致性的代价)” 中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。 -好消息是存在折衷的可能性。线性一致性并不是保持因果性的唯一途径 —— 还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其对于CAP定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。 +好消息是存在折衷的可能性。线性一致性并不是保持因果性的唯一途径 —— 还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其对于 CAP 定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。 在许多情况下,看上去需要线性一致性的系统,实际上需要的只是因果一致性,因果一致性可以更高效地实现。基于这种观察结果,研究人员正在探索新型的数据库,既能保证因果一致性,且性能与可用性与最终一致的系统类似【49,50,51】。 @@ -368,34 +368,34 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 为了维持因果性,你需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 -为了确定因果依赖,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y 的请求时已经看到了 X的值,则 X 和 Y 可能存在因果关系。这个分析使用了那些在欺诈指控刑事调查中常见的问题:CEO在做出决定 Y 时是否**知道** X ? +为了确定因果依赖,我们需要一些方法来描述系统中节点的 “知识”。如果节点在发出写入 Y 的请求时已经看到了 X 的值,则 X 和 Y 可能存在因果关系。这个分析使用了那些在欺诈指控刑事调查中常见的问题:CEO 在做出决定 Y 时是否 **知道** X ? -用于确定*哪些操作发生在其他操作之前* 的技术,与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性:为了防止丢失更新,我们需要检测到对同一个键的并发写入。因果一致性则更进一步:它需要跟踪整个数据库中的因果依赖,而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。 +用于确定 *哪些操作发生在其他操作之前* 的技术,与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性:为了防止丢失更新,我们需要检测到对同一个键的并发写入。因果一致性则更进一步:它需要跟踪整个数据库中的因果依赖,而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。 -为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图5-13 ](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI 的冲突检测中会出现类似的想法,如“[可串行化快照隔离](ch7.md#可串行化快照隔离)”中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。 +为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图 5-13](img/fig5-13.png) 中,来自先前操作的版本号在写入时被传回到数据库的原因。在 SSI 的冲突检测中会出现类似的想法,如 “[可串行化快照隔离](ch7.md#可串行化快照隔离)” 中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。 ### 序列号顺序 虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。 -但还有一个更好的方法:我们可以使用**序列号(sequence nunber)** 或**时间戳(timestamp)** 来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 +但还有一个更好的方法:我们可以使用 **序列号(sequence nunber)** 或 **时间戳(timestamp)** 来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个 **逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每个操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。 -特别是,我们可以使用**与因果一致(consistent with causality)** 的全序来生成序列号[^vii]:我们保证,如果操作 A 因果地发生在操作 B 前,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。 +特别是,我们可以使用 **与因果一致(consistent with causality)** 的全序来生成序列号 [^vii]:我们保证,如果操作 A 因果地发生在操作 B 前,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。 -[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的UUID,并按照字典序比较UUID,以定义操作的全序。这是一个有效的全序,但是随机的UUID并不能告诉你哪个操作先发生,或者操作是否为并发的。 +[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的 UUID,并按照字典序比较 UUID,以定义操作的全序。这是一个有效的全序,但是随机的 UUID 并不能告诉你哪个操作先发生,或者操作是否为并发的。 -在单主复制的数据库中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。 +在单主复制的数据库中(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。 #### 非因果序列号生成器 如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法: * 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。 -* 可以将日历时钟(物理时钟)的时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入胜利* 的冲突解决方法中(请参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 -* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,而节点 B 可能要求序列号1,001到2,000区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。 +*可以将日历时钟(物理时钟)的时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于* 最后写入胜利 * 的冲突解决方法中(请参阅 “[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 +* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号 1 到 1,000 区块的所有权,而节点 B 可能要求序列号 1,001 到 2,000 区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。 这三个选项都比单一主库的自增计数器表现要好,并且更具可伸缩性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。 @@ -403,29 +403,29 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 * 每个节点每秒可以处理不同数量的操作。因此,如果一个节点产生偶数序列号而另一个产生奇数序列号,则偶数计数器可能落后于奇数计数器,反之亦然。如果你有一个奇数编号的操作和一个偶数编号的操作,你无法准确地说出哪一个操作在因果上先发生。 -* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如[图8-3](img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^vii] +* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如 [图 8-3](img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^vii] - [^viii]: 可以使物理时钟时间戳与因果关系保持一致:在“[全局快照的同步时钟](#全局快照的同步时钟)”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。这种方法确保了实际上靠后的事务会有更大的时间戳。但是大多数时钟不能提供这种所需的不确定性度量。 + [^viii]: 可以使物理时钟时间戳与因果关系保持一致:在 “[全局快照的同步时钟](#全局快照的同步时钟)” 中,我们讨论了 Google 的 Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。这种方法确保了实际上靠后的事务会有更大的时间戳。但是大多数时钟不能提供这种所需的不确定性度量。 -* 在分配区块的情况下,某个操作可能会被赋予一个范围在1,001到2,000内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在1到1,000之间的数字。这里序列号与因果关系也是不一致的。 +* 在分配区块的情况下,某个操作可能会被赋予一个范围在 1,001 到 2,000 内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在 1 到 1,000 之间的数字。这里序列号与因果关系也是不一致的。 #### 兰伯特时间戳 -尽管刚才描述的三个序列号生成器与因果不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯特(Leslie Lamport)于1978年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 +尽管刚才描述的三个序列号生成器与因果不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利・兰伯特(Leslie Lamport)于 1978 年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 -[图9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。 兰伯特时间戳就是两者的简单组合:(计数器,节点ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点ID,每个时间戳都是唯一的。 +[图 9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。 兰伯特时间戳就是两者的简单组合:(计数器,节点 ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID,每个时间戳都是唯一的。 ![](img/fig9-8.png) -**图9-8 Lamport时间戳提供了与因果关系一致的全序。** +**图 9-8 Lamport 时间戳提供了与因果关系一致的全序。** -兰伯特时间戳与物理的日历时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则**计数器**值大者是更大的时间戳。如果计数器值相同,则节点ID越大的,时间戳越大。 +兰伯特时间戳与物理的日历时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则 **计数器** 值大者是更大的时间戳。如果计数器值相同,则节点 ID 越大的,时间戳越大。 -迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大**计数器**值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。 +迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大 **计数器** 值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。 -这如 [图9-8](img/fig9-8.png) 所示,其中客户端 A 从节点2 接收计数器值 `5` ,然后将最大值 `5` 发送到节点1 。此时,节点1 的计数器仅为 `1` ,但是它立即前移至 `5` ,所以下一个操作的计数器的值为 `6` 。 +这如 [图 9-8](img/fig9-8.png) 所示,其中客户端 A 从节点 2 接收计数器值 `5` ,然后将最大值 `5` 发送到节点 1 。此时,节点 1 的计数器仅为 `1` ,但是它立即前移至 `5` ,所以下一个操作的计数器的值为 `6` 。 只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。 @@ -435,11 +435,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。 -例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败(我们之前在“[领导者和锁](ch8.md#领导者和锁)”中提到过这个问题)。 +例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败(我们之前在 “[领导者和锁](ch8.md#领导者和锁)” 中提到过这个问题)。 乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。 -这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上(right now)** 决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存在其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。 +这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要 **马上(right now)** 决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存在其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。 为了确保没有其他节点正在使用相同的用户名和较小的时间戳并发创建同名账户,你必须检查其它每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题出现故障或不可达,则整个系统可能被拖至停机。这不是我们需要的那种容错系统。 @@ -447,15 +447,15 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 总之:为了实现诸如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。 -如何确定全序关系已经尘埃落定,这将在[全序广播](#全序广播)一节中详细说明。 +如何确定全序关系已经尘埃落定,这将在 [全序广播](#全序广播) 一节中详细说明。 ### 全序广播 -如果你的程序只运行在单个CPU核上,那么定义一个操作全序是很容易的:可以简单地就是CPU执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,就不能容忍任何错误,因为你必须要从每个节点都获取到最新的序列号)。 +如果你的程序只运行在单个 CPU 核上,那么定义一个操作全序是很容易的:可以简单地就是 CPU 执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,就不能容忍任何错误,因为你必须要从每个节点都获取到最新的序列号)。 -如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)** 或**原子广播(atomic broadcast)**[^ix]【25,57,58】。 +如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个 CPU 核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为 **全序广播(total order broadcast)** 或 **原子广播(atomic broadcast)**[^ix]【25,57,58】。 -[^ix]: “原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序组播(total order multicast)是另一个同义词。 +[^ix]: “原子广播” 是一个传统的术语,非常混乱,而且与 “原子” 一词的其他用法不一致:它与 ACID 事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序组播(total order multicast)是另一个同义词。 > #### 顺序保证的范围 > @@ -475,107 +475,107 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 #### 使用全序广播 -像ZooKeeper和etcd这样的共识服务实际上实现了全序广播。这一事实暗示了全序广播与共识之间有着紧密联系,我们将在本章稍后进行探讨。 +像 ZooKeeper 和 etcd 这样的共识服务实际上实现了全序广播。这一事实暗示了全序广播与共识之间有着紧密联系,我们将在本章稍后进行探讨。 -全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为**状态机复制(state machine replication)**【60】,我们将在[第十一章](ch11.md)中重新回到这个概念。 +全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为 **状态机复制(state machine replication)**【60】,我们将在 [第十一章](ch11.md) 中重新回到这个概念。 -与之类似,可以使用全序广播来实现可串行化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。 +与之类似,可以使用全序广播来实现可串行化的事务:如 “[真的串行执行](ch7.md#真的串行执行)” 中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。 全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳排序更强。 考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志、事务日志或预写式日志中):传递消息就像追加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。 -全序广播对于实现提供防护令牌的锁服务也很有用(请参阅“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。 +全序广播对于实现提供防护令牌的锁服务也很有用(请参阅 “[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在 ZooKeeper 中,这个序列号被称为 `zxid` 【15】。 #### 使用全序广播实现线性一致的存储 -如 [图9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系[^x]。 +如 [图 9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系 [^x]。 -[^x]: 从形式上讲,线性一致读写寄存器是一个“更容易”的问题。 全序广播等价于共识【67】,而共识问题在异步的崩溃-停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器**可以**在这种模型中实现【23,24,25】。 然而,支持诸如**比较并设置(CAS, compare-and-set)**,或**自增并返回(increment-and-get)** 的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。 +[^x]: 从形式上讲,线性一致读写寄存器是一个 “更容易” 的问题。 全序广播等价于共识【67】,而共识问题在异步的崩溃 - 停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器 **可以** 在这种模型中实现【23,24,25】。 然而,支持诸如 **比较并设置(CAS, compare-and-set)**,或 **自增并返回(increment-and-get)** 的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。 -全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息**何时**被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。 +全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息 **何时** 被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。 但如果有了全序广播,你就可以在此基础上构建线性一致的存储。例如,你可以确保用户名能唯一标识用户帐户。 -设想对于每一个可能的用户名,你都可以有一个带有CAS原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示未使用该用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行CAS操作,在先前寄存器值为空的条件,将其值设置为用户的账号ID。如果多个用户试图同时获取相同的用户名,则只有一个CAS操作会成功,因为其他用户会看到非空的值(由于线性一致性)。 +设想对于每一个可能的用户名,你都可以有一个带有 CAS 原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示未使用该用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行 CAS 操作,在先前寄存器值为空的条件,将其值设置为用户的账号 ID。如果多个用户试图同时获取相同的用户名,则只有一个 CAS 操作会成功,因为其他用户会看到非空的值(由于线性一致性)。 -你可以通过将全序广播当成仅追加日志【62,63】的方式来实现这种线性一致的CAS操作: +你可以通过将全序广播当成仅追加日志【62,63】的方式来实现这种线性一致的 CAS 操作: 1. 在日志中追加一条消息,试探性地指明你要声明的用户名。 2. 读日志,并等待你刚才追加的消息被读回。[^xi] 4. 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就是你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。 -[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核x86处理器内存的一致性模型【43】。 该模型既不是线性一致的也不是顺序一致的。 +[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核 x86 处理器内存的一致性模型【43】。 该模型既不是线性一致的也不是顺序一致的。 由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可串行化的多对象事务【62】。 -尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。 (精确地说,这里描述的过程提供了**顺序一致性(sequential consistency)**【47,64】,有时也称为**时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项: +尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。 (精确地说,这里描述的过程提供了 **顺序一致性(sequential consistency)**【47,64】,有时也称为 **时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项: -* 你可以通过在日志中追加一条消息,然后读取日志,直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点(etcd的法定人数读取有些类似这种情况【16】)。 -* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。 (这是Zookeeper `sync()` 操作背后的思想【15】)。 -* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的(这种技术用于链式复制(chain replication)【63】;请参阅“[关于复制的研究](ch5.md#关于复制的研究)”)。 +* 你可以通过在日志中追加一条消息,然后读取日志,直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点(etcd 的法定人数读取有些类似这种情况【16】)。 +* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。 (这是 Zookeeper `sync()` 操作背后的思想【15】)。 +* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的(这种技术用于链式复制(chain replication)【63】;请参阅 “[关于复制的研究](ch5.md#关于复制的研究)”)。 #### 使用线性一致性存储实现全序广播 -上一节介绍了如何从全序广播构建一个线性一致的CAS操作。我们也可以把它反过来,假设我们有线性一致的存储,接下来会展示如何在此基础上构建全序广播。 +上一节介绍了如何从全序广播构建一个线性一致的 CAS 操作。我们也可以把它反过来,假设我们有线性一致的存储,接下来会展示如何在此基础上构建全序广播。 -最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子**自增并返回**操作【28】。或者原子CAS操作也可以完成这项工作。 +最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子 **自增并返回** 操作【28】。或者原子 CAS 操作也可以完成这项工作。 -该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行**自增并返回**操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号依序传递(deliver)消息。 +该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行 **自增并返回** 操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号依序传递(deliver)消息。 请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同 —— 事实上,这是全序广播和时间戳排序间的关键区别。 -实现一个带有原子性**自增并返回**操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行过足够深入的思考,你不可避免地会得出一个共识算法。 +实现一个带有原子性 **自增并返回** 操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行过足够深入的思考,你不可避免地会得出一个共识算法。 -这并非巧合:可以证明,线性一致的CAS(或自增并返回)寄存器与全序广播都都等价于**共识**问题【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察! +这并非巧合:可以证明,线性一致的 CAS(或自增并返回)寄存器与全序广播都都等价于 **共识** 问题【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察! 现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。 ## 分布式事务与共识 -**共识**是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是**让几个节点达成一致(get serveral nodes to agree on something)**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。 +**共识** 是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是 **让几个节点达成一致(get serveral nodes to agree on something)**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。 尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第五章](ch5.md)),事务([第七章](ch7.md)),系统模型([第八章](ch8.md)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。 节点能达成一致,在很多场景下都非常重要,例如: - + * 领导选举 - 在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。 + 在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。 * 原子提交 - 在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)** 问题[^xii]。 + 在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就 ACID 而言,请参阅 “[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止 / 回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为 **原子提交(atomic commit)** 问题 [^xii]。 - [^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 请参阅“[三阶段提交](#三阶段提交)”。 + [^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在 **所有** 参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就 **任意一个** 被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞** 原子提交则要比共识更为困难 —— 请参阅 “[三阶段提交](#三阶段提交)”。 > ### 共识的不可能性 > -> 你可能已经听说过以作者Fischer,Lynch和Paterson命名的FLP结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在**总是**能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事? +> 你可能已经听说过以作者 Fischer,Lynch 和 Paterson 命名的 FLP 结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在 **总是** 能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事? > -> 答案是FLP结果是在**异步系统模型**中被证明的(请参阅“[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。 +> 答案是 FLP 结果是在 **异步系统模型** 中被证明的(请参阅 “[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用 **超时** 或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。 > -> 因此,虽然FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。 +> 因此,虽然 FLP 是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。 -在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交(2PC, two-phase commit)** 算法,这是解决原子提交问题最常见的办法,并在各种数据库、消息队列和应用服务器中被实现。事实证明2PC是一种共识算法,但不是一个非常好的共识算法【70,71】。 +在本节中,我们将首先更详细地研究 **原子提交** 问题。具体来说,我们将讨论 **两阶段提交(2PC, two-phase commit)** 算法,这是解决原子提交问题最常见的办法,并在各种数据库、消息队列和应用服务器中被实现。事实证明 2PC 是一种共识算法,但不是一个非常好的共识算法【70,71】。 -通过对2PC的学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 +通过对 2PC 的学习,我们将继续努力实现更好的一致性算法,比如 ZooKeeper(Zab)和 etcd(Raft)中使用的算法。 ### 原子提交与两阶段提交 -在[第七章](ch7.md)中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。 +在 [第七章](ch7.md) 中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。 -原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。 +原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构 —— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。 #### 从单节点到分布式原子提交 -对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,请参阅“[让B树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。 +对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。 -因此,在单个节点上,事务的提交主要取决于数据持久化落盘的**顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘的控制器,且挂载在单台机器上)使得提交具有原子性。 +因此,在单个节点上,事务的提交主要取决于数据持久化落盘的 **顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘的控制器,且挂载在单台机器上)使得提交具有原子性。 -但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的次级索引(其中索引条目可能位于与主数据不同的节点上;请参阅“[分区与次级索引](ch6.md#分区与次级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(请参阅“[实践中的分布式事务](#实践中的分布式事务)”)。 +但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的次级索引(其中索引条目可能位于与主数据不同的节点上;请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)。大多数 “NoSQL” 分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(请参阅 “[实践中的分布式事务](#实践中的分布式事务)”)。 在这些情况下,仅向所有节点发送提交请求并独立提交每个节点的事务是不够的。这样很容易发生违反原子性的情况:提交在某些节点上成功,而在其他节点上失败: @@ -583,123 +583,123 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 * 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。 * 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。 -如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如 [图7-3](img/fig7-3.png) 所示)。而且一旦在某个节点上提交了一个事务,如果事后发现它在其它节点上被中止了,它是无法撤回的。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 +如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如 [图 7-3](img/fig7-3.png) 所示)。而且一旦在某个节点上提交了一个事务,如果事后发现它在其它节点上被中止了,它是无法撤回的。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 -事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了**读已提交**隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了**已提交却又被追溯声明不存在数据**的事务也必须回滚。 +事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了 **读已提交** 隔离等级的基础,在 “[读已提交](ch7.md#读已提交)” 一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了 **已提交却又被追溯声明不存在数据** 的事务也必须回滚。 (提交事务的结果有可能通过事后执行另一个补偿事务(compensating transaction)来取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于跨事务正确性的保证都是应用自己的问题。) #### 两阶段提交简介 -**两阶段提交(two-phase commit)** 是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,也以**XA事务**的形式对应用可用【76,77】(例如Java Transaction API支持)或以SOAP Web服务的`WS-AtomicTransaction` 形式提供给应用【78,79】。 +**两阶段提交(two-phase commit)** 是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC 在某些数据库内部使用,也以 **XA 事务** 的形式对应用可用【76,77】(例如 Java Transaction API 支持)或以 SOAP Web 服务的 `WS-AtomicTransaction` 形式提供给应用【78,79】。 -[图9-9](img/fig9-9.png)说明了2PC的基本流程。2PC中的提交/中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。 +[图 9-9](img/fig9-9.png) 说明了 2PC 的基本流程。2PC 中的提交 / 中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。 ![](img/fig9-9.png) -**图9-9 两阶段提交(2PC)的成功执行** +**图 9-9 两阶段提交(2PC)的成功执行** > #### 不要把2PC和2PL搞混了 > -> 两阶段提交(2PC)和两阶段锁定(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”)是两个完全不同的东西。 2PC在分布式数据库中提供原子提交,而2PL提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。 +> 两阶段提交(2PC)和两阶段锁定(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”)是两个完全不同的东西。 2PC 在分布式数据库中提供原子提交,而 2PL 提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。 -2PC使用一个通常不会出现在单节点事务中的新组件:**协调者**(coordinator,也称为**事务管理器**,即transaction manager)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana、JOTM、BTM或MSDTC。 +2PC 使用一个通常不会出现在单节点事务中的新组件:**协调者**(coordinator,也称为 **事务管理器**,即 transaction manager)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在 Java EE 容器中),但也可以是单独的进程或服务。这种协调者的例子包括 Narayana、JOTM、BTM 或 MSDTC。 -正常情况下,2PC事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者(participants)**。当应用准备提交时,协调者开始阶段 1 :它发送一个**准备(prepare)** 请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应: +正常情况下,2PC 事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为 **参与者(participants)**。当应用准备提交时,协调者开始阶段 1 :它发送一个 **准备(prepare)** 请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应: -* 如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出**提交(commit)** 请求,然后提交真正发生。 -* 如果任意一个参与者回复了“否”,则协调者在阶段2 中向所有节点发送**中止(abort)** 请求。 +* 如果所有参与者都回答 “是”,表示它们已经准备好提交,那么协调者在阶段 2 发出 **提交(commit)** 请求,然后提交真正发生。 +* 如果任意一个参与者回复了 “否”,则协调者在阶段 2 中向所有节点发送 **中止(abort)** 请求。 -这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚,通常是从两方都收到“我愿意”的答复。收到两者的回复后,司仪宣布这对情侣成为夫妻:事务就提交了,这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复“我愿意”,婚礼就会中止【73】。 +这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚,通常是从两方都收到 “我愿意” 的答复。收到两者的回复后,司仪宣布这对情侣成为夫妻:事务就提交了,这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复 “我愿意”,婚礼就会中止【73】。 #### 系统承诺 -这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。 2PC又有什么不同呢? +这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。 2PC 又有什么不同呢? 为了理解它的工作原理,我们必须更详细地分解这个过程: -1. 当应用想要启动一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。 -2. 应用在每个参与者上启动单节点事务,并在单节点事务上捎带上这个全局事务ID。所有的读写都是在这些单节点事务中各自完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 -3. 当应用准备提交时,协调者向所有参与者发送一个**准备**请求,并打上全局事务ID的标记。如果任意一个请求失败或超时,则协调者向所有参与者发送针对该事务ID的中止请求。 -4. 参与者收到准备请求时,需要确保在任意情况下都的确可以提交事务。这包括将所有事务数据写入磁盘(出现故障,电源故障,或硬盘空间不足都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答“是”,节点承诺,只要请求,这个事务一定可以不出差错地提交。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 -5. 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务日志中,如果它随后就崩溃,恢复后也能知道自己所做的决定。这被称为**提交点(commit point)**。 -6. 一旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执行。如果参与者在此期间崩溃,事务将在其恢复后提交——由于参与者投了赞成,因此恢复后它不能拒绝提交。 +1. 当应用想要启动一个分布式事务时,它向协调者请求一个事务 ID。此事务 ID 是全局唯一的。 +2. 应用在每个参与者上启动单节点事务,并在单节点事务上捎带上这个全局事务 ID。所有的读写都是在这些单节点事务中各自完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 +3. 当应用准备提交时,协调者向所有参与者发送一个 **准备** 请求,并打上全局事务 ID 的标记。如果任意一个请求失败或超时,则协调者向所有参与者发送针对该事务 ID 的中止请求。 +4. 参与者收到准备请求时,需要确保在任意情况下都的确可以提交事务。这包括将所有事务数据写入磁盘(出现故障,电源故障,或硬盘空间不足都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答 “是”,节点承诺,只要请求,这个事务一定可以不出差错地提交。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 +5. 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务日志中,如果它随后就崩溃,恢复后也能知道自己所做的决定。这被称为 **提交点(commit point)**。 +6. 一旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执行。如果参与者在此期间崩溃,事务将在其恢复后提交 —— 由于参与者投了赞成,因此恢复后它不能拒绝提交。 -因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃);以及一旦协调者做出决定,这一决定是不可撤销的。这些承诺保证了2PC的原子性(单节点原子提交将这两个事件合为了一体:将提交记录写入事务日志)。 +因此,该协议包含两个关键的 “不归路” 点:当参与者投票 “是” 时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃);以及一旦协调者做出决定,这一决定是不可撤销的。这些承诺保证了 2PC 的原子性(单节点原子提交将这两个事件合为了一体:将提交记录写入事务日志)。 -回到婚姻的比喻,在说“我愿意”之前,你和你的新娘/新郎有中止这个事务的自由,只要回复 “没门!”就行(或者有类似效果的话)。然而在说了“我愿意”之后,你就不能撤回那个声明了。如果你说“我愿意”后晕倒了,没有听到司仪说“你们现在是夫妻了”,那也并不会改变事务已经提交的现实。当你稍后恢复意识时,可以通过查询司仪的全局事务ID状态来确定你是否已经成婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。 +回到婚姻的比喻,在说 “我愿意” 之前,你和你的新娘 / 新郎有中止这个事务的自由,只要回复 “没门!” 就行(或者有类似效果的话)。然而在说了 “我愿意” 之后,你就不能撤回那个声明了。如果你说 “我愿意” 后晕倒了,没有听到司仪说 “你们现在是夫妻了”,那也并不会改变事务已经提交的现实。当你稍后恢复意识时,可以通过查询司仪的全局事务 ID 状态来确定你是否已经成婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。 #### 协调者失效 -我们已经讨论了在2PC期间,如果参与者之一或网络发生故障时会发生什么情况:如果任何一个**准备**请求失败或者超时,协调者就会中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是如果协调者崩溃,会发生什么情况就不太清楚了。 +我们已经讨论了在 2PC 期间,如果参与者之一或网络发生故障时会发生什么情况:如果任何一个 **准备** 请求失败或者超时,协调者就会中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是如果协调者崩溃,会发生什么情况就不太清楚了。 -如果协调者在发送**准备**请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为**存疑(in doubt)** 的或**不确定(uncertain)** 的。 +如果协调者在发送 **准备** 请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了 “是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为 **存疑(in doubt)** 的或 **不确定(uncertain)** 的。 -情况如[图9-10](img/fig9-10.png) 所示。在这个特定的例子中,协调者实际上决定提交,数据库2 收到提交请求。但是,协调者在将提交请求发送到数据库1 之前发生崩溃,因此数据库1 不知道是否提交或中止。即使**超时**在这里也没有帮助:如果数据库1 在超时后单方面中止,它将最终与执行提交的数据库2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。 +情况如 [图 9-10](img/fig9-10.png) 所示。在这个特定的例子中,协调者实际上决定提交,数据库 2 收到提交请求。但是,协调者在将提交请求发送到数据库 1 之前发生崩溃,因此数据库 1 不知道是否提交或中止。即使 **超时** 在这里也没有帮助:如果数据库 1 在超时后单方面中止,它将最终与执行提交的数据库 2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。 ![](img/fig9-10.png) -**图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止** +**图 9-10 参与者投赞成票后,协调者崩溃。数据库 1 不知道是否提交或中止** -没有协调者的消息,参与者无法知道是提交还是放弃。原则上参与者可以相互沟通,找出每个参与者是如何投票的,并达成一致,但这不是2PC协议的一部分。 +没有协调者的消息,参与者无法知道是提交还是放弃。原则上参与者可以相互沟通,找出每个参与者是如何投票的,并达成一致,但这不是 2PC 协议的一部分。 -可以完成2PC的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前,将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC的**提交点**归结为协调者上的常规单节点原子提交。 +可以完成 2PC 的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前,将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC 的 **提交点** 归结为协调者上的常规单节点原子提交。 #### 三阶段提交 -两阶段提交被称为**阻塞(blocking)**- 原子提交协议,因为存在2PC可能卡住并等待协调者恢复的情况。理论上,可以使一个原子提交协议变为**非阻塞(nonblocking)** 的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。 +两阶段提交被称为 **阻塞(blocking)**- 原子提交协议,因为存在 2PC 可能卡住并等待协调者恢复的情况。理论上,可以使一个原子提交协议变为 **非阻塞(nonblocking)** 的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。 -作为2PC的替代方案,已经提出了一种称为**三阶段提交(3PC)** 的算法【13,80】。然而,3PC假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第八章](ch8.md)),它并不能保证原子性。 +作为 2PC 的替代方案,已经提出了一种称为 **三阶段提交(3PC)** 的算法【13,80】。然而,3PC 假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见 [第八章](ch8.md)),它并不能保证原子性。 -通常,非阻塞原子提交需要一个**完美的故障检测器(perfect failure detector)**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中,超时并不是一种可靠的故障检测机制,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。 +通常,非阻塞原子提交需要一个 **完美的故障检测器(perfect failure detector)**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中,超时并不是一种可靠的故障检测机制,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC 仍然被使用,尽管大家都清楚可能存在协调者故障的问题。 ### 实践中的分布式事务 分布式事务的名声毁誉参半,尤其是那些通过两阶段提交实现的。一方面,它被视作提供了一个难以实现的重要的安全性保证;另一方面,它们因为导致运维问题,造成性能下降,做出超过能力范围的承诺而饱受批评【81,82,83,84】。许多云服务由于其导致的运维问题,而选择不实现分布式事务【85,86】。 -分布式事务的某些实现会带来严重的性能损失 —— 例如据报告称,MySQL中的分布式事务比单节点事务慢10倍以上【87】,所以当人们建议不要使用它们时就不足为奇了。两阶段提交所固有的性能成本,大部分是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 +分布式事务的某些实现会带来严重的性能损失 —— 例如据报告称,MySQL 中的分布式事务比单节点事务慢 10 倍以上【87】,所以当人们建议不要使用它们时就不足为奇了。两阶段提交所固有的性能成本,大部分是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 -但我们不应该直接忽视分布式事务,而应当更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: +但我们不应该直接忽视分布式事务,而应当更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明 “**分布式事务**” 的含义。两种截然不同的分布式事务类型经常被混淆: * 数据库内部的分布式事务 - 一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。 + 一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持数据库节点之间的内部事务。例如,VoltDB 和 MySQL Cluster 的 NDB 存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。 * 异构分布式事务 - 在**异构(heterogeneous)** 事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 + 在 **异构(heterogeneous)** 事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 数据库内部事务不必与任何其他系统兼容,因此它们可以使用任何协议,并能针对特定技术进行特定的优化。因此数据库内部的分布式事务通常工作地很好。另一方面,跨异构技术的事务则更有挑战性。 #### 恰好一次的消息处理 -异构的分布式事务处理能够以强大的方式集成不同的系统。例如:消息队列中的一条消息可以被确认为已处理,当且仅当用于处理消息的数据库事务成功提交。这是通过在同一个事务中原子提交**消息确认**和**数据库写入**两个操作来实现的。藉由分布式事务的支持,即使消息代理和数据库是在不同机器上运行的两种不相关的技术,这种操作也是可能的。 +异构的分布式事务处理能够以强大的方式集成不同的系统。例如:消息队列中的一条消息可以被确认为已处理,当且仅当用于处理消息的数据库事务成功提交。这是通过在同一个事务中原子提交 **消息确认** 和 **数据库写入** 两个操作来实现的。藉由分布式事务的支持,即使消息代理和数据库是在不同机器上运行的两种不相关的技术,这种操作也是可能的。 -如果消息传递或数据库事务任意一者失败,两者都会中止,因此消息代理可能会在稍后安全地重传消息。因此,通过原子提交**消息处理及其副作用**,即使在成功之前需要几次重试,也可以确保消息被**有效地(effectively)** 恰好处理一次。中止会抛弃部分完成事务所导致的任何副作用。 +如果消息传递或数据库事务任意一者失败,两者都会中止,因此消息代理可能会在稍后安全地重传消息。因此,通过原子提交 **消息处理及其副作用**,即使在成功之前需要几次重试,也可以确保消息被 **有效地(effectively)** 恰好处理一次。中止会抛弃部分完成事务所导致的任何副作用。 -然而,只有当所有受事务影响的系统都使用同样的**原子提交协议(atomic commit protocol)** 时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。 +然而,只有当所有受事务影响的系统都使用同样的 **原子提交协议(atomic commit protocol)** 时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。 -在[第十一章](ch11.md)中将再次回到“恰好一次”消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。 +在 [第十一章](ch11.md) 中将再次回到 “恰好一次” 消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。 #### XA事务 -*X/Open XA*(**扩展架构(eXtended Architecture)** 的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实现:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 都支持XA。 +*X/Open XA*(**扩展架构(eXtended Architecture)** 的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于 1991 年推出并得到了广泛的实现:许多传统关系数据库(包括 PostgreSQL、MySQL、DB2、SQL Server 和 Oracle)和消息代理(包括 ActiveMQ、HornetQ、MSMQ 和 IBM MQ) 都支持 XA。 -XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定;例如在Java EE应用的世界中,XA事务是使用**Java事务API(JTA, Java Transaction API)** 实现的,而许多使用**Java数据库连接(JDBC, Java Database Connectivity)** 的数据库驱动,以及许多使用**Java消息服务(JMS)** API的消息代理都支持**Java事务API(JTA)**。 +XA 不是一个网络协议 —— 它只是一个用来与事务协调者连接的 C API。其他语言也有这种 API 的绑定;例如在 Java EE 应用的世界中,XA 事务是使用 **Java 事务 API(JTA, Java Transaction API)** 实现的,而许多使用 **Java 数据库连接(JDBC, Java Database Connectivity)** 的数据库驱动,以及许多使用 **Java 消息服务(JMS)** API 的消息代理都支持 **Java 事务 API(JTA)**。 -XA假定你的应用使用网络驱动或客户端库来与**参与者**(数据库或消息服务)进行通信。如果驱动支持XA,则意味着它会调用XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备、提交或中止。 +XA 假定你的应用使用网络驱动或客户端库来与 **参与者**(数据库或消息服务)进行通信。如果驱动支持 XA,则意味着它会调用 XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备、提交或中止。 -事务协调者需要实现XA API。标准没有指明应该如何实现,但实际上协调者通常只是一个库,被加载到发起事务的应用的同一个进程中(而不是单独的服务)。它在事务中跟踪所有的参与者,并在要求它们**准备**之后收集参与者的响应(通过驱动回调),并使用本地磁盘上的日志记录每次事务的决定(提交/中止)。 +事务协调者需要实现 XA API。标准没有指明应该如何实现,但实际上协调者通常只是一个库,被加载到发起事务的应用的同一个进程中(而不是单独的服务)。它在事务中跟踪所有的参与者,并在要求它们 **准备** 之后收集参与者的响应(通过驱动回调),并使用本地磁盘上的日志记录每次事务的决定(提交 / 中止)。 -如果应用进程崩溃,或者运行应用的机器报销了,协调者也随之往生极乐。然后任何带有**准备了**但未提交事务的参与者都会在疑虑中卡死。由于协调程序的日志位于应用服务器的本地磁盘上,因此必须重启该服务器,且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过客户端库。 +如果应用进程崩溃,或者运行应用的机器报销了,协调者也随之往生极乐。然后任何带有 **准备了** 但未提交事务的参与者都会在疑虑中卡死。由于协调程序的日志位于应用服务器的本地磁盘上,因此必须重启该服务器,且协调程序库必须读取日志以恢复每个事务的提交 / 中止结果。只有这样,协调者才能使用数据库驱动的 XA 回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过客户端库。 #### 怀疑时持有锁 为什么我们这么关心存疑事务?系统的其他部分就不能继续正常工作,无视那些终将被清理的存疑事务吗? -问题在于**锁(locking)**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常获取待修改的行上的**行级排他锁**,以防止脏写。此外,如果要使用可串行化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”)。 +问题在于 **锁(locking)**。正如在 “[读已提交](ch7.md#读已提交)” 中所讨论的那样,数据库事务通常获取待修改的行上的 **行级排他锁**,以防止脏写。此外,如果要使用可串行化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”)。 -在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要20分钟才能重启,那么这些锁将会被持有20分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。 +在事务提交或中止之前,数据库不能释放这些锁(如 [图 9-9](img/fig9-9.png) 中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要 20 分钟才能重启,那么这些锁将会被持有 20 分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。 当这些锁被持有时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能因为读取这些行而被阻塞。因此,其他事务没法儿简单地继续它们的业务了 —— 如果它们要访问同样的数据,就会被阻塞。这可能会导致应用大面积进入不可用状态,直到存疑事务被解决。 @@ -707,33 +707,33 @@ XA假定你的应用使用网络驱动或客户端库来与**参与者**(数 理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何存疑事务。然而在实践中,**孤立(orphaned)** 的存疑事务确实会出现【89,90】,即无论出于何种理由,协调者无法确定事务的结果(例如事务日志已经由于软件错误丢失或损坏)。这些事务无法自动解决,所以它们永远待在数据库中,持有锁并阻塞其他事务。 -即使重启数据库服务器也无法解决这个问题,因为在2PC的正确实现中,即使重启也必须保留存疑事务的锁(否则就会冒违反原子性保证的风险)。这是一种棘手的情况。 +即使重启数据库服务器也无法解决这个问题,因为在 2PC 的正确实现中,即使重启也必须保留存疑事务的锁(否则就会冒违反原子性保证的风险)。这是一种棘手的情况。 唯一的出路是让管理员手动决定提交还是回滚事务。管理员必须检查每个存疑事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(不然为什么协调者处于这种糟糕的状态),并很可能要在巨大精神压力和时间压力下完成。 -许多XA的实现都有一个叫做**启发式决策(heuristic decisions)** 的紧急逃生舱口:允许参与者单方面决定放弃或提交一个存疑事务,而无需协调者做出最终决定【76,77,91】。要清楚的是,这里**启发式**是**可能破坏原子性(probably breaking atomicity)** 的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。 +许多 XA 的实现都有一个叫做 **启发式决策(heuristic decisions)** 的紧急逃生舱口:允许参与者单方面决定放弃或提交一个存疑事务,而无需协调者做出最终决定【76,77,91】。要清楚的是,这里 **启发式** 是 **可能破坏原子性(probably breaking atomicity)** 的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。 #### 分布式事务的限制 -XA事务解决了保持多个参与者(数据系统)相互一致的现实的和重要的问题,但正如我们所看到的那样,它也引入了严重的运维问题。特别来讲,这里的核心认识是:事务协调者本身就是一种数据库(存储了事务的结果),因此需要像其他重要数据库一样小心地打交道: +XA 事务解决了保持多个参与者(数据系统)相互一致的现实的和重要的问题,但正如我们所看到的那样,它也引入了严重的运维问题。特别来讲,这里的核心认识是:事务协调者本身就是一种数据库(存储了事务的结果),因此需要像其他重要数据库一样小心地打交道: * 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。 -* 许多服务器端应用都是使用无状态模式开发的(受HTTP的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。 -* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与SSI(请参阅[可串行化快照隔离](ch7.md#可串行化快照隔离 ))协同工作,因为这需要一个跨系统定位冲突的协议。 -* 对于数据库内部的分布式事务(不是XA),限制没有这么大 —— 例如,分布式版本的SSI是可能的。然而仍然存在问题:2PC成功提交一个事务需要所有参与者的响应。因此,如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效(amplifying failures)** 的趋势,这又与我们构建容错系统的目标背道而驰。 +* 许多服务器端应用都是使用无状态模式开发的(受 HTTP 的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分 —— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。 +* 由于 XA 需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与 SSI(请参阅 [可串行化快照隔离](ch7.md#可串行化快照隔离))协同工作,因为这需要一个跨系统定位冲突的协议。 +* 对于数据库内部的分布式事务(不是 XA),限制没有这么大 —— 例如,分布式版本的 SSI 是可能的。然而仍然存在问题:2PC 成功提交一个事务需要所有参与者的响应。因此,如果系统的 **任何** 部分损坏,事务也会失败。因此,分布式事务又有 **扩大失效(amplifying failures)** 的趋势,这又与我们构建容错系统的目标背道而驰。 -这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于**共识**的话题。 +这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在 [第十一章](ch11.md) 和 [第十二章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于 **共识** 的话题。 ### 容错共识 -非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时(concurrently)** 尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容(mutually incompatible)** 的操作中,哪一个才是赢家。 +非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人 **同时(concurrently)** 尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些 **互不相容(mutually incompatible)** 的操作中,哪一个才是赢家。 -共识问题通常形式化如下:一个或多个节点可以**提议(propose)** 某些值,而共识算法**决定(decides)** 采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**将要服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。 +共识问题通常形式化如下:一个或多个节点可以 **提议(propose)** 某些值,而共识算法 **决定(decides)** 采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以 **提议** 将要服务的顾客的 ID,而 **决定** 指明了哪个顾客获得了座位。 在这种形式下,共识算法必须满足以下性质【25】:[^xiii] -[^xiii]: 这种共识的特殊形式被称为**统一共识(uniform consensus)**,相当于在具有不可靠故障检测器的异步系统中的**常规共识(regular consensus)**【71】。学术文献通常指的是**进程(process)** 而不是节点,但我们在这里使用**节点(node)** 来与本书的其余部分保持一致。 +[^xiii]: 这种共识的特殊形式被称为 **统一共识(uniform consensus)**,相当于在具有不可靠故障检测器的异步系统中的 **常规共识(regular consensus)**【71】。学术文献通常指的是 **进程(process)** 而不是节点,但我们在这里使用 **节点(node)** 来与本书的其余部分保持一致。 * 一致同意(Uniform agreement) @@ -751,44 +751,44 @@ XA事务解决了保持多个参与者(数据系统)相互一致的现实的 由所有未崩溃的节点来最终决定值。 -**一致同意**和**完整性**属性定义了共识的核心思想:所有人都决定了相同的结果,一旦决定了,你就不能改变主意。**有效性**属性主要是为了排除平凡的解决方案:例如,无论提议了什么值,你都可以有一个始终决定值为`null`的算法。;该算法满足**一致同意**和**完整性**属性,但不满足**有效性**属性。 +**一致同意** 和 **完整性** 属性定义了共识的核心思想:所有人都决定了相同的结果,一旦决定了,你就不能改变主意。**有效性** 属性主要是为了排除平凡的解决方案:例如,无论提议了什么值,你都可以有一个始终决定值为 `null` 的算法。;该算法满足 **一致同意** 和 **完整性** 属性,但不满足 **有效性** 属性。 -如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。 +如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为 “独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。 -**终止**属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定(**终止**是一种**活性属性**,而另外三种是**安全属性** —— 请参阅“[安全性和活性](ch8.md#安全性和活性)”)。 +**终止** 属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定(**终止** 是一种 **活性属性**,而另外三种是 **安全属性** —— 请参阅 “[安全性和活性](ch8.md#安全性和活性)”)。 -共识的系统模型假设,当一个节点“崩溃”时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足**终止**属性。特别是,2PC不符合终止属性的要求。 +共识的系统模型假设,当一个节点 “崩溃” 时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在 30 英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足 **终止** 属性。特别是,2PC 不符合终止属性的要求。 -当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数(majority)** 的节点正确工作,以确保终止属性【67】。多数可以安全地组成法定人数(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 +当然如果 **所有** 的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体 **多数(majority)** 的节点正确工作,以确保终止属性【67】。多数可以安全地组成法定人数(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”)。 -因此**终止**属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足—— 一致同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 +因此 **终止** 属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足 —— 一致同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 -大多数共识算法假设不存在**拜占庭式错误**,正如在“[拜占庭故障](ch8.md#拜占庭故障)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。 +大多数共识算法假设不存在 **拜占庭式错误**,正如在 “[拜占庭故障](ch8.md#拜占庭故障)” 一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。 #### 共识算法和全序广播 -最著名的容错共识算法是**视图戳复制(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】) +最著名的容错共识算法是 **视图戳复制(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】) -大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,并满足一致同意、完整性、有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(请参阅“[全序广播](#全序广播)”)。 +大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,并满足一致同意、完整性、有效性和终止属性)。取而代之的是,它们决定了值的 **顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(请参阅 “[全序广播](#全序广播)”)。 请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息【67】。 所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应): -* 由于**一致同意**属性,所有节点决定以相同的顺序传递相同的消息。 -* 由于**完整性**属性,消息不会重复。 -* 由于**有效性**属性,消息不会被损坏,也不能凭空编造。 -* 由于**终止**属性,消息不会丢失。 +* 由于 **一致同意** 属性,所有节点决定以相同的顺序传递相同的消息。 +* 由于 **完整性** 属性,消息不会重复。 +* 由于 **有效性** 属性,消息不会被损坏,也不能凭空编造。 +* 由于 **终止** 属性,消息不会丢失。 -视图戳复制,Raft和Zab直接实现了全序广播,因为这样做比重复**一次一值(one value a time)** 的共识更高效。在Paxos的情况下,这种优化被称为Multi-Paxos。 +视图戳复制,Raft 和 Zab 直接实现了全序广播,因为这样做比重复 **一次一值(one value a time)** 的共识更高效。在 Paxos 的情况下,这种优化被称为 Multi-Paxos。 #### 单领导者复制与共识 -在[第五章](ch5.md)中,我们讨论了单领导者复制(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢? +在 [第五章](ch5.md) 中,我们讨论了单领导者复制(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在 [第五章](ch5.md) 里一点都没担心过共识问题呢? -答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种**独裁类型**的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的**终止**属性,因为它需要人为干预才能取得**进展**。 +答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种 **独裁类型** 的 “共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的 **终止** 属性,因为它需要人为干预才能取得 **进展**。 -一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。 +一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。 但是还有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,而单主复制需要一个领导者,那么... @@ -796,87 +796,87 @@ XA事务解决了保持多个参与者(数据系统)相互一致的现实的 #### 纪元编号和法定人数 -迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**纪元编号**(epoch number,在Paxos中被称为**投票编号**,即ballot number,在视图戳复制中被称为**视图编号**,即view number,以及在Raft中被为**任期号码**,即term number),并确保在每个时代中,领导者都是唯一的。 +迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个 **纪元编号**(epoch number,在 Paxos 中被称为 **投票编号**,即 ballot number,在视图戳复制中被称为 **视图编号**,即 view number,以及在 Raft 中被为 **任期号码**,即 term number),并确保在每个时代中,领导者都是唯一的。 每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的纪元编号,因此纪元编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高纪元编号的领导说了算。 -在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真相由多数所定义](ch8.md#真相由多数所定义)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。 +在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在 “[真相由多数所定义](ch8.md#真相由多数所定义)” 中提到的:一个节点不一定能相信自己的判断 —— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。 -相反,它必须从**法定人数(quorum)** 的节点中获取选票(请参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。 +相反,它必须从 **法定人数(quorum)** 的节点中获取选票(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下,一个节点才会投票赞成提议。 -因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。 +因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的 **法定人群** 必须相互 **重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。 -这一投票过程表面上看起来很像两阶段提交。最大的区别在于,2PC中协调者不是由选举产生的,而且2PC则要求**所有**参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。 +这一投票过程表面上看起来很像两阶段提交。最大的区别在于,2PC 中协调者不是由选举产生的,而且 2PC 则要求 **所有** 参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。 #### 共识的局限性 -共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(请参阅“[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”)。 +共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(请参阅 “[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”)。 尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。 -节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步复制与异步复制](ch5.md#同步复制与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。 +节点在做出决定之前对提议进行投票的过程是一种同步复制。如 “[同步复制与异步复制](ch5.md#同步复制与异步复制)” 中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。 -共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(请参阅“[线性一致性的代价](#线性一致性的代价)”)。 +共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(请参阅 “[线性一致性的代价](#线性一致性的代价)”)。 -大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展(dynamic membership extension)** 允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。 +大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的 **动态成员扩展(dynamic membership extension)** 允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。 共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。 -有时共识算法对网络问题特别敏感。例如Raft已被证明存在让人不悦的极端情况【106】:如果整个网络工作正常,但只有一条特定的网络连接一直不可靠,Raft可能会进入领导者在两个节点间频繁切换的局面,或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。 +有时共识算法对网络问题特别敏感。例如 Raft 已被证明存在让人不悦的极端情况【106】:如果整个网络工作正常,但只有一条特定的网络连接一直不可靠,Raft 可能会进入领导者在两个节点间频繁切换的局面,或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。 ### 成员与协调服务 -像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调与配置服务”。这种服务的API看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果它们基本上算是数据库的话,为什么它们要把工夫全花在实现一个共识算法上呢?是什么使它们区别于其他任意类型的数据库? +像 ZooKeeper 或 etcd 这样的项目通常被描述为 “分布式键值存储” 或 “协调与配置服务”。这种服务的 API 看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果它们基本上算是数据库的话,为什么它们要把工夫全花在实现一个共识算法上呢?是什么使它们区别于其他任意类型的数据库? -为了理解这一点,简单了解如何使用ZooKeeper这类服务是很有帮助的。作为应用开发人员,你很少需要直接使用ZooKeeper,因为它实际上不适合当成通用数据库来用。更有可能的是,你会通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从它那里得到了什么? +为了理解这一点,简单了解如何使用 ZooKeeper 这类服务是很有帮助的。作为应用开发人员,你很少需要直接使用 ZooKeeper,因为它实际上不适合当成通用数据库来用。更有可能的是,你会通过其他项目间接依赖它,例如 HBase、Hadoop YARN、OpenStack Nova 和 Kafka 都依赖 ZooKeeper 在后台运行。这些项目从它那里得到了什么? -ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然会写入磁盘以保证持久性),所以你不会想着把所有应用数据放到这里。这些少量数据会通过容错的全序广播算法复制到所有节点上。正如前面所讨论的那样,数据库复制需要的就是全序广播:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以使副本之间保持一致。 +ZooKeeper 和 etcd 被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然会写入磁盘以保证持久性),所以你不会想着把所有应用数据放到这里。这些少量数据会通过容错的全序广播算法复制到所有节点上。正如前面所讨论的那样,数据库复制需要的就是全序广播:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以使副本之间保持一致。 -ZooKeeper模仿了Google的Chubby锁服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: +ZooKeeper 模仿了 Google 的 Chubby 锁服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: * 线性一致性的原子操作 - 使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。 + 使用原子 CAS 操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以 **租约(lease)** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅 “[进程暂停](ch8.md#进程暂停)”)。 * 操作的全序排序 - 如“[领导者和锁](ch8.md#领导者和锁)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。 + 如 “[领导者和锁](ch8.md#领导者和锁)” 中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。ZooKeeper 通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务 ID(`zxid`)和版本号(`cversion`)【15】。 * 失效检测 - 客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点**,即ephemeral nodes),会话持有的任何锁都可以配置为自动释放。 + 客户端在 ZooKeeper 服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者 ZooKeeper 节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper 会宣告该会话已死亡。当会话超时时(ZooKeeper 称这些节点为 **临时节点**,即 ephemeral nodes),会话持有的任何锁都可以配置为自动释放。 * 变更通知 - 客户端不仅可以读取其他客户端创建的锁和值,还可以监听它们的变更。因此,客户端可以知道另一个客户端何时加入集群(基于新客户端写入ZooKeeper的值),或发生故障(因其会话超时,而其临时节点消失)。通过订阅通知,客户端不用再通过频繁轮询的方式来找出变更。 + 客户端不仅可以读取其他客户端创建的锁和值,还可以监听它们的变更。因此,客户端可以知道另一个客户端何时加入集群(基于新客户端写入 ZooKeeper 的值),或发生故障(因其会话超时,而其临时节点消失)。通过订阅通知,客户端不用再通过频繁轮询的方式来找出变更。 -在这些功能中,只有线性一致的原子操作才真的需要共识。但正是这些功能的组合,使得像ZooKeeper这样的系统在分布式协调中非常有用。 +在这些功能中,只有线性一致的原子操作才真的需要共识。但正是这些功能的组合,使得像 ZooKeeper 这样的系统在分布式协调中非常有用。 #### 将工作分配给节点 -ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。 +ZooKeeper/Chubby 模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。 -另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅“[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。 +另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式 Actor 系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅 “[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。 -这类任务可以通过在ZooKeeper中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在ZooKeeper客户端API基础之上提供更高层工具的库,例如Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多,这样的尝试鲜有成功记录【107】。 +这类任务可以通过在 ZooKeeper 中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在 ZooKeeper 客户端 API 基础之上提供更高层工具的库,例如 Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多,这样的尝试鲜有成功记录【107】。 -应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 +应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反,ZooKeeper 在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper 提供了一种将协调节点(共识,操作排序和故障检测)的一些工作 “外包” 到外部服务的方式。 -通常,由ZooKeeper管理的数据类型的变化十分缓慢:代表“分区 7 中的节点运行在 `10.1.1.23` 上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用的运行时状态的,后者每秒可能会改变数千甚至数百万次。如果应用状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 +通常,由 ZooKeeper 管理的数据类型的变化十分缓慢:代表 “分区 7 中的节点运行在 `10.1.1.23` 上” 的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用的运行时状态的,后者每秒可能会改变数千甚至数百万次。如果应用状态需要从一个节点复制到另一个节点,则可以使用其他工具(如 Apache BookKeeper 【108】)。 #### 服务发现 -ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机来来往往很常见,你通常不会事先知道服务的IP地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。 +ZooKeeper、etcd 和 Consul 也经常用于服务发现 —— 也就是找出你需要连接到哪个 IP 地址才能到达特定的服务。在云数据中心环境中,虚拟机来来往往很常见,你通常不会事先知道服务的 IP 地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。 -但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS的可用性和对网络中断的鲁棒性更重要。 +但是,服务发现是否需要达成共识还不太清楚。 DNS 是查找服务名称的 IP 地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从 DNS 读取是绝对不线性一致性的,如果 DNS 查询的结果有点陈旧,通常不会有问题【109】。 DNS 的可用性和对网络中断的鲁棒性更重要。 尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果你的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。 #### 成员资格服务 -ZooKeeper和它的小伙伴们可以看作是成员资格服务(membership services)研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。 +ZooKeeper 和它的小伙伴们可以看作是成员资格服务(membership services)研究的悠久历史的一部分,这个历史可以追溯到 20 世纪 80 年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。 -成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在[第八章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。 +成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在 [第八章](ch8.md) 中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。 即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,知道哪些节点构成了当前的成员关系是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有的成员都有谁有不同意见,则这种方法将不起作用。 @@ -885,53 +885,53 @@ ZooKeeper和它的小伙伴们可以看作是成员资格服务(membership ser 在本章中,我们从几个不同的角度审视了关于一致性与共识的话题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使多副本数据看起来好像只有一个副本一样,并使其上所有操作都原子性地生效。虽然线性一致性因为简单易懂而很吸引人 —— 它使数据库表现的好像单线程程序中的一个变量一样,但它有着速度缓慢的缺点,特别是在网络延迟很大的环境中。 -我们还探讨了因果性,因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是**并发**的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。 +我们还探讨了因果性,因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是 **并发** 的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。 -但即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”一节的例子中,我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册。如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向**共识**。 +但即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:在 “[光有时间戳排序还不够](#光有时间戳排序还不够)” 一节的例子中,我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册。如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向 **共识**。 我们看到,达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。通过深入挖掘,结果我们发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价(从这个意义上来讲,如果你有其中之一的解决方案,就可以轻易将它转换为其他问题的解决方案)。这些等价的问题包括: -* 线性一致性的CAS寄存器 +* 线性一致性的 CAS 寄存器 - 寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。 + 寄存器需要基于当前值是否等于操作给出的参数,原子地 **决定** 是否设置新值。 * 原子事务提交 - 数据库必须**决定**是否提交或中止分布式事务。 + 数据库必须 **决定** 是否提交或中止分布式事务。 * 全序广播 - 消息系统必须**决定**传递消息的顺序。 + 消息系统必须 **决定** 传递消息的顺序。 * 锁和租约 - 当几个客户端争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。 + 当几个客户端争抢锁或租约时,由锁来 **决定** 哪个客户端成功获得锁。 -* 成员/协调服务 +* 成员 / 协调服务 - 给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需要被宣告死亡。 + 给定某种故障检测器(例如超时),系统必须 **决定** 哪些节点活着,哪些节点因为会话超时需要被宣告死亡。 * 唯一性约束 - 当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为违反约束而失败。 + 当多个事务同时尝试使用相同的键创建冲突记录时,约束必须 **决定** 哪一个被允许,哪些因为违反约束而失败。 如果你只有一个节点,或者你愿意将决策的权能分配给单个节点,所有这些事都很简单。这就是在单领导者数据库中发生的事情:所有决策权归属于领导者,这就是为什么这样的数据库能够提供线性一致的操作,唯一性约束,完全有序的复制日志,以及更多。 但如果该领导者失效,或者如果网络中断导致领导者不可达,这样的系统就无法取得任何进展。应对这种情况可以有三种方法: -1. 等待领导者恢复,接受系统将在这段时间阻塞的事实。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全达成共识,因为它不能满足**终止**属性的要求:如果领导者续命失败,系统可能会永久阻塞。 -2. 人工故障切换,让人类选择一个新的领导者节点,并重新配置系统使之生效,许多关系型数据库都采用这种方方式。这是一种来自“天意”的共识 —— 由计算机系统之外的运维人员做出决定。故障切换的速度受到人类行动速度的限制,通常要比计算机慢(得多)。 +1. 等待领导者恢复,接受系统将在这段时间阻塞的事实。许多 XA/JTA 事务协调者选择这个选项。这种方法并不能完全达成共识,因为它不能满足 **终止** 属性的要求:如果领导者续命失败,系统可能会永久阻塞。 +2. 人工故障切换,让人类选择一个新的领导者节点,并重新配置系统使之生效,许多关系型数据库都采用这种方方式。这是一种来自 “天意” 的共识 —— 由计算机系统之外的运维人员做出决定。故障切换的速度受到人类行动速度的限制,通常要比计算机慢(得多)。 3. 使用算法自动选择一个新的领导者。这种方法需要一种共识算法,使用成熟的算法来正确处理恶劣的网络条件是明智之举【107】。 -尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。 +尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是 “缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。 -像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受[第八章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。 +像 ZooKeeper 这样的工具为应用提供了 “外包” 的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受 [第八章](ch8.md) 中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似 ZooKeeper 的东西是明智之举。 -尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。 +尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(请参阅 “[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。 本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索。 -这里已经到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第五章](ch5.md))、分区([第六章](ch6.md))、事务([第七章](ch7.md))、分布式系统的故障模型([第八章](ch8.md))以及最后的一致性与共识([第九章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在[第三部分](part-iii.md)再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。 +这里已经到了本书 [第二部分](part-ii.md) 的末尾,第二部介绍了复制([第五章](ch5.md))、分区([第六章](ch6.md))、事务([第七章](ch7.md))、分布式系统的故障模型([第八章](ch8.md))以及最后的一致性与共识([第九章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在 [第三部分](part-iii.md) 再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。 ## 参考文献 diff --git a/zh-tw/ch10.md b/zh-tw/ch10.md index 1516b66..de826d7 100644 --- a/zh-tw/ch10.md +++ b/zh-tw/ch10.md @@ -10,11 +10,11 @@ [TOC] -在本書的前兩部分中,我們討論了很多關於**請求**和**查詢**以及相應的**響應**或**結果**。許多現有資料系統中都採用這種資料處理方式:你傳送請求指令,一段時間後(我們期望)系統會給出一個結果。資料庫、快取、搜尋索引、Web伺服器以及其他一些系統都以這種方式工作。 +在本書的前兩部分中,我們討論了很多關於 **請求** 和 **查詢** 以及相應的 **響應** 或 **結果**。許多現有資料系統中都採用這種資料處理方式:你傳送請求指令,一段時間後(我們期望)系統會給出一個結果。資料庫、快取、搜尋索引、Web 伺服器以及其他一些系統都以這種方式工作。 -像這樣的**線上(online)** 系統,無論是瀏覽器請求頁面還是呼叫遠端API的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱“[描述效能](ch1.md#描述效能)”)。 +像這樣的 **線上(online)** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](ch1.md#描述效能)”)。 -Web和越來越多的基於HTTP/REST的API使互動的請求/響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統: +Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統: * 服務(線上系統) @@ -22,24 +22,24 @@ Web和越來越多的基於HTTP/REST的API使互動的請求/響應風格變得 * 批處理系統(離線系統) - 一個批處理系統有大量的輸入資料,跑一個**作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。 + 一個批處理系統有大量的輸入資料,跑一個 **作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。 * 流處理系統(準實時系統) - 流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)** 或**準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。 + 流處理介於線上和離線(批處理)之間,所以有時候被稱為 **準實時(near-real-time)** 或 **準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在 [第十一章](ch11.md) 討論它。 -正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004年釋出的批處理演算法Map-Reduce(可能被過分熱情地)被稱為“造就Google大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括Hadoop,CouchDB和MongoDB。 +正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004 年釋出的批處理演算法 Map-Reduce(可能被過分熱情地)被稱為 “造就 Google 大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括 Hadoop,CouchDB 和 MongoDB。 -與多年前為資料倉庫開發的並行處理系統【3,4】相比,MapReduce是一個相當低級別的程式設計模型,但它使得在商用硬體上能進行的處理規模邁上一個新的臺階。雖然MapReduce的重要性正在下降【5】,但它仍然值得去理解,因為它描繪了一幅關於批處理為什麼有用,以及如何做到有用的清晰圖景。 +與多年前為資料倉庫開發的並行處理系統【3,4】相比,MapReduce 是一個相當低級別的程式設計模型,但它使得在商用硬體上能進行的處理規模邁上一個新的臺階。雖然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因為它描繪了一幅關於批處理為什麼有用,以及如何做到有用的清晰圖景。 -實際上,批處理是一種非常古老的計算方式。早在可程式設計數字計算機誕生之前,打孔卡製表機(例如1890年美國人口普查【6】中使用的霍爾里斯機)實現了半機械化的批處理形式,從大量輸入中彙總計算。 Map-Reduce與1940年代和1950年代廣泛用於商業資料處理的機電IBM卡片分類機器有著驚人的相似之處【7】。正如我們所說,歷史總是在不斷重複自己。 +實際上,批處理是一種非常古老的計算方式。早在可程式設計數字計算機誕生之前,打孔卡製表機(例如 1890 年美國人口普查【6】中使用的霍爾里斯機)實現了半機械化的批處理形式,從大量輸入中彙總計算。 Map-Reduce 與 1940 年代和 1950 年代廣泛用於商業資料處理的機電 IBM 卡片分類機器有著驚人的相似之處【7】。正如我們所說,歷史總是在不斷重複自己。 -在本章中,我們將瞭解MapReduce和其他一些批處理演算法和框架,並探索它們在現代資料系統中的作用。但首先我們將看看使用標準Unix工具的資料處理。即使你已經熟悉了它們,Unix的哲學也值得一讀,Unix的思想和經驗教訓可以遷移到大規模、異構的分散式資料系統中。 +在本章中,我們將瞭解 MapReduce 和其他一些批處理演算法和框架,並探索它們在現代資料系統中的作用。但首先我們將看看使用標準 Unix 工具的資料處理。即使你已經熟悉了它們,Unix 的哲學也值得一讀,Unix 的思想和經驗教訓可以遷移到大規模、異構的分散式資料系統中。 ## 使用Unix工具的批處理 -我們從一個簡單的例子開始。假設你有一臺Web伺服器,每次處理請求時都會在日誌檔案中附加一行。例如,使用nginx預設的訪問日誌格式,日誌的一行可能如下所示: +我們從一個簡單的例子開始。假設你有一臺 Web 伺服器,每次處理請求時都會在日誌檔案中附加一行。例如,使用 nginx 預設的訪問日誌格式,日誌的一行可能如下所示: ```bash 216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" @@ -54,14 +54,14 @@ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" $status $body_bytes_sent "$http_referer" "$http_user_agent" ``` -日誌的這一行表明在2015年2月27日17:55:11 UTC,伺服器從客戶端IP地址`216.58.210.78`接收到對檔案`/css/typography.css`的請求。使用者沒有被認證,所以`$remote_user`被設定為連字元(`-` )。響應狀態是200(即請求成功),響應的大小是3377位元組。網頁瀏覽器是Chrome 40,URL `http://martin.kleppmann.com/` 的頁面中的引用導致該檔案被載入。 +日誌的這一行表明在 2015 年 2 月 27 日 17:55:11 UTC,伺服器從客戶端 IP 地址 `216.58.210.78` 接收到對檔案 `/css/typography.css` 的請求。使用者沒有被認證,所以 `$remote_user` 被設定為連字元(`-` )。響應狀態是 200(即請求成功),響應的大小是 3377 位元組。網頁瀏覽器是 Chrome 40,URL `http://martin.kleppmann.com/` 的頁面中的引用導致該檔案被載入。 ### 簡單日誌分析 -很多工具可以從這些日誌檔案生成關於網站流量的漂亮的報告,但為了練手,讓我們使用基本的Unix功能建立自己的工具。 例如,假設你想在你的網站上找到五個最受歡迎的網頁。 則可以在Unix shell中這樣做:[^i] +很多工具可以從這些日誌檔案生成關於網站流量的漂亮的報告,但為了練手,讓我們使用基本的 Unix 功能建立自己的工具。 例如,假設你想在你的網站上找到五個最受歡迎的網頁。 則可以在 Unix shell 中這樣做:[^i] -[^i]: 有些人認為`cat`這裡並沒有必要,因為輸入檔案可以直接作為awk的引數。 但這種寫法讓線性管道更為顯眼。 +[^i]: 有些人認為 `cat` 這裡並沒有必要,因為輸入檔案可以直接作為 awk 的引數。 但這種寫法讓線性管道更為顯眼。 ```bash cat /var/log/nginx/access.log | #1 @@ -73,10 +73,10 @@ cat /var/log/nginx/access.log | #1 ``` 1. 讀取日誌檔案 -2. 將每一行按空格分割成不同的欄位,每行只輸出第七個欄位,恰好是請求的URL。在我們的例子中是`/css/typography.css`。 -3. 按字母順序排列請求的URL列表。如果某個URL被請求過n次,那麼排序後,檔案將包含連續重複出現n次的該URL。 -4. `uniq`命令透過檢查兩個相鄰的行是否相同來過濾掉輸入中的重複行。 `-c`則表示還要輸出一個計數器:對於每個不同的URL,它會報告輸入中出現該URL的次數。 -5. 第二種排序按每行起始處的數字(`-n`)排序,這是URL的請求次數。然後逆序(`-r`)返回結果,大的數字在前。 +2. 將每一行按空格分割成不同的欄位,每行只輸出第七個欄位,恰好是請求的 URL。在我們的例子中是 `/css/typography.css`。 +3. 按字母順序排列請求的 URL 列表。如果某個 URL 被請求過 n 次,那麼排序後,檔案將包含連續重複出現 n 次的該 URL。 +4. `uniq` 命令透過檢查兩個相鄰的行是否相同來過濾掉輸入中的重複行。 `-c` 則表示還要輸出一個計數器:對於每個不同的 URL,它會報告輸入中出現該 URL 的次數。 +5. 第二種排序按每行起始處的數字(`-n`)排序,這是 URL 的請求次數。然後逆序(`-r`)返回結果,大的數字在前。 6. 最後,只輸出前五行(`-n 5`),並丟棄其餘的。該系列命令的輸出如下所示: ``` @@ -87,13 +87,13 @@ cat /var/log/nginx/access.log | #1 915 /css/typography.css ``` -如果你不熟悉Unix工具,上面的命令列可能看起來有點吃力,但是它非常強大。它能在幾秒鐘內處理幾GB的日誌檔案,並且你可以根據需要輕鬆修改命令。例如,如果要從報告中省略CSS檔案,可以將awk引數更改為`'$7 !~ /\.css$/ {print $7}'`,如果想統計最多的客戶端IP地址,可以把awk引數改為`'{print $1}'`等等。 +如果你不熟悉 Unix 工具,上面的命令列可能看起來有點吃力,但是它非常強大。它能在幾秒鐘內處理幾 GB 的日誌檔案,並且你可以根據需要輕鬆修改命令。例如,如果要從報告中省略 CSS 檔案,可以將 awk 引數更改為 `'$7 !~ /\.css$/ {print $7}'`, 如果想統計最多的客戶端 IP 地址,可以把 awk 引數改為 `'{print $1}'` 等等。 -我們不會在這裡詳細探索Unix工具,但是它非常值得學習。令人驚訝的是,使用awk,sed,grep,sort,uniq和xargs的組合,可以在幾分鐘內完成許多資料分析,並且它們的效能相當的好【8】。 +我們不會在這裡詳細探索 Unix 工具,但是它非常值得學習。令人驚訝的是,使用 awk、sed、grep、sort、uniq 和 xargs 的組合,可以在幾分鐘內完成許多資料分析,並且它們的效能相當的好【8】。 #### 命令鏈與自定義程式 -除了Unix命令鏈,你還可以寫一個簡單的程式來做同樣的事情。例如在Ruby中,它可能看起來像這樣: +除了 Unix 命令鏈,你還可以寫一個簡單的程式來做同樣的事情。例如在 Ruby 中,它可能看起來像這樣: ```ruby counts = Hash.new(0) # 1 @@ -108,480 +108,480 @@ top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 top5.each{|count, url| puts "#{count} #{url}" } # 5 ``` -1. `counts`是一個儲存計數器的雜湊表,儲存了每個URL被瀏覽的次數,預設為0。 -2. 逐行讀取日誌,抽取每行第七個被空格分隔的欄位為URL(這裡的陣列索引是6,因為Ruby的陣列索引從0開始計數) -3. 將日誌當前行中URL對應的計數器值加一。 +1. `counts` 是一個儲存計數器的雜湊表,儲存了每個 URL 被瀏覽的次數,預設為 0。 +2. 逐行讀取日誌,抽取每行第七個被空格分隔的欄位為 URL(這裡的陣列索引是 6,因為 Ruby 的陣列索引從 0 開始計數) +3. 將日誌當前行中 URL 對應的計數器值加一。 4. 按計數器值(降序)對雜湊表內容進行排序,並取前五位。 5. 打印出前五個條目。 -這個程式並不像Unix管道那樣簡潔,但是它的可讀性很強,喜歡哪一種屬於口味的問題。但兩者除了表面上的差異之外,執行流程也有很大差異,如果你在大檔案上執行此分析,則會變得明顯。 +這個程式並不像 Unix 管道那樣簡潔,但是它的可讀性很強,喜歡哪一種屬於口味的問題。但兩者除了表面上的差異之外,執行流程也有很大差異,如果你在大檔案上執行此分析,則會變得明顯。 #### 排序 VS 記憶體中的聚合 -Ruby指令碼在記憶體中儲存了一個URL的雜湊表,將每個URL對映到它出現的次數。 Unix管道沒有這樣的雜湊表,而是依賴於對URL列表的排序,在這個URL列表中,同一個URL的只是簡單地重複出現。 +Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL 對映到它出現的次數。 Unix 管道沒有這樣的雜湊表,而是依賴於對 URL 列表的排序,在這個 URL 列表中,同一個 URL 的只是簡單地重複出現。 -哪種方法更好?這取決於你有多少個不同的URL。對於大多數中小型網站,你可能可以為所有不同網址提供一個計數器(假設我們使用1GB記憶體)。在此例中,作業的**工作集**(working set,即作業需要隨機訪問的記憶體大小)僅取決於不同URL的數量:如果日誌中只有單個URL,重複出現一百萬次,則散列表所需的空間表就只有一個URL加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的膝上型電腦上也可以正常工作。 +哪種方法更好?這取決於你有多少個不同的 URL。對於大多數中小型網站,你可能可以為所有不同網址提供一個計數器(假設我們使用 1GB 記憶體)。在此例中,作業的 **工作集**(working set,即作業需要隨機訪問的記憶體大小)僅取決於不同 URL 的數量:如果日誌中只有單個 URL,重複出現一百萬次,則散列表所需的空間表就只有一個 URL 加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的膝上型電腦上也可以正常工作。 -另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。 歸併排序具有在磁碟上執行良好的順序訪問模式。 (請記住,針對順序I/O進行最佳化是[第三章](ch3.md)中反覆出現的主題,相同的模式在此重現) +另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。 歸併排序具有在磁碟上執行良好的順序訪問模式。 (請記住,針對順序 I/O 進行最佳化是 [第三章](ch3.md) 中反覆出現的主題,相同的模式在此重現) -GNU Coreutils(Linux)中的`sort `程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個CPU核進行並行排序【9】。這意味著我們之前看到的簡單的Unix命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。 +GNU Coreutils(Linux)中的 `sort` 程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個 CPU 核進行並行排序【9】。這意味著我們之前看到的簡單的 Unix 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。 ### Unix哲學 -我們可以非常容易地使用前一個例子中的一系列命令來分析日誌檔案,這並非巧合:事實上,這實際上是Unix的關鍵設計思想之一,而且它直至今天也仍然令人訝異地重要。讓我們更深入地研究一下,以便從Unix中借鑑一些想法【10】。 +我們可以非常容易地使用前一個例子中的一系列命令來分析日誌檔案,這並非巧合:事實上,這實際上是 Unix 的關鍵設計思想之一,而且它直至今天也仍然令人訝異地重要。讓我們更深入地研究一下,以便從 Unix 中借鑑一些想法【10】。 -Unix管道的發明者道格·麥克羅伊(Doug McIlroy)在1964年首先描述了這種情況【11】:“我們需要一種類似園藝膠管的方式來拼接程式 —— 當我們需要將訊息從一個程式傳遞另一個程式時,直接接上去就行。I/O應該也按照這種方式進行“。水管的類比仍然在生效,透過管道連線程式的想法成為了現在被稱為**Unix哲學**的一部分 —— 這一組設計原則在Unix使用者與開發者之間流行起來,該哲學在1978年表述如下【12,13】: +Unix 管道的發明者道格・麥克羅伊(Doug McIlroy)在 1964 年首先描述了這種情況【11】:“我們需要一種類似園藝膠管的方式來拼接程式 —— 當我們需要將訊息從一個程式傳遞另一個程式時,直接接上去就行。I/O 應該也按照這種方式進行 “。水管的類比仍然在生效,透過管道連線程式的想法成為了現在被稱為 **Unix 哲學** 的一部分 —— 這一組設計原則在 Unix 使用者與開發者之間流行起來,該哲學在 1978 年表述如下【12,13】: -1. 讓每個程式都做好一件事。要做一件新的工作,寫一個新程式,而不是透過新增“功能”讓老程式複雜化。 +1. 讓每個程式都做好一件事。要做一件新的工作,寫一個新程式,而不是透過新增 “功能” 讓老程式複雜化。 2. 期待每個程式的輸出成為另一個程式的輸入。不要將無關資訊混入輸出。避免使用嚴格的列資料或二進位制輸入格式。不要堅持互動式輸入。 3. 設計和構建軟體時,即使是作業系統,也讓它們能夠儘早地被試用,最好在幾周內完成。不要猶豫,扔掉笨拙的部分,重建它們。 4. 優先使用工具來減輕程式設計任務,即使必須曲線救國編寫工具,且在用完後很可能要扔掉大部分。 -這種方法 —— 自動化,快速原型設計,增量式迭代,對實驗友好,將大型專案分解成可管理的塊 —— 聽起來非常像今天的敏捷開發和DevOps運動。奇怪的是,四十年來變化不大。 +這種方法 —— 自動化,快速原型設計,增量式迭代,對實驗友好,將大型專案分解成可管理的塊 —— 聽起來非常像今天的敏捷開發和 DevOps 運動。奇怪的是,四十年來變化不大。 -`sort`工具是一個很好的例子。可以說它比大多數程式語言標準庫中的實現(它們不會利用磁碟或使用多執行緒,即使這樣做有很大好處)要更好。然而,單獨使用`sort` 幾乎沒什麼用。它只能與其他Unix工具(如`uniq`)結合使用。 +`sort` 工具是一個很好的例子。可以說它比大多數程式語言標準庫中的實現(它們不會利用磁碟或使用多執行緒,即使這樣做有很大好處)要更好。然而,單獨使用 `sort` 幾乎沒什麼用。它只能與其他 Unix 工具(如 `uniq`)結合使用。 -像 `bash`這樣的Unix shell可以讓我們輕鬆地將這些小程式組合成令人訝異的強大資料處理任務。儘管這些程式中有很多是由不同人群編寫的,但它們可以靈活地結合在一起。 Unix如何實現這種可組合性? +像 `bash` 這樣的 Unix shell 可以讓我們輕鬆地將這些小程式組合成令人訝異的強大資料處理任務。儘管這些程式中有很多是由不同人群編寫的,但它們可以靈活地結合在一起。 Unix 如何實現這種可組合性? #### 統一的介面 -如果你希望一個程式的輸出成為另一個程式的輸入,那意味著這些程式必須使用相同的資料格式 —— 換句話說,一個相容的介面。如果你希望能夠將任何程式的輸出連線到任何程式的輸入,那意味著所有程式必須使用相同的I/O介面。 +如果你希望一個程式的輸出成為另一個程式的輸入,那意味著這些程式必須使用相同的資料格式 —— 換句話說,一個相容的介面。如果你希望能夠將任何程式的輸出連線到任何程式的輸入,那意味著所有程式必須使用相同的 I/O 介面。 -在Unix中,這種介面是一個**檔案**(file,更準確地說,是一個檔案描述符)。一個檔案只是一串有序的位元組序列。因為這是一個非常簡單的介面,所以可以使用相同的介面來表示許多不同的東西:檔案系統上的真實檔案,到另一個程序(Unix套接字,stdin,stdout)的通訊通道,裝置驅動程式(比如`/dev/audio`或`/dev/lp0`),表示TCP連線的套接字等等。很容易將這些設計視為理所當然的,但實際上能讓這些差異巨大的東西共享一個統一的介面是非常厲害的,這使得它們可以很容易地連線在一起[^ii]。 +在 Unix 中,這種介面是一個 **檔案**(file,更準確地說,是一個檔案描述符)。一個檔案只是一串有序的位元組序列。因為這是一個非常簡單的介面,所以可以使用相同的介面來表示許多不同的東西:檔案系統上的真實檔案,到另一個程序(Unix 套接字,stdin,stdout)的通訊通道,裝置驅動程式(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 連線的套接字等等。很容易將這些設計視為理所當然的,但實際上能讓這些差異巨大的東西共享一個統一的介面是非常厲害的,這使得它們可以很容易地連線在一起 [^ii]。 -[^ii]: 統一介面的另一個例子是URL和HTTP,這是Web的基石。 一個URL標識一個網站上的一個特定的東西(資源),你可以連結到任何其他網站的任何網址。 具有網路瀏覽器的使用者因此可以透過跟隨連結在網站之間無縫跳轉,即使伺服器可能由完全不相關的組織維護。 這個原則現在似乎非常明顯,但它卻是網路取能取得今天成就的關鍵。 之前的系統並不是那麼統一:例如,在公告板系統(BBS)時代,每個系統都有自己的電話號碼和波特率配置。 從一個BBS到另一個BBS的引用必須以電話號碼和調變解調器設定的形式;使用者將不得不掛斷,撥打其他BBS,然後手動找到他們正在尋找的資訊。 直接連結到另一個BBS內的一些內容當時是不可能的。 +[^ii]: 統一介面的另一個例子是 URL 和 HTTP,這是 Web 的基石。 一個 URL 標識一個網站上的一個特定的東西(資源),你可以連結到任何其他網站的任何網址。 具有網路瀏覽器的使用者因此可以透過跟隨連結在網站之間無縫跳轉,即使伺服器可能由完全不相關的組織維護。 這個原則現在似乎非常明顯,但它卻是網路取能取得今天成就的關鍵。 之前的系統並不是那麼統一:例如,在公告板系統(BBS)時代,每個系統都有自己的電話號碼和波特率配置。 從一個 BBS 到另一個 BBS 的引用必須以電話號碼和調變解調器設定的形式;使用者將不得不掛斷,撥打其他 BBS,然後手動找到他們正在尋找的資訊。 直接連結到另一個 BBS 內的一些內容當時是不可能的。 -按照慣例,許多(但不是全部)Unix程式將這個位元組序列視為ASCII文字。我們的日誌分析示例使用了這個事實:`awk`,`sort`,`uniq`和`head`都將它們的輸入檔案視為由`\n`(換行符,ASCII `0x0A`)字元分隔的記錄列表。 `\n`的選擇是任意的 —— 可以說,ASCII記錄分隔符`0x1E`本來就是一個更好的選擇,因為它是為了這個目的而設計的【14】,但是無論如何,所有這些程式都使用相同的記錄分隔符允許它們互操作。 +按照慣例,許多(但不是全部)Unix 程式將這個位元組序列視為 ASCII 文字。我們的日誌分析示例使用了這個事實:`awk`、`sort`、`uniq` 和 `head` 都將它們的輸入檔案視為由 `\n`(換行符,ASCII `0x0A`)字元分隔的記錄列表。`\n` 的選擇是任意的 —— 可以說,ASCII 記錄分隔符 `0x1E` 本來就是一個更好的選擇,因為它是為了這個目的而設計的【14】,但是無論如何,所有這些程式都使用相同的記錄分隔符允許它們互操作。 -每條記錄(即一行輸入)的解析則更加模糊。 Unix工具通常透過空白或製表符將行分割成欄位,但也使用CSV(逗號分隔),管道分隔和其他編碼。即使像`xargs`這樣一個相當簡單的工具也有六個命令列選項,用於指定如何解析輸入。 +每條記錄(即一行輸入)的解析則更加模糊。 Unix 工具通常透過空白或製表符將行分割成欄位,但也使用 CSV(逗號分隔),管道分隔和其他編碼。即使像 `xargs` 這樣一個相當簡單的工具也有六個命令列選項,用於指定如何解析輸入。 -ASCII文字的統一介面大多數時候都能工作,但它不是很優雅:我們的日誌分析示例使用`{print $7}`來提取網址,這樣可讀性不是很好。在理想的世界中可能是`{print $request_url}`或類似的東西。我們稍後會回顧這個想法。 +ASCII 文字的統一介面大多數時候都能工作,但它不是很優雅:我們的日誌分析示例使用 `{print $7}` 來提取網址,這樣可讀性不是很好。在理想的世界中可能是 `{print $request_url}` 或類似的東西。我們稍後會回顧這個想法。 -儘管幾十年後還不夠完美,但統一的Unix介面仍然是非常出色的設計。沒有多少軟體能像Unix工具一樣互動組合的這麼好:你不能透過自定義分析工具輕鬆地將電子郵件帳戶的內容和線上購物歷史記錄以管道傳送至電子表格中,並將結果釋出到社交網路或維基。今天,像Unix工具一樣流暢地執行程式是一種例外,而不是規範。 +儘管幾十年後還不夠完美,但統一的 Unix 介面仍然是非常出色的設計。沒有多少軟體能像 Unix 工具一樣互動組合的這麼好:你不能透過自定義分析工具輕鬆地將電子郵件帳戶的內容和線上購物歷史記錄以管道傳送至電子表格中,並將結果釋出到社交網路或維基。今天,像 Unix 工具一樣流暢地執行程式是一種例外,而不是規範。 -即使是具有**相同資料模型**的資料庫,將資料從一種資料庫匯出再匯入到另一種資料庫也並不容易。缺乏整合導致了資料的**巴爾幹化**[^譯註i]。 +即使是具有 **相同資料模型** 的資料庫,將資料從一種資料庫匯出再匯入到另一種資料庫也並不容易。缺乏整合導致了資料的 **巴爾幹化**[^譯註i]。 [^譯註i]: **巴爾幹化(Balkanization)** 是一個常帶有貶義的地緣政治學術語,其定義為:一個國家或政區分裂成多個互相敵對的國家或政區的過程。 #### 邏輯與佈線相分離 -Unix工具的另一個特點是使用標準輸入(`stdin`)和標準輸出(`stdout`)。如果你執行一個程式,而不指定任何其他的東西,標準輸入來自鍵盤,標準輸出指向螢幕。但是,你也可以從檔案輸入和/或將輸出重定向到檔案。管道允許你將一個程序的標準輸出附加到另一個程序的標準輸入(有個小記憶體緩衝區,而不需要將整個中間資料流寫入磁碟)。 +Unix 工具的另一個特點是使用標準輸入(`stdin`)和標準輸出(`stdout`)。如果你執行一個程式,而不指定任何其他的東西,標準輸入來自鍵盤,標準輸出指向螢幕。但是,你也可以從檔案輸入和 / 或將輸出重定向到檔案。管道允許你將一個程序的標準輸出附加到另一個程序的標準輸入(有個小記憶體緩衝區,而不需要將整個中間資料流寫入磁碟)。 -如果需要,程式仍然可以直接讀取和寫入檔案,但Unix方法在程式不關心特定的檔案路徑、只使用標準輸入和標準輸出時效果最好。這允許shell使用者以任何他們想要的方式連線輸入和輸出;該程式不知道或不關心輸入來自哪裡以及輸出到哪裡。 (人們可以說這是一種**松耦合(loose coupling)**,**晚期繫結(late binding)**【15】或**控制反轉(inversion of control)**【16】)。將輸入/輸出佈線與程式邏輯分開,可以將小工具組合成更大的系統。 +如果需要,程式仍然可以直接讀取和寫入檔案,但 Unix 方法在程式不關心特定的檔案路徑、只使用標準輸入和標準輸出時效果最好。這允許 shell 使用者以任何他們想要的方式連線輸入和輸出;該程式不知道或不關心輸入來自哪裡以及輸出到哪裡。 (人們可以說這是一種 **松耦合(loose coupling)**,**晚期繫結(late binding)**【15】或 **控制反轉(inversion of control)**【16】)。將輸入 / 輸出佈線與程式邏輯分開,可以將小工具組合成更大的系統。 -你甚至可以編寫自己的程式,並將它們與作業系統提供的工具組合在一起。你的程式只需要從標準輸入讀取輸入,並將輸出寫入標準輸出,它就可以加入資料處理的管道中。在日誌分析示例中,你可以編寫一個將Usage-Agent字串轉換為更靈敏的瀏覽器識別符號,或者將IP地址轉換為國家程式碼的工具,並將其插入管道。`sort`程式並不關心它是否與作業系統的另一部分或者你寫的程式通訊。 +你甚至可以編寫自己的程式,並將它們與作業系統提供的工具組合在一起。你的程式只需要從標準輸入讀取輸入,並將輸出寫入標準輸出,它就可以加入資料處理的管道中。在日誌分析示例中,你可以編寫一個將 Usage-Agent 字串轉換為更靈敏的瀏覽器識別符號,或者將 IP 地址轉換為國家程式碼的工具,並將其插入管道。`sort` 程式並不關心它是否與作業系統的另一部分或者你寫的程式通訊。 -但是,使用`stdin`和`stdout`能做的事情是有限的。需要多個輸入或輸出的程式雖然可能,卻非常棘手。你沒法將程式的輸出管道連線至網路連線中【17,18】[^iii] 。如果程式直接開啟檔案進行讀取和寫入,或者將另一個程式作為子程序啟動,或者開啟網路連線,那麼I/O的佈線就取決於程式本身了。它仍然可以被配置(例如透過命令列選項),但在Shell中對輸入和輸出進行佈線的靈活性就少了。 +但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多個輸入或輸出的程式雖然可能,卻非常棘手。你沒法將程式的輸出管道連線至網路連線中【17,18】[^iii] 。如果程式直接開啟檔案進行讀取和寫入,或者將另一個程式作為子程序啟動,或者開啟網路連線,那麼 I/O 的佈線就取決於程式本身了。它仍然可以被配置(例如透過命令列選項),但在 Shell 中對輸入和輸出進行佈線的靈活性就少了。 -[^iii]: 除了使用一個單獨的工具,如`netcat`或`curl`。 Unix起初試圖將所有東西都表示為檔案,但是BSD套接字API偏離了這個慣例【17】。研究用作業系統Plan 9和Inferno在使用檔案方面更加一致:它們將TCP連線表示為`/net/tcp`中的檔案【18】。 +[^iii]: 除了使用一個單獨的工具,如 `netcat` 或 `curl`。 Unix 起初試圖將所有東西都表示為檔案,但是 BSD 套接字 API 偏離了這個慣例【17】。研究用作業系統 Plan 9 和 Inferno 在使用檔案方面更加一致:它們將 TCP 連線表示為 `/net/tcp` 中的檔案【18】。 #### 透明度和實驗 -使Unix工具如此成功的部分原因是,它們使檢視正在發生的事情變得非常容易: +使 Unix 工具如此成功的部分原因是,它們使檢視正在發生的事情變得非常容易: -- Unix命令的輸入檔案通常被視為不可變的。這意味著你可以隨意執行命令,嘗試各種命令列選項,而不會損壞輸入檔案。 -- 你可以在任何時候結束管道,將管道輸出到`less`,然後檢視它是否具有預期的形式。這種檢查能力對除錯非常有用。 +- Unix 命令的輸入檔案通常被視為不可變的。這意味著你可以隨意執行命令,嘗試各種命令列選項,而不會損壞輸入檔案。 +- 你可以在任何時候結束管道,將管道輸出到 `less`,然後檢視它是否具有預期的形式。這種檢查能力對除錯非常有用。 - 你可以將一個流水線階段的輸出寫入檔案,並將該檔案用作下一階段的輸入。這使你可以重新啟動後面的階段,而無需重新執行整個管道。 -因此,與關係資料庫的查詢最佳化器相比,即使Unix工具非常簡單,但仍然非常有用,特別是對於實驗而言。 +因此,與關係資料庫的查詢最佳化器相比,即使 Unix 工具非常簡單,但仍然非常有用,特別是對於實驗而言。 -然而,Unix工具的最大侷限在於它們只能在一臺機器上執行 —— 而Hadoop這樣的工具即應運而生。 +然而,Unix 工具的最大侷限在於它們只能在一臺機器上執行 —— 而 Hadoop 這樣的工具即應運而生。 ## MapReduce和分散式檔案系統 -MapReduce有點像Unix工具,但分佈在數千臺機器上。像Unix工具一樣,它相當簡單粗暴,但令人驚異地管用。一個MapReduce作業可以和一個Unix程序相類比:它接受一個或多個輸入,併產生一個或多個輸出。 +MapReduce 有點像 Unix 工具,但分佈在數千臺機器上。像 Unix 工具一樣,它相當簡單粗暴,但令人驚異地管用。一個 MapReduce 作業可以和一個 Unix 程序相類比:它接受一個或多個輸入,併產生一個或多個輸出。 -和大多數Unix工具一樣,執行MapReduce作業通常不會修改輸入,除了生成輸出外沒有任何副作用。輸出檔案以連續的方式一次性寫入(一旦寫入檔案,不會修改任何現有的檔案部分)。 +和大多數 Unix 工具一樣,執行 MapReduce 作業通常不會修改輸入,除了生成輸出外沒有任何副作用。輸出檔案以連續的方式一次性寫入(一旦寫入檔案,不會修改任何現有的檔案部分)。 -雖然Unix工具使用`stdin`和`stdout`作為輸入和輸出,但MapReduce作業在分散式檔案系統上讀寫檔案。在Hadoop的MapReduce實現中,該檔案系統被稱為**HDFS(Hadoop分散式檔案系統)**,一個Google檔案系統(GFS)的開源實現【19】。 +雖然 Unix 工具使用 `stdin` 和 `stdout` 作為輸入和輸出,但 MapReduce 作業在分散式檔案系統上讀寫檔案。在 Hadoop 的 MapReduce 實現中,該檔案系統被稱為 **HDFS(Hadoop 分散式檔案系統)**,一個 Google 檔案系統(GFS)的開源實現【19】。 -除HDFS外,還有各種其他分散式檔案系統,如GlusterFS和Quantcast File System(QFS)【20】。諸如Amazon S3,Azure Blob儲存和OpenStack Swift【21】等物件儲存服務在很多方面都是相似的[^iv]。在本章中,我們將主要使用HDFS作為示例,但是這些原則適用於任何分散式檔案系統。 +除 HDFS 外,還有各種其他分散式檔案系統,如 GlusterFS 和 Quantcast File System(QFS)【20】。諸如 Amazon S3,Azure Blob 儲存和 OpenStack Swift【21】等物件儲存服務在很多方面都是相似的 [^iv]。在本章中,我們將主要使用 HDFS 作為示例,但是這些原則適用於任何分散式檔案系統。 -[^iv]: 一個不同之處在於,對於HDFS,可以將計算任務安排在儲存特定檔案副本的計算機上執行,而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸,從本地磁碟讀取有效能優勢。但是請注意,如果使用糾刪碼(Erasure Coding),則會丟失區域性性,因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。 +[^iv]: 一個不同之處在於,對於 HDFS,可以將計算任務安排在儲存特定檔案副本的計算機上執行,而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸,從本地磁碟讀取有效能優勢。但是請注意,如果使用糾刪碼(Erasure Coding),則會丟失區域性性,因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。 -與網路連線儲存(NAS)和儲存區域網路(SAN)架構的共享磁碟方法相比,HDFS基於**無共享**原則(請參閱[第二部分](part-ii.md)的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。 +與網路連線儲存(NAS)和儲存區域網路(SAN)架構的共享磁碟方法相比,HDFS 基於 **無共享** 原則(請參閱 [第二部分](part-ii.md) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。 -HDFS在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為**NameNode**的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此,HDFS在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。 +HDFS 在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為 **NameNode** 的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此,HDFS 在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。 -為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如[第五章](ch5.md)中所述,或者諸如Reed-Solomon碼這樣的糾刪碼方案,它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與RAID相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。 +為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](ch5.md) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案,它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。 -HDFS的可伸縮性已經很不錯了:在撰寫本書時,最大的HDFS部署執行在上萬臺機器上,總儲存容量達數百PB【23】。如此大的規模已經變得可行,因為使用商品硬體和開源軟體的HDFS上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。 +HDFS 的可伸縮性已經很不錯了:在撰寫本書時,最大的 HDFS 部署執行在上萬臺機器上,總儲存容量達數百 PB【23】。如此大的規模已經變得可行,因為使用商品硬體和開源軟體的 HDFS 上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。 ### MapReduce作業執行 -MapReduce是一個程式設計框架,你可以使用它編寫程式碼來處理HDFS等分散式檔案系統中的大型資料集。理解它的最簡單方法是參考“[簡單日誌分析](#簡單日誌分析)”中的Web伺服器日誌分析示例。MapReduce中的資料處理模式與此示例非常相似: +MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處理 HDFS 等分散式檔案系統中的大型資料集。理解它的最簡單方法是參考 “[簡單日誌分析](#簡單日誌分析)” 中的 Web 伺服器日誌分析示例。MapReduce 中的資料處理模式與此示例非常相似: -1. 讀取一組輸入檔案,並將其分解成**記錄(records)**。在Web伺服器日誌示例中,每條記錄都是日誌中的一行(即`\n`是記錄分隔符)。 -2. 呼叫Mapper函式,從每條輸入記錄中提取一對鍵值。在前面的例子中,Mapper函式是`awk '{print $7}'`:它提取URL(`$7`)作為鍵,並將值留空。 -3. 按鍵排序所有的鍵值對。在日誌的例子中,這由第一個`sort`命令完成。 -4. 呼叫Reducer函式遍歷排序後的鍵值對。如果同一個鍵出現多次,排序使它們在列表中相鄰,所以很容易組合這些值而不必在記憶體中保留很多狀態。在前面的例子中,Reducer是由`uniq -c`命令實現的,該命令使用相同的鍵來統計相鄰記錄的數量。 +1. 讀取一組輸入檔案,並將其分解成 **記錄(records)**。在 Web 伺服器日誌示例中,每條記錄都是日誌中的一行(即 `\n` 是記錄分隔符)。 +2. 呼叫 Mapper 函式,從每條輸入記錄中提取一對鍵值。在前面的例子中,Mapper 函式是 `awk '{print $7}'`:它提取 URL(`$7`)作為鍵,並將值留空。 +3. 按鍵排序所有的鍵值對。在日誌的例子中,這由第一個 `sort` 命令完成。 +4. 呼叫 Reducer 函式遍歷排序後的鍵值對。如果同一個鍵出現多次,排序使它們在列表中相鄰,所以很容易組合這些值而不必在記憶體中保留很多狀態。在前面的例子中,Reducer 是由 `uniq -c` 命令實現的,該命令使用相同的鍵來統計相鄰記錄的數量。 -這四個步驟可以作為一個MapReduce作業執行。步驟2(Map)和4(Reduce)是你編寫自定義資料處理程式碼的地方。步驟1(將檔案分解成記錄)由輸入格式解析器處理。步驟3中的排序步驟隱含在MapReduce中 —— 你不必編寫它,因為Mapper的輸出始終在送往Reducer之前進行排序。 +這四個步驟可以作為一個 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 - Mapper會在每條輸入記錄上呼叫一次,其工作是從輸入記錄中提取鍵值。對於每個輸入,它可以生成任意數量的鍵值對(包括None)。它不會保留從一個輸入記錄到下一個記錄的任何狀態,因此每個記錄都是獨立處理的。 + Mapper 會在每條輸入記錄上呼叫一次,其工作是從輸入記錄中提取鍵值。對於每個輸入,它可以生成任意數量的鍵值對(包括 None)。它不會保留從一個輸入記錄到下一個記錄的任何狀態,因此每個記錄都是獨立處理的。 * Reducer - MapReduce框架拉取由Mapper生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫Reducer。 Reducer可以產生輸出記錄(例如相同URL的出現次數)。 + MapReduce 框架拉取由 Mapper 生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫 Reducer。 Reducer 可以產生輸出記錄(例如相同 URL 的出現次數)。 -在Web伺服器日誌的例子中,我們在第5步中有第二個`sort`命令,它按請求數對URL進行排序。在MapReduce中,如果你需要第二個排序階段,則可以透過編寫第二個MapReduce作業並將第一個作業的輸出用作第二個作業的輸入來實現它。這樣看來,Mapper的作用是將資料放入一個適合排序的表單中,並且Reducer的作用是處理已排序的資料。 +在 Web 伺服器日誌的例子中,我們在第 5 步中有第二個 `sort` 命令,它按請求數對 URL 進行排序。在 MapReduce 中,如果你需要第二個排序階段,則可以透過編寫第二個 MapReduce 作業並將第一個作業的輸出用作第二個作業的輸入來實現它。這樣看來,Mapper 的作用是將資料放入一個適合排序的表單中,並且 Reducer 的作用是處理已排序的資料。 #### 分散式執行MapReduce -MapReduce與Unix命令管道的主要區別在於,MapReduce可以在多臺機器上並行執行計算,而無需編寫程式碼來顯式處理並行問題。Mapper和Reducer一次只能處理一條記錄;它們不需要知道它們的輸入來自哪裡,或者輸出去往什麼地方,所以框架可以處理在機器之間移動資料的複雜性。 +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作業中的資料流。其並行化基於分割槽(請參閱[第六章](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】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性性。 +每個輸入檔案的大小通常是數百兆位元組。 MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper,只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】:它節省了透過網路複製輸入檔案的開銷,減少網路負載並增加區域性性。 ![](../img/fig10-1.png) -**圖10-1 具有三個Mapper和三個Reducer的MapReduce任務** +**圖 10-1 具有三個 Mapper 和三個 Reducer 的 MapReduce 任務** -在大多數情況下,應該在Mapper任務中執行的應用程式碼在將要執行它的機器上還不存在,所以MapReduce框架首先將程式碼(例如Java程式中的JAR檔案)複製到適當的機器。然後啟動Map任務並開始讀取輸入檔案,一次將一條記錄傳入Mapper回撥函式。Mapper的輸出由鍵值對組成。 +在大多數情況下,應該在 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樹)”中討論的類似。 +鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個 Map 任務都按照 Reducer 對輸出進行分割槽。每個分割槽都被寫入 Mapper 程式的本地磁碟,使用的技術與我們在 “[SSTables 與 LSM 樹](ch3.md#SSTables與LSM樹)” 中討論的類似。 -只要當Mapper讀取完輸入檔案,並寫完排序後的輸出檔案,MapReduce排程器就會通知Reducer可以從該Mapper開始獲取輸出檔案。Reducer連線到每個Mapper,並下載自己相應分割槽的有序鍵值對檔案。按Reducer分割槽,排序,從Mapper向Reducer複製分割槽資料,這一整個過程被稱為**混洗(shuffle)**【26】(一個容易混淆的術語 —— 不像洗牌,在MapReduce中的混洗沒有隨機性)。 +只要當 Mapper 讀取完輸入檔案,並寫完排序後的輸出檔案,MapReduce 排程器就會通知 Reducer 可以從該 Mapper 開始獲取輸出檔案。Reducer 連線到每個 Mapper,並下載自己相應分割槽的有序鍵值對檔案。按 Reducer 分割槽,排序,從 Mapper 向 Reducer 複製分割槽資料,這一整個過程被稱為 **混洗(shuffle)**【26】(一個容易混淆的術語 —— 不像洗牌,在 MapReduce 中的混洗沒有隨機性)。 -Reduce任務從Mapper獲取檔案,並將它們合併在一起,並保留有序特性。因此,如果不同的Mapper生成了鍵相同的記錄,則在Reducer的輸入中,這些記錄將會相鄰。 +Reduce 任務從 Mapper 獲取檔案,並將它們合併在一起,並保留有序特性。因此,如果不同的 Mapper 生成了鍵相同的記錄,則在 Reducer 的輸入中,這些記錄將會相鄰。 -Reducer呼叫時會收到一個鍵,和一個迭代器作為引數,迭代器會順序地掃過所有具有該鍵的記錄(因為在某些情況可能無法完全放入記憶體中)。Reducer可以使用任意邏輯來處理這些記錄,並且可以生成任意數量的輸出記錄。這些輸出記錄會寫入分散式檔案系統上的檔案中(通常是在跑Reducer的機器本地磁碟上留一份,並在其他機器上留幾份副本)。 +Reducer 呼叫時會收到一個鍵,和一個迭代器作為引數,迭代器會順序地掃過所有具有該鍵的記錄(因為在某些情況可能無法完全放入記憶體中)。Reducer 可以使用任意邏輯來處理這些記錄,並且可以生成任意數量的輸出記錄。這些輸出記錄會寫入分散式檔案系統上的檔案中(通常是在跑 Reducer 的機器本地磁碟上留一份,並在其他機器上留幾份副本)。 #### MapReduce工作流 -單個MapReduce作業可以解決的問題範圍很有限。以日誌分析為例,單個MapReduce作業可以確定每個URL的頁面瀏覽次數,但無法確定最常見的URL,因為這需要第二輪排序。 +單個 MapReduce 作業可以解決的問題範圍很有限。以日誌分析為例,單個 MapReduce 作業可以確定每個 URL 的頁面瀏覽次數,但無法確定最常見的 URL,因為這需要第二輪排序。 -因此將MapReduce作業連結成為**工作流(workflow)** 中是極為常見的,例如,一個作業的輸出成為下一個作業的輸入。 Hadoop MapReduce框架對工作流沒有特殊支援,所以這個鏈是透過目錄名隱式實現的:第一個作業必須將其輸出配置為HDFS中的指定目錄,第二個作業必須將其輸入配置為從同一個目錄。從MapReduce框架的角度來看,這是兩個獨立的作業。 +因此將 MapReduce 作業連結成為 **工作流(workflow)** 中是極為常見的,例如,一個作業的輸出成為下一個作業的輸入。Hadoop MapReduce 框架對工作流沒有特殊支援,所以這個鏈是透過目錄名隱式實現的:第一個作業必須將其輸出配置為 HDFS 中的指定目錄,第二個作業必須將其輸入配置為從同一個目錄。從 MapReduce 框架的角度來看,這是兩個獨立的作業。 -因此,被連結的MapReduce作業並沒有那麼像Unix命令管道(它直接將一個程序的輸出作為另一個程序的輸入,僅用一個很小的記憶體緩衝區)。它更像是一系列命令,其中每個命令的輸出寫入臨時檔案,下一個命令從臨時檔案中讀取。這種設計有利也有弊,我們將在“[物化中間狀態](#物化中間狀態)”中討論。 +因此,被連結的 MapReduce 作業並沒有那麼像 Unix 命令管道(它直接將一個程序的輸出作為另一個程序的輸入,僅用一個很小的記憶體緩衝區)。它更像是一系列命令,其中每個命令的輸出寫入臨時檔案,下一個命令從臨時檔案中讀取。這種設計有利也有弊,我們將在 “[物化中間狀態](#物化中間狀態)” 中討論。 -只有當作業成功完成後,批處理作業的輸出才會被視為有效的(MapReduce會丟棄失敗作業的部分輸出)。因此,工作流中的一項作業只有在先前的作業 —— 即生產其輸入的作業 —— 成功完成後才能開始。為了處理這些作業之間的依賴,有很多針對Hadoop的工作流排程器被開發出來,包括Oozie,Azkaban,Luigi,Airflow和Pinball 【28】。 +只有當作業成功完成後,批處理作業的輸出才會被視為有效的(MapReduce 會丟棄失敗作業的部分輸出)。因此,工作流中的一項作業只有在先前的作業 —— 即生產其輸入的作業 —— 成功完成後才能開始。為了處理這些作業之間的依賴,有很多針對 Hadoop 的工作流排程器被開發出來,包括 Oozie、Azkaban、Luigi、Airflow 和 Pinball 【28】。 -這些排程程式還具有管理功能,在維護大量批處理作業時非常有用。在構建推薦系統時,由50到100個MapReduce作業組成的工作流是常見的【29】。而在大型組織中,許多不同的團隊可能執行不同的作業來讀取彼此的輸出。工具支援對於管理這樣複雜的資料流而言非常重要。 +這些排程程式還具有管理功能,在維護大量批處理作業時非常有用。在構建推薦系統時,由 50 到 100 個 MapReduce 作業組成的工作流是常見的【29】。而在大型組織中,許多不同的團隊可能執行不同的作業來讀取彼此的輸出。工具支援對於管理這樣複雜的資料流而言非常重要。 -Hadoop的各種高階工具(如Pig 【30】,Hive 【31】,Cascading 【32】,Crunch 【33】和FlumeJava 【34】)也能自動佈線組裝多個MapReduce階段,生成合適的工作流。 +Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【32】、Crunch 【33】和 FlumeJava 【34】)也能自動佈線組裝多個 MapReduce 階段,生成合適的工作流。 ### Reduce側連線與分組 -我們在[第二章](ch2.md)中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。 +我們在 [第二章](ch2.md) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。 -在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的**外來鍵**,文件模型中的**文件引用**或圖模型中的**邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如[第二章](ch2.md)所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除[^v]。 +在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外來鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](ch2.md) 所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除 [^v]。 -[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如ID)中具有**相同值**相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。 +[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如 ID)中具有 **相同值** 相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。 -在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用**索引**來快速定位感興趣的記錄(請參閱[第三章](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) -**圖10-2 使用者行為日誌與使用者檔案的連線** +**圖 10-2 使用者行為日誌與使用者檔案的連線** -分析任務可能需要將使用者活動與使用者檔案資訊相關聯:例如,如果檔案包含使用者的年齡或出生日期,系統就可以確定哪些頁面更受哪些年齡段的使用者歡迎。然而活動事件僅包含使用者ID,而沒有包含完整的使用者檔案資訊。在每個活動事件中嵌入這些檔案資訊很可能會非常浪費。因此,活動事件需要與使用者檔案資料庫相連線。 +分析任務可能需要將使用者活動與使用者檔案資訊相關聯:例如,如果檔案包含使用者的年齡或出生日期,系統就可以確定哪些頁面更受哪些年齡段的使用者歡迎。然而活動事件僅包含使用者 ID,而沒有包含完整的使用者檔案資訊。在每個活動事件中嵌入這些檔案資訊很可能會非常浪費。因此,活動事件需要與使用者檔案資料庫相連線。 -實現這一連線的最簡單方法是,逐個遍歷活動事件,併為每個遇到的使用者ID查詢使用者資料庫(在遠端伺服器上)。這是可能的,但是它的效能可能會非常差:處理吞吐量將受限於受資料庫伺服器的往返時間,本地快取的有效性很大程度上取決於資料的分佈,並行執行大量查詢可能會輕易壓垮資料庫【35】。 +實現這一連線的最簡單方法是,逐個遍歷活動事件,併為每個遇到的使用者 ID 查詢使用者資料庫(在遠端伺服器上)。這是可能的,但是它的效能可能會非常差:處理吞吐量將受限於受資料庫伺服器的往返時間,本地快取的有效性很大程度上取決於資料的分佈,並行執行大量查詢可能會輕易壓垮資料庫【35】。 -為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為**非確定的(nondeterministic)**,因為遠端資料庫中的資料可能會改變。 +為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為 **非確定的(nondeterministic)**,因為遠端資料庫中的資料可能會改變。 -因此,更好的方法是獲取使用者資料庫的副本(例如,使用ETL程序從資料庫備份中提取資料,請參閱“[資料倉庫](ch3.md#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在HDFS中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用MapReduce將所有相關記錄集中到同一個地方進行高效處理。 +因此,更好的方法是獲取使用者資料庫的副本(例如,使用 ETL 程序從資料庫備份中提取資料,請參閱 “[資料倉庫](ch3.md#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在 HDFS 中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用 MapReduce 將所有相關記錄集中到同一個地方進行高效處理。 #### 排序合併連線 -回想一下,Mapper的目的是從每個輸入記錄中提取一對鍵值。在[圖10-2](../img/fig10-2.png)的情況下,這個鍵就是使用者ID:一組Mapper會掃過活動事件(提取使用者ID作為鍵,活動事件作為值),而另一組Mapper將會掃過使用者資料庫(提取使用者ID作為鍵,使用者的出生日期作為值)。這個過程如[圖10-3](../img/fig10-3.png)所示。 +回想一下,Mapper 的目的是從每個輸入記錄中提取一對鍵值。在 [圖 10-2](../img/fig10-2.png) 的情況下,這個鍵就是使用者 ID:一組 Mapper 會掃過活動事件(提取使用者 ID 作為鍵,活動事件作為值),而另一組 Mapper 將會掃過使用者資料庫(提取使用者 ID 作為鍵,使用者的出生日期作為值)。這個過程如 [圖 10-3](../img/fig10-3.png) 所示。 ![](../img/fig10-3.png) -**圖10-3 在使用者ID上進行的Reduce端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個Mapper並行處理** +**圖 10-3 在使用者 ID 上進行的 Reduce 端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個 Mapper 並行處理** -當MapReduce框架透過鍵對Mapper輸出進行分割槽,然後對鍵值對進行排序時,效果是具有相同ID的所有活動事件和使用者記錄在Reducer輸入中彼此相鄰。 Map-Reduce作業甚至可以也讓這些記錄排序,使Reducer總能先看到來自使用者資料庫的記錄,緊接著是按時間戳順序排序的活動事件 —— 這種技術被稱為**二次排序(secondary sort)**【26】。 +當 MapReduce 框架透過鍵對 Mapper 輸出進行分割槽,然後對鍵值對進行排序時,效果是具有相同 ID 的所有活動事件和使用者記錄在 Reducer 輸入中彼此相鄰。 Map-Reduce 作業甚至可以也讓這些記錄排序,使 Reducer 總能先看到來自使用者資料庫的記錄,緊接著是按時間戳順序排序的活動事件 —— 這種技術被稱為 **二次排序(secondary sort)**【26】。 -然後Reducer可以容易地執行實際的連線邏輯:每個使用者ID都會被呼叫一次Reducer函式,且因為二次排序,第一個值應該是來自使用者資料庫的出生日期記錄。 Reducer將出生日期儲存在區域性變數中,然後使用相同的使用者ID遍歷活動事件,輸出**已觀看網址**和**觀看者年齡**的結果對。隨後的Map-Reduce作業可以計算每個URL的檢視者年齡分佈,並按年齡段進行聚集。 +然後 Reducer 可以容易地執行實際的連線邏輯:每個使用者 ID 都會被呼叫一次 Reducer 函式,且因為二次排序,第一個值應該是來自使用者資料庫的出生日期記錄。 Reducer 將出生日期儲存在區域性變數中,然後使用相同的使用者 ID 遍歷活動事件,輸出 **已觀看網址** 和 **觀看者年齡** 的結果對。隨後的 Map-Reduce 作業可以計算每個 URL 的檢視者年齡分佈,並按年齡段進行聚集。 -由於Reducer一次處理一個特定使用者ID的所有記錄,因此一次只需要將一條使用者記錄儲存在記憶體中,而不需要透過網路發出任何請求。這個演算法被稱為**排序合併連線(sort-merge join)**,因為Mapper的輸出是按鍵排序的,然後Reducer將來自連線兩側的有序記錄列表合併在一起。 +由於 Reducer 一次處理一個特定使用者 ID 的所有記錄,因此一次只需要將一條使用者記錄儲存在記憶體中,而不需要透過網路發出任何請求。這個演算法被稱為 **排序合併連線(sort-merge join)**,因為 Mapper 的輸出是按鍵排序的,然後 Reducer 將來自連線兩側的有序記錄列表合併在一起。 #### 把相關資料放在一起 -在排序合併連線中,Mapper和排序過程確保了所有對特定使用者ID執行連線操作的必須資料都被放在同一個地方:單次呼叫Reducer的地方。預先排好了所有需要的資料,Reducer可以是相當簡單的單執行緒程式碼,能夠以高吞吐量和與低記憶體開銷掃過這些記錄。 +在排序合併連線中,Mapper 和排序過程確保了所有對特定使用者 ID 執行連線操作的必須資料都被放在同一個地方:單次呼叫 Reducer 的地方。預先排好了所有需要的資料,Reducer 可以是相當簡單的單執行緒程式碼,能夠以高吞吐量和與低記憶體開銷掃過這些記錄。 -這種架構可以看做,Mapper將“訊息”傳送給Reducer。當一個Mapper發出一個鍵值對時,這個鍵的作用就像值應該傳遞到的目標地址。即使鍵只是一個任意的字串(不是像IP地址和埠號那樣的實際的網路地址),它表現的就像一個地址:所有具有相同鍵的鍵值對將被傳遞到相同的目標(一次Reducer的呼叫)。 +這種架構可以看做,Mapper 將 “訊息” 傳送給 Reducer。當一個 Mapper 發出一個鍵值對時,這個鍵的作用就像值應該傳遞到的目標地址。即使鍵只是一個任意的字串(不是像 IP 地址和埠號那樣的實際的網路地址),它表現的就像一個地址:所有具有相同鍵的鍵值對將被傳遞到相同的目標(一次 Reducer 的呼叫)。 -使用MapReduce程式設計模型,能將計算的物理網路通訊層面(從正確的機器獲取資料)從應用邏輯中剝離出來(獲取資料後執行處理)。這種分離與資料庫的典型用法形成了鮮明對比,從資料庫中獲取資料的請求經常出現在應用程式碼內部【36】。由於MapReduce處理了所有的網路通訊,因此它也避免了讓應用程式碼去擔心部分故障,例如另一個節點的崩潰:MapReduce在不影響應用邏輯的情況下能透明地重試失敗的任務。 +使用 MapReduce 程式設計模型,能將計算的物理網路通訊層面(從正確的機器獲取資料)從應用邏輯中剝離出來(獲取資料後執行處理)。這種分離與資料庫的典型用法形成了鮮明對比,從資料庫中獲取資料的請求經常出現在應用程式碼內部【36】。由於 MapReduce 處理了所有的網路通訊,因此它也避免了讓應用程式碼去擔心部分故障,例如另一個節點的崩潰:MapReduce 在不影響應用邏輯的情況下能透明地重試失敗的任務。 #### 分組 -除了連線之外,“把相關資料放在一起”的另一種常見模式是,按某個鍵對記錄分組(如SQL中的GROUP BY子句)。所有帶有相同鍵的記錄構成一個組,而下一步往往是在每個組內進行某種聚合操作,例如: +除了連線之外,“把相關資料放在一起” 的另一種常見模式是,按某個鍵對記錄分組(如 SQL 中的 GROUP BY 子句)。所有帶有相同鍵的記錄構成一個組,而下一步往往是在每個組內進行某種聚合操作,例如: -- 統計每個組中記錄的數量(例如在統計PV的例子中,在SQL中表示為`COUNT(*)`聚合) -- 對某個特定欄位求和(SQL中的`SUM(fieldname)`) -- 按某種分級函式取出排名前k條記錄。 +- 統計每個組中記錄的數量(例如在統計 PV 的例子中,在 SQL 中表示為 `COUNT(*)` 聚合) +- 對某個特定欄位求和(SQL 中的 `SUM(fieldname)`) +- 按某種分級函式取出排名前 k 條記錄。 -使用MapReduce實現這種分組操作的最簡單方法是設定Mapper,以便它們生成的鍵值對使用所需的分組鍵。然後分割槽和排序過程將所有具有相同分割槽鍵的記錄導向同一個Reducer。因此在MapReduce之上實現分組和連線看上去非常相似。 +使用 MapReduce 實現這種分組操作的最簡單方法是設定 Mapper,以便它們生成的鍵值對使用所需的分組鍵。然後分割槽和排序過程將所有具有相同分割槽鍵的記錄導向同一個 Reducer。因此在 MapReduce 之上實現分組和連線看上去非常相似。 -分組的另一個常見用途是整理特定使用者會話的所有活動事件,以找出使用者進行的一系列操作(稱為**會話化(sessionization)**【37】)。例如,可以使用這種分析來確定顯示新版網站的使用者是否比那些顯示舊版本的使用者更有購買慾(A/B測試),或者計算某個營銷活動是否值得。 +分組的另一個常見用途是整理特定使用者會話的所有活動事件,以找出使用者進行的一系列操作(稱為 **會話化(sessionization)**【37】)。例如,可以使用這種分析來確定顯示新版網站的使用者是否比那些顯示舊版本的使用者更有購買慾(A/B 測試),或者計算某個營銷活動是否值得。 -如果你有多個Web伺服器處理使用者請求,則特定使用者的活動事件很可能分散在各個不同的伺服器的日誌檔案中。你可以透過使用會話cookie,使用者ID或類似的識別符號作為分組鍵,以將特定使用者的所有活動事件放在一起來實現會話化,與此同時,不同使用者的事件仍然散佈在不同的分割槽中。 +如果你有多個 Web 伺服器處理使用者請求,則特定使用者的活動事件很可能分散在各個不同的伺服器的日誌檔案中。你可以透過使用會話 cookie,使用者 ID 或類似的識別符號作為分組鍵,以將特定使用者的所有活動事件放在一起來實現會話化,與此同時,不同使用者的事件仍然散佈在不同的分割槽中。 #### 處理偏斜 -如果存在與單個鍵關聯的大量資料,則“將具有相同鍵的所有記錄放到相同的位置”這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為**關鍵物件(linchpin object)**【38】或**熱鍵(hot key)**。 +如果存在與單個鍵關聯的大量資料,則 “將具有相同鍵的所有記錄放到相同的位置” 這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為 **關鍵物件(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】。 +如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig 中的 **偏斜連線(skewed join)** 方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper 會將熱鍵的關聯記錄 **隨機**(相對於傳統 MapReduce 基於鍵雜湊的確定性方法)傳送到幾個 Reducer 之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到 **所有** 處理該鍵的 Reducer 上【40】。 -這種技術將處理熱鍵的工作分散到多個Reducer上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個Reducer上。 Crunch中的**分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。 +這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。 Crunch 中的 **分片連線(sharded join)** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。 -Hive的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用Map端連線(請參閱下一節)。 +Hive 的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用 Map 端連線(請參閱下一節)。 -當按照熱鍵進行分組並聚合時,可以將分組分兩個階段進行。第一個MapReduce階段將記錄傳送到隨機Reducer,以便每個Reducer只對熱鍵的子集執行分組,為每個鍵輸出一個更緊湊的中間聚合結果。然後第二個MapReduce作業將所有來自第一階段Reducer的中間聚合結果合併為每個鍵一個值。 +當按照熱鍵進行分組並聚合時,可以將分組分兩個階段進行。第一個 MapReduce 階段將記錄傳送到隨機 Reducer,以便每個 Reducer 只對熱鍵的子集執行分組,為每個鍵輸出一個更緊湊的中間聚合結果。然後第二個 MapReduce 作業將所有來自第一階段 Reducer 的中間聚合結果合併為每個鍵一個值。 ### Map側連線 -上一節描述的連線演算法在Reducer中執行實際的連線邏輯,因此被稱為Reduce側連線。Mapper扮演著預處理輸入資料的角色:從每個輸入記錄中提取鍵值,將鍵值對分配給Reducer分割槽,並按鍵排序。 +上一節描述的連線演算法在 Reducer 中執行實際的連線邏輯,因此被稱為 Reduce 側連線。Mapper 扮演著預處理輸入資料的角色:從每個輸入記錄中提取鍵值,將鍵值對分配給 Reducer 分割槽,並按鍵排序。 -Reduce側方法的優點是不需要對輸入資料做任何假設:無論其屬性和結構如何,Mapper都可以對其預處理以備連線。然而不利的一面是,排序,複製至Reducer,以及合併Reducer輸入,所有這些操作可能開銷巨大。當資料透過MapReduce 階段時,資料可能需要落盤好幾次,取決於可用的記憶體緩衝區【37】。 +Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其屬性和結構如何,Mapper 都可以對其預處理以備連線。然而不利的一面是,排序,複製至 Reducer,以及合併 Reducer 輸入,所有這些操作可能開銷巨大。當資料透過 MapReduce 階段時,資料可能需要落盤好幾次,取決於可用的記憶體緩衝區【37】。 -另一方面,如果你**能**對輸入資料作出某些假設,則透過使用所謂的Map側連線來加快連線速度是可行的。這種方法使用了一個裁減掉Reducer與排序的MapReduce作業,每個Mapper只是簡單地從分散式檔案系統中讀取一個輸入檔案塊,然後將輸出檔案寫入檔案系統,僅此而已。 +另一方面,如果你 **能** 對輸入資料作出某些假設,則透過使用所謂的 Map 側連線來加快連線速度是可行的。這種方法使用了一個裁減掉 Reducer 與排序的 MapReduce 作業,每個 Mapper 只是簡單地從分散式檔案系統中讀取一個輸入檔案塊,然後將輸出檔案寫入檔案系統,僅此而已。 #### 廣播雜湊連線 -適用於執行Map端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個Mapper的記憶體中。 +適用於執行 Map 端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個 Mapper 的記憶體中。 -例如,假設在[圖10-2](../img/fig10-2.png)的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當Mapper啟動時,它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後,Mapper可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者ID[^vi]。 +例如,假設在 [圖 10-2](../img/fig10-2.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時,它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後,Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。 -[^vi]: 這個例子假定散列表中的每個鍵只有一個條目,這對使用者資料庫(使用者ID唯一標識一個使用者)可能是正確的。通常,雜湊表可能需要包含具有相同鍵的多個條目,而連線運算子將對每個鍵輸出所有的匹配。 +[^vi]: 這個例子假定散列表中的每個鍵只有一個條目,這對使用者資料庫(使用者 ID 唯一標識一個使用者)可能是正確的。通常,雜湊表可能需要包含具有相同鍵的多個條目,而連線運算子將對每個鍵輸出所有的匹配。 -參與連線的較大輸入的每個檔案塊各有一個Mapper(在[圖10-2](../img/fig10-2.png)的例子中活動事件是較大的輸入)。每個Mapper都會將較小輸入整個載入到記憶體中。 +參與連線的較大輸入的每個檔案塊各有一個 Mapper(在 [圖 10-2](../img/fig10-2.png) 的例子中活動事件是較大的輸入)。每個 Mapper 都會將較小輸入整個載入到記憶體中。 -這種簡單有效的演算法被稱為**廣播雜湊連線(broadcast hash join)**:**廣播**一詞反映了這樣一個事實,每個連線較大輸入端分割槽的Mapper都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上“廣播”到較大資料的所有分割槽上),**雜湊**一詞反映了它使用一個散列表。 Pig(名為“**複製連結(replicated join)**”),Hive(“**MapJoin**”),Cascading和Crunch支援這種連線。它也被諸如Impala的資料倉庫查詢引擎使用【41】。 +這種簡單有效的演算法被稱為 **廣播雜湊連線(broadcast hash join)**:**廣播** 一詞反映了這樣一個事實,每個連線較大輸入端分割槽的 Mapper 都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上 “廣播” 到較大資料的所有分割槽上),**雜湊** 一詞反映了它使用一個散列表。 Pig(名為 “**複製連結(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支援這種連線。它也被諸如 Impala 的資料倉庫查詢引擎使用【41】。 除了將較小的連線輸入載入到記憶體散列表中,另一種方法是將較小輸入儲存在本地磁碟上的只讀索引中【42】。索引中經常使用的部分將保留在作業系統的頁面快取中,因而這種方法可以提供與記憶體散列表幾乎一樣快的隨機查詢效能,但實際上並不需要資料集能放入記憶體中。 #### 分割槽雜湊連線 -如果Map側連線的輸入以相同的方式進行分割槽,則雜湊連線方法可以獨立應用於每個分割槽。在[圖10-2](../img/fig10-2.png)的情況中,你可以根據使用者ID的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽(因此連線兩側各有10個分割槽)。例如,Mapper3首先將所有具有以3結尾的ID的使用者載入到散列表中,然後掃描ID為3的每個使用者的所有活動事件。 +如果 Map 側連線的輸入以相同的方式進行分割槽,則雜湊連線方法可以獨立應用於每個分割槽。在 [圖 10-2](../img/fig10-2.png) 的情況中,你可以根據使用者 ID 的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽(因此連線兩側各有 10 個分割槽)。例如,Mapper3 首先將所有具有以 3 結尾的 ID 的使用者載入到散列表中,然後掃描 ID 為 3 的每個使用者的所有活動事件。 -如果分割槽正確無誤,可以確定的是,所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個Mapper只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個Mapper都可以在記憶體散列表中少放點資料。 +如果分割槽正確無誤,可以確定的是,所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個 Mapper 只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個 Mapper 都可以在記憶體散列表中少放點資料。 -這種方法只有當連線兩端輸入有相同的分割槽數,且兩側的記錄都是使用相同的鍵與相同的雜湊函式做分割槽時才適用。如果輸入是由之前執行過這種分組的MapReduce作業生成的,那麼這可能是一個合理的假設。 +這種方法只有當連線兩端輸入有相同的分割槽數,且兩側的記錄都是使用相同的鍵與相同的雜湊函式做分割槽時才適用。如果輸入是由之前執行過這種分組的 MapReduce 作業生成的,那麼這可能是一個合理的假設。 -分割槽雜湊連線在Hive中稱為**Map側桶連線(bucketed map joins)【37】**。 +分割槽雜湊連線在 Hive 中稱為 **Map 側桶連線(bucketed map joins)【37】**。 #### Map側合併連線 -如果輸入資料集不僅以相同的方式進行分割槽,而且還基於相同的鍵進行**排序**,則可適用另一種Map側連線的變體。在這種情況下,輸入是否小到能放入記憶體並不重要,因為這時候Mapper同樣可以執行歸併操作(通常由Reducer執行)的歸併操作:按鍵遞增的順序依次讀取兩個輸入檔案,將具有相同鍵的記錄配對。 +如果輸入資料集不僅以相同的方式進行分割槽,而且還基於相同的鍵進行 **排序**,則可適用另一種 Map 側連線的變體。在這種情況下,輸入是否小到能放入記憶體並不重要,因為這時候 Mapper 同樣可以執行歸併操作(通常由 Reducer 執行)的歸併操作:按鍵遞增的順序依次讀取兩個輸入檔案,將具有相同鍵的記錄配對。 -如果能進行Map側合併連線,這通常意味著前一個MapReduce作業可能一開始就已經把輸入資料做了分割槽並進行了排序。原則上這個連線就可以在前一個作業的Reduce階段進行。但使用獨立的僅Map作業有時也是合適的,例如,分好區且排好序的中間資料集可能還會用於其他目的。 +如果能進行 Map 側合併連線,這通常意味著前一個 MapReduce 作業可能一開始就已經把輸入資料做了分割槽並進行了排序。原則上這個連線就可以在前一個作業的 Reduce 階段進行。但使用獨立的僅 Map 作業有時也是合適的,例如,分好區且排好序的中間資料集可能還會用於其他目的。 #### MapReduce工作流與Map側連線 -當下遊作業使用MapReduce連線的輸出時,選擇Map側連線或Reduce側連線會影響輸出的結構。Reduce側連線的輸出是按照**連線鍵**進行分割槽和排序的,而Map端連線的輸出則按照與較大輸入相同的方式進行分割槽和排序(因為無論是使用分割槽連線還是廣播連線,連線較大輸入端的每個檔案塊都會啟動一個Map任務)。 +當下遊作業使用 MapReduce 連線的輸出時,選擇 Map 側連線或 Reduce 側連線會影響輸出的結構。Reduce 側連線的輸出是按照 **連線鍵** 進行分割槽和排序的,而 Map 端連線的輸出則按照與較大輸入相同的方式進行分割槽和排序(因為無論是使用分割槽連線還是廣播連線,連線較大輸入端的每個檔案塊都會啟動一個 Map 任務)。 -如前所述,Map側連線也對輸入資料集的大小,有序性和分割槽方式做出了更多假設。在最佳化連線策略時,瞭解分散式檔案系統中資料集的物理佈局變得非常重要:僅僅知道編碼格式和資料儲存目錄的名稱是不夠的;你還必須知道資料是按哪些鍵做的分割槽和排序,以及分割槽的數量。 +如前所述,Map 側連線也對輸入資料集的大小,有序性和分割槽方式做出了更多假設。在最佳化連線策略時,瞭解分散式檔案系統中資料集的物理佈局變得非常重要:僅僅知道編碼格式和資料儲存目錄的名稱是不夠的;你還必須知道資料是按哪些鍵做的分割槽和排序,以及分割槽的數量。 -在Hadoop生態系統中,這種關於資料集分割槽的元資料通常在HCatalog和Hive Metastore中維護【37】。 +在 Hadoop 生態系統中,這種關於資料集分割槽的元資料通常在 HCatalog 和 Hive Metastore 中維護【37】。 ### 批處理工作流的輸出 -我們已經說了很多用於實現MapReduce工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業? +我們已經說了很多用於實現 MapReduce 工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業? -在資料庫查詢的場景中,我們將事務處理(OLTP)與分析兩種目的區分開來(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。我們看到,OLTP查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前10項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。 +在資料庫查詢的場景中,我們將事務處理(OLTP)與分析兩種目的區分開來(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。我們看到,OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。 -批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而MapReduce作業工作流與用於分析目的的SQL查詢是不同的(請參閱“[Hadoop與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。 +批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而 MapReduce 作業工作流與用於分析目的的 SQL 查詢是不同的(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。 #### 建立搜尋索引 -Google最初使用MapReduce是為其搜尋引擎建立索引,其實現為由5到10個MapReduce作業組成的工作流【1】。雖然Google後來也不僅僅是為這個目的而使用MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解MapReduce。 (直至今日,Hadoop MapReduce仍然是為Lucene/Solr構建索引的好方法【44】) +Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解 MapReduce。 (直至今日,Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】) -我們在“[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”中簡要地瞭解了Lucene這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件ID列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名,糾正拼寫錯誤,解析同義詞等等 —— 但這個原則是成立的。 +我們在 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名,糾正拼寫錯誤,解析同義詞等等 —— 但這個原則是成立的。 -如果需要對一組固定文件執行全文搜尋,則批處理是一種構建索引的高效方法:Mapper根據需要對文件集合進行分割槽,每個Reducer構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)並行處理效果拔群。 +如果需要對一組固定文件執行全文搜尋,則批處理是一種構建索引的高效方法:Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)並行處理效果拔群。 由於按關鍵字查詢搜尋索引是隻讀操作,因而這些索引檔案一旦建立就是不可變的。 如果索引的文件集合發生更改,一種選擇是定期重跑整個索引工作流,並在完成後用新的索引檔案批次替換以前的索引檔案。如果只有少量的文件發生了變化,這種方法的計算成本可能會很高。但它的優點是索引過程很容易理解:文件進,索引出。 -另一個選擇是,可以增量建立索引。如[第三章](ch3.md)中討論的,如果要在索引中新增,刪除或更新文件,Lucene會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在[第十一章](ch11.md)中看到更多這種增量處理。 +另一個選擇是,可以增量建立索引。如 [第三章](ch3.md) 中討論的,如果要在索引中新增,刪除或更新文件,Lucene 會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在 [第十一章](ch11.md) 中看到更多這種增量處理。 #### 鍵值儲存作為批處理輸出 搜尋索引只是批處理工作流可能輸出的一個例子。批處理的另一個常見用途是構建機器學習系統,例如分類器(比如垃圾郵件過濾器,異常檢測,影象識別)與推薦系統(例如,你可能認識的人,你可能感興趣的產品或相關的搜尋【29】)。 -這些批處理作業的輸出通常是某種資料庫:例如,可以透過給定使用者ID查詢該使用者推薦好友的資料庫,或者可以透過產品ID查詢相關產品的資料庫【45】。 +這些批處理作業的輸出通常是某種資料庫:例如,可以透過給定使用者 ID 查詢該使用者推薦好友的資料庫,或者可以透過產品 ID 查詢相關產品的資料庫【45】。 -這些資料庫需要被處理使用者請求的Web應用所查詢,而它們通常是獨立於Hadoop基礎設施的。那麼批處理過程的輸出如何回到Web應用可以查詢的資料庫中呢? +這些資料庫需要被處理使用者請求的 Web 應用所查詢,而它們通常是獨立於 Hadoop 基礎設施的。那麼批處理過程的輸出如何回到 Web 應用可以查詢的資料庫中呢? -最直接的選擇可能是,直接在Mapper或Reducer中使用你最愛的資料庫的客戶端庫,並從批處理作業直接寫入資料庫伺服器,一次寫入一條記錄。它能工作(假設你的防火牆規則允許從你的Hadoop環境直接訪問你的生產資料庫),但這並不是一個好主意,出於以下幾個原因: +最直接的選擇可能是,直接在 Mapper 或 Reducer 中使用你最愛的資料庫的客戶端庫,並從批處理作業直接寫入資料庫伺服器,一次寫入一條記錄。它能工作(假設你的防火牆規則允許從你的 Hadoop 環境直接訪問你的生產資料庫),但這並不是一個好主意,出於以下幾個原因: - 正如前面在連線的上下文中討論的那樣,為每條記錄發起一個網路請求,要比批處理任務的正常吞吐量慢幾個數量級。即使客戶端庫支援批處理,效能也可能很差。 -- MapReduce作業經常並行執行許多工。如果所有Mapper或Reducer都同時寫入相同的輸出資料庫,並以批處理的預期速率工作,那麼該資料庫很可能被輕易壓垮,其查詢效能可能變差。這可能會導致系統其他部分的執行問題【35】。 -- 通常情況下,MapReduce為作業輸出提供了一個乾淨利落的“全有或全無”保證:如果作業成功,則結果就是每個任務恰好執行一次所產生的輸出,即使某些任務失敗且必須一路重試。如果整個作業失敗,則不會生成輸出。然而從作業內部寫入外部系統,會產生外部可見的副作用,這種副作用是不能以這種方式被隱藏的。因此,你不得不去操心對其他系統可見的部分完成的作業結果,並需要理解Hadoop任務嘗試與預測執行的複雜性。 +- MapReduce 作業經常並行執行許多工。如果所有 Mapper 或 Reducer 都同時寫入相同的輸出資料庫,並以批處理的預期速率工作,那麼該資料庫很可能被輕易壓垮,其查詢效能可能變差。這可能會導致系統其他部分的執行問題【35】。 +- 通常情況下,MapReduce 為作業輸出提供了一個乾淨利落的 “全有或全無” 保證:如果作業成功,則結果就是每個任務恰好執行一次所產生的輸出,即使某些任務失敗且必須一路重試。如果整個作業失敗,則不會生成輸出。然而從作業內部寫入外部系統,會產生外部可見的副作用,這種副作用是不能以這種方式被隱藏的。因此,你不得不去操心對其他系統可見的部分完成的作業結果,並需要理解 Hadoop 任務嘗試與預測執行的複雜性。 -更好的解決方案是在批處理作業**內**建立一個全新的資料庫,並將其作為檔案寫入分散式檔案系統中作業的輸出目錄,就像上節中的搜尋索引一樣。這些資料檔案一旦寫入就是不可變的,可以批次載入到處理只讀查詢的伺服器中。不少鍵值儲存都支援在MapReduce作業中構建資料庫檔案,包括Voldemort 【46】,Terrapin 【47】,ElephantDB 【48】和HBase批次載入【49】。 +更好的解決方案是在批處理作業 **內** 建立一個全新的資料庫,並將其作為檔案寫入分散式檔案系統中作業的輸出目錄,就像上節中的搜尋索引一樣。這些資料檔案一旦寫入就是不可變的,可以批次載入到處理只讀查詢的伺服器中。不少鍵值儲存都支援在 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】。 +將資料載入到 Voldemort 時,伺服器將繼續用舊資料檔案服務請求,同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成,伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題,它可以輕易回滾至舊檔案,因為它們仍然存在而且不可變【46】。 #### 批處理輸出的哲學 -本章前面討論過的Unix哲學(“[Unix哲學](#Unix哲學)”)鼓勵以顯式指明資料流的方式進行實驗:程式讀取輸入並寫入輸出。在這一過程中,輸入保持不變,任何先前的輸出都被新輸出完全替換,且沒有其他副作用。這意味著你可以隨心所欲地重新執行一個命令,略做改動或進行除錯,而不會攪亂系統的狀態。 +本章前面討論過的 Unix 哲學(“[Unix 哲學](#Unix哲學)”)鼓勵以顯式指明資料流的方式進行實驗:程式讀取輸入並寫入輸出。在這一過程中,輸入保持不變,任何先前的輸出都被新輸出完全替換,且沒有其他副作用。這意味著你可以隨心所欲地重新執行一個命令,略做改動或進行除錯,而不會攪亂系統的狀態。 -MapReduce作業的輸出處理遵循同樣的原理。透過將輸入視為不可變且避免副作用(如寫入外部資料庫),批處理作業不僅實現了良好的效能,而且更容易維護: +MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不可變且避免副作用(如寫入外部資料庫),批處理作業不僅實現了良好的效能,而且更容易維護: -- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。 (能夠從錯誤程式碼中恢復的概念被稱為**人類容錯(human fault tolerance)**【50】) -- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種**最小化不可逆性(minimizing irreversibility)** 的原則有利於敏捷軟體開發【51】。 -- 如果Map或Reduce任務失敗,MapReduce框架將自動重新排程,並在同樣的輸入上再次執行它。如果失敗是由程式碼中的錯誤造成的,那麼它會不斷崩潰,並最終導致作業在幾次嘗試之後失敗。但是如果故障是由於臨時問題導致的,那麼故障就會被容忍。因為輸入不可變,這種自動重試是安全的,而失敗任務的輸出會被MapReduce框架丟棄。 +- 如果在程式碼中引入了一個錯誤,而輸出錯誤或損壞了,則可以簡單地回滾到程式碼的先前版本,然後重新執行該作業,輸出將重新被糾正。或者,甚至更簡單,你可以將舊的輸出儲存在不同的目錄中,然後切換回原來的目錄。具有讀寫事務的資料庫沒有這個屬性:如果你部署了錯誤的程式碼,將錯誤的資料寫入資料庫,那麼回滾程式碼將無法修復資料庫中的資料。 (能夠從錯誤程式碼中恢復的概念被稱為 **人類容錯(human fault tolerance)**【50】) +- 由於回滾很容易,比起在錯誤意味著不可挽回的傷害的環境,功能開發進展能快很多。這種 **最小化不可逆性(minimizing irreversibility)** 的原則有利於敏捷軟體開發【51】。 +- 如果 Map 或 Reduce 任務失敗,MapReduce 框架將自動重新排程,並在同樣的輸入上再次執行它。如果失敗是由程式碼中的錯誤造成的,那麼它會不斷崩潰,並最終導致作業在幾次嘗試之後失敗。但是如果故障是由於臨時問題導致的,那麼故障就會被容忍。因為輸入不可變,這種自動重試是安全的,而失敗任務的輸出會被 MapReduce 框架丟棄。 - 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。 -- 與Unix工具類似,MapReduce作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。 +- 與 Unix 工具類似,MapReduce 作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。 -在這些領域,在Unix上表現良好的設計原則似乎也適用於Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因為大多數Unix工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用`{print $7}`來提取URL)。在Hadoop上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如Avro(請參閱“[Avro](ch4.md#Avro)”)和Parquet(請參閱“[列式儲存](ch3.md#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見[第四章](ch4.md))。 +在這些領域,在 Unix 上表現良好的設計原則似乎也適用於 Hadoop,但 Unix 和 Hadoop 在某些方面也有所不同。例如,因為大多數 Unix 工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用 `{print $7}` 來提取 URL)。在 Hadoop 上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如 Avro(請參閱 “[Avro](ch4.md#Avro)”)和 Parquet(請參閱 “[列式儲存](ch3.md#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見 [第四章](ch4.md))。 ### Hadoop與分散式資料庫的對比 -正如我們所看到的,Hadoop有點像Unix的分散式版本,其中HDFS是檔案系統,而MapReduce是Unix程序的怪異實現(總是在Map階段和Reduce階段執行`sort`工具)。我們瞭解瞭如何在這些原語的基礎上實現各種連線和分組操作。 +正如我們所看到的,Hadoop 有點像 Unix 的分散式版本,其中 HDFS 是檔案系統,而 MapReduce 是 Unix 程序的怪異實現(總是在 Map 階段和 Reduce 階段執行 `sort` 工具)。我們瞭解瞭如何在這些原語的基礎上實現各種連線和分組操作。 -當MapReduce論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的**大規模並行處理(MPP, massively parallel processing)** 資料庫中實現了【3,40】。比如Gamma database machine,Teradata和Tandem NonStop SQL就是這方面的先驅【52】。 +當 MapReduce 論文發表時【1】,它從某種意義上來說 —— 並不新鮮。我們在前幾節中討論的所有處理和並行連線演算法已經在十多年前所謂的 **大規模並行處理(MPP, massively parallel processing)** 資料庫中實現了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是這方面的先驅【52】。 -最大的區別是,MPP資料庫專注於在一組機器上並行執行分析SQL查詢,而MapReduce和分散式檔案系統【19】的組合則更像是一個可以執行任意程式的通用作業系統。 +最大的區別是,MPP 資料庫專注於在一組機器上並行執行分析 SQL 查詢,而 MapReduce 和分散式檔案系統【19】的組合則更像是一個可以執行任意程式的通用作業系統。 #### 儲存多樣性 資料庫要求你根據特定的模型(例如關係或文件)來構造資料,而分散式檔案系統中的檔案只是位元組序列,可以使用任何資料模型和編碼來編寫。它們可能是資料庫記錄的集合,但同樣可以是文字、影象、影片、感測器讀數、稀疏矩陣、特徵向量、基因組序列或任何其他型別的資料。 -說白了,Hadoop開放了將資料不加區分地轉儲到HDFS的可能性,允許後續再研究如何進一步處理【53】。相比之下,在將資料匯入資料庫專有儲存格式之前,MPP資料庫通常需要對資料和查詢模式進行仔細的前期建模。 +說白了,Hadoop 開放了將資料不加區分地轉儲到 HDFS 的可能性,允許後續再研究如何進一步處理【53】。相比之下,在將資料匯入資料庫專有儲存格式之前,MPP 資料庫通常需要對資料和查詢模式進行仔細的前期建模。 在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【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 資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。 #### 處理模型的多樣性 -MPP資料庫是單體的,緊密整合的軟體,負責磁碟上的儲存佈局,查詢計劃,排程和執行。由於這些元件都可以針對資料庫的特定需求進行調整和最佳化,因此整個系統可以在其設計針對的查詢型別上取得非常好的效能。而且,SQL查詢語言允許以優雅的語法表達查詢,而無需編寫程式碼,可以在業務分析師使用的視覺化工具(例如Tableau)中訪問到。 +MPP 資料庫是單體的,緊密整合的軟體,負責磁碟上的儲存佈局,查詢計劃,排程和執行。由於這些元件都可以針對資料庫的特定需求進行調整和最佳化,因此整個系統可以在其設計針對的查詢型別上取得非常好的效能。而且,SQL 查詢語言允許以優雅的語法表達查詢,而無需編寫程式碼,可以在業務分析師使用的視覺化工具(例如 Tableau)中訪問到。 -另一方面,並非所有型別的處理都可以合理地表達為SQL查詢。例如,如果要構建機器學習和推薦系統,或者使用相關性排名模型的全文搜尋索引,或者執行影象分析,則很可能需要更一般的資料處理模型。這些型別的處理通常是特別針對特定應用的(例如機器學習的特徵工程,機器翻譯的自然語言模型,欺詐預測的風險評估函式),因此它們不可避免地需要編寫程式碼,而不僅僅是查詢。 +另一方面,並非所有型別的處理都可以合理地表達為 SQL 查詢。例如,如果要構建機器學習和推薦系統,或者使用相關性排名模型的全文搜尋索引,或者執行影象分析,則很可能需要更一般的資料處理模型。這些型別的處理通常是特別針對特定應用的(例如機器學習的特徵工程,機器翻譯的自然語言模型,欺詐預測的風險評估函式),因此它們不可避免地需要編寫程式碼,而不僅僅是查詢。 -MapReduce使工程師能夠輕鬆地在大型資料集上執行自己的程式碼。如果你有HDFS和MapReduce,那麼你**可以**在它之上建立一個SQL查詢執行引擎,事實上這正是Hive專案所做的【31】。但是,你也可以編寫許多其他形式的批處理,這些批處理不必非要用SQL查詢表示。 +MapReduce 使工程師能夠輕鬆地在大型資料集上執行自己的程式碼。如果你有 HDFS 和 MapReduce,那麼你 **可以** 在它之上建立一個 SQL 查詢執行引擎,事實上這正是 Hive 專案所做的【31】。但是,你也可以編寫許多其他形式的批處理,這些批處理不必非要用 SQL 查詢表示。 -隨後,人們發現MapReduce對於某些型別的處理而言侷限性很大,表現很差,因此在Hadoop之上其他各種處理模型也被開發出來(我們將在“[MapReduce之後](#MapReduce之後)”中看到其中一些)。只有兩種處理模型,SQL和MapReduce,還不夠,需要更多不同的模型!而且由於Hadoop平臺的開放性,實施一整套方法是可行的,而這在單體MPP資料庫的範疇內是不可能的【58】。 +隨後,人們發現 MapReduce 對於某些型別的處理而言侷限性很大,表現很差,因此在 Hadoop 之上其他各種處理模型也被開發出來(我們將在 “[MapReduce 之後](#MapReduce之後)” 中看到其中一些)。只有兩種處理模型,SQL 和 MapReduce,還不夠,需要更多不同的模型!而且由於 Hadoop 平臺的開放性,實施一整套方法是可行的,而這在單體 MPP 資料庫的範疇內是不可能的【58】。 -至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在Hadoop方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。 +至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在 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 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。 #### 針對頻繁故障設計 -當比較MapReduce和MPP資料庫時,兩種不同的設計思路出現了:處理故障和使用記憶體與磁碟的方式。與線上系統相比,批處理對故障不太敏感,因為就算失敗也不會立即影響到使用者,而且它們總是能再次執行。 +當比較 MapReduce 和 MPP 資料庫時,兩種不同的設計思路出現了:處理故障和使用記憶體與磁碟的方式。與線上系統相比,批處理對故障不太敏感,因為就算失敗也不會立即影響到使用者,而且它們總是能再次執行。 -如果一個節點在執行查詢時崩潰,大多數MPP資料庫會中止整個查詢,並讓使用者重新提交查詢或自動重新執行它【3】。由於查詢通常最多執行幾秒鐘或幾分鐘,所以這種錯誤處理的方法是可以接受的,因為重試的代價不是太大。 MPP資料庫還傾向於在記憶體中保留儘可能多的資料(例如,使用雜湊連線)以避免從磁碟讀取的開銷。 +如果一個節點在執行查詢時崩潰,大多數 MPP 資料庫會中止整個查詢,並讓使用者重新提交查詢或自動重新執行它【3】。由於查詢通常最多執行幾秒鐘或幾分鐘,所以這種錯誤處理的方法是可以接受的,因為重試的代價不是太大。 MPP 資料庫還傾向於在記憶體中保留儘可能多的資料(例如,使用雜湊連線)以避免從磁碟讀取的開銷。 -另一方面,MapReduce可以容忍單個Map或Reduce任務的失敗,而不會影響作業的整體,透過以單個任務的粒度重試工作。它也會非常急切地將資料寫入磁碟,一方面是為了容錯,另一部分是因為假設資料集太大而不能適應記憶體。 +另一方面,MapReduce 可以容忍單個 Map 或 Reduce 任務的失敗,而不會影響作業的整體,透過以單個任務的粒度重試工作。它也會非常急切地將資料寫入磁碟,一方面是為了容錯,另一部分是因為假設資料集太大而不能適應記憶體。 -MapReduce方式更適用於較大的作業:要處理如此之多的資料並執行很長時間的作業,以至於在此過程中很可能至少遇到一個任務故障。在這種情況下,由於單個任務失敗而重新執行整個作業將是非常浪費的。即使以單個任務的粒度進行恢復引入了使得無故障處理更慢的開銷,但如果任務失敗率足夠高,這仍然是一種合理的權衡。 +MapReduce 方式更適用於較大的作業:要處理如此之多的資料並執行很長時間的作業,以至於在此過程中很可能至少遇到一個任務故障。在這種情況下,由於單個任務失敗而重新執行整個作業將是非常浪費的。即使以單個任務的粒度進行恢復引入了使得無故障處理更慢的開銷,但如果任務失敗率足夠高,這仍然是一種合理的權衡。 但是這些假設有多麼現實呢?在大多數叢集中,機器故障確實會發生,但是它們不是很頻繁 —— 可能少到絕大多數作業都不會經歷機器故障。為了容錯,真的值得帶來這麼大的額外開銷嗎? -要了解MapReduce節約使用記憶體和在任務的層次進行恢復的原因,瞭解最初設計MapReduce的環境是很有幫助的。 Google有著混用的資料中心,線上生產服務和離線批處理作業在同樣機器上執行。每個任務都有一個透過容器強制執行的資源配給(CPU核心,RAM,磁碟空間等)。每個任務也具有優先順序,如果優先順序較高的任務需要更多的資源,則可以終止(搶佔)同一臺機器上較低優先順序的任務以釋放資源。優先順序還決定了計算資源的定價:團隊必須為他們使用的資源付費,而優先順序更高的程序花費更多【59】。 +要了解 MapReduce 節約使用記憶體和在任務的層次進行恢復的原因,瞭解最初設計 MapReduce 的環境是很有幫助的。Google 有著混用的資料中心,線上生產服務和離線批處理作業在同樣機器上執行。每個任務都有一個透過容器強制執行的資源配給(CPU 核心、RAM、磁碟空間等)。每個任務也具有優先順序,如果優先順序較高的任務需要更多的資源,則可以終止(搶佔)同一臺機器上較低優先順序的任務以釋放資源。優先順序還決定了計算資源的定價:團隊必須為他們使用的資源付費,而優先順序更高的程序花費更多【59】。 -這種架構允許非生產(低優先順序)計算資源被**過量使用(overcommitted)**,因為系統知道必要時它可以回收資源。與分離生產和非生產任務的系統相比,過量使用資源可以更好地利用機器並提高效率。但由於MapReduce作業以低優先順序執行,它們隨時都有被搶佔的風險,因為優先順序較高的程序可能需要其資源。在高優先順序程序拿走所需資源後,批次作業能有效地“撿麵包屑”,利用剩下的任何計算資源。 +這種架構允許非生產(低優先順序)計算資源被 **過量使用(overcommitted)**,因為系統知道必要時它可以回收資源。與分離生產和非生產任務的系統相比,過量使用資源可以更好地利用機器並提高效率。但由於 MapReduce 作業以低優先順序執行,它們隨時都有被搶佔的風險,因為優先順序較高的程序可能需要其資源。在高優先順序程序拿走所需資源後,批次作業能有效地 “撿麵包屑”,利用剩下的任何計算資源。 -在谷歌,執行一個小時的MapReduce任務有大約有5%的風險被終止,為了給更高優先順序的程序挪地方。這一概率比硬體問題、機器重啟或其他原因的概率高了一個數量級【59】。按照這種搶佔率,如果一個作業有100個任務,每個任務執行10分鐘,那麼至少有一個任務在完成之前被終止的風險大於50%。 +在谷歌,執行一個小時的 MapReduce 任務有大約有 5% 的風險被終止,為了給更高優先順序的程序挪地方。這一概率比硬體問題、機器重啟或其他原因的概率高了一個數量級【59】。按照這種搶佔率,如果一個作業有 100 個任務,每個任務執行 10 分鐘,那麼至少有一個任務在完成之前被終止的風險大於 50%。 -這就是MapReduce被設計為容忍頻繁意外任務終止的原因:不是因為硬體很不可靠,而是因為任意終止程序的自由有利於提高計算叢集中的資源利用率。 +這就是 MapReduce 被設計為容忍頻繁意外任務終止的原因:不是因為硬體很不可靠,而是因為任意終止程序的自由有利於提高計算叢集中的資源利用率。 -在開源的叢集排程器中,搶佔的使用較少。 YARN的CapacityScheduler支援搶佔,以平衡不同佇列的資源分配【58】,但在編寫本文時,YARN,Mesos或Kubernetes不支援通用的優先順序搶佔【60】。在任務不經常被終止的環境中,MapReduce的這一設計決策就沒有多少意義了。在下一節中,我們將研究一些與MapReduce設計決策相異的替代方案。 +在開源的叢集排程器中,搶佔的使用較少。 YARN 的 CapacityScheduler 支援搶佔,以平衡不同佇列的資源分配【58】,但在編寫本文時,YARN,Mesos 或 Kubernetes 不支援通用的優先順序搶佔【60】。在任務不經常被終止的環境中,MapReduce 的這一設計決策就沒有多少意義了。在下一節中,我們將研究一些與 MapReduce 設計決策相異的替代方案。 ## MapReduce之後 -雖然MapReduce在2000年代後期變得非常流行,並受到大量的炒作,但它只是分散式系統的許多可能的程式設計模型之一。對於不同的資料量,資料結構和處理型別,其他工具可能更適合表示計算。 +雖然 MapReduce 在 2000 年代後期變得非常流行,並受到大量的炒作,但它只是分散式系統的許多可能的程式設計模型之一。對於不同的資料量,資料結構和處理型別,其他工具可能更適合表示計算。 -不管如何,我們在這一章花了大把時間來討論MapReduce,因為它是一種有用的學習工具,它是分散式檔案系統的一種相當簡單明晰的抽象。在這裡,**簡單**意味著我們能理解它在做什麼,而不是意味著使用它很簡單。恰恰相反:使用原始的MapReduce API來實現複雜的處理工作實際上是非常困難和費力的 —— 例如,任意一種連線演算法都需要你從頭開始實現【37】。 +不管如何,我們在這一章花了大把時間來討論 MapReduce,因為它是一種有用的學習工具,它是分散式檔案系統的一種相當簡單明晰的抽象。在這裡,**簡單** 意味著我們能理解它在做什麼,而不是意味著使用它很簡單。恰恰相反:使用原始的 MapReduce API 來實現複雜的處理工作實際上是非常困難和費力的 —— 例如,任意一種連線演算法都需要你從頭開始實現【37】。 -針對直接使用MapReduce的困難,在MapReduce上有很多高階程式設計模型(Pig,Hive,Cascading,Crunch)被創造出來,作為建立在MapReduce之上的抽象。如果你瞭解MapReduce的原理,那麼它們學起來相當簡單。而且它們的高階結構能顯著簡化許多常見批處理任務的實現。 +針對直接使用 MapReduce 的困難,在 MapReduce 上有很多高階程式設計模型(Pig、Hive、Cascading、Crunch)被創造出來,作為建立在 MapReduce 之上的抽象。如果你瞭解 MapReduce 的原理,那麼它們學起來相當簡單。而且它們的高階結構能顯著簡化許多常見批處理任務的實現。 -但是,MapReduce執行模型本身也存在一些問題,這些問題並沒有透過增加另一個抽象層次而解決,而對於某些型別的處理,它表現得非常差勁。一方面,MapReduce非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。 +但是,MapReduce 執行模型本身也存在一些問題,這些問題並沒有透過增加另一個抽象層次而解決,而對於某些型別的處理,它表現得非常差勁。一方面,MapReduce 非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。 -在本章的其餘部分中,我們將介紹一些批處理方法。在[第十一章](ch11.md)我們將轉向流處理,它可以看作是加速批處理的另一種方法。 +在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十一章](ch11.md) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。 ### 物化中間狀態 -如前所述,每個MapReduce作業都獨立於其他任何作業。作業與世界其他地方的主要連線點是分散式檔案系統上的輸入和輸出目錄。如果希望一個作業的輸出成為第二個作業的輸入,則需要將第二個作業的輸入目錄配置為第一個作業輸出目錄,且外部工作流排程程式必須在第一個作業完成後再啟動第二個。 +如前所述,每個 MapReduce 作業都獨立於其他任何作業。作業與世界其他地方的主要連線點是分散式檔案系統上的輸入和輸出目錄。如果希望一個作業的輸出成為第二個作業的輸入,則需要將第二個作業的輸入目錄配置為第一個作業輸出目錄,且外部工作流排程程式必須在第一個作業完成後再啟動第二個。 -如果第一個作業的輸出是要在組織內廣泛釋出的資料集,則這種配置是合理的。在這種情況下,你需要透過名稱引用它,並將其重用為多個不同作業的輸入(包括由其他團隊開發的作業)。將資料釋出到分散式檔案系統中眾所周知的位置能夠帶來**松耦合**,這樣作業就不需要知道是誰在提供輸入或誰在消費輸出(請參閱“[邏輯與佈線相分離](#邏輯與佈線相分離)”)。 +如果第一個作業的輸出是要在組織內廣泛釋出的資料集,則這種配置是合理的。在這種情況下,你需要透過名稱引用它,並將其重用為多個不同作業的輸入(包括由其他團隊開發的作業)。將資料釋出到分散式檔案系統中眾所周知的位置能夠帶來 **松耦合**,這樣作業就不需要知道是誰在提供輸入或誰在消費輸出(請參閱 “[邏輯與佈線相分離](#邏輯與佈線相分離)”)。 -但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的**中間狀態(intermediate state)**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由50或100個MapReduce作業組成的複雜工作流中,存在著很多這樣的中間狀態【29】。 +但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態(intermediate state)**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中,存在著很多這樣的中間狀態【29】。 -將這個中間狀態寫入檔案的過程稱為**物化(materialization)**。 (在“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算) +將這個中間狀態寫入檔案的過程稱為 **物化(materialization)**。 (在 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算) -作為對照,本章開頭的日誌分析示例使用Unix管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地**流(stream)** 向輸入。 +作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流(stream)** 向輸入。 -與Unix管道相比,MapReduce完全物化中間狀態的方法存在不足之處: +與 Unix 管道相比,MapReduce 完全物化中間狀態的方法存在不足之處: -- MapReduce作業只有在前驅作業(生成其輸入)中的所有任務都完成時才能啟動,而由Unix管道連線的程序會同時啟動,輸出一旦生成就會被消費。不同機器上的資料偏斜或負載不均意味著一個作業往往會有一些掉隊的任務,比其他任務要慢得多才能完成。必須等待至前驅作業的所有任務完成,拖慢了整個工作流程的執行。 -- Mapper通常是多餘的:它們僅僅是讀取剛剛由Reducer寫入的同樣檔案,為下一個階段的分割槽和排序做準備。在許多情況下,Mapper程式碼可能是前驅Reducer的一部分:如果Reducer和Mapper的輸出有著相同的分割槽與排序方式,那麼Reducer就可以直接串在一起,而不用與Mapper相互交織。 +- MapReduce 作業只有在前驅作業(生成其輸入)中的所有任務都完成時才能啟動,而由 Unix 管道連線的程序會同時啟動,輸出一旦生成就會被消費。不同機器上的資料偏斜或負載不均意味著一個作業往往會有一些掉隊的任務,比其他任務要慢得多才能完成。必須等待至前驅作業的所有任務完成,拖慢了整個工作流程的執行。 +- Mapper 通常是多餘的:它們僅僅是讀取剛剛由 Reducer 寫入的同樣檔案,為下一個階段的分割槽和排序做準備。在許多情況下,Mapper 程式碼可能是前驅 Reducer 的一部分:如果 Reducer 和 Mapper 的輸出有著相同的分割槽與排序方式,那麼 Reducer 就可以直接串在一起,而不用與 Mapper 相互交織。 - 將中間狀態儲存在分散式檔案系統中意味著這些檔案被複制到多個節點,對這些臨時資料這麼搞就比較過分了。 #### 資料流引擎 -為了解決MapReduce的這些問題,幾種用於分散式批處理的新執行引擎被開發出來,其中最著名的是Spark 【61,62】,Tez 【63,64】和Flink 【65,66】。它們的設計方式有很多區別,但有一個共同點:把整個工作流作為單個作業來處理,而不是把它分解為獨立的子作業。 +為了解決 MapReduce 的這些問題,幾種用於分散式批處理的新執行引擎被開發出來,其中最著名的是 Spark 【61,62】,Tez 【63,64】和 Flink 【65,66】。它們的設計方式有很多區別,但有一個共同點:把整個工作流作為單個作業來處理,而不是把它分解為獨立的子作業。 -由於它們將工作流顯式建模為資料從幾個處理階段穿過,所以這些系統被稱為**資料流引擎(dataflow engines)**。像MapReduce一樣,它們在一條線上透過反覆呼叫使用者定義的函式來一次處理一條記錄,它們透過輸入分割槽來並行化載荷,它們透過網路將一個函式的輸出複製到另一個函式的輸入。 +由於它們將工作流顯式建模為資料從幾個處理階段穿過,所以這些系統被稱為 **資料流引擎(dataflow engines)**。像 MapReduce 一樣,它們在一條線上透過反覆呼叫使用者定義的函式來一次處理一條記錄,它們透過輸入分割槽來並行化載荷,它們透過網路將一個函式的輸出複製到另一個函式的輸入。 -與MapReduce不同,這些函式不需要嚴格扮演交織的Map與Reduce的角色,而是可以以更靈活的方式進行組合。我們稱這些函式為**運算元(operators)**,資料流引擎提供了幾種不同的選項來將一個運算元的輸出連線到另一個運算元的輸入: +與 MapReduce 不同,這些函式不需要嚴格扮演交織的 Map 與 Reduce 的角色,而是可以以更靈活的方式進行組合。我們稱這些函式為 **運算元(operators)**,資料流引擎提供了幾種不同的選項來將一個運算元的輸出連線到另一個運算元的輸入: -- 一種選項是對記錄按鍵重新分割槽並排序,就像在MapReduce的混洗階段一樣(請參閱“[分散式執行MapReduce](#分散式執行MapReduce)”)。這種功能可以用於實現排序合併連線和分組,就像在MapReduce中一樣。 +- 一種選項是對記錄按鍵重新分割槽並排序,就像在 MapReduce 的混洗階段一樣(請參閱 “[分散式執行 MapReduce](#分散式執行MapReduce)”)。這種功能可以用於實現排序合併連線和分組,就像在 MapReduce 中一樣。 - 另一種可能是接受多個輸入,並以相同的方式進行分割槽,但跳過排序。當記錄的分割槽重要但順序無關緊要時,這省去了分割槽雜湊連線的工作,因為構建散列表還是會把順序隨機打亂。 - 對於廣播雜湊連線,可以將一個運算元的輸出,傳送到連線運算元的所有分割槽。 -這種型別的處理引擎是基於像Dryad【67】和Nephele【68】這樣的研究系統,與MapReduce模型相比,它有幾個優點: +這種型別的處理引擎是基於像 Dryad【67】和 Nephele【68】這樣的研究系統,與 MapReduce 模型相比,它有幾個優點: -- 排序等昂貴的工作只需要在實際需要的地方執行,而不是預設地在每個Map和Reduce階段之間出現。 -- 沒有不必要的Map任務,因為Mapper所做的工作通常可以合併到前面的Reduce運算元中(因為Mapper不會更改資料集的分割槽)。 +- 排序等昂貴的工作只需要在實際需要的地方執行,而不是預設地在每個 Map 和 Reduce 階段之間出現。 +- 沒有不必要的 Map 任務,因為 Mapper 所做的工作通常可以合併到前面的 Reduce 運算元中(因為 Mapper 不會更改資料集的分割槽)。 - 由於工作流中的所有連線和資料依賴都是顯式宣告的,因此排程程式能夠總覽全域性,知道哪裡需要哪些資料,因而能夠利用區域性性進行最佳化。例如,它可以嘗試將消費某些資料的任務放在與生成這些資料的任務相同的機器上,從而資料可以透過共享記憶體緩衝區傳輸,而不必透過網路複製。 -- 通常,運算元間的中間狀態足以儲存在記憶體中或寫入本地磁碟,這比寫入HDFS需要更少的I/O(必須將其複製到多臺機器,並將每個副本寫入磁碟)。 MapReduce已經對Mapper的輸出做了這種最佳化,但資料流引擎將這種思想推廣至所有的中間狀態。 +- 通常,運算元間的中間狀態足以儲存在記憶體中或寫入本地磁碟,這比寫入 HDFS 需要更少的 I/O(必須將其複製到多臺機器,並將每個副本寫入磁碟)。 MapReduce 已經對 Mapper 的輸出做了這種最佳化,但資料流引擎將這種思想推廣至所有的中間狀態。 - 運算元可以在輸入就緒後立即開始執行;後續階段無需等待前驅階段整個完成後再開始。 -- 與MapReduce(為每個任務啟動一個新的JVM)相比,現有Java虛擬機器(JVM)程序可以重用來執行新運算元,從而減少啟動開銷。 +- 與 MapReduce(為每個任務啟動一個新的 JVM)相比,現有 Java 虛擬機器(JVM)程序可以重用來執行新運算元,從而減少啟動開銷。 -你可以使用資料流引擎執行與MapReduce工作流同樣的計算,而且由於此處所述的最佳化,通常執行速度要明顯快得多。既然運算元是Map和Reduce的泛化,那麼相同的處理程式碼就可以在任一執行引擎上執行:Pig,Hive或Cascading中實現的工作流可以無需修改程式碼,可以透過修改配置,簡單地從MapReduce切換到Tez或Spark【64】。 +你可以使用資料流引擎執行與 MapReduce 工作流同樣的計算,而且由於此處所述的最佳化,通常執行速度要明顯快得多。既然運算元是 Map 和 Reduce 的泛化,那麼相同的處理程式碼就可以在任一執行引擎上執行:Pig,Hive 或 Cascading 中實現的工作流可以無需修改程式碼,可以透過修改配置,簡單地從 MapReduce 切換到 Tez 或 Spark【64】。 -Tez是一個相當薄的庫,它依賴於YARN shuffle服務來實現節點間資料的實際複製【58】,而Spark和Flink則是包含了獨立網路通訊層,排程器,及使用者向API的大型框架。我們將簡要討論這些高階API。 +Tez 是一個相當薄的庫,它依賴於 YARN shuffle 服務來實現節點間資料的實際複製【58】,而 Spark 和 Flink 則是包含了獨立網路通訊層,排程器,及使用者向 API 的大型框架。我們將簡要討論這些高階 API。 #### 容錯 -完全物化中間狀態至分散式檔案系統的一個優點是,它具有永續性,這使得MapReduce中的容錯相當容易:如果一個任務失敗,它可以在另一臺機器上重新啟動,並從檔案系統重新讀取相同的輸入。 +完全物化中間狀態至分散式檔案系統的一個優點是,它具有永續性,這使得 MapReduce 中的容錯相當容易:如果一個任務失敗,它可以在另一臺機器上重新啟動,並從檔案系統重新讀取相同的輸入。 -Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同的方法來容錯:如果一臺機器發生故障,並且該機器上的中間狀態丟失,則它會從其他仍然可用的資料重新計算(在可行的情況下是先前的中間狀態,要麼就只能是原始輸入資料,通常在HDFS上)。 +Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS,因此它們採取了不同的方法來容錯:如果一臺機器發生故障,並且該機器上的中間狀態丟失,則它會從其他仍然可用的資料重新計算(在可行的情況下是先前的中間狀態,要麼就只能是原始輸入資料,通常在 HDFS 上)。 -為了實現這種重新計算,框架必須跟蹤一個給定的資料是如何計算的 —— 使用了哪些輸入分割槽?應用了哪些運算元? Spark使用**彈性分散式資料集(RDD,Resilient Distributed Dataset)** 的抽象來跟蹤資料的譜系【61】,而Flink對運算元狀態存檔,允許恢復執行在執行過程中遇到錯誤的運算元【66】。 +為了實現這種重新計算,框架必須跟蹤一個給定的資料是如何計算的 —— 使用了哪些輸入分割槽?應用了哪些運算元? Spark 使用 **彈性分散式資料集(RDD,Resilient Distributed Dataset)** 的抽象來跟蹤資料的譜系【61】,而 Flink 對運算元狀態存檔,允許恢復執行在執行過程中遇到錯誤的運算元【66】。 -在重新計算資料時,重要的是要知道計算是否是**確定性的**:也就是說,給定相同的輸入資料,運算元是否始終產生相同的輸出?如果一些丟失的資料已經發送給下游運算元,這個問題就很重要。如果運算元重新啟動,重新計算的資料與原有的丟失資料不一致,下游運算元很難解決新舊資料之間的矛盾。對於不確定性運算元來說,解決方案通常是殺死下游運算元,然後再重跑新資料。 +在重新計算資料時,重要的是要知道計算是否是 **確定性的**:也就是說,給定相同的輸入資料,運算元是否始終產生相同的輸出?如果一些丟失的資料已經發送給下游運算元,這個問題就很重要。如果運算元重新啟動,重新計算的資料與原有的丟失資料不一致,下游運算元很難解決新舊資料之間的矛盾。對於不確定性運算元來說,解決方案通常是殺死下游運算元,然後再重跑新資料。 為了避免這種級聯故障,最好讓運算元具有確定性。但需要注意的是,非確定性行為很容易悄悄溜進來:例如,許多程式語言在迭代雜湊表的元素時不能對順序作出保證,許多概率和統計演算法顯式依賴於使用隨機數,以及用到系統時鐘或外部資料來源,這些都是都不確定性的行為。為了能可靠地從故障中恢復,需要消除這種不確定性因素,例如使用固定的種子生成偽隨機數。 @@ -589,136 +589,136 @@ Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同 #### 關於物化的討論 -回到Unix的類比,我們看到,MapReduce就像是將每個命令的輸出寫入臨時檔案,而資料流引擎看起來更像是Unix管道。尤其是Flink是基於管道執行的思想而建立的:也就是說,將運算元的輸出增量地傳遞給其他運算元,不待輸入完成便開始處理。 +回到 Unix 的類比,我們看到,MapReduce 就像是將每個命令的輸出寫入臨時檔案,而資料流引擎看起來更像是 Unix 管道。尤其是 Flink 是基於管道執行的思想而建立的:也就是說,將運算元的輸出增量地傳遞給其他運算元,不待輸入完成便開始處理。 排序運算元不可避免地需要消費全部的輸入後才能生成任何輸出,因為輸入中最後一條輸入記錄可能具有最小的鍵,因此需要作為第一條記錄輸出。因此,任何需要排序的運算元都需要至少暫時地累積狀態。但是工作流的許多其他部分可以以流水線方式執行。 -當作業完成時,它的輸出需要持續到某個地方,以便使用者可以找到並使用它—— 很可能它會再次寫入分散式檔案系統。因此,在使用資料流引擎時,HDFS上的物化資料集通常仍是作業的輸入和最終輸出。和MapReduce一樣,輸入是不可變的,輸出被完全替換。比起MapReduce的改進是,你不用再自己去將中間狀態寫入檔案系統了。 +當作業完成時,它的輸出需要持續到某個地方,以便使用者可以找到並使用它 —— 很可能它會再次寫入分散式檔案系統。因此,在使用資料流引擎時,HDFS 上的物化資料集通常仍是作業的輸入和最終輸出。和 MapReduce 一樣,輸入是不可變的,輸出被完全替換。比起 MapReduce 的改進是,你不用再自己去將中間狀態寫入檔案系統了。 ### 圖與迭代處理 -在“[圖資料模型](ch2.md#圖資料模型)”中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](ch2.md)的討論集中在OLTP風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。 +在 “[圖資料模型](ch2.md#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](ch2.md) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。 -批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是PageRank 【69】,它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分,用於確定網路搜尋引擎呈現結果的順序。 +批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是 PageRank 【69】,它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分,用於確定網路搜尋引擎呈現結果的順序。 -> 像Spark,Flink和Tez這樣的資料流引擎(請參閱“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂! +> 像 Spark、Flink 和 Tez 這樣的資料流引擎(請參閱 “[物化中間狀態](#物化中間狀態)”)通常將運算元作為 **有向無環圖(DAG)** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流** 被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂! -許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**傳遞閉包**,即transitive closure)。 +許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](../img/fig2-6.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure)。 -可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種“重複至完成”的想法不能用普通的MapReduce來表示,因為它只掃過一趟資料。這種演算法因此經常以**迭代**的風格實現: +可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種 “重複至完成” 的想法不能用普通的 MapReduce 來表示,因為它只掃過一趟資料。這種演算法因此經常以 **迭代** 的風格實現: 1. 外部排程程式執行批處理來計算演算法的一個步驟。 2. 當批處理過程完成時,排程器檢查它是否完成(基於完成條件 —— 例如,沒有更多的邊要跟進,或者與上次迭代相比的變化低於某個閾值)。 -3. 如果尚未完成,則排程程式返回到步驟1並執行另一輪批處理。 +3. 如果尚未完成,則排程程式返回到步驟 1 並執行另一輪批處理。 -這種方法是有效的,但是用MapReduce實現它往往非常低效,因為MapReduce沒有考慮演算法的迭代性質:它總是讀取整個輸入資料集併產生一個全新的輸出資料集,即使與上次迭代相比,改變的僅僅是圖中的一小部分。 +這種方法是有效的,但是用 MapReduce 實現它往往非常低效,因為 MapReduce 沒有考慮演算法的迭代性質:它總是讀取整個輸入資料集併產生一個全新的輸出資料集,即使與上次迭代相比,改變的僅僅是圖中的一小部分。 #### Pregel處理模型 -針對圖批處理的最佳化 —— **批次同步並行(BSP,Bulk Synchronous Parallel)** 計算模型【70】已經開始流行起來。其中,Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】實現了它。它也被稱為**Pregel**模型,因為Google的Pregel論文推廣了這種處理圖的方法【72】。 +針對圖批處理的最佳化 —— **批次同步並行(BSP,Bulk Synchronous Parallel)** 計算模型【70】已經開始流行起來。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】實現了它。它也被稱為 **Pregel** 模型,因為 Google 的 Pregel 論文推廣了這種處理圖的方法【72】。 -回想一下在MapReduce中,Mapper在概念上向Reducer的特定呼叫“傳送訊息”,因為框架將所有具有相同鍵的Mapper輸出集中在一起。 Pregel背後有一個類似的想法:一個頂點可以向另一個頂點“傳送訊息”,通常這些訊息是沿著圖的邊傳送的。 +回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定呼叫 “傳送訊息”,因為框架將所有具有相同鍵的 Mapper 輸出集中在一起。 Pregel 背後有一個類似的想法:一個頂點可以向另一個頂點 “傳送訊息”,通常這些訊息是沿著圖的邊傳送的。 -在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫Reducer一樣。與MapReduce的不同之處在於,在Pregel模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。 +在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。 -這與Actor模型有些相似(請參閱“[分散式的Actor框架](ch4.md#分散式的Actor框架)”),除了頂點狀態和頂點之間的訊息具有容錯性和永續性,且通訊以固定的回合進行:在每次迭代中,框架遞送上次迭代中傳送的所有訊息。Actor通常沒有這樣的時序保證。 +這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](ch4.md#分散式的Actor框架)”),除了頂點狀態和頂點之間的訊息具有容錯性和永續性,且通訊以固定的回合進行:在每次迭代中,框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。 #### 容錯 -頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高Pregel作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於Pregel模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。 +頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高 Pregel 作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於 Pregel 模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。 -即使底層網路可能丟失、重複或任意延遲訊息(請參閱“[不可靠的網路](ch8.md#不可靠的網路)”),Pregel的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像MapReduce一樣,框架能從故障中透明地恢復,以簡化在Pregel上實現演算法的程式設計模型。 +即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”),Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。 這種容錯是透過在迭代結束時,定期存檔所有頂點的狀態來實現的,即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失,則最簡單的解決方法是將整個圖計算回滾到上一個存檔點,然後重啟計算。如果演算法是確定性的,且訊息記錄在日誌中,那麼也可以選擇性地只恢復丟失的分割槽(就像之前討論過的資料流引擎)【72】。 #### 並行執行 -頂點不需要知道它在哪臺物理機器上執行;當它向其他頂點發送訊息時,它只是簡單地將訊息發往某個頂點ID。圖的分割槽取決於框架 —— 即,確定哪個頂點執行在哪臺機器上,以及如何透過網路路由訊息,以便它們到達正確的地方。 +頂點不需要知道它在哪臺物理機器上執行;當它向其他頂點發送訊息時,它只是簡單地將訊息發往某個頂點 ID。圖的分割槽取決於框架 —— 即,確定哪個頂點執行在哪臺機器上,以及如何透過網路路由訊息,以便它們到達正確的地方。 -由於程式設計模型一次僅處理一個頂點(有時稱為“像頂點一樣思考”),所以框架可以以任意方式對圖分割槽。理想情況下如果頂點需要進行大量的通訊,那麼它們最好能被分割槽到同一臺機器上。然而找到這樣一種最佳化的分割槽方法是很困難的 —— 在實踐中,圖經常按照任意分配的頂點ID分割槽,而不會嘗試將相關的頂點分組在一起。 +由於程式設計模型一次僅處理一個頂點(有時稱為 “像頂點一樣思考”),所以框架可以以任意方式對圖分割槽。理想情況下如果頂點需要進行大量的通訊,那麼它們最好能被分割槽到同一臺機器上。然而找到這樣一種最佳化的分割槽方法是很困難的 —— 在實踐中,圖經常按照任意分配的頂點 ID 分割槽,而不會嘗試將相關的頂點分組在一起。 因此,圖演算法通常會有很多跨機器通訊的額外開銷,而中間狀態(節點之間傳送的訊息)往往比原始圖大。透過網路傳送訊息的開銷會顯著拖慢分散式圖演算法的速度。 -出於這個原因,如果你的圖可以放入一臺計算機的記憶體中,那麼單機(甚至可能是單執行緒)演算法很可能會超越分散式批處理【73,74】。圖比記憶體大也沒關係,只要能放入單臺計算機的磁碟,使用GraphChi等框架進行單機處理是就一個可行的選擇【75】。如果圖太大,不適合單機處理,那麼像Pregel這樣的分散式方法是不可避免的。高效的並行圖演算法是一個進行中的研究領域【76】。 +出於這個原因,如果你的圖可以放入一臺計算機的記憶體中,那麼單機(甚至可能是單執行緒)演算法很可能會超越分散式批處理【73,74】。圖比記憶體大也沒關係,只要能放入單臺計算機的磁碟,使用 GraphChi 等框架進行單機處理是就一個可行的選擇【75】。如果圖太大,不適合單機處理,那麼像 Pregel 這樣的分散式方法是不可避免的。高效的並行圖演算法是一個進行中的研究領域【76】。 ### 高階API和語言 -自MapReduce開始流行的這幾年以來,分散式批處理的執行引擎已經很成熟了。到目前為止,基礎設施已經足夠強大,能夠儲存和處理超過10,000臺機器叢集上的數PB的資料。由於在這種規模下物理執行批處理的問題已經被認為或多或少解決了,所以關注點已經轉向其他領域:改進程式設計模型,提高處理效率,擴大這些技術可以解決的問題集。 +自 MapReduce 開始流行的這幾年以來,分散式批處理的執行引擎已經很成熟了。到目前為止,基礎設施已經足夠強大,能夠儲存和處理超過 10,000 臺機器叢集上的數 PB 的資料。由於在這種規模下物理執行批處理的問題已經被認為或多或少解決了,所以關注點已經轉向其他領域:改進程式設計模型,提高處理效率,擴大這些技術可以解決的問題集。 -如前所述,Hive,Pig,Cascading和Crunch等高階語言和API變得越來越流行,因為手寫MapReduce作業實在是個苦力活。隨著Tez的出現,這些高階語言還有一個額外好處,可以遷移到新的資料流執行引擎,而無需重寫作業程式碼。 Spark和Flink也有它們自己的高階資料流API,通常是從FlumeJava中獲取的靈感【34】。 +如前所述,Hive,Pig,Cascading 和 Crunch 等高階語言和 API 變得越來越流行,因為手寫 MapReduce 作業實在是個苦力活。隨著 Tez 的出現,這些高階語言還有一個額外好處,可以遷移到新的資料流執行引擎,而無需重寫作業程式碼。 Spark 和 Flink 也有它們自己的高階資料流 API,通常是從 FlumeJava 中獲取的靈感【34】。 -這些資料流API通常使用關係型構建塊來表達一個計算:按某個欄位連線資料集;按鍵對元組做分組;按某些條件過濾;並透過計數求和或其他函式來聚合元組。在內部,這些操作是使用本章前面討論過的各種連線和分組演算法來實現的。 +這些資料流 API 通常使用關係型構建塊來表達一個計算:按某個欄位連線資料集;按鍵對元組做分組;按某些條件過濾;並透過計數求和或其他函式來聚合元組。在內部,這些操作是使用本章前面討論過的各種連線和分組演算法來實現的。 -除了少寫程式碼的明顯優勢之外,這些高階介面還支援互動式用法,在這種互動式使用中,你可以在Shell中增量式編寫分析程式碼,頻繁執行來觀察它做了什麼。這種開發風格在探索資料集和試驗處理方法時非常有用。這也讓人聯想到Unix哲學,我們在“[Unix哲學](#Unix哲學)”中討論過這個問題。 +除了少寫程式碼的明顯優勢之外,這些高階介面還支援互動式用法,在這種互動式使用中,你可以在 Shell 中增量式編寫分析程式碼,頻繁執行來觀察它做了什麼。這種開發風格在探索資料集和試驗處理方法時非常有用。這也讓人聯想到 Unix 哲學,我們在 “[Unix 哲學](#Unix哲學)” 中討論過這個問題。 此外,這些高階介面不僅提高了人類的工作效率,也提高了機器層面的作業執行效率。 #### 向宣告式查詢語言的轉變 -與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。 Hive,Spark和Flink都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。 +與硬寫執行連線的程式碼相比,指定連線關係運算元的優點是,框架可以分析連線輸入的屬性,並自動決定哪種上述連線演算法最適合當前任務。 Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點,甚至可以改變連線順序,最小化中間狀態的數量【66,77,78,79】。 -連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以**宣告式(declarative)** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在“[資料查詢語言](ch2.md#資料查詢語言)”中見過這個想法。 +連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式(declarative)** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](ch2.md#資料查詢語言)” 中見過這個想法。 -但MapReduce及其資料流後繼者在其他方面,與SQL的完全宣告式查詢模型有很大區別。 MapReduce是圍繞著回撥函式的概念建立的:對於每條記錄或者一組記錄,呼叫一個使用者定義的函式(Mapper或Reducer),並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作:解析、自然語言分析、影象分析以及執行數值或統計演算法等。 +但 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資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。 +透過在高階 API 中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像 MPP 資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。 #### 專業化的不同領域 -儘管能夠執行任意程式碼的可擴充套件性是很有用的,但是也有很多常見的例子,不斷重複著標準的處理模式。因而這些模式值得擁有自己的可重用通用構建模組實現。傳統上,MPP資料庫滿足了商業智慧分析和業務報表的需求,但這只是許多使用批處理的領域之一。 +儘管能夠執行任意程式碼的可擴充套件性是很有用的,但是也有很多常見的例子,不斷重複著標準的處理模式。因而這些模式值得擁有自己的可重用通用構建模組實現。傳統上,MPP 資料庫滿足了商業智慧分析和業務報表的需求,但這只是許多使用批處理的領域之一。 -另一個越來越重要的領域是統計和數值演算法,它們是機器學習應用所需要的(例如分類器和推薦系統)。可重用的實現正在出現:例如,Mahout在MapReduce、Spark和Flink之上實現了用於機器學習的各種演算法,而MADlib在關係型MPP資料庫(Apache HAWQ)中實現了類似的功能【54】。 +另一個越來越重要的領域是統計和數值演算法,它們是機器學習應用所需要的(例如分類器和推薦系統)。可重用的實現正在出現:例如,Mahout 在 MapReduce、Spark 和 Flink 之上實現了用於機器學習的各種演算法,而 MADlib 在關係型 MPP 資料庫(Apache HAWQ)中實現了類似的功能【54】。 -空間演算法也是有用的,例如**k近鄰搜尋(k-nearest neighbors, kNN)**【80】,它在一些多維空間中搜索與給定項最近的專案 —— 這是一種相似性搜尋。近似搜尋對於基因組分析演算法也很重要,它們需要找到相似但不相同的字串【81】。 +空間演算法也是有用的,例如 **k 近鄰搜尋(k-nearest neighbors, kNN)**【80】,它在一些多維空間中搜索與給定項最近的專案 —— 這是一種相似性搜尋。近似搜尋對於基因組分析演算法也很重要,它們需要找到相似但不相同的字串【81】。 -批處理引擎正被用於分散式執行日益廣泛的各領域演算法。隨著批處理系統獲得各種內建功能以及高階宣告式運算元,且隨著MPP資料庫變得更加靈活和易於程式設計,兩者開始看起來相似了:最終,它們都只是儲存和處理資料的系統。 +批處理引擎正被用於分散式執行日益廣泛的各領域演算法。隨著批處理系統獲得各種內建功能以及高階宣告式運算元,且隨著 MPP 資料庫變得更加靈活和易於程式設計,兩者開始看起來相似了:最終,它們都只是儲存和處理資料的系統。 ## 本章小結 -在本章中,我們探索了批處理的主題。我們首先看到了諸如awk、grep和sort之類的Unix工具,然後我們看到了這些工具的設計理念是如何應用到MapReduce和更近的資料流引擎中的。一些設計原則包括:輸入是不可變的,輸出是為了作為另一個(仍未知的)程式的輸入,而複雜的問題是透過編寫“做好一件事”的小工具來解決的。 +在本章中,我們探索了批處理的主題。我們首先看到了諸如 awk、grep 和 sort 之類的 Unix 工具,然後我們看到了這些工具的設計理念是如何應用到 MapReduce 和更近的資料流引擎中的。一些設計原則包括:輸入是不可變的,輸出是為了作為另一個(仍未知的)程式的輸入,而複雜的問題是透過編寫 “做好一件事” 的小工具來解決的。 -在Unix世界中,允許程式與程式組合的統一介面是檔案與管道;在MapReduce中,該介面是一個分散式檔案系統。我們看到資料流引擎添加了自己的管道式資料傳輸機制,以避免將中間狀態物化至分散式檔案系統,但作業的初始輸入和最終輸出通常仍是HDFS。 +在 Unix 世界中,允許程式與程式組合的統一介面是檔案與管道;在 MapReduce 中,該介面是一個分散式檔案系統。我們看到資料流引擎添加了自己的管道式資料傳輸機制,以避免將中間狀態物化至分散式檔案系統,但作業的初始輸入和最終輸出通常仍是 HDFS。 分散式批處理框架需要解決的兩個主要問題是: * 分割槽 - 在MapReduce中,Mapper根據輸入檔案塊進行分割槽。Mapper的輸出被重新分割槽、排序併合併到可配置數量的Reducer分割槽中。這一過程的目的是把所有的**相關**資料(例如帶有相同鍵的所有記錄)都放在同一個地方。 + 在 MapReduce 中,Mapper 根據輸入檔案塊進行分割槽。Mapper 的輸出被重新分割槽、排序併合併到可配置數量的 Reducer 分割槽中。這一過程的目的是把所有的 **相關** 資料(例如帶有相同鍵的所有記錄)都放在同一個地方。 - 後MapReduce時代的資料流引擎若非必要會盡量避免排序,但它們也採取了大致類似的分割槽方法。 + 後 MapReduce 時代的資料流引擎若非必要會盡量避免排序,但它們也採取了大致類似的分割槽方法。 * 容錯 - MapReduce經常寫入磁碟,這使得從單個失敗的任務恢復很輕鬆,無需重新啟動整個作業,但在無故障的情況下減慢了執行速度。資料流引擎更多地將中間狀態儲存在記憶體中,更少地物化中間狀態,這意味著如果節點發生故障,則需要重算更多的資料。確定性運算元減少了需要重算的資料量。 + MapReduce 經常寫入磁碟,這使得從單個失敗的任務恢復很輕鬆,無需重新啟動整個作業,但在無故障的情況下減慢了執行速度。資料流引擎更多地將中間狀態儲存在記憶體中,更少地物化中間狀態,這意味著如果節點發生故障,則需要重算更多的資料。確定性運算元減少了需要重算的資料量。 -我們討論了幾種MapReduce的連線演算法,其中大多數也在MPP資料庫和資料流引擎內部使用。它們也很好地演示了分割槽演算法是如何工作的: +我們討論了幾種 MapReduce 的連線演算法,其中大多數也在 MPP 資料庫和資料流引擎內部使用。它們也很好地演示了分割槽演算法是如何工作的: * 排序合併連線 - 每個參與連線的輸入都透過一個提取連線鍵的Mapper。透過分割槽、排序和合並,具有相同鍵的所有記錄最終都會進入相同的Reducer呼叫。這個函式能輸出連線好的記錄。 + 每個參與連線的輸入都透過一個提取連線鍵的 Mapper。透過分割槽、排序和合並,具有相同鍵的所有記錄最終都會進入相同的 Reducer 呼叫。這個函式能輸出連線好的記錄。 * 廣播雜湊連線 - 兩個連線輸入之一很小,所以它並沒有分割槽,而且能被完全載入進一個雜湊表中。因此,你可以為連線輸入大端的每個分割槽啟動一個Mapper,將輸入小端的散列表載入到每個Mapper中,然後掃描大端,一次一條記錄,併為每條記錄查詢散列表。 + 兩個連線輸入之一很小,所以它並沒有分割槽,而且能被完全載入進一個雜湊表中。因此,你可以為連線輸入大端的每個分割槽啟動一個 Mapper,將輸入小端的散列表載入到每個 Mapper 中,然後掃描大端,一次一條記錄,併為每條記錄查詢散列表。 * 分割槽雜湊連線 如果兩個連線輸入以相同的方式分割槽(使用相同的鍵,相同的雜湊函式和相同數量的分割槽),則可以獨立地對每個分割槽應用散列表方法。 -分散式批處理引擎有一個刻意限制的程式設計模型:回撥函式(比如Mapper和Reducer)被假定是無狀態的,而且除了指定的輸出外,必須沒有任何外部可見的副作用。這一限制允許框架在其抽象下隱藏一些困難的分散式系統問題:當遇到崩潰和網路問題時,任務可以安全地重試,任何失敗任務的輸出都被丟棄。如果某個分割槽的多個任務成功,則其中只有一個能使其輸出實際可見。 +分散式批處理引擎有一個刻意限制的程式設計模型:回撥函式(比如 Mapper 和 Reducer)被假定是無狀態的,而且除了指定的輸出外,必須沒有任何外部可見的副作用。這一限制允許框架在其抽象下隱藏一些困難的分散式系統問題:當遇到崩潰和網路問題時,任務可以安全地重試,任何失敗任務的輸出都被丟棄。如果某個分割槽的多個任務成功,則其中只有一個能使其輸出實際可見。 得益於這個框架,你在批處理作業中的程式碼無需操心實現容錯機制:框架可以保證作業的最終輸出與沒有發生錯誤的情況相同,雖然實際上也許不得不重試各種任務。比起線上服務一邊處理使用者請求一邊將寫入資料庫作為處理請求的副作用,批處理提供的這種可靠性語義要強得多。 -批處理作業的顯著特點是,它讀取一些輸入資料併產生一些輸出資料,但不修改輸入—— 換句話說,輸出是從輸入衍生出的。最關鍵的是,輸入資料是**有界的(bounded)**:它有一個已知的,固定的大小(例如,它包含一些時間點的日誌檔案或資料庫內容的快照)。因為它是有界的,一個作業知道自己什麼時候完成了整個輸入的讀取,所以一個工作在做完後,最終總是會完成的。 +批處理作業的顯著特點是,它讀取一些輸入資料併產生一些輸出資料,但不修改輸入 —— 換句話說,輸出是從輸入衍生出的。最關鍵的是,輸入資料是 **有界的(bounded)**:它有一個已知的,固定的大小(例如,它包含一些時間點的日誌檔案或資料庫內容的快照)。因為它是有界的,一個作業知道自己什麼時候完成了整個輸入的讀取,所以一個工作在做完後,最終總是會完成的。 -在下一章中,我們將轉向流處理,其中的輸入是**無界的(unbounded)** —— 也就是說,你還有活兒要幹,然而它的輸入是永無止境的資料流。在這種情況下,作業永無完成之日。因為在任何時候都可能有更多的工作湧入。我們將看到,在某些方面上,流處理和批處理是相似的。但是關於無盡資料流的假設也對我們構建系統的方式產生了很多改變。 +在下一章中,我們將轉向流處理,其中的輸入是 **無界的(unbounded)** —— 也就是說,你還有活兒要幹,然而它的輸入是永無止境的資料流。在這種情況下,作業永無完成之日。因為在任何時候都可能有更多的工作湧入。我們將看到,在某些方面上,流處理和批處理是相似的。但是關於無盡資料流的假設也對我們構建系統的方式產生了很多改變。 ## 參考文獻 diff --git a/zh-tw/ch11.md b/zh-tw/ch11.md index 0af58ba..3a415b2 100644 --- a/zh-tw/ch11.md +++ b/zh-tw/ch11.md @@ -4,34 +4,34 @@ > 有效的複雜系統總是從簡單的系統演化而來。 反之亦然:從零設計的複雜系統沒一個能有效工作的。 > -> —— 約翰·加爾,Systemantics(1975) +> —— 約翰・加爾,Systemantics(1975) --------------- [TOC] -在[第十章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。 +在 [第十章](ch10.md) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **衍生資料(derived data)** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。 -然而,在[第十章](ch10.md)中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。 +然而,在 [第十章](ch10.md) 中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。 -實際上,很多資料是**無界限**的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式“完成”【1】。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。 +實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”【1】。因此,批處理程式必須將資料人為地分成固定時間段的資料塊,例如,在每天結束時處理一天的資料,或者在每小時結束時處理一小時的資料。 -日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是**流處理(stream processing)** 背後的想法。 +日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是 **流處理(stream processing)** 背後的想法。 -一般來說,“流”是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix的stdin和stdout,程式語言(惰性列表)【2】,檔案系統API(如Java的`FileInputStream`),TCP連線,透過網際網路傳送音訊和影片等等。 +一般來說,“流” 是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix 的 stdin 和 stdout,程式語言(惰性列表)【2】,檔案系統 API(如 Java 的 `FileInputStream`),TCP 連線,透過網際網路傳送音訊和影片等等。 -在本章中,我們將把**事件流(event stream)** 視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在“[資料庫與流](#資料庫與流)”中,我們將研究流和資料庫之間的關係。最後在“[流處理](#流處理)”中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。 +在本章中,我們將把 **事件流(event stream)** 視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在 “[資料庫與流](#資料庫與流)” 中,我們將研究流和資料庫之間的關係。最後在 “[流處理](#流處理)” 中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。 ## 傳遞事件流 在批處理領域,作業的輸入和輸出是檔案(也許在分散式檔案系統上)。流處理領域中的等價物看上去是什麼樣子的? -當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱“[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。 +當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱 “[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。 -例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或CPU利用率的週期性測量。在“[使用Unix工具的批處理](ch10.md#使用Unix工具的批處理)”的示例中,Web伺服器日誌的每一行都是一個事件。 +例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或 CPU 利用率的週期性測量。在 “[使用 Unix 工具的批處理](ch10.md#使用Unix工具的批處理)” 的示例中,Web 伺服器日誌的每一行都是一個事件。 -事件可能被編碼為文字字串或JSON,或者某種二進位制編碼,如[第四章](ch4.md)所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。 +事件可能被編碼為文字字串或 JSON,或者某種二進位制編碼,如 [第四章](ch4.md) 所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。 在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者(producer)** (也稱為 **釋出者(publisher)** 或 **傳送者(sender)** )生成一次,然後可能由多個 **消費者(consumer)** ( **訂閱者(subscribers)** 或 **接收者(recipients)** )進行處理【3】。在檔案系統中,檔名標識一組相關記錄;在流式系統中,相關的事件通常被聚合為一個 **主題(topic)** 或 **流(stream)** 。 @@ -44,30 +44,30 @@ ### 訊息傳遞系統 -向消費者通知新事件的常用方式是使用**訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中談到了這些系統,但現在我們將詳細介紹這些系統。 +向消費者通知新事件的常用方式是使用 **訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中談到了這些系統,但現在我們將詳細介紹這些系統。 -像生產者和消費者之間的Unix管道或TCP連線這樣的直接通道,是實現訊息傳遞系統的簡單方法。但是,大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是,Unix管道和TCP將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。 +像生產者和消費者之間的 Unix 管道或 TCP 連線這樣的直接通道,是實現訊息傳遞系統的簡單方法。但是,大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是,Unix 管道和 TCP 將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。 -在這個**釋出/訂閱**模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助: +在這個 **釋出 / 訂閱** 模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助: -1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓**(backpressure,也稱為**流量控制**,即flow control:阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。 +1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**(backpressure,也稱為 **流量控制**,即 flow control:阻塞生產者,以免其傳送更多的訊息)。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。 如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能【6】? -2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(請參閱“[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。 +2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。 是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。 -我們在[第十章](ch10.md)中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。 +我們在 [第十章](ch10.md) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。 #### 直接從生產者傳遞給消費者 許多訊息傳遞系統使用生產者和消費者之間的直接網路通訊,而不透過中間節點: -* 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發出請求。 +* 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 發出請求。 儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。 @@ -75,54 +75,54 @@ #### 訊息代理 -一種廣泛使用的替代方法是透過**訊息代理**(message broker,也稱為**訊息佇列**,即message queue)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。 +一種廣泛使用的替代方法是透過 **訊息代理**(message broker,也稱為 **訊息佇列**,即 message queue)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。 透過將資料集中在代理上,這些系統可以更容易地容忍來來去去的客戶端(連線,斷開連線和崩潰),而永續性問題則轉移到代理的身上。一些訊息代理只將訊息儲存在記憶體中,而另一些訊息代理(取決於配置)將其寫入磁碟,以便在代理崩潰的情況下不會丟失。針對緩慢的消費者,它們通常會允許無上限的排隊(而不是丟棄訊息或背壓),儘管這種選擇也可能取決於配置。 -排隊的結果是,消費者通常是**非同步(asynchronous)** 的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。 +排隊的結果是,消費者通常是 **非同步(asynchronous)** 的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。 #### 訊息代理與資料庫的對比 -有些訊息代理甚至可以使用XA或JTA參與兩階段提交協議(請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異: +有些訊息代理甚至可以使用 XA 或 JTA 參與兩階段提交協議(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異: * 資料庫通常保留資料直至顯式刪除,而大多數訊息代理在訊息成功遞送給消費者時會自動刪除訊息。這樣的訊息代理不適合長期的資料儲存。 -* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小—— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化【6】。 +* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小 —— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化【6】。 * 資料庫通常支援次級索引和各種搜尋資料的方式,而訊息代理通常支援按照某種模式匹配主題,訂閱其子集。雖然機制並不一樣,但對於客戶端選擇想要了解的資料的一部分,都是基本的方式。 * 查詢資料庫時,結果通常基於某個時間點的資料快照;如果另一個客戶端隨後向資料庫寫入一些改變了查詢結果的內容,則第一個客戶端不會發現其先前結果現已過期(除非它重複查詢或輪詢變更)。相比之下,訊息代理不支援任意查詢,但是當資料發生變化時(即新訊息可用時),它們會通知客戶端。 -這是關於訊息代理的傳統觀點,它被封裝在諸如JMS 【14】和AMQP 【15】的標準中,並且被諸如RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO企業訊息服務、IBM MQ、Azure Service Bus和Google Cloud Pub/Sub所實現 【16】。 +這是關於訊息代理的傳統觀點,它被封裝在諸如 JMS 【14】和 AMQP 【15】的標準中,並且被諸如 RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO 企業訊息服務、IBM MQ、Azure Service Bus 和 Google Cloud Pub/Sub 所實現 【16】。 #### 多個消費者 -當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如[圖11-1](../img/fig11-1.png)所示: +當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](../img/fig11-1.png) 所示: * 負載均衡(load balancing) - 每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱**,即shared subscription)。 + 每條訊息都被傳遞給消費者 **之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在 AMQP 中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在 JMS 中則稱之為 **共享訂閱**,即 shared subscription)。 * 扇出(fan-out) - 每條訊息都被傳遞給**所有**消費者。扇出允許幾個獨立的消費者各自“收聽”相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS中的主題訂閱與AMQP中的交叉繫結提供了這一功能)。 + 每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。 ![](../img/fig11-1.png) -**圖11-1 (a)負載平衡:在消費者間共享消費主題;(b)扇出:將每條訊息傳遞給多個消費者。** +**圖 11-1 (a)負載平衡:在消費者間共享消費主題;(b)扇出:將每條訊息傳遞給多個消費者。** 兩種模式可以組合使用:例如,兩個獨立的消費者組可以每組各訂閱同一個主題,每一組都共同收到所有訊息,但在每一組內部,每條訊息僅由單個節點處理。 #### 確認與重新傳遞 -消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用**確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。 +消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。 -如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。 (請注意可能發生這樣的情況,訊息**實際上是**處理完畢的,但**確認**在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”中所討論的那樣) +如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。 (請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)” 中所討論的那樣) -當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖11-2](../img/fig11-2.png)中,消費者通常按照生產者傳送的順序處理訊息。然而消費者2在處理訊息m3時崩潰,與此同時消費者1正在處理訊息m4。未確認的訊息m3隨後被重新發送給消費者1,結果消費者1按照m4,m3,m5的順序處理訊息。因此m3和m4的交付順序與生產者1的傳送順序不同。 +當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在 [圖 11-2](../img/fig11-2.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1,結果消費者 1 按照 m4,m3,m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。 ![](../img/fig11-2.png) -**圖11-2 在處理m3時消費者2崩潰,因此稍後重傳至消費者1** +**圖 11-2 在處理 m3 時消費者 2 崩潰,因此稍後重傳至消費者 1** -即使訊息代理試圖保留訊息的順序(如JMS和AMQP標準所要求的),負載均衡與重傳的組合也不可避免地導致訊息被重新排序。為避免此問題,你可以讓每個消費者使用單獨的佇列(即不使用負載均衡功能)。如果訊息是完全獨立的,則訊息順序重排並不是一個問題。但正如我們將在本章後續部分所述,如果訊息之間存在因果依賴關係,這就是一個很重要的問題。 +即使訊息代理試圖保留訊息的順序(如 JMS 和 AMQP 標準所要求的),負載均衡與重傳的組合也不可避免地導致訊息被重新排序。為避免此問題,你可以讓每個消費者使用單獨的佇列(即不使用負載均衡功能)。如果訊息是完全獨立的,則訊息順序重排並不是一個問題。但正如我們將在本章後續部分所述,如果訊息之間存在因果依賴關係,這就是一個很重要的問題。 ### 分割槽日誌 @@ -130,38 +130,38 @@ 資料庫和檔案系統採用截然相反的方法論:至少在某人顯式刪除前,通常寫入資料庫或檔案的所有內容都要被永久記錄下來。 -這種思維方式上的差異對建立衍生資料的方式有巨大影響。如[第十章](ch10.md)所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。 +這種思維方式上的差異對建立衍生資料的方式有巨大影響。如 [第十章](ch10.md) 所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS 風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。 如果你將新的消費者新增到訊息傳遞系統,通常只能接收到消費者註冊之後開始傳送的訊息。先前的任何訊息都隨風而逝,一去不復返。作為對比,你可以隨時為檔案和資料庫新增新的客戶端,且能讀取任意久遠的資料(只要應用沒有顯式覆蓋或刪除這些資料)。 -為什麼我們不能把它倆雜交一下,既有資料庫的持久儲存方式,又有訊息傳遞的低延遲通知?這就是**基於日誌的訊息代理(log-based message brokers)** 背後的想法。 +為什麼我們不能把它倆雜交一下,既有資料庫的持久儲存方式,又有訊息傳遞的低延遲通知?這就是 **基於日誌的訊息代理(log-based message brokers)** 背後的想法。 #### 使用日誌進行訊息儲存 -日誌只是磁碟上簡單的僅追加記錄序列。我們先前在[第三章](ch3.md)中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在[第五章](ch5.md)複製的上下文裡也討論了它。 +日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第三章](ch3.md) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第五章](ch5.md) 複製的上下文裡也討論了它。 -同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix工具`tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。 +同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix 工具 `tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。 -為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(按[第六章](ch6.md)的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。 +為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第六章](ch6.md) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 11-3](../img/fig11-3.png) 所示。 -在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或**偏移量**(offset,在[圖11-3](../img/fig11-3.png)中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。 +在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或 **偏移量**(offset,在 [圖 11-3](../img/fig11-3.png) 中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。 ![](../img/fig11-3.png) -**圖11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案** +**圖 11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案** -Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的DistributedLog 【20,21】都是基於日誌的訊息代理。 Google Cloud Pub/Sub在架構上類似,但對外暴露的是JMS風格的API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。 +Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 DistributedLog 【20,21】都是基於日誌的訊息代理。 Google Cloud Pub/Sub 在架構上類似,但對外暴露的是 JMS 風格的 API,而不是日誌抽象【16】。儘管這些訊息代理將所有訊息寫入磁碟,但透過跨多臺機器分割槽,每秒能夠實現數百萬條訊息的吞吐量,並透過複製訊息來實現容錯性【22,23】。 #### 日誌與傳統的訊息傳遞相比 基於日誌的方法天然支援扇出式訊息傳遞,因為多個消費者可以獨立讀取日誌,而不會相互影響 —— 讀取訊息不會將其從日誌中刪除。為了在一組消費者之間實現負載平衡,代理可以將整個分割槽分配給消費者組中的節點,而不是將單條訊息分配給消費者客戶端。 -然後每個客戶端將消費被指派分割槽中的**所有**訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點: +然後每個客戶端將消費被指派分割槽中的 **所有** 訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點: -* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點[^i]。 -* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱“[描述效能](ch1.md#描述效能)”)。 +* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點 [^i]。 +* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](ch1.md#描述效能)”)。 -因此在訊息處理代價高昂,希望逐條並行處理,以及訊息的順序並沒有那麼重要的情況下,JMS/AMQP風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。 +因此在訊息處理代價高昂,希望逐條並行處理,以及訊息的順序並沒有那麼重要的情況下,JMS/AMQP 風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。 [^i]: 要設計一種負載均衡方案也是有可能的,在這種方案中,兩個消費者透過讀取全部訊息來共享分割槽處理的工作,但是其中一個只考慮具有偶數偏移量的訊息,而另一個消費者只處理奇數編號的偏移量。或者你可以將訊息攤到一個執行緒池中來處理,但這種方法會使消費者偏移量管理變得複雜。一般來說,單執行緒處理單分割槽是合適的,可以透過增加更多分割槽來提高並行度。 @@ -169,7 +169,7 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 順序消費一個分割槽使得判斷訊息是否已經被處理變得相當容易:所有偏移量小於消費者的當前偏移量的訊息已經被處理,而具有更大偏移量的訊息還沒有被看到。因此,代理不需要跟蹤確認每條訊息,只需要定期記錄消費者的偏移即可。這種方法減少了額外簿記開銷,而且在批處理和流處理中採用這種方法有助於提高基於日誌的系統的吞吐量。 -實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在“[設定新從庫](ch5.md#設定新從庫)”中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。 +實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在 “[設定新從庫](ch5.md#設定新從庫)” 中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。 如果消費者節點失效,則失效消費者的分割槽將指派給其他節點,並從最後記錄的偏移量開始消費訊息。如果消費者已經處理了後續的訊息,但還沒有記錄它們的偏移量,那麼重啟後這些訊息將被處理兩次。我們將在本章後面討論這個問題的處理方法。 @@ -177,25 +177,25 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 如果只追加寫入日誌,則磁碟空間終究會耗盡。為了回收磁碟空間,日誌實際上被分割成段,並不時地將舊段刪除或移動到歸檔儲存。 (我們將在後面討論一種更為複雜的磁碟空間釋放方式) -這就意味著如果一個慢消費者跟不上訊息產生的速率而落後得太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為**迴圈緩衝區(circular buffer)** 或**環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此緩衝區可能相當的大。 +這就意味著如果一個慢消費者跟不上訊息產生的速率而落後得太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為 **迴圈緩衝區(circular buffer)** 或 **環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此緩衝區可能相當的大。 -讓我們做個簡單計算。在撰寫本文時,典型的大型硬碟容量為6TB,順序寫入吞吐量為150MB/s。如果以最快的速度寫訊息,則需要大約11個小時才能填滿磁碟。因而磁碟可以緩衝11個小時的訊息,之後它將開始覆蓋舊的訊息。即使使用多個磁碟和機器,這個比率也是一樣的。實踐中的部署很少能用滿磁碟的寫入頻寬,所以通常可以儲存一個幾天甚至幾周的日誌緩衝區。 +讓我們做個簡單計算。在撰寫本文時,典型的大型硬碟容量為 6TB,順序寫入吞吐量為 150MB/s。如果以最快的速度寫訊息,則需要大約 11 個小時才能填滿磁碟。因而磁碟可以緩衝 11 個小時的訊息,之後它將開始覆蓋舊的訊息。即使使用多個磁碟和機器,這個比率也是一樣的。實踐中的部署很少能用滿磁碟的寫入頻寬,所以通常可以儲存一個幾天甚至幾周的日誌緩衝區。 不管保留多長時間的訊息,日誌的吞吐量或多或少保持不變,因為無論如何,每個訊息都會被寫入磁碟【18】。這種行為與預設將訊息儲存在記憶體中,僅當佇列太長時才寫入磁碟的訊息傳遞系統形成鮮明對比。當佇列很短時,這些系統非常快;而當這些系統開始寫入磁碟時,就要慢的多,所以吞吐量取決於保留的歷史數量。 #### 當消費者跟不上生產者時 -在“[訊息傳遞系統](#訊息傳遞系統)”中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大但大小固定的緩衝區(受可用磁碟空間的限制)。 +在 “[訊息傳遞系統](#訊息傳遞系統)” 中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大但大小固定的緩衝區(受可用磁碟空間的限制)。 如果消費者遠遠落後,而所要求的資訊比保留在磁碟上的資訊還要舊,那麼它將不能讀取這些資訊,所以代理實際上丟棄了比緩衝區容量更大的舊資訊。你可以監控消費者落後日誌頭部的距離,如果落後太多就發出報警。由於緩衝區很大,因而有足夠的時間讓運維人員來修復慢消費者,並在訊息開始丟失之前讓其趕上。 即使消費者真的落後太多開始丟失訊息,也只有那個消費者受到影響;它不會中斷其他消費者的服務。這是一個巨大的運維優勢:你可以實驗性地消費生產日誌,以進行開發,測試或除錯,而不必擔心會中斷生產服務。當消費者關閉或崩潰時,會停止消耗資源,唯一剩下的只有消費者偏移量。 -這種行為也與傳統的訊息代理形成了鮮明對比,在那種情況下,你需要小心地刪除那些消費者已經關閉的佇列—— 否則那些佇列就會累積不必要的訊息,從其他仍活躍的消費者那裡佔走記憶體。 +這種行為也與傳統的訊息代理形成了鮮明對比,在那種情況下,你需要小心地刪除那些消費者已經關閉的佇列 —— 否則那些佇列就會累積不必要的訊息,從其他仍活躍的消費者那裡佔走記憶體。 #### 重播舊訊息 -我們之前提到,使用AMQP和JMS風格的訊息代理,處理和確認訊息是一個破壞性的操作,因為它會導致訊息在代理上被刪除。另一方面,在基於日誌的訊息代理中,使用訊息更像是從檔案中讀取資料:這是隻讀操作,不會更改日誌。 +我們之前提到,使用 AMQP 和 JMS 風格的訊息代理,處理和確認訊息是一個破壞性的操作,因為它會導致訊息在代理上被刪除。另一方面,在基於日誌的訊息代理中,使用訊息更像是從檔案中讀取資料:這是隻讀操作,不會更改日誌。 除了消費者的任何輸出之外,處理的唯一副作用是消費者偏移量的前進。但偏移量是在消費者的控制之下的,所以如果需要的話可以很容易地操縱:例如你可以用昨天的偏移量跑一個消費者副本,並將輸出寫到不同的位置,以便重新處理最近一天的訊息。你可以使用各種不同的處理程式碼重複任意次。 @@ -206,158 +206,158 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 我們已經在訊息代理和資料庫之間進行了一些比較。儘管傳統上它們被視為單獨的工具類別,但是我們看到基於日誌的訊息代理已經成功地從資料庫中獲取靈感並將其應用於訊息傳遞。我們也可以反過來:從訊息傳遞和流中獲取靈感,並將它們應用於資料庫。 -我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是**寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。 +我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是 **寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。 -事實上,複製日誌(請參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。 +事實上,複製日誌(請參閱 “[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。 -我們還在“[全序廣播](ch9.md#全序廣播)”中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景! +我們還在 “[全序廣播](ch9.md#全序廣播)” 中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景! 在本節中,我們將首先看看異構資料系統中出現的一個問題,然後探討如何透過將事件流的想法帶入資料庫來解決這個問題。 ### 保持系統同步 -正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用OLTP資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。 +正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用 OLTP 資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。 -由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由ETL程序執行(請參閱“[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。 +由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。 -如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是**雙寫(dual write)**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。 +如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是 **雙寫(dual write)**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。 -但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如[圖11-4](../img/fig11-4.png)所示。在這個例子中,兩個客戶端同時想要更新一個專案X:客戶端1想要將值設定為A,客戶端2想要將其設定為B。兩個客戶端首先將新值寫入資料庫,然後將其寫入到搜尋索引。因為運氣不好,這些請求的時序是交錯的:資料庫首先看到來自客戶端1的寫入將值設定為A,然後來自客戶端2的寫入將值設定為B,因此資料庫中的最終值為B。搜尋索引首先看到來自客戶端2的寫入,然後是客戶端1的寫入,所以搜尋索引中的最終值是A。即使沒發生錯誤,這兩個系統現在也永久地不一致了。 +但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如 [圖 11-4](../img/fig11-4.png) 所示。在這個例子中,兩個客戶端同時想要更新一個專案 X:客戶端 1 想要將值設定為 A,客戶端 2 想要將其設定為 B。兩個客戶端首先將新值寫入資料庫,然後將其寫入到搜尋索引。因為運氣不好,這些請求的時序是交錯的:資料庫首先看到來自客戶端 1 的寫入將值設定為 A,然後來自客戶端 2 的寫入將值設定為 B,因此資料庫中的最終值為 B。搜尋索引首先看到來自客戶端 2 的寫入,然後是客戶端 1 的寫入,所以搜尋索引中的最終值是 A。即使沒發生錯誤,這兩個系統現在也永久地不一致了。 ![](../img/fig11-4.png) -**圖11-4 在資料庫中X首先被設定為A,然後被設定為B,而在搜尋索引處,寫入以相反的順序到達** +**圖 11-4 在資料庫中 X 首先被設定為 A,然後被設定為 B,而在搜尋索引處,寫入以相反的順序到達** -除非有一些額外的併發檢測機制,例如我們在“[檢測併發寫入](ch5.md#檢測併發寫入)”中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。 +除非有一些額外的併發檢測機制,例如我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。 -雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱“[原子提交與兩階段提交](ch7.md#原子提交與兩階段提交)”)。 +雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱 “[原子提交與兩階段提交](ch7.md#原子提交與兩階段提交)”)。 -如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱“[多主複製](ch5.md#多主複製)“)。 +如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在 [圖 11-4](../img/fig11-4.png) 中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱 “[多主複製](ch5.md#多主複製)“)。 如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎? ### 變更資料捕獲 -大多數資料庫的複製日誌的問題在於,它們一直被當做資料庫的內部實現細節,而不是公開的API。客戶端應該透過其資料模型和查詢語言來查詢資料庫,而不是解析複製日誌並嘗試從中提取資料。 +大多數資料庫的複製日誌的問題在於,它們一直被當做資料庫的內部實現細節,而不是公開的 API。客戶端應該透過其資料模型和查詢語言來查詢資料庫,而不是解析複製日誌並嘗試從中提取資料。 數十年來,許多資料庫根本沒有記錄在檔的獲取變更日誌的方式。由於這個原因,捕獲資料庫中所有的變更,然後將其複製到其他儲存技術(搜尋索引、快取或資料倉庫)中是相當困難的。 -最近,人們對**變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。 CDC是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。 +最近,人們對 **變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。 CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。 -例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如[圖11-5](../img/fig11-5.png)所示。 +例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如 [圖 11-5](../img/fig11-5.png) 所示。 ![](../img/fig11-5.png) -**圖11-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統** +**圖 11-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統** #### 變更資料捕獲的實現 -我們可以將日誌消費者叫做**衍生資料系統**,正如在[第三部分](part-iii.md)的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是**記錄系統**資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。 +我們可以將日誌消費者叫做 **衍生資料系統**,正如在 [第三部分](part-iii.md) 的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是 **記錄系統** 資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。 -從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了[圖11-2](../img/fig11-2.png)的重新排序問題)。 +從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了 [圖 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】。 +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#複製延遲問題)”)。 #### 初始快照 -如果你擁有**所有**對資料庫進行變更的日誌,則可以透過重播該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重播過於費時,因此日誌需要被截斷。 +如果你擁有 **所有** 對資料庫進行變更的日誌,則可以透過重播該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重播過於費時,因此日誌需要被截斷。 -例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的“[設定新從庫](ch5.md#設定新從庫)”中所述。 +例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的 “[設定新從庫](ch5.md#設定新從庫)” 中所述。 -資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些CDC工具集成了這種快照功能,而其他工具則把它留給你手動執行。 +資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些 CDC 工具集成了這種快照功能,而其他工具則把它留給你手動執行。 #### 日誌壓縮 -如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但**日誌壓縮(log compaction)** 提供了一個很好的備選方案。 +如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但 **日誌壓縮(log compaction)** 提供了一個很好的備選方案。 -我們之前在“[雜湊索引](ch3.md#雜湊索引)”中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。 +我們之前在 “[雜湊索引](ch3.md#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](../img/fig3-2.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。 -在日誌結構儲存引擎中,具有特殊值NULL(**墓碑**,即tombstone)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。 +在日誌結構儲存引擎中,具有特殊值 NULL(**墓碑**,即 tombstone)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。 -在基於日誌的訊息代理與變更資料捕獲的上下文中也適用相同的想法。如果CDC系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。 +在基於日誌的訊息代理與變更資料捕獲的上下文中也適用相同的想法。如果 CDC 系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。 -現在,無論何時需要重建衍生資料系統(如搜尋索引),你可以從壓縮日誌主題的零偏移量處啟動新的消費者,然後依次掃描日誌中的所有訊息。日誌能保證包含資料庫中每個鍵的最新值(也可能是一些較舊的值)—— 換句話說,你可以使用它來獲取資料庫內容的完整副本,而無需從CDC源資料庫取一個快照。 +現在,無論何時需要重建衍生資料系統(如搜尋索引),你可以從壓縮日誌主題的零偏移量處啟動新的消費者,然後依次掃描日誌中的所有訊息。日誌能保證包含資料庫中每個鍵的最新值(也可能是一些較舊的值)—— 換句話說,你可以使用它來獲取資料庫內容的完整副本,而無需從 CDC 源資料庫取一個快照。 -Apache Kafka支援這種日誌壓縮功能。正如我們將在本章後面看到的,它允許訊息代理被當成永續性儲存使用,而不僅僅是用於臨時訊息。 +Apache Kafka 支援這種日誌壓縮功能。正如我們將在本章後面看到的,它允許訊息代理被當成永續性儲存使用,而不僅僅是用於臨時訊息。 #### 變更流的API支援 -越來越多的資料庫開始將變更流作為第一等的介面,而不像傳統上要去做加裝改造,或者費工夫逆向工程一個CDC。例如,RethinkDB允許查詢訂閱通知,當查詢結果變更時獲得通知【36】,Firebase 【37】和CouchDB 【38】基於變更流進行同步,該變更流同樣可用於應用。而Meteor使用MongoDB oplog訂閱資料變更,並改變了使用者介面【39】。 +越來越多的資料庫開始將變更流作為第一等的介面,而不像傳統上要去做加裝改造,或者費工夫逆向工程一個 CDC。例如,RethinkDB 允許查詢訂閱通知,當查詢結果變更時獲得通知【36】,Firebase 【37】和 CouchDB 【38】基於變更流進行同步,該變更流同樣可用於應用。而 Meteor 使用 MongoDB oplog 訂閱資料變更,並改變了使用者介面【39】。 -VoltDB允許事務以流的形式連續地從資料庫中匯出資料【40】。資料庫將關係資料模型中的輸出流表示為一個表,事務可以向其中插入元組,但不能查詢。已提交事務按照提交順序寫入這個特殊表,而流則由該表中的元組日誌構成。外部消費者可以非同步消費該日誌,並使用它來更新衍生資料系統。 +VoltDB 允許事務以流的形式連續地從資料庫中匯出資料【40】。資料庫將關係資料模型中的輸出流表示為一個表,事務可以向其中插入元組,但不能查詢。已提交事務按照提交順序寫入這個特殊表,而流則由該表中的元組日誌構成。外部消費者可以非同步消費該日誌,並使用它來更新衍生資料系統。 -Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲工具與Kafka整合。一旦變更事件進入Kafka中,它就可以用於更新衍生資料系統,比如搜尋索引,也可以用於本章稍後討論的流處理系統。 +Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲工具與 Kafka 整合。一旦變更事件進入 Kafka 中,它就可以用於更新衍生資料系統,比如搜尋索引,也可以用於本章稍後討論的流處理系統。 ### 事件溯源 -我們在這裡討論的想法和**事件溯源(Event Sourcing)** 之間有一些相似之處,這是一個在 **領域驅動設計(domain-driven design, DDD)** 社群中折騰出來的技術。我們將簡要討論事件溯源,因為它包含了一些關於流處理系統的有用想法。 +我們在這裡討論的想法和 **事件溯源(Event Sourcing)** 之間有一些相似之處,這是一個在 **領域驅動設計(domain-driven design, DDD)** 社群中折騰出來的技術。我們將簡要討論事件溯源,因為它包含了一些關於流處理系統的有用想法。 -與變更資料捕獲類似,事件溯源涉及到**將所有對應用狀態的變更**儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上: +與變更資料捕獲類似,事件溯源涉及到 **將所有對應用狀態的變更** 儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上: -* 在變更資料捕獲中,應用以**可變方式(mutable way)** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。 +* 在變更資料捕獲中,應用以 **可變方式(mutable way)** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免 [圖 11-4](../img/fig11-4.png) 中的競態條件。寫入資料庫的應用不需要知道 CDC 的存在。 * 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。 -事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用Bug(請參閱“[不可變事件的優點](#不可變事件的優點)”)。 +事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用 Bug(請參閱 “[不可變事件的優點](#不可變事件的優點)”)。 -例如,儲存“學生取消選課”事件以中性的方式清楚地表達了單個行為的意圖,而其副作用“從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表“則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如“將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。 +例如,儲存 “學生取消選課” 事件以中性的方式清楚地表達了單個行為的意圖,而其副作用 “從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表 “則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如 “將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。 -事件溯源類似於**編年史(chronicle)** 資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 +事件溯源類似於 **編年史(chronicle)** 資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 -諸如Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。 +諸如 Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。 #### 從事件日誌中派生出當前狀態 事件日誌本身並不是很有用,因為使用者通常期望看到的是系統的當前狀態,而不是變更歷史。例如,在購物網站上,使用者期望能看到他們購物車裡的當前內容,而不是他們購物車所有變更的一個僅追加列表。 -因此,使用事件溯源的應用需要拉取事件日誌(表示**寫入**系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統**讀取**資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中衍生出相同的應用狀態。 +因此,使用事件溯源的應用需要拉取事件日誌(表示 **寫入** 系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統 **讀取** 資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中衍生出相同的應用狀態。 與變更資料捕獲一樣,重播事件日誌允許讓你重新構建系統的當前狀態。不過,日誌壓縮需要採用不同的方式處理: -* 用於記錄更新的CDC事件通常包含記錄的**完整新版本**,因此主鍵的當前值完全由該主鍵的最近事件確定,而日誌壓縮可以丟棄相同主鍵的先前事件。 +* 用於記錄更新的 CDC 事件通常包含記錄的 **完整新版本**,因此主鍵的當前值完全由該主鍵的最近事件確定,而日誌壓縮可以丟棄相同主鍵的先前事件。 * 另一方面,事件溯源在更高層次進行建模:事件通常表示使用者操作的意圖,而不是因為操作而發生的狀態更新機制。在這種情況下,後面的事件通常不會覆蓋先前的事件,所以你需要完整的歷史事件來重新構建最終狀態。這裡進行同樣的日誌壓縮是不可能的。 -使用事件溯源的應用通常有一些機制,用於儲存從事件日誌中匯出的當前狀態快照,因此它們不需要重複處理完整的日誌。然而這只是一種效能最佳化,用來加速讀取,提高從崩潰中恢復的速度;真正的目的是系統能夠永久儲存所有原始事件,並在需要時重新處理完整的事件日誌。我們將在“[不變性的侷限性](#不變性的侷限性)”中討論這個假設。 +使用事件溯源的應用通常有一些機制,用於儲存從事件日誌中匯出的當前狀態快照,因此它們不需要重複處理完整的日誌。然而這只是一種效能最佳化,用來加速讀取,提高從崩潰中恢復的速度;真正的目的是系統能夠永久儲存所有原始事件,並在需要時重新處理完整的事件日誌。我們將在 “[不變性的侷限性](#不變性的侷限性)” 中討論這個假設。 #### 命令和事件 -事件溯源的哲學是仔細區分**事件(event)** 和**命令(command)**【48】。當來自使用者的請求剛到達時,它一開始是一個命令:在這個時間點上它仍然可能可能失敗,比如,因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受,則它變為一個持久化且不可變的事件。 +事件溯源的哲學是仔細區分 **事件(event)** 和 **命令(command)**【48】。當來自使用者的請求剛到達時,它一開始是一個命令:在這個時間點上它仍然可能可能失敗,比如,因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受,則它變為一個持久化且不可變的事件。 -例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在“[容錯共識](ch8.md#容錯共識)”中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者ID註冊的,或者座位已經預留給特定的顧客。 +例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](ch8.md#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。 -在事件生成的時刻,它就成為了**事實(fact)**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。 +在事件生成的時刻,它就成為了 **事實(fact)**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。 事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。 -或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。 +或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。 ### 狀態、流和不變性 -我們在[第十章](ch10.md)中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。 +我們在 [第十章](ch10.md) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。 我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢? -只要你的狀態發生了變化,那麼這個狀態就是這段時間中事件修改的結果。例如,當前可用的座位列表是你已處理的預訂所產生的結果,當前帳戶餘額是帳戶中的借與貸的結果,而Web伺服器的響應時間圖,是所有已發生Web請求的獨立響應時間的聚合結果。 +只要你的狀態發生了變化,那麼這個狀態就是這段時間中事件修改的結果。例如,當前可用的座位列表是你已處理的預訂所產生的結果,當前帳戶餘額是帳戶中的借與貸的結果,而 Web 伺服器的響應時間圖,是所有已發生 Web 請求的獨立響應時間的聚合結果。 -無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌—— **變化日誌(changelog)**,表示了隨時間演變的狀態。 +無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌 —— **變化日誌(changelog)**,表示了隨時間演變的狀態。 -如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如[圖11-6](../img/fig11-6.png)所示【49,50,51】。這個比喻有一些侷限性(例如,狀態的二階導似乎沒有意義),但這是考慮資料的一個實用出發點。 +如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如 [圖 11-6](../img/fig11-6.png) 所示【49,50,51】。這個比喻有一些侷限性(例如,狀態的二階導似乎沒有意義),但這是考慮資料的一個實用出發點。 $$ state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\ stream(t) = \frac{d\ state(t)}{dt} $$ ![](../img/fig11-6.png) -**圖11-6 應用當前狀態與事件流之間的關係** +**圖 11-6 應用當前狀態與事件流之間的關係** -如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你認為事件日誌是你的記錄系統,而所有的衍生狀態都從它派生而來,那麼系統中的資料流動就容易理解的多。正如帕特·赫蘭(Pat Helland)所說的【52】: +如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你認為事件日誌是你的記錄系統,而所有的衍生狀態都從它派生而來,那麼系統中的資料流動就容易理解的多。正如帕特・赫蘭(Pat Helland)所說的【52】: > 事務日誌記錄了資料庫的所有變更。高速追加是更改日誌的唯一方法。從這個角度來看,資料庫的內容其實是日誌中記錄最新值的快取。日誌才是真相,資料庫是日誌子集的快取,這一快取子集恰好來自日誌中每條記錄與索引值的最新值。 -日誌壓縮(如“[日誌壓縮](#日誌壓縮)”中所述)是連線日誌與資料庫狀態之間的橋樑:它只保留每條記錄的最新版本,並丟棄被覆蓋的版本。 +日誌壓縮(如 “[日誌壓縮](#日誌壓縮)” 中所述)是連線日誌與資料庫狀態之間的橋樑:它只保留每條記錄的最新版本,並丟棄被覆蓋的版本。 #### 不可變事件的優點 @@ -365,43 +365,43 @@ $$ 如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。 -儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。 +儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。 不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上,顧客可以將物品新增到他們的購物車,然後再將其移除。雖然從履行訂單的角度,第二個事件取消了第一個事件,但對分析目的而言,知道客戶考慮過某個特定項而之後又反悔,可能是很有用的。也許他們會選擇在未來購買,或者他們已經找到了替代品。這個資訊被記錄在事件日誌中,但對於移出購物車就刪除記錄的資料庫而言,這個資訊在移出購物車時可能就丟失了【42】。 #### 從同一事件日誌中派生多個檢視 -此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖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】。 -如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱[第三章](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#描述負載)”中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是**針對讀取最佳化的狀態**的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。 +在 “[描述負載](ch1.md#描述負載)” 中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是 **針對讀取最佳化的狀態** 的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。 #### 併發控制 -事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在“[讀己之寫](ch5.md#讀己之寫)”中討論了這個問題以及可能的解決方案。 +事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](ch5.md#讀己之寫)” 中討論了這個問題以及可能的解決方案。 -一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要**事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中討論的方法。 +一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中討論的方法。 -另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。 +另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。 -如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽3中的客戶事件只需要更新分割槽3中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱“[真的序列執行](ch7.md#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在[第十二章](ch12.md)討論。 +如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽 3 中的客戶事件只需要更新分割槽 3 中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱 “[真的序列執行](ch7.md#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在 [第十二章](ch12.md) 討論。 #### 不變性的侷限性 -許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱“[索引和快照隔離](ch7.md#索引和快照隔離)” )。 Git,Mercurial和Fossil等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。 +許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](ch7.md#索引和快照隔離)” )。 Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。 -永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新/刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。 +永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率;在這些情況下,不可變的歷史可能增至難以接受的巨大,碎片化可能成為一個問題,壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。 除了效能方面的原因外,也可能有出於管理方面的原因需要刪除資料的情況,儘管這些資料都是不可變的。例如,隱私條例可能要求在使用者關閉帳戶後刪除他們的個人資訊,資料保護立法可能要求刪除錯誤的資訊,或者可能需要阻止敏感資訊的意外洩露。 -在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史,並假裝資料從一開始就沒有寫入。例如,Datomic管這個特性叫**切除(excision)** 【62】,而Fossil版本控制系統有一個類似的概念叫**避免(shunning)** 【63】。 +在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史,並假裝資料從一開始就沒有寫入。例如,Datomic 管這個特性叫 **切除(excision)** 【62】,而 Fossil 版本控制系統有一個類似的概念叫 **避免(shunning)** 【63】。 -真正刪除資料是非常非常困難的【64】,因為副本可能存在於很多地方:例如,儲存引擎,檔案系統和SSD通常會向一個新位置寫入,而不是原地覆蓋舊資料【52】,而備份通常是特意做成不可變的,防止意外刪除或損壞。刪除操作更多的是指“使取回資料更困難”,而不是指“使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在“[立法與自律](ch12.md#立法與自律)”中所看到的。 +真正刪除資料是非常非常困難的【64】,因為副本可能存在於很多地方:例如,儲存引擎,檔案系統和 SSD 通常會向一個新位置寫入,而不是原地覆蓋舊資料【52】,而備份通常是特意做成不可變的,防止意外刪除或損壞。刪除操作更多的是指 “使取回資料更困難”,而不是指 “使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在 “[立法與自律](ch12.md#立法與自律)” 中所看到的。 ## 流處理 @@ -410,15 +410,15 @@ $$ 剩下的就是討論一下你可以用流做什麼 —— 也就是說,你可以處理它。一般來說,有三種選項: -1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如[圖11-5](../img/fig11-5.png)所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中所討論的,它是寫入儲存系統的流等價物。 +1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如 [圖 11-5](../img/fig11-5.png) 所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 中所討論的,它是寫入儲存系統的流等價物。 2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可實時顯示的儀表板上。在這種情況下,人是流的最終消費者。 -3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項1或2)。 +3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項 1 或 2)。 -在本章的剩餘部分中,我們將討論選項3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為**運算元(operator)** 或**作業(job)**。它與我們在[第十章](ch10.md)中討論過的Unix程序和MapReduce作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。 +在本章的剩餘部分中,我們將討論選項 3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為 **運算元(operator)** 或 **作業(job)**。它與我們在 [第十章](ch10.md) 中討論過的 Unix 程序和 MapReduce 作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。 -流處理中的分割槽和並行化模式也非常類似於[第十章](ch10.md)中介紹的MapReduce和資料流引擎,因此我們不再重複這些主題。基本的Map操作(如轉換和過濾記錄)也是一樣的。 +流處理中的分割槽和並行化模式也非常類似於 [第十章](ch10.md) 中介紹的 MapReduce 和資料流引擎,因此我們不再重複這些主題。基本的 Map 操作(如轉換和過濾記錄)也是一樣的。 -與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用**排序合併連線**(請參閱“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。 +與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用 **排序合併連線**(請參閱 “[Reduce 側連線與分組](ch10.md#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。 ### 流處理的應用 @@ -433,100 +433,100 @@ $$ #### 複合事件處理 -**複合事件處理(complex event processing, CEP)** 是20世紀90年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用【65,66】。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP允許你指定規則以在流中搜索某些事件模式。 +**複合事件處理(complex event processing, CEP)** 是 20 世紀 90 年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用【65,66】。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP 允許你指定規則以在流中搜索某些事件模式。 -CEP系統通常使用高層次的宣告式查詢語言,比如SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個**複合事件**(即complex event,CEP因此得名),並附有檢測到的事件模式詳情【67】。 +CEP 系統通常使用高層次的宣告式查詢語言,比如 SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個 **複合事件**(即 complex event,CEP 因此得名),並附有檢測到的事件模式詳情【67】。 -在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。 CEP引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。 +在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。 CEP 引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。 -CEP的實現包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO StreamBase和SQLstream。像Samza這樣的分散式流處理元件,支援使用SQL在流上進行宣告式查詢【71】。 +CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIBCO StreamBase 和 SQLstream。像 Samza 這樣的分散式流處理元件,支援使用 SQL 在流上進行宣告式查詢【71】。 #### 流分析 -使用流處理的另一個領域是對流進行分析。 CEP與流分析之間的邊界是模糊的,但一般來說,分析往往對找出特定事件序列並不關心,而更關注大量事件上的聚合與統計指標 —— 例如: +使用流處理的另一個領域是對流進行分析。 CEP 與流分析之間的邊界是模糊的,但一般來說,分析往往對找出特定事件序列並不關心,而更關注大量事件上的聚合與統計指標 —— 例如: * 測量某種型別事件的速率(每個時間間隔內發生的頻率) * 滾動計算一段時間視窗內某個值的平均值 * 將當前的統計值與先前的時間區間的值對比(例如,檢測趨勢,當指標與上週同比異常偏高或偏低時報警) -這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去5分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第99百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為**視窗(window)**,我們將在“[時間推理](#時間推理)”中更詳細地討論視窗。 +這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去 5 分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第 99 百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為 **視窗(window)**,我們將在 “[時間推理](#時間推理)” 中更詳細地討論視窗。 -流分析系統有時會使用概率演算法,例如Bloom filter(我們在“[效能最佳化](ch3.md#效能最佳化)”中遇到過)來管理成員資格,HyperLogLog【72】用於基數估計以及各種百分比估計演算法(請參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)“)。概率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而概率演算法只是一種最佳化【73】。 +流分析系統有時會使用概率演算法,例如 Bloom filter(我們在 “[效能最佳化](ch3.md#效能最佳化)” 中遇到過)來管理成員資格,HyperLogLog【72】用於基數估計以及各種百分比估計演算法(請參閱 “[實踐中的百分位點](ch1.md#實踐中的百分位點)“)。概率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而概率演算法只是一種最佳化【73】。 -許多開源分散式流處理框架的設計都是針對分析設計的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。託管服務包括Google Cloud Dataflow和Azure Stream Analytics。 +許多開源分散式流處理框架的設計都是針對分析設計的:例如 Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams 【74】。託管服務包括 Google Cloud Dataflow 和 Azure Stream Analytics。 #### 維護物化檢視 -我們在“[資料庫與流](#資料庫與流)”中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護**物化檢視(materialized view)** 的一種具體場景(請參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。 +我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視(materialized view)** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。 -同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的**所有**事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱“[日誌壓縮](#日誌壓縮)“)。實際上,你需要一個可以一直延伸到時間開端的視窗。 +同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#日誌壓縮)“)。實際上,你需要一個可以一直延伸到時間開端的視窗。 -原則上講,任何流處理元件都可以用於維護物化檢視,儘管“永遠執行”與一些面向分析的框架假設的“主要在有限時間段視窗上執行”背道而馳, Samza和Kafka Streams支援這種用法,建立在Kafka對日誌壓縮的支援上【75】。 +原則上講,任何流處理元件都可以用於維護物化檢視,儘管 “永遠執行” 與一些面向分析的框架假設的 “主要在有限時間段視窗上執行” 背道而馳, Samza 和 Kafka Streams 支援這種用法,建立在 Kafka 對日誌壓縮的支援上【75】。 #### 在流上搜索 -除了允許搜尋由多個事件構成模式的CEP外,有時也存在基於複雜標準(例如全文搜尋查詢)來搜尋單個事件的需求。 +除了允許搜尋由多個事件構成模式的 CEP 外,有時也存在基於複雜標準(例如全文搜尋查詢)來搜尋單個事件的需求。 -例如,媒體監測服務可以訂閱新聞文章Feed與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。 +例如,媒體監測服務可以訂閱新聞文章 Feed 與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch 的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。 -傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在CEP中一樣。最簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合【77】。 +傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在 CEP 中一樣。最簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合【77】。 #### 訊息傳遞和RPC -在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中我們討論過,訊息傳遞系統可以作為RPC的替代方案,即作為一種服務間通訊的機制,比如在Actor模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件: +在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件: -* Actor框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。 -* Actor之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。 -* Actor可以以任意方式進行通訊(包括迴圈的請求/響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。 +* Actor 框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。 +* Actor 之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。 +* Actor 可以以任意方式進行通訊(包括迴圈的請求 / 響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。 -也就是說,RPC類系統與流處理之間有一些交叉領域。例如,Apache Storm有一個稱為**分散式RPC**的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另請參閱“[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。 +也就是說,RPC 類系統與流處理之間有一些交叉領域。例如,Apache Storm 有一個稱為 **分散式 RPC** 的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另請參閱 “[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。 -也可以使用Actor框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。 +也可以使用 Actor 框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。 ### 時間推理 -流處理通常需要與時間打交道,尤其是用於分析目的時候,會頻繁使用時間視窗,例如“過去五分鐘的平均值”。“過去五分鐘”的含義看上去似乎是清晰而無歧義的,但不幸的是,這個概念非常棘手。 +流處理通常需要與時間打交道,尤其是用於分析目的時候,會頻繁使用時間視窗,例如 “過去五分鐘的平均值”。“過去五分鐘” 的含義看上去似乎是清晰而無歧義的,但不幸的是,這個概念非常棘手。 在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。 -批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱“[容錯](ch10.md#容錯)”)。 +批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](ch10.md#容錯)”)。 -另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即processing time)來確定**視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。 +另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time)來確定 **視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。 #### 事件時間與處理時間 -很多原因都可能導致處理延遲:排隊,網路故障(請參閱“[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理/訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱“[重播舊訊息](#重播舊訊息)”),或者在修復程式碼BUG之後。 +很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。 -而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個Web請求(由Web伺服器A處理),然後發出第二個請求(由伺服器B處理)。 A和B發出描述它們所處理請求的事件,但是B的事件在A的事件發生之前到達訊息代理。現在,流處理器將首先看到B事件,然後看到A事件,即使它們實際上是以相反的順序發生的。 +而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理)。 A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。 -有一個類比也許能幫助理解,“星球大戰”電影:第四集於1977年發行,第五集於1980年,第六集於1983年,緊隨其後的是1999年的第一集,2002年的第二集,和2005年的第三集,以及2015年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。 (集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。 +有一個類比也許能幫助理解,“星球大戰” 電影:第四集於 1977 年發行,第五集於 1980 年,第六集於 1983 年,緊隨其後的是 1999 年的第一集,2002 年的第二集,和 2005 年的第三集,以及 2015 年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。 (集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。 -[^ii]: 感謝Flink社群的Kostas Kloudas提出這個比喻。 +[^ii]: 感謝 Flink 社群的 Kostas Kloudas 提出這個比喻。 -將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖11-7](../img/fig11-7.png))。 +將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖 11-7](../img/fig11-7.png))。 ![](../img/fig11-7.png) -**圖11-7 按處理時間分窗,會因為處理速率的變動引入人為因素** +**圖 11-7 按處理時間分窗,會因為處理速率的變動引入人為因素** #### 知道什麼時候準備好了 用事件時間來定義視窗的一個棘手的問題是,你永遠也無法確定是不是已經收到了特定視窗的所有事件,還是說還有一些事件正在來的路上。 -例如,假設你將事件分組為一分鐘的視窗,以便統計每分鐘的請求數。你已經計數了一些帶有本小時內第37分鐘時間戳的事件,時間流逝,現在進入的主要都是本小時內第38和第39分鐘的事件。什麼時候才能宣佈你已經完成了第37分鐘的視窗計數,並輸出其計數器值? +例如,假設你將事件分組為一分鐘的視窗,以便統計每分鐘的請求數。你已經計數了一些帶有本小時內第 37 分鐘時間戳的事件,時間流逝,現在進入的主要都是本小時內第 38 和第 39 分鐘的事件。什麼時候才能宣佈你已經完成了第 37 分鐘的視窗計數,並輸出其計數器值? 在一段時間沒有看到任何新的事件之後,你可以超時並宣佈一個視窗已經就緒,但仍然可能發生這種情況:某些事件被緩衝在另一臺機器上,由於網路中斷而延遲。你需要能夠處理這種在視窗宣告完成之後到達的 **滯留(straggler)** 事件。大體上,你有兩種選擇【1】: 1. 忽略這些滯留事件,因為在正常情況下它們可能只是事件中的一小部分。你可以將丟棄事件的數量作為一個監控指標,並在出現大量丟訊息的情況時報警。 -2. 釋出一個**更正(correction)**,一個包括滯留事件的更新視窗值。你可能還需要收回以前的輸出。 +2. 釋出一個 **更正(correction)**,一個包括滯留事件的更新視窗值。你可能還需要收回以前的輸出。 -在某些情況下,可以使用特殊的訊息來指示“從現在開始,不會有比t更早時間戳的訊息了”,消費者可以使用它來觸發視窗【81】。但是,如果不同機器上的多個生產者都在生成事件,每個生產者都有自己的最小時間戳閾值,則消費者需要分別跟蹤每個生產者。在這種情況下,新增和刪除生產者都是比較棘手的。 +在某些情況下,可以使用特殊的訊息來指示 “從現在開始,不會有比 t 更早時間戳的訊息了”,消費者可以使用它來觸發視窗【81】。但是,如果不同機器上的多個生產者都在生成事件,每個生產者都有自己的最小時間戳閾值,則消費者需要分別跟蹤每個生產者。在這種情況下,新增和刪除生產者都是比較棘手的。 #### 你用的是誰的時鐘? 當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。 -在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱“[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。 +在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。 要校正不正確的裝置時鐘,一種方法是記錄三個時間戳【82】: @@ -544,58 +544,58 @@ CEP的實現包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO * 滾動視窗(Tumbling Window) - 滾動視窗有著固定的長度,每個事件都僅能屬於一個視窗。例如,假設你有一個1分鐘的滾動視窗,則所有時間戳在`10:03:00`和`10:03:59`之間的事件會被分組到一個視窗中,`10:04:00`和`10:04:59`之間的事件被分組到下一個視窗,依此類推。透過將每個事件時間戳四捨五入至最近的分鐘來確定它所屬的視窗,可以實現1分鐘的滾動視窗。 + 滾動視窗有著固定的長度,每個事件都僅能屬於一個視窗。例如,假設你有一個 1 分鐘的滾動視窗,則所有時間戳在 `10:03:00` 和 `10:03:59` 之間的事件會被分組到一個視窗中,`10:04:00` 和 `10:04:59` 之間的事件被分組到下一個視窗,依此類推。透過將每個事件時間戳四捨五入至最近的分鐘來確定它所屬的視窗,可以實現 1 分鐘的滾動視窗。 * 跳動視窗(Hopping Window) - 跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有1分鐘跳躍步長的5分鐘視窗將包含`10:03:00`至`10:07:59`之間的事件,而下一個視窗將覆蓋`10:04:00`至`10:08:59`之間的事件,等等。透過首先計算1分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。 + 跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有 1 分鐘跳躍步長的 5 分鐘視窗將包含 `10:03:00` 至 `10:07:59` 之間的事件,而下一個視窗將覆蓋 `10:04:00` 至 `10:08:59` 之間的事件,等等。透過首先計算 1 分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。 * 滑動視窗(Sliding Window) - 滑動視窗包含了彼此間距在特定時長內的所有事件。例如,一個5分鐘的滑動視窗應當覆蓋`10:03:39`和`10:08:12`的事件,因為它們相距不超過5分鐘(注意滾動視窗與步長5分鐘的跳動視窗可能不會把這兩個事件分組到同一個視窗中,因為它們使用固定的邊界)。透過維護一個按時間排序的事件緩衝區,並不斷從視窗中移除過期的舊事件,可以實現滑動視窗。 + 滑動視窗包含了彼此間距在特定時長內的所有事件。例如,一個 5 分鐘的滑動視窗應當覆蓋 `10:03:39` 和 `10:08:12` 的事件,因為它們相距不超過 5 分鐘(注意滾動視窗與步長 5 分鐘的跳動視窗可能不會把這兩個事件分組到同一個視窗中,因為它們使用固定的邊界)。透過維護一個按時間排序的事件緩衝區,並不斷從視窗中移除過期的舊事件,可以實現滑動視窗。 * 會話視窗(Session window) - 與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱“[分組](ch10.md#分組)”)。 + 與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](ch10.md#分組)”)。 ### 流連線 -在[第十章](ch10.md)中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。 +在 [第十章](ch10.md) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。 -然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流-流**連線,**流-表**連線,與**表-表**連線【84】。我們將在下面的章節中透過例子來說明。 +然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線【84】。我們將在下面的章節中透過例子來說明。 #### 流流連線(視窗連線) -假設你的網站上有搜尋功能,而你想要找出搜尋URL的近期趨勢。每當有人鍵入搜尋查詢時,都會記錄下一個包含查詢與其返回結果的事件。每當有人點選其中一個搜尋結果時,就會記錄另一個記錄點選事件。為了計算搜尋結果中每個URL的點選率,你需要將搜尋動作與點選動作的事件連在一起,這些事件透過相同的會話ID進行連線。廣告系統中需要類似的分析【85】。 +假設你的網站上有搜尋功能,而你想要找出搜尋 URL 的近期趨勢。每當有人鍵入搜尋查詢時,都會記錄下一個包含查詢與其返回結果的事件。每當有人點選其中一個搜尋結果時,就會記錄另一個記錄點選事件。為了計算搜尋結果中每個 URL 的點選率,你需要將搜尋動作與點選動作的事件連在一起,這些事件透過相同的會話 ID 進行連線。廣告系統中需要類似的分析【85】。 如果使用者丟棄了搜尋結果,點選可能永遠不會發生,即使它出現了,搜尋與點選之間的時間可能是高度可變的:在很多情況下,它可能是幾秒鐘,但也可能長達幾天或幾周(如果使用者執行搜尋,忘掉了這個瀏覽器頁面,過了一段時間後重新回到這個瀏覽器頁面上,並點選了一個結果)。由於可變的網路延遲,點選事件甚至可能先於搜尋事件到達。你可以選擇合適的連線視窗 —— 例如,如果點選與搜尋之間的時間間隔在一小時內,你可能會選擇連線兩者。 請注意,在點選事件中嵌入搜尋詳情與事件連線並不一樣:這樣做的話,只有當用戶點選了一個搜尋結果時你才能知道,而那些沒有點選的搜尋就無能為力了。為了衡量搜尋質量,你需要準確的點選率,為此搜尋事件和點選事件兩者都是必要的。 -為了實現這種型別的連線,流處理器需要維護**狀態**:例如,按會話ID索引最近一小時內發生的所有事件。無論何時發生搜尋事件或點選事件,都會被新增到合適的索引中,而流處理器也會檢查另一個索引是否有具有相同會話ID的事件到達。如果有匹配事件就會發出一個表示搜尋結果被點選的事件;如果搜尋事件直到過期都沒看見有匹配的點選事件,就會發出一個表示搜尋結果未被點選的事件。 +為了實現這種型別的連線,流處理器需要維護 **狀態**:例如,按會話 ID 索引最近一小時內發生的所有事件。無論何時發生搜尋事件或點選事件,都會被新增到合適的索引中,而流處理器也會檢查另一個索引是否有具有相同會話 ID 的事件到達。如果有匹配事件就會發出一個表示搜尋結果被點選的事件;如果搜尋事件直到過期都沒看見有匹配的點選事件,就會發出一個表示搜尋結果未被點選的事件。 #### 流表連線(流擴充) -在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖10-2](../img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者ID的活動事件流,而輸出還是活動事件流,但其中使用者ID已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來**擴充(enriching)** 活動事件。 +在 “[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖 10-2](../img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者 ID 的活動事件流,而輸出還是活動事件流,但其中使用者 ID 已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來 **擴充(enriching)** 活動事件。 -要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載【75】。 +要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者 ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在 “[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)” 一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載【75】。 -另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在“[Map側連線](ch10.md#Map側連線)”中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。 +另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在 “[Map 側連線](ch10.md#Map側連線)” 中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。 與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過變更資料捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活動事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。 -流表連線實際上非常類似於流流連線;最大的區別在於對於表的變更日誌流,連線使用了一個可以回溯到“時間起點”的視窗(概念上是無限的視窗),新版本的記錄會覆蓋更早的版本。對於輸入的流,連線可能壓根兒就沒有維護任何視窗。 +流表連線實際上非常類似於流流連線;最大的區別在於對於表的變更日誌流,連線使用了一個可以回溯到 “時間起點” 的視窗(概念上是無限的視窗),新版本的記錄會覆蓋更早的版本。對於輸入的流,連線可能壓根兒就沒有維護任何視窗。 #### 表表連線(維護物化檢視) -我們在“[描述負載](ch1.md#描述負載)”中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。 +我們在 “[描述負載](ch1.md#描述負載)” 中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。 -相反,我們需要一個時間線快取:一種每個使用者的“收件箱”,在傳送推文的時候寫入這些資訊,因而讀取時間線時只需要簡單地查詢即可。物化與維護這個快取需要處理以下事件: +相反,我們需要一個時間線快取:一種每個使用者的 “收件箱”,在傳送推文的時候寫入這些資訊,因而讀取時間線時只需要簡單地查詢即可。物化與維護這個快取需要處理以下事件: -* 當用戶u傳送新的推文時,它將被新增到每個關注使用者u的時間線上。 +* 當用戶 u 傳送新的推文時,它將被新增到每個關注使用者 u 的時間線上。 * 使用者刪除推文時,推文將從所有使用者的時間表中刪除。 -* 當用戶$u_1$開始關注使用者$u_2$時,$u_2$最近的推文將被新增到$u_1$的時間線上。 -* 當用戶$u_1$取消關注使用者$u_2$時,$u_2$的推文將從$u_1$的時間線中移除。 +* 當用戶 $u_1$ 開始關注使用者 $u_2$ 時,$u_2$ 最近的推文將被新增到 $u_1$ 的時間線上。 +* 當用戶 $u_1$ 取消關注使用者 $u_2$ 時,$u_2$ 的推文將從 $u_1$ 的時間線中移除。 要在流處理器中實現這種快取維護,你需要推文事件流(傳送與刪除)和關注關係事件流(關注與取消關注)。流處理需要維護一個數據庫,包含每個使用者的粉絲集合。以便知道當一條新推文到達時,需要更新哪些時間線【86】。 @@ -609,9 +609,9 @@ JOIN follows ON follows.followee_id = tweets.sender_id GROUP BY follows.follower_id ``` -流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新[^iii]。 +流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新 [^iii]。 -[^iii]: 如果你將流視作表的衍生物,如[圖11-6](../img/fig11-6.png)所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。 換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。 +[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](../img/fig11-6.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。 換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。 #### 連線的時間依賴性 @@ -621,80 +621,80 @@ GROUP BY follows.follower_id 這就產生了一個問題:如果不同流中的事件發生在近似的時間範圍內,則應該按照什麼樣的順序進行處理?在流表連線的例子中,如果使用者更新了它們的檔案,哪些活動事件與舊檔案連線(在檔案更新前處理),哪些又與新檔案連線(在檔案更新之後處理)?換句話說:你需要對一些狀態做連線,如果狀態會隨著時間推移而變化,那應當使用什麼時間點來連線呢【45】? -這種時序依賴可能出現在很多地方。例如銷售東西需要對發票應用適當的稅率,這取決於所處的國家/州,產品型別,銷售日期(因為稅率時不時會變化)。當連線銷售額與稅率表時,你可能期望的是使用銷售時的稅率參與連線。如果你正在重新處理歷史資料,銷售時的稅率可能和現在的稅率有所不同。 +這種時序依賴可能出現在很多地方。例如銷售東西需要對發票應用適當的稅率,這取決於所處的國家 / 州,產品型別,銷售日期(因為稅率時不時會變化)。當連線銷售額與稅率表時,你可能期望的是使用銷售時的稅率參與連線。如果你正在重新處理歷史資料,銷售時的稅率可能和現在的稅率有所不同。 如果跨越流的事件順序是未定的,則連線會變為不確定性的【87】,這意味著你在同樣輸入上重跑相同的作業未必會得到相同的結果:當你重跑任務時,輸入流上的事件可能會以不同的方式交織。 -在資料倉庫中,這個問題被稱為**緩慢變化的維度(slowly changing dimension, SCD)**,通常透過對特定版本的記錄使用唯一的識別符號來解決:例如,每當稅率改變時都會獲得一個新的識別符號,而發票在銷售時會帶有稅率的識別符號【88,89】。這種變化使連線變為確定性的,但也會導致日誌壓縮無法進行:表中所有的記錄版本都需要保留。 +在資料倉庫中,這個問題被稱為 **緩慢變化的維度(slowly changing dimension, SCD)**,通常透過對特定版本的記錄使用唯一的識別符號來解決:例如,每當稅率改變時都會獲得一個新的識別符號,而發票在銷售時會帶有稅率的識別符號【88,89】。這種變化使連線變為確定性的,但也會導致日誌壓縮無法進行:表中所有的記錄版本都需要保留。 ### 容錯 -在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在[第十章](ch10.md)中看到,批處理框架可以很容易地容錯:如果MapReduce作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到HDFS上的獨立檔案中,而輸出僅當任務成功完成後可見。 +在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在 [第十章](ch10.md) 中看到,批處理框架可以很容易地容錯:如果 MapReduce 作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到 HDFS 上的獨立檔案中,而輸出僅當任務成功完成後可見。 -特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為**恰好一次語義(exactly-once semantics)**,儘管**等效一次(effectively-once)** 可能會是一個更寫實的術語【90】。 +特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為 **恰好一次語義(exactly-once semantics)**,儘管 **等效一次(effectively-once)** 可能會是一個更寫實的術語【90】。 在流處理中也出現了同樣的容錯問題,但是處理起來沒有那麼直觀:等待某個任務完成之後再使其輸出可見並不是一個可行選項,因為你永遠無法處理完一個無限的流。 #### 微批次與存檔點 -一個解決方案是將流分解成小塊,並像微型批處理一樣處理每個塊。這種方法被稱為**微批次(microbatching)**,它被用於Spark Streaming 【91】。批次的大小通常約為1秒,這是對效能妥協的結果:較小的批次會導致更大的排程與協調開銷,而較大的批次意味著流處理器結果可見之前的延遲要更長。 +一個解決方案是將流分解成小塊,並像微型批處理一樣處理每個塊。這種方法被稱為 **微批次(microbatching)**,它被用於 Spark Streaming 【91】。批次的大小通常約為 1 秒,這是對效能妥協的結果:較小的批次會導致更大的排程與協調開銷,而較大的批次意味著流處理器結果可見之前的延遲要更長。 微批次也隱式提供了一個與批次大小相等的滾動視窗(按處理時間而不是事件時間戳分窗)。任何需要更大視窗的作業都需要顯式地將狀態從一個微批次轉移到下一個微批次。 -Apache Flink則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的**壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。 +Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的 **壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。 -在流處理框架的範圍內,微批次與存檔點方法提供了與批處理一樣的**恰好一次語義**。但是,只要輸出離開流處理器(例如,寫入資料庫,向外部訊息代理傳送訊息,或傳送電子郵件),框架就無法拋棄失敗批次的輸出了。在這種情況下,重啟失敗任務會導致外部副作用發生兩次,只有微批次或存檔點不足以阻止這一問題。 +在流處理框架的範圍內,微批次與存檔點方法提供了與批處理一樣的 **恰好一次語義**。但是,只要輸出離開流處理器(例如,寫入資料庫,向外部訊息代理傳送訊息,或傳送電子郵件),框架就無法拋棄失敗批次的輸出了。在這種情況下,重啟失敗任務會導致外部副作用發生兩次,只有微批次或存檔點不足以阻止這一問題。 #### 原子提交再現 -為了在出現故障時表現出恰好處理一次的樣子,我們需要確保事件處理的所有輸出和副作用**當且僅當**處理成功時才會生效。這些影響包括傳送給下游運算元或外部訊息傳遞系統(包括電子郵件或推送通知)的任何訊息,任何資料庫寫入,對運算元狀態的任何變更,以及對輸入訊息的任何確認(包括在基於日誌的訊息代理中將消費者偏移量前移)。 +為了在出現故障時表現出恰好處理一次的樣子,我們需要確保事件處理的所有輸出和副作用 **當且僅當** 處理成功時才會生效。這些影響包括傳送給下游運算元或外部訊息傳遞系統(包括電子郵件或推送通知)的任何訊息,任何資料庫寫入,對運算元狀態的任何變更,以及對輸入訊息的任何確認(包括在基於日誌的訊息代理中將消費者偏移量前移)。 -這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱“[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。 +這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。 -在[第九章](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 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。 #### 冪等性 -我們的目標是丟棄任何失敗任務的部分輸出,以便能安全地重試,而不會生效兩次。分散式事務是實現這個目標的一種方式,而另一種方式是依賴**冪等性(idempotence)**【97】。 +我們的目標是丟棄任何失敗任務的部分輸出,以便能安全地重試,而不會生效兩次。分散式事務是實現這個目標的一種方式,而另一種方式是依賴 **冪等性(idempotence)**【97】。 冪等操作是多次重複執行與單次執行效果相同的操作。例如,將鍵值儲存中的某個鍵設定為某個特定值是冪等的(再次寫入該值,只是用同樣的值替代),而遞增一個計數器不是冪等的(再次執行遞增意味著該值遞增兩次)。 -即使一個操作不是天生冪等的,往往可以透過一些額外的元資料做成冪等的。例如,在使用來自Kafka的訊息時,每條訊息都有一個持久的、單調遞增的偏移量。將值寫入外部資料庫時可以將這個偏移量帶上,這樣你就可以判斷一條更新是不是已經執行過了,因而避免重複執行。 +即使一個操作不是天生冪等的,往往可以透過一些額外的元資料做成冪等的。例如,在使用來自 Kafka 的訊息時,每條訊息都有一個持久的、單調遞增的偏移量。將值寫入外部資料庫時可以將這個偏移量帶上,這樣你就可以判斷一條更新是不是已經執行過了,因而避免重複執行。 -Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。 +Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。 -當從一個處理節點故障切換到另一個節點時,可能需要進行**防護**(fencing,請參閱“[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現**恰好一次語義**的有效方式,僅需很小的額外開銷。 +當從一個處理節點故障切換到另一個節點時,可能需要進行 **防護**(fencing,請參閱 “[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現 **恰好一次語義** 的有效方式,僅需很小的額外開銷。 #### 失敗後重建狀態 任何需要狀態的流處理 —— 例如,任何視窗聚合(例如計數器,平均值和直方圖)以及任何用於連線的表和索引,都必須確保在失敗之後能恢復其狀態。 -一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在“[流表連線(流擴充)](#流表連線(流擴充))”中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。 +一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在 “[流表連線(流擴充)](#流表連線(流擴充))” 中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。 -例如,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#真的序列執行)”)。 -在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱“[日誌壓縮](#日誌壓縮)”)。 +在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱 “[日誌壓縮](#日誌壓縮)”)。 然而,所有這些權衡取決於底層基礎架構的效能特徵:在某些系統中,網路延遲可能低於磁碟訪問延遲,網路頻寬也可能與磁碟頻寬相當。沒有針對所有情況的普適理想權衡,隨著儲存和網路技術的發展,本地狀態與遠端狀態的優點也可能會互換。 ## 本章小結 -在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在[第十章](ch10.md) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。 +在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在 [第十章](ch10.md) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。 我們花了一些時間比較兩種訊息代理: -* AMQP/JMS風格的訊息代理 +* AMQP/JMS 風格的訊息代理 - 代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的RPC(另請參閱“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。 + 代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC(另請參閱 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。 * 基於日誌的訊息代理 代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。 -基於日誌的方法與資料庫中的複製日誌(請參閱[第五章](ch5.md))和日誌結構儲存引擎(請參閱[第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。 +基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](ch5.md))和日誌結構儲存引擎(請參閱 [第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。 -就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和Feed資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。 +就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。 將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至衍生系統,你能使諸如搜尋索引、快取以及分析系統這類衍生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。 @@ -706,7 +706,7 @@ Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意 * 流流連線 - 兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者30分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線**,即self-join)。 + 兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者 30 分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線**,即 self-join)。 * 流表連線 diff --git a/zh-tw/ch12.md b/zh-tw/ch12.md index 20c3c06..a56b75d 100644 --- a/zh-tw/ch12.md +++ b/zh-tw/ch12.md @@ -4,165 +4,165 @@ > 如果船長的終極目標是保護船隻,他應該永遠待在港口。 > -> —— 聖托馬斯·阿奎那《神學大全》(1265-1274) +> —— 聖托馬斯・阿奎那《神學大全》(1265-1274) --------------- [TOC] -到目前為止,本書主要描述的是**現狀**。在這最後一章中,我們將放眼**未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。 +到目前為止,本書主要描述的是 **現狀**。在這最後一章中,我們將放眼 **未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。 對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。 -[第一章](ch1.md)概述了本書的目標:探索如何建立**可靠**,**可伸縮**和**可維護**的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯,正確,可演化,且最終對人類有益。 +[第一章](ch1.md) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。 ## 資料整合 -本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在[第三章](ch3.md)討論儲存引擎時,我們看到了日誌結構儲存,B樹,以及列式儲存。在[第五章](ch5.md)討論複製時,我們看到了單領導者,多領導者,和無領導者的方法。 +本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](ch3.md) 討論儲存引擎時,我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](ch5.md) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。 -如果你有一個類似於“我想儲存一些資料並稍後再查詢”的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。 +如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。 -因此軟體工具的最佳選擇也取決於情況。每一種軟體,甚至所謂的“通用”資料庫,都是針對特定的使用模式設計的。 +因此軟體工具的最佳選擇也取決於情況。每一種軟體,甚至所謂的 “通用” 資料庫,都是針對特定的使用模式設計的。 面對讓人眼花繚亂的諸多替代品,第一個挑戰就是弄清軟體與其適用環境的對映關係。供應商不願告訴你他們軟體不適用的工作負載,這是可以理解的。但是希望先前的章節能給你提供一些問題,讓你讀出字裡行間的言外之意,並更好地理解這些權衡。 -但是,即使你已經完全理解各種工具與其適用環境間的關係,還有一個挑戰:在複雜的應用中,資料的用法通常花樣百出。不太可能存在適用於**所有**不同資料應用場景的軟體,因此你不可避免地需要拼湊幾個不同的軟體來以提供應用所需的功能。 +但是,即使你已經完全理解各種工具與其適用環境間的關係,還有一個挑戰:在複雜的應用中,資料的用法通常花樣百出。不太可能存在適用於 **所有** 不同資料應用場景的軟體,因此你不可避免地需要拼湊幾個不同的軟體來以提供應用所需的功能。 ### 組合使用衍生資料的工具 -例如,為了處理任意關鍵詞的搜尋查詢,將OLTP資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更復雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。 +例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更復雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。 -我們在“[保持系統同步](ch11.md#保持系統同步)”中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。 +我們在 “[保持系統同步](ch11.md#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。 -令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99%的人只需要X”或者 “......不需要X”(對於各種各樣的X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。 +令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99% 的人只需要 X” 或者 “...... 不需要 X”(對於各種各樣的 X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。 #### 理解資料流 當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示衍生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方? -例如,你可能會首先將資料寫入**記錄系統**資料庫,捕獲對該資料庫所做的變更(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。 +例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。 -允許應用程式直接寫入搜尋索引和資料庫引入瞭如[圖11-4](../img/fig11-4.png)所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。 +允許應用程式直接寫入搜尋索引和資料庫引入瞭如 [圖 11-4](../img/fig11-4.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。 -如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在“[全序廣播](ch9.md#全序廣播)”中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。 +如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在 “[全序廣播](ch9.md#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。 -基於事件日誌來更新衍生資料的系統,通常可以做到**確定性**與**冪等性**(請參閱[冪等性](ch11.md#冪等性)”),使得從故障中恢復相當容易。 +基於事件日誌來更新衍生資料的系統,通常可以做到 **確定性** 與 **冪等性**(請參閱 “[冪等性](ch11.md#冪等性)”),使得從故障中恢復相當容易。 #### 衍生資料與分散式事務 -保持不同資料系統彼此一致的經典方法涉及分散式事務,如“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”中所述。與分散式事務相比,使用衍生資料系統的方法如何? +保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)” 中所述。與分散式事務相比,使用衍生資料系統的方法如何? -在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過**鎖**進行互斥來決定寫入的順序(請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”),而CDC和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於**確定性重試**和**冪等性**。 +在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 **鎖** 進行互斥來決定寫入的順序(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 **確定性重試** 和 **冪等性**。 -最大的不同之處在於事務系統通常提供[線性一致性](ch9.md#線性一致性),這包含著有用的保證,例如[讀己之寫](ch5.md#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。 +最大的不同之處在於事務系統通常提供 [線性一致性](ch9.md#線性一致性),這包含著有用的保證,例如 [讀己之寫](ch5.md#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。 -在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為XA的容錯能力和效能很差勁(請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。 +在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。 -在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的衍生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人“最終一致性是不可避免的 —— 忍一忍並學會和它打交道”是沒有什麼建設性的(至少在缺乏**如何**應對的良好指導時)。 +在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的衍生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人 “最終一致性是不可避免的 —— 忍一忍並學會和它打交道” 是沒有什麼建設性的(至少在缺乏 **如何** 應對的良好指導時)。 -在“[將事情做正確](#將事情做正確)”中,我們將討論一些在非同步衍生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。 +在 “[將事情做正確](#將事情做正確)” 中,我們將討論一些在非同步衍生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。 #### 全序的限制 對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更復雜的工作負載伸縮,限制開始出現: -* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的**單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱“[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。 -* 如果伺服器分佈在多個**地理位置分散**的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱“[多主複製](ch5.md#多主複製)“)。這意味著源自兩個不同資料中心的事件順序未定義。 -* 將應用程式部署為微服務時(請參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。 -* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱“[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。 +* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。 +* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](ch5.md#多主複製)“)。這意味著源自兩個不同資料中心的事件順序未定義。 +* 將應用程式部署為微服務時(請參閱 “[服務中的資料流:REST 與 RPC](ch4.md#服務中的資料流:REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。 +* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。 -在形式上,決定事件的全域性順序稱為**全序廣播**,相當於**共識**(請參閱“[共識演算法和全序廣播](ch9.md#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。 +在形式上,決定事件的全域性順序稱為 **全序廣播**,相當於 **共識**(請參閱 “[共識演算法和全序廣播](ch9.md#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。 #### 排序事件以捕獲因果關係 -在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件ID的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱“[順序與因果關係](ch9.md#順序與因果關係)”)。 +在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “[順序與因果關係](ch9.md#順序與因果關係)”)。 例如,考慮一個社交網路服務,以及一對曾處於戀愛關係但剛分手的使用者。其中一個使用者將另一個使用者從好友中移除,然後向剩餘的好友傳送訊息,抱怨他們的前任。使用者的心思是他們的前任不應該看到這些粗魯的訊息,因為訊息是在好友狀態解除後傳送的。 -但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現**解除好友**事件與**傳送訊息**事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在**解除好友**事件之前處理**傳送訊息**事件,從而錯誤地向前任傳送通知。 +但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現 **解除好友** 事件與 **傳送訊息** 事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在 **解除好友** 事件之前處理 **傳送訊息** 事件,從而錯誤地向前任傳送通知。 -在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱“[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括: +在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括: -* 邏輯時間戳可以提供無需協調的全域性順序(請參閱“[序列號順序](ch9.md#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。 -* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在“[讀也是事件](#讀也是事件)”中回到這個想法。 -* 衝突解決演算法(請參閱“[自動衝突解決](ch5.md#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。 +* 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “[序列號順序](ch9.md#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。 +* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。 +* 衝突解決演算法(請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。 也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。 ### 批處理與流處理 -我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入,轉換,連線,過濾,聚合,訓練模型,評估,以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。 +我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入、轉換、連線、過濾、聚合、訓練模型、評估、以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。 -批處理和流處理的輸出是衍生資料集,例如搜尋索引,物化檢視,向用戶顯示的建議,聚合指標等(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”和“[流處理的應用](ch11.md#流處理的應用)”)。 +批處理和流處理的輸出是衍生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 和 “[流處理的應用](ch11.md#流處理的應用)”)。 -正如我們在[第十章](ch10.md)和[第十一章](ch11.md)中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。 +正如我們在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。 -Spark在批處理引擎上執行流處理,將流分解為**微批次(microbatches)**,而Apache Flink則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。 +Spark 在批處理引擎上執行流處理,將流分解為 **微批次(microbatches)**,而 Apache Flink 則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。 #### 維護衍生狀態 -批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱“[失敗後重建狀態”](ch11.md#失敗後重建狀態))。 +批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態”](ch11.md#失敗後重建狀態))。 -具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱“[冪等性](ch11.md#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至衍生系統中。 +具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](ch11.md#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至衍生系統中。 -原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱“[分散式事務的限制](ch9.md#分散式事務的限制)”)。 +原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](ch9.md#分散式事務的限制)”)。 -我們在“[分割槽與次級索引](ch6.md#分割槽與次級索引)”中看到,次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱“[多分割槽資料處理](多分割槽資料處理)”)。 +我們在 “[分割槽與次級索引](ch6.md#分割槽與次級索引)” 中看到,次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱 “[多分割槽資料處理](#多分割槽資料處理)”)。 #### 應用演化後重新處理資料 在維護衍生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在衍生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。 -特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱[第四章](ch4.md))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。 +特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](ch4.md))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。 > ### 鐵路上的模式遷移 > -> 大規模的“模式遷移”也發生在非計算機系統中。例如,在19世紀英國鐵路建設初期,軌距(兩軌之間的距離)就有了各種各樣的競爭標準。為一種軌距而建的列車不能在另一種軌距的軌道上執行,這限制了火車網路中可能的相互連線【9】。 +> 大規模的 “模式遷移” 也發生在非計算機系統中。例如,在 19 世紀英國鐵路建設初期,軌距(兩軌之間的距離)就有了各種各樣的競爭標準。為一種軌距而建的列車不能在另一種軌距的軌道上執行,這限制了火車網路中可能的相互連線【9】。 > -> 在1846年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為**雙軌距(dual guage)** 或**混合軌距**。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。 +> 在 1846 年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為 **雙軌距(dual guage)** 或 **混合軌距**。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。 > -> 以這種方式“再加工”現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的BART系統使用了與美國大部分地區不同的軌距。 +> 以這種方式 “再加工” 現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的 BART 系統使用了與美國大部分地區不同的軌距。 -衍生檢視允許**漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。 +衍生檢視允許 **漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。 這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統【11】。 #### Lambda架構 -如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda架構【12】是這方面的一個建議,引起了很多關注。 +如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda 架構【12】是這方面的一個建議,引起了很多關注。 -Lambda架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱“[事件溯源](ch11.md#事件溯源)”)。為了從這些事件中衍生出讀取最佳化的檢視, Lambda架構建議並行執行兩個不同的系統:批處理系統(如Hadoop MapReduce)和獨立的流處理系統(如Storm)。 +Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](ch11.md#事件溯源)”)。為了從這些事件中衍生出讀取最佳化的檢視,Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce)和獨立的流處理系統(如 Storm)。 -在Lambda方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱“[容錯](ch11.md#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。 +在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](ch11.md#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。 -Lambda架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題: +Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題: -* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。 +* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。 * 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更復雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。 -* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了“[時間推理](ch11.md#時間推理)”中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。 +* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](ch11.md#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。 #### 統一批處理和流處理 -最近的工作使得Lambda架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(在事件到達時即處理)在同一個系統中實現【15】。 +最近的工作使得 Lambda 架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(在事件到達時即處理)在同一個系統中實現【15】。 在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供: -* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱“[重播舊訊息](ch11.md#重播舊訊息)”),某些流處理器可以從HDFS等分散式檔案系統讀取輸入。 -* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱“[容錯](ch11.md#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。 -* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱“[時間推理](ch11.md#時間推理)”)。例如,Apache Beam提供了用於表達這種計算的API,可以在Apache Flink或Google Cloud Dataflow使用。 +* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](ch11.md#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。 +* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](ch11.md#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。 +* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](ch11.md#時間推理)”)。例如,Apache Beam 提供了用於表達這種計算的 API,可以在 Apache Flink 或 Google Cloud Dataflow 使用。 ## 分拆資料庫 -在最抽象的層面上,資料庫,Hadoop和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是“資訊管理”系統【17】。正如我們在[第十章](ch10.md)中看到的,Hadoop生態系統有點像Unix的分散式版本。 +在最抽象的層面上,資料庫,Hadoop 和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是 “資訊管理” 系統【17】。正如我們在 [第十章](ch10.md) 中看到的,Hadoop 生態系統有點像 Unix 的分散式版本。 -當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含1000萬個小檔案的目錄,而包含1000萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。 +當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含 1000 萬個小檔案的目錄,而包含 1000 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。 -Unix和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性,併發性,崩潰恢復等等。Unix發展出的管道和檔案只是位元組序列,而資料庫則發展出了SQL和事務。 +Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix 認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性,併發性,崩潰恢復等等。Unix 發展出的管道和檔案只是位元組序列,而資料庫則發展出了 SQL 和事務。 -哪種方法更好?當然這取決於你想要的是什麼。 Unix是“簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是“更簡單”的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化,索引,連線方法,併發控制,複製等),而不需要查詢的作者理解其實現細節。 +哪種方法更好?當然這取決於你想要的是什麼。 Unix 是 “簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是 “更簡單” 的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化、索引、連線方法、併發控制、複製等),而不需要查詢的作者理解其實現細節。 -這些哲學之間的矛盾已經持續了幾十年(Unix和關係模型都出現在70年代初),仍然沒有解決。例如,我將NoSQL運動解釋為,希望將類Unix的低級別抽象方法應用於分散式OLTP資料儲存的領域。 +這些哲學之間的矛盾已經持續了幾十年(Unix 和關係模型都出現在 70 年代初),仍然沒有解決。例如,我將 NoSQL 運動解釋為,希望將類 Unix 的低級別抽象方法應用於分散式 OLTP 資料儲存的領域。 在這一部分我將試圖調和這兩個哲學,希望我們能各取其美。 @@ -170,83 +170,83 @@ Unix和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix 在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括: -* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱“[其他索引結構](ch3.md#其他索引結構)”) -* 物化檢視,這是一種預計算的查詢結果快取(請參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”) -* 複製日誌,保持其他節點上資料的副本最新(請參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”) -* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱“[全文搜尋和模糊索引](ch3.md#全文搜4索和模糊索引)”),也內置於某些關係資料庫【1】 +* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](ch3.md#其他索引結構)”) +* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”) +* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](ch5.md#複製日誌的實現)”) +* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”),也內置於某些關係資料庫【1】 -在[第十章](ch10.md)和[第十一章](ch11.md)中,出現了類似的主題。我們討論瞭如何構建全文搜尋索引(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱“[維護物化檢視](ch11.md#維護物化檢視)”)以及如何將變更從資料庫複製到衍生資料系統(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”)。 +在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中,出現了類似的主題。我們討論瞭如何構建全文搜尋索引(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱 “[維護物化檢視](ch11.md#維護物化檢視)”)以及如何將變更從資料庫複製到衍生資料系統(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”)。 資料庫中內建的功能與人們用批處理和流處理器構建的衍生資料系統似乎有相似之處。 #### 建立索引 -想想當你執行`CREATE INDEX`在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。 +想想當你執行 `CREATE INDEX` 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。 -此過程非常類似於設定新的從庫副本(請參閱“[設定新從庫](ch5.md#設定新從庫)”),也非常類似於流處理系統中的**引導(bootstrap)** 變更資料捕獲(請參閱“[初始快照](ch11.md#初始快照)”)。 +此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](ch5.md#設定新從庫)”),也非常類似於流處理系統中的 **引導(bootstrap)** 變更資料捕獲(請參閱 “[初始快照](ch11.md#初始快照)”)。 -無論何時執行`CREATE INDEX`,資料庫都會重新處理現有資料集(如“[應用演化後重新處理資料](#應用演化後重新處理資料)”中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱“[狀態、流和不變性](ch11.md#狀態、流和不變性)”)。 +無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](ch11.md#狀態、流和不變性)”)。 #### 一切的元資料庫 -有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或ETL過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。 +有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。 -從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援B樹索引、雜湊索引、空間索引(請參閱“[多列索引](ch3.md#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。 +從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](ch3.md#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。 這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統: **聯合資料庫:統一讀取** -可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為**聯合資料庫(federated database)** 或**多型儲存(polystore)** 的方法【18,19】。例如,PostgreSQL的**外部資料包裝器(foreign data wrapper)** 功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。 +可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為 **聯合資料庫(federated database)** 或 **多型儲存(polystore)** 的方法【18,19】。例如,PostgreSQL 的 **外部資料包裝器(foreign data wrapper)** 功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。 聯合查詢介面遵循著單一整合系統的關係型傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。 **分拆資料庫:統一寫入** -雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統**同步**寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。 +雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統 **同步** 寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。 -分拆方法遵循Unix傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低層級API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)【16】 。 +分拆方法遵循 Unix 傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低層級 API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)【16】 。 #### 開展分拆工作 聯合和分拆是一個硬幣的兩面:用不同的元件構成可靠、 可伸縮和可維護的系統。聯合只讀查詢需要將一個數據模型對映到另一個數據模型,這需要一些思考,但最終還是一個可解決的問題。而我認為同步寫入到幾個儲存系統是更困難的工程問題,所以我將重點關注它。 -傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱“[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。 +傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱 “[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。 -例如,分散式事務在某些流處理元件內部使用,以匹配**恰好一次(exactly-once)** 語義(請參閱“[原子提交再現](ch11.md#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱“[冪等性](ch11.md#冪等性)”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。 +例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次(exactly-once)** 語義(請參閱 “[原子提交再現](ch11.md#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](ch11.md#冪等性)”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。 -基於日誌的整合的一大優勢是各個元件之間的**鬆散耦合(loose coupling)**,這體現在兩個方面: +基於日誌的整合的一大優勢是各個元件之間的 **鬆散耦合(loose coupling)**,這體現在兩個方面: -1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱“[磁碟空間使用](ch11.md#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱“[分散式事務的限制](ch9.md#分散式事務的限制)”)。 +1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](ch11.md#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](ch9.md#分散式事務的限制)”)。 2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。 #### 分拆系統vs整合系統 -如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”與“[流處理](ch11.md#流處理)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 。 +如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 與 “[流處理](ch11.md#流處理)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 。 執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。 -分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”中討論的儲存和處理模型的多樣性一樣。 +分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。 因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。 #### 少了什麼? -用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與Unix shell類似的分拆資料庫等價物(即,一種宣告式的、簡單的、用於組裝儲存和處理系統的高階語言)。 +用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與 Unix shell 類似的分拆資料庫等價物(即,一種宣告式的、簡單的、用於組裝儲存和處理系統的高階語言)。 -例如,如果我們可以簡單地宣告`mysql | elasticsearch`,類似於Unix管道【22】,成為`CREATE INDEX`的分拆等價物:它將讀取MySQL資料庫中的所有文件並將其索引到Elasticsearch叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。 +例如,如果我們可以簡單地宣告 `mysql | elasticsearch`,類似於 Unix 管道【22】,成為 `CREATE INDEX` 的分拆等價物:它將讀取 MySQL 資料庫中的所有文件並將其索引到 Elasticsearch 叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。 -同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱“[圖資料模型](ch2.md#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如**差分資料流(differential dataflow)**【24,25】,我希望這些想法能夠在生產系統中找到自己的方法。 +同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱 “[圖資料模型](ch2.md#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如 **差分資料流(differential dataflow)**【24,25】,我希望這些想法能夠在生產系統中找到自己的方法。 ### 圍繞資料流設計應用 -使用應用程式碼組合專用儲存與處理系統來分拆資料庫的方法,也被稱為“**資料庫由內而外(database inside-out)**”方法【26】,該名稱來源於我在2014年的一次會議演講標題【27】。然而稱它為“新架構”過於誇大,我僅將其看作是一種設計模式,一個討論的起點,我們只是簡單地給它起一個名字,以便我們能更好地討論它。 +使用應用程式碼組合專用儲存與處理系統來分拆資料庫的方法,也被稱為 “**資料庫由內而外(database inside-out)**” 方法【26】,該名稱來源於我在 2014 年的一次會議演講標題【27】。然而稱它為 “新架構” 過於誇大,我僅將其看作是一種設計模式,一個討論的起點,我們只是簡單地給它起一個名字,以便我們能更好地討論它。 -這些想法不是我的;它們是很多人的思想的融合,這些思想非常值得我們學習。尤其是,以Oz【28】和Juttle【29】為代表的資料流語言,以Elm【30,31】為代表的**函式式響應式程式設計(functional reactive programming, FRP)**,以Bloom【32】為代表的邏輯程式語言。在這一語境中的術語**分拆(unbundling)** 是由Jay Kreps 提出的【7】。 +這些想法不是我的;它們是很多人的思想的融合,這些思想非常值得我們學習。尤其是,以 Oz【28】和 Juttle【29】為代表的資料流語言,以 Elm【30,31】為代表的 **函式式響應式程式設計(functional reactive programming, FRP)**,以 Bloom【32】為代表的邏輯程式語言。在這一語境中的術語 **分拆(unbundling)** 是由 Jay Kreps 提出的【7】。 -即使是**電子表格**也在資料流程式設計能力上甩開大多數主流程式語言幾條街【33】。在電子表格中,可以將公式放入一個單元格中(例如,對另一列中的單元格求和),並且只要公式的任何輸入發生變更,公式的結果都會自動重新計算。這正是我們在資料系統層次所需要的:當資料庫中的記錄發生變更時,我們希望自動更新該記錄的任何索引,並且自動重新整理依賴於記錄的任何快取檢視或聚合。你不必擔心這種重新整理如何發生的技術細節,但能夠簡單地相信它可以正常工作。 +即使是 **電子表格** 也在資料流程式設計能力上甩開大多數主流程式語言幾條街【33】。在電子表格中,可以將公式放入一個單元格中(例如,對另一列中的單元格求和),並且只要公式的任何輸入發生變更,公式的結果都會自動重新計算。這正是我們在資料系統層次所需要的:當資料庫中的記錄發生變更時,我們希望自動更新該記錄的任何索引,並且自動重新整理依賴於記錄的任何快取檢視或聚合。你不必擔心這種重新整理如何發生的技術細節,但能夠簡單地相信它可以正常工作。 -因此,我認為絕大多數資料系統仍然可以從VisiCalc在1979年已經具備的功能中學習【34】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某一種特定的語言、框架或工具來開發所有軟體是不切實際的。 +因此,我認為絕大多數資料系統仍然可以從 VisiCalc 在 1979 年已經具備的功能中學習【34】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某一種特定的語言、框架或工具來開發所有軟體是不切實際的。 在本節中,我將詳細介紹這些想法,並探討一些圍繞分拆資料庫和資料流的想法構建應用的方法。 @@ -254,171 +254,171 @@ Unix和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix 當一個數據集衍生自另一個數據集時,它會經歷某種轉換函式。例如: -* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用B樹或SSTable索引,按鍵排序,如[第三章](ch3.md)所述)。 +* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第三章](ch3.md) 所述)。 * 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。 * 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。 -* 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道UI中引用的欄位;UI中的變更可能需要更新快取填充方式的定義,並重建快取。 +* 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位;UI 中的變更可能需要更新快取填充方式的定義,並重建快取。 -用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過`CREATE INDEX`來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更復雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。 +用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更復雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。 -當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱“[傳遞事件流](ch11.md#傳遞事件流)”)。 +當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](ch11.md#傳遞事件流)”)。 #### 應用程式碼和狀態的分離 理論上,資料庫可以是任意應用程式碼的部署環境,就如同作業系統一樣。然而實踐中它們對這一目標適配的很差。它們不滿足現代應用開發的要求,例如依賴和軟體包管理、版本控制、滾動升級、可演化性、監控、指標、對網路服務的呼叫以及與外部系統的整合。 -另一方面,Mesos,YARN,Docker,Kubernetes等部署和叢集管理工具專為執行應用程式碼而設計。透過專注於做好一件事情,他們能夠做得比將資料庫作為其眾多功能之一執行使用者定義的功能要好得多。 +另一方面,Mesos、YARN、Docker、Kubernetes 等部署和叢集管理工具專為執行應用程式碼而設計。透過專注於做好一件事情,他們能夠做得比將資料庫作為其眾多功能之一執行使用者定義的功能要好得多。 我認為讓系統的某些部分專門用於持久資料儲存並讓其他部分專門執行應用程式程式碼是有意義的。這兩者可以在保持獨立的同時互動。 -現在大多數Web應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信**教會(Church)** 與**國家(state)** 的分離”【37】 [^i] +現在大多數 Web 應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信 **教會(Church)** 與 **國家(state)** 的分離”【37】 [^i] -[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。 在這裡,Church指代的是數學家的阿隆佐·邱奇,他創立了lambda演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。 lambda演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與Church的工作是分離的。 +[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。 在這裡,Church 指代的是數學家的阿隆佐・邱奇,他創立了 lambda 演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。 lambda 演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與 Church 的工作是分離的。 -在這個典型的Web應用模型中,資料庫充當一種可以透過網路同步訪問的可變共享變數。應用程式可以讀取和更新變數,而資料庫負責維持它的永續性,提供一些諸如併發控制和容錯的功能。 +在這個典型的 Web 應用模型中,資料庫充當一種可以透過網路同步訪問的可變共享變數。應用程式可以讀取和更新變數,而資料庫負責維持它的永續性,提供一些諸如併發控制和容錯的功能。 -但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為**觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能)。 +但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為 **觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能)。 -資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(請參閱“[變更流的API支援](ch11.md#變更流的API支援)”)。 +資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)”)。 #### 資料流:應用程式碼與狀態變化的互動 從資料流的角度思考應用程式,意味著重新協調應用程式碼和狀態管理之間的關係。我們不再將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。 -我們在“[資料庫與流](ch11.md#資料庫與流)”中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如Actor的訊息傳遞系統(請參閱“[訊息傳遞中的資料流 ](ch4.md#訊息傳遞中的資料流)”)也具有響應事件的概念。早在20世紀80年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。 +我們在 “[資料庫與流](ch11.md#資料庫與流)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。 如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取、全文搜尋索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。 -需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱“[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”): +需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”): -* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如“[確認與重新傳遞](ch11.md#確認與重新傳遞)”中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱“[保持系統同步](ch11.md#保持系統同步)”)。 -* 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多Actor系統預設在記憶體中維護Actor的狀態和訊息,所以如果執行Actor的機器崩潰,狀態和訊息就會丟失。 +* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](ch11.md#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](ch11.md#保持系統同步)”)。 +* 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。 穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。 -這些應用程式碼可以執行任意處理,包括資料庫內建衍生函式通常不提供的功能。就像透過管道連結的Unix工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。 +這些應用程式碼可以執行任意處理,包括資料庫內建衍生函式通常不提供的功能。就像透過管道連結的 Unix 工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。 #### 流處理器和服務 -當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如REST API)進行通訊的**服務**(service,請參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。 +當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API)進行通訊的 **服務**(service,請參閱 “[服務中的資料流:REST 與 RPC](ch4.md#服務中的資料流:REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。 -在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求/響應式互動。 +在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求 / 響應式互動。 -除了在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】: +除了在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】: 1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。 2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。 -第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱“[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。 +第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱 “[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。 [^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。 但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。 -連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱“[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。 +連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。 訂閱變更流,而不是在需要時查詢當前狀態,使我們更接近類似電子表格的計算模型:當某些資料發生變更時,依賴於此的所有衍生資料都可以快速更新。還有很多未解決的問題,例如關於時間相關連線等問題,但我認為圍繞資料流構建應用的想法是一個非常有希望的方向。 ### 觀察衍生資料狀態 -在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為**寫路徑(write path)**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖12-1](../img/fig12-1.png)顯示了一個更新搜尋索引的例子。 +在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為 **寫路徑(write path)**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖 12-1](../img/fig12-1.png) 顯示了一個更新搜尋索引的例子。 ![](../img/fig12-1.png) -**圖12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)** +**圖 12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)** -但你為什麼一開始就要建立衍生資料集?很可能是因為你想在以後再次查詢它。這就是**讀路徑(read path)**:當服務使用者請求時,你需要從衍生資料集中讀取,也許還要對結果進行一些額外處理,然後構建給使用者的響應。 +但你為什麼一開始就要建立衍生資料集?很可能是因為你想在以後再次查詢它。這就是 **讀路徑(read path)**:當服務使用者請求時,你需要從衍生資料集中讀取,也許還要對結果進行一些額外處理,然後構建給使用者的響應。 總而言之,寫路徑和讀路徑涵蓋了資料的整個旅程,從收集資料開始,到使用資料結束(可能是由另一個人)。寫路徑是預計算過程的一部分 —— 即,一旦資料進入,即刻完成,無論是否有人需要看它。讀路徑是這個過程中只有當有人請求時才會發生的部分。如果你熟悉函數語言程式設計語言,則可能會注意到寫路徑類似於立即求值,讀路徑類似於惰性求值。 -如[圖12-1](../img/fig12-1.png)所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。 +如 [圖 12-1](../img/fig12-1.png) 所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。 #### 物化檢視和快取 -全文搜尋索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND運算子)的文件,或者每個單詞(OR運算子)的任何同義詞。 +全文搜尋索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND 運算子)的文件,或者每個單詞(OR 運算子)的任何同義詞。 -如果沒有索引,搜尋查詢將不得不掃描所有文件(如grep),如果有著大量文件,這樣做的開銷巨大。沒有索引意味著寫入路徑上的工作量較少(沒有要更新的索引),但是在讀取路徑上需要更多工作。 +如果沒有索引,搜尋查詢將不得不掃描所有文件(如 grep),如果有著大量文件,這樣做的開銷巨大。沒有索引意味著寫入路徑上的工作量較少(沒有要更新的索引),但是在讀取路徑上需要更多工作。 -另一方面,可以想象為所有可能的查詢預先計算搜尋結果。在這種情況下,讀路徑上的工作量會減少:不需要布林邏輯,只需查詢查詢結果並返回即可。但寫路徑會更加昂貴:可能的搜尋查詢集合是無限大的,因此預先計算所有可能的搜尋結果將需要無限的時間和儲存空間。那肯定沒戲[^iii]。 +另一方面,可以想象為所有可能的查詢預先計算搜尋結果。在這種情況下,讀路徑上的工作量會減少:不需要布林邏輯,只需查詢查詢結果並返回即可。但寫路徑會更加昂貴:可能的搜尋查詢集合是無限大的,因此預先計算所有可能的搜尋結果將需要無限的時間和儲存空間。那肯定沒戲 [^iii]。 [^iii]: 假設一個有限的語料庫,那麼返回非空搜尋結果的搜尋查詢集合是有限的。然而,它是與語料庫中的術語數量呈指數關係,這仍是一個壞訊息。 -另一種選擇是預先計算一組固定的最常見查詢的搜尋結果,以便可以快速提供它們而無需轉到索引。不常見的查詢仍然可以透過索引來提供服務。這通常被稱為常見查詢的**快取(cache)**,儘管我們也可以稱之為**物化檢視(materialized view)**,因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。 +另一種選擇是預先計算一組固定的最常見查詢的搜尋結果,以便可以快速提供它們而無需轉到索引。不常見的查詢仍然可以透過索引來提供服務。這通常被稱為常見查詢的 **快取(cache)**,儘管我們也可以稱之為 **物化檢視(materialized view)**,因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。 -從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類grep掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。 +從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類 grep 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。 -在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在“[描述負載](ch1.md#描述負載)”中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在500頁之後,我們已經繞回了起點! +在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](ch1.md#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點! #### 有狀態、可離線的客戶端 我發現寫路徑和讀路徑之間的邊界很有趣,因為我們可以試著改變這個邊界,並探討這種改變的實際意義。我們來看看不同上下文中的這一想法。 -過去二十年來,Web應用的火熱讓我們對應用開發作出了一些很容易視作理所當然的假設。具體來說就是,客戶端/伺服器模型 —— 客戶端大多是無狀態的,而伺服器擁有資料的權威 —— 已經普遍到我們幾乎忘掉了還有其他任何模型的存在。但是技術在不斷地發展,我認為不時地質疑現狀非常重要。 +過去二十年來,Web 應用的火熱讓我們對應用開發作出了一些很容易視作理所當然的假設。具體來說就是,客戶端 / 伺服器模型 —— 客戶端大多是無狀態的,而伺服器擁有資料的權威 —— 已經普遍到我們幾乎忘掉了還有其他任何模型的存在。但是技術在不斷地發展,我認為不時地質疑現狀非常重要。 -傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的“單頁面”JavaScript Web應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及Web瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。 +傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。 -這些不斷變化的功能重新引發了對**離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱“[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。 +這些不斷變化的功能重新引發了對 **離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。 -當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為**伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本【27】。 +當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本【27】。 #### 將狀態變更推送給客戶端 -在典型的網頁中,如果你在Web瀏覽器中載入頁面,並且隨後伺服器上的資料發生變更,則瀏覽器在重新載入頁面之前對此一無所知。瀏覽器只能在一個時間點讀取資料,假設它是靜態的 —— 它不會訂閱來自伺服器的更新。因此裝置上的狀態是陳舊的快取,除非你顯式輪詢變更否則不會更新。(像RSS這樣基於HTTP的Feed訂閱協議實際上只是一種基本的輪詢形式) +在典型的網頁中,如果你在 Web 瀏覽器中載入頁面,並且隨後伺服器上的資料發生變更,則瀏覽器在重新載入頁面之前對此一無所知。瀏覽器只能在一個時間點讀取資料,假設它是靜態的 —— 它不會訂閱來自伺服器的更新。因此裝置上的狀態是陳舊的快取,除非你顯式輪詢變更否則不會更新。(像 RSS 這樣基於 HTTP 的 Feed 訂閱協議實際上只是一種基本的輪詢形式) -最近的協議已經超越了HTTP的基本請求/響應模式:服務端傳送的事件(EventSource API)和WebSockets提供了通訊通道,透過這些通道,Web瀏覽器可以與伺服器保持開啟的TCP連線,只要瀏覽器仍然連線著,伺服器就能主動向瀏覽器推送資訊。這為伺服器提供了主動通知終端使用者客戶端的機會,伺服器能告知客戶端其本地儲存狀態的任何變化,從而減少客戶端狀態的陳舊程度。 +最近的協議已經超越了 HTTP 的基本請求 / 響應模式:服務端傳送的事件(EventSource API)和 WebSockets 提供了通訊通道,透過這些通道,Web 瀏覽器可以與伺服器保持開啟的 TCP 連線,只要瀏覽器仍然連線著,伺服器就能主動向瀏覽器推送資訊。這為伺服器提供了主動通知終端使用者客戶端的機會,伺服器能告知客戶端其本地儲存狀態的任何變化,從而減少客戶端狀態的陳舊程度。 用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置【43】。 -這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在“[消費者偏移量](ch11.md#消費者偏移量)”中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。 +這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](ch11.md#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。 #### 端到端的事件流 -最近用於開發有狀態的客戶端與使用者介面的工具,例如如Elm語言【30】和Facebook的React,Flux和Redux工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱“[事件溯源](ch11.md#事件溯源)”)。 +最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](ch11.md#事件溯源)”)。 -將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過**端到端(end-to-end)** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。 +將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 **端到端(end-to-end)** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。 -一些應用(如即時訊息傳遞與線上遊戲)已經具有這種“實時”架構(在低延遲互動的意義上,不是在“[響應時間保證](ch8.md#響應時間保證)”中的意義上)。但我們為什麼不用這種方式構建所有的應用? +一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “實時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](ch8.md#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用? -挑戰在於,關於無狀態客戶端和請求/響應互動的假設已經根深蒂固地植入在在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱“[變更流的API支援](ch11.md#變更流的API支援)” )。 +挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)” )。 -為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求/響應互動轉向釋出/訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望你對訂閱變更的選項留有印象,而不只是查詢當前狀態。 +為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望你對訂閱變更的選項留有印象,而不只是查詢當前狀態。 #### 讀也是事件 我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。 -在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱“[流連線](ch11.md#流連線)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。 +在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](ch11.md#流連線)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。 我願意進一步思考這個想法。正如到目前為止所討論的那樣,對儲存的寫入是透過事件日誌進行的,而讀取是臨時的網路請求,直接流向儲存著待查資料的節點。這是一個合理的設計,但不是唯一可行的設計。也可以將讀取請求表示為事件流,並同時將讀事件與寫事件送往流處理器;流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。 -當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱“[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)“)。 +當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](ch10.md#Reduce側連線與分組)“)。 服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元,然後請求馬上就被忘掉了;而一個訂閱請求,則是與連線另一側過去與未來事件的持久化連線。 記錄讀取事件的日誌可能對於追蹤整個系統中的因果關係與資料來源也有好處:它可以讓你重現出當用戶做出特定決策之前看見了什麼。例如在網商中,向客戶顯示的預測送達日期與庫存狀態,可能會影響他們是否選擇購買一件商品【4】。要分析這種聯絡,則需要記錄使用者查詢運輸與庫存狀態的結果。 -將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱“[排序事件以捕獲因果關係](#排序事件以捕獲因果關係)”),但會產生額外的儲存與I/O成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。 +將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱 “[排序事件以捕獲因果關係](#排序事件以捕獲因果關係)”),但會產生額外的儲存與 I/O 成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。 #### 多分割槽資料處理 對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行復雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。 -Storm的分散式RPC功能支援這種使用模式(請參閱“[訊息傳遞和RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特URL的人數 —— 即,發推包含該URL的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。 +Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。 -這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者IP地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。 +這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。 -MPP資料庫的內部查詢執行圖有著類似的特徵(請參閱“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。 +MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。 ## 將事情做正確 對於只讀取資料的無狀態服務,出問題也沒什麼大不了的:你可以修復該錯誤並重啟服務,而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了:它們被設計為永遠記住事物(或多或少),所以如果出現問題,這種(錯誤的)效果也將潛在地永遠持續下去,這意味著它們需要更仔細的思考【50】。 -我們希望構建可靠且**正確**的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱“[弱隔離級別](ch7.md#弱隔離級別)”)。 +我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。 -事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更復雜的語義(例如,請參閱“[無主複製](ch5.md#無主複製)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(請參閱“[一致性](ch7.md#一致性)”和[第九章](ch9.md))。有些人斷言我們應當為了高可用而“擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。 +事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更復雜的語義(例如,請參閱 “[無主複製](ch5.md#無主複製)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(請參閱 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。 對於如此重要的話題,我們的理解,以及我們的工程方法卻是驚人地薄弱。例如,確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常,並且沒有錯誤,但在要求更高的情況下卻會出現許多微妙的錯誤。 -例如,Kyle Kingsbury的Jepsen實驗【53】標出了一些產品聲稱的安全保證與其在網路問題與崩潰時的實際行為之間的明顯差異。即使像資料庫這樣的基礎設施產品沒有問題,應用程式碼仍然需要正確使用它們提供的功能才行,如果配置很難理解,這是很容易出錯的(在這種情況下指的是弱隔離級別,法定人數配置等)。 +例如,Kyle Kingsbury 的 Jepsen 實驗【53】標出了一些產品聲稱的安全保證與其在網路問題與崩潰時的實際行為之間的明顯差異。即使像資料庫這樣的基礎設施產品沒有問題,應用程式碼仍然需要正確使用它們提供的功能才行,如果配置很難理解,這是很容易出錯的(在這種情況下指的是弱隔離級別,法定人數配置等)。 如果你的應用可以容忍偶爾的崩潰,以及以不可預料的方式損壞或丟失資料,那生活就要簡單得多,而你可能只要雙手合十念阿彌陀佛,期望佛祖能保佑最好的結果。另一方面,如果你需要更強的正確性保證,那麼可序列化與原子提交就是久經考驗的方法,但它們是有代價的:它們通常只在單個數據中心中工作(這就排除了地理位置分散的架構),並限制了系統能夠實現的規模與容錯特性。 @@ -426,27 +426,27 @@ MPP資料庫的內部查詢執行圖有著類似的特徵(請參閱“[Hadoop ### 資料庫的端到端原則 -僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個Bug,導致它寫入不正確的資料,或者從資料庫中刪除資料,那麼可序列化的事務也救不了你。 +僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 Bug,導致它寫入不正確的資料,或者從資料庫中刪除資料,那麼可序列化的事務也救不了你。 -這個例子可能看起來很無聊,但值得認真對待:應用會出Bug,而人也會犯錯誤。我在“[狀態、流和不變性](ch11.md#狀態、流和不變性)”中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。 +這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug,而人也會犯錯誤。我在 “[狀態、流和不變性](ch11.md#狀態、流和不變性)” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。 雖然不變性很有用,但它本身並非萬靈藥。讓我們來看一個可能發生的、非常微妙的資料損壞案例。 #### 正好執行一次操作 -在“[容錯](ch11.md#容錯)”中,我們見到了**恰好一次**(或**等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。 +在 “[容錯](ch11.md#容錯)” 中,我們見到了 **恰好一次**(或 **等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。 處理兩次是資料損壞的一種形式:為同樣的服務向客戶收費兩次(收費太多)或增長計數器兩次(誇大指標)都不是我們想要的。在這種情況下,恰好一次意味著安排計算,使得最終效果與沒有發生錯誤的情況一樣,即使操作實際上因為某種錯誤而重試。我們先前討論過實現這一目標的幾種方法。 -最有效的方法之一是使操作**冪等**(idempotent,請參閱“[冪等性](ch11.md#冪等性)”);即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作ID集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱“[領導者和鎖](ch8.md#領導者和鎖)”)。 +最有效的方法之一是使操作 **冪等**(idempotent,請參閱 “[冪等性](ch11.md#冪等性)”);即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “[領導者和鎖](ch8.md#領導者和鎖)”)。 #### 抑制重複 -除了流處理之外,其他許多地方也需要抑制重複的模式。例如,TCP使用了資料包上的序列號,以便接收方可以將它們正確排序,並確定網路上是否有資料包丟失或重複。在將資料交付應用前,TCP協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。 +除了流處理之外,其他許多地方也需要抑制重複的模式。例如,TCP 使用了資料包上的序列號,以便接收方可以將它們正確排序,並確定網路上是否有資料包丟失或重複。在將資料交付應用前,TCP 協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。 -但是,這種重複抑制僅適用於單條TCP連線的場景中。假設TCP連線是一個客戶端與資料庫的連線,並且它正在執行[例12-1]()中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個TCP連線上傳送的)。如果客戶端在傳送`COMMIT`之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖8-1](../img/fig8-1.png))。 +但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 [例 12-1]() 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 `COMMIT` 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖 8-1](../img/fig8-1.png))。 -**例12-1 資金從一個賬戶到另一個賬戶的非冪等轉移** +**例 12-1 資金從一個賬戶到另一個賬戶的非冪等轉移** ```sql BEGIN TRANSACTION; @@ -455,20 +455,20 @@ BEGIN TRANSACTION; COMMIT; ``` -客戶端可以重連到資料庫並重試事務,但現在已經處於TCP重複抑制的範圍之外了。因為[例12-1]()中的事務不是冪等的,可能會發生轉了\$22而不是期望的\$11。因此,儘管[例12-1]()是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事【3】。 +客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 12-1]() 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此,儘管 [例 12-1]() 是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事【3】。 -兩階段提交(請參閱“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”)協議會破壞TCP連線與事務之間的1:1對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。 +兩階段提交(請參閱 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。 -即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是Web瀏覽器,則它可能會使用HTTP POST請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了POST,但卻在能夠從伺服器接收響應之前沒了訊號。 +即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST,但卻在能夠從伺服器接收響應之前沒了訊號。 -在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。 Web瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選“是”,因為他們希望操作發生(Post/Redirect/Get模式【54】可以避免在正常操作中出現此警告訊息,但POST請求超時就沒辦法了)。從Web伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。 +在這種情況下,可能會向用戶顯示錯誤訊息,而他們可能會手動重試。 Web 瀏覽器警告說,“你確定要再次提交這個表單嗎?” —— 使用者選 “是”,因為他們希望操作發生(Post/Redirect/Get 模式【54】可以避免在正常操作中出現此警告訊息,但 POST 請求超時就沒辦法了)。從 Web 伺服器的角度來看,重試是一個獨立的請求;從資料庫的角度來看,這是一個獨立的事務。通常的除重機制無濟於事。 #### 操作識別符號 -要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮**端到端(end-to-end)** 的請求流。 -例如,你可以為操作生成一個唯一的識別符號(例如UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有表單相關欄位的雜湊來生成操作ID 【3】。如果Web瀏覽器提交了兩次POST請求,這兩個請求將具有相同的操作ID。然後,你可以將該操作ID一路傳遞到資料庫,並檢查你是否曾經使用給定的ID執行過一個操作,如[例12-2]()中所示。 +要在通過幾跳的網路通訊上使操作具有冪等性,僅僅依賴資料庫提供的事務機制是不夠的 —— 你需要考慮 **端到端(end-to-end)** 的請求流。 +例如,你可以為操作生成一個唯一的識別符號(例如 UUID),並將其作為隱藏表單欄位包含在客戶端應用中,或透過計算所有表單相關欄位的雜湊來生成操作 ID 【3】。如果 Web 瀏覽器提交了兩次 POST 請求,這兩個請求將具有相同的操作 ID。然後,你可以將該操作 ID 一路傳遞到資料庫,並檢查你是否曾經使用給定的 ID 執行過一個操作,如 [例 12-2]() 中所示。 -**例12-2 使用唯一ID來抑制重複請求** +**例 12-2 使用唯一 ID 來抑制重複請求** ```sql ALTER TABLE requests ADD UNIQUE (request_id); @@ -482,132 +482,132 @@ BEGIN TRANSACTION; COMMIT; ``` -[例12-2]()依賴於`request_id`列上的唯一約束。如果一個事務嘗試插入一個已經存在的ID,那麼`INSERT`失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”中討論過,應用級別的**檢查-然後-插入**可能會在不可序列化的隔離下失敗)。 +[例 12-2]() 依賴於 `request_id` 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID,那麼 `INSERT` 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)” 中討論過,應用級別的 **檢查 - 然後 - 插入** 可能會在不可序列化的隔離下失敗)。 -除了抑制重複的請求之外,[例12-2]()中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱“[事件溯源](ch11.md#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求ID來強制執行。 +除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](ch11.md#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。 #### 端到端原則 -抑制重複事務的這種情況只是一個更普遍的原則的一個例子,這個原則被稱為**端到端原則(end-to-end argument)**,它在1984年由Saltzer,Reed和Clark闡述【55】: +抑制重複事務的這種情況只是一個更普遍的原則的一個例子,這個原則被稱為 **端到端原則(end-to-end argument)**,它在 1984 年由 Saltzer、Reed 和 Clark 闡述【55】: > 只有在通訊系統兩端應用的知識與幫助下,所討論的功能才能完全地正確地實現。因而將這種被質疑的功能作為通訊系統本身的功能是不可能的(有時,通訊系統可以提供這種功能的不完備版本,可能有助於提高效能)。 > -在我們的例子中**所討論的功能**是重複抑制。我們看到TCP在TCP連線層次抑制了重複的資料包,一些流處理器在訊息處理層次提供了所謂的恰好一次語義,但這些都無法阻止當一個請求超時時,使用者親自提交重複的請求。TCP,資料庫事務,以及流處理器本身並不能完全排除這些重複。解決這個問題需要一個端到端的解決方案:從終端使用者的客戶端一路傳遞到資料庫的事務識別符號。 +在我們的例子中 **所討論的功能** 是重複抑制。我們看到 TCP 在 TCP 連線層次抑制了重複的資料包,一些流處理器在訊息處理層次提供了所謂的恰好一次語義,但這些都無法阻止當一個請求超時時,使用者親自提交重複的請求。TCP,資料庫事務,以及流處理器本身並不能完全排除這些重複。解決這個問題需要一個端到端的解決方案:從終端使用者的客戶端一路傳遞到資料庫的事務識別符號。 -端到端原則也適用於檢查資料的完整性:乙太網,TCP和TLS中內建的校驗和可以檢測網路中資料包的損壞情況,但是它們無法檢測到由連線兩端傳送/接收軟體中Bug導致的損壞。或資料儲存所在磁碟上的損壞。如果你想捕獲資料所有可能的損壞來源,你也需要端到端的校驗和。 +端到端原則也適用於檢查資料的完整性:乙太網,TCP 和 TLS 中內建的校驗和可以檢測網路中資料包的損壞情況,但是它們無法檢測到由連線兩端傳送 / 接收軟體中 Bug 導致的損壞。或資料儲存所在磁碟上的損壞。如果你想捕獲資料所有可能的損壞來源,你也需要端到端的校驗和。 -類似的原則也適用於加密【55】:家庭WiFi網路上的密碼可以防止人們竊聽你的WiFi流量,但無法阻止網際網路上其他地方攻擊者的窺探;客戶端與伺服器之間的TLS/SSL可以阻擋網路攻擊者,但無法阻止惡意伺服器。只有端到端的加密和認證可以防止所有這些事情。 +類似的原則也適用於加密【55】:家庭 WiFi 網路上的密碼可以防止人們竊聽你的 WiFi 流量,但無法阻止網際網路上其他地方攻擊者的窺探;客戶端與伺服器之間的 TLS/SSL 可以阻擋網路攻擊者,但無法阻止惡意伺服器。只有端到端的加密和認證可以防止所有這些事情。 -儘管低層級的功能(TCP重複抑制,乙太網校驗和,WiFi加密)無法單獨提供所需的端到端功能,但它們仍然很有用,因為它們能降低較高層級出現問題的可能性。例如,如果我們沒有TCP來將資料包排成正確的順序,那麼HTTP請求通常就會被攪爛。我們只需要記住,低級別的可靠性功能本身並不足以確保端到端的正確性。 +儘管低層級的功能(TCP 重複抑制、乙太網校驗和、WiFi 加密)無法單獨提供所需的端到端功能,但它們仍然很有用,因為它們能降低較高層級出現問題的可能性。例如,如果我們沒有 TCP 來將資料包排成正確的順序,那麼 HTTP 請求通常就會被攪爛。我們只需要記住,低級別的可靠性功能本身並不足以確保端到端的正確性。 #### 在資料系統中應用端到端思考 這將我帶回最初的論點:僅僅因為應用使用了提供相對較強安全屬性的資料系統,例如可序列化的事務,並不意味著應用的資料就不會丟失或損壞了。應用本身也需要採取端到端的措施,例如除重。 -這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如TCP中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。 +這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如 TCP 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。 -長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如[第七章](ch7.md)導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。 +長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](ch7.md) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。 -事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。 +事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。 出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。 ### 強制約束 -讓我們思考一下在[分拆資料庫](#分拆資料庫)上下文中的**正確性(correctness)**。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求ID實現。那麼其他型別的約束呢? +讓我們思考一下在 [分拆資料庫](#分拆資料庫) 上下文中的 **正確性(correctness)**。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求 ID 實現。那麼其他型別的約束呢? -我們先來特別關注一下**唯一性約束** —— 例如我們在[例12-2]()中所依賴的約束。在“[約束和唯一性保證](ch9.md#約束和唯一性保證)”中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。 +我們先來特別關注一下 **唯一性約束** —— 例如我們在 [例 12-2]() 中所依賴的約束。在 “[約束和唯一性保證](ch9.md#約束和唯一性保證)” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。 其他型別的約束也非常類似:例如,確保帳戶餘額永遠不會變為負數,確保不會超賣庫存,或者會議室沒有重複的預訂。執行唯一性約束的技術通常也可以用於這些約束。 #### 唯一性約束需要達成共識 -在[第九章](ch9.md)中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。 +在 [第九章](ch9.md) 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。 -達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱“[單領導者複製與共識](ch9.md#單領導者複製與共識)”)。 +達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “[單領導者複製與共識](ch9.md#單領導者複製與共識)”)。 -唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求ID確保唯一性(如[例12-2]()所示),你可以確保所有具有相同請求ID的請求都被路由到同一分割槽(請參閱[第六章](ch6.md))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。 +唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 [例 12-2]() 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 [第六章](ch6.md))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。 -但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱“[實現線性一致的系統](ch9.md#實現線性一致的系統)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。 +但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “[實現線性一致的系統](ch9.md#實現線性一致的系統)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。 #### 基於日誌訊息傳遞中的唯一性 -日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為**全序廣播(total order boardcast)** 並且等價於共識(請參閱“[全序廣播](ch9.md#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。 +日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為 **全序廣播(total order boardcast)** 並且等價於共識(請參閱 “[全序廣播](ch9.md#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。 -流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱“[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”)。因此,如果日誌是按需要確保唯一的值做的分割槽,則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】: +流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”)。因此,如果日誌是按需要確保唯一的值做的分割槽,則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】: 1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。 2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。 3. 請求使用者名稱的客戶端監視輸出流,等待與其請求相對應的成功或拒絕訊息。 -該演算法基本上與“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。 +該演算法基本上與 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。 -該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如“[什麼是衝突?](ch5.md#什麼是衝突?)”與“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與Bayou在90年代開創的方法類似【58】。 +該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “[什麼是衝突?](ch5.md#什麼是衝突?)” 與 “[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似【58】。 #### 多分割槽請求處理 -當涉及多個分割槽時,確保操作以原子方式執行且同時滿足約束就變得很有趣了。在[例12-2]()中,可能有三個分割槽:一個包含請求ID,一個包含收款人賬戶,另一個包含付款人賬戶。沒有理由把這三種東西放入同一個分割槽,因為它們都是相互獨立的。 +當涉及多個分割槽時,確保操作以原子方式執行且同時滿足約束就變得很有趣了。在 [例 12-2]() 中,可能有三個分割槽:一個包含請求 ID,一個包含收款人賬戶,另一個包含付款人賬戶。沒有理由把這三種東西放入同一個分割槽,因為它們都是相互獨立的。 在資料庫的傳統方法中,執行此事務需要跨全部三個分割槽進行原子提交,就這些分割槽上的所有其他事務而言,這實質上是將該事務嵌入一個全序。而這樣就要求跨分割槽協調,不同的分割槽無法再獨立地進行處理,因此吞吐量很可能會受到影響。 但事實證明,使用分割槽日誌可以達到等價的正確性而無需原子提交: -1. 從賬戶A向賬戶B轉賬的請求由客戶端提供一個唯一的請求ID,並按請求ID追加寫入相應日誌分割槽。 -2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶A的借記指令(按A分割槽),收款人B的貸記指令(按B分割槽)。被發出的訊息中會帶有原始的請求ID。 -3. 後續處理器消費借記/貸記指令流,按照請求ID除重,並將變更應用至賬戶餘額。 +1. 從賬戶 A 向賬戶 B 轉賬的請求由客戶端提供一個唯一的請求 ID,並按請求 ID 追加寫入相應日誌分割槽。 +2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。 +3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。 -步驟1和步驟2是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱“[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。 +步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。 -如果流處理器在步驟2中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟3中的處理器可以使用端到端請求ID輕鬆地對其除重。 +如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。 -如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟1中的請求日誌中。 +如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。 -透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在“[多分割槽資料處理](#多分割槽資料處理)”中討論的想法類似(也請參閱“[併發控制](ch11.md#併發控制)”)。 +透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](ch11.md#併發控制)”)。 ### 及時性與完整性 -事務的一個便利屬性是,它們通常是線性一致的(請參閱“[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。 +事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。 -當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在“[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)”一節中檢查唯一性約束時所做的事情。 +當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)” 一節中檢查唯一性約束時所做的事情。 在這個例子中,唯一性檢查的正確性不取決於訊息傳送者是否等待結果。等待的目的僅僅是同步通知傳送者唯一性檢查是否成功。但該通知可以與訊息處理的結果相解耦。 -更一般地來講,我認為術語**一致性(consistency)** 這個術語混淆了兩個值得分別考慮的需求: +更一般地來講,我認為術語 **一致性(consistency)** 這個術語混淆了兩個值得分別考慮的需求: * 及時性(Timeliness) - 及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。 + 及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。 - CAP定理(請參閱“[線性一致性的代價](ch9.md#線性一致性的代價)”)使用**線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像**寫後讀**這樣及時性更弱的一致性也很有用(請參閱“[讀己之寫](ch5.md#讀己之寫)”)。 + CAP 定理(請參閱 “[線性一致性的代價](ch9.md#線性一致性的代價)”)使用 **線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](ch5.md#讀己之寫)”)。 * 完整性(Integrity) - 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱“[從事件日誌中派生出當前狀態](ch11.md#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。 + 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](ch11.md#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。 - 如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在ACID事務的上下文中(請參閱“[ACID的含義](ch7.md#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。 + 如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](ch7.md#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。 口號形式:違反及時性,“最終一致性”;違反完整性,“永無一致性”。 我斷言在大多數應用中,完整性比及時性重要得多。違反及時性可能令人困惑與討厭,但違反完整性的結果可能是災難性的。 -例如在你的信用卡對賬單上,如果某一筆過去24小時內完成的交易尚未出現並不令人奇怪 —— 這些系統有一定的滯後是正常的。我們知道銀行是非同步核算與敲定交易的,這裡的及時性並不是非常重要【3】。但果當期對賬單餘額與上期對賬單餘額加交易總額對不上(求和錯誤),或者出現一筆向你收費但未向商家付款的交易(消失的錢),那實在是太糟糕了。這樣的問題就違背了系統的完整性。 +例如在你的信用卡對賬單上,如果某一筆過去 24 小時內完成的交易尚未出現並不令人奇怪 —— 這些系統有一定的滯後是正常的。我們知道銀行是非同步核算與敲定交易的,這裡的及時性並不是非常重要【3】。但果當期對賬單餘額與上期對賬單餘額加交易總額對不上(求和錯誤),或者出現一筆向你收費但未向商家付款的交易(消失的錢),那實在是太糟糕了。這樣的問題就違背了系統的完整性。 #### 資料流系統的正確性 -ACID事務通常既提供及時性(例如線性一致性)也提供完整性保證(例如原子提交)。因此如果你從ACID事務的角度來看待應用的正確性,那麼及時性與完整性的區別是無關緊要的。 +ACID 事務通常既提供及時性(例如線性一致性)也提供完整性保證(例如原子提交)。因此如果你從 ACID 事務的角度來看待應用的正確性,那麼及時性與完整性的區別是無關緊要的。 另一方面,對於在本章中討論的基於事件的資料流系統而言,它們的一個有趣特性就是將及時性與完整性分開。在非同步處理事件流時不能保證及時性,除非你顯式構建一個在返回之前明確等待特定訊息到達的消費者。但完整性實際上才是流處理系統的核心。 -**恰好一次**或**等效一次**語義(請參閱“[容錯](ch11.md#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。 +**恰好一次** 或 **等效一次** 語義(請參閱 “[容錯](ch11.md#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。 正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們有潛力達到與後者相當的正確性,同時還具備好得多的效能與運維穩健性。為了達成這種正確性,我們組合使用了多種機制: -* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱“[事件溯源](ch11.md#事件溯源)”)。 -* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱“[真的序列執行](ch7.md#真的序列執行)”和“[應用程式碼作為衍生函式](ch12.md#應用程式碼作為衍生函式)”) -* 將客戶端生成的請求ID傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。 -* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱“[不可變事件的優點](ch11.md#不可變事件的優點)”) +* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](ch11.md#事件溯源)”)。 +* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](ch7.md#真的序列執行)” 和 “[應用程式碼作為衍生函式](ch12.md#應用程式碼作為衍生函式)”) +* 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。 +* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](ch11.md#不可變事件的優點)”) 這種機制組合在我看來,是未來構建容錯應用的一個非常有前景的方向。 @@ -617,16 +617,16 @@ ACID事務通常既提供及時性(例如線性一致性)也提供完整性 然而另一個需要了解的事實是,許多真實世界的應用實際上可以擺脫這種形式,接受弱得多的唯一性: -* 如果兩個人同時註冊了相同的使用者名稱或預訂了相同的座位,你可以給其中一個人發訊息道歉,並要求他們換一個不同的使用者名稱或座位。這種糾正錯誤的變化被稱為**補償性事務(compensating transaction)**【59,60】。 +* 如果兩個人同時註冊了相同的使用者名稱或預訂了相同的座位,你可以給其中一個人發訊息道歉,並要求他們換一個不同的使用者名稱或座位。這種糾正錯誤的變化被稱為 **補償性事務(compensating transaction)**【59,60】。 * 如果客戶訂購的物品多於倉庫中的物品,你可以下單補倉,併為延誤向客戶道歉,向他們提供折扣。實際上,這麼說吧,如果叉車在倉庫中軋過了你的貨物,剩下的貨物比你想象的要少,那麼你也是得這麼做【61】。因此,既然道歉工作流無論如何已經成為你商業過程中的一部分了,那麼對庫存物品數目新增線性一致的約束可能就沒必要了。 -* 與之類似,許多航空公司都會超賣機票,打著一些旅客可能會錯過航班的算盤;許多旅館也會超賣客房,抱著部分客人可能會取消預訂的期望。在這些情況下,出於商業原因而故意違反了“一人一座”的約束;當需求超過供給的情況出現時,就會進入補償流程(退款、升級艙位/房型、提供隔壁酒店的免費的房間)。即使沒有超賣,為了應對由惡劣天氣或員工罷工導致的航班取消,你還是需要道歉與補償流程 —— 從這些問題中恢復僅僅是商業活動的正常組成部分。 +* 與之類似,許多航空公司都會超賣機票,打著一些旅客可能會錯過航班的算盤;許多旅館也會超賣客房,抱著部分客人可能會取消預訂的期望。在這些情況下,出於商業原因而故意違反了 “一人一座” 的約束;當需求超過供給的情況出現時,就會進入補償流程(退款、升級艙位 / 房型、提供隔壁酒店的免費的房間)。即使沒有超賣,為了應對由惡劣天氣或員工罷工導致的航班取消,你還是需要道歉與補償流程 —— 從這些問題中恢復僅僅是商業活動的正常組成部分。 * 如果有人從賬戶超額取款,銀行可以向他們收取透支費用,並要求他們償還欠款。透過限制每天的提款總額,銀行的風險是有限的。 -在許多商業場景中,臨時違背約束並稍後透過道歉來修復,實際上是可以接受的。道歉的成本各不相同,但通常很低(以金錢或名聲來算):你無法撤回已傳送的電子郵件,但可以傳送一封后續電子郵件進行更正。如果你不小心向信用卡收取了兩次費用,則可以將其中一項收費退款,而代價僅僅是手續費,也許還有客戶的投訴。儘管一旦ATM吐了錢,你無法直接取回,但原則上如果賬戶透支而客戶拒不支付,你可以派催收員收回欠款。 +在許多商業場景中,臨時違背約束並稍後透過道歉來修復,實際上是可以接受的。道歉的成本各不相同,但通常很低(以金錢或名聲來算):你無法撤回已傳送的電子郵件,但可以傳送一封后續電子郵件進行更正。如果你不小心向信用卡收取了兩次費用,則可以將其中一項收費退款,而代價僅僅是手續費,也許還有客戶的投訴。儘管一旦 ATM 吐了錢,你無法直接取回,但原則上如果賬戶透支而客戶拒不支付,你可以派催收員收回欠款。 道歉的成本是否能接受是一個商業決策。如果可以接受的話,在寫入資料之前檢查所有約束的傳統模型反而會帶來不必要的限制,而線性一致性的約束也不是必須的。樂觀寫入,事後檢查可能是一種合理的選擇。你仍然可以在做一些挽回成本高昂的事情前確保有相關的驗證,但這並不意味著寫入資料之前必須先進行驗證。 -這些應用**確實**需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時**並不需要**及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在“[處理寫入衝突](ch5.md#處理寫入衝突)”中討論的衝突解決方法類似。 +這些應用 **確實** 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 **並不需要** 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “[處理寫入衝突](ch5.md#處理寫入衝突)” 中討論的衝突解決方法類似。 #### 無協調資料系統 @@ -635,69 +635,69 @@ ACID事務通常既提供及時性(例如線性一致性)也提供完整性 1. 資料流系統可以維持衍生資料的完整性保證,而無需原子提交、線性一致性或者同步的跨分割槽協調。 2. 雖然嚴格的唯一性約束要求及時性和協調,但許多應用實際上可以接受寬鬆的約束:只要整個過程保持完整性,這些約束可能會被臨時違反並在稍後被修復。 -總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種**無協調(coordination-avoiding)** 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。 +總之這些觀察意味著,資料流系統可以為許多應用提供無需協調的資料管理服務,且仍能給出很強的完整性保證。這種 **無協調(coordination-avoiding)** 的資料系統有著很大的吸引力:比起需要執行同步協調的系統,它們能達到更好的效能與更強的容錯能力【56】。 例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。 -在這種情況下,可序列化事務作為維護衍生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如XA事務,請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。 +在這種情況下,可序列化事務作為維護衍生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如 XA 事務,請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。 另一種審視協調與約束的角度是:它們減少了由於不一致而必須做出的道歉數量,但也可能會降低系統的效能和可用性,從而可能增加由於宕機中斷而需要做出的道歉數量。你不可能將道歉數量減少到零,但可以根據自己的需求尋找最佳平衡點 —— 既不存在太多不一致性,又不存在太多可用性問題。 ### 信任但驗證 -我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的**系統模型**(system model,請參閱“[將系統模型對映到現實世界](ch8.md#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行`fsync`後不會丟失,記憶體中的資料沒有損壞,而CPU的乘法指令總是能返回正確的結果。 +我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 **系統模型**(system model,請參閱 “[將系統模型對映到現實世界](ch8.md#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 `fsync` 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。 -這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情**永遠**不會發生。實際上,這更像是一個概率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。 +這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情 **永遠** 不會發生。實際上,這更像是一個概率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。 -我們已經看到,資料可能會在尚未落盤時損壞(請參閱“[複製與永續性](ch7.md#複製與永續性)”),而網路上的資料損壞有時可能規避了TCP校驗和(請參閱“[弱謊言形式](ch8.md#弱謊言形式)” )。也許我們應當更關注這些事情? +我們已經看到,資料可能會在尚未落盤時損壞(請參閱 “[複製與永續性](ch7.md#複製與永續性)”),而網路上的資料損壞有時可能規避了 TCP 校驗和(請參閱 “[弱謊言形式](ch8.md#弱謊言形式)” )。也許我們應當更關注這些事情? -我過去所從事的一個應用收集了來自客戶端的崩潰報告,我們收到的一些報告,只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能,但是如果有足夠多的裝置執行你的軟體,那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外,一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】(這種技術被稱為**Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。 +我過去所從事的一個應用收集了來自客戶端的崩潰報告,我們收到的一些報告,只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能,但是如果有足夠多的裝置執行你的軟體,那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外,一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】(這種技術被稱為 **Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。 要澄清的是,隨機位翻轉在現代硬體上仍是非常罕見的【64】。我只想指出,它們並沒有超越可能性的範疇,所以值得一些關注。 #### 維護完整性,儘管軟體有Bug -除了這些硬體問題之外,總是存在軟體Bug的風險,這些錯誤不會被較低層次的網路、記憶體或檔案系統校驗和所捕獲。即使廣泛使用的資料庫軟體也有Bug:即使像MySQL與PostgreSQL這樣穩健、口碑良好、多年來被許多人充分測試過的軟體,就我個人所見也有Bug,比如MySQL未能正確維護唯一約束【65】,以及PostgreSQL的可序列化隔離等級存在特定的寫偏斜異常【66】。對於不那麼成熟的軟體來說,情況可能要糟糕得多。 +除了這些硬體問題之外,總是存在軟體 Bug 的風險,這些錯誤不會被較低層次的網路、記憶體或檔案系統校驗和所捕獲。即使廣泛使用的資料庫軟體也有 Bug:即使像 MySQL 與 PostgreSQL 這樣穩健、口碑良好、多年來被許多人充分測試過的軟體,就我個人所見也有 Bug,比如 MySQL 未能正確維護唯一約束【65】,以及 PostgreSQL 的可序列化隔離等級存在特定的寫偏斜異常【66】。對於不那麼成熟的軟體來說,情況可能要糟糕得多。 -儘管在仔細設計,測試,以及審查上做出很多努力,但Bug仍然會在不知不覺中產生。儘管它們很少,而且最終會被發現並被修復,但總會有那麼一段時間,這些Bug可能會損壞資料。 +儘管在仔細設計,測試,以及審查上做出很多努力,但 Bug 仍然會在不知不覺中產生。儘管它們很少,而且最終會被發現並被修復,但總會有那麼一段時間,這些 Bug 可能會損壞資料。 而對於應用程式碼,我們不得不假設會有更多的錯誤,因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能,例如外來鍵或唯一性約束【36】。 -ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有Bug時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。 +ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。 #### 不要盲目信任承諾 -由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為**審計(auditing)**。 +由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 **審計(auditing)**。 -如“[不可變事件的優點](ch11.md#不可變事件的優點)”一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。 +如 “[不可變事件的優點](ch11.md#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。 -成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性,並管理這種風險。例如,HDFS和Amazon S3等大規模儲存系統並不完全信任磁碟:它們執行後臺程序持續回讀檔案,並將其與其他副本進行比較,並將檔案從一個磁碟移動到另一個,以便降低靜默損壞的風險【67】。 +成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性,並管理這種風險。例如,HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟:它們執行後臺程序持續回讀檔案,並將其與其他副本進行比較,並將檔案從一個磁碟移動到另一個,以便降低靜默損壞的風險【67】。 如果你想確保你的資料仍然存在,你必須真正讀取它並進行檢查。大多數時候它們仍然會在那裡,但如果不是這樣,你一定想盡早知道答案,而不是更晚。按照同樣的原則,不時地嘗試從備份中恢復是非常重要的 —— 否則當你發現備份損壞時,你可能已經遇到了資料丟失,那時候就真的太晚了。不要盲目地相信它們全都管用。 #### 驗證的文化 -像HDFS和S3這樣的系統仍然需要假設磁碟大部分時間都能正常工作 —— 這是一個合理的假設,但與它們**始終**能正常工作的假設並不相同。然而目前還沒有多少系統採用這種“信任但是驗證”的方式來持續審計自己。許多人認為正確性保證是絕對的,並且沒有為罕見的資料損壞的可能性做過準備。我希望未來能看到更多的**自我驗證(self-validating)** 或**自我審計(self-auditing)** 系統,不斷檢查自己的完整性,而不是依賴盲目的信任【68】。 +像 HDFS 和 S3 這樣的系統仍然需要假設磁碟大部分時間都能正常工作 —— 這是一個合理的假設,但與它們 **始終** 能正常工作的假設並不相同。然而目前還沒有多少系統採用這種 “信任但是驗證” 的方式來持續審計自己。許多人認為正確性保證是絕對的,並且沒有為罕見的資料損壞的可能性做過準備。我希望未來能看到更多的 **自我驗證(self-validating)** 或 **自我審計(self-auditing)** 系統,不斷檢查自己的完整性,而不是依賴盲目的信任【68】。 -我擔心ACID資料庫的文化導致我們在盲目信任技術(如事務機制)的基礎上開發應用,而忽視了這種過程中的任何可審計性。由於我們所信任的技術在大多數情況下工作得很好,通常會認為審計機制並不值得投資。 +我擔心 ACID 資料庫的文化導致我們在盲目信任技術(如事務機制)的基礎上開發應用,而忽視了這種過程中的任何可審計性。由於我們所信任的技術在大多數情況下工作得很好,通常會認為審計機制並不值得投資。 -但隨之而來的是,資料庫的格局發生了變化:在NoSQL的旗幟下,更弱的一致性保證成為常態,更不成熟的儲存技術越來越被廣泛使用。但是由於審計機制還沒有被開發出來,儘管這種方式越來越危險,我們仍不斷在盲目信任的基礎上構建應用。讓我們想一想如何針對可審計性而設計吧。 +但隨之而來的是,資料庫的格局發生了變化:在 NoSQL 的旗幟下,更弱的一致性保證成為常態,更不成熟的儲存技術越來越被廣泛使用。但是由於審計機制還沒有被開發出來,儘管這種方式越來越危險,我們仍不斷在盲目信任的基礎上構建應用。讓我們想一想如何針對可審計性而設計吧。 #### 為可審計性而設計 -如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明**為什麼**要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。 +如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。 相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。 -顯式處理資料流(請參閱“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的**來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。 +顯式處理資料流(請參閱 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的 **來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。 -具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它**為什麼**做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力—— 一種時間旅行除錯功能是非常有價值的。 +具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。 #### 端到端原則重現 -如果我們不能完全相信系統的每個元件都不會損壞 —— 每一個硬體都沒缺陷,每一個軟體都沒有Bug —— 那我們至少必須定期檢查資料的完整性。如果我們不檢查,我們就不能發現損壞,直到無可挽回地導致對下游的破壞時,那時候再去追蹤問題就要難得多,且代價也要高的多。 +如果我們不能完全相信系統的每個元件都不會損壞 —— 每一個硬體都沒缺陷,每一個軟體都沒有 Bug —— 那我們至少必須定期檢查資料的完整性。如果我們不檢查,我們就不能發現損壞,直到無可挽回地導致對下游的破壞時,那時候再去追蹤問題就要難得多,且代價也要高的多。 -檢查資料系統的完整性,最好是以端到端的方式進行(請參閱“[資料庫的端到端原則](#資料庫的端到端原則)”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個衍生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟、網路、服務以及演算法的正確性檢查都隱含在其中了。 +檢查資料系統的完整性,最好是以端到端的方式進行(請參閱 “[資料庫的端到端原則](#資料庫的端到端原則)”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個衍生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟、網路、服務以及演算法的正確性檢查都隱含在其中了。 持續的端到端完整性檢查可以不斷提高你對系統正確性的信心,從而使你能更快地進步【70】。與自動化測試一樣,審計提高了快速發現錯誤的可能性,從而降低了系統變更或新儲存技術可能導致損失的風險。如果你不害怕進行變更,就可以更好地充分演化一個應用,使其滿足不斷變化的需求。 @@ -705,13 +705,13 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 目前,將可審計性作為頂層關注點的資料系統並不多。一些應用實現了自己的審計機制,例如將所有變更記錄到單獨的審計表中,但是確保審計日誌與資料庫狀態的完整性仍然是很困難的。可以定期使用硬體安全模組對事務日誌進行簽名來防止篡改,但這無法保證正確的事務一開始就能進入到日誌中。 -使用密碼學工具來證明系統的完整性是十分有趣的,這種方式對於寬泛的硬體與軟體問題,甚至是潛在的惡意行為都很穩健有效。加密貨幣,區塊鏈,以及諸如比特幣,以太坊,Ripple,Stellar的分散式賬本技術已經迅速出現在這一領域【71,72,73】。 +使用密碼學工具來證明系統的完整性是十分有趣的,這種方式對於寬泛的硬體與軟體問題,甚至是潛在的惡意行為都很穩健有效。加密貨幣、區塊鏈、以及諸如比特幣、以太坊、Ripple、Stellar 的分散式賬本技術已經迅速出現在這一領域【71,72,73】。 我沒有資格評論這些技術用於貨幣,或者合同商定機制的價值。但從資料系統的角度來看,它們包含了一些有趣的想法。實質上,它們是分散式資料庫,具有資料模型與事務機制,而不同副本可以由互不信任的組織託管。副本不斷檢查其他副本的完整性,並使用共識協議對應當執行的事務達成一致。 -我對這些技術的拜占庭容錯方面有些懷疑(請參閱“[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現**工作證明(proof of work)** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。 +我對這些技術的拜占庭容錯方面有些懷疑(請參閱 “[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現 **工作證明(proof of work)** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。 -密碼學審計與完整性檢查通常依賴**默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)** 也是一種依賴Merkle樹的安全技術,用來檢查TLS/SSL證書的有效性【75,76】。 +密碼學審計與完整性檢查通常依賴 **默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)** 也是一種依賴 Merkle 樹的安全技術,用來檢查 TLS/SSL 證書的有效性【75,76】。 我可以想象,那些在證書透明度與分散式賬本中使用的完整性檢查和審計演算法,將會在通用資料系統中得到越來越廣泛的應用。要使得這些演算法對於沒有密碼學審計的系統同樣可伸縮,並儘可能降低效能損失還需要一些工作。 但我認為這是一個值得關注的有趣領域。 @@ -724,25 +724,25 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 我們將資料當成一種抽象的東西來討論,但請記住,許多資料集都是關於人的:他們的行為,他們的興趣,他們的身份。對待這些資料,我們必須懷著人性與尊重。使用者也是人類,人類的尊嚴是至關重要的。 -軟體開發越來越多地涉及重要的道德抉擇。有一些指導原則可以幫助軟體工程師解決這些問題,例如ACM的軟體工程道德規範與專業實踐【77】,但實踐中很少會討論這些,更不用說應用與強制執行了。因此,工程師和產品經理有時會對隱私與產品潛在的負面後果抱有非常傲慢的態度【78,79,80】。 +軟體開發越來越多地涉及重要的道德抉擇。有一些指導原則可以幫助軟體工程師解決這些問題,例如 ACM 的軟體工程道德規範與專業實踐【77】,但實踐中很少會討論這些,更不用說應用與強制執行了。因此,工程師和產品經理有時會對隱私與產品潛在的負面後果抱有非常傲慢的態度【78,79,80】。 技術本身並無好壞之分 —— 關鍵在於它被如何使用,以及它如何影響人們。這對槍械這樣的武器是成立的,而搜尋引擎這樣的軟體系統與之類似。我認為,軟體工程師僅僅專注於技術而忽視其後果是不夠的:道德責任也是我們的責任。對道德推理很困難,但它太重要了,我們無法忽視。 ### 預測性分析 -舉個例子,預測性分析是“大資料”炒作的主要內容之一。使用資料分析預測天氣或疾病傳播是一碼事【81】;而預測一個罪犯是否可能再犯,一個貸款申請人是否有可能違約,或者一個保險客戶是否可能進行昂貴的索賠,則是另外一碼事。後者會直接影響到個人的生活。 +舉個例子,預測性分析是 “大資料” 炒作的主要內容之一。使用資料分析預測天氣或疾病傳播是一碼事【81】;而預測一個罪犯是否可能再犯,一個貸款申請人是否有可能違約,或者一個保險客戶是否可能進行昂貴的索賠,則是另外一碼事。後者會直接影響到個人的生活。 -當然,支付網路希望防止欺詐交易,銀行希望避免不良貸款,航空公司希望避免劫機,公司希望避免僱傭效率低下或不值得信任的人。從它們的角度來看,失去商機的成本很低,而不良貸款或問題員工的成本則要高得多,因而組織希望保持謹慎也是自然而然的事情。所以如果存疑,它們通常會Say No。 +當然,支付網路希望防止欺詐交易,銀行希望避免不良貸款,航空公司希望避免劫機,公司希望避免僱傭效率低下或不值得信任的人。從它們的角度來看,失去商機的成本很低,而不良貸款或問題員工的成本則要高得多,因而組織希望保持謹慎也是自然而然的事情。所以如果存疑,它們通常會 Say No。 -然而,隨著演算法決策變得越來越普遍,被某種演算法(準確地或錯誤地)標記為有風險的某人可能會遭受大量這種“No”的決定。系統性地被排除在工作,航旅,保險,租賃,金融服務,以及其他社會關鍵領域之外。這是一種對個體自由的極大約束,因此被稱為“演算法監獄”【82】。在尊重人權的國家,刑事司法系統會做無罪推定(預設清白,直到被證明有罪)。另一方面,自動化系統可以系統地,任意地將一個人排除在社會參與之外,不需要任何有罪的證明,而且幾乎沒有申訴的機會。 +然而,隨著演算法決策變得越來越普遍,被某種演算法(準確地或錯誤地)標記為有風險的某人可能會遭受大量這種 “No” 的決定。系統性地被排除在工作,航旅,保險,租賃,金融服務,以及其他社會關鍵領域之外。這是一種對個體自由的極大約束,因此被稱為 “演算法監獄”【82】。在尊重人權的國家,刑事司法系統會做無罪推定(預設清白,直到被證明有罪)。另一方面,自動化系統可以系統地,任意地將一個人排除在社會參與之外,不需要任何有罪的證明,而且幾乎沒有申訴的機會。 #### 偏見與歧視 演算法做出的決定不一定比人類更好或更差。每個人都可能有偏見,即使他們主動抗拒這一點;而歧視性做法也可能已經在文化上被制度化了。人們希望根據資料做出決定,而不是透過人的主觀評價與直覺,希望這樣能更加公平,並給予傳統體制中經常被忽視的人更好的機會【83】。 -當我們開發預測性分析系統時,不是僅僅用軟體透過一系列IF ELSE規則將人類的決策過程自動化,那些規則本身甚至都是從資料中推斷出來的。但這些系統學到的模式是個黑盒:即使資料中存在一些相關性,我們可能也壓根不知道為什麼。如果演算法的輸入中存在系統性的偏見,則系統很有可能會在輸出中學習並放大這種偏見【84】。 +當我們開發預測性分析系統時,不是僅僅用軟體透過一系列 IF ELSE 規則將人類的決策過程自動化,那些規則本身甚至都是從資料中推斷出來的。但這些系統學到的模式是個黑盒:即使資料中存在一些相關性,我們可能也壓根不知道為什麼。如果演算法的輸入中存在系統性的偏見,則系統很有可能會在輸出中學習並放大這種偏見【84】。 -在許多國家,反歧視法律禁止按種族、年齡、性別、性取向、殘疾或信仰等受保護的特徵區分對待不同的人。其他的個人特徵可能是允許用於分析的,但是如果這些特徵與受保護的特徵存在關聯,又會發生什麼?例如在種族隔離地區中,一個人的郵政編碼,甚至是他們的IP地址,都是很強的種族指示物。這樣的話,相信一種演算法可以以某種方式將有偏見的資料作為輸入,併產生公平和公正的輸出【85】似乎是很荒謬的。然而這種觀點似乎常常潛伏在資料驅動型決策的支持者中,這種態度被諷刺為“在處理偏差上,機器學習與洗錢類似”(machine learning is like money laundering for bias)【86】。 +在許多國家,反歧視法律禁止按種族、年齡、性別、性取向、殘疾或信仰等受保護的特徵區分對待不同的人。其他的個人特徵可能是允許用於分析的,但是如果這些特徵與受保護的特徵存在關聯,又會發生什麼?例如在種族隔離地區中,一個人的郵政編碼,甚至是他們的 IP 地址,都是很強的種族指示物。這樣的話,相信一種演算法可以以某種方式將有偏見的資料作為輸入,併產生公平和公正的輸出【85】似乎是很荒謬的。然而這種觀點似乎常常潛伏在資料驅動型決策的支持者中,這種態度被諷刺為 “在處理偏差上,機器學習與洗錢類似”(machine learning is like money laundering for bias)【86】。 預測性分析系統只是基於過去進行推斷;如果過去是歧視性的,它們就會將這種歧視歸納為規律。如果我們希望未來比過去更好,那麼就需要道德想象力,而這是隻有人類才能提供的東西【87】。資料與模型應該是我們的工具,而不是我們的主人。 @@ -750,11 +750,11 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 自動決策引發了關於責任與問責的問題【87】。如果一個人犯了錯誤,他可以被追責,受決定影響的人可以申訴。演算法也會犯錯誤,但是如果它們出錯,誰來負責【88】?當一輛自動駕駛汽車引發事故時,誰來負責?如果自動信用評分算法系統性地歧視特定種族或宗教的人,這些人是否有任何追索權?如果機器學習系統的決定要受到司法審查,你能向法官解釋演算法是如何做出決定的嗎? -收集關於人的資料並進行決策,信用評級機構是一個很經典的例子。不良的信用評分會使生活變得更艱難,但至少信用分通常是基於個人**實際的**借款歷史記錄,而記錄中的任何錯誤都能被糾正(儘管機構通常會設定門檻)。然而,基於機器學習的評分演算法通常會使用更寬泛的輸入,並且更不透明;因而很難理解特定決策是怎樣作出的,以及是否有人被不公正地,歧視性地對待【89】。 +收集關於人的資料並進行決策,信用評級機構是一個很經典的例子。不良的信用評分會使生活變得更艱難,但至少信用分通常是基於個人 **實際的** 借款歷史記錄,而記錄中的任何錯誤都能被糾正(儘管機構通常會設定門檻)。然而,基於機器學習的評分演算法通常會使用更寬泛的輸入,並且更不透明;因而很難理解特定決策是怎樣作出的,以及是否有人被不公正地,歧視性地對待【89】。 -信用分總結了“你過去的表現如何?”,而預測性分析通常是基於“誰與你類似,以及與你類似的人過去表現的如何?”。與他人的行為畫上等號意味著刻板印象,例如,根據他們居住的地方(與種族和階級關係密切的特徵)。那麼那些放錯位置的人怎麼辦?而且,如果是因為錯誤資料導致的錯誤決定,追索幾乎是不可能的【87】。 +信用分總結了 “你過去的表現如何?”,而預測性分析通常是基於 “誰與你類似,以及與你類似的人過去表現的如何?”。與他人的行為畫上等號意味著刻板印象,例如,根據他們居住的地方(與種族和階級關係密切的特徵)。那麼那些放錯位置的人怎麼辦?而且,如果是因為錯誤資料導致的錯誤決定,追索幾乎是不可能的【87】。 -很多資料本質上是統計性的,這意味著即使概率分佈在總體上是正確的,對於個例也可能是錯誤的。例如,如果貴國的平均壽命是80歲,這並不意味著你在80歲生日時就會死掉。很難從平均值與概率分佈中對某個特定個體的壽命作出什麼判斷,同樣,預測系統的輸出是概率性的,對於個例可能是錯誤的。 +很多資料本質上是統計性的,這意味著即使概率分佈在總體上是正確的,對於個例也可能是錯誤的。例如,如果貴國的平均壽命是 80 歲,這並不意味著你在 80 歲生日時就會死掉。很難從平均值與概率分佈中對某個特定個體的壽命作出什麼判斷,同樣,預測系統的輸出是概率性的,對於個例可能是錯誤的。 盲目相信資料決策至高無上,這不僅僅是一種妄想,而是有切實危險的。隨著資料驅動的決策變得越來越普遍,我們需要弄清楚,如何使演算法更負責任且更加透明,如何避免加強現有的偏見,以及如何在它們不可避免地出錯時加以修復。 @@ -762,11 +762,11 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 #### 反饋迴圈 -即使是那些對人直接影響比較小的預測性應用,比如推薦系統,也有一些必須正視的難題。當服務變得善於預測使用者想要看到什麼內容時,它最終可能只會向人們展示他們已經同意的觀點,將人們帶入滋生刻板印象,誤導資訊,與極端思想的**迴音室**。我們已經看到過社交媒體迴音室對競選的影響了【91】。 +即使是那些對人直接影響比較小的預測性應用,比如推薦系統,也有一些必須正視的難題。當服務變得善於預測使用者想要看到什麼內容時,它最終可能只會向人們展示他們已經同意的觀點,將人們帶入滋生刻板印象,誤導資訊,與極端思想的 **迴音室**。我們已經看到過社交媒體迴音室對競選的影響了【91】。 當預測性分析影響人們的生活時,自我強化的反饋迴圈會導致非常有害的問題。例如,考慮僱主使用信用分來評估候選人的例子。你可能是一個信用分不錯的好員工,但因不可抗力的意外而陷入財務困境。由於不能按期付賬單,你的信用分會受到影響,進而導致找到工作更為困難。失業使你陷入貧困,這進一步惡化了你的分數,使你更難找到工作【87】。在資料與數學嚴謹性的偽裝背後,隱藏的是由惡毒假設導致的惡性迴圈。 -我們無法預測這種反饋迴圈何時發生。然而透過對整個系統(不僅僅是計算機化的部分,而且還有與之互動的人)進行整體思考,許多後果是可以夠預測的 —— 一種稱為**系統思維(systems thinking)** 的方法【92】。我們可以嘗試理解資料分析系統如何響應不同的行為,結構或特性。該系統是否加強和增大了人們之間現有的差異(例如,損不足以奉有餘,富者愈富,貧者愈貧),還是試圖與不公作鬥爭?而且即使有著最好的動機,我們也必須當心意想不到的後果。 +我們無法預測這種反饋迴圈何時發生。然而透過對整個系統(不僅僅是計算機化的部分,而且還有與之互動的人)進行整體思考,許多後果是可以夠預測的 —— 一種稱為 **系統思維(systems thinking)** 的方法【92】。我們可以嘗試理解資料分析系統如何響應不同的行為,結構或特性。該系統是否加強和增大了人們之間現有的差異(例如,損不足以奉有餘,富者愈富,貧者愈貧),還是試圖與不公作鬥爭?而且即使有著最好的動機,我們也必須當心意想不到的後果。 ### 隱私和追蹤 @@ -774,7 +774,7 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 當系統只儲存使用者明確輸入的資料時,是因為使用者希望系統以特定方式儲存和處理這些資料,**系統是在為使用者提供服務**:使用者就是客戶。但是,當用戶的活動被跟蹤並記錄,作為他們正在做的其他事情的副作用時,這種關係就沒有那麼清晰了。該服務不再僅僅完成使用者想要它要做的事情,而是服務於它自己的利益,而這可能與使用者的利益相沖突。 -追蹤使用者行為資料對於許多面向用戶的線上服務而言,變得越來越重要:追蹤使用者點選了哪些搜尋結果有助於改善搜尋結果的排名;推薦“喜歡X的人也喜歡Y”,可以幫助使用者發現實用有趣的東西; A/B測試和使用者流量分析有助於改善使用者介面。這些功能需要一定量的使用者行為跟蹤,而使用者也可以從中受益。 +追蹤使用者行為資料對於許多面向用戶的線上服務而言,變得越來越重要:追蹤使用者點選了哪些搜尋結果有助於改善搜尋結果的排名;推薦 “喜歡 X 的人也喜歡 Y”,可以幫助使用者發現實用有趣的東西; A/B 測試和使用者流量分析有助於改善使用者介面。這些功能需要一定量的使用者行為跟蹤,而使用者也可以從中受益。 但不同公司有著不同的商業模式,追蹤並未止步於此。如果服務是透過廣告盈利的,那麼廣告主才是真正的客戶,而使用者的利益則屈居其次。跟蹤的資料會變得更詳細,分析變得更深入,資料會保留很長時間,以便為每個人建立詳細畫像,用於營銷。 @@ -782,9 +782,9 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 #### 監視 -讓我們做一個思想實驗,嘗試用**監視(surveillance)** 一詞替換**資料(data)**,再看看常見的短語是不是聽起來還那麼漂亮【93】。比如:“在我們的監視驅動的組織中,我們收集實時監視流並將它們儲存在我們的監視倉庫中。我們的監視科學家使用高階分析和監視處理來獲得新的見解。” +讓我們做一個思想實驗,嘗試用 **監視(surveillance)** 一詞替換 **資料(data)**,再看看常見的短語是不是聽起來還那麼漂亮【93】。比如:“在我們的監視驅動的組織中,我們收集實時監視流並將它們儲存在我們的監視倉庫中。我們的監視科學家使用高階分析和監視處理來獲得新的見解。” -對於本書《設計監控密集型應用》而言,這個思想實驗是罕見的爭議性內容,但我認為需要激烈的言辭來強調這一點。在我們嘗試製造軟體“吞噬世界”的過程中【94】,我們已經建立了世界上迄今為止所見過的最偉大的大規模監視基礎設施。我們正朝著萬物互聯邁進,我們正在迅速走近這樣一個世界:每個有人居住的空間至少包含一個帶網際網路連線的麥克風,以智慧手機、智慧電視、語音控制助理裝置、嬰兒監視器甚至兒童玩具的形式存在,並使用基於雲的語音識別。這些裝置中的很多都有著可怕的安全記錄【95】。 +對於本書《設計監控密集型應用》而言,這個思想實驗是罕見的爭議性內容,但我認為需要激烈的言辭來強調這一點。在我們嘗試製造軟體 “吞噬世界” 的過程中【94】,我們已經建立了世界上迄今為止所見過的最偉大的大規模監視基礎設施。我們正朝著萬物互聯邁進,我們正在迅速走近這樣一個世界:每個有人居住的空間至少包含一個帶網際網路連線的麥克風,以智慧手機、智慧電視、語音控制助理裝置、嬰兒監視器甚至兒童玩具的形式存在,並使用基於雲的語音識別。這些裝置中的很多都有著可怕的安全記錄【95】。 即使是最為極權與專制的政權,可能也只會想著在每個房間裝一個麥克風,並強迫每個人始終攜帶能夠追蹤其位置與動向的裝置。然而,我們顯然是自願地,甚至熱情地投身於這個全域監視的世界。不同之處在於,資料是由公司,而不是由政府機構收集的【96】。 @@ -794,29 +794,29 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 #### 同意與選擇的自由 -我們可能會斷言使用者是自願選擇使用了會跟蹤其活動的服務,而且他們已經同意了服務條款與隱私政策,因此他們同意資料收集。我們甚至可以聲稱,使用者在用所提供的資料來**換取**有價值的服務,並且為了提供服務,追蹤是必要的。毫無疑問,社交網路、搜尋引擎以及各種其他免費的線上服務對於使用者來說都是有價值的,但是這個說法卻存在問題。 +我們可能會斷言使用者是自願選擇使用了會跟蹤其活動的服務,而且他們已經同意了服務條款與隱私政策,因此他們同意資料收集。我們甚至可以聲稱,使用者在用所提供的資料來 **換取** 有價值的服務,並且為了提供服務,追蹤是必要的。毫無疑問,社交網路、搜尋引擎以及各種其他免費的線上服務對於使用者來說都是有價值的,但是這個說法卻存在問題。 使用者幾乎不知道他們提供給我們的是什麼資料,哪些資料被放進了資料庫,資料又是怎樣被保留與處理的 —— 大多數隱私政策都是模稜兩可的,忽悠使用者而不敢開啟天窗說亮話。如果使用者不瞭解他們的資料會發生什麼,就無法給出任何有意義的同意。有時來自一個使用者的資料還會提到一些關於其他人的事,而其他那些人既不是該服務的使用者,也沒有同意任何條款。我們在本書這一部分中討論的衍生資料集 —— 來自整個使用者群的資料,加上行為追蹤與外部資料來源 —— 就恰好是使用者無法(在真正意義上)理解的資料型別。 而且從使用者身上挖掘資料是一個單向過程,而不是真正的互惠關係,也不是公平的價值交換。使用者對能用多少資料換來什麼樣的服務,既沒有沒有發言權也沒有選擇權:服務與使用者之間的關係是非常不對稱與單邊的。這些條款是由服務提出的,而不是由使用者提出的【99】。 -對於不同意監視的使用者,唯一真正管用的備選項,就是簡單地不使用服務。但這個選擇也不是真正自由的:如果一項服務如此受歡迎,以至於“被大多數人認為是基本社會參與的必要條件”【99】,那麼指望人們選擇退出這項服務是不合理的 —— 使用它**事實上(de facto)** 是強制性的。例如,在大多數西方社會群體中,攜帶智慧手機,使用Facebook進行社交,以及使用Google查詢資訊已成為常態。特別是當一項服務具有網路效應時,人們選擇**不**使用會產生社會成本。 +對於不同意監視的使用者,唯一真正管用的備選項,就是簡單地不使用服務。但這個選擇也不是真正自由的:如果一項服務如此受歡迎,以至於 “被大多數人認為是基本社會參與的必要條件”【99】,那麼指望人們選擇退出這項服務是不合理的 —— 使用它 **事實上(de facto)** 是強制性的。例如,在大多數西方社會群體中,攜帶智慧手機,使用 Facebook 進行社交,以及使用 Google 查詢資訊已成為常態。特別是當一項服務具有網路效應時,人們選擇 **不** 使用會產生社會成本。 因為一個服務會跟蹤使用者而拒絕使用它,這只是少數人才擁有的權力,他們有足夠的時間與知識來了解隱私政策,並承受得起代價:錯過社會參與,以及使用服務可能帶來的專業機會。對於那些處境不太好的人而言,並沒有真正意義上的選擇:監控是不可避免的。 #### 隱私與資料使用 -有時候,人們聲稱“隱私已死”,理由是有些使用者願意把各種關於他們生活的事情釋出到社交媒體上,有時是平凡俗套,但有時是高度私密的。但這種說法是錯誤的,而且是對**隱私(privacy)** 一詞的誤解。 +有時候,人們聲稱 “隱私已死”,理由是有些使用者願意把各種關於他們生活的事情釋出到社交媒體上,有時是平凡俗套,但有時是高度私密的。但這種說法是錯誤的,而且是對 **隱私(privacy)** 一詞的誤解。 擁有隱私並不意味著保密一切東西;它意味著擁有選擇向誰展示哪些東西的自由,要公開什麼,以及要保密什麼。**隱私權是一項決定權**:在從保密到透明的光譜上,隱私使得每個人都能決定自己想要在什麼地方位於光譜上的哪個位置【99】。這是一個人自由與自主的重要方面。 -當透過監控基礎設施從人身上提取資料時,隱私權不一定受到損害,而是轉移到了資料收集者手中。獲取資料的公司實際上是說“相信我們會用你的資料做正確的事情”,這意味著,決定要透露什麼和保密什麼的權利從個體手中轉移到了公司手中。 +當透過監控基礎設施從人身上提取資料時,隱私權不一定受到損害,而是轉移到了資料收集者手中。獲取資料的公司實際上是說 “相信我們會用你的資料做正確的事情”,這意味著,決定要透露什麼和保密什麼的權利從個體手中轉移到了公司手中。 這些公司反過來選擇保密這些監視結果,因為揭露這些會令人毛骨悚然,並損害它們的商業模式(比其他公司更瞭解人)。使用者的私密資訊只會間接地披露,例如針對特定人群定向投放廣告的工具(比如那些患有特定疾病的人群)。 -即使特定使用者無法從特定廣告定向的人群中以個體的形式區分出來,但他們已經失去了披露一些私密資訊的能動性,例如他們是否患有某種疾病。決定向誰透露什麼並不是由個體按照自己的喜好決定的,而是由**公司**,以利潤最大化為目標來行使隱私權的。 +即使特定使用者無法從特定廣告定向的人群中以個體的形式區分出來,但他們已經失去了披露一些私密資訊的能動性,例如他們是否患有某種疾病。決定向誰透露什麼並不是由個體按照自己的喜好決定的,而是由 **公司**,以利潤最大化為目標來行使隱私權的。 -許多公司都有一個目標,不要讓人**感覺到**毛骨悚然 —— 先不說它們收集資料實際上是多麼具有侵犯性,讓我們先關注對使用者感受的管理。這些使用者感受經常被管理得很糟糕:例如,在事實上可能正確的一些東西,如果會觸發痛苦的回憶,使用者可能並不希望被提醒【100】。對於任何型別的資料,我們都應當考慮它出錯、不可取、不合時宜的可能性,並且需要建立處理這些失效的機制。無論是“不可取”還是“不合時宜”,當然都是由人的判斷決定的;除非我們明確地將演算法編碼設計為尊重人類的需求,否則演算法會無視這些概念。作為這些系統的工程師,我們必須保持謙卑,充分規劃,接受這些失效。 +許多公司都有一個目標,不要讓人 **感覺到** 毛骨悚然 —— 先不說它們收集資料實際上是多麼具有侵犯性,讓我們先關注對使用者感受的管理。這些使用者感受經常被管理得很糟糕:例如,在事實上可能正確的一些東西,如果會觸發痛苦的回憶,使用者可能並不希望被提醒【100】。對於任何型別的資料,我們都應當考慮它出錯、不可取、不合時宜的可能性,並且需要建立處理這些失效的機制。無論是 “不可取” 還是 “不合時宜”,當然都是由人的判斷決定的;除非我們明確地將演算法編碼設計為尊重人類的需求,否則演算法會無視這些概念。作為這些系統的工程師,我們必須保持謙卑,充分規劃,接受這些失效。 允許線上服務的使用者控制其隱私設定,例如控制其他使用者可以看到哪些東西,是將一些控制交還給使用者的第一步。但無論怎麼設定,服務本身仍然可以不受限制地訪問資料,並能以隱私策略允許的任何方式自由使用它。即使服務承諾不會將資料出售給第三方,它通常會授予自己不受限制的權利,以便在內部處理與分析資料,而且往往比使用者公開可見的部分要深入的多。 @@ -824,7 +824,7 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 #### 資料資產與權力 -由於行為資料是使用者與服務互動的副產品,因此有時被稱為“資料廢氣” —— 暗示資料是毫無價值的廢料。從這個角度來看,行為和預測性分析可以被看作是一種從資料中提取價值的回收形式,否則這些資料就會被浪費。 +由於行為資料是使用者與服務互動的副產品,因此有時被稱為 “資料廢氣” —— 暗示資料是毫無價值的廢料。從這個角度來看,行為和預測性分析可以被看作是一種從資料中提取價值的回收形式,否則這些資料就會被浪費。 更準確的看法恰恰相反:從經濟的角度來看,如果定向廣告是服務的金主,那麼關於人的行為資料就是服務的核心資產。在這種情況下,使用者與之互動的應用僅僅是一種誘騙使用者將更多的個人資訊提供給監控基礎設施的手段【99】。線上服務中經常表現出的令人愉悅的人類創造力與社會關係,十分諷刺地被資料提取機器所濫用。 @@ -832,7 +832,7 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 因為資料很有價值,所以很多人都想要它。當然,公司也想要它 —— 這就是為什麼它們一開始就收集資料的原因。但政府也想獲得它:透過祕密交易、脅迫、法律強制或者只是竊取【101】。當公司破產時,收集到的個人資料就是被出售的資產之一。而且資料安全很難保護,因此經常發生令人難堪的洩漏事件【102】。 -這些觀察已經導致批評者聲稱,資料不僅僅是一種資產,而且是一種“有毒資產”【101】,或者至少是“有害物質”【103】。即使我們認為自己有能力阻止資料濫用,但每當我們收集資料時,我們都需要平衡收益以及這些資料落入惡人手中的風險:計算機系統可能會被犯罪分子或敵國特務滲透,資料可能會被內鬼洩露,公司可能會落入不擇手段的管理層手中,而這些管理者有著迥然不同的價值觀,或者國家可能被能毫無愧色迫使我們交出資料的政權所接管。 +這些觀察已經導致批評者聲稱,資料不僅僅是一種資產,而且是一種 “有毒資產”【101】,或者至少是 “有害物質”【103】。即使我們認為自己有能力阻止資料濫用,但每當我們收集資料時,我們都需要平衡收益以及這些資料落入惡人手中的風險:計算機系統可能會被犯罪分子或敵國特務滲透,資料可能會被內鬼洩露,公司可能會落入不擇手段的管理層手中,而這些管理者有著迥然不同的價值觀,或者國家可能被能毫無愧色迫使我們交出資料的政權所接管。 俗話說,“知識就是力量”。更進一步,“在避免自己被審視的同時審視他人,是權力最重要的形式之一”【105】。這就是極權政府想要監控的原因:這讓它們有能力控制全體居民。儘管今天的科技公司並沒有公開地尋求政治權力,但是它們積累的資料與知識卻給它們帶來了很多權力,其中大部分是在公共監督之外偷偷進行的【106】。 @@ -844,7 +844,7 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 制定保護措施花費了很長的時間,例如環境保護條例、工作場所安全條例、宣佈使用童工非法以及食品衛生檢查。毫無疑問,生產成本增加了,因為工廠再也不能把廢物倒入河流、銷售汙染的食物或者剝削工人。但是整個社會都從中受益良多,我們中很少會有人想回到這些管制條例之前的日子【87】。 -就像工業革命有著黑暗面需要應對一樣,我們轉向資訊時代的過程中,也有需要應對與解決的重大問題。我相信資料的收集與使用就是其中一個問題。用Bruce Schneier的話來說【96】: +就像工業革命有著黑暗面需要應對一樣,我們轉向資訊時代的過程中,也有需要應對與解決的重大問題。我相信資料的收集與使用就是其中一個問題。用 Bruce Schneier 的話來說【96】: > 資料是資訊時代的汙染問題,保護隱私是環境挑戰。幾乎所有的電腦都能生產資訊。它堆積在周圍,開始潰爛。我們如何處理它 —— 我們如何控制它,以及如何擺脫它 —— 是資訊經濟健康發展的核心議題。正如我們今天回顧工業時代的早期年代,並想知道我們的祖先在忙於建設工業世界的過程時怎麼能忽略汙染問題;我們的孫輩在回望資訊時代的早期年代時,將會就我們如何應對資料收集和濫用的挑戰來評斷我們。 > @@ -852,9 +852,9 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 #### 立法與自律 -資料保護法可能有助於維護個人的權利。例如,1995年的“歐洲資料保護指示”規定,個人資料必須“為特定的、明確的和合法的目的收集,而不是以與這些目的不相符的方式進一步處理”,並且資料必須“就收集的目的而言適當、相關、不過分。“【107】。 +資料保護法可能有助於維護個人的權利。例如,1995 年的 “歐洲資料保護指示” 規定,個人資料必須 “為特定的、明確的和合法的目的收集,而不是以與這些目的不相符的方式進一步處理”,並且資料必須 “就收集的目的而言適當、相關、不過分。“【107】。 -但是,這個立法在今天的網際網路環境下是否有效還是有疑問的【108】。這些規則直接否定了大資料的哲學,即最大限度地收集資料,將其與其他資料集結合起來進行試驗和探索,以便產生新的洞察。探索意味著將資料用於未曾預期的目的,這與使用者同意的“特定和明確”目的相反(如果我們可以有意義地表示同意的話)【109】。更新的規章正在制定中【89】。 +但是,這個立法在今天的網際網路環境下是否有效還是有疑問的【108】。這些規則直接否定了大資料的哲學,即最大限度地收集資料,將其與其他資料集結合起來進行試驗和探索,以便產生新的洞察。探索意味著將資料用於未曾預期的目的,這與使用者同意的 “特定和明確” 目的相反(如果我們可以有意義地表示同意的話)【109】。更新的規章正在制定中【89】。 那些收集了大量有關人的資料的公司反對監管,認為這是創新的負擔與阻礙。在某種程度上,這種反對是有道理的。例如,分享醫療資料時,存在明顯的隱私風險,但也有潛在的機遇:如果資料分析能夠幫助我們實現更好的診斷或找到更好的治療方法,能夠阻止多少人的死亡【110】?過度監管可能會阻止這種突破。在這種潛在機會與風險之間找出平衡是很困難的【105】。 @@ -862,12 +862,12 @@ ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基 我們應該允許每個人保留自己的隱私 —— 即,對自己資料的控制 —— 而不是透過監視來竊取這種控制權。我們控制自己資料的個體權利就像是國家公園的自然環境:如果我們不去明確地保護它、關心它,它就會被破壞。這將是公地的悲劇,我們都會因此而變得更糟。無所不在的監視並非不可避免的 —— 我們現在仍然能阻止它。 -我們究竟能做到哪一步,是一個開放的問題。首先,我們不應該永久保留資料,而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳(請參閱“[不變性的侷限性](ch11.md#不變性的侷限性)”),但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。 +我們究竟能做到哪一步,是一個開放的問題。首先,我們不應該永久保留資料,而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳(請參閱 “[不變性的侷限性](ch11.md#不變性的侷限性)”),但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。 ## 本章小結 -在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論瞭如何使用批處理與事件流來解決這一**資料整合(data integration)** 問題,以便讓資料變更在不同系統之間流動。 +在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論瞭如何使用批處理與事件流來解決這一 **資料整合(data integration)** 問題,以便讓資料變更在不同系統之間流動。 在這種方法中,某些系統被指定為記錄系統,而其他資料則透過轉換衍生自記錄系統。透過這種方式,我們可以維護索引,物化檢視,機器學習模型,統計摘要等等。透過使這些衍生和轉換操作非同步且鬆散耦合,能夠防止一個區域中的問題擴散到系統中不相關部分,從而增加整個系統的穩健性與容錯性。 diff --git a/zh-tw/ch8.md b/zh-tw/ch8.md index cc2ae67..2684409 100644 --- a/zh-tw/ch8.md +++ b/zh-tw/ch8.md @@ -10,40 +10,40 @@ > > 無食我數 > -> —— Kyle Kingsbury, Carly Rae Jepsen 《網路分割槽的危害》(2013年)[^譯著1] +> —— Kyle Kingsbury, Carly Rae Jepsen 《網路分割槽的危害》(2013 年)[^譯著1] --------- [TOC] -最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了**副本故障切換**(“[處理節點中斷](ch5.md#處理節點宕機)”),**複製延遲**(“[複製延遲問題](ch5.md#複製延遲問題)”)和事務控制(“[弱隔離級別](ch7.md#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。 +最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了 **副本故障切換**(“[處理節點中斷](ch5.md#處理節點宕機)”),**複製延遲**(“[複製延遲問題](ch5.md#複製延遲問題)”)和事務控制(“[弱隔離級別](ch7.md#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。 -但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西**都會**出錯[^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事) +但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西 **都會** 出錯 [^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事) -[^i]: 除了一個例外:我們將假定故障是非拜占庭式的(請參閱“[拜占庭故障](#拜占庭故障)”)。 +[^i]: 除了一個例外:我們將假定故障是非拜占庭式的(請參閱 “[拜占庭故障](#拜占庭故障)”)。 使用分散式系統與在一臺計算機上編寫軟體有著根本的區別,主要的區別在於,有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中,我們將瞭解實踐中出現的問題,理解我們能夠依賴,和不可以依賴的東西。 -最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。 在[第九章](ch9.md)中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。 但首先,在本章中,我們必須瞭解我們面臨的挑戰。 +最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。 在 [第九章](ch9.md) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。 但首先,在本章中,我們必須瞭解我們面臨的挑戰。 本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。 我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。 所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。 ## 故障與部分失效 -當你在一臺計算機上編寫一個程式時,它通常會以一種相當可預測的方式執行:無論是工作還是不工作。充滿錯誤的軟體可能會讓人覺得電腦有時候也會有“糟糕的一天”(這種問題通常是重新啟動就恢復了),但這主要是軟體寫得不好的結果。 +當你在一臺計算機上編寫一個程式時,它通常會以一種相當可預測的方式執行:無論是工作還是不工作。充滿錯誤的軟體可能會讓人覺得電腦有時候也會有 “糟糕的一天”(這種問題通常是重新啟動就恢復了),但這主要是軟體寫得不好的結果。 單個計算機上的軟體沒有根本性的不可靠原因:當硬體正常工作時,相同的操作總是產生相同的結果(這是確定性的)。如果存在硬體問題(例如,記憶體損壞或聯結器鬆動),其後果通常是整個系統故障(例如,核心恐慌,“藍色畫面宕機”,啟動失敗)。裝有良好軟體的個人計算機通常要麼功能完好,要麼完全失效,而不是介於兩者之間。 -這是計算機設計中的一個有意的選擇:如果發生內部錯誤,我們寧願電腦完全崩潰,而不是返回錯誤的結果,因為錯誤的結果很難處理。因為計算機隱藏了模糊不清的物理實現,並呈現出一個理想化的系統模型,並以數學一樣的完美的方式運作。 CPU指令總是做同樣的事情;如果你將一些資料寫入記憶體或磁碟,那麼這些資料將保持不變,並且不會被隨機破壞。從第一臺數字計算機開始,*始終正確地計算*這個設計目標貫穿始終【3】。 +這是計算機設計中的一個有意的選擇:如果發生內部錯誤,我們寧願電腦完全崩潰,而不是返回錯誤的結果,因為錯誤的結果很難處理。因為計算機隱藏了模糊不清的物理實現,並呈現出一個理想化的系統模型,並以數學一樣的完美的方式運作。 CPU 指令總是做同樣的事情;如果你將一些資料寫入記憶體或磁碟,那麼這些資料將保持不變,並且不會被隨機破壞。從第一臺數字計算機開始,*始終正確地計算* 這個設計目標貫穿始終【3】。 當你編寫執行在多臺計算機上的軟體時,情況有本質上的區別。在分散式系統中,我們不再處於理想化的系統模型中,我們別無選擇,只能面對現實世界的混亂現實。而在現實世界中,各種各樣的事情都可能會出現問題【4】,如下面的軼事所述: -> 在我有限的經驗中,我已經和很多東西打過交道:單個**資料中心(DC)** 中長期存在的網路分割槽,配電單元PDU故障,交換機故障,整個機架的意外重啟,整個資料中心主幹網路故障,整個資料中心的電源故障,以及一個低血糖的司機把他的福特皮卡撞在資料中心的HVAC(加熱,通風和空調)系統上。而且我甚至不是一個運維。 +> 在我有限的經驗中,我已經和很多東西打過交道:單個 **資料中心(DC)** 中長期存在的網路分割槽,配電單元 PDU 故障,交換機故障,整個機架的意外重啟,整個資料中心主幹網路故障,整個資料中心的電源故障,以及一個低血糖的司機把他的福特皮卡撞在資料中心的 HVAC(加熱,通風和空調)系統上。而且我甚至不是一個運維。 > > —— 柯達黑爾 -在分散式系統中,儘管系統的其他部分工作正常,但系統的某些部分可能會以某種不可預知的方式被破壞。這被稱為**部分失效(partial failure)**。難點在於部分失效是**不確定性的(nonderterministic)**:如果你試圖做任何涉及多個節點和網路的事情,它有時可能會工作,有時會出現不可預知的失敗。正如我們將要看到的,你甚至不知道是否成功了,因為訊息透過網路傳播的時間也是不確定的! +在分散式系統中,儘管系統的其他部分工作正常,但系統的某些部分可能會以某種不可預知的方式被破壞。這被稱為 **部分失效(partial failure)**。難點在於部分失效是 **不確定性的(nonderterministic)**:如果你試圖做任何涉及多個節點和網路的事情,它有時可能會工作,有時會出現不可預知的失敗。正如我們將要看到的,你甚至不知道是否成功了,因為訊息透過網路傳播的時間也是不確定的! 這種不確定性和部分失效的可能性,使得分散式系統難以工作【5】。 @@ -51,27 +51,27 @@ 關於如何構建大型計算系統有一系列的哲學: -* 一個極端是高效能運算(HPC)領域。具有數千個CPU的超級計算機通常用於計算密集型科學計算任務,如天氣預報或分子動力學(模擬原子和分子的運動)。 -* 另一個極端是**雲端計算(cloud computing)**,雲端計算並不是一個良好定義的概念【6】,但通常與多租戶資料中心,連線IP網路(通常是乙太網)的商用計算機,彈性/按需資源分配以及計量計費等相關聯。 +* 一個極端是高效能運算(HPC)領域。具有數千個 CPU 的超級計算機通常用於計算密集型科學計算任務,如天氣預報或分子動力學(模擬原子和分子的運動)。 +* 另一個極端是 **雲端計算(cloud computing)**,雲端計算並不是一個良好定義的概念【6】,但通常與多租戶資料中心,連線 IP 網路(通常是乙太網)的商用計算機,彈性 / 按需資源分配以及計量計費等相關聯。 * 傳統企業資料中心位於這兩個極端之間。 -不同的哲學會導致不同的故障處理方式。在超級計算機中,作業通常會不時地會將計算的狀態存檔到持久儲存中。如果一個節點出現故障,通常的解決方案是簡單地停止整個叢集的工作負載。故障節點修復後,計算從上一個檢查點重新開始【7,8】。因此,超級計算機更像是一個單節點計算機而不是分散式系統:透過讓部分失敗升級為完全失敗來處理部分失敗——如果系統的任何部分發生故障,只是讓所有的東西都崩潰(就像單臺機器上的核心恐慌一樣)。 +不同的哲學會導致不同的故障處理方式。在超級計算機中,作業通常會不時地會將計算的狀態存檔到持久儲存中。如果一個節點出現故障,通常的解決方案是簡單地停止整個叢集的工作負載。故障節點修復後,計算從上一個檢查點重新開始【7,8】。因此,超級計算機更像是一個單節點計算機而不是分散式系統:透過讓部分失敗升級為完全失敗來處理部分失敗 —— 如果系統的任何部分發生故障,只是讓所有的東西都崩潰(就像單臺機器上的核心恐慌一樣)。 在本書中,我們將重點放在實現網際網路服務的系統上,這些系統通常與超級計算機看起來有很大不同: -* 許多與網際網路有關的應用程式都是**線上(online)** 的,因為它們需要能夠隨時以低延遲服務使用者。使服務不可用(例如,停止叢集以進行修復)是不可接受的。相比之下,像天氣模擬這樣的離線(批處理)工作可以停止並重新啟動,影響相當小。 +* 許多與網際網路有關的應用程式都是 **線上(online)** 的,因為它們需要能夠隨時以低延遲服務使用者。使服務不可用(例如,停止叢集以進行修復)是不可接受的。相比之下,像天氣模擬這樣的離線(批處理)工作可以停止並重新啟動,影響相當小。 -* 超級計算機通常由專用硬體構建而成,每個節點相當可靠,節點透過共享記憶體和**遠端直接記憶體訪問(RDMA)** 進行通訊。另一方面,雲服務中的節點是由商用機器構建而成的,由於規模經濟,可以以較低的成本提供相同的效能,而且具有較高的故障率。 +* 超級計算機通常由專用硬體構建而成,每個節點相當可靠,節點透過共享記憶體和 **遠端直接記憶體訪問(RDMA)** 進行通訊。另一方面,雲服務中的節點是由商用機器構建而成的,由於規模經濟,可以以較低的成本提供相同的效能,而且具有較高的故障率。 -* 大型資料中心網路通常基於IP和乙太網,以CLOS拓撲排列,以提供更高的對分(bisection)頻寬【9】。超級計算機通常使用專門的網路拓撲結構,例如多維網格和Torus網路 【10】,這為具有已知通訊模式的HPC工作負載提供了更好的效能。 +* 大型資料中心網路通常基於 IP 和乙太網,以 CLOS 拓撲排列,以提供更高的對分(bisection)頻寬【9】。超級計算機通常使用專門的網路拓撲結構,例如多維網格和 Torus 網路 【10】,這為具有已知通訊模式的 HPC 工作負載提供了更好的效能。 * 系統越大,其元件之一就越有可能壞掉。隨著時間的推移,壞掉的東西得到修復,新的東西又壞掉,但是在一個有成千上萬個節點的系統中,有理由認為總是有一些東西是壞掉的【7】。當錯誤處理的策略只由簡單放棄組成時,一個大的系統最終會花費大量時間從錯誤中恢復,而不是做有用的工作【8】。 -* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱[第四章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。 +* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱 [第四章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。 * 在地理位置分散的部署中(保持資料在地理位置上接近使用者以減少訪問延遲),通訊很可能透過網際網路進行,與本地網路相比,通訊速度緩慢且不可靠。超級計算機通常假設它們的所有節點都靠近在一起。 -如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如“[可靠性](ch1.md#可靠性)”中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。 +如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如 “[可靠性](ch1.md#可靠性)” 中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。 即使在只有少數節點的小型系統中,考慮部分故障也是很重要的。在一個小系統中,很可能大部分元件在大部分時間都正常工作。然而,遲早會有一部分系統出現故障,軟體必須以某種方式處理。故障處理必須是軟體設計的一部分,並且作為軟體的運維,你需要知道在發生故障的情況下,軟體可能會表現出怎樣的行為。 @@ -79,74 +79,74 @@ > #### 從不可靠的元件構建可靠的系統 > -> 你可能想知道這是否有意義——直觀地看來,系統只能像其最不可靠的元件(最薄弱的環節)一樣可靠。事實並非如此:事實上,從不太可靠的潛在基礎構建更可靠的系統是計算機領域的一個古老思想【11】。例如: +> 你可能想知道這是否有意義 —— 直觀地看來,系統只能像其最不可靠的元件(最薄弱的環節)一樣可靠。事實並非如此:事實上,從不太可靠的潛在基礎構建更可靠的系統是計算機領域的一個古老思想【11】。例如: > > * 糾錯碼允許數字資料在通訊通道上準確傳輸,偶爾會出現一些錯誤,例如由於無線網路上的無線電干擾【12】。 > * **網際網路協議(Internet Protocol, IP)** 不可靠:可能丟棄、延遲、重複或重排資料包。 傳輸控制協議(Transmission Control Protocol, TCP)在網際網路協議(IP)之上提供了更可靠的傳輸層:它確保丟失的資料包被重新傳輸,消除重複,並且資料包被重新組裝成它們被傳送的順序。 > -> 雖然這個系統可以比它的底層部分更可靠,但它的可靠性總是有限的。例如,糾錯碼可以處理少量的單位元錯誤,但是如果你的訊號被幹擾所淹沒,那麼透過通道可以得到多少資料,是有根本性的限制的【13】。 TCP可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。 +> 雖然這個系統可以比它的底層部分更可靠,但它的可靠性總是有限的。例如,糾錯碼可以處理少量的單位元錯誤,但是如果你的訊號被幹擾所淹沒,那麼透過通道可以得到多少資料,是有根本性的限制的【13】。 TCP 可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。 > -> 雖然更可靠的高階系統並不完美,但它仍然有用,因為它處理了一些棘手的低階錯誤,所以其餘的錯誤通常更容易推理和處理。我們將在“[資料庫的端到端原則](ch12.md#資料庫的端到端原則)”中進一步探討這個問題。 +> 雖然更可靠的高階系統並不完美,但它仍然有用,因為它處理了一些棘手的低階錯誤,所以其餘的錯誤通常更容易推理和處理。我們將在 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 中進一步探討這個問題。 ## 不可靠的網路 -正如在[第二部分](part-ii.md)的介紹中所討論的那樣,我們在本書中關注的分散式系統是無共享的系統,即透過網路連線的一堆機器。網路是這些機器可以通訊的唯一途徑——我們假設每臺機器都有自己的記憶體和磁碟,一臺機器不能訪問另一臺機器的記憶體或磁碟(除了透過網路向伺服器發出請求)。 +正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,我們在本書中關注的分散式系統是無共享的系統,即透過網路連線的一堆機器。網路是這些機器可以通訊的唯一途徑 —— 我們假設每臺機器都有自己的記憶體和磁碟,一臺機器不能訪問另一臺機器的記憶體或磁碟(除了透過網路向伺服器發出請求)。 -**無共享**並不是構建系統的唯一方式,但它已經成為構建網際網路服務的主要方式,其原因如下:相對便宜,因為它不需要特殊的硬體,可以利用商品化的雲端計算服務,透過跨多個地理分佈的資料中心進行冗餘可以實現高可靠性。 +**無共享** 並不是構建系統的唯一方式,但它已經成為構建網際網路服務的主要方式,其原因如下:相對便宜,因為它不需要特殊的硬體,可以利用商品化的雲端計算服務,透過跨多個地理分佈的資料中心進行冗餘可以實現高可靠性。 -網際網路和資料中心(通常是乙太網)中的大多數內部網路都是**非同步分組網路(asynchronous packet networks)**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果你傳送請求並期待響應,則很多事情可能會出錯(其中一些如[圖8-1](../img/fig8-1.png)所示): +網際網路和資料中心(通常是乙太網)中的大多數內部網路都是 **非同步分組網路(asynchronous packet networks)**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果你傳送請求並期待響應,則很多事情可能會出錯(其中一些如 [圖 8-1](../img/fig8-1.png) 所示): 1. 請求可能已經丟失(可能有人拔掉了網線)。 2. 請求可能正在排隊,稍後將交付(也許網路或接收方過載)。 3. 遠端節點可能已經失效(可能是崩潰或關機)。 -4. 遠端節點可能暫時停止了響應(可能會遇到長時間的垃圾回收暫停;請參閱“[程序暫停](#程序暫停)”),但稍後會再次響應。 +4. 遠端節點可能暫時停止了響應(可能會遇到長時間的垃圾回收暫停;請參閱 “[程序暫停](#程序暫停)”),但稍後會再次響應。 5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。 6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。 ![](../img/fig8-1.png) -**圖8-1 如果傳送請求並沒有得到響應,則無法區分(a)請求是否丟失,(b)遠端節點是否關閉,或(c)響應是否丟失。** +**圖 8-1 如果傳送請求並沒有得到響應,則無法區分(a)請求是否丟失,(b)遠端節點是否關閉,或(c)響應是否丟失。** 傳送者甚至不能分辨資料包是否被傳送:唯一的選擇是讓接收者傳送響應訊息,這可能會丟失或延遲。這些問題在非同步網路中難以區分:你所擁有的唯一資訊是,你尚未收到響應。如果你向另一個節點發送請求並且沒有收到響應,則不可能判斷是什麼原因。 -處理這個問題的通常方法是**超時(Timeout)**:在一段時間之後放棄等待,並且認為響應不會到達。但是,當發生超時時,你仍然不知道遠端節點是否收到了請求(如果請求仍然在某個地方排隊,那麼即使傳送者已經放棄了該請求,仍然可能會將其傳送給接收者)。 +處理這個問題的通常方法是 **超時(Timeout)**:在一段時間之後放棄等待,並且認為響應不會到達。但是,當發生超時時,你仍然不知道遠端節點是否收到了請求(如果請求仍然在某個地方排隊,那麼即使傳送者已經放棄了該請求,仍然可能會將其傳送給接收者)。 ### 真實世界的網路故障 -我們幾十年來一直在建設計算機網路——有人可能希望現在我們已經找出了使網路變得可靠的方法。但是現在似乎還沒有成功。 +我們幾十年來一直在建設計算機網路 —— 有人可能希望現在我們已經找出了使網路變得可靠的方法。但是現在似乎還沒有成功。 -有一些系統的研究和大量的軼事證據表明,即使在像一家公司運營的資料中心那樣的受控環境中,網路問題也可能出乎意料地普遍。在一家中型資料中心進行的一項研究發現,每個月大約有12個網路故障,其中一半斷開一臺機器,一半斷開整個機架【15】。另一項研究測量了架頂式交換機,匯聚交換機和負載平衡器等元件的故障率【16】。它發現新增冗餘網路裝置不會像你所希望的那樣減少故障,因為它不能防範人為錯誤(例如,錯誤配置的交換機),這是造成中斷的主要原因。 +有一些系統的研究和大量的軼事證據表明,即使在像一家公司運營的資料中心那樣的受控環境中,網路問題也可能出乎意料地普遍。在一家中型資料中心進行的一項研究發現,每個月大約有 12 個網路故障,其中一半斷開一臺機器,一半斷開整個機架【15】。另一項研究測量了架頂式交換機,匯聚交換機和負載平衡器等元件的故障率【16】。它發現新增冗餘網路裝置不會像你所希望的那樣減少故障,因為它不能防範人為錯誤(例如,錯誤配置的交換機),這是造成中斷的主要原因。 -諸如EC2之類的公有云服務因頻繁的暫態網路故障而臭名昭著【14】,管理良好的私有資料中心網路可能是更穩定的環境。儘管如此,沒有人不受網路問題的困擾:例如,交換機軟體升級過程中的一個問題可能會引發網路拓撲重構,在此期間網路資料包可能會延遲超過一分鐘【17】。鯊魚可能咬住海底電纜並損壞它們 【18】。其他令人驚訝的故障包括網路介面有時會丟棄所有入站資料包,但是成功傳送出站資料包 【19】:僅僅因為網路連結在一個方向上工作,並不能保證它也在相反的方向工作。 +諸如 EC2 之類的公有云服務因頻繁的暫態網路故障而臭名昭著【14】,管理良好的私有資料中心網路可能是更穩定的環境。儘管如此,沒有人不受網路問題的困擾:例如,交換機軟體升級過程中的一個問題可能會引發網路拓撲重構,在此期間網路資料包可能會延遲超過一分鐘【17】。鯊魚可能咬住海底電纜並損壞它們 【18】。其他令人驚訝的故障包括網路介面有時會丟棄所有入站資料包,但是成功傳送出站資料包 【19】:僅僅因為網路連結在一個方向上工作,並不能保證它也在相反的方向工作。 > #### 網路分割槽 > -> 當網路的一部分由於網路故障而被切斷時,有時稱為**網路分割槽(network partition)** 或**網路斷裂(netsplit)**。在本書中,我們通常會堅持使用更一般的術語**網路故障(network fault)**,以避免與[第六章](ch6.md)討論的儲存系統的分割槽(分片)相混淆。 +> 當網路的一部分由於網路故障而被切斷時,有時稱為 **網路分割槽(network partition)** 或 **網路斷裂(netsplit)**。在本書中,我們通常會堅持使用更一般的術語 **網路故障(network fault)**,以避免與 [第六章](ch6.md) 討論的儲存系統的分割槽(分片)相混淆。 即使網路故障在你的環境中非常罕見,故障可能發生的事實,意味著你的軟體需要能夠處理它們。無論何時透過網路進行通訊,都可能會失敗,這是無法避免的。 -如果網路故障的錯誤處理沒有定義與測試,武斷地講,各種錯誤可能都會發生:例如,即使網路恢復【20】,叢集可能會發生**死鎖**,永久無法為請求提供服務,甚至可能會刪除所有的資料【21】。如果軟體被置於意料之外的情況下,它可能會做出出乎意料的事情。 +如果網路故障的錯誤處理沒有定義與測試,武斷地講,各種錯誤可能都會發生:例如,即使網路恢復【20】,叢集可能會發生 **死鎖**,永久無法為請求提供服務,甚至可能會刪除所有的資料【21】。如果軟體被置於意料之外的情況下,它可能會做出出乎意料的事情。 -處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是Chaos Monkey背後的想法;請參閱“[可靠性](ch1.md#可靠性)”)。 +處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是 Chaos Monkey 背後的想法;請參閱 “[可靠性](ch1.md#可靠性)”)。 ### 檢測故障 許多系統需要自動檢測故障節點。例如: -* 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即out of rotation)。 -* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。 +* 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即 out of rotation)。 +* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。 不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,你可能會收到一些反饋資訊,明確告訴你某些事情沒有成功: -* 如果你可以連線到執行節點的機器,但沒有程序正在偵聽目標埠(例如,因為程序崩潰),作業系統將透過傳送FIN或RST來關閉並重用TCP連線。但是,如果節點在處理請求時發生崩潰,則無法知道遠端節點實際處理了多少資料【22】。 -* 如果節點程序崩潰(或被管理員殺死),但節點的作業系統仍在執行,則指令碼可以通知其他節點有關該崩潰的資訊,以便另一個節點可以快速接管,而無需等待超時到期。例如,HBase就是這麼做的【23】。 +* 如果你可以連線到執行節點的機器,但沒有程序正在偵聽目標埠(例如,因為程序崩潰),作業系統將透過傳送 FIN 或 RST 來關閉並重用 TCP 連線。但是,如果節點在處理請求時發生崩潰,則無法知道遠端節點實際處理了多少資料【22】。 +* 如果節點程序崩潰(或被管理員殺死),但節點的作業系統仍在執行,則指令碼可以通知其他節點有關該崩潰的資訊,以便另一個節點可以快速接管,而無需等待超時到期。例如,HBase 就是這麼做的【23】。 * 如果你有權訪問資料中心網路交換機的管理介面,則可以透過它們檢測硬體級別的鏈路故障(例如,遠端機器是否關閉電源)。如果你透過網際網路連線,或者如果你處於共享資料中心而無法訪問交換機,或者由於網路問題而無法訪問管理介面,則排除此選項。 -* 如果路由器確認你嘗試連線的IP地址不可用,則可能會使用ICMP目標不可達資料包回覆你。但是,路由器不具備神奇的故障檢測能力——它受到與網路其他參與者相同的限制。 +* 如果路由器確認你嘗試連線的 IP 地址不可用,則可能會使用 ICMP 目標不可達資料包回覆你。但是,路由器不具備神奇的故障檢測能力 —— 它受到與網路其他參與者相同的限制。 -關於遠端節點關閉的快速反饋很有用,但是你不能指望它。即使TCP確認已經傳送了一個數據包,應用程式在處理之前可能已經崩潰。如果你想確保一個請求是成功的,你需要應用程式本身的正確響應【24】。 +關於遠端節點關閉的快速反饋很有用,但是你不能指望它。即使 TCP 確認已經傳送了一個數據包,應用程式在處理之前可能已經崩潰。如果你想確保一個請求是成功的,你需要應用程式本身的正確響應【24】。 -相反,如果出了什麼問題,你可能會在堆疊的某個層次上得到一個錯誤響應,但總的來說,你必須假設你可能根本就得不到任何迴應。你可以重試幾次(TCP重試是透明的,但是你也可以在應用程式級別重試),等待超時過期,並且如果在超時時間內沒有收到響應,則最終宣告節點已經死亡。 +相反,如果出了什麼問題,你可能會在堆疊的某個層次上得到一個錯誤響應,但總的來說,你必須假設你可能根本就得不到任何迴應。你可以重試幾次(TCP 重試是透明的,但是你也可以在應用程式級別重試),等待超時過期,並且如果在超時時間內沒有收到響應,則最終宣告節點已經死亡。 ### 超時與無窮的延遲 @@ -154,87 +154,87 @@ 長時間的超時意味著長時間等待,直到一個節點被宣告死亡(在這段時間內,使用者可能不得不等待,或者看到錯誤資訊)。短的超時可以更快地檢測到故障,但有更高地風險誤將一個節點宣佈為失效,而該節點實際上只是暫時地變慢了(例如由於節點或網路上的負載峰值)。 -過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在“[知識、真相與謊言](#知識、真相與謊言)”以及[第九章](ch9.md)和[第十一章](ch11.md)中更詳細地討論這個問題。 +過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在 “[知識、真相與謊言](#知識、真相與謊言)” 以及 [第九章](ch9.md) 和 [第十一章](ch11.md) 中更詳細地討論這個問題。 -當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致**級聯失效**(即cascading failure,表示在極端情況下,所有節點都宣告對方死亡,所有節點都將停止工作)。 +當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致 **級聯失效**(即 cascading failure,表示在極端情況下,所有節點都宣告對方死亡,所有節點都將停止工作)。 -設想一個虛構的系統,其網路可以保證資料包的最大延遲——每個資料包要麼在一段時間內傳送,要麼丟失,但是傳遞永遠不會比$d$更長。此外,假設你可以保證一個非故障節點總是在一段時間內處理一個請求$r$。在這種情況下,你可以保證每個成功的請求在$2d + r$時間內都能收到響應,如果你在此時間內沒有收到響應,則知道網路或遠端節點不工作。如果這是成立的,$2d + r$ 會是一個合理的超時設定。 +設想一個虛構的系統,其網路可以保證資料包的最大延遲 —— 每個資料包要麼在一段時間內傳送,要麼丟失,但是傳遞永遠不會比 $d$ 更長。此外,假設你可以保證一個非故障節點總是在一段時間內處理一個請求 $r$。在這種情況下,你可以保證每個成功的請求在 $2d + r$ 時間內都能收到響應,如果你在此時間內沒有收到響應,則知道網路或遠端節點不工作。如果這是成立的,$2d + r$ 會是一個合理的超時設定。 -不幸的是,我們所使用的大多數系統都沒有這些保證:非同步網路具有無限的延遲(即儘可能快地傳送資料包,但資料包到達可能需要的時間沒有上限),並且大多數伺服器實現並不能保證它們可以在一定的最大時間內處理請求(請參閱“[響應時間保證](#響應時間保證)”)。對於故障檢測,即使系統大部分時間快速執行也是不夠的:如果你的超時時間很短,往返時間只需要一個瞬時尖峰就可以使系統失衡。 +不幸的是,我們所使用的大多數系統都沒有這些保證:非同步網路具有無限的延遲(即儘可能快地傳送資料包,但資料包到達可能需要的時間沒有上限),並且大多數伺服器實現並不能保證它們可以在一定的最大時間內處理請求(請參閱 “[響應時間保證](#響應時間保證)”)。對於故障檢測,即使系統大部分時間快速執行也是不夠的:如果你的超時時間很短,往返時間只需要一個瞬時尖峰就可以使系統失衡。 #### 網路擁塞和排隊 在駕駛汽車時,由於交通擁堵,道路交通網路的通行時間往往不盡相同。同樣,計算機網路上資料包延遲的可變性通常是由於排隊【25】: -* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如[圖8-2](../img/fig8-2.png)所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。 -* 當資料包到達目標機器時,如果所有CPU核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。 -* 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用CPU核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊(緩衝),進一步增加了網路延遲的可變性。 -* TCP執行**流量控制**(flow control,也稱為**擁塞避免**,即congestion avoidance,或**背壓**,即backpressure),其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前,在傳送者處就需要進行額外的排隊。 +* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如 [圖 8-2](../img/fig8-2.png) 所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。 +* 當資料包到達目標機器時,如果所有 CPU 核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。 +* 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用 CPU 核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊(緩衝),進一步增加了網路延遲的可變性。 +* TCP 執行 **流量控制**(flow control,也稱為 **擁塞避免**,即 congestion avoidance,或 **背壓**,即 backpressure),其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前,在傳送者處就需要進行額外的排隊。 ![](../img/fig8-2.png) -**圖8-2 如果有多臺機器將網路流量傳送到同一目的地,則其交換機佇列可能會被填滿。在這裡,埠1,2和4都試圖傳送資料包到埠3** +**圖 8-2 如果有多臺機器將網路流量傳送到同一目的地,則其交換機佇列可能會被填滿。在這裡,埠 1,2 和 4 都試圖傳送資料包到埠 3** -而且,如果TCP在某個超時時間內沒有被確認(這是根據觀察的往返時間計算的),則認為資料包丟失,丟失的資料包將自動重新發送。儘管應用程式沒有看到資料包丟失和重新傳輸,但它看到了延遲(等待超時到期,然後等待重新傳輸的資料包得到確認)。 +而且,如果 TCP 在某個超時時間內沒有被確認(這是根據觀察的往返時間計算的),則認為資料包丟失,丟失的資料包將自動重新發送。儘管應用程式沒有看到資料包丟失和重新傳輸,但它看到了延遲(等待超時到期,然後等待重新傳輸的資料包得到確認)。 > #### TCP與UDP > -> 一些對延遲敏感的應用程式,比如影片會議和IP語音(VoIP),使用了UDP而不是TCP。這是在可靠性和和延遲變化之間的折衷:由於UDP不執行流量控制並且不重傳丟失的分組,所以避免了網路延遲變化的一些原因(儘管它仍然易受切換佇列和排程延遲的影響)。 +> 一些對延遲敏感的應用程式,比如影片會議和 IP 語音(VoIP),使用了 UDP 而不是 TCP。這是在可靠性和和延遲變化之間的折衷:由於 UDP 不執行流量控制並且不重傳丟失的分組,所以避免了網路延遲變化的一些原因(儘管它仍然易受切換佇列和排程延遲的影響)。 > -> 在延遲資料毫無價值的情況下,UDP是一個不錯的選擇。例如,在VoIP電話呼叫中,可能沒有足夠的時間重新發送丟失的資料包,並在揚聲器上播放資料。在這種情況下,重發資料包沒有意義——應用程式必須使用靜音填充丟失資料包的時隙(導致聲音短暫中斷),然後在資料流中繼續。重試發生在人類層。 (“你能再說一遍嗎?聲音剛剛斷了一會兒。“) +> 在延遲資料毫無價值的情況下,UDP 是一個不錯的選擇。例如,在 VoIP 電話呼叫中,可能沒有足夠的時間重新發送丟失的資料包,並在揚聲器上播放資料。在這種情況下,重發資料包沒有意義 —— 應用程式必須使用靜音填充丟失資料包的時隙(導致聲音短暫中斷),然後在資料流中繼續。重試發生在人類層。 (“你能再說一遍嗎?聲音剛剛斷了一會兒。“) 所有這些因素都會造成網路延遲的變化。當系統接近其最大容量時,排隊延遲的變化範圍特別大:擁有足夠備用容量的系統可以輕鬆排空佇列,而在高利用率的系統中,很快就能積累很長的佇列。 -在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和CPU(在虛擬機器上執行時)。批處理工作負載(如MapReduce,請參閱[第十章](ch10.md))能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況,如果附近的某個人(嘈雜的鄰居)正在使用大量資源,則網路延遲可能會發生劇烈變化【28,29】。 +在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和 CPU(在虛擬機器上執行時)。批處理工作負載(如 MapReduce,請參閱 [第十章](ch10.md))能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況,如果附近的某個人(嘈雜的鄰居)正在使用大量資源,則網路延遲可能會發生劇烈變化【28,29】。 -在這種環境下,你只能透過實驗方式選擇超時:在一段較長的時期內、在多臺機器上測量網路往返時間的分佈,以確定延遲的預期變化。然後,考慮到應用程式的特性,可以確定**故障檢測延遲**與**過早超時風險**之間的適當折衷。 +在這種環境下,你只能透過實驗方式選擇超時:在一段較長的時期內、在多臺機器上測量網路往返時間的分佈,以確定延遲的預期變化。然後,考慮到應用程式的特性,可以確定 **故障檢測延遲** 與 **過早超時風險** 之間的適當折衷。 -更好的一種做法是,系統不是使用配置的常量超時時間,而是連續測量響應時間及其變化(抖動),並根據觀察到的響應時間分佈自動調整超時時間。這可以透過Phi Accrual故障檢測器【30】來完成,該檢測器在例如Akka和Cassandra 【31】中使用。 TCP的超時重傳機制也是以類似的方式工作【27】。 +更好的一種做法是,系統不是使用配置的常量超時時間,而是連續測量響應時間及其變化(抖動),並根據觀察到的響應時間分佈自動調整超時時間。這可以透過 Phi Accrual 故障檢測器【30】來完成,該檢測器在例如 Akka 和 Cassandra 【31】中使用。 TCP 的超時重傳機制也是以類似的方式工作【27】。 ### 同步網路與非同步網路 -如果我們可以依靠網路來傳遞一些**最大延遲固定**的資料包,而不是丟棄資料包,那麼分散式系統就會簡單得多。為什麼我們不能在硬體層面上解決這個問題,使網路可靠,使軟體不必擔心呢? +如果我們可以依靠網路來傳遞一些 **最大延遲固定** 的資料包,而不是丟棄資料包,那麼分散式系統就會簡單得多。為什麼我們不能在硬體層面上解決這個問題,使網路可靠,使軟體不必擔心呢? -為了回答這個問題,將資料中心網路與非常可靠的傳統固定電話網路(非蜂窩,非VoIP)進行比較是很有趣的:延遲音訊幀和掉話是非常罕見的。一個電話需要一個很低的端到端延遲,以及足夠的頻寬來傳輸你聲音的音訊取樣資料。在計算機網路中有類似的可靠性和可預測性不是很好嗎? +為了回答這個問題,將資料中心網路與非常可靠的傳統固定電話網路(非蜂窩,非 VoIP)進行比較是很有趣的:延遲音訊幀和掉話是非常罕見的。一個電話需要一個很低的端到端延遲,以及足夠的頻寬來傳輸你聲音的音訊取樣資料。在計算機網路中有類似的可靠性和可預測性不是很好嗎? -當你透過電話網路撥打電話時,它會建立一個電路:在兩個呼叫者之間的整個路線上為呼叫分配一個固定的,有保證的頻寬量。這個電路會保持至通話結束【32】。例如,ISDN網路以每秒4000幀的固定速率執行。呼叫建立時,每個幀內(每個方向)分配16位空間。因此,在通話期間,每一方都保證能夠每250微秒傳送一個精確的16位音訊資料【33,34】。 +當你透過電話網路撥打電話時,它會建立一個電路:在兩個呼叫者之間的整個路線上為呼叫分配一個固定的,有保證的頻寬量。這個電路會保持至通話結束【32】。例如,ISDN 網路以每秒 4000 幀的固定速率執行。呼叫建立時,每個幀內(每個方向)分配 16 位空間。因此,在通話期間,每一方都保證能夠每 250 微秒傳送一個精確的 16 位音訊資料【33,34】。 -這種網路是同步的:即使資料經過多個路由器,也不會受到排隊的影響,因為呼叫的16位空間已經在網路的下一跳中保留了下來。而且由於沒有排隊,網路的最大端到端延遲是固定的。我們稱之為**有限延遲(bounded delay)**。 +這種網路是同步的:即使資料經過多個路由器,也不會受到排隊的影響,因為呼叫的 16 位空間已經在網路的下一跳中保留了下來。而且由於沒有排隊,網路的最大端到端延遲是固定的。我們稱之為 **有限延遲(bounded delay)**。 #### 我們不能簡單地使網路延遲可預測嗎? -請注意,電話網路中的電路與TCP連線有很大不同:電路是固定數量的預留頻寬,在電路建立時沒有其他人可以使用,而TCP連線的資料包**機會性地**使用任何可用的網路頻寬。你可以給TCP一個可變大小的資料塊(例如,一個電子郵件或一個網頁),它會盡可能在最短的時間內傳輸它。 TCP連線空閒時,不使用任何頻寬[^ii]。 +請注意,電話網路中的電路與 TCP 連線有很大不同:電路是固定數量的預留頻寬,在電路建立時沒有其他人可以使用,而 TCP 連線的資料包 **機會性地** 使用任何可用的網路頻寬。你可以給 TCP 一個可變大小的資料塊(例如,一個電子郵件或一個網頁),它會盡可能在最短的時間內傳輸它。 TCP 連線空閒時,不使用任何頻寬 [^ii]。 -[^ii]: 除了偶爾的keepalive資料包,如果TCP keepalive被啟用。 +[^ii]: 除了偶爾的 keepalive 資料包,如果 TCP keepalive 被啟用。 -如果資料中心網路和網際網路是電路交換網路,那麼在建立電路時就可以建立一個受保證的最大往返時間。但是,它們並不是:乙太網和IP是**分組交換協議**,不得不忍受排隊的折磨,及其導致的網路無限延遲。這些協議沒有電路的概念。 +如果資料中心網路和網際網路是電路交換網路,那麼在建立電路時就可以建立一個受保證的最大往返時間。但是,它們並不是:乙太網和 IP 是 **分組交換協議**,不得不忍受排隊的折磨,及其導致的網路無限延遲。這些協議沒有電路的概念。 -為什麼資料中心網路和網際網路使用分組交換?答案是,它們針對**突發流量(bursty traffic)** 進行了最佳化。一個電路適用於音訊或影片通話,在通話期間需要每秒傳送相當數量的位元。另一方面,請求網頁,傳送電子郵件或傳輸檔案沒有任何特定的頻寬要求——我們只是希望它儘快完成。 +為什麼資料中心網路和網際網路使用分組交換?答案是,它們針對 **突發流量(bursty traffic)** 進行了最佳化。一個電路適用於音訊或影片通話,在通話期間需要每秒傳送相當數量的位元。另一方面,請求網頁,傳送電子郵件或傳輸檔案沒有任何特定的頻寬要求 —— 我們只是希望它儘快完成。 -如果想透過電路傳輸檔案,你得預測一個頻寬分配。如果你猜的太低,傳輸速度會不必要的太慢,導致網路容量閒置。如果你猜的太高,電路就無法建立(因為如果無法保證其頻寬分配,網路不能建立電路)。因此,將電路用於突發資料傳輸會浪費網路容量,並且使傳輸不必要地緩慢。相比之下,TCP動態調整資料傳輸速率以適應可用的網路容量。 +如果想透過電路傳輸檔案,你得預測一個頻寬分配。如果你猜的太低,傳輸速度會不必要的太慢,導致網路容量閒置。如果你猜的太高,電路就無法建立(因為如果無法保證其頻寬分配,網路不能建立電路)。因此,將電路用於突發資料傳輸會浪費網路容量,並且使傳輸不必要地緩慢。相比之下,TCP 動態調整資料傳輸速率以適應可用的網路容量。 -已經有一些嘗試去建立同時支援電路交換和分組交換的混合網路,比如ATM[^iii]。InfiniBand有一些相似之處【35】:它在鏈路層實現了端到端的流量控制,從而減少了在網路中排隊的需要,儘管它仍然可能因鏈路擁塞而受到延遲【36】。透過仔細使用**服務質量**(quality of service,即QoS,資料包的優先順序和排程)和**准入控制**(admission control,限速傳送器),可以在分組網路上類比電路交換,或提供統計上的**有限延遲**【25,32】。 +已經有一些嘗試去建立同時支援電路交換和分組交換的混合網路,比如 ATM [^iii]。InfiniBand 有一些相似之處【35】:它在鏈路層實現了端到端的流量控制,從而減少了在網路中排隊的需要,儘管它仍然可能因鏈路擁塞而受到延遲【36】。透過仔細使用 **服務質量**(quality of service,即 QoS,資料包的優先順序和排程)和 **准入控制**(admission control,限速傳送器),可以在分組網路上類比電路交換,或提供統計上的 **有限延遲**【25,32】。 -[^iii]: **非同步傳輸模式(Asynchronous Transfer Mode, ATM)** 在20世紀80年代是乙太網的競爭對手【32】,但在電話網核心交換機之外並沒有得到太多的採用。它與自動櫃員機(也稱為自動取款機)無關,儘管共用一個縮寫詞。或許,在一些平行的世界裡,網際網路是基於像ATM這樣的東西,因此它們的網際網路影片通話可能比我們的更可靠,因為它們不會遭受包的丟失和延遲。 +[^iii]: **非同步傳輸模式(Asynchronous Transfer Mode, ATM)** 在 20 世紀 80 年代是乙太網的競爭對手【32】,但在電話網核心交換機之外並沒有得到太多的採用。它與自動櫃員機(也稱為自動取款機)無關,儘管共用一個縮寫詞。或許,在一些平行的世界裡,網際網路是基於像 ATM 這樣的東西,因此它們的網際網路影片通話可能比我們的更可靠,因為它們不會遭受包的丟失和延遲。 -但是,目前在多租戶資料中心和公共雲或透過網際網路[^iv]進行通訊時,此類服務質量尚未啟用。當前部署的技術不允許我們對網路的延遲或可靠性作出任何保證:我們必須假設網路擁塞,排隊和無限的延遲總是會發生。因此,超時時間沒有“正確”的值——它需要透過實驗來確定。 +但是,目前在多租戶資料中心和公共雲或透過網際網路 [^iv] 進行通訊時,此類服務質量尚未啟用。當前部署的技術不允許我們對網路的延遲或可靠性作出任何保證:我們必須假設網路擁塞,排隊和無限的延遲總是會發生。因此,超時時間沒有 “正確” 的值 —— 它需要透過實驗來確定。 -[^iv]: 網際網路服務提供商之間的對等協議和透過**BGP閘道器協議(BGP)** 建立的路由,與IP協議相比,更接近於電路交換。在這個級別上,可以購買專用頻寬。但是,網際網路路由在網路級別執行,而不是主機之間的單獨連線,而且執行時間要長得多。 +[^iv]: 網際網路服務提供商之間的對等協議和透過 **BGP 閘道器協議(BGP)** 建立的路由,與 IP 協議相比,更接近於電路交換。在這個級別上,可以購買專用頻寬。但是,網際網路路由在網路級別執行,而不是主機之間的單獨連線,而且執行時間要長得多。 > ### 延遲和資源利用 > -> 更一般地說,可以將**延遲變化**視為**動態資源分割槽**的結果。 +> 更一般地說,可以將 **延遲變化** 視為 **動態資源分割槽** 的結果。 > -> 假設兩臺電話交換機之間有一條線路,可以同時進行10,000個呼叫。透過此線路切換的每個電路都佔用其中一個呼叫插槽。因此,你可以將線路視為可由多達10,000個併發使用者共享的資源。資源以靜態方式分配:即使你現在是電話上唯一的電話,並且所有其他9,999個插槽都未使用,你的電路仍將分配與導線充分利用時相同的固定數量的頻寬。 +> 假設兩臺電話交換機之間有一條線路,可以同時進行 10,000 個呼叫。透過此線路切換的每個電路都佔用其中一個呼叫插槽。因此,你可以將線路視為可由多達 10,000 個併發使用者共享的資源。資源以靜態方式分配:即使你現在是電話上唯一的電話,並且所有其他 9,999 個插槽都未使用,你的電路仍將分配與導線充分利用時相同的固定數量的頻寬。 > > 相比之下,網際網路動態分享網路頻寬。傳送者互相推擠和爭奪,以讓他們的資料包儘可能快地透過網路,並且網路交換機決定從一個時刻到另一個時刻傳送哪個分組(即,頻寬分配)。這種方法有排隊的缺點,但其優點是它最大限度地利用了電線。電線固定成本,所以如果你更好地利用它,你透過電線傳送的每個位元組都會更便宜。 > -> CPU也會出現類似的情況:如果你在多個執行緒間動態共享每個CPU核心,則一個執行緒有時必須在作業系統的執行佇列裡等待,而另一個執行緒正在執行,這樣每個執行緒都有可能被暫停一個不定的時間長度。但是,與為每個執行緒分配靜態數量的CPU週期相比,這會更好地利用硬體(請參閱“[響應時間保證](#響應時間保證)”)。更好的硬體利用率也是使用虛擬機器的重要動機。 +> CPU 也會出現類似的情況:如果你在多個執行緒間動態共享每個 CPU 核心,則一個執行緒有時必須在作業系統的執行佇列裡等待,而另一個執行緒正在執行,這樣每個執行緒都有可能被暫停一個不定的時間長度。但是,與為每個執行緒分配靜態數量的 CPU 週期相比,這會更好地利用硬體(請參閱 “[響應時間保證](#響應時間保證)”)。更好的硬體利用率也是使用虛擬機器的重要動機。 > -> 如果資源是靜態分割槽的(例如,專用硬體和專用頻寬分配),則在某些環境中可以實現**延遲保證**。但是,這是以降低利用率為代價的——換句話說,它是更昂貴的。另一方面,動態資源分配的多租戶提供了更好的利用率,所以它更便宜,但它具有可變延遲的缺點。 +> 如果資源是靜態分割槽的(例如,專用硬體和專用頻寬分配),則在某些環境中可以實現 **延遲保證**。但是,這是以降低利用率為代價的 —— 換句話說,它是更昂貴的。另一方面,動態資源分配的多租戶提供了更好的利用率,所以它更便宜,但它具有可變延遲的缺點。 > -> 網路中的可變延遲不是一種自然規律,而只是成本/收益權衡的結果。 +> 網路中的可變延遲不是一種自然規律,而只是成本 / 收益權衡的結果。 ## 不可靠的時鐘 @@ -242,7 +242,7 @@ 時鐘和時間很重要。應用程式以各種方式依賴於時鐘來回答以下問題: 1. 這個請求是否超時了? -2. 這項服務的第99百分位響應時間是多少? +2. 這項服務的第 99 百分位響應時間是多少? 3. 在過去五分鐘內,該服務平均每秒處理多少個查詢? 4. 使用者在我們的網站上花了多長時間? 5. 這篇文章在何時釋出? @@ -250,11 +250,11 @@ 7. 這個快取條目何時到期? 8. 日誌檔案中此錯誤訊息的時間戳是什麼? -[例1-4](ch1.md)測量了**持續時間**(durations,例如,請求傳送與響應接收之間的時間間隔),而[例5-8](ch5.md)描述了**時間點**(point in time,在特定日期和和特定時間發生的事件)。 +[例 1-4](ch1.md) 測量了 **持續時間**(durations,例如,請求傳送與響應接收之間的時間間隔),而 [例 5-8](ch5.md) 描述了 **時間點**(point in time,在特定日期和和特定時間發生的事件)。 在分散式系統中,時間是一件棘手的事情,因為通訊不是即時的:訊息透過網路從一臺機器傳送到另一臺機器需要時間。收到訊息的時間總是晚於傳送的時間,但是由於網路中的可變延遲,我們不知道晚了多少時間。這個事實導致有時很難確定在涉及多臺機器時發生事情的順序。 -而且,網路上的每臺機器都有自己的時鐘,這是一個實際的硬體裝置:通常是石英晶體振盪器。這些裝置不是完全準確的,所以每臺機器都有自己的時間概念,可能比其他機器稍快或更慢。可以在一定程度上同步時鐘:最常用的機制是**網路時間協議(NTP)**,它允許根據一組伺服器報告的時間來調整計算機時鐘【37】。伺服器則從更精確的時間源(如GPS接收機)獲取時間。 +而且,網路上的每臺機器都有自己的時鐘,這是一個實際的硬體裝置:通常是石英晶體振盪器。這些裝置不是完全準確的,所以每臺機器都有自己的時間概念,可能比其他機器稍快或更慢。可以在一定程度上同步時鐘:最常用的機制是 **網路時間協議(NTP)**,它允許根據一組伺服器報告的時間來調整計算機時鐘【37】。伺服器則從更精確的時間源(如 GPS 接收機)獲取時間。 ### 單調鍾與日曆時鐘 @@ -262,50 +262,50 @@ #### 日曆時鐘 -日曆時鐘是你直觀地瞭解時鐘的依據:它根據某個日曆(也稱為**掛鐘時間**,即wall-clock time)返回當前日期和時間。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC時間1970年1月1日午夜)以來的秒數(或毫秒),根據公曆(Gregorian)日曆,不包括閏秒。有些系統使用其他日期作為參考點。 +日曆時鐘是你直觀地瞭解時鐘的依據:它根據某個日曆(也稱為 **掛鐘時間**,即 wall-clock time)返回當前日期和時間。例如,Linux 上的 `clock_gettime(CLOCK_REALTIME)`[^v] 和 Java 中的 `System.currentTimeMillis()` 返回自 epoch(UTC 時間 1970 年 1 月 1 日午夜)以來的秒數(或毫秒),根據公曆(Gregorian)日曆,不包括閏秒。有些系統使用其他日期作為參考點。 -[^v]: 雖然該時鐘被稱為實時時鐘,但它與實時作業系統無關,如“[響應時間保證](#響應時間保證)”中所述。 +[^v]: 雖然該時鐘被稱為實時時鐘,但它與實時作業系統無關,如 “[響應時間保證](#響應時間保證)” 中所述。 -日曆時鐘通常與NTP同步,這意味著來自一臺機器的時間戳(理想情況下)與另一臺機器上的時間戳相同。但是如下節所述,日曆時鐘也具有各種各樣的奇特之處。特別是,如果本地時鐘在NTP伺服器之前太遠,則它可能會被強制重置,看上去好像跳回了先前的時間點。這些跳躍以及他們經常忽略閏秒的事實,使日曆時鐘不能用於測量經過時間(elapsed time)【38】。 +日曆時鐘通常與 NTP 同步,這意味著來自一臺機器的時間戳(理想情況下)與另一臺機器上的時間戳相同。但是如下節所述,日曆時鐘也具有各種各樣的奇特之處。特別是,如果本地時鐘在 NTP 伺服器之前太遠,則它可能會被強制重置,看上去好像跳回了先前的時間點。這些跳躍以及他們經常忽略閏秒的事實,使日曆時鐘不能用於測量經過時間(elapsed time)【38】。 -歷史上的日曆時鐘還具有相當粗略的解析度,例如,在較早的Windows系統上以10毫秒為單位前進【39】。在最近的系統中這已經不是一個問題了。 +歷史上的日曆時鐘還具有相當粗略的解析度,例如,在較早的 Windows 系統上以 10 毫秒為單位前進【39】。在最近的系統中這已經不是一個問題了。 #### 單調鍾 -單調鍾適用於測量持續時間(時間間隔),例如超時或服務的響應時間:Linux上的`clock_gettime(CLOCK_MONOTONIC)`,和Java中的`System.nanoTime()`都是單調時鐘。這個名字來源於他們保證總是往前走的事實(而日曆時鐘可以往回跳)。 +單調鍾適用於測量持續時間(時間間隔),例如超時或服務的響應時間:Linux 上的 `clock_gettime(CLOCK_MONOTONIC)`,和 Java 中的 `System.nanoTime()` 都是單調時鐘。這個名字來源於他們保證總是往前走的事實(而日曆時鐘可以往回跳)。 你可以在某個時間點檢查單調鐘的值,做一些事情,且稍後再次檢查它。這兩個值之間的差異告訴你兩次檢查之間經過了多長時間。但單調鐘的絕對值是毫無意義的:它可能是計算機啟動以來的納秒數,或類似的任意值。特別是比較來自兩臺不同計算機的單調鐘的值是沒有意義的,因為它們並不是一回事。 -在具有多個CPU插槽的伺服器上,每個CPU可能有一個單獨的計時器,但不一定與其他CPU同步。作業系統會補償所有的差異,並嘗試嚮應用執行緒表現出單調鐘的樣子,即使這些執行緒被排程到不同的CPU上。當然,明智的做法是不要太把這種單調性保證當回事【40】。 +在具有多個 CPU 插槽的伺服器上,每個 CPU 可能有一個單獨的計時器,但不一定與其他 CPU 同步。作業系統會補償所有的差異,並嘗試嚮應用執行緒表現出單調鐘的樣子,即使這些執行緒被排程到不同的 CPU 上。當然,明智的做法是不要太把這種單調性保證當回事【40】。 -如果NTP協議檢測到計算機的本地石英鐘比NTP伺服器要更快或更慢,則可以調整單調鍾向前走的頻率(這稱為**偏移(skewing)** 時鐘)。預設情況下,NTP允許時鐘速率增加或減慢最高至0.05%,但NTP不能使單調時鐘向前或向後跳轉。單調時鐘的解析度通常相當好:在大多數系統中,它們能在幾微秒或更短的時間內測量時間間隔。 +如果 NTP 協議檢測到計算機的本地石英鐘比 NTP 伺服器要更快或更慢,則可以調整單調鍾向前走的頻率(這稱為 **偏移(skewing)** 時鐘)。預設情況下,NTP 允許時鐘速率增加或減慢最高至 0.05%,但 NTP 不能使單調時鐘向前或向後跳轉。單調時鐘的解析度通常相當好:在大多數系統中,它們能在幾微秒或更短的時間內測量時間間隔。 -在分散式系統中,使用單調鍾測量**經過時間**(elapsed time,比如超時)通常很好,因為它不假定不同節點的時鐘之間存在任何同步,並且對測量的輕微不準確性不敏感。 +在分散式系統中,使用單調鍾測量 **經過時間**(elapsed time,比如超時)通常很好,因為它不假定不同節點的時鐘之間存在任何同步,並且對測量的輕微不準確性不敏感。 ### 時鐘同步與準確性 -單調鐘不需要同步,但是日曆時鐘需要根據NTP伺服器或其他外部時間源來設定才能有用。不幸的是,我們獲取時鐘的方法並不像你所希望的那樣可靠或準確——硬體時鐘和NTP可能會變幻莫測。舉幾個例子: +單調鐘不需要同步,但是日曆時鐘需要根據 NTP 伺服器或其他外部時間源來設定才能有用。不幸的是,我們獲取時鐘的方法並不像你所希望的那樣可靠或準確 —— 硬體時鐘和 NTP 可能會變幻莫測。舉幾個例子: -* 計算機中的石英鐘不夠精確:它會**漂移**(drifts,即執行速度快於或慢於預期)。時鐘漂移取決於機器的溫度。 Google假設其伺服器時鐘漂移為200 ppm(百萬分之一)【41】,相當於每30秒與伺服器重新同步一次的時鐘漂移為6毫秒,或者每天重新同步的時鐘漂移為17秒。即使一切工作正常,此漂移也會限制可以達到的最佳準確度。 -* 如果計算機的時鐘與NTP伺服器的時鐘差別太大,可能會拒絕同步,或者本地時鐘將被強制重置【37】。任何觀察重置前後時間的應用程式都可能會看到時間倒退或突然跳躍。 -* 如果某個節點被NTP伺服器的防火牆意外阻塞,有可能會持續一段時間都沒有人會注意到。有證據表明,這在實踐中確實發生過。 -* NTP同步只能和網路延遲一樣好,所以當你在擁有可變資料包延遲的擁塞網路上時,NTP同步的準確性會受到限制。一個實驗表明,當透過網際網路同步時,35毫秒的最小誤差是可以實現的,儘管偶爾的網路延遲峰值會導致大約一秒的誤差。根據配置,較大的網路延遲會導致NTP客戶端完全放棄。 -* 一些NTP伺服器是錯誤的或者配置錯誤的,報告的時間可能相差幾個小時【43,44】。還好NTP客戶端非常健壯,因為他們會查詢多個伺服器並忽略異常值。無論如何,依賴於網際網路上的陌生人所告訴你的時間來保證你的系統的正確性,這還挺讓人擔憂的。 -* 閏秒導致一分鐘可能有59秒或61秒,這會打破一些在設計之時未考慮閏秒的系統的時序假設【45】。閏秒已經使許多大型系統崩潰的事實【38,46】說明了,關於時鐘的錯誤假設是多麼容易偷偷溜入系統中。處理閏秒的最佳方法可能是讓NTP伺服器“撒謊”,並在一天中逐漸執行閏秒調整(這被稱為**拖尾**,即smearing)【47,48】,雖然實際的NTP伺服器表現各異【49】。 -* 在虛擬機器中,硬體時鐘被虛擬化,這對於需要精確計時的應用程式提出了額外的挑戰【50】。當一個CPU核心在虛擬機器之間共享時,每個虛擬機器都會暫停幾十毫秒,與此同時另一個虛擬機器正在執行。從應用程式的角度來看,這種停頓表現為時鐘突然向前跳躍【26】。 +* 計算機中的石英鐘不夠精確:它會 **漂移**(drifts,即執行速度快於或慢於預期)。時鐘漂移取決於機器的溫度。 Google 假設其伺服器時鐘漂移為 200 ppm(百萬分之一)【41】,相當於每 30 秒與伺服器重新同步一次的時鐘漂移為 6 毫秒,或者每天重新同步的時鐘漂移為 17 秒。即使一切工作正常,此漂移也會限制可以達到的最佳準確度。 +* 如果計算機的時鐘與 NTP 伺服器的時鐘差別太大,可能會拒絕同步,或者本地時鐘將被強制重置【37】。任何觀察重置前後時間的應用程式都可能會看到時間倒退或突然跳躍。 +* 如果某個節點被 NTP 伺服器的防火牆意外阻塞,有可能會持續一段時間都沒有人會注意到。有證據表明,這在實踐中確實發生過。 +* NTP 同步只能和網路延遲一樣好,所以當你在擁有可變資料包延遲的擁塞網路上時,NTP 同步的準確性會受到限制。一個實驗表明,當透過網際網路同步時,35 毫秒的最小誤差是可以實現的,儘管偶爾的網路延遲峰值會導致大約一秒的誤差。根據配置,較大的網路延遲會導致 NTP 客戶端完全放棄。 +* 一些 NTP 伺服器是錯誤的或者配置錯誤的,報告的時間可能相差幾個小時【43,44】。還好 NTP 客戶端非常健壯,因為他們會查詢多個伺服器並忽略異常值。無論如何,依賴於網際網路上的陌生人所告訴你的時間來保證你的系統的正確性,這還挺讓人擔憂的。 +* 閏秒導致一分鐘可能有 59 秒或 61 秒,這會打破一些在設計之時未考慮閏秒的系統的時序假設【45】。閏秒已經使許多大型系統崩潰的事實【38,46】說明了,關於時鐘的錯誤假設是多麼容易偷偷溜入系統中。處理閏秒的最佳方法可能是讓 NTP 伺服器 “撒謊”,並在一天中逐漸執行閏秒調整(這被稱為 **拖尾**,即 smearing)【47,48】,雖然實際的 NTP 伺服器表現各異【49】。 +* 在虛擬機器中,硬體時鐘被虛擬化,這對於需要精確計時的應用程式提出了額外的挑戰【50】。當一個 CPU 核心在虛擬機器之間共享時,每個虛擬機器都會暫停幾十毫秒,與此同時另一個虛擬機器正在執行。從應用程式的角度來看,這種停頓表現為時鐘突然向前跳躍【26】。 * 如果你在沒有完整控制權的裝置(例如,移動裝置或嵌入式裝置)上執行軟體,則可能完全不能信任該裝置的硬體時鐘。一些使用者故意將其硬體時鐘設定為不正確的日期和時間,例如,為了規避遊戲中的時間限制,時鐘可能會被設定到很遠的過去或將來。 -如果你足夠在乎這件事並投入大量資源,就可以達到非常好的時鐘精度。例如,針對金融機構的歐洲法規草案MiFID II要求所有高頻率交易基金在UTC時間100微秒內同步時鐘,以便除錯“閃崩”等市場異常現象,並幫助檢測市場操縱【51】。 +如果你足夠在乎這件事並投入大量資源,就可以達到非常好的時鐘精度。例如,針對金融機構的歐洲法規草案 MiFID II 要求所有高頻率交易基金在 UTC 時間 100 微秒內同步時鐘,以便除錯 “閃崩” 等市場異常現象,並幫助檢測市場操縱【51】。 -透過GPS接收機,精確時間協議(PTP)【52】以及仔細的部署和監測可以實現這種精確度。然而,這需要很多努力和專業知識,而且有很多東西都會導致時鐘同步錯誤。如果你的NTP守護程序配置錯誤,或者防火牆阻止了NTP通訊,由漂移引起的時鐘誤差可能很快就會變大。 +透過 GPS 接收機,精確時間協議(PTP)【52】以及仔細的部署和監測可以實現這種精確度。然而,這需要很多努力和專業知識,而且有很多東西都會導致時鐘同步錯誤。如果你的 NTP 守護程序配置錯誤,或者防火牆阻止了 NTP 通訊,由漂移引起的時鐘誤差可能很快就會變大。 ### 依賴同步時鐘 -時鐘的問題在於,雖然它們看起來簡單易用,但卻具有令人驚訝的缺陷:一天可能不會有精確的86,400秒,**日曆時鐘**可能會前後跳躍,而一個節點上的時間可能與另一個節點上的時間完全不同。 +時鐘的問題在於,雖然它們看起來簡單易用,但卻具有令人驚訝的缺陷:一天可能不會有精確的 86,400 秒,**日曆時鐘** 可能會前後跳躍,而一個節點上的時間可能與另一個節點上的時間完全不同。 本章早些時候,我們討論了網路丟包和任意延遲包的問題。儘管網路在大多數情況下表現良好,但軟體的設計必須假定網路偶爾會出現故障,而軟體必須正常處理這些故障。時鐘也是如此:儘管大多數時間都工作得很好,但需要準備健壯的軟體來處理不正確的時鐘。 -有一部分問題是,不正確的時鐘很容易被視而不見。如果一臺機器的CPU出現故障或者網路配置錯誤,很可能根本無法工作,所以很快就會被注意和修復。另一方面,如果它的石英時鐘有缺陷,或者它的NTP客戶端配置錯誤,大部分事情似乎仍然可以正常工作,即使它的時鐘逐漸偏離現實。如果某個軟體依賴於精確同步的時鐘,那麼結果更可能是悄無聲息的,僅有微量的資料丟失,而不是一次驚天動地的崩潰【53,54】。 +有一部分問題是,不正確的時鐘很容易被視而不見。如果一臺機器的 CPU 出現故障或者網路配置錯誤,很可能根本無法工作,所以很快就會被注意和修復。另一方面,如果它的石英時鐘有缺陷,或者它的 NTP 客戶端配置錯誤,大部分事情似乎仍然可以正常工作,即使它的時鐘逐漸偏離現實。如果某個軟體依賴於精確同步的時鐘,那麼結果更可能是悄無聲息的,僅有微量的資料丟失,而不是一次驚天動地的崩潰【53,54】。 因此,如果你使用需要同步時鐘的軟體,必須仔細監控所有機器之間的時鐘偏移。時鐘偏離其他時鐘太遠的節點應當被宣告死亡,並從叢集中移除。這樣的監控可以確保你在損失發生之前注意到破損的時鐘。 @@ -313,55 +313,55 @@ 讓我們考慮一個特別的情況,一件很有誘惑但也很危險的事情:依賴時鐘,在多個節點上對事件進行排序。 例如,如果兩個客戶端寫入分散式資料庫,誰先到達? 哪一個更近? -[圖8-3](../img/fig8-3.png)顯示了在具有多領導者複製的資料庫中對時鐘的危險使用(該例子類似於[圖5-9](../img/fig5-9.png))。 客戶端A在節點1上寫入`x = 1`;寫入被複制到節點3;客戶端B在節點3上增加x(我們現在有`x = 2`);最後這兩個寫入都被複制到節點2。 +[圖 8-3](../img/fig8-3.png) 顯示了在具有多領導者複製的資料庫中對時鐘的危險使用(該例子類似於 [圖 5-9](../img/fig5-9.png))。 客戶端 A 在節點 1 上寫入 `x = 1`;寫入被複制到節點 3;客戶端 B 在節點 3 上增加 x(我們現在有 `x = 2`);最後這兩個寫入都被複制到節點 2。 ![](../img/fig8-3.png) -**圖8-3 客戶端B的寫入比客戶端A的寫入要晚,但是B的寫入具有較早的時間戳。** +**圖 8-3 客戶端 B 的寫入比客戶端 A 的寫入要晚,但是 B 的寫入具有較早的時間戳。** -在[圖8-3](../img/fig8-3.png)中,當一個寫入被複制到其他節點時,它會根據發生寫入的節點上的日曆時鐘標記一個時間戳。在這個例子中,時鐘同步是非常好的:節點1和節點3之間的偏差小於3ms,這可能比你在實踐中能預期的更好。 +在 [圖 8-3](../img/fig8-3.png) 中,當一個寫入被複制到其他節點時,它會根據發生寫入的節點上的日曆時鐘標記一個時間戳。在這個例子中,時鐘同步是非常好的:節點 1 和節點 3 之間的偏差小於 3ms,這可能比你在實踐中能預期的更好。 -儘管如此,[圖8-3](../img/fig8-3.png)中的時間戳卻無法正確排列事件:寫入`x = 1`的時間戳為42.004秒,但寫入`x = 2`的時間戳為42.003秒,即使`x = 2`在稍後出現。當節點2接收到這兩個事件時,會錯誤地推斷出`x = 1`是最近的值,而丟棄寫入`x = 2`。效果上表現為,客戶端B的增量操作會丟失。 +儘管如此,[圖 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#檢測併發寫入)”)。 -* 兩個節點很可能獨立地生成具有相同時間戳的寫入,特別是在時鐘僅具有毫秒解析度的情況下。為了解決這樣的衝突,還需要一個額外的**決勝值**(tiebreaker,可以簡單地是一個大隨機數),但這種方法也可能會導致違背因果關係【53】。 +* LWW 無法區分 **高頻順序寫入**(在 [圖 8-3](../img/fig8-3.png) 中,客戶端 B 的增量操作 **一定** 發生在客戶端 A 的寫入之後)和 **真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止違背因果關係(請參閱 “[檢測併發寫入](ch5.md#檢測併發寫入)”)。 +* 兩個節點很可能獨立地生成具有相同時間戳的寫入,特別是在時鐘僅具有毫秒解析度的情況下。為了解決這樣的衝突,還需要一個額外的 **決勝值**(tiebreaker,可以簡單地是一個大隨機數),但這種方法也可能會導致違背因果關係【53】。 -因此,儘管透過保留最“最近”的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近”的定義取決於本地的**日曆時鐘**,這很可能是不正確的。即使用嚴格同步的NTP時鐘,一個數據包也可能在時間戳100毫秒(根據傳送者的時鐘)時傳送,並在時間戳99毫秒(根據接收者的時鐘)處到達——看起來好像資料包在傳送之前已經到達,這是不可能的。 +因此,儘管透過保留最 “最近” 的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近” 的定義取決於本地的 **日曆時鐘**,這很可能是不正確的。即使用嚴格同步的 NTP 時鐘,一個數據包也可能在時間戳 100 毫秒(根據傳送者的時鐘)時傳送,並在時間戳 99 毫秒(根據接收者的時鐘)處到達 —— 看起來好像資料包在傳送之前已經到達,這是不可能的。 -NTP同步是否能足夠準確,以至於這種不正確的排序不會發生?也許不能,因為NTP的同步精度本身,除了石英鐘漂移這類誤差源之外,還受到網路往返時間的限制。為了進行正確的排序,你需要一個比測量物件(即網路延遲)要精確得多的時鐘。 +NTP 同步是否能足夠準確,以至於這種不正確的排序不會發生?也許不能,因為 NTP 的同步精度本身,除了石英鐘漂移這類誤差源之外,還受到網路往返時間的限制。為了進行正確的排序,你需要一個比測量物件(即網路延遲)要精確得多的時鐘。 -所謂的**邏輯時鐘(logic clock)**【56,57】是基於遞增計數器而不是振盪石英晶體,對於排序事件來說是更安全的選擇(請參閱“[檢測併發寫入](ch5.md#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的**日曆時鐘**和**單調鍾**也被稱為**物理時鐘(physical clock)**。我們將在“[順序保證](ch9.md#順序保證)”中來看順序問題。 +所謂的 **邏輯時鐘(logic clock)**【56,57】是基於遞增計數器而不是振盪石英晶體,對於排序事件來說是更安全的選擇(請參閱 “[檢測併發寫入](ch5.md#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的 **日曆時鐘** 和 **單調鍾** 也被稱為 **物理時鐘(physical clock)**。我們將在 “[順序保證](ch9.md#順序保證)” 中來看順序問題。 #### 時鐘讀數存在置信區間 -你可能能夠以微秒或甚至納秒的精度讀取機器的時鐘。但即使可以得到如此細緻的測量結果,這並不意味著這個值對於這樣的精度實際上是準確的。實際上,大概率是不準確的——如前所述,即使你每分鐘與本地網路上的NTP伺服器進行同步,幾毫秒的時間漂移也很容易在不精確的石英時鐘上發生。使用公共網際網路上的NTP伺服器,最好的準確度可能達到幾十毫秒,而且當網路擁塞時,誤差可能會超過100毫秒【57】。 +你可能能夠以微秒或甚至納秒的精度讀取機器的時鐘。但即使可以得到如此細緻的測量結果,這並不意味著這個值對於這樣的精度實際上是準確的。實際上,大概率是不準確的 —— 如前所述,即使你每分鐘與本地網路上的 NTP 伺服器進行同步,幾毫秒的時間漂移也很容易在不精確的石英時鐘上發生。使用公共網際網路上的 NTP 伺服器,最好的準確度可能達到幾十毫秒,而且當網路擁塞時,誤差可能會超過 100 毫秒【57】。 -因此,將時鐘讀數視為一個時間點是沒有意義的——它更像是一段時間範圍:例如,一個系統可能以95%的置信度認為當前時間處於本分鐘內的第10.3秒和10.5秒之間,它可能沒法比這更精確了【58】。如果我們只知道±100毫秒的時間,那麼時間戳中的微秒數字部分基本上是沒有意義的。 +因此,將時鐘讀數視為一個時間點是沒有意義的 —— 它更像是一段時間範圍:例如,一個系統可能以 95% 的置信度認為當前時間處於本分鐘內的第 10.3 秒和 10.5 秒之間,它可能沒法比這更精確了【58】。如果我們只知道 ±100 毫秒的時間,那麼時間戳中的微秒數字部分基本上是沒有意義的。 -不確定性界限可以根據你的時間源來計算。如果你的GPS接收器或原子(銫)時鐘直接連線到你的計算機上,預期的錯誤範圍由製造商告知。如果從伺服器獲得時間,則不確定性取決於自上次與伺服器同步以來的石英鐘漂移的期望值,加上NTP伺服器的不確定性,再加上到伺服器的網路往返時間(只是獲取粗略近似值,並假設伺服器是可信的)。 +不確定性界限可以根據你的時間源來計算。如果你的 GPS 接收器或原子(銫)時鐘直接連線到你的計算機上,預期的錯誤範圍由製造商告知。如果從伺服器獲得時間,則不確定性取決於自上次與伺服器同步以來的石英鐘漂移的期望值,加上 NTP 伺服器的不確定性,再加上到伺服器的網路往返時間(只是獲取粗略近似值,並假設伺服器是可信的)。 -不幸的是,大多數系統不公開這種不確定性:例如,當呼叫`clock_gettime()`時,返回值不會告訴你時間戳的預期錯誤,所以你不知道其置信區間是5毫秒還是5年。 +不幸的是,大多數系統不公開這種不確定性:例如,當呼叫 `clock_gettime()` 時,返回值不會告訴你時間戳的預期錯誤,所以你不知道其置信區間是 5 毫秒還是 5 年。 -一個有趣的例外是Spanner中的Google TrueTime API 【41】,它明確地報告了本地時鐘的置信區間。當你詢問當前時間時,你會得到兩個值:[最早,最晚],這是最早可能的時間戳和最晚可能的時間戳。在不確定性估計的基礎上,時鐘知道當前的實際時間落在該區間內。區間的寬度取決於自從本地石英鐘最後與更精確的時鐘源同步以來已經過了多長時間。 +一個有趣的例外是 Spanner 中的 Google TrueTime API 【41】,它明確地報告了本地時鐘的置信區間。當你詢問當前時間時,你會得到兩個值:[最早,最晚],這是最早可能的時間戳和最晚可能的時間戳。在不確定性估計的基礎上,時鐘知道當前的實際時間落在該區間內。區間的寬度取決於自從本地石英鐘最後與更精確的時鐘源同步以來已經過了多長時間。 #### 全域性快照的同步時鐘 -在“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中,我們討論了快照隔離,這是資料庫中非常有用的功能,需要支援小型快速讀寫事務和大型長時間執行的只讀事務(用於備份或分析)。它允許只讀事務看到特定時間點的處於一致狀態的資料庫,且不會鎖定和干擾讀寫事務。 +在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中,我們討論了快照隔離,這是資料庫中非常有用的功能,需要支援小型快速讀寫事務和大型長時間執行的只讀事務(用於備份或分析)。它允許只讀事務看到特定時間點的處於一致狀態的資料庫,且不會鎖定和干擾讀寫事務。 -快照隔離最常見的實現需要單調遞增的事務ID。如果寫入比快照晚(即,寫入具有比快照更大的事務ID),則該寫入對於快照事務是不可見的。在單節點資料庫上,一個簡單的計數器就足以生成事務ID。 +快照隔離最常見的實現需要單調遞增的事務 ID。如果寫入比快照晚(即,寫入具有比快照更大的事務 ID),則該寫入對於快照事務是不可見的。在單節點資料庫上,一個簡單的計數器就足以生成事務 ID。 -但是當資料庫分佈在許多機器上,也許可能在多個數據中心中時,由於需要協調,(跨所有分割槽)全域性單調遞增的事務ID會很難生成。事務ID必須反映因果關係:如果事務B讀取由事務A寫入的值,則B必須具有比A更大的事務ID,否則快照就無法保持一致。在有大量的小規模、高頻率的事務情景下,在分散式系統中建立事務ID成為一個難以處理的瓶頸[^vi]。 +但是當資料庫分佈在許多機器上,也許可能在多個數據中心中時,由於需要協調,(跨所有分割槽)全域性單調遞增的事務 ID 會很難生成。事務 ID 必須反映因果關係:如果事務 B 讀取由事務 A 寫入的值,則 B 必須具有比 A 更大的事務 ID,否則快照就無法保持一致。在有大量的小規模、高頻率的事務情景下,在分散式系統中建立事務 ID 成為一個難以處理的瓶頸 [^vi]。 -[^vi]: 存在分散式序列號生成器,例如Twitter的雪花(Snowflake),其以可伸縮的方式(例如,透過將ID空間的塊分配給不同節點)近似單調地增加唯一ID。但是,它們通常無法保證與因果關係一致的排序,因為分配的ID塊的時間範圍比資料庫讀取和寫入的時間範圍要長。另請參閱“[順序保證](ch9.md#順序保證)”。 +[^vi]: 存在分散式序列號生成器,例如 Twitter 的雪花(Snowflake),其以可伸縮的方式(例如,透過將 ID 空間的塊分配給不同節點)近似單調地增加唯一 ID。但是,它們通常無法保證與因果關係一致的排序,因為分配的 ID 塊的時間範圍比資料庫讀取和寫入的時間範圍要長。另請參閱 “[順序保證](ch9.md#順序保證)”。 -我們可以使用同步時鐘的時間戳作為事務ID嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。 +我們可以使用同步時鐘的時間戳作為事務 ID 嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。 -Spanner以這種方式實現跨資料中心的快照隔離【59,60】。它使用TrueTime API報告的時鐘置信區間,並基於以下觀察結果:如果你有兩個置信區間,每個置信區間包含最早和最晚可能的時間戳( $A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),這兩個區間不重疊(即:$A_{earliest} < A_{latest} < B_{earliest} < B_{latest}$)的話,那麼B肯定發生在A之後——這是毫無疑問的。只有當區間重疊時,我們才不確定A和B發生的順序。 +Spanner 以這種方式實現跨資料中心的快照隔離【59,60】。它使用 TrueTime API 報告的時鐘置信區間,並基於以下觀察結果:如果你有兩個置信區間,每個置信區間包含最早和最晚可能的時間戳($A = [A_{earliest}, A_{latest}]$, $B=[B_{earliest}, B_{latest}]$),這兩個區間不重疊(即:$A_{earliest} #### 實時是真的嗎? > -> 在嵌入式系統中,實時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與Web上對實時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見[第十一章](ch11.md))。 +> 在嵌入式系統中,實時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與 Web 上對實時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見 [第十一章](ch11.md))。 -例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為GC暫停而延遲彈出。 +例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為 GC 暫停而延遲彈出。 -在系統中提供**實時保證**需要各級軟體棧的支援:一個實時作業系統(RTOS),允許在指定的時間間隔內保證CPU時間的分配。庫函式必須申明最壞情況下的執行時間;動態記憶體分配可能受到限制或完全不允許(實時垃圾收集器存在,但是應用程式仍然必須確保它不會給GC太多的負擔);必須進行大量的測試和測量,以確保達到保證。 +在系統中提供 **實時保證** 需要各級軟體棧的支援:一個實時作業系統(RTOS),允許在指定的時間間隔內保證 CPU 時間的分配。庫函式必須申明最壞情況下的執行時間;動態記憶體分配可能受到限制或完全不允許(實時垃圾收集器存在,但是應用程式仍然必須確保它不會給 GC 太多的負擔);必須進行大量的測試和測量,以確保達到保證。 -所有這些都需要大量額外的工作,嚴重限制了可以使用的程式語言、庫和工具的範圍(因為大多數語言和工具不提供實時保證)。由於這些原因,開發實時系統非常昂貴,並且它們通常用於安全關鍵的嵌入式裝置。而且,“**實時**”與“**高效能**”不一樣——事實上,實時系統可能具有較低的吞吐量,因為他們必須讓及時響應的優先順序高於一切(另請參閱“[延遲和資源利用](#延遲和資源利用)“)。 +所有這些都需要大量額外的工作,嚴重限制了可以使用的程式語言、庫和工具的範圍(因為大多數語言和工具不提供實時保證)。由於這些原因,開發實時系統非常昂貴,並且它們通常用於安全關鍵的嵌入式裝置。而且,“**實時**” 與 “**高效能**” 不一樣 —— 事實上,實時系統可能具有較低的吞吐量,因為他們必須讓及時響應的優先順序高於一切(另請參閱 “[延遲和資源利用](#延遲和資源利用)“)。 對於大多數伺服器端資料處理系統來說,實時保證是不經濟或不合適的。因此,這些系統必須承受在非實時環境中執行的暫停和時鐘不穩定性。 @@ -433,9 +433,9 @@ while (true) { 程序暫停的負面影響可以在不訴諸昂貴的實時排程保證的情況下得到緩解。語言執行時在計劃垃圾回收時具有一定的靈活性,因為它們可以跟蹤物件分配的速度和隨著時間的推移剩餘的空閒記憶體。 -一個新興的想法是將GC暫停視為一個節點的短暫計劃中斷,並在這個節點收集其垃圾的同時,讓其他節點處理來自客戶端的請求。如果執行時可以警告應用程式一個節點很快需要GC暫停,那麼應用程式可以停止向該節點發送新的請求,等待它完成處理未完成的請求,然後在沒有請求正在進行時執行GC。這個技巧向客戶端隱藏了GC暫停,並降低了響應時間的高百分比【70,71】。一些對延遲敏感的金融交易系統【72】使用這種方法。 +一個新興的想法是將 GC 暫停視為一個節點的短暫計劃中斷,並在這個節點收集其垃圾的同時,讓其他節點處理來自客戶端的請求。如果執行時可以警告應用程式一個節點很快需要 GC 暫停,那麼應用程式可以停止向該節點發送新的請求,等待它完成處理未完成的請求,然後在沒有請求正在進行時執行 GC。這個技巧向客戶端隱藏了 GC 暫停,並降低了響應時間的高百分比【70,71】。一些對延遲敏感的金融交易系統【72】使用這種方法。 -這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整GC)之前重新啟動程序【65,73】。一次可以重新啟動一個節點,在計劃重新啟動之前,流量可以從該節點移開,就像[第四章](ch4.md)裡描述的滾動升級一樣。 +這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整 GC)之前重新啟動程序【65,73】。一次可以重新啟動一個節點,在計劃重新啟動之前,流量可以從該節點移開,就像 [第四章](ch4.md) 裡描述的滾動升級一樣。 這些措施不能完全阻止垃圾回收暫停,但可以有效地減少它們對應用的影響。 @@ -444,63 +444,63 @@ while (true) { 本章到目前為止,我們已經探索了分散式系統與執行在單臺計算機上的程式的不同之處:沒有共享記憶體,只有透過可變延遲的不可靠網路傳遞的訊息,系統可能遭受部分失效,不可靠的時鐘和處理暫停。 -如果你不習慣於分散式系統,那麼這些問題的後果就會讓人迷惑不解。網路中的一個節點無法確切地知道任何事情——它只能根據它透過網路接收到(或沒有接收到)的訊息進行猜測。節點只能透過交換訊息來找出另一個節點所處的狀態(儲存了哪些資料,是否正確執行等等)。如果遠端節點沒有響應,則無法知道它處於什麼狀態,因為網路中的問題不能可靠地與節點上的問題區分開來。 +如果你不習慣於分散式系統,那麼這些問題的後果就會讓人迷惑不解。網路中的一個節點無法確切地知道任何事情 —— 它只能根據它透過網路接收到(或沒有接收到)的訊息進行猜測。節點只能透過交換訊息來找出另一個節點所處的狀態(儲存了哪些資料,是否正確執行等等)。如果遠端節點沒有響應,則無法知道它處於什麼狀態,因為網路中的問題不能可靠地與節點上的問題區分開來。 這些系統的討論與哲學有關:在系統中什麼是真什麼是假?如果感知和測量的機制都是不可靠的,那麼關於這些知識我們又能多麼確定呢?軟體系統應該遵循我們對物理世界所期望的法則,如因果關係嗎? 幸運的是,我們不需要去搞清楚生命的意義。在分散式系統中,我們可以陳述關於行為(系統模型)的假設,並以滿足這些假設的方式設計實際系統。演算法可以被證明在某個系統模型中正確執行。這意味著即使底層系統模型提供了很少的保證,也可以實現可靠的行為。 -但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在[第九章](ch9.md)中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。 +但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在 [第九章](ch9.md) 中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。 ### 真相由多數所定義 -設想一個具有不對稱故障的網路:一個節點能夠接收發送給它的所有訊息,但是來自該節點的任何傳出訊息被丟棄或延遲【19】。即使該節點執行良好,並且正在接收來自其他節點的請求,其他節點也無法聽到其響應。經過一段時間後,其他節點宣佈它已經死亡,因為他們沒有聽到節點的訊息。這種情況就像夢魘一樣:**半斷開(semi-disconnected)** 的節點被拖向墓地,敲打尖叫道“我沒死!” ——但是由於沒有人能聽到它的尖叫,葬禮隊伍繼續以堅忍的決心繼續行進。 +設想一個具有不對稱故障的網路:一個節點能夠接收發送給它的所有訊息,但是來自該節點的任何傳出訊息被丟棄或延遲【19】。即使該節點執行良好,並且正在接收來自其他節點的請求,其他節點也無法聽到其響應。經過一段時間後,其他節點宣佈它已經死亡,因為他們沒有聽到節點的訊息。這種情況就像夢魘一樣:**半斷開(semi-disconnected)** 的節點被拖向墓地,敲打尖叫道 “我沒死!” —— 但是由於沒有人能聽到它的尖叫,葬禮隊伍繼續以堅忍的決心繼續行進。 在一個稍微不那麼夢魘的場景中,半斷開的節點可能會注意到它傳送的訊息沒有被其他節點確認,因此意識到網路中必定存在故障。儘管如此,節點被其他節點錯誤地宣告為死亡,而半連線的節點對此無能為力。 -第三種情況,想象一個經歷了一個長時間**停止所有處理垃圾收集暫停(stop-the-world GC Pause)** 的節點。節點的所有執行緒被GC搶佔並暫停一分鐘,因此沒有請求被處理,也沒有響應被傳送。其他節點等待,重試,不耐煩,並最終宣佈節點死亡,並將其丟到靈車上。最後,GC完成,節點的執行緒繼續,好像什麼也沒有發生。其他節點感到驚訝,因為所謂的死亡節點突然從棺材中抬起頭來,身體健康,開始和旁觀者高興地聊天。GC後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。 +第三種情況,想象一個經歷了一個長時間 **停止所有處理垃圾收集暫停(stop-the-world GC Pause)** 的節點。節點的所有執行緒被 GC 搶佔並暫停一分鐘,因此沒有請求被處理,也沒有響應被傳送。其他節點等待,重試,不耐煩,並最終宣佈節點死亡,並將其丟到靈車上。最後,GC 完成,節點的執行緒繼續,好像什麼也沒有發生。其他節點感到驚訝,因為所謂的死亡節點突然從棺材中抬起頭來,身體健康,開始和旁觀者高興地聊天。GC 後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。 -這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)“):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。 +這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)“):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。 這也包括關於宣告節點死亡的決定。如果法定數量的節點宣告另一個節點已經死亡,那麼即使該節點仍感覺自己活著,它也必須被認為是死的。個體節點必須遵守法定決定並下臺。 -最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數——不能同時存在兩個相互衝突的多數決定。當我們在[第九章](ch9.md)中討論**共識演算法(consensus algorithms)** 時,我們將更詳細地討論法定人數的應用。 +最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數 —— 不能同時存在兩個相互衝突的多數決定。當我們在 [第九章](ch9.md) 中討論 **共識演算法(consensus algorithms)** 時,我們將更詳細地討論法定人數的應用。 #### 領導者和鎖 通常情況下,一些東西在一個系統中只能有一個。例如: -* 資料庫分割槽的領導者只能有一個節點,以避免**腦裂**(即split brain,請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。 -* 特定資源的鎖或物件只允許一個事務/客戶端持有,以防同時寫入和損壞。 +* 資料庫分割槽的領導者只能有一個節點,以避免 **腦裂**(即 split brain,請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。 +* 特定資源的鎖或物件只允許一個事務 / 客戶端持有,以防同時寫入和損壞。 * 一個特定的使用者名稱只能被一個使用者所註冊,因為使用者名稱必須唯一標識一個使用者。 -在分散式系統中實現這一點需要注意:即使一個節點認為它是“**天選者(the choosen one)**”(分割槽的負責人,鎖的持有者,成功獲取使用者名稱的使用者的請求處理程式),但這並不一定意味著有法定人數的節點同意!一個節點可能以前是領導者,但是如果其他節點在此期間宣佈它死亡(例如,由於網路中斷或GC暫停),則它可能已被降級,且另一個領導者可能已經當選。 +在分散式系統中實現這一點需要注意:即使一個節點認為它是 “**天選者(the choosen one)**”(分割槽的負責人,鎖的持有者,成功獲取使用者名稱的使用者的請求處理程式),但這並不一定意味著有法定人數的節點同意!一個節點可能以前是領導者,但是如果其他節點在此期間宣佈它死亡(例如,由於網路中斷或 GC 暫停),則它可能已被降級,且另一個領導者可能已經當選。 -如果一個節點繼續表現為**天選者**,即使大多數節點已經宣告它已經死了,則在考慮不周的系統中可能會導致問題。這樣的節點能以自己賦予的權能向其他節點發送訊息,如果其他節點相信,整個系統可能會做一些不正確的事情。 +如果一個節點繼續表現為 **天選者**,即使大多數節點已經宣告它已經死了,則在考慮不周的系統中可能會導致問題。這樣的節點能以自己賦予的權能向其他節點發送訊息,如果其他節點相信,整個系統可能會做一些不正確的事情。 -例如,[圖8-4](../img/fig8-4.png)顯示了由於不正確的鎖實現導致的資料損壞錯誤。 (這個錯誤不僅僅是理論上的:HBase曾經有這個問題【74,75】)假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問,因為如果多個客戶試圖對此寫入,該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。 +例如,[圖 8-4](../img/fig8-4.png) 顯示了由於不正確的鎖實現導致的資料損壞錯誤。 (這個錯誤不僅僅是理論上的:HBase 曾經有這個問題【74,75】)假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問,因為如果多個客戶試圖對此寫入,該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。 ![](../img/fig8-4.png) -**圖8-4 分散式鎖的實現不正確:客戶端1認為它仍然具有有效的租約,即使它已經過期,從而破壞了儲存中的檔案** +**圖 8-4 分散式鎖的實現不正確:客戶端 1 認為它仍然具有有效的租約,即使它已經過期,從而破壞了儲存中的檔案** -這個問題就是我們先前在“[程序暫停](#程序暫停)”中討論過的一個例子:如果持有租約的客戶端暫停太久,它的租約將到期。另一個客戶端可以獲得同一檔案的租約,並開始寫入檔案。當暫停的客戶端回來時,它認為(不正確)它仍然有一個有效的租約,並繼續寫入檔案。結果,客戶的寫入將產生衝突並損壞檔案。 +這個問題就是我們先前在 “[程序暫停](#程序暫停)” 中討論過的一個例子:如果持有租約的客戶端暫停太久,它的租約將到期。另一個客戶端可以獲得同一檔案的租約,並開始寫入檔案。當暫停的客戶端回來時,它認為(不正確)它仍然有一個有效的租約,並繼續寫入檔案。結果,客戶的寫入將產生衝突並損壞檔案。 #### 防護令牌 -當使用鎖或租約來保護對某些資源(如[圖8-4](../img/fig8-4.png)中的檔案儲存)的訪問時,需要確保一個被誤認為自己是“天選者”的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是**防護(fencing)**,如[圖8-5](../img/fig8-5.png)所示 +當使用鎖或租約來保護對某些資源(如 [圖 8-4](../img/fig8-4.png) 中的檔案儲存)的訪問時,需要確保一個被誤認為自己是 “天選者” 的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是 **防護(fencing)**,如 [圖 8-5](../img/fig8-5.png) 所示 ![](../img/fig8-5.png) -**圖8-5 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全** +**圖 8-5 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全** -我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個**防護令牌(fencing token)**,這個數字在每次授予鎖定時都會增加(例如,由鎖定服務增加)。然後,我們可以要求客戶端每次向儲存服務傳送寫入請求時,都必須包含當前的防護令牌。 +我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個 **防護令牌(fencing token)**,這個數字在每次授予鎖定時都會增加(例如,由鎖定服務增加)。然後,我們可以要求客戶端每次向儲存服務傳送寫入請求時,都必須包含當前的防護令牌。 -在[圖8-5](../img/fig8-5.png)中,客戶端1以33的令牌獲得租約,但隨後進入一個長時間的停頓並且租約到期。客戶端2以34的令牌(該數字總是增加)獲取租約,然後將其寫入請求傳送到儲存服務,包括34的令牌。稍後,客戶端1恢復生機並將其寫入儲存服務,包括其令牌值33。但是,儲存伺服器會記住它已經處理了一個具有更高令牌編號(34)的寫入,因此它會拒絕帶有令牌33的請求。 +在 [圖 8-5](../img/fig8-5.png) 中,客戶端 1 以 33 的令牌獲得租約,但隨後進入一個長時間的停頓並且租約到期。客戶端 2 以 34 的令牌(該數字總是增加)獲取租約,然後將其寫入請求傳送到儲存服務,包括 34 的令牌。稍後,客戶端 1 恢復生機並將其寫入儲存服務,包括其令牌值 33。但是,儲存伺服器會記住它已經處理了一個具有更高令牌編號(34)的寫入,因此它會拒絕帶有令牌 33 的請求。 -如果將ZooKeeper用作鎖定服務,則可將事務標識`zxid`或節點版本`cversion`用作防護令牌。由於它們保證單調遞增,因此它們具有所需的屬性【74】。 +如果將 ZooKeeper 用作鎖定服務,則可將事務標識 `zxid` 或節點版本 `cversion` 用作防護令牌。由於它們保證單調遞增,因此它們具有所需的屬性【74】。 -請注意,這種機制要求資源本身在檢查令牌方面發揮積極作用,透過拒絕使用舊的令牌,而不是已經被處理的令牌來進行寫操作——僅僅依靠客戶端檢查自己的鎖狀態是不夠的。對於不明確支援防護令牌的資源,可能仍然可以解決此限制(例如,在檔案儲存服務的情況下,可以將防護令牌包含在檔名中)。但是,為了避免在鎖的保護之外處理請求,需要進行某種檢查。 +請注意,這種機制要求資源本身在檢查令牌方面發揮積極作用,透過拒絕使用舊的令牌,而不是已經被處理的令牌來進行寫操作 —— 僅僅依靠客戶端檢查自己的鎖狀態是不夠的。對於不明確支援防護令牌的資源,可能仍然可以解決此限制(例如,在檔案儲存服務的情況下,可以將防護令牌包含在檔名中)。但是,為了避免在鎖的保護之外處理請求,需要進行某種檢查。 在伺服器端檢查一個令牌可能看起來像是一個缺點,但這可以說是一件好事:一個服務假定它的客戶總是守規矩並不明智,因為使用客戶端的人與執行服務的人優先順序非常不一樣【76】。因此,任何服務保護自己免受意外客戶的濫用是一個好主意。 @@ -508,42 +508,42 @@ while (true) { 防護令牌可以檢測和阻止無意中發生錯誤的節點(例如,因為它尚未發現其租約已過期)。但是,如果節點有意破壞系統的保證,則可以透過使用假防護令牌傳送訊息來輕鬆完成此操作。 -在本書中,我們假設節點是不可靠但誠實的:它們可能很慢或者從不響應(由於故障),並且它們的狀態可能已經過時(由於GC暫停或網路延遲),但是我們假設如果節點它做出了迴應,它正在說出“真相”:盡其所知,它正在按照協議的規則扮演其角色。 +在本書中,我們假設節點是不可靠但誠實的:它們可能很慢或者從不響應(由於故障),並且它們的狀態可能已經過時(由於 GC 暫停或網路延遲),但是我們假設如果節點它做出了迴應,它正在說出 “真相”:盡其所知,它正在按照協議的規則扮演其角色。 -如果存在節點可能“撒謊”(傳送任意錯誤或損壞的響應)的風險,則分散式系統的問題變得更困難了——例如,如果節點可能聲稱其實際上沒有收到特定的訊息。這種行為被稱為**拜占庭故障(Byzantine fault)**,**在不信任的環境中達成共識的問題被稱為拜占庭將軍問題**【77】。 +如果存在節點可能 “撒謊”(傳送任意錯誤或損壞的響應)的風險,則分散式系統的問題變得更困難了 —— 例如,如果節點可能聲稱其實際上沒有收到特定的訊息。這種行為被稱為 **拜占庭故障(Byzantine fault)**,**在不信任的環境中達成共識的問題被稱為拜占庭將軍問題**【77】。 > ### 拜占庭將軍問題 > -> 拜占庭將軍問題是對所謂“兩將軍問題”的泛化【78】,它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地,他們只能透過信使進行溝通,信使有時會被延遲或丟失(就像網路中的資訊包一樣)。我們將在[第九章](ch9.md)討論這個共識問題。 +> 拜占庭將軍問題是對所謂 “兩將軍問題” 的泛化【78】,它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地,他們只能透過信使進行溝通,信使有時會被延遲或丟失(就像網路中的資訊包一樣)。我們將在 [第九章](ch9.md) 討論這個共識問題。 > -> 在這個問題的拜占庭版本里,有n位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。 +> 在這個問題的拜占庭版本里,有 n 位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。 > -> 拜占庭是後來成為君士坦丁堡的古希臘城市,現在在土耳其的伊斯坦布林。沒有任何歷史證據表明拜占庭將軍比其他地方更容易出現陰謀和陰謀。相反,這個名字來源於拜占庭式的過度複雜,官僚,迂迴等意義,早在計算機之前就已經在政治中被使用了【79】。Lamport想要選一個不會冒犯任何讀者的國家,他被告知將其稱為阿爾巴尼亞將軍問題並不是一個好主意【80】。 +> 拜占庭是後來成為君士坦丁堡的古希臘城市,現在在土耳其的伊斯坦布林。沒有任何歷史證據表明拜占庭將軍比其他地方更容易出現陰謀和陰謀。相反,這個名字來源於拜占庭式的過度複雜,官僚,迂迴等意義,早在計算機之前就已經在政治中被使用了【79】。Lamport 想要選一個不會冒犯任何讀者的國家,他被告知將其稱為阿爾巴尼亞將軍問題並不是一個好主意【80】。 -當一個系統在部分節點發生故障、不遵守協議、甚至惡意攻擊、擾亂網路時仍然能繼續正確工作,稱之為**拜占庭容錯(Byzantine fault-tolerant)** 的,在特定場景下,這種擔憂在是有意義的: +當一個系統在部分節點發生故障、不遵守協議、甚至惡意攻擊、擾亂網路時仍然能繼續正確工作,稱之為 **拜占庭容錯(Byzantine fault-tolerant)** 的,在特定場景下,這種擔憂在是有意義的: -* 在航空航天環境中,計算機記憶體或CPU暫存器中的資料可能被輻射破壞,導致其以任意不可預知的方式響應其他節點。由於系統故障非常昂貴(例如,飛機撞毀和炸死船上所有人員,或火箭與國際空間站相撞),飛行控制系統必須容忍拜占庭故障【81,82】。 +* 在航空航天環境中,計算機記憶體或 CPU 暫存器中的資料可能被輻射破壞,導致其以任意不可預知的方式響應其他節點。由於系統故障非常昂貴(例如,飛機撞毀和炸死船上所有人員,或火箭與國際空間站相撞),飛行控制系統必須容忍拜占庭故障【81,82】。 * 在多個參與組織的系統中,一些參與者可能會試圖欺騙或欺騙他人。在這種情況下,節點僅僅信任另一個節點的訊息是不安全的,因為它們可能是出於惡意的目的而被傳送的。例如,像比特幣和其他區塊鏈一樣的對等網路可以被認為是讓互不信任的各方同意交易是否發生的一種方式,而不依賴於中心機構(central authority)【83】。 然而,在本書討論的那些系統中,我們通常可以安全地假設沒有拜占庭式的錯誤。在你的資料中心裡,所有的節點都是由你的組織控制的(所以他們可以信任),輻射水平足夠低,記憶體損壞不是一個大問題。製作拜占庭容錯系統的協議相當複雜【84】,而容錯嵌入式系統依賴於硬體層面的支援【81】。在大多數伺服器端資料系統中,部署拜占庭容錯解決方案的成本使其變得不切實際。 -Web應用程式確實需要預期受終端使用者控制的客戶端(如Web瀏覽器)的任意和惡意行為。這就是為什麼輸入驗證,資料清洗和輸出轉義如此重要:例如,防止SQL注入和跨站點指令碼。然而,我們通常不在這裡使用拜占庭容錯協議,而只是讓伺服器有權決定是否允許客戶端行為。但在沒有這種中心機構的對等網路中,拜占庭容錯更為重要。 +Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web 瀏覽器)的任意和惡意行為。這就是為什麼輸入驗證,資料清洗和輸出轉義如此重要:例如,防止 SQL 注入和跨站點指令碼。然而,我們通常不在這裡使用拜占庭容錯協議,而只是讓伺服器有權決定是否允許客戶端行為。但在沒有這種中心機構的對等網路中,拜占庭容錯更為重要。 -軟體中的一個錯誤(bug)可能被認為是拜占庭式的錯誤,但是如果你將相同的軟體部署到所有節點上,那麼拜占庭式的容錯演算法幫不到你。大多數拜占庭式容錯演算法要求超過三分之二的節點能夠正常工作(即,如果有四個節點,最多隻能有一個故障)。要使用這種方法對付bug,你必須有四個獨立的相同軟體的實現,並希望一個bug只出現在四個實現之一中。 +軟體中的一個錯誤(bug)可能被認為是拜占庭式的錯誤,但是如果你將相同的軟體部署到所有節點上,那麼拜占庭式的容錯演算法幫不到你。大多數拜占庭式容錯演算法要求超過三分之二的節點能夠正常工作(即,如果有四個節點,最多隻能有一個故障)。要使用這種方法對付 bug,你必須有四個獨立的相同軟體的實現,並希望一個 bug 只出現在四個實現之一中。 同樣,如果一個協議可以保護我們免受漏洞,安全滲透和惡意攻擊,那麼這將是有吸引力的。不幸的是,這也是不現實的:在大多數系統中,如果攻擊者可以滲透一個節點,那他們可能會滲透所有這些節點,因為它們可能都執行著相同的軟體。因此,傳統機制(認證,訪問控制,加密,防火牆等)仍然是抵禦攻擊者的主要保護措施。 #### 弱謊言形式 -儘管我們假設節點通常是誠實的,但值得向軟體中新增防止“撒謊”弱形式的機制——例如,由硬體問題導致的無效訊息,軟體錯誤和錯誤配置。這種保護機制並不是完全的拜占庭容錯,因為它們不能抵擋決心堅定的對手,但它們仍然是簡單而實用的步驟,以提高可靠性。例如: +儘管我們假設節點通常是誠實的,但值得向軟體中新增防止 “撒謊” 弱形式的機制 —— 例如,由硬體問題導致的無效訊息,軟體錯誤和錯誤配置。這種保護機制並不是完全的拜占庭容錯,因為它們不能抵擋決心堅定的對手,但它們仍然是簡單而實用的步驟,以提高可靠性。例如: -* 由於硬體問題或作業系統、驅動程式、路由器等中的錯誤,網路資料包有時會受到損壞。通常,損壞的資料包會被內建於TCP和UDP中的校驗和所俘獲,但有時它們也會逃脫檢測【85,86,87】 。要對付這種破壞通常使用簡單的方法就可以做到,例如應用程式級協議中的校驗和。 +* 由於硬體問題或作業系統、驅動程式、路由器等中的錯誤,網路資料包有時會受到損壞。通常,損壞的資料包會被內建於 TCP 和 UDP 中的校驗和所俘獲,但有時它們也會逃脫檢測【85,86,87】 。要對付這種破壞通常使用簡單的方法就可以做到,例如應用程式級協議中的校驗和。 * 可公開訪問的應用程式必須仔細清理來自使用者的任何輸入,例如檢查值是否在合理的範圍內,並限制字串的大小以防止透過大記憶體分配的拒絕服務。防火牆後面的內部服務對於輸入也許可以只採取一些不那麼嚴格的檢查,但是採取一些基本的合理性檢查(例如,在協議解析中)仍然是一個好主意。 -* NTP客戶端可以配置多個伺服器地址。同步時,客戶端聯絡所有的伺服器,估計它們的誤差,並檢查大多數伺服器是否對某個時間範圍達成一致。只要大多數的伺服器沒問題,一個配置錯誤的NTP伺服器報告的時間會被當成特異值從同步中排除【37】。使用多個伺服器使NTP更健壯(比起只用單個伺服器來)。 +* NTP 客戶端可以配置多個伺服器地址。同步時,客戶端聯絡所有的伺服器,估計它們的誤差,並檢查大多數伺服器是否對某個時間範圍達成一致。只要大多數的伺服器沒問題,一個配置錯誤的 NTP 伺服器報告的時間會被當成特異值從同步中排除【37】。使用多個伺服器使 NTP 更健壯(比起只用單個伺服器來)。 ### 系統模型與現實 -已經有很多演算法被設計以解決分散式系統問題——例如,我們將在[第九章](ch9.md)討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。 +已經有很多演算法被設計以解決分散式系統問題 —— 例如,我們將在 [第九章](ch9.md) 討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。 演算法的編寫方式不應該過分依賴於執行的硬體和軟體配置的細節。這就要求我們以某種方式將我們期望在系統中發生的錯誤形式化。我們透過定義一個系統模型來做到這一點,這個模型是一個抽象,描述一個演算法可以假設的事情。 @@ -559,30 +559,30 @@ Web應用程式確實需要預期受終端使用者控制的客戶端(如Web * 非同步模型 - 在這個模型中,一個演算法不允許對時序做任何假設——事實上它甚至沒有時鐘(所以它不能使用超時)。一些演算法被設計為可用於非同步模型,但非常受限。 + 在這個模型中,一個演算法不允許對時序做任何假設 —— 事實上它甚至沒有時鐘(所以它不能使用超時)。一些演算法被設計為可用於非同步模型,但非常受限。 -進一步來說,除了時序問題,我們還要考慮**節點失效**。三種最常見的節點系統模型是: +進一步來說,除了時序問題,我們還要考慮 **節點失效**。三種最常見的節點系統模型是: -* 崩潰-停止故障 +* 崩潰 - 停止故障 - 在**崩潰停止(crash-stop)** 模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失——它永遠不會回來。 + 在 **崩潰停止(crash-stop)** 模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失 —— 它永遠不會回來。 -* 崩潰-恢復故障 +* 崩潰 - 恢復故障 - 我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在**崩潰-恢復(crash-recovery)** 模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。 + 我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在 **崩潰 - 恢復(crash-recovery)** 模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。 * 拜占庭(任意)故障 節點可以做(絕對意義上的)任何事情,包括試圖戲弄和欺騙其他節點,如上一節所述。 -對於真實系統的建模,具有**崩潰-恢復故障(crash-recovery)** 的**部分同步模型(partial synchronous)** 通常是最有用的模型。分散式演算法如何應對這種模型? +對於真實系統的建模,具有 **崩潰 - 恢復故障(crash-recovery)** 的 **部分同步模型(partial synchronous)** 通常是最有用的模型。分散式演算法如何應對這種模型? #### 演算法的正確性 為了定義演算法是正確的,我們可以描述它的屬性。例如,排序演算法的輸出具有如下特性:對於輸出列表中的任何兩個不同的元素,左邊的元素比右邊的元素小。這只是定義對列表進行排序含義的一種形式方式。 -同樣,我們可以寫下我們想要的分散式演算法的屬性來定義它的正確含義。例如,如果我們正在為一個鎖生成防護令牌(請參閱“[防護令牌](#防護令牌)”),我們可能要求演算法具有以下屬性: +同樣,我們可以寫下我們想要的分散式演算法的屬性來定義它的正確含義。例如,如果我們正在為一個鎖生成防護令牌(請參閱 “[防護令牌](#防護令牌)”),我們可能要求演算法具有以下屬性: * 唯一性(uniqueness) @@ -590,7 +590,7 @@ Web應用程式確實需要預期受終端使用者控制的客戶端(如Web * 單調序列(monotonic sequence) - 如果請求 $x$ 返回了令牌 $t_x$,並且請求$y$返回了令牌$t_y$,並且 $x$ 在 $y$ 開始之前已經完成,那麼$t_x 好死還是賴活著? -> —— Jay Kreps, 關於Kafka與 Jepsen的若干筆記 (2013) +> 好死還是賴活著? +> —— Jay Kreps, 關於 Kafka 與 Jepsen 的若干筆記 (2013) --------------- [TOC] -正如[第八章](ch8.md)所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法—— 即使某些內部元件出現故障,服務也能正常執行。 +正如 [第八章](ch8.md) 所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法 —— 即使某些內部元件出現故障,服務也能正常執行。 -在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設[第八章](ch8.md)的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複遞送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰。 +在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設 [第八章](ch8.md) 的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複遞送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰。 -構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與[第七章](ch7.md)中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。 +構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與 [第七章](ch7.md) 中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。 -現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是**共識(consensus)**:**就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。 +現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是 **共識(consensus)**:**就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。 -一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在“[處理節點宕機](ch5.md#處理節點宕機)”中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為**腦裂(split brain)**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。 +一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在 “[處理節點宕機](ch5.md#處理節點宕機)” 中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為 **腦裂(split brain)**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。 -在本章後面的“[分散式事務與共識](#分散式事務與共識)”中,我們將研究解決共識和相關問題的演算法。但首先,我們首先需要探索可以在分散式系統中提供的保證和抽象的範圍。 +在本章後面的 “[分散式事務與共識](#分散式事務與共識)” 中,我們將研究解決共識和相關問題的演算法。但首先,我們首先需要探索可以在分散式系統中提供的保證和抽象的範圍。 我們需要了解可以做什麼和不可以做什麼的範圍:在某些情況下,系統可以容忍故障並繼續工作;在其他情況下,這是不可能的。我們將深入研究什麼可能而什麼不可能的限制,既透過理論證明,也透過實際實現。我們將在本章中概述這些基本限制。 -分散式系統領域的研究人員幾十年來一直在研究這些主題,所以有很多資料—— 我們只能介紹一些皮毛。在本書中,我們沒有空間去詳細介紹形式模型和證明的細節,所以我們將堅持非正式的直覺。如果你有興趣,參考文獻可以提供更多的深度。 +分散式系統領域的研究人員幾十年來一直在研究這些主題,所以有很多資料 —— 我們只能介紹一些皮毛。在本書中,我們沒有空間去詳細介紹形式模型和證明的細節,所以我們將堅持非正式的直覺。如果你有興趣,參考文獻可以提供更多的深度。 ## 一致性保證 -在“[複製延遲問題](ch5.md#複製延遲問題)”中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製,多主複製或無主複製),都會出現這些不一致情況。 +在 “[複製延遲問題](ch5.md#複製延遲問題)” 中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製,多主複製或無主複製),都會出現這些不一致情況。 -大多數複製的資料庫至少提供了**最終一致性**,這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間,那麼最終所有的讀取請求都會返回相同的值【1】。換句話說,不一致性是暫時的,最終會自行解決(假設網路中的任何故障最終都會被修復)。最終一致性的一個更好的名字可能是**收斂(convergence)**,因為我們預計所有的副本最終會收斂到相同的值【2】。 +大多數複製的資料庫至少提供了 **最終一致性**,這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間,那麼最終所有的讀取請求都會返回相同的值【1】。換句話說,不一致性是暫時的,最終會自行解決(假設網路中的任何故障最終都會被修復)。最終一致性的一個更好的名字可能是 **收斂(convergence)**,因為我們預計所有的副本最終會收斂到相同的值【2】。 -然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前,讀操作可能會返回任何東西或什麼都沒有【1】。例如,如果你寫入了一個值,然後立即再次讀取,這並不能保證你能看到剛才寫入的值,因為讀請求可能會被路由到另外的副本上。(請參閱“[讀己之寫](ch5.md#讀己之寫)” )。 +然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前,讀操作可能會返回任何東西或什麼都沒有【1】。例如,如果你寫入了一個值,然後立即再次讀取,這並不能保證你能看到剛才寫入的值,因為讀請求可能會被路由到另外的副本上。(請參閱 “[讀己之寫](ch5.md#讀己之寫)” )。 對於應用開發人員而言,最終一致性是很困難的,因為它與普通單執行緒程式中變數的行為有很大區別。對於後者,如果將一個值賦給一個變數,然後很快地再次讀取,不可能讀到舊的值,或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數,但實際上它有更復雜的語義【3】。 @@ -40,111 +40,111 @@ 本章將探索資料系統可能選擇提供的更強一致性模型。它不是免費的:具有較強保證的系統可能會比保證較差的系統具有更差的效能或更少的容錯性。儘管如此,更強的保證能夠吸引人,因為它們更容易用對。只有見過不同的一致性模型後,才能更好地決定哪一個最適合自己的需求。 -**分散式一致性模型**和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】(請參閱“[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了**避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於**在面對延遲和故障時如何協調副本間的狀態**。 +**分散式一致性模型** 和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】(請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了 **避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於 **在面對延遲和故障時如何協調副本間的狀態**。 本章涵蓋了廣泛的話題,但我們將會看到這些領域實際上是緊密聯絡在一起的: -* 首先看一下常用的**最強一致性模型**之一,**線性一致性(linearizability)**,並考察其優缺點。 -* 然後我們將檢查分散式系統中[**事件順序**](#順序保證)的問題,特別是因果關係和全域性順序的問題。 +* 首先看一下常用的 **最強一致性模型** 之一,**線性一致性(linearizability)**,並考察其優缺點。 +* 然後我們將檢查分散式系統中 [**事件順序**](#順序保證) 的問題,特別是因果關係和全域性順序的問題。 * 在第三節的(“[分散式事務與共識](#分散式事務與共識)”)中將探討如何原子地提交分散式事務,這將最終引領我們走向共識問題的解決方案。 ## 線性一致性 -在**最終一致**的資料庫,如果你在同一時刻問兩個不同副本相同的問題,可能會得到兩個不同的答案。這很讓人困惑。如果資料庫可以提供只有一個副本的假象(即,只有一個數據副本),那麼事情就簡單太多了。那麼每個客戶端都會有相同的資料檢視,且不必擔心複製滯後了。 +在 **最終一致** 的資料庫,如果你在同一時刻問兩個不同副本相同的問題,可能會得到兩個不同的答案。這很讓人困惑。如果資料庫可以提供只有一個副本的假象(即,只有一個數據副本),那麼事情就簡單太多了。那麼每個客戶端都會有相同的資料檢視,且不必擔心複製滯後了。 -這就是**線性一致性(linearizability)** 背後的想法【6】(也稱為**原子一致性(atomic consistency)**【7】,**強一致性(strong consistency)**,**立即一致性(immediate consistency)** 或**外部一致性(external consistency )**【8】)。線性一致性的精確定義相當微妙,我們將在本節的剩餘部分探討它。但是基本的想法是讓一個系統看起來好像只有一個數據副本,而且所有的操作都是原子性的。有了這個保證,即使實際中可能有多個副本,應用也不需要擔心它們。 +這就是 **線性一致性(linearizability)** 背後的想法【6】(也稱為 **原子一致性(atomic consistency)**【7】,**強一致性(strong consistency)**,**立即一致性(immediate consistency)** 或 **外部一致性(external consistency )**【8】)。線性一致性的精確定義相當微妙,我們將在本節的剩餘部分探討它。但是基本的想法是讓一個系統看起來好像只有一個數據副本,而且所有的操作都是原子性的。有了這個保證,即使實際中可能有多個副本,應用也不需要擔心它們。 -在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。要維護資料的單個副本的假象,系統應保障讀到的值是最近的、最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個**新鮮度保證(recency guarantee)**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。 +在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。要維護資料的單個副本的假象,系統應保障讀到的值是最近的、最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個 **新鮮度保證(recency guarantee)**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。 ![](../img/fig9-1.png) -**圖9-1 這個系統是非線性一致的,導致了球迷的困惑** +**圖 9-1 這個系統是非線性一致的,導致了球迷的困惑** -[圖9-1 ](../img/fig9-1.png)展示了一個關於體育網站的非線性一致例子【9】。Alice和Bob正坐在同一個房間裡,都盯著各自的手機,關注著2014年FIFA世界盃決賽的結果。在最後得分公佈後,Alice重新整理頁面,看到宣佈了獲勝者,並興奮地告訴Bob。Bob難以置信地重新整理了自己的手機,但他的請求路由到了一個落後的資料庫副本上,手機顯示比賽仍在進行。 +[圖 9-1](../img/fig9-1.png) 展示了一個關於體育網站的非線性一致例子【9】。Alice 和 Bob 正坐在同一個房間裡,都盯著各自的手機,關注著 2014 年 FIFA 世界盃決賽的結果。在最後得分公佈後,Alice 重新整理頁面,看到宣佈了獲勝者,並興奮地告訴 Bob。Bob 難以置信地重新整理了自己的手機,但他的請求路由到了一個落後的資料庫副本上,手機顯示比賽仍在進行。 -如果Alice和Bob在同一時間重新整理並獲得了兩個不同的查詢結果,也許就沒有那麼令人驚訝了。因為他們不知道伺服器處理他們請求的精確時刻。然而Bob是在聽到Alice驚呼最後得分**之後**,點選了重新整理按鈕(啟動了他的查詢),因此他希望查詢結果至少與愛麗絲一樣新鮮。但他的查詢返回了陳舊結果,這一事實違背了線性一致性的要求。 +如果 Alice 和 Bob 在同一時間重新整理並獲得了兩個不同的查詢結果,也許就沒有那麼令人驚訝了。因為他們不知道伺服器處理他們請求的精確時刻。然而 Bob 是在聽到 Alice 驚呼最後得分 **之後**,點選了重新整理按鈕(啟動了他的查詢),因此他希望查詢結果至少與愛麗絲一樣新鮮。但他的查詢返回了陳舊結果,這一事實違背了線性一致性的要求。 ### 什麼使得系統線性一致? 線性一致性背後的基本思想很簡單:使系統看起來好像只有一個數據副本。然而確切來講,實際上有更多要操心的地方。為了更好地理解線性一致性,讓我們再看幾個例子。 -[圖9-2](../img/fig9-2.png) 顯示了三個客戶端線上性一致資料庫中同時讀寫相同的鍵`x`。在分散式系統文獻中,`x`被稱為**暫存器(register)**,例如,它可以是鍵值儲存中的一個**鍵**,關係資料庫中的一**行**,或文件資料庫中的一個**文件**。 +[圖 9-2](../img/fig9-2.png) 顯示了三個客戶端線上性一致資料庫中同時讀寫相同的鍵 `x`。在分散式系統文獻中,`x` 被稱為 **暫存器(register)**,例如,它可以是鍵值儲存中的一個 **鍵**,關係資料庫中的一 **行**,或文件資料庫中的一個 **文件**。 ![](../img/fig9-2.png) -**圖9-2 如果讀取請求與寫入請求併發,則可能會返回舊值或新值** +**圖 9-2 如果讀取請求與寫入請求併發,則可能會返回舊值或新值** -為了簡單起見,[圖9-2](../img/fig9-2.png)採用了使用者請求的視角,而不是資料庫內部的視角。每個柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間——只知道它發生在傳送請求和接收響應的之間的某個時刻。[^i] +為了簡單起見,[圖 9-2](../img/fig9-2.png) 採用了使用者請求的視角,而不是資料庫內部的視角。每個柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間 —— 只知道它發生在傳送請求和接收響應的之間的某個時刻。[^i] -[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。即使真實的系統通常沒有準確的時鐘(請參閱“[不可靠的時鐘](ch8.md#不可靠的時鐘)”),但這種假設是允許的:為了分析分散式演算法,我們可以假設一個精確的全域性時鐘存在,不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和NTP產生的實時逼近。 +[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。即使真實的系統通常沒有準確的時鐘(請參閱 “[不可靠的時鐘](ch8.md#不可靠的時鐘)”),但這種假設是允許的:為了分析分散式演算法,我們可以假設一個精確的全域性時鐘存在,不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和 NTP 產生的實時逼近。 在這個例子中,暫存器有兩種型別的操作: * $read(x)⇒v$表示客戶端請求讀取暫存器 `x` 的值,資料庫返回值 `v`。 * $write(x,v)⇒r$ 表示客戶端請求將暫存器 `x` 設定為值 `v` ,資料庫返回響應 `r` (可能正確,可能錯誤)。 -在[圖9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端A和B反覆輪詢資料庫以讀取最新值。 A和B的請求可能會收到怎樣的響應? +在 [圖 9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端 C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。 A 和 B 的請求可能會收到怎樣的響應? -* 客戶端A的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`。 -* 客戶端A的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則必須在寫入之後處理讀取,因此它必須看到寫入的新值。 -* 與寫操作在時間上重疊的任何讀操作,可能會返回 `0` 或 `1` ,因為我們不知道讀取時,寫操作是否已經生效。這些操作是**併發(concurrent)** 的。 +* 客戶端 A 的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`。 +* 客戶端 A 的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則必須在寫入之後處理讀取,因此它必須看到寫入的新值。 +* 與寫操作在時間上重疊的任何讀操作,可能會返回 `0` 或 `1` ,因為我們不知道讀取時,寫操作是否已經生效。這些操作是 **併發(concurrent)** 的。 -但是,這還不足以完全描述線性一致性:如果與寫入同時發生的讀取可以返回舊值或新值,那麼讀者可能會在寫入期間看到數值在舊值和新值之間來回翻轉。這不是我們所期望的模擬“單一資料副本”的系統。[^ii] +但是,這還不足以完全描述線性一致性:如果與寫入同時發生的讀取可以返回舊值或新值,那麼讀者可能會在寫入期間看到數值在舊值和新值之間來回翻轉。這不是我們所期望的模擬 “單一資料副本” 的系統。[^ii] -[^ii]: 如果讀取(與寫入同時發生時)可能返回舊值或新值,則稱該暫存器為**常規暫存器(regular register)**【7,25】 +[^ii]: 如果讀取(與寫入同時發生時)可能返回舊值或新值,則稱該暫存器為 **常規暫存器(regular register)**【7,25】 -為了使系統線性一致,我們需要新增另一個約束,如[圖9-3](../img/fig9-3.png)所示 +為了使系統線性一致,我們需要新增另一個約束,如 [圖 9-3](../img/fig9-3.png) 所示 ![](../img/fig9-3.png) -**圖9-3 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。** +**圖 9-3 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。** -在一個線性一致的系統中,我們可以想象,在 `x` 的值從`0` 自動翻轉到 `1` 的時候(在寫操作的開始和結束之間)必定有一個時間點。因此,如果一個客戶端的讀取返回新的值 `1`,即使寫操作尚未完成,所有後續讀取也必須返回新值。 +在一個線性一致的系統中,我們可以想象,在 `x` 的值從 `0` 自動翻轉到 `1` 的時候(在寫操作的開始和結束之間)必定有一個時間點。因此,如果一個客戶端的讀取返回新的值 `1`,即使寫操作尚未完成,所有後續讀取也必須返回新值。 -[圖9-3](../img/fig9-3.png)中的箭頭說明了這個時序依賴關係。客戶端A 是第一個讀取新的值 `1` 的位置。在A 的讀取返回之後,B開始新的讀取。由於B的讀取嚴格在發生於A的讀取之後,因此即使C的寫入仍在進行中,也必須返回 `1`(與[圖9-1](../img/fig9-1.png)中的Alice和Bob的情況相同:在Alice讀取新值之後,Bob也希望讀取新的值)。 +[圖 9-3](../img/fig9-3.png) 中的箭頭說明了這個時序依賴關係。客戶端 A 是第一個讀取新的值 `1` 的位置。在 A 的讀取返回之後,B 開始新的讀取。由於 B 的讀取嚴格在發生於 A 的讀取之後,因此即使 C 的寫入仍在進行中,也必須返回 `1`(與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同:在 Alice 讀取新值之後,Bob 也希望讀取新的值)。 -我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖9-4](../img/fig9-4.png)顯示了一個更復雜的例子【10】。 +我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖 9-4](../img/fig9-4.png) 顯示了一個更復雜的例子【10】。 -在[圖9-4](../img/fig9-4.png)中,除了讀寫之外,還增加了第三種類型的操作: +在 [圖 9-4](../img/fig9-4.png) 中,除了讀寫之外,還增加了第三種類型的操作: -* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的[**比較與設定**](ch7.md#比較並設定(CAS))操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x≠v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。 $r$ 是資料庫的響應(正確或錯誤)。 +* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的 [**比較與設定**](ch7.md#比較並設定(CAS)) 操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x≠v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。 $r$ 是資料庫的響應(正確或錯誤)。 -[圖9-4](../img/fig9-4.png)中的每個操作都在我們認為執行操作的時候用豎線標出(在每個操作的條柱之內)。這些標記按順序連在一起,其結果必須是一個有效的暫存器讀寫序列(**每次讀取都必須返回最近一次寫入設定的值**)。 +[圖 9-4](../img/fig9-4.png) 中的每個操作都在我們認為執行操作的時候用豎線標出(在每個操作的條柱之內)。這些標記按順序連在一起,其結果必須是一個有效的暫存器讀寫序列(**每次讀取都必須返回最近一次寫入設定的值**)。 線性一致性的要求是,操作標記的連線總是按時間(從左到右)向前移動,而不是向後移動。這個要求確保了我們之前討論的新鮮度保證:一旦新的值被寫入或讀取,所有後續的讀都會看到寫入的值,直到它被再次覆蓋。 ![](../img/fig9-4.png) -**圖9-4 視覺化讀取和寫入看起來已經生效的時間點。 B的最後讀取不是線性一致性的** +**圖 9-4 視覺化讀取和寫入看起來已經生效的時間點。 B 的最後讀取不是線性一致性的** -[圖9-4](../img/fig9-4.png)中有一些有趣的細節需要指出: +[圖 9-4](../img/fig9-4.png) 中有一些有趣的細節需要指出: -* 第一個客戶端B傳送一個讀取 `x` 的請求,然後客戶端D傳送一個請求將 `x` 設定為 `0`,然後客戶端A傳送請求將 `x` 設定為 `1`。儘管如此,返回到B的讀取值為 `1`(由A寫入的值)。這是可以的:這意味著資料庫首先處理D的寫入,然後是A的寫入,最後是B的讀取。雖然這不是請求傳送的順序,但這是一個可以接受的順序,因為這三個請求是併發的。也許B的讀請求在網路上略有延遲,所以它在兩次寫入之後才到達資料庫。 +* 第一個客戶端 B 傳送一個讀取 `x` 的請求,然後客戶端 D 傳送一個請求將 `x` 設定為 `0`,然後客戶端 A 傳送請求將 `x` 設定為 `1`。儘管如此,返回到 B 的讀取值為 `1`(由 A 寫入的值)。這是可以的:這意味著資料庫首先處理 D 的寫入,然後是 A 的寫入,最後是 B 的讀取。雖然這不是請求傳送的順序,但這是一個可以接受的順序,因為這三個請求是併發的。也許 B 的讀請求在網路上略有延遲,所以它在兩次寫入之後才到達資料庫。 -* 在客戶端A從資料庫收到響應之前,客戶端B的讀取返回 `1` ,表示寫入值 `1` 已成功。這也是可以的:這並不意味著在寫之前讀到了值,這只是意味著從資料庫到客戶端A的正確響應在網路中略有延遲。 +* 在客戶端 A 從資料庫收到響應之前,客戶端 B 的讀取返回 `1` ,表示寫入值 `1` 已成功。這也是可以的:這並不意味著在寫之前讀到了值,這只是意味著從資料庫到客戶端 A 的正確響應在網路中略有延遲。 -* 此模型不假設有任何事務隔離:另一個客戶端可能隨時更改值。例如,C首先讀取 `1` ,然後讀取 `2` ,因為兩次讀取之間的值由B更改。可以使用原子**比較並設定(cas)** 操作來檢查該值是否未被另一客戶端同時更改:B和C的**cas**請求成功,但是D的**cas**請求失敗(在資料庫處理它時,`x` 的值不再是 `0` )。 +* 此模型不假設有任何事務隔離:另一個客戶端可能隨時更改值。例如,C 首先讀取 `1` ,然後讀取 `2` ,因為兩次讀取之間的值由 B 更改。可以使用原子 **比較並設定(cas)** 操作來檢查該值是否未被另一客戶端同時更改:B 和 C 的 **cas** 請求成功,但是 D 的 **cas** 請求失敗(在資料庫處理它時,`x` 的值不再是 `0` )。 -* 客戶B的最後一次讀取(陰影條柱中)不是線性一致性的。 該操作與C的**cas**寫操作併發(它將 `x` 從 `2` 更新為 `4` )。在沒有其他請求的情況下,B的讀取返回 `2` 是可以的。然而,在B的讀取開始之前,客戶端A已經讀取了新的值 `4` ,因此不允許B讀取比A更舊的值。再次,與[圖9-1](../img/fig9-1.png)中的Alice和Bob的情況相同。 +* 客戶 B 的最後一次讀取(陰影條柱中)不是線性一致性的。 該操作與 C 的 **cas** 寫操作併發(它將 `x` 從 `2` 更新為 `4` )。在沒有其他請求的情況下,B 的讀取返回 `2` 是可以的。然而,在 B 的讀取開始之前,客戶端 A 已經讀取了新的值 `4` ,因此不允許 B 讀取比 A 更舊的值。再次,與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同。 這就是線性一致性背後的直覺。 正式的定義【6】更準確地描述了它。 透過記錄所有請求和響應的時序,並檢查它們是否可以排列成有效的順序,以測試一個系統的行為是否線性一致性是可能的(儘管在計算上是昂貴的)【11】。 > ### 線性一致性與可序列化 > -> **線性一致性**容易和[**可序列化**](ch7.md#可序列化)相混淆,因為兩個詞似乎都是類似“可以按順序排列”的東西。但它們是兩種完全不同的保證,區分兩者非常重要: +> **線性一致性** 容易和 [**可序列化**](ch7.md#可序列化) 相混淆,因為兩個詞似乎都是類似 “可以按順序排列” 的東西。但它們是兩種完全不同的保證,區分兩者非常重要: > > ***可序列化*** > -> **可序列化(Serializability)** 是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)——請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”。它確保事務的行為,與它們按照**某種**順序依次執行的結果相同(每個事務在下一個事務開始之前執行完成)。這種執行順序可以與事務實際執行的順序不同。【12】。 +> **可序列化(Serializability)** 是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)—— 請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”。它確保事務的行為,與它們按照 **某種** 順序依次執行的結果相同(每個事務在下一個事務開始之前執行完成)。這種執行順序可以與事務實際執行的順序不同。【12】。 > > ***線性一致性*** > -> **線性一致性(Linearizability)** 是讀取和寫入暫存器(單個物件)的**新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(請參閱“[寫入偏差和幻讀](ch7.md#寫入偏斜與幻讀)”),除非採取其他措施(例如[物化衝突](ch7.md#物化衝突))。 +> **線性一致性(Linearizability)** 是讀取和寫入暫存器(單個物件)的 **新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(請參閱 “[寫入偏差和幻讀](ch7.md#寫入偏斜與幻讀)”),除非採取其他措施(例如 [物化衝突](ch7.md#物化衝突))。 > -> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或**強的單副本可序列化(strong-1SR)**【4,13】。基於兩階段鎖定的可序列化實現(請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”一節)或**真的序列執行**(請參閱第“[真的序列執行](ch7.md#真的序列執行)”)通常是線性一致性的。 +> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或 **強的單副本可序列化(strong-1SR)**【4,13】。基於兩階段鎖定的可序列化實現(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)” 一節)或 **真的序列執行**(請參閱第 “[真的序列執行](ch7.md#真的序列執行)”)通常是線性一致性的。 > -> 但是,可序列化的快照隔離(請參閱“[可序列化快照隔離](ch7.md#可序列化快照隔離)”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於**它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。 +> 但是,可序列化的快照隔離(請參閱 “[可序列化快照隔離](ch7.md#可序列化快照隔離)”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於 **它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。 ### 依賴線性一致性 @@ -155,98 +155,98 @@ 一個使用單主複製的系統,需要確保領導真的只有一個,而不是幾個(腦裂)。一種選擇領導者的方法是使用鎖:每個節點在啟動時嘗試獲取鎖,成功者成為領導者【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部署通常具有用於資料庫節點之間通訊的專用叢集互連網路。 +分散式鎖也在一些分散式資料庫(如 Oracle Real Application Clusters(RAC)【18】)中更多的粒度級別上使用。RAC 對每個磁碟頁面使用一個鎖,多個節點共享對同一個磁碟儲存系統的訪問許可權。由於這些線性一致的鎖處於事務執行的關鍵路徑上,RAC 部署通常具有用於資料庫節點之間通訊的專用叢集互連網路。 #### 約束和唯一性保證 唯一性約束在資料庫中很常見:例如,使用者名稱或電子郵件地址必須唯一標識一個使用者,而在檔案儲存服務中,不能有兩個具有相同路徑和檔名的檔案。如果要在寫入資料時強制執行此約束(例如,如果兩個人試圖同時建立一個具有相同名稱的使用者或檔案,其中一個將返回一個錯誤),則需要線性一致性。 -這種情況實際上類似於一個鎖:當一個使用者註冊你的服務時,可以認為他們獲得了所選使用者名稱的“鎖定”。該操作與原子性的比較與設定(CAS)非常相似:將使用者名稱賦予宣告它的使用者,前提是使用者名稱尚未被使用。 +這種情況實際上類似於一個鎖:當一個使用者註冊你的服務時,可以認為他們獲得了所選使用者名稱的 “鎖定”。該操作與原子性的比較與設定(CAS)非常相似:將使用者名稱賦予宣告它的使用者,前提是使用者名稱尚未被使用。 如果想要確保銀行賬戶餘額永遠不會為負數,或者不會出售比倉庫裡的庫存更多的物品,或者兩個人不會都預定了航班或劇院裡同一時間的同一個位置。這些約束條件都要求所有節點都同意一個最新的值(賬戶餘額,庫存水平,座位佔用率)。 -在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在“[及時性與完整性](ch12.md#及時性與完整性)”中討論這種寬鬆的約束。 +在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在 “[及時性與完整性](ch12.md#及時性與完整性)” 中討論這種寬鬆的約束。 然而,一個硬性的唯一性約束(關係型資料庫中常見的那種)需要線性一致性。其他型別的約束,如外來鍵或屬性約束,可以不需要線性一致性【19】。 #### 跨通道的時序依賴 -注意[圖9-1](../img/fig9-1.png) 中的一個細節:如果Alice沒有驚呼得分,Bob就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面,並最終看到最後的分數。由於系統中存在額外的通道(Alice的聲音傳到了Bob的耳朵中),線性一致性的違背才被注意到。 +注意 [圖 9-1](../img/fig9-1.png) 中的一個細節:如果 Alice 沒有驚呼得分,Bob 就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面,並最終看到最後的分數。由於系統中存在額外的通道(Alice 的聲音傳到了 Bob 的耳朵中),線性一致性的違背才被注意到。 -計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如[圖9-5](../img/fig9-5.png)所示。 +計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如 [圖 9-5](../img/fig9-5.png) 所示。 -影象縮放器需要明確的指令來執行尺寸縮放作業,指令是Web伺服器透過訊息佇列傳送的(請參閱[第十一章](ch11.md))。 Web伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。 +影象縮放器需要明確的指令來執行尺寸縮放作業,指令是 Web 伺服器透過訊息佇列傳送的(請參閱 [第十一章](ch11.md))。 Web 伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。 ![](../img/fig9-5.png) -**圖9-5 Web伺服器和影象縮放器透過檔案儲存和訊息佇列進行通訊,開啟競爭條件的可能性。** +**圖 9-5 Web 伺服器和影象縮放器透過檔案儲存和訊息佇列進行通訊,開啟競爭條件的可能性。** -如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖9-5](../img/fig9-5.png)中的步驟3和4)可能比儲存服務內部的複製(replication)更快。在這種情況下,當縮放器讀取影象(步驟5)時,可能會看到影象的舊版本,或者什麼都沒有。如果它處理的是舊版本的影象,則檔案儲存中的全尺寸圖和縮圖就產生了永久性的不一致。 +如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖 9-5](../img/fig9-5.png) 中的步驟 3 和 4)可能比儲存服務內部的複製(replication)更快。在這種情況下,當縮放器讀取影象(步驟 5)時,可能會看到影象的舊版本,或者什麼都沒有。如果它處理的是舊版本的影象,則檔案儲存中的全尺寸圖和縮圖就產生了永久性的不一致。 -出現這個問題是因為Web伺服器和縮放器之間存在兩個不同的通道:檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證,這兩個通道之間的競爭條件是可能的。這種情況類似於[圖9-1](../img/fig9-1.png),資料庫複製與Alice的嘴到Bob耳朵之間的真人音訊通道之間也存在競爭條件。 +出現這個問題是因為 Web 伺服器和縮放器之間存在兩個不同的通道:檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證,這兩個通道之間的競爭條件是可能的。這種情況類似於 [圖 9-1](../img/fig9-1.png),資料庫複製與 Alice 的嘴到 Bob 耳朵之間的真人音訊通道之間也存在競爭條件。 -線性一致性並不是避免這種競爭條件的唯一方法,但它是最容易理解的。如果你可以控制額外通道(例如訊息佇列的例子,而不是在Alice和Bob的例子),則可以使用在“[讀己之寫](ch5.md#讀己之寫)”討論過的類似方法,不過會有額外的複雜度代價。 +線性一致性並不是避免這種競爭條件的唯一方法,但它是最容易理解的。如果你可以控制額外通道(例如訊息佇列的例子,而不是在 Alice 和 Bob 的例子),則可以使用在 “[讀己之寫](ch5.md#讀己之寫)” 討論過的類似方法,不過會有額外的複雜度代價。 ### 實現線性一致的系統 我們已經見到了幾個線性一致性有用的例子,讓我們思考一下,如何實現一個提供線性一致語義的系統。 -由於線性一致性本質上意味著“表現得好像只有一個數據副本,而且所有的操作都是原子的”,所以最簡單的答案就是,真的只用一個數據副本。但是這種方法無法容錯:如果持有該副本的節點失效,資料將會丟失,或者至少無法訪問,直到節點重新啟動。 +由於線性一致性本質上意味著 “表現得好像只有一個數據副本,而且所有的操作都是原子的”,所以最簡單的答案就是,真的只用一個數據副本。但是這種方法無法容錯:如果持有該副本的節點失效,資料將會丟失,或者至少無法訪問,直到節點重新啟動。 -使系統容錯最常用的方法是使用複製。我們再來回顧[第五章](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#處理節點宕機)”),這同時違反了永續性和線性一致性。 * 共識演算法(線性一致) - 一些在本章後面討論的共識演算法,與單領導者複製類似。然而,共識協議包含防止腦裂和陳舊副本的措施。正是由於這些細節,共識演算法可以安全地實現線性一致性儲存。例如,Zookeeper 【21】和etcd 【22】就是這樣工作的。 + 一些在本章後面討論的共識演算法,與單領導者複製類似。然而,共識協議包含防止腦裂和陳舊副本的措施。正是由於這些細節,共識演算法可以安全地實現線性一致性儲存。例如,Zookeeper 【21】和 etcd 【22】就是這樣工作的。 * 多主複製(非線性一致) - 具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱“[處理寫入衝突](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#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也只是可能的,如下節所示。 #### 線性一致性和法定人數 -直覺上在Dynamo風格的模型中,嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時,就可能存在競爭條件,如[圖9-6](../img/fig9-6.png)所示。 +直覺上在 Dynamo 風格的模型中,嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時,就可能存在競爭條件,如 [圖 9-6](../img/fig9-6.png) 所示。 ![](../img/fig9-6.png) -**圖9-6 非線性一致的執行,儘管使用了嚴格的法定人數** +**圖 9-6 非線性一致的執行,儘管使用了嚴格的法定人數** -在[圖9-6](../img/fig9-6.png)中,$x$ 的初始值為0,寫入客戶端透過向所有三個副本( $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端A併發地從兩個節點組成的法定人群( $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端B也併發地從兩個不同的節點組成的法定人數中讀取,並從兩個節點中取回了舊值 `0` 。 +在 [圖 9-6](../img/fig9-6.png) 中,$x$ 的初始值為 0,寫入客戶端透過向所有三個副本( $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端 A 併發地從兩個節點組成的法定人群( $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端 B 也併發地從兩個不同的節點組成的法定人數中讀取,並從兩個節點中取回了舊值 `0` 。 -法定人數條件滿足( $w + r> n$ ),但是這個執行是非線性一致的:B的請求在A的請求完成後開始,但是B返回舊值,而A返回新值。 (又一次,如同Alice和Bob的例子 [圖9-1](../img/fig9-1.png)) +法定人數條件滿足( $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】。 -總而言之,最安全的做法是:假設採用Dynamo風格無主複製的系統不能提供線性一致性。 +總而言之,最安全的做法是:假設採用 Dynamo 風格無主複製的系統不能提供線性一致性。 ### 線性一致性的代價 一些複製方法可以提供線性一致性,另一些複製方法則不能,因此深入地探討線性一致性的優缺點是很有趣的。 -我們已經在[第五章](ch5.md)中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(請參閱“[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖9-7](../img/fig9-7.png)說明了這種部署的一個例子。 +我們已經在 [第五章](ch5.md) 中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(請參閱 “[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖 9-7](../img/fig9-7.png) 說明了這種部署的一個例子。 ![](../img/fig9-7.png) -**圖9-7 網路中斷迫使線上性一致性和可用性之間做出選擇。** +**圖 9-7 網路中斷迫使線上性一致性和可用性之間做出選擇。** 考慮這樣一種情況:如果兩個資料中心之間發生網路中斷會發生什麼?我們假設每個資料中心內的網路正在工作,客戶端可以訪問資料中心,但資料中心之間彼此無法互相連線。 @@ -262,101 +262,101 @@ 這個問題不僅僅是單主複製和多主複製的後果:任何線性一致的資料庫都有這個問題,不管它是如何實現的。這個問題也不僅僅侷限於多資料中心部署,而可能發生在任何不可靠的網路上,即使在同一個資料中心內也是如此。問題面臨的權衡如下:[^v] -* 如果應用需要線性一致性,且某些副本因為網路問題與其他副本斷開連線,那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決,或直接返回錯誤。(無論哪種方式,服務都**不可用**)。 +* 如果應用需要線性一致性,且某些副本因為網路問題與其他副本斷開連線,那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決,或直接返回錯誤。(無論哪種方式,服務都 **不可用**)。 * 如果應用不需要線性一致性,那麼某個副本即使與其他副本斷開連線,也可以獨立處理請求(例如多主複製)。在這種情況下,應用可以在網路問題前保持可用,但其行為不是線性一致的。 -[^v]: 這兩種選擇有時分別稱為CP(在網路分割槽下一致但不可用)和AP(在網路分割槽下可用但不一致)。 但是,這種分類方案存在一些缺陷【9】,所以最好不要這樣用。 +[^v]: 這兩種選擇有時分別稱為 CP(在網路分割槽下一致但不可用)和 AP(在網路分割槽下可用但不一致)。 但是,這種分類方案存在一些缺陷【9】,所以最好不要這樣用。 -因此,不需要線性一致性的應用對網路問題有更強的容錯能力。這種見解通常被稱為CAP定理【29,30,31,32】,由Eric Brewer於2000年命名,儘管70年代的分散式資料庫設計者早就知道了這種權衡【33,34,35,36】。 +因此,不需要線性一致性的應用對網路問題有更強的容錯能力。這種見解通常被稱為 CAP 定理【29,30,31,32】,由 Eric Brewer 於 2000 年命名,儘管 70 年代的分散式資料庫設計者早就知道了這種權衡【33,34,35,36】。 -CAP最初是作為一個經驗法則提出的,沒有準確的定義,目的是開始討論資料庫的權衡。那時候許多分散式資料庫側重於在共享儲存的叢集上提供線性一致性的語義【18】,CAP定理鼓勵資料庫工程師向分散式無共享系統的設計領域深入探索,這類架構更適合實現大規模的網路服務【37】。 對於這種文化上的轉變,CAP值得讚揚 —— 它見證了自00年代中期以來新資料庫的技術爆炸(即NoSQL)。 +CAP 最初是作為一個經驗法則提出的,沒有準確的定義,目的是開始討論資料庫的權衡。那時候許多分散式資料庫側重於在共享儲存的叢集上提供線性一致性的語義【18】,CAP 定理鼓勵資料庫工程師向分散式無共享系統的設計領域深入探索,這類架構更適合實現大規模的網路服務【37】。 對於這種文化上的轉變,CAP 值得讚揚 —— 它見證了自 00 年代中期以來新資料庫的技術爆炸(即 NoSQL)。 > #### CAP定理沒有幫助 > -> CAP有時以這種面目出現:一致性,可用性和分割槽容錯性:三者只能擇其二。不幸的是這種說法很有誤導性【32】,因為網路分割槽是一種故障型別,所以它並不是一個選項:不管你喜不喜歡它都會發生【38】。 +> CAP 有時以這種面目出現:一致性,可用性和分割槽容錯性:三者只能擇其二。不幸的是這種說法很有誤導性【32】,因為網路分割槽是一種故障型別,所以它並不是一個選項:不管你喜不喜歡它都會發生【38】。 > -> 在網路正常工作的時候,系統可以提供一致性(線性一致性)和整體可用性。發生網路故障時,你必須線上性一致性和整體可用性之間做出選擇。因此,CAP更好的表述成:在分割槽時要麼選擇一致,要麼選擇可用【39】。一個更可靠的網路需要減少這個選擇,但是在某些時候選擇是不可避免的。 +> 在網路正常工作的時候,系統可以提供一致性(線性一致性)和整體可用性。發生網路故障時,你必須線上性一致性和整體可用性之間做出選擇。因此,CAP 更好的表述成:在分割槽時要麼選擇一致,要麼選擇可用【39】。一個更可靠的網路需要減少這個選擇,但是在某些時候選擇是不可避免的。 > -> 在CAP的討論中,術語可用性有幾個相互矛盾的定義,形式化作為一個定理【30】並不符合其通常的含義【40】。許多所謂的“高可用”(容錯)系統實際上不符合CAP對可用性的特殊定義。總而言之,圍繞著CAP有很多誤解和困惑,並不能幫助我們更好地理解系統,所以最好避免使用CAP。 +> 在 CAP 的討論中,術語可用性有幾個相互矛盾的定義,形式化作為一個定理【30】並不符合其通常的含義【40】。許多所謂的 “高可用”(容錯)系統實際上不符合 CAP 對可用性的特殊定義。總而言之,圍繞著 CAP 有很多誤解和困惑,並不能幫助我們更好地理解系統,所以最好避免使用 CAP。 -CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一個一致性模型(即線性一致性)和一種故障(網路分割槽[^vi],或活躍但彼此斷開的節點)。它沒有討論任何關於網路延遲,死亡節點或其他權衡的事。 因此,儘管CAP在歷史上有一些影響力,但對於設計系統而言並沒有實際價值【9,40】。 +CAP 定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一個一致性模型(即線性一致性)和一種故障(網路分割槽 [^vi],或活躍但彼此斷開的節點)。它沒有討論任何關於網路延遲,死亡節點或其他權衡的事。 因此,儘管 CAP 在歷史上有一些影響力,但對於設計系統而言並沒有實際價值【9,40】。 -在分散式系統中有更多有趣的“不可能”的結果【41】,且CAP定理現在已經被更精確的結果取代【2,42】,所以它現在基本上成了歷史古蹟了。 +在分散式系統中有更多有趣的 “不可能” 的結果【41】,且 CAP 定理現在已經被更精確的結果取代【2,42】,所以它現在基本上成了歷史古蹟了。 -[^vi]: 正如“[真實世界的網路故障](ch8.md#真實世界的網路故障)”中所討論的,本書使用**分割槽(partition)** 指代將大資料集細分為小資料集的操作(分片;請參閱[第六章](ch6.md))。與之對應的是,**網路分割槽(network partition)** 是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是CAP的P,所以這種情況下我們無法避免混亂。 +[^vi]: 正如 “[真實世界的網路故障](ch8.md#真實世界的網路故障)” 中所討論的,本書使用 **分割槽(partition)** 指代將大資料集細分為小資料集的操作(分片;請參閱 [第六章](ch6.md))。與之對應的是,**網路分割槽(network partition)** 是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是 CAP 的 P,所以這種情況下我們無法避免混亂。 #### 線性一致性和網路延遲 -雖然線性一致是一個很有用的保證,但實際上,線性一致的系統驚人的少。例如,現代多核CPU上的記憶體甚至都不是線性一致的【43】:如果一個CPU核上執行的執行緒寫入某個記憶體地址,而另一個CPU核上執行的執行緒不久之後讀取相同的地址,並沒有保證一定能一定讀到第一個執行緒寫入的值(除非使用了**記憶體屏障(memory barrier)** 或**圍欄(fence)**【44】)。 +雖然線性一致是一個很有用的保證,但實際上,線性一致的系統驚人的少。例如,現代多核 CPU 上的記憶體甚至都不是線性一致的【43】:如果一個 CPU 核上執行的執行緒寫入某個記憶體地址,而另一個 CPU 核上執行的執行緒不久之後讀取相同的地址,並沒有保證一定能一定讀到第一個執行緒寫入的值(除非使用了 **記憶體屏障(memory barrier)** 或 **圍欄(fence)**【44】)。 -這種行為的原因是每個CPU核都有自己的記憶體快取和儲存緩衝區。預設情況下,記憶體訪問首先走快取,任何變更會非同步寫入主存。因為快取訪問比主存要快得多【45】,所以這個特性對於現代CPU的良好效能表現至關重要。但是現在就有幾個資料副本(一個在主存中,也許還有幾個在不同快取中的其他副本),而且這些副本是非同步更新的,所以就失去了線性一致性。 +這種行為的原因是每個 CPU 核都有自己的記憶體快取和儲存緩衝區。預設情況下,記憶體訪問首先走快取,任何變更會非同步寫入主存。因為快取訪問比主存要快得多【45】,所以這個特性對於現代 CPU 的良好效能表現至關重要。但是現在就有幾個資料副本(一個在主存中,也許還有幾個在不同快取中的其他副本),而且這些副本是非同步更新的,所以就失去了線性一致性。 -為什麼要做這個權衡?對多核記憶體一致性模型而言,CAP定理是沒有意義的:在同一臺計算機中,我們通常假定通訊都是可靠的。並且我們並不指望一個CPU核能在脫離計算機其他部分的條件下繼續正常工作。犧牲線性一致性的原因是**效能(performance)**,而不是容錯。 +為什麼要做這個權衡?對多核記憶體一致性模型而言,CAP 定理是沒有意義的:在同一臺計算機中,我們通常假定通訊都是可靠的。並且我們並不指望一個 CPU 核能在脫離計算機其他部分的條件下繼續正常工作。犧牲線性一致性的原因是 **效能(performance)**,而不是容錯。 -許多分散式資料庫也是如此:它們是**為了提高效能**而選擇了犧牲線性一致性,而不是為了容錯【46】。線性一致的速度很慢——這始終是事實,而不僅僅是網路故障期間。 +許多分散式資料庫也是如此:它們是 **為了提高效能** 而選擇了犧牲線性一致性,而不是為了容錯【46】。線性一致的速度很慢 —— 這始終是事實,而不僅僅是網路故障期間。 -能找到一個更高效的線性一致儲存實現嗎?看起來答案是否定的:Attiya和Welch 【47】證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中(請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在[第十二章](ch12.md)中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。 +能找到一個更高效的線性一致儲存實現嗎?看起來答案是否定的:Attiya 和 Welch 【47】證明,如果你想要線性一致性,讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中(請參閱 “[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在 [第十二章](ch12.md) 中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。 ## 順序保證 -之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們將操作以看上去被執行的順序連線起來,以此說明了[圖9-4](../img/fig9-4.png)中的順序。 +之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們將操作以看上去被執行的順序連線起來,以此說明了 [圖 9-4](../img/fig9-4.png) 中的順序。 -**順序(ordering)** 這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過**順序**的上下文: +**順序(ordering)** 這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過 **順序** 的上下文: -* 在[第五章](ch5.md)中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定**寫入順序(order of write)**——也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。 -* 在[第七章](ch7.md)中討論的**可序列化**,是關於事務表現的像按**某種先後順序(some sequential order)** 執行的保證。它可以字面意義上地以**序列順序(serial order)** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。 -* 在[第八章](ch8.md)討論過的在分散式系統中使用時間戳和時鐘(請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。 +* 在 [第五章](ch5.md) 中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定 **寫入順序(order of write)**—— 也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)。 +* 在 [第七章](ch7.md) 中討論的 **可序列化**,是關於事務表現的像按 **某種先後順序(some sequential order)** 執行的保證。它可以字面意義上地以 **序列順序(serial order)** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。 +* 在 [第八章](ch8.md) 討論過的在分散式系統中使用時間戳和時鐘(請參閱 “[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。 事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。 ### 順序與因果關係 -**順序**反覆出現有幾個原因,其中一個原因是,它有助於保持**因果關係(causality)**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的: +**順序** 反覆出現有幾個原因,其中一個原因是,它有助於保持 **因果關係(causality)**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的: -* 在“[一致字首讀](ch5.md#一致字首讀)”([圖5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對**因(cause)** 與**果(effect)** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在**因果依賴(causal dependency)**。 -* [圖5-9](../img/fig5-9.png)中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會“壓倒”其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。 -* 在“[檢測併發寫入](ch5.md#檢測併發寫入)”中我們觀察到,如果有兩個操作A和B,則存在三種可能性:A發生在B之前,或B發生在A之前,或者A和B**併發**。這種**此前發生(happened before)** 關係是因果關係的另一種表述:如果A在B前發生,那麼意味著B可能已經知道了A,或者建立在A的基礎上,或者依賴於A。如果A和B是**併發**的,那麼它們之間並沒有因果聯絡;換句話說,我們確信A和B不知道彼此。 -* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中“一致”到底又是什麼意思?這意味著**與因果關係保持一致(consistent with causality)**:如果快照包含答案,它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫,與因果關係保持一致意味著:因果上在該時間點之前發生的所有操作,其影響都是可見的,但因果上在該時間點之後發生的操作,其影響對觀察者不可見。**讀偏差(read skew)** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如[圖7-6](../img/fig7-6.png)所示)。 -* 事務之間**寫偏差(write skew)** 的例子(請參閱“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”)也說明了因果依賴:在[圖7-8](../img/fig7-8.png)中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離)透過跟蹤事務之間的因果依賴來檢測寫偏差。 -* 在愛麗絲和鮑勃看球的例子中([圖9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在“[跨通道的時序依賴](#跨通道的時序依賴)”一節中,以“影象大小調整服務”的偽裝再次出現。 +* 在 “[一致字首讀](ch5.md#一致字首讀)”([圖 5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對 **因(cause)** 與 **果(effect)** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在 **因果依賴(causal dependency)**。 +* [圖 5-9](../img/fig5-9.png) 中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會 “壓倒” 其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。 +* 在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中我們觀察到,如果有兩個操作 A 和 B,則存在三種可能性:A 發生在 B 之前,或 B 發生在 A 之前,或者 A 和 B**併發**。這種 **此前發生(happened before)** 關係是因果關係的另一種表述:如果 A 在 B 前發生,那麼意味著 B 可能已經知道了 A,或者建立在 A 的基礎上,或者依賴於 A。如果 A 和 B 是 **併發** 的,那麼它們之間並沒有因果聯絡;換句話說,我們確信 A 和 B 不知道彼此。 +* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中 “一致” 到底又是什麼意思?這意味著 **與因果關係保持一致(consistent with causality)**:如果快照包含答案,它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫,與因果關係保持一致意味著:因果上在該時間點之前發生的所有操作,其影響都是可見的,但因果上在該時間點之後發生的操作,其影響對觀察者不可見。**讀偏差(read skew)** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如 [圖 7-6](../img/fig7-6.png) 所示)。 +* 事務之間 **寫偏差(write skew)** 的例子(請參閱 “[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”)也說明了因果依賴:在 [圖 7-8](../img/fig7-8.png) 中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離) 透過跟蹤事務之間的因果依賴來檢測寫偏差。 +* 在愛麗絲和鮑勃看球的例子中([圖 9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在 “[跨通道的時序依賴](#跨通道的時序依賴)” 一節中,以 “影象大小調整服務” 的偽裝再次出現。 -因果關係對事件施加了一種**順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。 +因果關係對事件施加了一種 **順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。 -如果一個系統服從因果關係所規定的順序,我們說它是**因果一致(causally consistent)** 的。例如,快照隔離提供了因果一致性:當你從資料庫中讀取到一些資料時,你一定還能夠看到其因果前驅(假設在此期間這些資料還沒有被刪除)。 +如果一個系統服從因果關係所規定的順序,我們說它是 **因果一致(causally consistent)** 的。例如,快照隔離提供了因果一致性:當你從資料庫中讀取到一些資料時,你一定還能夠看到其因果前驅(假設在此期間這些資料還沒有被刪除)。 #### 因果順序不是全序的 -**全序(total order)** 允許任意兩個元素進行比較,所以如果有兩個元素,你總是可以說出哪個更大,哪個更小。例如,自然數集是全序的:給定兩個自然數,比如說5和13,那麼你可以告訴我,13大於5。 +**全序(total order)** 允許任意兩個元素進行比較,所以如果有兩個元素,你總是可以說出哪個更大,哪個更小。例如,自然數集是全序的:給定兩個自然數,比如說 5 和 13,那麼你可以告訴我,13 大於 5。 -然而數學集合並不完全是全序的:`{a, b}` 比 `{b, c}` 更大嗎?好吧,你沒法真正比較它們,因為二者都不是對方的子集。我們說它們是**無法比較(incomparable)** 的,因此數學集合是**偏序(partially order)** 的:在某些情況下,可以說一個集合大於另一個(如果一個集合包含另一個集合的所有元素),但在其他情況下它們是無法比較的[^譯註i]。 +然而數學集合並不完全是全序的:`{a, b}` 比 `{b, c}` 更大嗎?好吧,你沒法真正比較它們,因為二者都不是對方的子集。我們說它們是 **無法比較(incomparable)** 的,因此數學集合是 **偏序(partially order)** 的:在某些情況下,可以說一個集合大於另一個(如果一個集合包含另一個集合的所有元素),但在其他情況下它們是無法比較的 [^譯註i]。 -[^譯註i]: 設R為非空集合A上的關係,如果R是自反的、反對稱的和可傳遞的,則稱R為A上的偏序關係。簡稱偏序,通常記作≦。一個集合A與A上的偏序關係R一起叫作偏序集,記作$(A,R)$或$(A, ≦)$。全序、偏序、關係、集合,這些概念的精確定義可以參考任意一本離散數學教材。 +[^譯註i]: 設 R 為非空集合 A 上的關係,如果 R 是自反的、反對稱的和可傳遞的,則稱 R 為 A 上的偏序關係。簡稱偏序,通常記作≦。一個集合 A 與 A 上的偏序關係 R 一起叫作偏序集,記作 $(A,R)$ 或 $(A, ≦)$。全序、偏序、關係、集合,這些概念的精確定義可以參考任意一本離散數學教材。 全序和偏序之間的差異反映在不同的資料庫一致性模型中: * 線性一致性 - 線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在[圖9-4](../img/fig9-4.png)中以時間線表示。 + 線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在 [圖 9-4](../img/fig9-4.png) 中以時間線表示。 * 因果性 - 我們說過,如果兩個操作都沒有在彼此**之前發生**,那麼這兩個操作是併發的(請參閱[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。 + 我們說過,如果兩個操作都沒有在彼此 **之前發生**,那麼這兩個操作是併發的(請參閱 [“此前發生” 的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。 因此,根據這個定義,線上性一致的資料儲存中是不存在併發操作的:必須有且僅有一條時間線,所有的操作都在這條時間線上,構成一個全序關係。可能有幾個請求在等待處理,但是資料儲存確保了每個請求都是在唯一時間線上的某個時間點自動處理的,不存在任何併發。 -併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在[第五章](ch5.md)中我們看到了這種現象:例如,[圖5-14](../img/fig5-14.png) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。 +併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在 [第五章](ch5.md) 中我們看到了這種現象:例如,[圖 5-14](../img/fig5-14.png) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。 -如果你熟悉像Git這樣的分散式版本控制系統,那麼其版本歷史與因果關係圖極其相似。通常,一個**提交(Commit)** 發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併(Merge)** 會在這些併發建立的提交相融合時建立。 +如果你熟悉像 Git 這樣的分散式版本控制系統,那麼其版本歷史與因果關係圖極其相似。通常,一個 **提交(Commit)** 發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併(Merge)** 會在這些併發建立的提交相融合時建立。 #### 線性一致性強於因果一致性 -那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性**隱含著(implies)** 因果關係:任何線性一致的系統都能正確保持因果性【7】。特別是,如果系統中有多個通訊通道(如[圖9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。 +那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性 **隱含著(implies)** 因果關係:任何線性一致的系統都能正確保持因果性【7】。特別是,如果系統中有多個通訊通道(如 [圖 9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。 -線性一致性確保因果性的事實使線性一致系統變得簡單易懂,更有吸引力。然而,正如“[線性一致性的代價](#線性一致性的代價)”中所討論的,使系統線性一致可能會損害其效能和可用性,尤其是在系統具有嚴重的網路延遲的情況下(例如,如果系統在地理上散佈)。出於這個原因,一些分散式資料系統已經放棄了線性一致性,從而獲得更好的效能,但它們用起來也更為困難。 +線性一致性確保因果性的事實使線性一致系統變得簡單易懂,更有吸引力。然而,正如 “[線性一致性的代價](#線性一致性的代價)” 中所討論的,使系統線性一致可能會損害其效能和可用性,尤其是在系統具有嚴重的網路延遲的情況下(例如,如果系統在地理上散佈)。出於這個原因,一些分散式資料系統已經放棄了線性一致性,從而獲得更好的效能,但它們用起來也更為困難。 -好訊息是存在折衷的可能性。線性一致性並不是保持因果性的唯一途徑 —— 還有其他方法。一個系統可以是因果一致的,而無需承擔線性一致帶來的效能折損(尤其對於CAP定理不適用的情況)。實際上在所有的不會被網路延遲拖慢的一致性模型中,因果一致性是可行的最強的一致性模型。而且在網路故障時仍能保持可用【2,42】。 +好訊息是存在折衷的可能性。線性一致性並不是保持因果性的唯一途徑 —— 還有其他方法。一個系統可以是因果一致的,而無需承擔線性一致帶來的效能折損(尤其對於 CAP 定理不適用的情況)。實際上在所有的不會被網路延遲拖慢的一致性模型中,因果一致性是可行的最強的一致性模型。而且在網路故障時仍能保持可用【2,42】。 在許多情況下,看上去需要線性一致性的系統,實際上需要的只是因果一致性,因果一致性可以更高效地實現。基於這種觀察結果,研究人員正在探索新型的資料庫,既能保證因果一致性,且效能與可用性與最終一致的系統類似【49,50,51】。 @@ -368,34 +368,34 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 為了維持因果性,你需要知道哪個操作發生在哪個其他操作之前(**happened before**)。這是一個偏序:併發操作可以以任意順序進行,但如果一個操作發生在另一個操作之前,那它們必須在所有副本上以那個順序被處理。因此,當一個副本處理一個操作時,它必須確保所有因果前驅的操作(之前發生的所有操作)已經被處理;如果前面的某個操作丟失了,後面的操作必須等待,直到前面的操作被處理完畢。 -為了確定因果依賴,我們需要一些方法來描述系統中節點的“知識”。如果節點在發出寫入Y 的請求時已經看到了 X的值,則 X 和 Y 可能存在因果關係。這個分析使用了那些在欺詐指控刑事調查中常見的問題:CEO在做出決定 Y 時是否**知道** X ? +為了確定因果依賴,我們需要一些方法來描述系統中節點的 “知識”。如果節點在發出寫入 Y 的請求時已經看到了 X 的值,則 X 和 Y 可能存在因果關係。這個分析使用了那些在欺詐指控刑事調查中常見的問題:CEO 在做出決定 Y 時是否 **知道** X ? -用於確定*哪些操作發生在其他操作之前* 的技術,與我們在“[檢測併發寫入](ch5.md#檢測併發寫入)”中所討論的內容類似。那一節討論了無領導者資料儲存中的因果性:為了防止丟失更新,我們需要檢測到對同一個鍵的併發寫入。因果一致性則更進一步:它需要跟蹤整個資料庫中的因果依賴,而不僅僅是一個鍵。可以推廣版本向量以解決此類問題【54】。 +用於確定 *哪些操作發生在其他操作之前* 的技術,與我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中所討論的內容類似。那一節討論了無領導者資料儲存中的因果性:為了防止丟失更新,我們需要檢測到對同一個鍵的併發寫入。因果一致性則更進一步:它需要跟蹤整個資料庫中的因果依賴,而不僅僅是一個鍵。可以推廣版本向量以解決此類問題【54】。 -為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖5-13 ](../img/fig5-13.png)中,來自先前操作的版本號在寫入時被傳回到資料庫的原因。在SSI 的衝突檢測中會出現類似的想法,如“[可序列化快照隔離](ch7.md#可序列化快照隔離)”中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。 +為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖 5-13](../img/fig5-13.png) 中,來自先前操作的版本號在寫入時被傳回到資料庫的原因。在 SSI 的衝突檢測中會出現類似的想法,如 “[可序列化快照隔離](ch7.md#可序列化快照隔離)” 中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。 ### 序列號順序 雖然因果是一個重要的理論概念,但實際上跟蹤所有的因果關係是不切實際的。在許多應用中,客戶端在寫入內容之前會先讀取大量資料,我們無法弄清寫入因果依賴於先前全部的讀取內容,還是僅包括其中一部分。顯式跟蹤所有已讀資料意味著巨大的額外開銷。 -但還有一個更好的方法:我們可以使用**序列號(sequence nunber)** 或**時間戳(timestamp)** 來排序事件。時間戳不一定來自日曆時鐘(或物理時鐘,它們存在許多問題,如 “[不可靠的時鐘](ch8.md#不可靠的時鐘)” 中所述)。它可以來自一個**邏輯時鐘(logical clock)**,這是一個用來生成標識操作的數字序列的演算法,典型實現是使用一個每次操作自增的計數器。 +但還有一個更好的方法:我們可以使用 **序列號(sequence nunber)** 或 **時間戳(timestamp)** 來排序事件。時間戳不一定來自日曆時鐘(或物理時鐘,它們存在許多問題,如 “[不可靠的時鐘](ch8.md#不可靠的時鐘)” 中所述)。它可以來自一個 **邏輯時鐘(logical clock)**,這是一個用來生成標識操作的數字序列的演算法,典型實現是使用一個每次操作自增的計數器。 這樣的序列號或時間戳是緊湊的(只有幾個位元組大小),它提供了一個全序關係:也就是說每個操作都有一個唯一的序列號,而且總是可以比較兩個序列號,確定哪一個更大(即哪些操作後發生)。 -特別是,我們可以使用**與因果一致(consistent with causality)** 的全序來生成序列號[^vii]:我們保證,如果操作 A 因果地發生在操作 B 前,那麼在這個全序中 A 在 B 前( A 具有比 B 更小的序列號)。並行操作之間可以任意排序。這樣一個全序關係捕獲了所有關於因果的資訊,但也施加了一個比因果性要求更為嚴格的順序。 +特別是,我們可以使用 **與因果一致(consistent with causality)** 的全序來生成序列號 [^vii]:我們保證,如果操作 A 因果地發生在操作 B 前,那麼在這個全序中 A 在 B 前( A 具有比 B 更小的序列號)。並行操作之間可以任意排序。這樣一個全序關係捕獲了所有關於因果的資訊,但也施加了一個比因果性要求更為嚴格的順序。 -[^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的UUID,並按照字典序比較UUID,以定義操作的全序。這是一個有效的全序,但是隨機的UUID並不能告訴你哪個操作先發生,或者操作是否為併發的。 +[^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的 UUID,並按照字典序比較 UUID,以定義操作的全序。這是一個有效的全序,但是隨機的 UUID 並不能告訴你哪個操作先發生,或者操作是否為併發的。 -在單主複製的資料庫中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。 +在單主複製的資料庫中(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。 #### 非因果序列號生成器 如果主庫不存在(可能因為使用了多主資料庫或無主資料庫,或者因為使用了分割槽的資料庫),如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法: * 每個節點都可以生成自己獨立的一組序列號。例如有兩個節點,一個節點只能生成奇數,而另一個節點只能生成偶數。通常,可以在序列號的二進位制表示中預留一些位,用於唯一的節點識別符號,這樣可以確保兩個不同的節點永遠不會生成相同的序列號。 -* 可以將日曆時鐘(物理時鐘)的時間戳附加到每個操作上【55】。這種時間戳並不連續,但是如果它具有足夠高的解析度,那也許足以提供一個操作的全序關係。這一事實應用於 *最後寫入勝利* 的衝突解決方法中(請參閱“[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。 -* 可以預先分配序列號區塊。例如,節點 A 可能要求從序列號1到1,000區塊的所有權,而節點 B 可能要求序列號1,001到2,000區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號,並在序列號告急時請求分配一個新的區塊。 +*可以將日曆時鐘(物理時鐘)的時間戳附加到每個操作上【55】。這種時間戳並不連續,但是如果它具有足夠高的解析度,那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中(請參閱 “[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。 +* 可以預先分配序列號區塊。例如,節點 A 可能要求從序列號 1 到 1,000 區塊的所有權,而節點 B 可能要求序列號 1,001 到 2,000 區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號,並在序列號告急時請求分配一個新的區塊。 這三個選項都比單一主庫的自增計數器表現要好,並且更具可伸縮性。它們為每個操作生成一個唯一的,近似自增的序列號。然而它們都有同一個問題:生成的序列號與因果不一致。 @@ -403,29 +403,29 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 * 每個節點每秒可以處理不同數量的操作。因此,如果一個節點產生偶數序列號而另一個產生奇數序列號,則偶數計數器可能落後於奇數計數器,反之亦然。如果你有一個奇數編號的操作和一個偶數編號的操作,你無法準確地說出哪一個操作在因果上先發生。 -* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如[圖8-3](../img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^vii] +* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如 [圖 8-3](../img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^vii] - [^viii]: 可以使物理時鐘時間戳與因果關係保持一致:在“[全域性快照的同步時鐘](#全域性快照的同步時鐘)”中,我們討論了Google的Spanner,它可以估計預期的時鐘偏差,並在提交寫入之前等待不確定性間隔。這種方法確保了實際上靠後的事務會有更大的時間戳。但是大多數時鐘不能提供這種所需的不確定性度量。 + [^viii]: 可以使物理時鐘時間戳與因果關係保持一致:在 “[全域性快照的同步時鐘](#全域性快照的同步時鐘)” 中,我們討論了 Google 的 Spanner,它可以估計預期的時鐘偏差,並在提交寫入之前等待不確定性間隔。這種方法確保了實際上靠後的事務會有更大的時間戳。但是大多數時鐘不能提供這種所需的不確定性度量。 -* 在分配區塊的情況下,某個操作可能會被賦予一個範圍在1,001到2,000內的序列號,然而一個因果上更晚的操作可能被賦予一個範圍在1到1,000之間的數字。這裡序列號與因果關係也是不一致的。 +* 在分配區塊的情況下,某個操作可能會被賦予一個範圍在 1,001 到 2,000 內的序列號,然而一個因果上更晚的操作可能被賦予一個範圍在 1 到 1,000 之間的數字。這裡序列號與因果關係也是不一致的。 #### 蘭伯特時間戳 -儘管剛才描述的三個序列號生成器與因果不一致,但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳,萊斯利·蘭伯特(Leslie Lamport)於1978年提出【56】,現在是分散式系統領域中被引用最多的論文之一。 +儘管剛才描述的三個序列號生成器與因果不一致,但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳,萊斯利・蘭伯特(Leslie Lamport)於 1978 年提出【56】,現在是分散式系統領域中被引用最多的論文之一。 -[圖9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。 蘭伯特時間戳就是兩者的簡單組合:(計數器,節點ID)$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點ID,每個時間戳都是唯一的。 +[圖 9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。 蘭伯特時間戳就是兩者的簡單組合:(計數器,節點 ID)$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID,每個時間戳都是唯一的。 ![](../img/fig9-8.png) -**圖9-8 Lamport時間戳提供了與因果關係一致的全序。** +**圖 9-8 Lamport 時間戳提供了與因果關係一致的全序。** -蘭伯特時間戳與物理的日曆時鐘沒有任何關係,但是它提供了一個全序:如果你有兩個時間戳,則**計數器**值大者是更大的時間戳。如果計數器值相同,則節點ID越大的,時間戳越大。 +蘭伯特時間戳與物理的日曆時鐘沒有任何關係,但是它提供了一個全序:如果你有兩個時間戳,則 **計數器** 值大者是更大的時間戳。如果計數器值相同,則節點 ID 越大的,時間戳越大。 -迄今,這個描述與上節所述的奇偶計數器基本類似。使蘭伯特時間戳因果一致的關鍵思想如下所示:每個節點和每個客戶端跟蹤迄今為止所見到的最大**計數器**值,並在每個請求中包含這個最大計數器值。當一個節點收到最大計數器值大於自身計數器值的請求或響應時,它立即將自己的計數器設定為這個最大值。 +迄今,這個描述與上節所述的奇偶計數器基本類似。使蘭伯特時間戳因果一致的關鍵思想如下所示:每個節點和每個客戶端跟蹤迄今為止所見到的最大 **計數器** 值,並在每個請求中包含這個最大計數器值。當一個節點收到最大計數器值大於自身計數器值的請求或響應時,它立即將自己的計數器設定為這個最大值。 -這如 [圖9-8](../img/fig9-8.png) 所示,其中客戶端 A 從節點2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點1 。此時,節點1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6` 。 +這如 [圖 9-8](../img/fig9-8.png) 所示,其中客戶端 A 從節點 2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點 1 。此時,節點 1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6` 。 只要每一個操作都攜帶著最大計數器值,這個方案確保蘭伯特時間戳的排序與因果一致,因為每個因果依賴都會導致時間戳增長。 @@ -435,11 +435,11 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 雖然蘭伯特時間戳定義了一個與因果一致的全序,但它還不足以解決分散式系統中的許多常見問題。 -例如,考慮一個需要確保使用者名稱能唯一標識使用者帳戶的系統。如果兩個使用者同時嘗試使用相同的使用者名稱建立帳戶,則其中一個應該成功,另一個應該失敗(我們之前在“[領導者和鎖](ch8.md#領導者和鎖)”中提到過這個問題)。 +例如,考慮一個需要確保使用者名稱能唯一標識使用者帳戶的系統。如果兩個使用者同時嘗試使用相同的使用者名稱建立帳戶,則其中一個應該成功,另一個應該失敗(我們之前在 “[領導者和鎖](ch8.md#領導者和鎖)” 中提到過這個問題)。 乍看之下,似乎操作的全序關係足以解決這一問題(例如使用蘭伯特時間戳):如果建立了兩個具有相同使用者名稱的帳戶,選擇時間戳較小的那個作為勝者(第一個抓到使用者名稱的人),並讓帶有更大時間戳者失敗。由於時間戳上有全序關係,所以這個比較總是可行的。 -這種方法適用於事後確定勝利者:一旦你收集了系統中的所有使用者名稱建立操作,就可以比較它們的時間戳。然而當某個節點需要實時處理使用者建立使用者名稱的請求時,這樣的方法就無法滿足了。節點需要**馬上(right now)** 決定這個請求是成功還是失敗。在那個時刻,節點並不知道是否存在其他節點正在併發執行建立同樣使用者名稱的操作,罔論其它節點可能分配給那個操作的時間戳。 +這種方法適用於事後確定勝利者:一旦你收集了系統中的所有使用者名稱建立操作,就可以比較它們的時間戳。然而當某個節點需要實時處理使用者建立使用者名稱的請求時,這樣的方法就無法滿足了。節點需要 **馬上(right now)** 決定這個請求是成功還是失敗。在那個時刻,節點並不知道是否存在其他節點正在併發執行建立同樣使用者名稱的操作,罔論其它節點可能分配給那個操作的時間戳。 為了確保沒有其他節點正在使用相同的使用者名稱和較小的時間戳併發建立同名賬戶,你必須檢查其它每個節點,看看它在做什麼【56】。如果其中一個節點由於網路問題出現故障或不可達,則整個系統可能被拖至停機。這不是我們需要的那種容錯系統。 @@ -447,15 +447,15 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 總之:為了實現諸如使用者名稱上的唯一約束這種東西,僅有操作的全序是不夠的,你還需要知道這個全序何時會塵埃落定。如果你有一個建立使用者名稱的操作,並且確定在全序中沒有任何其他節點可以在你的操作之前插入對同一使用者名稱的聲稱,那麼你就可以安全地宣告操作執行成功。 -如何確定全序關係已經塵埃落定,這將在[全序廣播](#全序廣播)一節中詳細說明。 +如何確定全序關係已經塵埃落定,這將在 [全序廣播](#全序廣播) 一節中詳細說明。 ### 全序廣播 -如果你的程式只執行在單個CPU核上,那麼定義一個操作全序是很容易的:可以簡單地就是CPU執行這些操作的順序。但是在分散式系統中,讓所有節點對同一個全域性操作順序達成一致可能相當棘手。在上一節中,我們討論了按時間戳或序列號進行排序,但發現它還不如單主複製給力(如果你使用時間戳排序來實現唯一性約束,就不能容忍任何錯誤,因為你必須要從每個節點都獲取到最新的序列號)。 +如果你的程式只執行在單個 CPU 核上,那麼定義一個操作全序是很容易的:可以簡單地就是 CPU 執行這些操作的順序。但是在分散式系統中,讓所有節點對同一個全域性操作順序達成一致可能相當棘手。在上一節中,我們討論了按時間戳或序列號進行排序,但發現它還不如單主複製給力(如果你使用時間戳排序來實現唯一性約束,就不能容忍任何錯誤,因為你必須要從每個節點都獲取到最新的序列號)。 -如前所述,單主複製透過選擇一個節點作為主庫來確定操作的全序,並在主庫的單個CPU核上對所有操作進行排序。接下來的挑戰是,如果吞吐量超出單個主庫的處理能力,這種情況下如何擴充套件系統;以及,如果主庫失效(“[處理節點宕機](ch5.md#處理節點宕機)”),如何處理故障切換。在分散式系統文獻中,這個問題被稱為**全序廣播(total order broadcast)** 或**原子廣播(atomic broadcast)**[^ix]【25,57,58】。 +如前所述,單主複製透過選擇一個節點作為主庫來確定操作的全序,並在主庫的單個 CPU 核上對所有操作進行排序。接下來的挑戰是,如果吞吐量超出單個主庫的處理能力,這種情況下如何擴充套件系統;以及,如果主庫失效(“[處理節點宕機](ch5.md#處理節點宕機)”),如何處理故障切換。在分散式系統文獻中,這個問題被稱為 **全序廣播(total order broadcast)** 或 **原子廣播(atomic broadcast)**[^ix]【25,57,58】。 -[^ix]: “原子廣播”是一個傳統的術語,非常混亂,而且與“原子”一詞的其他用法不一致:它與ACID事務中的原子性沒有任何關係,只是與原子操作(在多執行緒程式設計的意義上 )或原子暫存器(線性一致儲存)有間接的聯絡。全序組播(total order multicast)是另一個同義詞。 +[^ix]: “原子廣播” 是一個傳統的術語,非常混亂,而且與 “原子” 一詞的其他用法不一致:它與 ACID 事務中的原子性沒有任何關係,只是與原子操作(在多執行緒程式設計的意義上 )或原子暫存器(線性一致儲存)有間接的聯絡。全序組播(total order multicast)是另一個同義詞。 > #### 順序保證的範圍 > @@ -475,66 +475,66 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 #### 使用全序廣播 -像ZooKeeper和etcd這樣的共識服務實際上實現了全序廣播。這一事實暗示了全序廣播與共識之間有著緊密聯絡,我們將在本章稍後進行探討。 +像 ZooKeeper 和 etcd 這樣的共識服務實際上實現了全序廣播。這一事實暗示了全序廣播與共識之間有著緊密聯絡,我們將在本章稍後進行探討。 -全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為**狀態機複製(state machine replication)**【60】,我們將在[第十一章](ch11.md)中重新回到這個概念。 +全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為 **狀態機複製(state machine replication)**【60】,我們將在 [第十一章](ch11.md) 中重新回到這個概念。 -與之類似,可以使用全序廣播來實現可序列化的事務:如“[真的序列執行](ch7.md#真的序列執行)”中所述,如果每個訊息都表示一個確定性事務,以儲存過程的形式來執行,且每個節點都以相同的順序處理這些訊息,那麼資料庫的分割槽和副本就可以相互保持一致【61】。 +與之類似,可以使用全序廣播來實現可序列化的事務:如 “[真的序列執行](ch7.md#真的序列執行)” 中所述,如果每個訊息都表示一個確定性事務,以儲存過程的形式來執行,且每個節點都以相同的順序處理這些訊息,那麼資料庫的分割槽和副本就可以相互保持一致【61】。 全序廣播的一個重要表現是,順序在訊息送達時被固化:如果後續的訊息已經送達,節點就不允許追溯地將(先前)訊息插入順序中的較早位置。這個事實使得全序廣播比時間戳排序更強。 考量全序廣播的另一種方式是,這是一種建立日誌的方式(如在複製日誌、事務日誌或預寫式日誌中):傳遞訊息就像追加寫入日誌。由於所有節點必須以相同的順序傳遞相同的訊息,因此所有節點都可以讀取日誌,並看到相同的訊息序列。 -全序廣播對於實現提供防護令牌的鎖服務也很有用(請參閱“[防護令牌](ch8.md#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在ZooKeeper中,這個序列號被稱為`zxid` 【15】。 +全序廣播對於實現提供防護令牌的鎖服務也很有用(請參閱 “[防護令牌](ch8.md#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在 ZooKeeper 中,這個序列號被稱為 `zxid` 【15】。 #### 使用全序廣播實現線性一致的儲存 -如 [圖9-4](../img/fig9-4.png) 所示,線上性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡[^x]。 +如 [圖 9-4](../img/fig9-4.png) 所示,線上性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡 [^x]。 -[^x]: 從形式上講,線性一致讀寫暫存器是一個“更容易”的問題。 全序廣播等價於共識【67】,而共識問題在非同步的崩潰-停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器**可以**在這種模型中實現【23,24,25】。 然而,支援諸如**比較並設定(CAS, compare-and-set)**,或**自增並返回(increment-and-get)** 的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。 +[^x]: 從形式上講,線性一致讀寫暫存器是一個 “更容易” 的問題。 全序廣播等價於共識【67】,而共識問題在非同步的崩潰 - 停止模型【68】中沒有確定性的解決方案,而線性一致的讀寫暫存器 **可以** 在這種模型中實現【23,24,25】。 然而,支援諸如 **比較並設定(CAS, compare-and-set)**,或 **自增並返回(increment-and-get)** 的原子操作使它等價於共識問題【28】。 因此,共識問題與線性一致暫存器問題密切相關。 -全序廣播是非同步的:訊息被保證以固定的順序可靠地傳送,但是不能保證訊息**何時**被送達(所以一個接收者可能落後於其他接收者)。相比之下,線性一致性是新鮮性的保證:讀取一定能看見最新的寫入值。 +全序廣播是非同步的:訊息被保證以固定的順序可靠地傳送,但是不能保證訊息 **何時** 被送達(所以一個接收者可能落後於其他接收者)。相比之下,線性一致性是新鮮性的保證:讀取一定能看見最新的寫入值。 但如果有了全序廣播,你就可以在此基礎上構建線性一致的儲存。例如,你可以確保使用者名稱能唯一標識使用者帳戶。 -設想對於每一個可能的使用者名稱,你都可以有一個帶有CAS原子操作的線性一致暫存器。每個暫存器最初的值為空值(表示未使用該使用者名稱)。當用戶想要建立一個使用者名稱時,對該使用者名稱的暫存器執行CAS操作,在先前暫存器值為空的條件,將其值設定為使用者的賬號ID。如果多個使用者試圖同時獲取相同的使用者名稱,則只有一個CAS操作會成功,因為其他使用者會看到非空的值(由於線性一致性)。 +設想對於每一個可能的使用者名稱,你都可以有一個帶有 CAS 原子操作的線性一致暫存器。每個暫存器最初的值為空值(表示未使用該使用者名稱)。當用戶想要建立一個使用者名稱時,對該使用者名稱的暫存器執行 CAS 操作,在先前暫存器值為空的條件,將其值設定為使用者的賬號 ID。如果多個使用者試圖同時獲取相同的使用者名稱,則只有一個 CAS 操作會成功,因為其他使用者會看到非空的值(由於線性一致性)。 -你可以透過將全序廣播當成僅追加日誌【62,63】的方式來實現這種線性一致的CAS操作: +你可以透過將全序廣播當成僅追加日誌【62,63】的方式來實現這種線性一致的 CAS 操作: 1. 在日誌中追加一條訊息,試探性地指明你要宣告的使用者名稱。 2. 讀日誌,並等待你剛才追加的訊息被讀回。[^xi] 4. 檢查是否有任何訊息聲稱目標使用者名稱的所有權。如果這些訊息中的第一條就是你自己的訊息,那麼你就成功了:你可以提交聲稱的使用者名稱(也許是透過向日志追加另一條訊息)並向客戶端確認。如果所需使用者名稱的第一條訊息來自其他使用者,則中止操作。 -[^xi]: 如果你不等待,而是在訊息入隊之後立即確認寫入,則會得到類似於多核x86處理器記憶體的一致性模型【43】。 該模型既不是線性一致的也不是順序一致的。 +[^xi]: 如果你不等待,而是在訊息入隊之後立即確認寫入,則會得到類似於多核 x86 處理器記憶體的一致性模型【43】。 該模型既不是線性一致的也不是順序一致的。 由於日誌項是以相同順序送達至所有節點,因此如果有多個併發寫入,則所有節點會對最先到達者達成一致。選擇衝突寫入中的第一個作為勝利者,並中止後來者,以此確定所有節點對某個寫入是提交還是中止達成一致。類似的方法可以在一個日誌的基礎上實現可序列化的多物件事務【62】。 -儘管這一過程保證寫入是線性一致的,但它並不保證讀取也是線性一致的 —— 如果你從與日誌非同步更新的儲存中讀取資料,結果可能是陳舊的。 (精確地說,這裡描述的過程提供了**順序一致性(sequential consistency)**【47,64】,有時也稱為**時間線一致性(timeline consistency)**【65,66】,比線性一致性稍微弱一些的保證)。為了使讀取也線性一致,有幾個選項: +儘管這一過程保證寫入是線性一致的,但它並不保證讀取也是線性一致的 —— 如果你從與日誌非同步更新的儲存中讀取資料,結果可能是陳舊的。 (精確地說,這裡描述的過程提供了 **順序一致性(sequential consistency)**【47,64】,有時也稱為 **時間線一致性(timeline consistency)**【65,66】,比線性一致性稍微弱一些的保證)。為了使讀取也線性一致,有幾個選項: -* 你可以透過在日誌中追加一條訊息,然後讀取日誌,直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點(etcd的法定人數讀取有些類似這種情況【16】)。 -* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。 (這是Zookeeper `sync()` 操作背後的思想【15】)。 -* 你可以從同步更新的副本中進行讀取,因此可以確保結果是最新的(這種技術用於鏈式複製(chain replication)【63】;請參閱“[關於複製的研究](ch5.md#關於複製的研究)”)。 +* 你可以透過在日誌中追加一條訊息,然後讀取日誌,直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點(etcd 的法定人數讀取有些類似這種情況【16】)。 +* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。 (這是 Zookeeper `sync()` 操作背後的思想【15】)。 +* 你可以從同步更新的副本中進行讀取,因此可以確保結果是最新的(這種技術用於鏈式複製(chain replication)【63】;請參閱 “[關於複製的研究](ch5.md#關於複製的研究)”)。 #### 使用線性一致性儲存實現全序廣播 -上一節介紹瞭如何從全序廣播構建一個線性一致的CAS操作。我們也可以把它反過來,假設我們有線性一致的儲存,接下來會展示如何在此基礎上構建全序廣播。 +上一節介紹瞭如何從全序廣播構建一個線性一致的 CAS 操作。我們也可以把它反過來,假設我們有線性一致的儲存,接下來會展示如何在此基礎上構建全序廣播。 -最簡單的方法是假設你有一個線性一致的暫存器來儲存一個整數,並且有一個原子**自增並返回**操作【28】。或者原子CAS操作也可以完成這項工作。 +最簡單的方法是假設你有一個線性一致的暫存器來儲存一個整數,並且有一個原子 **自增並返回** 操作【28】。或者原子 CAS 操作也可以完成這項工作。 -該演算法很簡單:每個要透過全序廣播發送的訊息首先對線性一致暫存器執行**自增並返回**操作。然後將從暫存器獲得的值作為序列號附加到訊息中。然後你可以將訊息傳送到所有節點(重新發送任何丟失的訊息),而收件人將按序列號依序傳遞(deliver)訊息。 +該演算法很簡單:每個要透過全序廣播發送的訊息首先對線性一致暫存器執行 **自增並返回** 操作。然後將從暫存器獲得的值作為序列號附加到訊息中。然後你可以將訊息傳送到所有節點(重新發送任何丟失的訊息),而收件人將按序列號依序傳遞(deliver)訊息。 請注意,與蘭伯特時間戳不同,透過自增線性一致性暫存器獲得的數字形式上是一個沒有間隙的序列。因此,如果一個節點已經發送了訊息 4 並且接收到序列號為 6 的傳入訊息,則它知道它在傳遞訊息 6 之前必須等待訊息 5 。蘭伯特時間戳則與之不同 —— 事實上,這是全序廣播和時間戳排序間的關鍵區別。 -實現一個帶有原子性**自增並返回**操作的線性一致暫存器有多困難?像往常一樣,如果事情從來不出差錯,那很容易:你可以簡單地把它儲存在單個節點內的變數中。問題在於處理當該節點的網路連線中斷時的情況,並在該節點失效時能恢復這個值【59】。一般來說,如果你對線性一致性的序列號生成器進行過足夠深入的思考,你不可避免地會得出一個共識演算法。 +實現一個帶有原子性 **自增並返回** 操作的線性一致暫存器有多困難?像往常一樣,如果事情從來不出差錯,那很容易:你可以簡單地把它儲存在單個節點內的變數中。問題在於處理當該節點的網路連線中斷時的情況,並在該節點失效時能恢復這個值【59】。一般來說,如果你對線性一致性的序列號生成器進行過足夠深入的思考,你不可避免地會得出一個共識演算法。 -這並非巧合:可以證明,線性一致的CAS(或自增並返回)暫存器與全序廣播都都等價於**共識**問題【28,67】。也就是說,如果你能解決其中的一個問題,你可以把它轉化成為其他問題的解決方案。這是相當深刻和令人驚訝的洞察! +這並非巧合:可以證明,線性一致的 CAS(或自增並返回)暫存器與全序廣播都都等價於 **共識** 問題【28,67】。也就是說,如果你能解決其中的一個問題,你可以把它轉化成為其他問題的解決方案。這是相當深刻和令人驚訝的洞察! 現在是時候正面處理共識問題了,我們將在本章的其餘部分進行討論。 ## 分散式事務與共識 -**共識**是分散式計算中最重要也是最基本的問題之一。從表面上看似乎很簡單:非正式地講,目標只是**讓幾個節點達成一致(get serveral nodes to agree on something)**。你也許會認為這不會太難。不幸的是,許多出故障的系統都是因為錯誤地輕信這個問題很容易解決。 +**共識** 是分散式計算中最重要也是最基本的問題之一。從表面上看似乎很簡單:非正式地講,目標只是 **讓幾個節點達成一致(get serveral nodes to agree on something)**。你也許會認為這不會太難。不幸的是,許多出故障的系統都是因為錯誤地輕信這個問題很容易解決。 儘管共識非常重要,但關於它的內容出現在本書的後半部分,因為這個主題非常微妙,欣賞細微之處需要一些必要的知識。即使在學術界,對共識的理解也是在幾十年的過程中逐漸沉澱而來,一路上也有著許多誤解。現在我們已經討論了複製([第五章](ch5.md)),事務([第七章](ch7.md)),系統模型([第八章](ch8.md)),線性一致以及全序廣播(本章),我們終於準備好解決共識問題了。 @@ -542,40 +542,40 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 * 領導選舉 - 在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。 + 在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。 * 原子提交 - 在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)** 問題[^xii]。 + 在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就 ACID 而言,請參閱 “[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止 / 回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為 **原子提交(atomic commit)** 問題 [^xii]。 - [^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 請參閱“[三階段提交](#三階段提交)”。 + [^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在 **所有** 參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就 **任意一個** 被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。 > ### 共識的不可能性 > -> 你可能已經聽說過以作者Fischer,Lynch和Paterson命名的FLP結果【68】,它證明,如果存在節點可能崩潰的風險,則不存在**總是**能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事? +> 你可能已經聽說過以作者 Fischer,Lynch 和 Paterson 命名的 FLP 結果【68】,它證明,如果存在節點可能崩潰的風險,則不存在 **總是** 能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事? > -> 答案是FLP結果是在**非同步系統模型**中被證明的(請參閱“[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用**超時**或其他方法來識別可疑的崩潰節點(即使懷疑有時是錯誤的),則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數,也足以繞過這個不可能的結果【69】。 +> 答案是 FLP 結果是在 **非同步系統模型** 中被證明的(請參閱 “[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用 **超時** 或其他方法來識別可疑的崩潰節點(即使懷疑有時是錯誤的),則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數,也足以繞過這個不可能的結果【69】。 > -> 因此,雖然FLP是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。 +> 因此,雖然 FLP 是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。 -在本節中,我們將首先更詳細地研究**原子提交**問題。具體來說,我們將討論**兩階段提交(2PC, two-phase commit)** 演算法,這是解決原子提交問題最常見的辦法,並在各種資料庫、訊息佇列和應用伺服器中被實現。事實證明2PC是一種共識演算法,但不是一個非常好的共識演算法【70,71】。 +在本節中,我們將首先更詳細地研究 **原子提交** 問題。具體來說,我們將討論 **兩階段提交(2PC, two-phase commit)** 演算法,這是解決原子提交問題最常見的辦法,並在各種資料庫、訊息佇列和應用伺服器中被實現。事實證明 2PC 是一種共識演算法,但不是一個非常好的共識演算法【70,71】。 -透過對2PC的學習,我們將繼續努力實現更好的一致性演算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的演算法。 +透過對 2PC 的學習,我們將繼續努力實現更好的一致性演算法,比如 ZooKeeper(Zab)和 etcd(Raft)中使用的演算法。 ### 原子提交與兩階段提交 -在[第七章](ch7.md)中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。 +在 [第七章](ch7.md) 中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。 -原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構—— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。 +原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構 —— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。 #### 從單節點到分散式原子提交 -對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。 +對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。 -因此,在單個節點上,事務的提交主要取決於資料持久化落盤的**順序**:首先是資料,然後是提交記錄【72】。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻:在此之前,仍有可能中止(由於崩潰),但在此之後,事務已經提交(即使資料庫崩潰)。因此,是單一的裝置(連線到單個磁碟的控制器,且掛載在單臺機器上)使得提交具有原子性。 +因此,在單個節點上,事務的提交主要取決於資料持久化落盤的 **順序**:首先是資料,然後是提交記錄【72】。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻:在此之前,仍有可能中止(由於崩潰),但在此之後,事務已經提交(即使資料庫崩潰)。因此,是單一的裝置(連線到單個磁碟的控制器,且掛載在單臺機器上)使得提交具有原子性。 -但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的次級索引(其中索引條目可能位於與主資料不同的節點上;請參閱“[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。大多數“NoSQL”分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(請參閱“[實踐中的分散式事務](#實踐中的分散式事務)”)。 +但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的次級索引(其中索引條目可能位於與主資料不同的節點上;請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。大多數 “NoSQL” 分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(請參閱 “[實踐中的分散式事務](#實踐中的分散式事務)”)。 在這些情況下,僅向所有節點發送提交請求並獨立提交每個節點的事務是不夠的。這樣很容易發生違反原子性的情況:提交在某些節點上成功,而在其他節點上失敗: @@ -583,123 +583,123 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一 * 某些提交請求可能在網路中丟失,最終由於超時而中止,而其他提交請求則透過。 * 在提交記錄完全寫入之前,某些節點可能會崩潰,並在恢復時回滾,而其他節點則成功提交。 -如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖7-3](../img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。 +如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖 7-3](../img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。 -事務提交必須是不可撤銷的 —— 事務提交之後,你不能改變主意,並追溯性地中止事務。這個規則的原因是,一旦資料被提交,其結果就對其他事務可見,因此其他客戶端可能會開始依賴這些資料。這個原則構成了**讀已提交**隔離等級的基礎,在“[讀已提交](ch7.md#讀已提交)”一節中討論了這個問題。如果一個事務在提交後被允許中止,所有那些讀取了**已提交卻又被追溯宣告不存在資料**的事務也必須回滾。 +事務提交必須是不可撤銷的 —— 事務提交之後,你不能改變主意,並追溯性地中止事務。這個規則的原因是,一旦資料被提交,其結果就對其他事務可見,因此其他客戶端可能會開始依賴這些資料。這個原則構成了 **讀已提交** 隔離等級的基礎,在 “[讀已提交](ch7.md#讀已提交)” 一節中討論了這個問題。如果一個事務在提交後被允許中止,所有那些讀取了 **已提交卻又被追溯宣告不存在資料** 的事務也必須回滾。 (提交事務的結果有可能透過事後執行另一個補償事務(compensating transaction)來取消【73,74】,但從資料庫的角度來看,這是一個單獨的事務,因此任何關於跨事務正確性的保證都是應用自己的問題。) #### 兩階段提交簡介 -**兩階段提交(two-phase commit)** 是一種用於實現跨多個節點的原子事務提交的演算法,即確保所有節點提交或所有節點中止。 它是分散式資料庫中的經典演算法【13,35,75】。 2PC在某些資料庫內部使用,也以**XA事務**的形式對應用可用【76,77】(例如Java Transaction API支援)或以SOAP Web服務的`WS-AtomicTransaction` 形式提供給應用【78,79】。 +**兩階段提交(two-phase commit)** 是一種用於實現跨多個節點的原子事務提交的演算法,即確保所有節點提交或所有節點中止。 它是分散式資料庫中的經典演算法【13,35,75】。 2PC 在某些資料庫內部使用,也以 **XA 事務** 的形式對應用可用【76,77】(例如 Java Transaction API 支援)或以 SOAP Web 服務的 `WS-AtomicTransaction` 形式提供給應用【78,79】。 -[圖9-9](../img/fig9-9.png)說明了2PC的基本流程。2PC中的提交/中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。 +[圖 9-9](../img/fig9-9.png) 說明了 2PC 的基本流程。2PC 中的提交 / 中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。 ![](../img/fig9-9.png) -**圖9-9 兩階段提交(2PC)的成功執行** +**圖 9-9 兩階段提交(2PC)的成功執行** > #### 不要把2PC和2PL搞混了 > -> 兩階段提交(2PC)和兩階段鎖定(請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”)是兩個完全不同的東西。 2PC在分散式資料庫中提供原子提交,而2PL提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。 +> 兩階段提交(2PC)和兩階段鎖定(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”)是兩個完全不同的東西。 2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。 -2PC使用一個通常不會出現在單節點事務中的新元件:**協調者**(coordinator,也稱為**事務管理器**,即transaction manager)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在Java EE容器中),但也可以是單獨的程序或服務。這種協調者的例子包括Narayana、JOTM、BTM或MSDTC。 +2PC 使用一個通常不會出現在單節點事務中的新元件:**協調者**(coordinator,也稱為 **事務管理器**,即 transaction manager)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在 Java EE 容器中),但也可以是單獨的程序或服務。這種協調者的例子包括 Narayana、JOTM、BTM 或 MSDTC。 -正常情況下,2PC事務以應用在多個數據庫節點上讀寫資料開始。我們稱這些資料庫節點為**參與者(participants)**。當應用準備提交時,協調者開始階段 1 :它傳送一個**準備(prepare)** 請求到每個節點,詢問它們是否能夠提交。然後協調者會跟蹤參與者的響應: +正常情況下,2PC 事務以應用在多個數據庫節點上讀寫資料開始。我們稱這些資料庫節點為 **參與者(participants)**。當應用準備提交時,協調者開始階段 1 :它傳送一個 **準備(prepare)** 請求到每個節點,詢問它們是否能夠提交。然後協調者會跟蹤參與者的響應: -* 如果所有參與者都回答“是”,表示它們已經準備好提交,那麼協調者在階段 2 發出**提交(commit)** 請求,然後提交真正發生。 -* 如果任意一個參與者回覆了“否”,則協調者在階段2 中向所有節點發送**中止(abort)** 請求。 +* 如果所有參與者都回答 “是”,表示它們已經準備好提交,那麼協調者在階段 2 發出 **提交(commit)** 請求,然後提交真正發生。 +* 如果任意一個參與者回覆了 “否”,則協調者在階段 2 中向所有節點發送 **中止(abort)** 請求。 -這個過程有點像西方傳統婚姻儀式:司儀分別詢問新娘和新郎是否要結婚,通常是從兩方都收到“我願意”的答覆。收到兩者的回覆後,司儀宣佈這對情侶成為夫妻:事務就提交了,這一幸福事實會廣播至所有的參與者中。如果新娘與新郎之一沒有回覆“我願意”,婚禮就會中止【73】。 +這個過程有點像西方傳統婚姻儀式:司儀分別詢問新娘和新郎是否要結婚,通常是從兩方都收到 “我願意” 的答覆。收到兩者的回覆後,司儀宣佈這對情侶成為夫妻:事務就提交了,這一幸福事實會廣播至所有的參與者中。如果新娘與新郎之一沒有回覆 “我願意”,婚禮就會中止【73】。 #### 系統承諾 -這個簡短的描述可能並沒有說清楚為什麼兩階段提交保證了原子性,而跨多個節點的一階段提交卻沒有。在兩階段提交的情況下,準備請求和提交請求當然也可以輕易丟失。 2PC又有什麼不同呢? +這個簡短的描述可能並沒有說清楚為什麼兩階段提交保證了原子性,而跨多個節點的一階段提交卻沒有。在兩階段提交的情況下,準備請求和提交請求當然也可以輕易丟失。 2PC 又有什麼不同呢? 為了理解它的工作原理,我們必須更詳細地分解這個過程: -1. 當應用想要啟動一個分散式事務時,它向協調者請求一個事務ID。此事務ID是全域性唯一的。 -2. 應用在每個參與者上啟動單節點事務,並在單節點事務上捎帶上這個全域性事務ID。所有的讀寫都是在這些單節點事務中各自完成的。如果在這個階段出現任何問題(例如,節點崩潰或請求超時),則協調者或任何參與者都可以中止。 -3. 當應用準備提交時,協調者向所有參與者傳送一個**準備**請求,並打上全域性事務ID的標記。如果任意一個請求失敗或超時,則協調者向所有參與者傳送針對該事務ID的中止請求。 -4. 參與者收到準備請求時,需要確保在任意情況下都的確可以提交事務。這包括將所有事務資料寫入磁碟(出現故障,電源故障,或硬碟空間不足都不能是稍後拒絕提交的理由)以及檢查是否存在任何衝突或違反約束。透過向協調者回答“是”,節點承諾,只要請求,這個事務一定可以不出差錯地提交。換句話說,參與者放棄了中止事務的權利,但沒有實際提交。 -5. 當協調者收到所有準備請求的答覆時,會就提交或中止事務作出明確的決定(只有在所有參與者投贊成票的情況下才會提交)。協調者必須把這個決定寫到磁碟上的事務日誌中,如果它隨後就崩潰,恢復後也能知道自己所做的決定。這被稱為**提交點(commit point)**。 -6. 一旦協調者的決定落盤,提交或放棄請求會發送給所有參與者。如果這個請求失敗或超時,協調者必須永遠保持重試,直到成功為止。沒有回頭路:如果已經做出決定,不管需要多少次重試它都必須被執行。如果參與者在此期間崩潰,事務將在其恢復後提交——由於參與者投了贊成,因此恢復後它不能拒絕提交。 +1. 當應用想要啟動一個分散式事務時,它向協調者請求一個事務 ID。此事務 ID 是全域性唯一的。 +2. 應用在每個參與者上啟動單節點事務,並在單節點事務上捎帶上這個全域性事務 ID。所有的讀寫都是在這些單節點事務中各自完成的。如果在這個階段出現任何問題(例如,節點崩潰或請求超時),則協調者或任何參與者都可以中止。 +3. 當應用準備提交時,協調者向所有參與者傳送一個 **準備** 請求,並打上全域性事務 ID 的標記。如果任意一個請求失敗或超時,則協調者向所有參與者傳送針對該事務 ID 的中止請求。 +4. 參與者收到準備請求時,需要確保在任意情況下都的確可以提交事務。這包括將所有事務資料寫入磁碟(出現故障,電源故障,或硬碟空間不足都不能是稍後拒絕提交的理由)以及檢查是否存在任何衝突或違反約束。透過向協調者回答 “是”,節點承諾,只要請求,這個事務一定可以不出差錯地提交。換句話說,參與者放棄了中止事務的權利,但沒有實際提交。 +5. 當協調者收到所有準備請求的答覆時,會就提交或中止事務作出明確的決定(只有在所有參與者投贊成票的情況下才會提交)。協調者必須把這個決定寫到磁碟上的事務日誌中,如果它隨後就崩潰,恢復後也能知道自己所做的決定。這被稱為 **提交點(commit point)**。 +6. 一旦協調者的決定落盤,提交或放棄請求會發送給所有參與者。如果這個請求失敗或超時,協調者必須永遠保持重試,直到成功為止。沒有回頭路:如果已經做出決定,不管需要多少次重試它都必須被執行。如果參與者在此期間崩潰,事務將在其恢復後提交 —— 由於參與者投了贊成,因此恢復後它不能拒絕提交。 -因此,該協議包含兩個關鍵的“不歸路”點:當參與者投票“是”時,它承諾它稍後肯定能夠提交(儘管協調者可能仍然選擇放棄);以及一旦協調者做出決定,這一決定是不可撤銷的。這些承諾保證了2PC的原子性(單節點原子提交將這兩個事件合為了一體:將提交記錄寫入事務日誌)。 +因此,該協議包含兩個關鍵的 “不歸路” 點:當參與者投票 “是” 時,它承諾它稍後肯定能夠提交(儘管協調者可能仍然選擇放棄);以及一旦協調者做出決定,這一決定是不可撤銷的。這些承諾保證了 2PC 的原子性(單節點原子提交將這兩個事件合為了一體:將提交記錄寫入事務日誌)。 -回到婚姻的比喻,在說“我願意”之前,你和你的新娘/新郎有中止這個事務的自由,只要回覆 “沒門!”就行(或者有類似效果的話)。然而在說了“我願意”之後,你就不能撤回那個聲明瞭。如果你說“我願意”後暈倒了,沒有聽到司儀說“你們現在是夫妻了”,那也並不會改變事務已經提交的現實。當你稍後恢復意識時,可以透過查詢司儀的全域性事務ID狀態來確定你是否已經成婚,或者你可以等待司儀重試下一次提交請求(因為重試將在你無意識期間一直持續)。 +回到婚姻的比喻,在說 “我願意” 之前,你和你的新娘 / 新郎有中止這個事務的自由,只要回覆 “沒門!” 就行(或者有類似效果的話)。然而在說了 “我願意” 之後,你就不能撤回那個聲明瞭。如果你說 “我願意” 後暈倒了,沒有聽到司儀說 “你們現在是夫妻了”,那也並不會改變事務已經提交的現實。當你稍後恢復意識時,可以透過查詢司儀的全域性事務 ID 狀態來確定你是否已經成婚,或者你可以等待司儀重試下一次提交請求(因為重試將在你無意識期間一直持續)。 #### 協調者失效 -我們已經討論了在2PC期間,如果參與者之一或網路發生故障時會發生什麼情況:如果任何一個**準備**請求失敗或者超時,協調者就會中止事務。如果任何提交或中止請求失敗,協調者將無條件重試。但是如果協調者崩潰,會發生什麼情況就不太清楚了。 +我們已經討論了在 2PC 期間,如果參與者之一或網路發生故障時會發生什麼情況:如果任何一個 **準備** 請求失敗或者超時,協調者就會中止事務。如果任何提交或中止請求失敗,協調者將無條件重試。但是如果協調者崩潰,會發生什麼情況就不太清楚了。 -如果協調者在傳送**準備**請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了“是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為**存疑(in doubt)** 的或**不確定(uncertain)** 的。 +如果協調者在傳送 **準備** 請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了 “是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為 **存疑(in doubt)** 的或 **不確定(uncertain)** 的。 -情況如[圖9-10](../img/fig9-10.png) 所示。在這個特定的例子中,協調者實際上決定提交,資料庫2 收到提交請求。但是,協調者在將提交請求傳送到資料庫1 之前發生崩潰,因此資料庫1 不知道是否提交或中止。即使**超時**在這裡也沒有幫助:如果資料庫1 在超時後單方面中止,它將最終與執行提交的資料庫2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。 +情況如 [圖 9-10](../img/fig9-10.png) 所示。在這個特定的例子中,協調者實際上決定提交,資料庫 2 收到提交請求。但是,協調者在將提交請求傳送到資料庫 1 之前發生崩潰,因此資料庫 1 不知道是否提交或中止。即使 **超時** 在這裡也沒有幫助:如果資料庫 1 在超時後單方面中止,它將最終與執行提交的資料庫 2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。 ![](../img/fig9-10.png) -**圖9-10 參與者投贊成票後,協調者崩潰。資料庫1不知道是否提交或中止** +**圖 9-10 參與者投贊成票後,協調者崩潰。資料庫 1 不知道是否提交或中止** -沒有協調者的訊息,參與者無法知道是提交還是放棄。原則上參與者可以相互溝通,找出每個參與者是如何投票的,並達成一致,但這不是2PC協議的一部分。 +沒有協調者的訊息,參與者無法知道是提交還是放棄。原則上參與者可以相互溝通,找出每個參與者是如何投票的,並達成一致,但這不是 2PC 協議的一部分。 -可以完成2PC的唯一方法是等待協調者恢復。這就是為什麼協調者必須在向參與者傳送提交或中止請求之前,將其提交或中止決定寫入磁碟上的事務日誌:協調者恢復後,透過讀取其事務日誌來確定所有存疑事務的狀態。任何在協調者日誌中沒有提交記錄的事務都會中止。因此,2PC的**提交點**歸結為協調者上的常規單節點原子提交。 +可以完成 2PC 的唯一方法是等待協調者恢復。這就是為什麼協調者必須在向參與者傳送提交或中止請求之前,將其提交或中止決定寫入磁碟上的事務日誌:協調者恢復後,透過讀取其事務日誌來確定所有存疑事務的狀態。任何在協調者日誌中沒有提交記錄的事務都會中止。因此,2PC 的 **提交點** 歸結為協調者上的常規單節點原子提交。 #### 三階段提交 -兩階段提交被稱為**阻塞(blocking)**- 原子提交協議,因為存在2PC可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為**非阻塞(nonblocking)** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。 +兩階段提交被稱為 **阻塞(blocking)**- 原子提交協議,因為存在 2PC 可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為 **非阻塞(nonblocking)** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。 -作為2PC的替代方案,已經提出了一種稱為**三階段提交(3PC)** 的演算法【13,80】。然而,3PC假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見[第八章](ch8.md)),它並不能保證原子性。 +作為 2PC 的替代方案,已經提出了一種稱為 **三階段提交(3PC)** 的演算法【13,80】。然而,3PC 假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見 [第八章](ch8.md)),它並不能保證原子性。 -通常,非阻塞原子提交需要一個**完美的故障檢測器(perfect failure detector)**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中,超時並不是一種可靠的故障檢測機制,因為即使沒有節點崩潰,請求也可能由於網路問題而超時。出於這個原因,2PC仍然被使用,儘管大家都清楚可能存在協調者故障的問題。 +通常,非阻塞原子提交需要一個 **完美的故障檢測器(perfect failure detector)**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中,超時並不是一種可靠的故障檢測機制,因為即使沒有節點崩潰,請求也可能由於網路問題而超時。出於這個原因,2PC 仍然被使用,儘管大家都清楚可能存在協調者故障的問題。 ### 實踐中的分散式事務 分散式事務的名聲譭譽參半,尤其是那些透過兩階段提交實現的。一方面,它被視作提供了一個難以實現的重要的安全性保證;另一方面,它們因為導致運維問題,造成效能下降,做出超過能力範圍的承諾而飽受批評【81,82,83,84】。許多雲服務由於其導致的運維問題,而選擇不實現分散式事務【85,86】。 -分散式事務的某些實現會帶來嚴重的效能損失 —— 例如據報告稱,MySQL中的分散式事務比單節點事務慢10倍以上【87】,所以當人們建議不要使用它們時就不足為奇了。兩階段提交所固有的效能成本,大部分是由於崩潰恢復所需的額外強制刷盤(`fsync`)【88】以及額外的網路往返。 +分散式事務的某些實現會帶來嚴重的效能損失 —— 例如據報告稱,MySQL 中的分散式事務比單節點事務慢 10 倍以上【87】,所以當人們建議不要使用它們時就不足為奇了。兩階段提交所固有的效能成本,大部分是由於崩潰恢復所需的額外強制刷盤(`fsync`)【88】以及額外的網路往返。 -但我們不應該直接忽視分散式事務,而應當更加仔細地審視這些事務,因為從中可以汲取重要的經驗教訓。首先,我們應該精確地說明“**分散式事務**”的含義。兩種截然不同的分散式事務型別經常被混淆: +但我們不應該直接忽視分散式事務,而應當更加仔細地審視這些事務,因為從中可以汲取重要的經驗教訓。首先,我們應該精確地說明 “**分散式事務**” 的含義。兩種截然不同的分散式事務型別經常被混淆: * 資料庫內部的分散式事務 - 一些分散式資料庫(即在其標準配置中使用複製和分割槽的資料庫)支援資料庫節點之間的內部事務。例如,VoltDB和MySQL Cluster的NDB儲存引擎就有這樣的內部事務支援。在這種情況下,所有參與事務的節點都執行相同的資料庫軟體。 + 一些分散式資料庫(即在其標準配置中使用複製和分割槽的資料庫)支援資料庫節點之間的內部事務。例如,VoltDB 和 MySQL Cluster 的 NDB 儲存引擎就有這樣的內部事務支援。在這種情況下,所有參與事務的節點都執行相同的資料庫軟體。 * 異構分散式事務 - 在**異構(heterogeneous)** 事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。 + 在 **異構(heterogeneous)** 事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。 資料庫內部事務不必與任何其他系統相容,因此它們可以使用任何協議,並能針對特定技術進行特定的最佳化。因此資料庫內部的分散式事務通常工作地很好。另一方面,跨異構技術的事務則更有挑戰性。 #### 恰好一次的訊息處理 -異構的分散式事務處理能夠以強大的方式整合不同的系統。例如:訊息佇列中的一條訊息可以被確認為已處理,當且僅當用於處理訊息的資料庫事務成功提交。這是透過在同一個事務中原子提交**訊息確認**和**資料庫寫入**兩個操作來實現的。藉由分散式事務的支援,即使訊息代理和資料庫是在不同機器上執行的兩種不相關的技術,這種操作也是可能的。 +異構的分散式事務處理能夠以強大的方式整合不同的系統。例如:訊息佇列中的一條訊息可以被確認為已處理,當且僅當用於處理訊息的資料庫事務成功提交。這是透過在同一個事務中原子提交 **訊息確認** 和 **資料庫寫入** 兩個操作來實現的。藉由分散式事務的支援,即使訊息代理和資料庫是在不同機器上執行的兩種不相關的技術,這種操作也是可能的。 -如果訊息傳遞或資料庫事務任意一者失敗,兩者都會中止,因此訊息代理可能會在稍後安全地重傳訊息。因此,透過原子提交**訊息處理及其副作用**,即使在成功之前需要幾次重試,也可以確保訊息被**有效地(effectively)** 恰好處理一次。中止會拋棄部分完成事務所導致的任何副作用。 +如果訊息傳遞或資料庫事務任意一者失敗,兩者都會中止,因此訊息代理可能會在稍後安全地重傳訊息。因此,透過原子提交 **訊息處理及其副作用**,即使在成功之前需要幾次重試,也可以確保訊息被 **有效地(effectively)** 恰好處理一次。中止會拋棄部分完成事務所導致的任何副作用。 -然而,只有當所有受事務影響的系統都使用同樣的**原子提交協議(atomic commit protocol)** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。 +然而,只有當所有受事務影響的系統都使用同樣的 **原子提交協議(atomic commit protocol)** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。 -在[第十一章](ch11.md)中將再次回到“恰好一次”訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。 +在 [第十一章](ch11.md) 中將再次回到 “恰好一次” 訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。 #### XA事務 -*X/Open XA*(**擴充套件架構(eXtended Architecture)** 的縮寫)是跨異構技術實現兩階段提交的標準【76,77】。它於1991年推出並得到了廣泛的實現:許多傳統關係資料庫(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和訊息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 都支援XA。 +*X/Open XA*(**擴充套件架構(eXtended Architecture)** 的縮寫)是跨異構技術實現兩階段提交的標準【76,77】。它於 1991 年推出並得到了廣泛的實現:許多傳統關係資料庫(包括 PostgreSQL、MySQL、DB2、SQL Server 和 Oracle)和訊息代理(包括 ActiveMQ、HornetQ、MSMQ 和 IBM MQ) 都支援 XA。 -XA不是一個網路協議——它只是一個用來與事務協調者連線的C API。其他語言也有這種API的繫結;例如在Java EE應用的世界中,XA事務是使用**Java事務API(JTA, Java Transaction API)** 實現的,而許多使用**Java資料庫連線(JDBC, Java Database Connectivity)** 的資料庫驅動,以及許多使用**Java訊息服務(JMS)** API的訊息代理都支援**Java事務API(JTA)**。 +XA 不是一個網路協議 —— 它只是一個用來與事務協調者連線的 C API。其他語言也有這種 API 的繫結;例如在 Java EE 應用的世界中,XA 事務是使用 **Java 事務 API(JTA, Java Transaction API)** 實現的,而許多使用 **Java 資料庫連線(JDBC, Java Database Connectivity)** 的資料庫驅動,以及許多使用 **Java 訊息服務(JMS)** API 的訊息代理都支援 **Java 事務 API(JTA)**。 -XA假定你的應用使用網路驅動或客戶端庫來與**參與者**(資料庫或訊息服務)進行通訊。如果驅動支援XA,則意味著它會呼叫XA API 以查明操作是否為分散式事務的一部分 —— 如果是,則將必要的資訊發往資料庫伺服器。驅動還會向協調者暴露回撥介面,協調者可以透過回撥來要求參與者準備、提交或中止。 +XA 假定你的應用使用網路驅動或客戶端庫來與 **參與者**(資料庫或訊息服務)進行通訊。如果驅動支援 XA,則意味著它會呼叫 XA API 以查明操作是否為分散式事務的一部分 —— 如果是,則將必要的資訊發往資料庫伺服器。驅動還會向協調者暴露回撥介面,協調者可以透過回撥來要求參與者準備、提交或中止。 -事務協調者需要實現XA API。標準沒有指明應該如何實現,但實際上協調者通常只是一個庫,被載入到發起事務的應用的同一個程序中(而不是單獨的服務)。它在事務中跟蹤所有的參與者,並在要求它們**準備**之後收集參與者的響應(透過驅動回撥),並使用本地磁碟上的日誌記錄每次事務的決定(提交/中止)。 +事務協調者需要實現 XA API。標準沒有指明應該如何實現,但實際上協調者通常只是一個庫,被載入到發起事務的應用的同一個程序中(而不是單獨的服務)。它在事務中跟蹤所有的參與者,並在要求它們 **準備** 之後收集參與者的響應(透過驅動回撥),並使用本地磁碟上的日誌記錄每次事務的決定(提交 / 中止)。 -如果應用程序崩潰,或者執行應用的機器報銷了,協調者也隨之往生極樂。然後任何帶有**準備了**但未提交事務的參與者都會在疑慮中卡死。由於協調程式的日誌位於應用伺服器的本地磁碟上,因此必須重啟該伺服器,且協調程式庫必須讀取日誌以恢復每個事務的提交/中止結果。只有這樣,協調者才能使用資料庫驅動的XA回撥來要求參與者提交或中止。資料庫伺服器不能直接聯絡協調者,因為所有通訊都必須透過客戶端庫。 +如果應用程序崩潰,或者執行應用的機器報銷了,協調者也隨之往生極樂。然後任何帶有 **準備了** 但未提交事務的參與者都會在疑慮中卡死。由於協調程式的日誌位於應用伺服器的本地磁碟上,因此必須重啟該伺服器,且協調程式庫必須讀取日誌以恢復每個事務的提交 / 中止結果。只有這樣,協調者才能使用資料庫驅動的 XA 回撥來要求參與者提交或中止。資料庫伺服器不能直接聯絡協調者,因為所有通訊都必須透過客戶端庫。 #### 懷疑時持有鎖 為什麼我們這麼關心存疑事務?系統的其他部分就不能繼續正常工作,無視那些終將被清理的存疑事務嗎? -問題在於**鎖(locking)**。正如在“[讀已提交](ch7.md#讀已提交)”中所討論的那樣,資料庫事務通常獲取待修改的行上的**行級排他鎖**,以防止髒寫。此外,如果要使用可序列化的隔離等級,則使用兩階段鎖定的資料庫也必須為事務所讀取的行加上共享鎖(請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”)。 +問題在於 **鎖(locking)**。正如在 “[讀已提交](ch7.md#讀已提交)” 中所討論的那樣,資料庫事務通常獲取待修改的行上的 **行級排他鎖**,以防止髒寫。此外,如果要使用可序列化的隔離等級,則使用兩階段鎖定的資料庫也必須為事務所讀取的行加上共享鎖(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”)。 -在事務提交或中止之前,資料庫不能釋放這些鎖(如[圖9-9](../img/fig9-9.png)中的陰影區域所示)。因此,在使用兩階段提交時,事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰,需要20分鐘才能重啟,那麼這些鎖將會被持有20分鐘。如果協調者的日誌由於某種原因徹底丟失,這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。 +在事務提交或中止之前,資料庫不能釋放這些鎖(如 [圖 9-9](../img/fig9-9.png) 中的陰影區域所示)。因此,在使用兩階段提交時,事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰,需要 20 分鐘才能重啟,那麼這些鎖將會被持有 20 分鐘。如果協調者的日誌由於某種原因徹底丟失,這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。 當這些鎖被持有時,其他事務不能修改這些行。根據資料庫的不同,其他事務甚至可能因為讀取這些行而被阻塞。因此,其他事務沒法兒簡單地繼續它們的業務了 —— 如果它們要訪問同樣的資料,就會被阻塞。這可能會導致應用大面積進入不可用狀態,直到存疑事務被解決。 @@ -707,33 +707,33 @@ XA假定你的應用使用網路驅動或客戶端庫來與**參與者**(資 理論上,如果協調者崩潰並重新啟動,它應該乾淨地從日誌中恢復其狀態,並解決任何存疑事務。然而在實踐中,**孤立(orphaned)** 的存疑事務確實會出現【89,90】,即無論出於何種理由,協調者無法確定事務的結果(例如事務日誌已經由於軟體錯誤丟失或損壞)。這些事務無法自動解決,所以它們永遠待在資料庫中,持有鎖並阻塞其他事務。 -即使重啟資料庫伺服器也無法解決這個問題,因為在2PC的正確實現中,即使重啟也必須保留存疑事務的鎖(否則就會冒違反原子性保證的風險)。這是一種棘手的情況。 +即使重啟資料庫伺服器也無法解決這個問題,因為在 2PC 的正確實現中,即使重啟也必須保留存疑事務的鎖(否則就會冒違反原子性保證的風險)。這是一種棘手的情況。 唯一的出路是讓管理員手動決定提交還是回滾事務。管理員必須檢查每個存疑事務的參與者,確定是否有任何參與者已經提交或中止,然後將相同的結果應用於其他參與者。解決這個問題潛在地需要大量的人力,並且可能發生在嚴重的生產中斷期間(不然為什麼協調者處於這種糟糕的狀態),並很可能要在巨大精神壓力和時間壓力下完成。 -許多XA的實現都有一個叫做**啟發式決策(heuristic decisions)** 的緊急逃生艙口:允許參與者單方面決定放棄或提交一個存疑事務,而無需協調者做出最終決定【76,77,91】。要清楚的是,這裡**啟發式**是**可能破壞原子性(probably breaking atomicity)** 的委婉說法,因為它違背了兩階段提交的系統承諾。因此,啟發式決策只是為了逃出災難性的情況而準備的,而不是為了日常使用的。 +許多 XA 的實現都有一個叫做 **啟發式決策(heuristic decisions)** 的緊急逃生艙口:允許參與者單方面決定放棄或提交一個存疑事務,而無需協調者做出最終決定【76,77,91】。要清楚的是,這裡 **啟發式** 是 **可能破壞原子性(probably breaking atomicity)** 的委婉說法,因為它違背了兩階段提交的系統承諾。因此,啟發式決策只是為了逃出災難性的情況而準備的,而不是為了日常使用的。 #### 分散式事務的限制 -XA事務解決了保持多個參與者(資料系統)相互一致的現實的和重要的問題,但正如我們所看到的那樣,它也引入了嚴重的運維問題。特別來講,這裡的核心認識是:事務協調者本身就是一種資料庫(儲存了事務的結果),因此需要像其他重要資料庫一樣小心地打交道: +XA 事務解決了保持多個參與者(資料系統)相互一致的現實的和重要的問題,但正如我們所看到的那樣,它也引入了嚴重的運維問題。特別來講,這裡的核心認識是:事務協調者本身就是一種資料庫(儲存了事務的結果),因此需要像其他重要資料庫一樣小心地打交道: * 如果協調者沒有複製,而是隻在單臺機器上執行,那麼它是整個系統的失效單點(因為它的失效會導致其他應用伺服器阻塞在存疑事務持有的鎖上)。令人驚訝的是,許多協調者實現預設情況下並不是高可用的,或者只有基本的複製支援。 -* 許多伺服器端應用都是使用無狀態模式開發的(受HTTP的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分—— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。 -* 由於XA需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與SSI(請參閱[可序列化快照隔離](ch7.md#可序列化快照隔離 ))協同工作,因為這需要一個跨系統定位衝突的協議。 -* 對於資料庫內部的分散式事務(不是XA),限制沒有這麼大 —— 例如,分散式版本的SSI是可能的。然而仍然存在問題:2PC成功提交一個事務需要所有參與者的響應。因此,如果系統的**任何**部分損壞,事務也會失敗。因此,分散式事務又有**擴大失效(amplifying failures)** 的趨勢,這又與我們構建容錯系統的目標背道而馳。 +* 許多伺服器端應用都是使用無狀態模式開發的(受 HTTP 的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分 —— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。 +* 由於 XA 需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與 SSI(請參閱 [可序列化快照隔離](ch7.md#可序列化快照隔離))協同工作,因為這需要一個跨系統定位衝突的協議。 +* 對於資料庫內部的分散式事務(不是 XA),限制沒有這麼大 —— 例如,分散式版本的 SSI 是可能的。然而仍然存在問題:2PC 成功提交一個事務需要所有參與者的響應。因此,如果系統的 **任何** 部分損壞,事務也會失敗。因此,分散式事務又有 **擴大失效(amplifying failures)** 的趨勢,這又與我們構建容錯系統的目標背道而馳。 -這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在[第十一章](ch11.md) 和[第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於**共識**的話題。 +這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在 [第十一章](ch11.md) 和 [第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於 **共識** 的話題。 ### 容錯共識 -非正式地,共識意味著讓幾個節點就某事達成一致。例如,如果有幾個人**同時(concurrently)** 嘗試預訂飛機上的最後一個座位,或劇院中的同一個座位,或者嘗試使用相同的使用者名稱註冊一個帳戶。共識演算法可以用來確定這些**互不相容(mutually incompatible)** 的操作中,哪一個才是贏家。 +非正式地,共識意味著讓幾個節點就某事達成一致。例如,如果有幾個人 **同時(concurrently)** 嘗試預訂飛機上的最後一個座位,或劇院中的同一個座位,或者嘗試使用相同的使用者名稱註冊一個帳戶。共識演算法可以用來確定這些 **互不相容(mutually incompatible)** 的操作中,哪一個才是贏家。 -共識問題通常形式化如下:一個或多個節點可以**提議(propose)** 某些值,而共識演算法**決定(decides)** 採用其中的某個值。在座位預訂的例子中,當幾個顧客同時試圖訂購最後一個座位時,處理顧客請求的每個節點可以**提議**將要服務的顧客的ID,而**決定**指明瞭哪個顧客獲得了座位。 +共識問題通常形式化如下:一個或多個節點可以 **提議(propose)** 某些值,而共識演算法 **決定(decides)** 採用其中的某個值。在座位預訂的例子中,當幾個顧客同時試圖訂購最後一個座位時,處理顧客請求的每個節點可以 **提議** 將要服務的顧客的 ID,而 **決定** 指明瞭哪個顧客獲得了座位。 在這種形式下,共識演算法必須滿足以下性質【25】:[^xiii] -[^xiii]: 這種共識的特殊形式被稱為**統一共識(uniform consensus)**,相當於在具有不可靠故障檢測器的非同步系統中的**常規共識(regular consensus)**【71】。學術文獻通常指的是**程序(process)** 而不是節點,但我們在這裡使用**節點(node)** 來與本書的其餘部分保持一致。 +[^xiii]: 這種共識的特殊形式被稱為 **統一共識(uniform consensus)**,相當於在具有不可靠故障檢測器的非同步系統中的 **常規共識(regular consensus)**【71】。學術文獻通常指的是 **程序(process)** 而不是節點,但我們在這裡使用 **節點(node)** 來與本書的其餘部分保持一致。 * 一致同意(Uniform agreement) @@ -751,44 +751,44 @@ XA事務解決了保持多個參與者(資料系統)相互一致的現實的 由所有未崩潰的節點來最終決定值。 -**一致同意**和**完整性**屬性定義了共識的核心思想:所有人都決定了相同的結果,一旦決定了,你就不能改變主意。**有效性**屬性主要是為了排除平凡的解決方案:例如,無論提議了什麼值,你都可以有一個始終決定值為`null`的演算法。;該演算法滿足**一致同意**和**完整性**屬性,但不滿足**有效性**屬性。 +**一致同意** 和 **完整性** 屬性定義了共識的核心思想:所有人都決定了相同的結果,一旦決定了,你就不能改變主意。**有效性** 屬性主要是為了排除平凡的解決方案:例如,無論提議了什麼值,你都可以有一個始終決定值為 `null` 的演算法。;該演算法滿足 **一致同意** 和 **完整性** 屬性,但不滿足 **有效性** 屬性。 -如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為“獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。 +如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為 “獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。 -**終止**屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止**是一種**活性屬性**,而另外三種是**安全屬性** —— 請參閱“[安全性和活性](ch8.md#安全性和活性)”)。 +**終止** 屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止** 是一種 **活性屬性**,而另外三種是 **安全屬性** —— 請參閱 “[安全性和活性](ch8.md#安全性和活性)”)。 -共識的系統模型假設,當一個節點“崩潰”時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在30英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足**終止**屬性。特別是,2PC不符合終止屬性的要求。 +共識的系統模型假設,當一個節點 “崩潰” 時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在 30 英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足 **終止** 屬性。特別是,2PC 不符合終止屬性的要求。 -當然如果**所有**的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體**多數(majority)** 的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。 +當然如果 **所有** 的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體 **多數(majority)** 的節點正確工作,以確保終止屬性【67】。多數可以安全地組成法定人數(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。 -因此**終止**屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足—— 一致同意,完整性和有效性【92】。因此,大規模的中斷可能會阻止系統處理請求,但是它不能透過使系統做出無效的決定來破壞共識系統。 +因此 **終止** 屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足 —— 一致同意,完整性和有效性【92】。因此,大規模的中斷可能會阻止系統處理請求,但是它不能透過使系統做出無效的決定來破壞共識系統。 -大多數共識演算法假設不存在**拜占庭式錯誤**,正如在“[拜占庭故障](ch8.md#拜占庭故障)”一節中所討論的那樣。也就是說,如果一個節點沒有正確地遵循協議(例如,如果它向不同節點發送矛盾的訊息),它就可能會破壞協議的安全屬性。克服拜占庭故障,穩健地達成共識是可能的,只要少於三分之一的節點存在拜占庭故障【25,93】。但我們沒有地方在本書中詳細討論這些演算法了。 +大多數共識演算法假設不存在 **拜占庭式錯誤**,正如在 “[拜占庭故障](ch8.md#拜占庭故障)” 一節中所討論的那樣。也就是說,如果一個節點沒有正確地遵循協議(例如,如果它向不同節點發送矛盾的訊息),它就可能會破壞協議的安全屬性。克服拜占庭故障,穩健地達成共識是可能的,只要少於三分之一的節點存在拜占庭故障【25,93】。但我們沒有地方在本書中詳細討論這些演算法了。 #### 共識演算法和全序廣播 -最著名的容錯共識演算法是**檢視戳複製(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。這些演算法之間有不少相似之處,但它們並不相同【103】。在本書中我們不會介紹各種演算法的詳細細節:瞭解一些它們共通的高階思想通常已經足夠了,除非你準備自己實現一個共識系統。(可能並不明智,相當難【98,104】) +最著名的容錯共識演算法是 **檢視戳複製(VSR, Viewstamped Replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。這些演算法之間有不少相似之處,但它們並不相同【103】。在本書中我們不會介紹各種演算法的詳細細節:瞭解一些它們共通的高階思想通常已經足夠了,除非你準備自己實現一個共識系統。(可能並不明智,相當難【98,104】) -大多數這些演算法實際上並不直接使用這裡描述的形式化模型(提議與決定單個值,並滿足一致同意、完整性、有效性和終止屬性)。取而代之的是,它們決定了值的**順序(sequence)**,這使它們成為全序廣播演算法,正如本章前面所討論的那樣(請參閱“[全序廣播](#全序廣播)”)。 +大多數這些演算法實際上並不直接使用這裡描述的形式化模型(提議與決定單個值,並滿足一致同意、完整性、有效性和終止屬性)。取而代之的是,它們決定了值的 **順序(sequence)**,這使它們成為全序廣播演算法,正如本章前面所討論的那樣(請參閱 “[全序廣播](#全序廣播)”)。 請記住,全序廣播要求將訊息按照相同的順序,恰好傳遞一次,準確傳送到所有節點。如果仔細思考,這相當於進行了幾輪共識:在每一輪中,節點提議下一條要傳送的訊息,然後決定在全序中下一條要傳送的訊息【67】。 所以,全序廣播相當於重複進行多輪共識(每次共識決定與一次訊息傳遞相對應): -* 由於**一致同意**屬性,所有節點決定以相同的順序傳遞相同的訊息。 -* 由於**完整性**屬性,訊息不會重複。 -* 由於**有效性**屬性,訊息不會被損壞,也不能憑空編造。 -* 由於**終止**屬性,訊息不會丟失。 +* 由於 **一致同意** 屬性,所有節點決定以相同的順序傳遞相同的訊息。 +* 由於 **完整性** 屬性,訊息不會重複。 +* 由於 **有效性** 屬性,訊息不會被損壞,也不能憑空編造。 +* 由於 **終止** 屬性,訊息不會丟失。 -檢視戳複製,Raft和Zab直接實現了全序廣播,因為這樣做比重複**一次一值(one value a time)** 的共識更高效。在Paxos的情況下,這種最佳化被稱為Multi-Paxos。 +檢視戳複製,Raft 和 Zab 直接實現了全序廣播,因為這樣做比重複 **一次一值(one value a time)** 的共識更高效。在 Paxos 的情況下,這種最佳化被稱為 Multi-Paxos。 #### 單領導者複製與共識 -在[第五章](ch5.md)中,我們討論了單領導者複製(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在[第五章](ch5.md)裡一點都沒擔心過共識問題呢? +在 [第五章](ch5.md) 中,我們討論了單領導者複製(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在 [第五章](ch5.md) 裡一點都沒擔心過共識問題呢? -答案取決於如何選擇領導者。如果主庫是由運維人員手動選擇和配置的,那麼你實際上擁有一種**獨裁型別**的“共識演算法”:只有一個節點被允許接受寫入(即決定寫入複製日誌的順序),如果該節點發生故障,則系統將無法寫入,直到運維手動配置其他節點作為主庫。這樣的系統在實踐中可以表現良好,但它無法滿足共識的**終止**屬性,因為它需要人為干預才能取得**進展**。 +答案取決於如何選擇領導者。如果主庫是由運維人員手動選擇和配置的,那麼你實際上擁有一種 **獨裁型別** 的 “共識演算法”:只有一個節點被允許接受寫入(即決定寫入複製日誌的順序),如果該節點發生故障,則系統將無法寫入,直到運維手動配置其他節點作為主庫。這樣的系統在實踐中可以表現良好,但它無法滿足共識的 **終止** 屬性,因為它需要人為干預才能取得 **進展**。 -一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。 +一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。 但是還有一個問題。我們之前曾經討論過腦裂的問題,並且說過所有的節點都需要同意是誰領導,否則兩個不同的節點都會認為自己是領導者,從而導致資料庫進入不一致的狀態。因此,選出一位領導者需要共識。但如果這裡描述的共識演算法實際上是全序廣播演算法,並且全序廣播就像單主複製,而單主複製需要一個領導者,那麼... @@ -796,87 +796,87 @@ XA事務解決了保持多個參與者(資料系統)相互一致的現實的 #### 紀元編號和法定人數 -迄今為止所討論的所有共識協議,在內部都以某種形式使用一個領導者,但它們並不能保證領導者是獨一無二的。相反,它們可以做出更弱的保證:協議定義了一個**紀元編號**(epoch number,在Paxos中被稱為**投票編號**,即ballot number,在檢視戳複製中被稱為**檢視編號**,即view number,以及在Raft中被為**任期號碼**,即term number),並確保在每個時代中,領導者都是唯一的。 +迄今為止所討論的所有共識協議,在內部都以某種形式使用一個領導者,但它們並不能保證領導者是獨一無二的。相反,它們可以做出更弱的保證:協議定義了一個 **紀元編號**(epoch number,在 Paxos 中被稱為 **投票編號**,即 ballot number,在檢視戳複製中被稱為 **檢視編號**,即 view number,以及在 Raft 中被為 **任期號碼**,即 term number),並確保在每個時代中,領導者都是唯一的。 每次當現任領導被認為掛掉的時候,節點間就會開始一場投票,以選出一個新領導。這次選舉被賦予一個遞增的紀元編號,因此紀元編號是全序且單調遞增的。如果兩個不同的時代的領導者之間出現衝突(也許是因為前任領導者實際上並未死亡),那麼帶有更高紀元編號的領導說了算。 -在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在“[真相由多數所定義](ch8.md#真相由多數所定義)”中提到的:一個節點不一定能相信自己的判斷—— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。 +在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在 “[真相由多數所定義](ch8.md#真相由多數所定義)” 中提到的:一個節點不一定能相信自己的判斷 —— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。 -相反,它必須從**法定人數(quorum)** 的節點中獲取選票(請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。 +相反,它必須從 **法定人數(quorum)** 的節點中獲取選票(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。對領導者想要做出的每一個決定,都必須將提議值傳送給其他節點,並等待法定人數的節點響應並贊成提案。法定人數通常(但不總是)由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下,一個節點才會投票贊成提議。 -因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的**法定人群**必須相互**重疊(overlap)**:如果一個提案的表決透過,則至少得有一個參與投票的節點也必須參加過最近的領導者選舉【105】。因此,如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論:沒有發生過更高時代的領導選舉,因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。 +因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的 **法定人群** 必須相互 **重疊(overlap)**:如果一個提案的表決透過,則至少得有一個參與投票的節點也必須參加過最近的領導者選舉【105】。因此,如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論:沒有發生過更高時代的領導選舉,因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。 -這一投票過程表面上看起來很像兩階段提交。最大的區別在於,2PC中協調者不是由選舉產生的,而且2PC則要求**所有**參與者都投贊成票,而容錯共識演算法只需要多數節點的投票。而且,共識演算法還定義了一個恢復過程,節點可以在選舉出新的領導者之後進入一個一致的狀態,確保始終能滿足安全屬性。這些區別正是共識演算法正確性和容錯性的關鍵。 +這一投票過程表面上看起來很像兩階段提交。最大的區別在於,2PC 中協調者不是由選舉產生的,而且 2PC 則要求 **所有** 參與者都投贊成票,而容錯共識演算法只需要多數節點的投票。而且,共識演算法還定義了一個恢復過程,節點可以在選舉出新的領導者之後進入一個一致的狀態,確保始終能滿足安全屬性。這些區別正是共識演算法正確性和容錯性的關鍵。 #### 共識的侷限性 -共識演算法對於分散式系統來說是一個巨大的突破:它為其他充滿不確定性的系統帶來了基礎的安全屬性(一致同意,完整性和有效性),然而它們還能保持容錯(只要多數節點正常工作且可達,就能取得進展)。它們提供了全序廣播,因此它們也可以以一種容錯的方式實現線性一致的原子操作(請參閱“[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”)。 +共識演算法對於分散式系統來說是一個巨大的突破:它為其他充滿不確定性的系統帶來了基礎的安全屬性(一致同意,完整性和有效性),然而它們還能保持容錯(只要多數節點正常工作且可達,就能取得進展)。它們提供了全序廣播,因此它們也可以以一種容錯的方式實現線性一致的原子操作(請參閱 “[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”)。 儘管如此,它們並不是在所有地方都用上了,因為好處總是有代價的。 -節點在做出決定之前對提議進行投票的過程是一種同步複製。如“[同步複製與非同步複製](ch5.md#同步複製與非同步複製)”中所述,通常資料庫會配置為非同步複製模式。在這種配置中發生故障切換時,一些已經提交的資料可能會丟失 —— 但是為了獲得更好的效能,許多人選擇接受這種風險。 +節點在做出決定之前對提議進行投票的過程是一種同步複製。如 “[同步複製與非同步複製](ch5.md#同步複製與非同步複製)” 中所述,通常資料庫會配置為非同步複製模式。在這種配置中發生故障切換時,一些已經提交的資料可能會丟失 —— 但是為了獲得更好的效能,許多人選擇接受這種風險。 -共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(請參閱“[線性一致性的代價](#線性一致性的代價)”)。 +共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(請參閱 “[線性一致性的代價](#線性一致性的代價)”)。 -大多數共識演算法假定參與投票的節點是固定的集合,這意味著你不能簡單的在叢集中新增或刪除節點。共識演算法的**動態成員擴充套件(dynamic membership extension)** 允許叢集中的節點集隨時間推移而變化,但是它們比靜態成員演算法要難理解得多。 +大多數共識演算法假定參與投票的節點是固定的集合,這意味著你不能簡單的在叢集中新增或刪除節點。共識演算法的 **動態成員擴充套件(dynamic membership extension)** 允許叢集中的節點集隨時間推移而變化,但是它們比靜態成員演算法要難理解得多。 共識系統通常依靠超時來檢測失效的節點。在網路延遲高度變化的環境中,特別是在地理上散佈的系統中,經常發生一個節點由於暫時的網路問題,錯誤地認為領導者已經失效。雖然這種錯誤不會損害安全屬性,但頻繁的領導者選舉會導致糟糕的效能表現,因系統最後可能花在權力傾紮上的時間要比花在建設性工作的多得多。 -有時共識演算法對網路問題特別敏感。例如Raft已被證明存在讓人不悅的極端情況【106】:如果整個網路工作正常,但只有一條特定的網路連線一直不可靠,Raft可能會進入領導者在兩個節點間頻繁切換的局面,或者當前領導者不斷被迫辭職以致系統實質上毫無進展。其他一致性演算法也存在類似的問題,而設計能健壯應對不可靠網路的演算法仍然是一個開放的研究問題。 +有時共識演算法對網路問題特別敏感。例如 Raft 已被證明存在讓人不悅的極端情況【106】:如果整個網路工作正常,但只有一條特定的網路連線一直不可靠,Raft 可能會進入領導者在兩個節點間頻繁切換的局面,或者當前領導者不斷被迫辭職以致系統實質上毫無進展。其他一致性演算法也存在類似的問題,而設計能健壯應對不可靠網路的演算法仍然是一個開放的研究問題。 ### 成員與協調服務 -像ZooKeeper或etcd這樣的專案通常被描述為“分散式鍵值儲存”或“協調與配置服務”。這種服務的API看起來非常像資料庫:你可以讀寫給定鍵的值,並遍歷鍵。所以如果它們基本上算是資料庫的話,為什麼它們要把工夫全花在實現一個共識演算法上呢?是什麼使它們區別於其他任意型別的資料庫? +像 ZooKeeper 或 etcd 這樣的專案通常被描述為 “分散式鍵值儲存” 或 “協調與配置服務”。這種服務的 API 看起來非常像資料庫:你可以讀寫給定鍵的值,並遍歷鍵。所以如果它們基本上算是資料庫的話,為什麼它們要把工夫全花在實現一個共識演算法上呢?是什麼使它們區別於其他任意型別的資料庫? -為了理解這一點,簡單瞭解如何使用ZooKeeper這類服務是很有幫助的。作為應用開發人員,你很少需要直接使用ZooKeeper,因為它實際上不適合當成通用資料庫來用。更有可能的是,你會透過其他專案間接依賴它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依賴ZooKeeper在後臺執行。這些專案從它那裡得到了什麼? +為了理解這一點,簡單瞭解如何使用 ZooKeeper 這類服務是很有幫助的。作為應用開發人員,你很少需要直接使用 ZooKeeper,因為它實際上不適合當成通用資料庫來用。更有可能的是,你會透過其他專案間接依賴它,例如 HBase、Hadoop YARN、OpenStack Nova 和 Kafka 都依賴 ZooKeeper 在後臺執行。這些專案從它那裡得到了什麼? -ZooKeeper和etcd被設計為容納少量完全可以放在記憶體中的資料(雖然它們仍然會寫入磁碟以保證永續性),所以你不會想著把所有應用資料放到這裡。這些少量資料會透過容錯的全序廣播演算法複製到所有節點上。正如前面所討論的那樣,資料庫複製需要的就是全序廣播:如果每條訊息代表對資料庫的寫入,則以相同的順序應用相同的寫入操作可以使副本之間保持一致。 +ZooKeeper 和 etcd 被設計為容納少量完全可以放在記憶體中的資料(雖然它們仍然會寫入磁碟以保證永續性),所以你不會想著把所有應用資料放到這裡。這些少量資料會透過容錯的全序廣播演算法複製到所有節點上。正如前面所討論的那樣,資料庫複製需要的就是全序廣播:如果每條訊息代表對資料庫的寫入,則以相同的順序應用相同的寫入操作可以使副本之間保持一致。 -ZooKeeper模仿了Google的Chubby鎖服務【14,98】,不僅實現了全序廣播(因此也實現了共識),而且還構建了一組有趣的其他特性,這些特性在構建分散式系統時變得特別有用: +ZooKeeper 模仿了 Google 的 Chubby 鎖服務【14,98】,不僅實現了全序廣播(因此也實現了共識),而且還構建了一組有趣的其他特性,這些特性在構建分散式系統時變得特別有用: * 線性一致性的原子操作 - 使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。 + 使用原子 CAS 操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以 **租約(lease)** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱 “[程序暫停](ch8.md#程序暫停)”)。 * 操作的全序排序 - 如“[領導者和鎖](ch8.md#領導者和鎖)”中所述,當某個資源受到鎖或租約的保護時,你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。 ZooKeeper透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務ID(`zxid`)和版本號(`cversion`)【15】。 + 如 “[領導者和鎖](ch8.md#領導者和鎖)” 中所述,當某個資源受到鎖或租約的保護時,你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。ZooKeeper 透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務 ID(`zxid`)和版本號(`cversion`)【15】。 * 失效檢測 - 客戶端在ZooKeeper伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者ZooKeeper節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper會宣告該會話已死亡。當會話超時時(ZooKeeper稱這些節點為**臨時節點**,即ephemeral nodes),會話持有的任何鎖都可以配置為自動釋放。 + 客戶端在 ZooKeeper 伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者 ZooKeeper 節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper 會宣告該會話已死亡。當會話超時時(ZooKeeper 稱這些節點為 **臨時節點**,即 ephemeral nodes),會話持有的任何鎖都可以配置為自動釋放。 * 變更通知 - 客戶端不僅可以讀取其他客戶端建立的鎖和值,還可以監聽它們的變更。因此,客戶端可以知道另一個客戶端何時加入叢集(基於新客戶端寫入ZooKeeper的值),或發生故障(因其會話超時,而其臨時節點消失)。透過訂閱通知,客戶端不用再透過頻繁輪詢的方式來找出變更。 + 客戶端不僅可以讀取其他客戶端建立的鎖和值,還可以監聽它們的變更。因此,客戶端可以知道另一個客戶端何時加入叢集(基於新客戶端寫入 ZooKeeper 的值),或發生故障(因其會話超時,而其臨時節點消失)。透過訂閱通知,客戶端不用再透過頻繁輪詢的方式來找出變更。 -在這些功能中,只有線性一致的原子操作才真的需要共識。但正是這些功能的組合,使得像ZooKeeper這樣的系統在分散式協調中非常有用。 +在這些功能中,只有線性一致的原子操作才真的需要共識。但正是這些功能的組合,使得像 ZooKeeper 這樣的系統在分散式協調中非常有用。 #### 將工作分配給節點 -ZooKeeper/Chubby模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。 +ZooKeeper/Chubby 模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。 -另一個例子是,當你有一些分割槽資源(資料庫,訊息流,檔案儲存,分散式Actor系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。 +另一個例子是,當你有一些分割槽資源(資料庫,訊息流,檔案儲存,分散式 Actor 系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱 “[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。 -這類任務可以透過在ZooKeeper中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在ZooKeeper客戶端API基礎之上提供更高層工具的庫,例如Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多,這樣的嘗試鮮有成功記錄【107】。 +這類任務可以透過在 ZooKeeper 中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在 ZooKeeper 客戶端 API 基礎之上提供更高層工具的庫,例如 Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多,這樣的嘗試鮮有成功記錄【107】。 -應用最初只能在單個節點上執行,但最終可能會增長到數千個節點。試圖在如此之多的節點上進行多數投票將是非常低效的。相反,ZooKeeper在固定數量的節點(通常是三到五個)上執行,並在這些節點之間執行其多數票,同時支援潛在的大量客戶端。因此,ZooKeeper提供了一種將協調節點(共識,操作排序和故障檢測)的一些工作“外包”到外部服務的方式。 +應用最初只能在單個節點上執行,但最終可能會增長到數千個節點。試圖在如此之多的節點上進行多數投票將是非常低效的。相反,ZooKeeper 在固定數量的節點(通常是三到五個)上執行,並在這些節點之間執行其多數票,同時支援潛在的大量客戶端。因此,ZooKeeper 提供了一種將協調節點(共識,操作排序和故障檢測)的一些工作 “外包” 到外部服務的方式。 -通常,由ZooKeeper管理的資料型別的變化十分緩慢:代表“分割槽 7 中的節點執行在 `10.1.1.23` 上”的資訊可能會在幾分鐘或幾小時的時間內發生變化。它不是用來儲存應用的執行時狀態的,後者每秒可能會改變數千甚至數百萬次。如果應用狀態需要從一個節點複製到另一個節點,則可以使用其他工具(如Apache BookKeeper 【108】)。 +通常,由 ZooKeeper 管理的資料型別的變化十分緩慢:代表 “分割槽 7 中的節點執行在 `10.1.1.23` 上” 的資訊可能會在幾分鐘或幾小時的時間內發生變化。它不是用來儲存應用的執行時狀態的,後者每秒可能會改變數千甚至數百萬次。如果應用狀態需要從一個節點複製到另一個節點,則可以使用其他工具(如 Apache BookKeeper 【108】)。 #### 服務發現 -ZooKeeper,etcd和Consul也經常用於服務發現——也就是找出你需要連線到哪個IP地址才能到達特定的服務。在雲資料中心環境中,虛擬機器來來往往很常見,你通常不會事先知道服務的IP地址。相反,你可以配置你的服務,使其在啟動時註冊服務登錄檔中的網路端點,然後可以由其他服務找到它們。 +ZooKeeper、etcd 和 Consul 也經常用於服務發現 —— 也就是找出你需要連線到哪個 IP 地址才能到達特定的服務。在雲資料中心環境中,虛擬機器來來往往很常見,你通常不會事先知道服務的 IP 地址。相反,你可以配置你的服務,使其在啟動時註冊服務登錄檔中的網路端點,然後可以由其他服務找到它們。 -但是,服務發現是否需要達成共識還不太清楚。 DNS是查詢服務名稱的IP地址的傳統方式,它使用多層快取來實現良好的效能和可用性。從DNS讀取是絕對不線性一致性的,如果DNS查詢的結果有點陳舊,通常不會有問題【109】。 DNS的可用性和對網路中斷的魯棒性更重要。 +但是,服務發現是否需要達成共識還不太清楚。 DNS 是查詢服務名稱的 IP 地址的傳統方式,它使用多層快取來實現良好的效能和可用性。從 DNS 讀取是絕對不線性一致性的,如果 DNS 查詢的結果有點陳舊,通常不會有問題【109】。 DNS 的可用性和對網路中斷的魯棒性更重要。 儘管服務發現並不需要共識,但領導者選舉卻是如此。因此,如果你的共識系統已經知道領導是誰,那麼也可以使用這些資訊來幫助其他服務發現領導是誰。為此,一些共識系統支援只讀快取副本。這些副本非同步接收共識演算法所有決策的日誌,但不主動參與投票。因此,它們能夠提供不需要線性一致性的讀取請求。 #### 成員資格服務 -ZooKeeper和它的小夥伴們可以看作是成員資格服務(membership services)研究的悠久歷史的一部分,這個歷史可以追溯到20世紀80年代,並且對建立高度可靠的系統(例如空中交通管制)非常重要【110】。 +ZooKeeper 和它的小夥伴們可以看作是成員資格服務(membership services)研究的悠久歷史的一部分,這個歷史可以追溯到 20 世紀 80 年代,並且對建立高度可靠的系統(例如空中交通管制)非常重要【110】。 -成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在[第八章](ch8.md)中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。 +成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在 [第八章](ch8.md) 中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。 即使它確實存在,仍然可能發生一個節點被共識錯誤地宣告死亡。但是對於一個系統來說,知道哪些節點構成了當前的成員關係是非常有用的。例如,選擇領導者可能意味著簡單地選擇當前成員中編號最小的成員,但如果不同的節點對現有的成員都有誰有不同意見,則這種方法將不起作用。 @@ -885,53 +885,53 @@ ZooKeeper和它的小夥伴們可以看作是成員資格服務(membership ser 在本章中,我們從幾個不同的角度審視了關於一致性與共識的話題。我們深入研究了線性一致性(一種流行的一致性模型):其目標是使多副本資料看起來好像只有一個副本一樣,並使其上所有操作都原子性地生效。雖然線性一致性因為簡單易懂而很吸引人 —— 它使資料庫表現的好像單執行緒程式中的一個變數一樣,但它有著速度緩慢的缺點,特別是在網路延遲很大的環境中。 -我們還探討了因果性,因果性對系統中的事件施加了順序(什麼發生在什麼之前,基於因與果)。與線性一致不同,線性一致性將所有操作放在單一的全序時間線中,因果一致性為我們提供了一個較弱的一致性模型:某些事件可以是**併發**的,所以版本歷史就像是一條不斷分叉與合併的時間線。因果一致性沒有線性一致性的協調開銷,而且對網路問題的敏感性要低得多。 +我們還探討了因果性,因果性對系統中的事件施加了順序(什麼發生在什麼之前,基於因與果)。與線性一致不同,線性一致性將所有操作放在單一的全序時間線中,因果一致性為我們提供了一個較弱的一致性模型:某些事件可以是 **併發** 的,所以版本歷史就像是一條不斷分叉與合併的時間線。因果一致性沒有線性一致性的協調開銷,而且對網路問題的敏感性要低得多。 -但即使捕獲到因果順序(例如使用蘭伯特時間戳),我們發現有些事情也不能透過這種方式實現:在“[光有時間戳排序還不夠](#光有時間戳排序還不夠)”一節的例子中,我們需要確保使用者名稱是唯一的,並拒絕同一使用者名稱的其他併發註冊。如果一個節點要透過註冊,則需要知道其他的節點沒有在併發搶注同一使用者名稱的過程中。這個問題引領我們走向**共識**。 +但即使捕獲到因果順序(例如使用蘭伯特時間戳),我們發現有些事情也不能透過這種方式實現:在 “[光有時間戳排序還不夠](#光有時間戳排序還不夠)” 一節的例子中,我們需要確保使用者名稱是唯一的,並拒絕同一使用者名稱的其他併發註冊。如果一個節點要透過註冊,則需要知道其他的節點沒有在併發搶注同一使用者名稱的過程中。這個問題引領我們走向 **共識**。 我們看到,達成共識意味著以這樣一種方式決定某件事:所有節點一致同意所做決定,且這一決定不可撤銷。透過深入挖掘,結果我們發現很廣泛的一系列問題實際上都可以歸結為共識問題,並且彼此等價(從這個意義上來講,如果你有其中之一的解決方案,就可以輕易將它轉換為其他問題的解決方案)。這些等價的問題包括: -* 線性一致性的CAS暫存器 +* 線性一致性的 CAS 暫存器 - 暫存器需要基於當前值是否等於操作給出的引數,原子地**決定**是否設定新值。 + 暫存器需要基於當前值是否等於操作給出的引數,原子地 **決定** 是否設定新值。 * 原子事務提交 - 資料庫必須**決定**是否提交或中止分散式事務。 + 資料庫必須 **決定** 是否提交或中止分散式事務。 * 全序廣播 - 訊息系統必須**決定**傳遞訊息的順序。 + 訊息系統必須 **決定** 傳遞訊息的順序。 * 鎖和租約 - 當幾個客戶端爭搶鎖或租約時,由鎖來**決定**哪個客戶端成功獲得鎖。 + 當幾個客戶端爭搶鎖或租約時,由鎖來 **決定** 哪個客戶端成功獲得鎖。 -* 成員/協調服務 +* 成員 / 協調服務 - 給定某種故障檢測器(例如超時),系統必須**決定**哪些節點活著,哪些節點因為會話超時需要被宣告死亡。 + 給定某種故障檢測器(例如超時),系統必須 **決定** 哪些節點活著,哪些節點因為會話超時需要被宣告死亡。 * 唯一性約束 - 當多個事務同時嘗試使用相同的鍵建立衝突記錄時,約束必須**決定**哪一個被允許,哪些因為違反約束而失敗。 + 當多個事務同時嘗試使用相同的鍵建立衝突記錄時,約束必須 **決定** 哪一個被允許,哪些因為違反約束而失敗。 如果你只有一個節點,或者你願意將決策的權能分配給單個節點,所有這些事都很簡單。這就是在單領導者資料庫中發生的事情:所有決策權歸屬於領導者,這就是為什麼這樣的資料庫能夠提供線性一致的操作,唯一性約束,完全有序的複製日誌,以及更多。 但如果該領導者失效,或者如果網路中斷導致領導者不可達,這樣的系統就無法取得任何進展。應對這種情況可以有三種方法: -1. 等待領導者恢復,接受系統將在這段時間阻塞的事實。許多XA/JTA事務協調者選擇這個選項。這種方法並不能完全達成共識,因為它不能滿足**終止**屬性的要求:如果領導者續命失敗,系統可能會永久阻塞。 -2. 人工故障切換,讓人類選擇一個新的領導者節點,並重新配置系統使之生效,許多關係型資料庫都採用這種方方式。這是一種來自“天意”的共識 —— 由計算機系統之外的運維人員做出決定。故障切換的速度受到人類行動速度的限制,通常要比計算機慢(得多)。 +1. 等待領導者恢復,接受系統將在這段時間阻塞的事實。許多 XA/JTA 事務協調者選擇這個選項。這種方法並不能完全達成共識,因為它不能滿足 **終止** 屬性的要求:如果領導者續命失敗,系統可能會永久阻塞。 +2. 人工故障切換,讓人類選擇一個新的領導者節點,並重新配置系統使之生效,許多關係型資料庫都採用這種方方式。這是一種來自 “天意” 的共識 —— 由計算機系統之外的運維人員做出決定。故障切換的速度受到人類行動速度的限制,通常要比計算機慢(得多)。 3. 使用演算法自動選擇一個新的領導者。這種方法需要一種共識演算法,使用成熟的演算法來正確處理惡劣的網路條件是明智之舉【107】。 -儘管單領導者資料庫可以提供線性一致性,且無需對每個寫操作都執行共識演算法,但共識對於保持及變更領導權仍然是必須的。因此從某種意義上說,使用單個領導者不過是“緩兵之計”:共識仍然是需要的,只是在另一個地方,而且沒那麼頻繁。好訊息是,容錯的共識演算法與容錯的共識系統是存在的,我們在本章中簡要地討論了它們。 +儘管單領導者資料庫可以提供線性一致性,且無需對每個寫操作都執行共識演算法,但共識對於保持及變更領導權仍然是必須的。因此從某種意義上說,使用單個領導者不過是 “緩兵之計”:共識仍然是需要的,只是在另一個地方,而且沒那麼頻繁。好訊息是,容錯的共識演算法與容錯的共識系統是存在的,我們在本章中簡要地討論了它們。 -像ZooKeeper這樣的工具為應用提供了“外包”的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受[第八章](ch8.md)中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似ZooKeeper的東西是明智之舉。 +像 ZooKeeper 這樣的工具為應用提供了 “外包” 的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受 [第八章](ch8.md) 中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似 ZooKeeper 的東西是明智之舉。 -儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。 +儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。 本章引用了大量關於分散式系統理論的研究。雖然理論論文和證明並不總是容易理解,有時也會做出不切實際的假設,但它們對於指導這一領域的實踐有著極其重要的價值:它們幫助我們推理什麼可以做,什麼不可以做,幫助我們找到反直覺的分散式系統缺陷。如果你有時間,這些參考資料值得探索。 -這裡已經到了本書[第二部分](part-ii.md)的末尾,第二部介紹了複製([第五章](ch5.md))、分割槽([第六章](ch6.md))、事務([第七章](ch7.md))、分散式系統的故障模型([第八章](ch8.md))以及最後的一致性與共識([第九章](ch9.md))。現在我們已經奠定了紮實的理論基礎,我們將在[第三部分](part-iii.md)再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。 +這裡已經到了本書 [第二部分](part-ii.md) 的末尾,第二部介紹了複製([第五章](ch5.md))、分割槽([第六章](ch6.md))、事務([第七章](ch7.md))、分散式系統的故障模型([第八章](ch8.md))以及最後的一致性與共識([第九章](ch9.md))。現在我們已經奠定了紮實的理論基礎,我們將在 [第三部分](part-iii.md) 再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。 ## 參考文獻