ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hadoop集群搭建排错:JAVA_HOME、SSH互信与OBS接入避坑

Hadoop集群搭建排错:JAVA_HOME、SSH互信与OBS接入避坑

简介:《鲲鹏云大数据实验docx》是一份面向大数据初学者、云计算实验课程学生及需要完成华为云Hadoop集群部署实践者的实验报告。文档围绕鲲鹏云环境下的完整建链过程展开,涵盖ECS与OBS购买配置、AK/SK获取、节点互信与SSH免密、Hadoop安装包分发、core-site.xml等核心配置文件设置,以及NameNode初始化与HDFS启动等环节。资源包仅含1个docx文件,大小约2.52MB,篇幅集中,便于按实验阶段对照查阅。文档还保留了Java家目录配置错误、重新分发与多次尝试等排错记录,能帮助读者理解云上Hadoop集群的部署流程、节点一致性维护和常见问题定位。目前已有893人学习,适合作为课程实验参考和动手实践前的流程预演材料。

1. JAVA_HOME 少写一个 aarch64 后缀,Hadoop 集群白折腾一晚上

按鲲鹏云大数据实验报告里的顺序,ECS 买好、OBS 建好、AK/SK 抄下来、三个节点互信也通了,最后卡在start-dfs.sh弹一句JAVA_HOME is not set and could not be found。我第一遍就是这么翻车的:java -version明明能跑,/etc/profile改了、source也执行了,重新分发 Hadoop 包、重启节点全试过,报错一个字都没变。真正的原因小得离谱——OpenJDK 在鲲鹏(aarch64)机器上的安装目录名带架构后缀,手上那份配置抄的是别处的路径,改完还不生效是因为走的是 ssh 非交互执行。这类「环境看着全对、路径差一点」的故障在集群搭建里出现频率极高,新手会被它耗掉整晚,熟手也常因为一次手滑重来一遍。下面按我实际踩过的顺序,把 JAVA_HOME、SSH 互信、OBS 接入这几道关卡的报错、根因和修法逐个拆开。

2. JAVA_HOME 报错:改完 /etc/profile 为什么远程启动还是找不到

2.1 报错原文拆开看:三种 JAVA_HOME 报错不是一回事

同样是「找不到 Java」,不同脚本吐出来的字不一样,指向的位置也不一样,先分清再动手能省一半时间。

第一种是JAVA_HOME is not set and could not be found.,出自 Hadoop 的启动脚本链(hadoop-config.sh被hdfs、yarn、start-dfs.sh一路 source)。它的含义很明确:脚本执行到这里时,JAVA_HOME这个变量在当前进程环境里是空的,于是它试图用一个内置的猜测逻辑去找 java,找不到就退出。

第二种是ERROR: JAVA_HOME is incorrectly set.,这通常来自 HBase、Hive 或某些版本的守护进程脚本。注意措辞是「incorrectly」而不是「not set」——变量有值,但那个路径下面bin/java不存在或者不可执行。这几乎百分百是把 x86 的路径写到了 ARM 机器上,或者路径末尾多写了一个斜杠、少写了一段版本号。

第三种最容易被忽略:/usr/bin/env: 'java': No such file or directory,或者干脆hdfs: command not found。前者是 PATH 里没有 java,后者是 PATH 里没有$HADOOP_HOME/bin,跟 JAVA_HOME 是两个层面的问题,别混在一起改。

我一般的判断顺序是:先在报错节点上登录着敲一次echo $JAVA_HOME和java -version,如果都有值,那问题基本不在机器本身,而在「脚本执行时的环境」——这就引出下面最关键的一节。

2.2 定位 aarch64 的真路径,别再抄 x86 的写法

鲲鹏 ECS 是 ARM 架构,用系统包管理器装出来的 OpenJDK 目录名会带架构标识,典型形态长这样:

/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.h5.oe1.aarch64

最后那一段aarch64和前面的h5.oe1(系统发行版标识)都是版本相关字段,换个镜像、换个小版本就可能变。所以千万别复制粘贴别人文档里的完整路径,一定要在自己机器上现场查:

# 1. 列出所有已安装的 JVM 目录,注意看带 aarch64 的那条 ls -ld /usr/lib/jvm/*/ # 2. 从 java 可执行文件反查真实安装根目录(最可靠) readlink -f "$(command -v java)" | sed 's#/bin/java$##' # 3. 用查到的路径直接验证,能出版本号才算数 JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.h5.oe1.aarch64 "$JAVA_HOME/bin/java" -version

