跳转至

HDFS 高可用集群部署实战(QJM 手动切换版)

作者:JiangChong | 撰写时间:2026年09月

适用场景:动手从零搭一套 HDFS HA 测试集群(双 NameNode + 3 JournalNode,手动切换,全程不依赖 ZooKeeper)。理论前置:HDFS 高可用(QJM、ZKFC、Federation)(重点第 2、3、5 节)。本文搭好的环境可继续承接 ZooKeeper 部署运维与监控实战:同一批机器部署 ZK、升级为自动故障转移(见文末「后续扩展」)。

阅读前回顾(30 秒)

关联文章

理解全文脉络

按「规划 → 准备 → 配置 → 初始化 → 验证 → 排障」组织:第 1 节定布局(3 台机器谁跑什么,预留 ZK 槽位),第 2 节环境准备(用户 / 免密 / 目录),第 3 节三个配置文件,第 4 节初始化与启动顺序(最容易出错的环节,逐命令配「为什么」),第 5 节验证与手动切换演练,第 6 节高频起不来原因速查。

新手建议按顺序走一遍;已经有环境的读者重点看第 4、5 节。

本文命令在 Linux(systemd 发行版)上执行;以 Hadoop 3.x 为准(2.x 差异处标注)。所有默认值以 Apache Hadoop 官方文档为准。


1 规划与布局

1.1 三台机器怎么分

机器 部署 预留
hadoop001.example.com NN1 + JN① + DataNode ZK①(见文末「后续扩展」)
hadoop002.example.com NN2 + JN② + DataNode ZK②
hadoop003.example.com JN③ + DataNode ZK③

布局逻辑:JN 必须摊到 3 台机器——多数派的容错意义在于「一台机器挂掉只损失 1 个 JN」;两个 NN 分开放——同机等于一次硬件故障带走整个 HA 双机;每台一个 DataNode——数据要三副本落盘(副本数默认 3,正好一台一份)。

1.2 资源与端口

内存预算:NN 堆 1-2G + JN 约 512M-1G + DN 1-2G,4G/台可跑、8G 舒适(预留 ZK 后再加 2-4G,见 ZK 02 篇)。

组件 端口(Hadoop 3.x) 2.x 差异
NameNode RPC 8020 相同
NameNode Web UI 9870 50070
JournalNode RPC 8485 相同
JournalNode Web UI 8480 相同
DataNode 数据传输 9866 50010
DataNode Web 9864 50075

版本:Hadoop 3.x 二进制包(示例用 3.3.6,以官方最新 stable 为准;Debian 13 须用 3.5.x,原因见 2 节差异块)。


2 环境准备(每台机器执行一遍)

# ① 安装 JDK:Hadoop 3.x 需 8+(3.5.x 起要求 Java 17;CentOS/RHEL 示例,Debian 13 见下方差异块)
yum install -y java-1.8.0-openjdk-devel
java -version

# ② 创建 hdfs 用户(三台都建;sshfence 以该用户 ssh 到对端执行)
useradd -m -s /bin/bash hdfs

# ③ 下载解压(版本号以官网最新 stable 为准)
wget https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
tar -xzf hadoop-3.3.6.tar.gz -C /opt
ln -s /opt/hadoop-3.3.6 /opt/hadoop
chown -R hdfs:hdfs /opt/hadoop
# 验证:ls -ld /opt/hadoop 应显示 hdfs:hdfs;否则启动 daemon 时报
# 「Unable to create /opt/hadoop/logs. Permission denied」

# ④ 主机名互认:三台机器的 /etc/hosts 都加上这三行
cat >> /etc/hosts <<EOF
172.20.65.69 hadoop001 hadoop001.example.com
172.20.65.71 hadoop002 hadoop002.example.com
172.20.65.70 hadoop003 hadoop003.example.com
EOF

