前言
HDFS小文件过多会对Hadoop扩展性以及稳定性造成影响,NameNode需维护大量元数据信息导致占用过多的存储空间。大量的小文件也会导致很差的查询分析性能,因为执行查询时需要进行频繁的文件打开/读取/关闭,造成查询性能的损耗。
一、小文件治理的背景
1.1 小文件产生原因
- 日常任务及动态分区插入数据会产生大量的小文件,从而导致map数量剧增;
- reduce数量越多,小文件也越多(reduce个数和输出文件是对应的)
- 数据源本身就包含大量的小文件,例如api,kafka消息管道等。
- 实时数据落hive也会产生大量小文件。
1.2 小文件的危害
小文件通常是指文件大小要比HDFS块大小(一般是128M)还要小很多的文件。
- 从 Hive 角度看,一个小文件会开启一个 MapTask,一个 MapTask开一个 JVM 去执行,这些任务的启动及初始化,会浪费大量的资源,严重影响性能。
-
NameNode在内存中维护整个文件系统的元数据镜像、其中每个HDFS文件元数据信息(位置、大小、分块等)对象约占150字节,如果小文件过多会占用大量内存,会直接影响NameNode性能。相对的,HDFS读写小文件也会更加耗时,因为每次都需要从NameNode获取元信息,并与对应的DataNode建立pipeline连接。
-
小文件会给SparkSQL等查询引擎造成查询性能的损耗,大量的数据分片信息以及对应产生的task元信息会给Spark Driver的内存造成压力。
二、小文件解决方案
hive中小文件的解决思路主要有两个方向:1.小文件的预防;2.已存在的小文件合并操作
2.1 小文件的预防
通过调整参数进行合并,在 hive 中执行 insert overwrite tableA select xx from tableB 之前设置合并参数,即可自动合并小文件。
2.1.1 减少Map数量
- 设置map输入时的合并参数:
#执行Map前进行小文件合并
#CombineHiveInputFormat底层是 Hadoop的 CombineFileInputFormat 方法
#此方法是在mapper中将多个文件合成一个split切片作为输入
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; -- 默认
#每个Map最大的输入大小(这个值决定了合并后文件的数量)
set mapred.max.split.size=256*1000*100; -- 256M
#一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并)
set mapred.min.split.size.per.node=100*100*100; -- 100M
#一个交换机下split的至少的大小(这个值决定了多个交换机上的文件是否需要合并)
set mapred.min.split.size.per.rack=100*100*100; -- 100M
- 设置map输出时和reduce输出时的合并参数:
#设置map端输出进行合并,默认为true
set hive.merge.mapfiles = true;
#设置reduce端输出进行合并,默认为false
set hive.merge.mapredfiles = true;
#设置合并文件的大小
set hive.merge.size.per.task = 256*1000*1000; -- 256M
#当输出文件的平均大小小于该值时,启动一个独立的MapReduce任务进行文件merge
set hive.merge.smallfiles.avgsize=16000000; -- 16M
- 启用压缩(小文件合并后,也可以选择启用压缩)
# hive的查询结果输出是否进行压缩
set hive.exec.compress.output=true;
# MapReduce Job的结果输出是否使用压缩
set mapreduce.output.fileoutputformat.compress=true;
2.1.2 减少Reduce的数量
#reduce的个数决定了输出的文件的个数,所以可以调整reduce的个数控制hive表的文件数量,
#通过设置reduce的数量,利用distribute by使得数据均衡的进入每个reduce。
#设置reduce的数量有两种方式,第一种是直接设置reduce个数
set mapreduce.job.reduces=10;
#第二种是设置每个reduceTask的大小,Hive会根据数据总大小猜测确定一个reduce个数
set hive.exec.reducers.bytes.per.reducer=512*1000*1000; -- 默认是1G,这里为设置为5G
#执行以下语句,将数据均衡的分配到reduce中
set mapreduce.job.reduces=10;
insert overwrite table A partition(dt)
select * from B
distribute by cast(rand()*10 as int);
解释:如设置reduce数量为10,则使用cast(rand()*10 as int),生成0-10之间的随机整数,根据【随机整数 % 10】计算分区编号,这样数据就会均衡的分发到各reduce中,防止出现有的文件过大或过小
2.2 已存在的小文件合并
对集群上已存在的小文件进行定时或实时的合并操作,定时操作可在访问低峰期操作,如凌晨2点,合并操作主要有以下几种方式:
2.2.1 方式一:insert overwrite
执行流程总体如下:
(1)创建备份表(创建备份表时需和原表的表结构一致)
create table test.table_hive_back like test.table_hive ;
(2)设置合并文件相关参数,并使用insert overwrite 语句读取原表,再插入备份表
- 设置合并文件相关参数
使用 hive的merger合并参数,在正式 insert overwrite 之前做一个合并,合并的时候注意设置好压缩,不然文件会比较大。
- 合并文件至备份表中,执行前保证没有数据写入原表
#如果有多级分区,将分区名放到partition中
insert overwrite table test.table_hive_back partition(batch_date)
select * from test.table_hive;
ps:insert overwrite table test.table_hive_back 备份表的时候,可以使用distribute by 命令设置合并后的batch_date分区下的文件数据量
insert overwrite table 目标表 [partition(hour=...)] select * from 目标表
distribute by cast( rand() * 具体最后落地生成多少个文件数 as int);
上面关键字解释:
- insert overwrite:会重写数据,先进行删除后插入(不用担心如果overwrite失败,数据没了,这里面是有事务保障的);
- distribute by分区:能控制数据从map端发往到哪个reduceTask中,distribute by的分区规则:分区字段的hashcode值对reduce 个数取模后, 余数相同的数据会分发到同一个reduceTask中。
- rand()函数:生成0-1随机小数,控制最终输出多少个文件。
# 使用distribute by rand()将数据随机分配给reduce,这样可以使得每个reduce处理的数据大体一致。 避免出现有的文件特别大, 有的文件特别小,例如:控制dt分区目录下生成100个文件,那么hsql如下:
insert overwrite table A partition(dt)
select * from B
distribute by cast(rand()*100 as int);
如果合并之后的文件竟然还变大了,可能是 select from的原数据是被压缩的,但是insert overwrite目标表的时候,没有设置输出文件压缩功能,解决方案:
# hive的查询结果输出是否进行压缩
set hive.exec.compress.output=true;
# MapReduce Job的结果输出是否使用压缩
set mapreduce.output.fileoutputformat.compress=true;
#设置压缩方式是snappy
set parquet.compression = snappy;
(3)确认表数据一致后,将原表修改名称为临时表tmp,将备份表修改名称为原表
- 先查看原表和备份表数据量,确保表数据一致
#查看原表和备份表数据量
set hive.compute.query.using.stats=false ;
set hive.fetch.task.conversion=none;
select count(*) from test.table_hive;
select count(*) from test.table_hive_back ;
- 将原表修改名称为临时表tmp,将备份表修改名称为原表
alter table test.table_hive rename to test.table_hive_tmp;
alter table test.table_hive_back rename to test.table_hive ;
(4)检验合并后的分区数和小文件数量
正常情况下:hdfs文件系统上的table_hive表的分区数量没有改变,但是每个分区的几个小文件已经合并为一个文件。
#统计合并后的分区数
[atguigu@bigdata102 ~]$ hdfs dfs -ls /user/hive/warehouse/test/table_hive
#统计合并后的分区数下的文件数
[atguigu@bigdata102 ~]$ hdfs dfs -ls /user/hive/warehouse/test/table_hive/batch_date=20210608
(5)观察一段时间后再删除临时表
drop table test.table_hive_tmp ;
ps:注意修改hive表名的时候,对应表的存储路径会发生变化,如果有新的任务上传数据到具体路径,需要注意可能需要修改。
2.2.2 方式二:concatenate
对于orc文件,可以使用 hive 自带的 concatenate 命令,自动合并小文件。
#对于非分区表
alter table test concatenate;
#对于分区表
alter table test [partition(...)] concatenate
#例如:alter table test partition(dt='2021-05-07',hr='12') concatenate;
注意:
- concatenate 命令只支持 rcfile和 orc文件类型。
- concatenate合并小文件时不能指定合并后的文件数量,但可以多次执行该命令。
- 假设多次使用concatenate后文件数量不变化,这个跟参数 mapreduce.input.fileinputformat.split.minsize=256mb 的设置有关,可设定每个文件的最小size。
2.2.3 方式三:使用hive的archive归档
每日定时脚本,对于已经产生小文件的hive
表使用har
归档,然后已归档的分区不能insert overwrite ,必须先unarchive。
#用来控制归档是否可用
set hive.archive.enabled=true;
#通知Hive在创建归档时是否可以设置父目录
set hive.archive.har.parentdir.settable=true;
#控制需要归档文件的大小
set har.partfile.size=256000000;
#对表的某个分区进行归档
alter table test_rownumber2 archive partition(dt='20230324');
#对已归档的分区恢复为原文件
alter table test_rownumber2 unarchive partition(dt='20230324');
2.2.4 使用spark3合并小文件
该方案介绍使用spark引擎情况下合并小文件方式。通常能想到的方案是通过Spark API 对文件目录下的小文件进行读取,然后通过Spark的repartition 算子合并小文件,Repartition 分区数通过输入文件的总大小和期望输出文件的大小通过预计算而得。
如果分区较多,利用Spark3.2+Rebalance进行动态分区刷新,如果分区较少或者未分区的表可以考虑直接将表删再重建,使用Spark3跑数据进行回刷。
Spark3.0引入AQE(Adaptive Query Execution)特性,该特性目的是提高Spark SQL的执行效率和可扩展性。AQE可以自适应地优化查询计划并动态调整任务大小,同时可以自动跟踪有关数据的统计信息。
以下是AQE的一些主要功能:
1.自适应的Shuffle Partitions : Spark AQE可以根据输入数据量和集群资源动态设置shuffle分区数量,并在运行时进行调整。这样可以避免产生小文件或过度分片等问题。
2.动态重组join策略:使用AQE,Spark SQL可以根据输入数据大小、分布式环境和其他因素来选择最佳的连接策略。它能够在传统的join和broadcast join之间做出决策,并且可以智能地将一个join操作拆分成多个阶段来减少内存压力。
3.运行时统计信息收集:通过收集实际执行期间的运行时指标(如I/O开销、网络瓶颈等),AQE能够更好的估算操作所需的时间和资源,并相应的
对其进行优化。
待补充~ 当底层计算引擎使用spark时,产生的小文件解决方案:
三、小文件治理工具化
借助云平台进行小文件治理,平台图展示如下:可以通过小文件执行趋势、存储量、分区量判断需要治理的数据表有哪些,然后通过右侧的【合并小文件、任务运维、数据趋势】操作进行小文件的优化。
3.1 合并小文件功能
实际是通过用户配置spark3调度任务,每天定时调度将数据写入新创建的临时表(与凌晨线上任务错峰执行,避免争抢资源),对数据进行校验,如果校验失败则回滚,校验成功则将数据写入线上数据表中。
- 倒排文件数后,找到需要治理的表;
- 通过上述方法在任务中治理;
- 点击操作-小文件,每日扫描去合并小文件,对于分区多的表(例如:分区数大于1年以上),可以60天/90天等定期清理;
3.2 任务运维
等同于日常的离线任务运维,可看合并小文件任务执行情况。
3.3 数据趋势
是指任务中文件总数优化趋势,合并任务功能图:
四、治理效果
(1)对小文件数量高于xx+以上的数仓表进行治理,总计x+张,小文件总数由xxx万下降至xx万(小文件治理(定期治理,60天/90天)),优化率xx%。
注:文章参考:
[离线计算-Spark|Hive] HDFS小文件处理-腾讯云开发者社区-腾讯云
hive 处理已经存在的小文件方案-腾讯云开发者社区-腾讯云