本文主要内容:
- spark standalone模式的 job执行流程;
- sparkContext主要作用;
- Master选主原理;
- Master主备切换原理等
作业提交和执行流程
以standalone模式为例的基本流程:
- spark-submit 提交代码,执行 new SparkContext(),在 SparkContext 里构造 DAGScheduler 和 TaskScheduler。
- TaskScheduler 会通过后台的一个进程,连接 Master,向 Master 注册 Application。
- Master 接收到 Application 请求后,会使用相应的资源调度算法,在 Worker 上为这个 Application 启动多个 Executer。
- Executor 启动后,会自己反向注册到 TaskScheduler 中。 所有 Executor 都注册到 Driver 上之后,SparkContext 结束初始化,接下来往下执行我们自己的代码。
- 每执行到一个 Action,就会创建一个 Job。Job 会提交给 DAGScheduler。
- DAGScheduler 会将 Job划分为多个 stage,然后每个 stage 创建一个 TaskSet。
- TaskScheduler 会把每一个 TaskSet 里的 Task,提交到 Executor 上执行。
- Executor 上有线程池,每接收到一个 Task,就用 TaskRunner 封装,然后从线程池里取出一个线程执行这个 task。(TaskRunner 将我们编写的代码,拷贝,反序列化,执行 Task,每个 Task 执行 RDD 里的一个 partition)
下面再赘述一遍哈哈:
spark-submit shell提交Application,提交到Driver进程。Spark使用DriverWrapper启动用户APP的main函数,如图。
Driver进程执行Application应用代码,我们的代码里先getOrCreate() 构建sparkContext。
sparkContext初始化时,主要是构建出TaskScheduler和DAGScheduler。
TaskScheduler通过后台进程连接Master,向Master注册Application.
Master收到注册请求后,通知集群的Worker(资源调度算法),为这个Application启动多个Executor.
Executor启动后,各自反向注册到Driver上的TaskScheduler,
之后Driver继续执行Application自己的代码。
每执行到一个Action,就会创建一个Job,提交给DAGScheduler。
DAGScheduler会将job划分为多个stage(划分算法),然后每个stage创建一个TaskSet。
TaskScheduler把TaskSet里每一个task提交到Executor上去执行(task分配算法)。
Executor接收到task会用TaskRunner来封装task,然后从线程池中取出一个线程,准备执行这个task.
TaskRunner将算子和函数拷贝,反序列化,然后执行task.
Task有两种,ShuffleMapTask和ResultTask,最后一个stage是ResultTask.
spark程序的执行,就是stage分批次作为taskset提交到executor执行,
每个task针对RDD的一个partition,执行我们定义的算子和函数。
源码spark2.4.3
1. SparkContext.scala
位于core - org.apache.spark
// 创建并启动 TaskScheduler
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
// 创建 DAGScheduler
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
// start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's constructor
_taskScheduler.start()
......
//注册spark指定的监听器,然后启动监听器总线 listenerBus。
setupAndStartListenerBus()
// TaskScheduler启动后,用listenerBus Post环境更新事件
postEnvironmentUpdate()
// Post 应用启动事件 (by listenerBus)
postApplicationStart()
_taskScheduler.postStartHook()
SparkContext主要工作:
- 创建TaskScheduler
- 调用TaskSchedulerImpl的start()
- 创建了DAGScheduler
下面分开看。
1.1 创建TaskScheduler
createTaskScheduler()方法
根据不同的Master启动模式,分别处理。分为Local模式,Standalone模式,Cluster模式三类。
//只看Standalone模式
//sparkUrl: 集群的 master URL
case SPARK_REGEX(sparkUrl) =>
val scheduler = new TaskSchedulerImpl(sc)
val masterUrls = sparkUrl.split(",").map("spark://" + _)
val backend = new StandaloneSchedulerBackend(scheduler, sc, masterUrls)
scheduler.initialize(backend)
(backend, scheduler) //返回值
先new一个 TaskSchedulerImpl,然后用某种SchedulerBackend来初始化。
- TaskScheduler通过一个SchedulerBackend, 针对不同的cluster模式来调度task。如果使用LocalSchedulerBackend,并将isLocal设为true,则可以工作在本地模式下。
- TaskScheduler负责处理通用逻辑,比如决定多个job的调度顺序,启动推测任务执行
- 使用时,应先调用initialize()和start()方法,然后再通过submitTasks()方法提交task Set.
initialize()方法根据调度模式是FIFO还是FAIR,创建不同的SchedulableBuilder,调用其buildPools()方法,创建一个调度池。
1.2 调用TaskSchedulerImpl的start()
TaskSchedulerImpl的start(),调用的是SchedulerBackend的start()。
StandaloneSchedulerBackend的start()方法主要逻辑:
// ApplicationDescription是一个Wrapper
// 包含了submit脚本的参数,例如num-executors,executor-memory,executor-core 等
val appDesc = ApplicationDescription(sc.appName, maxCores, sc.executorMemory, command,
webUrl, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor, initialExecutorLimit)
// AppClient,负责app与spark集群通信,它会接收一个spark master url,ApplicationDescription
// 和一个集群事件的监听器,把各种事件回调给监听器。
// AppClient有一个内部类ClientActor,去连接某个master的url,发送RegisterApplication消息。
client = new StandaloneAppClient(sc.env.rpcEnv, masters, appDesc, this, conf)
client.start()
// 保存变更状态
launcherBackend.setState(SparkAppHandle.State.SUBMITTED)
waitForRegistration()
launcherBackend.setState(SparkAppHandle.State.RUNNING)
1.3 创建DAGScheduler
package org.apache.spark.scheduler下的DAGScheduler.scala
- DAGScheduler作为high-level层的调度,实现了面向stage的调度。它计算DAG每个作业的stages,跟踪哪些RDDs和stage输出被实体化,并找到最小调度计划来运行job。然后它将stages作为TaskSets提交给具体的TaskScheduler实现,并在集群上运行。
TaskSet包含了完全独立的任务,可以基于集群中已有的数据(比如,前一stage中map()的输出文件)立即运行。如果该数据变得不可用,它可能会失败。
- DAGScheduler还决定了要在其上运行的每个task的最佳位置,并将这些位置传递给low-level的TaskScheduler。
此外,它还处理由于shuffle输出文件丢失而导致的故障。这种情况下,旧的stages可能需要重新提交。
一个stage内部的失败,如果不是由于shuffle文件丢失所导致的,会被TaskScheduler重试几次,直到最后仍然失败,才会撤销整个stage.
2. Master.scala
位于core下的package org.apache.spark.deploy.master
Master主要负责:
- Leader选举
选举成为Leader后,如果状态是RECOVERING就恢复storedApps, storedDrivers, storedWorkers重新调度; - 管理Application
createApplication/registerApplication/UnregisterApplication/removeApplication - 管理Driver
createDriver/launchDriver/removeDriver/KillDriver/relaunchDriver - 管理Workers以及worker上的Executors
RegisterWorker/removeWorker
scheduleExecutorsOnWorkers/startExecutorsOnWorkers/allocateWorkerResourceToExecutors/launchExecutor/handleRequestExecutors/handleKillExecutors - 处理状态改变ExecutorStateChanged
- 主备切换completeRecovery
2.1 leader选举详解
Master内部我们称"leader选举",Master外部我们称为“Master选举”。
Spark支持以下几种选主(Master选举)策略,这种策略可以通过配置文件spark-env.sh配置spark.deploy.recoveryMode。
- ZOOKEEPER: 集群元数据持久化到zookeeper,当master出现异常的时候,zookeeper会通过选举机制选举出新的Master,新的Master接管集群时需要从zookeeper获取持久化信息,并根据这些信息恢复集群状态。
- FILESYSTEM: 集群的元数据持久化到文件系统,当Master出现异常的时候,只要在该机器上重启Master,启动后的Master获取持久化信息并根据持久化信息恢复集群状态。
- CUSTOM: 自定义恢复模式,实现StandaloneRecoveryModeFactory抽象类进行实现,并把该类配置到配置文件,当Master出现异常,会根据用户自定义的方式进行恢复集群状况。
- NONE: 不持久化集群元数据,当Master出现异常时,新启动的Master不进行恢复集群状态。
Apache Curator是一个Zookeeper的开源客户端,它提供了Zookeeper各种应用场景(Recipe,如共享锁服务、master选举、分布式计数器等)的抽象封装。
Curator提供了两种选举方案:Leader Latch 和 Leader Election。
Spark的Zookeeper模式使用的是Leader Latch选举方式。
master onStart()时候,根据当前配置的spark.deploy.recoveryMode策略,比如Zookeeper,初始化这两个引擎:
org.apache.spark.deploy.master.ZooKeeperLeaderElectionAgent.scala
org.apache.spark.deploy.master.ZooKeeperPersistenceEngine
ZooKeeperLeaderElectionAgent会在构造时,执行start()方法。
在ZK的选举目录发生变化时,会自动触发Watch,执行回调方法。选举成功时回调isLeader(),落选时回调notLeader()。
回调中执行updateLeadershipStatus()更新状态。
// WORKING_DIR就是要监听的ZK目录
var WORKING_DIR = conf.get("spark.deploy.zookeeper.dir", "/spark") + "/leader_election"
private def start() {
logInfo("Starting ZooKeeper LeaderElection agent")
zk = SparkCuratorUtil.newClient(conf)
// 用WORKING_DIR构造LeaderLatch
leaderLatch = new LeaderLatch(zk, WORKING_DIR)
// 添加监听器,ZooKeeperLeaderElectionAgent继承了LeaderLatchListener,
// 并且实现了isLeader/notLeader等方法。
leaderLatch.addListener(this)
// 启动LeaderLatch
leaderLatch.start()
}
override def stop() {
leaderLatch.close()
zk.close()
}
override def isLeader() {
synchronized {
// could have lost leadership by now.
if (!leaderLatch.hasLeadership) {
return
}
logInfo("We have gained leadership")
updateLeadershipStatus(true)
}
}
override def notLeader() {
synchronized {
// could have gained leadership by now.
if (leaderLatch.hasLeadership) {
return
}
logInfo("We have lost leadership")
updateLeadershipStatus(false)
}
}
private def updateLeadershipStatus(isLeader: Boolean) {
if (isLeader && status == LeadershipStatus.NOT_LEADER) {
status = LeadershipStatus.LEADER
// 执行Master的electedLeader()方法。
masterInstance.electedLeader()
} else if (!isLeader && status == LeadershipStatus.LEADER) {
status = LeadershipStatus.NOT_LEADER
masterInstance.revokedLeadership()
}
}
Master继承了LeaderElectable,并实现了electedLeader方法。因此在updateLeadershipStatus时,
Master执行:
override def electedLeader() {
self.send(ElectedLeader)
}
给发个消息通知自己,读取持久化引擎的数据用来恢复:
case ElectedLeader =>
val (storedApps, storedDrivers, storedWorkers) = persistenceEngine.readPersistedData(rpcEnv)
state = if (storedApps.isEmpty && storedDrivers.isEmpty && storedWorkers.isEmpty) {
RecoveryState.ALIVE
} else {
RecoveryState.RECOVERING
}
logInfo("I have been elected leader! New state: " + state)
2.2 处理Application消息
创建Application,registerApplication(),添加到持久化引擎,发送注册完成消息,最后调用schedule()。
createApplication()返回一个ApplicationInfo对象
registerApplication()传入的app的driver地址已存在,就不再注册。否则把app加入到apps/idToApp/actorToApp(内存缓存),以及waitingApps这个等待调度的队列(FIFO ArrayBuffer)。
发送注册完成消息:向SparkDeploySchedulerBackend的AppClient的ClientActor发送消息(Registered)。
2.3 管理Driver
2.4 管理Workers以及worker上的Executors
2.5 处理状态改变
case DriverStateChanged
如果driver的状态是错误、完成、被杀掉、失败,就移除之:
removeDriver()用scala的find()找到id对应的driver,
从内存缓冲中清除,加入到completeedDrivers,使用持久化引擎去除driver的信息,设置driver的状态,将driver所在的worker移除,最后调用schedule()。
case ExecutorStateChanged
从idToApp内存缓存中查找Executor,如果找到就更新状态,向driver同步发送ExecutorUpdated消息,
判断如果Executor完成了,就从appInfo中移除,从运行executor的worker缓存中移除。
判断如果Executor的退出状态是非正常的,如果application重试次数小于最大值,就重新schedule(),超过了最大次数,认为Application失败了,就removeApplication()。
2.6 主备切换(Master恢复)
- 首先完成选举,成为leader。见上述2.1。
- 执行beginRecovery()开始恢复
- worker接收到MasterChange消息,向master发送消息:WorkerSchedulerStateResponse
- Application接收到MasterChange消息,向Master发送消息:MasterChangeAcknowledged
- Master接收到WorkerSchedulerStateResponse和MasterChangeAcknowledged消息后,调用completeRecovery操作
- 执行completeRecovery()完成恢复
beginRecovery()
选主成功,从持久化引擎读取storedApps, storedDrivers, storedWorkers,
- 对每个app/worker,重新注册给本Master的内存记录,修改状态为UNKNOWN,通知app的driver发送MasterChanged消息。
- 对每个driver,just存入driver列表。
worker.scala收到MasterChanged
org.apache.spark.deploy.worker
case MasterChanged(masterRef, masterWebUiUrl) =>
logInfo("Master has changed, new master is at " + masterRef.address.toSparkURL)
//获取新的master的url和master地址,连接状态置为true, 取消之前的重新注册的attempts
changeMaster(masterRef, masterWebUiUrl, masterRef.address)
val execs = executors.values.
map(e => new ExecutorDescription(e.appId, e.execId, e.cores, e.state))
// 向新的master发送WorkerSchedulerStateResponse消息
masterRef.send(WorkerSchedulerStateResponse(workerId, execs.toList, drivers.keys.toSeq))
Application收到MasterChanged
org.apache.spark.deploy.client.StandaloneAppClient.scala
case MasterChanged(masterRef, masterWebUiUrl) =>
logInfo("Master has changed, new master is at " + masterRef.address.toSparkURL)
master = Some(masterRef)
alreadyDisconnected = false
masterRef.send(MasterChangeAcknowledged(appId.get))
completeRecovery()
收到MasterChangeAcknowledged/WorkerSchedulerStateResponse消息,或者Master onDisconnected的时候,如果canCompleteRecovery为true,可以执行completeRecovery()来完成Master恢复。
将Application和Worker过滤出目前状态还是Unknown的,分别调用removeWorker和finishApplication方法,对可能已经出故障,甚至已经死掉的Application和Worker,进行清理。
清理包括:1、从内存缓存结构中移除;2、从相关的组件的内存缓存中移除; 3、从持久化存储中移除。最后调用schedule().
def canCompleteRecovery =
workers.count(_.state == WorkerState.UNKNOWN) == 0 &&
apps.count(_.state == ApplicationState.UNKNOWN) == 0
private def completeRecovery() {
// 使用一个较小的同步周期,来确保"Only-Once" 的恢复语义。
if (state != RecoveryState.RECOVERING) { return }
state = RecoveryState.COMPLETING_RECOVERY
// 消灭所有不再回应的workers和apps
workers.filter(_.state == WorkerState.UNKNOWN).foreach(
removeWorker(_, "Not responding for recovery"))
// 执行finishApplication方法
apps.filter(_.state == ApplicationState.UNKNOWN).foreach(finishApplication)
// 把恢复的apps状态改为RUNNING
apps.filter(_.state == ApplicationState.WAITING).foreach(_.state = ApplicationState.RUNNING)
// 重新调度workers为空的的Drivers
drivers.filter(_.worker.isEmpty).foreach { d =>
logWarning(s"Driver ${d.id} was not found after master recovery")
if (d.desc.supervise) {
logWarning(s"Re-launching ${d.id}")
relaunchDriver(d)
} else {
removeDriver(d.id, DriverState.ERROR, None)
logWarning(s"Did not re-launch ${d.id} because it was not supervised")
}
}
state = RecoveryState.ALIVE
schedule()
logInfo("Recovery complete - resuming operations!")
}
后续任务:
Master资源调度算法、worker主要工作等
…