数据倾斜就是我们在计算数据的时候,数据的分散度不够,导致大量的数据集中到了集群中的一台或者几台机器上计算,而集群中的其他节点空闲。这些倾斜了的数据的计算速度远远低于平均计算速度,导致整个计算过程过慢。
1)虽说Hive最后也是用MR来执行,但是毕竟写的内容逻辑区别很大,一个是程序,一个是 sql,因此这里稍作区分。
2)表现Hadoop中的数据倾斜主要表现在、Reduce 阶段卡在 99.99%,一直不能结束。这里如果详细的看日志或者和监控界面的话会发现:
3)Hive的数据倾斜,一般都发生在Sql中group by、join和count distinct 上,而且和数据逻辑绑定比较深。
函数 | 情形 | 后果 |
---|---|---|
Join | 其中一个表较小,但是key集中 | 分发到某一个或几个Reduce上的数据远高于平均值 |
Join | 大表与大表,但是分桶的判断字段0值或空值过多 | 这些空值都由一个reduce处理,灰常慢 |
group by | group by 维度过小,某值的数量过多 | 处理某值的reduce灰常耗时 |
Count Distinct | 某特殊值过多 | 处理此特殊值的reduce耗时 |
Spark 中的数据倾斜也很常见,这里包括Spark Streaming 和Spark Sql,表现主要有下面几种:
需要注意的是,在Spark streaming 程序中,数据倾斜更容易出现,特别是在程序中包含一些类似sql的join、group这种操作的时候。 因为Spark Streaming程序在运行的时候,一般不会分配特别多的内存,因此一旦在这个过程中出现一些数据倾斜,就十分容易造成OOM。
一般来说造成数据倾斜的主要原因可以总结为以下几点:
Hadoop 和 Spark 在 Shuffle 过程中产生数据倾斜的原理基本类似即数据不均匀。因为数据分布不均匀,导致大量数据分配到一个节点。
举一个栗子,假设我们有两张表:
users(用户信息表):userid,register_ip
ip(IP 表):ip,register_user_cnt
这可能是两个不同的人开发的数据表。如果我们的数据规范不太完善的话,会出现一种情况:user表中的register_ip 字段,如果获取不到这个信息,我们默认为null;但是在ip表中,我们在统计这个值的时候,为了方便,我们把获取不到 ip 的用户,统一认为他们的 ip 为 0。 两边其实都没有错的,但是一旦我们做关联了,这个任务会在做关联的阶段,也就是 sql 的 join on 的阶段卡死。
数据往往和业务是强相关的,业务的场景直接影响到了数据的分布。 再举一个栗子,比如就说订单场景吧,在某一天,北京和上海两个城市做了强力推广,结果这两个城市的订单量增长了10000倍,其余城市的数据量不变。 然后我们要统计不同城市的订单情况,这样,一做 group by 操作,可能直接就数据倾斜了。
很多数据倾斜的问题,基本上都是由于数据设计与业务的理解不足而造成的。通常情况下,通过更适合的数据预处理,异常值的过滤等,就可以解决数据倾斜的大部分问题。
解决数据倾斜的基本思路大致分为为三种:
业务逻辑
从业务逻辑的层面上来优化数据倾斜,比如上面的两个城市做推广活动导致那两个城市数据量激增的栗子,我们可以单独对这两个城市来做count,单独做时可用两次MR,第一次打散计算,第二次再最终聚合计算。完成后和其它城市做整合。
代码层面
配置调优
Hadoop 和 Spark都自带了很多的参数和机制来调节数据倾斜,合理利用它们就能解决大部分问题。
举个栗子:
拿之前ip为null的栗子说明,并通过设计的角度尝试解决它。
1. 有损的方法:找到异常数据,比如 ip 为 0 的数据,过滤掉。
2. 无损的方法:对分布不均匀的数据,单独计算,先对 key 做一层hash、加盐等打散操作,先将数据随机打散让它的并行度变大,再汇集。
解决方法:
select * from ip a
join users b
on a.ip is not null
and a.ip = b.register_ip
union all
select * from ip a
where a.ip is null;
select *
from ip a
left outer join users b
on case when a.ip is null then concat(‘hive’,rand() ) else a.ip end = b.register_ip;
方法2比方法1效率更好,不但io少了,而且作业数也少了。解决方法1中 log读取两次,jobs是2。解决方法2 job数是1 。这个优化适合无效 id (比如 -99 , ’’, null 等) 产生的倾斜问题。把空值的 key 变成一个字符串加上随机数,就能把倾斜的数据分到不同的reduce上 ,解决数据倾斜问题。
在 Hive 中,经常遇到 count(distinct)操作,这样会导致最终只有一个 Reduce 任务。 我们可以先 group by,再在外面包一层 count,就可以了。举个栗子,计算按用户名去重后的总用户量:
// 优化前只有一个reduce,先去重再count负担比较大
select name,count(distinct name)from user;
// 优化后
// 启动两个job,一个负责子查询(可以有多个reduce),另一个负责count(1)
select count(1) from (select name from user group by name) tmp;
场景:用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。当按照user_id进行两个表的Join操作时,默认的Hash操作会按int型的id来进行分配,这样会导致所有string类型id的记录都分配到一个Reducer中。
解决方法:把数字类型转换成字符串类型
select * from users a
left outer join logs b
on a.usr_id = cast(b.user_id as string)
使用 map join 解决小表(记录数少)关联大表的数据倾斜问题,这个方法使用的频率非常高,但如果小表很大,大到map join会出现bug或异常,这时就需要特别的处理。 以下例子:
select * from log a
left outer join users b
on a.user_id = b.user_id;
users 表有 600w+ 的记录,把 users 分发到所有的 map 上也是个不小的开销,而且 map join 不支持这么大的小表。如果用普通的 join,又会碰到数据倾斜的问题。
在Hive0.11后,Hive默认启动该优化。只需调整以下参数即可自动判断并使用。
对于join,在判断小表不大于1G的情况下,使用map join。
对于group by或distinct,设定 hive.groupby.skewindata=true。决定 group by 操作是否支持倾斜数据。
注意:只能对单个字段聚合。
尽量使用上述的SQL语句调节进行优化。