HDFS 分布式文件系统

本文合并自:03-分布式文件系统HDFS.md、04-HDFS分布式文件系统.md

HDFS(Hadoop Distributed File System)是 Hadoop 生态的存储底座,是 Google GFS 论文的开源实现。它利用由廉价硬件构成的计算机集群实现海量数据的分布式存储,为 MapReduce、Hive、Spark、HBase 等上层组件提供统一的数据访问接口。

本文内容组织:

  • 分布式文件系统与 HDFS 架构
  • 读写流程与容错机制
  • HDFS Shell 命令完整详解
  • Java API、小文件治理与排错练习

一、分布式文件系统与 HDFS 架构

1.1 传统文件系统的局限与分布式文件系统的必要性

传统的单机文件系统(ext4、xfs、NTFS)把文件存放在一台机器的本地磁盘上,在大数据场景下存在三个无法回避的瓶颈:

  1. 容量瓶颈:单机磁盘容量有限。即使做 RAID,单机可挂载的磁盘数量、总线带宽、机箱槽位都有物理上限,无法容纳 PB 级数据。
  2. 吞吐瓶颈:单块磁盘的顺序读带宽约 100~200MB/s。读取 100TB 数据即使全速顺序读也需要数天,无法满足离线分析的时效要求。
  3. 可靠性瓶颈:单机磁盘的年故障率并不低(业界经验值 2%~8%)。数据集中在一台机器上,磁盘损坏、主板损坏、机器断电都会造成数据不可用甚至永久丢失。RAID 只能容忍磁盘级故障,不能容忍整机故障。

分布式文件系统把文件切分后分布存储到多个计算机节点上,成千上万的计算机节点构成计算机集群。与之前使用多个处理器和专用高级硬件的并行化处理装置不同,目前分布式文件系统所采用的计算机集群都由普通硬件(commodity hardware)构成,这大大降低了硬件上的开销。

由此带来三个直接收益:

  • 容量可线性扩展:加机器即加容量,总容量不受单机限制。
  • 吞吐可线性扩展:一个文件的多个块分布在多台机器上,多台机器的磁盘可以并行读取,聚合带宽随节点数增长。
  • 可靠性由软件保证:通过多副本冗余,在廉价硬件上实现高可靠,不依赖昂贵的专用存储设备。

(原图:图3-1 计算机集群的基本架构)

1.2 分布式文件系统的通用结构

分布式文件系统在物理结构上由计算机集群中的多个节点构成,这些节点分为两类:

  • 一类叫"主节点"(Master Node),也称为"名称节点"(NameNode);
  • 另一类叫"从节点"(Slave Node),也称为"数据节点"(DataNode)。

(原图:图3-2 大规模文件系统的整体结构)

这种结构的核心设计思想是 元数据与数据分离:

  • 元数据(metadata):描述"数据在哪里"的数据,包括目录树、文件名、权限、文件由哪些块组成、每个块的副本存放在哪些节点上。元数据量小、访问频繁、要求强一致,因此集中由主节点管理,并常驻内存以保证低延迟。
  • 数据(data):文件的实际字节内容,按块分散存储在众多从节点的本地磁盘上。数据量大、访问以流式为主,因此分散存储并就近读取。

分离的好处是:客户端先问主节点"数据在哪",再直接与从节点交换数据,真正的数据流不经过主节点,主节点不会成为数据带宽瓶颈。

1.3 HDFS 的设计目标与假设

HDFS 要实现以下目标:

  • 兼容廉价的硬件设备:把硬件出错看作常态而非异常,用软件层的副本与自动恢复机制抵御硬件故障,从而可以使用普通 PC 服务器搭建集群。
  • 流数据读写:为满足批量数据处理的需求,HDFS 放弃了对随机读写的优化,专注于以流式方式顺序读取大块数据,追求高吞吐率。
  • 大数据集:HDFS 中的文件通常可以达到 GB 甚至 TB 级别,一个集群可以支持数以千万计的文件。
  • 简单的文件模型:采用"一次写入、多次读取"(write-once-read-many)模型。文件一旦创建、写入并关闭之后就不能再修改(只支持追加),大大简化了数据一致性问题。
  • 强大的跨平台兼容性:HDFS 采用 Java 语言实现,具有很好的跨平台兼容性,支持 JVM 的机器都可以运行 HDFS。

对应的核心假设(来自 GFS/HDFS 设计文档):

假设 说明
超大文件 文件大小以 GB/TB 计,单文件远大于单块磁盘容量
流式访问 一次读取大量连续数据,而不是频繁的小随机读
一次写入多次读取 数据写入后不再修改,只被反复分析读取
廉价硬件与容错 节点故障是常态,系统必须自动检测并恢复
高吞吐优先于低延迟 宁可单次请求慢一点,也要保证整体数据处理带宽高
移动计算比移动数据更划算 把计算任务调度到数据所在节点,减少网络传输

1.4 HDFS 的局限

HDFS 特殊的设计在实现上述优良特性的同时,也带来了一些应用局限性:

(1)不适合低延迟数据访问

HDFS 为高吞吐量的批处理而设计,而非为低延迟的交互式访问而设计。每次读写都要先经过 NameNode 的 RPC 交互获取块位置,再建立到 DataNode 的连接,单次操作的延迟在毫秒到几十毫秒级别,无法满足毫秒级以下的实时查询需求。需要低延迟随机访问时应使用 HBase 等 KV 存储。

(2)无法高效存储大量小文件

这是 HDFS 最著名的局限。原因在于 元数据占用 NameNode 内存:

  • NameNode 把整个文件系统的元数据(目录树、文件、块、块到 DataNode 的映射)全部保存在内存中,以保证元数据操作的高性能。
  • 每个文件、每个目录、每个块在 NameNode 内存中大约都要占用 150 字节 左右的对象空间。
  • 一个 1KB 的小文件与一个 128MB 的大文件,在 NameNode 中占用的元数据空间几乎相同。
  • 因此:存储 1 亿个小文件大约需要 1 亿 × 150B × (文件对象 + 块对象)≈ 30GB 以上的 NameNode 堆内存;而同样的内存如果用来管理 128MB 的大文件块,可以管理数十 PB 的数据。

注意区分两件事:

  • 占用实际磁盘空间:HDFS 的块是逻辑上的上限,不是预分配。一个 1KB 的文件放在块大小为 128MB 的 HDFS 上,在 DataNode 本地磁盘上只占约 1KB(加上少量校验文件开销),不会浪费 128MB 磁盘空间。这一点与某些传统文件系统的簇分配不同。
  • 占用元数据空间:这 1KB 的文件仍然要在 NameNode 内存里占一个文件对象 + 一个块对象的位置。所以小文件"不浪费磁盘,但浪费内存",同时还会让 MapReduce 产生大量小 task,拖慢作业。

(3)不支持任意修改文件,只支持追加

HDFS 只允许对文件执行追加(append)操作,不允许在文件的任意位置执行随机写、覆盖写。这是"一次写入多次读取"模型的直接结果,简化了多副本之间的一致性维护。

(4)不适合多用户并发写同一个文件

HDFS 中一个文件同一时刻只能有一个写入者(通过文件租约 lease 机制保证),不支持多个客户端同时向同一个文件写入。

(5)HDFS 体系结构(1.0)的局限性

HDFS 只设置唯一一个名称节点,虽然大大简化了系统设计,但也带来一些明显局限性:

  1. 命名空间限制:名称节点保存在内存中,因此名称节点能够容纳的对象(文件、块)个数受内存空间大小限制;
  2. 性能瓶颈:整个分布式文件系统的吞吐量受限于单个名称节点的吞吐量;
  3. 隔离问题:集群中只有一个名称节点、一个命名空间,无法对不同应用程序进行隔离;
  4. 集群可用性:一旦唯一的名称节点发生故障,会导致整个集群不可用。

这四点局限分别由 HDFS 联邦(Federation,解决前三点)和 HDFS HA(解决第四点)在 2.x 之后加以缓解。

1.5 核心概念

1.5.1 块(Block)

一个文件被分成多个块,以块作为存储单位。块的大小远远大于普通文件系统(普通文件系统的块通常是 4KB),这样可以最小化寻址开销。

默认块大小的版本差异(重要):

版本 默认块大小 配置项
Hadoop 1.x(及 2.x 之前) 64MB dfs.block.size(旧名)
Hadoop 2.x 128MB dfs.blocksize
Hadoop 3.x 128MB dfs.blocksize

早期教材(含本文来源之一 03-分布式文件系统HDFS.md)写的是"HDFS 默认一个块为 64MB",这是 Hadoop 1.x 时代的值;从 Hadoop 2.x 起默认值改为 128MB。生产环境常见调整为 256MB 或 512MB。

修改块大小的配置:

<!-- hdfs-site.xml -->
<property>
  <name>dfs.blocksize</name>
  <value>134217728</value>
  <description>默认块大小,单位字节。134217728 = 128MB;67108864 = 64MB;268435456 = 256MB。
  Hadoop 2.x 起也支持带单位写法,如 128m</description>
</property>

为什么块要设计得这么大(寻址开销权衡)

块大小的选择本质上是在"寻址开销"与"并行度/元数据量"之间做权衡:

  • 磁盘一次读取的总时间 = 寻址时间 + 传输时间。机械磁盘的寻址时间约 10ms,传输速率约 100MB/s。
  • 工程上的经验法则是:让寻址时间占传输时间的 1% 左右,磁盘效率最优。
  • 若寻址 10ms 要占传输时间的 1%,则传输时间应为 1s,1s 能传输约 100MB —— 这正是 128MB 这个数量级的来源。
  • 块太小:一个文件被切成大量块,寻址次数增多,磁盘时间大量花在寻道上;同时 NameNode 元数据对象激增,内存吃紧;MapReduce 的 map 任务数暴涨,调度开销大。
  • 块太大:一个块只能被一个 map 任务处理,块过大会降低并行度,使个别任务成为长尾;同时块级别故障恢复(重新复制一个块)的代价变大。

(原图:图3-5 HDFS 数据块多副本存储)

HDFS 采用抽象的块概念带来以下明显好处:

  • 支持大规模文件存储:文件以块为单位存储,一个大规模文件可被拆分成若干文件块,不同文件块可分发到不同节点上,因此文件大小不受单个节点存储容量的限制,可远远大于网络中任意节点的存储容量;
  • 简化系统设计:首先,文件块大小固定,很容易计算一个节点可存储多少文件块,大大简化了存储管理;其次,方便了元数据管理,元数据不需要和文件块一起存储,可由其他系统负责管理;
  • 适合数据备份:每个文件块都可冗余存储到多个节点上,大大提高了系统的容错性和可用性。

1.5.2 副本机制

作为分布式文件系统,为保证容错性和可用性,HDFS 采用多副本方式对数据进行冗余存储。通常一个数据块的多个副本会被分布到不同的数据节点上,例如数据块1被分别存放到数据节点 A 和 C 上,数据块2被存放在数据节点 A 和 B 上。

默认副本数为 3,配置项:

<property>
  <name>dfs.replication</name>
  <value>3</value>
  <description>默认副本因子。伪分布式环境通常设为 1。
  注意:这是客户端侧参数,文件创建时确定;修改后不影响已有文件</description>
</property>

多副本方式具有以下优点:

  1. 加快数据传输速度:多个客户端可以并发从不同副本读取同一份数据,聚合读带宽成倍提升;
  2. 容易检查数据错误:多个副本之间可以互相参照校验,配合校验和可以发现并定位损坏数据;
  3. 保证数据可靠:任意一个(甚至两个)副本所在节点故障,数据依然完整可读。

1.5.3 机架感知与副本放置策略

大型集群的机器分布在多个机架(rack)上。同一机架内的机器通过同一台接入交换机互联,带宽高、延迟低;跨机架通信需要经过汇聚交换机,带宽相对紧张。而机架级故障(交换机挂掉、机架掉电)会让整个机架的机器同时不可用。

机架感知(Rack Awareness) 就是让 NameNode 知道每台 DataNode 处于哪个机架,从而在放置副本时同时兼顾"可靠性"(跨机架)与"写入带宽"(同机架)。

Block 的副本放置策略(三副本分布规则):

  • 第一个副本:放置在上传文件的数据节点;如果是集群外提交,则随机挑选一台磁盘不太满、CPU 不太忙的节点;
  • 第二个副本:放置在与第一个副本不同机架的节点上;
  • 第三个副本:放置在与第二个副本相同机架的其他节点上(早期教材表述为"与第一个副本相同机架的其他节点",两种表述在不同版本文档中均出现;Hadoop 2.x/3.x 实际实现是"第二、三副本同处于另一个机架的不同节点");
  • 更多副本:随机选择节点,同时受"每机架副本数上限"约束。

这样安排的效果是:

  • 写入时只需跨机架传输一次(第一个副本到第二个副本),节约跨机架带宽;
  • 任何单节点故障可由同机架或跨机架副本恢复;
  • 任何单机架整体故障,数据在另一个机架仍有副本,不会丢失。

数据读取时的机架选择

HDFS 提供了 API 可以确定一个数据节点所属的机架 ID,客户端也可调用 API 获取自己所属的机架 ID。当客户端读取数据时,从名称节点获得数据块不同副本的存放位置列表,列表中包含副本所在的数据节点;可调用 API 确定客户端和这些数据节点所属的机架 ID,当发现某个数据块副本对应的机架 ID 与客户端对应的机架 ID 相同时,优先选择该副本读取;如果没有发现,则随机选择一个副本读取。

机架感知的配置(脚本方式):

<property>
  <name>net.topology.script.file.name</name>
  <value>/usr/local/hadoop/etc/hadoop/rack-topology.sh</value>
</property>
#!/bin/bash
# rack-topology.sh:输入若干 IP/主机名,输出对应机架路径,一行一个
while [ $# -gt 0 ]; do
  case $1 in
    192.168.224.1*) echo "/dc1/rack1" ;;
    192.168.224.2*) echo "/dc1/rack2" ;;
    *)              echo "/default-rack" ;;
  esac
  shift
done

验证机架拓扑是否生效:

hdfs dfsadmin -printTopology

1.5.4 命名空间与元数据

HDFS 的命名空间包含目录、文件和块。在 HDFS 1.0 体系结构中,整个 HDFS 集群只有一个命名空间,并且只有唯一一个名称节点负责管理该命名空间。HDFS 使用传统的分级文件体系,用户可以像使用普通文件系统一样创建、删除目录和文件,在目录间转移文件,重命名文件等。

元数据主要包括:

  • 命名空间树:目录与文件的层级结构、文件名、权限、所属用户与组、修改/访问时间、副本因子、块大小、配额;
  • 文件到块的映射:一个文件由哪些 Block(含 blockId、块大小、时间戳)按什么顺序组成;
  • 块到 DataNode 的映射:每个 Block 的各个副本当前存放在哪些 DataNode 上。这部分不持久化,由 DataNode 启动时上报的块报告重建。

1.6 架构组件详解

HDFS 采用主从(Master/Slave)结构模型,一个 HDFS 集群包括一个名称节点(NameNode)和若干个数据节点(DataNode)。名称节点作为中心服务器,负责管理文件系统的命名空间及客户端对文件的访问;集群中的数据节点一般一个节点运行一个数据节点进程,负责处理文件系统客户端的读/写请求,在名称节点的统一调度下进行数据块的创建、删除和复制等操作。每个数据节点的数据实际上保存在本地 Linux 文件系统中。

(原图:图3-4 HDFS 体系结构)

1.6.1 NameNode(名称节点)

在 HDFS 中,名称节点负责管理分布式文件系统的命名空间(Namespace),保存了两个核心数据结构,即 FsImage 和 EditLog:

  • FsImage 用于维护文件系统树以及文件树中所有文件和文件夹的元数据;
  • EditLog 操作日志文件记录了所有针对文件的创建、删除、重命名等操作。

名称节点还记录了每个文件中各个块所在的数据节点的位置信息。(原图:图3-3 名称节点的数据结构)

FsImage 的内容

FsImage 文件包含文件系统中所有目录和文件 inode 的序列化形式。每个 inode 是一个文件或目录元数据的内部表示,包含文件的复制等级、修改和访问时间、访问权限、块大小以及组成文件的块等信息;对于目录,则存储修改时间、权限和配额元数据。

FsImage 文件没有记录文件包含哪些块以及每个块存储在哪个数据节点,这些映射信息由名称节点保留在内存中。当数据节点加入 HDFS 集群时,数据节点会把自己所包含的块列表告知名称节点,此后会定期执行这种告知操作,以确保名称节点的块映射是最新的。

名称节点的启动流程

名称节点启动时:

  1. 将 FsImage 文件内容加载到内存中;
  2. 之后执行 EditLog 文件中的各项操作,使内存中的元数据与实际同步。存在内存中的元数据支持客户端的读操作;
  3. 一旦在内存中成功建立文件系统元数据映射,则创建一个新的 FsImage 文件和一个空的 EditLog 文件;
  4. 等待 DataNode 上报块报告,满足安全模式阈值后退出安全模式,开始提供写服务。

名称节点起来之后,HDFS 中的更新操作会重新写到 EditLog 文件中。因为 FsImage 文件一般都很大(GB 级别很常见),若所有更新都往 FsImage 添加会导致系统运行缓慢;而 EditLog 小很多,写入更快。每次执行写操作之后、在向客户端发送成功代码之前,edits 文件都需要同步更新(这是 HDFS 元数据持久性的保证点)。

元数据目录配置:

<property>
  <name>dfs.namenode.name.dir</name>
  <value>file:///data1/dfs/name,file:///data2/dfs/name</value>
  <description>FsImage 与 EditLog 的存放目录,可配置多个(逗号分隔),
  NameNode 会同步写入所有目录,实现元数据的多目录冗余备份。
  建议至少配置两个不同物理磁盘的目录,最好其中一个为 NFS 挂载</description>
</property>

名称节点运行期间 EditLog 不断变大的问题

名称节点运行期间,所有更新操作都直接写到 EditLog,久而久之 EditLog 会变得很大。这虽然对名称节点运行时没有明显影响,但当名称节点重启时,需要先将 FsImage 全部内容映像到内存,再逐条执行 EditLog 中的记录。当 EditLog 非常大时,会导致名称节点启动非常慢,而这段时间 HDFS 处于安全模式,无法对外提供写操作,影响用户使用。

解决方案是 SecondaryNameNode(第二名称节点)。

1.6.2 SecondaryNameNode(第二名称节点)

第二名称节点是 HDFS 架构的组成部分,用来保存名称节点中 HDFS 元数据信息的备份,并减少名称节点重启时间,一般单独运行在一台机器上。

checkpoint 合并流程(完整说明)

  1. SecondaryNameNode 定期与 NameNode 通信,请求其停止使用 EditLog 文件,暂时将新的写操作写到一个新文件 edit.new。这个操作瞬间完成,上层写日志函数完全感觉不到差别;
  2. SecondaryNameNode 通过 HTTP GET 方式从 NameNode 获取 FsImage 和 EditLog 文件,并下载到本地相应目录;
  3. SecondaryNameNode 将下载的 FsImage 载入内存,然后逐条执行 EditLog 中的各项更新操作,使内存中的 FsImage 保持最新。这个过程就是 EditLog 和 FsImage 文件合并;
  4. SecondaryNameNode 执行完上一步后,通过 POST 方式将新的 FsImage 文件发送到 NameNode;
  5. NameNode 用从 SecondaryNameNode 接收到的新 FsImage 替换旧 FsImage,同时用 edit.new 替换 EditLog 文件。通过这个过程 EditLog 就变小了。

触发 checkpoint 的配置:

<property>
  <name>dfs.namenode.checkpoint.period</name>
  <value>3600</value>
  <description>两次 checkpoint 的最大时间间隔,单位秒,默认 1 小时</description>
</property>
<property>
  <name>dfs.namenode.checkpoint.txns</name>
  <value>1000000</value>
  <description>EditLog 事务数达到该值即触发 checkpoint,默认 100 万</description>
</property>
<property>
  <name>dfs.namenode.checkpoint.dir</name>
  <value>file:///data1/dfs/namesecondary</value>
</property>

关键点:SecondaryNameNode 不是 NameNode 的热备

这是最容易被误解的一点,必须明确:

  • SecondaryNameNode 不能在 NameNode 挂掉后自动接管服务,它没有 NameNode 的完整实时状态(尤其没有块到 DataNode 的映射,也没有最后一次 checkpoint 之后的 EditLog);
  • 它的唯一职责是周期性地合并 FsImage 与 EditLog,缩短 NameNode 下次启动的时间;
  • 它保存的元数据副本可以在 NameNode 元数据损坏时用于手工恢复,但会丢失最后一次 checkpoint 之后的所有变更;
  • 真正的热备是 HDFS HA 中的 Standby NameNode(配合 JournalNode 与 ZKFC),Standby 会实时消费 JournalNode 上的 EditLog,并同时承担 checkpoint 职责 —— 因此 开启 HA 后不再需要 SecondaryNameNode。

名称节点元数据出错时的恢复

名称节点保存了所有元数据信息,其中最核心的两大数据结构是 FsImage 和 EditLog,如果这两个文件损坏,整个 HDFS 实例将失效。因此,HDFS 设置了备份机制,把这些核心文件同步复制到备份服务器 SecondaryNameNode 上。当名称节点出错时,可根据备份服务器 SecondaryNameNode 中的 FsImage 和 EditLog 数据进行恢复。

# 用 SecondaryNameNode 的目录数据恢复 NameNode(先停掉 NameNode)
# 方式一:把 namesecondary 目录内容拷贝到 dfs.namenode.name.dir 后启动
# 方式二:以 importCheckpoint 方式启动
hdfs namenode -importCheckpoint

1.6.3 DataNode(数据节点)

数据节点是分布式文件系统 HDFS 的工作节点,负责数据的存储和读取,会根据客户端或名称节点的调度进行数据的存储和检索,并向名称节点定期发送自己所存储的块的列表。每个数据节点中的数据会保存在各自节点的本地 Linux 文件系统中。

