spark性能调优9------------数据倾斜问题一站式解决方案

 

1.数据倾斜产生的原因

在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,比如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。数据倾斜只会发生在shuffle过程中。常用的并且可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。 Spark作业看起来会运行得非常缓慢,甚至可能因为某个task处理的数据量过大导致内存溢出。

2.数据倾斜两种表现

a.大部分task都执行特别快,剩下几个task跑得特别慢

b.其他task都执行完了,剩下几个task出现oom

3.解决方案:

a.从数据源上解决数据倾斜问题:

一些聚合的操作,比如groupByKey、reduceByKey,groupByKey说白了就是拿到每个key对应的values。reduceByKey说白了就是对每个key对应的values执行一定的计算。

这些操作,比如groupByKey和reduceByKey,包括之前说的join。都是在spark作业中执行的。

spark作业的数据来源,通常是哪里呢?90%的情况下,数据来源都是hive表(hdfs,大数据分布式存储系统)。hdfs上存储的大数据。hive表中的数据通常是怎么出来的呢?有了spark以后,hive比较适合做什么事情?hive就是适合做离线的,晚上凌晨跑的,ETL(extract transform load,数据的采集、清洗、导入),hive sql,去做这些事情,从而去形成一个完整的hive中的数据仓库。说白了,数据仓库,就是一堆表。

spark作业的源表,hive表,通常情况下来说,也是通过某些hive etl生成的。hive etl可能是晚上凌晨在那儿跑。今天跑昨天的数据。

数据倾斜,某个key对应的80万数据,某些key对应几百条,某些key对应几十条。现在咱们直接在生成hive表的hive etl中对数据进行聚合。比如按key来分组,将key对应的所有的values全部用一种特殊的格式拼接到一个字符串里面去,比如“key=sessionid, value: action_seq=1|user_id=1|search_keyword=火锅|category_id=001;action_seq=2|user_id=1|search_keyword=涮肉|category_id=001”。

对key进行group,在spark中,拿到key=sessionid,values<Iterable>。hive etl中,直接对key进行了聚合。那么也就意味着,每个key就只对应一条数据。在spark中,就不需要再去执行groupByKey+map这种操作了。直接对每个key对应的values字符串进行map操作,进行你需要的操作即可。

spark中,可能对这个操作,就不需要执行shffule操作了,也就根本不可能导致数据倾斜。

或者是对每个key在hive etl中进行聚合,对所有values聚合一下,不一定是拼接起来,可能是直接进行计算。reduceByKey计算函数应用在hive etl中,从而得到每个key的values。

聚合源数据方案第二种做法是,你可能没有办法对每个key聚合出来一条数据。那么也可以做一个妥协,对每个key对应的数据,10万条。有好几个粒度,比如10万条里面包含了几个城市、几天、几个地区的数据,现在放粗粒度。直接就按照城市粒度,做一下聚合,几个城市,几天、几个地区粒度的数据,都给聚合起来。比如说

city_id date area_id

select ... from ... group by city_id

尽量去聚合,减少每个key对应的数量,也许聚合到比较粗的粒度之后,原先有10万数据量的key,现在只有1万数据量。减轻数据倾斜的现象和问题。

 

b.过滤一些key

如果你能够接受某些数据在spark作业中直接就摒弃掉不使用。比如说,总共有100万个key。只有2个key是数据量达到10万的。其他所有的key,对应的数量都是几十万。

这个时候,你自己可以去取舍,如果业务和需求可以理解和接受的话,在你从hive表查询源数据的时候,直接在sql中用where条件,过滤掉某几个key。

那么这几个原先有大量数据,会导致数据倾斜的key,被过滤掉之后,那么在你的spark作业中,自然就不会发生数据倾斜了。
 

c.提高reduce的并行度

第三个方案,提高shuffle操作的reduce并行度

reduce task的数量,变多,就可以让每个reduce task分配到更少的数据量,这样的话,也许就可以缓解,或者甚至是基本解决掉数据倾斜的问题。

给所有的shuffle算子,比如groupByKeycountByKeyreduceByKey。在调用的时候,传入进去一个参数。一个数字。那个数字,就代表了那个shuffle操作的reduce端的并行度。那么在进行shuffle操作的时候,就会对应着创建指定数量的reduce task

