简介:大数据入门学习者和需要搭建开发环境的开发者,可借助该doc文档快速完成Hadoop集群部署与MapReduce开发的完整流程。内容从VM虚拟机中安装Ubuntu Kylin系统开始,依次覆盖SSH免密登录、Java环境、Hadoop安装、集群网络与分布式配置,再到Eclipse下Hadoop插件安装、HDFS文件操作与MapReduce项目创建运行,步骤连贯并在关键位置配有完整代码和解释。第3部分还专门总结了集群启动与运行中的典型问题,如No route to host、Too many fetch-failures、Java heap space、DataNode未启动等,便于读者对照排查。资源包为单份doc文档,约12.37MB,结构清晰,零基础用户按步骤操作即可复用。已有1047人学习下载,适合需要快速落地Hadoop环境并上手MapReduce开发的大数据初学者。
1. Hadoop集群部署与MapReduce开发:这份资源到底要解决什么
某公司的数据平台从单机日志处理切换到Hadoop,项目从零开始搭三节点集群,一开始所有人都以为难点在MapReduce代码,结果前两周全耗在环境、配置和参数调优上。Hadoop集群部署与MapReduce开发,真正的门槛不是启动脚本,而是搞清楚每个配置文件参数为什么这么写、出现故障时怎么定位。这份资源对应一套完整的集群搭建部署与MapReduce程序关键点个性化开发文档,核心价值在于把部署流程里的隐含条件和MR开发中的序列化、分区、数据倾斜这些硬骨头一次性讲透。适合第一次独立搭集群、需要跑通业务MapReduce作业、以及被集群故障折腾过的工程师。
2. 环境准备与选型:三节点集群从零开始的版本与免密细节
在动手部署Hadoop之前,先把基础动作压实。Hadoop不挑操作系统,但它对JVM版本、SSH互信、时间同步极其固执,这三件事没做对,后面的搭建会反复返工。
2.1 环境选型:物理机、虚拟机与容器的取舍
先给结论:学习、验证部署流程,用虚拟机最快。虚拟机平台自带快照功能,配置改坏了可以一键回滚,这就是后悔药。物理机性能最好,但重装系统成本高,不适合做部署练习。容器方案的资源隔离和网络配置都比较麻烦,HDFS这类依赖固定端口和主机名解析的服务,在容器里初期调试会额外消耗大量时间。
常见做法是准备3台虚拟机,操作系统选CentOS 7.x或Ubuntu Server 18.04以上,每台内存4GB起步、磁盘100GB、CPU至少2核。内存是第一个硬约束——NameNode和ResourceManager各自默认堆内存经常超过1GB,节点总内存如果低于4GB,DataNode和NodeManager很容易被系统OOM杀掉。磁盘100GB不只是给HDFS用,日志、临时文件、ZooKeeper快照都会占空间。
我一般会在搭建前用下面一组命令确认三台机器的硬件信息,避免装到一半才发现资源不够:
# 在三台机器上分别执行 free -g # 查看可用内存,单位GB,重点看Mem这一行 nproc # 查看CPU核数 df -h /data # 确认数据盘挂在哪个目录 hostnamectl set-hostname hadoop01 # 如果主机名不规范,先改掉参数说明:free -g按GB显示内存,方便直接判断是否达到4GB底线;hostnamectl改完主机名后一定要重新登录,Hadoop在启动时会解析主机名,用旧主机名会导致DataNode注册不上集群;/data是预留的数据目录,确保挂载在独立分区里,不要和系统根分区共用。
2.2 JDK与大版本匹配:装错版本连jps都没有
Hadoop是纯Java实现,JDK版本直接影响启动脚本能否正常工作。我部署Hadoop 3.x集群时用OpenJDK 8最稳,很多生态组件在JDK 11上也能跑,但匹配旧版本依赖时容易踩坑。如果是维护Hadoop 2.7.x老集群,认准JDK 8不要换。
装系统时如果顺手装了headless版JDK,一个很明显的问题是jps命令不存在。Hadoop所有启停脚本都靠jps探测进程状态,没有jps,start-dfs.sh执行完你根本不知道NameNode到底起没起,排查全靠猜,这体验非常痛苦。
我的做法是显式设置JAVA_HOME并验证真实路径:
# 以OpenJDK 8为例,安装后确认路径 java -version which java # 输出通常是 /usr/bin/java,用readlink穿透软链拿真实路径 readlink -f $(which java) # 结果类似 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java # 把JAVA_HOME写进 /etc/profile.d/hadoop_env.sh,所有节点统一执行 echo 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' > /etc/profile.d/hadoop_env.sh echo 'export PATH=$PATH:$JAVA_HOME/bin' >> /etc/profile.d/hadoop_env.sh source /etc/profile.d/hadoop_env.sh java -version逻辑说明:readlink -f是为了拿到Java的真实安装路径,防止把/usr/bin/java这种软链路径直接当成JAVA_HOME,否则Hadoop的脚本会在加载libjvm.so时报错。这个环境变量文件放到/etc/profile.d/下,SSH登录时自动生效,三个节点都要执行一遍。
2.3 SSH免密与时间同步:三个节点互信的细节
Hadoop的DataNode和NodeManager由主节点通过SSH拉起,三台机器之间必须双向免密。不只是hadoop01到hadoop02、hadoop03,HDFS HA场景下NameNode之间也要互信。很多人在这一步只做了单向免密,结果active节点切换时连接失败,进程起一半就挂掉。
# 在hadoop01上生成密钥并分发到所有节点 ssh-keygen -t rsa -b 2048 -N '' -f ~/.ssh/id_rsa # 把公钥追加到三台机器的authorized_keys ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop01 ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop02 ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop03 # 验证每个方向的免密登录 ssh hadoop02 hostname ssh hadoop03 hostname参数说明:-N ''表示不设置口令,否则后面SSH连接会卡在密码输入上;-b 2048是RSA最小可接受强度,-f直接指定密钥文件路径。ssh-copy-id会自动处理公钥追加和权限设置,不需要手动编辑authorized_keys。验证时如果还提示输入密码,第一件事检查目标机上~/.ssh目录和authorized_keys文件的权限,这两个文件权限过宽SSH会直接拒绝读取。
时间同步这一步经常被跳过,但HDFS的租约恢复依赖客户端系统时间。三台机器时间偏差过大,客户端写入的文件可能出现租约过期,NameNode进入安全模式后迟迟退不出来。常见做法是装chrony并指向同一个NTP源,配置很简单,不在这里展开,但这一步真的不要省。
3. 搭建部署与HA配置:HDFS、YARN的参数边界与启动验证
环境到位后进入核心配置。这一步最怕的不是不会写xml,而是不知道每个参数为什么这样设。我按目录规划、HDFS参数、YARN参数、HA配置这个顺序讲,每个参数都会说明边界。
3.1 目录规划与core-site、hdfs-site核心参数
先做目录规划,这是很多人数据目录和日志目录混在一起,最后磁盘满了都不知道被谁占掉的根源。我一般建三个独立目录:安装目录/opt/hadoop、数据目录/data/hadoop、日志目录/data/logs。元数据、数据块、运行日志分开,各管各的磁盘配额。
# 每个节点都执行 mkdir -p /opt/hadoop mkdir -p /data/hadoop/tmp mkdir -p /data/hadoop/namenode mkdir -p /data/hadoop/datanode mkdir -p /data/logs/hadoop # 解压Hadoop安装包到 /opt/hadoop tar -zxvf hadoop-3.3.x.tar.gz -C /opt/hadoop # 进入配置目录 cd /opt/hadoop/hadoop-3.3.x/etc/hadoopcore-site.xml里最重要的两个参数是fs.defaultFS和hadoop.tmp.dir:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://hadoop01:8020</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/data/hadoop/tmp</value> </property> </configuration>逻辑说明:fs.defaultFS是所有HDFS客户端访问的默认入口,写成hdfs://hadoop01:8020后,后续命令可以省略全量路径。hadoop.tmp.dir在默认配置里指向/tmp,Linux系统重启会清理/tmp目录,NameNode元数据一旦被清,整个集群直接报废。所以必须改到持久化目录。
hdfs-site.xml里的参数需要更谨慎,副本数、NameNode元数据目录、DataNode数据块目录、权限检查,四个参数决定集群长期稳定性:
<configuration> <property> <name>dfs.replication</name> <value>3</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>file:///data/hadoop/namenode</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>file:///data/hadoop/datanode</value> </property> <property> <name>dfs.permissions.enabled</name> <value>false</value> </property> </configuration>参数说明:dfs.replication设为3,三节点集群就是每个数据块存3份,这是物理节点数的上限,设成4以上反而会导致副本永远凑不齐。数据块默认128MB,小文件多的话建议降到64MB,但块大小改动会影响后续所有新文件的存储布局,部署前要一起想好。dfs.permissions.enabled测试环境我建议设false,省去处理权限报错的时间;生产环境必须开true。
3.2 YARN与mapred-site:内存、CPU的分配边界
YARN的参数是集群资源分配的中枢,很多任务卡在ACCEPTED状态都是因为这里配置不合理。yarn-site.xml里的核心参数包括ResourceManager地址和NodeManager资源上限:
<configuration> <property> <name>yarn.resourcemanager.hostname</name> <value>hadoop01</value> </property> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>3072</value> </property> <property> <name>yarn.nodemanager.resource.cpu-vcores</name> <value>2</value> </property> <property> <name>yarn.scheduler.minimum-allocation-mb</name> <value>512</value> </property> <property> <name>yarn.scheduler.maximum-allocation-mb</name> <value>2048</value> </property> </configuration>逻辑说明:4GB内存的节点,我给YARN分配3072MB,剩下1GB留给操作系统和DataNode,不然两个进程会抢内存,出现反复被OOM杀掉的情况。cpu-vcores设2代表该节点最多提供2个虚拟核,调度器再按任务需要切分。minimum和maximum allocation限定了单个任务能申请到的容器范围,minimum太小任务并发太多,maximum太大单个任务会占满所有资源。
mapred-site.xml需要确认两件事:MapReduce框架指定为YARN,以及Map和Reduce的JVM内存边界:
<configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <property> <name>mapreduce.map.memory.mb</name> <value>1024</value> </property> <property> <name>mapreduce.reduce.memory.mb</name> <value>1024</value> </property> </configuration>参数说明:framework.name写成yarn是必须的,如果默认的local,作业会直接运行在本地JVM而不是提交到集群,这会导致三台机器只有一台在干活。map和reduce内存上限要略小于调度器单容器上限,这里设1024MB,对应scheduler.maximum-allocation-mb的2048MB,给超配留出buffer。
3.3 高可用(HA):引入ZooKeeper后的启动序列
三节点集群如果单NameNode挂在hadoop01,hadoop01宕机整个HDFS就不可写。HA方案需要两台NameNode,再配合ResourceManager双活,通过ZooKeeper做故障切换。部署前先装ZooKeeper集群,三台机器各一个ZK节点。
# 每台机器解压ZooKeeper后,配置zoo.cfg并启动 /opt/zookeeper/bin/zkServer.sh start # 验证ZK集群状态 /opt/zookeeper/bin/zkServer.sh status # 三台中应有一台显示leader,两台显示follower<configuration> <property> <name>dfs.nameservices</name> <value>mycluster</value> </property> <property> <name>dfs.ha.namenodes.mycluster</name> <value>nn1,nn2</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn1</name> <value>hadoop01:8020</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn2</name> <value>hadoop02:8020</value> </property> <property> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://hadoop01:8485;hadoop02:8485;hadoop03:8485/mycluster</value> </property> <property> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property> </configuration>逻辑说明:nameservices是逻辑名,mycluster只是别名,作用是把两台NameNode的RPC地址注册到同一个命名空间下。shared.edits.dir必须写三台JournalNode的地址,所有元数据修改会通过JournalNode同步。automatic-failover.enabled开启后,ZKFC进程会和ZooKeeper通信,active节点失联时自动切换NameNode。
HA集群的启动顺序和单NameNode完全不同。第一次启动必须先起ZK集群,然后在一台节点上格式化NameNode,逐台起JournalNode,再启动NameNode并执行zkfc -formatZK。千万不要在没起ZK时就执行start-dfs.sh,否则HA状态不会注册到ZK,故障切换完全失效。
4. 避坑排查:集群起不来时的五个高频故障记录
部署和开发过程中踩过的坑,比任何文档都能说明问题。这里整理五个高频故障,直接对号入座。
4.1 现象一:NameNode进程在,但Web UI打不开
现象:jps能看到NameNode进程,但浏览器访问9870端口一直超时。
原因:多半是防火墙或安全组没放行HDFS相关端口,也可能是core-site.xml里绑定的地址写成了内网IP,而你的浏览器走的是另一个网段。
解决:先用curl -v http://hadoop01:9870看本机通不通,再排查外部链路。局域网环境我一般直接开放8020、9870、9866、8088几个核心端口。注意Hadoop 3.x的NameNode Web UI端口是9870,不是2.x时代的50070,按旧教程排查会白折腾。
4.2 现象二:DataNode一直显示Incompatible clusterIDs
现象:启动完成后,hdfs dfsadmin -report里只有NameNode,没有DataNode,日志里刷Incompatible clusterIDs。
原因:90%来自多次格式化NameNode。每次格式化会生成新的clusterID,但DataNode的数据目录里记录的还是旧的clusterID,两者对不上。
解决:如果集群还没有业务数据,最干脆的做法是清空所有DataNode的数据目录,再重新格式化NameNode。不要单独改某个DataNode的clusterID去凑,三台机器很容易改到不一致。格式化前务必确认:data目录里没有需要保留的数据,否则先备份再动手。
4.3 现象三:SSH免密配置了却还要输密码
现象:ssh hadoop02回车后还是提示输入密码,密钥配置看起来没有生效。
原因:.ssh目录权限不对,或者sshd_config里StrictModes严格要求权限。authorized_keys文件必须是600,~/.ssh目录必须是700,多一个组权限都会导致SSH拒绝有效。
解决:执行chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys,再重启sshd验证。如果用了ssh-copy-id还是不行,用ssh -v hadoop02看输出里的debug信息,哪里权限有问题它会直接指出来。
4.4 现象四:YARN任务一直ACCEPTED但就是不跑
现象:提交MapReduce作业后,状态一直停在ACCEPTED,过了很久都不进RUNNING。
原因:ResourceManager认为没有节点能满足容器资源请求。常见于NodeManager内存配得太大——4GB物理内存却给YARN配了4096MB,操作系统本身没内存了,NodeManager报错或心跳异常,调度器拿不到可用容器。
解决:把yarn.nodemanager.resource.memory-mb调成3072,并确认每台机器的可用内存确实达标。同时检查NodeManager日志里的Available resources行,看它上报给ResourceManager的剩余资源和调度器实际看到的是否一致。
4.5 现象五:NameNode进入安全模式,磁盘却还有余量
现象:NameNode日志显示安全模式,核查磁盘却发现数据目录还有空间。
原因:安全模式不只看磁盘剩余量,关键指标是DataNode上报的数据块比例。NameNode启动时会等待99.9%的数据块报告,如果某台DataNode进程异常、心跳断过,它就不会上报,NameNode迟迟退不出安全模式。
解决:先看hdfs dfsadmin -report确认所有DataNode是否正常,再执行hdfs dfsadmin -safemode get查看当前状态。确认数据完整后,可以临时用hdfs dfsadmin -safemode leave退出。但正确做法是先恢复DataNode心跳,而不是强行退出——我见过有人反复强制leave,结果副本数不够,最后还是重建副本收场。
5. MapReduce开发实战:从WordCount到自定义Writable与分区
集群跑起来只是开始,真正干活的是MapReduce程序。这个阶段不只是抄一遍WordCount,实际业务里的ETL、统计、去重,都会遇到序列化、分区、数据倾斜三个坎。
5.1 Mapper、Reducer与Driver:WordCount的完整骨架
先看最标准的WordCount骨架,它把MapReduce的组件串起来:
public class WordCount { 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); } } } public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> { @Override public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable val : values) { sum += val.get(); } context.write(key, new IntWritable(sum)); } } 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); 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); } }逻辑说明:Mapper的输入key是行偏移量,value是整行文本,输出Text和IntWritable;Reducer把相同key的所有value累加。Driver里setCombinerClass指定了Combiner,本质是本地Reducer,能在map端提前聚合,减少shuffle数据量。System.exit返回0表示成功,非0表示失败,调度平台依赖这个退出码判断作业结果。
编译和提交命令如下:
# 编译,需要引入hadoop-client依赖 javac -classpath $(hadoop classpath) WordCount.java jar cf wordcount.jar WordCount*.class # 提交到集群,输入输出路径都写在HDFS上 hdfs dfs -put /tmp/input /data/input hadoop jar wordcount.jar WordCount /data/input /data/wordcount_out hdfs dfs -cat /data/wordcount_out/part-r-00000参数说明:hadoop classpath会自动展开所有Hadoop依赖jar包,不需要手动写几十个jar路径。输出目录一定不能提前存在,MapReduce作业遇到已存在输出目录直接报错。执行完以后检查part-r-00000文件,这是Reducer写入的标准文件名。
5.2 自定义Writable:不只是接口,还有序列化顺序
业务里经常要输出结构化对象,比如日志解析出来的IP、请求路径、响应时间三个字段。Hadoop没有现成的三字段类型,最稳的做法是实现Writable接口自定义类型:
public class LogWritable implements WritableComparable<LogWritable> { private Text ip = new Text(); private Text path = new Text(); private IntWritable latency = new IntWritable(); @Override public void write(DataOutput out) throws IOException { ip.write(out); path.write(out); latency.write(out); } @Override public void readFields(DataInput in) throws IOException { ip.readFields(in); path.readFields(in); latency.readFields(in); } @Override public int compareTo(LogWritable o) { // 按响应时间倒序,再按IP升序 int cmp = Integer.compare(o.latency.get(), this.latency.get()); if (cmp != 0) return cmp; return this.ip.compareTo(o.ip); } }逻辑说明:自定义类型最关键的是write和readFields的顺序必须完全一致。这里先写IP,再写path,最后写latency,反序列化时也必须按相同顺序读,任何不一致都会导致数据错位或EOF异常。compareTo方法决定排序规则,这段代码实现了按响应时间降序,业务端拿到的输出就是最慢的请求排在最前面。
有个细节要注意:实现WritableComparable而不是只实现Writable,是为了让这个类型能作为Map的输出key并参与排序。如果只实现Writable接口,它只能当value用——这个区别经常被忽略,等发现排序结果不对时已经跑了很久的数据。序列化类型选择的对照关系我列在下面:
| 业务数据类型 | Java原生类型 | Hadoop Writable类型 |
|---|---|---|
| 文本 | String | Text |
| 整数 | Integer | IntWritable |
| 长整数 | Long | LongWritable |
| 浮点数 | Double | DoubleWritable |
| 布尔 | Boolean | BooleanWritable |
5.3 Partitioner与Combiner:数据倾斜的两道防线
数据倾斜是MapReduce最典型的性能杀手。某项目里有个统计任务,95%的请求集中在少数几个热门IP上,Reduce阶段单节点负载拖垮整个作业。解决思路是用自定义Partitioner,把热门key拆散:
public class IPPartitioner extends Partitioner<Text, IntWritable> { @Override public int getPartition(Text key, IntWritable value, int numPartitions) { // 只对IP最后一段做哈希,打散同网段的主机 String ip = key.toString(); String[] parts = ip.split("\\."); int last = Integer.parseInt(parts[3]); return Math.abs(last * 31) % numPartitions; } }逻辑说明:默认Partitioner对整串key做hash,相同IP必然进同一个Reduce。改成取IP最后一段参与哈希,同一个网段的不同主机就能分散到不同分区。注意getPartition的返回值必须落在0到numPartitions-1范围内,否则任务会直接崩溃。
判断数据倾斜可以先看Reducer的输入大小,在MR控制台或HistoryServer里检查每个Reduce收了多少MB数据。如果某一个Reducer的输入是其它Reducer的10倍以上,基本就是倾斜,优先考虑自定义Partitioner或两阶段聚合。
Combiner的使用有一条线:只有聚合操作满足交换律和结合律时才能用。WordCount的累加没问题,但求平均值不能随便用Combiner,因为多个局部平均值拼不回全局平均值。我见过有人把求中位数的逻辑挂在Combiner上,最终结果偏离了百分之三四十。两阶段聚合的完整实现我放在配套文档里,适合单key占比极高的极端场景。
6. 上线前最后一步:用Counter和Log验证你的MR作业
6.1 Counter与日志:把作业运行状态暴露给排查
作业跑完不报错,不代表结果正确。MapReduce自带的Counter是最快的数据质量验证工具。我在每个业务Mapper里会增加几个自定义Counter,统计输入行数、过滤掉的异常行数、输出记录数。执行完成后在控制台或HistoryServer里直接看这几个数字,如果异常行数占比异常,说明上游数据格式或清洗逻辑有问题。日志则是排查数据倾斜的入口,在Mapper里用log.info输出key的分布,观察哪些key的Value非常多,比猜测哪里倾斜可靠得多。
6.2 个性化输出与调参习惯:从日志里找到优化信号
多个维度的业务结果写同一个目录,会给下游读取带来压力。用MultipleOutputs把不同类别的数据拆分到独立目录,下游任务只需要扫描自己关心的子目录,单个Reduce输出文件过大的问题也能缓解。这个技巧适合日志按天、按渠道、按状态码分流的场景。从那以后我每次上线MR任务,都强制走一遍Counter核对、日志采样、分区均衡检查的流程,没有验证完坚决不交付结果,希望帮到你。配套文档里包含了三节点HA部署脚本、自定义Writable完整示例和两阶段聚合代码,照着跑一遍就能把整套流程串起来。
本文还有配套的精品资源,点击获取