Spark 数据倾斜

本文探讨了Spark数据倾斜的两种表现形式,如何通过预聚合源数据、调整shuffle并行度、随机key双层聚合等方法来预防和处理数据倾斜问题。重点介绍了在Hive表预处理、shuffle操作优化和代码层面的排查技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

执行shuffle操作的时候,大家都知道,我们之前讲解过shuffle的原理。是按照key,来进行values的数据的输出、拉取和聚合的

同一个key的values,一定是分配到一个reduce task进行处理的

多个key对应的values,总共是90万。但是问题是,可能某个key对应了88万数据,key-88万values,分配到一个task上去面去执行

另外两个task,可能各分配到了1万数据,可能是数百个key,对应的1万条数据

第一个和第二个task各分配到了1万数据;那么可能1万条数据,需要10分钟计算完毕;第一个和第二个task,可能同时在10分钟内都运行完了第三个task要88万条,88 * 10 = 880分钟 = 14.5个小时

spark数据倾斜,有两种表现

1、你的大部分的task,都执行的特别特别快,刷刷刷,就执行完了(你要用client模式,standalone client,yarn client,本地机器主要一执行spark-submit脚本,就会开始打印log),task175 finished;剩下几个task,执行的特别特别慢前面的task,一般1s可以执行完5个;最后发现1000个task,998,999 task,要执行1个小时,2个小时才能执行完一个task。

2、运行的时候,其他task都刷刷刷执行完了,也没什么特别的问题;但是有的task,就是会突然间,啪,报了一个OOM,JVM Out Of Memory,内存溢出了,task failed,task lost,resubmitting task。反复执行几次都到了某个task就是跑不通,最后就挂掉

某个task就直接OOM,那么基本上也是因为数据倾斜了,task分配的数量实在是太大了!!!所以内存放不下,然后你的task每处理一条数据,还要创建大量的对象。内存爆掉了。

定位原因与出现问题的位置:

根据log去定位

出现数据倾斜的原因,基本只可能是因为发生了shuffle操作,在shuffle的过程中,出现了数据倾斜的问题。因为某个,或者某些key对应的数据,远远的高于其他的key

1、你在自己的程序里面找找,哪些地方用了会产生shuffle的算子groupByKey、countByKey、reduceByKey、join

2、看log

log一般会报是在你的哪一行代码,导致了OOM异常;或者呢,看log,看看是执行到了第几个stage!!!

  1. 预聚合源数据,对hive源表提前进行聚合操作,在hive聚合之后,spark任务再去读取

spark作业的数据来源,90%的情况下,数据来源都是hive表。hdfs上存储的大数据。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;hive etl中,直接对key进行了聚合。那么也就意味着,每个key就只对应一条数据。在spark中,就不需要再去执行groupByKey+map这种操作了。直接对每个key对应的values字符串,map操作,进行你需要的操作即可。key,values串。

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万数据量。减轻数据倾斜的现象和问题。

  1. 检查倾斜的key是否是脏数据,可以提前过滤

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

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

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

  1. 提高shuffle操作reduce的并行度
spark.default.parallelism,100

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

提升shuffle reduce并行度的缺陷

治标不治本的意思,因为,它没有从根本上改变数据倾斜的本质和问题。不像第一个和第二个方案(直接避免了数据倾斜的发生)。原理没有改变,只是说,尽可能地去缓解和减轻shuffle reduce task的数据压力,以及数据倾斜的问题

实际生产环境中的经验

1、如果最理想的情况下,提升并行度以后,减轻了数据倾斜的问题,或者甚至可以让数据倾斜的现象忽略不计,那么就最好。就不用做其他的数据倾斜解决方案了。

2、不太理想的情况下,就是比如之前某个task运行特别慢,要5个小时,现在稍微快了一点,变成了4个小时;或者是原先运行到某个task,直接OOM,现在至少不会OOM了,但是那个task运行特别慢,要5个小时才能跑完

那么,如果出现第二种情况的话,各位,就立即放弃第三种方案,开始去尝试和选择后面的四种方案。

  1. 使用随机key实现双重聚合
    使用场景
    (1)groupByKey
    (2)reduceByKey

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

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

