- 浏览: 73877 次
之前看了Spark Streaming和Spark SQL, 自己还花了一些时间去玩了些machine learning的算法, 像 线性回归, kmeans, 协同过滤等。
现在回过头来, 打算看一下spark core部分代码, 就先找了下saveAsTextFile这个方法作为入口, 看一下是怎么保存文档到hadoop中,并且怎么切分stage以及提交Task。 中间也会触碰到DAGScheduler, 也能明白为什么大家都说DAGScheduler是作业调度的核心了
看一下saveAsTextFile代码:
这里很简单, 就是定义了一些输出类, 给每个partition设置了一下, 然后通过执行saveAsHadoopFile 来创建hadoop的txt文件, 看一下saveAsHadoopFile:
直接调用了saveAsHadoopFile, 那么我们继续跟进去:
这个里面其实主要就是设置了hadoopconf的属性,然后设置到了FileOutputFormat里面, 再通过saveAsHadoopDataset继续执行saveAsTextFile:
这里主要做了几件事:
1.创建了一个writer: val writer = new SparkHadoopWriter(hadoopConf)
2. 创建了writeToFile这个function, 这个function会被作为一个Job提交: self.context.runJob(self, writeToFile)
writeToFile其实就是从Job中获取数据写到对应的partition里面去
主要还是要看runJob里面做了什么:
继续:
再继续, 这里有一个回调函数(index, res) => results(index) = res, 就是把计算的result存到results里面:
注意这里的参数func一直就是在最开始定义的 将数据写到partition里面的writeToFile。
看到这里有调用到dagScheduler, 在初始化SparkContext之前, dagScheduler已经被构造了: (回头会写一下SparkContext的初始化)
private[spark] def dagScheduler: DAGScheduler = _dagScheduler
_dagScheduler = new DAGScheduler(this)
我们看一下dagScheduler里面的runJob干了什么:
里面调用了submitJob, 所以我们说Job是通过DAGScheduler去提交的, 可以看到Job提交后会有waiter一直awaitResult(), 将结果打印到日志里面, Job结束的时候writeToFile也执行完成了, txt文件也存到hadoop里面了。
那么接下来看一下DAGScheduler怎么提交Job的, 进入submitJob:
里面先确定patition的数量是正常范围内, 然后创建JobId, 如果partions是0 代表最终没有task, 所以直接返回JobWaiter, 如果定partition大于0, 则创建JobWaiter用来返回去执行 awaitResult, 然后通过eventProcessLoop 把JobSubmitted的event加入进去, 那么 eventProcessLoop 是什么呢:
看到eventProcessLoop 其实是DAGSchedulerEventProcessLoop,(继承自EventLoop) 那么问题来了, 放进去的event是怎么被调用的呢, 那么我们要回到DAGScheduler的构造过程中, 看到创建DAGScheduler里面执行了eventProcessLoop.start()
这个start直接调用的是EventLoop的start方法:
在DAGSchedulerEventProcessLoop没有定义onStart方法, 所以其实有用的是eventThread.start()方法, 这个方法如下:
他就是一个thread, 然后start方法回去跑run里面的东西, 所以调用了onReceive(event)方法, 如下: (DAGSchedulerEventProcessLoop的onReceive)
继续调用doOnReceive(event) :
好啦, 看到了case JobSubmitted。。 这个就是我们event加进去后, 执行的时候就会走到这个case里面, 去执行
dagScheduler.handleJobSubmitted
那么在handleJobSubmitted里面做了什么呢:
先拿到
finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite)
在newResultStage里面的代码:
他会通过getParentStagesAndId拿到parents和stage ID然后根据这两个参数创建一个resultStage返回, 那么getParentStagesAndId里面是怎么做的呢:
stageID是从一个increment里面创建的, parentStages是从getParentStages方法里面拿的:
这里可以看到实际上spark是根据rdd的dependence, 如果是ShuffleDependency那么就分割出来, 如果不是那么放到waitingForVisit的列表中继续查找他的父rdd, 直到循环结束, 或者父rdd的dependence是ShuffleDependency为止, 然后getShuffleMapStage返回到parents里面再返回到前面调用的方法. 所以我们可以看到stage的划分其实是根据rdd的dependence是不是ShuffleDependency来分的。
接下来看一下getShuffleMapStage里面做了什么:
其实就是创建一个shuffleStage及返回。 顺便再通过getAncestorShuffleDependencies 把所有和当前stage相关联的ShuffleDependency全部加到shuffleToMapStage, 以备后用。 好了现在知道了之前创建的那个resultStage其实是根据一堆ShuffleDependency的stage创建出来的, 那么我们回到handleJobSubmitted方法里面, 在拿到了finalStage (一个resultStage)后会根据其创建一个ActiveJob:
再通过finalStage.setActiveJob(job) 和finalStage关联起来, 最后通过submitStage(finalStage)提交。
submitStage里面:
这里面其实做的就是先去查祖先stage是不是都active了, 如果不是active的话就放到missing里面, 先提交所有的inactive的stage,并且把当前stage放入waitingStages里面 把所有当前stage的祖先stage都submit后才有可能submit当前的stage。 所以stage都是有关联顺序的, 只有所有祖先stage都提交了, 才会去执行当前stage。 执行当前stage的时候其实是调用submitMissingTasks这个方法是根据stage提交task, 后面有机会说一下。 waitingStages 会通过submitWaitingStages方法去执行:
可以看到里面其实也是调用submitStage去对所有的waitingstage做处理, 最后以task提交。 当task提交后我们的writeToFile就会被执行, 数据就会写到指定的hadoop路径中, 整个过程大概就是这个样子, 哪里不对的麻烦指正一下
现在回过头来, 打算看一下spark core部分代码, 就先找了下saveAsTextFile这个方法作为入口, 看一下是怎么保存文档到hadoop中,并且怎么切分stage以及提交Task。 中间也会触碰到DAGScheduler, 也能明白为什么大家都说DAGScheduler是作业调度的核心了
看一下saveAsTextFile代码:
def saveAsTextFile(path: String): Unit = withScope { // https://issues.apache.org/jira/browse/SPARK-2075 // // NullWritable is a `Comparable` in Hadoop 1.+, so the compiler cannot find an implicit // Ordering for it and will use the default `null`. However, it's a `Comparable[NullWritable]` // in Hadoop 2.+, so the compiler will call the implicit `Ordering.ordered` method to create an // Ordering for `NullWritable`. That's why the compiler will generate different anonymous // classes for `saveAsTextFile` in Hadoop 1.+ and Hadoop 2.+. // // Therefore, here we provide an explicit Ordering `null` to make sure the compiler generate // same bytecodes for `saveAsTextFile`. val nullWritableClassTag = implicitly[ClassTag[NullWritable]] val textClassTag = implicitly[ClassTag[Text]] val r = this.mapPartitions { iter => val text = new Text() iter.map { x => text.set(x.toString) (NullWritable.get(), text) } } RDD.rddToPairRDDFunctions(r)(nullWritableClassTag, textClassTag, null) .saveAsHadoopFile[TextOutputFormat[NullWritable, Text]](path) }
这里很简单, 就是定义了一些输出类, 给每个partition设置了一下, 然后通过执行saveAsHadoopFile 来创建hadoop的txt文件, 看一下saveAsHadoopFile:
def saveAsHadoopFile[F <: OutputFormat[K, V]]( path: String)(implicit fm: ClassTag[F]): Unit = self.withScope { saveAsHadoopFile(path, keyClass, valueClass, fm.runtimeClass.asInstanceOf[Class[F]]) }
直接调用了saveAsHadoopFile, 那么我们继续跟进去:
def saveAsHadoopFile( path: String, keyClass: Class[_], valueClass: Class[_], outputFormatClass: Class[_ <: OutputFormat[_, _]], conf: JobConf = new JobConf(self.context.hadoopConfiguration), codec: Option[Class[_ <: CompressionCodec]] = None): Unit = self.withScope { // Rename this as hadoopConf internally to avoid shadowing (see SPARK-2038). val hadoopConf = conf hadoopConf.setOutputKeyClass(keyClass) hadoopConf.setOutputValueClass(valueClass) conf.setOutputFormat(outputFormatClass) for (c <- codec) { hadoopConf.setCompressMapOutput(true) hadoopConf.set("mapred.output.compress", "true") hadoopConf.setMapOutputCompressorClass(c) hadoopConf.set("mapred.output.compression.codec", c.getCanonicalName) hadoopConf.set("mapred.output.compression.type", CompressionType.BLOCK.toString) } // Use configured output committer if already set if (conf.getOutputCommitter == null) { hadoopConf.setOutputCommitter(classOf[FileOutputCommitter]) } // When speculation is on and output committer class name contains "Direct", we should warn // users that they may loss data if they are using a direct output committer. val speculationEnabled = self.conf.getBoolean("spark.speculation", false) val outputCommitterClass = hadoopConf.get("mapred.output.committer.class", "") if (speculationEnabled && outputCommitterClass.contains("Direct")) { val warningMessage = s"$outputCommitterClass may be an output committer that writes data directly to " + "the final location. Because speculation is enabled, this output committer may " + "cause data loss (see the case in SPARK-10063). If possible, please use a output " + "committer that does not have this behavior (e.g. FileOutputCommitter)." logWarning(warningMessage) } FileOutputFormat.setOutputPath(hadoopConf, SparkHadoopWriter.createPathFromString(path, hadoopConf)) saveAsHadoopDataset(hadoopConf) }
这个里面其实主要就是设置了hadoopconf的属性,然后设置到了FileOutputFormat里面, 再通过saveAsHadoopDataset继续执行saveAsTextFile:
def saveAsHadoopDataset(conf: JobConf): Unit = self.withScope { // Rename this as hadoopConf internally to avoid shadowing (see SPARK-2038). val hadoopConf = conf val outputFormatInstance = hadoopConf.getOutputFormat val keyClass = hadoopConf.getOutputKeyClass val valueClass = hadoopConf.getOutputValueClass if (outputFormatInstance == null) { throw new SparkException("Output format class not set") } if (keyClass == null) { throw new SparkException("Output key class not set") } if (valueClass == null) { throw new SparkException("Output value class not set") } SparkHadoopUtil.get.addCredentials(hadoopConf) logDebug("Saving as hadoop file of type (" + keyClass.getSimpleName + ", " + valueClass.getSimpleName + ")") if (isOutputSpecValidationEnabled) { // FileOutputFormat ignores the filesystem parameter val ignoredFs = FileSystem.get(hadoopConf) hadoopConf.getOutputFormat.checkOutputSpecs(ignoredFs, hadoopConf) } val writer = new SparkHadoopWriter(hadoopConf) writer.preSetup() val writeToFile = (context: TaskContext, iter: Iterator[(K, V)]) => { // Hadoop wants a 32-bit task attempt ID, so if ours is bigger than Int.MaxValue, roll it // around by taking a mod. We expect that no task will be attempted 2 billion times. val taskAttemptId = (context.taskAttemptId % Int.MaxValue).toInt val (outputMetrics, bytesWrittenCallback) = initHadoopOutputMetrics(context) writer.setup(context.stageId, context.partitionId, taskAttemptId) writer.open() var recordsWritten = 0L Utils.tryWithSafeFinallyAndFailureCallbacks { while (iter.hasNext) { val record = iter.next() writer.write(record._1.asInstanceOf[AnyRef], record._2.asInstanceOf[AnyRef]) // Update bytes written metric every few records maybeUpdateOutputMetrics(bytesWrittenCallback, outputMetrics, recordsWritten) recordsWritten += 1 } }(finallyBlock = writer.close()) writer.commit() bytesWrittenCallback.foreach { fn => outputMetrics.setBytesWritten(fn()) } outputMetrics.setRecordsWritten(recordsWritten) } self.context.runJob(self, writeToFile) writer.commitJob() }
这里主要做了几件事:
1.创建了一个writer: val writer = new SparkHadoopWriter(hadoopConf)
2. 创建了writeToFile这个function, 这个function会被作为一个Job提交: self.context.runJob(self, writeToFile)
writeToFile其实就是从Job中获取数据写到对应的partition里面去
主要还是要看runJob里面做了什么:
def runJob[T, U: ClassTag](rdd: RDD[T], func: (TaskContext, Iterator[T]) => U): Array[U] = { runJob(rdd, func, 0 until rdd.partitions.length) }
继续:
def runJob[T, U: ClassTag]( rdd: RDD[T], func: (TaskContext, Iterator[T]) => U, partitions: Seq[Int]): Array[U] = { val results = new Array[U](partitions.size) runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res) results }
再继续, 这里有一个回调函数(index, res) => results(index) = res, 就是把计算的result存到results里面:
def runJob[T, U: ClassTag]( rdd: RDD[T], func: (TaskContext, Iterator[T]) => U, partitions: Seq[Int], resultHandler: (Int, U) => Unit): Unit = { if (stopped.get()) { throw new IllegalStateException("SparkContext has been shutdown") } val callSite = getCallSite val cleanedFunc = clean(func) logInfo("Starting job: " + callSite.shortForm) if (conf.getBoolean("spark.logLineage", false)) { logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString) } dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get) progressBar.foreach(_.finishAll()) rdd.doCheckpoint() }
注意这里的参数func一直就是在最开始定义的 将数据写到partition里面的writeToFile。
看到这里有调用到dagScheduler, 在初始化SparkContext之前, dagScheduler已经被构造了: (回头会写一下SparkContext的初始化)
private[spark] def dagScheduler: DAGScheduler = _dagScheduler
_dagScheduler = new DAGScheduler(this)
我们看一下dagScheduler里面的runJob干了什么:
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 val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties) waiter.awaitResult() match { case JobSucceeded => logInfo("Job %d finished: %s, took %f s".format (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9)) case JobFailed(exception: Exception) => logInfo("Job %d failed: %s, took %f s".format (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9)) // SPARK-8644: Include user stack trace in exceptions coming from DAGScheduler. val callerStackTrace = Thread.currentThread().getStackTrace.tail exception.setStackTrace(exception.getStackTrace ++ callerStackTrace) throw exception } }
里面调用了submitJob, 所以我们说Job是通过DAGScheduler去提交的, 可以看到Job提交后会有waiter一直awaitResult(), 将结果打印到日志里面, Job结束的时候writeToFile也执行完成了, txt文件也存到hadoop里面了。
那么接下来看一下DAGScheduler怎么提交Job的, 进入submitJob:
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] = { // Check to make sure we are not launching a task on a partition that does not exist. val maxPartitions = rdd.partitions.length partitions.find(p => p >= maxPartitions || p < 0).foreach { p => throw new IllegalArgumentException( "Attempting to access a non-existent partition: " + p + ". " + "Total number of partitions: " + maxPartitions) } val jobId = nextJobId.getAndIncrement() if (partitions.size == 0) { // Return immediately if the job is running 0 tasks return new JobWaiter[U](this, jobId, 0, resultHandler) } assert(partitions.size > 0) val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _] val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler) eventProcessLoop.post(JobSubmitted( jobId, rdd, func2, partitions.toArray, callSite, waiter, SerializationUtils.clone(properties))) waiter }
里面先确定patition的数量是正常范围内, 然后创建JobId, 如果partions是0 代表最终没有task, 所以直接返回JobWaiter, 如果定partition大于0, 则创建JobWaiter用来返回去执行 awaitResult, 然后通过eventProcessLoop 把JobSubmitted的event加入进去, 那么 eventProcessLoop 是什么呢:
private[scheduler] val eventProcessLoop = new DAGSchedulerEventProcessLoop(this)
看到eventProcessLoop 其实是DAGSchedulerEventProcessLoop,(继承自EventLoop) 那么问题来了, 放进去的event是怎么被调用的呢, 那么我们要回到DAGScheduler的构造过程中, 看到创建DAGScheduler里面执行了eventProcessLoop.start()
这个start直接调用的是EventLoop的start方法:
def start(): Unit = { if (stopped.get) { throw new IllegalStateException(name + " has already been stopped") } // Call onStart before starting the event thread to make sure it happens before onReceive onStart() eventThread.start() }
在DAGSchedulerEventProcessLoop没有定义onStart方法, 所以其实有用的是eventThread.start()方法, 这个方法如下:
private val eventThread = new Thread(name) { setDaemon(true) override def run(): Unit = { try { while (!stopped.get) { val event = eventQueue.take() try { 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) } } }
他就是一个thread, 然后start方法回去跑run里面的东西, 所以调用了onReceive(event)方法, 如下: (DAGSchedulerEventProcessLoop的onReceive)
override def onReceive(event: DAGSchedulerEvent): Unit = { val timerContext = timer.time() try { doOnReceive(event) } finally { timerContext.stop() } }
继续调用doOnReceive(event) :
private def doOnReceive(event: DAGSchedulerEvent): Unit = event match { 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) => dagScheduler.handleStageCancellation(stageId) case JobCancelled(jobId) => dagScheduler.handleJobCancellation(jobId) case JobGroupCancelled(groupId) => dagScheduler.handleJobGroupCancelled(groupId) case AllJobsCancelled => dagScheduler.doCancelAllJobs() case ExecutorAdded(execId, host) => dagScheduler.handleExecutorAdded(execId, host) case ExecutorLost(execId) => dagScheduler.handleExecutorLost(execId, fetchFailed = false) case BeginEvent(task, taskInfo) => dagScheduler.handleBeginEvent(task, taskInfo) case GettingResultEvent(taskInfo) => dagScheduler.handleGetTaskResult(taskInfo) case completion @ CompletionEvent(task, reason, _, _, taskInfo, taskMetrics) => dagScheduler.handleTaskCompletion(completion) case TaskSetFailed(taskSet, reason, exception) => dagScheduler.handleTaskSetFailed(taskSet, reason, exception) case ResubmitFailedStages => dagScheduler.resubmitFailedStages() }
好啦, 看到了case JobSubmitted。。 这个就是我们event加进去后, 执行的时候就会走到这个case里面, 去执行
dagScheduler.handleJobSubmitted
那么在handleJobSubmitted里面做了什么呢:
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 { // New stage creation may throw an exception if, for example, jobs are run on a // HadoopRDD whose underlying HDFS files have been deleted. finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite) } catch { case e: Exception => logWarning("Creating new stage failed due to exception - job: " + jobId, e) listener.jobFailed(e) return } val job = new ActiveJob(jobId, finalStage, callSite, listener, properties) clearCacheLocs() logInfo("Got job %s (%s) with %d output partitions".format( job.jobId, callSite.shortForm, partitions.length)) logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")") logInfo("Parents of final stage: " + finalStage.parents) logInfo("Missing parents: " + getMissingParentStages(finalStage)) val jobSubmissionTime = clock.getTimeMillis() jobIdToActiveJob(jobId) = job activeJobs += job finalStage.setActiveJob(job) val stageIds = jobIdToStageIds(jobId).toArray val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo)) listenerBus.post( SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties)) submitStage(finalStage) submitWaitingStages() }
先拿到
finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite)
在newResultStage里面的代码:
private def newResultStage( rdd: RDD[_], func: (TaskContext, Iterator[_]) => _, partitions: Array[Int], jobId: Int, callSite: CallSite): ResultStage = { val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId) val stage = new ResultStage(id, rdd, func, partitions, parentStages, jobId, callSite) stageIdToStage(id) = stage updateJobIdStageIdMaps(jobId, stage) stage }
他会通过getParentStagesAndId拿到parents和stage ID然后根据这两个参数创建一个resultStage返回, 那么getParentStagesAndId里面是怎么做的呢:
private def getParentStagesAndId(rdd: RDD[_], firstJobId: Int): (List[Stage], Int) = { val parentStages = getParentStages(rdd, firstJobId) val id = nextStageId.getAndIncrement() (parentStages, id) }
stageID是从一个increment里面创建的, parentStages是从getParentStages方法里面拿的:
private def getParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = { val parents = new HashSet[Stage] val visited = new HashSet[RDD[_]] // We are manually maintaining a stack here to prevent StackOverflowError // caused by recursively visiting val waitingForVisit = new Stack[RDD[_]] def visit(r: RDD[_]) { if (!visited(r)) { visited += r // Kind of ugly: need to register RDDs with the cache here since // we can't do it in its constructor because # of partitions is unknown for (dep <- r.dependencies) { dep match { case shufDep: ShuffleDependency[_, _, _] => parents += getShuffleMapStage(shufDep, firstJobId) case _ => waitingForVisit.push(dep.rdd) } } } } waitingForVisit.push(rdd) while (waitingForVisit.nonEmpty) { visit(waitingForVisit.pop()) } parents.toList }
这里可以看到实际上spark是根据rdd的dependence, 如果是ShuffleDependency那么就分割出来, 如果不是那么放到waitingForVisit的列表中继续查找他的父rdd, 直到循环结束, 或者父rdd的dependence是ShuffleDependency为止, 然后getShuffleMapStage返回到parents里面再返回到前面调用的方法. 所以我们可以看到stage的划分其实是根据rdd的dependence是不是ShuffleDependency来分的。
接下来看一下getShuffleMapStage里面做了什么:
private def getShuffleMapStage( shuffleDep: ShuffleDependency[_, _, _], firstJobId: Int): ShuffleMapStage = { shuffleToMapStage.get(shuffleDep.shuffleId) match { case Some(stage) => stage case None => // We are going to register ancestor shuffle dependencies getAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep => shuffleToMapStage(dep.shuffleId) = newOrUsedShuffleStage(dep, firstJobId) } // Then register current shuffleDep val stage = newOrUsedShuffleStage(shuffleDep, firstJobId) shuffleToMapStage(shuffleDep.shuffleId) = stage stage } }
其实就是创建一个shuffleStage及返回。 顺便再通过getAncestorShuffleDependencies 把所有和当前stage相关联的ShuffleDependency全部加到shuffleToMapStage, 以备后用。 好了现在知道了之前创建的那个resultStage其实是根据一堆ShuffleDependency的stage创建出来的, 那么我们回到handleJobSubmitted方法里面, 在拿到了finalStage (一个resultStage)后会根据其创建一个ActiveJob:
val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
再通过finalStage.setActiveJob(job) 和finalStage关联起来, 最后通过submitStage(finalStage)提交。
submitStage里面:
private def submitStage(stage: Stage) { val jobId = activeJobForStage(stage) if (jobId.isDefined) { logDebug("submitStage(" + stage + ")") if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) { val missing = getMissingParentStages(stage).sortBy(_.id) logDebug("missing: " + missing) if (missing.isEmpty) { logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents") submitMissingTasks(stage, jobId.get) } else { for (parent <- missing) { submitStage(parent) } waitingStages += stage } } } else { abortStage(stage, "No active job for stage " + stage.id, None) } }
这里面其实做的就是先去查祖先stage是不是都active了, 如果不是active的话就放到missing里面, 先提交所有的inactive的stage,并且把当前stage放入waitingStages里面 把所有当前stage的祖先stage都submit后才有可能submit当前的stage。 所以stage都是有关联顺序的, 只有所有祖先stage都提交了, 才会去执行当前stage。 执行当前stage的时候其实是调用submitMissingTasks这个方法是根据stage提交task, 后面有机会说一下。 waitingStages 会通过submitWaitingStages方法去执行:
private def submitWaitingStages() { // TODO: We might want to run this less often, when we are sure that something has become // runnable that wasn't before. logTrace("Checking for newly runnable parent stages") logTrace("running: " + runningStages) logTrace("waiting: " + waitingStages) logTrace("failed: " + failedStages) val waitingStagesCopy = waitingStages.toArray waitingStages.clear() for (stage <- waitingStagesCopy.sortBy(_.firstJobId)) { submitStage(stage) } }
可以看到里面其实也是调用submitStage去对所有的waitingstage做处理, 最后以task提交。 当task提交后我们的writeToFile就会被执行, 数据就会写到指定的hadoop路径中, 整个过程大概就是这个样子, 哪里不对的麻烦指正一下
发表评论
-
kafka + flume + hdfs + zookeeper + spark 测试环境搭建
2017-07-20 11:28 1105最近由于项目需要, 搭建了一个类似线上环境的处理流数据的环境 ... -
源码跟踪executor如何写数据到blockmanager, 以及如何从blockmanager读数据
2016-08-10 19:41 1414之前看了Job怎么submit 以 ... -
Spark中Blockmanager相关代码解析
2016-08-04 19:47 1843前一段时间看了如何划分stage以及如何提交Job, 最后把结 ... -
Spark在submitStage后如何通过clustermanager调度执行task到Driver接收计算结果的代码解析
2016-08-01 14:08 1472前文: http://humingminghz.iteye.c ... -
SparkSQL DF.agg 执行过程解析
2016-07-19 10:21 4120在上一篇文章前, 我一直没看懂为什么下面的代码就能得到max或 ... -
SparkSQL SQL语句解析过程源代码浅析
2016-07-15 19:34 6641前两天一直在忙本职工 ... -
SparkSQL SQL语句解析过程浅析
2016-07-15 19:06 0前两天一直在忙本职工 ... -
SparkStreaming从启动Receiver到收取数据生成RDD的代码浅析
2016-07-08 17:54 2233前面一片文章介绍了SocketTextStream 是如何从b ... -
Sparkstreaming是如何获取数据组成Dstream的源码浅析
2016-07-08 11:23 1472前面一篇文章介绍了SparkStreaming是如何不停的循环 ... -
SparkSQL 使用SQLContext读取csv文件 分析数据 (含部分数据)
2016-07-06 11:24 10149前两天开始研究SparkSQL, 其主要分为HiveConte ... -
SparkStreaming是如何完成不停的循环处理的代码浅析
2016-07-02 12:26 4654一直很好奇Sparkstreaming的ssc.start是怎 ... -
SparkStreaming 对Window的reduce的方法解析
2016-06-30 11:57 4727在sparkstreaming中对窗口 ... -
Sparkstreaming reduceByKeyAndWindow(_+_, _-_, Duration, Duration) 的源码/原理解析
2016-06-29 19:50 8790最近在玩spark streaming, 感觉到了他的强大。 ... -
关于Eclipse开发环境下 Spark+Kafka 获取topic的时候连接出错
2016-06-28 17:20 7403林林总总玩了Spark快一个月了, 打算试一下kafka的消息 ...
相关推荐
pandas whl安装包,对应各个python版本和系统(具体看资源名字),找准自己对应的下载即可! 下载后解压出来是已.whl为后缀的安装包,进入终端,直接pip install pandas-xxx.whl即可,非常方便。 再也不用担心pip联网下载网络超时,各种安装不成功的问题。
基于java的大学生兼职信息系统答辩PPT.pptx
基于java的乐校园二手书交易管理系统答辩PPT.pptx
tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl
Android Studio Ladybug 2024.2.1(android-studio-2024.2.1.10-mac.dmg)适用于macOS Intel系统,文件使用360压缩软件分割成两个压缩包,必须一起下载使用: part1: https://download.csdn.net/download/weixin_43800734/89954174 part2: https://download.csdn.net/download/weixin_43800734/89954175
有学生和教师两种角色 登录和注册模块 考场信息模块 考试信息模块 点我收藏 功能 监考安排模块 考场类型模块 系统公告模块 个人中心模块: 1、修改个人信息,可以上传图片 2、我的收藏列表 账号管理模块 服务模块 eclipse或者idea 均可以运行 jdk1.8 apache-maven-3.6 mysql5.7及以上 tomcat 8.0及以上版本
tornado-6.1b2-cp38-cp38-macosx_10_9_x86_64.whl
Android Studio Ladybug 2024.2.1(android-studio-2024.2.1.10-mac.dmg)适用于macOS Intel系统,文件使用360压缩软件分割成两个压缩包,必须一起下载使用: part1: https://download.csdn.net/download/weixin_43800734/89954174 part2: https://download.csdn.net/download/weixin_43800734/89954175
matlab
基于java的毕业生就业信息管理系统答辩PPT.pptx
随着高等教育的普及和毕业设计的日益重要,为了方便教师、学生和管理员进行毕业设计的选题和管理,我们开发了这款基于Web的毕业设计选题系统。 该系统主要包括教师管理、院系管理、学生管理等多个模块。在教师管理模块中,管理员可以新增、删除教师信息,并查看教师的详细资料,方便进行教师资源的分配和管理。院系管理模块则允许管理员对各个院系的信息进行管理和维护,确保信息的准确性和完整性。 学生管理模块是系统的核心之一,它提供了学生选题、任务书管理、开题报告管理、开题成绩管理等功能。学生可以在此模块中进行毕业设计的选题,并上传任务书和开题报告,管理员和教师则可以对学生的报告进行审阅和评分。 此外,系统还具备课题分类管理和课题信息管理功能,方便对毕业设计课题进行分类和归档,提高管理效率。在线留言功能则为学生、教师和管理员提供了一个交流互动的平台,可以就毕业设计相关问题进行讨论和解答。 整个系统设计简洁明了,操作便捷,大大提高了毕业设计的选题和管理效率,为高等教育的发展做出了积极贡献。
这个数据集来自世界卫生组织(WHO),包含了2000年至2015年期间193个国家的预期寿命和相关健康因素的数据。它提供了一个全面的视角,用于分析影响全球人口预期寿命的多种因素。数据集涵盖了从婴儿死亡率、GDP、BMI到免疫接种覆盖率等多个维度,为研究者提供了丰富的信息来探索和预测预期寿命。 该数据集的特点在于其跨国家的比较性,使得研究者能够识别出不同国家之间预期寿命的差异,并分析这些差异背后的原因。数据集包含22个特征列和2938行数据,涉及的变量被分为几个大类:免疫相关因素、死亡因素、经济因素和社会因素。这些数据不仅有助于了解全球健康趋势,还可以辅助制定公共卫生政策和社会福利计划。 数据集的处理包括对缺失值的处理、数据类型转换以及去重等步骤,以确保数据的准确性和可靠性。研究者可以使用这个数据集来探索如教育、健康习惯、生活方式等因素如何影响人们的寿命,以及不同国家的经济发展水平如何与预期寿命相关联。此外,数据集还可以用于预测模型的构建,通过回归分析等统计方法来预测预期寿命。 总的来说,这个数据集是研究全球健康和预期寿命变化的宝贵资源,它不仅提供了历史数据,还为未来的研究和政策制
基于微信小程序的高校毕业论文管理系统小程序答辩PPT.pptx
基于java的超市 Pos 收银管理系统答辩PPT.pptx
基于java的网上报名系统答辩PPT.pptx
基于java的网上书城答辩PPT.pptx
婚恋网站 SSM毕业设计 附带论文 启动教程:https://www.bilibili.com/video/BV1GK1iYyE2B
基于java的戒烟网站答辩PPT.pptx
基于微信小程序的“健康早知道”微信小程序答辩PPT.pptx
Capital Bikeshare 数据集是一个包含从2020年5月到2024年8月的自行车共享使用情况的数据集。这个数据集记录了华盛顿特区Capital Bikeshare项目中自行车的租赁模式,包括了骑行的持续时间、开始和结束日期时间、起始和结束站点、使用的自行车编号、用户类型(注册会员或临时用户)等信息。这些数据可以帮助分析和预测自行车共享系统的需求模式,以及了解用户行为和偏好。 数据集的特点包括: 时间范围:覆盖了四年多的时间,提供了长期的数据观察。 细节丰富:包含了每次骑行的详细信息,如日期、时间、天气条件、季节等,有助于深入分析。 用户分类:数据中区分了注册用户和临时用户,可以分析不同用户群体的使用习惯。 天气和季节因素:包含了天气情况和季节信息,可以研究这些因素对骑行需求的影响。 通过分析这个数据集,可以得出关于自行车共享使用模式的多种见解,比如一天中不同时间段的使用高峰、不同天气条件下的使用差异、季节性变化对骑行需求的影响等。这些信息对于城市规划者、交通管理者以及自行车共享服务提供商来说都是非常宝贵的,可以帮助他们优化服务、提高效率和满足用户需求。同时,这个数据集也