# ⑤ SSH 免密:hadoop001 ↔ hadoop002 双向(fencing 时新 Active 要 ssh 到旧 Active 侧杀进程)
#    hadoop003 不需要:它没有 NN——免密只服务于 fencing(hadoop001↔hadoop002);本文逐台
#    hdfs --daemon 启动(不用 start-dfs.sh),没有「主控机 ssh 全部节点」的需求
#    hadoop001 上执行:
su - hdfs
ssh-keygen -t rsa -m PEM -f ~/.ssh/id_rsa -N ''
#    -m PEM 必须加:sshfence 走 Java 的 JSch 库,不认 OpenSSH 新格式私钥(报 invalid
#    privatekey);OpenSSH 客户端两种格式都认——光测 ssh 免密通过不代表 sshfence 可用
ssh-copy-id hdfs@hadoop002.example.com
#    hadoop002 上执行:
su - hdfs
ssh-keygen -t rsa -m PEM -f ~/.ssh/id_rsa -N ''
ssh-copy-id hdfs@hadoop001.example.com
# 验证:hadoop001 上执行 ssh hdfs@hadoop002.example.com 'echo ok',不应提示密码

# ⑥ 数据目录:NN 元数据 / JN edits / DN 数据,全部显式指定到持久化磁盘(默认全落 /tmp!)
# 使用 root 执行
mkdir -p /data/hdfs/{namenode,journalnode,datanode}
chown -R hdfs:hdfs /data/hdfs

Debian 13 差异:①②③三步命令不同,其余步骤(④⑤⑥)完全相同:

# ① JDK:Debian 13 主仓库只有 OpenJDK 21/25,而 Hadoop 3.5 服务端要求 Java 17
#    → 装 Adoptium Temurin 17;Hadoop 换 3.5.x(3.3.x/3.4.x 不支持 Java 17 运行时)
#    先刷新索引:新装系统不 apt update 直接 install 会报 no installation candidate
apt update && apt install -y curl
#    仓库 key 直接存 .asc(armored 格式),apt 的 signed-by 原生可读,无需 gpg/gnupg
curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public -o /usr/share/keyrings/adoptium.asc
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" > /etc/apt/sources.list.d/adoptium.list
apt update && apt install -y temurin-17-jdk
java -version

# ② 创建用户:Debian 习惯用 adduser(--disabled-password 跳过交互)
adduser --disabled-password --gecos "" hdfs

# ③ 下载:Debian 13 默认没有 wget,用 curl(-L 跟随重定向)
curl -fLO https://dlcdn.apache.org/hadoop/common/hadoop-3.5.0/hadoop-3.5.0.tar.gz

tar -xzf hadoop-3.5.0.tar.gz -C /opt
ln -s /opt/hadoop-3.5.0 /opt/hadoop
chown -R hdfs:hdfs /opt/hadoop /opt/hadoop-3.5.0
# 验证:ls -ld /opt/hadoop 应显示 hdfs:hdfs;否则启动 daemon 时报
# 「Unable to create /opt/hadoop/logs. Permission denied」

为方便后续命令,做两处环境配置(三台都要,以 hdfs 用户执行):

# ① ~/.bashrc 加两行:PATH 方便直接敲 hdfs/haadmin
export HADOOP_HOME=/opt/hadoop
export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH

source ~/.bashrc

# ② 设置 JAVA_HOME:Linux 上 hdfs --daemon 启动必须显式设置,
#    否则报 ERROR: JAVA_HOME is not set and could not be found.
#    先查 JDK 真实路径,去掉末尾的 /bin/java 即 JAVA_HOME:
readlink -f $(which java)
# 输出示例(Debian 13 + Temurin 17):/usr/lib/jvm/temurin-17-jdk-amd64/bin/java
# 追加到 hadoop-env.sh(Hadoop 官方推荐位置,所有 daemon 启动都读它):
echo 'export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64' >> $HADOOP_HOME/etc/hadoop/hadoop-env.sh
# 验证:能打印版本信息即生效,不应再报 JAVA_HOME 错误
hdfs version

3 配置:三个文件

三个文件都位于 $HADOOP_HOME/etc/hadoop/(本文即 /opt/hadoop/etc/hadoop/),解压包自带空模板(内容只有 <configuration></configuration>),用下面内容直接覆盖即可;三个文件每台机器一份、内容相同。

