第 10 章开启第三部分"派生数据(Derived Data)",关于把数据从一处取出并转换到另一处的系统——索引、缓存、聚合、模型。它从批处理(batch processing)开始:取一个大的、有界输入,处理它,产生一个输出,不期待低延迟。Kleppmann 从底层构建想法——从朴实的 Unix 命令行工具开始,它们的设计哲学结果证明是 MapReduce 和驱动大规模分析的现代数据流引擎的概念祖先。

⚡ 速览要点
  • 三种系统类型——服务(在线、延迟受限)、批处理(离线、有界输入、吞吐受限)、流(近实时、无界输入)。本章是批处理。
  • Unix 哲学向上扩展——做好一件事、用统一接口(管道上的文本)组合、预期被拼在一起。MapReduce 是这个想法跨数千机器。
  • MapReduce = map + shuffle + reduce——mapper 抽取键值对,框架按 key 排序和分组(shuffle),reducer 处理每组。作业链成工作流。
  • Join 是难的部分——reduce 侧(sort-merge)join 通用;map 侧(广播/分区哈希)join 在一个输入小或共分区时更快。热 key 造成倾斜。
  • 不可变输入使容错平凡——作业确定且无副作用,所以失败任务只是重跑。
  • 数据流引擎(Spark、Flink、Tez)胜过朴素 MapReduce——它们把整个工作流建模为一个作业,避免把每个中间步骤写到磁盘。
tldr

批处理把一个大的有界数据集转换成一个派生输出。Unix 哲学——带统一接口的小可组合工具——是模板;MapReduce 跨集群在分布式文件系统上应用它,带 map/shuffle/reduce 和链式工作流。Join 和倾斜是主要挑战。因为输入不可变、作业确定,容错只是"重跑失败任务"。数据流引擎通过把中间数据保在内存、把整个管道一起调度来改进 MapReduce。

用 Unix 工具做批处理

Kleppmann 从一个看似简单的任务开始:找一个 web 服务器日志里最热门的五个页面。你能用一串标准 Unix 工具做它,而这个例子有出人意料的深度。

top 5 URLs from a web log — a Unix pipeline
cat access.log |             # 流式读日志
  awk '{print $7}' |          # 抽取 URL 字段
  sort |                     # 把相同 URL 分组到一起
  uniq -c |                  # 计数每段重复
  sort -rn |                 # 按计数降序排
  head -n 5                  # 取前 5

魔法在 Unix 哲学:每个程序做好一件事、程序被预期组合、它们共享一个统一接口——stdin/stdout 上的文本流。那种统一正是为什么 sort 能喂 uniq 而两者都不知道对方。一个微妙但重要的细节:sort 通过把有序块溢出到磁盘并合并它们(外部归并排序)来处理大于内存的数据集——分布式系统在规模上用的同一技巧。

MapReduce 与分布式文件系统

MapReduce 在精神上是 Unix 管道铺到数千机器上。不读写本地文件,作业读写一个像 HDFS 的分布式文件系统——跨许多机器的 shared-nothing 磁盘池,文件被切成块、为容错复制。一个 MapReduce 作业有程序员写的两个回调和框架提供的一个步骤:

单个 map-reduce 步骤有限,所以真实作业是工作流:MapReduce 作业的链,一个的输出成为下一个的输入,由 Oozie 或 Airflow 等工具拼在一起。

Join 与分组

把相关记录聚到一起——join——是大部分工程所在:

MapReduce vs 分布式数据库

MapReduce 体现一种不同于 MPP(大规模并行处理)分析数据库的哲学。MPP 数据库想数据先加载进 schema 再跑优化 SQL。MapReduce(和 Hadoop 生态)拥抱读时 schema(schema-on-read):把原始数据倾倒进分布式文件系统,读它时再弄清它的结构,支持对同一原始数据的多样、演化的处理(SQL、ML、自定义代码)。这使 Hadoop 成为灵活的"数据湖",代价是 MPP 的查询优化。

输出与容错

批作业通常产生批量输出:构建搜索索引(Google 最初如何用 MapReduce)、训练推荐/ML 模型,或构造要加载进服务系统的只读键值存储。关键设计原则是输入不可变且作业无副作用——作业只读它的输入并写一个全新输出,从不原地修改输入。这几乎免费给出容错:若一个任务失败(机器崩溃),框架只是在另一台机器上重跑它,因为对同一不可变输入的确定计算产生同样结果。它也使实验安全——一个有 bug 的作业能被重跑而没损坏任何东西。

超越 MapReduce:数据流引擎

MapReduce 最大的低效:它在每个 map-reduce 步骤之间把中间状态物化到磁盘(分布式文件系统)。对许多步骤的工作流,这意味反复写和重读(并重复制)数 GB,且每个步骤在前一个完全结束前无法开始。数据流引擎——Spark、Tez、Flink——通过把整个工作流建模为一个连接算子的单一作业来修复这个:

方面MapReduce数据流引擎(Spark/Flink)
中间状态每步写到磁盘流水线 / 保在内存
作业模型独立的 map+reduce 步骤一个算子图(DAG)
延迟高(磁盘 + 步骤屏障)较低(重叠、内存)
容错重读物化输出从血缘重算(Spark RDD)
迭代作业痛苦(每遍重读)高效(在内存缓存)

取舍是容错:没有可回退的物化中间状态,数据流引擎必须重算丢失的数据。Spark 跟踪每个数据集的血缘(lineage)(产生它的操作),这样它能只重算丢失的分区——前提是计算确定。对图和迭代算法(如 PageRank),Pregel 的"像顶点一样思考"方法等专门模型在迭代间于顶点之间传消息,比每遍重跑 MapReduce 高效得多。

总结

批处理的持久教训关于设计纪律,而非仅吞吐:不可变输入和无副作用、确定的作业使容错、重试和实验平凡。MapReduce 通过借用 Unix 哲学在规模上证明了这个模型;数据流引擎通过不在每步之间碰磁盘改进它。同样的"从不可变输入派生数据"想法,以连续形式,在流处理里重现。

🎯 面试速答

MapReduce 的三个阶段是什么?Map(emit 键值对)、shuffle(框架按 key 排序和分组)、reduce(处理每个 key 的值)。shuffle 是昂贵、必要的中间。
Reduce 侧 vs map 侧 join?Reduce 侧(sort-merge)通用但 shuffle 一切;map 侧(广播或分区哈希)在一个输入小或两者共分区时跳过 shuffle。
为什么批处理容错容易?输入不可变、作业确定且无副作用,所以失败任务只是重跑、产生同样结果。
为什么 Spark/Flink 比 MapReduce 快?它们把工作流建模为一个算子图,在内存流水线数据而非把每个中间步骤物化到磁盘。

← 上一篇
一致性与共识