Hadoop在运行一个mapreduce job之前,需要估算这个job的maptask数和reducetask数。首先分析一下job的maptask数,当一个job提交时,jobclient首先分析job被拆分的split数量,然后吧job.split文件放置在HDFS中,一个job的MapTask数量就等于split的个数。

job.split中包含split的个数由FileInputFormat.getSplits计算出,方法的逻辑如下:

1.  读取参数mapred.map.tasks,这个参数默认设置为0,生产系统中很少修改。

2.  计算input文件的总字节数,总字节数/(mapred.map.tasks==0 ? 1: mapred.map.tasks )=goalsize

3.  每个split的最小值minSize由mapred.min.split.size参数设置,这个参数默认设置为0,生产系统中很少修改。

4.  调用computeSplitSize方法,计算出splitsize= Math.max(minSize, Math.min(goalSize, blockSize)),通常这个值=blockSize,输入的文件较小,文件字节数之和小于blocksize时,splitsize=输入文件字节数之和。

5.  对于input的每个文件,计算split的个数。

a)  文件大小/splitsize>1.1,创建一个split,这个split的字节数=splitsize,文件剩余字节数=文件大小-splitsize

b)  文件剩余字节数/splitsize<1.1,剩余的部分作为一个split

举例说明:

1.  input只有一个文件,大小为100M,splitsize=blocksize,则split数为2,第一个split为64M,第二个为36M

2.  input只有一个文件,大小为65M,splitsize=blocksize,则split数为1,split大小为65M

3.  input只有一个文件,大小为129M,splitsize=blocksize,则split数为2,第一个split为64M,第二个为65M(最后一个split的大小可能超过splitsize)

4.  input只有一个文件,大小为20M ,splitsize=blocksize,则split数为1,split大小为20M

5.  input有两个文件,大小为100M和20M,splitsize=blocksize,则split数为3,第一个文件分为两个split,第一个split为64M,第二个为36M,第二个文件为一个split,大小为20M

6.  input有两个文件,大小为25M和20M,splitsize=blocksize,则split数为2,第一个文件为一个split,大小为25M,第二个文件为一个split,大小为20M

假设一个job的input大小固定为100M,当只包含一个文件时,split个数为2,maptask数为2,但当包含10个10M的文件时,maptask数为10。

下面来分析reducetask,纯粹的mapreduce task的reduce task数很简单,就是参数mapred.reduce.tasks的值,hadoop-site.xml文件中和mapreduce job运行时不设置的话默认为1。

在HIVE中运行sql的情况又不同,hive会估算reduce task的数量,估算方法如下:

通常是ceil(input文件大小/1024*1024*1024),每1GB大小的输入文件对应一个reduce task。

特殊的情况是当sql只查询count(*)时,reduce task数被设置成1。

总结:通过map和reducetask数量的分析可以看出,hadoop/hive估算的map和reduce task数可能和实际情况相差甚远。假定某个job的input数据量庞大,reduce task数量也会随之变大,而通过join和group by,实际output的数据可能不多,但reduce会输出大量的小文件,这个job的下游任务将会启动同样多的map来处理前面reduce产生的大量文件。在生产环境中每个user group有一个map task数的限额,一个job启动大量的map task很显然会造成其他job等待释放资源。

Hive对于上面描述的情况有一种补救措施,参数hive.merge.smallfiles.avgsize控制hive对output小文件的合并,当hiveoutput的文件的平均大小小于hive.merge.smallfiles.avgsize-默认为16MB左右,hive启动一个附加的mapreducejob合并小文件,合并后文件大小不超过hive.merge.size.per.task-默认为256MB。

尽管Hive可以启动小文件合并的过程,但会消耗掉额外的计算资源,控制单个reduce task的输出大小>64MB才是最好的解决办法。

map数据计算示例:

hive> set dfs.block.size;
dfs.block.size=268435456
hive> set mapred.map.tasks;
mapred.map.tasks=2
文件块大小为256MB,map.tasks为2

在输入源是数据库的情况下,比如mysql,对于map的数量需要用户自己指定,比如

jobconf.set("mapred.map.tasks.nums",20);

如果数据源是HBase的话,map的数量就是该表对应的region数量。


一、    

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

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

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


4.    
答案也是不一定。比如有一个127m的文件,正常会用一个map去完成,但这个文件只有一个或者两个小字段,却有几千万的记录,
如果map处理的逻辑比较复杂,用一个map任务去做,肯定也比较耗时。

针对上面的问题3和4,我们需要采取两种方式来解决:即减少map数和增加map数;

如何合并小文件,减少map数? 
    假设一个SQL任务:
         Select count(1) from popt_tbaccountcopy_mes where pt = ‘2012-07-04’;
         该任务的inputdir  /group/p_sdo_data/p_sdo_data_etl/pt/popt_tbaccountcopy_mes/pt=2012-07-04
         共有194个文件,其中很多是远远小于128m的小文件,总大小9G,正常执行会用194个map任务。
         Map总共消耗的计算资源: SLOTS_MILLIS_MAPS= 623,020

         我通过以下方法来在map执行前合并小文件,减少map数:
         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(几百万记录)的数据,效率肯定会好很多。
    
   看上去,貌似这两种有些矛盾,一个是要合并小文件,一个是要把大文件拆成小文件,这点正是重点需要关注的地方,
  

二、    

1.    
 reduce个数的设定极大影响任务执行效率,不指定reduce个数的情况下,Hive会猜测确定一个reduce个数,基于以下两个设定:
 hive.exec.reducers.bytes.per.reducer(每个reduce任务处理的数据量,默认为1000^3=1G) 
 hive.exec.reducers.max(每个任务最大的reduce数,默认为999)
 计算reducer数的公式很简单N=min(参数2,总输入数据量/参数1)
 即,如果reduce的输入(map的输出)总大小不超过1G,那么只会有一个reduce任务;
 如:select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt; 
             /group/p_sdo_data/p_sdo_data_etl/pt/popt_tbaccountcopy_mes/pt=2012-07-04 总大小为9G多,因此这句有10个reduce

2.    
 调整hive.exec.reducers.bytes.per.reducer参数的值;
 set hive.exec.reducers.bytes.per.reducer=500000000; (500M)
 select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt; 这次有20个reduce
          
3.    
 set mapred.reduce.tasks = 15;
 select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt;这次有15个reduce

4.    
 同map一样,启动和初始化reduce也会消耗时间和资源;
 另外,有多少个reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;

5.    
 很多时候你会发现任务中不管数据量多大,不管你有没有设置调整reduce个数的参数,任务中一直都只有一个reduce任务;
 其实只有一个reduce任务的情况,除了数据量小于hive.exec.reducers.bytes.per.reducer参数值的情况外,还有以下原因:
 a)    没有group by的汇总,比如把select pt,count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04' group by pt; 写成 select count(1) from popt_tbaccountcopy_mes where pt = '2012-07-04';
 这点非常常见,希望大家尽量改写。
 b)    用了Order by
 c)    有笛卡尔积


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