
简介《云计算技术实验报告四HDFS文件的读写》是一份计算机专业《云计算技术》课程的实验报告面向正在学习Hadoop生态、需要掌握HDFS文件读写编程实现的本科生或开发者。报告记录了完整的实验过程从Eclipse安装、配置连接Hadoop集群、添加hadoop-eclipse-plugin插件到新建Map/Reduce项目并编程实现PutMerge与GetMerger功能清晰呈现了本地多文件合并上传至HDFS、以及从HDFS路径下载多文件合并保存到本地的实现思路。实验过程中还总结了环境配置的排错经验对解决Eclipse与Hadoop连接时容易遇到的路径配置、JAR版本不兼容等问题有直接参考价值。资源为1份PDF文档大小1.1MB内容包含实验目的、要求、过程与总结适合用于课前预习、实验参考或复习复盘。目前已有561人学习下载可作为高校云计算课程实验的配套参考资料。1. HDFS读写实验最容易被低估的一环Eclipse与Hadoop插件的连通性计算机系《云计算技术》实验报告里的“HDFS 文件的读写”表面上要写的是 PutMerge 和 GetMerge 两个小程序实际操作中大部分人的时间却耗在 Eclipse 装插件、配 Map/Reduce Location 这一关。这个实验的本质不是“会写代码”而是“代码能连上 HDFS”牵涉到 NameNode 地址、端口、Linux 用户权限和 FileSystem API 的调用方式。对刚开始做 HDFS 编程实践的同学来说环境配置的坑远比算法逻辑大对有几年工作经验的工程师来说从这个实验可以重新梳理 HDFS 读写流程与小文件合并的边界条件。本文基于实验报告中的步骤把 hdfs 读写流程、插件配置、Java API 实现和验证手段拆开尽量做到每一步都能复现。2. HDFS 读写流程与 PutMerge/GetMerge 的语义边界2.1 写流程客户端、NameNode 与 DataNode 之间的协调一次最简单的 HDFS 文件写入远比hdfs dfs -put看起来复杂。客户端调用FileSystem.create()时不是直接连接 DataNode而是先向 NameNode 发起请求“我要创建一个/user/data/part-0001文件”。NameNode 会检查路径是否合法、父目录是否存在、用户是否有权限然后返回一个带租约的LocatedFileStatus里面包含“下一批 block 应该写到哪些 DataNode”的列表。这个列表通常按机架感知排序目的是让数据在机架之间冗余。客户端拿到列表后会按 128MB 分块可以配置dfs.blocksize把第一块数据推给第一个 DataNode第一个 DataNode 再复制给第二个第二个复制给第三个形成管道。每个数据包写入成功后应答会沿着管道反向回到客户端客户端继续写下一批数据。整个过程受dfs.client.block.write.replace-datanode-on-failure等参数约束如果中途某个 DataNode 挂掉客户端会自动从管道中剔除它并重新建立复制。这里有一个容易误解的地方FSDataOutputStream执行write()后数据还在客户端的写入缓冲区只有调用close()或者hsync()数据才对其他读取方可见。所以实验代码里如果写完就立刻读取必须保证流已经关闭否则看到的还是一个长度为 0 的文件。2.2 读流程NameNode 定位客户端就近拉取读流程相对简单。客户端调用FileSystem.open(path)NameNode 返回该文件所有 block 的LocatedBlock列表每个LocatedBlock包含 block 的 Datanode 地址和与客户端所在机器的距离。客户端在读取时会选择距离最近的 DataNode同一节点优先然后同机架最后跨机架。FSDataInputStream的read()会自动处理与 DataNode 的连接切换。当一个 block 读取完毕它会根据 NameNode 返回的元数据跳到下一个 block 所在的 DataNode而不是重新创建连接。对于连续大文件的读取HDFS 做到了顺序读的优化但对于GetMerge这种需要打开多个小文件的场景效率瓶颈反而在“每个文件都要向 NameNode 请求一次元数据”上。2.3 PutMerge 与 GetMerge 在语义上的不对称实验报告里写的PutMerge和GetMerger实际上对应两个不对称的操作。PutMerge是把本地磁盘上的多个文件合并成一个文件再上传到 HDFS。合并过程可以在本地先做也可以在 HDFS 上边写边合并。前者会产生一个巨大的临时文件后者更常见打开一个 HDFS 输出流遍历本地所有文件把每个文件的内容按顺序写入这个输出流。GetMerge则是从 HDFS 下载一个目录目录下包含多个文件把这些文件合并后保存到本地单个文件中。Hadoop 本身提供了一个工具方法FileUtil.copyMerge()但实验报告明确要求“编程实现 GetMerge”所以最好还是用FileSystemAPI 手动走一遍读写流程否则就失去了训练意义。这里还要注意命名实验报告写作GetMerger标准 API 里是getmerge命令和FileUtil.copyMerge()没有Merger后缀。写代码时不要被实验报告的拼写误导。2.4 命令与 API 的对照hdfs dfs -getmerge 可以直接做实验在写 Java 代码之前先看这些功能在 hdfs 常用命令里是什么样子。这样可以让你知道程序要复刻的目标是什么也方便后面验证结果。# 合并本地 /tmp/local_dir 下的文件上传到 HDFS hdfs dfs -put /tmp/local_dir/* /user/hadoop/merged.txt # 下载 HDFS 目录 /user/hadoop/out 下所有文件合并到本地 /tmp/getmerge_result.txt hdfs dfs -getmerge /user/hadoop/out /tmp/getmerge_result.txthdfs dfs -getmerge的原生语法是hdfs dfs -getmerge [-nl] src localdst。其中-nl表示在合并后的文件末尾添加一个换行符避免两个文件边界处因为缺少换行而粘连。这个参数在实际工作中经常用到尤其当源文件是文本日志时忘记加-nl会让下一行日志直接拼到上一行末尾。命令与 API 的对应关系如下操作命令行程序 API关键路径创建目录hdfs dfs -mkdir -p /user/outfs.mkdirs(new Path(/user/out))NameNode 元数据变更上传文件hdfs dfs -put /tmp/a /user/out/afs.copyFromLocalFile()写流程下载文件hdfs dfs -get /user/out/a /tmp/fs.copyToLocalFile()读流程列出目录hdfs dfs -ls /user/outfs.listStatus()返回 FileStatus 数组合并下载目录hdfs dfs -getmerge /user/out /tmp/all.txtFileUtil.copyMerge()读多个文件写一个本地文件实验里要求的PutMerge和GetMerge理论上就是手工实现表格里最后一行。理解了这一点再去看代码就不会觉得 API 很神秘listStatus()负责拿到目录下所有文件的清单open()负责读取每个文件BufferedOutputStream负责把内容写入本地目标。3. Eclipse 连接 Hadoop插件 jar、Map/Reduce Location 与端口配置3.1 插件版本 2.10.1 与 Hadoop 版本的匹配边界实验报告里把hadoop-eclipse-plugin-2.10.1.jar放到了 Eclipse 的plugins目录下。这个文件名的 2.10.1 通常对应 Hadoop 2.10.x 版本。如果你的集群是 Hadoop 3.x那么这个插件大概率会出现“连接成功但无法列出文件”“上传报错IllegalArgumentException: Not a host:port pair”等问题。原因很简单Hadoop 3.x 的 RPC 协议在鉴权和默认端口方面有变化而 Eclipse 插件的Map/Reduce Location内部还是按 Hadoop 2.x 的mapred.job.tracker方式解析地址。遇到这种情况常见做法是从源码重新编译插件或者放弃插件直接用 Eclipse 写 Java 代码并打成 jar 包在 Linux 上用hadoop jar命令提交。对于做实习和课程设计的学生来说后者更可控。如果你确认集群是 Hadoop 2.10.1插件安装步骤本身没有太多技术含量把 jar 放入eclipse/plugins重启 Eclipse然后进入Window - Preferences - Hadoop Map/Reduce指定 Hadoop 安装目录注意这个目录需要包含bin、share/hadoop等子目录。3.2 安装插件后 Eclipse 不出现 DFS Locations 的排查很多人在安装插件之后找不到Window - Show View - Other - Map/Reduce这是因为 Eclipse 版本与插件不兼容。Eclipse 的equinox容器不是所有 jar 都会在启动时加载成功插件初始化失败后 Eclipse 通常只在 log 里打一条错误界面上没有任何提示。排查顺序我是这样走的# 1. 在 Eclipse 的 configuration 目录下找到日志 tail -n 50 /path/to/eclipse/configuration/*.log # 2. 检查插件的 bundle 是否被识别 # 在 Eclipse 安装目录下执行 ./eclipse -clean -consolelog如果日志里出现Failed to create the parts controls或者ClassNotFound org.apache.hadoop.eclipse.Activator基本就是 jar 包里的MANIFEST.MF声明的依赖和当前 Eclipse 版本不匹配。不要浪费时间直接换一个与 Hadoop 版本配套的插件 jar。网上不少教程会让用户修改 Eclipse 的eclipse.ini加-clean那只是解决缓存问题对插件初始化失败无能为力。一个更稳妥的做法是不要在 Eclipse 里做 HDFS 可视化浏览。用浏览器访问http://namenode地址:9870就能图形化浏览 HDFS 3.2.1 系统中的文件列表器这比 Eclipse 插件可靠得多。Eclipse 只用来写代码连接测试全部用命令行完成。3.3 配置 Map/Reduce Location 的四个关键参数Eclipse 插件配置页面里的Map/Reduce Location对话框虽然简陋但每个字段都有实际意义。下表是常用的配置项配置项示例值含义与常见错误Location namehdp_1只是显示名可任意写Map/Reduce Masterhadoop-master:9001Hadoop 2.x 是YARN ResourceManager的地址和端口如果你的实验环境只是单机伪分布式则填localhost:9001DFS Masterhadoop-master:9000NameNode 的 RPC 端口默认 9000也可能被配置成 8020。不要写 50070 或 9870那是 web UI 端口User namehadoop提交任务到 HDFS 的用户名必须存在于集群上否则会Permission denied注意Map/Reduce Location对话框里的Port默认是 9001这是早年 Eclipse 插件对mapred.job.tracker的直传。如果你用的是 Hadoop 2.x 的 YARN这个端口其实不会被真正用来提交任务但插件校验时会要求它可达。一个常见的假象是DFS Master 地址填错了反而报错提示“Map/Reduce Master 错误”因为插件先校验 Map/Reduce 连接。所以配置时先把两个地址都填对否则会走弯路。3.4 可视化验证浏览器 9870 与 Eclipse 的 DFS Locations配置完成后在 Eclipse 的Project Explorer中会多出一个DFS Locations树形节点。展开它如果能看到/user、/tmp等目录说明插件连接成功。但这里要提醒即使插件连接失败也不影响你写 Java 代码因为你写的程序运行在hadoop jar提交的 JVM 里它读的是core-site.xml里的fs.defaultFS与 Eclipse 插件无关。我一般建议学生在配置插件的同时打开浏览器输入http://namenode地址:9870确认Datanodes列表是否正常。如果这个页面上能看到 DataNode说明 NameNode 和 DataNode 已经建立物理连接后续代码报错就大概率出在用户权限或者代码写法上而不是集群没启动。4. 用 FileSystem API 实现 PutMerge 与 GetMerge 的 Java 代码4.1 项目构建与依赖导入不要只靠 hadoop-eclipse-pluginEclipse 里新建一个Map/Reduce Project时插件会自动把所有 Hadoop 相关 jar 加入构建路径但这是一种很“粗”的依赖方式。换一个环境、换一个集群版本项目很容易编译不过或运行时报NoClassDefFoundError。更规范的做法是在项目根目录加一个pom.xml用 Maven 管理 Hadoop 客户端依赖。dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version2.10.1/version /dependency这个hadoop-client依赖会传递引入hadoop-hdfs、hadoop-common等核心模块足够写实验里的读写程序。如果你是 Hadoop 3.2.1就把版本改成 3.2.1包结构没有变。引入依赖之后不再依赖 Eclipse 插件提供的Library这样程序拿到任何环境都能运行。4.2 PutMerge本地多个文件合并写入 HDFS下面这段代码演示从本地目录读取多个文件合并后写成一个 HDFS 文件。核心思路是先拿到本地文件列表再打开一个 HDFS 输出流逐个文件写入。import org.apache.hadoop.conf.Configuration; 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.Path; import java.io.BufferedInputStream; import java.io.IOException; public class PutMerge { public static void main(String[] args) throws IOException { // args[0] 本地目录比如 /tmp/localdata // args[1] HDFS 目标文件比如 /user/hadoop/merged.txt String localDir args[0]; String hdfsFile args[1]; Configuration conf new Configuration(); // 读取 core-site.xml 和 hdfs-site.xml否则不知道 NameNode 地址 conf.addResource(core-site.xml); conf.addResource(hdfs-site.xml); FileSystem localFs FileSystem.getLocal(conf); FileSystem hdfsFs FileSystem.get(conf); Path localPath new Path(localDir); Path remotePath new Path(hdfsFile); // 列出本地目录下所有文件不包括目录 FileStatus[] statusList localFs.listStatus(localPath); FSDataOutputStream out hdfsFs.create(remotePath, true); for (FileStatus status : statusList) { if (status.isFile()) { Path oneFile status.getPath(); FSDataInputStream in localFs.open(oneFile); byte[] buffer new byte[4096]; int bytesRead 0; while ((bytesRead in.read(buffer)) 0) { out.write(buffer, 0, bytesRead); } in.close(); } } out.close(); } }这里第一个关键参数是FileSystem.get(conf)它会从当前环境变量和core-site.xml中读取fs.defaultFS。实验环境里常见的错误是core-site.xml没在 classpath 中导致程序默认连接file:///创建一个本地文件而不是 HDFS 文件。判断方法很简单看最终生成的目录是hdfs://...还是本地路径。第二个参数是hdfsFs.create(remotePath, true)中的布尔值表示如果目标文件已存在则覆盖。实验报告中没有明确要求必须覆盖但课程设计中通常希望多次运行不报错。如果你希望第二次运行时提示FileAlreadyExistsException把true改成false即可。4.3 GetMergelistStatus 枚举目录并合并到本地GetMerge是实验报告的拼写实际对应getmerge的编程实现。下面的代码列出 HDFS 目录下所有文件逐个读取并追加写入本地文件。import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; public class GetMerge { public static void main(String[] args) throws IOException { // args[0] HDFS 目录比如 /user/hadoop/out // args[1] 本地合并文件比如 /tmp/result.txt String hdfsDir args[0]; String localFile args[1]; Configuration conf new Configuration(); conf.addResource(core-site.xml); conf.addResource(hdfs-site.xml); FileSystem hdfsFs FileSystem.get(conf); Path dirPath new Path(hdfsDir); BufferedOutputStream out new BufferedOutputStream(new FileOutputStream(localFile)); // listStatus 返回目录下第一层所有条目过滤掉目录 FileStatus[] statusList hdfsFs.listStatus(dirPath); for (FileStatus status : statusList) { if (status.isFile()) { Path filePath status.getPath(); FSDataInputStream in hdfsFs.open(filePath); byte[] buffer new byte[4096]; int bytesRead 0; while ((bytesRead in.read(buffer)) 0) { out.write(buffer, 0, bytesRead); } in.close(); } } out.close(); } }这里的hdfsFs.listStatus(dirPath)只列出目录下的直接子项不会递归子目录。实验中如果输入路径里有嵌套目录GetMerge会静默跳过这些目录。要递归所有文件必须改用hdfsFs.listFiles(dirPath, true)第二个参数true表示递归。这是很多人在真实数据上跑完发现合并文件缺少大量内容的主要原因。另一个常见的问题是文件顺序。listStatus返回的顺序并不保证按文件名排序虽然在 HDFS 上大部分时候是按字典序返回但生产环境不应依赖这个顺序。如果你需要特定顺序可以先拿到Path列表用Collections.sort()排序再逐个读取。4.4 运行方法与参数说明代码写完后在 Eclipse 里可以直接右键Run As - Java Application但是运行环境必须能解析core-site.xml否则会连接file:///。更贴近生产的方式是在 hadoop 集群节点上手动运行hdfs dfs -mkdir -p /user/hadoop/out hdfs dfs -put /tmp/a.txt /tmp/b.txt /user/hadoop/out/ # 先执行 PutMerge hadoop jar putmerge.jar PutMerge /tmp/localdata /user/hadoop/merged.txt # 然后执行 GetMerge hadoop jar getmerge.jar GetMerge /user/hadoop/out /tmp/result.txt这里有一个容易被忽略的参数FileSystem.get(conf)在 Hadoop 客户端中会尝试读取当前用户身份Linux 命令行的hadoop jar用了什么系统用户代码就会以什么用户身份访问 HDFS。如果你在 Eclipse 里以admin身份运行而集群的/user/hadoop目录只有hadoop用户可写就会提示Permission denied。解决方式是在代码中通过环境变量传身份而不是改代码里的UserGroupInformation后者需要额外的 Kerberos 配置。下表是实验中最常调整的几处参数代码位置参数典型值影响Configurationfs.defaultFShdfs://localhost:9000决定目标集群createoverwritetrue/false是否覆盖已有文件listStatus/listFilesrecursivefalse/true是否递归子目录缓冲区buffer大小4096越大磁盘 IO 越少但内存占用越高5. 实验后的四个验证技巧hdfs 常用命令与 FileUtil.copyMerge5.1 用 hdfs 常用命令核对实验结果写完代码先不要急着收工用命令行反向验证合并结果是否和预期一致。这一组命令是 HDFS 文件操作的基本功结合它们能快速定位是“代码写错”还是“HDFS 上数据本来就有问题”。# 查看远程合并文件的大小与 block 数 hdfs fsck /user/hadoop/merged.txt -files -blocks # 查看远程目录下所有文件 hdfs dfs -ls -R /user/hadoop/out # 比较本地合并文件与远端文件大小 hdfs dfs -stat %b /user/hadoop/merged.txt stat -c %s /tmp/result.txt如果本地合并文件比预期小优先检查GetMerge的缓冲读取循环。FSDataInputStream.read(buffer)不保证一次读完整个文件但循环判断bytesRead 0已经覆盖了这种情况。更隐蔽的问题是listStatus把符号链接或隐藏文件也算进来了可以用status.isFile()过滤掉目录但_SUCCESS这类 HDFS 作业标记文件依然会混入结果实验中使用hdfs dfs -getmerge可以通过-nl加换行而手写代码里需要自己判断是否跳过元数据文件。5.2 权限、端口、版本不一致的排查顺序如果程序运行时报错先看报错发生在哪个阶段。FileSystem.get阶段报Connection refused检查 NameNode 的 RPC 端口不要拿 9870 这个 web 端口的通断来推断 RPC 正常。Permission denied则优先检查当前 Linux 用户和 HDFS 目录属主hdfs dfs -ls /user第一列就是目录所有者。最麻烦的是版本不一致日志里出现Could not obtain block: BP-xxxx或UnsupportedOperationException基本是客户端 Hadoop jar 版本和集群端不一致需要把 Maven 里的hadoop-client版本对齐到集群版本。5.3 把手动流合并替换为 FileUtil.copyMerge 的边界实验要求编程实现但生产环境中常用的合并手段是FileUtil.copyMerge()。它接受 5 个参数其中最后一个boolean deleteSource如果设为true会在合并完成后删除源目录这里要非常谨慎。有一次我在生产环境清理临时目录时设成了true结果合并大表临时文件后源文件被自动删除下游任务找不到输入数据。对实验来说手动读写流程已经足够理解 HDFS 读写机制但了解FileUtil.copyMerge的边界仍然有用它内部实现就是先listStatus再逐个打开文件和写入目标流与上面的GetMerge代码逻辑几乎一致只是多了对_SUCCESS等日志文件的过滤。当你把实验代码替换成这个工具类时记得保留deleteSourcefalse并且留意conf中的io.file.buffer.size参数默认 4096对于大量几十 MB 的文件把这个值调大到 128KB 可以减少一半以上的系统调用次数合并速度提升明显。本文还有配套的精品资源点击获取