IT 论文精读 · PAPER 18
Jeffrey Dean & Sanjay Ghemawat · Google · OSDI 2004
2004 年,Google 两位工程师公开了一套叫 MapReduce 的做法,专治一个头疼问题:有一大堆数据要算,一台机器算不完,得摊到上千台机器上一起算。问题是,「让一千台机器协同干一件活」本身极其难写——谁分到哪块数据、机器算到一半坏了怎么办、算完的碎片怎么拼回去……写这些琐事比写正经算法还累。MapReduce 的贡献是:把这些琐事一次性打包藏起来,只让你填两个空。它后来直接催生了开源的 Hadoop,开启了「大数据」这十几年。
假设老板让你数一数:Google 爬回来的整个网页库里,每个单词各出现了多少次。逻辑简单到不能再简单——从头到尾扫一遍、见一个词就给它记一笔。可数据有几十 TB,一台机器扫到天荒地老。你只能拆给一千台机器分头数,最后再把一千份「小账本」合成一份大账本。真正折磨人的不是「数数」,而是「怎么把这件事安全地摊到一千台机器上」:怎么切、怎么分、哪台掉线了谁来补、结果怎么归拢——每写一个新的大数据任务,你都要把这套烂摊子重新收拾一遍。
MapReduce 说:这类活儿其实都能拆成同样的两步,你只要分别写清这两步,剩下的我全包。
第一步「Map(分头处理)」:告诉我,拿到一小片数据,你想吐出哪些「标签 → 数值」的小纸条。数单词就是:每见一个词,吐一张 (这个词, 1)。第二步「Reduce(按标签归总)」:告诉我,把同一个标签的所有小纸条收到一起后,你想怎么把它们并成一个答案。数单词就是:把某个词收到的那一堆 1 加起来。你只写这两个「怎么做」,至于「谁去做、坏了怎么办、纸条怎么按标签归堆」——全是框架替你干的。
几个朴素但极管用的招。一、坏了就重算那一小块。框架有个「工头」盯着上千个「工人」,哪个工人算到一半没气了,工头把它那一小块活儿另派一个人重做即可,整个任务不用推倒重来——机器天天坏,也照跑不误。二、把活儿送到数据身边。数据本来就分散存在这些机器的硬盘上,框架尽量让「算某块数据的工人」正好就是「存着那块数据的机器」,省下海量网络搬运。三、专治「拖后腿的」。一千个工人里总有那么几个机器抽风、干得奇慢,拖着整个任务收不了尾;框架在快结束时会给这些慢活儿再找个人抢着做一遍,谁先干完算谁的——这一招能把总耗时砍掉一大截。
MapReduce 把「写一个能在上千台机器上跑、还扛得住机器随时坏的大规模程序」这件从前只有分布式高手才敢碰的事,变成了普通工程师填两个函数就能做。Google 内部很快跑起成千上万个这样的任务,连它的搜索索引都用这套重写了一遍。开源世界照着它做出 Hadoop,几乎整个「大数据」行业都建在这个思路上。诚实的代价:它只擅长「从头到尾扫一大批数据」这种批量活儿,要你反复迭代、或要秒级实时响应,它就笨重了——后来的 Spark 等正是冲这点来的。
把「让上千台机器协同算一大堆数据」这件苦差,缩成你只需填的两个函数:Map(分头把数据变成「标签→数值」的小纸条)和 Reduce(按标签把纸条归总成答案)。谁去算、机器坏了怎么补、慢的怎么救、结果怎么拼——框架全包。这一下让普通人也能写大规模并行程序,开启了大数据时代。
想看它的执行流程图、容错与「抢跑救慢」机制、还有真实集群排序 1 TB 的数字? → 切到精读版
MapReduce 是 Google 提出的一套编程模型 + 运行时框架:用户只写两个函数——map(把输入映射成一批中间「键→值」对)和 reduce(把同一个键的所有值归并成结果)——框架就自动把计算并行摊到上千台机器上,并替你打理数据切分、任务调度、机器容错、慢节点补救、跨机数据洗牌(shuffle)这一切脏活。它让不懂分布式的工程师也能写出扛得住机器故障的大规模并行程序,是「老三件套」的第二件,直接催生了开源的 Hadoop 与整个大数据生态。
("cat", 1);标签叫键(key),内容叫值(value)。MapReduce 全程都在搬键值对。作者是 Jeffrey Dean 与 Sanjay Ghemawat,来自 Google,论文发表于 OSDI 2004。它是 Google「分布式系统老三件套」的第二件:下承 Paper 17 的 GFS(数据就存在 GFS 上、靠 GFS 提供容错的存储),上启两年后的 Bigtable(很多批量数据加工用 MapReduce 生成)。思想上它把函数式编程里古老的 map/reduce 原语搬到了千机规模;工程上它启发了开源的 Hadoop MapReduce,几乎定义了此后十余年「大数据」的入门范式,也为后来的 Spark、Dataflow 等更灵活的数据处理引擎立下了要超越的靶子。
2000 年代初,Google 内部到处是「概念简单、数据巨大」的计算:统计词频、构建倒排索引、分析爬取日志、计算网页图的各种统计……算法逻辑往往一眼就懂,难的是数据量大到必须摊到上千台机器上跑。
而一旦要摊到上千台机器,工程师就被迫反复重写同一批与业务无关的脏活:怎么切分输入、怎么把任务调度到成百上千台机器、机器算到一半崩了怎么办、怎么把散在各机的中间结果按键归拢、怎么处理慢机器拖后腿……这些容错与并行的样板代码,把一个本该几十行的简单计算,淹没在几千行分布式管道里。Google 团队发现:这些计算虽然五花八门,但它们的「并行骨架」惊人地一致。于是他们把这套通用骨架抽出来做成框架,只在两个地方留出「填空位」交给用户,其余全部由框架承担——让工程师重新只关心「算什么」,把「怎么在千机上又并行又容错地算」彻底交出去。
用户把计算表达成两个函数,二者都以键值对为进出货:
map: (k1, v1) → list(k2, v2) 拿到一条输入(如一个文件名和它的一行内容),吐出零到多条中间键值对。reduce: (k2, list(v2)) → list(v2) 框架已把同一个中间键 k2 的所有值收集成一个列表交给你,你把它们归并成更小的结果(通常零或一个值)。经典例子——数词频:map 对文档里每个词吐一张 (词, "1");框架自动把同一个词的所有 "1" 聚到一起;reduce 收到 ("cat", ["1","1","1"]),把它们加起来吐 ("cat", "3")。用户全程没写一行关于「并行」「机器」「网络」的代码——这正是模型的魔力:只要你的问题能套进「先逐条 map、再按键 reduce」这个模具,就能白拿千机并行与容错。
框架怎么把上面这件事跑到千机上?核心是把输入切成 M 份、把中间结果分成 R 桶,再由一个 master 把这 M+R 个任务派给一群 worker:
hash(key) mod R)分成 R 桶,对应 R 个 reduce 任务。一个特殊的 master 进程把这些任务不断派给空闲的 worker。map,产出的中间键值对先缓存在内存、再周期性落到本机本地磁盘,并按分区函数分成 R 个区。这些本地文件的位置回报给 master。reduce,结果写回 GFS。整个 job 的最终输出是 R 个文件(每个 reduce 任务一份),通常无需再合并——直接当作下一个 MapReduce 的输入即可。注意一个关键取舍:map 的中间结果写在 本地磁盘(便宜、快),最终结果才写进 GFS(贵、可靠)——这个区别,稍后决定了两种任务失败时截然不同的处理方式。
上千台机器同时跑,故障是家常便饭,容错是框架的立身之本。master 周期性 ping 每个 worker;一段时间没回应就判它挂了。妙处在于两类任务的失败处理截然不同,而这正源于「中间结果落本地磁盘、最终结果落 GFS」的取舍:
因为 map/reduce 都是确定性函数(同样输入必得同样输出),重算一遍结果完全一致,框架能放心地对失败任务「一键重跑」,最终产出和「从没坏过」时一模一样。master 本身失败的概率极低,论文里的实现直接让整个 job 重来(也可周期性存检查点后从检查点恢复)。
集群里网络带宽是稀缺资源。因为输入本就存在 GFS 上、每块有多份副本散落各机,master 派 map 任务时会刻意挑「输入副本正好在本机、或同机架邻居」的那台 worker。于是大量输入数据是从本地磁盘读的、根本不过网——在大 job 上,这让绝大部分输入不消耗任何网络带宽,是吞吐能拉满的关键工程细节。这也呼应了 GFS 的设计:存储与计算贴在一起。
一个常被忽视却极其致命的问题叫 straggler(掉队者):整个 job 已经算完 99%,就剩少数几个任务卡在某台抽风的机器上——磁盘将坏、CPU 被别的进程抢、配置出错……它一个人能把整个 job 的收尾拖长几倍。MapReduce 的解法朴素而有效:当 job 接近完成,master 为仍在进行的任务额外启动「备份任务(backup task)」,让另一台机器同时抢做同一份活,谁先完成就采用谁的、另一个作废。代价只是多花一点点资源,却能把长尾大幅削平——论文里,关掉这个机制会让排序任务多花约 44% 的时间。
(the,1) 出现上千次)。允许用户提供一个 combiner,在 map 端先局部合并一次(把上千个 (the,1) 先并成 (the,1000)),大幅减少要跨网 shuffle 的数据量。它通常就是 reduce 的逻辑。hash(key) mod R,用户也可自定义(如按 URL 的域名分区,让同一网站的数据落进同一个输出文件)。论文在约 1800 台机器的集群(每台约 2 GHz、4 GB 内存、两块 IDE 硬盘、千兆网)上用两个代表性任务实测:
10¹⁰ 条、每条 100 字节的记录(约 1 TB)里找一个稀有的三字符模式。切成 M≈15000 个分片,整个计算约 150 秒完成(含约一分钟启动开销)。10¹⁰ 条 100 字节记录)排序,M≈15000、R=4000。正常约 891 秒完成。两个对照实验最能说明设计的价值:① 关掉备份任务——同一排序任务变成约 1283 秒,慢了约 44%,长尾被几个 straggler 拖住;② 故意杀机器——排序途中人为干掉 200 个 worker 进程,框架自动把丢失的 map 任务在别处重跑,整个 job 仅多花约 5% 就正确完成。这两组数字直接证明了「备份任务治长尾」「重算治故障」确实管用。论文还给出 Google 内部的采用情况:MapReduce 上线一年多,内部已写出上千个不同的 MapReduce 程序、每天跑成百上千个 job,并用它彻底重写了生产环境的搜索索引流水线——代码更短、更好懂、更易改。
MapReduce 的伟大不在算法,而在抽象。它把「大规模并行 + 容错」这件从前需要分布式专家小心手写的事,一次性沉淀进框架、只给用户留两个填空位,从此普通工程师也能驾驭上千台机器。它与 GFS、Bigtable 合成 Google「老三件套」,而在开源世界,Yahoo 等照着这两篇做出了 Hadoop(HDFS + Hadoop MapReduce),几乎以一己之力点燃了「大数据」产业——此后十余年的数据仓库、日志分析、推荐系统、机器学习特征工程,大多先从一个 MapReduce 作业起步。更深远的是它普及的一整套心法:用简单而受限的编程模型换取自动并行、把容错做进框架而非应用、移动计算而非移动数据、用备份任务对抗长尾——这些理念被后来的 Spark、Flink、Google Dataflow / Beam 继承并发扬。
① 一句话:一套编程模型 + 运行时,让用户只写 map/reduce 两个函数,框架自动把计算并行摊到上千台机器并全程容错。
② 模型:map:(k1,v1)→list(k2,v2) 逐条产出中间键值对;框架按键归堆;reduce:(k2,list(v2))→list(v2) 归并。数词频是典型例子。
③ 执行:输入切 M 个 map 任务,中间键分 R 桶做 reduce 任务;一个 master 派活监控,reduce 从各 map 本地磁盘拉数据(shuffle)、排序、归并、写回 GFS。
④ 关键取舍:map 中间结果落本地磁盘(便宜),最终结果落 GFS(可靠)——这决定了两类失败的不同处理。
⑤ 容错:master ping worker;失败任务因 map/reduce 确定性而可安全重跑;已完成的 map 也要重做(产出困在挂掉的本地磁盘),已完成的 reduce 不必(已在 GFS)。
⑥ 本地性:把 map 任务调度到输入副本所在的机器,大量输入从本地磁盘读、不过网。
⑦ 备份任务治 straggler:临近收尾给慢任务找人抢跑、先完成者胜;关掉它排序慢约 44%。combiner 在 map 端预归并、削减 shuffle 量。
⑧ 结果:约 1800 机集群上,1 TB grep ≈150 秒、1 TB 排序 ≈891 秒;杀 200 台仅多花约 5%;内部上千个程序、并用它重写了搜索索引。
⑨ 影响:与 GFS/Bigtable 合成老三件套,直接催生 Hadoop 与整个大数据生态,普及「受限模型换自动并行、移动计算而非数据」的范式。
⑩ 局限:只擅批处理、迭代/实时慢、模型死板、被数据库界批为「倒退」;Google 自己以 Dataflow 等接棒。