ls -ld里的-d是关键,不加它会把目录内容全列出来,几十行刷屏反而看不清名字。readlink -f会一路解析软链接到最终的真实文件,因为/usr/bin/java往往是个指向alternatives的软链,顺着它反推根目录最不容易出错。sed那一段是把结尾的/bin/java切掉,得到纯粹的家目录。

顺带提醒一句:如果机器上装了多个 JDK,/usr/bin/java指向哪个由alternatives决定,readlink -f拿到的就是当前生效的那个。想显式切换可以用alternatives --config java,但在集群场景里我不建议切来切去,直接把JAVA_HOME写死成一条确定的路径更省心。

2.3 hadoop-env.sh 写死才是终点,profile 只是起点

这是整篇文章里我认为最值钱的一条经验。很多教程让你把JAVA_HOME写进/etc/profile然后source一下,你照做了,登录进去敲java -version也确实正常,可start-dfs.sh依旧报错——因为两者执行环境根本不是同一个。

Shell 读配置文件的规则大致是这样:交互式登录 shell 读/etc/profile和~/.bash_profile;交互式非登录 shell 读~/.bashrc;而ssh 远程执行单条命令时,是非交互非登录 shell,两者都不读。start-dfs.sh恰恰是通过 ssh 到每个从节点去执行hadoop-daemon.sh的,从节点那边启动 DataNode 的进程,压根没经历过你手动登录时的那些source。

所以正确做法是让配置管到「进程启动的那一刻」,有两条路,我通常两条都做:

# 方案 A(首选):写进 Hadoop 自己的 hadoop-env.sh,不依赖任何 shell 环境 # 编辑 $HADOOP_HOME/etc/hadoop/hadoop-env.sh, # 找到被注释掉的 # export JAVA_HOME=... 那一行,替换成真实路径并取消注释: export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.h5.oe1.aarch64 # 顺带把日志和 pid 目录也固定下来,避免默认写到 /tmp 被清理 export HADOOP_LOG_DIR=/home/modules/hadoop/logs export HADOOP_PID_DIR=/home/modules/hadoop/pids # 方案 B(兜底):放在 /etc/profile.d 下,并被 bashrc 加载 cat > /etc/profile.d/java.sh <<'EOF' export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.h5.oe1.aarch64 export PATH=$JAVA_HOME/bin:$PATH EOF # 让非交互 shell 也能拿到(/etc/bashrc 或 ~/.bashrc 末尾加一行) grep -q 'profile.d/java.sh' /etc/bashrc || echo '. /etc/profile.d/java.sh' >> /etc/bashrc

方案 A 是决定性的,因为它绕开了所有 shell 加载顺序问题,hadoop-env.sh是被启动脚本显式 source 的,跑不掉。方案 B 解决的是「我登录进去想直接敲 java、jps 也能用」的体验问题,顺手做掉。HADOOP_LOG_DIR和HADOOP_PID_DIR单独拎出来是因为默认值在/tmp下,机器一重启 pid 文件没了、日志也可能被清,查问题时连黑匣子都找不到。

改完先别急着启集群,用下面这条命令模拟 Hadoop 的启动方式,能干净地暴露问题:

# 模拟 start-dfs.sh 的行为:ssh 到对端,在非交互 shell 里取变量 for h in node-0002 node-0003; do echo "== $h ==" ssh -o BatchMode=yes "$h" 'echo "JAVA_HOME=$JAVA_HOME"; command -v java' done

如果这里JAVA_HOME是空的,但command -v java有输出,说明方案 B 生效了、方案 A 还没分发到从节点;如果两个都空,那从节点连方案 B 都没做。-o BatchMode=yes的作用是禁止任何交互式输入,密码提示会直接变成失败退出,不会让你误以为「卡一下就好了」。

另外别忘了,hadoop-env.sh改完必须分发到每个节点,它躺在$HADOOP_HOME/etc/hadoop/里,属于配置目录,不是共享存储,不会自己同步过去。

2.4 已经格式化过的半成品集群怎么救回来

如果报错出现在hdfs namenode -format之前,直接改好重启即可。但如果格式化那一步已经跑过(哪怕后面启动失败),事情会麻烦一点。

hdfs namenode -format会生成一个新的clusterID并写进 NameNode 的元数据目录,同时 DataNode 首次启动时会把这个 ID 记在自己的目录里。如果你之前已经启动过一次 DataNode,后来改了路径重新 format,两边 ID 对不上,DataNode 起来就会报Incompatible clusterIDs或者直接拒绝注册。这类问题在实验环境里特别常见,因为大家习惯「反正才三个节点,删了重来」。

我一般的做法是先判断需不需要保留数据:

