hive的数据倾斜解决(Map端、reduce 端 、join中)

hive的数据倾斜解决(Map端、reduce 端 、join中)

lianchaozhao 2020-11-02 15:24:08  667  收藏 4
分类专栏: 工作实践 hive 大数据 文章标签: hive 大数据
版权
hive 的数据倾斜一般我们可以分为 Map倾斜、reduce 倾斜和join 倾斜这几种。

一、Map 倾斜
Map 端是 MR 任务的起始阶段, Map 端的主要功能是从磁盘中将数据读人内存, Map 端的两个主要过程如图所示。


此处复习仪表map 过程

1、客户端将每个block块切片(逻辑切分),每个切片都对应一个map任务,默认一个block块对应一个切片和一个map任务,split包含的信息:分片的元数据信息,包含起始位置,长度,和所在节点列表等
2、客户端将每个block块切片(逻辑切分),每个切片都对应一个map任务,默认一个block块对应一个切片和一个map任务,split包含的信息:分片的元数据信息,包含起始位置,长度,和所在节点列表等
3、map函数对键值对进行计算,输出<key,value,partition(分区号)>格式数据,partition指定该键值对由哪个reducer进行处理。通过分区器,key的hashcode对reducer个数取模。
4、map将kvp写入环形缓冲区内,环形缓冲区默认为100MB,阈值为80%,当环形缓冲区达到80%时,就向磁盘溢写小文件,该小文件先按照分区号排序,区号相同的再按照key进行排序,归并排序。溢写的小文件如果达到三个,则进行归并,归并为大文件,大文件也按照分区和key进行排序,目的是降低中间结果数据量(网络传输),提升运行效率
5、如果map任务处理完毕,则reducer发送http get请求到map主机上下载数据,该过程被称为洗牌shuffle
6、可以设置combinclass(需要算法满足结合律),先在map端对数据进行一个压缩,再进行传输,map任务结束,reduce任务开始
7、reduce会对洗牌获取的数据进行归并,如果有时间,会将归并好的数据落入磁盘(其他数据还在洗牌状态)
8、每个分区对应一个reduce,每个reduce按照key进行分组,每个分组调用一次reduce方法,该方法迭代计算,将结果写到hdfs输出

在Map 端读数据时,由于读入数据文件大小分布不均匀,因此会导致有些Map Instance 读取并且处理的数据特别 多,而有些 Map Instance 处理的数据特别少,造成 Map 端长尾。以下两种情况可能会导Map 端长尾:
1、上游文件的大小特别不均匀,并且小文件特别多,导致当前表Map端读取的数据分布不均匀,引起长尾。
2、Map端做聚合时,由于某些Map Instance 读取文件的某个值特别多而引起长尾,主要是指Count Distinct 操作。
方案
第一种情况导致的 Map 端长尾,可通过对上游合并小文件,同时调节本节点的小文件的参数来进行优化,即通过设置set mapred.max.split.size=256000000; --决定每个map处理的最大的文件大小,单位为B。
第二种情况,使用distribute by rand(),来打乱数据分布,使数据尽可能分布均匀。(这种情况 比较适合没有去重操作或笛卡尔积join情况,应用较少)

二、join 的倾斜
join操作需要我们参与Map 和Reduce的整个阶段,首先我们通过一段join 的SQL 来看整个个 Map Reduce 阶段的执行过程以及数据的变化,进而对 Join 的执行原理有所了解。

假设有下面的一段 join 的SQL

通过上面执行过程可以看出,在join执行阶会将 Join Key 相同的数据分发到同一个执行 Instance 上处理 。如果某个Key 上的数据量比较大,则会导致该 Instance 执行时间较长。其表现为:在执行日志中该 Join Task 的大部分 Instance 都已执行完成,但少数几Instance 一直处于执行中(这种现象称之为长尾)。
因为数据倾斜导致长尾的现象比较普遍,严重影响任务的执行时间,尤其是在电商大型活动期间,长尾程度比平时更严重。比如某些大型店铺的 PV 远远超过一般店铺的 PV ,当用浏览日志数据和卖家维表关联时,会按照卖家 ID 进行分发,导致某些大卖家所在Instance 处理的数据量远远超过其他 Instance ,而整个任务会因为这个长尾的 Instance 迟迟无法结束。
类似上面的情况 在执行JOIN阶段主要常见三种倾斜场景。
1、join 的某路输入比较小,可以采用mapJOin,将小表放到内存中,避免分发的长尾。
2、JOin的每路输入都较大,且长尾是空值导致的,可以将空值处理成随机值,避免聚集。
3、JOIN 的每路输入都较大,且长尾是热点值导致的,可以对热点值和非热点值分别进行处理,再合并数据。

