Hadoop学习教程,从入门到精通, Hadoop 3.x 高可用集群 — 知识点详解(6)

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)
相关推荐
heart_66621 小时前
AMD平台实战:ModelScope 一键微调 Gemma 4 情绪分类实战
大数据·人工智能·datawhale·amdev
Agilex松灵机器人1 小时前
万小时数据落地!松灵机器人构建具身智能数据新基建
大数据·人工智能·机器人·具身智能·松灵机器人
searchforAI2 小时前
坚持用AI做笔记,我的知识留存与学习速度大幅提升
人工智能·笔记·学习·ai·知识图谱·知识管理
Chris _data2 小时前
c#学习WPF笔记(一)
学习·c#·wpf
大大大大晴天️2 小时前
Flink Resource Providers 深度解析:机制原理、部署模式与最佳实践
大数据·flink
AOwhisky10 小时前
Redis 学习笔记(第三期):持久化与主从复制
运维·数据库·redis·笔记·学习·云计算
听你说3211 小时前
科技护航极限征程 三诺生物助力雄关330长城越野赛
大数据·科技·健康医疗
电商API_1800790524711 小时前
bilibili关键字搜索视频列表|获取视频详情API调用示例
大数据·数据挖掘·网络爬虫·音视频
Tbisnic11 小时前
AI大模型学习第十一天:技术选型、安全防护与金融实战
python·学习·ai·大模型·提示词工程