# 清空现场前先停服务,否则守护进程还在写目录 stop-yarn.sh 2>/dev/null; stop-dfs.sh 2>/dev/null # 确认没有残留的 java 进程 jps # 输出里如果还有 NameNode / DataNode / ResourceManager,用 kill 收掉 # 在【每个节点】上清空数据目录(路径以 hdfs-site.xml 里的配置为准) rm -rf /home/modules/data/buf/* # 回到 node-0001 重新格式化,这一步只能做一次 hdfs namenode -format # 启动前先确认从节点环境变量已就位(用 2.3 里那条 ssh 检查) start-dfs.sh && start-yarn.sh # 最后核对进程,三个节点上应该能看到对应角色 jps

rm -rf那行对应的是实验报告里的/home/modules/data/buf,这个目录通常是dfs.namenode.name.dir和dfs.datanode.data.dir的父目录。如果你的hdfs-site.xml里把 name 和 data 分到了不同目录,就分别清,别只清一个——只清 NameNode 不清 DataNode,反而会把 ID 冲突坐实。

jps是这套流程里最廉价的体检工具,NameNode 在 node-0001,DataNode 和 NodeManager 在全部节点,缺哪个就去看那个节点的日志。日志位置就是上面HADOOP_LOG_DIR设的地方,hadoop-<用户名>-datanode-<主机名>.log。很多人卡在「起了但不对」,其实日志第一屏就写了原因。

3. 节点互信失败:Permission denied (publickey) 的四条排查线

3.1 先分清是解析不到主机还是公钥被拒

报错文本能直接把你导向不同方向,别看到红字就当同一类问题处理:

报错关键词真正的问题先去查
Could not resolve hostname node-0002主机名解析失败/etc/hosts
Permission denied (publickey,gssapi-keyex,gssapi-with-mic)公钥没有被接受authorized_keys与权限
Host key verification failed.对端指纹变了或 known_hosts 有旧记录~/.ssh/known_hosts
Authentication refused: bad ownership or modes目录/文件权限过宽家目录与.ssh权限
停在Are you sure you want to continue connecting首次连接等待交互确认StrictHostKeyChecking

最左列那几种里,第一种其实跟 SSH 密钥一点关系没有,纯粹是名字解析不到;第三种也不是密钥问题,是重装 ECS 后 IP 复用了、对端 host key 变了。把这两类误判成「密钥没配好」,会导致你反复重做ssh-copy-id,做十遍也不管用。

3.2 /etc/hosts 里写内网 IP,三个节点必须写成同一份

Hadoop 的各处配置(slaves、core-site.xml里的fs.defaultFS)都会用到主机名,而主机名要靠/etc/hosts或者内部 DNS 解析。最省事的做法是把三台机器的映射写死,且每一台写成完全一样的内容:

# 三个节点都执行,内容必须一模一样 cat >> /etc/hosts <<'EOF' 192.168.0.11 node-0001 192.168.0.12 node-0002 192.168.0.13 node-0003 EOF # 逐条验证解析结果,注意看返回的 IP 是否与上面一致 getent hosts node-0001 node-0002 node-0003

这里有两个容易翻车的点。一是别写公网 IP:ECS 之间走公网 IP 通信,流量会绕出去再绕回来,延迟和费用都不划算,而且不少环境的安全组默认只放行内网互通,公网回环反而连不上。二是三份内容必须一致,我曾经遇到某台机器只写了另外两台、没写自己,结果那台机器上start-dfs.sh能起来,但它自己的 DataNode 连不上 NameNode——因为它解析node-0001失败了,只是这个失败发生在子进程里,主脚本不报错。

getent hosts比ping更适合做验证,它直接走系统解析器、不受 ICMP 是否被禁的影响,输出也干净。ping在某些云环境里默认被拦,你 ping 不通不代表解析有问题,容易误判。

写完 hosts 之后再用hostname确认一下本机的主机名确实是node-0001这类短名,而不是带域名的全名;如果全限定名和短名混用,slaves文件里写的东西可能对不上。

3.3 authorized_keys 与权限位:700/600 不是玄学

公钥认证被拒,绝大多数情况不是「没复制」,而是「复制了但权限不对被拒绝加载」。SSH 服务端对权限的要求近乎偏执:家目录不能对 group 和 other 可写,.ssh目录必须是 700,authorized_keys必须是 600,否则直接忽略这个文件,且不会在客户端报出细节,只会给你一句 publickey denied。

