背景

Hive query将运算好的数据写回hdfs(比如insert
into语句),有时候会产生大量的小文件,如果不采用CombineHiveInputFormat就对这些小文件进行操作的话会产生大量的map
task,耗费大量集群资源,而且小文件过多会对namenode造成很大压力。所以Hive在正常job执行完之后,会起一个conditional
task,来判断是否需要合并小文件,如果满足要求就会另外启动一个map-only job 或者mapred job来完成合并

 

一、    控制hive任务中的map数: 

1.    通常情况下,作业会通过input的目录产生一个或者多个map任务。 

主要的决定因素有: input的文件总个数,input的文件大小,集群设置的文件块大小(目前为128M, 可在hive中通过set
dfs.block.size;命令查看到,该参数不能自定义修改);

2.  
 举例:假设input目录下有1个文件a,大小为780M,那么hadoop会将该文件a分隔成7个块(6个128m的块和1个12m的块),从而产生7个map数。假设input目录下有3个文件a,b,c,大小分别为10m,20m,130m,那么hadoop会分隔成4个块10m,20m,128m,2m),从而产生4个map数。即,如果文件大于块大小(128m),那么会拆分,如果小于块大小,则把该文件当成一个块。

3.  
 map数越多越好?否定的。如果一个任务有很多小文件(远远小于块大小128m),则每个小文件也会被当做一个块,用一个map任务来完成,而一个map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。而且,同时可执行的map数是受限的。

4.  
 是不是保证每个map处理接近128m的文件块,就可以了?也不一定。比如有一个127m的文件,正常会用一个map去完成,但这个文件只有一个或者两个小字段,却有几千万的记录,如果map处理的逻辑比较复杂,用一个map任务去做,肯定也比较耗时。

解决:减少map数

    假设一个SQL任务:select brand,("hour") time_type,("2018-12-04") times,("05") as
hours,("201812") as months,("20181204") as days,count(distinct(openId))
brandCount from log_db.commodlog where create_time like "2018-12-04 05%" and
openId !='' group by brand SORT BY brand ASC;

    该任务的inputdir
 /user/hive/warehouse/log_db/commodlog/month=201812/day=20181204

    共有194个文件,其中很多是远远小于128m的小文件,总大小9G,正常执行会用194个map任务。

   Map总共消耗的计算资源: SLOTS_MILLIS_MAPS= 623,020

 

通过以下方法来在map执行前合并小文件,减少map数:

启动压缩
set hive.exec.compress.output=true; set
mapreduce.output.fileoutputformat.compress=true;  set
mapreduce.input.fileinputformat.split.maxsize=1073741824; set
mapreduce.input.fileinputformat.split.minsize=1; set
mapreduce.input.fileinputformat.split.minsize.per.node=536870912; set
mapreduce.input.fileinputformat.split.minsize.per.rack=536870912;
经过测试,这种设置可以在map阶段和并小文件,减少map的数量。

注意:在测试的时候,如果文件格式为Textfile,并且启用lzo压缩,不能生效。 rcfile
以及orc可以生效,Textfile不启用lzo压缩也可以生效。如果是新集群的话,没有历史遗留的问题的话,建议hive都使用orc文件格式,以及启用lzo压缩。 
   
set mapred.max.split.size=100000000;        set
mapred.min.split.size.per.node=100000000;        set
mapred.min.split.size.per.rack=100000000;        set
hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
        再执行上面的语句,用了74个map任务,map消耗的计算资源:SLOTS_MILLIS_MAPS=
333,500. 对于这个简单SQL任务,执行时间上可能差不多,但节省了一半的计算资源。

   大概解释一下,100000000表示100M

set
hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;这个参数表示执行前进行小文件合并,

       
 前面三个参数确定合并文件块的大小,大于文件块大小128m的,按照128m来分隔,小于128m,大于100m的,按照100m来分隔,把那些小于100m的(包括小文件和分隔大文件剩下的),

         进行合并,最终生成了74个块。

         

