diff --git a/content/zh/ch11.md b/content/zh/ch11.md index e54ac73..5fb9d73 100644 --- a/content/zh/ch11.md +++ b/content/zh/ch11.md @@ -2,69 +2,70 @@ title: "第十一章:批处理" linkTitle: "11. 批处理" weight: 311 -math: true breadcrumbs: false --- -{{< callout type="warning" >}} -当前页面来自本书第一版,第二版尚不可用 -{{< /callout >}} + ![](/map/ch10.png) -> 带有太强个人色彩的系统无法成功。当最初的设计完成并且相对稳定时,不同的人们以自己的方式进行测试,真正的考验才开始。 +> *带有太强个人色彩的系统无法成功。当最初的设计完成并且相对稳健时,真正的考验才刚开始:此后会有许多持不同观点的人做出各自的实验。* > -> —— 高德纳 +> 高德纳 -在本书的前两部分中,我们讨论了很多关于 **请求** 和 **查询** 以及相应的 **响应** 或 **结果**。许多现有数据系统中都采用这种数据处理方式:你发送请求指令,一段时间后(我们期望)系统会给出一个结果。数据库、缓存、搜索索引、Web 服务器以及其他一些系统都以这种方式工作。 +到目前为止,本书大部分内容都围绕着 *请求(request)* 与 *查询(query)* 以及对应的 *响应(response)* 或 *结果(result)* 展开。现代很多数据系统都默认采用这种处理方式:你发出请求或指令,系统尽快给出答案。 -像这样的 **在线(online)** 系统,无论是浏览器请求页面还是调用远程 API 的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅 “[描述性能](/ch1#描述性能)”)。 +网页浏览器请求页面、服务调用远程 API、数据库、缓存、搜索索引,以及很多其他系统都如此运作。我们称这类系统为 *在线系统(online systems)*。它们通常以响应时间作为主要性能指标,并且往往需要良好的容错能力来保证高可用。 -Web 和越来越多的基于 HTTP/REST 的 API 使交互的请求 / 响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统: +但有时候,你需要执行的计算比一次交互式请求大得多,或者要处理的数据量远超单次请求能承载的范围。例如训练 AI 模型、把海量数据从一种形式转换成另一种形式、或者在超大数据集上做分析计算。我们把这类任务称为 *批处理(batch processing)* 作业,有时也称为 *离线系统(offline systems)*。 -服务(在线系统) -: 服务等待客户的请求或指令到达。每收到一个,服务会试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要(如果客户端无法访问服务,用户可能会收到错误消息)。 +批处理作业读取一批输入数据(只读),并生成一批输出数据(每次运行都从头生成)。它通常不会像读写事务那样原地修改数据。因此,输出是由输入推导出的 *衍生数据(derived data)*(见[“记录系统与衍生数据”](/ch1#sec_introduction_derived)):如果不满意输出,你可以直接删除它,修改作业逻辑,再跑一遍即可。把输入视为不可变并尽量避免副作用(例如直接写外部数据库),不仅有助于性能,也带来其他好处: -批处理系统(离线系统) -: 一个批处理系统有大量的输入数据,跑一个 **作业(job)** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。 +- 如果你在代码中引入了 bug 导致输出错误或损坏,可以直接回滚代码并重跑作业,输出就会恢复正确。更简单的做法是把旧输出保留在另一个目录,直接切回旧版本。多数对象存储与开放表格式(见[“云数据仓库”](/ch4#sec_cloud_data_warehouses))都支持这种能力,通常称为 *时间旅行(time travel)*。大多数支持读写事务的数据库不具备这种特性:如果错误代码把坏数据写进数据库,仅回滚代码并不能修复已写入的数据。能够从错误代码中恢复的能力被称为 *容忍人为失误* [^1]。 -流处理系统(准实时系统) -: 流处理介于在线和离线(批处理)之间,所以有时候被称为 **准实时(near-real-time)** 或 **准在线(nearline)** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在 [第十二章](/ch12) 讨论它。 +- 因为回滚容易,功能开发能比“犯错会造成不可逆损害”的环境更快推进。这个 *最小化不可逆性* 的原则对敏捷开发非常有益 [^2]。 -正如我们将在本章中看到的那样,批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004 年发布的批处理算法 Map-Reduce(可能被过分热情地)被称为 “造就 Google 大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用,包括 Hadoop、CouchDB 和 MongoDB。 +- 同一组文件可以作为多种作业的输入,包括监控类作业:例如计算指标、验证输出是否符合预期(如与上一次结果比较并度量偏差)。 -与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce 是一个相当低级别的编程模型,但它使得在商用硬件上能进行的处理规模迈上一个新的台阶。虽然 MapReduce 的重要性正在下降【5】,但它仍然值得去理解,因为它描绘了一幅关于批处理为什么有用,以及如何做到有用的清晰图景。 +- 批处理框架能更高效地利用计算资源。虽然也可以用 OLTP 数据库和应用服务器等在线系统做批处理,但资源成本通常显著更高。 -实际上,批处理是一种非常古老的计算方式。早在可编程数字计算机诞生之前,打孔卡制表机(例如 1890 年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,从大量输入中汇总计算。Map-Reduce 与 1940 年代和 1950 年代广泛用于商业数据处理的机电 IBM 卡片分类机器有着惊人的相似之处【7】。正如我们所说,历史总是在不断重复自己。 +批处理也有挑战。多数框架中,作业只有在整体完成后,其输出才能被下游进一步处理。批处理也可能低效:输入哪怕只变动一个字节,也可能需要重算整个输入数据集。尽管如此,批处理在大量场景中依然非常有用,我们会在[“批处理用例”](/ch11#sec_batch_output)中回到这个话题。 -在本章中,我们将了解 MapReduce 和其他一些批处理算法和框架,并探索它们在现代数据系统中的作用。但首先我们将看看使用标准 Unix 工具的数据处理。即使你已经熟悉了它们,Unix 的哲学也值得一读,Unix 的思想和经验教训可以迁移到大规模、异构的分布式数据系统中。 +批处理作业可能运行很久:几分钟、几小时甚至几天。很多作业是周期调度的(例如每天一次)。它的核心性能指标通常是吞吐量:单位时间能处理多少数据。有些批处理系统通过“中止并整体重启”应对故障,也有些具备更细粒度容错能力,可以在部分节点崩溃时仍让作业完成。 +> [!NOTE] +> 批处理的另一种替代形态是 *流处理(stream processing)*:作业不会在“处理完输入后结束”,而是持续监听输入,并在变化发生后很快处理。我们将在[第十二章](/ch12#ch_stream)讨论流处理。 -## 使用Unix工具的批处理 +在线处理与批处理的边界并不总是清晰:一个运行很久的数据库查询,看起来也很像批处理过程。但批处理有一些独特特性,使其成为构建可靠、可伸缩、可维护应用的重要积木。例如,它常在 *数据集成(data integration)* 中发挥作用,即把多个数据系统组合起来完成单一系统做不到的事。ETL(见[“数据仓库”](/ch1#sec_introduction_dwh))就是典型例子。 -我们从一个简单的例子开始。假设你有一台 Web 服务器,每次处理请求时都会在日志文件中附加一行。例如,使用 nginx 默认的访问日志格式,日志的一行可能如下所示: +现代批处理深受 MapReduce 影响。Google 在 2004 年发表了这一批处理算法 [^3],随后 Hadoop、CouchDB、MongoDB 等开源系统都实现了它。MapReduce 是相对底层的编程模型,其能力不如数据仓库中的并行查询执行引擎成熟 [^4] [^5]。它在诞生时确实让商用硬件上的处理规模跃升一大步,但今天已大体过时,Google 内部也不再使用 [^6] [^7]。 -```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" -``` +如今批处理更常通过 Spark、Flink 或数据仓库查询引擎完成。它们与 MapReduce 一样高度依赖分片(见[第七章](/ch7#ch_sharding))和并行执行,但缓存与执行策略更成熟。随着这些系统走向成熟,运维问题已大幅缓解,重点转向可用性:数据流 API、查询语言、DataFrame API 得到广泛支持;任务与工作流编排也显著进化。以 Hadoop 为中心的 Oozie、Azkaban 等调度器,正被 Airflow、Dagster、Prefect 这类更通用方案替代,它们可协调多种批处理框架与云数据仓库。 -(实际上这只是一行,分成多行只是为了便于阅读。)这一行中有很多信息。为了解释它,你需要了解日志格式的定义,如下所示: +云计算已无处不在。批处理存储层也正在从 HDFS、GlusterFS、CephFS 这类分布式文件系统(DFS)向 S3 等对象存储迁移。BigQuery、Snowflake 这类可伸缩云数据仓库,正在模糊“数据仓库”和“批处理系统”之间的边界。 -```bash - $remote_addr - $remote_user [$time_local] "$request" - $status $body_bytes_sent "$http_referer" "$http_user_agent" -``` +为了建立直觉,本章先从单机 Unix 工具示例出发,再扩展到分布式多机处理。你会看到,分布式批处理框架在很多方面很像操作系统:它也有调度器和文件系统。随后我们会讨论编写批处理作业的几种处理模型,最后给出常见应用场景。 -日志的这一行表明在 UTC 时间的 2015 年 2 月 27 日 17 点 55 分 11 秒,服务器从客户端 IP 地址 `216.58.210.78` 接收到对文件 `/css/typography.css` 的请求。用户没有认证,所以 `$remote_user` 被设置为连字符(`-`)。响应状态是 200(即请求成功),响应的大小是 3377 字节。网页浏览器是 Chrome 40,它加载了这个文件是因为该文件在网址为 `http://martin.kleppmann.com/` 的页面中被引用到了。 +## 使用 Unix 工具的批处理 {#sec_batch_unix} +假设你有一台 Web 服务器,每处理一个请求就在日志文件末尾追加一行。例如,使用 nginx 默认访问日志格式,一行可能像这样: -### 简单日志分析 + 216.58.210.78 - - [27/Jun/2025:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" + 200 3377 "https://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X + 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" -很多工具可以从这些日志文件生成关于网站流量的漂亮的报告,但为了练手,让我们使用基本的 Unix 功能创建自己的工具。例如,假设你想在你的网站上找到五个最受欢迎的网页。则可以在 Unix shell 中这样做:[^i] +(实际上这是一行,这里为了阅读方便换了行。)这一行包含了很多信息。要正确解释它,你需要日志格式定义: -[^i]: 有些人认为 `cat` 这里并没有必要,因为输入文件可以直接作为 awk 的参数。但这种写法让线性管道更为显眼。 + $remote_addr - $remote_user [$time_local] "$request" + $status $body_bytes_sent "$http_referer" "$http_user_agent" + +这表示:UTC 时间 2025 年 6 月 27 日 17:55:11,服务器收到来自客户端 IP `216.58.210.78` 对 `/css/typography.css` 的请求。用户未认证,因此 `$remote_user` 是连字符(`-`)。响应状态码是 200(成功),响应体大小 3,377 字节。浏览器是 Chrome 137,该文件是从页面 *[*https://martin.kleppmann.com/*](https://martin.kleppmann.com/)* 引用而来。 + +看起来“解析日志”有点朴素,但它在现代科技公司里是核心能力之一,从广告流水线到支付处理都大量依赖。事实上,这也是 MapReduce 与“大数据”浪潮快速兴起的重要推动力。 + +### 简单日志分析 {#sec_batch_log_analysis} + +很多工具都能从日志生成漂亮的网站流量报告。这里为了练手,我们只用基础 Unix 工具自己做一个。比如你想找出网站最受欢迎的五个页面,可以在 shell 中这样做: ```bash cat /var/log/nginx/access.log | #1 @@ -75,727 +76,492 @@ cat /var/log/nginx/access.log | #1 head -n 5 #6 ``` -1. 读取日志文件 -2. 将每一行按空格分割成不同的字段,每行只输出第七个字段,恰好是请求的 URL。在我们的例子中是 `/css/typography.css`。 -3. 按字母顺序排列请求的 URL 列表。如果某个 URL 被请求过 n 次,那么排序后,文件将包含连续重复出现 n 次的该 URL。 -4. `uniq` 命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。`-c` 则表示还要输出一个计数器:对于每个不同的 URL,它会报告输入中出现该 URL 的次数。 -5. 第二种排序按每行起始处的数字(`-n`)排序,这是 URL 的请求次数。然后逆序(`-r`)返回结果,大的数字在前。 -6. 最后,只输出前五行(`-n 5`),并丢弃其余的。该系列命令的输出如下所示: +1. 读取日志文件。(严格说这里不需要 `cat`,可直接把文件作为 `awk` 参数;但这样写更直观看出线性管道。) +2. 以空白字符切分每行,只输出第 7 个字段,也就是请求 URL。上面的样例中是 `/css/typography.css`。 +3. 按字典序对 URL 排序。某个 URL 若出现 *n* 次,排序后会连续出现 *n* 行。 +4. `uniq` 通过比较相邻两行是否相同来去重。`-c` 让它输出计数:每个不同 URL 出现了多少次。 +5. 第二次 `sort` 按每行开头的数字(`-n`)排序,并用 `-r` 逆序,出现次数最多的排在最前。 +6. `head` 只保留前 5 行(`-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 + 3631 /2016/02/08/how-to-do-distributed-locking.html + 2124 /2020/11/18/distributed-systems-and-elliptic-curves.html 1369 / 915 /css/typography.css + +如果你不熟悉 Unix 工具,这条命令看起来可能有点晦涩,但它威力很强。它能在几秒内处理 GB 级日志,而且修改分析逻辑也非常方便:例如要排除 CSS 文件,可把 `awk` 参数改成 `'$7 !~ /\.css$/ {print $7}'`;若要统计访问最多的客户端 IP,把 `awk` 参数改成 `'{print $1}'` 即可。 + +本书篇幅有限,无法展开讲 Unix 工具,但它们非常值得学。令人惊讶的是,仅靠 `awk`、`sed`、`grep`、`sort`、`uniq`、`xargs` 的组合,就能在几分钟内做出很多数据分析,并且性能相当好 [^8]。 + +### 命令链与自定义程序 {#sec_batch_custom_program} + +你也可以不用 Unix 管道,而写个小程序完成同样的事。比如用 Python: + +```python +from collections import defaultdict + +counts = defaultdict(int) #1 + +with open('/var/log/nginx/access.log', 'r') as file: + for line in file: + url = line.split()[6] #2 + counts[url] += 1 #3 + +top5 = sorted(((count, url) for url, count in counts.items()), reverse=True)[:5] #4 + +for count, url in top5: #5 + print(f"{count} {url}") ``` -如果你不熟悉 Unix 工具,上面的命令行可能看起来有点吃力,但是它非常强大。它能在几秒钟内处理几 GB 的日志文件,并且你可以根据需要轻松修改命令。例如,如果要从报告中省略 CSS 文件,可以将 awk 参数更改为 `'$7 !~ /\.css$/ {print $7}'`, 如果想统计最多的客户端 IP 地址,可以把 awk 参数改为 `'{print $1}'`,等等。 +1. `counts` 是散列表,记录每个 URL 出现次数,默认值为 0。 +2. 每行按空白字符切分,取第 7 个字段作为 URL(Python 数组从 0 开始,所以索引是 6)。 +3. 当前行对应 URL 的计数器加一。 +4. 按计数降序排序,取前五项。 +5. 打印前五项。 -我们不会在这里详细探索 Unix 工具,但是它非常值得学习。令人惊讶的是,使用 awk、sed、grep、sort、uniq 和 xargs 的组合,可以在几分钟内完成许多数据分析,并且它们的性能相当的好【8】。 +这个程序不如 Unix 管道简洁,但可读性也不错,偏好取决于习惯。不过两者除了语法差异,执行流程也很不一样;在大文件上运行时,这种差异会很明显。 -#### 命令链与自定义程序 +### 排序与内存聚合 {#id275} -除了 Unix 命令链,你还可以写一个简单的程序来做同样的事情。例如在 Ruby 中,它可能看起来像这样: +Python 脚本在内存里维护了一个“URL -> 出现次数”的散列表。Unix 管道示例没有这种散列表,而是通过排序把同一 URL 的多次出现排到一起。 -```ruby -counts = Hash.new(0) # 1 -File.open('/var/log/nginx/access.log') do |file| - file.each do |line| - url = line.split[6] # 2 - counts[url] += 1 # 3 - end -end +哪种方法更好?取决于不同 URL 的数量。对多数中小网站而言,通常可以把所有不同 URL 及其计数器放进(比如)1GB 内存。这个作业的 *工作集(working set)*(需要随机访问的内存规模)只取决于不同 URL 的个数:即便一百万条日志都指向同一 URL,散列表也只存一个 URL 和一个计数器。工作集足够小时,内存散列表很好用,笔记本都能跑。 -top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 -top5.each{|count, url| puts "#{count} #{url}" } # 5 -``` +但如果工作集大于可用内存,排序法就有优势:它能高效使用磁盘。这与[“日志结构存储”](/ch4#sec_storage_log_structured)中的原理一样:先在内存对数据块排序并写成段文件,再把多个有序段合并成更大的有序文件。归并排序的顺序访问模式对磁盘很友好(见[“SSD 上的顺序写与随机写”](/ch4#sidebar_sequential))。 -1. `counts` 是一个存储计数器的哈希表,保存了每个 URL 被浏览的次数,默认为 0。 -2. 逐行读取日志,抽取每行第七个被空格分隔的字段为 URL(这里的数组索引是 6,因为 Ruby 的数组索引从 0 开始计数) -3. 将日志当前行中 URL 对应的计数器值加一。 -4. 按计数器值(降序)对哈希表内容进行排序,并取前五位。 -5. 打印出前五个条目。 +GNU Coreutils(Linux)中的 `sort` 能自动把超内存数据溢写到磁盘,并自动利用多核并行排序 [^9]。这意味着前面的 Unix 命令链可以自然扩展到大数据集而不耗尽内存,瓶颈通常变成磁盘读取输入文件的速率。 -这个程序并不像 Unix 管道那样简洁,但是它的可读性很强,喜欢哪一种属于口味的问题。但两者除了表面上的差异之外,执行流程也有很大差异,如果你在大文件上运行此分析,则会变得明显。 +Unix 工具的一个局限是它们只在单机运行。当数据大到单机内存或本地磁盘都放不下时,就需要分布式批处理框架。 -#### 排序 VS 内存中的聚合 +## 分布式系统中的批处理 {#sec_batch_distributed} -Ruby 脚本在内存中保存了一个 URL 的哈希表,将每个 URL 映射到它出现的次数。Unix 管道没有这样的哈希表,而是依赖于对 URL 列表的排序,在这个 URL 列表中,同一个 URL 的只是简单地重复出现。 +在前面的 Unix 示例中,单机有几个协同组件在处理日志: -哪种方法更好?这取决于你有多少个不同的 URL。对于大多数中小型网站,你可能可以为所有不同网址提供一个计数器(假设我们使用 1GB 内存)。在此例中,作业的 **工作集**(working set,即作业需要随机访问的内存大小)仅取决于不同 URL 的数量:如果日志中只有单个 URL,重复出现一百万次,则散列表所需的空间表就只有一个 URL 加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。 +- 通过操作系统文件系统接口访问的存储设备。 +- 决定进程何时运行、如何分配 CPU 资源的调度器。 +- 一串通过管道把 `stdin`/`stdout` 连接起来的 Unix 程序。 -另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。归并排序具有在磁盘上运行良好的顺序访问模式。(请记住,针对顺序 I/O 进行优化是 [第三章](/ch3) 中反复出现的主题,相同的模式在此重现) +分布式批处理框架也有对应组件。某种意义上,你可以把分布式处理框架看成“分布式操作系统”:它有文件系统、有任务调度器,还有通过文件系统或其他通道互相传递数据的程序。 -GNU Coreutils(Linux)中的 `sort` 程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个 CPU 核进行并行排序【9】。这意味着我们之前看到的简单的 Unix 命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 +### 分布式文件系统 {#sec_batch_dfs} +操作系统提供的文件系统由多层组成: -### Unix哲学 +- 最底层是块设备驱动,直接与磁盘交互,向上层提供原始块读写。 +- 块层之上是页缓存,缓存最近访问块以提升读取速度。 +- 块 API 之上是文件系统层,负责把大文件切块,并维护 inode、目录、文件等元数据。Linux 常见实现如 ext4、XFS。 +- 最上层,操作系统通过统一 API(虚拟文件系统,VFS)向应用暴露不同文件系统,让应用以统一方式读写底层不同实现。 -我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是 Unix 的关键设计思想之一,而且它直至今天也仍然令人讶异地重要。让我们更深入地研究一下,以便从 Unix 中借鉴一些想法【10】。 +分布式文件系统(DFS)工作方式很类似:文件被切成块并分散到多台机器。DFS 的块通常比本地文件系统大得多:HDFS 默认 128MB,JuiceFS 和许多对象存储常用 4MB,而 ext4 默认块通常是 4096 字节。块越大,需要维护的元数据越少,这对 PB 级数据非常关键;同时寻道开销占比也更低。 -Unix 管道的发明者道格・麦克罗伊(Doug McIlroy)在 1964 年首先描述了这种情况【11】:“我们需要一种类似园艺胶管的方式来拼接程序 —— 当我们需要将消息从一个程序传递另一个程序时,直接接上去就行。I/O 应该也按照这种方式进行 ”。水管的类比仍然在生效,通过管道连接程序的想法成为了现在被称为 **Unix 哲学** 的一部分 —— 这一组设计原则在 Unix 用户与开发者之间流行起来,该哲学在 1978 年表述如下【12,13】: +大多数物理存储设备不能做“部分块写入”,即使数据不足一个块也得写满块。DFS 的块更大且通常构建在操作系统文件系统之上,因此一般没有这个约束。比如一个 900MB 文件在 128MB 分块下,会有 7 个 128MB 块和 1 个 4MB 块。 -1. 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加 “功能” 让老程序复杂化。 -2. 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。 -3. 设计和构建软件时,即使是操作系统,也让它们能够尽早地被试用,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。 -4. 优先使用工具来减轻编程任务,即使必须绕道去编写工具,且在用完后很可能要扔掉大部分。 +读取 DFS 块需要通过网络请求到持有该块的集群节点。每台机器都运行守护进程,对外提供 API,使远程进程能把本地文件系统中的块当作文件读写。HDFS 把这些守护进程叫 DataNode,GlusterFS 叫 glusterfsd。后文统称 *数据节点(data node)*。 -这种方法 —— 自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和 DevOps 运动。奇怪的是,四十年来变化不大。 +DFS 也实现了“分布式版本”的页缓存。因为 DFS 块作为文件存放在数据节点本地,读写会经过数据节点操作系统,自带内存页缓存,热门块会被缓存在内存中。某些 DFS 还提供更多缓存层,例如 JuiceFS 的客户端缓存和本地磁盘缓存。 -`sort` 工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用 `sort` 几乎没什么用。它只能与其他 Unix 工具(如 `uniq`)结合使用。 +像 ext4/XFS 这样的文件系统会维护空闲空间、块位置、目录结构、权限等元数据。DFS 同样需要记录“文件块分布在哪些机器”“权限如何”等信息。Hadoop 使用 NameNode 维护集群元数据;DeepSeek 的 3FS 使用元数据服务并把元数据持久化到 FoundationDB 之类键值存储。 -像 `bash` 这样的 Unix shell 可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。Unix 如何实现这种可组合性? +在文件系统之上是 VFS。批处理系统里最接近它的是 DFS 协议:批处理框架需要通过协议/接口来读写存储。只要实现协议,就能作为可插拔存储接入。例如 S3 API 已被 MinIO、Cloudflare R2、Tigris、Backblaze B2 等大量系统兼容支持。具备 S3 支持的批处理系统通常可直接使用这些存储。 -#### 统一的接口 +有些 DFS 还提供 POSIX 兼容文件系统,让操作系统 VFS 把它当普通文件系统。常见集成方式是 FUSE 或 NFS 协议。NFS 可能是最知名分布式文件系统协议,最初用于让多个客户端读写单个服务器上的数据。后来 AWS EFS、Archil 等提供了更可伸缩的 NFS 兼容实现。NFS 客户端虽仍连到一个端点,但底层会与分布式元数据服务和数据节点交互完成读写。 -如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 —— 换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的 I/O 接口。 +> [!TIP] 分布式文件系统与网络存储 +> 分布式文件系统基于 *无共享(shared-nothing)* 原则(见[“共享内存、共享磁盘与无共享架构”](/ch2#sec_introduction_shared_nothing)),与 NAS(网络附加存储)和 SAN(存储区域网络)等 *共享磁盘* 方案形成对照。共享磁盘通常依赖集中式存储设备、定制硬件和专用网络(如光纤通道);无共享方案不要求专用硬件,只需普通数据中心网络互联的机器。 -在 Unix 中,这种接口是一个 **文件**(file,更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix 套接字,stdin,stdout)的通信通道,设备驱动程序(比如 `/dev/audio` 或 `/dev/lp0`),表示 TCP 连接的套接字,等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起 [^ii]。 +很多 DFS 构建在商用硬件上,成本更低但故障率高于企业级专用硬件。为容忍机器和磁盘故障,文件块通常复制到多台机器。这也让调度器更容易均衡负载:任务可在任一持有输入副本的节点运行。复制可以是多副本(见[第六章](/ch6#ch_replication)),也可以是 Reed-Solomon 等 *纠删码* 方案,以更低存储开销恢复丢失数据 [^10] [^11] [^12]。这与 RAID 思想类似,只是 RAID 面向同一机器上的多块磁盘,而 DFS 是通过普通数据中心网络跨机器做访问和复制。 -[^ii]: 统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。一个 URL 标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。从一个 BBS 到另一个 BBS 的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他 BBS,然后手动找到他们正在寻找的信息。直接链接到另一个 BBS 内的一些内容当时是不可能的。 +### 对象存储 {#id277} -按照惯例,许多(但不是全部)Unix 程序将这个字节序列视为 ASCII 文本。我们的日志分析示例使用了这个事实:`awk`、`sort`、`uniq` 和 `head` 都将它们的输入文件视为由 `\n`(换行符,ASCII `0x0A`)字符分隔的记录列表。`\n` 的选择是任意的 —— 可以说,ASCII 记录分隔符 `0x1E` 本来就是一个更好的选择,因为它是为了这个目的而设计的【14】,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。 +Amazon S3、Google Cloud Storage、Azure Blob Storage、OpenStack Swift 等对象存储,已成为批处理场景中对 DFS 的主流替代。实际上两者边界越来越模糊:正如前一节和[“由对象存储支撑的数据库”](/ch6#sec_replication_object_storage)所述,FUSE 可以把 S3 这类对象存储“挂载成文件系统”;JuiceFS、Ceph 等系统也同时提供对象 API 与文件系统 API。但这些接口、性能、以及一致性保证差异很大,即便 API 看似兼容,也需要仔细验证行为是否符合预期。 -每条记录(即一行输入)的解析则更加模糊。Unix 工具通常通过空白或制表符将行分割成字段,但也使用 CSV(逗号分隔),管道分隔和其他编码。即使像 `xargs` 这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。 +对象存储中的每个对象有一个 URL,例如 `s3://my-photo-bucket/2025/04/01/birthday.png`。其中主机部分(`my-photo-bucket`)是 bucket 名,后半部分是对象 *键(key)*(示例里是 `/2025/04/01/birthday.png`)。bucket 名全局唯一;对象键在 bucket 内必须唯一。 -ASCII 文本的统一接口大多数时候都能工作,但它不是很优雅:我们的日志分析示例使用 `{print $7}` 来提取网址,这样可读性不是很好。在理想的世界中可能是 `{print $request_url}` 或类似的东西。我们稍后会回顾这个想法。 +对象读取用 `get`,写入用 `put`。与文件系统文件不同,对象写入后通常不可变;更新对象需要通过 `put` 全量重写,类似键值存储。Azure Blob Storage 和 S3 Express One Zone 支持追加,但多数对象存储不支持。它也没有 `fopen`、`fseek` 这类文件句柄 API。 -尽管几十年后还不够完美,但统一的 Unix 接口仍然是非常出色的设计。没有多少软件能像 Unix 工具一样交互组合的这么好:你不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录以管道传送至电子表格中,并将结果发布到社交网络或维基。今天,像 Unix 工具一样流畅地运行程序是一种例外,而不是规范。 +对象看起来像按目录组织,这很容易让人误解:对象存储并没有真正目录概念。所谓路径只是约定,斜杠也是 key 的一部分。这个约定允许你按前缀列出对象,类似“目录列表”,但与文件系统目录列举有两点不同: -即使是具有 **相同数据模型** 的数据库,将数据从一种数据库导出再导入到另一种数据库也并不容易。缺乏整合导致了数据的 **巴尔干化**[^译注i]。 +- 前缀 `list` 行为更像 Unix 的递归 `ls -R`:会返回所有以该前缀开头的对象,包括“子路径”下的对象。 +- 不存在“空目录”。如果你删除了 `s3://my-photo-bucket/2025/04/01` 下所有对象,再列 `s3://my-photo-bucket/2025/04` 时就看不到 `01`。常见做法是创建 0 字节对象表示空目录(如创建空对象 `s3://my-photo-bucket/2025/04/01` 以保留目录占位)。 -[^译注i]: **巴尔干化(Balkanization)** 是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 +DFS 常支持硬链接、符号链接、文件锁、原子重命名等文件系统操作,而对象存储通常缺失这些能力:链接和锁大多不支持;重命名也非原子,通常是“复制到新 key,再删除旧 key”。若要“重命名目录”,因为目录名是 key 的一部分,实际上要逐个对象重命名。 +[第四章](/ch4#ch_storage)讨论的键值存储通常面向小值(通常 KB 级)和高频低延迟读写。相比之下,DFS 和对象存储通常优化的是大对象(MB 到 GB)和低频大块读写。不过近年对象存储也在增强小对象高频访问能力,例如 S3 Express One Zone 已提供单毫秒级延迟,计费模型也更接近键值存储。 -#### 逻辑与布线相分离 +DFS 与对象存储另一个区别是:HDFS 等 DFS 可把计算任务调度到持有文件副本的机器上,让任务本地读文件,减少网络传输(当任务代码远小于待读文件时尤其划算)。对象存储通常把存储和计算解耦,虽然可能用更多带宽,但现代数据中心网络很快,通常可接受。同时这种解耦让 CPU/内存与存储容量可以独立扩展。 -Unix 工具的另一个特点是使用标准输入(`stdin`)和标准输出(`stdout`)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和 / 或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(有个小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 +### 分布式作业编排 {#id278} -如果需要,程序仍然可以直接读取和写入文件,但 Unix 方法在程序不关心特定的文件路径、只使用标准输入和标准输出时效果最好。这允许 shell 用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。(人们可以说这是一种 **松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或 **控制反转(inversion of control)**【16】)。将输入 / 输出布线与程序逻辑分开,可以将小工具组合成更大的系统。 +前面的“操作系统类比”同样适用于作业编排。在单机上跑 Unix 批处理任务时,总得有东西真正去执行 `awk`、`sort`、`uniq`、`head` 进程;需要把一个进程输出送到另一个进程输入;要给每个进程分配内存;公平调度 CPU 指令;隔离内存与 I/O 边界,等等。单机里这由操作系统内核负责;分布式环境里,这就是作业编排器(orchestrator)的职责。 -你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,它就可以加入数据处理的管道中。在日志分析示例中,你可以编写一个将 User-Agent 字符串转换为更灵敏的浏览器标识符,或者将 IP 地址转换为国家代码的工具,并将其插入管道。`sort` 程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 +批处理框架会向编排器的调度器发起“运行作业”请求。请求通常包含如下元数据: -但是,使用 `stdin` 和 `stdout` 能做的事情是有限的。需要多个输入或输出的程序虽然可能,却非常棘手。你没法将程序的输出管道连接至网络连接中【17,18】[^iii] 。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,那么 I/O 的布线就取决于程序本身了。它仍然可以被配置(例如通过命令行选项),但在 Shell 中对输入和输出进行布线的灵活性就少了。 +- 需要执行的任务数量; +- 每个任务所需内存、CPU、磁盘; +- 作业标识符; +- 访问凭据; +- 输入输出等作业参数; +- 所需硬件信息(如 GPU、磁盘类型); +- 作业可执行代码的位置。 -[^iii]: 除了使用一个单独的工具,如 `netcat` 或 `curl`。Unix 起初试图将所有东西都表示为文件,但是 BSD 套接字 API 偏离了这个惯例【17】。研究用操作系统 Plan 9 和 Inferno 在使用文件方面更加一致:它们将 TCP 连接表示为 `/net/tcp` 中的文件【18】。 +Kubernetes、Hadoop YARN(Yet Another Resource Negotiator)[^13] 等编排器会结合这些请求与集群状态,依靠以下组件执行任务: +任务执行器(Task executors) -#### 透明度和实验 +: 每个节点上运行执行器守护进程,例如 YARN 的 *NodeManager* 或 Kubernetes 的 *kubelet*。执行器负责拉起任务、通过心跳上报存活状态、跟踪节点上的任务状态与资源占用。收到“启动任务”请求后,执行器会获取作业代码并执行启动命令;随后持续监控进程直至结束或失败,并更新对应状态元数据。 -使 Unix 工具如此成功的部分原因是,它们使查看正在发生的事情变得非常容易: + 很多执行器还配合操作系统实现安全与性能隔离,例如 YARN 和 Kubernetes 都会使用 Linux *cgroups*。这样可防止任务越权访问数据,或因资源滥用影响同机其他任务。 -- Unix 命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 -- 你可以在任何时候结束管道,将管道输出到 `less`,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 -- 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。 +资源管理器(Resource Manager) -因此,与关系数据库的查询优化器相比,即使 Unix 工具非常简单,但仍然非常有用,特别是对于实验而言。 +: 资源管理器维护各节点元数据:可用硬件(CPU、GPU、内存、磁盘等)、任务状态、网络位置、节点健康状态等,从而形成全局视图。其中心化特性可能成为可用性和可伸缩性瓶颈。YARN 借助 ZooKeeper,Kubernetes 借助 etcd 存储集群状态(见[“协调服务”](/ch10#sec_consistency_coordination))。 -然而,Unix 工具的最大局限在于它们只能在一台机器上运行 —— 而 Hadoop 这样的工具即应运而生。 +调度器(Scheduler) +: 编排器通常包含中心化调度子系统,接收启动/停止作业与状态查询请求。例如收到“启动 10 个任务,使用指定 Docker 镜像,且必须运行在某类 GPU 节点上”的请求后,调度器会基于请求和资源管理器状态决定“哪些任务跑在哪些节点”,再通知执行器执行。 -## MapReduce和分布式文件系统 +不同编排器命名各异,但几乎都具备这些核心组件。 -MapReduce 有点像 Unix 工具,但分布在数千台机器上。像 Unix 工具一样,它相当简单粗暴,但令人惊异地管用。一个 MapReduce 作业可以和一个 Unix 进程相类比:它接受一个或多个输入,并产生一个或多个输出。 +> [!NOTE] +> 有些调度决策需要“应用特定调度器”参与,才能考虑更具体的业务约束,例如当查询量达到阈值时自动扩容只读副本。中心调度器与应用调度器协同决定如何执行任务。YARN 把这类子调度器称为 *ApplicationMaster*,Kubernetes 通常称为 *operator*。 -和大多数 Unix 工具一样,运行 MapReduce 作业通常不会修改输入,除了生成输出外没有任何副作用。输出文件以连续的方式一次性写入(一旦写入文件,不会修改任何现有的文件部分)。 +#### 资源分配 {#id279} -虽然 Unix 工具使用 `stdin` 和 `stdout` 作为输入和输出,但 MapReduce 作业在分布式文件系统上读写文件。在 Hadoop 的 MapReduce 实现中,该文件系统被称为 **HDFS(Hadoop 分布式文件系统)**,一个 Google 文件系统(GFS)的开源实现【19】。 +调度器在编排系统中最具挑战的职责之一,就是在资源有限且作业需求冲突时,做出合理分配。它本质上是在公平与效率之间做平衡。 -除 HDFS 外,还有各种其他分布式文件系统,如 GlusterFS 和 Quantcast File System(QFS)【20】。诸如 Amazon S3、Azure Blob 存储和 OpenStack Swift【21】等对象存储服务在很多方面都是相似的 [^iv]。在本章中,我们将主要使用 HDFS 作为示例,但是这些原则适用于任何分布式文件系统。 +假设一个小集群有 5 个节点,共 160 个 CPU 核。调度器收到两个作业请求,每个都想要 100 核。怎么排最好? -[^iv]: 一个不同之处在于,对于 HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用纠删码(Erasure Coding),则会丢失局部性,因为来自多台机器的数据必须进行合并以重建原始文件【20】。 +- 可以给每个作业先分 80 个任务,剩余 20 个等前面的任务结束后再启动。 +- 也可以先跑完其中一个作业,再等 100 核都空出来后跑另一个。这叫 *gang scheduling*(成组调度)。 +- 如果一个请求先到,调度器还要决定是立即把 100 核都给它,还是为未来请求预留一部分资源。 -与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS 基于 **无共享** 原则(请参阅 [第二部分](/part-ii) 的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 +这是很简化的例子,但已经能看到艰难权衡。以成组调度为例,如果调度器为了凑齐 100 核而长期预留资源,节点会闲置,资源利用率下降,若其他作业也在抢占式预留,还可能死锁。 -HDFS 在每台机器上运行了一个守护进程,它对外暴露网络服务,允许其他节点访问存储在该机器上的文件(假设数据中心中的每台通用计算机都挂载着一些磁盘)。名为 **NameNode** 的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS 在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。 +反过来,如果只是被动等 100 核“自然可用”,中间可能被别的作业拿走,导致长时间凑不齐,从而产生 *饥饿(starvation)*。调度器也可以 *抢占(preempt)* 一部分先到作业任务,把它们杀掉给后到作业腾资源;但被杀任务之后还要重跑,整体效率同样下降。 -为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如 [第五章](/ch5) 中所述,或者诸如 Reed-Solomon 码这样的纠删码方案,它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与 RAID 相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 +把这个问题放大到数百甚至数百万个请求,想求全局最优几乎不可行。事实上这是 *NP-hard* 问题:除了很小规模,很难在可接受时间内算出最优解 [^14] [^15]。 -HDFS 的可伸缩性已经很不错了:在撰写本书时,最大的 HDFS 部署运行在上万台机器上,总存储容量达数百 PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的 HDFS 上的数据存储和访问成本远低于在专用存储设备上支持同等容量的成本【24】。 +因此工程上调度器通常采用启发式方法,在非最优前提下做“足够好”的决策。常见算法包括 FIFO、主导资源公平(DRF)、优先级队列、容量/配额调度、各种装箱算法等。细节超出本书范围,但这是非常有趣的研究领域。 -### MapReduce作业执行 +#### 工作流调度 {#sec_batch_workflows} -MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS 等分布式文件系统中的大型数据集。理解它的最简单方法是参考 “[简单日志分析](#简单日志分析)” 中的 Web 服务器日志分析示例。MapReduce 中的数据处理模式与此示例非常相似: +本章开头的 Unix 示例是多个命令串联。分布式批处理中同样常见:一个作业输出要成为一个或多个后续作业输入,而每个作业又可能依赖多个上游输入。这个依赖结构称为 *工作流(workflow)* 或 *有向无环图(DAG)*。 -1. 读取一组输入文件,并将其分解成 **记录(records)**。在 Web 服务器日志示例中,每条记录都是日志中的一行(即 `\n` 是记录分隔符)。 -2. 调用 Mapper 函数,从每条输入记录中提取一对键值。在前面的例子中,Mapper 函数是 `awk '{print $7}'`:它提取 URL(`$7`)作为键,并将值留空。 -3. 按键排序所有的键值对。在日志的例子中,这由第一个 `sort` 命令完成。 -4. 调用 Reducer 函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,Reducer 是由 `uniq -c` 命令实现的,该命令使用相同的键来统计相邻记录的数量。 +> [!NOTE] +> 我们在[“持久化执行与工作流”](/ch5#sec_encoding_dataflow_workflows)中讨论过“按步骤执行 RPC”的工作流引擎;在批处理语境里,“工作流”指的是一串批处理过程:每一步读输入、产输出,通常不直接对外做 RPC。持久化执行引擎通常单次请求处理的数据量小于批处理系统,但两者边界并非绝对。 -这四个步骤可以作为一个 MapReduce 作业执行。步骤 2(Map)和 4(Reduce)是你编写自定义数据处理代码的地方。步骤 1(将文件分解成记录)由输入格式解析器处理。步骤 3 中的排序步骤隐含在 MapReduce 中 —— 你不必编写它,因为 Mapper 的输出始终在送往 Reducer 之前进行排序。 +需要多作业工作流常见有以下原因: -要创建 MapReduce 作业,你需要实现两个回调函数,Mapper 和 Reducer,其行为如下(请参阅 “[MapReduce 查询](/ch2#MapReduce查询)”): +- 一个作业输出可能被多个团队维护的下游作业消费。此时先把输出写到公共位置更合理,下游可按“数据更新触发”或定时方式运行。 +- 你可能要在多个处理工具间传递数据。比如 Spark 作业写 HDFS,再由 Python 触发 Trino SQL 查询(见[“云数据仓库”](/ch4#sec_cloud_data_warehouses))继续处理并写入 S3。 +- 有些流水线内部天然需要多阶段。例如第一阶段按某键分片,下一阶段按另一键分片,那么第一阶段需要先产出符合第二阶段要求的数据布局。 + +在 Unix 里,管道用很小的内存缓冲连接前后命令,不落盘。若缓冲区满,上游必须等待下游消费,这是一种 *背压(backpressure)*。Spark、Flink 等批处理执行引擎也支持类似模式:一个任务输出直接传给下一任务(跨机时经网络传输)。 + +但在工作流中,更常见仍是“上游作业写 DFS/对象存储,下游再读”,这样可让作业在时间上解耦。若一个作业有多个输入,工作流调度器通常会等待所有上游输入生产成功后再启动它。 + +YARN ResourceManager 或 Spark 内置调度器主要做“作业内调度”,不负责整条工作流。为管理跨作业依赖,出现了 Airflow、Dagster、Prefect 等工作流调度器。它们在维护大量批作业时非常关键:包含 50~100 个作业的工作流并不罕见;大型组织内很多团队会跨系统互相消费输出。没有工具支撑,很难管理这种复杂数据流。 + +#### 故障处理 {#id281} + +批处理作业往往运行时间长。长时间运行且并行任务多的作业,在执行过程中遇到至少一次任务失败几乎是常态。正如[“硬件与软件故障”](/ch2#sec_introduction_hardware_faults)和[“不可靠网络”](/ch9#sec_distributed_networks)所述,原因可能是硬件故障(商用硬件尤甚)、网络中断等。 + +任务无法完成的另一原因是被调度器主动抢占(kill)。当系统有多优先级队列时,这很常见:低优先级任务便宜、高优先级任务昂贵。低优先级任务可用空闲算力跑,但高优先级任务一到就可能把它们抢占掉。云厂商的对应产品名分别是:AWS 的 *spot instances*、Azure 的 *spot virtual machines*、GCP 的 *preemptible instances* [^16]。 + +批处理很多时候对实时性要求不高,因此很适合利用低优先级资源/抢占式实例降成本:本质上它在“吃”否则会闲置的算力,提高集群利用率。但代价是更高的被杀概率:实际里抢占往往比硬件故障更常见 [^17]。 + +由于批处理每次都从头生成输出,任务失败比在线系统更容易处理:删掉失败任务的部分输出,把任务重新调度到别的机器重跑即可。若只因一个任务失败就重跑整个作业会非常浪费,因此 MapReduce 及其后继系统都尽量让并行任务彼此独立,从而把重试粒度降到单个任务 [^3]。 + +当一个任务输出成为另一任务输入(即在工作流内传递)时,容错更复杂。MapReduce 的做法是:中间数据总是写回 DFS,且只有写入任务成功后才允许下游读取。这个方案在频繁抢占环境中也能工作,但会带来大量 DFS 写入,效率不高。 + +Spark 更倾向把中间数据放内存或溢写本地磁盘,只把最终结果写 DFS;它还记录中间数据的计算血缘,丢失时可重算 [^18]。Flink 则采用定期检查点快照机制 [^19]。我们会在[“数据流引擎”](/ch11#sec_batch_dataflow)继续讨论。 + +## 批处理模型 {#id431} + +前面我们讨论了分布式环境中批作业如何调度。现在转向“批处理框架如何处理数据”。最常见的两类模型是 MapReduce 与数据流引擎。尽管实践中数据流引擎已大面积替代 MapReduce,但理解 MapReduce 仍然重要,因为它深刻影响了现代批处理框架。 + +MapReduce 与数据流引擎都发展出多种编程接口:低层 API、关系查询语言、DataFrame API。它们让应用工程师、数据分析工程师、业务分析师乃至非技术人员都能参与数据处理。我们将在[“批处理用例”](/ch11#sec_batch_output)中讨论这些用途。 + +### MapReduce {#sec_batch_mapreduce} + +MapReduce 的处理模式与[“简单日志分析”](/ch11#sec_batch_log_analysis)几乎同构: + +1. 读取输入文件并切分为 *记录(records)*。在日志例子里,每条记录就是一行(`\n` 为记录分隔符)。在 Hadoop MapReduce 中,输入通常存放在 HDFS 或 S3 等对象存储,文件格式可能是 Parquet(列式,见[“面向列存储”](/ch4#sec_storage_column))或 Avro(行式,见[“Avro”](/ch5#sec_encoding_avro))。 +2. 调用 mapper,从每条输入记录中提取键和值。Unix 示例中 mapper 相当于 `awk '{print $7}'`:URL(`$7`)是键,值可留空。 +3. 按键排序所有键值对。日志示例中这一步对应第一次 `sort`。 +4. 调用 reducer 遍历排序后的键值对。同键记录会相邻,因此可以在很小内存状态下合并。Unix 示例中 reducer 等价于 `uniq -c`,统计相邻同键记录数。 + +这四步就是一个 MapReduce 作业。第 2 步(map)与第 4 步(reduce)是你写业务逻辑的地方;第 1 步(文件切记录)由输入格式解析器完成;第 3 步排序在 MapReduce 中是隐式内置的,你无需手写。这一步是批处理的基础算法,我们会在[“混洗数据”](/ch11#sec_shuffle)再讨论。 + +要创建 MapReduce 作业,你需实现两个回调:mapper 与 reducer,其行为如下。 Mapper -: Mapper 会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括 None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 + +: 对每条输入记录调用一次。它从输入记录中提取键和值,并可为每条输入产生任意数量键值对(包括 0 条)。它不保留跨记录状态,每条记录独立处理。 Reducer -: MapReduce 框架拉取由 Mapper 生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用 Reducer。Reducer 可以产生输出记录(例如相同 URL 的出现次数)。 -在 Web 服务器日志的例子中,我们在第 5 步中有第二个 `sort` 命令,它按请求数对 URL 进行排序。在 MapReduce 中,如果你需要第二个排序阶段,则可以通过编写第二个 MapReduce 作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper 的作用是将数据放入一个适合排序的表单中,并且 Reducer 的作用是处理已排序的数据。 +: 框架收集 mapper 产生的键值对,把同键值集合交给 reducer(以迭代器形式)。reducer 可输出结果记录(如同一 URL 的出现次数)。 -#### 分布式执行MapReduce +在日志示例里,第 5 步还有一次 `sort` 用于按请求次数排名 URL。MapReduce 若要第二轮排序,通常要再写一个作业:前一个输出作为后一个输入。换个角度看,mapper 的作用是把数据整理成适合排序的形态;reducer 的作用是处理已排序数据。 -MapReduce 与 Unix 命令管道的主要区别在于,MapReduce 可以在多台机器上并行执行计算,而无需编写代码来显式处理并行问题。Mapper 和 Reducer 一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。 +> [!TIP] MapReduce 与函数式编程 +> MapReduce 虽用于批处理,但其编程模型来自函数式编程。Lisp 把 *map* 与 *reduce/fold* 作为列表上的高阶函数引入,后来进入 Python、Rust、Java 等主流语言。包括 SQL 在内的大量数据处理操作都可在 MapReduce 之上表达。Map 和 reduce 以及函数式编程的一些特性恰好契合 MapReduce:可组合、天然适合数据处理链;map 还是典型“令人尴尬地并行”(每条输入独立处理);reduce 则可按不同键并行。 -在分布式计算中可以使用标准的 Unix 工具作为 Mapper 和 Reducer【25】,但更常见的是,它们被实现为传统编程语言的函数。在 Hadoop MapReduce 中,Mapper 和 Reducer 都是实现特定接口的 Java 类。在 MongoDB 和 CouchDB 中,Mapper 和 Reducer 都是 JavaScript 函数(请参阅 “[MapReduce 查询](/ch2#MapReduce查询)”)。 +但用原始 MapReduce API 写复杂处理其实很费力,例如各种连接算法都要自己实现 [^20]。MapReduce 相比现代批处理引擎也偏慢,一个重要原因是其“以文件为中心”的 I/O 让作业流水化困难:上游不结束,下游很难提前处理输出。 -[图 10-1](/v1/ddia_1001.png) 显示了 Hadoop MapReduce 作业中的数据流。其并行化基于分区(请参阅 [第六章](/v1/ch6)):作业的输入通常是 HDFS 中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理 map 任务([图 10-1](/v1/ddia_1001.png) 中的 m1,m2 和 m3 标记)。 +### 数据流引擎 {#sec_batch_dataflow} -每个输入文件的大小通常是数百兆字节。MapReduce 调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个 Mapper,只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务【26】。这个原则被称为 **将计算放在数据附近**【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。 +为解决 MapReduce 的局限,出现了多种分布式批处理执行引擎,最著名的是 Spark [^18] [^21] 和 Flink [^19]。它们设计细节各异,但有一个共同点:把整条工作流当成一个作业处理,而不是拆成互相独立的小作业。 -![](/v1/ddia_1001.png) +因为它们显式建模了跨多个处理阶段的数据流动,所以称为 *数据流引擎(dataflow engines)*。与 MapReduce 一样,它们提供低层 API(反复调用用户函数逐条处理记录),也提供更高层算子(如 *join*、*group by*)。它们通过分片并行输入,并通过网络把一个任务输出传给另一个任务输入。与 MapReduce 不同,算子不必严格在 map/reduce 两类角色间交替,而可以更灵活组合。 -**图 10-1 具有三个 Mapper 和三个 Reducer 的 MapReduce 任务** +这些 API 通常以关系风格构件表达计算:按字段值连接数据集、按键分组、按条件过滤、按计数或求和等函数聚合。内部实现依赖的正是下一节要讲的混洗算法。 -在大多数情况下,应该在 Mapper 任务中运行的应用代码在将要运行它的机器上还不存在,所以 MapReduce 框架首先将代码(例如 Java 程序中的 JAR 文件)复制到适当的机器。然后启动 Map 任务并开始读取输入文件,一次将一条记录传入 Mapper 回调函数。Mapper 的输出由键值对组成。 +这种处理引擎风格可追溯到 Dryad [^22]、Nephele [^23] 等研究系统。相比 MapReduce,它有几个优势: -计算的 Reduce 端也被分区。虽然 Map 任务的数量由输入文件块的数量决定,但 Reducer 的任务的数量是由作业作者配置的(它可以不同于 Map 任务的数量)。为了确保具有相同键的所有键值对最终落在相同的 Reducer 处,框架使用键的散列值来确定哪个 Reduce 任务应该接收到特定的键值对(请参阅 “[根据键的散列分区](/ch6#根据键的散列分区)”)。 +- 像排序这类昂贵操作只在“确实需要”的地方执行,而不是每个 map 与 reduce 阶段之间都默认做。 +- 连续多个不改变分片方式的算子(如 map/filter)可融合成一个任务,减少数据复制开销。 +- 由于工作流里的连接与数据依赖都显式声明,调度器能全局优化数据局部性。比如把“消费某数据”的任务放到“生产该数据”的同机上,用共享内存缓冲交换,而非走网络拷贝。 +- 算子间中间状态通常放内存或本地磁盘即可,比写 DFS/对象存储 I/O 更低(后者要多副本并落到多机磁盘)。MapReduce 仅对 mapper 输出做了这类优化,数据流引擎把它推广到所有中间状态。 +- 输入一就绪就能启动下游算子,无需等待整个上游阶段全部完成。 +- 可复用已有进程运行新算子,减少启动开销;MapReduce 往往为每个任务起一个新 JVM。 -键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个 Map 任务都按照 Reducer 对输出进行分区。每个分区都被写入 Mapper 程序的本地磁盘,使用的技术与我们在 “[SSTables 与 LSM 树](/ch3#SSTables和LSM树)” 中讨论的类似。 +因此,数据流引擎能实现与 MapReduce 工作流同样的计算,但通常速度明显更快。 -只要当 Mapper 读取完输入文件,并写完排序后的输出文件,MapReduce 调度器就会通知 Reducer 可以从该 Mapper 开始获取输出文件。Reducer 连接到每个 Mapper,并下载自己相应分区的有序键值对文件。按 Reducer 分区,排序,从 Mapper 向 Reducer 复制分区数据,这一整个过程被称为 **混洗(shuffle)**【26】(一个容易混淆的术语 —— 不像洗牌,在 MapReduce 中的混洗没有随机性)。 +### 混洗数据 {#sec_shuffle} -Reduce 任务从 Mapper 获取文件,并将它们合并在一起,并保留有序特性。因此,如果不同的 Mapper 生成了键相同的记录,则在 Reducer 的输入中,这些记录将会相邻。 +本章开头的 Unix 工具示例和 MapReduce 都建立在排序之上。批处理系统要能排序 PB 级数据,单机放不下,因此必须使用“输入与输出都分片”的分布式排序算法,这就是 *混洗(shuffle)*。 -Reducer 调用时会收到一个键,和一个迭代器作为参数,迭代器会顺序地扫过所有具有该键的记录(因为在某些情况可能无法完全放入内存中)。Reducer 可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录会写入分布式文件系统上的文件中(通常是在跑 Reducer 的机器本地磁盘上留一份,并在其他机器上留几份副本)。 +> [!NOTE] 混洗不是随机 +> “shuffle” 容易引发误解。洗牌会得到随机顺序;而这里的 shuffle 产出的是排序后的确定顺序,不含随机性。 -#### MapReduce工作流 +混洗是批处理系统的基础算法,连接与聚合都依赖它。MapReduce、Spark、Flink、Daft、Dataflow、BigQuery [^24] 都实现了高可伸缩且高性能的混洗机制以处理大数据集。这里用 Hadoop MapReduce 的混洗实现做说明 [^25],但核心思想在其他系统同样适用。 -单个 MapReduce 作业可以解决的问题范围很有限。以日志分析为例,单个 MapReduce 作业可以确定每个 URL 的页面浏览次数,但无法确定最常见的 URL,因为这需要第二轮排序。 +[图 11-1](/ch11#fig_batch_mapreduce) 展示了一个 MapReduce 作业的数据流。假设输入已分片,标记为 *m1*、*m2*、*m3*。例如每个分片可以是 HDFS 中一个文件,或对象存储中的一个对象;同一数据集的所有分片可以放在同一 HDFS 目录,或使用同一对象前缀。 -因此将 MapReduce 作业链接成为 **工作流(workflow)** 中是极为常见的,例如,一个作业的输出成为下一个作业的输入。Hadoop MapReduce 框架对工作流没有特殊支持,所以这个链是通过目录名隐式实现的:第一个作业必须将其输出配置为 HDFS 中的指定目录,第二个作业必须将其输入配置为从同一个目录。从 MapReduce 框架的角度来看,这是两个独立的作业。 +{{< figure src="/fig/ddia_1101.png" id="fig_batch_mapreduce" caption="图 11-1. 一个包含三个 mapper 和三个 reducer 的 MapReduce 作业。" class="w-full my-4" >}} -因此,被链接的 MapReduce 作业并没有那么像 Unix 命令管道(它直接将一个进程的输出作为另一个进程的输入,仅用一个很小的内存缓冲区)。它更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利也有弊,我们将在 “[物化中间状态](#物化中间状态)” 中讨论。 +框架会为每个输入分片启动一个 map 任务。任务读取分配到的文件,并逐条记录调用 mapper 回调。reduce 侧也会分片。map 任务数由输入分片数决定;reduce 任务数由作业作者配置(可与 map 数不同)。 -只有当作业成功完成后,批处理作业的输出才会被视为有效的(MapReduce 会丢弃失败作业的部分输出)。因此,工作流中的一项作业只有在先前的作业 —— 即生产其输入的作业 —— 成功完成后才能开始。为了处理这些作业之间的依赖,有很多针对 Hadoop 的工作流调度器被开发出来,包括 Oozie、Azkaban、Luigi、Airflow 和 Pinball 【28】。 +mapper 输出是键值对。框架需要保证:若不同 mapper 输出了同一个键,这些键值对最终必须由同一个 reducer 处理。为此,每个 mapper 会在本地磁盘为每个 reducer 维护一个输出文件(例如[图 11-1](/ch11#fig_batch_mapreduce)中的 *m1,r2*:由 mapper1 生成,目标是 reducer2)。mapper 每输出一条键值对,通常会按键的哈希决定写入哪个 reducer 文件(类似[“按键哈希分片”](/ch7#sec_sharding_hash))。 -这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统时,由 50 到 100 个 MapReduce 作业组成的工作流是常见的【29】。而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流而言非常重要。 +mapper 写这些文件的同时,也会在每个文件内部按键排序。可用的正是[“日志结构存储”](/ch4#sec_storage_log_structured)中的技术:先在内存有序结构里积累一批键值对,写成有序段文件,再把小段逐步合并成大段。 -Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【32】、Crunch 【33】和 FlumeJava 【34】)也能自动布线组装多个 MapReduce 阶段,生成合适的工作流。 +每个 mapper 完成后,reducer 会连接到 mapper,把属于自己的有序文件拷贝到本地磁盘。reducer 拿到所有 mapper 的对应分片后,再用归并排序方式合并它们并保持有序。同键记录即便来自不同 mapper,也会在合并后相邻。随后 reducer 以“每个键一次调用”的方式执行,每次拿到一个可迭代器,遍历该键所有值。 -### Reduce侧连接与分组 +reducer 输出记录会顺序写入文件,每个 reduce 任务一个文件。[图 11-1](/ch11#fig_batch_mapreduce)中的 *r1*、*r2*、*r3* 就是输出数据集的分片,最终写回 DFS 或对象存储。 -我们在 [第二章](/ch2) 中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。 +MapReduce 在 map 与 reduce 之间执行混洗;现代数据流引擎和云数据仓库则更复杂。BigQuery 等系统已优化混洗,使数据尽量留在内存,并写入外部排序服务 [^24],以提升速度并通过复制增强韧性。 -在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的 **外键**,文档模型中的 **文档引用** 或图模型中的 **边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如 [第二章](/ch2) 所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除 [^v]。 +#### JOIN 与 GROUP BY {#sec_batch_join} -[^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录通过与其他记录在特定字段(例如 ID)中具有 **相同值** 相关联。有些数据库支持更通用的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有地方来讲这些东西。 +下面看“有序数据”如何简化分布式连接与聚合。为便于说明仍以 MapReduce 为例,但概念适用于大多数批处理系统。 -在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用 **索引** 来快速定位感兴趣的记录(请参阅 [第三章](/ch3))。如果查询涉及到连接,则可能涉及到查找多个索引。然而 MapReduce 没有索引的概念 —— 至少在通常意义上没有。 +批处理里常见连接场景见[图 11-2](/ch11#fig_batch_join_example)。左边是用户活动日志(*activity events* 或 *clickstream data*),右边是用户数据库。它可以看作星型模型的一部分(见[“星型与雪花型:分析模式”](/ch3#sec_datamodels_analytics)):活动日志是事实表,用户库是维度表之一。 -当 MapReduce 作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为 **全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。 +{{< figure src="/fig/ddia_1102.png" id="fig_batch_join_example" caption="图 11-2. 用户活动日志与用户画像数据库的连接。" class="w-full my-4" >}} -当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。例如我们假设一个作业是同时处理所有用户的数据,而非仅仅是为某个特定用户查找数据(而这能通过索引更高效地完成)。 +如果你要做“结合用户库信息的活动分析”(例如利用用户出生日期字段,判断哪些页面更受年轻或年长用户欢迎),就需要连接这两张表。若两边都大到必须分片,怎么做? -#### 示例:用户活动事件分析 +可利用 MapReduce 的关键特性:混洗会把同键键值对汇聚到同一个 reducer,无论它们最初在哪个分片。这里用户 ID 就可以作为键。因此可写一个 mapper 扫活动日志,输出“按用户 ID 键控的页面访问 URL”(见[图 11-3](/ch11#fig_batch_join_reduce));再写一个 mapper 按行扫描用户表,提取“用户 ID 作为键、出生日期作为值”。 -[图 10-2](/fig/ddia_1102.png) 给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为 **活动事件**,即 activity events,或 **点击流数据**,即 clickstream data),右侧是用户数据库。你可以将此示例看作是星型模式的一部分(请参阅 “[星型和雪花型:分析的模式](/ch3#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。 +{{< figure src="/fig/ddia_1103.png" id="fig_batch_join_reduce" caption="图 11-3. 基于用户 ID 的排序合并连接。若输入数据集由多个文件分片组成,可并行启动多个 mapper 处理。" class="w-full my-4" >}} -![](/v1/ddia_1002.png) +混洗保证 reducer 能同时拿到某用户的出生日期和该用户全部页面访问事件。MapReduce 甚至可以把记录进一步排成 reducer 先看到用户记录、再按时间戳看到活动事件,这称为 *二次排序(secondary sort)* [^25]。 -**图 10-2 用户行为日志与用户档案的连接** +于是 reducer 很容易实现连接逻辑:先拿到出生日期并存入局部变量,再遍历同一用户 ID 的活动事件,输出“被访问 URL + 访问者出生日期”。因为 reducer 一次处理一个用户的全部记录,所以内存里只要保留一条用户记录,也无需发任何网络请求。这个算法称为 *排序合并连接(sort-merge join)*:mapper 输出先按键排序,reducer 再把连接两侧有序记录合并。 -分析任务可能需要将用户活动与用户档案信息相关联:例如,如果档案包含用户的年龄或出生日期,系统就可以确定哪些页面更受哪些年龄段的用户欢迎。然而活动事件仅包含用户 ID,而没有包含完整的用户档案信息。在每个活动事件中嵌入这些档案信息很可能会非常浪费。因此,活动事件需要与用户档案数据库相连接。 +工作流中的下一个 MapReduce 作业就可以继续计算“每个 URL 的访问者年龄分布”:先按 URL 做一次混洗,再在 reducer 中遍历同 URL 的所有访问记录(含出生日期),按年龄段维护计数并逐条累加,从而实现 *group by* 与聚合。 -实现这一连接的最简单方法是,逐个遍历活动事件,并为每个遇到的用户 ID 查询用户数据库(在远程服务器上)。这是可能的,但是它的性能可能会非常差:处理吞吐量将受限于受数据库服务器的往返时间,本地缓存的有效性很大程度上取决于数据的分布,并行运行大量查询可能会轻易压垮数据库【35】。 +### 查询语言 {#sec_batch_query_lanauges} -为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。而且,查询远程数据库意味着批处理作业变为 **非确定的(nondeterministic)**,因为远程数据库中的数据可能会改变。 +这些年分布式批处理执行引擎不断成熟。如今在上万台机器的集群上存储并处理数 PB 数据,基础设施已足够稳健。随着“如何在这规模下把系统跑起来”基本被解决,重点开始转向编程模型的可用性。 -因此,更好的方法是获取用户数据库的副本(例如,使用 ETL 进程从数据库备份中提取数据,请参阅 “[数据仓库](/ch3#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在 HDFS 中的一组文件中,而用户活动记录存储在另一组文件中,并能用 MapReduce 将所有相关记录集中到同一个地方进行高效处理。 +MapReduce、数据流引擎、云数据仓库都把 SQL 作为批处理“通用语”。这很自然:传统数据仓库本就用 SQL,数据分析/ETL 工具都支持 SQL,几乎所有开发者和分析师也都熟悉 SQL。 -#### 排序合并连接 +相比手写 MapReduce,查询语言接口不仅代码更少,还支持交互式使用:可在终端或 GUI 里写分析 SQL 并直接执行。这种交互式查询对于业务分析、产品、销售、财务等角色探索数据非常高效。虽然它不完全是“经典批处理”形态,但 SQL 让探索式查询也能在分布式批处理系统中高效完成。 -回想一下,Mapper 的目的是从每个输入记录中提取一对键值。在 [图 10-2](/v1/ddia_1002.png) 的情况下,这个键就是用户 ID:一组 Mapper 会扫过活动事件(提取用户 ID 作为键,活动事件作为值),而另一组 Mapper 将会扫过用户数据库(提取用户 ID 作为键,用户的出生日期作为值)。这个过程如 [图 10-3](/v1/ddia_1003.png) 所示。 +高级查询语言不只提升人的生产力,也提高机器执行效率。正如[“云数据仓库”](/ch4#sec_cloud_data_warehouses)所述,查询引擎要把 SQL 转成在集群里执行的批处理作业。这个从查询到语法树再到物理算子的转换过程,让引擎有机会做优化。Hive、Trino、Spark、Flink 等查询引擎都具备代价优化器:它们可分析连接输入特征,自动选择更合适的连接算法,甚至重排连接顺序以减少中间状态 [^19] [^26] [^27] [^28]。 -![](/fig/ddia_1103.png) +SQL 是最流行的通用批处理语言,但在一些细分场景中仍有其他语言。Apache Pig 提供了基于关系算子的逐步式数据流水线描述方式,而非“一个超大 SQL 查询”。DataFrame(下一节)有相似特征,Morel 则是受 Pig 影响的更现代语言。还有用户采用 jq、JMESPath、JsonPath 等 JSON 查询语言。 -**图 10-3 在用户 ID 上进行的 Reduce 端连接。如果输入数据集分区为多个文件,则每个分区都会被多个 Mapper 并行处理** +在[“图状数据模型”](/ch3#sec_datamodels_graph)中,我们讨论了图建模与图查询语言如何遍历边和顶点。许多图处理框架也支持通过查询语言做批计算,例如 Apache TinkerPop 的 Gremlin。我们会在[“批处理用例”](/ch11#sec_batch_output)继续看图处理场景。 -当 MapReduce 框架通过键对 Mapper 输出进行分区,然后对键值对进行排序时,效果是具有相同 ID 的所有活动事件和用户记录在 Reducer 输入中彼此相邻。Map-Reduce 作业甚至可以也让这些记录排序,使 Reducer 总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为 **二次排序(secondary sort)**【26】。 +> [!TIP] 批处理与云数据仓库正在收敛 +> 历史上,数据仓库运行在专用硬件设备上,主要提供关系数据的 SQL 分析查询;而 MapReduce 等批处理框架强调更高可伸缩性与更高灵活性,允许使用通用编程语言写处理逻辑,并读写任意数据格式。 +> +> 随着发展,两者越来越像。现代批处理框架已经支持 SQL,并借助 Parquet 等列式格式和优化执行引擎(见[“查询执行:编译与向量化”](/ch4#sec_storage_vectorized))在关系查询上获得良好性能。与此同时,数据仓库通过云化(见[“云数据仓库”](/ch4#sec_cloud_data_warehouses))获得更强可伸缩能力,并实现了许多与分布式批处理框架相同的调度、容错和混洗技术,很多也使用分布式文件系统。 +> +> 正如批处理系统采纳 SQL,云仓库也在采纳 DataFrame 等替代处理模型(下一节)。例如 BigQuery 提供 BigQuery DataFrames,Snowflake 的 Snowpark 能与 Pandas 集成。Airflow、Prefect、Dagster 等批处理工作流编排器也已广泛集成云仓库。 +> +> 当然,并非所有批任务都容易用 SQL 表达。PageRank 等迭代图算法、复杂机器学习任务都很难用 SQL 写。涉及图像、视频、音频等非关系多模态数据的 AI 处理同样如此。 +> +> 此外,云数据仓库在某些负载上并不理想。行级逐条计算与列式存储不匹配,效率较低,此时更适合使用仓库的其他 API 或批处理系统。云仓库通常也比其他批处理系统更贵,某些大作业放到 Spark/Flink 等系统可能更具成本优势。 +> +> 因此,“用批处理系统还是数据仓库”最终要看成本、便利性、实现复杂度、可用性等综合因素。大型企业往往并存多套系统以保留选择空间;小公司通常一套系统也能跑起来。 -然后 Reducer 可以容易地执行实际的连接逻辑:每个用户 ID 都会被调用一次 Reducer 函数,且因为二次排序,第一个值应该是来自用户数据库的出生日期记录。Reducer 将出生日期存储在局部变量中,然后使用相同的用户 ID 遍历活动事件,输出 **已观看网址** 和 **观看者年龄** 的结果对。随后的 Map-Reduce 作业可以计算每个 URL 的查看者年龄分布,并按年龄段进行聚集。 +### DataFrames {#id287} -由于 Reducer 一次处理一个特定用户 ID 的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为 **排序合并连接(sort-merge join)**,因为 Mapper 的输出是按键排序的,然后 Reducer 将来自连接两侧的有序记录列表合并在一起。 +随着数据科学家和统计学家开始用分布式批处理框架做机器学习,他们发现原有处理模型不够顺手,因为他们更习惯 R 与 Pandas 里的 DataFrame 数据模型(见[“DataFrame、矩阵与数组”](/ch3#sec_datamodels_dataframes))。DataFrame 与关系库里的表很像:由多行组成,同一列值类型一致。它不是写一个超大 SQL,而是通过调用对应关系算子的函数来做过滤、连接、排序、分组等操作。 -#### 把相关数据放在一起 +早期 DataFrame 操作大多在本地内存执行,因此只能处理单机装得下的数据集。数据科学家希望在批处理环境中,仍用熟悉的 DataFrame API 处理大数据。Spark、Flink、Daft 等分布式框架都因此提供了 DataFrame API。需要注意的是,本地 DataFrame 通常带索引且有顺序,而分布式 DataFrame 往往没有 [^29],迁移时可能出现性能“意外”。 -在排序合并连接中,Mapper 和排序过程确保了所有对特定用户 ID 执行连接操作的必须数据都被放在同一个地方:单次调用 Reducer 的地方。预先排好了所有需要的数据,Reducer 可以是相当简单的单线程代码,能够以高吞吐量和与低内存开销扫过这些记录。 +DataFrame API 看起来和数据流 API 相似,但实现方式差别不小。Pandas 调用方法后通常立刻执行;Spark 则会先把 DataFrame API 调用翻译为查询计划,做查询优化后,再在分布式数据流引擎上执行,从而获得更好性能。 -这种架构可以看做,Mapper 将 “消息” 发送给 Reducer。当一个 Mapper 发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使键只是一个任意的字符串(不是像 IP 地址和端口号那样的实际的网络地址),它表现的就像一个地址:所有具有相同键的键值对将被传递到相同的目标(一次 Reducer 的调用)。 +Daft 等框架甚至同时支持客户端与服务端计算:小规模内存操作在客户端执行,大数据与重计算在服务端执行。Apache Arrow 等列式格式提供统一数据模型,可被两侧执行引擎共享。 -使用 MapReduce 编程模型,能将计算的物理网络通信层面(从正确的机器获取数据)从应用逻辑中剥离出来(获取数据后执行处理)。这种分离与数据库的典型用法形成了鲜明对比,从数据库中获取数据的请求经常出现在应用代码内部【36】。由于 MapReduce 处理了所有的网络通信,因此它也避免了让应用代码去担心部分故障,例如另一个节点的崩溃:MapReduce 在不影响应用逻辑的情况下能透明地重试失败的任务。 +## 批处理用例 {#sec_batch_output} -#### 分组 +了解了批处理如何工作后,我们来看它在不同应用中的落地。批处理非常适合“海量数据的批量计算”,但不适合低延迟场景。因此,只要数据多且新鲜度要求不高,几乎都能看到批处理的身影。这听起来像限制,但现实里大量工作都符合这个模型: -除了连接之外,“把相关数据放在一起” 的另一种常见模式是,按某个键对记录分组(如 SQL 中的 GROUP BY 子句)。所有带有相同键的记录构成一个组,而下一步往往是在每个组内进行某种聚合操作,例如: +- 会计对账与库存核对:企业定期验证交易、银行账户与库存是否一致,常由批处理完成 [^30]。 +- 制造业需求预测:通常以周期性批任务计算 [^31]。 +- 电商、媒体、社交平台推荐模型训练:大量依赖批处理 [^32] [^33]。 +- 许多金融系统也是批处理驱动。例如美国银行网络几乎完全基于批任务运行 [^34]。 -- 统计每个组中记录的数量(例如在统计 PV 的例子中,在 SQL 中表示为 `COUNT(*)` 聚合) -- 对某个特定字段求和(SQL 中的 `SUM(fieldname)`) -- 按某种分级函数取出排名前 k 条记录。 +下面分别讨论几个几乎所有行业都常见的批处理用例。 -使用 MapReduce 实现这种分组操作的最简单方法是设置 Mapper,以便它们生成的键值对使用所需的分组键。然后分区和排序过程将所有具有相同分区键的记录导向同一个 Reducer。因此在 MapReduce 之上实现分组和连接看上去非常相似。 +### 提取-转换-加载(ETL) {#sec_batch_etl_usage} -分组的另一个常见用途是整理特定用户会话的所有活动事件,以找出用户进行的一系列操作(称为 **会话化(sessionization)**【37】)。例如,可以使用这种分析来确定显示新版网站的用户是否比那些显示旧版本的用户更有购买欲(A/B 测试),或者计算某个营销活动是否值得。 +[“数据仓库”](/ch1#sec_introduction_dwh)介绍了 ETL/ELT:从生产数据库抽取数据、进行转换,再加载到下游系统。本节用“ETL”统称这两类负载。尤其当下游是数据仓库时,ETL 常由批处理作业承载。 -如果你有多个 Web 服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话 cookie,用户 ID 或类似的标识符作为分组键,以将特定用户的所有活动事件放在一起来实现会话化,与此同时,不同用户的事件仍然散布在不同的分区中。 +批处理天然并行,非常适合数据转换。很多转换任务都是“令人尴尬地并行”:过滤、字段投影及大量常见仓库转换都可并行完成。 -#### 处理偏斜 +批处理环境通常自带成熟工作流调度器,便于安排、编排和调试 ETL 流水线。发生故障时,调度器常会自动重试以覆盖瞬时问题;若持续失败,则明确标记失败,便于工程师快速定位流水线中断点。像 Airflow 还内置大量 source/sink/query 算子,可直接对接 MySQL、PostgreSQL、Snowflake、Spark、Flink 等数十种系统。调度器与数据处理系统的紧密集成显著简化了数据集成。 -如果存在与单个键关联的大量数据,则 “将具有相同键的所有记录放到相同的位置” 这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为 **关键对象(linchpin object)**【38】或 **热键(hot key)**。 +我们也看到,批处理在“出错后排障与修复”方面很友好,这对调试数据流水线极其关键。失败文件可直接检查,ETL 作业可修复后重跑。比如输入文件不再包含某个转换逻辑依赖字段,数据工程师就能据此更新转换逻辑或修复上游生产作业。 -在单个 Reducer 中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的 **偏斜**(也称为 **热点**,即 hot spot)—— 也就是说,一个 Reducer 必须比其他 Reducer 处理更多的记录(请参阅 “[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)”)。由于 MapReduce 作业只有在所有 Mapper 和 Reducer 都完成时才完成,所有后续作业必须等待最慢的 Reducer 才能启动。 +过去数据流水线往往由单一数据工程团队集中维护,因为让产品团队自行编写和维护复杂批流水线不太现实。近年随着处理模型和元数据管理改进,组织内更多团队都能参与并维护自己的流水线。*data mesh* [^35] [^36]、*data contract* [^37]、*data fabric* [^38] 等实践,正通过规范和工具帮助团队安全发布可被全组织消费的数据。 -如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig 中的 **偏斜连接(skewed join)** 方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper 会将热键的关联记录 **随机**(相对于传统 MapReduce 基于键散列的确定性方法)发送到几个 Reducer 之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到 **所有** 处理该键的 Reducer 上【40】。 +如今数据流水线与分析查询不仅共享处理模型,也常共享执行引擎。很多 ETL 作业与消费其输出的分析查询都运行在同一系统里:例如同样以 SparkSQL、Trino 或 DuckDB 查询执行。这样的架构进一步模糊了应用工程、数据工程、分析工程与业务分析之间的界限。 -这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。Crunch 中的 **分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。 +### 分析(Analytics) {#sec_batch_olap} -Hive 的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用 Map 端连接(请参阅下一节)。 +在[“操作型系统与分析型系统”](/ch1#sec_introduction_analytics)中我们看到,分析查询(OLAP)通常要扫描大量记录并做分组聚合。这类负载可以与其他批任务一起运行在批处理系统中。分析人员写 SQL,经查询引擎执行,读写底层 DFS 或对象存储。表到文件映射、名称、类型等表元数据通常由 Apache Iceberg 等表格式与 Unity 等 catalog 管理(见[“云数据仓库”](/ch4#sec_cloud_data_warehouses))。这种架构称为 *数据湖仓(data lakehouse)* [^39]。 -当按照热键进行分组并聚合时,可以将分组分两个阶段进行。第一个 MapReduce 阶段将记录发送到随机 Reducer,以便每个 Reducer 只对热键的子集执行分组,为每个键输出一个更紧凑的中间聚合结果。然后第二个 MapReduce 作业将所有来自第一阶段 Reducer 的中间聚合结果合并为每个键一个值。 +与 ETL 类似,SQL 接口改进让很多组织用 Spark 等批框架直接承载分析。常见模式有两类: +- 预聚合查询:先把数据滚动聚合为 OLAP 立方体或数据集市,以提升查询速度(见[“物化视图与数据立方”](/ch4#sec_storage_materialized_views))。预聚合结果可在仓库查询,或推送到 Apache Druid、Apache Pinot 这类实时 OLAP 系统。预聚合通常按固定周期运行,通常由[“工作流调度”](/ch11#sec_batch_workflows)中提到的调度器管理。 +- 临时查询(ad hoc):用户为回答具体业务问题、分析用户行为、排查运行问题等随时发起。该场景非常看重响应时间,分析师通常会根据每次结果继续迭代提问。执行快的批处理查询引擎可显著缩短等待。 -### Map侧连接 +SQL 支持还让批处理系统更易接入电子表格与可视化工具,如 Tableau、Power BI、Looker、Apache Superset。比如 Tableau 有 SparkSQL、Presto 连接器;Superset 支持 Trino、Hive、Spark SQL、Presto 等大量最终会触发批任务的数据系统。 -上一节描述的连接算法在 Reducer 中执行实际的连接逻辑,因此被称为 Reduce 侧连接。Mapper 扮演着预处理输入数据的角色:从每个输入记录中提取键值,将键值对分配给 Reducer 分区,并按键排序。 +### 机器学习 {#id290} -Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper 都可以对其预处理以备连接。然而不利的一面是,排序,复制至 Reducer,以及合并 Reducer 输入,所有这些操作可能开销巨大。当数据通过 MapReduce 阶段时,数据可能需要落盘好几次,取决于可用的内存缓冲区【37】。 +机器学习(ML)高度依赖批处理。数据科学家、ML 工程师、AI 工程师会用批处理框架探索数据模式、做数据转换、训练模型。常见用途包括: -另一方面,如果你 **能** 对输入数据作出某些假设,则通过使用所谓的 Map 侧连接来加快连接速度是可行的。这种方法使用了一个裁减掉 Reducer 与排序的 MapReduce 作业,每个 Mapper 只是简单地从分布式文件系统中读取一个输入文件块,然后将输出文件写入文件系统,仅此而已。 +- 特征工程:把原始数据过滤并转换为可训练数据。预测模型往往要求数值特征,因此文本或离散值等数据需要先转成目标格式。 +- 模型训练:训练数据是批过程输入,训练后模型权重是输出。 +- 批量推理:当数据集很大且不要求实时结果时,可对整批数据做预测,也包括在测试集上评估模型预测效果。 -#### 广播散列连接 +很多框架为这些场景提供了专用工具。例如 Spark 的 MLlib、Flink 的 FlinkML 都内置丰富的特征工程工具、统计函数与分类器。 -适用于执行 Map 端连接的最简单场景是大数据集与小数据集连接的情况。要点在于小数据集需要足够小,以便可以将其全部加载到每个 Mapper 的内存中。 +推荐系统和排序系统等 ML 应用也大量使用图处理(见[“图状数据模型”](/ch3#sec_datamodels_graph))。许多图算法表达为“沿边逐步传播信息并反复迭代”:把一个顶点与相邻顶点连接,传递某些信息,重复直到满足停止条件,例如无边可继续,或某个指标收敛。 -例如,假设在 [图 10-2](/fig/ddia_1102.png) 的情况下,用户数据库小到足以放进内存中。在这种情况下,当 Mapper 启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的散列表中。完成此操作后,Mapper 可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户 ID [^vi]。 +*批同步并行(bulk synchronous parallel, BSP)* 计算模型 [^40] 已成为批图计算常用模型。Apache Giraph [^20]、Spark GraphX、Flink Gelly [^41] 等都实现了它。它也常被称为 *Pregel* 模型,因为 Google 的 Pregel 论文让这一方法广为人知 [^42]。 -[^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户 ID 唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,而连接运算符将对每个键输出所有的匹配。 +批处理同样是大语言模型(LLM)数据准备与训练的重要组成部分。网页等原始文本通常存放在 DFS 或对象存储中,必须先预处理才能用于训练。适合批处理框架的预处理步骤包括: -参与连接的较大输入的每个文件块各有一个 Mapper(在 [图 10-2](/fig/ddia_1102.png) 的例子中活动事件是较大的输入)。每个 Mapper 都会将较小输入整个加载到内存中。 +- 从 HTML 中提取纯文本,并修复损坏文本; +- 检测并清理低质量、无关或重复文档; +- 对文本做分词并转换为嵌入向量(词或片段的数值表示)。 -这种简单有效的算法被称为 **广播散列连接(broadcast hash join)**:**广播** 一词反映了这样一个事实,每个连接较大输入端分区的 Mapper 都会将较小输入端数据集整个读入内存中(所以较小输入实际上 “广播” 到较大数据的所有分区上),**散列** 一词反映了它使用一个散列表。Pig(名为 “**复制链接(replicated join)**”),Hive(“**MapJoin**”),Cascading 和 Crunch 支持这种连接。它也被诸如 Impala 的数据仓库查询引擎使用【41】。 +Kubeflow、Flyte、Ray 等框架就专为这类负载构建。以 OpenAI 为例,ChatGPT 训练流程中就使用了 Ray [^43]。这些框架通常内置与 PyTorch、TensorFlow、XGBoost 等 LLM/AI 库的集成,并支持特征工程、模型训练、批量推理、微调等能力。 -除了将较小的连接输入加载到内存散列表中,另一种方法是将较小输入存储在本地磁盘上的只读索引中【42】。索引中经常使用的部分将保留在操作系统的页面缓存中,因而这种方法可以提供与内存散列表几乎一样快的随机查找性能,但实际上并不需要数据集能放入内存中。 +最后,数据科学家常在 Jupyter、Hex 等交互式 Notebook 中实验数据。Notebook 由多个 *cell* 组成,每个 cell 是一小段 Markdown、Python 或 SQL;按顺序执行可得到表格、图表或数据结果。很多 Notebook 背后通过 DataFrame API 或 SQL 调用批处理系统。 -#### 分区散列连接 +### 对外提供衍生数据 {#sec_batch_serving_derived} -如果 Map 侧连接的输入以相同的方式进行分区,则散列连接方法可以独立应用于每个分区。在 [图 10-2](/fig/ddia_1102.png) 的情况中,你可以根据用户 ID 的最后一位十进制数字来对活动事件和用户数据库进行分区(因此连接两侧各有 10 个分区)。例如,Mapper3 首先将所有具有以 3 结尾的 ID 的用户加载到散列表中,然后扫描 ID 为 3 的每个用户的所有活动事件。 +批处理常用于构建预计算/衍生数据集,如商品推荐、面向用户的报表、机器学习特征等。这些数据通常由生产数据库、键值存储或搜索引擎对外服务。不论目标系统是什么,都需要把批处理环境中的 DFS/对象存储输出,回灌到线上服务数据库。 -如果分区正确无误,可以确定的是,所有你可能需要连接的记录都落在同一个编号的分区中。因此每个 Mapper 只需要从输入两端各读取一个分区就足够了。好处是每个 Mapper 都可以在内存散列表中少放点数据。 +最直观的做法是:在批作业里直接使用数据库客户端库,一条条写生产数据库(假设防火墙允许)。这虽然能工作,但通常不是好主意,原因有三: -这种方法只有当连接两端输入有相同的分区数,且两侧的记录都是使用相同的键与相同的哈希函数做分区时才适用。如果输入是由之前执行过这种分组的 MapReduce 作业生成的,那么这可能是一个合理的假设。 +- 每条记录一次网络请求,比批任务正常吞吐低几个数量级。即便客户端支持批写,性能通常也不理想。 +- 批处理框架常并行跑很多任务。若所有任务同时以批处理速率写同一数据库,很容易把数据库压垮,进而影响其在线查询性能,引发系统其他部分故障 [^44]。 +- 批作业通常提供清晰的“全有或全无”输出语义:作业成功时,结果等价于每个任务恰好执行一次;作业失败时,无有效输出。但如果在作业内直接写外部系统,就产生了外部可见副作用,难以隐藏:部分完成结果可能被其他系统看到,任务失败重启还可能造成重复写。 -分区散列连接在 Hive 中称为 **Map 侧桶连接(bucketed map joins)【37】**。 +更好的方案是把预计算结果先推送到 Kafka 这类流系统(我们会在[第十二章](/ch12#ch_stream)深入讨论)。Elasticsearch、Apache Pinot、Apache Druid、Venice 这类衍生数据存储 [^45],以及 ClickHouse 等云数仓,都支持从 Kafka 摄入数据。通过流系统过渡可以改善前述问题: -#### Map侧合并连接 +- 流系统针对顺序写优化,更适合批作业的大吞吐写入模式; +- 流系统可在批作业与生产库间充当缓冲层,下游可按自身能力限速读取,避免影响线上流量; +- 一个批作业输出可被多个下游系统同时消费; +- 流系统还可作为批处理网络与生产网络之间的安全边界(可部署在 DMZ)。 -如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行 **排序**,则可适用另一种 Map 侧连接的变体。在这种情况下,输入是否小到能放入内存并不重要,因为这时候 Mapper 同样可以执行归并操作(通常由 Reducer 执行)的归并操作:按键递增的顺序依次读取两个输入文件,将具有相同键的记录配对。 +但“经由流”并不会自动解决“全有或全无”语义。要实现这一点,批作业需要在完成后向下游发出“作业完成,可对外可见”的通知。流消费者需要像 *读已提交(read committed)* 事务那样,在收到完成通知前让新数据对查询不可见(见[“读已提交”](/ch8#sec_transactions_read_committed))。 -如果能进行 Map 侧合并连接,这通常意味着前一个 MapReduce 作业可能一开始就已经把输入数据做了分区并进行了排序。原则上这个连接就可以在前一个作业的 Reduce 阶段进行。但使用独立的仅 Map 作业有时也是合适的,例如,分好区且排好序的中间数据集可能还会用于其他目的。 +另一种在数据库冷启动(bootstrap)时更常见的模式,是在批作业内直接构建一个全新数据库,再把文件从 DFS、对象存储或本地文件系统批量导入目标数据库。很多系统都提供这类批量导入工具,如 TiDB Lightning、Apache Pinot/Apache Druid 的 Hadoop 导入作业,RocksDB 也提供从批作业批量导入 SST 的 API。 -#### MapReduce工作流与Map侧连接 +“批构建 + 批导入”速度非常快,也更容易在不同数据版本间做原子切换。但对于需要持续增量更新的场景,这种“每次构建全新库”的方式会更难。很多系统采用混合策略,同时支持冷启动与增量加载。比如 Venice 就支持混合存储,可同时做基于行的批更新和全量数据集切换。 -当下游作业使用 MapReduce 连接的输出时,选择 Map 侧连接或 Reduce 侧连接会影响输出的结构。Reduce 侧连接的输出是按照 **连接键** 进行分区和排序的,而 Map 端连接的输出则按照与较大输入相同的方式进行分区和排序(因为无论是使用分区连接还是广播连接,连接较大输入端的每个文件块都会启动一个 Map 任务)。 +## 本章小结 {#id292} -如前所述,Map 侧连接也对输入数据集的大小,有序性和分区方式做出了更多假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据是按哪些键做的分区和排序,以及分区的数量。 +本章讨论了批处理系统的设计与实现。我们先从经典 Unix 工具链(awk、sort、uniq 等)出发,说明了批处理的基础原语,例如排序和计数。 -在 Hadoop 生态系统中,这种关于数据集分区的元数据通常在 HCatalog 和 Hive Metastore 中维护【37】。 +然后我们把视角扩展到分布式批处理系统。批处理以“不可变、有限(bounded)的输入数据集”为对象,生成输出数据,这使得重跑和调试可以不引入副作用。围绕这一模式,批处理框架通常包含三层核心能力:决定作业何时何地运行的编排层,负责持久化数据的存储层,以及执行实际计算的计算层。 +我们看了分布式文件系统和对象存储如何通过分块复制、缓存和元数据服务管理大文件,也讨论了现代批处理框架如何通过可插拔 API 与这些存储交互。我们还讨论了编排器在大集群中如何调度任务、分配资源和处理故障,以及“按作业调度”的编排器与“按依赖图管理整组作业生命周期”的工作流编排器之间的区别。 -### 批处理工作流的输出 +在处理模型方面,我们回顾了 MapReduce 及其经典 map/reduce 函数,又介绍了 Spark、Flink 等更易用且性能更好的数据流引擎。为了理解批作业如何扩展到大规模,我们重点讲了混洗(shuffle)算法,它是实现分组、连接、聚合的基础操作。 -我们已经说了很多用于实现 MapReduce 工作流的算法,但却忽略了一个重要的问题:这些处理完成之后的最终结果是什么?我们最开始为什么要跑这些作业? +随着批处理系统成熟,焦点转向可用性。高级查询语言(尤其 SQL)和 DataFrame API 让批处理作业更易编写,也更容易被优化器优化。查询优化器把声明式查询转换为高效执行计划。 -在数据库查询的场景中,我们将事务处理(OLTP)与分析两种目的区分开来(请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”)。我们看到,OLTP 查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前 10 项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。 +最后我们回顾了批处理常见用例: -批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而 MapReduce 作业工作流与用于分析目的的 SQL 查询是不同的(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。 +- ETL 流水线:通过定时工作流在不同系统间提取、转换、加载数据; +- 分析:既支持预聚合报表,也支持临时探索查询; +- 机器学习:用于准备与处理大规模训练数据; +- 把批处理输出灌入面向生产流量的系统:常通过流系统或批量导入工具,把衍生数据提供给用户。 -#### 建立搜索索引 +下一章我们将转向流处理。与批处理不同,流处理输入是 *无界(unbounded)* 的:作业仍在,但输入是持续不断的数据流,因此作业不会“完成”。我们会看到,流处理与批处理在一些方面很相似,但“输入无界”这一前提也会显著改变系统设计。 -Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】,但如果从构建搜索索引的角度来看,更能帮助理解 MapReduce。(直至今日,Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】) -我们在 “[全文检索与模糊索引](/ch3#全文检索与模糊索引)” 中简要地了解了 Lucene 这样的全文检索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。 +### 参考文献 {#references} -如果需要对一组固定文档执行全文检索,则批处理是一种构建索引的高效方法:Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)并行处理效果拔群。 - -由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。 - -如果索引的文档集合发生更改,一种选择是定期重跑整个索引工作流,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法的计算成本可能会很高。但它的优点是索引过程很容易理解:文档进,索引出。 - -另一个选择是,可以增量建立索引。如 [第三章](/ch3) 中讨论的,如果要在索引中添加,删除或更新文档,Lucene 会写新的段文件,并在后台异步合并压缩段文件。我们将在 [第十二章](/ch12) 中看到更多这种增量处理。 - -#### 键值存储作为批处理输出 - -搜索索引只是批处理工作流可能输出的一个例子。批处理的另一个常见用途是构建机器学习系统,例如分类器(比如垃圾邮件过滤器,异常检测,图像识别)与推荐系统(例如,你可能认识的人,你可能感兴趣的产品或相关的搜索【29】)。 - -这些批处理作业的输出通常是某种数据库:例如,可以通过给定用户 ID 查询该用户推荐好友的数据库,或者可以通过产品 ID 查询相关产品的数据库【45】。 - -这些数据库需要被处理用户请求的 Web 应用所查询,而它们通常是独立于 Hadoop 基础设施的。那么批处理过程的输出如何回到 Web 应用可以查询的数据库中呢? - -最直接的选择可能是,直接在 Mapper 或 Reducer 中使用你最爱的数据库的客户端库,并从批处理作业直接写入数据库服务器,一次写入一条记录。它能工作(假设你的防火墙规则允许从你的 Hadoop 环境直接访问你的生产数据库),但这并不是一个好主意,出于以下几个原因: - -- 正如前面在连接的上下文中讨论的那样,为每条记录发起一个网络请求,要比批处理任务的正常吞吐量慢几个数量级。即使客户端库支持批处理,性能也可能很差。 -- MapReduce 作业经常并行运行许多任务。如果所有 Mapper 或 Reducer 都同时写入相同的输出数据库,并以批处理的预期速率工作,那么该数据库很可能被轻易压垮,其查询性能可能变差。这可能会导致系统其他部分的运行问题【35】。 -- 通常情况下,MapReduce 为作业输出提供了一个干净利落的 “全有或全无” 保证:如果作业成功,则结果就是每个任务恰好执行一次所产生的输出,即使某些任务失败且必须一路重试。如果整个作业失败,则不会生成输出。然而从作业内部写入外部系统,会产生外部可见的副作用,这种副作用是不能以这种方式被隐藏的。因此,你不得不去操心对其他系统可见的部分完成的作业结果,并需要理解 Hadoop 任务尝试与预测执行的复杂性。 - -更好的解决方案是在批处理作业 **内** 创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上节中的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。不少键值存储都支持在 MapReduce 作业中构建数据库文件,包括 Voldemort 【46】、Terrapin 【47】、ElephantDB 【48】和 HBase 批量加载【49】。 - -构建这些数据库文件是 MapReduce 的一种好用法:使用 Mapper 提取出键并按该键排序,已经完成了构建索引所必需的大量工作。由于这些键值存储大多都是只读的(文件只能由批处理作业一次性写入,然后就不可变),所以数据结构非常简单。比如它们就不需要预写式日志(WAL,请参阅 “[让 B 树更可靠](/ch3#让B树更可靠)”)。 - -将数据加载到 Voldemort 时,服务器将继续用旧数据文件服务请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动将查询切换到新文件。如果在这个过程中出现任何问题,它可以轻易回滚至旧文件,因为它们仍然存在而且不可变【46】。 - -#### 批处理输出的哲学 - -本章前面讨论过的 Unix 哲学(“[Unix 哲学](#Unix哲学)”)鼓励以显式指明数据流的方式进行实验:程序读取输入并写入输出。在这一过程中,输入保持不变,任何先前的输出都被新输出完全替换,且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,略做改动或进行调试,而不会搅乱系统的状态。 - -MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护: - -- 如果在代码中引入了一个错误,而输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将重新被纠正。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。(能够从错误代码中恢复的概念被称为 **人类容错(human fault tolerance)**【50】) -- 由于回滚很容易,比起在错误意味着不可挽回的伤害的环境,功能开发进展能快很多。这种 **最小化不可逆性(minimizing irreversibility)** 的原则有利于敏捷软件开发【51】。 -- 如果 Map 或 Reduce 任务失败,MapReduce 框架将自动重新调度,并在同样的输入上再次运行它。如果失败是由代码中的错误造成的,那么它会不断崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于临时问题导致的,那么故障就会被容忍。因为输入不可变,这种自动重试是安全的,而失败任务的输出会被 MapReduce 框架丢弃。 -- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。 -- 与 Unix 工具类似,MapReduce 作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。 - -在这些领域,在 Unix 上表现良好的设计原则似乎也适用于 Hadoop,但 Unix 和 Hadoop 在某些方面也有所不同。例如,因为大多数 Unix 工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用 `{print $7}` 来提取 URL)。在 Hadoop 上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如 Avro(请参阅 “[Avro](/ch4#Avro)”)和 Parquet(请参阅 “[列式存储](/ch3#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见 [第四章](/ch4))。 - -### Hadoop与分布式数据库的对比 - -正如我们所看到的,Hadoop 有点像 Unix 的分布式版本,其中 HDFS 是文件系统,而 MapReduce 是 Unix 进程的怪异实现(总是在 Map 阶段和 Reduce 阶段运行 `sort` 工具)。我们了解了如何在这些原语的基础上实现各种连接和分组操作。 - -当 MapReduce 论文发表时【1】,它从某种意义上来说 —— 并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前所谓的 **大规模并行处理(MPP,massively parallel processing)** 数据库中实现了【3,40】。比如 Gamma database machine、Teradata 和 Tandem NonStop SQL 就是这方面的先驱【52】。 - -最大的区别是,MPP 数据库专注于在一组机器上并行执行分析 SQL 查询,而 MapReduce 和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。 - -#### 存储多样性 - -数据库要求你根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本、图像、视频、传感器读数、稀疏矩阵、特征向量、基因组序列或任何其他类型的数据。 - -说白了,Hadoop 开放了将数据不加区分地转储到 HDFS 的可能性,允许后续再研究如何进一步处理【53】。相比之下,在将数据导入数据库专有存储格式之前,MPP 数据库通常需要对数据和查询模式进行仔细的前期建模。 - -在纯粹主义者看来,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更高质量的数据来处理。然而实践经验表明,简单地使数据快速可用 —— 即使它很古怪,难以使用,使用原始格式 —— 也通常要比事先决定理想数据模型要更有价值【54】。 - -这个想法与数据仓库类似(请参阅 “[数据仓库](/ch3#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨越以前相互分离的数据集进行连接。MPP 数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为 “**数据湖(data lake)**” 或 “**企业数据中心(enterprise data hub)**”【55】)。 - -不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式** 方法【56】;请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为 **寿司原则(sushi principle)**:“原始数据更好”【57】。 - -因此,Hadoop 经常被用于实现 ETL 过程(请参阅 “[数据仓库](/ch3#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写 MapReduce 作业来清理数据,将其转换为关系形式,并将其导入 MPP 数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。 - -#### 处理模型的多样性 - -MPP 数据库是单体的,紧密集成的软件,负责磁盘上的存储布局,查询计划,调度和执行。由于这些组件都可以针对数据库的特定需求进行调整和优化,因此整个系统可以在其设计针对的查询类型上取得非常好的性能。而且,SQL 查询语言允许以优雅的语法表达查询,而无需编写代码,可以在业务分析师使用的可视化工具(例如 Tableau)中访问到。 - -另一方面,并非所有类型的处理都可以合理地表达为 SQL 查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文检索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常是特别针对特定应用的(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 - -MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码。如果你有 HDFS 和 MapReduce,那么你 **可以** 在它之上建立一个 SQL 查询执行引擎,事实上这正是 Hive 项目所做的【31】。但是,你也可以编写许多其他形式的批处理,这些批处理不必非要用 SQL 查询表示。 - -随后,人们发现 MapReduce 对于某些类型的处理而言局限性很大,表现很差,因此在 Hadoop 之上其他各种处理模型也被开发出来(我们将在 “[MapReduce 之后](#MapReduce之后)” 中看到其中一些)。只有两种处理模型,SQL 和 MapReduce,还不够,需要更多不同的模型!而且由于 Hadoop 平台的开放性,实施一整套方法是可行的,而这在单体 MPP 数据库的范畴内是不可能的【58】。 - -至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在 Hadoop 方式中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个集群内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。 - -Hadoop 生态系统包括随机访问的 OLTP 数据库,如 HBase(请参阅 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)”)和 MPP 风格的分析型数据库,如 Impala 【41】。HBase 与 Impala 都不使用 MapReduce,但都使用 HDFS 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。 - -#### 针对频繁故障设计 - -当比较 MapReduce 和 MPP 数据库时,两种不同的设计思路出现了:处理故障和使用内存与磁盘的方式。与在线系统相比,批处理对故障不太敏感,因为就算失败也不会立即影响到用户,而且它们总是能再次运行。 - -如果一个节点在执行查询时崩溃,大多数 MPP 数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种错误处理的方法是可以接受的,因为重试的代价不是太大。MPP 数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的开销。 - -另一方面,MapReduce 可以容忍单个 Map 或 Reduce 任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也会非常急切地将数据写入磁盘,一方面是为了容错,另一部分是因为假设数据集太大而不能适应内存。 - -MapReduce 方式更适用于较大的作业:要处理如此之多的数据并运行很长时间的作业,以至于在此过程中很可能至少遇到一个任务故障。在这种情况下,由于单个任务失败而重新运行整个作业将是非常浪费的。即使以单个任务的粒度进行恢复引入了使得无故障处理更慢的开销,但如果任务失败率足够高,这仍然是一种合理的权衡。 - -但是这些假设有多么现实呢?在大多数集群中,机器故障确实会发生,但是它们不是很频繁 —— 可能少到绝大多数作业都不会经历机器故障。为了容错,真的值得带来这么大的额外开销吗? - -要了解 MapReduce 节约使用内存和在任务的层次进行恢复的原因,了解最初设计 MapReduce 的环境是很有帮助的。Google 有着混用的数据中心,在线生产服务和离线批处理作业在同样机器上运行。每个任务都有一个通过容器强制执行的资源配给(CPU 核心、RAM、磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的进程花费更多【59】。 - -这种架构允许非生产(低优先级)计算资源被 **过量使用(overcommitted)**,因为系统知道必要时它可以回收资源。与分离生产和非生产任务的系统相比,过量使用资源可以更好地利用机器并提高效率。但由于 MapReduce 作业以低优先级运行,它们随时都有被抢占的风险,因为优先级较高的进程可能需要其资源。在高优先级进程拿走所需资源后,批量作业能有效地 “捡面包屑”,利用剩下的任何计算资源。 - -在谷歌,运行一个小时的 MapReduce 任务有大约有 5% 的风险被终止,为了给更高优先级的进程挪地方。这一概率比硬件问题、机器重启或其他原因的概率高了一个数量级【59】。按照这种抢占率,如果一个作业有 100 个任务,每个任务运行 10 分钟,那么至少有一个任务在完成之前被终止的风险大于 50%。 - -这就是 MapReduce 被设计为容忍频繁意外任务终止的原因:不是因为硬件很不可靠,而是因为任意终止进程的自由有利于提高计算集群中的资源利用率。 - -在开源的集群调度器中,抢占的使用较少。YARN 的 CapacityScheduler 支持抢占,以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos 或 Kubernetes 不支持通用的优先级抢占【60】。在任务不经常被终止的环境中,MapReduce 的这一设计决策就没有多少意义了。在下一节中,我们将研究一些与 MapReduce 设计决策相异的替代方案。 - - -## MapReduce之后 - -虽然 MapReduce 在 2000 年代后期变得非常流行,并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。对于不同的数据量,数据结构和处理类型,其他工具可能更适合表示计算。 - - -不管如何,我们在这一章花了大把时间来讨论 MapReduce,因为它是一种有用的学习工具,它是分布式文件系统的一种相当简单明晰的抽象。在这里,**简单** 意味着我们能理解它在做什么,而不是意味着使用它很简单。恰恰相反:使用原始的 MapReduce API 来实现复杂的处理工作实际上是非常困难和费力的 —— 例如,任意一种连接算法都需要你从头开始实现【37】。 - -针对直接使用 MapReduce 的困难,在 MapReduce 上有很多高级编程模型(Pig、Hive、Cascading、Crunch)被创造出来,作为建立在 MapReduce 之上的抽象。如果你了解 MapReduce 的原理,那么它们学起来相当简单。而且它们的高级结构能显著简化许多常见批处理任务的实现。 - -但是,MapReduce 执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次而解决,而对于某些类型的处理,它表现得非常差劲。一方面,MapReduce 非常稳健:你可以使用它在任务会频繁终止的多租户系统上处理几乎任意大量级的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理而言,其他工具有时会快上几个数量级。 - -在本章的其余部分中,我们将介绍一些批处理方法。在 [第十二章](/ch12) 我们将转向流处理,它可以看作是加速批处理的另一种方法。 - -### 物化中间状态 - -如前所述,每个 MapReduce 作业都独立于其他任何作业。作业与世界其他地方的主要连接点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为第一个作业输出目录,且外部工作流调度程序必须在第一个作业完成后再启动第二个。 - -如果第一个作业的输出是要在组织内广泛发布的数据集,则这种配置是合理的。在这种情况下,你需要通过名称引用它,并将其重用为多个不同作业的输入(包括由其他团队开发的作业)。将数据发布到分布式文件系统中众所周知的位置能够带来 **松耦合**,这样作业就不需要知道是谁在提供输入或谁在消费输出(请参阅 “[逻辑与布线相分离](#逻辑与布线相分离)”)。 - -但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的 **中间状态(intermediate state)**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由 50 或 100 个 MapReduce 作业组成的复杂工作流中,存在着很多这样的中间状态【29】。 - -将这个中间状态写入文件的过程称为 **物化(materialization)**。(在 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算) - -作为对照,本章开头的日志分析示例使用 Unix 管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而是只使用一个小的内存缓冲区,将输出增量地 **流(stream)** 向输入。 - -与 Unix 管道相比,MapReduce 完全物化中间状态的方法存在不足之处: - -- MapReduce 作业只有在前驱作业(生成其输入)中的所有任务都完成时才能启动,而由 Unix 管道连接的进程会同时启动,输出一旦生成就会被消费。不同机器上的数据偏斜或负载不均意味着一个作业往往会有一些掉队的任务,比其他任务要慢得多才能完成。必须等待至前驱作业的所有任务完成,拖慢了整个工作流程的执行。 -- Mapper 通常是多余的:它们仅仅是读取刚刚由 Reducer 写入的同样文件,为下一个阶段的分区和排序做准备。在许多情况下,Mapper 代码可能是前驱 Reducer 的一部分:如果 Reducer 和 Mapper 的输出有着相同的分区与排序方式,那么 Reducer 就可以直接串在一起,而不用与 Mapper 相互交织。 -- 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,对这些临时数据这么搞就比较过分了。 - -#### 数据流引擎 - -为了解决 MapReduce 的这些问题,几种用于分布式批处理的新执行引擎被开发出来,其中最著名的是 Spark 【61,62】,Tez 【63,64】和 Flink 【65,66】。它们的设计方式有很多区别,但有一个共同点:把整个工作流作为单个作业来处理,而不是把它分解为独立的子作业。 - -由于它们将工作流显式建模为数据从几个处理阶段穿过,所以这些系统被称为 **数据流引擎(dataflow engines)**。像 MapReduce 一样,它们在一条线上通过反复调用用户定义的函数来一次处理一条记录,它们通过输入分区来并行化载荷,它们通过网络将一个函数的输出复制到另一个函数的输入。 - -与 MapReduce 不同,这些函数不需要严格扮演交织的 Map 与 Reduce 的角色,而是可以以更灵活的方式进行组合。我们称这些函数为 **算子(operators)**,数据流引擎提供了几种不同的选项来将一个算子的输出连接到另一个算子的输入: - -- 一种选项是对记录按键重新分区并排序,就像在 MapReduce 的混洗阶段一样(请参阅 “[分布式执行 MapReduce](#分布式执行MapReduce)”)。这种功能可以用于实现排序合并连接和分组,就像在 MapReduce 中一样。 -- 另一种可能是接受多个输入,并以相同的方式进行分区,但跳过排序。当记录的分区重要但顺序无关紧要时,这省去了分区散列连接的工作,因为构建散列表还是会把顺序随机打乱。 -- 对于广播散列连接,可以将一个算子的输出,发送到连接算子的所有分区。 - -这种类型的处理引擎是基于像 Dryad【67】和 Nephele【68】这样的研究系统,与 MapReduce 模型相比,它有几个优点: - -- 排序等昂贵的工作只需要在实际需要的地方执行,而不是默认地在每个 Map 和 Reduce 阶段之间出现。 -- 没有不必要的 Map 任务,因为 Mapper 所做的工作通常可以合并到前面的 Reduce 算子中(因为 Mapper 不会更改数据集的分区)。 -- 由于工作流中的所有连接和数据依赖都是显式声明的,因此调度程序能够总览全局,知道哪里需要哪些数据,因而能够利用局部性进行优化。例如,它可以尝试将消费某些数据的任务放在与生成这些数据的任务相同的机器上,从而数据可以通过共享内存缓冲区传输,而不必通过网络复制。 -- 通常,算子间的中间状态足以保存在内存中或写入本地磁盘,这比写入 HDFS 需要更少的 I/O(必须将其复制到多台机器,并将每个副本写入磁盘)。MapReduce 已经对 Mapper 的输出做了这种优化,但数据流引擎将这种思想推广至所有的中间状态。 -- 算子可以在输入就绪后立即开始执行;后续阶段无需等待前驱阶段整个完成后再开始。 -- 与 MapReduce(为每个任务启动一个新的 JVM)相比,现有 Java 虚拟机(JVM)进程可以重用来运行新算子,从而减少启动开销。 - -你可以使用数据流引擎执行与 MapReduce 工作流同样的计算,而且由于此处所述的优化,通常执行速度要明显快得多。既然算子是 Map 和 Reduce 的泛化,那么相同的处理代码就可以在任一执行引擎上运行:Pig,Hive 或 Cascading 中实现的工作流可以无需修改代码,可以通过修改配置,简单地从 MapReduce 切换到 Tez 或 Spark【64】。 - -Tez 是一个相当薄的库,它依赖于 YARN shuffle 服务来实现节点间数据的实际复制【58】,而 Spark 和 Flink 则是包含了独立网络通信层,调度器,及用户向 API 的大型框架。我们将简要讨论这些高级 API。 - -#### 容错 - -完全物化中间状态至分布式文件系统的一个优点是,它具有持久性,这使得 MapReduce 中的容错相当容易:如果一个任务失败,它可以在另一台机器上重新启动,并从文件系统重新读取相同的输入。 - -Spark、Flink 和 Tez 避免将中间状态写入 HDFS,因此它们采取了不同的方法来容错:如果一台机器发生故障,并且该机器上的中间状态丢失,则它会从其他仍然可用的数据重新计算(在可行的情况下是先前的中间状态,要么就只能是原始输入数据,通常在 HDFS 上)。 - -为了实现这种重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用了哪些输入分区?应用了哪些算子? Spark 使用 **弹性分布式数据集(RDD,Resilient Distributed Dataset)** 的抽象来跟踪数据的谱系【61】,而 Flink 对算子状态存档,允许恢复运行在执行过程中遇到错误的算子【66】。 - -在重新计算数据时,重要的是要知道计算是否是 **确定性的**:也就是说,给定相同的输入数据,算子是否始终产生相同的输出?如果一些丢失的数据已经发送给下游算子,这个问题就很重要。如果算子重新启动,重新计算的数据与原有的丢失数据不一致,下游算子很难解决新旧数据之间的矛盾。对于不确定性算子来说,解决方案通常是杀死下游算子,然后再重跑新数据。 - -为了避免这种级联故障,最好让算子具有确定性。但需要注意的是,非确定性行为很容易悄悄溜进来:例如,许多编程语言在迭代哈希表的元素时不能对顺序作出保证,许多概率和统计算法显式依赖于使用随机数,以及用到系统时钟或外部数据源,这些都是都不确定性的行为。为了能可靠地从故障中恢复,需要消除这种不确定性因素,例如使用固定的种子生成伪随机数。 - -通过重算数据来从故障中恢复并不总是正确的答案:如果中间状态数据要比源数据小得多,或者如果计算量非常大,那么将中间数据物化为文件可能要比重新计算廉价的多。 - -#### 关于物化的讨论 - -回到 Unix 的类比,我们看到,MapReduce 就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是 Unix 管道。尤其是 Flink 是基于管道执行的思想而建立的:也就是说,将算子的输出增量地传递给其他算子,不待输入完成便开始处理。 - -排序算子不可避免地需要消费全部的输入后才能生成任何输出,因为输入中最后一条输入记录可能具有最小的键,因此需要作为第一条记录输出。因此,任何需要排序的算子都需要至少暂时地累积状态。但是工作流的许多其他部分可以以流水线方式执行。 - -当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它 —— 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS 上的物化数据集通常仍是作业的输入和最终输出。和 MapReduce 一样,输入是不可变的,输出被完全替换。比起 MapReduce 的改进是,你不用再自己去将中间状态写入文件系统了。 - -### 图与迭代处理 - -在 “[图数据模型](/ch2#图数据模型)” 中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](/ch2) 的讨论集中在 OLTP 风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。 - -批处理上下文中的图也很有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是 PageRank 【69】,它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分,用于确定网络搜索引擎呈现结果的顺序。 - -> 像 Spark、Flink 和 Tez 这样的数据流引擎(请参阅 “[物化中间状态](#物化中间状态)”)通常将算子作为 **有向无环图(DAG)** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流** 被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱! - -许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在 [图 2-6](/v1/ddia_0206.png) 中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为 **传递闭包**,即 transitive closure)。 - -可以在分布式文件系统中存储图(包含顶点和边的列表的文件),但是这种 “重复至完成” 的想法不能用普通的 MapReduce 来表示,因为它只扫过一趟数据。这种算法因此经常以 **迭代** 的风格实现: - -1. 外部调度程序运行批处理来计算算法的一个步骤。 -2. 当批处理过程完成时,调度器检查它是否完成(基于完成条件 —— 例如,没有更多的边要跟进,或者与上次迭代相比的变化低于某个阈值)。 -3. 如果尚未完成,则调度程序返回到步骤 1 并运行另一轮批处理。 - -这种方法是有效的,但是用 MapReduce 实现它往往非常低效,因为 MapReduce 没有考虑算法的迭代性质:它总是读取整个输入数据集并产生一个全新的输出数据集,即使与上次迭代相比,改变的仅仅是图中的一小部分。 - -#### Pregel处理模型 - -针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)** 计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark 的 GraphX API 和 Flink 的 Gelly API 【71】实现了它。它也被称为 **Pregel** 模型,因为 Google 的 Pregel 论文推广了这种处理图的方法【72】。 - -回想一下在 MapReduce 中,Mapper 在概念上向 Reducer 的特定调用 “发送消息”,因为框架将所有具有相同键的 Mapper 输出集中在一起。Pregel 背后有一个类似的想法:一个顶点可以向另一个顶点 “发送消息”,通常这些消息是沿着图的边发送的。 - -在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用 Reducer 一样。与 MapReduce 的不同之处在于,在 Pregel 模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。 - -这与 Actor 模型有些相似(请参阅 “[分布式的 Actor 框架](/ch4#分布式的Actor框架)”),除了顶点状态和顶点之间的消息具有容错性和持久性,且通信以固定的回合进行:在每次迭代中,框架递送上次迭代中发送的所有消息。Actor 通常没有这样的时序保证。 - -#### 容错 - -顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高 Pregel 作业的性能,因为消息可以成批处理,且等待通信的次数也减少了。唯一的等待是在迭代之间:由于 Pregel 模型保证所有在一轮迭代中发送的消息都在下轮迭代中送达,所以在下一轮迭代开始前,先前的迭代必须完全完成,而所有的消息必须在网络上完成复制。 - -即使底层网络可能丢失、重复或任意延迟消息(请参阅 “[不可靠的网络](/ch8#不可靠的网络)”),Pregel 的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像 MapReduce 一样,框架能从故障中透明地恢复,以简化在 Pregel 上实现算法的编程模型。 - -这种容错是通过在迭代结束时,定期存档所有顶点的状态来实现的,即将其全部状态写入持久化存储。如果某个节点发生故障并且其内存中的状态丢失,则最简单的解决方法是将整个图计算回滚到上一个存档点,然后重启计算。如果算法是确定性的,且消息记录在日志中,那么也可以选择性地只恢复丢失的分区(就像之前讨论过的数据流引擎)【72】。 - -#### 并行执行 - -顶点不需要知道它在哪台物理机器上执行;当它向其他顶点发送消息时,它只是简单地将消息发往某个顶点 ID。图的分区取决于框架 —— 即,确定哪个顶点运行在哪台机器上,以及如何通过网络路由消息,以便它们到达正确的地方。 - -由于编程模型一次仅处理一个顶点(有时称为 “像顶点一样思考”),所以框架可以以任意方式对图分区。理想情况下如果顶点需要进行大量的通信,那么它们最好能被分区到同一台机器上。然而找到这样一种优化的分区方法是很困难的 —— 在实践中,图经常按照任意分配的顶点 ID 分区,而不会尝试将相关的顶点分组在一起。 - -因此,图算法通常会有很多跨机器通信的额外开销,而中间状态(节点之间发送的消息)往往比原始图大。通过网络发送消息的开销会显著拖慢分布式图算法的速度。 - -出于这个原因,如果你的图可以放入一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理【73,74】。图比内存大也没关系,只要能放入单台计算机的磁盘,使用 GraphChi 等框架进行单机处理是就一个可行的选择【75】。如果图太大,不适合单机处理,那么像 Pregel 这样的分布式方法是不可避免的。高效的并行图算法是一个进行中的研究领域【76】。 - - -### 高级API和语言 - -自 MapReduce 开始流行的这几年以来,分布式批处理的执行引擎已经很成熟了。到目前为止,基础设施已经足够强大,能够存储和处理超过 10,000 台机器集群上的数 PB 的数据。由于在这种规模下物理执行批处理的问题已经被认为或多或少解决了,所以关注点已经转向其他领域:改进编程模型,提高处理效率,扩大这些技术可以解决的问题集。 - -如前所述,Hive、Pig、Cascading 和 Crunch 等高级语言和 API 变得越来越流行,因为手写 MapReduce 作业实在是个苦力活。随着 Tez 的出现,这些高级语言还有一个额外好处,可以迁移到新的数据流执行引擎,而无需重写作业代码。Spark 和 Flink 也有它们自己的高级数据流 API,通常是从 FlumeJava 中获取的灵感【34】。 - -这些数据流 API 通常使用关系型构建块来表达一个计算:按某个字段连接数据集;按键对元组做分组;按某些条件过滤;并通过计数求和或其他函数来聚合元组。在内部,这些操作是使用本章前面讨论过的各种连接和分组算法来实现的。 - -除了少写代码的明显优势之外,这些高级接口还支持交互式用法,在这种交互式使用中,你可以在 Shell 中增量式编写分析代码,频繁运行来观察它做了什么。这种开发风格在探索数据集和试验处理方法时非常有用。这也让人联想到 Unix 哲学,我们在 “[Unix 哲学](#Unix哲学)” 中讨论过这个问题。 - -此外,这些高级接口不仅提高了人类的工作效率,也提高了机器层面的作业执行效率。 - -#### 向声明式查询语言的转变 - -与硬写执行连接的代码相比,指定连接关系算子的优点是,框架可以分析连接输入的属性,并自动决定哪种上述连接算法最适合当前任务。Hive、Spark 和 Flink 都有基于代价的查询优化器可以做到这一点,甚至可以改变连接顺序,最小化中间状态的数量【66,77,78,79】。 - -连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以 **声明式(declarative)** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在 “[数据查询语言](/ch2#数据查询语言)” 中见过这个想法。 - -但 MapReduce 及其数据流后继者在其他方面,与 SQL 的完全声明式查询模型有很大区别。MapReduce 是围绕着回调函数的概念建立的:对于每条记录或者一组记录,调用一个用户定义的函数(Mapper 或 Reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作:解析、自然语言分析、图像分析以及运行数值或统计算法等。 - -自由运行任意代码,长期以来都是传统 MapReduce 批处理系统与 MPP 数据库的区别所在(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)” 一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems)。 - -然而数据流引擎已经发现,支持除连接之外的更多 **声明式特性** 还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外 CPU 开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅 “[列式存储](/ch3#列式存储)”),只从磁盘读取所需的列。Hive、Spark DataFrames 和 Impala 还使用了向量化执行(请参阅 “[内存带宽和矢量化处理](/ch3#内存带宽和矢量化处理)”):在对 CPU 缓存友好的内部循环中迭代数据,避免函数调用。Spark 生成 JVM 字节码【79】,Impala 使用 LLVM 为这些内部循环生成本机代码【41】。 - -通过在高级 API 中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像 MPP 数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。 - -#### 专业化的不同领域 - -尽管能够运行任意代码的可扩展性是很有用的,但是也有很多常见的例子,不断重复着标准的处理模式。因而这些模式值得拥有自己的可重用通用构建模块实现。传统上,MPP 数据库满足了商业智能分析和业务报表的需求,但这只是许多使用批处理的领域之一。 - -另一个越来越重要的领域是统计和数值算法,它们是机器学习应用所需要的(例如分类器和推荐系统)。可重用的实现正在出现:例如,Mahout 在 MapReduce、Spark 和 Flink 之上实现了用于机器学习的各种算法,而 MADlib 在关系型 MPP 数据库(Apache HAWQ)中实现了类似的功能【54】。 - -空间算法也是有用的,例如 **k 近邻搜索(k-nearest neighbors, kNN)**【80】,它在一些多维空间中搜索与给定项最近的项目 —— 这是一种相似性搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串【81】。 - -批处理引擎正被用于分布式执行日益广泛的各领域算法。随着批处理系统获得各种内置功能以及高级声明式算子,且随着 MPP 数据库变得更加灵活和易于编程,两者开始看起来相似了:最终,它们都只是存储和处理数据的系统。 - - -## 本章小结 - -在本章中,我们探索了批处理的主题。我们首先看到了诸如 awk、grep 和 sort 之类的 Unix 工具,然后我们看到了这些工具的设计理念是如何应用到 MapReduce 和更近的数据流引擎中的。一些设计原则包括:输入是不可变的,输出是为了作为另一个(仍未知的)程序的输入,而复杂的问题是通过编写 “做好一件事” 的小工具来解决的。 - -在 Unix 世界中,允许程序与程序组合的统一接口是文件与管道;在 MapReduce 中,该接口是一个分布式文件系统。我们看到数据流引擎添加了自己的管道式数据传输机制,以避免将中间状态物化至分布式文件系统,但作业的初始输入和最终输出通常仍是 HDFS。 - -分布式批处理框架需要解决的两个主要问题是: - -分区 -: 在 MapReduce 中,Mapper 根据输入文件块进行分区。Mapper 的输出被重新分区、排序并合并到可配置数量的 Reducer 分区中。这一过程的目的是把所有的 **相关** 数据(例如带有相同键的所有记录)都放在同一个地方。 - 后 MapReduce 时代的数据流引擎若非必要会尽量避免排序,但它们也采取了大致类似的分区方法。 - -容错 -: MapReduce 经常写入磁盘,这使得从单个失败的任务恢复很轻松,无需重新启动整个作业,但在无故障的情况下减慢了执行速度。数据流引擎更多地将中间状态保存在内存中,更少地物化中间状态,这意味着如果节点发生故障,则需要重算更多的数据。确定性算子减少了需要重算的数据量。 - - -我们讨论了几种 MapReduce 的连接算法,其中大多数也在 MPP 数据库和数据流引擎内部使用。它们也很好地演示了分区算法是如何工作的: - -排序合并连接 -: 每个参与连接的输入都通过一个提取连接键的 Mapper。通过分区、排序和合并,具有相同键的所有记录最终都会进入相同的 Reducer 调用。这个函数能输出连接好的记录。 - -广播散列连接 -: 两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个 Mapper,将输入小端的散列表加载到每个 Mapper 中,然后扫描大端,一次一条记录,并为每条记录查询散列表。 - -分区散列连接 -: 如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地对每个分区应用散列表方法。 - -分布式批处理引擎有一个刻意限制的编程模型:回调函数(比如 Mapper 和 Reducer)被假定是无状态的,而且除了指定的输出外,必须没有任何外部可见的副作用。这一限制允许框架在其抽象下隐藏一些困难的分布式系统问题:当遇到崩溃和网络问题时,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则其中只有一个能使其输出实际可见。 - -得益于这个框架,你在批处理作业中的代码无需操心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,虽然实际上也许不得不重试各种任务。比起在线服务一边处理用户请求一边将写入数据库作为处理请求的副作用,批处理提供的这种可靠性语义要强得多。 - -批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入 —— 换句话说,输出是从输入派生出的。最关键的是,输入数据是 **有界的(bounded)**:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。 - -在下一章中,我们将转向流处理,其中的输入是 **无界的(unbounded)** —— 也就是说,你还有活儿要干,然而它的输入是永无止境的数据流。在这种情况下,作业永无完成之日。因为在任何时候都可能有更多的工作涌入。我们将看到,在某些方面上,流处理和批处理是相似的。但是关于无尽数据流的假设也对我们构建系统的方式产生了很多改变。 - - -## 参考文献 - -1. Jeffrey Dean and Sanjay Ghemawat: “[MapReduce: Simplified Data Processing on Large Clusters](https://research.google/pubs/pub62/),” at *6th USENIX Symposium on Operating System Design and Implementation* (OSDI), December 2004. -1. Joel Spolsky: “[The Perils of JavaSchools](https://www.joelonsoftware.com/2005/12/29/the-perils-of-javaschools-2/),” *joelonsoftware.com*, December 29, 2005. -1. Shivnath Babu and Herodotos Herodotou: “[Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf),” *Foundations and Trends in Databases*, volume 5, number 1, pages 1–104, November 2013. [doi:10.1561/1900000036](http://dx.doi.org/10.1561/1900000036) -1. David J. DeWitt and Michael Stonebraker: “[MapReduce: A Major Step Backwards](https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html),” originally published at *databasecolumn.vertica.com*, January 17, 2008. -1. Henry Robinson: “[The Elephant Was a Trojan Horse: On the Death of Map-Reduce at Google](https://www.the-paper-trail.org/post/2014-06-25-the-elephant-was-a-trojan-horse-on-the-death-of-map-reduce-at-google/),” *the-paper-trail.org*, June 25, 2014. -1. “[The Hollerith Machine](https://www.census.gov/history/www/innovations/technology/the_hollerith_tabulator.html),” United States Census Bureau, *census.gov*. -1. “[IBM 82, 83, and 84 Sorters Reference Manual](https://bitsavers.org/pdf/ibm/punchedCard/Sorter/A24-1034-1_82-83-84_sorters.pdf),” Edition A24-1034-1, International Business Machines Corporation, July 1962. -1. Adam Drake: “[Command-Line Tools Can Be 235x Faster than Your Hadoop Cluster](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html),” *aadrake.com*, January 25, 2014. -1. “[GNU Coreutils 8.23 Documentation](http://www.gnu.org/software/coreutils/manual/html_node/index.html),” Free Software Foundation, Inc., 2014. -1. Martin Kleppmann: “[Kafka, Samza, and the Unix Philosophy of Distributed Data](http://martin.kleppmann.com/2015/08/05/kafka-samza-unix-philosophy-distributed-data.html),” *martin.kleppmann.com*, August 5, 2015. -1. Doug McIlroy: [Internal Bell Labs memo](https://swtch.com/~rsc/thread/mdmpipe.pdf), October 1964. Cited in: Dennis M. Richie: “[Advice from Doug McIlroy](https://www.bell-labs.com/usr/dmr/www/mdmpipe.html),” *bell-labs.com*. -1. M. D. McIlroy, E. N. Pinson, and B. A. Tague: “[UNIX Time-Sharing System: Foreword](https://archive.org/details/bstj57-6-1899),” *The Bell System Technical Journal*, volume 57, number 6, pages 1899–1904, July 1978. -1. Eric S. Raymond: [*The Art of UNIX Programming*](http://www.catb.org/~esr/writings/taoup/html/). Addison-Wesley, 2003. ISBN: 978-0-13-142901-7 -1. Ronald Duncan: “[Text File Formats – ASCII Delimited Text – Not CSV or TAB Delimited Text](https://ronaldduncan.wordpress.com/2009/10/31/text-file-formats-ascii-delimited-text-not-csv-or-tab-delimited-text/),” *ronaldduncan.wordpress.com*, October 31, 2009. -1. Alan Kay: “[Is 'Software Engineering' an Oxymoron?](http://tinlizzie.org/~takashi/IsSoftwareEngineeringAnOxymoron.pdf),” *tinlizzie.org*. -1. Martin Fowler: “[InversionOfControl](http://martinfowler.com/bliki/InversionOfControl.html),” *martinfowler.com*, June 26, 2005. -1. Daniel J. Bernstein: “[Two File Descriptors for Sockets](http://cr.yp.to/tcpip/twofd.html),” *cr.yp.to*. -1. Rob Pike and Dennis M. Ritchie: “[The Styx Architecture for Distributed Systems](http://doc.cat-v.org/inferno/4th_edition/styx),” *Bell Labs Technical Journal*, volume 4, number 2, pages 146–152, April 1999. -1. Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung: “[The Google File System](http://research.google.com/archive/gfs-sosp2003.pdf),” at *19th ACM Symposium on Operating Systems Principles* (SOSP), October 2003. [doi:10.1145/945445.945450](http://dx.doi.org/10.1145/945445.945450) -1. Michael Ovsiannikov, Silvius Rus, Damian Reeves, et al.: “[The Quantcast File System](http://db.disi.unitn.eu/pages/VLDBProgram/pdf/industry/p808-ovsiannikov.pdf),” *Proceedings of the VLDB Endowment*, volume 6, number 11, pages 1092–1101, August 2013. [doi:10.14778/2536222.2536234](http://dx.doi.org/10.14778/2536222.2536234) -1. “[OpenStack Swift 2.6.1 Developer Documentation](http://docs.openstack.org/developer/swift/),” OpenStack Foundation, *docs.openstack.org*, March 2016. -1. Zhe Zhang, Andrew Wang, Kai Zheng, et al.: “[Introduction to HDFS Erasure Coding in Apache Hadoop](https://blog.cloudera.com/introduction-to-hdfs-erasure-coding-in-apache-hadoop/),” *blog.cloudera.com*, September 23, 2015. -1. Peter Cnudde: “[Hadoop Turns 10](https://web.archive.org/web/20190119112713/https://yahoohadoop.tumblr.com/post/138739227316/hadoop-turns-10),” *yahoohadoop.tumblr.com*, February 5, 2016. -1. Eric Baldeschwieler: “[Thinking About the HDFS vs. Other Storage Technologies](https://web.archive.org/web/20190529215115/http://hortonworks.com/blog/thinking-about-the-hdfs-vs-other-storage-technologies/),” *hortonworks.com*, July 25, 2012. -1. Brendan Gregg: “[Manta: Unix Meets Map Reduce](https://web.archive.org/web/20220125052545/http://dtrace.org/blogs/brendan/2013/06/25/manta-unix-meets-map-reduce/),” *dtrace.org*, June 25, 2013. -1. Tom White: *Hadoop: The Definitive Guide*, 4th edition. O'Reilly Media, 2015. ISBN: 978-1-491-90163-2 -1. Jim N. Gray: “[Distributed Computing Economics](http://arxiv.org/pdf/cs/0403019.pdf),” Microsoft Research Tech Report MSR-TR-2003-24, March 2003. -1. Márton Trencséni: “[Luigi vs Airflow vs Pinball](http://bytepawn.com/luigi-airflow-pinball.html),” *bytepawn.com*, February 6, 2016. -1. Roshan Sumbaly, Jay Kreps, and Sam Shah: “[The 'Big Data' Ecosystem at LinkedIn](http://www.slideshare.net/s_shah/the-big-data-ecosystem-at-linkedin-23512853),” at *ACM International Conference on Management of Data* (SIGMOD), July 2013. [doi:10.1145/2463676.2463707](http://dx.doi.org/10.1145/2463676.2463707) -1. Alan F. Gates, Olga Natkovich, Shubham Chopra, et al.: “[Building a High-Level Dataflow System on Top of Map-Reduce: The Pig Experience](http://www.vldb.org/pvldb/vol2/vldb09-1074.pdf),” at *35th International Conference on Very Large Data Bases* (VLDB), August 2009. -1. Ashish Thusoo, Joydeep Sen Sarma, Namit Jain, et al.: “[Hive – A Petabyte Scale Data Warehouse Using Hadoop](http://i.stanford.edu/~ragho/hive-icde2010.pdf),” at *26th IEEE International Conference on Data Engineering* (ICDE), March 2010. [doi:10.1109/ICDE.2010.5447738](http://dx.doi.org/10.1109/ICDE.2010.5447738) -1. “[Cascading 3.0 User Guide](https://web.archive.org/web/20231206195311/http://docs.cascading.org/cascading/3.0/userguide/),” Concurrent, Inc., *docs.cascading.org*, January 2016. -1. “[Apache Crunch User Guide](https://crunch.apache.org/user-guide.html),” Apache Software Foundation, *crunch.apache.org*. -1. Craig Chambers, Ashish Raniwala, Frances Perry, et al.: “[FlumeJava: Easy, Efficient Data-Parallel Pipelines](https://research.google.com/pubs/archive/35650.pdf),” at *31st ACM SIGPLAN Conference on Programming Language Design and Implementation* (PLDI), June 2010. [doi:10.1145/1806596.1806638](http://dx.doi.org/10.1145/1806596.1806638) -1. Jay Kreps: “[Why Local State is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing),” *oreilly.com*, July 31, 2014. -1. Martin Kleppmann: “[Rethinking Caching in Web Apps](http://martin.kleppmann.com/2012/10/01/rethinking-caching-in-web-apps.html),” *martin.kleppmann.com*, October 1, 2012. -1. Mark Grover, Ted Malaska, Jonathan Seidman, and Gwen Shapira: *[Hadoop Application Architectures](http://shop.oreilly.com/product/0636920033196.do)*. O'Reilly Media, 2015. ISBN: 978-1-491-90004-8 -1. Philippe Ajoux, Nathan Bronson, Sanjeev Kumar, et al.: “[Challenges to Adopting Stronger Consistency at Scale](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015. -1. Sriranjan Manjunath: “[Skewed Join](https://web.archive.org/web/20151228114742/https://wiki.apache.org/pig/PigSkewedJoinSpec),” *wiki.apache.org*, 2009. -1. David J. DeWitt, Jeffrey F. Naughton, Donovan A. Schneider, and S. Seshadri: “[Practical Skew Handling in Parallel Joins](http://www.vldb.org/conf/1992/P027.PDF),” at *18th International Conference on Very Large Data Bases* (VLDB), August 1992. -1. Marcel Kornacker, Alexander Behm, Victor Bittorf, et al.: “[Impala: A Modern, Open-Source SQL Engine for Hadoop](http://pandis.net/resources/cidr15impala.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015. -1. Matthieu Monsch: “[Open-Sourcing PalDB, a Lightweight Companion for Storing Side Data](https://engineering.linkedin.com/blog/2015/10/open-sourcing-paldb--a-lightweight-companion-for-storing-side-da),” *engineering.linkedin.com*, October 26, 2015. -1. Daniel Peng and Frank Dabek: “[Large-Scale Incremental Processing Using Distributed Transactions and Notifications](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf),” at *9th USENIX conference on Operating Systems Design and Implementation* (OSDI), October 2010. -1. “["Cloudera Search User Guide,"](http://www.cloudera.com/documentation/cdh/5-1-x/Search/Cloudera-Search-User-Guide/Cloudera-Search-User-Guide.html) Cloudera, Inc., September 2015. -1. Lili Wu, Sam Shah, Sean Choi, et al.: “[The Browsemaps: Collaborative Filtering at LinkedIn](http://ceur-ws.org/Vol-1271/Paper3.pdf),” at *6th Workshop on Recommender Systems and the Social Web* (RSWeb), October 2014. -1. Roshan Sumbaly, Jay Kreps, Lei Gao, et al.: “[Serving Large-Scale Batch Computed Data with Project Voldemort](http://static.usenix.org/events/fast12/tech/full_papers/Sumbaly.pdf),” at *10th USENIX Conference on File and Storage Technologies* (FAST), February 2012. -1. Varun Sharma: “[Open-Sourcing Terrapin: A Serving System for Batch Generated Data](https://web.archive.org/web/20170215032514/https://engineering.pinterest.com/blog/open-sourcing-terrapin-serving-system-batch-generated-data-0),” *engineering.pinterest.com*, September 14, 2015. -1. Nathan Marz: “[ElephantDB](http://www.slideshare.net/nathanmarz/elephantdb),” *slideshare.net*, May 30, 2011. -1. Jean-Daniel (JD) Cryans: “[How-to: Use HBase Bulk Loading, and Why](https://blog.cloudera.com/how-to-use-hbase-bulk-loading-and-why/),” *blog.cloudera.com*, September 27, 2013. -1. Nathan Marz: “[How to Beat the CAP Theorem](http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html),” *nathanmarz.com*, October 13, 2011. -1. Molly Bartlett Dishman and Martin Fowler: “[Agile Architecture](https://web.archive.org/web/20161130034721/http://conferences.oreilly.com/software-architecture/sa2015/public/schedule/detail/40388),” at *O'Reilly Software Architecture Conference*, March 2015. -1. David J. DeWitt and Jim N. Gray: “[Parallel Database Systems: The Future of High Performance Database Systems](http://www.cs.cmu.edu/~pavlo/courses/fall2013/static/papers/dewittgray92.pdf),” *Communications of the ACM*, volume 35, number 6, pages 85–98, June 1992. [doi:10.1145/129888.129894](http://dx.doi.org/10.1145/129888.129894) -1. Jay Kreps: “[But the multi-tenancy thing is actually really really hard](https://twitter.com/jaykreps/status/528235702480142336),” tweetstorm, *twitter.com*, October 31, 2014. -1. Jeffrey Cohen, Brian Dolan, Mark Dunlap, et al.: “[MAD Skills: New Analysis Practices for Big Data](http://www.vldb.org/pvldb/vol2/vldb09-219.pdf),” *Proceedings of the VLDB Endowment*, volume 2, number 2, pages 1481–1492, August 2009. [doi:10.14778/1687553.1687576](http://dx.doi.org/10.14778/1687553.1687576) -1. Ignacio Terrizzano, Peter Schwarz, Mary Roth, and John E. Colino: “[Data Wrangling: The Challenging Journey from the Wild to the Lake](http://cidrdb.org/cidr2015/Papers/CIDR15_Paper2.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015. -1. Paige Roberts: “[To Schema on Read or to Schema on Write, That Is the Hadoop Data Lake Question](https://web.archive.org/web/20171105001306/http://adaptivesystemsinc.com/blog/to-schema-on-read-or-to-schema-on-write-that-is-the-hadoop-data-lake-question/),” *adaptivesystemsinc.com*, July 2, 2015. -1. Bobby Johnson and Joseph Adler: “[The Sushi Principle: Raw Data Is Better](https://web.archive.org/web/20161126104941/https://conferences.oreilly.com/strata/big-data-conference-ca-2015/public/schedule/detail/38737),” at *Strata+Hadoop World*, February 2015. -1. Vinod Kumar Vavilapalli, Arun C. Murthy, Chris Douglas, et al.: “[Apache Hadoop YARN: Yet Another Resource Negotiator](https://www.cs.cmu.edu/~garth/15719/papers/yarn.pdf),” at *4th ACM Symposium on Cloud Computing* (SoCC), October 2013. [doi:10.1145/2523616.2523633](http://dx.doi.org/10.1145/2523616.2523633) -1. Abhishek Verma, Luis Pedrosa, Madhukar Korupolu, et al.: “[Large-Scale Cluster Management at Google with Borg](http://research.google.com/pubs/pub43438.html),” at *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741964](http://dx.doi.org/10.1145/2741948.2741964) -1. Malte Schwarzkopf: “[The Evolution of Cluster Scheduler Architectures](https://web.archive.org/web/20201109052657/http://www.firmament.io/blog/scheduler-architectures.html),” *firmament.io*, March 9, 2016. -1. Matei Zaharia, Mosharaf Chowdhury, Tathagata Das, et al.: “[Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing](https://www.usenix.org/system/files/conference/nsdi12/nsdi12-final138.pdf),” at *9th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), April 2012. -1. Holden Karau, Andy Konwinski, Patrick Wendell, and Matei Zaharia: *Learning Spark*. O'Reilly Media, 2015. ISBN: 978-1-449-35904-1 -1. Bikas Saha and Hitesh Shah: “[Apache Tez: Accelerating Hadoop Query Processing](http://www.slideshare.net/Hadoop_Summit/w-1205phall1saha),” at *Hadoop Summit*, June 2014. -1. Bikas Saha, Hitesh Shah, Siddharth Seth, et al.: “[Apache Tez: A Unifying Framework for Modeling and Building Data Processing Applications](http://home.cse.ust.hk/~weiwa/teaching/Fall15-COMP6611B/reading_list/Tez.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742790](http://dx.doi.org/10.1145/2723372.2742790) -1. Kostas Tzoumas: “[Apache Flink: API, Runtime, and Project Roadmap](http://www.slideshare.net/KostasTzoumas/apache-flink-api-runtime-and-project-roadmap),” *slideshare.net*, January 14, 2015. -1. Alexander Alexandrov, Rico Bergmann, Stephan Ewen, et al.: “[The Stratosphere Platform for Big Data Analytics](https://ssc.io/pdf/2014-VLDBJ_Stratosphere_Overview.pdf),” *The VLDB Journal*, volume 23, number 6, pages 939–964, May 2014. [doi:10.1007/s00778-014-0357-y](http://dx.doi.org/10.1007/s00778-014-0357-y) -1. Michael Isard, Mihai Budiu, Yuan Yu, et al.: “[Dryad: Distributed Data-Parallel Programs from Sequential Building Blocks](https://www.microsoft.com/en-us/research/publication/dryad-distributed-data-parallel-programs-from-sequential-building-blocks/),” at *European Conference on Computer Systems* (EuroSys), March 2007. [doi:10.1145/1272996.1273005](http://dx.doi.org/10.1145/1272996.1273005) -1. Daniel Warneke and Odej Kao: “[Nephele: Efficient Parallel Data Processing in the Cloud](https://stratosphere2.dima.tu-berlin.de/assets/papers/Nephele_09.pdf),” at *2nd Workshop on Many-Task Computing on Grids and Supercomputers* (MTAGS), November 2009. [doi:10.1145/1646468.1646476](http://dx.doi.org/10.1145/1646468.1646476) -1. Lawrence Page, Sergey Brin, Rajeev Motwani, and Terry Winograd: “[The PageRank Citation Ranking: Bringing Order to the Web](https://web.archive.org/web/20230219170930/http://ilpubs.stanford.edu:8090/422/),” Stanford InfoLab Technical Report 422, 1999. -1. Leslie G. Valiant: “[A Bridging Model for Parallel Computation](http://dl.acm.org/citation.cfm?id=79181),” *Communications of the ACM*, volume 33, number 8, pages 103–111, August 1990. [doi:10.1145/79173.79181](http://dx.doi.org/10.1145/79173.79181) -1. Stephan Ewen, Kostas Tzoumas, Moritz Kaufmann, and Volker Markl: “[Spinning Fast Iterative Data Flows](http://vldb.org/pvldb/vol5/p1268_stephanewen_vldb2012.pdf),” *Proceedings of the VLDB Endowment*, volume 5, number 11, pages 1268-1279, July 2012. [doi:10.14778/2350229.2350245](http://dx.doi.org/10.14778/2350229.2350245) -1. Grzegorz Malewicz, Matthew H. Austern, Aart J. C. Bik, et al.: “[Pregel: A System for Large-Scale Graph Processing](https://kowshik.github.io/JPregel/pregel_paper.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2010. [doi:10.1145/1807167.1807184](http://dx.doi.org/10.1145/1807167.1807184) -1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015. -1. Ionel Gog, Malte Schwarzkopf, Natacha Crooks, et al.: “[Musketeer: All for One, One for All in Data Processing Systems](http://www.cl.cam.ac.uk/research/srg/netos/camsas/pubs/eurosys15-musketeer.pdf),” at *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741968](http://dx.doi.org/10.1145/2741948.2741968) -1. Aapo Kyrola, Guy Blelloch, and Carlos Guestrin: “[GraphChi: Large-Scale Graph Computation on Just a PC](https://www.usenix.org/system/files/conference/osdi12/osdi12-final-126.pdf),” at *10th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2012. -1. Andrew Lenharth, Donald Nguyen, and Keshav Pingali: “[Parallel Graph Analytics](http://cacm.acm.org/magazines/2016/5/201591-parallel-graph-analytics/fulltext),” *Communications of the ACM*, volume 59, number 5, pages 78–87, May 2016. [doi:10.1145/2901919](http://dx.doi.org/10.1145/2901919) -1. Fabian Hüske: “[Peeking into Apache Flink's Engine Room](http://flink.apache.org/news/2015/03/13/peeking-into-Apache-Flinks-Engine-Room.html),” *flink.apache.org*, March 13, 2015. -1. Mostafa Mokhtar: “[Hive 0.14 Cost Based Optimizer (CBO) Technical Overview](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/),” *hortonworks.com*, March 2, 2015. -1. Michael Armbrust, Reynold S Xin, Cheng Lian, et al.: “[Spark SQL: Relational Data Processing in Spark](http://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](http://dx.doi.org/10.1145/2723372.2742797) -1. Daniel Blazevski: “[Planting Quadtrees for Apache Flink](https://blog.insightdatascience.com/planting-quadtrees-for-apache-flink-b396ebc80d35),” *insightdataengineering.com*, March 25, 2016. -1. Tom White: “[Genome Analysis Toolkit: Now Using Apache Spark for Data Processing](https://web.archive.org/web/20190215132904/http://blog.cloudera.com/blog/2016/04/genome-analysis-toolkit-now-using-apache-spark-for-data-processing/),” *blog.cloudera.com*, April 6, 2016. +[^1]: Nathan Marz. [How to Beat the CAP Theorem](http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html). *nathanmarz.com*, October 2011. Archived at [perma.cc/4BS9-R9A4](https://perma.cc/4BS9-R9A4) +[^2]: Molly Bartlett Dishman and Martin Fowler. [Agile Architecture](https://www.youtube.com/watch?v=VjKYO6DP3fo&list=PL055Epbe6d5aFJdvWNtTeg_UEHZEHdInE). At *O'Reilly Software Architecture Conference*, March 2015. +[^3]: Jeffrey Dean and Sanjay Ghemawat. [MapReduce: Simplified Data Processing on Large Clusters](https://www.usenix.org/legacy/publications/library/proceedings/osdi04/tech/full_papers/dean/dean.pdf). At *6th USENIX Symposium on Operating System Design and Implementation* (OSDI), December 2004. +[^4]: Shivnath Babu and Herodotos Herodotou. [Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf). *Foundations and Trends in Databases*, volume 5, issue 1, pages 1--104, November 2013. [doi:10.1561/1900000036](https://doi.org/10.1561/1900000036) +[^5]: David J. DeWitt and Michael Stonebraker. [MapReduce: A Major Step Backwards](https://homes.cs.washington.edu/~billhowe/mapreduce_a_major_step_backwards.html). Originally published at *databasecolumn.vertica.com*, January 2008. Archived at [perma.cc/U8PA-K48V](https://perma.cc/U8PA-K48V) +[^6]: Henry Robinson. [The Elephant Was a Trojan Horse: On the Death of Map-Reduce at Google](https://www.the-paper-trail.org/post/2014-06-25-the-elephant-was-a-trojan-horse-on-the-death-of-map-reduce-at-google/). *the-paper-trail.org*, June 2014. Archived at [perma.cc/9FEM-X787](https://perma.cc/9FEM-X787) +[^7]: Urs Hölzle. [R.I.P. MapReduce. After having served us well since 2003, today we removed the remaining internal codebase for good](https://twitter.com/uhoelzle/status/1177360023976067077). *twitter.com*, September 2019. Archived at [perma.cc/B34T-LLY7](https://perma.cc/B34T-LLY7) +[^8]: Adam Drake. [Command-Line Tools Can Be 235x Faster than Your Hadoop Cluster](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html). *aadrake.com*, January 2014. Archived at [perma.cc/87SP-ZMCY](https://perma.cc/87SP-ZMCY) +[^9]: [`sort`: Sort text files](https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html). GNU Coreutils 9.7 Documentation, Free Software Foundation, Inc., 2025. +[^10]: Michael Ovsiannikov, Silvius Rus, Damian Reeves, Paul Sutter, Sriram Rao, and Jim Kelly. [The Quantcast File System](https://db.disi.unitn.eu/pages/VLDBProgram/pdf/industry/p808-ovsiannikov.pdf). *Proceedings of the VLDB Endowment*, volume 6, issue 11, pages 1092--1101, August 2013. [doi:10.14778/2536222.2536234](https://doi.org/10.14778/2536222.2536234) +[^11]: Andrew Wang, Zhe Zhang, Kai Zheng, Uma Maheswara G., and Vinayakumar B. [Introduction to HDFS Erasure Coding in Apache Hadoop](https://www.cloudera.com/blog/technical/introduction-to-hdfs-erasure-coding-in-apache-hadoop.html). *blog.cloudera.com*, September 2015. Archived at [archive.org](https://web.archive.org/web/20250731115546/https://www.cloudera.com/blog/technical/introduction-to-hdfs-erasure-coding-in-apache-hadoop.html) +[^12]: Andy Warfield. [Building and operating a pretty big storage system called S3](https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html). *allthingsdistributed.com*, July 2023. Archived at [perma.cc/7LPK-TP7V](https://perma.cc/7LPK-TP7V) +[^13]: Vinod Kumar Vavilapalli, Arun C. Murthy, Chris Douglas, Sharad Agarwal, Mahadev Konar, Robert Evans, Thomas Graves, Jason Lowe, Hitesh Shah, Siddharth Seth, Bikas Saha, Carlo Curino, Owen O'Malley, Sanjay Radia, Benjamin Reed, and Eric Baldeschwieler. [Apache Hadoop YARN: Yet Another Resource Negotiator](https://opencourse.inf.ed.ac.uk/sites/default/files/2023-10/yarn-socc13.pdf). At *4th Annual Symposium on Cloud Computing* (SoCC), October 2013. [doi:10.1145/2523616.2523633](https://doi.org/10.1145/2523616.2523633) +[^14]: Richard M. Karp. [Reducibility Among Combinatorial Problems](https://www.cs.purdue.edu/homes/hosking/197/canon/karp.pdf). *Complexity of Computer Computations. The IBM Research Symposia Series*. Springer, 1972. [doi:10.1007/978-1-4684-2001-2_9](https://doi.org/10.1007/978-1-4684-2001-2_9) +[^15]: J. D. Ullman. [NP-Complete Scheduling Problems](https://www.cs.montana.edu/bhz/classes/fall-2018/csci460/paper4.pdf). *Journal of Computer and System Sciences*, volume 10, issue 3, June 1975. [doi:10.1016/S0022-0000(75)80008-0](https://doi.org/10.1016/S0022-0000(75)80008-0) +[^16]: Gilad David Maayan. [The complete guide to spot instances on AWS, Azure and GCP](https://www.datacenterdynamics.com/en/opinions/complete-guide-spot-instances-aws-azure-and-gcp/). *datacenterdynamics.com*, March 2021. Archived at [archive.org](https://web.archive.org/web/20250722114617/https://www.datacenterdynamics.com/en/opinions/complete-guide-spot-instances-aws-azure-and-gcp/) +[^17]: Abhishek Verma, Luis Pedrosa, Madhukar Korupolu, David Oppenheimer, Eric Tune, and John Wilkes. [Large-Scale Cluster Management at Google with Borg](https://dl.acm.org/doi/pdf/10.1145/2741948.2741964). At *10th European Conference on Computer Systems* (EuroSys), April 2015. [doi:10.1145/2741948.2741964](https://doi.org/10.1145/2741948.2741964) +[^18]: Matei Zaharia, Mosharaf Chowdhury, Tathagata Das, Ankur Dave, Justin Ma, Murphy McCauley, Michael J. Franklin, Scott Shenker, and Ion Stoica. [Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing](https://www.usenix.org/system/files/conference/nsdi12/nsdi12-final138.pdf). At *9th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), April 2012. +[^19]: Paris Carbone, Stephan Ewen, Seif Haridi, Asterios Katsifodimos, Volker Markl, and Kostas Tzoumas. [Apache Flink™: Stream and Batch Processing in a Single Engine](http://sites.computer.org/debull/A15dec/p28.pdf). *Bulletin of the IEEE Computer Society Technical Committee on Data Engineering*, volume 38, issue 4, December 2015. Archived at [perma.cc/G3N3-BKX5](https://perma.cc/G3N3-BKX5) +[^20]: Mark Grover, Ted Malaska, Jonathan Seidman, and Gwen Shapira. *[Hadoop Application Architectures](https://learning.oreilly.com/library/view/hadoop-application-architectures/9781491910313/)*. O'Reilly Media, 2015. ISBN: 978-1-491-90004-8 +[^21]: Jules S. Damji, Brooke Wenig, Tathagata Das, and Denny Lee. *[Learning Spark, 2nd Edition](https://learning.oreilly.com/library/view/learning-spark-2nd/9781492050032/)*. O'Reilly Media, 2020. ISBN: 978-1492050049 +[^22]: Michael Isard, Mihai Budiu, Yuan Yu, Andrew Birrell, and Dennis Fetterly. [Dryad: Distributed Data-Parallel Programs from Sequential Building Blocks](https://www.microsoft.com/en-us/research/publication/dryad-distributed-data-parallel-programs-from-sequential-building-blocks/). At *2nd European Conference on Computer Systems* (EuroSys), March 2007. [doi:10.1145/1272996.1273005](https://doi.org/10.1145/1272996.1273005) +[^23]: Daniel Warneke and Odej Kao. [Nephele: Efficient Parallel Data Processing in the Cloud](https://stratosphere2.dima.tu-berlin.de/assets/papers/Nephele_09.pdf). At *2nd Workshop on Many-Task Computing on Grids and Supercomputers* (MTAGS), November 2009. [doi:10.1145/1646468.1646476](https://doi.org/10.1145/1646468.1646476) +[^24]: Hossein Ahmadi. [In-memory query execution in Google BigQuery](https://cloud.google.com/blog/products/bigquery/in-memory-query-execution-in-google-bigquery). *cloud.google.com*, August 2016. Archived at [perma.cc/DGG2-FL9W](https://perma.cc/DGG2-FL9W) +[^25]: Tom White. *[Hadoop: The Definitive Guide](https://learning.oreilly.com/library/view/hadoop-the-definitive/9781491901687/)*, 4th edition. O'Reilly Media, 2015. ISBN: 978-1-491-90163-2 +[^26]: Fabian Hüske. [Peeking into Apache Flink's Engine Room](https://flink.apache.org/2015/03/13/peeking-into-apache-flinks-engine-room/). *flink.apache.org*, March 2015. Archived at [perma.cc/44BW-ALJX](https://perma.cc/44BW-ALJX) +[^27]: Mostafa Mokhtar. [Hive 0.14 Cost Based Optimizer (CBO) Technical Overview](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/). *hortonworks.com*, March 2015. Archived on [archive.org](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/) +[^28]: Michael Armbrust, Reynold S. Xin, Cheng Lian, Yin Huai, Davies Liu, Joseph K. Bradley, Xiangrui Meng, Tomer Kaftan, Michael J. Franklin, Ali Ghodsi, and Matei Zaharia. [Spark SQL: Relational Data Processing in Spark](https://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](https://doi.org/10.1145/2723372.2742797) +[^29]: Kaya Kupferschmidt. [Spark vs Pandas, part 2 -- Spark](https://towardsdatascience.com/spark-vs-pandas-part-2-spark-c57f8ea3a781/). *towardsdatascience.com*, October 2020. Archived at [perma.cc/5BRK-G4N5](https://perma.cc/5BRK-G4N5) +[^30]: Ammar Chalifah. [Tracking payments at scale](https://bolt.eu/en/blog/tracking-payments-at-scale). *bolt.eu.com*, June 2025. Archived at [perma.cc/Q4KX-8K3J](https://perma.cc/Q4KX-8K3J) +[^31]: Nafi Ahmet Turgut, Hamza Akyıldız, Hasan Burak Yel, Mehmet İkbal Özmen, Mutlu Polatcan, Pinar Baki, and Esra Kayabali. [Demand forecasting at Getir built with Amazon Forecast](https://aws.amazon.com/blogs/machine-learning/demand-forecasting-at-getir-built-with-amazon-forecast). *aws.amazon.com.com*, May 2023. Archived at [perma.cc/H3H6-GNL7](https://perma.cc/H3H6-GNL7) +[^32]: Jason (Siyu) Zhu. [Enhancing homepage feed relevance by harnessing the power of large corpus sparse ID embeddings](https://www.linkedin.com/blog/engineering/feed/enhancing-homepage-feed-relevance-by-harnessing-the-power-of-lar). *linkedin.com*, August 2023. Archived at [archive.org](https://web.archive.org/web/20250225094424/https://www.linkedin.com/blog/engineering/feed/enhancing-homepage-feed-relevance-by-harnessing-the-power-of-lar) +[^33]: Avery Ching, Sital Kedia, and Shuojie Wang. [Apache Spark \@Scale: A 60 TB+ production use case](https://engineering.fb.com/2016/08/31/core-infra/apache-spark-scale-a-60-tb-production-use-case/). *engineering.fb.com*, August 2016. Archived at [perma.cc/F7R5-YFAV](https://perma.cc/F7R5-YFAV) +[^34]: Edward Kim. [How ACH works: A developer perspective --- Part 1](https://engineering.gusto.com/how-ach-works-a-developer-perspective-part-1-339d3e7bea1). *engineering.gusto.com*, April 2014. Archived at [perma.cc/F67P-VBLK](https://perma.cc/F67P-VBLK) +[^35]: Zhamak Dehghani. [How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh](https://martinfowler.com/articles/data-monolith-to-mesh.html). *martinfowler.com*, May 2019. Archived at [perma.cc/LN2L-L4VC](https://perma.cc/LN2L-L4VC) +[^36]: Chris Riccomini. [What the Heck is a Data Mesh?!](https://cnr.sh/essays/what-the-heck-data-mesh) *cnr.sh*, June 2021. Archived at [perma.cc/NEJ2-BAX3](https://perma.cc/NEJ2-BAX3) +[^37]: Chad Sanderson, Mark Freeman, B. E. Schmidt. [*Data Contracts*](https://www.oreilly.com/library/view/data-contracts/9781098157623/). O'Reilly Media, 2025. ISBN: 9781098157623 +[^38]: Daniel Abadi. [Data Fabric vs. Data Mesh: What's the Difference?](https://www.starburst.io/blog/data-fabric-vs-data-mesh-whats-the-difference/) *starburst.io*, November 2021. Archived at [perma.cc/RSK3-HXDK](https://perma.cc/RSK3-HXDK) +[^39]: Michael Armbrust, Ali Ghodsi, Reynold Xin, and Matei Zaharia. [Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics](https://www.cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf). At *11th Annual Conference on Innovative Data Systems Research* (CIDR), January 2021. +[^40]: Leslie G. Valiant. [A Bridging Model for Parallel Computation](https://dl.acm.org/doi/pdf/10.1145/79173.79181). *Communications of the ACM*, volume 33, issue 8, pages 103--111, August 1990. [doi:10.1145/79173.79181](https://doi.org/10.1145/79173.79181) +[^41]: Stephan Ewen, Kostas Tzoumas, Moritz Kaufmann, and Volker Markl. [Spinning Fast Iterative Data Flows](https://vldb.org/pvldb/vol5/p1268_stephanewen_vldb2012.pdf). *Proceedings of the VLDB Endowment*, volume 5, issue 11, pages 1268-1279, July 2012. [doi:10.14778/2350229.2350245](https://doi.org/10.14778/2350229.2350245) +[^42]: Grzegorz Malewicz, Matthew H. Austern, Aart J. C. Bik, James C. Dehnert, Ilan Horn, Naty Leiser, and Grzegorz Czajkowski. [Pregel: A System for Large-Scale Graph Processing](https://kowshik.github.io/JPregel/pregel_paper.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2010. [doi:10.1145/1807167.1807184](https://doi.org/10.1145/1807167.1807184) +[^43]: Richard MacManus. [OpenAI Chats about Scaling LLMs at Anyscale's Ray Summit](https://thenewstack.io/openai-chats-about-scaling-llms-at-anyscales-ray-summit/). *thenewstack.io*, September 2023. Archived at [perma.cc/YJD6-KUXU](https://perma.cc/YJD6-KUXU) +[^44]: Jay Kreps. [Why Local State is a Fundamental Primitive in Stream Processing](https://www.oreilly.com/ideas/why-local-state-is-a-fundamental-primitive-in-stream-processing). *oreilly.com*, July 2014. Archived at [perma.cc/P8HU-R5LA](https://perma.cc/P8HU-R5LA) +[^45]: Félix GV. [Open Sourcing Venice -- LinkedIn's Derived Data Platform](https://www.linkedin.com/blog/engineering/open-source/open-sourcing-venice-linkedin-s-derived-data-platform). *linkedin.com*, September 2022. Archived at [archive.org](https://web.archive.org/web/20250226160927/https://www.linkedin.com/blog/engineering/open-source/open-sourcing-venice-linkedin-s-derived-data-platform)