Spark代码可读性与性能优化——示例五(MapJoin)
1. 内容点大纲
-
数据集之间的Join
-
大数据集、小数据集
-
数据倾斜
-
减少Shuffle
-
更好的写法 flatMap + Some + None
-
广播常见的陷阱、误操作
注意:和前面文章内容重复的不再做提示,已直接修改
2. 原代码
import org.apache.spark.{SparkConf, SparkContext}
object MapJoin {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("MapJoin").setMaster("local[4]")
val sc = new SparkContext(conf)
val smallRDD = sc.parallelize(
Seq((1, 'a'), (2, 'a'), (3, 'y'), (4, 'a')),
4
)
val largeRDD = sc.parallelize(
for (x <- 1 to 10000) yield (x % 4, x),
4
)
// 对2份数据做join
val joined = smallRDD.join(largeRDD )
joined.collect().foreach(println)
sc.stop();
}
}
3. 代码性能、可读性优化
- 大数据集和小数据集做Join时,大数据集应在前面,小数据集在后面(SQL恰好相反),具体示例如下:
val joined = largeRDD.join(smallRDD) joined.collect().foreach(println)
- 在做超大数据量的Join计算时,你可能会发现像上面这样做了后,Join操作的最后几个partition计算非常非常慢(WTF?),这是因为发生了数据倾斜。
- 那么你可以利用广播小数据集到大数据集的map算子中,减少一次shuffle,消灭数据倾斜造成的问题!示例如下:
- 写法一: map + fliter (一般)
// 将小部分数据集collect到Driver端,转为Map,然后以广播的形式分发到各个Executor val smallMap = smallRDD.collect().toMap val smallBroadcast = sc.broadcast(smallMap) val joined = largeRDD.map { case (key, value) => // 从广播中获取value val smallValue: Option[Char] = smallBroadcast.value.get(key) (key, (value, smallValue)) }.filter(_._2._2.isDefined) // 过滤掉无效数据 joined.collect().foreach(println)
- 写法二:map + ListBuffer (一般,但适用于一分多的情况)
val smallMap = smallRDD.collect().toMap val smallBroadcast = sc.broadcast(smallMap) val joined = largeRDD.flatMap { case (key, value) => // 提前准备一个Buffer // 注意:如果单行数据拆出数据条数过多(例如几百MB),会导致ListBuffer过大,最终假死(性能极低) val buffer = ListBuffer[(Int, (Int, Char))]() // ListBuffer val smallValue: Option[Char] = smallBroadcast.value.get(key) // 存在数据时,将数据放入Buffer if (smallValue.isDefined) buffer.append((key, (value, smallValue.get))) // 返回Buffer buffer } joined.collect().foreach(println)
- 写法三: flatMap + Some + None (最佳)
val smallMap = smallRDD.collect().toMap val smallBroadcast = sc.broadcast(smallMap) val joinedRDD = largeRDD.flatMap { case (key, value) => // 利用匹配模式,可读性较高 smallBroadcast.value.get(key) match { // 返回类型为Option,为None的将被flatMap打平,从而过滤掉 case Some(v) => Some((key, (value, v))) case None => None } } joinedRDD.collect().foreach(println)
- 常见的误操作、陷阱
- 常见误操作:利用遍历的容器的方式查值(下面是错误示例)
val small = smallRDD.collect() val smallBroadcast = sc.broadcast(small) val joined = largeRDD.flatMap { case (key, value) => // 使用容器的find找到key相等的值,将会遍历容器,效率较差 // 正确的做法:使用HashMap,利用hash算法根据key查找value,效率更高 smallBroadcast.value .find(_._1 == key) match { case Some(v) => Some((key, (value, v))) case None => None } }
- 常见误操作:在map、flatMap类算子中转换广播数据类型(下面是错误示例)
val small = smallRDD.collect() val smallBroadcast = sc.broadcast(small) val joined = largeRDD.flatMap { case (key, value) => // 在RDD的map、flatMap等算子中,将广播再次转换为其他数据(此处是toMap) // RDD中有多少条数据,这个转换就会被执行多少次,对性能影响极大 // 正确做法:应该在广播前,将需要广播的数据转换好 smallBroadcast.value.toMap .get(key) match { case Some(v) => Some((key, (value, v))) case None => None } }
- 常见陷阱:原数据的key不唯一(下面是正确示例)
// 原数据key不唯一 val smallRDD = sc.parallelize( Seq((1, 'a'), (1, 'c'), (2, 'a'), (3, 'x'), (3, 'y'), (4, 'a')), 4 ) val largeRDD = sc.parallelize( for (x <- 1 to 10000) yield (x % 4, x), 4 ) // 当原数据的key不唯一时,应该提前分组 val smallMap = smallRDD.groupByKey() .collect() .toMap val smallBroadcast = sc.broadcast(smallMap) val joinedRDD = largeRDD.flatMap { case (key, value) => smallBroadcast.value.get(key) match { case Some(iter) => iter.map(v => (key, (value, v))) case None => Iterable[(Int, (Int, Char))]() } } joinedRDD.collect().foreach(println)
- 注意:
- 利用广播的方式,需要Driver、Executor内存足以容纳smallRDD.collect()后的数据
- 当Driver、Executor内存不足以容纳小数据集collect的大小,那么你还是只有走Join算子
- 当然遇上广播的数据集太大,内存不足的情况,你还可以将广播的数据集分成n份,分n次进行map join
- 提前对原始数据进行过滤(空值、异常值等),可以优化Join性能
4. 最终版:优化后的代码+注释
import org.apache.spark.{SparkConf, SparkContext}
object MapJoin2 {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("MapJoin2").setMaster("local[4]")
val sc = new SparkContext(conf)
// 原数据的key不唯一
val smallRDD = sc.parallelize(
Seq((1, 'a'), (1, 'c'), (2, 'a'), (3, 'x'), (3, 'y'), (4, 'a')),
4
)
val largeRDD = sc.parallelize(
for (x <- 1 to 10000) yield (x % 4, x),
4
)
// 利用广播小数据集到大数据集的map算子中,减少一次shuffle,消灭数据倾斜造成的问题
// 当本地内存足以容纳smallRDD.collect()后的数据时,可以这样做
// 将小部分数据集collect到Driver端,转为Map,然后以广播的形式分发到各个Executor
// 考虑当原数据的key不唯一时,应该提前分组
val smallMap = smallRDD.groupByKey()
.collect()
.toMap
val smallBroadcast = sc.broadcast(smallMap)
val joinedRDD = largeRDD.flatMap { case (key, value) =>
// 利用匹配模式,可读性较高
smallBroadcast.value.get(key) match {
// 返回类型为Option,为None的将被flatMap打平,从而过滤掉
case Some(iter) => iter.map(v => (key, (value, v)))
case None => Iterable[(Int, (Int, Char))]()
}
}
joinedRDD.collect().foreach(println)
sc.stop();
}
}