# 1. 在每台节点生成密钥(-N '' 表示空口令,集群内部免交互必须这样) ssh-keygen -t rsa -b 2048 -N '' -f ~/.ssh/id_rsa # 2. 把所有节点的公钥收拢后统一追加(这里演示本机分发到三台) for h in node-0001 node-0002 node-0003; do ssh-copy-id -i ~/.ssh/id_rsa.pub "root@$h" done # 3. 修权限,一行都不能省 chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys ~/.ssh/id_rsa chmod 755 ~/. # 家目录不能组内可写 # 4. 用 sshd 自己的视角检查配置是否被拒 sshd -T 2>/dev/null | grep -Ei 'pubkeyauth|strictmodes|authorizedkeysfile'

第 2 步用ssh-copy-id比手抄公钥可靠,它会自动建目录、追加内容、修权限。第 3 步的chmod 755 ~/.常被忽略,StrictModes yes(默认值)会检查家目录权限,家目录如果是 777 或者属于别的用户,整条链路直接废掉。

第 4 步查sshd -T是我后来养成的习惯:如果PubkeyAuthentication是no,你把密钥复制一百遍也没用;如果AuthorizedKeysFile指向的路径不是你写的那个(有些镜像改过默认值),同样是白忙。这个命令能一次性排掉这两种可能。

还有一个隐藏条件:互信要求的是两两之间都能通,不是只从 node-0001 到其他节点。DataNode 之间、NodeManager 之间都有通信,start-dfs.sh也会 ssh 回来操作本机。所以我一般用嵌套循环把九个方向全测一遍,而不是只测三条。

3.4 用 BatchMode 把「卡住」变成「立刻报错」

首次 ssh 到新机器会问Are you sure you want to continue connecting (yes/no)?,手动敲一次 yes 就过去了,但在批量脚本里它会静默等待,表现成「命令执行了但没反应」,这是最折磨人的一类「故障」——它连报错都不给你。

# 关闭首次连接的交互确认,并把失败立即暴露出来 cat >> ~/.ssh/config <<'EOF' Host node-0001 node-0002 node-0003 StrictHostKeyChecking no UserKnownHostsFile /dev/null LogLevel ERROR EOF chmod 600 ~/.ssh/config # 九个方向一次性验证,任何一条失败都会立刻打出来 for a in node-0001 node-0002 node-0003; do for b in node-0001 node-0002 node-0003; do ssh -o BatchMode=yes -o ConnectTimeout=5 "$a" "ssh -o BatchMode=yes -o ConnectTimeout=5 $b hostname" \ >/dev/null 2>&1 && echo "OK $a -> $b" || echo "FAIL $a -> $b" done done

StrictHostKeyChecking no加UserKnownHostsFile /dev/null的组合适合实验环境里频繁重建节点的场景,代价是放弃了主机指纹校验,生产环境不要这么写。BatchMode=yes才是这套检测的核心,它让任何需要人工输入的场景直接以非零码退出,FAIL那一行出现的位置就是你要修的方向。

ConnectTimeout=5防止某台机器网络不通时卡满默认的几十秒,九个方向跑一遍也就几十秒。这个双层嵌套是故意的:外层 ssh 到 a,内层再从 a ssh 到 b,模拟的正是start-dfs.sh从 node-0001 跳到其他节点的真实调用路径。

4. OBS 接不上:No FileSystem for scheme "obs" 的三个层级

4.1 第一层:jar 没进 classpath

No FileSystem for scheme: obs这个报错的意思很直白:Hadoop 的FileSystem是一个可插拔的抽象层,靠fs.<scheme>.impl配置去反射出实现类,反射不到就抛这个异常。它跟网络、跟密钥、跟桶权限统统无关,纯粹是当前 JVM 的 classpath 里没有那个 jar。

# 1. 先确认配置里确实声明了实现类(这一步在后面 core-site.xml 里配) grep -A2 'fs.obs.impl' "$HADOOP_HOME/etc/hadoop/core-site.xml" # 2. 把 OBS 的接入 jar 放进 Hadoop 的公共库目录 cp /root/hadoop-obs-*.jar "$HADOOP_HOME/share/hadoop/common/lib/" # 3. 验证 jar 是否真的进了 classpath hadoop classpath | tr ':' '\n' | grep -i obs # 4. 直接调实现类,能打印出类名说明加载成功 hadoop org.apache.hadoop.fs.obs.OBSFileSystem 2>&1 | head -3

第 2 步的落点很关键:share/hadoop/common/lib/会被所有守护进程(NameNode、DataNode、ResourceManager、NodeManager)以及hdfs命令行共享。放在share/hadoop/hdfs/lib/下,命令行能用但 YARN 上的作业读不到;放在自己的 classpath 里,只有当前用户当前目录生效。每个节点都要放,这一点在第 5 章还会再提一次。

