Spark核心编程系列(三)——RDD行动算子、序列化以及依赖关系

目录

RDD行动算子

RDD序列化

1、闭包检查

2、序列化方法和属性

3、Kryo序列化框架

RDD依赖关系

1、RDD血缘关系

2、依赖关系

3、RDD阶段划分

RDD任务划分

参考


RDD行动算子

    所谓的行动算子,其实就是触发作业(Job)执行的方法。底层代码调用的是环境对象的runJob方法。底层代码中会创建ActiveJob,并提交执行。

    其实算子也就是operator(操作),RDD的方法和Scala集合对象的方法不一样。集合对象的方法都是在同一个节点的内存中完成的,而RDD的方法可以将计算逻辑发送到Executor端(分布式节点)执行。所以,为了区分不同的处理效果,将RDD的方法称之为算子。

    RDD的方法外部的操作都是在Driver端执行的,而方法内部的逻辑代码是在Executor端执行的。

    RDD算子中传递的函数时会包含闭包操作,那么就会进行检测功能。若方法内部存在的数据没有进行序列化,则会进行报错。例如foreach中打印类中的元素,而类没有混入序列化特质。一般我们可以使用样例类,因为样例类自动混入序列化特质

1)reduce

函数签名

def reduce(f:(T,T)=>T):T

函数说明

    聚集RDD中所有元素,先聚合分区内数据,再聚合分区间数据。

2)collect

函数签名

def collect():Array[T]

函数说明

    在驱动程序中,以数组Array的形式返回数据集所有的元素。会将不同的分区的数据按照分区顺序采集到Driver端内存中,形成数组。

3)count

函数签名

def count():Long

函数说明

    返回RDD中元素的个数。

4)first

函数签名

def first():T

函数说明

    返回数据源中的第一个元素。

5)take

函数签名

def take(num:Int):Array[T]

函数说明

    返回数据源中N个元素。

6)takeOrdered

函数签名

def takeOrdered(num:Int)(implicit ord:Ordering[T]):Array[T]

函数说明

   返回该RDD排序后的前n个元素组成的数组。

7)aggregate

函数签名

def aggregate[U:ClassTag](zeroValue:U)(seqOp:(U,T)=>U,combOp:(U,U)=>U):U

函数说明

    分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合。

    aggregateByKey:初始值只会参与分区内计算

    aggregate:初始值会参与分区内和分区间计算

    而当分区内和分区间的计算规则相同时,可以使用fold函数,类似于foldByKey。

8)countByKey

函数签名

def countByKey():Map[K,Long]

函数说明

    统计每种key的个数。

9)save

函数签名

def saveAsTextFile(path:String):Unit
def saveAsObjectFile(path:String):Unit
def saveAsSequenceFile(
    path:String,
    codec:Option[Class[_<:CompressionCodec]]=None):Unit
)

函数说明

    将数据保存到不同格式的文件中。其中,saveAsSequenceFile方法要求数据的格式必须为K-V类型。

9)foreach

函数签名

def foreach(f:T=>Unit):Unit=withScope{
    val cleanF=sc.clean(f)
    sc.runJob(this,(iter:Iterator[T])=>iter.foreach(cleanF))
}

函数说明

    分布式遍历RDD中的每一个元素,调用指定函数。

    若先进行collect再foreach,其实是Driver端内存集合的循环遍历方法。

    若直接foreach,其实是Executor端内存数据打印。

RDD序列化

1、闭包检查

    从计算的角度,算子以外的代码都是在Driver端执行,算子里面的代码都是在Executor端执行。那么在scala的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor端执行,这个操作我们称之为闭包检测Scala2.12版本后闭包编译方式发生了改变

2、序列化方法和属性

    从计算的角度,算子以外的代码都是在Driver端执行,算子里面的代码都是在Executor端执行。

package spark.core.serializable

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

