Hadoop学习教程,从入门到精通, HDFS分布式文件系统 — 完整知识点与案例代码(3)

HDFS分布式文件系统 --- 完整知识点与案例代码


一、文件系统的分类

1.1 知识点概述

分类方式 类型 说明
按存储位置 本地文件系统 数据存储在单机磁盘上,如 ext4、NTFS、XFS
分布式文件系统 数据分布在多台机器上,如 HDFS、Ceph、GFS
按访问方式 磁盘文件系统 传统机械硬盘/固态硬盘存储
网络文件系统(NFS) 通过网络访问远程文件
虚拟文件系统 如 /proc、/sys 等内核虚拟文件系统
按数据模型 块存储 以固定大小的块为单位存储(HDFS采用)
对象存储 以对象为单位,如 Amazon S3
文件存储 传统目录树结构

1.2 本地文件系统 vs 分布式文件系统

复制代码
本地文件系统:
┌──────────┐
│ 单台机器  │ → 数据量受限于单机磁盘容量
│  ext4/NTFS│ → 无容错能力,磁盘损坏数据丢失
└──────────┘

分布式文件系统:
┌──────┐  ┌──────┐  ┌──────┐
│机器1 │  │机器2 │  │机器3 │  → 数据分散存储在多台机器
│      │  │      │  │      │  → 自动冗余备份
└──────┘  └──────┘  └──────┘  → 水平扩展,容量几乎无限

二、HDFS 简介

2.1 核心概念

HDFS(Hadoop Distributed File System)是 Apache Hadoop 项目的核心子项目,是一个分布式、可扩展、高容错的文件系统。

设计目标:

  • 存储超大文件:适合 GB、TB、PB 级别的数据
  • 流式数据访问:一次写入,多次读取(Write-Once-Read-Many)
  • 运行在廉价硬件上:通过软件层面实现高可用,不依赖高端硬件
  • 高吞吐量:牺牲低延迟,换取高数据吞吐

2.2 HDFS 不适合的场景

不适合场景 原因
低延迟数据访问 HDFS 追求吞吐量,延迟较高(毫秒~秒级)
大量小文件 NameNode 内存有限,每个文件约占150字节元数据
多方写入 HDFS 不支持多个写入者并发写同一个文件
随机修改文件 HDFS 只支持追加(append),不支持随机修改

三、HDFS 架构

3.1 核心组件

复制代码
                    HDFS 架构总览
                    
     ┌─────────────────────────────────────┐
     │          Client(客户端)             │
     │  文件读写请求、与NameNode/DataNode通信  │
     └──────────┬──────────────┬────────────┘
                │              │
                ▼              ▼
     ┌──────────────┐  ┌──────────────────┐
     │   NameNode    │  │  Secondary        │
     │  (主节点)    │  │  NameNode         │
     │  管理元数据    │  │  (辅助节点)      │
     │  协调数据访问  │  │  合并FsImage+Edits│
     └──────┬───────┘  └──────────────────┘
            │
    ┌───────┼───────────┬──────────────┐
    ▼       ▼           ▼              ▼
┌──────┐┌──────┐   ┌──────┐     ┌──────┐
│DataNode│DataNode│  │DataNode│   │DataNode│
│节点1  ││节点2  │   │节点3  │     │节点4  │
│存数据块││存数据块│   │存数据块│     │存数据块│
└──────┘└──────┘   └──────┘     └──────┘

3.2 各组件详细职责

NameNode(名称节点):

  • 管理文件系统的命名空间(Namespace)
  • 维护文件 → 数据块(Block)→ DataNode 的映射关系
  • 处理客户端的文件读写请求
  • 接收 DataNode 的心跳(Heartbeat)和块报告(Block Report)
  • 元数据存储:FsImage (镜像文件) + EditLog(编辑日志)

DataNode(数据节点):

  • 实际存储数据块(Block,默认128MB)
  • 执行数据的读写操作
  • 定期向 NameNode 发送心跳和块报告
  • 执行数据块的创建、删除、复制

Secondary NameNode:

  • 不是 NameNode 的热备份
  • 定期合并 FsImage 和 EditLog,防止 EditLog 过大
  • 在 NameNode 故障时可辅助恢复(但有一定数据丢失风险)

3.3 元数据存储机制

复制代码
NameNode 启动流程:
1. 加载 FsImage(磁盘)→ 内存中的旧元数据
2. 加载 EditLog(磁盘)→ 内存中重放增量操作
3. 合并后得到最新元数据快照
4. 生成新的 FsImage,清空 EditLog
5. 开始对外服务

Secondary NameNode 合并流程(Checkpoint):
1. 请求 NameNode 滚动 EditLog → edits.new
2. 从 NameNode 下载 FsImage + 旧 EditLog
3. 在本地合并为新的 FsImage
4. 将新 FsImage 上传回 NameNode
5. NameNode 用新 FsImage 替换旧的

四、HDFS 的特点

4.1 核心特点详解

特点 说明
高容错性 数据自动冗余存储(默认副本数=3),自动检测和恢复故障节点
高吞吐量 数据分块存储,并行读写,适合批量数据处理
流式数据访问 数据一次写入,多次读取,适合MapReduce/Spark批处理
大数据集 单个文件可达 PB 级别,集群可扩展到数千节点
简单一致性模型 一个文件写入后不能修改,只能追加,简化了数据一致性问题
移动计算而非移动数据 将计算任务调度到数据所在节点,减少网络传输

4.2 副本放置策略(机架感知)

复制代码
默认3副本放置策略:

第1个副本:写入客户端所在节点(同机架)
第2个副本:不同机架的某个随机节点
第3个副本:与第2个副本同一机架的不同节点

         机架1                机架2
    ┌──────────┐        ┌──────────┐
    │ DataNode1 │        │ DataNode4 │
    │ (副本1)   │        │ (副本2)   │
    │ DataNode2 │        │ DataNode5 │
    │           │        │ (副本3)   │
    │ DataNode3 │        │ DataNode6 │
    └──────────┘        └──────────┘

4.3 数据块(Block)

复制代码
HDFS Block 知识点:
- Hadoop 2.x/3.x 默认块大小:128MB(Hadoop 1.x 为64MB)
- 一个文件会被切分为多个 Block
- 每个 Block 会以独立文件的形式存储在 DataNode 的本地磁盘上
- 即使文件不足一个 Block 大小,也不会占用整个 Block 的磁盘空间
  (例如 100MB 的文件只占 100MB,不会占 128MB)

五、HDFS 的文件读写流程

5.1 HDFS 读数据流程

复制代码
Client                    NameNode                 DataNode
  │                          │                        │
  │  1. open(file)           │                        │
  │─────────────────────────>│                        │
  │                          │                        │
  │  2. 返回文件的Block列表   │                        │
  │     (每个Block的位置信息) │                        │
  │<─────────────────────────│                        │
  │                          │                        │
  │  3. read(Block)          │                        │
  │──────────────────────────────────────────────────>│
  │                          │                        │
  │  4. 返回数据流            │                        │
  │<──────────────────────────────────────────────────│
  │                          │                        │
  │  5. 读取完毕,close()     │                        │

读取策略:

  • Client 优先读取距离自己最近的副本(同节点 > 同机架 > 同数据中心 > 跨数据中心)
  • 如果某个 DataNode 读取失败,自动切换到下一个最近的副本

5.2 HDFS 写数据流程

复制代码
Client         NameNode        DataNode1   DataNode2   DataNode3
  │                │               │            │           │
  │ 1.create(file) │               │            │           │
  │───────────────>│               │            │           │
  │                │               │            │           │
  │ 2.返回FSData   │               │            │           │
  │  OutputStream  │               │            │           │
  │<───────────────│               │            │           │
  │                │               │            │           │
  │ 3.请求写入Block │               │            │           │
  │───────────────>│               │            │           │
  │                │               │            │           │
  │ 4.返回DataNode列表              │            │           │
  │<───────────────│               │            │           │
  │                │               │            │           │
  │ 5.建立Pipeline(DN1→DN2→DN3)  │            │           │
  │──────────────────────────────>│───────────>│──────────>│
  │                │               │            │           │
  │ 6.写数据Packet  │               │            │           │
  │──────────────────────────────>│───────────>│──────────>│
  │                │               │            │           │
  │ 7.ack确认       │               │            │           │
  │<──────────────────────────────│<───────────│<──────────│
  │                │               │            │           │
  │ 8.close()       │               │            │           │
  │                │               │            │           │

写入关键步骤:

  1. Client 向 NameNode 请求创建文件
  2. NameNode 校验权限和文件是否已存在
  3. Client 请求写入数据,NameNode 返回可用的 DataNode 列表
  4. Client 与 DataNode 建立 Pipeline(管道)
  5. 数据以 Packet(默认64KB)为单位沿 Pipeline 传输
  6. 每个 DataNode 收到数据后向上传递 ACK 确认
  7. 所有数据写入完成后关闭文件

六、HDFS 的健壮性

6.1 故障类型与处理机制

故障类型 检测机制 处理方式
DataNode 故障 心跳机制(默认3秒/次,超时10分钟+30秒判定死亡) NameNode 重新调度副本复制到其他节点
数据损坏 Checksum 校验(CRC32) 从其他副本读取正确数据,删除损坏副本
NameNode 故障 HA 机制(Active/Standby 双NameNode) Standby NameNode 自动接管
网络分区 心跳超时 触发副本重复制策略

6.2 心跳机制

复制代码
DataNode 每隔 3 秒(dfs.heartbeat.interval)向 NameNode 发送心跳:

DataNode ──heartbeat──> NameNode
DataNode ──heartbeat──> NameNode
DataNode ──heartbeat──> NameNode
         ... (若连续 10 分 30 秒无心跳)
NameNode: 标记该 DataNode 为 DEAD
NameNode: 检查该节点上的 Block 副本数
          若某个 Block 副本数 < 目标副本数(3)
          → 调度其他 DataNode 进行副本复制

6.3 安全模式

