1. 先搞明白MapReduce到底解决了什么问题1.1 分布式不是多几台机器这么简单我第一次接触Hadoop那会儿正好赶上部门里的日志分析任务开始吃紧。原来单机脚本处理一天的访问日志要跑四五个小时领导说上集群吧我就天真地以为把脚本丢到几台机器上同时跑就完事了。真上手才发现分布式计算不是把活分给大家干这么一句话能概括的——它要处理机器之间怎么通信、一个任务挂了怎么办、数据放在哪台机器上、结果怎么汇总这几个问题任何一个没解决好集群跑起来比单机还慢。MapReduce这套框架的价值就是把上述这些分布式计算的脏活累活全部封装起来。你只需要写两个函数一个Map一个Reduce剩下的事——任务拆分、调度、容错、跨节点数据传输——框架全部帮你扛了。这就好比你要请一百个人吃饭不需要亲自给每个人端盘子上菜只需要告诉后厨做什么菜、告诉服务员往哪张桌上菜中间的调度体系是MapReduce帮你搭好的。Hadoop里的MapReduce简称MR是个离线批处理的计算引擎它和HDFS这套分布式文件系统是天生一对。HDFS负责存数据MapReduce负责算数据。很多新手容易把这两个概念搞混其实一句话就能区分HDFS管的是数据怎么分散放在多台机器上MapReduce管的是如何把分散的数据联合起来算出一个结果。1.2 一个分而治之的思维革命MapReduce的整套设计哲学本质上是分而治之这四个字。你得把一个大的计算问题拆成无数小的、互不依赖的子问题并行处理完再把子问题的结果合并起来。对应到框架上就是两个阶段Map阶段负责分Reduce阶段负责合。我举个特别容易理解的例子。假设你要统计一个图书馆里所有书一共出现了多少个不同的字单个人干这件事得翻书翻到天荒地老。MapReduce的做法是先找一百个人每人分几本书把自己手里这几本书里的每个字各出现多少次统计出来这就是Map。然后找一个人或者几个人把这一百份统计结果按字合并比如所有的字的数据汇总到一组再加一遍得出整个图书馆的字出现多少次这就是Reduce。关键点在于Map阶段那一百个人之间完全不需要互相通信各干各的这就能并行。Reduce阶段虽然在汇总但不同的字也可以分给不同的人分别汇总依然可以并行。整个计算过程中跨节点的数据通信是最少的只在Map结束、Reduce开始的中间地带发生一次模型简单所以调度起来也稳。这是MapReduce比早年那些复杂的分布式计算方案高明的地方——它把程序员需要理解的分布式模型简化到了极致。1.3 数据本地性计算向数据靠拢还有一个被很多人忽略的设计思想叫数据本地性Data Locality。如果你去问一个刚学分布式的人集群有一千台机器数据分散存储现在要算一个任务应该怎么调度大多数人会说把数据通过网络搬到一台中心机器上算。这个答案在数据量小的时候没错可当数据是PB级的时候把数据搬到计算节点这一路网络的传输成本会比计算本身贵得多。MapReduce反着来——计算节点不搬数据而是让任务尽量在数据所在的机器上执行。HDFS会把一个大文件切成128MB大小的块分散存在不同的DataNode上MapReduce的调度器会优先把Map任务分配到存有对应数据块的节点上。存哪台算哪台实在不行才退而求其次往同机架的机器上调度。这笔账算下来网络开销能省掉一大截。我之前跑过一个几百GB的排序任务特意对比过开启数据本地性和不开启的差距同样是十个节点前者的作业时间能缩短一半以上。这套计算向数据靠拢的思路后来也被Spark、Presto这些计算引擎继承了下来。你学MapReduce如果只是背了几个API就觉得自己会了那等于白学把这些底层设计理念吃透了后面学任何一套分布式框架都会轻松很多。2. 从WordCount看透MapReduce的五个阶段2.1 输入分片与RecordReader几乎所有讲MapReduce的教程都会拿WordCount开刀我也一样但我不打算只是贴一段能跑的代码而是想把这个作业的完整执行过程掰开揉碎给你看。你理解了WordCount背后每一个阶段发生了什么MapReduce你就算入门了。一个MR作业跑起来第一步不是执行你写的map函数而是先把输入数据切成若干个输入分片InputSplit。分片是个逻辑概念默认情况下一个分片对应HDFS里的一个Block默认128MB而每个分片会被分配到一个MapTask去处理。注意Block是物理存储的单位Split是计算处理的单位它俩默认对齐但Split也可以自己定义切分逻辑后面讲小文件问题时会用到。分片生成之后框架会为每个分片创建一个RecordReader它干的事情是把分片里的数据按行读出来解析成键值对key-value pair喂给map函数。对于最常见的文本文件来说key是这一行第一个字节在整个文件中的偏移量value是这一行的内容。这两个就对应你写Mapper时看到的LongWritable和Text参数。我见过不少新手第一次看map(LongWritable key, Text value)的时候反应不过来——key怎么是一串数字那是因为他没有理解RecordReader这一层的作用。key在这里不代表任何业务含义纯粹只是给value的行号标识方便框架在出错时定位到具体是哪一行数据出了问题。2.2 Map阶段你的map函数到底在跑什么map函数接收的是一个个键值对处理完输出的是你自己定义的键值对。在WordCount里输入是行偏移量hello hadoop hello world输出则是hello, 1、hadoop, 1、hello, 1、world, 1也就是把每个词拆开来打上一个1的标签。这个1不是随便写的它代表这个词出现过一次。需要强调一个很容易被忽视的设计map函数的输出不会直接写到磁盘上而是先写进一个内存缓冲区。这个环形缓冲区默认大小是100MB当写入量达到缓冲区容量的80%也就是80MB时后台线程会开始将数据溢写到本地磁盘这个过程叫Spill。在溢写之前缓冲区里的数据会先做一次分区Partition然后分区内按键排序Sort。分区数不是随便定的它决定了数据该流向哪个Reducer。默认情况下MapReduce通过key的哈希值对ReduceTask数量取模来确定该进哪个区。你如果没写自定义Partitioner多个Reducer之间分到的数据量大致均匀但如果有某个key特别大就可能出现后面要说的数据倾斜问题。这里的排序也很有意思它不是全排序而是每个分区内部排序。也就是说每个Map节点在自己的数据范围内把键排好序为后面Reduce端做归并排序做准备。这一步看着不起眼实际上对整个框架的效率影响非常大。2.3 Shuffle与Sort最容易被忽略的隐形关卡Map和Reduce之间这一段是整个MapReduce里最复杂、也最容易成为性能瓶颈的地方叫做Shuffle。Shuffle不是用户自己能写代码控制的阶段完全是框架内部的行为但恰恰是因为不可见很多人就干脆不关心它结果作业跑得慢都不知道慢在哪。Shuffle可以拆成Map端和Reduce端两段来看。Map端刚才说的环形缓冲区溢写后会生成多个Spill文件这些文件会被合并成一个更大的文件同时按分区把属于不同Reducer的数据划分开。这个合并过程是一个多路归并排序最终Map任务结束时每个Reducer都能从这个节点上拉到属于自己分区的数据。Reduce端则要启动若干个拷贝线程从各个Map节点把属于自己的数据抓过来全部到位后再做一次归并排序形成Reduce函数的输入。网上有句话说得很形象Map是分Reduce是合Shuffle是中间那个运输大队。运输环节最容易出现的问题有三个数据量太大导致网络拥塞、Map端溢写次数过多导致磁盘IO飙升、Reduce端数据到达不及时导致空转等待。解决思路一般都落在压缩和Combiner上——Map端输出先做一次压缩减少网络流量或者提前做一次局部聚合减少传输量。这两个优化手段实战里绝大多数场景都值得默认开启。2.4 Reduce阶段与OutputFormatReduce端拿到的是key, value-list的格式也就是说同一个key的所有value会被打包成一个迭代器传进来。WordCount的reduce函数做的事情就一行逻辑把一个词的value列表里所有的值加起来输出hello, 总次数。为什么MapReduce要设计成一组相同key的value这种形式而不是一个key一个value地处理这是因为框架在Shuffle阶段已经帮你完成了分组你拿到手的天然就是同一键下的所有值这让你可以非常方便地做聚合统计——求和、求最值、求均值全都是在Reduce里遍历这个value-list完成的。Reduce阶段最后一步是OutputFormat。最常见的是TextOutputFormat把key, value格式化写成文本文件行与行之间用制表符分隔。如果你跑完一个作业在输出目录里看到_SUCCESS文件和一个part-r-00000之类的文件不要奇怪前者是作业成功的标记文件后者才是真正的结果。part-r-00000里的r表示这是Reduce输出如果是part-m-00000那就是Map-only任务的输出。很多新手以为part文件是自己写的代码生成的其实那全是框架的默认行为。2.5 Combiner用预聚合换性能Combiner是我一直主张新手要认真理解的一个组件因为它在真实生产环境中几乎每次都能带来肉眼可见的性能提升。Combiner可以理解为运行在Map端的迷你Reducer它的作用是在Map节点上先把相同key的value做一次合并再把合并后的结果传给Reduce。WordCount的场景特别适合Combiner因为map端输出的hello, 1可能有成千上万个如果不做预聚合这一大堆hello, 1全都要通过网络传到Reducer那边。加了Combiner之后每个Map节点本地先算好hello, N传输量从原来的几万条降到几十条网络开销直接少一个数量级。但要注意Combiner的使用有大前提它不能改变最终的计算结果。也就是说它只适用于求和、求极值这类满足交换律和结合律的计算。求平均值的逻辑就不能随便用Combiner你如果拿2, 4, 6求均值得到4再拿另一组4, 8求均值得到6两级均值合并不等于整体均值。这种场景要坚持在Reduce里算Combiner里只能去做部分中间数据的汇总。3. 手写一个MapReduce作业从Driver到业务逻辑3.1 环境准备与三种运行模式的取舍纸上谈兵聊完了现在说点能动手的。我假设你本机已经装好了JDK和Maven并且有一套Hadoop环境。如果你还没装可以先用本地模式跑这个模式下不需要任何集群程序直接在JVM里以单进程方式模拟分布式执行非常适合用来调试逻辑。整套环境的准备里我最想提醒的是版本匹配问题。Hadoop生态的版本兼容性这些年一直是重灾区你在网上随便搜一个教程照着配大概率会遇到依赖冲突或者类找不到的报错。我的建议是用Hadoop 3.x系列的稳定版本配套依赖统一用Maven仓库里和该版本对应的artifact不要混搭。另外在Windows下开发还需要在本地放置和集群版本一致的winutils.exe和hadoop.dll否则跑本地模式会报Failed to locate the winutils binary之类的问题这是新手经常卡住的第一道坎。三种运行模式——本地模式、伪分布式、完全分布式——选型逻辑也简单本地调代码伪分布式练流程真正干活上集群。伪分布式就是在一台机器上同时跑NameNode、DataNode、ResourceManager和NodeManager它和完全分布式唯一的区别是节点数量而已配置文件写法是完全一致的。3.2 Mapper核心逻辑的写法与命名细节写WordCount的Mapper代码量很少但里面藏着几个会让新手满头问号的细节。先看主要逻辑import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class WordCountMapper extends MapperLongWritable, Text, Text, IntWritable { private final static IntWritable one new IntWritable(1); private Text word new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString(); String[] words line.split( ); for (String w : words) { word.set(w); context.write(word, one); } } }第一个值得说的是四个泛型参数。MapperLongWritable, Text, Text, IntWritable这四格分别对应输入key、输入value、输出key、输出value的类型。前两个由InputFormat决定后面两个是你自己指定的。不少资料会把这里的参数简写成MapperObject, Text, Text, IntWritable因为输入key那行偏移量你用不上直接写Object完事。这没问题但你要知道LongWritable只是Hadoop对long类型的封装并不是什么神秘类型。第二个细节是为什么不用String而用Text。Hadoop把Java里所有的常用类型都做了一套自己的Writable序列化封装Text对应StringIntWritable对应intLongWritable对应long。原因我在后面章节单独说这里你只需要记住在Mapper和Reducer里传递的数据类型必须用Writable的封装类型这是框架的硬性规定。第三个细节是word这个对象的使用方式。注意我用的是word.set(w)而不是word new Text(w)。原因是Context在传递数据时走的是对象引用如果每次循环都new一个新对象虽然也能跑但对象频繁创建销毁会给GC带来很大压力。复用同一个Text对象只改它的内容这才是生产环境的写法。我见过有的教程直接每次new Text(w)功能没错但你在处理上亿行数据时这种写法会让垃圾回收成为性能瓶颈。3.3 Reducer与Driver串联整个流程的关键Reducer的写法和对偶但很多人不知道的是Reducer的reduce方法并不是一个key调用一次而是一个key对应的一组value调用一次。看代码import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class WordCountReducer extends ReducerText, IntWritable, Text, IntWritable { Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { int sum 0; for (IntWritable val : values) { sum val.get(); } context.write(key, new IntWritable(sum)); } }这里有个经典的坑千万别在for循环里把同一个IntWritable对象反复写入Context。values里虽然你看到的是一个个IntWritable但框架为了复用对象可能每次迭代返回的都是同一个对象的不同值。如果你把val直接传给context.write最终写出去的很可能全是最后一个值。正确做法是在循环里把值取出来用新的对象或者已经准备好的独立对象去写。再写Driver类它是作业的入口负责把Map、Reduce、输入输出路径等参数拼装成完整的作业描述import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class WordCountDriver { public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, Word Count); job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.class); job.setReducerClass(WordCountReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }Driver里最容易被忽视的是setMapOutputKeyClass和setOutputKeyClass这两组。如果Mapper输出的key/value类型和Reducer输出的不一样像本例就一样都是Text和IntWritable很多人就会偷懒只写setOutputKeyClass。其实框架内部的Shuffle阶段需要知道Map输出的类型才能做序列化比较如果类型不匹配或者没有设置轻则告警重则直接报序列化错误。稳定起见建议每次都把两组类型都写上养成习惯。输出路径的坑也值得提一句Hadoop要求输出目录必须不存在否则作业会直接失败。因为框架不会帮你覆盖旧目录这是避免误删数据的保护机制。每次试跑之前先删掉上一次的out目录你就能少踩很多雷。3.4 为什么序列化不用Java自带的看到这一节你可能奇怪为什么Hadoop不直接用Java的Serializable接口来传输数据非要自己造一套Writable机制。这个问题的答案本身就是Hadoop性能设计的精髓。Java自带的序列化机制有个臭名昭著的问题序列化后的字节流特别臃肿。它会写入完整的类描述信息、继承层次、对象引用关系一个简单的int被序列化后可能占几十个字节。这在单机应用里无所谓但在分布式系统里这些多余字节全要落到磁盘、通过网络传输数据量放大个好几倍成本是灾难性的。Hadoop的Writable序列化则极度精简。一个IntWritable的序列化结果就是4个字节的大端序整型没有任何类名信息因为反序列化的类型是在代码编译期就确定的框架只要知道读4个字节就够了。Text对象序列化的方式是4字节长度前缀加UTF-8编码内容同样没有多余的描述信息。这种紧凑的序列化方式让Hadoop在处理TB级别数据时光传输开销就能比Java原生序列化节省好几个量级。还有一个原因是为跨语言做准备。Java原生序列化只有Java自己能识别但Hadoop生态里的排序、压缩、索引等组件很多需要支持多种语言客户端。Writable协议是自定义的二进制格式其他语言只要实现同一套协议就能互通。虽然实际使用中MapReduce主要还是Java生态但这个设计为整个生态的扩展留了后路。4. 把作业跑上真实集群我踩过的坑比你想象的多4.1 小文件问题一个看似无害的开局第一次往集群上丢真实数据时我遇到的头号坑就是小文件。当时我处理的数据是好几万个几十KB的小日志文件往HDFS上一传MapReduce一跑作业时间是按小时计的而且大量时间都花在各种等待上。小文件问题分两条线。存储端每个文件在HDFS里都要占用NameNode内存来维护元数据一个小文件大约占150字节左右的堆内存一万个文件就是1.5MB看起来不多但如果你有千万级的小文件NameNode内存直接爆掉。计算端MapReduce默认一个分片对应一个MapTask每个MapTask启动、初始化、调度都有固定开销几分钟到十几分钟不等的作业时间最后能有三分之二耗在Task启动上真正计算的时间不到三分之一。解决办法有两个思路。一是源头控制在上传数据时把小文件合并成大文件或者用SequenceFile这类容器格式打包存储。二是计算侧兜底用Hadoop自带的CombineTextInputFormat代替默认的TextInputFormat它可以把多个小文件合并到一个分片里让一个MapTask处理多个文件。我在配置里加上job.setInputFormatClass(CombineTextInputFormat.class); CombineTextInputFormat.setMaxInputSplitSize(job, 134217728);设置最大分片大小为128MB几百个小文件就会被合并成一个分片MapTask数量瞬间从几千降到几十个作业时间也降到了十分钟以内。这个优化操作性价比极高可以说是小文件场景的第一选择。4.2 数据倾斜从现象到底层的完整排查链路数据倾斜是我见过最容易让人想破头的问题。它的典型表现是Reduce阶段大部分任务早就跑完了唯独某一个或多个任务卡在99%一直不动整个作业在那里干等。先说本质原因Shuffle阶段是按key的哈希值分区的如果某个key的数据量比其他key大得多那么对应那个分区的Reducer就会收到海量数据处理时间被无限拉长。最经典的案例是空值问题日志里有一半数据的某个字段是空值你按这个字段做聚合所有空值全进了同一个分区那个Reducer不炸才怪。排查链路是这样的第一步看Counter里的HDFS读写量确认是不是某个ReduceTask的输入数据量异常大第二步检查业务数据里key的分布情况先跑一个简单的count按key分组统计看看前几名的key占比多少第三步针对具体的key决定处理方案。我之前遇到一次用户行为日志统计就是某个特殊渠道的标识字段占了总数据量的40%导致那一个Reducer拖垮整个作业。最终的解决方案是两阶段聚合Map端先加上随机数前缀把一个大key拆散到多个Reducer上做第一轮局部聚合然后第二轮作业去掉随机数前缀把所有结果汇总到少数Reducer做最终聚合。代价是多一次作业和一轮落盘但换来的是作业总时间从两小时降到二十分钟值。还有一种思路是自定义Partitioner把数据倾斜严重的key单独分到某个Reducer给它加人加资源或者干脆改变业务逻辑比如过滤掉那些没意义的空值。方案没有银弹只有先搞清楚倾斜的key是什么、为什么倾斜、业务上允不允许拆才能选出合适的处理方式。4.3 任务挂掉之后从日志到根因的快速定位法MapReduce作业挂掉是家常便饭新手看到一片红色的ERROR日志就慌了其实绝大多数问题原因就藏在几个固定位置上。最典型的错误是Java虚拟机内存溢出日志里会看到java.lang.OutOfMemoryError: Java heap space。这时候别急着加内存先看是哪个阶段挂的。如果是Map阶段挂的通常是map函数里处理了超大值或者缓存了太多数据如果是Reduce阶段挂的一般是reduce函数对value迭代器做了全量缓存——这里我特别提醒一句Iterable values看起来能反复遍历但底层其实是框架包装的流式对象你没法随机访问也不能无脑存进集合里随着数据量增长内存肯定会爆。还有一类出现频率极高的错误是ClassCastException或者NullPointerException这种多半是泛型类型写错了或者Map输出类型在Driver里没配对。排查方式就是看日志里报错的行号对应到自己的Mapper或Reducer代码里定位。我自己的一套排查顺序是先看作业失败的具体Task编号和所在节点再顺着这个节点的日志文件路径通常是yarn的logs目录找Container日志往上翻异常栈的前十行基本就能定位。如果异常信息看不出来先把no. of reduce tasks调成0跑一个纯Map作业试试这样能区分问题是出在Map阶段还是Shuffle/Reduce阶段把排查范围迅速缩到一半。4.4 调优参数哪些值得动哪些别乱动MapReduce参数多如牛毛但真正值得手动调的其实就那么几个。我在集群上调优的时候奉行一个原则让默认参数先跑一遍根据瓶颈数据再动手绝不上来就瞎改。先说Map端内存。默认情况下MapTask的堆内存是1GB环形缓冲区占100MB溢写阈值是80%。如果你的Map任务处理的是大记录很容易出现缓冲区写满后频繁溢写严重的时候一个MapTask可能产生几十个Spill文件。这时可以适当调大mapreduce.task.io.sort.mb缓冲区大小和mapreduce.map.java.optsJVM堆大小让缓冲区能扛住更多数据再溢写Spill次数就会明显下降。Reduce端最常见的瓶颈是拉取Map输出太慢。默认的mapreduce.reduce.shuffle.parallelcopies是5也就是同时只能从5个Map节点拉数据。如果你有几百个MapTask这个值明显不够可以调到20甚至更高减少等待时间。另外Reduce的堆内存同样可以调大mapreduce.reduce.java.opts避免归并排序阶段的内存压力。有一个参数我建议不要乱动就是mapreduce.job.reduces也就是ReduceTask数量。默认是1意味着所有数据只有一个Reducer来完成大作业必挂。但也不是越大越好很多人把ReduceTask设成几百几千结果每个Reducer处理的数据量太小启动开销占比反而变大。经验值大约是ReduceTask数量为集群CPU核心数的1到2倍或者根据输出文件的大小来决定——每个Reduce输出128MB到256MB左右比较合适。还有一个参数叫mapreduce.map.speculative推测执行。它的作用是当某个Task运行明显慢于其他同阶段任务时集群会在另一个节点上启动一个同样的任务哪个先跑完用哪个的结果。这个机制在小集群上经常帮倒忙因为节点资源本来就紧张额外启动重复任务造成资源竞争反而让整体更慢。我在生产环境直接把推测执行关掉了property namemapreduce.map.speculative/name valuefalse/value /property property namemapreduce.reduce.speculative/name valuefalse/value /property当然如果你的集群资源充裕且任务确实存在长尾问题开着它也有用具体环境具体分析。5. MapReduce的边界与它的后浪5.1 为什么MapReduce不适合迭代计算MapReduce的局限性和它的优点一样鲜明。最大的短板在于每个作业做完Map和Reduce之后中间结果要落到磁盘上下一个阶段再重新读入。对于单轮的批处理来说这个没问题但碰上迭代计算——比如机器学习里的梯度下降、图算法里的PageRank——每一轮都要读写磁盘开销就太夸张了。举个例子如果你要跑十轮迭代MapReduce就得启动十个作业每个作业都要读HDFS、写HDFS这意味着数据要被网络和磁盘来回刷二十遍。Spark之所以在那个年代被誉为革命性的就是因为它把中间结果尽可能留在内存里通过DAG有向无环图把多个计算阶段串联起来迭代计算不用反复落盘性能直接提升一个数量级。还有一点是实时性问题。MapReduce是批量提交、批量运行的作业从启动到输出结果通常要几十秒甚至几分钟根本没法满足秒级响应的需求。实时流计算场景需要的是Flink、Storm这类事件驱动、持续运行的引擎。5.2 学MapReduce的真正价值在哪听到这你可能觉得MapReduce过时了那我还费劲学它干嘛直接上Spark不香吗我在带新人时经常被问到这个问题我的回答是MapReduce不是用来驰骋沙场的它是用来理解分布式系统基本盘的最好的教材。你要知道Spark、Flink本质上做的还是把计算任务分成多个阶段、每个阶段并行处理、阶段之间传输数据这一套模型只是把Map和Reduce这两个粗粒度的算子细化成了更多更灵活的算子map、flatMap、filter、reduceByKey、join等等再把数据传递方式从落盘进化成了内存管道。你如果连MapReduce的Shuffle都不理解那Spark里面的shuffle优化、数据倾斜排查、广播变量设计的原理理解起来也只会是一知半解。另外MapReduce的容错机制至今仍是分布式系统的范本。它的做法是每一个MapTask的输出都落盘Reduce端只拉取已经成功完成的任务的结果这样任何一个环节挂了只需要重放这个Task而不需要把整个作业重跑一遍。这种失败重放粒度的设计思路在很多现代计算引擎里都能看到影子。5.3 面向未来如何把你掌握的MapReduce迁移到其他框架说到底框架会迭代但思维模型不会过时。你现在花时间搞明白MapReduce的每一个阶段在干什么等你转去学Spark的时候只需要做一个映射Map对应Spark里的map或mapPartitionsShuffle对应Spark里的shuffle依赖Reduce对应reduceByKey或者aggregate。Spark的调优经验里那些关于shuffle分区数、数据倾斜、内存调节的讨论底层逻辑和MapReduce完全是一个模子刻出来的。我个人在实际带项目的过程中还有一个体会老旧的MapReduce作业不一定非要用Spark重写一遍。如果作业本身已经稳定运行数据量也没涨到离谱把核心参数调优一下、把小文件治理一下它依然能把活干得漂亮。技术选型最忌讳的就是炫技——什么新上什么最后只会给自己和团队添乱。先把MapReduce基础打牢再在合适的场景里去接触新的计算引擎这条路反而走得最快。