这样的话,就可以让每个reduce task分配到更少的数据。基本可以缓解数据倾斜的问题。

比如说,原本某个task分配数据特别多,直接OOM,内存溢出了,程序没法运行,直接挂掉。按照log,找到发生数据倾斜的shuffle操作,给它传入一个并行度数字,这样的话,原先那个task分配到的数据,肯定会变少。就至少可以避免OOM的情况,程序至少是可以跑的。

RDD.reduceByKey(function(x), 1000);

d.使用随机key进行双重聚合

1、原理

2、使用场景

1groupByKey

2reduceByKey

比较适合使用这种方式;join,咱们通常不会这样来做,后面会讲三种,针对不同的join造成的数据倾斜的问题的解决方案。

第一轮聚合的时候,对key进行打散,将原先一样的key,变成不一样的key,相当于是将每个key分为多组;

先针对多个组,进行key的局部聚合;接着,再去除掉每个key的前缀,然后对所有的key,进行全局的聚合。

对groupByKeyreduceByKey造成的数据倾斜,有比较好的效果。

如果说,之前的第一、第二、第三种方案,都没法解决数据倾斜的问题,那么就只能依靠这一种方式了

 #rdd1是一个二元元组,第一步key拼接上一个随机数
    rdd2 = rdd1.map(lambda x:(str(random.randint(0,10))+"_"+x[0],x[1]))
    #第二步聚合
    rdd3 = rdd2.reduceByKey(lambda x,y:x+y)
    #第三步去除前缀
    rdd4 = rdd3.map(lambda x:(x[0].split("_")[1],x[1]))
    #第五步再进行一次聚合
    rdd5 = rdd4.reduceByKey(lambda x,y:x+y)

e.reduce join转化为map join 

普通的join,那么肯定是要走shuffle;那么,所以既然是走shuffle,那么普通的join,就肯定是走的是reduce join

先将所有相同的key,对应的values,汇聚到一个task中,然后再进行join

换一种思路:

reduce join转换为map join,适合在什么样的情况下,可以来使用?

如果两个RDD要进行join,其中一个RDD是比较小的。一个RDD100万数据,一个RDD1万数据。(一个RDD1亿数据,一个RDD100万数据)

其中一个RDD必须是比较小的,broadcast出去那个小RDD的数据以后,就会在每个executorblock manager中都驻留一份。要确保你的内存足够存放那个小RDD中的数据

这种方式下,根本不会发生shuffle操作,肯定也不会发生数据倾斜;从根本上杜绝了join操作可能导致的数据倾斜的问题;

对于join中有数据倾斜的情况,大家尽量第一时间先考虑这种方式,效果非常好;如果某个RDD比较小的情况下。

不适合的情况:

两个RDD都比较大,那么这个时候,你去将其中一个RDD做成broadcast,就很笨拙了。很可能导致内存不足。最终导致内存溢出,程序挂掉。

而且其中某些key(或者是某个key),还发生了数据倾斜;此时可以采用最后两种方式。

对于join这种操作,不光是考虑数据倾斜的问题;即使是没有数据倾斜问题,也完全可以优先考虑,用我们讲的这种高级的reduce joinmap join的技术,不要用普通的join,去通过shuffle,进行数据的join;完全可以通过简单的map,使用map join的方式,牺牲一点内存资源;在可行的情况下,优先这么使用。

不走shuffle,直接走map,是不是性能也会高很多?这是肯定的。

友情链接:在spark中总共有三种join方式:https://www.cnblogs.com/0xcafedaddy/p/7614299.html

f。对倾斜的 Keys 采样后进行 单独的 Join操作

本节采用采样算法的思想来解决数据倾斜。数据倾斜的时候如果能把 Join 的方式去除, 在 Mapper 端就能完成 Join 的操作,这是最好的 , 但有一个前提条件:要进行 Join 的 RDD, 其中有一个 RDD 的数据比较少。而在实际的生产环境下 ,有时不具备这样的前提条件,如 果两个 RDD 的数据都比较多,我们将尝试采取进一步的做法来解决这个问题。

