1. 为什么HDFS、YARN、MapReduce总是被绑在一起讲
HDFS、YARN、MapReduce这三个词,几乎是大数据入门必背的组合。很多教材喜欢把它们拆成独立章节,一章讲存储、一章讲资源调度、一章讲计算模型,看起来界限分明。但到了真实集群上,这三者从来不是孤立运行的。你提交一个MapReduce作业,它要经过客户端、ResourceManager、NameNode、DataNode、NodeManager这一整条链路,任何一个环节掉链子,作业都跑不完。所以理解原理不能只盯着单个组件,得把三者串成一条线看。
这三者组成的"铁三角",本质上回答了一个问题:当数据量大到单台机器放不下、单颗CPU算不动的时候,怎么把任务拆到一堆机器上协同完成? HDFS负责把数据拆成块分散存储,YARN负责管理集群里每台机器的CPU和内存资源,MapReduce负责把计算逻辑拆成Map和Reduce两个阶段并行跑。存储、资源、计算各司其职,又互相配合。
这套组合虽然是好几年前的设计,但直到今天,业界主流的大数据平台(包括Spark、Flink)依然沿用HDFS做分布式存储、YARN做资源调度,只是把计算引擎换成了内存模型。如果你把HDFS、YARN、MapReduce的底层逻辑吃透,再看Spark和Flink的架构会轻松很多,因为它们要解决的问题是同一批。这篇文章不打算抄官方文档,我会按照实际集群上作业从提交到结束的完整路径来讲,顺便把实训里最容易翻车的几个点也揉进去。
1.1 从单机到集群,存储与计算为什么必须分离
单机环境下,程序读本地文件,算完输出到本地磁盘,逻辑很简单。但数据量一旦到了TB甚至PB级别,一台机器的磁盘放不下,哪怕放得下,一次全量扫描要跑好几天。这时候第一反应是"多搞几台机器,把数据分散存"。数据分散之后,新的问题来了:文件A被拆成了3份放在机器1、机器2、机器3上,我怎么知道哪个文件的哪份在哪个机器上?机器崩溃了数据丢了怎么办?某份数据访问特别频繁但所在机器磁盘满了怎么办?
HDFS就是来回答这些的。它把文件切成固定大小的块(默认128MB),每个块复制多份放到不同机器上,再通过一个中央节点NameNode记录"哪个块在哪个DataNode上"。计算任务发起时,客户端先问NameNode要数据位置,然后直接去对应机器上读,不需要把数据搬到一台中心机器上。这就引出了后面要讲的核心思想:移动计算比移动数据更划算。
YARN的出现晚于HDFS和MapReduce。早期的Hadoop版本里,资源调度和计算框架是紧耦合的,JobTracker既要管作业调度又要管任务监控,压力非常大,而且MapReduce之外的框架没法复用这套资源管理。YARN从1.x开始被拆出来,把"资源管理"和"作业控制"彻底分开,变成一个独立的通用资源调度平台。现在你跑Spark、Flink、MapReduce,它们都只是YARN上的一个Application而已。
1.2 一个作业涉及的三层角色划分
为了后面看得不晕,先把角色厘清。数据层面有NameNode和DataNode,NameNode管元数据,DataNode管真实数据块。资源层面有ResourceManager(全局老大)和NodeManager(每台机器的打工仔),ApplicationMaster是每个作业专属的"项目经理"。计算层面有MapTask和ReduceTask,它们才真正跑你写的map和reduce函数。很多人分不清ApplicationMaster和ResourceManager的区别,简单记:ResourceManager只管资源分配,不管作业内部怎么调度;ApplicationMaster是某个具体作业的代表,它向ResourceManager申请资源,然后指挥手下的task干活。
这套分层设计很像是开公司:HDFS是仓库,货按区块分布在各个分部;YARN的ResourceManager是集团总部HR,只管发人力和核算工时;ApplicationMaster是每个项目的负责人,去HR那儿要人,分到人之后安排他们干活;MapReduce框架是制定好的工作流程,所有人按流程一步步来。把这三层角色装进脑子里,后面看流程就顺了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. HDFS的核心机制:读写流程与常用命令
HDFS的设计目标很朴素:存储超大文件,容忍机器故障,适合一次性写入、多次读取的场景。它不太适合大量小文件,也不适合随机改写,这些先说清楚,不然你拿它当普通文件系统用会碰一鼻子灰。下面我从组件职责讲起,再拆解读和写两条流程,最后给出一组实训里高频使用的命令。
2.1 NameNode、DataNode和SecondaryNameNode各管什么
NameNode是整个HDFS的"大脑",它维护两个关键内容:文件系统的目录树,以及每个文件对应哪些块、这些块分布在哪些DataNode上。注意,NameNode本身不存文件内容,它只存元数据。所有对文件元数据的操作(创建目录、删除文件、打开文件的读请求)都要经过NameNode。由于它是单点,元数据会同时持久化到本地磁盘的fsimage和edits日志里,避免进程挂了丢数据。
DataNode是真正存储数据块的机器。一个块默认128MB,会复制3份放在不同机架的DataNode上,这叫副本策略。DataNode启动后会主动向NameNode汇报它有哪些块,还会每3秒发一次心跳,如果NameNode超过10分钟没收到某DataNode的心跳,就会认定它挂了,并把它的副本重新复制到别的节点上。这个自愈过程是自动的,不需要人干预。
SecondaryNameNode这个名字很有迷惑性,它并不是NameNode的热备,它只是定期拉取NameNode的edits日志,合并生成新的fsimage再传回去,帮NameNode"瘦身"。生产上高可用靠的是Active和Standby两个NameNode共享JournalNode,但单机学习环境通常只有一个NameNode加SecondaryNameNode。
2.2 文件写入流程:从客户端到数据节点的三次握手
往HDFS写一个文件时,流程可以拆成五步。
- 客户端调用DistributedFileSystem.create(),向NameNode发起"我要创建文件 /data/xxx.txt"的请求。NameNode检查路径是否合法、用户是否有权限,然后返回一个可用于写入的FSDataOutputStream。
- 客户端开始写数据时,不是直接写给DataNode,而是先把数据写入本地缓存,按块大小(128MB)切分。写完一个块后,再次向NameNode请求:这个块我要写到哪些DataNode去?NameNode根据网络拓扑选出一批节点,比如返回dn1、dn2、dn3。
- 客户端把这块数据包(packet)发给第一个DataNode(dn1),dn1收到后一边落盘一边转发给dn2,dn2再转发给dn3,这条通道叫pipeline。每个节点写完后按逆方向发送ack确认,从dn3到dn2到dn1再到客户端,确认这一块的3副本都写成功。
- 一个块写完后,客户端继续写下一个块,重复第2、3步。全部块写完,调用close()关闭输出流。
- 客户端最后通知NameNode"文件写完了",NameNode收到后把元数据标记为已完成状态,此时文件才对其他用户可见。
这个流程里有几个关键点。第一,为什么是pipeline而不是客户端直接同时发给三个节点?因为这样最少占用客户端网络带宽,数据只在节点间串行转发。第二,ack必须按逆方向逐级返回,如果dn2写失败,dn1会收到不成功的消息,随后NameNode会重新分配节点,保证副本数。第三,客户端写数据时NameNode不参与数据搬运,所以HDFS能扛住高并发写入。
2.3 文件读取流程:就近读,并行拉
读文件比写简单,但也有一条固定的链路。客户端调用FileSystem.open(),NameNode返回该文件每个块在哪些DataNode上有副本。随后客户端拿到一个FSDataInputStream,它会对返回的节点按"离客户端最近"排序,优先读同一机架的节点,如果同机架没有就读同数据中心的,尽量减少跨机架流量。
读取块数据时,客户端会并行打开多个DataNode的连接,分别拉取不同的块。比如一个文件有4个块,客户端可能同时从dn1读块1、从dn2读块2、从dn3读块3、从dn4读块4,这样能充分利用各节点的磁盘和网络IO。如果读某个块时DataNode挂了,输入流会自动切换到另一个存有该副本的节点,这个透明切换不需要用户改代码。
实际实训中你会发现,读文件比写文件快很多,因为读只需要拉一个副本,而写要同时写三份。如果你觉得某个MapReduce作业的输入读取慢,那多半不是HDFS读流程的问题,而更多是数据本地性问题——也就是读数据的任务和数据副本在不在同一台机器上,这个后面讲YARN和MapReduce协同时会提到。
2.4 实训高频的HDFS常用命令
虽然现在有很多工具可以自动操作HDFS,但命令行永远是排查问题的最直接手段。我把实训里常用的命令整理一下,基本都是hdfs dfs -开头的。
| 操作 | 命令示例 | 说明 |
|---|---|---|
| 查看目录 | hdfs dfs -ls /data |
列出目录下文件和子目录 |
| 建目录 | hdfs dfs -mkdir -p /user/hadoop/input |
递归创建目录 |
| 上传文件 | hdfs dfs -put local.txt /data/ |
把本地文件拷到HDFS |
| 下载文件 | hdfs dfs -get /data/local.txt ./ |
拉回本地 |
| 查看文件内容 | hdfs dfs -cat /data/part-r-00000 |
适合查看MapReduce输出 |
| 实时查看尾部 | hdfs dfs -tail -f /data/log.txt |
类似linux的tail |
| 删除文件/目录 | hdfs dfs -rm -r /data/old |
递归删除 |
| 查看块信息 | hdfs fsck /data/file.bin -files -blocks -locations |
定位文件的块分布和所在节点 |
这里特别想说一下fsck,它是排查数据本地性问题的重要工具。比如你发现某个MapTask跑得特别慢,可以用它查一下输入文件对应块分布在哪些机器上,然后对比YARN日志里这个Task实际跑在哪个节点,如果完全不重合,说明调度没做到数据本地性,就要检查网络拓扑配置或调度器设置。这属于调优层面的内容,后面单独说。
3. YARN的资源调度:ApplicationMaster与Container的博弈
在Hadoop 1.x时代,资源管理和作业控制全压在JobTracker一个进程上,集群规模超过几千台节点时它就成了瓶颈。YARN的诞生把"资源分配"和"作业控制"解耦:ResourceManager管全局资源,NodeManager管单机资源,ApplicationMaster管单个作业的执行。这套设计让同一个集群既能跑MapReduce,也能跑Spark和Flink,实现资源按需共享。
3.1 ResourceManager、NodeManager与ApplicationMaster的分工
按我之前说的比喻,ResourceManager负责"发人",它维护整个集群所有节点的资源清单。每台NodeManager会周期性地向ResourceManager上报本节点有多少可用内存、多少核、正在跑几个container。ResourceManager把这些汇总起来,形成一个全局资源视图。它不关心你的作业逻辑,只负责回答"现在还能分出去多少资源,从哪个节点分"。
NodeManager是每台机器上的"监工",它接受ResourceManager的指令,在本机启动或杀死container。注意container是YARN对资源的一种抽象,它封装了CPU核数和内存大小,不是一个真正的进程。NodeManager启动container时,会用一个子进程去运行指定的任务命令,并不断收集任务的CPU、内存使用情况,超过配额就杀掉。这就是为什么有时候你的任务什么都没做就挂掉,去看日志很可能是"Container killed by NodeManager"。
ApplicationMaster是每个提交到YARN上的作业的"项目经理"。客户端把作业提交给ResourceManager后,RM会找一台有空余资源的NodeManager启动一个特殊的container,用来跑ApplicationMaster(简称AM)。AM启动后,它会向RM注册并汇报"我是哪个作业的负责人",然后根据作业需要,分批向RM申请container来跑MapTask或ReduceTask。任务跑完后,AM向RM注销并退出,整个作业宣告结束。
3.2 作业提交与调度的核心工作流程
一个作业从提交到结束,在YARN侧的流程大致这样:
- 客户端向ResourceManager提交Application请求,提交内容包括作业的jar包、配置、依赖等。
- ResourceManager接受请求后,在一个NodeManager节点上启动ApplicationMaster的container。这个节点怎么选?通常是当前资源最空闲的节点。
- ApplicationMaster启动后,与ResourceManager保持心跳通信,同时根据作业的map数、reduce数计算需要的资源总量,然后以container的形式向RM提出申请:"我要5个container,每个2GB内存、1核,分布在不同节点上。"
- ResourceManager查看全局资源表,如果满足就返回一组节点列表,并在对应的NodeManager上预留资源。
- ApplicationMaster拿到资源后,逐个通知NodeManager启动container,每个container里运行一个task(MapTask或ReduceTask)。
- Task运行时通过ApplicationMaster间接上报进度,AM最后汇总给客户端。
- 所有Task完成后,ApplicationMaster向ResourceManager申请注销,RM释放所有container占用的资源。
这里面有一个容易被忽略的细节:MapTask跑完后,AM才向RM申请ReduceTask的container。因为ReduceTask的数量和位置是Map阶段结束后才能确定的,尤其是需要知道每个分区数据的分布情况。这也是YARN能弹性利用资源的原因——Map和Reduce两阶段可以复用同一批资源。
3.3 三种调度器:FIFO、Capacity和Fair怎么选
YARN内置了三种调度策略,很多教程一笔带过,但选错调度器在实训中会造成"资源饥荒"。
FIFO是最简单的队列调度,谁先来谁先占资源,前一个作业没跑完,后一个只能等着。适合单用户、任务串行的场景,但多人共用集群时体验很差,先提交的大作业可能把后面的小作业卡死几十个小时。
Capacity Scheduler按队列划分资源,比如给A队列50%、B队列30%、C队列20%,每个队列内部再按FIFO或优先级调度。这样即使A队列有大作业在跑,B队列的小作业也能得到自己的那份资源。这是当前Hadoop默认使用的调度器(Apache Hadoop 3.x默认就是Capacity)。
Fair Scheduler追求"所有作业尽可能平均得到资源"。它没有固定分配比例,而是根据当前运行的作业数动态调整。如果只有一个作业,它可以用全部资源;新作业来了,两个作业各自分一半;多个作业时资源会按权重轮转。这种调度器适合多用户、任务长短不一的共享集群。
实训选择建议:如果你只是单机或三台机器的伪分布式,用默认的Capacity就行;如果做课程设计,多个人共用一个集群,建议改成Fair,体验会好很多。改配置位置是yarn-site.xml里的yarn.resourcemanager.scheduler.class。
4. MapReduce的计算模型:从Map到Reduce的完整数据流
MapReduce的鼻祖思想来自Google的论文,核心就是把分布式计算抽象成两个阶段:Map阶段负责并行处理原始数据,生成中间结果;Reduce阶段负责把中间结果按key聚合,输出最终结果。框架帮你搞定任务的拆分、调度、容错,你只需要写业务逻辑。
4.1 移动计算还是移动数据?MapReduce的设计哲学
MapReduce最反直觉的设计是:框架不是把数据拉到程序里,而是尽量把程序推到数据所在的节点上跑。因为移动程序很快,移动大量数据很慢。举一个数字:如果输入数据有10TB,分布在100台机器上,把所有数据拉到一台机器计算,光网络传输就是天文数字;而如果让每台机器只算自己本地的1TB数据,最后再合并结果,网络开销就小得多。
HDFS和YARN正是为这个哲学服务的。HDFS保证每个块有3副本,YARN的调度器在分配container时,会优先把它分配到数据块所在的节点(这叫数据本地性),如果实在无法满足,才退而求其次放到和块同机架的节点上跨机架读。
Map阶段天然适合并行:一个1GB的文件有8个块(按128MB算),HDFS因此会生成8个split,对应8个MapTask,分散到不同节点上同时算。每个MapTask只处理自己对应的块,互不干扰。Reduce阶段则相反,它必须"Shuffle"所有MapTask输出的相同key的数据,才能算出全局结果。
4.2 Map到Reduce的中间发生了什么:shuffle与sort
很多初学者把shuffle和sort当作MapReduce的黑魔法,其实拆开看就几件事:分区(partition)、排序(sort)、合并(combiner)、拉取(fetch)。完整的数据流是这样:
- MapTask从输入split中逐行读取key-value,调用map()函数处理,产生的输出先写入内存缓冲区(默认100MB)。缓冲区不是满了才处理,而是达到阈值(默认80%)就会启动一个后台线程,把数据溢写(spill)到本地磁盘。
- 溢写前,框架会把缓冲区里的数据按"分区号"分组,每个分区对应一个ReduceTask。每个分区内部再按key排序。如果写了Combiner,会在此时做一次局部合并,减少写到磁盘的数据量。
- 溢写多次后,磁盘上会有多个spill文件,MapTask结束前会把这些文件归并(merge)成一个大的输出文件,同时保持分区和排序规则不变。这个最终文件会留在MapTask所在节点的本地磁盘上,不会写入HDFS,因为它是中间数据,丢失了可以重新计算。
- ReduceTask从所有MapTask的输出文件里,把自己负责那个分区的数据下载到本地磁盘。这个过程叫fetch,默认5个并发拉取线程,边拉边归并排序。
- 所有数据拉齐并排序完毕后,ReduceTask把同一key的一组key-value交给reduce()函数处理,输出结果由OutputFormat写入HDFS。
这里有个常见误解:以为shuffle发生在HDFS上。实际上Map输出的中间数据都存在节点本地磁盘,HDFS只管最终结果的存储。这也解释了为什么Map阶段结束前ReduceTask不能启动——因为Reduce需要所有MapTask都产出了输出文件才能开始拉数据。
4.3 编程实例:WordCount的Java实现与运行逻辑
WordCount是MapReduce的"hello world",代码不长,但足够说明编程模型。下面是一个标准的Java实现,适合在实训中直接改着用。
java复制public class WordCount {
// Mapper类:输入是文本行,输出是 (单词, 1)
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
protected void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one);
}
}
}
// Reducer类:输入是 (单词, 所有1),输出是 (单词, 总数)
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class); // 使用Combiner减少shuffle数据量
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
注意第三行注解里的CombinerClass,这个设计很多新手会忽略。Combiner本质上是一个在Map端先做一次局部Reduce的函数,它可以把大量"(单词, 1)"先合并成"(单词, N)",从而大幅减少Map输出写到磁盘以及Reduce拉取的数据量。前提是reduce函数满足交换律和结合律,WordCount正好满足,所以可以直接把Reducer类当Combiner用。如果业务逻辑不满足这个条件(比如求平均值),就不能直接复用,需要单独写一个Combiner类。
4.4 数据倾斜在哪个环节爆发
MapReduce最经典的性能问题就是数据倾斜。所谓倾斜,是指大量相同key的数据集中在同一个ReduceTask上处理,导致其他Reduce早就跑完了,就这一个卡在那里。典型场景包括:按地域分组时,北京的数据占了80%;按用户分组时,某个爬虫用户产生了海量日志。
倾斜发生在shuffle阶段之后。ReduceTask拉到自己分区里的数据后,会发现某个key的value列表特别长。比如"北京"这个key有几百万条记录,reduce()函数要对这几百万条做循环处理,时间自然被拖长。更麻烦的是大key可能造成reduce端内存溢出。
缓解手段有几个:第一,增加ReduceTask个数,这只能缓解整体并行度,但大key还是压在同一个Task上;第二,对key做加盐处理,比如给key加上随机后缀分散到多个Reduce,算完之后再按原key合并一轮;第三,使用Combiner尽量在Map端压缩数据。实训中排查倾斜最快的方法,是看作业的总耗时,再去看每个Reduce Task的counter,如果某个reduce处理的数据量是其他reduce的好几倍,基本就是倾斜了。
5. 一个WordCount作业的完整旅途:HDFS+YARN+MapReduce如何协同
前面把三个组件分开讲,现在把它们串起来。我以wordcount为例,从你敲下提交命令开始,完整追踪一条数据在里面走过了哪些环节。这个视角能帮你把前面所有知识点黏合成一张图。
5.1 客户端提交作业,第一站去哪里
你执行hadoop jar wordcount.jar WordCount /input /output后,客户端首先做几件准备动作。第一,把作业需要的jar包、配置文件、输入路径等打包,提交给ResourceManager,同时把输入路径下的数据split信息也算好。split的划分规则是:每个128MB块对应一个split,但如果有多个小文件,每个小文件可能单独split,这会导致MapTask数量变多。
ResourceManager收到请求后,会先把它放入调度队列,然后选择一个空闲NodeManager启动ApplicationMaster。AM启动之后,作业才真正开始"自己管自己"。注意,此刻还没有任何MapTask在跑,AM做的第一件事是从分布式缓存里拉取作业的资源文件,然后解析输入路径,和NameNode确认输入数据的块位置,用于后续的资源本地性计算。
5.2 MapTask怎么从HDFS上拿到数据
AM向ResourceManager申请到container后,会在每个container节点上启动MapTask。MapTask启动时,它会拿到一份"我要处理哪个split"的指令,这个split对应HDFS上的某个块。MapTask通过HDFS的读取流程,向NameNode询问该块的位置,然后建立输入流读取数据。如果这个container恰好跑在块副本所在的节点上,读取走的是本地磁盘IO,非常快;如果不在,就得通过网络读,速度慢几个量级。
所以在实际运行中,你会看到MapTask有两种状态:RUNNING和RUNNING but data not localized,后者就是当前节点没有数据副本,必须跨网络拉取。这个信息在YARN的Web UI上能看到,也可以从日志里找split的host列表来验证。AM在申请资源时已经尽量做本地化,但有时节点资源不够,只能分配非本地container,这是正常现象。
5.3 Shuffle完成中间数据的交接
每个MapTask处理完自己的split后,输出文件留在本地磁盘,同时向AM汇报"我已经产出成果了,我的输出有3个分区,分别放在这个路径下"。AM把这些信息登记到自己的内存里,相当于一张分发表。
ReduceTask启动后,先从AM那里拿到这张分发表:每个MapTask所在的节点、输出文件路径、各个分区怎么对应。然后ReduceTask发起fetch,去每个MapTask节点拉取自己负责分区的数据。拉取使用的协议是简单的HTTP GET,所以MapTask节点上还会启动一个辅助线程,用来响应ReduceTask的请求。
这里有一个值得注意的点:ReduceTask的所有输入数据都来自MapTask的本地磁盘,而MapTask完成任务后,如果这个节点被重新分配去跑其他作业,本地数据是会删除的。所以如果ReduceTask拉取失败,它不能简单的"重试拉一次",而是要通过AM联系是否重新运行某个失败的MapTask。这就是MapReduce容错机制的一部分——中间数据丢失后,通过重放计算来恢复。
5.4 最终结果写回HDFS,作业才算真正结束
ReduceTask的reduce()函数输出后,OutputFormat会把结果写到输出目录。默认是写part-r-00000这样的文件,有多少个ReduceTask就有多少个part文件。写入过程同样走HDFS写入流程,每个输出块都要写3份才返回成功。
作业结束时,FileOutputFormat还有一个隐藏操作:校验输出目录是否为空、是否有临时文件。如果你设置输出目录已经存在,作业会在启动时直接报错,这也是MapReduce默认不允许覆盖输出的原因。这个设计让多次运行同一作业时不会不小心覆盖之前的结果,但也意味着每次跑作业前都得手动删除旧输出目录,实训中很多人在这上面栽过。所有ReduceTask都写完并且汇报完成后,AM会向ResourceManager注销,整个作业的状态变为FINISHED。此时你再去hdfs里看/output目录,才能看到最终的part文件。
6. 实训中容易踩的坑与几个调优技巧
讲完原理,最后说点实际的。我见过太多同学照着教程搭完集群,跑个示例作业,不是这里报错就是那里卡死。下面这些坑都是我在课程设计和实习里实际踩过的,按出现的概率排个序。
6.1 默认配置的"水土不服"
本地跑伪分布式时,Hadoop默认允许的内存、CPU核数都很小。如果你用虚拟机的默认配置,可能跑起NameNode和DataNode后资源管理器就快满了。最常遇到的异常是Container killed by the ApplicationMaster或Container is running beyond physical memory limits。原因往往是默认的mapreduce.map.memory.mb和yarn.nodemanager.resource.memory-mb没调过。我经常建议,如果在虚拟机上实训,把NodeManager能用的内存设成物理内存的70%,并把MapTask和ReduceTask的堆内存调小,至少先保证任务不被系统杀掉。
还有端口问题。Hadoop的各个组件默认端口很容易被其他服务占用,比如50070在Hadoop 3.x变成了9870,如果你照着老教程打开错误的Web UI地址,会觉得"我的集群是不是挂了"。建议一次配好,把core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml里的主机名和端口都检查一遍。
6.2 小文件太多,MapTask数爆炸
MapReduce生成MapTask的数量,直接取决于输入split的数量。如果你有一个目录,里面有1万个小文件(每个只有几KB),HDFS会把每个文件当做一个split,作业就会有1万个MapTask。每个MapTask启动、初始化、读取数据都有固定开销,这样作业总开销远大于实际计算量,跑得非常慢。
缓解办法:第一,用hdfs dfs -put上传数据前先在本地合并文件;第二,用CombineFileInputFormat把多个小文件组合成一个split,这是最常见的解决方案;第三,如果是自己写工具类,可以选择先做一次MapReduce把小文件合并成大文件。原理很简单:控制split数量就控制了MapTask数量,也就控制了资源消耗和任务调度开销。
6.3 使用Combiner前,先确认reduce逻辑是否可交换
前面提到Combiner能提升很多性能,但它不是免费的。Combiner相当于在Map端对同一个key的多个value做一次预聚合,然后才真正shuffle给ReduceTask。如果你的reduce逻辑是"求和"或"取最大值",没问题,因为多步局部求和再汇总求和,结果一致。但如果你的reduce逻辑是"求平均值",就不能直接复用。比如两个MapTask分别产出(key, [2, 4])和(key, [6, 8]),局部求平均会得到(key, 3)和(key, 7),再求平均是5,但全局平均值应该是(2+4+6+8)/4=5。有时候碰巧对一个分区来说是对的,但从算法上不可靠。所以Combiner的正确用法是:可以结合的场景一定用,不能结合的单独为它写一个"局部汇总"函数。
6.4 怎么用日志和Counter快速定位作业问题
作业跑挂后,第一反应是去YARN Web UI上找日志,而不是盯着客户端那截报错看。进入ResourceManager页面,找到对应application,点击logs,能看到Container的stdout和stderr。注意MapTask和ReduceTask日志是分开的,如果某个Reduce失败了,你直接在"Reduce Tasks"栏目里点失败的记录看对应日志。
另一个好用的工具是JVM自带的内存和GC日志。可以在mapred-site.xml里加上-verbose:gc等JVM参数,观察Task的内存回收情况,帮助判断map或reduce是不是频繁FULL GC导致性能骤降。还有各种Counter,比如HDFS_BYTES_READ、MAP_INPUT_RECORDS、REDUCE_OUTPUT_RECORDS,它们可以帮你快速判断数据量是否符合预期。如果reduce输出records明显比map输出records少很多,先想想是不是map输出key就不对,或者reduce过滤条件太狠了。
最后再分享一个小技巧:用hdfs dfs -tail -f监控MapReduce的part文件写入情况。如果你发现某个part文件大小一直不涨,而其他part文件在正常增长,说明对应的ReduceTask可能卡在某个处理上。这时候去日志看GC日志或者fetch进度,往往能定位到数据倾斜或者单条记录处理超时。**记住,光看客户端打印的进度条是看不出来问题的,必须进到YARN的日志层才能看到真相。**这就是我为什么一直强调要把HDFS、YARN、MapReduce当一条链路去排查,而不是各查各的。
