1. 先把版本和环境这件事说清楚
前几天有个做数据平台的朋友找我,说手上有一批老作业需要在新机器上重跑,环境要求固定在 hadoop-3.1.0 这一版,问我有没有现成的安装清单可以直接参照。我把当年的笔记翻出来,顺手在几台虚拟机上又跑了一遍,踩坑记录也更新了一轮。这套流程到现在依然有参考价值:hadoop-3.1.0 安装这件事看着套路很固定,但真正决定你后面三天是顺利跑作业还是天天对着日志发呆的,恰恰是安装过程中那几个不起眼的配置项。
先把话说清楚:这篇文章不是搬运官方文档。它面向的是手里有 2 到 4 台机器(物理机、虚拟机、云主机都行)、准备搭一套能跑离线批处理的 HDFS + YARN 小集群的人。整套内容包含版本的选取逻辑、系统层的准备工作、六个配置文件逐行拆解、内存与并发参数的计算过程、第一次格式化和启动的完整链路,以及一堆只有真的动手装过才会遇到的报错。新手照着抄能跑通,有经验的人可以重点看第 3 章的参数计算和第 5 章的排查表。我尽量把每个动作背后的意图都讲清楚,因为安装 Hadoop 最怕的就是“别人这么写我就这么抄”,抄完出问题完全没有排查方向。
1.1 hadoop-3.1.0 在今天的定位:为什么还有人锁这个版本
先聊一个很多人忽略的前提:为什么偏偏是 3.1.0。这个版本是 Hadoop 3 系列早期的一个正式版,它的意义在于把 3.x 的基础架构定型了——NameNode 默认 RPC 端口从 9000 改成 8020、Web UI 从 50070 改成 9870、内置了纠删码相关的代码路径、支持多 NameNode 的极简部署方式、默认文件系统路径的写法也和 2.x 有细微差别。很多存量资料、课程实验、历史作业都是在 3.1.0 上验证过的,所以它成了一个事实上的“基准版本”。
这一点直接决定了安装时的两个判断。第一,网上大量 2.x 时代的教程不能直接套用,尤其是端口号、配置文件名(2.x 的 slaves 在 3.x 里改叫 workers)和启动脚本里的用户变量,照抄会撞出一堆莫名其妙的错误。第二,JDK 必须锁在 8。3.1.0 不认 JDK 7,也没准备好迎接 JDK 11(那个支持要到 3.3 之后才逐步完善),你把 JAVA_HOME 指到 17 上,启动脚本大概率直接报反射或者模块访问相关的异常,而且报错位置离真正的原因很远,特别容易误判成配置写错了。
还有个实际问题:3.1.0 的官方二进制包在归档区,下载下来记得核对一下包完整性。我习惯在解压前比对一次校验值,虽然麻烦,但省得后面出现“jar 包损坏导致 ClassNotFound”这种查半天查不出来的问题。
1.2 三种部署形态怎么选:单机、伪分布式、完全分布式
装之前先定形态,不然配置写完才发现方向不对,返工成本很高。三种模式的差别不在于装法,而在于你希望验证什么。
本地模式就是解压完把 fs.defaultFS 设成 file:///,所有进程跑在一个 JVM 里,用本地文件系统冒充 HDFS。它适合第一次确认二进制包本身没问题、环境变量能读到——装完之后跑一个内置的 pi 例子,十秒钟就知道 JDK 和包有没有问题。
伪分布式把所有守护进程起在同一台机器上,NameNode、DataNode、ResourceManager、NodeManager 各自独立进程,走的是真正的网络端口。这是绝大多数人真正需要的形态:它和完全分布式的配置字段几乎完全一样,只是主机名都写同一个。学配置、调试作业、验证端口连通性,伪分布式足够,而且不需要考虑 SSH 免密、时间同步、防火墙这些跨机问题。
完全分布式才是生产形态,至少一台主节点加两台从节点,需要解决免密登录、主机名互相解析、端口放通、时钟一致。我建议的顺序是:先在一台机器上把伪分布式跑通,确认 Web UI、上传下载、作业提交全链路正常,然后把这份配置原样复制到多台机器,只改主机名和 workers 文件。这个顺序能帮你把“配置错误”和“网络问题”两类故障彻底分开,不然两台机器混着调,你根本分不清是哪一类。
我见过不少人一上来就上三台集群,结果免密没配好、hosts 写错一个字母,卡一整天。伪分布式先通关,是性价比最高的做法。
1.3 上机前必须拍板的四个前置项
动手写配置文件之前,有四个决定必须先落地,它们会渗透到你后面写的每一行配置里。
主机名和 IP 映射。定好一台主节点和若干从节点,主机名建议用有语义的短名,比如 master、worker1、worker2,不要用 localhost 或者临时拼出来的乱名。每一台机器的 /etc/hosts 里必须写全所有节点的“IP + 主机名”,且主从一致。这一步看着琐碎,但它是后面百分之八十“连不上”类故障的根源。
JDK 路径。定好装在哪,比如 /usr/local/jdk8,路径里不要带空格和中文,也别用软链接指来指去——Hadoop 的启动脚本会把这个路径透传给子进程,中间任何一层解析失败都会变成难读的报错。
数据目录规划。NameNode 元数据目录、DataNode 数据目录、YARN 本地目录、日志目录,建议统一放到一个独立的数据盘路径下,比如 /data/hadoop/ 下面分几个子目录。把 hadoop.tmp.dir 默认放在 /tmp 下面是新手最常犯的错,机器一重启数据就没了,然后你就看到一个空集群。
运行账号。用一个专用普通账号跑最省事;如果用 root 跑,3.x 的启动脚本会拦你,需要额外设置一组用户变量,这个在第 3 章会详细说。
2. 环境准备:系统层做不干净,后面全是玄学
装 Hadoop 有一句老话:八成的安装失败不是 Hadoop 的配置错了,是系统层没准备好。我现在的习惯是先把下面这四件事做完再解压安装包,做完之后后面几乎不会出现“玄学问题”。
2.1 主机名、防火墙、SELinux 与时间同步
主机名的设置要注意两点:一是用 hostnamectl 永久改,不要只改当前会话的临时名,否则重启之后名字变了,配置里写的主机名就解析不上;二是改完之后必须重新登录一次,因为很多脚本读的是登录时缓存的主机名。
/etc/hosts 的写法有个细节,很多人漏掉。每台机器除了写所有节点的映射,还要保证自己那台机器的“主机名”能解析到本机的实际 IP,而不是 127.0.0.1。如果你的 hosts 里出现127.0.0.1 master这样的行,NameNode 启动后会绑定到回环地址,从节点的 DataNode 就永远连不上它,表现是 NameNode 的 Web UI 里活节点数是 0,日志里刷 Connection refused。
防火墙的处理原则很简单:实验环境直接关掉,生产环境按端口放通。需要放通的端口在下一节会列。别抱着“先开着试试”的心态,Hadoop 的节点间通信端口不少,逐个试会让你浪费大量时间在排除法上。
SELinux 也要看一眼状态,处于 enforcing 时可能导致目录权限被拦。实验环境设成 permissive 或者直接关闭,改完要重启才生效。
时间同步是分布式系统里最容易被忽略的一项。各节点时间偏差过大,会导致作业的尝试记录、日志时间线全部错乱,排查问题时你根本对不上哪条日志对应哪次操作。用系统自带的时间同步服务把几台机器对齐到同一时区同一时间源,这件事花五分钟,能省你后面几小时的困惑。
2.2 JDK 的选型与 JAVA_HOME 的两种配置姿势
JDK 用 8,这个没有讨论空间。具体到小版本,选一个稳定的 8u 更新版本就够了,不建议用各种裁剪版或者绿色版运行环境,因为 Hadoop 需要完整的 JDK(含 jps、jmap 这些工具),只有 JRE 的包会让你后面没有排查手段。
JAVA_HOME 有两种配法,各有适用场景:
第一种,配在系统环境变量里(/etc/profile.d/ 下放一个脚本)。好处是所有账号都能读到,启动脚本、手动执行命令、crontab 表现一致。这也是我推荐的方式。
第二种,只写在 hadoop-env.sh 里。好处是隔离性好,同一台机器上跑多个不同 JDK 版本的服务时不会互相干扰。缺点是你在命令行手动敲 hdfs 命令时,如果外部环境没有 JAVA_HOME,某些工具会报找不到 Java。
不管用哪种,配完必须验证一次:echo $JAVA_HOME能打印出路径,$JAVA_HOME/bin/java -version能输出正确的版本号。这两条不通过,后面一定会在某个环节报 “JAVA_HOME is not set and could not be found”。
还有一个隐蔽的坑:路径里存在软链接。比如你装的是 /usr/local/jdk1.8.0_xxx,然后建了个软链接 /usr/local/java 指过去。有的启动脚本在判断 Java 版本时会去 resolve 真实路径,个别版本的解析逻辑会在软链接这一层出问题。稳妥做法是直接把 JAVA_HOME 指向真实安装目录。
2.3 账号、免密登录与目录规划
完全分布式必配免密,伪分布式其实也需要——因为 start-dfs.sh 这类脚本会通过 SSH 去启动远端或本地的守护进程,没有免密你就得反复输密码,脚本化启动会直接卡住。
配免密的步骤:在运行启动脚本的那台机器上生成一对密钥,把公钥追加到所有节点(含自己)目标账号的 authorized_keys 里,权限设成 700(.ssh 目录)和 600(authorized_keys)。权限不对是最常见的失败原因,SSH 会因为权限过宽直接拒绝使用密钥,而且默认不告诉你原因。
验证方法很直接:ssh worker1 hostname应该无密码直接返回 worker1。如果还要输密码,先看权限,再看目标账号的家目录权限,最后看 sshd 配置是否允许密钥登录。
目录规划我一般按这个思路:所有持久化数据放一个盘,路径形如 /data/hadoop/,下面按用途分:
- name:NameNode 元数据
- data1、data2:DataNode 数据目录,多块盘就分多个目录
- tmp:hadoop.tmp.dir,各类临时文件
- yarn/local:容器本地目录
- yarn/logs:容器日志
- logs:守护进程自身的日志(默认在安装目录下,可以改到数据盘)
这么分的好处是后面做磁盘容量规划、备份、清理都很清晰,不会出现“不知道哪个目录能删”的窘境。
3. 六个配置文件:一行一行拆
Hadoop 的安装本质上就是改六个文件:hadoop-env.sh、core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml、workers。下面按依赖顺序拆,重点讲每个参数的意图和取值理由。
3.1 hadoop-env.sh:环境变量和 root 运行的那道坎
这个文件是 shell 脚本,不是 XML,改动时注意不要写错语法。最少要改两处:
export JAVA_HOME=/usr/local/jdk1.8.0_xxx export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop export HADOOP_LOG_DIR=/data/hadoop/logs第二行在完全分布式下很重要,有些组件(尤其是 yarn 的容器)需要知道配置目录在哪,否则会读到默认值。第三行把守护进程日志从安装目录挪到数据盘,方便统一收集。
然后是我个人认为 3.x 里最坑的一处:用 root 账号启动。3.x 的启动脚本增加了一道检查,发现你是 root 会直接退出,报一行类似“Attempting to operate on hdfs namenode as root”的错误,然后建议你设置 xxx_USER 变量。解决办法在同一个文件里加:
export HDFS_NAMENODE_USER=root export HDFS_DATANODE_USER=root export HDFS_SECONDARYNAMENODE_USER=root export YARN_RESOURCEMANAGER_USER=root export YARN_NODEMANAGER_USER=root这五个变量名在 3.x 里有明确对应关系,与 2.x 那套写法不同,不要混用。用普通账号跑则完全不需要这一组,这也说明为什么我倾向用专用账号:少一类问题。
顺便说一下 JVM 参数。3.x 里每种守护进程的 JVM 参数都拆成了独立变量,比如 HDFS_NAMENODE_OPTS、YARN_RESOURCEMANAGER_OPTS。想给 NameNode 加堆内存,写export HDFS_NAMENODE_OPTS="-Xmx4g -Xms4g",而不是去动通用的 HADOOP_OPTS。用通用变量会把这个堆大小一并塞给客户端命令,你敲个 hdfs dfs -ls 都要吃 4G 内存,机器小一点直接 OOM。
3.2 core-site.xml:一个 defaultFS 定生死
这个文件里真正必须改的只有两个,但每一个都不能错。
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://master:8020</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/data/hadoop/tmp</value> </property> <property> <name>io.file.buffer.size</name> <value>131072</value> </property> </configuration>fs.defaultFS 决定了客户端“默认访问哪个文件系统”。这里写的主机名必须是所有节点都能解析的,端口写 8020(3.x 默认)。写成 localhost 或者 IP,客户端在其他机器上就会连错地方。如果有人在这台机器上装了别的版本或者别的组件,这个值还会被其他组件的默认配置覆盖,所以务必确认最终生效值是你要的那个——用hdfs getconf -confKey fs.defaultFS查一下最直接。
hadoop.tmp.dir 是很多隐性问题的源头。它默认落在 /tmp 下,而 /tmp 在不少系统上配了定期清理策略或者重启清空。集群跑得好好的,某天重启之后 DataNode 报了 clusterID 不一致,十有八九就是因为某些临时状态被清掉了。统一指到持久化路径,能避免一大类诡异现象。
3.3 hdfs-site.xml:副本数、目录、端口三件套
<configuration> <property> <name>dfs.replication</name> <value>2</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/data/hadoop/dfs/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/data1/hadoop/dfs/data,/data2/hadoop/dfs/data</value> </property> <property> <name>dfs.namenode.secondary.http-address</name> <value>master:9868</value> </property> <property> <name>dfs.namenode.handler.count</name> <value>60</value> </property> </configuration>副本数这里有个必须算清的关系:副本数不能大于 DataNode 数量。只有一台从节点时设成 3,客户端上传文件会一直卡在等待副本满足的状态,最后超时报错,日志里刷“could only be replicated to 0 nodes”。实验环境两三个节点,设 2 就够;真正上生产,3 是合理值,因为机架感知和容灾都是按 3 副本设计的。
dfs.namenode.name.dir 和 dfs.datanode.data.dir 都支持逗号分隔的多目录写法,这是做磁盘冗余的手段。NameNode 目录写成多个(最好在不同物理盘上),一份坏了还能从另一份恢复;DataNode 目录写多个,HDFS 会在多块盘之间均匀分布数据块,避免某一块盘写满拖垮整个节点。
dfs.namenode.handler.count 是 NameNode 的 RPC 处理线程数,官方给的经验公式是 20 * log10(节点数)。10 台节点算下来约 20,100 台约 40,1000 台约 60。小集群设成 60 完全够用,不必纠结。这个值设太大反而会加剧 NameNode 内部的锁竞争。
关于 NameNode 堆内存,我给一个量级估算方法:堆大小大致与元数据对象数量成正比,经验上每一百万个文件/块对象大约需要几百 MB 堆。举个例子,集群计划存 1000 万个小文件、每个文件占一个块,就是 1000 万量级的块对象,按每百万约 300MB 估算约 3GB,再给元数据加载、快照、并发请求留出余量,8GB 是个比较稳的起点。这个数字只能作为装机初值,真实场景请用 jmap 观察实际占用后再调,小文件多的集群和单个大文件的集群差别可能有几倍。
3.4 yarn-site.xml 与 mapred-site.xml:内存和 vcore 怎么算
这一段是整套安装里唯一需要动笔算的地方,也是最容易配出“作业一直处于 ACCEPTED 状态”的地方。
<!-- yarn-site.xml --> <property><name>yarn.nodemanager.aux-services</name><value>mapreduce_shuffle</value></property> <property><name>yarn.resourcemanager.hostname</name><value>master</value></property> <property><name>yarn.nodemanager.resource.memory-mb</name><value>12288</value></property> <property><name>yarn.nodemanager.resource.cpu-vcores</name><value>6</value></property> <property><name>yarn.scheduler.maximum-allocation-mb</name><value>8192</value></property> <property><name>yarn.scheduler.minimum-allocation-mb</name><value>1024</value></property> <property><name>yarn.nodemanager.vmem-check-enabled</name><value>false</value></property>参数怎么来的?我用一台 16GB 内存、8 核的机器举例。先扣掉系统本身、HDFS 守护进程、日志和预留空间,大约 4GB,剩下的 12GB 给 NodeManager,所以 resource-memory-mb 写 12288。vcore 不建议等于物理核数,留出两核给系统和其他进程,写 6。
minimum-allocation-mb 是调度器分配内存的最小粒度,设成 1024 表示所有容器申请都是 1G 的整数倍,这样分片更整齐、碎片更少。maximum-allocation-mb 是单个容器的上限,必须大于等于你的最大 reduce 容器申请值,否则申请会被直接拒绝,作业挂在那里不动。我这里设 8192,因为下面的 reduce 容器最大只申请到 4096,留了一倍余量。
vmem-check-enabled 设成 false 是我在实验环境里的固定动作。YARN 默认会检查容器的虚拟内存使用,而 JVM 的虚拟内存占用远大于堆大小,一些小机器上容器刚起来就被判超限然后 kill 掉,日志里只有一行“Container killed on request”,非常难排查。物理内存检查该留还是要留,虚拟内存这项在开发环境关掉能省很多解释成本。
然后是 mapred-site.xml:
<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> <property><name>mapreduce.map.memory.mb</name><value>2048</value></property> <property><name>mapreduce.reduce.memory.mb</name><value>4096</value></property> <property><name>mapreduce.map.java.opts</name><value>-Xmx1638m</value></property> <property><name>mapreduce.reduce.java.opts</name><value>-Xmx3276m</value></property> <property><name>yarn.app.mapreduce.am.resource.mb</name><value>2048</value></property> <property><name>mapreduce.task.io.sort.mb</name><value>512</value></property>这里有个必须讲清的换算规则:容器的物理内存申请值(memory.mb)和 JVM 堆大小(java.opts 里的 -Xmx)不是一回事。堆必须明显小于申请值,给 JVM 自身结构、元空间、栈、直接内存留出空间,通用经验是堆占申请值的 75% 到 80%。所以 2048 对应 1638m,4096 对应 3276m,都是我按 80% 上浮一点点凑的整。
如果堆写得太接近申请值,容器会在运行中因为物理内存超限被 kill,日志里能看到 exit code 137 之类的痕迹。这个现象在 reduce 阶段最明显,因为 reduce 要缓存大量数据。
mapreduce.task.io.sort.mb 是 map 端排序缓冲区,默认 100MB 偏小,调大到 512MB 能明显减少溢写次数。但它占用的是 map 容器堆内的内存,调大之后要同步确认 map 的堆够用,这几个参数是联动的,不能单独看。
还有一处特别隐蔽的坑:yarn.nodemanager.env-whitelist。如果你的配置文件是从某个旧模板继承过来的,可能会覆盖这个白名单,导致容器里读不到 HADOOP_MAPRED_HOME,作业报“Error: Could not find or load main class”。处理方式是确认白名单里包含 HADOOP_MAPRED_HOME,或者干脆不写这一项用默认值。
3.5 workers 文件与 3.x 的脚本变更
workers 文件(2.x 里叫 slaves)里一行一个从节点主机名,主节点自己要不要写进去取决于你是否希望主节点也承担 DataNode——伪分布式必须写,完全分布式通常不写。写完注意两件事:末尾不要有多余空行以外的奇怪字符,从本地编辑器拷进去的文件可能带 CRLF 换行符,导致主机名解析出一个带 \r 的怪名字,节点起不来。
再提醒几个 2.x 到 3.x 的变化点,这些都是直接套用旧教程会撞上的:
| 项目 | 2.x 写法 | 3.x 写法 |
|---|---|---|
| NameNode RPC 端口 | 9000 | 8020 |
| NameNode Web UI | 50070 | 9870 |
| DataNode HTTP | 50075 | 9864 |
| SecondaryNameNode Web | 50090 | 9868 |
| 从节点清单文件 | slaves | workers |
| 特权用户变量 | HADOOP_NAMENODE_USER 等 | HDFS_NAMENODE_USER 等 |
端口这一项特别容易漏,因为改端口不会报错,只会表现为“Web UI 打不开”或者“客户端连不上”。我在排查时养成了一个习惯:启动之前先确认最终生效的端口,用grep -r "8020\|9000" etc/hadoop/过一遍配置文件,比事后猜要快得多。
4. 格式化到跑通:第一次启动的完整链路
配置写完只是开始,真正让人紧张的是第一次格式化。这一步做错,后面全是连锁反应。
4.1 namenode -format 到底做了什么,为什么只做一次
格式化做的事是:在 dfs.namenode.name.dir 指定的目录下,创建 HDFS 的初始元数据结构和存储目录,并生成一个集群唯一标识(clusterID),同时把版本信息、命名空间 ID 这些写进 VERSION 文件。
它只应该做一次,原因是:DataNode 启动时会读取自己的 VERSION 文件和 NameNode 的 clusterID 比对,一致才允许注册。你格式化第二次,NameNode 会生成一个新的 clusterID,而老 DataNode 数据目录里存的还是旧的那个,于是它们永远注册不上,NameNode 页面上活节点数为 0,DataNode 日志里反复报 clusterID 不一致。
真的手抖格式化了两次怎么办?两条路:一是把所有节点的 DataNode 数据目录清空,让它们以新集群的身份重新注册(数据全丢,实验环境无所谓);二是把 NameNode 数据目录里的 VERSION 文件改成 DataNode 记录的 clusterID(麻烦,且容易埋雷)。实验环境直接清目录重来最干净。
格式化命令:
hdfs namenode -format输出里会打印一串信息,重点确认最后出现 “Storage directory ... has been successfully formatted”。如果中途报了目录权限错误或者路径不存在,先解决掉再重试,不要带着错误继续往下走。
4.2 启动顺序、进程清单和端口对照
启动顺序建议:先 HDFS,再 YARN,最后历史服务。HDFS 没起来就启 YARN,NodeManager 会因为连不上 NameNode 报错。
start-dfs.sh start-yarn.sh mapred --daemon start historyserver在 3.x 里历史服务的启动方式就是 mapred 命令带 --daemon start,不要再去找 mr-jobhistory-daemon.sh,那个脚本在新的发行结构里可能不存在或者路径变了。
启动完用 jps 检查进程。主节点的期望清单是:NameNode、SecondaryNameNode、ResourceManager,如果主节点也做 DataNode 和 NodeManager,还要加 DataNode、NodeManager;从节点是 DataNode 和 NodeManager。少一个就去看对应的日志文件,logs 目录下按进程名分好了。
端口对照表我在排查时用得最多:
| 服务 | 端口 | 用途 |
|---|---|---|
| NameNode RPC | 8020 | 客户端与 DataNode 连接 |
| NameNode Web | 9870 | HDFS 状态页面 |
| DataNode HTTP | 9864 | 单节点状态 |
| SecondaryNameNode Web | 9868 | 检查点页面 |
| ResourceManager Web | 8088 | YARN 与作业页面 |
| ResourceManager RPC | 8032 | 客户端提交 |
| NodeManager Web | 8042 | 单节点容器状态 |
| JobHistory Web | 19888 | 历史作业查询 |
| JobHistory RPC | 10020 | 作业记录写入 |
Web UI 打不开是最高频的问题,排查顺序固定下来能省很多时间:先在主节点本地curl -I http://localhost:9870,本地通再看ss -lntp | grep 9870确认监听地址是不是 0.0.0.0(绑定到具体 IP 时外网访问不到),最后才怀疑防火墙和主机名解析。这个从内到外的顺序,能快速定位是服务没起、绑错地址还是网络不通。
4.3 用 WordCount 和 pi 做验收
进程齐了不代表能用。我固定做三步验收:
第一步,基础文件操作。hdfs dfs -mkdir -p /user/root、hdfs dfs -ls /、上传一个小文件再 cat 出来。这一步验证的是 RPC 通道和权限。
第二步,看集群报告。hdfs dfsadmin -report会列出每个 DataNode 的容量、已用、剩余空间和活节点数。活节点数对不上就说明有节点没注册成功。
第三步,跑内置例子。最快的验证是 pi:
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.0.jar pi 2 5它会起两个 map 做蒙特卡洛估算,能跑完并输出结果,说明 YARN 调度、容器启动、shuffle 全链路都通了。如果作业卡在 ACCEPTED 不动,八成是容量调度器算不出可用资源,回头检查 nodemanager 的内存和 vcore 配置与实际机器是否匹配,或者确认集群里没有别的残留作业占着资源。
再看一次 8088 页面上的容器日志入口能不能点开,能点开说明日志聚合配置生效,后面调作业出问题全靠它。
4.4 关闭与开机自启的处理思路
关闭顺序反过来:先停 YARN,再停 HDFS。
mapred --daemon stop historyserver stop-yarn.sh stop-dfs.sh需要长期跑的环境,别用 start-all.sh 这种一把梭的脚本,它的启动顺序和错误处理都不够清晰。建议写一个带健康检查的启动脚本:启动后轮询端口,超时未起就打印对应日志尾部并退出非零码,这样自动化部署时才不会出现“脚本说成功了但服务其实没起来”的情况。
5. 报错现场:我踩过的坑和排查速查表
安装过程中的报错有很强的规律性,下面按阶段分,都是我或者身边人真实撞过的。
5.1 启动阶段的高频报错逐条拆
报错一:JAVA_HOME is not set and could not be found。这句话字面意思是找不到 Java,但实际原因可能是三种:环境变量没配、hadoop-env.sh 里写错了路径、或者写对了但路径指向的是软链接而脚本解析失败。判断方法是在同一台机器上跑bash -x start-dfs.sh看它读到的值,或者直接grep JAVA_HOME $HADOOP_HOME/etc/hadoop/hadoop-env.sh确认没有多余引号、没有拼写错误。
报错二:Attempting to operate on hdfs namenode as root。前面提过,root 运行需要补五个用户变量。这个报错的好处是提示很明确,坏处是很多人只看前半句就以为是权限问题,去改目录属主。
报错三:Permission denied (publickey)。免密没配好。检查链:密钥是否存在、公钥是否追加到目标节点的 authorized_keys、.ssh 目录是否 700、authorized_keys 是否 600、家目录是否对目标账号可写。特别注意一种情况:用 A 账号生成密钥,却追加到了 B 账号的 authorized_keys,或者启动脚本执行时用的是另一个账号。
报错四:java.net.UnknownHostException: master。主机名解析不了。/etc/hosts 检查三遍:本机名有没有、写的是不是真实 IP、有没有重复项冲突。这个错误也会影响 NameNode 的绑定,格式化和启动都可能失败。
报错五:NameNode 启动后立刻退出。去看 logs 下对应的日志文件,最常见的是 9870 端口被占用(可能别的服务占了),或者元数据目录不可写。端口占用可以先用 ss 查一下,日志里会有 BindException。
报错六:DataNode 注册不上,日志报 clusterID 不一致。就是重复格式化的后果,处理方式前面说过。
报错七:DataNode 启动时报目录不存在或权限不足。dfs.datanode.data.dir 里写的多块盘路径必须提前 mkdir 并给运行账号写权限。Hadoop 不会替你创建,只会报错。
5.2 运行阶段的高频报错逐条拆
作业一直 ACCEPTED 不动。三种可能:NodeManager 没起来或没注册成功(看 8088 页面的节点数)、申请的容器内存超过 maximum-allocation-mb(回头核对两个值)、调度器队列资源被吃满(看队列页面)。
容器被 kill,日志显示 physical memory 超限。就是堆设太大了,把 java.opts 的 -Xmx 降到申请值的 80% 以内。
上传文件卡在等待副本。副本数大于可用 DataNode 数,或者某个节点磁盘写满、DataNode 处于不健康状态。用 dfsadmin -report 看各节点剩余空间。
作业报 Could not find or load main class。容器里读不到 HADOOP_MAPRED_HOME,检查 yarn.nodemanager.env-whitelist 是否被覆盖。
Web UI 打不开但端口在监听。大概率是绑定地址问题,也可能是中间有网络策略。先在本地验证,再逐段往外测。
日志聚合开了但页面上看不到日志。检查 yarn.nodemanager.log-aggregation-enable 是否真的生效(配置文件是否在所有节点都同步了),以及历史服务是否在跑。配置文件只改了主节点、从节点没同步,是这类问题最常见的成因。
5.3 排查速查表与我的几条硬经验
| 现象 | 最可能原因 | 首查位置 |
|---|---|---|
| 启动脚本要输密码 | 免密或权限 | .ssh 目录与 authorized_keys 权限 |
| 活节点数为 0 | clusterID 不一致 / 端口不通 | DataNode 日志、dfsadmin -report |
| Web UI 无法访问 | 绑定地址 / 网络策略 | 本地 curl、ss -lntp |
| 作业卡 ACCEPTED | 资源申请超上限 | 8088 队列页、yarn 内存配置 |
| 容器被 kill | 堆超过物理内存申请 | java.opts 与 memory.mb 比例 |
| 上传卡住 | 副本数 > 节点数 | dfs.replication 与实际节点 |
| 命令找不到主类 | 环境白名单缺失 | yarn env-whitelist |
| 重启后集群空 | 临时目录被清理 | hadoop.tmp.dir 路径 |
几个我不会妥协的经验:
配置文件改完必须全节点同步。我习惯用一条循环命令把 etc/hadoop 整个目录推到所有机器,推完在每台机器上 diff 一次。只改主节点是最常见的“配置看起来对了但就是不行”。
排查永远从日志开始,不要从猜测开始。logs 目录下每个进程一个文件,报错信息通常在最上面几行或者最后一次异常栈里。很多人从页面现象开始猜,猜了半小时最后发现日志第一行就写着原因。
装完之后立刻删掉安装包和临时解压目录,只保留一份配置文件的备份。配置文件才是你的资产,包随时能重新解压,配置改乱了想回滚就难了。
最后一个小技巧:把这套配置整理成一份可以重复执行的安装脚本,用变量控制主机名和目录路径,每换一批机器只改顶部几个变量。我第一次装的时候手改六台机器,改漏了一个 property 结果查了三小时。后来脚本化之后,这套流程重复了十几次没再出过配置类问题。