☰
Hadoop三节点集群搭建与MapReduce个性化开发避坑实践
2026/10/14 13:00:12 网站建设 项目流程

简介:一份面向大数据初学者和Hadoop入门者的实战型操作指南,详细演示从零完成集群部署与MapReduce程序开发,解决步骤零散、易出错的问题。内容以三个任务模块展开:集群搭建部分包括在VM虚拟机中安装Ubuntu Kylin系统、更新apt、配置SSH免密登录、安装Java与Hadoop、规划主机名和IP映射、设置分布式相关参数并运行分布式实例;MapReduce开发部分涵盖Eclipse安装、Hadoop-Eclipse-Plugin配置、HDFS文件操作、MapReduce项目创建与运行;最后整理了集群启动时出现的网络路由异常、数据节点未启动、内存溢出等典型错误的原因与处理方法。全文为单个Word文档文件,共1个文件,整体容量约12.37MB。每个步骤均配有完整代码和解释,既适合新手按顺序实操,也方便有基础者快速查阅关键配置和排错经验。已有1047人学习下载,可作为大数据入门和课程设计的参考资料。

1. 集群和MapReduce为什么总要一起调:那些“本地能跑,集群不行”的老问题

不少实训项目的标题都叫“Hadoop集群搭建部署与MapReduce程序关键点个性化开发”,听起来像是先搭好集群、再写几个样例程序就完事。但真正上手会发现,集群启动成功和MapReduce作业顺利跑完之间隔着一大段空白:伪分布式模式下明明能出结果,一到三节点集群上就出现reduce一直Pending、Container反复被杀、自定义分区后任务结果全挤到一个Reducer里。这些问题不在配置阶段暴露,偏要在写代码、打包、提交之后才跳出来,排查起来特别费劲。

这篇记录按我现在实际做实验的顺序来讲:先搭一个最小三节点集群,再讲MapReduce作业里那些容易忽略的关键参数和个性化改造点,最后把避坑记录按“现象、原因、解决”拆开,方便你在类似问题上直接照方抓药。适合正在做课程设计、实训作业,或者刚接手Hadoop数据分析任务的入门工程师。

2. 三节点集群搭建部署:节点角色、版本和配置文件一次讲清

多节点集群不需要一上来就上高可用,先掌握基础部署流程,之后要扩展ZooKeeper和高可用再单独加组件。常见做法是一台主节点加两台从节点,主节点跑NameNode和ResourceManager,从节点跑DataNode和NodeManager,主节点也可以同时参与存储和计算。这样规模虽小,但能完整暴露HDFS和YARN两套体系的配置问题。

2.1 节点分工与版本组合:先决定谁当主、谁干活

我的规划表如下,三台机器都使用Linux,统一创建hadoop用户,所有Hadoop相关进程都用这个用户启动,避免权限不一致导致的诡异问题。

主机名角色建议内存
masterNameNode、ResourceManager、JobHistoryServer,额外跑DataNode和NodeManager8GB
node1DataNode、NodeManager4GB
node2DataNode、NodeManager4GB

版本组合方面,我一般固定为JDK 8配Hadoop 3.3.x。Hadoop 3.x对JDK 8的支持最稳,JDK 11以上虽然能跑,但部分依赖包和编译选项要额外调试,对于实训场景不值得。操作系统用CentOS 7或Ubuntu 20.04都行,关键是三个节点的主机名、系统时间、/etc/hosts要一致,不然NodeManager注册和DataNode汇报会互相找不到。

提示:这里不涉及ZooKeeper整合或NameNode高可用,先把单NameNode集群跑通,再做HA时会少一半干扰项。

2.2 四个配置文件加一个workers文件:参数为什么这样设

Hadoop集群配置的核心看得懂四份XML就胜利了一大半:core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml,另外还要在workers文件里写上从节点列表。下面按顺序来,每份配置我都写成可以直接用的最小版本。

第一份是core-site.xml:

<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://master:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop/data/tmp</value> </property> </configuration>

fs.defaultFS告诉所有客户端和进程,“默认文件系统在master的9000端口”,写错主机名会导致DataNode往错误地址注册。hadoop.tmp.dir是集群元数据和数据块的根目录,后面hdfs-site.xml里的NameNode、DataNode目录都会基于它生成。生产环境通常把tmp.dir放到单独数据盘,实验环境无所谓,但目录权限必须属于hadoop用户。

第二份是hdfs-site.xml:

<configuration> <property> <name>dfs.replication</name> <value>2</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/opt/hadoop/data/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/opt/hadoop/data/datanode</value> </property> </configuration>