第 3 步的hadoop classpath | tr ':' '\n'是把长串路径拆成一行一条,方便 grep,比肉眼在那条几百字符的字符串里找快得多。第 4 步是很多人不知道的用法:直接用hadoop命令去跑一个类的main,类加载成功会打出帮助或抛参数错误,类加载失败就是ClassNotFoundException,一眼分明。

关于 jar 版本:Hadoop 2.x 时代 OBS 的接入包有多个发行版本,命名方式和依赖关系不太一样,有些还需要一并放入对象存储的 SDK 包。遇到NoClassDefFoundError而不是ClassNotFoundException时,通常就是缺了传递依赖,把 SDK 包一起丢进同一个目录即可。

4.2 第二层:core-site.xml 的四个必填项与 endpoint

配置文件的写法决定了 Hadoop 去哪里找实现类、用哪套凭据、连哪个接入点。这四个属性缺一个都跑不通:

<configuration> <!-- 1. 声明 obs:// 协议由哪个类处理,缺了就是 No FileSystem for scheme --> <property> <name>fs.obs.impl</name> <value>org.apache.hadoop.fs.obs.OBSFileSystem</value> </property> <!-- 2. AK/SK:从控制台下载的凭据文件里取,注意不要带引号不要带空格 --> <property> <name>fs.obs.access.key</name> <value>替换成你的AK</value> </property> <property> <name>fs.obs.secret.key</name> <value>替换成你的SK</value> </property> <!-- 3. endpoint 必须和桶所在区域一致,写错会 301 或 404 --> <property> <name>fs.obs.endpoint</name> <value>obs.cn-north-4.你的平台域名</value> </property> <!-- 4. 默认文件系统仍指向 HDFS,OBS 用全路径访问 --> <property> <name>fs.defaultFS</name> <value>hdfs://node-0001:9000</value> </property> </configuration>

第一条是无脑照抄的,但要注意大小写:fs.obs.impl全是小写,类名是全限定名,多一个字母就会回到No FileSystem for scheme。第二条里的值一定不要加引号,XML 里加了引号会被当成值的一部分,最终签名错误报 403,而报错信息不会告诉你「你的密钥多了个引号」。

第三条的 endpoint 是最容易搞错的一项。控制台的桶概览页会给出两种地址,普通地址和双栈地址,格式分别是obs.<区域>.<平台域名>和obs.dualstack.<区域>.<平台域名>。集群在 ECS 内网跑,用普通地址就够了,别手动拼双栈;如果桶和 ECS 不在同一区域,跨区访问不仅慢,还可能因为 endpoint 与桶区域不匹配直接返回 301。

第四条是我特意保留的一个坑位。fs.defaultFS指向hdfs://而不是obs://,因为你仍然需要一个完整的 HDFS 集群来跑 NameNode、DataNode、YARN,OBS 在这里的角色是外挂的数据源,不是替换 HDFS。如果把它改成obs://桶名,后续所有不带 scheme 的相对路径都会被解析到 OBS,hdfs dfs -put之类操作的行为会变得非常反直觉,还会冒出Wrong FS: obs://..., expected: hdfs://node-0001:9000这类报错。访问 OBS 时一律写全路径obs://桶名/路径,干净利落。

顺带说一句并行文件系统:实验报告里提到创建并行文件系统,它在 Hadoop 侧同样按obs://访问,配置项完全一样,差别在于元数据操作延迟和 rename 语义更接近本地文件系统,适合跑对目录操作敏感的计算任务。普通对象桶在 rename 上是「复制再删除」,跑大作业时能明显感觉到差别。

4.3 第三层:AK/SK 与 403/404 报错对照

配置齐了、jar 也加载了,剩下的报错基本都能从状态码反推:

报错片段含义排查方向
Status Code: 403+AccessDenied凭据无效或无权限AK/SK 抄错、多了空格引号、凭据已失效
Status Code: 404+NoSuchBucket桶名或 endpoint 不对桶名拼错、endpoint 区域与桶不一致
Status Code: 301请求被重定向endpoint 写的区域不是桶所在区域
SignatureDoesNotMatch签名计算不一致SK 错误,或系统时间偏差过大
Connect timed out网络不通安全组未放行出方向、endpoint 写错

SignatureDoesNotMatch这一条我单独说一下:它跟时间强相关,签名里带时间戳,如果 ECS 的系统时间偏差超过允许窗口,签名就会对不上,而凭据完全正确。云主机通常有时间同步,但如果你手动改过时间做别的实验忘了改回来,就会出现「昨天还好好的今天全 403」的诡异局面。date看一眼就知道。