第一种方案:我们一般可以通过yarn 的任务管理页面中,找到任务分配流程图,确定每一个Map 读取数据量的大小。确定是不是第一种情况。即采用MapJoin 的方案, 具体可以通过配置解决 参考自己以前文章:
https://editor.csdn.net/md/?articleId=99551151

第二种方案:JOIN 因为空值导致长尾
数据表中经常出现空值的数据,如果关联 key 为空值且数据量比较大, oin 时就会因为空值的聚集导致长尾 ,针对这种情况可以将空值处理成随机值。因为空值无法关联上,知识分发到一处,因此处理成随机值不会影响关联的结果,也能很好的避免空值聚焦导致的长尾。例如:

select .......from  
table_a  
left outer join 
table_b  
on coalesce(table_a.key,rand()*9999) =table_b.key
--coalesce 用方当table_a.key 为空时候用随机值代替
COALESCE是一个函数, (expression_1, expression_2, ...,expression_n)依次参考各参数表达式,遇到非null值即停止并返回该值。如果所有的表达式都是空值,最终将返回一个空值。



第三种 Join 因为热点值导致长尾。
如果是因为热点值导致的长尾,并且 Join 的输入比较大无法使用MapJoin ,则可以先将热点key取出,对于主表数据用热点key切分成热点数据和非热点数据两部分分别处理,最后合并,这里以某宝的Pv日志表关联商品维表去商品属性为例子介绍。
其具体步骤为:
(1)、取热点key:将Pv 大于50000的商品ID抽取到临时表中。

INSERT OVERWRITE TABLE topk_item 
SELECT iter_id 
FROM 
SELECT iter_id 
,count(l) as cnt 
FROM pv --pv
WHERE ds = ’ ${ bizdate } ’ 
AND url type = ’ ipv ’ 
AND item id is not null 
GROUP BY item_id 
) a 
WHERE cnt >= 50000



(2)、取出非热点数据。
将主表(pv表)和热点key表(topk_item表)外关联后,通过条件“ bl .item_id is null ,,取 关联不到的数据即非热点商品的日志数据 ,此时需要使用 MapJoin 。再用非热点数据关联商品维表,因为已经排除了热点数据,所以不会存在长尾。

SELECT ... 
FROM 
--商品表
(SELECT * 
FROM item --商品表
WHERE ds =’${ bizdate }’ 
) a 
RIGHT OUTER JOIN 
--非热点数据的日志数据
(SELECT /*+MAPJOIN(bl)*/ 
b2 . * 
FROM 
(
SELECT item
FROM topk_item 一热点表
WHERE ds = ’ ${ bizdate }’ 
) bl 
RIGHT OUTER JOIN 
SELECT
FROM pv --pv
WHERE ds = ’ ${ bizdate } ’ 
AND url_type = ’ ipv ’ 
) b2 
ON bl.item_id= coalesce(b2.item id, concat (” tbcdm” ,rand() ) 
WHERE bl. item id is null 
) l
ON a.item_id= coalesce(l.item_id, concat (” tbcdm”, rand() )



(3)取出热点数据。
将主表(pv表)和热点key表(topK_item表)内关联,此时需要使用MapJoin,取到热点商品的日志数据,同时需要经商品维表(item表)和热点 key topk item )内 联,取到热 商品的维 数据然后将第一部分数据外 第二部分数据,因为第 部分数据只有热点商品的维表 数据量 小,可以使用 apJoin 避免长尾。

