HDFS 硬核拆解 --- 分布式文件系统的读写 Pipeline 与 Java 实战
你有一个 10TB 的文件,单机存不下怎么办?答案是:把它切成小块,分散存储在多台机器上。但这又带来了新问题------如何保证数据不丢?如何找到我需要的数据?HDFS 就是为此而生的。
一、开篇:从"单机不够用"说起
传统文件系统(比如 Windows 的 NTFS、Linux 的 ext4)是在一台机器上管理文件。当文件大到超出单块硬盘容量时,你只能:
- 换更大的硬盘(但总有上限)
- 自己做 RAID 磁盘阵列(但管理复杂)
- 手动切分文件,放在不同目录(但你得自己记住哪部分在哪)
HDFS(Hadoop Distributed File System)的出现,就是为了解决这个问题:
把巨大的文件切成块,分布存储在一个集群的成百上千台机器上,对用户而言,感觉仍然是在操作一个"单个的大文件系统"。
用户只管读写文件,至于文件被切成了多少块、存在哪台机器上------全部由 HDFS 框架自动管理,对用户透明。
二、HDFS 的演化
HDFS 不是凭空造出来的。它脱胎于 Google 在 2003 年发表的 GFS(Google File System) 论文。
有趣的是:Google 发表 GFS 论文的初衷是分享经验 ,而不是开源。但开源社区受其启发,写出了 HDFS。一篇论文改变了一个产业,这就是最好的例子。
| 时间 | 事件 |
|---|---|
| 2003 年 10 月 | Google 发表 GFS 论文 |
| 2004-2005 年 | Doug Cutting 在 Nutch 中参考 GFS 实现 DFS |
| 2006 年 | DFS 随 Hadoop 独立出来,命名为 HDFS |
| 至今 | HDFS 已成为海量数据存储的事实标准 |
三、HDFS 核心概念(逐个深度解析)
3.1 NameNode --- "集群的大脑"
一句话 :NameNode 是 HDFS 的主节点 ,负责管理整个文件系统的元数据。
具体职责:
- 维护文件系统的目录树结构(哪个目录下有哪些文件和子目录)
- 记录每个文件的块信息(一个文件被分成了几个块、每个块有多大)
- 记录每个块存储在哪些 DataNode 上
- 处理客户端的读写请求------告诉客户端该去哪个 DataNode 读/写数据
NameNode 存储的所有信息,统称为元数据(Metadata) 。元数据是 HDFS 的灵魂,NameNode 一旦宕机,整个集群就无法服务(除非配置了 HA 高可用)。
3.2 DataNode --- "干活的工人"
一句话 :DataNode 是 HDFS 的从节点 ,负责实际存储数据块。
具体职责:
- 存储 HDFS 的数据块(Block),每个块默认 128MB
- 定期向 NameNode 发送心跳 + 块报告("我还活着,我手里有这些块")
- 响应客户端的读写请求------真正传输数据的节点
- 执行数据块的创建、删除、复制等操作
NameNode vs DataNode:
- NameNode 知道"这个文件的块在哪里"(元数据)
- DataNode 存储"这些块的实际内容"(数据本身)
3.3 Block(数据块)--- 为什么是 128MB?
传统文件系统(如 ext4)的块大小通常是 4KB 。HDFS 的块默认是 128MB ------大了 32000 倍!
为什么 HDFS 要用这么大的块?
核心原因:减少寻址开销
HDFS 的假设是:文件很大(GB 到 TB 级),并且以流式方式顺序读取为主。
在磁带上读取数据时,寻址时间 (磁头移动到数据位置的时间)是巨大的开销。如果块太小,寻址时间占比会很高,严重影响性能。
数学计算:
- 假设寻址时间:10ms
- 数据传输率:100MB/s
- 如果块大小是 4KB:传输时间 = 4KB / 100MB = 0.04ms,寻址时间占比 = 10 / (10 + 0.04) ≈ 99.6% --- 几乎全在找数据,没在传输!
- 如果块大小是 128MB:传输时间 = 128MB / 100MB = 1.28s = 1280ms,寻址时间占比 = 10 / (10 + 1280) ≈ 0.78% --- 几乎全在传输数据!
所以 128MB 是一个经过权衡的平衡点:太大浪费存储空间,太小寻址开销成倍增加。
分块带来的三大好处
| 好处 | 说明 |
|---|---|
| ① 文件大小"无限" | 一个文件可以切成无数个 Block,分布在集群所有机器上,突破了单机容量的限制 |
| ② 高容错 | 每个 Block 默认存储 3 个副本。某台 DataNode 挂了,从副本读取即可 |
| ③ 并行访问 | 多个消费者可以从不同 DataNode 同时读取不同的 Block,加速处理 |
3.4 Rack(机架)--- 物理拓扑感知
一个 Hadoop 集群通常跨多个机架(Rack)。机架是一个物理概念------机架上的服务器共享一个网络交换机。
机架感知(Rack Awareness):NameNode 知道每个 DataNode 属于哪个机架。
副本放置策略(默认 3 副本)
第一副本:放在客户端所在的节点(如果客户端不在集群中,随机选一个)
第二副本:放在与第一副本不同机架的一个节点上
第三副本:放在与第二副本同一机架的另一个节点上
为什么要这么放?
- 第一副本:尽可能近(写入快)
- 第二副本:放在不同机架(整个机架断电也不丢数据)
- 第三副本:和第二个在同一机架但不同节点(既保证机架内冗余,又减少跨机架网络流量)
这个策略保证了:即使一个机架完全断电,数据仍然可用(因为另一个机架上有副本)。
3.5 Metadata(元数据)--- NameNode 的"记忆"
NameNode 中维护的元数据分为三种:
| 元数据类型 | 内容 |
|---|---|
| 文件和目录信息 | 文件名、目录名、父目录、创建时间、修改时间、文件大小、权限 |
| 文件块映射 | 每个文件被分成了哪些 Block、每个 Block 在哪些 DataNode 上 |
| DataNode 信息 | 集群中所有 DataNode 的状态、存储容量、负载等 |
四、HDFS 的架构特点
主从(Master/Slave)架构
┌──────────────┐
│ NameNode │ ← 主节点(单点做 HA)
│ (元数据) │
└──────┬───────┘
│ 管理
┌─────────────────┼─────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ DataNode 1│ │ DataNode 2 │ │ DataNode 3 │ ← 从节点
│ Block A │ │ Block A │ │ Block A │
│ Block B │ │ Block C │ │ Block B │ ← Block 分散且冗余
└───────────┘ └─────────────┘ └─────────────┘
优缺点对照
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 高容错:Block 默认 3 副本,机器挂掉自动恢复 | 高延迟:不适合低延迟数据访问(秒级响应) |
| 流式访问:适合批量读取,最大化吞吐量 | 小文件问题:大量小文件会占满 NameNode 内存 |
| 超大文件:PB 级文件的存储和管理 | 不支持并发写入:一个文件同时只能由一个客户端写 |
| 高吞吐量:多节点并行读/写 | 不支持文件随机修改:只能追加或覆盖 |
五、HDFS 读写原理(Pipeline 深度拆解)
5.1 HDFS 写入流程(Write Pipeline)
这是 HDFS 最核心、最精巧的设计之一。假设我们要上传一个 300MB 的文件 data.txt 到 HDFS:
文件大小:300MB
每个 Block:128MB
所以文件被分为 3 个 Block(128MB + 128MB + 44MB)
默认 3 副本
步骤 1:客户端向 NameNode 发起写请求
Client ───RPC───→ NameNode
客户端通过 RPC(远程过程调用)请求 NameNode:"我想上传一个文件 /data.txt"。
NameNode 检查:
- ✅ 文件父目录是否存在?
- ✅ 同名文件是否已存在?
- ✅ 客户端是否有写权限?
一切就绪 → NameNode 返回:"可以上传了。"
步骤 2:请求第一个 Block 的位置
Client ───RPC───→ NameNode
"我要上传第一个 Block,3 个副本"
NameNode ───返回───→ Client
"存到 DataNode_01 / DataNode_02 / DataNode_03"
NameNode 根据副本放置策略 和各个 DataNode 的当前负载 ,返回一个有序的 DataNode 列表。
步骤 3:建立 Pipeline 流水线
Client DataNode_01 ─→ DataNode_02 ─→ DataNode_03
│ │ │ │
└─────建立连接────┘ │ │
└─────转发──────┘ │
└─────转发──────┘
客户端与第一个 DataNode(DN_01)建立连接,DN_01 再与 DN_02 连接,DN_02 与 DN_03 连接------形成一条 Pipeline 流水线。
步骤 4:逐级确认(Pipeline 反向 Ack)
DataNode_01 ←── Ack ── DataNode_02 ←── Ack ── DataNode_03
每个 DataNode 收到数据并写入磁盘后,向上游发送确认(Ack):
- DN_03 写入成功 → 告诉 DN_02
- DN_02 写入成功 → 告诉 DN_01
- DN_01 写入成功 → 告诉客户端
只有收到全部三个 Ack,客户端才确认这个数据包传输成功。
步骤 5:数据传输的"内部切割"
你可能以为客户端直接发一个 128MB 的大包给 DataNode------并不是。HDFS 会把数据进一步切成更小的单位:
128MB Block
│
├── Packet(包,约 64KB)
│ │
│ ├── Chunk(块,约 512B)
│ └── Checksum(校验和,4B)
│
└── Packet ...
Packet 是 Pipeline 上传输的基本单位,Chunk + Checksum 是保证数据完整性的机制。每个 Chunk 后面跟一个 4 字节的校验和,接收方验证校验和不匹配就表示数据损坏了,可以要求重传。
步骤 6:重复步骤 2-5,直到所有 Block 上传完毕
第一个 Block(128MB)上传完毕后,客户端再次请求 NameNode 获取第二个 Block 的存储位置,重复 Pipeline 过程,直到三个 Block 全部上传完毕。
写入流程总结(7 步):
① Client → NN:请求上传文件
② NN 检查权限和目录树 → 返回"可以上传"
③ Client → NN:请求第一个 Block 的位置(3 副本)
④ NN 返回 DN 列表(DN_01, DN_02, DN_03)
⑤ Client 与 DN_01 建立 Pipeline → DN_01 → DN_02 → DN_03
⑥ Client 发送数据(Packet → Chunk + Checksum)→ 反向 Ack 确认
⑦ 第一个 Block 完成 → 重复③~⑥直到所有 Block 上传完毕
5.2 HDFS 读取流程
读数据比写数据简单,但也非常巧妙。
步骤 1:客户端向 NameNode 请求文件位置
Client ───"我想读取 /data.txt"───→ NameNode
NameNode 查找元数据,返回 data.txt 的所有 Block 的位置列表:
Block 0:DN_01, DN_02, DN_03
Block 1:DN_02, DN_04, DN_05
Block 2:DN_01, DN_03, DN_05
步骤 2:就近读取(网络拓扑距离)
HDFS 的读取有一个重要的优化原则:读取最近的副本。
NameNode 返回的 DN 列表是按网络拓扑距离排序 的。如果客户端在 DN_01 所在节点,它会优先从 DN_01 读取(数据本地性),避免跨网络传输。
步骤 3:并行读取 + 校验和验证
Client
├──→ 从 DN_01 读取 Block 0(同时校验 Checksum)
├──→ 从 DN_02 读取 Block 1(并行!)
└──→ 从 DN_03 读取 Block 2(并行!)
↓
合并为完整文件 /data.txt
- 客户端同时 从多个 DataNode 读取不同的 Block(并行 I/O)
- 每个 Block 读取后验证 Checksum,如果校验失败→尝试从另一个副本读取
- 所有 Block 读取完毕 → 在客户端合并为完整文件
所以如果 128MB 的 Block 存了 3 份,你读取时可能从 3 台不同的机器各取一部分------这既是容错,也是负载均衡。
六、HDFS Shell 操作
HDFS 提供了 shell 命令,让你像操作本地文件系统一样操作分布式文件系统。
常用命令大全
bash
# 查看目录结构
hdfs dfs -ls / # 列出根目录下所有文件和文件夹
hdfs dfs -ls -R / # 递归列出所有子目录
# 查看文件内容
hdfs dfs -cat /data.txt # 查看文件内容(适用于文本文件)
hdfs dfs -text /data.txt # 以文本格式输出
# 创建目录
hdfs dfs -mkdir /user/data # 创建单级目录
hdfs dfs -mkdir -p /a/b/c # 创建多级目录(类似 mkdir -p)
# 上传文件(本地 → HDFS)
hdfs dfs -put local_file.txt /hdfs/path/ # 上传单个文件
hdfs dfs -put local_dir/ /hdfs/path/ # 上传整个目录
# 下载文件(HDFS → 本地)
hdfs dfs -get /hdfs/file.txt local_dir/
# 删除文件/目录
hdfs dfs -rm /hdfs/file.txt # 删除文件
hdfs dfs -rm -r /hdfs/dir/ # 递归删除目录
hdfs dfs -rm -r -skipTrash /xxx # 跳过回收站(直接删)
# 移动/重命名
hdfs dfs -mv /hdfs/a /hdfs/b
# 复制
hdfs dfs -cp /hdfs/a /hdfs/b
# 统计大小
hdfs dfs -du -h /path # 查看目录/文件大小(人可读格式)
# 查看帮助
hdfs dfs -help # 查看所有命令
hdfs dfs -help ls # 查看某个命令的详细用法
实际案例:定时采集日志到 HDFS
bash
#!/bin/bash
# 每天凌晨采集应用服务器的日志文件到 HDFS
# 设置变量
LOG_DIR="/var/log/myapp"
HDFS_BASE="/logs/myapp"
DATE=$(date +%Y%m%d)
# 采集并上传
hdfs dfs -put $LOG_DIR/*.log $HDFS_BASE/$DATE/
# 检查是否成功
if [ $? -eq 0 ]; then
echo "Upload success: $DATE"
else
echo "Upload failed: $DATE"
exit 1
fi
七、HDFS Java API --- 用代码操作分布式文件系统
Shell 命令适合手动操作,但在实际开发中,我们通常使用 Java API 编程访问 HDFS。
7.1 核心类讲解
Configuration --- 配置对象
java
Configuration conf = new Configuration();
// 设置 HDFS 的访问地址
conf.set("fs.defaultFS", "hdfs://hadoop01:9000");
作用 :加载 Hadoop 配置文件(core-site.xml、hdfs-site.xml 等)中的配置项。你可以通过 conf.set(key, value) 覆盖默认配置。
常用配置项:
fs.defaultFS:HDFS 的 URI,例如hdfs://localhost:9000dfs.replication:副本数(默认 3)dfs.blocksize:块大小(默认 134217728 = 128MB)
FileSystem --- 文件系统客户端
java
// 获取 HDFS 文件系统实例(工厂方法)
FileSystem fs = FileSystem.get(conf);
作用 :FileSystem 是一个抽象基类 ,HDFS 的实现是 DistributedFileSystem。通过 FileSystem.get(conf) 工厂方法获取实例,后续所有 HDFS 操作都通过这个对象完成。
常用方法:
| 方法签名 | 功能说明 |
|---|---|
copyFromLocalFile(Path src, Path dst) |
从本地复制文件到 HDFS |
copyToLocalFile(Path src, Path dst) |
从 HDFS 复制文件到本地 |
mkdirs(Path f) |
创建目录(支持多级) |
rename(Path src, Path dst) |
重命名文件或目录 |
delete(Path f, boolean recursive) |
删除文件/目录(recursive=true 可删非空目录) |
listFiles(Path f, boolean recursive) |
返回所有文件的迭代器(包含块信息) |
listStatus(Path f) |
返回目录下所有文件和目录的元数据状态 |
Path --- 路径
java
Path src = new Path("D:/test.txt"); // 本地路径
Path dst = new Path("/user/data/test.txt"); // HDFS 路径
Path 封装了 Hadoop 中的文件或目录路径,支持 hdfs://、file://、s3a:// 等 URI 前缀。
FileStatus --- 文件/目录元数据
java
FileStatus status = fs.getFileStatus(new Path("/data.txt"));
status.isFile(); // 是否是文件
status.isDirectory(); // 是否是目录
status.getLen(); // 文件大小
status.getBlockSize(); // 块大小
status.getPermission(); // 访问权限
status.getPath(); // 文件路径
LocatedFileStatus --- 带位置信息的 FileStatus
java
LocatedFileStatus fileStatus = ...;
BlockLocation[] blocks = fileStatus.getBlockLocations();
for (BlockLocation bl : blocks) {
bl.getHosts(); // 这个块在哪些 DataNode 上
bl.getOffset(); // 起始偏移量
bl.getLength(); // 块长度
}
为什么需要这个类 ?因为对于 MapReduce 来说,"这个块在哪个节点上"至关重要------它决定了数据本地性(尽量在存数据的节点上计算,避免网络传输)。
RemoteIterator --- 远程迭代器
java
RemoteIterator<LocatedFileStatus> iterator = fs.listFiles(new Path("/"), true);
while (iterator.hasNext()) {
LocatedFileStatus status = iterator.next();
// 处理每个文件
}
listFiles() 返回的不是集合而是迭代器 ,因为 HDFS 集群可能有海量文件,一次性返回全部文件列表会撑爆内存。迭代器可以按需取数据。
7.2 完整 CRUD 代码实战
下面是一个完整的 HDFS CRUD 类,包含初始化、上传、下载、创建/删除/重命名、列出文件等所有常见操作。我们逐段分析。
完整代码
java
import java.io.FileNotFoundException;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import org.junit.Before;
import org.junit.Test;
public class HDFS_CRUD {
FileSystem fs = null;
// ==================== 初始化连接 ====================
@Before
public void init() throws Exception {
// 1. 创建配置对象
Configuration conf = new Configuration();
// 2. 设置 HDFS 的 URI
conf.set("fs.defaultFS", "hdfs://hadoop01:9000");
// 3. 设置访问 HDFS 的用户名(覆盖系统当前用户)
System.setProperty("HADOOP_USER_NAME", "root");
// 4. 获取文件系统客户端
fs = FileSystem.get(conf);
}
逐行解释:
@Before:JUnit 注解,每个测试方法执行前都运行此方法Configuration conf = new Configuration():创建配置对象,自动加载 classpath 下的core-site.xml等配置文件conf.set("fs.defaultFS", "hdfs://hadoop01:9000"):设置 HDFS 的访问地址。hadoop01:9000是 NameNode 的 RPC 地址,实际使用时替换为自己的 NameNode 地址System.setProperty("HADOOP_USER_NAME", "root"):设置客户端身份为root,避免权限不足问题。生产环境中应该使用合适的用户FileSystem.get(conf):工厂方法,返回一个DistributedFileSystem实例。后续所有操作都通过fs对象进行
上传文件
java
// ==================== 上传文件(本地 → HDFS)====================
@Test
public void testAddFileToHdfs() throws IOException {
Path src = new Path("D:/test.txt"); // 本地源文件
Path dst = new Path("/testFile"); // HDFS 目标路径
fs.copyFromLocalFile(src, dst); // 上传
fs.close(); // 关闭资源
}
API 详解:
Path src = new Path("D:/test.txt"):本地文件的路径。在 Windows 上使用D:/格式,Linux 上使用/home/user/格式Path dst = new Path("/testFile"):HDFS 上的目标路径。/testFile是 HDFS 根目录下的路径fs.copyFromLocalFile(src, dst):核心方法,将本地文件复制 到 HDFS。这是 HDFS Java API 最常用的方法之一,等同于 Shell 命令-putfs.close():关闭客户端连接,释放资源。务必在 finally 块或 try-with-resources 中调用
copyFromLocalFile 的重载形式:
java
// 参数:是否删除源文件、是否覆盖、本地路径、HDFS 路径
fs.copyFromLocalFile(false, true, src, dst);
// false:不删除本地源文件
// true:覆盖 HDFS 上已存在的文件
下载文件
java
@Test
public void testDownloadFileToLocal() throws IllegalArgumentException, IOException {
fs.copyToLocalFile(new Path("/testFile"), new Path("D:/"));
}
API 详解:
fs.copyToLocalFile(hdfsPath, localPath):从 HDFS 复制到本地。如果本地已有同名文件,注意会抛出FileAlreadyExistsException- 第一个参数:HDFS 上的路径
- 第二个参数:本地目标路径(必须是目录,不能是文件名)
创建/删除/重命名
java
@Test
public void testMkdirAndDeleteAndRename() throws Exception {
// 创建多级目录
fs.mkdirs(new Path("/a/b/c"));
// 重命名(将 /a 重命名为 /a3)
fs.rename(new Path("/a"), new Path("/a3"));
// 删除目录(非空目录必须设置 recursive = true)
fs.delete(new Path("/a2"), true);
}
API 详解:
fs.mkdirs(Path f):创建目录,支持多级 。相当于 Shell 命令mkdir -p。如果父目录不存在,会自动创建fs.rename(Path src, Path dst):重命名或移动文件/目录。相当于 Shell 命令mv。如果dst是目录且已存在,src会被移动到dst下面fs.delete(Path f, boolean recursive):删除文件或目录。
- 如果
f是文件:recursive参数无效- 如果
f是空目录:recursive可设为false- 如果
f是非空目录:必须 设置recursive = true,否则会抛出异常
列出文件信息
java
@Test
public void testListFiles() throws FileNotFoundException, IllegalArgumentException, IOException {
// 获取文件迭代器(第二个参数 recursive = true,递归列出所有子目录)
RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/"), true);
while (listFiles.hasNext()) {
LocatedFileStatus fileStatus = listFiles.next();
// 打印文件名
System.out.println(fileStatus.getPath().getName());
// 打印块大小
System.out.println(fileStatus.getBlockSize());
// 打印权限
System.out.println(fileStatus.getPermission());
// 打印文件长度
System.out.println(fileStatus.getLen());
// 获取该文件的所有块的位置信息
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
for (BlockLocation bl : blockLocations) {
System.out.println("block-length:" + bl.getLength()
+ "--" + "block-offset:" + bl.getOffset());
String[] hosts = bl.getHosts();
for (String host : hosts) {
System.out.println(host);
}
}
System.out.println("----------------------------");
}
}
API 详解:
fs.listFiles(Path f, boolean recursive):只返回文件 (不返回目录),返回一个RemoteIterator<LocatedFileStatus>。
- 第一个参数:起始路径
- 第二个参数:是否递归遍历子目录
RemoteIterator<LocatedFileStatus>:迭代器模式,避免一次性加载所有文件信息导致内存溢出。用法和普通 Iterator 一样LocatedFileStatus:比FileStatus多了块位置信息fileStatus.getPath().getName():获取文件名fileStatus.getBlockSize():获取块大小(默认 128MB = 134217728 字节)fileStatus.getPermission():获取文件权限,如rw-r--r--fileStatus.getLen():获取文件长度(字节数)BlockLocation[] blockLocations:一个文件可能由多个 Block 组成,每个 Block 一个 BlockLocationbl.getHosts():这个 Block 所在的 DataNode 主机名列表bl.getOffset():Block 在文件中的起始偏移量(第 0 字节、第 128MB 字节......)bl.getLength():Block 的长度(最后一个 Block 可能小于 128MB)
查看文件和目录信息
java
@Test
public void testListAll() throws FileNotFoundException, IllegalArgumentException, IOException {
// 列出指定目录下的所有文件和目录(第一级)
FileStatus[] listStatus = fs.listStatus(new Path("/"));
String flag = "d-- ";
for (FileStatus fstatus : listStatus) {
// 判断是文件还是文件夹
if (fstatus.isFile())
flag = "f-- ";
System.out.println(flag + fstatus.getPath().getName());
}
}
}
API 详解:
fs.listStatus(Path f):列出指定目录下的所有文件和目录 (不递归,只查一层),返回FileStatus[]- 对比
listFiles():listStatus返回的是数组 (不是迭代器),并且同时包含文件和目录 。而listFiles返回迭代器(按需取),且只返回文件fstatus.isFile()/fstatus.isDirectory():判断当前条目是文件还是目录fstatus.getPath().getName():获取文件/目录名
7.3 开发中的常见坑与最佳实践
1️⃣ 输出目录必须不存在
Hadoop MapReduce 有个严格的安全机制:输出目录必须不存在 ,否则会抛出
FileAlreadyExistsException。
java
// 最佳实践:运行前自动删除已存在的输出目录
Path outputPath = new Path("D:\\output");
FileSystem fs = FileSystem.get(conf);
if (fs.exists(outputPath)) {
fs.delete(outputPath, true); // true = 递归删除
}
FileOutputFormat.setOutputPath(job, outputPath);
2️⃣ Windows 本地开发需要 winutils
Hadoop 底层依赖本地系统库。如果在 Windows 上运行 MapReduce 任务,需要配置 winutils.exe 和 hadoop.dll。
3️⃣ 路径格式建议
// ❌ 不推荐:反斜杠
new Path("D:\\data\\input");
// ✅ 推荐:正斜杠 + file:// 前缀
new Path("file:///D:/data/input");
八、篇末小结
HDFS 用分布式存储 解决了海量数据的存贮问题,用副本机制 解决了数据的可靠性问题,用Pipeline 流水线解决了高吞吐的写入问题。
核心知识点回顾
HDFS 核心知识链路:
存储单位 → Block(128MB,为什么?→ 减少寻址时间占比)
↓
管理节点 → NameNode(元数据的"大脑")
↓
工作节点 → DataNode(存储 Block 的"工人")
↓
容错机制 → 3 副本 + 机架感知策略
↓
写入流程 → Pipeline 流水线(7 步详解)
↓
读取流程 → 就近读取 + 并行 I/O
↓
Shell 操作 → -put / -get / -ls / -cat / -rm ...
↓
Java API → Configuration → FileSystem → Path → CRUD 方法
下一篇预告:MapReduce------理解了大数据的存储(HDFS),接下来就是大数据的计算。你将看到"分而治之"的哲学如何在代码中体现,三个经典案例助你完全吃透分布式计算。