ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HDFS文件读写原理与Java API实战:从架构到实验避坑指南

HDFS文件读写原理与Java API实战:从架构到实验避坑指南 简介本资源为计算机系《云计算技术》课程配套实验报告聚焦HDFS分布式文件系统的读写、合并与上传下载实践适合正在学习Hadoop生态、需要完成同类实验或理解HDFS编程接口的高校学生。报告完整展示了在Linux环境下安装Eclipse、配置Hadoop连接插件及Map/Reduce视图的流程并围绕PutMerge与GetMerger两项核心功能展开覆盖本地多文件合并上传至HDFS、从HDFS下载目录并合并保存到本地的编程思路与API调用要点。资源共1个文件为PDF格式压缩包大小1.1MB便于直接阅读、打印或作为实验报告撰写参考。已有561人学习下载。除实验目标、内容和过程外还收录了实验者在配置Eclipse与Hadoop连接时遇到的环境问题及解决过程对于初次搭建Hadoop开发环境、调试文件读写程序具有实际借鉴价值可帮助读者少走弯路并深化对HDFS读写流程的整体理解。1. 云计算实验四HDFS文件读写到底在练什么交完实验三的MapReduce很多同学以为实验四只是把文件传上去再读出来结果一上手才发现光是让Java程序连上HDFS就够折腾一晚上。这个实验的题眼不在“读写”这两个字而在“透过读写看HDFS的架构设计”——客户端到底跟谁说话、数据从哪条链路走、为什么写副本要写三个、为什么读文件可以不锁文件。把这层逻辑理清实验报告才能写出区分度面试被问HDFS读写流程时也才有话可说。这篇笔记按我自己的实验路径来写先讲清楚原理层面的读写链路再给出可直接复现的伪分布式环境搭建步骤然后是Java API的核心代码和参数调法最后是五个最容易让实验翻车的坑。不管你是云计算专业要做课程实验还是在补HDFS编程实践这块短板照着走完一遍读写的黑匣子基本就打开了。2. 动手前先看清HDFS读写原理客户端、NameNode与DataNode的分工2.1 读流程和写流程的链路差异在哪HDFS读和写的最大区别在于数据流向不同。读操作是客户端主动向NameNode要元数据然后直接跟DataNode建连接拉数据写操作则是客户端先把数据切块再逐块推给DataNodeNode之间还要做流水线复制。这个差异解释了为什么读路径短、写路径长也解释了为什么写操作的失败点比读操作多。读流程大概是客户端调用FileSystem.open()RPC通知NameNodeNameNode返回文件每个block的DataNode位置列表客户端按距离排序后直接跟最优的DataNode建立TCP连接读数据。注意“按距离排序”这一步——HDFS在2.x之后引入了NetworkTopology会把“本机、同机架、跨机架”的节点排个优先级目的是减少跨机架流量。实验里虽然是伪分布式读数据时客户端和DataNode在同一台机器上这个排序逻辑不会触发但你得在报告里写出来才算理解了这个设计。写流程则复杂得多客户端先调FileSystem.create()让NameNode在命名空间里建文件条目拿到一个租约lease然后按128MB切块每写满一个块就调用addBlock()申请新的block位置列表最后把chunk packet沿着DataNode流水线逐级推送。每个packet在最后一个DataNode写完后会沿原路返回确认消息ack客户端收到确认才算这个packet写成功。有一个细节经常被实验报告忽略客户端写文件时NameNode只在内存里标记文件为“正在写入”数据块实际是落盘到DataNode的磁盘后DataNode才向NameNode汇报block上报。所以实验里如果写完立刻用hdfs fsck检查偶尔会看到“Under-replicated Blocks”或“Missing Blocks”这不是写失败了而是DataNode的增量汇报还没跟上。等一两分钟再查就正常了。2.2 副本放置策略和租约机制三个副本不是随便放的实验报告的“结果分析”部分建议花一段话写副本放置策略。HDFS默认副本数是3放置策略是第一个副本放在客户端所在节点如果客户端不在集群内则随机选一个负载低的节点第二个副本放在与第一个不同机架的节点第三个副本放在与第二个相同机架的不同节点。这个“21”布局是为了平衡容错和写入带宽——如果三个副本放三个机架任何机架挂了数据都还在但跨机架的写流量会翻倍如果三个副本放一个机架写入快但机架断电就全没了。租约lease机制是写流程里的隐含约束。客户端开始写文件时要向NameNode申请租约租约默认软限制60秒、硬限制1小时。软限制过期时如果客户端还在续租NameNode不会干预如果客户端崩溃NameNode要等到硬限制过期才能回收租约并允许其他客户端写同一路径。实验里最容易踩的坑是上一个程序没正常关闭FileSystem就退出导致租约没释放下一个程序再往同路径写就报LeaseExpiredException。这不是代码逻辑错了而是没有在finally块里调fs.close()。连接HDFS时的RPC超时参数也可以在这里提一下。默认ipc.client.connect.timeout是20000msipc.client.read.timeout是120000ms。如果实验环境开了一堆虚拟机网络有丢包写大文件时经常读超时把超时值调大能减少重试引起的二次故障。3. 搭建最小实验环境从单机伪分布式到命令验证3.1 伪分布式部署的关键配置文件与启动命令做HDFS读写实验最省事的路径是单机伪分布式也就是在一台Linux机器上同时跑NameNode和DataNode进程。我一般用CentOS 7或Ubuntu 18.04的虚拟机装好JDK 1.8再解压Hadoop发行版。这里不写具体安装命令直接给最小配置清单四个文件改完就能起服务。core-site.xml里最关键的配置是fs.defaultFS它决定了HDFS的对外地址。实验里写成hdfs://localhost:9000就行注意不能用file:///否则所有HDFS操作都会落到本地磁盘上。另一个值得配的是hadoop.tmp.dir默认值是/tmp/hadoop-${user}Linux重启会清空最好改成固定路径例如/data/hadoop/tmp否则每次开机NameNode的元数据都没了。hdfs-site.xml需要配三个参数dfs.replication伪分布式只能设1设3会在写文件时报副本不足的告警、dfs.namenode.name.dir元数据目录、dfs.datanode.data.dir数据块目录。yarn-site.xml和mapred-site.xml在纯HDFS读写实验里不是必需项但如果后续实验要做MapReduce第一步就把它们配好能省得返工。启动顺序上有一个容易漏的点第一次启动NameNode之前必须先执行hdfs namenode -format如果不格式化直接start-dfs.sh日志里会报“NameNode is not formatted”之类的错误。# 第一次启动前必须格式化NameNode元数据目录 hdfs namenode -format # 一次性启动NameNode和DataNode start-dfs.sh # 验证四个关键进程是否都在 jps # 在Web UI上确认NameNode和DataNode状态可选 # 默认地址 http://localhost:9870格式化动作会清空NameNode的元数据目录所以这个命令只能执行一次。如果不小心执行了第二次原来HDFS里所有文件都会“消失”——因为元数据没了。实验时如果手滑唯一的后悔药是把dfs.namenode.name.dir目录删掉重新格式化同时把DataNode的dfs.datanode.data.dir清掉否则DataNode的cluster ID会和NameNode不一致启动时报Incompatible clusterIDs。这个清洗操作是血泪经验建议在报告里专门写一段。3.2 先用Shell命令验证读写链路常用命令与结果判读Java程序跑通之前先用命令行验证HDFS本身是健康的。hdfs dfs是实验里最常用的一组命令我一般按“目录操作—文件上传—文件读取—文件下载”的顺序走一遍每一个命令都要能解释输出。# 1. 创建实验目录-p 表示递归创建父目录不存在时自动创建 hdfs dfs -mkdir -p /user/exp04/input # 2. 生成一个本地测试文件并上传 echo HDFS write test data local_test.txt hdfs dfs -put local_test.txt /user/exp04/input/ # 3. 查看HDFS上的文件信息注意看副本数、块大小、所属用户 hdfs dfs -ls -R /user/exp04/ # 4. 读取文件内容到标准输出等价于查看文件 hdfs dfs -cat /user/exp04/input/local_test.txt # 5. 把HDFS上的文件拉回本地覆盖同名文件时用 -f hdfs dfs -get /user/exp04/input/local_test.txt local_download.txt # 6. 对比原始文件和下载文件是否一致 diff local_test.txt local_download.txt-put和-get是最典型的读写入口。注意-put的语义与Linux的cp不同-put会把本地文件复制到HDFS原文件还留在本地-get同理。如果上传的是大目录-put默认递归上传子目录不需要额外的-r参数这点和Linux的cp -r不同容易在实验报告里写错。块大小的查看命令是hdfs fsck /user/exp04/input/local_test.txt -files -blocks -locations它会列出文件占用了哪些块、每个块在哪个DataNode上。写完数据之后跑一下这个命令可以把“数据确实切块存储在DataNode上”作为实验结论写进报告比单纯贴-ls的输出更有说服力。4. 用Java API完成HDFS文件读写实验报告的核心代码与参数说明4.1 写文件FileSystem.create的正确打开方式Java API是实验四报告的主体多数课程要求用org.apache.hadoop.fs.FileSystem完成文件写入和读取。写文件的标准姿势是拿到FileSystem实例然后用create()获取输出流逐块写入数据。这里有三个参数直接影响运行结果是否覆盖、缓冲区大小、副本系数。import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import java.net.URI; public class HdfsWriteDemo { public static void main(String[] args) throws Exception { // 关键点1core-site.xml的fs.defaultFS会被自动加载 // 但如果你没有把配置文件放在classpath里就必须显式指定地址 Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://localhost:9000); // 关键点2第二个参数是用户名缺省时会取系统当前用户名 // 实验环境里如果启动hadoop的用户和当前用户不同这里必须显式指定 FileSystem fs FileSystem.get(new URI(hdfs://localhost:9000), conf, root); // 创建文件Path的语义是HDFS上的绝对路径或相对路径 Path remotePath new Path(/user/exp04/output/java_write.txt); // create()返回FSDataOutputStreamwrite后必须flushclose FSDataOutputStream out fs.create(remotePath, true, 4096, (short) 1, 128L * 1024 * 1024); String content experiment 04: hdfs write test\n; byte[] bytes content.getBytes(UTF-8); // 逐块写入这里只演示单次写入 out.write(bytes, 0, bytes.length); out.flush(); // 关键点3不close就退出租约不会释放下次写同路径会失败 out.close(); fs.close(); System.out.println(write done); } }create(Path f, boolean overwrite, int bufferSize, short replication, long blockSize)这五个参数的顺序容易记混特别是中间的bufferSize它控制的是客户端写数据时的缓冲字节数不是HDFS的block大小。replication传(short) 1是因为伪分布式只有一个DataNode如果传3写文件本身不报错但后续NameNode会发现副本数永远不满足而持续告警。blockSize传128L * 1024 * 1024这是HDFS 2.x/3.x的默认块大小实验文件达不到128MB也没关系HDFS会在文件写完后按实际大小分配块空间。FileSystem.get()的第三个参数在本地Windows环境跑的时候特别关键。如果直接写成FileSystem.get(conf)Hadoop会通过UserGroupInformation.getCurrentUser()拿系统用户Windows下拿到的一般是Administrator而HDFS的目录权限是按Linux用户管理的常导致Permission denied。显式指定为root或启动Hadoop的用户能绕开这个权限坑。4.2 读文件FSDataInputStream的两种读法与EOF判断读文件相对简单但有一个新手容易犯的错用FileSystem.open()拿到流之后直接调read()读固定长度的字节却不检查返回值。HDFS的read()语义和普通InputStream一致返回-1表示读取结束如果忽略这个返回值就会把文件尾部不足缓冲区大小的数据读丢。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.net.URI; public class HdfsReadDemo { public static void main(String[] args) throws Exception { Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://localhost:9000); FileSystem fs FileSystem.get(new URI(hdfs://localhost:9000), conf, root); Path remotePath new Path(/user/exp04/output/java_write.txt); FSDataInputStream in fs.open(remotePath); byte[] buffer new byte[1024]; int bytesRead; StringBuilder sb new StringBuilder(); // 循环读取read返回-1表示到达文件末尾 // 这里不能用 read(buffer) 的返回值当作实际字节数 // 因为最后一次read可能只填满部分缓冲区 while ((bytesRead in.read(buffer)) ! -1) { sb.append(new String(buffer, 0, bytesRead, UTF-8)); } in.close(); fs.close(); System.out.println(read content:\n sb); } }这段代码的实际执行逻辑是每次in.read(buffer)最多填满1024字节返回实际读取的字节数循环直到返回-1。new String(buffer, 0, bytesRead, UTF-8)这里指定了读取长度避免把缓冲区尾部的旧数据一起打印出来。FSDataInputStream还有一个read(long position, byte[] buffer, int offset, int length)方法可以从指定偏移量读取不改变流指针位置。实验报告如果要写“支持随机读”可以在正文里提一下这个接口HDFS在2.x之后支持PositionedRead客户端可以指定block编号和偏移量读取任意一段数据MapReduce的InputSplit就是基于这个能力做的。不过实验代码不需要实现这个作为扩展知识点写在分析里即可。4.3 实验代码的编译运行classpath与日志级别两个小坑代码写完怎么跑是另一个高频翻车点。Hadoop的依赖包很多用javac编译时要加上$HADOOP_HOME/share/hadoop下的几个关键jar包用hadoop jar运行则可以省去手动拼classpath的麻烦。# 编译把hadoop-common和hadoop-hdfs相关的jar都带上 javac -classpath $(hadoop classpath) -d ./classes HdfsWriteDemo.java HdfsReadDemo.java # 运行用hadoop命令直接跑会自动加载所有依赖 hadoop jar ./classes/HdfsWriteDemo.jarhadoop classpath会输出一大串jar包路径直接作为classpath参数传给javac比自己逐个找包可靠得多。运行时的日志格式是Log4j控制的默认会输出INFO级别控制台会刷一堆Creating a new HdfsClient之类的信息不影响结果但影响观感。要调整的话在$HADOOP_HOME/etc/hadoop/log4j.properties里把log4j.logger.org.apache.hadoop改成WARN重新运行就不会刷屏了。这个操作虽然不是实验必需但在报告里写一句“日志级别调整为WARN便于观察结果”能显得心细。5. HDFS读写避坑指南权限、覆盖写与网络抖动5.1 现象Permission denied 但目录明明在现象hdfs dfs -put或Java程序写文件时报Permission denied: userubuntu, accessWRITE, inode/user:root:supergroup。原因HDFS的根目录默认属主是root用户组是supergroup非root用户没有根目录的写权限。实验环境里登录用户一般是ubuntu或hadoop直接用会撞权限墙。解决两个办法一是用hdfs dfs -chmod -R 777 /user改权限二是把当前用户加入supergroup组。实验中我一般用前者快且不影响其他配置。另外注意/user目录下每个用户默认有自己的home目录如果没有就先hdfs dfs -mkdir -p /user/当前用户名。5.2 现象文件已存在却又被写了一遍现象Java程序第二次运行时抛FileAlreadyExistsExceptionShell命令第二次-put时报File already exists。原因FileSystem.create()的第二个参数overwritefalse表示不覆盖。解决Shell命令加-f参数或用hdfs dfs -rm删掉旧文件Java代码把create()的第二个参数改为true。这里有个细节值得在报告里写overwritetrue只对文件生效如果目标路径是一个非空目录create()还是抛异常HDFS不提供“递归覆盖”的语义。5.3 现象写完文件立刻读末尾少了一段数据现象用-cat看内容最后几十个字节丢了但-ls显示的文件大小是对的。原因客户端写入的最后一块数据还在DataNode的缓冲区里虽然out.flush()会推给DataNode但DataNode是否落盘完成并不由flush()保证通常要等close()触发complete()调用后才真正结束。解决养成写完立即close()的习惯。如果业务上必须边写边读需要在写端调用out.hflush()它会把数据推送到DataNode的OS缓存再严格一点用out.hsync()强制落盘。实验里不需要用后两个方法但报告里能写出flush、hflush、hsync三者的区别是很大的加分项。5.4 现象NameNode进程正常但写文件一直超时现象Java程序运行到fs.create()或第一次write()时卡住最终报SocketTimeoutException。原因NameNode和DataNode都在但DataNode的dfs.datanode.data.dir目录所在磁盘满了DataNode虽然活着却不接受新块写入客户端等ack超时。解决用df -h查看磁盘使用率清理旧块或者在hdfs-site.xml里把dfs.datanode.data.dir指向有空间的磁盘。还有一种概率较小的原因虚拟机网络配置为NAT模式客户端连DataNode的9000端口被防火墙拦了解决方法是检查firewalld状态把9000端口加到白名单。5.5 现象Windows本地跑Java程序FileSystem.get卡一分钟才报错现象FileSystem.get()长时间不返回最后报java.net.UnknownHostException: localhost或Connection refused。原因Windows下Hosts文件里没有localhost的IPv6映射Hadoop的InetAddress解析失败。解决在C:\Windows\System32\drivers\etc\hosts里加一行127.0.0.1 localhost。另一个Windows专属坑是Hadoop在Windows下依赖winutils.exe如果没有它运行时可能报Failed to locate the winutils binary。实验课一般都在Linux虚拟机里跑如果老师让在Windows本机连远程集群环境和Linux完全不同建议先问清楚集群的详细配置再动手。6. 让实验报告更有说服力读写验证、日志排查与一分钟定位问题写到这里实验基本跑通了但报告要拿高分还得往“验证”和“排障”方向再走两步。第一步是验证数据的完整性——用hdfs fsck检查块的健康状态命令是hdfs fsck /user/exp04/output -files -blocks -locations。看到Total blocks (valid): 1且没有CORRUPT字样说明实验数据没问题。第二步是验证读写性能——在文件里写入至少256MB数据用time hadoop jar WriteDemo.jar跑一遍记录耗时再算一下吞吐量。伪分布式环境下写100MB数据耗时通常在几十秒到几分钟不等吞吐量低不用慌报告里把“单机伪分布式只有单个DataNode写流水线不成立”作为分析写进去反而显得深入。还有个很实用的排查习惯日志定级定位。运行时抛异常不要只看控制台最后10行先看第一条ERROR或WARN通常那才是根因。比如报FileAlreadyExistsException时堆栈里会同时出现create相关的调用链说明问题在创建文件那一步而不是写数据那一步。用hdfs dfs -tail /user/exp04/output/java_write.txt可以只读文件的最后1KB内容快速确认写入是否完整。我自己的经验是每次在命令行和Java API之间来回切换时最容易混淆的是路径前缀Shell命令里写/user/exp04/inputJava代码里就要写hdfs://localhost:9000/user/exp04/input前者用的是默认文件系统后者显式指定绝对地址。两边的写法混着用就会莫名报Path not found。这个细节我教过好几个学弟他们看完都是一副“原来如此”的表情——希望这篇也能帮你省下今晚的排查时间。本文还有配套的精品资源点击获取
返回列表