SELECT /*+MAPJOIN (a ) */ 
FROM 
(
SELECT /*+MAPJOIN (bl)*/ 
b2 . * 
FROM 
(
SELECT tern id 
FROM topk_item 
WHERE ds = ’ ${ bizdate } ’ 
) bl 
JOIN 
(
SELECT * 
FROM pv -- pv
WHERE ds = ’ ${ bizdate } ’ 
AND url type pv
AND item id is not null 
) b2 
ON (bl.item_id = b2.item_id) 
) 1 
LEFT OUTER JOIN 
(
SELECT /*+MAPJOIN (al ) */ 
a2
FROM 
(
SELECT item id 
FROM t opk item 
WHERE ds =‘¥ bizda }’ 
) al 
JOIN
(
SELECT * 
FROM item --商品表
WHERE ds =’ ${ bizdate } ’ 
) a2 
ON (al.item——id = a2.item_id) 
)a 
ON a .item_id = l.item_id



将上面取到的非热点数据和热点数据通过“union all ”合并后即得到完整的日志数据,并且关联了商品信息。

针对此类数据倾斜问题,hive提供了专门的参数用来解决长尾问题,如下所示。参考自己以前文章:
https://editor.csdn.net/md/?articleId=99551151**

第四种:因为不同数据类型导致数据倾斜
不同数据类型字段关联,比如服务日志表info_id 为string 类型,商品表info_id 为bigint,产生数据倾斜。

select count(a.info_id),count(b.info_id) 
from t_info_all a  --商品表
 left join
 (select get_json_object(t.datapool,'$.infoid') as info_id 
 from t_log_action t   --服务日志表
where t.date='2017-08-15'  and t.action='visit' group by get_json_object(t.datapool,'$.infoid')) b 
on (a.info_id=b.info_id) 
where a.date='2017-08-15'


导致原因是默认日志表中的info_id 转成数字id 做hash 来分配reduce ,所以日志表商品就会到一个reduce 中导致数据倾斜

解决方式join 时候把其转化为string 类型:

select count(a.info_id),count(b.info_id) 
from t_info_all a --商品表
left join
 (select get_json_object(t.datapool,'$.infoid') as info_id
  from t_log_action t  --服务日志表
where t.date='2017-08-15' and t.action='visit' group by 
get_json_object(t.datapool,'$.infoid')) b on **(cast(a.info_id as string)=b.info_id**) where a.date='2017-08-15'



二、Reduce 的倾斜
reduce 端负责的是将Map 端梳理后的有序Key-value键值对进行聚合,即进行count、sum、Avg等聚合操作,得到最终聚合的结果。
Distinct用于对字段去重。比如计算在某个时间段内支付买家数、访问 UV 等,都是需要用 Distinct进行去重的。Distinct的执行原理是将需要去重的字段以及Gro up By 宇段联合作为 key 将数据分发到 Reduce 端。
因为 Distinct 操作,数据无法在 Map 端的 Shuffle 阶段根据 Group By 先做一次聚合操作,以减少传输的数据量,而是将所有的数据都传输到Reduce 端,当 key 的数据分发不均匀时,就会导致 Reduce 端长尾。
Reduce 端产生长尾的主要原因就是 key 的数据分布不均匀。比如有些 Reduce 任务 Instance 处理的数据记录多,有些处理的数据记录少,造成 Reduce 端长尾 。如下几种情况会造成 Reduce 端长尾:
(1)、对同一个表按照维度对不同列进行count Distict 操作,造成Map 端数据膨胀,从而使得下游的Join 和Reduce 出现链路上的长尾。
(2)、Map端直接做聚合时出现Key值分布不均匀,造成Reduce端长尾。
(3)、动态分区数过多造成小文件过多。从而引起Reduce 端长尾。
(4)、多个 Distinct 同时出现在 SQL 代码中时,数据会被分发多次,
不仅会造成数据膨胀 倍,还会把长尾现象放大 倍。