拿到凭据文件后,正确姿势是只取需要的两列,而不是整份文件到处传:

# 凭据文件是 CSV,第四列是 AK、第五列是 SK,跳过表头读取 tail -n +2 credentials.csv | awk -F',' '{print "AK="$2"\nSK="$3}' # 更稳妥的做法:用环境变量临时注入,不落盘到配置文件里 export OBS_AK='你的AK' export OBS_SK='你的SK'

tail -n +2跳过表头那行,awk -F','按逗号切列。凭据文件如果被换了行尾符(Windows 的 CRLF),SK 末尾会多一个\r,肉眼完全看不出来但签名必然失败。遇到「怎么都对但就是 403」,用cat -A credentials.csv看一眼行尾是不是^M$,能省下大量时间。

注意:AK/SK 等同于账号密码,不要写进要提交的代码、不要贴进文档、不要出现在截图里。实验做完在控制台把旧凭据删掉重建是最省事的收尾方式。

4.4 验证:用 hdfs dfs 打通 OBS 再回去配集群

我调整了验证顺序:先在单机上把 OBS 通路彻底打通,再去动集群。因为集群一旦启动,OBS 出问题时的报错会混在 YARN 的容器日志里,噪音极大。

# 1. 列桶根目录,这一步能过说明 jar、配置、凭据、网络四条线全通 hdfs dfs -ls obs://你的桶名/ # 2. 写一个小文件上去,验证写权限和签名 echo "obs connectivity test" > /tmp/t.txt hdfs dfs -put -f /tmp/t.txt obs://你的桶名/test/t.txt # 3. 读回来看内容是否一致 hdfs dfs -cat obs://你的桶名/test/t.txt # 4. 确认删除能生效(能测出桶权限是否给全) hdfs dfs -rm -r obs://你的桶名/test

第 1 步如果报No FileSystem就回 4.1,报 403 就回 4.3,报超时就查安全组。这个顺序把四类问题完全解耦,比对着集群日志猜要快得多。

还有一点值得提醒:hdfs dfs走的是客户端侧的配置,集群侧的守护进程走的是它自己读到的core-site.xml。你在命令行测通了,不代表 DataNode 也能读 OBS——后者的配置文件是分发过去的那一份,可能还是旧的。所以第 1 章里反复强调的「改完要分发」,到这一章依然成立。

5. 避坑清单:分发、格式化、重启三个环节的五个翻车现场

5.1 分发环节:改了配置只发一半

现象:node-0001 上一切正常,start-dfs.sh也能把进程拉起来,但 node-0003 上的 DataNode 死活不出现,或者出现了却报ClassNotFoundException。

原因:配置或 jar 只更新了部分节点。Hadoop 的安装目录是每台机器各自的本地目录,不存在共享,scp到一台不等于到了三台。更隐蔽的情况是分发时用了scp -r覆盖,目标节点上原有的文件因为同名被覆盖、不同名的旧文件被保留,造成「新老混装」。

解决:固定用一条循环命令分发,并在分发后做校验,而不是靠记忆:

# 从 node-0001 把整个 Hadoop 目录推到其他节点,--delete 保证目标与源一致 for h in node-0002 node-0003; do rsync -az --delete "$HADOOP_HOME/" "root@$h:$HADOOP_HOME/" done # 校验关键文件的一致性,MD5 不同就是没同步到 md5sum "$HADOOP_HOME/etc/hadoop/core-site.xml" "$HADOOP_HOME/etc/hadoop/hadoop-env.sh" ssh node-0002 "md5sum $HADOOP_HOME/etc/hadoop/core-site.xml $HADOOP_HOME/etc/hadoop/hadoop-env.sh"

rsync -az里的-a保留权限和时间戳,-z压缩传输,--delete是核心——它让目标端与源端完全一致,避免旧文件残留。/结尾不能丢,$HADOOP_HOME/和$HADOOP_HOME在 rsync 里语义完全不同,前者同步内容,后者会在目标下再套一层目录。分发完跑一次md5sum,两边输出一模一样才算完,这一步花十秒,能省掉半小时的「为什么行为不一致」排查。

5.2 启动环节:重复 format 与 slaves 写错

现象一:NameNode 正常起来,但 DataNode 一个都没有,日志里写Incompatible clusterIDs,或者 NameNode 日志里出现Datanode denied communication。

原因:hdfs namenode -format被执行了不止一次,或者清数据目录时只清了 NameNode 侧。每次 format 都会生成新的 clusterID,DataNode 目录里存的还是旧的,两边对不上就拒绝注册。

