4. 分布式存储HDFS:读写流程、Shell命令操作、Java API编程、数据块与副本机制

好,咱们今天聊聊HDFS。说实话,这是整个大数据生态的基石。你想想看,没有分布式存储,后面那些Spark、Flink跑什么呢?我当年刚接触大数据时,第一个被震撼到的就是HDFS——原来文件还能这么存。

4.1 数据块与副本机制:HDFS的命根子

先讲最核心的概念:数据块和副本。这是HDFS设计的灵魂。

传统文件系统里,一个文件就是一个整体。但HDFS不一样。它会把文件切成一块一块的,默认每块128MB。为什么是128MB?嗯,早期Hadoop版本是64MB,后来磁盘和网络都快了,就翻倍了。我个人习惯在生产环境里保持默认值,除非有特殊场景。

每个数据块默认存3份副本。这个3是怎么来的?其实是个权衡。少了怕丢数据,多了浪费存储。我在项目中遇到过,有个同事为了省空间把副本数改成2,结果一台机器挂了,数据恢复时差点出问题。所以啊,除非你特别清楚自己在做什么,否则别动这个3。

核心要点:

  • 数据块大小:默认128MB,可配置
  • 副本因子:默认3,保证数据可靠性
  • 副本放置策略:机架感知,保证高可用

副本怎么放?这里有个讲究。HDFS会尽量把副本放在不同的机架上。比如第一个副本放在本地节点,第二个放在同机架另一个节点,第三个放在不同机架。这样就算整个机架断电,数据还在。说白了,就是用空间换安全。

4.2 HDFS读写流程:数据是怎么流动的

理解了数据块,咱们看看读写流程。我画了个图,帮你理清思路。

HDFS读写流程图 客户端 NameNode DataNode 1 DataNode 2 DataNode 3 ① 请求写 ② 返回DataNode列表 ③ 写入数据块 ④ 请求读 ⑤ 返回数据位置 ⑥ 读取数据 写流程 读流程

写流程:

  1. 客户端向NameNode发起写请求
  2. NameNode检查权限和空间,返回可用的DataNode列表
  3. 客户端将数据块依次写入第一个DataNode
  4. 第一个DataNode自动将数据复制到第二个,第二个再复制到第三个
  5. 所有副本写完后,客户端通知NameNode完成

这里有个坑。我曾经遇到过网络抖动导致写超时的情况。客户端写数据时,如果某个DataNode挂了,它会自动重试下一个。但如果你网络不稳定,建议调大超时参数。具体怎么调?后面讲配置时再说。

读流程:

  1. 客户端问NameNode:我要读这个文件,数据在哪?
  2. NameNode返回数据块的位置信息(哪个DataNode上有哪些块)
  3. 客户端直接连接最近的DataNode读取数据
  4. 如果某个DataNode挂了,自动换一个

你发现没?读流程比写流程简单多了。而且客户端是直接从DataNode读数据,NameNode不参与数据传输。这样设计就是为了避免NameNode成为瓶颈。

4.3 Shell命令操作:日常必备技能

好了,理论讲完了,咱们上手实操。HDFS的Shell命令跟Linux命令很像,但前面要加个hdfs dfs。我刚开始用的时候老记混,后来总结了个规律:把hdfs dfs当成Linux的lscp的前缀就行。

我的习惯: 把常用的HDFS命令写了个别名,比如 alias hls='hdfs dfs -ls',省事不少。

常用命令一览:

操作 命令 说明
查看目录 hdfs dfs -ls /path 类似Linux的ls
创建目录 hdfs dfs -mkdir /path 支持-p递归创建
上传文件 hdfs dfs -put localfile /hdfspath 从本地上传到HDFS
下载文件 hdfs dfs -get /hdfspath localfile 从HDFS下载到本地
查看文件内容 hdfs dfs -cat /path/file 直接输出到终端
删除文件 hdfs dfs -rm /path/file 支持-r递归删除
查看块信息 hdfs fsck /path/file -files -blocks 查看文件的数据块分布

