Spark代码可读性与性能优化——示例五(HashJoin)

Spark代码可读性与性能优化——示例五(HashJoin)

1. 内容点大纲

  • 数据集之间的Join
  • 大数据集、小数据集
  • 数据倾斜
  • 减少Shuffle
  • 更好的写法 flatMap + Some + None
  • 广播常见的陷阱、误操作
    *注意:和前面文章内容重复的不再做提示,已直接修改

2. 原代码

import org.apache.spark.{SparkConf, SparkContext}


object HashJoin {
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("HashJoin").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时,大数据集应在前面,小数据集在后面,具体示例如下:
    val joined = largeRDD.join(smallRDD)
    joined.collect().foreach(println)
    
  • 在做超大数据量的Join计算时,你可能会发现像上面这样做了后,Join操作的最后几个partition计算非常非常慢(WTF?)这是因为发生了数据倾斜。那么你可以利用广播小数据集到大数据集的map算子中,减少一次shuffle,消灭数据倾斜造成的问题!示例如下:
    1. 写法一: 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)
    
    1. 写法二:map + ListBuffer (一般,但适用于一分多的情况)
    val smallMap = smallRDD.collect().toMap
    val smallBroadcast = sc.broadcast(smallMap)
    
    val joined = largeRDD.flatMap { case (key, value) =>
      // 提前准备一个Buffer
      val buffer = ListBuffer[(Int, (Int, Char))]()
      val smallValue: Option[Char] = smallBroadcast.value.get(key)
      // 存在数据时,将数据放入Buffer
      if (smallValue.isDefined)
        buffer.append((key, (value, smallValue.get)))
      // 返回Buffer
      buffer
    }
    
    joined.collect().foreach(println)
    
    1. 写法三: 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)
    
  • 常见的误操作、陷阱
    1. 常见误操作:利用遍历的容器的方式查值(下面是错误示例)
    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
      }
    }
    
    1. 常见误操作:在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
      }
    }
    
    1. 常见陷阱:原数据的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。

4. 最终版:优化后的代码+注释

import org.apache.spark.{SparkConf, SparkContext}

object HashJoin2 {
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("HashJoin").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();
  }
}
发布了128 篇原创文章 · 获赞 45 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/alionsss/article/details/89529161
今日推荐