解决:要么把所有节点的数据目录全部清空后只 format 一次,要么用-clusterid参数显式指定同一个 ID。实验环境里前者更省事:

# 停服务 -> 清所有节点的数据目录 -> 只 format 一次 -> 启动 stop-yarn.sh; stop-dfs.sh for h in node-0001 node-0002 node-0003; do ssh "$h" 'rm -rf /home/modules/data/buf/*' done hdfs namenode -format start-dfs.sh

现象二:slaves文件里明明写了三行,实际只有一两个 DataNode 起来。

原因:slaves里写的是主机名,但文件里混入了空行、末尾多了空白字符,或者写的是 IP 而/etc/hosts里只有主机名。start-dfs.sh逐行读取并 ssh 过去,读到解析不了的行就静默跳过,不报错。

解决:用cat -A查出隐藏字符,并保证写的内容与getent hosts能解析的名字一致:

# 检查是否有 ^M(CRLF)或行尾多余空格 cat -A "$HADOOP_HOME/etc/hadoop/slaves" # 逐个验证 slaves 里的名字都能解析 while read -r h; do [ -n "$h" ] && { getent hosts "$h" >/dev/null && echo "OK $h" || echo "FAIL $h"; } done < "$HADOOP_HOME/etc/hadoop/slaves"

cat -A会把不可见字符显式打出来,行尾是$才正常,出现^M$就是 Windows 换行。那个 while 循环做的事很朴素:把slaves里的每一个名字拿去解析一遍,解析不了立刻暴露,比等start-dfs.sh静默跳过强太多。

5.3 重启环节:ECS 重启后被覆盖的 /etc/hosts

现象:昨天还好好的集群,今天机器重启后互信失效、getent hosts node-0002也查不到,但文件看着没动过。

原因:部分云镜像带 cloud-init 之类的初始化逻辑,重启时会根据元数据重写/etc/hosts或重置主机名,你手工追加的那几行被覆盖。也有情况是主机名被改回了默认值,导致slaves里的名字与本机hostname对不上。

解决:把主机名固定下来,并把 hosts 条目交给 cloud-init 管理而不是手工改:

# 1. 永久设置主机名(每个节点设成自己的名字) hostnamectl set-hostname node-0002 # 2. 检查本机主机名与 hosts 里的条目是否自洽 hostname; getent hosts "$(hostname)" # 3. 若确认是启动时被覆盖,把追加动作写成开机后执行的脚本, # 而不是只改 /etc/hosts 文件本身 cat >> /etc/rc.local <<'EOF' grep -q 'node-0001' /etc/hosts || echo '192.168.0.11 node-0001' >> /etc/hosts EOF chmod +x /etc/rc.local

hostnamectl set-hostname是持久化设置,比hostname xxx那种临时改法可靠得多,后者重启就没了。第 2 步那个自洽检查很关键:本机主机名如果解析不到自己,很多组件会用回环地址或者直接报错,而这在单机测试时完全看不出来。

5.4 权限与用户:目录建好了但守护进程写不进去

现象:启动脚本没报错,进程也在,但 DataNode 日志里持续刷Permission denied,或者 YARN 的容器一直失败。

原因:hdfs-site.xml里配的数据目录是用 root 建的,守护进程却以别的用户启动;或者locals目录的权限是 755,容器需要在该目录下再建子目录时被拒。

解决:目录的属主必须和实际运行守护进程的用户一致,权限给到 755 就够,不要图省事给 777:

# 确认目录属主与运行用户一致(用 ps 看进程实际以谁的身份跑) ps -eo user,pid,cmd | grep -E 'DataNode|NodeManager' | grep -v grep # 按实际用户修正属主与权限 chown -R root:root /home/modules/data /home/nm/localdir chmod -R 755 /home/modules/data /home/nm/localdir

ps -eo user,pid,cmd是判断「到底以谁的身份在跑」的唯一可靠方式,配置文件里写什么都不算数。修正属主时-R递归要慎用,指到系统目录上会很危险,落点一定是自己建的这几个业务目录。

5.5 时钟与主机名混用:那些「昨天还好好的」

现象:集群跑了一阵突然大面积失败,作业提交不了、OBS 访问 403,重启后又恢复。

原因:多台机器的系统时间出现偏差,或者某台机器的主机名被改过后没有重开 ssh 会话。前者影响签名和 Kerberos 类认证,后者影响基于主机名的服务注册。

解决:把时间同步和主机名检查放进巡检脚本,而不是等出问题再查。这类问题最气人的地方在于它不可复现——你去看的时候往往已经自己好了,只能靠事前检查兜住。