首先我们谈采样。采样是有一个数据的全量,假如有 100亿条数据,采取一个规则来选 取 100亿条数据中的一部分数据,如 5%、 10%、 15%, 采样通常不可能超过 30%的数据。采 样算法的优劣决定了采样的效果。所谓采样的效果,即我们采样的结果能否代表全局的数据 (100 亿条数据〉 。在 Spark 中,我们可以直接采用采样算法 Sample。 采样算法对解决数据 倾斜的作用 : 数据产生数据倾斜是由于某个 Key 或者某几个 Key,数据的 Value特别多,进 行 Shuffle 的 时候 , Key 是进行数据分类的依据 。 如果能够精准地找出是哪个 Key 或者哪 几个 Key 导致了数据倾斜,这是解决问题的第 一步 : 找出谁导致数据倾斜,就可以进行分而 泊之。

例如, RDDl和RDD2进行Join操作,我们采用采样的方式发现RDDI中有严重的数据 倾斜的 Key。

第一步 : 采用 SparkRDD 中提供的采样接口,可以很方便地对全体(如 100 亿条)数掘 进行采样,然后基于采样的数据可以计算出哪个(哪些) Key 的 Values 个数最多 。

第二步:把全体数据分成两部分,即把原来的一个 RDDI 变成 RDDll 和 RDD12,其中 RDDII 代表导致数据倾斜的 Key, RDD12 中包含的是不会产生数据倾斜的 Keyo

第三步 :把 RDDl l 和 RDD2 进行 Join 操作,且把 RDD12 和 RDD2 进行 Join 操作, 然 后把 Join操作后的结果进行 Union操作,从而得出和 RDDl 与 RDD2直接进行 Join相同的 结果。

这样就解决了数据倾斜的 问题 : RDD12 和 RDD2 进行 Join 操作不会产生数据倾斜,因 为里面没有特别的Key,即没有哪个Key的Value特别多; 即DII和RDD2进行Join操作, 假设 RDDll 中只有一个 Key,其 Key 值的 Value 特别 多 ,利用 Spark Core 天然 的并行机制 对 RDDll 的 Key 的数据进行拆分。

适用场景:

比如就是一个key对应的数据量特别多。

此时可以采用咱们的这种方案,单拉出来那个最多的key;单独进行join,尽可能地将key分散到各个task上去进行join操作。

如果一个RDD中,导致数据倾斜的key,特别多;那么此时,最好还是不要这样了;还是使用我们最后一个方案,终极的join数据倾斜的解决方案。

实现:

 global sc
    rdd1 = sc.parallelize((("a",3),("a",2),("n",3),("a",1),("n",3),("a",3),("a",45),("m",1),("a",1),("b",23)))
    #rdd1是一个二元元组,
    # 对rdd1进行随机采样
    rdd2 = rdd1.sample(False,0.1,9)
    #第二步统计样本中的key排序
    rdd3 = rdd2.map(lambda x:(x[0],1))
    #第三步进行累加
    rdd4 = rdd3.reduceByKey(lambda x,y:x+y)
    #第四步反转
    rdd5 = rdd4.map(lambda x:(x[1],x[0]))
    #第五部找出value值最高的key
    targetKey = rdd5.sortByKey(False).take(1)[0][0]
    #第六步拆分rdd1,大批数据的rdd
    leanRdd = rdd1.filter(lambda x:targetKey==x)
    notleanRdd = rdd1.filter(lambda x:targetKey!=x)
    waitJoinRdd = sc.parallelize(
        (("a", 3), ("a", 2), ("n", 3), ("a", 1), ("n", 3), ("a", 3), ("a", 45), ("m", 1), ("a", 1), ("b", 23)))
    #分别两个rdd与之join
    joinRdd1 = leanRdd.join(waitJoinRdd)
    joinRdd2 = notleanRdd.join(waitJoinRdd)
    #合并两个join后的rdd
    resultRdd = joinRdd1.union(joinRdd2)

对于上述方法还有一个改良版本

对于包含倾斜key得rdd,可以在之前加一个随机数,然后再需要join得rdd相同的key也加一个随机数,然后在进行join

代码如下:

def doubleGroup():
    global sc
    rdd1 = sc.parallelize(
        (("a", 3), ("a", 2), ("n", 3), ("a", 1), ("n", 3), ("a", 3), ("a", 45), ("m", 1), ("a", 1), ("b", 23)))
    # rdd1是一个二元元组,
    # 对rdd1进行随机采样
    rdd2 = rdd1.sample(False, 0.1, 9)
    # 第二步统计样本中的key排序
    rdd3 = rdd2.map(lambda x: (x[0], 1))
    # 第三步进行累加
    rdd4 = rdd3.reduceByKey(lambda x, y: x + y)
    # 第四步反转
    rdd5 = rdd4.map(lambda x: (x[1], x[0]))
    # 第五部找出value值最高的key
    targetKey = rdd5.sortByKey(False).take(1)[0][0]
    # 第六步拆分rdd1,大批数据的rdd
    leanRdd = rdd1.filter(lambda x: targetKey == x)
    notleanRdd = rdd1.filter(lambda x: targetKey != x)
    waitJoinRdd = sc.parallelize(
        (("a", 3), ("a", 2), ("n", 3), ("a", 1), ("n", 3), ("a", 3), ("a", 45), ("m", 1), ("a", 1), ("b", 23)))

    # -----------不同得地方——————————————————————
    # 给leanRdd加随机数
    randomNum = str(random.randint(0, 100)
    randomedRdd1 = leanRdd.map(lambda x: (str(randomNum) + "-" + x[0], x[1])))
    # 过滤出包含相同key的并且顺序加上0-100上的数字或者是一个固定的0-100内得数字
    filteredWaitJoinRdd = waitJoinRdd.filter(lambda x: x[0] == targetKey).flatMap(func1)
    # join两个rdd
    joinRdd1 = randomedRdd1.join(filteredWaitJoinRdd)
    joinRdd2 = notleanRdd.join(waitJoinRdd)
    # 合并两个join后的rdd
    resultRdd = joinRdd1.union(joinRdd2)


doubleGroup()

还有一种方式就是把randomedRdd1广播出去

e.扩容

在f得补充说明中其实已经是一种扩容得方式了,上面方法有个局限性:如果两个rdd中倾斜得数据都比较多,这样不能一个个找出来,那么就用下面方法

1、选择一个RDD,要用flatMap,进行扩容,将每条数据,映射为多条数据,每个映射出来的数据,都带了一个n以内的随机数,通常来说,会选择10

2、将另外一个RDD,做普通的map映射操作,每条数据,都打上一个10以内的随机数。

3、最后,将两个处理后的RDD,进行join操作。

局限性:

1、因为你的两个RDD都很大,所以你没有办法去将某一个RDD扩的特别大,一般咱们就是10倍。

2、如果就是10倍的话,那么数据倾斜问题,的确是只能说是缓解和减轻,不能说彻底解决

sample采样倾斜key并单独进行join

key,从另外一个RDD中过滤出的数据,可能只有一条,或者几条,此时,咱们可以任意进行扩容,扩成1000倍。

将从第一个RDD中拆分出来的那个倾斜key RDD,打上1000以内的一个随机数。

这种情况下,还可以配合上,提升shuffle reduce并行度,join(rdd, 1000)。通常情况下,效果还是非常不错的。

打散成100份,甚至1000份,2000份,去进行join,那么就肯定没有数据倾斜的问题了吧。

代码:

def doubleGroup():
    global sc
    rdd1 = sc.parallelize(
        (("a", 3), ("a", 2), ("n", 3), ("a", 1), ("n", 3), ("a", 3), ("a", 45), ("m", 1), ("a", 1), ("b", 23)))
    waitJoinRdd = sc.parallelize(
        (("a", 3), ("a", 2), ("n", 3), ("a", 1), ("n", 3), ("a", 3), ("a", 45), ("m", 1), ("a", 1), ("b", 23)))
    # rdd1每个元素加上个随机数前缀
    randomNum = str(random.randint(0, 100)
    randomedRdd1 = rdd1.map(lambda x: (randomNum + "-" + x[0], x[1])))
    # 待join的每个数字加上一个100以内的顺序数
    filteredWaitJoinRdd = waitJoinRdd.flatMap(func1)
    # join
    resultRdd = randomedRdd1.union(filteredWaitJoinRdd)


doubleGroup()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值