长尾问题(数据倾斜)
发生长尾问题的原因
在MapReduce中,Map阶段和Reduce阶段都有可能由多个节点进行分布式计算,而如果在分布式计算时,每个节点分配的任务不均衡,比如绝大多数操作分配在极少数的节点上,导致大多数任务并没有合理分解到各节点完成,整体任务效率远低于预期。
举个例子,举个例子,假如我们要计算一个SQL:
SELECT key,COUNT(*) AS cnt FROM table GROUP BY key
在shuffle的后期dispatch,相同key的数据进入同一个reduce时,如果不同key分到的数据量差异很大,就会产生长尾问题。
Map倾斜及解决
Map阶段只是负责处理逻辑切片的数据,所以一般情况下不同map之间数据规模差异不大,在Map阶段发生数据倾斜的可能性也不大,但是也有以下特殊情况会发生Map端倾斜。
小文件问题
上游表文件大小特别不均匀,并且小文件特别多,导致当前表Map端读取的数据分布不均匀,引起长尾。
问题起因
通常情况下,作业会通过input目录产生一个或多个map任务,主要决定map任务数量的三个因素是:
- input文件的总个数。
- input文件大小。
- 集群设置的文件块大小(目前默认128M,通过set dfs.block.size命令可查看到,该参数不可自定义修改);
另外,分桶表由于将数据切分成不同文件,也会影响map数量。
举例:
- 假设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),那么会拆分,如果小于块大小,则把该文件当成一个块。
问题解决
可以通过参数设置合并小文件。
以ODPS为例,在SQL代码中也可以设置参数:
//设置Map任务的Map Instance个数;
set odps.sql.mapper.merge.limit.size=64
//设置单个Map Instance读取的小文件个数
set odps.sql.mapper.split.size=256
笛卡尔积问题
常见一种使用笛卡尔积的场景是两个表关联时的模糊匹配。
问题起因
select ...
FROM
(
SELECT ca
FROM ta
) a
LEFT OUT JOIN
(
SELECT cb
FROM tb
) b
ON 1=1
WHERE a.ca rlike b.cb;
假设a是大表,那么有可能导致Map读入的文件块大小不均匀,造成Map端长尾。
问题解决
针对这种情况,可以考虑对大表使用"distribute by rand()"来打乱数据分布,使数据尽可能分布均匀:
select ...
FROM
(
SELECT ca
FROM ta
DISTRIBUTE BY rand()
) a
LEFT OUT JOIN
(
SELECT cb
FROM tb
) b
ON 1=1
WHERE a.ca rlike b.cb;
"distribute by rand()"会将Map端分发后的数据重新按随机值再进行一次分发,因此在Map阶段不再有复杂的聚合或者笛卡尔积,因此不会导致Map端长尾。
Reduce端倾斜及解决
由于过程设计Key时由于待处理的数据Key值分布不均衡,导致在shuffle时大部分数据任务集中分布在某些Reducetask上,使得Reduce端发生倾斜是常见的问题。
join倾斜
一般表通过join关联时,会以joinkey作为分区的依据,这会导致大量数据集中在少数joinkey对应的分区上。
通常把join倾斜的场景分为大表关联小表和大表关联大表两类。
大表关联小表
大表关联小表,可以采用Mapjoin避免倾斜,其原理是将Join操作提前到Map阶段执行,将小表读入内存,顺序扫描大表完成join。
hive中一般通过参数hive.mapjoin.smalltable.filesize定义小表的大小,除此之外可以强制通过代码申明将某表作为小表,读入内存:
select /*+mapjoin(b)*/ a.*,b.* from a join b on a.id=b.id;
大表关联大表
Joinkey因为空值导致长尾
数据表中经常出现空值,如果joinkey为空值且数据量大,这些空值会聚集到一个分区中,导致长尾问题,这时我们可以考虑将空值替换成随机值处理:
select ...
FROM ta
LEFT JOIN tb
ON coalesce(ta.key,rand()*9999) = tb.key
Joinkey因为非空热点值导致长尾
如果joinkey有集中的空值热点值,这些空值会聚集到少数个分区中,导致长尾问题。
处理的思路是将主表数据用热点key切分成两部分处理后合并,这里以淘宝PV日志表关联商品维表取商品属性为例:
取热点key:将PV大于50000的商品ID取出到临时表中:
INSERT OVERWRITE TABLE topk_item
select item_id
FROM
(
SELECT item_id,count(1) as cnt
FROM pv -- pv表
GROUP BY item_id
) a
WHERE cnt >= 50000
取热点数据:
select ...
from
(
select *
from item --商品表
) a
right join
(
select /*+MAPJOIN(b1)*/b2.*
from
(
select item_id
from topk_item
) b1
right join
(
select *
from pv
) b2
on b1.item_id = coalesce(b2.item_id,concat('tbcdm',rand())
) b
on a.item_id = coalesce(b.item_id,concat('tbcdm',rand())
取非热点数据:
select /*+MAPJOIN(a)*/ ...
from
(
select /*+MAPJOIN(b1)*/b2.*
from
(
select item_id
from topk_item
) b1
right join
(
select *
from pv
) b2
on b1.item_id = coalesce(b2.item_id,concat('tbcdm',rand())
) b
left join
(
select *+MAPJOIN(a1)*/a2.*
from
(
select item_id
from topk_item
) a1
join
(
select *
from item --商品表
) a2 on a1.item_id = a2.item_id
) a
on a.item_id = b.item_id
之后将上述两部分通过"union all"拼接即可。
针对倾斜问题,odps也提供专门的参数来解决:
//开启功能
set odps.sql.skewjoin=true
//设置倾斜的key以及对应的值
//其中skewed_key代表倾斜的列,skewed_value代表倾斜列上的倾斜值。
set odps.sql.skewinfo=skewed_src:(skewed_key)[("skewed_value")]
聚合倾斜
聚合倾斜可以参考上述join倾斜时热点切分,union all合并的方式处理。
动态分区导致的小文件问题
假设有K个Map Instance,N个目标分区,则最坏的情况下会产生K乘N个小文件。
ODPS的动态分区处理是引入一个额外的Reduce Task来合并同一个目标分区的小文件,默认在其以下参数:
set odps.sql.reshuffle.dynamicpt=true
当然如果目标分区少,不会出现问题,则开启这个功能会浪费计算资源,可以考虑关闭。