部署Hadoop集群这件事,我在2018年第一次做是照着一篇教程机械复制,结果光是配置环境变量就踩了一整天坑。后来在多个版本切换、重装、增删节点、甚至误格式化NameNode之后,才慢慢形成一套自己的部署流程。这次分享的是Hadoop 2.7.3的完整集群部署、配置和环境变量调优记录,面向需要快速搭一套稳定多节点集群的同学,也适合准备运维面试时系统梳理部署链路。重点会放在“为什么这样配”和“哪些地方容易出事”,而不是粘贴官方文档,希望你看完能自己独立重演一遍,别像我当年那样每步都要问搜索引擎。
选择2.7.3而不是3.x或新版本,不是因为我没有用过新版,而是因为存量业务和周边生态往往被旧版本绑定。很多内部系统、ETL组件、Spark版本、甚至二次开发源码都基于2.x编写,盲目升3会带来一系列兼容性适配成本。写这篇文章时我手里维护的集群也仍是2.7.3,生产运行稳定,因此把它的部署细节和调优心得完整整理一遍。
1. 部署前的硬件、软件和版本规划,为什么我仍推荐2.7.3
1.1 先想清楚集群规模,再动手下载安装包
很多新手最容易犯的错误是拿到安装包就开始配置,等到启动服务时才发现内存不够、主机名不对、磁盘目录冲突。我建议动手前先画一张拓扑草图,明确几个问题:总共几台机器?哪台跑NameNode?哪台跑ResourceManager?DataNode和NodeManager是否同节点混布?
对于入门级实验集群,常见方案是1台主节点加2台数据节点,主节点同时跑NameNode、SecondaryNameNode和ResourceManager,数据节点跑DataNode和NodeManager。如果是单机学习,可以只用伪分布式模式,但本文章重点讲多节点,因为伪分布式的坑常常到真实集群才会爆发。
内存规划上,NameNode建议至少2GB堆内存,DataNode分配1GB,ResourceManager和NodeManager各分配1GB。如果机器物理内存只有8GB,那么跑3节点的实验集群是可行的,但需要严格控制每个服务的堆大小,并且关闭不必要的GUI或桌面环境。
磁盘规划同样重要。HDFS数据目录和操作系统、日志目录分开挂载,避免日志写满根分区导致系统崩溃。另外DataNode的数据盘最好有独立挂载点,这样格式化时不用担心误删系统文件。
1.2 版本选型:JDK、操作系统、Hadoop 2.7.3的兼容性
Hadoop 2.7.3 官方文档强调支持 Java 7和Java 8。实操下来,JDK 1.8.0_131之后的版本比较稳,太新的JDK比如11、17在2.7.3上会有反射类访问报错,因为当时Hadoop内部大量使用sun包,新版JDK默认不允许未开放模块被访问。因此,如果坚持用2.7.3,就不要在JDK版本上追求新。
操作系统我使用CentOS 6.10和7.9都成功部署过。CentOS 6需要额外处理/etc/hosts中IPv6问题,CentOS 7相对容易。不建议用CentOS 8以上,原因是自带openssl版本和Hadoop的libhadoop组件可能存在不兼容。如果你手上只有Ubuntu,也可以跑,但需要额外安装libssl等兼容库,很多教程省略了这步导致启动失败。
还有一个容易忽略的地方:Hadoop启动脚本会读取/etc/java软链接或JAVA_HOME。部分发行版的Java包不会自动创建/etc/java,所以必须显式在hadoop-env.sh里写死JAVA_HOME。截图路径时,最好用readlink -f $(which java)得到真实路径,因为Java经常安装在带有版本号的目录里。
1.3 节点规划、主机名、IP映射与统一用户设计
节点命名要有规律,比如node01、node02、node03。在/etc/hosts中把每个节点的IP和主机名写全,注意主节点和子节点都要写,不要只改一台。很多datanode连接不上namenode,就是因为子节点不知道namenode的主机名,或者namenode的hosts把自身映射成localhost导致。
统一运行用户也很关键。我习惯创建hadoop用户,所有节点使用相同uid,并授权这个用户免密登录到所有节点。为什么不用root?因为Hadoop官方明确不推荐root运行,不仅存在脚本目录权限安全问题,还可能触发容器执行时的某些保护机制。
另外还要统一系统时间。跨节点的RPC通信对时间敏感度不高,但Kerberos、ZooKeeper相关功能如果后续要上,时间偏差会被放大。建议配置NTP同步,哪怕只是crontab里每10分钟执行一次ntpdate也行。
2. 下载解压与最基础的三项环境设置:JDK、SSH、目录规范
2.1 JDK安装与环境变量的三处联动
安装JDK建议使用tar.gz包,不要用系统仓库安装。因为系统安装的openjdk默认安装路径可能在不同版本变化,导致Hadoop脚本找不到。我推荐手动解压到/usr/local/jdk1.8.0_131,然后配置/etc/profile.d/java.sh:
export JAVA_HOME=/usr/local/jdk1.8.0_131 export PATH=/usr/local/jdk1.8.0_131/bin:$PATH配置完执行source /etc/profile,再用java -version验证。这里有个细节:Hadoop脚本默认会读JAVA_HOME,但某些脚本还会检查JAVA_HOME/bin/java是否存在。如果你用export JAVA_HOME=/usr/lib/jvm/java-1.8.0这种写法,软链接本身没问题,但在hadoop-env.sh里一定要确保路径不再套软链接,否则在个别服务启动时可能出现“找不到java命令”。
所有节点都要保持JDK路径一致,最好连安装路径都一模一样。如果你已经建好了/usr/local/jdk1.8.0_131,子节点直接从这里打包复制,能省去很多环境不一致的麻烦。
2.2 创建Hadoop用户与SSH免密登录的最终形态
先找一个节点作为主节点,创建用户:
useradd hadoop passwd hadoop然后生成SSH密钥,确保主节点可以免密登录自己以及所有子节点:
su - hadoop ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys再把这个公钥复制到所有子节点的authorized_keys中,同时子节点也要生成各自的密钥并加入自己的authorized_keys。实验经验:即使不要求子节点主动反向免密登录主节点,也建议在两两之间都配好,因为后续执行hdfs命令可能从任意节点发起。
验证免密的关键是首次执行ssh node02不出现密码输入且不出现“yes/no”确认。如果出现主机密钥确认,说明~/.ssh/known_hosts没有提前写入,可以在首次连接前执行ssh-keyscan node02 >> ~/.ssh/known_hosts。
2.3 下载、校验与目录拆分
从Apache官网下载hadoop-2.7.3.tar.gz后,先做MD5或SHA校验,尽管是实验环境,也值得养成习惯。解压时统一放到/opt/hadoop,然后创建软链:
cd /opt tar xzf hadoop-2.7.3.tar.gz ln -s hadoop-2.7.3 hadoop目录拆分方面,我会单独建三个目录:
/opt/hadoop/tmp,存放Hadoop临时文件、pid文件;/opt/hadoop/logs,存放所有日志;/opt/hadoop/hdfs-data,存放元数据和数据块。
不要将这三种内容混在默认目录里,否则日志写多了磁盘爆掉,数据目录也容易被误清。每个节点的数据目录独立规划,建议在hdfs-site.xml中指定。
3. 六大核心配置文件的角色分工与逐项详解
3.1 先弄清这六个文件分别管什么
Hadoop 2.7.3的配置集中在$HADOOP_HOME/etc/hadoop下,分别有6个关键文件:
hadoop-env.sh:Java环境变量、脚本守护进程的堆内存设置。core-site.xml:全局文件系统配置,如默认文件系统、临时目录、IPC设置。hdfs-site.xml:HDFS参数,包括副本数、NameNode和DataNode存储路径、块大小、HA相关配置。yarn-site.xml:YARN资源调度、ResourceManager地址、NodeManager资源的配置。mapred-site.xml:MapReduce作业调度框架,决定作业运行在YARN上还是本地。slaves:列出所有DataNode/NodeManager主机名。
很多部署失败的根源是某一个文件里的主机名写错,或端口不统一。我在实践中习惯先在Excel里列出所有节点的IP、主机名、角色,然后按这个表来写配置,避免边写边忘。
3.2 从core-site.xml开始固定文件系统入口
core-site.xml最核心的配置是fs.defaultFS,它决定HDFS客户端访问的NameNode地址。比如主节点是node01端口9000:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://node01:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop/tmp</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>node01:2181,node02:2181,node03:2181</value> </property> </configuration>这里hadoop.tmp.dir是NameNode和DataNode存储元数据时的默认根目录,许多初学教程图省事放在/tmp下,但/tmp在系统重启后可能被清空,就再也起不来集群了。一定要放在独立持久目录。fs.defaultFS使用主机名而不是IP的好处是后续迁移或切换HA时,只需改hosts指向,不用反复改xml。
注意端口的选择。9000是HDFS RPC默认端口,也会有人改成8020。如果端口被其他服务占用,务必检查netstat。很多新手在这卡住,以为怎么配都连不上。
3.3hdfs-site.xml:副本数、块大小与目录布局
hdfs-site.xml的第一关键参数是dfs.replication。三节点实验集群建议设2,这样既能容忍单节点故障,又不至于因为副本数大于节点数而出现块处于UNDER_REPLICATED状态。如果你的集群是2节点,副本数就只能设为1,否则它会一直报找不到额外节点。
NameNode元数据目录dfs.namenode.name.dir建议设成两个不同的挂载路径,但实验室环境只要指定一个。DataNode数据目录dfs.datanode.data.dir可以逗号分隔写多个磁盘,比如/disk1,/disk2,Hadoop会自动选择空闲多的盘。
dfs.namenode.secondary.http-address指定SecondaryNameNode的HTTP地址,一般放在主节点,端口5000。如果不配置这项,默认有可能是0.0.0.0:50090和实际不匹配。检查Web UI是否能访问NameNode页面时,多半与此有关。
一个典型的hdfs-site.xml片段:
<configuration> <property> <name>dfs.replication</name> <value>2</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/opt/hadoop/hdfs-data/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/opt/hadoop/hdfs-data/data</value> </property> <property> <name>dfs.namenode.secondary.http-address</name> <value>node01:50090</value> </property> </configuration>3.4yarn-site.xml与mapred-site.xml:资源调度桥接
YARN的ResourceManager默认会跑在所有节点,但必须指定yarn.resourcemanager.hostname为真正的主节点。否则每个子节点都会启动一个ResourceManager尝试抢占。相同的问题也出现在yarn.nodemanager.resource.memory-mb,默认会读取宿主机内存的75%作为该节点可用内存,实验环境如果内存只有8G,很可能把资源算爆。建议显式设成较小的值,例如4GB,同时设置虚拟内存与物理内存比率,避免作业频繁被杀死。
mapred-site.xml必须设置mapreduce.framework.name=yarn,否则MapReduce作业会把YARN忽略掉,退回本地跑,失去分布式能力。此外,MapReduce作业的内存配置与YARN资源设置要联动,例如mapreduce.map.memory.mb和mapreduce.reduce.memory.mb的值不要超过容器的最大内存。
3.5 从模板复制、替换文本到生效
每个配置文件的原始模板在etc/hadoop/*.xml.template,正式使用时需要复制为*.xml,并把模板内容替换成自己的配置。这里我习惯先复制整个目录,再逐个改:
cd $HADOOP_HOME/etc/hadoop cp core-site.xml.template core-site.xml cp hdfs-site.xml.template hdfs-site.xml cp mapred-site.xml.template mapred-site.xml cp yarn-site.xml.template yarn-site.xml改完后用diff对比所有节点上的配置文件是否一致。多节点同步配置,可以用scp手工复制,也可以在后续用Ansible或rsync统一管理。我个人最惨的教训就是漏同步了某个节点的hdfs-site.xml,结果NameNode格式化了两次还找不到DataNode。
4. 格式化NameNode、启动集群、验证HDFS/YARN的完整流程
4.1 格式化之前必须确认的三件事
格式化NameNode是高风险操作,会让HDFS集群进入全新的文件系统状态,并清空之前的所有元数据。如果集群已经运行过,必须清楚自己在做什么。我每次格式化前都会反复确认下面三件事:
- 是否已经停止所有Hadoop进程?如果还有DataNode残留,格式化后它们的clusterID和NameNode不一致,启动时会报“Incompatible clusterIDs”。
dfs.namenode.name.dir指定的目录是否包含旧数据?如果数据不重要,可以直接清空;如果重要,至少备份到其他目录。- 是否所有节点都使用了相同的格式化后的元数据?格式化命令只需在主节点执行一次,然后启动所有节点,由NameNode广播集群状态给DataNode。
执行命令前,建议先确认所有节点配置完全一致,再在node01执行:
hdfs namenode -format4.2 启动与停止的顺序逻辑
启动顺序必须是NameNode和SecondaryNameNode先起,再起DataNode;YARN方面则先启动ResourceManager,再启动NodeManager。因为它们之间存在注册关系,从节点启动时会寻找主节点注册,若主节点还没起来会表现为连接失败,虽然不是致命的,但会拖延启动时间。
全启动可以使用脚本:
$HADOOP_HOME/sbin/start-dfs.sh $HADOOP_HOME/sbin/start-yarn.sh脚本会按slaves文件中的主机名依次启动各节点进程。如果某些节点启动失败,脚本并不会重试,输出日志中可以看到异常。停止时顺序相反:
$HADOOP_HOME/sbin/stop-yarn.sh $HADOOP_HOME/sbin/stop-dfs.sh要注意脚本运行的前提是SSH免密成功,且HADOOP_HOME与HADOOP_CONF_DIR环境变量在远程登录后依然有效。很多场景下,远程不会加载你的/etc/profile.d/hadoop.sh,因此最好把必要变量写入~/.bashrc。
4.3 首次验证:jps、Web UI、dfsadmin与wordcount
启动完成后,在每个节点执行jps,观察进程是否齐全:
- node01:
NameNode、SecondaryNameNode、ResourceManager(如果配置了) - node02/node03:
DataNode、NodeManager
如果发现某个进程没有,立即去对应日志logs目录查看错误,而不是盲目重启。
HDFS Web UI默认地址是http://node01:50070,YARN ResourceManager UI是http://node01:8088。配置为2.7.3时,可能看到页面显示“NameNode is in SafeMode”,一开始不必害怕,因为HDFS启动后默认会进入安全模式几分钟,等待数据块上报完毕后自动退出。可以通过命令查看:
hdfs dfsadmin -safemode get若长时间卡在安全模式,多半是副本不健康或DataNode没有正常注册。
接下来丢一个实测WordCount任务。先用传统命令上传测试文件:
echo "hello hadoop world hello hdfs" > /tmp/test.txt hdfs dfs -mkdir -p /tmp/input hdfs dfs -put /tmp/test.txt /tmp/input/然后运行官方自带样本:
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.3.jar wordcount /tmp/input /tmp/output任务跑完后,用hdfs dfs -cat /tmp/output/part-r-00000查看结果。能跑通这个流程,说明HDFS读写、YARN调度、节点心跳都正常,部署基本成功。
5. 环境变量调优:从默认参数到性能可观测
5.1 HADOOP_HOME、JAVA_HOME与PATH的冗余设置
环境变量看似简单,却是我见过最容易出问题的环节。Hadoop启动脚本在读取环境变量时,除了从~/.bashrc读取,还会去hadoop-env.sh里再读一次。如果你在/etc/profile.d里设置,但远程SSH启动脚本时没有加载,就会身影失踪。
所以我的习惯是:
- 在
/etc/profile.d/hadoop.sh写入export HADOOP_HOME=/opt/hadoop和export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH - 在
hadoop-env.sh中再次写死export JAVA_HOME=/usr/local/jdk1.8.0_131 - 在
~/.bashrc里加载source /etc/profile
通过冗余设置保证三种常见的启动方式都能正确找到路径。
5.2 堆内存与GC参数:不只在hadoop-env.sh里写数字
HADOOP_HEAPSIZE控制的是所有Hadoop守护进程的默认堆大小。直接设置它是有用的,但会缺少NameNode之类的特定优化参数。更精细的做法是通过HADOOP_NAMENODE_OPTS、HADOOP_DATANODE_OPTS等单独指定:
export HADOOP_NAMENODE_OPTS="-Xmx4g -Xms4g -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/opt/hadoop/logs/gc-namenode.log"NameNode堆大小和集群元数据规模强相关。对于百万级文件对象,2GB可能不够;主要观察NameNode UI上的内存使用曲线,不要等OOM了才去找原因。-Xms和-Xmx设成一致,能避免JVM运行时扩容导致的阶段卡顿,这是我事后复盘GC日志发现的。GC参数中,2.7.3时代我非常推荐CMS而不是G1,因为在NameNode场景下CMS的Full GC更可控。JDK8以后的默认GC在不同版本下还不一样,最好显式指定。
DataNode的堆内存同样要限制,防止数据块传输产生的大量短生命周期对象把堆塞满。NodeManager则相对宽松,但需要关注容器占用总内存是否大于物理内存,否则会触发系统OOM。
5.3 系统级参数:文件句柄、进程数与内存虚拟化
Hadoop进程数比较多,且会打开大量TCP连接和文件句柄。部署阶段必须调高系统参数。
/etc/security/limits.conf加入:
hadoop - nofile 65536 hadoop - nproc 65536同时修改/etc/sysctl.conf:
fs.file-max = 6815744 vm.swappiness = 10 vm.overcommit_memory = 1vm.swappiness建议设为10或更小,减少交换对JVM带来的停顿。vm.overcommit_memory=1可以避免某些情况下Hadoop启动时申请大内存失败。注意,这些参数需要重启或执行sysctl -p才能生效。
还要关闭防火墙或精确放行端口。本地实验可以直接关闭firewalld和iptables服务,但如果生产环境不允许,请至少放行9000、50070、8088、50010、8030、8020等关键端口。我曾经因为只有主节点的防火墙开着,导致DataNode无法注册,排查了快半小时才用telnet发现在端口被拦。
5.4 调优后如何验证参数是否真正生效
修改完参数后不要只在日志里自嗨。有两个简单方法验证:
- 查看进程的启动命令行:
ps -ef | grep NameNode观察-Xmx和GC参数是否已出现在Java进程参数中。
- 查看NameNode日志中的启动信息:
cat /opt/hadoop/logs/hadoop-hadoop-namenode-node01.log | grep 'Heap'日志会打印初始堆大小。另外,访问http://node01:50070/dfshealth.html查看NameNode JVM内存指标,可以看到堆使用率与GC次数。
6. 部署中的典型问题,以及我踩过的坑的排查链路
6.1 DataNode启动后自动退出的定位流程
这种问题几乎算得上Hadoop部署第一坑。现象是jps能看到DataNode进程,但几秒后进程消失,NameNode UI里的Live Nodes列表为空。日志中常见报错是java.io.IOException: Incompatible clusterIDs,或者“Initialization failed for Block pool ...”。
定位链路如下:
- 查看最近的
datanode日志,通常路径是/opt/hadoop/logs/hadoop-hadoop-datanode-*.log。 - 如果出现clusterID不一致,那么需要将每个节点的
dfs.datanode.data.dir下的current/VERSION文件内容与NameNode的VERSION文件对齐。 - 解决办法有两种:一是删除DataNode目录下的所有数据重新注册,但传输过的数据会丢;二是手工修改所有DataNode的VERSION文件中的
clusterID为NameNode的clusterID。使用第二种前务必备份原文件。
另外,如果日志显示Could not find or load main class,则是HADOOP_CLASSPATH配置残缺;显示Connection refused则是端口或防火墙问题。
6.2 格式化后clusterID不一致的具体修复过程
我曾在测试集群格式化后直接重启,结果DataNode一直报错。原因是格式化NameNode时,会在NameNode状态目录生成一个随机的clusterID,但DataNode的VERSION文件保留了旧的clusterID。除非格式化时添加-force和-clear选项,否则两者不会自动同步。
简单可靠的解决方法是:停掉所有DataNode,在所有数据节点上删除/opt/hadoop/hdfs-data/data/current目录,然后重新启动DataNode。这样新的DataNode会从NameNode拉取当前的clusterID。如果HDFS里存着业务数据,这种方法会丢失所有块,所以生产环境不要轻易格式化。
6.3 内存不足导致的NameNode启动失败
有一次我在一个4GB内存的节点上部署,NameNode启动后立刻退出。日志显示There is insufficient memory for the Java Runtime Environment to continue。原因是默认的HADOOP_HEAPSIZE=1000即1GB,再加上元数据缓存和其他JVM开销,机器物理内存就爆了。
修复思路不是一味调小堆内存,而是要分清机器内存和JVM堆内存的概念。我当时把NameNode的-Xmx降到512MB,虽然能启动,但小文件多时频繁Full GC。最后增加交换空间,并把其他无关服务停掉,才勉强稳定。经验是:如果要做正经测试,主节点内存不要低于8GB,同时统一设置HADOOP_HEAPSIZE=2048,避免每个守护进程都去申请1GB。
6.4 JPS与PID文件:那些隐藏的进程残留教训
服务停止脚本偶尔杀掉不干净,导致端口被占用。重新启动时,新进程绑定失败,旧进程却占着位置。这时jps可能看不到旧进程?其实jps能看到的进程名是以主类名为准的,偶尔也会看到僵尸缓存。排查时使用:
netstat -tlnp | grep 9000 lsof -i :9000如果发现PID对应的进程不属于任何Hadoop脚本,直接用kill -9清理。同时检查/opt/hadoop/tmp下的*.pid文件,如果进程已经不存在但pid文件还在,删除它们再启动。
这类问题表面上是“启动失败”,实际是进程管理不到位。建议写一个简单的集群运维脚本,把start-dfs.sh、start-yarn.sh、jps、netstat、tail logs组合在一起,至少能省一半排障时间。
7. 部署常态化之后,我给后来者的三点核心建议
第一,版本和配置必须统一。哪怕只是某个节点的hdfs-site.xml少了dfs.datanode.data.dir,也会导致数据不均衡和异常退出。强烈建议写一个同步脚本,把主节点上除了数据目录和本地化设置外的所有配置统一推送到其他节点。
第二,从第一次部署就建立“基线环境变量清单”。把JAVA_HOME、HADOOP_HOME、HADOOP_CONF_DIR、HADOOP_HEAPSIZE、HADOOP_NAMENODE_OPTS等参数全部记录成文档。每次调整参数后,手动更新清单,不要等出问题再回忆上个月到底改了哪一行。
第三,不要怕看日志。启动失败后,第一个动作应该是打开对应角色的日志文件找异常堆栈。这比盲目改xml、反复重启的效率高一个量级。如果你能坚持从日志定位一次DataNode启动失败的问题,之后遇到任何集群故障都会从容很多。
我在实际部署中还有个习惯:装完集群后立刻跑一遍完整测试,包括上传大文件、运行MapReduce样例、重启集群恢复数据,确认各项指标都正常再交付。Hadoop 2.7.3虽然老,但把部署流程吃透以后,稳定性完全不输新版本,这也是它至今仍被许多系统使用的原因。希望这篇总结能让你少走几年弯路。