复制代码
NameNode 启动时进入安全模式(Safe Mode):
┌─────────────────────────────────────────────┐
│ 1. 加载 FsImage 和 EditLog                    │
│ 2. 接收 DataNode 的 Block Report              │
│ 3. 检查每个 Block 的副本数是否满足最小要求       │
│ 4. 当满足条件的 Block 比例达到阈值(99.9%)        │
│    → 退出安全模式                              │
│                                               │
│ 安全模式期间:                                  │
│ - 不允许文件的创建、修改、删除                    │
│ - 只允许读取操作                                │
└─────────────────────────────────────────────┘

相关命令:
hdfs dfsadmin -safemode get      # 查看安全模式状态
hdfs dfsadmin -safemode enter    # 进入安全模式
hdfs dfsadmin -safemode leave    # 离开安全模式

七、HDFS 的 Shell 操作

7.1 HDFS Shell 介绍

HDFS 提供了命令行工具,格式为 hdfs dfshadoop fs(两者功能等价)。

7.2 常用 Shell 命令大全

7.2.1 文件与目录操作
bash 复制代码
# ============================================================
# 1. 创建目录
# ============================================================
# -p 表示递归创建多级目录
hdfs dfs -mkdir -p /user/hadoop/data/input

# ============================================================
# 2. 查看目录内容
# ============================================================
# 列出 / 目录下的所有文件和目录
hdfs dfs -ls /

# 递归列出所有子目录中的文件(类似Linux的find)
hdfs dfs -ls -R /user/hadoop

# 以人类可读的方式显示文件大小(KB/MB/GB)
hdfs dfs -ls -h /user/hadoop/data

# ============================================================
# 3. 上传文件到HDFS
# ============================================================
# 将本地文件上传到HDFS
hdfs dfs -put /local/path/file.txt /hdfs/path/

# 等价于 -put
hdfs dfs -copyFromLocal /local/path/file.txt /hdfs/path/

# -f 表示覆盖已存在的文件
hdfs dfs -put -f /local/path/file.txt /hdfs/path/

# -l 允许并发上传(Hadoop 3.x)
hdfs dfs -put -l /local/path/largefile.dat /hdfs/path/

# ============================================================
# 4. 从HDFS下载文件
# ============================================================
# 将HDFS文件下载到本地
hdfs dfs -get /hdfs/path/file.txt /local/path/

# 等价于 -get
hdfs dfs -copyToLocal /hdfs/path/file.txt /local/path/

# ============================================================
# 5. 查看文件内容
# ============================================================
# 查看整个文件内容
hdfs dfs -cat /hdfs/path/file.txt

# 查看文件末尾1KB数据(类似Linux的tail)
hdfs dfs -tail /hdfs/path/file.txt

# 分页查看(按Enter翻页)
hdfs dfs -cat /hdfs/path/largefile.txt | less

# 统计文件行数、单词数、字节数
hdfs dfs -cat /hdfs/path/file.txt | wc -l

# ============================================================
# 6. 文件复制与移动
# ============================================================
# 在HDFS内部复制文件
hdfs dfs -cp /hdfs/source/file.txt /hdfs/dest/

# 移动/重命名文件
hdfs dfs -mv /hdfs/oldname.txt /hdfs/newname.txt

# ============================================================
# 7. 删除文件和目录
# ============================================================
# 删除文件
hdfs dfs -rm /hdfs/path/file.txt

# 递归删除目录及其下所有内容
hdfs dfs -rm -r /hdfs/path/directory/

# 跳过回收站直接删除
hdfs dfs -rm -r -skipTrash /hdfs/path/directory/

# 清空回收站
hdfs dfs -expunge

# ============================================================
# 8. 查看文件/磁盘信息
# ============================================================
# 显示文件大小(字节)
hdfs dfs -du /hdfs/path/

# 以人类可读方式显示大小
hdfs dfs -du -h /hdfs/path/

# 显示HDFS总容量、已用空间、可用空间
hdfs dfs -df -h

# ============================================================
# 9. 修改文件权限
# ============================================================
# 修改文件权限(类似Linux chmod)
hdfs dfs -chmod 755 /hdfs/path/file.txt

# 递归修改目录权限
hdfs dfs -chmod -R 755 /hdfs/path/

# 修改文件所有者
hdfs dfs -chown hadoop:hadoop /hdfs/path/file.txt

# ============================================================
# 10. 文件追加
# ============================================================
# 向HDFS文件追加内容
hdfs dfs -appendToFile /local/file.txt /hdfs/path/file.txt

# 从标准输入追加内容
echo "new line data" | hdfs dfs -appendToFile - /hdfs/path/file.txt

# ============================================================
# 11. 测试文件是否存在
# ============================================================
# 如果文件存在返回0,否则返回1
hdfs dfs -test -e /hdfs/path/file.txt
echo $?  # 输出0表示存在,1表示不存在

# 测试是否为目录
hdfs dfs -test -d /hdfs/path/

# 测试是否为文件
hdfs dfs -test -f /hdfs/path/file.txt

# ============================================================
# 12. 集群管理命令(dfsadmin)
# ============================================================
# 查看HDFS基本状态报告
hdfs dfsadmin -report

# 查看某个节点的状态
hdfs dfsadmin -report -live

# 刷新节点列表(动态添加/移除DataNode)
hdfs dfsadmin -refreshNodes

# 设置某个目录的配额(最多N个文件/目录)
hdfs dfsadmin -setQuota 1000 /user/hadoop/data

# 设置空间配额(最多10GB)
hdfs dfsadmin -setSpaceQuota 10g /user/hadoop/data

# 清除配额
hdfs dfsadmin -clrQuota /user/hadoop/data
hdfs dfsadmin -clrSpaceQuota /user/hadoop/data

7.3 Shell 操作完整案例

bash 复制代码
#!/bin/bash
# ============================================================
# 案例:HDFS Shell 综合操作演示
# 功能:演示HDFS常用文件操作
# ============================================================

# ---------- 步骤1:创建实验目录 ----------
echo "===== 创建HDFS目录 ====="
hdfs dfs -mkdir -p /chapter3/demo/input    # 创建多级目录
hdfs dfs -mkdir -p /chapter3/demo/output   # 创建输出目录

# ---------- 步骤2:准备本地测试数据 ----------
echo "===== 准备本地测试数据 ====="
# 创建本地临时目录
mkdir -p /tmp/hdfs_test

# 生成测试文件
echo "Hello HDFS" > /tmp/hdfs_test/test1.txt           # 创建文件1
echo "Hello Hadoop" > /tmp/hdfs_test/test2.txt         # 创建文件2
echo "HDFS is a distributed file system" > /tmp/hdfs_test/test3.txt  # 创建文件3

# ---------- 步骤3:上传文件到HDFS ----------
echo "===== 上传文件到HDFS ====="
hdfs dfs -put /tmp/hdfs_test/test1.txt /chapter3/demo/input/
hdfs dfs -put /tmp/hdfs_test/test2.txt /chapter3/demo/input/
hdfs dfs -put /tmp/hdfs_test/test3.txt /chapter3/demo/input/

# ---------- 步骤4:查看上传的文件 ----------
echo "===== 查看HDFS文件列表 ====="
hdfs dfs -ls /chapter3/demo/input/

# ---------- 步骤5:查看文件内容 ----------
echo "===== 查看文件内容 ====="
hdfs dfs -cat /chapter3/demo/input/test1.txt   # 查看文件1
hdfs dfs -cat /chapter3/demo/input/test3.txt   # 查看文件3

# ---------- 步骤6:文件复制和移动 ----------
echo "===== 复制和移动文件 ====="
hdfs dfs -cp /chapter3/demo/input/test1.txt /chapter3/demo/input/test1_backup.txt  # 复制
hdfs dfs -mv /chapter3/demo/input/test2.txt /chapter3/demo/output/test2_moved.txt  # 移动

# ---------- 步骤7:查看目录大小 ----------
echo "===== 查看目录大小 ====="
hdfs dfs -du -h /chapter3/demo/

# ---------- 步骤8:追加内容到文件 ----------
echo "===== 追加文件内容 ====="
echo "New appended line" | hdfs dfs -appendToFile - /chapter3/demo/input/test1.txt
hdfs dfs -cat /chapter3/demo/input/test1.txt   # 查看追加后的结果

# ---------- 步骤9:下载文件到本地 ----------
echo "===== 下载文件到本地 ====="
hdfs dfs -get /chapter3/demo/input/test3.txt /tmp/hdfs_test/downloaded_test3.txt
cat /tmp/hdfs_test/downloaded_test3.txt        # 查看下载的文件

# ---------- 步骤10:修改文件权限 ----------
echo "===== 修改文件权限 ====="
hdfs dfs -chmod 777 /chapter3/demo/input/test1.txt     # 设置权限
hdfs dfs -ls /chapter3/demo/input/test1.txt             # 查看权限变更

# ---------- 步骤11:删除文件和目录 ----------
echo "===== 删除操作 ====="
hdfs dfs -rm /chapter3/demo/input/test1_backup.txt       # 删除文件
hdfs dfs -rm -r /chapter3/demo/output/                    # 递归删除目录

# ---------- 步骤12:查看集群状态 ----------
echo "===== 集群状态 ====="
hdfs dfs -df -h     # 查看HDFS总容量和可用空间

# ---------- 清理 ----------
echo "===== 清理实验数据 ====="
hdfs dfs -rm -r /chapter3/
rm -rf /tmp/hdfs_test/

echo "===== 实验完成 ====="

八、案例------通过 Shell 脚本定时采集数据到 HDFS

8.1 案例背景

实际生产环境中,经常需要将服务器上的日志文件定期采集到 HDFS 中进行大数据分析。

8.2 完整案例代码

bash 复制代码
#!/bin/bash
# ============================================================
# 案例:定时采集Web服务器日志到HDFS
# 
# 功能描述:
#   1. 每天定时将前一天的Web访问日志上传到HDFS
#   2. 在HDFS上按日期建立目录结构(/logs/web/yyyy/MM/dd/)
#   3. 记录采集日志,支持断点续传
#   4. 自动清理N天前的本地日志文件
#
# 使用方式:
#   手动执行:./collect_log_to_hdfs.sh
#   定时执行:crontab -e 添加如下行:
#   0 2 * * * /home/hadoop/scripts/collect_log_to_hdfs.sh >> /var/log/collect_hdfs.log 2>&1
# ============================================================