举个例子,上传一个日志文件:

# 先看看本地文件
ls -lh access.log
# 输出:-rw-r--r-- 1 user user 256M Jan 15 10:00 access.log

# 上传到HDFS
hdfs dfs -put access.log /user/logs/

# 验证一下
hdfs dfs -ls /user/logs/
# 输出:Found 1 items
# -rw-r--r--   3 user supergroup  256M 2024-01-15 10:01 /user/logs/access.log

# 看看这个文件占了多少数据块
hdfs fsck /user/logs/access.log -files -blocks

注意: 上传大文件时,HDFS会自动分块。你看到的是256M的文件,实际上它被分成了2个128M的数据块。用fsck命令就能看到每个块分布在哪些DataNode上。

4.4 Java API编程:用代码操作HDFS

Shell命令适合日常运维,但真正的数据处理还得靠代码。Java API是操作HDFS最正统的方式。我当年写第一个HDFS程序时,踩了个坑——忘记关流,结果文件一直显示写入中。后来养成了习惯,所有流操作都用try-with-resources。

先加依赖:

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

核心代码示例:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import java.io.*;

public class HdfsDemo {
    public static void main(String[] args) throws Exception {
        // 创建配置对象
        Configuration conf = new Configuration();
        // 设置HDFS地址
        conf.set("fs.defaultFS", "hdfs://localhost:9000");
        
        // 获取文件系统对象
        try (FileSystem fs = FileSystem.get(conf)) {
            
            // 1. 创建目录
            Path dir = new Path("/user/demo");
            if (!fs.exists(dir)) {
                fs.mkdirs(dir);
                System.out.println("目录创建成功");
            }
            
            // 2. 上传文件
            Path localFile = new Path("local.txt");
            Path hdfsFile = new Path("/user/demo/remote.txt");
            fs.copyFromLocalFile(localFile, hdfsFile);
            System.out.println("文件上传成功");
            
            // 3. 读取文件内容
            try (FSDataInputStream in = fs.open(hdfsFile)) {
                BufferedReader reader = new BufferedReader(
                    new InputStreamReader(in));
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            }
            
            // 4. 查看文件块信息
            FileStatus status = fs.getFileStatus(hdfsFile);
            BlockLocation[] blocks = fs.getFileBlockLocations(
                status, 0, status.getLen());
            for (BlockLocation block : blocks) {
                System.out.println("块偏移: " + block.getOffset() + 
                    ", 长度: " + block.getLength() + 
                    ", 所在节点: " + String.join(",", block.getHosts()));
            }
        }
    }
}

这段代码涵盖了最常用的操作:创建目录、上传文件、读取内容、查看块信息。你想想看,实际项目中无非就是这些操作的组合。

避坑指南: 我曾经在生产环境遇到过一个问题——客户端和集群的Hadoop版本不一致,导致RPC通信失败。所以啊,hadoop-client的版本一定要和集群版本保持一致。

4.5 实战经验总结

最后,分享几个我在项目中积累的经验:

  • 小文件问题: HDFS不适合存大量小文件。每个文件都要占用NameNode内存,几百万个小文件能把NameNode撑爆。我一般建议用SequenceFile或合并工具把小文件合并成大文件。
  • 副本数调整: 临时数据可以设成2副本,重要数据设成3副本。但别设成1,除非你做好了数据丢失的心理准备。
  • 数据均衡: 新加节点后,用hdfs balancer做数据均衡。我习惯在凌晨跑,因为比较耗资源。
  • 安全模式: NameNode启动时会进入安全模式,此时不能写数据。等它检查完数据块完整性后自动退出。如果一直卡在安全模式,多半是有数据块损坏了。

好了,HDFS的核心内容就这些。记住一句话:HDFS是为大文件、流式读取设计的,别拿它当普通文件系统用。理解了这一点,后面学Hive、HBase时就会轻松很多。


公众号:蓝海资料掘金营,微信deep3321