3、HDFS实战:Shell操作、Java API操作、读写流程详解、机架感知与副本策略

好,咱们今天来啃HDFS这块硬骨头。说实话,HDFS是大数据离线批处理的基石,你后面所有跟存储打交道的活儿,都绕不开它。我当年刚接触Hadoop时,觉得HDFS不就是个分布式文件系统嘛,能有多复杂?结果第一次在生产环境遇到DataNode挂掉、副本丢失的情况,差点没把我整懵。嗯,从那以后,我对HDFS的敬畏心就上来了。

这一章,咱们不讲虚的,直接上干货。我会从最常用的Shell操作讲起,再到Java API的实战,然后深入剖析读写流程,最后聊聊机架感知和副本策略——这些可都是面试高频题,也是你排查问题的基本功。

3.1 Shell操作:你每天都要用的命令

说白了,HDFS的Shell命令跟Linux文件系统命令长得差不多,但细节上有不少坑。我个人习惯把HDFS Shell命令分成三类:文件操作、目录操作、系统管理。

3.1.1 文件与目录操作

先来几个最常用的:

# 查看根目录
hdfs dfs -ls /

# 创建目录
hdfs dfs -mkdir -p /user/hive/warehouse

# 上传文件
hdfs dfs -put localfile.txt /user/data/

# 下载文件
hdfs dfs -get /user/data/remotefile.txt ./

# 查看文件内容
hdfs dfs -cat /user/data/sample.txt

# 删除文件
hdfs dfs -rm /user/data/oldfile.txt

# 递归删除目录
hdfs dfs -rm -r /user/data/olddir/

这里有个坑,我必须要提醒你。用 -rm 删除文件时,默认不会进入回收站。我曾经在测试环境手滑删了一个重要目录,还好有快照备份。所以,建议你养成习惯:

警告:生产环境慎用 -rm -r,最好先开启HDFS回收站机制。在 core-site.xml 中配置 fs.trash.interval 参数,比如设为1440分钟(24小时),这样删除的文件会先进入 /user/用户名/.Trash 目录。

3.1.2 文件权限与属性

HDFS的权限模型跟Linux类似,但更简单。它只有Owner、Group、Other三级权限,没有ACL(除非你开启)。

# 修改权限
hdfs dfs -chmod 755 /user/data/

# 修改所有者
hdfs dfs -chown hadoop:hadoop /user/data/

# 查看文件块信息
hdfs fsck /user/data/bigfile.log -files -blocks -locations

fsck 这个命令特别有用。我记得有一次集群报错说某个文件不可读,我直接用 fsck 查看它的块分布,发现有两个块所在的DataNode都挂了。嗯,这就是典型的副本丢失问题。

3.2 Java API操作:写代码才是硬道理

Shell命令只是开胃菜,真正干活还得靠Java API。我带你写一个完整的读写示例。

3.2.1 环境准备

首先,你得引入依赖。我用的是Maven:

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>3.3.4</version>
</dependency>

3.2.2 文件写入

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

public class HdfsWriter {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://namenode:9000");
        
        FileSystem fs = FileSystem.get(conf);
        Path path = new Path("/user/data/test.txt");
        
        // 创建文件,如果已存在则覆盖
        FSDataOutputStream out = fs.create(path, true);
        out.writeBytes("Hello, HDFS!\n");
        out.writeBytes("这是第二行数据。\n");
        out.close();
        
        System.out.println("写入成功!");
        fs.close();
    }
}

这里有个细节:fs.create() 方法默认会创建文件,如果文件已存在且第二个参数为 true,则会覆盖。我建议你在生产环境用 fs.exists() 先检查一下,避免误覆盖。

3.2.3 文件读取

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

public class HdfsReader {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://namenode:9000");
        
        FileSystem fs = FileSystem.get(conf);
        Path path = new Path("/user/data/test.txt");
        
        FSDataInputStream in = fs.open(path);
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
        
        reader.close();
        fs.close();
    }
}
小技巧:读取大文件时,别一次性全读到内存里。用 FSDataInputStreamseek() 方法可以跳转到指定位置读取,这在处理超大文件时特别有用。我做过一个日志分析任务,文件有200GB,就是用 seek() 分片读取的。

3.3 读写流程详解:数据到底是怎么跑的?

你想想看,一个128MB的文件上传到HDFS,它内部是怎么处理的?这里我画个流程图(用文字描述):

3.3.1 写入流程

  1. 客户端发起请求:客户端调用 fs.create(),向NameNode请求创建文件。
  2. NameNode检查:检查文件是否存在、权限是否足够。没问题就返回一个 FSDataOutputStream
  3. 客户端切分数据:客户端将文件切分成块(默认128MB),然后向NameNode请求每个块应该写到哪些DataNode。
  4. NameNode分配节点:根据机架感知策略,返回一个DataNode列表(比如3个节点)。
  5. 客户端写入数据:客户端将第一个块写入第一个DataNode,然后这个DataNode将数据复制到第二个,第二个再复制到第三个——形成流水线复制。
  6. 确认反馈:每个DataNode写入成功后,逐级返回确认。客户端收到所有确认后,继续写下一个块。
  7. 关闭文件:所有块写完后,客户端调用 close(),NameNode更新元数据。

