HDFS硬核拆解-读写Pipeline与Java实战

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.xmlhdfs-site.xml 等)中的配置项。你可以通过 conf.set(key, value) 覆盖默认配置。

常用配置项

  • fs.defaultFS:HDFS 的 URI,例如 hdfs://localhost:9000
  • dfs.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 命令 -put
  • fs.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 一个 BlockLocation
  • bl.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.exehadoop.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),接下来就是大数据的计算。你将看到"分而治之"的哲学如何在代码中体现,三个经典案例助你完全吃透分布式计算。

相关推荐
.Cnn2 小时前
Redis基础day02
java·数据库·redis
能有时光2 小时前
[MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道
java·数据库
什巳2 小时前
JAVA练习277- 找到字符串中所有字母异位
java·开发语言·算法·leetcode
小则又沐风a2 小时前
深入理解文件系统(二)
java·服务器·前端
jojo甜在心2 小时前
(开源)宠物商城系统 全栈开发实录 采用 Java + SpringBoot + Vue + MySQL
java·开源·宠物
zhanghaofaowhrql2 小时前
Spring Data JPA Repository 详解:从基础到高级用法
java·数据库·sql
ljs6482739512 小时前
VMware 中部署 HDFS 集群环境(Hadoop 3.4.3)完整指南
大数据·hadoop·hdfs
编程(变成)小辣鸡3 小时前
如何防止接口被恶意刷?
java·后端·网络安全
大圣编程8 小时前
Java 多维数组详解
java·开发语言