object Spark_RDD_Serializable_Study1 {
  def main(args: Array[String]): Unit = {
    // 1、创建 SparkConf 并设置 App 名称
    val conf: SparkConf = new SparkConf().
      setMaster("local[*]").
      set("spark.driver.host", "localhost").
      setAppName("rdd")

    // 2、创建 SparkContext,该对象是提交 Spark App 的入口
    val sc: SparkContext = new SparkContext(conf)

    // 3、创建一个 RDD
    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "spark"))

    // 3.1 创建一个 Search 对象
    val search = new Search("hello")

    // 3.2 函数传递,打印:ERROR Task not serializable
    search.getMatch1(rdd).collect().foreach(println)

    println("------------------------------------")

    // 3.3 属性传递,打印:ERROR Task not serializable
    search.getMatch2(rdd).collect().foreach(println)

    // 4、关闭连接
    sc.stop()
  }

  // 如果不混入特质,会报错
  class Search(query:String) extends Serializable {
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

    // 函数序列化案例
    def getMatch1 (rdd: RDD[String]): RDD[String] = {
      rdd.filter(isMatch)
    }

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      // 可以加上 val s = query,然后传入参数改为query,也可以
      rdd.filter(x => x.contains(query))
    }
  }
}

3、Kryo序列化框架

    Java的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。但是它不支持所有的系列化对象,而且要求用户注册类。

    注意:即使使用Kryo序列化,也要继承Serializable接口。

RDD依赖关系

1、RDD血缘关系

    RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

    多个连续的RDD的依赖关系,称之为血缘关系。且每个RDD会保存血缘关系。由于RDD不会保存数据,所以,当在某个环节错误,RDD为了提高容错性,需要将RDD间的关系保存下来,一旦出现错误,可以根据血缘关系将数据源重新读取进行计算。

2、依赖关系

    

    OneToOne依赖由于底层继承了窄依赖,所以OneToOne也可以称之为窄依赖,相对的,shuffle依赖可以称之为宽依赖。

    窄依赖表示每个父(上游)RDD的Partition最多被子(下游)RDD的一个Partition依赖,可以比喻为独生子女。而宽依赖表示每个父(上游)RDD的Partition被多个子(下游)RDD的一个Partition依赖,会引起Shuffle,可以比喻为多生。

3、RDD阶段划分

    DAG有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如:DAG记录了RDD的转换过程和任务的阶段。阶段划分源码:

// --------------- 1 -----------------
def collect(): Array[T] = withScope {
  // 跳进这个runJob
  val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
  Array.concat(results: _*)
}
 
 
// --------------- 2 -----------------
def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
  // 继续往里跳, 跳到最后一个runJob
  runJob(rdd, func, 0 until rdd.partitions.length)
}
 
// --------------- 3 -----------------
def runJob[T, U: ClassTag](
    rdd: RDD[T],
    func: Iterator[T] => U,
    partitions: Seq[Int]): Array[U] = {
  val cleanedFunc = clean(func)
  // 继续往里跳, 跳到最后一个runJob
  runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
}
 
 
// --------------- 4 -----------------
  def runJob[T, U] rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,partitions: Seq[Int],
callSite: CallSite,resultHandler: (Int, U) => Unit,properties: Properties): Unit = {
    val start = System.nanoTime
    // 向submitJob内部跳
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
    // ...
  }
 
// --------------- 5 -----------------
  def submitJob[T, U](rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],callSite: CallSite, resultHandler: (Int, U) => Unit,
      properties: Properties): JobWaiter[U] = {
    // 此处忽略N行。。。
 
    val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
    /* 
    * 提交到eventProcessLoop事件进程池进行操作
    * 这里需要记住post的是JobSubmitted这个类,待会会用到
    */
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      SerializationUtils.clone(properties)))
    waiter
  }

紧接着JobSubmitted方法跳转至DAGSchedulerEventProcessLoop

//--------------- 1 -----------------
/* 
* EventLoop只是一个抽象类,OnReceive方法也是一个抽象方法
* 因此转到它的唯一子类DAGSchedulerEventProcessLoop,代码如下:
*/
 
