TT Lab
开始
学习 学习路径 课程

Apache Hadoop — 在一个 Pod 里搭起并运维 HDFS 与 YARN

小文件吃掉的不是磁盘,而是 NameNode 的内存

在 TT Lab 中继续学习

一句话总结

在 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 缩减的是命名空间的对象数量。即使把 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 是清理已经堆积起来的东西的工具。对于新增加的文件,必须在采集阶段就打包写入,或者安排定期合并成大文件的作业,问题才不会再次增长。

实际工作中真正重要的事

下一项实验要做什么

把 2,000 个小文件上传到 HDFS,用 count 和 fsck 统计文件、目录和块的数量,衡量 NameNode 承担的对象数量。不启动 YARN,用本地模式运行 hadoop archive,把这些文件打包成一个副本数为 1 的 HAR,通过 har 地址查看列表,数出其中的文件数,再读取一个文件并取到本地。把原始目录和 HAR 的对象数量并排比较,用数字确认减少了多少,最后用 distcp 为归档制作备份副本。