简介:这份实验报告出自计算机系《云计算技术》课程,以Hadoop IO为主题,聚焦如何改写实验4的GetMerge程序,从而将HDFS云端多个文件经过Gzip压缩后合并下载到本地。报告详细记录了实验目标、Eclipse环境下MapReduce项目的创建过程、核心代码片段以及实验结果,同时体现了从Hadoop配置初始化、压缩器创建到数据流复制的完整流程,适合正在学习Hadoop分布式文件系统读写与压缩编程的高校学生、云计算课程实践者参考。包体为1个PDF文件,容量仅573KB,内容精炼,便于快速阅读和打印使用。目前已有307人学习下载。报告中重点展示了CompressionCodec、GzipCodec和IOUtils.copyBytes等API的实际用法,并附有压缩工具类的初始化、压缩输出流创建等关键步骤说明,对于想掌握HDFS数据本地化压缩处理、完成类似实验任务或撰写实验报告的人来说,是一份有直接借鉴价值的参考材料。
1. 云计算技术实验报告五:Hadoop IO 到底在考察什么
如果只把“云计算技术实验报告五 Hadoop IO”当成一次文件读写演示,那实验做完你也不会留下多少东西;反过来,如果能看到这 8 个英文字母背后是 HDFS 的完整数据通路,一次实验就能把分布式存储的副本放置、数据管道、序列化、压缩和调优串成一条线。多数人在这一步的困惑不是“命令不会敲”,而是看不懂hdfs dfs -put之后发生了什么:数据从客户端内存到 DataNode 磁盘,中间经过几层缓冲、几次网络往返、哪些参数在起作用。这份实验报告本质上是在要求你完整复现并解释 Hadoop 的 I/O 路径,而不是跑通一个 WordCount。本文按实验报告常见的五步骤展开:先建立 HDFS 写入链路的心智模型,再分别拆数据写、数据读、序列化与压缩、I/O 调优与验证,最后落在实验课自己动手验证的关键技巧上。适合正在做《云计算技术》课程设计、需要提交 Hadoop 实验报告,或者准备云计算相关岗位面试的人。
2. HDFS 写入链路:从 put 命令到三副本落盘
2.1 客户端、NameNode 与 DataNode 三方协作的写入模型
hdfs dfs -put看起来像一次普通文件复制,实际由客户端、NameNode、DataNode 三个角色配合完成。客户端把文件按块切分,默认块大小dfs.blocksize为 128MB;每写一个块,客户端先向 NameNode 发起addBlock请求,NameNode 根据副本放置策略返回一组 DataNode 地址,客户端再与这些 DataNode 建立管道(pipeline)执行写入。这个模型决定了 HDFS 写入的“一次写入、多次读取”语义——文件一旦关闭就不能修改,适合分析型负载,不适合随机写。
副本放置策略是理解写入链路的第一关。默认策略dfs.replication=3时,第一个副本放在客户端所在节点;如果客户端在集群外,则随机挑一个负载较低的 DataNode;第二个副本放在与第一个不同机架的节点;第三个副本放在与第二个相同机架的另一节点。这个“机架感知”写法是为了平衡容错(机架级故障不至于丢数据)与写带宽(跨机架网络流量有限)。实验报告里如果你只搭了伪分布式,那么三个副本实际上落在一个节点的三块不同磁盘目录里,这一点要在报告中说明,否则会让人觉得你不清楚副本与节点的区别。
2.2 数据管道与 ack 机制:每 512 字节要经过一次校验
管道建立后,客户端按dfs.client-write-packet-size(默认 64KB)打包数据,包内又按 512 字节做 CRC32 校验。数据包按“客户端 → DataNode1 → DataNode2 → DataNode3”逐级传递,每个 DataNode 收到包后先落盘再转发给下游,然后以相反方向返回 ack。只有客户端收到管道中所有 DataNode 的成功 ack,这个数据包才算写成功。这段逻辑你应该能复述清楚:HDFS 不做写入后的异步复制,而是用同步管道保证副本之间的一致性,代价是写入延迟被拉高,但读取时任意副本都是完整可用的。
用命令验证管道写入状态时,最直接的方法是看 DataNode 日志和系统网络连接:
# 在 DataNode 节点上查看与客户端或相邻 DataNode 的连接(伪分布式时是本机回环) ss -tnp | grep java # 实时跟踪 DataNode 日志,观察 block 接收和 ack 返回 tail -f $HADOOP_HOME/logs/hadoop-hadoop-datanode-*.log | grep -E "Receiving|writeBlock|ack"日志中如果频繁出现Timeout waiting for ack或Packet ack timeout,说明管道中某个 DataNode 写入慢或网络抖动,客户端会触发管道重建,把故障节点剔除后重建剩余副本管道。这个行为在实验报告里可以作为“故障处理”小节写:HDFS 的写入不是简单失败重试,而是“定位故障节点 → 剔除 → 重建管道 → 补充副本”。
2.3 写入链路中的核心参数速查与实测建议
伪分布式做实验时,以下参数和默认值会直接影响实测表现,报告里建议用表格列出并说明调整后的效果:
| 参数名 | 默认值 | 作用 | 实验建议 |
|---|---|---|---|
dfs.blocksize | 128MB | 文件块大小 | 调成 1MB 便于观察 block 分布 |
dfs.replication | 3 | 副本数 | 伪分布式调成 1 避免报错 |
dfs.client-write-packet-size | 64KB | 写入包大小 | 调大可提升吞吐,调小可降低延迟 |
dfs.client.block.write.replace-datanode-on-failure.policy | DEFAULT | 管道故障处理 | 观察写入失败场景时保留默认值 |
dfs.namenode.handler.count | 10 | NameNode 处理线程数 | 小文件场景调大,避免 RPC 积压 |
实际写入性能与块大小的关系,可以用一条时间线描述:4KB 小文件配 128MB 块,每个文件一个块,每写一个文件都要与 NameNode 做一次 RPC;而 1GB 文件配 128MB 块,只需要 8 次块申请。因此,块大小与文件大小匹配时吞吐最好,这也是为什么实验里-put一个几 GB 的测试文件比大量小文件省时间。
3. 读路径与本地性优化:为什么 HDFS 读比写快
3.1 从 DFSInputStream 到短路读的完整读链路
HDFS 读取时,客户端创建DFSInputStream,先从 NameNode 拿文件块与副本位置的映射,然后选择“最近”的副本建立 TCP 连接读取。这里的“最近”由dfs.client.use.datanode.hostname与网络拓扑共同决定,客户端与 DataNode 在同一节点时优先读本地副本,避免网络传输。读路径上数据不经过 NameNode,NameNode 只负责元数据与块定位,所以读吞吐可以接近本机磁盘上限。
实验报告里最常见的问题是“为什么读比写快很多”。原因有三个层面。一,写入要同步复制三份,至少产生两次跨节点传输;读只需要一份数据。二,写入要返回 ack,每个数据包都有等待延迟,而读是流水线式拉取,一个连接上连续发请求。三,HDFS 读取命中本地副本时,直接走本地文件系统读,不占网络。验证方法很直接:
# 对比读写同一文件的耗时(在集群内某节点执行) time hdfs dfs -put /data/testfile.bin /tmp/testfile.bin time hdfs dfs -cat /tmp/testfile.bin > /dev/null如果写耗时是读的 2 到 3 倍,说明副本同步在起作用。若相差不大,检查是否副本数被改成了 1。
3.2 短路读:让客户端绕过 TCP 直接读 DataNode 文件
标准读路径上,即使客户端在 DataNode 本机,也要通过 TCP 把数据从 DataNode 的 50010 端口取回来。短路读(Short-Circuit Read)允许客户端直接以文件描述符映射方式读 DataNode 上的块文件,省掉一次本机 TCP 回环。这是对“本地读还是走了网络栈”的优化,实验环境里的效果可能不明显,但面试会问。
开启短路读需要配置两个地方。一是 DataNode 的dfs.domain.socket.path指定 socket 文件路径,且目录权限要满足 DataNode 与客户端都可访问;二是dfs.client.read.shortcircuit=true。伪分布式单节点下配置如下:
<property> <name>dfs.client.read.shortcircuit</name> <value>true</value> </property> <property> <name>dfs.domain.socket.path</name> <value>/var/lib/hadoop-hdfs/dn_socket</value> </property> <property> <name>dfs.client.read.shortcircuit.skip.checksum</name> <value>false</value> </property>配置后建议在核心代码里执行一次测试小文件读取,再从 DataNode 日志中确认短路读是否生效:日志出现Setting up short-circuit read即成功。注意如果实验报告里只验证了hdfs dfs -cat,短路读可能不触发,因为 HDFS Shell 客户端默认走完整读路径,用 Java API 的FSDataInputStream才稳定触发。
3.3 小文件读放大效应对伪分布式实验的影响
小文件问题在实验里被低估。每个文件、每块副本在 NameNode 内存里对应一条元数据记录,默认每条约 150 字节;一万个 1KB 小文件在 NameNode 里要占约 4MB 堆内存,而读这些小文件时磁盘寻道开销远大于传输开销。实验报告建议补一个“小文件读放大”测试:生成 10000 个 1KB 文件与 10 个 1GB 文件,对比总读取时间。前者常见耗时是后者数倍,原因是每个文件都要发起 RPC、建立连接、等待响应。
伪分布式下 I/O 性能明显下降时,第一步看是不是小文件撑爆了 NameNode 的 RPC 队列。可以查 NameNode 日志里的rpc相关统计,或直接执行:
hdfs dfsadmin -report | grep -E "Configured Capacity|Present Capacity|DFS Used|Total Files"Total Files数量异常大且DFS Used很小,就是小文件问题。缓解手段是har归档或 SequenceFile 合并,这正好引出第四章的序列化与压缩。
4. SequenceFile 与小文件合并:实验里 IO 层面的序列化方案
4.1 为什么 MapReduce 的中间数据需要 SequenceFile
MapReduce 执行过程中,Map 阶段输出写到本地磁盘,Shuffle 阶段再把输出拉取到 Reduce 端。这个中间数据不是纯文本,也不是普通二进制,而是带 key 与 value 类型标识的序列化记录。Hadoop 官方方案是 SequenceFile:一种二进制键值容器,持久化 key 的类名与 value 的字节流,读的时候能拿到类型信息。实验中用 SequenceFile 合并小文件,既能减少 NameNode 元数据条目,又能避免 Map 端大量小文件导致的 InputSplit 膨胀。
SequenceFile 的写入逻辑很简单,注意Writer关闭后不能继续追加:
Configuration conf = new Configuration(); Path path = new Path("/tmp/seq/part-00000.seq"); SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{ SequenceFile.Writer.file(path), SequenceFile.Writer.keyClass(Text.class), SequenceFile.Writer.valueClass(BytesWritable.class), SequenceFile.Writer.compression(SequenceFile.CompressionType.RECORD, new DefaultCodec()) }; SequenceFile.Writer writer = SequenceFile.createWriter(conf, opts); Text key = new Text(); BytesWritable value = new BytesWritable(); key.set("file-001"); value.set(bytes, 0, bytes.length); writer.append(key, value); writer.close();代码中每一个写法都有对应考点:CompressionType.RECORD表示每条记录独立压缩,随机读时能定位到某条记录直接解压;若换成CompressionType.BLOCK则压缩比更高,但读取时要把整个块载入内存解压。DefaultCodec使用 zlib,压缩率中等偏上、CPU 占用高;实验里如果数据是日志文本,GzipCodec更合适,若是压测环境图吞吐,SnappyCodec更好。
4.2 三种压缩编解码器的选型对比表
Hadoop 压缩选型看两个指标:压缩比与压缩速度,二者不可兼得。常见编解码器对比如下,实验报告可以直接引用:
| 编解码器 | 压缩比 | 压缩速度 | 是否可切分 | 适用场景 |
|---|---|---|---|---|
| DefaultCodec(zlib) | 高 | 慢 | 否 | 冷数据存储 |
| GzipCodec | 高 | 慢 | 否 | 日志归档 |
| BZip2Codec | 最高 | 最慢 | 是 | 极高压缩率 |
| SnappyCodec | 中 | 极快 | 否 | 中间数据、实时查询 |
| Lz4Codec | 中 | 极快 | 否 | 高频写入 |
“是否可切分”这一项在 MapReduce 里直接决定并行度。不可切分的压缩文件放在 HDFS 上,一个文件只能由一个 Map 任务处理,即使块被拆成两半也无解。因此实验里如果用 Gzip 归档大日志,Map 阶段并行度等于文件数而不是块数;想兼顾压缩与并行,要么用 BZip2,要么改用 LZO(需要额外安装 native 库)。
4.3 用 SequenceFile 合并小文件的完整 Java 代码
真实场景中,把 HDFS 上一个目录下的全部小文件合并成一个 SequenceFile,代码可复现如下:
public class SmallFileToSeq { public static void main(String[] args) throws IOException { Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(conf); Path srcDir = new Path(args[0]); Path seqFile = new Path(args[1]); SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{ SequenceFile.Writer.file(seqFile), SequenceFile.Writer.keyClass(Text.class), SequenceFile.Writer.valueClass(BytesWritable.class), SequenceFile.Writer.compression(SequenceFile.CompressionType.BLOCK, new GzipCodec()) }; SequenceFile.Writer writer = SequenceFile.createWriter(conf, opts); FileStatus[] files = fs.listStatus(srcDir); Text key = new Text(); BytesWritable value = new BytesWritable(); for (FileStatus file : files) { if (file.isDirectory()) continue; try (FSDataInputStream in = fs.open(file.getPath())) { byte[] buffer = new byte[(int) file.getLen()]; in.readFully(buffer); key.set(file.getPath().getName()); value.set(buffer, 0, buffer.length); writer.append(key, value); } } writer.close(); System.out.println("merged files: " + files.length); } }in.readFully保证一次循环把整个文件读进内存,适合小文件;如果文件平均超过 10MB,就不要用这种方式,改成IOUtils.copyBytes分块读。压缩选 BLOCK 而非 RECORD,是因为这里读取要扫描全部记录,BLOCK 压缩比更高,Gzip 对文本日志能压到原体积的四分之一以下。
4.4 读取 SequenceFile 并验证合并结果
合并完成后必须验证内容与原始文件一致,这一步是实验报告里的依据:
SequenceFile.Reader reader = new SequenceFile.Reader(conf, SequenceFile.Reader.file(new Path(args[1]))); Text key = new Text(); BytesWritable value = new BytesWritable(); int count = 0; while (reader.next(key, value)) { if (count < 3) { System.out.println("key=" + key + ", len=" + value.getLength()); } count++; } reader.close(); System.out.println("total records: " + count);验证时注意两点:BytesWritable.getLength()才是真实长度,getBytes()返回的数组可能大于实际长度,拷贝时不能直接Arrays.copyOf(value.getBytes(), value.getLength())之外的做法容易读出脏尾部字节。另外,若是用hdfs dfs -text查看 SequenceFile,需要保证 key/value 类在 classpath 中,否则输出二进制乱码。
5. Hadoop IO 调优实战:吞吐量、校验与实验验证技巧
5.1 三组必调参数:缓冲、并发与校验开关
实验做到这一步,报告里应该有一节“性能调优”。HDFS 客户端 I/O 表现受三处影响。缓冲上,dfs.client.read.prefetch.size(HDFS 2.x 后由dfs.client.read.prefetch.size控制,默认 4MB)决定每次读取预取数据量,跑大文件顺序读时建议至少 8MB;dfs.client-write-packet-size控制写入包大小,64KB 与 128KB 之间通常有一个吞吐拐点。并发上,客户端到 DataNode 的读连接由dfs.client.mmap.enabled和dfs.client.mmap.cache.size影响,mmap 开启后读小文件省去用户态拷贝。校验上,dfs.client.read.shortcircuit.skip.checksum若开启可提升吞吐,但会失去数据完整性保护,生产环境不建议开启;实验里想对比校验开销,可以分别在 true 与 false 下测同一文件读取时间。
# 读取吞吐粗略测试:读取 + 丢弃 hdfs dfs -cat /tmp/largefile.bin > /dev/null5.2 用日志和 dfsadmin 验证调优效果
调优不能只看“感觉变快了”,要拿数据说话。实验环境可用以下链条验证。第一,从写入侧观察吞吐,使用hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-*-tests.jar TestDFSIO -write -nrFiles 4 -fileSize 128MB,这是 Hadoop 自带压测工具,输出会给出吞吐与平均 IO 速率。第二,从系统侧观察磁盘队列,用iostat -x 1看%util与await,如果%util长期 90% 以上磁盘已是瓶颈,调参数徒劳。第三,从 HDFS 侧确认块分布均衡,hdfs fsck /tmp/largefile.bin -files -blocks -locations能列出每个块与副本位置,能直观看到 128MB 块与文件的映射。
5.3 伪分布式环境下最容易踩的三个 IO 坑
伪分布式与环境变量相关的坑最隐蔽。第一,io.file.buffer.size默认 4096,用默认值跑大文件读写,磁盘 IO 次数被放大数倍,改为 131072 效果立竿见影。第二,Hadoop 3.x 移植到新机器上常见Error: Could not find or load main class,这通常是YARN相关 classpath 没配全,排查时先hadoop classpath确认输出,再决定去留,不必急着重装。第三,DataNode 与 NameNode 的dfs.datanode.data.dir落在同一块磁盘的两个分区,看起来磁盘容量变大了,实际写三副本时三个副本竞争同一块磁盘的 IO 队列,写放大不降反升;实验报告如果做了多目录配置,建议声明“多目录不等于多磁盘”。
5.4 实验结论部分可以这样写
实验报告收尾时不要只写“完成文件上传下载”,建议给出一个“HDFS IO 行为对照表”:文件大小从 1MB 到 512MB,测写时间、读时间和 NameNode RPC 次数(hdfs dfsadmin -printTopology与 Namenode 日志做参考);序列化与压缩对比:原始文本、Gzip 压缩前后体积、SequenceFile 记录数、压缩比;调优前后对照:默认配置与调优后io.file.buffer.size=131072、dfs.blocksize=64MB下的吞吐差异。用几组数字写出实测结论,比如“128MB 块下 512MB 文件写入耗时是 64MB 块下的 1.2 倍,说明块大小并非越大越好”或“Snappy 压缩在写入场景下比 Gzip 快 3 倍,但文件体积增加 20%”。这比“通过本次实验掌握了 HDFS”有价值得多。
本文还有配套的精品资源,点击获取