# ==================== 配置区 ====================
# 本地日志文件所在目录
LOCAL_LOG_DIR="/var/log/nginx"

# HDFS上日志存放的根目录
HDFS_LOG_BASE_DIR="/logs/web"

# 日志文件名前缀(如 access_log)
LOG_FILE_PREFIX="access_log"

# 需要保留的本地日志天数(超过此天数的本地日志将被删除)
LOCAL_LOG_RETENTION_DAYS=7

# 采集状态记录文件(记录已成功上传的文件,防止重复上传)
COLLECT_RECORD_FILE="/home/hadoop/scripts/.collect_record.log"

# 采集日志文件
SCRIPT_LOG_FILE="/var/log/collect_hdfs.log"

# ==================== 函数定义 ====================

# 日志输出函数:在控制台和日志文件中同时输出带时间戳的信息
# 参数:$1 - 日志级别(INFO/WARN/ERROR),$2 - 日志内容
log_info() {
    local level=$1      # 日志级别
    local message=$2    # 日志内容
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')  # 获取当前时间戳
    # 输出格式:[2024-01-15 02:00:01] [INFO] 日志内容
    echo "[$timestamp] [$level] $message"
}

# 检查HDFS目录是否存在,不存在则创建
# 参数:$1 - HDFS目录路径
check_and_create_hdfs_dir() {
    local hdfs_dir=$1   # 要检查的HDFS目录
    # 使用 test -d 判断HDFS目录是否存在
    hdfs dfs -test -d "$hdfs_dir"
    # $? 为上一条命令的返回值,0表示存在,非0表示不存在
    if [ $? -ne 0 ]; then
        # 目录不存在,递归创建
        hdfs dfs -mkdir -p "$hdfs_dir"
        log_info "INFO" "创建HDFS目录: $hdfs_dir"
    fi
}

# 检查文件是否已上传过(防止重复上传)
# 参数:$1 - 文件名
# 返回值:0-已上传过,1-未上传过
is_file_uploaded() {
    local filename=$1   # 要检查的文件名
    # 在记录文件中搜索该文件名
    if grep -q "$filename" "$COLLECT_RECORD_FILE" 2>/dev/null; then
        return 0   # 已上传过
    else
        return 1   # 未上传过
    fi
}

# 记录已上传的文件
# 参数:$1 - 文件名
record_uploaded_file() {
    local filename=$1   # 已上传的文件名
    # 将文件名追加到记录文件中
    echo "$filename" >> "$COLLECT_RECORD_FILE"
}

# ==================== 主逻辑 ====================
log_info "INFO" "========== 日志采集任务开始 =========="

# 计算前一天的日期(用于获取前一天的日志文件)
# 格式:YESTERDAY=2024-01-14, YEAR=2024, MONTH=01, DAY=14
YESTERDAY=$(date -d '-1 day' '+%Y-%m-%d')
YEAR=$(date -d '-1 day' '+%Y')
MONTH=$(date -d '-1 day' '+%m')
DAY=$(date -d '-1 day' '+%d')

log_info "INFO" "采集日期: $YESTERDAY (年:$YEAR 月:$MONTH 日:$DAY)"

# ---------- 步骤1:确认HDFS目标目录存在 ----------
# HDFS目录结构:/logs/web/2024/01/14/
HDFS_TARGET_DIR="${HDFS_LOG_BASE_DIR}/${YEAR}/${MONTH}/${DAY}"
check_and_create_hdfs_dir "$HDFS_TARGET_DIR"

# ---------- 步骤2:遍历本地日志文件并上传 ----------
# 构造日志文件名模式:如 access_log-20240114 或 access_log.2024-01-14
# 这里支持多种常见的日志文件命名格式
LOG_PATTERNS=(
    "${LOG_FILE_PREFIX}-${YEAR}${MONTH}${DAY}"          # access_log-20240114
    "${LOG_FILE_PREFIX}.${YEAR}-${MONTH}-${DAY}"         # access_log.2024-01-14
    "${LOG_FILE_PREFIX}.${YEAR}${MONTH}${DAY}"            # access_log.20240114
)

UPLOAD_COUNT=0      # 成功上传的文件计数
SKIP_COUNT=0        # 跳过的文件计数
FAIL_COUNT=0        # 上传失败的文件计数

# 遍历每种可能的日志文件名模式
for pattern in "${LOG_PATTERNS[@]}"; do
    LOG_FILE="${LOCAL_LOG_DIR}/${pattern}"   # 拼接完整文件路径

    # 检查文件是否存在(-f 判断是否为普通文件)
    if [ -f "$LOG_FILE" ]; then
        log_info "INFO" "发现日志文件: $LOG_FILE"

        # 检查文件是否已经上传过
        if is_file_uploaded "$pattern"; then
            log_info "WARN" "文件已上传过,跳过: $pattern"
            SKIP_COUNT=$((SKIP_COUNT + 1))   # 跳过计数+1
            continue                          # 跳过本次循环
        fi

        # 获取文件大小(字节数)
        FILE_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo "0")
        log_info "INFO" "文件大小: $FILE_SIZE 字节"

        # 检查文件是否为空
        if [ "$FILE_SIZE" -eq 0 ]; then
            log_info "WARN" "文件为空,跳过: $LOG_FILE"
            SKIP_COUNT=$((SKIP_COUNT + 1))
            continue
        fi

        # ---------- 执行上传操作 ----------
        # -f 参数表示如果HDFS上已有同名文件则覆盖
        hdfs dfs -put -f "$LOG_FILE" "${HDFS_TARGET_DIR}/"
        PUT_RESULT=$?    # 获取上传命令的返回码

        # 判断上传是否成功
        if [ $PUT_RESULT -eq 0 ]; then
            log_info "INFO" "上传成功: $LOG_FILE -> ${HDFS_TARGET_DIR}/"
            record_uploaded_file "$pattern"   # 记录已上传
            UPLOAD_COUNT=$((UPLOAD_COUNT + 1))  # 成功计数+1

            # 验证HDFS上的文件大小是否与本地一致
            HDFS_FILE_SIZE=$(hdfs dfs -du -s "${HDFS_TARGET_DIR}/${pattern}" 2>/dev/null | awk '{print $1}')
            if [ "$HDFS_FILE_SIZE" -eq "$FILE_SIZE" ]; then
                log_info "INFO" "文件大小验证通过: 本地=${FILE_SIZE}, HDFS=${HDFS_FILE_SIZE}"
            else
                log_info "WARN" "文件大小不一致! 本地=${FILE_SIZE}, HDFS=${HDFS_FILE_SIZE}"
            fi
        else
            log_info "ERROR" "上传失败: $LOG_FILE (返回码: $PUT_RESULT)"
            FAIL_COUNT=$((FAIL_COUNT + 1))    # 失败计数+1
        fi
    fi
done

# ---------- 步骤3:清理过期的本地日志文件 ----------
log_info "INFO" "清理${LOCAL_LOG_RETENTION_DAYS}天前的本地日志..."
# find 命令查找修改时间超过指定天数的文件并删除
# -mtime +N: 修改时间在N天之前的文件
# -type f: 只查找普通文件
# -name "${LOG_FILE_PREFIX}*": 文件名匹配日志前缀
DELETED_COUNT=$(find "$LOCAL_LOG_DIR" -mtime +${LOCAL_LOG_RETENTION_DAYS} \
    -type f -name "${LOG_FILE_PREFIX}*" -delete -print 2>/dev/null | wc -l)
log_info "INFO" "已清理 ${DELETED_COUNT} 个过期日志文件"

# ---------- 步骤4:输出采集报告 ----------
log_info "INFO" "---------- 采集报告 ----------"
log_info "INFO" "成功上传: ${UPLOAD_COUNT} 个文件"
log_info "INFO" "已跳过:   ${SKIP_COUNT} 个文件"
log_info "INFO" "上传失败: ${FAIL_COUNT} 个文件"
log_info "INFO" "HDFS目标: ${HDFS_TARGET_DIR}"
log_info "INFO" "========== 日志采集任务结束 =========="

# 如果有失败的文件,以非0状态码退出(方便监控系统检测)
if [ $FAIL_COUNT -gt 0 ]; then
    exit 1    # 有失败,返回错误码1
fi

exit 0   # 全部成功,返回0

8.3 配置 Crontab 定时任务

bash 复制代码
# ============================================================
# 配置 crontab 定时执行采集脚本
# ============================================================

# 编辑当前用户的 crontab
crontab -e

# 添加以下行:
# 每天凌晨 2:00 执行日志采集脚本
0 2 * * * /home/hadoop/scripts/collect_log_to_hdfs.sh >> /var/log/collect_hdfs.log 2>&1

# crontab 时间格式说明:
# ┌────────── 分钟 (0-59)
# │ ┌──────── 小时 (0-23)
# │ │ ┌────── 日 (1-31)
# │ │ │ ┌──── 月 (1-12)
# │ │ │ │ ┌── 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# 0 2 * * *

# 其他示例:
# 每小时执行一次
# 0 * * * * /home/hadoop/scripts/collect_log_to_hdfs.sh

# 每隔5分钟执行一次
# */5 * * * * /home/hadoop/scripts/collect_log_to_hdfs.sh

# 查看已有的 crontab 任务
crontab -l

九、HDFS 的 Java API 操作

9.1 HDFS Java API 介绍

HDFS 提供了丰富的 Java API 用于程序化操作文件系统。核心类:

类名 作用
Configuration Hadoop 配置类,用于加载 core-site.xmlhdfs-site.xml 等配置
FileSystem HDFS 文件系统的抽象类,是操作HDFS的入口
Path 表示HDFS中的文件/目录路径
FSDataInputStream HDFS 文件输入流,用于读取文件
FSDataOutputStream HDFS 文件输出流,用于写入文件
FileStatus 文件/目录的元数据信息(权限、大小、修改时间等)
BlockLocation 数据块的位置信息(存储在哪些DataNode上)
RemoteIterator 远程迭代器,用于遍历大量文件/目录
LocatedFileStatus 包含块位置信息的文件状态

