Spark-Core高级篇

高级应用

1、RDD的分片数量

def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }

def defaultParallelism: Int = {
    assertNotStopped()
    taskScheduler.defaultParallelism
  }

numSlices:分片数,一个分片就是一个任务

2、函数转换的问题

class SearchFunctions(val query: String) extends java.io.Serializable{
	  def isMatch(s: String): Boolean = {
	    s.contains(query)  }  
	  def getMatchesFunctionReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {   
	    // 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"     
	    rdd.filter(isMatch)  
	  }  
	  def getMatchesFieldReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
	    // 问题:"query"表示"this.query",因此我们要传递整个"this"
	    rdd.filter(x => x.contains(query))   
	  }  
	  def getMatchesNoReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {    
	    // 安全:只把我们需要的字段拿出来放入局部变量中
	  val query_ = this.query    
	    rdd.filter(x => x.contains(query_))  
	  } 
} 

<1>如果RDD的转换中使用到了class中的方法或者变量,那么该class需要支持序列化
<2>如果通过局部变量的方式将class中的变量赋值为局部变量,那么不需要传递对象

3、RDD的持久化

def persist(newLevel: StorageLevel): this.type = {
    if (isLocallyCheckpointed) {
      // This means the user previously called localCheckpoint(), which should have already
      // marked this RDD for persisting. Here we should override the old storage level with
      // one that is explicitly requested by the user (after adapting it to use disk).
      persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
    } else {
      persist(newLevel, allowOverride = false)
    }
  }

  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

RDD是通过persist方法或者是cache方法可以将前面的计算结果缓存,默认情况下persist()会把数据以序列化的形式缓存在JVM的堆空间中;但是并不是这俩个方法被调用的时候立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点内存中,给后面提供重用。
而且通过观察源码我么可以发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别有很多种,存储级别在object的StorageLevel中定义的。

object StorageLevel {
  val NONE = new StorageLevel(false, false, false, false)
  val DISK_ONLY = new StorageLevel(true, false, false, false)
  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
  val MEMORY_ONLY = new StorageLevel(false, true, false, true)
  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
  val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

注意:在存储级别的末尾加上"_2"来把持久化数据存为俩份。

在这里插入图片描述

scala> val rdd = sc.makeRDD(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at makeRDD at <console>:25

scala> val nocache = rdd.map(_.toString+"["+System.currentTimeMillis+"]")
nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[20] at map at <console>:27

scala> val cache =  rdd.map(_.toString+"["+System.currentTimeMillis+"]")
cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[21] at map at <console>:27

scala> cache.cache
res24: cache.type = MapPartitionsRDD[21] at map at <console>:27

scala> nocache.collect
res25: Array[String] = Array(1[1505479375155], 2[1505479374674], 3[1505479374674], 4[1505479375153], 5[1505479375153], 6[1505479374675], 7[1505479375154], 8[1505479375154], 9[1505479374676], 10[1505479374676])

scala> nocache.collect
res26: Array[String] = Array(1[1505479375679], 2[1505479376157], 3[1505479376157], 4[1505479375680], 5[1505479375680], 6[1505479376159], 7[1505479375680], 8[1505479375680], 9[1505479376158], 10[1505479376158])

scala> nocache.collect
res27: Array[String] = Array(1[1505479376743], 2[1505479377218], 3[1505479377218], 4[1505479376745], 5[1505479376745], 6[1505479377219], 7[1505479376747], 8[1505479376747], 9[1505479377218], 10[1505479377218])

scala> cache.collect
res28: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])

scala> cache.collect
res29: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])

scala> cache.collect
res30: Array[String] = Array(1[1505479382745], 2[1505479382253], 3[1505479382253], 4[1505479382748], 5[1505479382748], 6[1505479382257], 7[1505479382747], 8[1505479382747], 9[1505479382253], 10[1505479382253])

cache.persist(org.apache.spark.storage.StorageLevel.MEMORY_ONLY)

缓存是有可能丢失的,或者存储于内存中的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也可以保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,是因为由于RDD的各个Partition是相互独立的,因此只需要将丢失的部分计算即可,并不需要重新计算全部的Partition。

注意:使用Tachyon可以实现堆外缓存。

4、RDD的检查点机制