与 HDFS 高可用(QJM、ZKFC、Federation) 第 8 节的关系:那里是完整注释版(并拆分了 NN/JN 两个版本),本文给出可复制的手动切换版,两者唯一差别是本文没有 dfs.ha.automatic-failover.enabled 和 ha.zookeeper.quorum 两个参数——那是接 ZK 时才加的(见文末「后续扩展」)。

3.1 core-site.xml(所有机器相同)

<configuration>
  <!-- 默认文件系统指向逻辑名,客户端写 hdfs://mycluster/... -->
  <property>
    <name>fs.defaultFS</name>
    <value>hdfs://mycluster</value>
  </property>
</configuration>

3.2 hdfs-site.xml(所有机器相同,一份通用)

<configuration>
  <!-- 逻辑命名空间与两个 NN -->
  <property><name>dfs.nameservices</name><value>mycluster</value></property>
  <property><name>dfs.ha.namenodes.mycluster</name><value>nn1,nn2</value></property>

  <!-- 两个 NN 的 RPC 与 Web 地址 -->
  <property><name>dfs.namenode.rpc-address.mycluster.nn1</name><value>hadoop001.example.com:8020</value></property>
  <property><name>dfs.namenode.rpc-address.mycluster.nn2</name><value>hadoop002.example.com:8020</value></property>
  <property><name>dfs.namenode.http-address.mycluster.nn1</name><value>hadoop001.example.com:9870</value></property>
  <property><name>dfs.namenode.http-address.mycluster.nn2</name><value>hadoop002.example.com:9870</value></property>

  <!-- 共享 EditLog:qjournal 协议 + 3 台 JN(分号分隔) -->
  <property><name>dfs.namenode.shared.edits.dir</name><value>qjournal://hadoop001.example.com:8485;hadoop002.example.com:8485;hadoop003.example.com:8485/mycluster</value></property>

  <!-- 客户端故障转移代理:连的 NN 挂了自动重连另一个 -->
  <property><name>dfs.client.failover.proxy.provider.mycluster</name><value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value></property>

  <!-- fencing:切换时先 ssh 杀掉旧 Active(防脑裂的保险,03 篇第 5 节) -->
  <property><name>dfs.ha.fencing.methods</name><value>sshfence</value></property>
  <property><name>dfs.ha.fencing.ssh.private-key-files</name><value>/home/hdfs/.ssh/id_rsa</value></property>

  <!-- 三个数据目录全部显式指定:默认会落到 /tmp,重启即丢 -->
  <property><name>dfs.namenode.name.dir</name><value>/data/hdfs/namenode</value></property>
  <property><name>dfs.datanode.data.dir</name><value>/data/hdfs/datanode</value></property>
  <property><name>dfs.journalnode.edits.dir</name><value>/data/hdfs/journalnode</value></property>
</configuration>

说明:dfs.journalnode.edits.dir 只被 JournalNode 读取,NN/DN 主机上配了也无害——为上手方便三台用同一份文件;想按机器拆分见 03 篇第 8 节。

3.3 workers 文件(DataNode 列表,所有机器相同)

hadoop001.example.com
hadoop002.example.com
hadoop003.example.com

2.x 注意:该文件叫 slaves,放 $HADOOP_HOME/etc/hadoop/ 下。

3.4 防火墙放行

云上(阿里云等)注意:本文 /etc/hosts 用内网 IP,组件间通信走内网、无需公网放行;只有想从公网访问的 Web 端口(9870/8480/9864)需要在云控制台安全组放行——建议源 IP 只限自己的电脑,用完即关(Web UI 无鉴权)。

# CentOS/RHEL firewalld 示例
firewall-cmd --permanent --add-port={8020,8485,8480,9870,9864,9866,8019}/tcp
firewall-cmd --reload

# Debian 13 / Ubuntu:Debian 13 默认未启用防火墙,装 ufw 后放行
apt install -y ufw
ufw allow ssh                              # 先放行 22:否则远程 SSH 下 enable 会切断自己
ufw allow 8020,8485,8480,9870,9864,9866,8019/tcp
ufw --force enable                         # --force 跳过「may disrupt ssh」交互确认(22 已放行,安全)
ufw status

