简介:本资源是一份面向高校云计算课程学习者与大数据初学者的实验报告,聚焦Hadoop生态中SequenceFile的核心应用,解决多小文件高效封装与键值查询的实际问题。报告完整覆盖随机生成100+(整数,字符串)文本文件、封装为压缩SequenceFile、以及三种典型查询场景(按文件名提取、按key全局检索、按文件+key精准定位)的Java实现与过程分析,适合作为《云计算技术》课程实验六的参考范例与代码实践模板。资源为单个PDF文件,大小1.39MB,内容包含实验目的、环境配置(Linux+Eclipse+MapReduce项目)、详细步骤说明、关键代码片段(含SequenceFile.Reader读取、ReflectionUtils实例化、Scanner交互式查询逻辑)及结果展示,结构清晰便于对照复现。目前已有322人学习下载,读者可直接获取规范的实验文档框架、可运行的查询逻辑实现思路、以及Hadoop序列化文件操作的典型调试要点。
1. SequenceFile 封装百个小文件:为什么 Hadoop 生产环境宁可多写 200 行代码,也不让小文件裸奔?
你有没有遇到过这样的场景:爬虫每分钟吐出 300 个 KB 级日志文件,HDFS 上瞬间堆满 5 万+ 小文件?NameNode 内存暴涨、MapReduce 任务启动慢得像在加载 Windows 95、YARN 调度器频繁 GC——这不是玄学,是小文件病。本实验不是教你怎么“跑通一个 demo”,而是用最朴素的 Java + Hadoop Client API,在本地 Eclipse 环境里,亲手把 100+ 个散落的小文本文件,打包成一个带压缩的 SequenceFile,并实现三种生产级查询能力:按原始文件名提取内容、按 key 全局检索、按“文件名+key”精准定位。它不依赖 YARN 或集群,但每一步都踩在 Hadoop 文件系统设计的底层逻辑上:SequenceFile 的 sync marker、key/value 类型反射实例化、Text 对象的序列化边界、路径字符串的精确匹配。适合正在头歌实践平台搭 Hadoop 环境、用 Eclipse 跑 MapReduce 作业、被小文件卡住进度的云计算课学生,也适合想快速验证 SequenceFile 封装效果的运维同学——你不需要会写 MapReduce,但必须懂FileSystem.getLocal(conf)和IOUtils.closeStream()为什么不能少。
2. 从零生成 100+ 小文件:随机数据构造与路径规范
2.1 为什么必须用整数+字符串作为 (key, value)?
SequenceFile 是二进制键值对容器,不认“文本格式”,只认序列化后的字节流。若用纯字符串做 key(如"file001"),后续按整数 key 查询时需强制类型转换,极易抛NumberFormatException;若用IntWritable做 key,value 却用Text,则ReflectionUtils.newInstance(reader.getKeyClass(), conf)才能正确实例化——这是 Hadoop 序列化框架的硬约束。实验要求(整数, 字符串),本质是在模拟真实日志场景:key 是事件时间戳或用户 ID(整型),value 是 JSON 日志体(字符串)。我们不用IntWritable而用Text存整数,是因为实验代码中 key 实际存储为"filename\t12345"形式(见后文解析),所以 key 类型必须统一为Text,否则reader.getKeyClass()返回class org.apache.hadoop.io.Text,你却试图new IntWritable(),直接 ClassCastException。
2.2 生成 100+ 文件的 Java 实现(含路径陷阱)
关键点:所有文件必须存入ex6/files/目录下,且文件名不含路径分隔符。实验报告里那句“第一种查询没结果,因为输入文件名没带路径”就是血泪教训。下面代码生成的每个文件,路径是ex6/files/file_001.txt,但文件名(即file_001.txt)才是后续 SequenceFile 中 key 的一部分:
// GenerateFiles.java import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import java.io.*; import java.util.Random; public class GenerateFiles { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); FileSystem fs = FileSystem.getLocal(conf); Path baseDir = new Path("ex6/files"); fs.mkdirs(baseDir); // 确保目录存在 Random rand = new Random(); for (int i = 1; i <= 100; i++) { String filename = String.format("file_%03d.txt", i); Path filePath = new Path(baseDir, filename); BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(fs.create(filePath), "UTF-8") ); // 每个文件写 5~10 行,每行格式:整数\t随机字符串 int lines = 5 + rand.nextInt(6); for (int j = 0; j < lines; j++) { int key = rand.nextInt(10000); String value = "log_" + rand.nextInt(1000000) + "_data"; writer.write(key + "\t" + value); writer.newLine(); } writer.close(); System.out.println("Generated: " + filename); } } }提示:
fs.create(filePath)自动创建父目录,但baseDir必须显式fs.mkdirs(),否则某些 Hadoop 版本会报FileNotFoundException。filename变量值仅为file_001.txt,绝不能是ex6/files/file_001.txt——这是 SequenceFile 封装时 key 构造的源头。
2.3 文件内容格式必须严格为key\tvalue
SequenceFile 封装时,key 是Text类型,其内容为"ex6/files/file_001.txt\t12345"(注意:路径前缀ex6/files/是硬编码进 key 的!)。value 是该行原始字符串"log_789_data"。这意味着:
- 每个
.txt文件内部,每行必须是整数\t字符串格式(\t分隔); - 后续查询时,
str1[0].equals(str2[0])比较的是ex6/files/file_001.txtvsex6/files/file_001.txt,不是file_001.txtvsfile_001.txt; - 若生成文件时漏写
\t,或 value 包含\t,keytmp.split("\t")会数组越界,str2[1]报ArrayIndexOutOfBoundsException。
2.4 验证生成结果:检查文件数量与内容
运行GenerateFiles后,在项目根目录执行:
ls -l ex6/files/ | wc -l # 输出应 ≥101(含 .gitignore 等隐藏文件,实际 .txt 文件数应为 100) head -n 2 ex6/files/file_001.txt # 输出示例: # 4567 log_123456_data # 8901 log_789012_data若head显示无\t或数字后跟空格,说明生成逻辑有误,必须修正writer.write(key + "\t" + value)。
3. SequenceFile 封装:压缩格式选型与二进制写入
3.1 为什么 SequenceFile 比普通 ZIP 更适合 Hadoop?
ZIP 是归档格式,解压需全量读取;SequenceFile 是 Hadoop 原生序列化格式,支持:
- Splittable:可被 MapReduce 切片并行处理;
- Sync marker:每 2000 行插入同步点,断点续读;
- Native compression:Gzip/Deflate/BZip2 压缩后仍支持 seek(跳转到指定 key);
- Type-aware:key/value 类型在文件头声明,无需外部 schema。
本实验用SequenceFile.Writer,而非FileOutputStream,正是为了获得这些能力。
3.2 封装代码详解(含压缩参数控制)
实验代码未给出封装部分,我们补全——这是整个流程最易翻车的环节:
// SequenceFileWriter.java import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.io.SequenceFile; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Writable; import org.apache.hadoop.util.ReflectionUtils; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class SequenceFileWriter { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); // 启用 Gzip 压缩(实验要求“压缩格式任意”,Gzip 平衡速度与压缩率) conf.setBoolean("io.seqfile.compress", true); conf.set("io.seqfile.compression.type", "BLOCK"); // BLOCK 比 RECORD 压缩率高 conf.set("io.seqfile.compression.codec", "org.apache.hadoop.io.compress.GzipCodec"); FileSystem fs = FileSystem.getLocal(conf); Path seqPath = new Path("ex6/fi"); // 注意:是 ex6/fi,不是 ex6/fi.seq Path inputDir = new Path("ex6/files"); // 创建 Writer,指定 key/value 类型、压缩配置 SequenceFile.Writer writer = SequenceFile.createWriter( fs, conf, seqPath, Text.class, Text.class, // key 和 value 都是 Text SequenceFile.CompressionType.BLOCK, new org.apache.hadoop.io.compress.GzipCodec() ); // 遍历 ex6/files 下所有 .txt 文件 FileStatus[] files = fs.listStatus(inputDir); for (FileStatus file : files) { if (!file.getPath().getName().endsWith(".txt")) continue; String filename = file.getPath().toString(); // 得到完整路径 ex6/files/file_001.txt BufferedReader reader = new BufferedReader( new InputStreamReader(fs.open(file.getPath()), "UTF-8") ); String line; while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) continue; // 构造 key: "ex6/files/file_001.txt\t12345" String[] parts = line.split("\t", 2); // 仅分割第一个 \t,防 value 含 \t if (parts.length < 2) continue; String keyStr = filename + "\t" + parts[0]; // 关键!路径+key Text key = new Text(keyStr); Text value = new Text(parts[1]); writer.append(key, value); } reader.close(); } writer.close(); System.out.println("SequenceFile written to: " + seqPath); } }参数说明:
CompressionType.BLOCK:对连续 block 压缩,比RECORD(每 record 单独压缩)节省 15~20% 空间;GzipCodec:Hadoop 自带,无需额外 JAR;若换BZip2Codec,需确认hadoop-lzo是否在 classpath;filename.toString()返回ex6/files/file_001.txt,这是后续查询时str1[0]必须匹配的字符串——路径一致性是查询成功的前提。
3.3 封装后文件结构验证
运行后检查ex6/fi:
hadoop fs -cat ex6/fi | head -n 5 # 会失败,因是二进制 xxd -l 100 ex6/fi | head -n 5 # 查看十六进制头 # 应看到类似:00000000: 7365 7175 656e 6365 6669 6c65 0000 0000 sequencefile....更可靠的方式是用hadoop fs -text(仅当未压缩或用 RECORD 压缩时有效):
hadoop fs -D hadoop.tmp.dir=/tmp -text ex6/fi 2>/dev/null | head -n 3 # 若输出为空,说明是 BLOCK 压缩,需用 SequenceFile.Reader 读取3.4 常见问题排查:封装失败的四大坑
| 现象 | 原因 | 解决 |
|---|---|---|
java.lang.NoClassDefFoundError: org/apache/hadoop/io/compress/GzipCodec | 缺少hadoop-common.jar或hadoop-mapreduce-client-core.jar | 在 Eclipse 中右键项目 → Properties → Java Build Path → Libraries → Add External JARs,添加$HADOOP_HOME/share/hadoop/common/*.jar和$HADOOP_HOME/share/hadoop/mapreduce/*.jar |
java.io.IOException: File exists | ex6/fi已存在且未设置fs.delete(seqPath, true) | 在createWriter前加fs.delete(seqPath, true) |
| 封装后文件大小为 0 | writer.append()未调用,或line.split("\t",2)失败导致跳过所有行 | 在while循环内加System.out.println("Writing: " + keyStr),确认是否进入循环 |
查询时找不到文件,但ex6/files/下确实存在 | filename.toString()返回file_001.txt(无路径),而代码中拼接了ex6/files/ | 用file.getPath().toString()而非file.getPath().getName(),后者只返回文件名 |
4. 三种查询实现:从控制台输入到精准定位
4.1 查询逻辑总览:统一 Reader,分支处理
实验代码Query()方法核心是复用SequenceFile.Reader,但根据输入参数个数(0/1/2 个空格)走不同分支。所有分支共享同一套 key 解析逻辑:key.toString().split("\t")得到[full_path, integer_key],value.toString()是原始 value。这是 SequenceFile 设计的精妙之处——key 不是单纯 ID,而是携带上下文的复合标识。
4.2 查询 1:按文件名提取全部内容(file_001.txt→ 本地文件)
此功能模拟“下载原始日志”。关键点:输入file_001.txt,但 key 中存的是ex6/files/file_001.txt,必须补全路径才能匹配。实验代码第 49 行str1[0]="ex6/files/"+str1[0]正是为此:
// Query1: 输入 file_001.txt,输出到 ./output/file_001.txt if (f == false && input.startsWith("file")) { // 简化判断,实际用 input.charAt(0)=='f' System.out.print("请输入指定的路径名字(如 ./output):"); String outputDir = new Scanner(System.in).nextLine(); Path outputPath = new Path(outputDir, input); FileOutputStream fout = new FileOutputStream(outputPath.toString()); PrintStream pStream = new PrintStream(new BufferedOutputStream(fout)); SequenceFile.Reader reader = new SequenceFile.Reader(fs, seqPath, conf); Text key = (Text) ReflectionUtils.newInstance(reader.getKeyClass(), conf); Text value = (Text) ReflectionUtils.newInstance(reader.getValueClass(), conf); String targetPath = "ex6/files/" + input; // 补全路径! while (reader.next(key, value)) { String[] keyParts = key.toString().split("\t", 2); if (keyParts.length == 2 && keyParts[0].equals(targetPath)) { pStream.println(keyParts[1] + "\t" + value.toString()); // 输出 key_value 对 } } pStream.close(); fout.close(); IOUtils.closeStream(reader); }注意:
pStream.println(keyParts[1] + "\t" + value.toString())输出的是12345\tlog_789_data,还原了原始文件格式,方便下游工具处理。
4.3 查询 2:按整数 key 全局检索(12345→ 所有匹配行及来源文件)
这是典型的“事件溯源”场景。代码第 102 行if(str2[1].equals(input))中str2[1]即 key 的整数值部分:
// Query2: 输入 12345,输出所有 value 及其文件名 } else if (!input.startsWith("file") && input.matches("\\d+")) { SequenceFile.Reader reader = new SequenceFile.Reader(fs, seqPath, conf); Text key = (Text) ReflectionUtils.newInstance(reader.getKeyClass(), conf); Text value = (Text) ReflectionUtils.newInstance(reader.getValueClass(), conf); System.out.printf("%-30s %-25s\n", "Value", "Source File"); System.out.println("-".repeat(55)); while (reader.next(key, value)) { String[] keyParts = key.toString().split("\t", 2); if (keyParts.length == 2 && keyParts[1].equals(input)) { // keyParts[0] 是 full_path,提取文件名:ex6/files/file_001.txt → file_001.txt String fileName = keyParts[0].substring(keyParts[0].lastIndexOf("/") + 1); System.out.printf("%-30s %-25s\n", value.toString(), fileName); } } IOUtils.closeStream(reader); }技巧:
keyParts[0].substring(...)提取纯文件名,避免输出冗长路径,提升可读性。
4.4 查询 3:按“文件名+key”精准定位(file_001.txt 12345→ 单行 value)
这是最严格的条件查询。实验代码第 55 行if(str1[0].equals(str2[0]) && str1[1].equals(str2[1]))直接比对,但str1[0]已补ex6/files/,str2[0]是 key 的第一段,天然一致:
// Query3: 输入 "file_001.txt 12345" String[] parts = input.split(" ", 2); if (parts.length == 2) { String targetFile = "ex6/files/" + parts[0]; String targetKey = parts[1]; SequenceFile.Reader reader = new SequenceFile.Reader(fs, seqPath, conf); Text key = (Text) ReflectionUtils.newInstance(reader.getKeyClass(), conf); Text value = (Text) ReflectionUtils.newInstance(reader.getValueClass(), conf); boolean found = false; while (reader.next(key, value)) { String[] keyParts = key.toString().split("\t", 2); if (keyParts.length == 2 && keyParts[0].equals(targetFile) && keyParts[1].equals(targetKey)) { System.out.println("Found: " + value.toString()); found = true; break; // 精准定位,找到即停 } } if (!found) System.out.println("Not found."); IOUtils.closeStream(reader); }性能提示:
break很关键——SequenceFile 无索引,全量扫描,不提前退出会遍历整个文件。
4.5 避坑:查询失败的五大血泪经验
| 现象 | 原因 | 解决 |
|---|---|---|
| 查询 1 总是无输出 | 控制台输入file_001.txt,但代码str1[0]="ex6/files/"+str1[0]拼成ex6/files/file_001.txt,而 key 中存的是ex6/files/file_001.txt——看似一致,实则ex6/files/是相对路径,若当前工作目录不是项目根目录,FileSystem.getLocal(conf)会解析错 | 统一用绝对路径:new Path("/full/path/to/ex6/files/file_001.txt"),或确保 Eclipse 运行配置中Working directory设为项目根目录 |
查询 2 输出文件名带ex6/files/前缀 | str2[0]直接打印,未截取文件名 | 用str2[0].substring(str2[0].lastIndexOf("/")+1)提取 |
查询 3 输入file_001.txt 12345后程序卡死 | input.split(" ")未限制长度,若 value 含空格(如"user login success"),str1[1]取到login而非12345 | 改用input.split(" ", 2),确保最多切两段 |
所有查询都报java.lang.NullPointerExceptionatreader.next(key, value) | reader初始化失败,但try块外未检查reader != null | 在while前加if (reader == null) throw new RuntimeException("Reader not initialized"); |
查询结果乱码(中文显示为?) | PrintStream未指定编码,FileOutputStream默认平台编码 | new PrintStream(new BufferedOutputStream(fout), true, "UTF-8") |
5. 调试与验证:用命令行工具反向检验 SequenceFile 内容
5.1 用hadoop fs -text查看未压缩内容(快速验证)
若封装时用了CompressionType.RECORD,可直接查看:
hadoop fs -D io.seqfile.compression.codec=org.apache.hadoop.io.compress.DefaultCodec \ -text ex6/fi 2>/dev/null | head -n 10输出应类似:
ex6/files/file_001.txt 12345 log_789_data ex6/files/file_001.txt 67890 log_123_data ex6/files/file_002.txt 23456 log_456_data注意:-text对 BLOCK 压缩无效,此时必须写 Java Reader。
5.2 编写独立 Reader 验证工具(绕过 Eclipse 依赖)
新建VerifySequenceFile.java,不依赖 Eclipse 项目结构,只用 Hadoop JAR:
// VerifySequenceFile.java (编译后可独立运行) import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.io.SequenceFile; import org.apache.hadoop.io.Text; import java.net.URI; public class VerifySequenceFile { public static void main(String[] args) throws Exception { if (args.length == 0) { System.err.println("Usage: java VerifySequenceFile <seqfile-path>"); System.exit(1); } Configuration conf = new Configuration(); // 强制使用本地文件系统 conf.set("fs.defaultFS", "file:///"); FileSystem fs = FileSystem.get(URI.create(args[0]), conf); SequenceFile.Reader reader = new SequenceFile.Reader(fs, new Path(args[0]), conf); Text key = new Text(); Text value = new Text(); long count = 0; while (reader.next(key, value)) { String[] k = key.toString().split("\t", 2); if (k.length == 2) { System.out.printf("[%d] %s -> %s\n", ++count, k[0], k[1]); if (count >= 10) break; // 只看前 10 行 } } System.out.println("Total records: " + count); IOUtils.closeStream(reader); } }编译运行:
javac -cp "$(hadoop classpath)" VerifySequenceFile.java java -cp ".:$(hadoop classpath)" VerifySequenceFile ex6/fi若输出ex6/files/file_001.txt 12345,证明封装成功;若报ClassNotFoundException,说明 classpath 未包含 Hadoop JAR。
5.3 文件大小对比:量化 SequenceFile 优势
生成 100 个文件后,执行:
du -sh ex6/files/ # 原始小文件总大小 du -sh ex6/fi # SequenceFile 大小 # 示例结果: # 1.2M ex6/files/ # 890K ex6/fi # Gzip 压缩后节省 26%再测试hadoop fs -stat "%o" ex6/fi查看块大小,确认是否 splittable。
5.4 查询响应时间基准测试
用time命令测查询 2(全局 key 检索):
time java -cp ".:$(hadoop classpath)" YourQueryClass <<< "12345" # 对比:直接 grep 100 个文件 time grep -r "12345" ex6/files/ | wc -l通常 SequenceFile 全扫比grep -r快 3~5 倍(因免磁盘寻道),但不如数据库索引——这正是它定位:批处理场景下的小文件聚合方案,非实时 OLTP。
6. 生产环境迁移指南:从本地 Eclipse 到 HDFS 集群
6.1 修改FileSystem为远程 HDFS
本地测试用FileSystem.getLocal(conf),生产必须切换为 HDFS URI:
// 替换这一行 // FileSystem fs = FileSystem.getLocal(conf); Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://namenode:9000"); // HDFS 地址 FileSystem fs = FileSystem.get(conf);同时确保core-site.xml和hdfs-site.xml在 classpath 中,或用conf.addResource(new Path("/etc/hadoop/conf/core-site.xml"))。
6.2 SequenceFile 路径必须为 HDFS 路径
ex6/fi在本地是相对路径,HDFS 中需用绝对路径:
Path seqPath = new Path("hdfs://namenode:9000/user/yourname/ex6/fi"); // 或简写(依赖 fs.defaultFS) Path seqPath = new Path("/user/yourname/ex6/fi");6.3 压缩格式选型决策表
| 压缩格式 | CPU 开销 | 压缩率 | Splittable | 适用场景 |
|---|---|---|---|---|
NONE | 无 | 0% | 否 | 调试、极速写入 |
RECORD(Snappy) | 低 | 20~30% | 否 | 需快速随机读单 record |
BLOCK(Gzip) | 中 | 60~70% | 是 | 推荐:平衡压缩与并行性 |
BLOCK(BZip2) | 高 | 75~85% | 是 | 存储成本敏感,CPU 充裕 |
经验:Gzip BLOCK 是 Hadoop 生产默认,Snappy RECORD 用于 Impala,BZip2 仅存档。
6.4 避免路径硬编码的工程化改造
实验代码中ex6/files/处处硬编码,生产必须抽取为配置项:
// config.properties input.dir=hdfs://namenode:9000/user/data/raw output.seqfile=hdfs://namenode:9000/user/data/seqfiles compression.type=BLOCK compression.codec=org.apache.hadoop.io.compress.GzipCodecJava 中用conf.setStrings("input.dir", props.getProperty("input.dir"))加载。
6.5 最后一道防线:封装前校验小文件完整性
在SequenceFileWriter开头加入 MD5 校验(防生成中断):
// 计算每个小文件的 MD5,存入 manifest.txt FileStatus[] files = fs.listStatus(inputDir); List<String> manifest = new ArrayList<>(); for (FileStatus f : files) { if (f.getPath().getName().endsWith(".txt")) { String md5 = DigestUtils.md5Hex(fs.open(f.getPath())); manifest.add(f.getPath().getName() + "\t" + md5); } } // 写入 manifest.txt,供后续验证 FSDataOutputStream out = fs.create(new Path(inputDir, "manifest.txt")); for (String line : manifest) out.writeBytes(line + "\n"); out.close();封装完成后,用相同逻辑重算 MD5 并比对manifest.txt,确保无文件损坏。
从那以后我每次写 SequenceFile 封装脚本,都强制走一遍VerifySequenceFile+manifest校验,哪怕多花 30 秒——因为线上环境一旦 SequenceFile 损坏,MapReduce 任务会静默失败,日志里只有一行IOException: Premature EOF,debug 成本远超预防成本。希望帮到你。
本文还有配套的精品资源,点击获取