spark1.6 WordCount排序取Top 10

对于这个话题的帖子,网上一大堆,大多也就是相互copy,转载。本文当然不会落入俗套,网上通用的实体,不仅本身存在一定的问题,更是不能在生产上运行。废话不多说,先列举网上最多的一种实现:

val dataRDD =  sparkContext.textFile("data")
dataRDD.flatMap(_.split(",")).map((_, 1L)).
  reduceByKey((a ,b) => a + b).
  sortBy(x => (x._2,x._1), false).make(10).foreach(println)

拷贝,运行,执行成功,就理解了,可以上线发布了!浮躁的心也让现网越来越多的坑。来看看take的源码实现

def take(num: Int): Array[T] = withScope {
    if (num == 0) {
      new Array[T](0)
    } else {
      val buf = new ArrayBuffer[T]
      val totalParts = this.partitions.length
      var partsScanned = 0
      while (buf.size < num && partsScanned < totalParts) {
        var numPartsToTry = 1
        if (partsScanned > 0) {
          if (buf.size == 0) {
            numPartsToTry = partsScanned * 4
          } else {
            numPartsToTry = Math.max((1.5 * num * partsScanned / buf.size).toInt - partsScanned, 1)
            numPartsToTry = Math.min(numPartsToTry, partsScanned * 4)
          }
        }
        val left = num - buf.size
        val p = partsScanned until math.min(partsScanned + numPartsToTry, totalParts)
        // 第一次执行第0个分区,并返回take(num),如果第0个分区的数据数量>num,
        // 那下一步的buf.size将=num。那么rdd.take(num)返回的仅仅是第0分区的数据
        val res = sc.runJob(this, (it: Iterator[T]) => it.take(left).toArray, p)
        res.foreach(buf ++= _.take(num - buf.size))
        partsScanned += numPartsToTry
      }
      buf.toArray
    }
  }

注释中,已经提到了问题所在。那既然只取了一个分区的数据,那让数据重分区,在执行take前将分区数降为1不就好了?先上代码:

val dataRDD =  sparkContext.textFile("data")
dataRDD.flatMap(_.split(",")).map((_, 1L)).
  reduceByKey((a ,b) => a + b).
  sortBy(x => (x._2,x._1), false,1).make(10).foreach(println)

sortBy增加第三个参数,相当于repartition。这对于普通业务来说确实没有太大的问题,但是试想一下现在有一个100G的文件,重复率50%,通过reduceByKey后仍然有50G+,repartition时所有数据都将聚合到一个executor中。这不仅对该executor内存有着很大的要求,shuffle时的网络传输也会耗费很长的时间。并且文件更大,影响也就越大。这就好比在进行计数时为啥不用groupByKey而采用reduceByKey一样。既然取top 10,那么每个分区都取100,再进行shuffle,大大的缩减了数据量。那不就用top嘛?确实是用top,但是不是简单的如网上大部分列举的:

val dataRDD =  sparkContext.textFile("data")
dataRDD.flatMap(_.split(",")).map((_, 1L)).
  reduceByKey((a ,b) => a + b).
  sortBy(x => (x._2,x._1), false,1).top(10).foreach(println)

如果仅仅如上面,那肯定是有问题的,先看看top的源码实现:

def top(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
    takeOrdered(num)(ord.reverse)
  }

通过前面的计数,现有的数据是键值对的形式,或者说是tuple。top需要传入一个Ordering的隐式转换,通过依赖关系可知,Ordering的实现中并不存在一个Tuple或者Map的Ordering,如下图:


image.png

那问题也就在这,既然没有对于tuple的Ordering,那如果采用默认值,将会使用StringOrdering。显然是不对的,所以正确的做法就是要实现一个KeyValueOrdering。takeOrdered中会对每个分区取take,然后再进行排序再取take。所以,生产上正确的做法是这样:

implicit object KeyVauleOrdering extends scala.math.Ordering[(String, Long)] {

    def compare(x: (String, Long), y: (String, Long)) =
      if (x._2.compareTo(y._2) == 0){
        x._1.compareTo(y._1)
      } else{
        x._2.compareTo(y._2)
      }
  }

val dataRDD =  sparkContext.textFile("data")
dataRDD.flatMap(_.split(",")).map((_, 1L)).
  reduceByKey((a ,b) => a + b).
  sortBy(x => (x._2,x._1), false).top(10)(KeyVauleOrdering).foreach(println)

你可能感兴趣的:(spark1.6 WordCount排序取Top 10)