vlambda博客
学习文章列表

HDFS小文件治理总结

作者 | 荣翔

目录

  • 背景

  • 第一部分   回本溯源

  • 第二部分  HDFS大量小文件的危害

  • 第三部分  小文件治理方案总结

  • 第四部分 总结

  • 参考文献及资料

背景

企业级Hadoop大数据平台在实际使用过程中,可能大部分会遭遇小文件问题,并体验它的破坏性。HDFS文件系统的 inode 信息和 block 信息以及 block 的位置信息,这些原数据信息均由 NameNode 的内存中维护,这使得 NameNode 对内存的要求非常高,特别是遭遇海量小文件。

例如:京东的 NameNode 内存是 512GB,甚至还有大厂的 NameNode 的机器是 1TB的内存。能力强的大厂,钱就不花在买机器上了,例如字节跳动使用C++重写 NameNode ,这样分配内存和释放内存都由程序控制。但是NameNode天生的架构缺陷,所以元数据的扩展性终是受限于单机物理内存大小。

本篇文章首先回本溯源,分析小文件的产生原理以及对业务平台的危害,最后总结分析治理方法。

第一部分 回本溯源

HDFS基于Google的论文《分布式文件系统》思想实现的,设计目的是解决大文件的读写。

1.1 HDFS存储原理

HDFS集群(hadoop 2.0 +)中,有两类服务角色:NameNodeDataNode。文件数据按照固定大小(block size,默认128M)切分后,分布式存储在DataNode节点上。而数据的元数据信息加载在NameNode服务内存中。为防止服务单机会持久化一份在文件中(即fsimage文件,最新的元数据存储在edits log日志中,一般为 64MB,当 edits log 文件大小达到 64MB时,就会将这些元数据追加到 fsimage 文件中)。

每个文件/目录和block元数据信息存储在内存中,内存中分别对应:INodeFile、INodeDirectory、BlockInfo,每个对象大约150-200 bytes

1.2 检查文件系统

1.2.1 命令 fsck

HDFS提供了fsck命令用来检查HDFS文件系统的健康状态和Block信息。需要有HDFSsupergroup特权用户组的用户才有执行权限。参考下面的命令:

[root@quickstart cloudera]# hdfs fsck / -blocks -locations
Connecting to namenode via http://quickstart.cloudera:50070
FSCK started by admin (auth:KERBEROS_SSL) from /172.17.0.2 for path / at Sun May 09 05:35:33 UTC 2021
Status: HEALTHY
Total size:837797031 B
Total dirs:285
Total files:921
Total symlinks:0
Total blocks (validated):916 (avg. block size 914625 B)
Minimally replicated blocks:916 (100.0 %)
Over-replicated blocks:0 (0.0 %)
Under-replicated blocks:0 (0.0 %)
Mis-replicated blocks:0 (0.0 %)
Default replication factor:1
Average block replication:1.0
Corrupt blocks:0
Missing replicas:0 (0.0 %)
Number of data-nodes:1
Number of racks:1
FSCK ended at Sun May 09 05:35:34 UTC 2021 in 90 milliseconds

The filesystem under path '/' is HEALTHY

其中Total blocks (validated): 916 (avg. block size 914625 B),表示集群一共有916个Block,平均一个Block存储大小为0.87M

1.2.2 命令 count

HDFS 提供查询目录中文件数量的命令,例如:

[root@quickstart cloudera]# hdfs dfs -count /
        285          923          837797031 /

回显说明,依次为:DIR_COUNT(文件目录数量), FILE_COUNT(文件数量), CONTENT_SIZE(存储量) FILE_NAME(查询文件目录名)

1.2.3 NameNode WebUI界面

NameNode WebUI提供展示界面,显示HDFS文件系统的关键指标信息。例如:

Security is on.
Safemode is off.
1,206 files and directories, 916 blocks = 2,122 total filesystem object(s).
Heap Memory used 23.5 MB of 48.38 MB Heap Memory. Max Heap Memory is 48.38 MB.
Non Heap Memory used 49.16 MB of 77.85 MB Commited Non Heap Memory. Max Non Heap Memory is 130 MB.

目前HDFS文件系统中有1206个文件和文件目录,916block,一共2122个文件系统对象。

可以jmap命令参看jvm堆栈实际使用:

# jmap -histo:live 8204(namenode进程pid )
num     #instances         #bytes class name
#(省略)
Total        435970       61136008

实际使用61136008/1024/1024=58.3 M

1.2.4 元数据内存资源估算