方案
对于上面提到的第二种情况,可以对热点 key 进行单独处理,然后通过“ Union All ”合并。这种解决方案已经在“Join 倾斜”一节中介绍过。
对于上面提到的第三种情况,可以把符合不同条件的数据放到不同的分区,避免通过多长“Insert Overwrite ,写人表中,特别是分区数比较多时,能够很好地简化代码。但是动态分区也有可能带来小文件过多的困扰。以最简单sql为例子

INSERT OVERWRITE TABLE part test PARTITION(ds) 
SELECT * 
FROM part test;



假设有K个Map Instance, 个目标分区:那么在最坏的情况下,可能产生 KxN 个小文件,而过多的小文件会对文件系统造成巨大的管理压力,对动态分区的处
理是引人额外一级的 Reduce Task ,把相同的目标分区交由同 个(或
少量几个) Reduce Instance 来写人,避免小文件过多,并且这个 Reduce
肯定是最后一个 Reduce Task 操作。 MaxCompute 是默认开启这个功能
的,也就是将下面参数设置为 true

可以用如下参数解决生产过多小文件的问题

set hive.merge.mapfiles=true 在Map-only的任务结束时合并小文件
set hive.merge.mapredfiles=true 任务执行完了以后是否执行合并文件,默认false 参考值:true
set hive.merge.size.per.task=512000000 合并后每个文件的大小,默认256M 参考值:512M
set hive.merge.smallfiles.avgsize=50000000 当输出文件的平均大小小于该值时,启动一个独立的map-reduce任务进行文件,默认16M 参考:50



第四种情况解决方案为:

上图这段代码是在7天,30天等时间范围内,分pc端、无线端、所有终端,计算支付买家数和支付商品数其中支付买家数和支付商品数指标需要去重。因为需要根据日期、终端等多种条件组合对买家和商品进行去重计算,因此有 12 Count Distinct 计算。在计算过程中会根据 12 个组合 key 分发数据来统计支付买家数和支付商品数。这样做使得节点运行效率变低。

针对上面的问题,可以先分别进行查询,执行 Gro up By 原表粒度+ uyer_id ,计算出 PC 端、无线端、所有终端以及在 天、 30 天等统计口径下的 buyer_id (这里可以理解为买家支付的次数)。然后子查询外Group By 原表粒度,当上一步的count 值大于0 时,说明这一买家在这个统计口径下有过支付,计入支付买家数,否则不计入。计算支付商品数采用同样的处理方式。最后对支付商品数和支付买家数进行Join 操作。