Spark中对于数据的保存除了持久化之外,还提供了一种检查点机制。虽然cache和checkpoint都是给RDD提供缓存作用的,但是俩这存在着显著地区别:cache是把RDD计算出来之后放在内存中,checkpoint是把RDD保存在HDFS中,是多副本可靠存储,所以依赖链可以丢掉了,就斩断了依赖链是通过赋值实现的高容错性。
适合场景:
<1>DAG中的Lineage过长,如果要是重新计算的话,开销太大
<2>在宽依赖上做checkpoint获得的收益会更大
在这里插入图片描述

scala> val data = sc.parallelize(1 to 100 , 5)
data: org.apache.spark.rdd.RDD[Int] =ParallelCollectionRDD[12] at parallelize at <console>:12
 
scala> sc.setCheckpointDir("hdfs://master01:9000/checkpoint")
 
scala> data.checkpoint
 
scala> data.count

scala> val ch1 = sc.parallelize(1 to 2)
ch1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[33] at parallelize at <console>:25

scala> val ch2 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[36] at map at <console>:27

scala> val ch3 = ch1.map(_.toString+"["+System.currentTimeMillis+"]")
ch3: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[37] at map at <console>:27

scala> ch3.checkpoint

scala> ch2.collect
res62: Array[String] = Array(1[1505480940726], 2[1505480940243])

scala> ch2.collect
res63: Array[String] = Array(1[1505480941957], 2[1505480941480])

scala> ch2.collect
res64: Array[String] = Array(1[1505480942736], 2[1505480942257])

在checkpoint过程中,该RDD的所有依赖于父RDD中的信息将全部转移出。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

5、RDD运行方式

<1>RDD任务划分

在这里插入图片描述

小结:

  1. 一个jar包就是一个Application
  2. 一个行动操作就是一个job,对应Hadoop中的一个MapReduce任务
  3. 一个Job有很多Stage组成,划分Stage是从后往前划分,遇到宽依赖则将前面的所有转换分为一个Stage
  4. 一个Stage是由很多Task组成,一个分区被一个Task所处理,所有分区数也叫做并行度
    RDD的运行规划图如下:
    在这里插入图片描述

<2>RDD依赖关系

<1>宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,同时会引起shuffle
在这里插入图片描述
<2>窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个Partition使用
在这里插入图片描述

<3>DAG的生成

在这里插入图片描述

需要用Oozie来实现资源的调度

<4>Lineage