块的本地存储方式

DataNode 把每个块存成本地文件系统上的两个文件:

  • blk_<blockId>:块的实际数据;
  • blk_<blockId>_<generationStamp>.meta:该块的校验和文件(checksum,含每 512 字节数据对应的 CRC32C 校验值)。

存储目录由以下配置指定,可以配置多个目录以使用多块磁盘:

<property>
  <name>dfs.datanode.data.dir</name>
  <value>file:///data1/dfs/data,file:///data2/dfs/data,file:///data3/dfs/data</value>
  <description>DataNode 存放块的本地目录,每块数据盘配一个目录,不要做 RAID0,
  由 HDFS 自己负责跨盘分布</description>
</property>

心跳机制(heartbeat)

DataNode 周期性向 NameNode 发送心跳,报告自身存活状态、剩余容量、正在传输的连接数等,同时接收 NameNode 通过心跳响应下发的指令(复制块、删除块、重新注册、关闭等)。

<property>
  <name>dfs.heartbeat.interval</name>
  <value>3</value>
  <description>心跳间隔,单位秒,默认 3s</description>
</property>
<property>
  <name>dfs.namenode.heartbeat.recheck-interval</name>
  <value>300000</value>
  <description>NameNode 检查 DataNode 死亡的重检间隔,单位毫秒,默认 5 分钟</description>
</property>

块报告(block report)

DataNode 启动时以及之后周期性地向 NameNode 发送完整块报告,列出自己持有的所有块。NameNode 据此重建"块 → DataNode"映射。此外还有增量块报告(IBR),在块新增或删除时即时上报。

<property>
  <name>dfs.blockreport.intervalMsec</name>
  <value>21600000</value>
  <description>完整块报告间隔,单位毫秒,默认 6 小时</description>
</property>

1.6.4 客户端

客户端是用户操作 HDFS 最常用的方式,HDFS 部署时都提供了客户端。HDFS 客户端是一个库,暴露了 HDFS 文件系统接口,这些接口隐藏了 HDFS 实现中的大部分复杂性。严格来说,客户端并不算是 HDFS 的一部分。

客户端支持打开、读取、写入等常见操作,并提供了类似 Shell 的命令行方式访问 HDFS 数据。此外,HDFS 也提供了 Java API,作为应用程序访问文件系统的客户端编程接口。

客户端在读写中承担的实际工作很重:文件切块、选择 DataNode 建立 pipeline、计算与校验 checksum、失败重试与副本切换,都由客户端库完成。

1.7 通信协议

HDFS 是部署在集群上的分布式文件系统,很多数据需要通过网络传输。所有 HDFS 通信协议都构建在 TCP/IP 协议基础之上:

  • 客户端通过一个可配置的端口向名称节点主动发起 TCP 连接,并使用客户端协议与名称节点交互;
  • 名称节点和数据节点之间使用数据节点协议交互;
  • 客户端与数据节点的交互通过 RPC(Remote Procedure Call,远程过程调用)实现。

在设计上,名称节点不会主动发起 RPC,而是响应来自客户端和数据节点的 RPC 请求。NameNode 对 DataNode 的指令都是"搭载"在心跳响应里下发的。

常用端口的版本差异(重要)

用途 Hadoop 2.x Hadoop 3.x
NameNode RPC(客户端访问,fs.defaultFS) 8020(教程中常配 9000) 8020(常配 9820)
NameNode Web UI(HTTP) 50070 9870
NameNode Web UI(HTTPS) 50470 9871
SecondaryNameNode HTTP 50090 9868
DataNode 数据传输 50010 9866
DataNode IPC 50020 9867
DataNode HTTP 50075 9864
JournalNode RPC / HTTP 8485 / 8480 8485 / 8480

早期教材中的 http://[NameNodeIP]:50070 是 Hadoop 2.x 的 Web UI 地址;在 Hadoop 3.x 中应访问 http://[NameNodeIP]:9870。

1.8 HDFS 高可用与联邦简述

  • HDFS HA(High Availability):部署两个(3.x 支持多个)NameNode,一个 Active 一个 Standby。两者通过奇数个 JournalNode 组成的共享日志同步 EditLog;DataNode 同时向两个 NameNode 上报心跳与块报告;ZKFC(ZKFailoverController)借助 ZooKeeper 完成故障检测与自动切换,并通过 fencing 防止脑裂。HA 下 Standby 兼任 checkpoint 角色,因此不再部署 SecondaryNameNode。
  • HDFS Federation(联邦):部署多个相互独立的 NameNode/NameSpace,各自管理命名空间的一部分(如 /user、/hive),共用同一组 DataNode(每个 NameSpace 使用独立的 Block Pool)。它解决的是"单 NameNode 内存受限、吞吐受限、无法隔离"三个问题,但不解决单点故障(每个 NameNode 仍需各自做 HA)。通常配合 ViewFS 或 RBF(Router-Based Federation) 对客户端屏蔽多命名空间细节。

详见 16-02-Hadoop架构与安装部署.md

二、读写流程与容错机制

HDFS 提供了 Java API 用于读写文件。理解读写流程的关键在于:元数据交互走 NameNode,数据流量直连 DataNode。

相关类说明:

  • FileSystem 是通用文件系统的抽象基类,可被分布式文件系统继承,所有可能使用 Hadoop 文件系统的代码都要使用这个类;
  • Hadoop 为 FileSystem 抽象类提供了多种具体实现,DistributedFileSystem 就是 FileSystem 在 HDFS 文件系统中的具体实现;
  • FileSystem 的 open() 方法返回输入流 FSDataInputStream 对象,在 HDFS 中具体输入流为 DFSInputStream;create() 方法返回输出流 FSDataOutputStream 对象,在 HDFS 中具体输出流为 DFSOutputStream。
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://localhost:9000");
conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
FileSystem fs = FileSystem.get(conf);
FSDataInputStream in = fs.open(new Path(uri));
FSDataOutputStream out = fs.create(new Path(uri));

2.1 HDFS 写入流程

FSDataOutputStream 封装了 DFSOutputStream,写数据的流程如下:

  1. 客户端通过 FileSystem.get(conf) 获取实例,调用 create() 创建文件,并通过 RPC 远程调用名称节点,在文件系统的命名空间中新建一个文件;名称节点会执行一些检查(文件是否已存在、父目录是否存在、客户端是否有写权限等)。检查通过则在 EditLog 中记录一条创建记录并返回,同时为该文件授予客户端一个租约(lease),保证同一时刻只有该客户端能写这个文件;检查不通过则抛出 IOException(如 FileAlreadyExistsException、AccessControlException)。

  2. 数据被分成一个个分包(packet,默认 64KB,内含若干 512 字节的 chunk 及其 CRC 校验和),分包被放入 DFSOutputStream 对象的内部队列(data queue)。当开始写第一个块时,DFSOutputStream 通过 addBlock() 向名称节点申请保存数据块的若干数据节点(数量 = 副本数,顺序已按网络拓扑排好),这些数据节点形成一个数据流管道(pipeline)。

  3. 客户端与 pipeline 中第一个 DataNode 建立连接,第一个 DataNode 连第二个,第二个连第三个,形成串联通道。队列中的分包被发往数据流管道中的第一个数据节点,第一个数据节点在本地落盘的同时把数据包转发到第二个节点,第二个节点同样边写边转发到第三个,依此类推,形成"流水线复制(pipeline replication)"。

    流水线的意义:客户端只需付出一份网络出口带宽,副本间的复制由 DataNode 之间接力完成,且写盘与转发并行流水进行,因此写 3 副本的耗时接近写 1 副本,而不是 3 倍。

  4. 为保证节点数据准确,接收到数据的数据节点要向发送者发送"确认包(ack)";确认包沿着数据流管道逆流而上,经过各个节点最终到达客户端。客户端维护一个 ack queue(确认队列)存放已发送但未被确认的 packet;客户端收到应答时,将对应的分包从内部队列(ack queue)移除。

  5. 一个块写满(达到 dfs.blocksize)后,pipeline 关闭,客户端再次向 NameNode 申请下一个块的 DataNode 列表,建立新的 pipeline,重复步骤 2~4,逐块确认。

  6. 全部数据写完后,客户端调用 close() 刷出剩余 packet 并等待全部 ack,DFSOutputStream 调用 ClientProtocal.complete() 方法通知名称节点关闭文件。NameNode 等待块达到最小副本数(dfs.namenode.replication.min,默认 1)后返回成功,剩余副本由 NameNode 在后台异步复制补齐。

写入示意(3 副本)

Client --packet--> DN1 --packet--> DN2 --packet--> DN3
       <---ack---      <---ack---      <---ack---

写入过程中 DataNode 失败的处理

  1. 关闭当前 pipeline;
  2. 把 ack queue 中所有未确认的 packet 重新放回 data queue 头部,保证不丢数据;
  3. 为当前正在写的块申请一个新的版本时间戳(generation stamp),使故障节点上的残缺副本在恢复后被识别为过期块并被删除;
  4. 从 pipeline 中剔除故障节点,用剩余的正常节点重建 pipeline,继续写入;
  5. 只要剩余副本数不少于 dfs.namenode.replication.min(默认 1),写入就继续进行,不向客户端报错;不足的副本由 NameNode 后续异步复制补齐。

相关容错开关(小集群常需调整):

<property>
  <name>dfs.client.block.write.replace-datanode-on-failure.enable</name>
  <value>true</value>
</property>
<property>
  <name>dfs.client.block.write.replace-datanode-on-failure.policy</name>
  <value>DEFAULT</value>
  <description>节点数少于 3 的小集群若频繁写失败,可设为 NEVER</description>
</property>

写入文件的示例代码

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;

public class Chapter3 {
    public static void main(String[] args) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://localhost:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            byte[] buff = "Hello world".getBytes(); // 要写入的内容
            String filename = "test"; // 要写入的文件名
            FSDataOutputStream os = fs.create(new Path(filename));
            os.write(buff,0,buff.length);
            System.out.println("Create:"+ filename);
            os.close();
            fs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.2 HDFS 读取流程

FSDataInputStream 封装了 DFSInputStream,读数据的流程如下:

  1. 客户端通过 FileSystem.get(conf) 获取文件系统实例,调用 open() 获得 FSDataInputStream;
  2. 输入流通过 ClientProtocal.getBlockLocations() 远程调用名称节点,获得文件开始部分数据块的位置;对于该数据块,名称节点返回保存该数据块的所有数据节点地址,并根据距离客户端远近进行排序(按网络拓扑:同节点 → 同机架 → 同数据中心 → 跨数据中心)。NameNode 一次只返回一批块的位置(默认预取 10 个块),不会把超大文件的全部块位置一次性返回;
  3. 客户端获得输入流后调用 read() 函数开始读取数据,输入流根据排序结果选择距离客户端最近的数据节点建立连接并读取数据;
  4. 数据从数据节点读到客户端,读取过程中客户端对每个 chunk 进行校验和验证(与 .meta 文件中的 CRC 比对);当该数据块读取完毕时,FSDataInputStream 关闭与该数据节点的连接;
  5. 通过 ClientProtocal.getBlockLocations() 查找下一个数据块,继续读取,直到文件末尾;最后调用 close() 关闭输入流。

从上层看,客户端把分散在多个 DataNode 上的块顺序拼接成一个连续的字节流返回给应用;MapReduce 等框架则会把不同的块分配给不同的 task 并行读取。

读取失败时的处理

  • 若与某 DataNode 建立连接失败或读取中断,客户端记下该节点并从块位置列表中选择下一个最近的副本重试;
  • 若校验和验证失败,说明该副本数据损坏。客户端会向 NameNode 报告这个损坏块,然后换用另一个副本读取;NameNode 将该副本标记为损坏,安排从健康副本重新复制,并在复制完成后删除损坏副本;
  • 这些重试对上层应用是透明的,应用只有在所有副本都不可用时才会收到异常。

短路读取(short-circuit read)简述

当客户端与目标 DataNode 位于同一台机器时,正常流程仍要经过 TCP 与 DataNode 进程转发数据,产生额外的拷贝与上下文切换。短路读取允许客户端通过 Unix Domain Socket 从 DataNode 获取块文件的文件描述符,然后直接读取本地磁盘文件,绕过 DataNode 的数据转发,显著提升本地读性能(对 HBase RegionServer 尤其重要)。

<property>
  <name>dfs.client.read.shortcircuit</name>
  <value>true</value>
</property>
<property>
  <name>dfs.domain.socket.path</name>
  <value>/var/lib/hadoop-hdfs/dn_socket</value>
  <description>该路径的父目录必须存在且权限受控(如 750,属主 hdfs)</description>
</property>
<property>
  <name>dfs.client.read.shortcircuit.streams.cache.size</name>
  <value>4096</value>
</property>

读取文件的示例代码

import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;

public class Chapter3 {
    public static void main(String[] args) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://localhost:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            Path file = new Path("test");
            FSDataInputStream getIt = fs.open(file);
            BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
            String content = d.readLine(); // 读取文件一行
            System.out.println(content);
            d.close(); // 关闭文件
            fs.close(); // 关闭hdfs
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.3 容错机制

HDFS 具有较高容错性,可以兼容廉价硬件,它把硬件出错看作一种常态(而非异常),并设计了相应机制检测数据错误和自动恢复,主要包括名称节点出错、数据节点出错和数据出错三种情形。

2.3.1 DataNode 故障:检测、超时判定与副本自动复制

每个数据节点会定期向名称节点发送"心跳"信息,报告自己的状态。当数据节点发生故障或网络断网时,名称节点收不到一些数据节点的心跳,这些数据节点会被标记为"宕机"(dead),节点上的所有数据被标记为"不可读",名称节点不会再给它们发送任何 I/O 请求。

超时判定公式:

timeout = 2 × dfs.namenode.heartbeat.recheck-interval + 10 × dfs.heartbeat.interval
        = 2 × 300000ms + 10 × 3s
        = 600s + 30s = 630s ≈ 10.5 分钟

即默认配置下,DataNode 失联约 10 分 30 秒 后才被判定为 dead。这个值故意设得比较长,以避免因短暂的 GC 停顿或网络抖动就触发大规模副本复制风暴。

此时可能因一些数据节点不可用而导致一些数据块的副本数量小于冗余因子。名称节点会定期检查,一旦发现某个数据块副本数量小于冗余因子,就启动数据冗余复制,为它生成新副本。HDFS 与其他分布式文件系统的最大区别就是可以调整冗余数据的位置。

复制限速相关配置:

<property>
  <name>dfs.namenode.replication.work.multiplier.per.iteration</name>
  <value>2</value>
  <description>每个心跳周期每个 DataNode 分派的复制任务倍数,恢复慢时可调大</description>
</property>
<property>
  <name>dfs.namenode.replication.max-streams</name>
  <value>2</value>
</property>

2.3.2 数据块损坏:校验和与 .meta 文件

网络传输和磁盘错误等因素都会造成数据错误。客户端读取到数据后,会采用校验算法(早期教材提到 md5 和 sha1;HDFS 实际使用 CRC32C)对数据块进行校验,以确定读取到正确的数据。

在文件被创建时,客户端就会对每一个文件块进行信息摘录(每 io.bytes.per.checksum,默认 512 字节计算一个校验和),并把这些信息写入同一个路径的隐藏文件(即 DataNode 上的 blk_xxx_yyy.meta)里。客户端读取文件时,会先读取该信息文件,然后利用该信息文件对每个读取的数据块进行校验;如果校验出错,客户端会请求到另一个数据节点读取该文件块,并向名称节点报告该文件块有错误,名称节点会定期检查并重新复制这个块。

除了读时校验,DataNode 还运行 DataBlockScanner 后台线程,周期性(默认 3 周,dfs.datanode.scan.period.hours=504)扫描本地所有块并校验,主动发现"静默损坏"(bit rot)。

2.3.3 NameNode 单点故障与元数据备份

  • 多目录备份:dfs.namenode.name.dir 配置多个目录(建议跨物理磁盘,最好包含一个 NFS 挂载点),NameNode 同步写入所有目录,任一目录损坏都可用另一个恢复;
  • SecondaryNameNode:保存 checkpoint 后的 FsImage 备份,可用于手工恢复(会丢失最后一次 checkpoint 之后的变更);
  • HA:Active/Standby 双 NameNode + JournalNode 共享 EditLog + ZKFC 自动切换,这才是真正解决单点故障的方案。

2.3.4 机架故障

三副本策略保证副本不会全部落在同一机架,因此单机架整体掉电/交换机故障不会导致数据丢失。机架恢复或被下线后,NameNode 会重新平衡副本的机架分布(fsck 报告中的 Mis-replicated blocks 就是指机架分布不满足策略的块)。

2.3.5 安全模式(safemode)

NameNode 启动时会进入安全模式,此时文件系统只读:可以 ls、cat、get,但不能 put、mkdir、rm、setrep 等任何修改元数据的操作。

触发与退出条件:

