Apache Hadoop — 在一个 Pod 里搭起并运维 HDFS 与 YARN
小文件吃掉的不是磁盘,而是 NameNode 的内存
一句话总结
在 HDFS 中,一个文件的成本不是用字节来衡量,而是用它在 NameNode 内存中的对象数量来衡量。2,000 个 1KB 的文件,从磁盘上看只有 2MB,但对 NameNode 来说,是 2,000 个文件对象和 2,000 个块对象。Hadoop Archive(HAR)会把这些对象折叠成寥寥几个,让命名空间变轻,而删除原始文件则要由人来做。
为什么这会成为问题
HDFS 设计文档指出,NameNode 把整个命名空间和块映射表放在内存中。磁盘只要增加 DataNode 就能任意扩展,而命名空间则存活在 NameNode 这一个进程的堆里。一个文件、一个目录、一个块,都是这个堆里的对象。所以 HDFS 的容量有两根轴:用字节衡量的磁盘容量,和用对象数量衡量的命名空间容量。
同一份文档明确说明,HDFS 是为大文件而调整的。典型的文件在 GB 到 TB 之间,并且一个实例需要支持数千万个文件。数千万这个规模,才是设计所预期的大小。同样是数千万个文件,如果平均每个 1GB,就能装下几十 PB;如果平均每个 10KB,还没装到几百 GB,命名空间就先满了。Federation 文档之所以把“小文件很多的部署”直接列为设置多个 NameNode 的理由,也是这个缘故。
小文件在计算一侧同样吃亏。根据 MapReduce 教程,Map 的数量取决于输入块的数量,而准备任务需要时间,所以一个 Map 最好至少运行 1 分钟。2,000 个 1KB 的文件就是 2,000 个块,准备一次要花几秒钟的 Map 任务,要为了不到 1 秒钟就能完成的工作启动几千次。
工作原理
对象数量可以用数字看到。在指标文档的 FSNamesystem 条目中,FilesTotal 是文件和目录的数量,BlocksTotal 是已分配的块数。通过 NameNode 网页地址(默认 9870 端口)的 /jmx 就能直接读取。在一个空目录里上传 2,000 个小文件,这两个值会像下面这样变化(前面的数字只是示例)。
put 전 FilesTotal= 41 BlocksTotal= 12
put 2,000 FilesTotal=2042 BlocksTotal=2012 <- 파일 2,000 + 디렉터리 1, 블록 2,000
一个小文件会产生一个块。hdfs-default.xml 中 dfs.blocksize 的默认值是 128MB,但那只是块的最大大小。1KB 文件的块,在 DataNode 磁盘上只占 1KB 多一点。磁盘并没有被浪费。被浪费的,是 NameNode 为记住这个块而用的对象,以及报告和管理这个块的工作。如果把小文件问题解释成“浪费磁盘”,那就是错误的解释。
Hadoop Archive(HAR)会把这些对象折叠起来。根据 Hadoop Archives 指南,.har 是文件系统中的一个目录,里面有元数据 _index、_masterindex 和数据文件 part-*。原始文件的内容被依次拼接到几个 part 文件里,_index 记录每个原始文件的名称及其在 part 文件中的位置。2,000 个文件,在 NameNode 看来就变成了寥寥几个文件。
hadoop archive -archiveName logs.har -p /data/raw day1 /data/archive
hdfs dfs -ls har:///data/archive/logs.har/day1
HAR 以文件系统层的形式呈现自己。用 har:// 地址,ls、cat 这样的 shell 命令照样可用,也可以作为 MapReduce 的输入。但也有一些代价。
- 不可变。正如文档所述,在 HAR 内部重命名、删除、创建都会报错。要修改某一天的数据,就得重新制作归档。
- 制作时要运行 MapReduce 作业。通常在 YARN 上运行,如果没有启动 YARN,就用
-D mapreduce.framework.name=local在客户端 JVM 里运行。distcp 也一样。 - 不会删除原始文件。正如文档强调的,要缩小命名空间,必须在确认归档之后,亲手删除原始文件。在删除之前,对象数量反而是增加的。
- 副本数默认是 3。不指定
-r的话,会以副本数 3 制作。在只有一个 DataNode 的这个实验环境里,如果不指定-r 1,就会产生副本不足的块。 - 读取要多经过一步。读取一个文件,要先在索引中查到位置,再读取 part 文件中的那一段。它不是用于频繁随机读取少量数据的,而是用来归档保存的。
HAR 缩减的是命名空间的对象数量。即使把 HAR 作为 MapReduce 的输入,其中的逻辑文件看起来依然是文件,所以 Map 数量的问题并不会自动解决。如果问题出在计算上,从一开始就合并成大文件来写更好。
用 distcp 迁移与解包
大批量复制用 DistCp。这同样是一个 MapReduce 作业。它把要复制的文件列表展开,分给各个 Map 任务,每个 Map 复制自己的那一份。根据文档,它会尽量让每个 Map 分担大致相同的字节数,但文件是最小单位,所以小文件很多时,复制也同样会被文件数量拖慢。-update 只复制目标中没有或不同的文件,所以从第二次运行起就很快。把 HAR 解开也是复制。以 har:// 地址为源执行 hdfs dfs -cp,会依次解开;执行 distcp,则会并行解开。
在现场相遇的样子
第一,NameNode 因 GC 而停顿。磁盘空着一半,NameNode 的堆却满了,反复出现长时间的 GC。追查原因,发现有人的采集器每 5 分钟就扔进几千个小文件。这就是要把 FilesTotal 的趋势纳入监控项的原因。
第二,一个目录里的文件太多。dfs.namenode.fs-limits.max-directory-items 的默认值是 1,048,576。总是往同一个目录里扔文件的采集器,迟早会撞上这堵墙,写入就会失败。
第三,做了归档,对象却没有减少。因为没有删除原始文件。应当通过 har 地址读取归档来确认,然后再删除原始文件。如果顺序颠倒,就没有回头路了。
第四,真正的解决办法在写入一侧。HAR 是清理已经堆积起来的东西的工具。对于新增加的文件,必须在采集阶段就打包写入,或者安排定期合并成大文件的作业,问题才不会再次增长。
实际工作中真正重要的事
- 文件的成本是对象数量。文件、目录、块都是 NameNode 堆中的对象。
- 小块不会浪费磁盘。被浪费的是 NameNode 的记忆和任务数量。
- 要看 FilesTotal 和 BlocksTotal。只看磁盘使用率,看不到这个问题。
- HAR 不会删除原始文件。确认之后亲手删除,对象才会减少。
- HAR 不可变,默认副本数是 3。不适合需要修改的数据,在小集群中要指定 -r。
- 根本的解决办法是在写入一侧打包。用清理工具,堵不住泄漏的源头。
下一项实验要做什么
把 2,000 个小文件上传到 HDFS,用 count 和 fsck 统计文件、目录和块的数量,衡量 NameNode 承担的对象数量。不启动 YARN,用本地模式运行 hadoop archive,把这些文件打包成一个副本数为 1 的 HAR,通过 har 地址查看列表,数出其中的文件数,再读取一个文件并取到本地。把原始目录和 HAR 的对象数量并排比较,用数字确认减少了多少,最后用 distcp 为归档制作备份副本。