最近社区里聊自动化脚本的人不少,有做 Spring Boot 自定义自动配置的,也有写各种提效小工具的。但对我来说,最有成就感的自动化脚本,还是把 Hadoop 3.3.6 的安装过程一键化。我记得第一次手动部署 Hadoop 3.3.6 时,光是在 JAVA_HOME 上就卡了快一个小时:明明是照着教程一步步来的,start-dfs.sh 却始终报JAVA_HOME is not set。后来我把这些坑全部整理进一套自动化脚本,路径探测、JAVA_HOME 自动配置、四个 XML 生成、SSH 免密、NameNode 格式化、服务启动验证,一次跑完直接能看到全套进程。这篇文章就从头到尾拆解这套脚本的设计思路、关键实现和排错方法,适合刚入门 Hadoop、不想在环境搭建上反复踩坑的同学,也适合需要在多台机器上重复部署的运维朋友。
1. 手动装 Hadoop 3.3.6 有多痛:一次真实部署复盘
1.1 看似简单的人工部署流程,为什么总在细节翻车
网上关于 Hadoop 安装的教程大体是同一个流程:装 JDK、配 JAVA_HOME、下载 Hadoop 压缩包、解压、改 core-site.xml、改 hdfs-site.xml、配 SSH 免密、格式化 NameNode、执行 start-dfs.sh 和 start-yarn.sh,最后用 jps 验证进程。看起来每个步骤都不难,真正走一遍就会发现问题几乎全藏在细节里。
最容易被卡住的就是 JAVA_HOME。Hadoop 的启动脚本会读取$HADOOP_HOME/etc/hadoop/hadoop-env.sh里的export JAVA_HOME,而官方默认模板里这一行基本是被注释掉的。很多人只在/etc/profile里设置了 JAVA_HOME,没有同步到 hadoop-env.sh,结果java -version明明正常,NameNode 却死活起不来,日志里就甩给你一句干巴巴的JAVA_HOME is not set。
XML 配置是第二个重灾区。core-site.xml、hdfs-site.xml、yarn-site.xml 这几个文件对格式极度敏感,少一个闭合标签、多一个不可见字符,服务启动时就会报各种莫名其妙的错。我见过有人把fs.defaultFS拼成fs.default.name,也有人把<value>和<name>的顺序写反,这些错误在日志里通常不会直接告诉你"你写错了",而是让你在一片异常栈里猜。
权限问题同样隐蔽。用 root 启动 HDFS 虽然能撑起来,但后续往 HDFS 里写文件、跑 MapReduce 任务时,会频繁遇到 Permission denied。如果创建一个专用 hadoop 用户,又需要把安装目录、数据目录的属主全部改一遍,这个环节手动操作非常零碎,漏一步就埋雷。
还有一类问题在教程里几乎不会提,就是重复执行。同一台机器上把安装流程跑两遍,环境变量可能被重复追加,hadoop-env.sh 里出现两行export JAVA_HOME,配置文件残留旧值,甚至可能出现二次格式化把集群元数据清空的情况。这些只有真正手动部署过的人才知道有多消耗耐心。
1.2 从踩坑经历提炼的脚本需求清单
踩过一圈坑之后,我给这套自动化脚本定了四条硬性标准,后面的所有实现都是围绕这四条展开的。
第一,输入要少。理想状态是一条命令搞定,最多允许通过环境变量覆盖版本号或用户名,而不是要求用户回答一堆交互式问题。第二,必须幂等。同一台机器跑两遍、跑十遍,最终状态都应该一致,不能越执行越乱。第三,JAVA_HOME 要实现自动探测,并写入所有需要它的位置。第四,失败要明确报错退出,任何一步出错都要让用户知道卡在哪、为什么,不允许留下一个半残的集群让人猜。
这四条标准听起来普通,真正在脚本里落实时会牵扯出不少细节,尤其是 JAVA_HOME 的探测和配置文件生成这两块,下面会展开讲。
2. 写脚本前的三个关键决策:JDK、目录与用户模型
2.1 JDK 版本选择:兼容边界不是越新越好
Hadoop 3.3.6 官方文档里明确的支持范围是 Java 8 和 Java 11。Java 17 虽然在 3.3.x 后期版本里做了不少适配,但并不是所有发行版都能保证完全稳定。我自己的选择是 OpenJDK 8,原因很实在:生态兼容性最好,跑 HDFS 和 YARN 时遇到的神奇问题最少,网上能查到的报错案例也最全。
这里要特别提醒一件事:不要因为机器上装了更高版本的 JDK,就想当然地让 Hadoop 直接复用它。我见过有人在开发机上装了 JDK 17,然后让部署脚本探测到了一个并不适合 Hadoop 的路径,启动时日志里全是奇怪的NoClassDefFoundError。自动化脚本做路径探测时,最好顺手校验一下探测到的 JDK 版本,不符合 Hadoop 支持范围就直接报错退出,并把检测到的版本号打印出来,让用户自己去决定换 JDK 还是换 Hadoop 版本。
2.2 目录规划:软链统一入口,升级不伤配置
很多教程直接解压到/usr/local/hadoop-3.3.6,所有配置也都写死在这个路径。这个做法本身没错,但版本升级或者多版本切换时会非常痛苦。我在脚本里做了一层很轻的抽象:安装时把 Hadoop 解压到/opt/hadoop-3.3.6,然后在/opt/hadoop建立一个指向当前版本的软链,所有环境变量和配置都只关心/opt/hadoop。
以后升级 Hadoop 版本时,只需要把新版本解压到/opt,切换软链,配置文件基本不用大改。这个思路其实和很多应用的部署方式一致,相当于给版本留了一个可以回滚的入口。软链虽然简单,脚本里却要注意一个细节:如果/opt/hadoop已经存在,不要无脑rm -rf再重建。更稳妥的做法是先判断对应进程是否在运行,再把旧目录 mv 成带时间戳的备份,例如/opt/hadoop-backup-20250101120000。自动化脚本里最怕的,就是为了省一行判断把正在使用的数据目录删了。
2.3 用户模型:root 安装、专用用户运行的服务边界
很多人图省事直接用 root 启动 Hadoop,短时间内确实能跑起来,但 HDFS 读写、MapReduce 任务经常会报权限问题。从安全角度说,让分布式计算进程拥有 root 权限也不是好实践。我的方案是安装阶段用 root,服务运行阶段全部归属于一个独立的 hadoop 用户。
这样设计之后,数据目录的所有权、SSH 密钥的权限、日志文件的归属都能统一在同一个用户下,后续出问题时的排查路径会清晰很多。有人问,为什么不让全程都用 hadoop 用户跑?因为创建用户、写/etc/profile.d、修改系统目录下的配置这些事必须有 root 权限。安装阶段用 root,运行阶段切用户,是操作便捷和安全边界之间的平衡点。
| 操作步骤 | root 执行 | hadoop 用户执行 |
|---|---|---|
| 创建系统用户 | 是 | 否 |
| 写入 profile.d 环境变量 | 是 | 否 |
| 解压安装包、建立软链 | 是 | 否 |
| 修改 etc/hadoop 下的配置 | 是 | 否 |
| 生成 SSH 密钥 | 否 | 是 |
| 格式化 NameNode | 否 | 是 |
| 启动 DFS / YARN 进程 | 否 | 是 |
这个分工表基本就是脚本里关于用户权限的完整约定。凡是涉及安装路径和系统级配置的,用 root 一次性完成;凡是涉及运行状态和密钥数据的,全部落到专用用户身上。
3. JAVA_HOME 自动配置:脚本里最容易被轻视的环节
3.1 探测 JAVA_HOME 的三种方式与优先级
JAVA_HOME 自动探测是整个脚本里最早要处理、也最容易被做砸的部分。我的探测顺序是:已存在的 JAVA_HOME 环境变量优先,其次是which java解析真实路径,最后是常见 JDK 目录兜底扫描。
第一种最简单。机器上如果已经装好了 JDK,JAVA_HOME 也写在/etc/profile或~/.bashrc里,直接复用这个值就行,没必要多做无意义的解析。第二种是大多数情况下的主力方案:先执行command -v java拿到 java 命令的路径,再用readlink -f解析出真实路径,取上一级目录作为 JAVA_HOME。为什么要多这一步readlink -f?因为很多发行版默认的 java 本身就是一个软链。
举例来说,CentOS 系统上/usr/bin/java通常指向/etc/alternatives/java,而这个文件又指向/usr/lib/jvm/java-1.8.0-openjdk-xxx/bin/java。如果不解析深层链接,直接用dirname取两次,很容易得到/usr/bin这种错误结果,Java 进程要么找不到,要么行为诡异。第三种兜底是为了处理一种更麻烦的场景:JDK 装了,但 java 命令没有放进 PATH。这时候脚本可以扫描/usr/lib/jvm/java-8-openjdk-*、/usr/lib/jvm/java-11-openjdk-*、/usr/local/java这类常见目录,只要找到bin/java就采用。三种方式都找不到,就直接报错退出,绝对不往下走。
3.2 hadoop-env.sh 动态改写:删除旧值再追加,保证幂等
找到 JAVA_HOME 之后,要把它写进 hadoop-env.sh。很多人第一反应是 sed 替换,比如把#export JAVA_HOME=这一行注释去掉,替换成实际路径。这个思路听起来没问题,实际操作却容易翻车,因为默认模板里和 JAVA_HOME 相关的行可能不止一处,有的被注释,有的是占位符,有的在文件末尾还带一段说明文字。单纯 sed 替换很容易漏。
我用的方案是先删除所有以export JAVA_HOME=开头的行,再在文件末尾追加一行正确值。这样无论模板里原本是什么状态,最终结果都只有一行有效配置,天然幂等。
JAVA_HOME_FINAL="$(detect_java_home)" ENV_FILE="${INSTALL_DIR}/etc/hadoop/hadoop-env.sh" # 删除旧值,避免重复执行时累积多行 sed -i '/^export JAVA_HOME=/d' "${ENV_FILE}" # 追加当前探测到的路径 echo "export JAVA_HOME=${JAVA_HOME_FINAL}" >> "${ENV_FILE}"注意 sed 的删除条件要精确匹配^export JAVA_HOME=,不要写成匹配所有包含 JAVA_HOME 的行,否则文件里的注释和模板说明也会被误删。追加时不要给export JAVA_HOME=${JAVA_HOME_FINAL}加单引号,因为我们希望变量展开后再写入文件,最终保存的是实际路径字符串。
3.3 profile.d 环境变量写入与 PATH 拼接细节
Hadoop 本身也要通过环境变量暴露给所有登录用户。传统做法是改/etc/profile或/etc/bashrc,但更干净的做法是在/etc/profile.d/下新建一个 hadoop.sh,登录 shell 会自动加载这个文件。
cat > /etc/profile.d/hadoop.sh <<'EOF' export HADOOP_HOME=/opt/hadoop export HADOOP_HOME_WARN_SUPPRESS=1 export PATH=$PATH:${HADOOP_HOME}/bin:${HADOOP_HOME}/sbin EOF chmod 644 /etc/profile.d/hadoop.sh这里有几个容易被忽略的细节。第一,heredoc 的定界符EOF加了单引号,里面的$PATH不会被当前 shell 展开,最终写进文件的就是字符串$PATH,等用户登录时由 shell 动态解析。第二,HADOOP_HOME_WARN_SUPPRESS的作用是抑制 Hadoop 检测到环境变量时的警告信息,自动化部署时那种黄色大段提示纯属噪音,能关就关。第三,/etc/profile.d只影响新登录的会话,当前正在运行的脚本 shell 不会自动获得 HADOOP_HOME,所以在脚本末尾要自己export HADOOP_HOME=/opt/hadoop一次,保证后续调用的 hadoop 命令能找到路径。
3.4 两个容易翻车的细节:空格路径与发行版路径差异
JAVA_HOME 自动配置里还有两个我踩过实坑的细节。第一个是 JDK 安装路径带空格或特殊字符。某些定制安装或 Windows 风格迁移过来的目录容易出现这种问题,sed 解析、Hadoop 脚本遍历都会出问题。我的处理方式是不去猜,探测到路径之后加一个简单校验:如果路径包含空格或非 ASCII 字符,直接报错,建议用户把 JDK 重新装到无空格的纯路径下。与其在一个畸形路径上反复填坑,不如在源头把它挡掉。
第二个是发行版差异。Ubuntu 上的 OpenJDK 8 路径通常是/usr/lib/jvm/java-8-openjdk-amd64,CentOS 7 上同样的 OpenJDK 8 则可能是/usr/lib/jvm/java-1.8.0-openjdk。兜底扫描时要用通配符把这类目录尽量覆盖全。我实际写的是扫/usr/lib/jvm/java-8-openjdk-*、/usr/lib/jvm/java-11-openjdk-*和/usr/local/java,宁可多扫几个候选目录,也不要让用户手填参数。
4. 核心配置文件自动生成:heredoc 整体覆盖比 sed 硬改更稳
4.1 配置文件是结构化文件,逐行替换不是好主意
Hadoop 的配置文件是 XML,结构固定但内容风格因版本而异。用 sed 去改默认模板里的某个 property 的值,需要精确匹配标签行,一长串 XML 堆在一起,误伤概率很高。再加上脚本要保证幂等,sed 反复执行时的结果很难判断。
所以我的选择是直接用 heredoc 整体覆盖这几个 XML。原因也很直白:配置文件本来就来自安装包,没有需要保留的运行时状态,每次执行脚本时用一份经过验证的固定内容覆盖,反而最容易保证最终状态一致。前提是这份配置内容必须是验证过的,不是网上随手抄来的。
4.2 core-site.xml 与 hdfs-site.xml 的生成与数据目录规划
core-site.xml 里最关键的是fs.defaultFS。伪分布式部署下通常写成hdfs://localhost:9000,让 HDFS 成为默认文件系统。这里注意 9000 是 NameNode 的 RPC 端口,不要和 9870 的 Web UI 端口混在一起。
hdfs-site.xml 里有三项必须写清楚:dfs.replication、dfs.namenode.name.dir、dfs.datanode.data.dir。伪分布式只有一个 DataNode 节点,副本数必须设成 1。如果保持默认的 3,单节点上会反复尝试复制数据,虽然多数情况下不会直接失败,但日志和性能都会很脏。数据目录要单独规划,不要放进/tmp,否则系统一重启,HDFS 的元数据和数据块就全没了。
mkdir -p /data/hadoop/namenode /data/hadoop/datanode chown -R ${SOFTWARE_USER}:${SOFTWARE_USER} /data/hadoop cat > "${INSTALL_DIR}/etc/hadoop/core-site.xml" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> </configuration> EOF cat > "${INSTALL_DIR}/etc/hadoop/hdfs-site.xml" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>dfs.replication</name> <value>1</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> </configuration> EOF这两个文件生成之后,数据目录的属主一定要改成 hadoop 用户,这是启动阶段最常见的 Permission denied 来源。
4.3 mapred-site.xml 与 yarn-site.xml:两个容易漏掉的配置
想用 YARN 跑 MapReduce,光配 HDFS 不够。mapred-site.xml 需要设置mapreduce.framework.name=yarn,告诉 MapReduce 作业把资源调度交给 YARN;yarn-site.xml 需要设置yarn.nodemanager.aux-services=mapreduce_shuffle,否则 NodeManager 不知道要为 MapReduce 的 shuffle 阶段提供辅助服务。这两个配置非常容易漏,很多手动安装教程只是提一句"按需配置",结果用户跑 WordCount 时遇到各种 Shuffle Error,排查老半天才发现是 aux-services 没配。
cat > "${INSTALL_DIR}/etc/hadoop/mapred-site.xml" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> </configuration> EOF cat > "${INSTALL_DIR}/etc/hadoop/yarn-site.xml" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>2048</value> </property> </configuration> EOF004
4.4 端口冲突与内存参数:自动化脚本必须留的覆盖口
自动化脚本生成固定配置,必须给一些可变参数留出口。第一类是可覆盖端口:9000、9870、8088 这些端口如果在机器上被其他服务占用,脚本里应该允许用户通过环境变量HDFS_PORT、YARN_PORT覆盖默认值。第二类是内存参数:YARN 默认的分配在某些小内存机器上会显得很夸张,我在 yarn-site.xml 里显式设置yarn.nodemanager.resource.memory-mb,避免在 2G 内存的机器上直接把系统吃满。实际脚本里可以读取free命令的输出自动计算一个合理值,也可以留一个变量让用户手工指定。
还有一点容易被忽略:Hadoop 的启动脚本会执行bin/hdfs和bin/yarn,这些命令依赖 libexec 下的脚本。整体覆盖配置不影响这部分,但如果自定义目录结构,例如把数据目录放到独立挂载盘上,需要保证目录的属主、属组和 hadoop 用户一致,否则启动时照样会碰到 Permission denied。
5. SSH 免密、NameNode 格式化与服务启动的幂等设计
5.1 localhost 免密不是可选项:HDFS 启动机制要求
很多伪分布式教程会让用户配置ssh localhost免密,新手往往不理解:这台机器不就一个节点吗,为什么还要 SSH?原因是 HDFS 的启动脚本在拉起 DataNode 和 SecondaryNameNode 时,默认会通过 SSH 方式在目标主机上执行命令,即使目标就是 localhost 也一样。不配免密,start-dfs.sh 会在每一步都停下来等密码输入,自动化就无从谈起。
这一步在脚本里其实不难实现。真正的坑在于权限:authorized_keys文件必须是 600,.ssh目录必须是 700,否则 SSH 客户端会直接拒绝信任这个文件。脚本里生成密钥、追加公钥之后,一定要手动 chmod、chown 一遍。
5.2 密钥生成与 authorized_keys 去重实现
生成密钥时用-N ''表示空密码,配合-q静默模式。大多数现代 Linux 上的 SSH 客户端生成 RSA 4096 密钥没有兼容性问题。authorized_keys的去重逻辑也很简单:如果id_rsa.pub的内容已经存在于authorized_keys中,就不再追加,防止重复执行脚本时公钥越堆越多。
KEY="${SOFTWARE_HOME}/.ssh/id_rsa" if [[ ! -f "${KEY}" ]]; then su - ${SOFTWARE_USER} -c "ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N '' -q" fi if ! grep -qF "$(cat ${KEY}.pub)" "${SOFTWARE_HOME}/.ssh/authorized_keys" 2>/dev/null; then cat "${KEY}.pub" >> "${SOFTWARE_HOME}/.ssh/authorized_keys" fi chmod 700 "${SOFTWARE_HOME}/.ssh" chmod 600 "${SOFTWARE_HOME}/.ssh/authorized_keys" chown -R ${SOFTWARE_USER}:${SOFTWARE_USER} "${SOFTWARE_HOME}/.ssh"grep -qF的-F参数值得多说一句:它表示把公钥内容当作固定字符串匹配,而不是正则表达式。公钥里含有很多.、+、/这样的字符,如果不加-F,某些字符会被当作正则元字符,匹配结果不可信。这是一个很小但非常实用的脚本习惯。
5.3 格式化 NameNode:最危险操作的自我保护
bin/hdfs namenode -format一执行,NameNode 的数据目录就会被初始化。如果同一台机器不小心执行了两次,第二次格式化会清空上一次的元数据。虽然 name 目录里的 VERSION 文件会被更新,但集群里的 DataNode 还是旧版本的注册信息,重启后经常会出现 DataNode 无法加入集群的情况。所以脚本里绝不能无脑每次执行都格式化。
我的做法是检查dfs.namenode.name.dir对应目录下是否已经存在current/VERSION文件,存在就跳过格式化,不存在才执行。这样脚本可以安全地重复执行。真要强制重建集群时,应该手动删除数据目录,或者给脚本加一个--force参数,这个选项绝不能做成默认流程。
if [[ ! -f /data/hadoop/namenode/current/VERSION ]]; then su - ${SOFTWARE_USER} -c "${INSTALL_DIR}/bin/hdfs namenode -format" else log "NameNode 已格式化,跳过。如需重建请手动删除 /data/hadoop/namenode" fi5.4 启动顺序、进程检查与 pid 文件清理
配置和格式化都没问题之后,启动顺序一般是先start-dfs.sh,再start-yarn.sh。等几秒钟,然后用hdfs dfsadmin -report或jps验证进程。脚本里至少要做两层验证:一是启动命令本身的退出码,二是进程数量。jps的输出里应该至少能看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 这几个进程。只要有一个进程缺失,脚本就应该返回非 0 退出码,并把日志文件路径打出来。
这里补充一个我个人的习惯:启动前先清理/tmp/hadoop-${USER}下的 pid 文件。Hadoop 启动时会把进程号写到这个临时目录,进程异常退出后 pid 文件可能残留,导致下一次启动时脚本误以为进程还在运行,各种启动流程半途而废。这个坑在手动部署时很难想到,但遇到一次就会记得很牢。
6. 完整脚本逐段拆解与运行观察
6.1 一条命令触发的主流程顺序
整套脚本我习惯命名为install-hadoop-3.3.6.sh,放在一台干净的最小化 Linux 机器上,用 root 执行sudo bash install-hadoop-3.3.6.sh。脚本不依赖交互式输入,版本号、安装目录、用户名都集中在顶部变量区。执行流程依次是:检查 root 权限、探测 JDK、创建用户、下载并解压安装包、写入环境变量、生成四个 XML、配置 SSH 免密、格式化 NameNode、启动服务、输出验证结果。每一步都有带[INFO]前缀的日志,出错则[ERROR]终止。
6.2 脚本主流程骨架与设计亮点
下面是一个精简但能够跑通主流程的脚本骨架,去掉了部分日志和防御性判断,保留核心逻辑:
#!/usr/bin/env bash set -euo pipefail HADOOP_VERSION=3.3.6 INSTALL_DIR=/opt/hadoop SOFTWARE_USER=hadoop SOFTWARE_HOME=/home/hadoop log(){ echo "[INFO] $(date '+%F %T') $*"; } die(){ echo "[ERROR] $*" >&2; exit 1; } # JDK 探测:环境变量优先,其次 which java,最后常见目录 detect_java_home(){ [[ -n "${JAVA_HOME:-}" ]] && { echo "$JAVA_HOME"; return; } local bin real bin=$(command -v java 2>/dev/null || true) if [[ -n "$bin" ]]; then real=$(readlink -f "$bin") echo "$(cd "$(dirname "$real")/.." && pwd)" return fi for d in /usr/lib/jvm/java-8-openjdk-* /usr/lib/jvm/java-11-openjdk-* /usr/local/java; do [[ -x "$d/bin/java" ]] && { echo "$d"; return; } done return 1 } [[ $(id -u) -eq 0 ]] || die "请使用 root 运行" JAVA_HOME_FINAL=$(detect_java_home) || die "没有找到 JDK 安装路径" # 创建专用用户 id "$SOFTWARE_USER" &>/dev/null || useradd -m -s /bin/bash "$SOFTWARE_USER" # 下载并解压 if [[ ! -f /tmp/hadoop-${HADOOP_VERSION}.tar.gz ]]; then wget -q "https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}.tar.gz" \ -O /tmp/hadoop-${HADOOP_VERSION}.tar.gz fi tar -xzf /tmp/hadoop-${HADOOP_VERSION}.tar.gz -C /opt ln -sfn "/opt/hadoop-${HADOOP_VERSION}" "$INSTALL_DIR" # JAVA_HOME 写入 hadoop-env.sh ENV_FILE="$INSTALL_DIR/etc/hadoop/hadoop-env.sh" sed -i '/^export JAVA_HOME=/d' "$ENV_FILE" echo "export JAVA_HOME=${JAVA_HOME_FINAL}" >> "$ENV_FILE" # profile.d 环境变量 cat > /etc/profile.d/hadoop.sh <<'EOF' export HADOOP_HOME=/opt/hadoop export HADOOP_HOME_WARN_SUPPRESS=1 export PATH=$PATH:${HADOOP_HOME}/bin:${HADOOP_HOME}/sbin EOF # 生成核心配置(此处以 core-site.xml 为例,其余文件同理) cat > "$INSTALL_DIR/etc/hadoop/core-site.xml" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> </configuration> EOF # SSH 免密 mkdir -p "$SOFTWARE_HOME/.ssh" [[ -f "$SOFTWARE_HOME/.ssh/id_rsa" ]] || su - "$SOFTWARE_USER" -c "ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N '' -q" grep -qF "$(cat $SOFTWARE_HOME/.ssh/id_rsa.pub)" "$SOFTWARE_HOME/.ssh/authorized_keys" 2>/dev/null || \ cat "$SOFTWARE_HOME/.ssh/id_rsa.pub" >> "$SOFTWARE_HOME/.ssh/authorized_keys" chmod 700 "$SOFTWARE_HOME/.ssh" chmod 600 "$SOFTWARE_HOME/.ssh/authorized_keys" chown -R "$SOFTWARE_USER":"$SOFTWARE_USER" "$SOFTWARE_HOME/.ssh" # 格式化保护 if [[ -f /data/hadoop/namenode/current/VERSION ]]; then log "NameNode 已格式化,跳过" else su - "$SOFTWARE_USER" -c "$INSTALL_DIR/bin/hdfs namenode -format" fi # 启动与验证 su - "$SOFTWARE_USER" -c "$INSTALL_DIR/sbin/start-dfs.sh" su - "$SOFTWARE_USER" -c "$INSTALL_DIR/sbin/start-yarn.sh" sleep 5 su - "$SOFTWARE_USER" -c "jps" | grep -E "NameNode|DataNode|ResourceManager|NodeManager" log "部署完成"这段骨架里有两个地方值得再强调。第一是set -euo pipefail必须放在最前面,它让脚本在出现未定义变量、管道失败、任何命令非零退出时及时终止,避免带着错误继续往下执行。第二是ln -sfn之前没有做旧版本备份,这是我在 2.2 里提到的边界情况,生产环境里应该补上备份逻辑。
工程化一点的话,我还会在这个骨架基础上加三样东西。第一,下载安装包后校验 SHA256,防止半路断点导致解压失败。第二,把脚本的完整输出 tee 到/var/log/install-hadoop.log,这样即使脚本跑挂了,也能翻日志回看是哪一步出的问题。第三,在脚本入口解析参数,支持--skip-format、--force这类显式开关,把危险动作完全交给用户控制。
6.3 一次实际运行的关键输出与验证
首次运行时,格式化 NameNode 那一步会输出一串类似 Base64 的 key 和日志文字,那是 Hadoop 生成 cluster ID 的正常过程,不要看到大段输出就以为脚本卡死。启动完成后执行jps,正常情况下会看到类似下面的输出:
12345 NameNode 12346 DataNode 12347 SecondaryNameNode 12348 ResourceManager 12349 NodeManager同时打开浏览器访问http://<ip>:9870,可以看到 NameNode 的 Web UI。如果这里看不到 DataNode,或者 Web UI 起不来,优先查/opt/hadoop/logs目录下的hadoop-hadoop-namenode-<hostname>.log。日志文件里通常已经写了很明确的根因,比任何脑补都靠谱。
7. 常见故障、排查思路与后续扩展
7.1 三个高频报错:从日志到根因的排查链路
第一个高频报错是JAVA_HOME is not set。如果你是用这套脚本装的,先看hadoop-env.sh里export JAVA_HOME=这一行是否存在,然后手动执行$HADOOP_HOME/bin/hdfs version,看能否正常输出版本信息。如果hdfs命令都识别不了 JDK,说明 profile.d 的环境变量还没有在当前 shell 生效,或者 JAVA_HOME 路径本身有问题,需要先验证这个路径下能不能直接执行bin/java。
第二个高频报错是 NameNode 进程反复重启或直接闪退。优先看日志文件,最常见的原因是数据目录没有写权限,或者dfs.namenode.name.dir指定的目录不存在。脚本里已经做了 mkdir 和 chown,但如果你自行修改了数据目录,这些权限就需要自己确认。判断方法很简单:切到 hadoop 用户,手动执行$HADOOP_HOME/bin/hdfs namenode -format,如果系统提示无法创建目录或无权访问,那就是权限问题。
第三个高频报错是 DataNode 无法注册。最典型的发生场景是格式化执行了两次,NameNode 的 clusterID 变了,DataNode 还在用旧的。现象是 DataNode 进程存在,但始终连不上 NameNode,日志里反复出现注册失败的提示。处理方式也直接:停止所有进程,删除 namenode 和 datanode 目录下的内容,重新格式化,再启动。这个操作会清空 HDFS 上所有数据,执行前一定确认清楚。
7.2 重复执行与版本升级的边界
这套脚本本身是幂等的,重复执行不会产生两份配置文件。但有一个边界要说明白:脚本里ln -sfn之前没有做旧版本备份逻辑。如果两次执行之间换了 Hadoop 大版本,/opt/hadoop会指向新版本,而/data/hadoop里的元数据还是旧版本格式。小版本升级问题不大,跨大版本建议重建集群,不要硬撑。
另一个重复执行时的隐患是下载包缓存。脚本用/tmp/hadoop-3.3.6.tar.gz作为缓存,如果第一次下载时文件损坏,第二次脚本发现文件存在就会跳过下载,解压时直接失败。稳妥的做法是在解压前校验 tar.gz 的 SHA256 值。这一行校验代码非常值得加,能省掉一类很隐蔽的失败场景。
7.3 从伪分布式扩展到完全分布式与容器化的思路
这套脚本的设计思路完全可以扩展到完全分布式。核心改动是:把localhost换成主节点主机名,把fs.defaultFS的 value 改成hdfs://namenode:9000,在 workers 文件里列出所有 DataNode 主机,SSH 免密从单机自己做变成批量分发公钥,格式化和启动只在主节点执行。关键配置可以抽到一个 config.env 里,让脚本统一读取。
如果不想折腾虚拟机,而是希望上容器,脚本里的目录探测和配置生成逻辑依然可以复用,只是要把su - hadoop换成容器内的 ENTRYPOINT,把/data/hadoop挂载成持久化卷。我个人经验是,先在一台机器上把这个单机伪分布式跑透,理解 NameNode、DataNode、ResourceManager、NodeManager 之间的关系,再上容器或者完全分布式,会顺畅很多。
最后分享一个小习惯:不管脚本写得再好,我都建议在跑之前看一眼当前机器的内存和磁盘。Hadoop 3.3.6 的 NameNode 默认 JVM 参数不算激进,但 2G 内存的机器上同时起 NameNode、DataNode、ResourceManager、NodeManager 四个进程还是会非常紧张。自动化脚本能帮你省去大多数重复劳动,但机器资源这件事,脚本永远无法替你判断。