6. 快速定位:把报错关键词映射成一条命令

排查这类集群故障,我的原则是先按依赖顺序体检,再看具体报错。因为大量「报错」其实是上游某个环节失败后引发的连锁反应,从下游往上找,永远在追影子。下面这套顺序是我自己固定会跑的,从最底层开始,第一个失败项基本就是根因。

顺序检查项一条命令失败时的典型结论
1主机名解析getent hosts node-0001 node-0002 node-0003/etc/hosts缺失或被覆盖
2免密互信ssh -o BatchMode=yes node-0002 hostname公钥或权限问题
3Java 环境ssh -o BatchMode=yes node-0002 'echo $JAVA_HOME'hadoop-env.sh未分发
4配置一致ssh node-0002 "md5sum $HADOOP_HOME/etc/hadoop/*.xml"分发不完整
5jar 加载hadoop classpath | tr ':' '\n' | grep -i obs接入包没放对位置
6进程存活jps(每台都跑)角色缺位,去看对应日志
7HDFS 通路hdfs dfs -ls /NameNode 未格式化或未启动
8OBS 通路hdfs dfs -ls obs://桶名/凭据、endpoint 或网络

把上面这些串起来,写成一份可以反复执行的体检脚本,比每次现查命令可靠得多:

#!/bin/bash # cluster-check.sh:按依赖顺序体检,遇到第一个 FAIL 就停,那个就是根因 set -u NODES="node-0001 node-0002 node-0003" fail() { echo "FAIL [$1] $2"; exit 1; } # 1. 解析 for h in $NODES; do getent hosts "$h" >/dev/null || fail "dns" "$h 解析失败"; done echo "OK 主机名解析" # 2. 互信(BatchMode 保证失败立刻返回而不是卡住) for h in $NODES; do ssh -o BatchMode=yes -o ConnectTimeout=5 "$h" hostname >/dev/null 2>&1 \ || fail "ssh" "$h 免密失败" done echo "OK 节点互信" # 3. 各节点 JAVA_HOME 非空且可执行 for h in $NODES; do ssh -o BatchMode=yes "$h" 'test -x "$JAVA_HOME/bin/java"' \ || fail "java" "$h 上 JAVA_HOME 无效" done echo "OK Java 环境" # 4. 配置一致性(以 node-0001 为基准) base=$(md5sum "$HADOOP_HOME"/etc/hadoop/*.xml | md5sum | cut -d' ' -f1) for h in $NODES; do cur=$(ssh -o BatchMode=yes "$h" "md5sum \$HADOOP_HOME/etc/hadoop/*.xml | md5sum | cut -d' ' -f1") [ "$base" = "$cur" ] || fail "config" "$h 配置文件与基准不一致" done echo "OK 配置一致" echo "体检通过,可以去看具体报错了"

这个脚本有几个设计取舍值得说明。set -u让未定义变量直接报错退出,避免因为$HADOOP_HOME恰好为空导致md5sum /*.xml这类灾难性展开。fail()里直接exit,是因为按依赖顺序检查时,第一个失败一定在上游,后面的检查结果没有参考价值,继续跑只会刷出一屏噪音误导判断。第 4 步的配置一致性用的是「把所有 xml 的 md5 再取一次 md5」的折叠写法,好处是节点数量变化时不用改脚本,坏处是定位不到具体哪个文件不同,所以它只负责报警,具体哪个不一样还得回去让rsync --delete重推一遍。

真正跑起来之后你会发现,绝大多数「莫名其妙」的故障都能在前三步被抓住。我印象最深的一次是某个节点jps里 DataNode 时有时无,日志翻遍了没线索,最后是第 3 步暴露出来的:那个节点的hadoop-env.sh里JAVA_HOME写的是旧路径,那个旧 JDK 被另一个实验删掉了,于是 DataNode 启动时偶尔能起来(走 PATH 里的 java)、偶尔起不来(走$JAVA_HOME里的),表现出完全随机的样子。如果没有这条固定检查项,我大概率还在猜是不是磁盘满或者内存泄漏。

从那以后我每次改完集群配置,不管改的只是一行 XML 还是一个 jar 包,都强制走一遍「分发 → md5 校验 → 体检脚本 → 看 jps」这四步,再动手启服务。多花的那两分钟,比起在日志里大海捞针,实在太划算了。希望这套顺序也能帮你把卡住的那个报错快速定位出来,完整的实验流程和配置参数,都在那份鲲鹏云大数据实验报告文档里可以对照着走一遍。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进