From b70136534e5fb5b538d00515e66ae180f7a4706d Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 03:09:34 +0800 Subject: [PATCH] ch10 rough trans --- README.md | 2 +- ch10.md | 431 +++++++++++++++++++++++++++--------------------------- ch11.md | 126 ++++++++-------- ch12.md | 210 +++++++++++++------------- ch3.md | 2 +- 5 files changed, 389 insertions(+), 382 deletions(-) diff --git a/README.md b/README.md index c1574b4..47ec489 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ | 第八章:分布式系统中的问题 | 初翻 | | | 第九章:一致性与共识 | 初翻 65% | Vonng | | 第三部分:前言 | 精翻 | | -| 第十章:批处理 | 机翻 | | +| 第十章:批处理 | 草翻 | | | 第十一章:流处理 | 机翻 | | | 第十二章:数据系统的未来 | 机翻 | | | 术语表 | - | | diff --git a/ch10.md b/ch10.md index 21f7d28..acb5ffe 100644 --- a/ch10.md +++ b/ch10.md @@ -2,7 +2,7 @@ ![](img/ch10.png) -> 带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,不同的人们以自己的方式来测试,真正的考验才开始。 +> 带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,不同的人们以自己的方式来测试时,真正的考验才开始。 > > ——高德纳 @@ -10,61 +10,58 @@ [TOC] -在本书的前两部分中,我们讨论了很多关于请求和查询以及相应的响应或结果。这种数据处理方式在许多现代数据系统中都是假设的:你要求什么,或者发送指令,一段时间后系统(希望)会给你一个答案。数据库,缓存,搜索索引,Web服务器以及其他许多系统都以这种方式工作。 +在本书的前两部分中,我们讨论了很多关于**请求(requests)**和**查询(queries)**以及相应的**响应(response)**或**结果(results)**。在许多现代数据系统中都假设采用这种数据处理方式:你要求某些东西,或者发送指令,一段时间后(希望)系统会给你一个答案。数据库,缓存,搜索索引,Web服务器以及其他许多系统都以这种方式工作。 -在这样的在线系统中,无论是浏览器请求页面还是调用远程API的服务,我们通常都假设请求是由人类用户触发的,并且用户正在等待响应。他们不必等太久,所以我们非常重视这些系统的响应时间(请参阅第13页的“描述性能”)。 +在这类**在线(online)**系统中,无论是浏览器请求页面还是调用远程API的服务,我们通常都假设请求是由人类用户触发的,且用户正在等待响应。他们不必等太久,所以我们非常重视这些系统的响应时间(参阅“[描述性能](ch1.md)”)。 -Web和越来越多的基于HTTP / REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来区分三种不同类型的系统: -服务(在线系统) +Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来区分三种不同类型的系统: + +***服务(在线系统)*** 服务等待客户的请求或指令到达。当收到一个,服务试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要(如果客户端无法访问服务,用户可能会收到错误消息)。 ***批处理系统(离线系统)*** -一个批处理系统需要大量的输入数据,运行一个工作来处理它,并产生一些输出数据。工作往往需要一段时间(从几分钟到几天),所以通常不会有用户等待工作完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(通过特定大小的输入数据集所需的时间)。我们讨论本章中的批处理。 +一个批处理系统需要大量的输入数据,运行一个工作来处理它,并产生一些输出数据。工作往往需要一段时间(从几分钟到几天),所以通常不会有用户等待工作完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(通过特定大小的输入数据集所需的时间)。我们将在本章中讨论批处理。 ***流处理系统(近实时系统)*** -流处理是在线和离线/批处理之间的一个地方(所以有时候被称为近实时或近线处理)。像批处理系统一样,流处理器消耗输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则使用固定的一组输入数据进行操作。这种差异使流处理系统比等效的批处理系统具有更低的延迟。由于流处理基于批处理,我们将在第11章讨论它。 +流处理是在线和离线/批处理之间的一个地方(所以有时候被称为近实时或近线处理)。像批处理系统一样,流处理器消耗输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则使用固定的一组输入数据进行操作。这种差异使流处理系统比等效的批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第11章](ch11.md)讨论它。 -正如我们将在本章中看到的那样,批量处理是构建可靠,可扩展和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能过度热情地)被称为“使得Google具有如此大规模可扩展性的算法”[2]。随后在各种开源数据系统中实施,包括Hadoop,CouchDB和MongoDB。 +正如我们将在本章中看到的那样,批量处理是构建可靠,可扩展和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能过热地)被称为“使得Google具有如此大规模可扩展性的算法”【2】。随后在各种开源数据系统中实现,包括Hadoop,CouchDB和MongoDB。 -与多年前为数据仓库开发的并行处理系统[3,4]相比,MapReduce是一个相当低级别的编程模型,但它在处理规模方面迈出了重要的一步。在商品硬件上。虽然MapReduce的重要性正在下降[5],但它仍然值得理解,因为它提供了批处理为什么以及如何有用的清晰画面。 - -实际上,批处理是一种非常古老的计算形式。早在可编程数字计算机诞生之前,打孔卡制表机(例如1890年美国人口普查[6]中使用的霍尔里斯机)实现了半机械化的批处理形式,以计算来自大量输入的汇总统计量。 Map-Reduce与1940年代和1950年代广泛用于商业数据处理的机电IBM卡片分类机器有着惊人的相似之处[7]。像往常一样,历史有重演的趋势。 - -在本章中,我们将看看MapReduce和其他一些批处理算法和框架,并探讨它们在现代数据系统中的使用方式。但首先,要开始,我们将看看使用标准Unix工具的数据处理。即使你已经熟悉了它们,Unix的哲学提醒也是值得的,因为从Unix的想法和经验教训转移到大规模,异构的分布式数据系统。 +与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce是一个相当低级别的编程模型,但它对于在商用硬件上的处理规模上迈出了重要的一步。。虽然MapReduce的重要性正在下降【5】,但它仍然值得理解,因为它提供了一副批处理为什么,以及如何有用的清晰画面。 +实际上,批处理是一种非常古老的计算形式。早在可编程数字计算机诞生之前,打孔卡制表机(例如1890年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,以计算来自大量输入的汇总统计量。 Map-Reduce与1940年代和1950年代广泛用于商业数据处理的机电IBM卡片分类机器有着惊人的相似之处【7】。像往常一样,历史总是在不断重复自己。 +在本章中,我们将看看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) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" ``` -(实际上这只是一行,为了便于阅读,它只是分成多行)。这一行中有很多信息。为了解释它,你需要看看日志格式的定义,如下所示:: +(实际上这只是一行,为了可读性拆分为多行)。这一行中有很多信息。为了解释它,你需要看一看日志格式的定义,如下所示:: -``` +```bash $remote_addr - $remote_user [$time_local] "$request" $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-02-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的参数。 但这种写法让流水线更显眼。 @@ -80,11 +77,11 @@ cat /var/log/nginx/access.log | #1 1. 读取日志文件 2. 将每一行按空格分割成不同的字段,每行只输出第七个这样的字段,恰好是请求的URL。在我们的示例行中,这个请求URL是`/css/typography.css`。 3. 按字母顺序排列请求的URL列表。如果某个URL被请求过n次,那么排序后,该文件将包含连续n次重复的URL。 -4. uniq命令通过检查两条相邻的行是否相同来过滤掉其输入中的重复行。 -c选项告诉它也输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 -5. 第二种排序按每行起始处的数字(-n)排序,这是请求URL的次数。然后以反向(-r)顺序返回结果,即首先以最大的数字返回结果。 -6. 最后,头只输出输入的前五行(-n 5),并丢弃其余的。该系列命令的输出如下所示: +4. uniq命令通过检查两条相邻的行是否相同来过滤掉其输入中的重复行。 `-c`选项告诉它也输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 +5. 第二种排序按每行起始处的数字(`-n`)排序,这是请求URL的次数。然后以反向(`-r`)顺序返回结果,即首先以最大的数字返回结果。 +6. 最后,头只输出输入的前五行(`-n 5`),并丢弃其余的。该系列命令的输出如下所示: -``` +```bash 4189 /favicon.ico 3631 /2013/05/24/improving-security-of-ssh-private-keys.html 2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html @@ -92,19 +89,19 @@ cat /var/log/nginx/access.log | #1 915 /css/typography.css ``` -尽管如果你不熟悉Unix工具,上面的命令行可能看起来有点模糊,但是它非常强大。它将在几秒钟内处理千兆字节的日志文件,您可以轻松修改分析以适应您的需求。例如,如果要从报告中省略CSS文件,请将awk参数更改为`'$7 !~ /\.css$/ {print $7}'`等等。 +尽管如果你不熟悉Unix工具,上面的命令行可能看起来有点模糊,但是它非常强大。它将在几秒钟内处理千兆字节的日志文件,你可以轻松修改分析以适应你的需求。例如,如果要从报告中省略CSS文件,请将`awk`参数更改为`'$7 !~ /\.css$/ {print $7}'`等等。 -本书中没有空间来详细探索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.each do |line| - url = line.split[6] # 2 + url = line.split【6】 # 2 counts[url] += 1 # 3 end end @@ -113,72 +110,76 @@ top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 top5.each{|count, url| puts "#{count} #{url}" } # 5 ``` -1. `counts`是一个哈希表,保持一个计数器的次数,我们已经看到每个网址。计数器默认为零。 -2. 从日志的每一行,我们都把URL作为第七个空格分隔的字段(这里的数组索引是6,因为Ruby的数组是零索引的)。 +1. `counts`是一个哈希表,保存了我们浏览每个URL次数的计数器,计数器默认为零。 +2. 对日志的每一行,从第七个空格分隔的字段提取URL(这里的数组索引是6,因为Ruby的数组索引从零开始)。 3. 增加日志当前行中URL的计数器。 4. 按计数器值(降序)对散列表内容进行排序,并取前五位。 5. 打印出前五个条目。 -这个程序并不像Unix管道那样简洁,但是它的可读性很强,你喜欢的两个中的哪一个是味道的一部分。但是,除了两者之间的表面差异之外,执行流程也有很大差异,如果您在大文件上运行此分析,则会变得明显。 +这个程序并不像Unix管道那样简洁,但是它的可读性很强,你喜欢哪一个属于口味的问题。但两者除了表面上的差异之外,执行流程也有很大差异,如果你在大文件上运行此分析,则会变得明显。 #### 排序与内存中的聚合 Ruby脚本保存一个URL的内存哈希表,其中每个URL映射到它已经被看到的次数。 Unix流水线的例子没有这样一个哈希表,而是依赖于对一个URL列表进行排序,在这个URL列表中,同一个URL的多个发生只是简单的重复。 -哪种方法更好?这取决于你有多少个不同的网址。对于大多数中小型网站,您可能可以适应所有不同的URL,并且可以为每个网址(例如1GB内存)提供一个计数器。在此示例中,作业的工作集(作业需要随机访问的内存量)仅取决于不同URL的数量:如果单个URL有一百万个日志条目,则散列中所需的空间表仍然只有一个URL加上计数器的大小。如果这个工作集足够小,那么内存散列表工作正常,甚至在笔记本电脑上也是如此。 +哪种方法更好?这取决于你有多少个不同的网址。对于大多数中小型网站,你可能可以适应所有不同的URL,并且可以为每个网址(例如1GB内存)提供一个计数器。在此示例中,作业的工作集(作业需要随机访问的内存量)仅取决于不同URL的数量:如果单个URL有一百万个日志条目,则散列中所需的空间表仍然只有一个URL加上计数器的大小。如果这个工作集足够小,那么内存散列表工作正常,甚至在笔记本电脑上也是如此。 -另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在第74页的“SSTables和LSM-Trees”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序的段可以合并为一个更大的排序文件。 Mergesort具有在磁盘上运行良好的顺序访问模式。 (请记住,在顺序I / O中进行优化是第3章中反复出现的主题。这里再次出现相同的模式。) +另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在第74页的“[SSTables和LSM树](ch3.md#SSTables和LSM树)”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,优化为顺序I/O是[第3章](ch3.md)中反复出现的主题。这里再次出现相同的模式。) -GNU Coreutils(Linux)中的排序实用程序通过溢出到磁盘自动处理大于内存的数据集,并自动并行排序跨多个CPU核心[9]。这意味着我们之前看到的简单的Unix命令链很容易扩展到大数据集,而不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 +GNU Coreutils(Linux)中的排序实用程序通过溢出到磁盘自动处理大于内存的数据集,并自动并行排序跨多个CPU核心【9】。这意味着我们之前看到的简单的Unix命令链很容易扩展到大数据集,而不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 ### Unix哲学 -我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是Unix的关键设计思想之一,而且它今天依然令人惊讶。让我们更深入地研究一下,这样我们可以从Unix中借鉴一些想法[10]。 +我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是Unix的关键设计思想之一,且它今天仍然令人惊讶地关联。让我们更深入地研究一下,以便从Unix中借鉴一些想法【10】。 -Unix管道的发明者道格·麦克罗伊(Doug McIlroy)在1964年首先描述了这种情况[11]:“当需要以另一种方式处理数据时,我们应该有一些连接程序的方法,比如[a] 。这也是I / O的方式。“管道类比困难了,连接程序和管道的想法成为了现在被称为Unix哲学的一部分 - 一套在开发者中流行的设计原则。 Unix的用户。哲学在1978年描述如下[12,13]: +Unix管道的发明者道格·麦克罗伊(Doug McIlroy)在1964年首先描述了这种情况【11】:“当需要以另一种方式处理数据时,我们应该有一些连接程序的方法,比如[a] 。这也是I/O的方式。“管道类比困难了,连接程序和管道的想法成为了现在被称为**Unix哲学**的一部分 —— 一套在开发者中流行的设计原则。 Unix哲学在1978年描述如下【12,13】: -1. 让每个程序都做好一件事。要做好新的工作,重新建立一个新的“特征”,而不是使旧的计划复杂化。 -2. 期待每个程序的输出成为另一个程序的输入。不要混淆输出与无关的信息。避免使用严格的柱状或二进制输入格式。不要坚持交互式输入。 +1. 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加“功能”让老程序复杂化。 +2. 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。 3. 设计和构建软件,甚至是操作系统,要尽早尝试,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。 -4. 使用工具优先于不熟练的帮助来减轻编程任务,即使您必须绕道建立工具,并期望在完成使用后将其中一些工具扔掉。 +4. 优先使用工具,而不是不熟练的帮助来减轻编程任务,即使必须曲线救国编写工具,并期望在用完后扔掉大部分。 -这种方法 - 自动化,快速原型设计,迭代式迭代,对实验友好,将大型项目分解成可管理的块 - 听起来非常像今天的敏捷和DevOps运动。奇怪的是,四十年来变化不大。 +这种方法 —— 自动化,快速原型设计,迭代式迭代,实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和DevOps运动。奇怪的是,四十年来变化不大。 -排序工具是一个很好的例子。它可以说是一个比大多数编程语言在其标准库(不会溢出到磁盘并且不使用多线程,即使是有益的)中更好的排序实现。然而,这种分类几乎没有用处。它只能与其他Unix工具(如uniq)结合使用。 +排序工具是一个很好的例子。它可以说是一个比大多数编程语言在其标准库(不会溢出到磁盘并且不使用多线程,即使是有益的)中更好的排序实现。然而,单独使用`sort` 几乎没什么用。它只能与其他Unix工具(如`uniq`)结合使用。 像bash这样的Unix shell可以让我们轻松地将这些小程序组合成令人惊讶的强大数据处理作业。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix如何实现这种可组合性? #### 统一的接口 -如果您希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 - 换句话说,一个兼容的接口。如果您希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的输入/输出接口。 +如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 —— 换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的I/O接口。 -在Unix中,该接口是一个文件(更准确地说,是一个文件描述符)。一个文件只是一个有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的实际文件,到另一个进程(Unix套接字,stdin,stdout)的通信通道,设备驱动程序(比如`/dev/audio`或`/dev/lp0`),表示TCP连接的套接字等等。理所当然的事很容易,但实际上这些非常不同的事物可以共享一个统一的界面,所以它们可以很容易地连接在一起[^ii]。 +在Unix中,这种接口是一个**文件(file)**(更准确地说,是一个**文件描述符(file descriptor)**)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix套接字,stdin,stdout)的通信通道,设备驱动程序(比如`/dev/audio`或`/dev/lp0`),表示TCP连接的套接字等等。很容易将这些视为理所当然的事情,但实际上这是非常出色的设计:这些非常不同的事物可以共享一个统一的接口,所以它们可以很容易地连接在一起[^ii]。 [^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工具一样流畅地运行程序是一个例外,而不是规范。 -即使是具有相同数据模型的数据库,也往往不容易将数据从一个数据模型中移出。这种缺乏整合导致数据的巴尔干化。 +即使是具有**相同数据模型(same data model)**的数据库,将数据从一种导到另一种也并不容易。缺乏整合导致了数据的巴尔干化[^译注i]。 -#### 逻辑和布线的分离 +[^译注i]: ****巴尔干化(Balkanization)**是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 -Unix工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘和标准输出到屏幕上。但是,您也可以从文件输入和/或将输出重定向到文件。管道允许您将一个进程的标准输出附加到另一个进程的标准输入(具有小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 -程序仍然可以直接读取和写入文件,但如果程序不担心特定的文件路径,只使用标准输入和标准输出,则Unix方法效果最好。这允许shell用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种松耦合,后期绑定[15]或控制反转[16])。将输入/输出接线与程序逻辑分开,可以将小工具组合成更大的系统。 -您甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,并且可以参与数据处理流水线。在日志分析示例中,您可以编写一个工具将用户代理字符串转换为更灵敏的浏览器标识符,或者将IP地址转换为国家代码的工具,并将其插入管道。排序程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 +#### 逻辑和布线相分离 -但是,使用stdin和stdout可以做什么是有限的。需要多个输入或输出的程序是可能的但棘手的。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,则无法将程序的输出传输到网络连接中【17,18】[^iii] 。 I / O由程序本身连接。它仍然可以配置(例如通过命令行选项),但是减少了在Shell中连接输入和输出的灵活性。 +Unix工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和/或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(具有小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 -[^iii]: 除了使用一个单独的工具,如netcat或curl。 Unix开始试图将所有东西都表示为文件,但是BSD套接字API偏离了这个惯例[17]。研究操作系统Plan 9和Inferno在使用文件方面更加一致:它们将TCP连接表示为/ net / tcp中的文件[18]。 +程序仍然可以直接读取和写入文件,但如果程序不担心特定的文件路径,只使用标准输入和标准输出,则Unix方法效果最好。这允许shell用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种**松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或**控制反转(inversion of control)**【16】)。将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。 + +你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,并且可以参与数据处理流水线。在日志分析示例中,你可以编写一个工具将用户代理字符串转换为更灵敏的浏览器标识符,或者将IP地址转换为国家代码的工具,并将其插入管道。排序程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 + +但是,使用stdin和stdout可以做什么是有限的。需要多个输入或输出的程序是可能的但棘手的。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,则无法将程序的输出传输到网络连接中【17,18】[^iii] 。 I/O由程序本身连接。它仍然可以配置(例如通过命令行选项),但是减少了在Shell中连接输入和输出的灵活性。 + +[^iii]: 除了使用一个单独的工具,如`netcat`或`curl`。 Unix开始试图将所有东西都表示为文件,但是BSD套接字API偏离了这个惯例【17】。研究用操作系统Plan 9和Inferno在使用文件方面更加一致:它们将TCP连接表示为`/net/tcp`中的文件【18】。 @@ -186,14 +187,15 @@ Unix工具的另一个特点是使用标准输入(stdin)和标准输出(st 使Unix工具如此成功的部分原因是它们使得查看正在发生的事情变得非常容易: -Unix命令的输入文件通常被视为不可变的。这意味着您可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 +* Unix命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 -* 您可以在任何时候结束管道,将输出管道输送到较少的位置,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 -* 您可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使您可以重新启动后面的阶段,而无需重新运行整个管道。 + +* 你可以在任何时候结束管道,将管道输出到`less`,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 +* 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。 因此,与关系数据库的查询优化器相比,即使Unix工具非常简单,工具简单,但仍然非常有用,特别是对于实验而言。 -然而,Unix工具的最大局限在于它们只能在一台机器上运行 - 而Hadoop这样的工具就是在这里工作的。 +然而,Unix工具的最大局限在于它们只能在一台机器上运行 —— 而Hadoop这样的工具即为此而生。 @@ -207,90 +209,91 @@ MapReduce有点像Unix工具,但分布在数千台机器上。像Unix工具一 和大多数Unix工具一样,运行MapReduce作业通常不会修改输入,除了生成输出外没有任何副作用。输出文件以连续的方式写入一次(一旦写入文件,不会修改任何现有的文件部分)。 -虽然Unix工具使用stdin和stdout作为输入和输出,但MapReduce作业在分布式文件系统上读写文件。在Hadoop的Map-Reduce实现中,该文件系统被称为HDFS(Hadoop分布式文件系统),一个开源的重新实现Google文件系统(GFS)[19]。 +虽然Unix工具使用stdin和stdout作为输入和输出,但MapReduce作业在分布式文件系统上读写文件。在Hadoop的Map-Reduce实现中,该文件系统被称为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,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用删除编码,局部优势将会丢失,因为来自多台机器的数据必须进行合并以重建原始文件[20]。 +[^iv]: 一个不同之处在于,对于HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用删除编码,局部优势将会丢失,因为来自多台机器的数据必须进行合并以重建原始文件【20】。 -与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则(参见第二部分的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 +与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则(参见[第二部分](part-ii.md)的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 HDFS包含在每台机器上运行的守护进程,暴露一个允许其他节点访问存储在该机器上的文件的网络服务(假设数据中心中的每台通用计算机都附带有一些磁盘)。名为NameNode的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS在概念上创建一个可以使用运行守护进程的所有机器的磁盘上的空间的大文件系统。 -为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如第5章中所述,或者像Reed-Solomon代码这样的擦除编码方案,它允许以比完全复制更低的存储开销恢复丢失的数据[20, 22。这些技术与RAID相似,可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 +为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如[第5章](ch5.md)中所述,或者像Reed-Solomon代码这样的擦除编码方案,它允许以比完全复制更低的存储开销恢复丢失的数据【20,22】。这些技术与RAID相似,可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 -HDFS已经很好地扩展了:在撰写本文时,最大的HDFS部署运行在成千上万台机器上,总存储容量达数百peta-bytes [23]。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于专用存储设备上的同等容量[24]。 +HDFS已经很好地扩展了:在撰写本文时,最大的HDFS部署运行在成千上万台机器上,总存储容量达数百PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于专用存储设备上的同等容量【24】。 ### MapReduce作业执行 -MapReduce是一个编程框架,您可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。理解它的最简单方法是参考第391页上的“简单日志分析”中的Web服务器日志分析示例。MapReduce中的数据处理模式与此示例非常相似: +MapReduce是一个编程框架,你可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。理解它的最简单方法是参考“[简单日志分析](#简单日志分析)”中的Web服务器日志分析示例。MapReduce中的数据处理模式与此示例非常相似: -1. 读取一组输入文件,并将其分解成记录。在Web服务器日志示例中,每条记录都是日志中的一行(即\ n是记录分隔符)。 -2. 调用Mapper函数从每个输入记录中提取一个键和值。在前面的例子中,mapper函数是`awk'{print $ 7}'`:它提取`URL($7)`作为关键字,并将值保留为空。 +1. 读取一组输入文件,并将其分解成记录。在Web服务器日志示例中,每条记录都是日志中的一行(即`\n`是记录分隔符)。 +2. 调用Mapper函数从每个输入记录中提取一个键和值。在前面的例子中,mapper函数是`awk'{print $7}'`:它提取`URL($7)`作为关键字,并将值保留为空。 3. 按键排序所有的键值对。在日志示例中,这由第一个排序命令完成。 -4. 调用reducer函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,reducer是由uniq -c命令实现的,该命令使用相同的密钥来统计相邻记录的数量。 +4. 调用reducer函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,reducer是由`uniq -c`命令实现的,该命令使用相同的键来统计相邻记录的数量。 -这四个步骤可以由一个MapReduce作业执行。步骤2(地图)和4(减少)是您编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 - 您不必编写它,因为映射器的输出始终在给予reducer之前进行排序。 +这四个步骤可以由一个MapReduce作业执行。步骤2(Map)和4(Reduce)是你编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 —— 你不必编写它,因为Mapper的输出始终在送往reducer之前进行排序。 -要创建MapReduce作业,您需要实现两个回调函数,mapper和reducer,其行为如下(另请参阅“MapReduce查询”(第46页)): +要创建MapReduce作业,你需要实现两个回调函数,mapper和reducer,其行为如下(参阅“[MapReduce查询](ch2.md#MapReduce查询)”): ***Mapper*** -每个输入记录都会调用一次映射器,其工作是从输入记录中提取键和值。对于每个输入,它可以生成任意数量的键值对(包括无)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 +每个输入记录都会调用一次Mapper,其工作是从输入记录中提取键和值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 ***Reducer*** -MapReduce框架采用由映射器生成的键值对,收集属于同一个键的所有值,并使用迭代器调用reducer以调用该值集合。 Reducer可以产生输出记录(例如相同URL的出现次数)。 +MapReduce框架采用由Mapper生成的键值对,收集属于同一个键的所有值,并使用迭代器调用reducer以调用该值集合。 Reducer可以产生输出记录(例如相同URL的出现次数)。 -在Web服务器日志示例中,我们在第5步中有第二个排序命令,它按请求数对URL进行排序。在MapReduce中,如果您需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,映射器的作用是将数据放入一个适合排序的表单中,并且还原器的作用是处理已排序的数据。 +在Web服务器日志示例中,我们在第5步中有第二个排序命令,它按请求数对URL进行排序。在MapReduce中,如果你需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper的作用是将数据放入一个适合排序的表单中,并且Reducer的作用是处理已排序的数据。 #### 分布式执行MapReduce -Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来明确处理并行性。映射器和简化器一次只能处理一条记录;他们不需要知道他们的输入来自哪里或者输出什么地方,所以框架可以处理机器之间移动数据的复杂性。 +Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来明确处理并行性。Mapper和Reducer一次只能处理一条记录;他们不需要知道他们的输入来自哪里或者输出什么地方,所以框架可以处理机器之间移动数据的复杂性。 -在分布式计算中可以使用标准的Unix工具作为映射器和简化器[25],但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,映射器和简化器都是实现特定接口的Java类。在MongoDB和CouchDB中,映射器和简化器都是JavaScript函数(请参阅第46页的“MapReduce查询”)。 +在分布式计算中可以使用标准的Unix工具作为Mapper和Reducer【25】,但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,Mapper和Reducer都是实现特定接口的Java类。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函数(参阅“[MapReduce查询](ch2.md#MapReduce查询)”)。 -[图10-1]()显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(参见第6章):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务(图10-1中的m 1,m 2和m 3标记)。 +[图10-1]()显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(参见[第6章](ch6.md)):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务([图10-1](img/fig10-1.png)中的m1,m2和m3标记)。 -每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个映射器,只要该机器有足够的备用RAM和CPU资源来运行映射任务[26]。这个原则被称为将数据放在数据附近[27]:它节省了通过网络复制输入文件,减少网络负载和增加局部性。 +每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个Mapper,只要该机器有足够的备用RAM和CPU资源来运行映射任务【26】。这个原则被称为将数据放在数据附近【27】:它节省了通过网络复制输入文件,减少网络负载和增加局部性。 ![](img/fig10-1.png) **图10-1 具有三个Mapper和三个Reducer的MapReduce任务** -在大多数情况下,应该在映射任务中运行的应用程序代码在分配运行它的任务的计算机上还不存在,所以MapReduce框架首先复制代码(例如Java程序中的JAR文件)到适当的机器。然后启动地图任务并开始读取输入文件,一次将一条记录传递给mapper回调。映射器的输出由键值对组成。 +在大多数情况下,应该在映射任务中运行的应用程序代码在分配运行它的任务的计算机上还不存在,所以MapReduce框架首先复制代码(例如Java程序中的JAR文件)到适当的机器。然后启动Map任务并开始读取输入文件,一次将一条记录传递给Mapper回调。Mapper的输出由键值对组成。 -计算的减少方面也被分割。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于地图任务的数量)。为了确保具有相同密钥的所有键值对在相同的缩减器处结束,框架使用密钥的散列值来确定哪个减少的任务应该接收到特定的键值对(参见“通过密钥散列分区”)第203页)。 +计算的减少方面也被分割。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对在相同的Reducer处结束,框架使用键的散列值来确定哪个减少的任务应该接收到特定的键值对(参见“[按键散列分区](ch6.md#按键散列分区)”))。 -键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先,每个映射任务都基于密钥的散列,通过简化器分割其输出。这些分区中的每一个都被写入映射程序本地磁盘上的已排序文件,使用的技术与我们在第76页的“SSTables and LSM-Trees”中讨论的类似。 +键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先,每个映射任务都基于键的散列,通过Reducer分割其输出。这些分区中的每一个都被写入映射程序本地磁盘上的已排序文件,使用的技术与我们在“[SSTables与LSM树](ch3.md#SSTables与LSM树)”中讨论的类似。 -只要映射器完成读取输入文件并写入其排序后的输出文件,MapReduce调度器就会通知减速器他们可以从该映射器开始获取输出文件。减法器连接到每个映射器,并为其分区下载排序后的键值对的文件。通过简化,分类和将数据分区从映射器复制到简化器的过程被称为混洗[26](一个令人困惑的术语 - 不像洗牌一样,在MapReduce中没有随机性)。 +只要Mapper完成读取输入文件并写入其排序后的输出文件,MapReduce调度器就会通知减速器他们可以从该Mapper开始获取输出文件。减法器连接到每个Mapper,并为其分区下载排序后的键值对的文件。通过简化,分类和将数据分区从Mapper复制到Reducer的过程被称为**混洗(shuffle)**【26】(一个令人困惑的术语 —— 不像洗牌一样,在MapReduce中没有随机性)。 -reduce任务从映射器获取文件并将它们合并在一起,并保存排序顺序。因此,如果不同的映射器使用相同的键生成记录,则它们将在合并的缩减器输入中相邻。 +reduce任务从Mapper获取文件并将它们合并在一起,并保存排序顺序。因此,如果不同的Mapper使用相同的键生成记录,则它们将在合并的Reducer输入中相邻。 使用一个键和一个迭代器调用reducer,迭代器使用相同的键(在某些情况下可能不是全部适合内存)逐步扫描所有记录。 Reducer可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录写入到分布式文件系统上的文件(通常是运行reducer的机器的本地磁盘上的一个副本,其他机器上的副本)。 -MapReduce工作流程 -单个MapReduce作业可以解决的问题范围有限。请参阅日志分析示例,一个MapReduce作业可以确定每个URL的页面浏览次数,但不是最常用的URL,因为这需要第二轮排序。 +#### MapReduce工作流 + +单个MapReduce作业可以解决的问题范围有限。参阅日志分析示例,一个MapReduce作业可以确定每个URL的页面浏览次数,但不是最常用的URL,因为这需要第二轮排序。 因此,将MapReduce作业链接到工作流中是非常常见的,例如,一个作业的输出成为下一个作业的输入。 Hadoop Map-Reduce框架对工作流程没有特别的支持,所以这个链接是通过目录名隐含完成的:第一个作业必须被配置为将其输出写入HDFS中的指定目录,第二个作业必须是配置为读取与其输入相同的目录名称。从MapReduce框架的角度来看,他们是两个独立的工作。 -因此,被链接的MapReduce作业不如Unix命令的流水线(它直接将一个进程的输出作为输入传递给另一个进程,只使用一个小的内存缓冲区),更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利有弊,我们将在第419页“中间状态的物化”中讨论。 +因此,被链接的MapReduce作业不如Unix命令的流水线(它直接将一个进程的输出作为输入传递给另一个进程,只使用一个小的内存缓冲区),更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利有弊,我们将在“[物化中间状态](#物化中间状态)”中讨论。 -当作业成功完成时,批处理作业的输出仅被视为有效(MapReduce丢弃失败作业的部分输出)。因此,工作流程中的一项工作只有在先前的工作 - 即生产其投入方向的工作 - 成功完成时才能开始。处理这些作业之间的依赖关系执行,为Hadoop开发了各种工作流调度器,包括Oozie,Azkaban,Luigi,Airflow和Pinball [28]。 +当作业成功完成时,批处理作业的输出仅被视为有效(MapReduce丢弃失败作业的部分输出)。因此,工作流程中的一项工作只有在先前的工作 —— 即生产其投入方向的工作 —— 成功完成时才能开始。处理这些作业之间的依赖关系执行,为Hadoop开发了各种工作流调度器,包括Oozie,Azkaban,Luigi,Airflow和Pinball 【28】。 -这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统[29]时,由50到100个MapReduce作业组成的工作流是常见的,而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流非常重要。 +这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统【29】时,由50到100个MapReduce作业组成的工作流是常见的,而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流非常重要。 -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端连接与分组 -我们在第2章中讨论了数据模型和查询语言的联接,但是我们还没有深入探讨联接是如何实现的。现在是我们再次拿起那个线程的时候了。 +我们在[第2章](ch2.md)中讨论了数据模型和查询语言的联接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次拿起那个线程的时候了。 -在许多数据集中,通常一条记录与另一条记录有关联:关系模型中的外键,文档模型中的文档引用或图模型中的边。只要有一些代码需要访问该关联两边的记录(包含引用的记录和被引用的记录),连接就是必需的。正如第2章所讨论的,非规范化可以减少对连接的需求,但通常不会将其完全移除[^v]。 +在许多数据集中,通常一条记录与另一条记录有关联:关系模型中的外键,文档模型中的文档引用或图模型中的边。只要有一些代码需要访问该关联两边的记录(包含引用的记录和被引用的记录),连接就是必需的。正如[第2章](ch2.md)所讨论的,非规范化可以减少对连接的需求,但通常不会将其完全移除[^v]。 -在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用索引来快速定位感兴趣的记录(请参阅第3章)。如果查询涉及连接,则可能需要多个索引查找。然而,MapReduce没有索引的概念 - 至少不是通常意义上的。 +在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用索引来快速定位感兴趣的记录(参阅[第3章](ch3.md))。如果查询涉及连接,则可能需要多个索引查找。然而,MapReduce没有索引的概念 —— 至少不是通常意义上的。 -当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;一个数据库会调用这个操作一个全表扫描。如果您只想读取少量的记录,则与索引查找相比,全表扫描的成本非常高昂。但是,在分析查询中(请参阅第88页上的“事务处理或分析?”),通常需要计算大量记录的聚合。在这种情况下,扫描整个输入可能是相当合理的事情,特别是如果可以在多台机器上并行处理。 +当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;一个数据库会调用这个操作一个全表扫描。如果你只想读取少量的记录,则与索引查找相比,全表扫描的成本非常高昂。但是,在分析查询中(参阅“[事务处理或分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,扫描整个输入可能是相当合理的事情,特别是如果可以在多台机器上并行处理。 [^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录与其他记录在特定字段(例如ID)中具有相同的值相关联。有些数据库支持更一般的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有空间来覆盖它们。 @@ -298,7 +301,7 @@ Hadoop的各种高级工具(如Pig [30],Hive [31],Cascading [32],Crunch #### 示例:分析用户活动事件 -图10-2给出了一个批处理作业中加入典型的例子。 在左侧是事件日志,描述登录用户在网站上做的事情(称为活动事件或点击流数据),右侧是用户数据库。 您可以将此示例看作是星型模式的一部分(请参阅“星号和雪花:分析的示意图”(第93页)):事件日志是事实表,用户数据库是其中一个尺寸。 +[图10-2](img/fig10-2.png)给出了一个批处理作业中加入典型的例子。 在左侧是事件日志,描述登录用户在网站上做的事情(称为活动事件或点击流数据),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中一个纬度。 ![](img/fig10-2.png) @@ -306,183 +309,184 @@ Hadoop的各种高级工具(如Pig [30],Hive [31],Cascading [32],Crunch 分析任务可能需要将用户活动与用户简档信息相关联:例如,如果简档包含用户的年龄或出生日期,则系统可以确定哪些年龄组最受欢迎。但是,活动事件仅包含用户标识,而不包含完整的用户配置文件信息。在每一个活动事件中嵌入这个简介信息很可能是非常浪费的。因此,活动事件需要加入用户配置文件数据库。 -这个连接的最简单实现将逐个遍历活动事件,并为每个遇到的用户ID查询用户数据库(在远程服务器上)。这是可能的,但是它很可能会遭受非常差的性能:处理吞吐量将受到数据库服务器的往返时间的限制,本地缓存的有效性将很大程度上取决于数据的分布,并行运行大量查询可能会轻易压倒数据库[35]。 +这个连接的最简单实现将逐个遍历活动事件,并为每个遇到的用户ID查询用户数据库(在远程服务器上)。这是可能的,但是它很可能会遭受非常差的性能:处理吞吐量将受到数据库服务器的往返时间的限制,本地缓存的有效性将很大程度上取决于数据的分布,并行运行大量查询可能会轻易压倒数据库【35】。 -为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)在一台机器上进行。通过网络为您要处理的每个记录进行随机访问请求太慢。而且,查询远程数据库意味着批处理作业变得不确定,因为远程数据库中的数据可能会改变。 +为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)在一台机器上进行。通过网络为你要处理的每个记录进行随机访问请求太慢。而且,查询远程数据库意味着批处理作业变得不确定,因为远程数据库中的数据可能会改变。 -因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,请参阅第91页上的“数据仓库”),并将其放入与日志相同的分布式文件系统用户活动事件。然后,您可以将用户数据库存储在HDFS中的一组文件中,并将用户活动记录在另一组文件中,并且可以使用MapReduce将所有相关记录集中到同一地点并高效地处理它们。 +因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,参阅“[数据仓库](ch3.md#数据仓库)”),并将其放入与日志相同的分布式文件系统用户活动事件。然后,你可以将用户数据库存储在HDFS中的一组文件中,并将用户活动记录在另一组文件中,并且可以使用MapReduce将所有相关记录集中到同一地点并高效地处理它们。 #### 排序合并连接 -回想一下,映射器的目的是从每个输入记录中提取一个键和值。在图10-2的情况下,这个键就是用户ID:一组映射器会覆盖活动事件(提取用户ID作为键和活动事件作为值),而另一组映射器将会检查用户数据库(提取用户ID作为键和用户的出生日期作为值)。这个过程如图10-3所示。 +回想一下,Mapper的目的是从每个输入记录中提取一个键和值。在[图10-2](img/fig10-2.png)的情况下,这个键就是用户ID:一组Mapper会覆盖活动事件(提取用户ID作为键和活动事件作为值),而另一组Mapper将会检查用户数据库(提取用户ID作为键和用户的出生日期作为值)。这个过程如[图10-3](img/fig10-3.png)所示。 ![](img/fig10-3.png) **图10-3 Reduce端在user ID上进行归并排序连接,如果输入数据集分片成多个文件,则每个都会被多个Mapper并行处理** 当MapReduce框架通过key对mapper输出进行分区,然后对键值对进行排序时,效果是所有活动事件和用户ID相同的用户记录在reducer输入中彼此相邻。 Map-Reduce作业甚至可以安排记录进行排序,使减速器始终如一 -首先从用户数据库中查看记录,然后按照时间戳顺序查看活动事件 - 这种技术被称为次级排序[26]。 +首先从用户数据库中查看记录,然后按照时间戳顺序查看活动事件 —— 这种技术被称为次级排序【26】。 -然后reducer可以很容易地执行实际的加入逻辑:每个用户ID调用一次reducer函数,并且由于二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出已观看网址和观看者年龄对。随后的Map- Reduce作业可以计算每个URL的查看者年龄分布,并按年龄组进行聚类。 +然后reducer可以很容易地执行实际的加入逻辑:每个用户ID调用一次reducer函数,并且由于二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出已观看网址和观看者年龄对。随后的Map-Reduce作业可以计算每个URL的查看者年龄分布,并按年龄组进行聚类。 -由于reducer一次处理一个特定用户ID的所有记录,因此只需要一次将一个用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为排序合并连接,因为映射器输出是按键排序的,然后缩减器将来自连接两边的排序的记录列表合并在一起。 +由于reducer一次处理一个特定用户ID的所有记录,因此只需要一次将一个用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为排序合并连接,因为Mapper输出是按键排序的,然后Reducer将来自连接两边的排序的记录列表合并在一起。 #### 把相关数据放在一起 -在排序合并连接中,映射器和排序过程确保将执行特定用户标识的连接操作的所有必需数据放在一起:一次调用reducer。预先排列了所有需要的数据,reducer可以是一个相当简单,单线程的代码,可以通过高吞吐量和低内存开销通过记录。 +在排序合并连接中,Mapper和排序过程确保将执行特定用户标识的连接操作的所有必需数据放在一起:一次调用reducer。预先排列了所有需要的数据,reducer可以是一个相当简单,单线程的代码,可以通过高吞吐量和低内存开销通过记录。 -查看这种体系结构的一种方法是映射器将“消息”发送给reducer。当一个映射器发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使密钥只是一个任意的字符串(不是像IP地址和端口号那样的实际的网络地址),它的行为就像一个地址:所有具有相同密钥的密钥对将被传送到相同的目标(a呼叫减速机)。 +查看这种体系结构的一种方法是Mapper将“消息”发送给reducer。当一个Mapper发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使键只是一个任意的字符串(不是像IP地址和端口号那样的实际的网络地址),它的行为就像一个地址:所有具有相同键的键对将被传送到相同的目标(一次Reduce的调用)。 -使用MapReduce编程模型将计算的物理网络通信方面(从正确的计算机获取数据)从应用程序逻辑中分离出来(处理完数据后)。这种分离与数据库的典型使用形成了鲜明的对比,从数据库中获取数据的请求经常发生在应用程序代码的深处[36]。由于MapReduce能够处理所有的网络通信,因此它也避免了应用程序代码担心部分故障,例如另一个节点的崩溃:MapReduce在不影响应用程序逻辑的情况下,透明地重试失败的任务。 +使用MapReduce编程模型将计算的物理网络通信方面(从正确的计算机获取数据)从应用程序逻辑中分离出来(处理完数据后)。这种分离与数据库的典型使用形成了鲜明的对比,从数据库中获取数据的请求经常发生在应用程序代码的深处【36】。由于MapReduce能够处理所有的网络通信,因此它也避免了应用程序代码担心部分故障,例如另一个节点的崩溃:MapReduce在不影响应用程序逻辑的情况下,透明地重试失败的任务。 ### GROUP BY -除了连接之外,“将相关数据引入同一地点”模式的另一个常见用法是通过某个键(如SQL中的GROUP BY子句)对记录进行分组。所有用相同的密钥记录一个组,并且下一步往往是在每个组内进行某种聚合,例如: +除了连接之外,“将相关数据引入同一地点”模式的另一个常见用法是通过某个键(如SQL中的GROUP BY子句)对记录进行分组。所有用相同的键记录一个组,并且下一步往往是在每个组内进行某种聚合,例如: -* 计算每个组中记录的数量(例如,在统计页面视图的示例中,您将在SQL中表示为COUNT(*)聚合) -* 在SQL中的一个特定字段(SUM(fieldname))中添加值 +* 计算每个组中记录的数量(例如,在统计页面视图的示例中,你将在SQL中表示为COUNT(*)聚合) +* 在SQL中的一个特定字段(`SUM(fieldname)`)中添加值 * 根据某些排名函数选择前k个记录 -使用MapReduce实现这种分组操作的最简单方法是设置映射器,以便它们生成的键值对使用所需的分组键。然后,分区和排序过程将所有记录与同一个缩减器中的相同键集合在一起。因此,在MapReduce上实现时,分组和连接看起来非常相似。 +使用MapReduce实现这种分组操作的最简单方法是设置Mapper,以便它们生成的键值对使用所需的分组键。然后,分区和排序过程将所有记录与同一个Reducer中的相同键集合在一起。因此,在MapReduce上实现时,分组和连接看起来非常相似。 -分组的另一个常见用途是整理特定用户会话的所有活动事件,以便找出用户采取的一系列操作(称为会话化[37])。例如,可以使用这种分析来确定显示网站新版本的用户是否比那些显示旧版本(A / B测试)的用户更有可能进行购买,或计算某个营销活动是值得的。 +分组的另一个常见用途是整理特定用户会话的所有活动事件,以便找出用户采取的一系列操作(称为会话化【37】)。例如,可以使用这种分析来确定显示网站新版本的用户是否比那些显示旧版本(A/B测试)的用户更有可能进行购买,或计算某个营销活动是值得的。 -如果您有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。您可以通过使用会话cookie,用户ID或类似的标识符作为分组键来实现会话,并将特定用户的所有活动事件放在一起,同时将不同用户的事件分配到不同的分区。 +如果你有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话cookie,用户ID或类似的标识符作为分组键来实现会话,并将特定用户的所有活动事件放在一起,同时将不同用户的事件分配到不同的分区。 #### 处理倾斜 -如果存在与单个密钥相关的大量数据,则“将具有相同密钥的所有记录带到相同位置”的模式被破坏。例如,在社交网络中,大多数用户可能会连接到几百人,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为关键对象[38]或热键。 +如果存在与单个键相关的大量数据,则“将具有相同键的所有记录带到相同位置”的模式被破坏。例如,在社交网络中,大多数用户可能会连接到几百人,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为关键对象【38】或热键。 -在单个缩减器中收集与名人相关的所有活动(例如回复他们发布的内容)可能导致严重的偏差(也称为热点) - 也就是说,一个减速器必须处理比其他更多的记录(参见“歪曲的工作负载和消除热点“)。由于MapReduce作业只有在其所有映射器和缩减器都完成时才完成,所有后续作业必须等待最慢的缩减器才能启动。 +在单个Reducer中收集与名人相关的所有活动(例如回复他们发布的内容)可能导致严重的倾斜(也称为热点)—— 也就是说,一个减速器必须处理比其他更多的记录(参见“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)“)。由于MapReduce作业只有在其所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。 -如果加入输入有热键,则可以使用一些算法进行补偿。例如,Pig中的偏斜连接方法首先运行一个抽样作业来确定哪些键是热的[39]。执行实际加入时,映射器发送任何随机选择一个与几个减速器之一相关的热键的记录(与传统的MapReduce相比,它选择一个基于密钥散列确定性的减速器)。对于加入的其他输入,与热键相关的记录需要被复制到所有处理该密钥的缩减器[40]。 +如果加入输入有热点键,则可以使用一些算法进行补偿。例如,Pig中的倾斜连接方法首先运行一个抽样作业来确定哪些键是热的【39】。执行实际加入时,Mapper发送任何随机选择一个与几个减速器之一相关的热键的记录(与传统的MapReduce相比,它选择一个基于键散列确定性的减速器)。对于加入的其他输入,与热键相关的记录需要被复制到所有处理该键的Reducer【40】。 -这种技术将处理热键的工作分散到多个reducer上,这样可以使其更好地并行化,而不必将其他join连接复制到多个reducer。 Crunch中的分片连接方法是相似的,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在第205页的“倾斜的工作负载和减轻热点”中讨论的技术,使用随机化来缓解分区数据库中的热点。 +这种技术将处理热键的工作分散到多个reducer上,这样可以使其更好地并行化,而不必将其他join连接复制到多个reducer。 Crunch中的分片连接方法是相似的,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)”中讨论的技术,使用随机化来缓解分区数据库中的热点。 -Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中明确指定热键,并将与这些键相关的记录与其余文件分开存放。在该表上执行连接时,它将使用地图边连接(请参阅下一节)获取热键。 +Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中明确指定热键,并将与这些键相关的记录与其余文件分开存放。在该表上执行连接时,它将使用Map边连接(参阅下一节)获取热键。 -使用热键对记录进行分组并汇总记录时,可以分两个阶段进行分组。第一个MapReduce阶段将记录发送到随机缩减器,以便每个缩减器对热键的记录子集执行分组,并为每个键输出更紧凑的聚合值。第二个Map-Reduce作业然后将来自所有第一阶段减速器的值合并为每个键的单个值。 +使用热键对记录进行分组并汇总记录时,可以分两个阶段进行分组。第一个MapReduce阶段将记录发送到随机Reducer,以便每个Reducer对热键的记录子集执行分组,并为每个键输出更紧凑的聚合值。第二个Map-Reduce作业然后将来自所有第一阶段减速器的值合并为每个键的单个值。 ### Map端连接 -上一节描述的连接算法在reducer中执行实际的连接逻辑,因此被称为reduce-side连接。映射器扮演着输入数据的角色:从每个输入记录中提取键和值,将键值对分配给reducer分区,并按键排序。 +上一节描述的连接算法在reducer中执行实际的连接逻辑,因此被称为reduce-side连接。Mapper扮演着输入数据的角色:从每个输入记录中提取键和值,将键值对分配给reducer分区,并按键排序。 -减少方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,映射器都可以准备数据以准备加入。然而,不利的一面是,所有这些排序,复制到缩减器以及合并减速器输入可能是非常昂贵的。取决于可用的内存缓冲区,当数据通过MapReduce [37]阶段时,数据可能被写入磁盘几次。 +减少方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper都可以准备数据以准备加入。然而,不利的一面是,所有这些排序,复制到Reducer以及合并减速器输入可能是非常昂贵的。取决于可用的内存缓冲区,当数据通过MapReduce 【37】阶段时,数据可能被写入磁盘几次。 -另一方面,如果您可以对输入数据进行某些假设,则可以通过使用所谓的map端连接来加快连接速度。这种方法使用了一个缩减的MapReduce作业,其中没有减速器,也没有排序。相反,每个映射器只需从分布式文件系统读取一个输入文件块,然后将一个输出文件写入文件系统即可。 +另一方面,如果你可以对输入数据进行某些假设,则可以通过使用所谓的map端连接来加快连接速度。这种方法使用了一个Reduce的MapReduce作业,其中没有减速器,也没有排序。相反,每个Mapper只需从分布式文件系统读取一个输入文件块,然后将一个输出文件写入文件系统即可。 #### 广播散列连接 -执行地图边连接最简单的方法适用于大数据集与小数据集连接的情况。特别是,小数据集需要足够小,以便可以将其全部加载到每个映射器的内存中。 +执行Map端连接最简单的方法适用于大数据集与小数据集连接的情况。特别是,小数据集需要足够小,以便可以将其全部加载到每个Mapper的内存中。 -例如,假设在图10-2的情况下,用户数据库足够小以适应内存。在这种情况下,当映射器启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的哈希表中。完成此操作后,映射程序可以扫描用户活动事件,并简单地查找散列表中每个事件的用户标识[^vi]。 +例如,假设在[图10-2](img/fig10-2.png)的情况下,用户数据库足够小以适应内存。在这种情况下,当Mapper启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的哈希表中。完成此操作后,Map程序可以扫描用户活动事件,并简单地查找散列表中每个事件的用户标识[^vi]。 [^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户ID唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,并且连接运算符将输出关键字的所有匹配。 -仍然可以有几个映射任务:一个用于连接的大输入的每个文件块(在图10-2的例子中,活动事件是大输入)。这些映射器中的每一个都将小输入全部加载到内存中。 +仍然可以有几个映射任务:一个用于连接的大输入的每个文件块(在[图10-2](img/fig10-2.png)的例子中,活动事件是大输入)。这些Mapper中的每一个都将小输入全部加载到内存中。 -这种简单而有效的算法被称为广播散列连接:广播词反映了这样一个事实,即大输入的分区的每个映射器都读取整个小输入(所以小输入有效地“广播”到大的输入),单词hash反映了它使用一个哈希表。 Pig(名为“replicated join”),Hive(“MapJoin”),Cascading和Crunch支持此连接方法。它也用于数据仓库查询引擎,如Impala [41]。 +这种简单而有效的算法被称为广播散列连接:广播一词反映了这样一个事实,即大输入的分区的每个Mapper都读取整个小输入(所以小输入有效地“广播”到大的输入),单词hash反映了它使用一个哈希表。 Pig(名为“replicated join”),Hive(“MapJoin”),Cascading和Crunch支持此连接方法。它也用于数据仓库查询引擎,如Impala 【41】。 -而不是将小连接输入加载到内存散列表中,另一种方法是将小连接输入存储在本地磁盘上的只读索引中[42]。该索引中经常使用的部分将保留在操作系统的页面缓存中,因此这种方法可以提供与内存中哈希表几乎一样快的随机访问查找,但实际上并不需要数据集适合内存。 +而不是将小连接输入加载到内存散列表中,另一种方法是将小连接输入存储在本地磁盘上的只读索引中【42】。该索引中经常使用的部分将保留在操作系统的页面缓存中,因此这种方法可以提供与内存中哈希表几乎一样快的随机访问查找,但实际上并不需要数据集适合内存。 #### 分区散列连接 -如果以相同方式对映射端连接的输入进行分区,则散列连接方法可以独立应用于每个分区。在图10-2的情况下,您可以根据用户标识的最后一位十进制数字来安排活动事件和用户数据库的每一个(因此每边有10个分区)。例如,映射器3首先将所有具有以3结尾的ID的用户加载到散列表中,然后扫描ID为3的每个用户的所有活动事件。 +如果以相同方式对映射端连接的输入进行分区,则散列连接方法可以独立应用于每个分区。在[图10-2](img/fig10-2.png)的情况下,你可以根据用户标识的最后一位十进制数字来安排活动事件和用户数据库的每一个(因此每边有10个分区)。例如,Mapper3首先将所有具有以3结尾的ID的用户加载到散列表中,然后扫描ID为3的每个用户的所有活动事件。 -如果分区正确完成,您可以确定所有您可能要加入的记录都位于相同编号的分区中,因此每个映射器只能从每个输入数据集中读取一个分区就足够了。这具有的优点是每个映射器都可以将较少量的数据加载到其哈希表中。 +如果分区正确完成,你可以确定所有你可能要加入的记录都位于相同编号的分区中,因此每个Mapper只能从每个输入数据集中读取一个分区就足够了。这具有的优点是每个Mapper都可以将较少量的数据加载到其哈希表中。 -这种方法只适用于两个连接的输入具有相同数量的分区,记录根据相同的密钥和相同的散列函数分配给分区。如果输入是由之前执行过这个分组的MapReduce作业生成的,那么这可能是一个合理的假设。 +这种方法只适用于两个连接的输入具有相同数量的分区,记录根据相同的键和相同的散列函数分配给分区。如果输入是由之前执行过这个分组的MapReduce作业生成的,那么这可能是一个合理的假设。 -分区散列连接在Hive [37]中称为bucketed映射连接。地图边合并连接 +分区散列连接在Hive 【37】中称为bucketed映射连接。Map端合并连接 -如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行排序,则应用另一种地图端联接的变体。在这种情况下,输入是否足够小以适应内存并不重要,因为映射器可以执行通常由reducer执行的相同合并操作:按递增键递增读取两个输入文件,以及匹配相同的密钥记录。 +如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行排序,则应用另一种Map端联接的变体。在这种情况下,输入是否足够小以适应内存并不重要,因为Mapper可以执行通常由reducer执行的相同合并操作:按递增键递增读取两个输入文件,以及匹配相同的键记录。 -如果地图边合并连接是可能的,则可能意味着先前的MapReduce作业首先将输入数据集引入到这个分区和排序的表单中。原则上,这个加入可以在之前工作的缩减阶段进行。但是,在单独的仅用于地图的作业中执行合并连接仍然是适当的,例如,除了此特定连接之外,还需要分区和排序数据集以用于其他目的。 +如果Map端合并连接是可能的,则可能意味着先前的MapReduce作业首先将输入数据集引入到这个分区和排序的表单中。原则上,这个加入可以在之前工作的Reduce阶段进行。但是,在单独的仅用于Map的作业中执行合并连接仍然是适当的,例如,除了此特定连接之外,还需要分区和排序数据集以用于其他目的。 #### MapReduce与Map端连接的工作流程 当下游作业使用MapReduce连接的输出时,map-side或reduce-side连接的选择会影响输出的结构。 reduce-side连接的输出按连接键进行分区和排序,而map-side连接的输出按照与大输入相同的方式进行分区和排序(因为对每个文件块启动一个map任务无论是使用分区连接还是广播连接,连接的大输入)。 -如前所述,地图边连接也对输入数据集的大小,排序和分区做出了更多的假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;您还必须知道数据分区和排序的分区数量和密钥。 +如前所述,Map边连接也对输入数据集的大小,排序和分区做出了更多的假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据分区和排序的分区数量和键。 -在Hadoop生态系统中,这种关于数据集分区的元数据经常在HCatalog和Hive Metastore中维护[37]。 +在Hadoop生态系统中,这种关于数据集分区的元数据经常在HCatalog和Hive Metastore中维护【37】。 ### 工作流的输出 我们已经谈了很多关于实现MapReduce工作流程的各种算法,但是我们忽略了一个重要的问题:一旦完成,所有处理的结果是什么?我们为什么要把所有这些工作放在首位? -在数据库查询的情况下,我们根据分析目的来区分事务处理(OLTP)目的(请参阅第90页上的“事务处理或分析?”)。我们看到,OLTP查询通常使用索引按键查找少量记录,以便将其呈现给用户(例如,在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组和汇总,输出通常具有报告的形式:显示某个指标随时间变化的图表,或前10个项目根据一些排名,或一些数量分解成子类别。这种报告的消费者通常是需要做出商业决策的分析师或经理。 +在数据库查询的情况下,我们根据分析目的来区分事务处理(OLTP)目的(参阅“[事务处理或分析?](ch3.md#事务处理或分析?)”)。我们看到,OLTP查询通常使用索引按键查找少量记录,以便将其呈现给用户(例如,在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组和汇总,输出通常具有报告的形式:显示某个指标随时间变化的图表,或前10个项目根据一些排名,或一些数量分解成子类别。这种报告的消费者通常是需要做出商业决策的分析师或经理。 -批处理在哪里适合?这不是交易处理,也不是分析。与分析更接近,因为批处理过程通常扫描输入数据集的大部分。但是,MapReduce作业的工作流程与用于分析目的的SQL查询不同(请参阅第418页的“比较Hadoop与分布式数据库”)。批处理过程的输出通常不是报告,而是一些其他类型的结构。 +批处理在哪里适合?这不是交易处理,也不是分析。与分析更接近,因为批处理过程通常扫描输入数据集的大部分。但是,MapReduce作业的工作流程与用于分析目的的SQL查询不同(参阅“[比较Hadoop与分布式数据库](比较Hadoop与分布式数据库)”)。批处理过程的输出通常不是报告,而是一些其他类型的结构。 #### 建立搜索索引 -Google最初使用的MapReduce是为其搜索引擎建立索引,这个索引是作为5到10个MapReduce作业的工作流实现的[1]。虽然Google为了这个目的后来不再使用MapReduce [43],但是如果从建立搜索索引的角度来看,它可以帮助理解MapReduce。 (即使在今天,Hadoop MapReduce仍然是构建Lucene / Solr索引的好方法。) +Google最初使用的MapReduce是为其搜索引擎建立索引,这个索引是作为5到10个MapReduce作业的工作流实现的【1】。虽然Google为了这个目的后来不再使用MapReduce 【43】,但是如果从建立搜索索引的角度来看,它可以帮助理解MapReduce。 (即使在今天,Hadoop MapReduce仍然是构建Lucene / Solr索引的好方法。) -我们在第88页的“全文搜索和模糊索引”中简要地看到了Lucene这样的全文搜索索引是如何工作的:它是一个文件(术语字典),您可以在其中高效地查找特定关键字并找到包含该关键字的所有文档ID列表(发布列表)。这是一个非常简单的搜索索引视图 - 实际上,它需要各种附加数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等,但这一原则是成立的。 +我们在“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”中简要地看到了Lucene这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字并找到包含该关键字的所有文档ID列表(发布列表)。这是一个非常简单的搜索索引视图 —— 实际上,它需要各种附加数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等,但这一原则是成立的。 -如果需要对一组固定文档执行全文搜索,则批处理是构建索引的一种非常有效的方法:映射器根据需要对文档集进行分区,每个reducer构建其分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅“分区和二级索引”(第184页))并行处理非常好。 +如果需要对一组固定文档执行全文搜索,则批处理是构建索引的一种非常有效的方法:Mapper根据需要对文档集进行分区,每个reducer构建其分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(参阅“[分区和二级索引](ch6.md#分区和二级索引)”)并行处理非常好。 由于按关键字查询搜索索引是只读操作,因此这些索引文件一旦创建就是不可变的。 如果索引的文档集合发生更改,则可以选择定期重新运行整个索引工作流程,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法可能会带来很高的计算成本,但是它的优点是索引过程很容易推理:文档,索引。 -或者,可以逐渐建立索引。如第3章所述,如果要添加,删除或更新索引中的文档,Lucene会写出新的段文件,并异步合并和压缩背景中的段文件。我们将在第11章中看到更多这样的增量处理。 +或者,可以逐渐建立索引。如第3章所述,如果要添加,删除或更新索引中的文档,Lucene会写出新的段文件,并异步合并和压缩背景中的段文件。我们将在[第11章](ch11.md)中看到更多这样的增量处理。 #### 键值存储作为批处理输出 -搜索索引只是批处理工作流程可能输出的一个示例。批量处理的另一个常见用途是构建机器学习系统,如分类器(例如,垃圾邮件过滤器,异常检测,图像识别)和推荐系统(例如,您可能认识的人,您可能感兴趣的产品或相关搜索) ])。 +搜索索引只是批处理工作流程可能输出的一个示例。批量处理的另一个常见用途是构建机器学习系统,如分类器(例如,垃圾邮件过滤器,异常检测,图像识别)和推荐系统(例如,你可能认识的人,你可能感兴趣的产品或相关搜索)。 -这些批处理作业的输出通常是某种数据库:例如,可以通过用户ID查询以获取该用户的建议朋友的数据库,或者可以通过产品ID查询的数据库以获取相关产品[45]。 +这些批处理作业的输出通常是某种数据库:例如,可以通过用户ID查询以获取该用户的建议朋友的数据库,或者可以通过产品ID查询的数据库以获取相关产品【45】。 这些数据库需要从处理用户请求的Web应用程序中查询,这些请求通常与Hadoop基础架构分离。那么批处理过程的输出如何返回到Web应用程序可以查询的数据库? -最明显的选择可能是直接在映射器或简化器中使用客户端库作为您最喜欢的数据库,并从批处理作业直接写入数据库服务器,一次写入一条记录。这将起作用(假设您的防火墙规则允许从您的Hadoop环境直接访问您的生产数据库),但由于以下几个原因,这是一个坏主意: +最明显的选择可能是直接在Mapper或Reducer中使用客户端库作为你最喜欢的数据库,并从批处理作业直接写入数据库服务器,一次写入一条记录。这将起作用(假设你的防火墙规则允许从你的Hadoop环境直接访问你的生产数据库),但由于以下几个原因,这是一个坏主意: * 正如前面讨论的连接一样,为每个记录提出一个网络请求比批处理任务的正常吞吐量要慢几个数量级。即使客户端库支持批处理,性能也可能很差。 -* MapReduce作业经常并行运行许多任务。如果所有映射器或简化器都同时写入相同的输出数据库,并且批处理过程期望的速率,那么该数据库可能很容易被压倒,并且其查询性能可能受到影响。这可能会导致系统其他部分的操作问题[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的一个很好的使用方法:使用映射器提取一个键,然后使用该键进行排序已经成为构建索引所需的大量工作。由于大多数这些键值存储是只读的(文件只能由批处理作业一次写入,而且是不可变的),所以数据结构非常简单。例如,它们不需要WAL(请参阅第82页的「使B树可靠」)。 +构建这些数据库文件是MapReduce的一个很好的使用方法:使用Mapper提取一个键,然后使用该键进行排序已经成为构建索引所需的大量工作。由于大多数这些键值存储是只读的(文件只能由批处理作业一次写入,而且是不可变的),所以数据结构非常简单。例如,它们不需要WAL(参阅“[使B树可靠](ch3.md#使B树可靠)”)。 -将数据加载到Voldemort时,服务器将继续向旧数据文件提供请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动切换到查询新文件。如果在这个过程中出现任何问题,它可以很容易地再次切换回旧的文件,因为它们仍然存在,并且是不变的[46]。 +将数据加载到Voldemort时,服务器将继续向旧数据文件提供请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动切换到查询新文件。如果在这个过程中出现任何问题,它可以很容易地再次切换回旧的文件,因为它们仍然存在,并且是不变的【46】。 #### 批量过程输出的哲学 -本章前面讨论过的Unix哲学(第394页的“Unix哲学”)鼓励通过对数据流的非常明确的实验来进行实验:程序读取输入并写入输出。在这个过程中,输入保持不变,任何以前的输出都被新输出完全替换,并且没有其他副作用。这意味着您可以随心所欲地重新运行一个命令,调整或调试它,而不会扰乱系统的状态。 +本章前面讨论过的Unix哲学(“[Unix哲学](#Unix哲学)”)鼓励通过对数据流的非常明确的实验来进行实验:程序读取输入并写入输出。在这个过程中,输入保持不变,任何以前的输出都被新输出完全替换,并且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,调整或调试它,而不会扰乱系统的状态。 MapReduce作业的输出处理遵循相同的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护: -* 如果在代码中引入了一个错误,并且输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将再次正确。或者,甚至更简单,您可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的思想被称为人类容错[50]。) -* 由于易于回滚,功能开发可以比错误意味着不可挽回的损害的环境更快地进行。这种使不可逆性最小化的原则有利于敏捷软件的开发[51]。 +* 如果在代码中引入了一个错误,并且输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将再次正确。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的思想被称为人类容错【50】。) +* 由于易于回滚,功能开发可以比错误意味着不可挽回的损害的环境更快地进行。这种使不可逆性最小化的原则有利于敏捷软件的开发【51】。 * 如果映射或减少任务失败,MapReduce框架将自动重新调度并在同一个输入上再次运行它。如果失败是由于代码中的一个错误造成的,那么它会一直崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于暂时的问题引起的,那么故障是可以容忍的。这种自动重试只是安全的,因为输入是不可变的,而失败任务的输出被MapReduce框架丢弃。 * 同一组文件可用作各种不同作业的输入,其中包括计算度量标准的计算作业,并评估作业的输出是否具有预期的特性(例如,将其与前一次运行的输出进行比较并测量差异) 。 * 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分开,这就提供了关注点的分离,并且可以重用代码:一个团队可以专注于实现一件好事的工作其他团队可以决定何时何地运行这项工作。 -在这些领域,对Unix运行良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假定没有类型的文本文件,所以他们必须做大量的输入解析(本章开头的日志分析示例使用{print $ 7}来提取URL)。在Hadoop上,通过使用更多结构化的文件格式,可以消除一些低价值的语法转换:Avro(请参阅第122页上的“Avro”)和Parquet(请参阅第95页上的“面向列的存储”)经常使用,因为它们提供高效的基于模式的编码,并允许随着时间的推移模式的演变(见第4章)。 +在这些领域,对Unix运行良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假定没有类型的文本文件,所以他们必须做大量的输入解析(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上,通过使用更多结构化的文件格式,可以消除一些低价值的语法转换:Avro(参阅“[Avro](ch4.md#Avro)”)和Parquet(参阅第95页上的“[列存储](ch3.md#列存储)”)经常使用,因为它们提供高效的基于模式的编码,并允许随着时间的推移模式的演变(见第4章)。 ### 比较Hadoop和分布式数据库 正如我们所看到的,Hadoop有点像Unix的分布式版本,其中HDFS是文件系统,而MapReduce是Unix进程的古怪实现(这恰好总是在映射阶段和缩小阶段之间运行排序实用程序)。我们看到了如何在这些基元之上实现各种连接和分组操作。 -当MapReduce论文[1]发表时,它在某种意义上说并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前的所谓的大规模并行处理(MPP)数据库中实现了[3,40]。例如,Gamma数据库机器,Teradata和Tandem NonStop SQL是这方面的先驱[52]。 +当MapReduce论文【1】发表时,它在某种意义上说并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前的所谓的**大规模并行处理(MPP, massively parallel processing)**数据库中实现了【3,40】。例如,Gamma数据库机器,Teradata和Tandem NonStop SQL是这方面的先驱【52】。 -最大的区别是MPP数据库集中于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统[19]的组合则更像是一个可以运行任意程序的通用操作系统。 +最大的区别是MPP数据库集中于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。 #### 存储的多样性 -数据库要求您根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本,图像,视频,传感器读数,稀疏矩阵,特征向量,基因组序列或任何其他类型的数据。 +数据库要求你根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本,图像,视频,传感器读数,稀疏矩阵,特征向量,基因组序列或任何其他类型的数据。 -说白了,Hadoop开放了将数据不加区分地转储到HDFS的可能性,之后才想出如何进一步处理它[53]。相比之下,在将数据导入数据库专有存储格式之前,MPP数据库通常需要对数据和查询模式进行仔细的前期建模。 -从纯粹的角度来看,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更好的质量数据来处理。然而,在实践中,似乎只是简单地使数据可用 - 即使它是一个古怪的,难以使用的原始格式 - 通常比尝试决定理想的数据模型更有价值[54 ]。 +说白了,Hadoop开放了将数据不加区分地转储到HDFS的可能性,之后才想出如何进一步处理它【53】。相比之下,在将数据导入数据库专有存储格式之前,MPP数据库通常需要对数据和查询模式进行仔细的前期建模。 -这个想法与数据仓库类似(请参阅第91页上的“数据仓库”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨以前不同的数据集进行联接。 MPP数据库所要求的谨慎的模式设计减慢了集中式数据收集速度;以原始形式收集数据,以后担心模式设计,使数据收集速度加快(有时被称为“数据湖”或“企业数据中心”[55])。 +从纯粹的角度来看,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更好的质量数据来处理。然而,在实践中,似乎只是简单地使数据可用 —— 即使它是一个古怪的,难以使用的原始格式 —— 通常比尝试决定理想的数据模型更有价值[54 ]。 -不加区别的数据倾销改变了解释数据的负担:不是强迫数据集的生产者将其转化为标准化的格式,而是数据的解释成为消费者的问题(模式在读方法[56];请参阅第39页上的“文档模型中的模式灵活性”)。如果生产者和消费者是不同优先级的不同团队,这可能是一个优势。甚至可能不存在一个理想的数据模型,而是对适合不同目的的数据有不同的看法。以原始形式简单地转储数据可以进行多次这样的转换。这种方法被称为寿司原则:“原始数据更好”[57]。 +这个想法与数据仓库类似(参阅“[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨以前不同的数据集进行联接。 MPP数据库所要求的谨慎的模式设计减慢了集中式数据收集速度;以原始形式收集数据,以后担心模式设计,使数据收集速度加快(有时被称为“**数据湖(data lake)**”或“**企业数据中心(enterprise data hub)**”【55】)。 -因此,Hadoop经常被用于实现ETL过程(请参阅“数据仓库”第91页):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系表单,并将其导入MPP数据仓库以进行分析。数据建模仍然在发生,但它是在一个单独的步骤中,从数据收集中分离出来的。这种解耦是可能的,因为分布式文件系统支持以任何格式编码的数据。 +不加区别的数据倾销改变了解释数据的负担:不是强迫数据集的生产者将其转化为标准化的格式,而是数据的解释成为消费者的问题(读时模式方法【56】;参阅“[文档模型中的架构灵活性](ch2.md#文档模型中的架构灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一个优势。甚至可能不存在一个理想的数据模型,而是对适合不同目的的数据有不同的看法。以原始形式简单地转储数据可以进行多次这样的转换。这种方法被称为寿司原则:“原始数据更好”【57】。 + +因此,Hadoop经常被用于实现ETL过程(参阅“[数据仓库](ch3.md#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系表单,并将其导入MPP数据仓库以进行分析。数据建模仍然在发生,但它是在一个单独的步骤中,从数据收集中分离出来的。这种解耦是可能的,因为分布式文件系统支持以任何格式编码的数据。 #### 加工模型的多样性 @@ -490,35 +494,35 @@ MPP数据库是单一的,紧密集成的软件,负责磁盘上的存储布 另一方面,并非所有类型的处理都可以合理地表达为SQL查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文搜索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常对特定的应用程序非常具体(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 -MapReduce使工程师能够轻松地在大型数据集上运行自己的代码。如果你有HDFS和MapReduce,那么你可以在它上面建立一个SQL查询执行引擎,事实上这正是Hive项目所做的[31]。但是,您也可以编写许多其他形式的批处理,这些批处理不适合用SQL查询表示。 +MapReduce使工程师能够轻松地在大型数据集上运行自己的代码。如果你有HDFS和MapReduce,那么你可以在它上面建立一个SQL查询执行引擎,事实上这正是Hive项目所做的【31】。但是,你也可以编写许多其他形式的批处理,这些批处理不适合用SQL查询表示。 -随后,人们发现MapReduce对于某些类型的处理来说太过于限制,执行得太差,因此其他各种处理模型都是在Hadoop之上开发的(我们将在第419页的“Beyond MapReduce”中看到其中的一些)。有两种处理模型,SQL和MapReduce,还不够,需要更多不同的模型!而且由于Hadoop平台的开放性,实施一整套方法是可行的,而这在整体MPP数据库的范围内是不可能的[58]。 +随后,人们发现MapReduce对于某些类型的处理来说太过于限制,执行得太差,因此其他各种处理模型都是在Hadoop之上开发的(我们将在“[后MapReduce时代](#后MapReduce时代)”中看到其中的一些)。有两种处理模型,SQL和MapReduce,还不够,需要更多不同的模型!而且由于Hadoop平台的开放性,实施一整套方法是可行的,而这在整体MPP数据库的范围内是不可能的【58】。 至关重要的是,这些不同的处理模型都可以在一个共享的机器上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在Hadoop方法中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个群集内不同的工作负载。不需要移动数据使得从数据中获得价值变得容易得多,并且使用新的处理模型更容易进行实验。 -Hadoop生态系统包括随机访问的OLTP数据库,如HBase(请参阅第70页的“SSTables和LSM-Trees”)和MPA样式的分析数据库,如Impala [41]。 HBase和Impala都不使用MapReduce,但都使用HDFS进行存储。它们是访问和处理数据的非常不同的方法,但是它们可以共存并被集成到同一个系统中。 +Hadoop生态系统包括随机访问的OLTP数据库,如HBase(参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)和MPA样式的分析数据库,如Impala 【41】。 HBase和Impala都不使用MapReduce,但都使用HDFS进行存储。它们是访问和处理数据的非常不同的方法,但是它们可以共存并被集成到同一个系统中。 #### 为频繁的故障而设计 在比较MapReduce和MPP数据库时,设计方法的另外两个不同点是:处理故障和使用内存和磁盘。与在线系统相比,批处理对故障不太敏感,因为如果失败,用户不会立即影响用户,并且可以再次运行。 -如果一个节点在执行查询时崩溃,大多数MPP数据库会中止整个查询,并让用户重新提交查询或自动重新运行它[3]。由于查询通常最多运行几秒钟或几分钟,所以这种处理错误的方法是可以接受的,因为重试的代价不是太大。 MPP数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的成本。 +如果一个节点在执行查询时崩溃,大多数MPP数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种处理错误的方法是可以接受的,因为重试的代价不是太大。 MPP数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的成本。 另一方面,MapReduce可以容忍映射或减少任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也非常渴望将数据写入磁盘,一方面是为了容错,另一方面是假设数据集太大而不能适应内存。 MapReduce方法更适用于较大的作业:处理如此之多的数据并运行很长时间的作业,以至于在此过程中可能至少遇到一个任务故障。在这种情况下,由于单个任务失败而重新运行整个工作将是浪费的。即使以单个任务的粒度进行恢复引入了使得无故障处理更慢的开销,但如果任务失败率足够高,仍然可以进行合理的权衡。 -但是这些假设有多现实呢?在大多数集群中,机器故障确实发生,但是它们不是很频繁 - 可能很少,大多数工作都不会经验,因为机器故障。为了容错,真的值得引起重大的开销吗? +但是这些假设有多现实呢?在大多数集群中,机器故障确实发生,但是它们不是很频繁 —— 可能很少,大多数工作都不会经验,因为机器故障。为了容错,真的值得引起重大的开销吗? -要了解MapReduce节省使用内存和任务级恢复的原因,查看最初设计MapReduce的环境是很有帮助的。 Google拥有混合使用的数据中心,在线生产服务和离线批处理作业在同一台机器上运行。每个任务都有一个使用容器执行的资源分配(CPU核心,RAM,磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的流程花费更多[59]。 +要了解MapReduce节省使用内存和任务级恢复的原因,查看最初设计MapReduce的环境是很有帮助的。 Google拥有混合使用的数据中心,在线生产服务和离线批处理作业在同一台机器上运行。每个任务都有一个使用容器执行的资源分配(CPU核心,RAM,磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的流程花费更多【59】。 这种架构允许非生产(低优先级)计算资源被过度使用,因为系统知道如果必要的话它可以回收资源。与分离生产和非生产任务的系统相比,过度使用资源可以更好地利用机器和提高效率。但是,由于MapReduce作业以低优先级运行,因此它们随时都有被抢占的风险,因为优先级较高的进程需要其资源。批量工作有效地“拿起桌子下面的碎片”,利用高优先级进程已经采取的任何计算资源。 -在谷歌,运行一个小时的MapReduce任务有大约5%被终止的风险,为更高优先级的进程腾出空间。由于硬件问题,机器重新启动或其他原因,这个速率比故障率高出一个数量级[59]。按照这种抢先率,如果一个作业有100个任务,每个任务运行10分钟,那么至少有一个任务在完成之前将被终止的风险大于50%。 +在谷歌,运行一个小时的MapReduce任务有大约5%被终止的风险,为更高优先级的进程腾出空间。由于硬件问题,机器重新启动或其他原因,这个速率比故障率高出一个数量级【59】。按照这种抢先率,如果一个作业有100个任务,每个任务运行10分钟,那么至少有一个任务在完成之前将被终止的风险大于50%。 这就是为什么MapReduce能够容忍频繁意外的任务终止的原因:这不是因为硬件特别不可靠,这是因为任意终止进程的自由可以在计算集群中更好地利用资源。 -在开源的集群调度器中,抢占的使用较少。 YARN的CapacityScheduler支持抢占以平衡不同队列的资源分配[58],但在编写本文时,YARN,Mesos或Kubernetes不支持通用优先级抢占[60]。在任务不经常被终止的环境中,MapReduce的设计决策没有多少意义。在下一节中,我们将看看MapReduce的一些替代方案,这些替代方案做出了不同的设计决定。 +在开源的集群调度器中,抢占的使用较少。 YARN的CapacityScheduler支持抢占以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos或Kubernetes不支持通用优先级抢占【60】。在任务不经常被终止的环境中,MapReduce的设计决策没有多少意义。在下一节中,我们将看看MapReduce的一些替代方案,这些替代方案做出了不同的设计决定。 @@ -526,11 +530,11 @@ MapReduce方法更适用于较大的作业:处理如此之多的数据并运 虽然MapReduce在二十世纪二十年代后期变得非常流行并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。根据数据量,数据结构和处理类型,其他工具可能更适合表达计算。 -尽管如此,我们在讨论MapReduce的这一章花了很多时间,因为它是一个有用的学习工具,因为它是分布式文件系统的一个相当清晰和简单的抽象。也就是说,能够理解它在做什么,而不是在易于使用的意义上是简单的。恰恰相反:使用原始的MapReduce API来实现复杂的处理工作实际上是非常困难和费力的 - 例如,您需要从头开始实现任何连接算法[37]。 +尽管如此,我们在讨论MapReduce的这一章花了很多时间,因为它是一个有用的学习工具,因为它是分布式文件系统的一个相当清晰和简单的抽象。也就是说,能够理解它在做什么,而不是在易于使用的意义上是简单的。恰恰相反:使用原始的MapReduce API来实现复杂的处理工作实际上是非常困难和费力的 —— 例如,你需要从头开始实现任何连接算法【37】。 -针对直接使用MapReduce的困难,在MapReduce上创建了各种更高级的编程模型(Pig,Hive,Cascading,Crunch)作为抽象。如果您了解MapReduce的工作原理,那么它们相当容易学习,而且它们的高级构造使许多常见的批处理任务更容易实现。 +针对直接使用MapReduce的困难,在MapReduce上创建了各种更高级的编程模型(Pig,Hive,Cascading,Crunch)作为抽象。如果你了解MapReduce的工作原理,那么它们相当容易学习,而且它们的高级构造使许多常见的批处理任务更容易实现。 -但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次来解决,而且在某些类型的处理中表现得很差。一方面,MapReduce非常强大:您可以使用它来处理频繁任务终止的不可靠多租户系统上几乎任意大量的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理来说,其他工具有时也会更快。 +但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次来解决,而且在某些类型的处理中表现得很差。一方面,MapReduce非常强大:你可以使用它来处理频繁任务终止的不可靠多租户系统上几乎任意大量的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理来说,其他工具有时也会更快。 在本章的其余部分中,我们将介绍一些批处理方法。在第十一章我们将转向流处理,这可以看作是加速批处理的另一种方法。 @@ -538,44 +542,44 @@ MapReduce方法更适用于较大的作业:处理如此之多的数据并运 如前所述,每个MapReduce作业都独立于其他任何作业。作业与世界其他地方的主要联系点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为与第一个作业的输出目录相同,并且外部工作流调度程序必须仅在第一份工作已经完成。 -如果第一个作业的输出是要在组织内广泛发布的数据集,则此设置是合理的。在这种情况下,您需要能够通过名称来引用它,并将其用作多个不同作业(包括由其他团队开发的作业)的输入。将数据发布到分布式文件系统中的众所周知的位置允许松耦合,这样作业就不需要知道是谁在输入输出或消耗其输出(请参阅“分离逻辑和布线”在本页395)。 +如果第一个作业的输出是要在组织内广泛发布的数据集,则此设置是合理的。在这种情况下,你需要能够通过名称来引用它,并将其用作多个不同作业(包括由其他团队开发的作业)的输入。将数据发布到分布式文件系统中的众所周知的位置允许松耦合,这样作业就不需要知道是谁在输入输出或消耗其输出(参阅“[分离逻辑和布线](#分离逻辑和布线)”在本页395)。 -但是,在很多情况下,您知道一个工作的输出只能用作另一个工作的输入,这个工作由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的中间状态:一种将数据从一个作业传递到下一个作业的方式。在用于构建由50或100个MapReduce作业[29]组成的推荐系统的复杂工作流程中,存在很多这样的中间状态。 +但是,在很多情况下,你知道一个工作的输出只能用作另一个工作的输入,这个工作由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的中间状态:一种将数据从一个作业传递到下一个作业的方式。在用于构建由50或100个MapReduce作业【29】组成的推荐系统的复杂工作流程中,存在很多这样的中间状态。 -将这个中间状态写入文件的过程称为物化。 (我们在第101页的“聚合:数据立方体和物化视图”中已经在物化视图的背景下遇到了这个术语。它意味着要急于计算某个操作的结果并写出来,而不是计算需要时按要求。) +将这个中间状态写入文件的过程称为物化。 (我们在“[聚合:数据立方体和物化视图](ch2.md#聚合:数据立方体和物化视图)”中已经在物化视图的背景下遇到了这个术语。它意味着要急于计算某个操作的结果并写出来,而不是计算需要时按要求。) 相反,本章开头的日志分析示例使用Unix管道将一个命令的输出与另一个命令的输出连接起来。管道并没有完全实现中间状态,而是只使用一个小的内存缓冲区,将输出逐渐流向输入。 MapReduce的完全实现中间状态的方法与Unix管道相比存在不足: * MapReduce作业只有在前面的作业(生成其输入)中的所有任务都完成时才能启动,而由Unix管道连接的进程同时启动,输出一旦生成就会被使用。不同机器上的偏差或不同的负荷意味着一份工作往往会有一些比其他人更快完成的离散任务。必须等到所有前面的工作完成才能减慢整个工作流程的执行。 -* 映射器通常是多余的:它们只读取刚刚由reducer写入的相同文件,并为下一个分区和排序阶段做好准备。在许多情况下,映射器代码可能是以前的reducer的一部分:如果reducer输出被分区和排序的方式与mapper输出相同,那么reducers可以直接链接在一起,而不与mapper阶段交错。 +* Mapper通常是多余的:它们只读取刚刚由reducer写入的相同文件,并为下一个分区和排序阶段做好准备。在许多情况下,Mapper代码可能是以前的reducer的一部分:如果reducer输出被分区和排序的方式与mapper输出相同,那么reducers可以直接链接在一起,而不与mapper阶段交错。 * 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,这对于这样的临时数据通常是过度的。 #### 数据流引擎 -了解决MapReduce的这些问题,开发了几种用于分布式批量计算的新的执行引擎,其中最着名的是Spark [61,62],Tez [63,64]和Flink [65,66]。他们设计的方式有很多不同之处,但他们有一个共同点:他们把整个工作流作为一项工作来处理,而不是把它分解成独立的子作业。 +了解决MapReduce的这些问题,开发了几种用于分布式批量计算的新的执行引擎,其中最着名的是Spark 【61,62】,Tez 【63,64】和Flink 【65,66】。他们设计的方式有很多不同之处,但他们有一个共同点:他们把整个工作流作为一项工作来处理,而不是把它分解成独立的子作业。 由于它们通过几个处理阶段明确地建模数据流,所以这些系统被称为数据流引擎。像MapReduce一样,它们通过反复调用用户定义的函数来在单个线程上一次处理一条记录。他们通过对输入进行分区来并行工作,并将一个功能的输出复制到网络上,成为另一个功能的输入。 -与MapReduce不同,这些功能不需要交替映射和缩减的严格角色,而是可以以更灵活的方式进行组合。我们称之为这些函数操作符,数据流引擎提供了几个不同的选项来连接一个操作符的输出到另一个的输入: +与MapReduce不同,这些功能不需要交替映射和Reduce的严格角色,而是可以以更灵活的方式进行组合。我们称之为这些函数操作符,数据流引擎提供了几个不同的选项来连接一个操作符的输出到另一个的输入: -* 一个选项是通过键对记录进行重新分区和排序,就像在MapReduce的混洗阶段一样(请参阅“分布式执行MapReduce”)。此功能可以像在MapReduce中一样启用排序合并连接和分组。 +* 一个选项是通过键对记录进行重新分区和排序,就像在MapReduce的混洗阶段一样(参阅“[分布式执行MapReduce](#分布式执行MapReduce)”)。此功能可以像在MapReduce中一样启用排序合并连接和分组。 * 另一种可能是采取几个输入,并以相同的方式进行分区,但跳过排序。这节省了分区散列连接的工作,其中记录的分区是重要的,但顺序是不相关的,因为构建散列表随机化了顺序。 * 对于广播散列连接,可以将一个运算符的相同输出发送到连接运算符的所有分区。 -这种处理引擎的风格基于像Dryad [67]和Nephele [68]这样的研究系统,与MapReduce模型相比,它提供了几个优点: +这种处理引擎的风格基于像Dryad 【67】和Nephele 【68】这样的研究系统,与MapReduce模型相比,它提供了几个优点: * 排序等昂贵的工作只需要在实际需要的地方执行,而不是在每个Map和Reduce阶段之间默认发生。 -* 没有不必要的地图任务,因为映射器所做的工作通常可以合并到前面的reduce操作器中(因为映射器不会更改数据集的分区)。 +* 没有不必要的Map任务,因为Mapper所做的工作通常可以合并到前面的reduce操作器中(因为Mapper不会更改数据集的分区)。 * 由于工作流程中的所有连接和数据依赖性都是明确声明的,因此调度程序会概述哪些数据是必需的,因此可以进行本地优化。例如,它可以尝试将占用某些数据的任务放在与生成它的任务相同的机器上,以便可以通过共享内存缓冲区交换数据,而不必通过网络复制数据。 -* 通常将操作员之间的中间状态保存在内存中或写入本地磁盘就足够了,这比写入HDFS需要更少的I / O(必须将其复制到多个计算机并写入到每个代理的磁盘上)。 MapReduce已经将这种优化用于映射器的输出,但是数据流引擎将该思想推广到了所有的中间状态。 +* 通常将操作员之间的中间状态保存在内存中或写入本地磁盘就足够了,这比写入HDFS需要更少的I/O(必须将其复制到多个计算机并写入到每个代理的磁盘上)。 MapReduce已经将这种优化用于Mapper的输出,但是数据流引擎将该思想推广到了所有的中间状态。 * 操作员可以在输入准备就绪后立即开始执行;在下一个开始之前不需要等待整个前一阶段的完成。 * 与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。 #### 容错 @@ -583,7 +587,7 @@ Tez是一个相当薄的库,它依赖于YARN shuffle服务来实现节点间 Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同的方法来容忍错误:如果一台机器发生故障,并且该机器上的中间状态丢失,则会从其他仍然可用的数据重新计算在可能的情况下是在先的中间阶段,或者是通常在HDFS上的原始输入数据)。 -为了实现这个重新计算,框架必须跟踪一个给定的数据是如何计算的 - 使用哪个输入分区,以及哪个操作符被应用到它。 Spark使用弹性分布式数据集(RDD)抽象来追踪数据的祖先[61],而Flink检查点操作符状态,允许其恢复运行在执行过程中遇到错误的操作符[66]。 +为了实现这个重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用哪个输入分区,以及哪个操作符被应用到它。 Spark使用弹性分布式数据集(RDD)抽象来追踪数据的祖先【61】,而Flink检查点操作符状态,允许其恢复运行在执行过程中遇到错误的操作符【66】。 在重新计算数据时,重要的是要知道计算是否是确定性的:也就是说,给定相同的输入数据,操作员是否始终生成相同的输出?如果一些丢失的数据已经发送给下游运营商,这个问题就很重要。如果运营商重新启动,重新计算的数据与原有的丢失数据不一致,下游运营商很难解决新旧数据之间的矛盾。对于不确定性运营商来说,解决方案通常是杀死下游运营商,然后再运行新数据。 @@ -595,19 +599,19 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同 回到Unix的类比,我们看到MapReduce就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是Unix管道。尤其是Flink是围绕流水线执行的思想而建立的:也就是说,将运算符的输出递增地传递给其他操作符,并且在开始处理之前不等待输入完成。 -排序操作不可避免地需要消耗其整个输入,然后才能生成任何输出,因为最后一个输入记录可能是具有最低密钥的输入记录,因此需要作为第一个输出记录。任何需要分类的操作员都需要至少暂时地累积状态。但是工作流程的许多其他部分可以以流水线方式执行。 +排序操作不可避免地需要消耗其整个输入,然后才能生成任何输出,因为最后一个输入记录可能是具有最低键的输入记录,因此需要作为第一个输出记录。任何需要分类的操作员都需要至少暂时地累积状态。但是工作流程的许多其他部分可以以流水线方式执行。 -当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它 - 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和最终输出。和MapReduce一样,输入是不可变的,输出被完全替换。对MapReduce的改进是,您可以节省自己将所有中间状态写入文件系统。 +当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它—— 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和最终输出。和MapReduce一样,输入是不可变的,输出被完全替换。对MapReduce的改进是,你可以节省自己将所有中间状态写入文件系统。 ### 图与迭代处理 -在第49页上的“类似图形的数据模型”中,我们讨论了使用图形来建模数据,并使用图形查询语言来遍历图形中的边和顶点。第2章的讨论集中在OLTP风格的使用上:快速执行查询来查找少量符合特定条件的顶点。 +在“[图数据模型](ch2.md#图数据模型)”中,我们讨论了使用图形来建模数据,并使用图形查询语言来遍历图形中的边和顶点。[第2章](ch2.md)的讨论集中在OLTP风格的使用上:快速执行查询来查找少量符合特定条件的顶点。 -在批处理环境中查看图表也很有趣,其目标是在整个图表上执行某种离线处理或分析。这种需求经常出现在机器学习应用程序(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank [69],它试图根据其他网页链接的网页来估计网页的流行度。它被用作确定网络搜索引擎呈现结果的顺序的公式的一部分。 +在批处理环境中查看图表也很有趣,其目标是在整个图表上执行某种离线处理或分析。这种需求经常出现在机器学习应用程序(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank 【69】,它试图根据其他网页链接的网页来估计网页的流行度。它被用作确定网络搜索引擎呈现结果的顺序的公式的一部分。 -> 像Spark,Flink和Tez这样的数据流引擎(参见第419页“中间状态的实现化”)通常将操作符作为有向无环图(DAG)排列在作业中。这与图形处理不一样:在数据流引擎中,从一个操作符到另一个操作符的数据流被构造成一个图,而数据本身通常由关系式元组构成。在图形处理中,数据本身具有图形的形式。另一个不幸的命名混乱! +> 像Spark,Flink和Tez这样的数据流引擎(参见“[中间状态的物化](#中间状态的物化)”)通常将操作符作为**有向无环图(DAG)**排列在作业中。这与图形处理不一样:在数据流引擎中,从一个操作符到另一个操作符的数据流被构造成一个图,而数据本身通常由关系式元组构成。在图形处理中,数据本身具有图形的形式。另一个不幸的命名混乱! -许多图算法是通过一次遍历一个边来表示的,将一个顶点与相邻的顶点连接起来以便传播一些信息,并且重复直到满足一些条件为止 - 例如,直到没有更多的边要跟随,或者直到一些度量收敛。我们在图2-6中看到一个例子,它通过重复地跟踪指示哪个位置在哪个其他位置(这种算法被称为传递闭包)的边缘,列出了包含在数据库中的北美所有位置。 +许多图算法是通过一次遍历一个边来表示的,将一个顶点与相邻的顶点连接起来以便传播一些信息,并且重复直到满足一些条件为止——例如,直到没有更多的边要跟随,或者直到一些度量收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复地跟踪指示哪个位置在哪个其他位置(这种算法被称为传递闭包)的边缘,列出了包含在数据库中的北美所有位置。 可以在分布式文件系统(包含顶点和边的列表的文件)中存储图形,但是这种“重复直到完成”的想法不能用普通的MapReduce来表示,因为它只执行一次数据传递。这种算法因此经常以迭代方式实现: @@ -618,20 +622,20 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同 这种方法是有效的,但是用MapReduce实现它往往是非常低效的,因为MapReduce没有考虑算法的迭代性质:它总是读取整个输入数据集并产生一个全新的输出数据集,即使只有一小部分该图与上次迭代相比已经改变。 Pregel处理模型 -作为批处理图形的优化,计算的批量同步并行(BSP)模型[70]已经流行起来。其中,它由Apache Giraph [37],Spark的GraphX API和Flink的Gelly API [71]实现。它也被称为Pregel模型,正如Google的Pregel论文推广这种处理图的方法[72]。 +作为批处理图形的优化,计算的批量同步并行(BSP)模型【70】已经流行起来。其中,它由Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】实现。它也被称为Pregel模型,正如Google的Pregel论文推广这种处理图的方法【72】。 -回想一下在MapReduce中,映射器在概念上“发送消息”给reducer的特定调用,因为框架将所有的mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以“发送消息”到另一个顶点,通常这些消息沿着图的边被发送。 +回想一下在MapReduce中,Mapper在概念上“发送消息”给reducer的特定调用,因为框架将所有的mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以“发送消息”到另一个顶点,通常这些消息沿着图的边被发送。 -在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 - 就像调用reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点从一次迭代到下一次迭代记忆它的状态,所以这个函数只需要处理新的传入消息。如果在图的某个部分没有发送消息,则不需要做任何工作。 +在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点从一次迭代到下一次迭代记忆它的状态,所以这个函数只需要处理新的传入消息。如果在图的某个部分没有发送消息,则不需要做任何工作。 -这与演员模型有些相似(请参阅第130页上的“分布式演员框架”),除非顶点状态和顶点之间的消息具有容错性和耐久性,并且通信以固定的方式进行,否则将每个顶点视为主角轮次:在每一次迭代中,框架传递在前一次迭代中发送的所有消息。演员通常没有这样的时间保证。 +这与演员模型有些相似(参阅“[分布式的Actor框架](ch4.md#分布式的Actor框架)”),除非顶点状态和顶点之间的消息具有容错性和耐久性,并且通信以固定的方式进行,否则将每个顶点视为主角轮次:在每一次迭代中,框架传递在前一次迭代中发送的所有消息。演员通常没有这样的时间保证。 #### 容错 顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高Pregel作业的性能,因为消息可以成批处理,而且等待通信的次数也减少了。唯一的等待是在迭代之间:由于Pregel模型保证所有在一次迭代中发送的消息都在下一次迭代中传递,所以先前的迭代必须完全完成,并且所有的消息必须在网络上复制,然后下一个开始。 -即使底层网络可能丢失,重复或任意延迟消息(请参阅第267页上的“不可靠网络”),Pregel实施可保证在接下来的迭代中消息在其目标顶点处理一次。像MapReduce一样,该框架透明地从故障中恢复,以简化Pregel顶层算法的编程模型。 +即使底层网络可能丢失,重复或任意延迟消息(参阅“[不可靠的网络](ch8.md#不可靠的网络)”),Pregel实施可保证在接下来的迭代中消息在其目标顶点处理一次。像MapReduce一样,该框架透明地从故障中恢复,以简化Pregel顶层算法的编程模型。 -这种容错是通过在迭代结束时定期检查所有顶点的状态来实现的,即将其全部状态写入持久存储。如果某个节点发生故障并且其内存中状态丢失,则最简单的解决方法是将整个图计算回滚到上一个检查点,然后重新启动计算。如果算法是确定性的并且记录了消息,那么也可以选择性地只恢复丢失的分区(就像我们之前讨论过的数据流引擎)[72]。 +这种容错是通过在迭代结束时定期检查所有顶点的状态来实现的,即将其全部状态写入持久存储。如果某个节点发生故障并且其内存中状态丢失,则最简单的解决方法是将整个图计算回滚到上一个检查点,然后重新启动计算。如果算法是确定性的并且记录了消息,那么也可以选择性地只恢复丢失的分区(就像我们之前讨论过的数据流引擎)【72】。 #### 并行执行 @@ -641,7 +645,7 @@ Pregel处理模型 因此,图算法通常会有很多跨机器通信,而中间状态(节点之间发送的消息)往往比原始图大。通过网络发送消息的开销会显着减慢分布式图算法的速度。 -出于这个原因,如果你的图可以放在一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理[73,74]。即使图形大于内存,也可以放在单个计算机的磁盘上,使用GraphChi等框架进行单机处理是一个可行的选择[75]。如果图形太大而不适合单个机器,像Pregel这样的分布式方法是不可避免的。有效的并行化图算法是一个正在进行的领域。 +出于这个原因,如果你的图可以放在一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理【73,74】。即使图形大于内存,也可以放在单个计算机的磁盘上,使用GraphChi等框架进行单机处理是一个可行的选择【75】。如果图形太大而不适合单个机器,像Pregel这样的分布式方法是不可避免的。有效的并行化图算法是一个正在进行的领域。 @@ -649,27 +653,27 @@ Pregel处理模型 自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通常使用关系式构建块来表达一个计算:连接数据集以获取某个字段的值;按键分组元组;过滤一些条件;并通过计数,求和或其他函数来聚合元组。在内部,这些操作是使用本章前面讨论过的各种连接和分组算法来实现的。 -除了需要较少代码的明显优势之外,这些高级接口还允许交互式使用,在这种交互式使用中,您可以将分析代码逐步编写到shell中并经常运行,以观察它正在做什么。这种发展风格在探索数据集和试验处理方法时非常有用。这也让人联想到Unix哲学,我们在第394页的“Unix哲学”中讨论过这个问题。 +除了需要较少代码的明显优势之外,这些高级接口还允许交互式使用,在这种交互式使用中,你可以将分析代码逐步编写到shell中并经常运行,以观察它正在做什么。这种发展风格在探索数据集和试验处理方法时非常有用。这也让人联想到Unix哲学,我们在第394页的“Unix哲学”中讨论过这个问题。 而且,这些高级接口不仅使人类使用系统的效率更高,而且提高了机器级别的工作执行效率。 #### 向声明式查询语言的转变 -与拼写执行连接的代码相比,指定连接为关系运算符的优点是,框架可以分析连接输入的属性,并自动决定哪个上述连接算法最适合手头的任务。 Hive,Spark和Flink都有基于代价的查询优化器,可以做到这一点,甚至可以改变连接顺序,使中间状态的数量最小化[66,77,78,79]。 +与拼写执行连接的代码相比,指定连接为关系运算符的优点是,框架可以分析连接输入的属性,并自动决定哪个上述连接算法最适合手头的任务。 Hive,Spark和Flink都有基于代价的查询优化器,可以做到这一点,甚至可以改变连接顺序,使中间状态的数量最小化【66,77,78,79】。 连接算法的选择可以对批处理作业的性能产生很大的影响,不必理解和记住本章中讨论的各种连接算法。如果以声明的方式指定连接,则这是可能的:应用程序简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在第42页的“数据的查询语言”中遇到了这个想法。 但是,在其他方面,MapReduce及其数据流后继与SQL的完全声明性查询模型有很大不同。 MapReduce是围绕函数回调的思想构建的:对于每个记录或者一组记录,调用一个用户定义的函数(mapper或reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以绘制在现有库的大型生态系统上进行分析,自然语言分析,图像分析以及运行数字或统计算法等。 -轻松运行任意代码的自由是从MPP数据库(参见“比较Hadoop到分布式数据库”一节,第414页)中分离出来的MapReduce传统批处理系统。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统(例如Maven for Java,npm for Java-Script,和Ruby的Ruby的Ruby)。 +轻松运行任意代码的自由是从MPP数据库(参见“[比较Hadoop和分布式数据库](#比较Hadoop和分布式数据库)”一节)中分离出来的MapReduce传统批处理系统。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统(例如Maven for Java,npm for Java-Script,和Ruby的Ruby的Ruby)。 -但是,数据流引擎已经发现,除了连接之外,在合并更多的声明性特征方面也是有优势的。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在调用每条记录的函数时会有相当大的CPU开销。如果以声明方式表示这样简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(请参阅第95页的“面向列的存储”),并从磁盘只读取所需的列。 Hive,Spark DataFrames和Impala也使用向量化执行(请参阅第99页的“内存带宽和向量化处理”):在对CPU缓存很友好的内部循环中迭代数据,并避免函数调用。 +但是,数据流引擎已经发现,除了连接之外,在合并更多的声明性特征方面也是有优势的。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在调用每条记录的函数时会有相当大的CPU开销。如果以声明方式表示这样简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(参阅“[列存储](ch3.md#列存储)”),并从磁盘只读取所需的列。 Hive,Spark DataFrames和Impala也使用向量化执行(参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存很友好的内部循环中迭代数据,并避免函数调用。 -Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机代码[41]。 +Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。 通过将声明性方面与高级API结合起来,并使查询优化器可以在执行期间利用这些优化方法,批处理框架看起来更像MPP数据库(并且可以实现可比较的性能)。同时,通过具有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。 @@ -677,12 +681,14 @@ Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机 尽管能够运行任意代码的可扩展性是有用的,但是在标准处理模式不断重复发生的情况下也有许多常见的情况,所以值得重用通用构建块的实现。传统上,MPP数据库满足了商业智能分析师和业务报告的需求,但这只是许多使用批处理的领域之一。 -另一个越来越重要的领域是统计和数值算法,它们是机器学习应用(如分类和推荐系统)所需要的。可重复使用的实现正在出现:例如,Mahout在MapReduce,Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能[54]。 +另一个越来越重要的领域是统计和数值算法,它们是机器学习应用(如分类和推荐系统)所需要的。可重复使用的实现正在出现:例如,Mahout在MapReduce,Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能【54】。 -空间算法也是有用的,例如k-最近邻居[80],它在一些多维空间中搜索与给定物品接近的物品 - 这是一种类似的搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串[81]。 +空间算法也是有用的,例如最近邻搜索(kNN)【80】,它在一些多维空间中搜索与给定物品接近的物品 - 这是一种类似的搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串【81】。 批处理引擎正被用于分布式执行来自日益广泛的领域的算法。随着批处理系统获得内置功能和高级声明性操作符,并且随着MPP数据库变得更加可编程和灵活,两者开始看起来更相似:最终,它们都只是存储和处理数据的系统。 + + ## 本章小结 @@ -694,8 +700,9 @@ Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机 ***分区*** -在MapReduce中,映射器根据输入文件块进行分区。映射器的输出被重新分区,分类并合并到可配置数量的reducer分区中。这个过程的目的是把所有的相关数据 - 例如,所有的记录都放在同一个地方。 -Post-MapReduce数据流引擎尽量避免排序,除非它是必需的,但它们采取了大致类似的分区方法。 +在MapReduce中,Mapper根据输入文件块进行分区。Mapper的输出被重新分区,分类并合并到可配置数量的reducer分区中。这个过程的目的是把所有的相关数据 —— 例如,所有的记录都放在同一个地方。 + +后MapReduce数据流引擎尽量避免排序,除非它是必需的,但它们采取了大致类似的分区方法。 ***容错*** @@ -705,23 +712,23 @@ MapReduce经常写入磁盘,这使得从单个失败的任务中轻松地恢 ***排序合并连接*** -每个正在连接的输入都通过一个提取连接键的映射器。通过分区,排序和合并,具有相同密钥的所有记录最终都会进入reducer的相同调用。这个函数可以输出连接的记录。 +每个正在连接的输入都通过一个提取连接键的Mapper。通过分区,排序和合并,具有相同键的所有记录最终都会进入reducer的相同调用。这个函数可以输出连接的记录。 ***广播散列连接*** -两个连接输入之一是小的,所以它没有分区,它可以被完全加载到一个哈希表。因此,您可以为大连接输入的每个分区启动一个映射器,将小输入的散列表加载到每个映射器中,然后一次扫描大输入一条记录,查询每条记录的散列表。 +两个连接输入之一是小的,所以它没有分区,它可以被完全加载到一个哈希表。因此,你可以为大连接输入的每个分区启动一个Mapper,将小输入的散列表加载到每个Mapper中,然后一次扫描大输入一条记录,查询每条记录的散列表。 ***分区散列连接*** -如果两个连接输入以相同的方式分区(使用相同的密钥,相同的散列函数和相同数量的分区),则可以独立地为每个分区使用散列表方法。 +如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地为每个分区使用散列表方法。 -分布式批处理引擎有一个有意限制的编程模型:回调函数(比如映射器和简化器)被认为是无状态的,除了指定的输出外,没有外部可见的副作用。这个限制允许框架隐藏抽象背后的一些硬分布式系统问题:面对崩溃和网络问题,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则只有其中一个实际上使其输出可见。 +分布式批处理引擎有一个有意限制的编程模型:回调函数(比如Mapper和Reducer)被认为是无状态的,除了指定的输出外,没有外部可见的副作用。这个限制允许框架隐藏抽象背后的一些硬分布式系统问题:面对崩溃和网络问题,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则只有其中一个实际上使其输出可见。 -得益于这个框架,您在批处理作业中的代码无需担心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,也许不得不重新尝试各种任务。这些可靠的语义要比在线服务处理用户请求时经常使用的要多得多,而且在处理请求的副作用时写入数据库。 +得益于这个框架,你在批处理作业中的代码无需担心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,也许不得不重新尝试各种任务。这些可靠的语义要比在线服务处理用户请求时经常使用的要多得多,而且在处理请求的副作用时写入数据库。 -批量处理工作的显着特点是它读取一些输入数据并产生一些输出数据,而不修改输入 - 换句话说,输出是从输入导出的。重要的是,输入数据是有界的:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个工作知道什么时候它完成了整个输入的读取,所以一个工作最终完成。 +批量处理工作的显着特点是它读取一些输入数据并产生一些输出数据,而不修改输入—— 换句话说,输出是从输入衍生出的。重要的是,输入数据是有界的:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个工作知道什么时候它完成了整个输入的读取,所以一个工作最终完成。 -在下一章中,我们将转向流处理,其中的输入是未知的 - 也就是说,你还有一份工作,但是它的输入是永无止境的数据流。在这种情况下,工作永远不会完成,因为在任何时候都可能有更多的工作进来。我们将看到流和批处理在某些方面是相 +在下一章中,我们将转向流处理,其中的输入是未知的 —— 也就是说,你还有一份工作,但是它的输入是永无止境的数据流。在这种情况下,工作永远不会完成,因为在任何时候都可能有更多的工作进来。我们将看到流和批处理在某些方面是相似的。但是关于无尽数据流的假设,也对我们构建系统的方式产生了很大的改变。 diff --git a/ch11.md b/ch11.md index cb38386..87f4be0 100644 --- a/ch11.md +++ b/ch11.md @@ -14,11 +14,11 @@ 然而,在第10章中仍然有一个大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成了它的输入。例如,MapReduce中心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生最后一个输入记录是具有最低键的输入记录,因此必须是第一个输出记录,所以提前开始输出不是一种选择。 -实际上,很多数据是无限的,因为它随着时间的推移逐渐到达:你的用户昨天和今天产生了数据,明天他们将继续产生更多的数据。除非您停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”[1]。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 +实际上,很多数据是无限的,因为它随着时间的推移逐渐到达:你的用户昨天和今天产生了数据,明天他们将继续产生更多的数据。除非您停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 日常批处理过程中的问题是,输入的更改只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 - 比如说,在每秒钟的末尾,甚至连续地处理秒数的数据,完全放弃固定的时间片,并简单地处理每一个事件。这是流处理背后的想法。 -一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(lazy lists)[2],文件系统API(如Java的FileInputStream),TCP连接,通过互联网传送音频和视频等等。 +一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(lazy lists)【2】,文件系统API(如Java的FileInputStream),TCP连接,通过互联网传送音频和视频等等。 在本章中,我们将把事件流视为一个数据管理机制:我们在上一章中看到的批量数据的无界的,递增处理的对应物。我们将首先讨论流如何被表示,存储和通过网络传输。在第451页的“数据库和流”中,我们将调查流和数据库之间的关系。最后,在第464页的“Processing Streams”中,我们将探索连续处理这些流的方法和工具,以及它们可以用来构建应用程序的方法。 @@ -41,13 +41,13 @@ 事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如第4章所述。这种编码允许您存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许您通过网络将事件发送到另一个节点以进行处理。 -在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流媒体术语中,一个事件由生产者(也称为发布者或发送者)生成一次,然后由多个消费者(订阅者或接收者)进行处理[3]。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被组合成一个主题或流。 +在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流媒体术语中,一个事件由生产者(也称为发布者或发送者)生成一次,然后由多个消费者(订阅者或接收者)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被组合成一个主题或流。 原则上,文件或数据库足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,并且每个使用者定期轮询数据存储以检查自上次运行以来出现的事件。这实际上是批处理在每天结束时处理一天的数据的过程。 但是,如果数据存储不是为这种用途而设计的,那么在延迟较小的情况下继续进行处理时,轮询将变得非常昂贵。您调查的次数越多,返回新事件的请求百分比越低,因此开销越高。相反,当新事件出现时,最好通知消费者。 -数据库传统上不太支持这种通知机制:关系数据库通常具有触发器,它们可以对变化作出反应(例如,将一行插入到表中),但是它们的功能非常有限,有点在数据库设计中是事后的[4,5]。相反,已经开发了专门的工具来提供事件通知。 +数据库传统上不太支持这种通知机制:关系数据库通常具有触发器,它们可以对变化作出反应(例如,将一行插入到表中),但是它们的功能非常有限,有点在数据库设计中是事后的【4,5】。相反,已经开发了专门的工具来提供事件通知。 @@ -61,11 +61,11 @@ 1. 如果生产者发送消息的速度比消费者能够处理的速度快,会发生什么?一般来说,有三种选择:系统可以放置消息,缓冲队列中的消息或应用背压(也称为流量控制;即阻止生产者发送更多的消息)。例如,Unix管道和TCP使用背压:他们有一个小的固定大小的缓冲区,如果填满,发件人被阻塞,直到收件人从缓冲区中取出数据(参见“网络拥塞和排队”(第282页))。 - 如果消息被缓存在队列中,那么了解该队列增长会发生什么很重要。如果队列不再适合内存,或者将消息写入磁盘,系统是否会崩溃?如果是这样,磁盘访问如何影响邮件系统的性能[6]? + 如果消息被缓存在队列中,那么了解该队列增长会发生什么很重要。如果队列不再适合内存,或者将消息写入磁盘,系统是否会崩溃?如果是这样,磁盘访问如何影响邮件系统的性能【6】? 2. 如果节点崩溃或暂时脱机,会发生什么情况 - 是否有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅第227页的侧栏“复制和耐久性”),这有成本。如果您有时可能会丢失消息,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 -消息丢失是否可以接受取决于应用程序。例如,对于定期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新后的值将在短时间后发送。但是,要注意,如果大量的消息被丢弃,那么衡量标准是不正确的[7]。如果您正在计数事件,那么更重要的是它们可靠地传送,因为每个丢失的消息都意味着不正确的计数器。 +消息丢失是否可以接受取决于应用程序。例如,对于定期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新后的值将在短时间后发送。但是,要注意,如果大量的消息被丢弃,那么衡量标准是不正确的【7】。如果您正在计数事件,那么更重要的是它们可靠地传送,因为每个丢失的消息都意味着不正确的计数器。 我们在第10章中探讨的批处理系统的一个很好的特性是它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流式上下文中提供类似的保证。 @@ -73,13 +73,13 @@ 许多消息传递系统使用生产者和消费者之间的直接网络通信,而不通过中间节点: -* UDP组播广泛应用于金融行业,例如股票市场,其中低时延非常重要[8]。虽然UDP本身是不可靠的,但应用程序级别的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便它可以按需重新发送数据包)。 +* UDP组播广泛应用于金融行业,例如股票市场,其中低时延非常重要【8】。虽然UDP本身是不可靠的,但应用程序级别的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便它可以按需重新发送数据包)。 -* 无代理的消息库,如ZeroMQ [9]和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。 +* 无代理的消息库,如ZeroMQ 【9】和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。 - StatsD [10]和Brubeck [7]使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为[11]。另请参阅“TCP与UDP” + StatsD 【10】和Brubeck 【7】使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为【11】。另请参阅“TCP与UDP” -* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(请参阅第131页的“通过服务进行数据流:REST和RPC”)以将消息推送给使用者。这就是webhooks背后的想法[12],一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 +* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(请参阅第131页的“通过服务进行数据流:REST和RPC”)以将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 尽管这些直接消息传递系统在设计它们的情况下运行良好,但是它们通常要求应用程序代码知道消息丢失的可能性。他们可以容忍的错误是相当有限的:即使协议检测并重新传输在网络中丢失的数据包,他们通常假设生产者和消费者不断在线。 @@ -98,11 +98,11 @@ 有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(请参阅第367页的“实践中的分布式事务”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在重要的实际差异: * 数据库通常保留数据,直到被明确删除,而大多数消息代理在消息成功传递给消费者时自动删除消息。这样的消息代理不适合长期的数据存储。 -* 由于他们很快删除了邮件,大多数邮件经纪人都认为他们的工作集合相当小,即队列很短。如果代理需要缓冲很多消息,因为消费者速度较慢(如果消息不再适合内存,则可能会将消息泄漏到磁盘),每个消息需要更长的时间处理,整体吞吐量可能会降低[6]。 +* 由于他们很快删除了邮件,大多数邮件经纪人都认为他们的工作集合相当小,即队列很短。如果代理需要缓冲很多消息,因为消费者速度较慢(如果消息不再适合内存,则可能会将消息泄漏到磁盘),每个消息需要更长的时间处理,整体吞吐量可能会降低【6】。 * 数据库通常支持二级索引和各种搜索数据的方式,而消息代理通常支持某种方式订阅匹配某种模式的主题子集。机制是不同的,但是这两种方式都是客户选择想要了解的数据部分的根本途径。 * 查询数据库时,结果通常基于数据的时间点快照;如果另一个客户端随后向数据库写入更改查询结果的内容,则第一个客户端不会发现其先前的结果现在已过期(除非它重复查询或轮询更改)。相比之下,消息代理不支持任意查询,但是当数据发生变化时(即新消息可用时),它们会通知客户端。 -这是消息代理的传统观点,它被封装在JMS [14]和AMQP [15]等标准中,并以RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO企业消息服务,IBM MQ,Azure服务总线和Google Cloud Pub/Sub [16]。 +这是消息代理的传统观点,它被封装在JMS 【14】和AMQP 【15】等标准中,并以RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO企业消息服务,IBM MQ,Azure服务总线和Google Cloud Pub/Sub 【16】。 #### 多个消费者 @@ -161,7 +161,7 @@ **图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】。 #### 日志与传统消息相比 @@ -192,7 +192,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 让我们来做一个后台计算。在撰写本文时,典型的大型硬盘驱动器容量为6TB,顺序写入吞吐量为150MB / s。如果您以最快的速度写邮件,则需要大约11个小时才能填满驱动器。因此,磁盘可以缓存11个小时的消息,之后它将开始覆盖旧的消息。即使您使用多个硬盘驱动器和机器,这个比率也是一样的。在实践中,部署很少使用磁盘的完整写入带宽,所以日志通常可以保存几天甚至几周的缓冲区。 -不管你保留多长时间的消息,一个日志的吞吐量或多或少保持不变,因为无论如何每个消息都被写入磁盘[18]。这种行为与将邮件默认保存在内存中的消息传递系统形成鲜明对比,如果队列变得太大,只将其写入磁盘:当这些系统开始写入磁盘时,这些系统速度很快,并且变慢得多,所以吞吐量取决于保留的历史数量。 +不管你保留多长时间的消息,一个日志的吞吐量或多或少保持不变,因为无论如何每个消息都被写入磁盘【18】。这种行为与将邮件默认保存在内存中的消息传递系统形成鲜明对比,如果队列变得太大,只将其写入磁盘:当这些系统开始写入磁盘时,这些系统速度很快,并且变慢得多,所以吞吐量取决于保留的历史数量。 #### 当消费者跟不上生产者时 @@ -210,7 +210,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 处理的唯一副作用,除了消费者的任何产出之外,消费者补偿正在向前发展。但是偏移量是在消费者的控制之下的,所以如果需要的话可以很容易地被操纵:例如,你可以用昨天的偏移量开始一个消费者的副本,并将输出写到不同的位置,以便重新处理最后一天值得的消息。您可以重复这个任意次数,改变处理代码。 -这方面使得基于日志的消息传递更像上一章的批处理过程,其中派生数据通过可重复的转换过程与输入数据明确分离。它允许更多的实验,更容易从错误和错误中恢复,使其成为在组织内集成数据流的好工具[24]。 +这方面使得基于日志的消息传递更像上一章的批处理过程,其中派生数据通过可重复的转换过程与输入数据明确分离。它允许更多的实验,更容易从错误和错误中恢复,使其成为在组织内集成数据流的好工具【24】。 @@ -269,7 +269,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 数据库触发器可用于通过注册触发器来实现更改数据捕获(请参阅第152页的“基于触发器的复制”),这些触发器可观察数据表的所有更改,并将相应的条目添加到更改日志表中。但是,他们往往是脆弱的,并有显着的性能开销。解析复制日志可以是一个更强大的方法,但它也带来了挑战,例如处理模式更改。 -LinkedIn的Databus [25],Facebook的Wormhole [26]和Yahoo!的Sherpa [27]大规模地使用这个想法。 Bottled Water使用解码预写日志的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使用解码预写日志的API来实现PostgreSQL的CDC 【28】,Maxwell和Debezium通过解析binlog为MySQL做类似的事情【29,30,31】,Mongoriver读取MongoDB oplog 【32,33】 ,而GoldenGate为Oracle提供类似的功能【34,35】。 像消息代理一样,更改数据捕获通常是异步的:记录数据库系统不会等待更改应用到消费者,然后再进行更改。这种设计具有的操作优势是添加缓慢的使用者不会影响记录系统太多,但是它具有所有复制滞后问题的缺点(请参见第161页中的“复制滞后问题”)。 @@ -297,11 +297,11 @@ 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中,它就可以用来更新派生的数据系统,比如搜索索引,也可以用于本章稍后讨论的流处理系统。 ### 事件源 @@ -316,9 +316,9 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 例如,存储“学生取消课程注册”事件清楚地表达了单一行为的中性意图,而副作用“从注册表中删除了一个条目,并且一个取消原因被添加到学生反馈表“嵌入了很多有关方式的假设数据稍后将被使用。如果引入新的应用程序功能,例如“将地点提供给等待列表中的下一个人” - 事件采购方法允许将新的副作用轻松地链接到现有事件上。 -事件采购类似于编年史数据模型[45],事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(请参阅第93页上的“星星和雪花:分析模式”) 。 +事件采购类似于编年史数据模型【45】,事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(请参阅第93页上的“星星和雪花:分析模式”) 。 -专门的数据库如Event Store [46]已经被开发来支持使用事件采购的应用程序,但总的来说,这个方法是独立于任何特定的工具的。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用程序。 +专门的数据库如Event Store 【46】已经被开发来支持使用事件采购的应用程序,但总的来说,这个方法是独立于任何特定的工具的。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用程序。 #### 从事件日志中导出当前状态 @@ -335,7 +335,7 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 命令和事件 -事件采购哲学是仔细区分事件和命令[48]。当来自用户的请求首先到达时,它最初是一个命令:在这一点上它可能仍然失败,例如因为违反了一些完整性条件。应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变成一个持久且不可变的事件。 +事件采购哲学是仔细区分事件和命令【48】。当来自用户的请求首先到达时,它最初是一个命令:在这一点上它可能仍然失败,例如因为违反了一些完整性条件。应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变成一个持久且不可变的事件。 例如,如果用户试图注册特定用户名,或在飞机上或剧院中预定座位,则应用程序需要检查用户名或座位是否已被占用。 (我们先前在第364页的“容错概念”中讨论过这个例子。)当检查成功时,应用程序可以生成一个事件来指示特定的用户名是由特定的用户ID注册的,座位已经预留给特定的顾客。 @@ -361,7 +361,7 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 **图11-6 当前应用程序状态和事件流之间的关系** -如果你持久地存储更新日志,那么这只是使状态重现的效果。如果你认为事件的日志是你的记录系统,并且从它派生出任何可变状态,那么就更容易推断通过系统的数据流。正如帕特·赫兰(Pat Helland)所说的[52]: +如果你持久地存储更新日志,那么这只是使状态重现的效果。如果你认为事件的日志是你的记录系统,并且从它派生出任何可变状态,那么就更容易推断通过系统的数据流。正如帕特·赫兰(Pat Helland)所说的【52】: > 事务日志记录对数据库所做的所有更改。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容会保存日志中最新记录值的缓存。事实是日志。数据库是日志子集的缓存。该缓存子集恰好是来自日志的每个记录和索引值的最新值。 @@ -369,23 +369,23 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 不可变事件的优点 -数据库中的不变性是一个古老的想法。例如,会计师在数个世纪以来一直使用不变性财务簿记。当一笔交易发生时,它被记录在一个仅追加分类帐中,这本质上是描述货币,商品或服务已经转手的事件日志。账目,如损益或资产负债表,是从分类账中的交易中加起来得来的[53]。 +数据库中的不变性是一个古老的想法。例如,会计师在数个世纪以来一直使用不变性财务簿记。当一笔交易发生时,它被记录在一个仅追加分类帐中,这本质上是描述货币,商品或服务已经转手的事件日志。账目,如损益或资产负债表,是从分类账中的交易中加起来得来的【53】。 -如果发生错误,会计师不会删除或更改分类帐中的错误交易 - 而是增加另一笔交易,以补偿错误,例如退还不正确的费用。不正确的交易将永远保留在分类帐中,因为审计原因可能很重要。如果从不正确的分类账导出的错误数字已经公布,那么下一个会计期间的数字就包括一个更正。这个过程在会计中是完全正常的[54]。 +如果发生错误,会计师不会删除或更改分类帐中的错误交易 - 而是增加另一笔交易,以补偿错误,例如退还不正确的费用。不正确的交易将永远保留在分类帐中,因为审计原因可能很重要。如果从不正确的分类账导出的错误数字已经公布,那么下一个会计期间的数字就包括一个更正。这个过程在会计中是完全正常的【54】。 尽管这种可审计性在金融系统中尤其重要,但对于不受这种严格管制的许多其他系统也是有益的。如“批处理输出的哲学”(第439页)中所述,如果您意外地部署了将错误数据写入数据库的错误代码,那么如果代码能够破坏性地覆盖数据,恢复将更加困难。通过不可变事件的追加日志,诊断发生的事情和从问题中恢复起来要容易得多。 -不可变的事件也捕获比当前状态更多的信息。例如,在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然第二个事件从订单履行角度取消了第一个事件,但为了分析目的,客户正在考虑某个特定项目,但是之后决定采取反对措施。也许他们会选择在未来购买,或者他们找到替代品。这个信息被记录在一个事件日志中,但是当它们从购物车中被删除时,这个信息会丢失在删除项目的数据库中[42]。 +不可变的事件也捕获比当前状态更多的信息。例如,在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然第二个事件从订单履行角度取消了第一个事件,但为了分析目的,客户正在考虑某个特定项目,但是之后决定采取反对措施。也许他们会选择在未来购买,或者他们找到替代品。这个信息被记录在一个事件日志中,但是当它们从购物车中被删除时,这个信息会丢失在删除项目的数据库中【42】。 #### 从同一事件日志中获取多个视图 -而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作(图11-5):例如,分析数据库Druid使用这种方法从Kafka直接获取[55],Pista chio是一个分布式的键值存储,使用Kafka作为提交日志[56],Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引[41]。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 +而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作(图11-5):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 从事件日志到数据库有一个明确的转换步骤,可以更容易地随时间推移您的应用程序:如果您想要引入一个以新的方式呈现现有数据的新功能,您可以使用事件日志来构建一个单独的新功能的读取优化视图,并与现有的一起运行 -系统而不必修改它们。并行运行旧系统和新系统通常比在现有系统中执行复杂的模式迁移更容易。一旦旧的系统不再需要,你可以简单地关闭它并回收它的资源[47,57]。 +系统而不必修改它们。并行运行旧系统和新系统通常比在现有系统中执行复杂的模式迁移更容易。一旦旧的系统不再需要,你可以简单地关闭它并回收它的资源【47,57】。 -如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见第3章)。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)[42,58,59]。 +如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见第3章)。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)【42,58,59】。 数据库和模式设计的传统方法是基于数据必须以与查询相同的形式写入的谬误。有关正常化和非规范化的争论(请参阅第31页上的“多对一和多对多关系”),如果可以将数据从写入优化的事件日志转换为读取优化的应用程序状态,则变得基本无关紧要:在读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程为您提供了一种机制,使其与事件日志保持一致。 @@ -399,19 +399,19 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 另一方面,从事件日志导出当前状态也简化了并发控制的某些方面。对多个对象事务的需求(请参阅第228页上的“单对象和多对象操作”)源于单个用户操作,需要在多个不同的位置更改数据。通过事件采购,您可以设计一个事件,以便对用户操作进行独立的描述。用户操作只需要在一个地方进行一次写操作,即将事件附加到日志中,这很容易使原子化。 -如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(另请参阅第252页的“实际的串行执行”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性[24]。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在第12章讨论。 +如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(另请参阅第252页的“实际的串行执行”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在第12章讨论。 #### 不变性的限制 许多不使用事件源模型的系统依赖于不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参见“索引和快照隔离” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 -永远保持所有变化的不变的历史在多大程度上是可行的?答案取决于数据集中的流失量。一些工作负载主要是添加数据,很少更新或删除;他们很容易使不变。其他工作负载在较小的数据集上有较高的更新和删除率;在这些情况下,不可改变的历史可能变得过于庞大,碎片化可能成为一个问题,压缩和垃圾收集的表现对于操作的鲁棒性变得至关重要[60,61]。 +永远保持所有变化的不变的历史在多大程度上是可行的?答案取决于数据集中的流失量。一些工作负载主要是添加数据,很少更新或删除;他们很容易使不变。其他工作负载在较小的数据集上有较高的更新和删除率;在这些情况下,不可改变的历史可能变得过于庞大,碎片化可能成为一个问题,压缩和垃圾收集的表现对于操作的鲁棒性变得至关重要【60,61】。 除了性能方面的原因外,也可能出于管理方面的原因需要删除数据的情况,尽管这些数据都是不可变的。例如,隐私条例可能要求在关闭帐户后删除用户的个人信息,数据保护立法可能要求删除错误的信息,或者可能需要包含敏感信息的意外泄露。 -在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 - 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision [62],而Fossil版本控制系统有一个类似的概念叫做shunning [63]。 +在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 - 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision 【62】,而Fossil版本控制系统有一个类似的概念叫做shunning 【63】。 -真正的删除数据是非常困难的[64],因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位[52],而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“立法和自律”中所看到的第542页。 +真正的删除数据是非常困难的【64】,因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位【52】,而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“立法和自律”中所看到的第542页。 @@ -444,13 +444,13 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 复杂的事件处理 -复杂事件处理(CEP)是20世纪90年代为分析事件流而开发的一种方法,尤其适用于需要搜索某些事件模式的应用程序[65,66]。与正则表达式允许您在字符串中搜索特定字符模式的方式类似,CEP允许您指定规则以在流中搜索某些事件模式。 +复杂事件处理(CEP)是20世纪90年代为分析事件流而开发的一种方法,尤其适用于需要搜索某些事件模式的应用程序【65,66】。与正则表达式允许您在字符串中搜索特定字符模式的方式类似,CEP允许您指定规则以在流中搜索某些事件模式。 -CEP系统通常使用高级声明式查询语言(如SQL或图形用户界面)来描述应该检测到的事件模式。这些查询被提交给一个处理引擎,该引擎使用输入流并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个复杂的事件(因此名字)与事件模式的细节[67]。 +CEP系统通常使用高级声明式查询语言(如SQL或图形用户界面)来描述应该检测到的事件模式。这些查询被提交给一个处理引擎,该引擎使用输入流并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个复杂的事件(因此名字)与事件模式的细节【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】。 #### 流分析 @@ -464,23 +464,23 @@ CEP的实现包括Esper [69],IBM InfoSphere Streams [70],Apama,TIBCO Strea -流分析系统有时使用概率算法,例如Bloom filter(我们在第79页的“性能优化”中遇到过),设置成员资格,HyperLogLog [72]基数估计以及各种百分比估计算法(请参阅“Percentiles in Practice “第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化[73]。 -许多开源分布式流处理框架的设计都是以分析为基础的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams [74]。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 +流分析系统有时使用概率算法,例如Bloom filter(我们在第79页的“性能优化”中遇到过),设置成员资格,HyperLogLog 【72】基数估计以及各种百分比估计算法(请参阅“Percentiles in Practice “第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化【73】。 +许多开源分布式流处理框架的设计都是以分析为基础的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 #### 保持物化视图 -我们在第451页的“数据库和数据流”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“聚合:数据多维数据集和实例化视图”(第101页)):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改[50]。 +我们在第451页的“数据库和数据流”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“聚合:数据多维数据集和实例化视图”(第101页)):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改【50】。 同样,在事件采购中,应用程序状态通过应用事件日志来维护;这里的应用状态也是一种物化视图。与流分析场景不同,在某个时间窗口内仅考虑事件通常是不够的:构建物化视图可能需要在任意时间段内的所有事件,除了可能由日志压缩丢弃的任何过时事件(请参阅“日志压缩“)。实际上,您需要一个可以一直延伸到一开始的窗口。 -原则上,任何流处理器都可以用于物化视图维护,尽管永久维护事件的需要与一些主要在有限持续时间的窗口上运行的面向分析的框架的假设背道而驰。 Samza和Kafka Streams支持这种用法,建立在Kafka对夯实的支持上[75]。 +原则上,任何流处理器都可以用于物化视图维护,尽管永久维护事件的需要与一些主要在有限持续时间的窗口上运行的面向分析的框架的假设背道而驰。 Samza和Kafka Streams支持这种用法,建立在Kafka对夯实的支持上【75】。 #### 在流上搜索 除了允许搜索由多个事件组成的模式的CEP外,还有时需要基于复杂的标准(例如全文搜索查询)来搜索单个事件。 -例如,媒体监测服务可以订阅新闻文章和媒体广播,并搜索任何关于公司,产品或感兴趣的话题的新闻。这是通过预先制定一个搜索查询来完成的,然后不断地将新闻项目流与这个查询进行匹配。在一些网站上也有类似的功能:例如,房地产网站的用户在市场上出现符合其搜索条件的新房产时,可以要求通知。 Elasticsearch [76]的渗滤器功能是实现这种流式搜索的一种选择。 +例如,媒体监测服务可以订阅新闻文章和媒体广播,并搜索任何关于公司,产品或感兴趣的话题的新闻。这是通过预先制定一个搜索查询来完成的,然后不断地将新闻项目流与这个查询进行匹配。在一些网站上也有类似的功能:例如,房地产网站的用户在市场上出现符合其搜索条件的新房产时,可以要求通知。 Elasticsearch 【76】的渗滤器功能是实现这种流式搜索的一种选择。 -传统的搜索引擎首先索引文件,然后在索引上运行查询。相比之下,搜索一个数据流将会处理它的头部:查询被存储,文档通过查询运行,就像CEP一样。在最简单的情况下,您可以针对每个查询来测试每个文档,但是如果您有大量查询,这可能会变慢。为了优化过程,可以对查询和文档进行索引,从而缩小可能匹配的查询集合[77]。 +传统的搜索引擎首先索引文件,然后在索引上运行查询。相比之下,搜索一个数据流将会处理它的头部:查询被存储,文档通过查询运行,就像CEP一样。在最简单的情况下,您可以针对每个查询来测试每个文档,但是如果您有大量查询,这可能会变慢。为了优化过程,可以对查询和文档进行索引,从而缩小可能匹配的查询集合【77】。 #### 消息传递和RPC @@ -491,7 +491,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 * 参与者之间的交流往往是短暂的,而且是一对一的,而事件日志则是持久的,多用户的。 * 参与者可以以任意方式进行通信(包括循环请求/响应模式),但流处理器通常设置在非循环流水线中,其中每个流是一个特定作业的输出,并且从一组明确定义的输入流派生。 -也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户[78]。 (另请参阅“多分区数据处理”(第514页)。) +也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户【78】。 (另请参阅“多分区数据处理”(第514页)。) 也可以使用actor框架来处理流。但是,很多这样的框架在崩溃的情况下不能保证消息的传递,所以这个过程不是容错的,除非你实现了额外的重试逻辑。 @@ -503,7 +503,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间表是历史的一年,而不是几分钟的处理。而且,在事件中使用时间戳允许处理确定性的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“故障容错”在页面429)。 -另一方面,许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口[79]。这种方法具有简单的优点,事件创建和事件处理之间的延迟可以忽略不计。然而,如果存在任何显着的处理滞后,即处理可能比事件实际发生的时间显着晚,则会中断处理。 +另一方面,许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口【79】。这种方法具有简单的优点,事件创建和事件处理之间的延迟可以忽略不计。然而,如果存在任何显着的处理滞后,即处理可能比事件实际发生的时间显着晚,则会中断处理。 #### 事件时间与处理时间 @@ -527,12 +527,12 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 例如,假设您将事件分组为一分钟的窗口,以便您可以统计每分钟的请求数。你已经计算了一些事件,这些事件的时间戳是在第37分钟的时间落下的,时间已经推移了。现在大部分的事件都在一小时的第38和第39分钟之内。你什么时候宣布你已经完成了第37分钟的窗口,并输出其计数器值? -在一段时间没有看到任何新的事件之后,您可以超时并宣布一个窗口,但仍然可能发生某些事件被缓存在另一台计算机上,由于网络中断而延迟。您需要能够处理窗口已经声明完成后到达的这样的滞留事件。大体上,你有两个选择[1]: +在一段时间没有看到任何新的事件之后,您可以超时并宣布一个窗口,但仍然可能发生某些事件被缓存在另一台计算机上,由于网络中断而延迟。您需要能够处理窗口已经声明完成后到达的这样的滞留事件。大体上,你有两个选择【1】: 1. 忽略这些零散的事件,因为它们在正常情况下可能只是一小部分事件。您可以将丢弃事件的数量作为度量标准进行跟踪,并在您开始丢弃大量数据时发出警报。 2. 发布一个更正,更新的窗口与包含散兵队员的价值。您可能还需要收回以前的输出。 -在某些情况下,可以使用特殊的消息来指示“从现在开始,不会有比t更早的时间戳的消息”,消费者可以使用它来触发窗口[81]。但是,如果不同机器上的多个生产者正在生成事件,每个事件都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下添加和删除生产者是比较棘手的。 +在某些情况下,可以使用特殊的消息来指示“从现在开始,不会有比t更早的时间戳的消息”,消费者可以使用它来触发窗口【81】。但是,如果不同机器上的多个生产者正在生成事件,每个事件都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下添加和删除生产者是比较棘手的。 #### 你用的是什么时间? @@ -540,7 +540,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 在这种情况下,根据移动设备的本地时钟,事件的时间戳实际上应该是发生用户交互的时间。但是,用户控制的设备上的时钟通常是不可信的,因为它可能会被意外或故意设置为错误的时间(请参见“时钟同步和精度”(第269页))。服务器收到事件的时间(根据服务器的时钟)更可能是准确的,因为服务器在您的控制之下,但在描述用户交互方面意义不大。 -要调整不正确的设备时钟,一种方法是记录三个时间戳[82]: +要调整不正确的设备时钟,一种方法是记录三个时间戳【82】: * 事件发生的时间,根据设备时钟 * 根据设备时钟将事件发送到服务器的时间 @@ -552,7 +552,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 #### 窗口的类型 -一旦你知道如何确定一个事件的时间戳,下一步就是决定如何定义一段时间的窗口。窗口然后可以用于聚合,例如计数事件,或计算窗口内的值的平均值。有几种窗口是常用的[79,83]: +一旦你知道如何确定一个事件的时间戳,下一步就是决定如何定义一段时间的窗口。窗口然后可以用于聚合,例如计数事件,或计算窗口内的值的平均值。有几种窗口是常用的【79,83】: ***Tumbling窗口*** @@ -574,10 +574,10 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 在第10章中,我们讨论了批处理作业如何通过关键连接数据集,以及这种连接如何构成数据管道的重要组成部分。由于流处理将数据管道概括为对无界数据集进行增量处理,因此对流进行连接的需求也完全相同。 -然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流 - 流连接,流表连接和表连接[84]。在下面的章节中,我们将通过例子来说明。 +然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流 - 流连接,流表连接和表连接【84】。在下面的章节中,我们将通过例子来说明。 流 - 流连接(窗口连接) -假设您的网站上有搜索功能,并且想要检测搜索到的网址的近期趋势。每次有人输入搜索查询时,都会记录包含查询和返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击的事件。为了计算搜索结果中每个网址的点击率,您需要将搜索操作和点击操作的事件组合在一起,这些事件通过具有相同的会话ID进行连接。广告系统需要类似的分析[85]。 +假设您的网站上有搜索功能,并且想要检测搜索到的网址的近期趋势。每次有人输入搜索查询时,都会记录包含查询和返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击的事件。为了计算搜索结果中每个网址的点击率,您需要将搜索操作和点击操作的事件组合在一起,这些事件通过具有相同的会话ID进行连接。广告系统需要类似的分析【85】。 如果用户放弃他们的搜索,点击可能永远不会到来,即使它到了,搜索和点击之间的时间可能是高度可变的:在很多情况下,它可能是几秒钟,但可能长达几天或几周(如果用户运行搜索,忘记关于该浏览器选项卡,然后返回到选项卡,稍后再单击一个结果)。由于可变的网络延迟,点击事件甚至可能在搜索事件之前到达。您可以选择合适的加入窗口,例如,如果间隔至多一小时发生一次搜索,您可以选择加入搜索。 @@ -591,7 +591,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 包含用户ID的活动事件流,并且输出是活动事件流,其中用户ID已经用关于用户的简档信息来扩充。这个过程有时被称为使用来自数据库的信息来丰富活动事件。 -要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“示例:分析用户活动事件”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载[75]。 +要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“示例:分析用户活动事件”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载【75】。 另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在408页的“Map-Side连接”中讨论的散列连接非常相似:如果数据库的本地副本足够小,则本地副本可能是内存中的散列表,或者是本地磁盘上的索引。 与批处理作业的区别在于批处理作业使用数据库的时间点快照作为输入,而流处理器是长时间运行的,并且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持最新。这个问题可以通过更改数据捕获来解决:流处理器可以订阅用户配置文件数据库的更新日志以及活动事件流。在创建或修改配置文件时,流处理器会更新其本地副本。因此,我们获得两个流之间的连接:活动事件和配置文件更新。 流表连接实际上非常类似于流 - 流连接;最大的区别在于对于表changelog流,连接使用一个可以回溯到“开始时间”(概念上是无限的窗口)的窗口,新版本的记录会覆盖较早的版本。对于流输入,连接可能根本没有维护窗口。 @@ -606,7 +606,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 * 当用户u1开始跟随用户u2时,u2最近的tweets被添加到u1的时间线上。 * 当用户u1取消关注用户u2时,u1的推文将从u1的时间线中移除。 -要在流处理器中实现这种缓存维护,需要用于推文(发送和删除)和跟随关系(跟随和取消跟随)的事件流。流过程需要维护一个包含每个用户关注者集合的数据库,以便知道当一个新的tweet到达时需要更新哪些时间轴[86]。 +要在流处理器中实现这种缓存维护,需要用于推文(发送和删除)和跟随关系(跟随和取消跟随)的事件流。流过程需要维护一个包含每个用户关注者集合的数据库,以便知道当一个新的tweet到达时需要更新哪些时间轴【86】。 查看这个流过程的另一种方式是维护一个连接两个表(tweet和follow)的查询的物化视图,如下所示: ```sql @@ -623,29 +623,29 @@ GROUP BY follows.follower_id #### 连接的时间依赖性 -这就产生了一个问题:如果不同的事件发生在相似的时间周围,他们按照何种顺序进行处理?在流表连接示例中,如果用户更新其配置文件,哪些活动事件与旧配置文件(在配置文件更新之前处理)结合,哪些与新配置文件结合(在配置文件更新之后处理)?换句话说:如果状态随着时间的推移而改变,并且你加入了某个状态,那么你使用什么时间点来加入[45]? +这就产生了一个问题:如果不同的事件发生在相似的时间周围,他们按照何种顺序进行处理?在流表连接示例中,如果用户更新其配置文件,哪些活动事件与旧配置文件(在配置文件更新之前处理)结合,哪些与新配置文件结合(在配置文件更新之后处理)?换句话说:如果状态随着时间的推移而改变,并且你加入了某个状态,那么你使用什么时间点来加入【45】? 这种时间依赖性可能发生在许多地方。例如,如果您销售东西,则需要对发票进行适当的税率,这取决于国家或州,产品类型和销售日期(因为税率会随时变化)。将销售额加入税率表时,如果您正在重新处理历史数据,您可能希望加入销售时的税率,这可能与当前的税率不同。 -如果跨流的事件排序是未确定的,那么这个连接变得不确定[87],这意味着你不能在相同的输入上重新运行相同的工作,并且必然会得到相同的结果:输入流上的事件可能交织在当你再次运行这个工作时,采用不同的方式 +如果跨流的事件排序是未确定的,那么这个连接变得不确定【87】,这意味着你不能在相同的输入上重新运行相同的工作,并且必然会得到相同的结果:输入流上的事件可能交织在当你再次运行这个工作时,采用不同的方式 -在数据仓库中,这个问题被称为缓慢变化的维度(SCD),通常通过对特定版本的联合记录使用唯一的标识符来解决:例如,每当税率改变时,新的标识符,并且发票包括销售时的税率标识符[88,89]。这种变化使连接成为确定性的,但是由于表中所有记录的版本都需要保留,导致日志压缩是不可能的。 +在数据仓库中,这个问题被称为缓慢变化的维度(SCD),通常通过对特定版本的联合记录使用唯一的标识符来解决:例如,每当税率改变时,新的标识符,并且发票包括销售时的税率标识符【88,89】。这种变化使连接成为确定性的,但是由于表中所有记录的版本都需要保留,导致日志压缩是不可能的。 ### 容错 在本章的最后一节中,让我们考虑流处理器如何容忍错误。我们在第10章中看到,批处理框架可以很容易地容忍错误:如果MapReduce作业中的任务失败,可以简单地在另一台机器上重新启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的单独文件,并且输出仅在任务成功完成时可见。 -特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 - 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 - 一次将是一个更具描述性的术语[90]。 +特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 - 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 - 一次将是一个更具描述性的术语【90】。 在流处理过程中也出现了同样的容错问题,但是处理起来不那么直观:等到某个任务完成之后才使其输出可见,因为流是无限的,因此您永远无法完成处理。 #### 小批量和检查点 -一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为microbatching,它被用于Spark Streaming [91]。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 +一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为microbatching,它被用于Spark Streaming 【91】。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 微缩也隐含地提供了与批量大小相等的翻滚窗口(通过处理时间而不是事件时间戳)。任何需要更大窗口的作业都需要明确地将状态从一个微阵列转移到下一个微阵列。 -Apache Flink中使用的一种变体方法是定期生成状态滚动检查点并将其写入持久存储器[92,93]。如果流操作符崩溃,它可以从最近的检查点重新启动,并放弃在最后一个检查点和崩溃之间生成的任何输出。检查点由消息流中的条形码触发,类似于微型图形之间的边界,但不强制特定的窗口大小。 +Apache Flink中使用的一种变体方法是定期生成状态滚动检查点并将其写入持久存储器【92,93】。如果流操作符崩溃,它可以从最近的检查点重新启动,并放弃在最后一个检查点和崩溃之间生成的任何输出。检查点由消息流中的条形码触发,类似于微型图形之间的边界,但不强制特定的窗口大小。 在流处理框架的范围内,微观网格化和检查点方法提供了与批处理一样的一次语义。但是,只要输出离开流处理器(例如,通过写入数据库,向外部消息代理发送消息或发送电子邮件),框架将不再能够放弃失败批处理的输出。在这种情况下,重新启动失败的任务会导致外部副作用发生两次,单独使用微配量或检查点不足以防止此问题。 @@ -655,17 +655,17 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 这些事情要么都是原子地发生,要么都不发生,但是不应该彼此不同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的情况下,在第360页的“准确一次的消息处理”中讨论了它。 -在第9章中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流[81,92]和VoltDB [94]中使用了这种方法,并计划在Apache Kafka [95,96]中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 +在第9章中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流【81,92】和VoltDB 【94】中使用了这种方法,并计划在Apache Kafka 【95,96】中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 #### 幂等 -我们的目标是放弃任何失败的任务的部分输出,以便他们可以安全地重试,而不会两次生效。分布式事务是实现这一目标的一种方式,但另一种方式是依赖幂等性[97]。 +我们的目标是放弃任何失败的任务的部分输出,以便他们可以安全地重试,而不会两次生效。分布式事务是实现这一目标的一种方式,但另一种方式是依赖幂等性【97】。 幂等操作是可以多次执行的操作,并且与只执行一次操作具有相同的效果。例如,将键值存储中的某个键设置为某个固定值是幂等的(再次写入该值会覆盖具有相同值的值),而递增计数器不是幂等的(再次执行递增意味着该值递增两次)。 即使一个操作不是天生的幂等,它往往可以与一些额外的元数据幂等。例如,在使用来自卡夫卡的消息时,每条消息都有一个持续的,单调递增的偏移量。将值写入外部数据库时,可以将触发上次写入的消息的偏移量与值包含在一起。因此,您可以判断是否已应用更新,并避免再次执行相同的更新。 -风暴三叉戟的状态处理基于类似的想法[78]。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值[ 98,99]。 +风暴三叉戟的状态处理基于类似的想法【78】。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值[ 98,99]。 当从一个处理节点故障转移到另一个处理节点时,可能需要进行防护(请参阅第291页上的“领导和锁定”),以防止被认为是死的节点的干扰 @@ -675,7 +675,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 一种选择是将状态保持在远程数据存储中并复制它,尽管如每个单独消息的远程数据库查询速度可能会很慢,正如在“流表加入(第473页)”中所述。另一种方法是保持流处理器的本地状态,并定期复制。然后,当流处理器从故障中恢复时,新任务可以读取复制状态并恢复处理而不丢失数据。 -例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中[92,93]。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获[84,100]。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 +例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获[84,100]。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 在某些情况下,甚至可能不需要复制状态,因为它可以从输入流重建。例如,如果状态由一个相当短的窗口中的聚合组成,则它可能足够快,以便重放与该窗口相对应的输入事件。如果状态是通过更改数据捕获维护的数据库的本地副本,那么也可以从日志压缩的更改流重建数据库(请参阅“日志压缩”一节第456页)。 diff --git a/ch12.md b/ch12.md index de0a052..2692b77 100644 --- a/ch12.md +++ b/ch12.md @@ -30,7 +30,7 @@ ### 组合使用衍生数据的工具 -例如,为了处理任意关键字的查询,需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库(如PostgreSQL)包含了全文索引功能,可以满足简单的应用需求[1],但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。 +例如,为了处理任意关键字的查询,需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库(如PostgreSQL)包含了全文索引功能,可以满足简单的应用需求【1】,但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。 我们谈到了将数据系统集成到“使系统保持同步”(第452页)的问题。随着数据的不同表示数量的增加,集成问题变得更加困难。除了数据库和搜索索引之外,也许您需要保留分析系统(数据仓库或批处理和流处理系统)中的数据副本。维护从原始数据衍生的对象的高速缓存或非规范化版本;通过机器学习,分类,排名或推荐系统传递数据;或根据对数据的更改发送通知。 @@ -85,10 +85,10 @@ 但是,在一个地方存储友谊状态并在另一个地方存储消息的系统中,不友好事件和消息发送事件之间的顺序依赖关系可能会丢失。如果未捕获到因果依赖关系,则发送有关新消息的通知的服务可能会在不友好事件之前处理消息发送事件,从而错误地向前伙伴发送通知。 -在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“连接的时间依赖性”)。不幸的是,这个问题似乎并没有一个简单的答案[2,3]。起点包括: +在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“连接的时间依赖性”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: * 逻辑时间戳可以提供没有协调的全部订购(请参见“序列号排序”第页343),因此它们可能有助于总订单广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 -* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系[4] 。我们将在第513页的“Reads are events too”中回到这个想法。 +* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在第513页的“Reads are events too”中回到这个想法。 * 冲突解决算法(请参阅“自动冲突解决”(第165页))有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用程序开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全部命令广播的瓶颈。 ### 批量处理与流处理 @@ -99,17 +99,17 @@ 正如我们在第10章和第11章中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无界数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别开始模糊。 -Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache Flink则在流处理引擎上执行批处理[5]。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳[6]。 +Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳【6】。 #### 保持衍生状态 批处理具有非常强大的功能特性(即使代码不是用函数式编程语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,处理输入作为不可变的,并作为附加的输出。流处理类似,但它扩展了运算符以允许受管理的容错状态(请参阅第478页的“重建失败后的状态”)。 -具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“幂等性”),但也简化了有关组织中数据流的推理[7]。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 +具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“幂等性”),但也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 原则上,衍生数据系统可以同步维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被本地包含,而如果任何一个参与者失败,分布式事务将中止,因此他们倾向于通过将故障扩展到系统的其余部分(请参阅第363页的“分布式事务的限制”)。 -我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的[8](另请参阅“多分区数据处理”第479页)。 +我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“多分区数据处理”第479页)。 #### 为应用程序演变重新处理数据 @@ -119,19 +119,19 @@ Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache > ### 在铁路上的模式迁移 > -> 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行,这限制了火车网络中可能的相互连接[9]。 +> 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行,这限制了火车网络中可能的相互连接【9】。 > > 在1846年最终确定了一个标准仪表之后,其他仪表的轨道必须转换 - 但是如何在不关闭火车线路的情况下进行数月甚至数年?解决的办法是首先将轨道转换为双轨或混合轨距,方法是增加第三轨。这种转换可以逐渐完成,当完成时,两个仪表的列车可以在三条轨道中的两条轨道上运行。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 > > 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准仪表仍然存在的原因。例如,旧金山湾区的BART系统使用与美国大部分地区不同的仪表。 -衍生视图允许逐步演变。如果您想重新构建数据集,则不需要执行迁移作为突然切换。相反,您可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后,您可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。逐渐地,您可以增加访问新视图的用户比例,最终您可以删除旧视图[10]。 +衍生视图允许逐步演变。如果您想重新构建数据集,则不需要执行迁移作为突然切换。相反,您可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后,您可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。逐渐地,您可以增加访问新视图的用户比例,最终您可以删除旧视图【10】。 -这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:您始终有一个可以回到的工作系统。通过降低不可逆损害的风险,您可以更有信心继续前进,从而更快地改善您的系统[11]。 +这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:您始终有一个可以回到的工作系统。通过降低不可逆损害的风险,您可以更有信心继续前进,从而更快地改善您的系统【11】。 #### Lambda架构 -如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构[12]是这方面的一个建议,引起了很多关注。 +如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构【12】是这方面的一个建议,引起了很多关注。 lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(请参阅第457页上的“事件源”)。从这些事件中,推导出读取优化的视图。 lambda体系结构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 @@ -139,13 +139,13 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 拉姆达体系结构是一个有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广将视图衍生到不可变事件流和在需要时重新处理事件的原则。但是,我也认为它有一些实际问题: -* 必须保持相同的逻辑才能在批处理和流处理框架中运行,这是额外的工作。虽然像Summingbird [13]这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然[14]。 +* 必须保持相同的逻辑才能在批处理和流处理框架中运行,这是额外的工作。虽然像Summingbird 【13】这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然【14】。 * 由于流管道和批处理管道产生单独的输出,因此需要合并它们以响应用户请求。如果计算是通过滚动窗口的简单聚合,则合并相当容易,但如果使用更复杂的操作(例如连接和会话化)导出视图,或者输出不是时间序列,则显得非常困难。 * 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会很昂贵。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了第468页的“关于时间的推理”中讨论的问题,例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 #### 统一批处理和流处理 -最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现[15]。 +最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现【15】。 在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛: @@ -161,7 +161,7 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 ## 拆分数据库 -在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据[16]。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的,Hadoop生态系统有点像Unix的分布式版本。 +在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据【16】。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的,Hadoop生态系统有点像Unix的分布式版本。 当然,有很多实际的差异。例如,许多文件系统不能很好地处理包含1000万个小文件的目录,而包含1000万个小记录的数据库是完全正常且不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。 @@ -180,7 +180,7 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni * 二级索引,使您可以根据字段的值有效地搜索记录(请参阅第79页上的“其他索引结构” * 物化视图,这是一种预先计算的查询结果缓存(请参阅“聚合:数据立方体和物化视图”(第101页) * 复制日志,保持其他节点上数据的副本最新(请参阅第158页中的“复制日志的实现”) -* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“全文搜索和模糊索引”)以及内置于某些关系数据库[1] +* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“全文搜索和模糊索引”)以及内置于某些关系数据库【1】 在第十章和第十一章中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅第357页上的“批处理工作流的输出”),了解有关实例化视图维护(请参阅“维护实例化视图”一节第437页)以及有关将更改从数据库复制到衍生数据系统(请参阅第454页的“更改数据捕获”)。 @@ -196,7 +196,7 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni #### 一切的元数据库 -有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库[7]。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并形成表单时,就像数据库子系统一样,使索引或物化视图保持最新。 +有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并形成表单时,就像数据库子系统一样,使索引或物化视图保持最新。 像这样看,批处理和流处理器就像触发器,存储过程和物化视图维护例程的精细实现。他们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引,散列索引,空间索引(请参阅第79页的“多列索引”)以及其他类型的索引。在新兴的衍生数据系统体系结构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 @@ -204,23 +204,23 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni **联合数据库:统一读取** -可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法[18,19]。例如,PostgreSQL的外部数据包装功能符合这种模式[20]。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 +可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法【18,19】。例如,PostgreSQL的外部数据包装功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 联合查询接口遵循单一集成系统的关系传统,具有高级查询语言和优雅的语义,但却是一个复杂的实现。 **非捆绑数据库:统一写入** -虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开[7,21]。 +虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。 -unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件事[22],通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)[16] 。 +unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)【16】 。 #### 开展分拆工作 联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可管理的问题。我认为保持写入到几个存储系统是同步的更困难的工程问题,所以我将重点关注它。 -传统的同步写入方法需要跨异构存储系统的分布式事务[18],我认为这是错误的解决方案(请参阅“导出的数据与分布式事务”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 +传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“导出的数据与分布式事务”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 -例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“重新访问原子提交”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“幂等性”)是一种更简单的抽象,因此在异构系统中实现更加可行[7]。 +例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“重新访问原子提交”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“幂等性”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 基于日志的集成的一大优势是各个组件之间的松散耦合,这体现在两个方面: @@ -231,7 +231,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 如果分拆确实成为未来的方式,它将不会取代目前形式的数据库 - 它们仍然会像以往一样需要。数据库仍然需要维护流处理器中的状态,并且为批处理和流处理器的输出提供查询服务(请参阅第419页上的“批处理工作流的输出”和第464页上的“处理流”)。专门的查询引擎将继续对特定的工作负载非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅第417页的“将Hadoop与分布式数据库进行比较” 。 -运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码[23]组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。 +运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码【23】组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。 分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 - 与我们在第414页上的“比较Hadoop与分布式数据库”中讨论的存储和处理模型的多样性一样。 @@ -241,19 +241,19 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 用于组成数据系统的工具正在变得越来越好,但我认为缺少一个主要部分:我们还没有Unix shell的非捆绑式数据库(即,用于组成存储和处理系统的高级语言简单和陈述的方式)。 -例如,如果我们可以简单地声明mysql |,我就会喜欢它elasticsearch,类似于Unix管道[22],这将成为CREATE INDEX的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 +例如,如果我们可以简单地声明mysql |,我就会喜欢它elasticsearch,类似于Unix管道【22】,这将成为CREATE INDEX的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 -同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“类图数据模型”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流[24,25],我希望这些想法能够在生产系统中找到自己的方法。 +同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“类图数据模型”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 ### 围绕数据流设计应用 -通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法[26],在2014年的一次会议演讲标题之后[27]。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 +通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法【26】,在2014年的一次会议演讲标题之后【27】。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 -这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是,数据流语言(如Oz [28]和Juttle [29]),功能反应式编程(FRP)语言(如Elm [30,31])和逻辑编程语言(如Bloom [ 32。 Jay Kreps [7]提出了在这种背景下解绑的术语。 +这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是,数据流语言(如Oz 【28】和Juttle 【29】),功能反应式编程(FRP)语言(如Elm 【30,31】)和逻辑编程语言(如Bloom [ 32。 Jay Kreps 【7】提出了在这种背景下解绑的术语。 -即使电子表格也具有比大多数主流编程语言遥远的数据流编程功能[33]。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格总和),并且只要公式的任何输入发生更改,公式的结果都会自动重新计算。这正是我们在数据系统级所需要的:当数据库中的记录发生更改时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 +即使电子表格也具有比大多数主流编程语言遥远的数据流编程功能【33】。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格总和),并且只要公式的任何输入发生更改,公式的结果都会自动重新计算。这正是我们在数据系统级所需要的:当数据库中的记录发生更改时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 -因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习[34]。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可扩展性以及持久存储数据。他们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。 +因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可扩展性以及持久存储数据。他们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。 在本节中,我将详细介绍这些想法,并探讨一些围绕非捆绑数据库和数据流的想法构建应用程序的方法。 @@ -266,7 +266,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 * 在机器学习系统中,我们可以将模型视为通过应用各种特征提取和统计分析功能从训练数据中导出。当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中导出的。 * 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段; UI中的更改可能需要更新缓存填充方式的定义以及重建缓存。 -辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说CREATE INDEX来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识[35]。 +辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说CREATE INDEX来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识【35】。 当创建衍生数据集的函数不是像创建二级索引那样的标准Cookie切割函数时,需要自定义代码来处理特定于应用程序的方面。而这个自定义代码是许多数据库难以抗争虽然关系数据库通常支持触发器,存储过程和用户定义的函数,它们可以用来在数据库中执行应用程序代码,但它们在数据库设计中已经有所反应了(请参阅“传输事件流”(第447页))。 @@ -277,7 +277,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用程序代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。 我认为让系统的某些部分专门用于持久数据存储以及专门运行应用程序代码的其他部分是有意义的。这两者可以在保持独立的同时互动。 -现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中[36]。正如职能规划界人士喜欢开玩笑说的那样,“我们相信教会与国家的分离”【37】 [^i] +现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如职能规划界人士喜欢开玩笑说的那样,“我们相信教会与国家的分离”【37】 [^i] [^i]: 解释一个笑话很少改进它,但我不想让任何人感到被遗漏。 在这里,Church是数学家Alonzo Church的参考资料,他创建了lambda演算,这是早期的计算形式,是大多数函数式编程语言的基础。 lambda演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与Church的工作是分开的。 @@ -291,7 +291,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 从数据流的角度思考应用意味着重新谈判应用代码和状态管理之间的关系。我们不是将数据库视为被应用程序操纵的被动变量,而是更多地考虑状态,状态更改和处理它们的代码之间的相互作用和协作。应用程序代码通过在另一个地方触发状态更改来响应状态更改。 -我们在第451页的“数据库和数据流”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“消息传递数据流”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应[38,39]。 +我们在第451页的“数据库和数据流”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“消息传递数据流”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应【38,39】。 如前所述,当触发器由于数据更改而触发时,或者次级索引更新以反映索引表中的更改时,数据库内部会发生类似的情况。分解数据库意味着将此想法应用于在主数据库之外创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 @@ -307,9 +307,9 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 当前流行的应用程序开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的服务(请参阅第121页的“通过服务实现数据流:REST和RPC”)。这种面向服务的体系结构优于单个单一应用程序的优势主要在于通过松散耦合的组织可伸缩性:不同的团队可以在不同的服务上工作,从而减少团队之间的协调工作(只要服务可以独立部署和更新)。 -将流操作符合并到数据流系统中与微服务方法有很多相似的特征[40]。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。 +将流操作符合并到数据流系统中与微服务方法有很多相似的特征【40】。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。 -除了第136页上的“消息传递数据流”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现[40,41]: +除了第136页上的“消息传递数据流”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: 1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库以获取特定货币的当前汇率。 2. 在数据流方法中,处理采购的代码将提前订阅汇率更新流,并在当地数据库发生更改时将当前汇率记录下来。处理采购时,只需查询本地数据库即可。 @@ -364,9 +364,9 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 传统上,网络浏览器是无状态的客户端,只有在您连接到互联网时才能做有用的事情(只有您可以离线执行的唯一的事情是在您之前在线加载的页面上下滚动)。然而,最近的“单页面”JavaScript Web应用程序已经获得了很多有状态的功能,包括客户端用户界面交互和Web浏览器中的持久本地存储。移动应用程序可以类似地在设备上存储大量状态,并且不需要往返于大多数用户交互的服务器。 -这些不断变化的功能引发了对离线优先应用程序的重新兴趣,这些应用程序尽可能地在同一设备上使用本地数据库,无需连接互联网,并且在网络连接时与后台远程服务器同步可用[42]。由于移动设备通常具有缓慢且不可靠的蜂窝互联网连接,因此,如果用户的用户界面不必等待同步网络请求,并且应用程序大多离线工作,则对用户来说是一大优势(请参阅“具有离线操作的客户端”第170页)。 +这些不断变化的功能引发了对离线优先应用程序的重新兴趣,这些应用程序尽可能地在同一设备上使用本地数据库,无需连接互联网,并且在网络连接时与后台远程服务器同步可用【42】。由于移动设备通常具有缓慢且不可靠的蜂窝互联网连接,因此,如果用户的用户界面不必等待同步网络请求,并且应用程序大多离线工作,则对用户来说是一大优势(请参阅“具有离线操作的客户端”第170页)。 -当我们摆脱无国籍客户与中央数据库交谈的假设,并转向终端用户设备上维护的状态时,开启了一个全新的机会。特别是,我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的像素是客户端应用程序中模型对象的物化视图;模型对象是远程数据中心的本地状态副本[27]。 +当我们摆脱无国籍客户与中央数据库交谈的假设,并转向终端用户设备上维护的状态时,开启了一个全新的机会。特别是,我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的像素是客户端应用程序中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。 #### 将状态更改推送给客户端 @@ -374,13 +374,13 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 更新的协议已经超越了HTTP的基本请求/响应模式:服务器发送的事件(EventSource API)和WebSockets提供了通信渠道,通过这些通信渠道,Web浏览器可以与服务器保持开放的TCP连接,服务器可以只要保持连接状态,就会主动将消息推送到浏览器。这为服务器提供了一个机会,主动通知最终用户客户端本地存储状态的任何变化,从而减少客户端状态的陈旧程度。 -就我们的写入路径和读取路径模型而言,主动将状态改变到客户端设备意味着将写入路径一直延伸到最终用户。当客户端首次初始化时,它仍然需要使用读取路径来获取其初始状态,但此后可能依赖于服务器发送的状态更改流。我们在流处理和消息传递方面讨论的想法并不局限于仅在数据中心运行:我们可以进一步采用这些想法,并将它们一直延伸到终端用户设备[43]。 +就我们的写入路径和读取路径模型而言,主动将状态改变到客户端设备意味着将写入路径一直延伸到最终用户。当客户端首次初始化时,它仍然需要使用读取路径来获取其初始状态,但此后可能依赖于服务器发送的状态更改流。我们在流处理和消息传递方面讨论的想法并不局限于仅在数据中心运行:我们可以进一步采用这些想法,并将它们一直延伸到终端用户设备【43】。 这些设备有时会脱机,并且在此期间无法收到服务器状态更改的任何通知。但是我们已经解决了这个问题:在第449页的“消费者偏移量”中,我们讨论了基于日志的消息中介的使用者在失败或断开连接后可以重新连接,并确保它不会错过任何到达的消息它被断开。同样的技术适用于单个用户,每个设备都是小事件的小用户。 #### 端到端的事件流 -最近用于开发有状态客户端和用户界面的工具(如Elm语言[30]和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“事件源”)。 +最近用于开发有状态客户端和用户界面的工具(如Elm语言【30】和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“事件源”)。 将这种编程模型扩展为允许服务器将状态改变事件推送到客户端事件管道中是非常自然的。因此,状态变化可以通过端到端的写入路径流动:从触发状态改变的一个设备上的交互,通过事件日志以及通过多个衍生的数据系统和流处理器,一直到用户界面在另一台设备上观察状态的人。这些状态变化可以以相当低的延迟传播 - 比如说,在一秒内结束。 @@ -388,46 +388,46 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 挑战在于无状态客户端和请求/响应交互的假设在我们的数据库,库,框架和协议中非常深入。许多数据存储支持读取和写入操作,请求返回一个响应,但是少得多提供订阅更改的能力 - 即随着时间的推移返回响应流的请求(请参阅“更改流的API支持” 。 -为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流[27]。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。 +为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流【27】。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。 #### 读也是事件 我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写入路径和读取路径之间的边界。该商店允许对数据进行随机访问读取查询,否则这些查询将需要扫描整个事件日志。 -在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“流连接”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询[45],将流处理器本身变成一种简单的数据库。 +在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“流连接”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 -我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件[46]。 +我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件【46】。 当写入和读取都被表示为事件,并且被路由到同一个流操作符以便处理时,我们实际上是在读查询流和数据库之间执行流表连接。读取事件需要发送到保存数据的数据库分区(请参阅第214页的“请求路由”),就像批处理和流处理器在连接时需要在同一个键上共同输入一样(请参阅“Reduce-Side连接和分组“)。 -服务请求和正在执行的连接之间的这种对应关系是非常重要的[47]。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。 +服务请求和正在执行的连接之间的这种对应关系是非常重要的【47】。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。 -记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品[4]。要分析此连接,您需要记录用户查询运输和库存状态的结果。 +记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品【4】。要分析此连接,您需要记录用户查询运输和库存状态的结果。 -将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“订购事件以捕获因果关系”),但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题[2]。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 +将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“订购事件以捕获因果关系”),但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 #### 多分区数据处理 对于只涉及单个分区的查询,通过流发送查询和收集响应流的努力可能是过度的。然而,这个想法打开了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和加入的基础设施。 -Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“消息传递和RPC”)。例如,它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合[48]。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 +Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“消息传递和RPC”)。例如,它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合【48】。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 -这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,您可以检查用户的IP地址,电子邮件地址,帐单地址,送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的,因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合[49]。 +这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,您可以检查用户的IP地址,电子邮件地址,帐单地址,送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的,因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合【49】。 MPP数据库的内部查询执行图具有相似的特征(请参阅第417页的“比较Hadoop与分布式数据库”)。如果您需要执行这种多分区连接,使用提供此功能的数据库可能比使用流处理器实现它更简单。但是,将查询视为流提供了一个选项,可以实现大规模应用程序,这些应用程序可以在传统的现成解决方案的限制下运行。 ## 目标是正确性 -对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考[50]。 +对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考【50】。 我们希望构建可靠和正确的应用程序(即,即使面对各种故障,其语义也能很好地定义和理解的程序)。大约四十年来,原子性,隔离性和耐久性(第7章)的交易特性一直是构建正确应用的首选工具。但是,这些基础比看起来更弱:例如见证弱隔离级别的混合(请参见“弱隔离级别”(第233页))。 在某些领域,事务被完全抛弃,并被提供更好性能和可伸缩性的模型取代,但是更复杂的语义(例如,请参阅第167页上的“无Leaderless复制”)。一致性经常被讨论,但定义不明确(参见第224页的“一致性”和第9章)。有些人主张我们应该“为了更好的可用性而拥抱弱一致性”,而对实际上的实际意义缺乏清晰的认识。 -对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的[51,52]。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。 +对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的【51,52】。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。 -例如,凯尔金斯伯里(Kyle Kingsbury)的杰普森(Jepsen)实验[53]强调了一些产品声称的安全保证与存在网络问题和崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用程序代码仍然需要正确使用它们提供的功能,如果配置很难理解,这是很容易出错的(这是弱隔离级别,法定配置, 等等)。 +例如,凯尔金斯伯里(Kyle Kingsbury)的杰普森(Jepsen)实验【53】强调了一些产品声称的安全保证与存在网络问题和崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用程序代码仍然需要正确使用它们提供的功能,如果配置很难理解,这是很容易出错的(这是弱隔离级别,法定配置, 等等)。 如果您的应用程序可以容忍偶尔以不可预测的方式破坏或丢失数据,那么生活就会简单得多,您可能只需横过手指就能逃脱,希望获得最好的效果。另一方面,如果您需要更强的正确性保证,那么可序列化和原子提交就是建立的方法,但是它们是有代价的:它们通常只在单个数据中心(排除地理分布式体系结构)中工作,您可以实现的规模和容错性能。 @@ -464,16 +464,16 @@ BEGIN TRANSACTION; COMMIT; ``` -客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管例12-1是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作[3]。 +客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管例12-1是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作【3】。 两阶段提交(请参阅第354页上的“原子提交和两阶段提交(2PC)”)协议会破坏TCP连接和事务之间的1:1映射,因为它们必须允许事务协调器在数据库之后重新连接到数据库一个网络故障,并告诉它是否提交或中止有疑问的交易。这足以确保交易只能执行一次吗?不幸的是,即使我们可以抑制数据库客户端和服务器之间的重复事务,我们仍然需要担心最终用户设备和应用程序服务器之间的网络。例如,如果最终用户客户端是Web浏览器,则可能使用HTTP POST请求向服务器提交指令。也许用户处于一个微弱的蜂窝数据连接,他们成功地发送POST,但是信号在他们能够从服务器接收响应之前变得太弱。 -在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” - 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式[54]可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 +在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” - 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式【54】可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 #### 操作标识符 要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 - 您需要考虑请求的端到端流。 -例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID [3]。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如示例12-2中所示。 +例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID 【3】。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如示例12-2中所示。 **例12-2 使用唯一的ID来抑制重复的请求** ```sql @@ -493,14 +493,14 @@ COMMIT; **端到端的论点** -抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述[55]: +抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述【55】: 只有在通信系统端点的应用程序的知识和帮助下,所讨论的功能才能够完全正确地实现。因此,将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统提供的功能的不完整版本可能有助于提高性能。) 在我们的例子中,有问题的函数是重复抑制。我们看到TCP在TCP连接级别抑制重复的数据包,一些流处理器在消息处理级别提供了所谓的唯一的语义,但是这不足以防止用户提交重复请求一次。 TCP,数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从最终用户客户端一直传递到数据库的事务标识符。 端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到发送和接收软件中的错误网络连接的末端,或数据存储在磁盘上的损坏。如果您想要捕获所有可能的数据损坏源,则还需要端到端的校验和。 -类似的说法适用于加密[55]:家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量,但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者,而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。 +类似的说法适用于加密【55】:家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量,但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者,而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。 尽管低级功能(TCP复制抑制,以太网校验和,WiFi加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们可以降低较高级别出现问题的可能性。例如,如果我们没有TCP将数据包放回正确的顺序,那么HTTP请求通常会被破坏。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 @@ -534,13 +534,13 @@ COMMIT; 唯一性检查可以根据需要唯一的值进行划分。例如,如果需要通过请求标识确保唯一性(如例12-2所示),则可以确保具有相同请求标识的所有请求都路由到同一分区(请参阅第6章)。如果您需要用户名是唯一的,您可以通过用户名的哈希分区。 -但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“实现可线性化系统”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的[56]。 +但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“实现可线性化系统”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的【56】。 #### 基于日志的消息传递的唯一性 该日志确保所有消费者以相同的顺序查看消息 - 这种保证在形式上被称为全部命令广播并且等同于共识(参见第346页上的“全部命令广播”)。在使用基于日志的消息传递的非捆绑数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 -流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“与传统消息传递相比的日志”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下[57]: +流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“与传统消息传递相比的日志”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下【57】: 1. 对用户名的每个请求都被编码为一条消息,并附加到由用户名散列确定的分区。 2. 流处理器使用本地数据库连续读取日志中的请求,以跟踪使用哪些用户名。对于每个可用的用户名请求,它都会记录该名称并将成功消息发送到输出流。对于每个已经被使用的用户名请求,它都会向输出流发送拒绝消息。 @@ -548,7 +548,7 @@ COMMIT; 该算法基本上与第363页上的“使用全阶广播实现线性化存储”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量,因为可以独立处理每个分区。 -该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“写入偏移和幻影”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似[58]。 +该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“写入偏移和幻影”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似【58】。 #### 多分区请求处理 @@ -590,7 +590,7 @@ CAP定理(参见第359页的“线性化成本”)使用线性化的意义 我要断言,在大多数应用中,完整性比时间要重要得多。违反时效可能令人讨厌和混淆,但是对正直的侵犯可能是灾难性的。 -例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要[3]。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 +例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要【3】。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 #### 数据流系统的正确性 @@ -617,8 +617,8 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 然而,另一个要认识到的是,许多真正的应用程序实际上可以摆脱唯一性较弱的概念: -* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易[59,60]。 -* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做[61]。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 +* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易【59,60】。 +* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做【61】。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 * 同样地,许多航空公司预计飞行员会错过飞机,许多旅馆超额预订客房,预计部分客人将取消预订。在这些情况下,出于商业原因故意违反了“每个座位一人”的约束,并且处理补偿过程(退款,升级,在邻近酒店提供免费房间)以处理需求超过供应的情况。即使没有超额预订,为了应对由于恶劣天气而被取消的航班或者罢工的员工,这些问题的恢复仅仅是商业活动的正常组成部分,就需要道歉和赔偿流程。 * 如果有人收取比他们账户中更多的钱,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 @@ -635,11 +635,11 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 1. 数据流系统可以保持对衍生数据的完整性保证,而无需原子提交,线性化或同步跨分区协调。 2. 虽然严格的唯一性约束要求及时性和协调性,但许多应用程序实际上可以很好地处理宽松的约束,只要整个过程保持完整性,它们可能会被暂时违反并予以修复。 -总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力[56]。 +总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力【56】。 例如,这种系统可以在多引导器配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 - 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。 -在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好[8]。异构分布式事务(如XA事务)(请参阅“实践中的分布式事务”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它[43]。 +在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“实践中的分布式事务”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它【43】。 查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 - 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。 @@ -651,17 +651,17 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 我们已经看到,数据可能会在磁盘未触及时破损(请参阅第227页的“复制和耐久性”),并且网络上的数据损坏有时可能会妨碍TCP校验和(请参阅第293页的“虚弱形式的说谎” )。也许这是我们应该更加关注的事情? -我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位[62] - 可用于破坏操作系统中安全机制的效应[63]技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 +我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 - 可用于破坏操作系统中安全机制的效应【63】技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 -要清楚的是,随机位翻转在现代硬件上仍然非常罕见[64]。我只想指出,他们并没有超越可能性领域,所以他们值得关注。 +要清楚的是,随机位翻转在现代硬件上仍然非常罕见【64】。我只想指出,他们并没有超越可能性领域,所以他们值得关注。 #### 在软件缺陷面前保持完整性 -除了这些硬件问题之外,总是存在软件错误的风险,这些错误不会被较低级别的网络,内存或文件系统校验和所捕获。即使广泛使用的数据库软件也存在一些问题:即使MySQL和PostgreSQL是健壮且充分的,但我个人发现MySQL的例子未能正确维护唯一性约束[65]和PostgreSQL的序列化隔离级别,表现出写入偏斜异常[66]他们认为这些数据库已经被许多人进行了多年的战斗测试。在不太成熟的软件中,情况可能会更糟糕。 +除了这些硬件问题之外,总是存在软件错误的风险,这些错误不会被较低级别的网络,内存或文件系统校验和所捕获。即使广泛使用的数据库软件也存在一些问题:即使MySQL和PostgreSQL是健壮且充分的,但我个人发现MySQL的例子未能正确维护唯一性约束【65】和PostgreSQL的序列化隔离级别,表现出写入偏斜异常【66】他们认为这些数据库已经被许多人进行了多年的战斗测试。在不太成熟的软件中,情况可能会更糟糕。 尽管在仔细设计,测试和审查方面做出了相当大的努力,但错误仍在蔓延。虽然它们很少,并且最终被发现并修复,但仍然存在一段时期,这些错误可能会破坏数据。 -当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束[36]。 +当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束【36】。 ACID意义上的一致性(请参阅第224页的“一致性”)基于数据库以一致状态启动并且事务将其从一个一致状态转换为另一个一致状态的想法。因此,我们期望数据库始终处于一致状态。但是,如果您认为该交易没有错误,则这个概念才有意义。如果应用程序错误地使用数据库以某种方式,例如,不安全地使用弱隔离级别,数据库的完整性无法得到保证。 @@ -671,13 +671,13 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 如“不可变事件的优点”一节中所述,审计不仅适用于财务应用程序。然而,可审计性在财务中非常重要,因为每个人都知道错误发生了,而且我们都认识到需要能够检测和解决问题。 -成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS和Amazon S3等大规模存储系统不完全信任磁盘:它们运行后台进程,这些后台进程持续读回文件,将其与其他副本进行比较,并将文件从一个磁盘移动到另一个磁盘,以便减轻沉默腐败的风险[67]。 +成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS和Amazon S3等大规模存储系统不完全信任磁盘:它们运行后台进程,这些后台进程持续读回文件,将其与其他副本进行比较,并将文件从一个磁盘移动到另一个磁盘,以便减轻沉默腐败的风险【67】。 如果你想确保你的数据仍然存在,你必须真正阅读并检查。大多数时候它仍然会在那里,但如果不是这样,你真的很想早日找到答案。通过同样的说法,尝试从备份中不时恢复是非常重要的,否则只有当它太迟而您已经丢失数据时才会发现备份被破坏。不要盲目地相信它是全部工作。 #### 验证的文化 -像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任[68]。 +像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任【68】。 我担心ACID数据库的文化导致我们在盲目信任技术(如交易机制)的基础上开发应用程序,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,审计机制并不值得投资。 @@ -691,7 +691,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 清楚地看到数据流(请参阅第419页上的“批处理输出的原理”)可以使数据的来源更加清晰,从而使完整性检查更加可行。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理和流处理器,以检查是否获得相同的结果,或者甚至并行运行冗余衍生。 -确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情[4,69]。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。 +确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情【4,69】。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。 #### 再次是端到端的论点 @@ -699,13 +699,13 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 检查数据系统的完整性最好是以端到端的方式完成(请参阅“数据库的端到端争论”页码:51):我们可以在完整性检查中包含的系统越多,那里的机会就越少在这个过程的某个阶段,腐败是被忽视的。如果我们可以检查整个衍生数据管道是端到端正确的,那么沿着路径的任何磁盘,网络,服务和算法都隐含在检查中。 -持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快[70]。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。 +持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快【70】。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。 #### 审计数据系统的工具 目前,数据系统并不多,这使可审计性成为高层关注的重点。一些应用程序实现自己的审计机制,例如将所有更改记录到单独的审计表中,但是保证审计日志的完整性,并且数据库状态仍然很困难。事务日志可以通过定期使用硬件安全模块对事务日志进行签名来进行防篡改,但这并不能保证正确的事务首先进入日志。 -使用加密工具来证明系统的完整性是十分有趣的,这种方式对于广泛的硬件和软件问题以及甚至是潜在的恶意行为都很有效。加密货币,区块链和分布式账本技术,如比特币,以太坊,波纹,恒星等等[71,72,73]已经出现在这个领域。 +使用加密工具来证明系统的完整性是十分有趣的,这种方式对于广泛的硬件和软件问题以及甚至是潜在的恶意行为都很有效。加密货币,区块链和分布式账本技术,如比特币,以太坊,波纹,恒星等等【71,72,73】已经出现在这个领域。 我没有资格评论这些技术作为商定合同的货币或机制的优点。但是,从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型和事务机制,不同副本可以由互不信任的组织托管。复制品不断检查彼此的完整性,并使用共识协议来约定应执行的交易。 @@ -725,47 +725,47 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 我们将数据作为一个抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。我们必须以人性和尊重来对待这些数据。用户也是人类,人的尊严是至高无上的。 -软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范和专业实践[77],但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度[78,79,80]。 +软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范和专业实践【77】,但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度【78,79,80】。 技术本身并不好或不好,重要的是如何使用它以及它如何影响人。对于像搜索引擎这样的软件系统来说,就像使用像武器这样的武器非常相似。我认为,软件工程师仅仅专注于技术并忽视其后果是不够的:伦理责任也是我们的责任。有关道德的推理是困难的,但忽视这一点太重要了。 ### 预测分析 -例如,预测分析是“大数据”炒作的主要部分。使用数据分析预测天气或疾病传播是一回事[81];另一个问题是要预测一个罪犯是否可能再犯罪,一个贷款申请人是否有可能违约,或者一个保险客户是否有可能做出昂贵的索赔。后者直接影响到个人的生活。当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫持,公司希望避免激怒无效或不可靠的人。从他们的角度来看,错过商业机会的成本很低,但不良贷款或有问题的员工的成本要高得多,因此组织希望保持谨慎是很自然的。如果有疑问,他们最好不要说。 +例如,预测分析是“大数据”炒作的主要部分。使用数据分析预测天气或疾病传播是一回事【81】;另一个问题是要预测一个罪犯是否可能再犯罪,一个贷款申请人是否有可能违约,或者一个保险客户是否有可能做出昂贵的索赔。后者直接影响到个人的生活。当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫持,公司希望避免激怒无效或不可靠的人。从他们的角度来看,错过商业机会的成本很低,但不良贷款或有问题的员工的成本要高得多,因此组织希望保持谨慎是很自然的。如果有疑问,他们最好不要说。 -然而,随着算法决策变得越来越普遍,某人(准确地或错误地)被某种算法标记为有风险,可能会遭受大量“不”决定。系统地排除工作,航空旅行,保险,物业租赁,金融服务和社会其他关键方面是个人自由的巨大制约因素,因此被称为“算法监狱”[82]。在尊重人权的国家,刑事司法系统在被证明有罪之前就认定无罪。另一方面,自动化系统可以有系统地,任意地排除一个人参与社会活动,而没有任何内疚的证据,而且几乎没有上诉的机会。 +然而,随着算法决策变得越来越普遍,某人(准确地或错误地)被某种算法标记为有风险,可能会遭受大量“不”决定。系统地排除工作,航空旅行,保险,物业租赁,金融服务和社会其他关键方面是个人自由的巨大制约因素,因此被称为“算法监狱”【82】。在尊重人权的国家,刑事司法系统在被证明有罪之前就认定无罪。另一方面,自动化系统可以有系统地,任意地排除一个人参与社会活动,而没有任何内疚的证据,而且几乎没有上诉的机会。 #### 偏见和歧视 -算法做出的决定不一定比人类做出的更好或更差。每个人都可能有偏见,即使他们积极地试图抵消它们,歧视性做法也可能在文化上被制度化。人们希望根据数据做出决定,而不是通过人们的主观和现场评估来更加公平,给那些在传统系统中经常被忽视的人更好的机会[83]。 +算法做出的决定不一定比人类做出的更好或更差。每个人都可能有偏见,即使他们积极地试图抵消它们,歧视性做法也可能在文化上被制度化。人们希望根据数据做出决定,而不是通过人们的主观和现场评估来更加公平,给那些在传统系统中经常被忽视的人更好的机会【83】。 -当我们开发预测分析系统时,我们不仅仅是通过使用软件来指定什么时候说是或否的规则来自动化人的决定;我们甚至将规则本身从数据中推断出来。但是,这些系统学到的模式是不透明的:即使数据中存在一些相关性,我们也可能不知道为什么。如果在算法输入中存在系统偏差,系统很可能会在输出中学习和放大偏差[84]。 +当我们开发预测分析系统时,我们不仅仅是通过使用软件来指定什么时候说是或否的规则来自动化人的决定;我们甚至将规则本身从数据中推断出来。但是,这些系统学到的模式是不透明的:即使数据中存在一些相关性,我们也可能不知道为什么。如果在算法输入中存在系统偏差,系统很可能会在输出中学习和放大偏差【84】。 -在许多国家,反歧视法律禁止根据种族,年龄,性别,性别,残疾或信仰等受保护特征对待不同的人。一个人的数据的其他特征可能会被分析,但是如果他们与受保护的特征相关会发生什么?例如,在种族隔离的邻居中,一个人的邮政编码,甚至是他们的IP地址,都是一个强烈的种族预测。这样说,相信一种算法可以以某种方式将偏倚的数据作为输入并产生公平和公正的输出[85],这似乎是荒谬的。然而,这种观点似乎常常被数据驱动型决策制定的支持者所暗示,这种态度被讽刺为“机器学习就像洗钱对于偏见”[86]。 +在许多国家,反歧视法律禁止根据种族,年龄,性别,性别,残疾或信仰等受保护特征对待不同的人。一个人的数据的其他特征可能会被分析,但是如果他们与受保护的特征相关会发生什么?例如,在种族隔离的邻居中,一个人的邮政编码,甚至是他们的IP地址,都是一个强烈的种族预测。这样说,相信一种算法可以以某种方式将偏倚的数据作为输入并产生公平和公正的输出【85】,这似乎是荒谬的。然而,这种观点似乎常常被数据驱动型决策制定的支持者所暗示,这种态度被讽刺为“机器学习就像洗钱对于偏见”【86】。 -预测分析系统只是从过去推断出来的;如果过去是歧视性的,他们就会把这种歧视归为法律。如果我们希望未来比过去更好,那么就需要道德想象力,而这只有人类才能提供[87]。数据和模型应该是我们的工具,而不是我们的主人。 +预测分析系统只是从过去推断出来的;如果过去是歧视性的,他们就会把这种歧视归为法律。如果我们希望未来比过去更好,那么就需要道德想象力,而这只有人类才能提供【87】。数据和模型应该是我们的工具,而不是我们的主人。 #### 责任和问责 -自动决策开启了责任和问责的问题[87]。如果一个人犯了错误,他们可以被追究责任,受决定影响的人可以上诉。算法也会犯错误,但是如果他们出错,谁会犯错误[88]?当一辆自驾车引发事故时,谁负责?如果自动信用评分算法系统地区分特定种族或宗教的人,是否有任何追索权?如果机器学习系统的决定受到司法审查,您能向法官解释算法是如何做出决定的吗? +自动决策开启了责任和问责的问题【87】。如果一个人犯了错误,他们可以被追究责任,受决定影响的人可以上诉。算法也会犯错误,但是如果他们出错,谁会犯错误【88】?当一辆自驾车引发事故时,谁负责?如果自动信用评分算法系统地区分特定种族或宗教的人,是否有任何追索权?如果机器学习系统的决定受到司法审查,您能向法官解释算法是如何做出决定的吗? -信用评级机构是收集数据来做出人们决策的一个老例子。不良信用评分会使生活变得困难,但至少一个信用评分通常是基于一个人的实际借款历史的相关事实,并且记录中的任何错误都可以得到纠正(尽管机构通常不会这么容易)。然而,基于机器学习的评分算法通常使用更广泛的输入范围,并且更加不透明,使得难以理解特定决策是如何发生的以及是否有人正在以不公平或歧视的方式被对待[89]。 +信用评级机构是收集数据来做出人们决策的一个老例子。不良信用评分会使生活变得困难,但至少一个信用评分通常是基于一个人的实际借款历史的相关事实,并且记录中的任何错误都可以得到纠正(尽管机构通常不会这么容易)。然而,基于机器学习的评分算法通常使用更广泛的输入范围,并且更加不透明,使得难以理解特定决策是如何发生的以及是否有人正在以不公平或歧视的方式被对待【89】。 -信用评分总结了“你过去的表现如何?”,而预测分析通常是基于“谁与你类似,以及过去人们喜欢你的行为如何?”。与他人行为相似的绘图意味着刻板印象人们,例如根据他们居住的地方(一个关于种族和社会经济阶层的密切代理人)。那些放错人的人呢?而且,如果由于错误的数据而做出的决定是不正确的,则追索权几乎是不可能的[87]。 +信用评分总结了“你过去的表现如何?”,而预测分析通常是基于“谁与你类似,以及过去人们喜欢你的行为如何?”。与他人行为相似的绘图意味着刻板印象人们,例如根据他们居住的地方(一个关于种族和社会经济阶层的密切代理人)。那些放错人的人呢?而且,如果由于错误的数据而做出的决定是不正确的,则追索权几乎是不可能的【87】。 很多数据本质上是统计的,这意味着即使概率分布总体上是正确的,个别情况也可能是错的。例如,如果贵国的平均寿命是80岁,那么这并不意味着在80岁生日时就会死亡。从平均分布和概率分布来看,你不能说很多关于某个特定人的生活年龄。同样,预测系统的输出是概率性的,在个别情况下可能是错误的。 盲目相信数据至高无上的决策不仅是妄想,它是非常危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚如何使算法负责任和透明,如何避免加强现有的偏见,以及如何在不可避免的错误时加以修复。 -我们还需要弄清楚如何防止数据被用来伤害人,并且实现其正向潜力。例如,分析可以揭示人们生活的财务和社会特征。一方面,这种权力可以用来把援助和支持集中在帮助那些最需要援助的人身上。另一方面,它有时被掠夺性企业用于识别弱势群体并向高风险产品销售,如高成本贷款和无价值的大学学位[87,90]。 +我们还需要弄清楚如何防止数据被用来伤害人,并且实现其正向潜力。例如,分析可以揭示人们生活的财务和社会特征。一方面,这种权力可以用来把援助和支持集中在帮助那些最需要援助的人身上。另一方面,它有时被掠夺性企业用于识别弱势群体并向高风险产品销售,如高成本贷款和无价值的大学学位【87,90】。 #### 反馈回路 -即使预测性应用程序对人们的影响较小,比如推荐系统,也存在难以解决的问题。当服务善于预测用户想要看到什么内容时,他们最终可能只会向人们展示他们已经同意的观点,导致产生陈腔滥调,错误信息和极化可能滋生的回声室。我们已经看到社交媒体呼应室对竞选活动的影响[91]。 +即使预测性应用程序对人们的影响较小,比如推荐系统,也存在难以解决的问题。当服务善于预测用户想要看到什么内容时,他们最终可能只会向人们展示他们已经同意的观点,导致产生陈腔滥调,错误信息和极化可能滋生的回声室。我们已经看到社交媒体呼应室对竞选活动的影响【91】。 -当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作[87]。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。 +当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作【87】。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。 -我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法[92]。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 +我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 #### 隐私和跟踪 @@ -781,24 +781,24 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 监控 -作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好[93]。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“ +作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好【93】。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“ -这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中[94],我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录[95]。 +这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录【95】。 -即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的[96]。 +即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的【96】。 -并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害[97]。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 +并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害【97】。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 -我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)[98]。而分析算法只会变得更好。 +我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)【98】。而分析算法只会变得更好。 #### 同意和选择的自由 我们可能会断言用户自愿选择使用跟踪其活动的服务,并且他们已同意服务条款和隐私政策,因此他们同意收集数据。我们甚至可以声称,用户正在接受有价值的服务,以换取所提供的数据,并且为了提供服务,跟踪是必要的。毫无疑问,社交网络,搜索引擎以及其他各种免费的在线服务对于用户来说都是有价值的,但是这个说法存在问题。 用户几乎不知道他们提供给我们的数据库的数据,或者它们如何保留和处理 - 而大多数隐私政策的作用更多的是模糊而不是照亮。如果不了解他们的数据会发生什么,用户无法给出任何有意义的同意。通常,来自一个用户的数据还说明关于不是该服务的用户并且没有同意任何条款的其他人的事情。我们在本书的这一部分讨论的衍生数据集(来自整个用户群的数据可能与行为跟踪和外部数据源相结合)恰恰是用户无法获得任何有意义理解的数据种类。 -而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户[99]。 +而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户【99】。 -对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”[99],那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 +对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 由于跟踪用户而拒绝使用服务,这只是少数拥有足够的时间和知识来了解其隐私政策的人员的一种选择,并且有可能错过社会参与或专业人士如果他们参与了服务,可能会出现机会。对于处境不太好的人来说,没有任何意义上的自由选择:监督变得不可避免。 @@ -806,7 +806,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 有时候,人们声称“隐私已经死了”,理由是有些用户愿意把各种有关他们生活的事情发布到社交媒体上,有时是通常的,有时是个人的。但是,这种说法是错误的,并且依赖于对隐私一词的错误理解。 -拥有隐私并不意味着保密一切;它意味着要有自由选择要向谁揭示哪些事情,要公开什么,以及要保密什么。隐私权是一项决定权:它可以让每个人决定他们希望在每个场合保密和透明度之间的区域[99]。这是一个人的自由和自主的重要方面。 +拥有隐私并不意味着保密一切;它意味着要有自由选择要向谁揭示哪些事情,要公开什么,以及要保密什么。隐私权是一项决定权:它可以让每个人决定他们希望在每个场合保密和透明度之间的区域【99】。这是一个人的自由和自主的重要方面。 当通过监控基础设施从人身上提取数据时,隐私权不一定被侵蚀,而是转移到数据收集器。获取数据的公司本质上是说“相信我们用你的数据做正确的事情”,这意味着决定要透露什么和保密的权利是从个人转移到公司的。 @@ -814,35 +814,35 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 即使特定用户无法从特定广告定位的人群中个人身份识别,他们已经失去了一些关于披露一些隐私信息的机构,例如他们是否患有某种疾病。决定根据个人喜好向谁透露什么的不是用户,而是公司以最大化利润为目标行使隐私权。 -许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它[100]。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 +许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它【100】。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 允许在线服务的用户控制其他用户可以看到的数据的哪些方面的隐私设置是将一些控制交还给用户的起点。但是,无论设置如何,服务本身仍然可以不受限制地访问数据,并且可以以隐私策略允许的任何方式自由使用它。即使服务承诺不会将数据出售给第三方,它通常会授予自己不受限制的权利,以在内部处理和分析数据,而且往往比用户明显看到的要多得多。 -这种从个人到公司的大规模隐私权转移历史上是史无前例的[99]。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 +这种从个人到公司的大规模隐私权转移历史上是史无前例的【99】。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 #### 数据作为资产和权力 由于行为数据是用户与服务交互的副产品,因此有时称为“数据耗尽” - 表明数据是毫无价值的浪费材料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据会被抛弃。 -更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段[99]。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。 +更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段【99】。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。 个人数据是宝贵资产的说法得到了数据中介的支持,这个数据中介是一个隐蔽的行业,它主要是为了市场目的而采购,收集,分析,推断和转售侵入性个人数据[ 90。初创公司通过他们的用户数量,通过“眼球”,即通过他们的监视能力来估价。 -因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃[101]。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生[102]。 +因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃【101】。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生【102】。 -这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”[101],或者至少是“有害物质”[103]。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。 +这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”【101】,或者至少是“有害物质”【103】。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。 -正如古老的格言所言,“知识就是力量”。此外,“在避免审查自己的同时审视他人是最重要的权力形式之一”[105]。这就是为什么极权政府希望监督:这让他们有能力控制人口。尽管今天的科技公司并没有公开地寻求政治权力,但是他们积累的数据和知识却给他们带来了很多权力,其中大部分是在公共监督之外偷偷摸摸的[106]。 +正如古老的格言所言,“知识就是力量”。此外,“在避免审查自己的同时审视他人是最重要的权力形式之一”【105】。这就是为什么极权政府希望监督:这让他们有能力控制人口。尽管今天的科技公司并没有公开地寻求政治权力,但是他们积累的数据和知识却给他们带来了很多权力,其中大部分是在公共监督之外偷偷摸摸的【106】。 #### 记住工业革命 -数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。随着我们的日常生活和社会组织在过去的十年中发生了变化,并且在未来的十年中可能会继续发生根本性的变化,所以我们就会想到与工业革命的比较[87,96]。 +数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。随着我们的日常生活和社会组织在过去的十年中发生了变化,并且在未来的十年中可能会继续发生根本性的变化,所以我们就会想到与工业革命的比较【87,96】。 工业革命是通过重大的技术和农业进步来实现的,它带来了持续的经济增长,长期的生活水平显着提高。然而,它也带来了一些重大问题:空气污染(由于烟雾和化学过程)以及水(工业和人类的废物)造成的污染是可怕的。工厂老板生活在辉煌中,而城市工人经常住在非常贫困的住房里,并且在恶劣的条件下长时间工作。童工是常见的,包括在矿场工作的危险和低薪。 -花了很长时间才制定了保护措施,例如环境保护条例,工作场所安全协议,禁止使用童工和食品卫生检查。毫无疑问,生产成本增加了,因为事实再也不能把废物倒入河流,销售污染的食物或者剥削工人。但是,整个社会都从中受益匪浅,我们中很少人会想要回到这些规定之前的时间[87]。 +花了很长时间才制定了保护措施,例如环境保护条例,工作场所安全协议,禁止使用童工和食品卫生检查。毫无疑问,生产成本增加了,因为事实再也不能把废物倒入河流,销售污染的食物或者剥削工人。但是,整个社会都从中受益匪浅,我们中很少人会想要回到这些规定之前的时间【87】。 -就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)[96]的话来说: +就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)【96】的话来说: > 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 - 我们如何控制它以及如何处理它 - 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。 > @@ -850,17 +850,17 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 立法和自律 -数据保护法可能有助于维护个人的权利。例如,1995年的“欧洲数据保护指令”规定,个人数据必须“为特定的,明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须“充分,与收集目的相关并不过分“[107]。 +数据保护法可能有助于维护个人的权利。例如,1995年的“欧洲数据保护指令”规定,个人数据必须“为特定的,明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须“充分,与收集目的相关并不过分“【107】。 -但是,这个立法在今天的互联网环境下是否有效还是有疑问的[108]。这些规则直接违背了大数据的理念,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的见解。探索意味着将数据用于未预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地谈论同意的话)[109]。目前正在制定更新的规定[89]。 +但是,这个立法在今天的互联网环境下是否有效还是有疑问的【108】。这些规则直接违背了大数据的理念,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的见解。探索意味着将数据用于未预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地谈论同意的话)【109】。目前正在制定更新的规定【89】。 -收集大量有关人员数据的公司反对监管,认为这是创新的负担和阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,隐私存在明显风险,但也有潜在机会:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,可以预防多少人死亡[110]?过度监管可能会阻止这种突破。这种潜在机会与风险之间难以平衡[105]。 +收集大量有关人员数据的公司反对监管,认为这是创新的负担和阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,隐私存在明显风险,但也有潜在机会:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,可以预防多少人死亡【110】?过度监管可能会阻止这种突破。这种潜在机会与风险之间难以平衡【105】。 -从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任[111]。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。 +从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任【111】。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。 我们应该允许每个人保持他们的隐私 - 即他们控制自己的数据 - 而不是通过监视来窃取他们的控制权。我们控制数据的个人权利就像是一个国家公园的自然环境:如果我们没有明确的保护和关心,它将被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视不是不可避免的,我们仍然能够阻止它。 -我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据[111,112]。清除数据与不变性的想法背道而驰(请参阅第463页的“不变性的限制”),但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略[113,114]。总的来说,文化和态度的变化是必要的。 +我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅第463页的“不变性的限制”),但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化和态度的变化是必要的。 ## 本章小结 diff --git a/ch3.md b/ch3.md index 651f869..575e50d 100644 --- a/ch3.md +++ b/ch3.md @@ -142,7 +142,7 @@ $ cat database -### SSTables和LSM-Trees +### SSTables和LSM树 在[图3-3](img/fig3-3.png)中,每个日志结构存储段都是一系列键值对。这些对按照它们写入的顺序出现,日志中稍后的值优先于日志中较早的相同键的值。除此之外,文件中键值对的顺序并不重要。