8019 是 HA 切换通道端口:Hadoop 2 手动切换版里是 NameNode 的 HA admin 端口(dfs.namenode.ha.admin-address),haadmin -failover 直连它;Hadoop 3 + ZKFC 自动切换时由 ZKFC 监听(dfs.ha.zkfc.port)。漏放它不会影响日常读写,但 haadmin -failover 会卡死(connect 20s 超时重试)。


4 初始化与启动顺序(最容易错的一节)

顺序就是命:JN 先就位 → NN1 格式化 → 初始化共享 edits → NN1 启动 → NN2 拷贝元数据并启动 → DataNode。逐条执行:

# 1. 先起全部 JournalNode(三台都执行)
#    为什么:共享记账必须先就位(03 篇第 3 节),否则后面每一步都失败
hdfs --daemon start journalnode
# 验证:jps 应看到 JournalNode 进程;浏览器看 http://hadoop001.example.com:8480

# 2. 格式化 NN1(只在 hadoop001 执行一次;NN2 永远不要执行 -format!)
#    为什么:双 NN 必须共享同一份元数据,两个 NN 各 format 一次 = 两个不同的 namespace
hdfs namenode -format
# 验证:输出含 successfully formatted;末尾 SHUTDOWN_MSG 是正常收尾(format 完即
#    退出,不是报错);若提示 Re-format ... (Y or N) 说明目录已有旧元数据,确认
#    不要了再输入 Y
# 注意:-format 会连 3 台 JN 一起初始化,且要求全部成功;JN 的 format 不支持覆盖
#    (目录已有内容即报「current directory is not empty」,重跑带 force 也无效),
#    失败重试前要把三台 JN 的 mycluster 全部清空,见第 6 节

# 3. 初始化共享 edits(兜底步骤,通常可跳过:-format 已把 3 台 JN 一起初始化——
#    format 失败的历史堆栈里 QuorumJournalManager.format 正是 JN 初始化环节)
#    验证:三台 ls /data/hdfs/journalnode/mycluster/current/ 都有 VERSION 即已就绪;
#    某台没有 VERSION 才需要执行本步(只在 hadoop001 执行一次)
#    注意:必须在 format 之后执行,顺序颠倒会报「NameNode is not formatted」
hdfs namenode -initializeSharedEdits

# 4. 启动 NN1
hdfs --daemon start namenode

# 5. NN2 拷贝元数据(在 hadoop002 执行;是 bootstrapStandby,不是 format!)
#    为什么:把 NN1 的 FSImage + EditLog 完整拷到 NN2,保证两个 NN「记忆一致」
hdfs namenode -bootstrapStandby

# 6. 启动 NN2
hdfs --daemon start namenode

# 7. 起 DataNode(三台都执行)
hdfs --daemon start datanode

# 8. 验证并指定 Active:手动切换版(无 ZKFC)新集群起完两个 NN 都是 standby,
#    没有任何组件自动推选 active——必须人工指定(官方文档明确此行为)
hdfs haadmin -getAllServiceState
# 预期:两行都是 standby(格式:地址:端口 状态)→ 正常,继续下一步

hdfs haadmin -transitionToActive nn1    # 人工指定 nn1 为 Active
hdfs haadmin -getAllServiceState
# 预期:hadoop001.example.com:8020  active / hadoop002.example.com:8020  standby

Web UI 怎么看(服务器无桌面环境):不必在服务器装浏览器,两种方式:

  1. 云上(阿里云等,有公网 IP):安全组放行 Web 端口(9870/8480/9864),浏览器直接访问 http://<公网IP>:9870(NN1)、http://<公网IP>:8480(JN)。⚠️ Web UI 无鉴权且可浏览/下载文件——安全组源 IP 只放行自己的电脑,用完即关。
  2. 内网 / 无公网 IP:SSH 隧道带回本地(保持隧道开着,第 4、5 节的「浏览器看」都走它):
ssh -L 8480:hadoop001.example.com:8480 -L 9870:hadoop001.example.com:9870 -L 19870:hadoop002.example.com:9870 hdfs@hadoop001.example.com