NameNode的内存主要由NameSpaceBlocksMap占用,其中NameSpace存储的主要是INodeFile和INodeDirectory对象。BlocksMap存储的主要是BlockInfo对象,所以估算NameNode占用的内存大小也就是估算集群中INodeFileINodeDirectoryBlockInfo这些对象占用的heap空间。下面是估算NameNode内存数据空间占用资源大小的预估公式。

Total = 198 ∗ num(directories + Files) + 176 ∗ num(blocks) + 2% ∗ size(JVM Memory Size)

例如测试集群:(1206*198+176*916)/1024/1024+2%*1000=20.38M,和实际值23.5 M误差较小。

1.2.5 堆栈配置建议

NameNode WebUI界面的Summary可以看到文件系统对象(filesystem objects)的统计。下面是NameNode根据文件数量的堆栈大小配置建议。

文件数量(1文件对应1block) 文件系统对象数量(filesystem objects=files+blocks 参考值(GC_OPTS
5,000,000 10,000,000 -Xms6G -Xmx6G -XX:NewSize=512M -XX:MaxNewSize=512M
10,000,000 20,000,000 -Xms12G -Xmx12G -XX:NewSize=1G -XX:MaxNewSize=1G
25,000,000 50,000,000 -Xms32G -Xmx32G -XX:NewSize=3G -XX:MaxNewSize=3G
50,000,000 100,000,000 -Xms64G -Xmx64G -XX:NewSize=6G -XX:MaxNewSize=6G
100,000,000 200,000,000 -Xms96G -Xmx96G -XX:NewSize=9G -XX:MaxNewSize=9G
150,000,000 300,000,000 -Xms164G -Xmx164G -XX:NewSize=12G -XX:MaxNewSize=12G

1.3 产生的场景分析

在实际生产环境中,很多场景会产生小文件。

1.3.1 MapReduce产生

Mapreduce任务中reduce数量设置过多,reduce的个数和输出文件个数一致,从而导致输出大量小文件。

1.3.2 hive产生

hive表设置过量分区,每次写入数据会分别落盘到各自分区中,每个分区的数据量越小,对应的分区表文件也就会越小。从而导致产生大量小文件。

1.3.3 实时流任务处理

流任务处理数据通常要求短时间数据落盘,例如Spark Streaming 从外部数据源接收数据,每个微批(默认60s)需要落盘一次结果数据到HDFS,如果数据量小,会产生大量小文件落盘文件。

1.3.4 数据自身特性产生

除了数据处理产生,还有是由于数据自身特性决定的。例如使用HDFS存储图片、短视频等数据。这些数据本身单体就不大,就会以小文件形式存储。

第二部分 HDFS大量小文件的危害

2.1 MapReduce任务消耗大量计算资源

MapReduce任务处理HDFS文件的时候回根据数据的Block数量启动对应数量Map task,如果是小文件系统,这会导致任务启动大量的Map task,一个taskYarn上对应一个CPU,实际线上环境会短时间申请成千个CPU资源,造成集群运行颠簸。可以通过设置map端文件合并及reduce端文件合并来优化。

2.2 NameNode服务过载

HDFS作为分布式文件系统的一个优点就是可以横向伸缩扩展,但是由于元数据存储在NameNode中,事实上,当数据量达到一定程度,NameNode服务单机内存资源(普通PC物理机内存通常是256G)反而成为横向扩展的瓶颈。特别是面对小文件系统,可能集群实际存储并不大,但是元数据信息已经使得NameNode服务过载,这时候横向扩容DataNode是无济于事的。

HDFS中元数据的操作均在NameNode服务完成,小文件系统造成服务过载后,元数据更新性能会下降。严重的时候,服务会经常Full GC,如果GC停顿过长甚至会导致服务故障。

如果导致NameNode出现故障,在没有HA保障时,服务启动是一个漫长的过程。服务需要重新将fsImage文件数据加载至服务内存,最新的日志数据editlogs需要回放,最后CheckpointDataNodeBlockReport。这个过程当元数据过大时候是个漫长过程。

例如美团的提供案例:当元数据规模达到5亿(NamespaceINode数超过2亿,Block数接近3亿),fsImage文件大小将接近到20GB,加载FsImage数据就需要约14minCheckpoint需要约6min,再加上其它阶段整个重启过程将持续约50min,极端情况甚至超过60min

笔者公司HA集群,曾经由于小文件系统导致NameNode服务故障,甚至出现主备元数据未同步,整个恢复过程需要完成先完成主备同步,整整需要8个小时,这个期间HDFS无法对外服务,影响较大。

第三部分 小文件治理方案总结

面对HDFSNameNode内存过载带来的线上问题,Hadoop社区给出治理方案和架构上优化。主要有:

  • 横向扩展NameNode能力,分散单点负载;例如联邦(Federation)。

  • NameNode元数据调整为外置存储;例如LevelDB作为存储对象。

另外还有美团技术文章《HDFS NameNode内存全景》提到互联网大厂的最佳实践和尝试:

除社区外,业界也在尝试自己的解决方案。Baidu HDFS2[5]将元数据管理通过主从架构的集群形式提供服务,本质上是将原生NameNode管理的NamespaceBlockManagement进行物理拆分。其中Namespace负责管理整个文件系统的目录树及文件到BlockID集合的映射关系,BlockIDDataNode的映射关系是按照一定的规则分到多个服务节点分布式管理,这种方案与Lustre有相似之处(Hash-based Partition)。Taobao HDFS2[6]尝试过采用另外的思路,借助高速存储设备,将元数据通过外存设备进行持久化存储,保持NameNode完全无状态,实现NameNode无限扩展的可能。其它类似的诸多方案不一而足。

尽管社区和业界均对NameNode内存瓶颈有成熟的解决方案,但是不一定适用所有的场景,尤其是中小规模集群。

3.1 联邦HDFS

Hadoop 2.x发行版中引入了联邦(Federation)HDFS功能。联邦HDFS允许集群通过添加多个NameNode来实现扩展,每个NameNode管理一份元数据。架构上解决了横向扩展,但是这不是一个真正的分布式NameNode,仍然存在单点故障风险。具体可以参考美团技术的最佳实践[3]。

HDFS小文件治理总结

3.2 归档文件

对于小文件问题,Hadoop自身提供了三种解决方案:Hadoop ArchiveSequence FileCombineFileInputFormat

3.2.1 Hadoop Archive

Hadoop Archives (HAR files)0.18.0版本中引入,目的是为了缓解大量小文件消耗 NameNode 内存的问题。HAR不会减少文件存储大小,而是减少NameNode 的内存资源。例如下图展示了将HDFS文件目录foo中大量小文件file-*归档为bar.har文件。

HDFS小文件治理总结

可以使用下面的命令对HDFS文件进行归档:

# hadoop archive -archiveName name -p <parent> [-r <replication factor>] <src>* <dest>
  • -archiveName指定创建归档文件的名称,如:foo.har,也就是说需要在归档名称后面添加一个*.har的扩展。

  • -p 指定需归档的文件的相对路径,如:-p /foo/bar a/b/c e/f/g/foo/bar是根目录,a/b/ce/f/g是相对根目录的相对路径。

  • -r 指定所需的复制因子,如果未被指定,默认为3。

例如下面的命令执行后,将提交一个Mapreduce任务。

[root@quickstart /]# hadoop archive -archiveName test.har -p /user/test /tmp

使用下面的命令查看归档后的文件:

[root@quickstart /]# hadoop fs -lsr har:///tmp/test.har
lsr: DEPRECATED: Please use 'ls -R' instead.
-rw-r--r--   3 admin supergroup          0 2021-05-10 23:02 har:///tmp/test.har/file-1
(略)

需要注意的是:

  • 归档文件一旦创建就不能改变,要增加或者删除文件,就需要重新创建。

  • 归档不支持压缩数据,类似于Unix中的tar命令。

  • 归档命令不会删除原HDFS文件,需要自行删除。

  • *.harHDFS上是一个目录,不是一个文件。

Hive内置了将现有分区中的文件转换为Hadoop Archive (HAR)的支持,具体参数为:

# 启用归档
hive> set hive.archive.enabled=true;
# 创建存档时是否可以设置父目录
hive> set hive.archive.har.parentdir.settable=true;
# 控制组成存档的文件的大小
hive> set har.partfile.size=1099511627776;

实际线上环境,可以对长期保存的hive表以分区颗粒度进行归档,在需要查询的时候进行归档恢复。具体实践案例如下:

# hive分区归档
ALTER TABLE tablename ARCHIVE PARTITION(ds='2008-04-08', hr='12')
# hive归档分区恢复
ALTER TABLE tablename UNARCHIVE PARTITION(ds='2008-04-08', hr='12')

数据归档为har后,数据仍然是可以被查询的(但是是不可写的),只是查询会比非归档慢,如果要提高效率需要归档恢复。

3.2.2 Sequence File

SequenceFileHadoop API提供的一种二进制文件,它将数据以<key,value>的形式序列化到文件中,使用Hadoop的标准Writable接口实现序列化和反序列化。Mapreduce计算的中间结果的落盘就是SequenceFile格式的文件。

线上生产环境中,将SequenceFile格式的文件作为HDFS小文件的容器。即读取小文件然后以Append追加的形式写入"文件容器"。

3.2.3 CombineFileInputFormat

Hadoop内置提供了一个 CombineFileInputFormat 类来专门处理合并小文件,其核心功能是将HDFS上多个小文件合并到一个 InputSplit中,然后会启用一个map来处理这里面的文件,以此减少Mapreduce整体作业的运行时间,同时也减少了map任务的数量。

这个接口天然具备处理小文件的能力,只要将合并后的小文件落盘即可。

另外对于Hive输入也是支持合并方式读取的,参数配置参考:

# 执行Map前进行小文件合并
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat
set mapreduce.input.fileinputformat.split.maxsize=1073741824
set mapreduce.input.fileinputformat.split.minsize=1073741824

注意以上MapreduceHive Sql使用CombineFileInputFormat方式并不会缓解NameNode内存管理问题,因为合并的文件并不会持久化保存到磁盘。只是提高MapreduceHive作业的性能。

3.3 更换存储介质

技术架构中没有最牛逼的技术,只有最合适的技术。HDFS并不适合小文件,那么可以改变存储介质。用HBase来存储小文件,也是比较常见的选型方案。HBase在设计上主要为了应对快速插入、存储海量数据、单个记录的快速查找以及流式数据处理。适用于存储海量小文件(小于10k),并支持对文件的低延迟读写。

HDFS小文件治理总结

HBase同一类的文件存储在同一个列族下面,若文件数量太多,同样会导致regionserver内存占用过大,JVM内存过大时gc失败影响业务。根据实际测试,无法有效支持10亿以上的小文件存储。

如果数据量存储不大可以考虑传统的Mysql数据库,数据量较大可以考虑Tidb分布式数据库等等。

3.4 客户端写入规范

解决小文件问题的最简单方法就是在生成阶段就避免小文件的产生。

3.4.1 Hive 写入优化

Hive SQL执行的背后实际是Mapreduce任务,所有优化涉及大量小文件读优化、总结过程小文件优化、输出小文件邮件。本次只介绍最后一种优化场景。主要优化目标就是减少Reduce数量和增加Reduce处理能力,涉及参数有:

# 设置reduce的数量
set mapred.reduce.tasks = 1;
# 每个reduce任务处理的数据量,默认为1G
set hive.exec.reducers.bytes.per.reducer = 1000000000
# 每个任务最大的reduce数
set hive.exec.reducers.max = 50

还可以对输出数据进行压缩,涉及参数有:

# 开启hive最终输出数据压缩功能
set hive.exec.compress.output=true;
# 开启mapreduce最终输出数据压缩
set mapreduce.output.fileoutputformat.compress=true;
# 设置数据输出压缩方式
set mapreduce.output.fileoutputformat.compress.codec =org.apache.hadoop.io.compress.SnappyCodec;
# 设置mapreduce最终数据输出压缩为块压缩
set mapreduce.output.fileoutputformat.compress.type=BLOCK;

另外就是输出结果进行归档,这在前文已介绍。

3.5 合并小文件系统

3.5.1 离线合并

HDFS提供命令将hdfs多个文件合并后下载到本地文件系统,然后可以将文件重新上传HDFS文件系统。

hdfs dfs -getmerge hdfs文件1 hdfs文件2 hdfs文件3 输出本地文件名

这显然是低效的。

3.5.2 脚本实现

HDFS文件系统由于疏忽已经受到小文件系统侵扰时,就需要我们被动治理了。通常的做法是检查所有文件系统,并确认哪些文件夹中的小文件过年需要合并。可以通过自定义的脚本或程序。例如通过调用HDFSsync()方法和append()方法,将小文件和目录每隔一定时间生成一个大文件,或者可以通过写程序来合并这些小文件。

这里推荐一个开源工具File Crush。

https://github.com/edwardcapriolo/filecrush/

另外还有文章《分析hdfs文件变化及监控小文件》可以参考。

3.6 HDFS项目解决方案

前文小文件治理方案虽然能够解决小文件的问题,但是这些方法都有不足或成本较高。那就需要在架构设计进行根本优化解决,目前 Hadoop 社区已经有很多相应的讨论和架构规划。例如下面的提案:

  • HDFS-7836(将BlocksMap放在堆外) ,未实现;

  • HDFS-8286 (将元数据保存在LevelDB),已实现;

  • HDFS-8998(一个Block对应多个文件),未实现;

但是提案进度堪忧呀,大部分还处于讨论或推进一部分就没后续了.......,所以提出问题简单,解决问题难呀。扯远了,本文只挑两个简单看一下。

3.6.1 HDFS-8998

目前HDFS版本中一个Block只能对应一个文件。社区和项目组考虑:能否一个Block能对应多个小文件。这就是解决提案:《Small files storage supported inside HDFS》,提案编号:HDFS-8998,提案参考说明文档。

主要的设计目标为:

  • 为小文件设置单独的区域,称为Small file zone,用于小文件创建和写操作;

  • NameNode 保存一个固定大小的 Block List,列表的大小是可以配置的;

  • client1第一次向 NameNode 发出写请求时,NameNode 将为client1创建第一个 blockid,并锁定这个Block

  • 当其他客户端client2NameNode 发出写请求时,NameNode 将尝试为其分配未锁定的块(unlocked block),如果没有,并且现有的块数小于Block List,那么 NameNode 则为client2分配创建新的 blockid ,并且同时锁定。

  • 其他客户端Client3...client N类似;

  • 客户端如果获取不到未锁定的块资源,并且也不能新建(Block List资源上限)。这时候需要客户端等待其他客户端释放Block资源。

  • 客户端写数据是将数据追加(appenging)到块上;

  • 当客户端的读写(OutputStream)关闭,被客户端占用的Block将被释放;

  • 当某个块被写满,也会分配新的一个块给客户端。这个写满的Block将从Block List中移除,同时会添加新的Block资源进行资源List

新的设计中一个 Block 将包含多个文件。需要新的文件操作设计:

  • 读取,读写小文件和平常读取hdfs文件类似。

  • 删除,新的设计小文件是 Block 的一部分(segment),所以删除操作不能直接删除一个 Block。删除操作调整为:从 NameNode 中的 BlocksMap 删除 INode;然后当这个块中被删除的数据达到一定的阈值(可配置) ,对应的块将会被重写。

  • append 和 truncate,对小文件的 truncate 和 append 是不支持的,因为这些操作代价非常高,而且是不常用的。会增加后台进程对Block的小文件segment进行合并(segment合并触发数量可配置)

  • 高可用:继承原架构的副本机制;

3.6.2 HDFS-8286

提案:《Scaling out the namespace using KV store》,提案编号:HDFS-8286。该提案目标调整元数据的存储形式,从内存调整外置存储( KV存储系统)。现HDFS中以层次结构的形式来管理文件和目录,文件和目录表示为inode 对象。调整为KV存储,核心解决的问题是设计合适数据结构将元数据以KV格式存储。

详细设计就不展开了,可以参考提示设计说明文档。

3.6.3 Hadoop Ozone项目

如果线上的业务数据是非结构化的小数据对象,例如海量图片(如银行业务保存的文件影像数据)、音频、小视频等。这种类型数据可以有适合的存储方式,对象存储。而Hadoop生态圈也正好有个项目。

OzoneHortonworks 基于 HDFS 实现的对象存储,OZoneHDFS有着很深的关系,在设计上也对HDFS存在的不足做了很多改进,使用HDFS的生态系统可以无缝切换到OZone,参考提案: HDFS-7240。

目前项目已经成为 Apache Hadoop的子项目。已经告别alpha版本阶段,最新的Release 1.1.0 版本已发布。

OzoneHDFS相同,也是采用 Master/Slave 架构,但是对管理服务namespaceBlockManager进行拆分,将元数据管理分成两个,一个是 Ozone Manager 作为对象存储元数据服务,另一个是 StorageContainerManager,作为存储容器管理服务。

另外Ozone ManagerStorageContainerManager的元数据都是使用RocksDB 进行单独存储,而不是放在NameNode内存中,架构上不再被堆内存限制,可以横向扩展。

第四部分 总结

  • 技术没有银弹,只有合适的技术

    实际生产环境面对HDFS小文件问题,需要提前管控业务写入,优化写入的客户端程序。为业务数据选择合适的数据存储方式,因地适宜。

  • 追根溯源

    当我们面多小文件的问题时,需要检查业务数据处理流,定位小文件产生的根因。

  • 量力而行

    需要评估企业大数据团队的技术能力。如果没有能力对原架构进行二次开发优化,就需要编写一些自定义程序来处理小文件。尽量摸透开源架构的特性,做好参数优化。

参考文献及资料

[1] HDFS NameNode内存全景,链接:https://tech.meituan.com/2016/08/26/namenode.html

[2] The Small Files Problem,链接:https://blog.cloudera.com/the-small-files-problem/

[3] HDFS Federation在美团点评的应用与改进,链接:https://tech.meituan.com/2017/04/14/hdfs-federation.html

[4]Introducing Apache Hadoop Ozone: An Object Store for Apache Hadoop,链接:https://blog.cloudera.com/introducing-apache-hadoop-ozone-object-store-apache-hadoop/