private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)
  extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging {
 
  private[this] val timer = dagScheduler.metricsSource.messageProcessingTimer
 
  /**
   * The main event loop of the DAG scheduler.
   */
  override def onReceive(event: DAGSchedulerEvent): Unit = {
    val timerContext = timer.time()
    try {
      // 继续往doOnReceive里跳
      doOnReceive(event)
    } finally {
      timerContext.stop()
    }
  }
 
//...
}
 
//--------------- 2 -----------------
private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
    // 所以刚刚提交的event是JobSubmitted,所以会匹配到这里
    case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
      dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
 
    case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
      dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)
 
    case StageCancelled(stageId, reason) =>
      dagScheduler.handleStageCancellation(stageId, reason)
 
// ....
  }
 

而handleJobSubmitted方法就是关键,里面由createResultStage,是划分阶段的核心代码。

 private[scheduler] def handleJobSubmitted(jobId: Int, finalRDD: RDD[_],
      func: (TaskContext, Iterator[_]) => _, partitions: Array[Int],
      callSite: CallSite, listener: JobListener, properties: Properties) {
 
    var finalStage: ResultStage = null
    try {
      // createResultStage这个是划分阶段的核心方法,finalStage这个变量名翻译过来是"最后阶段"
      finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
 
    } catch {
      // 处理异常相关代码
    }
    // Job submitted, clear internal data.
    barrierJobIdToNumTasksCheckFailures.remove(jobId)
 
    // 省略N行代码
    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
    submitStage(finalStage)
  }
 
// 进入createResultStage方法
private def createResultStage(
      rdd: RDD[_], func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int], jobId: Int,
      callSite: CallSite): ResultStage = {
 
    // 省略N行代码...    
    // 这个判断是否存在上一个阶段,或者创建
 
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    // 这个是创建阶段的代码
    val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
    stageIdToStage(id) = stage
    updateJobIdStageIdMaps(jobId, stage)
    stage
}
 
// 进入getOrCreateParentStages方法
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
    // 接下来分析getShuffleDependencies方法
    getShuffleDependencies(rdd).map { shuffleDep =>
      getOrCreateShuffleMapStage(shuffleDep, firstJobId)
    }.toList
}
 

接着分析getShuffleDependencies方法

 /**
   * 此方法返回给定RDD的直接父级的shuffle依赖项。
   *
   * 此函数将不会返回更上一级的祖先。 比如C依赖B,B依赖A
   * 即:A <-- B <-- C
   *
   * rdd C调用此函数只会返回 B <-C 依赖项
   *
   * This function is scheduler-visible for the purpose of unit testing.
 */
 
  private[scheduler] def getShuffleDependencies(
    rdd: RDD[_]):HashSet[ShuffleDependency[_, _, _]] = {
    
    // rdd的父依赖(后面会以此划分阶段)
    val parents = new HashSet[ShuffleDependency[_, _, _]]
 
    // 标记rdd是否访问过
    val visited = new HashSet[RDD[_]]
 
    // 用于存放用于遍历的rdd的容器
    val waitingForVisit = new ArrayStack[RDD[_]]
 
    // 将当前rdd放入遍历容器
    waitingForVisit.push(rdd)
 
    while (waitingForVisit.nonEmpty) {
      // 从容器中取出rdd
      val toVisit = waitingForVisit.pop()
      // 判断rdd是否被访问过
      if (!visited(toVisit)) {
        visited += toVisit
        // 
        toVisit.dependencies.foreach {
          // 如果是Shuffle类型依赖则加入parent
          case shuffleDep: ShuffleDependency[_, _, _] =>
            parents += shuffleDep
         // 其他依赖加入遍历容器,稍后继续遍历
          case dependency =>
            waitingForVisit.push(dependency.rdd)
        }
      }
    }
    // 最后返回parents,也就是说返回的都是ShuffleDependency类型
    // 只有进行Shuffle操作才会被划分阶段处理
    parents
  }

返回上一级的getOrCreateParentStages方法,继续向下分析getOrCreateShuffleMapStage。

