一、问题描述
为了支撑相应的业务需求,本次生产环境通过Hive SQL来完成动态插入分区表数据的脚本开发。但是,动态分区的插入往往会伴随产生大量的小文件的发生。而小文件产生过多的影响主要分为以下两种情况:
(1) 从Hive的角度看,小文件会开很多map,一个map开一个JVM去执行,所以这些任务的初始化,启动,执行会浪费大量的资源,严重影响性能。
(2)在HDFS中,每个小文件对象约占150byte,如果小文件过多会占用大量内存。这样NameNode内存容量严重制约了集群的扩展。 所以,我们必须要解决上述小文件过多的情况,可以从输入、中间过程和输出来入手进行优化。
二、知识概括
1.简要描述动态分区 所谓的动态分区,就是对分区表insert数据时候,数据库自动会根据分区字段的值,将数据插入到相应的分区中,Hive也提供了类似的机制,即动态分区。
·在PARTITION (month,day)中指定分区字段名即可;
·在SELECT子句的最后两个字段,必须对应前面PARTITION (month,day)中指定的分区字段,包括顺序。
[注意] hive使用动态分区,需要进行相应的配置。
2. 动态分区相关配置项介绍
● hive.exec.dynamic.partition
○ 默认值:
false
○ 是否开启动态分区功能,默认为关闭状态
○ 使用动态分区,必须将该参数设置为
true
● hive.exec.dynamic.partition.mode
○ 默认值为:strict
○ 动态分能不能将数据均匀的分配呢?可以!我们可以使用DISTRIBUTE BY rand()控制在map端如何拆分数据给reduce端的,hive会根据distribute by后面列,对应reduce的个数进行分发,默认采用的是hash算法。rand()方法会生成一个
0
~
1
之间的随机数[rand(
int
param)返回一个固定的数值],通过随机数进行数据的划分,因为每次都随机的,所以每个reducer上的数据会很均匀。将数据随机分配给Reduce,这样可以使得每个Reduce处理的数据大体一致区的模式,默认为:strict,表示必须指定至少一个分区为静态分区。nonstrict模式表示允
许所有的分区字段都可以动态分区。
○ 一般需要设置为nonstrict
● hive.exec.max.dynamic.partitions.pernode
○ 默认值为:
100
○ 在每个执行的MR任务的节点上,最大可以创建多少个动态分区
○ 该参数根据实际数据情况来定
○ 比如:源数据中包含了一年的数据,即day字段有
365
个值,那么该参数就需要设置成大于
365
,如果使用默认值
100
,则会报错。
● hive.exec.max.dynamic.partitions
○ 在所有执行MR的节点上,最大一共可以创建多少个动态分区
○ 一般默认值足够,除非你的数据量非常大,需要创建的文件数大于
100000
,可以根据实际情况来调整
● hive.exec.max.created.files
○ 默认值为:
100000
○ 整个MR job中,最大可以创建多少个hdfs文件
○ 一般默认值足够,除非你的数据量非常大,需要创建的文件数大于
100000
,可以根据实际情况来调整
● hive.error.on.empty.partition
○ 默认值为:
false
○ 当有空分区生成时,是否抛出异常
○ 一般不需要设置
3.产生小文件的原因:
Spark程序以动态分区的方式写入Hive表时,会出现了大量的小文件,导致最后移动文件到hive表目录非常耗时,这是因为在Shuffle时Hive多个分区的数据随机落到Spark的多个Task中,此时Task与Hive分区数据的关系是多对多,即每个Task会包含多个分区的部分数据,每个Task中包含的每个分区的数据都很少,最终会导致Task写多个分区文件,每个分区文件都比较小。
为了减少小文件的数量,需要将数据按照分区字段进行Shuffle,将各个分区的数据尽量各自集中在一个Task,在Spark SQL中就是通过distribute by 关键字来完成这个功能的。
当使用distribute by 关键字在后出现了数据倾斜,即有的分区数据多,有的分区数据少,也会导致spark 作业整体耗时长。需要在distribute by后面增加随机数,例如:
insert overwrite table target partition(p1,p2)select * from sourcedistribute by p1, p2, cast(rand() * N as int)
N值可以在文件数量和倾斜度之间做权衡。
在Spark SQL中使用动态分区写入数据,需要同时使用distribute by 和sort by 才能有效减少小文件数量。
4.解决办法:
distribute by:
少用动态分区,如果场景下必须使用时,那么记得在SQL语句最后添加上distribute by
假设现在有20个分区,我们可以将dt(分区键)相同的数据放到同一个Reduce处理,这样最多也就产生20个文件,dt相同的数据放到同一个Reduce可以使用DISTRIBUTE BY dt实现,所以修改之后的SQL如下:
insert overwrite table temp.wlb_tmp_smallfile partition(dt)
select * from process_table
DISTRIBUTE BY dt;
修改完之后的SQL运行良好,并没有出现上面的异常信息,但是这里也有个问题,这20个分区目录下每个都只有一个文件!!这样用计算框架(MR/Spark)读取计算时,Mapper/Task数量根据文件数而定,并发度上不去,直接导致了这个SQL运行的速度很慢
如果想要具体最后落地生成多少个文件数,使用 distribute by cast( rand * N as int) 这里的N是指具体最后落地生成多少个文件数,那么最终就是每个分区目录下生成7个 文件大小基本一致的文件。修改的SQL如下
insert overwrite table temp.wlb_smallfile partition(dt)
select * from process_table
distribute by cast(rand()*
7
as
int
);
合理调整Reduce端的读取的文件的大小:
-- 在 map only 的任务结束时合并小文件
set hive.merge.mapfiles =
true
;
-- 在 MapReduce 的任务结束时合并小文件
set hive.merge.mapredfiles =
true
;
-- 作业结束时合并文件的大小
set hive.merge.size.per.task =
256000000
;
-- 每个Map最大输入大小(这个值决定了合并后文件的数量)
set mapred.max.split.size=
256000000
;
-- 每个reducer的大小, 默认是1G,输入文件如果是10G,那么就会起
10
个reducer;
set hive.exec.reducers.bytes.per.reducer=
1073741824
;
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
参考文章:Hive Distribute by 应用之动态分区小文件过多问题优化_distribute by cast(rand() * 99 as int)-CSDN博客