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() │ │ │ │
│ │ │ │ │
写入关键步骤:
- Client 向 NameNode 请求创建文件
- NameNode 校验权限和文件是否已存在
- Client 请求写入数据,NameNode 返回可用的 DataNode 列表
- Client 与 DataNode 建立 Pipeline(管道)
- 数据以 Packet(默认64KB)为单位沿 Pipeline 传输
- 每个 DataNode 收到数据后向上传递 ACK 确认
- 所有数据写入完成后关闭文件
六、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 dfs 或 hadoop 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.xml、hdfs-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,存在两个关键瓶颈:
- 内存瓶颈:所有文件的元数据都存储在单个 NameNode 的内存中,受限于单机内存
- 性能瓶颈:单个 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>