Hadoop 3.x 高可用集群 --- 知识点详解
一、HDFS 高可用集群
1.1 HDFS HA 架构概述
核心知识点:
在 Hadoop 1.x 中,NameNode 存在单点故障(SPOF)。HDFS HA 通过配置 Active/Standby 两个 NameNode 来解决此问题。
关键组件:
| 组件 | 作用 |
|---|---|
| Active NameNode | 处理所有客户端请求,维护文件系统元数据 |
| Standby NameNode | 作为热备,同步 Active 的编辑日志,随时接管 |
| JournalNode (JN) | 共享存储系统,存储 EditLog,保证两个 NN 之间数据同步 |
| ZKFailoverController (ZKFC) | 监控 NameNode 健康状态,通过 ZooKeeper 实现自动故障转移 |
| ZooKeeper | 提供分布式协调服务,维护 Active/Standby 的锁(ephemeral node) |
| DataNode | 向两个 NameNode 同时发送 Block 报告和心跳 |
架构图文字描述:
┌──────────────┐
│ ZooKeeper │
│ Cluster │
└──┬───────┬───┘
│ │
ZKFC │ │ ZKFC
┌──────┴──┐ ┌──┴──────┐
│ Active │ │ Standby │
│ NameNode│ │ NameNode│
└────┬────┘ └────┬────┘
│ │
┌────┴───────────┴────┐
│ JournalNode 集群 │
│ (至少3个,奇数个) │
└─────────────────────┘
│ │
┌──────────┴───────────┴──────────┐
│ DataNode 1, 2, 3 ... N │
└─────────────────────────────────┘
1.2 JournalNode 工作机制
知识点:
- JournalNode 是一个轻量级的守护进程,通常部署奇数个(至少3个)
- Active NameNode 将 EditLog 写入 JournalNode 集群
- Standby NameNode 从 JournalNode 集群读取 EditLog 并应用到内存
- JournalNode 使用 Paxos 协议 保证数据一致性,需要超过半数(N/2+1)节点写入成功
配置 JournalNode 的 hdfs-site.xml 核心参数:
xml
<!-- hdfs-site.xml -->
<!-- 指定 JournalNode 集群的 URI 地址,至少配置3个 -->
<!-- qjournal 是协议名,后面跟 JN 的主机名和端口 -->
<!-- /mycluster 是 nameservice 的逻辑名称 -->
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>qjournal://node1:8485;node2:8485;node3:8485/mycluster</value>
</property>
<!-- JournalNode 本地存储 EditLog 数据的目录 -->
<property>
<name>dfs.journalnode.edits.dir</name>
<value>/opt/module/hadoop-3.1.3/data/journalnode</value>
</property>
<!-- JournalNode 的 RPC 服务地址 -->
<property>
<name>dfs.journalnode.rpc-address</name>
<value>0.0.0.0:8485</value>
</property>
<!-- JournalNode 的 HTTP 服务地址 -->
<property>
<name>dfs.journalnode.http-address</name>
<value>0.0.0.0:8480</value>
</property>
1.3 HDFS Federation(联邦)与 HA 的区别
知识点:
| 对比项 | HDFS Federation | HDFS HA |
|---|---|---|
| 目的 | 解决单个 NameNode 内存瓶颈 | 解决 NameNode 单点故障 |
| NameNode 数量 | 多个 NN 管理不同的命名空间 | 两个 NN(Active + Standby)管理同一个命名空间 |
| 元数据隔离 | 不同 NN 管理不同的 BlockPool | 共享同一份元数据 |
| 是否互补 | 可以与 HA 结合使用 | 可以与 Federation 结合使用 |
Federation 配置示例:
xml
<!-- hdfs-site.xml:联邦模式下配置多个 nameservice -->
<!-- 配置 nameservices 列表,包含两个命名空间 -->
<property>
<name>dfs.nameservices</name>
<value>ns1,ns2</value>
</property>
<!-- ns1 的 NameNode 地址 -->
<property>
<name>dfs.namenode.rpc-address.ns1</name>
<value>node1:8020</value>
</property>
<!-- ns2 的 NameNode 地址 -->
<property>
<name>dfs.namenode.rpc-address.ns2</name>
<value>node2:8020</value>
</property>
1.4 HDFS HA 完整配置详解
1.4.1 core-site.xml 配置
xml
<!-- core-site.xml -->
<!--
指定 HDFS 的默认文件系统名称
"mycluster" 是 nameservice 的逻辑名,不是具体的主机名
客户端通过此名称访问 HDFS,由 nameservice 内部解析到具体的 Active NN
-->
<property>
<name>fs.defaultFS</name>
<value>hdfs://mycluster</value>
</property>
<!--
指定 ZooKeeper 集群的地址
客户端通过 ZooKeeper 发现当前 Active NameNode 的地址
2181 是 ZooKeeper 默认的客户端连接端口
-->
<property>
<name>ha.zookeeper.quorum</name>
<value>node1:2181,node2:2181,node3:2181</value>
</property>
<!-- Hadoop 临时数据存储目录 -->
<property>
<name>hadoop.tmp.dir</name>
<value>/opt/module/hadoop-3.1.3/data</value>
</property>
1.4.2 hdfs-site.xml 完整 HA 配置
xml
<!-- hdfs-site.xml -->
<!-- ==================== 1. NameService 基本配置 ==================== -->
<!--
指定 HDFS 的 nameservices 列表
可以配置多个 nameservice(联邦+HA模式)
这里只配置一个 nameservice 名为 "mycluster"
-->
<property>
<name>dfs.nameservices</name>
<value>mycluster</value>
</property>
<!--
指定 mycluster 下两个 NameNode 的唯一标识符(nn1、nn2)
这是逻辑名称,用于区分 HA 中的两个 NN
-->
<property>
<name>dfs.ha.namenodes.mycluster</name>
<value>nn1,nn2</value>
</property>
<!-- ==================== 2. NameNode RPC 地址配置 ==================== -->
<!-- nn1 的 RPC 地址:客户端通过此地址进行文件操作 -->
<property>
<name>dfs.namenode.rpc-address.mycluster.nn1</name>
<value>node1:8020</value>
</property>
<!-- nn2 的 RPC 地址 -->
<property>
<name>dfs.namenode.rpc-address.mycluster.nn2</name>
<value>node2:8020</value>
</property>
<!-- ==================== 3. NameNode HTTP 地址配置 ==================== -->
<!-- nn1 的 Web UI 地址,用于在浏览器中查看 HDFS 状态 -->
<property>
<name>dfs.namenode.http-address.mycluster.nn1</name>
<value>node1:9870</value>
</property>
<!-- nn2 的 Web UI 地址 -->
<property>
<name>dfs.namenode.http-address.mycluster.nn2</name>
<value>node2:9870</value>
</property>
<!-- ==================== 4. JournalNode 配置 ==================== -->
<!--
指定 NameNode 读写 EditLog 的 JournalNode 地址
qjournal 是专用协议
/mycluster 与 dfs.nameservices 中配置的名称一致
-->
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>qjournal://node1:8485;node2:8485;node3:8485/mycluster</value>
</property>
<!-- JournalNode 本地存储 EditLog 的目录 -->
<property>
<name>dfs.journalnode.edits.dir</name>
<value>/opt/module/hadoop-3.1.3/data/journalnode</value>
</property>
<!-- ==================== 5. 故障转移代理配置 ==================== -->
<!--
指定 HDFS 客户端联系 Active NameNode 的代理类
ConfiguredFailoverProxyProvider 会自动尝试连接 NN1,失败则尝试 NN2
-->
<property>
<name>dfs.client.failover.proxy.provider.mycluster</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>
<!-- ==================== 6. 隔离机制(Fencing)配置 ==================== -->
<!--
配置隔离方法,防止脑裂(split-brain)问题
当 Active NN 失效时,确保旧的 Active 不再接受请求
sshfence: 通过 SSH 登录到旧 Active NN 并 kill 进程
shell(/bin/true): 兜底配置,确保隔离命令成功返回
-->
<property>
<name>dfs.ha.fencing.methods</name>
<value>
sshfence
shell(/bin/true)
</value>
</property>
<!-- SSH 私钥文件路径,用于 sshfence 隔离方式的免密登录 -->
<property>
<name>dfs.ha.fencing.ssh.private-key-files</name>
<value>/home/hadoop/.ssh/id_rsa</value>
</property>
<!-- SSH 连接超时时间(毫秒),超时后判定隔离失败 -->
<property>
<name>dfs.ha.fencing.ssh.connect-timeout</name>
<value>30000</value>
</property>
<!-- ==================== 7. 自动故障转移配置 ==================== -->
<!--
开启自动故障转移功能
设为 true 后,ZKFC 会自动监控 NN 并通过 ZooKeeper 实现主备切换
设为 false 则需要手动执行 hdfs haadmin 命令切换
-->
<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
<!-- ==================== 8. 自动故障转移超时配置 ==================== -->
<!-- NN 在 ZK 注册的 session 超时时间(毫秒),超时后 ZKFC 判定 NN 不可用 -->
<property>
<name>ha.zookeeper.session-timeout.ms</name>
<value>10000</value>
</property>
1.5 自动故障转移流程
知识点:
正常状态:
nn1 (Active) 持有 ZooKeeper 的 ephemeral node(临时节点)锁
nn2 (Standby) 尝试获取锁但失败,保持 Standby
故障发生:
1. nn1 进程崩溃或网络断开
2. nn1 的 ZKFC 与 ZooKeeper 的 session 超时
3. ZooKeeper 删除 nn1 的 ephemeral node
4. nn2 的 ZKFC 发现锁被释放,立即创建自己的 ephemeral node
5. nn2 的 ZKFC 获取到锁,调用 nn2 变为 Active
6. nn2 从 JournalNode 集群读取所有 EditLog 并应用(元数据与 nn1 同步)
7. nn2 开始对外提供服务
注意:旧 nn1 恢复后自动变为 Standby
手动故障转移命令:
bash
# 查看当前 HA 状态
hdfs haadmin -getServiceState nn1 # 返回 active 或 standby
hdfs haadmin -getServiceState nn2
# 手动将 nn1 切换为 Active(需要 nn1 处于 Standby)
hdfs haadmin -transitionToActive nn1
# 手动将 nn2 切换为 Standby
hdfs haadmin -transitionToStandby nn2
# 手动进行故障转移(nn1 -> nn2)
hdfs haadmin -failover nn1 nn2
# 强制故障转移(不考虑目标 NN 状态)
hdfs haadmin -failover nn1 nn2 --forcefence
hdfs haadmin -failover nn1 nn2 --forceactive
1.6 HDFS HA 启动顺序(手动方式)
bash
# ============ 第一步:启动 ZooKeeper 集群(每台 ZK 机器执行) ============
# 启动 ZooKeeper 服务进程
# ZooKeeper 必须最先启动,因为后续的 ZKFC 和 HDFS HA 都依赖它
zkServer.sh start
# 查看 ZooKeeper 启动状态,确认 leader/follower 角色
zkServer.sh status
# ============ 第二步:启动 JournalNode 集群(node1, node2, node3) ============
# 启动 JournalNode 守护进程
# JournalNode 负责存储 HDFS 的 EditLog,是 HA 的共享存储层
hdfs --daemon start journalnode
# ============ 第三步:格式化 NameNode(仅首次部署执行) ============
# 在 node1 上格式化 NameNode
# 此命令会在本地生成 fsimage 和 edits 文件
hdfs namenode -format
# ============ 第四步:启动 node1 的 NameNode ============
# 启动第一个 NameNode(将成为 Active)
hdfs --daemon start namenode
# ============ 第五步:同步元数据到 node2 ============
# 在 node2 上执行,从 node1 拷贝 NameNode 元数据(fsimage)
# 这样 node2 的 Standby NN 就拥有与 node1 相同的初始元数据
hdfs namenode -bootstrapStandby
# ============ 第六步:启动 node2 的 NameNode ============
# 启动第二个 NameNode(将成为 Standby)
hdfs --daemon start namenode
# ============ 第七步:格式化 ZKFC(仅首次部署执行) ============
# 在任意一个 NameNode 节点上执行
# 此命令在 ZooKeeper 中创建 /hadoop-ha/mycluster znode
# 用于后续的自动故障转移
hdfs zkfc -formatZK
# ============ 第八步:启动所有 DataNode ============
# 在 node1 上启动所有 DataNode(需要配置 workers 文件)
hdfs --daemon start datanode
# 或者在每个 DataNode 节点上分别执行
# hdfs --daemon start datanode
# ============ 第九步:启动 ZKFC ============
# 在 node1 和 node2 上分别启动 ZKFC
# ZKFC 负责监控 NN 健康状态并与 ZooKeeper 交互
hdfs --daemon start zkfc
# ============ 验证 HA 状态 ============
# 查看 NameNode 角色
hdfs haadmin -getServiceState nn1
hdfs haadmin -getServiceState nn2
# 通过 Web UI 访问
# Active NN: http://node1:9870
# Standby NN: http://node2:9870
一键启动脚本(基于 start-dfs.sh):
bash
#!/bin/bash
# start-ha-cluster.sh
# 启动 HDFS HA 集群的一键脚本
echo "============ 1. 启动 ZooKeeper 集群 ============"
# 遍历 ZooKeeper 所在的三台机器,通过 SSH 远程启动 ZK
for host in node1 node2 node3; do
echo "在 $host 上启动 ZooKeeper..."
ssh $host "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
# 等待 ZooKeeper 完全启动(建议等待 5 秒)
sleep 5
echo "============ 2. 启动 JournalNode ============"
# 在每台机器上启动 JournalNode 进程
for host in node1 node2 node3; do
echo "在 $host 上启动 JournalNode..."
ssh $host "/opt/module/hadoop-3.1.3/bin/hdfs --daemon start journalnode"
done
# 等待 JournalNode 就绪
sleep 3
echo "============ 3. 启动 HDFS(含 NameNode、DataNode、ZKFC) ============"
# 使用 Hadoop 自带的 start-dfs.sh 脚本
# 该脚本会读取 etc/hadoop/workers 文件,在对应节点上启动 DataNode
# 同时在有 NameNode 配置的节点上启动 NN 和 ZKFC
/opt/module/hadoop-3.1.3/sbin/start-dfs.sh
echo "============ 4. 验证集群状态 ============"
# 显示 HDFS 集群的基本信息:总容量、已用容量、活跃节点数等
hdfs dfsadmin -report
# 查看 HA 状态
echo "nn1 状态: $(hdfs haadmin -getServiceState nn1)"
echo "nn2 状态: $(hdfs haadmin -getServiceState nn2)"
echo "HDFS HA 集群启动完成!"
1.7 HDFS HA Java 客户端代码
java
import org.apache.hadoop.conf.Configuration; // Hadoop 配置类
import org.apache.hadoop.fs.*; // HDFS 文件系统 API
import org.apache.hadoop.io.IOUtils; // IO 工具类
import java.io.*;
import java.net.URI;
/**
* HDFS HA 集群客户端操作示例
* 演示在 HA 模式下如何读写 HDFS 文件
*/
public class HdfsHAClient {
// HDFS 文件系统对象
private FileSystem fileSystem;
/**
* 初始化方法:连接 HDFS HA 集群
* 在 HA 模式下不需要指定具体的 NameNode 地址
* 只需提供 nameservice 名称和 ZooKeeper 地址即可
*/
public void init() throws Exception {
// 创建 Hadoop 配置对象
Configuration conf = new Configuration();
// 设置默认文件系统为 nameservice 名称(而非具体的 NN 地址)
// "mycluster" 对应 hdfs-site.xml 中 dfs.nameservices 的值
conf.set("fs.defaultFS", "hdfs://mycluster");
// 设置 ZooKeeper 集群地址
// 客户端通过 ZK 发现当前 Active NN 的实际地址
conf.set("ha.zookeeper.quorum", "node1:2181,node2:2181,node3:2181");
// 创建文件系统实例
// URI 使用 nameservice 名称,客户端内部会自动探测 Active NN
fileSystem = FileSystem.get(new URI("hdfs://mycluster"), conf, "hadoop");
}
/**
* 上传本地文件到 HDFS
* @param localPath 本地文件路径
* @param hdfsPath HDFS 目标路径
*/
public void uploadFile(String localPath, String hdfsPath) throws Exception {
// Path 类用于表示 Hadoop 文件路径
Path srcPath = new Path(localPath); // 源路径(本地)
Path dstPath = new Path(hdfsPath); // 目标路径(HDFS)
// copyFromLocalFile 方法将本地文件复制到 HDFS
// 参数1: 是否删除源文件(false 表示保留本地文件)
// 参数2: 是否覆盖目标文件(true 表示覆盖已存在的文件)
// 参数3: 源路径
// 参数4: 目标路径
fileSystem.copyFromLocalFile(false, true, srcPath, dstPath);
System.out.println("文件上传成功: " + localPath + " -> " + hdfsPath);
}
/**
* 从 HDFS 下载文件到本地
* @param hdfsPath HDFS 源文件路径
* @param localPath 本地目标路径
*/
public void downloadFile(String hdfsPath, String localPath) throws Exception {
Path srcPath = new Path(hdfsPath); // HDFS 源路径
Path dstPath = new Path(localPath); // 本地目标路径
// copyToLocalFile 方法将 HDFS 文件复制到本地
// 参数1: 是否删除 HDFS 上的源文件(false 不删除)
// 参数2: 源路径
// 参数3: 目标路径
// 参数4: 是否使用本地文件系统(true 表示使用本地 fs)
fileSystem.copyToLocalFile(false, srcPath, dstPath);
System.out.println("文件下载成功: " + hdfsPath + " -> " + localPath);
}
/**
* 通过流的方式写入数据到 HDFS
* 这种方式更灵活,适合程序生成数据的场景
* @param hdfsPath HDFS 文件路径
* @param content 要写入的内容
*/
public void writeFileByStream(String hdfsPath, String content) throws Exception {
Path path = new Path(hdfsPath);
// 创建 HDFS 输出流
// create 方法返回 FSDataOutputStream,可以向 HDFS 写入数据
// 如果文件已存在,默认会覆盖
FSDataOutputStream outputStream = fileSystem.create(path);
// 将字符串转为字节数组并写入
outputStream.writeBytes(content);
// 刷新缓冲区,确保数据写入
outputStream.hflush();
// 关闭输出流,释放资源
outputStream.close();
System.out.println("流式写入成功: " + hdfsPath);
}
/**
* 通过流的方式读取 HDFS 文件内容
* @param hdfsPath HDFS 文件路径
* @return 文件内容字符串
*/
public String readFileByStream(String hdfsPath) throws Exception {
Path path = new Path(hdfsPath);
// 检查文件是否存在
if (!fileSystem.exists(path)) {
System.out.println("文件不存在: " + hdfsPath);
return null;
}
// 创建 HDFS 输入流
// open 方法返回 FSDataInputStream,用于读取 HDFS 文件内容
FSDataInputStream inputStream = fileSystem.open(path);
// 使用 StringBuilder 拼接读取的内容
StringBuilder content = new StringBuilder();
// 逐行读取文件内容
// readLine() 方法读取一行文本,到达文件末尾返回 null
String line;
while ((line = inputStream.readLine()) != null) {
content.append(line).append("\n"); // 追加每行内容和换行符
}
// 关闭输入流,释放资源
inputStream.close();
return content.toString();
}
/**
* 列出指定目录下的所有文件和子目录
* @param dirPath HDFS 目录路径
*/
public void listDirectory(String dirPath) throws Exception {
Path path = new Path(dirPath);
// 检查路径是否存在
if (!fileSystem.exists(path)) {
System.out.println("目录不存在: " + dirPath);
return;
}
// listStatus 方法返回目录下所有文件/目录的状态数组
// FileStatus 包含文件名、大小、权限、修改时间等信息
FileStatus[] fileStatuses = fileSystem.listStatus(path);
// 遍历并打印每个文件/目录的信息
System.out.println("======== 目录内容: " + dirPath + " ========");
for (FileStatus status : fileStatuses) {
// 判断是文件还是目录
String type = status.isDirectory() ? "[目录]" : "[文件]";
// 获取路径的文件名部分
String name = status.getPath().getName();
// 获取文件大小(目录大小为 0)
long size = status.getLen();
// 获取权限
String permission = status.getPermission().toString();
System.out.printf("%-8s %-30s 大小: %-10d 权限: %s%n",
type, name, size, permission);
}
}
/**
* 递归列出目录下所有文件(类似 Linux 的 find 命令)
* @param dirPath 起始目录路径
*/
public void listFilesRecursive(String dirPath) throws Exception {
Path path = new Path(dirPath);
// listFiles 方法递归列出所有文件
// 参数1: 路径
// 参数2: true 表示递归进入子目录
RemoteIterator<LocatedFileStatus> fileIterator = fileSystem.listFiles(path, true);
System.out.println("======== 递归文件列表 ========");
while (fileIterator.hasNext()) {
LocatedFileStatus fileStatus = fileIterator.next();
// 获取文件路径
String filePath = fileStatus.getPath().toString();
// 获取文件大小
long size = fileStatus.getLen();
// 获取文件块的位置信息(包含副本所在的 DataNode 地址)
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
System.out.printf("文件: %-60s 大小: %-10d 块数: %d%n",
filePath, size, blockLocations.length);
}
}
/**
* 删除 HDFS 文件或目录
* @param path 文件/目录路径
* @param recursive 是否递归删除(删除目录时需要设为 true)
*/
public void deleteFile(String path, boolean recursive) throws Exception {
Path filePath = new Path(path);
// delete 方法删除指定路径的文件或目录
// 参数1: 路径
// 参数2: true 表示递归删除(目录下有文件时必须为 true)
boolean result = fileSystem.delete(filePath, recursive);
if (result) {
System.out.println("删除成功: " + path);
} else {
System.out.println("删除失败: " + path);
}
}
/**
* 创建目录
* @param dirPath 目录路径
*/
public void mkdir(String dirPath) throws Exception {
Path path = new Path(dirPath);
// mkdirs 方法创建目录(包含所有不存在的父目录)
// 类似于 Linux 的 mkdir -p 命令
boolean result = fileSystem.mkdirs(path);
if (result) {
System.out.println("目录创建成功: " + dirPath);
} else {
System.out.println("目录创建失败: " + dirPath);
}
}
/**
* 关闭文件系统连接,释放资源
*/
public void close() throws Exception {
if (fileSystem != null) {
fileSystem.close(); // 关闭 HDFS 连接
System.out.println("HDFS 连接已关闭");
}
}
/**
* 主方法:测试 HDFS HA 客户端
*/
public static void main(String[] args) {
HdfsHAClient client = new HdfsHAClient();
try {
// 1. 初始化连接(连接 HA 集群)
client.init();
// 2. 创建目录
client.mkdir("/ha-test");
// 3. 流式写入文件
client.writeFileByStream("/ha-test/hello.txt",
"Hello HDFS HA Cluster!\nThis is a test file.\n");
// 4. 上传本地文件到 HDFS
client.uploadFile("/tmp/local-file.txt", "/ha-test/uploaded.txt");
// 5. 列出目录内容
client.listDirectory("/ha-test");
// 6. 读取文件内容
String content = client.readFileByStream("/ha-test/hello.txt");
System.out.println("======== 文件内容 ========");
System.out.println(content);
// 7. 递归列出所有文件
client.listFilesRecursive("/");
// 8. 下载文件到本地
client.downloadFile("/ha-test/hello.txt", "/tmp/downloaded.txt");
// 9. 删除文件
client.deleteFile("/ha-test/hello.txt", false);
// 10. 删除目录(递归删除)
client.deleteFile("/ha-test", true);
} catch (Exception e) {
// 捕获并打印异常信息
e.printStackTrace();
} finally {
// 无论是否发生异常,都关闭连接
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1.8 HDFS HA 状态检查与故障排查脚本
bash
#!/bin/bash
# hdfs-ha-check.sh
# HDFS HA 集群健康检查脚本
echo "============ HDFS HA 集群健康检查 ============"
echo "检查时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# ---- 1. 检查 ZooKeeper 状态 ----
echo "【1】ZooKeeper 集群状态:"
for host in node1 node2 node3; do
# 远程执行 zkServer.sh status,获取 ZK 角色信息
status=$(ssh $host "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status 2>&1")
echo " $host: $status"
done
echo ""
# ---- 2. 检查 JournalNode 状态 ----
echo "【2】JournalNode 进程状态:"
for host in node1 node2 node3; do
# 检查 JournalNode 进程是否存在
# jps -l 列出 Java 进程,grep 过滤 JournalNode
jn_pid=$(ssh $host "jps -l | grep JournalNode")
if [ -n "$jn_pid" ]; then
echo " $host: 运行中 - $jn_pid"
else
echo " $host: 未运行!"
fi
done
echo ""
# ---- 3. 检查 NameNode 状态 ----
echo "【3】NameNode HA 状态:"
# 获取 nn1 的角色(active/standby)
nn1_state=$(hdfs haadmin -getServiceState nn1 2>/dev/null)
# 获取 nn2 的角色
nn2_state=$(hdfs haadmin -getServiceState nn2 2>/dev/null)
echo " nn1 (node1): ${nn1_state:-'无法连接'}"
echo " nn2 (node2): ${nn2_state:-'无法连接'}"
# 判断是否有一个 Active 和一个 Standby
if [ "$nn1_state" = "active" ] && [ "$nn2_state" = "standby" ]; then
echo " HA 状态: 正常 (nn1=Active, nn2=Standby)"
elif [ "$nn1_state" = "standby" ] && [ "$nn2_state" = "active" ]; then
echo " HA 状态: 正常 (nn1=Standby, nn2=Active)"
else
echo " HA 状态: 异常!请检查 NameNode"
fi
echo ""
# ---- 4. 检查 DataNode 状态 ----
echo "【4】DataNode 状态:"
# hdfs dfsadmin -report 输出集群报告,grep 过滤活跃节点数
live_nodes=$(hdfs dfsadmin -report 2>/dev/null | grep "Live datanodes" | grep -oP '\d+')
dead_nodes=$(hdfs dfsadmin -report 2>/dev/null | grep "Dead datanodes" | grep -oP '\d+')
echo " 活跃 DataNode: ${live_nodes:-0}"
echo " 死亡 DataNode: ${dead_nodes:-0}"
echo ""
# ---- 5. 检查 HDFS 可用性 ----
echo "【5】HDFS 读写测试:"
# 尝试创建测试目录
test_dir="/ha-health-check-$(date +%s)"
if hdfs dfs -mkdir -p $test_dir 2>/dev/null; then
echo " 写入测试: 成功"
# 尝试列出目录
if hdfs dfs -ls $test_dir 2>/dev/null; then
echo " 读取测试: 成功"
else
echo " 读取测试: 失败"
fi
# 清理测试目录
hdfs dfs -rm -r $test_dir 2>/dev/null
else
echo " 写入测试: 失败!HDFS 可能不可用"
fi
echo ""
echo "============ 检查完成 ============"
二、YARN 高可用集群
2.1 YARN HA 架构概述
核心知识点:
YARN HA 与 HDFS HA 类似,通过配置 Active/Standby 两个 ResourceManager 来解决单点故障问题。
关键组件:
| 组件 | 作用 |
|---|---|
| Active ResourceManager | 处理客户端请求,管理资源分配,调度 Application |
| Standby ResourceManager | 热备,同步 Active RM 的状态,随时接管 |
| ZKFailoverController (RM ZKFC) | 内嵌在 RM 中,负责与 ZooKeeper 交互实现自动故障转移 |
| ZooKeeper | 存储 RM 的选举信息,维护 Active/Standby 的锁 |
| ResourceManagerStateStore | 存储 RM 的应用状态(内存或 ZooKeeper) |
| NodeManager | 向所有 RM 注册,但只接收 Active RM 的指令 |
YARN HA 与 HDFS HA 的对比:
| 对比项 | HDFS HA | YARN HA |
|---|---|---|
| 共享存储 | JournalNode | ZooKeeper(RMStateStore) |
| 状态同步方式 | EditLog 实时同步 | 应用状态存储到 ZK |
| ZKFC | 独立进程 | 内嵌在 ResourceManager 中 |
| 故障转移影响 | 客户端透明切换 | 正在运行的 Application 需要重新提交 |
2.2 YARN HA 完整配置
2.2.1 yarn-site.xml 配置
xml
<!-- yarn-site.xml -->
<!-- ==================== 1. ResourceManager HA 基本配置 ==================== -->
<!--
开启 ResourceManager 高可用
设为 true 后,YARN 会启用 RM 的 Active/Standby 机制
-->
<property>
<name>yarn.resourcemanager.ha.enabled</name>
<value>true</value>
</property>
<!--
指定 ResourceManager 集群的逻辑 ID
与 HDFS 的 nameservice 类似,这是一个逻辑名称
-->
<property>
<name>yarn.resourcemanager.cluster-id</name>
<value>yarn-cluster</value>
</property>
<!--
指定两个 ResourceManager 的标识符
rm1 和 rm2 是逻辑名称,用于区分 HA 中的两个 RM
-->
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
<!-- ==================== 2. ResourceManager 地址配置 ==================== -->
<!-- rm1 的主机名 -->
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>node1</value>
</property>
<!-- rm2 的主机名 -->
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>node2</value>
</property>
<!-- rm1 的 Web UI 地址,用于在浏览器中查看 YARN 应用状态 -->
<property>
<name>yarn.resourcemanager.webapp.address.rm1</name>
<value>node1:8088</value>
</property>
<!-- rm2 的 Web UI 地址 -->
<property>
<name>yarn.resourcemanager.webapp.address.rm2</name>
<value>node2:8088</value>
</property>
<!-- rm1 的 RPC 地址,客户端提交应用和内部通信使用 -->
<property>
<name>yarn.resourcemanager.address.rm1</name>
<value>node1:8032</value>
</property>
<!-- rm2 的 RPC 地址 -->
<property>
<name>yarn.resourcemanager.address.rm2</name>
<value>node2:8032</value>
</property>
<!-- rm1 的 Scheduler 地址,ApplicationMaster 通过此地址申请资源 -->
<property>
<name>yarn.resourcemanager.scheduler.address.rm1</name>
<value>node1:8030</value>
</property>
<!-- rm2 的 Scheduler 地址 -->
<property>
<name>yarn.resourcemanager.scheduler.address.rm2</name>
<value>node2:8030</value>
</property>
<!-- rm1 的 Resource Tracker 地址,NodeManager 通过此地址注册和汇报 -->
<property>
<name>yarn.resourcemanager.resource-tracker.address.rm1</name>
<value>node1:8031</value>
</property>
<!-- rm2 的 Resource Tracker 地址 -->
<property>
<name>yarn.resourcemanager.resource-tracker.address.rm2</name>
<value>node2:8031</value>
</property>
<!-- rm1 的 Admin 地址,管理员通过此地址执行 RM 管理命令 -->
<property>
<name>yarn.resourcemanager.admin.address.rm1</name>
<value>node1:8033</value>
</property>
<!-- rm2 的 Admin 地址 -->
<property>
<name>yarn.resourcemanager.admin.address.rm2</name>
<value>node2:8033</value>
</property>
<!-- ==================== 3. ZooKeeper 配置 ==================== -->
<!--
配置 ZooKeeper 集群地址
YARN HA 使用 ZooKeeper 进行 Leader 选举和状态存储
-->
<property>
<name>ha.zookeeper.quorum</name>
<value>node1:2181,node2:2181,node3:2181</value>
</property>
<!-- ==================== 4. 自动故障转移配置 ==================== -->
<!--
开启 ResourceManager 自动故障转移
通过 ZKFC(内嵌在 RM 中)与 ZooKeeper 交互实现自动切换
-->
<property>
<name>yarn.resourcemanager.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
<!--
指定 RM 的选举算法
ActiveStandbyElector 是 Hadoop 内置的选举算法
基于 ZooKeeper 的 ephemeral sequential node 实现
-->
<property>
<name>yarn.resourcemanager.ha.automatic-failover.embedded</name>
<value>true</value>
</property>
<!-- ==================== 5. 状态存储配置 ==================== -->
<!--
配置 ResourceManager 的状态存储类
RMStateStore 用于持久化 RM 中的应用状态信息
可选值:
- org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore
将状态存储在 ZooKeeper 中(推荐,适合 HA)
- org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore
将状态存储在 HDFS 中
- org.apache.hadoop.yarn.server.resourcemanager.recovery.LeveldbRMStateStore
将状态存储在 LevelDB 中
-->
<property>
<name>yarn.resourcemanager.store.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
</property>
<!-- ==================== 6. NodeManager 配置 ==================== -->
<!--
配置 NodeManager 辅助服务
mapreduce_shuffle: 允许 MapReduce 框架使用 shuffle 功能
这是 MapReduce 作业的必需配置
-->
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<!--
NodeManager 可用的内存资源(字节)
这里设置为 4GB,应根据实际机器内存配置
-->
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>4096</value>
</property>
<!--
NodeManager 可用的 CPU 虚拟核数
-->
<property>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>4</value>
</property>
<!-- ==================== 7. 资源调度器配置 ==================== -->
<!--
指定 YARN 使用的资源调度器类型
可选值: FifoScheduler, CapacityScheduler, FairScheduler
CapacityScheduler 是 Hadoop 3.x 的默认调度器
-->
<property>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
</property>
<!-- ==================== 8. 日志聚合配置 ==================== -->
<!--
开启日志聚合功能
任务运行结束后,将各容器的日志聚合到 HDFS 上
方便用户在 Web UI 上查看任务日志
-->
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<!-- 日志在 HDFS 上保留的时间(秒),这里设置为 7 天 -->
<property>
<name>yarn.log-aggregation.retain-seconds</name>
<value>604800</value>
</property>
2.3 YARN HA 故障转移流程
正常状态:
rm1 (Active) 在 ZooKeeper 上创建 ephemeral node
rm2 (Standby) 监听 ZK 上的节点变化
所有 NodeManager 向两个 RM 都注册,但只接受 Active RM 的指令
故障发生:
1. rm1 进程崩溃
2. rm1 在 ZooKeeper 上的 ephemeral node 自动删除
3. rm2 的 StandbyElector 检测到节点删除事件
4. rm2 创建自己的 ephemeral node,成为新的 Active
5. rm2 从 ZooKeeper 的 ZKRMStateStore 中恢复应用状态
6. rm2 向所有 NodeManager 重新注册
7. 正在运行的 Application 会经历短暂中断后恢复
8. 已提交但未运行的 Application 由新 Active RM 重新调度
注意:
- Container 级别的任务如果正在运行,通常可以继续执行
- ApplicationMaster 需要重新与新 Active RM 建立连接
- 配置了 retry 的客户端会自动重试连接到新 RM
2.4 YARN HA 启动与管理命令
bash
# ============ 启动 YARN HA 集群 ============
# 方法一:使用 start-yarn.sh 脚本(推荐)
# 此脚本会自动在 node1 和 node2 上启动 ResourceManager
# 在 workers 文件中列出的所有节点上启动 NodeManager
/opt/module/hadoop-3.1.3/sbin/start-yarn.sh
# 方法二:在各个节点上分别启动
# 在 node1 上启动 ResourceManager
yarn --daemon start resourcemanager
# 在 node2 上启动 ResourceManager
yarn --daemon start resourcemanager
# 在每个 NodeManager 节点上启动 NodeManager
yarn --daemon start nodemanager
# ============ YARN HA 管理命令 ============
# 查看 rm1 的状态(active/standby)
yarn rmadmin -getServiceState rm1
# 查看 rm2 的状态(active/standby)
yarn rmadmin -getServiceState rm2
# 手动故障转移:从 rm1 切换到 rm2
yarn rmadmin -failover rm1 rm2
# 将 rm1 手动切换为 Active
yarn rmadmin -transitionToActive rm1
# 将 rm2 手动切换为 Standby
yarn rmadmin -transitionToStandby rm2
# 列出所有队列及其状态
yarn rmadmin -getAllServiceState
# ============ YARN 作业提交命令 ============
# 提交 MapReduce 作业到 HA 集群
# 在 HA 模式下,ResourceManager 地址自动从配置中读取
# 客户端通过 ZooKeeper 发现 Active RM
hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar \
wordcount \
/input \
/output
# 查看 YARN 作业列表
yarn application -list
# 查看指定应用的状态
yarn application -status application_1234567890123_0001
# 终止指定应用
yarn application -kill application_1234567890123_0001
2.5 YARN HA Java 客户端代码
java
import org.apache.hadoop.conf.Configuration; // Hadoop 配置类
import org.apache.hadoop.yarn.api.records.*; // YARN 记录类
import org.apache.hadoop.yarn.client.api.YarnClient; // YARN 客户端 API
import org.apache.hadoop.yarn.exceptions.YarnException; // YARN 异常类
import java.io.IOException;
import java.util.List;
import java.util.EnumSet;
/**
* YARN HA 集群客户端操作示例
* 演示在 HA 模式下如何与 YARN 集群交互
*/
public class YarnHAClient {
// YARN 客户端对象
private YarnClient yarnClient;
/**
* 初始化 YARN 客户端(HA 模式)
* 在 HA 模式下,客户端无需指定具体的 ResourceManager 地址
* 只需提供集群 ID 和 ZooKeeper 地址,客户端会自动发现 Active RM
*/
public void init() {
// 创建 Hadoop 配置对象
Configuration conf = new Configuration();
// ---- HA 相关配置 ----
// 开启 ResourceManager HA
conf.setBoolean("yarn.resourcemanager.ha.enabled", true);
// 设置 YARN 集群 ID(对应 yarn-site.xml 中的 yarn.resourcemanager.cluster-id)
conf.set("yarn.resourcemanager.cluster-id", "yarn-cluster");
// 设置两个 ResourceManager 的标识符
conf.set("yarn.resourcemanager.ha.rm-ids", "rm1,rm2");
// 设置 rm1 的主机地址
conf.set("yarn.resourcemanager.hostname.rm1", "node1");
// 设置 rm2 的主机地址
conf.set("yarn.resourcemanager.hostname.rm2", "node2");
// 设置 ZooKeeper 集群地址(用于发现 Active RM)
conf.set("ha.zookeeper.quorum", "node1:2181,node2:2181,node3:2181");
// ---- 创建并启动 YARN 客户端 ----
// 使用 YarnClient.create() 工厂方法创建客户端实例
yarnClient = YarnClient.createYarnClient();
// 使用配置初始化客户端
yarnClient.init(conf);
// 启动客户端,建立与 YARN 集群的连接
// 在 HA 模式下,此步骤会通过 ZooKeeper 找到 Active RM
yarnClient.start();
}
/**
* 获取集群的基本信息(节点数、资源量等)
*/
public void getClusterInfo() throws IOException, YarnException {
// 获取 YARN 集群报告
// YarnClusterMetrics 包含集群的节点数等统计信息
YarnClusterMetrics clusterMetrics = yarnClient.getYarnClusterMetrics();
System.out.println("============ YARN HA 集群信息 ============");
// 获取所有节点的总数
System.out.println("总节点数: " + clusterMetrics.getNumNodeManagers());
// 获取活跃节点数
System.out.println("活跃节点数: " + clusterMetrics.getNumActiveNodeManagers());
// 获取不健康节点数
System.out.println("不健康节点数: " + clusterMetrics.getUnhealthyNodeManagers());
// 获取已停用节点数
System.out.println("已停用节点数: " + clusterMetrics.getNumDecommissionedNodeManagers());
// 获取丢失节点数
System.out.println("丢失节点数: " + clusterMetrics.getNumLostNodeManagers());
}
/**
* 列出集群中所有 NodeManager 的详细信息
*/
public void listNodeManagers() throws IOException, YarnException {
// 获取所有 NodeManager 的状态信息列表
// EnumSet.of(NodeState.RUNNING) 表示只获取状态为 RUNNING 的节点
List<NodeReport> nodeReports = yarnClient.getNodeReports(
EnumSet.of(NodeState.RUNNING));
System.out.println("============ NodeManager 详细信息 ============");
for (NodeReport node : nodeReports) {
// 获取 NodeManager 的主机名和端口
String nodeId = node.getNodeId().toString();
// 获取节点的 HTTP 地址(用于查看节点 Web UI)
String httpAddress = node.getHttpAddress();
// 获取节点可用的内存大小(MB)
long memoryTotal = node.getCapability().getMemorySize();
// 获取节点已使用的内存大小(MB)
long memoryUsed = node.getUsed().getMemorySize();
// 获取节点可用的 CPU 核数
int vcoresTotal = node.getCapability().getVirtualCores();
// 获取节点已使用的 CPU 核数
int vcoresUsed = node.getUsed().getVirtualCores();
// 获取节点上正在运行的容器数
int numContainers = node.getNumContainers();
// 获取节点健康状态
String healthReport = node.getHealthReport();
System.out.printf("节点: %-20s HTTP: %-25s%n", nodeId, httpAddress);
System.out.printf(" 内存: %dMB / %dMB (已用/总量)%n", memoryUsed, memoryTotal);
System.out.printf(" CPU: %d / %d 核 (已用/总量)%n", vcoresUsed, vcoresTotal);
System.out.printf(" 容器数: %d%n", numContainers);
System.out.printf(" 健康报告: %s%n", healthReport.isEmpty() ? "正常" : healthReport);
System.out.println();
}
}
/**
* 列出所有应用的状态
* @throws IOException IO异常
* @throws YarnException YARN异常
*/
public void listApplications() throws IOException, YarnException {
// 获取所有应用的状态报告
// getApplications() 不带参数时返回所有状态的应用
List<ApplicationReport> apps = yarnClient.getApplications();
System.out.println("============ YARN 应用列表 ============");
System.out.printf("%-40s %-15s %-12s %-20s%n",
"Application ID", "Application Name", "State", "Tracking URL");
for (ApplicationReport app : apps) {
// 获取应用 ID(如 application_1234567890123_0001)
ApplicationId appId = app.getApplicationId();
// 获取应用名称
String appName = app.getName();
// 获取应用状态(ACCEPTED, RUNNING, FINISHED, FAILED, KILLED)
YarnApplicationState state = app.getYarnApplicationState();
// 获取应用跟踪 URL(用于查看应用详情)
String trackingUrl = app.getTrackingUrl();
System.out.printf("%-40s %-15s %-12s %-20s%n",
appId.toString(), appName, state, trackingUrl);
}
}
/**
* 获取指定应用的详细信息
* @param applicationId 应用 ID(如 application_1234567890123_0001)
*/
public void getApplicationDetail(String applicationId)
throws IOException, YarnException {
// 将字符串形式的 ApplicationId 转换为 ApplicationId 对象
ApplicationId appId = ApplicationId.fromString(applicationId);
// 获取指定应用的状态报告
ApplicationReport report = yarnClient.getApplicationReport(appId);
System.out.println("============ 应用详细信息 ============");
// 应用 ID
System.out.println("应用 ID: " + report.getApplicationId());
// 应用名称
System.out.println("应用名称: " + report.getName());
// 应用类型(如 MAPREDUCE, SPARK 等)
System.out.println("应用类型: " + report.getApplicationType());
// 应用当前状态
System.out.println("应用状态: " + report.getYarnApplicationState());
// 最终状态(SUCCEEDED, FAILED, KILLED, UNDEFINED)
System.out.println("最终状态: " + report.getFinalApplicationStatus());
// 提交用户
System.out.println("提交用户: " + report.getUser());
// 队列名称
System.out.println("所在队列: " + report.getQueue());
// 提交时间
System.out.println("提交时间: " + new java.util.Date(report.getSubmitTime()));
// 启动时间
System.out.println("启动时间: " + new java.util.Date(report.getStartTime()));
// 结束时间(如果尚未结束则为 0)
long finishTime = report.getFinishTime();
if (finishTime > 0) {
System.out.println("结束时间: " + new java.util.Date(finishTime));
}
// 跟踪 URL
System.out.println("跟踪 URL: " + report.getTrackingUrl());
// 应用诊断信息(如果失败,包含失败原因)
System.out.println("诊断信息: " + report.getDiagnostics());
}
/**
* 获取集群的队列信息
*/
public void listQueues() throws IOException, YarnException {
// 获取根队列信息
// YARN 的队列是树形结构,根队列名为 "root"
QueueInfo rootQueue = yarnClient.getQueueInfo("root");
System.out.println("============ YARN 队列信息 ============");
printQueueInfo(rootQueue, 0);
}
/**
* 递归打印队列信息(辅助方法)
* @param queue 队列信息对象
* @param indent 缩进层级(用于树形展示)
*/
private void printQueueInfo(QueueInfo queue, int indent) {
// 构建缩进字符串
String indentStr = " ".repeat(indent);
// 打印队列基本信息
System.out.printf("%s队列名: %s%n", indentStr, queue.getQueueName());
// 队列状态(RUNNING, STOPPED)
System.out.printf("%s 状态: %s%n", indentStr, queue.getQueueState());
// 队列容量(百分比)
System.out.printf("%s 容量: %.1f%%%n", indentStr, queue.getCapacity() * 100);
// 队列最大容量(百分比)
System.out.printf("%s 最大容量: %.1f%%%n", indentStr, queue.getMaximumCapacity() * 100);
// 当前使用的容量(百分比)
System.out.printf("%s 当前使用: %.1f%%%n", indentStr, queue.getCurrentCapacity() * 100);
// 队列中的应用数量
System.out.printf("%s 应用数: %d%n", indentStr, queue.getApplications().size());
System.out.println();
// 递归打印子队列
List<QueueInfo> childQueues = queue.getChildQueues();
if (childQueues != null) {
for (QueueInfo child : childQueues) {
printQueueInfo(child, indent + 1);
}
}
}
/**
* 关闭 YARN 客户端,释放资源
*/
public void close() {
if (yarnClient != null) {
// 停止 YARN 客户端,断开与集群的连接
yarnClient.stop();
System.out.println("YARN 客户端已关闭");
}
}
/**
* 主方法:测试 YARN HA 客户端
*/
public static void main(String[] args) {
YarnHAClient client = new YarnHAClient();
try {
// 1. 初始化连接(HA 模式自动发现 Active RM)
client.init();
// 2. 获取集群基本信息
client.getClusterInfo();
// 3. 列出所有 NodeManager
client.listNodeManagers();
// 4. 列出所有应用
client.listApplications();
// 5. 列出队列信息
client.listQueues();
// 6. 获取指定应用详情(示例,需要替换为实际的应用 ID)
// client.getApplicationDetail("application_1234567890123_0001");
} catch (Exception e) {
// 捕获并打印异常信息
e.printStackTrace();
} finally {
// 无论是否发生异常,都关闭客户端连接
client.close();
}
}
}
三、部署 Hadoop 高可用集群
3.1 环境规划
集群节点规划:
| 主机名 | IP 地址 | 角色 |
|---|---|---|
| node1 | 192.168.10.101 | NameNode(active), DataNode, ResourceManager(active), NodeManager, JournalNode, ZooKeeper, ZKFC |
| node2 | 192.168.10.102 | NameNode(standby), DataNode, ResourceManager(standby), NodeManager, JournalNode, ZooKeeper, ZKFC |
| node3 | 192.168.10.103 | DataNode, NodeManager, JournalNode, ZooKeeper |
软件版本规划:
| 软件 | 版本 |
|---|---|
| JDK | 1.8 (jdk-8u212) |
| Hadoop | 3.1.3 |
| ZooKeeper | 3.5.7 |
端口规划:
| 服务 | 端口 | 说明 |
|---|---|---|
| NameNode RPC | 8020 | 客户端与 NN 的 RPC 通信 |
| NameNode HTTP | 9870 | NN Web UI |
| DataNode | 9866 | DN 数据传输端口 |
| DataNode HTTP | 9864 | DN Web UI |
| ResourceManager HTTP | 8088 | RM Web UI |
| ResourceManager RPC | 8032 | 客户端与 RM 的 RPC 通信 |
| NodeManager HTTP | 8042 | NM Web UI |
| JournalNode RPC | 8485 | JN RPC 通信端口 |
| JournalNode HTTP | 8480 | JN Web UI |
| ZooKeeper | 2181 | ZK 客户端连接端口 |
| ZooKeeper Leader Election | 2888, 3888 | ZK 集群内部通信 |
3.2 基础环境准备
bash
#!/bin/bash
# setup-env.sh
# Hadoop HA 集群基础环境配置脚本(需要在每台机器上执行)
# ============ 1. 关闭防火墙 ============
# 生产环境建议配置防火墙规则而非直接关闭
# systemctl stop firewalld: 停止防火墙服务
# systemctl disable firewalld: 禁止防火墙开机自启
sudo systemctl stop firewalld
sudo systemctl disable firewalld
echo "防火墙已关闭"
# ============ 2. 配置主机名 ============
# 设置当前机器的主机名(需要根据实际节点修改)
# hostnamectl set-hostname 会修改 /etc/hostname 文件
# 以 node1 为例,其他节点改为 node2、node3
sudo hostnamectl set-hostname node1
echo "主机名已设置为 node1"
# ============ 3. 配置 hosts 文件 ============
# 在 /etc/hosts 中添加集群所有节点的 IP-主机名映射
# 这样节点之间可以通过主机名互相访问
cat >> /etc/hosts << 'EOF'
192.168.10.101 node1
192.168.10.102 node2
192.168.10.103 node3
EOF
echo "hosts 文件已配置"
# ============ 4. 配置 SSH 免密登录 ============
# 生成 SSH 密钥对(如果尚未生成)
# -t rsa: 使用 RSA 算法
# -P '': 不设置密码(免密)
# -f: 指定密钥文件路径
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
# 将公钥分发到集群中的所有节点(包括自身)
# ssh-copy-id 将本机公钥追加到目标机器的 ~/.ssh/authorized_keys 文件中
for host in node1 node2 node3; do
ssh-copy-id $host
echo "已配置到 $host 的免密登录"
done
# ============ 5. 安装 JDK ============
# 解压 JDK 安装包到指定目录
# tar -zxvf: z 表示解压 gzip, x 表示解压, v 显示过程, f 指定文件
tar -zxvf /opt/software/jdk-8u212-linux-x64.tar.gz -C /opt/module/
# 配置 JDK 环境变量
# JAVA_HOME: JDK 安装根目录
# PATH: 将 JDK 的 bin 目录加入系统 PATH
# CLASSPATH: Java 类路径
cat >> ~/.bash_profile << 'EOF'
# Java Environment
export JAVA_HOME=/opt/module/jdk1.8.0_212
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
EOF
# 使环境变量立即生效
source ~/.bash_profile
# 验证 JDK 安装
java -version
echo "JDK 安装完成"
# ============ 6. 配置 NTP 时间同步 ============
# 集群各节点的时间必须同步,否则 ZooKeeper 选举可能出现问题
# 安装 chrony 时间同步服务(CentOS 7+)
sudo yum install -y chrony
# 启动并设置开机自启
sudo systemctl start chronyd
sudo systemctl enable chronyd
echo "时间同步服务已配置"
3.3 ZooKeeper 集群部署
bash
# ============ 1. 解压 ZooKeeper ============
# 解压 ZooKeeper 安装包到 /opt/module 目录
tar -zxvf /opt/software/apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
# 重命名目录(方便管理)
mv /opt/module/apache-zookeeper-3.5.7-bin /opt/module/zookeeper-3.5.7
# ============ 2. 配置 ZooKeeper 环境变量 ============
cat >> ~/.bash_profile << 'EOF'
# ZooKeeper Environment
export ZOOKEEPER_HOME=/opt/module/zookeeper-3.5.7
export PATH=$PATH:$ZOOKEEPER_HOME/bin
EOF
source ~/.bash_profile
# ============ 3. 创建配置文件 ============
# ZooKeeper 默认加载 zoo.cfg 配置文件
# 从模板文件复制一份
cp $ZOOKEEPER_HOME/conf/zoo_sample.cfg $ZOOKEEPER_HOME/conf/zoo.cfg
# ============ 4. 编辑 zoo.cfg ============
cat > $ZOOKEEPER_HOME/conf/zoo.cfg << 'EOF'
# ZooKeeper 服务器之间或客户端与服务器之间的心跳间隔(毫秒)
# 每隔 2000ms 发送一次心跳
tickTime=2000
# Follower 服务器初始连接到 Leader 时的最大心跳数
# 即初始化连接时最长能忍受 tickTime * initLimit = 2000 * 10 = 20000ms = 20秒
initLimit=10
# Follower 服务器与 Leader 服务器之间请求和应答的最大心跳数
# 即通信超时时长为 tickTime * syncLimit = 2000 * 5 = 10000ms = 10秒
syncLimit=5
# ZooKeeper 数据存储目录(存放内存数据快照和事务日志)
dataDir=/opt/module/zookeeper-3.5.7/zkData
# ZooKeeper 客户端连接端口
clientPort=2181
# 集群服务器配置
# server.A=B:C:D
# A: 服务器编号(对应 myid 文件中的数字)
# B: 服务器的 IP 地址或主机名
# C: Follower 与 Leader 交换信息的端口(数据同步端口)
# D: 选举端口(Leader 选举时使用的端口)
server.1=node1:2888:3888
server.2=node2:2888:3888
server.3=node3:2888:3888
# 配置 ZooKeeper 的 4 字命令(白名单)
# 包括 stat, ruok, conf, isro 等管理命令
# "四字命令"是指通过 telnet 或 nc 发送的 4 个字符命令
4lw.commands.whitelist=*
EOF
# ============ 5. 创建数据目录和 myid 文件 ============
# 创建 ZooKeeper 数据存储目录
mkdir -p $ZOOKEEPER_HOME/zkData
# 创建 myid 文件(每台机器的值不同)
# myid 文件是 ZooKeeper 识别集群成员的标识
# node1 设置为 1(对应 zoo.cfg 中的 server.1)
echo "1" > $ZOOKEEPER_HOME/zkData/myid
# 【注意】在 node2 上执行: echo "2" > $ZOOKEEPER_HOME/zkData/myid
# 【注意】在 node3 上执行: echo "3" > $ZOOKEEPER_HOME/zkData/myid
echo "ZooKeeper 配置完成"
# ============ 6. 分发到其他节点 ============
# 使用 rsync 或 scp 将 ZooKeeper 安装目录同步到其他节点
# 然后修改各节点的 myid 文件
for host in node2 node3; do
# -r: 递归复制目录
# -a: 归档模式,保留权限和时间
# -z: 传输时压缩
rsync -az /opt/module/zookeeper-3.5.7 $host:/opt/module/
echo "已同步到 $host"
done
# 【重要】需要在 node2 上执行: echo "2" > /opt/module/zookeeper-3.5.7/zkData/myid
# 【重要】需要在 node3 上执行: echo "3" > /opt/module/zookeeper-3.5.7/zkData/myid
# ============ 7. 启动 ZooKeeper 集群 ============
# 遍历所有节点,通过 SSH 远程启动 ZooKeeper
for host in node1 node2 node3; do
echo "在 $host 上启动 ZooKeeper..."
ssh $host "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
# 等待 ZooKeeper 完全启动
sleep 5
# ============ 8. 检查集群状态 ============
# 查看每台机器上 ZooKeeper 的角色(leader/follower)
for host in node1 node2 node3; do
echo "==== $host ===="
ssh $host "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
done
3.4 Hadoop 安装与配置
bash
# ============ 1. 解压 Hadoop ============
tar -zxvf /opt/software/hadoop-3.1.3.tar.gz -C /opt/module/
# ============ 2. 配置 Hadoop 环境变量 ============
cat >> ~/.bash_profile << 'EOF'
# Hadoop Environment
export HADOOP_HOME=/opt/module/hadoop-3.1.3
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
EOF
source ~/.bash_profile
# ============ 3. 配置 hadoop-env.sh ============
# hadoop-env.sh 是 Hadoop 的环境配置脚本
# 必须显式设置 JAVA_HOME,因为 SSH 远程启动时不会加载 .bash_profile
cat >> $HADOOP_HOME/etc/hadoop/hadoop-env.sh << 'EOF'
# 指定 JDK 安装路径
export JAVA_HOME=/opt/module/jdk1.8.0_212
# 指定 HDFS 相关进程的运行用户
# 如果不配置,启动时可能会报 "the xxx is not allowed to run" 错误
export HDFS_NAMENODE_USER=hadoop
export HDFS_DATANODE_USER=hadoop
export HDFS_JOURNALNODE_USER=hadoop
export HDFS_ZKFC_USER=hadoop
export HDFS_SECONDARYNAMENODE_USER=hadoop
# 指定 YARN 相关进程的运行用户
export YARN_RESOURCEMANAGER_USER=hadoop
export YARN_NODEMANAGER_USER=hadoop
EOF
# ============ 4. 配置 core-site.xml ============
cat > $HADOOP_HOME/etc/hadoop/core-site.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- 指定 HDFS 的默认文件系统名称为 nameservice 的逻辑名 -->
<property>
<name>fs.defaultFS</name>
<value>hdfs://mycluster</value>
</property>
<!-- 指定 Hadoop 临时数据存储目录 -->
<property>
<name>hadoop.tmp.dir</name>
<value>/opt/module/hadoop-3.1.3/data</value>
</property>
<!-- 指定 ZooKeeper 集群地址 -->
<property>
<name>ha.zookeeper.quorum</name>
<value>node1:2181,node2:2181,node3:2181</value>
</property>
</configuration>
EOF
# ============ 5. 配置 hdfs-site.xml ============
cat > $HADOOP_HOME/etc/hadoop/hdfs-site.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- NameService 逻辑名称 -->
<property>
<name>dfs.nameservices</name>
<value>mycluster</value>
</property>
<!-- 两个 NameNode 的标识符 -->
<property>
<name>dfs.ha.namenodes.mycluster</name>
<value>nn1,nn2</value>
</property>
<!-- nn1 RPC 地址 -->
<property>
<name>dfs.namenode.rpc-address.mycluster.nn1</name>
<value>node1:8020</value>
</property>
<!-- nn2 RPC 地址 -->
<property>
<name>dfs.namenode.rpc-address.mycluster.nn2</name>
<value>node2:8020</value>
</property>
<!-- nn1 HTTP 地址 -->
<property>
<name>dfs.namenode.http-address.mycluster.nn1</name>
<value>node1:9870</value>
</property>
<!-- nn2 HTTP 地址 -->
<property>
<name>dfs.namenode.http-address.mycluster.nn2</name>
<value>node2:9870</value>
</property>
<!-- JournalNode 集群地址 -->
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>qjournal://node1:8485;node2:8485;node3:8485/mycluster</value>
</property>
<!-- JournalNode 数据存储目录 -->
<property>
<name>dfs.journalnode.edits.dir</name>
<value>/opt/module/hadoop-3.1.3/data/journalnode</value>
</property>
<!-- 客户端故障转移代理类 -->
<property>
<name>dfs.client.failover.proxy.provider.mycluster</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>
<!-- 隔离机制:防止脑裂 -->
<property>
<name>dfs.ha.fencing.methods</name>
<value>sshfence(shell(/bin/true))</value>
</property>
<!-- SSH 私钥路径 -->
<property>
<name>dfs.ha.fencing.ssh.private-key-files</name>
<value>/home/hadoop/.ssh/id_rsa</value>
</property>
<!-- 开启自动故障转移 -->
<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
<!-- 副本数 -->
<property>
<name>dfs.replication</name>
<value>3</value>
</property>
<!-- 关闭权限检查(测试环境) -->
<property>
<name>dfs.permissions.enabled</name>
<value>false</value>
</property>
</configuration>
EOF
# ============ 6. 配置 yarn-site.xml ============
cat > $HADOOP_HOME/etc/hadoop/yarn-site.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- 开启 ResourceManager HA -->
<property>
<name>yarn.resourcemanager.ha.enabled</name>
<value>true</value>
</property>
<!-- 集群 ID -->
<property>
<name>yarn.resourcemanager.cluster-id</name>
<value>yarn-cluster</value>
</property>
<!-- ResourceManager 标识符 -->
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
<!-- rm1 主机名 -->
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>node1</value>
</property>
<!-- rm2 主机名 -->
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>node2</value>
</property>
<!-- rm1 Web UI -->
<property>
<name>yarn.resourcemanager.webapp.address.rm1</name>
<value>node1:8088</value>
</property>
<!-- rm2 Web UI -->
<property>
<name>yarn.resourcemanager.webapp.address.rm2</name>
<value>node2:8088</value>
</property>
<!-- ZooKeeper 地址 -->
<property>
<name>ha.zookeeper.quorum</name>
<value>node1:2181,node2:2181,node3:2181</value>
</property>
<!-- 开启自动故障转移 -->
<property>
<name>yarn.resourcemanager.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
<!-- 内嵌式选举 -->
<property>
<name>yarn.resourcemanager.ha.automatic-failover.embedded</name>
<value>true</value>
</property>
<!-- 状态存储在 ZooKeeper -->
<property>
<name>yarn.resourcemanager.store.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
</property>
<!-- NodeManager 辅助服务 -->
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<!-- 开启日志聚合 -->
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
<!-- 日志保留 7 天 -->
<property>
<name>yarn.log-aggregation.retain-seconds</name>
<value>604800</value>
</property>
<!-- NodeManager 可用内存 -->
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>4096</value>
</property>
<!-- NodeManager 可用 CPU 核数 -->
<property>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>4</value>
</property>
</configuration>
EOF
# ============ 7. 配置 mapred-site.xml ============
cat > $HADOOP_HOME/etc/hadoop/mapred-site.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- 指定 MapReduce 运行在 YARN 上 -->
<!-- 可选值: local(本地模式)、classic(经典模式)、yarn -->
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<!-- 历史服务器地址(用于查看已完成的 MapReduce 作业历史) -->
<property>
<name>mapreduce.jobhistory.address</name>
<value>node1:10020</value>
</property>
<!-- 历史服务器 Web UI 地址 -->
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>node1:19888</value>
</property>
</configuration>
EOF
# ============ 8. 配置 workers 文件 ============
# workers 文件列出所有 DataNode 和 NodeManager 的主机名
# start-dfs.sh 会在此文件中列出的每个节点上启动 DataNode
# start-yarn.sh 会在此文件中列出的每个节点上启动 NodeManager
cat > $HADOOP_HOME/etc/hadoop/workers << 'EOF'
node1
node2
node3
EOF
# ============ 9. 分发 Hadoop 到其他节点 ============
for host in node2 node3; do
echo "正在同步 Hadoop 到 $host..."
rsync -az /opt/module/hadoop-3.1.3 $host:/opt/module/
rsync -az ~/.bash_profile $host:~/
echo "$host 同步完成"
done
echo "Hadoop 配置完成,已分发到所有节点"
3.5 集群初始化与启动
bash
#!/bin/bash
# init-and-start-ha.sh
# Hadoop HA 集群初始化和启动脚本
# 【注意】此脚本仅在首次部署时执行初始化步骤
echo "============ Hadoop HA 集群初始化与启动 ============"
# ---- 步骤 1: 启动 ZooKeeper 集群 ----
echo "【步骤1】启动 ZooKeeper 集群..."
for host in node1 node2 node3; do
ssh $host "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
sleep 5
# 验证 ZK 状态
for host in node1 node2 node3; do
echo "$host: $(ssh $host '/opt/module/zookeeper-3.5.7/bin/zkServer.sh status' | grep Mode)"
done
# ---- 步骤 2: 启动 JournalNode ----
echo ""
echo "【步骤2】启动 JournalNode..."
for host in node1 node2 node3; do
ssh $host "/opt/module/hadoop-3.1.3/bin/hdfs --daemon start journalnode"
done
sleep 3
# 验证 JournalNode 进程
for host in node1 node2 node3; do
jn=$(ssh $host "jps | grep JournalNode")
echo "$host: $jn"
done
# ---- 步骤 3: 格式化 NameNode(仅首次执行) ----
echo ""
echo "【步骤3】格式化 NameNode(仅在 node1 上执行)..."
# hdfs namenode -format 命令会:
# 1. 生成集群唯一的 ClusterID
# 2. 创建空的 fsimage 文件
# 3. 创建 edits 文件
# 4. 将格式化信息写入 VERSION 文件
/opt/module/hadoop-3.1.3/bin/hdfs namenode -format
# ---- 步骤 4: 启动 node1 的 NameNode ----
echo ""
echo "【步骤4】启动 node1 的 NameNode..."
/opt/module/hadoop-3.1.3/bin/hdfs --daemon start namenode
sleep 3
echo "node1 NameNode 已启动"
# ---- 步骤 5: 在 node2 上同步元数据并启动 NameNode ----
echo ""
echo "【步骤5】在 node2 上同步 NameNode 元数据..."
# hdfs namenode -bootstrapStandby 命令会:
# 1. 从 Active NameNode (node1) 下载最新的 fsimage 文件
# 2. 从 JournalNode 下载尚未合并的 edits 文件
# 3. 合并生成完整的元数据
# 4. 此步骤使 node2 成为有效的 Standby NameNode
ssh node2 "/opt/module/hadoop-3.1.3/bin/hdfs namenode -bootstrapStandby"
echo "启动 node2 的 NameNode..."
ssh node2 "/opt/module/hadoop-3.1.3/bin/hdfs --daemon start namenode"
sleep 3
echo "node2 NameNode 已启动"
# ---- 步骤 6: 格式化 ZKFC(仅首次执行) ----
echo ""
echo "【步骤6】格式化 ZKFC..."
# hdfs zkfc -formatZK 命令会:
# 1. 连接到 ZooKeeper 集群
# 2. 在 ZK 中创建 /hadoop-ha/mycluster znode(PERSISTENT 类型)
# 3. 此 znode 用于后续 ZKFC 的 Active/Standby 选举
# 【注意】如果已经格式化过,会提示已存在,忽略即可
/opt/module/hadoop-3.1.3/bin/hdfs zkfc -formatZK
# ---- 步骤 7: 启动 HDFS ----
echo ""
echo "【步骤7】启动 HDFS 集群..."
# start-dfs.sh 脚本会:
# 1. 在 workers 文件中的每个节点上启动 DataNode
# 2. 在配置了 NameNode 的节点上启动 ZKFC
/opt/module/hadoop-3.1.3/sbin/start-dfs.sh
# ---- 步骤 8: 启动 YARN ----
echo ""
echo "【步骤8】启动 YARN 集群..."
# start-yarn.sh 脚本会:
# 1. 在配置了 ResourceManager 的节点 (node1, node2) 上启动 RM
# 2. 在 workers 文件中的每个节点上启动 NodeManager
/opt/module/hadoop-3.1.3/sbin/start-yarn.sh
# ---- 步骤 9: 启动历史服务器 ----
echo ""
echo "【步骤9】启动 MapReduce 历史服务器..."
# 在 node1 上启动 JobHistoryServer
# 历史服务器用于查看已完成的 MapReduce 作业的详细信息
ssh node1 "/opt/module/hadoop-3.1.3/bin/mapred --daemon start historyserver"
# ---- 验证集群状态 ----
echo ""
echo "============ 集群状态验证 ============"
# 等待所有服务完全启动
sleep 10
# 检查 HDFS HA 状态
echo "HDFS HA 状态:"
echo " nn1: $(hdfs haadmin -getServiceState nn1)"
echo " nn2: $(hdfs haadmin -getServiceState nn2)"
# 检查 YARN HA 状态
echo "YARN HA 状态:"
echo " rm1: $(yarn rmadmin -getServiceState rm1)"
echo " rm2: $(yarn rmadmin -getServiceState rm2)"
# 检查 HDFS 集群报告
echo ""
echo "HDFS 集群报告:"
hdfs dfsadmin -report | head -20
# 检查所有 Java 进程
echo ""
echo "各节点 Java 进程:"
for host in node1 node2 node3; do
echo "==== $host ===="
ssh $host "jps"
echo ""
done
echo "============ Hadoop HA 集群启动完成 ============"
3.6 集群验证测试
bash
#!/bin/bash
# test-ha-cluster.sh
# Hadoop HA 集群功能验证脚本
echo "============ Hadoop HA 集群功能测试 ============"
# ---- 测试 1: HDFS 基本读写 ----
echo ""
echo "【测试1】HDFS 基本读写测试"
# 创建测试目录
hdfs dfs -mkdir -p /ha-test
# 创建测试文件
echo "Hello Hadoop HA Cluster!" > /tmp/test.txt
echo "This is line 2." >> /tmp/test.txt
echo "This is line 3." >> /tmp/test.txt
# 上传文件到 HDFS
hdfs dfs -put /tmp/test.txt /ha-test/
# 读取文件内容
echo "HDFS 文件内容:"
hdfs dfs -cat /ha-test/test.txt
# 查看文件列表
echo "HDFS 文件列表:"
hdfs dfs -ls /ha-test/
# ---- 测试 2: HDFS NameNode 故障转移 ----
echo ""
echo "【测试2】HDFS NameNode 故障转移测试"
# 查看当前 Active NN
active_nn=$(hdfs haadmin -getServiceState nn1 2>/dev/null)
if [ "$active_nn" = "active" ]; then
old_active="nn1"
new_active="nn2"
else
old_active="nn2"
new_active="nn1"
fi
echo "当前 Active NameNode: $old_active"
# 手动触发故障转移
echo "执行故障转移: $old_active -> $new_active"
hdfs haadmin -failover $old_active $new_active
# 验证故障转移结果
echo "故障转移后状态:"
echo " nn1: $(hdfs haadmin -getServiceState nn1)"
echo " nn2: $(hdfs haadmin -getServiceState nn2)"
# 验证故障转移后 HDFS 仍可正常读写
echo "故障转移后读取文件:"
hdfs dfs -cat /ha-test/test.txt
echo "HDFS NameNode 故障转移测试: 通过!"
# 将 Active 切换回 nn1
hdfs haadmin -failover $new_active $old_active
# ---- 测试 3: YARN ResourceManager 故障转移 ----
echo ""
echo "【测试3】YARN ResourceManager 故障转移测试"
# 查看当前 Active RM
active_rm=$(yarn rmadmin -getServiceState rm1 2>/dev/null)
if [ "$active_rm" = "active" ]; then
old_active="rm1"
new_active="rm2"
else
old_active="rm2"
new_active="rm1"
fi
echo "当前 Active ResourceManager: $old_active"
# 手动故障转移
echo "执行故障转移: $old_active -> $new_active"
yarn rmadmin -failover $old_active $new_active
# 验证
echo "故障转移后状态:"
echo " rm1: $(yarn rmadmin -getServiceState rm1)"
echo " rm2: $(yarn rmadmin -getServiceState rm2)"
echo "YARN ResourceManager 故障转移测试: 通过!"
# 切换回来
yarn rmadmin -failover $new_active $old_active
# ---- 测试 4: MapReduce 作业测试 ----
echo ""
echo "【测试4】MapReduce 作业提交测试(WordCount)"
# 上传测试数据
hdfs dfs -mkdir -p /wordcount/input
hdfs dfs -put /tmp/test.txt /wordcount/input/
# 提交 WordCount 作业
# 使用 Hadoop 自带的 MapReduce 示例 jar
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar \
wordcount \
/wordcount/input \
/wordcount/output
# 查看作业输出结果
echo "WordCount 结果:"
hdfs dfs -cat /wordcount/output/part-r-00000
echo "MapReduce 作业测试: 通过!"
# ---- 测试 5: 自动故障转移测试 ----
echo ""
echo "【测试5】自动故障转移测试"
active_nn=$(hdfs haadmin -getServiceState nn1 2>/dev/null)
if [ "$active_nn" = "active" ]; then
kill_host="node1"
else
kill_host="node2"
fi
echo "当前 Active 在 $kill_host 上,将杀死该节点的 NameNode..."
# SSH 到目标节点,杀死 NameNode 进程
ssh $kill_host "kill \$(jps | grep NameNode | awk '{print \$1}')"
echo "已杀死 $kill_host 上的 NameNode,等待 15 秒..."
# 等待自动故障转移
sleep 15
# 验证另一个节点是否自动成为 Active
echo "自动故障转移后状态:"
echo " nn1: $(hdfs haadmin -getServiceState nn1 2>/dev/null || echo '无法连接')"
echo " nn2: $(hdfs haadmin -getServiceState nn2 2>/dev/null || echo '无法连接')"
# 验证 HDFS 仍可正常读写
echo "自动故障转移后读取文件:"
hdfs dfs -cat /ha-test/test.txt
echo "自动故障转移测试: 通过!"
# 恢复被杀死的 NameNode
echo "恢复 $kill_host 上的 NameNode..."
ssh $kill_host "/opt/module/hadoop-3.1.3/bin/hdfs --daemon start namenode"
sleep 5
# ---- 清理测试数据 ----
echo ""
echo "清理测试数据..."
hdfs dfs -rm -r /ha-test
hdfs dfs -rm -r /wordcount
echo ""
echo "============ 所有测试完成 ============"
3.7 集群日常运维脚本
bash
#!/bin/bash
# cluster-maintenance.sh
# Hadoop HA 集群日常运维管理脚本
# 定义集群节点列表
NODES="node1 node2 node3"
HADOOP_HOME="/opt/module/hadoop-3.1.3"
ZK_HOME="/opt/module/zookeeper-3.5.7"
# 函数:启动集群全部服务
start_all() {
echo "============ 启动全部服务 ============"
# 1. 启动 ZooKeeper
echo "启动 ZooKeeper..."
for host in $NODES; do
ssh $host "$ZK_HOME/bin/zkServer.sh start"
done
sleep 5
# 2. 启动 HDFS(包含 NameNode, DataNode, JournalNode, ZKFC)
echo "启动 HDFS..."
$HADOOP_HOME/sbin/start-dfs.sh
# 3. 启动 YARN(包含 ResourceManager, NodeManager)
echo "启动 YARN..."
$HADOOP_HOME/sbin/start-yarn.sh
# 4. 启动历史服务器
echo "启动 HistoryServer..."
$HADOOP_HOME/bin/mapred --daemon start historyserver
echo "全部服务启动完成"
}
# 函数:停止集群全部服务
stop_all() {
echo "============ 停止全部服务 ============"
# 1. 停止历史服务器
echo "停止 HistoryServer..."
$HADOOP_HOME/bin/mapred --daemon stop historyserver
# 2. 停止 YARN
echo "停止 YARN..."
$HADOOP_HOME/sbin/stop-yarn.sh
# 3. 停止 HDFS
echo "停止 HDFS..."
$HADOOP_HOME/sbin/stop-dfs.sh
# 4. 停止 ZooKeeper
echo "停止 ZooKeeper..."
for host in $NODES; do
ssh $host "$ZK_HOME/bin/zkServer.sh stop"
done
echo "全部服务已停止"
}
# 函数:查看集群状态
show_status() {
echo "============ 集群状态 ============"
echo "检查时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# ZooKeeper 状态
echo "--- ZooKeeper ---"
for host in $NODES; do
status=$(ssh $host "$ZK_HOME/bin/zkServer.sh status 2>&1" | grep Mode)
echo " $host: $status"
done
# Java 进程
echo ""
echo "--- Java 进程 ---"
for host in $NODES; do
echo " $host:"
ssh $host "jps" | sed 's/^/ /'
done
# HDFS HA 状态
echo ""
echo "--- HDFS HA ---"
echo " nn1: $(hdfs haadmin -getServiceState nn1 2>/dev/null || echo '无法连接')"
echo " nn2: $(hdfs haadmin -getServiceState nn2 2>/dev/null || echo '无法连接')"
# YARN HA 状态
echo ""
echo "--- YARN HA ---"
echo " rm1: $(yarn rmadmin -getServiceState rm1 2>/dev/null || echo '无法连接')"
echo " rm2: $(yarn rmadmin -getServiceState rm2 2>/dev/null || echo '无法连接')"
# HDFS 存储信息
echo ""
echo "--- HDFS 存储 ---"
hdfs dfsadmin -report 2>/dev/null | grep -E "DFS Used|Configured Capacity|Live datanodes"
}
# 主菜单
case "$1" in
start)
start_all
;;
stop)
stop_all
;;
restart)
stop_all
sleep 5
start_all
;;
status)
show_status
;;
*)
echo "用法: $0 {start|stop|restart|status}"
echo " start - 启动全部服务"
echo " stop - 停止全部服务"
echo " restart - 重启全部服务"
echo " status - 查看集群状态"
exit 1
;;
esac
3.8 HDFS Shell 综合操作命令示例
bash
# ============ 文件和目录操作 ============
# 创建多级目录
hdfs dfs -mkdir -p /user/hadoop/input
# 上传单个文件
hdfs dfs -put /local/file.txt /user/hadoop/input/
# 上传整个目录(递归上传)
hdfs dfs -put -r /local/directory /user/hadoop/
# 使用 copyFromLocal 上传文件(与 put 类似)
hdfs dfs -copyFromLocal /local/file.txt /user/hadoop/input/
# 下载文件到本地
hdfs dfs -get /user/hadoop/output/part-r-00000 /local/
# 使用 moveToLocal 下载(HDFS 上的文件被标记为删除)
hdfs dfs -moveToLocal /user/hadoop/output/part-r-00000 /local/
# 列出目录内容(详细信息)
hdfs dfs -ls -h /user/hadoop/input/
# -h: 以人类可读的格式显示文件大小(KB, MB, GB)
# 递归列出所有文件
hdfs dfs -ls -R /user/hadoop/
# 查看文件内容
hdfs dfs -cat /user/hadoop/input/file.txt
# 查看文件末尾内容(最后 1KB)
hdfs dfs -tail /user/hadoop/input/file.txt
# 显示文件大小
hdfs dfs -du -h /user/hadoop/input/
# -h: 以人类可读的格式显示
# 复制文件(HDFS 内部)
hdfs dfs -cp /user/hadoop/input/file.txt /user/hadoop/output/
# 移动/重命名文件
hdfs dfs -mv /user/hadoop/input/old.txt /user/hadoop/input/new.txt
# 删除文件
hdfs dfs -rm /user/hadoop/output/file.txt
# 递归删除目录
hdfs dfs -rm -r /user/hadoop/output/
# 跳过回收站直接删除(Hadoop 3.x 默认启用回收站)
hdfs dfs -rm -r -skipTrash /user/hadoop/output/
# ============ HDFS 管理命令 ============
# 查看 HDFS 集群报告
hdfs dfsadmin -report
# 查看 HDFS 健康状态(检查损坏的块)
hdfs fsck /
# 查看详细的块信息
hdfs fsck / -files -blocks -locations
# 进入安全模式(只读模式,用于维护)
hdfs dfsadmin -safemode enter
# 离开安全模式
hdfs dfsadmin -safemode leave
# 查看安全模式状态
hdfs dfsadmin -safemode get
# 刷新节点(添加新节点后执行)
hdfs dfsadmin -refreshNodes
# 设置副本数
hdfs dfs -setrep 3 /user/hadoop/input/file.txt
四、本章小结
4.1 知识要点总结
┌─────────────────────────────────────────────────────────────────────┐
│ Hadoop 3.x 高可用集群知识体系 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. HDFS HA │
│ ├── 核心组件: Active NN + Standby NN + JournalNode + ZKFC + ZK │
│ ├── 共享存储: JournalNode 集群(至少3个,奇数个,Paxos 协议) │
│ ├── 状态同步: Active NN 写 EditLog → JN → Standby NN 读取 │
│ ├── 故障转移: ZKFC 通过 ZK 的 ephemeral node 实现自动切换 │
│ ├── 脑裂防护: sshfence / shellfence 隔离旧 Active NN │
│ └── 客户端透明: nameservice 逻辑名 + ZK 自动发现 Active NN │
│ │
│ 2. YARN HA │
│ ├── 核心组件: Active RM + Standby RM + ZKFC(内嵌) + ZK │
│ ├── 状态存储: ZKRMStateStore(ZooKeeper 存储应用状态) │
│ ├── 故障转移: StandbyElector 通过 ZK 选举自动切换 │
│ ├── NM 注册: NodeManager 向所有 RM 注册,但只接受 Active 指令 │
│ └── 作业影响: 正在运行的 Container 通常可继续,AM 需重新连接 │
│ │
│ 3. 部署要点 │
│ ├── 环境准备: JDK、SSH 免密、hosts、关闭防火墙、NTP 时间同步 │
│ ├── ZooKeeper: 必须先于 Hadoop 启动,myid 文件不可忘记 │
│ ├── 启动顺序: ZK → JN → NN(format) → NN(bootstrapStandby) │
│ │ → ZKFC(formatZK) → DN → RM → NM → HistoryServer │
│ ├── 核心配置: core-site.xml + hdfs-site.xml + yarn-site.xml │
│ └── 运维工具: haadmin / rmadmin 故障转移、脚本自动化管理 │
│ │
│ 4. 关键注意事项 │
│ ├── JournalNode 必须奇数个部署(通常 3 个或 5 个) │
│ ├── ZooKeeper 集群也必须奇数个部署 │
│ ├── hdfs-site.xml 中 dfs.ha.fencing.methods 必须配置 │
│ ├── 格式化 NN 只需在一台机器上执行一次 │
│ ├── bootstrapStandby 只需在 Standby 机器上执行一次 │
│ └── formatZK 只需执行一次,会创建 HA 的 znode │
│ │
└─────────────────────────────────────────────────────────────────────┘
4.2 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| NameNode 无法启动 | EditLog 损坏或 JN 不可用 | 检查 JN 进程是否运行;检查 JN 日志 |
| 两个 NameNode 都是 Standby | ZKFC 未启动或 ZK 连接失败 | 启动 ZKFC;检查 ZK 集群状态 |
| 脑裂(两个 Active NN) | 隔离机制配置错误 | 检查 dfs.ha.fencing.methods 配置和 SSH 免密 |
| ResourceManager 无法切换 | ZKRMStateStore 数据异常 | 检查 ZK 中 /yarn-leader-election znode |
| bootstrapStandby 失败 | Active NN 未启动或网络不通 | 确认 Active NN 已启动且端口可达 |
| DataNode 不注册到 NN | dfs.nameservices 配置不匹配 |
检查所有节点的 hdfs-site.xml 一致性 |
4.3 HA 架构优缺点
优点:
- 消除了 NameNode 和 ResourceManager 的单点故障
- 自动故障转移,无需人工干预
- 故障切换时间通常在秒级(30 秒以内)
- 对客户端透明,无需修改客户端代码
缺点:
- 增加了集群的复杂度(ZooKeeper、JournalNode 等额外组件)
- Standby NN 需要同步元数据,增加了网络和 I/O 开销
- YARN HA 故障转移时,正在运行的 Application 可能需要重新提交
- 需要更多的服务器资源(至少 3 台机器用于 ZK 和 JN)