数据倾斜问题与解决

长尾问题(数据倾斜)

发生长尾问题的原因

在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任务数量的三个因素是:

  1. input文件的总个数。
  2. input文件大小。
  3. 集群设置的文件块大小(目前默认128M,通过set dfs.block.size命令可查看到,该参数不可自定义修改);
    另外,分桶表由于将数据切分成不同文件,也会影响map数量。
    举例:
  1. 假设input目录下有1个文件a,大小为780M,那么hadoop会将该文件a分隔成7个块(6个128m的块和1个12m的块),从而产生7个map数
  2. 假设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

当然如果目标分区少,不会出现问题,则开启这个功能会浪费计算资源,可以考虑关闭。

你可能感兴趣的:(数据倾斜问题与解决)