HDFS小文件合并实战:PutMerge与GetMerger Java实现

发布时间:2026/9/30 18:30:31
HDFS小文件合并实战:PutMerge与GetMerger Java实现 简介本资源是一份面向计算机科学专业本科生的《云计算技术》课程实验报告聚焦HDFS分布式文件系统的编程实践帮助学习者掌握HDFS文件上传、下载与合并的核心操作能力。报告完整呈现了在Linux环境下基于Eclipse集成开发环境配置Hadoop插件hadoop-eclipse-plugin-2.10.1.jar、搭建Map/Reduce项目、编写Java代码实现PutMerge本地多文件合并上传至HDFS和GetMergerHDFS目录下载并本地合并两大功能的全过程包含环境配置截图提示、关键API调用说明及典型排错经验总结。资源为单个PDF文件大小1.1MB内容结构清晰涵盖实验目的、步骤详解、结果分析与个人总结反思便于快速复现实验并理解HDFS读写底层逻辑。目前已有561人学习下载适合初学Hadoop生态、需强化动手能力与调试思维的云计算入门学习者。1. HDFS 文件合并实战PutMerge 与 GetMerger 的 Java 实现到底在解决什么问题你有没有遇到过这样的场景Hadoop 集群里跑完 MapReduce 任务输出目录下生成了 200 个 part-r-00000 到 part-r-00199 的小文件想本地分析却要一个个hdfs dfs -get下来再手动 cat 拼接——不仅慢、易出错还根本没法自动化。这个实验报告里的PutMerge和GetMerger不是教你怎么点几下 Eclipse 菜单而是直击 HDFS 生产环境最真实的“小文件痛点”用 Java API 封装文件批量上传/下载合并逻辑把零散 I/O 变成可控的原子操作。它面向的是正在学云计算的本科生但背后是运维工程师每天要写的脚本、数据工程师调试 pipeline 时反复验证的流程。别被“实验报告”四个字骗了——这份代码能直接扔进生产调度系统当工具类用前提是你得搞懂FileSystem的连接生命周期、FSDataInputStream的缓冲边界、以及SequenceFile和普通文本合并的本质区别。我当年第一次跑通GetMerger时发现本地合并后的文件比 HDFS 原始内容少了一行翻了三小时日志才定位到BufferedReader.readLine()在最后一行无换行符时的返回逻辑……这玩意儿真不是写个 for 循环就完事的。2. 从零配置 Eclipse Hadoop 开发环境为什么必须用 2.10.1 插件而非最新版2.1 插件版本与 Hadoop 运行时的 ABI 兼容性陷阱实验报告里明确要求hadoop-eclipse-plugin-2.10.1.jar这不是凑巧。Hadoop 2.x 系列尤其是 2.10.1的 RPC 协议、序列化格式、甚至org.apache.hadoop.fs.FileSystem的抽象方法签名和 3.x 存在不兼容变更。如果你强行用hadoop-eclipse-plugin-3.3.6.jarEclipse 启动时会报java.lang.NoSuchMethodError: org.apache.hadoop.conf.Configuration.getPropsWithPrefix(Ljava/lang/String;)Ljava/util/Map;——因为 3.x 把getPropsWithPrefix改成了getPropsWithPrefixOrNull。而实验环境大概率是 Hadoop 2.10.1 伪分布式集群core-site.xml中fs.defaultFS指向hdfs://localhost:9000插件必须严格对齐。提示不要去官网找“最新版”直接搜索hadoop-eclipse-plugin-2.10.1.jar download从 Apache 官方归档镜像如archive.apache.org/dist/hadoop/core/hadoop-2.10.1/下载对应 tar.gz 包解压后提取share/hadoop/tools/lib/hadoop-eclipse-plugin-2.10.1.jar。别信第三方打包站我见过一个“2.10.1”插件实为 2.7.3 编译导致listStatus()返回空数组。2.2 Eclipse Preference 配置中的三个致命路径配置过程看似简单但三个路径任何一个填错后续所有 Java API 调用都会静默失败不报错但FileSystem.get()返回 null 或listStatus()返回空列表Hadoop installation directory必须指向解压后的 Hadoop 根目录含bin/,etc/,share/子目录且该目录下etc/hadoop/core-site.xml和hdfs-site.xml必须存在并正确配置。常见错误是只指向hadoop-2.10.1/share/hadoop/漏掉根目录。Map/Reduce location 的 Master Host Port若用伪分布式Host 填localhostPort 填9000fs.defaultFS的端口不是8020旧版端口或50070Web UI 端口。Project Build Path 中的 Libraries新建 Map/Reduce Project 后右键 → Properties → Java Build Path → Libraries → Add Library → Hadoop Classpath Container。此容器会自动加载hadoop-common-2.10.1.jar,hadoop-hdfs-2.10.1.jar,hadoop-client-2.10.1.jar等缺一不可。若手动 Add External JARs极易漏掉commons-collections4-4.1.jarHadoop 2.10 依赖导致ClassNotFoundException。2.3 Map/Reduce Perspective 的隐藏依赖必须启动 LocalJobRunnerEclipse 的 Map/Reduce 视图本身不运行任何服务它只是 UI 层。真正让FileSystem连上 HDFS 的是 Hadoop 客户端库读取core-site.xml并初始化DistributedFileSystem实例。但如果你没在Run As → Run on Hadoop中选择LocalJobRunner而非YARNEclipse 会尝试连接 YARN ResourceManager而伪分布式环境通常未启动 YARN。解决方案右键项目 → Run As → Run on Hadoop → 在弹出窗口中勾选Use local job runner并确保hadoop.tmp.dir指向本地可写路径如/tmp/hadoop-${user.name}。否则FileSystem.get()会卡死 30 秒后抛java.net.ConnectException: Connection refused。3. PutMerge把本地多个小文件合并上传到 HDFS 的完整 Java 实现3.1 PutMerge 的核心逻辑流式合并避免内存溢出PutMerge不是先cat file1 file2 merged.txt再hdfs dfs -put而是边读边写全程流式处理。这样即使合并 10GB 文件JVM 堆内存也只需 64MB。关键在于用FileInputStream逐个打开本地文件而非Files.readAllBytes()加载全量用FSDataOutputStream的write()方法直接写入 HDFS跳过本地临时文件每个文件写入前插入分隔符如\n--- FILE: filename ---\n便于后续GetMerger解析边界。public class PutMerge { public static void mergeAndUpload(String localDir, String hdfsPath, Configuration conf) throws IOException { FileSystem fs FileSystem.get(conf); FSDataOutputStream out fs.create(new Path(hdfsPath)); File dir new File(localDir); File[] files dir.listFiles((d, name) - name.endsWith(.txt)); // 过滤条件可调 if (files null) throw new IllegalArgumentException(No files found in localDir); for (File file : files) { out.writeBytes(\n--- FILE: file.getName() ---\n); try (FileInputStream fis new FileInputStream(file); BufferedInputStream bis new BufferedInputStream(fis)) { byte[] buffer new byte[8192]; int len; while ((len bis.read(buffer)) ! -1) { out.write(buffer, 0, len); } } } out.close(); fs.close(); System.out.println(Merged files.length files to hdfsPath); } }参数说明localDir本地绝对路径如/home/user/input/必须存在且有读权限hdfsPathHDFS 目标路径如/user/hadoop/merged_output.txt父目录需已存在fs.mkdirs()可提前创建conf已加载core-site.xml和hdfs-site.xml的Configuration实例通常通过new Configuration()自动加载 classpath 下配置。3.2 分隔符设计为什么不用\0而用--- FILE: xxx ---HDFS 文件本质是字节流GetMerger需靠分隔符切分原始文件。用\0空字符看似简洁但风险极高若原始文件内容含\0如二进制日志、图片 base64GetMerger会误判分割点Windows 系统记事本默认用\r\nLinux 用\n\0在文本编辑器中不可见调试困难。而--- FILE: xxx ---是人类可读、内容中极难自然出现的字符串。实测中我们用正则^--- FILE: (.?) ---$Pattern.compile(^--- FILE: (.?) ---$, Pattern.MULTILINE)精准匹配比String.split()更可靠。3.3 权限与编码UTF-8 BOM 导致的诡异乱码如果本地文件是 Windows 记事本保存的 UTF-8带 BOMBufferedInputStream读出的首 3 字节是EF BB BF直接写入 HDFS 后GetMerger读取时会在第一行开头看到。解决方案用InputStreamReader指定StandardCharsets.UTF_8并设置BOM处理Java 11 支持CharsetDecoder自动跳过 BOM或更简单用vim打开文件执行:set nobomb | wq去除 BOM。注意Hadoop 默认使用 UTF-8但FileSystem不做字符集转换它只管字节。乱码一定是本地文件编码与读取方式不匹配所致。4. GetMerger从 HDFS 目录下载并合并所有文件的健壮实现4.1 listStatus() 的递归陷阱如何正确遍历子目录FileSystem.listStatus(Path)默认只返回一级子文件不递归。实验报告说“下载一个包含多个文件的路径文件夹”但没说是否允许子目录。生产环境常见结构是/output/part-*扁平或/output/year2023/month01/*分区嵌套。GetMerger必须支持两种模式public static void downloadAndMerge(String hdfsDir, String localOutput, Configuration conf) throws IOException { FileSystem fs FileSystem.get(conf); Path hdfsPath new Path(hdfsDir); // 递归获取所有文件非目录 RemoteIteratorLocatedFileStatus iter fs.listFiles(hdfsPath, true); ListLocatedFileStatus files new ArrayList(); while (iter.hasNext()) { LocatedFileStatus status iter.next(); if (!status.isDirectory()) { // 过滤掉目录 files.add(status); } } try (FileOutputStream fos new FileOutputStream(localOutput); BufferedOutputStream bos new BufferedOutputStream(fos)) { for (LocatedFileStatus file : files) { bos.write((\n--- FILE: file.getPath().getName() ---\n).getBytes(StandardCharsets.UTF_8)); try (FSDataInputStream fis fs.open(file.getPath()); BufferedInputStream bis new BufferedInputStream(fis)) { byte[] buffer new byte[8192]; int len; while ((len bis.read(buffer)) ! -1) { bos.write(buffer, 0, len); } } } } fs.close(); }关键点fs.listFiles(hdfsPath, true)的true参数开启递归返回RemoteIterator避免一次性加载全部元数据到内存LocatedFileStatus比FileStatus多了块位置信息但此处仅用getPath()和isDirectory()bos.write()用getBytes(UTF_8)显式指定编码防止平台默认编码差异。4.2 文件名冲突HDFS 同名文件覆盖 vs 本地重命名HDFS 目录下若存在part-r-00000和subdir/part-r-00000listFiles()会返回两个同名文件。GetMerger合并时若只用file.getPath().getName()会导致分隔符重复--- FILE: part-r-00000 ---出现两次。解决方案用相对路径作为标识符// 替换原代码中的分隔符行 String relativePath hdfsPath.toUri().relativize(file.getPath().toUri()).getPath(); bos.write((\n--- FILE: relativePath ---\n).getBytes(StandardCharsets.UTF_8));这样分隔符变成--- FILE: part-r-00000 ---和--- FILE: subdir/part-r-00000 ---完全可区分。4.3 网络中断恢复断点续传的简易实现HDFS 下载大目录时可能因网络抖动中断。GetMerger若重跑会覆盖整个localOutput文件。加入断点续传逻辑// 在方法开头检查本地文件是否存在及大小 File localFile new File(localOutput); long writtenSize 0; if (localFile.exists()) { writtenSize localFile.length(); // 读取已写入的最后分隔符位置跳过已处理文件 // 简化版假设每次写入文件后都有分隔符记录已处理文件数 } // ... 后续循环中跳过前 writtenSize 对应的 files 子集实际生产中建议用FileChannel.transferFrom()配合RandomAccessFile实现精确断点但实验阶段用writtenSize 0判断并提示用户手动清理更稳妥。5. 避坑指南Eclipse Hadoop 开发中最常踩的 5 个坑5.1 现象FileSystem.get(conf)返回 null原因conf未正确加载core-site.xml或fs.defaultFS配置项缺失/拼写错误如写成fs.default.fs。解决在代码中加调试输出System.out.println(Default FS: conf.get(fs.defaultFS));确认值为hdfs://localhost:9000检查core-site.xml是否在src/main/resources/下且 Eclipse 的 Build Path → Source 中包含该目录。5.2 现象listStatus()返回空数组但hdfs dfs -ls /path能看到文件原因HDFS 权限问题。Hadoop 伪分布式默认启用权限检查dfs.permissions.enabledtrue而 Eclipse 进程以当前 Linux 用户运行该用户在 HDFS 中无读权限。解决临时关闭权限仅开发用——修改hdfs-site.xmlproperty namedfs.permissions.enabled/name valuefalse/value /property然后重启 HDFS$HADOOP_HOME/sbin/stop-dfs.sh $HADOOP_HOME/sbin/start-dfs.sh。5.3 现象java.lang.NoClassDefFoundError: org/apache/hadoop/fs/FileSystem原因Eclipse 的 Run Configuration 中 JRE System Library 版本过低如 JRE 1.7而 Hadoop 2.10.1 要求 Java 8。解决右键项目 → Properties → Java Build Path → Libraries → 双击JRE System Library→ Alternate JRE → 选择 JavaSE-1.8 或更高版本。5.4 现象GetMerger合并后文件末尾多出乱码或缺失最后一行原因BufferedReader.readLine()在文件末尾无换行符时返回null但PrintWriter.println()会额外添加换行。若原始文件末尾无\nreadLine()读取最后一行后readLine()返回null循环结束但最后一行内容已写入未加换行。解决改用InputStreamBufferedInputStream如前述代码绕过readLine()的换行处理逻辑直接按字节操作。5.5 现象Eclipse 中Run As → Run on Hadoop无响应或报Connection refused原因Hadoop 服务未启动或core-site.xml中fs.defaultFS指向错误地址如hdfs://127.0.0.1:9000与localhostDNS 解析不一致。解决终端执行jps确认NameNode、DataNode、SecondaryNameNode进程存在执行hdfs dfsadmin -report验证集群健康将core-site.xml中fs.defaultFS统一改为hdfs://localhost:9000。6. 进阶技巧用 Shell 脚本封装 PutMerge/GetMerger实现一键自动化6.1 打包成可执行 JAR避开 Eclipse 依赖地狱Eclipse 导出的 Runnable JAR 会把所有依赖打进去但 Hadoop 库与本地 JDK 冲突频发如slf4j-log4j12版本冲突。更可靠的方式是导出为普通 JAR用hadoop jar命令运行# 1. Eclipse 导出File → Export → Java → JAR file → 选中 src/ 和 lib/ 下的必要 JARhadoop-common, hadoop-hdfs, hadoop-client # 2. 命令行运行确保 HADOOP_HOME 已设置 hadoop jar hdfs-merge-tool.jar com.example.PutMerge /local/input /hdfs/output/merged.txt hadoop jar hdfs-merge-tool.jar com.example.GetMerger /hdfs/output /local/download/merged.txthadoop jar会自动将$HADOOP_HOME/share/hadoop/common/等路径加入 classpath避免手动管理依赖。6.2 参数校验表防止运行时崩溃的 7 个必检项检查项验证命令期望输出失败后果HDFS 是否存活hdfs dfsadmin -safemode getSafe mode is OFFFileSystem.get()报IOException目标路径权限hdfs dfs -ls -d /hdfs/outputdrwxr-xr-x - user supergroup 0 ... /hdfs/outputmkdirs()失败本地目录可读ls -l /local/input/-rw-r--r-- 1 user user 1024 ... file1.txtFileInputStream抛FileNotFoundExceptionHadoop 配置加载hadoop classpath | grep core-site/etc/hadoop/core-site.xmlconf.get(fs.defaultFS)返回 nullJava 版本java -versionopenjdk version 1.8.0_362NoClassDefFoundError网络连通性telnet localhost 9000Connected to localhost.Connection refused磁盘空间df -h /local/downloadAvailable 2x expected output sizeIOException: No space left on device6.3 日志增强在关键节点打印 HDFS 块分布快速定位数据倾斜GetMerger下载前可打印每个文件的块位置判断是否所有文件都集中在同一 DataNode导致下载瓶颈// 在 downloadAndMerge() 中遍历 files 前插入 for (LocatedFileStatus file : files) { BlockLocation[] locations fs.getFileBlockLocations(file.getPath(), 0, file.getLen()); System.out.printf(File %s: %d blocks, hosts%s%n, file.getPath().getName(), locations.length, Arrays.toString(locations[0].getHosts())); // 仅打印首块 host }若所有文件hosts都是[node1]说明数据未均衡需运行hdfs balancer。从那以后我每次写 HDFS 工具类都强制在main()开头加System.out.println(Hadoop version: VersionInfo.getVersion());和System.out.println(Config loaded from: conf.get(hadoop.config.resources));两行代码能省掉 80% 的环境排查时间。希望帮到你。本文还有配套的精品资源点击获取