在大数据时代的浪潮中,如何可靠、高效地存储海量数据是所有计算任务的前提。Hadoop 分布式文件系统(HDFS)作为 Hadoop 生态系统的核心组件,正是为了解决这一问题而生。它运行在廉价的商用硬件上,为上层应用(如 MapReduce, Spark, Hive)提供了高吞吐量的数据访问服务。本文将深入剖析 HDFS 的架构设计、核心组件工作机制以及日常运维操作。
Hadoop 相关知识与文章参考:
1. HDFS概述
1.1. HDFS简介
HDFS(Hadoop Distributed File System)作为 Hadoop 生态系统的核心组件,是一个文件系统,用于存储文件,其设计灵感来源于 Google File System (GFS),其核心设计理念是建立在“硬件故障是常态”这一假设之上的,所以,HDFS 是分布式的,由多台服务器联合起来实现其功能,且具备极强的容错能力。
1.2. HDFS使用场景
- 海量日志分析与归档(冷数据存储)。
- 作为 MapReduce、Spark 等批处理框架的数据源。
- 物联网(IoT)传感器数据的流式写入与收集。
总结一下,就是一次写入,多次读取,适合用来做分析。
1.3. HDFS优缺点
HDFS 并非适用于所有场景,它的设计具体鲜明的倾向性:
- 高容错性: 数据自动保存多个副本(默认为 3 份),某个节点宕机后,系统会自动恢复副本。
- 高吞吐量: 针对批量数据处理进行了优化,通过并行读写提供高带宽,适合“一次写入,多次读取”的模式。
- 适合海量数据存储: 支持 GB、TB 甚至 PB 级别的数据集。
- 流式数据访问: 侧重于高吞吐量而非低延迟,不适合毫秒级响应的实时查询。
但是,HDFS 也存在其局限性:
- 低延迟数据访问: 不适合对响应时间要求极高的场景(如 HBase 更适合)。
- 大量小文件: 文件系统的元数据存储在 NameNode 内存中,海量小文件会耗尽内存,限制集群扩展性。
- 并发写入与随机修改: 不支持文件的随机修改,仅支持追加(Append)操作。
1.4. HDFS 组成架构
HDFS 采用经典的 Master/Slave(主/从)架构,主要由以下三类角色组成:
- NameNode(主节点): HDFS 的“大脑”。它负责管理文件系统的命名空间(Namespace),维护文件目录树以及文件到数据块(Block)的映射关系。它不存储实际数据,只管理元数据。
- DataNode(从节点): HDFS 的“躯干”。负责实际的数据存储,处理客户端的读写请求,并执行数据块的创建、删除和复制。
- Secondary NameNode(辅助节点): 这是一个容易被误解的角色。它不是 NameNode 的热备(NameNode 挂掉后,不能代替 NameNode 提供服务),而是辅助 NameNode 进行元数据合并(Checkpoint)的节点,用于减轻 NameNode 的负担。
2. HDFS的Shell操作
2.1. 查看命令参数
./bin/hadoop fs
打印命令的所有参数,具体输出如下:
[-appendToFile <localsrc> ... <dst>]
[-cat [-ignoreCrc] <src> ...]
[-chgrp [-R] GROUP PATH...]
[-chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...]
[-chown [-R] [OWNER][:[GROUP]] PATH...]
[-copyFromLocal [-f] [-p] <localsrc> ... <dst>]
[-copyToLocal [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
[-count [-q] <path> ...]
[-cp [-f] [-p] <src> ... <dst>]
[-df [-h] [<path> ...]]
[-du [-s] [-h] <path> ...]
[-get [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
[-getmerge [-nl] <src> <localdst>]
[-help [cmd ...]]
[-ls [-d] [-h] [-R] [<path> ...]]
[-mkdir [-p] <path> ...]
[-moveFromLocal <localsrc> ... <dst>]
[-moveToLocal <src> <localdst>]
[-mv <src> ... <dst>]
[-put [-f] [-p] <localsrc> ... <dst>]
[-rm [-f] [-r|-R] [-skipTrash] <src> ...]
[-rmdir [--ignore-fail-on-non-empty] <dir> ...]
<acl_spec> <path>]]
[-setrep [-R] [-w] <rep> <path> ...]
2.2. HDFS基础命令
2.2.1. 查看目录信息
hadoop fs -ls /
输出:
Found 2 items
drwxr-xr-x - linux supergroup 0 2026-03-20 13:13 /test
drwxrwx--- - linux supergroup 0 2026-03-20 13:13 /tmp
2.2.2. 新建目录
hadoop fs -mkdir /a
hadoop fs -mkdir -p /a/b/c
2.2.3. 复制
hadoop fs -cp /a/a.txt /b/b.txt
2.2.4. 移动
hadoop fs -mv /a/a.txt /b/b.txt
2.2.5. 修改文件权限
hadoop fs -chown xm:xm /a/a.txt
hadoop fs -chmod 666 /a/a.txt
2.2.6. 显示文件内容
hadoop fs -cat /a/a.txt
输出:
aaa
aaa
aaa
2.2.7. 删除文件或文件夹
hadoop fs -rm /b/b.txt
hadoop fs -rm -r /a
2.2.8. 统计文件夹的大小
hadoop fs -du -h /
输出:
74 148 /test
361.7 K 723.3 K /tmp
2.2.9. 显示空间
hadoop fs -df -h /
输出:
Filesystem Size Used Available Use%
hdfs://hadoop102:8020 140.9 G 816 K 120.5 G 0%
2.3. HDFS上传命令
2.3.1. 上传本地文件
hadoop fs -put ./test.txt /abc/def
2.3.2. 从本地拷贝到HDFS
-copyFromLocal从本地拷贝,命令执行后,本地文件还存在,相当于复制。
hadoop fs -copyFromLocal ./test.txt /abc/def
2.3.3. 从本地移动到HDFS
-moveFromLocal从本地移动,命令执行后,本地文件消失,相当于剪切。
hadoop fs -moveFromLocal ./test.txt /abc/def
2.3.4. 追加一个文件到已存在的文件末尾
-appendToFile追加到文件,将本地文件内容,追加到 HDFS 上文件的末尾。
hadoop fs -appendToFile ./test.txt /abc/def/hhh.txt
2.4. HDFS下载命令
2.4.1. 下载HDFS文件
hadoop fs -get /abc/def/hhh.txt ./
2.4.2. 合并下载多个文件
hadoop fs -getmerge /abc/def/* ./
2.4.3. 从HDFS拷贝到本地
-copyToLocal拷贝到本地,命令执行后,HDFS 文件还存在,相当于复制。
hadoop fs -copyToLocal /abc/def/hhh.txt ./
3. HDFS客户端操作
这里只是简单演示,实际场景中,基本用不到。
- 创建一个 Maven 工程。
<groupId>org.example.hadoop</groupId>
<artifactId>HDFSClientDemo</artifactId>
<version>1.0-SNAPSHOT</version>
- 添加依赖。
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
</dependency>
- 创建
log4j.properties配置文件。
log4j.rootLogger=INFO, stdout, logfile
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=./log/HDFSClientDemo/log-all.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
- 创建测试类
HdfsClient。
package com.exmpale.hadoop.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.Test;
import java.io.IOException;
import java.net.URI;
/**
* HDFS 的客户端操作示例
*/
public class HdfsClient {
@Test
public void testMkdir() throws IOException, InterruptedException {
//todo 1.创建配置文件对象
//将配置以 key-value 形式添加到 configuration 中
Configuration config = new Configuration();
config.set("dfs.replication", "2");
//todo 2.获取 HDFS 文件系统的抽象
FileSystem fileSystem = FileSystem.get(
URI.create("hdfs://hadoop102:8020"),
config,
"xkee"
);
// todo 3.具体操作
fileSystem.mkdirs(new Path("/a/b"));
//todo 4.关闭资源
fileSystem.close();
}
}
- 其他几个 API。
//上传
fileSystem.copyFromLocalFile(new Path("D://a.txt"), new Path("/test/iptIdea.txt"));
//下载
fileSystem.copyToLocalFile(new Path("/input/bcopy.txt"), new Path("D:\\"));
//重命名
boolean rename = fileSystem.rename(new Path("/input/bcopy.txt"), new Path("/input/copyCopy.txt"));
if (rename) {
System.out.println("修改成功!");
}
4. HDFS数据流
4.1. HDFS写数据流
流程详解:
- 客户端通过 Distributed FileSystem 向 Namenode 请求上传文件,Namenode 检查目标文件是否已经存在,父目录是否存在;
- Namenode 应答可以上传;
- 客户端将待上传文件进行逻辑分块,创建输出,并请求将第一个 Block 进行上传;
- Namenode 会从元数据中返回可以上传的 Datanode 节点列表(节点个数与副本数对应);
- 客户端请求与 Datanode 列表建立连接通道(串行通道);
- Datanode 应答成功;
- 客户端以 Packet 包的形式进行文件的传输,Packet 包大小为 64k,Datanode 依次将数据拷贝到本地,同时将数据向下一节点发送;
- 当第一个 Block 块数据传输完成后,客户端再请求上传第二个 Block 块;
- 重复4,5,6,7;
- 当所有块传输完成后,客户端向 Namenode 发送传输完毕。
4.2. HDFS读数据流
流程详解:
- 客户端通过 Distributed FileSystem 向 Namenode 请求下载文件,Namenode 会检查请求下载文件是否存在等;
- Namenode 进行校验,并返回文件存在;
- 客户端请求下载第一个 Block 块;
- Namenode 通过查询元数据,找到文件所在的 Datanode 地址,然后挑选一台 Datanode(就近原则,然后随机)服务器响应客户端的读请求;
- 客户端向 Datanode1 发送读数据请求;
- Datanode1 以Packet 形式传输数据给客户端;
- 客户端以 Packet 为单位接收数据,并先放入本地缓存,然后写入目标文件;
- 客户端继续请求 Namenode 下载第二个 Block 块;
- 重复4, 5, 6;
- 客户端向 Namenode 发送传输完毕。
5. NN和2NN
5.1. 工作机制
第一阶段:NameNode 启动
- 第一次启动 NameNode 格式化后,创建Fsimage 和 Edits 文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
- 在加载完编辑日志和镜像文件后,NameNode 会进行一次在存档操作;
- 客户端对元数据进行增删改的请求。
- NameNode 记录操作日志,更新滚动日志。
- NameNode 在内存中对元数据进行增删改。
第二阶段:Secondary NameNode 工作
- Secondary NameNode 询问 NameNode 是否需要 CheckPoint。直接带回 NameNode 是否检查结果。
- Secondary NameNode 请求执行 CheckPoint。
- NameNode 滚动正在写的 Edits日志。
- 将滚动前的编辑日志和镜像文件拷贝到 Secondary NameNode。
- Secondary NameNode 加载编辑日志和镜像文件到内存,并合并。
- 生成新的镜像文件fsimage.chkpoint。
- 拷贝 fsimage.chkpoint 到 NameNode。
- NameNode 将 fsimage.chkpoint 重新命名成 fsimage。
5.2. CheckPoint时间设置
- 通常情况下,SecondaryNameNode 每隔一小时执行一次,参考
hdfs-site.xml。
<property>
<name>dfs.namenode.checkpoint.period</name>
<value>3600s</value>
</property>
- 一分钟检查一次操作次数,当操作次数达到100万时,SecondaryNameNode执行一次,参考
hdfs-site.xml。
<property>
<name>dfs.namenode.checkpoint.txns</name>
<value>1000000</value>
<description>操作动作次数</description>
</property>
<property>
<name>dfs.namenode.checkpoint.check.period</name>
<value>60s</value>
<description>1 分钟检查一次操作次数</description>
</property>
5.3. NameNode故障处理
NameNode 存储数据的目录在/opt/module/hadoop-3.1.3/data/tmp/dfs/name/*中,可以把目录中的文件手动删除,模拟元数据丢失。
当 NameNode 元数据丢失后,集群肯定是无法正常工作的,这时,可以手动使用scp命令,将 SecondaryNameNode 数据拷贝到 NameNode 中。
scp -r linux@hadoop104:/opt/module/hadoop-3.1.3/data/tmp/dfs/namesecondary/* /opt/model/hadoop-3.1.3/data/tmp/dfs/name/
重新启动 NameNode 测试一下,看是否恢复正常。
注意,补充说明一下:
- SecondaryNameNode 不是高可用(HA)方案,它只是一个辅助工具,用于防止
edits日志无限增长,并加速 NameNode 的重启过程,并不能在 NameNode 故障时自动接管服务。 - 如果想要实现 NameNode 的故障自动恢复,需要实现元数据的高可用和自动故障转移,也就是 NameNode HA(High Availability)架构。通过 Standby NameNode 实时同步 Active NameNode 的元数据,当主节点故障时,可以自动或手动快速切换到备用节点,从而最大程序的减少数据丢失和停机时间。
6. DataNode
DataNode 是实际干活的节点,其工作机制主要围绕“汇报”和“执行”展开。
6.1. 心跳机制
DataNode 默认每隔3秒向 NameNode 发送一次心跳。
- 作用:主要是告诉 NameNode “我还活着”。
- 内容:包含节点 ID、存储容量、可用空间。
- 故障判定:如果 NameNode 在特定时间(默认10分钟)内未收到某 DataNode 的心跳,就会判定该节点宕机,并启动数据恢复流程(在其他节点复制丢失的块)。
6.2. 多目录配置
DataNode 也可以配置成多个目录,每个目录存储的数据不一样。
修改hdfs-site.xml配置文件:
<property>
<name>dfs.datanode.data.dir</name>
<value>file:///${hadoop.tmp.dir}/dfs/data1,file:///${hadoop.tmp.dir}/dfs/data2</value>
</property>
最佳实践:
- 不要使用
${hadoop.tmp.dir},建议将路径写死,例如:/data/hadoop/dfs/data1。 - 物理隔离,确保配置的多个目录,分别位于不同的物理硬盘上。生产环境推荐配置示例:
file:///disk1/hadoop/dfs/data,file:///disk2/hadoop/dfs/data。
7. HDFS小文件问题
HDFS 的“小文件问题”是大数据集群中最经典、也最头疼的性能瓶颈之一。
简单来说,HDFS 的设计初衷是“大文件、高吞吐”,它并不擅长处理海量的小文件。当集群中出现大量远小于 HDFS 默认块大小(通常是 128MB)的文件时,就会引发一系列连锁反应。
为了让你更直观地理解,我们可以从危害、成因以及解决方案三个维度来拆解。
7.1. 为什么小文件是“性能杀手”
小文件问题的核心痛点在于 NameNode 的内存 和 计算引擎的效率。
NameNode 内存压力(最致命):HDFS 的架构决定了 NameNode 必须将所有文件的元数据(文件名、权限、位置、块信息等)加载到内存中。
- 数据量化: 无论文件是 1KB 还是 100GB,每个文件在 NameNode 内存中大约占用 150 字节 的元数据空间。
- 后果: 如果你有 1 亿个小文件,仅元数据就会消耗约 15GB 的内存。这会迅速耗尽 NameNode 的堆内存,导致 NameNode 响应变慢甚至宕机,进而拖垮整个集群。
计算效率低下(MapReduce/Spark):
- 任务爆炸: 在 MapReduce 或 Spark 中,通常一个 HDFS 块对应一个 Map 任务。如果你有 100 万个 1KB 的小文件,计算引擎就会启动 100 万个 Map 任务。
- 资源浪费: 任务的启动、初始化 JVM、分配容器的开销,远远超过了实际处理数据的时间。这被称为“调度风暴”,导致集群 CPU 资源被大量空转消耗。
7.2. 小文件是如何产生的
通常有以下几种常见场景:
- 流式采集:像 Flume 或日志采集程序,如果配置不当,每隔几秒就生成一个文件,一天就能产生数万个文件。
- 分区过多:在 Hive 中,如果按照“用户ID”或“分钟”进行分区,会导致每个分区目录下只有一个极小的文件。
- Reduce 阶段:如果 Reduce 任务设置过多,而数据量较少,输出结果也会是一堆小文件。
7.3. 解决方案和最佳实践
解决小文件问题通常遵循“预防为主,治理为辅”的原则。
7.3.1. 预防阶段
写入时合并。
在数据进入 HDFS 之前或写入时就进行合并,这是最根本的解决方式。
| 方案 | 描述 | 适用场景 |
|---|---|---|
| 调整采集配置 | 调整 Flume/Kafka Connect 的 Sink 配置,设置滚动策略(如按时间或大小),避免频繁生成小文件。 | 日志采集、实时数据接入 |
| SequenceFile | 将多个小文件作为 Key-Value 对(文件名作为 Key,内容作为 Value)合并存储在一个大的 SequenceFile 中。 | 适合存储大量二进制小文件,支持压缩和切分 |
| HAR (Hadoop Archive) | 类似 Linux 的 tar 包,将小文件打包成一个 HAR 文件。NameNode 只感知 HAR 文件,不感知内部的小文件。 | 冷数据归档。适合“一次写入,多次读取”的场景,不支持修改。 |
7.3.2. 治理阶段
读取/计算时优化。
如果小文件已经存在于 HDFS 中,可以通过以下方式优化:
- Hive 合并:开启 Hive 的自动合并参数,让 Hive 在 Map 端或 Reduce 端自动合并输出文件。
set hive.merge.mapfiles = true;
set hive.merge.mapredfiles = true;
# 合并后的目标大小
set hive.merge.size.per.task = 256000000;
- Spark 合并:在写入时使用
coalesce或repartition减少输出文件数量。
// 示例:将数据合并为 10 个文件再写入
df.coalesce(10).write.mode("overwrite").save("/path/to/data")
- CombineFileInputFormat:
在运行 MapReduce 任务时,使用 CombineFileInputFormat 替代默认的输入格式。它允许一个 Map 任务处理多个小文件,从而减少任务数量,但这只是逻辑上的合并,并没有改变 HDFS 底层的存储状态。
总结一下,HDFS 小文件问题的本质是元数据管理能力的瓶颈。从长期治理来看,尽量在数据采集阶段,确保写入 HDFS 的文件尽可能大(建议接近128M的块大小);再一个就是,对于历史冷数据,使用 HAR 归档或定期清理。
7.4. HAR 归档文件示例
这里简单给一个归档示例。
- 需要集群各个组件启动,包括 HDFS 和 Yarn。
- 创建几个文件,并写入点数据,模拟小文件。
# 模拟小文件
echo 111 > /opt/test/1.txt
echo 222 > /opt/test/2.txt
echo 333 > /opt/test/3.txt
- 归档小文件,并上传至 HDFS。
# 将 /opt/test 文件上传到 HDFS 根目录,命名为 input.har
hadoop archive -archiveName input.har -p /opt/test /
注意:在 HDFS 上,归档文件实际还是一个目录。
- 查看归档文件。
hadoop fs -lsr /input.har
hadoop fs -lsr har:///input.har
hadoop fs -cat har:///input.har/3.txt
- 将归档文件解归档。
# 将 input.har 中的 2.txt 解压到 HDFS 根目录
hadoop fs -cp har:///input.har/2.txt /
8. 总结
HDFS 通过主从架构、数据分块、多副本冗余以及心跳检测等机制,在廉价的硬件上构建了高可靠、高吞吐的分布式存储系统。理解 NameNode 的元数据管理、Secondary NameNode 的辅助合并以及 DataNode 的数据汇报机制,是掌握大数据底层原理的第一步。