spark自省

383 阅读5分钟

MR篇

首先,MR是否理解透彻,各个阶段的自定义配置是否清楚怎么个流程,例如

  • 自定义key(MR中对key的设计很重要,虽然现在几乎不写MR了)
  • 自定义排序比较器
  • 自定义分区器
  • 自定义combine(*)
  • 自定义分组比较器
  • 理解MR过程中的排序。理解其设计目的。
  • Map输入输出源码
  • Reduce输入输出源码
  • Map端join,cache的设置。
  • 编程题:找共同好友问题。
  • MR和Spark的比较(*)

Spark基础

scala里的迭代器很牛逼,比如iterator.map返回的还是迭代器

  • spark迭代器pipeline思想(嵌套迭代器,在MR中也有体现)。以及其中的模板方法iterator是父类RDD中的方法,里面调用子类的compute。
  • flatMap是怎么进行扁平化的?
  • HadoopRDD是怎么得到数据的。
  • 通过文件创建的HadoopRDD他的分区数是怎么创建的(追源码)
  • combineByKey原理
  • cogroup原理,以及join是怎么实现的。
  • union是shuffle操作吗?
  • sortByKey会产生job吗?大数据排序问题。
  • rdd的宽窄依赖是什么?
  • mapvalues比map多了一点优化。preservesPartitioning
  • 行转列,列转行
//行转列了
val group = source.groupByKey
// 列转行,这么写是为了清楚flatMap接受的函数返回值是一个迭代器。
group.flatMap(x => x._2.iterator.map((_, x._1)))
// 另一个算子
group.flatMapValues(_.iterator)
  • groupByKey源码注释中写到了This operation may be very expensive.

统计算子

统计计算

val source = sc.parallelize(List(
  (104005, 20),
  (104005, 23),
  (104005, 25),
  (204005, 25),
  (404005, 25),
  (404005, 30)
))
  • 求max:source.reduceByKey((a, b) => Math.max(a, b)).foreach(println)
  • 求min:source.reduceByKey((a, b) => Math.min(a, b)).foreach(println)
  • 求count:source.mapValues(_ => 1).reduceByKey(_+_).foreach(println)
  • 求sum:source.reduceByKey(_+_).foreach(println)
  • 求avg(*)combineByKey,很多时候有些特殊的逻辑需要它来处理
// 这种做法不可取,复杂度高,产生了两笔shuffle
val count = source.mapValues(_ => 1).reduceByKey(_ + _)
val sum = source.reduceByKey(_ + _)
sum.join(count).mapValues(x => x._1 / x._2).foreach(println)
// 通过使用底层算子combineByKey来拿到sum和count,只通过一次shuffle
source.combineByKey(
  x => (x, 1), // 第一笔数据怎么处理
  (x: (Int, Int), y) => (x._1 + y, x._2 + 1), // 第二笔和第一笔怎么reduce
  (x: (Int, Int), y: (Int, Int)) => (x._1 + y._1, x._2 + y._2) // 两波combine怎么reduce
).mapValues(x => x._1 / x._2).foreach(println)

// combineByKey 来算 count 纯碎练api
source.combineByKey(
  _ => 1,
  (o: Int, _: Int) => o + 1,
  (x: Int, y: Int) => x + y
).foreach(println)

分区相关操作

对一个分区进行操作

有这么一个需求,每一条数据需要查一下DB做丰富。
如果使用是map算子,那么每笔数据操作时都需要DB连接打开和关闭
这就需要mapPartitionsWithIndex
val source = sc.parallelize(seq = 1 to 50, 5)
source
  .mapPartitionsWithIndex((x, y) => {
    println(s"$x---打开数据库连接")
    val iterator = new Iterator[Int] {
      override def hasNext: Boolean = {
        if (y.hasNext) {
          return true
        }
        else println("关闭数据库连接")
        false
      }

      override def next(): Int = {
        val ret = y.next
        println(s"select ..$ret.")
        ret
      }

    }
    iterator
  })
  .foreach(println)


调整分区的数量底层算子coalesce,可以有shuffle也可以没有shuffle。

val source = sc.parallelize(seq = 1 to 50, 5)

// 从4040页面看是产生了shuffle的,此行为是把分区数从5增加到6
source.mapPartitionsWithIndex((e,i)=>i.map((e,_)))
  .repartition(6)
  .mapPartitionsWithIndex((e,i)=>i.map((e,_)))
  .foreach(println)

