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 +
)中,有两类服务角色:NameNode
、DataNode
。文件数据按照固定大小(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信息。需要有HDFS
的supergroup
特权用户组的用户才有执行权限。参考下面的命令:
[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
个文件和文件目录,916
个block
,一共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
的内存主要由NameSpace
和BlocksMap
占用,其中NameSpace
存储的主要是INodeFile和INodeDirectory
对象。BlocksMap
存储的主要是BlockInfo
对象,所以估算NameNode
占用的内存大小也就是估算集群中INodeFile
、INodeDirectory
和BlockInfo
这些对象占用的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
,一个task
在Yarn
上对应一个CPU
,实际线上环境会短时间申请成千个CPU
资源,造成集群运行颠簸。可以通过设置map端文件合并及reduce端文件合并来优化。
2.2 NameNode
服务过载
HDFS
作为分布式文件系统的一个优点就是可以横向伸缩扩展,但是由于元数据存储在NameNode
中,事实上,当数据量达到一定程度,NameNode
服务单机内存资源(普通PC物理机内存通常是256G
)反而成为横向扩展的瓶颈。特别是面对小文件系统,可能集群实际存储并不大,但是元数据信息已经使得NameNode
服务过载,这时候横向扩容DataNode
是无济于事的。
HDFS
中元数据的操作均在NameNode
服务完成,小文件系统造成服务过载后,元数据更新性能会下降。严重的时候,服务会经常Full GC
,如果GC
停顿过长甚至会导致服务故障。
如果导致NameNode
出现故障,在没有HA
保障时,服务启动是一个漫长的过程。服务需要重新将fsImage
文件数据加载至服务内存,最新的日志数据editlogs
需要回放,最后Checkpoint
和DataNode
的BlockReport
。这个过程当元数据过大时候是个漫长过程。
例如美团的提供案例:当元数据规模达到5亿(Namespace
中INode
数超过2亿,Block
数接近3亿),fsImage
文件大小将接近到20GB
,加载FsImage
数据就需要约14min
,Checkpoint
需要约6min
,再加上其它阶段整个重启过程将持续约50min
,极端情况甚至超过60min
。
笔者公司HA
集群,曾经由于小文件系统导致NameNode
服务故障,甚至出现主备元数据未同步,整个恢复过程需要完成先完成主备同步,整整需要8个小时,这个期间HDFS
无法对外服务,影响较大。
第三部分 小文件治理方案总结
面对HDFS
的NameNode
内存过载带来的线上问题,Hadoop
社区给出治理方案和架构上优化。主要有:
横向扩展
NameNode
能力,分散单点负载;例如联邦(Federation)。NameNode
元数据调整为外置存储;例如LevelDB
作为存储对象。
另外还有美团技术文章《HDFS NameNode
内存全景》提到互联网大厂的最佳实践和尝试:
除社区外,业界也在尝试自己的解决方案。
Baidu HDFS2
[5]将元数据管理通过主从架构的集群形式提供服务,本质上是将原生NameNode
管理的Namespace
和BlockManagement
进行物理拆分。其中Namespace
负责管理整个文件系统的目录树及文件到BlockID
集合的映射关系,BlockID
到DataNode
的映射关系是按照一定的规则分到多个服务节点分布式管理,这种方案与Lustre
有相似之处(Hash-based Partition)。Taobao HDFS2[6]
尝试过采用另外的思路,借助高速存储设备,将元数据通过外存设备进行持久化存储,保持NameNode
完全无状态,实现NameNode
无限扩展的可能。其它类似的诸多方案不一而足。
尽管社区和业界均对NameNode
内存瓶颈有成熟的解决方案,但是不一定适用所有的场景,尤其是中小规模集群。
3.1 联邦HDFS
在Hadoop 2.x
发行版中引入了联邦(Federation)HDFS
功能。联邦HDFS
允许集群通过添加多个NameNode
来实现扩展,每个NameNode
管理一份元数据。架构上解决了横向扩展,但是这不是一个真正的分布式NameNode
,仍然存在单点故障风险。具体可以参考美团技术的最佳实践[3]。
3.2 归档文件
对于小文件问题,Hadoop
自身提供了三种解决方案:Hadoop Archive
、 Sequence File
和 CombineFileInputFormat
。
3.2.1 Hadoop Archive
Hadoop Archives (HAR files)
在 0.18.0
版本中引入,目的是为了缓解大量小文件消耗 NameNode
内存的问题。HAR
不会减少文件存储大小,而是减少NameNode
的内存资源。例如下图展示了将HDFS
文件目录foo
中大量小文件file-*
归档为bar.har
文件。
可以使用下面的命令对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/c
,e/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
文件,需要自行删除。*.har
在HDFS
上是一个目录,不是一个文件。
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
SequenceFile
是Hadoop 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
注意以上Mapreduce
和Hive Sql
使用CombineFileInputFormat
方式并不会缓解NameNode
内存管理问题,因为合并的文件并不会持久化保存到磁盘。只是提高Mapreduce
和Hive
作业的性能。
3.3 更换存储介质
技术架构中没有最牛逼的技术,只有最合适的技术。HDFS
并不适合小文件,那么可以改变存储介质。用HBase
来存储小文件,也是比较常见的选型方案。HBase
在设计上主要为了应对快速插入、存储海量数据、单个记录的快速查找以及流式数据处理。适用于存储海量小文件(小于10k
),并支持对文件的低延迟读写。
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
文件系统由于疏忽已经受到小文件系统侵扰时,就需要我们被动治理了。通常的做法是检查所有文件系统,并确认哪些文件夹中的小文件过年需要合并。可以通过自定义的脚本或程序。例如通过调用HDFS
的sync()
方法和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
;当其他客户端
client2
向NameNode
发出写请求时,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
生态圈也正好有个项目。
Ozone
是 Hortonworks
基于 HDFS
实现的对象存储,OZone
与HDFS
有着很深的关系,在设计上也对HDFS
存在的不足做了很多改进,使用HDFS
的生态系统可以无缝切换到OZone
,参考提案: HDFS-7240。
目前项目已经成为 Apache Hadoop的子项目。已经告别alpha
版本阶段,最新的Release 1.1.0 版本已发布。
Ozone
和HDFS
相同,也是采用 Master/Slave 架构,但是对管理服务namespace
和BlockManager
进行拆分,将元数据管理分成两个,一个是 Ozone Manager
作为对象存储元数据服务,另一个是 StorageContainerManager
,作为存储容器管理服务。
另外Ozone Manager
和StorageContainerManager
的元数据都是使用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/