火山日常啰嗦
学习了一些大数据的相关框架后,发现应用层的东西确实不难,真正难的都是底层原理,所以我查看了很多资料,借鉴了前人的方法再加上自己的理解,写下了这篇文章。
数据倾斜的直白概念:
数据倾斜就是数据的分布不平衡,某些地方特别多,某些地方又特别少,导致的在处理数据的时候,有些很快就处理完了,而有些又迟迟未能处理完,导致整体任务最终迟迟无法完成,这种现象就是数据倾斜。
针对mapreduce的过程来说就是,有多个reduce,其中有一个或者若干个reduce要处理的数据量特别大,而其他的reduce处理的数据量则比较小,那么这些数据量小的reduce很快就可以完成,而数据量大的则需要很多时间,导致整个任务一直在等它而迟迟无法完成。
跑mr任务时常见的reduce的进度总是卡在99%,这种现象很大可能就是数据倾斜造成的。
产生数据倾斜的原因:
1) key的分布不均匀或者说某些key太集中。
上面就说过,reduce的数据量大小差异过大,而reduce的数据是分区的结果,分区是对key求hash值,根据hash值决定该key被分到某个分区,进而进入到某个reduce,而如果key很集中或者相同,那么计算得到它们的hash值可能一样,那么就会被分配到同一个reduce,就会造成这个reduce所要处理的数据量过大。
2) 业务数据自身的特性。
比如某些业务数据作为key的字段本就很集中,那么结果肯定会导致数据倾斜啊。
还有其他的一些原因,但是,根本原因还是key的分布不均匀,而其他的原因就是会造成key不均匀,进而导致数据倾斜的后果,所以说根本原因是key的分布不均匀。
既然有数据倾斜这种现象,就必须要有数据倾斜对应的处理方案啊。
简单地说数据倾斜这种现象导致的任务迟迟不能完成,耗费了太多时间,极大地影响了性能,所以我们数据倾斜的解决方案设计思路就是往如何提高性能,即如何缩短任务的处理时间这方面考虑的,而要提高性能,就要让key分布相对均衡,所以我们的终极目标就是考虑如何预处理数据才能够使得它的key分布均匀。
常见的数据倾斜处理方案:
1 设置参数
1)设置hive.map.aggr=true //开启map端部分聚合功能,就是将key相同的归到一起,减少数据量,这样就可以相对地减少进入reduce的数据量,在一定程度上可以提高性能,当然,如果数据的减少量微乎其微,那对性能的影响几乎没啥变化。
2)设置hive.groupby.skewindata=true //如果发生了数据倾斜就可以通过它来进行负载均衡。当选项设定为 true,生成的查询计划会有两个 MR Job。第一个 MR Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个 Reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的Key 有可能被分发到不同的 Reduce 中,从而达到负载均衡的目的;第二个 MR Job 再根据预处理的数据结果按照Key 分布到 Reduce 中(这个过程是按照key的hash值进行分区的,不同于mr job1的随机分配,这次可以保证相同的Key 被分布到同一个 Reduce 中),最后完成最终的聚合操作。所以它主要就是先通过第一个mr job将key随机分配到reduce,使得会造成数据倾斜的key可能被分配到不同的reduce上,从而达到负载均衡的目的。到第二个mr job中,因为第一个mr job已经在reduce中对这些数据进行了部分聚合(就像单词统计的例子,a这个字母在不同的reduce中,已经算出它在每个reduce中的个数,但是最终的总的个数还没算出来,那么就将它传到第二个mr job,这样就可以得到总的单词个数),所以这里直接进行最后的聚合就可以了。
3)hive.exec.reducers.bytes.per.reducer=1000000000 (单位是字节)
每个reduce能够处理的数据量大小,默认是1G
4)hive.exec.reducers.max=999
最大可以开启的reduce个数,默认是999个
在只配了hive.exec.reducers.bytes.per.reducer以及hive.exec.reducers.max的情况下,实际的reduce个数会根据实际的数据总量/每个reduce处理的数据量来决定。
5)mapred.reduce.tasks=-1
实际运行的reduce个数,默认是-1,可以认为指定,但是如果认为在此指定了,那么就不会通过实际的总数据量/hive.exec.reducers.bytes.per.reducer来决定reduce的个数了。
2 sql语句优化
给几个具体的场景以及在这些场景下的处理方案:
1)进行表的join这种业务操作时,经常会产生数据倾斜。
原因就是这些业务数据本就存在key会分布不均匀的风险,所以我们join时不能使用普通的join(reduce端join)或者可以使用普通join,但是是优化后的。
不使用普通join的原因:数据要进入reduce,肯定要先进行分区,而分区就是根据key的hash值来进行的,既然数据的key本身就是不均匀的了(即某些key很集中,或者干脆就是有很多相同的key,比如key为无效值null),那这样分区的结果就是这些集中的key会被分到同一个分区中,而这些key的数量本就大,所以会产生数据倾斜。
既然不使用普通join,那么我们可以使用map join,对于原本就有数据倾斜的风险的业务数据,我们可以使用map join来避免数据倾斜这种风险,原因:
上面就说过了,普通join就是因为分区这一阶段导致较为集中的key会被分到同一个分区,进而进入同一个reduce,这样机会产生数据倾斜。但是呢,使用map join的话,直接在map端就完成表的join操作(表的join得到的结果(一张新表)就是我们这次的目标了),进入map端的数据都是经过split(分片)得到的,没有根据key分区这一操作,所以数据都是相对均匀地分布在每个map task中的,所以就不会产生数据倾斜。
但是这种操作有个前提条件就是仅适用于小表join大表,而小表怎么定义它的大小,多小的表才算小表,这里有个参数可以确定的(但是这个参数名我暂时忘记了),如果小表的数据大小小于这个值,就可以使用map join,而是在这种情况下是自动使用map join这种方案的。所以如果是大小表join,直接用map join,避免数据倾斜。
但是如果都是大表呢,不满足小表join大表,那就无法使用map join,那该怎么办呢?别担心,有办法:
分情况讨论:
1)业务数据有数据倾斜的风险,但是这些导致数据倾斜风险的key一般都是无效的(比如日志中user_id的值很容易丢失而成为null),那对于这些null值得数据,我们最终关联得到的结果表中一定不会有这样的记录的,因为它们的user_id为null,而在某些业务需求中我们就是要分析与user_id有关的行为,所以当user_id为null时,它对应的这些数据都是没有意义的,所以根本不会出现在结果表中。所以当业务数据很大,但是数据中的大部分(一般都是80%)可能都是无效数据,那么我们就可以在join时过滤(清洗)掉它们,没有了这些无效数据,自然就不存在这么大量集中的key了,数据倾斜的风险也就消失了。
比如将日志和用户表进行关联,关联条件是user_id相同,按照上述说法,那么为了避免数据倾斜风险,我们应该这样做:
方法1:(普通join)
select * from log a join users b on (a.user_id is not null and a.user_id = b.user_id );
这是属于表的内连接的,两张表不满足条件的记录都不保留。
方法2:检测到user_id是null时给它赋予一个新值(这个新值由一个字符串(比如我自己给它定一个 hive)加上一个随机数组成),这样就可以将原来集中的key分散开来,也避免了数据倾斜的风险。而且因为这些数据本来就是无效数据,根本不会出现在结果表中,所以,这样处理user_id(由一个字符串(比如我自己给它定一个 hive)加上一个随机数),它也无法关联的,因为有效的数据的user_id没有这种形式的,所以就算这些无效数据出现在不同的reduce中还是不会影响结果的,我这样处理只是为了将它们分散开而已,所以用这种方法处理,结果表中也不会出现null这些无效数据,跟过滤处理方案得到的结果是一样的。(普通join)
select *
from log a
join users b
on case when a.user_id is null then concat(‘hive’,rand() ) else a.user_id end = b.user_id;
但是这两种方案只是适用于大表join大表的内连接,两张表的无效数据都不保留。
但是如果对于左外连接或者右外连接这种情况,即使驱动表中某些记录在另一张表中没有数据与它对应,但我们是依然需要保留驱动表的这些数据的,那该怎么办呢?其实很简单,只需要将上述方法得到的结果再与驱动表的这些无数据取并集就可以了。
如下:
select * from log a
left outer join users b
on a.user_id is not null
and a.user_id = b.user_id
union all
select * from log a
where a.user_id is null;
2)虽然都是大表,但是呢对于某些业务数据而言,其有用的部分只占它所在表的很少一部分,那么我们就可以将它们先取出来,得到的结果应该是一张小表,那么就可以使用map join来避免数据倾斜了。
不同数据类型关联产生数据倾斜(普通join)
场景:用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。
当按照user_id进行两个表的Join操作时,因为我们在连接时要进行user_id的比较,所以需要user_id的类型都相同,如果我们选择将log表中的String类型转换为int类型,那么就可能会出现这种情况:String类型转换为int类型得到的都是null值(这就是类型转换的问题了,String类型数据转换为int类型会失败,数据丢失,就会赋null值),如果所有的String类型的user_id都变成了null,那么就又出现了集中的key,分区后就又会导致数据倾斜。所以我们进行类型转换时不能选择将String类型转换为int,而应该将int类型转换为String,因为int转换为String不会出问题,int类型原来的值是什么,转换为String后对应的字符串就会是什么,形式没变,只是类型变了而已。
解决方法:把int类型转换成字符串类型
select * from users a
join logs b
on (a.usr_id = cast(b.user_id as string));
数据本身有数据倾斜风险,通过count(distinct xxx)这种操作方式就产生数据倾斜后果
比如有一份日志,要你从日志中统计某天有多少个用户访问网站,即统计有多少个不同的user_id;但是呢这个网站却又恰巧遭到攻击,日志中大部分都是同一个user_id的记录,其他的user_id属于正常访问,访问量不会很大,在这种情况下,当你直接使用count(distinct user_id)时,这也是要跑mr任务的啊,这时这些大量的相同的user_id就是集中的key了,结果就是通过分区它们都被分到一个reduce中,就会造成这个reduce处理的数据特别大,而其中的reduce处理的数据都很小,所以就会造成数据倾斜。
那么要怎么优化呢?
方法1:可以先找出这个user_id是什么,过滤掉它,然后通过count(distinct user_id)计算出剩余的那些user_id的个数,最后再加1(这1个就是那个被过滤掉的user_id,虽然它有大量的记录,但是ser_id相同的都是同一个用户,而我们要计算的就是用户数)
sql语句展示:
分组求和后降序排序,就可以得到这个数据量最大的user_id是什么,然后我们下一步操作时就过滤它,等计算完其他的再加上它这一个。
select user_id,count(user_id) from log group by user_id desc limit 2;
select count(distinct user_id)+1 as sum from log;
sum就是最终的结果–用户数
方法2:我们可以先通过group by分组,然后再在分组得到的结果的基础之上进行count
sql语句展示:
select count(*) from (select user_id from log group by user_id) new_log;
总的来说就是,数据倾斜的根源是key分布不均匀,所以应对方案要么是从源头解决(不让数据分区,直接在map端搞定),要么就是在分区时将这些集中却无效的key过滤(清洗)掉,或者是想办法将这些key打乱,让它们进入到不同的reduce中。
性能调优是指通过调整使得机器处理任务的速度更快,所花的时间更少,而数据倾斜的处理是hive性能调优的一部分,通过处理能够大大减少任务的运行时间。
除了数据倾斜的处理之外,hive的优化还有其他方面的,例如where子句优化:
select * from a left outer join b on (a.key=b.key) where a.date=’2017-07-11′ and b.date=’2017-07-11′;
这是一个左外连接。
这个sql语句执行的结果是:得到的结果是表a与表b的连接表,且表中的记录的date都是’2017-07-11’。
而这个sql语句的执行过程是:逐条获取到a表的记录,然后扫描b表,寻找字段key值为a.key的记录,找到后将b表的这条记录连接到a表上,然后判断连接后的这条记录是否满足条件a.date=’2017-07-11′ and b.date=’2017-07-11’,如果满足,则显示,否则,丢弃。
因为这是一个左外连接,且a为驱动表,连接时在a中发现key而在b中没有发现与之相等的key时,b中的列将置为null,包括列date,一个不为null,一个为null,这样后边的where就没有用了。
简答的说这个方案的做法就是先按连接条件进行连接,连接后再看where条件,如果不满足就丢弃,那之前连接所做的那些功夫就浪费了,白白耗费了资源(cpu等),增加了运行的总时间,如果有一种方案可以在未进行连接之前就直接判断出不满足最终的条件,那么就可以直接丢弃它,这样对于这样的记录就不要浪费资源以及时间去连接了,这样也是能提升性能的,下面就看看这种方案:
sql语句:
将刚才的where限制条件直接放到on里面,那么就变成了满足这三个条件才会进行连接,不满足的直接过滤掉,就像上面所说的,少了无效连接那一步,就相对地节约了时间,如果这样的无效连接的记录很多的话,那么采用这种改进版的方案无疑能够较大程度地提高性能。
select * from a left outer join b on (a.key=b.key and a.date=’2017-07-11′ and b.date=’2017-07-11’);
不管怎么说,我们在运行任务时,总是希望能加快运行速度,缩短运行时间,更快地得到结果,即提升性能,这是我们的目的,这就是我们所谓的性能调优。
关于小表join大表的补充:
表join时的操作是这样的:
当操作到驱动表的某条记录时,就会全局扫描另一张表,寻找满足条件的记录,而当扫描它时,为了读取速度更快,一般都选先将它加载到内存,而内存的大小是有限的,为了不占据过多的内存或者避免内存溢出,加载进入内存的表一般是小表,即数据量比较小,map join就是这样做的。
即驱动表不放进内存,而另一张表(即要连接到驱动表的那一张表)就要先加载进内存,为了扫描速度更快,提高性能。
比如select * from a left outer join b on (a.key=b.key);
左外连接,驱动表是a,表b的记录是要被连接到表a的,所以每在a上连接一条记录就要被全局扫描一次的表是b,所以表b应先加载到内存(前提是表b是小表,如果是大表的话,估计会产生oom异常–out of memory内存溢出异常)。
select * from aa right outer join bb on (a.key=b.key);
右外连接,驱动表是bb,aa应先加载到内存(前提是小表)。
ps:希望我的分享能帮助到有需要的伙伴哦。我不是大神的哦,如果文中有误,还请大家不吝赐教,帮忙指正,谢谢了!!!