1.任务提交分析
这里以org.apache.spark.examples.SparkPi为例。当执行reduce(_+_)方法时,其底层调用了sc.runJob方法。核心代码如下:
/**
* 注释:(rdd, func, partitions, callSite, resultHandler, properties)
* 1、应用程序调用 action 算子
* 2、sparkContext.runJob()
* 3、dagScheduler.runJob()
* 4、TaskScheduler.submitTasks(new TaskSet())
* 5、SchedulerBackEnd.driverEndpoint 提交任务
*/
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
其中runJob方法中执行的核心代码:
/**
* TODO 注释: 提交任务
* 参数解析:
* 1、rdd:要在其上运行任务的参数RDD目标RDD
* 2、func:在RDD的每个分区上运行的函数
* 3、partitions:要运行的分区的集;某些作业可能不希望在目标RDD的所有分区上进行计算,例如,对于 first() 之类的操作。
* 4、callSite:在用户程序中调用此作业的位置
* 5、resultHandler:回调函数,以将每个分区结果传递给Xxx
* 6、properties:要附加到此作业的scheduler属性,例如fair scheduler pool name
*
* rdd1.xx1().xx2().xx3().xx4() 这里的 rdd = rdd1.xx1().xx2().xx3()
*
*/
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
其内部核心代码为:
/**
* TODO 注释:
* 第一步:封装一个JobWaiter对象;
* 第二步:将 JobWaiter 对象赋值给 JobSubmitted 的 listener 属性,
* 并将 JobSubmitted(DAGSchedulerEvent事件)对象传递给 eventProcessLoop 事件循环处理器。
* eventProcessLoop 内部事件消息处理线程将会接收 JobSubmitted 事件,
* 并调用dagScheduler.handleJobSubmitted(...) 方法来处理事件;
* 第三步:返回 JobWaiter 对象。
*/
val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
/**
* TODO 注释:这是提交任务运行
* eventProcessLoop 就是当初 DAGScheduler 在初始化的时候,创建的一个 DAGSchedulerEventProcessLoop
* 这个组件主要负责:任务的提交执行
* 把 JobSubmitted 这个消息,放入了 eventQueue 队列中
*/
eventProcessLoop.post(JobSubmitted(jobId, rdd, func2, partitions.toArray, callSite, waiter, SerializationUtils.clone(properties)))
// TODO 注释: 返回结果对象的引用
waiter
通过eventProcessLoop.post(...)将任务放入了eventQueue队列中,有通过eventProcessLoop.start()方法将任务提交。前面有见到以下代码是等待任务的提交,正好有对应上:
/**
* TODO 注释: driver 中,初始化了一个 dagSchedudelr
* 它里面又初始化了一个 eventThread 专门用来处理 JobSubmitted
*/
// Exposed for testing.
private[spark] val eventThread = new Thread(name) {
setDaemon(true)
override def run(): Unit = {
try {
while (!stopped.get) {
// TODO 注释:获取消息
// TODO 注释:一定要注意:当 sparkContext 还没有初始化好的时候,是不执行 sc.runJob 提交任务的。
// TODO 注释:当执行 sc.runJob(sc) 的时候,就会提交 Job 到这儿来。
val event = eventQueue.take()
try {
/**
* TODO 注释:根据事件的类型,调用不同的 handleXXX 方法来进行处理。
* 当接收到任务提交的时候: event = JobSubmitted
*/
onReceive(event)
} catch {
case NonFatal(e) => try {
onError(e)
} catch {
case NonFatal(e) => logError("Unexpected error in " + name, e)
}
}
}
} catch {
case ie: InterruptedException => // exit even if eventQueue is not empty
case NonFatal(e) => logError("Unexpected error in " + name, e)
}
}
}
在这里通过onReceive进行任务的提交,任务提交给了DAGScheduler
2.Stage切分与提交
2.1.Stage切分
任务提交核心代码如下,包含了2个方面,Stage切分与Task分发与执行。在DAGScheduler.handleJobSubmitted(...)中有如下代码:
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
这个finalRDD就是rdd链条中的最后一个RDD,也就是触发sc.runJob()方法执行的RDD。必然是针对某个RDD调用了一个action算子才触发执行的,则该RDD就是finalRDD。
注意以下概念:
- ShuffleMapStage + ResultStage
- ShuffleMapTask + ResultTask
- ShuffleDependency + NarrowDependency
在createResultStage(...)方法,返回所有的ResultStage,其中包含了所有的父Stage。如下注释:
/**
* TODO 注释: Stage 切分
* 这个 finalRDD 就是 rdd链条中的最后一个 RDD,也就是触发 sc.runJob() 方法执行的RDD
* 必然是针对某个 RDD 调用了一个 action 算子才触发执行的,则该 RDD 就是 finallRDD
*
* stage切分的核心方法:createResultStage
* 返回的是一个 ResultStage对象,但是这个对象中,会包含他的所有的 父stage
* 这样做的最大好处:容错!
* spark DAG引擎:血脉关系 RDD之间的依赖被构建成了 dependency ,也被构建成了 stage 之间的依赖关系
*/
可查看下面的详细代码:
/**
* TODO Create a ResultStage associated with the provided jobId.
* 进行Stage切分的详细方法实现
*/
private def createResultStage(rdd: RDD[_], func: (TaskContext, Iterator[_]) => _, partitions: Array[Int], jobId: Int,
callSite: CallSite): ResultStage = {
checkBarrierStageWithDynamicAllocation(rdd)
checkBarrierStageWithNumSlots(rdd)
checkBarrierStageWithRDDChainPattern(rdd, partitions.toSet.size)
// TODO 注释:获得父stage,若没有shuffle则返回空List
// TODO 注释:获取当前Stage的parent Stage,这个方法是划分Stage的核心实现
// 所有的父类stage都已经构建完成并返回给parents。
val parents = getOrCreateParentStages(rdd, jobId)
// TODO 注释: finalStage=resultStage 的 stageID 这里返回的是最后一个stage的Id
val id = nextStageId.getAndIncrement()
// TODO 注释:创建当前最后的ResultStage
// TODO 注释:parents 所有的 父 stage
val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
// TODO 注释:将 ResultStage 与 stageId 相关联, 保存在 map 中
// TODO 注释:stageIdToStage = new HashMap[Int, Stage]
stageIdToStage(id) = stage
// TODO 注释:更新该job中包含的stage
updateJobIdStageIdMaps(jobId, stage)
// TODO 注释:返回ResultStage
stage
}
在上面的getOrCreateParentStages(rdd, jobId)代码,获取父Stage,若没有父Stage,则返回空List。所有的父类stage都已经构建完成并返回给parents。调用的方法是以下代码:
getShuffleDependencies(rdd).map { shuffleDep => getOrCreateShuffleMapStage(shuffleDep, firstJobId) }.toList
虽然只有一行代码,但是做了很多事情。
- 遍历 RDD 的依赖关系,找到第一个 ShuffleDependency (可能多个,也可能没有)。然后放入 HashSet 并返回
- 如果获取不到 ShuffleDependency,逻辑在此终止返回空 list
- 里面会创建当前 ShuffleDependency 的所有父ShuffleMapStage
1.getShuffleDependencies(rdd),这里的rdd是指finalRDD。其实这一步只会返回当前RDD的紧邻的父依赖Stage。具体的可以查看此方法的注释。
/**
* Returns shuffle dependencies that are immediate parents of the given RDD.
*
* This function will not return more distant ancestors.
* For example, if C has a shuffle dependency on B which has a shuffle dependency on A:
* A <-- B <-- C
* calling this function with rdd C will only return the B <-- C dependency.
* This function is scheduler-visible for the purpose of unit testing.
*
*/
2.getOrCreateShuffleMapStage(shuffleDep, firstJobId)这里用递归的方式,最终是将所有的Stage放在以下2个Map当中。
// TODO 注释:自增id对应stage
private[scheduler] val stageIdToStage = new HashMap[Int, Stage]
// TODO 注释:自增id对应ShuffleMapStage
private[scheduler] val shuffleIdToMapStage = new HashMap[Int, ShuffleMapStage]
具体的可以看下这个方法:
/**
* TODO 如果 shuffleIdToMapStage 中存在 shuffle,则获取 shuffle map stage。
* 否则,如果 shuffle map stage 不存在,该方法将创建 shuffle map stage
* 以及任何丢失的 parent shuffle map stage。
*
* 第一次来到这里传进来的是左到右首个shuffleDep,没有父stage,
* shuffleIdToMapStage也没有记录,会调 getMissingAncestorShuffleDependencies 并创建 stage
* 之后进来可直接根据 shuffleIdToMapStage 提取到 ShuffleMapStage
*
* 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 = {
// TODO 注释: 根据每个 ShuffleDep 的 shuffleID 来获取 Stage 对象
shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
// TODO 注释:如果有,则直接返回
case Some(stage) => stage
// TODO 注释:如果还有 ShuffleDependency 没有构建 ShuffleMapStage 则创建一个
// TODO 注释:如果这个stage没有构建
// TODO 注释:第一件事:先把这个stage的所有父stage都构建出来
// TODO 注释:然后再次构建当前stage
case None =>
// TODO 注释:给当前 stage 的所有父stage 创建 ShuffleMapStage
// TODO 注释:举例:C <---B <-- A
// 这里使用了递归。将所有的父依赖,即stage都放入
// Create stages for all missing ancestor shuffle dependencies.
getMissingAncestorShuffleDependencies(shuffleDep.rdd)
.foreach { dep => // Even though getMissingAncestorShuffleDependencies only returns shuffle dependencies
// that were not already in shuffleIdToMapStage, it's possible that by the time we
// get to a particular dependency in the foreach loop, it's been added to
// shuffleIdToMapStage by the stage creation process for an earlier dependency. See
// SPARK-13902 for more information.
if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
createShuffleMapStage(dep, firstJobId)
}
}
/**
* TODO
* 注释:为指定的 ShuffleDependency 创建一个 ShuffleMapStage
* 构建当前Stage。
*/
// Finally, create a stage for the given shuffle dependency.
createShuffleMapStage(shuffleDep, firstJobId)
}
}
总结如下:
1、createResultStage(传入finalRDD获得ResultStage) ->2
2、getOrCreateParentStages(传入rdd获得父stage) ->3->4
3、getShuffleDependencies(传入rdd获得宽依赖)
4、getOrCreateShuffleMapStage(传入宽依赖获得ShuffleMapStage) ->5->6
5、getMissingAncestorShuffleDependencies(传入一个rdd获得所有宽依赖) ->3
6、createShuffleMapStage(传入宽依赖获得ShuffleMapStage) ->2
2.2.Stage提交
核心代码:submitStage(finalStage)。提交Stage, 包含了所有的父stage,这里面也是通过递归来提交执行stage。
/**
* TODO
* 注释:
* 1、参数:stage是 ResultStage,目标就是提交最后一个Stage
* 2、必须要所有的父 Stage 执行完毕之后才能执行
* 3、存在着递归就执行的方式:recursively
*/
/** Submits stage, but first recursively submits any missing parents. */
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) {
logDebug(s"submitStage($stage (name=${stage.name};" + s"jobs=${stage.jobIds.toSeq.sorted.mkString(",")}))")
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
// TODO 注释:获取父 stage
// TODO 注释:父stage不为空,则先提交父stage,然后提交该stage
// TODO 注释:获取该stage未提交的父stages,并按stage id从小到大排序
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: " + missing)
if (missing.isEmpty) {
/**
* TODO
* 注释:若无未提交的父stage, 则提交该stage对应的tasks
*/
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
// TODO 注释:提交一个stage运行
submitMissingTasks(stage, jobId.get)
} else {
for (parent <- missing) {
/**
* TODO
* 注释: 递归提交stage
* 若存在未提交的父stage, 依次提交所有父stage (若父stage也存在未提交的父stage, 则提交之, 依次类推);
* 并把该stage添加到等待stage队列中
*/
submitStage(parent)
}
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}
真正提交Stage执行的代码为submitMissingTasks(stage, jobId.get)。查看详细处理过程。分为四步:
/**
* TODO
* 注释: 提交一个stage的Task来执行
* Step1: 得到RDD中需要计算的partition
* 对于Shuffle类型的stage,需要判断stage中是否缓存了该结果;对于Result类型的Final Stage,
* 则判断计算Job中该partition是否已经计算完成。这么做(没有直接提交全部tasks)的原因是,
* stage中某个task执行失败其他执行成功的时候就需要找出这个失败的task对应要计算的partition而不是要计算所有partition
* Step2: 序列化task的binary
* Executor可以通过广播变量得到它。每个task运行的时候首先会反序列化
* Step3: 为每个需要计算的partiton生成一个task
* ShuffleMapStage对应的task全是ShuffleMapTask; ResultStage对应的全是ResultTask。
* task继承Serializable,要确保task是可序列化的。
* Step4: 提交tasks
* 先用tasks来初始化一个TaskSet对象,再调用TaskScheduler.submitTasks提交
*/
/** Called when stage's parents are available and we can now do its task. */
private def submitMissingTasks(stage: Stage, jobId: Int) {
logDebug("submitMissingTasks(" + stage + ")")
/**
* TODO
* 注释: Step1: 得到RDD中需要计算的partition
* 1、首先得到RDD中需要计算的partition,对于Shuffle类型的stage,需要判断stage中是否缓存了该结果;
* 2、对于Result类型的Final Stage,则判断计算Job中该partition是否已经计算完成
* 3、这么做的原因是,stage中某个task执行失败其他执行成功地时候就需要找出这个失败的task对应要计算的partition而不是要计算所有partition
*/
// First figure out the indexes of partition ids to compute.
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
// Use the scheduling pool, job group, description, etc. from an ActiveJob associated with this Stage
val properties = jobIdToActiveJob(jobId).properties
// SparkListenerStageSubmitted should be posted before testing whether tasks are
runningStages += stage
// // serializable. If tasks are not serializable, a SparkListenerStageCompleted event
// // will be posted, which should always come after a corresponding SparkListenerStageSubmitted event.
stage match {
case s: ShuffleMapStage => outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)
case s: ResultStage => outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)
}
val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
stage match {
case s: ShuffleMapStage => partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id)) }.toMap
case s: ResultStage => partitionsToCompute.map { id =>
val p = s.partitions(id)
(id, getPreferredLocs(stage.rdd, p))
}.toMap
}
} catch {
case NonFatal(e) => stage.makeNewStageAttempt(partitionsToCompute.size)
listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
runningStages -= stage
return
}
stage.makeNewStageAttempt(partitionsToCompute.size, taskIdToLocations.values.toSeq)
// If there are tasks to execute, record the submission time of the stage. Otherwise,
// post the even without the submission time, which indicates that this stage was skipped.
if (partitionsToCompute.nonEmpty) {
stage.latestInfo.submissionTime = Some(clock.getTimeMillis())
}
listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
/**
* TODO
* 注释:Step2: 序列化 task 的 binary
*/
// TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.
// Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast
// the serialized copy of the RDD and for each task we will deserialize it, which means each
// task gets a different copy of the RDD. This provides stronger isolation between tasks that
// might modify state of objects referenced in their closures. This is necessary in Hadoop
// where the JobConf/Configuration object is not thread-safe.
var taskBinary: Broadcast[Array[Byte]] = null
var partitions: Array[Partition] = null
try {
// For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).
// For ResultTask, serialize and broadcast (rdd, func).
var taskBinaryBytes: Array[Byte] = null // taskBinaryBytes and partitions are both effected by the checkpoint status. We need
// this synchronization in case another concurrent job is checkpointing this RDD, so we get a
// consistent view of both variables.
RDDCheckpointData.synchronized {
// TODO 注释:taskBinaryBytes的初始化 序列化成字节
taskBinaryBytes = stage match {
case stage: ShuffleMapStage => JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))
case stage: ResultStage => JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))
}
partitions = stage.rdd.partitions
}
// TODO 注释:进行广播
taskBinary = sc.broadcast(taskBinaryBytes)
} catch {
// In the case of a failure during serialization, abort the stage.
case e: NotSerializableException => abortStage(stage, "Task not serializable: " + e.toString, Some(e))
runningStages -= stage // Abort execution
return
case e: Throwable => abortStage(stage, s"Task serialization failed: $e\n${Utils.exceptionString(e)}", Some(e))
runningStages -= stage // Abort execution
return
}
总结:
1、用户编写 spark 应用程序
2、达成jar包
3、通过spark-submit 提交执行
4、sparkSessioin sparkContext 初始化
5、执行action算子
6、sparkContext.runJob()
7、dagScheduler.handleJobSubmitted()
8、dagScheduler.runJob()
createResultStage() stage切分
submitStage()
9、taskScheduler.submitTasks(new TaskSet())
10、schedulerBackEnd.reviveOffers();
11、Driver发送 LaunchTask 消息给 Executor
12、Executor 就会封装Task 为一个 TaskRunner 对象,提交给该 Executor 的线程池执
13、Executor 执行的Task 有可能是 ShuffleMapTask,也有可能是ResultTask
14、ShuffleMapTask 会后续的 Shuffle操作,具体有 Writer 完成
注意:TaskScheduler、DAGScheduler、SchedulerBackend。
初始化顺序:TaskScheduler、SchedulerBackend、DAGScheduler
提交任务执行顺序:DAGScheduler、TaskScheduler、SchedulerBackend
由上面代码可知:最终是通过taskScheduler.submitTasks(...)来提交任务。即调用backend.reviveOffers()方法来提交任务。使用的是CoarseGrainedSchedulerBackend类的receiveOffers方法(),自己给自己发送消息:
override def reviveOffers() {
/**
* TODO 注释: 给 DriverEndpoint 发送 ReviveOffers 消息
* 其实就是发送给自己:CoarseGrainedSchedulerBackend
*/
driverEndpoint.send(ReviveOffers)
}
然后通过launchTasks(taskDescs)来加载Task。调用以下方法:
// Launch tasks returned by a set of resource offers
private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
for (task <- tasks.flatten) {
// TODO 注释:序列化 转换为字节
val serializedTask = TaskDescription.encode(task)
// TODO 注释: 当前这个 Task 的序列化数据如果超过规定的 RpcMessage 的最大大小
if (serializedTask.limit() >= maxRpcMessageSize) {
Option(scheduler.taskIdToTaskSetManager.get(task.taskId)).foreach { taskSetMgr =>
try {
var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " + "spark.rpc.message.maxSize (%d bytes). Consider increasing " + "spark.rpc.message.maxSize or using broadcast variables for large values."
msg = msg.format(task.taskId, task.index, serializedTask.limit(), maxRpcMessageSize)
taskSetMgr.abort(msg)
} catch {
case e: Exception => logError("Exception in error callback", e)
}
}
} else {
val executorData = executorDataMap(task.executorId)
executorData.freeCores -= scheduler.CPUS_PER_TASK
logDebug(s"Launching task ${task.taskId} on executor id: ${task.executorId} hostname: " + s"${executorData.executorHost}.")
/**
* TODO 注释:以下两句话都对:
* 1、实际上:发送 LaunchTask 消息给:CoarseGrainedExecutorBackend
* 2、逻辑上:发送 LaunchTask 消息给: Executor
*/
executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
}
}
}
是Driver的SchedulerBackEnd给Executor的ExecutorBackEnd发送launch来执行任务。
可以在CoarseGrainedExecutorBackend看到以下方法来接收任务执行,最终是将任务提交给Executor执行:
/**
* TODO
* 注释: Executor 的 BackEnd 接受任务执行
* data = 就是序列化的 Task 对象
*/
case LaunchTask(data) => if (executor == null) {
exitExecutor(1, "Received LaunchTask command but executor was null")
} else {
// TODO_MA 注释:反序列化
val taskDesc = TaskDescription.decode(data.value)
logInfo("Got assigned task " + taskDesc.taskId)
/**
* TODO
* 注释: 提交 Task 给 Executor
*/
executor.launchTask(this, taskDesc)
}
后续是将任务提交给TaskRunner的run方法来执行任务。最终是在这里执行任务。在这里做的事有很多,包括执行前的准备,真正执行,以及收尾工作(包括stage完成通知下一个Stage)。这里主要看执行的方法:
// TODO: 运行 Task
val res = task.run(taskAttemptId = taskId, attemptNumber = taskDescription.attemptNumber, metricsSystem = env.metricsSystem)
threwException = false
res
2.2.1.ShuffleMapTask
在这个task.run(...)方法里面的runTask()来指定了使用哪种方法,主要区分究竟是ShuffleMapTask还是ResultTask。
在这里先看下ShuffleMapTask中实现的核心代码:
/**
* TODO 注释: 开始执行 逻辑操作和 shuffle
*
* Spark 的shuffle 其实有两种大版本:
* 1、spark-1.2.x 以前,默认实现是: HashShuffleManager
* 2、spark-1.2.x 以后:默认实现是: SortShuffleManager
* 3、到 spark-2.x 以后,就移除了 HashShuffleManager
* 4、如果spark 的shuffle策略,分的很详细,其实可以分为四种:
* HashShuffleManager 两种
* SortShuffleManager 两种
*/
var writer: ShuffleWriter[Any, Any] = null
try {
/**
* TODO 注释:获得shuffle管理器, 在 SparkEnv 初始化的时候,当前这个 shuffleManager 已经初始化了
* 默认实现是: manager = org.apache.spark.shuffle.sort.SortShuffleManager
*/
val manager = SparkEnv.get.shuffleManager
/**
* TODO 注释:获取一个shuffle写入器, 三种实现!
*/
writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
/**
* TODO
* 注释:这里可以看到 rdd 计算的核心方法就是 iterator 方法
* SortShuffleWriter 的 write 方法可以分为几个步骤:
* 1、将上游 rdd 计算出的数据(通过调用 rdd.iterator方法)写入内存缓冲区,
* 在写的过程中如果超过 内存阈值就会溢写磁盘文件,可能会写多个文件
* 最后将溢写的文件和内存中剩余的数据一起进行归并排序后写入到磁盘中形成一个大的数据文件
* 这个排序是先按分区排序,再按key排序
* 在最后归并排序后写的过程中,每写一个分区就会手动刷写一遍,并记录下这个分区数据在文件中的位移
* 所以实际上最后写完一个 task 的数据后,磁盘上会有两个文件:
* 数据文件和记录每个 reduce 端 partition 数据位移的索引文件
*
* rdd.iterator(partition, context) 返回的就是,当前RDD的一个分区执行计算之后得到的结果数据
*
* 每个partition的结果数据都会输出写入
*/
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
/**
* TODO
* 注释:主要是删除中间过程的溢写文件,向内存管理器释放申请的内存
*/
writer.stop(success = true).get
} catch {
case e: Exception => try {
if (writer != null) {
writer.stop(success = false)
}
} catch {
case e: Exception => log.debug("Could not stop writer", e)
}
throw e
}
1、在manager.getWriter(...)有指定使用哪一种shuffle策略,具体可看其实现类:
override def getWriter[K, V](handle: ShuffleHandle, mapId: Int, context: TaskContext): ShuffleWriter[K, V] = {
numMapsForShuffle.putIfAbsent(handle.shuffleId, handle.asInstanceOf[BaseShuffleHandle[_, _, _]].numMaps)
val env = SparkEnv.get
handle match {
// TODO 注释:在不需要map端聚合、partition数量小于16777216,Serializer支持relocation的情况下 普通机制
case unsafeShuffleHandle: SerializedShuffleHandle[K@unchecked, V@unchecked] => new UnsafeShuffleWriter(env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver], context.taskMemoryManager(), unsafeShuffleHandle, mapId, context,
env.conf)
// TODO 注释:在不需要map端聚合、partition数量小于200的情况返回BypassMergeSortShuffleHandle对象 bypass机制
case bypassMergeSortHandle: BypassMergeSortShuffleHandle[K@unchecked, V@unchecked] => new BypassMergeSortShuffleWriter(env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver], bypassMergeSortHandle, mapId, context, env.conf)
// TODO 注释:其他情况,通用情况
case other: BaseShuffleHandle[K@unchecked, V@unchecked, _] => new SortShuffleWriter(shuffleBlockResolver, other, mapId, context)
}
}
注意上面的ShuffleManager的说明:更多可以查看https://blog.csdn.net/Yuan_CSDF/article/details/119428392
Spark 的shuffle 其实有两种大版本:
1、spark-1.2.x 以前,默认实现是: HashShuffleManager
2、spark-1.2.x 以后:默认实现是: SortShuffleManager
3、到 spark-2.x 以后,就移除了 HashShuffleManager
4、如果spark 的shuffle策略,分的很详细,其实可以分为四种:
HashShuffleManager 两种
SortShuffleManager 两种
2、再来看writer.write(...)核心方法
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
2.1、rdd.iterator(...)
rdd.iterator(partition,context)返回的就是,当前RDD的一个分区执行计算之后得到的结果数据。在此方法中,有2种实现方法的选择:
getOrCompute(split, context) //有使用缓存
computeOrReadCheckpoint(split, context) //没有使用缓存
我们这里主要看下没有使用缓存的方法,其内部调用的为:
if (isCheckpointedAndMaterialized) {
/**
* TODO
* 注释:被checkPoint
*/
firstParent[T].iterator(split, context)
} else {
/**
* TODO
* 注释:计算
* 如果你们看 RDD 的源码,发现, RDD 有五大属性!
* compute 底层执行的就是 你的 作用在 RDD 之上的 函数 的 参数参数
* rdd1.flatMap(func3).map(func1).reduce(func2) action
* stage1 执行的是func3和func1 执行的是参数函数
*
*/
compute(split, context)
}
然后compute根据rdd的算子选择其实现类来执行多个逻辑操作,如下:
2.2、writer.write(...)
先看其实现类:SortShuffleWriter
在这里做了很多操作,主要是使用SortShuffleManager将task数据写入本地文件
/** Write a bunch of records to this task's output */
override def write(records: Iterator[Product2[K, V]]): Unit = {
/**
* TODO 注释: 获取一个排序器,根据是否需要 map 端聚合传递不同的参数
*/
sorter = if (dep.mapSideCombine) {
new ExternalSorter[K, V, C](context, dep.aggregator, Some(dep.partitioner), dep.keyOrdering, dep.serializer)
} else {
// In this case we pass neither an aggregator nor an ordering to the sorter, because we don't
// care whether the keys get sorted in each partition; that will be done on the reduce side
// if the operation being run is sortByKey.
new ExternalSorter[K, V, V](context, aggregator = None, Some(dep.partitioner), ordering = None, dep.serializer)
}
// TODO 注释:将数据插入排序器中,这个过程或溢写出多个磁盘文件
sorter.insertAll(records)
/**
* TODO 注释:根据shuffleid和分区id获取一个磁盘文件名,mapId就是shuffleMap端RDD的partitionId
* 将多个溢写的磁盘文件和内存中的排序数据进行归并排序,
* 并写到一个文件中,同时返回每个reduce端分区的数据在这个文件中的位移
*/
// Don't bother including the time to open the merged output file in the shuffle write time,
// because it just opens a single file, so is typically too fast to measure accurately (see SPARK-3570).
val output = shuffleBlockResolver.getDataFile(dep.shuffleId, mapId)
// TODO 注释:将索引写入一个索引文件,并将数据文件的文件名由临时文件名改成正式的文件名。
// TODO 注释:为输出文件名加一个uuid后缀
val tmp = Utils.tempFileWith(output)
/**
* TODO 注释:
*/
try {
val blockId = ShuffleBlockId(dep.shuffleId, mapId, IndexShuffleBlockResolver.NOOP_REDUCE_ID)
/**
* TODO 注释:这一步将溢写到的磁盘的文件和内存中的数据进行归并排序,
* 并溢写到一个文件中,这一步写的文件是临时文件名
*
* 文件合并
*/
val partitionLengths = sorter.writePartitionedFile(blockId, tmp)
// TODO 注释:这一步主要是写入索引文件,使用File.renameTo方法将临时索引和临时数据文件重命名为正常的文件名
shuffleBlockResolver.writeIndexFileAndCommit(dep.shuffleId, mapId, partitionLengths, tmp)
// TODO 注释:返回一个状态对象,包含 shuffle 服务 Id 和各个分区数据在文件中的位移
mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths)
} finally {
if (tmp.exists() && !tmp.delete()) {
logError(s"Error while deleting temp file ${tmp.getAbsolutePath}")
}
}
}
2.2.2.ResultTask
override def runTask(context: TaskContext): U = {
// Deserialize the RDD and the func using the broadcast variables.
val threadMXBean = ManagementFactory.getThreadMXBean
val deserializeStartTime = System.currentTimeMillis()
val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime
} else 0L
val ser = SparkEnv.get.closureSerializer.newInstance()
/**
* TODO 注释: 反序列化 RDD,Function
* 本身拿到的就是 ResultTask,的最后一定是输出结果:
* 1、collect 收集结果到 Driver
* 2、foreach 当前Task 直接打印输出
* 3、写出到外部系统 HDFS MySQL, saveAsTextFile()
*/
val (rdd, func) = ser
.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
_executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
_executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
} else 0L
/**
* TODO 注释:
* 1、rdd.iterator(partition, context) 执行逻辑计算
* 2、func(context, rdd.iterator(partition, context)) 写出结果
*/
func(context, rdd.iterator(partition, context))
}
总结:
Stage切分入口:
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
提交Stage入口:
submitStage(finalStage)