面筋
Spark任务提交、调度、执行过程
Spark的架构有三种方式:local模式、standalone模式、cluster模式(yarn、mesos、k8s等),因此对执行过程也可以拆分为3种。
Standalone
是Spark实现的资源调度框架,主要的节点有Client节点、Master节点和Worker节点。Driver既可以运行在Master节点上,也可以运行在本地Client端。
当以standalone模式向spark集群提交作业时,作业的运行流程如下(图片来源:公众号旧时光大数据):
- 我们提交一个任务,任务就叫Application
- 初始化程序的入口SparkContext,
- 初始化DAG Scheduler
- 初始化Task Scheduler
- SparkContext中的Task Scheduler连接到Master,向Master注册并申请资源(CPU Core和Memory)
- Worker定期发送心跳信息给Master并报告Executor状态
- Master根据SparkContext的资源申请要求和Worker心跳周期内报告的信息决定在哪个worker上分配资源,然后在该worker上获取资源,启动StandaloneExecutorBackend
- StandaloneExecutorBackend向SparkContext注册
- SparkContext将Applicaiton代码发送给StandaloneExecutorBackend;并且SparkContext解析Applicaiton代码,构建DAG图,并提交给DAG Scheduler分解成Stage(当碰到Action操作 时,就会催生Job;每个Job中含有1个或多个Stage,Stage一般在获取外部数据和shuffle之前产生)
- 将Stage(或者称为TaskSet)提交给Task Scheduler。Task Scheduler负责将Task分配到相应的Worker,最后提交给StandaloneExecutorBackend执行
- 对task进行序列化,并根据task的分配算法,分配task
- 对接收过来的task进行反序列化,把task封装成一个线程
- 开始执行Task,并向SparkContext报告,直至Task完成。
- 资源注销
yarn-client
图片来源:公众号旧时光大数据:
- Spark Yarn Client向YARN的ResourceManager申请启动Application Master。同时在SparkContent初始化中将创建DAGScheduler和TASKScheduler等,由于我们选择的是Yarn-Client模式,程序会选择YarnClientClusterScheduler和YarnClientSchedulerBackend
- ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,与YARN-Cluster区别的是在该ApplicationMaster不运行SparkContext,只与SparkContext进行联系进行资源的分派;
- Client中的SparkContext初始化完毕后,与ApplicationMaster建立通讯,向ResourceManager注册,根据任务信息向ResourceManager申请资源(Container);
- 一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向Client中的SparkContext注册并申请Task;
- Client中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向Driver汇报运行的状态和进度,以让Client随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;
- 应用程序运行完成后,Client的SparkContext向ResourceManager申请注销并关闭自己。
yarn-cluster
图片来源:公众号旧时光大数据:
- Spark Yarn Client向YARN中提交应用程序,包括ApplicationMaster程序、启动ApplicationMaster的命令、需要在Executor中运行的程序等;
- ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,其中ApplicationMaster进行SparkContext等的初始化;
- ApplicationMaster向ResourceManager注册,这样用户可以直接通过ResourceManage查看应用程序的运行状态,然后它将采用轮询的方式通过RPC协议为各个任务申请资源,并监控它们的运行状态直到运行结束;
- 一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向ApplicationMaster中的SparkContext注册并申请Task。这一点和Standalone模式一样,只不过SparkContext在Spark Application中初始化时,使用CoarseGrainedSchedulerBackend配合YarnClusterScheduler进行任务的调度,其中YarnClusterScheduler只是对TaskSchedulerImpl的一个简单包装,增加了对Executor的等待逻辑等;
- ApplicationMaster中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向ApplicationMaster汇报运行的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;
- 应用程序运行完成后,ApplicationMaster向ResourceManager申请注销并关闭自己。
yarn-client和yarn-cluster的区别
理解YARN-Client和YARN-Cluster深层次的区别之前先清楚一个概念:Application Master。在YARN中,每个Application实例都有一个ApplicationMaster进程,它是Application启动的第一个容器。它负责和ResourceManager打交道并请求资源,获取资源之后告诉NodeManager为其启动Container。从深层次的含义讲YARN-Cluster和YARN-Client模式的区别其实就是ApplicationMaster进程的区别。
- YARN-Cluster模式下,Driver运行在AM(Application Master)中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行,因而YARN-Cluster模式不适合运行交互类型的作业;
- YARN-Client模式下,Application Master仅仅向YARN请求Executor,Client会和请求的Container通信来调度他们工作,也就是说Client不能离开。
Spark任务提交
当使用spark-submit
命令提交任务的时候,脚本内部调用的是SparkSubmit类。定位到其main函数中,其中执行了**submit.doSubmit(args)**的函数。
def doSubmit(args: Array[String]): Unit = {
// Initialize logging if it hasn't been done yet. Keep track of whether logging needs to
// be reset before the application starts.
// 如果尚未完成日志记录,则初始化日志记录。跟踪在应用程序启动之前是否需要重置日志记录
val uninitLog = initializeLogIfNecessary(true, silent = true)
val appArgs = parseArguments(args)
if (appArgs.verbose) {
logInfo(appArgs.toString)
}
// 根据4种行为处理
appArgs.action match {
// 代理入口点为这个方法,提交应用程序的处理方法
case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog) // 提交,调用submit方法
case SparkSubmitAction.KILL => kill(appArgs) // 杀死,调用kill方法
case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs) // 请求状态,调用requestStast方法
case SparkSubmitAction.PRINT_VERSION => printVersion()
}
}
经过一系列的处理,最后执行app.start方法,这时候就是执行不同模式的分水岭。
Standalone cluster模式
程序会走到这个位置
override def start(args: Array[String], conf: SparkConf): Unit = {
// 实例化出一个SparkConfig对象,通过这个配置对象,可以在代码中指定一些配置项,
// 如appName、Master地址等。val driverArgs = new ClientArguments(args)
// 使用传入的args参数构建一个ClientArguments对象,该对象同样保留传入的配置信息,
// 如Executor memory、Executor cores等都包含在这个
val driverArgs = new ClientArguments(args)
// 设置RPC请求超时时间为10sif (!conf.contains("spark.rpc.
if (!conf.contains(RPC_ASK_TIMEOUT)) {
conf.set(RPC_ASK_TIMEOUT, "10s")
}
Logger.getRootLogger.setLevel(driverArgs.logLevel)
// 使用RpcEnv的create创建RPC环境
// 使用该成员设置好到Master的通信端点,通过该端点实现同Master的通信
val rpcEnv =
RpcEnv.create("driverClient", Utils.localHostName(), 0, conf, new SecurityManager(conf))
// 得到master的URL并得到Master的Endpoints,用于同Master通信
val masterEndpoints = driverArgs.masters.map(RpcAddress.fromSparkURL).
map(rpcEnv.setupEndpointRef(_, Master.ENDPOINT_NAME))
// 在rpcEnv.setupEndpoint方法中调用new()函数创建一个Driver ClientEndpoint。
// ClientEndpoint是一个ThreadSafeRpcEndpoint消息循环体,
// 至此就生成了Driver ClientEndpoint。
// 在ClientEndpoint的onStart方法中向Master提交注册。
// 这里通过masterEndpoint向Master发送RequestSubmitDriver(driverDescription)请求,
// 完成Driver的注册
// Master收到Driver ClientEndpoint的RequestSubmitDriver消息以后,就将Driver的信息加入到waitingDrivers和drivers的数据结构中。
// 然后进行schedule()资源分配,Master向Worker发送LaunchDriver的消息指令
rpcEnv.setupEndpoint("client", new ClientEndpoint(rpcEnv, driverArgs, masterEndpoints, conf))
// 等待rpcEnv的终止
rpcEnv.awaitTermination()
}
在这里,主要的流程是
- 注册ClientEndpoint
- 封装DriverDescription,比如
- 调用onStart()方法向Master提交Driver注册向Master发送注册请求RequestSubmitDriver(driverDescription))
Master接受RequestSubmitDriver请求,然后执行如下代码
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
case RequestSubmitDriver(description) =>
// 若 state 不为 ALIVE,直接向 Client 返回 SubmitDriverResponse(selffalse
// None_msg)消息
if (state != RecoveryState.ALIVE) {
val msg = s"${Utils.BACKUP_STANDALONE_MASTER_PREFIX}: $state. " +
"Can only accept driver submissions in ALIVE state."
context.reply(SubmitDriverResponse(self, false, None, msg))
} else {
logInfo("Driver submitted " + description.command.mainClass)
// 使用description创建driver,该方法返回DriverDescription
val driver = createDriver(description)
// watingDrviers等待在调度数组中加入该driver
persistenceEngine.addDriver(driver)
waitingDrivers += driver
// 用schedule方法调度资源
drivers.add(driver)
schedule()
// TODO: It might be good to instead have the submission client poll the master to determine
// the current status of the driver. For now it's simply "fire and forget".
context.reply(SubmitDriverResponse(self, true, Some(driver.id),
s"Driver successfully submitted as ${driver.id}"))
}
...
}
Master收到Driver ClientEndpoint的RequestSubmitDriver消息以后,就将Driver的信息加入到waitingDrivers和drivers的数据结构中。然后进行schedule()资源分配,Master向Worker发送LaunchDriver的消息指令。
在Master.launchDriver
代码如下,表示发送命令给worker节点,让远程的worker启动driver。
private def launchDriver(worker: WorkerInfo, driver: DriverInfo): Unit = {
logInfo("Launching driver " + driver.id + " on worker " + worker.id)
worker.addDriver(driver)
driver.worker = Some(worker)
// 发指令给worker,让远程的worker启动driver,driver启动之后,Driver的状态就变成DriverState.RUNNING
worker.endpoint.send(LaunchDriver(driver.id, driver.desc, driver.resources))
driver.state = DriverState.RUNNING
}
在worker接收到启动driver的命令后,内部使用Thread来处理Driver和Executor的启动。代码在Worker.receive
部分
case LaunchDriver(driverId, driverDesc, resources_) =>
// Worker进程:Worker的DriverRunner调用start方法,内部使用Thread来处理Driver启动。
// DriverRunner创建Driver在本地系统的工作目录(即Linux的文件目录),每次工作都有自己的目录,
// 封装好Driver的启动Command,通过ProcessBuilder启动Driver。这些内容都属于Worker进程。
logInfo(s"Asked to launch driver $driverId")
val driver = new DriverRunner(
conf,
driverId,
workDir,
sparkHome,
driverDesc.copy(command = Worker.maybeUpdateSSLSettings(driverDesc.command, conf)),
self,
workerUri,
workerWebUiUrl,
securityMgr,
resources_)
drivers(driverId) = driver
// 启动Driver
driver.start()
// start之后,将消耗的cores、memory增加到coresUsed、memoryUsed
coresUsed += driverDesc.cores
memoryUsed += driverDesc.mem
addResourcesUsed(resources_)
点击计入DriverRunner.start
的代码中,可以看到有一行代码写的是如下代码,表示准备Driver的jar包,并且运行Driver
val exitCode = prepareAndRunDriver()
这里运行的是DriverRunner.runCommandWithRetry
方法,在这里执行之前spark.submit
上传的jar包方法
在Driver执行jar包的过程中,发送Driver的启动DriverStateChanged
信息至Master。Master接受到信息之后,移除Master内存、持久化引擎中的drivers信息,并把当前driver加入到Master的已完成的drivers队列,并减去该driver关联的worker信息中的Driver内存和cores资源,最后重新调度schedule()
方法。
在schedule()
方法中有一个调用startExecutorsOnWorkers()
为当前的程序调度和启动Worker的Executor的方法,默认情况下排队的方式是FIFO
private def startExecutorsOnWorkers(): Unit = {
// Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
// in the queue, then the second app, etc.
// 这是一个非常简单的 FIFO 调度。我们尝试在队列中推入第一个应用程序,然后推入第2个应用程序等
for (app <- waitingApps) {
// 筛选出workers,其没有足够资源来启动Executor
val coresPerExecutor = app.desc.coresPerExecutor.getOrElse(1)
// If the cores left is less than the coresPerExecutor,the cores left will not be allocated
if (app.coresLeft >= coresPerExecutor) {
// Filter out workers that don't have enough resources to launch an executor
// 如果剩余的核心小于coresPerExecutor,则不会分配剩余的核心
val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
.filter(canLaunchExecutor(_, app.desc))
// 在此基础上进行排序,产生计算资源由大到小的coresFree数据结构
.sortBy(_.coresFree).reverse
val appMayHang = waitingApps.length == 1 &&
waitingApps.head.executors.isEmpty && usableWorkers.isEmpty
if (appMayHang) {
logWarning(s"App ${app.id} requires more resource than any of Workers could have.")
}
// spreadOutApps默认让应用程序尽可能地多运行在所有的Node上
val assignedCores = scheduleExecutorsOnWorkers(app, usableWorkers, spreadOutApps)
// 现在我们决定每个worker分配多少个cores,进行分配
// Now that we've decided how many cores to allocate on each worker, let's allocate them
for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
allocateWorkerResourceToExecutors(
app, assignedCores(pos), app.desc.coresPerExecutor, usableWorkers(pos))
}
}
}
在其中遍历wattingApps需要分配的apps,然后查找可用的worker列表内存和cores是否满足并根据剩余cores排序;根据分配机制尽可能地将应用程序多运行在所有的Node上。
根据分配结果,执行allocateWorkerResourceToExecutors
中launchExecutor(worker, exec)
的方法,通过向worker发送LaunchExecutor
启动executor,并把executor添加的ExecutorAdded
信息向Driver发送。
worker接收到launchExecutor
信息之后,启动一个线程Thread,在run方法中调用fetchAndRunExecutor
方法
/**
* Download and run the executor described in our ApplicationDescription
* fetchAndRunExecutor类似于启动Driver的过程,在启动Executor时首先构建CommandUtils.buildProcessBuilder,
* 然后是builder.start(),退出时发送ExecutorStateChanged消息给Worker
*/
private def fetchAndRunExecutor(): Unit = {
try {
val resourceFileOpt = prepareResourcesFile(SPARK_EXECUTOR_PREFIX, resources, executorDir)
// Launch the process
// 传入的入口类是"org.apache.spark.executor.CoarseGrainedExecutorBackend
val arguments = appDesc.command.arguments ++ resourceFileOpt.map(f =>
Seq("--resourcesFile", f.getAbsolutePath)).getOrElse(Seq.empty)
// 在ExecutorRunner中将通过CommandUtil构建一个ProcessBuilder,
// 调用ProcessBuilder的start方法将会以进程的方式启动org.apache.spark.executor.CoarseGrainedExecutorBackend
// 当Worker节点中启动ExecutorRunner时,ExecutorRunner中会启动CoarseGrainedExecutorBackend进程,
// 在CoarseGrainedExecutorBackend的onStart方法中,向Driver发出RegisterExecutor注册请求
val builder = CommandUtils.buildProcessBuilder(subsCommand, new SecurityManager(conf),
memory, sparkHome.getAbsolutePath, substituteVariables)
val command = builder.command()
...
process = builder.start()
} catch {
...
}
}
可以看到,上述的代码中实际上是启动了CoarseGrainedExecutorBackend
的进程,这个进程实际上调用了onStrart()
的方法
// 当Worker节点中启动ExecutorRunner时,ExecutorRunner中会启动CoarseGrainedExecutorBackend进程,
// 在CoarseGrainedExecutorBackend的onStart方法中,向Driver发出RegisterExecutor注册请求
override def onStart(): Unit = {
...
rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap { ref =>
// This is a very fast action so we can use "ThreadUtils.sameThread"
// 将向Driver端发送RegisterExecutor消息请求注册,完成注册后
driver = Some(ref)
// 向Driver发送ask请求,等待Driver回应
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
extractAttributes, _resources, resourceProfile.id))
}
...
}
由上述代码可以看到,CoarseGrainedExecutorBackend
执行onStart
向Driver端发送了RegisterExecutor
的信息,表示向Driver端发送的是已经注册后的信息,这就跟面筋对应上了!
client模式
在SparkSubmit.prepareSubmitEnviorment
中有一段代码显示
if (deployMode == CLIENT) {
childMainClass = args.mainClass
...
}
这段代码的childMainClass就是你自己构建应用程序的类,表示直接运行你应用程序的代码。
YarnCluster模式
SparkSubmit
最终调用的是YarnClusterApplication
对象中的start方法
private[spark] class YarnClusterApplication extends SparkApplication {
override def start(args: Array[String], conf: SparkConf): Unit = {
// SparkSubmit would use yarn cache to distribute files & jars in yarn mode,
// so remove them from sparkConf here for yarn mode.
conf.remove(JARS)
conf.remove(FILES)
conf.remove(ARCHIVES)
new Client(new ClientArguments(args), conf, null).run()
}
}
在这里创建了Clinet对象,并执行run方法。run方法的注释写的很清楚,这个方法就是向ResourceManager提交申请。如果设置Spark.yarn.Submit.WaitappCompletion to true,它将活着报告应用程序状态,直到由于任何原因退出了应用程序。否则,客户流程将在提交后退出。如果申请以失败,杀死或未定义的身份完成,请抛出适当的Sparkexception。
其中执行的是this.appId = submitApplication()
/**
* Submit an application running our ApplicationMaster to the ResourceManager.
*
* The stable Yarn API provides a convenience method (YarnClient#createApplication) for
* creating applications and setting up the application submission context. This was not
* available in the alpha API.
*/
def submitApplication(): ApplicationId = {
ResourceRequestHelper.validateResources(sparkConf)
var appId: ApplicationId = null
try {
launcherBackend.connect()
yarnClient.init(hadoopConf)
yarnClient.start()
logInfo("Requesting a new application from cluster with %d NodeManagers"
.format(yarnClient.getYarnClusterMetrics.getNumNodeManagers))
// Get a new application from our RM
val newApp = yarnClient.createApplication()
val newAppResponse = newApp.getNewApplicationResponse()
appId = newAppResponse.getApplicationId()
// The app staging dir based on the STAGING_DIR configuration if configured
// otherwise based on the users home directory.
// scalastyle:off FileSystemGet
val appStagingBaseDir = sparkConf.get(STAGING_DIR)
.map { new Path(_, UserGroupInformation.getCurrentUser.getShortUserName) }
.getOrElse(FileSystem.get(hadoopConf).getHomeDirectory())
stagingDirPath = new Path(appStagingBaseDir, getAppStagingDir(appId))
// scalastyle:on FileSystemGet
new CallerContext("CLIENT", sparkConf.get(APP_CALLER_CONTEXT),
Option(appId.toString)).setCurrentContext()
// Verify whether the cluster has enough resources for our AM
verifyClusterResources(newAppResponse)
// Set up the appropriate contexts to launch our AM
val containerContext = createContainerLaunchContext(newAppResponse)
val appContext = createApplicationSubmissionContext(newApp, containerContext)
// Finally, submit and monitor the application
logInfo(s"Submitting application $appId to ResourceManager")
yarnClient.submitApplication(appContext)
launcherBackend.setAppId(appId.toString)
reportLauncherState(SparkAppHandle.State.SUBMITTED)
appId
} catch {
case e: Throwable =>
if (stagingDirPath != null) {
cleanupStagingDir()
}
throw e
}
}
在submitApplication()
执行了以下步骤:
- 初始化YarnClient对象,执行YarnClient方法,提交Application
- 创建AM容器启动的上下文环境、启动命令、上传程序包到HDFS
- 调用yarnClient的方法,提交创建AM Container请求
- 执行APP启动Command bin/java ApplicationMaster –class --jar:根据deployMode不同,启动ApplicationMaster(YarnCluster)和ExecutorLauncher(YarnClient)
在ApplicationMaster
中的main方法中执行master.run()
,这里有个代码表示如下
if (isClusterMode) {
runDriver()
} else {
runExecutorLauncher()
}
点进去runDriver()
代码中一看
private def runDriver(): Unit = {
addAmIpFilter(None, System.getenv(ApplicationConstants.APPLICATION_WEB_PROXY_BASE_ENV))
userClassThread = startUserApplication()
// This a bit hacky, but we need to wait until the spark.driver.port property has
// been set by the Thread executing the user class.
logInfo("Waiting for spark context initialization...")
val totalWaitTime = sparkConf.get(AM_MAX_WAIT_TIME)
try {
val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
Duration(totalWaitTime, TimeUnit.MILLISECONDS))
if (sc != null) {
val rpcEnv = sc.env.rpcEnv
val userConf = sc.getConf
val host = userConf.get(DRIVER_HOST_ADDRESS)
val port = userConf.get(DRIVER_PORT)
registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)
val driverRef = rpcEnv.setupEndpointRef(
RpcAddress(host, port),
YarnSchedulerBackend.ENDPOINT_NAME)
createAllocator(driverRef, userConf, rpcEnv, appAttemptId,
}
其中userClassThread = startUserApplication()
在单独的userThread线程中启动包含driver的user class。
然后回到ApplicationMaster.runDriver
代码中,在创建driver的过程向ResourceManager注册ApplicationMaster,并启动ApplicationMaster。
AM向RM申请Container容器资源,分配资源并在YarnAllocator.runAllocatedContainers
启动ExcutorRunner对象
启动ExecutorRunner对象的代码如下:
if (launchContainers) {
launcherPool.execute(() => {
try {
new ExecutorRunnable(
Some(container),
conf,
sparkConf,
driverUrl,
executorId,
executorHostname,
containerMem,
containerCores,
appAttemptId.getApplicationId.toString,
securityMgr,
localResources,
rp.id
).run()
YarnClient模式可自行查阅源码,执行流程和YarnCluster类似,仅最终启动的组件不同(ExecutorLauncher)
SparkContext初始化
Driver启动完成后就是在调用自定义的MainClass方法,比如下面的代码
object WordCount {
def main(args: Array[String]) {
// Logger.getLogger("org").setLevel(Level.WARN)
/**
* 第1步:创建Spark的配置对象SparkConf,设置Spark程序的运行时的配置信息,
* 例如说通过setMaster来设置程序要链接的Spark集群的Master的URL,如果设置
* 为local,则代表Spark程序在本地运行,特别适合于机器配置条件非常差(例如
* 只有1G的内存)的初学者 *
*/
val conf = new SparkConf() //创建SparkConf对象
conf.setAppName("Wow,My First Spark App!") //设置应用程序的名称,在程序运行的监控界面可以看到名称
conf.setMaster("local[*]") //此时,程序在本地运行,不需要安装Spark集群
/**
* 第2步:创建SparkContext对象
* SparkContext是Spark程序所有功能的唯一入口,无论是采用Scala、Java、Python、R等都必须有一个SparkContext
* SparkContext核心作用:初始化Spark应用程序运行所需要的核心组件,包括DAGScheduler、TaskScheduler、SchedulerBackend
* 同时还会负责Spark程序往Master注册程序等
* SparkContext是整个Spark应用程序中最为至关重要的一个对象
*/
val sc = new SparkContext(conf) //创建SparkContext对象,通过传入SparkConf实例来定制Spark运行的具体参数和配置信息
/**
* 第3步:根据具体的数据来源(HDFS、HBase、Local FS、DB、S3等)通过SparkContext来创建RDD
* RDD的创建基本有三种方式:根据外部的数据来源(例如HDFS)、根据Scala集合、由其它的RDD操作
* 数据会被RDD划分成为一系列的Partitions,分配到每个Partition的数据属于一个Task的处理范畴
*/
val lines = sc.textFile("D:\\programe\\BigDataPrograms\\spark-3.1.3\\spark-3.1.3\\examples\\src\\main\\scala\\org\\apache\\spark\\examples\\my_test\\chapter3_RDD_DataSet\\helloSpark.txt", 1) //读取本地文件并设置为一个Partion
/**
* 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
* 第4.1步:讲每一行的字符串拆分成单个的单词
*/
val words = lines.flatMap { line => line.split(" ") } //对每一行的字符串进行单词拆分并把所有行的拆分结果通过flat合并成为一个大的单词集合
/**
* 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
* 第4.2步:在单词拆分的基础上对每个单词实例计数为1,也就是word => (word, 1)
*/
val pairs = words.map { word => (word, 1) }
/**
* 第4步:对初始的RDD进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算
* 第4.3步:在每个单词实例计数为1基础之上统计每个单词在文件中出现的总次数
*/
val wordCountsOdered = pairs.reduceByKey(_ + _, 1000).map(pair => (pair._2, pair._1)).sortByKey(false).map(pair => (pair._2, pair._1)) //对相同的Key,进行Value的累计(包括Local和Reducer级别同时Reduce)
wordCountsOdered.collect.foreach(wordNumberPair => println(wordNumberPair._1 + " : " + wordNumberPair._2))
sc.stop()
}
点击去val sc = new SparkContext(conf)
方法的时候,表示开始创建SparkContext环境,后续的什么RDD、累加器和广播变量,所有的执行都是在这里定义的,所以就是非常重要的
sparkEnv环境创建
执行代码
// Create the Spark execution environment (cache, map output tracker, etc)
_env = createSparkEnv(_conf, isLocal, listenerBus)
继续执行SparkEnv.createDriverEnv
的代码,其中调用了create
方法,接下来讲按顺序讲解里面SparkEnv环境的组件的启动
- 创建Driver RPC Endpoint对象,是Driver的RPC通信对象,用于和外部组件通信
val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
securityManager, numUsableCores, !isDriver)
- 创建SerializerManager(默认为JavaSerializer)。为各种 Spark 组件配置序列化、压缩和加密的组件,包括自动选择要用于随机shuffle的Serializer程序。
val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey)
- 创建broadcastManager。用于管理broadcast的过程
val broadcastManager = new BroadcastManager(isDriver, conf, securityManager)
- 创建MapOutTrackerMaster。MapOutputTrackerMaster负责Shuffle中数据输出和读取的管理。Shuffle的时候将数据写到本地,下一个Stage要使用上一个Stage的数据, 因此写数据的时候要告诉Driver中的MapOutputTrackerMaster具体写到哪里,下一个Stage读取数据的时候也要访问Driver的MapOutputTrackerMaster 获取数据的具体位置。
// 如果是driver的话,就创建MapOutputTrackerMaster,否则创建MapOutputTrackerWorker
val mapOutputTracker = if (isDriver) {
new MapOutputTrackerMaster(conf, broadcastManager, isLocal)
} else {
new MapOutputTrackerWorker(conf)
}
- 创建ShuffleManager(默认为sort shuffle)
// ShuffleManager是一个用于shuffle系统的可插拔接口。在Driver端SparkEnv中创建ShuffleManager,在每个Executor上也会创建。基于spark.shuffle.manager进行设置。
val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)
- MemoryManager(默认为UnifiedMemoryManager)
// 创建memoryManager,是UnifiedMemoryManager
val memoryManager: MemoryManager = UnifiedMemoryManager(conf, numUsableCores)
- 创建BlockManagerMaster
// 创建BlockManagerMasterEndpoint
// blockManagerMaster主要对内提供各节点之间的指令通信服
// BlockManagerMaster对整个集群的Block数据进行管理,Block是Spark数据管理的单位,与数据存储没有关系,
// 数据可能存在磁盘上,也可能存储在内存中,还可能存储在offline,如Alluxio
val blockManagerMaster = new BlockManagerMaster(
registerOrLookupEndpoint(
BlockManagerMaster.DRIVER_ENDPOINT_NAME,
new BlockManagerMasterEndpoint(
rpcEnv,
isLocal,
conf,
listenerBus,
if (conf.get(config.SHUFFLE_SERVICE_FETCH_RDD_ENABLED)) {
externalShuffleClient
} else {
None
}, blockManagerInfo,
mapOutputTracker.asInstanceOf[MapOutputTrackerMaster])),
registerOrLookupEndpoint(
BlockManagerMaster.DRIVER_HEARTBEAT_ENDPOINT_NAME,
new BlockManagerMasterHeartbeatEndpoint(rpcEnv, isLocal, blockManagerInfo)),
conf,
// isDriver判断是否运行在Driver
isDriver)
- 创建BlockManager
// 创建BlockManager,主要对外提供统一的访问接口
// 注:blockManager无效,直到initialize()被调用
// 这里的BlockManagerMaster和BlockManager属于聚合关系
val blockManager = new BlockManager(
executorId,
rpcEnv,
blockManagerMaster,
serializerManager,
conf,
memoryManager,
mapOutputTracker,
// shuffleManager是在SparkEnv中创建的,将shuffleManager传入到BlockManager,BlockManager就拥有shuffleManager的成员变量,
// 从而可以调用shuffleManager的相关方法
shuffleManager,
blockTransferService,
securityManager,
externalShuffleClient)
- 创建blockTransferService。是使用netty来获取blocks的工具
val blockTransferService =
new NettyBlockTransferService(conf, securityManager, bindAddress, advertiseAddress,
blockManagerPort, numUsableCores, blockManagerMaster.driverEndpoint)
- 除此之外,还创建了OutputCommitCoordinator、outputCommitCoordinatorRef等
上述这些对象将传入new SparkEnv的构造函数返回出来。
TaskScheduler以及SchedulerBackend的初始化
TaskSchedulerImpl是底层的任务调度接口TaskScheduler的实现,这些Schedulers从每一个Stage中的DAGScheduler中获取TaskSet, 运行它们,尝试是否有故障。DAGScheduler是高层调度,它计算每个Job的Stage的DAG, 然后提交Stage,用TaskSets的形式启动底层TaskScheduler调度在集群中运行。
SchedulerBackend负责集群计算资源的管理和调度,这是从作业的角度来考虑的。
回归到SparkContext的代码中,可以看到这个位置创建taskscheduler和schedulerbackend
// Create and start the scheduler
// 创建一个TaskSchedulerImpl和schedulerBackend的实例,
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
根据不同的maste urlr,会创建不同的scheduler,以5种情况考虑
- Local(本地单CPU模式): TaskSchedulerImpl:max_local_task_failures:1(本地最大任务重试)。返回LocalSchedulerBackend:totalCores:1(本地启动cpu核数数量1)
- Local_N_REGEX(Local[*]模式): TaskSchedulerImpl :max_local_task_failures:1((本地最大任务重试)。返回LocalSchedulerBackend:threadCount:1(本地启动指定数目CPU/所以可执行cpu)
- LOCAL_N_FAILURES_REGEX (Local[n,m]本地失败重试模式): TaskSchedulerImpl :maxFailures:m(本地最大任务失败重试) 。返回LocalSchedulerBackend:threadCount:1(本地启动指定数目CPU/所以可执行cpu)
- SPARK_REGEX(StandAlone模式): 返回TaskSchedulerImpl/StandaloneSchedulerBackend
- Yarn模式下的TaskScheduler和SchedulerBackend创建 TaskSchedulerImpl:根据master-url(cluster/client)初始化 YarnClient/YarnClusterSchedulerBackend: 根据master url(cluster/client)初始化相应的Backend
创建完之后就会立刻调用scheduler.initialize(backend)的方法,这里以standalone模式来解释
def initialize(backend: SchedulerBackend): Unit = {
this.backend = backend
// 根据调度模式的配置创建实现了schedulableBuilder接口的相应的实例对象
// 并且创建的对象会立即调用buildPools创建相应数量的Pool存放和管理TaskSetManager的实例对象。
// 实现SchedulerBuilder接口的具体类都是SchedulerBuilder的内部类。
schedulableBuilder = {
// 默认情况FIFO
schedulingMode match {
case SchedulingMode.FIFO =>
new FIFOSchedulableBuilder(rootPool)
case SchedulingMode.FAIR =>
new FairSchedulableBuilder(rootPool, conf)
case _ =>
throw new IllegalArgumentException(s"Unsupported $SCHEDULER_MODE_PROPERTY: " +
s"$schedulingMode")
}
}
schedulableBuilder.buildPools()
}
经过上述的代码,这时候已经创建了taskScheduler以及shcedulerBackend,于是在这里调用_taskScheduler.start()
表示启动taskScheduler,创建FIFO(默认模式)/FAIR的taskset资源调度池,这里后续调度taskset任务
创建启动DAGScheduler
DAGScheduler负责高层调度(如Job中Stage的划分、数据本地性等内容)
DAGScheduler是面向Stage调度的高层调度实现。它为每一个Job计算DAG, 跟踪RDDS及Stage输出结果进行物化,并找到一个最小的计划去运行Job, 然后提交stages中的TaskSets到底层调度器TaskScheduler提交集群运行, TaskSet包含完全独立的任务,基于集群上已存在的数据运行(如从上一个Stage输出的文件), 如果这个数据不可用,获取数据可能会失败。
Spark Stages根据RDD图中Shuffle的边界来创建,如果RDD的操作是窄依赖,如map()和filter(), 在每个Stages中将一系列tasks组合成流水线执行。但是,如果是宽依赖,Shuffle依赖需要多个Stages (上一个Stage进行map输出写入文件,下一个Stage读取数据文件),每个Stage依赖于其他的Stage,其中进行多个算子操作。 算子操作在各种类型的RDDS(如MappedRDD、FilteredRDD)的RDD.compute()中实际执行。
DAGScheduler是面向Stage调度的高层调度实现。它为每一个Job计算DAG,跟踪RDDS及Stage输出结果进行物化, 并找到一个最小的计划去运行Job,然后提交stages中的TaskSets到底层调度器TaskScheduler提交集群运行, TaskSet包含完全独立的任务,基于集群上已存在的数据运行(如从上一个Stage输出的文件),如果这个数据不可用,获取数据可能会失败。
执行以下代码_dagScheduler = new DAGScheduler(this)
,等待后续Job的任务DAG调度
- 执行事件处理
DAGSchedulerEventProcessLoop
线程,主要作用于后续处理DAG切分的核心逻辑 - 发送TaskScheduler成功创建心跳到HeartbeatReceiver
启动SchedulerBackend
_taskScheduler.start()
在Standalone环境种调用的是taskSchedulerImpl的start方法。在这个方法中
-
backend.start()
最终注册应用程序AppClient - 根据配置判断是否周期性的检查任务的推测执行
推测任务的执行
speculationScheduler.scheduleWithFixedDelay(
() => Utils.tryOrStopSparkContext(sc) { checkSpeculatableTasks() },
SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
对一个Stage里面运行慢的Task,会在其他节点的Executor上再次启动这个task,如果其中一个Task实例运行成功则将这个最先完成的Task的计算结果作为最终结果,同时会干掉其他Executor上运行的实例,从而加快运行速度。
-
checkSpeculatableTasks
检测是否有需要推测式执行的Task, 满足非local模式下开启spark.speculation,开启推测执行,存在则backend调用reviveOffers获取资源运行推测任务。 -
TaskSetManager.checkSpeculatableTasks
当成功的Task数超过总Task数的75% (spark.speculation.quantile: 0.75),再统计任务运行时间中位数乘以1.5(spark.speculation.multiplier)的运行时间阈值,如果超出该阈值则启动推测 - 在TasksetManager为下个task分配executor时候
dequeueTask()
中启用调度检测,先过滤掉已经成功执行的task,另外,推测执行task不在和正在执行的tasks同一Host执行,不在黑名单executor里执行。
schedulerBackend.start()执行流程
以Standalone模式讲解
Master发指令给Worker去启动Executor所有的进程时加载的Main方法所在的入口类就是command中的CoarseGrainedExecutorBackend, 在CoarseGrainedExecutorBackend中启动Executor(Executor是先注册,再实例化),Executor通过线程池并发执行Task,然后再调用它的run方法。
// 构建了一个command对象
// 将command封装注册给Master,Master转过来要Worker启动具体的Executor。
// command已经封装好指令,Executor具体要启动进程入口类CoarseGrainedExecutorBackend
val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",
args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)
获取executor_cores的参数,就是每个executor多少个cores
val coresPerExecutor = conf.getOption(config.EXECUTOR_CORES.key).map(_.toInt)
将各种运行信息封装成ApplicationDescription
val appDesc = ApplicationDescription(sc.appName, maxCores, sc.executorMemory, command,
webUrl, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor, initialExecutorLimit,
resourceReqsPerExecutor = executorResourceReqs)
创建StandaloneAppCient
// 创建一个很重要的对象,然后调用它的client.start方法
// 这个携带了command信息,command信息中指定了要启动的ExecutorBackend的实现类
client = new StandaloneAppClient(sc.env.rpcEnv, masters, appDesc, this, conf)
最后调用
client.start()
这个是一个RpcEndPoint,首先调用自己的onStart方法,接下来向Master注册。
blockManager初始化
// 初始化blockManager
_env.blockManager.initialize(_applicationId)
进入具体初始化方法
def initialize(appId: String /** 应用id **/): Unit = {
// 调用blockTransferService的init方法,blockTransferService用于在不同节点fetch数据,传送数据
// 初始化BlockTransferService
blockTransferService.init(this)
externalBlockStoreClient.foreach { blockStoreClient =>
// 用于读取其他Executor上的shuffle files
// 初始化ShuffleClient
blockStoreClient.init(appId)
}
blockReplicationPolicy = {
val priorityClass = conf.get(config.STORAGE_REPLICATION_POLICY)
val clazz = Utils.classForName(priorityClass)
val ret = clazz.getConstructor().newInstance().asInstanceOf[BlockReplicationPolicy]
logInfo(s"Using $priorityClass for block replication policy")
ret
}
// 创建BlockManagerId
val id =
BlockManagerId(executorId, blockTransferService.hostName, blockTransferService.port, None)
// 向blockManagerMaster注册BlockManager,在registerBlockManager方法中传入了slaveEndpoint,slaveEndpoint为BlockManager
// 中的RPC对象,用于和blockManagerMasater通信
val idFromMaster = master.registerBlockManager(
id,
diskBlockManager.localDirsString,
maxOnHeapMemory,
maxOffHeapMemory,
storageEndpoint ) // 这个消息循环体来接收Driver中BlockManagerMaster的信息
// 得到shuffleManageId
blockManagerId = if (idFromMaster != null) idFromMaster else id
// 得到shuffleServerId
shuffleServerId = if (externalShuffleServiceEnabled) {
logInfo(s"external shuffle service port = $externalShuffleServicePort")
BlockManagerId(executorId, blockTransferService.hostName, externalShuffleServicePort)
} else {
blockManagerId
}
// Register Executors' configuration with the local shuffle service, if one should exist.
// 注册shuffleserver
// 如果存在,将注册Executors配置与本地shuffle服务
if (externalShuffleServiceEnabled && !blockManagerId.isDriver) {
registerWithExternalShuffleServer()
}
hostLocalDirManager = {
if (conf.get(config.SHUFFLE_HOST_LOCAL_DISK_READING_ENABLED) &&
!conf.get(config.SHUFFLE_USE_OLD_FETCH_PROTOCOL)) {
Some(new HostLocalDirManager(
futureExecutionContext,
conf.get(config.STORAGE_LOCAL_DISK_BY_EXECUTORS_CACHE_SIZE),
blockStoreClient))
} else {
None
}
}
logInfo(s"Initialized BlockManager: $blockManagerId")
}
- 首先是调用
blockTransferService.init
方法,blockTransferService用于在不同节点fetch数据,传送数据 - 然后遍历exterlBlockStoreClient,初始化blockStoreClient
- 创建BlockManagerId
-
val idFromMaster = master.registerBlockManager
,向blockManagerMaster注册BlockManager,在registerBlockManager方法中传入了slaveEndpoint,slaveEndpoint为BlockManager中的RPC对象,用于和blockManagerMasater通信 - 创建shuffleServerId
- 注册shuffleServer
- 最后是创建hostLocalDirManager
由此,blockManager初始化成功
激活SparkContext
SparkContext.setActiveContext(this)
将当前SparkContext的状态从contextBeingConstructed(正在构建中)改为activeContext(已激活)
Executor启动
当在上文调用schedulerBackend.start()
的时候,实际上就是向Master注册RegisterApplication,并携带要启用的worker信息,在Masetr中有一段代码表示如下
case RegisterApplication(description, driver) =>
// TODO Prevent repeated registrations from some driver
if (state == RecoveryState.STANDBY) {
// ignore, don't send response
} else {
logInfo("Registering app " + description.name)
// 创建ApplicationInfo
val app = createApplication(description, driver)
// 完成application的注册
registerApplication(app)
logInfo("Registered app " + description.name + " with ID " + app.id)
// 通过持久化引擎(如ZooKeeper)把注册信息持久化
persistenceEngine.addApplication(app)
// 给driver发送RegisteredApplication的信息
driver.send(RegisteredApplication(app.id, self))
schedule()
}
将description从SchedulerBackend发送到Master的时候,Master,完成application的注册,并且给driver发送RegisteredApplication的信息。最后调用shedule()开始调度,里面有个方法叫做startExecutorsOnWorkers()
就是启动executor,调度方式默认的是FIFO的模式
CoarseGrainedExecutorBackend运行
刚才Master已经调度了executor的启动,接下来,CoarseGrainedExecutorBackendk开始运行。
在其onStart
方法中,向Driver发送ask注册请求
// 向Driver发送ask请求,等待Driver回应
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
extractAttributes, _resources, resourceProfile.id))
Driver的CoarseGrainedScheduleBackend接收请求并注册Executor
如果注册Executor成功,则给自己发送RegisteredExecutor,并且在receive接收到RegisteredExecutor的信息,立即创建Executor
executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false,
resources = _resources)
driver.get.send(LaunchedExecutor(executorId))
如果创建executer成功了,就像driver发送LauchedExecutor启动的信息
在Driver中,接受LauchedExecutor启动的信息,并且调用makeOffers(executorId)
等待后续分配taskset给Executor。
Task启动
Task任务切分
在完成SparkContext初始化和Executor启动后,这里还是回到我们提交的任务Main方法中。
wordCountsOdered.collect.foreach(wordNumberPair => println(wordNumberPair._1 + " : " + wordNumberPair._2))
这里有个collect的方法,表示收集所有的wordCountsOrdered,收集到driver端中。进入这个方法里面,可以看到里面执行的是Action算子,在其中执行sc.runJob。
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出现了,可以切分stage
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
rdd.doCheckpoint()
}
注意里面执行了dagScheduler.runJob,并在内部执行了submitJob来提交任务。主要就是用来切分stage的了。
submitJob方法调用eventProcessLoop的post方法,调用eventProcessLoop post将JobSubmitted事件添加到DAGScheduler事件队列,给自己发送一个提交任务的作业。
定位到DAGscheduler的doOnReceive中,可以看到DagScheduler接受了JobSubmitted的任务处理,在中间调用
dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
来处理JobSubmitted的任务。
private[scheduler] def handleJobSubmitted(jobId: Int,
finalRDD: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
callSite: CallSite,
listener: JobListener,
properties: Properties): Unit = {
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.
// 如果作业运行在HadoopRDD上,而底层HDFS 的文件已被删除,那么在创建新的Stage
// 时将会跑出一个异常
// 通过createResultStage创建finalStage,传入的参数包括最后一个finalRDD,
// 操作的函数func,分区partitions、jobId、
// callSite等内容。创建过程中可能捕获异常。例如,
// 在Hadoop上,底层的hdfs文件被删除了或者被修改了,就出现异常。
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
} catch {
case e: BarrierJobSlotsNumberCheckFailed =>
// If jobId doesn't exist in the map, Scala coverts its value null to 0: Int automatically.
// 如果jobId在映射中不存在,scala会自动将其值nul1转换为0:Int
val numCheckFailures = barrierJobIdToNumTasksCheckFailures.compute(jobId,
(_: Int, value: Int) => value + 1)
logWarning(s"Barrier stage in job $jobId requires ${e.requiredConcurrentTasks} slots, " +
s"but only ${e.maxConcurrentTasks} are available. " +
s"Will retry up to ${maxFailureNumTasksCheck - numCheckFailures + 1} more times")
if (numCheckFailures <= maxFailureNumTasksCheck) {
messageScheduler.schedule(
new Runnable {
override def run(): Unit = eventProcessLoop.post(JobSubmitted(jobId, finalRDD, func,
partitions, callSite, listener, properties))
},
timeIntervalNumTasksCheck,
TimeUnit.SECONDS
)
return
} else {
// Job failed, clear internal data.
// Job失败,清理内部数据
barrierJobIdToNumTasksCheckFailures.remove(jobId)
listener.jobFailed(e)
return
}
case e: Exception =>
logWarning("Creating new stage failed due to exception - job: " + jobId, e)
listener.jobFailed(e)
return
}
// Job submitted, clear internal data.
// 提交作业,清除内部数据
barrierJobIdToNumTasksCheckFailures.remove(jobId)
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)
// 根据finalStage找父Stage,如果有父Stage,就直接返回,如果没有父Stage,就进行创建
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,
Utils.cloneProperties(properties)))
// 以递归的方式提交stage
submitStage(finalStage)
}
handleJobSubmitted代码处理流程如下:
- 触发job的最后一个rdd,创建finalStage并同时创建shuffleMapStage
- 用finalStage创建一个Job,这个job的最后一个stage,就是finalStage
- 将Job相关信息,加入内存缓冲中
- 第四步,使用submitStage方法提交finalStage,这是以递归的方式进行的。
private def submitStage(stage: Stage): Unit = {
// activeJobForStage中获得JobID
val jobId = activeJobForStage(stage)
// 如果已经定义isDefined,那就获得即将计算的Stage(getMissingParentStages),然后进行升序排列
if (jobId.isDefined) {
logDebug(s"submitStage($stage (name=${stage.name};" +
s"jobs=${stage.jobIds.toSeq.sorted.mkString(",")}))")
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
// 获得即将计算的Stage,然后进行升序排序
val missing = getMissingParentStages(stage).sortBy(_.id)
logDebug("missing: " + missing)
// 已经没有missing的parents了
if (missing.isEmpty) {
logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
// 提交任务,将每一个Stage和jobId传入
submitMissingTasks(stage, jobId.get)
} else {
// 如果父Stage不为空,将循环递归调用submitStage,从后往前回溯,也就是说,只有前面的依赖的Stage计算完毕后,
// 后面的Stage才会运行。submitStage一直循环调用,导致的结果是父Stage的父Stage……一直回溯到最左侧的父Stage开始计算。
for (parent <- missing) {
submitStage(parent)
}
waitingStages += stage
}
}
} else {
abortStage(stage, "No active job for stage " + stage.id, None)
}
}
过程如下:
- 获取final RDD的shuffleDependies,遍历调用查找finalStage的父stage
- 调用getMissingParentStage查找finalStage的父stage(根据rdd的dependies判断, 如果是shuffleDependency宽依赖则生成stage, Narrow窄依赖则继续压入栈中继续向上遍历),最后返回stage列表
- 如果存在父stage, 则递归调用submitStage(如果一直存在则递归直到stage0), 将当前stage加入waitingStage待提交;
- 如果不存在stage, 则直接提交stage中未提交的tasks(submitMissingTasks)
- 后续submitMissingTask, 为stage创建一批tasks,数量等同于partitions(final RDD的); 计算每个task对应的Partition最佳位置
- 对于stage的task,创建taskset对象,调用TaskSchduler的submitTasks方法
根据上述的过程,这里是递归获取parent依赖,如果已经没有parent依赖了,就开始submitMissingTask,中间过程如下:
- 获取当前stage没有计算的partitions和properities
- 如果是shuffleMapStage,调用MapOutputTrackerMaster的findMissingpartitions方法查找MapOutputTracker中需要参与计算的该stage的partitionIds
- 如果是ResultStage, 获取当前job中未计算的partitionId
- 将stage添加到runningStage中
- 匹配stage类型,获取task对应partition的最优资源位置来运行job(查看缓存cache中内存->查找BlockManager存储优先级别)
- 根据stage类型不同封装task, 传递给TaskScheduler调用task
/**
* 创建了一个TaskSet对象,将所有任务的信息封装,包括task任务列表,stageId,任务id,分区数参数等
*/
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties,
stage.resourceProfileId))
此时已经把stage切分成很多个task的了,点进去taskScheduler.submitTasks的方法中,代码表述如下
override def submitTasks(taskSet: TaskSet): Unit = {
val tasks = taskSet.tasks
logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks "
+ "resource profile " + taskSet.resourceProfileId)
this.synchronized {
// 创建TaskSetManager保存了taskSet任务列表
// TaskSetManager对其生命周期进行管理,当TaskSchedulerImpl得到Worker节点上的Executor计算资源的时候,
// 会通过TaskSetManager发送具体的Task到Executor上执行计算。
val manager = createTaskSetManager(taskSet, maxTaskFailures)
val stage = taskSet.stageId
val stageTaskSets =
taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
// 在添加新任务时,将此阶段的所有现有任务集管理器TaskSetManagers 标记为zombi,
// 这是处理极端情况所必需的。假设一个 stage 有 10个分区,
// 2个分区任务集管理器TaskSetManagers:TSM1(zombie)和 TSM2(active)。
// TSM1的10个分区的任务都运行完成了。TSM2 完成了分区 1-9 的任务,
// 认为它仍然活跃,因为分区 10 还没有完成。但是,DAGScheduler
// 获取到所有 10 个分区完成任务的事件,并认为该阶段已完成。
// 如果是洗牌阶段shuffle不知何故缺少映射输出,DAGScheduler
// 将重新提交它并创建一个TSM3。由于一个阶段不能有多个活动任务集管理器,
// 因此必须标记TSM2是zombie(实际上是)。
stageTaskSets.foreach { case (_, ts) =>
ts.isZombie = true
}
// 将任务加入调度池
stageTaskSets(taskSet.stageAttemptId) = manager
// 创建了TaskSetManager后,非常关键的一行是这个代码
// SchedulableBuilder会确定TaskSetManager的调度顺序,
// 然后按照TaskSetManager的locality aware来确定每个Task具体运行在哪个ExecutorBackend中。
//
// 两种调度模式FIFOSchedulableBuilder、FairSchedulableBuilder
// 这里的调度策略可以通过spark-env.sh中的spark.sheduler.mode进行具体设置,默认是FIFO的方式
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
if (!isLocal && !hasReceivedTask) {
starvationTimer.scheduleAtFixedRate(new TimerTask() {
override def run(): Unit = {
if (!hasLaunchedTask) {
logWarning("Initial job has not accepted any resources; " +
"check your cluster UI to ensure that workers are registered " +
"and have sufficient resources")
} else {
this.cancel()
}
}
}, STARVATION_TIMEOUT_MS, STARVATION_TIMEOUT_MS)
}
hasReceivedTask = true
}
// 接受任务,这里的backend是coarseGrainedSchedulerBackend一个Executor任务调度对象
backend.reviveOffers()
}
代码主要完成了以下操作
- 把task封装到TaskSetManager,并且放入到了调度器
- 执行backend.reviveOffers(),调用CoarseGrainedSchedulerBackend的reviveOffers进行任务分配(executor最优位置分配),然后把ReviveOffers内部消息发回给自己
driver接受到ReviveOffers之后就调用makeOffers()方法,然后执行启动tasks,launchTasks(scheduler.resourceOffers(workOffers)),这些task已经在前面确定好运行在那个ExecutorBackend上。在内部将task序列化然后发送LaunchTask(data)信息给已经选定的CoarseGrainedExecutorBackend。
随后CoarseGrainedExecutorBackend解析task任务信息,并且启动executor.launchTask(this, taskDesc)
def launchTask(context: ExecutorBackend, taskDescription: TaskDescription): Unit = {
// 调用TaskRunner句柄创建TaskRunner对象
// TaskRunner是一个Runnable,里面的run方法种包括任务的反序列化等内容,通过Runnable封装任务,然后放入到runningTasks中,
val tr = new TaskRunner(context, taskDescription, plugins)
// 将创建的TaskRunner对象放入即将进行的堆栈中
runningTasks.put(taskDescription.taskId, tr)
// 从线程池中分配一条线程给TaskRunner,是一个newDaemonCachedThreadPool,任务交给Executor的线程池中的线程去执行,执行
// 的时候下载资源、数据等内容
threadPool.execute(tr)
if (decommissioned) {
log.error(s"Launching a task while in decommissioned state.")
}
}
在方法内部创建TaskRunner对象,然后把创建的TaskRunner对象放入到即将进行的堆栈中。从线程池中分配一条线程给TaskRunner,是一个newDaemonCachedThreadPool,任务交给Executor的线程池中的线程去执行。
至此,task任务的划分和分配已经完成,下面就是task任务在executor的启动执行过程
Task任务执行
已知道TaskRunner继承了Runnable对象,所以在里面实现了run方法。
点进去方法看一下,主要实现了
- 初始化Task线程环境和TaskMemoryManger等组件
- 对序列化的task数据进行反序列化
- 远程网络通信拉取文件(文件、资源、jar等)
- 调用task.run方法,在方法内部调用了runTask(context)。runTask方法内部回调用RDD的iterator()方法,该方法就是针对前Task对应的Partition进行计算的关键所在,在处理的方法内部回迭代Partition的元素,并交给自定以的function进行处理。注意,这里的runTask有两种实现,一种是ShuffleMapTask、另外一种是ResultTask
ShufleMapTask任务实现
在ShuffleMapTask的实现中,首先,ShuffleMapTask会反序列化RDD及其依赖关系,然后通过调用RDD的iterator方法进行计算, 而iterator方法中进行的最终运算的方法是compute()。
override def runTask(context: TaskContext): MapStatus = {
// Deserialize the RDD using the broadcast variable.
// 使用广播变量反序列化RDD
val threadMXBean = ManagementFactory.getThreadMXBean
val deserializeStartTimeNs = System.nanoTime()
val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime
} else 0L
// 创建序列化器
val ser = SparkEnv.get.closureSerializer.newInstance()
// 反序列化出RDD和依赖关系
val rddAndDep = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
// RDD反序列化的时间
_executorDeserializeTimeNs = System.nanoTime() - deserializeStartTimeNs
_executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
} else 0L
val rdd = rddAndDep._1
val dep = rddAndDep._2
// While we use the old shuffle fetch protocol, we use partitionId as mapId in the
// ShuffleBlockId construction.
val mapId = if (SparkEnv.get.conf.get(config.SHUFFLE_USE_OLD_FETCH_PROTOCOL)) {
partitionId
} else context.taskAttemptId()
// 用来将结果写入Shuffle管理器
dep.shuffleWriterProcessor.write(rdd, dep, mapId, context, partition)
}
可以见到,首先反序列化出RDD和依赖关系,然后调用 dep.shuffleWriterProcessor.write(rdd, dep, mapId, context, partition)
来最终执行你定义的RDD算子func,点进去看一下
// 将计算结果通过writer对象的write方法写出
writer.write(
// 会调用RDD的iterator,然后针对partition进行计算
rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
ShuffleMapTask在write写出map信息的时候,在内部调用了RDD的iterator,针对partition进行计算
final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
// 判断此RDD的持久化等级是否为None,(不进行持久化)
if (storageLevel != StorageLevel.NONE) {
// iterator方法中进行的最终运算方法是compute
// RDD的compute方法是一个抽象方法,每个RDD都需要重写的方法。
getOrCompute(split, context)
} else {
computeOrReadCheckpoint(split, context)
}
}
在这里根据RDD是否为持久化来执行
private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
{ // 如果RDD进行了checkpoint,则从父RDD的iterator中直接获取数据
if (isCheckpointedAndMaterialized) {
firstParent[T].iterator(split, context)
} else {
// 没有checkpoint,则重新计算RDD的数据
// 具体计算有具体的RDD,如果MapPartitionsRDD的compute,传进去的Partition及TaskContext上下文
compute(split, context)
}
}
如果RDD进行了checkpoint,则从父RDD的iterator中直接获取数据,而如果没有checkpoint,则通过compute来执行自己定义的算子方法。
返回到ShuffleMapTask的writer.write方法中,实际上这里就是shuffle的过程了,这里可以看我写的一篇shuffle过程文章【读懂面经中的源码】SPARK源码解析——shuffle过程_番茄薯仔的博客-CSDN博客,中间的过程也是非常的刺激好玩~
ResultTask
override def runTask(context: TaskContext): U = {
// Deserialize the RDD and the func using the broadcast variables.
// 使用广播变量反序列化RDD及函数
val threadMXBean = ManagementFactory.getThreadMXBean
val deserializeStartTimeNs = System.nanoTime()
val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime
} else 0L
/// 创建序列化器
val ser = SparkEnv.get.closureSerializer.newInstance()
// 反序列RDD和func处理函数
val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
_executorDeserializeTimeNs = System.nanoTime() - deserializeStartTimeNs
_executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
} else 0L
func(context, rdd.iterator(partition, context))
}
这里反序列化的func处理函数,最终调用func(context, rdd.iterator(partition, context))
来执行自定义RDD算子方法
至此,Spark任务提交、调度、执行过程就全部全部梳理一遍
总结
因为读源码中间过程代码量巨大,在提交、调度、执行的过程每个环节都有其非常复杂的实现,篇幅有限则无法深入到具体的实现(比如递归切分stage、比如如何获取内存block,这些具体的理解太过于缺乏了),只能浮在表面去解释。