目录
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 (阶段划分和任务划分源码)