dfs.replication在三节点上设为2,既能保证一块数据有副本,又不要求三个节点全部在线才能写入。namenode.name.dir和datanode.data.dir是实际数据落盘位置,新搭建时一定保证这两个路径为空。如果你之前做过伪分布式搭建,身上残留的旧目录没清理,格式化后多半要出clusterID不一致问题,具体见后面避坑章。

第三份是yarn-site.xml:

<configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>master</value> </property> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>4096</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>2</value> </property> </configuration>

yarn.resourcemanager.hostname要指向主节点,不能配成0.0.0.0或localhost,否则从节点拿到地址后根本连不上。nodemanager.aux-services固定为mapreduce_shuffle,这是Map任务和Reduce任务之间数据交换的通道,漏掉这一项,Map跑完Reduce永远起不来。resource.memory-mb和cpu-vcores分别限制每个NodeManager能分配多少内存和虚拟核,我这几台机器都是8GB内存,所以给NodeManager留4GB,剩下4GB给操作系统和DataNode。如果你机器只有4GB,这个值就要压到2GB左右,否则启动后物理内存耗尽会OOM。

第四份是mapred-site.xml:

<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <property> <name>mapreduce.jobhistory.address</name> <value>master:10020</value> </property> <property> <name>mapreduce.jobhistory.webapp.address</name> <value>master:19888</value> </property> </configuration>

这里最关键的是mapreduce.framework.name必须等于yarn。默认值是local,如果只照抄了伪分布式的某些片段、漏了这一行,MapReduce任务会在提交端本地跑完,YARN界面上完全看不到作业,集群也感知不到压力,整个部署等于白做。jobhistory配置是给后续查日志用的,地址写master。

最后是workers文件,位于$HADOOP_HOME/etc/hadoop/workers,内容写三行:

master node1 node2

在Hadoop 3.x里这个文件名是workers,旧版本叫slaves。start-dfs.sh和start-yarn.sh启动时,会逐个读这个文件、通过SSH到对应节点拉起脚本。很多同学启动后只在master上看到进程,原因就是workers文件没有同步到所有节点,或者里面写了注释符、空格、多余乱码。

2.3 免密、格式化与启动:格式化为什么只能做一次

三个节点统一安装完JDK和Hadoop并配置好环境变量后,第一步做SSH免密。在master上生成密钥,然后把公钥分发给master自己、node1、node2:

ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa ssh-copy-id hadoop@master ssh-copy-id hadoop@node1 ssh-copy-id hadoop@node2

生成密钥时-P ''表示空口令,这样脚本启动远端进程时不用反复输入密码。ssh-copy-id需要提前配好三个节点的/etc/hosts,否则只能用IP。免密做完先手动ssh到node1和node2确认能直接登录,再往下走,这是排查启动失败最快的一步。

下一步是格式化NameNode,然后启动整个集群:

hdfs namenode -format sbin/start-dfs.sh sbin/start-yarn.sh jps

格式化只在初次搭建时执行。它会在namenode.name.dir下生成一个current目录,内含集群唯一的clusterID。之后如果因为配置变更想“重新格式化”,必须先把所有节点上的namenode和datanode目录删干净,再执行格式化,否则DataNode进程会反复启动失败,日志里写着一句类似Incompatible clusterIDs的信息。至于为什么,避坑章节详细展开。

启动后jps能看到这些进程才算正常:master上有NameNode、ResourceManager、DataNode、NodeManager、JobHistoryServer四种,node1和node2上有DataNode、NodeManager。看到之后再在master上执行一条写入测试:

hdfs dfs -mkdir -p /demo/input hdfs dfs -put /etc/profile /demo/input/

能正常创建目录和上传文件,说明HDFS侧通了,之后正式进入MapReduce开发环节。

3. MapReduce程序关键点开发:作业参数、自定义Writable与排序分组的组合

集群状态正常只是前提,MapReduce能不能按你的业务逻辑跑出结果,取决于代码里几个容易被忽略的设置。常见做法是在IDEA里面建工程、写好Driver、打成jar后再交到YARN;这里绕不开依赖管理、作业参数和自定义类型这三个坎,我逐个讲。

3.1 开发环境和依赖:为什么IDEA能跑,打完包就不行

开发环境用IDEA还是Eclipse都无所谓,但pom.xml里依赖范围要匹配好。以Maven为例,最小化依赖是引入hadoop-client,版本与集群Hadoop版本保持一致:

<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.3.6</version> </dependency>