在这里插入图片描述
RDD只支持粗粒度转换,即使是在大量记录上执行的单个操作。将会创建RDD的一系列Lineage(即血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转化安行为,当该RDD的部分分区数据丢失时,它就能够根据这些信息来重新计算和恢复丢失的数据分区。

6、键值对RDD分区

Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce个数。
在这里插入图片描述
但是HashPartitioner存在一些弊端:可能会导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。这是由于HashPartitioner分区原理就是对于给定的value,计算器hashCode并除以分区个数取余,如果余数小于0,则用余数+分区个数,最后返回值就是这个key所属分区的ID
所以更多使用RangePartitioner:尽量保存每个分区中的数据量的均匀,而且分区与分区之间是有序的,一个分区中的 元素肯定都是比另一个分区内的元素小或者是大的。这种分区的作用:将一定范围内的数映射到某一个分区内,在实现中分界算法尤为重要,用到了水塘抽样算法。

注意:

 1. 只有key-value类型的RDD才有分区的,非key-value类型的RDD分区值是None
 2. 每个RDD的分区范围:0~numPartitions-1,决定这个值是属于哪个分区
 3. 自定义分区主要通过继承partitioner抽象类实现,需要实现俩个方法
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

class CustomerPartitioner(numPartition:Int) extends Partitioner{

  //返回分区的总数
  override def numPartitions: Int = {
    numPartition
  }

  //根据传入的key返回分区的索引
  override def getPartition(key: Any): Int ={
    key.toString.toInt % numPartition
  }
}

object CustomerPartitioner {
  def main(args: Array[String]): Unit = {

    val sc = new SparkContext(new SparkConf().setAppName("partitions").setMaster("local[*]"))

    val rdd = sc.makeRDD(0 to 10,1).zipWithIndex()

    val r = rdd.mapPartitionsWithIndex((index,items) => Iterator(index+"["+items.mkString(",")+"]")).collect()
    for (i <- r){
      println(i)
    }

    val rdd1 = rdd.partitionBy(new CustomerPartitioner(5))
    val r1 = rdd1.mapPartitionsWithIndex((index,items) => Iterator(index+"["+items.mkString(",")+"]")).collect()
    for (i <- r1){
      println(i)
    }
    sc.stop()
  }
}

运行结果如下:
在这里插入图片描述

7、RDD进阶

<1>累计器

Spark内部提供了一个默认的累加器,但是只能用于求和(局限性)
使用方法:

  1. 通过accumulator声明一个累加器,0是初始值
  2. 转换或者是行动操作中,通过blanklines+=n
  3. 在driver程序中,通过blanklines.value来获取值
  4. 累加器是懒执行的,需要行动触发
scala> val license = sc.textFile("./LICENSE")
license: org.apache.spark.rdd.RDD[String] = ./LICENSE MapPartitionsRDD[1] at textFile at <console>:24

scala> val blanklines = sc.accumulator(0)
warning: there were two deprecation warnings; re-run with -deprecation for details
blanklines: org.apache.spark.Accumulator[Int] = 0

scala> val rdd = license.flatMap(line => {
     |         if (line == "") {
     |             blanklines += 1
     |         }
     |         line.split(" ")
     |      })
rdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at <console>:28

scala> rdd.count
res4: Long = 3453

scala> blanklines.value
res5: Int = 48

<2>自定义累加器

使用方法:

  1. 需要继承AccumulatorV2抽象类,In就是输入的类型,Out是累加器输出的数据
  2. 如何使用:
    . [1] 通过accumulator声明一个累加器,0位初始值
    . [2]通过sc.register注册一个新的累加器
    . [3]通过累加器实例名.add来添加数据
    . [4]通过累加器实例名.value来获取累加器的值

3.最好不要在转换的时候访问累加器,而是在行动操作时候访问
4.转换或者行动操作过程中不能访问累加器的值,只能添加
具体操作如下:

package sparkAccumlator

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

import scala.collection.mutable

class Accumlator extends AccumulatorV2[String,mutable.HashMap[String,Int]]{

  //定义一个累加器
  private val _hashAcc = new mutable.HashMap[String,Int]()

  //检测是否为空
  override def isZero: Boolean = {
    _hashAcc.isEmpty
  }
  //拷贝一个新的累计器
  override def copy(): AccumulatorV2[String,mutable.HashMap[String,Int]] = {
    val newAcc = new Accumlator()
    _hashAcc.synchronized{
      newAcc._hashAcc ++= (_hashAcc)
    }
    newAcc
  }
  //重置一个新的累加器
  override def reset(): Unit = {
    _hashAcc.clear()
  }
  //每一个分区中用于添加数据的方法
  override def add(v: String): Unit = {
    _hashAcc.get(v) match {
      case None => _hashAcc += ((v, 1))
      case Some(a) => _hashAcc += ((v, a+1))
    }
  }
  //合并每一个分区的输出
  override def merge(other: AccumulatorV2[String,mutable.HashMap[String,Int]]): Unit = {
    other match {
      case o:AccumulatorV2[String,mutable.HashMap[String,Int]] => {
        for ((k, v) <- o.value){
          _hashAcc.get(k) match {
            case None => _hashAcc += ((k, v))
            case Some(a) => _hashAcc += ((k,a+v))
          }
        }
      }
    }

  }
  //输出值
  override def value: mutable.HashMap[String, Int] = {
    _hashAcc
  }
}

object Accumlator {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("accumlator").setMaster("local[*]")
    val sc = new SparkContext(sparkConf)
    val hashAcc = new Accumlator()
    //注册累计器
    sc.register(hashAcc)
    val rdd = sc.makeRDD(Array("a", "b", "c", "a", "b", "c", "d"))
    rdd.foreach(hashAcc.add(_))
    for ((k, v) <- hashAcc.value) {
      println("【" + k + ":" + v + "】")
    }
    sc.stop()
  }
}

在这里插入图片描述

<3>广播变量

  • [1]如果使用本地变量不采用广播变量形式,那么每个分区需要进行一个拷贝
  • [2]如果使用了广播变,那么每一个Excutor中会有该变量的一次拷贝,一个Excutor[JVM进程]中有很多分区
    使用方法:
  • [1] 通过sc.broadcast来创建一个广播变量
  • [2]通过value方法来获取广播变量的内容
  • [3]广播变量只会给每个节点分发一次,因此用该作为只读值处理(修改这个值不会影响别的节点)
    具体代码如下:
scala> val broadcast = sc.broadcast(Array(1, 2, 3, 4, 5))
broadcast: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(2)

scala> broadcast.value
res6: Array[Int] = Array(1, 2, 3, 4, 5)

注意:广播变量用来处理高效分发较大的对象;主要用在百兆数据的分发。

猜你喜欢

转载自blog.csdn.net/weixin_44240370/article/details/89279837
今日推荐