之后本地浏览器访问 http://localhost:8480(JN)、http://localhost:9870(NN1)、http://localhost:19870(NN2,本地端口不能重复所以用 19870);或直接在服务器上 curl -s http://hadoop001.example.com:8480 | head 确认有响应。

2.x 注意:hdfs --daemon start <进程> 换成 hadoop-daemon.sh start <进程>。

三个高频翻车点再强调一次:NN2 执行了 -format(元数据分叉)、漏了 -initializeSharedEdits(NN 报共享 edits 目录不存在)、-format 与 -initializeSharedEdits 顺序颠倒(报 NameNode is not formatted)。第 6 节有对应排查。


5 验证清单与切换演练

5.1 基础验证与写测试

# 两个 NN 的 Web UI(hadoop001/hadoop002 的 9870 端口)应分别显示 active / standby

# 写测试
hdfs dfs -mkdir /demo
echo hello > /tmp/hello.txt
hdfs dfs -put /tmp/hello.txt /demo/
hdfs dfs -cat /demo/hello.txt
# hello

# 看 JN 的 edits 段文件(hadoop001 上):每次元数据写都会留下痕迹
ls /data/hdfs/journalnode/mycluster/current/ | head
# edits_0000000000000000001-...

5.2 手动切换演练

hdfs haadmin -getAllServiceState   # 前置确认:nn1 是 active
# 预期:hadoop001.example.com:8020  active

hdfs haadmin -failover nn1 nn2
# 输出:Failover from nn1 to nn2 successful
# 关键认知:对端健康时是「优雅降级」——nn2 先通过 RPC 请 nn1 转 standby(进程保留),
#    所以 hadoop001 上 jps 看到 NameNode 还在是正常的;sshfence 杀进程只是
#    「对端无响应」时的防脑裂保险,正常切换不触发

hdfs haadmin -getAllServiceState
# 预期:hadoop001.example.com:8020  standby / hadoop002.example.com:8020  active

这就是「手动切换版」的含义:NN 挂了集群就不可写,直到人来换。要消灭这个人肉窗口,就是文末「后续扩展」的升级路径——加上 ZKFC 的自动故障转移(03 篇第 4 节)。


6 高频起不来原因

现象 原因 处理
hdfs --daemon 报「JAVA_HOME is not set and could not be found」 没设置 JAVA_HOME(Linux 上必须显式设,不像 java 命令能靠 PATH 找到) 见 2 节末尾:readlink -f $(which java) 取路径写入 hadoop-env.sh
启动 daemon 报「Unable to create /opt/hadoop/logs. Permission denied」 /opt/hadoop 归属不是 hdfs(chown 漏跑) root 执行 chown -R hdfs:hdfs /opt/hadoop(见 2 节③)
NN 启动报「共享 edits 目录不存在」 JN 没先起 / 漏执行 -initializeSharedEdits 按第 4 节顺序重来
-initializeSharedEdits 报「NameNode is not formatted」 顺序颠倒:先执行了 initializeSharedEdits(它要读本地已 format 的元数据) 先 hdfs namenode -format,再 -initializeSharedEdits(第 4 节顺序)
-format 报「Can't format the storage directory because the current directory is not empty」 JN 的 edits 目录已有内容(上次失败/成功的 format 留下);JN format 不支持覆盖,且 QJM format 要求 3 台全部成功 停三台 JN → 三台都 rm -rf /data/hdfs/journalnode/mycluster → 起 JN → hadoop001 重跑 -format(提示输 Y)
JN 重启后集群异常(VERSION 不匹配) dfs.journalnode.edits.dir 默认落 /tmp 显式配置到持久化磁盘后重做
haadmin -failover 卡住 / 报 fencing 错误 sshfence 免密不通(单向、密钥路径错、home 权限问题) 双向免密;hdfs 用户 home 目录建议 700 权限
failover 报「invalid privatekey / Unable to create SSH session」→ Fencing failed sshfence 的 JSch 库不认 OpenSSH 新格式私钥(ssh-keygen 默认生成;OpenSSH 客户端却能用,所以免密测试通过不代表 sshfence 可用) 两台都执行 ssh-keygen -p -m PEM -f ~/.ssh/id_rsa -N '' 原地转 PEM(公钥不变、免密关系保留)
两个 NN 各自为政、元数据不一致 NN2 用了 -format 而非 -bootstrapStandby 停 NN2,重新 -bootstrapStandby
客户端连不上 / 写超时 防火墙未放行 8020/8485/9870 等 按 3.4 放行(2.x 注意 50070)
写失败但 NN 状态正常 DataNode 没起 / data.dir 落 /tmp 起 DN;显式配置 dfs.datanode.data.dir

