1. 数据倾斜概述
在任何大数据类的项目中,都是最棘手的性能问题,最能体现人的技术能力,最能体现 RD(Research Developer,研发工程师)的技术水平。也是面试最喜欢问的一个问题,如果没有丰富的经验,是很难解决数据倾斜问题的,数据倾斜被称之为性能杀手
1.1. 数据倾斜的原理
在执行 shuffle 操作的时候,大家都知道,我们之前讲解过 shuffle 的原理。是按照 key,来进行 values 的数据的输出、拉取和聚合的。
同一个 key 的 values,一定是分配到一个 reduce task 进行处理的。
多个 key 对应的 values,总共是 90 万。但是问题是,可能某个 key 对应了 88 万数据,key-88 万 values, 分配到一个 task 上去面去执行 另外两个 task,可能各分配到了 1 万数据,可能是数百个 key,对应的 1 万条数据。
想象一下,出现数据倾斜以后的运行的情况。很糟糕!极其糟糕!无比糟糕!第一个和第二个 task,各分配到了 1 万数据;那么可能 1 万条数据,需要 5 分钟计算完毕;第一个和 第二个 task,可能同时在 5 分钟内都运行完了;第三个 task 要 88 万条,88 * 5 = 440 分钟 = 7 个小时;
大家看看,本来另外两个 task 很快就运行完毕了(5 分钟),但是由于一个拖后腿的家伙,第三个 task, 要 7 个小时才能运行完,就导致整个 spark 作业,也得 7 个小时才能运行完。
导致 spark 作业,跑的特别特别特别特别慢!!!像老牛拉破车!
数据倾斜,一旦出现,是不是性能杀手。。。。
1.2. 数据倾斜的现象
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 每处理一条数据,还要创建大量的对象。内存爆掉了。
出现数据倾斜了
这种就不太好了,因为你的程序如果不去解决数据倾斜的问题,压根儿就跑不出来。
作业都跑不完,还谈什么性能调优这些东西。扯淡。。。
1.3. 数据倾斜的产生原因与定位
根据 log 去定位
出现数据倾斜的原因,基本只可能是因为发生了 shuffle 操作,在 shuffle 的过程中,出现了数据倾斜的问题。因为某个,或者某些 key 对应的数据,远远的高于其他的 key。
1、你在自己的程序里面找找,哪些地方用了会产生 shuffle 的算子,groupByKey、countByKey、reduceByKey、join2、看 log
log 一般会报是在你的哪一行代码,导致了 OOM 异常;或者呢,看 log,看看是执行到了第几个 stage!!!去找找,代码那个地方,是哪个 shuffle 操作。
2. 数据倾斜解决方案
性能调优,调了半天,最有效,最直接,最简单的方式,就是加资源,加并行度,注意 RDD 架构(复用同一个 RDD,加上 cache 缓存);shuffle、jvm 等,次要的。
数据倾斜的解决方案,常见的两个方案就是聚合元数据和过滤倾斜的 key。是最朴素、最简谱、最直接、最有效、最简单的,解决数据倾斜问题的方案。
2.1. 聚合元数据
咱们现在做一些聚合的操作,groupByKey、reduceByKey;groupByKey,说白了,就是拿到每个key 对应的 values;reduceByKey,说白了,就是对每个 key 对应的 values 执行一定的计算。
现在这些操作,比如 groupByKey 和 reduceByKey,包括之前说的 join。都是在 spark 作业中执行的。spark 作业的数据来源,通常是哪里呢?90%的情况下,数据来源都是 hive 表(hdfs,大数据分布式存储系统)。hdfs 上存储的大数据。hive 表,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=hello|category_id=001;action_seq=2|user_id=1|search_ keyword=涮肉|category_id=001”。
对 key 进行 group,在 spark 中,拿到 key=sessionid,values
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 万数据量。减轻数据倾斜的现象和问题。
2.2. 过滤导致倾斜的 Key
如果你能够接受某些数据,在 spark 作业中直接就摒弃掉,不使用。比如说,总共有 100 万个 key。只有 2 个 key,是数据量达到 10 万的。其他所有的 key,对应的数量都是几十。
这个时候,你自己可以去取舍,如果业务和需求可以理解和接受的话,在你从 hive 表查询源数据的时候,直接在 sql 中用 where 条件,过滤掉某几个 key。
那么这几个原先有大量数据,会导致数据倾斜的 key,被过滤掉之后,那么在你的 spark 作业中,自然就不会发生数据倾斜了。
2.3. 提高 Shuffle 操作的 Reduce 端的并行度
具体的实现:
很简单,主要给我们所有的 shuffle 算子,比如 groupByKey、countByKey、reduceByKey。在调用的时候,传入进去一个参数。一个数字。那个数字,就代表了那个 shuffle 操作的 reduce 端的并行度。那么在进行 shuffle 操作的时候,就会对应着创建指定数量的 reduce task。
这样的话,就可以让每个 reduce task 分配到更少的数据。基本可以缓解数据倾斜的问题。
比如说,原本某个 task 分配数据特别多,直接 OOM,内存溢出了,程序没法运行,直接挂掉。按照 log,找到发生数据倾斜的 shuffle 操作,给它传入一个并行度数字,这样的话,原先那个 task 分配到的数据,肯定会变少。就至少可以避免 OOM 的情况,程序至少是可以跑的。
2.4. 随机 key 实现双重聚合
主要使用场景
(1)groupByKey
(2)reduceByKey
比较适合使用这种方式;join,咱们通常不会这样来做,后面会讲三种,针对不同的 join 造成的数据倾斜的问题的解决方案。
第一轮聚合的时候,对 key 进行打散,将原先一样的 key,变成不一样的 key,相当于是将每个 key 分为多组;
先针对多个组,进行 key 的局部聚合;接着,再去除掉每个 key 的前缀,然后对所有的 key,进行全局的聚合。
对 groupByKey、reduceByKey 造成的数据倾斜,有比较好的效果。
如果说,之前的第一、第二、第三种方案,都没法解决数据倾斜的问题,那么就只能依靠这一种方式了。
代码实现
object Demo01 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Demo01")
val sc = new SparkContext(conf)
val rdd1: RDD[String] = sc.textFile(args(0))
val rdd2: RDD[String] = rdd1.flatMap(_.split(" "))
// val rdd3: RDD[String] = rdd2.filter((word:String)=>true)
//第一步: 映射的时候对 key 前面加上一个 10 以内的随机数
val rdd4: RDD[(String, Int)] = rdd2.map((word:String)=>{
(Random.nextInt(10)+"_"+word,1)
})
// 第二部: 进行 groupByKey 操作
val rdd5: RDD[(String, Iterable[Int])] = rdd4.groupByKey()
val rdd6: RDD[(String, Int)] = rdd5.map(record => {
val iter: Iterator[Int] = record._2.iterator
var count = 0;
while (iter.hasNext) {
iter.next()
count += 1
}
(record._1, count)
})
// 第三步: 对 key 进行重新映射, 返回正常的 key
val rdd7: RDD[(String, Iterable[Int])] = rdd6.map(record => {
(record._1.split("_")(1), record._2)
}).groupByKey()
val rdd8: RDD[(String, Int)] = rdd7.map(record => {
val iter: Iterator[Int] = record._2.iterator
var count = 0;
while (iter.hasNext) {
iter.next()
count += 1
}
(record._1, count)
})
rdd8.saveAsTextFile(args(1))
}
}
2.5. reduce join 转换为 map join
对于 join 这种操作,不光是考虑数据倾斜的问题;即使是没有数据倾斜问题,也完全可以优先考虑,用我们讲的这种高级的 reduce join 转 map join 的技术,不要用普通的 join,去通过 shuffle,进行数据的 join;完全可以通过简单的 map,使用 map join 的方式,牺牲一点内存资源;在可行的情况下,优先这么使用。
普通的 join,那么肯定是要走 shuffle;那么,所以既然是走 shuffle,那么普通的 join,就肯定是走的是reduce join。
先将所有相同的 key,对应的 values,汇聚到一个 task 中,然后再进行 join。
reduce join 转换为 map join,适合在什么样的情况下,可以来使用?
如果两个 RDD 要进行 join,其中一个 RDD 是比较小的。一个 RDD 是 100 万数据,一个 RDD 是 1 万数据。(一个 RDD 是 1 亿数据,一个 RDD 是 100 万数据)
其中一个 RDD 必须是比较小的,broadcast 出去那个小 RDD 的数据以后,就会在每个 executor 的 blockmanager 中都驻留一份。要确保你的内存足够存放那个小 RDD 中的数据
这种方式下,根本不会发生 shuffle 操作,肯定也不会发生数据倾斜;从根本上杜绝了 join 操作可能导致的数据倾斜的问题;
对于 join 中有数据倾斜的情况,大家尽量第一时间先考虑这种方式,效果非常好;如果某个 RDD 比较小的情况下。
不适合的情况:
两个 RDD 都比较大,那么这个时候,你去将其中一个 RDD 做成 broadcast,就很笨拙了。很可能导致内存不足。最终导致内存溢出,程序挂掉。
针对两个大表的解决方案:扩容表机制