private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
    getShuffleDependencies(rdd).map { shuffleDep =>
      // 此方法从字面意思上理解就是创建阶段,进入查看
      getOrCreateShuffleMapStage(shuffleDep, firstJobId)
    }.toList
}
 
 
  /**
   * Gets a shuffle map stage if one exists in shuffleIdToMapStage. Otherwise, if the
   * shuffle map stage doesn't already exist, this method will create the shuffle map stage in
   * addition to any missing ancestor shuffle map stages.
   */
 
  private def getOrCreateShuffleMapStage(
      shuffleDep: ShuffleDependency[_, _, _],
      firstJobId: Int): ShuffleMapStage = {
 
    // 判断shuffleId对应的阶段是否已经存在
    shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
      case Some(stage) =>
        stage
      
      // 未创建Stage
      case None =>
        // 找出尚未在shuffleIdToMapStage中注册的对应rdd的祖先的shuffle依赖
        // 内部代码与getShuffleDependencies类似
        getMissingAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep =>
          
          if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
            // 给祖先创建阶段
            createShuffleMapStage(dep, firstJobId)
          }
        }
        // 创建阶段
        createShuffleMapStage(shuffleDep, firstJobId)
    }
  }
  /**
   * 创建一个ShuffleMapStage,它生成给定的shuffle依赖项的分区。
   * 如果之前运行的阶段生成了相同的shuffle数据,则此功能将复制之前shuffle的输出位置,
   * 以避免不必要地重新生成数据。
   */
 
def createShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], jobId: Int): ShuffleMapStage = {
    val rdd = shuffleDep.rdd
    // 省略了一堆检查的代码
 
    val numTasks = rdd.partitions.length
 
    // 下面这两行代码已分析过,尤其是getOrCreateParentStages方法
    // 此处形成了一个递归,不断向上一级创建/获取父Stage(文章最后有流程图)
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
 
    val stage = new ShuffleMapStage(
      id, rdd, numTasks, parents, jobId, rdd.creationSite, shuffleDep, mapOutputTracker)
    
    // 将stage放入容器中
    stageIdToStage(id) = stage
    shuffleIdToMapStage(shuffleDep.shuffleId) = stage
    updateJobIdStageIdMaps(jobId, stage)
 
    stage
  }
 
 // 代码分析到此结束

    当RDD中存在shuffle依赖时,阶段会自动增加一个,阶段的数量 = shuffle依赖的数量 + 1.

ResultStage只有一个,最后需要执行的阶段。

RDD任务划分

    RDD任务切分中间分为:Application、Job、Stage和Task。

  • Application:初始化一个SparkContext即生成一个Application;
  • Job:一个Action算子就会生成一个Job;
  • Stage:Stage等于宽依赖的个数加一;
  • Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。

    注意:Application->Job->Stage->Task 每一层都是1对n的关系

核心代码:

	val tasks: Seq[Task[_]] = try {
      val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
      stage match {
        case stage: ShuffleMapStage => //ShuffleMapStage 阶段
          stage.pendingPartitions.clear()
          //partitionsToCompute决定map的size
          partitionsToCompute.map { id => //map的size决定后面的匿名函数会被调用几次,也就是Task被new的个数
            val locs = taskIdToLocations(id)
            val part = partitions(id)
            stage.pendingPartitions += id
            //新建ShuffleMapTask,任务数量取决于新建Task数量
            new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
              Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
          }

        case stage: ResultStage => //ResultStage 阶段
          partitionsToCompute.map { id =>
            val p: Int = stage.partitions(id)
            val part = partitions(p)
            val locs = taskIdToLocations(id)
            //新建ResultTask
            new ResultTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, id, properties, serializedTaskMetrics,
              Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
              stage.rdd.isBarrier())
          }
      }
    }

  override def findMissingPartitions(): Seq[Int] = {
    val job = activeJob.get
    //job.numPartitions是最后一个RDD的分区数量
    (0 until job.numPartitions).filter(id => !job.finished(id))
  }

 

(ps:这段内容有点难懂,记录一下慢慢啃了…  下一篇我尽快出)

 

参考

https://blog.csdn.net/weixin_44343282/article/details/116645095    (阶段划分和任务划分源码)

https://www.bilibili.com/video/BV11A411L7CK?p=98

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值