9.2 Maven 依赖配置

xml 复制代码
<!-- pom.xml 中需要添加以下依赖 -->

<properties>
    <!-- 统一管理Hadoop版本 -->
    <hadoop.version>3.3.6</hadoop.version>
</properties>

<dependencies>
    <!-- Hadoop 客户端核心依赖 -->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>${hadoop.version}</version>
    </dependency>

    <!-- HDFS 客户端依赖 -->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs-client</artifactId>
        <version>${hadoop.version}</version>
    </dependency>

    <!-- Hadoop Common 工具类 -->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-common</artifactId>
        <version>${hadoop.version}</version>
    </dependency>

    <!-- 日志依赖(避免日志冲突警告) -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.30</version>
    </dependency>
</dependencies>

十、案例------使用 Java API 操作 HDFS

10.1 案例一:获取 FileSystem 连接并创建目录

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.BasicConfigurator;

import java.io.IOException;
import java.net.URI;

/**
 * 案例1:获取HDFS连接并创建目录
 * 
 * 功能:
 *   1. 创建 Hadoop Configuration 对象
 *   2. 获取 FileSystem 实例(连接HDFS)
 *   3. 在HDFS上创建多级目录
 *   4. 释放资源
 */
public class HDFSMkdirDemo {

    public static void main(String[] args) {
        // ======== 步骤1:配置日志(避免控制台警告) ========
        BasicConfigurator.configure();

        FileSystem fs = null;   // 声明FileSystem对象,用于操作HDFS

        try {
            // ======== 步骤2:创建Hadoop配置对象 ========
            Configuration conf = new Configuration();
            // 设置HDFS的NameNode地址(根据实际集群地址修改)
            // "hdfs://node1:9000" 是NameNode的RPC通信地址
            conf.set("fs.defaultFS", "hdfs://node1:9000");

            // 设置HDFS副本数(可选,会覆盖hdfs-site.xml中的配置)
            conf.set("dfs.replication", "3");

            // ======== 步骤3:获取FileSystem实例 ========
            // 方式1:通过URI和配置获取(推荐)
            // 参数说明:
            //   URI: HDFS的NameNode地址
            //   conf: Hadoop配置
            //   "hadoop": HDFS的操作用户(需要与集群配置的用户名一致)
            fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");

            // 方式2(替代):直接通过配置获取(使用默认用户)
            // fs = FileSystem.get(conf);

            // ======== 步骤4:创建目录 ========
            // 定义要创建的目录路径
            Path dirPath = new Path("/chapter3/java_api/demos");

            // mkdirs() 会递归创建所有不存在的父目录(类似 mkdir -p)
            // 返回值: true 表示目录创建成功或已存在,false 表示失败
            boolean result = fs.mkdirs(dirPath);

            // 输出创建结果
            if (result) {
                System.out.println("目录创建成功: " + dirPath);
            } else {
                System.out.println("目录创建失败: " + dirPath);
            }

            // 再创建一个目录
            Path dirPath2 = new Path("/chapter3/java_api/data");
            fs.mkdirs(dirPath2);
            System.out.println("目录创建成功: " + dirPath2);

        } catch (IOException e) {
            // 捕获IO异常(如网络连接失败、权限不足等)
            System.err.println("HDFS操作异常: " + e.getMessage());
            e.printStackTrace();
        } catch (InterruptedException e) {
            // 捕获中断异常
            System.err.println("操作被中断: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            // 捕获其他异常(如URI解析异常)
            System.err.println("未知异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // ======== 步骤5:释放FileSystem资源 ========
            // 无论是否发生异常,都要关闭FileSystem连接
            if (fs != null) {
                try {
                    fs.close();   // 关闭文件系统连接,释放网络资源
                    System.out.println("FileSystem连接已关闭");
                } catch (IOException e) {
                    System.err.println("关闭FileSystem异常: " + e.getMessage());
                }
            }
        }
    }
}

10.2 案例二:上传和下载文件

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.BasicConfigurator;

import java.io.IOException;
import java.net.URI;

/**
 * 案例2:上传和下载文件
 * 
 * 功能:
 *   1. 将本地文件上传到HDFS
 *   2. 将HDFS文件下载到本地
 *   3. 覆盖已有文件
 */
public class HDFSUploadDownloadDemo {

    public static void main(String[] args) {
        BasicConfigurator.configure();  // 配置日志

        FileSystem fs = null;   // HDFS文件系统对象

        try {
            // ======== 初始化配置和连接 ========
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://node1:9000");

            // 获取FileSystem实例(以hadoop用户身份连接)
            fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");

            // ======== 上传文件 ========
            // 定义本地源文件路径
            Path localSrc = new Path("/tmp/test_upload.txt");
            // 定义HDFS目标路径
            Path hdfsDst = new Path("/chapter3/java_api/data/uploaded.txt");

            // 方式1: copyFromLocalFile() - 从本地复制文件到HDFS
            // 参数: delSrc=false 表示不删除本地源文件
            //       overwrite=true 表示覆盖HDFS上已存在的同名文件
            //       src=本地路径, dst=HDFS目标路径
            fs.copyFromLocalFile(false, true, localSrc, hdfsDst);
            System.out.println("文件上传成功: " + localSrc + " -> " + hdfsDst);

            // 方式2(替代): 使用文件流上传(适用于需要更精细控制的场景)
            // FSDataOutputStream out = fs.create(new Path("/chapter3/java_api/data/stream_upload.txt"));
            // FileInputStream in = new FileInputStream("/tmp/test_upload.txt");
            // IOUtils.copyBytes(in, out, 4096, true);  // 4096为缓冲区大小, true=关闭流
            // 说明: IOUtils 是 Hadoop 提供的工具类,简化流操作

            // ======== 下载文件 ========
            // 定义HDFS源文件路径
            Path hdfsSrc = new Path("/chapter3/java_api/data/uploaded.txt");
            // 定义本地目标路径
            Path localDst = new Path("/tmp/test_download.txt");

            // 方式1: copyToLocalFile() - 从HDFS复制文件到本地
            // 参数: delSrc=false 表示不删除HDFS源文件
            //       src=HDFS路径, dst=本地路径
            //       useRawLocalFileSystem=false 使用校验和验证文件完整性
            fs.copyToLocalFile(false, hdfsSrc, localDst, false);
            System.out.println("文件下载成功: " + hdfsSrc + " -> " + localDst);

            // ======== 使用FileUtil进行文件复制(替代方案) ========
            // FileUtil.copy(fs, hdfsSrc, new FileSystem("file:///", conf), 
            //               new Path("/tmp/copy_test.txt"), false, conf);
            // 说明: FileUtil 提供了在不同FileSystem之间复制文件的通用方法

        } catch (IOException e) {
            System.err.println("IO异常: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 释放资源
            if (fs != null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10.3 案例三:读取文件内容并写入文件

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.log4j.BasicConfigurator;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;

/**
 * 案例3:读取和写入HDFS文件内容
 * 
 * 功能:
 *   1. 向HDFS写入文本文件
 *   2. 读取HDFS文件(字节流方式)
 *   3. 逐行读取HDFS文件(字符流方式)
 */
public class HDFSReadWriteDemo {

    public static void main(String[] args) {
        BasicConfigurator.configure();

        FileSystem fs = null;

        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://node1:9000");
            fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");

            // ======== 写入文件到HDFS ========
            Path writePath = new Path("/chapter3/java_api/data/written_file.txt");

            // create() 创建文件并返回输出流
            // 如果文件已存在,默认覆盖(可通过参数控制不覆盖)
            FSDataOutputStream out = fs.create(writePath);
            // true: 覆盖已有文件
            // FSDataOutputStream out = fs.create(writePath, true);

            // 写入多行文本数据
            String[] lines = {
                "Line 1: Hello HDFS!",
                "Line 2: Hadoop Distributed File System",
                "Line 3: 这是通过Java API写入的内容",
                "Line 4: HDFS supports large-scale data storage",
                "Line 5: 写入完成"
            };

            for (String line : lines) {
                // writeBytes() 将字符串以字节方式写入
                out.writeBytes(line);
                out.writeBytes("\n");   // 手动写入换行符
            }

            out.hsync();  // 强制将数据刷到磁盘(比flush更强,包含元数据同步)
            out.close();  // 关闭输出流(会自动flush)
            System.out.println("文件写入成功: " + writePath);

            // ======== 方式1:字节流方式读取整个文件 ========
            System.out.println("\n===== 字节流方式读取 =====");
            Path readPath = new Path("/chapter3/java_api/data/written_file.txt");

            // open() 打开文件并返回输入流
            FSDataInputStream in = fs.open(readPath);

            // 使用Hadoop的IOUtils工具类将流内容输出到控制台
            // 参数: in=输入流, out=输出流(System.out), bufferSize=4096, closeAfter=true
            IOUtils.copyBytes(in, System.out, 4096, true);
            // 注意: copyBytes的第4个参数为true会自动关闭输入流

            // ======== 方式2:字符流方式逐行读取 ========
            System.out.println("\n===== 逐行读取方式 =====");
            FSDataInputStream lineIn = fs.open(readPath);

            // 包装为字符缓冲流,支持逐行读取
            // InputStreamReader: 将字节流转换为字符流,指定UTF-8编码
            // BufferedReader: 提供缓冲,支持readLine()逐行读取
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(lineIn, StandardCharsets.UTF_8)
            );

            String line;         // 存储当前读取的行
            int lineNumber = 0;  // 行号计数器

            // readLine() 每次读取一行文本(不包含换行符)
            // 当读取到文件末尾时返回 null
            while ((line = reader.readLine()) != null) {
                lineNumber++;
                System.out.println("第" + lineNumber + "行: " + line);
            }

            reader.close();   // 关闭字符流(会级联关闭底层的FSDataInputStream)

            // ======== 方式3:指定偏移量读取(读取文件的一部分) ========
            System.out.println("\n===== 指定偏移量读取 =====");
            FSDataInputStream seekIn = fs.open(readPath);

            byte[] buffer = new byte[64];   // 创建64字节的缓冲区
            seekIn.seek(10);   // 跳过前10个字节,从第11个字节开始读取

            // read(buffer) 将数据读入缓冲区,返回实际读取的字节数
            int bytesRead = seekIn.read(buffer);
            String partialContent = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
            System.out.println("从偏移量10开始读取的内容: " + partialContent);
            System.out.println("读取了 " + bytesRead + " 字节");

            seekIn.close();   // 关闭流

        } catch (IOException e) {
            System.err.println("IO异常: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            if (fs != null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10.4 案例四:遍历目录与文件信息查询

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.log4j.BasicConfigurator;

import java.io.IOException;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 案例4:遍历HDFS目录和查询文件元数据
 * 
 * 功能:
 *   1. 列出目录下的文件和子目录
 *   2. 递归遍历所有文件
 *   3. 获取文件的详细元数据(大小、权限、修改时间、块位置等)
 *   4. 查找特定类型的文件
 */
public class HDFSFileStatusDemo {

    public static void main(String[] args) {
        BasicConfigurator.configure();

        FileSystem fs = null;

        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://node1:9000");
            fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");

            // ======== 1. 获取单个文件/目录的状态信息 ========
            System.out.println("===== 1. 文件状态信息 =====");
            Path filePath = new Path("/chapter3/java_api/data/written_file.txt");

            // getFileStatus() 返回文件/目录的元数据
            FileStatus status = fs.getFileStatus(filePath);

            // 输出文件的各项元数据
            System.out.println("文件路径: " + status.getPath());           // 完整HDFS路径
            System.out.println("文件名: " + status.getPath().getName());   // 文件名
            System.out.println("文件大小: " + status.getLen() + " 字节"); // 文件大小(字节)
            System.out.println("副本数: " + status.getReplication());     // 副本数量
            System.out.println("块大小: " + status.getBlockSize() + " 字节"); // 块大小

            // 权限信息
            FsPermission permission = status.getPermission();
            System.out.println("权限: " + permission);                    // 如: rwxr-xr-x
            System.out.println("所有者: " + status.getOwner());           // 文件所有者
            System.out.println("所属组: " + status.getGroup());           // 所属用户组

            // 修改时间(毫秒时间戳转换为可读格式)
            long modificationTime = status.getModificationTime();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String modTime = sdf.format(new Date(modificationTime));
            System.out.println("修改时间: " + modTime);

            // 判断是文件还是目录
            System.out.println("是否为目录: " + status.isDirectory());     // true/false
            System.out.println("是否为文件: " + status.isFile());         // true/false
            System.out.println("是否为符号链接: " + status.isSymlink()); // true/false

            // ======== 2. 列出目录下的文件和子目录 ========
            System.out.println("\n===== 2. 列出目录内容 =====");
            Path dirPath = new Path("/chapter3/java_api/");

            // listStatus() 返回目录下所有文件和子目录的FileStatus数组
            FileStatus[] fileStatuses = fs.listStatus(dirPath);

            System.out.println("目录 " + dirPath + " 下共有 " + fileStatuses.length + " 个条目:");
            for (FileStatus fileStatus : fileStatuses) {
                // 判断是目录还是文件,用不同标记显示
                String type = fileStatus.isDirectory() ? "[目录]" : "[文件]";
                String size = fileStatus.isDirectory() ? "-" : fileStatus.getLen() + " 字节";
                System.out.println("  " + type + " " + fileStatus.getPath().getName() + " (" + size + ")");
            }

            // ======== 3. 递归遍历所有文件(使用FileSystem.listFiles) ========
            System.out.println("\n===== 3. 递归遍历所有文件 =====");
            Path rootPath = new Path("/chapter3/java_api/");

            // listFiles() 返回一个RemoteIterator(远程迭代器),递归遍历所有文件
            // 参数: rootPath=起始目录, recursive=true表示递归子目录
            RemoteIterator<LocatedFileStatus> fileIterator = fs.listFiles(rootPath, true);

            int fileCount = 0;  // 文件计数器
            long totalSize = 0; // 总大小计数器

            // RemoteIterator 实现了迭代器接口,使用 hasNext()/next() 遍历
            while (fileIterator.hasNext()) {
                LocatedFileStatus locatedFile = fileIterator.next();  // 获取下一个文件
                fileCount++;

                System.out.println("\n  文件 #" + fileCount);
                System.out.println("  路径: " + locatedFile.getPath());
                System.out.println("  大小: " + locatedFile.getLen() + " 字节");
                totalSize += locatedFile.getLen();

                // 获取数据块的位置信息
                // LocatedFileStatus 继承了FileStatus,额外包含块位置信息
                BlockLocation[] blockLocations = locatedFile.getBlockLocations();
                System.out.println("  块数量: " + blockLocations.length);

                // 遍历每个数据块,输出其存储位置
                for (int i = 0; i < blockLocations.length; i++) {
                    BlockLocation block = blockLocations[i];

                    // getHosts() 返回存储该块的DataNode主机名
                    String[] hosts = block.getHosts();
                    // getNames() 返回存储该块的DataNode的IP:端口
                    String[] names = block.getNames();

                    System.out.println("    块" + i + ": 长度=" + block.getLength()
                        + ", 偏移=" + block.getOffset()
                        + ", DataNode=" + String.join(", ", hosts));
                }
            }

            System.out.println("\n共 " + fileCount + " 个文件, 总大小: " + totalSize + " 字节");

            // ======== 4. 使用globStatus()按模式查找文件 ========
            System.out.println("\n===== 4. 按模式匹配查找文件 =====");

            // globStatus() 支持通配符匹配
            // * 匹配任意字符序列
            // ? 匹配任意单个字符
            // [abc] 匹配a、b或c中的一个
            // [a-z] 匹配a到z之间的任意字符

            // 查找所有 .txt 文件
            Path globPath = new Path("/chapter3/java_api/**/*.txt");
            FileStatus[] matchedFiles = fs.globStatus(globPath);
            System.out.println("匹配 *.txt 的文件:");
            for (FileStatus matched : matchedFiles) {
                System.out.println("  " + matched.getPath() + " (" + matched.getLen() + " 字节)");
            }

            // ======== 5. 使用listLocatedStatus()查看目录下文件及其块位置 ========
            System.out.println("\n===== 5. 目录下文件的块位置信息 =====");
            Path dataPath = new Path("/chapter3/java_api/data/");
            RemoteIterator<LocatedFileStatus> locatedFiles = fs.listLocatedStatus(dataPath);

            while (locatedFiles.hasNext()) {
                LocatedFileStatus lfs = locatedFiles.next();
                System.out.println("文件: " + lfs.getPath().getName());

                BlockLocation[] blocks = lfs.getBlockLocations();
                for (int i = 0; i < blocks.length; i++) {
                    System.out.println("  块" + i + " 存储在: "
                        + String.join(", ", blocks[i].getHosts()));
                }
            }

        } catch (IOException e) {
            System.err.println("IO异常: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            if (fs != null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10.5 案例五:删除和重命名文件

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.BasicConfigurator;

import java.io.IOException;
import java.net.URI;

/**
 * 案例5:删除和重命名HDFS文件/目录
 * 
 * 功能:
 *   1. 重命名文件
 *   2. 删除文件
 *   3. 递归删除目录
 *   4. 移动文件(本质是重命名到不同路径)
 */
public class HDFSDeleteRenameDemo {

    public static void main(String[] args) {
        BasicConfigurator.configure();

        FileSystem fs = null;

        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://node1:9000");
            fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");

            // ======== 1. 重命名文件 ========
            System.out.println("===== 1. 重命名文件 =====");
            Path oldPath = new Path("/chapter3/java_api/data/written_file.txt");
            Path newPath = new Path("/chapter3/java_api/data/renamed_file.txt");

            // rename() 重命名文件或移动文件
            // 返回值: true=成功, false=失败
            boolean renameResult = fs.rename(oldPath, newPath);
            System.out.println("重命名结果: " + (renameResult ? "成功" : "失败"));
            System.out.println(oldPath + " -> " + newPath);

            // ======== 2. 移动文件(rename到不同目录) ========
            System.out.println("\n===== 2. 移动文件 =====");
            Path srcMove = new Path("/chapter3/java_api/data/renamed_file.txt");
            Path dstMove = new Path("/chapter3/java_api/demos/moved_file.txt");

            // rename也可以用于移动文件到不同目录
            boolean moveResult = fs.rename(srcMove, dstMove);
            System.out.println("移动结果: " + (moveResult ? "成功" : "失败"));

            // ======== 3. 删除单个文件 ========
            System.out.println("\n===== 3. 删除文件 =====");
            Path deletePath = new Path("/chapter3/java_api/demos/moved_file.txt");

            // delete() 删除文件或目录
            // 参数: path=要删除的路径, recursive=是否递归删除
            //       对于文件,recursive参数无影响
            //       对于目录,recursive=true表示删除目录及其所有内容
            //       recursive=false表示仅在目录为空时才删除
            boolean deleteResult = fs.delete(deletePath, false);
            System.out.println("删除文件结果: " + (deleteResult ? "成功" : "失败"));

            // ======== 4. 创建测试目录结构用于演示目录删除 ========
            System.out.println("\n===== 4. 创建测试目录结构 =====");
            // 创建多层目录和文件用于测试删除
            fs.mkdirs(new Path("/chapter3/java_api/to_delete/subdir1"));
            fs.mkdirs(new Path("/chapter3/java_api/to_delete/subdir2"));

            // 在目录下创建测试文件
            Path testFile1 = new Path("/chapter3/java_api/to_delete/file1.txt");
            Path testFile2 = new Path("/chapter3/java_api/to_delete/subdir1/file2.txt");

            fs.create(testFile1).writeBytes("test data 1");
            fs.create(testFile2).writeBytes("test data 2");
            System.out.println("测试目录结构创建完成");

            // ======== 5. 非递归删除非空目录(会失败) ========
            System.out.println("\n===== 5. 尝试非递归删除非空目录 =====");
            Path nonEmptyDir = new Path("/chapter3/java_api/to_delete");
            boolean deleteNonRecursive = fs.delete(nonEmptyDir, false);
            // 非空目录使用recursive=false会失败
            System.out.println("非递归删除结果: " + (deleteNonRecursive ? "成功" : "失败(目录非空)"));

            // ======== 6. 递归删除目录 ========
            System.out.println("\n===== 6. 递归删除目录 =====");
            // 使用recursive=true删除目录及其所有子目录和文件
            boolean deleteRecursive = fs.delete(nonEmptyDir, true);
            System.out.println("递归删除结果: " + (deleteRecursive ? "成功" : "失败"));

        } catch (IOException e) {
            System.err.println("IO异常: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            if (fs != null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10.6 案例六:综合案例------HDFS 文件管理工具

java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.log4j.BasicConfigurator;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 综合案例:HDFS 文件管理工具
 * 
 * 功能:封装常用HDFS操作为方法,方便复用
 *   1. uploadFile() - 上传文件
 *   2. downloadFile() - 下载文件
 *   3. readFile() - 读取文件内容
 *   4. writeFile() - 写入文件内容
 *   5. listFiles() - 列出文件
 *   6. deleteFile() - 删除文件
 *   7. isExist() - 判断文件是否存在
 *   8. getDiskUsage() - 获取磁盘使用情况
 */
public class HDFSFileUtil {

    private FileSystem fs;   // HDFS文件系统实例
    private static final SimpleDateFormat DATE_FORMAT = 
        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  // 日期格式化工具

    /**
     * 构造方法:初始化HDFS连接
     * 
     * @param hdfsUri HDFS NameNode地址,如 hdfs://node1:9000
     * @param user    HDFS操作用户名
     * @throws Exception 初始化失败时抛出异常
     */
    public HDFSFileUtil(String hdfsUri, String user) throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", hdfsUri);
        conf.set("dfs.replication", "3");            // 默认副本数
        conf.set("dfs.client.use.datanode.hostname", "true");  // 使用主机名(跨网段时需要)
        this.fs = FileSystem.get(new URI(hdfsUri), conf, user);
    }

    /**
     * 上传本地文件到HDFS
     * 
     * @param localPath   本地文件路径
     * @param hdfsPath    HDFS目标路径
     * @param deleteSrc   是否删除本地源文件
     * @param overwrite   是否覆盖HDFS已有的同名文件
     * @return 是否上传成功
     */
    public boolean uploadFile(String localPath, String hdfsPath, 
                              boolean deleteSrc, boolean overwrite) {
        try {
            Path src = new Path(localPath);   // 创建本地路径对象
            Path dst = new Path(hdfsPath);    // 创建HDFS路径对象

            // copyFromLocalFile(delSrc, overwrite, src, dst)
            // delSrc: 是否删除本地源文件
            // overwrite: 是否覆盖目标已存在文件
            fs.copyFromLocalFile(deleteSrc, overwrite, src, dst);
            System.out.println("[上传成功] " + localPath + " -> " + hdfsPath);
            return true;
        } catch (IOException e) {
            System.err.println("[上传失败] " + e.getMessage());
            return false;
        }
    }

    /**
     * 从HDFS下载文件到本地
     * 
     * @param hdfsPath    HDFS文件路径
     * @param localPath   本地目标路径
     * @return 是否下载成功
     */
    public boolean downloadFile(String hdfsPath, String localPath) {
        try {
            Path src = new Path(hdfsPath);
            Path dst = new Path(localPath);
            // 从HDFS下载文件到本地
            // 参数: delSrc=false(不删除HDFS源文件), src, dst, useRawLocalFS=false(使用校验和)
            fs.copyToLocalFile(false, src, dst, false);
            System.out.println("[下载成功] " + hdfsPath + " -> " + localPath);
            return true;
        } catch (IOException e) {
            System.err.println("[下载失败] " + e.getMessage());
            return false;
        }
    }

    /**
     * 读取HDFS文件全部内容并返回字符串
     * 
     * @param hdfsPath HDFS文件路径
     * @return 文件内容字符串,读取失败返回null
     */
    public String readFile(String hdfsPath) {
        StringBuilder content = new StringBuilder();  // 用于拼接文件内容
        try {
            Path path = new Path(hdfsPath);
            // 检查文件是否存在
            if (!fs.exists(path)) {
                System.err.println("[读取失败] 文件不存在: " + hdfsPath);
                return null;
            }

            // 打开文件输入流
            FSDataInputStream in = fs.open(path);
            // 使用BufferedReader逐行读取
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(in, StandardCharsets.UTF_8)
            );

            String line;
            // 逐行读取文件内容
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");   // 拼接每行内容
            }
            reader.close();

            System.out.println("[读取成功] 文件: " + hdfsPath);
            return content.toString();
        } catch (IOException e) {
            System.err.println("[读取异常] " + e.getMessage());
            return null;
        }
    }

    /**
     * 向HDFS写入文件内容
     * 
     * @param hdfsPath  HDFS文件路径
     * @param content   要写入的内容
     * @param overwrite 是否覆盖已有文件
     * @return 是否写入成功
     */
    public boolean writeFile(String hdfsPath, String content, boolean overwrite) {
        try {
            Path path = new Path(hdfsPath);

            // create(path, overwrite) 创建文件
            // overwrite=true: 如果文件存在则覆盖
            FSDataOutputStream out = fs.create(path, overwrite);

            // 写入内容的字节数据
            out.write(content.getBytes(StandardCharsets.UTF_8));

            // hsync(): 将数据和元数据都同步到磁盘
            // flush(): 只将缓冲区数据发送到DataNode,不保证持久化
            out.hsync();
            out.close();

            System.out.println("[写入成功] 文件: " + hdfsPath + " (" + content.length() + " 字符)");
            return true;
        } catch (IOException e) {
            System.err.println("[写入失败] " + e.getMessage());
            return false;
        }
    }

    /**
     * 列出指定目录下的所有文件和子目录,以树形结构显示
     * 
     * @param dirPath    HDFS目录路径
     * @param recursive  是否递归列出子目录
     */
    public void listFiles(String dirPath, boolean recursive) {
        try {
            Path path = new Path(dirPath);

            // 检查路径是否存在
            if (!fs.exists(path)) {
                System.err.println("[路径不存在] " + dirPath);
                return;
            }

            System.out.println("\n目录: " + dirPath);
            System.out.println("类型\t\t大小\t\t副本\t修改时间\t\t\t\t文件名");
            System.out.println("----\t\t----\t\t---\t--------\t\t\t\t------");

            if (recursive) {
                // 递归列出所有文件
                RemoteIterator<LocatedFileStatus> iterator = fs.listFiles(path, true);
                while (iterator.hasNext()) {
                    LocatedFileStatus file = iterator.next();
                    printFileStatus(file);   // 打印文件信息
                }
            } else {
                // 只列出当前目录
                FileStatus[] statuses = fs.listStatus(path);
                for (FileStatus status : statuses) {
                    printFileStatus(status);   // 打印每个条目信息
                }
            }
        } catch (IOException e) {
            System.err.println("[列出文件失败] " + e.getMessage());
        }
    }

    /**
     * 打印文件状态信息的辅助方法
     * 
     * @param status FileStatus对象,包含文件元数据
     */
    private void printFileStatus(FileStatus status) {
        // 判断类型
        String type = status.isDirectory() ? "目录" : "文件";
        // 格式化大小
        String size = status.isDirectory() ? "-" : formatSize(status.getLen());
        // 副本数(目录无副本)
        String replication = status.isDirectory() ? "-" : String.valueOf(status.getReplication());
        // 格式化修改时间
        String modTime = DATE_FORMAT.format(new Date(status.getModificationTime()));

        System.out.printf("%-4s\t%-12s\t%s\t%-24s\t%s\n",
            type, size, replication, modTime, status.getPath().getName());
    }

    /**
     * 将字节数格式化为人类可读的大小表示
     * 
     * @param bytes 字节数
     * @return 格式化后的字符串(如 1.5 GB)
     */
    private String formatSize(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }

    /**
     * 删除HDFS文件或目录
     * 
     * @param hdfsPath  要删除的路径
     * @param recursive 是否递归删除(对于目录)
     * @return 是否删除成功
     */
    public boolean deleteFile(String hdfsPath, boolean recursive) {
        try {
            Path path = new Path(hdfsPath);
            if (!fs.exists(path)) {
                System.err.println("[删除失败] 路径不存在: " + hdfsPath);
                return false;
            }
            boolean result = fs.delete(path, recursive);
            System.out.println("[删除" + (result ? "成功" : "失败") + "] " + hdfsPath);
            return result;
        } catch (IOException e) {
            System.err.println("[删除异常] " + e.getMessage());
            return false;
        }
    }

    /**
     * 判断HDFS上文件或目录是否存在
     * 
     * @param hdfsPath HDFS路径
     * @return true=存在, false=不存在
     */
    public boolean isExist(String hdfsPath) {
        try {
            return fs.exists(new Path(hdfsPath));
        } catch (IOException e) {
            System.err.println("[检查异常] " + e.getMessage());
            return false;
        }
    }

    /**
     * 获取HDFS磁盘使用情况
     */
    public void getDiskUsage() {
        try {
            // 获取HDFS的文件系统状态
            FsStatus fsStatus = fs.getStatus();

            long capacity = fsStatus.getCapacity();     // 总容量(字节)
            long used = fsStatus.getUsed();              // 已使用空间(字节)
            long remaining = fsStatus.getRemaining();    // 剩余空间(字节)

            System.out.println("\n===== HDFS 磁盘使用情况 =====");
            System.out.println("总容量:   " + formatSize(capacity));
            System.out.println("已使用:   " + formatSize(used));
            System.out.println("剩余:     " + formatSize(remaining));
            System.out.printf("使用率:   %.2f%%\n", (used * 100.0 / capacity));
        } catch (IOException e) {
            System.err.println("[获取磁盘信息失败] " + e.getMessage());
        }
    }

    /**
     * 关闭FileSystem连接,释放资源
     */
    public void close() {
        if (fs != null) {
            try {
                fs.close();
                System.out.println("[已关闭] FileSystem连接已释放");
            } catch (IOException e) {
                System.err.println("[关闭异常] " + e.getMessage());
            }
        }
    }

    // ==================== 主方法:测试所有功能 ====================
    public static void main(String[] args) {
        BasicConfigurator.configure();

        HDFSFileUtil hdfsUtil = null;

        try {
            // 创建工具实例
            hdfsUtil = new HDFSFileUtil("hdfs://node1:9000", "hadoop");

            // --- 测试写入文件 ---
            System.out.println("=== 测试写入 ===");
            hdfsUtil.writeFile(
                "/chapter3/java_api/util_test/hello.txt",
                "Hello HDFS!\nThis is a test file.\nHDFS Java API is powerful.\n",
                true
            );

            // --- 测试判断文件是否存在 ---
            System.out.println("\n=== 测试文件存在判断 ===");
            String testPath = "/chapter3/java_api/util_test/hello.txt";
            System.out.println(testPath + " 存在? " + hdfsUtil.isExist(testPath));
            System.out.println("/not_exist.txt 存在? " + hdfsUtil.isExist("/not_exist.txt"));

            // --- 测试读取文件 ---
            System.out.println("\n=== 测试读取 ===");
            String content = hdfsUtil.readFile(testPath);
            if (content != null) {
                System.out.println("文件内容:\n" + content);
            }

            // --- 测试列出文件 ---
            System.out.println("=== 测试列出文件 ===");
            hdfsUtil.listFiles("/chapter3/java_api/", true);

            // --- 测试获取磁盘使用情况 ---
            System.out.println("\n=== 测试磁盘使用情况 ===");
            hdfsUtil.getDiskUsage();

            // --- 测试删除 ---
            System.out.println("\n=== 测试删除 ===");
            hdfsUtil.deleteFile("/chapter3/java_api/util_test", true);

        } catch (Exception e) {
            System.err.println("程序异常: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 释放资源
            if (hdfsUtil != null) {
                hdfsUtil.close();
            }
        }
    }
}

十一、Federation 机制

11.1 Federation 机制的实现原理

问题背景:

在 HDFS 1.x 中,只有一个 NameNode,存在两个关键瓶颈:

  1. 内存瓶颈:所有文件的元数据都存储在单个 NameNode 的内存中,受限于单机内存
  2. 性能瓶颈:单个 NameNode 处理所有客户端请求,成为性能瓶颈

Federation 解决方案:

复制代码
传统架构(单NameNode):
                    ┌──────────┐
                    │ NameNode  │ ← 所有元数据存储在一个NameNode
                    │ (单个NS)  │ ← 内存有限,性能有限
                    └─────┬────┘
                    ┌─────┼─────┐
                    ▼     ▼     ▼
                  DN1   DN2   DN3

Federation架构(多NameNode):
    ┌──────────┐  ┌──────────┐  ┌──────────┐
    │ NameNode1 │  │ NameNode2 │  │ NameNode3 │  ← 每个NameNode
    │ (NS1)     │  │ (NS2)     │  │ (NS3)     │    管理一个独立的
    │ /user     │  │ /data     │  │ /logs     │    命名空间(Namespace)
    └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
          │              │              │
    ┌─────┴──────────────┴──────────────┴─────┐
    │          共享的 DataNode 集群              │
    │    DN1    DN2    DN3    DN4    DN5        │
    └─────────────────────────────────────────┘

关键点:
- 每个 NameNode 管理一个独立的命名空间(Namespace Volume)
- 不同 NameNode 之间互不通信、互不影响
- 所有 NameNode 共享同一组 DataNode
- 每个 NameNode 独立管理自己的 Block Pool

11.2 Federation 机制的核心概念

概念 说明
Namespace 命名空间,由目录、文件和块组成。每个NameNode管理一个独立的命名空间
Block Pool 块池,属于同一个命名空间的所有数据块的集合。每个NameNode有自己的Block Pool
Namespace Volume 命名空间卷 = Namespace + Block Pool,是一个独立的完整单元
Cluster ID 集群标识,用于标识一个HDFS集群。所有NameNode和DataNode共享同一个Cluster ID

11.3 Federation 机制的特点

优点 说明
水平扩展 可以通过增加NameNode来扩展命名空间和吞吐量
命名空间隔离 不同NameNode管理不同的命名空间,互不影响
性能提升 多个NameNode并行处理请求,提高整体吞吐量
灵活性 可以按照业务/部门划分命名空间
缺点/限制 说明
不支持跨Namespace通信 不同命名空间之间的文件无法直接互相访问
管理复杂度增加 需要管理多个NameNode
并非HA Federation 不解决单个 NameNode 的高可用问题(需配合 HA)

11.4 Federation 机制的实现

hdfs-site.xml 配置示例
xml 复制代码
<!-- ============================================================
     Federation 配置:定义多个NameNode
     ============================================================ -->

<!-- ====== NameNode1 的配置(ns1)====== -->

<!-- 定义第一个NameNode的RPC地址 -->
<property>
    <name>dfs.namenode.rpc-address.ns1.nn1</name>
    <value>node1:9000</value>
</property>

<!-- 定义第一个NameNode的HTTP地址(Web UI) -->
<property>
    <name>dfs.namenode.http-address.ns1.nn1</name>
    <value>node1:9870</value>
</property>

<!-- ====== NameNode2 的配置(ns2)====== -->

<!-- 定义第二个NameNode的RPC地址 -->
<property>
    <name>dfs.namenode.rpc-address.ns2.nn2</name>
    <value>node2:9000</value>
</property>

<!-- 定义第二个NameNode的HTTP地址 -->
<property>
    <name>dfs.namenode.http-address.ns2.nn2</name>
    <value>node2:9870</value>
</property>

<!-- ====== 联邦配置 ====== -->

<!-- 配置所有NameService的逻辑名称列表,逗号分隔 -->
<property>
    <name>dfs.nameservices</name>
    <value>ns1,ns2</value>
</property>

<!-- 配置每个NameService包含哪些NameNode -->
<!-- ns1 包含 nn1 -->
<property>
    <name>dfs.ha.namenodes.ns1</name>
    <value>nn1</value>
</property>

<!-- ns2 包含 nn2 -->
<property>
    <name>dfs.ha.namenodes.ns2</name>
    <value>nn2</value>
</property>

<!-- ====== DataNode配置 ====== -->

<!-- DataNode的数据存储目录(多个目录用逗号分隔,每个目录对应一块磁盘) -->
<property>
    <name>dfs.datanode.data.dir</name>
    <value>/data/hdfs/dn1,/data/hdfs/dn2</value>
</property>

<!-- 数据块副本数 -->
<property>
    <name>dfs.replication</name>
    <value>3</value>
</property>

<!-- 是否启用Federation -->
<property>
    <name>dfs.federation.enabled</name>
    <value>true</value>
</property>
core-site.xml 配置
xml 复制代码
<!-- ============================================================
     使用Federation时,客户端需要指定访问哪个NameService
     ============================================================ -->

<!-- 方式1:默认访问ns1 -->
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://ns1</value>
</property>

<!-- 方式2:配置ViewFS(统一挂载点,透明访问多个NameService) -->
<property>
    <name>fs.viewfs.mounttable.default.link./ns1</name>
    <value>hdfs://ns1</value>
</property>
<property>
    <name>fs.viewfs.mounttable.default.link./ns2</name>
    <value>hdfs://ns2</value>
</property>
Federation 下的 Java API 访问
java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.BasicConfigurator;

import java.net.URI;

/**
 * Federation 机制下的 Java API 操作
 * 
 * 关键点:访问不同的NameService需要指定不同的URI
 */
public class FederationDemo {

    public static void main(String[] args) throws Exception {
        BasicConfigurator.configure();

        Configuration conf = new Configuration();
        // Federation下,fs.defaultFS可以不指定,因为访问时需要明确指定NameService
        // conf.set("fs.defaultFS", "hdfs://ns1");

        // ======== 访问 NameService ns1 ========
        // 明确指定URI为 hdfs://ns1
        FileSystem fs1 = FileSystem.get(new URI("hdfs://ns1"), conf, "hadoop");

        // 在 ns1 管理的命名空间下创建目录和文件
        fs1.mkdirs(new Path("/data_ns1"));
        fs1.create(new Path("/data_ns1/file_from_ns1.txt"))
           .writeBytes("This file is stored in NS1 namespace");
        System.out.println("NS1 - 文件创建成功");

        // 列出ns1根目录
        System.out.println("NS1 根目录内容:");
        for (var status : fs1.listStatus(new Path("/"))) {
            System.out.println("  " + status.getPath());
        }
        fs1.close();

        // ======== 访问 NameService ns2 ========
        // 明确指定URI为 hdfs://ns2
        FileSystem fs2 = FileSystem.get(new URI("hdfs://ns2"), conf, "hadoop");

        // 在 ns2 管理的命名空间下创建目录和文件
        fs2.mkdirs(new Path("/data_ns2"));
        fs2.create(new Path("/data_ns2/file_from_ns2.txt"))
           .writeBytes("This file is stored in NS2 namespace");
        System.out.println("\nNS2 - 文件创建成功");

        // 列出ns2根目录
        System.out.println("NS2 根目录内容:");
        for (var status : fs2.listStatus(new Path("/"))) {
            System.out.println("  " + status.getPath());
        }
        fs2.close();

        // 注意:ns1 中的文件在 ns2 中是看不到的,因为它们是独立的命名空间
        System.out.println("\n两个NameService的命名空间完全隔离");
    }
}

十二、Erasure Coding(纠删码)

12.1 概述

传统副本机制的存储开销问题:

复制代码
传统3副本机制:
原始数据: 100GB
存储消耗: 100GB × 3 = 300GB
存储效率: 33.3%(1/3)

Erasure Coding(以 RS-6-3 为例):
原始数据: 100GB
存储消耗: 100GB × (6+3)/6 = 150GB
存储效率: 66.7%(2/3)

节省存储: 50%!

12.2 Erasure Coding 原理

复制代码
RS-6-3 编码原理(Reed-Solomon编码):

原始数据文件(拆分为6个数据块):
┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐
│ D1 ││ D2 ││ D3 ││ D4 ││ D5 ││ D6 │  ← 6个Data Block(数据块)
└──┬─┘└──┬─┘└──┬─┘└──┬─┘└──┬─┘└──┬─┘
   │      │      │      │      │      │
   └──────┴──────┴──────┴──────┴──────┘
                  │
            编码计算(RS算法)
                  │
   ┌──────────────┼──────────────┐
   ▼              ▼              ▼
┌────┐       ┌────┐       ┌────┐
│ P1 │       │ P2 │       │ P3 │  ← 3个Parity Block(校验块)
└────┘       └────┘       └────┘

最终存储:6个数据块 + 3个校验块 = 9个块
容错能力:任意丢失3个块(数据块或校验块),都可以恢复原始数据

12.3 Erasure Coding 策略

编码策略 数据块(k) 校验块§ 容错能力 存储开销 存储效率
RS-6-3-64k 6 3 允许丢失3个块 1.5x 66.7%
RS-3-2-64k 3 2 允许丢失2个块 1.67x 60%
RS-10-4-64k 10 4 允许丢失4个块 1.4x 71.4%
XOR-1-1-64k 1 1 允许丢失1个块 2x 50%

12.4 Erasure Coding 使用方式

Shell 命令设置 EC 策略
bash 复制代码
# ============================================================
# 1. 查看当前支持的EC策略
# ============================================================
hdfs ec -listPolicies

# ============================================================
# 2. 启用指定的EC策略
# ============================================================
# 启用 RS-6-3-64k 策略
hdfs ec -enablePolicy -policy RS-6-3-64k

# 启用 RS-3-2-64k 策略
hdfs ec -enablePolicy -policy RS-3-2-64k

# ============================================================
# 3. 为指定目录设置EC策略
# ============================================================
# 为 /data/ec_test 目录设置 RS-6-3-64k 策略
hdfs ec -setPolicy -path /data/ec_test -policy RS-6-3-64k

# ============================================================
# 4. 查看指定目录的EC策略
# ============================================================
hdfs ec -getPolicy -path /data/ec_test

# ============================================================
# 5. 取消指定目录的EC策略
# ============================================================
hdfs ec -unsetPolicy -path /data/ec_test

# ============================================================
# 6. 查看EC相关的文件信息
# ============================================================
# 查看文件的EC编码信息
hdfs fsck /data/ec_test/file.txt -files -blocks -locations
Erasure Coding 的限制
bash 复制代码
# ============================================================
# EC 策略的限制条件
# ============================================================

# 1. EC文件不能使用 hdfs dfs -appendToFile 追加内容
#    因为纠删码文件一旦写入就不能修改

# 2. EC文件需要使用条带化(Striping)写入
#    要求文件大小 > 一个条带单元(stripe cell)
#    最小文件大小 = dataBlockNum × cellSize(如 RS-6-3: 6 × 64KB = 384KB)

# 3. EC不适合小文件
#    小于一个条带大小的文件无法充分利用EC的优势

# 4. EC编解码会消耗CPU资源
#    需要在存储节省和CPU消耗之间权衡

# 5. EC文件的恢复比副本机制慢
#    需要从多个节点读取数据进行编解码计算
Java API 设置 EC 策略
java 复制代码
package com.example.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.client.HdfsAdmin;
import org.apache.hadoop.hdfs.protocol.ErasureCodingPolicy;
import org.apache.log4j.BasicConfigurator;

import java.net.URI;

/**
 * Erasure Coding 纠删码 Java API 操作
 * 
 * 功能:
 *   1. 查看可用的EC策略
 *   2. 为目录设置EC策略
 *   3. 查看目录的EC策略
 *   4. 取消EC策略
 */
public class ErasureCodingDemo {

    public static void main(String[] args) {
        BasicConfigurator.configure();

        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://node1:9000");

            // ======== 创建HdfsAdmin对象 ========
            // HdfsAdmin 提供了管理HDFS的高级API(包括EC操作)
            // 注意:需要Hadoop 3.x
            HdfsAdmin admin = new HdfsAdmin(
                new URI("hdfs://node1:9000"),   // HDFS URI
                conf                              // 配置对象
            );

            // ======== 1. 创建测试目录 ========
            FileSystem fs = FileSystem.get(new URI("hdfs://node1:9000"), conf, "hadoop");
            Path ecDir = new Path("/data/ec_test");
            fs.mkdirs(ecDir);
            System.out.println("创建目录: " + ecDir);

            // ======== 2. 设置EC策略 ========
            // RS-6-3-64k: 6个数据块 + 3个校验块,条带单元大小64KB
            ErasureCodingPolicy policy = admin.getErasureCodingPolicy(ecDir);
            System.out.println("当前EC策略: " + (policy != null ? policy.getName() : "无(默认副本)"));

            // 设置新的EC策略
            // setErasureCodingPolicy(path, policyName)
            admin.setErasureCodingPolicy(ecDir, "RS-6-3-64k");
            System.out.println("已设置EC策略: RS-6-3-64k");

            // ======== 3. 验证EC策略 ========
            ErasureCodingPolicy currentPolicy = admin.getErasureCodingPolicy(ecDir);
            if (currentPolicy != null) {
                System.out.println("验证EC策略:");
                System.out.println("  策略名称: " + currentPolicy.getName());
                System.out.println("  数据块数: " + currentPolicy.getNumDataUnits());
                System.out.println("  校验块数: " + currentPolicy.getNumParityUnits());
                System.out.println("  条带单元大小: " + currentPolicy.getCellSize() + " 字节");
                System.out.println("  容错能力: 可丢失 " + currentPolicy.getNumParityUnits() + " 个块");
            }

            // ======== 4. 取消EC策略 ========
            // admin.unsetErasureCodingPolicy(ecDir);
            // System.out.println("已取消EC策略");

            fs.close();

        } catch (Exception e) {
            System.err.println("操作异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

十三、本章小结

知识点汇总表

知识点 核心要点
文件系统分类 本地FS vs 分布式FS;块存储 vs 对象存储 vs 文件存储
HDFS简介 适合大文件、流式访问、一次写多次读;不适合低延迟、小文件、随机写
HDFS架构 NameNode(管理元数据)+ DataNode(存储数据)+ SecondaryNameNode(合并元数据)
HDFS特点 高容错(3副本)、高吞吐、流式访问、廉价硬件、移动计算
文件读流程 Client→NN获取Block列表→就近读取DN数据
文件写流程 Client→NN创建文件→获取DN列表→建立Pipeline→数据流式写入→ACK确认
HDFS健壮性 心跳检测、Checksum校验、安全模式、HA机制
Shell操作 hdfs dfs -put/get/cat/ls/rm/mkdir/cp/mv/chmod/du/df
Shell定时采集 crontab + shell脚本,按日期目录结构上传日志
Java API FileSystem为核心类,Configuration配置,Path表示路径,FSDataInputStream/OutputStream读写
Federation 多个NameNode管理不同命名空间,共享DataNode,水平扩展命名空间
Erasure Coding RS编码,k个数据块+p个校验块,比3副本节省约50%存储,但增加CPU开销

关键配置参数汇总

xml 复制代码
<!-- HDFS核心配置参数速查 -->

<!-- NameNode地址 -->
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://node1:9000</value>
</property>

<!-- 数据块大小(默认128MB) -->
<property>
    <name>dfs.blocksize</name>
    <value>134217728</value>  <!-- 128MB -->
</property>

<!-- 副本数(默认3) -->
<property>
    <name>dfs.replication</name>
    <value>3</value>
</property>

<!-- NameNode数据目录 -->
<property>
    <name>dfs.namenode.name.dir</name>
    <value>/data/hdfs/nn</value>
</property>

<!-- DataNode数据目录 -->
<property>
    <name>dfs.datanode.data.dir</name>
    <value>/data/hdfs/dn</value>
</property>

<!-- 心跳间隔(默认3秒) -->
<property>
    <name>dfs.heartbeat.interval</name>
    <value>3</value>
</property>

<!-- 心跳超时(默认10分钟+30秒) -->
<property>
    <name>dfs.namenode.heartbeat.recheck-interval</name>
    <value>300000</value>  <!-- 5分钟 -->
</property>

<!-- 安全模式阈值(99.9%的Block满足最小副本数时退出) -->
<property>
    <name>dfs.namenode.safemode.threshold-pct</name>
    <value>0.999f</value>
</property>
相关推荐
星恒随风2 小时前
C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解
开发语言·c++·笔记·学习
数智工坊10 小时前
机器人运动控制:采样、优化与学习三大流派深度对比与实战
android·学习·机器人
ZC跨境爬虫11 小时前
跟着 MDN 学JavaScript day_7:数学运算与逻辑判断实战测试
开发语言·前端·javascript·学习·ecmascript
MartinYeung513 小时前
[论文学习]隐私保护联邦特徵选择与差分隐私的的工程实践框架
学习
qeen8713 小时前
【C++】类与对象之类的默认成员函数(二)
android·c语言·开发语言·c++·笔记·学习
Flandern111114 小时前
Pull Requests(PR)
学习·github·pr
nashane15 小时前
HarmonyOS 6学习:JsCrash“闪退”法医指南——从FaultLog堆栈还原崩溃现场的终极手册
学习·华为·harmonyos
for_ever_love__15 小时前
UI学习:UICollectionView瀑布流
学习·ui·ios·objective-c·cocoa
AOwhisky15 小时前
MySQL 学习笔记(第六期):MySQL 备份与恢复
运维·数据库·笔记·学习·mysql·云计算