SELECT t2 . seller id 
, t2.price_seg_id 
, SUM (case when pay ord byr cnt lw 001>0 then 1 else 0 d)
AS pay ord byr cnt lw 001 一最近 天支付买家数
, SUM (case when pay ord byr cnt lw 002>0 then 1 else 0 end) 
AS pay_ord_byr_cnt_lw_002 一最近 PC 端支付买家数
, SUM (case when pay ord byr cnt lw 003>0 then 1 else 0 eηd) 
AS pay_ord_byr_cnt_lw_003 最近 天无线端支付买家数
, SUM (case when pay ord byr cnt lm 002>0 then 1 else 0 end)
AS pay ord byr cnt lm 002 一最近 30 天支付买家数
, SUM (case when pay ord byr cnt lm 003>0 then 1 else 0 end) 
AS pay ord byr lm 003 一最近 30 PC 端支付买家数
, SUM (case when pay ord byr cnt lm 004>0 then 1 else 0 end) 
AS pay ord byr cnt lm 004 最近 30 天无线端支付买家数
from 
(
SELECT al . seller id 
30 天支付买家数
, a2 . price seg id 
,buyer
, COUNT(buyer_id) AS pay_ord_byr_cnt_lm_002 一最近
,COUNT(CASE WHEN is_wireless = ’ N ’ THEN buyer_id 
ELSE NULL END) AS pay ord_byr_cnt_lm_003 一最近 30 PC 端支付买家
, COUNT(CASE WHEN 工 S wireless = ’ Y ’ THEN buyer id 
ELSE NULL END) AS pay ord byr cnt lm 004 一最近 30 天无线端支付买家
, COUNT(case 
when al.ds>=TO CHAR(DATEADD(TO DATE 
(’${ bizdate }’,’ yyyymmdd ’) , - 6,’dd ’ ),’ yyyymmdd ’ ) then buyer id 
else null 
end) AS pay_ord_byr_cηt_lw 001 一最近 天支
付买家数
, COUNT(CASE WHEN al . ds>=TO CHAR (DATEADD(TO DATE 
(’♀{ bizdate }’,’ yyyymmdd ’) , - 6, ’ dd ’),’ yyyymmdd ’) and 
is wireless =’N ’ THEN buyer id 
ELSE NULL 
END) AS pay ord byr cnt lw 002 一最近 PC
端支付买家数
, COUNT(CASE WHEN al . ds>=TO CHAR(DATEADD(TO DATE 
('${bizdate }勺’ yyyymmdd ’) , - 6, ’ dd ’),’ yyyymmdd ’ ) and is wireless = ’ Y’ THEN buyer_id 
ELSE NULL 
END) AS pay ord byr cnt lw 003 最近 天无
线端支付买家数
 
 from(
 select * 
from table pay 
)al 
JOIN ( SELECT item id 
, price_seg_id 
FROM tag itm 
WHERE ds = ’ ${ bizdate ) ’ 
) a2 
--
商品 tag
ON ( al. item id = a2. item id ) 
GROUP BY al . seller id 一原表粒度
, a2 . price seg id 原表粒度
, buyer id 
) t2

GROUP BY t2 . seller id 原表粒度
,t2 . price seg id; 原表粒度



经测试,修改后的运行时间为 13min 后的效果还是非常的。可以看到和 Count Distinct 计算方式相比数据没有膨胀,约为原方式的 1/ 10。

综上:
1、上述方案中如果出现多个需要去重的指标,那么在把不同指标
Join 一起之前, 一定要确保指标的粒度是原始表的数据粒度。比如支付买家数和支付商品数,在子查询中指标粒度分别是:原始表的数据粒度+ buyer_id 和原始表的数据粒度 item_id ,这时两个指标不是同一数据粒度,所以不能 Join ,需要再套一层代码,分别把指标 Group By 到“原始表的数据粒度”,然后再进行 Join操作。

2、修改前 Multi Distinct 代码的可读性比较强,代码简洁,便于维护;修改后的代码较为复杂。当出现的 Distinct 个数不多、表的数据量也不是很大、表的数据分布较均匀时,不使用 MultiDistinct 的计算效果也是可以接受的。所以,在性能和代码简洁、可维护之间需要根据具体情况进行权衡。另外,这种代码改动还是比较大的,需要投入一定的时间成本,因此可以考虑做成自动化,通过检测代码、优化代码自动生成将会更加方便。

3、当代码 比较膝肿时,也可以将上述子查询落到中间表里,这样数据模型更合理、复用性更强、层次更清晰。当需要去除类似的多Distinct 时,也可以查一下是否有更细粒度的表可用,避免重复计算。

目前Reduce 端数据倾斜很多是由 Count distinct 问题引起的,因此在ETL 开发工作中应该重视 Count Distict 问题,避免数据膨胀。对于一些表的Join 阶段的N值问题,应该对表的数据分布要有清楚认识,在开发时解决这个问题。

三、group by 的数据倾斜

类如在电商业务中会遇到统计每个商品浏览的uv

select get_json_object(a.datapool,’$.infoid’)
,count(distinct a.token) 
from *****log_t  a
 where a.date='2017-08-17'
and a.action='visit' 
group by get_json_object(a.datapool,’$.infoid’);

在存在某几个商品浏览用户热点问题时可以修改sql 语句
(实际底层逻辑是增加一次shuffer 过程来避免某个reduce 处理出现热点问题)

select b.info_id,count(1) from
 (select get_json_object(a.datapool,’$.infoid’) as info_id,
 a.token 
from   *****log_t a
 where a.date='2017-08-17'  and a.action='visit' group by get_json_object(a.datapool,’$.infoid’),a.token) b 
group by b.info_id


————————————————
版权声明:本文为CSDN博主「lianchaozhao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_40809627/article/details/109449780

猜你喜欢

转载自blog.csdn.net/someInNeed/article/details/117992175