这里有个关键点:流水线复制。它不是客户端同时发给三个节点,而是一个传一个。这样做的好处是减少客户端的网络带宽压力。我刚开始不理解,觉得这样不是更慢吗?后来一想,客户端带宽有限,如果同时发三个副本,客户端出口就成瓶颈了。

3.3.2 读取流程

  1. 客户端发起请求:调用 fs.open(),向NameNode请求文件信息。
  2. NameNode返回元数据:返回文件每个块的位置信息(块ID、所在的DataNode列表)。
  3. 客户端选择节点:客户端根据网络拓扑,选择离自己最近的DataNode(优先同机架,其次同数据中心)。
  4. 客户端读取数据:客户端直接连接DataNode,读取块数据。如果读取失败,会尝试下一个副本。
  5. 合并数据:客户端将所有块的数据合并成完整的文件。
重点:读取时,客户端会优先读取本地或同机架的副本。这就是为什么机架感知配置好了,能大幅提升读取性能。我见过一个集群,机架感知没配好,所有读取都跨机架,网络延迟直接翻倍。

3.4 机架感知与副本策略

机架感知,说白了就是让HDFS知道每个DataNode在哪个机架上。这样它才能做出聪明的副本放置决策。

3.4.1 机架感知配置

配置机架感知需要写一个脚本,比如 /etc/hadoop/conf/topology.sh

#!/bin/bash
# 根据IP地址返回机架信息
case $1 in
    192.168.1.*) echo "/rack1" ;;
    192.168.2.*) echo "/rack2" ;;
    192.168.3.*) echo "/rack3" ;;
    *) echo "/default-rack" ;;
esac

然后在 core-site.xml 中配置:

<property>
    <name>net.topology.script.file.name</name>
    <value>/etc/hadoop/conf/topology.sh</value>
</property>

配置好后,用 hdfs dfsadmin -printTopology 可以查看当前的机架拓扑。

3.4.2 副本放置策略

默认副本数是3,放置策略是这样的:

副本序号 放置位置 说明
第1副本 客户端所在节点 如果客户端不在集群内,则随机选一个
第2副本 不同机架的任意节点 保证跨机架容错
第3副本 与第2副本同机架的不同节点 在同机架内再做一个备份

你可能会问:为什么第3副本不放到另一个机架?嗯,这是权衡的结果。如果放到另一个机架,写入时就要跨两个机架,网络开销更大。现在的策略是:一个副本在本地机架,两个副本在另一个机架。这样既保证了容错(一个机架挂了还有两个副本),又控制了写入带宽。

注意:副本数不是越多越好。副本越多,存储成本越高,写入性能也会下降(因为要复制更多份)。我见过有人把副本数设为5,结果集群存储利用率只有20%。一般生产环境3副本就够了,重要数据可以设到4。

3.4.3 副本选择策略(读取时)

读取时,客户端会按照以下优先级选择副本:

  1. 本地节点:如果数据就在本机,直接读,速度最快。
  2. 同机架节点:如果本机没有,找同机架的其他节点。
  3. 跨机架节点:最后才考虑跨机架读取。

这个策略很聪明,它最大限度地减少了网络传输。我在做数据倾斜优化时,就利用了这个特性——把计算任务调度到数据所在的节点上,避免数据移动。

3.5 实战经验总结

最后,分享几个我在项目中踩过的坑:

  • 小文件问题:HDFS不适合存大量小文件(比如几KB的日志)。每个文件都要占用NameNode内存(约150字节),1000万个文件就要1.5GB内存。我建议用 SequenceFileAvro 把小文件合并成大文件。
  • 块大小设置:默认128MB,但如果你处理的是超大文件(比如几百GB),可以调大到256MB甚至512MB。块越大,NameNode的元数据越少,但MapReduce的并行度也会降低。需要根据业务场景权衡。
  • 数据均衡:集群运行久了,DataNode之间的数据分布会不均匀。定期执行 hdfs balancer 是个好习惯。我一般设置带宽为100MB/s,在业务低峰期跑。
  • 安全模式:NameNode启动时会进入安全模式,此时不能写数据。如果安全模式一直不退出,说明有块丢失。用 hdfs dfsadmin -safemode leave 强制退出前,一定要先检查块健康状态。

好了,HDFS实战这块就讲到这里。下一章咱们会聊MapReduce,那可是离线批处理的灵魂。你先把HDFS玩熟了,后面学MapReduce会轻松很多。