启动
什么叫做启动Spark Application?
首先,要想象为什么需要用Spark?
计算一个文件行数,直接启动一个jvm去读取文件,计算行数。
文件越来越来大,单机效果不理想,于是希望采取分布式的方式执行,让多台机子分别去统计一部分数据,之后汇总结果。分布式任务执行又涉及到资源的调度、failover、分布式信息的同步等等复杂问题,此时我们需要一个框架来帮我们屏蔽这些。对了,这样我们就用到了Spark框架来方便我们执行分布式的计算任务。 因此,启动一个Spark Application,可以简单的认为启动一个分布式的任务执行中间件,这个application可以将我提交的任务分布式得完成计算。
那回到问题,怎么才算启动了一个Spark Application?
要说明这一点,又需要引入两个概念:SparkSession, SparkContext.
SparkContext:A SparkContext represents the connection to a Spark cluster, and can be used to create RDDs, accumulators and broadcast variables on that cluster. we can only have one active SparkContext per JVM
SparkSession: Spark 2+, 引入DataSet、DataFrame的 API入口
查看两者源代码,可以看出SparkSession将SparkContext 封装在其里面,并额外添加了基于DataSet、DataFrame的API. SparkContext则更为具体,首先从API层面上,SparkContext直接提供的是RDD、broadcast、accumulator、job等Spark基础概念的底层API;其次从类成员变量上看,可以发现SparkContext的内容十分丰富且十分关键,比如_dagScheduler、_env、_taskScheduler、_applicationId.从变量和方法定义上就可以看出,SparkContext维护driver到executor之间的所有交互(任务分发、任务状态等),同时还包括与资源管理者(yarn)的交互(在物理资源上申请executor)等等。而_applicationId基本就在明说一个SparkContext=一个Spark Application.
== to-complete ==
experiment: create two session connected to one application
复制代码
启动
local Application
一般用于本地调试用
spark2.2
val spark = SparkSession
.builder()
.appName("Spark SQL basic example")
.master("local[4]")
.getOrCreate()
复制代码
spark 1.6
val config = new SparkConf().setMaster("local[5]").setAppName("test")
val sc = SparkContext.getOrCreate(config)
复制代码
driver SparkContext 启动代码
如前文所说,SparkContext就对应一个application(spark 工作集群),SparkContext的create过程即为application的启动过程。( 只有driver有sparkContext, executor都会有SparkEnv, not SparkContext)
查看SparkContext.scala源文件,可以发现有SparkContext类以及其伴生对象
class SparkContext : SparkContext 封装类,当前开放设计上一个jvm只能有一个对象实例
object SparkContext : 提供一些SparkContext相关的静态方法, 比方说getOrCrea0te
复制代码
以SparkContext.getOrCreate(sparkConf) 为入口,通过加锁控制,获取当前active context 或新建一个SparkContext实例。
SparkContext.class
主要成员变量
其中几个主要的变量说明:
listenerBus: 用来传输spark事件,如webui通过监听一些启动、task完成等等事件来完成web上的数据刷新等。
_conf: SparkConf 配置信息,从外部传入数据拷贝生成
_env: Spark运行环境, 非常重要, 包括了该instance运行时所有需要需要的东西,如序列化、数据块管理、broadcast管理 、map output tracker
_jobProgressListener: JobProgressListener
_ui: Spark UI
复制代码
查看SparkContext的初始化代码,关键步骤说明如下
-
SparkEnv初始化
// Create the Spark execution environment (cache, map output tracker, etc) _env = createSparkEnv(_conf, isLocal, listenerBus) SparkEnv.set(_env) 复制代码
SparkEnv中封装了该spark instance运行时需要的所有东西,如序列化、数据块管理、broadcast管理 、map output tracker。
初始化步骤
1.1 生成一个RpcEnv,RpcEnv中包含streamingManager,用来管理jars以及files,若当前instance为driver,则会按照RpcEnv里的端口设定等,启动netty server,作为RPC server;若为executor,==todo==
1.2 生成序列化相关,serializerManager(用户可指定序列户类) 、closureSerializer(使用JavaSerializer)
1.3 生成boradcastManager, 用来管理boradcast变量
1.4 生成mapOutputTracker,有driver / executor之分
1.5 shuffleManager
1.6 memoryManager
1.7 blockManager
1.8 metricsSystem
1.9 outputCommitCoordinator
-
_ui SparkUI spark 提供的web UI,可查看spark 集群中任务的运行情况、内存、task分布等等,十分有用。实际上就是基于jetty实现的http server.
_ui = if (conf.getBoolean("spark.ui.enabled", true)) { Some(SparkUI.createLiveUI(this, _conf, listenerBus, _jobProgressListener, _env.securityManager, appName, startTime = startTime)) } else { // For tests, do not enable the UI None } // Bind the UI before starting the task scheduler to communicate // the bound port to the cluster manager properly _ui.foreach(_.bind()) 复制代码
-
添加jars,files
添加jars、files到env.fileserver里,后续通过filerserver提供访问这些文件的接口,目前看来是通过NettryStreamMananger实现
-
任务执行相关
4.1 读取executor设置参数,driver的rpc server添加hearbeat相关处理。
4.2 生成调度相关
_dagScheduler,最上层的调度层,以stage为单位进行调度。负责将每一个job按照shuffle分为stage,形成DAG图,优化stage的调度顺序。 一个stage里包含多个task,每个task内容一样,task数目=partition数目,dagScheduler将task提交给 _taskScheduler _taskScheduler, 负责接受DAG提交的stage,也就是提交的任务集(taskSet),放入task Pool中。根据调度策略(FIFO/FAIR)向下层(物理资源层)提交任务,屏蔽了底层具体的物理资源调度,物理层上的一层抽象。根据 集群资源管理设置(local, yarn, mesos)会有不同的实现,但差异 应该不大(继承自TaskSchedulerImpl) _schedulerBackend, 根据 集群资源管理设置(local, yarn, mesos) 来实际完成与资源管理器的交互, 从_schedulerBackend中获取applicaitonId, 并更新给其他 复制代码
4.3 启动_taskScheduler,(此时应该启动driver的executor? local模式下此时启动了driver-executor)
4.3 启动_executorAllocationManager
至此,Spark Application已经成功启动,local 模式下,_schedulerBackend实现为LocalSchedulerBackend,local模式下,所有进程均在同一jvm中,此时driver executor已经启动,executor里自带线程池,用来接受提交的任务。
job 启动
示例:
val dataRdd = sc.parallelize(data, 3)
dataRdd.count()
复制代码
dataframe在RDD上封装一层,直接从dataframe作为入口查看任务执行,太复杂了
复制代码
count函数实现为:
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
复制代码
即通过sparkContext提交任务,将每个partition计算的Utils.getIteratorSize( job 的运行结果Array[T])进行求和返回。 主题核心为runJob方法
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int]): Array[U] = {
// job 返回的result 为 Array[U] 大小为partition个数
val results = new Array[U](partitions.size)
// runJob 参数: 数据源rdd, partition, resultHandler:此处为将各个子结果放入result数组中
runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res)
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
// Clean a closure to make it ready to serialized and send to tasks (removes unreferenced variables in $outer's, updates REPL variables)
val cleanedFunc = clean(func)
logInfo("Starting job: " + callSite.shortForm)
if (conf.getBoolean("spark.logLineage", false)) {
logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
}
//向dag调度器提交该job,
// 数据源rdd, 生成的闭包(任务执行需要的fun以及需要access 的field),partiton, driver code 中该方法的触发调用地址, resultHandler函数, properties
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
rdd.doCheckpoint()
}
复制代码
DAGScheduler.scala
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
//提交job, 生成job waiter(相当与futer), 这个方法向eventProcessLoop里提交任务信息,由DAGSchedulerEventProcessLoop来负责处理。DAGSchedulerEventProcessLoop实际即为生产-消费,blockingQueue存放各种DAGSchedulerEvent,另一个线程负责持续消费,最后都是交由dagScheduler来对event进行各种异步处理。
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
//job 要经过各个executor执行,最后当所有 task都完成,对job waiter进行唤醒, driver的程序唤醒后对Array[T]进行处理,获得最终结果。
ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
waiter.completionFuture.value.get match {
case scala.util.Success(_) =>
logInfo("Job %d finished: %s, took %f s".format
(waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
case scala.util.Failure(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
}
}
复制代码
至此,Spark最外层的job执行流程走完,总结下即
- driver将job的提交生成一个JobSubmitted event, 等待job complete,接受partition上计算出来的结果
- 对结果进行汇总
要点:
- DAG eventloop 是driver进行分布式调度消息分发的核心,executor的所有事件(包括executor事件、Job事件、task事件)均投递至此,并由DAG进行相应的处理
- JobWaiter的阻塞和唤醒
- resultHandler partition结果汇总至此
通过代码可以看出,任务执行的核心包括任务提交的处理,分布式下任务状态的维护等等,核心逻辑均在于DAGScheduler如何处理其相对应的事件。
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)
......
复制代码