长尾问题是分布式计算里最常见的问题之一。主要原因是因为数据分布不均,导致各个节点的工作量不同,整个任务就需要等最慢的节点完成才能完成。
Map长尾主要原因是某些Map Instance读取的数据量相对于其他的Instance多很多。
优化方法:
- 使用统计好的中间层汇总表,减少大数据量读取;
- 行裁剪:检查代码读取的数据量是不是比自己的需求多,尽可能限制分区或者使用where条件过滤掉不需要的数据;
- 列裁剪:限制select的字段数据,尽量不用select *,列的利用率低。比如原表有100个字段,如果只用了其中1个字段,那么利用率只有1%。对于列数特别多的输入表,Map阶段处理只需要其中的某几列,可以通过在添加输入表时,明确指定输入的列,减少输入量;
- 使用union all:在所取的数据量很大的时候,可以尝试使用union all将所取的数据分开查询并集合在一起。比如要取3个月的数据,则可以分别写三段sql,每段取一个月的数据,再union all起来。
主要原因是分发键分布不均匀,存在热点数据(比如按照城市汇总用户量时,某一个城市的用户量占比超过60%,就可能出现Reduce长尾)。
优化方法:
- 使用统计好的中间层汇总表,这一点同Map长尾处理方式一样。
- 行裁剪/列裁剪:这一点也和Map长尾处理方式一样。
- 不同的列进行count distinct操作,造成map端数据膨胀,优化sql语句, 两次group by分多次处理
比如:
SELECT CITY ,COUNT(DISTINCT USER_ID) AS USER_COUNT FROM TABLE GROUP BY CITY ;
如果长尾发生,可以考虑减少聚合时的数据量,比如可以先对CITY和USER_ID进行group by作为一个子查询,再在外层对CITY进行分组计算,具体优化语句为:
SELECt CITY ,COUNT(USER_ID) AS USER_COUNT FROM ( SELECt CITY ,USER_ID FROM TABLE GROUP BY CITY ,USER_ID ) GROUP BY CITY ;Join长尾
JOIN阶段会将JOIN Key相同的数据分发到同一个Join Instance上执行处理。如果某个Key上的数据量比较多,会导致该Instance执行时间比其他Instance执行时间长。该JOIN Task的大部分Instance已执行完成,但少数几个Instance一直处于执行中,这种现象称之为Join长尾。
主要原因是表关联的键(key)分布不均匀,存在热点(本质就是关联的时候key值出现1对多的情况,这个多的部分就是热点)。
优化方法:
- 进行关联之前对数据进行去重,或者先对每个表进行分组汇总,尽量使用主键(唯一键)进行关联。
- 两表关联存在热点key:
a、大小表关联,使用Mapjoin,将小表加载到内存,直接分发到Map Instance所在机器上,在读取的阶段就做hash-join。值得注意的是Mapjoin的小表大小有限制,而且小表必须要是从表(即不能是left join的左表,也不能是right join的右表)。
比如:假设表a左关联表b,b是小表:
SELECt a.id ,b.name FROM a LEFT OUTER JOIN b ON a.id = b.id ;
转化为:
SELECt a.id ,b.name FROM a LEFT OUTER JOIN b ON a.id = b.id ;
如果大小表关联,而小表是主表,此时无法使用mapjoin,参考如下处理方式:
先将小表和大表利用MAP JOIN进行inner join,得到小表和大表的交集中间表,且这个中间表一定是不大于大表的(key倾斜程度与表的膨胀大小成正比)。然后小表再和这个中间表进行LEFT JOIN,这样操作的效果等于小表LEFT JOIN大表。
b、关联的两个表都比较大。
首先考虑去重。其次考虑将热点数据分开处理:先将主表热点Key取出,再用热点Key切分成热点数据和非热点数据两部分分别处理,最后合并。
- JOIN Key存在很多空值导致长尾:此时可以将空值处理成随机值。因为空值无法关联上,只是分发到了一处,因此给予随机值既不会影响关联也能避免聚集。
假设a表的id存在很多空值,那么我们在关联的时候可以将关联条件转化。见下:
on a.id = b.id --转化为 on coalesce(a.id,rand()*9999) = b.id