解决:增加map数

       
 当input的文件都很大,任务逻辑复杂,map执行非常慢的时候,可以考虑增加Map数,来使得每个map处理的数据量减少,从而提高任务的执行效率。

         假设有这样一个任务:

         Select data_desc,

                count(1),

                count(distinct id),

                sum(case when …),

                sum(case when ...),

                sum(…)

        from a group by data_desc

                 
 如果表a只有一个文件,大小为120M,但包含几千万的记录,如果用1个map去完成这个任务,肯定是比较耗时的,这种情况下,我们要考虑将这一个文件合理的拆分成多个,

                   这样就可以用多个map任务去完成。

                   set mapred.reduce.tasks=10;

                   create table a_1 as 

                   select * from a 

                   distribute by rand(123); 

                   

                 
 这样会将a表的记录,随机的分散到包含10个文件的a_1表中,再用a_1代替上面sql中的a表,则会用10个map任务去完成。

                   每个map任务处理大于12M(几百万记录)的数据,效率肯定会好很多。

    

   看上去,貌似这两种有些矛盾,一个是要合并小文件,一个是要把大文件拆成小文件,这点正是重点需要关注的地方,

   根据实际情况,控制map数量需要遵循两个原则:使大数据量利用合适的map数;使单个map任务处理合适的数据量;

二、    控制hive任务的reduce数: 

1. 如何确定reduce数: 

reduce个数的设定极大影响任务执行效率,不指定reduce个数的情况下,Hive会猜测确定一个reduce个数,基于以下两个设定:

hive.exec.reducers.bytes.per.reducer(每个reduce任务处理的数据量,默认为1000^3=1G) 

hive.exec.reducers.max(每个任务最大的reduce数,默认为1009)

计算reducer数的公式很简单N=min(参数2,总输入数据量/参数1)

即,如果reduce的输入(map的输出)总大小不超过1G,那么只会有一个reduce任务;

如:select brand,("hour") time_type,("2018-12-04") times,("05") as
hours,("201812") as months,("20181204") as days,count(distinct(openId))
brandCount from log_db.commodlog where create_time like "2018-12-04 05%" and
openId !='' group by brand SORT BY brand ASC; 总大小为9G多,因此这句有10个reduce

2.    调整reduce个数方法一: 

调整hive.exec.reducers.bytes.per.reducer参数的值;

set hive.exec.reducers.bytes.per.reducer=500000000; (500M)

select brand,("hour") time_type,("2018-12-04") times,("05") as
hours,("201812") as months,("20181204") as days,count(distinct(openId))
brandCount from log_db.commodlog where create_time like "2018-12-04 05%" and
openId !='' group by brand SORT BY brand ASC;这次有20个reduce

3.    调整reduce个数方法二; 

set mapred.reduce.tasks = 15;

select brand,("hour") time_type,("2018-12-04") times,("05") as
hours,("201812") as months,("20181204") as days,count(distinct(openId))
brandCount from log_db.commodlog where create_time like "2018-12-04 05%" and
openId !='' group by brand SORT BY brand ASC;

这次有15个reduce

4.    reduce个数并不是越多越好; 

同map一样,启动和初始化reduce也会消耗时间和资源;

另外,有多少个reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;

5.    什么情况下只有一个reduce; 

很多时候你会发现任务中不管数据量多大,不管你有没有设置调整reduce个数的参数,任务中一直都只有一个reduce任务;

其实只有一个reduce任务的情况,除了数据量小于hive.exec.reducers.bytes.per.reducer参数值的情况外,还有以下原因:

a)    没有group by的汇总.这点非常常见,希望大家尽量改写。

b)    用了Order by

c)    有笛卡尔积

通常这些情况下,除了找办法来变通和避免,我暂时没有什么好的办法,因为这些操作都是全局的,所以hadoop不得不用一个reduce去完成;

同样的,在设置reduce个数的时候也需要考虑这两个原则:使大数据量利用合适的reduce数;使单个reduce任务处理合适的数据量;

 

MR作业结束后,判断生成文件的平均大小,如果小于阀值,就再启动一个job来合并文件

set hive.merge.mapredfiles=true;

set hive.merge.mapfiles=true;

set hive.merge.smallfiles.avgsize=268435456;

 

参数解释

set hive.mergejob.maponly (默认为true)

如果hadoop版本支持CombineFileInputFormat,则启动Map-only job for merge,否则启动MapReduce
merge job,map端combine file是比较高效的做法

 

hive.merge.mapfiles(默认为true)

正常的map-only job后,是否启动merge job来合并map端输出的结果

 

hive.merge.mapredfiles(默认为false)

