第一次接触Spark源码时,我被DAGScheduler这个组件的精妙设计深深吸引。作为Spark作业调度的"大脑",它负责将复杂的计算逻辑拆解成可并行执行的任务单元。想象一下,你正在组装一辆自行车,DAGScheduler就像那个把整张装配图分解成车轮组、车架组、传动组等独立模块的工程师,让不同工人可以同时开工。
在实际项目中,我遇到过这样一个案例:某电商平台的用户行为分析作业突然从20分钟延长到2小时。通过Spark UI查看执行计划时,发现某个Stage处理的数据量是其他Stage的10倍。这正是DAGScheduler划分Stage的典型场景——当遇到Shuffle操作时(类似SQL中的group by),就会产生新的Stage边界。
DAGScheduler主要完成三个关键工作:
它的工作流程就像快递分拣中心:先按省份(Stage)分拣包裹,再按城市(Task)分配配送路线,最后考虑最优的配送车辆(Executor)调度。
在Spark中,窄依赖(Narrow Dependency)就像父子间的DNA传递——每个父RDD的分区最多被一个子RDD分区引用。而宽依赖(Wide Dependency)则像广播通知——每个父RDD的分区可能被多个子RDD分区引用。这种差异直接决定了Stage的划分边界。
我曾调试过一个数据倾斜案例,发现某个join操作导致产生了200个分区数据的Shuffle。通过分析RDD.toDebugString输出,确认这是由宽依赖引起的Stage分割点。实际优化时,我们通过调整partition数量解决了这个问题。
当Action触发job提交时,DAGScheduler会从finalRDD开始反向解析。关键方法是createResultStage,它会:
scala复制// 简化后的核心代码逻辑
def createResultStage(finalRDD: RDD[_], ...): ResultStage = {
val (shuffleDeps, _) = getShuffleDependenciesAndResourceProfiles(finalRDD)
val parents = getOrCreateParentStages(shuffleDeps, jobId)
new ResultStage(id, finalRDD, ..., parents)
}
在电商案例中,我们发现getShuffleDependenciesAndResourceProfiles方法会标记所有需要Shuffle的RDD依赖,这些节点就是Stage的天然分界点。
DAGScheduler通过多级策略确定任务最佳执行位置:
scala复制private def getPreferredLocsInternal(rdd: RDD[_], partition: Int): Seq[TaskLocation] = {
// 第一优先级:检查内存缓存位置
if (getCacheLocs(rdd)(partition).nonEmpty) return cached
// 第二优先级:原始数据位置
val rddPrefs = rdd.preferredLocations(rdd.partitions(partition))
if (rddPrefs.nonEmpty) return rddPrefs.map(TaskLocation(_))
// 第三优先级:窄依赖传递
rdd.dependencies.foreach {
case n: NarrowDependency[_] =>
n.getParents(partition).foreach { inPart =>
val locs = getPreferredLocsInternal(n.rdd, inPart, visited)
if (locs != Nil) return locs
}
}
}
当某个Stage失败时,DAGScheduler会根据依赖类型采取不同策略:
在日志分析系统中,我们曾遇到因节点宕机导致的Stage重试。得益于DAGScheduler的stageIdToStage映射,它能快速定位需要重新计算的Stage范围,而不用从头开始整个作业。
scala复制val smallTable = spark.table("small").collect()
val bc = sc.broadcast(smallTable)
largeTable.map { x =>
bc.value.find(_._1 == x._1)
}
scala复制df.repartition(100, $"userId") // 按用户ID均匀分布
使用map-side组合器:reduceByKey替代groupByKey
分区数调优公式:
code复制最佳分区数 = max(集群总核数 × 2, HDFS块数 × 1.5)
通过Spark UI观察这些关键指标:
某次优化中,我们发现某个Task的运行时间是平均值的50倍。通过分析发现是该分区包含异常多的热点数据,最终通过加盐处理解决了问题。
在log4j.properties中添加:
code复制log4j.logger.org.apache.spark.scheduler.DAGScheduler=DEBUG
典型日志分析:
code复制INFO DAGScheduler: Submitting 10 missing tasks from Stage 1
DEBUG DAGScheduler: Preferred locations for task 3: List(hdfs-node1:50010)
scala复制rdd.toDebugString.split("\n").foreach(println)
scala复制class MyListener extends SparkListener {
override def onStageSubmitted(stage: SparkListenerStageSubmitted) {
println(s"Stage ${stage.stageInfo.stageId} submitted")
}
}
spark.sparkContext.addSparkListener(new MyListener)
在最近一次性能排查中,我们结合日志和可视化工具,发现某个filter操作意外导致了全表Shuffle。通过提前repartition,作业时间从40分钟缩短到8分钟。