我这边集群装的是3.3.x,所以pom里也写3.3.6。这样开发阶段可以在本机以local模式直接run一个作业,用于验证Mapper和Reducer的纯逻辑。但注意,本地跑通不等于集群能跑,原因很直接:Maven默认打包的jar只包含你写的类,不会把hadoop-client这些传递依赖打进去。集群节点上确实装了Hadoop,但它的Hadoop类库路径和你本地引入的版本未必完全一致,一旦出现冲突或缺失,运行时立刻报ClassNotFoundException或NoClassDefFoundError。

所以我的习惯是开发期用IDEA直接run,调试函数逻辑;准备提交前用Maven打包,并且把最终提交的jar固定为一个“薄jar”,依赖交给YARN的libjars机制或shade插件的胖包。瘦jar提交有一条命令值得记住:

hadoop jar demo.jar com.demo.WordCountDriver /demo/input /demo/output -libjars lib/guava-31.1-jre.jar

-libjars会把这些jar提交到各个NodeManager的本地工作目录,加载进classpath。这样既避免fat jar体积过大,也减少了依赖版本冲突面的扩大。

3.2 Driver和作业参数:4个必调设置

很多MapReduce示例代码只写了Mapper和Reducer,真正决定成败的其实是Driver里的配置。拿WordCount为例,一个能提交到YARN的Driver至少要注意下面几行:

Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "word count demo"); job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.class); job.setCombinerClass(WordCountReducer.class); job.setReducerClass(WordCountReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); job.setNumReduceTasks(2); boolean success = job.waitForCompletion(true); System.exit(success ? 0 : 1);

setJarByClass是必须的,它告诉框架去哪里找这个作业所在的jar,不写这条很多提交场景会直接报找不到类。setCombinerClass要不要设置需要想清楚:Combiner是Mapper端的本地Reducer,逻辑必须满足交换律和结合律,WordCount的求和可以加,求平均值就绝对不能加。setOutputKeyClass和setOutputValueClass设置的是Reducer输出类型,如果你的Mapper输出类型和Reducer不同,还要额外写setMapOutputKeyClass、setMapOutputValueClass,漏掉这一组,框架按默认类型反序列化时会报类型转换异常。

FileOutputFormat.setOutputPath这一步有个经典陷阱:输出目录已存在时会直接拒绝执行并抛FileAlreadyExistsException。这是HDFS保护数据的一种方式,不是报错,只需要提交前把同名输出目录删掉或换成新目录。

最后是setNumReduceTasks。不设置时默认只有一个Reduce任务,数据全部进一个Reducer,集群的并行优势完全发挥不出来。但也不是越大越好,Reduce任务数和输出part文件数量一一对应,后面第3.3节的自定义分区还会和它关联。

3.3 个性化开发:自定义Writable、分区器、分组比较器

实训项目里提“关键点个性化开发”,最常见的落点是按业务需求自定义键、自定义分区、自定义排序和自定义分组。以订单数据汇总为例,需求是把同一个用户的订单分到同一个Reduce里,并且在该Reduce内按时间升序排列,最终输出这个用户的所有订单摘要。这个需求必须同时使用WritableComparable、Partitioner、GroupingComparator三个自定义组件。

首先定义复合键OrderKey:

public class OrderKey implements WritableComparable<OrderKey> { private Text userId = new Text(); private LongWritable orderTime = new LongWritable(); public void set(String userId, long orderTime) { this.userId.set(userId); this.orderTime.set(orderTime); } @Override public void write(DataOutput out) throws IOException { userId.write(out); orderTime.write(out); } @Override public void readFields(DataInput in) throws IOException { userId.readFields(in); orderTime.readFields(in); } @Override public int compareTo(OrderKey o) { int c = this.userId.compareTo(o.userId); if (c != 0) return c; return Long.compare(this.orderTime.get(), o.orderTime.get()); } }

write和readFields里的字段顺序必须一一对应,字段顺序不一致会把整个反序列化结果读歪,这是自定义Writable最常见的玄学问题。compareTo实现了先按用户ID排序、再按时间排序的规则,这一步会直接影响进入Reducer前的全排序结果。

接着定义分区器,让同一个用户ID进入同一个Reduce:

public static class UserIdPartitioner extends Partitioner<OrderKey, IntWritable> { @Override public int getPartition(OrderKey key, IntWritable value, int numPartitions) { return (key.getUserId().hashCode() & Integer.MAX_VALUE) % numPartitions; } }

注意hashCode可能返回负数,用& Integer.MAX_VALUE把负数位去掉,否则模出来的分区索引可能为负,运行时直接抛ArrayIndexOutOfBoundsException。

再定义分组比较器,把同一个用户的多条记录当作一组传给Reducer的同一个reduce方法调用:

public static class UserGroupComparator extends WritableComparator { protected UserGroupComparator() { super(OrderKey.class, true); } @Override public int compare(WritableComparable a, WritableComparable b) { OrderKey oa = (OrderKey) a; OrderKey ob = (OrderKey) b; return oa.getUserId().compareTo(ob.getUserId()); } }

这个类最容易被忽略的细节是构造器里必须调用super(OrderKey.class, true),第二个参数true表示让框架为比较器创建其实例。如果你写的是无参构造器且漏了super,运行时会收到空指针或比较失败,而且日志不一定指向这个类。分组比较器只影响同一分区内如何把相邻键合并成一组,不影响分区,也不影响键的全排序。

Driver里把它们串起来:

job.setMapperClass(OrderMapper.class); job.setReducerClass(OrderReducer.class); job.setMapOutputKeyClass(OrderKey.class); job.setMapOutputValueClass(IntWritable.class); job.setPartitionerClass(UserIdPartitioner.class); job.setGroupingComparatorClass(UserGroupComparator.class); job.setSortComparatorClass(OrderKey.class); job.setNumReduceTasks(3);

setSortComparatorClass如果是直接写成OrderKey.class,框架会调用OrderKey里的compareTo方法,也就是按用户ID和时间两个维度排序。setGroupingComparatorClass另走一套比较规则,只比较用户ID。两者各司其职,这个组合正是二次排序和分组统计落地的标准结构。在实训题里会看到“MapReduce排序—自定义排序”“MapReduce排序—分组排序”这类关卡,本质上都是在同一个框架下套不同比较器。

4. 集群部署和作业开发避坑记录:五条高频问题按现象、原因、解决拆开

这一章把我在部署和调试过程中遇到最多、也最容易让人原地转圈的问题集中列出来。代码写完不代表结束,能定位报错、看日志、修正参数才算真正掌握。

4.1 格式化后DataNode起不来:NameNode的clusterID对不上

现象:start-dfs.sh正常执行,但jps显示node1和node2上的DataNode进程消失,或反复启动后停止。日志中Storage directory … is not formatted,或DatanodeRegistration出现Incompatible clusterIDs。

原因:NameNode执行过多次格式化。每次格式化会生成新的clusterID,而DataNode第一次启动时已经把旧clusterID写进了自己的datanode.data.dir,之后NameNode换了ID,DataNode来注册时对不上,就被拒绝了。这是实训环境里最典型的翻车现场,而且是在重装或重置集群时最容易踩。

解决:如果数据不重要,最干脆的做法是把所有节点的数据目录清掉再格式化:

# master、node1、node2 上都要执行 rm -rf /opt/hadoop/data/namenode rm -rf /opt/hadoop/data/datanode # 只在 master 上执行 hdfs namenode -format sbin/start-dfs.sh

清理前一定确认数据不需要保留,这个操作没有后悔药。

4.2 作业一直Pending:YARN调度器等不到资源

现象:作业提交到YARN后,ResourceManager页面里Application状态一直在ACCEPTED或RUNNING,但Container实际没有分配,进度条长时间停在0%。

原因:优先在ResourceManager的8088页面看Active Nodes数量是否为2。如果Active Nodes为0,说明NodeManager没有成功注册,多半是前面提到的aux-services配错或yarn.resourcemanager.hostname配错。如果Active Nodes正常但Container不分配,则是资源空余量不够:单个Container requested的资源比NodeManager剩余额度大,调度器要等之前作业释放。

解决:确认每个NodeManager可分配总量与单个作业申请量的关系。例如yarn.nodemanager.resource.memory-mb设为4096,mapreduce.map.memory.mb默认1024,一个作业请求4个Map,内存共4GB,而当前NodeManager还同时跑着DataNode和其他服务,剩余不够,调度就会一直等待。把内存调低、或者等前一批作业结束,问题通常会消失。

4.3 Container killed:物理内存和虚拟内存超限

现象:任务日志中出现Container killed on request. Exit code is 143,以及physical memory near limit或virtual memory exceeded。Map或Reduce跑到一半被杀,重试多次仍然失败。

原因:YARN对每个Container都做了内存上限检查,Container实际使用超过申请额度就会杀进程。常见于mapreduce.map.memory.mb设置过小,比如默认1024MB,而你的JVM启动参数-Xmx也偏高,堆外内存加上对象开销一超就被检查机制干掉。另一种情况是Linux虚拟内存计费偏高,让很多代码量大的任务“无端”超限。

解决:在mapred-site.xml里把单个Map和Reduce的Container内存调大:

<property> <name>mapreduce.map.memory.mb</name> <value>2048</value> </property> <property> <name>mapreduce.reduce.memory.mb</name> <value>2048</value> </property> <property> <name>mapreduce.map.java.opts</name> <value>-Xmx1536m</value> </property> <property> <name>mapreduce.reduce.java.opts</name> <value>-Xmx1536m</value> </property>

java.opts里的堆大小要低于Container的memory.mb值,堆外内存、Metaspace、网络缓冲都要占空间,一刀切等于没调。如果是测试环境排查时间紧张,可以先临时把虚拟内存检查关掉:

<property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property> <property> <name>yarn.nodemanager.pmem-check-enabled</name> <value>false</value> </property>

关掉检查只是跳过限制,生产环境别这么干,内存翻车会让整个节点卡死。

4.4 本地IDEA能跑,集群提交就ClassNotFoundException

现象:同样的数据,在IDEA里右键run能正常完成,打包后hadoop jar提交却报java.lang.ClassNotFoundException,有时定位到自己的类,有时定位到第三方库类。

原因:IDEA运行时会把所有依赖按classpath加载,而打包生成的jar只包含项目自身代码。hadoop-client相关类在集群上可能也存在,但自定义的三方库比如Fastjson、Guava、Jackson在集群节点上没有,因为集群Hadoop发行版不会带这些库。

解决:优先使用Maven的shade插件把依赖打进同一个jar:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.demo.OrderDriver</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>

如果不想打胖包,就在提交时把依赖用-libjars带上。还要核对pom里的Hadoop版本和集群版本一致,不一致时即使类名存在,方法签名也可能对不上,报错格式类似NoSuchMethodError。

4.5 小文件太多,Map数量成百上千地膨胀

现象:输入目录里存了上千个几百KB的小文件,提交作业后Map任务数量等于目录下的文件数量,光调度就花掉好几分钟,NameNode负载也明显升高。

原因:FileInputFormat默认按文件切分输入,每个文件至少生成一个InputSplit,就算文件比HDFS块小很多也不会合并。小文件产生的Map任务大多是空转,没处理多少数据却占满了资源槽。

解决:写入端尽量合并成大文件,这是根治。已有的小文件可以用CombineFileInputFormat合并输入分片:

job.setInputFormatClass(CombineFileInputFormat.class); CombineFileInputFormat.setMaxInputSplitSize(job, 256L * 1024 * 1024);

这个类会把相邻的小文件放进同一个分片,Map数量不再跟着文件数走,而是跟着总数据量走。要注意的是,CombineFileInputFormat下同一Split可能包含多个文件,如果你的业务逻辑要求Mapper完整拿到单个文件全量数据,默认实现不满足,需要自行包一层MultiFileInputFormat,这时改造复杂度明显上升。

5. 从“会跑”到“会调”:用日志、Web界面和计数器验证你的集群

作业能跑完只是第一步,真正要验证的是“结果对不对、资源用没用到位”。

先看YARN的ResourceManager Web界面,默认端口8088,打开后能看到每个Application的Map和Reduce进度、已分配内存、队列信息。如果页面里一个作业都没有,先回头检查mapred-site.xml里framework.name是否还是local。再看HDFS的Web界面,Hadoop 3.x默认9870端口,能确认NameNode、DataNode以及数据副本数是否符合预期。这两个页面能解决80%的“作业是不是真的在跑”的疑问。

要看作业内部日志,用命令行比页面快得多:

yarn logs -applicationId application_1667889000000_0012 | grep -E "ERROR|Exception"

applicationId可以从ResourceManager页面或yarn application -list里拿到。排错时不能只看最后几行,要把stderr和stdout分开看:程序自己的System.out输出进stdout,JVM异常和日志记录进stderr。出现数据倾斜或处理不符合预期时,我会先在Reducer里加计数器,把过滤掉的记录数和有效输出数分开统计:

context.getCounter("MyCounter", "filteredRows").increment(1L); context.getCounter("MyCounter", "validRows").increment(1L);

作业跑完后在控制台输出里找到MyCounter组,直接看两个数值的差距,就能判断过滤逻辑有没有把不该丢的数据丢掉。这个方法比反复改代码重跑快得多,尤其适合做数据清洗和统计分析类实训项目。

我个人的习惯是:每次修改配置或代码后,先记录当前的输入文件数、输入大小、Reduce任务数、运行时长,再改下一个参数。否则调了五六个地方,最后跑出个意外结果,根本不知道是谁引起的。集群搭建和MapReduce开发这类事情,最大的成本不是命令不会写,而是排查方向反复横跳。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询