HDFS没那么难:手把手带你搞懂分布式存储的核心逻辑

95 阅读14分钟

在大数据时代的浪潮中,如何可靠、高效地存储海量数据是所有计算任务的前提。Hadoop 分布式文件系统(HDFS)作为 Hadoop 生态系统的核心组件,正是为了解决这一问题而生。它运行在廉价的商用硬件上,为上层应用(如 MapReduce, Spark, Hive)提供了高吞吐量的数据访问服务。本文将深入剖析 HDFS 的架构设计、核心组件工作机制以及日常运维操作。

Hadoop 相关知识与文章参考:

拒绝纸上谈兵:从零开始构建你的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客户端操作

这里只是简单演示,实际场景中,基本用不到。

  1. 创建一个 Maven 工程。
<groupId>org.example.hadoop</groupId>
<artifactId>HDFSClientDemo</artifactId>
<version>1.0-SNAPSHOT</version>
  1. 添加依赖。
<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>
  1. 创建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
  1. 创建测试类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();
    }
}
  1. 其他几个 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写数据流

HDFS写数据流

流程详解:

  1. 客户端通过 Distributed FileSystem 向 Namenode 请求上传文件,Namenode 检查目标文件是否已经存在,父目录是否存在;
  2. Namenode 应答可以上传;
  3. 客户端将待上传文件进行逻辑分块,创建输出,并请求将第一个 Block 进行上传;
  4. Namenode 会从元数据中返回可以上传的 Datanode 节点列表(节点个数与副本数对应);
  5. 客户端请求与 Datanode 列表建立连接通道(串行通道);
  6. Datanode 应答成功;
  7. 客户端以 Packet 包的形式进行文件的传输,Packet 包大小为 64k,Datanode 依次将数据拷贝到本地,同时将数据向下一节点发送;
  8. 当第一个 Block 块数据传输完成后,客户端再请求上传第二个 Block 块;
  9. 重复4,5,6,7;
  10. 当所有块传输完成后,客户端向 Namenode 发送传输完毕。

4.2. HDFS读数据流

HDFS读数据流

流程详解:

  1. 客户端通过 Distributed FileSystem 向 Namenode 请求下载文件,Namenode 会检查请求下载文件是否存在等;
  2. Namenode 进行校验,并返回文件存在;
  3. 客户端请求下载第一个 Block 块;
  4. Namenode 通过查询元数据,找到文件所在的 Datanode 地址,然后挑选一台 Datanode(就近原则,然后随机)服务器响应客户端的读请求;
  5. 客户端向 Datanode1 发送读数据请求;
  6. Datanode1 以Packet 形式传输数据给客户端;
  7. 客户端以 Packet 为单位接收数据,并先放入本地缓存,然后写入目标文件;
  8. 客户端继续请求 Namenode 下载第二个 Block 块;
  9. 重复4, 5, 6;
  10. 客户端向 Namenode 发送传输完毕。

5. NN和2NN

5.1. 工作机制

NN和2NN工作机制

第一阶段:NameNode 启动

  1. 第一次启动 NameNode 格式化后,创建Fsimage 和 Edits 文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
  2. 在加载完编辑日志和镜像文件后,NameNode 会进行一次在存档操作;
  3. 客户端对元数据进行增删改的请求。
  4. NameNode 记录操作日志,更新滚动日志。
  5. NameNode 在内存中对元数据进行增删改。

第二阶段:Secondary NameNode 工作

  1. Secondary NameNode 询问 NameNode 是否需要 CheckPoint。直接带回 NameNode 是否检查结果。
  2. Secondary NameNode 请求执行 CheckPoint。
  3. NameNode 滚动正在写的 Edits日志。
  4. 将滚动前的编辑日志和镜像文件拷贝到 Secondary NameNode。
  5. Secondary NameNode 加载编辑日志和镜像文件到内存,并合并。
  6. 生成新的镜像文件fsimage.chkpoint。
  7. 拷贝 fsimage.chkpoint 到 NameNode。
  8. NameNode 将 fsimage.chkpoint 重新命名成 fsimage。

5.2. CheckPoint时间设置

  1. 通常情况下,SecondaryNameNode 每隔一小时执行一次,参考hdfs-site.xml。
<property>
  <name>dfs.namenode.checkpoint.period</name>
  <value>3600s</value>
</property>
  1. 一分钟检查一次操作次数,当操作次数达到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 归档文件示例

这里简单给一个归档示例。

  1. 需要集群各个组件启动,包括 HDFS 和 Yarn。
  2. 创建几个文件,并写入点数据,模拟小文件。
# 模拟小文件
echo 111 > /opt/test/1.txt
echo 222 > /opt/test/2.txt
echo 333 > /opt/test/3.txt
  1. 归档小文件,并上传至 HDFS。
# 将 /opt/test 文件上传到 HDFS 根目录,命名为 input.har
hadoop archive -archiveName input.har -p /opt/test /

注意:在 HDFS 上,归档文件实际还是一个目录。

  1. 查看归档文件。
hadoop fs -lsr /input.har
hadoop fs -lsr har:///input.har
hadoop fs -cat har:///input.har/3.txt
  1. 将归档文件解归档。
# 将 input.har 中的 2.txt 解压到 HDFS 根目录
hadoop fs -cp har:///input.har/2.txt /

8. 总结

HDFS 通过主从架构、数据分块、多副本冗余以及心跳检测等机制,在廉价的硬件上构建了高可靠、高吞吐的分布式存储系统。理解 NameNode 的元数据管理、Secondary NameNode 的辅助合并以及 DataNode 的数据汇报机制,是掌握大数据底层原理的第一步。