最佳实践清单

  1. JN 固定 3 台奇数、分机部署 —— 为什么:多数派摊到 3 台机器才有「挂 1 台不慌」的容错(03 篇第 3.1 节)。
  2. name.dir / edits.dir / data.dir 全部显式配置到持久化磁盘 —— 为什么:三者默认都落 /tmp,重启即丢,是本系列第一大部署事故源。
  3. sshfence 免密先测通(hadoop001 ↔ hadoop002 双向) —— 为什么:fencing 是防脑裂的最后保险,免密不通 = 保险失效(03 篇最佳实践第 7 条)。
  4. 双 NN 元数据只用 -bootstrapStandby 同步,绝不二次 format —— 为什么:两次 format = 两个不同 namespace,NN 元数据分叉。
  5. 客户端一律用 nameservice 逻辑名(hdfs://mycluster) —— 为什么:直连 NN IP 等于把 HA 架空(03 篇最佳实践第 4 条)。
  6. 部署完成后做一次手动 failover 演练 —— 为什么:把「fencing 杀进程 → 切换 → 客户端重连」的纸面流程变成手感(03 篇最佳实践第 3 条)。
  7. JN 升级/重启逐个滚动,保证多数派在线 —— 为什么:同一时刻最多停 1 台 JN,停第 2 台 EditLog 写入即阻塞(03 篇最佳实践第 9 条)。
  8. 本套环境继续承接 ZK 02 篇 —— 为什么:同一批机器部署 ZooKeeper、升级自动故障转移,是 03 篇第 4 节 ZKFC 理论的最佳落地路径。

附录:新手常见疑问(FAQ)

Q1:没有 ZooKeeper,这套集群能自动切换吗? 不能。自动故障转移必须 ZKFC + ZooKeeper(03 篇第 4 节),本文是手动切换版。升级路径:ZooKeeper 部署运维与监控实战 第 1 节(同一批机器部署 ZK)→ 第 5 节(升级步骤与联动验证)。

Q2:-bootstrapStandby 和 -format 到底差在哪? format 创建全新元数据——一套集群只能 format 一次;bootstrapStandby 从当前 Active 把 FSImage + EditLog 完整拷过来,保证第二个 NN 与第一个「记忆一致」。

Q3:能只配 2 个 JN 吗? 不能。2 台的多数派 = 2,挂 1 台就停摆,与 1 台容错相同但更贵(03 篇第 3.1 节的表)。3 台是最小配置。

Q4:haadmin -failover 报 fencing 失败怎么办? 按顺序查:①私钥格式——sshfence 走 JSch,不认 OpenSSH 新格式(报 invalid privatekey),ssh-keygen -p -m PEM -f ~/.ssh/id_rsa -N '' 转 PEM;②ssh 免密是否双向;③dfs.ha.fencing.ssh.private-key-files 路径;④hdfs 用户 home 目录权限(建议 700)。

Q5:Hadoop 2.x 和 3.x 有哪些差异要注意? Web 端口(50070 → 9870 等,见 1.2 表)、启动脚本(hadoop-daemon.sh → hdfs --daemon)、DataNode 列表文件(slaves → workers)。


后续扩展:升级为自动故障转移

给这套集群加自动故障转移,只需两步走:ZooKeeper 部署运维与监控实战 第 1 节把 ZooKeeper 部署到同一批机器(本文 1.1 的布局已预留 ZK①/②/③ 槽位);再按该文第 5 节执行升级(补两个参数 → hdfs zkfc -formatZK → 起 ZKFC),即可把 5.3 节「NN 挂了等人来」的窗口关掉。

扩展阅读