来看源码注释
Return a new RDD that has exactly numPartitions partitions. 
Can increase or decrease the level of parallelism in this RDD. 
Internally, this uses a shuffle to redistribute data. 
If you are decreasing the number of partitions in this RDD, 
consider using coalesce, which can avoid performing a shuffle.
如果想要减少rdd的分区数,使用coalesce可以避免产生shuffle
这个在调优的时候很有用,我还没有实际经验所以先记录一下。

TopN问题

原始数据如下,求出每个月温度最高的两天。

val source = sc.parallelize(List(
  // 日期 温度
  "2019-06-01 32",
  "2019-06-02 36",
  "2019-06-03 33",
  "2019-05-01 32",
  "2019-05-02 34",
  "2019-05-03 34",
  "2019-04-02 29",
  "2019-04-02 24",
  "2019-04-22 31",
  "2019-04-22 30"
))
val t4 = source.map(line => {
  val strings = line.split(' ')
  val strings1 = strings(0).split('-')
  (strings1(0).toInt, strings1(1).toInt, strings1(2).toInt, strings(1).toInt)
})

这个综合练习各种算子的应用,以及观察4040web页面,观察作业的复杂度。 需要解决的问题如下:

  • 如何进行数据的去重(数据中存在日期相同的数据,这时候需要将相同日期的温度最大的取出来)
  • 取出同一个年月中最大温度的两天

planA

// 年月日为key,这样能够去重数据。这步处理之后一个月最多也就只有31条数据了。
t4.map(x => ((x._1, x._2, x._3), x._4))
  .reduceByKey(Math.max)
  // 改为年月为key
  .map(x => ((x._1._1, x._1._2), (x._1._3, x._2)))
  // 通过groupByKey拿到相同年月的所有数据,这里不会产生oom。一个月最多31笔
  .groupByKey()
  // 相同年月的数据 按温度倒序 取出前两条
  .mapValues(_.toList.sortWith((x, y) => x._2 > y._2).take(2))
  .foreach(println)

产生两次shuffle

planB

首先使用sortBy对年月温度进行倒序,再把年月转换为key,进行group。此时数据整体是温度降序,相同年月,拿出第一条,然后只要拿出日期和第一条不同的即为第二条。我感觉这种算法不如上面那种,io比上面多很多。

t4.sortBy(x => (x._1, x._2, x._4), ascending = false)
  .map(x => ((x._1, x._2), (x._3, x._4)))
  .groupByKey()
  .mapValues(x => {
    val iterator = x.iterator
    val result = new Array[(Int, Int)](2)
    var tuple = (0, 0)

    if (iterator.hasNext) {
      tuple = iterator.next()
      result(0) = tuple
    }

    var flag = true
    while (iterator.hasNext && flag) {
      val tuple1 = iterator.next()
      if (tuple1._1 != tuple._1) {
        result(1) = tuple1
        flag = false
      }
    }
    result.toList
  })
  .foreach(println)

两次shuffle

终极

combineByKey是底层算子,只要逻辑写的对。

t4.map(x => ((x._1, x._2), (x._3, x._4)))
  .combineByKey(
    x => {
      Array(x, (0, 0), (0, 0))
    },
    (arr: Array[(Int, Int)], x: (Int, Int)) => {
      if (arr(0)._1 == x._1 && arr(0)._1 < x._1) {
        arr(0) = x
      } else if (arr(1)._1 == x._1 && arr(1)._1 < x._1) {
        arr(1) = x
      } else if (arr(0)._1 != x._1 && arr(1)._1 != x._1) {
        arr(2) = x
      }
      // 这中排序直接对原始数据进行修改
      util.Sorting.quickSort(arr)(new Ordering[(Int, Int)] {
        override def compare(x: (Int, Int), y: (Int, Int)): Int = Integer.compare(y._2, x._2)
      })
      arr
    },
    (arr1: Array[(Int, Int)], arr2: Array[(Int, Int)]) => {
      arr1.union(arr2).sortWith((x, y) => x._2 > y._2).take(2)
    }
  )
  .mapValues(_.toList)
  .foreach(println)

复杂度仅一次shuffle

集群相关

历史服务器

spark.eventLog.enabled true
#计算层把日志写到哪里,历史服务器没起也不影响写日志
spark.eventLog.dir hdfs://mycluster/spark_log
#历史服务器进程去哪里取日志
spark.history.fs.logDirectory  hdfs://mycluster/spark_log
#历史服务器的启动
在sbin目录下 sbin/start-history-server.sh

yarn

  • 上传spark jar包去hdfs。这样不会每次提交任务都上传200多MB的jar,配置spark.yarn.jars 官方文档有写
  • 观察进程,ExecutorLaunch Application这两个是什么意思
  • spark example 求π,逻辑得知道怎么回事。