Apache Hadoop — 在一个 Pod 里搭起并运维 HDFS 与 YARN
把 2,000 个小文件打包成归档,并测量 NameNode 对象数
目标
把 2,000 个传感器 CSV(每个文件只有几百字节)上传到 HDFS,测出 NameNode 必须持有的对象(文件、目录、块)有多少个,再用 Hadoop Archive(HAR)打包,看这个数量减少了多少。打包之后,还要体验通过 har:// 仍然能逐个读取原来的文件,以及用 distcp 整体迁移归档。
为什么重要
NameNode 把每个文件、目录和块都当作 Java 对象放在堆里。无论文件是 1 字节还是 1GB,对象数量都一样。所以 HDFS 的极限大多不在磁盘,而是在 NameNode 的堆,几百万个小文件几乎不占磁盘,却会最先压垮 NameNode。从 MapReduce 和 Spark 的角度看,小文件也会因为每个文件都要附带任务或打开的开销而变慢。
解决办法是少生成文件——写入时就合并后再写(Spark 的 repartition、coalesce,大的容器文件格式),或者把已经产生的文件打包。HAR 属于后一种办法。它把许多文件变成几个大文件(part-*)和索引(_index、_masterindex),以减少 NameNode 的对象数量,读取时由 har:// 文件系统查看索引,按原来的路径找到文件。代价是,归档一旦创建就无法修改(只读)。
这个实验把归档和 distcp 用本地执行器运行,不使用 YARN。两者都是 MapReduce 作业,但运行位置用一项配置就能改变——同一个作业,在笔记本上和在集群上都能运行。
步骤
- 把
/data/small目录(2,000 个文件)上传到 HDFS 的 /user/root/small/raw(用hdfs dfs -put -t 8以多个线程上传会更快)。 - 把 raw 的文件数、目录数和块数,按
{"raw_files": 정수, "raw_dirs": 정수, "raw_blocks": 정수}的格式写入 /root/hdp/small/objects.json(占位符均为整数)。 - 用
hadoop archive -D mapreduce.framework.name=local -archiveName raw.har -p /user/root/small -r 1 raw /user/root/small/archive创建归档。 - 用
hdfs dfs -ls -R har:///user/root/small/archive/raw.har统计归档内文件的数量,以整数写入 /root/hdp/small/har_files.txt。 - 通过
har://路径读取raw/sensor-0042.csv,保存到本地的 /root/hdp/small/sensor-0042.csv。 - 把 raw 和 raw.har 的 NameNode 对象数(文件 + 目录 + 块),按
{"raw_objects": 정수, "har_objects": 정수}的格式写入 /root/hdp/small/compare.json(占位符均为整数)。 - 用
hadoop distcp -D mapreduce.framework.name=local /user/root/small/archive /user/root/backup/small-archive复制归档。 - 在 /root/hdp/small/report.md 中写出
## NameNode 가 치르는 값、## HAR、## 옮기기三个小节。第一节写入第 6 步的raw_objects,第二节写入har_objects。
参考
- 文件和目录数:
hdfs dfs -count <경로>(或 WebHDFS 的GETCONTENTSUMMARY;占位符为路径),块数:hdfs fsck <경로>摘要中的Total blocks (validated)(占位符为路径)。 - HAR 的内部:
_index(每个文件一行——路径、类型、位于哪个 part 的哪个位置、多少字节)、_masterindex(索引的索引)、part-0(内容依次拼接而成)。请用hdfs dfs -cat /user/root/small/archive/raw.har/_index | head查看。 -r 1是归档文件的副本系数(默认 3——只有一个 DataNode,所以会变成副本不足)。- 常见错误:
har://路径中漏掉归档名称;把相对于-p指定的父目录的路径(raw)写成绝对路径;把目录行也计入文件数。 - 官方文档:Hadoop Archives Guide · DistCp Guide · HDFS Architecture · FileSystem Shell — count
上传 2,000 个小文件
把 /data/small 上传到 HDFS 的 /user/root/small/raw。像 hdfs dfs -put -t 8 /data/small /user/root/small/raw 这样用多个线程上传会更快。
单线程上传时,每个文件都要与 NameNode 往返好几次,2,000 个文件要花 10 多分钟。小文件付出成本,凭的不是大小,而是个数——这本身就是小文件问题。
统计 NameNode 持有的对象
用 hdfs dfs -count 和 hdfs fsck 统计 /user/root/small/raw 的文件数、目录数和块数,按 {"raw_files": 정수, "raw_dirs": 정수, "raw_blocks": 정수} 的格式写入 /root/hdp/small/objects.json(占位符均为整数)。
一个文件一个块(因为文件比块小),再加上一个目录。在 NameNode 的堆里,这三样都是对象。块大小虽然是 128MB,但只有几百字节的文件的块并不会占用 128MB,而对象却同样占用一个。
打包成归档
用 hadoop archive -D mapreduce.framework.name=local -archiveName raw.har -p /user/root/small -r 1 raw /user/root/small/archive 创建 /user/root/small/archive/raw.har。
归档是一个 MapReduce 作业。它把输入文件列表分开,把内容拼接到 part-* 里,再把每个文件放在什么位置记到 _index 中。-D mapreduce.framework.name=local 的意思是不使用 YARN,在当前 JVM 里运行。原始文件(raw)保持原样。
通过 har:// 查看内部
统计 hdfs dfs -ls -R har:///user/root/small/archive/raw.har 输出中文件(权限以 - 开头的行)的数量,以整数写入 /root/hdp/small/har_files.txt。
har:// 文件系统读取 _index,呈现出原来的目录树。NameNode 并不知道这 2,000 个文件——归档内的文件不是 NameNode 的对象,而是索引中的一行。
读取归档内的一个文件
把用 hdfs dfs -cat har:///user/root/small/archive/raw.har/raw/sensor-0042.csv 读取到的内容保存到本地的 /root/hdp/small/sensor-0042.csv。
路径是 har://<아카이브 경로>/<-p 기준 상대 경로>(占位符依次为归档路径与以 -p 指定的父目录为基准的相对路径)。har 文件系统通过 _masterindex 和 _index 找到 part 文件中的位置,只读取那部分字节。请用 cmp 与原始文件 /data/small/sensor-0042.csv 对比。
对比对象数量
分别求出 raw 和 raw.har 的 NameNode 对象数(文件数 + 目录数 + 块数),按 {"raw_objects": 정수, "har_objects": 정수} 的格式写入 /root/hdp/small/compare.json(占位符均为整数)。
raw 是 2,000 个文件 + 1 个目录 + 2,000 个块。raw.har 只有一个目录、几个文件(_index、_masterindex、part-0、_SUCCESS)以及它们的块。请算一算缩减到了几分之一。
用 distcp 整体迁移
用 hadoop distcp -D mapreduce.framework.name=local /user/root/small/archive /user/root/backup/small-archive 复制归档。复制品的 raw.har 中的各个文件必须与原件长度相同。
distcp 是把文件列表拆开、由多个 Map 并行复制的 MapReduce 作业。它是集群之间迁移和备份的标准工具,把小文件打包之后,要复制的文件数减少,复制也会变快。如果目标路径不存在,源目录的内容会以那个名字被复制过去。
用数字记录小文件的代价
在 /root/hdp/small/report.md 中写出 ## NameNode 가 치르는 값、## HAR、## 옮기기 三个小节。第一节以数字写入第 6 步的 raw_objects,第二节以数字写入 har_objects。
请写出:NameNode 的堆统计的是什么;HAR 缩减了什么、放弃了什么(只读);要让小文件从一开始就不产生,写入一侧该做什么。