正常的map-reduce job后,是否启动merge job来合并reduce端输出的结果,建议开启

 

hive.merge.smallfiles.avgsize(默认为16MB)

如果不是partitioned table的话,输出table文件的平均大小小于这个值,启动merge job,如果是partitioned
table,则分别计算 <https://www.aliyun.com/>
每个partition下文件平均大小,只merge平均大小小于这个值的partition。这个值只有当hive.merge.mapfiles或hive.merge.mapredfiles设定为true时,才有效

hive.exec.reducers.bytes.per.reducer(默认为1G)

如果用户不主动设置mapred.reduce.tasks数,则会根据input directory计算出所有读入文件的input summary
size,然后除以这个值算出reduce number
reducers = (int) ((totalInputFileSize + bytesPerReducer - 1) /
bytesPerReducer); reducers = Math.max(1, reducers); reducers =
Math.min(maxReducers, reducers);
 

hive.merge.size.per.task(默认是256MB)

merge job后每个文件的目标大小(targetSize),用之前job输出文件的total size除以这个值,就可以决定merge
job的reduce数目。merge job的map端相当于identity map,然后shuffle到reduce,每个reduce
dump一个文件,通过这种方式控制文件的数量和大小
MapredWork work = (MapredWork) mrTask.getWork(); if (work.getNumReduceTasks()
> 0) { int maxReducers = conf.getIntVar(HiveConf.ConfVars.MAXREDUCERS); int
reducers = (int) ((totalSize +targetSize - 1) / targetSize); reducers =
Math.max(1, reducers); reducers = Math.min(maxReducers, reducers);
work.setNumReduceTasks(reducers); }
 

mapred.max.split.size(默认256MB)

mapred.min.split.size.per.node(默认1 byte)

mapred.min.split.size.per.rack(默认1 byte)


这三个参数CombineFileInputFormat中会使用,Hive默认的InputFormat是CombineHiveInputFormat,里面所有的调用(包括最重要的getSplits和getRecordReader)都会转换成CombineFileInputFormat的调用,所以可以看成是它的一个包装。CombineFileInputFormat
可以将许多小文件合并成一个map的输入,如果文件很大,也可以对大文件进行切分,分成多个map的输入。一个CombineFileSplit对应一个map的输入,包含一组path(hdfs路径list),startoffset,
lengths, locations(文件所在hostname list)mapred.max.split.size是一个split
最大的大小,mapred.min.split.size.per.node是一个节点上(datanode)split至少的大小,mapred.min.split.size.per.rack是同一个交换机(rack
locality)下split至少的大小通过这三个数的调节,组成了一串CombineFileSplit用户可以通过增大mapred.max.split.size的值来减少Map
Task数量

 

压缩文件的处理


对于输出结果为压缩文件形式存储的情况,要解决小文件问题,如果在Map输入前合并,对输出的文件存储格式并没有限制。但是如果使用输出合并,则必须配合SequenceFile来存储,否则无法进行合并,以下是示例:
set mapred.output.compression. type=BLOCK;
set hive.exec.compress.output= true;
set mapred.output.compression.codec=org.apache.hadoop.io.compress.LzoCodec;
set hive.merge.smallfiles.avgsize=100000000;
drop table if exists dw_stage.zj_small; create table dw_stage.zj_small
STORED AS SEQUENCEFILE as select * from dw_db.dw_soj_imp_dtl where log_dt
= '2014-04-14' and paid like '%baidu%' ;
使用HAR归档文件

 

Hadoop的归档文件格式也是解决小文件问题的方式之一。而且Hive提供了原生支持:
set hive.archive.enabled= true; set hive.archive.har.parentdir.settable= true;
set har.partfile.size=1099511627776; ALTER TABLE srcpart ARCHIVE
PARTITION(ds= '2008-04-08', hr= '12' ); ALTER TABLE srcpart UNARCHIVE
PARTITION(ds= '2008-04-08', hr= '12' );
 

如果使用的不是分区表,则可创建成外部表,并使用har://协议来指定路径。

 

结论

hive 通过上述几个值来控制是否启动merge file
job,通常是建议大家都开启,如果是一堆顺序执行的作业链,只有最后一张表需要固化落地,中间表用好就删除的话,可以在最后一个insert into
table之前再开启,防止之前的作业也会launch merge job使得作业变慢。


友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信