对groupByKey、reduceByKey造成的数据倾斜,有比较好的效果

  1. 将reduce端 join转换成map端 join

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

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

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

不适合的情况:

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

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

  1. sample采样倾斜key,单独进行join后在union

优先对于 join,肯定是希望能够采用上一讲讲的,reduce join 转换 map join两个 RDD 数
据都比较大
,那么就不要那么搞了。

针对你的 RDD 的数据,你可以自己把它转换成一个中间表,或者是直接用 countByKey()
的方式,你可以看一下这个 RDD 各个 key 对应的数据量
;此时如果你发现整个 RDD 就一
个,或者少数几个 key,是对应的数据量特别多;尽量建议,比如就是一个 key 对应的数据
量特别多

发生数据倾斜的 key,单独拉出来,放到一个 RDD 中去;就用这个原本会倾斜的 key RDD其他 RDD,单独去 join 一下,这个时候,key 对应的数据,可能就会分散到多个 task 中去进行 join 操作,最后将 join 后的表进行 union 操作

如果一个 RDD 中,导致数据倾斜的 key,特别多;那么此时,最好还是不要这样了;

  1. 使用随机数以及扩容表进行join

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

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

c. 最后,将两个处理后的RDD,进行join操作

### 解决 Spark 数据倾斜问题的最佳实践 #### 设置合理的 Shuffle Partition 数量 为了防止由于分区数量不足而导致的数据倾斜,在配置 Spark 应用程序时应适当增加 `spark.sql.shuffle.partitions` 参数的值。此参数决定了 shuffle 类操作(如 group by, join)产生的分区数目,默认情况下为200,这可能不足以应对大规模数据集的情况[^5]。 ```scala // 增加shuffle partitions的数量以减少单个task的压力 val sparkSession = SparkSession.builder() .appName("DataSkewExample") .config("spark.sql.shuffle.partitions", "500") // 调整至更合适的数值 .getOrCreate() ``` #### 使用广播变量优化 Join 操作 当参与 join 的两个 RDD 或 DataFrame 中有一个较小而另一个较大时,可以通过广播机制将小表加载到内存中作为广播变量传递给每一个工作节点上的任务去执行 join 操作。这样不仅可以提高效率还能缓解因 key 分布不均引起的数据倾斜现象[^3]。 ```scala import org.apache.spark.broadcast.Broadcast // 对于小表bdf创建广播变量 val broadcastVar: Broadcast[DataFrame] = sparkSession.sparkContext.broadcast(bdf) // 在大表adf上应用mapPartitions函数完成join逻辑 val resultDf = adf.mapPartitions(partition => { val localBDF = broadcastVar.value partition.flatMap(rowA => { val matchingRowsInB = localBDF.filter(_.key == rowA.key).collect() // 这里假设存在名为'key'的列用于关联两表 matchingRowsInB.map(rowB => (rowA, rowB)) }) }) ``` #### 合并少量的小分区 如果某些阶段结束后出现了大量空闲或几乎为空的任务,则意味着这些任务对应的输入数据很少甚至不存在。此时应该考虑合并那些规模很小的分区成更大的几个分区来进行后续计算,从而降低资源浪费并改善性能表现[^4]。 ```sql SET spark.sql.adaptive.enabled=true; -- 开启自适应查询计划功能后,Spark会自动尝试调整分区大小 ``` #### 随机前缀法处理极端倾斜情况 针对特别严重且难以通过上述方法完全消除影响的关键字分布模式,可以在原有 key 上附加随机生成的部分位数作为新的组合键进行重新分配,使得原本集中在少数几个特定位置的数据能够更加均匀地散布开来被不同的 worker 处理。 ```scala def addRandomPrefix(key: String): String = s"${new java.util.Random().nextInt(10)}-$key" rdd.map{case (k,v) => (addRandomPrefix(k), v)} .reduceByKey(_ + _) .map{case (prefixKey,value) => prefixKey.split("-")(1)->value} ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值