  • 启动时自动进入,等待 DataNode 上报块报告;
  • 当"已上报且满足最小副本数的块"占"总块数"的比例达到 dfs.namenode.safemode.threshold-pct(默认 0.999,即 99.9%),并且存活 DataNode 数达到 dfs.namenode.safemode.min.datanodes(默认 0)后,再等待 dfs.namenode.safemode.extension(默认 30000ms,即 30 秒)即自动退出;
  • 运行期间若因块丢失导致该比例跌破阈值,NameNode 会重新进入安全模式,这通常是集群出问题的信号。
<property>
  <name>dfs.namenode.safemode.threshold-pct</name>
  <value>0.999f</value>
  <description>退出安全模式所需的块上报比例。设为 0 表示不等待、启动即退出;
  设为大于 1 的值表示永不自动退出(需手工 leave)</description>
</property>
<property>
  <name>dfs.namenode.safemode.extension</name>
  <value>30000</value>
</property>
<property>
  <name>dfs.namenode.safemode.min.datanodes</name>
  <value>0</value>
</property>
hdfs dfsadmin -safemode get     # 查看当前状态
hdfs dfsadmin -safemode enter   # 手工进入(做维护、备份元数据前使用)
hdfs dfsadmin -safemode leave   # 手工强制离开
hdfs dfsadmin -safemode wait    # 阻塞等待直到退出安全模式,常用于脚本中

2.4 数据完整性与均衡

2.4.1 Balancer:节点间数据均衡

新增节点、下线节点或长期不均匀写入后,各 DataNode 的磁盘使用率会出现差异,导致热点节点成为瓶颈。Balancer 是一个独立运行的工具,把数据块从使用率高的节点搬到使用率低的节点。

原理:计算集群平均使用率 avg,若某节点使用率与 avg 的偏差超过阈值 threshold(默认 10%),则该节点被判定为 over-utilized 或 under-utilized,Balancer 在两者之间迁移块,同时保证迁移不破坏副本放置策略(不减少机架数、不让两个副本落到同一节点)。

# 启动 balancer,阈值 5%(各节点使用率与集群平均值偏差不超过 5%)
hdfs balancer -threshold 5

# 只均衡指定节点
hdfs balancer -include dn1,dn2,dn3
hdfs balancer -exclude dn9

# 3.x 支持按运行时间限制
hdfs balancer -threshold 10 -idleiterations 5

# 限速:设置 balancer 使用的最大带宽(字节/秒),此处 10MB/s,无需重启即时生效
hdfs dfsadmin -setBalancerBandwidth 10485760
<property>
  <name>dfs.datanode.balance.bandwidthPerSec</name>
  <value>10485760</value>
  <description>每个 DataNode 用于均衡的带宽上限,默认 1MB/s(3.x 默认 10MB/s),
  过小会导致均衡极慢,过大会抢占业务带宽</description>
</property>

2.4.2 DiskBalancer:单节点内多磁盘均衡

Balancer 只解决节点之间的不均衡;如果同一个 DataNode 上换了新盘,或某块盘接近写满而其他盘空闲,需要 DiskBalancer(Hadoop 3.x 引入,2.x 部分发行版回合)。

<property>
  <name>dfs.disk.balancer.enabled</name>
  <value>true</value>
</property>
# 1. 生成计划(产出 <host>.plan.json)
hdfs diskbalancer -plan dn1.example.com

# 2. 执行计划
hdfs diskbalancer -execute /system/diskbalancer/xxx/dn1.example.com.plan.json

# 3. 查询进度
hdfs diskbalancer -query dn1.example.com

# 4. 取消
hdfs diskbalancer -cancel /system/diskbalancer/xxx/dn1.example.com.plan.json

# 生成集群整体报告 / 指定节点报告
hdfs diskbalancer -report -top 5
hdfs diskbalancer -report -node dn1.example.com

2.5 回收站机制

HDFS 的 -rm 默认行为取决于是否启用回收站。启用后删除的文件会被移动到用户目录下的 .Trash 中,超过保留期才真正物理删除。

<!-- core-site.xml -->
<property>
  <name>fs.trash.interval</name>
  <value>1440</value>
  <description>回收站中文件的保留时间,单位分钟。0 表示禁用回收站(默认值 0)。
  1440 = 1 天。该值在服务端与客户端都生效:服务端负责过期清理,
  客户端据此决定 rm 是否走 Trash</description>
</property>
<property>
  <name>fs.trash.checkpoint.interval</name>
  <value>60</value>
  <description>回收站检查点创建间隔,单位分钟。应小于或等于 fs.trash.interval;
  为 0 时取 fs.trash.interval 的值。每次 checkpoint 会把 Current 目录
  重命名为时间戳目录,并删除超期的时间戳目录</description>
</property>

.Trash 目录结构:

/user/<用户名>/.Trash/Current/<被删文件的原始完整路径>
/user/<用户名>/.Trash/<yymmddHHMM>/<被删文件的原始完整路径>   # checkpoint 后的快照目录
# 删除文件(进回收站)
hadoop fs -rm /user/hadoop/data.txt
# 输出类似:
# INFO fs.TrashPolicyDefault: Moved: 'hdfs://ns/user/hadoop/data.txt' to
# trash at: hdfs://ns/user/hadoop/.Trash/Current/user/hadoop/data.txt

# 跳过回收站,直接物理删除(不可恢复,慎用)
hadoop fs -rm -skipTrash /user/hadoop/data.txt
hadoop fs -rm -r -skipTrash /user/hadoop/tmpdir

# 立即触发回收站 checkpoint 并清理过期数据
hadoop fs -expunge

# 查看回收站占用
hadoop fs -du -h /user/hadoop/.Trash

误删恢复的完整步骤

# 1. 确认回收站已启用(fs.trash.interval > 0),且删除时未加 -skipTrash
hdfs getconf -confKey fs.trash.interval

# 2. 在回收站中定位文件
hadoop fs -ls -R /user/hadoop/.Trash/
# 或直接搜索文件名
hadoop fs -ls -R /user/hadoop/.Trash/ | grep data.txt

# 3. 用 mv 移回原位置(原目录需存在)
hadoop fs -mkdir -p /user/hadoop
hadoop fs -mv /user/hadoop/.Trash/Current/user/hadoop/data.txt /user/hadoop/

# 4. 如果已被 checkpoint,路径在时间戳目录下
hadoop fs -mv /user/hadoop/.Trash/2608221030/user/hadoop/data.txt /user/hadoop/

# 5. 验证
hadoop fs -ls -h /user/hadoop/data.txt

注意:-skipTrash 删除、以及 Java API 的 fs.delete()(不走 Trash)都无法通过回收站恢复,此时只能依赖快照或外部备份。

2.6 快照(Snapshot)

快照是某个目录在某一时刻的只读镜像,创建是 O(1) 操作(只记录元数据差异,不复制数据块),几乎不占额外空间,非常适合防误删与做一致性备份。

# 1. 对目录启用快照功能(需要超级用户权限)
hdfs dfsadmin -allowSnapshot /user/hadoop/important
# 关闭快照功能(需先删除该目录下所有快照)
hdfs dfsadmin -disallowSnapshot /user/hadoop/important

# 2. 创建快照(不指定名称时默认使用时间戳 s<yyyyMMdd-HHmmss.SSS>)
hdfs dfs -createSnapshot /user/hadoop/important snap1
hdfs dfs -createSnapshot /user/hadoop/important

# 3. 查看快照(快照位于隐藏目录 .snapshot 下)
hdfs dfs -ls /user/hadoop/important/.snapshot
hdfs dfs -ls /user/hadoop/important/.snapshot/snap1
# 列出所有可快照目录
hdfs lsSnapshottableDir

# 4. 重命名快照
hdfs dfs -renameSnapshot /user/hadoop/important snap1 snap1-baseline

# 5. 删除快照
hdfs dfs -deleteSnapshot /user/hadoop/important snap1-baseline

# 6. 比较两个快照的差异(. 表示当前状态)
hdfs snapshotDiff /user/hadoop/important snap1 snap2
hdfs snapshotDiff /user/hadoop/important snap1 .

snapshotDiff 输出符号含义:

符号 含义
+ 新增的文件/目录
- 删除的文件/目录
M 被修改的文件/目录(含元数据变更)
R 重命名

用快照恢复误删数据

# 场景:/user/hadoop/important/orders.csv 被误删(且被 -skipTrash)
# 1. 确认快照中存在该文件
hdfs dfs -ls /user/hadoop/important/.snapshot/snap1/orders.csv

# 2. 从快照中拷回(快照目录只读,必须用 cp 而不是 mv)
hdfs dfs -cp /user/hadoop/important/.snapshot/snap1/orders.csv \
            /user/hadoop/important/orders.csv

# 3. 恢复整个目录
hdfs dfs -cp -p /user/hadoop/important/.snapshot/snap1/subdir \
               /user/hadoop/important/

# 4. 大规模恢复可用 distcp 并行拷贝
hadoop distcp /user/hadoop/important/.snapshot/snap1 /user/hadoop/important_restore

distcp 及跨集群数据迁移的完整用法 > 详见 16-02-Hadoop架构与安装部署.md

三、HDFS Shell 命令完整详解

Hadoop 提供了关于 HDFS 在 Linux 操作系统上进行文件操作的常用 Shell 命令以及 Java API,同时还可以利用 Web 界面查看和管理 Hadoop 文件系统。

备注:Hadoop 安装成功后已经包含 HDFS 和 MapReduce,不需要额外安装;而 HBase 等其他组件则需要另外下载安装。

在学习 HDFS 编程实践前,需要先启动 Hadoop。

3.1 命令的三种形式与区别

HDFS 有很多 Shell 命令,其中 fs 命令是最常用的命令。利用该命令可以查看 HDFS 文件系统的目录结构、上传和下载数据、创建文件等。命令用法为:

hadoop fs [genericOptions] [commandOptions]

Hadoop 中有三种 Shell 命令方式:

命令 适用范围 状态
hadoop fs 适用于任何不同的文件系统,如本地文件系统(file://)、HDFS(hdfs://)、S3(s3a://)等 通用,推荐用于跨文件系统操作
hadoop dfs 只能适用于 HDFS 文件系统 已废弃(deprecated),执行时会打印 DEPRECATED: Use of this script to execute hdfs command is deprecated. 警告
hdfs dfs 跟 hadoop dfs 作用一样,也只能适用于 HDFS 文件系统 推荐用于 HDFS 专用操作

结论:操作 HDFS 用 hdfs dfs,需要在本地与 HDFS 之间通用时用 hadoop fs,不要再用 hadoop dfs。两者的子命令参数完全一致,本文示例中 hadoop fs 与 hdfs dfs 可互换。

路径 URI 的完整写法

# 1. 完整 URI(明确指定 NameNode 地址与端口,跨集群操作时必须用)
hadoop fs -ls hdfs://192.168.224.10:9000/user/hadoop     # Hadoop 2.x 常见端口
hadoop fs -ls hdfs://namenode:8020/user/hadoop           # 2.x 默认 RPC 端口
hadoop fs -ls hdfs://namenode:9820/user/hadoop           # 3.x 默认 RPC 端口
hadoop fs -ls hdfs://mycluster/user/hadoop               # HA 集群用 nameservice 名

# 2. 绝对路径(省略 scheme 与主机,使用 core-site.xml 中 fs.defaultFS 的值)
hadoop fs -ls /user/hadoop

# 3. 相对路径(相对于当前用户的家目录 /user/<当前用户名>)
hadoop fs -ls tempDir          # 等价于 /user/<user>/tempDir
hadoop fs -ls .                # 等价于 /user/<user>

# 4. 显式访问本地文件系统
hadoop fs -ls file:///home/hadoop
hadoop fs -cat file:///etc/hosts

# 查看当前 fs.defaultFS
hdfs getconf -confKey fs.defaultFS
<!-- core-site.xml -->
<property>
  <name>fs.defaultFS</name>
  <value>hdfs://192.168.224.10:9000</value>
</property>

3.2 文件与目录操作命令逐项详解

先看四个最基础的命令原型:

# 显示 <path> 指定文件的详细信息
hadoop fs -ls <path>

# 创建 <path> 指定的文件夹
hadoop fs -mkdir <path>

# 将 <path> 指定文件的内容输出到标准输出(stdout)
hadoop fs -cat <path>

# 将本地源文件 <localsrc> 复制到路径 <dst> 指定的文件或文件夹
hadoop fs -copyFromLocal <localsrc> <dst>

入门操作示例(对应源讲义的实验步骤)

  • 利用 mkdir 命令在计算机上创建 HDFS 目录 tempDir;
  • 利用 ls 命令显示 127.0.0.1 机器上现有的 HDFS 文件;
  • 利用 copyFromLocal 命令从本地计算机上传 tempfile 文件夹里的所有文件到 127.0.0.1 计算机的 tempDir 文件夹下;
  • 利用 ls 命令显示 127.0.0.1 计算机中 tempDir 文件夹里的所有文件;
  • 利用 cat 命令打印出 127.0.0.1 计算机中 tempDir 文件夹里所有文件的内容。
hadoop fs -mkdir /tempDir
hadoop fs -ls /
hadoop fs -copyFromLocal /home/administrator/tempfile/* /tempDir
hadoop fs -ls /tempDir
hadoop fs -cat /tempDir/*

3.2.1 -ls:列出目录内容

hadoop fs -ls /                  # 列出根目录
hadoop fs -ls -R /user           # 递归列出所有子目录与文件
hadoop fs -ls -h /user/hadoop    # 文件大小人类可读(K/M/G)
hadoop fs -ls -d /user/hadoop    # 只显示目录本身而非其内容
hadoop fs -ls -t /user/hadoop    # 按修改时间排序(新的在前)
hadoop fs -ls -r /user/hadoop    # 反序
hadoop fs -ls -S /user/hadoop    # 按文件大小排序
hadoop fs -ls -u /user/hadoop    # 显示访问时间而非修改时间
hadoop fs -ls "/user/hadoop/*.log"   # 支持通配符

输出示例与各列含义:

-rw-r--r--   3 hadoop supergroup     1048576 2026-08-22 10:30 /user/hadoop/a.log
drwxr-xr-x   - hadoop supergroup           0 2026-08-22 09:15 /user/hadoop/logs
[1]        [2]   [3]      [4]            [5]        [6]        [7]
列 含义
[1] 权限位。首字符 - 表示文件、d 表示目录;其后为 owner/group/other 的 rwx 权限。若结尾有 + 表示该路径设置了 ACL
[2] 副本数。目录没有副本概念,显示 -
[3] 所属用户(owner)
[4] 所属组(group),默认 supergroup
[5] 文件大小(字节)。目录恒为 0,因为 HDFS 目录不占数据空间
[6] 最后修改时间
[7] 完整路径

3.2.2 -mkdir:创建目录

hadoop fs -mkdir /data                    # 父目录必须已存在,否则报 No such file or directory
hadoop fs -mkdir -p /data/2026/08/22      # -p 递归创建父目录,且目录已存在时不报错
hadoop fs -mkdir /a /b /c                 # 一次创建多个

3.2.3 上传:-put / -copyFromLocal / -moveFromLocal

三者都是"本地 → HDFS",区别如下:

命令 行为 本地文件是否保留 备注
-put 复制本地(或标准输入)到 HDFS 保留 功能最全,可从 stdin 读取
-copyFromLocal 复制本地到 HDFS 保留 与 -put 基本等价,但源只能是本地路径
-moveFromLocal 复制后删除本地源文件 删除 相当于"剪切",用于上传后即可清理本地的场景
# 基本上传
hadoop fs -put localfile.txt /user/hadoop/
hadoop fs -put dir1 dir2 /user/hadoop/          # 上传多个
hadoop fs -copyFromLocal localfile.txt /user/hadoop/

# -f 覆盖目标已存在的文件(不加则报 File exists)
hadoop fs -put -f localfile.txt /user/hadoop/

# -p 保留访问/修改时间、所有权和权限
hadoop fs -put -p localfile.txt /user/hadoop/

# -l 允许 DataNode 延迟持久化到磁盘,副本数强制为 1(提速,但降低可靠性)
hadoop fs -put -l bigfile /user/hadoop/

# -d 跳过创建 ._COPYING_ 临时文件
hadoop fs -put -d localfile.txt /user/hadoop/

# 从标准输入上传("-" 表示 stdin),非常实用
cat access.log | hadoop fs -put - /user/hadoop/access.log
tar czf - /var/log | hadoop fs -put - /backup/log.tar.gz

# 上传后删除本地文件
hadoop fs -moveFromLocal /tmp/staged.csv /user/hadoop/

3.2.4 下载:-get / -copyToLocal / -getmerge / -moveToLocal

# 下载单个文件/目录
hadoop fs -get /user/hadoop/a.log /home/hadoop/
hadoop fs -copyToLocal /user/hadoop/a.log /home/hadoop/

# 下载并重命名
hadoop fs -get /user/hadoop/a.log /home/hadoop/b.log

# -p 保留时间戳与权限;-f 覆盖本地已存在文件
hadoop fs -get -p -f /user/hadoop/a.log /home/hadoop/

# -ignoreCrc 忽略 CRC 校验失败(用于抢救已损坏的文件)
hadoop fs -get -ignoreCrc /user/hadoop/broken.log /home/hadoop/
# -crc 同时下载 .crc 校验文件
hadoop fs -get -crc /user/hadoop/a.log /home/hadoop/

# 下载到标准输出后管道处理
hadoop fs -get /user/hadoop/a.log - | head -20

# -getmerge:把 HDFS 目录下所有文件按顺序合并成一个本地文件
# 典型用途:合并 MapReduce 输出的 part-r-00000、part-r-00001 ...
hadoop fs -getmerge /output/wordcount /home/hadoop/result.txt
# -nl 在每个文件末尾追加换行符(避免最后一行与下个文件首行粘连)
hadoop fs -getmerge -nl /output/wordcount /home/hadoop/result.txt
# -skip-empty-file 跳过空文件
hadoop fs -getmerge -nl -skip-empty-file /output/wordcount /home/hadoop/result.txt

-getmerge 会把所有内容拉到本地单机上,目录总量很大时会撑爆本地磁盘,属于常见坑,见第四章排错清单。

3.2.5 查看内容:-cat / -text / -tail / -head

# -cat:输出文件原始字节到 stdout
hadoop fs -cat /user/hadoop/a.log
hadoop fs -cat /user/hadoop/part-*             # 支持通配符,多文件依次输出
hadoop fs -cat /user/hadoop/a.log | wc -l      # 配合管道统计行数
hadoop fs -cat -ignoreCrc /user/hadoop/a.log   # 忽略校验错误

# -text:先解码再输出,能自动识别 gzip/bzip2/SequenceFile/Avro 等格式
hadoop fs -text /user/hadoop/a.log.gz
hadoop fs -text /user/hadoop/seqfile

# -tail:查看文件最后 1KB 内容
hadoop fs -tail /user/hadoop/a.log
hadoop fs -tail -f /user/hadoop/a.log          # 持续跟踪(类似 tail -f),常用于看正在追加的日志

# -head:查看文件开头 1KB 内容(Hadoop 3.1+ 提供)
hadoop fs -head /user/hadoop/a.log

3.2.6 复制与移动:-cp / -mv

# HDFS 内部复制
hadoop fs -cp /user/hadoop/a.log /user/hadoop/backup/
hadoop fs -cp -f /src/a.log /dst/a.log                 # 覆盖目标
hadoop fs -cp -p /src/a.log /dst/                      # 保留属性(时间戳、所有权、权限、ACL、xattr)
hadoop fs -cp -p=tap /src/a.log /dst/                  # 只保留 timestamps/ownership/permission
hadoop fs -cp /src/dir1 /src/dir2 /dst/                # 多源
# 跨集群复制(小数据量;大数据量请用 distcp)
hadoop fs -cp hdfs://nn1:8020/data hdfs://nn2:8020/data

# HDFS 内部移动/重命名(同一文件系统内为纯元数据操作,瞬间完成,不搬数据)
hadoop fs -mv /user/hadoop/a.log /user/hadoop/a.log.bak
hadoop fs -mv /user/hadoop/a.log /user/hadoop/archive/
hadoop fs -mv /f1 /f2 /f3 /targetdir/                  # 多源移动,目标必须是目录

-mv 不能跨文件系统(不能在 hdfs:// 与 file:// 之间移动)。

3.2.7 删除:-rm / -rmdir

hadoop fs -rm /user/hadoop/a.log             # 删除文件(默认进回收站)
hadoop fs -rm -r /user/hadoop/dir            # -r/-R 递归删除目录及内容
hadoop fs -rm -f /user/hadoop/notexist       # -f 文件不存在时不报错、不返回非零退出码
hadoop fs -rm -skipTrash /user/hadoop/big    # 跳过回收站,立即释放空间
hadoop fs -rm -r -skipTrash /user/hadoop/dir
hadoop fs -rm -safely -r /user/hadoop/dir    # 删除超过 100 万文件的目录前要求确认
hadoop fs -rmdir /user/hadoop/emptydir       # 只能删除空目录
hadoop fs -rmdir --ignore-fail-on-non-empty /user/hadoop/dir  # 非空时不报错

3.2.8 空间统计:-du / -df / -count

# -du:显示目录下每个子项的大小
hadoop fs -du /user/hadoop
# -s 汇总(只输出总计一行);-h 人类可读
hadoop fs -du -s -h /user/hadoop
hadoop fs -du -h /user/hadoop
# -v 显示表头;-x 排除快照占用
hadoop fs -du -s -h -v /user/hadoop
hadoop fs -du -s -h -x /user/hadoop

# -du 输出三列:逻辑大小 / 含副本的实际磁盘占用 / 路径
# 例:  1048576   3145728   /user/hadoop/a.log     (1MB 数据 × 3 副本 = 3MB)

# -df:显示文件系统整体容量使用情况
hadoop fs -df
hadoop fs -df -h
hadoop fs -df -h /
# 输出:Filesystem  Size  Used  Available  Use%

# -count:统计目录数、文件数、总字节数
hadoop fs -count /user/hadoop
# 输出:DIR_COUNT  FILE_COUNT  CONTENT_SIZE  PATHNAME

# -q 显示配额信息(共 8 列)
hadoop fs -count -q /user/hadoop
# 输出:QUOTA  REMAINING_QUOTA  SPACE_QUOTA  REMAINING_SPACE_QUOTA
#       DIR_COUNT  FILE_COUNT  CONTENT_SIZE  PATHNAME
# 未设置配额时显示 none 与 inf

hadoop fs -count -q -h /user/hadoop      # 人类可读
hadoop fs -count -v -q -h /user/hadoop   # 带表头
hadoop fs -count -x /user/hadoop         # 排除快照
hadoop fs -count -t -v /user/hadoop      # 按存储类型显示配额(3.x)
hadoop fs -count -u -h /user/hadoop      # 只显示配额与使用量

3.2.9 其他常用命令

# -touchz:创建一个长度为 0 的文件(若文件已存在且非空则报错)
hadoop fs -touchz /user/hadoop/_SUCCESS
# -touch:3.x 提供,可修改时间戳
hadoop fs -touch -m -t 20260822103000 /user/hadoop/a.log

# -stat:按格式输出文件统计信息
hadoop fs -stat /user/hadoop/a.log                    # 默认输出修改时间
hadoop fs -stat "%n %b %r %o %y %u %g %F" /user/hadoop/a.log

-stat 的格式化占位符:

占位符 含义
%b 文件大小(字节),block 累计
%F 类型:regular file / directory / symlink
%g 所属组名
%n 文件名(不含路径)
%o 块大小(字节)
%r 副本数
%u 所属用户名
%x 最后访问时间(%X 为毫秒时间戳)
%y 最后修改时间(%Y 为毫秒时间戳)
# -find:按条件查找(类似 Linux find,但功能有限)
hadoop fs -find / -name "*.log" -print
hadoop fs -find /user -iname "PART-*" -print         # -iname 忽略大小写
hadoop fs -find /user -name "*.tmp" -print0          # 用 \0 分隔,便于配合 xargs -0

# -appendToFile:把本地文件内容追加到 HDFS 文件末尾(HDFS 唯一支持的"修改")
hadoop fs -appendToFile localfile.txt /user/hadoop/a.log
hadoop fs -appendToFile f1.txt f2.txt /user/hadoop/a.log
echo "new line" | hadoop fs -appendToFile - /user/hadoop/a.log   # 从 stdin 追加

# -checksum:显示文件的校验和(用于比对两份数据是否一致)
hadoop fs -checksum /user/hadoop/a.log
# 输出:/user/hadoop/a.log  MD5-of-0MD5-of-512CRC32C  0000020000...

# -setrep:修改文件或目录的副本数
hadoop fs -setrep 2 /user/hadoop/a.log         # 单文件设为 2 副本
hadoop fs -setrep -R 2 /user/hadoop/cold       # -R 递归对目录下所有文件生效
hadoop fs -setrep -w 3 /user/hadoop/hot        # -w 阻塞等待副本复制真正完成(大目录会很慢)
# 说明:调低副本数后 NameNode 会删除多余副本,空间立即释放;
#      调高副本数是后台异步复制,不加 -w 命令会立刻返回

# -chmod / -chown / -chgrp:修改权限与归属
hadoop fs -chmod 755 /user/hadoop/a.log
hadoop fs -chmod -R 775 /user/hadoop/data          # -R 递归
hadoop fs -chmod u+x,g-w /user/hadoop/a.log        # 支持符号模式
hadoop fs -chmod -R 777 /                          # 实验环境下解决权限问题的粗暴做法(生产禁用)
hadoop fs -chown hadoop /user/hadoop/a.log
hadoop fs -chown -R hadoop:supergroup /user/hadoop
hadoop fs -chgrp -R hive /user/hive/warehouse

# -test:条件判断,通过退出码返回结果(0 为真),常用于 shell 脚本
hadoop fs -test -e /user/hadoop/a.log    # 路径是否存在(exists)
hadoop fs -test -z /user/hadoop/a.log    # 文件长度是否为 0(zero length)
hadoop fs -test -d /user/hadoop          # 是否为目录(directory)
hadoop fs -test -f /user/hadoop/a.log    # 是否为普通文件
hadoop fs -test -s /user/hadoop/a.log    # 路径大小是否大于 0
hadoop fs -test -r /user/hadoop/a.log    # 是否有读权限
hadoop fs -test -w /user/hadoop/a.log    # 是否有写权限

# 脚本中的典型用法
if hadoop fs -test -e /user/hadoop/_SUCCESS; then
  echo "上游任务已完成"
else
  echo "等待上游数据"
fi

# -expunge:清空回收站中已过期的检查点
hadoop fs -expunge
hadoop fs -expunge -immediate         # 3.x:立即删除全部回收站内容,不等保留期

# -truncate:把文件截断到指定长度(3.x)
hadoop fs -truncate 1024 /user/hadoop/a.log
hadoop fs -truncate -w 1024 /user/hadoop/a.log   # -w 等待恢复完成

# -usage / -help:查看命令用法
hadoop fs -usage put
hadoop fs -help setrep
hadoop fs -help                     # 列出全部子命令

3.3 权限与配额管理

3.3.1 HDFS 权限模型

HDFS 的权限模型与 POSIX 高度相似,但有关键差异:

相似点:

  • 每个文件/目录都有 owner、group 和权限位(rwx,三组共 9 位);
  • 对文件:r 表示可读取内容,w 表示可写入/追加,x 无意义(HDFS 不执行文件);
  • 对目录:r 表示可列出内容(ls),w 表示可在其中创建/删除子项,x 表示可访问(穿越)该目录。

差异点:

  • 没有 setuid/setgid/sticky 的完整语义(目录上的 sticky bit 被支持,用于 /tmp 防止用户互删文件);
  • 身份不做真实认证(非 Kerberos 模式下):HDFS 简单地信任客户端进程上报的用户名(whoami 的结果或 HADOOP_USER_NAME 环境变量),因此简单模式下的权限只能防误操作,不能防恶意用户。要真正的安全必须启用 Kerberos;
  • 文件的 x 位无效;不支持硬链接。

权限总开关:

<property>
  <name>dfs.permissions.enabled</name>
  <value>true</value>
  <description>是否检查权限。设为 false 后所有权限检查被跳过(owner/group 仍会记录)</description>
</property>

3.3.2 超级用户与超级用户组

  • 超级用户:启动 NameNode 进程的那个操作系统用户就是 HDFS 的超级用户(通常是 hdfs 或 hadoop)。超级用户跳过所有权限检查。注意"超级用户"不是固定叫 root,root 在 HDFS 中只是普通用户。
  • 超级用户组:属于该组的所有用户都拥有超级用户权限。
<property>
  <name>dfs.permissions.superusergroup</name>
  <value>supergroup</value>
  <description>超级用户组名,默认 supergroup。可改为运维实际使用的 OS 组,如 hadoop</description>
</property>

3.3.3 umask

新建文件与目录的默认权限由 umask 决定:目录 777 - umask,文件 666 - umask。

<property>
  <name>fs.permissions.umask-mode</name>
  <value>022</value>
  <description>默认 022,即新目录 755、新文件 644。也可写成 u=rwx,g=rx,o=rx 形式</description>
</property>

3.3.4 ACL(访问控制列表)

9 位权限位只能表达"一个用户 + 一个组 + 其他人",无法满足"目录要给 A 组读写、B 组只读、用户 c 只读"这类需求,这时需要 ACL。

<property>
  <name>dfs.namenode.acls.enabled</name>
  <value>true</value>
  <description>启用 ACL,默认 false。修改后需重启 NameNode</description>
</property>
<property>
  <name>dfs.namenode.posix.acl.inheritance.enabled</name>
  <value>true</value>
  <description>启用 POSIX 风格的默认 ACL 继承(3.x)</description>
</property>
# 查看 ACL
hadoop fs -getfacl /user/hadoop/data
hadoop fs -getfacl -R /user/hadoop/data          # 递归

# 输出示例
# file: /user/hadoop/data
# owner: hadoop
# group: supergroup
# user::rwx
# user:alice:r-x
# group::r-x
# mask::r-x
# other::---

# 给用户/组授权
hadoop fs -setfacl -m user:alice:rwx /user/hadoop/data
hadoop fs -setfacl -m group:analyst:r-x /user/hadoop/data
hadoop fs -setfacl -m user:bob:r--,group:dev:rwx /user/hadoop/data   # 一次多条

# 设置"默认 ACL",使新建子项自动继承(只对目录有效)
hadoop fs -setfacl -m default:user:alice:rwx /user/hadoop/data
hadoop fs -setfacl -R -m default:group:analyst:r-x /user/hadoop/data

# 删除某条 ACL
hadoop fs -setfacl -x user:alice /user/hadoop/data
# 删除所有默认 ACL
hadoop fs -setfacl -k /user/hadoop/data
# 删除全部扩展 ACL(只保留基本权限位)
hadoop fs -setfacl -b /user/hadoop/data
# 用文件批量设置(--set 会完全替换现有 ACL)
hadoop fs -setfacl --set user::rw-,user:alice:r--,group::r--,other::--- /user/hadoop/a.log
hadoop fs -setfacl -R -m user:alice:r-x /user/hadoop/data     # 递归

设置了 ACL 的路径,hadoop fs -ls 输出的权限位末尾会显示 +,例如 -rw-r--r--+。 mask 条目是所有命名用户/组条目的权限上限,chmod 修改组权限位时实际改的是 mask。

3.3.5 目录配额(文件数配额)

限制一个目录下最多能有多少个"名字"(含目录自身、子目录、文件)。

# 设置配额为 100(该目录连自己在内最多 100 个 name)
hdfs dfsadmin -setQuota 100 /user/hadoop/project
hdfs dfsadmin -setQuota 1000 /user/a /user/b        # 一次设置多个目录

# 清除配额
hdfs dfsadmin -clrQuota /user/hadoop/project

# 查看
hadoop fs -count -q -h -v /user/hadoop/project

超限报错:

put: The NameSpace quota (directories and files) of directory
/user/hadoop/project is exceeded: quota=100 file count=101

3.3.6 空间配额

限制一个目录下含副本的总磁盘用量。注意是"含副本":3 副本下要存 1GB 数据,空间配额至少要 3GB。

# 设置空间配额(支持 t/g/m/k 单位后缀)
hdfs dfsadmin -setSpaceQuota 10g /user/hadoop/project
hdfs dfsadmin -setSpaceQuota 1t /user/hive/warehouse
hdfs dfsadmin -setSpaceQuota 21474836480 /user/hadoop/project   # 也可用字节

# 按存储类型设置配额(异构存储,3.x)
hdfs dfsadmin -setSpaceQuota 5t -storageType SSD /user/hadoop/hot
hdfs dfsadmin -clrSpaceQuota -storageType SSD /user/hadoop/hot

# 清除空间配额
hdfs dfsadmin -clrSpaceQuota /user/hadoop/project

# 查看
hadoop fs -count -q -h -v /user/hadoop/project

超限报错:

put: The DiskSpace quota of /user/hadoop/project is exceeded:
quota = 10737418240 B = 10 GB but diskspace consumed = 11542724608 B = 10.75 GB

常见误区:空间配额被"块大小 × 副本数"整体计算。一个正在写入的空文件也会先按一个完整块的空间预留计算配额,因此配额必须至少大于 blocksize × replication,否则连一个文件都写不进去。

3.4 dfsadmin 管理命令

hdfs dfsadmin 是集群管理员命令,大部分子命令需要超级用户权限。

hdfs dfsadmin -help                 # 查看全部子命令

3.4.1 -report:集群状态报告

hdfs dfsadmin -report
hdfs dfsadmin -report -live          # 只看存活节点
hdfs dfsadmin -report -dead          # 只看死亡节点
hdfs dfsadmin -report -decommissioning
hdfs dfsadmin -report -enteringmaintenance   # 3.x

输出逐字段解读:

Configured Capacity: 3221225472000 (3.00 TB)      # 集群总配置容量(所有 DataNode 数据盘之和)
Present Capacity: 3006477107200 (2.73 TB)         # 当前可用于 HDFS 的容量(扣除保留空间与非 HDFS 占用)
DFS Remaining: 2147483648000 (1.95 TB)            # HDFS 剩余可写空间
DFS Used: 858993459200 (800.00 GB)                # HDFS 已用(含所有副本)
DFS Used%: 28.57%                                 # 已用比例
Under replicated blocks: 12                       # 副本数不足的块数(正在恢复中)
Blocks with corrupt replicas: 0                   # 存在损坏副本的块数
Missing blocks: 0                                 # 所有副本都丢失的块(数据已不可读,最严重)
Missing blocks (with replication factor 1): 0     # 单副本文件的丢块(通常是伪分布式/临时数据)
Pending deletion blocks: 0                        # 等待删除的块

-------------------------------------------------
Live datanodes (3):                               # 存活 DataNode 数量

Name: 192.168.224.11:50010 (dn1)                  # DataNode 的数据传输地址
Hostname: dn1
Rack: /dc1/rack1                                  # 机架(机架感知生效后才有非默认值)
Decommission Status : Normal                      # 退役状态:Normal/Decommission in progress/Decommissioned
Configured Capacity: 1073741824000 (1000.00 GB)   # 该节点总容量
DFS Used: 286331153066 (266.67 GB)                # 该节点 HDFS 已用
Non DFS Used: 53687091200 (50.00 GB)              # 被非 HDFS 数据占用的空间(OS、日志、其他程序)
DFS Remaining: 733723579734 (683.33 GB)           # 该节点剩余
DFS Used%: 26.67%                                 # 该节点使用率,Balancer 依据此值均衡
DFS Remaining%: 68.33%
Configured Cache Capacity: 0 (0 B)                # 集中式缓存容量
Cache Used: 0 (0 B)
Cache Remaining: 0 (0 B)
Xceivers: 4                                       # 当前数据传输线程数,接近上限说明并发过高
Last contact: Sat Aug 22 10:30:12 CST 2026        # 最近一次心跳时间,若长时间无更新说明节点异常
Last Block Report: Sat Aug 22 06:00:03 CST 2026   # 最近一次完整块报告时间
Num of Blocks: 20431                              # 该节点持有的块数

3.4.2 其他 dfsadmin 子命令

# 安全模式
hdfs dfsadmin -safemode get      # 查询:Safe mode is ON/OFF
hdfs dfsadmin -safemode enter    # 进入(做元数据备份、维护前)
hdfs dfsadmin -safemode leave    # 强制离开
hdfs dfsadmin -safemode wait     # 阻塞等待自动退出,脚本中常用
hdfs dfsadmin -safemode forceExit  # 3.x:强制退出(可能丢数据,慎用)

# 刷新节点列表:读取 dfs.hosts / dfs.hosts.exclude,用于上线新节点与退役旧节点
hdfs dfsadmin -refreshNodes

# 设置 balancer 带宽(字节/秒),即时生效无需重启
hdfs dfsadmin -setBalancerBandwidth 10485760
hdfs dfsadmin -getBalancerBandwidth dn1:50020    # 查询某节点当前值(3.x)

# 立即把内存中的元数据落盘生成新 FsImage 并清空 EditLog(必须先进入安全模式)
hdfs dfsadmin -safemode enter
hdfs dfsadmin -saveNamespace
hdfs dfsadmin -safemode leave

# 手工滚动 EditLog(结束当前 edits 段,开启新段)
hdfs dfsadmin -rollEdits

# 把 NameNode 的元数据信息(块状态、DataNode 状态)dump 到日志目录下的文件,用于诊断
hdfs dfsadmin -metasave metasave-report.txt
# 输出文件位于 ${HADOOP_LOG_DIR}/metasave-report.txt,
# 内含 Under-replicated / Corrupt / Pending 块清单,是排查丢块的利器

# 查看机架拓扑(验证机架感知脚本是否生效)
hdfs dfsadmin -printTopology
# 输出示例:
# Rack: /dc1/rack1
#    192.168.224.11:50010 (dn1)
#    192.168.224.12:50010 (dn2)
# Rack: /dc1/rack2
#    192.168.224.13:50010 (dn3)

# 快照开关
hdfs dfsadmin -allowSnapshot /user/hadoop/important
hdfs dfsadmin -disallowSnapshot /user/hadoop/important

# 配额
hdfs dfsadmin -setQuota 10000 /user/hadoop
hdfs dfsadmin -clrQuota /user/hadoop
hdfs dfsadmin -setSpaceQuota 10g /user/hadoop
hdfs dfsadmin -clrSpaceQuota /user/hadoop

# 刷新配置(部分参数支持不重启生效)
hdfs dfsadmin -refreshServiceAcl
hdfs dfsadmin -refreshUserToGroupsMappings
hdfs dfsadmin -refreshSuperUserGroupsConfiguration
hdfs dfsadmin -refreshCallQueue

# 手工触发 DataNode 上报完整块报告
hdfs dfsadmin -triggerBlockReport dn1:50020
hdfs dfsadmin -triggerBlockReport -incremental dn1:50020

# 版本与升级
hdfs dfsadmin -rollingUpgrade query|prepare|finalize
hdfs dfsadmin -finalizeUpgrade

# 优雅关闭 DataNode(3.x 维护模式)
hdfs dfsadmin -shutdownDatanode dn1:50020 upgrade
hdfs dfsadmin -getDatanodeInfo dn1:50020

# 查看/管理 HA 状态(HA 集群)
hdfs haadmin -getServiceState nn1
hdfs haadmin -failover nn1 nn2

3.5 fsck:文件系统检查

hdfs fsck 检查文件系统的健康状况,只读不修改(除显式指定 -delete/-move)。它不能修复损坏的数据,只能报告并清理。

# 检查整个文件系统
hdfs fsck /

# 只检查某个路径
hdfs fsck /user/hadoop/data

# -files 显示每个被检查的文件
hdfs fsck /user/hadoop -files

# -blocks 显示每个块的信息(需与 -files 同用)
hdfs fsck /user/hadoop -files -blocks

# -locations 显示每个块的副本所在 DataNode 地址
hdfs fsck /user/hadoop -files -blocks -locations

# -racks 显示 DataNode 的机架信息
hdfs fsck /user/hadoop -files -blocks -locations -racks

# 只列出有问题的文件
hdfs fsck / -list-corruptfileblocks
hdfs fsck / -openforwrite          # 列出正在被写入(未关闭)的文件
hdfs fsck / -includeSnapshots      # 包含快照
hdfs fsck / -storagepolicies       # 显示存储策略(3.x)
hdfs fsck / -blockId blk_1073741825   # 按块 ID 查询该块详情

fsck 输出报告各项含义:

Status: HEALTHY                          # 总体状态:HEALTHY / CORRUPT
 Total size:    858993459200 B           # 总逻辑数据量(不含副本)
 Total dirs:    1024                     # 目录总数
 Total files:   20431                    # 文件总数
 Total symlinks:0
 Total blocks (validated): 20500 (avg. block size 41902 B)
                                         # 有效块总数与平均块大小(平均块远小于 blocksize → 小文件多)
 Minimally replicated blocks: 20500 (100.0 %)     # 至少有 min 副本的块比例,应为 100%
 Over-replicated blocks: 0 (0.0 %)       # 副本超出设定值的块(通常会自动清理)
 Under-replicated blocks: 12 (0.058 %)   # 副本数不足的块,NameNode 会自动复制补齐
 Mis-replicated blocks: 3 (0.014 %)      # 副本放置不符合机架策略的块(如都在同一机架)
 Default replication factor: 3
 Average block replication: 2.9995       # 实际平均副本数
 Corrupt blocks: 0                       # 所有副本都校验失败的块(数据损坏,不可恢复)
 Missing replicas: 36 (0.058 %)          # 缺失的副本总数
 Number of data-nodes: 3
 Number of racks: 2
FSCK ended at Sat Aug 22 10:35:12 CST 2026 in 812 milliseconds

Corrupt blocks 与 Missing replicas 的处理

# 1. 定位问题文件
hdfs fsck / -list-corruptfileblocks
hdfs fsck / | grep -E "CORRUPT|MISSING"
hdfs fsck /user/hadoop -files -blocks -locations | grep -B2 CORRUPT

# 2. Under-replicated / Missing replicas(副本不足,数据仍完整)
#    通常等待 NameNode 自动复制即可;若长期不恢复:
hdfs dfsadmin -report                       # 确认 DataNode 是否都存活、是否磁盘满
hdfs dfs -setrep -R 3 /user/hadoop          # 重新触发副本数设置
# 加快恢复速度
hdfs dfsadmin -setBalancerBandwidth 52428800

# 3. Corrupt blocks / Missing blocks(数据已损坏或丢失)
#    先尝试抢救能读的部分
hadoop fs -get -ignoreCrc /user/hadoop/broken.log /tmp/
#    确认无法恢复后,删除损坏文件(NameNode 会随之清理块)
hdfs fsck /user/hadoop -delete
#    或把损坏文件移动到 /lost+found 以便后续分析,不直接删除
hdfs fsck /user/hadoop -move
#    也可精确删除单个文件
hadoop fs -rm -skipTrash /user/hadoop/broken.log

-delete 会删除含损坏/丢失块的整个文件,是不可逆操作,执行前务必先用 -list-corruptfileblocks 确认清单并评估影响。

3.6 Web UI 与 REST 接口

Web UI

配置好 Hadoop 集群后,可以通过浏览器访问 HDFS 文件系统:

Hadoop 2.x:http://[NameNodeIP]:50070
Hadoop 3.x:http://[NameNodeIP]:9870

通过 Web 界面的"Browse the filesystem"(3.x 中为 Utilities → Browse the file system)可以查看文件,例如 hdfs://localhost/home/administrator/tempfile/file1.txt。

Web UI 常用页面:

  • Overview:集群版本、启动时间、安全模式状态、块与文件总数、堆内存使用;
  • Datanodes:各 DataNode 容量、使用率、最后心跳、退役状态;
  • Snapshot:可快照目录与快照列表;
  • Startup Progress:NameNode 启动阶段进度(加载 FsImage、回放 EditLog、安全模式);
  • Utilities → Browse the file system:目录浏览、下载文件、查看块信息;
  • Utilities → Logs:查看服务日志。

WebHDFS REST API

WebHDFS 直接由 NameNode/DataNode 内置的 HTTP 服务提供,无需 Java 客户端即可读写 HDFS。

<property>
  <name>dfs.webhdfs.enabled</name>
  <value>true</value>
  <description>2.x 中默认为 true;3.x 已内置无需配置</description>
</property>
# 列出目录
curl -i "http://nn:50070/webhdfs/v1/user/hadoop?op=LISTSTATUS&user.name=hadoop"

# 获取文件状态
curl -i "http://nn:9870/webhdfs/v1/user/hadoop/a.log?op=GETFILESTATUS"

# 读取文件(会 307 重定向到 DataNode,需要 -L 跟随)
curl -i -L "http://nn:9870/webhdfs/v1/user/hadoop/a.log?op=OPEN"

# 创建目录
curl -i -X PUT "http://nn:9870/webhdfs/v1/user/hadoop/newdir?op=MKDIRS&user.name=hadoop"

# 创建并写入文件(两步:先 PUT 拿重定向地址,再向 DataNode PUT 数据)
curl -i -X PUT "http://nn:9870/webhdfs/v1/user/hadoop/b.txt?op=CREATE&overwrite=true&user.name=hadoop"
curl -i -X PUT -T local.txt "http://dn1:9864/webhdfs/v1/user/hadoop/b.txt?op=CREATE&namenoderpcaddress=nn:9820&overwrite=true"

# 追加、删除、重命名、改权限
curl -i -X POST -T more.txt "http://nn:9870/webhdfs/v1/user/hadoop/b.txt?op=APPEND"
curl -i -X DELETE "http://nn:9870/webhdfs/v1/user/hadoop/b.txt?op=DELETE&recursive=false"
curl -i -X PUT "http://nn:9870/webhdfs/v1/a?op=RENAME&destination=/user/hadoop/b"
curl -i -X PUT "http://nn:9870/webhdfs/v1/user/hadoop/b.txt?op=SETPERMISSION&permission=644"
curl -i -X PUT "http://nn:9870/webhdfs/v1/user/hadoop/b.txt?op=SETREPLICATION&replication=2"

WebHDFS 的缺点:客户端必须能直接访问所有 DataNode的 HTTP 端口(因为读写会被重定向到 DataNode)。

HttpFS

HttpFS 是一个独立的网关服务(内嵌 Tomcat/Jetty),提供与 WebHDFS 完全相同的 REST 接口,但所有数据都经由网关代理转发,客户端只需访问 HttpFS 一个地址与端口(默认 14000)。

  • 适用场景:客户端在集群网络之外、只能开放单个端口、需要通过防火墙访问 HDFS;
  • 代价:所有数据流经单点网关,吞吐受网关带宽限制,不适合大数据量传输。
# 启动/停止(2.x)
$HADOOP_HOME/sbin/httpfs.sh start
$HADOOP_HOME/sbin/httpfs.sh stop
# 3.x
hdfs --daemon start httpfs
hdfs --daemon stop httpfs

# 访问(路径与 WebHDFS 一致,只是主机端口不同)
curl -i "http://gateway:14000/webhdfs/v1/user/hadoop?op=LISTSTATUS&user.name=hadoop"

四、Java API 编程

HDFS 编程实践的详细内容可参考厦门大学数据库实验室建设的中国高校大数据课程公共服务平台技术文章《大数据技术原理与应用 第三章 Hadoop分布式文件系统 学习指南》(http://dblab.xmu.edu.cn/blog/290-2/ )。

本节内容基于课程实验讲义,介绍如何在本地(Windows + Eclipse)远程或本地运行 Java 程序访问 HDFS。示例连接的 HDFS 地址为 hdfs://192.168.224.10:9000,Hadoop 版本为 2.7.1。

4.1 开发环境准备

4.1.1 在 Windows 中安装并启动 Eclipse

将 eclipse2018.zip 解压到本地目录,执行其中的 eclipse.exe 启动 Eclipse,然后创建一个 Java 项目。

4.1.2 在 Ubuntu 中安装 Eclipse

可利用 Ubuntu 左侧边栏自带的软件中心安装 Eclipse:打开软件中心,在搜索栏输入"ec",软件中心会自动搜索相关软件;点击 Eclipse 进行安装。安装需要管理员权限,需在弹出的"认证"窗口输入当前用户登录密码;安装结束后进度条消失,即可在搜索工具中找到并打开 Eclipse。

4.1.3 在 Eclipse 中创建项目

第一次打开 Eclipse 需要填写 workspace(工作空间)以保存程序位置,按默认即可。点击 OK 进入软件后,选择顶部菜单 File → New → Java Project 创建项目,输入项目名称(教程中为"Dblab"),其他保持默认,点击 Finish。

4.1.4 为项目加载所需的 jar 包

Java API 所在的 jar 包都在已安装好的 Hadoop 文件夹里,路径为 /usr/local/hadoop/share/hadoop(若安装目录不同,请找到 jar 包所在文件夹)。在 Eclipse 项目(Dblab)上右键 → Properties → Java Build Path → Libraries → Add External JARs,加载所需 jar 包。

在 Java 项目中添加以下 Hadoop 安装目录下的 JAR 包,用于编译和运行 HDFS 客户端程序:

序号 来源目录 需要添加的 JAR 包
1 /usr/local/hadoop/share/hadoop/common hadoop-common-2.7.1.jar、hadoop-nfs-2.7.1.jar
2 /usr/local/hadoop/share/hadoop/common/lib 该目录下所有 JAR 包
3 /usr/local/hadoop/share/hadoop/hdfs hadoop-hdfs-2.7.1.jar、hadoop-hdfs-nfs-2.7.1.jar
4 /usr/local/hadoop/share/hadoop/hdfs/lib 该目录下所有 JAR 包

注:讲义中部分包名写作 haoop-*,为笔误,实际文件名以 hadoop- 开头。

导入 JAR 包后,即可在项目中编写 Java 代码访问 HDFS。

4.1.5 Maven 依赖(推荐方式)

现代项目更推荐用 Maven 管理依赖,只需两三个坐标即可自动拉齐全部传递依赖:

<properties>
  <hadoop.version>2.7.1</hadoop.version>
  <!-- Hadoop 3.x 用 <hadoop.version>3.3.6</hadoop.version> -->
</properties>

<dependencies>
  <dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-common</artifactId>
    <version>${hadoop.version}</version>
  </dependency>
  <dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-hdfs</artifactId>
    <version>${hadoop.version}</version>
  </dependency>
  <dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>${hadoop.version}</version>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

4.1.6 配置文件的放置

第一步需要把集群上的 core-site.xml 和 hdfs-site.xml(位于 /hadoop/etc/hadoop 目录下)放到当前工程项目下(Eclipse 工作目录的 bin 文件夹;Maven 项目放在 src/main/resources)。第二步编写实现代码。

放好配置文件后,new Configuration() 会自动从 classpath 加载 core-default.xml、core-site.xml、hdfs-default.xml、hdfs-site.xml,代码中就不必再手工 conf.set("fs.defaultFS", ...)。

4.2 Configuration 与 FileSystem 的获取

FileSystem 是通用文件系统的抽象基类,DistributedFileSystem 是它在 HDFS 上的具体实现。获取实例有三种常见方式:

// 方式一:依赖 classpath 中的配置文件(fs.defaultFS)
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(conf);

// 方式二:在代码中显式指定地址(讲义示例采用的方式)
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://192.168.224.10:9000");
conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
FileSystem fs = FileSystem.get(conf);

// 方式三:用 URI + 用户身份获取(推荐,可绕过权限问题)
Configuration conf = new Configuration();
conf.set("dfs.replication", "2");        // 客户端侧参数同样生效
FileSystem fs = FileSystem.get(
        new URI("hdfs://192.168.224.10:9000"), conf, "hadoop");  // 第三个参数为 HDFS 用户名

指定用户身份的三种做法(解决 Permission denied: user=Administrator):

// 1. FileSystem.get(URI, conf, "hadoop")
// 2. 设置 JVM 系统属性(也可作为 VM 参数 -DHADOOP_USER_NAME=hadoop)
System.setProperty("HADOOP_USER_NAME", "hadoop");
// 3. 设置环境变量 HADOOP_USER_NAME=hadoop

4.3 示例一:判断文件是否存在

利用 Hadoop 的 Java API 检测伪分布式文件系统 HDFS 上是否存在某个文件。

版本 A(源文档 03,使用默认配置文件,检测 test.txt)

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

public class Chapter3 {
    public static void main(String[] args) {
        try {
            String filename = "hdfs://localhost:9000/user/hadoop/test.txt";
            Configuration conf = new Configuration();
            FileSystem fs = FileSystem.get(conf);
            if(fs.exists(new Path(filename))){
                System.out.println("文件存在");
            }else{
                System.out.println("文件不存在");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

版本 B(源文档 04,远程访问 192.168.224.10,检测根目录下的 /test.txt)

package com.testing;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

public class HDFSFileIfExist {
    public static void main(String[] args){
        try{
            String fileName = "/test.txt";
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            if(fs.exists(new Path(fileName))){
                System.out.println("文件存在");
            }else{
                System.out.println("文件不存在");
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

4.4 示例二:写入文件

向 HDFS 写入文件 /testx,内容为字符串 Hello world。通过 fs.create() 创建输出流,写入字节后关闭流。

如果执行代码时遇到权限问题,可在 Hadoop 服务端执行以下命令放开权限:

hadoop fs -chmod -R 777 /

(仅限实验环境,生产环境应使用正确的用户身份与 ACL 授权)

package com.testing;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;

public class HDFSFileIfExist {

    public static void main(String[] args) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            byte[] buff = "Hello world".getBytes(); // 要写入的内容
            String filename = "/testx"; //要写入的文件名
            FSDataOutputStream os = fs.create(new Path(filename));
            os.write(buff,0,buff.length);
            System.out.println("Create:"+ filename);
            os.close();
            fs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.5 示例三:读取文件

读取上一步写入的 /testx,通过 FSDataInputStream 包装为 BufferedReader,读取一行内容并输出。

package com.testing;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;

public class HDFSFileIfExist {
    public static void main(String args[]) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            Path file = new Path("/testx");
            FSDataInputStream getIt = fs.open(file);
            BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
            String content = d.readLine(); //读取文件一行
            System.out.println(content);
            d.close(); //关闭文件
            fs.close(); //关闭hdfs
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行后控制台输出:

Hello world

4.6 常用 API 小结

类 / 方法 作用
Configuration 加载 HDFS 配置参数,如 fs.defaultFS
FileSystem.get(conf) 获取 HDFS 文件系统实例
fs.exists(Path) 判断路径是否存在
fs.create(Path) 创建文件并返回 FSDataOutputStream
fs.open(Path) 打开文件并返回 FSDataInputStream
FSDataOutputStream HDFS 文件输出流,用于写入数据
FSDataInputStream HDFS 文件输入流,用于读取数据
fs.mkdirs(Path) 递归创建目录
fs.copyFromLocalFile(src, dst) 从本地上传文件
fs.copyToLocalFile(src, dst) 下载文件到本地
fs.delete(Path, recursive) 删除文件或目录
fs.rename(src, dst) 重命名或移动
fs.listStatus(Path) 列出目录下一层的 FileStatus[]
fs.listFiles(Path, recursive) 递归迭代列出文件 RemoteIterator<LocatedFileStatus>
fs.getFileBlockLocations(...) 获取文件块的位置信息
fs.setReplication(Path, short) 修改副本数
fs.append(Path) 追加写入
IOUtils.copyBytes(in, out, buf, close) 流之间拷贝数据的工具方法

说明:原始讲义(04-HDFS分布式文件系统.md)以 Eclipse 中编写并运行 Java 程序为主,HDFS 的架构、块、副本与 Shell 命令等内容未在该讲义中展开,本文已由另一份来源(03-分布式文件系统HDFS.md)及扩展内容补齐。

4.7 完整可运行的示例类

下面这个类覆盖了目录创建、上传下载、流式读写、删除、重命名、遍历、块位置查询等常用操作,并使用 try-with-resources 管理资源。

package com.testing;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
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.apache.hadoop.io.IOUtils;

/**
 * HDFS Java API 综合示例
 * 运行前请确认:
 *   1. NameNode 与 DataNode 均已启动
 *   2. 客户端能解析并连通所有 DataNode 主机名与数据端口
 */
public class HdfsDemo {

    private static final String HDFS_URI = "hdfs://192.168.224.10:9000";
    private static final String HDFS_USER = "hadoop";

    /** 统一获取 FileSystem 实例 */
    private static FileSystem getFileSystem() throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", HDFS_URI);
        conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
        conf.set("dfs.replication", "2");     // 客户端指定副本数
        conf.set("dfs.blocksize", "134217728"); // 客户端指定块大小 128MB
        return FileSystem.get(new URI(HDFS_URI), conf, HDFS_USER);
    }

    /** 1. 创建目录 */
    public static void mkdir(FileSystem fs, String dir) throws Exception {
        boolean ok = fs.mkdirs(new Path(dir));   // mkdirs 会递归创建父目录
        System.out.println("mkdirs " + dir + " => " + ok);
    }

    /** 2. 上传:copyFromLocalFile */
    public static void upload(FileSystem fs, String local, String remote) throws Exception {
        // 参数:delSrc(是否删除源文件), overwrite(是否覆盖), src, dst
        fs.copyFromLocalFile(false, true, new Path(local), new Path(remote));
        System.out.println("uploaded " + local + " -> " + remote);
    }

    /** 3. 上传:用 create() 写流,可精细控制副本数与块大小 */
    public static void writeByStream(FileSystem fs, String remote, String content) throws Exception {
        Path path = new Path(remote);
        // create(path, overwrite, bufferSize, replication, blockSize)
        try (FSDataOutputStream out = fs.create(path, true, 4096, (short) 2, 134217728L)) {
            out.write(content.getBytes("UTF-8"));
            out.hflush();   // 刷到 DataNode 内存,其他客户端可见
            out.hsync();    // 强制落盘,保证持久化
        }
        System.out.println("written " + remote);
    }

    /** 4. 追加写入 */
    public static void append(FileSystem fs, String remote, String content) throws Exception {
        try (FSDataOutputStream out = fs.append(new Path(remote))) {
            out.write(("\n" + content).getBytes("UTF-8"));
        }
        System.out.println("appended to " + remote);
    }

    /** 5. 下载:copyToLocalFile */
    public static void download(FileSystem fs, String remote, String local) throws Exception {
        // 参数:delSrc, src, dst, useRawLocalFileSystem(true 可避免生成 .crc 文件)
        fs.copyToLocalFile(false, new Path(remote), new Path(local), true);
        System.out.println("downloaded " + remote + " -> " + local);
    }

    /** 6. 下载:用 open() 读流 + IOUtils */
    public static void readByStream(FileSystem fs, String remote, String local) throws Exception {
        try (FSDataInputStream in = fs.open(new Path(remote));
             OutputStream out = new FileOutputStream(local)) {
            // 参数:in, out, bufferSize, close(是否关闭流,此处交由 try-with-resources)
            IOUtils.copyBytes(in, out, 4096, false);
        }
        System.out.println("stream downloaded " + remote);
    }

    /** 7. 按行读取并打印 */
    public static void cat(FileSystem fs, String remote) throws Exception {
        try (FSDataInputStream in = fs.open(new Path(remote));
             BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }

    /** 8. 从文件中间位置开始读(随机读) */
    public static void seekRead(FileSystem fs, String remote, long offset) throws Exception {
        try (FSDataInputStream in = fs.open(new Path(remote))) {
            in.seek(offset);                 // 定位到指定字节偏移
            IOUtils.copyBytes(in, System.out, 4096, false);
        }
    }

    /** 9. 删除 */
    public static void delete(FileSystem fs, String remote) throws Exception {
        // 第二个参数 recursive:删除目录时必须为 true
        boolean ok = fs.delete(new Path(remote), true);
        System.out.println("delete " + remote + " => " + ok);
        // 注意:API 的 delete 不走回收站,删除即不可恢复
    }

    /** 10. 重命名 / 移动 */
    public static void rename(FileSystem fs, String src, String dst) throws Exception {
        boolean ok = fs.rename(new Path(src), new Path(dst));
        System.out.println("rename => " + ok);
    }

    /** 11. 判断存在 */
    public static void exists(FileSystem fs, String remote) throws Exception {
        System.out.println(remote + " exists => " + fs.exists(new Path(remote)));
    }

    /** 12. 遍历一层目录:listStatus */
    public static void listStatus(FileSystem fs, String dir) throws Exception {
        FileStatus[] list = fs.listStatus(new Path(dir));
        for (FileStatus st : list) {
            System.out.printf("%s\t%s\t%d\t%d\t%s\t%s\t%s%n",
                    st.isDirectory() ? "d" : "-",
                    st.getPermission(),            // 权限
                    st.getReplication(),           // 副本数
                    st.getLen(),                   // 长度(字节)
                    st.getOwner() + ":" + st.getGroup(),
                    st.getBlockSize(),             // 块大小
                    st.getPath());                 // 完整路径
            // 其他方法:getModificationTime()、getAccessTime()、isFile()、isSymlink()
        }
    }

    /** 13. 递归遍历所有文件:listFiles */
    public static void listFilesRecursive(FileSystem fs, String dir) throws Exception {
        RemoteIterator<LocatedFileStatus> it = fs.listFiles(new Path(dir), true);
        while (it.hasNext()) {
            LocatedFileStatus st = it.next();
            System.out.println(st.getPath() + "  size=" + st.getLen());
            for (BlockLocation bl : st.getBlockLocations()) {
                System.out.println("    block offset=" + bl.getOffset()
                        + " len=" + bl.getLength()
                        + " hosts=" + String.join(",", bl.getHosts()));
            }
        }
    }

    /** 14. 获取块位置信息 */
    public static void blockLocations(FileSystem fs, String remote) throws Exception {
        FileStatus status = fs.getFileStatus(new Path(remote));
        BlockLocation[] locations = fs.getFileBlockLocations(status, 0, status.getLen());
        for (int i = 0; i < locations.length; i++) {
            BlockLocation bl = locations[i];
            System.out.printf("block[%d] offset=%d length=%d hosts=%s racks=%s%n",
                    i, bl.getOffset(), bl.getLength(),
                    String.join(",", bl.getHosts()),
                    String.join(",", bl.getTopologyPaths()));
        }
    }

    /** 15. 修改副本数 */
    public static void setReplication(FileSystem fs, String remote, short rep) throws Exception {
        System.out.println("setReplication => " + fs.setReplication(new Path(remote), rep));
    }

    public static void main(String[] args) {
        // try-with-resources:FileSystem 实现了 Closeable,自动关闭
        try (FileSystem fs = getFileSystem()) {
            mkdir(fs, "/user/hadoop/demo");
            writeByStream(fs, "/user/hadoop/demo/hello.txt", "Hello world");
            append(fs, "/user/hadoop/demo/hello.txt", "second line");
            cat(fs, "/user/hadoop/demo/hello.txt");
            exists(fs, "/user/hadoop/demo/hello.txt");
            listStatus(fs, "/user/hadoop/demo");
            listFilesRecursive(fs, "/user/hadoop/demo");
            blockLocations(fs, "/user/hadoop/demo/hello.txt");
            setReplication(fs, "/user/hadoop/demo/hello.txt", (short) 3);
            download(fs, "/user/hadoop/demo/hello.txt", "D:/tmp/hello.txt");
            rename(fs, "/user/hadoop/demo/hello.txt", "/user/hadoop/demo/hello.bak");
            // delete(fs, "/user/hadoop/demo");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:FileSystem.get() 内部有缓存,同一 URI + 用户会返回同一实例。因此在 Web 应用等长生命周期程序中,不要在每次请求后 close() 掉共享实例,否则会影响其他线程;可用 FileSystem.newInstance() 获取独立实例,或设置 fs.hdfs.impl.disable.cache=true。

打包与运行

# 1. Maven 打包(建议用 shade 插件打成 fat jar,或用 hadoop jar 自动带上依赖)
mvn clean package -DskipTests

# 2. 用 hadoop jar 运行(Hadoop 会自动把自身依赖加入 classpath,最省事)
hadoop jar hdfs-demo-1.0.jar com.testing.HdfsDemo

# 3. 用 java 运行时需要手工拼 classpath
java -cp "hdfs-demo-1.0.jar:$(hadoop classpath)" com.testing.HdfsDemo

# 4. 指定运行用户
HADOOP_USER_NAME=hadoop hadoop jar hdfs-demo-1.0.jar com.testing.HdfsDemo
java -DHADOOP_USER_NAME=hadoop -cp "hdfs-demo-1.0.jar:$(hadoop classpath)" com.testing.HdfsDemo

# 5. 查看 classpath 内容
hadoop classpath

4.8 序列化与文件格式简述

HDFS 只负责存字节流,具体怎么组织记录由文件格式决定。选错格式是大数据性能问题的常见根源。

格式 类型 特点 可切分 适用场景
TextFile 行式(纯文本) 简单通用、可读;无 schema、体积大、解析慢 是(未压缩时) 原始日志落地、数据交换
SequenceFile 行式(二进制 K-V) Hadoop 原生格式,支持记录级/块级压缩,可切分(内含 sync 标记);只能被 Hadoop 生态读取 是 小文件合并容器(key=文件名,value=内容)、MapReduce 中间结果
Avro 行式(二进制) 自带 schema(JSON 描述),schema 演进能力强,跨语言 是 数据交换、Kafka 消息、写多读少的明细数据
Parquet 列式 列存 + 高压缩比 + 谓词下推 + 列裁剪;写入较慢 是 数仓分析主力格式,Spark/Hive/Impala/Presto 通用
ORC 列式 列存 + 轻量索引(行组统计、Bloom filter)+ 支持 ACID 是 Hive 数仓主力格式,尤其需要事务/更新时
RCFile 列式(早期) ORC 的前身,已不推荐新用 是 历史遗留

列式存储的优势:

  1. 列裁剪:SELECT a, b FROM t(表有 100 列)只需读 2 列的数据块,I/O 量降到 1/50;
  2. 压缩比高:同一列的数据类型相同、取值相近,字典编码 + RLE + 位图能获得远高于行式的压缩比(常见 3~10 倍);
  3. 谓词下推:文件/行组/页级别保存 min/max/count 统计信息,WHERE dt='2026-08-22' 可直接跳过整个行组,不解压不读取;
  4. 向量化执行:连续同类型数据便于按批处理与 CPU SIMD 优化。

代价:写入需要缓冲整个行组、CPU 编码开销大;不适合逐条追加与需要读取整行全部字段的点查场景。

Hive/Spark 中格式与压缩的组合选择 > 详见 阶段16 其他章节

五、小文件治理、压缩与性能调优

5.1 小文件问题的治理

5.1.1 危害的量化说明

假设 NameNode 中每个对象(文件 inode、块)约占 150 字节,块大小 128MB、3 副本:

场景 文件数 块数 NameNode 内存占用 说明
1000 个 128MB 文件(共 125GB) 1000 1000 约 300KB 理想情况
100 万个 128KB 文件(共 122GB) 100 万 100 万 约 300MB 同样数据量,元数据膨胀 1000 倍
1 亿个小文件 1 亿 1 亿 约 30GB 以上 NameNode 堆内存告急,GC 停顿严重

三重危害:

  1. NameNode 内存压力:如上表所示,元数据对象数量与文件数成正比,与数据量无关。堆内存增大后 Full GC 时间可达数十秒,期间整个集群不可用。
  2. 计算任务膨胀:MapReduce/Spark 默认按块(split)划分任务,100 万个小文件产生 100 万个 task。每个 task 的 JVM 启动与调度开销约几百毫秒到数秒,真正计算时间可能只有几毫秒,开销远大于计算。
  3. 随机 I/O 增加:读一个大文件是顺序读,读 100 万个小文件是 100 万次寻址,磁盘吞吐骤降。

5.1.2 HAR 归档文件(Hadoop Archive)

HAR 把大量小文件打包成一个 HAR 文件(本质是一个目录,含 _index、_masterindex、part-0 等),减少 NameNode 中的文件与块对象数量,同时仍可通过 har:// 协议按原路径访问其中的单个文件。

# 创建归档:-archiveName 归档名(必须以 .har 结尾)
#           -p 源文件的父目录
#           后接要归档的相对路径(可多个),最后是归档输出目录
hadoop archive -archiveName logs2026.har -p /user/hadoop/logs 08 09 /user/hadoop/archive

# 归档整个目录
hadoop archive -archiveName data.har -p /user/hadoop/data /user/hadoop/archive

# 指定归档任务的 reduce 数(归档本质是一个 MapReduce 作业)
hadoop archive -archiveName data.har -p /user/hadoop/data -r 3 /user/hadoop/archive

# 查看归档内容(用 har:// 协议)
hadoop fs -ls har:///user/hadoop/archive/logs2026.har
hadoop fs -ls -R har:///user/hadoop/archive/logs2026.har
hadoop fs -cat har:///user/hadoop/archive/logs2026.har/08/a.log

# 完整 URI 形式
hadoop fs -ls har://hdfs-192.168.224.10:9000/user/hadoop/archive/logs2026.har

# 解归档(从 har 中拷回普通目录)
hadoop fs -cp har:///user/hadoop/archive/logs2026.har/08 /user/hadoop/restore
hadoop distcp har:///user/hadoop/archive/logs2026.har/08 /user/hadoop/restore

# 归档完成并验证无误后,删除原始小文件以释放元数据
hadoop fs -rm -r -skipTrash /user/hadoop/logs/08

HAR 的局限:

  • 不可变:不能向已有 HAR 增删文件,只能重新创建;
  • 读性能有损:读 HAR 中一个文件要先读 _masterindex 与 _index 两级索引,多两次 I/O;
  • 不支持压缩:HAR 本身不做压缩,只做打包;
  • MapReduce 并行度不变:HAR 中每个原始小文件依然是独立的 split(除非配合 CombineFileInputFormat),因此不解决 task 过多的问题,只解决 NameNode 内存问题。

5.1.3 SequenceFile 合并

用 SequenceFile 作为容器:key = 小文件名,value = 小文件内容(BytesWritable)。相比 HAR 的优势是支持块级压缩且可切分,因此既减少元数据又减少 task 数。

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.io.*;
import org.apache.hadoop.io.SequenceFile.Writer;
import org.apache.hadoop.io.compress.SnappyCodec;

public class SmallFilesToSequenceFile {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://192.168.224.10:9000");
        FileSystem fs = FileSystem.get(conf);

        Path outPath = new Path("/user/hadoop/merged.seq");
        try (Writer writer = SequenceFile.createWriter(conf,
                Writer.file(outPath),
                Writer.keyClass(Text.class),
                Writer.valueClass(BytesWritable.class),
                // 块级压缩,可切分
                Writer.compression(SequenceFile.CompressionType.BLOCK, new SnappyCodec()))) {

            RemoteIterator<LocatedFileStatus> it =
                    fs.listFiles(new Path("/user/hadoop/smallfiles"), true);
            while (it.hasNext()) {
                LocatedFileStatus st = it.next();
                if (st.getLen() == 0) continue;
                byte[] buf = new byte[(int) st.getLen()];
                try (FSDataInputStream in = fs.open(st.getPath())) {
                    IOUtils.readFully(in, buf, 0, buf.length);
                }
                writer.append(new Text(st.getPath().getName()), new BytesWritable(buf));
            }
        }
        fs.close();
    }
}
# 查看 SequenceFile 内容
hadoop fs -text /user/hadoop/merged.seq | head

5.1.4 CombineFileInputFormat

不改变 HDFS 上的文件布局,而是在计算侧把多个小文件(可跨节点、跨机架)合并进同一个 InputSplit,从而把 task 数从"文件数"降到"合理数量"。这是治小文件"task 爆炸"最直接的手段。

// MapReduce 中使用
job.setInputFormatClass(CombineTextInputFormat.class);
// 每个 split 的最大字节数:128MB
CombineTextInputFormat.setMaxInputSplitSize(job, 134217728L);
CombineTextInputFormat.setMinInputSplitSizeNode(job, 67108864L);   // 同节点最小
CombineTextInputFormat.setMinInputSplitSizeRack(job, 67108864L);   // 同机架最小

对应的引擎侧参数:

# Hive:在 map 阶段合并小文件输入
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
set mapreduce.input.fileinputformat.split.maxsize=268435456;
# Hive:在输出阶段合并小文件
set hive.merge.mapfiles=true;
set hive.merge.mapredfiles=true;
set hive.merge.size.per.task=268435456;
set hive.merge.smallfiles.avgsize=134217728;

# Spark:写出时控制分区数量
# df.repartition(10).write... 或 df.coalesce(10).write...
set spark.sql.files.maxPartitionBytes=134217728;
set spark.sql.adaptive.enabled=true;
set spark.sql.adaptive.coalescePartitions.enabled=true;

Hive / Spark 参数的完整说明 > 详见 阶段16 其他章节

5.1.5 上游合并(治本之道)

  • 采集端攒批:Flume/Logstash 配置 rollSize/rollInterval,让单个落地文件接近一个块大小再滚动,而不是每分钟一个文件;
  • Kafka + 流式写入:Flink/Spark Streaming 的 checkpoint 间隔调大(如 5~10 分钟),并开启文件合并/紧凑(compaction);
  • 分区粒度合理化:按天分区而不是按分钟分区,避免分区数暴涨带来的小文件;
  • 定期合并作业:每天凌晨跑一个合并任务,把前一天的小文件重写为大文件(INSERT OVERWRITE 或 distcp + 重分区)。

5.1.6 HDFS 联邦缓解元数据压力

当小文件治理仍不足以让单个 NameNode 承载全部元数据时,用 Federation 横向拆分命名空间:把 /user、/hive、/hbase 分给不同的 NameNode,各自管理独立的 namespace 与 block pool,共用同一组 DataNode。这样元数据总量被分摊到多个 NameNode 的堆内存中,同时 RPC 吞吐也被分摊。

详见 16-02-Hadoop架构与安装部署.md

5.2 压缩

压缩能显著减少存储占用与网络/磁盘 I/O,但会增加 CPU 开销。是否可切分(splittable)是最关键的属性:不可切分的压缩文件只能由一个 task 处理,一个 5GB 的 gzip 文件会让整个作业退化成单线程。

格式 编解码器类 扩展名 压缩比 压缩/解压速度 是否可切分 是否需额外安装
DEFLATE org.apache.hadoop.io.compress.DefaultCodec .deflate 中 中 否 否
gzip org.apache.hadoop.io.compress.GzipCodec .gz 高(约 2.7:1) 中 否 否
bzip2 org.apache.hadoop.io.compress.BZip2Codec .bz2 最高(约 3.4:1) 最慢 是(唯一原生可切分) 否
LZO com.hadoop.compression.lzo.LzopCodec .lzo 低(约 2:1) 快 建索引后可切分 是(GPL 协议,需单独安装 hadoop-lzo)
Snappy org.apache.hadoop.io.compress.SnappyCodec .snappy 低(约 2:1) 最快 否 3.x 内置;2.x 需 native 库
LZ4 org.apache.hadoop.io.compress.Lz4Codec .lz4 低 最快 否 内置
Zstandard org.apache.hadoop.io.compress.ZStandardCodec .zst 高(可调级别) 快 否 3.x 内置

配置:

<!-- core-site.xml:注册可用的编解码器 -->
<property>
  <name>io.compression.codecs</name>
  <value>
    org.apache.hadoop.io.compress.GzipCodec,
    org.apache.hadoop.io.compress.DefaultCodec,
    org.apache.hadoop.io.compress.BZip2Codec,
    org.apache.hadoop.io.compress.SnappyCodec,
    org.apache.hadoop.io.compress.Lz4Codec
  </value>
</property>
<!-- mapred-site.xml:MapReduce 中间结果与最终输出压缩 -->
<property>
  <name>mapreduce.map.output.compress</name>
  <value>true</value>
  <description>压缩 map 输出,减少 shuffle 网络传输,推荐开启</description>
</property>
<property>
  <name>mapreduce.map.output.compress.codec</name>
  <value>org.apache.hadoop.io.compress.SnappyCodec</value>
  <description>中间结果追求速度,选 Snappy/LZ4</description>
</property>
<property>
  <name>mapreduce.output.fileoutputformat.compress</name>
  <value>true</value>
</property>
<property>
  <name>mapreduce.output.fileoutputformat.compress.codec</name>
  <value>org.apache.hadoop.io.compress.GzipCodec</value>
</property>
<property>
  <name>mapreduce.output.fileoutputformat.compress.type</name>
  <value>BLOCK</value>
  <description>SequenceFile 输出的压缩粒度:NONE / RECORD / BLOCK,推荐 BLOCK</description>
</property>

适用场景选择:

  • 冷数据长期归档:bzip2 或 gzip(压缩比优先,访问频率低,CPU 开销可接受);
  • 需要被 MapReduce 直接切分处理的大文本:bzip2、或 LZO + 索引、或改用列式格式(Parquet/ORC)内部压缩(推荐做法:列式格式本身可切分,内部块用 Snappy 压缩,兼得压缩比与并行度);
  • shuffle 中间结果:Snappy 或 LZ4(速度优先);
  • 单个文件小于一个块:可切分性无关紧要,此时可自由选 gzip;
  • 通用推荐组合:Parquet/ORC + Snappy(或 ORC + Zstd),几乎适用于所有数仓场景。
# 为 LZO 文件建索引使其可切分
hadoop jar hadoop-lzo.jar com.hadoop.compression.lzo.DistributedLzoIndexer /user/hadoop/data.lzo

# 检查本地压缩库是否可用(Snappy/LZ4/zstd 需要 native 支持)
hadoop checknative -a

5.3 性能调优

5.3.1 NameNode 堆内存估算与 GC

经验估算:每 100 万个块约需 1GB 堆内存(含文件对象、块对象、DataNode 映射与运行时开销)。

预估堆大小(GB) ≈ (文件数 + 块数) × 150B / 1024^3 × 3(安全冗余系数)
例:5000 万文件 + 6000 万块 ≈ 1.1 亿 × 150B ≈ 16.5GB → 配置 48GB 较稳妥
# hadoop-env.sh
export HDFS_NAMENODE_OPTS="-Xms48g -Xmx48g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+ParallelRefProcEnabled \
  -XX:+UnlockExperimentalVMOptions \
  -XX:G1NewSizePercent=20 \
  -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
  -Xloggc:${HADOOP_LOG_DIR}/nn-gc.log \
  -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=64m"

# 2.x 中变量名为 HADOOP_NAMENODE_OPTS
export HADOOP_DATANODE_OPTS="-Xms4g -Xmx4g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC"

要点:-Xms 与 -Xmx 设为相同值避免堆动态伸缩;堆大于 32GB 时失去指针压缩收益,且 CMS 的 Full GC 停顿可达分钟级,因此大堆务必用 G1GC。

5.3.2 RPC handler 数量

NameNode 用固定数量的 handler 线程处理客户端与 DataNode 的 RPC 请求。数量太少会造成 RPC 排队(表现为 RpcQueueTimeAvgTime 升高、客户端操作变慢)。

经验公式:handler 数 = 20 × log₂(集群节点数),并向上取整到 10 的倍数。

集群规模 计算值 建议配置
10 节点 20 × log₂10 ≈ 66 70
50 节点 20 × log₂50 ≈ 113 120
100 节点 20 × log₂100 ≈ 133 140
500 节点 20 × log₂500 ≈ 179 180
<property>
  <name>dfs.namenode.handler.count</name>
  <value>120</value>
  <description>处理客户端 RPC 的线程数,默认 10(生产环境远远不够)</description>
</property>
<property>
  <name>dfs.namenode.service.handler.count</name>
  <value>60</value>
  <description>处理 DataNode 心跳/块报告的线程数(配置了 servicerpc-address 时生效)</description>
</property>
<property>
  <name>dfs.datanode.handler.count</name>
  <value>20</value>
  <description>DataNode 端 RPC 线程数,默认 10</description>
</property>
<property>
  <name>dfs.datanode.max.transfer.threads</name>
  <value>8192</value>
  <description>DataNode 同时处理的数据传输线程上限(旧名 dfs.datanode.max.xcievers),
  默认 4096。HBase 等高并发场景需调大,否则报 xcievers exceeded</description>
</property>

5.3.3 块大小的选择

场景 建议块大小 理由
文件多为几百 MB,作业多而小 128MB(默认) 平衡并行度与元数据
文件为 GB~TB 级,扫描型作业 256MB / 512MB 减少块数与元数据,提升顺序吞吐
文件普遍小于 128MB 保持 128MB,重点治理小文件 减小块大小不会节省磁盘,只会增加元数据
# 单次上传指定块大小与副本数(客户端参数优先于服务端默认值)
hadoop fs -D dfs.blocksize=268435456 -D dfs.replication=2 -put bigfile /user/hadoop/

5.3.4 副本数与可靠性/成本的权衡

副本数 存储放大 可容忍同时故障节点数 适用场景
1 1.0x 0(节点挂即丢数据) 伪分布式、可重算的临时中间数据
2 2.0x 1 冷数据、可从上游重新拉取的数据
3 3.0x 2 默认,生产标准配置
5+ 5.0x 4 极热数据(提升并发读带宽)、跨机房
EC (6,3) 1.5x 3 冷数据归档(3.x)
# 分层管理:热数据 3 副本,冷数据 2 副本
hadoop fs -setrep -R 3 /user/hadoop/hot
hadoop fs -setrep -R 2 /user/hadoop/cold

5.3.5 其他关键调优项

<!-- 短路读取(本地读性能,HBase 必开) -->
<property><name>dfs.client.read.shortcircuit</name><value>true</value></property>
<property><name>dfs.domain.socket.path</name><value>/var/lib/hadoop-hdfs/dn_socket</value></property>

<!-- DataNode 多磁盘与选盘策略 -->
<property>
  <name>dfs.datanode.data.dir</name>
  <value>/data1/dfs/dn,/data2/dfs/dn,/data3/dfs/dn,/data4/dfs/dn</value>
  <description>每块盘一个目录(JBOD),不要做 RAID0;HDFS 自身已有副本冗余</description>
</property>
<property>
  <name>dfs.datanode.fsdataset.volume.choosing.policy</name>
  <value>org.apache.hadoop.hdfs.server.datanode.fsdataset.AvailableSpaceVolumeChoosingPolicy</value>
  <description>选盘策略。默认 RoundRobinVolumeChoosingPolicy 轮询;
  当各盘容量不一致(如新旧盘混插)时改用 AvailableSpace 策略,优先写空闲多的盘</description>
</property>
<property>
  <name>dfs.datanode.available-space-volume-choosing-policy.balanced-space-threshold</name>
  <value>10737418240</value>
  <description>盘间可用空间差异超过 10GB 才认为不均衡</description>
</property>
<property>
  <name>dfs.datanode.available-space-volume-choosing-policy.balanced-space-preference-fraction</name>
  <value>0.75</value>
  <description>75% 的新块优先写到空闲较多的盘</description>
</property>
<property>
  <name>dfs.datanode.du.reserved</name>
  <value>21474836480</value>
  <description>每块盘为非 HDFS 用途保留的空间(此处 20GB),防止磁盘 100% 写满导致节点异常</description>
</property>
<property>
  <name>dfs.datanode.failed.volumes.tolerated</name>
  <value>1</value>
  <description>允许坏几块盘而不让整个 DataNode 下线。默认 0 表示一块盘坏就整节点退出</description>
</property>

<!-- 机架感知脚本 -->
<property>
  <name>net.topology.script.file.name</name>
  <value>/usr/local/hadoop/etc/hadoop/rack-topology.sh</value>
</property>
<property>
  <name>net.topology.script.number.args</name>
  <value>100</value>
  <description>一次传给脚本的最大参数个数</description>
</property>

操作系统层调优:

# 1. 文件句柄数与进程数(HDFS 打开的文件与 socket 极多,默认 1024 远远不够)
cat >> /etc/security/limits.conf <<'EOF'
hdfs   soft   nofile   65536
hdfs   hard   nofile   131072
hdfs   soft   nproc    65536
hdfs   hard   nproc    65536
*      soft   nofile   65536
*      hard   nofile   131072
EOF
ulimit -n     # 验证

# 2. 关闭透明大页 THP(THP 的内存整理会造成 JVM 长时间停顿)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化:写入 /etc/rc.local 或 grub 参数 transparent_hugepage=never
cat /sys/kernel/mm/transparent_hugepage/enabled   # 验证应显示 [never]

# 3. 降低 swap 倾向(避免 JVM 堆被换出导致 GC 超长)
sysctl -w vm.swappiness=1
echo "vm.swappiness=1" >> /etc/sysctl.conf

# 4. 文件系统挂载参数:关闭 atime 更新,减少写放大
# /etc/fstab: /dev/sdb1 /data1 ext4 defaults,noatime,nodiratime 0 0

# 5. 网络参数
sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=8192

# 6. 关闭防火墙或放行 HDFS 端口
systemctl stop firewalld && systemctl disable firewalld
# 或按需放行
firewall-cmd --permanent --add-port=8020/tcp    # NameNode RPC
firewall-cmd --permanent --add-port=50010/tcp   # DataNode 数据传输(3.x 为 9866)
firewall-cmd --permanent --add-port=50070/tcp   # NameNode Web UI(3.x 为 9870)
firewall-cmd --reload

5.4 异构存储与存储策略简述

HDFS 支持在同一集群中混用不同介质,并按目录设置存储策略,实现冷热分层。

DataNode 上为每个目录标注介质类型:

<property>
  <name>dfs.datanode.data.dir</name>
  <value>[SSD]file:///ssd1/dfs/dn,[DISK]file:///data1/dfs/dn,[ARCHIVE]file:///arch1/dfs/dn,[RAM_DISK]file:///mnt/ramdisk/dfs/dn</value>
</property>
<property>
  <name>dfs.storage.policy.enabled</name>
  <value>true</value>
</property>

四种介质类型:

类型 含义
RAM_DISK 内存盘(tmpfs),用于 Lazy Persist 写入,极快但断电即失
SSD 固态盘,高 IOPS,用于热数据
DISK 普通机械盘,默认类型
ARCHIVE 高密度低性能存储,用于归档冷数据

内置存储策略:

策略名 ID 块放置(副本分布) 用途
Lazy_Persist 15 1 副本 RAM_DISK,其余 DISK 临时小数据超低延迟写
All_SSD 12 全部 SSD 极热数据
One_SSD 10 1 副本 SSD,其余 DISK 热数据,兼顾成本
Hot(默认) 7 全部 DISK 常规数据
Warm 5 1 副本 DISK,其余 ARCHIVE 温数据
Cold 2 全部 ARCHIVE 冷数据归档
# 查看/设置/取消存储策略
hdfs storagepolicies -listPolicies
hdfs storagepolicies -getStoragePolicy -path /user/hadoop/cold
hdfs storagepolicies -setStoragePolicy -path /user/hadoop/cold -policy COLD
hdfs storagepolicies -setStoragePolicy -path /user/hadoop/hot -policy ONE_SSD
hdfs storagepolicies -unsetStoragePolicy -path /user/hadoop/cold

# 设置策略只影响新写入的块;已有数据需要用 Mover 迁移到目标介质
hdfs mover -p /user/hadoop/cold

5.5 纠删码(Erasure Coding)简述

Hadoop 3.x 新特性。三副本的存储放大是 3.0x(存 1PB 数据要 3PB 磁盘),对冷数据来说成本过高。纠删码借用 RAID 的思路,用 Reed-Solomon 编码替代副本:

原理:把数据切成 k 个数据单元,计算出 m 个校验单元,共 k+m 个单元分布在不同节点上。任意丢失不超过 m 个单元都能通过解码恢复原始数据。

策略 k 数据 + m 校验 存储放大 可容忍故障数 对比 3 副本
RS-3-2-1024k 3 + 2 1.67x 2 省 44% 空间,容错相同
RS-6-3-1024k(常用) 6 + 3 1.5x 3 省 50% 空间,容错更强
RS-10-4-1024k 10 + 4 1.4x 4 省 53%,需 ≥14 节点
XOR-2-1-1024k 2 + 1 1.5x 1 计算最轻

代价(因此只适合冷数据):

  1. CPU 开销:写入要做 RS 编码,读取降级时要做解码,CPU 消耗明显高于副本方式;
  2. 失去数据本地性:EC 采用条带式(striping)布局,一个逻辑块的各单元分散在 k+m 个不同节点上,计算任务无法在"本地"读到完整块,读取必然产生网络流量;
  3. 恢复代价大:修复一个丢失单元需要读取其余 k 个单元并重新解码(副本方式只需拷贝一个副本),恢复期间网络与 CPU 压力大;
  4. 节点数要求:至少需要 k+m 个 DataNode(RS-6-3 至少 9 个,建议更多);
  5. 不支持追加与 hflush/hsync,功能受限。
# 查看可用策略与已启用策略
hdfs ec -listPolicies

# 启用某个策略(默认只有 RS-6-3-1024k 是 enabled)
hdfs ec -enablePolicy -policy RS-3-2-1024k
hdfs ec -disablePolicy -policy RS-3-2-1024k

# 对目录设置 EC 策略(只影响之后写入该目录的新文件)
hdfs ec -setPolicy -path /user/hadoop/archive -policy RS-6-3-1024k
hdfs ec -getPolicy -path /user/hadoop/archive
hdfs ec -unsetPolicy -path /user/hadoop/archive

# 查看某文件是否为 EC 文件
hdfs fsck /user/hadoop/archive -files -blocks

# 把已有的副本数据转换为 EC:需要重写(拷贝到已设策略的目录)
hadoop distcp /user/hadoop/old /user/hadoop/archive/old

实践建议:EC 用于 3 个月以上不再访问的归档数据;活跃分析数据仍用 3 副本。

六、排错清单

6.1 Name node is in safe mode,写入失败

put: Cannot create file/user/hadoop/a.txt. Name node is in safe mode.
org.apache.hadoop.hdfs.server.namenode.SafeModeException

原因与排查:

  1. NameNode 刚启动,正在等待 DataNode 上报块报告(正常现象,等几分钟即可);
  2. 上报块比例未达到 dfs.namenode.safemode.threshold-pct(0.999),说明有块丢失或 DataNode 未全部启动;
  3. 磁盘空间不足或元数据目录不可写,NameNode 主动进入安全模式。
# 1. 确认状态与原因(3.x 的 get 会给出详细原因说明)
hdfs dfsadmin -safemode get
# 查看 NameNode 日志中的 "The reported blocks X needs additional Y blocks"
tail -200 $HADOOP_LOG_DIR/hadoop-hdfs-namenode-*.log

# 2. 检查 DataNode 是否都起来了
hdfs dfsadmin -report | grep -E "Live datanodes|Dead datanodes"
jps    # 在各 DataNode 上确认 DataNode 进程存在

# 3. 检查是否有丢块
hdfs fsck / | tail -30

# 4. 若确认块已丢且可接受,先离开安全模式再清理
hdfs dfsadmin -safemode leave
hdfs fsck / -delete            # 谨慎:会删除含丢块的整个文件

# 5. 脚本中等待自动退出
hdfs dfsadmin -safemode wait

6.2 Missing blocks / Corrupt blocks

There are 12 missing blocks. The following files may be corrupted:
blk_1073742001  /user/hadoop/data/part-00003
# 1. 定位受影响的文件
hdfs fsck / | grep -E "MISSING|CORRUPT"
hdfs fsck / -list-corruptfileblocks
hdfs dfsadmin -metasave metasave.txt    # 结果在 $HADOOP_LOG_DIR/metasave.txt

# 2. 判断成因
hdfs dfsadmin -report          # 是否有 DataNode dead?磁盘是否被换掉/格式化?
# 常见成因:多个持有副本的 DataNode 同时下线;DataNode 数据目录被误删;
#          磁盘故障;单副本文件所在节点故障

# 3. DataNode 只是临时下线 → 启动它,块会自动恢复
hdfs --daemon start datanode        # 3.x
hadoop-daemon.sh start datanode     # 2.x

# 4. 数据确实无法恢复 → 删除损坏文件
hdfs fsck /user/hadoop/data -delete
# 或先移到 /lost+found 留证
hdfs fsck /user/hadoop/data -move

# 5. 事后预防:不要用单副本存重要数据;重要目录开启快照
hdfs dfs -setrep -R 3 /user/hadoop/important
hdfs dfsadmin -allowSnapshot /user/hadoop/important

6.3 小文件过多导致 NameNode OOM

java.lang.OutOfMemoryError: GC overhead limit exceeded
(或 NameNode Full GC 频繁、Web UI 打不开、RPC 超时)
# 1. 确认小文件规模:看 fsck 的平均块大小
hdfs fsck / | grep "avg. block size"
# 平均块远小于 128MB(如几十 KB)即为典型小文件集群

# 2. 找出小文件最多的目录
hadoop fs -count -h -v /user/*/
hdfs dfs -du -h /user | sort -h

# 3. 短期缓解:扩大 NameNode 堆并换 G1GC
export HDFS_NAMENODE_OPTS="-Xms64g -Xmx64g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 4. 中期治理:归档 + 合并 + 删原始
hadoop archive -archiveName old.har -p /user/hadoop/logs 2025 /user/hadoop/archive
hadoop fs -rm -r -skipTrash /user/hadoop/logs/2025

# 5. 长期治本:上游攒批写、定期合并作业、CombineFileInputFormat、Federation

6.4 DataNode 磁盘满或变 unhealthy

DataNode 日志:All specified directories have failed to load / 
Too many failed volumes / DiskChecker$DiskErrorException
Web UI 中该节点显示 unhealthy 或直接消失
# 1. 检查磁盘使用与挂载
df -h
mount | grep /data
dmesg | tail -50        # 查看是否有磁盘 I/O 错误

# 2. 检查 HDFS 视角的节点容量
hdfs dfsadmin -report | grep -A12 "Name: <该节点IP>"

# 3. 磁盘满的处理
#    a) 提高保留空间避免写到 100%
#       dfs.datanode.du.reserved = 21474836480 (20GB)
#    b) 清理无用数据与回收站
hadoop fs -expunge
hadoop fs -du -h /user/*/.Trash
hadoop fs -rm -r -skipTrash /tmp/old_staging
#    c) 节点间均衡
hdfs dfsadmin -setBalancerBandwidth 52428800
hdfs balancer -threshold 5
#    d) 节点内多盘均衡
hdfs diskbalancer -plan dn1 && hdfs diskbalancer -execute /system/diskbalancer/.../dn1.plan.json

# 4. 单块盘损坏:允许容忍并重启该 DataNode
#    dfs.datanode.failed.volumes.tolerated = 1
#    从 dfs.datanode.data.dir 中摘掉坏盘目录后重启 DataNode
hdfs --daemon stop datanode && hdfs --daemon start datanode

# 5. 检查 DataNode 本地目录权限(常见于手工改动后)
ls -ld /data1/dfs/dn
chown -R hdfs:hadoop /data1/dfs/dn

6.5 副本数不足(Under-replicated blocks)

# 1. 查看数量
hdfs fsck / | grep "Under-replicated"
hdfs dfsadmin -report | grep "Under replicated blocks"

# 2. 常见原因
#    a) 有 DataNode 下线(等它回来或等自动复制完成)
#    b) 存活 DataNode 数 < 副本数(如 2 节点集群设了 3 副本,永远补不齐)
#    c) 所有节点磁盘都接近写满,没有空间放新副本
#    d) 复制速度慢,积压

# 3. 若节点数就是不够,把副本数降到实际可满足的值
hdfs dfs -setrep -R 2 /
# 同时修改 hdfs-site.xml 的 dfs.replication 避免新文件再次不足

# 4. 加速复制
hdfs dfsadmin -setBalancerBandwidth 52428800
# hdfs-site.xml:
#   dfs.namenode.replication.work.multiplier.per.iteration = 10
#   dfs.namenode.replication.max-streams = 20

# 5. 强制重新评估某目录的副本
hdfs dfs -setrep -R -w 3 /user/hadoop/important

6.6 put 报 Permission denied

org.apache.hadoop.security.AccessControlException: Permission denied:
user=Administrator, access=WRITE, inode="/user/hadoop":hadoop:supergroup:drwxr-xr-x

原因:客户端上报的用户名(Windows 下常是 Administrator,Linux 下是当前登录用户)与 HDFS 目录的 owner 不一致,且 other 位无写权限。

# 方案一(推荐):显式指定 HDFS 用户身份
export HADOOP_USER_NAME=hadoop         # Linux/Mac
set HADOOP_USER_NAME=hadoop            # Windows CMD
$env:HADOOP_USER_NAME="hadoop"         # PowerShell
# Java 中:-DHADOOP_USER_NAME=hadoop 或 System.setProperty("HADOOP_USER_NAME","hadoop")
# 或 FileSystem.get(new URI(...), conf, "hadoop")

# 方案二:改目录归属或权限
hadoop fs -chown -R Administrator:supergroup /user/Administrator
hadoop fs -chmod -R 775 /user/hadoop
hadoop fs -mkdir -p /user/Administrator && hadoop fs -chown Administrator /user/Administrator

# 方案三:用 ACL 精确授权
hadoop fs -setfacl -m user:Administrator:rwx /user/hadoop

# 方案四(仅实验环境):放开全部权限,或直接关闭权限检查
hadoop fs -chmod -R 777 /
# hdfs-site.xml: dfs.permissions.enabled = false (需重启 NameNode,生产禁用)

6.7 File could only be replicated to 0 nodes instead of minReplication (=1)

org.apache.hadoop.ipc.RemoteException(java.io.IOException): File /user/hadoop/a.txt
could only be replicated to 0 nodes instead of minReplication (=1).
There are 0 datanode(s) running and no node(s) are excluded in this operation.

这是最经典的 HDFS 报错,含义是"NameNode 找不到任何可用于放置副本的 DataNode"。逐项排查:

# 原因 1:DataNode 根本没启动
jps                                    # 各节点上确认有 DataNode 进程
hdfs dfsadmin -report | head -20       # Live datanodes 是否为 0
hdfs --daemon start datanode

# 原因 2:DataNode 启动了但注册失败——clusterID 不一致
#   (典型场景:重复执行了 hdfs namenode -format)
cat /data1/dfs/dn/current/VERSION      # DataNode 的 clusterID
cat /data1/dfs/name/current/VERSION    # NameNode 的 clusterID
#   二者不同 → 停掉集群,删除 DataNode 数据目录后重启(会丢数据),或手工改写 VERSION 中的 clusterID
tail -100 $HADOOP_LOG_DIR/hadoop-hdfs-datanode-*.log | grep -i "Incompatible clusterIDs"

# 原因 3:所有 DataNode 空间不足(可用空间 < dfs.datanode.du.reserved + blocksize)
hdfs dfsadmin -report | grep -E "DFS Remaining|Non DFS Used"
df -h

# 原因 4:防火墙/SELinux 阻断了 DataNode 与 NameNode 或客户端的通信
systemctl status firewalld
systemctl stop firewalld && systemctl disable firewalld
setenforce 0

# 原因 5:DataNode 全部被判定为 unhealthy(磁盘坏/目录权限错)
ls -ld /data1/dfs/dn && chown -R hdfs:hadoop /data1/dfs/dn

# 原因 6:安全模式(另见 6.1)
hdfs dfsadmin -safemode get

# 原因 7:所有 DataNode 都在 exclude 名单里(正在退役)
cat $HADOOP_CONF_DIR/dfs.hosts.exclude

6.8 客户端能连上 NameNode,但读写卡住/超时

DFSClient: Failed to connect to /192.168.224.11:50010 for block ... 
java.net.SocketTimeoutException / java.net.ConnectException: Connection refused
Could not obtain block: blk_xxx file=/user/hadoop/a.txt

原因:ls、mkdir 等只与 NameNode 交互,所以看起来"能连上";但真正读写数据要直连每一个 DataNode 的数据传输端口,该端口未放行或不可达。

# 1. 从客户端逐个测试 DataNode 数据端口连通性
telnet 192.168.224.11 50010     # Hadoop 2.x
telnet 192.168.224.11 9866      # Hadoop 3.x
nc -zv 192.168.224.11 50010

# 2. 服务端放行端口
firewall-cmd --permanent --add-port=50010/tcp
firewall-cmd --permanent --add-port=50020/tcp
firewall-cmd --permanent --add-port=50075/tcp
firewall-cmd --reload
# 或直接关闭(实验环境)
systemctl stop firewalld

# 3. 确认 DataNode 监听地址不是只绑 127.0.0.1
netstat -tlnp | grep -E "50010|9866"
# hdfs-site.xml 中确保:
#   dfs.datanode.address = 0.0.0.0:50010

6.9 hostname 解析问题:客户端拿到内网地址连不上

现象:NameNode 返回的块位置是 DataNode 的主机名或内网 IP(如 dn1、172.17.0.3),客户端所在网络无法解析或无法路由到该地址,于是读写超时。这在"Windows 客户端 + Linux 集群"、"公网访问云上集群"、"Docker/K8s 部署"三种场景中极为常见。

# 方案一:在客户端配置 hosts 映射(最简单)
# Windows: C:\Windows\System32\drivers\etc\hosts
# Linux:   /etc/hosts
192.168.224.10  nn  namenode
192.168.224.11  dn1
192.168.224.12  dn2
192.168.224.13  dn3

# 方案二:让客户端改用 IP 而不是主机名与 DataNode 通信
<!-- 客户端侧 hdfs-site.xml / 代码中 conf.set -->
<property>
  <name>dfs.client.use.datanode.hostname</name>
  <value>true</value>
  <description>客户端用 DataNode 的 hostname 连接(配合客户端 hosts 或 DNS 使用);
  跨网络访问云主机时通常需要设为 true 并保证 hostname 能解析为公网地址</description>
</property>
<property>
  <name>dfs.datanode.use.datanode.hostname</name>
  <value>true</value>
  <description>DataNode 之间也用 hostname 通信</description>
</property>
<property>
  <name>dfs.namenode.datanode.registration.ip-hostname-check</name>
  <value>false</value>
  <description>关闭 DataNode 注册时的 IP 与 hostname 反解一致性校验,
  在 DNS 不完备的环境下避免 DataNode 注册失败</description>
</property>
// 代码中设置
conf.set("dfs.client.use.datanode.hostname", "true");
# 验证:确认集群内 hostname 正反解析一致
hostname -f
ping dn1
nslookup 192.168.224.11
hdfs dfsadmin -report | grep -E "Name:|Hostname:"
# 确认 /etc/hosts 中没有把主机名解析到 127.0.0.1(这会导致 DataNode 上报 127.0.0.1)
cat /etc/hosts

6.10 getmerge / cat 大文件的内存与磁盘风险

# 危险操作 1:getmerge 一个 TB 级目录到本地
hadoop fs -getmerge /user/hive/warehouse/big_table /home/hadoop/all.txt
# → 本地磁盘瞬间写满,或 /home 分区打满导致系统异常

# 危险操作 2:cat 一个超大文件到终端或变量
hadoop fs -cat /user/hadoop/10TB.log
VAR=$(hadoop fs -cat /user/hadoop/10TB.log)     # → shell 内存爆掉

# 危险操作 3:Java 中一次性读入整个文件
# byte[] buf = new byte[(int) status.getLen()];   // 文件大于 2GB 直接溢出 int,且 OOM

安全做法:

# 1. 先看大小再决定
hadoop fs -du -s -h /user/hive/warehouse/big_table
df -h /home                     # 确认本地剩余空间足够

# 2. 只看头尾,不要全量输出
hadoop fs -cat /user/hadoop/big.log | head -100
hadoop fs -head /user/hadoop/big.log
hadoop fs -tail /user/hadoop/big.log

# 3. 用管道流式处理,不落地
hadoop fs -cat /user/hadoop/big.log | awk '{print $1}' | sort | uniq -c > /tmp/stat.txt
hadoop fs -cat "/user/hadoop/part-*" | gzip > /tmp/all.gz

# 4. 需要合并就在 HDFS 内部合并(用计算引擎),不要拉到单机
# Hive:  INSERT OVERWRITE TABLE t2 SELECT * FROM t1;   (配合 merge 参数)
# Spark: df.coalesce(1).write.text("/user/hadoop/merged")
hadoop distcp /src /dst

# 5. Java 中务必流式读取,固定缓冲区
# IOUtils.copyBytes(in, out, 4096, false);

其他常见问题速查

现象 常见原因与处理
Incompatible clusterIDs 重复 format 过 NameNode。同步 VERSION 中的 clusterID,或清空 DataNode 数据目录
NoSuchMethodError / ClassNotFoundException 客户端与服务端 Hadoop 版本或 jar 冲突;用 hadoop classpath,避免混入不同版本 jar
Unable to load native-hadoop library 只是警告,native 库缺失。影响 Snappy/LZ4,用 hadoop checknative -a 检查
HADOOP_HOME and hadoop.home.dir are unset(Windows) 需下载 winutils.exe 与 hadoop.dll 放入 %HADOOP_HOME%\bin 并设置环境变量
Operation category READ is not supported in state standby HA 集群中访问了 Standby NameNode,应使用 nameservice 名(hdfs://mycluster)而非具体主机
Lease expired / No lease on file 写入客户端异常退出或 GC 过久,租约被回收;检查客户端稳定性与网络
xcievers exceeded 调大 dfs.datanode.max.transfer.threads(如 8192)
Too many open files 调大 ulimit -n(65536)
hdfs dfs -ls 卡住无响应 NameNode Full GC 或 RPC 队列积压;查 GC 日志,调大堆与 dfs.namenode.handler.count

七、本章小结

分布式文件系统是大数据时代解决大规模数据存储问题的有效解决方案。HDFS 开源实现了 GFS,可以利用由廉价硬件构成的计算机集群实现海量数据的分布式存储。

HDFS 具有兼容廉价硬件设备、流数据读写、大数据集、简单文件模型、强大跨平台兼容性等特点;同时也有自身局限性,如不适合低延迟数据访问、无法高效存储大量小文件、不支持多用户写入及任意修改文件等。

块是 HDFS 的核心概念,一个大文件会被拆分成很多个块。HDFS 采用抽象的块概念,具有支持大规模文件存储、简化系统设计、适合数据备份等优点。

HDFS 采用主从(Master/Slave)结构模型,一个 HDFS 集群包括一个名称节点和若干个数据节点。名称节点负责管理分布式文件系统的命名空间;数据节点是 HDFS 的工作节点,负责数据的存储和读取。

HDFS 采用冗余数据存储,增强了数据可靠性,加快了数据传输速度;还采用相应的数据存放、数据读取和数据复制策略来提升系统整体读写响应性能。HDFS 把硬件出错看作一种常态,设计了错误恢复机制。

本章还介绍了 HDFS 的数据读写过程、完整的 Shell 命令体系、Java API 编程实践,以及小文件治理、压缩、性能调优与常见故障排查方法。

版本差异速查表

项目 Hadoop 2.x Hadoop 3.x
默认块大小 128MB(1.x 为 64MB) 128MB
NameNode Web UI 端口 50070 9870
DataNode 数据传输端口 50010 9866
NameNode 默认 RPC 端口 8020(教程常用 9000) 8020(默认 9820)
命令形式 hadoop fs / hdfs dfs(hadoop dfs 已废弃) 同左
纠删码 EC 不支持 支持
多 Standby NameNode 仅 1 个 Standby 支持多个
-head / -truncate 命令 部分版本无 支持
默认均衡带宽 1MB/s 10MB/s
环境变量名 HADOOP_NAMENODE_OPTS HDFS_NAMENODE_OPTS
守护进程管理脚本 hadoop-daemon.sh start xxx hdfs --daemon start xxx

练习

一、概念题

1. 什么是分布式文件系统?它由哪两类节点构成?

答:分布式文件系统把文件分布存储到多个计算机节点上,成千上万的计算机节点构成计算机集群,且集群由普通廉价硬件构成。它在物理结构上由两类节点构成:一类叫"主节点"(Master Node),也称为"名称节点"(NameNode);另一类叫"从节点"(Slave Node),也称为"数据节点"(DataNode)。

2. HDFS 要实现的设计目标有哪些?

答:(1)兼容廉价的硬件设备;(2)流数据读写;(3)大数据集;(4)简单的文件模型;(5)强大的跨平台兼容性。

3. HDFS 有哪些应用局限性?

答:(1)不适合低延迟数据访问;(2)无法高效存储大量小文件;(3)不支持多用户写入及任意修改文件。

4. HDFS 采用抽象的块概念带来哪些好处?

答:(1)支持大规模文件存储——文件以块为单位存储,一个大规模文件可被拆分成若干文件块,不同文件块可分发到不同节点上,因此文件大小不受单个节点存储容量的限制;(2)简化系统设计——文件块大小固定,容易计算一个节点可存储多少文件块,大大简化了存储管理,同时方便了元数据管理,元数据不需要和文件块一起存储;(3)适合数据备份——每个文件块都可冗余存储到多个节点上,大大提高了系统的容错性和可用性。

5. HDFS 的默认块大小是多少?为什么块要设计得远大于普通文件系统?

答:Hadoop 1.x(及 2.x 之前)默认 64MB;Hadoop 2.x 与 3.x 默认 128MB(配置项 dfs.blocksize)。块设计得大是为了最小化寻址开销:磁盘读取总时间 = 寻址时间 + 传输时间,工程上希望寻址时间只占传输时间的约 1%。机械磁盘寻址约 10ms、传输速率约 100MB/s,则传输时间应为 1s,对应约 100MB 的数据量,故块大小取 128MB 这一数量级。块太小会导致寻址次数多、元数据对象激增、map 任务过多;块太大会降低并行度、增大故障恢复代价。

6. NameNode 保存了哪两个核心数据结构?各自的作用是什么?两者关系如何?

答:FsImage 和 EditLog。FsImage 用于维护文件系统树以及文件树中所有文件和文件夹的元数据(所有目录和文件 inode 的序列化形式,含复制等级、修改和访问时间、访问权限、块大小以及组成文件的块等;对目录则存储修改时间、权限和配额元数据)。EditLog 是操作日志文件,记录了所有针对文件的创建、删除、重命名等操作。关系:FsImage 是某一时刻的元数据全量快照,EditLog 是该时刻之后的增量变更;NameNode 启动时先加载 FsImage 再回放 EditLog 得到最新元数据;运行期所有更新只写 EditLog(因为 FsImage 太大写入慢),由 checkpoint 过程周期性把两者合并。

7. FsImage 中是否记录了"块存储在哪个 DataNode 上"?为什么?

答:没有。FsImage 不记录文件包含哪些块以及每个块存储在哪个数据节点,这些映射信息由 NameNode 保留在内存中。原因是块位置随时可能因节点故障、副本复制、均衡而变化,持久化没有意义;当数据节点加入 HDFS 集群时会把自己包含的块列表告知 NameNode(块报告),此后定期执行该操作以确保块映射是最新的。

8. 为什么运行期间 EditLog 会不断变大?会带来什么问题?如何解决?

答:NameNode 运行期间所有更新操作都直接写到 EditLog,久而久之会变得很大。这对运行时没有明显影响,但 NameNode 重启时需要先把 FsImage 全部内容映像到内存,再逐条执行 EditLog 中的记录,EditLog 非常大时会导致启动非常慢,而这段时间 HDFS 处于安全模式无法对外提供写操作,影响用户使用。解决方案是引入 SecondaryNameNode 周期性合并 FsImage 与 EditLog。

9. 完整描述 SecondaryNameNode 的 checkpoint 工作流程。

答:(1)SecondaryNameNode 定期与 NameNode 通信,请求其停止使用 EditLog 文件,暂时将新的写操作写到一个新文件 edit.new,这个操作瞬间完成,上层写日志函数完全感觉不到差别;(2)SecondaryNameNode 通过 HTTP GET 方式从 NameNode 获取 FsImage 和 EditLog 文件,并下载到本地相应目录;(3)SecondaryNameNode 将下载的 FsImage 载入内存,然后逐条执行 EditLog 中的各项更新操作,使内存中的 FsImage 保持最新,这个过程就是 EditLog 和 FsImage 文件合并;(4)通过 POST 方式将新的 FsImage 文件发送到 NameNode;(5)NameNode 用接收到的新 FsImage 替换旧 FsImage,同时用 edit.new 替换 EditLog 文件,EditLog 就变小了。

10. SecondaryNameNode 是 NameNode 的热备吗?为什么?

答:不是。SecondaryNameNode 不能在 NameNode 挂掉后自动接管服务,它没有 NameNode 的完整实时状态(尤其没有块到 DataNode 的映射,也没有最后一次 checkpoint 之后的 EditLog)。它的职责是周期性合并 FsImage 与 EditLog 以缩短 NameNode 启动时间,并保存一份元数据备份用于手工恢复(会丢失最后一次 checkpoint 之后的变更)。真正的热备是 HDFS HA 中的 Standby NameNode(配合 JournalNode 与 ZKFC),开启 HA 后不再需要 SecondaryNameNode。

11. DataNode 的职责是什么?块在本地是怎么存储的?

答:DataNode 是 HDFS 的工作节点,负责数据的存储和读取,根据客户端或名称节点的调度进行数据的存储和检索,并向名称节点定期发送自己所存储的块的列表。每个 DataNode 的数据保存在各自节点的本地 Linux 文件系统中(目录由 dfs.datanode.data.dir 指定),每个块存成两个文件:blk_<blockId> 存实际数据,blk_<blockId>_<generationStamp>.meta 存该块的校验和(CRC32C)。

12. HDFS 的通信协议是怎样设计的?

答:所有 HDFS 通信协议都构建在 TCP/IP 协议基础之上。客户端通过一个可配置的端口向名称节点主动发起 TCP 连接,并使用客户端协议与名称节点交互;名称节点和数据节点之间使用数据节点协议交互;客户端与数据节点的交互通过 RPC 实现。在设计上,名称节点不会主动发起 RPC,而是响应来自客户端和数据节点的 RPC 请求(NameNode 对 DataNode 的指令搭载在心跳响应中下发)。

13. HDFS(1.0)只设置唯一一个名称节点带来哪些局限性?分别由什么方案缓解?

答:(1)命名空间限制——元数据保存在内存中,可容纳的对象个数受内存大小限制;(2)性能瓶颈——整个文件系统吞吐量受限于单个名称节点的吞吐量;(3)隔离问题——只有一个命名空间,无法对不同应用程序进行隔离;(4)集群可用性——唯一名称节点故障会导致整个集群不可用。前三点由 HDFS Federation(联邦)缓解,第四点由 HDFS HA 解决。

14. 简述副本放置策略(三副本)。

答:第一个副本放置在上传文件的数据节点;如果是集群外提交,则随机挑选一台磁盘不太满、CPU 不太忙的节点。第二个副本放置在与第一个副本不同机架的节点上。第三个副本放置在与第二个副本相同机架的其他节点上。更多副本随机选择节点。这样写入只需跨机架传输一次,节约带宽;同时保证单节点、单机架故障都不丢数据。

15. 客户端读取数据时如何选择副本?

答:客户端从名称节点获得数据块不同副本的存放位置列表,NameNode 已按距离客户端远近排序;客户端可调用 API 确定自己和这些数据节点所属的机架 ID,当发现某个副本的机架 ID 与客户端的机架 ID 相同时优先选择该副本读取;如果没有发现,则随机选择一个副本读取。

16. HDFS 的三类错误与恢复机制分别是什么?

答:(1)名称节点出错——FsImage 和 EditLog 损坏会使整个 HDFS 实例失效,HDFS 把这些核心文件同步复制到备份服务器 SecondaryNameNode(以及 dfs.namenode.name.dir 的多个目录),出错时据此恢复;生产环境用 HA。(2)数据节点出错——DataNode 定期向 NameNode 发送心跳,收不到心跳的节点被标记为"宕机",其数据被标记为"不可读",NameNode 不再向其发送 I/O 请求;NameNode 定期检查,发现某数据块副本数小于冗余因子就启动数据冗余复制生成新副本。(3)数据出错——网络传输和磁盘错误会造成数据错误,文件创建时客户端对每个文件块做信息摘录(校验和)并写入同一路径的隐藏文件;读取时先读该信息文件再校验每个数据块,校验出错则请求到另一个数据节点读取该块,并向 NameNode 报告该块有错误,NameNode 会定期检查并重新复制这个块。

17. DataNode 被判定为 dead 需要多久?公式是什么?

答:timeout = 2 × dfs.namenode.heartbeat.recheck-interval + 10 × dfs.heartbeat.interval = 2 × 300000ms + 10 × 3s = 630s ≈ 10.5 分钟。设得较长是为了避免短暂 GC 停顿或网络抖动引发大规模副本复制风暴。

18. 安全模式的触发条件与作用是什么?

答:NameNode 启动时自动进入安全模式,此时文件系统只读(可 ls/cat/get,不能 put/mkdir/rm/setrep)。当"已上报且满足最小副本数的块"占总块数的比例达到 dfs.namenode.safemode.threshold-pct(默认 0.999),且存活 DataNode 数达到 dfs.namenode.safemode.min.datanodes 后,再等 dfs.namenode.safemode.extension(默认 30s)即自动退出。运行期间若该比例跌破阈值会重新进入安全模式。作用是保护元数据与数据一致性,避免在块位置信息不完整时做出错误的副本复制/删除决策。

19. 小文件"占用磁盘空间"与"占用元数据"有什么区别?

答:HDFS 的块是逻辑上限而非预分配,一个 1KB 的文件在块大小 128MB 的 HDFS 上,在 DataNode 本地磁盘只占约 1KB,不浪费 128MB 磁盘空间;但这个文件仍要在 NameNode 内存里占一个文件 inode 对象 + 一个块对象(每个约 150 字节)。所以小文件"不浪费磁盘,但浪费 NameNode 内存",同时会让 MapReduce 产生大量小 task 拖慢作业、增加随机 I/O。

二、流程题

20. 完整描述 HDFS 的写数据流程。

答:(1)客户端通过 FileSystem.get(conf) 获取实例,调用 create() 创建文件,通过 RPC 远程调用名称节点在命名空间中新建文件;名称节点执行检查(文件是否已存在、父目录是否存在、客户端权限等),通过则记录 EditLog 并授予客户端租约。(2)数据被分成一个个分包(packet,默认 64KB,内含若干 512 字节 chunk 及 CRC 校验和),放入 DFSOutputStream 的内部 data queue;DFSOutputStream 向名称节点申请保存数据块的若干数据节点,形成数据流管道(pipeline)。(3)队列中的分包被发往管道中第一个数据节点,第一个节点边落盘边转发给第二个,第二个转发给第三个,形成"流水线复制"。(4)接收到数据的节点向发送者发送"确认包"(ack),确认包沿管道逆流而上最终到达客户端;客户端收到应答后将对应分包从 ack queue 移除。(5)一个块写满后关闭 pipeline,重新申请下一个块的节点列表,逐块重复。(6)全部写完后 DFSOutputStream 调用 ClientProtocal.complete() 通知名称节点关闭文件。

21. 写入过程中某个 DataNode 挂了怎么办?

答:(1)关闭当前 pipeline;(2)把 ack queue 中所有未确认的 packet 重新放回 data queue 头部,保证不丢数据;(3)为当前正在写的块申请新的版本时间戳(generation stamp),使故障节点上的残缺副本恢复后被识别为过期块并删除;(4)从 pipeline 中剔除故障节点,用剩余正常节点重建 pipeline 继续写入;(5)只要剩余副本数不少于 dfs.namenode.replication.min(默认 1)写入就继续,不向客户端报错,不足的副本由 NameNode 后续异步复制补齐。

22. 完整描述 HDFS 的读数据流程。

答:(1)客户端通过 FileSystem.get(conf) 获取文件系统实例,调用 open() 获得 FSDataInputStream;(2)输入流通过 ClientProtocal.getBlockLocations() 远程调用名称节点获得文件开始部分数据块的位置,名称节点返回保存该块的所有数据节点地址,并根据距离客户端远近排序;(3)客户端调用 read() 开始读取,输入流根据排序结果选择距离最近的数据节点建立连接并读取;(4)数据从数据节点读到客户端,读取时用校验和验证数据完整性,该块读完后 FSDataInputStream 关闭与该节点的连接;(5)继续通过 getBlockLocations() 查找下一个数据块并读取,直到文件末尾,最后调用 close()。

23. 读取过程中校验和验证失败会发生什么?什么是短路读取?

答:校验和失败说明该副本损坏,客户端会向 NameNode 报告该损坏块,然后换用列表中下一个最近的副本继续读取;NameNode 将该副本标记为损坏,安排从健康副本重新复制,复制完成后删除损坏副本。整个过程对上层应用透明,只有所有副本都不可用时应用才会收到异常。短路读取(short-circuit read):当客户端与目标 DataNode 在同一台机器时,客户端通过 Unix Domain Socket 从 DataNode 获取块文件的文件描述符,直接读取本地磁盘文件,绕过 DataNode 的 TCP 数据转发,减少拷贝与上下文切换,显著提升本地读性能(需配置 dfs.client.read.shortcircuit=true 与 dfs.domain.socket.path)。

三、命令题

24. 写出创建 HDFS 目录 /tempDir、列出根目录、上传本地 tempfile 目录下所有文件、查看结果、打印所有文件内容的命令。

hadoop fs -mkdir /tempDir
hadoop fs -ls /
hadoop fs -copyFromLocal /home/administrator/tempfile/* /tempDir
hadoop fs -ls /tempDir
hadoop fs -cat /tempDir/*

25. hadoop fs、hadoop dfs、hdfs dfs 三者有什么区别?

答:hadoop fs 适用于任何文件系统(本地 file://、HDFS hdfs://、S3 等);hadoop dfs 只适用于 HDFS,但已废弃,执行会打印 DEPRECATED 警告;hdfs dfs 与 hadoop dfs 作用相同、也只适用于 HDFS,是当前推荐写法。实践中:操作 HDFS 用 hdfs dfs,跨文件系统用 hadoop fs,不要再用 hadoop dfs。

26. -put、-copyFromLocal、-moveFromLocal 有什么区别?

答:三者都是"本地 → HDFS"。-put 功能最全,源可以是本地路径也可以是标准输入(-),本地文件保留;-copyFromLocal 与 -put 基本等价但源只能是本地路径,本地文件保留;-moveFromLocal 上传成功后会删除本地源文件,相当于剪切。

27. 写出命令:把 /user/hadoop/cold 目录下所有文件副本数改为 2,并等待复制完成。

hadoop fs -setrep -R -w 2 /user/hadoop/cold

28. 写出命令:查看 /user/hadoop 的总占用(人类可读)、配额信息,并给它设置 10GB 空间配额与 10000 个文件数配额。

hadoop fs -du -s -h /user/hadoop
hadoop fs -count -q -h -v /user/hadoop
hdfs dfsadmin -setSpaceQuota 10g /user/hadoop
hdfs dfsadmin -setQuota 10000 /user/hadoop

29. 误删了 /user/hadoop/data.txt(未加 -skipTrash),如何恢复?

hdfs getconf -confKey fs.trash.interval                 # 确认回收站已启用(>0)
hadoop fs -ls -R /user/hadoop/.Trash/ | grep data.txt   # 定位
hadoop fs -mv /user/hadoop/.Trash/Current/user/hadoop/data.txt /user/hadoop/
hadoop fs -ls -h /user/hadoop/data.txt                  # 验证

30. 误删了一个已开启快照的目录中的文件(且用了 -skipTrash),如何恢复?

hdfs dfs -ls /user/hadoop/important/.snapshot/snap1/orders.csv
hdfs dfs -cp /user/hadoop/important/.snapshot/snap1/orders.csv /user/hadoop/important/orders.csv
# 快照目录只读,必须用 cp 而不能用 mv;大规模恢复可用 distcp

31. 写出为目录启用快照、创建、列出、重命名、删除快照以及比较两个快照差异的命令。

hdfs dfsadmin -allowSnapshot /user/hadoop/important
hdfs dfs -createSnapshot /user/hadoop/important snap1
hdfs dfs -ls /user/hadoop/important/.snapshot
hdfs lsSnapshottableDir
hdfs dfs -renameSnapshot /user/hadoop/important snap1 snap1-baseline
hdfs snapshotDiff /user/hadoop/important snap1-baseline .
hdfs dfs -deleteSnapshot /user/hadoop/important snap1-baseline

32. 写出把 /output/wordcount 下所有 part 文件合并下载到本地 result.txt(每个文件末尾加换行)的命令,并说明风险。

hadoop fs -getmerge -nl /output/wordcount /home/hadoop/result.txt

风险:-getmerge 会把 HDFS 目录中所有内容拉到本地单机磁盘,目录总量很大时会撑爆本地磁盘或分区。执行前应先用 hadoop fs -du -s -h 查看总量、df -h 确认本地剩余空间;数据量大时应在 HDFS 内部用计算引擎合并(如 Hive INSERT OVERWRITE、Spark coalesce(1).write)或 distcp。

33. 写出检查整个文件系统健康状况、列出损坏块、并删除含损坏块文件的命令。

hdfs fsck /
hdfs fsck / -files -blocks -locations -racks
hdfs fsck / -list-corruptfileblocks
hdfs fsck / -delete        # 谨慎:删除含损坏/丢失块的整个文件,不可逆
hdfs fsck / -move          # 或移到 /lost+found 留证

34. 用 shell 脚本判断 HDFS 上是否存在 _SUCCESS 标记文件,存在则继续、否则退出。

if hadoop fs -test -e /output/wordcount/_SUCCESS; then
  echo "上游任务已完成,开始处理"
else
  echo "上游数据未就绪,退出"
  exit 1
fi

35. 给用户 alice 授予 /user/hadoop/data 目录的读写执行权限,并让新建子项自动继承该权限。

# 先确保 dfs.namenode.acls.enabled = true
hadoop fs -setfacl -m user:alice:rwx /user/hadoop/data
hadoop fs -setfacl -m default:user:alice:rwx /user/hadoop/data
hadoop fs -getfacl /user/hadoop/data

36. 集群新增了 3 台节点,磁盘使用率严重不均,如何均衡且不影响业务?

hdfs dfsadmin -report | grep "DFS Used%"        # 先确认不均衡程度
hdfs dfsadmin -setBalancerBandwidth 10485760    # 限速 10MB/s,避免抢占业务带宽
hdfs balancer -threshold 5                       # 业务低峰期执行
# 单节点内多盘不均衡则用 diskbalancer
hdfs diskbalancer -plan dn1
hdfs diskbalancer -execute /system/diskbalancer/xxx/dn1.plan.json
hdfs diskbalancer -query dn1

37. 把 /user/hadoop/logs 下 08、09 两个月的小文件归档,验证后删除原始文件。

hadoop archive -archiveName logs2026.har -p /user/hadoop/logs 08 09 /user/hadoop/archive
hadoop fs -ls -R har:///user/hadoop/archive/logs2026.har
hadoop fs -cat har:///user/hadoop/archive/logs2026.har/08/a.log
hadoop fs -rm -r -skipTrash /user/hadoop/logs/08 /user/hadoop/logs/09

四、编程题

38. 编写 Java 程序,判断 HDFS 上 /test.txt 是否存在。

package com.testing;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

public class HDFSFileIfExist {
    public static void main(String[] args){
        try{
            String fileName = "/test.txt";
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", "hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            if(fs.exists(new Path(fileName))){
                System.out.println("文件存在");
            }else{
                System.out.println("文件不存在");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

39. 编写 Java 程序,向 HDFS 的 /testx 写入字符串 Hello world。

package com.testing;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;

public class HDFSWrite {
    public static void main(String[] args) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            byte[] buff = "Hello world".getBytes(); // 要写入的内容
            String filename = "/testx"; //要写入的文件名
            FSDataOutputStream os = fs.create(new Path(filename));
            os.write(buff,0,buff.length);
            System.out.println("Create:"+ filename);
            os.close();
            fs.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

若报权限错误,服务端执行 hadoop fs -chmod -R 777 /(仅实验环境),或在代码中用 FileSystem.get(new URI(...), conf, "hadoop") 指定用户。

40. 编写 Java 程序,读取 /testx 的第一行并输出。

package com.testing;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;

public class HDFSRead {
    public static void main(String args[]) {
        try {
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS","hdfs://192.168.224.10:9000");
            conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
            FileSystem fs = FileSystem.get(conf);
            Path file = new Path("/testx");
            FSDataInputStream getIt = fs.open(file);
            BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
            String content = d.readLine(); //读取文件一行
            System.out.println(content);
            d.close(); //关闭文件
            fs.close(); //关闭hdfs
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

Hello world

41. 编写方法:递归列出目录下所有文件,并打印每个文件的每个块所在的主机。

public static void listFilesRecursive(FileSystem fs, String dir) throws Exception {
    RemoteIterator<LocatedFileStatus> it = fs.listFiles(new Path(dir), true);
    while (it.hasNext()) {
        LocatedFileStatus st = it.next();
        System.out.println(st.getPath() + "  size=" + st.getLen()
                + "  rep=" + st.getReplication() + "  blockSize=" + st.getBlockSize());
        for (BlockLocation bl : st.getBlockLocations()) {
            System.out.println("    offset=" + bl.getOffset()
                    + " len=" + bl.getLength()
                    + " hosts=" + String.join(",", bl.getHosts()));
        }
    }
}

42. 编写方法:把本地文件上传到 HDFS,副本数指定为 2、块大小指定为 64MB,并用 IOUtils 完成流拷贝。

public static void uploadWithParams(FileSystem fs, String local, String remote) throws Exception {
    try (InputStream in = new FileInputStream(local);
         FSDataOutputStream out = fs.create(new Path(remote), true, 4096,
                 (short) 2, 67108864L)) {   // replication=2, blockSize=64MB
        IOUtils.copyBytes(in, out, 4096, false);
        out.hflush();
        out.hsync();
    }
}

43. 编写方法:把 /user/hadoop/smallfiles 下的所有小文件合并写入一个块压缩的 SequenceFile。

答:见正文 5.1.3 节 SmallFilesToSequenceFile 示例:以 Text(文件名)为 key、BytesWritable(文件内容)为 value,用 SequenceFile.createWriter(...Writer.compression(CompressionType.BLOCK, new SnappyCodec())) 创建写入器,用 fs.listFiles(path, true) 递归遍历并逐个 append。要点是块压缩使 SequenceFile 既可切分又有压缩收益,从而同时缓解 NameNode 元数据压力与 task 数量膨胀。

44. 为什么在 Web 应用中不应该在每次请求结束后调用 fs.close()?

答:FileSystem.get() 内部有缓存,同一 URI + 同一用户会返回同一个实例。若某个请求处理完就 close(),会导致其他线程持有的同一实例被关闭,出现 Filesystem closed 异常。正确做法:全局共享一个实例并在应用关闭时统一释放;或用 FileSystem.newInstance() 获取独立实例(用完自行关闭);或设置 fs.hdfs.impl.disable.cache=true 禁用缓存。

五、排错题

45. 上传文件报 Name node is in safe mode,如何处理?

答:先 hdfs dfsadmin -safemode get 查看状态与原因,检查 NameNode 日志中的 "reported blocks ... needs additional ... blocks";用 hdfs dfsadmin -report 确认所有 DataNode 是否存活、hdfs fsck / 确认是否有丢块。若是刚启动则等待自动退出(可用 hdfs dfsadmin -safemode wait);若确认块丢失且可接受,hdfs dfsadmin -safemode leave 后用 hdfs fsck / -delete 清理。

46. 报 File /xxx could only be replicated to 0 nodes instead of minReplication (=1),列出至少 5 个可能原因。

答:(1)DataNode 根本没启动(jps/hdfs dfsadmin -report 中 Live datanodes 为 0);(2)DataNode 启动但注册失败,Incompatible clusterIDs(重复执行过 hdfs namenode -format);(3)所有 DataNode 磁盘空间不足(可用空间小于 dfs.datanode.du.reserved + 一个块大小);(4)防火墙/SELinux 阻断 DataNode 与 NameNode 或客户端通信;(5)DataNode 数据目录权限错误或磁盘故障导致全部 volume 失效变为 unhealthy;(6)NameNode 处于安全模式;(7)所有 DataNode 都在 dfs.hosts.exclude 名单中正在退役。

47. Windows 上的 Java 客户端能执行 ls,但 put 时卡住最后超时,最可能的两个原因是什么?怎么解决?

答:(1)DataNode 数据端口未放行:ls/mkdir 只与 NameNode 交互,读写数据必须直连每个 DataNode 的数据传输端口(2.x 为 50010,3.x 为 9866)。解决:在服务端放行该端口(firewall-cmd --add-port=50010/tcp 或关闭 firewalld),并用 telnet <dn> 50010 从客户端验证连通。(2)hostname 解析问题:NameNode 返回的是 DataNode 的主机名或内网 IP,客户端无法解析或路由。解决:在客户端 hosts 文件中加入所有 DataNode 的映射;或设置 dfs.client.use.datanode.hostname=true 配合可解析的 hostname;服务端可设 dfs.namenode.datanode.registration.ip-hostname-check=false。同时确认集群 /etc/hosts 中没有把本机主机名解析到 127.0.0.1。

48. NameNode 频繁 Full GC、Web UI 打不开,fsck 显示平均块大小只有 30KB,如何分析与治理?

答:平均块大小远小于 128MB 说明是典型的小文件集群,元数据对象过多导致堆压力。(1)短期:扩大 NameNode 堆并改用 G1GC(-Xms64g -Xmx64g -XX:+UseG1GC -XX:MaxGCPauseMillis=200),同时调大 dfs.namenode.handler.count;(2)定位:hadoop fs -count -h -v /user/*/ 找出文件数最多的目录;(3)中期治理:hadoop archive 归档历史小文件、用 SequenceFile 合并、验证后 -rm -r -skipTrash 删除原始文件、清理回收站 hadoop fs -expunge;(4)计算侧用 CombineFileInputFormat/CombineHiveInputFormat 降低 task 数;(5)长期治本:上游采集攒批(调大 Flume rollSize/rollInterval、Flink checkpoint 间隔)、合理分区粒度、每日合并作业;(6)元数据总量仍超单机承载时上 HDFS Federation 拆分命名空间。

49. 3 副本集群中,fsck 报 Under-replicated blocks: 5000,长时间不下降,可能原因?

答:(1)有 DataNode 处于 dead 状态且未恢复;(2)存活 DataNode 数量少于副本数(如只剩 2 个节点却要求 3 副本,永远补不齐);(3)所有节点磁盘接近写满,没有空间放新副本;(4)复制带宽/并发限制太小,积压严重。处理:hdfs dfsadmin -report 确认节点与容量;启动缺失的 DataNode;节点数确实不够就 hdfs dfs -setrep -R 2 / 并同步调整 dfs.replication;调大 dfs.namenode.replication.work.multiplier.per.iteration 与 dfs.namenode.replication.max-streams、hdfs dfsadmin -setBalancerBandwidth 加速恢复。

50. 为什么给一个 3 副本目录设置 1GB 空间配额后,连一个 300MB 的文件都写不进去?

答:空间配额计算的是含副本的磁盘占用,且正在写入的文件会按"完整块 × 副本数"预留配额。300MB 数据 3 副本实际占 900MB,加上按块预留(128MB × 3 = 384MB 的粒度)很容易超过 1GB 限制,报 The DiskSpace quota of ... is exceeded。正确做法:空间配额至少要大于 dfs.blocksize × dfs.replication,并按"预期数据量 × 副本数 × 1.2 冗余"来设定。


出处:厦门大学计算机科学系 · 林子雨《大数据技术原理与应用》(第 3 章 分布式文件系统 HDFS);课程实验讲义《HDFS 分布式文件系统》(Eclipse + Java API 实验部分)

Department of Computer Science, Xiamen University

参考:《大数据技术原理与应用 第三章 Hadoop分布式文件系统 学习指南》http://dblab.xmu.edu.cn/blog/290-2/

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