Map阶段的优化
作业会通过input的目录产生一个或多个map任务。
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),那么会拆分,如果小于块大小,则把该文件当成一个块。
如果小文件数量非常多,而且本身的处理逻辑时间远小于map任务的启动和初始化时间,可通过下面的参数来控制map的数量
mapred.min.split.size:指的是数据的最小分割单元大小;min的默认值是1B
mapred.max.split.size:指的是数据的最大分割单元大小;max的默认值是256MB
通过调整max可以起到调整map数的作用,减小max可以增加map数,增大max可以减少map数。
需要提醒的是,直接调整mapred.map.tasks这个参数是没有效果的。
map数量越多越好吗?是不是保证每个map处理接近文件块大小?
这得根据情况来分析,假设:
有一张窄表,大小为120M,300w条数据,表里有2个字段(user_id,age)
另一张宽表,大小为12M,1w条数据,但是表字段有30多个
两个文件大小差不多,但是map处理数据量差距却非常大。第一张表处理逻辑如果很复杂,一个map去处理的话肯定更耗时了。
这时需要根据情况来减少或增加map数量
- 减少map数量
当一个任务中,input文件数量很多,但是都远小于128M时,比如有200个文件,就会启动200个map任务,但是花在处理逻辑上的时间不多,大多都是花在map的启动和初始化上的话可以增大合并文件的大小来减少map数量
通过以下方法来在map执行前合并小文件,减少map数:
set mapred.max.split.size=128000000; -- 每个Map最大输入大小
set mapred.min.split.size.per.node=100000000; -- 一个节点上map最小分割,决定结点间是否合并
set mapred.min.split.size.per.rack=100000000; -- 一个机架下map最小分割,决定机架间是否合并
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; -- 执行Map前进行小文件合并, 合并的大小由`mapred.max.split.size`参数决定
100000000表示100M, 前面三个参数确定合并文件块的大小,大于文件块大小128m的,按照128m来分隔,小于128m,大于100m的,按照100m来分隔,把那些小于100m的(包括小文件和分隔大文件剩下的),进行合并
-
增加map数量
当input的文件都很大,任务逻辑复杂,map执行非常慢的时候,可以考虑调整以上参数增加map数量
来使得每个map处理的数据量减少,从而提高任务的执行效率。 -
map端聚合
hive.map.aggr=true
相当于MR中的Combiners,默认为true。在Map端做combiner,假如map各条数据基本上不一样, 聚合没什么意义,做combiner反而画蛇添足,hive里也考虑的比较周到通过参数:
hive.groupby.mapaggr.checkinterval = 100000(默认)
hive.map.aggr.hash.min.reduction = 0.5(默认)
预先取100000条数据聚合,如果聚合后的条数/100000>0.5,则不再聚合
Reduce阶段优化
reduce优化比map优化更重要
hive是如何确定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任务;
hive.exec.reducers.bytes.per.reducer
这个设置在数据倾斜中会用到,在出现数据倾斜时,一个reduce处理不了的可以分成多个reduce来处理,但是需要将reduce处理完的结果进行汇总。
- 调整reduce数量的方法
set hive.exec.reducers.bytes.per.reducer=500000000; (按大小 每个500M)
set mapred.reduce.tasks = 15; (直接设定reduce数量)
reduce数量不是越多越好
同map一样,启动和初始化reduce也会消耗时间和资源;
另外,有多少个reduce,就会有多少个输出文件,如果生成了很多个小文件,那么如果这些小文件作为下一个任务的输入,则也会出现小文件过多的问题;
什么情况下只有一个reduce
很多时候你会发现任务中不管数据量多大,不管你有没有设置调整reduce个数的参数,任务中一直都只有一个reduce任务;其实只有一个reduce任务的情况,除了数据量小于hive.exec.reducers.bytes.per.reducer
参数值的情况外,还有以下原因:
- 没有group by的汇总
比如把select dt, count(1) from table where dt = '2019-09-14' group by dt;
写成select count(1) from table where dt = '2019-09-14';
- 用了order by
order by进行全局统计只有一个reduce,应注意数据量过大的时候,会导致任务执行太慢(可以用 distribute by分区排序或者 sort by单个reduce数据排序) - 有笛卡尔积
join的时候不加on条件或者无效的on条件,Hive只能使用1个reducer来完成笛卡尔积--无效的on条件 --比如查询用户对于同一件商品的购买行为记录,但是没有关联product_id,这时就会根据user_id进行笛卡尔积查询出所有商品 select user_id,product_id from ( select user_id,product_id,buy from table1 )t1 join ( select user_id,product_id,cart from table2 )t2 where/on t1.user_id = t2.user_id --and t1.product_id = t2.product_id
Hive的优化
-
分区裁剪(partition)
where中的分区条件会提前生效,不必特意做子查询,直接join和groupBy -
MapJoin
MapJoin简单说就是在Map阶段将小表读入内存,顺序扫描大表完成Join
上图是Hive MapJoin的原理图,出自Facebook工程师Liyin Tang的一篇介绍Join优化的slice,从图中可以看出MapJoin分为两个阶段:
1.通过MapReduce Local Task,将小表读入内存,生成HashTableFiles上传至Distributed Cache中,这里会对HashTableFiles进行压缩。
2.MapReduce Job在Map阶段,每个Mapper从Distributed Cache读取HashTableFiles到内存中,顺序扫描大表,在Map阶段直接进行Join,将数据传递给下一个MapReduce任务。在Hive v0.7之前,需要使用hint提示 /*+ mapjoin(table1) */ 才会执行MapJoin。Hivev0.7之后的版本已经不需要给出MapJoin的指示就进行优化。它是通过如下配置参数来控制的(此处的table1就是小表):
set hive.auto.convert.join = true;
Hive还提供另外一个参数——表文件的大小作为开启和关闭MapJoin的阈值。
hive.mapjoin.smalltable.filesize = 25000000 即25M
小表的数据不要超过1G或者50万条记录,因为小表会加载到内存中,查询的时候,不同节点在拉取内存中的数据做查询时(两个节点内存中存的数据可能是其中的一部分,当需要另一部分数据的时候需要从别的节点上拉取过来),数据量少可以减少网络传输上的消耗。加快查询速度,在map端进行join避免了shuffle
-
Union all/distinct
先做union all再做join或group by等操作可以有效减少MR过程,尽管是多个Select,最终只有一个mr -
Automatic merge
当文件大小比阈值小时,hive会启动一个mr进行合并
hive.merge.mapfiles = true
是否合并Map输出文件,默认为True
hive.merge.mapredfiles = false
是否合并Reduce输出文件,默认为False
hive.merge.size.per.task = 256 * 1000 * 1000
合并文件的大小 -
Multi-Count Distinct
决定 group by 操作是否支持倾斜数据。必须设置参数:
set hive.groupby.skewindata = true
例如双十一数据量特别大,如下这种根据日期group by的写法很可能会导致数据倾斜,设置这个参数的目的是:
控制生成两个MR Job,第一个MR Job Map的输出结果随机分配到reduce中减少某些key值条数过多某些key条数过小造成的数据倾斜问题。
在第一个 MapReduce 中,map 的输出结果集合会随机分布到 reduce 中,每个reduce 做部分聚合操作,并输出结果。这样处理的结果是,相同的 Group By Key 有可能分发到不同的reduce中,从而达到负载均衡的目的;
第二个 MapReduce 任务再根据预处理的数据结果按照 Group By Key 分布到 reduce 中(这个过程可以保证相同的 Group By Key 分布到同一个 reduce 中),最后完成最终的聚合操作。select dt, count(distinct uniq id), count(distinct ip) from ods_log group by dt;
-
并行执行
同步执行hive的多个阶段,hive在执行过程, 将一个查询转化成一个或者多个阶段。 某个特定的job可能包含众多的阶段,而这些阶段可能并非完全相互依赖的,也就是说可以并行执行的,这样可能使得整个job的执行时间缩短。
hive执行开启:set hive.exec.parallel = true
-
JVM重用
正常情况下,MapReduce启动的JVM在完成一个task之后就退出了,但是如果任务花费时间很短,又要多次启动JVM的情况下(比如对很大数据量进行计数操作),JVM的启动时间就会变成一个比较大的overhead。在这种情况下,可以使用jvm重用的参数:set mapred.job.reuse.jvm.num.tasks=5; // 一个jvm完成多少个task之后再退出
数据倾斜处理
- 原因
- key值分布不均导致的
- 人为的建表疏忽,如表设计时有地区,年龄等非必填字段时,很多用户选择不填,会出现大量有默认值的数据,如null,-1等
- 业务的数据特点
- 症状
- 任务进度长时间维持在99% (或100%),查看任务监控页面,发现只有少量(1个或几个) reduce子任务未完成。
- 查看未完成的子任务,可以看到本地读写数据量积累非常大,通常超过10GB可以认定为发生数据倾斜。
- 倾斜度
- 平均记录数超过50w且最大记录数是超过平均记录数的4倍。
- 最长时长比平均时长超过4分钟,且最大时长超过平均时长的2倍。
- 定位
出现数据倾斜时,对数据进行分桶取样,看哪个key的数量远大于其他key的数量 - 万能方法
set hive.groupby.skewindata = true
控制生成两个MR Job,第一个MR Job Map让key随机分发到reducer,而不是同样的key分发到同一个reducer,然后reduce做聚合,减少某些key值条数过多某些key条数过小造成的数据倾斜问题。把map端聚合放到了reduce端,增加了reducer新的开销,大多数情况效果并不好。
大小表关联
- 原因
Hive在进行join时,按照join的key进行分发,而在join左边的表的数据会首先读入内存,如果左边表的key相对分散,读入内存的数据会比较小,join任务执行会比较快而如果左边的表key比较集中,而这张表的数据量很大,那么数据倾斜就会比较严重,而如果这张表是小表,则还是应该把这张表放在join左边。 - 思路
- 将key相对分散,并且数据量小的表放在join的左边,这样可以有效减少内存溢出错误发生的几率
- 使用map join让小的维度表先进内存。
- 方法
Small_table join big_table
大大表关联
- 原因
日志中有一部分的user_id是空或者是0的情况,导致在用user_id进行hash分桶的时候,会将日志中user_id为0或者空的数据分到一起,导致了过大的斜率。 - 思路
把空值的key变成一个字符串加上随机数,把倾斜的数据分到不同的reduce上,由于null值关联不上,处理后并不影响最终结果。 - 方法
on case when (x.uid = '-' or x.uid= '0' or x.uid is null) then concat('-',rand()) else x.uid end = table2.user id;
大大表关联(业务削弱)
- 案例
Select * from dw_log t join dw_user t1 on t.user_id = t1.user_id
- 现象:两个表都上千万,跑起来很悬
- 思路
当天登录的用户其实很少 - 方法
select /*+ MAPJOIN(t12)*/ * from dw_log t11 join ( select /*+ MAPJOIN(t)*/ t1.* from ( select distinct user_id from dw_log --group by user_id ) t join dw_user t1 on t.user_id = t1.user_id ) t12 on t11.user_id = t12.user_id
聚合时存在大量特殊值
- 原因:
做count distinct时,该字段存在大量值为NULL或空的记录。 - 思路:
- count distinct时,将值为空的情况单独处理,如果是计算count distinct,可以不用处理,直接过滤,在最后结果中加1;
- 如果还有其他计算,需要进行group by,可以先将值为空的记录单独处理,再和其他计算结果进行union;
- 方法:
select cast(count(distinct user id) +1 as bigint) as user_cnt from tab_a where user id is not null and user id <> ''
参考:
https://blog.youkuaiyun.com/yu0_zhang0/article/details/81776459
https://www.jianshu.com/p/5d9a1fd869a8